@lizard-build/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +72 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +117 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +71 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +120 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/destroy.d.ts +2 -0
- package/dist/commands/destroy.js +51 -0
- package/dist/commands/destroy.js.map +1 -0
- package/dist/commands/git.d.ts +2 -0
- package/dist/commands/git.js +67 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +107 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +50 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +92 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +16 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +28 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/ps.d.ts +2 -0
- package/dist/commands/ps.js +52 -0
- package/dist/commands/ps.js.map +1 -0
- package/dist/commands/redeploy.d.ts +2 -0
- package/dist/commands/redeploy.js +69 -0
- package/dist/commands/redeploy.js.map +1 -0
- package/dist/commands/regions.d.ts +2 -0
- package/dist/commands/regions.js +23 -0
- package/dist/commands/regions.js.map +1 -0
- package/dist/commands/restart.d.ts +2 -0
- package/dist/commands/restart.js +21 -0
- package/dist/commands/restart.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +33 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +138 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +41 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +37 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +21 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +19 -0
- package/dist/lib/api.js +114 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/auth.d.ts +23 -0
- package/dist/lib/auth.js +89 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config.d.ts +23 -0
- package/dist/lib/config.js +77 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/format.d.ts +11 -0
- package/dist/lib/format.js +86 -0
- package/dist/lib/format.js.map +1 -0
- package/install.sh +59 -0
- package/package.json +35 -0
- package/src/commands/add.ts +100 -0
- package/src/commands/connect.ts +145 -0
- package/src/commands/context.ts +93 -0
- package/src/commands/deploy.ts +153 -0
- package/src/commands/destroy.ts +51 -0
- package/src/commands/git.ts +80 -0
- package/src/commands/init.ts +139 -0
- package/src/commands/link.ts +63 -0
- package/src/commands/login.ts +158 -0
- package/src/commands/logout.ts +17 -0
- package/src/commands/logs.ts +104 -0
- package/src/commands/open.ts +17 -0
- package/src/commands/projects.ts +45 -0
- package/src/commands/ps.ts +80 -0
- package/src/commands/redeploy.ts +74 -0
- package/src/commands/regions.ts +38 -0
- package/src/commands/restart.ts +24 -0
- package/src/commands/run.ts +43 -0
- package/src/commands/secrets.ts +175 -0
- package/src/commands/status.ts +65 -0
- package/src/commands/update.ts +44 -0
- package/src/commands/version.ts +37 -0
- package/src/commands/whoami.ts +27 -0
- package/src/index.ts +168 -0
- package/src/lib/api.ts +134 -0
- package/src/lib/auth.ts +113 -0
- package/src/lib/config.ts +93 -0
- package/src/lib/format.ts +95 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,100 @@
|
|
|
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 {
|
|
7
|
+
success,
|
|
8
|
+
info,
|
|
9
|
+
isJSONMode,
|
|
10
|
+
printJSON,
|
|
11
|
+
isTTY,
|
|
12
|
+
table,
|
|
13
|
+
} from "../lib/format.js";
|
|
14
|
+
|
|
15
|
+
const CATALOG = [
|
|
16
|
+
{ name: "postgres", label: "PostgreSQL", description: "Relational database" },
|
|
17
|
+
{ name: "redis", label: "Redis", description: "In-memory key-value store" },
|
|
18
|
+
{ name: "mysql", label: "MySQL", description: "Relational database" },
|
|
19
|
+
{ name: "mongodb", label: "MongoDB", description: "Document database" },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export function registerAdd(program: Command) {
|
|
23
|
+
program
|
|
24
|
+
.command("add")
|
|
25
|
+
.argument("[name]", "Service name from catalog (postgres, redis, mysql, mongodb)")
|
|
26
|
+
.description("Add a service to the project")
|
|
27
|
+
.option("--list", "Show available services")
|
|
28
|
+
.option("--region <region>", "Region for the service")
|
|
29
|
+
.action(async (name: string | undefined, opts) => {
|
|
30
|
+
// Show catalog
|
|
31
|
+
if (opts.list || (!name && !isTTY())) {
|
|
32
|
+
if (isJSONMode()) {
|
|
33
|
+
printJSON(CATALOG);
|
|
34
|
+
} else {
|
|
35
|
+
table(
|
|
36
|
+
["Name", "Description"],
|
|
37
|
+
CATALOG.map((c) => [c.name, c.description]),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Interactive selection
|
|
44
|
+
if (!name) {
|
|
45
|
+
const selected = await p.select({
|
|
46
|
+
message: "Select service to add",
|
|
47
|
+
options: CATALOG.map((c) => ({
|
|
48
|
+
value: c.name,
|
|
49
|
+
label: c.label,
|
|
50
|
+
hint: c.description,
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
if (p.isCancel(selected)) process.exit(5);
|
|
54
|
+
name = selected as string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate name is in catalog
|
|
58
|
+
const catalogEntry = CATALOG.find((c) => c.name === name);
|
|
59
|
+
if (!catalogEntry) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unknown service "${name}". Available: ${CATALOG.map((c) => c.name).join(", ")}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
66
|
+
const region = opts.region || program.opts().region;
|
|
67
|
+
|
|
68
|
+
info(`Adding ${chalk.cyan(catalogEntry.label)}...`);
|
|
69
|
+
|
|
70
|
+
const addon = await api.post<{
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
addonType: string;
|
|
74
|
+
status: string;
|
|
75
|
+
hostname?: string;
|
|
76
|
+
connectionString?: string;
|
|
77
|
+
envVars?: Record<string, string>;
|
|
78
|
+
}>(`/api/projects/${projectId}/addons`, {
|
|
79
|
+
addonType: name,
|
|
80
|
+
region,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (isJSONMode()) {
|
|
84
|
+
printJSON(addon);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
success(`${catalogEntry.label} added`);
|
|
89
|
+
|
|
90
|
+
if (addon.hostname) {
|
|
91
|
+
info(` Host: ${chalk.cyan(addon.hostname)}`);
|
|
92
|
+
}
|
|
93
|
+
if (addon.envVars) {
|
|
94
|
+
info(chalk.dim("\n Environment variables added to project:"));
|
|
95
|
+
for (const [key, val] of Object.entries(addon.envVars)) {
|
|
96
|
+
info(` ${chalk.bold(key)}=${chalk.dim(val)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { api } from "../lib/api.js";
|
|
6
|
+
import { resolveProjectId } from "../lib/config.js";
|
|
7
|
+
import { info, isJSONMode, printJSON, isTTY } from "../lib/format.js";
|
|
8
|
+
|
|
9
|
+
interface Addon {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
addonType: string;
|
|
13
|
+
status: string;
|
|
14
|
+
hostname?: string;
|
|
15
|
+
config?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CLIENT_COMMANDS: Record<string, string> = {
|
|
19
|
+
postgres: "psql",
|
|
20
|
+
mysql: "mysql",
|
|
21
|
+
mongodb: "mongosh",
|
|
22
|
+
redis: "redis-cli",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function registerConnect(program: Command) {
|
|
26
|
+
program
|
|
27
|
+
.command("connect")
|
|
28
|
+
.argument("[service]", "Service type or ID (postgres, redis, etc.)")
|
|
29
|
+
.description("Connect to a managed service")
|
|
30
|
+
.option("--url", "Print connection string without connecting")
|
|
31
|
+
.action(async (service: string | undefined, opts) => {
|
|
32
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
33
|
+
|
|
34
|
+
// Get addons
|
|
35
|
+
const addons = await api.get<Addon[]>(
|
|
36
|
+
`/api/projects/${projectId}/addons`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (addons.length === 0) {
|
|
40
|
+
throw new Error("No managed services in this project. Use `lizard add`.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let addon: Addon | undefined;
|
|
44
|
+
|
|
45
|
+
if (service) {
|
|
46
|
+
// Match by type or ID
|
|
47
|
+
addon =
|
|
48
|
+
addons.find((a) => a.addonType === service) ||
|
|
49
|
+
addons.find((a) => a.id === service) ||
|
|
50
|
+
addons.find((a) => a.name === service);
|
|
51
|
+
} else if (addons.length === 1) {
|
|
52
|
+
addon = addons[0];
|
|
53
|
+
} else {
|
|
54
|
+
if (!isTTY()) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Multiple services found. Specify one: " +
|
|
57
|
+
addons.map((a) => a.addonType || a.name).join(", "),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const selected = await p.select({
|
|
61
|
+
message: "Select service to connect to",
|
|
62
|
+
options: addons.map((a) => ({
|
|
63
|
+
value: a.id,
|
|
64
|
+
label: a.name || a.addonType,
|
|
65
|
+
hint: a.addonType,
|
|
66
|
+
})),
|
|
67
|
+
});
|
|
68
|
+
if (p.isCancel(selected)) process.exit(5);
|
|
69
|
+
addon = addons.find((a) => a.id === selected);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!addon) {
|
|
73
|
+
throw new Error(`Service "${service}" not found`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (addon.status !== "running") {
|
|
77
|
+
throw new Error(`Service is ${addon.status}, not running`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build connection string from secrets
|
|
81
|
+
const secrets = await api.get<Array<{ key: string; value: string }>>(
|
|
82
|
+
`/api/projects/${projectId}/secrets`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const connString = findConnectionString(addon.addonType, secrets);
|
|
86
|
+
|
|
87
|
+
if (opts.url || isJSONMode()) {
|
|
88
|
+
if (isJSONMode()) {
|
|
89
|
+
printJSON({ type: addon.addonType, connectionString: connString });
|
|
90
|
+
} else {
|
|
91
|
+
console.log(connString || "Connection string not found in secrets");
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!connString) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Connection string not found in project secrets. Check `lizard secret list --show`.",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Connect using native client
|
|
103
|
+
const clientCmd = CLIENT_COMMANDS[addon.addonType];
|
|
104
|
+
if (!clientCmd) {
|
|
105
|
+
info(`Connection string: ${connString}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
info(chalk.dim(`Connecting via ${clientCmd}...\n`));
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (addon.addonType === "postgres") {
|
|
113
|
+
execSync(`${clientCmd} "${connString}"`, { stdio: "inherit" });
|
|
114
|
+
} else if (addon.addonType === "redis") {
|
|
115
|
+
// redis-cli -u redis://...
|
|
116
|
+
execSync(`${clientCmd} -u "${connString}"`, { stdio: "inherit" });
|
|
117
|
+
} else if (addon.addonType === "mysql") {
|
|
118
|
+
execSync(`${clientCmd} "${connString}"`, { stdio: "inherit" });
|
|
119
|
+
} else if (addon.addonType === "mongodb") {
|
|
120
|
+
execSync(`${clientCmd} "${connString}"`, { stdio: "inherit" });
|
|
121
|
+
}
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
process.exit(err.status || 1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findConnectionString(
|
|
129
|
+
type: string,
|
|
130
|
+
secrets: Array<{ key: string; value: string }>,
|
|
131
|
+
): string | null {
|
|
132
|
+
const envKeys: Record<string, string[]> = {
|
|
133
|
+
postgres: ["DATABASE_URL", "POSTGRES_URL", "PG_URL"],
|
|
134
|
+
mysql: ["MYSQL_URL", "DATABASE_URL"],
|
|
135
|
+
mongodb: ["MONGODB_URL", "MONGO_URL"],
|
|
136
|
+
redis: ["REDIS_URL"],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const keys = envKeys[type] || [];
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
const s = secrets.find((s) => s.key === key);
|
|
142
|
+
if (s) return s.value;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { api } from "../lib/api.js";
|
|
4
|
+
import { resolveProjectId, findProjectConfig } from "../lib/config.js";
|
|
5
|
+
import { isJSONMode, printJSON, statusColor, table } from "../lib/format.js";
|
|
6
|
+
|
|
7
|
+
export function registerContext(program: Command) {
|
|
8
|
+
program
|
|
9
|
+
.command("context")
|
|
10
|
+
.description("Show full project context (optimized for AI agents)")
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
13
|
+
const config = findProjectConfig();
|
|
14
|
+
|
|
15
|
+
const [project, services, secrets] = await Promise.all([
|
|
16
|
+
api.get<{ id: string; name: string; slug: string }>(
|
|
17
|
+
`/api/projects/${projectId}`,
|
|
18
|
+
),
|
|
19
|
+
api.get<{ apps: any[]; addons: any[] }>(
|
|
20
|
+
`/api/projects/${projectId}/services`,
|
|
21
|
+
),
|
|
22
|
+
api.get<Array<{ key: string; value: string }>>(
|
|
23
|
+
`/api/projects/${projectId}/secrets`,
|
|
24
|
+
).catch(() => []),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const context = {
|
|
28
|
+
project: {
|
|
29
|
+
id: project.id,
|
|
30
|
+
name: project.name,
|
|
31
|
+
slug: project.slug,
|
|
32
|
+
},
|
|
33
|
+
environment: config?.environment || "production",
|
|
34
|
+
apps: (services.apps || []).map((a: any) => ({
|
|
35
|
+
id: a.id,
|
|
36
|
+
name: a.name,
|
|
37
|
+
status: a.status,
|
|
38
|
+
domain: a.domain,
|
|
39
|
+
repo: a.repo,
|
|
40
|
+
branch: a.branch,
|
|
41
|
+
cpuLimit: a.cpuLimit,
|
|
42
|
+
memoryLimit: a.memoryLimit,
|
|
43
|
+
})),
|
|
44
|
+
addons: (services.addons || []).map((a: any) => ({
|
|
45
|
+
id: a.id,
|
|
46
|
+
name: a.name,
|
|
47
|
+
type: a.addonType,
|
|
48
|
+
status: a.status,
|
|
49
|
+
hostname: a.hostname,
|
|
50
|
+
})),
|
|
51
|
+
secrets: secrets.map((s) => s.key), // names only, not values
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Always JSON for pipe, since this is designed for AI agents
|
|
55
|
+
if (isJSONMode() || !process.stdout.isTTY) {
|
|
56
|
+
printJSON(context);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Human-readable
|
|
61
|
+
console.log(chalk.bold(context.project.name) + chalk.dim(` (${context.project.id})`));
|
|
62
|
+
console.log(chalk.dim(`Environment: ${context.environment}`));
|
|
63
|
+
console.log();
|
|
64
|
+
|
|
65
|
+
if (context.apps.length > 0) {
|
|
66
|
+
console.log(chalk.bold("Apps:"));
|
|
67
|
+
table(
|
|
68
|
+
["Name", "Status", "Domain"],
|
|
69
|
+
context.apps.map((a) => [
|
|
70
|
+
a.name,
|
|
71
|
+
statusColor(a.status),
|
|
72
|
+
a.domain ? `https://${a.domain}` : "—",
|
|
73
|
+
]),
|
|
74
|
+
);
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (context.addons.length > 0) {
|
|
79
|
+
console.log(chalk.bold("Addons:"));
|
|
80
|
+
table(
|
|
81
|
+
["Name", "Type", "Status", "Host"],
|
|
82
|
+
context.addons.map((a) => [a.name, a.type, statusColor(a.status), a.hostname || "—"]),
|
|
83
|
+
);
|
|
84
|
+
console.log();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (context.secrets.length > 0) {
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.bold("Secrets:") + " " + context.secrets.join(", "),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { api, streamSSE } from "../lib/api.js";
|
|
5
|
+
import { resolveProjectId } from "../lib/config.js";
|
|
6
|
+
import {
|
|
7
|
+
success,
|
|
8
|
+
info,
|
|
9
|
+
error,
|
|
10
|
+
isJSONMode,
|
|
11
|
+
printJSON,
|
|
12
|
+
statusColor,
|
|
13
|
+
table,
|
|
14
|
+
} from "../lib/format.js";
|
|
15
|
+
|
|
16
|
+
interface App {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
status: string;
|
|
20
|
+
domain?: string;
|
|
21
|
+
repo?: string;
|
|
22
|
+
branch?: string;
|
|
23
|
+
builds?: Array<{ id: string; status: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerDeploy(program: Command) {
|
|
27
|
+
program
|
|
28
|
+
.command("deploy")
|
|
29
|
+
.description("Deploy the current project")
|
|
30
|
+
.option("--detach", "Run in background without streaming logs")
|
|
31
|
+
.option("--region <region>", "Region for deployment")
|
|
32
|
+
.action(async (opts) => {
|
|
33
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
34
|
+
|
|
35
|
+
// Check if there's already an app, if so redeploy
|
|
36
|
+
const services = await api.get<{ apps: App[] }>(
|
|
37
|
+
`/api/projects/${projectId}/services`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (services.apps && services.apps.length > 0) {
|
|
41
|
+
// Redeploy existing app
|
|
42
|
+
const app = services.apps[0];
|
|
43
|
+
info(`Redeploying ${chalk.bold(app.name)}...`);
|
|
44
|
+
|
|
45
|
+
await api.post(`/api/apps/${app.id}/redeploy`);
|
|
46
|
+
|
|
47
|
+
if (opts.detach) {
|
|
48
|
+
if (isJSONMode()) {
|
|
49
|
+
printJSON({ appId: app.id, status: "deploying" });
|
|
50
|
+
} else {
|
|
51
|
+
success(`Redeploy started for ${app.name}`);
|
|
52
|
+
info(chalk.dim(` Check status: lizard deploy status ${app.id}`));
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Stream build logs
|
|
58
|
+
await streamBuildLogs(app.id);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// First deploy — create app
|
|
63
|
+
// TODO: detect repo from git remote, create app from repo
|
|
64
|
+
throw new Error(
|
|
65
|
+
"First deploy requires an existing app. Create one from the dashboard or use `lizard init` + push via git.",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command("deploy-status")
|
|
71
|
+
.argument("<id>", "App or deploy ID")
|
|
72
|
+
.description("Show deployment status")
|
|
73
|
+
.action(async (id) => {
|
|
74
|
+
const app = await api.get<App>(`/api/apps/${id}`);
|
|
75
|
+
|
|
76
|
+
if (isJSONMode()) {
|
|
77
|
+
printJSON(app);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`${chalk.bold(app.name)} ${statusColor(app.status)}`);
|
|
82
|
+
if (app.domain) console.log(` URL: ${chalk.cyan(`https://${app.domain}`)}`);
|
|
83
|
+
if (app.builds?.length) {
|
|
84
|
+
const latest = app.builds[0];
|
|
85
|
+
console.log(` Latest build: ${statusColor(latest.status)}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function streamBuildLogs(appId: string) {
|
|
91
|
+
// Wait a moment for the build to start
|
|
92
|
+
const spinner = ora("Waiting for build...").start();
|
|
93
|
+
|
|
94
|
+
// Poll until we get a build ID
|
|
95
|
+
let buildId: string | null = null;
|
|
96
|
+
for (let i = 0; i < 30; i++) {
|
|
97
|
+
await sleep(2000);
|
|
98
|
+
try {
|
|
99
|
+
const app = await api.get<App>(`/api/apps/${appId}`);
|
|
100
|
+
if (app.builds?.length) {
|
|
101
|
+
const latest = app.builds[0];
|
|
102
|
+
if (["building", "deploying", "running", "failed"].includes(latest.status)) {
|
|
103
|
+
buildId = latest.id;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
spinner.stop();
|
|
110
|
+
|
|
111
|
+
if (!buildId) {
|
|
112
|
+
info(chalk.dim("No build found. Check `lizard deploy status <id>` for status."));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
info(chalk.dim("Streaming build logs...\n"));
|
|
117
|
+
|
|
118
|
+
await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
|
|
119
|
+
if (event === "done" || event === "error") {
|
|
120
|
+
if (event === "error") {
|
|
121
|
+
error(`Build failed: ${data}`);
|
|
122
|
+
} else {
|
|
123
|
+
success("Build complete");
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Print log line
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(data);
|
|
131
|
+
if (parsed.line) {
|
|
132
|
+
process.stdout.write(parsed.line + "\n");
|
|
133
|
+
} else if (typeof parsed === "string") {
|
|
134
|
+
process.stdout.write(parsed + "\n");
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
process.stdout.write(data + "\n");
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Check final status
|
|
143
|
+
const app = await api.get<App>(`/api/apps/${appId}`);
|
|
144
|
+
if (app.status === "running") {
|
|
145
|
+
success(`Deployed! ${app.domain ? chalk.cyan(`https://${app.domain}`) : ""}`);
|
|
146
|
+
} else if (app.status === "failed") {
|
|
147
|
+
error("Deploy failed. Check logs with `lizard logs --build`");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sleep(ms: number) {
|
|
152
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
153
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { success, isJSONMode, printJSON, isTTY } from "../lib/format.js";
|
|
7
|
+
|
|
8
|
+
export function registerDestroy(program: Command) {
|
|
9
|
+
program
|
|
10
|
+
.command("destroy")
|
|
11
|
+
.argument("<id>", "Service ID to destroy")
|
|
12
|
+
.description("Destroy a service (irreversible)")
|
|
13
|
+
.action(async (id: string) => {
|
|
14
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
15
|
+
const yes = program.opts().yes;
|
|
16
|
+
|
|
17
|
+
// Confirm
|
|
18
|
+
if (!yes) {
|
|
19
|
+
if (!isTTY()) {
|
|
20
|
+
throw new Error("Use -y to confirm destruction in non-interactive mode");
|
|
21
|
+
}
|
|
22
|
+
const confirm = await p.confirm({
|
|
23
|
+
message: `Destroy service ${chalk.bold(id)}? This is irreversible.`,
|
|
24
|
+
});
|
|
25
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
26
|
+
process.exit(5);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Try as app first, then as addon
|
|
31
|
+
try {
|
|
32
|
+
await api.delete(`/api/apps/${id}`);
|
|
33
|
+
if (isJSONMode()) {
|
|
34
|
+
printJSON({ id, status: "destroyed", type: "app" });
|
|
35
|
+
} else {
|
|
36
|
+
success(`Service ${id} destroyed`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
if (err.status !== 404) throw err;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try as addon
|
|
44
|
+
await api.delete(`/api/projects/${projectId}/addons/${id}`);
|
|
45
|
+
if (isJSONMode()) {
|
|
46
|
+
printJSON({ id, status: "destroyed", type: "addon" });
|
|
47
|
+
} else {
|
|
48
|
+
success(`Service ${id} destroyed`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { api } from "../lib/api.js";
|
|
4
|
+
import { resolveProjectId } from "../lib/config.js";
|
|
5
|
+
import { success, info, isJSONMode, printJSON } from "../lib/format.js";
|
|
6
|
+
|
|
7
|
+
export function registerGit(program: Command) {
|
|
8
|
+
const git = program
|
|
9
|
+
.command("git")
|
|
10
|
+
.description("Git integration");
|
|
11
|
+
|
|
12
|
+
git
|
|
13
|
+
.command("connect")
|
|
14
|
+
.argument("<repo>", "GitHub repository (user/repo)")
|
|
15
|
+
.description("Connect Git repository for auto-deploy")
|
|
16
|
+
.option("--branch <name>", "Branch for auto-deploy", "main")
|
|
17
|
+
.action(async (repo: string, opts) => {
|
|
18
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
19
|
+
|
|
20
|
+
// This requires a server endpoint for programmatic webhook setup
|
|
21
|
+
// For now, guide the user to use the dashboard
|
|
22
|
+
if (isJSONMode()) {
|
|
23
|
+
printJSON({
|
|
24
|
+
error: "not_implemented",
|
|
25
|
+
message: "Git connect via CLI requires server endpoint. Use the dashboard.",
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
info(`To connect ${chalk.cyan(repo)} for auto-deploy:`);
|
|
29
|
+
info(` 1. Open your project on lizard.build`);
|
|
30
|
+
info(` 2. Go to Settings → Git Integration`);
|
|
31
|
+
info(` 3. Connect ${repo} (branch: ${opts.branch})`);
|
|
32
|
+
info("");
|
|
33
|
+
info(chalk.dim("CLI git connect will be available in a future update."));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
git
|
|
38
|
+
.command("disconnect")
|
|
39
|
+
.description("Disconnect Git auto-deploy")
|
|
40
|
+
.action(async () => {
|
|
41
|
+
info(chalk.dim("Git disconnect via CLI will be available in a future update."));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
git
|
|
45
|
+
.command("status")
|
|
46
|
+
.description("Show Git integration status")
|
|
47
|
+
.action(async () => {
|
|
48
|
+
const projectId = resolveProjectId(program.opts().project);
|
|
49
|
+
|
|
50
|
+
// Get apps to check for repo info
|
|
51
|
+
const services = await api.get<{ apps: any[] }>(
|
|
52
|
+
`/api/projects/${projectId}/services`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const appsWithRepo = (services.apps || []).filter((a: any) => a.repo);
|
|
56
|
+
|
|
57
|
+
if (isJSONMode()) {
|
|
58
|
+
printJSON({
|
|
59
|
+
connected: appsWithRepo.length > 0,
|
|
60
|
+
apps: appsWithRepo.map((a: any) => ({
|
|
61
|
+
name: a.name,
|
|
62
|
+
repo: a.repo,
|
|
63
|
+
branch: a.branch,
|
|
64
|
+
})),
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (appsWithRepo.length === 0) {
|
|
70
|
+
console.log("No Git repositories connected.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const app of appsWithRepo) {
|
|
75
|
+
console.log(
|
|
76
|
+
`${chalk.bold(app.name)}: ${chalk.cyan(app.repo)} (${app.branch || "main"})`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|