@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 +3 -3
- package/src/argv.ts +40 -7
- package/src/cli-tree-host.ts +1 -25
- package/src/eval-fd.ts +35 -0
- package/src/inline-imports.ts +3 -3
- package/src/run-program.ts +15 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaag/cli",
|
|
3
|
-
"version": "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.
|
|
25
|
-
"@yaag/tui": "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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
}
|
package/src/cli-tree-host.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/inline-imports.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(`:
|
|
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);",
|
package/src/run-program.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Composes the
|
|
3
|
-
*
|
|
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
|
}
|