@yaag/cli 0.3.0 → 0.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@earendil-works/pi-tui": "^0.84.0",
24
- "@yaag/runtime": "0.3.0",
25
- "@yaag/tui": "0.3.0",
24
+ "@yaag/runtime": "0.4.0",
25
+ "@yaag/tui": "0.4.0",
26
26
  "typebox": "1.3.7"
27
27
  }
28
28
  }
package/src/argv.ts CHANGED
@@ -7,6 +7,7 @@ import { resolve } from "node:path";
7
7
  export const USAGE = `usage:
8
8
  yaag run <program.ts> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
9
9
  yaag run --eval <source> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
10
+ yaag run --eval-fd <n> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
10
11
  yaag describe <program.ts>
11
12
  yaag setup-workspace [dir]
12
13
 
@@ -15,8 +16,12 @@ program file or --eval, but do not give both. A program that --eval runs can
15
16
  import "@yaag/runtime" and "typebox" only. A program that imports other modules
16
17
  must be a file.
17
18
 
18
- A --resume or --replay Run also needs the program: give the program file, or
19
- --eval <source>. A Cassette holds the history of a Run, and never the program
19
+ --eval-fd reads the program source from descriptor <n>, and needs the writer to
20
+ close that descriptor. It keeps the source out of the process argument list,
21
+ where every local user can read it. The yaag extension always uses it.
22
+
23
+ A --resume or --replay Run also needs the program: give the program file,
24
+ --eval <source>, or --eval-fd <n>. A Cassette holds the history of a Run, and never the program
20
25
  to run.
21
26
 
22
27
  Warning: describe imports the module and executes its top level. Keep program module top level side-effect free.`;
@@ -24,7 +29,8 @@ Warning: describe imports the module and executes its top level. Keep program mo
24
29
  /** The program one `run` invocation names: a file, or source text (ADR-0033). */
25
30
  export type ProgramSource =
26
31
  | { readonly kind: "file"; readonly file: string }
27
- | { readonly kind: "inline"; readonly source: string };
32
+ | { readonly kind: "inline"; readonly source: string }
33
+ | { readonly kind: "inline-fd"; readonly fd: number };
28
34
 
29
35
  export type ParsedArgv =
30
36
  | {
@@ -60,6 +66,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
60
66
  function parseRun(tokens: readonly string[]): ParsedArgv {
61
67
  let file: string | undefined;
62
68
  let evalSource: string | undefined;
69
+ let evalFd: number | undefined;
63
70
  let args: unknown = {};
64
71
  let eventsFd: number | undefined;
65
72
  let record: string | undefined;
@@ -74,6 +81,7 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
74
81
  } else if (
75
82
  token === "--args" ||
76
83
  token === "--eval" ||
84
+ token === "--eval-fd" ||
77
85
  token === "--events-fd" ||
78
86
  token === "--record" ||
79
87
  token === "--replay" ||
@@ -88,6 +96,10 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
88
96
  args = parsed.value;
89
97
  } else if (token === "--eval") {
90
98
  evalSource = value;
99
+ } else if (token === "--eval-fd") {
100
+ const parsed = parseEvalDescriptor(value);
101
+ if (!parsed.ok) return parsed;
102
+ evalFd = parsed.value;
91
103
  } else if (token === "--events-fd") {
92
104
  const parsed = parseDescriptor(value);
93
105
  if (!parsed.ok) return parsed;
@@ -108,9 +120,9 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
108
120
  }
109
121
  }
110
122
 
111
- const clash = conflict({ file, evalSource, record, replay, resume });
123
+ const clash = conflict({ file, evalSource, evalFd, eventsFd, record, replay, resume });
112
124
  if (clash !== null) return failure(`${clash}\n${USAGE}`);
113
- const program = programSource(file, evalSource);
125
+ const program = programSource(file, evalSource, evalFd);
114
126
  if (program === undefined) return missingProgram(resume, replay);
115
127
  return {
116
128
  ok: true,
@@ -128,6 +140,8 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
128
140
  interface RunFlags {
129
141
  readonly file?: string;
130
142
  readonly evalSource?: string;
143
+ readonly evalFd?: number;
144
+ readonly eventsFd?: number;
131
145
  readonly record?: string;
132
146
  readonly replay?: string;
133
147
  readonly resume?: string;
@@ -135,10 +149,19 @@ interface RunFlags {
135
149
 
136
150
  /** Names the first pair of options that cannot appear in one invocation. */
137
151
  function conflict(flags: RunFlags): string | null {
138
- const { file, evalSource, record, replay, resume } = flags;
152
+ const { file, evalSource, evalFd, eventsFd, record, replay, resume } = flags;
139
153
  if (file !== undefined && evalSource !== undefined) {
140
154
  return "--eval and a program file cannot be used together";
141
155
  }
156
+ if (evalSource !== undefined && evalFd !== undefined) {
157
+ return "--eval and --eval-fd cannot be used together";
158
+ }
159
+ if (file !== undefined && evalFd !== undefined) {
160
+ return "--eval-fd and a program file cannot be used together";
161
+ }
162
+ if (evalFd !== undefined && eventsFd !== undefined && evalFd === eventsFd) {
163
+ return "--eval-fd and --events-fd must use different descriptors";
164
+ }
142
165
  if (record !== undefined && replay !== undefined) {
143
166
  return "--record and --replay cannot be used together";
144
167
  }
@@ -159,7 +182,7 @@ function missingProgram(
159
182
  if (resume === undefined && replay === undefined) return failure(USAGE);
160
183
  const flag = resume !== undefined ? "--resume" : "--replay";
161
184
  return failure(
162
- `${flag} needs the program too: give <program.ts> or --eval <source>. ` +
185
+ `${flag} needs the program too: give <program.ts>, --eval <source>, or --eval-fd <n>. ` +
163
186
  `A cassette holds the history of a Run, and never the program to run.\n${USAGE}`,
164
187
  );
165
188
  }
@@ -167,8 +190,10 @@ function missingProgram(
167
190
  function programSource(
168
191
  file: string | undefined,
169
192
  source: string | undefined,
193
+ fd: number | undefined,
170
194
  ): ProgramSource | undefined {
171
195
  if (source !== undefined) return { kind: "inline", source };
196
+ if (fd !== undefined) return { kind: "inline-fd", fd };
172
197
  return file === undefined ? undefined : { kind: "file", file };
173
198
  }
174
199
 
@@ -202,6 +227,14 @@ function parseDescriptor(value: string): ParsedValue<number> {
202
227
  : failure(`--events-fd must be a descriptor number\n${USAGE}`);
203
228
  }
204
229
 
230
+ /** 0, 1 and 2 are stdin, stdout and stderr, so an Inline Program needs 3 or higher. */
231
+ function parseEvalDescriptor(value: string): ParsedValue<number> {
232
+ const fd = Number(value);
233
+ return Number.isInteger(fd) && fd >= 3
234
+ ? { ok: true, value: fd }
235
+ : failure(`--eval-fd must be a descriptor number of 3 or higher\n${USAGE}`);
236
+ }
237
+
205
238
  function failure(error: string): { readonly ok: false; readonly error: string } {
206
239
  return { ok: false, error };
207
240
  }
@@ -6,9 +6,7 @@
6
6
  * caller adds `stop` and `done`: nothing in this module can prompt
7
7
  * an Agent (ADR — a Peek observes, it never sends).
8
8
  */
9
- import { readFile, writeFile } from "node:fs/promises";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
9
+ import { readFile } from "node:fs/promises";
12
10
  import type { Terminal } from "@earendil-works/pi-tui";
13
11
  import type { AgentInfo } from "@yaag/runtime";
14
12
  import { cliKeybindings, type RunTreeViewHost, type TreeState } from "@yaag/tui";
@@ -21,8 +19,6 @@ export interface CliTreeHostOptions {
21
19
  /** Shows a transient message; `tree-app.ts` routes it to the TUI. */
22
20
  notify(message: string, level: "info" | "warning" | "error"): void;
23
21
  requestRender(): void;
24
- /** Where `openEditor` puts the body; defaults to the system temp directory. */
25
- readonly scratchDir?: string;
26
22
  }
27
23
 
28
24
  /** The read-only capabilities the CLI Run view has. */
@@ -41,17 +37,6 @@ export function createCliTreeHost(options: CliTreeHostOptions): ReadOnlyCliHost
41
37
  copyPath: async (text) => {
42
38
  terminal.write(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
43
39
  },
44
- // Spawning $EDITOR inside a live alt-screen would fight the TUI for the
45
- // terminal, so the body lands in a file and the path is reported instead.
46
- openEditor: async (title, body) => {
47
- const path = join(options.scratchDir ?? tmpdir(), scratchName(title));
48
- try {
49
- await writeFile(path, body, "utf8");
50
- options.notify(`Wrote ${title} to ${path}`, "info");
51
- } catch (error) {
52
- options.notify(`Could not write ${title}: ${message(error)}`, "error");
53
- }
54
- },
55
40
  notify: (text, level) => options.notify(text, level),
56
41
  requestRender: () => options.requestRender(),
57
42
  rows: () => Math.max(1, terminal.rows - 1),
@@ -70,12 +55,3 @@ async function readSession(info: AgentInfo | undefined): Promise<string | null>
70
55
  return null;
71
56
  }
72
57
  }
73
-
74
- function scratchName(title: string): string {
75
- const slug = title.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "");
76
- return `yaag-${slug === "" ? "note" : slug}.txt`;
77
- }
78
-
79
- function message(error: unknown): string {
80
- return error instanceof Error ? error.message : String(error);
81
- }
package/src/eval-fd.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Reads the source of an Inline Program from a file descriptor (ADR-0033).
3
+ *
4
+ * An argv element is readable by every local user through `ps` and
5
+ * `/proc/<pid>/cmdline`, so the source of an Inline Program travels on a
6
+ * descriptor instead. Descriptor 3 stays exclusive to Run Lifecycle Events
7
+ * (ADR-0016), so the extension writes the source to descriptor 4.
8
+ */
9
+
10
+ /** The descriptor the extension writes an Inline Program to; fd 3 is events. */
11
+ export const DEFAULT_EVAL_FD = 4;
12
+
13
+ /**
14
+ * Reads the whole Inline Program source from `fd`, until end of file.
15
+ *
16
+ * The read stops when the writer closes the descriptor, so a caller that
17
+ * keeps the descriptor open parks the Run before the program loads.
18
+ *
19
+ * Throws an `Error` that names the descriptor when the read fails, and when
20
+ * the descriptor gives no source.
21
+ */
22
+ export async function readEvalSource(fd: number): Promise<string> {
23
+ let source: string;
24
+ try {
25
+ source = await Bun.file(fd).text();
26
+ } catch (error) {
27
+ throw new Error(`--eval-fd ${fd} cannot be read: ${String(error)}`);
28
+ }
29
+ if (source === "") {
30
+ throw new Error(
31
+ `--eval-fd ${fd} gave no source; write the Orchestration Program to the descriptor and close it`,
32
+ );
33
+ }
34
+ return source;
35
+ }
@@ -35,7 +35,7 @@ export function assertClosedImports(source: string): void {
35
35
  throw new Error(
36
36
  `inline program cannot import a computed specifier ` +
37
37
  `(import call on line ${computed.line}): ` +
38
- `--eval allows ${allowedText()} only, each one as a string literal`,
38
+ `an Inline Program allows ${allowedText()} only, each one as a string literal`,
39
39
  );
40
40
  }
41
41
  const transpiler = new Bun.Transpiler({ loader: "ts" });
@@ -43,7 +43,7 @@ export function assertClosedImports(source: string): void {
43
43
  if (ALLOWED_INLINE_IMPORTS.includes(scanned.path)) continue;
44
44
  throw new Error(
45
45
  `inline program cannot import ${JSON.stringify(scanned.path)}: ` +
46
- `--eval allows ${allowedText()} only`,
46
+ `an Inline Program allows ${allowedText()} only`,
47
47
  );
48
48
  }
49
49
  }
@@ -61,7 +61,7 @@ export function assertClosedImports(source: string): void {
61
61
  * by exactly one.
62
62
  */
63
63
  export function inlineRequirePrelude(): string {
64
- const suffix = JSON.stringify(`: --eval allows ${allowedText()} only`);
64
+ const suffix = JSON.stringify(`: an Inline Program allows ${allowedText()} only`);
65
65
  return [
66
66
  "const require = ((real, allowed) => (id) => {",
67
67
  " if (allowed.includes(id)) return real(id);",
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Composes the two ways `yaag run` gets its Orchestration Program: a file, or
3
- * the source text of an Inline Program.
2
+ * Composes the ways `yaag run` gets its Orchestration Program: a file, or the
3
+ * source of an Inline Program. That source arrives as text, or on a
4
+ * descriptor (ADR-0033); the loader below does not know which.
4
5
  *
5
6
  * The composition sits above both loaders, so neither loader knows the other
6
7
  * and the imports stay a DAG.
@@ -8,6 +9,7 @@
8
9
  import { resolve } from "node:path";
9
10
  import type { OrchestrationProgram } from "@yaag/runtime";
10
11
  import type { ProgramSource } from "./argv.ts";
12
+ import { readEvalSource } from "./eval-fd.ts";
11
13
  import { loadInlineProgram } from "./inline-program.ts";
12
14
  import { loadProgram } from "./program-loader.ts";
13
15
 
@@ -25,7 +27,9 @@ export interface LoadedProgram {
25
27
  * A file path is resolved against the working directory, and its
26
28
  * `programFile` is that absolute path. An Inline Program reports no
27
29
  * `programFile`, because its temporary module is unlinked after the import; it
28
- * reports its source text as its identity instead.
30
+ * reports its source text as its identity instead. A `--eval-fd` program is
31
+ * read to end of file first, so a writer that never closes the descriptor
32
+ * parks the Run (see `eval-fd.ts`).
29
33
  * Each failure of `loadProgram` or `loadInlineProgram` — a missing file, a
30
34
  * specifier outside the closed contract, a module that is not a program —
31
35
  * passes through unchanged.
@@ -42,5 +46,13 @@ export async function loadRunProgram(source: ProgramSource): Promise<LoadedProgr
42
46
  programFile: undefined,
43
47
  programSource: source.source,
44
48
  };
49
+ case "inline-fd": {
50
+ const text = await readEvalSource(source.fd);
51
+ return {
52
+ program: await loadInlineProgram(text),
53
+ programFile: undefined,
54
+ programSource: text,
55
+ };
56
+ }
45
57
  }
46
58
  }