@grida/fx 0.0.0-stage → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # @grida/fx
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Grida FX for JavaScript and TypeScript: the `grida-fx` command, and an SDK to write workflow
4
+ files and to plan, price and run them. Grida FX is a workflow engine for generative asset
5
+ pipelines: write a workflow file, and FX plans it, prices it before anything is spent, caches
6
+ every paid call by its request, and records the run. It works with your own provider keys.
7
+
8
+ This is a **preview** (`0.x` pre-releases under the npm tag `next`).
9
+
10
+ ```sh
11
+ npm install @grida/fx@next # or: npm install -g @grida/fx@next
12
+ npx grida-fx --help
13
+ ```
14
+
15
+ The engine is a native binary. npm installs it with this package, as an optional dependency for
16
+ your machine:
17
+
18
+ | Platform | Engine package |
19
+ |---|---|
20
+ | macOS on Apple silicon | `@grida/fx-darwin-arm64` |
21
+ | macOS on Intel | `@grida/fx-darwin-x64` |
22
+ | Linux on x64, glibc 2.28 or later | `@grida/fx-linux-x64-gnu` |
23
+ | Linux on arm64, glibc 2.28 or later | `@grida/fx-linux-arm64-gnu` |
24
+
25
+ **Windows is not in the preview**: the engine's runner uses Unix process groups and signals. Use
26
+ WSL 2, which runs the Linux packages. Linux with musl (Alpine) is not supported either. Node 18 or
27
+ later.
28
+
29
+ `grida-fx` reports what is missing when it finds no engine. If you install with
30
+ `--omit=optional`, or need a binary of your own, set `GRIDA_FX_BIN` to the path of a `grida-fx`
31
+ binary: the command and the SDK use it first.
32
+
33
+ Some steps run on Python in the preview (your own Python nodes, and the built-in local types such
34
+ as `image.resize`): those projects also need `pip install --pre grida`, or `GRIDA_FX_PYTHON`
35
+ pointing at a Python that has them. A project whose steps are all paid built-ins and `select`
36
+ needs only this package. See [Getting started](https://github.com/gridaco/fx/blob/main/docs/guide/01-getting-started.md).
37
+
38
+ ## The SDK
39
+
40
+ Every function drives the engine and reads the JSON it prints. Planning never spends; a run
41
+ spends only with `live: true`, within a ceiling.
42
+
43
+ ```ts
44
+ import { plan, run } from "@grida/fx";
45
+
46
+ const planned = await plan("workflows/icon.yaml", { inputs: { name: "copper lantern" } });
47
+ console.log(planned.ok, planned.estimate); // { lowUsd, highUsd, ceilingUsd }
48
+
49
+ const result = await run(planned, { live: true }); // the plan's own target, inputs and ceiling
50
+ console.log(result.ok, result.chargedUsd, result.failed);
51
+ const icon = result.outputs.icon; // a RunFile: digest, kind, path, read()
52
+ ```
53
+
54
+ - `plan(target, options)`: the expanded graph (`plan.graph`, fx-graph-v1) and its price
55
+ (`plan.price`), with `ok`, `problems`, `estimate` and `phases`.
56
+ - `expand`, `identity`, `price`: the documents of `grida-fx expand`, `identity` and `price`.
57
+ - `run(target | plan, options)`: plans first (a plan with problems rejects with `PlanRefused`),
58
+ runs, and reads the run back: `ok`, `incomplete`, `chargedUsd`, `failed`, `outputs`, `steps`.
59
+ A failed step does not reject.
60
+ - `project(runDir)` and `inspect(runOrWorkflowId, { verify })`: a run's record and summary.
61
+ - `binary()` and `engineVersion()`: the engine in use.
62
+
63
+ A target is a workflow file, a workflow id, or a builder `file.py:function`. Options:
64
+ `inputs` (values), `inputFiles`, `routes`, `args` (a builder's arguments), `maxUsd`, `cwd`,
65
+ `env` (for example `GRIDA_FX_PYTHON`) and `signal` (an `AbortSignal`); `run` also takes `live`,
66
+ `yesUpTo`, `runDir` and `deliver`. Relative paths start at `cwd`. The engine's errors reject with
67
+ `FxError`, which carries its `exitCode` and `stderr`.
68
+
69
+ ## Writing workflows
70
+
71
+ ```ts
72
+ import { writeFile } from "node:fs/promises";
73
+ import { expr, toYaml, workflow } from "@grida/fx";
74
+
75
+ const icon = workflow({
76
+ id: "icon",
77
+ title: "One item icon",
78
+ inputs: { name: { type: "string", description: "What the item is" } },
79
+ steps: {
80
+ draw: {
81
+ uses: "fx/image.generate@1",
82
+ with: { prompt: `A single ${expr("inputs.name")} game icon`, background: "transparent" },
83
+ },
84
+ },
85
+ outputs: { icon: expr("steps.draw.outputs.image") },
86
+ });
87
+ await writeFile("workflows/icon.yaml", toYaml(icon));
88
+ ```
89
+
90
+ `toYaml` writes the strict YAML subset the engine reads, as the engine writes it: every string
91
+ that would read back as something else (`on`, `017`, `2026-10-05`, a digest) is quoted.
92
+
93
+ The [guide](https://github.com/gridaco/fx/tree/main/docs/guide) covers workflow files, nodes,
94
+ cost and cache, and running.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // The grida-fx command: runs the engine for this machine (src/cli.ts).
3
+ import { main } from "../dist/cli.js";
4
+
5
+ main(process.argv.slice(2));
package/dist/api.d.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Planning and running from JavaScript, by driving the `grida-fx` binary: the engine is the only
3
+ * place with engine logic, and these functions read the documents it prints.
4
+ *
5
+ * - A target is what the command takes: a workflow file, a workflow id, or a builder
6
+ * `file.py:function` (with `args`). `run` also takes a {@link Plan}: it runs it with the target
7
+ * and options it was planned with.
8
+ * - Relative paths (`inputFiles`, `routes`, `runDir`, paths inside `inputs`) start at `cwd`, as on
9
+ * the command line.
10
+ * - Planning never spends. A run spends only with `live: true`, within a ceiling.
11
+ * - The engine's errors (exit status 2), a run it refuses to start and an interrupted run reject
12
+ * with {@link FxError}; a plan with problems that is asked to run rejects with
13
+ * {@link PlanRefused}. A failed step does not reject: read `result.ok` and `result.failed`.
14
+ */
15
+ import type { Encoded, FileRef, Graph, Identities, Inspection, Price, PricePhase, Problem, ProjectedInstance, Projection } from "./documents.js";
16
+ import { type CallOptions, type Exit } from "./engine.js";
17
+ import { FxError } from "./errors.js";
18
+ import { type PlanOptions, type PlanRequest, type RunOptions } from "./request.js";
19
+ import type { JsonValue } from "./workflow.js";
20
+ /** A plan the engine made: the expanded graph and its price. */
21
+ export declare class Plan {
22
+ /** The expanded graph (fx-graph-v1), as `grida-fx plan --json` prints it. */
23
+ readonly graph: Graph;
24
+ /** The price by phase, as `grida-fx price` prints it. */
25
+ readonly price: Price;
26
+ /** The target and options it was planned with; `run(plan)` uses them. */
27
+ readonly request: PlanRequest;
28
+ constructor(graph: Graph, price: Price, request: PlanRequest);
29
+ /** Whether the plan has no problems, so it may run. */
30
+ get ok(): boolean;
31
+ /** Why the plan is refused; empty when it is not. */
32
+ get problems(): Problem[];
33
+ /** What the run may spend on what is not cached, and its ceiling, in US dollars. */
34
+ get estimate(): {
35
+ lowUsd: number;
36
+ highUsd: number;
37
+ ceilingUsd: number | null;
38
+ };
39
+ /** The price by phase. */
40
+ get phases(): PricePhase[];
41
+ /** The workflow's id. */
42
+ get workflowId(): string;
43
+ }
44
+ /** A plan with problems was asked to run: `the plan is refused:` and one line per problem. */
45
+ export declare class PlanRefused extends FxError {
46
+ /** The refused plan. */
47
+ readonly plan: Plan;
48
+ constructor(plan: Plan);
49
+ }
50
+ /** `grida-fx expand`: the expanded graph (fx-graph-v1), with `problems`. Never spends. */
51
+ export declare function expand(target: string, options?: PlanOptions): Promise<Graph>;
52
+ /** `grida-fx identity`: each live instance's identity (null while it waits on a result). */
53
+ export declare function identity(target: string, options?: PlanOptions): Promise<Identities>;
54
+ /** `grida-fx price`: the plan's price by phase. Never spends. */
55
+ export declare function price(target: string, options?: PlanOptions): Promise<Price>;
56
+ /** Expands, checks and prices `target` (`grida-fx plan --json` and `price`). Never spends;
57
+ * `plan.problems` says what is wrong. */
58
+ export declare function plan(target: string, options?: PlanOptions): Promise<Plan>;
59
+ /** A file a run made, by digest. */
60
+ export declare class RunFile {
61
+ readonly digest: string;
62
+ /** The file kind (`image/png`, `json`, …). */
63
+ readonly kind: string;
64
+ readonly name: string;
65
+ readonly size: number;
66
+ /** A collection item's key, if it has one. */
67
+ readonly key: string | null;
68
+ /** Where the run placed it (an absolute path in the run folder), or null when no step of this
69
+ * run placed it (a workflow output that is one of its input files). Read it; never write it:
70
+ * it shares its bytes with the cache. */
71
+ readonly path: string | null;
72
+ constructor(ref: FileRef, path: string | null);
73
+ /** The file's bytes. */
74
+ read(): Promise<Buffer>;
75
+ }
76
+ /** A value of a run: a file, a keyed collection (in its order), a list, a plain value, or null. */
77
+ export type RunValue = RunFile | Map<string, RunValue> | RunValue[] | JsonValue;
78
+ /** A finished `run`: read from the engine's `project` and `inspect --json` of its folder. */
79
+ export declare class RunResult {
80
+ #private;
81
+ /** The run folder, absolute. */
82
+ readonly runDir: string;
83
+ /** The engine's exit status: 0 when the run is ok (and every `deliver` found its output). */
84
+ readonly exitCode: number;
85
+ /** What `grida-fx run` printed: the plan and the run's summary. */
86
+ readonly stdout: string;
87
+ /** The run's record, projected (`grida-fx project`). */
88
+ readonly projection: Projection;
89
+ /** The run's summary (`grida-fx inspect --json`). */
90
+ readonly inspection: Inspection;
91
+ constructor(runDir: string, exit: Exit, projection: Projection, inspection: Inspection);
92
+ private get finished();
93
+ /** Whether every step that had to run succeeded and the run ended normally. */
94
+ get ok(): boolean;
95
+ /** Whether the run stopped before everything ran (running it again continues it). */
96
+ get incomplete(): boolean;
97
+ /** What the run folder has been charged, in US dollars, over every invocation. */
98
+ get chargedUsd(): number;
99
+ /** The ids of the instances that failed. */
100
+ get failed(): string[];
101
+ /** Why the run stopped early, if it did. */
102
+ get stopped(): string | null;
103
+ /** Each instance's state, by instance id. */
104
+ get steps(): Record<string, ProjectedInstance>;
105
+ /** Each declared output: a {@link RunFile}, a `Map` for a keyed collection, an array, a plain
106
+ * value, or null. */
107
+ get outputs(): Record<string, RunValue>;
108
+ }
109
+ /** A value of the run's record (fx-run-events-v1 `encoded`). */
110
+ export declare function decode(encoded: Encoded, placed: ReadonlyMap<string, string>): RunValue;
111
+ /**
112
+ * Runs `target`, or a plan, after planning it: a plan with problems rejects with
113
+ * {@link PlanRefused} before any folder exists. `live: true` admits paid calls, within a
114
+ * ceiling. Then reads the folder back through `grida-fx project` and `inspect --json`.
115
+ */
116
+ export declare function run(target: string | Plan, options?: RunOptions): Promise<RunResult>;
117
+ /** `grida-fx project <run>`: a run's record projected to its state. */
118
+ export declare function project(runDir: string, options?: CallOptions): Promise<Projection>;
119
+ /** Options of {@link inspect}. */
120
+ export interface InspectOptions extends CallOptions {
121
+ /** Re-check every placed file against its record (`--verify`). */
122
+ readonly verify?: boolean;
123
+ }
124
+ /** `grida-fx inspect <run> --json`: a run's summary. `run` is a run folder, or a workflow id for
125
+ * its newest run. With `verify`, `verification` says whether every placed file still matches. */
126
+ export declare function inspect(run: string, options?: InspectOptions): Promise<Inspection>;
127
+ /** The engine's version (`grida-fx --version`), e.g. `0.1.0-alpha.1`. */
128
+ export declare function engineVersion(options?: CallOptions): Promise<string>;
package/dist/api.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Planning and running from JavaScript, by driving the `grida-fx` binary: the engine is the only
3
+ * place with engine logic, and these functions read the documents it prints.
4
+ *
5
+ * - A target is what the command takes: a workflow file, a workflow id, or a builder
6
+ * `file.py:function` (with `args`). `run` also takes a {@link Plan}: it runs it with the target
7
+ * and options it was planned with.
8
+ * - Relative paths (`inputFiles`, `routes`, `runDir`, paths inside `inputs`) start at `cwd`, as on
9
+ * the command line.
10
+ * - Planning never spends. A run spends only with `live: true`, within a ceiling.
11
+ * - The engine's errors (exit status 2), a run it refuses to start and an interrupted run reject
12
+ * with {@link FxError}; a plan with problems that is asked to run rejects with
13
+ * {@link PlanRefused}. A failed step does not reject: read `result.ok` and `result.failed`.
14
+ */
15
+ import { readFile } from "node:fs/promises";
16
+ import { resolve } from "node:path";
17
+ import { call, document, failure, INTERRUPTED } from "./engine.js";
18
+ import { FxError } from "./errors.js";
19
+ import { makeRequest, planningArgs, runArgs, withInputsFile, } from "./request.js";
20
+ /** The first line of a run's summary: `run` padded to 10 columns, then the folder as named. */
21
+ const RUN_LINE = "run ";
22
+ /** How the engine says it would not start a run (on stdout, exit status 1). */
23
+ const REFUSED = "refused: ";
24
+ // ------------------------------------------------------------------------------------------------
25
+ // Plans
26
+ /** A plan the engine made: the expanded graph and its price. */
27
+ export class Plan {
28
+ /** The expanded graph (fx-graph-v1), as `grida-fx plan --json` prints it. */
29
+ graph;
30
+ /** The price by phase, as `grida-fx price` prints it. */
31
+ price;
32
+ /** The target and options it was planned with; `run(plan)` uses them. */
33
+ request;
34
+ constructor(graph, price, request) {
35
+ this.graph = graph;
36
+ this.price = price;
37
+ this.request = request;
38
+ }
39
+ /** Whether the plan has no problems, so it may run. */
40
+ get ok() {
41
+ return this.problems.length === 0;
42
+ }
43
+ /** Why the plan is refused; empty when it is not. */
44
+ get problems() {
45
+ return (this.graph.problems ?? []).map((problem) => ({
46
+ where: String(problem.where ?? ""),
47
+ message: String(problem.message ?? ""),
48
+ }));
49
+ }
50
+ /** What the run may spend on what is not cached, and its ceiling, in US dollars. */
51
+ get estimate() {
52
+ const estimate = this.graph.estimate;
53
+ return {
54
+ lowUsd: Number(this.price.estimate?.low_usd ?? estimate?.low_usd ?? 0),
55
+ highUsd: Number(this.price.estimate?.high_usd ?? estimate?.high_usd ?? 0),
56
+ ceilingUsd: this.price.ceiling_usd ?? estimate?.ceiling_usd ?? null,
57
+ };
58
+ }
59
+ /** The price by phase. */
60
+ get phases() {
61
+ return this.price.phases ?? [];
62
+ }
63
+ /** The workflow's id. */
64
+ get workflowId() {
65
+ return this.graph.workflow?.id ?? "";
66
+ }
67
+ }
68
+ /** A plan with problems was asked to run: `the plan is refused:` and one line per problem. */
69
+ export class PlanRefused extends FxError {
70
+ /** The refused plan. */
71
+ plan;
72
+ constructor(plan) {
73
+ const lines = plan.problems.map((problem) => `${problem.where}: ${problem.message}`);
74
+ super(["the plan is refused:", ...lines].join("\n"));
75
+ this.name = "PlanRefused";
76
+ this.plan = plan;
77
+ }
78
+ }
79
+ /** `grida-fx expand`: the expanded graph (fx-graph-v1), with `problems`. Never spends. */
80
+ export async function expand(target, options = {}) {
81
+ return planningDocument("expand", makeRequest(target, options), options.signal);
82
+ }
83
+ /** `grida-fx identity`: each live instance's identity (null while it waits on a result). */
84
+ export async function identity(target, options = {}) {
85
+ return planningDocument("identity", makeRequest(target, options), options.signal);
86
+ }
87
+ /** `grida-fx price`: the plan's price by phase. Never spends. */
88
+ export async function price(target, options = {}) {
89
+ return planningDocument("price", makeRequest(target, options), options.signal);
90
+ }
91
+ /** Expands, checks and prices `target` (`grida-fx plan --json` and `price`). Never spends;
92
+ * `plan.problems` says what is wrong. */
93
+ export async function plan(target, options = {}) {
94
+ return planRequest(makeRequest(target, options), options.signal);
95
+ }
96
+ async function planRequest(request, signal) {
97
+ const graph = await planningDocument("plan", request, signal, ["--json"]);
98
+ const priced = await planningDocument("price", request, signal);
99
+ return new Plan(graph, priced, request);
100
+ }
101
+ async function planningDocument(verb, request, signal, extra = []) {
102
+ return withInputsFile(request, (inputsFile) => document(verb, [...planningArgs(request, inputsFile), ...extra], callOptions(request, signal)));
103
+ }
104
+ function callOptions(request, signal) {
105
+ return {
106
+ cwd: request.cwd,
107
+ ...(request.env !== undefined ? { env: request.env } : {}),
108
+ ...(signal !== undefined ? { signal } : {}),
109
+ };
110
+ }
111
+ // ------------------------------------------------------------------------------------------------
112
+ // Runs
113
+ /** A file a run made, by digest. */
114
+ export class RunFile {
115
+ digest;
116
+ /** The file kind (`image/png`, `json`, …). */
117
+ kind;
118
+ name;
119
+ size;
120
+ /** A collection item's key, if it has one. */
121
+ key;
122
+ /** Where the run placed it (an absolute path in the run folder), or null when no step of this
123
+ * run placed it (a workflow output that is one of its input files). Read it; never write it:
124
+ * it shares its bytes with the cache. */
125
+ path;
126
+ constructor(ref, path) {
127
+ if (typeof ref?.digest !== "string" || !/^[0-9a-f]{64}$/.test(ref.digest)) {
128
+ throw new FxError(`the run's record names a file by ${JSON.stringify(ref?.digest)}, which is not a digest`);
129
+ }
130
+ this.digest = ref.digest;
131
+ this.kind = ref.kind ?? "file";
132
+ this.name = ref.name ?? "";
133
+ this.size = ref.size ?? 0;
134
+ this.key = ref.key ?? null;
135
+ this.path = path;
136
+ }
137
+ /** The file's bytes. */
138
+ async read() {
139
+ if (this.path === null) {
140
+ throw new FxError(`the run placed no copy of ${this.name || this.digest} in its folder`);
141
+ }
142
+ return readFile(this.path);
143
+ }
144
+ }
145
+ /** A finished `run`: read from the engine's `project` and `inspect --json` of its folder. */
146
+ export class RunResult {
147
+ /** The run folder, absolute. */
148
+ runDir;
149
+ /** The engine's exit status: 0 when the run is ok (and every `deliver` found its output). */
150
+ exitCode;
151
+ /** What `grida-fx run` printed: the plan and the run's summary. */
152
+ stdout;
153
+ /** The run's record, projected (`grida-fx project`). */
154
+ projection;
155
+ /** The run's summary (`grida-fx inspect --json`). */
156
+ inspection;
157
+ #outputs;
158
+ constructor(runDir, exit, projection, inspection) {
159
+ this.runDir = runDir;
160
+ this.exitCode = exit.status ?? -1;
161
+ this.stdout = exit.stdout;
162
+ this.projection = projection;
163
+ this.inspection = inspection;
164
+ }
165
+ get finished() {
166
+ return this.projection.run?.run_finished;
167
+ }
168
+ /** Whether every step that had to run succeeded and the run ended normally. */
169
+ get ok() {
170
+ return this.finished?.ok === true;
171
+ }
172
+ /** Whether the run stopped before everything ran (running it again continues it). */
173
+ get incomplete() {
174
+ const finished = this.finished;
175
+ return finished === undefined ? true : finished.incomplete === true;
176
+ }
177
+ /** What the run folder has been charged, in US dollars, over every invocation. */
178
+ get chargedUsd() {
179
+ return Number(this.inspection.run?.charged_usd ?? this.finished?.charged_usd ?? 0);
180
+ }
181
+ /** The ids of the instances that failed. */
182
+ get failed() {
183
+ return (this.finished?.failed ?? []).map(String);
184
+ }
185
+ /** Why the run stopped early, if it did. */
186
+ get stopped() {
187
+ return this.finished?.stopped ?? null;
188
+ }
189
+ /** Each instance's state, by instance id. */
190
+ get steps() {
191
+ return this.projection.instances ?? {};
192
+ }
193
+ /** Each declared output: a {@link RunFile}, a `Map` for a keyed collection, an array, a plain
194
+ * value, or null. */
195
+ get outputs() {
196
+ if (this.#outputs === undefined) {
197
+ const placed = new Map();
198
+ for (const step of this.inspection.run?.steps ?? []) {
199
+ for (const file of step.files ?? []) {
200
+ if (!placed.has(file.digest)) {
201
+ placed.set(file.digest, resolve(this.runDir, file.path));
202
+ }
203
+ }
204
+ }
205
+ const outputs = {};
206
+ for (const [name, value] of Object.entries(this.finished?.outputs ?? {})) {
207
+ outputs[name] = decode(value, placed);
208
+ }
209
+ this.#outputs = outputs;
210
+ }
211
+ return this.#outputs;
212
+ }
213
+ }
214
+ /** A value of the run's record (fx-run-events-v1 `encoded`). */
215
+ export function decode(encoded, placed) {
216
+ if (typeof encoded !== "object" || encoded === null || Object.keys(encoded).length !== 1) {
217
+ throw new FxError(`the run's record holds a value in no known form: ${JSON.stringify(encoded)}`);
218
+ }
219
+ if ("file" in encoded) {
220
+ return new RunFile(encoded.file, placed.get(encoded.file?.digest) ?? null);
221
+ }
222
+ if ("collection" in encoded) {
223
+ return new Map(encoded.collection.map(([key, item]) => [String(key), decode(item, placed)]));
224
+ }
225
+ if ("list" in encoded) {
226
+ return encoded.list.map((item) => decode(item, placed));
227
+ }
228
+ if ("none" in encoded) {
229
+ return null;
230
+ }
231
+ if ("value" in encoded) {
232
+ return encoded.value;
233
+ }
234
+ throw new FxError(`the run's record holds a value in no known form: ${JSON.stringify(encoded)}`);
235
+ }
236
+ /** The options `run` still takes with a {@link Plan}. */
237
+ const RUN_ONLY = ["live", "yesUpTo", "runDir", "deliver", "signal"];
238
+ /**
239
+ * Runs `target`, or a plan, after planning it: a plan with problems rejects with
240
+ * {@link PlanRefused} before any folder exists. `live: true` admits paid calls, within a
241
+ * ceiling. Then reads the folder back through `grida-fx project` and `inspect --json`.
242
+ */
243
+ export async function run(target, options = {}) {
244
+ let request;
245
+ if (target instanceof Plan) {
246
+ const given = Object.entries(options)
247
+ .filter(([name, value]) => value !== undefined && !RUN_ONLY.includes(name))
248
+ .map(([name]) => name);
249
+ if (given.length > 0) {
250
+ throw new TypeError(`a Plan runs with the target and options it was planned with, so it takes no ` +
251
+ `${given.join(", ")}: plan again to change them`);
252
+ }
253
+ request = target.request;
254
+ }
255
+ else {
256
+ request = makeRequest(target, options);
257
+ }
258
+ const own = runArgs(options);
259
+ const signal = options.signal;
260
+ const planned = target instanceof Plan ? target : await planRequest(request, signal);
261
+ if (!planned.ok) {
262
+ throw new PlanRefused(planned);
263
+ }
264
+ const done = await withInputsFile(request, (inputsFile) => call(["run", ...planningArgs(request, inputsFile), ...own], callOptions(request, signal)));
265
+ if (done.status === INTERRUPTED) {
266
+ throw new FxError("the run was interrupted", {
267
+ exitCode: done.status,
268
+ stdout: done.stdout,
269
+ stderr: done.stderr,
270
+ args: done.args,
271
+ });
272
+ }
273
+ if (done.status !== 0 && done.status !== 1) {
274
+ throw failure("run", done);
275
+ }
276
+ const lines = done.stdout.split("\n");
277
+ const named = lines
278
+ .filter((line) => line.startsWith(RUN_LINE))
279
+ .map((line) => line.slice(RUN_LINE.length));
280
+ const folder = options.runDir ?? named.at(-1);
281
+ if (folder === undefined) {
282
+ const refused = lines.filter((line) => line.startsWith(REFUSED));
283
+ if (refused.length > 0) {
284
+ throw new FxError(refused.at(-1) ?? "refused", {
285
+ exitCode: done.status,
286
+ stdout: done.stdout,
287
+ stderr: done.stderr,
288
+ args: done.args,
289
+ });
290
+ }
291
+ const replanned = await planRequest(request, signal);
292
+ if (!replanned.ok) {
293
+ throw new PlanRefused(replanned);
294
+ }
295
+ throw failure("run", done);
296
+ }
297
+ const readOptions = callOptions(request, signal);
298
+ const projection = await project(folder, readOptions);
299
+ const inspection = await inspect(folder, readOptions);
300
+ return new RunResult(resolve(request.cwd, folder), done, projection, inspection);
301
+ }
302
+ /** `grida-fx project <run>`: a run's record projected to its state. */
303
+ export async function project(runDir, options = {}) {
304
+ return document("project", [runDir], options, [0]);
305
+ }
306
+ /** `grida-fx inspect <run> --json`: a run's summary. `run` is a run folder, or a workflow id for
307
+ * its newest run. With `verify`, `verification` says whether every placed file still matches. */
308
+ export async function inspect(run, options = {}) {
309
+ const args = [run, "--json", ...(options.verify === true ? ["--verify"] : [])];
310
+ return document("inspect", args, options, [0, 1]);
311
+ }
312
+ /** The engine's version (`grida-fx --version`), e.g. `0.1.0-alpha.1`. */
313
+ export async function engineVersion(options = {}) {
314
+ const done = await call(["--version"], options);
315
+ if (done.status !== 0) {
316
+ throw failure("--version", done);
317
+ }
318
+ return done.stdout.trim().replace(/^grida-fx\s+/, "");
319
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Finding the engine, the `grida-fx` binary. The `grida-fx` command this package installs and
3
+ * every function of the SDK find it the same way:
4
+ *
5
+ * 1. `GRIDA_FX_BIN`, when set and not empty: the path of a binary (relative to the working
6
+ * directory). It must be a file.
7
+ * 2. The engine package for this machine (`@grida/fx-darwin-arm64`, …; see `platforms.ts`), an
8
+ * optional dependency of `@grida/fx`: its `bin/grida-fx`.
9
+ *
10
+ * Nothing else: the command never looks on `PATH`, where it would find itself.
11
+ */
12
+ /** The environment variable naming the binary to use. */
13
+ export declare const BINARY_VARIABLE = "GRIDA_FX_BIN";
14
+ /** An environment, as `process.env` holds it. */
15
+ export type Environment = Readonly<Record<string, string | undefined>>;
16
+ /** What finding the engine depends on, so it can be checked without the machine. */
17
+ export interface EngineLookup {
18
+ readonly env: Environment;
19
+ /** `process.platform`. */
20
+ readonly os: string;
21
+ /** `process.arch`. */
22
+ readonly cpu: string;
23
+ /** Where a relative `GRIDA_FX_BIN` starts. */
24
+ readonly cwd: string;
25
+ /** The path of a package's `package.json`, as `require.resolve` finds it; throws when absent. */
26
+ resolvePackageJson(name: string): string;
27
+ /** Whether a path is a file. */
28
+ isFile(path: string): boolean;
29
+ /** Whether this Linux system's C library is musl rather than glibc (asked only on Linux). */
30
+ isMusl(): boolean;
31
+ }
32
+ /**
33
+ * The engine's path, as the module comment says; throws an {@link FxError} that says what to do
34
+ * when there is none.
35
+ */
36
+ export declare function findEngine(lookup: EngineLookup): string;
37
+ /** Options of {@link binary}. */
38
+ export interface BinaryOptions {
39
+ /** Variables over `process.env` (a variable given `undefined` is removed). */
40
+ readonly env?: Environment;
41
+ /** Where a relative `GRIDA_FX_BIN` starts (default: the process's working directory). */
42
+ readonly cwd?: string;
43
+ }
44
+ /** The `grida-fx` binary the SDK drives, found as the module comment says. */
45
+ export declare function binary(options?: BinaryOptions): string;
46
+ /** The lookup of this machine and this installation of `@grida/fx`. */
47
+ export declare function machineLookup(env: Environment, cwd: string): EngineLookup;
48
+ /** `process.env` with `overrides` over it; a variable given `undefined` is removed. */
49
+ export declare function mergeEnvironment(overrides: Environment | undefined): Record<string, string>;