gzero-cli 0.1.0-preview.1 → 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/LICENSE +37 -0
- package/README.md +116 -35
- package/dist/index.js +794 -109
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -25,15 +25,185 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
mod
|
|
26
26
|
));
|
|
27
27
|
|
|
28
|
-
// ../
|
|
28
|
+
// ../shared/dist/deploy-readiness.js
|
|
29
|
+
var require_deploy_readiness = __commonJS({
|
|
30
|
+
"../shared/dist/deploy-readiness.js"(exports) {
|
|
31
|
+
"use strict";
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.evaluateDeployReadiness = evaluateDeployReadiness;
|
|
34
|
+
function evaluateDeployReadiness(input) {
|
|
35
|
+
const blockers = [];
|
|
36
|
+
const warnings = [];
|
|
37
|
+
if (!input.framework || input.framework === "unknown") {
|
|
38
|
+
blockers.push({
|
|
39
|
+
id: "framework",
|
|
40
|
+
level: "error",
|
|
41
|
+
message: "Could not detect a supported framework",
|
|
42
|
+
recommendation: "Use Vite, Astro (static), Next.js static export, or plain HTML."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!input.buildCommand && input.framework !== "plain-html") {
|
|
46
|
+
blockers.push({
|
|
47
|
+
id: "build",
|
|
48
|
+
level: "error",
|
|
49
|
+
message: "No build command configured",
|
|
50
|
+
recommendation: 'Add a "build" script to package.json or run scan again.'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!input.outputDir && input.framework !== "plain-html") {
|
|
54
|
+
blockers.push({
|
|
55
|
+
id: "output",
|
|
56
|
+
level: "error",
|
|
57
|
+
message: "No output directory configured",
|
|
58
|
+
recommendation: "Run scan again to detect the build output folder."
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (input.secretsFound) {
|
|
62
|
+
warnings.push({
|
|
63
|
+
id: "secrets",
|
|
64
|
+
level: "warning",
|
|
65
|
+
message: "An env file looks committed to your repo",
|
|
66
|
+
recommendation: "We never read it: app settings are injected at build time. For safety, remove it from the repo and add it to .gitignore."
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (const envVar of input.requiredEnvVars) {
|
|
70
|
+
if (!input.configuredEnvKeys.includes(envVar)) {
|
|
71
|
+
warnings.push({
|
|
72
|
+
id: `env-${envVar}`,
|
|
73
|
+
level: "warning",
|
|
74
|
+
message: `${envVar} is not set yet`,
|
|
75
|
+
recommendation: `Paste your .env to add ${envVar}, or deploy now and add it later.`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (input.checks) {
|
|
80
|
+
for (const check of input.checks) {
|
|
81
|
+
if (check.id.startsWith("env-"))
|
|
82
|
+
continue;
|
|
83
|
+
const isFrameworkConfigError = check.id === "framework-config" && check.level === "error";
|
|
84
|
+
if (check.level !== "warning" && !isFrameworkConfigError)
|
|
85
|
+
continue;
|
|
86
|
+
warnings.push({
|
|
87
|
+
id: check.id,
|
|
88
|
+
level: "warning",
|
|
89
|
+
message: check.message,
|
|
90
|
+
recommendation: check.recommendation
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
canDeploy: blockers.length === 0,
|
|
96
|
+
blockers,
|
|
97
|
+
warnings,
|
|
98
|
+
technicalNotes: input.technicalNotes ?? []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ../shared/dist/index.js
|
|
29
105
|
var require_dist = __commonJS({
|
|
106
|
+
"../shared/dist/index.js"(exports) {
|
|
107
|
+
"use strict";
|
|
108
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
109
|
+
exports.PLAN_LIMITS = exports.PLAN_LIMITS_FALLBACK = exports.API_RUNTIME_UNAVAILABLE_MESSAGE = exports.API_RUNTIME_AVAILABLE = exports.evaluateDeployReadiness = void 0;
|
|
110
|
+
exports.parseDotEnv = parseDotEnv2;
|
|
111
|
+
function parseDotEnv2(raw) {
|
|
112
|
+
const entries = [];
|
|
113
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
115
|
+
let line = rawLine.trim();
|
|
116
|
+
if (!line || line.startsWith("#"))
|
|
117
|
+
continue;
|
|
118
|
+
if (line.startsWith("export "))
|
|
119
|
+
line = line.slice("export ".length).trim();
|
|
120
|
+
const eq = line.indexOf("=");
|
|
121
|
+
if (eq === -1)
|
|
122
|
+
continue;
|
|
123
|
+
const key = line.slice(0, eq).trim();
|
|
124
|
+
if (!key)
|
|
125
|
+
continue;
|
|
126
|
+
let value = line.slice(eq + 1).trim();
|
|
127
|
+
const quoted = value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"));
|
|
128
|
+
if (quoted) {
|
|
129
|
+
value = value.slice(1, -1);
|
|
130
|
+
} else {
|
|
131
|
+
const inlineComment = value.indexOf(" #");
|
|
132
|
+
if (inlineComment !== -1)
|
|
133
|
+
value = value.slice(0, inlineComment).trim();
|
|
134
|
+
}
|
|
135
|
+
const existing = indexByKey.get(key);
|
|
136
|
+
if (existing !== void 0) {
|
|
137
|
+
entries[existing] = { key, value };
|
|
138
|
+
} else {
|
|
139
|
+
indexByKey.set(key, entries.length);
|
|
140
|
+
entries.push({ key, value });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return entries;
|
|
144
|
+
}
|
|
145
|
+
var deploy_readiness_1 = require_deploy_readiness();
|
|
146
|
+
Object.defineProperty(exports, "evaluateDeployReadiness", { enumerable: true, get: function() {
|
|
147
|
+
return deploy_readiness_1.evaluateDeployReadiness;
|
|
148
|
+
} });
|
|
149
|
+
exports.API_RUNTIME_AVAILABLE = true;
|
|
150
|
+
exports.API_RUNTIME_UNAVAILABLE_MESSAGE = "API runtime is coming soon. ExpressoTS backend deploy is not available yet.";
|
|
151
|
+
exports.PLAN_LIMITS_FALLBACK = {
|
|
152
|
+
trial: {
|
|
153
|
+
maxStacks: 3,
|
|
154
|
+
maxBuilds: null,
|
|
155
|
+
buildsPeriod: "trial_lifetime",
|
|
156
|
+
maxBandwidthBytes: 5368709120,
|
|
157
|
+
maxCustomDomains: 0,
|
|
158
|
+
trialDurationDays: null,
|
|
159
|
+
stacksLabel: "3 preview sites",
|
|
160
|
+
buildsLabel: "Unlimited preview deploys",
|
|
161
|
+
trafficLabel: "About 2,500 visits per site",
|
|
162
|
+
domainsLabel: null,
|
|
163
|
+
planSummary: "3 free preview sites on a GroundZero URL. Go live with your own domain anytime."
|
|
164
|
+
},
|
|
165
|
+
solo: {
|
|
166
|
+
maxStacks: 3,
|
|
167
|
+
maxBuilds: null,
|
|
168
|
+
buildsPeriod: "billing_month",
|
|
169
|
+
maxBandwidthBytes: 53687091200,
|
|
170
|
+
maxCustomDomains: 1,
|
|
171
|
+
trialDurationDays: null,
|
|
172
|
+
stacksLabel: "Up to 3 products",
|
|
173
|
+
buildsLabel: "Unlimited deploys (fair use)",
|
|
174
|
+
trafficLabel: "About 25,000 visits per month",
|
|
175
|
+
domainsLabel: "Your own domain with SSL",
|
|
176
|
+
planSummary: "Go live with your own domain, SSL, and rollback. First product included; add more for $3/mo."
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
exports.PLAN_LIMITS = {
|
|
180
|
+
trial: {
|
|
181
|
+
apps: exports.PLAN_LIMITS_FALLBACK.trial.maxStacks,
|
|
182
|
+
buildsPerMonth: exports.PLAN_LIMITS_FALLBACK.trial.maxBuilds,
|
|
183
|
+
bandwidthGb: 5,
|
|
184
|
+
customDomains: 0,
|
|
185
|
+
previewExpiryDays: null
|
|
186
|
+
},
|
|
187
|
+
solo: {
|
|
188
|
+
apps: exports.PLAN_LIMITS_FALLBACK.solo.maxStacks,
|
|
189
|
+
buildsPerMonth: exports.PLAN_LIMITS_FALLBACK.solo.maxBuilds,
|
|
190
|
+
bandwidthGb: 50,
|
|
191
|
+
customDomains: 1,
|
|
192
|
+
previewExpiryDays: null
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ../sdk/dist/index.js
|
|
199
|
+
var require_dist2 = __commonJS({
|
|
30
200
|
"../sdk/dist/index.js"(exports) {
|
|
31
201
|
"use strict";
|
|
32
202
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.GzeroApiError = exports.DEFAULT_BASE_URL = void 0;
|
|
34
|
-
exports.createClient =
|
|
203
|
+
exports.parseDotEnv = exports.GzeroApiError = exports.DEFAULT_BASE_URL = void 0;
|
|
204
|
+
exports.createClient = createClient3;
|
|
35
205
|
exports.DEFAULT_BASE_URL = "https://api.gzer0.app";
|
|
36
|
-
var
|
|
206
|
+
var GzeroApiError3 = class extends Error {
|
|
37
207
|
status;
|
|
38
208
|
code;
|
|
39
209
|
constructor(message, status, code) {
|
|
@@ -43,12 +213,12 @@ var require_dist = __commonJS({
|
|
|
43
213
|
this.name = "GzeroApiError";
|
|
44
214
|
}
|
|
45
215
|
};
|
|
46
|
-
exports.GzeroApiError =
|
|
216
|
+
exports.GzeroApiError = GzeroApiError3;
|
|
47
217
|
function normalizeBaseUrl(raw) {
|
|
48
218
|
const value = (raw ?? exports.DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
|
|
49
219
|
return value.endsWith("/api") ? value.slice(0, -"/api".length) : value;
|
|
50
220
|
}
|
|
51
|
-
function
|
|
221
|
+
function createClient3(options = {}) {
|
|
52
222
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
53
223
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
54
224
|
let token = options.token;
|
|
@@ -66,7 +236,7 @@ var require_dist = __commonJS({
|
|
|
66
236
|
if (!res.ok) {
|
|
67
237
|
const body = await res.json().catch(() => ({}));
|
|
68
238
|
const detail = body.blockers?.map((b) => b.message).join("; ");
|
|
69
|
-
throw new
|
|
239
|
+
throw new GzeroApiError3(detail ?? body.error ?? `API ${res.status}: ${path}`, res.status, body.error);
|
|
70
240
|
}
|
|
71
241
|
if (res.status === 204)
|
|
72
242
|
return void 0;
|
|
@@ -125,6 +295,11 @@ var require_dist = __commonJS({
|
|
|
125
295
|
method: "POST",
|
|
126
296
|
body: JSON.stringify({ name, description })
|
|
127
297
|
}),
|
|
298
|
+
/** Attach an ExpressoTS API service to a product. Mirrors the web dashboard's API flow. */
|
|
299
|
+
addApiService: (productId, input) => request(`/products/${productId}/services/api`, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
body: JSON.stringify(input)
|
|
302
|
+
}),
|
|
128
303
|
// --- Projects ---
|
|
129
304
|
listProjects: () => request("/projects"),
|
|
130
305
|
getProjectStatus: (projectId) => request(`/projects/${projectId}/status`),
|
|
@@ -186,15 +361,19 @@ var require_dist = __commonJS({
|
|
|
186
361
|
listInvoices: () => request("/billing/invoices")
|
|
187
362
|
};
|
|
188
363
|
}
|
|
364
|
+
var shared_1 = require_dist();
|
|
365
|
+
Object.defineProperty(exports, "parseDotEnv", { enumerable: true, get: function() {
|
|
366
|
+
return shared_1.parseDotEnv;
|
|
367
|
+
} });
|
|
189
368
|
}
|
|
190
369
|
});
|
|
191
370
|
|
|
192
371
|
// src/index.ts
|
|
193
|
-
var
|
|
194
|
-
import {
|
|
372
|
+
var import_sdk3 = __toESM(require_dist2(), 1);
|
|
373
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
195
374
|
import { Command } from "commander";
|
|
196
|
-
import * as
|
|
197
|
-
import
|
|
375
|
+
import * as p3 from "@clack/prompts";
|
|
376
|
+
import pc4 from "picocolors";
|
|
198
377
|
|
|
199
378
|
// src/config.ts
|
|
200
379
|
import { homedir } from "os";
|
|
@@ -239,7 +418,82 @@ async function resolveSettings(flags2) {
|
|
|
239
418
|
};
|
|
240
419
|
}
|
|
241
420
|
|
|
242
|
-
// src/
|
|
421
|
+
// src/theme.ts
|
|
422
|
+
import pc from "picocolors";
|
|
423
|
+
var enabled = pc.isColorSupported;
|
|
424
|
+
var DEEP = [91, 75, 224];
|
|
425
|
+
var VIOLET = [124, 111, 240];
|
|
426
|
+
var BRIGHT = [165, 148, 255];
|
|
427
|
+
function fg([r, g, b]) {
|
|
428
|
+
if (!enabled) return (s) => s;
|
|
429
|
+
const prefix = `\x1B[38;2;${r};${g};${b}m`;
|
|
430
|
+
return (s) => `${prefix}${s}\x1B[39m`;
|
|
431
|
+
}
|
|
432
|
+
var deep = fg(DEEP);
|
|
433
|
+
var violet = fg(VIOLET);
|
|
434
|
+
var bright = fg(BRIGHT);
|
|
435
|
+
function lerp(a, b, t) {
|
|
436
|
+
return Math.round(a + (b - a) * t);
|
|
437
|
+
}
|
|
438
|
+
function gradient(text2, from = VIOLET, to = BRIGHT) {
|
|
439
|
+
if (!enabled) return text2;
|
|
440
|
+
const chars = [...text2];
|
|
441
|
+
const last = Math.max(chars.length - 1, 1);
|
|
442
|
+
const painted = chars.map((ch, i) => {
|
|
443
|
+
const t = i / last;
|
|
444
|
+
const r = lerp(from[0], to[0], t);
|
|
445
|
+
const g = lerp(from[1], to[1], t);
|
|
446
|
+
const b = lerp(from[2], to[2], t);
|
|
447
|
+
return `\x1B[38;2;${r};${g};${b}m${ch}`;
|
|
448
|
+
}).join("");
|
|
449
|
+
return `${painted}\x1B[39m`;
|
|
450
|
+
}
|
|
451
|
+
var brand = {
|
|
452
|
+
deep,
|
|
453
|
+
violet,
|
|
454
|
+
bright,
|
|
455
|
+
/** The wordmark, always gradient + bold. */
|
|
456
|
+
word: () => pc.bold(gradient("GroundZero"))
|
|
457
|
+
};
|
|
458
|
+
var symbol = {
|
|
459
|
+
ok: pc.green("\u2713"),
|
|
460
|
+
err: pc.red("\u2717"),
|
|
461
|
+
warn: pc.yellow("\u25B2"),
|
|
462
|
+
info: violet("\u203A"),
|
|
463
|
+
dot: bright("\u2022"),
|
|
464
|
+
bullet: violet("\u25C6")
|
|
465
|
+
};
|
|
466
|
+
function markLines() {
|
|
467
|
+
return [
|
|
468
|
+
` ${bright("\u257B")}`,
|
|
469
|
+
` ${violet("\u256D\u2500\u256F \u2570\u2500\u256E")}`,
|
|
470
|
+
` ${violet("\u2502 \u2502")}`,
|
|
471
|
+
` ${violet("\u2570\u2500\u2500\u2500\u2500\u2500\u256F")}`
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
function banner(version) {
|
|
475
|
+
const v = version ? pc.dim(`v${version}`) : "";
|
|
476
|
+
const rows = markLines();
|
|
477
|
+
const right = [
|
|
478
|
+
"",
|
|
479
|
+
`${brand.word()} ${v}`,
|
|
480
|
+
pc.dim("Deploy UIs & APIs from your terminal or an AI agent."),
|
|
481
|
+
pc.dim("gzer0.app")
|
|
482
|
+
];
|
|
483
|
+
const lines = rows.map((left, i) => `${left} ${right[i] ?? ""}`.trimEnd());
|
|
484
|
+
return `
|
|
485
|
+
${lines.join("\n")}
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
function lockup(label) {
|
|
489
|
+
return `${bright("\u23FB")} ${brand.word()} ${pc.dim(label)}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/util.ts
|
|
493
|
+
var import_sdk = __toESM(require_dist2(), 1);
|
|
494
|
+
import { spawn } from "child_process";
|
|
495
|
+
import * as p from "@clack/prompts";
|
|
496
|
+
import pc2 from "picocolors";
|
|
243
497
|
function openBrowser(url) {
|
|
244
498
|
const platform = process.platform;
|
|
245
499
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
@@ -255,6 +509,10 @@ function die(message) {
|
|
|
255
509
|
p.cancel(message);
|
|
256
510
|
process.exit(1);
|
|
257
511
|
}
|
|
512
|
+
function fail(err) {
|
|
513
|
+
if (err instanceof import_sdk.GzeroApiError) die(err.message);
|
|
514
|
+
die(err instanceof Error ? err.message : String(err));
|
|
515
|
+
}
|
|
258
516
|
async function client(flags2, requireAuth = true) {
|
|
259
517
|
const settings = await resolveSettings(flags2);
|
|
260
518
|
if (requireAuth && !settings.token) {
|
|
@@ -262,9 +520,12 @@ async function client(flags2, requireAuth = true) {
|
|
|
262
520
|
}
|
|
263
521
|
return (0, import_sdk.createClient)({ baseUrl: settings.baseUrl, token: settings.token });
|
|
264
522
|
}
|
|
265
|
-
function
|
|
266
|
-
if (
|
|
267
|
-
|
|
523
|
+
function statusColor(status) {
|
|
524
|
+
if (status === "live") return pc2.green(status);
|
|
525
|
+
if (status === "failed") return pc2.red(status);
|
|
526
|
+
if (status === "building" || status === "deploying" || status === "pending")
|
|
527
|
+
return pc2.yellow(status);
|
|
528
|
+
return pc2.dim(status);
|
|
268
529
|
}
|
|
269
530
|
async function resolveProject(c, ref) {
|
|
270
531
|
let projects;
|
|
@@ -273,7 +534,7 @@ async function resolveProject(c, ref) {
|
|
|
273
534
|
} catch (err) {
|
|
274
535
|
fail(err);
|
|
275
536
|
}
|
|
276
|
-
if (projects.length === 0) die("No projects yet.
|
|
537
|
+
if (projects.length === 0) die("No projects yet. Run `gzero init` to create one.");
|
|
277
538
|
if (ref) {
|
|
278
539
|
const match = projects.find(
|
|
279
540
|
(proj) => proj.id === ref || proj.slug === ref || proj.name === ref
|
|
@@ -286,24 +547,313 @@ async function resolveProject(c, ref) {
|
|
|
286
547
|
message: "Select a project",
|
|
287
548
|
options: projects.map((proj) => ({
|
|
288
549
|
value: proj.id,
|
|
289
|
-
label: `${proj.name} ${
|
|
550
|
+
label: `${proj.name} ${pc2.dim(`(${proj.slug})`)}`
|
|
290
551
|
}))
|
|
291
552
|
});
|
|
292
553
|
if (p.isCancel(picked)) die("Cancelled.");
|
|
293
554
|
return projects.find((proj) => proj.id === picked);
|
|
294
555
|
}
|
|
556
|
+
|
|
557
|
+
// src/create.ts
|
|
558
|
+
var import_sdk2 = __toESM(require_dist2(), 1);
|
|
559
|
+
import * as p2 from "@clack/prompts";
|
|
560
|
+
import pc3 from "picocolors";
|
|
561
|
+
var MANUAL = "__manual__";
|
|
562
|
+
var CREATE_PRODUCT = "__create__";
|
|
563
|
+
var REPO_ROOT = ".";
|
|
564
|
+
function guard(value) {
|
|
565
|
+
if (p2.isCancel(value)) die("Cancelled.");
|
|
566
|
+
return value;
|
|
567
|
+
}
|
|
568
|
+
async function pickRepo(c) {
|
|
569
|
+
const s = p2.spinner();
|
|
570
|
+
s.start("Loading your GitHub repositories");
|
|
571
|
+
let repos;
|
|
572
|
+
try {
|
|
573
|
+
({ repos } = await c.listRepos());
|
|
574
|
+
} catch (err) {
|
|
575
|
+
s.stop("Failed to load repositories");
|
|
576
|
+
fail(err);
|
|
577
|
+
}
|
|
578
|
+
s.stop(`Found ${repos.length} repositor${repos.length === 1 ? "y" : "ies"}`);
|
|
579
|
+
let choice = MANUAL;
|
|
580
|
+
if (repos.length === 0) {
|
|
581
|
+
p2.note("No repositories found. Push your code to GitHub, then run this again.", "GitHub");
|
|
582
|
+
} else {
|
|
583
|
+
const options = repos.slice(0, 100).map((r) => ({ value: r.fullName, label: r.fullName, hint: r.private ? "private" : void 0 }));
|
|
584
|
+
options.push({ value: MANUAL, label: pc3.dim("Enter a repository manually") });
|
|
585
|
+
choice = guard(
|
|
586
|
+
await p2.select({ message: "Select a GitHub repository", options, maxItems: 12 })
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (choice === MANUAL) {
|
|
590
|
+
const entered = guard(
|
|
591
|
+
await p2.text({
|
|
592
|
+
message: "Repository (owner/repo)",
|
|
593
|
+
placeholder: "acme/my-app",
|
|
594
|
+
validate: (v) => /^[^/\s]+\/[^/\s]+$/.test(v.trim()) ? void 0 : "Use the owner/repo format."
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
const full = entered.trim();
|
|
598
|
+
return { repoFullName: full, defaultBranch: repos.find((r) => r.fullName === full)?.defaultBranch ?? "main" };
|
|
599
|
+
}
|
|
600
|
+
return { repoFullName: choice, defaultBranch: repos.find((r) => r.fullName === choice)?.defaultBranch ?? "main" };
|
|
601
|
+
}
|
|
602
|
+
async function pickProduct(c, defaultName) {
|
|
603
|
+
let products;
|
|
604
|
+
try {
|
|
605
|
+
products = await c.listProducts();
|
|
606
|
+
} catch (err) {
|
|
607
|
+
fail(err);
|
|
608
|
+
}
|
|
609
|
+
let productId = CREATE_PRODUCT;
|
|
610
|
+
if (products.length > 0) {
|
|
611
|
+
const options = [
|
|
612
|
+
{ value: CREATE_PRODUCT, label: brand.violet("+ New product") },
|
|
613
|
+
...products.map((prod) => ({ value: prod.id, label: `${prod.name} ${pc3.dim(`(${prod.slug})`)}` }))
|
|
614
|
+
];
|
|
615
|
+
productId = guard(await p2.select({ message: "Attach to which product?", options }));
|
|
616
|
+
}
|
|
617
|
+
if (productId === CREATE_PRODUCT) {
|
|
618
|
+
const name = guard(
|
|
619
|
+
await p2.text({ message: "Product name", defaultValue: defaultName, placeholder: defaultName })
|
|
620
|
+
);
|
|
621
|
+
try {
|
|
622
|
+
const created = await c.createProduct((name || defaultName).trim());
|
|
623
|
+
return created.id;
|
|
624
|
+
} catch (err) {
|
|
625
|
+
fail(err);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return productId;
|
|
629
|
+
}
|
|
630
|
+
async function choosePath(scan, kind) {
|
|
631
|
+
const fallback = kind === "api" ? "apps/api" : "apps/web";
|
|
632
|
+
const candidates = scan.candidates ?? [];
|
|
633
|
+
if (candidates.length > 0) {
|
|
634
|
+
const options = candidates.map((cand) => ({
|
|
635
|
+
value: cand.scanPath,
|
|
636
|
+
label: `${cand.scanPath === REPO_ROOT ? "repo root" : cand.scanPath} ${pc3.dim(`(${cand.label})`)}`
|
|
637
|
+
}));
|
|
638
|
+
options.push({ value: REPO_ROOT, label: "Repo root (.)" });
|
|
639
|
+
options.push({ value: MANUAL, label: pc3.dim("Enter a path manually") });
|
|
640
|
+
const picked = guard(
|
|
641
|
+
await p2.select({
|
|
642
|
+
message: `Which folder holds your ${kind === "api" ? "API" : "app"}?`,
|
|
643
|
+
options
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
if (picked !== MANUAL) return picked;
|
|
647
|
+
}
|
|
648
|
+
const entered = guard(
|
|
649
|
+
await p2.text({
|
|
650
|
+
message: "Folder path (relative to repo root)",
|
|
651
|
+
placeholder: fallback,
|
|
652
|
+
defaultValue: scan.scanPath && scan.scanPath !== REPO_ROOT ? scan.scanPath : fallback
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
return entered.trim() || REPO_ROOT;
|
|
656
|
+
}
|
|
657
|
+
async function scanWithPath(c, repoFullName, branch, kind) {
|
|
658
|
+
let scanPath;
|
|
659
|
+
for (; ; ) {
|
|
660
|
+
const s = p2.spinner();
|
|
661
|
+
s.start(`Scanning ${repoFullName}${scanPath ? ` at ${scanPath}` : ""}`);
|
|
662
|
+
let result;
|
|
663
|
+
try {
|
|
664
|
+
result = await c.scanRepo({ repoFullName, branch, scanPath, scanKind: kind });
|
|
665
|
+
} catch (err) {
|
|
666
|
+
s.stop("Scan failed");
|
|
667
|
+
fail(err);
|
|
668
|
+
}
|
|
669
|
+
s.stop("Repository scanned");
|
|
670
|
+
if (result.needsPathSelection && !scanPath) {
|
|
671
|
+
scanPath = await choosePath(result, kind);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function scanSummary(scan, repoFullName, branch) {
|
|
678
|
+
const lines = [
|
|
679
|
+
`${pc3.dim("repo")} ${repoFullName}`,
|
|
680
|
+
`${pc3.dim("branch")} ${branch}`,
|
|
681
|
+
`${pc3.dim("framework")} ${scan.framework}`
|
|
682
|
+
];
|
|
683
|
+
if (scan.scanPath && scan.scanPath !== REPO_ROOT) lines.push(`${pc3.dim("path")} ${scan.scanPath}`);
|
|
684
|
+
if (scan.buildCommand) lines.push(`${pc3.dim("build")} ${scan.buildCommand}`);
|
|
685
|
+
if (scan.outputDir) lines.push(`${pc3.dim("output")} ${scan.outputDir}`);
|
|
686
|
+
if (scan.requiredEnvVars.length > 0)
|
|
687
|
+
lines.push(`${pc3.dim("env vars")} ${scan.requiredEnvVars.join(", ")}`);
|
|
688
|
+
return lines.join("\n");
|
|
689
|
+
}
|
|
690
|
+
async function interactiveCreate(c, opts = {}) {
|
|
691
|
+
let repoFullName;
|
|
692
|
+
let defaultBranch;
|
|
693
|
+
if (opts.repo) {
|
|
694
|
+
repoFullName = opts.repo;
|
|
695
|
+
defaultBranch = "main";
|
|
696
|
+
} else {
|
|
697
|
+
({ repoFullName, defaultBranch } = await pickRepo(c));
|
|
698
|
+
}
|
|
699
|
+
let kind = guard(
|
|
700
|
+
await p2.select({
|
|
701
|
+
message: "What are you deploying?",
|
|
702
|
+
options: [
|
|
703
|
+
{ value: "web", label: "Web app", hint: "static site (React, Vue, Astro, Svelte, ...)" },
|
|
704
|
+
{ value: "api", label: "API", hint: "ExpressoTS backend" }
|
|
705
|
+
]
|
|
706
|
+
})
|
|
707
|
+
);
|
|
708
|
+
const defaultName = repoFullName.split("/")[1] ?? repoFullName;
|
|
709
|
+
const productId = await pickProduct(c, defaultName);
|
|
710
|
+
let scan = await scanWithPath(c, repoFullName, defaultBranch, kind);
|
|
711
|
+
if (kind === "web" && scan.apiRepoDetected) {
|
|
712
|
+
const switchToApi = guard(
|
|
713
|
+
await p2.confirm({
|
|
714
|
+
message: "This looks like an API repo, not a web app. Deploy it as an API instead?",
|
|
715
|
+
initialValue: true
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
if (switchToApi) {
|
|
719
|
+
kind = "api";
|
|
720
|
+
scan = await scanWithPath(c, repoFullName, defaultBranch, "api");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
p2.note(scanSummary(scan, repoFullName, defaultBranch), "Detected configuration");
|
|
724
|
+
const s = p2.spinner();
|
|
725
|
+
s.start(kind === "api" ? "Creating API service" : "Creating project");
|
|
726
|
+
let project;
|
|
727
|
+
try {
|
|
728
|
+
if (kind === "api") {
|
|
729
|
+
project = await c.addApiService(productId, {
|
|
730
|
+
repoFullName,
|
|
731
|
+
framework: scan.framework,
|
|
732
|
+
scanPath: scan.scanPath ?? REPO_ROOT,
|
|
733
|
+
buildCommand: scan.buildCommand,
|
|
734
|
+
outputDir: scan.outputDir,
|
|
735
|
+
defaultBranch
|
|
736
|
+
});
|
|
737
|
+
} else {
|
|
738
|
+
project = await c.createProject({
|
|
739
|
+
repoFullName,
|
|
740
|
+
productId,
|
|
741
|
+
defaultBranch,
|
|
742
|
+
framework: scan.framework,
|
|
743
|
+
scanPath: scan.scanPath ?? REPO_ROOT,
|
|
744
|
+
buildCommand: scan.buildCommand,
|
|
745
|
+
outputDir: scan.outputDir,
|
|
746
|
+
spaFallback: scan.spaFallback,
|
|
747
|
+
requiredEnvVars: scan.requiredEnvVars,
|
|
748
|
+
secretsFound: scan.secretsFound
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
} catch (err) {
|
|
752
|
+
s.stop("Create failed");
|
|
753
|
+
fail(err);
|
|
754
|
+
}
|
|
755
|
+
s.stop(`${symbol.ok} Created ${brand.violet(project.name)} ${pc3.dim(`(${project.slug})`)}`);
|
|
756
|
+
return { project, scan };
|
|
757
|
+
}
|
|
758
|
+
async function deployProject(c, project, opts = {}) {
|
|
759
|
+
const s = p2.spinner();
|
|
760
|
+
s.start(`Deploying ${project.name}`);
|
|
761
|
+
let deploy;
|
|
762
|
+
try {
|
|
763
|
+
deploy = await c.deploy(project.id, {
|
|
764
|
+
acknowledgeWarnings: Boolean(opts.yes),
|
|
765
|
+
branch: opts.branch,
|
|
766
|
+
environment: opts.env
|
|
767
|
+
});
|
|
768
|
+
} catch (err) {
|
|
769
|
+
s.stop("Deploy failed to start");
|
|
770
|
+
if (err instanceof import_sdk2.GzeroApiError) {
|
|
771
|
+
die(`${err.message}
|
|
772
|
+
${pc3.dim("Re-run with --yes to acknowledge warnings.")}`);
|
|
773
|
+
}
|
|
774
|
+
fail(err);
|
|
775
|
+
}
|
|
776
|
+
if (opts.follow === false) {
|
|
777
|
+
s.stop(`Deploy queued (${deploy.id}).`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const terminal = /* @__PURE__ */ new Set(["live", "failed", "rolled_back"]);
|
|
781
|
+
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
782
|
+
let current = deploy;
|
|
783
|
+
while (!terminal.has(current.status) && Date.now() < deadline) {
|
|
784
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
785
|
+
try {
|
|
786
|
+
current = await c.getDeploy(deploy.id);
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
s.message(`${project.name}: ${current.phase ?? current.status}`);
|
|
790
|
+
}
|
|
791
|
+
if (current.status === "live") {
|
|
792
|
+
s.stop(`${symbol.ok} ${pc3.green("Live")} ${pc3.cyan(current.url ?? project.slug)}`);
|
|
793
|
+
} else if (current.status === "failed") {
|
|
794
|
+
s.stop(`${symbol.err} ${pc3.red("Deploy failed")}`);
|
|
795
|
+
if (current.buildLog) console.log(pc3.dim(current.buildLog.slice(-2e3)));
|
|
796
|
+
process.exit(1);
|
|
797
|
+
} else {
|
|
798
|
+
s.stop(`${symbol.warn} Deploy status: ${current.status}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async function offerDeploy(c, project, opts) {
|
|
802
|
+
let hardBlockers = [];
|
|
803
|
+
try {
|
|
804
|
+
const readiness = await c.getReadiness(project.id);
|
|
805
|
+
hardBlockers = readiness.blockers.filter((b) => !b.id.startsWith("env-"));
|
|
806
|
+
const envIssues = [...readiness.blockers, ...readiness.warnings].filter(
|
|
807
|
+
(i) => i.id.startsWith("env-")
|
|
808
|
+
);
|
|
809
|
+
if (envIssues.length > 0) {
|
|
810
|
+
p2.note(
|
|
811
|
+
envIssues.map((i) => `${symbol.warn} ${i.message}`).join("\n") + `
|
|
812
|
+
|
|
813
|
+
${pc3.dim("Set them with")} gzero env set ${project.slug} KEY value`,
|
|
814
|
+
"Environment variables"
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
if (hardBlockers.length > 0) {
|
|
820
|
+
p2.note(hardBlockers.map((b) => `${symbol.err} ${b.message}`).join("\n"), "Fix before deploying");
|
|
821
|
+
p2.outro(`Resolve the blockers above, then run ${brand.violet(`gzero deploy ${project.slug}`)}.`);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
let doDeploy = true;
|
|
825
|
+
if (opts.prompt) {
|
|
826
|
+
doDeploy = guard(await p2.confirm({ message: "Deploy now?", initialValue: true }));
|
|
827
|
+
}
|
|
828
|
+
if (!doDeploy) {
|
|
829
|
+
p2.outro(`Run ${brand.violet(`gzero deploy ${project.slug}`)} when you're ready.`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
await deployProject(c, project, { yes: true });
|
|
833
|
+
p2.outro("Done.");
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/index.ts
|
|
837
|
+
var VERSION = "0.1.0";
|
|
295
838
|
var program = new Command();
|
|
296
|
-
program.name("gzero").description("GroundZero: deploy UIs and APIs from the terminal or an AI agent.").version("
|
|
839
|
+
program.name("gzero").description("GroundZero: deploy UIs and APIs from the terminal or an AI agent.").version(VERSION, "-v, --version", "Print the CLI version.").option("--api-url <url>", "API base URL (default: config or GZERO_API_URL)").option("--token <token>", "API token (default: config or GZERO_TOKEN)").configureHelp({
|
|
840
|
+
styleTitle: (s) => pc4.bold(s),
|
|
841
|
+
styleCommandText: (s) => brand.violet(s),
|
|
842
|
+
styleOptionTerm: (s) => pc4.cyan(s),
|
|
843
|
+
styleSubcommandTerm: (s) => brand.violet(s)
|
|
844
|
+
}).addHelpText("beforeAll", () => banner(VERSION)).addHelpText("after", () => `
|
|
845
|
+
${pc4.dim("Docs:")} ${pc4.underline("https://gzer0.app/docs")}
|
|
846
|
+
`).showHelpAfterError(pc4.dim("(run `gzero --help` for usage)"));
|
|
297
847
|
function flags(command) {
|
|
298
848
|
return command.optsWithGlobals();
|
|
299
849
|
}
|
|
300
850
|
program.command("login").description("Sign in via the browser (device-code flow) and save an API token.").action(async (_opts, command) => {
|
|
301
851
|
const gf = flags(command);
|
|
302
|
-
|
|
852
|
+
p3.intro(lockup("login"));
|
|
303
853
|
const settings = await resolveSettings(gf);
|
|
304
|
-
const baseUrl = settings.baseUrl ??
|
|
305
|
-
const c = (0,
|
|
306
|
-
const s =
|
|
854
|
+
const baseUrl = settings.baseUrl ?? import_sdk3.DEFAULT_BASE_URL;
|
|
855
|
+
const c = (0, import_sdk3.createClient)({ baseUrl });
|
|
856
|
+
const s = p3.spinner();
|
|
307
857
|
s.start("Requesting device code");
|
|
308
858
|
let start;
|
|
309
859
|
try {
|
|
@@ -313,15 +863,15 @@ program.command("login").description("Sign in via the browser (device-code flow)
|
|
|
313
863
|
fail(err);
|
|
314
864
|
}
|
|
315
865
|
s.stop("Device code ready");
|
|
316
|
-
|
|
317
|
-
`${
|
|
866
|
+
p3.note(
|
|
867
|
+
`${pc4.bold("Your code:")} ${pc4.cyan(start.user_code)}
|
|
318
868
|
|
|
319
869
|
Open this URL to authorize:
|
|
320
|
-
${
|
|
870
|
+
${pc4.underline(start.verification_uri_complete)}`,
|
|
321
871
|
"Authorize this device"
|
|
322
872
|
);
|
|
323
873
|
openBrowser(start.verification_uri_complete);
|
|
324
|
-
const waiting =
|
|
874
|
+
const waiting = p3.spinner();
|
|
325
875
|
waiting.start("Waiting for authorization in the browser");
|
|
326
876
|
const deadline = Date.now() + start.expires_in * 1e3;
|
|
327
877
|
let intervalMs = Math.max(start.interval, 1) * 1e3;
|
|
@@ -350,33 +900,35 @@ ${pc.underline(start.verification_uri_complete)}`,
|
|
|
350
900
|
waiting.stop("Authorized");
|
|
351
901
|
await saveConfig({ baseUrl, token });
|
|
352
902
|
c.setToken(token);
|
|
353
|
-
|
|
903
|
+
p3.note(pc4.dim(configPath()), "Credentials saved to");
|
|
354
904
|
try {
|
|
355
905
|
const me = await c.me();
|
|
356
|
-
|
|
906
|
+
p3.outro(`${symbol.ok} Signed in as ${brand.violet(me.githubLogin ?? me.name ?? "your account")}.`);
|
|
357
907
|
} catch {
|
|
358
|
-
|
|
908
|
+
p3.outro(`${symbol.ok} Signed in.`);
|
|
359
909
|
}
|
|
360
910
|
});
|
|
361
911
|
program.command("logout").description("Revoke the current token and remove local credentials.").action(async (_opts, command) => {
|
|
362
912
|
const gf = flags(command);
|
|
363
913
|
const settings = await resolveSettings(gf);
|
|
364
914
|
if (settings.token) {
|
|
365
|
-
const c = (0,
|
|
915
|
+
const c = (0, import_sdk3.createClient)({ baseUrl: settings.baseUrl, token: settings.token });
|
|
366
916
|
try {
|
|
367
917
|
await c.device.revoke();
|
|
368
918
|
} catch {
|
|
369
919
|
}
|
|
370
920
|
}
|
|
371
921
|
await clearConfig();
|
|
372
|
-
|
|
922
|
+
p3.outro(`${symbol.ok} Signed out.`);
|
|
373
923
|
});
|
|
374
924
|
program.command("whoami").description("Show the signed-in account.").action(async (_opts, command) => {
|
|
375
925
|
const c = await client(flags(command));
|
|
376
926
|
try {
|
|
377
927
|
const me = await c.me();
|
|
378
|
-
console.log(
|
|
379
|
-
|
|
928
|
+
console.log(
|
|
929
|
+
`${symbol.bullet} ${pc4.bold(me.githubLogin ?? me.name ?? me.id)} ${pc4.dim(me.email ?? "")}`
|
|
930
|
+
);
|
|
931
|
+
console.log(` ${pc4.dim(`plan: ${me.plan}${me.isAdmin ? " (admin)" : ""}`)}`);
|
|
380
932
|
} catch (err) {
|
|
381
933
|
fail(err);
|
|
382
934
|
}
|
|
@@ -386,71 +938,53 @@ program.command("projects").alias("ls").description("List your projects.").actio
|
|
|
386
938
|
try {
|
|
387
939
|
const projects = await c.listProjects();
|
|
388
940
|
if (projects.length === 0) {
|
|
389
|
-
console.log(
|
|
941
|
+
console.log(pc4.dim("No projects yet. Run `gzero init` to create one."));
|
|
390
942
|
return;
|
|
391
943
|
}
|
|
944
|
+
const count = projects.length;
|
|
945
|
+
console.log(pc4.dim(`${count} project${count === 1 ? "" : "s"}
|
|
946
|
+
`));
|
|
392
947
|
for (const proj of projects) {
|
|
393
948
|
const status = proj.activeDeployStatus ?? "-";
|
|
394
949
|
const url = proj.activeDeployUrl ?? "";
|
|
395
950
|
console.log(
|
|
396
|
-
`${
|
|
951
|
+
`${symbol.bullet} ${pc4.bold(proj.name)} ${pc4.dim(`(${proj.slug})`)} ${statusColor(status)}${url ? ` ${pc4.cyan(url)}` : ""}`
|
|
397
952
|
);
|
|
398
953
|
}
|
|
399
954
|
} catch (err) {
|
|
400
955
|
fail(err);
|
|
401
956
|
}
|
|
402
957
|
});
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
958
|
+
program.command("init").argument("[repo]", "GitHub repository (owner/repo) to skip the picker").description("Create a project from a GitHub repo (interactive) and optionally deploy.").action(async (repo, _opts, command) => {
|
|
959
|
+
const c = await client(flags(command));
|
|
960
|
+
p3.intro(lockup("init"));
|
|
961
|
+
const { project } = await interactiveCreate(c, { repo });
|
|
962
|
+
await offerDeploy(c, project, { prompt: true });
|
|
963
|
+
});
|
|
410
964
|
program.command("deploy").argument("[project]", "project id, slug, or name").option("-b, --branch <branch>", "branch to deploy").option("-e, --env <environment>", "environment (production or preview)").option("-y, --yes", "acknowledge readiness warnings and deploy anyway").option("--no-follow", "return immediately instead of streaming status").description("Trigger a deploy and follow it to completion.").action(async (projectRef, opts, command) => {
|
|
411
965
|
const c = await client(flags(command));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
s.start(`Deploying ${project.name}`);
|
|
415
|
-
let deploy;
|
|
416
|
-
try {
|
|
417
|
-
deploy = await c.deploy(project.id, {
|
|
418
|
-
acknowledgeWarnings: Boolean(opts.yes),
|
|
419
|
-
branch: opts.branch,
|
|
420
|
-
environment: opts.env
|
|
421
|
-
});
|
|
422
|
-
} catch (err) {
|
|
423
|
-
s.stop("Deploy failed to start");
|
|
424
|
-
if (err instanceof import_sdk.GzeroApiError) {
|
|
425
|
-
die(`${err.message}
|
|
426
|
-
${pc.dim("Re-run with --yes to acknowledge warnings.")}`);
|
|
427
|
-
}
|
|
428
|
-
fail(err);
|
|
429
|
-
}
|
|
430
|
-
if (opts.follow === false) {
|
|
431
|
-
s.stop(`Deploy queued (${deploy.id}).`);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
const terminal = /* @__PURE__ */ new Set(["live", "failed", "rolled_back"]);
|
|
435
|
-
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
436
|
-
let current = deploy;
|
|
437
|
-
while (!terminal.has(current.status) && Date.now() < deadline) {
|
|
438
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
966
|
+
if (!projectRef) {
|
|
967
|
+
let projects;
|
|
439
968
|
try {
|
|
440
|
-
|
|
441
|
-
} catch {
|
|
969
|
+
projects = await c.listProjects();
|
|
970
|
+
} catch (err) {
|
|
971
|
+
fail(err);
|
|
972
|
+
}
|
|
973
|
+
if (projects.length === 0) {
|
|
974
|
+
p3.intro(lockup("deploy"));
|
|
975
|
+
p3.note("You don't have any projects yet. Let's create one.", "Setup");
|
|
976
|
+
const { project: project2 } = await interactiveCreate(c);
|
|
977
|
+
await offerDeploy(c, project2, { prompt: false });
|
|
978
|
+
return;
|
|
442
979
|
}
|
|
443
|
-
s.message(`${project.name}: ${current.phase ?? current.status}`);
|
|
444
|
-
}
|
|
445
|
-
if (current.status === "live") {
|
|
446
|
-
s.stop(pc.green(`Live: ${current.url ?? project.slug}`));
|
|
447
|
-
} else if (current.status === "failed") {
|
|
448
|
-
s.stop(pc.red("Deploy failed"));
|
|
449
|
-
if (current.buildLog) console.log(pc.dim(current.buildLog.slice(-2e3)));
|
|
450
|
-
process.exit(1);
|
|
451
|
-
} else {
|
|
452
|
-
s.stop(`Deploy status: ${current.status}`);
|
|
453
980
|
}
|
|
981
|
+
const project = await resolveProject(c, projectRef);
|
|
982
|
+
await deployProject(c, project, {
|
|
983
|
+
yes: Boolean(opts.yes),
|
|
984
|
+
branch: opts.branch,
|
|
985
|
+
env: opts.env,
|
|
986
|
+
follow: opts.follow
|
|
987
|
+
});
|
|
454
988
|
});
|
|
455
989
|
program.command("status").argument("[project]", "project id, slug, or name").description("Show the active deploy and readiness for a project.").action(async (projectRef, _opts, command) => {
|
|
456
990
|
const c = await client(flags(command));
|
|
@@ -458,11 +992,11 @@ program.command("status").argument("[project]", "project id, slug, or name").des
|
|
|
458
992
|
try {
|
|
459
993
|
const status = await c.getProjectStatus(project.id);
|
|
460
994
|
const active = status.activeDeploy;
|
|
461
|
-
console.log(
|
|
462
|
-
console.log(` status: ${active ? statusColor(active.status) :
|
|
463
|
-
if (active?.url) console.log(` url: ${
|
|
464
|
-
console.log(` env vars: ${status.envConfigured ?
|
|
465
|
-
console.log(` ready: ${status.readiness.canDeploy ?
|
|
995
|
+
console.log(`${symbol.bullet} ${pc4.bold(project.name)}${pc4.dim(` (${project.slug})`)}`);
|
|
996
|
+
console.log(` status: ${active ? statusColor(active.status) : pc4.dim("no deploys")}`);
|
|
997
|
+
if (active?.url) console.log(` url: ${pc4.cyan(active.url)}`);
|
|
998
|
+
console.log(` env vars: ${status.envConfigured ? pc4.green("configured") : pc4.yellow("missing")}`);
|
|
999
|
+
console.log(` ready: ${status.readiness.canDeploy ? pc4.green("yes") : pc4.red("no")}`);
|
|
466
1000
|
} catch (err) {
|
|
467
1001
|
fail(err);
|
|
468
1002
|
}
|
|
@@ -475,12 +1009,74 @@ program.command("logs").argument("[project]", "project id, slug, or name").descr
|
|
|
475
1009
|
const latest = deploys[0];
|
|
476
1010
|
if (!latest) die("No deploys yet.");
|
|
477
1011
|
const full = await c.getDeploy(latest.id);
|
|
478
|
-
console.log(
|
|
479
|
-
console.log(full.buildLog ??
|
|
1012
|
+
console.log(pc4.dim(`# ${project.name} deploy ${full.id} (${full.status})`));
|
|
1013
|
+
console.log(full.buildLog ?? pc4.dim("(no build log)"));
|
|
480
1014
|
} catch (err) {
|
|
481
1015
|
fail(err);
|
|
482
1016
|
}
|
|
483
1017
|
});
|
|
1018
|
+
program.command("open").argument("[project]", "project id, slug, or name").description("Open the project's live URL in your browser.").action(async (projectRef, _opts, command) => {
|
|
1019
|
+
const c = await client(flags(command));
|
|
1020
|
+
const project = await resolveProject(c, projectRef);
|
|
1021
|
+
const url = project.activeDeployUrl;
|
|
1022
|
+
if (!url) die("No live deploy to open. Run `gzero deploy` first.");
|
|
1023
|
+
console.log(`${symbol.info} Opening ${pc4.cyan(url)}`);
|
|
1024
|
+
openBrowser(url);
|
|
1025
|
+
});
|
|
1026
|
+
async function selectDeploy(c, projectId, opts) {
|
|
1027
|
+
let deploys;
|
|
1028
|
+
try {
|
|
1029
|
+
deploys = await c.listDeploys(projectId);
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
fail(err);
|
|
1032
|
+
}
|
|
1033
|
+
const candidates = opts.filter ? deploys.filter(opts.filter) : deploys;
|
|
1034
|
+
if (candidates.length === 0) die("No matching deploys.");
|
|
1035
|
+
const picked = await p3.select({
|
|
1036
|
+
message: opts.message,
|
|
1037
|
+
options: candidates.slice(0, 20).map((d) => ({
|
|
1038
|
+
value: d.id,
|
|
1039
|
+
label: `${statusColor(d.status)} ${pc4.dim(d.environment)} ${d.commitMessage?.split("\n")[0]?.slice(0, 48) ?? d.id}`,
|
|
1040
|
+
hint: new Date(d.createdAt).toLocaleString()
|
|
1041
|
+
}))
|
|
1042
|
+
});
|
|
1043
|
+
if (p3.isCancel(picked)) die("Cancelled.");
|
|
1044
|
+
return picked;
|
|
1045
|
+
}
|
|
1046
|
+
program.command("rollback").argument("[project]", "project id, slug, or name").description("Roll back to a previous successful deploy.").action(async (projectRef, _opts, command) => {
|
|
1047
|
+
const c = await client(flags(command));
|
|
1048
|
+
const project = await resolveProject(c, projectRef);
|
|
1049
|
+
const deployId = await selectDeploy(c, project.id, {
|
|
1050
|
+
message: "Roll back to which deploy?",
|
|
1051
|
+
filter: (d) => d.status === "live" || d.status === "rolled_back"
|
|
1052
|
+
});
|
|
1053
|
+
const s = p3.spinner();
|
|
1054
|
+
s.start("Rolling back");
|
|
1055
|
+
try {
|
|
1056
|
+
await c.rollback(project.id, deployId);
|
|
1057
|
+
s.stop(`${symbol.ok} Rolled back ${brand.violet(project.name)}.`);
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
s.stop("Rollback failed");
|
|
1060
|
+
fail(err);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
program.command("promote").argument("[project]", "project id, slug, or name").description("Promote a deploy to production.").action(async (projectRef, _opts, command) => {
|
|
1064
|
+
const c = await client(flags(command));
|
|
1065
|
+
const project = await resolveProject(c, projectRef);
|
|
1066
|
+
const deployId = await selectDeploy(c, project.id, {
|
|
1067
|
+
message: "Promote which deploy to production?",
|
|
1068
|
+
filter: (d) => d.status === "live"
|
|
1069
|
+
});
|
|
1070
|
+
const s = p3.spinner();
|
|
1071
|
+
s.start("Promoting");
|
|
1072
|
+
try {
|
|
1073
|
+
await c.promote(project.id, deployId);
|
|
1074
|
+
s.stop(`${symbol.ok} Promoted ${brand.violet(project.name)} to production.`);
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
s.stop("Promote failed");
|
|
1077
|
+
fail(err);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
484
1080
|
var env = program.command("env").description("Manage project environment variables.");
|
|
485
1081
|
env.command("list").argument("[project]", "project id, slug, or name").description("List env var keys for a project.").action(async (projectRef, _opts, command) => {
|
|
486
1082
|
const c = await client(flags(command));
|
|
@@ -488,11 +1084,11 @@ env.command("list").argument("[project]", "project id, slug, or name").descripti
|
|
|
488
1084
|
try {
|
|
489
1085
|
const vars = await c.listEnvVars(project.id);
|
|
490
1086
|
if (vars.length === 0) {
|
|
491
|
-
console.log(
|
|
1087
|
+
console.log(pc4.dim("No env vars set."));
|
|
492
1088
|
return;
|
|
493
1089
|
}
|
|
494
1090
|
for (const v of vars) {
|
|
495
|
-
console.log(`${v.key} ${v.configured ?
|
|
1091
|
+
console.log(`${v.key} ${v.configured ? pc4.green("set") : pc4.yellow("empty")}`);
|
|
496
1092
|
}
|
|
497
1093
|
} catch (err) {
|
|
498
1094
|
fail(err);
|
|
@@ -503,40 +1099,125 @@ env.command("set").argument("<project>", "project id, slug, or name").argument("
|
|
|
503
1099
|
const project = await resolveProject(c, projectRef);
|
|
504
1100
|
try {
|
|
505
1101
|
await c.upsertEnvVar(project.id, key, value);
|
|
506
|
-
|
|
1102
|
+
p3.outro(`${symbol.ok} Set ${pc4.bold(key)} on ${brand.violet(project.name)}.`);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
fail(err);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
env.command("rm").alias("delete").argument("<project>", "project id, slug, or name").argument("<key>", "variable name").description("Delete an environment variable.").action(async (projectRef, key, _opts, command) => {
|
|
1108
|
+
const c = await client(flags(command));
|
|
1109
|
+
const project = await resolveProject(c, projectRef);
|
|
1110
|
+
try {
|
|
1111
|
+
await c.deleteEnvVar(project.id, key);
|
|
1112
|
+
p3.outro(`${symbol.ok} Removed ${pc4.bold(key)} from ${brand.violet(project.name)}.`);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
fail(err);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
env.command("import").argument("<project>", "project id, slug, or name").argument("<file>", "path to a .env file").description("Bulk-import environment variables from a .env file.").action(async (projectRef, file, _opts, command) => {
|
|
1118
|
+
const c = await client(flags(command));
|
|
1119
|
+
const project = await resolveProject(c, projectRef);
|
|
1120
|
+
let entries;
|
|
1121
|
+
try {
|
|
1122
|
+
entries = (0, import_sdk3.parseDotEnv)(await readFile2(file, "utf8"));
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
die(`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (entries.length === 0) die("No variables found in that file.");
|
|
1127
|
+
try {
|
|
1128
|
+
const result = await c.bulkUpsertEnvVars(project.id, entries);
|
|
1129
|
+
const skipped = result.skipped.length > 0 ? ` ${pc4.dim(`(skipped ${result.skipped.length})`)}` : "";
|
|
1130
|
+
p3.outro(`${symbol.ok} Imported ${pc4.bold(String(result.saved.length))} vars into ${brand.violet(project.name)}.${skipped}`);
|
|
507
1131
|
} catch (err) {
|
|
508
1132
|
fail(err);
|
|
509
1133
|
}
|
|
510
1134
|
});
|
|
511
1135
|
var domains = program.command("domains").description("Manage custom domains.");
|
|
1136
|
+
domains.command("list").argument("[project]", "project id, slug, or name").description("List custom domains for a project.").action(async (projectRef, _opts, command) => {
|
|
1137
|
+
const c = await client(flags(command));
|
|
1138
|
+
const project = await resolveProject(c, projectRef);
|
|
1139
|
+
try {
|
|
1140
|
+
const list = await c.listDomains(project.id);
|
|
1141
|
+
if (list.length === 0) {
|
|
1142
|
+
console.log(pc4.dim("No custom domains."));
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
for (const d of list) {
|
|
1146
|
+
const state = d.verified ? pc4.green("verified") : pc4.yellow("pending");
|
|
1147
|
+
console.log(`${symbol.bullet} ${pc4.bold(d.hostname)} ${state} ${pc4.dim(`ssl: ${d.sslStatus}`)}`);
|
|
1148
|
+
}
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
fail(err);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
512
1153
|
domains.command("add").argument("<project>", "project id, slug, or name").argument("<hostname>", "custom domain, e.g. app.example.com").description("Attach a custom domain to a project.").action(async (projectRef, hostname, _opts, command) => {
|
|
513
1154
|
const c = await client(flags(command));
|
|
514
1155
|
const project = await resolveProject(c, projectRef);
|
|
515
1156
|
try {
|
|
516
1157
|
const domain = await c.addDomain(project.id, hostname);
|
|
517
|
-
|
|
518
|
-
|
|
1158
|
+
p3.outro(
|
|
1159
|
+
`${symbol.ok} Added ${pc4.bold(hostname)}. Point a CNAME to ${pc4.cyan(domain.cnameTarget)}, then run \`gzero domains verify\`.`
|
|
519
1160
|
);
|
|
520
1161
|
} catch (err) {
|
|
521
1162
|
fail(err);
|
|
522
1163
|
}
|
|
523
1164
|
});
|
|
1165
|
+
async function resolveDomainId(c, projectId, hostname) {
|
|
1166
|
+
let list;
|
|
1167
|
+
try {
|
|
1168
|
+
list = await c.listDomains(projectId);
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
fail(err);
|
|
1171
|
+
}
|
|
1172
|
+
const match = list.find((d) => d.hostname === hostname);
|
|
1173
|
+
if (!match) die(`No domain "${hostname}" on this project.`);
|
|
1174
|
+
return match.id;
|
|
1175
|
+
}
|
|
1176
|
+
domains.command("verify").argument("<project>", "project id, slug, or name").argument("<hostname>", "custom domain to verify").description("Re-check DNS and verify a custom domain.").action(async (projectRef, hostname, _opts, command) => {
|
|
1177
|
+
const c = await client(flags(command));
|
|
1178
|
+
const project = await resolveProject(c, projectRef);
|
|
1179
|
+
const domainId = await resolveDomainId(c, project.id, hostname);
|
|
1180
|
+
const s = p3.spinner();
|
|
1181
|
+
s.start(`Verifying ${hostname}`);
|
|
1182
|
+
try {
|
|
1183
|
+
const domain = await c.verifyDomain(project.id, domainId);
|
|
1184
|
+
if (domain.verified) {
|
|
1185
|
+
s.stop(`${symbol.ok} ${pc4.bold(hostname)} verified ${pc4.dim(`(ssl: ${domain.sslStatus})`)}`);
|
|
1186
|
+
} else {
|
|
1187
|
+
s.stop(`${symbol.warn} ${pc4.bold(hostname)} not verified yet. Check the CNAME and try again.`);
|
|
1188
|
+
}
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
s.stop("Verify failed");
|
|
1191
|
+
fail(err);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
domains.command("remove").alias("rm").argument("<project>", "project id, slug, or name").argument("<hostname>", "custom domain to remove").description("Remove a custom domain from a project.").action(async (projectRef, hostname, _opts, command) => {
|
|
1195
|
+
const c = await client(flags(command));
|
|
1196
|
+
const project = await resolveProject(c, projectRef);
|
|
1197
|
+
const domainId = await resolveDomainId(c, project.id, hostname);
|
|
1198
|
+
try {
|
|
1199
|
+
await c.removeDomain(project.id, domainId);
|
|
1200
|
+
p3.outro(`${symbol.ok} Removed ${pc4.bold(hostname)} from ${brand.violet(project.name)}.`);
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
fail(err);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
524
1205
|
var billing = program.command("billing").description("View plan and start payments.");
|
|
525
1206
|
billing.command("status", { isDefault: true }).description("Show current plan, usage, and available add-ons.").action(async (_opts, command) => {
|
|
526
1207
|
const c = await client(flags(command));
|
|
527
1208
|
try {
|
|
528
1209
|
const status = await c.getBillingStatus();
|
|
529
|
-
console.log(`${
|
|
530
|
-
console.log(`${
|
|
531
|
-
console.log(`${
|
|
1210
|
+
console.log(`${symbol.bullet} ${pc4.bold("Plan:")} ${brand.violet(status.planDisplayName)} ${pc4.dim(`(${status.subscriptionStatus})`)}`);
|
|
1211
|
+
console.log(`${pc4.bold("Stacks:")} ${status.limits.stacksLabel}`);
|
|
1212
|
+
console.log(`${pc4.bold("Traffic:")} ${status.limits.trafficLabel}`);
|
|
532
1213
|
if (status.availableAddons.length > 0) {
|
|
533
|
-
console.log(
|
|
1214
|
+
console.log(pc4.bold("\nAvailable add-ons:"));
|
|
534
1215
|
for (const a of status.availableAddons) {
|
|
535
1216
|
console.log(
|
|
536
|
-
` ${
|
|
1217
|
+
` ${pc4.cyan(a.id)} ${a.displayName} ${pc4.dim(`$${(a.priceCents / 100).toFixed(2)}/mo`)}`
|
|
537
1218
|
);
|
|
538
1219
|
}
|
|
539
|
-
console.log(
|
|
1220
|
+
console.log(pc4.dim("\nRun `gzero billing addon <id>` to purchase."));
|
|
540
1221
|
}
|
|
541
1222
|
} catch (err) {
|
|
542
1223
|
fail(err);
|
|
@@ -546,7 +1227,7 @@ billing.command("upgrade").description("Open Stripe checkout to subscribe to the
|
|
|
546
1227
|
const c = await client(flags(command));
|
|
547
1228
|
try {
|
|
548
1229
|
const { url } = await c.createCheckout();
|
|
549
|
-
|
|
1230
|
+
p3.note(pc4.underline(url), "Complete payment in your browser");
|
|
550
1231
|
openBrowser(url);
|
|
551
1232
|
} catch (err) {
|
|
552
1233
|
fail(err);
|
|
@@ -556,7 +1237,7 @@ billing.command("addon").argument("<addonId>", "add-on id (see `gzero billing`)"
|
|
|
556
1237
|
const c = await client(flags(command));
|
|
557
1238
|
try {
|
|
558
1239
|
const { url } = await c.createAddonCheckout(addonId, opts.product);
|
|
559
|
-
|
|
1240
|
+
p3.note(pc4.underline(url), "Complete payment in your browser");
|
|
560
1241
|
openBrowser(url);
|
|
561
1242
|
} catch (err) {
|
|
562
1243
|
fail(err);
|
|
@@ -566,7 +1247,7 @@ billing.command("portal").description("Open the Stripe billing portal to manage
|
|
|
566
1247
|
const c = await client(flags(command));
|
|
567
1248
|
try {
|
|
568
1249
|
const { url } = await c.createBillingPortal();
|
|
569
|
-
|
|
1250
|
+
p3.note(pc4.underline(url), "Manage billing in your browser");
|
|
570
1251
|
openBrowser(url);
|
|
571
1252
|
} catch (err) {
|
|
572
1253
|
fail(err);
|
|
@@ -584,18 +1265,22 @@ function mcpEndpoint(baseUrl) {
|
|
|
584
1265
|
}
|
|
585
1266
|
program.command("mcp").description("Print config to connect an AI client to the hosted GroundZero MCP server.").action(async (_opts, command) => {
|
|
586
1267
|
const settings = await resolveSettings(flags(command));
|
|
587
|
-
const url = mcpEndpoint(settings.baseUrl ??
|
|
1268
|
+
const url = mcpEndpoint(settings.baseUrl ?? import_sdk3.DEFAULT_BASE_URL);
|
|
588
1269
|
const config = JSON.stringify({ mcpServers: { gzero: { url } } }, null, 2);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
`The MCP server is hosted at ${
|
|
592
|
-
Add it to your AI client (Cursor: ${
|
|
1270
|
+
p3.intro(lockup("mcp"));
|
|
1271
|
+
p3.note(
|
|
1272
|
+
`The MCP server is hosted at ${pc4.cyan(url)}.
|
|
1273
|
+
Add it to your AI client (Cursor: ${pc4.dim("~/.cursor/mcp.json")}) and it will
|
|
593
1274
|
open a browser to sign in with GitHub the first time. Nothing to install.`,
|
|
594
1275
|
"Hosted MCP"
|
|
595
1276
|
);
|
|
596
|
-
|
|
597
|
-
|
|
1277
|
+
p3.note(config, "Client config");
|
|
1278
|
+
p3.outro("Paste the config above, then reload your AI client.");
|
|
598
1279
|
});
|
|
1280
|
+
if (process.argv.length <= 2) {
|
|
1281
|
+
program.outputHelp();
|
|
1282
|
+
process.exit(0);
|
|
1283
|
+
}
|
|
599
1284
|
program.parseAsync(process.argv).catch((err) => {
|
|
600
1285
|
fail(err);
|
|
601
1286
|
});
|