@lizard-build/cli 0.3.74 → 0.3.76
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/dist/commands/sandbox.d.ts +2 -0
- package/dist/commands/sandbox.js +409 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/volume.d.ts +2 -0
- package/dist/commands/volume.js +103 -0
- package/dist/commands/volume.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +4 -1
- package/dist/lib/api.js +25 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/src/commands/sandbox.ts +461 -0
- package/src/commands/volume.ts +143 -0
- package/src/index.ts +4 -0
- package/src/lib/api.ts +25 -1
- package/src/lib/updater.ts +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { api, withScope, type ResourceScope } from "../lib/api.js";
|
|
5
|
+
import { resolveProjectScope } from "../lib/resolve.js";
|
|
6
|
+
import { success, info, isJSONMode, printJSON, table, isTTY } from "../lib/format.js";
|
|
7
|
+
|
|
8
|
+
interface VolumeRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
sizeGb: number;
|
|
12
|
+
status: string;
|
|
13
|
+
attachedTo?: string | null;
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Resolve a volume by name or ID within a project. Mirrors resolveService. */
|
|
18
|
+
async function resolveVolume(
|
|
19
|
+
projectId: string,
|
|
20
|
+
scope: ResourceScope,
|
|
21
|
+
nameOrId: string,
|
|
22
|
+
): Promise<VolumeRecord> {
|
|
23
|
+
const volumes = await api.get<VolumeRecord[]>(
|
|
24
|
+
withScope(`/api/projects/${projectId}/volumes`, scope),
|
|
25
|
+
);
|
|
26
|
+
const lower = nameOrId.toLowerCase();
|
|
27
|
+
const match = volumes.find(
|
|
28
|
+
(v) => v.id.toLowerCase() === lower || v.name.toLowerCase() === lower,
|
|
29
|
+
);
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Volume "${nameOrId}" not found. Available: ${volumes.map((v) => v.name).join(", ") || "(none)"}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return match;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseIntOption(v: string): number {
|
|
39
|
+
const n = parseInt(v, 10);
|
|
40
|
+
if (Number.isNaN(n)) throw new Error(`Invalid number: ${v}`);
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function registerVolume(program: Command) {
|
|
45
|
+
const vol = program
|
|
46
|
+
.command("volume")
|
|
47
|
+
.alias("vol")
|
|
48
|
+
.description("Manage persistent volumes for sandboxes");
|
|
49
|
+
|
|
50
|
+
vol
|
|
51
|
+
.command("list")
|
|
52
|
+
.alias("ls")
|
|
53
|
+
.description("List volumes in a project")
|
|
54
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
57
|
+
const volumes = await api.get<VolumeRecord[]>(
|
|
58
|
+
withScope(`/api/projects/${projectId}/volumes`, scope),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (isJSONMode()) {
|
|
62
|
+
printJSON(volumes);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (volumes.length === 0) {
|
|
67
|
+
console.log("No volumes. Use `lizard volume create <name>`.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
table(
|
|
72
|
+
["Name", "ID", "Size", "Status", "Attached to"],
|
|
73
|
+
volumes.map((v) => [
|
|
74
|
+
v.name,
|
|
75
|
+
chalk.dim(v.id),
|
|
76
|
+
`${v.sizeGb} GB`,
|
|
77
|
+
v.status,
|
|
78
|
+
v.attachedTo ? chalk.dim(v.attachedTo) : chalk.dim("—"),
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
vol
|
|
84
|
+
.command("create")
|
|
85
|
+
.argument("<name>", "Volume name")
|
|
86
|
+
.description("Create a persistent volume")
|
|
87
|
+
.option("--size <gb>", "Size in GB (1-100, default 5)", parseIntOption)
|
|
88
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
89
|
+
.action(async (name: string, opts) => {
|
|
90
|
+
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
91
|
+
const sizeGb = opts.size ?? 5;
|
|
92
|
+
if (sizeGb < 1 || sizeGb > 100) {
|
|
93
|
+
throw new Error(`--size must be between 1 and 100 (got ${sizeGb}).`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isJSONMode()) info(`Creating volume ${chalk.cyan(name)}...`);
|
|
97
|
+
const created = await api.post<VolumeRecord>(
|
|
98
|
+
withScope(`/api/projects/${projectId}/volumes`, scope),
|
|
99
|
+
{ name, sizeGb },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (isJSONMode()) {
|
|
103
|
+
printJSON(created);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
success(`Volume ${chalk.bold(created.name)} created (${created.sizeGb} GB)`);
|
|
107
|
+
info(chalk.dim(` ID: ${created.id}`));
|
|
108
|
+
info(chalk.dim(` Attach it to a sandbox: lizard sandbox create --volume ${created.id}`));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
vol
|
|
112
|
+
.command("rm")
|
|
113
|
+
.alias("delete")
|
|
114
|
+
.argument("<volume>", "Volume name or ID")
|
|
115
|
+
.description("Delete a volume")
|
|
116
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
117
|
+
.option("-y, --yes", "Skip confirmation")
|
|
118
|
+
.action(async (nameOrId: string, opts) => {
|
|
119
|
+
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
120
|
+
const volume = await resolveVolume(projectId, scope, nameOrId);
|
|
121
|
+
|
|
122
|
+
if (volume.attachedTo) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Volume "${volume.name}" is attached to sandbox ${volume.attachedTo}. Delete the sandbox first.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!opts.yes && isTTY() && !isJSONMode()) {
|
|
129
|
+
const ok = await p.confirm({
|
|
130
|
+
message: `Delete volume ${chalk.bold(volume.name)}? This cannot be undone.`,
|
|
131
|
+
});
|
|
132
|
+
if (p.isCancel(ok) || !ok) process.exit(5);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await api.delete(withScope(`/api/projects/${projectId}/volumes/${volume.id}`, scope));
|
|
136
|
+
|
|
137
|
+
if (isJSONMode()) {
|
|
138
|
+
printJSON({ id: volume.id, status: "deleted" });
|
|
139
|
+
} else {
|
|
140
|
+
success(`Volume ${chalk.bold(volume.name)} deleted`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -79,6 +79,7 @@ import { registerRedeploy } from "./commands/redeploy.js";
|
|
|
79
79
|
import { registerRegions } from "./commands/regions.js";
|
|
80
80
|
import { registerRestart } from "./commands/restart.js";
|
|
81
81
|
import { registerRun } from "./commands/run.js";
|
|
82
|
+
import { registerSandbox } from "./commands/sandbox.js";
|
|
82
83
|
import { registerScale } from "./commands/scale.js";
|
|
83
84
|
import { registerSecrets } from "./commands/secrets.js";
|
|
84
85
|
import { registerService } from "./commands/service.js";
|
|
@@ -88,6 +89,7 @@ import { registerStatus } from "./commands/status.js";
|
|
|
88
89
|
import { registerUnlink } from "./commands/unlink.js";
|
|
89
90
|
import { registerUp } from "./commands/up.js";
|
|
90
91
|
import { registerUpgrade } from "./commands/upgrade.js";
|
|
92
|
+
import { registerVolume } from "./commands/volume.js";
|
|
91
93
|
import { registerWhoami } from "./commands/whoami.js";
|
|
92
94
|
import { registerWorkspace } from "./commands/workspace.js";
|
|
93
95
|
|
|
@@ -169,6 +171,7 @@ registerRedeploy(program);
|
|
|
169
171
|
registerRegions(program);
|
|
170
172
|
registerRestart(program);
|
|
171
173
|
registerRun(program);
|
|
174
|
+
registerSandbox(program);
|
|
172
175
|
registerScale(program);
|
|
173
176
|
registerSecrets(program);
|
|
174
177
|
registerService(program);
|
|
@@ -178,6 +181,7 @@ registerStatus(program);
|
|
|
178
181
|
registerUnlink(program);
|
|
179
182
|
registerUp(program);
|
|
180
183
|
registerUpgrade(program);
|
|
184
|
+
registerVolume(program);
|
|
181
185
|
registerWhoami(program);
|
|
182
186
|
registerWorkspace(program);
|
|
183
187
|
|
package/src/lib/api.ts
CHANGED
|
@@ -131,6 +131,30 @@ async function request<T = any>(
|
|
|
131
131
|
return JSON.parse(text) as T;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** Like api.get, but returns the raw response body instead of JSON.parse-ing
|
|
135
|
+
* it — for endpoints that reply with `text/plain` (e.g. sandbox file reads). */
|
|
136
|
+
export async function getRawText(path: string): Promise<string> {
|
|
137
|
+
const url = baseURL + path;
|
|
138
|
+
const token = _accessToken || getToken();
|
|
139
|
+
const headers: Record<string, string> = { "User-Agent": USER_AGENT };
|
|
140
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
141
|
+
|
|
142
|
+
const res = await fetch(url, { method: "GET", headers });
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
let msg = res.statusText;
|
|
145
|
+
let code = "";
|
|
146
|
+
let body: unknown = null;
|
|
147
|
+
try {
|
|
148
|
+
const j = (await res.json()) as any;
|
|
149
|
+
body = j;
|
|
150
|
+
msg = j.error || j.message || msg;
|
|
151
|
+
code = j.code || "";
|
|
152
|
+
} catch {}
|
|
153
|
+
throw new APIError(res.status, msg, code, body);
|
|
154
|
+
}
|
|
155
|
+
return res.text();
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
export const api = {
|
|
135
159
|
get: <T = any>(path: string) => request<T>("GET", path),
|
|
136
160
|
post: <T = any>(path: string, body?: unknown, headers?: Record<string, string>) =>
|
|
@@ -139,7 +163,7 @@ export const api = {
|
|
|
139
163
|
request<T>("PUT", path, body),
|
|
140
164
|
patch: <T = any>(path: string, body?: unknown) =>
|
|
141
165
|
request<T>("PATCH", path, body),
|
|
142
|
-
delete: <T = any>(path: string) => request<T>("DELETE", path),
|
|
166
|
+
delete: <T = any>(path: string, body?: unknown) => request<T>("DELETE", path, body),
|
|
143
167
|
};
|
|
144
168
|
|
|
145
169
|
/** Compare two Redis-stream-style event ids (`<ms>-<seq>`). Returns true when
|
package/src/lib/updater.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
export const CURRENT_VERSION = "0.3.
|
|
8
|
+
export const CURRENT_VERSION = "0.3.76";
|
|
9
9
|
const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
|
|
10
10
|
const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
|
|
11
11
|
|