@miosa/cli 1.1.18 → 1.1.19
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 +220 -526
- 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 +2 -2
- 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,191 +45,49 @@ 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
|
-
function client() {
|
|
162
|
-
return new MiosaClient(loadConfig());
|
|
163
|
-
}
|
|
164
|
-
function readDockerfile(path) {
|
|
165
|
-
try {
|
|
166
|
-
const contents = readFileSync(path, "utf8");
|
|
167
|
-
if (contents.trim() === "") {
|
|
168
|
-
throw new UserError(`The Dockerfile at ${path} is empty.`, "A template needs at least a FROM line.");
|
|
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;
|
|
189
|
-
console.log();
|
|
190
|
-
console.log(` ${chalk.bold("ID")} ${template.id}`);
|
|
191
|
-
console.log(` ${chalk.bold("Name")} ${template.name}`);
|
|
192
|
-
if (template.slug)
|
|
193
|
-
console.log(` ${chalk.bold("Slug")} ${template.slug}`);
|
|
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}`);
|
|
216
|
-
console.log();
|
|
217
|
-
if (status.usable) {
|
|
218
|
-
console.log(chalk.dim(` Boot it: miosa sandbox create --template ${reference}`));
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
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}`));
|
|
232
|
-
console.log();
|
|
233
|
-
}
|
|
234
91
|
export function register(program) {
|
|
235
92
|
const templates = program
|
|
236
93
|
.command("templates")
|
|
@@ -242,7 +99,9 @@ export function register(program) {
|
|
|
242
99
|
.option("--json", "Output the complete catalog as JSON")
|
|
243
100
|
.action(async (opts) => {
|
|
244
101
|
try {
|
|
245
|
-
const
|
|
102
|
+
const config = loadConfig();
|
|
103
|
+
const client = new MiosaClient(config);
|
|
104
|
+
const raw = await client.apiGet("/api/v1/templates");
|
|
246
105
|
const rows = productTemplates(raw);
|
|
247
106
|
const filtered = opts.product
|
|
248
107
|
? rows.filter((template) => template.product === opts.product)
|
|
@@ -268,7 +127,7 @@ export function register(program) {
|
|
|
268
127
|
]);
|
|
269
128
|
}
|
|
270
129
|
catch (err) {
|
|
271
|
-
handleError(err
|
|
130
|
+
handleError(err);
|
|
272
131
|
}
|
|
273
132
|
});
|
|
274
133
|
templates
|
|
@@ -278,12 +137,13 @@ export function register(program) {
|
|
|
278
137
|
.option("--json", "Output raw readiness rows")
|
|
279
138
|
.action(async (id, opts) => {
|
|
280
139
|
try {
|
|
281
|
-
const
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
const client = new MiosaClient(config);
|
|
142
|
+
const raw = await client.apiGet("/api/v1/templates");
|
|
282
143
|
const rows = productTemplates(raw);
|
|
283
|
-
const template = rows.find((candidate) => candidate.id === id &&
|
|
284
|
-
(!opts.product || candidate.product === opts.product));
|
|
144
|
+
const template = rows.find((candidate) => candidate.id === id && (!opts.product || candidate.product === opts.product));
|
|
285
145
|
if (!template)
|
|
286
|
-
throw new
|
|
146
|
+
throw new Error(`Product template not found: ${id}`);
|
|
287
147
|
const readiness = template.sizes ?? [];
|
|
288
148
|
if (isJsonMode(opts)) {
|
|
289
149
|
printJson(readiness);
|
|
@@ -315,109 +175,99 @@ export function register(program) {
|
|
|
315
175
|
]);
|
|
316
176
|
}
|
|
317
177
|
catch (err) {
|
|
318
|
-
handleError(err
|
|
178
|
+
handleError(err);
|
|
319
179
|
}
|
|
320
180
|
});
|
|
321
181
|
// list
|
|
322
182
|
templates
|
|
323
183
|
.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")
|
|
184
|
+
.description("List sandbox templates, distinguishing confirmed-absent from cannot-verify")
|
|
328
185
|
.option("--json", "Output raw JSON")
|
|
329
186
|
.action(async (opts) => {
|
|
187
|
+
const config = loadConfig();
|
|
188
|
+
const client = new MiosaClient(config);
|
|
189
|
+
const json = isJsonMode(opts);
|
|
190
|
+
const spinner = json ? null : spin("Fetching templates...");
|
|
191
|
+
const checkedAt = new Date().toISOString();
|
|
192
|
+
let rows;
|
|
330
193
|
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
|
-
}
|
|
194
|
+
rows = unwrapTemplates(await client.apiGet("/api/v1/sandbox-templates"));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
// The listing could NOT be verified. This is emphatically not "no
|
|
198
|
+
// templates" - reporting an empty list here would tell a customer their
|
|
199
|
+
// templates are gone when we simply failed to ask.
|
|
359
200
|
spinner?.stop();
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
201
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
202
|
+
if (json) {
|
|
203
|
+
printJson({
|
|
204
|
+
listing: {
|
|
205
|
+
state: "unverified",
|
|
206
|
+
source: "live",
|
|
207
|
+
checkedAt,
|
|
208
|
+
reason,
|
|
209
|
+
},
|
|
210
|
+
templates: null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log();
|
|
215
|
+
console.log(` ${chalk.yellow("Cannot verify")} - the template list could not be retrieved.`);
|
|
216
|
+
console.log(` ${chalk.dim(reason)}`);
|
|
217
|
+
console.log(` ${chalk.dim("This is NOT the same as having no templates. Run: miosa doctor")}`);
|
|
218
|
+
}
|
|
219
|
+
// Non-zero: a script must be able to tell an unverified listing from an
|
|
220
|
+
// empty one.
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
spinner?.stop();
|
|
225
|
+
try {
|
|
365
226
|
if (json) {
|
|
366
|
-
printJson(
|
|
227
|
+
printJson({
|
|
228
|
+
// Never presented as current without saying when it was checked,
|
|
229
|
+
// and never cached: this is a live read every time.
|
|
230
|
+
listing: {
|
|
231
|
+
state: "confirmed",
|
|
232
|
+
source: "live",
|
|
233
|
+
checkedAt,
|
|
234
|
+
count: rows.length,
|
|
235
|
+
},
|
|
236
|
+
templates: rows.map((t) => {
|
|
237
|
+
const { presence, why } = templatePresence(t);
|
|
238
|
+
return { ...t, presence, presenceReason: why };
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
367
241
|
return;
|
|
368
242
|
}
|
|
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
|
-
}
|
|
243
|
+
if (rows.length === 0) {
|
|
244
|
+
console.log(chalk.dim(`No templates. Confirmed empty by a live read at ${checkedAt}.`));
|
|
377
245
|
return;
|
|
378
246
|
}
|
|
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 },
|
|
247
|
+
renderTable(rows, [
|
|
248
|
+
{ header: "ID", key: (t) => t.id.slice(0, 12), width: 14 },
|
|
249
|
+
{ header: "NAME", key: "name", width: 28 },
|
|
400
250
|
{
|
|
401
251
|
header: "STATE",
|
|
402
252
|
key: (t) => fmtTemplateState(templateState(t)),
|
|
403
|
-
width:
|
|
253
|
+
width: 12,
|
|
404
254
|
},
|
|
405
255
|
{
|
|
406
|
-
header: "
|
|
407
|
-
key: (t) =>
|
|
408
|
-
width:
|
|
256
|
+
header: "IMAGE",
|
|
257
|
+
key: (t) => fmtPresence(templatePresence(t).presence),
|
|
258
|
+
width: 12,
|
|
409
259
|
},
|
|
410
260
|
{
|
|
411
|
-
header: "IMAGE",
|
|
261
|
+
header: "IMAGE ID",
|
|
412
262
|
key: (t) => {
|
|
413
263
|
const image = templateImage(t);
|
|
414
264
|
return image
|
|
415
|
-
? image.length >
|
|
416
|
-
? `${image.slice(0,
|
|
265
|
+
? image.length > 26
|
|
266
|
+
? `${image.slice(0, 23)}...`
|
|
417
267
|
: image
|
|
418
268
|
: chalk.dim("-");
|
|
419
269
|
},
|
|
420
|
-
width:
|
|
270
|
+
width: 28,
|
|
421
271
|
},
|
|
422
272
|
{
|
|
423
273
|
header: "CREATED",
|
|
@@ -428,280 +278,153 @@ export function register(program) {
|
|
|
428
278
|
width: 12,
|
|
429
279
|
},
|
|
430
280
|
]);
|
|
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
|
-
}
|
|
281
|
+
const unverified = rows.filter((t) => templatePresence(t).presence === "unknown");
|
|
282
|
+
if (unverified.length > 0) {
|
|
283
|
+
console.log();
|
|
284
|
+
console.log(chalk.dim(` ${unverified.length} template(s) report no image and could not be confirmed either way.`));
|
|
455
285
|
}
|
|
456
|
-
console.log();
|
|
457
286
|
}
|
|
458
287
|
catch (err) {
|
|
459
|
-
handleError(err
|
|
288
|
+
handleError(err);
|
|
460
289
|
}
|
|
461
290
|
});
|
|
462
|
-
//
|
|
291
|
+
// versions
|
|
463
292
|
templates
|
|
464
|
-
.command("
|
|
465
|
-
.description("
|
|
293
|
+
.command("versions <id>")
|
|
294
|
+
.description("List a template's versions (a version is a build; specs are immutable)")
|
|
466
295
|
.option("--json", "Output raw JSON")
|
|
467
296
|
.action(async (id, opts) => {
|
|
468
297
|
try {
|
|
469
|
-
const
|
|
298
|
+
const config = loadConfig();
|
|
299
|
+
const client = new MiosaClient(config);
|
|
470
300
|
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;
|
|
301
|
+
const spinner = json ? null : spin("Fetching versions...");
|
|
302
|
+
const raw = (await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}/versions`));
|
|
486
303
|
spinner?.stop();
|
|
304
|
+
const rows = Array.isArray(raw) ? raw : (raw.data ?? []);
|
|
487
305
|
if (json) {
|
|
488
|
-
printJson(
|
|
489
|
-
...template,
|
|
490
|
-
usable: usability(template, builds).usable,
|
|
491
|
-
usable_reason: usability(template, builds).reason,
|
|
492
|
-
...(builds ? { builds } : {}),
|
|
493
|
-
});
|
|
306
|
+
printJson(rows);
|
|
494
307
|
return;
|
|
495
308
|
}
|
|
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
|
-
});
|
|
309
|
+
if (rows.length === 0) {
|
|
310
|
+
console.log(chalk.dim("No versions yet."));
|
|
558
311
|
return;
|
|
559
312
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
313
|
+
renderTable(rows, [
|
|
314
|
+
{
|
|
315
|
+
header: "VERSION",
|
|
316
|
+
key: (v) => (v.current ? chalk.bold(`v${v.version} *`) : `v${v.version}`),
|
|
317
|
+
width: 10,
|
|
318
|
+
},
|
|
319
|
+
{ header: "REF", key: (v) => v.ref ?? chalk.dim("-"), width: 26 },
|
|
320
|
+
{
|
|
321
|
+
header: "STATE",
|
|
322
|
+
key: (v) => fmtBuildState(v.state),
|
|
323
|
+
width: 12,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
// Whether this version can actually be run or migrated to. A failed
|
|
327
|
+
// build is listed - you need to see it to understand why the next
|
|
328
|
+
// version is unavailable - but it is not usable.
|
|
329
|
+
header: "USABLE",
|
|
330
|
+
key: (v) => (v.usable ? chalk.green("yes") : chalk.dim("no")),
|
|
331
|
+
width: 8,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
header: "IMAGE",
|
|
335
|
+
key: (v) => v.image_id ?? chalk.dim("-"),
|
|
336
|
+
width: 26,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
header: "CREATED",
|
|
340
|
+
key: (v) => (v.created_at ? String(v.created_at).slice(0, 10) : chalk.dim("-")),
|
|
341
|
+
width: 12,
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
console.log();
|
|
345
|
+
console.log(chalk.dim(" * = current. Specs are immutable: editing a template mints a new version,"));
|
|
346
|
+
console.log(chalk.dim(" and existing sandboxes keep running the version they were created from."));
|
|
566
347
|
}
|
|
567
348
|
catch (err) {
|
|
568
|
-
handleError(err
|
|
349
|
+
handleError(err);
|
|
569
350
|
}
|
|
570
351
|
});
|
|
571
|
-
//
|
|
352
|
+
// get
|
|
572
353
|
templates
|
|
573
|
-
.command("
|
|
574
|
-
.description("
|
|
575
|
-
.requiredOption("--dockerfile <path>", "Path to the new Dockerfile")
|
|
576
|
-
.option("--description <text>", "Replace the description")
|
|
354
|
+
.command("get <id>")
|
|
355
|
+
.description("Get sandbox template details")
|
|
577
356
|
.option("--json", "Output raw JSON")
|
|
578
357
|
.action(async (id, opts) => {
|
|
579
|
-
const json = isJsonMode(opts);
|
|
580
358
|
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);
|
|
359
|
+
const config = loadConfig();
|
|
360
|
+
const client = new MiosaClient(config);
|
|
361
|
+
const json = isJsonMode(opts);
|
|
362
|
+
const spinner = json ? null : spin("Fetching template...");
|
|
363
|
+
const tmpl = unwrapTemplate(await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}`));
|
|
364
|
+
spinner?.stop();
|
|
643
365
|
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
|
-
});
|
|
366
|
+
printJson(tmpl);
|
|
652
367
|
return;
|
|
653
368
|
}
|
|
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
369
|
console.log();
|
|
659
|
-
console.log(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
370
|
+
console.log(` ${chalk.bold("ID")} ${tmpl.id}`);
|
|
371
|
+
console.log(` ${chalk.bold("Name")} ${tmpl.name}`);
|
|
372
|
+
console.log(` ${chalk.bold("State")} ${fmtTemplateState(templateState(tmpl))}`);
|
|
373
|
+
const image = templateImage(tmpl);
|
|
374
|
+
if (image)
|
|
375
|
+
console.log(` ${chalk.bold("Image")} ${image}`);
|
|
376
|
+
const createdAt = templateCreatedAt(tmpl);
|
|
377
|
+
if (createdAt)
|
|
378
|
+
console.log(` ${chalk.bold("Created")} ${createdAt}`);
|
|
379
|
+
if (tmpl.updated_at)
|
|
380
|
+
console.log(` ${chalk.bold("Updated")} ${tmpl.updated_at}`);
|
|
381
|
+
console.log();
|
|
382
|
+
console.log(chalk.dim(` Run "miosa templates builds ${tmpl.id}" to view build history.`));
|
|
383
|
+
console.log();
|
|
663
384
|
}
|
|
664
385
|
catch (err) {
|
|
665
|
-
handleError(err
|
|
386
|
+
handleError(err);
|
|
666
387
|
}
|
|
667
388
|
});
|
|
668
|
-
//
|
|
389
|
+
// create
|
|
669
390
|
templates
|
|
670
|
-
.command("
|
|
671
|
-
.description("
|
|
672
|
-
.
|
|
391
|
+
.command("create")
|
|
392
|
+
.description("Create a sandbox template from a Dockerfile")
|
|
393
|
+
.requiredOption("--name <name>", "Template name")
|
|
394
|
+
.requiredOption("--dockerfile <path>", "Path to Dockerfile to build the template from")
|
|
673
395
|
.option("--json", "Output raw JSON")
|
|
674
|
-
.action(async (
|
|
675
|
-
const json = isJsonMode(opts);
|
|
396
|
+
.action(async (opts) => {
|
|
676
397
|
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;
|
|
398
|
+
let dockerfileContent;
|
|
683
399
|
try {
|
|
684
|
-
|
|
685
|
-
build = raw.data ?? raw;
|
|
400
|
+
dockerfileContent = readFileSync(opts.dockerfile, "utf8");
|
|
686
401
|
}
|
|
687
402
|
catch (err) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
403
|
+
console.error(chalk.red(`Cannot read Dockerfile at ${opts.dockerfile}: ${err instanceof Error ? err.message : String(err)}`));
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
const config = loadConfig();
|
|
407
|
+
const client = new MiosaClient(config);
|
|
408
|
+
const spinner = spin(`Creating template ${opts.name}...`);
|
|
409
|
+
const tmpl = unwrapTemplate(await client.apiPost("/api/v1/sandbox-templates", {
|
|
410
|
+
name: opts.name,
|
|
411
|
+
dockerfile: dockerfileContent,
|
|
412
|
+
}));
|
|
413
|
+
spinner.succeed(`Created template ${tmpl.name}`);
|
|
414
|
+
if (opts.json) {
|
|
415
|
+
console.log(JSON.stringify(tmpl, null, 2));
|
|
694
416
|
return;
|
|
695
417
|
}
|
|
696
418
|
console.log();
|
|
697
|
-
console.log(` ${chalk.bold("
|
|
698
|
-
console.log(` ${chalk.bold("
|
|
419
|
+
console.log(` ${chalk.bold("ID")} ${tmpl.id}`);
|
|
420
|
+
console.log(` ${chalk.bold("Name")} ${tmpl.name}`);
|
|
421
|
+
console.log(` ${chalk.bold("State")} ${fmtTemplateState(tmpl.state)}`);
|
|
699
422
|
console.log();
|
|
700
|
-
console.log(chalk.dim(`
|
|
423
|
+
console.log(chalk.dim(` Run "miosa templates builds ${tmpl.id}" to track the build.`));
|
|
701
424
|
console.log();
|
|
702
425
|
}
|
|
703
426
|
catch (err) {
|
|
704
|
-
handleError(err
|
|
427
|
+
handleError(err);
|
|
705
428
|
}
|
|
706
429
|
});
|
|
707
430
|
// builds
|
|
@@ -711,17 +434,11 @@ export function register(program) {
|
|
|
711
434
|
.option("--json", "Output raw JSON")
|
|
712
435
|
.action(async (id, opts) => {
|
|
713
436
|
try {
|
|
714
|
-
const
|
|
437
|
+
const config = loadConfig();
|
|
438
|
+
const client = new MiosaClient(config);
|
|
715
439
|
const json = isJsonMode(opts);
|
|
716
440
|
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
|
-
}
|
|
441
|
+
const rows = unwrapBuilds(await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(id)}/builds`));
|
|
725
442
|
spinner?.stop();
|
|
726
443
|
if (json) {
|
|
727
444
|
printJson(rows);
|
|
@@ -729,20 +446,14 @@ export function register(program) {
|
|
|
729
446
|
}
|
|
730
447
|
if (rows.length === 0) {
|
|
731
448
|
console.log(chalk.dim("No builds found."));
|
|
732
|
-
console.log(chalk.dim(` Start one: miosa templates rebuild ${id}`));
|
|
733
449
|
return;
|
|
734
450
|
}
|
|
735
451
|
renderTable(rows, [
|
|
736
|
-
{ header: "BUILD ID", key: (b) => b.id, width:
|
|
452
|
+
{ header: "BUILD ID", key: (b) => b.id.slice(0, 12), width: 14 },
|
|
737
453
|
{
|
|
738
454
|
header: "STATE",
|
|
739
455
|
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,
|
|
456
|
+
width: 12,
|
|
746
457
|
},
|
|
747
458
|
{
|
|
748
459
|
header: "STARTED",
|
|
@@ -760,22 +471,17 @@ export function register(program) {
|
|
|
760
471
|
},
|
|
761
472
|
{
|
|
762
473
|
header: "ERROR",
|
|
763
|
-
key: (b) =>
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
? `${message.slice(0, 37)}...`
|
|
770
|
-
: message)
|
|
771
|
-
: chalk.dim("-");
|
|
772
|
-
},
|
|
773
|
-
width: 42,
|
|
474
|
+
key: (b) => b.error
|
|
475
|
+
? chalk.red(b.error.length > 30
|
|
476
|
+
? `${b.error.slice(0, 27)}...`
|
|
477
|
+
: b.error)
|
|
478
|
+
: chalk.dim("-"),
|
|
479
|
+
width: 32,
|
|
774
480
|
},
|
|
775
481
|
]);
|
|
776
482
|
}
|
|
777
483
|
catch (err) {
|
|
778
|
-
handleError(err
|
|
484
|
+
handleError(err);
|
|
779
485
|
}
|
|
780
486
|
});
|
|
781
487
|
// delete
|
|
@@ -801,28 +507,16 @@ export function register(program) {
|
|
|
801
507
|
process.exit(0);
|
|
802
508
|
}
|
|
803
509
|
}
|
|
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 });
|
|
510
|
+
const config = loadConfig();
|
|
511
|
+
const client = new MiosaClient(config);
|
|
512
|
+
const spinner = spin("Deleting template...");
|
|
513
|
+
const result = await client.apiDelete(`/api/v1/sandbox-templates/${encodeURIComponent(id)}`);
|
|
514
|
+
spinner.succeed("Template deleted");
|
|
515
|
+
if (opts.json)
|
|
516
|
+
console.log(JSON.stringify(result ?? { ok: true }, null, 2));
|
|
823
517
|
}
|
|
824
518
|
catch (err) {
|
|
825
|
-
handleError(err
|
|
519
|
+
handleError(err);
|
|
826
520
|
}
|
|
827
521
|
});
|
|
828
522
|
}
|