@lizard-build/cli 0.3.39 → 0.3.41

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 (44) hide show
  1. package/dist/commands/git.js +7 -6
  2. package/dist/commands/git.js.map +1 -1
  3. package/dist/commands/login.js +5 -1
  4. package/dist/commands/login.js.map +1 -1
  5. package/dist/commands/logs.js +9 -2
  6. package/dist/commands/logs.js.map +1 -1
  7. package/dist/commands/redeploy.js +8 -6
  8. package/dist/commands/redeploy.js.map +1 -1
  9. package/dist/commands/secrets.js +5 -0
  10. package/dist/commands/secrets.js.map +1 -1
  11. package/dist/commands/ssh.js +24 -18
  12. package/dist/commands/ssh.js.map +1 -1
  13. package/dist/commands/up.js +26 -16
  14. package/dist/commands/up.js.map +1 -1
  15. package/dist/commands/upgrade.js +20 -1
  16. package/dist/commands/upgrade.js.map +1 -1
  17. package/dist/index.js +10 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib/api.d.ts +8 -2
  20. package/dist/lib/api.js +28 -5
  21. package/dist/lib/api.js.map +1 -1
  22. package/dist/lib/auth.d.ts +8 -1
  23. package/dist/lib/auth.js +33 -3
  24. package/dist/lib/auth.js.map +1 -1
  25. package/dist/lib/config.js +14 -2
  26. package/dist/lib/config.js.map +1 -1
  27. package/dist/lib/updater.d.ts +22 -4
  28. package/dist/lib/updater.js +137 -49
  29. package/dist/lib/updater.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/commands/git.ts +11 -6
  32. package/src/commands/login.ts +5 -0
  33. package/src/commands/logs.ts +17 -6
  34. package/src/commands/redeploy.ts +12 -6
  35. package/src/commands/secrets.ts +8 -0
  36. package/src/commands/ssh.ts +24 -25
  37. package/src/commands/up.ts +26 -17
  38. package/src/commands/upgrade.ts +21 -1
  39. package/src/index.ts +12 -1
  40. package/src/lib/api.ts +25 -4
  41. package/src/lib/auth.ts +33 -3
  42. package/src/lib/config.ts +13 -2
  43. package/src/lib/updater.ts +130 -45
  44. package/test/unit/config.test.ts +29 -0
@@ -1,11 +1,14 @@
1
- import { createWriteStream, existsSync, renameSync, chmodSync } from "node:fs";
1
+ import { createWriteStream, existsSync, renameSync, chmodSync, unlinkSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { pipeline } from "node:stream/promises";
3
3
  import { Readable } from "node:stream";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- export const CURRENT_VERSION = "0.3.39";
4
+ import { join, dirname } from "node:path";
5
+ import os from "node:os";
6
+ import { spawn } from "node:child_process";
7
+ export const CURRENT_VERSION = "0.3.41";
7
8
  const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
8
9
  const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
10
+ /** Minimum gap between background update checks. */
11
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
9
12
  function getBinaryName() {
10
13
  const os = process.platform;
11
14
  const arch = process.arch;
@@ -19,6 +22,21 @@ function getBinaryName() {
19
22
  return "lizard-linux-arm64";
20
23
  return null;
21
24
  }
25
+ /**
26
+ * True only when running as the Bun-compiled standalone binary. Under
27
+ * npm/node, `process.execPath` is the *node* executable — self-update would
28
+ * overwrite the user's Node.js install with the lizard binary.
29
+ */
30
+ export function isStandaloneBinary() {
31
+ return typeof globalThis.Bun !== "undefined";
32
+ }
33
+ function stateDir() {
34
+ return process.env.LIZARD_HOME
35
+ ? join(process.env.LIZARD_HOME, ".lizard")
36
+ : join(os.homedir(), ".lizard");
37
+ }
38
+ const checkStampFile = () => join(stateDir(), "update-check.json");
39
+ const updateNoticeFile = () => join(stateDir(), "update-notice.json");
22
40
  export async function getLatestVersion() {
23
41
  try {
24
42
  const res = await fetch(RELEASES_API, {
@@ -39,64 +57,134 @@ export async function getLatestVersion() {
39
57
  return { kind: "error" };
40
58
  }
41
59
  }
60
+ export function isNewerVersion(latest, current) {
61
+ const [maj, min, pat] = latest.split(".").map(Number);
62
+ const [cmaj, cmin, cpat] = current.split(".").map(Number);
63
+ if (![maj, min, pat, cmaj, cmin, cpat].every(Number.isFinite))
64
+ return false;
65
+ return maj > cmaj || (maj === cmaj && min > cmin) || (maj === cmaj && min === cmin && pat > cpat);
66
+ }
42
67
  export async function selfUpdate(onProgress) {
43
68
  const binaryName = getBinaryName();
44
69
  if (!binaryName)
45
70
  return false;
46
- // Find current executable path
71
+ // Refuse to replace anything that isn't the standalone lizard binary —
72
+ // under npm the execPath is the user's node executable.
73
+ if (!isStandaloneBinary())
74
+ return false;
47
75
  const currentBin = process.execPath;
48
76
  if (!existsSync(currentBin))
49
77
  return false;
50
78
  const url = `${RELEASE_BASE}/${binaryName}`;
51
- const tmp = join(tmpdir(), `lizard-update-${Date.now()}`);
79
+ // Download next to the target binary: rename() must stay on one filesystem
80
+ // (tmpdir is often tmpfs on Linux → EXDEV).
81
+ const tmp = join(dirname(currentBin), `.lizard-update-${process.pid}`);
52
82
  onProgress?.(`Downloading ${binaryName}...`);
53
- const res = await fetch(url, { signal: AbortSignal.timeout(60000) });
54
- if (!res.ok)
55
- throw new Error(`Download failed: ${res.status}`);
56
- const writer = createWriteStream(tmp);
57
- await pipeline(Readable.fromWeb(res.body), writer);
58
- chmodSync(tmp, 0o755);
59
- onProgress?.("Installing...");
60
- renameSync(tmp, currentBin);
61
- return true;
83
+ try {
84
+ const res = await fetch(url, { signal: AbortSignal.timeout(60000) });
85
+ if (!res.ok)
86
+ throw new Error(`Download failed: ${res.status}`);
87
+ const writer = createWriteStream(tmp);
88
+ await pipeline(Readable.fromWeb(res.body), writer);
89
+ chmodSync(tmp, 0o755);
90
+ onProgress?.("Installing...");
91
+ renameSync(tmp, currentBin);
92
+ return true;
93
+ }
94
+ catch (err) {
95
+ try {
96
+ unlinkSync(tmp);
97
+ }
98
+ catch { }
99
+ throw err;
100
+ }
62
101
  }
63
- /** Check for a newer version and auto-install it in the background.
64
- * Prints a one-line notice on exit — either "Updated to vX.Y.Z" or nothing on failure.
65
- * Never blocks or crashes the current command. */
102
+ function readJSON(file) {
103
+ try {
104
+ return JSON.parse(readFileSync(file, "utf8"));
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ function writeJSON(file, data) {
111
+ try {
112
+ mkdirSync(stateDir(), { recursive: true });
113
+ writeFileSync(file, JSON.stringify(data));
114
+ }
115
+ catch { }
116
+ }
117
+ function autoUpdateDisabled() {
118
+ return Boolean(process.env.LIZARD_NO_UPDATE || process.env.CI);
119
+ }
120
+ /** Print (once) the notice left behind by a completed background update. */
121
+ function flushUpdateNotice() {
122
+ const notice = readJSON(updateNoticeFile());
123
+ if (!notice?.to)
124
+ return;
125
+ try {
126
+ unlinkSync(updateNoticeFile());
127
+ }
128
+ catch { }
129
+ // We are already running the replaced binary, so notice.to should match.
130
+ if (notice.to === CURRENT_VERSION && notice.from !== CURRENT_VERSION) {
131
+ process.stderr.write(` lizard auto-updated: v${notice.from} → v${notice.to}\n`);
132
+ }
133
+ }
134
+ /**
135
+ * Kick off an update check without delaying the current command.
136
+ *
137
+ * The check+download runs in a *detached child process* (`lizard
138
+ * __lizard-update`): an in-process fetch would keep the event loop alive and
139
+ * make every command linger until GitHub answers. Checks are throttled via a
140
+ * stamp file (6h), disabled with LIZARD_NO_UPDATE/CI, and only run for the
141
+ * standalone binary — npm installs upgrade through npm.
142
+ */
66
143
  export function checkForUpdateInBackground() {
67
- // Only auto-update in TTY; skip CI / piped output
68
144
  if (!process.stdout.isTTY)
69
145
  return;
70
- let updateMessage = null;
71
- const promise = getLatestVersion().then(async (r) => {
72
- if (r.kind !== "ok")
73
- return;
74
- const latest = r.version;
75
- if (latest === CURRENT_VERSION)
76
- return;
77
- const [maj, min, pat] = latest.split(".").map(Number);
78
- const [cmaj, cmin, cpat] = CURRENT_VERSION.split(".").map(Number);
79
- const isNewer = maj > cmaj || (maj === cmaj && min > cmin) || (maj === cmaj && min === cmin && pat > cpat);
80
- if (!isNewer)
81
- return;
82
- try {
83
- const ok = await selfUpdate();
84
- if (ok) {
85
- updateMessage =
86
- `\n Updating lizard v${CURRENT_VERSION} → v${latest}...\n` +
87
- ` lizard updated to v${latest}\n`;
88
- }
89
- }
90
- catch {
91
- // silent — don't interrupt the current command
146
+ if (autoUpdateDisabled())
147
+ return;
148
+ flushUpdateNotice();
149
+ if (!isStandaloneBinary())
150
+ return;
151
+ const stamp = readJSON(checkStampFile());
152
+ if (stamp?.lastCheckAt && Date.now() - stamp.lastCheckAt < CHECK_INTERVAL_MS)
153
+ return;
154
+ // Stamp before spawning so parallel commands don't pile up children.
155
+ writeJSON(checkStampFile(), { lastCheckAt: Date.now(), lastVersion: CURRENT_VERSION });
156
+ try {
157
+ const child = spawn(process.execPath, ["__lizard-update"], {
158
+ detached: true,
159
+ stdio: "ignore",
160
+ });
161
+ child.unref();
162
+ }
163
+ catch {
164
+ // never break the actual command over an update check
165
+ }
166
+ }
167
+ /**
168
+ * Body of the hidden `__lizard-update` command: check the latest release and
169
+ * install it, leaving a notice file for the next foreground run.
170
+ */
171
+ export async function runBackgroundUpdate() {
172
+ if (autoUpdateDisabled() || !isStandaloneBinary())
173
+ return;
174
+ const r = await getLatestVersion();
175
+ writeJSON(checkStampFile(), { lastCheckAt: Date.now(), lastVersion: r.kind === "ok" ? r.version : CURRENT_VERSION });
176
+ if (r.kind !== "ok")
177
+ return;
178
+ if (!isNewerVersion(r.version, CURRENT_VERSION))
179
+ return;
180
+ try {
181
+ const ok = await selfUpdate();
182
+ if (ok) {
183
+ writeJSON(updateNoticeFile(), { from: CURRENT_VERSION, to: r.version, at: Date.now() });
92
184
  }
93
- }).catch(() => { });
94
- process.on("exit", () => {
95
- if (updateMessage)
96
- process.stderr.write(updateMessage);
97
- });
98
- // Don't block process exit
99
- if (typeof promise.unref === "function")
100
- promise.unref();
185
+ }
186
+ catch {
187
+ // silent — retried after the next throttle window
188
+ }
101
189
  }
102
190
  //# sourceMappingURL=updater.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"updater.js","sourceRoot":"","sources":["../../src/lib/updater.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"updater.js","sourceRoot":"","sources":["../../src/lib/updater.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lizard-build/cli",
3
- "version": "0.3.39",
3
+ "version": "0.3.41",
4
4
  "description": "Lizard CLI — deploy and manage apps on Lizard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -112,22 +112,27 @@ export function registerGit(program: Command) {
112
112
  });
113
113
  spinner.succeed(`Branch set to ${chalk.cyan(branch)}`);
114
114
 
115
- // Trigger redeploy
115
+ // Trigger redeploy — the endpoint returns the freshly created Build
116
+ // record; using its id avoids racing against a previous build.
116
117
  const deploySpinner = ora("Starting redeploy...").start();
117
- await api.post(withScope(`/api/apps/${serviceId}/redeploy`, scope));
118
+ const build = await api.post<{ id?: string }>(
119
+ withScope(`/api/apps/${serviceId}/redeploy`, scope),
120
+ undefined,
121
+ { "X-Deploy-Source": "cli" },
122
+ );
118
123
  deploySpinner.stop();
119
124
 
120
125
  if (opts.detach || isJSONMode()) {
121
- if (isJSONMode()) printJSON({ id: serviceId, branch, status: "deploying" });
126
+ if (isJSONMode()) printJSON({ id: serviceId, buildId: build?.id, branch, status: "deploying" });
122
127
  else success(`Redeploy started on branch ${chalk.cyan(branch)}`);
123
128
  return;
124
129
  }
125
130
 
126
131
  info(`Redeploying ${chalk.bold(serviceName)} on ${chalk.cyan(branch)}...`);
127
132
 
128
- // Wait for build to appear
129
- let buildId: string | null = null;
130
- for (let i = 0; i < 30; i++) {
133
+ let buildId: string | null = build?.id ?? null;
134
+ // Fallback for older servers that respond without a Build record.
135
+ for (let i = 0; !buildId && i < 30; i++) {
131
136
  await new Promise((r) => setTimeout(r, 2000));
132
137
  try {
133
138
  const app = await api.get<{ builds?: Array<{ id: string; status: string }> }>(`/api/apps/${serviceId}`);
@@ -4,6 +4,7 @@ import { Command } from "commander";
4
4
  import {
5
5
  saveCredentials,
6
6
  openURL,
7
+ jwtExpiryMs,
7
8
  type Credentials,
8
9
  } from "../lib/auth.js";
9
10
  import { getBaseURL } from "../lib/api.js";
@@ -86,9 +87,11 @@ export async function performLogin(): Promise<Credentials> {
86
87
 
87
88
  if (result.status === "complete" && result.accessToken && result.user) {
88
89
  spinner.stop();
90
+ const expMs = jwtExpiryMs(result.accessToken);
89
91
  const creds: Credentials = {
90
92
  accessToken: result.accessToken,
91
93
  refreshToken: result.refreshToken,
94
+ expiresAt: expMs ? new Date(expMs).toISOString() : undefined,
92
95
  userId: result.user.id,
93
96
  username: result.user.username,
94
97
  email: result.user.email,
@@ -139,8 +142,10 @@ export function registerLogin(program: Command) {
139
142
  });
140
143
  if (!res.ok) throw new Error("Invalid token");
141
144
  const user = (await res.json()) as any;
145
+ const expMs = jwtExpiryMs(token);
142
146
  saveCredentials({
143
147
  accessToken: token,
148
+ expiresAt: expMs ? new Date(expMs).toISOString() : undefined,
144
149
  userId: user.id,
145
150
  username: user.username,
146
151
  email: user.email,
@@ -49,18 +49,22 @@ export function registerLogs(program: Command) {
49
49
  // Resolve -s flag (may be name, slug, or ID) once up front so every
50
50
  // branch below talks to the API with a real service ID.
51
51
  let serviceId: string | undefined;
52
+ let serviceName: string | undefined;
52
53
  if (opts.service) {
53
54
  const svc = await resolveService(projectId, opts.service);
54
55
  serviceId = svc.id;
56
+ serviceName = svc.name;
55
57
  }
56
58
 
57
59
  // --tail: fetch historical logs and exit
58
60
  if (tailN !== undefined) {
61
+ // The project log stream tags entries with the service *name*
62
+ // (not ID), so the historical filter must use the name too.
59
63
  const entries = await api.get<any[]>(
60
64
  withScope(
61
65
  withQuery(`/api/projects/${projectId}/logs`, {
62
66
  limit: tailN,
63
- service: serviceId,
67
+ service: serviceName,
64
68
  }),
65
69
  scope,
66
70
  ),
@@ -347,12 +351,19 @@ async function showBuildLogs(
347
351
  info(chalk.dim(`Build ${buildId}\n`));
348
352
 
349
353
  if (tailN !== undefined) {
354
+ // Snapshot semantics: the server replays history immediately; if the
355
+ // build is still running the stream would otherwise follow it forever.
356
+ // Stop after 3s without new events and print what we have.
350
357
  const lines: string[] = [];
351
- await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
352
- if (event === "done" || event === "error") return false;
353
- lines.push(data);
354
- return true;
355
- });
358
+ await streamSSE(
359
+ `/api/builds/${buildId}/logs`,
360
+ (event, data) => {
361
+ if (event === "done" || event === "error") return false;
362
+ lines.push(data);
363
+ return true;
364
+ },
365
+ { idleTimeoutMs: 3000 },
366
+ );
356
367
  for (const line of lines.slice(-tailN)) printLogLine(line);
357
368
  return;
358
369
  }
@@ -54,24 +54,30 @@ export function registerRedeploy(program: Command) {
54
54
  }
55
55
 
56
56
  const spinner = ora("Starting redeploy...").start();
57
- await api.post(`/api/apps/${id}/redeploy`);
57
+ // The endpoint pre-creates and returns the Build record — use its id
58
+ // instead of polling builds[0], which races against a previous build.
59
+ const build = await api.post<{ id?: string; status?: string }>(
60
+ `/api/apps/${id}/redeploy`,
61
+ undefined,
62
+ { "X-Deploy-Source": "cli" },
63
+ );
58
64
  spinner.stop();
59
65
 
60
66
  if (opts.detach || isJSONMode()) {
61
67
  if (isJSONMode()) {
62
- printJSON({ id, status: "deploying" });
68
+ printJSON({ id, buildId: build?.id, status: "deploying" });
63
69
  } else {
64
70
  success("Redeploy started");
65
- info(chalk.dim(` Check status: lizard deploy status ${id}`));
71
+ info(chalk.dim(` Check status: lizard up status ${id}`));
66
72
  }
67
73
  return;
68
74
  }
69
75
 
70
76
  info("Redeploying...");
71
77
 
72
- // Poll for build
73
- let buildId: string | null = null;
74
- for (let i = 0; i < 30; i++) {
78
+ let buildId: string | null = build?.id ?? null;
79
+ // Fallback for older servers that respond without a Build record.
80
+ for (let i = 0; !buildId && i < 30; i++) {
75
81
  await new Promise((r) => setTimeout(r, 2000));
76
82
  try {
77
83
  const app = await api.get<{ builds?: Array<{ id: string; status: string }> }>(
@@ -278,6 +278,14 @@ Notes:
278
278
  .option("-s, --service <name>", "Service to scope to (overrides linked)")
279
279
  .option("-p, --project <id>", "Project to scope to")
280
280
  .action(async (opts, sub) => {
281
+ // Reading a TTY stdin waits for Ctrl+D forever — fail fast instead.
282
+ if (process.stdin.isTTY) {
283
+ throw new Error(
284
+ "secrets import reads KEY=value lines from stdin. Pipe a file:\n" +
285
+ " lizard secrets import < .env",
286
+ );
287
+ }
288
+
281
289
  const inherited = sub.parent?.opts() || {};
282
290
  const scope = await resolveScope(
283
291
  opts.project ?? inherited.project,
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import * as p from "@clack/prompts";
3
3
  import { Command } from "commander";
4
4
  import { api, getBaseURL, streamSSE, withScope } from "../lib/api.js";
5
- import { resolveProjectScope } from "../lib/resolve.js";
5
+ import { resolveProjectScope, resolveService } from "../lib/resolve.js";
6
6
  import { error, isTTY } from "../lib/format.js";
7
7
  import { getToken } from "../lib/auth.js";
8
8
  import * as https from "node:https";
@@ -22,10 +22,19 @@ Examples:
22
22
  lizard ssh -s my-app -- bash -c "ps aux | head"`)
23
23
  .action(async (cmdArgs: string[], opts) => {
24
24
  const { projectId, scope } = await resolveProjectScope(opts.project);
25
- let serviceId = opts.service as string | undefined;
26
-
27
- // Resolve service interactively if not given
28
- if (!serviceId) {
25
+ let serviceId: string | undefined;
26
+
27
+ if (opts.service) {
28
+ // Resolve by name or ID through the shared resolver — guessing
29
+ // "looks like an ID" by length breaks for long service names.
30
+ const svc = await resolveService(projectId, opts.service);
31
+ if (svc.kind !== "app") {
32
+ error(`"${svc.name}" is an addon — ssh works only for app services.`);
33
+ process.exit(1);
34
+ }
35
+ serviceId = svc.id;
36
+ } else {
37
+ // Resolve service interactively if not given
29
38
  const data = await api.get<{ apps: Array<{ id: string; name: string; status: string }> }>(
30
39
  withScope(`/api/projects/${projectId}/services`, scope),
31
40
  );
@@ -49,17 +58,6 @@ Examples:
49
58
  }
50
59
  }
51
60
 
52
- // Resolve service ID if a name was given (lookup by name)
53
- if (serviceId && !serviceId.match(/^[A-Za-z0-9_-]{20,}$/)) {
54
- const data = await api.get<{ apps: Array<{ id: string; name: string; serviceName?: string }> }>(
55
- withScope(`/api/projects/${projectId}/services`, scope),
56
- );
57
- const match = (data.apps || []).find(
58
- (a) => a.name === serviceId || a.serviceName === serviceId || a.id === serviceId,
59
- );
60
- if (match) serviceId = match.id;
61
- }
62
-
63
61
  if (cmdArgs.length === 0) {
64
62
  error("No command given. Usage: lizard ssh -s <service> -- <cmd> [args...]");
65
63
  process.exit(1);
@@ -71,29 +69,29 @@ Examples:
71
69
  const cmd = cmdArgs.map(shellQuote).join(" ");
72
70
  process.stdout.write(chalk.dim(`$ ${cmd}\n`));
73
71
 
74
- let exitCode = 0;
75
-
76
- await execStream(serviceId!, cmd, (stream, line) => {
72
+ const exitCode = await execStream(serviceId!, cmd, (stream, line) => {
77
73
  if (stream === "stderr") {
78
74
  process.stderr.write(line + "\n");
79
75
  } else {
80
76
  process.stdout.write(line + "\n");
81
77
  }
82
- }, (code) => {
83
- exitCode = code;
84
78
  });
85
79
 
86
80
  process.exit(exitCode);
87
81
  });
88
82
  }
89
83
 
84
+ /** Run a command on the VM, streaming output. Resolves with the exit code:
85
+ * the remote command's code from the `exit` event, or 1 when the server
86
+ * reported an `error` event without one. */
90
87
  function execStream(
91
88
  appId: string,
92
89
  cmd: string,
93
90
  onLine: (stream: string, line: string) => void,
94
- onExit: (code: number) => void,
95
- ): Promise<void> {
91
+ ): Promise<number> {
96
92
  return new Promise((resolve, reject) => {
93
+ let exitCode: number | null = null;
94
+ let sawError = false;
97
95
  const baseURL = getBaseURL();
98
96
  const url = new URL(`${baseURL}/api/apps/${appId}/exec`);
99
97
  const token = getToken();
@@ -141,8 +139,9 @@ function execStream(
141
139
  } else if (trimmed.startsWith("data:")) {
142
140
  const data = trimmed.slice(5).trimStart();
143
141
  if (currentEvent === "exit") {
144
- try { onExit(JSON.parse(data).exitCode ?? 0); } catch {}
142
+ try { exitCode = JSON.parse(data).exitCode ?? 0; } catch {}
145
143
  } else if (currentEvent === "error") {
144
+ sawError = true;
146
145
  error(data);
147
146
  } else {
148
147
  try {
@@ -156,7 +155,7 @@ function execStream(
156
155
  }
157
156
  });
158
157
 
159
- res.on("end", resolve);
158
+ res.on("end", () => resolve(exitCode ?? (sawError ? 1 : 0)));
160
159
  res.on("error", reject);
161
160
  },
162
161
  );
@@ -198,7 +198,7 @@ async function deployFromLocal(args: {
198
198
  : success(`Deploy started ${chalk.dim(`lizard up status ${newApp.id}`)}`);
199
199
  return;
200
200
  }
201
- await streamBuildLogs(newApp.id, args.opts.ci);
201
+ await streamBuildLogs(newApp.id, args.opts.ci, newApp.buildId);
202
202
  }
203
203
 
204
204
  // ── helpers ──────────────────────────────────────────────────────────────────
@@ -260,12 +260,16 @@ const EXCLUDE_DIRS = new Set([
260
260
  ".turbo",
261
261
  ".vercel",
262
262
  ]);
263
- const EXCLUDE_EXT = new Set([".pyc", ".pyo", ".log", ".DS_Store"]);
263
+ const EXCLUDE_EXT = new Set([".pyc", ".pyo", ".log"]);
264
+ // Matched by full name — `path.extname(".DS_Store")` is "" (dotfile), so
265
+ // extension matching never catches these.
266
+ const EXCLUDE_FILES = new Set([".DS_Store"]);
264
267
 
265
268
  function collectFilesManually(root: string, dir: string): string[] {
266
269
  const results: string[] = [];
267
270
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
268
271
  if (EXCLUDE_DIRS.has(entry.name)) continue;
272
+ if (EXCLUDE_FILES.has(entry.name)) continue;
269
273
  if (EXCLUDE_EXT.has(path.extname(entry.name))) continue;
270
274
  const full = path.join(dir, entry.name);
271
275
  if (entry.isDirectory()) results.push(...collectFilesManually(root, full));
@@ -324,23 +328,28 @@ function prompt(question: string): Promise<string> {
324
328
  });
325
329
  }
326
330
 
327
- async function streamBuildLogs(appId: string, ciMode: boolean = false) {
328
- const spinner = ora("Waiting for build...").start();
329
- let buildId: string | null = null;
330
- for (let i = 0; i < 30; i++) {
331
- await sleep(2000);
332
- try {
333
- const app = await api.get<App>(`/api/apps/${appId}`);
334
- if (app.builds?.length) {
335
- const latest = app.builds[0];
336
- if (["building", "deploying", "running", "failed"].includes(latest.status)) {
337
- buildId = latest.id;
338
- break;
331
+ async function streamBuildLogs(appId: string, ciMode: boolean = false, knownBuildId?: string) {
332
+ // Prefer the buildId returned by the upload/redeploy response — polling
333
+ // builds[0] races against a still-running previous build and can attach
334
+ // to the wrong one.
335
+ let buildId: string | null = knownBuildId ?? null;
336
+ if (!buildId) {
337
+ const spinner = ora("Waiting for build...").start();
338
+ for (let i = 0; i < 30; i++) {
339
+ await sleep(2000);
340
+ try {
341
+ const app = await api.get<App>(`/api/apps/${appId}`);
342
+ if (app.builds?.length) {
343
+ const latest = app.builds[0];
344
+ if (["building", "deploying", "running", "failed"].includes(latest.status)) {
345
+ buildId = latest.id;
346
+ break;
347
+ }
339
348
  }
340
- }
341
- } catch {}
349
+ } catch {}
350
+ }
351
+ spinner.stop();
342
352
  }
343
- spinner.stop();
344
353
  if (!buildId) {
345
354
  info(chalk.dim("No build found. Check `lizard up status <id>`."));
346
355
  return;
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { Command } from "commander";
3
3
  import { info, success, isJSONMode, printJSON } from "../lib/format.js";
4
- import { CURRENT_VERSION, getLatestVersion, selfUpdate } from "../lib/updater.js";
4
+ import { CURRENT_VERSION, getLatestVersion, selfUpdate, isStandaloneBinary } from "../lib/updater.js";
5
5
 
6
6
  export function registerUpgrade(program: Command) {
7
7
  program
@@ -67,6 +67,26 @@ export function registerUpgrade(program: Command) {
67
67
  return;
68
68
  }
69
69
 
70
+ // npm install — self-replacing process.execPath would overwrite the
71
+ // user's node binary. Point at npm instead.
72
+ if (!isStandaloneBinary()) {
73
+ if (isJSONMode()) {
74
+ printJSON({
75
+ currentVersion: CURRENT_VERSION,
76
+ latestVersion: latest,
77
+ updateAvailable: true,
78
+ upgraded: false,
79
+ method: "npm",
80
+ hint: "npm install -g @lizard-build/cli@latest",
81
+ });
82
+ return;
83
+ }
84
+ info(`Update available: v${CURRENT_VERSION} → ${chalk.green("v" + latest)}`);
85
+ info(`This copy was installed via npm. Upgrade with:`);
86
+ info(` ${chalk.cyan("npm install -g @lizard-build/cli@latest")}`);
87
+ return;
88
+ }
89
+
70
90
  info(`Upgrading v${CURRENT_VERSION} → ${chalk.green("v" + latest)}...`);
71
91
 
72
92
  try {