@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.
- package/.github/workflows/release.yml +90 -0
- package/README.md +41 -0
- package/dist/commands/add.js +318 -45
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +68 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +13 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/domain.d.ts +9 -0
- package/dist/commands/domain.js +195 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/git.js +175 -36
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +128 -86
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +7 -0
- package/dist/commands/link.js +104 -33
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.js +4 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.js +223 -30
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/open.js +3 -2
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/port.d.ts +7 -0
- package/dist/commands/port.js +49 -0
- package/dist/commands/port.js.map +1 -0
- package/dist/commands/projects.js +36 -6
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/ps.js +32 -39
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/redeploy.js +48 -8
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/regions.js +2 -5
- package/dist/commands/regions.js.map +1 -1
- package/dist/commands/restart.js +84 -10
- package/dist/commands/restart.js.map +1 -1
- package/dist/commands/run.d.ts +9 -0
- package/dist/commands/run.js +61 -22
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/scale.d.ts +10 -0
- package/dist/commands/scale.js +166 -0
- package/dist/commands/scale.js.map +1 -0
- package/dist/commands/secrets.js +200 -89
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/service-set.d.ts +49 -0
- package/dist/commands/service-set.js +552 -0
- package/dist/commands/service-set.js.map +1 -0
- package/dist/commands/service-show.d.ts +11 -0
- package/dist/commands/service-show.js +44 -0
- package/dist/commands/service-show.js.map +1 -0
- package/dist/commands/service.d.ts +8 -0
- package/dist/commands/service.js +262 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/ssh.d.ts +2 -0
- package/dist/commands/ssh.js +161 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +49 -38
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +5 -0
- package/dist/commands/unlink.js +18 -0
- package/dist/commands/unlink.js.map +1 -0
- package/dist/commands/up.d.ts +9 -0
- package/dist/commands/up.js +417 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +79 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/whoami.js +26 -6
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +36 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.js +204 -82
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +17 -2
- package/dist/lib/api.js +85 -51
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -11
- package/dist/lib/auth.js +16 -36
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config.d.ts +36 -15
- package/dist/lib/config.js +71 -58
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +17 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/name.d.ts +11 -0
- package/dist/lib/name.js +26 -0
- package/dist/lib/name.js.map +1 -0
- package/dist/lib/picker.d.ts +32 -0
- package/dist/lib/picker.js +91 -0
- package/dist/lib/picker.js.map +1 -0
- package/dist/lib/resolve.d.ts +85 -0
- package/dist/lib/resolve.js +203 -0
- package/dist/lib/resolve.js.map +1 -0
- package/dist/lib/updater.d.ts +16 -0
- package/dist/lib/updater.js +102 -0
- package/dist/lib/updater.js.map +1 -0
- package/lizard-wrapper.sh +2 -0
- package/package.json +11 -3
- package/src/commands/add.ts +388 -56
- package/src/commands/config.ts +80 -0
- package/src/commands/docs.ts +15 -0
- package/src/commands/domain.ts +248 -0
- package/src/commands/git.ts +201 -40
- package/src/commands/init.ts +149 -100
- package/src/commands/link.ts +127 -35
- package/src/commands/login.ts +4 -3
- package/src/commands/logs.ts +283 -27
- package/src/commands/open.ts +3 -2
- package/src/commands/port.ts +57 -0
- package/src/commands/projects.ts +43 -6
- package/src/commands/ps.ts +39 -60
- package/src/commands/redeploy.ts +51 -10
- package/src/commands/regions.ts +2 -6
- package/src/commands/restart.ts +84 -10
- package/src/commands/run.ts +68 -24
- package/src/commands/scale.ts +216 -0
- package/src/commands/secrets.ts +277 -100
- package/src/commands/service-set.ts +669 -0
- package/src/commands/service-show.ts +52 -0
- package/src/commands/service.ts +298 -0
- package/src/commands/ssh.ts +176 -0
- package/src/commands/status.ts +51 -46
- package/src/commands/unlink.ts +17 -0
- package/src/commands/up.ts +461 -0
- package/src/commands/upgrade.ts +87 -0
- package/src/commands/whoami.ts +34 -6
- package/src/commands/workspace.ts +44 -0
- package/src/index.ts +214 -85
- package/src/lib/api.ts +114 -51
- package/src/lib/auth.ts +22 -46
- package/src/lib/config.ts +100 -65
- package/src/lib/format.ts +18 -4
- package/src/lib/name.ts +27 -0
- package/src/lib/picker.ts +133 -0
- package/src/lib/resolve.ts +285 -0
- package/src/lib/updater.ts +106 -0
- package/test/cli.test.ts +491 -0
- package/test/fixtures/hello-app/Dockerfile +5 -0
- package/test/fixtures/hello-app/index.js +5 -0
- package/test/unit/api.test.ts +66 -0
- package/test/unit/config.test.ts +94 -0
- package/test/unit/init.test.ts +211 -0
- package/test/unit/json.test.ts +208 -0
- package/test/unit/picker.test.ts +161 -0
- package/test/unit/resolve.test.ts +124 -0
- package/test/unit/service-set.test.ts +355 -0
- package/vitest.config.ts +10 -0
- package/dist/commands/connect.d.ts +0 -2
- package/dist/commands/connect.js +0 -117
- package/dist/commands/connect.js.map +0 -1
- package/dist/commands/context.d.ts +0 -2
- package/dist/commands/context.js +0 -71
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -2
- package/dist/commands/deploy.js +0 -120
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/destroy.d.ts +0 -2
- package/dist/commands/destroy.js +0 -51
- package/dist/commands/destroy.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.js +0 -41
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/version.d.ts +0 -2
- package/dist/commands/version.js +0 -37
- package/dist/commands/version.js.map +0 -1
- package/src/commands/connect.ts +0 -145
- package/src/commands/context.ts +0 -93
- package/src/commands/deploy.ts +0 -153
- package/src/commands/destroy.ts +0 -51
- package/src/commands/update.ts +0 -44
- package/src/commands/version.ts +0 -37
package/src/commands/logs.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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/${
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
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(
|
|
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(
|
|
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;
|
package/src/commands/open.ts
CHANGED
|
@@ -8,8 +8,9 @@ export function registerOpen(program: Command) {
|
|
|
8
8
|
program
|
|
9
9
|
.command("open")
|
|
10
10
|
.description("Open project in browser")
|
|
11
|
-
.
|
|
12
|
-
|
|
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
|
+
}
|
package/src/commands/projects.ts
CHANGED
|
@@ -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
|
-
.
|
|
23
|
-
|
|
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", "
|
|
47
|
+
["Name", "Workspace", "Slug", "Role", "Members"],
|
|
37
48
|
projects.map((p) => [
|
|
38
49
|
p.name,
|
|
39
|
-
p.
|
|
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
|
}
|
package/src/commands/ps.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
.
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 (
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
}
|