@lizard-build/cli 4.0.48 → 4.0.50

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.
@@ -4,6 +4,7 @@ import { Command } from "commander";
4
4
  import { api, withQuery, type Workspace } from "../lib/api.js";
5
5
  import { fetchWorkspaces, resolveWorkspace } from "../lib/picker.js";
6
6
  import { isJSONMode, printJSON, table, success, info, isTTY } from "../lib/format.js";
7
+ import { printBulkResult, type BulkPauseResult } from "./pause.js";
7
8
 
8
9
  /**
9
10
  * `lizard workspace` — create, list, and delete workspaces.
@@ -61,6 +62,21 @@ export function registerWorkspace(program: Command) {
61
62
  );
62
63
  });
63
64
 
65
+ for (const op of ["pause", "resume"] as const) {
66
+ ws.command(op)
67
+ .argument("<name-or-id>", "Workspace name, slug, or ID")
68
+ .description(op === "pause"
69
+ ? "Pause every service in every project of a workspace (owner only; keeps URLs, passwords, settings and data)"
70
+ : "Resume the services you paused in a workspace (owner only)")
71
+ .action(async (nameOrId: string) => {
72
+ const workspace = await resolveWorkspace(nameOrId);
73
+ if (workspace.role !== "owner") throw new Error(`Only the workspace owner can ${op} all of it.`);
74
+ const result = await api.post<BulkPauseResult>(`/api/workspaces/${workspace.id}/${op}`);
75
+ if (isJSONMode()) { printJSON({ id: workspace.id, ...result }); return; }
76
+ printBulkResult(op, `workspace ${chalk.bold(workspace.name)}`, result);
77
+ });
78
+ }
79
+
64
80
  ws.command("rm")
65
81
  .alias("delete")
66
82
  .argument("<name-or-id>", "Workspace name, slug, or ID")
package/src/index.ts CHANGED
@@ -82,6 +82,7 @@ import { registerProjects } from "./commands/projects.js";
82
82
  import { registerPs } from "./commands/ps.js";
83
83
  import { registerRedeploy } from "./commands/redeploy.js";
84
84
  import { registerRegions } from "./commands/regions.js";
85
+ import { registerPause } from "./commands/pause.js";
85
86
  import { registerRestart } from "./commands/restart.js";
86
87
  import { registerRun } from "./commands/run.js";
87
88
  import { registerS3 } from "./commands/s3.js";
@@ -178,6 +179,7 @@ registerProjects(program);
178
179
  registerPs(program);
179
180
  registerRedeploy(program);
180
181
  registerRegions(program);
182
+ registerPause(program);
181
183
  registerRestart(program);
182
184
  registerRun(program);
183
185
  registerS3(program);
package/src/lib/format.ts CHANGED
@@ -107,6 +107,7 @@ export function statusColor(status: string): string {
107
107
  case "pending":
108
108
  return chalk.yellow(status);
109
109
  case "deleting":
110
+ case "paused":
110
111
  return chalk.dim(status);
111
112
  default:
112
113
  return status;
package/src/lib/signin.ts CHANGED
@@ -48,11 +48,44 @@ export interface CheckResponse {
48
48
  };
49
49
  }
50
50
 
51
- export interface Me {
51
+ /** One project or workspace a scoped API key reaches, by name. */
52
+ export interface KeyScope {
53
+ type: "workspace" | "project";
52
54
  id: string;
53
- username: string;
55
+ name?: string | null;
56
+ workspaceId?: string | null;
57
+ workspaceName?: string | null;
58
+ }
59
+
60
+ export interface Me {
61
+ // Absent for a scoped API key: the platform never tells a scoped key whose account it
62
+ // is (it is handed to people who are not the account's members).
63
+ id?: string;
64
+ username?: string;
54
65
  email?: string | null;
55
66
  scoped?: boolean;
67
+ scopes?: KeyScope[];
68
+ }
69
+
70
+ /** "Project X (workspace Y)" for one scope; the id when the name is unknown. */
71
+ export function describeScope(sc: KeyScope): string {
72
+ if (sc.type === "project") {
73
+ const ws = sc.workspaceName ? ` (workspace ${sc.workspaceName})` : "";
74
+ return `project ${sc.name || sc.id}${ws}`;
75
+ }
76
+ return `workspace ${sc.name || sc.id}`;
77
+ }
78
+
79
+ /** Who a login is, for "Signed in as ...": the username for an account, and the
80
+ * project/workspace for a scoped key -- never the account owner's name, even from an
81
+ * older platform that still sends it. */
82
+ export function describeMe(me: Me): string {
83
+ if (me.scoped) {
84
+ const scopes = me.scopes ?? [];
85
+ if (scopes.length === 0) return "a scoped API key";
86
+ return `a key for ${scopes.map(describeScope).join(", ")}`;
87
+ }
88
+ return me.username ?? "unknown";
56
89
  }
57
90
 
58
91
  export interface Pending {
@@ -315,6 +348,6 @@ export async function signInInteractive(flag?: AuthProvider): Promise<Me> {
315
348
  saveSessionLogin({ accessToken: result.accessToken, user: result.user });
316
349
  spin.stop("Authorized");
317
350
  const me = await api.get<Me>("/api/auth/me");
318
- p.log.success(`Signed in as ${chalk.bold(me.username)}`);
351
+ p.log.success(`Signed in as ${chalk.bold(describeMe(me))}`);
319
352
  return me;
320
353
  }
@@ -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 = "4.0.48";
8
+ export const CURRENT_VERSION = "4.0.50";
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
 
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { describeMe } from "../../src/lib/signin.js";
3
+
4
+ describe("describeMe", () => {
5
+ it("names an account by its username", () => {
6
+ expect(describeMe({ id: "u1", username: "alice" })).toBe("alice");
7
+ });
8
+
9
+ it("names a project-scoped key by its project and workspace, never the owner", () => {
10
+ const text = describeMe({
11
+ // An older platform still sent the owner for a scoped key.
12
+ id: "u1", username: "alice-the-owner", scoped: true,
13
+ scopes: [{ type: "project", id: "p1", name: "Shop", workspaceId: "w1", workspaceName: "Acme" }],
14
+ });
15
+ expect(text).toBe("a key for project Shop (workspace Acme)");
16
+ expect(text).not.toContain("alice");
17
+ });
18
+
19
+ it("names a workspace-scoped key by its workspace, falling back to the id", () => {
20
+ expect(describeMe({ scoped: true, scopes: [{ type: "workspace", id: "w1", name: "Acme" }, { type: "workspace", id: "w2" }] }))
21
+ .toBe("a key for workspace Acme, workspace w2");
22
+ });
23
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, test, expect, vi, afterEach } from "vitest";
2
+ import { servicePausePath, printBulkResult } from "../../src/commands/pause.js";
3
+
4
+ afterEach(() => vi.restoreAllMocks());
5
+
6
+ describe("servicePausePath", () => {
7
+ test("an app pauses and resumes through /api/apps/:id", () => {
8
+ expect(servicePausePath("pause", { id: "a1", kind: "app" }, "p1", {})).toBe("/api/apps/a1/pause");
9
+ expect(servicePausePath("resume", { id: "a1", kind: "app" }, "p1", { workspaceId: "w1" })).toBe("/api/apps/a1/resume?workspaceId=w1");
10
+ });
11
+ test("a database (addon) pauses through its project", () => {
12
+ expect(servicePausePath("pause", { id: "d1", kind: "addon" }, "p1", { workspaceId: "w1" }))
13
+ .toBe("/api/projects/p1/addons/d1/pause?workspaceId=w1");
14
+ });
15
+ });
16
+
17
+ describe("printBulkResult", () => {
18
+ test("lists what was paused and what was skipped with why", () => {
19
+ const out: string[] = [];
20
+ vi.spyOn(process.stderr, "write").mockImplementation((chunk: any) => { out.push(String(chunk)); return true; });
21
+ printBulkResult("pause", "project demo", {
22
+ done: [{ kind: "app", id: "a1", name: "web" }, { kind: "database", id: "d1", name: "postgres" }],
23
+ skipped: [{ kind: "app", id: "a2", name: "worker", reason: "already paused" }],
24
+ });
25
+ const text = out.join("\n");
26
+ expect(text).toContain("Paused 2 services in project demo");
27
+ expect(text).toContain("web");
28
+ expect(text).toContain("worker");
29
+ expect(text).toContain("already paused");
30
+ });
31
+ test("says when there was nothing to do", () => {
32
+ const out: string[] = [];
33
+ vi.spyOn(process.stderr, "write").mockImplementation((chunk: any) => { out.push(String(chunk)); return true; });
34
+ printBulkResult("resume", "workspace team", { done: [], skipped: [] });
35
+ expect(out.join("\n")).toContain("nothing to resume");
36
+ });
37
+ });