@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/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]';
|
|
@@ -16,35 +16,59 @@ const MARK = {
|
|
|
16
16
|
function state(entry) {
|
|
17
17
|
return entry.check ? MARK[entry.check.state] : dim('unchecked');
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* An ambient credential borrowed into the shape the rest of this file works
|
|
21
|
+
* in. It is not in the store and never will be: `store.find` misses it, so
|
|
22
|
+
* `store.record` drops its check and nothing is written to disk. That is the
|
|
23
|
+
* intent — it can be listed and probed, not chosen or forgotten.
|
|
24
|
+
*/
|
|
25
|
+
function asEntry(cred) {
|
|
26
|
+
return {
|
|
27
|
+
provider: cred.provider,
|
|
28
|
+
name: cred.env ? `$${cred.env}` : 'adc',
|
|
29
|
+
holds: cred.holds,
|
|
30
|
+
value: cred.value,
|
|
31
|
+
env: cred.env,
|
|
32
|
+
addedAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function rows(store, borrowed) {
|
|
20
36
|
const out = [
|
|
21
37
|
[bold(''), bold('KEY'), bold('VALUE'), bold('STATE'), bold('CHECKED')],
|
|
22
38
|
];
|
|
23
39
|
for (const provider of OWNERS) {
|
|
24
40
|
for (const entry of store.for(provider)) {
|
|
25
|
-
const
|
|
41
|
+
const shadow = envNames(provider).find((name) => process.env[name]);
|
|
26
42
|
out.push([
|
|
27
43
|
store.isActive(entry) ? green('*') : ' ',
|
|
28
44
|
keyId(entry),
|
|
29
45
|
dim(describe(store, entry)),
|
|
30
46
|
state(entry),
|
|
31
47
|
dim(entry.check ? ago(entry.check.at) : '—') +
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
(shadow && store.isActive(entry) ? yellow(` shadowed by $${shadow}`) : ''),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
for (const [cred, entry] of borrowed.filter(([c]) => c.provider === provider)) {
|
|
52
|
+
out.push([
|
|
53
|
+
dim('~'),
|
|
54
|
+
dim(ambientId(cred)),
|
|
55
|
+
dim(describe(store, entry)),
|
|
56
|
+
state(entry),
|
|
57
|
+
dim(cred.env ? 'from the environment' : 'from gcloud'),
|
|
35
58
|
]);
|
|
36
59
|
}
|
|
37
60
|
}
|
|
38
61
|
return table(out);
|
|
39
62
|
}
|
|
40
63
|
/**
|
|
41
|
-
* Checking is a network round trip per key, so it says which
|
|
42
|
-
*
|
|
64
|
+
* Checking is a network round trip per key, so it says which ones have come
|
|
65
|
+
* back. Silence for ten seconds is indistinguishable from a hang.
|
|
43
66
|
*/
|
|
44
67
|
async function checkAll(ctx, store, targets) {
|
|
45
68
|
const bar = ctx.json ? undefined : progress();
|
|
69
|
+
bar?.update(dim(`checking ${targets.length} key${targets.length === 1 ? '' : 's'} …`));
|
|
46
70
|
try {
|
|
47
|
-
return await probeAll(store, targets, (entry,
|
|
71
|
+
return await probeAll(store, targets, (entry, done, total) => bar?.update(dim(`checked ${keyId(entry)} … ${done}/${total}`)));
|
|
48
72
|
}
|
|
49
73
|
finally {
|
|
50
74
|
bar?.done();
|
|
@@ -53,29 +77,47 @@ async function checkAll(ctx, store, targets) {
|
|
|
53
77
|
const ls = async (ctx, args) => {
|
|
54
78
|
const { values } = parse(args, { check: { type: 'boolean' } }, USAGE);
|
|
55
79
|
const store = await KeyStore.open();
|
|
80
|
+
const borrowed = ambient(store).map((cred) => [cred, asEntry(cred)]);
|
|
56
81
|
if (values.check) {
|
|
57
|
-
|
|
82
|
+
// Ambient credentials are checked alongside the stored ones and, unlike
|
|
83
|
+
// them, forgotten again: there is nowhere to write the result, and a
|
|
84
|
+
// variable that changes between runs would make a cached one a lie.
|
|
85
|
+
const checks = await checkAll(ctx, store, [
|
|
86
|
+
...store.entries,
|
|
87
|
+
...borrowed.map(([, entry]) => entry),
|
|
88
|
+
]);
|
|
58
89
|
for (const [entry, check] of checks) {
|
|
59
90
|
store.record(entry, check);
|
|
91
|
+
entry.check = check;
|
|
60
92
|
}
|
|
61
93
|
store.save();
|
|
62
94
|
}
|
|
63
95
|
if (ctx.json) {
|
|
64
|
-
json(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
json({
|
|
97
|
+
keys: store.entries.map((e) => ({
|
|
98
|
+
...e,
|
|
99
|
+
value: e.holds === 'file' ? store.fileOf(e) : mask(e.value),
|
|
100
|
+
active: store.isActive(e),
|
|
101
|
+
env: envOf(e),
|
|
102
|
+
})),
|
|
103
|
+
ambient: borrowed.map(([cred, entry]) => ({
|
|
104
|
+
provider: cred.provider,
|
|
105
|
+
id: ambientId(cred),
|
|
106
|
+
env: cred.env ?? null,
|
|
107
|
+
holds: cred.holds,
|
|
108
|
+
value: cred.holds === 'file' ? cred.value : mask(cred.value),
|
|
109
|
+
check: entry.check,
|
|
110
|
+
})),
|
|
111
|
+
});
|
|
70
112
|
return;
|
|
71
113
|
}
|
|
72
|
-
if (store.entries.length === 0) {
|
|
114
|
+
if (store.entries.length === 0 && borrowed.length === 0) {
|
|
73
115
|
note('the keyring is empty');
|
|
74
116
|
note(dim('add one: zen key add openai'));
|
|
75
117
|
return;
|
|
76
118
|
}
|
|
77
|
-
writeAll(rows(store));
|
|
78
|
-
const missing = OWNERS.filter((p) => !store.active(p) && !
|
|
119
|
+
writeAll(rows(store, borrowed));
|
|
120
|
+
const missing = OWNERS.filter((p) => !store.active(p) && !borrowed.some(([c]) => c.provider === p));
|
|
79
121
|
if (missing.length) {
|
|
80
122
|
note('');
|
|
81
123
|
note(dim(`no key for: ${missing.join(', ')}`));
|
|
@@ -87,7 +129,12 @@ const ls = async (ctx, args) => {
|
|
|
87
129
|
* Piped stdin or an echo-off prompt are the only two ways in.
|
|
88
130
|
*/
|
|
89
131
|
const add = async (ctx, args) => {
|
|
90
|
-
const { values, positionals } = parse(args, {
|
|
132
|
+
const { values, positionals } = parse(args, {
|
|
133
|
+
name: { type: 'string' },
|
|
134
|
+
'no-check': { type: 'boolean' },
|
|
135
|
+
project: { type: 'string' },
|
|
136
|
+
location: { type: 'string' },
|
|
137
|
+
}, 'zen key add <provider>[/name] [--name <name>] [--project <id>] [--location <region>] [--no-check]');
|
|
91
138
|
const ref = positionals[0];
|
|
92
139
|
if (!ref) {
|
|
93
140
|
throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
|
|
@@ -96,6 +143,9 @@ const add = async (ctx, args) => {
|
|
|
96
143
|
const provider = parsed.provider;
|
|
97
144
|
const name = values.name ?? parsed.name ?? 'default';
|
|
98
145
|
const shape = SHAPES[provider];
|
|
146
|
+
if ((values.project || values.location) && provider !== 'vertex') {
|
|
147
|
+
throw usageError(`--project and --location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
148
|
+
}
|
|
99
149
|
ensureHome();
|
|
100
150
|
const store = await KeyStore.open();
|
|
101
151
|
if (store.find(provider, name) && isInteractive()) {
|
|
@@ -103,14 +153,20 @@ const add = async (ctx, args) => {
|
|
|
103
153
|
throw usageError('cancelled');
|
|
104
154
|
}
|
|
105
155
|
}
|
|
106
|
-
const raw = (await readStdin()) ?? (await promptFor(shape
|
|
156
|
+
const raw = (await readStdin()) ?? (await promptFor(shape));
|
|
107
157
|
if (!raw) {
|
|
108
158
|
throw usageError('no value given');
|
|
109
159
|
}
|
|
110
|
-
|
|
160
|
+
// A provider with one form can say up front that a path is wrong. One with
|
|
161
|
+
// two cannot: a value that is not a path is not a mistake there, it is the
|
|
162
|
+
// other kind of credential, and `add` decides which by looking.
|
|
163
|
+
if (shape.forms.length === 1 && shape.forms[0].holds === 'file' && !existsSync(raw)) {
|
|
111
164
|
throw usageError(`no such file: ${raw}`);
|
|
112
165
|
}
|
|
113
|
-
const entry = store.add(provider, name, raw
|
|
166
|
+
const entry = store.add(provider, name, raw, {
|
|
167
|
+
project: values.project,
|
|
168
|
+
location: values.location,
|
|
169
|
+
});
|
|
114
170
|
// Verified before it is trusted, but stored either way: a key that cannot
|
|
115
171
|
// be checked right now — offline, behind a proxy — is not a key that is
|
|
116
172
|
// wrong, and refusing to save it would make `zen key add` fail on a plane.
|
|
@@ -128,20 +184,37 @@ const add = async (ctx, args) => {
|
|
|
128
184
|
if (ctx.json) {
|
|
129
185
|
json({
|
|
130
186
|
key: keyId(entry),
|
|
131
|
-
env:
|
|
187
|
+
env: envOf(entry),
|
|
132
188
|
active: store.isActive(entry),
|
|
133
189
|
check: entry.check,
|
|
134
190
|
});
|
|
135
191
|
return;
|
|
136
192
|
}
|
|
137
|
-
note(`${green('added')} ${bold(keyId(entry))} ${dim(describe(store, entry))}
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
note(`${green('added')} ${bold(keyId(entry))} ${dim(describe(store, entry))} ` +
|
|
194
|
+
`${dim(`→ $${envOf(entry)}`)} ${state(entry)}`);
|
|
195
|
+
if (process.env[envOf(entry)]) {
|
|
196
|
+
note(yellow(`$${envOf(entry)} is set and will win over this`));
|
|
197
|
+
}
|
|
198
|
+
if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
|
|
199
|
+
note(dim('no --project given; the project_id inside the file will be used'));
|
|
140
200
|
}
|
|
141
201
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
202
|
+
/**
|
|
203
|
+
* A provider with two forms cannot ask for one of them. It asks with the echo
|
|
204
|
+
* off, because one of the two answers is a secret and there is no way to know
|
|
205
|
+
* which is coming until it arrives.
|
|
206
|
+
*/
|
|
207
|
+
async function promptFor(shape) {
|
|
208
|
+
const [first, ...rest] = shape.forms;
|
|
209
|
+
if (rest.length) {
|
|
210
|
+
note(dim(`${shape.label} — ${first.where},`));
|
|
211
|
+
for (const form of rest) {
|
|
212
|
+
note(dim(` or ${form.where}`));
|
|
213
|
+
}
|
|
214
|
+
return askSecret('Paste the key, or a path to the file:');
|
|
215
|
+
}
|
|
216
|
+
note(dim(`${shape.label} — ${first.where}`));
|
|
217
|
+
return first.holds === 'file'
|
|
145
218
|
? ask('Path to the credentials file:')
|
|
146
219
|
: askSecret('Paste the key (it will not be shown):');
|
|
147
220
|
}
|
|
@@ -159,7 +232,7 @@ const use = async (ctx, args) => {
|
|
|
159
232
|
const entry = store.use(provider, name);
|
|
160
233
|
store.save();
|
|
161
234
|
if (ctx.json) {
|
|
162
|
-
json({ key: keyId(entry), env:
|
|
235
|
+
json({ key: keyId(entry), env: envOf(entry) });
|
|
163
236
|
return;
|
|
164
237
|
}
|
|
165
238
|
note(`${green('using')} ${bold(keyId(entry))} for ${SHAPES[provider].label}`);
|
|
@@ -242,7 +315,7 @@ const show = async (ctx, args) => {
|
|
|
242
315
|
if (ctx.json) {
|
|
243
316
|
json({
|
|
244
317
|
key: keyId(entry),
|
|
245
|
-
env:
|
|
318
|
+
env: envOf(entry),
|
|
246
319
|
value,
|
|
247
320
|
revealed: Boolean(values.reveal),
|
|
248
321
|
});
|
|
@@ -256,8 +329,10 @@ const show = async (ctx, args) => {
|
|
|
256
329
|
}
|
|
257
330
|
writeAll(table([
|
|
258
331
|
[dim('key'), keyId(entry)],
|
|
259
|
-
[dim('env'),
|
|
332
|
+
[dim('env'), envOf(entry)],
|
|
260
333
|
[dim('value'), value],
|
|
334
|
+
...(entry.project ? [[dim('project'), entry.project]] : []),
|
|
335
|
+
...(entry.location ? [[dim('location'), entry.location]] : []),
|
|
261
336
|
[dim('state'), state(entry)],
|
|
262
337
|
[dim('added'), ago(entry.addedAt)],
|
|
263
338
|
]));
|
|
@@ -278,8 +353,18 @@ const env = async (ctx, args) => {
|
|
|
278
353
|
const vars = {};
|
|
279
354
|
for (const provider of only ?? OWNERS) {
|
|
280
355
|
const entry = store.active(provider);
|
|
281
|
-
if (entry) {
|
|
282
|
-
|
|
356
|
+
if (!entry) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
vars[envOf(entry)] = store.reveal(entry);
|
|
360
|
+
// A service account says which project it belongs to but not which
|
|
361
|
+
// region to call, and a shell that has the file and not the region is
|
|
362
|
+
// still a shell that cannot reach a model.
|
|
363
|
+
if (entry.project) {
|
|
364
|
+
vars.GOOGLE_CLOUD_PROJECT = entry.project;
|
|
365
|
+
}
|
|
366
|
+
if (entry.location) {
|
|
367
|
+
vars.GOOGLE_CLOUD_LOCATION = entry.location;
|
|
283
368
|
}
|
|
284
369
|
}
|
|
285
370
|
if (ctx.json) {
|
|
@@ -306,11 +391,16 @@ export const key = {
|
|
|
306
391
|
usage: USAGE,
|
|
307
392
|
details: [
|
|
308
393
|
'Keys live in ~/.zenera/neo/keys.json (0600) and are materialised into',
|
|
309
|
-
'the environment before a run. A real environment variable always wins
|
|
394
|
+
'the environment before a run. A real environment variable always wins,',
|
|
395
|
+
'and is listed as ~ so it is clear where a working provider comes from.',
|
|
310
396
|
'',
|
|
311
397
|
'Model providers: openai, anthropic, google, vertex, openrouter.',
|
|
312
398
|
'Services the tools call: exa.',
|
|
313
399
|
'',
|
|
400
|
+
'Vertex takes either shape: a service-account JSON file, which wants',
|
|
401
|
+
'--project and --location too, or an express-mode API key, which wants',
|
|
402
|
+
'neither. Which one you gave is read off the value.',
|
|
403
|
+
'',
|
|
314
404
|
' zen key ls [--check] Everything stored, and its state.',
|
|
315
405
|
' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
|
|
316
406
|
' zen key use <provider>/<name> Choose which one a run uses.',
|
package/dist/commands/models.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadProject, readProjectConfig } from '@zenera/neo';
|
|
2
2
|
import { parse } from "../args.js";
|
|
3
|
-
import { KeyStore, PROVIDERS
|
|
3
|
+
import { envNames, form, KeyStore, PROVIDERS } from "../keys.js";
|
|
4
4
|
import { project as resolveProject } from "../resolve.js";
|
|
5
5
|
import { bold, credentialError, cyan, dim, green, invalidError, json, note, red, table, writeAll, yellow, } from "../term.js";
|
|
6
6
|
import { availableTools } from "../validate.js";
|
|
@@ -22,7 +22,7 @@ export const models = {
|
|
|
22
22
|
// Asked before materialising, because materialising is exactly what
|
|
23
23
|
// erases the difference between "the environment had it" and "the
|
|
24
24
|
// keyring supplied it".
|
|
25
|
-
const fromEnv = new Set(PROVIDERS.filter((p) => process.env[
|
|
25
|
+
const fromEnv = new Set(PROVIDERS.filter((p) => envNames(p).some((name) => process.env[name])));
|
|
26
26
|
store.materialize();
|
|
27
27
|
let project;
|
|
28
28
|
try {
|
|
@@ -64,7 +64,7 @@ export const models = {
|
|
|
64
64
|
}));
|
|
65
65
|
const credentials = PROVIDERS.map((p) => ({
|
|
66
66
|
provider: p,
|
|
67
|
-
env:
|
|
67
|
+
env: envNames(p).find((name) => process.env[name]) ?? form(p).env,
|
|
68
68
|
source: fromEnv.has(p)
|
|
69
69
|
? 'environment'
|
|
70
70
|
: store.active(p)
|
package/dist/commands/open.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { delimiter, join, resolve, sep } from 'node:path';
|
|
5
5
|
import { one, parse } from "../args.js";
|
|
6
6
|
import { project as resolveProject } from "../resolve.js";
|
|
7
|
-
import {
|
|
7
|
+
import { editorFiles } from "../scaffold.js";
|
|
8
8
|
import { CliError, cyan, dim, EXIT, json, note, usageError } from "../term.js";
|
|
9
9
|
const USAGE = 'zen open [project] [--editor <cmd>] [--wait]';
|
|
10
10
|
/**
|
|
@@ -42,7 +42,7 @@ export const open = {
|
|
|
42
42
|
// opened on, and a project may predate either of them — or the version
|
|
43
43
|
// this `zen` writes. They are ours, so they are written fresh before
|
|
44
44
|
// the window is there to read them.
|
|
45
|
-
const written =
|
|
45
|
+
const written = editorFiles(dir);
|
|
46
46
|
if (ctx.json) {
|
|
47
47
|
json({
|
|
48
48
|
name: found.name,
|
package/dist/commands/run.js
CHANGED
|
@@ -19,6 +19,7 @@ export const run = {
|
|
|
19
19
|
' --model <ref> Override the default model.',
|
|
20
20
|
' --image <ref> Override the sandbox image commands run in.',
|
|
21
21
|
' --read-only Give the agent no way to write.',
|
|
22
|
+
' --no-keys Keep the API keys out of the sandbox.',
|
|
22
23
|
' --quiet Answer only; no narration.',
|
|
23
24
|
' --plain One shot, even on a terminal.',
|
|
24
25
|
' --theme <dark|light> Force the palette. Detected otherwise; $ZENERA_THEME.',
|
|
@@ -43,6 +44,7 @@ export const run = {
|
|
|
43
44
|
workspace: { type: 'string' },
|
|
44
45
|
model: { type: 'string' },
|
|
45
46
|
image: { type: 'string' },
|
|
47
|
+
'no-keys': { type: 'boolean' },
|
|
46
48
|
'read-only': { type: 'boolean' },
|
|
47
49
|
yes: { type: 'boolean' },
|
|
48
50
|
quiet: { type: 'boolean' },
|
|
@@ -84,6 +86,7 @@ export const run = {
|
|
|
84
86
|
readOnly: values['read-only'],
|
|
85
87
|
model: values.model,
|
|
86
88
|
image: values.image,
|
|
89
|
+
keys: values['no-keys'] ? false : undefined,
|
|
87
90
|
yes: values.yes || ctx.json,
|
|
88
91
|
});
|
|
89
92
|
try {
|