@zenera/cli 1.1.0 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -11
- package/dist/audit.d.ts +8 -6
- package/dist/audit.js +14 -22
- package/dist/commands/check.js +79 -19
- package/dist/commands/init.js +71 -11
- package/dist/commands/key.js +126 -36
- package/dist/commands/models.js +3 -3
- package/dist/commands/open.js +2 -2
- package/dist/commands/run.js +3 -0
- package/dist/commands/sandbox.js +226 -22
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +10 -2
- package/dist/image.d.ts +16 -0
- package/dist/image.js +85 -0
- package/dist/keys.d.ts +95 -12
- package/dist/keys.js +175 -34
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +2 -2
- package/dist/liveness.d.ts +16 -6
- package/dist/liveness.js +74 -23
- package/dist/main.js +0 -0
- package/dist/podman.d.ts +57 -1
- package/dist/podman.js +177 -12
- package/dist/projects.d.ts +18 -0
- package/dist/projects.js +60 -1
- package/dist/sandbox.d.ts +14 -1
- package/dist/sandbox.js +88 -8
- package/dist/scaffold.d.ts +21 -15
- package/dist/scaffold.js +133 -167
- package/dist/term.d.ts +2 -0
- package/dist/term.js +14 -0
- package/dist/validate.d.ts +20 -3
- package/dist/validate.js +309 -14
- package/package.json +2 -18
- package/templates/{.github → editor/.github}/copilot-instructions.md +161 -48
- package/templates/{.github → editor/.github}/prompts/new-skill.prompt.md +13 -6
- package/templates/editor/.github/skills/api-schema-index/SKILL.md +292 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +74 -0
- package/templates/editor/.github/skills/zen-cli/references/check.md +92 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +111 -0
- package/templates/editor/.github/skills/zen-cli/references/frame.md +119 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/keys.md +114 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +99 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +159 -0
- package/templates/editor/.github/skills/zen-cli/references/run.md +104 -0
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +91 -0
- package/templates/editor/.vscode/settings.json +6 -0
- package/templates/parts/exa.yaml.tmpl +5 -0
- package/templates/parts/model.yaml.tmpl +4 -0
- package/templates/parts/models.yaml.tmpl +10 -0
- package/templates/project/INSTRUCTIONS.md +7 -0
- package/templates/project/SPECIFICATION.md +6 -0
- package/templates/project/agents/prompts/default.md +15 -0
- package/templates/project/agents.yaml.tmpl +44 -0
- package/templates/project/assets/README.md +12 -0
- package/templates/project/gitignore +9 -0
- package/templates/project/sandbox/Dockerfile +21 -0
- package/templates/.github/skills/zen-cli/SKILL.md +0 -110
- /package/templates/{.github → editor/.github}/prompts/new-agent.prompt.md +0 -0
- /package/templates/{.github → editor/.github}/prompts/review-project.prompt.md +0 -0
package/README.md
CHANGED
|
@@ -22,12 +22,11 @@ carries your keys with it.
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
-
Node.js 24+.
|
|
26
|
-
|
|
25
|
+
Node.js 24+. One command — the OpenAI, Anthropic, Google and OpenRouter SDKs
|
|
26
|
+
all ship with the CLI, so any provider works out of the box.
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
npm i -g @zenera/cli
|
|
30
|
-
# or @anthropic-ai/sdk, @google/genai, @openrouter/sdk — any mix of them
|
|
29
|
+
npm i -g @zenera/cli
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
Or without installing anything:
|
|
@@ -41,7 +40,7 @@ npx @zenera/cli --help
|
|
|
41
40
|
Four commands, from nothing to an answer:
|
|
42
41
|
|
|
43
42
|
```sh
|
|
44
|
-
npm i -g @zenera/cli
|
|
43
|
+
npm i -g @zenera/cli # every vendor SDK comes with it
|
|
45
44
|
zen key add openai # prompts with the echo off; stored in ~/.zenera
|
|
46
45
|
zen init my-project # scaffolds a project and registers it
|
|
47
46
|
cd my-project && zen run "introduce yourself"
|
|
@@ -199,6 +198,87 @@ command — so `zen run … | jq` is a supported way to use it, not an accident.
|
|
|
199
198
|
|
|
200
199
|
The binary is installed under three names: `zen`, `zn` and `zenera`.
|
|
201
200
|
|
|
201
|
+
## Credentials
|
|
202
|
+
|
|
203
|
+
One keyring serves every provider, and a key goes in the same way whatever it
|
|
204
|
+
is for:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
zen key add <provider> # prompts with the echo off
|
|
208
|
+
zen key add <provider> < key.txt # or pipe it
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The value never comes from argv — a command line lands in `ps`, in shell
|
|
212
|
+
history and in CI logs — so piped stdin and the echo-off prompt are the only
|
|
213
|
+
two ways in. Entries live in `~/.zenera/neo/keys.json` (mode `0600`) and are
|
|
214
|
+
materialised into the environment just before a run, so a real environment
|
|
215
|
+
variable always wins and a project checked out on a machine without `zen` still
|
|
216
|
+
runs.
|
|
217
|
+
|
|
218
|
+
| Provider | The value is | Exported as |
|
|
219
|
+
| ------------ | ------------------------------------- | -------------------------------- |
|
|
220
|
+
| `openai` | a secret | `OPENAI_API_KEY` |
|
|
221
|
+
| `anthropic` | a secret | `ANTHROPIC_API_KEY` |
|
|
222
|
+
| `google` | a secret — AI Studio | `GEMINI_API_KEY` |
|
|
223
|
+
| `vertex` | a path to a service-account JSON file | `GOOGLE_APPLICATION_CREDENTIALS` |
|
|
224
|
+
| `openrouter` | a secret | `OPENROUTER_API_KEY` |
|
|
225
|
+
| `exa` | a secret — for the search tool | `EXA_API_KEY` |
|
|
226
|
+
|
|
227
|
+
`zen key add` verifies the credential against the provider before it finishes,
|
|
228
|
+
but stores it either way: a key that cannot be checked right now — offline,
|
|
229
|
+
behind a proxy — is not a key that is wrong. `--no-check` skips the call.
|
|
230
|
+
|
|
231
|
+
### Which key a model uses
|
|
232
|
+
|
|
233
|
+
A model reference is `[provider[/api]:]model`, and the first segment names a
|
|
234
|
+
**provider, not a vendor**. So `vertex:gemini-3.5-flash` and
|
|
235
|
+
`google:gemini-3.5-flash` are the same model reached through two different
|
|
236
|
+
services, needing two different credentials — and a bare `gpt-5.4-mini` goes to
|
|
237
|
+
the default provider, `openai`. `zen models` resolves every reference in a
|
|
238
|
+
project against what is stored, and calls nothing.
|
|
239
|
+
|
|
240
|
+
### Vertex AI
|
|
241
|
+
|
|
242
|
+
Vertex accepts either shape, and which one you gave is read off the value.
|
|
243
|
+
|
|
244
|
+
The usual one is a **service-account JSON file** — give its path, not its
|
|
245
|
+
contents:
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
printf '%s' ~/keys/vertex-sa.json | zen key add vertex --location us-central1
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The file is copied into `~/.zenera/neo/keys/` at mode `0600`, so moving or
|
|
252
|
+
cleaning up the original later cannot break it.
|
|
253
|
+
|
|
254
|
+
- `--location <region>` is worth setting. It must be `global` or a **concrete
|
|
255
|
+
region**; multi-region names like `us` are rejected with a 404. `global`
|
|
256
|
+
routes across regions and pays about ten seconds of cold start on the first
|
|
257
|
+
request each process makes — a region answers in about two.
|
|
258
|
+
- `--project <id>` is only needed when the `project_id` inside the file is not
|
|
259
|
+
the project you want.
|
|
260
|
+
|
|
261
|
+
The alternative is an **express-mode API key**, an ordinary secret under
|
|
262
|
+
`VERTEX_API_KEY`. Express mode addresses no project, so `--project` and
|
|
263
|
+
`--location` mean nothing there and are not stored.
|
|
264
|
+
|
|
265
|
+
### More than one key per provider
|
|
266
|
+
|
|
267
|
+
Entries are named, so a provider can hold several and one of them is active:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
zen key add openai/work # a second entry
|
|
271
|
+
zen key use openai/work # which one runs use
|
|
272
|
+
zen key ls --check # everything stored, and whether it still works
|
|
273
|
+
zen key show vertex/default # masked — --reveal prints the secret
|
|
274
|
+
zen key env openai # shell exports, for other tools
|
|
275
|
+
zen key rm openai/work
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`zen key ls` marks the active entry with `*`, and something it found in your
|
|
279
|
+
real environment or in `gcloud`'s ADC with `~`, so it is always clear where a
|
|
280
|
+
working provider actually comes from.
|
|
281
|
+
|
|
202
282
|
## Concepts
|
|
203
283
|
|
|
204
284
|
- **Project** — a named directory holding a complete agent definition and the
|
|
@@ -225,13 +305,10 @@ want the runtime inside your own application rather than on a terminal:
|
|
|
225
305
|
|
|
226
306
|
## Documentation
|
|
227
307
|
|
|
228
|
-
Full specification:
|
|
229
|
-
[packages/cli/DESIGN.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/cli/DESIGN.md).
|
|
230
|
-
Also
|
|
231
|
-
[DESIGN.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/DESIGN.md) ·
|
|
232
308
|
[docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md)
|
|
233
|
-
·
|
|
234
|
-
[docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
|
|
309
|
+
— the folder a project is ·
|
|
310
|
+
[docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
|
|
311
|
+
— every key in the configuration file.
|
|
235
312
|
|
|
236
313
|
## License
|
|
237
314
|
|
package/dist/audit.d.ts
CHANGED
|
@@ -22,13 +22,15 @@ export interface ModelIssue {
|
|
|
22
22
|
* Whether a requirement is actually met, and under which variable.
|
|
23
23
|
*
|
|
24
24
|
* `satisfied` is the library's answer and it is about api keys, which is the
|
|
25
|
-
* wrong question for Vertex: that kind is `keyOptional` because it
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* wrong question for Vertex: that kind is `keyOptional` because it can
|
|
26
|
+
* authenticate from a service-account file instead. So both of its variables
|
|
27
|
+
* are looked for, along with the file `gcloud` leaves behind.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* The variable named back is the one that is actually set, because a report
|
|
30
|
+
* that named `VERTEX_API_KEY` while the audit silently checked
|
|
31
|
+
* `GOOGLE_APPLICATION_CREDENTIALS` would be two answers to one question, and
|
|
32
|
+
* the wrong one is the one people would act on. With nothing set there is no
|
|
33
|
+
* such answer, so it names the form the provider is usually reached by.
|
|
32
34
|
*/
|
|
33
35
|
export declare function credentialFor(need: ModelRequirement): {
|
|
34
36
|
env: string;
|
package/dist/audit.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
1
|
import { projectRegistry, readProjectConfig, } from '@zenera/neo';
|
|
5
|
-
import {
|
|
2
|
+
import { envNames, form, gcloudAdc, isProvider, SHAPES, } from "./keys.js";
|
|
6
3
|
import { bold, dim } from "./term.js";
|
|
7
4
|
/**
|
|
8
5
|
* Every model the project names, keyed by how one would refer to it.
|
|
@@ -36,32 +33,27 @@ function declared(config) {
|
|
|
36
33
|
['embedding', embeddings],
|
|
37
34
|
]);
|
|
38
35
|
}
|
|
39
|
-
/**
|
|
40
|
-
* `gcloud auth application-default login` writes here, and the GenAI SDK finds
|
|
41
|
-
* it with no variable set — so without this check every developer using ADC
|
|
42
|
-
* would be told their working Vertex setup is broken.
|
|
43
|
-
*/
|
|
44
|
-
function hasGcloudAdc() {
|
|
45
|
-
const dir = process.env.CLOUDSDK_CONFIG ?? join(homedir(), '.config', 'gcloud');
|
|
46
|
-
return existsSync(join(dir, 'application_default_credentials.json'));
|
|
47
|
-
}
|
|
48
36
|
/**
|
|
49
37
|
* Whether a requirement is actually met, and under which variable.
|
|
50
38
|
*
|
|
51
39
|
* `satisfied` is the library's answer and it is about api keys, which is the
|
|
52
|
-
* wrong question for Vertex: that kind is `keyOptional` because it
|
|
53
|
-
*
|
|
54
|
-
*
|
|
40
|
+
* wrong question for Vertex: that kind is `keyOptional` because it can
|
|
41
|
+
* authenticate from a service-account file instead. So both of its variables
|
|
42
|
+
* are looked for, along with the file `gcloud` leaves behind.
|
|
55
43
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
44
|
+
* The variable named back is the one that is actually set, because a report
|
|
45
|
+
* that named `VERTEX_API_KEY` while the audit silently checked
|
|
46
|
+
* `GOOGLE_APPLICATION_CREDENTIALS` would be two answers to one question, and
|
|
47
|
+
* the wrong one is the one people would act on. With nothing set there is no
|
|
48
|
+
* such answer, so it names the form the provider is usually reached by.
|
|
59
49
|
*/
|
|
60
50
|
export function credentialFor(need) {
|
|
61
51
|
const provider = isProvider(need.kind) ? need.kind : undefined;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
// A provider with a file form can authenticate without an api key, which is
|
|
53
|
+
// the only thing `satisfied` knows how to look for.
|
|
54
|
+
if (provider && SHAPES[provider].forms.some((f) => f.holds === 'file')) {
|
|
55
|
+
const set = envNames(provider).find((name) => process.env[name]);
|
|
56
|
+
return { env: set ?? form(provider).env, present: Boolean(set) || Boolean(gcloudAdc()) };
|
|
65
57
|
}
|
|
66
58
|
return { env: need.apiKeyEnv, present: need.satisfied };
|
|
67
59
|
}
|
package/dist/commands/check.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
2
3
|
import { one, parse } from "../args.js";
|
|
3
4
|
import { KeyStore } from "../keys.js";
|
|
4
5
|
import { Registry } from "../projects.js";
|
|
5
6
|
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 { bold, count, cyan, dim, green, invalidError, json, progress, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
7
8
|
import { validateProject, } from "../validate.js";
|
|
8
|
-
const USAGE = 'zen check [dir] [--project <name|dir>] [--strict] [--quiet]';
|
|
9
|
+
const USAGE = 'zen check [name|dir] [--project <name|dir>] [--no-sandbox] [--strict] [--quiet]';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// zen check
|
|
11
12
|
//
|
|
@@ -16,38 +17,47 @@ const USAGE = 'zen check [dir] [--project <name|dir>] [--strict] [--quiet]';
|
|
|
16
17
|
// stable code and a fix, and the whole thing goes to stdout — it is the answer,
|
|
17
18
|
// not narration.
|
|
18
19
|
//
|
|
19
|
-
// Nothing
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// Nothing is contacted or paid for. The one thing that *runs* is the sandbox:
|
|
21
|
+
// the project's image is built and one command is executed in it, because a
|
|
22
|
+
// Dockerfile that does not build is a broken project and nothing short of
|
|
23
|
+
// building it says so. It happens against a temporary directory, the container
|
|
24
|
+
// is removed on the way out, and `--no-sandbox` skips it — so the report is
|
|
25
|
+
// still worth having on the machine that has no container engine at all.
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
23
27
|
export const check = {
|
|
24
28
|
summary: 'Validate agents.yaml and every file it names, and report in full.',
|
|
25
29
|
usage: USAGE,
|
|
26
30
|
details: [
|
|
27
|
-
'Checks the whole project
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
31
|
+
'Checks the whole project: the configuration parses and satisfies the',
|
|
32
|
+
'schema, every prompt, skill and catalog it names is on disk, hand-offs',
|
|
33
|
+
'and forks name agents that exist, tool selectors resolve, skills bind to',
|
|
34
|
+
'a catalog that holds them, and the models it declares have a credential',
|
|
35
|
+
'on this machine.',
|
|
36
|
+
'',
|
|
37
|
+
'It also builds the sandbox image and runs one command in it, against a',
|
|
38
|
+
'temporary directory rather than your workspace. That is the only thing',
|
|
39
|
+
'it starts, and --no-sandbox skips it. No container engine is a warning,',
|
|
40
|
+
'not an error.',
|
|
32
41
|
'',
|
|
33
42
|
'Unlike a run, it does not stop at the first problem — the report lists',
|
|
34
43
|
'everything it found, each with a code and the fix for it.',
|
|
35
44
|
'',
|
|
45
|
+
'The argument is a directory if one is there and a registered project',
|
|
46
|
+
'name otherwise; with neither, the project you are standing in.',
|
|
47
|
+
'',
|
|
36
48
|
'Exit codes: 0 nothing wrong, 3 at least one error (or, with --strict,',
|
|
37
49
|
'at least one warning). --quiet prints the findings and nothing else.',
|
|
38
50
|
],
|
|
39
51
|
run: async (ctx) => {
|
|
40
52
|
const { values, positionals } = parse(ctx.args, {
|
|
41
53
|
project: { type: 'string' },
|
|
54
|
+
'no-sandbox': { type: 'boolean' },
|
|
42
55
|
strict: { type: 'boolean' },
|
|
43
56
|
quiet: { type: 'boolean' },
|
|
44
57
|
}, USAGE);
|
|
45
|
-
|
|
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);
|
|
58
|
+
const here = one(positionals, 'project or directory', USAGE);
|
|
49
59
|
const dir = here
|
|
50
|
-
?
|
|
60
|
+
? await locate(ctx.cwd, here)
|
|
51
61
|
: await resolveProject({ cwd: ctx.cwd, project: values.project }).then((p) => p.dir);
|
|
52
62
|
// Being listed is the registry's answer, not the directory's, so it is
|
|
53
63
|
// read here and handed to the check rather than looked up inside it.
|
|
@@ -58,12 +68,18 @@ export const check = {
|
|
|
58
68
|
// the same key by the time the library asks.
|
|
59
69
|
const keys = await KeyStore.open();
|
|
60
70
|
keys.materialize();
|
|
71
|
+
const bar = progress();
|
|
61
72
|
const report = await validateProject({
|
|
62
73
|
dir,
|
|
63
74
|
name,
|
|
64
75
|
registered: entry !== undefined,
|
|
65
76
|
keys,
|
|
77
|
+
sandbox: {
|
|
78
|
+
enabled: !values['no-sandbox'],
|
|
79
|
+
onProgress: (what) => bar.update(dim(what)),
|
|
80
|
+
},
|
|
66
81
|
});
|
|
82
|
+
bar.done();
|
|
67
83
|
if (ctx.json) {
|
|
68
84
|
json(report);
|
|
69
85
|
}
|
|
@@ -81,6 +97,32 @@ export const check = {
|
|
|
81
97
|
}
|
|
82
98
|
},
|
|
83
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* What a bare argument means: a directory when one is there, a registered name
|
|
102
|
+
* otherwise. A directory is tried first, and it does not have to be a project
|
|
103
|
+
* yet — an unregistered folder, a checkout, a scaffold in progress is exactly
|
|
104
|
+
* what there is to check.
|
|
105
|
+
*
|
|
106
|
+
* A word that is neither is a usage error and stops here. The check itself
|
|
107
|
+
* would answer it too, but it would answer at the length of a full report, and
|
|
108
|
+
* a page of empty sections about a directory that does not exist buries the one
|
|
109
|
+
* line that matters: there is nothing by that name.
|
|
110
|
+
*/
|
|
111
|
+
async function locate(cwd, arg) {
|
|
112
|
+
const at = resolve(cwd, arg);
|
|
113
|
+
if (existsSync(at) && statSync(at).isDirectory()) {
|
|
114
|
+
return at;
|
|
115
|
+
}
|
|
116
|
+
const entry = (await Registry.open()).find(arg);
|
|
117
|
+
if (!entry) {
|
|
118
|
+
throw usageError(`no project or directory named "${arg}"`, 'see what is registered: zen list');
|
|
119
|
+
}
|
|
120
|
+
const path = resolve(entry.path);
|
|
121
|
+
if (!existsSync(path)) {
|
|
122
|
+
throw usageError(`project "${entry.name}" is registered at ${path}, which is gone`, 'forget it: zen list --prune');
|
|
123
|
+
}
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
84
126
|
// ---------------------------------------------------------------------------
|
|
85
127
|
// Rendering
|
|
86
128
|
// ---------------------------------------------------------------------------
|
|
@@ -121,12 +163,22 @@ function render(report) {
|
|
|
121
163
|
? `${count(report.skills.entries.length, 'skill')} in ${report.skills.dirs.join(', ')}`
|
|
122
164
|
: 'no catalog')}`);
|
|
123
165
|
if (report.skills.entries.length) {
|
|
124
|
-
|
|
166
|
+
// A folder skill's own files travel with it: they are mounted at
|
|
167
|
+
// /skills/<folder> and the body may point the model straight at them,
|
|
168
|
+
// so say what is there and under which name.
|
|
169
|
+
const rows = table(report.skills.entries.map((s) => [
|
|
125
170
|
` ${s.name}`,
|
|
126
171
|
dim(s.path),
|
|
127
172
|
dim(s.tools?.length ? `unlocks ${s.tools.join(' ')}` : ''),
|
|
128
173
|
dim(s.usedBy.length ? `used by ${s.usedBy.join(', ')}` : 'unused'),
|
|
129
|
-
]))
|
|
174
|
+
]));
|
|
175
|
+
report.skills.entries.forEach((s, i) => {
|
|
176
|
+
push(rows[i] ?? '');
|
|
177
|
+
if (s.files?.length) {
|
|
178
|
+
const at = basename(dirname(s.path));
|
|
179
|
+
push(dim(` /skills/${at}/ ${s.files.join(' ')}`));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
130
182
|
}
|
|
131
183
|
// Models ----------------------------------------------------------------
|
|
132
184
|
push('', bold('Models'));
|
|
@@ -156,13 +208,21 @@ function render(report) {
|
|
|
156
208
|
push('', bold('Sandbox'));
|
|
157
209
|
push(...table([
|
|
158
210
|
[' image', report.sandbox.image ?? dim('the default image')],
|
|
211
|
+
...(report.sandbox.dockerfile
|
|
212
|
+
? [[' built from', report.sandbox.dockerfile]]
|
|
213
|
+
: []),
|
|
159
214
|
[
|
|
160
215
|
' reached by',
|
|
161
216
|
report.sandbox.used
|
|
162
217
|
? 'at least one agent has the shell tools'
|
|
163
218
|
: dim('nothing — the block is declared but no agent can run a command'),
|
|
164
219
|
],
|
|
165
|
-
[
|
|
220
|
+
[
|
|
221
|
+
' tried',
|
|
222
|
+
report.sandbox.probed
|
|
223
|
+
? green('built, started, and a command ran in it')
|
|
224
|
+
: dim('no — nothing was built or started'),
|
|
225
|
+
],
|
|
166
226
|
]));
|
|
167
227
|
}
|
|
168
228
|
// Findings --------------------------------------------------------------
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { readProjectConfig } from '@zenera/neo';
|
|
1
2
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
3
|
import { basename, resolve } from 'node:path';
|
|
3
4
|
import { one, parse } from "../args.js";
|
|
4
5
|
import { ensureHome } from "../home.js";
|
|
5
|
-
import {
|
|
6
|
+
import { resolveBuild } from "../image.js";
|
|
7
|
+
import { envNames, isProvider, keyId, KeyStore, PROVIDERS } from "../keys.js";
|
|
6
8
|
import { probeAll } from "../liveness.js";
|
|
9
|
+
import { ensurePodmanReady } from "../podman.js";
|
|
7
10
|
import { isProjectDir, Registry } from "../projects.js";
|
|
8
11
|
import { scaffold } from "../scaffold.js";
|
|
9
12
|
import { bold, cyan, dim, green, invalidError, json, note, progress, usageError, write, yellow, } from "../term.js";
|
|
@@ -55,7 +58,7 @@ const DEFAULT_MODEL = {
|
|
|
55
58
|
* the wifi is down.
|
|
56
59
|
*/
|
|
57
60
|
async function reachableProvider(store) {
|
|
58
|
-
const fromEnv = PROVIDERS.find((p) => process.env[
|
|
61
|
+
const fromEnv = PROVIDERS.find((p) => envNames(p).some((name) => process.env[name]));
|
|
59
62
|
if (fromEnv) {
|
|
60
63
|
return fromEnv;
|
|
61
64
|
}
|
|
@@ -64,7 +67,8 @@ async function reachableProvider(store) {
|
|
|
64
67
|
return undefined;
|
|
65
68
|
}
|
|
66
69
|
const bar = progress();
|
|
67
|
-
|
|
70
|
+
bar.update(dim(`checking ${active.length} key${active.length === 1 ? '' : 's'} …`));
|
|
71
|
+
const checks = await probeAll(store, active, (entry, done, total) => bar.update(dim(`checked ${keyId(entry)} … ${done}/${total}`)));
|
|
68
72
|
bar.done();
|
|
69
73
|
for (const [entry, check] of checks) {
|
|
70
74
|
store.record(entry, check);
|
|
@@ -86,7 +90,7 @@ async function reachableProvider(store) {
|
|
|
86
90
|
* `init`.
|
|
87
91
|
*/
|
|
88
92
|
function hasExa(store) {
|
|
89
|
-
return
|
|
93
|
+
return envNames('exa').some((name) => process.env[name]) || store.active('exa') !== undefined;
|
|
90
94
|
}
|
|
91
95
|
export const init = {
|
|
92
96
|
summary: 'Create a project here, or in <dir>, and register it.',
|
|
@@ -101,6 +105,9 @@ export const init = {
|
|
|
101
105
|
'`exa:*` when the keyring holds an Exa key. Without --model, the',
|
|
102
106
|
'keyring is checked and the model is picked from a credential the',
|
|
103
107
|
'provider accepts.',
|
|
108
|
+
'',
|
|
109
|
+
'The sandbox image is built here too, so the first run does not have',
|
|
110
|
+
'to. A machine without a container engine is told, and carries on.',
|
|
104
111
|
],
|
|
105
112
|
run: async (ctx) => {
|
|
106
113
|
const { values, positionals } = parse(ctx.args, {
|
|
@@ -126,19 +133,31 @@ export const init = {
|
|
|
126
133
|
: DEFAULT_MODEL[provider ?? 'openai'];
|
|
127
134
|
const model = choice.ref;
|
|
128
135
|
const web = hasExa(store);
|
|
129
|
-
const
|
|
136
|
+
const written = scaffold({ dir, model, modelOptions: choice.options, web });
|
|
130
137
|
const registry = await Registry.open();
|
|
131
138
|
registry.add(name, dir);
|
|
132
139
|
registry.save();
|
|
140
|
+
if (!ctx.json) {
|
|
141
|
+
note(`${green('created')} ${bold(name)} ${dim(dir)}`);
|
|
142
|
+
for (const file of written.files.filter((f) => !f.startsWith('.'))) {
|
|
143
|
+
note(` ${dim(file)}`);
|
|
144
|
+
}
|
|
145
|
+
note();
|
|
146
|
+
}
|
|
147
|
+
const sandbox = await prepareSandbox(dir, ctx.json);
|
|
133
148
|
if (ctx.json) {
|
|
134
|
-
json({
|
|
149
|
+
json({
|
|
150
|
+
name,
|
|
151
|
+
path: dir,
|
|
152
|
+
model,
|
|
153
|
+
files: written.files,
|
|
154
|
+
editor: written.editor,
|
|
155
|
+
credential: provider ?? null,
|
|
156
|
+
web,
|
|
157
|
+
sandbox,
|
|
158
|
+
});
|
|
135
159
|
return;
|
|
136
160
|
}
|
|
137
|
-
note(`${green('created')} ${bold(name)} ${dim(dir)}`);
|
|
138
|
-
for (const file of files) {
|
|
139
|
-
note(` ${dim(file)}`);
|
|
140
|
-
}
|
|
141
|
-
note();
|
|
142
161
|
if (web) {
|
|
143
162
|
note(`${green('exa key found')} ${dim('— the default agent gets web search')}`);
|
|
144
163
|
note();
|
|
@@ -154,4 +173,45 @@ export const init = {
|
|
|
154
173
|
write(dir);
|
|
155
174
|
},
|
|
156
175
|
};
|
|
176
|
+
/**
|
|
177
|
+
* Builds the sandbox image the project was just given, here rather than on the
|
|
178
|
+
* first run.
|
|
179
|
+
*
|
|
180
|
+
* It has to happen once either way, and the two moments are not equally good:
|
|
181
|
+
* a build during `init` is a command that is visibly setting a project up,
|
|
182
|
+
* while the same minutes during `zen run` land in the middle of a question
|
|
183
|
+
* somebody asked and read as a hung model.
|
|
184
|
+
*
|
|
185
|
+
* Never fatal. A machine with no container engine has a perfectly good project
|
|
186
|
+
* on it — one whose agent cannot start a shell yet — and refusing to finish
|
|
187
|
+
* `init` over that would throw away everything already written.
|
|
188
|
+
*/
|
|
189
|
+
async function prepareSandbox(dir, quiet) {
|
|
190
|
+
let image;
|
|
191
|
+
try {
|
|
192
|
+
const { root, config } = readProjectConfig(dir);
|
|
193
|
+
const build = resolveBuild(root, config.sandbox);
|
|
194
|
+
image = build?.tag ?? config.sandbox?.image;
|
|
195
|
+
if (!build && !image) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
// Nothing to answer: `init` is not the place to be asked whether to
|
|
199
|
+
// install a package manager's worth of software.
|
|
200
|
+
await ensurePodmanReady({ image, build, yes: true });
|
|
201
|
+
if (!quiet) {
|
|
202
|
+
note(`${green('sandbox ready')} ${dim(image ?? '')}`);
|
|
203
|
+
note();
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (!quiet) {
|
|
209
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
210
|
+
note(`${yellow('sandbox not ready')} ${dim(`— ${why}`)}`);
|
|
211
|
+
note(` ${cyan('zen sandbox up')} ${dim('— or the first run will try again')}`);
|
|
212
|
+
note();
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
157
217
|
//# sourceMappingURL=init.js.map
|