@miosa/cli 1.1.18 → 1.1.20
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 +0 -21
- package/dist/client.d.ts +1 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +44 -118
- package/dist/client.js.map +1 -1
- package/dist/commands/databases.d.ts.map +1 -1
- package/dist/commands/databases.js +33 -10
- package/dist/commands/databases.js.map +1 -1
- package/dist/commands/devices.js +2 -5
- package/dist/commands/devices.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +127 -58
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +91 -23
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/templates.d.ts.map +1 -1
- package/dist/commands/templates.js +252 -521
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/util.d.ts +0 -1
- package/dist/commands/util.d.ts.map +1 -1
- package/dist/commands/util.js +2 -15
- package/dist/commands/util.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +1 -5
- package/dist/commands/watch.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +2 -18
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -37
- package/dist/errors.js.map +1 -1
- package/dist/net-diagnosis.d.ts +87 -0
- package/dist/net-diagnosis.d.ts.map +1 -0
- package/dist/net-diagnosis.js +408 -0
- package/dist/net-diagnosis.js.map +1 -0
- package/dist/types.d.ts +6 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/api-validation.d.ts +0 -53
- package/dist/api-validation.d.ts.map +0 -1
- package/dist/api-validation.js +0 -200
- package/dist/api-validation.js.map +0 -1
- package/dist/endpoint.d.ts +0 -37
- package/dist/endpoint.d.ts.map +0 -1
- package/dist/endpoint.js +0 -123
- package/dist/endpoint.js.map +0 -1
|
@@ -5,7 +5,6 @@ import { MiosaClient } from "../client.js";
|
|
|
5
5
|
import { renderTable } from "../ui/table.js";
|
|
6
6
|
import { spin } from "../ui/spinner.js";
|
|
7
7
|
import { handleError, isJsonMode, printJson } from "./util.js";
|
|
8
|
-
import { ApiResponseError, UserError } from "../errors.js";
|
|
9
8
|
function productTemplates(raw) {
|
|
10
9
|
const rows = Array.isArray(raw["data"])
|
|
11
10
|
? raw["data"]
|
|
@@ -36,7 +35,7 @@ function fmtTemplateState(state) {
|
|
|
36
35
|
return chalk.dim("-");
|
|
37
36
|
if (state === "ready" || state === "active")
|
|
38
37
|
return chalk.green(state);
|
|
39
|
-
if (state === "building" || state === "pending"
|
|
38
|
+
if (state === "building" || state === "pending")
|
|
40
39
|
return chalk.yellow(state);
|
|
41
40
|
if (state === "failed" || state === "error")
|
|
42
41
|
return chalk.red(state);
|
|
@@ -46,190 +45,77 @@ function templateState(template) {
|
|
|
46
45
|
return template.state ?? template.status;
|
|
47
46
|
}
|
|
48
47
|
function templateImage(template) {
|
|
49
|
-
return template.image ?? template.image_id
|
|
48
|
+
return template.image ?? template.image_id;
|
|
50
49
|
}
|
|
51
50
|
function templateCreatedAt(template) {
|
|
52
51
|
return template.created_at ?? template.inserted_at;
|
|
53
52
|
}
|
|
54
|
-
|
|
55
|
-
function isCustom(template) {
|
|
56
|
-
if (typeof template.built_in === "boolean")
|
|
57
|
-
return !template.built_in;
|
|
58
|
-
// Older payloads without `built_in`: a custom row is the only kind with a
|
|
59
|
-
// UUID id and the "custom" category.
|
|
60
|
-
return template.category === "custom" || isUuid(template.id);
|
|
61
|
-
}
|
|
62
|
-
function isUuid(value) {
|
|
63
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* A definitive answer to "can I boot a sandbox from this right now?".
|
|
67
|
-
*
|
|
68
|
-
* A template row exists the moment `POST /api/v1/sandbox-templates` returns
|
|
69
|
-
* 201, but it has no bootable image until a build reaches "ready". Reporting
|
|
70
|
-
* the row's mere existence as readiness is what left the customer unable to
|
|
71
|
-
* tell whether his template had been created (2026-08-26 live customer call).
|
|
72
|
-
*/
|
|
73
|
-
function usability(template, builds) {
|
|
53
|
+
function templatePresence(template) {
|
|
74
54
|
const state = templateState(template);
|
|
75
55
|
const image = templateImage(template);
|
|
76
|
-
if (
|
|
77
|
-
return image
|
|
78
|
-
? { usable: true, reason: `build complete, image ${image}` }
|
|
79
|
-
: {
|
|
80
|
-
usable: false,
|
|
81
|
-
reason: "state is ready but no image was reported - contact support",
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (state === "archived") {
|
|
85
|
-
return { usable: false, reason: "archived" };
|
|
56
|
+
if (image) {
|
|
57
|
+
return { presence: "present", why: `image ${image} is recorded on the template` };
|
|
86
58
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
"snapshotting",
|
|
92
|
-
"running",
|
|
93
|
-
"pending",
|
|
94
|
-
].includes(build.state ?? ""));
|
|
95
|
-
if (active) {
|
|
96
|
-
return {
|
|
97
|
-
usable: false,
|
|
98
|
-
reason: `build ${shortId(active.id)} is ${active.state} - not finished yet`,
|
|
99
|
-
};
|
|
59
|
+
// A draft or failed build has no image BY DEFINITION - that is a confirmed
|
|
60
|
+
// absence, not a gap in our knowledge.
|
|
61
|
+
if (state === "draft" || state === "failed" || state === "error") {
|
|
62
|
+
return { presence: "absent", why: `template is ${state}, so no image was ever published` };
|
|
100
63
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// fail_build/2 records BUILDS_TEMPORARILY_UNAVAILABLE on the build while
|
|
104
|
-
// template_status_after_failure(:builds_disabled) keeps the template "draft"
|
|
105
|
-
// so its name stays reusable. Reporting only "no completed build yet (state
|
|
106
|
-
// draft)" there would hide the one fact the caller needs.
|
|
107
|
-
const failed = builds?.find((build) => build.state === "failed");
|
|
108
|
-
if (failed) {
|
|
109
|
-
const why = buildError(failed);
|
|
110
|
-
return {
|
|
111
|
-
usable: false,
|
|
112
|
-
reason: why ? `last build failed: ${why}` : "last build failed",
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
if (state === "failed") {
|
|
116
|
-
return { usable: false, reason: "last build failed" };
|
|
117
|
-
}
|
|
118
|
-
if (builds && builds.length === 0) {
|
|
119
|
-
return { usable: false, reason: "no build has been started yet" };
|
|
64
|
+
if (state === "building" || state === "pending" || state === "queued") {
|
|
65
|
+
return { presence: "absent", why: `build is ${state}, no image published yet` };
|
|
120
66
|
}
|
|
67
|
+
// Ready, but no image recorded. We genuinely cannot tell.
|
|
121
68
|
return {
|
|
122
|
-
|
|
123
|
-
|
|
69
|
+
presence: "unknown",
|
|
70
|
+
why: `template reports ${state ?? "no state"} but records no image - cannot confirm either way`,
|
|
124
71
|
};
|
|
125
72
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return Boolean(builds?.some((build) => build.error_code === "BUILDS_TEMPORARILY_UNAVAILABLE"));
|
|
133
|
-
}
|
|
134
|
-
function buildError(build) {
|
|
135
|
-
if (!build)
|
|
136
|
-
return undefined;
|
|
137
|
-
const message = build.error_message ?? build.error;
|
|
138
|
-
if (message && build.error_code)
|
|
139
|
-
return `${build.error_code}: ${message}`;
|
|
140
|
-
return message ?? build.error_code ?? undefined;
|
|
141
|
-
}
|
|
142
|
-
function shortId(id) {
|
|
143
|
-
return id.length > 12 ? `${id.slice(0, 8)}...` : id;
|
|
73
|
+
function fmtPresence(p) {
|
|
74
|
+
if (p === "present")
|
|
75
|
+
return chalk.green("present");
|
|
76
|
+
if (p === "absent")
|
|
77
|
+
return chalk.yellow("absent");
|
|
78
|
+
return chalk.dim("unverified");
|
|
144
79
|
}
|
|
145
80
|
function fmtBuildState(state) {
|
|
146
81
|
if (!state)
|
|
147
82
|
return chalk.dim("-");
|
|
148
|
-
if (state === "
|
|
83
|
+
if (state === "success" || state === "complete")
|
|
149
84
|
return chalk.green(state);
|
|
150
|
-
if (state === "building" ||
|
|
151
|
-
state === "running" ||
|
|
152
|
-
state === "pending" ||
|
|
153
|
-
state === "queued" ||
|
|
154
|
-
state === "certifying" ||
|
|
155
|
-
state === "snapshotting")
|
|
85
|
+
if (state === "building" || state === "running" || state === "pending")
|
|
156
86
|
return chalk.yellow(state);
|
|
157
|
-
if (state === "failed" || state === "error"
|
|
87
|
+
if (state === "failed" || state === "error")
|
|
158
88
|
return chalk.red(state);
|
|
159
89
|
return chalk.dim(state);
|
|
160
90
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return contents;
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
if (err instanceof UserError)
|
|
174
|
-
throw err;
|
|
175
|
-
throw new UserError(`Cannot read Dockerfile at ${path}: ${err instanceof Error ? err.message : String(err)}`, "Pass --dockerfile with a path to a readable Dockerfile.");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/** Fetch the tenant's own templates, resolved to their full shape. */
|
|
179
|
-
async function fetchTemplate(api, id) {
|
|
180
|
-
return unwrapTemplate(await api.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}`));
|
|
181
|
-
}
|
|
182
|
-
async function fetchBuilds(api, id) {
|
|
183
|
-
return unwrapBuilds(await api.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}/builds`));
|
|
184
|
-
}
|
|
185
|
-
/** Print the definitive existence/usability block for one custom template. */
|
|
186
|
-
function printTemplateDetail(template, builds, opts) {
|
|
187
|
-
const status = usability(template, builds);
|
|
188
|
-
const reference = template.slug ?? template.id;
|
|
91
|
+
// The whole point of a template is to reference it from your own code. Print
|
|
92
|
+
// ready-to-paste create() calls for both SDKs and the CLI, using this exact
|
|
93
|
+
// template's id, so a customer never has to guess the field name or hunt for
|
|
94
|
+
// the id in a truncated table. If the template has no usable image yet, we say
|
|
95
|
+
// so instead of implying the snippet will boot today.
|
|
96
|
+
function printIntegrationSnippet(t) {
|
|
97
|
+
const ref = t.id;
|
|
98
|
+
const isReady = (templateState(t) ?? "") === "ready" && !!templateImage(t);
|
|
99
|
+
console.log(` ${chalk.bold("Use this template")}`);
|
|
189
100
|
console.log();
|
|
190
|
-
console.log(
|
|
191
|
-
console.log(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
console.log(` ${chalk.bold("State")} ${fmtTemplateState(templateState(template))}`);
|
|
195
|
-
console.log(` ${chalk.bold("Exists")} ${opts.verified
|
|
196
|
-
? chalk.green("yes (confirmed by a follow-up read)")
|
|
197
|
-
: chalk.green("yes")}`);
|
|
198
|
-
console.log(` ${chalk.bold("Usable")} ${status.usable
|
|
199
|
-
? chalk.green(`yes - ${status.reason}`)
|
|
200
|
-
: chalk.yellow(`no - ${status.reason}`)}`);
|
|
201
|
-
const image = templateImage(template);
|
|
202
|
-
console.log(` ${chalk.bold("Image")} ${image ?? chalk.dim("none yet")}`);
|
|
203
|
-
if (builds) {
|
|
204
|
-
console.log(` ${chalk.bold("Builds")} ${builds.length === 0
|
|
205
|
-
? chalk.dim("none")
|
|
206
|
-
: `${builds.length} (latest ${shortId(builds[0]?.id ?? "")} ${builds[0]?.state ?? "unknown"})`}`);
|
|
207
|
-
const failure = buildError(builds.find((build) => build.state === "failed"));
|
|
208
|
-
if (failure)
|
|
209
|
-
console.log(` ${chalk.bold("Failure")} ${chalk.red(failure)}`);
|
|
210
|
-
}
|
|
211
|
-
const createdAt = templateCreatedAt(template);
|
|
212
|
-
if (createdAt)
|
|
213
|
-
console.log(` ${chalk.bold("Created")} ${createdAt}`);
|
|
214
|
-
if (template.updated_at)
|
|
215
|
-
console.log(` ${chalk.bold("Updated")} ${template.updated_at}`);
|
|
101
|
+
console.log(chalk.dim(" # Python SDK"));
|
|
102
|
+
console.log(" from miosa import Miosa");
|
|
103
|
+
console.log(" client = Miosa()");
|
|
104
|
+
console.log(` sandbox = client.sandboxes.create(template_id=${JSON.stringify(ref)})`);
|
|
216
105
|
console.log();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.log(chalk.yellow(` Not bootable yet. "miosa sandbox create --template ${reference}" will fail until a build reaches "ready".`));
|
|
222
|
-
if (buildsGatedOff(builds)) {
|
|
223
|
-
// Nothing for the caller to fix, and re-running create with the same
|
|
224
|
-
// name works because the row stays a draft and create_template/3 upserts
|
|
225
|
-
// over a draft for the same tenant+slug.
|
|
226
|
-
console.log(chalk.dim(" This is a platform build gate, not a problem with your Dockerfile. Nothing to fix on your side."));
|
|
227
|
-
console.log(chalk.dim(` The name stays yours: re-run the same create, or "miosa templates rebuild ${reference}", once builds are re-enabled.`));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
console.log(chalk.dim(` Builds: miosa templates builds ${template.id}`));
|
|
231
|
-
console.log(chalk.dim(` Re-verify: miosa templates get ${reference}`));
|
|
106
|
+
console.log(chalk.dim(" # TypeScript SDK"));
|
|
107
|
+
console.log(' import { Miosa } from "@miosa/sdk";');
|
|
108
|
+
console.log(" const client = new Miosa();");
|
|
109
|
+
console.log(` const sandbox = await client.sandboxes.create({ templateId: ${JSON.stringify(ref)} });`);
|
|
232
110
|
console.log();
|
|
111
|
+
console.log(chalk.dim(" # CLI"));
|
|
112
|
+
console.log(` miosa sandbox create --template ${ref}`);
|
|
113
|
+
console.log();
|
|
114
|
+
if (!isReady) {
|
|
115
|
+
console.log(chalk.yellow(" Note: this template has no usable image yet, so create() will not boot it"));
|
|
116
|
+
console.log(chalk.dim(` until its build completes. Check: miosa templates builds ${ref}`));
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
233
119
|
}
|
|
234
120
|
export function register(program) {
|
|
235
121
|
const templates = program
|
|
@@ -242,7 +128,9 @@ export function register(program) {
|
|
|
242
128
|
.option("--json", "Output the complete catalog as JSON")
|
|
243
129
|
.action(async (opts) => {
|
|
244
130
|
try {
|
|
245
|
-
const
|
|
131
|
+
const config = loadConfig();
|
|
132
|
+
const client = new MiosaClient(config);
|
|
133
|
+
const raw = await client.apiGet("/api/v1/templates");
|
|
246
134
|
const rows = productTemplates(raw);
|
|
247
135
|
const filtered = opts.product
|
|
248
136
|
? rows.filter((template) => template.product === opts.product)
|
|
@@ -268,7 +156,7 @@ export function register(program) {
|
|
|
268
156
|
]);
|
|
269
157
|
}
|
|
270
158
|
catch (err) {
|
|
271
|
-
handleError(err
|
|
159
|
+
handleError(err);
|
|
272
160
|
}
|
|
273
161
|
});
|
|
274
162
|
templates
|
|
@@ -278,12 +166,13 @@ export function register(program) {
|
|
|
278
166
|
.option("--json", "Output raw readiness rows")
|
|
279
167
|
.action(async (id, opts) => {
|
|
280
168
|
try {
|
|
281
|
-
const
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
const client = new MiosaClient(config);
|
|
171
|
+
const raw = await client.apiGet("/api/v1/templates");
|
|
282
172
|
const rows = productTemplates(raw);
|
|
283
|
-
const template = rows.find((candidate) => candidate.id === id &&
|
|
284
|
-
(!opts.product || candidate.product === opts.product));
|
|
173
|
+
const template = rows.find((candidate) => candidate.id === id && (!opts.product || candidate.product === opts.product));
|
|
285
174
|
if (!template)
|
|
286
|
-
throw new
|
|
175
|
+
throw new Error(`Product template not found: ${id}`);
|
|
287
176
|
const readiness = template.sizes ?? [];
|
|
288
177
|
if (isJsonMode(opts)) {
|
|
289
178
|
printJson(readiness);
|
|
@@ -315,109 +204,99 @@ export function register(program) {
|
|
|
315
204
|
]);
|
|
316
205
|
}
|
|
317
206
|
catch (err) {
|
|
318
|
-
handleError(err
|
|
207
|
+
handleError(err);
|
|
319
208
|
}
|
|
320
209
|
});
|
|
321
210
|
// list
|
|
322
211
|
templates
|
|
323
212
|
.command("list")
|
|
324
|
-
.description("List sandbox templates
|
|
325
|
-
.option("--mine", "Show only templates this workspace created")
|
|
326
|
-
.option("--built-in", "Show only platform built-in templates")
|
|
327
|
-
.option("--verify", "Read each of your templates back individually to confirm it exists and fill in the slug and created date the catalog omits")
|
|
213
|
+
.description("List sandbox templates, distinguishing confirmed-absent from cannot-verify")
|
|
328
214
|
.option("--json", "Output raw JSON")
|
|
329
215
|
.action(async (opts) => {
|
|
216
|
+
const config = loadConfig();
|
|
217
|
+
const client = new MiosaClient(config);
|
|
218
|
+
const json = isJsonMode(opts);
|
|
219
|
+
const spinner = json ? null : spin("Fetching templates...");
|
|
220
|
+
const checkedAt = new Date().toISOString();
|
|
221
|
+
let rows;
|
|
330
222
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
catch (err) {
|
|
339
|
-
spinner?.fail("Could not fetch templates");
|
|
340
|
-
throw err;
|
|
341
|
-
}
|
|
342
|
-
let mine = rows.filter(isCustom);
|
|
343
|
-
const platform = rows.filter((row) => !isCustom(row));
|
|
344
|
-
if (opts.verify && mine.length > 0) {
|
|
345
|
-
spinner?.stop();
|
|
346
|
-
// The catalog rendering drops `slug` and `inserted_at` from custom
|
|
347
|
-
// rows, so the only way to report them is a per-row read. This also
|
|
348
|
-
// proves each row is individually retrievable, not just present in
|
|
349
|
-
// an aggregate list.
|
|
350
|
-
mine = await Promise.all(mine.map(async (row) => {
|
|
351
|
-
try {
|
|
352
|
-
return await fetchTemplate(api, row.id);
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
return row;
|
|
356
|
-
}
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
223
|
+
rows = unwrapTemplates(await client.apiGet("/api/v1/sandbox-templates"));
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// The listing could NOT be verified. This is emphatically not "no
|
|
227
|
+
// templates" - reporting an empty list here would tell a customer their
|
|
228
|
+
// templates are gone when we simply failed to ask.
|
|
359
229
|
spinner?.stop();
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
230
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
231
|
+
if (json) {
|
|
232
|
+
printJson({
|
|
233
|
+
listing: {
|
|
234
|
+
state: "unverified",
|
|
235
|
+
source: "live",
|
|
236
|
+
checkedAt,
|
|
237
|
+
reason,
|
|
238
|
+
},
|
|
239
|
+
templates: null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(` ${chalk.yellow("Cannot verify")} - the template list could not be retrieved.`);
|
|
245
|
+
console.log(` ${chalk.dim(reason)}`);
|
|
246
|
+
console.log(` ${chalk.dim("This is NOT the same as having no templates. Run: miosa doctor")}`);
|
|
247
|
+
}
|
|
248
|
+
// Non-zero: a script must be able to tell an unverified listing from an
|
|
249
|
+
// empty one.
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
spinner?.stop();
|
|
254
|
+
try {
|
|
365
255
|
if (json) {
|
|
366
|
-
printJson(
|
|
256
|
+
printJson({
|
|
257
|
+
// Never presented as current without saying when it was checked,
|
|
258
|
+
// and never cached: this is a live read every time.
|
|
259
|
+
listing: {
|
|
260
|
+
state: "confirmed",
|
|
261
|
+
source: "live",
|
|
262
|
+
checkedAt,
|
|
263
|
+
count: rows.length,
|
|
264
|
+
},
|
|
265
|
+
templates: rows.map((t) => {
|
|
266
|
+
const { presence, why } = templatePresence(t);
|
|
267
|
+
return { ...t, presence, presenceReason: why };
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
367
270
|
return;
|
|
368
271
|
}
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
console.log(chalk.dim("This workspace has not created any sandbox templates."));
|
|
372
|
-
console.log(chalk.dim(" Create one: miosa templates create --name my-template --dockerfile ./Dockerfile"));
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
console.log(chalk.dim("No templates found."));
|
|
376
|
-
}
|
|
272
|
+
if (rows.length === 0) {
|
|
273
|
+
console.log(chalk.dim(`No templates. Confirmed empty by a live read at ${checkedAt}.`));
|
|
377
274
|
return;
|
|
378
275
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const anySlug = selected.some((row) => Boolean(row.slug));
|
|
383
|
-
renderTable(selected, [
|
|
384
|
-
{
|
|
385
|
-
header: "SOURCE",
|
|
386
|
-
key: (t) => isCustom(t) ? chalk.cyan("yours") : chalk.dim("built-in"),
|
|
387
|
-
width: 10,
|
|
388
|
-
},
|
|
389
|
-
{ header: "ID", key: (t) => t.id, width: 38 },
|
|
390
|
-
...(anySlug
|
|
391
|
-
? [
|
|
392
|
-
{
|
|
393
|
-
header: "TEMPLATE REF",
|
|
394
|
-
key: (t) => t.slug ?? (isCustom(t) ? chalk.dim("-") : t.id),
|
|
395
|
-
width: 24,
|
|
396
|
-
},
|
|
397
|
-
]
|
|
398
|
-
: []),
|
|
399
|
-
{ header: "NAME", key: "name", width: 26 },
|
|
276
|
+
renderTable(rows, [
|
|
277
|
+
{ header: "ID", key: (t) => t.id.slice(0, 12), width: 14 },
|
|
278
|
+
{ header: "NAME", key: "name", width: 28 },
|
|
400
279
|
{
|
|
401
280
|
header: "STATE",
|
|
402
281
|
key: (t) => fmtTemplateState(templateState(t)),
|
|
403
|
-
width:
|
|
282
|
+
width: 12,
|
|
404
283
|
},
|
|
405
284
|
{
|
|
406
|
-
header: "
|
|
407
|
-
key: (t) =>
|
|
408
|
-
width:
|
|
285
|
+
header: "IMAGE",
|
|
286
|
+
key: (t) => fmtPresence(templatePresence(t).presence),
|
|
287
|
+
width: 12,
|
|
409
288
|
},
|
|
410
289
|
{
|
|
411
|
-
header: "IMAGE",
|
|
290
|
+
header: "IMAGE ID",
|
|
412
291
|
key: (t) => {
|
|
413
292
|
const image = templateImage(t);
|
|
414
293
|
return image
|
|
415
|
-
? image.length >
|
|
416
|
-
? `${image.slice(0,
|
|
294
|
+
? image.length > 26
|
|
295
|
+
? `${image.slice(0, 23)}...`
|
|
417
296
|
: image
|
|
418
297
|
: chalk.dim("-");
|
|
419
298
|
},
|
|
420
|
-
width:
|
|
299
|
+
width: 28,
|
|
421
300
|
},
|
|
422
301
|
{
|
|
423
302
|
header: "CREATED",
|
|
@@ -428,280 +307,161 @@ export function register(program) {
|
|
|
428
307
|
width: 12,
|
|
429
308
|
},
|
|
430
309
|
]);
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
console.log(
|
|
434
|
-
console.log(chalk.dim(
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
console.log(opts.mine
|
|
438
|
-
? chalk.dim(` ${mine.length} template${mine.length === 1 ? "" : "s"} created by this workspace. Platform built-ins are hidden; drop --mine to see them.`)
|
|
439
|
-
: chalk.dim(` ${mine.length} of these ${mine.length === 1 ? "is" : "are"} yours (SOURCE=yours); the other ${platform.length} are platform built-ins.`));
|
|
440
|
-
if (!opts.mine) {
|
|
441
|
-
console.log(chalk.dim(" Only yours: miosa templates list --mine"));
|
|
442
|
-
}
|
|
443
|
-
const unusable = mine.filter((row) => !usability(row).usable);
|
|
444
|
-
if (unusable.length > 0) {
|
|
445
|
-
// The catalog carries no build history, so the reason a row is
|
|
446
|
-
// not usable takes a second request. Point at it rather than
|
|
447
|
-
// leaving a bare "no" in the table.
|
|
448
|
-
const first = unusable[0];
|
|
449
|
-
console.log(chalk.yellow(` ${unusable.length} of yours ${unusable.length === 1 ? "has" : "have"} no usable build yet, so ${unusable.length === 1 ? "it cannot" : "they cannot"} boot a sandbox.`));
|
|
450
|
-
console.log(chalk.dim(` Reason for each: miosa templates get ${first?.slug ?? first?.id ?? "<id>"}`));
|
|
451
|
-
}
|
|
452
|
-
if (!opts.verify) {
|
|
453
|
-
console.log(chalk.dim(" The catalog omits the slug and created date of your own rows; add --verify to read them back individually."));
|
|
454
|
-
}
|
|
310
|
+
const unverified = rows.filter((t) => templatePresence(t).presence === "unknown");
|
|
311
|
+
if (unverified.length > 0) {
|
|
312
|
+
console.log();
|
|
313
|
+
console.log(chalk.dim(` ${unverified.length} template(s) report no image and could not be confirmed either way.`));
|
|
455
314
|
}
|
|
315
|
+
// The ID column is truncated to keep the table readable. Point users at
|
|
316
|
+
// the one command that prints the FULL, copyable id plus paste-ready
|
|
317
|
+
// SDK/CLI code for wiring the template into their own app.
|
|
456
318
|
console.log();
|
|
319
|
+
console.log(chalk.dim(` Run "miosa templates get <id>" for the full id and copy-paste integration code.`));
|
|
457
320
|
}
|
|
458
321
|
catch (err) {
|
|
459
|
-
handleError(err
|
|
322
|
+
handleError(err);
|
|
460
323
|
}
|
|
461
324
|
});
|
|
462
|
-
//
|
|
325
|
+
// versions
|
|
463
326
|
templates
|
|
464
|
-
.command("
|
|
465
|
-
.description("
|
|
327
|
+
.command("versions <id>")
|
|
328
|
+
.description("List a template's versions (a version is a build; specs are immutable)")
|
|
466
329
|
.option("--json", "Output raw JSON")
|
|
467
330
|
.action(async (id, opts) => {
|
|
468
331
|
try {
|
|
469
|
-
const
|
|
332
|
+
const config = loadConfig();
|
|
333
|
+
const client = new MiosaClient(config);
|
|
470
334
|
const json = isJsonMode(opts);
|
|
471
|
-
const spinner = json ? null : spin("Fetching
|
|
472
|
-
|
|
473
|
-
try {
|
|
474
|
-
template = await fetchTemplate(api, id);
|
|
475
|
-
}
|
|
476
|
-
catch (err) {
|
|
477
|
-
spinner?.fail(`Template not found: ${id}`);
|
|
478
|
-
throw err;
|
|
479
|
-
}
|
|
480
|
-
// Builds are what decide usability, and the read is scoped to the same
|
|
481
|
-
// template, so fetch them rather than guessing from `status` alone.
|
|
482
|
-
// Built-ins have no build history endpoint of their own.
|
|
483
|
-
const builds = isCustom(template)
|
|
484
|
-
? await fetchBuilds(api, template.id).catch(() => undefined)
|
|
485
|
-
: undefined;
|
|
335
|
+
const spinner = json ? null : spin("Fetching versions...");
|
|
336
|
+
const raw = (await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}/versions`));
|
|
486
337
|
spinner?.stop();
|
|
338
|
+
const rows = Array.isArray(raw) ? raw : (raw.data ?? []);
|
|
487
339
|
if (json) {
|
|
488
|
-
printJson(
|
|
489
|
-
...template,
|
|
490
|
-
usable: usability(template, builds).usable,
|
|
491
|
-
usable_reason: usability(template, builds).reason,
|
|
492
|
-
...(builds ? { builds } : {}),
|
|
493
|
-
});
|
|
340
|
+
printJson(rows);
|
|
494
341
|
return;
|
|
495
342
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
catch (err) {
|
|
499
|
-
handleError(err, opts);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
// create
|
|
503
|
-
templates
|
|
504
|
-
.command("create")
|
|
505
|
-
.description("Create a sandbox template from a Dockerfile")
|
|
506
|
-
.requiredOption("--name <name>", "Template name")
|
|
507
|
-
.requiredOption("--dockerfile <path>", "Path to Dockerfile to build the template from")
|
|
508
|
-
.option("--description <text>", "Human description of the template")
|
|
509
|
-
.option("--no-verify", "Skip the follow-up read that confirms the template exists")
|
|
510
|
-
.option("--json", "Output raw JSON")
|
|
511
|
-
.action(async (opts) => {
|
|
512
|
-
const json = isJsonMode(opts);
|
|
513
|
-
try {
|
|
514
|
-
const dockerfile = readDockerfile(opts.dockerfile);
|
|
515
|
-
const api = client();
|
|
516
|
-
const spinner = json
|
|
517
|
-
? null
|
|
518
|
-
: spin(`Creating template ${opts.name}...`);
|
|
519
|
-
let template;
|
|
520
|
-
try {
|
|
521
|
-
template = unwrapTemplate(await api.apiPost("/api/v1/sandbox-templates", {
|
|
522
|
-
name: opts.name,
|
|
523
|
-
dockerfile,
|
|
524
|
-
...(opts.description ? { description: opts.description } : {}),
|
|
525
|
-
}));
|
|
526
|
-
}
|
|
527
|
-
catch (err) {
|
|
528
|
-
// Leaving the spinner spinning under an error message was how the
|
|
529
|
-
// customer's failed create looked like a hung command.
|
|
530
|
-
spinner?.fail(`Could not create template ${opts.name}`);
|
|
531
|
-
throw err;
|
|
532
|
-
}
|
|
533
|
-
spinner?.succeed(`Created template ${template.name}`);
|
|
534
|
-
// The create response is rendered from the in-memory row, so its
|
|
535
|
-
// `current_build_id` is still null even though the controller has
|
|
536
|
-
// already enqueued the initial build. Read the row and its builds
|
|
537
|
-
// back to give a definitive answer instead of an optimistic one.
|
|
538
|
-
let verified = false;
|
|
539
|
-
let builds;
|
|
540
|
-
if (opts.verify) {
|
|
541
|
-
try {
|
|
542
|
-
template = await fetchTemplate(api, template.id);
|
|
543
|
-
verified = true;
|
|
544
|
-
}
|
|
545
|
-
catch {
|
|
546
|
-
verified = false;
|
|
547
|
-
}
|
|
548
|
-
builds = await fetchBuilds(api, template.id).catch(() => undefined);
|
|
549
|
-
}
|
|
550
|
-
if (json) {
|
|
551
|
-
printJson({
|
|
552
|
-
...template,
|
|
553
|
-
verified,
|
|
554
|
-
usable: usability(template, builds).usable,
|
|
555
|
-
usable_reason: usability(template, builds).reason,
|
|
556
|
-
...(builds ? { builds } : {}),
|
|
557
|
-
});
|
|
343
|
+
if (rows.length === 0) {
|
|
344
|
+
console.log(chalk.dim("No versions yet."));
|
|
558
345
|
return;
|
|
559
346
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
347
|
+
renderTable(rows, [
|
|
348
|
+
{
|
|
349
|
+
header: "VERSION",
|
|
350
|
+
key: (v) => (v.current ? chalk.bold(`v${v.version} *`) : `v${v.version}`),
|
|
351
|
+
width: 10,
|
|
352
|
+
},
|
|
353
|
+
{ header: "REF", key: (v) => v.ref ?? chalk.dim("-"), width: 26 },
|
|
354
|
+
{
|
|
355
|
+
header: "STATE",
|
|
356
|
+
key: (v) => fmtBuildState(v.state),
|
|
357
|
+
width: 12,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
// Whether this version can actually be run or migrated to. A failed
|
|
361
|
+
// build is listed - you need to see it to understand why the next
|
|
362
|
+
// version is unavailable - but it is not usable.
|
|
363
|
+
header: "USABLE",
|
|
364
|
+
key: (v) => (v.usable ? chalk.green("yes") : chalk.dim("no")),
|
|
365
|
+
width: 8,
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
header: "IMAGE",
|
|
369
|
+
key: (v) => v.image_id ?? chalk.dim("-"),
|
|
370
|
+
width: 26,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
header: "CREATED",
|
|
374
|
+
key: (v) => (v.created_at ? String(v.created_at).slice(0, 10) : chalk.dim("-")),
|
|
375
|
+
width: 12,
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(chalk.dim(" * = current. Specs are immutable: editing a template mints a new version,"));
|
|
380
|
+
console.log(chalk.dim(" and existing sandboxes keep running the version they were created from."));
|
|
566
381
|
}
|
|
567
382
|
catch (err) {
|
|
568
|
-
handleError(err
|
|
383
|
+
handleError(err);
|
|
569
384
|
}
|
|
570
385
|
});
|
|
571
|
-
//
|
|
386
|
+
// get
|
|
572
387
|
templates
|
|
573
|
-
.command("
|
|
574
|
-
.description("
|
|
575
|
-
.requiredOption("--dockerfile <path>", "Path to the new Dockerfile")
|
|
576
|
-
.option("--description <text>", "Replace the description")
|
|
388
|
+
.command("get <id>")
|
|
389
|
+
.description("Get sandbox template details")
|
|
577
390
|
.option("--json", "Output raw JSON")
|
|
578
391
|
.action(async (id, opts) => {
|
|
579
|
-
const json = isJsonMode(opts);
|
|
580
392
|
try {
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
catch (err) {
|
|
589
|
-
spinner?.fail(`Template not found: ${id}`);
|
|
590
|
-
throw err;
|
|
591
|
-
}
|
|
592
|
-
if (!isCustom(existing)) {
|
|
593
|
-
spinner?.fail(`${existing.name} is a platform built-in`);
|
|
594
|
-
throw new UserError(`${existing.id} is a platform built-in template and cannot be updated.`, "Create your own template instead: miosa templates create --name <name> --dockerfile ./Dockerfile");
|
|
595
|
-
}
|
|
596
|
-
const state = templateState(existing);
|
|
597
|
-
// The API has two update mechanisms and which one applies depends on
|
|
598
|
-
// whether the row already owns a usable image:
|
|
599
|
-
//
|
|
600
|
-
// draft / failed -> POST /api/v1/sandbox-templates upserts the row
|
|
601
|
-
// in place for the same tenant+slug, replacing build_spec and
|
|
602
|
-
// clearing image_id/current_build_id.
|
|
603
|
-
// ready / building / archived -> that upsert is a real uniqueness
|
|
604
|
-
// collision, so the supported path is a new build from the new
|
|
605
|
-
// spec: POST /api/v1/sandbox-templates/:id/builds.
|
|
606
|
-
const replaceInPlace = state === "draft" || state === "failed";
|
|
607
|
-
let template;
|
|
608
|
-
let build;
|
|
609
|
-
if (replaceInPlace) {
|
|
610
|
-
spinner?.start(`Replacing the spec of ${existing.name}...`);
|
|
611
|
-
try {
|
|
612
|
-
template = unwrapTemplate(await api.apiPost("/api/v1/sandbox-templates", {
|
|
613
|
-
name: existing.name,
|
|
614
|
-
...(existing.slug ? { slug: existing.slug } : {}),
|
|
615
|
-
dockerfile,
|
|
616
|
-
...(opts.description !== undefined
|
|
617
|
-
? { description: opts.description }
|
|
618
|
-
: existing.description
|
|
619
|
-
? { description: existing.description }
|
|
620
|
-
: {}),
|
|
621
|
-
}));
|
|
622
|
-
}
|
|
623
|
-
catch (err) {
|
|
624
|
-
spinner?.fail(`Could not update ${existing.name}`);
|
|
625
|
-
throw err;
|
|
626
|
-
}
|
|
627
|
-
spinner?.succeed(`Replaced the spec of ${template.name} in place (it had no usable build)`);
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
spinner?.start(`Starting a new build of ${existing.name}...`);
|
|
631
|
-
try {
|
|
632
|
-
const raw = await api.apiPost(`/api/v1/sandbox-templates/${encodeURIComponent(existing.id)}/builds`, { dockerfile });
|
|
633
|
-
build = raw.data ?? raw;
|
|
634
|
-
}
|
|
635
|
-
catch (err) {
|
|
636
|
-
spinner?.fail(`Could not start a new build of ${existing.name}`);
|
|
637
|
-
throw err;
|
|
638
|
-
}
|
|
639
|
-
spinner?.succeed(`Started build ${shortId(build.id)} of ${existing.name} from the new Dockerfile`);
|
|
640
|
-
template = await fetchTemplate(api, existing.id).catch(() => existing);
|
|
641
|
-
}
|
|
642
|
-
const builds = await fetchBuilds(api, template.id).catch(() => undefined);
|
|
393
|
+
const config = loadConfig();
|
|
394
|
+
const client = new MiosaClient(config);
|
|
395
|
+
const json = isJsonMode(opts);
|
|
396
|
+
const spinner = json ? null : spin("Fetching template...");
|
|
397
|
+
const tmpl = unwrapTemplate(await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}`));
|
|
398
|
+
spinner?.stop();
|
|
643
399
|
if (json) {
|
|
644
|
-
printJson(
|
|
645
|
-
...template,
|
|
646
|
-
mechanism: replaceInPlace ? "spec_replaced" : "new_build",
|
|
647
|
-
...(build ? { build } : {}),
|
|
648
|
-
usable: usability(template, builds).usable,
|
|
649
|
-
usable_reason: usability(template, builds).reason,
|
|
650
|
-
...(builds ? { builds } : {}),
|
|
651
|
-
});
|
|
400
|
+
printJson(tmpl);
|
|
652
401
|
return;
|
|
653
402
|
}
|
|
654
|
-
// Which of the two mechanisms ran is the one thing a caller must not
|
|
655
|
-
// have to guess, so state it on stdout rather than only in the
|
|
656
|
-
// spinner line (which ora writes to stderr and which is lost the
|
|
657
|
-
// moment output is redirected).
|
|
658
403
|
console.log();
|
|
659
|
-
console.log(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
404
|
+
console.log(` ${chalk.bold("ID")} ${tmpl.id}`);
|
|
405
|
+
console.log(` ${chalk.bold("Name")} ${tmpl.name}`);
|
|
406
|
+
if (tmpl.slug)
|
|
407
|
+
console.log(` ${chalk.bold("Slug")} ${tmpl.slug}`);
|
|
408
|
+
console.log(` ${chalk.bold("State")} ${fmtTemplateState(templateState(tmpl))}`);
|
|
409
|
+
const image = templateImage(tmpl);
|
|
410
|
+
if (image)
|
|
411
|
+
console.log(` ${chalk.bold("Image")} ${image}`);
|
|
412
|
+
const createdAt = templateCreatedAt(tmpl);
|
|
413
|
+
if (createdAt)
|
|
414
|
+
console.log(` ${chalk.bold("Created")} ${createdAt}`);
|
|
415
|
+
if (tmpl.updated_at)
|
|
416
|
+
console.log(` ${chalk.bold("Updated")} ${tmpl.updated_at}`);
|
|
417
|
+
console.log();
|
|
418
|
+
printIntegrationSnippet(tmpl);
|
|
419
|
+
console.log(chalk.dim(` Run "miosa templates builds ${tmpl.id}" to view build history.`));
|
|
420
|
+
console.log();
|
|
663
421
|
}
|
|
664
422
|
catch (err) {
|
|
665
|
-
handleError(err
|
|
423
|
+
handleError(err);
|
|
666
424
|
}
|
|
667
425
|
});
|
|
668
|
-
//
|
|
426
|
+
// create
|
|
669
427
|
templates
|
|
670
|
-
.command("
|
|
671
|
-
.description("
|
|
672
|
-
.
|
|
428
|
+
.command("create")
|
|
429
|
+
.description("Create a sandbox template from a Dockerfile")
|
|
430
|
+
.requiredOption("--name <name>", "Template name")
|
|
431
|
+
.requiredOption("--dockerfile <path>", "Path to Dockerfile to build the template from")
|
|
673
432
|
.option("--json", "Output raw JSON")
|
|
674
|
-
.action(async (
|
|
675
|
-
const json = isJsonMode(opts);
|
|
433
|
+
.action(async (opts) => {
|
|
676
434
|
try {
|
|
677
|
-
|
|
678
|
-
const body = opts.dockerfile
|
|
679
|
-
? { dockerfile: readDockerfile(opts.dockerfile) }
|
|
680
|
-
: {};
|
|
681
|
-
const spinner = json ? null : spin(`Starting a build of ${id}...`);
|
|
682
|
-
let build;
|
|
435
|
+
let dockerfileContent;
|
|
683
436
|
try {
|
|
684
|
-
|
|
685
|
-
build = raw.data ?? raw;
|
|
437
|
+
dockerfileContent = readFileSync(opts.dockerfile, "utf8");
|
|
686
438
|
}
|
|
687
439
|
catch (err) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
440
|
+
console.error(chalk.red(`Cannot read Dockerfile at ${opts.dockerfile}: ${err instanceof Error ? err.message : String(err)}`));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const config = loadConfig();
|
|
444
|
+
const client = new MiosaClient(config);
|
|
445
|
+
const spinner = spin(`Creating template ${opts.name}...`);
|
|
446
|
+
const tmpl = unwrapTemplate(await client.apiPost("/api/v1/sandbox-templates", {
|
|
447
|
+
name: opts.name,
|
|
448
|
+
dockerfile: dockerfileContent,
|
|
449
|
+
}));
|
|
450
|
+
spinner.succeed(`Created template ${tmpl.name}`);
|
|
451
|
+
if (opts.json) {
|
|
452
|
+
console.log(JSON.stringify(tmpl, null, 2));
|
|
694
453
|
return;
|
|
695
454
|
}
|
|
696
455
|
console.log();
|
|
697
|
-
console.log(` ${chalk.bold("
|
|
698
|
-
console.log(` ${chalk.bold("
|
|
456
|
+
console.log(` ${chalk.bold("ID")} ${tmpl.id}`);
|
|
457
|
+
console.log(` ${chalk.bold("Name")} ${tmpl.name}`);
|
|
458
|
+
console.log(` ${chalk.bold("State")} ${fmtTemplateState(tmpl.state)}`);
|
|
699
459
|
console.log();
|
|
700
|
-
console.log(chalk.dim(`
|
|
460
|
+
console.log(chalk.dim(` Run "miosa templates builds ${tmpl.id}" to track the build.`));
|
|
701
461
|
console.log();
|
|
702
462
|
}
|
|
703
463
|
catch (err) {
|
|
704
|
-
handleError(err
|
|
464
|
+
handleError(err);
|
|
705
465
|
}
|
|
706
466
|
});
|
|
707
467
|
// builds
|
|
@@ -711,17 +471,11 @@ export function register(program) {
|
|
|
711
471
|
.option("--json", "Output raw JSON")
|
|
712
472
|
.action(async (id, opts) => {
|
|
713
473
|
try {
|
|
714
|
-
const
|
|
474
|
+
const config = loadConfig();
|
|
475
|
+
const client = new MiosaClient(config);
|
|
715
476
|
const json = isJsonMode(opts);
|
|
716
477
|
const spinner = json ? null : spin("Fetching builds...");
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
rows = await fetchBuilds(api, id);
|
|
720
|
-
}
|
|
721
|
-
catch (err) {
|
|
722
|
-
spinner?.fail(`Could not fetch builds for ${id}`);
|
|
723
|
-
throw err;
|
|
724
|
-
}
|
|
478
|
+
const rows = unwrapBuilds(await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}/builds`));
|
|
725
479
|
spinner?.stop();
|
|
726
480
|
if (json) {
|
|
727
481
|
printJson(rows);
|
|
@@ -729,20 +483,14 @@ export function register(program) {
|
|
|
729
483
|
}
|
|
730
484
|
if (rows.length === 0) {
|
|
731
485
|
console.log(chalk.dim("No builds found."));
|
|
732
|
-
console.log(chalk.dim(` Start one: miosa templates rebuild ${id}`));
|
|
733
486
|
return;
|
|
734
487
|
}
|
|
735
488
|
renderTable(rows, [
|
|
736
|
-
{ header: "BUILD ID", key: (b) => b.id, width:
|
|
489
|
+
{ header: "BUILD ID", key: (b) => b.id.slice(0, 12), width: 14 },
|
|
737
490
|
{
|
|
738
491
|
header: "STATE",
|
|
739
492
|
key: (b) => fmtBuildState(b.state),
|
|
740
|
-
width:
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
header: "IMAGE",
|
|
744
|
-
key: (b) => b.image_id ?? b.image_digest ?? chalk.dim("-"),
|
|
745
|
-
width: 30,
|
|
493
|
+
width: 12,
|
|
746
494
|
},
|
|
747
495
|
{
|
|
748
496
|
header: "STARTED",
|
|
@@ -760,22 +508,17 @@ export function register(program) {
|
|
|
760
508
|
},
|
|
761
509
|
{
|
|
762
510
|
header: "ERROR",
|
|
763
|
-
key: (b) =>
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
? `${message.slice(0, 37)}...`
|
|
770
|
-
: message)
|
|
771
|
-
: chalk.dim("-");
|
|
772
|
-
},
|
|
773
|
-
width: 42,
|
|
511
|
+
key: (b) => b.error
|
|
512
|
+
? chalk.red(b.error.length > 30
|
|
513
|
+
? `${b.error.slice(0, 27)}...`
|
|
514
|
+
: b.error)
|
|
515
|
+
: chalk.dim("-"),
|
|
516
|
+
width: 32,
|
|
774
517
|
},
|
|
775
518
|
]);
|
|
776
519
|
}
|
|
777
520
|
catch (err) {
|
|
778
|
-
handleError(err
|
|
521
|
+
handleError(err);
|
|
779
522
|
}
|
|
780
523
|
});
|
|
781
524
|
// delete
|
|
@@ -801,28 +544,16 @@ export function register(program) {
|
|
|
801
544
|
process.exit(0);
|
|
802
545
|
}
|
|
803
546
|
}
|
|
804
|
-
const
|
|
805
|
-
const
|
|
806
|
-
const spinner =
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
catch (err) {
|
|
812
|
-
spinner?.fail(`Could not delete template ${id}`);
|
|
813
|
-
// TEMPLATE_IN_USE is a refusal with a specific remedy, not a bug.
|
|
814
|
-
if (err instanceof ApiResponseError &&
|
|
815
|
-
err.code === "TEMPLATE_IN_USE") {
|
|
816
|
-
throw new UserError(err.message, "Stop the sandboxes booted from this template, or wait for its build to finish, then retry.");
|
|
817
|
-
}
|
|
818
|
-
throw err;
|
|
819
|
-
}
|
|
820
|
-
spinner?.succeed("Template deleted");
|
|
821
|
-
if (json)
|
|
822
|
-
printJson(result ?? { ok: true });
|
|
547
|
+
const config = loadConfig();
|
|
548
|
+
const client = new MiosaClient(config);
|
|
549
|
+
const spinner = spin("Deleting template...");
|
|
550
|
+
const result = await client.apiDelete(`/api/v1/sandbox-templates/${encodeURIComponent(id)}`);
|
|
551
|
+
spinner.succeed("Template deleted");
|
|
552
|
+
if (opts.json)
|
|
553
|
+
console.log(JSON.stringify(result ?? { ok: true }, null, 2));
|
|
823
554
|
}
|
|
824
555
|
catch (err) {
|
|
825
|
-
handleError(err
|
|
556
|
+
handleError(err);
|
|
826
557
|
}
|
|
827
558
|
});
|
|
828
559
|
}
|