@zenera/cli 1.1.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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/args.d.ts +40 -0
  4. package/dist/args.js +99 -0
  5. package/dist/audit.d.ts +53 -0
  6. package/dist/audit.js +144 -0
  7. package/dist/banner.d.ts +13 -0
  8. package/dist/banner.js +103 -0
  9. package/dist/command.d.ts +14 -0
  10. package/dist/command.js +12 -0
  11. package/dist/commands/check.d.ts +3 -0
  12. package/dist/commands/check.js +287 -0
  13. package/dist/commands/index.d.ts +22 -0
  14. package/dist/commands/index.js +56 -0
  15. package/dist/commands/init.d.ts +3 -0
  16. package/dist/commands/init.js +157 -0
  17. package/dist/commands/inspect.d.ts +3 -0
  18. package/dist/commands/inspect.js +158 -0
  19. package/dist/commands/key.d.ts +3 -0
  20. package/dist/commands/key.js +335 -0
  21. package/dist/commands/list.d.ts +3 -0
  22. package/dist/commands/list.js +101 -0
  23. package/dist/commands/models.d.ts +9 -0
  24. package/dist/commands/models.js +120 -0
  25. package/dist/commands/open.d.ts +9 -0
  26. package/dist/commands/open.js +270 -0
  27. package/dist/commands/run.d.ts +3 -0
  28. package/dist/commands/run.js +167 -0
  29. package/dist/commands/sandbox.d.ts +3 -0
  30. package/dist/commands/sandbox.js +112 -0
  31. package/dist/commands/version.d.ts +6 -0
  32. package/dist/commands/version.js +39 -0
  33. package/dist/engine.d.ts +49 -0
  34. package/dist/engine.js +208 -0
  35. package/dist/external.d.ts +10 -0
  36. package/dist/external.js +56 -0
  37. package/dist/home.d.ts +31 -0
  38. package/dist/home.js +108 -0
  39. package/dist/ids.d.ts +12 -0
  40. package/dist/ids.js +44 -0
  41. package/dist/keys.d.ts +124 -0
  42. package/dist/keys.js +309 -0
  43. package/dist/lib.d.ts +9 -0
  44. package/dist/lib.js +31 -0
  45. package/dist/liveness.d.ts +23 -0
  46. package/dist/liveness.js +221 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.js +155 -0
  49. package/dist/narrate.d.ts +19 -0
  50. package/dist/narrate.js +124 -0
  51. package/dist/podman.d.ts +46 -0
  52. package/dist/podman.js +254 -0
  53. package/dist/projects.d.ts +70 -0
  54. package/dist/projects.js +232 -0
  55. package/dist/resolve.d.ts +27 -0
  56. package/dist/resolve.js +138 -0
  57. package/dist/sandbox.d.ts +36 -0
  58. package/dist/sandbox.js +104 -0
  59. package/dist/scaffold.d.ts +29 -0
  60. package/dist/scaffold.js +220 -0
  61. package/dist/session.d.ts +77 -0
  62. package/dist/session.js +156 -0
  63. package/dist/term.d.ts +69 -0
  64. package/dist/term.js +242 -0
  65. package/dist/tui/app.d.ts +8 -0
  66. package/dist/tui/app.js +257 -0
  67. package/dist/tui/theme.d.ts +23 -0
  68. package/dist/tui/theme.js +134 -0
  69. package/dist/tui/wrap.d.ts +12 -0
  70. package/dist/tui/wrap.js +62 -0
  71. package/dist/validate.d.ts +145 -0
  72. package/dist/validate.js +959 -0
  73. package/package.json +76 -0
  74. package/templates/.github/copilot-instructions.md +1579 -0
  75. package/templates/.github/prompts/new-agent.prompt.md +38 -0
  76. package/templates/.github/prompts/new-skill.prompt.md +37 -0
  77. package/templates/.github/prompts/review-project.prompt.md +31 -0
  78. package/templates/.github/skills/zen-cli/SKILL.md +110 -0
@@ -0,0 +1,120 @@
1
+ import { loadProject, readProjectConfig } from '@zenera/neo';
2
+ import { parse } from "../args.js";
3
+ import { KeyStore, PROVIDERS, SHAPES } from "../keys.js";
4
+ import { project as resolveProject } from "../resolve.js";
5
+ import { bold, credentialError, cyan, dim, green, invalidError, json, note, red, table, writeAll, yellow, } from "../term.js";
6
+ import { availableTools } from "../validate.js";
7
+ const USAGE = 'zen models [--project <name|dir>]';
8
+ /**
9
+ * Everything a run would resolve, resolved — and nothing called. Loading a
10
+ * project constructs the model clients, so a config that names an impossible
11
+ * provider or an agent that hands off to nobody fails here, in a command that
12
+ * costs nothing, instead of three seconds into a run that costs money.
13
+ */
14
+ export const models = {
15
+ summary: 'Resolve providers and models and validate the config, calling nothing.',
16
+ usage: USAGE,
17
+ run: async (ctx) => {
18
+ const { values } = parse(ctx.args, { project: { type: 'string' } }, USAGE);
19
+ const found = await resolveProject({ cwd: ctx.cwd, project: values.project });
20
+ const dir = found.dir;
21
+ const store = await KeyStore.open();
22
+ // Asked before materialising, because materialising is exactly what
23
+ // erases the difference between "the environment had it" and "the
24
+ // keyring supplied it".
25
+ const fromEnv = new Set(PROVIDERS.filter((p) => process.env[SHAPES[p].env]));
26
+ store.materialize();
27
+ let project;
28
+ try {
29
+ // The same tools a run would register. Without them every
30
+ // `workspace:*` or `sandbox:*` selector in the config resolves
31
+ // against an empty list and the project fails to load with
32
+ // "no tools in group" — a report about this command, not about
33
+ // the project.
34
+ const { config } = readProjectConfig(dir);
35
+ project = await loadProject(dir, { tools: availableTools(dir, config) });
36
+ }
37
+ catch (err) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ // Constructing a client needs a credential, so a keyless machine
40
+ // cannot get far enough to validate anything. Saying "invalid
41
+ // project" there would be a lie about whose fault it is.
42
+ if (!fromEnv.size && !PROVIDERS.some((p) => store.active(p))) {
43
+ throw credentialError(message, 'add a key: zen key add openai');
44
+ }
45
+ throw invalidError(message, dir);
46
+ }
47
+ const agents = project.agents.map((a) => ({
48
+ name: a.name,
49
+ entry: a.name === project.entry,
50
+ model: a.model?.id ?? project.config.model ?? null,
51
+ tools: a.tools.map((t) => t.name),
52
+ handoffs: a.handoffs,
53
+ }));
54
+ // Resolved through the registry, which proves the ref parses and the
55
+ // provider exists without any call being made.
56
+ const embeddings = Object.keys(project.config.embeddings ?? {})
57
+ .concat(project.config.embedding && !project.config.embeddings?.[project.config.embedding]
58
+ ? [project.config.embedding]
59
+ : [])
60
+ .map((name) => ({
61
+ name,
62
+ model: project.embedder(name)?.id ?? null,
63
+ default: name === project.config.embedding,
64
+ }));
65
+ const credentials = PROVIDERS.map((p) => ({
66
+ provider: p,
67
+ env: SHAPES[p].env,
68
+ source: fromEnv.has(p)
69
+ ? 'environment'
70
+ : store.active(p)
71
+ ? 'keyring'
72
+ : 'missing',
73
+ }));
74
+ if (ctx.json) {
75
+ json({
76
+ project: found.name,
77
+ source: project.source,
78
+ providers: project.models.names(),
79
+ agents,
80
+ embeddings,
81
+ credentials,
82
+ });
83
+ return;
84
+ }
85
+ note(`${bold(found.name)} ${dim(project.source)}`);
86
+ note('');
87
+ note(bold('Agents'));
88
+ writeAll(table(agents.map((a) => [
89
+ ` ${a.entry ? green('→') : ' '} ${a.name}`,
90
+ cyan(a.model ?? dim('inherited')),
91
+ dim(a.tools.length ? a.tools.join(' ') : 'no tools'),
92
+ dim(a.handoffs.length ? `→ ${a.handoffs.join(', ')}` : ''),
93
+ ])));
94
+ if (embeddings.length) {
95
+ note('');
96
+ note(bold('Embeddings'));
97
+ writeAll(table(embeddings.map((e) => [
98
+ ` ${e.default ? green('→') : ' '} ${e.name}`,
99
+ cyan(e.model ?? dim('unresolved')),
100
+ ])));
101
+ }
102
+ note('');
103
+ note(bold('Credentials'));
104
+ writeAll(table(credentials.map((c) => [
105
+ ` ${c.provider}`,
106
+ dim(c.env),
107
+ c.source === 'missing' ? red('missing') : green(c.source),
108
+ ])));
109
+ const declared = project.models.names();
110
+ if (declared.length) {
111
+ note('');
112
+ note(`${bold('Providers')} ${dim(declared.join(', '))}`);
113
+ }
114
+ if (credentials.every((c) => c.source === 'missing')) {
115
+ note('');
116
+ note(yellow('nothing can be reached — add a key: zen key add openai'));
117
+ }
118
+ },
119
+ };
120
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1,9 @@
1
+ import type { Command } from '../command.ts';
2
+ /**
3
+ * A project is resolved by name or from the cwd, and the path is handed to an
4
+ * editor. It exists because opening the project is the thing everybody does
5
+ * second, and because getting the editor right — which one, with which
6
+ * arguments, attached or detached — is fiddly enough to be worth settling once.
7
+ */
8
+ export declare const open: Command;
9
+ //# sourceMappingURL=open.d.ts.map
@@ -0,0 +1,270 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { accessSync, constants, existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { delimiter, join, resolve, sep } from 'node:path';
5
+ import { one, parse } from "../args.js";
6
+ import { project as resolveProject } from "../resolve.js";
7
+ import { copilotInstructions, editorSettings } from "../scaffold.js";
8
+ import { CliError, cyan, dim, EXIT, json, note, usageError } from "../term.js";
9
+ const USAGE = 'zen open [project] [--editor <cmd>] [--wait]';
10
+ /**
11
+ * A project is resolved by name or from the cwd, and the path is handed to an
12
+ * editor. It exists because opening the project is the thing everybody does
13
+ * second, and because getting the editor right — which one, with which
14
+ * arguments, attached or detached — is fiddly enough to be worth settling once.
15
+ */
16
+ export const open = {
17
+ summary: 'Open a project in your editor.',
18
+ usage: USAGE,
19
+ details: [
20
+ 'Editor: --editor, then $ZENERA_EDITOR, then the editor this terminal',
21
+ 'belongs to, then $VISUAL or $EDITOR, then a known editor on PATH or',
22
+ 'installed, then the platform opener.',
23
+ 'The editor files `zen init` writes (.vscode/settings.json and the',
24
+ '.github/ tree) are refreshed in the directory being opened; edits to',
25
+ 'them do not survive.',
26
+ 'VS Code and its forks are launched with --disable-workspace-trust so',
27
+ 'the settings written there apply to the window straight away.',
28
+ ],
29
+ run: async (ctx) => {
30
+ const { values, positionals } = parse(ctx.args, {
31
+ editor: { type: 'string' },
32
+ wait: { type: 'boolean' },
33
+ }, USAGE);
34
+ const found = await resolveProject({
35
+ cwd: ctx.cwd,
36
+ project: one(positionals, 'project', USAGE),
37
+ });
38
+ const dir = found.dir;
39
+ const editor = choose(values.editor, values.wait === true);
40
+ verify(editor);
41
+ // The editor reads the settings and instructions of the folder it was
42
+ // opened on, and a project may predate either of them — or the version
43
+ // this `zen` writes. They are ours, so they are written fresh before
44
+ // the window is there to read them.
45
+ const written = [editorSettings(dir), ...copilotInstructions(dir)];
46
+ if (ctx.json) {
47
+ json({
48
+ name: found.name,
49
+ path: dir,
50
+ editor: [editor.command, ...editor.args].join(' '),
51
+ from: editor.from,
52
+ files: written,
53
+ });
54
+ }
55
+ else {
56
+ note(`opening ${cyan(dir)} with ${editor.label} ${dim(`(${editor.from})`)}`);
57
+ for (const file of written) {
58
+ note(` ${dim(`wrote ${file}`)}`);
59
+ }
60
+ }
61
+ await launch(editor, dir);
62
+ },
63
+ };
64
+ // ---------------------------------------------------------------------------
65
+ // Choosing one
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * The window would otherwise open untrusted, and the settings `zen init`
69
+ * writes into the project are restricted ones: they are ignored until someone
70
+ * clicks through the trust dialog. This is a directory the user just asked to
71
+ * open by name, so answering that question for them is the point.
72
+ */
73
+ const TRUST = '--disable-workspace-trust';
74
+ /** Guessed only when nothing was asked for, in the order they are tried. */
75
+ const KNOWN = [
76
+ { command: 'code', label: 'VS Code', app: 'Visual Studio Code', trusts: true },
77
+ { command: 'cursor', label: 'Cursor', app: 'Cursor', trusts: true },
78
+ {
79
+ command: 'code-insiders',
80
+ label: 'VS Code Insiders',
81
+ app: 'Visual Studio Code - Insiders',
82
+ trusts: true,
83
+ },
84
+ { command: 'windsurf', label: 'Windsurf', app: 'Windsurf', trusts: true },
85
+ { command: 'zed', label: 'Zed', app: 'Zed', waits: '-w' },
86
+ { command: 'subl', label: 'Sublime Text', app: 'Sublime Text', waits: '-w' },
87
+ { command: 'idea', label: 'IntelliJ IDEA', app: 'IntelliJ IDEA' },
88
+ ];
89
+ function choose(asked, wait) {
90
+ const explicit = asked ?? process.env.ZENERA_EDITOR;
91
+ if (explicit !== undefined && explicit.trim() !== '') {
92
+ return {
93
+ ...split(explicit),
94
+ from: asked !== undefined ? '--editor' : '$ZENERA_EDITOR',
95
+ attached: wait,
96
+ };
97
+ }
98
+ // Before $EDITOR, deliberately. $EDITOR conventionally names something to
99
+ // edit *a file* with — it is very often `vim`, set once years ago — and
100
+ // this command opens a directory as a project. The editor you are already
101
+ // sitting in is the better answer to that, and $ZENERA_EDITOR above is the
102
+ // way to say otherwise.
103
+ const host = hosting(wait);
104
+ if (host !== undefined)
105
+ return host;
106
+ // $VISUAL and $EDITOR are, by convention, editors that run *in* the
107
+ // terminal, so they are attached and waited for unless proven otherwise.
108
+ for (const name of ['VISUAL', 'EDITOR']) {
109
+ const value = process.env[name];
110
+ if (value !== undefined && value.trim() !== '') {
111
+ return { ...split(value), from: `$${name}`, attached: true };
112
+ }
113
+ }
114
+ for (const known of KNOWN) {
115
+ if (lookup(known.command) !== undefined) {
116
+ return {
117
+ command: known.command,
118
+ args: [
119
+ ...(known.trusts === true ? [TRUST] : []),
120
+ ...(wait ? [known.waits ?? '--wait'] : []),
121
+ ],
122
+ label: known.label,
123
+ from: 'PATH',
124
+ attached: wait,
125
+ };
126
+ }
127
+ }
128
+ // Installed but never asked to add itself to PATH — which is the default
129
+ // for VS Code on macOS, and so the case this whole branch exists for.
130
+ // LaunchServices can open it without any of that.
131
+ for (const known of KNOWN) {
132
+ const bundle = known.app === undefined ? undefined : installed(known.app);
133
+ if (bundle !== undefined) {
134
+ return {
135
+ command: 'open',
136
+ args: wait ? ['-W', '-a', bundle] : ['-a', bundle],
137
+ label: known.label,
138
+ from: 'Applications',
139
+ attached: wait,
140
+ };
141
+ }
142
+ }
143
+ // Nothing named an editor, so hand the directory to the desktop and let it
144
+ // decide. On macOS that is Finder unless a folder handler is registered.
145
+ const command = process.platform === 'darwin'
146
+ ? 'open'
147
+ : process.platform === 'win32'
148
+ ? 'explorer'
149
+ : 'xdg-open';
150
+ return { command, args: [], label: command, from: 'system', attached: false };
151
+ }
152
+ /**
153
+ * The editor whose integrated terminal this is. VS Code and every fork of it
154
+ * export `VSCODE_GIT_ASKPASS_MAIN`, which points inside the running
155
+ * installation, so the app root is four directories up from it and
156
+ * `product.json` there names both the CLI and the product. That is exact where
157
+ * guessing is not: it picks Cursor when you are in Cursor, and it works when
158
+ * the shell command was never installed, which on macOS is the default.
159
+ */
160
+ function hosting(wait) {
161
+ const askpass = process.env.VSCODE_GIT_ASKPASS_MAIN;
162
+ if (process.env.TERM_PROGRAM !== 'vscode' || askpass === undefined)
163
+ return undefined;
164
+ // <appRoot>/extensions/git/dist/askpass-main.js
165
+ const appRoot = resolve(askpass, '..', '..', '..', '..');
166
+ let product;
167
+ try {
168
+ product = JSON.parse(readFileSync(join(appRoot, 'product.json'), 'utf8'));
169
+ }
170
+ catch {
171
+ return undefined; // not a layout we recognise; fall through to the guesses
172
+ }
173
+ if (product.applicationName === undefined)
174
+ return undefined;
175
+ const cli = join(appRoot, 'bin', product.applicationName);
176
+ for (const candidate of [cli, `${cli}.cmd`]) {
177
+ if (!existsSync(candidate))
178
+ continue;
179
+ return {
180
+ command: candidate,
181
+ args: wait ? [TRUST, '--wait'] : [TRUST],
182
+ label: product.nameLong ?? product.applicationName,
183
+ from: 'this terminal',
184
+ attached: wait,
185
+ };
186
+ }
187
+ return undefined;
188
+ }
189
+ /** The macOS bundle for an application name, in either place they live. */
190
+ function installed(app) {
191
+ if (process.platform !== 'darwin')
192
+ return undefined;
193
+ for (const dir of ['/Applications', join(homedir(), 'Applications')]) {
194
+ const bundle = join(dir, `${app}.app`);
195
+ if (existsSync(bundle))
196
+ return bundle;
197
+ }
198
+ return undefined;
199
+ }
200
+ /**
201
+ * `$EDITOR` may carry arguments — `code -n`, `emacsclient -c`. Splitting on
202
+ * whitespace rather than handing the string to a shell: the path is always an
203
+ * argument, so nothing in it is ever interpreted.
204
+ */
205
+ function split(value) {
206
+ const parts = value.trim().split(/\s+/);
207
+ return { command: parts[0], args: parts.slice(1), label: parts[0] };
208
+ }
209
+ /**
210
+ * PATH lookup without shelling out to `which`, so the resolution failure can
211
+ * be reported before anything is spawned rather than as an ENOENT nobody sees
212
+ * on a detached child.
213
+ */
214
+ function lookup(command) {
215
+ if (command.includes(sep) || command.includes('/')) {
216
+ return existsSync(command) ? command : undefined;
217
+ }
218
+ const extensions = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
219
+ for (const dir of (process.env.PATH ?? '').split(delimiter)) {
220
+ if (dir === '')
221
+ continue;
222
+ for (const extension of extensions) {
223
+ const candidate = join(dir, command + extension);
224
+ try {
225
+ accessSync(candidate, constants.X_OK);
226
+ return candidate;
227
+ }
228
+ catch {
229
+ // not here, keep walking
230
+ }
231
+ }
232
+ }
233
+ return undefined;
234
+ }
235
+ // ---------------------------------------------------------------------------
236
+ // Running it
237
+ // ---------------------------------------------------------------------------
238
+ /**
239
+ * Both ways this can be wrong, settled before anything is spawned and before
240
+ * anything is said: an ENOENT on a detached child is a failure nobody would
241
+ * ever see, and a terminal editor with no terminal to take over would hang.
242
+ */
243
+ function verify(editor) {
244
+ if (editor.from !== 'system' && lookup(editor.command) === undefined) {
245
+ throw usageError(`no such editor: ${editor.command}`, editor.from === 'PATH'
246
+ ? 'name one with --editor, or set $ZENERA_EDITOR'
247
+ : `${editor.from} names a command that is not on PATH`);
248
+ }
249
+ if ((editor.from === '$VISUAL' || editor.from === '$EDITOR') && !process.stdin.isTTY) {
250
+ throw usageError(`${editor.from} names a terminal editor and there is no terminal here`, 'name a windowed one with --editor, or set $ZENERA_EDITOR');
251
+ }
252
+ }
253
+ async function launch(editor, dir) {
254
+ const args = [...editor.args, dir];
255
+ if (!editor.attached) {
256
+ // Detached and unreferenced: the window outlives this process, which is
257
+ // the whole point of a GUI editor.
258
+ spawn(editor.command, args, { stdio: 'ignore', detached: true }).unref();
259
+ return;
260
+ }
261
+ const code = await new Promise((settle, reject) => {
262
+ const child = spawn(editor.command, args, { stdio: 'inherit' });
263
+ child.once('error', reject);
264
+ child.once('close', (status) => settle(status ?? 0));
265
+ });
266
+ if (code !== 0) {
267
+ throw new CliError(`${editor.label} exited with ${code}`, EXIT.failed);
268
+ }
269
+ }
270
+ //# sourceMappingURL=open.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from '../command.ts';
2
+ export declare const run: Command;
3
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1,167 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { parse } from "../args.js";
3
+ import * as Engine from "../engine.js";
4
+ import { duration, Narrator, stopMark, summary } from "../narrate.js";
5
+ import * as Projects from "../projects.js";
6
+ import { target } from "../resolve.js";
7
+ import { display } from "../session.js";
8
+ import { bold, cyan, dim, json, note, readStdin, usageError, write } from "../term.js";
9
+ import { parseChoice } from "../tui/theme.js";
10
+ const USAGE = 'zen run [project] [prompt] [options]';
11
+ export const run = {
12
+ summary: 'Run the project — the TUI on a terminal, one shot otherwise.',
13
+ usage: USAGE,
14
+ details: [
15
+ ' --project <name|dir> Which project. Inferred from the directory.',
16
+ ' --session <id> Continue a particular session.',
17
+ ' --new Start a fresh one.',
18
+ ' --workspace <dir> What the agent can read and write.',
19
+ ' --model <ref> Override the default model.',
20
+ ' --image <ref> Override the sandbox image commands run in.',
21
+ ' --read-only Give the agent no way to write.',
22
+ ' --quiet Answer only; no narration.',
23
+ ' --plain One shot, even on a terminal.',
24
+ ' --theme <dark|light> Force the palette. Detected otherwise; $ZENERA_THEME.',
25
+ ' --out <file> Write the answer here as well as to stdout.',
26
+ ' --yes Accept the questions this would otherwise ask.',
27
+ '',
28
+ 'The first word is the project when it names one, as in `zen run acme`,',
29
+ 'and the first word of the prompt when it does not. --project settles it.',
30
+ '',
31
+ 'The prompt comes from the argument, or stdin, or the TUI. There is no',
32
+ '`resume`: a session continues itself, because its state is what it is.',
33
+ '',
34
+ 'With a prompt on the command line nothing is asked: a fresh session, the',
35
+ 'directory you are in as the workspace, writable. --session, --workspace',
36
+ 'and --read-only override that; the TUI still asks.',
37
+ ],
38
+ run: async (ctx) => {
39
+ const { values, positionals } = parse(ctx.args, {
40
+ project: { type: 'string' },
41
+ session: { type: 'string' },
42
+ new: { type: 'boolean' },
43
+ workspace: { type: 'string' },
44
+ model: { type: 'string' },
45
+ image: { type: 'string' },
46
+ 'read-only': { type: 'boolean' },
47
+ yes: { type: 'boolean' },
48
+ quiet: { type: 'boolean' },
49
+ plain: { type: 'boolean' },
50
+ theme: { type: 'string' },
51
+ out: { type: 'string' },
52
+ }, USAGE);
53
+ const piped = await readStdin();
54
+ // `zen run acme` is what everyone types before finding --project, and a
55
+ // project name is a bare word where a prompt is a sentence. So the
56
+ // first positional is read as a project when it names one and as the
57
+ // first word of the prompt when it does not — which also makes
58
+ // `zen run acme "what changed?"` mean what it looks like. --project is
59
+ // there for the day a project is called "why".
60
+ const [head, ...rest] = positionals;
61
+ const named = !values.project && head ? await Projects.find(head) : undefined;
62
+ const prompt = (named ? rest : positionals).join(' ').trim() || piped;
63
+ if (values.theme !== undefined && !parseChoice(values.theme)) {
64
+ throw usageError(`unknown theme: ${values.theme}`, 'dark, light or auto');
65
+ }
66
+ // A prompt on the command line is a request for an answer, not a
67
+ // conversation to pick up. So it answers the three questions itself:
68
+ // a fresh session, the directory it was typed in as the workspace,
69
+ // and no confirmation for it — `zen run acme "what changed?"` should
70
+ // read the code that is right there. Every flag still wins, and the
71
+ // TUI, where there is someone to ask, still asks.
72
+ const shot = Boolean(prompt);
73
+ const where = await target({
74
+ cwd: ctx.cwd,
75
+ project: values.project ?? (named ? head : undefined),
76
+ session: values.session,
77
+ fresh: values.new || (shot && !values.session),
78
+ workspace: values.workspace ?? (shot ? ctx.cwd : undefined),
79
+ yes: values.yes || shot,
80
+ });
81
+ const engine = await Engine.open({
82
+ project: where.project,
83
+ session: where.session,
84
+ readOnly: values['read-only'],
85
+ model: values.model,
86
+ image: values.image,
87
+ yes: values.yes || ctx.json,
88
+ });
89
+ try {
90
+ // The TUI is for the case it is actually good at: a person at a
91
+ // terminal, with nothing to say yet. A prompt on the command line
92
+ // is a request for an answer, and drawing a full-screen interface
93
+ // over it would be worse than not drawing one.
94
+ const drawing = !prompt &&
95
+ !values.plain &&
96
+ !values.quiet &&
97
+ !ctx.json &&
98
+ Boolean(process.stdout.isTTY && process.stdin.isTTY);
99
+ if (drawing) {
100
+ const { start } = await import("../tui/app.js");
101
+ await start(engine, {
102
+ readOnly: Boolean(values['read-only']),
103
+ theme: values.theme,
104
+ });
105
+ return;
106
+ }
107
+ if (!prompt) {
108
+ throw usageError('nothing to ask', 'give a prompt, or pipe one in');
109
+ }
110
+ await once(engine, prompt, values, ctx.json, ctx.cwd);
111
+ }
112
+ finally {
113
+ await engine.close();
114
+ }
115
+ },
116
+ };
117
+ // ---------------------------------------------------------------------------
118
+ // One shot
119
+ // ---------------------------------------------------------------------------
120
+ async function once(engine, prompt, values, asJson, cwd) {
121
+ const narrator = new Narrator({
122
+ quiet: Boolean(values.quiet) || asJson,
123
+ live: Boolean(process.stderr.isTTY),
124
+ });
125
+ // Ctrl-C asks the run to stop rather than killing the process, so the turn
126
+ // still lands on disk and the session stays resumable.
127
+ const stopping = new AbortController();
128
+ const onInterrupt = () => stopping.abort();
129
+ process.once('SIGINT', onInterrupt);
130
+ if (!values.quiet && !asJson) {
131
+ note(`${bold(engine.name)} ${dim(engine.session.id)} ` + dim(display(engine.workspace, cwd)));
132
+ }
133
+ let outcome;
134
+ try {
135
+ outcome = await Engine.run(engine, prompt, narrator.handle, stopping.signal);
136
+ }
137
+ finally {
138
+ narrator.done();
139
+ process.off('SIGINT', onInterrupt);
140
+ }
141
+ if (values.out) {
142
+ await writeFile(values.out, `${outcome.text}\n`, 'utf8');
143
+ }
144
+ if (asJson) {
145
+ json({
146
+ session: engine.session.id,
147
+ run: outcome.run.id,
148
+ agent: outcome.result.agent,
149
+ stopReason: outcome.result.stopReason,
150
+ durationMs: outcome.durationMs,
151
+ usage: outcome.result.usage,
152
+ output: outcome.text,
153
+ report: outcome.run.report,
154
+ });
155
+ return;
156
+ }
157
+ write(outcome.text);
158
+ if (!values.quiet) {
159
+ note('');
160
+ note(`${stopMark(outcome.result.stopReason)} ${dim(duration(outcome.durationMs))} ` +
161
+ dim(summary(outcome.result.usage)));
162
+ if (outcome.report) {
163
+ note(dim(`report: ${cyan(display(outcome.report, cwd))}`));
164
+ }
165
+ }
166
+ }
167
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from '../command.ts';
2
+ export declare const sandbox: Command;
3
+ //# sourceMappingURL=sandbox.d.ts.map