import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { Awaitable, BooleanFlagOptions, CacheMode, CacheSource, CleanField, Cleaned, Merge, PhaseOptions, RunResult, ScriptOptions, SlotValue, StepDef, StringFlagOptions, UnknownFlags, UnknownSlots, InheritedKeyStepDef } from "./types.js"; /** * Keys for the phantom markers on {@link Script} and {@link Routine}. Exported * only because declaration emit has to name them; there is no runtime value * behind either and nothing should import them. */ export declare const REQUIRES: unique symbol; export declare const PRODUCES: unique symbol; /** * Phases and steps recorded away from any particular script, ready to be * spliced into one with {@link Script.use}. * * All four parameters are inferred — `In` and `Ctx` from the `routineFor()` call that declared the routine, `Out` and `Reserved` from the steps * it went on to add. */ export interface Routine { /** * Phantom. `In` and `Ctx` sit in contravariant position here, which is what * makes a mount site prove it already offers at least this much input and * context. Erased at compile time. */ readonly [REQUIRES]: (context: { input: In; ctx: Ctx; }) => void; /** Phantom. Carries what the routine contributes so `use` can infer it. */ readonly [PRODUCES]: [Out, Slots]; readonly name: string; } /** * A mounted fragment's slots, as the host addresses them. `as` renames the * phases, so it has to rename their slots too — otherwise every * `cache.clear("Fetch")` written against an unprefixed mount would keep * compiling while silently addressing nothing. */ export type Mounted = [As] extends [string] ? { [K in keyof Slots as `${As} / ${K & string}`]: Slots[K]; } : Slots; export interface MountOptions { /** * Prefix for the routine's phase names — `"Amazon"` turns `Fetch` into * `Amazon / Fetch`. Required whenever the same routine is mounted twice, * since phase names have to stay unique. */ as?: string; } /** * What a mount feeds its fragment instead of the host's own input — either a * fixed value or a function of the host's input, context, and flags, resolved * once when the mount is reached. This is the native way to project a flag * onto a routine's input: `use(routine, { input: ({ flags }) => ({ env: flags.environment }) })`. */ export type MountInput = SubIn | ((context: { input: In; ctx: Ctx; flags: Flags; }) => Awaitable); /** * The phase currently being built, carried so its slot can be committed once * the phase closes — which is the first moment its delta is fully known. * * `name` is `never` for a phase that declared no cache, so committing it adds * nothing — see {@link Commit}. That is also the starting state, which is why * no separate "is anything open" flag is needed. */ export interface OpenPhase { /** The phase's cache slot, or `never` when the phase declared no cache. */ name: string | never; /** * The phase's name, cached or not — a step's slot is `phase::step`, so a * cached step inside an uncached phase still needs it. */ phase: string; /** The `schema`'s output type, or `unknown` when none was declared. */ schema: unknown; /** What this phase's steps have contributed so far. */ delta: object; } /** No phase open yet: steps land in the implicit "Main" phase. */ export type ClosedPhase = { name: never; phase: "Main"; schema: unknown; delta: {}; }; /** * Fold the open phase's slot into the map. A no-op for uncached phases. * * The `never` case is written out rather than left to `Record` * resolving to `{}`: `Record` is a mapped type, and — for the reason * {@link Merge} explains at length — a mapped type in an accumulated * intersection is a chain link the compiler has to walk on every later call, * while a plain intersection of leaves is free. Uncached phases are the * common case, and this keeps them from spending a script's depth budget. */ export type Commit = [Open["name"]] extends [never] ? Slots : Slots & Record>; /** Phase options with `cache` required, so the cached overload is unambiguous. */ export type CachedPhaseOptions = Omit, "cache"> & { cache: CacheSource; }; export interface RunOptions { /** Cancel the run from the outside. */ signal?: AbortSignal; /** * What this run may do with the caches the script declares. Default `"on"`. * Wire it to a flag — `cache: argv.includes("--no-cache") ? "off" : "on"` — * to control caching without touching the script. */ cache?: CacheMode; /** * Argv to parse `defineFlag`'s flags out of. Default `process.argv.slice(2)` * — override it in a test, or when this script is one subcommand of a * larger CLI that has already sliced its own argv apart. */ argv?: readonly string[]; } /** * A saga-style script: phases of steps, each step optionally compensating when * a later step fails. * * The second type parameter is inferred, never written by hand — every * `addStep` whose handler returns an object widens it, so later handlers see * everything earlier ones produced. * * ```ts * const result = await new Script<{ userId: string }>("provision") * .addPhase("Validation") * .addStep({ * name: "load user", * handler: async ({ input }) => ({ user: await db.user(input.userId) }), * }) * .addPhase("Provision") * .addStep({ * name: "create tenant", * handler: async ({ ctx }) => ({ tenantId: await api.create(ctx.user.org) }), * rollback: async ({ output }) => api.destroy(output.tenantId), * }) * .run({ userId: "u_1" }); * ``` * * The third parameter is inferred too: it collects the keys steps reserved * through `rollbackKeys`, which is how `clean` knows what it may not remove. */ export declare class Script { /** * Phantom, erased at compile time. `run` is a *method*, so its parameter * compares bivariantly and `In` would otherwise carry no safety across two * `Script` types — a script needing `{ a, b }` would mount into a host * offering only `{ a }`. A property-position function makes `In` * contravariant, which is what `use()` relies on. */ readonly [REQUIRES]: (input: In) => void; private readonly options; private readonly definition; private readonly reserved; private readonly phaseNames; /** False right after `use()`, so a bare `addStep` cannot land in a fragment. */ private openPhase; private _schema; private readonly flagDefs; private readonly flagNames; private readonly flagLongs; private readonly flagShorts; constructor(options?: ScriptOptions | string); /** * Provide a [Standard Schema](https://standardschema.dev) describing the run * input instead of writing `In` by hand — any compliant library works (Zod, * Valibot, ArkType, Effect Schema, ...). `In` is inferred from the schema's * output type, and the value passed to `run()` is validated against it * before any phase executes, throwing {@link SchemaValidationError} on * failure. * * Call this first, right after construction — every `addStep`/`addPhase` * called before it still sees the old `In`. * * ```ts * const deploy = new Script({ name: "deploy" }) * .defineInput(z.object({ service: z.string() })) * .addStep({ * name: "resolve commit", * handler: async ({ input }) => ({ sha: await git.head(input.service) }), * }); * ``` */ defineInput(schema: StandardSchemaV1): Script; /** Validate `input` against the schema from `defineInput`, if any. */ private validateInput; /** * Declare a flag this script reads off the command line. `name` is both the * property `context.flags` exposes it under and — kebab-cased — the default * long `--` form; `short` adds a single-letter `-` alias, and `long` * overrides the derived one when it should read differently from `name`. * * A `boolean: true` flag is present or absent (`myScript --force`) and * always resolves to a `boolean`, defaulting to `false` unless * `default: true`. Anything else is a string: `myScript --env prod` or * `myScript -e prod`, resolving to `string` when a `default` is given, or * `string | undefined` otherwise. Values are parsed from `process.argv` * (or `run(input, { argv })`) once, before any phase executes. * * ```ts * new Script({ name: "deploy" }) * .defineFlag({ name: "environment", long: "env", short: "e", default: "staging" }) * .defineFlag({ name: "force", short: "f", boolean: true }) * .addStep({ * name: "deploy", * // flags.environment: string, flags.force: boolean * handler: async ({ flags }) => { ... }, * }); * ``` * * @throws {DuplicateNameError} if `name`, the resolved long flag, or `short` * is already used by another flag on this script. */ defineFlag(options: BooleanFlagOptions): Script>; defineFlag(options: StringFlagOptions): Script>; /** * Flag identities have to be unique the same way phase and step names do — * `name` is the property key on `context.flags`, and the long/short forms * are what argv is matched against. */ private claimFlagIdentity; /** Parse `argv` against this script's declared flags — `{}` when none are. */ private resolveFlags; /** * Open a cached phase. Its name becomes a slot addressable through * `context.cache` in every step declared after it — typed as the phase's own * delta, or as the `schema`'s output when one is given. */ addPhase(name: Name, options: CachedPhaseOptions): Script, { name: Name; phase: Name; schema: Value; delta: {}; }, Flags>; /** Open a new phase. Subsequent `addStep` calls land in it. */ addPhase(name: Name, options?: PhaseOptions): Script, { name: never; phase: Name; schema: unknown; delta: {}; }, Flags>; /** Open a phase and populate it inside a callback, keeping the type flow. */ addPhase(name: string, build: (script: Script, ClosedPhase, Flags>) => Script): Script; addPhase(name: string, options: PhaseOptions, build: (script: Script, ClosedPhase, Flags>) => Script): Script; /** * Append a step to the current phase. Whatever the handler resolves to is * merged into the context and becomes visible to every later step; whatever * it lists in `clean` is dropped from both. * * There are four signatures, along two independent splits. * * A step that caches contributes its own slot, `phase::step`, holding * exactly what the handler returns — addressable through `context.cache` in * every step declared after it. That is split from the plain form rather * than inferred from the presence of `cache`: an optional property cannot be * told apart from its own default, so a single signature always resolved to * "no cache" and quietly dropped the slot. * * Crossed with that, each form is tried first as an * {@link InheritedKeyStepDef} — `rollbackKeys` naming only keys the context * already has — which is what keeps the handler as the sole inference site * for `Out`. A step naming one of its own output keys fails that signature's * `RollbackKeys` constraint in inference's first pass and falls through to * the general {@link StepDef} form below. * * @throws {StepDefinitionError} if `clean` names a key reserved by some * step's `rollbackKeys`. */ /** Cached, `rollbackKeys` inherited. See {@link InheritedKeyStepDef}. */ addStep(def: Omit, "name"> & CleanField & { name: Name; cache: CacheSource; }): Script, CleanKeys[number]>, Reserved | RollbackKeys[number], Slots & Record<`${Open["phase"]}::${Name}`, Out>, { name: Open["name"]; phase: Open["phase"]; schema: Open["schema"]; delta: Cleaned, CleanKeys[number]>; }, Flags>; /** Plain, `rollbackKeys` inherited. See {@link InheritedKeyStepDef}. */ addStep(def: InheritedKeyStepDef & CleanField): Script, CleanKeys[number]>, Reserved | RollbackKeys[number], Slots, { name: Open["name"]; phase: Open["phase"]; schema: Open["schema"]; delta: Cleaned, CleanKeys[number]>; }, Flags>; /** Cached, general form. See {@link StepDef}. */ addStep(def: Omit, "name"> & CleanField & { name: Name; cache: CacheSource; }): Script, CleanKeys[number]>, Reserved | RollbackKeys[number], Slots & Record<`${Open["phase"]}::${Name}`, Out>, { name: Open["name"]; phase: Open["phase"]; schema: Open["schema"]; delta: Cleaned, CleanKeys[number]>; }, Flags>; /** Plain, general form. See {@link StepDef}. */ addStep(def: StepDef & CleanField): Script, CleanKeys[number]>, Reserved | RollbackKeys[number], Slots, { name: Open["name"]; phase: Open["phase"]; schema: Open["schema"]; delta: Cleaned, CleanKeys[number]>; }, Flags>; /** * Splice a reusable fragment into this script. Its phases land here in * order, and everything it produced is merged into the context — so the * steps that follow see it, typed, exactly as if they had been written * inline. * * The fragment states what it needs and TypeScript checks it at the mount * site: a routine declared `routineFor<{ name: string }, { hash: string }>()` * refuses to mount into a script whose input lacks `name`, or which has not * produced `hash` yet. * * ```ts * new Script({ name: "monthly report" }) * .use(fetchChannel) * .addPhase("Report") * .addStep({ name: "aggregate", handler: ({ ctx }) => summarise(ctx.orders) }); * ``` * * A plain {@link Script} mounts too, which is what keeps a reusable pipeline * runnable on its own. Its own {@link ScriptOptions} — rollback mode, * `logPlacement`, `silent` — are ignored in favour of the host's; only its * phases and steps come across. * * @throws {DuplicateNameError} if a phase it brings is already defined here. * Mount the same routine twice by naming each mount with `as`. */ use(routine: Routine, options: { as?: As; input: MountInput; }): Script, Reserved | R, Commit & Mounted, ClosedPhase, Flags>; use(script: Script, options: { as?: As; input: MountInput; }): Script, Reserved | R, Commit & Mounted>, ClosedPhase, Flags>; use(script: Script, options?: { as?: As; }): Script, Reserved | R, Commit & Mounted>, ClosedPhase, Flags>; use(routine: Routine, options?: { as?: As; }): Script, Reserved | R, Commit & Mounted, ClosedPhase, Flags>; /** Every step name, in execution order — handy for tests and docs. */ outline(): Array<{ phase: string; steps: string[]; }>; private currentPhase; /** * Phase names have to be unique: they are what the frame labels, what * `outline()` reports, and — since the cache landed — what identifies a * phase's stored entry. Two phases sharing a name share a cache slot, which * reads as wrong data rather than as an error. */ private claimPhaseName; run(input: In, runOptions?: RunOptions): Promise>; /** Retry + timeout wrapper around a single handler invocation. */ private executeStep; /** Compensate completed steps in reverse order. */ private unwind; /** * Wire up cancellation. The first interrupt stops the run and lets * compensation proceed; a second one abandons compensation too. */ private attachCancellation; } /** * Define a step in its own module while keeping full inference. Bind the input * and the context the step expects, then pass the definition to `addStep`. * * ```ts * export const verifyId = stepFor()({ * name: "verify id", * handler: async ({ ctx }) => ({ verified: ctx.user.id }), * }); * ``` * * `rollbackKeys` works the same here, and the keys it names stay reserved once * the step is handed to `addStep`. */ export declare function stepFor(): { (def: InheritedKeyStepDef): StepDef; (def: StepDef): StepDef; }; /** * Declare phases and steps away from any particular script, then mount them * with {@link Script.use}. * * The two parameters are the *minimum* the fragment needs — the input it * reads, and the context it expects to already exist — exactly as they are for * {@link stepFor}. Whatever it produces flows on into whichever script mounts * it, and any script that cannot satisfy the requirement is a compile error at * the `use()` call, not an `undefined` at run time. * * ```ts * export const fetchChannel = routineFor<{ channel: string }>()("fetch channel", (s) => * s * .addPhase("Fetch") * .addStep({ name: "authenticate", handler: … }) * .addStep({ name: "pull orders", handler: … }) * .addPhase("Normalize") * .addStep({ name: "dedupe", handler: … }), * ); * ``` * * A routine owns whole phases, so phase-level options — `when`, `cache` — * travel with it. */ export declare function routineFor(): (name: string, build: (script: Script) => Script) => Routine>; /** Convenience factory so callers can skip `new`. */ export declare function script(options?: ScriptOptions | string): Script; //# sourceMappingURL=script.d.ts.map