@lizard-build/cli 0.1.0 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/.github/workflows/release.yml +90 -0
  2. package/README.md +41 -0
  3. package/dist/commands/add.js +318 -45
  4. package/dist/commands/add.js.map +1 -1
  5. package/dist/commands/config.d.ts +2 -0
  6. package/dist/commands/config.js +68 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/docs.d.ts +2 -0
  9. package/dist/commands/docs.js +13 -0
  10. package/dist/commands/docs.js.map +1 -0
  11. package/dist/commands/domain.d.ts +9 -0
  12. package/dist/commands/domain.js +195 -0
  13. package/dist/commands/domain.js.map +1 -0
  14. package/dist/commands/git.js +175 -36
  15. package/dist/commands/git.js.map +1 -1
  16. package/dist/commands/init.d.ts +24 -0
  17. package/dist/commands/init.js +128 -86
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/link.d.ts +7 -0
  20. package/dist/commands/link.js +104 -33
  21. package/dist/commands/link.js.map +1 -1
  22. package/dist/commands/login.js +4 -3
  23. package/dist/commands/login.js.map +1 -1
  24. package/dist/commands/logs.js +223 -30
  25. package/dist/commands/logs.js.map +1 -1
  26. package/dist/commands/open.js +3 -2
  27. package/dist/commands/open.js.map +1 -1
  28. package/dist/commands/port.d.ts +7 -0
  29. package/dist/commands/port.js +49 -0
  30. package/dist/commands/port.js.map +1 -0
  31. package/dist/commands/projects.js +36 -6
  32. package/dist/commands/projects.js.map +1 -1
  33. package/dist/commands/ps.js +32 -39
  34. package/dist/commands/ps.js.map +1 -1
  35. package/dist/commands/redeploy.js +48 -8
  36. package/dist/commands/redeploy.js.map +1 -1
  37. package/dist/commands/regions.js +2 -5
  38. package/dist/commands/regions.js.map +1 -1
  39. package/dist/commands/restart.js +84 -10
  40. package/dist/commands/restart.js.map +1 -1
  41. package/dist/commands/run.d.ts +9 -0
  42. package/dist/commands/run.js +61 -22
  43. package/dist/commands/run.js.map +1 -1
  44. package/dist/commands/scale.d.ts +10 -0
  45. package/dist/commands/scale.js +166 -0
  46. package/dist/commands/scale.js.map +1 -0
  47. package/dist/commands/secrets.js +200 -89
  48. package/dist/commands/secrets.js.map +1 -1
  49. package/dist/commands/service-set.d.ts +49 -0
  50. package/dist/commands/service-set.js +552 -0
  51. package/dist/commands/service-set.js.map +1 -0
  52. package/dist/commands/service-show.d.ts +11 -0
  53. package/dist/commands/service-show.js +44 -0
  54. package/dist/commands/service-show.js.map +1 -0
  55. package/dist/commands/service.d.ts +8 -0
  56. package/dist/commands/service.js +262 -0
  57. package/dist/commands/service.js.map +1 -0
  58. package/dist/commands/ssh.d.ts +2 -0
  59. package/dist/commands/ssh.js +161 -0
  60. package/dist/commands/ssh.js.map +1 -0
  61. package/dist/commands/status.d.ts +7 -0
  62. package/dist/commands/status.js +49 -38
  63. package/dist/commands/status.js.map +1 -1
  64. package/dist/commands/unlink.d.ts +5 -0
  65. package/dist/commands/unlink.js +18 -0
  66. package/dist/commands/unlink.js.map +1 -0
  67. package/dist/commands/up.d.ts +9 -0
  68. package/dist/commands/up.js +417 -0
  69. package/dist/commands/up.js.map +1 -0
  70. package/dist/commands/upgrade.d.ts +2 -0
  71. package/dist/commands/upgrade.js +79 -0
  72. package/dist/commands/upgrade.js.map +1 -0
  73. package/dist/commands/whoami.js +26 -6
  74. package/dist/commands/whoami.js.map +1 -1
  75. package/dist/commands/workspace.d.ts +8 -0
  76. package/dist/commands/workspace.js +36 -0
  77. package/dist/commands/workspace.js.map +1 -0
  78. package/dist/index.js +204 -82
  79. package/dist/index.js.map +1 -1
  80. package/dist/lib/api.d.ts +17 -2
  81. package/dist/lib/api.js +85 -51
  82. package/dist/lib/api.js.map +1 -1
  83. package/dist/lib/auth.d.ts +3 -11
  84. package/dist/lib/auth.js +16 -36
  85. package/dist/lib/auth.js.map +1 -1
  86. package/dist/lib/config.d.ts +36 -15
  87. package/dist/lib/config.js +71 -58
  88. package/dist/lib/config.js.map +1 -1
  89. package/dist/lib/format.d.ts +1 -0
  90. package/dist/lib/format.js +17 -4
  91. package/dist/lib/format.js.map +1 -1
  92. package/dist/lib/name.d.ts +11 -0
  93. package/dist/lib/name.js +26 -0
  94. package/dist/lib/name.js.map +1 -0
  95. package/dist/lib/picker.d.ts +32 -0
  96. package/dist/lib/picker.js +91 -0
  97. package/dist/lib/picker.js.map +1 -0
  98. package/dist/lib/resolve.d.ts +85 -0
  99. package/dist/lib/resolve.js +203 -0
  100. package/dist/lib/resolve.js.map +1 -0
  101. package/dist/lib/updater.d.ts +16 -0
  102. package/dist/lib/updater.js +102 -0
  103. package/dist/lib/updater.js.map +1 -0
  104. package/lizard-wrapper.sh +2 -0
  105. package/package.json +11 -3
  106. package/src/commands/add.ts +388 -56
  107. package/src/commands/config.ts +80 -0
  108. package/src/commands/docs.ts +15 -0
  109. package/src/commands/domain.ts +248 -0
  110. package/src/commands/git.ts +201 -40
  111. package/src/commands/init.ts +149 -100
  112. package/src/commands/link.ts +127 -35
  113. package/src/commands/login.ts +4 -3
  114. package/src/commands/logs.ts +283 -27
  115. package/src/commands/open.ts +3 -2
  116. package/src/commands/port.ts +57 -0
  117. package/src/commands/projects.ts +43 -6
  118. package/src/commands/ps.ts +39 -60
  119. package/src/commands/redeploy.ts +51 -10
  120. package/src/commands/regions.ts +2 -6
  121. package/src/commands/restart.ts +84 -10
  122. package/src/commands/run.ts +68 -24
  123. package/src/commands/scale.ts +216 -0
  124. package/src/commands/secrets.ts +277 -100
  125. package/src/commands/service-set.ts +669 -0
  126. package/src/commands/service-show.ts +52 -0
  127. package/src/commands/service.ts +298 -0
  128. package/src/commands/ssh.ts +176 -0
  129. package/src/commands/status.ts +51 -46
  130. package/src/commands/unlink.ts +17 -0
  131. package/src/commands/up.ts +461 -0
  132. package/src/commands/upgrade.ts +87 -0
  133. package/src/commands/whoami.ts +34 -6
  134. package/src/commands/workspace.ts +44 -0
  135. package/src/index.ts +214 -85
  136. package/src/lib/api.ts +114 -51
  137. package/src/lib/auth.ts +22 -46
  138. package/src/lib/config.ts +100 -65
  139. package/src/lib/format.ts +18 -4
  140. package/src/lib/name.ts +27 -0
  141. package/src/lib/picker.ts +133 -0
  142. package/src/lib/resolve.ts +285 -0
  143. package/src/lib/updater.ts +106 -0
  144. package/test/cli.test.ts +491 -0
  145. package/test/fixtures/hello-app/Dockerfile +5 -0
  146. package/test/fixtures/hello-app/index.js +5 -0
  147. package/test/unit/api.test.ts +66 -0
  148. package/test/unit/config.test.ts +94 -0
  149. package/test/unit/init.test.ts +211 -0
  150. package/test/unit/json.test.ts +208 -0
  151. package/test/unit/picker.test.ts +161 -0
  152. package/test/unit/resolve.test.ts +124 -0
  153. package/test/unit/service-set.test.ts +355 -0
  154. package/vitest.config.ts +10 -0
  155. package/dist/commands/connect.d.ts +0 -2
  156. package/dist/commands/connect.js +0 -117
  157. package/dist/commands/connect.js.map +0 -1
  158. package/dist/commands/context.d.ts +0 -2
  159. package/dist/commands/context.js +0 -71
  160. package/dist/commands/context.js.map +0 -1
  161. package/dist/commands/deploy.d.ts +0 -2
  162. package/dist/commands/deploy.js +0 -120
  163. package/dist/commands/deploy.js.map +0 -1
  164. package/dist/commands/destroy.d.ts +0 -2
  165. package/dist/commands/destroy.js +0 -51
  166. package/dist/commands/destroy.js.map +0 -1
  167. package/dist/commands/update.d.ts +0 -2
  168. package/dist/commands/update.js +0 -41
  169. package/dist/commands/update.js.map +0 -1
  170. package/dist/commands/version.d.ts +0 -2
  171. package/dist/commands/version.js +0 -37
  172. package/dist/commands/version.js.map +0 -1
  173. package/src/commands/connect.ts +0 -145
  174. package/src/commands/context.ts +0 -93
  175. package/src/commands/deploy.ts +0 -153
  176. package/src/commands/destroy.ts +0 -51
  177. package/src/commands/update.ts +0 -44
  178. package/src/commands/version.ts +0 -37
@@ -0,0 +1,285 @@
1
+ import { api, withScope, type ResourceScope } from "./api.js";
2
+ import {
3
+ getProjectLink,
4
+ resolveProjectId,
5
+ updateProjectLink,
6
+ } from "./config.js";
7
+
8
+ /**
9
+ * Build a ResourceScope for an arbitrary project. When the project matches the
10
+ * cwd link, reuse its workspaceId (no extra round-trip). Otherwise fetch the
11
+ * project's workspace once so cross-project commands (`-p other-project`) still
12
+ * get `?workspaceId=…` on the request — without it the backend can 404 a
13
+ * project the user reaches via workspace membership.
14
+ */
15
+ export async function scopeForProject(projectId: string): Promise<ResourceScope> {
16
+ const link = getProjectLink();
17
+ if (link?.projectId === projectId && link.workspaceId) {
18
+ return { workspaceId: link.workspaceId };
19
+ }
20
+ const fetched = await lookupProjectWorkspace(projectId);
21
+ return { workspaceId: fetched?.workspaceId ?? null };
22
+ }
23
+
24
+ interface AppLite {
25
+ id: string;
26
+ name: string;
27
+ status?: string;
28
+ }
29
+
30
+ interface AddonLite {
31
+ id: string;
32
+ name: string;
33
+ addonType?: string;
34
+ status?: string;
35
+ }
36
+
37
+ interface ServicesResponse {
38
+ apps?: AppLite[];
39
+ addons?: AddonLite[];
40
+ }
41
+
42
+ /**
43
+ * Resolve a service (app or addon) within a project. Match by ID or name.
44
+ * Throws with a helpful list of available services when not found.
45
+ */
46
+ export async function resolveService(
47
+ projectId: string,
48
+ nameOrId: string,
49
+ ): Promise<{ id: string; name: string; kind: "app" | "addon" }> {
50
+ const data = await api.get<ServicesResponse>(
51
+ withScope(`/api/projects/${projectId}/services`, await scopeForProject(projectId)),
52
+ );
53
+
54
+ const apps = data.apps || [];
55
+ const addons = data.addons || [];
56
+
57
+ const lower = nameOrId.toLowerCase();
58
+
59
+ const app = apps.find(
60
+ (a) => a.id.toLowerCase() === lower || a.name?.toLowerCase() === lower,
61
+ );
62
+ if (app) return { id: app.id, name: app.name, kind: "app" };
63
+
64
+ const addon = addons.find(
65
+ (a) =>
66
+ a.id.toLowerCase() === lower ||
67
+ a.name?.toLowerCase() === lower ||
68
+ a.addonType?.toLowerCase() === lower,
69
+ );
70
+ if (addon) {
71
+ return { id: addon.id, name: addon.name || addon.addonType || "", kind: "addon" };
72
+ }
73
+
74
+ const available = [
75
+ ...apps.map((a) => a.name),
76
+ ...addons.map((a) => a.name || a.addonType),
77
+ ].filter(Boolean);
78
+
79
+ throw new Error(
80
+ `Service "${nameOrId}" not found in project. ` +
81
+ (available.length ? `Available: ${available.join(", ")}` : "No services exist."),
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Pick the active service for a command:
87
+ * 1. --service flag (resolve by name/id)
88
+ * 2. linked service in cwd
89
+ * 3. throw with hint to pass --service
90
+ */
91
+ export async function getActiveService(
92
+ serviceFlag: string | undefined,
93
+ projectId: string,
94
+ ): Promise<{ id: string; name: string }> {
95
+ if (serviceFlag) {
96
+ const resolved = await resolveService(projectId, serviceFlag);
97
+ return { id: resolved.id, name: resolved.name };
98
+ }
99
+
100
+ const link = getProjectLink();
101
+ if (link?.serviceId) {
102
+ return {
103
+ id: link.serviceId,
104
+ name: link.serviceName || link.serviceId,
105
+ };
106
+ }
107
+
108
+ throw new Error(
109
+ "No service specified. Pass --service <name> or run `lizard service link <name>`.",
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Same as `getActiveService`, but also returns whether the target is an app
115
+ * or an addon. Costs one extra `/services` lookup when the service is taken
116
+ * from the cwd link (which otherwise resolves locally), so use this only when
117
+ * the caller branches on kind (e.g. `lizard scale`).
118
+ */
119
+ export async function getActiveServiceWithKind(
120
+ serviceFlag: string | undefined,
121
+ projectId: string,
122
+ ): Promise<{ id: string; name: string; kind: "app" | "addon" }> {
123
+ if (serviceFlag) {
124
+ return resolveService(projectId, serviceFlag);
125
+ }
126
+
127
+ const link = getProjectLink();
128
+ if (link?.serviceId) {
129
+ const data = await api.get<ServicesResponse>(
130
+ withScope(`/api/projects/${projectId}/services`, await scopeForProject(projectId)),
131
+ );
132
+ const app = data.apps?.find((a) => a.id === link.serviceId);
133
+ if (app) return { id: app.id, name: app.name, kind: "app" };
134
+ const addon = data.addons?.find((a) => a.id === link.serviceId);
135
+ if (addon) {
136
+ return {
137
+ id: addon.id,
138
+ name: addon.name || addon.addonType || link.serviceName || link.serviceId,
139
+ kind: "addon",
140
+ };
141
+ }
142
+ throw new Error(
143
+ `Linked service "${link.serviceName || link.serviceId}" no longer exists in this project.`,
144
+ );
145
+ }
146
+
147
+ throw new Error(
148
+ "No service specified. Pass --service <name> or run `lizard service link <name>`.",
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Look up the workspace id for a project. Used to lazy-fill legacy links
154
+ * that were saved before workspaces existed. Returns null if the project
155
+ * isn't accessible to the current user.
156
+ */
157
+ export async function lookupProjectWorkspace(
158
+ projectId: string,
159
+ ): Promise<{ workspaceId?: string | null; workspaceName?: string | null } | null> {
160
+ try {
161
+ const proj = await api.get<{
162
+ workspaceId?: string | null;
163
+ workspaceName?: string | null;
164
+ }>(`/api/projects/${projectId}`);
165
+ return {
166
+ workspaceId: proj.workspaceId ?? null,
167
+ workspaceName: proj.workspaceName ?? null,
168
+ };
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Resolve a project flag → `{ projectId, scope }`. Scope carries
176
+ * workspaceId for `withScope(url, scope)` queries.
177
+ *
178
+ * For the linked project, lazy-fills missing workspaceId into the cwd link.
179
+ * For a different project (`-p other-project`), fetches the target's
180
+ * workspace so the scope reflects the target — not whatever the cwd link
181
+ * happened to be pointing at.
182
+ */
183
+ export async function resolveProjectScope(
184
+ projectFlag?: string,
185
+ ): Promise<{ projectId: string; scope: ResourceScope }> {
186
+ const projectId = await resolveProjectId(projectFlag);
187
+ const link = getProjectLink();
188
+
189
+ // Same project as the cwd link — reuse cached workspaceId, lazy-fill if missing.
190
+ if (link?.projectId === projectId) {
191
+ let workspaceId: string | null | undefined = link.workspaceId ?? null;
192
+ let workspaceName: string | undefined = link.workspaceName;
193
+ if (!workspaceId) {
194
+ const fetched = await lookupProjectWorkspace(projectId);
195
+ if (fetched?.workspaceId) {
196
+ workspaceId = fetched.workspaceId;
197
+ workspaceName = fetched.workspaceName ?? undefined;
198
+ try {
199
+ updateProjectLink({ workspaceId, workspaceName });
200
+ } catch {}
201
+ }
202
+ }
203
+ return { projectId, scope: { workspaceId: workspaceId ?? null } };
204
+ }
205
+
206
+ // Different project — never reuse the link's workspaceId. Look up the
207
+ // target project's workspace directly. Mirrors `scopeForProject`.
208
+ const scope = await scopeForProject(projectId);
209
+ return { projectId, scope };
210
+ }
211
+
212
+ export interface ResolvedContext {
213
+ projectId: string;
214
+ workspaceId?: string;
215
+ workspaceName?: string;
216
+ service?: { id: string; name: string };
217
+ }
218
+
219
+ /** Build the scope object for `withScope(url, scope)` API calls. */
220
+ export function getScope(ctx: ResolvedContext): ResourceScope {
221
+ return {
222
+ workspaceId: ctx.workspaceId ?? null,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Convenience: resolve project + active service in one go.
228
+ *
229
+ * Lazily backfills `workspaceId` into the cwd link when missing (legacy
230
+ * configs written before workspaces existed). Once filled, subsequent
231
+ * commands get the scope param for free.
232
+ */
233
+ export async function resolveContext(opts: {
234
+ projectFlag?: string;
235
+ serviceFlag?: string;
236
+ workspaceFlag?: string;
237
+ requireService?: boolean;
238
+ }): Promise<ResolvedContext> {
239
+ const projectId = await resolveProjectId(opts.projectFlag);
240
+
241
+ let service: { id: string; name: string } | undefined;
242
+ const link = getProjectLink();
243
+ if (opts.serviceFlag || opts.requireService) {
244
+ service = await getActiveService(opts.serviceFlag, projectId);
245
+ } else if (link?.serviceId) {
246
+ service = {
247
+ id: link.serviceId,
248
+ name: link.serviceName || link.serviceId,
249
+ };
250
+ }
251
+
252
+ // Workspace resolution: only reuse the link's workspaceId when it
253
+ // describes the same project. For cross-project commands we fetch the
254
+ // target project's workspace so the scope is correct — using the link's
255
+ // ws here would scope into the wrong workspace.
256
+ let workspaceId: string | undefined;
257
+ let workspaceName: string | undefined;
258
+ if (link?.projectId === projectId) {
259
+ workspaceId = link.workspaceId;
260
+ workspaceName = link.workspaceName;
261
+ if (!workspaceId) {
262
+ const fetched = await lookupProjectWorkspace(projectId);
263
+ if (fetched?.workspaceId) {
264
+ workspaceId = fetched.workspaceId;
265
+ workspaceName = fetched.workspaceName ?? undefined;
266
+ try {
267
+ updateProjectLink({ workspaceId, workspaceName });
268
+ } catch {
269
+ // Non-fatal: link may not exist for this cwd (e.g. --project flag).
270
+ }
271
+ }
272
+ }
273
+ } else {
274
+ const fetched = await lookupProjectWorkspace(projectId);
275
+ workspaceId = fetched?.workspaceId ?? undefined;
276
+ workspaceName = fetched?.workspaceName ?? undefined;
277
+ }
278
+
279
+ return {
280
+ projectId,
281
+ workspaceId,
282
+ workspaceName,
283
+ service,
284
+ };
285
+ }
@@ -0,0 +1,106 @@
1
+ import { createWriteStream, existsSync, renameSync, chmodSync } from "node:fs";
2
+ import { pipeline } from "node:stream/promises";
3
+ import { Readable } from "node:stream";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+
8
+ export const CURRENT_VERSION = "0.3.29";
9
+ const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
10
+ const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
11
+
12
+ function getBinaryName(): string | null {
13
+ const os = process.platform;
14
+ const arch = process.arch;
15
+ if (os === "darwin" && arch === "arm64") return "lizard-darwin-arm64";
16
+ if (os === "darwin" && arch === "x64") return "lizard-darwin-x64";
17
+ if (os === "linux" && arch === "x64") return "lizard-linux-x64";
18
+ if (os === "linux" && arch === "arm64") return "lizard-linux-arm64";
19
+ return null;
20
+ }
21
+
22
+ export type LatestVersionResult =
23
+ | { kind: "ok"; version: string }
24
+ | { kind: "rate-limited"; resetAt: number }
25
+ | { kind: "error" };
26
+
27
+ export async function getLatestVersion(): Promise<LatestVersionResult> {
28
+ try {
29
+ const res = await fetch(RELEASES_API, {
30
+ headers: { "User-Agent": "lizard-cli" },
31
+ signal: AbortSignal.timeout(5000),
32
+ });
33
+ if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
34
+ const reset = Number(res.headers.get("x-ratelimit-reset"));
35
+ return { kind: "rate-limited", resetAt: Number.isFinite(reset) ? reset : 0 };
36
+ }
37
+ if (!res.ok) return { kind: "error" };
38
+ const data = (await res.json()) as { tag_name?: string };
39
+ const version = data.tag_name?.replace(/^v/, "");
40
+ return version ? { kind: "ok", version } : { kind: "error" };
41
+ } catch {
42
+ return { kind: "error" };
43
+ }
44
+ }
45
+
46
+ export async function selfUpdate(onProgress?: (msg: string) => void): Promise<boolean> {
47
+ const binaryName = getBinaryName();
48
+ if (!binaryName) return false;
49
+
50
+ // Find current executable path
51
+ const currentBin = process.execPath;
52
+ if (!existsSync(currentBin)) return false;
53
+
54
+ const url = `${RELEASE_BASE}/${binaryName}`;
55
+ const tmp = join(tmpdir(), `lizard-update-${Date.now()}`);
56
+
57
+ onProgress?.(`Downloading ${binaryName}...`);
58
+
59
+ const res = await fetch(url, { signal: AbortSignal.timeout(60000) });
60
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
61
+
62
+ const writer = createWriteStream(tmp);
63
+ await pipeline(Readable.fromWeb(res.body as any), writer);
64
+ chmodSync(tmp, 0o755);
65
+
66
+ onProgress?.("Installing...");
67
+ renameSync(tmp, currentBin);
68
+ return true;
69
+ }
70
+
71
+ /** Check for a newer version and auto-install it in the background.
72
+ * Prints a one-line notice on exit — either "Updated to vX.Y.Z" or nothing on failure.
73
+ * Never blocks or crashes the current command. */
74
+ export function checkForUpdateInBackground(): void {
75
+ // Only auto-update in TTY; skip CI / piped output
76
+ if (!process.stdout.isTTY) return;
77
+
78
+ let updateMessage: string | null = null;
79
+
80
+ const promise = getLatestVersion().then(async (r) => {
81
+ if (r.kind !== "ok") return;
82
+ const latest = r.version;
83
+ if (latest === CURRENT_VERSION) return;
84
+ const [maj, min, pat] = latest.split(".").map(Number);
85
+ const [cmaj, cmin, cpat] = CURRENT_VERSION.split(".").map(Number);
86
+ const isNewer = maj > cmaj || (maj === cmaj && min > cmin) || (maj === cmaj && min === cmin && pat > cpat);
87
+ if (!isNewer) return;
88
+ try {
89
+ const ok = await selfUpdate();
90
+ if (ok) {
91
+ updateMessage =
92
+ `\n Updating lizard v${CURRENT_VERSION} → v${latest}...\n` +
93
+ ` lizard updated to v${latest}\n`;
94
+ }
95
+ } catch {
96
+ // silent — don't interrupt the current command
97
+ }
98
+ }).catch(() => {});
99
+
100
+ process.on("exit", () => {
101
+ if (updateMessage) process.stderr.write(updateMessage);
102
+ });
103
+
104
+ // Don't block process exit
105
+ if (typeof (promise as any).unref === "function") (promise as any).unref();
106
+ }