@lizard-build/cli 0.3.39 → 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/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/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/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 });
|