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.
Files changed (4) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +116 -35
  3. package/dist/index.js +794 -109
  4. 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
- // ../sdk/dist/index.js
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 = createClient2;
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 GzeroApiError2 = class extends Error {
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 = GzeroApiError2;
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 createClient2(options = {}) {
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 GzeroApiError2(detail ?? body.error ?? `API ${res.status}: ${path}`, res.status, body.error);
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 import_sdk = __toESM(require_dist(), 1);
194
- import { spawn } from "child_process";
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 p from "@clack/prompts";
197
- import pc from "picocolors";
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/index.ts
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 fail(err) {
266
- if (err instanceof import_sdk.GzeroApiError) die(err.message);
267
- die(err instanceof Error ? err.message : String(err));
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. Create one in the dashboard, then deploy.");
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} ${pc.dim(`(${proj.slug})`)}`
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("0.1.0-preview.1").option("--api-url <url>", "API base URL (default: config or GZERO_API_URL)").option("--token <token>", "API token (default: config or GZERO_TOKEN)");
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
- p.intro(pc.bgCyan(pc.black(" GroundZero login ")));
852
+ p3.intro(lockup("login"));
303
853
  const settings = await resolveSettings(gf);
304
- const baseUrl = settings.baseUrl ?? import_sdk.DEFAULT_BASE_URL;
305
- const c = (0, import_sdk.createClient)({ baseUrl });
306
- const s = p.spinner();
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
- p.note(
317
- `${pc.bold("Your code:")} ${pc.cyan(start.user_code)}
866
+ p3.note(
867
+ `${pc4.bold("Your code:")} ${pc4.cyan(start.user_code)}
318
868
 
319
869
  Open this URL to authorize:
320
- ${pc.underline(start.verification_uri_complete)}`,
870
+ ${pc4.underline(start.verification_uri_complete)}`,
321
871
  "Authorize this device"
322
872
  );
323
873
  openBrowser(start.verification_uri_complete);
324
- const waiting = p.spinner();
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
- p.note(pc.dim(configPath()), "Credentials saved to");
903
+ p3.note(pc4.dim(configPath()), "Credentials saved to");
354
904
  try {
355
905
  const me = await c.me();
356
- p.outro(`Signed in as ${pc.green(me.githubLogin ?? me.name ?? "your account")}.`);
906
+ p3.outro(`${symbol.ok} Signed in as ${brand.violet(me.githubLogin ?? me.name ?? "your account")}.`);
357
907
  } catch {
358
- p.outro("Signed in.");
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, import_sdk.createClient)({ baseUrl: settings.baseUrl, token: settings.token });
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
- p.outro("Signed out.");
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(`${pc.bold(me.githubLogin ?? me.name ?? me.id)} ${pc.dim(me.email ?? "")}`);
379
- console.log(pc.dim(`plan: ${me.plan}${me.isAdmin ? " (admin)" : ""}`));
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(pc.dim("No projects yet."));
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
- `${pc.bold(proj.name)} ${pc.dim(`(${proj.slug})`)} ${statusColor(status)} ${pc.cyan(url)}`
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
- function statusColor(status) {
404
- if (status === "live") return pc.green(status);
405
- if (status === "failed") return pc.red(status);
406
- if (status === "building" || status === "deploying" || status === "pending")
407
- return pc.yellow(status);
408
- return pc.dim(status);
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
- const project = await resolveProject(c, projectRef);
413
- const s = p.spinner();
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
- current = await c.getDeploy(deploy.id);
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(pc.bold(project.name) + pc.dim(` (${project.slug})`));
462
- console.log(` status: ${active ? statusColor(active.status) : pc.dim("no deploys")}`);
463
- if (active?.url) console.log(` url: ${pc.cyan(active.url)}`);
464
- console.log(` env vars: ${status.envConfigured ? pc.green("configured") : pc.yellow("missing")}`);
465
- console.log(` ready: ${status.readiness.canDeploy ? pc.green("yes") : pc.red("no")}`);
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(pc.dim(`# ${project.name} deploy ${full.id} (${full.status})`));
479
- console.log(full.buildLog ?? pc.dim("(no build log)"));
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(pc.dim("No env vars set."));
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 ? pc.green("set") : pc.yellow("empty")}`);
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
- p.outro(`Set ${pc.bold(key)} on ${project.name}.`);
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
- p.outro(
518
- `Added ${pc.bold(hostname)}. Point a CNAME to ${pc.cyan(domain.cnameTarget)}, then verify in the dashboard.`
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(`${pc.bold("Plan:")} ${status.planDisplayName} ${pc.dim(`(${status.subscriptionStatus})`)}`);
530
- console.log(`${pc.bold("Stacks:")} ${status.limits.stacksLabel}`);
531
- console.log(`${pc.bold("Traffic:")} ${status.limits.trafficLabel}`);
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(pc.bold("\nAvailable add-ons:"));
1214
+ console.log(pc4.bold("\nAvailable add-ons:"));
534
1215
  for (const a of status.availableAddons) {
535
1216
  console.log(
536
- ` ${pc.cyan(a.id)} ${a.displayName} ${pc.dim(`$${(a.priceCents / 100).toFixed(2)}/mo`)}`
1217
+ ` ${pc4.cyan(a.id)} ${a.displayName} ${pc4.dim(`$${(a.priceCents / 100).toFixed(2)}/mo`)}`
537
1218
  );
538
1219
  }
539
- console.log(pc.dim("\nRun `gzero billing addon <id>` to purchase."));
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
- p.note(pc.underline(url), "Complete payment in your browser");
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
- p.note(pc.underline(url), "Complete payment in your browser");
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
- p.note(pc.underline(url), "Manage billing in your browser");
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 ?? import_sdk.DEFAULT_BASE_URL);
1268
+ const url = mcpEndpoint(settings.baseUrl ?? import_sdk3.DEFAULT_BASE_URL);
588
1269
  const config = JSON.stringify({ mcpServers: { gzero: { url } } }, null, 2);
589
- p.intro(pc.bgCyan(pc.black(" GroundZero MCP ")));
590
- p.note(
591
- `The MCP server is hosted at ${pc.cyan(url)}.
592
- Add it to your AI client (Cursor: ${pc.dim("~/.cursor/mcp.json")}) and it will
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
- p.note(config, "Client config");
597
- p.outro("Paste the config above, then reload your AI client.");
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
  });