@zenera/cli 1.1.2 → 1.1.4
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 +228 -31
- package/dist/audit.d.ts +13 -8
- package/dist/audit.js +21 -24
- package/dist/catalog.d.ts +111 -0
- package/dist/catalog.js +439 -0
- package/dist/commands/check.js +72 -17
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +3 -2
- package/dist/commands/init.js +71 -11
- package/dist/commands/key.js +144 -36
- package/dist/commands/models.d.ts +0 -6
- package/dist/commands/models.js +546 -101
- package/dist/commands/open.js +2 -2
- package/dist/commands/run.js +3 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +1 -0
- package/dist/home.d.ts +2 -0
- package/dist/home.js +2 -0
- package/dist/keys.d.ts +104 -13
- package/dist/keys.js +175 -34
- package/dist/lib.d.ts +2 -1
- package/dist/lib.js +2 -1
- package/dist/liveness.d.ts +48 -6
- package/dist/liveness.js +268 -28
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +58 -7
- package/dist/scaffold.d.ts +21 -21
- package/dist/scaffold.js +132 -204
- package/dist/validate.d.ts +17 -1
- package/dist/validate.js +100 -10
- package/package.json +2 -18
- package/templates/{.github → editor/.github}/copilot-instructions.md +37 -9
- package/templates/editor/.github/skills/api-schema-index/SKILL.md +292 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/check.md +88 -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 +119 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +108 -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/{sandbox → project/sandbox}/Dockerfile +2 -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/new-skill.prompt.md +0 -0
- /package/templates/{.github → editor/.github}/prompts/review-project.prompt.md +0 -0
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
|
package/dist/commands/key.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { parse } from "../args.js";
|
|
3
3
|
import { ensureHome } from "../home.js";
|
|
4
|
-
import { assertNotEmpty, describe, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
4
|
+
import { ambient, ambientId, assertNotEmpty, describe, envNames, envOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
5
5
|
import { probe, probeAll } from "../liveness.js";
|
|
6
6
|
import { ago, ask, askSecret, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
7
7
|
const USAGE = 'zen key <ls|add|use|check|rm|show|env> [ref] [options]';
|
|
@@ -11,40 +11,65 @@ const USAGE = 'zen key <ls|add|use|check|rm|show|env> [ref] [options]';
|
|
|
11
11
|
const MARK = {
|
|
12
12
|
live: green('live'),
|
|
13
13
|
dead: red('dead'),
|
|
14
|
+
blocked: yellow('blocked'),
|
|
14
15
|
unknown: dim('unknown'),
|
|
15
16
|
};
|
|
16
17
|
function state(entry) {
|
|
17
18
|
return entry.check ? MARK[entry.check.state] : dim('unchecked');
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* An ambient credential borrowed into the shape the rest of this file works
|
|
22
|
+
* in. It is not in the store and never will be: `store.find` misses it, so
|
|
23
|
+
* `store.record` drops its check and nothing is written to disk. That is the
|
|
24
|
+
* intent — it can be listed and probed, not chosen or forgotten.
|
|
25
|
+
*/
|
|
26
|
+
function asEntry(cred) {
|
|
27
|
+
return {
|
|
28
|
+
provider: cred.provider,
|
|
29
|
+
name: cred.env ? `$${cred.env}` : 'adc',
|
|
30
|
+
holds: cred.holds,
|
|
31
|
+
value: cred.value,
|
|
32
|
+
env: cred.env,
|
|
33
|
+
addedAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function rows(store, borrowed) {
|
|
20
37
|
const out = [
|
|
21
38
|
[bold(''), bold('KEY'), bold('VALUE'), bold('STATE'), bold('CHECKED')],
|
|
22
39
|
];
|
|
23
40
|
for (const provider of OWNERS) {
|
|
24
41
|
for (const entry of store.for(provider)) {
|
|
25
|
-
const
|
|
42
|
+
const shadow = envNames(provider).find((name) => process.env[name]);
|
|
26
43
|
out.push([
|
|
27
44
|
store.isActive(entry) ? green('*') : ' ',
|
|
28
45
|
keyId(entry),
|
|
29
46
|
dim(describe(store, entry)),
|
|
30
47
|
state(entry),
|
|
31
48
|
dim(entry.check ? ago(entry.check.at) : '—') +
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
(shadow && store.isActive(entry) ? yellow(` shadowed by $${shadow}`) : ''),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
for (const [cred, entry] of borrowed.filter(([c]) => c.provider === provider)) {
|
|
53
|
+
out.push([
|
|
54
|
+
dim('~'),
|
|
55
|
+
dim(ambientId(cred)),
|
|
56
|
+
dim(describe(store, entry)),
|
|
57
|
+
state(entry),
|
|
58
|
+
dim(cred.env ? 'from the environment' : 'from gcloud'),
|
|
35
59
|
]);
|
|
36
60
|
}
|
|
37
61
|
}
|
|
38
62
|
return table(out);
|
|
39
63
|
}
|
|
40
64
|
/**
|
|
41
|
-
* Checking is a network round trip per key, so it says which
|
|
42
|
-
*
|
|
65
|
+
* Checking is a network round trip per key, so it says which ones have come
|
|
66
|
+
* back. Silence for ten seconds is indistinguishable from a hang.
|
|
43
67
|
*/
|
|
44
68
|
async function checkAll(ctx, store, targets) {
|
|
45
69
|
const bar = ctx.json ? undefined : progress();
|
|
70
|
+
bar?.update(dim(`checking ${targets.length} key${targets.length === 1 ? '' : 's'} …`));
|
|
46
71
|
try {
|
|
47
|
-
return await probeAll(store, targets, (entry,
|
|
72
|
+
return await probeAll(store, targets, (entry, done, total) => bar?.update(dim(`checked ${keyId(entry)} … ${done}/${total}`)));
|
|
48
73
|
}
|
|
49
74
|
finally {
|
|
50
75
|
bar?.done();
|
|
@@ -53,29 +78,47 @@ async function checkAll(ctx, store, targets) {
|
|
|
53
78
|
const ls = async (ctx, args) => {
|
|
54
79
|
const { values } = parse(args, { check: { type: 'boolean' } }, USAGE);
|
|
55
80
|
const store = await KeyStore.open();
|
|
81
|
+
const borrowed = ambient(store).map((cred) => [cred, asEntry(cred)]);
|
|
56
82
|
if (values.check) {
|
|
57
|
-
|
|
83
|
+
// Ambient credentials are checked alongside the stored ones and, unlike
|
|
84
|
+
// them, forgotten again: there is nowhere to write the result, and a
|
|
85
|
+
// variable that changes between runs would make a cached one a lie.
|
|
86
|
+
const checks = await checkAll(ctx, store, [
|
|
87
|
+
...store.entries,
|
|
88
|
+
...borrowed.map(([, entry]) => entry),
|
|
89
|
+
]);
|
|
58
90
|
for (const [entry, check] of checks) {
|
|
59
91
|
store.record(entry, check);
|
|
92
|
+
entry.check = check;
|
|
60
93
|
}
|
|
61
94
|
store.save();
|
|
62
95
|
}
|
|
63
96
|
if (ctx.json) {
|
|
64
|
-
json(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
json({
|
|
98
|
+
keys: store.entries.map((e) => ({
|
|
99
|
+
...e,
|
|
100
|
+
value: e.holds === 'file' ? store.fileOf(e) : mask(e.value),
|
|
101
|
+
active: store.isActive(e),
|
|
102
|
+
env: envOf(e),
|
|
103
|
+
})),
|
|
104
|
+
ambient: borrowed.map(([cred, entry]) => ({
|
|
105
|
+
provider: cred.provider,
|
|
106
|
+
id: ambientId(cred),
|
|
107
|
+
env: cred.env ?? null,
|
|
108
|
+
holds: cred.holds,
|
|
109
|
+
value: cred.holds === 'file' ? cred.value : mask(cred.value),
|
|
110
|
+
check: entry.check,
|
|
111
|
+
})),
|
|
112
|
+
});
|
|
70
113
|
return;
|
|
71
114
|
}
|
|
72
|
-
if (store.entries.length === 0) {
|
|
115
|
+
if (store.entries.length === 0 && borrowed.length === 0) {
|
|
73
116
|
note('the keyring is empty');
|
|
74
117
|
note(dim('add one: zen key add openai'));
|
|
75
118
|
return;
|
|
76
119
|
}
|
|
77
|
-
writeAll(rows(store));
|
|
78
|
-
const missing = OWNERS.filter((p) => !store.active(p) && !
|
|
120
|
+
writeAll(rows(store, borrowed));
|
|
121
|
+
const missing = OWNERS.filter((p) => !store.active(p) && !borrowed.some(([c]) => c.provider === p));
|
|
79
122
|
if (missing.length) {
|
|
80
123
|
note('');
|
|
81
124
|
note(dim(`no key for: ${missing.join(', ')}`));
|
|
@@ -87,7 +130,12 @@ const ls = async (ctx, args) => {
|
|
|
87
130
|
* Piped stdin or an echo-off prompt are the only two ways in.
|
|
88
131
|
*/
|
|
89
132
|
const add = async (ctx, args) => {
|
|
90
|
-
const { values, positionals } = parse(args, {
|
|
133
|
+
const { values, positionals } = parse(args, {
|
|
134
|
+
name: { type: 'string' },
|
|
135
|
+
'no-check': { type: 'boolean' },
|
|
136
|
+
project: { type: 'string' },
|
|
137
|
+
location: { type: 'string' },
|
|
138
|
+
}, 'zen key add <provider>[/name] [--name <name>] [--project <id>] [--location <region>] [--no-check]');
|
|
91
139
|
const ref = positionals[0];
|
|
92
140
|
if (!ref) {
|
|
93
141
|
throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
|
|
@@ -96,6 +144,9 @@ const add = async (ctx, args) => {
|
|
|
96
144
|
const provider = parsed.provider;
|
|
97
145
|
const name = values.name ?? parsed.name ?? 'default';
|
|
98
146
|
const shape = SHAPES[provider];
|
|
147
|
+
if ((values.project || values.location) && provider !== 'vertex') {
|
|
148
|
+
throw usageError(`--project and --location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
149
|
+
}
|
|
99
150
|
ensureHome();
|
|
100
151
|
const store = await KeyStore.open();
|
|
101
152
|
if (store.find(provider, name) && isInteractive()) {
|
|
@@ -103,14 +154,20 @@ const add = async (ctx, args) => {
|
|
|
103
154
|
throw usageError('cancelled');
|
|
104
155
|
}
|
|
105
156
|
}
|
|
106
|
-
const raw = (await readStdin()) ?? (await promptFor(shape
|
|
157
|
+
const raw = (await readStdin()) ?? (await promptFor(shape));
|
|
107
158
|
if (!raw) {
|
|
108
159
|
throw usageError('no value given');
|
|
109
160
|
}
|
|
110
|
-
|
|
161
|
+
// A provider with one form can say up front that a path is wrong. One with
|
|
162
|
+
// two cannot: a value that is not a path is not a mistake there, it is the
|
|
163
|
+
// other kind of credential, and `add` decides which by looking.
|
|
164
|
+
if (shape.forms.length === 1 && shape.forms[0].holds === 'file' && !existsSync(raw)) {
|
|
111
165
|
throw usageError(`no such file: ${raw}`);
|
|
112
166
|
}
|
|
113
|
-
const entry = store.add(provider, name, raw
|
|
167
|
+
const entry = store.add(provider, name, raw, {
|
|
168
|
+
project: values.project,
|
|
169
|
+
location: values.location,
|
|
170
|
+
});
|
|
114
171
|
// Verified before it is trusted, but stored either way: a key that cannot
|
|
115
172
|
// be checked right now — offline, behind a proxy — is not a key that is
|
|
116
173
|
// wrong, and refusing to save it would make `zen key add` fail on a plane.
|
|
@@ -123,25 +180,48 @@ const add = async (ctx, args) => {
|
|
|
123
180
|
if (check.state === 'dead') {
|
|
124
181
|
note(`${red('rejected')} ${check.detail ?? 'the provider refused this key'}`);
|
|
125
182
|
}
|
|
183
|
+
else if (check.state === 'blocked') {
|
|
184
|
+
note(`${yellow('blocked')} ${check.detail ?? 'the account cannot use this key'}`);
|
|
185
|
+
if (check.fix) {
|
|
186
|
+
note(dim(` ${check.fix}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
126
189
|
}
|
|
127
190
|
store.save();
|
|
128
191
|
if (ctx.json) {
|
|
129
192
|
json({
|
|
130
193
|
key: keyId(entry),
|
|
131
|
-
env:
|
|
194
|
+
env: envOf(entry),
|
|
132
195
|
active: store.isActive(entry),
|
|
133
196
|
check: entry.check,
|
|
134
197
|
});
|
|
135
198
|
return;
|
|
136
199
|
}
|
|
137
|
-
note(`${green('added')} ${bold(keyId(entry))} ${dim(describe(store, entry))}
|
|
138
|
-
|
|
139
|
-
|
|
200
|
+
note(`${green('added')} ${bold(keyId(entry))} ${dim(describe(store, entry))} ` +
|
|
201
|
+
`${dim(`→ $${envOf(entry)}`)} ${state(entry)}`);
|
|
202
|
+
if (process.env[envOf(entry)]) {
|
|
203
|
+
note(yellow(`$${envOf(entry)} is set and will win over this`));
|
|
204
|
+
}
|
|
205
|
+
if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
|
|
206
|
+
note(dim('no --project given; the project_id inside the file will be used'));
|
|
140
207
|
}
|
|
141
208
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
/**
|
|
210
|
+
* A provider with two forms cannot ask for one of them. It asks with the echo
|
|
211
|
+
* off, because one of the two answers is a secret and there is no way to know
|
|
212
|
+
* which is coming until it arrives.
|
|
213
|
+
*/
|
|
214
|
+
async function promptFor(shape) {
|
|
215
|
+
const [first, ...rest] = shape.forms;
|
|
216
|
+
if (rest.length) {
|
|
217
|
+
note(dim(`${shape.label} — ${first.where},`));
|
|
218
|
+
for (const form of rest) {
|
|
219
|
+
note(dim(` or ${form.where}`));
|
|
220
|
+
}
|
|
221
|
+
return askSecret('Paste the key, or a path to the file:');
|
|
222
|
+
}
|
|
223
|
+
note(dim(`${shape.label} — ${first.where}`));
|
|
224
|
+
return first.holds === 'file'
|
|
145
225
|
? ask('Path to the credentials file:')
|
|
146
226
|
: askSecret('Paste the key (it will not be shown):');
|
|
147
227
|
}
|
|
@@ -159,7 +239,7 @@ const use = async (ctx, args) => {
|
|
|
159
239
|
const entry = store.use(provider, name);
|
|
160
240
|
store.save();
|
|
161
241
|
if (ctx.json) {
|
|
162
|
-
json({ key: keyId(entry), env:
|
|
242
|
+
json({ key: keyId(entry), env: envOf(entry) });
|
|
163
243
|
return;
|
|
164
244
|
}
|
|
165
245
|
note(`${green('using')} ${bold(keyId(entry))} for ${SHAPES[provider].label}`);
|
|
@@ -183,9 +263,19 @@ const check = async (ctx, args) => {
|
|
|
183
263
|
MARK[result.state],
|
|
184
264
|
dim(result.detail ?? ''),
|
|
185
265
|
])));
|
|
266
|
+
for (const [entry, result] of checks) {
|
|
267
|
+
if (result.fix) {
|
|
268
|
+
note(dim(`${keyId(entry)}: ${result.fix}`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
186
271
|
if (checks.some(([, r]) => r.state === 'dead')) {
|
|
187
272
|
throw credentialError('at least one key was refused');
|
|
188
273
|
}
|
|
274
|
+
// Separately, because the action is not the same one: these authenticated,
|
|
275
|
+
// and a replacement key would be refused for exactly the same reason.
|
|
276
|
+
if (checks.some(([, r]) => r.state === 'blocked')) {
|
|
277
|
+
throw credentialError('at least one key authenticated but cannot be used');
|
|
278
|
+
}
|
|
189
279
|
};
|
|
190
280
|
function select(store, ref) {
|
|
191
281
|
const { provider, name } = parseRef(ref);
|
|
@@ -242,7 +332,7 @@ const show = async (ctx, args) => {
|
|
|
242
332
|
if (ctx.json) {
|
|
243
333
|
json({
|
|
244
334
|
key: keyId(entry),
|
|
245
|
-
env:
|
|
335
|
+
env: envOf(entry),
|
|
246
336
|
value,
|
|
247
337
|
revealed: Boolean(values.reveal),
|
|
248
338
|
});
|
|
@@ -256,9 +346,12 @@ const show = async (ctx, args) => {
|
|
|
256
346
|
}
|
|
257
347
|
writeAll(table([
|
|
258
348
|
[dim('key'), keyId(entry)],
|
|
259
|
-
[dim('env'),
|
|
349
|
+
[dim('env'), envOf(entry)],
|
|
260
350
|
[dim('value'), value],
|
|
351
|
+
...(entry.project ? [[dim('project'), entry.project]] : []),
|
|
352
|
+
...(entry.location ? [[dim('location'), entry.location]] : []),
|
|
261
353
|
[dim('state'), state(entry)],
|
|
354
|
+
...(entry.check?.fix ? [[dim('fix'), entry.check.fix]] : []),
|
|
262
355
|
[dim('added'), ago(entry.addedAt)],
|
|
263
356
|
]));
|
|
264
357
|
note(dim('--reveal prints the secret itself'));
|
|
@@ -278,8 +371,18 @@ const env = async (ctx, args) => {
|
|
|
278
371
|
const vars = {};
|
|
279
372
|
for (const provider of only ?? OWNERS) {
|
|
280
373
|
const entry = store.active(provider);
|
|
281
|
-
if (entry) {
|
|
282
|
-
|
|
374
|
+
if (!entry) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
vars[envOf(entry)] = store.reveal(entry);
|
|
378
|
+
// A service account says which project it belongs to but not which
|
|
379
|
+
// region to call, and a shell that has the file and not the region is
|
|
380
|
+
// still a shell that cannot reach a model.
|
|
381
|
+
if (entry.project) {
|
|
382
|
+
vars.GOOGLE_CLOUD_PROJECT = entry.project;
|
|
383
|
+
}
|
|
384
|
+
if (entry.location) {
|
|
385
|
+
vars.GOOGLE_CLOUD_LOCATION = entry.location;
|
|
283
386
|
}
|
|
284
387
|
}
|
|
285
388
|
if (ctx.json) {
|
|
@@ -306,11 +409,16 @@ export const key = {
|
|
|
306
409
|
usage: USAGE,
|
|
307
410
|
details: [
|
|
308
411
|
'Keys live in ~/.zenera/neo/keys.json (0600) and are materialised into',
|
|
309
|
-
'the environment before a run. A real environment variable always wins
|
|
412
|
+
'the environment before a run. A real environment variable always wins,',
|
|
413
|
+
'and is listed as ~ so it is clear where a working provider comes from.',
|
|
310
414
|
'',
|
|
311
415
|
'Model providers: openai, anthropic, google, vertex, openrouter.',
|
|
312
416
|
'Services the tools call: exa.',
|
|
313
417
|
'',
|
|
418
|
+
'Vertex takes either shape: a service-account JSON file, which wants',
|
|
419
|
+
'--project and --location too, or an express-mode API key, which wants',
|
|
420
|
+
'neither. Which one you gave is read off the value.',
|
|
421
|
+
'',
|
|
314
422
|
' zen key ls [--check] Everything stored, and its state.',
|
|
315
423
|
' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
|
|
316
424
|
' zen key use <provider>/<name> Choose which one a run uses.',
|
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import type { Command } from '../command.ts';
|
|
2
|
-
/**
|
|
3
|
-
* Everything a run would resolve, resolved — and nothing called. Loading a
|
|
4
|
-
* project constructs the model clients, so a config that names an impossible
|
|
5
|
-
* provider or an agent that hands off to nobody fails here, in a command that
|
|
6
|
-
* costs nothing, instead of three seconds into a run that costs money.
|
|
7
|
-
*/
|
|
8
2
|
export declare const models: Command;
|
|
9
3
|
//# sourceMappingURL=models.d.ts.map
|