@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.
@@ -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
@@ -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.74";
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