@lizard-build/cli 0.1.0 → 0.3.30
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/.github/workflows/release.yml +90 -0
- package/AGENTS.md +113 -0
- package/README.md +41 -0
- package/dist/commands/add.js +318 -45
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +68 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +13 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/domain.d.ts +9 -0
- package/dist/commands/domain.js +195 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/git.js +175 -36
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +128 -86
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +7 -0
- package/dist/commands/link.js +104 -33
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.js +4 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.js +223 -30
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/open.js +3 -2
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/port.d.ts +7 -0
- package/dist/commands/port.js +49 -0
- package/dist/commands/port.js.map +1 -0
- package/dist/commands/projects.js +36 -6
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/ps.js +32 -39
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/redeploy.js +48 -8
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/regions.js +2 -5
- package/dist/commands/regions.js.map +1 -1
- package/dist/commands/restart.js +84 -10
- package/dist/commands/restart.js.map +1 -1
- package/dist/commands/run.d.ts +9 -0
- package/dist/commands/run.js +61 -22
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/scale.d.ts +10 -0
- package/dist/commands/scale.js +166 -0
- package/dist/commands/scale.js.map +1 -0
- package/dist/commands/secrets.js +200 -89
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/service-set.d.ts +49 -0
- package/dist/commands/service-set.js +552 -0
- package/dist/commands/service-set.js.map +1 -0
- package/dist/commands/service-show.d.ts +11 -0
- package/dist/commands/service-show.js +44 -0
- package/dist/commands/service-show.js.map +1 -0
- package/dist/commands/service.d.ts +8 -0
- package/dist/commands/service.js +262 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.js +146 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/ssh.d.ts +2 -0
- package/dist/commands/ssh.js +161 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +49 -38
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +5 -0
- package/dist/commands/unlink.js +18 -0
- package/dist/commands/unlink.js.map +1 -0
- package/dist/commands/up.d.ts +9 -0
- package/dist/commands/up.js +417 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +79 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/whoami.js +26 -6
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +36 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.js +209 -82
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +17 -2
- package/dist/lib/api.js +85 -51
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -11
- package/dist/lib/auth.js +16 -36
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config.d.ts +36 -15
- package/dist/lib/config.js +71 -58
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +17 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/name.d.ts +11 -0
- package/dist/lib/name.js +26 -0
- package/dist/lib/name.js.map +1 -0
- package/dist/lib/picker.d.ts +32 -0
- package/dist/lib/picker.js +91 -0
- package/dist/lib/picker.js.map +1 -0
- package/dist/lib/resolve.d.ts +85 -0
- package/dist/lib/resolve.js +203 -0
- package/dist/lib/resolve.js.map +1 -0
- package/dist/lib/updater.d.ts +16 -0
- package/dist/lib/updater.js +102 -0
- package/dist/lib/updater.js.map +1 -0
- package/lizard-wrapper.sh +2 -0
- package/package.json +11 -3
- package/skill-data/core/SKILL.md +239 -0
- package/src/commands/add.ts +388 -56
- package/src/commands/config.ts +80 -0
- package/src/commands/docs.ts +15 -0
- package/src/commands/domain.ts +248 -0
- package/src/commands/git.ts +201 -40
- package/src/commands/init.ts +149 -100
- package/src/commands/link.ts +127 -35
- package/src/commands/login.ts +4 -3
- package/src/commands/logs.ts +283 -27
- package/src/commands/open.ts +3 -2
- package/src/commands/port.ts +57 -0
- package/src/commands/projects.ts +43 -6
- package/src/commands/ps.ts +39 -60
- package/src/commands/redeploy.ts +51 -10
- package/src/commands/regions.ts +2 -6
- package/src/commands/restart.ts +84 -10
- package/src/commands/run.ts +68 -24
- package/src/commands/scale.ts +216 -0
- package/src/commands/secrets.ts +277 -100
- package/src/commands/service-set.ts +669 -0
- package/src/commands/service-show.ts +52 -0
- package/src/commands/service.ts +298 -0
- package/src/commands/skill.ts +157 -0
- package/src/commands/ssh.ts +176 -0
- package/src/commands/status.ts +51 -46
- package/src/commands/unlink.ts +17 -0
- package/src/commands/up.ts +461 -0
- package/src/commands/upgrade.ts +87 -0
- package/src/commands/whoami.ts +34 -6
- package/src/commands/workspace.ts +44 -0
- package/src/index.ts +219 -85
- package/src/lib/api.ts +114 -51
- package/src/lib/auth.ts +22 -46
- package/src/lib/config.ts +100 -65
- package/src/lib/format.ts +18 -4
- package/src/lib/name.ts +27 -0
- package/src/lib/picker.ts +133 -0
- package/src/lib/resolve.ts +285 -0
- package/src/lib/updater.ts +106 -0
- package/test/cli.test.ts +491 -0
- package/test/fixtures/hello-app/Dockerfile +5 -0
- package/test/fixtures/hello-app/index.js +5 -0
- package/test/unit/api.test.ts +66 -0
- package/test/unit/config.test.ts +94 -0
- package/test/unit/init.test.ts +211 -0
- package/test/unit/json.test.ts +208 -0
- package/test/unit/picker.test.ts +161 -0
- package/test/unit/resolve.test.ts +124 -0
- package/test/unit/service-set.test.ts +355 -0
- package/vitest.config.ts +10 -0
- package/dist/commands/connect.d.ts +0 -2
- package/dist/commands/connect.js +0 -117
- package/dist/commands/connect.js.map +0 -1
- package/dist/commands/context.d.ts +0 -2
- package/dist/commands/context.js +0 -71
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -2
- package/dist/commands/deploy.js +0 -120
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/destroy.d.ts +0 -2
- package/dist/commands/destroy.js +0 -51
- package/dist/commands/destroy.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.js +0 -41
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/version.d.ts +0 -2
- package/dist/commands/version.js +0 -37
- package/dist/commands/version.js.map +0 -1
- package/src/commands/connect.ts +0 -145
- package/src/commands/context.ts +0 -93
- package/src/commands/deploy.ts +0 -153
- package/src/commands/destroy.ts +0 -51
- package/src/commands/update.ts +0 -44
- package/src/commands/version.ts +0 -37
package/src/commands/status.ts
CHANGED
|
@@ -1,65 +1,70 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isJSONMode, printJSON,
|
|
3
|
+
import { getProjectLink, updateProjectLink } from "../lib/config.js";
|
|
4
|
+
import { lookupProjectWorkspace } from "../lib/resolve.js";
|
|
5
|
+
import { isJSONMode, printJSON, info } from "../lib/format.js";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* `lizard status` — print the linked workspace / project / service for
|
|
9
|
+
* the current working directory. Mirrors `railway status`.
|
|
10
|
+
*
|
|
11
|
+
* Lazy-fills workspaceId into the link when missing so legacy configs
|
|
12
|
+
* surface their workspace too.
|
|
13
|
+
*/
|
|
7
14
|
export function registerStatus(program: Command) {
|
|
8
15
|
program
|
|
9
16
|
.command("status")
|
|
10
|
-
.description("Show project
|
|
17
|
+
.description("Show linked workspace, project, and service")
|
|
11
18
|
.action(async () => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
api.get<{ apps: any[]; addons: any[] }>(
|
|
20
|
-
`/api/projects/${projectId}/services`,
|
|
21
|
-
),
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
if (isJSONMode()) {
|
|
25
|
-
printJSON({ project, services, environment: config?.environment || "production" });
|
|
19
|
+
const link = getProjectLink();
|
|
20
|
+
if (!link) {
|
|
21
|
+
if (isJSONMode()) {
|
|
22
|
+
printJSON({ cwd: process.cwd(), linked: false });
|
|
23
|
+
} else {
|
|
24
|
+
info("Not linked. Run `lizard init` to create or link a project.");
|
|
25
|
+
}
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// Backfill workspace info for legacy links (saved before workspaces existed)
|
|
30
|
+
let workspaceName = link.workspaceName;
|
|
31
|
+
if (!link.workspaceId) {
|
|
32
|
+
const fetched = await lookupProjectWorkspace(link.projectId);
|
|
33
|
+
if (fetched?.workspaceId) {
|
|
34
|
+
workspaceName = fetched.workspaceName ?? undefined;
|
|
35
|
+
try {
|
|
36
|
+
updateProjectLink({
|
|
37
|
+
workspaceId: fetched.workspaceId,
|
|
38
|
+
workspaceName,
|
|
39
|
+
});
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
32
42
|
}
|
|
33
|
-
console.log();
|
|
34
43
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
status: a.status,
|
|
46
|
-
url: a.hostname || "",
|
|
47
|
-
})),
|
|
48
|
-
];
|
|
44
|
+
const out = {
|
|
45
|
+
cwd: process.cwd(),
|
|
46
|
+
linked: true,
|
|
47
|
+
workspace: workspaceName ?? null,
|
|
48
|
+
workspaceId: link.workspaceId ?? null,
|
|
49
|
+
project: link.projectName ?? null,
|
|
50
|
+
projectId: link.projectId,
|
|
51
|
+
service: link.serviceName ?? null,
|
|
52
|
+
serviceId: link.serviceId ?? null,
|
|
53
|
+
};
|
|
49
54
|
|
|
50
|
-
if (
|
|
51
|
-
|
|
55
|
+
if (isJSONMode()) {
|
|
56
|
+
printJSON(out);
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const fmt = (v: string | null) => v ?? chalk.dim("—");
|
|
61
|
+
|
|
62
|
+
console.log(` ${chalk.dim("Workspace:")} ${fmt(out.workspace)}`);
|
|
63
|
+
console.log(` ${chalk.dim("Project:")} ${chalk.bold(out.project ?? link.projectId)}`);
|
|
64
|
+
console.log(
|
|
65
|
+
` ${chalk.dim("Service:")} ${
|
|
66
|
+
out.service ? chalk.bold(out.service) : chalk.dim("(none — `lizard service link`)")
|
|
67
|
+
}`,
|
|
63
68
|
);
|
|
64
69
|
});
|
|
65
70
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { clearProjectLink } from "../lib/config.js";
|
|
3
|
+
import { isJSONMode, printJSON, success } from "../lib/format.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `lizard unlink` — drops the cwd↔project mapping.
|
|
7
|
+
*/
|
|
8
|
+
export function registerUnlink(program: Command) {
|
|
9
|
+
program
|
|
10
|
+
.command("unlink")
|
|
11
|
+
.description("Disassociate the current directory from any project")
|
|
12
|
+
.action(() => {
|
|
13
|
+
clearProjectLink();
|
|
14
|
+
if (isJSONMode()) printJSON({ status: "unlinked" });
|
|
15
|
+
else success("Directory unlinked");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { execSync, spawn } from "child_process";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as readline from "node:readline";
|
|
8
|
+
import { api, streamSSE, getBaseURL, type ResourceScope } from "../lib/api.js";
|
|
9
|
+
import { updateProjectLink, DEFAULT_REGION } from "../lib/config.js";
|
|
10
|
+
import { resolveContext, getScope } from "../lib/resolve.js";
|
|
11
|
+
import { ensureLinked } from "./init.js";
|
|
12
|
+
import {
|
|
13
|
+
success,
|
|
14
|
+
info,
|
|
15
|
+
error,
|
|
16
|
+
isJSONMode,
|
|
17
|
+
isTTY,
|
|
18
|
+
printJSON,
|
|
19
|
+
statusColor,
|
|
20
|
+
} from "../lib/format.js";
|
|
21
|
+
|
|
22
|
+
interface App {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
status: string;
|
|
26
|
+
domain?: string;
|
|
27
|
+
repoUrl?: string;
|
|
28
|
+
branch?: string;
|
|
29
|
+
sourceType?: string;
|
|
30
|
+
builds?: Array<{ id: string; status: string }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds the `up` command:
|
|
35
|
+
* - upload local code (or `[path]`) as a tarball
|
|
36
|
+
* - target a service via --service / linked / first-in-project
|
|
37
|
+
* - --ci streams build logs only and exits when build finishes
|
|
38
|
+
* - --detach returns immediately after upload
|
|
39
|
+
*/
|
|
40
|
+
export function registerUp(program: Command) {
|
|
41
|
+
const up = program
|
|
42
|
+
.command("up")
|
|
43
|
+
.description("Upload and deploy code to Lizard")
|
|
44
|
+
.argument("[path]", "Path to deploy (default: current directory)")
|
|
45
|
+
.option("-d, --detach", "Don't attach to the log stream")
|
|
46
|
+
.option("-c, --ci", "Stream build logs only, exit on completion")
|
|
47
|
+
.option("-s, --service <name>", "Service to deploy to (defaults to linked)")
|
|
48
|
+
.option("--no-gitignore", "Don't ignore paths from .gitignore")
|
|
49
|
+
.option("--region <code>", "Region to create the service in (new services only)")
|
|
50
|
+
.option("--build-command <cmd>", "Build command to run (e.g. 'npm run build')")
|
|
51
|
+
.option("--start-command <cmd>", "Start command to run (e.g. 'node dist/index.js')")
|
|
52
|
+
.option("--pre-deploy-command <cmd>", "Pre-deploy command (e.g. 'node dist/migrate.js')")
|
|
53
|
+
.option("--port <number>", "Container port (default: 3000)")
|
|
54
|
+
.action(async (pathArg: string | undefined, opts, cmd) => {
|
|
55
|
+
const merged = cmd.optsWithGlobals();
|
|
56
|
+
const serviceFlag = merged.service ?? opts.service;
|
|
57
|
+
const projectFlag = merged.project;
|
|
58
|
+
const region: string | undefined = opts.region;
|
|
59
|
+
|
|
60
|
+
// Run init flow if cwd isn't linked yet
|
|
61
|
+
await ensureLinked({ projectName: projectFlag });
|
|
62
|
+
|
|
63
|
+
// Resolve target service: --service flag → linked → first-in-project → prompt-or-fail
|
|
64
|
+
const ctx = await resolveContext({
|
|
65
|
+
projectFlag,
|
|
66
|
+
serviceFlag,
|
|
67
|
+
});
|
|
68
|
+
const projectId = ctx.projectId;
|
|
69
|
+
const scope = getScope(ctx);
|
|
70
|
+
|
|
71
|
+
const targetPath = pathArg ? path.resolve(pathArg) : process.cwd();
|
|
72
|
+
|
|
73
|
+
// `up` always uploads a local tarball. For redeploy of an existing
|
|
74
|
+
// build without re-uploading, use `lizard redeploy`.
|
|
75
|
+
await deployFromLocal({
|
|
76
|
+
projectId,
|
|
77
|
+
scope,
|
|
78
|
+
targetPath,
|
|
79
|
+
useGitignore: opts.gitignore !== false,
|
|
80
|
+
serviceFlag,
|
|
81
|
+
existingServiceId: ctx.service?.id,
|
|
82
|
+
region,
|
|
83
|
+
buildCommand: opts.buildCommand,
|
|
84
|
+
startCommand: opts.startCommand,
|
|
85
|
+
preDeployCommand: opts.preDeployCommand,
|
|
86
|
+
port: opts.port ? parseInt(opts.port, 10) : undefined,
|
|
87
|
+
opts,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// `lizard up status <id>` — show build/deploy status
|
|
92
|
+
up
|
|
93
|
+
.command("status")
|
|
94
|
+
.argument("<id>", "App or deploy ID")
|
|
95
|
+
.description("Show deployment status")
|
|
96
|
+
.action(async (id: string) => {
|
|
97
|
+
const app = await api.get<App>(`/api/apps/${id}`);
|
|
98
|
+
if (isJSONMode()) {
|
|
99
|
+
printJSON(app);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(`${chalk.bold(app.name)} ${statusColor(app.status)}`);
|
|
103
|
+
if (app.domain) console.log(` URL: ${chalk.cyan(`https://${app.domain}`)}`);
|
|
104
|
+
if (app.builds?.length)
|
|
105
|
+
console.log(` Latest build: ${statusColor(app.builds[0].status)}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── deploy strategies ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
async function deployFromLocal(args: {
|
|
112
|
+
projectId: string;
|
|
113
|
+
scope: ResourceScope;
|
|
114
|
+
targetPath: string;
|
|
115
|
+
buildCommand?: string;
|
|
116
|
+
startCommand?: string;
|
|
117
|
+
preDeployCommand?: string;
|
|
118
|
+
port?: number;
|
|
119
|
+
useGitignore: boolean;
|
|
120
|
+
serviceFlag: string | undefined;
|
|
121
|
+
existingServiceId: string | undefined;
|
|
122
|
+
region: string | undefined;
|
|
123
|
+
opts: any;
|
|
124
|
+
}) {
|
|
125
|
+
const defaultName = args.serviceFlag || getDefaultAppName(args.targetPath);
|
|
126
|
+
info(
|
|
127
|
+
`${args.existingServiceId ? "Uploading" : "Creating service from"} ${chalk.dim(args.targetPath)}`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
let appName = args.serviceFlag || defaultName;
|
|
131
|
+
if (!args.existingServiceId && !args.serviceFlag && isTTY()) {
|
|
132
|
+
const nameInput = await prompt(`Service name [${defaultName}]: `);
|
|
133
|
+
appName = nameInput || defaultName;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const files = getUploadFiles(args.targetPath, args.useGitignore);
|
|
137
|
+
if (files.length === 0) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"No files to upload. Run from a directory with files, or pass a path: `lizard up <path>`.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
info(chalk.dim(` ${files.length} files selected`));
|
|
143
|
+
|
|
144
|
+
const detectedPort = args.port === undefined ? detectLocalPort(args.targetPath) : undefined;
|
|
145
|
+
if (detectedPort) info(`Detected port ${chalk.bold(detectedPort)} from Dockerfile`);
|
|
146
|
+
|
|
147
|
+
const tarball = await createTarball(files, args.targetPath);
|
|
148
|
+
info(chalk.dim(` Tarball: ${(tarball.length / 1024 / 1024).toFixed(1)} MB`));
|
|
149
|
+
|
|
150
|
+
const spinner = ora("Uploading...").start();
|
|
151
|
+
let newApp: App & { buildId?: string };
|
|
152
|
+
try {
|
|
153
|
+
const resolvedPort = args.port ?? detectedPort;
|
|
154
|
+
const qs = new URLSearchParams();
|
|
155
|
+
// Only send port when explicitly given or detected — lets server keep the stored
|
|
156
|
+
// containerPort on redeploy instead of overwriting it with the 3000 default.
|
|
157
|
+
if (resolvedPort !== undefined) qs.set("port", String(resolvedPort));
|
|
158
|
+
if (!args.existingServiceId) {
|
|
159
|
+
qs.set("name", appName);
|
|
160
|
+
if (args.region) qs.set("region", args.region);
|
|
161
|
+
// New services with no detected port default to 3000
|
|
162
|
+
if (resolvedPort === undefined) qs.set("port", "3000");
|
|
163
|
+
}
|
|
164
|
+
if (args.existingServiceId) qs.set("appId", args.existingServiceId);
|
|
165
|
+
if (args.buildCommand) qs.set("buildCommand", args.buildCommand);
|
|
166
|
+
if (args.startCommand) qs.set("startCommand", args.startCommand);
|
|
167
|
+
if (args.preDeployCommand) qs.set("preDeployCommand", args.preDeployCommand);
|
|
168
|
+
if (args.scope.workspaceId) qs.set("workspaceId", args.scope.workspaceId);
|
|
169
|
+
|
|
170
|
+
const url = `${getBaseURL()}/api/projects/${args.projectId}/apps/upload?${qs.toString()}`;
|
|
171
|
+
const res = await fetch(url, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/octet-stream",
|
|
175
|
+
Authorization: `Bearer ${(await import("../lib/auth.js")).getToken()}`,
|
|
176
|
+
},
|
|
177
|
+
body: tarball.buffer as ArrayBuffer,
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const text = await res.text();
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Upload failed (${res.status}): ${text || res.statusText}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
newApp = (await res.json()) as App & { buildId?: string };
|
|
186
|
+
spinner.succeed(`Service ${chalk.bold(newApp.name)} ${args.existingServiceId ? "updated" : "created"}`);
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
spinner.fail("Upload failed");
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
saveServiceToConfig(args.projectId, newApp.id, newApp.name);
|
|
193
|
+
|
|
194
|
+
if (args.opts.detach) {
|
|
195
|
+
isJSONMode()
|
|
196
|
+
? printJSON({ appId: newApp.id, version: 1, status: "deploying" })
|
|
197
|
+
: success(`Deploy started ${chalk.dim(`lizard up status ${newApp.id}`)}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await streamBuildLogs(newApp.id, args.opts.ci);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function saveServiceToConfig(_projectId: string, serviceId: string, serviceName: string) {
|
|
206
|
+
try {
|
|
207
|
+
updateProjectLink({ serviceId, serviceName });
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getDefaultAppName(cwd: string = process.cwd()): string {
|
|
212
|
+
try {
|
|
213
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
214
|
+
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
215
|
+
} catch {}
|
|
216
|
+
return path.basename(cwd);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getUploadFiles(cwd: string, useGitignore: boolean): string[] {
|
|
220
|
+
if (useGitignore) {
|
|
221
|
+
try {
|
|
222
|
+
// `-z` outputs NUL-separated paths and disables git's default behaviour
|
|
223
|
+
// of double-quoting names with unusual characters. Without it, a file
|
|
224
|
+
// called `weird\nname.txt` would either get quoted (and never matched
|
|
225
|
+
// by tar) or split the listing across newlines.
|
|
226
|
+
const tracked = execSync("git ls-files -z", {
|
|
227
|
+
cwd,
|
|
228
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
229
|
+
})
|
|
230
|
+
.toString("utf8")
|
|
231
|
+
.split("\0")
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
const untracked = execSync("git ls-files --others --exclude-standard -z", {
|
|
234
|
+
cwd,
|
|
235
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
236
|
+
})
|
|
237
|
+
.toString("utf8")
|
|
238
|
+
.split("\0")
|
|
239
|
+
.filter(Boolean);
|
|
240
|
+
return [...new Set([...tracked, ...untracked])];
|
|
241
|
+
} catch {
|
|
242
|
+
// fall through to manual collection
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return collectFilesManually(cwd, cwd);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const EXCLUDE_DIRS = new Set([
|
|
249
|
+
"node_modules",
|
|
250
|
+
".git",
|
|
251
|
+
"dist",
|
|
252
|
+
".next",
|
|
253
|
+
"build",
|
|
254
|
+
"__pycache__",
|
|
255
|
+
".venv",
|
|
256
|
+
"venv",
|
|
257
|
+
".cache",
|
|
258
|
+
"coverage",
|
|
259
|
+
".turbo",
|
|
260
|
+
".vercel",
|
|
261
|
+
]);
|
|
262
|
+
const EXCLUDE_EXT = new Set([".pyc", ".pyo", ".log", ".DS_Store"]);
|
|
263
|
+
|
|
264
|
+
function collectFilesManually(root: string, dir: string): string[] {
|
|
265
|
+
const results: string[] = [];
|
|
266
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
267
|
+
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
268
|
+
if (EXCLUDE_EXT.has(path.extname(entry.name))) continue;
|
|
269
|
+
const full = path.join(dir, entry.name);
|
|
270
|
+
if (entry.isDirectory()) results.push(...collectFilesManually(root, full));
|
|
271
|
+
else results.push(path.relative(root, full));
|
|
272
|
+
}
|
|
273
|
+
return results;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function createTarball(files: string[], cwd: string): Promise<Uint8Array> {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const chunks: Uint8Array[] = [];
|
|
279
|
+
// `--null` makes tar read NUL-separated paths from stdin, matching what
|
|
280
|
+
// `git ls-files -z` writes. Newline-separated input would split filenames
|
|
281
|
+
// containing `\n` across multiple entries. Both bsdtar (macOS) and GNU
|
|
282
|
+
// tar accept `--null` before `-T -`.
|
|
283
|
+
const tar = spawn("tar", ["--null", "-czf", "-", "-T", "-"], { cwd });
|
|
284
|
+
tar.stdout.on("data", (c: Buffer) => chunks.push(c));
|
|
285
|
+
tar.stderr.on("data", () => {});
|
|
286
|
+
tar.on("close", (code: number) => {
|
|
287
|
+
if (code === 0) {
|
|
288
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
289
|
+
const out = new Uint8Array(total);
|
|
290
|
+
let off = 0;
|
|
291
|
+
for (const c of chunks) {
|
|
292
|
+
out.set(c, off);
|
|
293
|
+
off += c.length;
|
|
294
|
+
}
|
|
295
|
+
resolve(out);
|
|
296
|
+
} else {
|
|
297
|
+
reject(new Error(`tar exited ${code}`));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
if (files.length > 0) tar.stdin.write(files.join("\0") + "\0");
|
|
301
|
+
tar.stdin.end();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function detectLocalPort(dir: string): number | undefined {
|
|
306
|
+
for (const name of ["Dockerfile", "dockerfile", "Dockerfile.production"]) {
|
|
307
|
+
try {
|
|
308
|
+
const text = fs.readFileSync(path.join(dir, name), "utf8");
|
|
309
|
+
const match = text.match(/^EXPOSE\s+(\d+)/m);
|
|
310
|
+
if (match) return parseInt(match[1], 10);
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function prompt(question: string): Promise<string> {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
319
|
+
rl.question(question, (answer) => {
|
|
320
|
+
rl.close();
|
|
321
|
+
resolve(answer.trim());
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function streamBuildLogs(appId: string, ciMode: boolean = false) {
|
|
327
|
+
const spinner = ora("Waiting for build...").start();
|
|
328
|
+
let buildId: string | null = null;
|
|
329
|
+
for (let i = 0; i < 30; i++) {
|
|
330
|
+
await sleep(2000);
|
|
331
|
+
try {
|
|
332
|
+
const app = await api.get<App>(`/api/apps/${appId}`);
|
|
333
|
+
if (app.builds?.length) {
|
|
334
|
+
const latest = app.builds[0];
|
|
335
|
+
if (["building", "deploying", "running", "failed"].includes(latest.status)) {
|
|
336
|
+
buildId = latest.id;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch {}
|
|
341
|
+
}
|
|
342
|
+
spinner.stop();
|
|
343
|
+
if (!buildId) {
|
|
344
|
+
info(chalk.dim("No build found. Check `lizard up status <id>`."));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
info(chalk.dim("Streaming build logs...\n"));
|
|
348
|
+
|
|
349
|
+
// Stream with auto-reconnect — connections can drop mid-build (Cloudflare
|
|
350
|
+
// idle timeout, network blips). Reconnect until the build itself reports
|
|
351
|
+
// a terminal status, with a hard cap so we don't loop forever.
|
|
352
|
+
const deadline = Date.now() + 15 * 60 * 1000; // 15 min max
|
|
353
|
+
while (Date.now() < deadline) {
|
|
354
|
+
let dropped = false;
|
|
355
|
+
try {
|
|
356
|
+
await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
|
|
357
|
+
if (event === "done" || event === "error") {
|
|
358
|
+
if (event === "error") emitBuildError(data);
|
|
359
|
+
else emitBuildDone();
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
emitBuildLine(data);
|
|
363
|
+
return true;
|
|
364
|
+
});
|
|
365
|
+
} catch {
|
|
366
|
+
dropped = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Whether we got a clean SSE end or a dropped connection, check the
|
|
370
|
+
// build state — terminal status means we stop reconnecting.
|
|
371
|
+
try {
|
|
372
|
+
const build = await api.get<{ status: string }>(`/api/builds/${buildId}`);
|
|
373
|
+
if (build.status === "done" || build.status === "failed") break;
|
|
374
|
+
} catch {}
|
|
375
|
+
|
|
376
|
+
if (!dropped) break; // clean SSE end — don't reconnect
|
|
377
|
+
await sleep(2000);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (ciMode) return;
|
|
381
|
+
|
|
382
|
+
const app = await api.get<App>(`/api/apps/${appId}`);
|
|
383
|
+
if (app.status === "running") {
|
|
384
|
+
if (isJSONMode()) {
|
|
385
|
+
process.stdout.write(
|
|
386
|
+
JSON.stringify({
|
|
387
|
+
event: "deployed",
|
|
388
|
+
status: "running",
|
|
389
|
+
url: app.domain ? `https://${app.domain}` : null,
|
|
390
|
+
}) + "\n",
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
success(`Deployed! ${app.domain ? chalk.cyan(`https://${app.domain}`) : ""}`);
|
|
394
|
+
}
|
|
395
|
+
} else if (app.status === "failed") {
|
|
396
|
+
if (isJSONMode()) {
|
|
397
|
+
process.stdout.write(
|
|
398
|
+
JSON.stringify({ event: "failed", status: "failed" }) + "\n",
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
error("Deploy failed. Check logs with `lizard logs --build`");
|
|
402
|
+
}
|
|
403
|
+
} else if (app.status === "deploying") {
|
|
404
|
+
if (isJSONMode()) {
|
|
405
|
+
process.stdout.write(
|
|
406
|
+
JSON.stringify({ event: "deploying", status: "deploying" }) + "\n",
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
info(chalk.dim("Still deploying... check status with `lizard ps`"));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── build stream emitters ────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
function emitBuildLine(data: string) {
|
|
417
|
+
let parsed: any;
|
|
418
|
+
try {
|
|
419
|
+
parsed = JSON.parse(data);
|
|
420
|
+
} catch {
|
|
421
|
+
parsed = { line: data };
|
|
422
|
+
}
|
|
423
|
+
if (typeof parsed === "string") parsed = { line: parsed };
|
|
424
|
+
const line = parsed.line ?? parsed.message ?? "";
|
|
425
|
+
|
|
426
|
+
if (isJSONMode()) {
|
|
427
|
+
process.stdout.write(
|
|
428
|
+
JSON.stringify({ event: "log", line, ...stripLine(parsed) }) + "\n",
|
|
429
|
+
);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (line) process.stdout.write(line + "\n");
|
|
433
|
+
else process.stdout.write(data + "\n");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function stripLine(obj: any): Record<string, unknown> {
|
|
437
|
+
const { line: _l, message: _m, ...rest } = obj;
|
|
438
|
+
return rest;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function emitBuildDone() {
|
|
442
|
+
if (isJSONMode()) {
|
|
443
|
+
process.stdout.write(JSON.stringify({ event: "done" }) + "\n");
|
|
444
|
+
} else {
|
|
445
|
+
success("Build complete");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function emitBuildError(data: string) {
|
|
450
|
+
if (isJSONMode()) {
|
|
451
|
+
process.stdout.write(
|
|
452
|
+
JSON.stringify({ event: "error", message: data }) + "\n",
|
|
453
|
+
);
|
|
454
|
+
} else {
|
|
455
|
+
error(`Build failed: ${data}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function sleep(ms: number) {
|
|
460
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
461
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { info, success, isJSONMode, printJSON } from "../lib/format.js";
|
|
4
|
+
import { CURRENT_VERSION, getLatestVersion, selfUpdate } from "../lib/updater.js";
|
|
5
|
+
|
|
6
|
+
export function registerUpgrade(program: Command) {
|
|
7
|
+
program
|
|
8
|
+
.command("upgrade")
|
|
9
|
+
.description("Upgrade Lizard CLI to latest version")
|
|
10
|
+
.option("--check", "Only check for updates without installing")
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const result = await getLatestVersion();
|
|
13
|
+
|
|
14
|
+
if (result.kind === "rate-limited") {
|
|
15
|
+
if (isJSONMode()) {
|
|
16
|
+
printJSON({
|
|
17
|
+
currentVersion: CURRENT_VERSION,
|
|
18
|
+
error: { code: "rate_limited", resetAt: result.resetAt },
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const eta =
|
|
23
|
+
result.resetAt > 0
|
|
24
|
+
? ` Try again in ~${Math.max(1, Math.ceil((result.resetAt * 1000 - Date.now()) / 60_000))} min.`
|
|
25
|
+
: "";
|
|
26
|
+
info(`GitHub API rate-limited (60 req/h per IP).${eta}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (result.kind === "error") {
|
|
31
|
+
if (isJSONMode()) {
|
|
32
|
+
printJSON({
|
|
33
|
+
currentVersion: CURRENT_VERSION,
|
|
34
|
+
error: { code: "check_failed" },
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
info("Could not check for updates. Check your internet connection.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const latest = result.version;
|
|
43
|
+
const updateAvailable = latest !== CURRENT_VERSION;
|
|
44
|
+
|
|
45
|
+
if (isJSONMode() && opts.check) {
|
|
46
|
+
printJSON({ currentVersion: CURRENT_VERSION, latestVersion: latest, updateAvailable });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!updateAvailable) {
|
|
51
|
+
if (isJSONMode()) {
|
|
52
|
+
printJSON({
|
|
53
|
+
currentVersion: CURRENT_VERSION,
|
|
54
|
+
latestVersion: latest,
|
|
55
|
+
updateAvailable: false,
|
|
56
|
+
upgraded: false,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
info(`Already up to date (v${CURRENT_VERSION})`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (opts.check) {
|
|
65
|
+
info(`Update available: v${CURRENT_VERSION} → ${chalk.green("v" + latest)}`);
|
|
66
|
+
info(chalk.dim(`Run \`lizard upgrade\` to install`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
info(`Upgrading v${CURRENT_VERSION} → ${chalk.green("v" + latest)}...`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await selfUpdate((msg) => info(chalk.dim(msg)));
|
|
74
|
+
if (isJSONMode()) {
|
|
75
|
+
printJSON({
|
|
76
|
+
previousVersion: CURRENT_VERSION,
|
|
77
|
+
latestVersion: latest,
|
|
78
|
+
upgraded: true,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
success(`Upgraded to v${latest}`);
|
|
82
|
+
}
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
throw new Error(`Upgrade failed: ${e.message}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|