@lizard-build/cli 0.3.38 → 0.3.40
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/dist/commands/add.js +2 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/git.js +7 -6
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/login.js +5 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.js +4 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/redeploy.js +8 -6
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/secrets.js +5 -0
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/ssh.js +24 -18
- package/dist/commands/ssh.js.map +1 -1
- package/dist/commands/up.js +26 -16
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/upgrade.js +20 -1
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +8 -2
- package/dist/lib/api.js +28 -5
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +8 -1
- package/dist/lib/auth.js +33 -3
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config.js +14 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/updater.d.ts +22 -4
- package/dist/lib/updater.js +137 -49
- package/dist/lib/updater.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/add.ts +2 -2
- package/src/commands/git.ts +11 -6
- package/src/commands/login.ts +5 -0
- package/src/commands/logs.ts +12 -5
- package/src/commands/redeploy.ts +12 -6
- package/src/commands/secrets.ts +8 -0
- package/src/commands/ssh.ts +24 -25
- package/src/commands/up.ts +26 -17
- package/src/commands/upgrade.ts +21 -1
- package/src/index.ts +12 -1
- package/src/lib/api.ts +25 -4
- package/src/lib/auth.ts +33 -3
- package/src/lib/config.ts +13 -2
- package/src/lib/updater.ts +130 -45
- package/test/unit/config.test.ts +29 -0
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import { setJSONMode, isJSONMode, error } from "./lib/format.js";
|
|
6
6
|
import { requireAuth, isLoggedIn } from "./lib/auth.js";
|
|
7
7
|
import { setBaseURL, setAccessToken, APIError } from "./lib/api.js";
|
|
8
|
-
import { checkForUpdateInBackground, CURRENT_VERSION } from "./lib/updater.js";
|
|
8
|
+
import { checkForUpdateInBackground, runBackgroundUpdate, CURRENT_VERSION } from "./lib/updater.js";
|
|
9
9
|
|
|
10
10
|
const BANNER = chalk.rgb(16, 185, 129)(
|
|
11
11
|
[
|
|
@@ -223,6 +223,13 @@ function dumpCommand(cmd: Command): any {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
async function main() {
|
|
226
|
+
// Hidden entry point: detached child spawned by checkForUpdateInBackground.
|
|
227
|
+
// Handled before commander so it never shows up in help or telemetry.
|
|
228
|
+
if (process.argv.includes("__lizard-update")) {
|
|
229
|
+
await runBackgroundUpdate();
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
226
233
|
// Set JSON mode from argv *before* parseAsync so the catch block below
|
|
227
234
|
// honors --json even when commander rejects before our preAction hook
|
|
228
235
|
// fires (e.g. unknown command, malformed global flag). Non-TTY auto-mode
|
|
@@ -280,10 +287,14 @@ async function main() {
|
|
|
280
287
|
);
|
|
281
288
|
} else {
|
|
282
289
|
error(msg);
|
|
290
|
+
if (status === 401) {
|
|
291
|
+
process.stderr.write("Run `lizard login` to re-authenticate.\n");
|
|
292
|
+
}
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
// Exit codes derived from APIError.status (or tagged error codes), not message text
|
|
286
296
|
const isAuth = status === 401 || status === 403 || code === "NOT_AUTHENTICATED";
|
|
297
|
+
|
|
287
298
|
const isNotFound = status === 404;
|
|
288
299
|
const isTimeout =
|
|
289
300
|
status === 408 ||
|
package/src/lib/api.ts
CHANGED
|
@@ -129,10 +129,15 @@ export const api = {
|
|
|
129
129
|
delete: <T = any>(path: string) => request<T>("DELETE", path),
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
/** Stream SSE and call handler for each data line. Return false to stop.
|
|
132
|
+
/** Stream SSE and call handler for each data line. Return false to stop.
|
|
133
|
+
*
|
|
134
|
+
* `opts.idleTimeoutMs` — stop (resolve) when no *event* arrives for that
|
|
135
|
+
* long. Heartbeat comments don't reset the timer. Used by `--tail`-style
|
|
136
|
+
* snapshot reads that must not follow a live stream forever. */
|
|
133
137
|
export function streamSSE(
|
|
134
138
|
path: string,
|
|
135
139
|
handler: (event: string, data: string) => boolean | void,
|
|
140
|
+
opts: { idleTimeoutMs?: number } = {},
|
|
136
141
|
): Promise<void> {
|
|
137
142
|
return new Promise((resolve, reject) => {
|
|
138
143
|
const url = new URL(baseURL + path);
|
|
@@ -155,6 +160,19 @@ export function streamSSE(
|
|
|
155
160
|
return;
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
164
|
+
const finish = () => {
|
|
165
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
166
|
+
req.destroy();
|
|
167
|
+
resolve();
|
|
168
|
+
};
|
|
169
|
+
const armIdleTimer = () => {
|
|
170
|
+
if (!opts.idleTimeoutMs) return;
|
|
171
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
172
|
+
idleTimer = setTimeout(finish, opts.idleTimeoutMs);
|
|
173
|
+
};
|
|
174
|
+
armIdleTimer();
|
|
175
|
+
|
|
158
176
|
let buffer = "";
|
|
159
177
|
let currentEvent = "";
|
|
160
178
|
let currentData = "";
|
|
@@ -169,10 +187,10 @@ export function streamSSE(
|
|
|
169
187
|
const trimmed = line.replace(/\r$/, "");
|
|
170
188
|
if (trimmed === "") {
|
|
171
189
|
if (currentData) {
|
|
190
|
+
armIdleTimer();
|
|
172
191
|
const cont = handler(currentEvent, currentData);
|
|
173
192
|
if (cont === false) {
|
|
174
|
-
|
|
175
|
-
resolve();
|
|
193
|
+
finish();
|
|
176
194
|
return;
|
|
177
195
|
}
|
|
178
196
|
}
|
|
@@ -186,7 +204,10 @@ export function streamSSE(
|
|
|
186
204
|
}
|
|
187
205
|
});
|
|
188
206
|
|
|
189
|
-
res.on("end",
|
|
207
|
+
res.on("end", () => {
|
|
208
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
209
|
+
resolve();
|
|
210
|
+
});
|
|
190
211
|
res.on("error", reject);
|
|
191
212
|
},
|
|
192
213
|
);
|
package/src/lib/auth.ts
CHANGED
|
@@ -38,7 +38,32 @@ function isTTY(): boolean {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* Expiry of a JWT in epoch-ms, decoded from the `exp` claim. Returns null
|
|
42
|
+
* for opaque/undecodable tokens — those are treated as valid and left for
|
|
43
|
+
* the server to reject.
|
|
44
|
+
*/
|
|
45
|
+
export function jwtExpiryMs(token: string): number | null {
|
|
46
|
+
try {
|
|
47
|
+
const payload = token.split(".")[1];
|
|
48
|
+
if (!payload) return null;
|
|
49
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
50
|
+
return typeof decoded.exp === "number" ? decoded.exp * 1000 : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isExpired(creds: Credentials): boolean {
|
|
57
|
+
const expMs =
|
|
58
|
+
jwtExpiryMs(creds.accessToken) ??
|
|
59
|
+
(creds.expiresAt ? Date.parse(creds.expiresAt) : null);
|
|
60
|
+
if (expMs === null || Number.isNaN(expMs)) return false;
|
|
61
|
+
return Date.now() > expMs - 60_000; // 60s margin
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensure the user is authenticated. If not logged in (or the saved token
|
|
66
|
+
* has expired — there is no refresh endpoint) and TTY, auto-login.
|
|
42
67
|
* Returns credentials or throws.
|
|
43
68
|
*/
|
|
44
69
|
export async function requireAuth(): Promise<Credentials> {
|
|
@@ -51,16 +76,21 @@ export async function requireAuth(): Promise<Credentials> {
|
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
const creds = loadCredentials();
|
|
54
|
-
if (creds) return creds;
|
|
79
|
+
if (creds && !isExpired(creds)) return creds;
|
|
55
80
|
|
|
56
81
|
if (!isTTY()) {
|
|
57
82
|
const err = new Error(
|
|
58
|
-
|
|
83
|
+
creds
|
|
84
|
+
? "Session expired. Run `lizard login` again or set LIZARD_TOKEN."
|
|
85
|
+
: "Not authenticated. Set LIZARD_TOKEN or run `lizard login` first.",
|
|
59
86
|
) as Error & { code: string };
|
|
60
87
|
err.code = "NOT_AUTHENTICATED";
|
|
61
88
|
throw err;
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
if (creds) {
|
|
92
|
+
process.stderr.write("Session expired — logging in again...\n");
|
|
93
|
+
}
|
|
64
94
|
const { performLogin } = await import("../commands/login.js");
|
|
65
95
|
return performLogin();
|
|
66
96
|
}
|
package/src/lib/config.ts
CHANGED
|
@@ -96,7 +96,13 @@ export function updateProjectLink(
|
|
|
96
96
|
) {
|
|
97
97
|
const existing = getProjectLink(cwd);
|
|
98
98
|
if (!existing) return;
|
|
99
|
-
|
|
99
|
+
const merged = { ...existing, ...patch };
|
|
100
|
+
// An explicit serviceId/serviceName in the patch (including `undefined`,
|
|
101
|
+
// i.e. "clear it") must override the legacy mirror too — otherwise
|
|
102
|
+
// setProjectLink resurrects the old value from appId/appName.
|
|
103
|
+
if ("serviceId" in patch) merged.appId = patch.serviceId;
|
|
104
|
+
if ("serviceName" in patch) merged.appName = patch.serviceName;
|
|
105
|
+
setProjectLink(merged, cwd);
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
export function clearProjectLink(cwd: string = process.cwd()) {
|
|
@@ -120,8 +126,13 @@ export async function resolveProjectId(flagValue?: string): Promise<string> {
|
|
|
120
126
|
}
|
|
121
127
|
const { api } = await import("./api.js");
|
|
122
128
|
const projects = await api.get<Array<{ id: string; name: string; slug: string }>>("/api/projects");
|
|
129
|
+
// Case-insensitive, matching `lizard link` / `lizard init` behaviour.
|
|
130
|
+
const lower = flagValue.toLowerCase();
|
|
123
131
|
const match = projects.find(
|
|
124
|
-
(p) =>
|
|
132
|
+
(p) =>
|
|
133
|
+
p.id.toLowerCase() === lower ||
|
|
134
|
+
p.slug?.toLowerCase() === lower ||
|
|
135
|
+
p.name?.toLowerCase() === lower,
|
|
125
136
|
);
|
|
126
137
|
if (!match) throw new Error(`Project "${flagValue}" not found.`);
|
|
127
138
|
return match.id;
|
package/src/lib/updater.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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 {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
export const CURRENT_VERSION = "0.3.
|
|
8
|
+
export const CURRENT_VERSION = "0.3.40";
|
|
9
9
|
const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
|
|
10
10
|
const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
|
|
11
11
|
|
|
12
|
+
/** Minimum gap between background update checks. */
|
|
13
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
14
|
+
|
|
12
15
|
function getBinaryName(): string | null {
|
|
13
16
|
const os = process.platform;
|
|
14
17
|
const arch = process.arch;
|
|
@@ -19,6 +22,23 @@ function getBinaryName(): string | null {
|
|
|
19
22
|
return null;
|
|
20
23
|
}
|
|
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(): boolean {
|
|
31
|
+
return typeof (globalThis as any).Bun !== "undefined";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stateDir(): string {
|
|
35
|
+
return process.env.LIZARD_HOME
|
|
36
|
+
? join(process.env.LIZARD_HOME, ".lizard")
|
|
37
|
+
: join(os.homedir(), ".lizard");
|
|
38
|
+
}
|
|
39
|
+
const checkStampFile = () => join(stateDir(), "update-check.json");
|
|
40
|
+
const updateNoticeFile = () => join(stateDir(), "update-notice.json");
|
|
41
|
+
|
|
22
42
|
export type LatestVersionResult =
|
|
23
43
|
| { kind: "ok"; version: string }
|
|
24
44
|
| { kind: "rate-limited"; resetAt: number }
|
|
@@ -43,64 +63,129 @@ export async function getLatestVersion(): Promise<LatestVersionResult> {
|
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
|
|
66
|
+
export function isNewerVersion(latest: string, current: string): boolean {
|
|
67
|
+
const [maj, min, pat] = latest.split(".").map(Number);
|
|
68
|
+
const [cmaj, cmin, cpat] = current.split(".").map(Number);
|
|
69
|
+
if (![maj, min, pat, cmaj, cmin, cpat].every(Number.isFinite)) return false;
|
|
70
|
+
return maj > cmaj || (maj === cmaj && min > cmin) || (maj === cmaj && min === cmin && pat > cpat);
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
export async function selfUpdate(onProgress?: (msg: string) => void): Promise<boolean> {
|
|
47
74
|
const binaryName = getBinaryName();
|
|
48
75
|
if (!binaryName) return false;
|
|
49
76
|
|
|
50
|
-
//
|
|
77
|
+
// Refuse to replace anything that isn't the standalone lizard binary —
|
|
78
|
+
// under npm the execPath is the user's node executable.
|
|
79
|
+
if (!isStandaloneBinary()) return false;
|
|
80
|
+
|
|
51
81
|
const currentBin = process.execPath;
|
|
52
82
|
if (!existsSync(currentBin)) return false;
|
|
53
83
|
|
|
54
84
|
const url = `${RELEASE_BASE}/${binaryName}`;
|
|
55
|
-
|
|
85
|
+
// Download next to the target binary: rename() must stay on one filesystem
|
|
86
|
+
// (tmpdir is often tmpfs on Linux → EXDEV).
|
|
87
|
+
const tmp = join(dirname(currentBin), `.lizard-update-${process.pid}`);
|
|
56
88
|
|
|
57
89
|
onProgress?.(`Downloading ${binaryName}...`);
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(60000) });
|
|
93
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
94
|
+
|
|
95
|
+
const writer = createWriteStream(tmp);
|
|
96
|
+
await pipeline(Readable.fromWeb(res.body as any), writer);
|
|
97
|
+
chmodSync(tmp, 0o755);
|
|
98
|
+
|
|
99
|
+
onProgress?.("Installing...");
|
|
100
|
+
renameSync(tmp, currentBin);
|
|
101
|
+
return true;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
try { unlinkSync(tmp); } catch {}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readJSON(file: string): any {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
61
115
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
116
|
+
function writeJSON(file: string, data: unknown) {
|
|
117
|
+
try {
|
|
118
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
119
|
+
writeFileSync(file, JSON.stringify(data));
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
65
122
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return true;
|
|
123
|
+
function autoUpdateDisabled(): boolean {
|
|
124
|
+
return Boolean(process.env.LIZARD_NO_UPDATE || process.env.CI);
|
|
69
125
|
}
|
|
70
126
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
127
|
+
/** Print (once) the notice left behind by a completed background update. */
|
|
128
|
+
function flushUpdateNotice(): void {
|
|
129
|
+
const notice = readJSON(updateNoticeFile());
|
|
130
|
+
if (!notice?.to) return;
|
|
131
|
+
try { unlinkSync(updateNoticeFile()); } catch {}
|
|
132
|
+
// We are already running the replaced binary, so notice.to should match.
|
|
133
|
+
if (notice.to === CURRENT_VERSION && notice.from !== CURRENT_VERSION) {
|
|
134
|
+
process.stderr.write(` lizard auto-updated: v${notice.from} → v${notice.to}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Kick off an update check without delaying the current command.
|
|
140
|
+
*
|
|
141
|
+
* The check+download runs in a *detached child process* (`lizard
|
|
142
|
+
* __lizard-update`): an in-process fetch would keep the event loop alive and
|
|
143
|
+
* make every command linger until GitHub answers. Checks are throttled via a
|
|
144
|
+
* stamp file (6h), disabled with LIZARD_NO_UPDATE/CI, and only run for the
|
|
145
|
+
* standalone binary — npm installs upgrade through npm.
|
|
146
|
+
*/
|
|
74
147
|
export function checkForUpdateInBackground(): void {
|
|
75
|
-
// Only auto-update in TTY; skip CI / piped output
|
|
76
148
|
if (!process.stdout.isTTY) return;
|
|
149
|
+
if (autoUpdateDisabled()) return;
|
|
77
150
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(() => {});
|
|
151
|
+
flushUpdateNotice();
|
|
152
|
+
|
|
153
|
+
if (!isStandaloneBinary()) return;
|
|
99
154
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
155
|
+
const stamp = readJSON(checkStampFile());
|
|
156
|
+
if (stamp?.lastCheckAt && Date.now() - stamp.lastCheckAt < CHECK_INTERVAL_MS) return;
|
|
157
|
+
// Stamp before spawning so parallel commands don't pile up children.
|
|
158
|
+
writeJSON(checkStampFile(), { lastCheckAt: Date.now(), lastVersion: CURRENT_VERSION });
|
|
103
159
|
|
|
104
|
-
|
|
105
|
-
|
|
160
|
+
try {
|
|
161
|
+
const child = spawn(process.execPath, ["__lizard-update"], {
|
|
162
|
+
detached: true,
|
|
163
|
+
stdio: "ignore",
|
|
164
|
+
});
|
|
165
|
+
child.unref();
|
|
166
|
+
} catch {
|
|
167
|
+
// never break the actual command over an update check
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Body of the hidden `__lizard-update` command: check the latest release and
|
|
173
|
+
* install it, leaving a notice file for the next foreground run.
|
|
174
|
+
*/
|
|
175
|
+
export async function runBackgroundUpdate(): Promise<void> {
|
|
176
|
+
if (autoUpdateDisabled() || !isStandaloneBinary()) return;
|
|
177
|
+
|
|
178
|
+
const r = await getLatestVersion();
|
|
179
|
+
writeJSON(checkStampFile(), { lastCheckAt: Date.now(), lastVersion: r.kind === "ok" ? r.version : CURRENT_VERSION });
|
|
180
|
+
if (r.kind !== "ok") return;
|
|
181
|
+
if (!isNewerVersion(r.version, CURRENT_VERSION)) return;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const ok = await selfUpdate();
|
|
185
|
+
if (ok) {
|
|
186
|
+
writeJSON(updateNoticeFile(), { from: CURRENT_VERSION, to: r.version, at: Date.now() });
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// silent — retried after the next throttle window
|
|
190
|
+
}
|
|
106
191
|
}
|
package/test/unit/config.test.ts
CHANGED
|
@@ -75,6 +75,35 @@ describe("ProjectLink schema", () => {
|
|
|
75
75
|
expect(got?.workspaceName).toBe("filled");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("updateProjectLink with explicit undefined clears the service (no appId resurrection)", () => {
|
|
79
|
+
setProjectLink(
|
|
80
|
+
{ projectId: "proj_1", serviceId: "svc_1", serviceName: "api" },
|
|
81
|
+
tmpDir,
|
|
82
|
+
);
|
|
83
|
+
// Simulates `lizard service delete` of the linked service.
|
|
84
|
+
updateProjectLink({ serviceId: undefined, serviceName: undefined }, tmpDir);
|
|
85
|
+
|
|
86
|
+
const got = getProjectLink(tmpDir);
|
|
87
|
+
expect(got?.projectId).toBe("proj_1");
|
|
88
|
+
expect(got?.serviceId).toBeUndefined();
|
|
89
|
+
expect(got?.serviceName).toBeUndefined();
|
|
90
|
+
expect(got?.appId).toBeUndefined();
|
|
91
|
+
expect(got?.appName).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("updateProjectLink with a new serviceId also updates the legacy mirror", () => {
|
|
95
|
+
setProjectLink(
|
|
96
|
+
{ projectId: "proj_1", serviceId: "svc_old", serviceName: "old" },
|
|
97
|
+
tmpDir,
|
|
98
|
+
);
|
|
99
|
+
updateProjectLink({ serviceId: "svc_new", serviceName: "new" }, tmpDir);
|
|
100
|
+
|
|
101
|
+
const got = getProjectLink(tmpDir);
|
|
102
|
+
expect(got?.serviceId).toBe("svc_new");
|
|
103
|
+
expect(got?.appId).toBe("svc_new");
|
|
104
|
+
expect(got?.appName).toBe("new");
|
|
105
|
+
});
|
|
106
|
+
|
|
78
107
|
test("config.json without workspaceId still loads (legacy compat)", () => {
|
|
79
108
|
const cfgFile = path.join(tmpDir, ".lizard", "config.json");
|
|
80
109
|
fs.mkdirSync(path.dirname(cfgFile), { recursive: true });
|