@lizard-build/cli 0.1.0

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 (115) hide show
  1. package/dist/commands/add.d.ts +2 -0
  2. package/dist/commands/add.js +72 -0
  3. package/dist/commands/add.js.map +1 -0
  4. package/dist/commands/connect.d.ts +2 -0
  5. package/dist/commands/connect.js +117 -0
  6. package/dist/commands/connect.js.map +1 -0
  7. package/dist/commands/context.d.ts +2 -0
  8. package/dist/commands/context.js +71 -0
  9. package/dist/commands/context.js.map +1 -0
  10. package/dist/commands/deploy.d.ts +2 -0
  11. package/dist/commands/deploy.js +120 -0
  12. package/dist/commands/deploy.js.map +1 -0
  13. package/dist/commands/destroy.d.ts +2 -0
  14. package/dist/commands/destroy.js +51 -0
  15. package/dist/commands/destroy.js.map +1 -0
  16. package/dist/commands/git.d.ts +2 -0
  17. package/dist/commands/git.js +67 -0
  18. package/dist/commands/git.js.map +1 -0
  19. package/dist/commands/init.d.ts +2 -0
  20. package/dist/commands/init.js +107 -0
  21. package/dist/commands/init.js.map +1 -0
  22. package/dist/commands/link.d.ts +2 -0
  23. package/dist/commands/link.js +50 -0
  24. package/dist/commands/link.js.map +1 -0
  25. package/dist/commands/login.d.ts +7 -0
  26. package/dist/commands/login.js +123 -0
  27. package/dist/commands/login.js.map +1 -0
  28. package/dist/commands/logout.d.ts +2 -0
  29. package/dist/commands/logout.js +17 -0
  30. package/dist/commands/logout.js.map +1 -0
  31. package/dist/commands/logs.d.ts +2 -0
  32. package/dist/commands/logs.js +92 -0
  33. package/dist/commands/logs.js.map +1 -0
  34. package/dist/commands/open.d.ts +2 -0
  35. package/dist/commands/open.js +16 -0
  36. package/dist/commands/open.js.map +1 -0
  37. package/dist/commands/projects.d.ts +2 -0
  38. package/dist/commands/projects.js +28 -0
  39. package/dist/commands/projects.js.map +1 -0
  40. package/dist/commands/ps.d.ts +2 -0
  41. package/dist/commands/ps.js +52 -0
  42. package/dist/commands/ps.js.map +1 -0
  43. package/dist/commands/redeploy.d.ts +2 -0
  44. package/dist/commands/redeploy.js +69 -0
  45. package/dist/commands/redeploy.js.map +1 -0
  46. package/dist/commands/regions.d.ts +2 -0
  47. package/dist/commands/regions.js +23 -0
  48. package/dist/commands/regions.js.map +1 -0
  49. package/dist/commands/restart.d.ts +2 -0
  50. package/dist/commands/restart.js +21 -0
  51. package/dist/commands/restart.js.map +1 -0
  52. package/dist/commands/run.d.ts +2 -0
  53. package/dist/commands/run.js +33 -0
  54. package/dist/commands/run.js.map +1 -0
  55. package/dist/commands/secrets.d.ts +2 -0
  56. package/dist/commands/secrets.js +138 -0
  57. package/dist/commands/secrets.js.map +1 -0
  58. package/dist/commands/status.d.ts +2 -0
  59. package/dist/commands/status.js +51 -0
  60. package/dist/commands/status.js.map +1 -0
  61. package/dist/commands/update.d.ts +2 -0
  62. package/dist/commands/update.js +41 -0
  63. package/dist/commands/update.js.map +1 -0
  64. package/dist/commands/version.d.ts +2 -0
  65. package/dist/commands/version.js +37 -0
  66. package/dist/commands/version.js.map +1 -0
  67. package/dist/commands/whoami.d.ts +2 -0
  68. package/dist/commands/whoami.js +21 -0
  69. package/dist/commands/whoami.js.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +151 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/lib/api.d.ts +19 -0
  74. package/dist/lib/api.js +114 -0
  75. package/dist/lib/api.js.map +1 -0
  76. package/dist/lib/auth.d.ts +23 -0
  77. package/dist/lib/auth.js +89 -0
  78. package/dist/lib/auth.js.map +1 -0
  79. package/dist/lib/config.d.ts +23 -0
  80. package/dist/lib/config.js +77 -0
  81. package/dist/lib/config.js.map +1 -0
  82. package/dist/lib/format.d.ts +11 -0
  83. package/dist/lib/format.js +86 -0
  84. package/dist/lib/format.js.map +1 -0
  85. package/install.sh +59 -0
  86. package/package.json +35 -0
  87. package/src/commands/add.ts +100 -0
  88. package/src/commands/connect.ts +145 -0
  89. package/src/commands/context.ts +93 -0
  90. package/src/commands/deploy.ts +153 -0
  91. package/src/commands/destroy.ts +51 -0
  92. package/src/commands/git.ts +80 -0
  93. package/src/commands/init.ts +139 -0
  94. package/src/commands/link.ts +63 -0
  95. package/src/commands/login.ts +158 -0
  96. package/src/commands/logout.ts +17 -0
  97. package/src/commands/logs.ts +104 -0
  98. package/src/commands/open.ts +17 -0
  99. package/src/commands/projects.ts +45 -0
  100. package/src/commands/ps.ts +80 -0
  101. package/src/commands/redeploy.ts +74 -0
  102. package/src/commands/regions.ts +38 -0
  103. package/src/commands/restart.ts +24 -0
  104. package/src/commands/run.ts +43 -0
  105. package/src/commands/secrets.ts +175 -0
  106. package/src/commands/status.ts +65 -0
  107. package/src/commands/update.ts +44 -0
  108. package/src/commands/version.ts +37 -0
  109. package/src/commands/whoami.ts +27 -0
  110. package/src/index.ts +168 -0
  111. package/src/lib/api.ts +134 -0
  112. package/src/lib/auth.ts +113 -0
  113. package/src/lib/config.ts +93 -0
  114. package/src/lib/format.ts +95 -0
  115. package/tsconfig.json +17 -0
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import * as p from "@clack/prompts";
5
+ import { Command } from "commander";
6
+ import { api } from "../lib/api.js";
7
+ import {
8
+ findProjectConfig,
9
+ saveProjectConfig,
10
+ } from "../lib/config.js";
11
+ import {
12
+ success,
13
+ info,
14
+ isJSONMode,
15
+ printJSON,
16
+ isTTY,
17
+ } from "../lib/format.js";
18
+
19
+ interface Project {
20
+ id: string;
21
+ name: string;
22
+ slug: string;
23
+ }
24
+
25
+ /** Detect framework from files in cwd */
26
+ function detectFramework(): {
27
+ name: string;
28
+ port: number;
29
+ buildCmd: string;
30
+ startCmd: string;
31
+ } | null {
32
+ const cwd = process.cwd();
33
+ const has = (f: string) => fs.existsSync(path.join(cwd, f));
34
+
35
+ if (has("next.config.js") || has("next.config.mjs") || has("next.config.ts"))
36
+ return { name: "Next.js", port: 3000, buildCmd: "npm run build", startCmd: "npm start" };
37
+ if (has("nuxt.config.ts") || has("nuxt.config.js"))
38
+ return { name: "Nuxt", port: 3000, buildCmd: "npm run build", startCmd: "npm start" };
39
+ if (has("remix.config.js") || has("remix.config.ts"))
40
+ return { name: "Remix", port: 3000, buildCmd: "npm run build", startCmd: "npm start" };
41
+ if (has("astro.config.mjs") || has("astro.config.ts"))
42
+ return { name: "Astro", port: 4321, buildCmd: "npm run build", startCmd: "npm start" };
43
+ if (has("vite.config.ts") || has("vite.config.js"))
44
+ return { name: "Vite", port: 3000, buildCmd: "npm run build", startCmd: "npm run preview" };
45
+ if (has("Dockerfile"))
46
+ return { name: "Docker", port: 8080, buildCmd: "", startCmd: "" };
47
+ if (has("go.mod"))
48
+ return { name: "Go", port: 8080, buildCmd: "go build -o app .", startCmd: "./app" };
49
+ if (has("requirements.txt") || has("pyproject.toml"))
50
+ return { name: "Python", port: 8000, buildCmd: "pip install -r requirements.txt", startCmd: "python app.py" };
51
+ if (has("package.json"))
52
+ return { name: "Node.js", port: 3000, buildCmd: "npm run build", startCmd: "npm start" };
53
+ return null;
54
+ }
55
+
56
+ function writeLizardToml(
57
+ framework: { name: string; port: number; buildCmd: string; startCmd: string } | null,
58
+ ) {
59
+ const port = framework?.port ?? 3000;
60
+ const build = framework?.buildCmd ?? "";
61
+ const start = framework?.startCmd ?? "";
62
+
63
+ let toml = `# lizard.toml\n\n`;
64
+ if (build) toml += `[build]\ncommand = "${build}"\n\n`;
65
+ toml += `[deploy]\nport = ${port}\n`;
66
+ if (start) toml += `start_command = "${start}"\n`;
67
+ toml += `\n[resources]\ncpu = 1\nmemory = 512\n`;
68
+
69
+ const tomlPath = path.join(process.cwd(), "lizard.toml");
70
+ if (!fs.existsSync(tomlPath)) {
71
+ fs.writeFileSync(tomlPath, toml);
72
+ }
73
+ }
74
+
75
+ export function registerInit(program: Command) {
76
+ program
77
+ .command("init")
78
+ .description("Create a new project and link current directory")
79
+ .option("--name <name>", "Project name")
80
+ .action(async (opts) => {
81
+ // Check if already initialized
82
+ const existing = findProjectConfig();
83
+ if (existing) {
84
+ throw new Error(
85
+ "Already initialized. Run `lizard link` to change project.",
86
+ );
87
+ }
88
+
89
+ // Detect framework
90
+ const framework = detectFramework();
91
+ if (framework && !isJSONMode()) {
92
+ info(`Detected framework: ${chalk.cyan(framework.name)}`);
93
+ }
94
+
95
+ // Get project name
96
+ let projectName = opts.name;
97
+ if (!projectName) {
98
+ if (!isTTY()) {
99
+ throw new Error("--name is required in non-interactive mode");
100
+ }
101
+ const result = await p.text({
102
+ message: "Project name",
103
+ defaultValue: path.basename(process.cwd()),
104
+ placeholder: path.basename(process.cwd()),
105
+ });
106
+ if (p.isCancel(result)) process.exit(5);
107
+ projectName = result as string;
108
+ }
109
+
110
+ // Create project via API
111
+ const project = await api.post<Project>("/api/projects", {
112
+ name: projectName,
113
+ });
114
+
115
+ // Save local config
116
+ saveProjectConfig({
117
+ projectId: project.id,
118
+ projectName: project.name,
119
+ });
120
+
121
+ // Write lizard.toml
122
+ writeLizardToml(framework);
123
+
124
+ if (isJSONMode()) {
125
+ printJSON({
126
+ projectId: project.id,
127
+ name: project.name,
128
+ framework: framework?.name,
129
+ });
130
+ } else {
131
+ success(`Project "${chalk.bold(project.name)}" created`);
132
+ info(chalk.dim(" Linked to current directory"));
133
+ info(chalk.dim(" Config saved to .lizard/config.json"));
134
+ if (framework) {
135
+ info(chalk.dim(` lizard.toml created for ${framework.name}`));
136
+ }
137
+ }
138
+ });
139
+ }
@@ -0,0 +1,63 @@
1
+ import chalk from "chalk";
2
+ import * as p from "@clack/prompts";
3
+ import { Command } from "commander";
4
+ import { api } from "../lib/api.js";
5
+ import { saveProjectConfig, findProjectConfig } from "../lib/config.js";
6
+ import { success, isJSONMode, printJSON, isTTY } from "../lib/format.js";
7
+
8
+ interface Project {
9
+ id: string;
10
+ name: string;
11
+ slug: string;
12
+ }
13
+
14
+ export function registerLink(program: Command) {
15
+ program
16
+ .command("link")
17
+ .description("Link current directory to an existing project")
18
+ .option("--project <id>", "Project ID")
19
+ .action(async (opts) => {
20
+ let projectId = opts.project;
21
+ let projectName: string | undefined;
22
+
23
+ if (!projectId) {
24
+ // Fetch projects and let user pick
25
+ const projects = await api.get<Project[]>("/api/projects");
26
+ if (projects.length === 0) {
27
+ throw new Error("No projects found. Run `lizard init` to create one.");
28
+ }
29
+
30
+ if (!isTTY()) {
31
+ throw new Error(
32
+ "Use --project <id> in non-interactive mode. Available: " +
33
+ projects.map((p) => `${p.name} (${p.id})`).join(", "),
34
+ );
35
+ }
36
+
37
+ const selected = await p.select({
38
+ message: "Select project",
39
+ options: projects.map((proj) => ({
40
+ value: proj.id,
41
+ label: proj.name,
42
+ hint: proj.id,
43
+ })),
44
+ });
45
+ if (p.isCancel(selected)) process.exit(5);
46
+ projectId = selected as string;
47
+ projectName = projects.find((p) => p.id === projectId)?.name;
48
+ }
49
+
50
+ const old = findProjectConfig();
51
+ saveProjectConfig({ projectId, projectName });
52
+
53
+ if (isJSONMode()) {
54
+ printJSON({ projectId, projectName });
55
+ } else if (old?.projectId && old.projectId !== projectId) {
56
+ success(
57
+ `Relinked to ${chalk.bold(projectName || projectId)} (was ${old.projectName || old.projectId})`,
58
+ );
59
+ } else {
60
+ success(`Linked to ${chalk.bold(projectName || projectId)}`);
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,158 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { Command } from "commander";
4
+ import {
5
+ saveCredentials,
6
+ openURL,
7
+ type Credentials,
8
+ } from "../lib/auth.js";
9
+ import { getBaseURL } from "../lib/api.js";
10
+ import { success, info, isJSONMode, printJSON } from "../lib/format.js";
11
+
12
+ const POLL_INTERVAL = 2000;
13
+ const SESSION_TIMEOUT = 300_000; // 5 min
14
+
15
+ interface SessionResponse {
16
+ sessionId: string;
17
+ sessionSecret: string;
18
+ expiresIn: number;
19
+ }
20
+
21
+ interface PollResponse {
22
+ status: "pending" | "complete" | "expired";
23
+ accessToken?: string;
24
+ refreshToken?: string;
25
+ user?: {
26
+ id: string;
27
+ username: string;
28
+ email?: string;
29
+ avatarUrl?: string;
30
+ };
31
+ }
32
+
33
+ /** Create a CLI login session on the server */
34
+ async function createSession(): Promise<SessionResponse> {
35
+ const res = await fetch(`${getBaseURL()}/api/auth/cli/session`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ });
39
+ if (!res.ok) {
40
+ throw new Error(`Failed to create login session: ${res.statusText}`);
41
+ }
42
+ return res.json() as Promise<SessionResponse>;
43
+ }
44
+
45
+ /** Poll the server to check if the user completed login */
46
+ async function pollSession(
47
+ sessionId: string,
48
+ sessionSecret: string,
49
+ ): Promise<PollResponse> {
50
+ const res = await fetch(`${getBaseURL()}/api/auth/cli/poll`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ sessionId, sessionSecret }),
54
+ });
55
+ if (!res.ok) {
56
+ throw new Error(`Poll failed: ${res.statusText}`);
57
+ }
58
+ return res.json() as Promise<PollResponse>;
59
+ }
60
+
61
+ /**
62
+ * Perform the login flow. Used by the login command and by auto-login in requireAuth.
63
+ */
64
+ export async function performLogin(): Promise<Credentials> {
65
+ // 1. Create session
66
+ const session = await createSession();
67
+ const loginURL = `${getBaseURL()}/auth/cli?session=${session.sessionId}`;
68
+
69
+ // 2. Try to open browser
70
+ const opened = await openURL(loginURL);
71
+ if (opened) {
72
+ info("Opening browser to log in...");
73
+ } else {
74
+ info(`Open this URL in your browser to log in:\n ${chalk.cyan(loginURL)}`);
75
+ }
76
+
77
+ // 3. Poll until complete
78
+ const spinner = ora("Waiting for login...").start();
79
+ const deadline = Date.now() + SESSION_TIMEOUT;
80
+
81
+ while (Date.now() < deadline) {
82
+ await sleep(POLL_INTERVAL);
83
+
84
+ try {
85
+ const result = await pollSession(session.sessionId, session.sessionSecret);
86
+
87
+ if (result.status === "complete" && result.accessToken && result.user) {
88
+ spinner.stop();
89
+ const creds: Credentials = {
90
+ accessToken: result.accessToken,
91
+ refreshToken: result.refreshToken,
92
+ userId: result.user.id,
93
+ username: result.user.username,
94
+ email: result.user.email,
95
+ avatarUrl: result.user.avatarUrl,
96
+ };
97
+ saveCredentials(creds);
98
+
99
+ if (isJSONMode()) {
100
+ printJSON({ username: creds.username, email: creds.email });
101
+ } else {
102
+ success(`Logged in as ${chalk.bold(creds.username)}`);
103
+ }
104
+ return creds;
105
+ }
106
+
107
+ if (result.status === "expired") {
108
+ spinner.stop();
109
+ throw new Error("Login session expired. Please try again.");
110
+ }
111
+ } catch (err: any) {
112
+ if (err.message?.includes("expired")) {
113
+ spinner.stop();
114
+ throw err;
115
+ }
116
+ // Network error — keep trying
117
+ }
118
+ }
119
+
120
+ spinner.stop();
121
+ throw new Error("Login timed out. Please try again.");
122
+ }
123
+
124
+ function sleep(ms: number): Promise<void> {
125
+ return new Promise((r) => setTimeout(r, ms));
126
+ }
127
+
128
+ export function registerLogin(program: Command) {
129
+ program
130
+ .command("login")
131
+ .description("Log in to Lizard")
132
+ .option("--token <token>", "Authenticate with an API token")
133
+ .action(async (opts) => {
134
+ if (opts.token) {
135
+ // Direct token auth — validate it
136
+ const res = await fetch(`${getBaseURL()}/api/auth/me`, {
137
+ headers: { Authorization: `Bearer ${opts.token}` },
138
+ });
139
+ if (!res.ok) throw new Error("Invalid token");
140
+ const user = (await res.json()) as any;
141
+ saveCredentials({
142
+ accessToken: opts.token,
143
+ userId: user.id,
144
+ username: user.username,
145
+ email: user.email,
146
+ avatarUrl: user.avatarUrl,
147
+ });
148
+ if (isJSONMode()) {
149
+ printJSON({ username: user.username });
150
+ } else {
151
+ success(`Logged in as ${chalk.bold(user.username)}`);
152
+ }
153
+ return;
154
+ }
155
+
156
+ await performLogin();
157
+ });
158
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ import { clearCredentials } from "../lib/auth.js";
3
+ import { success, isJSONMode, printJSON } from "../lib/format.js";
4
+
5
+ export function registerLogout(program: Command) {
6
+ program
7
+ .command("logout")
8
+ .description("Log out of Lizard")
9
+ .action(async () => {
10
+ clearCredentials();
11
+ if (isJSONMode()) {
12
+ printJSON({ status: "logged_out" });
13
+ } else {
14
+ success("Logged out");
15
+ }
16
+ });
17
+ }
@@ -0,0 +1,104 @@
1
+ import chalk from "chalk";
2
+ 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";
6
+
7
+ export function registerLogs(program: Command) {
8
+ program
9
+ .command("logs")
10
+ .description("Stream runtime logs")
11
+ .option("--build", "Show build logs instead of runtime")
12
+ .option("--service <id>", "Only show logs for a specific service")
13
+ .action(async (opts) => {
14
+ const projectId = resolveProjectId(program.opts().project);
15
+
16
+ if (opts.build) {
17
+ // Show build logs for the latest build
18
+ await showBuildLogs(opts.service, projectId);
19
+ return;
20
+ }
21
+
22
+ if (opts.service) {
23
+ // Stream logs for a specific app
24
+ info(chalk.dim("Streaming logs... (Ctrl+C to stop)\n"));
25
+ await streamSSE(`/api/apps/${opts.service}/logs`, (event, data) => {
26
+ if (event === "error") {
27
+ error(data);
28
+ return false;
29
+ }
30
+ printLogLine(data);
31
+ return true;
32
+ });
33
+ return;
34
+ }
35
+
36
+ // Stream all project logs
37
+ info(chalk.dim("Streaming project logs... (Ctrl+C to stop)\n"));
38
+ await streamSSE(
39
+ `/api/projects/${projectId}/logs/stream`,
40
+ (event, data) => {
41
+ if (event === "error") {
42
+ error(data);
43
+ return false;
44
+ }
45
+ printLogLine(data);
46
+ return true;
47
+ },
48
+ );
49
+ });
50
+ }
51
+
52
+ function printLogLine(data: string) {
53
+ 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
+ }
67
+ } catch {
68
+ process.stdout.write(data + "\n");
69
+ }
70
+ }
71
+
72
+ async function showBuildLogs(serviceId: string | undefined, projectId: string) {
73
+ let appId = serviceId;
74
+
75
+ if (!appId) {
76
+ // Get first app in project
77
+ const data = await api.get<{ apps: Array<{ id: string; name: string }> }>(
78
+ `/api/projects/${projectId}/services`,
79
+ );
80
+ if (!data.apps?.length) {
81
+ throw new Error("No apps in project");
82
+ }
83
+ appId = data.apps[0].id;
84
+ }
85
+
86
+ // Get latest build
87
+ const app = await api.get<{
88
+ builds?: Array<{ id: string; status: string }>;
89
+ }>(`/api/apps/${appId}`);
90
+ if (!app.builds?.length) {
91
+ throw new Error("No builds found");
92
+ }
93
+
94
+ const buildId = app.builds[0].id;
95
+ info(chalk.dim(`Build ${buildId}\n`));
96
+
97
+ await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
98
+ if (event === "done" || event === "error") {
99
+ return false;
100
+ }
101
+ printLogLine(data);
102
+ return true;
103
+ });
104
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ import open from "open";
3
+ import { resolveProjectId } from "../lib/config.js";
4
+ import { getBaseURL } from "../lib/api.js";
5
+ import { success } from "../lib/format.js";
6
+
7
+ export function registerOpen(program: Command) {
8
+ program
9
+ .command("open")
10
+ .description("Open project in browser")
11
+ .action(async () => {
12
+ const projectId = resolveProjectId(program.opts().project);
13
+ const url = `${getBaseURL()}/projects/${projectId}`;
14
+ await open(url);
15
+ success("Opened in browser");
16
+ });
17
+ }
@@ -0,0 +1,45 @@
1
+ import { Command } from "commander";
2
+ import { api } from "../lib/api.js";
3
+ import { isJSONMode, printJSON, table } from "../lib/format.js";
4
+
5
+ interface Project {
6
+ id: string;
7
+ name: string;
8
+ slug: string;
9
+ role: string;
10
+ memberCount: number;
11
+ createdAt: number;
12
+ }
13
+
14
+ export function registerProjects(program: Command) {
15
+ const proj = program
16
+ .command("project")
17
+ .description("Project management");
18
+
19
+ proj
20
+ .command("list")
21
+ .description("List all projects")
22
+ .action(async () => {
23
+ const projects = await api.get<Project[]>("/api/projects");
24
+
25
+ if (isJSONMode()) {
26
+ printJSON(projects);
27
+ return;
28
+ }
29
+
30
+ if (projects.length === 0) {
31
+ console.log("No projects. Run `lizard init` to create one.");
32
+ return;
33
+ }
34
+
35
+ table(
36
+ ["Name", "ID", "Role", "Members"],
37
+ projects.map((p) => [
38
+ p.name,
39
+ p.id,
40
+ p.role || "owner",
41
+ String(p.memberCount || 1),
42
+ ]),
43
+ );
44
+ });
45
+ }
@@ -0,0 +1,80 @@
1
+ import chalk from "chalk";
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
+ }
22
+
23
+ export function registerPs(program: Command) {
24
+ program
25
+ .command("ps")
26
+ .description("List all services in the project")
27
+ .action(async () => {
28
+ const projectId = resolveProjectId(program.opts().project);
29
+ const data = await api.get<{ apps: any[]; addons: any[] }>(
30
+ `/api/projects/${projectId}/services`,
31
+ );
32
+
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
+ });
44
+ }
45
+
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
+ }
57
+
58
+ if (isJSONMode()) {
59
+ printJSON(services);
60
+ return;
61
+ }
62
+
63
+ if (services.length === 0) {
64
+ console.log("No services. Use `lizard add` or `lizard deploy`.");
65
+ return;
66
+ }
67
+
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
+ );
79
+ });
80
+ }
@@ -0,0 +1,74 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { Command } from "commander";
4
+ import { api, streamSSE } from "../lib/api.js";
5
+ import { success, info, error, isJSONMode, printJSON } from "../lib/format.js";
6
+
7
+ export function registerRedeploy(program: Command) {
8
+ program
9
+ .command("redeploy")
10
+ .argument("<id>", "Service ID to redeploy")
11
+ .description("Redeploy a service from latest build with current secrets")
12
+ .option("--detach", "Run in background")
13
+ .action(async (id: string, opts) => {
14
+ const spinner = ora("Starting redeploy...").start();
15
+
16
+ await api.post(`/api/apps/${id}/redeploy`);
17
+
18
+ spinner.stop();
19
+
20
+ if (opts.detach || isJSONMode()) {
21
+ if (isJSONMode()) {
22
+ printJSON({ id, status: "deploying" });
23
+ } else {
24
+ success("Redeploy started");
25
+ info(chalk.dim(` Check status: lizard deploy-status ${id}`));
26
+ }
27
+ return;
28
+ }
29
+
30
+ // Stream build logs if not detached
31
+ info("Redeploying...");
32
+
33
+ // Poll for build
34
+ let buildId: string | null = null;
35
+ for (let i = 0; i < 30; i++) {
36
+ await new Promise((r) => setTimeout(r, 2000));
37
+ try {
38
+ const app = await api.get<{ builds?: Array<{ id: string; status: string }> }>(
39
+ `/api/apps/${id}`,
40
+ );
41
+ if (app.builds?.length) {
42
+ const latest = app.builds[0];
43
+ if (["building", "deploying", "running", "failed"].includes(latest.status)) {
44
+ buildId = latest.id;
45
+ break;
46
+ }
47
+ }
48
+ } catch {}
49
+ }
50
+
51
+ if (buildId) {
52
+ await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
53
+ if (event === "done" || event === "error") {
54
+ if (event === "error") error(`Build failed: ${data}`);
55
+ return false;
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(data);
59
+ process.stdout.write((parsed.line || data) + "\n");
60
+ } catch {
61
+ process.stdout.write(data + "\n");
62
+ }
63
+ return true;
64
+ });
65
+ }
66
+
67
+ const app = await api.get<{ status: string; domain?: string }>(`/api/apps/${id}`);
68
+ if (app.status === "running") {
69
+ success(`Redeployed! ${app.domain ? chalk.cyan(`https://${app.domain}`) : ""}`);
70
+ } else {
71
+ error("Redeploy failed");
72
+ }
73
+ });
74
+ }