Skip to content

Commit 2f6415f

Browse files
committed
feat: add docker deploy cli commands
1 parent aaf8022 commit 2f6415f

3 files changed

Lines changed: 464 additions & 0 deletions

File tree

src/bin/miosa.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ const commandModules = [
177177
"../commands/up.js",
178178
"../commands/update.js",
179179
"../commands/deploy.js",
180+
"../commands/docker-deploy.js",
180181
"../commands/apps.js",
181182
"../commands/logs.js",
182183
"../commands/releases.js",

src/commands/docker-deploy.ts

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import type { Command } from "commander";
2+
import chalk from "chalk";
3+
import { renderTable } from "../ui/table.js";
4+
import {
5+
createClient,
6+
handleError,
7+
isJsonMode,
8+
objectOf,
9+
printJson,
10+
shortId,
11+
} from "./util.js";
12+
13+
type HostStatus =
14+
| "pending"
15+
| "provisioning"
16+
| "bootstrapping"
17+
| "active"
18+
| "degraded"
19+
| "suspended"
20+
| "retired"
21+
| "error";
22+
23+
interface DockerDeployHost {
24+
[key: string]: unknown;
25+
id: string;
26+
tenant_id: string;
27+
workspace_id: string;
28+
external_workspace_id?: string | null;
29+
computer_id?: string | null;
30+
status: HostStatus | string;
31+
size: string;
32+
region: string;
33+
portal_domain?: string | null;
34+
runtime_base_url?: string | null;
35+
agent_base_url?: string | null;
36+
appliance_image?: string | null;
37+
appliance_version?: string | null;
38+
appliance_status?: string | null;
39+
agent_last_seen_at?: string | null;
40+
updated_at?: string | null;
41+
}
42+
43+
interface DockerDeployTemplate {
44+
[key: string]: unknown;
45+
id: string;
46+
name: string;
47+
description?: string | null;
48+
category?: string | null;
49+
runtime?: string | null;
50+
tags?: string[] | null;
51+
}
52+
53+
function unwrapHosts(payload: unknown): DockerDeployHost[] {
54+
if (Array.isArray(payload)) return payload as DockerDeployHost[];
55+
if (payload && typeof payload === "object") {
56+
const record = payload as Record<string, unknown>;
57+
if (Array.isArray(record["data"])) return record["data"] as DockerDeployHost[];
58+
if (Array.isArray(record["hosts"])) return record["hosts"] as DockerDeployHost[];
59+
}
60+
return [];
61+
}
62+
63+
function unwrapTemplates(payload: unknown): DockerDeployTemplate[] {
64+
if (Array.isArray(payload)) return payload as DockerDeployTemplate[];
65+
if (payload && typeof payload === "object") {
66+
const record = payload as Record<string, unknown>;
67+
if (Array.isArray(record["data"])) return record["data"] as DockerDeployTemplate[];
68+
if (Array.isArray(record["templates"])) return record["templates"] as DockerDeployTemplate[];
69+
}
70+
return [];
71+
}
72+
73+
function statusLabel(status: string): string {
74+
switch (status) {
75+
case "active":
76+
return chalk.green(status);
77+
case "bootstrapping":
78+
case "provisioning":
79+
case "pending":
80+
return chalk.yellow(status);
81+
case "degraded":
82+
case "error":
83+
return chalk.red(status);
84+
default:
85+
return chalk.dim(status);
86+
}
87+
}
88+
89+
function hostReady(host: DockerDeployHost): boolean {
90+
return host.status === "active" && host.appliance_status === "healthy";
91+
}
92+
93+
function printHost(host: DockerDeployHost): void {
94+
console.log();
95+
console.log(chalk.bold("Docker Deploy host"));
96+
console.log();
97+
console.log(` ID: ${host.id}`);
98+
console.log(` Workspace: ${host.workspace_id}`);
99+
if (host.external_workspace_id) {
100+
console.log(` External: ${host.external_workspace_id}`);
101+
}
102+
console.log(` Computer: ${host.computer_id ?? "—"}`);
103+
console.log(` Status: ${statusLabel(host.status)}`);
104+
console.log(` Appliance: ${host.appliance_status ?? "unknown"}`);
105+
console.log(` Ready: ${hostReady(host) ? chalk.green("yes") : chalk.yellow("no")}`);
106+
console.log(` Size/region: ${host.size} / ${host.region}`);
107+
console.log(` Portal: ${host.portal_domain ?? "—"}`);
108+
console.log(` Runtime: ${host.runtime_base_url ?? "—"}`);
109+
console.log(` Updated: ${host.updated_at ?? "—"}`);
110+
console.log();
111+
}
112+
113+
function printTemplate(template: DockerDeployTemplate): void {
114+
console.log();
115+
console.log(chalk.bold(template.name));
116+
console.log();
117+
console.log(` ID: ${template.id}`);
118+
console.log(` Category: ${template.category ?? "—"}`);
119+
console.log(` Runtime: ${template.runtime ?? "—"}`);
120+
console.log(` Tags: ${(template.tags ?? []).join(", ") || "—"}`);
121+
if (template.description) {
122+
console.log(` Description: ${template.description}`);
123+
}
124+
console.log();
125+
}
126+
127+
export function register(program: Command): void {
128+
const root = program
129+
.command("docker-deploy")
130+
.alias("docker")
131+
.description("Manage MIOSA Docker Deploy appliance hosts");
132+
133+
root
134+
.command("hosts")
135+
.alias("list")
136+
.description("List Docker Deploy appliance hosts")
137+
.option("--workspace <id>", "Filter by workspace ID")
138+
.option("--json", "Output raw JSON")
139+
.action(async (opts: { workspace?: string; json?: boolean }) => {
140+
try {
141+
const client = createClient();
142+
const params = new URLSearchParams();
143+
if (opts.workspace) params.set("workspace_id", opts.workspace);
144+
const suffix = params.toString() ? `?${params.toString()}` : "";
145+
const raw = await client.apiGet<unknown>(`/api/v1/docker-deploy/hosts${suffix}`);
146+
const hosts = unwrapHosts(raw);
147+
148+
if (isJsonMode(opts) || opts.json) {
149+
printJson(hosts);
150+
return;
151+
}
152+
153+
console.log();
154+
console.log(`${chalk.bold(String(hosts.length))} Docker Deploy host(s)`);
155+
console.log();
156+
renderTable(hosts, [
157+
{ header: "ID", key: (h) => shortId(h.id), width: 10 },
158+
{ header: "WORKSPACE", key: (h) => shortId(h.workspace_id), width: 12 },
159+
{ header: "STATUS", key: (h) => h.status, width: 14, color: (v) => statusLabel(v.trim()).padEnd(14) },
160+
{ header: "APPLIANCE", key: (h) => h.appliance_status ?? "unknown", width: 14 },
161+
{ header: "COMPUTER", key: (h) => shortId(h.computer_id), width: 12 },
162+
{ header: "PORTAL", key: (h) => h.portal_domain ?? "—", width: 28 },
163+
]);
164+
console.log();
165+
} catch (err) {
166+
handleError(err);
167+
}
168+
});
169+
170+
root
171+
.command("templates")
172+
.description("List Docker Deploy starter templates")
173+
.option("--json", "Output raw JSON")
174+
.action(async (opts: { json?: boolean }) => {
175+
try {
176+
const client = createClient();
177+
const raw = await client.apiGet<unknown>("/api/v1/docker-deploy/templates");
178+
const templates = unwrapTemplates(raw);
179+
180+
if (isJsonMode(opts) || opts.json) {
181+
printJson(templates);
182+
return;
183+
}
184+
185+
console.log();
186+
console.log(`${chalk.bold(String(templates.length))} Docker Deploy template(s)`);
187+
console.log();
188+
renderTable(templates, [
189+
{ header: "ID", key: (t) => t.id, width: 24 },
190+
{ header: "NAME", key: (t) => t.name, width: 28 },
191+
{ header: "CATEGORY", key: (t) => t.category ?? "—", width: 16 },
192+
{ header: "RUNTIME", key: (t) => t.runtime ?? "—", width: 16 },
193+
]);
194+
console.log();
195+
} catch (err) {
196+
handleError(err);
197+
}
198+
});
199+
200+
root
201+
.command("template")
202+
.description("Show one Docker Deploy starter template")
203+
.argument("<template-id>", "Docker Deploy template ID")
204+
.option("--json", "Output raw JSON")
205+
.action(async (templateId: string, opts: { json?: boolean }) => {
206+
try {
207+
const client = createClient();
208+
const raw = await client.apiGet<unknown>(
209+
`/api/v1/docker-deploy/templates/${encodeURIComponent(templateId)}`,
210+
);
211+
const template = objectOf<DockerDeployTemplate>(raw, ["template"]);
212+
213+
if (isJsonMode(opts) || opts.json) {
214+
printJson(template);
215+
return;
216+
}
217+
218+
printTemplate(template);
219+
} catch (err) {
220+
handleError(err);
221+
}
222+
});
223+
224+
root
225+
.command("ensure")
226+
.description("Provision or return the workspace Docker Deploy host")
227+
.option("--workspace <id>", "Workspace ID")
228+
.option("--external-workspace <id>", "External workspace/customer ID")
229+
.option("--json", "Output raw JSON")
230+
.action(async (opts: { workspace?: string; externalWorkspace?: string; json?: boolean }) => {
231+
try {
232+
const client = createClient();
233+
const raw = await client.apiPost<unknown>("/api/v1/docker-deploy/hosts/ensure", {
234+
...(opts.workspace ? { workspace_id: opts.workspace } : {}),
235+
...(opts.externalWorkspace ? { external_workspace_id: opts.externalWorkspace } : {}),
236+
});
237+
const host = objectOf<DockerDeployHost>(raw, ["host"]);
238+
239+
if (isJsonMode(opts) || opts.json) {
240+
printJson(host);
241+
return;
242+
}
243+
244+
printHost(host);
245+
if (!hostReady(host)) {
246+
console.log(
247+
chalk.dim(
248+
" The appliance is not ready yet. Re-run `miosa docker-deploy show " +
249+
host.id +
250+
"` until status=active and appliance=healthy.",
251+
),
252+
);
253+
console.log();
254+
}
255+
} catch (err) {
256+
handleError(err);
257+
}
258+
});
259+
260+
root
261+
.command("show")
262+
.description("Show one Docker Deploy appliance host")
263+
.argument("<host-id>", "Docker Deploy host ID")
264+
.option("--json", "Output raw JSON")
265+
.action(async (hostId: string, opts: { json?: boolean }) => {
266+
try {
267+
const client = createClient();
268+
const raw = await client.apiGet<unknown>(
269+
`/api/v1/docker-deploy/hosts/${encodeURIComponent(hostId)}`,
270+
);
271+
const host = objectOf<DockerDeployHost>(raw, ["host"]);
272+
273+
if (isJsonMode(opts) || opts.json) {
274+
printJson(host);
275+
return;
276+
}
277+
278+
printHost(host);
279+
} catch (err) {
280+
handleError(err);
281+
}
282+
});
283+
}

0 commit comments

Comments
 (0)