@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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { api } from "../lib/api.js";
|
|
5
|
+
import { resolveProjectId } from "../lib/config.js";
|
|
6
|
+
import { getActiveService } from "../lib/resolve.js";
|
|
7
|
+
import { success, isJSONMode, printJSON, table, isTTY } from "../lib/format.js";
|
|
8
|
+
|
|
9
|
+
interface DomainResponse {
|
|
10
|
+
ok?: boolean;
|
|
11
|
+
hostname: string;
|
|
12
|
+
generated?: boolean;
|
|
13
|
+
verified?: boolean;
|
|
14
|
+
txtRecord?: string;
|
|
15
|
+
txtValue?: string;
|
|
16
|
+
cnameTarget?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AppLite {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
domain?: string | null;
|
|
23
|
+
containerPort?: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `lizard domain` — domain management.
|
|
28
|
+
* bare → if no domain, auto-generate one; otherwise show current
|
|
29
|
+
* <hostname> → attach a custom domain
|
|
30
|
+
* delete <h> → remove a domain
|
|
31
|
+
* generate → force-generate a fresh *.onlizard.com subdomain
|
|
32
|
+
*/
|
|
33
|
+
export function registerDomain(program: Command) {
|
|
34
|
+
const dom = program
|
|
35
|
+
.command("domain")
|
|
36
|
+
.alias("domains")
|
|
37
|
+
.argument("[hostname]", "Custom domain to attach (e.g. app.example.com)")
|
|
38
|
+
.description("Manage service domains")
|
|
39
|
+
.option("-s, --service <name>", "Service name or ID")
|
|
40
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
41
|
+
.option("--port <n>", "Port to expose", parseIntOption)
|
|
42
|
+
.action(async (hostname: string | undefined, opts, _cmd) => {
|
|
43
|
+
const projectId = await resolveProjectId(opts.project);
|
|
44
|
+
const service = await getActiveService(opts.service, projectId);
|
|
45
|
+
|
|
46
|
+
if (!hostname) {
|
|
47
|
+
// Bare `lizard domain` — show or auto-generate.
|
|
48
|
+
const appRow = await api
|
|
49
|
+
.get<AppLite>(`/api/apps/${service.id}`)
|
|
50
|
+
.catch(() => null);
|
|
51
|
+
const existing = appRow?.domain;
|
|
52
|
+
|
|
53
|
+
if (existing) {
|
|
54
|
+
if (isJSONMode()) {
|
|
55
|
+
printJSON({ hostname: existing, generated: false });
|
|
56
|
+
} else {
|
|
57
|
+
console.log(chalk.cyan(`https://${existing}`));
|
|
58
|
+
console.log(
|
|
59
|
+
chalk.dim(
|
|
60
|
+
` Reference from other services: ${chalk.cyan(`\${{${service.name}.LIZARD_PUBLIC_DOMAIN}}`)}`,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await api
|
|
68
|
+
.post<DomainResponse>(`/api/apps/${service.id}/domains`, { generate: true })
|
|
69
|
+
.catch((err: any) => {
|
|
70
|
+
if (err?.status === 404) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Domain endpoint not yet implemented. The API needs " +
|
|
73
|
+
"`POST /api/apps/{id}/domains` with body { generate: true }.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (isJSONMode()) {
|
|
80
|
+
printJSON(result);
|
|
81
|
+
} else {
|
|
82
|
+
success(`Domain generated: ${chalk.cyan(`https://${result.hostname}`)}`);
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.dim(
|
|
85
|
+
` Reference from other services: ${chalk.cyan(`\${{${service.name}.LIZARD_PUBLIC_DOMAIN}}`)}`,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Attach custom hostname
|
|
93
|
+
const result = await api
|
|
94
|
+
.post<DomainResponse>(`/api/apps/${service.id}/domains`, {
|
|
95
|
+
hostname,
|
|
96
|
+
port: opts.port,
|
|
97
|
+
})
|
|
98
|
+
.catch((err: any) => {
|
|
99
|
+
if (err?.status === 404) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Domain endpoint not yet implemented. The API needs " +
|
|
102
|
+
"`POST /api/apps/{id}/domains` with body { hostname }.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
throw err;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (isJSONMode()) {
|
|
109
|
+
printJSON(result);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Custom domain — print verification + DNS instructions
|
|
114
|
+
success(`Custom domain ${chalk.cyan(hostname)} registered (pending verification)`);
|
|
115
|
+
console.log();
|
|
116
|
+
console.log(chalk.bold("1) Verify ownership — add this TXT record at your DNS provider:"));
|
|
117
|
+
console.log(` ${chalk.dim("Name: ")}${chalk.cyan(result.txtRecord || `_lizard-verify.${hostname}`)}`);
|
|
118
|
+
console.log(` ${chalk.dim("Value:")} ${chalk.cyan(result.txtValue || "")}`);
|
|
119
|
+
console.log();
|
|
120
|
+
if (result.cnameTarget) {
|
|
121
|
+
console.log(chalk.bold("2) Point traffic — add this CNAME record:"));
|
|
122
|
+
console.log(` ${chalk.dim("Name: ")}${chalk.cyan(hostname)}`);
|
|
123
|
+
console.log(` ${chalk.dim("Value:")} ${chalk.cyan(result.cnameTarget)}`);
|
|
124
|
+
console.log(chalk.dim(` (${result.cnameTarget} is a multi-A record across all load balancers — no IP to track.)`));
|
|
125
|
+
console.log();
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.bold("3) Once both records propagate, run:"));
|
|
128
|
+
console.log(` ${chalk.cyan(`lizard domain verify ${hostname}`)}`);
|
|
129
|
+
console.log();
|
|
130
|
+
console.log(chalk.dim("HTTPS certificate will be issued automatically by Let's Encrypt on first request."));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Subcommands intentionally don't redeclare -s/-p: Commander 14 binds a
|
|
134
|
+
// duplicate short flag to the parent, leaving the subcommand action with
|
|
135
|
+
// `opts.service === undefined`. Read parent values via optsWithGlobals().
|
|
136
|
+
dom
|
|
137
|
+
.command("generate")
|
|
138
|
+
.description("Generate a fresh *.onlizard.com subdomain")
|
|
139
|
+
.action(async (_opts, sub) => {
|
|
140
|
+
const opts = sub.optsWithGlobals();
|
|
141
|
+
const projectId = await resolveProjectId(opts.project);
|
|
142
|
+
const service = await getActiveService(opts.service, projectId);
|
|
143
|
+
|
|
144
|
+
const result = await api
|
|
145
|
+
.post<DomainResponse>(`/api/apps/${service.id}/domains`, { generate: true })
|
|
146
|
+
.catch((err: any) => {
|
|
147
|
+
if (err?.status === 404) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"Domain endpoint not yet implemented. The API needs " +
|
|
150
|
+
"`POST /api/apps/{id}/domains` with body { generate: true }.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
throw err;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (isJSONMode()) {
|
|
157
|
+
printJSON(result);
|
|
158
|
+
} else {
|
|
159
|
+
success(`Domain generated: ${chalk.cyan(`https://${result.hostname}`)}`);
|
|
160
|
+
console.log(
|
|
161
|
+
chalk.dim(
|
|
162
|
+
` Reference from other services: ${chalk.cyan(`\${{${service.name}.LIZARD_PUBLIC_DOMAIN}}`)}`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
dom
|
|
169
|
+
.command("verify")
|
|
170
|
+
.argument("<hostname>", "Custom domain to verify")
|
|
171
|
+
.description("Check the TXT record and activate the domain")
|
|
172
|
+
.action(async (hostname: string, _opts, sub) => {
|
|
173
|
+
const opts = sub.optsWithGlobals();
|
|
174
|
+
const projectId = await resolveProjectId(opts.project);
|
|
175
|
+
const service = await getActiveService(opts.service, projectId);
|
|
176
|
+
|
|
177
|
+
const result = await api
|
|
178
|
+
.post<{ ok: boolean; verified: boolean; hostname?: string; message?: string }>(
|
|
179
|
+
`/api/apps/${service.id}/domains/verify`,
|
|
180
|
+
{ hostname },
|
|
181
|
+
)
|
|
182
|
+
.catch((err: any) => {
|
|
183
|
+
if (err?.status === 404) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`No pending verification for ${hostname}. Run \`lizard domain ${hostname}\` first.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (isJSONMode()) {
|
|
192
|
+
printJSON(result);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (result.verified) {
|
|
197
|
+
success(`Domain ${chalk.cyan(hostname)} verified and active`);
|
|
198
|
+
console.log(chalk.dim(`https://${hostname} — TLS issues on first HTTPS request.`));
|
|
199
|
+
} else {
|
|
200
|
+
console.log(chalk.yellow(`Not verified yet.`));
|
|
201
|
+
if (result.message) console.log(chalk.dim(` ${result.message}`));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
dom
|
|
206
|
+
.command("delete")
|
|
207
|
+
.alias("rm")
|
|
208
|
+
.argument("<hostname>", "Domain to remove")
|
|
209
|
+
.description("Remove a domain")
|
|
210
|
+
.option("-y, --yes", "Skip confirmation")
|
|
211
|
+
.action(async (hostname: string, _opts, sub) => {
|
|
212
|
+
const opts = sub.optsWithGlobals();
|
|
213
|
+
const projectId = await resolveProjectId(opts.project);
|
|
214
|
+
const service = await getActiveService(opts.service, projectId);
|
|
215
|
+
|
|
216
|
+
if (!opts.yes) {
|
|
217
|
+
if (!isTTY()) throw new Error("Use -y to confirm in non-interactive mode");
|
|
218
|
+
const confirm = await p.confirm({
|
|
219
|
+
message: `Remove domain ${chalk.bold(hostname)} from ${chalk.bold(service.name)}?`,
|
|
220
|
+
});
|
|
221
|
+
if (p.isCancel(confirm) || !confirm) process.exit(5);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await api
|
|
225
|
+
.delete(`/api/apps/${service.id}/domains/${encodeURIComponent(hostname)}`)
|
|
226
|
+
.catch((err: any) => {
|
|
227
|
+
if (err?.status === 404) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"Domain delete endpoint not yet implemented. The API needs " +
|
|
230
|
+
"`DELETE /api/apps/{id}/domains/{hostname}`.",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (isJSONMode()) {
|
|
237
|
+
printJSON({ domain: hostname, status: "deleted" });
|
|
238
|
+
} else {
|
|
239
|
+
success(`Domain ${chalk.cyan(hostname)} removed`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseIntOption(v: string): number {
|
|
245
|
+
const n = parseInt(v, 10);
|
|
246
|
+
if (Number.isNaN(n)) throw new Error(`Invalid number: ${v}`);
|
|
247
|
+
return n;
|
|
248
|
+
}
|
package/src/commands/git.ts
CHANGED
|
@@ -1,80 +1,241 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import * as readline from "node:readline";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
2
5
|
import { Command } from "commander";
|
|
3
|
-
import { api } from "../lib/api.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { api, getBaseURL, streamSSE, withScope } from "../lib/api.js";
|
|
7
|
+
import { openURL } from "../lib/auth.js";
|
|
8
|
+
import { resolveProjectScope, resolveService } from "../lib/resolve.js";
|
|
9
|
+
import { success, error, info, isJSONMode, printJSON, isTTY, link } from "../lib/format.js";
|
|
10
|
+
|
|
11
|
+
interface GitHubInstallation {
|
|
12
|
+
id: number;
|
|
13
|
+
account: { login: string; type: string };
|
|
14
|
+
htmlUrl: string;
|
|
15
|
+
repoCount: number | null;
|
|
16
|
+
privateCount: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface GitHubStatus {
|
|
20
|
+
installed: boolean;
|
|
21
|
+
installationId: number | null;
|
|
22
|
+
installations?: GitHubInstallation[];
|
|
23
|
+
manageUrl?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
6
26
|
|
|
7
27
|
export function registerGit(program: Command) {
|
|
8
28
|
const git = program
|
|
9
29
|
.command("git")
|
|
10
|
-
.description("Git integration");
|
|
30
|
+
.description("Git and GitHub integration");
|
|
11
31
|
|
|
32
|
+
// lizard git connect — install GitHub App for private repo access
|
|
12
33
|
git
|
|
13
34
|
.command("connect")
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
.description("Connect GitHub App to access private repositories")
|
|
36
|
+
.action(async () => {
|
|
37
|
+
// Check current status
|
|
38
|
+
const status = await api.get<GitHubStatus>("/api/github/status");
|
|
39
|
+
|
|
40
|
+
if (status.installed) {
|
|
41
|
+
if (isJSONMode()) {
|
|
42
|
+
printJSON({
|
|
43
|
+
installed: true,
|
|
44
|
+
installationId: status.installationId,
|
|
45
|
+
alreadyConnected: true,
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
success("GitHub App is already connected.");
|
|
50
|
+
info(chalk.dim(" Use `lizard git status` to see connected repositories."));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const installUrl = `${getBaseURL()}/api/auth/github/install`;
|
|
55
|
+
|
|
56
|
+
// Non-interactive callers (--json, piped) can't press Enter; return
|
|
57
|
+
// the install URL so they can drive the flow themselves and re-run.
|
|
58
|
+
if (isJSONMode() || !isTTY()) {
|
|
59
|
+
if (isJSONMode()) {
|
|
60
|
+
printJSON({ installed: false, installUrl });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
info(`Open this URL to connect GitHub:\n ${chalk.cyan(installUrl)}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const opened = await openURL(installUrl);
|
|
68
|
+
if (opened) {
|
|
69
|
+
info("Opening GitHub to install the Lizard GitHub App...");
|
|
27
70
|
} else {
|
|
28
|
-
info(`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
info(`Open this URL to connect GitHub:\n ${chalk.cyan(installUrl)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Wait for user to complete installation in browser
|
|
75
|
+
await pressEnter(chalk.dim("\nPress Enter after completing GitHub installation..."));
|
|
76
|
+
|
|
77
|
+
// Verify
|
|
78
|
+
const spinner = ora("Verifying GitHub connection...").start();
|
|
79
|
+
const newStatus = await api.get<GitHubStatus>("/api/github/status");
|
|
80
|
+
spinner.stop();
|
|
81
|
+
|
|
82
|
+
if (newStatus.installed) {
|
|
83
|
+
success("GitHub connected! You can now deploy private repositories.");
|
|
84
|
+
info(chalk.dim(" Run `lizard deploy` to deploy your project."));
|
|
85
|
+
} else {
|
|
86
|
+
error("GitHub App not detected. Please try again or connect via the dashboard.");
|
|
87
|
+
process.exit(1);
|
|
34
88
|
}
|
|
35
89
|
});
|
|
36
90
|
|
|
91
|
+
// lizard git checkout <service> <branch> — switch branch and redeploy
|
|
37
92
|
git
|
|
38
|
-
.command("
|
|
39
|
-
.description("
|
|
40
|
-
.
|
|
41
|
-
|
|
93
|
+
.command("checkout")
|
|
94
|
+
.description("Switch a service to a different branch and redeploy")
|
|
95
|
+
.argument("<service>", "Service name (as shown in the project)")
|
|
96
|
+
.argument("<branch>", "Branch name to switch to")
|
|
97
|
+
.option("--detach", "Start redeploy and exit without streaming logs")
|
|
98
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
99
|
+
.action(async (serviceArg: string, branch: string, opts) => {
|
|
100
|
+
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
101
|
+
|
|
102
|
+
// Resolve service by name
|
|
103
|
+
const svc = await resolveService(projectId, serviceArg);
|
|
104
|
+
if (svc.kind !== "app") throw new Error(`"${serviceArg}" is not an app`);
|
|
105
|
+
const serviceId = svc.id;
|
|
106
|
+
const serviceName = svc.name ?? serviceArg;
|
|
107
|
+
|
|
108
|
+
// Patch the branch
|
|
109
|
+
const spinner = ora(`Switching ${chalk.bold(serviceName)} to branch ${chalk.cyan(branch)}...`).start();
|
|
110
|
+
await api.post(withScope(`/api/projects/${projectId}/config:apply`, scope), {
|
|
111
|
+
services: [{ name: serviceName, branch }],
|
|
112
|
+
});
|
|
113
|
+
spinner.succeed(`Branch set to ${chalk.cyan(branch)}`);
|
|
114
|
+
|
|
115
|
+
// Trigger redeploy
|
|
116
|
+
const deploySpinner = ora("Starting redeploy...").start();
|
|
117
|
+
await api.post(withScope(`/api/apps/${serviceId}/redeploy`, scope));
|
|
118
|
+
deploySpinner.stop();
|
|
119
|
+
|
|
120
|
+
if (opts.detach || isJSONMode()) {
|
|
121
|
+
if (isJSONMode()) printJSON({ id: serviceId, branch, status: "deploying" });
|
|
122
|
+
else success(`Redeploy started on branch ${chalk.cyan(branch)}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
info(`Redeploying ${chalk.bold(serviceName)} on ${chalk.cyan(branch)}...`);
|
|
127
|
+
|
|
128
|
+
// Wait for build to appear
|
|
129
|
+
let buildId: string | null = null;
|
|
130
|
+
for (let i = 0; i < 30; i++) {
|
|
131
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
132
|
+
try {
|
|
133
|
+
const app = await api.get<{ builds?: Array<{ id: string; status: string }> }>(`/api/apps/${serviceId}`);
|
|
134
|
+
const latest = app.builds?.[0];
|
|
135
|
+
if (latest && ["building", "deploying", "running", "failed"].includes(latest.status)) {
|
|
136
|
+
buildId = latest.id;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (buildId) {
|
|
143
|
+
await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
|
|
144
|
+
if (event === "done" || event === "error") {
|
|
145
|
+
if (event === "error") error(`Build failed: ${data}`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(data);
|
|
150
|
+
process.stdout.write((typeof parsed === "string" ? parsed : (parsed.line ?? data)) + "\n");
|
|
151
|
+
} catch {
|
|
152
|
+
process.stdout.write(data + "\n");
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const app = await api.get<{ status: string; domain?: string }>(`/api/apps/${serviceId}`);
|
|
159
|
+
if (app.status === "running") {
|
|
160
|
+
success(`Deployed! ${app.domain ? chalk.cyan(`https://${app.domain}`) : ""}`);
|
|
161
|
+
} else {
|
|
162
|
+
error("Deploy failed");
|
|
163
|
+
}
|
|
42
164
|
});
|
|
43
165
|
|
|
166
|
+
// lizard git status
|
|
44
167
|
git
|
|
45
168
|
.command("status")
|
|
46
|
-
.description("Show
|
|
47
|
-
.
|
|
48
|
-
|
|
169
|
+
.description("Show GitHub connection and repository status")
|
|
170
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
171
|
+
.action(async (opts) => {
|
|
172
|
+
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
49
173
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`/api/projects/${projectId}/services`,
|
|
53
|
-
);
|
|
174
|
+
const [githubStatus, services] = await Promise.all([
|
|
175
|
+
api.get<GitHubStatus>("/api/github/status"),
|
|
176
|
+
api.get<{ apps: any[] }>(withScope(`/api/projects/${projectId}/services`, scope)),
|
|
177
|
+
]);
|
|
54
178
|
|
|
55
|
-
const appsWithRepo = (services.apps || []).filter((a: any) => a.repo);
|
|
179
|
+
const appsWithRepo = (services.apps || []).filter((a: any) => a.repo || a.repoUrl);
|
|
56
180
|
|
|
57
181
|
if (isJSONMode()) {
|
|
58
182
|
printJSON({
|
|
59
|
-
|
|
183
|
+
github: {
|
|
184
|
+
installed: githubStatus.installed,
|
|
185
|
+
installationId: githubStatus.installationId,
|
|
186
|
+
installations: githubStatus.installations ?? [],
|
|
187
|
+
manageUrl: githubStatus.manageUrl,
|
|
188
|
+
},
|
|
60
189
|
apps: appsWithRepo.map((a: any) => ({
|
|
61
190
|
name: a.name,
|
|
62
|
-
repo: a.repo,
|
|
191
|
+
repo: a.repo || a.repoUrl,
|
|
63
192
|
branch: a.branch,
|
|
64
193
|
})),
|
|
65
194
|
});
|
|
66
195
|
return;
|
|
67
196
|
}
|
|
68
197
|
|
|
198
|
+
// GitHub App status
|
|
199
|
+
if (githubStatus.installed) {
|
|
200
|
+
info(`GitHub App: ${chalk.green("connected")}`);
|
|
201
|
+
const installs = githubStatus.installations ?? [];
|
|
202
|
+
for (const inst of installs) {
|
|
203
|
+
const typeLabel = inst.account.type === "Organization" ? "org" : "user";
|
|
204
|
+
const repoLabel = inst.repoCount !== null
|
|
205
|
+
? chalk.dim(`${inst.repoCount} repos${inst.privateCount ? `, ${inst.privateCount} private` : ""}`)
|
|
206
|
+
: "";
|
|
207
|
+
info(` ${chalk.bold(inst.account.login)} ${chalk.dim(`(${typeLabel}, installation #${inst.id})`)} ${repoLabel}`);
|
|
208
|
+
}
|
|
209
|
+
const manageUrl = githubStatus.manageUrl ?? `https://github.com/apps/lizard-app/installations/select_target`;
|
|
210
|
+
info(chalk.dim(` Manage: ${link(manageUrl)}`));
|
|
211
|
+
} else {
|
|
212
|
+
info(`GitHub App: ${chalk.yellow("not connected")} ${chalk.dim("→ run `lizard git connect`")}`);
|
|
213
|
+
if (githubStatus.error) {
|
|
214
|
+
info(chalk.dim(` (${githubStatus.error})`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Connected repos
|
|
69
219
|
if (appsWithRepo.length === 0) {
|
|
70
|
-
|
|
220
|
+
info(chalk.dim("\nNo repositories connected to this project."));
|
|
71
221
|
return;
|
|
72
222
|
}
|
|
73
223
|
|
|
224
|
+
info("");
|
|
74
225
|
for (const app of appsWithRepo) {
|
|
75
|
-
|
|
76
|
-
`${chalk.bold(app.name)}: ${chalk.cyan(app.repo)} (${app.branch || "main"})`,
|
|
226
|
+
info(
|
|
227
|
+
`${chalk.bold(app.name)}: ${chalk.cyan(app.repo || app.repoUrl)} ${chalk.dim(`(${app.branch || "main"})`)}`,
|
|
77
228
|
);
|
|
78
229
|
}
|
|
79
230
|
});
|
|
80
231
|
}
|
|
232
|
+
|
|
233
|
+
function pressEnter(question: string): Promise<void> {
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
236
|
+
rl.question(question, () => {
|
|
237
|
+
rl.close();
|
|
238
|
+
resolve();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|