import { z } from 'zod'; type Fields = Record; /** A running step. Exactly one of `ok` / `fail` ends it. */ interface Step { /** Trailing text on the finished line — `build 1786437647 (…)`. */ detail(text: string): void; /** Fields for this step's machine event only. */ data(fields: Fields): void; ok(): void; fail(): void; } /** One line of the header. `event` also emits it on the machine channel. */ interface HeaderRow { label: string; text: string; event?: string; data?: Fields; } interface RunStart { /** The machine name of the command: `install`, `link`, `bundles rm`. */ command: string; /** Segments of the identity line — `Acme Wave · io.acme.app.dev.wave · Debug`. */ title: string[]; data?: Fields; rows?: HeaderRow[]; /** * Step labels this run will use, where they are known up front — device names, * mostly. The label column is sized to them once, so one long name cannot make * every other row's duration sit somewhere else. */ labels?: string[]; } interface Reporter { /** * The header, and the clock the closing total is measured from. Called once, * before the first slow step. */ start(run: RunStart): void; /** A human aside. Never reaches the machine channel. */ note(text: string): void; warn(text: string): void; begin(step: string, label: string): Step; /** `begin`, run, end — for a step that either returns or throws. */ run(step: string, label: string, body: (handle: Step) => Promise): Promise; /** One machine event. A no-op without `--json`. */ emit(event: string, data?: Fields): void; /** * A stdout payload with a shape of its own — the `bundles ls` table, and its * `--json` array. Not an event, and never wrapped in one. */ write(text: string): void; error(message: string): void; done(ok?: boolean, data?: Fields): void; readonly json: boolean; } interface ReporterOptions { /** The machine channel. Written verbatim, newlines included. */ out(text: string): void; /** The human channel. Written raw, so the reporter owns every newline. */ err(text: string): void; /** * Monotonic milliseconds. Injected rather than read from `Date.now`, which * both jumps under NTP and cannot be controlled by a test. */ elapsedMs(): number; /** * Whether a line can be rewritten in place. This is stderr's TTY-ness, not * stdout's: the escapes land on stderr, so `jetbridge install 2> build.log` * is exactly the case that must degrade to one plain line per step. */ tty: boolean; json: boolean; } /** `0.3s` · `18s` · `1m 12s` · `2m`. */ declare function formatDuration(ms: number): string; declare function createReporter(options: ReporterOptions): Reporter; /** The reporter a programmatic caller gets when it asks for nothing. */ declare const silentReporter: () => Reporter; interface RunOptions { cwd?: string; env?: Record; /** Stream the child's output to the terminal instead of capturing it. */ stdio?: 'pipe' | 'inherit'; /** Return a failed result instead of throwing. */ allowFail?: boolean; input?: string; timeoutMs?: number; } interface CommandResult { ok: boolean; /** null when the process was killed by a signal. */ code: number | null; stdout: string; stderr: string; } /** A command as it was invoked — what tests assert against. */ interface CommandCall { command: string; args: string[]; options: RunOptions; } interface HttpResponse { status: number; ok: boolean; headers: Record; text: () => Promise; json: () => Promise; } interface HttpRequest { method: string; url: string; headers: Record; body?: string; /** Overrides the default. A request that never answers is a hang, so there is always one. */ timeoutMs?: number; } interface FileSystem { exists(path: string): boolean; read(path: string): string; readBytes(path: string): Uint8Array; write(path: string, contents: string | Uint8Array): void; mkdirp(path: string): void; readdir(path: string): string[]; rm(path: string, opts?: { recursive?: boolean; }): void; isDirectory(path: string): boolean; size(path: string): number; } /** * Plain messages from deep inside a step — one line per profile provisioned, * per file patched. Every one of them goes through the reporter, which indents * them under the step that produced them; written straight to stderr they would * land ON the step's own half-drawn line. */ interface Logger { info(message: string): void; warn(message: string): void; error(message: string): void; step(message: string): void; } interface Boundary { exec: { run(command: string, args: string[], options?: RunOptions): Promise; runSync(command: string, args: string[], options?: RunOptions): CommandResult; }; fs: FileSystem; http(request: HttpRequest): Promise; env(name: string): string | undefined; /** Unix seconds. Injected because build numbers are derived from it. */ now(): number; /** * Monotonic milliseconds since this process started — every duration the tool * reports is a difference of two readings. Separate from `now` on purpose: a * wall clock is what an NTP correction moves, and a build that appears to take * minus four seconds is worse than no timing at all. */ elapsedMs(): number; homedir(): string; cwd(): string; log: Logger; /** Progress, timing and machine events. See `reporter.ts` for the two channels. */ report: Reporter; /** False behind a pipe or in CI: never prompt, never expect a terminal. */ interactive: boolean; } declare const nodeFileSystem: FileSystem; /** * Is there a person to ask? * * A TTY that is not CI, and that nobody has explicitly opted out of. All three * halves matter: `CI=1` on a developer's machine is the documented way to force * the non-interactive path, and `JETBRIDGE_NON_INTERACTIVE` is the same lever * for someone who is not in CI and does not want to be prompted. * * Nothing REQUIRES any of them. No terminal is sufficient on its own — an agent * behind a pipe gets the unattended behaviour without setting a variable, which * is the whole point. */ declare function isInteractive(env?: NodeJS.ProcessEnv): boolean; /** * The real reporter: human lines on stderr, machine events on stdout, and line * rewriting only where stderr is a terminal that can redraw them. */ declare function nodeReporter(options?: { json?: boolean; }): Reporter; interface NodeBoundaryOptions { /** * Force the unattended path even on a terminal — what `--non-interactive` * hands down. Never needed behind a pipe, which is already unattended. */ nonInteractive?: boolean; /** The CLI builds one before parsing, so a failure has somewhere to go. */ reporter?: Reporter; } declare function nodeBoundary(options?: NodeBoundaryOptions): Boundary; /** * `jetbridge.config.json` — one schema, three jobs: the TypeScript types, the * runtime validator, and the published JSON Schema editors autocomplete * against. Defining it three times is how they drift. * * Every value here was a hardcoded constant in the tool this package was * extracted from. Nothing project-specific is allowed back in as a default. * * A field that merely REPEATS the Xcode project does not belong here — it goes * stale, and a stale copy refuses a build that works or patches a line that * moved. Those are read from the project instead (see `xcodeproject.ts`). * * `project.teamId`, `project.baseBundleId` and `variant.displayName` look like * exactly that and are NOT. They are the DEV build's identity, which is * deliberately different from what the project declares for a release build: a * separate bundle id so both can be installed at once, its own home-screen * label, and sometimes another development team. Deriving them from the * project would produce a dev build indistinguishable from the shipping one, * which is the problem this tool exists to solve. Do not delete them. */ declare const CONFIG_FILENAMES: readonly ["jetbridge.config.json", ".jetbridge.json"]; declare const CONFIG_VERSION = 1; /** * An Xcode target that gets its own bundle id — the app, plus any extension * that ships inside it. An extension's id is the app's id plus its own segment, * which is why a variant has to register more than one. */ declare const targetSchema: z.ZodObject<{ name: z.ZodString; variantEntitlements: z.ZodOptional; }, z.core.$strip>; declare const configSchema: z.ZodObject<{ $schema: z.ZodOptional; configVersion: z.ZodLiteral<1>; project: z.ZodObject<{ iosDir: z.ZodDefault; xcodeproj: z.ZodOptional; xcworkspace: z.ZodOptional; scheme: z.ZodString; teamId: z.ZodString; baseBundleId: z.ZodString; xcodegen: z.ZodDefault; appIcon: z.ZodOptional; deviceTypes: z.ZodDefault>>; }, z.core.$strip>; configurations: z.ZodPrefault; sim: z.ZodDefault; archive: z.ZodDefault; }, z.core.$strip>>; variant: z.ZodObject<{ displayName: z.ZodString; labelPrefix: z.ZodOptional; labelBudget: z.ZodDefault; noiseWords: z.ZodOptional>; targets: z.ZodDefault; }, z.core.$strip>>>; }, z.core.$strip>; hooks: z.ZodPrefault; postBuild: z.ZodOptional; }, z.core.$strip>>; credentials: z.ZodPrefault; ascIssuerId: z.ZodDefault; ascKeyPath: z.ZodDefault; }, z.core.$strip>>; ota: z.ZodOptional; endpoint: z.ZodOptional; publicBaseUrl: z.ZodString; prefix: z.ZodDefault; accessKeyId: z.ZodDefault; secretAccessKey: z.ZodDefault; }, z.core.$strip>; exportMethod: z.ZodDefault>; maxBuilds: z.ZodDefault; access: z.ZodDefault>; branding: z.ZodPrefault; accent: z.ZodOptional; }, z.core.$strip>>; }, z.core.$strip>>; git: z.ZodPrefault; remote: z.ZodDefault; requireCleanDefaultBranch: z.ZodDefault; }, z.core.$strip>>; }, z.core.$strip>; type Config = z.infer; type OtaConfig = NonNullable; type TargetConfig = z.infer; /** * The noise words actually used: whatever was configured, or the default — * commit types plus the segments of the base bundle id. */ declare function noiseWordsFor(config: Config): Set; /** `Acme Dev` → `Acme`, unless a prefix was set explicitly. */ declare function labelPrefixFor(config: Config): string; interface ConfigError { path: string; message: string; } type ParseResult = { ok: true; config: Config; } | { ok: false; errors: ConfigError[]; }; /** Validate an already-parsed object. Separated so the wizard can check its own draft. */ declare function parseConfig(value: unknown): ParseResult; declare function formatConfigErrors(errors: ConfigError[]): string; /** The published JSON Schema, generated from the same definition the runtime uses. */ declare function jsonSchema(): unknown; interface Project { /** Directory holding the config file. Everything relative resolves from here. */ root: string; configPath: string; config: Config; /** Absolute path to a project-relative one. */ path(...segments: string[]): string; /** Absolute path inside the Xcode project directory. */ iosPath(...segments: string[]): string; /** * The `.xcodeproj` name, resolved. Undefined only for a workspace build, or * for a generated project that XcodeGen has not produced yet — and it is * re-resolved on each miss, so it finds the file the moment generation makes * it. Never cache the answer across a generate step. */ xcodeprojName(): string | undefined; /** The `-project` or `-workspace` argument pair for xcodebuild. */ xcodebuildTarget(): [flag: string, path: string]; } /** The nearest config file at or above `from`, or null. */ declare function findConfigFile(boundary: Boundary, from: string): string | null; declare function projectFrom(configPath: string, config: Config, boundary?: Boundary): Project; interface LoadOptions { /** Start the upward walk here instead of the working directory. */ from?: string; /** Use exactly this file, e.g. from `--config-file`. */ configPath?: string; } declare function loadProject(boundary: Boundary, options?: LoadOptions): Project; /** * Naming a variant build. * * A variant gets its own bundle id and its own home-screen label, so two * branches install side by side as two distinguishable apps instead of * overwriting each other. Every name below comes from ONE parse of the raw * slug: derive the bundle-id segment and the label separately and they drift * (`feat/seeded-feed` became bundle `featseededfeed` but label "Seeded Feed"). */ /** * The words a variant is named from. * * Leading noise words are dropped, repeatedly: a conventional-commit type, and * the segments of the base bundle id someone might paste back in. So * `feat/seeded-feed` and `seeded-feed` name the same app, and * `com.example.dev.testtestes` yields `testtestes` rather than the absurd * `com.example.app.dev.comexampledevtesttestes`. * * Guarded on `length > 1` at each step, so an explicit `--suffix fix` still * names the variant "fix". */ declare function variantWords(raw: string, noiseWords: Set): string[]; /** Bundle-id segment and DNS label: lowercase alphanumerics only. */ declare function sanitizeSuffix(raw: string, config: Config): string; /** * Human words for a variant, from the RAW slug — before `sanitizeSuffix` * collapses the separators a bundle id cannot carry. `seeded-feed` → `Seeded * Feed`; derive it from the collapsed `seededfeed` instead and you get * `Seededfeed`. */ declare function variantTitle(raw: string, config: Config): string; /** Interpolated into plist XML and a build setting — keep it boring. */ declare function sanitizeName(raw: string): string; /** * The name App Store Connect accepts for a bundle-id entity. * * Not the same string as the home-screen label, though it is derived from it. * iOS is happy with `CFBundleDisplayName = "Acme_Hello_World"`; the ASC API * rejects it outright: * * 409 ENTITY_ERROR.ATTRIBUTE.INVALID * The attribute 'name' is invalid: 'Acme_Hello_World' * * Apple allows letters, digits and spaces there — not the `._-` a display name * permits. Punctuation becomes a space rather than vanishing, so * `Acme_Hello_World` reads `Acme Hello World`, not `AcmeHelloWorld`. */ declare function ascEntityName(displayName: string): string; declare function bundleIdFor(suffix: string, config: Config): string; /** * The home-screen label. An explicit name always wins; otherwise a variant gets * ` `, so two branches are never two identical icons. A plain * build keeps the project's own default. */ declare function displayNameFor(rawSuffix: string, explicit: string, config: Config): string; /** True when iOS will visibly truncate this label on the home screen. */ declare function exceedsLabelBudget(label: string, config: Config): boolean; /** * A slug is ONE bundle-id segment. A pasted bundle id is rejected rather than * quietly flattened: `--suffix com.example.dev.foo` used to build * `com.example.app.dev.comexampledevfoo`. */ declare function assertSingleSegment(value: string, config: Config): string; export { nodeBoundary as A, type Boundary as B, type Config as C, nodeFileSystem as D, nodeReporter as E, type FileSystem as F, noiseWordsFor as G, type HeaderRow as H, parseConfig as I, projectFrom as J, sanitizeName as K, type LoadOptions as L, sanitizeSuffix as M, silentReporter as N, type OtaConfig as O, type Project as P, variantTitle as Q, type Reporter as R, type Step as S, type TargetConfig as T, variantWords as U, assertSingleSegment as a, CONFIG_FILENAMES as b, CONFIG_VERSION as c, type CommandCall as d, type CommandResult as e, type ConfigError as f, type HttpRequest as g, type HttpResponse as h, type Logger as i, type ParseResult as j, type ReporterOptions as k, type RunOptions as l, type RunStart as m, ascEntityName as n, bundleIdFor as o, configSchema as p, createReporter as q, displayNameFor as r, exceedsLabelBudget as s, findConfigFile as t, formatConfigErrors as u, formatDuration as v, isInteractive as w, jsonSchema as x, labelPrefixFor as y, loadProject as z };