hatchkit 0.1.1 → 0.1.3

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 (82) hide show
  1. package/dist/completion.d.ts +2 -0
  2. package/dist/completion.d.ts.map +1 -0
  3. package/dist/completion.js +207 -0
  4. package/dist/completion.js.map +1 -0
  5. package/dist/config.d.ts +33 -1
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +455 -117
  8. package/dist/config.js.map +1 -1
  9. package/dist/deploy/keys.d.ts +6 -2
  10. package/dist/deploy/keys.d.ts.map +1 -1
  11. package/dist/deploy/keys.js +16 -2
  12. package/dist/deploy/keys.js.map +1 -1
  13. package/dist/deploy/pages.d.ts +2 -0
  14. package/dist/deploy/pages.d.ts.map +1 -0
  15. package/dist/deploy/pages.js +537 -0
  16. package/dist/deploy/pages.js.map +1 -0
  17. package/dist/deploy/rename-domain.d.ts +55 -0
  18. package/dist/deploy/rename-domain.d.ts.map +1 -0
  19. package/dist/deploy/rename-domain.js +290 -0
  20. package/dist/deploy/rename-domain.js.map +1 -0
  21. package/dist/deploy/terraform.d.ts.map +1 -1
  22. package/dist/deploy/terraform.js +90 -0
  23. package/dist/deploy/terraform.js.map +1 -1
  24. package/dist/dns.d.ts +7 -0
  25. package/dist/dns.d.ts.map +1 -0
  26. package/dist/dns.js +124 -0
  27. package/dist/dns.js.map +1 -0
  28. package/dist/doctor.d.ts +13 -0
  29. package/dist/doctor.d.ts.map +1 -0
  30. package/dist/doctor.js +368 -0
  31. package/dist/doctor.js.map +1 -0
  32. package/dist/explain.d.ts +4 -0
  33. package/dist/explain.d.ts.map +1 -0
  34. package/dist/explain.js +173 -0
  35. package/dist/explain.js.map +1 -0
  36. package/dist/index.js +504 -66
  37. package/dist/index.js.map +1 -1
  38. package/dist/provision/glitchtip.d.ts +3 -0
  39. package/dist/provision/glitchtip.d.ts.map +1 -1
  40. package/dist/provision/glitchtip.js +18 -0
  41. package/dist/provision/glitchtip.js.map +1 -1
  42. package/dist/provision/index.d.ts +26 -0
  43. package/dist/provision/index.d.ts.map +1 -1
  44. package/dist/provision/index.js +435 -60
  45. package/dist/provision/index.js.map +1 -1
  46. package/dist/provision/openpanel.d.ts +7 -0
  47. package/dist/provision/openpanel.d.ts.map +1 -1
  48. package/dist/provision/openpanel.js +113 -48
  49. package/dist/provision/openpanel.js.map +1 -1
  50. package/dist/provision/resend.d.ts +23 -1
  51. package/dist/provision/resend.d.ts.map +1 -1
  52. package/dist/provision/resend.js +62 -1
  53. package/dist/provision/resend.js.map +1 -1
  54. package/dist/provision/write-env.d.ts +31 -0
  55. package/dist/provision/write-env.d.ts.map +1 -0
  56. package/dist/provision/write-env.js +94 -0
  57. package/dist/provision/write-env.js.map +1 -0
  58. package/dist/scaffold/infra.d.ts.map +1 -1
  59. package/dist/scaffold/infra.js +18 -1
  60. package/dist/scaffold/infra.js.map +1 -1
  61. package/dist/status.d.ts +30 -0
  62. package/dist/status.d.ts.map +1 -0
  63. package/dist/status.js +169 -0
  64. package/dist/status.js.map +1 -0
  65. package/dist/templates/addons/analytics/sentry.ts.hbs +6 -0
  66. package/dist/utils/cloudflare-api.d.ts +30 -0
  67. package/dist/utils/cloudflare-api.d.ts.map +1 -0
  68. package/dist/utils/cloudflare-api.js +85 -0
  69. package/dist/utils/cloudflare-api.js.map +1 -0
  70. package/dist/utils/coolify-api.d.ts +3 -1
  71. package/dist/utils/coolify-api.d.ts.map +1 -1
  72. package/dist/utils/coolify-api.js +38 -4
  73. package/dist/utils/coolify-api.js.map +1 -1
  74. package/dist/utils/inwx-api.d.ts +36 -0
  75. package/dist/utils/inwx-api.d.ts.map +1 -0
  76. package/dist/utils/inwx-api.js +105 -0
  77. package/dist/utils/inwx-api.js.map +1 -0
  78. package/dist/utils/secrets.d.ts +8 -1
  79. package/dist/utils/secrets.d.ts.map +1 -1
  80. package/dist/utils/secrets.js +8 -1
  81. package/dist/utils/secrets.js.map +1 -1
  82. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import { join, resolve } from "node:path";
3
3
  import { confirm } from "@inquirer/prompts";
4
4
  import chalk from "chalk";
5
- import { ensureCoolify, ensureDns, ensureGitHub, ensureGlitchtip, ensureGpuProvider, ensureHetzner, ensureOpenpanel, ensureResend, ensureS3, getConfig, getConfigPath, getMlServices, isFirstRun, resetConfig, runOnboarding, } from "./config.js";
5
+ import { ensureCoolify, ensureGitHub, ensureHetzner, ensureS3, getConfig, getConfigPath, getMlServices, isFirstRun, reconfigureProvider, resetConfig, runOnboarding, } from "./config.js";
6
6
  import { runCoolifySetup } from "./deploy/coolify.js";
7
7
  import { setupGitHub } from "./deploy/github.js";
8
8
  import { deployMlServices } from "./deploy/gpu.js";
9
9
  import { pushProjectKeyToCoolify, showProjectKey } from "./deploy/keys.js";
10
10
  import { runTerraform } from "./deploy/terraform.js";
11
11
  import { collectProjectConfig } from "./prompts.js";
12
- import { runProvision } from "./provision/index.js";
12
+ import { runProvision, runUnprovision } from "./provision/index.js";
13
13
  import { scaffoldApp } from "./scaffold/app.js";
14
14
  import { scaffoldInfra } from "./scaffold/infra.js";
15
15
  import { mlEnvVarName, printMlSummary, resolveMlServices } from "./scaffold/ml-client.js";
@@ -35,16 +35,22 @@ async function main() {
35
35
  console.log(getCliVersion());
36
36
  return;
37
37
  }
38
- console.log(chalk.bold(`\n hatchkit v${getCliVersion()}\n`));
39
- // Global --help without a subcommand prints the top-level help.
40
- if (command === "--help" || command === "-h") {
41
- printHelp();
38
+ const isJson = args.includes("--json");
39
+ // Suppress the banner for machine-readable output so stdout is pure JSON.
40
+ if (!isJson) {
41
+ console.log(chalk.bold(`\n hatchkit v${getCliVersion()}\n`));
42
+ }
43
+ // Global --help / help subcommand (with optional topic).
44
+ if (command === "--help" || command === "-h" || command === "help") {
45
+ const topic = command === "help" ? args[1] : undefined;
46
+ printHelp(topic);
42
47
  return;
43
48
  }
44
49
  switch (command) {
45
50
  case "init":
51
+ case "setup":
46
52
  if (args.includes("--help"))
47
- return printHelp("init");
53
+ return printHelp("setup");
48
54
  await runOnboarding();
49
55
  break;
50
56
  case "config":
@@ -52,12 +58,46 @@ async function main() {
52
58
  return printHelp("config");
53
59
  await handleConfig();
54
60
  break;
61
+ case "status": {
62
+ if (args.includes("--help"))
63
+ return printHelp("status");
64
+ const { collectStatus, renderStatusHuman } = await import("./status.js");
65
+ const s = collectStatus();
66
+ if (isJson) {
67
+ console.log(JSON.stringify(s, null, 2));
68
+ }
69
+ else {
70
+ console.log(renderStatusHuman(s));
71
+ }
72
+ break;
73
+ }
74
+ case "explain": {
75
+ if (args.includes("--help"))
76
+ return printHelp("explain");
77
+ const { renderExplain } = await import("./explain.js");
78
+ console.log(renderExplain({ json: isJson }));
79
+ break;
80
+ }
81
+ case "completion": {
82
+ if (args.includes("--help"))
83
+ return printHelp("completion");
84
+ const { renderCompletion } = await import("./completion.js");
85
+ const shell = (args[1] ?? "").toLowerCase();
86
+ if (shell !== "zsh" && shell !== "bash" && shell !== "fish") {
87
+ console.log("Usage: hatchkit completion <zsh|bash|fish>");
88
+ process.exit(1);
89
+ }
90
+ console.log(renderCompletion(shell));
91
+ break;
92
+ }
55
93
  case "create":
56
- case undefined:
57
94
  if (args.includes("--help"))
58
95
  return printHelp("create");
59
96
  await handleCreate();
60
97
  break;
98
+ case undefined:
99
+ await handleNoArgs();
100
+ break;
61
101
  case "update":
62
102
  if (args.includes("--help"))
63
103
  return printHelp("update");
@@ -68,15 +108,77 @@ async function main() {
68
108
  return printHelp("keys");
69
109
  await handleKeys();
70
110
  break;
71
- case "provision":
111
+ case "add":
112
+ if (args.includes("--help"))
113
+ return printHelp("add");
114
+ await handleAdd();
115
+ break;
116
+ case "remove":
117
+ if (args.includes("--help"))
118
+ return printHelp("remove");
119
+ await handleRemove();
120
+ break;
121
+ case "rename-domain": {
122
+ if (args.includes("--help"))
123
+ return printHelp("rename-domain");
124
+ const { runRenameDomainCli } = await import("./deploy/rename-domain.js");
125
+ await runRenameDomainCli(args.slice(1), MONOREPO_ROOT);
126
+ break;
127
+ }
128
+ case "doctor": {
129
+ if (args.includes("--help"))
130
+ return printHelp("doctor");
131
+ const { runDoctor } = await import("./doctor.js");
132
+ await runDoctor({ json: isJson });
133
+ break;
134
+ }
135
+ case "dns": {
72
136
  if (args.includes("--help"))
73
- return printHelp("provision");
74
- await handleProvision();
137
+ return printHelp("dns");
138
+ await handleDns();
75
139
  break;
140
+ }
141
+ case "gh-pages":
142
+ case "pages": {
143
+ if (args.includes("--help"))
144
+ return printHelp("gh-pages");
145
+ if (command === "pages") {
146
+ console.log(chalk.yellow(" Note: `hatchkit pages` has been renamed to `hatchkit gh-pages`."));
147
+ }
148
+ const { runPagesSetup } = await import("./deploy/pages.js");
149
+ await runPagesSetup(resolve("."));
150
+ break;
151
+ }
76
152
  default:
77
153
  printHelp();
78
154
  }
79
155
  }
156
+ /** No-args: show the status-aware menu. If stdin is a TTY, also offer
157
+ * to kick off the most likely next step (setup or create). Agents
158
+ * running non-interactively just get the menu + exit 0. */
159
+ async function handleNoArgs() {
160
+ const { collectStatus, renderMenu } = await import("./status.js");
161
+ const s = collectStatus();
162
+ console.log(renderMenu(s));
163
+ if (!process.stdin.isTTY)
164
+ return;
165
+ const hasCore = s.providers.find((p) => p.key === "coolify")?.configured &&
166
+ s.providers.find((p) => p.key === "hetzner")?.configured &&
167
+ s.providers.find((p) => p.key === "dns")?.configured &&
168
+ s.providers.find((p) => p.key === "github")?.configured;
169
+ if (!hasCore) {
170
+ const ok = await confirm({ message: "Run `hatchkit setup` now?", default: true });
171
+ if (ok)
172
+ await runOnboarding();
173
+ return;
174
+ }
175
+ const ok = await confirm({
176
+ message: "Scaffold a new project now (`hatchkit create`)?",
177
+ default: true,
178
+ });
179
+ if (ok)
180
+ await handleCreate();
181
+ }
80
182
  async function handleKeys() {
81
183
  const sub = args[1];
82
184
  const projectName = args[2];
@@ -84,9 +186,10 @@ async function handleKeys() {
84
186
  console.log("Usage: hatchkit keys <show|push> <project-name>");
85
187
  process.exit(1);
86
188
  }
189
+ const isJson = args.includes("--json");
87
190
  switch (sub) {
88
191
  case "show":
89
- await showProjectKey(projectName);
192
+ await showProjectKey(projectName, { json: isJson });
90
193
  break;
91
194
  case "push":
92
195
  await pushProjectKeyToCoolify(projectName);
@@ -97,12 +200,12 @@ async function handleKeys() {
97
200
  process.exit(1);
98
201
  }
99
202
  }
100
- async function handleProvision() {
203
+ async function handleAdd() {
101
204
  // Positional args are optional — anything missing is prompted for.
102
- // hatchkit provision (fully interactive)
103
- // hatchkit provision raptor-runner (prompts for services)
104
- // hatchkit provision raptor-runner all
105
- // hatchkit provision raptor-runner glitchtip,resend
205
+ // hatchkit add (fully interactive)
206
+ // hatchkit add raptor-runner (prompts for services)
207
+ // hatchkit add raptor-runner all
208
+ // hatchkit add raptor-runner glitchtip,resend
106
209
  const positional = args.slice(1).filter((a) => !a.startsWith("--"));
107
210
  let baseName = positional[0];
108
211
  const rawService = positional[1];
@@ -111,7 +214,7 @@ async function handleProvision() {
111
214
  const { input } = await import("@inquirer/prompts");
112
215
  const { validateProjectName } = await import("./utils/validate.js");
113
216
  baseName = await input({
114
- message: "Base name for the new clients (e.g. raptor-runner):",
217
+ message: "Project name (e.g. raptor-runner):",
115
218
  validate: validateProjectName,
116
219
  });
117
220
  }
@@ -119,7 +222,7 @@ async function handleProvision() {
119
222
  if (!rawService) {
120
223
  const { checkbox } = await import("@inquirer/prompts");
121
224
  services = await checkbox({
122
- message: "Which services to provision (-dev and -prod pair each)?",
225
+ message: "Which services to add (-dev and -prod pair each)?",
123
226
  choices: [
124
227
  { name: "GlitchTip (error tracking)", value: "glitchtip", checked: true },
125
228
  { name: "OpenPanel (product analytics)", value: "openpanel", checked: true },
@@ -141,7 +244,126 @@ async function handleProvision() {
141
244
  }
142
245
  services = requested;
143
246
  }
144
- await runProvision({ baseName, services });
247
+ // Flag parsing:
248
+ // --no-write → never write; print a cache summary only
249
+ // --enable-dev-obs → also populate .env.development with GlitchTip/OpenPanel creds
250
+ // --surfaces=<shared|separate|server-only|client-only>
251
+ // --server-dir <path> → absolute or project-relative env dir for the server
252
+ // --client-dir <path> → same for the client
253
+ // (no surface flags) → prompt interactively
254
+ const noWrite = args.includes("--no-write");
255
+ const enableDevObs = args.includes("--enable-dev-obs");
256
+ const surfaceFlag = args.find((a) => a.startsWith("--surfaces="))?.slice("--surfaces=".length);
257
+ const serverDirIdx = args.indexOf("--server-dir");
258
+ const clientDirIdx = args.indexOf("--client-dir");
259
+ const serverDirFlag = serverDirIdx >= 0 ? args[serverDirIdx + 1] : undefined;
260
+ const clientDirFlag = clientDirIdx >= 0 ? args[clientDirIdx + 1] : undefined;
261
+ const { resolve: resolvePath } = await import("node:path");
262
+ const validSurfaceModes = ["shared", "separate", "server-only", "client-only"];
263
+ let surfaces = undefined;
264
+ if (noWrite) {
265
+ surfaces = false;
266
+ }
267
+ else if (surfaceFlag || serverDirFlag || clientDirFlag) {
268
+ // Non-interactive surface config: require every field we need.
269
+ if (!surfaceFlag || !validSurfaceModes.includes(surfaceFlag)) {
270
+ console.log(chalk.red(` --surfaces=<mode> is required when --server-dir/--client-dir is passed.\n Valid: ${validSurfaceModes.join(", ")}`));
271
+ process.exit(1);
272
+ }
273
+ const mode = surfaceFlag;
274
+ const needsServer = mode === "shared" || mode === "separate" || mode === "server-only";
275
+ const needsClient = mode === "shared" || mode === "separate" || mode === "client-only";
276
+ if (needsServer && !serverDirFlag) {
277
+ console.log(chalk.red(" --server-dir <path> is required for this --surfaces mode."));
278
+ process.exit(1);
279
+ }
280
+ if (needsClient && !clientDirFlag) {
281
+ console.log(chalk.red(" --client-dir <path> is required for this --surfaces mode."));
282
+ process.exit(1);
283
+ }
284
+ surfaces = {
285
+ mode,
286
+ serverEnvDir: needsServer ? resolvePath(serverDirFlag) : undefined,
287
+ clientEnvDir: needsClient ? resolvePath(clientDirFlag) : undefined,
288
+ };
289
+ }
290
+ await runProvision({ baseName, services, surfaces, enableDevObs });
291
+ }
292
+ async function handleRemove() {
293
+ // Mirrors handleAdd: `hatchkit remove [<name>] [<services>] [--dry-run] [--yes]`
294
+ // hatchkit remove (fully interactive)
295
+ // hatchkit remove raptor-runner (prompts for services)
296
+ // hatchkit remove raptor-runner all
297
+ // hatchkit remove raptor-runner glitchtip,resend
298
+ // hatchkit remove raptor-runner all --yes (skip confirmation)
299
+ const positional = args.slice(1).filter((a) => !a.startsWith("--"));
300
+ const dryRun = args.includes("--dry-run");
301
+ const skipConfirm = args.includes("--yes") || args.includes("-y");
302
+ let baseName = positional[0];
303
+ const rawService = positional[1];
304
+ const allServices = ["glitchtip", "openpanel", "resend"];
305
+ if (!baseName) {
306
+ const { input } = await import("@inquirer/prompts");
307
+ const { validateProjectName } = await import("./utils/validate.js");
308
+ baseName = await input({
309
+ message: "Project name to remove (e.g. raptor-runner):",
310
+ validate: validateProjectName,
311
+ });
312
+ }
313
+ let services;
314
+ if (!rawService) {
315
+ const { checkbox } = await import("@inquirer/prompts");
316
+ services = await checkbox({
317
+ message: "Which services to remove (-dev AND -prod clients each)?",
318
+ choices: [
319
+ { name: "GlitchTip (deletes the project)", value: "glitchtip", checked: true },
320
+ { name: "OpenPanel (deletes the project)", value: "openpanel", checked: true },
321
+ { name: "Resend (deletes the API key)", value: "resend", checked: true },
322
+ ],
323
+ required: true,
324
+ });
325
+ }
326
+ else if (rawService === "all") {
327
+ services = allServices;
328
+ }
329
+ else {
330
+ const requested = rawService.split(",").map((s) => s.trim().toLowerCase());
331
+ const invalid = requested.filter((s) => !allServices.includes(s));
332
+ if (invalid.length > 0) {
333
+ console.log(chalk.red(` Unknown service(s): ${invalid.join(", ")}`));
334
+ console.log(chalk.dim(` Valid: ${allServices.join(", ")}, or 'all'`));
335
+ process.exit(1);
336
+ }
337
+ services = requested;
338
+ }
339
+ // Confirmation — deletion is permanent upstream. Skip on --yes or --dry-run.
340
+ if (!skipConfirm && !dryRun) {
341
+ const { confirm } = await import("@inquirer/prompts");
342
+ const ok = await confirm({
343
+ message: `Delete -dev and -prod clients of "${baseName}" from ${services.join(", ")}? This can't be undone.`,
344
+ default: false,
345
+ });
346
+ if (!ok) {
347
+ console.log(chalk.dim(" Cancelled."));
348
+ return;
349
+ }
350
+ }
351
+ await runUnprovision({ baseName, services, dryRun });
352
+ }
353
+ async function handleDns() {
354
+ const sub = args[1];
355
+ switch (sub) {
356
+ case "link-to-cloudflare": {
357
+ const rest = args.slice(2);
358
+ const dryRun = rest.includes("--dry-run");
359
+ const domains = rest.filter((a) => !a.startsWith("--"));
360
+ const { runDnsLinkToCloudflare } = await import("./dns.js");
361
+ await runDnsLinkToCloudflare({ domains, dryRun });
362
+ break;
363
+ }
364
+ default:
365
+ printHelp("dns");
366
+ }
145
367
  }
146
368
  // ---------------------------------------------------------------------------
147
369
  // Commands
@@ -372,22 +594,12 @@ async function handleConfig() {
372
594
  const isGpuPlatform = (p) => gpuPlatforms.includes(p);
373
595
  switch (provider) {
374
596
  case "coolify":
375
- await ensureCoolify();
376
- break;
377
597
  case "hetzner":
378
- await ensureHetzner();
379
- break;
380
598
  case "dns":
381
- await ensureDns();
382
- break;
383
599
  case "glitchtip":
384
- await ensureGlitchtip();
385
- break;
386
600
  case "openpanel":
387
- await ensureOpenpanel();
388
- break;
389
601
  case "resend":
390
- await ensureResend();
602
+ await reconfigureProvider(provider);
391
603
  break;
392
604
  case "s3": {
393
605
  const { select } = await import("@inquirer/prompts");
@@ -399,7 +611,7 @@ async function handleConfig() {
399
611
  { name: "R2", value: "r2" },
400
612
  ],
401
613
  });
402
- await ensureS3(p);
614
+ await reconfigureProvider(`s3.${p}`);
403
615
  break;
404
616
  }
405
617
  default:
@@ -408,7 +620,7 @@ async function handleConfig() {
408
620
  console.log(chalk.dim(" Valid: coolify, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, resend"));
409
621
  return;
410
622
  }
411
- await ensureGpuProvider(provider);
623
+ await reconfigureProvider(`gpu.${provider}`);
412
624
  }
413
625
  break;
414
626
  }
@@ -471,13 +683,19 @@ function printHelp(topic) {
471
683
  `);
472
684
  return;
473
685
  }
474
- if (topic === "init") {
686
+ if (topic === "init" || topic === "setup") {
475
687
  console.log(`
476
- ${chalk.bold("hatchkit init")} — one-time onboarding
688
+ ${chalk.bold("hatchkit setup")} — one-time onboarding ${chalk.dim("(alias: init)")}
689
+
690
+ Interactively wires up every credential hatchkit needs:
691
+ - GitHub (via gh CLI)
692
+ - Coolify (URL + token)
693
+ - Hetzner Cloud, DNS provider, S3 (optional)
694
+ - GlitchTip, OpenPanel, Resend (optional)
477
695
 
478
- Prompts for: GitHub (via gh CLI), Coolify (URL + token), optionally
479
- Hetzner Cloud, DNS provider, S3, and GPU platforms. Tokens go to the
480
- OS keychain; metadata to ${chalk.dim(getConfigPath())}.
696
+ Tokens go to the OS keychain; metadata to
697
+ ${chalk.dim(getConfigPath())}.
698
+ Any skipped providers are prompted for on first use.
481
699
  `);
482
700
  return;
483
701
  }
@@ -516,34 +734,196 @@ function printHelp(topic) {
516
734
  `);
517
735
  return;
518
736
  }
519
- if (topic === "provision") {
737
+ if (topic === "gh-pages") {
738
+ console.log(`
739
+ ${chalk.bold("hatchkit gh-pages")} — wire GitHub Pages for the current repo
740
+
741
+ ${chalk.bold("Usage:")}
742
+ cd <project-dir> && hatchkit gh-pages
743
+
744
+ ${chalk.bold("What it does:")}
745
+ 1. Reads the repo via \`gh repo view\` (must be a GitHub repo you own).
746
+ 2. Scans the repo root + ${chalk.dim("docs/ site/ www/ web/")} for candidate sites:
747
+ - ${chalk.cyan("jekyll")} (Gemfile + _config.yml)
748
+ - ${chalk.cyan("node-build")} (package.json with a \`build\` script)
749
+ - ${chalk.cyan("static")} (index.html)
750
+ If multiple sites are found, prompts you to pick. If none are
751
+ found, prompts for kind + location manually.
752
+ 3. Enables Pages via the GitHub API with ${chalk.dim("build_type=workflow")}.
753
+ 4. Writes ${chalk.cyan(".github/workflows/gh-pages.yml")} tailored to the site kind.
754
+ Refuses to overwrite any existing Pages workflow in the repo.
755
+ 5. Optionally registers a custom domain + wires DNS:
756
+ - Cloudflare: auto-configured via API (uses your stored token)
757
+ - INWX / manual: prints the records you need to add
758
+ Also writes a ${chalk.cyan("CNAME")} file into the published folder (or
759
+ ${chalk.dim("public/")} for build-step projects).
760
+
761
+ ${chalk.bold("After running:")}
762
+ git add -A && git commit -m "ci: deploy to GitHub Pages" && git push
763
+
764
+ ${chalk.bold("Notes:")}
765
+ - Private repos need a paid GitHub plan for Pages. Free-tier repos
766
+ must be made public first.
767
+ - For ${chalk.dim("node-build")} sites, confirm the detected publish dir matches what
768
+ your build tool actually outputs (Vite → dist, CRA → build, etc).
769
+ - Monorepos / hybrids: if both the root and ${chalk.dim("docs/")} have sites, you'll
770
+ be prompted to pick one. Run the command twice if you want both.
771
+ `);
772
+ return;
773
+ }
774
+ if (topic === "dns") {
775
+ console.log(`
776
+ ${chalk.bold("hatchkit dns")} — DNS reconciliation helpers
777
+
778
+ ${chalk.bold("Subcommands:")}
779
+ link-to-cloudflare [domain...]
780
+ For each Cloudflare zone, push its nameservers to INWX as the
781
+ registrar delegation. Use after importing zones into Cloudflare
782
+ when you don't want to click through INWX per-domain.
783
+
784
+ No args → processes every zone the token can see.
785
+ Args → space-separated domain names, filters to those.
786
+ ${chalk.dim("--dry-run")} → print-only, no API calls.
787
+ ${chalk.dim("INWX_SANDBOX=1")} → use the OTE sandbox instead of production.
788
+
789
+ ${chalk.bold("Prerequisites:")}
790
+ Run ${chalk.cyan("hatchkit config add dns")} and choose Cloudflare, then answer
791
+ ${chalk.cyan("yes")} to "Is INWX your domain registrar?" when prompted.
792
+ `);
793
+ return;
794
+ }
795
+ if (topic === "doctor") {
796
+ console.log(`
797
+ ${chalk.bold("hatchkit doctor")} — verify every configured provider
798
+
799
+ Runs a read-only API call against each provider whose credentials are
800
+ stored (Coolify /version, Hetzner /servers, Cloudflare /tokens/verify,
801
+ Resend /domains, …). Reports ok / fail / not-configured per provider
802
+ and exits non-zero if any check fails. Safe to run repeatedly.
803
+ `);
804
+ return;
805
+ }
806
+ if (topic === "add") {
807
+ console.log(`
808
+ ${chalk.bold("hatchkit add")} — provision per-project clients and write env files
809
+
810
+ ${chalk.bold("Usage:")}
811
+ hatchkit add [<project-name>] [<services>] [flags]
812
+
813
+ ${chalk.bold("What it does:")}
814
+ · GlitchTip / OpenPanel: ${chalk.bold("one project per product")}, events tagged by
815
+ \`environment\` so dev / staging / prod share the same dashboard.
816
+ Written to ${chalk.cyan(".env.production")} only — dev noise pollutes real metrics.
817
+ Pass ${chalk.cyan("--enable-dev-obs")} to populate ${chalk.cyan(".env.development")} too.
818
+ · Resend: separate ${chalk.cyan("-dev")} and ${chalk.cyan("-prod")} API keys (audience
819
+ safety). Written to the server's dev + prod env respectively.
820
+ · ${chalk.cyan(".env.production")} is dotenvx-encrypted — commit-safe.
821
+ ${chalk.cyan(".env.development")} is plaintext — gitignored, not encrypted.
822
+ · A 0600 cache of every value is saved under
823
+ ${chalk.dim("<config-dir>/provisioned/<project>.*.env")} for recoverability.
824
+ ${chalk.dim("Secret values never hit stdout.")}
825
+
826
+ ${chalk.bold("Surfaces:")}
827
+ hatchkit asks which surfaces your project has. Options:
828
+ · ${chalk.cyan("shared")} — server + client, one obs project (recommended)
829
+ · ${chalk.cyan("server-only")} — no browser bundle (API, CLI, worker)
830
+ · ${chalk.cyan("client-only")} — static site / SPA with no backend
831
+ · ${chalk.cyan("separate")} — server + client, one obs project per surface
832
+
833
+ Env for each surface is written to its own directory (e.g.
834
+ ${chalk.dim("packages/server/.env.production")}, ${chalk.dim("packages/client/.env.production")}).
835
+
836
+ ${chalk.bold("Services:")}
837
+ glitchtip GLITCHTIP_DSN (server) / PUBLIC_GLITCHTIP_DSN (client)
838
+ openpanel OPENPANEL_* (server) / PUBLIC_OPENPANEL_* (client)
839
+ resend RESEND_API_KEY (server only)
840
+
841
+ ${chalk.bold("Flags:")}
842
+ --enable-dev-obs Also populate .env.development with obs creds.
843
+ --no-write Skip writing; save 0600 cache only.
844
+ --surfaces=<mode> shared | server-only | client-only | separate
845
+ --server-dir <path> Server env directory (skips prompt when set).
846
+ --client-dir <path> Client env directory (skips prompt when set).
847
+
848
+ ${chalk.bold("Examples:")}
849
+ hatchkit add
850
+ hatchkit add raptor-runner
851
+ hatchkit add raptor-runner all --enable-dev-obs
852
+ hatchkit add raptor-runner glitchtip,resend --no-write
853
+ hatchkit add raptor-runner all --surfaces=shared \\
854
+ --server-dir ./raptor-runner/packages/server \\
855
+ --client-dir ./raptor-runner/packages/client
856
+ `);
857
+ return;
858
+ }
859
+ if (topic === "remove") {
520
860
  console.log(`
521
- ${chalk.bold("hatchkit provision")} — create per-service clients for an existing project
861
+ ${chalk.bold("hatchkit remove")} — inverse of ${chalk.cyan("add")}: tear down per-project clients
522
862
 
523
863
  ${chalk.bold("Usage:")}
524
- hatchkit provision [<base-name>] [<services>]
864
+ hatchkit remove [<project-name>] [<services>] [--dry-run] [--yes]
525
865
 
526
- Both args are optional — anything missing is prompted for, including a
527
- multi-select of which services to run. ${chalk.dim("(<services> is 'all', a single")}
528
- ${chalk.dim("service, or a comma-separated list.)")}
866
+ Both positional args are optional — anything missing is prompted for.
867
+ ${chalk.dim("(<services> is 'all', a single service, or a comma-separated list.)")}
529
868
 
530
869
  ${chalk.bold("What it does:")}
531
- For every selected service, creates two clients:
532
- - ${chalk.cyan("<base-name>-dev")}
533
- - ${chalk.cyan("<base-name>-prod")}
534
- …and prints an env block for each, plus saves it under
535
- ${chalk.dim("<config-dir>/provisioned/<base-name>.{dev,prod}.env")}.
870
+ For every selected service, deletes both clients:
871
+ - ${chalk.cyan("<project-name>-dev")}
872
+ - ${chalk.cyan("<project-name>-prod")}
873
+ Also removes the local env cache at
874
+ ${chalk.dim("<config-dir>/provisioned/<project-name>.{dev,prod}.env")}.
875
+
876
+ Re-runs are idempotent — missing upstream resources log
877
+ ${chalk.dim("already gone")} and keep going.
536
878
 
537
879
  ${chalk.bold("Services:")}
538
- glitchtip Creates a GlitchTip project, returns GLITCHTIP_DSN
539
- openpanel Creates an OpenPanel client, returns OPENPANEL_CLIENT_ID/_SECRET
540
- resend Creates a restricted Resend API key, returns RESEND_API_KEY
880
+ glitchtip Deletes the GlitchTip project
881
+ openpanel Deletes the OpenPanel project (and clears cached creds)
882
+ resend Finds API keys by name and deletes them
883
+
884
+ ${chalk.bold("Options:")}
885
+ --dry-run Print what would be deleted; hit no APIs, remove no files.
886
+ --yes, -y Skip the interactive confirmation prompt.
541
887
 
542
888
  ${chalk.bold("Examples:")}
543
- hatchkit provision
544
- hatchkit provision raptor-runner
545
- hatchkit provision raptor-runner all
546
- hatchkit provision raptor-runner glitchtip,resend
889
+ hatchkit remove raptor-runner all
890
+ hatchkit remove raptor-runner glitchtip,resend --dry-run
891
+ hatchkit remove raptor-runner all --yes
892
+ `);
893
+ return;
894
+ }
895
+ if (topic === "rename-domain") {
896
+ console.log(`
897
+ ${chalk.bold("hatchkit rename-domain")} — move a project to a new domain
898
+
899
+ ${chalk.bold("Usage:")}
900
+ cd <project-dir> && hatchkit rename-domain --to <new-domain>
901
+ hatchkit rename-domain --dir <project-dir> --to <new-domain> --dry-run
902
+
903
+ ${chalk.bold("What it rewrites:")}
904
+ ${chalk.cyan(".hatchkit.json")} (manifest domain)
905
+ ${chalk.cyan("infra/terraform/stacks/<stack>/<name>.tfvars")} (domain + subdomain keys)
906
+ ${chalk.cyan("infra/stacks/<name>.env")} (APP_DOMAIN +
907
+ any line that mentions the
908
+ old full domain; skips
909
+ COOLIFY_URL)
910
+
911
+ ${chalk.bold("What it does NOT touch (you run these manually):")}
912
+ - ${chalk.dim("terraform apply")} — review the plan before destroying old records.
913
+ - Coolify app FQDN + redeploy — UI or API. New TLS cert: 1-3 min.
914
+ - ${chalk.dim("hatchkit dns link-to-cloudflare")} if NS flip is needed.
915
+ - OAuth redirect URIs, Stripe webhooks, app-code references.
916
+
917
+ ${chalk.bold("Options:")}
918
+ --to <domain> Target domain (prompted if omitted).
919
+ --dir <path> Project dir (defaults to cwd).
920
+ --dry-run Show the plan; don't write.
921
+ --yes, -y Skip the confirmation prompt.
922
+
923
+ ${chalk.bold("Example:")}
924
+ cd ~/src/my-project
925
+ hatchkit rename-domain --to my-project.com --dry-run
926
+ hatchkit rename-domain --to my-project.com
547
927
  `);
548
928
  return;
549
929
  }
@@ -552,30 +932,88 @@ function printHelp(topic) {
552
932
  ${chalk.bold("hatchkit config")} — manage provider credentials
553
933
 
554
934
  ${chalk.bold("Subcommands:")}
555
- config Show status of every configured provider
935
+ config Show status of every configured provider (alias: \`status\`)
556
936
  config add <p> Configure a provider
557
- (coolify, hetzner, dns, s3, modal, runpod, hf, replicate)
937
+ (coolify, hetzner, dns, s3, modal, runpod, hf, replicate,
938
+ glitchtip, openpanel, resend)
558
939
  config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
940
+ `);
941
+ return;
942
+ }
943
+ if (topic === "status") {
944
+ console.log(`
945
+ ${chalk.bold("hatchkit status")} — show provider status + next-step hint
946
+
947
+ ${chalk.bold("Usage:")}
948
+ hatchkit status [--json]
949
+
950
+ ${chalk.bold("Output:")}
951
+ Human: ✓/· per provider, next-best-step, config path.
952
+ JSON: full StatusSnapshot — stable shape for agents / scripts.
953
+ `);
954
+ return;
955
+ }
956
+ if (topic === "explain") {
957
+ console.log(`
958
+ ${chalk.bold("hatchkit explain")} — one-page mental model of the CLI
959
+
960
+ ${chalk.bold("Usage:")}
961
+ hatchkit explain [--json]
962
+
963
+ Dumps a plain-text (or JSON) description of concepts, commands, and
964
+ the canonical workflow. Useful for humans with zero context and for
965
+ agents that need to "grok" hatchkit before driving it.
966
+ `);
967
+ return;
968
+ }
969
+ if (topic === "completion") {
970
+ console.log(`
971
+ ${chalk.bold("hatchkit completion")} — print a shell-completion script
972
+
973
+ ${chalk.bold("Usage:")}
974
+ hatchkit completion <zsh|bash|fish>
975
+
976
+ Pipe into your shell config, e.g.:
977
+ hatchkit completion zsh > ~/.zsh/completions/_hatchkit
978
+ hatchkit completion bash > /usr/local/etc/bash_completion.d/hatchkit
979
+ hatchkit completion fish > ~/.config/fish/completions/hatchkit.fish
559
980
  `);
560
981
  return;
561
982
  }
562
983
  console.log(`
563
984
  ${chalk.bold("Usage:")} hatchkit <command> [options]
564
985
 
565
- ${chalk.bold("Commands:")}
566
- create Scaffold a new project (default)
567
- provision Create GlitchTip / OpenPanel / Resend clients for an existing project
986
+ ${chalk.bold("Getting started:")}
987
+ setup One-time onboarding wires up all credentials (alias: init)
988
+ status Show what's configured and what's next
989
+ doctor Health-check every provider with contextual fix hints
990
+ explain One-page mental model of the CLI
991
+
992
+ ${chalk.bold("Projects:")}
993
+ create Scaffold a new project (interactive)
568
994
  update Add features to an already-scaffolded project (run in project dir)
995
+ add Create GlitchTip / OpenPanel / Resend clients for an existing project
996
+ remove Delete the -dev/-prod clients created by 'add' (inverse of add)
997
+ rename-domain Move a scaffolded project to a new domain (rewrites tfvars/env/manifest)
998
+ gh-pages Wire GitHub Pages for the current repo (static / Vite / Jekyll — with DNS)
999
+ dns DNS reconciliation helpers (link-to-cloudflare, …)
569
1000
  keys show <p> Print the dotenvx private key for a project
570
1001
  keys push <p> Push the key onto the project's Coolify app
571
- init Run first-time setup / onboarding
572
- config Show provider status
573
- config add <p> Configure a provider (coolify, hetzner, dns, s3, modal, etc.)
1002
+
1003
+ ${chalk.bold("Config:")}
1004
+ config Show provider status (same as \`status\`)
1005
+ config add <p> Configure a provider (coolify, hetzner, dns, s3, modal, …)
574
1006
  config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
575
1007
 
1008
+ ${chalk.bold("For agents / scripts:")}
1009
+ status --json StatusSnapshot as JSON
1010
+ doctor --json Per-provider health with fix hints as JSON
1011
+ completion <shell> Print a zsh/bash/fish completion script
1012
+
576
1013
  ${chalk.bold("Options:")}
577
1014
  --version, -v Print the CLI version
578
1015
  --help, -h Show this help message (pass to a subcommand for detail)
1016
+ --json Machine-readable output (status, doctor, explain)
579
1017
  --dry-run (with \`create\`) show what would change without writing
580
1018
  --yes, -y (with \`create\`) skip prompts, use defaults / --config values
581
1019
  --config <path> (with \`create\`) load JSON overrides for ProjectConfig fields
@@ -585,8 +1023,8 @@ function printHelp(topic) {
585
1023
 
586
1024
  ${chalk.bold("Environment:")}
587
1025
  HATCHKIT_CONF_DIR Override the config/ports-registry location
588
- (advanced — useful for isolated per-workspace state
589
- or automated testing).
1026
+
1027
+ ${chalk.dim("Run `hatchkit help <command>` for per-command detail.")}
590
1028
  `);
591
1029
  }
592
1030
  // ---------------------------------------------------------------------------