@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/dist/lib/updater.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updater.js","sourceRoot":"","sources":["../../src/lib/updater.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"updater.js","sourceRoot":"","sources":["../../src/lib/updater.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACnI,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC;AACxC,MAAM,YAAY,GAAG,sEAAsE,CAAC;AAC5F,MAAM,YAAY,GAAG,qEAAqE,CAAC;AAE3F,oDAAoD;AACpD,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK;AAEnD,SAAS,aAAa;IACpB,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1B,IAAI,EAAE,KAAK,QAAQ,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,qBAAqB,CAAC;IACtE,IAAI,EAAE,KAAK,QAAQ,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,mBAAmB,CAAC;IAClE,IAAI,EAAE,KAAK,OAAO,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,kBAAkB,CAAC;IAChE,IAAI,EAAE,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,oBAAoB,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,OAAQ,UAAkB,CAAC,GAAG,KAAK,WAAW,CAAC;AACxD,CAAC;AAED,SAAS,QAAQ;IACf,OAAO,OAAO,CAAC,GAAG,CAAC,WAAW;QAC5B,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC;QAC1C,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;AACpC,CAAC;AACD,MAAM,cAAc,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,mBAAmB,CAAC,CAAC;AACnE,MAAM,gBAAgB,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,oBAAoB,CAAC,CAAC;AAOtE,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,YAAY,EAAE;YACpC,OAAO,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE;YACvC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC;YAC3D,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA0B,CAAC;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjD,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,OAAe;IAC5D,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1D,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5E,OAAO,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,GAAG,IAAI,CAAC,CAAC;AACpG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAkC;IACjE,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAE9B,uEAAuE;IACvE,wDAAwD;IACxD,IAAI,CAAC,kBAAkB,EAAE;QAAE,OAAO,KAAK,CAAC;IAExC,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAE1C,MAAM,GAAG,GAAG,GAAG,YAAY,IAAI,UAAU,EAAE,CAAC;IAC5C,2EAA2E;IAC3E,4CAA4C;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,kBAAkB,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAEvE,UAAU,EAAE,CAAC,eAAe,UAAU,KAAK,CAAC,CAAC;IAE7C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAE/D,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1D,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAEtB,UAAU,EAAE,CAAC,eAAe,CAAC,CAAC;QAC9B,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC;YAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACjC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,IAAa;IAC5C,IAAI,CAAC;QACH,SAAS,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;AACZ,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,4EAA4E;AAC5E,SAAS,iBAAiB;IACxB,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,EAAE,EAAE;QAAE,OAAO;IACxB,IAAI,CAAC;QAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAChD,yEAAyE;IACzE,IAAI,MAAM,CAAC,EAAE,KAAK,eAAe,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QACrE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;IACnF,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,0BAA0B;IACxC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO;IAClC,IAAI,kBAAkB,EAAE;QAAE,OAAO;IAEjC,iBAAiB,EAAE,CAAC;IAEpB,IAAI,CAAC,kBAAkB,EAAE;QAAE,OAAO;IAElC,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC;IACzC,IAAI,KAAK,EAAE,WAAW,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,WAAW,GAAG,iBAAiB;QAAE,OAAO;IACrF,qEAAqE;IACrE,SAAS,CAAC,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC,CAAC;IAEvF,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,iBAAiB,CAAC,EAAE;YACzD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAC;QACH,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;IACxD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,IAAI,kBAAkB,EAAE,IAAI,CAAC,kBAAkB,EAAE;QAAE,OAAO;IAE1D,MAAM,CAAC,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACnC,SAAS,CAAC,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;IACrH,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO;IAC5B,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,EAAE,eAAe,CAAC;QAAE,OAAO;IAExD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,UAAU,EAAE,CAAC;QAC9B,IAAI,EAAE,EAAE,CAAC;YACP,SAAS,CAAC,gBAAgB,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
package/src/commands/git.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
129
|
-
|
|
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}`);
|
package/src/commands/login.ts
CHANGED
|
@@ -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,
|
package/src/commands/logs.ts
CHANGED
|
@@ -347,12 +347,19 @@ async function showBuildLogs(
|
|
|
347
347
|
info(chalk.dim(`Build ${buildId}\n`));
|
|
348
348
|
|
|
349
349
|
if (tailN !== undefined) {
|
|
350
|
+
// Snapshot semantics: the server replays history immediately; if the
|
|
351
|
+
// build is still running the stream would otherwise follow it forever.
|
|
352
|
+
// Stop after 3s without new events and print what we have.
|
|
350
353
|
const lines: string[] = [];
|
|
351
|
-
await streamSSE(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
await streamSSE(
|
|
355
|
+
`/api/builds/${buildId}/logs`,
|
|
356
|
+
(event, data) => {
|
|
357
|
+
if (event === "done" || event === "error") return false;
|
|
358
|
+
lines.push(data);
|
|
359
|
+
return true;
|
|
360
|
+
},
|
|
361
|
+
{ idleTimeoutMs: 3000 },
|
|
362
|
+
);
|
|
356
363
|
for (const line of lines.slice(-tailN)) printLogLine(line);
|
|
357
364
|
return;
|
|
358
365
|
}
|
package/src/commands/redeploy.ts
CHANGED
|
@@ -54,24 +54,30 @@ export function registerRedeploy(program: Command) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const spinner = ora("Starting redeploy...").start();
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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 }> }>(
|
package/src/commands/secrets.ts
CHANGED
|
@@ -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,
|
package/src/commands/ssh.ts
CHANGED
|
@@ -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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
);
|
package/src/commands/up.ts
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
}
|
|
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;
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -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 {
|
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;
|