@shardflux/cli 0.3.0 → 0.4.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/CHANGELOG.md +47 -1
- package/README.md +79 -7
- package/dist/args.d.ts +1 -1
- package/dist/args.js +124 -7
- package/dist/commands.js +330 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +18 -4
- package/dist/format.d.ts +19 -2
- package/dist/format.js +97 -7
- package/dist/main.js +3 -0
- package/dist/template-yaml.d.ts +7 -0
- package/dist/template-yaml.js +62 -0
- package/dist/timing.d.ts +29 -1
- package/dist/timing.js +96 -0
- package/package.json +3 -2
package/dist/commands.js
CHANGED
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* the SDK has no typed method for yet (GET /v1/operations/{id}, usage summary).
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { basename, dirname, resolve as resolvePath } from 'node:path';
|
|
9
10
|
import { SDK_VERSION, ShardfluxApiError } from '@shardflux/sdk';
|
|
11
|
+
import { parse as parseYaml } from 'yaml';
|
|
10
12
|
import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
|
|
11
13
|
import { InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
14
|
+
import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
|
|
15
|
+
import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
|
|
12
16
|
import { timingJson } from "./timing.js";
|
|
13
|
-
import { bindingsText, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, fileTable, json, kv, operationDetail, operationTable, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, templateEntryText, templateFileTable, templateTable, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
17
|
+
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, fileTable, languageTable, json, kv, operationDetail, operationTable, packageTable, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, templateDetailText, templateEntryText, templateFileTable, templateTable, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
14
18
|
const DEFAULT_WAIT_MS = 300_000;
|
|
15
19
|
const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
|
|
16
20
|
const MAX_STDIN = 1024 * 1024;
|
|
@@ -154,6 +158,87 @@ function parseVersion(text, what) {
|
|
|
154
158
|
throw new UsageError(`${what} must be a version number (got "${text}")`);
|
|
155
159
|
return Number(text);
|
|
156
160
|
}
|
|
161
|
+
const SLUG = /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/;
|
|
162
|
+
/** `<slug>@<version>`. */
|
|
163
|
+
function parseSlugVersion(text, what) {
|
|
164
|
+
const at = text.lastIndexOf('@');
|
|
165
|
+
if (at <= 0)
|
|
166
|
+
throw new UsageError(`${what} must be <slug>@<version>, e.g. acme-dev@3 (got "${text}")`);
|
|
167
|
+
const slug = text.slice(0, at);
|
|
168
|
+
if (!SLUG.test(slug))
|
|
169
|
+
throw new UsageError(`${what}: "${slug}" is not a template slug`);
|
|
170
|
+
return { slug, version: parseVersion(text.slice(at + 1), what) };
|
|
171
|
+
}
|
|
172
|
+
const INPUT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
173
|
+
/**
|
|
174
|
+
* Open-time text inputs (contracts §24.3) from --input-file (NAME=value lines with # comments, or a JSON object of
|
|
175
|
+
* strings), then each --input NAME=value (a later one wins). Undefined when neither option is given.
|
|
176
|
+
*/
|
|
177
|
+
async function inputsFrom(ctx, values) {
|
|
178
|
+
const file = str(values['input-file']);
|
|
179
|
+
const pairs = strs(values.input);
|
|
180
|
+
if (file === undefined && pairs.length === 0)
|
|
181
|
+
return undefined;
|
|
182
|
+
const out = {};
|
|
183
|
+
const put = (name, value, where) => {
|
|
184
|
+
if (!INPUT_NAME.test(name))
|
|
185
|
+
throw new UsageError(`${where}: "${name}" is not an input name (^[A-Za-z_][A-Za-z0-9_]*$)`);
|
|
186
|
+
if (/[\r\n\0]/.test(value))
|
|
187
|
+
throw new UsageError(`${where}: the value of ${name} must be one line without NUL`);
|
|
188
|
+
out[name] = value;
|
|
189
|
+
};
|
|
190
|
+
if (file !== undefined) {
|
|
191
|
+
const text = Buffer.from(await readInput(ctx, file, 1024 * 1024, '--input-file')).toString('utf8');
|
|
192
|
+
if (text.trimStart().startsWith('{')) {
|
|
193
|
+
let doc;
|
|
194
|
+
try {
|
|
195
|
+
doc = JSON.parse(text);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
throw new UsageError(`--input-file: not valid JSON (${e instanceof Error ? e.message : String(e)})`);
|
|
199
|
+
}
|
|
200
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc))
|
|
201
|
+
throw new UsageError('--input-file: expected a JSON object of NAME: "value"');
|
|
202
|
+
for (const [k, v] of Object.entries(doc)) {
|
|
203
|
+
if (typeof v !== 'string')
|
|
204
|
+
throw new UsageError(`--input-file: the value of ${k} must be a string`);
|
|
205
|
+
put(k, v, '--input-file');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
for (const [i, raw] of text.split(/\r?\n/).entries()) {
|
|
210
|
+
const line = raw.trim();
|
|
211
|
+
if (line === '' || line.startsWith('#'))
|
|
212
|
+
continue;
|
|
213
|
+
const eq = line.indexOf('=');
|
|
214
|
+
if (eq <= 0)
|
|
215
|
+
throw new UsageError(`--input-file line ${i + 1}: expected NAME=value`);
|
|
216
|
+
put(line.slice(0, eq).trim(), line.slice(eq + 1), `--input-file line ${i + 1}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const p of pairs) {
|
|
221
|
+
const eq = p.indexOf('=');
|
|
222
|
+
if (eq <= 0)
|
|
223
|
+
throw new UsageError(`--input expects NAME=value (got "${p}")`);
|
|
224
|
+
put(p.slice(0, eq), p.slice(eq + 1), '--input');
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
const BUILD_STATES = ['queued', 'building', 'testing', 'publishing', 'published', 'succeeded', 'failed', 'canceled'];
|
|
229
|
+
/** The exit code of a build the command followed: 6 when it failed, was canceled or could not be registered. */
|
|
230
|
+
function buildExitCode(b) {
|
|
231
|
+
return b.state === 'failed' || b.state === 'canceled' || b.registration.state === 'failed' ? 6 : 0;
|
|
232
|
+
}
|
|
233
|
+
async function fileExists(path) {
|
|
234
|
+
return access(path).then(() => true, () => false);
|
|
235
|
+
}
|
|
236
|
+
/** Writes `text` to `path`, refusing to overwrite an existing file without --force. */
|
|
237
|
+
async function writeNew(path, text, force) {
|
|
238
|
+
if (!force && (await fileExists(path)))
|
|
239
|
+
throw new UsageError(`${path} exists; pass --force to overwrite it`);
|
|
240
|
+
await writeFile(path, text);
|
|
241
|
+
}
|
|
157
242
|
/** Waits with the SDK's backoff; Ctrl-C reports the operation that keeps running. */
|
|
158
243
|
async function waitOperation(ctx, operationId, timeoutMs) {
|
|
159
244
|
try {
|
|
@@ -279,6 +364,7 @@ export const HANDLERS = {
|
|
|
279
364
|
const c = caps(values);
|
|
280
365
|
const bind = secretNames(strs(values.secret), '--secret');
|
|
281
366
|
const lifetime = oneOf(values, 'lifetime', LIFETIMES);
|
|
367
|
+
const inputs = await inputsFrom(ctx, values);
|
|
282
368
|
const timeoutMs = waitTimeout(values);
|
|
283
369
|
const cloud = ctx.cloud();
|
|
284
370
|
// The SDK's waited open: the server holds the request until the workspace is ready (Prefer: wait), then the SDK
|
|
@@ -288,6 +374,7 @@ export const HANDLERS = {
|
|
|
288
374
|
template,
|
|
289
375
|
...(c ? { caps: c } : {}),
|
|
290
376
|
...(bind.length ? { secrets: bind } : {}),
|
|
377
|
+
...(inputs ? { inputs } : {}),
|
|
291
378
|
...(lifetime ? { lifetime } : {}),
|
|
292
379
|
agentLabel: ctx.agentLabel,
|
|
293
380
|
wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
|
|
@@ -340,6 +427,13 @@ export const HANDLERS = {
|
|
|
340
427
|
out(ctx, workspaceDetail(ws.data), ws.data);
|
|
341
428
|
return 0;
|
|
342
429
|
},
|
|
430
|
+
async 'workspaces inputs'(ctx, _values, [ref]) {
|
|
431
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
432
|
+
const inputs = await ws.inputs();
|
|
433
|
+
const names = Object.keys(inputs);
|
|
434
|
+
out(ctx, names.length ? kv(names.map((n) => [n, inputs[n]])) : 'No text inputs (secret inputs are bound secrets: shard ws secrets).\n', { inputs });
|
|
435
|
+
return 0;
|
|
436
|
+
},
|
|
343
437
|
async 'workspaces exec'(ctx, values, [ref, ...argv]) {
|
|
344
438
|
const env = parseEnvPairs(values.env ?? []);
|
|
345
439
|
const timeout = str(values.timeout);
|
|
@@ -504,6 +598,240 @@ export const HANDLERS = {
|
|
|
504
598
|
ctx.io.stderr.write(`more: shard templates list --cursor ${next}\n`);
|
|
505
599
|
return 0;
|
|
506
600
|
},
|
|
601
|
+
async 'templates get'(ctx, values, [slug]) {
|
|
602
|
+
const owner = oneOf(values, 'owner', OWNERS);
|
|
603
|
+
const detail = await ctx.cloud().templates.get(slug, { ...(owner ? { owner } : {}), ...(values['include-archived'] === true ? { includeArchived: true } : {}) });
|
|
604
|
+
out(ctx, templateDetailText(detail), detail);
|
|
605
|
+
return 0;
|
|
606
|
+
},
|
|
607
|
+
async 'templates init'(ctx, values) {
|
|
608
|
+
const baseText = str(values.base);
|
|
609
|
+
if (!baseText)
|
|
610
|
+
throw new UsageError('--base <slug[@version]> is required for "shard templates init", e.g. --base ubuntu-24.04');
|
|
611
|
+
const dest = str(values.out) ?? 'template.yaml';
|
|
612
|
+
if (values.force !== true && (await fileExists(dest)))
|
|
613
|
+
throw new UsageError(`${dest} exists; pass --force to overwrite it`);
|
|
614
|
+
let base;
|
|
615
|
+
if (baseText.includes('@')) {
|
|
616
|
+
const { slug, version } = parseSlugVersion(baseText, '--base');
|
|
617
|
+
base = `${slug}@${version}`;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
if (!SLUG.test(baseText))
|
|
621
|
+
throw new UsageError(`--base: "${baseText}" is not a template slug`);
|
|
622
|
+
const t = await ctx.cloud().templates.get(baseText);
|
|
623
|
+
const v = t.open_resolves_to?.version ?? t.open_version?.version;
|
|
624
|
+
if (v === undefined)
|
|
625
|
+
throw new NotFoundError(`${baseText} has no published version to start from; name one with --base ${baseText}@<version>`);
|
|
626
|
+
base = `${baseText}@${v}`;
|
|
627
|
+
}
|
|
628
|
+
await writeNew(dest, initTemplateYaml(base), values.force === true);
|
|
629
|
+
const dir = basename(dirname(resolvePath(dest)));
|
|
630
|
+
const slugHint = SLUG.test(dir.toLowerCase()) ? '' : ' --slug <slug>';
|
|
631
|
+
out(ctx, `Wrote ${dest} (base ${base}).\nEdit it, then build it with: shard templates build ${dest}${slugHint} --wait\n`, { path: dest, base });
|
|
632
|
+
return 0;
|
|
633
|
+
},
|
|
634
|
+
async 'templates build'(ctx, values, [file]) {
|
|
635
|
+
const path = resolvePath(file);
|
|
636
|
+
const given = str(values.slug);
|
|
637
|
+
const dirName = basename(dirname(path));
|
|
638
|
+
const slug = given ?? dirName.toLowerCase();
|
|
639
|
+
if (!SLUG.test(slug))
|
|
640
|
+
throw new UsageError(given !== undefined ? `--slug "${given}" is not a template slug (lower-case letters, digits, ".", "_" and "-")` : `the directory name "${dirName}" is not a template slug; pass --slug <slug>`);
|
|
641
|
+
const ack = acknowledged(values);
|
|
642
|
+
const t = str(values.timeout);
|
|
643
|
+
const timeoutMs = t === undefined ? DEFAULT_BUILD_WAIT_MS : parseDuration(t, 'timeout');
|
|
644
|
+
const displayName = str(values['display-name']);
|
|
645
|
+
const description = str(values.description);
|
|
646
|
+
const timer = new BuildTimer();
|
|
647
|
+
const progress = (e) => {
|
|
648
|
+
timer.record(e);
|
|
649
|
+
if (!ctx.json)
|
|
650
|
+
ctx.io.stderr.write(progressLine(e));
|
|
651
|
+
};
|
|
652
|
+
if (!ctx.json)
|
|
653
|
+
ctx.io.stderr.write(`Building ${slug} from ${file}${given === undefined ? ' (template slug from the directory name; --slug to choose)' : ''}\n`);
|
|
654
|
+
let res;
|
|
655
|
+
try {
|
|
656
|
+
res = await ctx.cloud().templates.buildFromFile(path, {
|
|
657
|
+
templateSlug: slug,
|
|
658
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
659
|
+
...(description !== undefined ? { description } : {}),
|
|
660
|
+
autoPublish: values.publish === true,
|
|
661
|
+
...(ack ? { acknowledgedScanFindings: ack } : {}),
|
|
662
|
+
parseYaml: (text) => parseYaml(text),
|
|
663
|
+
onProgress: progress,
|
|
664
|
+
...(values.wait === true ? { wait: { timeoutMs, signal: ctx.signal } } : {}),
|
|
665
|
+
signal: ctx.signal,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
timer.finish(null);
|
|
670
|
+
if (ctx.timing && !ctx.json)
|
|
671
|
+
ctx.io.stderr.write(buildTimingText(timer));
|
|
672
|
+
if (ctx.signal.aborted)
|
|
673
|
+
throw new InterruptedError(timer.buildId ? `Interrupted; build ${timer.buildId} continues server side.` : 'Interrupted.');
|
|
674
|
+
throw err;
|
|
675
|
+
}
|
|
676
|
+
timer.finish(res.build);
|
|
677
|
+
if (ctx.timing && !ctx.json)
|
|
678
|
+
ctx.io.stderr.write(buildTimingText(timer));
|
|
679
|
+
const b = res.build;
|
|
680
|
+
const next = b.template_version && !b.template_version.published ? `test it with: shard templates test ${b.template.slug}@${b.template_version.version}\n` : '';
|
|
681
|
+
const machine = { build: b, uploads: res.uploads, ...(ctx.timing ? { timing: buildTimingJson(timer) } : {}) };
|
|
682
|
+
ctx.io.stdout.write(ctx.json ? json(machine) : `${buildText(b)}${next}`);
|
|
683
|
+
return values.wait === true ? buildExitCode(b) : 0;
|
|
684
|
+
},
|
|
685
|
+
async 'templates export'(ctx, values, [ref]) {
|
|
686
|
+
const { slug, version } = parseSlugVersion(ref, '<slug@version>');
|
|
687
|
+
const owner = oneOf(values, 'owner', OWNERS);
|
|
688
|
+
const exp = await ctx.cloud().templates.versions.recipe(slug, version, owner ? { owner } : {});
|
|
689
|
+
if (ctx.json) {
|
|
690
|
+
ctx.io.stdout.write(json(exp));
|
|
691
|
+
return 0;
|
|
692
|
+
}
|
|
693
|
+
if (exp.recipe === null) {
|
|
694
|
+
throw new NotFoundError(`${slug} v${version} has no recipe to export (${exp.source_kind === 'workspace' ? 'it was saved from a workspace or a draft' : 'a platform version or a version without a recipe'}); its settings: shard templates get ${slug}`);
|
|
695
|
+
}
|
|
696
|
+
const text = recipeYaml(exp.recipe, `${slug} v${version}${exp.source_kind ? ` (${exp.source_kind})` : ''}. Build it again with: shard templates build <this file> --slug ${slug}`);
|
|
697
|
+
const dest = str(values.out);
|
|
698
|
+
if (dest === undefined)
|
|
699
|
+
ctx.io.stdout.write(text);
|
|
700
|
+
else {
|
|
701
|
+
await writeNew(dest, text, values.force === true);
|
|
702
|
+
ctx.io.stderr.write(`wrote ${dest}\n`);
|
|
703
|
+
}
|
|
704
|
+
return 0;
|
|
705
|
+
},
|
|
706
|
+
async 'templates test'(ctx, values, [ref]) {
|
|
707
|
+
const { slug, version } = parseSlugVersion(ref, '<slug@version>');
|
|
708
|
+
const c = caps(values);
|
|
709
|
+
const inputs = await inputsFrom(ctx, values);
|
|
710
|
+
const timeoutMs = waitTimeout(values);
|
|
711
|
+
const instanceKey = str(values['instance-key']);
|
|
712
|
+
let ws = await ctx.cloud().templates.versionTestInstances.create(slug, version, {
|
|
713
|
+
...(instanceKey ? { key: instanceKey } : {}),
|
|
714
|
+
...(c ? { caps: c } : {}),
|
|
715
|
+
...(inputs ? { inputs } : {}),
|
|
716
|
+
agentLabel: ctx.agentLabel,
|
|
717
|
+
wait: false,
|
|
718
|
+
});
|
|
719
|
+
const op = ws.activeOperation;
|
|
720
|
+
if (values.wait !== false && !ws.ready && op) {
|
|
721
|
+
await waitOperation(ctx, op.id, timeoutMs);
|
|
722
|
+
ws = await ctx.cloud().workspaces.get(ws.id);
|
|
723
|
+
}
|
|
724
|
+
const human = ws.ready
|
|
725
|
+
? `Test instance ${ws.key} (${ws.id}) of ${slug} v${version} is ready. It ends with: shard ws close ${ws.key}\n${workspaceDetail(ws.data)}`
|
|
726
|
+
: `Test instance ${ws.key} (${ws.id}) of ${slug} v${version} is ${ws.state}${op ? `; operation ${op.id} ${op.kind} ${op.state}` : ''}.\n${op ? `wait with: shard operations wait ${op.id}\n` : ''}`;
|
|
727
|
+
out(ctx, human, ws.data);
|
|
728
|
+
return 0;
|
|
729
|
+
},
|
|
730
|
+
async 'templates builds list'(ctx, values) {
|
|
731
|
+
const cloud = ctx.cloud();
|
|
732
|
+
const org = (await principal(ctx)).organization_id;
|
|
733
|
+
const state = oneOf(values, 'state', BUILD_STATES);
|
|
734
|
+
const template = str(values.template);
|
|
735
|
+
const limit = listLimit(values, 200);
|
|
736
|
+
const params = { ...(state ? { state } : {}), ...(template ? { template } : {}) };
|
|
737
|
+
let rows = [];
|
|
738
|
+
let next = null;
|
|
739
|
+
if (values.all === true) {
|
|
740
|
+
let cursor;
|
|
741
|
+
do {
|
|
742
|
+
const page = await cloud.templates.builds.list(org, { ...params, limit: limit ?? 200, ...(cursor ? { cursor } : {}) });
|
|
743
|
+
rows.push(...page.data);
|
|
744
|
+
cursor = page.nextCursor ?? undefined;
|
|
745
|
+
if (ctx.signal.aborted)
|
|
746
|
+
throw new InterruptedError('Interrupted.');
|
|
747
|
+
} while (cursor);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const cursor = str(values.cursor);
|
|
751
|
+
const page = await cloud.templates.builds.list(org, { ...params, ...(limit ? { limit } : {}), ...(cursor ? { cursor } : {}) });
|
|
752
|
+
rows = page.data;
|
|
753
|
+
next = page.nextCursor;
|
|
754
|
+
}
|
|
755
|
+
out(ctx, rows.length ? buildTable(rows) : 'No template builds.\n', { data: rows, next_cursor: next });
|
|
756
|
+
if (!ctx.json && next)
|
|
757
|
+
ctx.io.stderr.write(`more: shard templates builds list --cursor ${next}\n`);
|
|
758
|
+
return 0;
|
|
759
|
+
},
|
|
760
|
+
async 'templates builds get'(ctx, _values, [id]) {
|
|
761
|
+
if (!UUID.test(id))
|
|
762
|
+
throw new UsageError(`<build-id> must be a UUID (got "${id}")`);
|
|
763
|
+
const org = (await principal(ctx)).organization_id;
|
|
764
|
+
const b = await ctx.cloud().templates.builds.get(org, id);
|
|
765
|
+
out(ctx, buildText(b), b);
|
|
766
|
+
return 0;
|
|
767
|
+
},
|
|
768
|
+
async 'templates builds log'(ctx, values, [id]) {
|
|
769
|
+
if (!UUID.test(id))
|
|
770
|
+
throw new UsageError(`<build-id> must be a UUID (got "${id}")`);
|
|
771
|
+
const org = (await principal(ctx)).organization_id;
|
|
772
|
+
const cloud = ctx.cloud();
|
|
773
|
+
let text;
|
|
774
|
+
try {
|
|
775
|
+
const link = await cloud.templates.builds.logUrl(org, id);
|
|
776
|
+
const res = await fetch(link.url, { signal: ctx.signal });
|
|
777
|
+
if (!res.ok)
|
|
778
|
+
throw new Error(`the log download answered HTTP ${res.status}`);
|
|
779
|
+
text = new Uint8Array(await res.arrayBuffer());
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
// Not recorded yet (or expired): the build view's tail is the most there is.
|
|
783
|
+
if (!(err instanceof ShardfluxApiError && err.status === 404))
|
|
784
|
+
throw err;
|
|
785
|
+
const b = await cloud.templates.builds.get(org, id);
|
|
786
|
+
if (!b.log.tail)
|
|
787
|
+
throw err;
|
|
788
|
+
ctx.io.stderr.write(`the full log is not available (${typeof err.details?.reason === 'string' ? err.details.reason : err.code}); the build's log tail follows\n`);
|
|
789
|
+
text = new TextEncoder().encode(b.log.tail.endsWith('\n') ? b.log.tail : `${b.log.tail}\n`);
|
|
790
|
+
}
|
|
791
|
+
const dest = str(values.out);
|
|
792
|
+
if (dest !== undefined) {
|
|
793
|
+
await writeFile(dest, text);
|
|
794
|
+
out(ctx, `wrote ${text.length} bytes to ${dest}\n`, { build_id: id, bytes: text.length, out: dest });
|
|
795
|
+
}
|
|
796
|
+
else if (ctx.json)
|
|
797
|
+
ctx.io.stdout.write(json({ build_id: id, bytes: text.length, log: Buffer.from(text).toString('utf8') }));
|
|
798
|
+
else
|
|
799
|
+
ctx.io.stdout.write(text);
|
|
800
|
+
return 0;
|
|
801
|
+
},
|
|
802
|
+
async 'templates builds cancel'(ctx, _values, [id]) {
|
|
803
|
+
if (!UUID.test(id))
|
|
804
|
+
throw new UsageError(`<build-id> must be a UUID (got "${id}")`);
|
|
805
|
+
const org = (await principal(ctx)).organization_id;
|
|
806
|
+
const b = await ctx.cloud().templates.builds.cancel(org, id);
|
|
807
|
+
out(ctx, `${b.state === 'canceled' ? 'Canceled' : 'Cancellation requested for'} build ${b.id} (${b.template.slug} v${b.target_version ?? '?'}).\n${buildText(b)}`, b);
|
|
808
|
+
return 0;
|
|
809
|
+
},
|
|
810
|
+
async 'templates languages'(ctx, values) {
|
|
811
|
+
const base = str(values.base);
|
|
812
|
+
if (base === undefined)
|
|
813
|
+
throw new UsageError('--base <slug@version> is required for "shard templates languages", e.g. --base ubuntu-24.04@1');
|
|
814
|
+
parseSlugVersion(base, '--base');
|
|
815
|
+
const langs = await ctx.cloud().templates.languages(base);
|
|
816
|
+
out(ctx, `${languageTable(langs)}platform base ${langs.platform_base}; apt packages need ${langs.apt_hosts.join(', ') || '(no host)'}\n`, langs);
|
|
817
|
+
return 0;
|
|
818
|
+
},
|
|
819
|
+
async 'templates packages search'(ctx, values, [ecosystem, query]) {
|
|
820
|
+
if (ecosystem !== 'apt' && ecosystem !== 'pip' && ecosystem !== 'npm')
|
|
821
|
+
throw new UsageError(`<ecosystem> must be apt, pip or npm (got "${ecosystem}")`);
|
|
822
|
+
const base = str(values.base);
|
|
823
|
+
if (ecosystem === 'apt' && base === undefined)
|
|
824
|
+
throw new UsageError('apt needs --base <slug@version> (the base whose package index to search)');
|
|
825
|
+
if (base !== undefined)
|
|
826
|
+
parseSlugVersion(base, '--base');
|
|
827
|
+
if (query.length < 1 || query.length > 100)
|
|
828
|
+
throw new UsageError('<query> must be 1-100 characters');
|
|
829
|
+
const limit = listLimit(values, 50);
|
|
830
|
+
const page = await ctx.cloud().templates.packages.search(ecosystem, query, { ...(base ? { base } : {}), ...(limit ? { limit } : {}) });
|
|
831
|
+
const note = page.source === 'pypi_names' ? '(pip: names only; the build installs the latest version unless you pin one, e.g. pandas==2.3.2)\n' : '';
|
|
832
|
+
out(ctx, page.data.length ? `${packageTable(page)}${note}` : `No ${ecosystem} packages match "${query}".\n`, page);
|
|
833
|
+
return 0;
|
|
834
|
+
},
|
|
507
835
|
async 'templates files'(ctx, values, [slug, versionText, path]) {
|
|
508
836
|
const version = parseVersion(versionText, '<version>');
|
|
509
837
|
const owner = oneOf(values, 'owner', OWNERS);
|
package/dist/errors.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare const EXIT: {
|
|
|
10
10
|
readonly auth: 4;
|
|
11
11
|
/** Waiting ran out of time (the operation continues server side), or a request timed out. */
|
|
12
12
|
readonly timeout: 5;
|
|
13
|
-
/** The awaited operation ended `failed` or `canceled
|
|
13
|
+
/** The awaited operation ended `failed` or `canceled` (the error's `retryable` says whether a retry may succeed). */
|
|
14
14
|
readonly operationFailed: 6;
|
|
15
15
|
/** SIGINT (Ctrl-C). */
|
|
16
16
|
readonly interrupted: 130;
|
package/dist/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* stderr with the API's error code, message and request id; the process exit
|
|
4
4
|
* code tells scripts what kind of failure it was (README "Exit codes").
|
|
5
5
|
*/
|
|
6
|
-
import { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from '@shardflux/sdk';
|
|
6
|
+
import { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
7
7
|
export const EXIT = {
|
|
8
8
|
ok: 0,
|
|
9
9
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -16,7 +16,7 @@ export const EXIT = {
|
|
|
16
16
|
auth: 4,
|
|
17
17
|
/** Waiting ran out of time (the operation continues server side), or a request timed out. */
|
|
18
18
|
timeout: 5,
|
|
19
|
-
/** The awaited operation ended `failed` or `canceled
|
|
19
|
+
/** The awaited operation ended `failed` or `canceled` (the error's `retryable` says whether a retry may succeed). */
|
|
20
20
|
operationFailed: 6,
|
|
21
21
|
/** SIGINT (Ctrl-C). */
|
|
22
22
|
interrupted: 130,
|
|
@@ -69,10 +69,22 @@ export function describeError(err) {
|
|
|
69
69
|
if (err instanceof OperationTimeoutError)
|
|
70
70
|
return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState };
|
|
71
71
|
if (err instanceof OperationFailedError) {
|
|
72
|
-
|
|
72
|
+
// retryable: the operation error's own flag (true for capacity_unavailable: nothing was started, retry later).
|
|
73
|
+
// details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
|
|
74
|
+
const opDetails = err.operation.error?.details;
|
|
75
|
+
const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
|
|
76
|
+
const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
|
|
77
|
+
return { code: 'operation_failed', message: err.message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
|
|
73
78
|
}
|
|
74
79
|
if (err instanceof UsageError)
|
|
75
80
|
return { code: 'usage', message: err.message };
|
|
81
|
+
// Template builds (0.4.0): a template file that cannot be read or packed is a usage error; a wait that ran out names the build.
|
|
82
|
+
if (err instanceof TemplateFileError)
|
|
83
|
+
return { code: 'usage', message: err.message };
|
|
84
|
+
if (err instanceof TemplateBuildTimeoutError)
|
|
85
|
+
return { code: 'timeout', message: `${err.message}; follow it with: shard templates builds get ${err.build.id}`, last_state: err.build.state };
|
|
86
|
+
if (err instanceof TemplateUploadError)
|
|
87
|
+
return { code: 'upload_failed', message: err.message, ...(err.status ? { status: err.status } : {}), details: { sha256: err.sha256, ...(err.code ? { storage_code: err.code } : {}) } };
|
|
76
88
|
if (err instanceof AuthConfigError)
|
|
77
89
|
return { code: 'unauthenticated', message: err.message };
|
|
78
90
|
if (err instanceof NotFoundError)
|
|
@@ -95,10 +107,12 @@ export function describeError(err) {
|
|
|
95
107
|
export function exitCodeFor(err, interrupted = false) {
|
|
96
108
|
if (interrupted || err instanceof InterruptedError)
|
|
97
109
|
return EXIT.interrupted;
|
|
98
|
-
if (err instanceof UsageError)
|
|
110
|
+
if (err instanceof UsageError || err instanceof TemplateFileError)
|
|
99
111
|
return EXIT.usage;
|
|
100
112
|
if (err instanceof AuthConfigError)
|
|
101
113
|
return EXIT.auth;
|
|
114
|
+
if (err instanceof TemplateBuildTimeoutError)
|
|
115
|
+
return EXIT.timeout;
|
|
102
116
|
if (err instanceof NotFoundError)
|
|
103
117
|
return EXIT.notFound;
|
|
104
118
|
if (err instanceof OperationTimeoutError || isNamed(err, 'TimeoutError'))
|
package/dist/format.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `--json` output is the API's own objects (see README "JSON output"), so
|
|
4
4
|
* these functions only shape what people read.
|
|
5
5
|
*/
|
|
6
|
-
import type { AgentSession, DraftState, FileList, Operation, Secret, SecretAccessEvent, SecretVersion, TemplateBuild, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
|
|
6
|
+
import type { AgentSession, DraftState, FileList, Operation, Secret, SecretAccessEvent, SecretVersion, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
|
|
7
7
|
import type { ErrorInfo } from './errors.js';
|
|
8
8
|
export declare function json(value: unknown): string;
|
|
9
9
|
export declare function table(headers: readonly string[], rows: readonly (readonly string[])[]): string;
|
|
@@ -19,6 +19,13 @@ export declare function workspaceState(w: WorkspaceView): string;
|
|
|
19
19
|
export declare function workspaceRow(w: WorkspaceView): string[];
|
|
20
20
|
export declare function workspaceTable(ws: readonly WorkspaceView[]): string;
|
|
21
21
|
export declare function isReady(w: WorkspaceView): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The startup of a workspace's template version (contracts §24.4) in one line: state, trigger, and for a failure the
|
|
24
|
+
* step or service, exit code and reason. Null when the version has no start commands or services.
|
|
25
|
+
*/
|
|
26
|
+
export declare function startupText(s: WorkspaceView['startup'] | undefined): string | null;
|
|
27
|
+
/** The failed step's output tail, indented, after the detail block (empty otherwise). */
|
|
28
|
+
export declare function startupTail(s: WorkspaceView['startup'] | undefined): string;
|
|
22
29
|
export declare function workspaceDetail(w: WorkspaceView): string;
|
|
23
30
|
export declare const OPERATION_HEADERS: readonly ["ID", "KIND", "STATE", "REASON", "CREATED", "COMPLETED"];
|
|
24
31
|
export declare function operationTable(ops: readonly Operation[]): string;
|
|
@@ -36,7 +43,10 @@ export declare function bindingsText(b: WorkspaceSecretBindings): string;
|
|
|
36
43
|
* freshness, then meters and allowances as tables. Columns absent from the response are omitted.
|
|
37
44
|
*/
|
|
38
45
|
export declare function usageText(summary: unknown): string;
|
|
39
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. A failed
|
|
48
|
+
* operation also shows `retryable=true|false`.
|
|
49
|
+
*/
|
|
40
50
|
export declare function errorText(e: ErrorInfo, hint?: string): string;
|
|
41
51
|
export declare function templateTable(rows: readonly TemplateSummary[]): string;
|
|
42
52
|
/** Permission bits as the four-digit octal `ls -l` users read (e.g. 0755). */
|
|
@@ -49,3 +59,10 @@ export declare function changesTable(rows: readonly WorkspaceChange[]): string;
|
|
|
49
59
|
export declare function draftText(d: TemplateDraft): string;
|
|
50
60
|
export declare function draftStateTable(rows: readonly DraftState[]): string;
|
|
51
61
|
export declare function buildText(b: TemplateBuild): string;
|
|
62
|
+
export declare function buildTable(rows: readonly TemplateBuild[]): string;
|
|
63
|
+
export declare function languageTable(l: TemplateLanguages): string;
|
|
64
|
+
export declare function packageTable(page: TemplatePackagePage): string;
|
|
65
|
+
/** A template: identity, what open resolves to, its versions, and the open version's settings. */
|
|
66
|
+
export declare function templateDetailText(t: TemplateDetail): string;
|
|
67
|
+
/** A version's settings (contracts §24.3), one line per part. */
|
|
68
|
+
export declare function settingsText(s: TemplateSettings): string;
|
package/dist/format.js
CHANGED
|
@@ -67,8 +67,43 @@ export function workspaceTable(ws) {
|
|
|
67
67
|
export function isReady(w) {
|
|
68
68
|
return w.observed_state === 'running' && w.desired_state === 'running' && w.deleted_at === null;
|
|
69
69
|
}
|
|
70
|
+
/** A workspace's origin: a fork, a draft state (draft test instance) or a template version (version test instance). */
|
|
71
|
+
function originText(o) {
|
|
72
|
+
switch (o.kind) {
|
|
73
|
+
case 'fork':
|
|
74
|
+
return `fork of ${o.workspace_id}`;
|
|
75
|
+
case 'draft_state':
|
|
76
|
+
return `draft state ${o.checkpoint_id ?? '(capturing)'} of ${o.draft_workspace_id}`;
|
|
77
|
+
default:
|
|
78
|
+
return `template ${o.template_id} version ${o.version}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The startup of a workspace's template version (contracts §24.4) in one line: state, trigger, and for a failure the
|
|
83
|
+
* step or service, exit code and reason. Null when the version has no start commands or services.
|
|
84
|
+
*/
|
|
85
|
+
export function startupText(s) {
|
|
86
|
+
if (!s)
|
|
87
|
+
return null;
|
|
88
|
+
if (s.state === 'pending')
|
|
89
|
+
return 'pending (a start is in progress)';
|
|
90
|
+
const where = s.service ? `service ${s.service}` : s.step ? `step ${s.step}` : null;
|
|
91
|
+
if (s.state === 'failed') {
|
|
92
|
+
const exit = s.exit_code !== null ? `, exit ${s.exit_code}` : '';
|
|
93
|
+
return `failed${where ? ` at ${where}` : ''}${exit}${s.reason ? ` (${s.reason})` : ''}${s.trigger ? `, trigger ${s.trigger}` : ''}; the next open runs it again`;
|
|
94
|
+
}
|
|
95
|
+
return `${s.state}${s.trigger ? ` (${s.trigger})` : ''}${s.state === 'running' && where ? `: ${where}` : ''}`;
|
|
96
|
+
}
|
|
97
|
+
/** The failed step's output tail, indented, after the detail block (empty otherwise). */
|
|
98
|
+
export function startupTail(s) {
|
|
99
|
+
if (!s || s.state !== 'failed' || !s.output_tail)
|
|
100
|
+
return '';
|
|
101
|
+
const lines = s.output_tail.replace(/\n$/, '').split('\n');
|
|
102
|
+
return `startup output (last ${Buffer.byteLength(s.output_tail)} bytes):\n${lines.map((l) => ` | ${l}`).join('\n')}\n`;
|
|
103
|
+
}
|
|
70
104
|
export function workspaceDetail(w) {
|
|
71
105
|
const c = w.ceilings;
|
|
106
|
+
const startup = startupText(w.startup);
|
|
72
107
|
return kv([
|
|
73
108
|
['id', w.id],
|
|
74
109
|
['key', w.workspace_key],
|
|
@@ -81,19 +116,20 @@ export function workspaceDetail(w) {
|
|
|
81
116
|
['caps', pairs(w.caps)],
|
|
82
117
|
['operation', w.active_operation ? `${operationSummary(w.active_operation)} ${w.active_operation.id}` : '-'],
|
|
83
118
|
['pending', w.pending_reason ?? '-'],
|
|
119
|
+
...(startup ? [['startup', startup]] : []),
|
|
84
120
|
['forked from', w.forked_from_workspace_id ?? '-'],
|
|
85
121
|
...(w.lifetime !== undefined
|
|
86
122
|
? [
|
|
87
123
|
['lifetime', w.lifetime === 'session' ? `session (ends after ${w.idle_timeout_seconds ?? '-'} s idle, or on close)` : 'persistent'],
|
|
88
124
|
['purpose', w.purpose],
|
|
89
125
|
['disk layout', w.disk_layout],
|
|
90
|
-
...(w.origin ? [['origin', w.origin
|
|
126
|
+
...(w.origin ? [['origin', originText(w.origin)]] : []),
|
|
91
127
|
...(w.ended_reason ? [['ended', w.ended_reason]] : []),
|
|
92
128
|
]
|
|
93
129
|
: []),
|
|
94
130
|
['created', time(w.created_at)],
|
|
95
131
|
['deleted', time(w.deleted_at)],
|
|
96
|
-
]);
|
|
132
|
+
]) + startupTail(w.startup);
|
|
97
133
|
}
|
|
98
134
|
export const OPERATION_HEADERS = ['ID', 'KIND', 'STATE', 'REASON', 'CREATED', 'COMPLETED'];
|
|
99
135
|
export function operationTable(ops) {
|
|
@@ -212,7 +248,10 @@ export function usageText(summary) {
|
|
|
212
248
|
out += `\n${allowances}`;
|
|
213
249
|
return out || json(summary);
|
|
214
250
|
}
|
|
215
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. A failed
|
|
253
|
+
* operation also shows `retryable=true|false`.
|
|
254
|
+
*/
|
|
216
255
|
export function errorText(e, hint) {
|
|
217
256
|
const meta = [`code=${e.code}`];
|
|
218
257
|
if (e.status !== undefined)
|
|
@@ -223,6 +262,8 @@ export function errorText(e, hint) {
|
|
|
223
262
|
meta.push(`operation_id=${e.operation_id}`);
|
|
224
263
|
if (e.source === 'cell')
|
|
225
264
|
meta.push('source=cell');
|
|
265
|
+
if (e.code === 'operation_failed' && e.retryable !== undefined)
|
|
266
|
+
meta.push(`retryable=${e.retryable}`);
|
|
226
267
|
const reason = e.details && typeof e.details.reason === 'string' ? ` (${e.details.reason})` : '';
|
|
227
268
|
const issues = Array.isArray(e.details?.issues)
|
|
228
269
|
? e.details.issues.filter(isObject).map((i) => ` ${scalar(i.path)}: ${scalar(i.message)}\n`).join('')
|
|
@@ -296,13 +337,62 @@ export function draftText(d) {
|
|
|
296
337
|
export function draftStateTable(rows) {
|
|
297
338
|
return table(['STATE', 'LABEL', 'CAPTURED', 'BYTES', 'TEST INSTANCES'], rows.map((st) => [st.checkpoint_id, st.label ?? '-', time(st.captured_at), st.bytes === null ? '-' : String(st.bytes), String(st.test_instances)]));
|
|
298
339
|
}
|
|
340
|
+
function buildSource(b) {
|
|
341
|
+
if (b.source_kind === 'workspace')
|
|
342
|
+
return `workspace ${b.source_workspace_id ?? '-'}${b.source_checkpoint_id ? ` checkpoint ${b.source_checkpoint_id}` : ''}`;
|
|
343
|
+
const v2 = b.provenance.recipe_schema === 'shardflux.template-recipe.v2';
|
|
344
|
+
return `recipe${v2 ? ' v2' : ''}${b.provenance.recipe_sha256 ? ` (sha256 ${b.provenance.recipe_sha256.slice(0, 12)})` : ''}`;
|
|
345
|
+
}
|
|
299
346
|
export function buildText(b) {
|
|
300
|
-
|
|
347
|
+
const settled = b.state === 'published' || b.state === 'failed' || b.state === 'canceled' || b.state === 'succeeded';
|
|
348
|
+
const denied = b.denied_hosts ?? [];
|
|
349
|
+
return (kv([
|
|
301
350
|
['build', b.id],
|
|
302
351
|
['template', `${b.template.slug} v${b.target_version ?? '?'}`],
|
|
303
352
|
['state', `${b.state}${b.failure ? ` (${b.failure.code}: ${b.failure.message})` : ''}`],
|
|
304
|
-
['registration', b.registration.state],
|
|
305
|
-
['source', b
|
|
353
|
+
['registration', `${b.registration.state}${b.registration.error ? ` (${b.registration.error.code}: ${b.registration.error.message})` : ''}`],
|
|
354
|
+
['source', buildSource(b)],
|
|
355
|
+
['network', b.network.mode === 'none' ? 'none' : `allowlist ${b.network.allow_hosts.join(', ') || '(empty)'}`],
|
|
356
|
+
...(denied.length ? [['denied hosts', `${denied.join(', ')} (allow one: add it to build.network.extra_hosts and build again)`]] : []),
|
|
306
357
|
['version', b.template_version ? `${b.template_version.id}${b.template_version.published ? ' (published)' : ' (unpublished)'}` : '-'],
|
|
307
|
-
|
|
358
|
+
['created', time(b.created_at)],
|
|
359
|
+
]) + (settled ? '' : `the build continues server side (--wait follows it; shard templates builds get ${b.id} reports it)\n`));
|
|
360
|
+
}
|
|
361
|
+
export function buildTable(rows) {
|
|
362
|
+
return table(['ID', 'TEMPLATE', 'STATE', 'REGISTRATION', 'SOURCE', 'CREATED'], rows.map((b) => [b.id, `${b.template.slug} v${b.target_version ?? '?'}`, b.state, b.registration.state, b.source_kind === 'workspace' ? 'workspace' : b.provenance.recipe_schema === 'shardflux.template-recipe.v2' ? 'recipe v2' : 'recipe', time(b.created_at)]));
|
|
363
|
+
}
|
|
364
|
+
export function languageTable(l) {
|
|
365
|
+
return table(['ID', 'NAME', 'VERSION', 'DEFAULT', 'SOURCE', 'HOSTS'], l.data.map((x) => [x.id, x.name, x.version, x.default ? 'yes' : '-', x.included ? `included${x.base_version ? ` (${x.base_version})` : ''}` : x.apt.length ? `apt ${x.apt.join(' ')}` : 'download', x.hosts.join(', ') || '-']));
|
|
366
|
+
}
|
|
367
|
+
export function packageTable(page) {
|
|
368
|
+
return table(['NAME', 'VERSION', 'SUMMARY'], page.data.map((p) => [p.name, p.version ?? '-', p.summary ? (p.summary.length > 80 ? `${p.summary.slice(0, 79)}…` : p.summary) : '-']));
|
|
369
|
+
}
|
|
370
|
+
/** A template: identity, what open resolves to, its versions, and the open version's settings. */
|
|
371
|
+
export function templateDetailText(t) {
|
|
372
|
+
const open = t.open_version;
|
|
373
|
+
const head = kv([
|
|
374
|
+
['template', `${t.slug} (${t.name})`],
|
|
375
|
+
['owner', `${t.owner}${t.category ? `, ${t.category}` : ''}`],
|
|
376
|
+
['open resolves to', t.open_resolves_to ? `v${t.open_resolves_to.version} (${t.open_resolves_to.owner})` : '-'],
|
|
377
|
+
['draft', t.draft ? `${t.draft.workspace_key} (${t.draft.observed_state})` : '-'],
|
|
378
|
+
['archived', time(t.archived_at)],
|
|
379
|
+
]);
|
|
380
|
+
const versions = t.versions.length
|
|
381
|
+
? table(['VERSION', 'STATE', 'SOURCE', 'LAYOUTS', 'CREATED', 'DESCRIPTION'], t.versions.map((v) => [`v${v.version}${v.is_open_version ? ' *' : ''}`, v.state, v.source?.kind ?? '-', v.disk_layouts.join(',') || '-', time(v.created_at), v.description ?? '-']))
|
|
382
|
+
: 'No versions.\n';
|
|
383
|
+
return `${head}\n${versions}${open?.settings ? `\nsettings of v${open.version}:\n${settingsText(open.settings)}` : ''}`;
|
|
384
|
+
}
|
|
385
|
+
/** A version's settings (contracts §24.3), one line per part. */
|
|
386
|
+
export function settingsText(s) {
|
|
387
|
+
const inputs = Object.entries(s.inputs).map(([n, i]) => `${n} (${i.kind}${i.required ? ', required' : ''}${i.default !== null ? `, default ${JSON.stringify(i.default)}` : ''})`);
|
|
388
|
+
const d = s.defaults;
|
|
389
|
+
const egress = d.egress ? (d.egress.mode === 'none' ? 'none' : `allowlist ${d.egress.allow_hosts.join(', ')}`) : 'internet (the project policy)';
|
|
390
|
+
return kv([
|
|
391
|
+
['env', Object.keys(s.env).length ? Object.keys(s.env).join(', ') : '-'],
|
|
392
|
+
['inputs', inputs.length ? inputs.join('; ') : '-'],
|
|
393
|
+
['start', s.start.length ? s.start.map((c) => `${c.name} (${c.when})`).join(', ') : '-'],
|
|
394
|
+
['services', Object.keys(s.services).length ? Object.entries(s.services).map(([n, v]) => `${n}${v.ready ? ('port' in v.ready ? ` (ready: port ${v.ready.port})` : ' (ready: command)') : ''}`).join(', ') : '-'],
|
|
395
|
+
['lifetime', `${d.lifetime}${d.idle_timeout_seconds ? `, idle ${d.idle_timeout_seconds} s` : ''}`],
|
|
396
|
+
['egress', egress],
|
|
397
|
+
]).replace(/^/gm, ' ');
|
|
308
398
|
}
|
package/dist/main.js
CHANGED
|
@@ -31,6 +31,9 @@ export function abortableSleep(signal) {
|
|
|
31
31
|
function hintFor(info) {
|
|
32
32
|
if (info.code === 'timeout' && info.operation_id)
|
|
33
33
|
return `the operation continues server side; keep waiting with: shard operations wait ${info.operation_id}`;
|
|
34
|
+
if (info.code === 'operation_failed' && info.retryable) {
|
|
35
|
+
return info.details?.error_code === 'capacity_unavailable' ? 'no host could admit it before its deadline; nothing was started. Retry later.' : 'the operation may succeed if you retry it later.';
|
|
36
|
+
}
|
|
34
37
|
if (info.code === 'interrupted' && info.operation_id)
|
|
35
38
|
return `keep waiting with: shard operations wait ${info.operation_id}`;
|
|
36
39
|
if (info.code === 'unauthenticated' && info.status === undefined)
|