hatchkit 0.1.2 → 0.1.4

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 (113) 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 +439 -127
  8. package/dist/config.js.map +1 -1
  9. package/dist/deploy/coolify-mongo.d.ts +12 -0
  10. package/dist/deploy/coolify-mongo.d.ts.map +1 -0
  11. package/dist/deploy/coolify-mongo.js +109 -0
  12. package/dist/deploy/coolify-mongo.js.map +1 -0
  13. package/dist/deploy/gpu.d.ts +9 -2
  14. package/dist/deploy/gpu.d.ts.map +1 -1
  15. package/dist/deploy/gpu.js +63 -39
  16. package/dist/deploy/gpu.js.map +1 -1
  17. package/dist/deploy/keys.d.ts +6 -2
  18. package/dist/deploy/keys.d.ts.map +1 -1
  19. package/dist/deploy/keys.js +16 -2
  20. package/dist/deploy/keys.js.map +1 -1
  21. package/dist/deploy/pages.d.ts +2 -0
  22. package/dist/deploy/pages.d.ts.map +1 -0
  23. package/dist/deploy/pages.js +537 -0
  24. package/dist/deploy/pages.js.map +1 -0
  25. package/dist/deploy/rename-domain.d.ts +55 -0
  26. package/dist/deploy/rename-domain.d.ts.map +1 -0
  27. package/dist/deploy/rename-domain.js +290 -0
  28. package/dist/deploy/rename-domain.js.map +1 -0
  29. package/dist/deploy/terraform.d.ts.map +1 -1
  30. package/dist/deploy/terraform.js +90 -0
  31. package/dist/deploy/terraform.js.map +1 -1
  32. package/dist/dns.d.ts +7 -0
  33. package/dist/dns.d.ts.map +1 -0
  34. package/dist/dns.js +124 -0
  35. package/dist/dns.js.map +1 -0
  36. package/dist/doctor.d.ts +13 -0
  37. package/dist/doctor.d.ts.map +1 -0
  38. package/dist/doctor.js +368 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/explain.d.ts +4 -0
  41. package/dist/explain.d.ts.map +1 -0
  42. package/dist/explain.js +173 -0
  43. package/dist/explain.js.map +1 -0
  44. package/dist/index.js +521 -61
  45. package/dist/index.js.map +1 -1
  46. package/dist/prompts.d.ts +15 -2
  47. package/dist/prompts.d.ts.map +1 -1
  48. package/dist/prompts.js +52 -7
  49. package/dist/prompts.js.map +1 -1
  50. package/dist/provision/glitchtip.d.ts +3 -0
  51. package/dist/provision/glitchtip.d.ts.map +1 -1
  52. package/dist/provision/glitchtip.js +18 -0
  53. package/dist/provision/glitchtip.js.map +1 -1
  54. package/dist/provision/index.d.ts +26 -0
  55. package/dist/provision/index.d.ts.map +1 -1
  56. package/dist/provision/index.js +435 -60
  57. package/dist/provision/index.js.map +1 -1
  58. package/dist/provision/openpanel.d.ts +7 -0
  59. package/dist/provision/openpanel.d.ts.map +1 -1
  60. package/dist/provision/openpanel.js +113 -48
  61. package/dist/provision/openpanel.js.map +1 -1
  62. package/dist/provision/resend.d.ts +23 -1
  63. package/dist/provision/resend.d.ts.map +1 -1
  64. package/dist/provision/resend.js +62 -1
  65. package/dist/provision/resend.js.map +1 -1
  66. package/dist/provision/write-env.d.ts +31 -0
  67. package/dist/provision/write-env.d.ts.map +1 -0
  68. package/dist/provision/write-env.js +94 -0
  69. package/dist/provision/write-env.js.map +1 -0
  70. package/dist/scaffold/app.js +8 -0
  71. package/dist/scaffold/app.js.map +1 -1
  72. package/dist/scaffold/dotenvx.d.ts.map +1 -1
  73. package/dist/scaffold/dotenvx.js +8 -1
  74. package/dist/scaffold/dotenvx.js.map +1 -1
  75. package/dist/scaffold/infra.d.ts.map +1 -1
  76. package/dist/scaffold/infra.js +18 -1
  77. package/dist/scaffold/infra.js.map +1 -1
  78. package/dist/scaffold/manifest.d.ts +4 -2
  79. package/dist/scaffold/manifest.d.ts.map +1 -1
  80. package/dist/scaffold/manifest.js +1 -1
  81. package/dist/scaffold/manifest.js.map +1 -1
  82. package/dist/scaffold/ml-client.d.ts +9 -2
  83. package/dist/scaffold/ml-client.d.ts.map +1 -1
  84. package/dist/scaffold/ml-client.js +11 -1
  85. package/dist/scaffold/ml-client.js.map +1 -1
  86. package/dist/status.d.ts +30 -0
  87. package/dist/status.d.ts.map +1 -0
  88. package/dist/status.js +169 -0
  89. package/dist/status.js.map +1 -0
  90. package/dist/templates/addons/analytics/sentry.ts.hbs +6 -0
  91. package/dist/templates/base/env.example.hbs +10 -0
  92. package/dist/templates/base/src/config.ts.hbs +24 -4
  93. package/dist/utils/cloudflare-api.d.ts +30 -0
  94. package/dist/utils/cloudflare-api.d.ts.map +1 -0
  95. package/dist/utils/cloudflare-api.js +85 -0
  96. package/dist/utils/cloudflare-api.js.map +1 -0
  97. package/dist/utils/coolify-api.d.ts +47 -1
  98. package/dist/utils/coolify-api.d.ts.map +1 -1
  99. package/dist/utils/coolify-api.js +75 -4
  100. package/dist/utils/coolify-api.js.map +1 -1
  101. package/dist/utils/flags.d.ts.map +1 -1
  102. package/dist/utils/flags.js +4 -0
  103. package/dist/utils/flags.js.map +1 -1
  104. package/dist/utils/inwx-api.d.ts +36 -0
  105. package/dist/utils/inwx-api.d.ts.map +1 -0
  106. package/dist/utils/inwx-api.js +105 -0
  107. package/dist/utils/inwx-api.js.map +1 -0
  108. package/dist/utils/secrets.d.ts +8 -1
  109. package/dist/utils/secrets.d.ts.map +1 -1
  110. package/dist/utils/secrets.js +8 -1
  111. package/dist/utils/secrets.js.map +1 -1
  112. package/package.json +5 -4
  113. package/scripts/release-prep.mjs +130 -0
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,10 +35,15 @@ 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) {
@@ -53,12 +58,46 @@ async function main() {
53
58
  return printHelp("config");
54
59
  await handleConfig();
55
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
+ }
56
93
  case "create":
57
- case undefined:
58
94
  if (args.includes("--help"))
59
95
  return printHelp("create");
60
96
  await handleCreate();
61
97
  break;
98
+ case undefined:
99
+ await handleNoArgs();
100
+ break;
62
101
  case "update":
63
102
  if (args.includes("--help"))
64
103
  return printHelp("update");
@@ -74,10 +113,72 @@ async function main() {
74
113
  return printHelp("add");
75
114
  await handleAdd();
76
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": {
136
+ if (args.includes("--help"))
137
+ return printHelp("dns");
138
+ await handleDns();
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
+ }
77
152
  default:
78
153
  printHelp();
79
154
  }
80
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
+ }
81
182
  async function handleKeys() {
82
183
  const sub = args[1];
83
184
  const projectName = args[2];
@@ -85,9 +186,10 @@ async function handleKeys() {
85
186
  console.log("Usage: hatchkit keys <show|push> <project-name>");
86
187
  process.exit(1);
87
188
  }
189
+ const isJson = args.includes("--json");
88
190
  switch (sub) {
89
191
  case "show":
90
- await showProjectKey(projectName);
192
+ await showProjectKey(projectName, { json: isJson });
91
193
  break;
92
194
  case "push":
93
195
  await pushProjectKeyToCoolify(projectName);
@@ -142,7 +244,126 @@ async function handleAdd() {
142
244
  }
143
245
  services = requested;
144
246
  }
145
- 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
+ }
146
367
  }
147
368
  // ---------------------------------------------------------------------------
148
369
  // Commands
@@ -276,6 +497,22 @@ async function handleCreate() {
276
497
  // Step 6: Coolify setup
277
498
  if (config.runDeployment) {
278
499
  await runCoolifySetup(config, INFRA_ROOT);
500
+ // Provision a per-project MongoDB container on Coolify when the
501
+ // user picked that path. Best-effort: a failure here doesn't undo
502
+ // the app deploy — we surface clear instructions instead.
503
+ if (config.mongodbProvider === "coolify" && config.scaffoldRepo) {
504
+ try {
505
+ const { provisionCoolifyMongo } = await import("./deploy/coolify-mongo.js");
506
+ const serverEnvDir = join(appDir, "packages/server");
507
+ await provisionCoolifyMongo(config, serverEnvDir);
508
+ }
509
+ catch (err) {
510
+ console.log(chalk.yellow(` Couldn't auto-provision MongoDB: ${err.message}`));
511
+ console.log(chalk.dim(` Create one manually in Coolify: New → Database → MongoDB,\n` +
512
+ ` then set MONGODB_URI on the app's env (or run\n` +
513
+ ` \`dotenvx set MONGODB_URI <url> -f packages/server/.env.production\`).`));
514
+ }
515
+ }
279
516
  // Push the dotenvx private key to Coolify so the starter's server
280
517
  // can decrypt .env.production at runtime. Best-effort — if the
281
518
  // Coolify app doesn't exist yet (race with the stack script), we
@@ -291,24 +528,37 @@ async function handleCreate() {
291
528
  }
292
529
  }
293
530
  // Step 7: Deploy ML services
294
- if (config.runDeployment && deploy.length > 0 && config.gpuPlatform) {
295
- const endpoints = await deployMlServices(deploy, config.gpuPlatform, SERVICES_ROOT, config.customHfModelId);
531
+ if (config.runDeployment &&
532
+ deploy.length > 0 &&
533
+ config.gpuPlatforms &&
534
+ config.gpuPlatforms.length > 0) {
535
+ const endpoints = await deployMlServices(deploy, config.gpuPlatforms, SERVICES_ROOT, config.customHfModelId);
296
536
  // Print env vars to set
297
537
  if (Object.keys(endpoints).length > 0) {
538
+ const { mlPlatformUrlEnv } = await import("./scaffold/ml-client.js");
539
+ const knownServices = [
540
+ "3d-extraction",
541
+ "subtitles",
542
+ "image-recognition",
543
+ "background-removal",
544
+ "custom-hf",
545
+ ];
298
546
  console.log(chalk.bold("\n ML service endpoints (add to Coolify env):"));
299
- for (const [service, endpoint] of Object.entries(endpoints)) {
300
- // Service keys come from our own `deploy: MlService[]` so the
301
- // cast is sound, but narrow via the literal-array check so a
302
- // stray unknown slips don't silently format wrong.
303
- const knownServices = [
304
- "3d-extraction",
305
- "subtitles",
306
- "image-recognition",
307
- "background-removal",
308
- "custom-hf",
309
- ];
310
- if (knownServices.includes(service)) {
311
- console.log(chalk.dim(` ${mlEnvVarName(service)}=${endpoint}`));
547
+ console.log(chalk.dim(` ML_BACKEND=${config.gpuPlatforms[0]}`));
548
+ for (const [service, byPlatform] of Object.entries(endpoints)) {
549
+ if (!knownServices.includes(service))
550
+ continue;
551
+ const svc = service;
552
+ // Per-platform URL — the runtime config picks one based on ML_BACKEND.
553
+ for (const [platform, url] of Object.entries(byPlatform)) {
554
+ if (!url)
555
+ continue;
556
+ console.log(chalk.dim(` ${mlPlatformUrlEnv(svc, platform)}=${url}`));
557
+ }
558
+ // Legacy ENDPOINT for back-compat — points at the default platform.
559
+ const defaultUrl = byPlatform[config.gpuPlatforms[0]];
560
+ if (defaultUrl) {
561
+ console.log(chalk.dim(` ${mlEnvVarName(svc)}=${defaultUrl}`));
312
562
  }
313
563
  }
314
564
  }
@@ -373,22 +623,12 @@ async function handleConfig() {
373
623
  const isGpuPlatform = (p) => gpuPlatforms.includes(p);
374
624
  switch (provider) {
375
625
  case "coolify":
376
- await ensureCoolify();
377
- break;
378
626
  case "hetzner":
379
- await ensureHetzner();
380
- break;
381
627
  case "dns":
382
- await ensureDns();
383
- break;
384
628
  case "glitchtip":
385
- await ensureGlitchtip();
386
- break;
387
629
  case "openpanel":
388
- await ensureOpenpanel();
389
- break;
390
630
  case "resend":
391
- await ensureResend();
631
+ await reconfigureProvider(provider);
392
632
  break;
393
633
  case "s3": {
394
634
  const { select } = await import("@inquirer/prompts");
@@ -400,7 +640,7 @@ async function handleConfig() {
400
640
  { name: "R2", value: "r2" },
401
641
  ],
402
642
  });
403
- await ensureS3(p);
643
+ await reconfigureProvider(`s3.${p}`);
404
644
  break;
405
645
  }
406
646
  default:
@@ -409,7 +649,7 @@ async function handleConfig() {
409
649
  console.log(chalk.dim(" Valid: coolify, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, resend"));
410
650
  return;
411
651
  }
412
- await ensureGpuProvider(provider);
652
+ await reconfigureProvider(`gpu.${provider}`);
413
653
  }
414
654
  break;
415
655
  }
@@ -520,37 +760,199 @@ function printHelp(topic) {
520
760
 
521
761
  ${chalk.bold("Removal is not supported.")} Removing features could delete
522
762
  user code — remove manually + edit the manifest.
763
+ `);
764
+ return;
765
+ }
766
+ if (topic === "gh-pages") {
767
+ console.log(`
768
+ ${chalk.bold("hatchkit gh-pages")} — wire GitHub Pages for the current repo
769
+
770
+ ${chalk.bold("Usage:")}
771
+ cd <project-dir> && hatchkit gh-pages
772
+
773
+ ${chalk.bold("What it does:")}
774
+ 1. Reads the repo via \`gh repo view\` (must be a GitHub repo you own).
775
+ 2. Scans the repo root + ${chalk.dim("docs/ site/ www/ web/")} for candidate sites:
776
+ - ${chalk.cyan("jekyll")} (Gemfile + _config.yml)
777
+ - ${chalk.cyan("node-build")} (package.json with a \`build\` script)
778
+ - ${chalk.cyan("static")} (index.html)
779
+ If multiple sites are found, prompts you to pick. If none are
780
+ found, prompts for kind + location manually.
781
+ 3. Enables Pages via the GitHub API with ${chalk.dim("build_type=workflow")}.
782
+ 4. Writes ${chalk.cyan(".github/workflows/gh-pages.yml")} tailored to the site kind.
783
+ Refuses to overwrite any existing Pages workflow in the repo.
784
+ 5. Optionally registers a custom domain + wires DNS:
785
+ - Cloudflare: auto-configured via API (uses your stored token)
786
+ - INWX / manual: prints the records you need to add
787
+ Also writes a ${chalk.cyan("CNAME")} file into the published folder (or
788
+ ${chalk.dim("public/")} for build-step projects).
789
+
790
+ ${chalk.bold("After running:")}
791
+ git add -A && git commit -m "ci: deploy to GitHub Pages" && git push
792
+
793
+ ${chalk.bold("Notes:")}
794
+ - Private repos need a paid GitHub plan for Pages. Free-tier repos
795
+ must be made public first.
796
+ - For ${chalk.dim("node-build")} sites, confirm the detected publish dir matches what
797
+ your build tool actually outputs (Vite → dist, CRA → build, etc).
798
+ - Monorepos / hybrids: if both the root and ${chalk.dim("docs/")} have sites, you'll
799
+ be prompted to pick one. Run the command twice if you want both.
800
+ `);
801
+ return;
802
+ }
803
+ if (topic === "dns") {
804
+ console.log(`
805
+ ${chalk.bold("hatchkit dns")} — DNS reconciliation helpers
806
+
807
+ ${chalk.bold("Subcommands:")}
808
+ link-to-cloudflare [domain...]
809
+ For each Cloudflare zone, push its nameservers to INWX as the
810
+ registrar delegation. Use after importing zones into Cloudflare
811
+ when you don't want to click through INWX per-domain.
812
+
813
+ No args → processes every zone the token can see.
814
+ Args → space-separated domain names, filters to those.
815
+ ${chalk.dim("--dry-run")} → print-only, no API calls.
816
+ ${chalk.dim("INWX_SANDBOX=1")} → use the OTE sandbox instead of production.
817
+
818
+ ${chalk.bold("Prerequisites:")}
819
+ Run ${chalk.cyan("hatchkit config add dns")} and choose Cloudflare, then answer
820
+ ${chalk.cyan("yes")} to "Is INWX your domain registrar?" when prompted.
821
+ `);
822
+ return;
823
+ }
824
+ if (topic === "doctor") {
825
+ console.log(`
826
+ ${chalk.bold("hatchkit doctor")} — verify every configured provider
827
+
828
+ Runs a read-only API call against each provider whose credentials are
829
+ stored (Coolify /version, Hetzner /servers, Cloudflare /tokens/verify,
830
+ Resend /domains, …). Reports ok / fail / not-configured per provider
831
+ and exits non-zero if any check fails. Safe to run repeatedly.
523
832
  `);
524
833
  return;
525
834
  }
526
835
  if (topic === "add") {
527
836
  console.log(`
528
- ${chalk.bold("hatchkit add")} — create per-service clients for an existing project
837
+ ${chalk.bold("hatchkit add")} — provision per-project clients and write env files
838
+
839
+ ${chalk.bold("Usage:")}
840
+ hatchkit add [<project-name>] [<services>] [flags]
841
+
842
+ ${chalk.bold("What it does:")}
843
+ · GlitchTip / OpenPanel: ${chalk.bold("one project per product")}, events tagged by
844
+ \`environment\` so dev / staging / prod share the same dashboard.
845
+ Written to ${chalk.cyan(".env.production")} only — dev noise pollutes real metrics.
846
+ Pass ${chalk.cyan("--enable-dev-obs")} to populate ${chalk.cyan(".env.development")} too.
847
+ · Resend: separate ${chalk.cyan("-dev")} and ${chalk.cyan("-prod")} API keys (audience
848
+ safety). Written to the server's dev + prod env respectively.
849
+ · ${chalk.cyan(".env.production")} is dotenvx-encrypted — commit-safe.
850
+ ${chalk.cyan(".env.development")} is plaintext — gitignored, not encrypted.
851
+ · A 0600 cache of every value is saved under
852
+ ${chalk.dim("<config-dir>/provisioned/<project>.*.env")} for recoverability.
853
+ ${chalk.dim("Secret values never hit stdout.")}
854
+
855
+ ${chalk.bold("Surfaces:")}
856
+ hatchkit asks which surfaces your project has. Options:
857
+ · ${chalk.cyan("shared")} — server + client, one obs project (recommended)
858
+ · ${chalk.cyan("server-only")} — no browser bundle (API, CLI, worker)
859
+ · ${chalk.cyan("client-only")} — static site / SPA with no backend
860
+ · ${chalk.cyan("separate")} — server + client, one obs project per surface
861
+
862
+ Env for each surface is written to its own directory (e.g.
863
+ ${chalk.dim("packages/server/.env.production")}, ${chalk.dim("packages/client/.env.production")}).
864
+
865
+ ${chalk.bold("Services:")}
866
+ glitchtip GLITCHTIP_DSN (server) / PUBLIC_GLITCHTIP_DSN (client)
867
+ openpanel OPENPANEL_* (server) / PUBLIC_OPENPANEL_* (client)
868
+ resend RESEND_API_KEY (server only)
869
+
870
+ ${chalk.bold("Flags:")}
871
+ --enable-dev-obs Also populate .env.development with obs creds.
872
+ --no-write Skip writing; save 0600 cache only.
873
+ --surfaces=<mode> shared | server-only | client-only | separate
874
+ --server-dir <path> Server env directory (skips prompt when set).
875
+ --client-dir <path> Client env directory (skips prompt when set).
876
+
877
+ ${chalk.bold("Examples:")}
878
+ hatchkit add
879
+ hatchkit add raptor-runner
880
+ hatchkit add raptor-runner all --enable-dev-obs
881
+ hatchkit add raptor-runner glitchtip,resend --no-write
882
+ hatchkit add raptor-runner all --surfaces=shared \\
883
+ --server-dir ./raptor-runner/packages/server \\
884
+ --client-dir ./raptor-runner/packages/client
885
+ `);
886
+ return;
887
+ }
888
+ if (topic === "remove") {
889
+ console.log(`
890
+ ${chalk.bold("hatchkit remove")} — inverse of ${chalk.cyan("add")}: tear down per-project clients
529
891
 
530
892
  ${chalk.bold("Usage:")}
531
- hatchkit add [<project-name>] [<services>]
893
+ hatchkit remove [<project-name>] [<services>] [--dry-run] [--yes]
532
894
 
533
- Both args are optional — anything missing is prompted for, including a
534
- multi-select of which services to run. ${chalk.dim("(<services> is 'all', a single")}
535
- ${chalk.dim("service, or a comma-separated list.)")}
895
+ Both positional args are optional — anything missing is prompted for.
896
+ ${chalk.dim("(<services> is 'all', a single service, or a comma-separated list.)")}
536
897
 
537
898
  ${chalk.bold("What it does:")}
538
- For every selected service, creates two clients:
899
+ For every selected service, deletes both clients:
539
900
  - ${chalk.cyan("<project-name>-dev")}
540
901
  - ${chalk.cyan("<project-name>-prod")}
541
- …and prints an env block for each, plus saves it under
902
+ Also removes the local env cache at
542
903
  ${chalk.dim("<config-dir>/provisioned/<project-name>.{dev,prod}.env")}.
543
904
 
905
+ Re-runs are idempotent — missing upstream resources log
906
+ ${chalk.dim("already gone")} and keep going.
907
+
544
908
  ${chalk.bold("Services:")}
545
- glitchtip Creates a GlitchTip project, returns GLITCHTIP_DSN
546
- openpanel Creates an OpenPanel client, returns OPENPANEL_CLIENT_ID/_SECRET
547
- resend Creates a restricted Resend API key, returns RESEND_API_KEY
909
+ glitchtip Deletes the GlitchTip project
910
+ openpanel Deletes the OpenPanel project (and clears cached creds)
911
+ resend Finds API keys by name and deletes them
912
+
913
+ ${chalk.bold("Options:")}
914
+ --dry-run Print what would be deleted; hit no APIs, remove no files.
915
+ --yes, -y Skip the interactive confirmation prompt.
548
916
 
549
917
  ${chalk.bold("Examples:")}
550
- hatchkit add
551
- hatchkit add raptor-runner
552
- hatchkit add raptor-runner all
553
- hatchkit add raptor-runner glitchtip,resend
918
+ hatchkit remove raptor-runner all
919
+ hatchkit remove raptor-runner glitchtip,resend --dry-run
920
+ hatchkit remove raptor-runner all --yes
921
+ `);
922
+ return;
923
+ }
924
+ if (topic === "rename-domain") {
925
+ console.log(`
926
+ ${chalk.bold("hatchkit rename-domain")} — move a project to a new domain
927
+
928
+ ${chalk.bold("Usage:")}
929
+ cd <project-dir> && hatchkit rename-domain --to <new-domain>
930
+ hatchkit rename-domain --dir <project-dir> --to <new-domain> --dry-run
931
+
932
+ ${chalk.bold("What it rewrites:")}
933
+ ${chalk.cyan(".hatchkit.json")} (manifest domain)
934
+ ${chalk.cyan("infra/terraform/stacks/<stack>/<name>.tfvars")} (domain + subdomain keys)
935
+ ${chalk.cyan("infra/stacks/<name>.env")} (APP_DOMAIN +
936
+ any line that mentions the
937
+ old full domain; skips
938
+ COOLIFY_URL)
939
+
940
+ ${chalk.bold("What it does NOT touch (you run these manually):")}
941
+ - ${chalk.dim("terraform apply")} — review the plan before destroying old records.
942
+ - Coolify app FQDN + redeploy — UI or API. New TLS cert: 1-3 min.
943
+ - ${chalk.dim("hatchkit dns link-to-cloudflare")} if NS flip is needed.
944
+ - OAuth redirect URIs, Stripe webhooks, app-code references.
945
+
946
+ ${chalk.bold("Options:")}
947
+ --to <domain> Target domain (prompted if omitted).
948
+ --dir <path> Project dir (defaults to cwd).
949
+ --dry-run Show the plan; don't write.
950
+ --yes, -y Skip the confirmation prompt.
951
+
952
+ ${chalk.bold("Example:")}
953
+ cd ~/src/my-project
954
+ hatchkit rename-domain --to my-project.com --dry-run
955
+ hatchkit rename-domain --to my-project.com
554
956
  `);
555
957
  return;
556
958
  }
@@ -559,30 +961,88 @@ function printHelp(topic) {
559
961
  ${chalk.bold("hatchkit config")} — manage provider credentials
560
962
 
561
963
  ${chalk.bold("Subcommands:")}
562
- config Show status of every configured provider
964
+ config Show status of every configured provider (alias: \`status\`)
563
965
  config add <p> Configure a provider
564
- (coolify, hetzner, dns, s3, modal, runpod, hf, replicate)
966
+ (coolify, hetzner, dns, s3, modal, runpod, hf, replicate,
967
+ glitchtip, openpanel, resend)
565
968
  config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
969
+ `);
970
+ return;
971
+ }
972
+ if (topic === "status") {
973
+ console.log(`
974
+ ${chalk.bold("hatchkit status")} — show provider status + next-step hint
975
+
976
+ ${chalk.bold("Usage:")}
977
+ hatchkit status [--json]
978
+
979
+ ${chalk.bold("Output:")}
980
+ Human: ✓/· per provider, next-best-step, config path.
981
+ JSON: full StatusSnapshot — stable shape for agents / scripts.
982
+ `);
983
+ return;
984
+ }
985
+ if (topic === "explain") {
986
+ console.log(`
987
+ ${chalk.bold("hatchkit explain")} — one-page mental model of the CLI
988
+
989
+ ${chalk.bold("Usage:")}
990
+ hatchkit explain [--json]
991
+
992
+ Dumps a plain-text (or JSON) description of concepts, commands, and
993
+ the canonical workflow. Useful for humans with zero context and for
994
+ agents that need to "grok" hatchkit before driving it.
995
+ `);
996
+ return;
997
+ }
998
+ if (topic === "completion") {
999
+ console.log(`
1000
+ ${chalk.bold("hatchkit completion")} — print a shell-completion script
1001
+
1002
+ ${chalk.bold("Usage:")}
1003
+ hatchkit completion <zsh|bash|fish>
1004
+
1005
+ Pipe into your shell config, e.g.:
1006
+ hatchkit completion zsh > ~/.zsh/completions/_hatchkit
1007
+ hatchkit completion bash > /usr/local/etc/bash_completion.d/hatchkit
1008
+ hatchkit completion fish > ~/.config/fish/completions/hatchkit.fish
566
1009
  `);
567
1010
  return;
568
1011
  }
569
1012
  console.log(`
570
1013
  ${chalk.bold("Usage:")} hatchkit <command> [options]
571
1014
 
572
- ${chalk.bold("Commands:")}
573
- create Scaffold a new project (default)
574
- add Create GlitchTip / OpenPanel / Resend clients for an existing project
1015
+ ${chalk.bold("Getting started:")}
1016
+ setup One-time onboarding wires up all credentials (alias: init)
1017
+ status Show what's configured and what's next
1018
+ doctor Health-check every provider with contextual fix hints
1019
+ explain One-page mental model of the CLI
1020
+
1021
+ ${chalk.bold("Projects:")}
1022
+ create Scaffold a new project (interactive)
575
1023
  update Add features to an already-scaffolded project (run in project dir)
1024
+ add Create GlitchTip / OpenPanel / Resend clients for an existing project
1025
+ remove Delete the -dev/-prod clients created by 'add' (inverse of add)
1026
+ rename-domain Move a scaffolded project to a new domain (rewrites tfvars/env/manifest)
1027
+ gh-pages Wire GitHub Pages for the current repo (static / Vite / Jekyll — with DNS)
1028
+ dns DNS reconciliation helpers (link-to-cloudflare, …)
576
1029
  keys show <p> Print the dotenvx private key for a project
577
1030
  keys push <p> Push the key onto the project's Coolify app
578
- setup Run first-time setup / onboarding (alias: init)
579
- config Show provider status
580
- config add <p> Configure a provider (coolify, hetzner, dns, s3, modal, etc.)
1031
+
1032
+ ${chalk.bold("Config:")}
1033
+ config Show provider status (same as \`status\`)
1034
+ config add <p> Configure a provider (coolify, hetzner, dns, s3, modal, …)
581
1035
  config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
582
1036
 
1037
+ ${chalk.bold("For agents / scripts:")}
1038
+ status --json StatusSnapshot as JSON
1039
+ doctor --json Per-provider health with fix hints as JSON
1040
+ completion <shell> Print a zsh/bash/fish completion script
1041
+
583
1042
  ${chalk.bold("Options:")}
584
1043
  --version, -v Print the CLI version
585
1044
  --help, -h Show this help message (pass to a subcommand for detail)
1045
+ --json Machine-readable output (status, doctor, explain)
586
1046
  --dry-run (with \`create\`) show what would change without writing
587
1047
  --yes, -y (with \`create\`) skip prompts, use defaults / --config values
588
1048
  --config <path> (with \`create\`) load JSON overrides for ProjectConfig fields
@@ -592,8 +1052,8 @@ function printHelp(topic) {
592
1052
 
593
1053
  ${chalk.bold("Environment:")}
594
1054
  HATCHKIT_CONF_DIR Override the config/ports-registry location
595
- (advanced — useful for isolated per-workspace state
596
- or automated testing).
1055
+
1056
+ ${chalk.dim("Run `hatchkit help <command>` for per-command detail.")}
597
1057
  `);
598
1058
  }
599
1059
  // ---------------------------------------------------------------------------