@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.
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/args.d.ts +40 -0
- package/dist/args.js +99 -0
- package/dist/audit.d.ts +53 -0
- package/dist/audit.js +144 -0
- package/dist/banner.d.ts +13 -0
- package/dist/banner.js +103 -0
- package/dist/command.d.ts +14 -0
- package/dist/command.js +12 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.js +287 -0
- package/dist/commands/index.d.ts +22 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.js +158 -0
- package/dist/commands/key.d.ts +3 -0
- package/dist/commands/key.js +335 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/models.d.ts +9 -0
- package/dist/commands/models.js +120 -0
- package/dist/commands/open.d.ts +9 -0
- package/dist/commands/open.js +270 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +167 -0
- package/dist/commands/sandbox.d.ts +3 -0
- package/dist/commands/sandbox.js +112 -0
- package/dist/commands/version.d.ts +6 -0
- package/dist/commands/version.js +39 -0
- package/dist/engine.d.ts +49 -0
- package/dist/engine.js +208 -0
- package/dist/external.d.ts +10 -0
- package/dist/external.js +56 -0
- package/dist/home.d.ts +31 -0
- package/dist/home.js +108 -0
- package/dist/ids.d.ts +12 -0
- package/dist/ids.js +44 -0
- package/dist/keys.d.ts +124 -0
- package/dist/keys.js +309 -0
- package/dist/lib.d.ts +9 -0
- package/dist/lib.js +31 -0
- package/dist/liveness.d.ts +23 -0
- package/dist/liveness.js +221 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +155 -0
- package/dist/narrate.d.ts +19 -0
- package/dist/narrate.js +124 -0
- package/dist/podman.d.ts +46 -0
- package/dist/podman.js +254 -0
- package/dist/projects.d.ts +70 -0
- package/dist/projects.js +232 -0
- package/dist/resolve.d.ts +27 -0
- package/dist/resolve.js +138 -0
- package/dist/sandbox.d.ts +36 -0
- package/dist/sandbox.js +104 -0
- package/dist/scaffold.d.ts +29 -0
- package/dist/scaffold.js +220 -0
- package/dist/session.d.ts +77 -0
- package/dist/session.js +156 -0
- package/dist/term.d.ts +69 -0
- package/dist/term.js +242 -0
- package/dist/tui/app.d.ts +8 -0
- package/dist/tui/app.js +257 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +134 -0
- package/dist/tui/wrap.d.ts +12 -0
- package/dist/tui/wrap.js +62 -0
- package/dist/validate.d.ts +145 -0
- package/dist/validate.js +959 -0
- package/package.json +76 -0
- package/templates/.github/copilot-instructions.md +1579 -0
- package/templates/.github/prompts/new-agent.prompt.md +38 -0
- package/templates/.github/prompts/new-skill.prompt.md +37 -0
- package/templates/.github/prompts/review-project.prompt.md +31 -0
- package/templates/.github/skills/zen-cli/SKILL.md +110 -0
package/dist/banner.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { dim, note } from "./term.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// The banner
|
|
5
|
+
//
|
|
6
|
+
// The one piece of output that exists to be looked at rather than read. It goes
|
|
7
|
+
// to **stderr**, like every other piece of narration, and only when stderr is a
|
|
8
|
+
// terminal — a pipeline asking for the answer gets the answer, and a CI log is
|
|
9
|
+
// not decorated with block letters.
|
|
10
|
+
//
|
|
11
|
+
// The font is here rather than pulled in as a dependency because it is a
|
|
12
|
+
// kilobyte of constant and the whole CLI is otherwise Node's own. Two things
|
|
13
|
+
// about its shape are deliberate: the stroke is **two cells wide**, because a
|
|
14
|
+
// one-cell stem in a dotted fill reads as noise rather than as a letter, and
|
|
15
|
+
// the ink is the dotted block (U+2592) rather than a solid one, which makes a
|
|
16
|
+
// thick stroke a screen-print instead of a slab.
|
|
17
|
+
//
|
|
18
|
+
// A terminal too narrow for the art gets the wordmark on one line. A banner
|
|
19
|
+
// that wraps is worse than no banner.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const HEIGHT = 5;
|
|
22
|
+
const WIDTH = 6;
|
|
23
|
+
const FONT = {
|
|
24
|
+
A: [' ▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒', '▒▒ ▒▒'],
|
|
25
|
+
B: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ '],
|
|
26
|
+
C: [' ▒▒▒▒▒', '▒▒ ', '▒▒ ', '▒▒ ', ' ▒▒▒▒▒'],
|
|
27
|
+
D: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒▒▒▒ '],
|
|
28
|
+
E: ['▒▒▒▒▒▒', '▒▒ ', '▒▒▒▒▒ ', '▒▒ ', '▒▒▒▒▒▒'],
|
|
29
|
+
F: ['▒▒▒▒▒▒', '▒▒ ', '▒▒▒▒▒ ', '▒▒ ', '▒▒ '],
|
|
30
|
+
G: [' ▒▒▒▒▒', '▒▒ ', '▒▒ ▒▒▒', '▒▒ ▒▒', ' ▒▒▒▒▒'],
|
|
31
|
+
H: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒', '▒▒ ▒▒'],
|
|
32
|
+
I: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', ' ▒▒ ', '▒▒▒▒▒▒'],
|
|
33
|
+
J: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', '▒▒ ▒▒ ', ' ▒▒▒ '],
|
|
34
|
+
K: ['▒▒ ▒▒', '▒▒ ▒▒ ', '▒▒▒▒ ', '▒▒ ▒▒ ', '▒▒ ▒▒'],
|
|
35
|
+
L: ['▒▒ ', '▒▒ ', '▒▒ ', '▒▒ ', '▒▒▒▒▒▒'],
|
|
36
|
+
M: ['▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒', '▒▒ ▒▒'],
|
|
37
|
+
N: ['▒▒ ▒▒', '▒▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒▒', '▒▒ ▒▒'],
|
|
38
|
+
O: [' ▒▒▒▒ ', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', ' ▒▒▒▒ '],
|
|
39
|
+
P: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ ', '▒▒ ', '▒▒ '],
|
|
40
|
+
Q: [' ▒▒▒▒ ', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒ ', ' ▒▒ ▒▒'],
|
|
41
|
+
R: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ ', '▒▒ ▒▒ ', '▒▒ ▒▒'],
|
|
42
|
+
S: [' ▒▒▒▒▒', '▒▒ ', ' ▒▒▒▒ ', ' ▒▒', '▒▒▒▒▒ '],
|
|
43
|
+
T: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', ' ▒▒ ', ' ▒▒ '],
|
|
44
|
+
U: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', ' ▒▒▒▒ '],
|
|
45
|
+
V: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', ' ▒▒▒▒ ', ' ▒▒ '],
|
|
46
|
+
W: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒'],
|
|
47
|
+
X: ['▒▒ ▒▒', ' ▒▒▒▒ ', ' ▒▒ ', ' ▒▒▒▒ ', '▒▒ ▒▒'],
|
|
48
|
+
Y: ['▒▒ ▒▒', ' ▒▒▒▒ ', ' ▒▒ ', ' ▒▒ ', ' ▒▒ '],
|
|
49
|
+
Z: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', ' ▒▒ ', '▒▒▒▒▒▒'],
|
|
50
|
+
};
|
|
51
|
+
const BLANK = ' '.repeat(WIDTH);
|
|
52
|
+
/** One word as five rows of equal length — so two words line up when joined. */
|
|
53
|
+
function big(word) {
|
|
54
|
+
const rows = new Array(HEIGHT).fill('');
|
|
55
|
+
for (const ch of word.toUpperCase()) {
|
|
56
|
+
const glyph = FONT[ch];
|
|
57
|
+
for (let r = 0; r < HEIGHT; r++) {
|
|
58
|
+
rows[r] += `${glyph?.[r] ?? BLANK} `;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
63
|
+
/** A wordmark rather than a sentence, so the letters are set apart. */
|
|
64
|
+
const spaced = (s) => [...s.toUpperCase()].join(' ');
|
|
65
|
+
/** The name, in the brightest thing the terminal has. */
|
|
66
|
+
const bright = (s) => styleText(['bold', 'whiteBright'], s);
|
|
67
|
+
/** The half that carries the colour. */
|
|
68
|
+
const neon = (s) => styleText(['bold', 'magentaBright'], s);
|
|
69
|
+
export const NEO_BANNER = {
|
|
70
|
+
head: 'Zenera',
|
|
71
|
+
accent: 'Neo',
|
|
72
|
+
subtitle: 'Agentic Runtime',
|
|
73
|
+
};
|
|
74
|
+
export function bannerLines(text, columns = process.stderr.columns || 80) {
|
|
75
|
+
const head = big(text.head);
|
|
76
|
+
// Trimmed: the widest accent row is the banner's right edge, and a pad left
|
|
77
|
+
// inside a styled string cannot be trimmed away later.
|
|
78
|
+
const accent = big(text.accent).map((row) => row.trimEnd());
|
|
79
|
+
const width = 2 + head[0].length + Math.max(...accent.map((row) => row.length));
|
|
80
|
+
const foot = ` ${dim(spaced(text.subtitle))}`;
|
|
81
|
+
if (width > columns) {
|
|
82
|
+
return [` ${bright(text.head.toUpperCase())} ${neon(text.accent.toUpperCase())}`, foot];
|
|
83
|
+
}
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (let r = 0; r < HEIGHT; r++) {
|
|
86
|
+
lines.push(` ${bright(head[r])} ${neon(accent[r])}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(foot);
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
/** Narration, and only for someone watching. */
|
|
93
|
+
export function printBanner(text) {
|
|
94
|
+
if (!process.stderr.isTTY) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
note('');
|
|
98
|
+
for (const line of bannerLines(text)) {
|
|
99
|
+
note(line);
|
|
100
|
+
}
|
|
101
|
+
note('');
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Context {
|
|
2
|
+
readonly args: readonly string[];
|
|
3
|
+
readonly json: boolean;
|
|
4
|
+
/** honours `-C <dir>`, so nothing below reads `process.cwd()` directly */
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Command {
|
|
8
|
+
readonly summary: string;
|
|
9
|
+
readonly usage: string;
|
|
10
|
+
/** lines printed under the usage line by `zen help <command>` */
|
|
11
|
+
readonly details?: readonly string[];
|
|
12
|
+
run(ctx: Context): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=command.d.ts.map
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The command contract
|
|
3
|
+
//
|
|
4
|
+
// A command receives its own arguments, already stripped of the name, and the
|
|
5
|
+
// two things the frame settled before dispatching: whether output should be
|
|
6
|
+
// machine-readable, and which directory the whole invocation is relative to.
|
|
7
|
+
//
|
|
8
|
+
// It returns nothing. Success is the absence of a throw; every failure worth
|
|
9
|
+
// distinguishing is a `CliError` carrying its exit code.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path';
|
|
2
|
+
import { one, parse } from "../args.js";
|
|
3
|
+
import { KeyStore } from "../keys.js";
|
|
4
|
+
import { Registry } from "../projects.js";
|
|
5
|
+
import { project as resolveProject } from "../resolve.js";
|
|
6
|
+
import { bold, count, cyan, dim, green, invalidError, json, red, table, write, writeAll, yellow, } from "../term.js";
|
|
7
|
+
import { validateProject, } from "../validate.js";
|
|
8
|
+
const USAGE = 'zen check [dir] [--project <name|dir>] [--strict] [--quiet]';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// zen check
|
|
11
|
+
//
|
|
12
|
+
// Written to be read by a program as much as by a person, because the program
|
|
13
|
+
// reading it is usually a model: an agent asked to fix a project needs to know
|
|
14
|
+
// what is wrong, where, and what would fix it, without opening six files to
|
|
15
|
+
// find out. So every section names its files by path, every finding carries a
|
|
16
|
+
// stable code and a fix, and the whole thing goes to stdout — it is the answer,
|
|
17
|
+
// not narration.
|
|
18
|
+
//
|
|
19
|
+
// Nothing here is contacted, started or paid for. That is the point: the check
|
|
20
|
+
// has to work on the machine that is not set up yet, which is the machine that
|
|
21
|
+
// most needs it.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export const check = {
|
|
24
|
+
summary: 'Validate agents.yaml and every file it names, and report in full.',
|
|
25
|
+
usage: USAGE,
|
|
26
|
+
details: [
|
|
27
|
+
'Checks the whole project without running anything: the configuration',
|
|
28
|
+
'parses and satisfies the schema, every prompt, skill and catalog it',
|
|
29
|
+
'names is on disk, hand-offs and forks name agents that exist, tool',
|
|
30
|
+
'selectors resolve, skills bind to a catalog that holds them, and the',
|
|
31
|
+
'models it declares have a credential on this machine.',
|
|
32
|
+
'',
|
|
33
|
+
'Unlike a run, it does not stop at the first problem — the report lists',
|
|
34
|
+
'everything it found, each with a code and the fix for it.',
|
|
35
|
+
'',
|
|
36
|
+
'Exit codes: 0 nothing wrong, 3 at least one error (or, with --strict,',
|
|
37
|
+
'at least one warning). --quiet prints the findings and nothing else.',
|
|
38
|
+
],
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
const { values, positionals } = parse(ctx.args, {
|
|
41
|
+
project: { type: 'string' },
|
|
42
|
+
strict: { type: 'boolean' },
|
|
43
|
+
quiet: { type: 'boolean' },
|
|
44
|
+
}, USAGE);
|
|
45
|
+
// A bare directory is accepted so an unregistered folder — a checkout,
|
|
46
|
+
// a scaffold in progress — can be checked at all. `--project` goes
|
|
47
|
+
// through the registry, like everywhere else.
|
|
48
|
+
const here = one(positionals, 'directory', USAGE);
|
|
49
|
+
const dir = here
|
|
50
|
+
? resolve(ctx.cwd, here)
|
|
51
|
+
: await resolveProject({ cwd: ctx.cwd, project: values.project }).then((p) => p.dir);
|
|
52
|
+
// Being listed is the registry's answer, not the directory's, so it is
|
|
53
|
+
// read here and handed to the check rather than looked up inside it.
|
|
54
|
+
const entry = (await Registry.open()).findPath(dir);
|
|
55
|
+
const name = entry?.name ?? basename(dir);
|
|
56
|
+
// Materialised first, so the credential verdicts are the ones a run
|
|
57
|
+
// would reach: a key in the environment and a key in the keyring are
|
|
58
|
+
// the same key by the time the library asks.
|
|
59
|
+
const keys = await KeyStore.open();
|
|
60
|
+
keys.materialize();
|
|
61
|
+
const report = await validateProject({
|
|
62
|
+
dir,
|
|
63
|
+
name,
|
|
64
|
+
registered: entry !== undefined,
|
|
65
|
+
keys,
|
|
66
|
+
});
|
|
67
|
+
if (ctx.json) {
|
|
68
|
+
json(report);
|
|
69
|
+
}
|
|
70
|
+
else if (values.quiet) {
|
|
71
|
+
writeAll(findingLines(report.findings));
|
|
72
|
+
write(verdict(report, Boolean(values.strict)));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
writeAll(render(report));
|
|
76
|
+
}
|
|
77
|
+
const failed = report.counts.errors > 0 || (Boolean(values.strict) && report.counts.warnings > 0);
|
|
78
|
+
if (failed) {
|
|
79
|
+
throw invalidError(`${name}: ${count(report.counts.errors, 'error')}` +
|
|
80
|
+
(values.strict ? `, ${count(report.counts.warnings, 'warning')}` : ''), 'the report above says what to change');
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Rendering
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
function render(report) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const push = (...lines) => {
|
|
90
|
+
out.push(...lines);
|
|
91
|
+
};
|
|
92
|
+
push(`${bold('Project')} ${report.project.name ?? dim('(unregistered)')}`);
|
|
93
|
+
push(...table([
|
|
94
|
+
[' root', report.project.root],
|
|
95
|
+
[' config', report.project.config ?? red('none found')],
|
|
96
|
+
[
|
|
97
|
+
' schema version',
|
|
98
|
+
report.project.version === null ? dim('—') : String(report.project.version),
|
|
99
|
+
],
|
|
100
|
+
[' entry agent', report.project.entry ?? red('undecided')],
|
|
101
|
+
[' registered', report.project.registered ? green('yes') : yellow('not in zen list')],
|
|
102
|
+
]));
|
|
103
|
+
if (report.project.shadowed.length) {
|
|
104
|
+
push(` ${yellow('also present')} ${dim(report.project.shadowed.join(', '))}`);
|
|
105
|
+
}
|
|
106
|
+
// Files -----------------------------------------------------------------
|
|
107
|
+
push('', `${bold('Files')} ${dim(`${report.files.length} checked`)}`);
|
|
108
|
+
push(...table(report.files.map((f) => [
|
|
109
|
+
` ${f.exists ? green('ok') : f.required ? red('MISSING') : yellow('absent')}`,
|
|
110
|
+
f.path + (f.kind === 'directory' ? '/' : ''),
|
|
111
|
+
dim(f.exists && f.bytes !== undefined ? size(f.bytes) : ''),
|
|
112
|
+
dim(f.from ? `${f.role} — named by ${f.from}` : f.role),
|
|
113
|
+
])));
|
|
114
|
+
// Agents ----------------------------------------------------------------
|
|
115
|
+
push('', `${bold('Agents')} ${dim(`${report.agents.length} declared, entry is ${report.project.entry ?? '—'}`)}`);
|
|
116
|
+
for (const agent of report.agents) {
|
|
117
|
+
push(...agentBlock(agent));
|
|
118
|
+
}
|
|
119
|
+
// Skills ----------------------------------------------------------------
|
|
120
|
+
push('', `${bold('Skills')} ${dim(report.skills.dirs.length
|
|
121
|
+
? `${count(report.skills.entries.length, 'skill')} in ${report.skills.dirs.join(', ')}`
|
|
122
|
+
: 'no catalog')}`);
|
|
123
|
+
if (report.skills.entries.length) {
|
|
124
|
+
push(...table(report.skills.entries.map((s) => [
|
|
125
|
+
` ${s.name}`,
|
|
126
|
+
dim(s.path),
|
|
127
|
+
dim(s.tools?.length ? `unlocks ${s.tools.join(' ')}` : ''),
|
|
128
|
+
dim(s.usedBy.length ? `used by ${s.usedBy.join(', ')}` : 'unused'),
|
|
129
|
+
])));
|
|
130
|
+
}
|
|
131
|
+
// Models ----------------------------------------------------------------
|
|
132
|
+
push('', bold('Models'));
|
|
133
|
+
if (report.models.length) {
|
|
134
|
+
push(...table(report.models.map((m) => [
|
|
135
|
+
` ${m.name}`,
|
|
136
|
+
dim(m.provider ? `${m.provider} (${m.kind})` : red('unresolved')),
|
|
137
|
+
dim(m.env ?? ''),
|
|
138
|
+
credential(m.credential),
|
|
139
|
+
// Nothing consumes an embedding yet, so `usedBy` would
|
|
140
|
+
// always read "declared, unused" and say the wrong thing.
|
|
141
|
+
dim(m.role === 'embedding'
|
|
142
|
+
? 'embedding'
|
|
143
|
+
: m.usedBy.length
|
|
144
|
+
? `used by ${m.usedBy.join(', ')}`
|
|
145
|
+
: 'declared, unused'),
|
|
146
|
+
])));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
push(` ${dim('none declared')}`);
|
|
150
|
+
}
|
|
151
|
+
if (report.providers.length) {
|
|
152
|
+
push(` ${dim(`providers: ${report.providers.join(', ')}`)}`);
|
|
153
|
+
}
|
|
154
|
+
// Sandbox ---------------------------------------------------------------
|
|
155
|
+
if (report.sandbox.used || report.sandbox.declared) {
|
|
156
|
+
push('', bold('Sandbox'));
|
|
157
|
+
push(...table([
|
|
158
|
+
[' image', report.sandbox.image ?? dim('the default image')],
|
|
159
|
+
[
|
|
160
|
+
' reached by',
|
|
161
|
+
report.sandbox.used
|
|
162
|
+
? 'at least one agent has the shell tools'
|
|
163
|
+
: dim('nothing — the block is declared but no agent can run a command'),
|
|
164
|
+
],
|
|
165
|
+
[' requires', dim('podman on this machine: zen sandbox status')],
|
|
166
|
+
]));
|
|
167
|
+
}
|
|
168
|
+
// Findings --------------------------------------------------------------
|
|
169
|
+
push('', `${bold('Findings')} ${dim(tallyLine(report))}`);
|
|
170
|
+
const lines = findingLines(report.findings);
|
|
171
|
+
push(...(lines.length ? lines : [` ${green('nothing to report')}`]));
|
|
172
|
+
push('', verdict(report, false));
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function agentBlock(agent) {
|
|
176
|
+
const head = ` ${bold(agent.name)}${agent.entry ? ` ${green('(entry)')}` : ''}`;
|
|
177
|
+
const rows = [];
|
|
178
|
+
if (agent.description) {
|
|
179
|
+
rows.push([' description', agent.description]);
|
|
180
|
+
}
|
|
181
|
+
rows.push([
|
|
182
|
+
' model',
|
|
183
|
+
agent.model
|
|
184
|
+
? `${cyan(agent.model)} ${dim(`(${agent.modelSource === 'project' ? 'inherited from the project' : 'pinned here'})`)}`
|
|
185
|
+
: yellow('none — needs `model:` or --model'),
|
|
186
|
+
]);
|
|
187
|
+
rows.push([
|
|
188
|
+
' instructions',
|
|
189
|
+
agent.instructions.length ? agent.instructions.join(' + ') : yellow('none'),
|
|
190
|
+
]);
|
|
191
|
+
rows.push([
|
|
192
|
+
' tools',
|
|
193
|
+
agent.toolSelectors.length
|
|
194
|
+
? `${agent.toolSelectors.join(' ')} ${dim(`→ ${agent.tools.length}: ${agent.tools.join(' ') || 'nothing'}`)}`
|
|
195
|
+
: dim('none'),
|
|
196
|
+
]);
|
|
197
|
+
if (agent.handoffs.length) {
|
|
198
|
+
rows.push([' handoffs', `→ ${agent.handoffs.join(', ')}`]);
|
|
199
|
+
}
|
|
200
|
+
if (agent.skills) {
|
|
201
|
+
const bits = [`provider "${agent.skills.provider}"`, `discovery ${agent.skills.discovery}`];
|
|
202
|
+
if (agent.skills.allow) {
|
|
203
|
+
bits.push(`allow: ${agent.skills.allow.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
if (agent.skills.preload) {
|
|
206
|
+
bits.push(`preload: ${agent.skills.preload.join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
rows.push([' skills', bits.join(', ')]);
|
|
209
|
+
}
|
|
210
|
+
if (agent.fork) {
|
|
211
|
+
const bits = [
|
|
212
|
+
agent.fork.agents ? `branches run ${agent.fork.agents.join(', ')}` : 'any agent',
|
|
213
|
+
];
|
|
214
|
+
if (agent.fork.maxBranches) {
|
|
215
|
+
bits.push(`at most ${agent.fork.maxBranches}`);
|
|
216
|
+
}
|
|
217
|
+
rows.push([' fork', bits.join(', ')]);
|
|
218
|
+
}
|
|
219
|
+
if (agent.ownSandbox) {
|
|
220
|
+
rows.push([' sandbox', 'overrides the project container']);
|
|
221
|
+
}
|
|
222
|
+
return [head, ...table(rows)];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* One finding, three lines at most: what and where, then why, then the fix.
|
|
226
|
+
* The code is printed because it is the part a script — or a model asked to
|
|
227
|
+
* fix this — can match on without parsing prose.
|
|
228
|
+
*/
|
|
229
|
+
function findingLines(findings) {
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const f of order(findings)) {
|
|
232
|
+
out.push(` ${label(f.severity)} ${bold(f.where)} ${dim(`[${f.code}]`)}`);
|
|
233
|
+
out.push(` ${f.message}`);
|
|
234
|
+
if (f.fix) {
|
|
235
|
+
out.push(` ${dim(`fix: ${f.fix}`)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
const RANK = { error: 0, warning: 1, note: 2 };
|
|
241
|
+
function order(findings) {
|
|
242
|
+
return [...findings].sort((a, b) => RANK[a.severity] - RANK[b.severity]);
|
|
243
|
+
}
|
|
244
|
+
function label(severity) {
|
|
245
|
+
if (severity === 'error') {
|
|
246
|
+
return red('error ');
|
|
247
|
+
}
|
|
248
|
+
return severity === 'warning' ? yellow('warning') : dim('note ');
|
|
249
|
+
}
|
|
250
|
+
function credential(state) {
|
|
251
|
+
if (state === 'present') {
|
|
252
|
+
return green('present');
|
|
253
|
+
}
|
|
254
|
+
if (state === 'missing') {
|
|
255
|
+
return red('missing');
|
|
256
|
+
}
|
|
257
|
+
return state === 'rejected' ? red('rejected') : dim('unchecked');
|
|
258
|
+
}
|
|
259
|
+
function tallyLine(report) {
|
|
260
|
+
const { errors, warnings, notes } = report.counts;
|
|
261
|
+
return `${count(errors, 'error')}, ${count(warnings, 'warning')}, ${count(notes, 'note')}`;
|
|
262
|
+
}
|
|
263
|
+
/** The one line a caller that reads nothing else should read. */
|
|
264
|
+
function verdict(report, strict) {
|
|
265
|
+
if (report.counts.errors > 0) {
|
|
266
|
+
return red(`${count(report.counts.errors, 'error')} — this project will not load. ` +
|
|
267
|
+
'Fix them and run `zen check` again.');
|
|
268
|
+
}
|
|
269
|
+
if (strict && report.counts.warnings > 0) {
|
|
270
|
+
return yellow(`${count(report.counts.warnings, 'warning')} — the project loads, but --strict ` +
|
|
271
|
+
'treats these as failures.');
|
|
272
|
+
}
|
|
273
|
+
if (report.counts.warnings > 0) {
|
|
274
|
+
return yellow(`The project loads. ${count(report.counts.warnings, 'warning')} worth reading above.`);
|
|
275
|
+
}
|
|
276
|
+
return green('The project loads, and nothing about it looks wrong.');
|
|
277
|
+
}
|
|
278
|
+
function size(bytes) {
|
|
279
|
+
if (bytes < 1024) {
|
|
280
|
+
return `${bytes} B`;
|
|
281
|
+
}
|
|
282
|
+
if (bytes < 1024 * 1024) {
|
|
283
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
284
|
+
}
|
|
285
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BannerText } from '../banner.ts';
|
|
2
|
+
import type { Command } from '../command.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Insertion order is the order help prints in, and it is deliberate: the four
|
|
5
|
+
* a new user needs first, then the two about credentials and models, then the
|
|
6
|
+
* two that are only ever run on purpose.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMMANDS: Record<string, Command>;
|
|
9
|
+
/** Names that are not listed in help but still work. */
|
|
10
|
+
export declare const ALIASES: Record<string, string>;
|
|
11
|
+
export interface External {
|
|
12
|
+
/** the package to import `<pkg>/command` from */
|
|
13
|
+
readonly package: string;
|
|
14
|
+
readonly summary: string;
|
|
15
|
+
readonly usage: string;
|
|
16
|
+
/** what to tell someone who has not got it */
|
|
17
|
+
readonly install: string;
|
|
18
|
+
/** printed instead of the `zen` banner, so the sub-brand shows through */
|
|
19
|
+
readonly banner?: BannerText;
|
|
20
|
+
}
|
|
21
|
+
export declare const EXTERNAL: Record<string, External>;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { check } from "./check.js";
|
|
2
|
+
import { init } from "./init.js";
|
|
3
|
+
import { inspect } from "./inspect.js";
|
|
4
|
+
import { key } from "./key.js";
|
|
5
|
+
import { list } from "./list.js";
|
|
6
|
+
import { models } from "./models.js";
|
|
7
|
+
import { open } from "./open.js";
|
|
8
|
+
import { run } from "./run.js";
|
|
9
|
+
import { sandbox } from "./sandbox.js";
|
|
10
|
+
import { version } from "./version.js";
|
|
11
|
+
/**
|
|
12
|
+
* Insertion order is the order help prints in, and it is deliberate: the four
|
|
13
|
+
* a new user needs first, then the two about credentials and models, then the
|
|
14
|
+
* two that are only ever run on purpose.
|
|
15
|
+
*/
|
|
16
|
+
export const COMMANDS = {
|
|
17
|
+
init,
|
|
18
|
+
list,
|
|
19
|
+
run,
|
|
20
|
+
open,
|
|
21
|
+
key,
|
|
22
|
+
models,
|
|
23
|
+
check,
|
|
24
|
+
inspect,
|
|
25
|
+
sandbox,
|
|
26
|
+
version,
|
|
27
|
+
};
|
|
28
|
+
/** Names that are not listed in help but still work. */
|
|
29
|
+
export const ALIASES = {
|
|
30
|
+
ls: 'list',
|
|
31
|
+
new: 'init',
|
|
32
|
+
keys: 'key',
|
|
33
|
+
validate: 'check',
|
|
34
|
+
doctor: 'check',
|
|
35
|
+
report: 'inspect',
|
|
36
|
+
edit: 'open',
|
|
37
|
+
code: 'open',
|
|
38
|
+
mock: 'faker',
|
|
39
|
+
};
|
|
40
|
+
export const EXTERNAL = {
|
|
41
|
+
faker: {
|
|
42
|
+
package: '@zenera/faker',
|
|
43
|
+
summary: 'A mock API from an openapi/swagger document.',
|
|
44
|
+
usage: 'zen faker <serve|build|cache> [spec...]',
|
|
45
|
+
install: 'npm i -g @zenera/faker',
|
|
46
|
+
banner: { head: 'Zenera', accent: 'Faker', subtitle: 'Mock API Server' },
|
|
47
|
+
},
|
|
48
|
+
rag: {
|
|
49
|
+
package: '@zenera/rag',
|
|
50
|
+
summary: 'Search an openapi/swagger document as a graph.',
|
|
51
|
+
usage: 'zen rag schema <index|search|show|stats> [spec...]',
|
|
52
|
+
install: 'npm i -g @zenera/rag',
|
|
53
|
+
banner: { head: 'Zenera', accent: 'Rag', subtitle: 'Api Retrieval' },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { one, parse } from "../args.js";
|
|
4
|
+
import { ensureHome } from "../home.js";
|
|
5
|
+
import { isProvider, keyId, KeyStore, PROVIDERS, SHAPES } from "../keys.js";
|
|
6
|
+
import { probeAll } from "../liveness.js";
|
|
7
|
+
import { isProjectDir, Registry } from "../projects.js";
|
|
8
|
+
import { scaffold } from "../scaffold.js";
|
|
9
|
+
import { bold, cyan, dim, green, invalidError, json, note, progress, usageError, write, yellow, } from "../term.js";
|
|
10
|
+
const USAGE = 'zen init [dir] [--name <name>] [--model <ref>] [--force]';
|
|
11
|
+
/**
|
|
12
|
+
* The model each provider gets scaffolded with, when it is the one chosen.
|
|
13
|
+
*
|
|
14
|
+
* Every ref names its provider. A bare id is not a vendor hint — the shorthand
|
|
15
|
+
* reads the first segment as a *provider name*, so an unprefixed
|
|
16
|
+
* `gemini-3.5-flash` resolves to the default provider and the project asks
|
|
17
|
+
* OpenAI for a Google model.
|
|
18
|
+
*
|
|
19
|
+
* Only OpenAI carries options, because it is the only vendor whose reasoning is
|
|
20
|
+
* silent unless asked for: Gemini's `includeThoughts` is on by default and
|
|
21
|
+
* Anthropic streams nothing to summarise. Without them a gpt-5 project sits
|
|
22
|
+
* there showing no progress while the model thinks, which reads as a hang.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_MODEL = {
|
|
25
|
+
openai: {
|
|
26
|
+
ref: 'openai:gpt-5.4-mini',
|
|
27
|
+
options: [
|
|
28
|
+
'# Reasoning and tools only meet on the responses API — chat',
|
|
29
|
+
'# completions, which is the default, rejects the two together.',
|
|
30
|
+
'api: responses',
|
|
31
|
+
'# gpt-5 models reason whether or not you ask. `reasoningSummary` is',
|
|
32
|
+
'# what makes that visible while it happens; drop it for silence, or',
|
|
33
|
+
'# lower the effort for shorter, cheaper turns.',
|
|
34
|
+
'reasoningEffort: medium',
|
|
35
|
+
'reasoningSummary: auto',
|
|
36
|
+
].join('\n'),
|
|
37
|
+
},
|
|
38
|
+
anthropic: { ref: 'anthropic:claude-sonnet-4-5' },
|
|
39
|
+
google: { ref: 'google:gemini-3.5-flash' },
|
|
40
|
+
vertex: { ref: 'vertex:gemini-3.5-flash' },
|
|
41
|
+
openrouter: { ref: 'openrouter:inclusionai/ling-3.0-flash-fin:free' },
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The provider this machine can actually reach.
|
|
45
|
+
*
|
|
46
|
+
* Holding a key is not the same as holding a working one, so stored
|
|
47
|
+
* credentials are asked rather than counted: guessing OpenAI when only a
|
|
48
|
+
* revoked OpenAI key exists produces a project that scaffolds cleanly and
|
|
49
|
+
* fails on its first run, which is the worst possible moment to find out. A
|
|
50
|
+
* key that came from the environment is taken at its word — the user set it
|
|
51
|
+
* deliberately, and there is no entry to record a verdict against.
|
|
52
|
+
*
|
|
53
|
+
* `dead` is a verdict; `unknown` only means the provider could not be asked, so
|
|
54
|
+
* it is still worth scaffolding around rather than refusing to choose because
|
|
55
|
+
* the wifi is down.
|
|
56
|
+
*/
|
|
57
|
+
async function reachableProvider(store) {
|
|
58
|
+
const fromEnv = PROVIDERS.find((p) => process.env[SHAPES[p].env]);
|
|
59
|
+
if (fromEnv) {
|
|
60
|
+
return fromEnv;
|
|
61
|
+
}
|
|
62
|
+
const active = PROVIDERS.map((p) => store.active(p)).filter((e) => e !== undefined);
|
|
63
|
+
if (active.length === 0) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const bar = progress();
|
|
67
|
+
const checks = await probeAll(store, active, (entry, index, total) => bar.update(dim(`checking ${keyId(entry)} … ${index + 1}/${total}`)));
|
|
68
|
+
bar.done();
|
|
69
|
+
for (const [entry, check] of checks) {
|
|
70
|
+
store.record(entry, check);
|
|
71
|
+
}
|
|
72
|
+
store.save();
|
|
73
|
+
const chosen = checks.find(([, c]) => c.state === 'live') ?? checks.find(([, c]) => c.state === 'unknown');
|
|
74
|
+
// `active` was built from PROVIDERS, so this only ever holds a model
|
|
75
|
+
// provider; the guard says so to the type system rather than a cast
|
|
76
|
+
// asserting it, which would survive `SERVICES` growing into this list.
|
|
77
|
+
const provider = chosen?.[0].provider;
|
|
78
|
+
return provider !== undefined && isProvider(provider) ? provider : undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether this machine can reach the web tools.
|
|
82
|
+
*
|
|
83
|
+
* Presence, not liveness: unlike the model, nothing about the project depends
|
|
84
|
+
* on the key working — an agent with a dead Exa key is an agent one tool call
|
|
85
|
+
* poorer, not one that cannot run — so it is not worth a round trip during
|
|
86
|
+
* `init`.
|
|
87
|
+
*/
|
|
88
|
+
function hasExa(store) {
|
|
89
|
+
return Boolean(process.env[SHAPES.exa.env]) || store.active('exa') !== undefined;
|
|
90
|
+
}
|
|
91
|
+
export const init = {
|
|
92
|
+
summary: 'Create a project here, or in <dir>, and register it.',
|
|
93
|
+
usage: USAGE,
|
|
94
|
+
details: [
|
|
95
|
+
'Writes INSTRUCTIONS.md, agents.yaml and agents/, then records the',
|
|
96
|
+
'directory so `zen list` and `zen open` can find it by name. Editor files',
|
|
97
|
+
'(.vscode/settings.json and the .github/ tree) are written',
|
|
98
|
+
'alongside, replacing any already there.',
|
|
99
|
+
'',
|
|
100
|
+
'The default agent gets the file tools and a sandboxed shell, plus',
|
|
101
|
+
'`exa:*` when the keyring holds an Exa key. Without --model, the',
|
|
102
|
+
'keyring is checked and the model is picked from a credential the',
|
|
103
|
+
'provider accepts.',
|
|
104
|
+
],
|
|
105
|
+
run: async (ctx) => {
|
|
106
|
+
const { values, positionals } = parse(ctx.args, {
|
|
107
|
+
name: { type: 'string' },
|
|
108
|
+
model: { type: 'string' },
|
|
109
|
+
force: { type: 'boolean' },
|
|
110
|
+
}, USAGE);
|
|
111
|
+
const dir = resolve(ctx.cwd, one(positionals, 'directory', USAGE) ?? '.');
|
|
112
|
+
const name = values.name ?? basename(dir);
|
|
113
|
+
if (isProjectDir(dir)) {
|
|
114
|
+
throw invalidError(`${dir} is already a project`);
|
|
115
|
+
}
|
|
116
|
+
if (existsSync(dir) && readdirSync(dir).length > 0 && !values.force) {
|
|
117
|
+
throw usageError(`${dir} is not empty`, 'pass --force to write into it anyway');
|
|
118
|
+
}
|
|
119
|
+
ensureHome();
|
|
120
|
+
const store = await KeyStore.open();
|
|
121
|
+
const provider = values.model ? undefined : await reachableProvider(store);
|
|
122
|
+
// An explicit --model is taken as written, options and all: guessing at
|
|
123
|
+
// knobs for a model nobody here has heard of is how a scaffold breaks.
|
|
124
|
+
const choice = values.model
|
|
125
|
+
? { ref: values.model }
|
|
126
|
+
: DEFAULT_MODEL[provider ?? 'openai'];
|
|
127
|
+
const model = choice.ref;
|
|
128
|
+
const web = hasExa(store);
|
|
129
|
+
const files = scaffold({ dir, model, modelOptions: choice.options, web });
|
|
130
|
+
const registry = await Registry.open();
|
|
131
|
+
registry.add(name, dir);
|
|
132
|
+
registry.save();
|
|
133
|
+
if (ctx.json) {
|
|
134
|
+
json({ name, path: dir, model, files, credential: provider ?? null, web });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
note(`${green('created')} ${bold(name)} ${dim(dir)}`);
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
note(` ${dim(file)}`);
|
|
140
|
+
}
|
|
141
|
+
note();
|
|
142
|
+
if (web) {
|
|
143
|
+
note(`${green('exa key found')} ${dim('— the default agent gets web search')}`);
|
|
144
|
+
note();
|
|
145
|
+
}
|
|
146
|
+
// Said once, here, rather than left for the first run to discover: the
|
|
147
|
+
// project names a model, and nothing on this machine can pay for it.
|
|
148
|
+
if (!values.model && !provider) {
|
|
149
|
+
note(`${yellow('no working key')} ${dim(`— ${model} will not run yet`)}`);
|
|
150
|
+
note(` ${cyan('zen key add openai')} ${dim('(or anthropic, google, vertex)')}`);
|
|
151
|
+
note();
|
|
152
|
+
}
|
|
153
|
+
note(`Next: ${cyan(`cd ${dir}`)} then ${cyan('zen run')}`);
|
|
154
|
+
write(dir);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=init.js.map
|