@lizard-build/cli 0.1.0 → 0.3.29

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.
Files changed (178) hide show
  1. package/.github/workflows/release.yml +90 -0
  2. package/README.md +41 -0
  3. package/dist/commands/add.js +318 -45
  4. package/dist/commands/add.js.map +1 -1
  5. package/dist/commands/config.d.ts +2 -0
  6. package/dist/commands/config.js +68 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/docs.d.ts +2 -0
  9. package/dist/commands/docs.js +13 -0
  10. package/dist/commands/docs.js.map +1 -0
  11. package/dist/commands/domain.d.ts +9 -0
  12. package/dist/commands/domain.js +195 -0
  13. package/dist/commands/domain.js.map +1 -0
  14. package/dist/commands/git.js +175 -36
  15. package/dist/commands/git.js.map +1 -1
  16. package/dist/commands/init.d.ts +24 -0
  17. package/dist/commands/init.js +128 -86
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/link.d.ts +7 -0
  20. package/dist/commands/link.js +104 -33
  21. package/dist/commands/link.js.map +1 -1
  22. package/dist/commands/login.js +4 -3
  23. package/dist/commands/login.js.map +1 -1
  24. package/dist/commands/logs.js +223 -30
  25. package/dist/commands/logs.js.map +1 -1
  26. package/dist/commands/open.js +3 -2
  27. package/dist/commands/open.js.map +1 -1
  28. package/dist/commands/port.d.ts +7 -0
  29. package/dist/commands/port.js +49 -0
  30. package/dist/commands/port.js.map +1 -0
  31. package/dist/commands/projects.js +36 -6
  32. package/dist/commands/projects.js.map +1 -1
  33. package/dist/commands/ps.js +32 -39
  34. package/dist/commands/ps.js.map +1 -1
  35. package/dist/commands/redeploy.js +48 -8
  36. package/dist/commands/redeploy.js.map +1 -1
  37. package/dist/commands/regions.js +2 -5
  38. package/dist/commands/regions.js.map +1 -1
  39. package/dist/commands/restart.js +84 -10
  40. package/dist/commands/restart.js.map +1 -1
  41. package/dist/commands/run.d.ts +9 -0
  42. package/dist/commands/run.js +61 -22
  43. package/dist/commands/run.js.map +1 -1
  44. package/dist/commands/scale.d.ts +10 -0
  45. package/dist/commands/scale.js +166 -0
  46. package/dist/commands/scale.js.map +1 -0
  47. package/dist/commands/secrets.js +200 -89
  48. package/dist/commands/secrets.js.map +1 -1
  49. package/dist/commands/service-set.d.ts +49 -0
  50. package/dist/commands/service-set.js +552 -0
  51. package/dist/commands/service-set.js.map +1 -0
  52. package/dist/commands/service-show.d.ts +11 -0
  53. package/dist/commands/service-show.js +44 -0
  54. package/dist/commands/service-show.js.map +1 -0
  55. package/dist/commands/service.d.ts +8 -0
  56. package/dist/commands/service.js +262 -0
  57. package/dist/commands/service.js.map +1 -0
  58. package/dist/commands/ssh.d.ts +2 -0
  59. package/dist/commands/ssh.js +161 -0
  60. package/dist/commands/ssh.js.map +1 -0
  61. package/dist/commands/status.d.ts +7 -0
  62. package/dist/commands/status.js +49 -38
  63. package/dist/commands/status.js.map +1 -1
  64. package/dist/commands/unlink.d.ts +5 -0
  65. package/dist/commands/unlink.js +18 -0
  66. package/dist/commands/unlink.js.map +1 -0
  67. package/dist/commands/up.d.ts +9 -0
  68. package/dist/commands/up.js +417 -0
  69. package/dist/commands/up.js.map +1 -0
  70. package/dist/commands/upgrade.d.ts +2 -0
  71. package/dist/commands/upgrade.js +79 -0
  72. package/dist/commands/upgrade.js.map +1 -0
  73. package/dist/commands/whoami.js +26 -6
  74. package/dist/commands/whoami.js.map +1 -1
  75. package/dist/commands/workspace.d.ts +8 -0
  76. package/dist/commands/workspace.js +36 -0
  77. package/dist/commands/workspace.js.map +1 -0
  78. package/dist/index.js +204 -82
  79. package/dist/index.js.map +1 -1
  80. package/dist/lib/api.d.ts +17 -2
  81. package/dist/lib/api.js +85 -51
  82. package/dist/lib/api.js.map +1 -1
  83. package/dist/lib/auth.d.ts +3 -11
  84. package/dist/lib/auth.js +16 -36
  85. package/dist/lib/auth.js.map +1 -1
  86. package/dist/lib/config.d.ts +36 -15
  87. package/dist/lib/config.js +71 -58
  88. package/dist/lib/config.js.map +1 -1
  89. package/dist/lib/format.d.ts +1 -0
  90. package/dist/lib/format.js +17 -4
  91. package/dist/lib/format.js.map +1 -1
  92. package/dist/lib/name.d.ts +11 -0
  93. package/dist/lib/name.js +26 -0
  94. package/dist/lib/name.js.map +1 -0
  95. package/dist/lib/picker.d.ts +32 -0
  96. package/dist/lib/picker.js +91 -0
  97. package/dist/lib/picker.js.map +1 -0
  98. package/dist/lib/resolve.d.ts +85 -0
  99. package/dist/lib/resolve.js +203 -0
  100. package/dist/lib/resolve.js.map +1 -0
  101. package/dist/lib/updater.d.ts +16 -0
  102. package/dist/lib/updater.js +102 -0
  103. package/dist/lib/updater.js.map +1 -0
  104. package/lizard-wrapper.sh +2 -0
  105. package/package.json +11 -3
  106. package/src/commands/add.ts +388 -56
  107. package/src/commands/config.ts +80 -0
  108. package/src/commands/docs.ts +15 -0
  109. package/src/commands/domain.ts +248 -0
  110. package/src/commands/git.ts +201 -40
  111. package/src/commands/init.ts +149 -100
  112. package/src/commands/link.ts +127 -35
  113. package/src/commands/login.ts +4 -3
  114. package/src/commands/logs.ts +283 -27
  115. package/src/commands/open.ts +3 -2
  116. package/src/commands/port.ts +57 -0
  117. package/src/commands/projects.ts +43 -6
  118. package/src/commands/ps.ts +39 -60
  119. package/src/commands/redeploy.ts +51 -10
  120. package/src/commands/regions.ts +2 -6
  121. package/src/commands/restart.ts +84 -10
  122. package/src/commands/run.ts +68 -24
  123. package/src/commands/scale.ts +216 -0
  124. package/src/commands/secrets.ts +277 -100
  125. package/src/commands/service-set.ts +669 -0
  126. package/src/commands/service-show.ts +52 -0
  127. package/src/commands/service.ts +298 -0
  128. package/src/commands/ssh.ts +176 -0
  129. package/src/commands/status.ts +51 -46
  130. package/src/commands/unlink.ts +17 -0
  131. package/src/commands/up.ts +461 -0
  132. package/src/commands/upgrade.ts +87 -0
  133. package/src/commands/whoami.ts +34 -6
  134. package/src/commands/workspace.ts +44 -0
  135. package/src/index.ts +214 -85
  136. package/src/lib/api.ts +114 -51
  137. package/src/lib/auth.ts +22 -46
  138. package/src/lib/config.ts +100 -65
  139. package/src/lib/format.ts +18 -4
  140. package/src/lib/name.ts +27 -0
  141. package/src/lib/picker.ts +133 -0
  142. package/src/lib/resolve.ts +285 -0
  143. package/src/lib/updater.ts +106 -0
  144. package/test/cli.test.ts +491 -0
  145. package/test/fixtures/hello-app/Dockerfile +5 -0
  146. package/test/fixtures/hello-app/index.js +5 -0
  147. package/test/unit/api.test.ts +66 -0
  148. package/test/unit/config.test.ts +94 -0
  149. package/test/unit/init.test.ts +211 -0
  150. package/test/unit/json.test.ts +208 -0
  151. package/test/unit/picker.test.ts +161 -0
  152. package/test/unit/resolve.test.ts +124 -0
  153. package/test/unit/service-set.test.ts +355 -0
  154. package/vitest.config.ts +10 -0
  155. package/dist/commands/connect.d.ts +0 -2
  156. package/dist/commands/connect.js +0 -117
  157. package/dist/commands/connect.js.map +0 -1
  158. package/dist/commands/context.d.ts +0 -2
  159. package/dist/commands/context.js +0 -71
  160. package/dist/commands/context.js.map +0 -1
  161. package/dist/commands/deploy.d.ts +0 -2
  162. package/dist/commands/deploy.js +0 -120
  163. package/dist/commands/deploy.js.map +0 -1
  164. package/dist/commands/destroy.d.ts +0 -2
  165. package/dist/commands/destroy.js +0 -51
  166. package/dist/commands/destroy.js.map +0 -1
  167. package/dist/commands/update.d.ts +0 -2
  168. package/dist/commands/update.js +0 -41
  169. package/dist/commands/update.js.map +0 -1
  170. package/dist/commands/version.d.ts +0 -2
  171. package/dist/commands/version.js +0 -37
  172. package/dist/commands/version.js.map +0 -1
  173. package/src/commands/connect.ts +0 -145
  174. package/src/commands/context.ts +0 -93
  175. package/src/commands/deploy.ts +0 -153
  176. package/src/commands/destroy.ts +0 -51
  177. package/src/commands/update.ts +0 -44
  178. package/src/commands/version.ts +0 -37
@@ -1,28 +1,93 @@
1
1
  import chalk from "chalk";
2
+ import * as p from "@clack/prompts";
2
3
  import { Command } from "commander";
3
- import { streamSSE, api } from "../lib/api.js";
4
- import { resolveProjectId } from "../lib/config.js";
5
- import { info, error } from "../lib/format.js";
4
+ import { streamSSE, api, withScope, withQuery, type ResourceScope } from "../lib/api.js";
5
+ import { resolveProjectScope, resolveService, getActiveService } from "../lib/resolve.js";
6
+ import { info, error, isTTY, isJSONMode, printJSON, table, statusColor, timeAgo } from "../lib/format.js";
6
7
 
7
8
  export function registerLogs(program: Command) {
8
9
  program
9
10
  .command("logs")
10
11
  .description("Stream runtime logs")
11
12
  .option("--build", "Show build logs instead of runtime")
12
- .option("--service <id>", "Only show logs for a specific service")
13
+ .option("-s, --service <id>", "Only show logs for a specific service")
14
+ .option("-p, --project <id>", "Project name, slug, or ID")
15
+ .option("--tail <n>", "Print last N log lines and exit (no follow)")
16
+ .option("--restarts [n]", "List last N restart events (default 20) and exit")
17
+ .option("--restart <id>", "Print log tail of a specific restart event (or 'latest')")
13
18
  .action(async (opts) => {
14
- const projectId = resolveProjectId(program.opts().project);
19
+ if (opts.restarts !== undefined && opts.restart !== undefined) {
20
+ error("Use --restarts (list) or --restart <id> (detail), not both");
21
+ process.exit(1);
22
+ }
23
+
24
+ const { projectId, scope } = await resolveProjectScope(opts.project);
25
+
26
+ if (opts.restarts !== undefined) {
27
+ await showRestartList(opts.service, projectId, parseRestartsN(opts.restarts));
28
+ return;
29
+ }
30
+ if (opts.restart !== undefined) {
31
+ await showRestartLogTail(opts.service, projectId, String(opts.restart));
32
+ return;
33
+ }
34
+
35
+ const tailN = opts.tail !== undefined ? parseTail(opts.tail) : undefined;
15
36
 
16
37
  if (opts.build) {
17
- // Show build logs for the latest build
18
- await showBuildLogs(opts.service, projectId);
38
+ await showBuildLogs(opts.service, projectId, scope, tailN);
19
39
  return;
20
40
  }
21
41
 
42
+ // Resolve -s flag (may be name, slug, or ID) once up front so every
43
+ // branch below talks to the API with a real service ID.
44
+ let serviceId: string | undefined;
22
45
  if (opts.service) {
46
+ const svc = await resolveService(projectId, opts.service);
47
+ serviceId = svc.id;
48
+ }
49
+
50
+ // --tail: fetch historical logs and exit
51
+ if (tailN !== undefined) {
52
+ const entries = await api.get<any[]>(
53
+ withScope(
54
+ withQuery(`/api/projects/${projectId}/logs`, {
55
+ limit: tailN,
56
+ service: serviceId,
57
+ }),
58
+ scope,
59
+ ),
60
+ );
61
+ for (const e of entries) printLogEntry(e);
62
+ return;
63
+ }
64
+
65
+ if (!serviceId && isTTY() && !isJSONMode()) {
66
+ // Offer to pick a specific service or stream all
67
+ const data = await api.get<{ apps: any[] }>(
68
+ withScope(`/api/projects/${projectId}/services`, scope),
69
+ );
70
+ const apps = data.apps || [];
71
+
72
+ if (apps.length > 1) {
73
+ const choices = [
74
+ { value: "all", label: "All services", hint: "stream combined logs" },
75
+ ...apps.map((a: any) => ({
76
+ value: a.id,
77
+ label: a.name || a.id,
78
+ hint: a.status,
79
+ })),
80
+ ];
81
+ const selected = await p.select({ message: "Show logs for", options: choices });
82
+ if (p.isCancel(selected)) process.exit(5);
83
+ if (selected !== "all") serviceId = selected as string;
84
+ }
85
+ }
86
+
87
+ if (serviceId) {
23
88
  // Stream logs for a specific app
24
89
  info(chalk.dim("Streaming logs... (Ctrl+C to stop)\n"));
25
- await streamSSE(`/api/apps/${opts.service}/logs`, (event, data) => {
90
+ await streamSSE(`/api/apps/${serviceId}/logs`, (event, data) => {
26
91
  if (event === "error") {
27
92
  error(data);
28
93
  return false;
@@ -36,7 +101,7 @@ export function registerLogs(program: Command) {
36
101
  // Stream all project logs
37
102
  info(chalk.dim("Streaming project logs... (Ctrl+C to stop)\n"));
38
103
  await streamSSE(
39
- `/api/projects/${projectId}/logs/stream`,
104
+ withScope(`/api/projects/${projectId}/logs/stream`, scope),
40
105
  (event, data) => {
41
106
  if (event === "error") {
42
107
  error(data);
@@ -49,36 +114,214 @@ export function registerLogs(program: Command) {
49
114
  });
50
115
  }
51
116
 
117
+ function parseTail(raw: string): number {
118
+ const n = parseInt(raw, 10);
119
+ if (isNaN(n) || n < 1) {
120
+ error("--tail must be a positive integer");
121
+ process.exit(1);
122
+ }
123
+ if (n > 1000) {
124
+ info(chalk.yellow("--tail capped at 1000 (server limit)"));
125
+ return 1000;
126
+ }
127
+ return n;
128
+ }
129
+
130
+ function parseRestartsN(raw: unknown): number {
131
+ if (raw === true || raw === "" || raw === undefined) return 20;
132
+ const n = parseInt(String(raw), 10);
133
+ if (isNaN(n) || n < 1) {
134
+ error("--restarts must be a positive integer");
135
+ process.exit(1);
136
+ }
137
+ return n;
138
+ }
139
+
140
+ // Shape returned by /api/apps/:id/deploy-events
141
+ interface DeployEvent {
142
+ buildId: string;
143
+ trigger: string | null;
144
+ status: string;
145
+ commitSha: string | null;
146
+ createdAt: number;
147
+ events: Array<{
148
+ id: string;
149
+ source: string;
150
+ status: string;
151
+ exitInfo: string | null;
152
+ logsTail: string | null;
153
+ crashedAt: number;
154
+ nextRetryAt?: number | null;
155
+ triggeredBy?: string;
156
+ }>;
157
+ }
158
+
159
+ type FlatEvent = DeployEvent["events"][number] & {
160
+ buildId: string;
161
+ commitSha: string | null;
162
+ };
163
+
164
+ async function fetchFlatRestarts(appId: string): Promise<FlatEvent[]> {
165
+ const builds = await api.get<DeployEvent[]>(`/api/apps/${appId}/deploy-events`);
166
+ const flat: FlatEvent[] = [];
167
+ for (const b of builds) {
168
+ for (const e of b.events) {
169
+ flat.push({ ...e, buildId: b.buildId, commitSha: b.commitSha });
170
+ }
171
+ }
172
+ flat.sort((a, b) => b.crashedAt - a.crashedAt);
173
+ return flat;
174
+ }
175
+
176
+ function replicaPrefix(e: any): string {
177
+ if (!e.replica) return "";
178
+ return chalk.magenta(`[${e.replica}]`) + " ";
179
+ }
180
+
181
+ function printLogEntry(e: any) {
182
+ if (isJSONMode()) {
183
+ process.stdout.write(JSON.stringify(e) + "\n");
184
+ return;
185
+ }
186
+ const rep = replicaPrefix(e);
187
+ if (e.service && e.message) {
188
+ const prefix = chalk.cyan(`[${e.service}]`);
189
+ process.stdout.write(`${prefix} ${rep}${e.message}\n`);
190
+ } else if (e.message) {
191
+ process.stdout.write(`${rep}${e.message}\n`);
192
+ }
193
+ }
194
+
52
195
  function printLogLine(data: string) {
196
+ let parsed: any;
53
197
  try {
54
- const parsed = JSON.parse(data);
55
- if (parsed.service && parsed.line) {
56
- const prefix = chalk.cyan(`[${parsed.service}]`);
57
- process.stdout.write(`${prefix} ${parsed.line}\n`);
58
- } else if (parsed.line) {
59
- process.stdout.write(parsed.line + "\n");
60
- } else if (parsed.message) {
61
- process.stdout.write(parsed.message + "\n");
62
- } else if (typeof parsed === "string") {
63
- process.stdout.write(parsed + "\n");
64
- } else {
65
- process.stdout.write(data + "\n");
66
- }
198
+ parsed = JSON.parse(data);
67
199
  } catch {
200
+ parsed = { message: data };
201
+ }
202
+ if (typeof parsed === "string") parsed = { message: parsed };
203
+
204
+ if (isJSONMode()) {
205
+ process.stdout.write(JSON.stringify(parsed) + "\n");
206
+ return;
207
+ }
208
+
209
+ const rep = replicaPrefix(parsed);
210
+ if (parsed.service && parsed.message) {
211
+ const prefix = chalk.cyan(`[${parsed.service}]`);
212
+ process.stdout.write(`${prefix} ${rep}${parsed.message}\n`);
213
+ } else if (parsed.message) {
214
+ process.stdout.write(`${rep}${parsed.message}\n`);
215
+ } else {
68
216
  process.stdout.write(data + "\n");
69
217
  }
70
218
  }
71
219
 
72
- async function showBuildLogs(serviceId: string | undefined, projectId: string) {
73
- let appId = serviceId;
220
+ async function showRestartList(
221
+ serviceRef: string | undefined,
222
+ projectId: string,
223
+ n: number,
224
+ ) {
225
+ const svc = await getActiveService(serviceRef, projectId);
226
+ const events = await fetchFlatRestarts(svc.id);
227
+ const slice = events.slice(0, n);
228
+
229
+ if (isJSONMode()) {
230
+ for (const e of slice) {
231
+ process.stdout.write(JSON.stringify(e) + "\n");
232
+ }
233
+ return;
234
+ }
235
+
236
+ if (slice.length === 0) {
237
+ info(chalk.dim(`No restart events for ${svc.name}.`));
238
+ return;
239
+ }
240
+
241
+ table(
242
+ ["When", "Source", "Status", "Exit", "By", "ID"],
243
+ slice.map((e) => [
244
+ timeAgo(e.crashedAt),
245
+ e.source,
246
+ statusColor(e.status),
247
+ (e.exitInfo ?? "").slice(0, 60),
248
+ e.triggeredBy ?? chalk.dim("—"),
249
+ chalk.dim(e.id),
250
+ ]),
251
+ );
252
+
253
+ if (events.length > slice.length) {
254
+ info(chalk.dim(`\n(${events.length - slice.length} more — re-run with --restarts ${events.length})`));
255
+ }
256
+ info(chalk.dim("\nInspect: lizard logs --restart <id> (or 'latest')"));
257
+ }
258
+
259
+ async function showRestartLogTail(
260
+ serviceRef: string | undefined,
261
+ projectId: string,
262
+ ref: string,
263
+ ) {
264
+ const svc = await getActiveService(serviceRef, projectId);
265
+ const events = await fetchFlatRestarts(svc.id);
266
+
267
+ let evt: FlatEvent | undefined;
268
+ if (ref === "latest") {
269
+ evt = events[0];
270
+ if (!evt) {
271
+ error(`No restart events for ${svc.name}.`);
272
+ process.exit(1);
273
+ }
274
+ } else {
275
+ evt = events.find((e) => e.id === ref);
276
+ if (!evt) {
277
+ error(`Restart event "${ref}" not found for ${svc.name}.`);
278
+ process.exit(1);
279
+ }
280
+ }
281
+
282
+ if (isJSONMode()) {
283
+ printJSON(evt);
284
+ return;
285
+ }
286
+
287
+ console.log(chalk.dim("Event: ") + evt.id);
288
+ console.log(chalk.dim("When: ") + timeAgo(evt.crashedAt));
289
+ console.log(chalk.dim("Source: ") + evt.source);
290
+ console.log(chalk.dim("Status: ") + statusColor(evt.status));
291
+ if (evt.exitInfo) console.log(chalk.dim("Exit: ") + evt.exitInfo);
292
+ if (evt.triggeredBy) console.log(chalk.dim("By: ") + evt.triggeredBy);
293
+ if (evt.buildId) console.log(chalk.dim("Build: ") + evt.buildId);
294
+ console.log();
295
+
296
+ if (evt.logsTail) {
297
+ process.stdout.write(evt.logsTail);
298
+ if (!evt.logsTail.endsWith("\n")) process.stdout.write("\n");
299
+ } else {
300
+ info(chalk.dim(`<no log tail captured — source=${evt.source} (manual restarts don't capture logs)>`));
301
+ }
302
+ }
303
+
304
+ async function showBuildLogs(
305
+ serviceRef: string | undefined,
306
+ projectId: string,
307
+ scope: ResourceScope,
308
+ tailN?: number,
309
+ ) {
310
+ let appId: string | undefined;
311
+ if (serviceRef) {
312
+ const svc = await resolveService(projectId, serviceRef);
313
+ appId = svc.id;
314
+ }
74
315
 
75
316
  if (!appId) {
76
317
  // Get first app in project
77
318
  const data = await api.get<{ apps: Array<{ id: string; name: string }> }>(
78
- `/api/projects/${projectId}/services`,
319
+ withScope(`/api/projects/${projectId}/services`, scope),
79
320
  );
80
321
  if (!data.apps?.length) {
81
- throw new Error("No apps in project");
322
+ throw new Error(
323
+ "No apps in project. Create one with `lizard up` or `lizard add`.",
324
+ );
82
325
  }
83
326
  appId = data.apps[0].id;
84
327
  }
@@ -88,12 +331,25 @@ async function showBuildLogs(serviceId: string | undefined, projectId: string) {
88
331
  builds?: Array<{ id: string; status: string }>;
89
332
  }>(`/api/apps/${appId}`);
90
333
  if (!app.builds?.length) {
91
- throw new Error("No builds found");
334
+ throw new Error(
335
+ "No builds for this app yet. Trigger one with `lizard up` or `lizard redeploy`.",
336
+ );
92
337
  }
93
338
 
94
339
  const buildId = app.builds[0].id;
95
340
  info(chalk.dim(`Build ${buildId}\n`));
96
341
 
342
+ if (tailN !== undefined) {
343
+ const lines: string[] = [];
344
+ await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
345
+ if (event === "done" || event === "error") return false;
346
+ lines.push(data);
347
+ return true;
348
+ });
349
+ for (const line of lines.slice(-tailN)) printLogLine(line);
350
+ return;
351
+ }
352
+
97
353
  await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
98
354
  if (event === "done" || event === "error") {
99
355
  return false;
@@ -8,8 +8,9 @@ export function registerOpen(program: Command) {
8
8
  program
9
9
  .command("open")
10
10
  .description("Open project in browser")
11
- .action(async () => {
12
- const projectId = resolveProjectId(program.opts().project);
11
+ .option("-p, --project <id>", "Project name, slug, or ID")
12
+ .action(async (opts) => {
13
+ const projectId = await resolveProjectId(opts.project);
13
14
  const url = `${getBaseURL()}/projects/${projectId}`;
14
15
  await open(url);
15
16
  success("Opened in browser");
@@ -0,0 +1,57 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { api, withScope } from "../lib/api.js";
4
+ import { getActiveServiceWithKind, resolveProjectScope } from "../lib/resolve.js";
5
+ import { success, info, isJSONMode, printJSON } from "../lib/format.js";
6
+
7
+ /**
8
+ * `lizard port [number]`
9
+ * bare → show current container port
10
+ * <number> → update container port (takes effect on next deploy)
11
+ */
12
+ export function registerPort(program: Command) {
13
+ program
14
+ .command("port")
15
+ .argument("[port]", "Port number to set")
16
+ .description("Show or change the container port for a service")
17
+ .option("-s, --service <name>", "Service name or ID")
18
+ .option("-p, --project <id>", "Project name, slug, or ID")
19
+ .action(async (portArg: string | undefined, opts) => {
20
+ const { projectId, scope } = await resolveProjectScope(opts.project);
21
+ const service = await getActiveServiceWithKind(opts.service, projectId);
22
+ if (service.kind === "addon") {
23
+ throw new Error("Addons don't have a container port.");
24
+ }
25
+
26
+ if (portArg === undefined) {
27
+ const app = await api.get<{ containerPort?: number }>(`/api/apps/${service.id}`);
28
+ const port = app.containerPort ?? 3000;
29
+ if (isJSONMode()) {
30
+ printJSON({ port });
31
+ } else {
32
+ info(`${chalk.bold(service.name)} container port: ${chalk.cyan(port)}`);
33
+ }
34
+ return;
35
+ }
36
+
37
+ const newPort = parseInt(portArg, 10);
38
+ if (isNaN(newPort) || newPort < 1 || newPort > 65535) {
39
+ throw new Error(`Invalid port: ${portArg}. Must be 1–65535.`);
40
+ }
41
+
42
+ // PATCH /api/apps/:id is 410-Gone server-side; writes go through
43
+ // POST /api/projects/:id/config:apply.
44
+ await api.post(
45
+ withScope(`/api/projects/${projectId}/config:apply`, scope),
46
+ { services: [{ id: service.id, name: service.name, containerPort: newPort }] },
47
+ );
48
+
49
+ if (isJSONMode()) {
50
+ printJSON({ ok: true, port: newPort });
51
+ } else {
52
+ success(
53
+ `${chalk.bold(service.name)} container port set to ${chalk.cyan(newPort)} — takes effect on next deploy`,
54
+ );
55
+ }
56
+ });
57
+ }
@@ -1,6 +1,8 @@
1
+ import chalk from "chalk";
1
2
  import { Command } from "commander";
2
- import { api } from "../lib/api.js";
3
- import { isJSONMode, printJSON, table } from "../lib/format.js";
3
+ import { api, withQuery } from "../lib/api.js";
4
+ import { success, isJSONMode, printJSON, table } from "../lib/format.js";
5
+ import { pickWorkspace, resolveWorkspace } from "../lib/picker.js";
4
6
 
5
7
  interface Project {
6
8
  id: string;
@@ -8,6 +10,8 @@ interface Project {
8
10
  slug: string;
9
11
  role: string;
10
12
  memberCount: number;
13
+ workspaceId?: string | null;
14
+ workspaceName?: string | null;
11
15
  createdAt: number;
12
16
  }
13
17
 
@@ -19,8 +23,15 @@ export function registerProjects(program: Command) {
19
23
  proj
20
24
  .command("list")
21
25
  .description("List all projects")
22
- .action(async () => {
23
- const projects = await api.get<Project[]>("/api/projects");
26
+ .option("-w, --workspace <ws>", "Filter by workspace id, slug, or name")
27
+ .action(async (opts) => {
28
+ let workspaceId: string | undefined;
29
+ if (opts.workspace) {
30
+ workspaceId = (await resolveWorkspace(opts.workspace)).id;
31
+ }
32
+ const projects = await api.get<Project[]>(
33
+ withQuery("/api/projects", { workspaceId }),
34
+ );
24
35
 
25
36
  if (isJSONMode()) {
26
37
  printJSON(projects);
@@ -33,13 +44,39 @@ export function registerProjects(program: Command) {
33
44
  }
34
45
 
35
46
  table(
36
- ["Name", "ID", "Role", "Members"],
47
+ ["Name", "Workspace", "Slug", "Role", "Members"],
37
48
  projects.map((p) => [
38
49
  p.name,
39
- p.id,
50
+ p.workspaceName || chalk.dim("—"),
51
+ p.slug,
40
52
  p.role || "owner",
41
53
  String(p.memberCount || 1),
42
54
  ]),
43
55
  );
44
56
  });
57
+
58
+ proj
59
+ .command("create")
60
+ .argument("<name>", "Project name")
61
+ .description("Create a new project without linking it to this directory")
62
+ .option("-w, --workspace <ws>", "Workspace to create the project in")
63
+ .action(async (name: string, opts) => {
64
+ const workspace = await pickWorkspace({ flag: opts.workspace });
65
+ const project = await api.post<Project>("/api/projects", {
66
+ name,
67
+ workspaceId: workspace.id,
68
+ });
69
+
70
+ if (isJSONMode()) {
71
+ printJSON({
72
+ ...project,
73
+ workspaceId: project.workspaceId ?? workspace.id,
74
+ workspaceName: project.workspaceName ?? workspace.name,
75
+ });
76
+ } else {
77
+ success(
78
+ `Project ${chalk.bold(project.name)} created in ${chalk.bold(workspace.name)}`,
79
+ );
80
+ }
81
+ });
45
82
  }
@@ -1,80 +1,59 @@
1
1
  import chalk from "chalk";
2
2
  import { Command } from "commander";
3
- import { api } from "../lib/api.js";
4
- import { resolveProjectId } from "../lib/config.js";
5
- import {
6
- isJSONMode,
7
- printJSON,
8
- table,
9
- statusColor,
10
- } from "../lib/format.js";
11
-
12
- interface Service {
13
- id: string;
14
- name: string;
15
- type: "app" | "addon";
16
- addonType?: string;
17
- status: string;
18
- domain?: string;
19
- hostname?: string;
20
- createdAt?: number;
21
- }
3
+ import { api, withScope } from "../lib/api.js";
4
+ import { getProjectLink } from "../lib/config.js";
5
+ import { resolveProjectScope } from "../lib/resolve.js";
6
+ import { isJSONMode, printJSON, table, statusColor } from "../lib/format.js";
22
7
 
23
8
  export function registerPs(program: Command) {
24
9
  program
25
10
  .command("ps")
26
11
  .description("List all services in the project")
27
- .action(async () => {
28
- const projectId = resolveProjectId(program.opts().project);
12
+ .option("-p, --project <id>", "Project name, slug, or ID")
13
+ .action(async (opts) => {
14
+ const { projectId, scope } = await resolveProjectScope(opts.project);
29
15
  const data = await api.get<{ apps: any[]; addons: any[] }>(
30
- `/api/projects/${projectId}/services`,
16
+ withScope(`/api/projects/${projectId}/services`, scope),
31
17
  );
32
18
 
33
- const services: Service[] = [];
34
-
35
- for (const app of data.apps || []) {
36
- services.push({
37
- id: app.id,
38
- name: app.name,
39
- type: "app",
40
- status: app.status,
41
- domain: app.domain,
42
- createdAt: app.createdAt,
43
- });
19
+ if (isJSONMode()) {
20
+ printJSON(data);
21
+ return;
44
22
  }
45
23
 
46
- for (const addon of data.addons || []) {
47
- services.push({
48
- id: addon.id,
49
- name: addon.name || addon.addonType,
50
- type: "addon",
51
- addonType: addon.addonType,
52
- status: addon.status,
53
- hostname: addon.hostname,
54
- createdAt: addon.createdAt,
55
- });
56
- }
24
+ const apps = data.apps || [];
25
+ const addons = data.addons || [];
57
26
 
58
- if (isJSONMode()) {
59
- printJSON(services);
27
+ if (apps.length === 0 && addons.length === 0) {
28
+ console.log("No services. Use `lizard add` or `lizard up`.");
60
29
  return;
61
30
  }
62
31
 
63
- if (services.length === 0) {
64
- console.log("No services. Use `lizard add` or `lizard deploy`.");
65
- return;
32
+ const linkedId = getProjectLink()?.serviceId;
33
+
34
+ if (apps.length > 0) {
35
+ table(
36
+ ["App", "Status", "URL", "Linked"],
37
+ apps.map((a: any) => [
38
+ a.name || a.id,
39
+ statusColor(a.status),
40
+ a.domain ? chalk.cyan(`https://${a.domain}`) : chalk.dim("—"),
41
+ a.id === linkedId ? chalk.green("✓") : "",
42
+ ]),
43
+ );
66
44
  }
67
45
 
68
- table(
69
- ["Name", "Type", "Status", "URL/Host"],
70
- services.map((s) => [
71
- s.name,
72
- s.addonType || s.type,
73
- statusColor(s.status),
74
- s.domain
75
- ? `https://${s.domain}`
76
- : s.hostname || chalk.dim("—"),
77
- ]),
78
- );
46
+ if (addons.length > 0) {
47
+ if (apps.length > 0) console.log();
48
+ table(
49
+ ["Addon", "Type", "Status", "Host"],
50
+ addons.map((a: any) => [
51
+ a.name || a.type,
52
+ a.type,
53
+ statusColor(a.status),
54
+ a.hostname ? chalk.dim(a.hostname) : chalk.dim("—"),
55
+ ]),
56
+ );
57
+ }
79
58
  });
80
59
  }