hatchkit 0.1.39 → 0.1.41

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 (77) hide show
  1. package/dist/adopt.d.ts.map +1 -1
  2. package/dist/adopt.js +311 -77
  3. package/dist/adopt.js.map +1 -1
  4. package/dist/deploy/coolify-app.d.ts +9 -9
  5. package/dist/deploy/coolify-app.d.ts.map +1 -1
  6. package/dist/deploy/coolify-app.js +14 -19
  7. package/dist/deploy/coolify-app.js.map +1 -1
  8. package/dist/deploy/coolify.d.ts.map +1 -1
  9. package/dist/deploy/coolify.js +6 -2
  10. package/dist/deploy/coolify.js.map +1 -1
  11. package/dist/deploy/keys.d.ts +7 -2
  12. package/dist/deploy/keys.d.ts.map +1 -1
  13. package/dist/deploy/keys.js +27 -7
  14. package/dist/deploy/keys.js.map +1 -1
  15. package/dist/deploy/pages.d.ts +41 -0
  16. package/dist/deploy/pages.d.ts.map +1 -1
  17. package/dist/deploy/pages.js +360 -13
  18. package/dist/deploy/pages.js.map +1 -1
  19. package/dist/deploy/regen-infra.js +4 -0
  20. package/dist/deploy/regen-infra.js.map +1 -1
  21. package/dist/deploy/rollback.d.ts.map +1 -1
  22. package/dist/deploy/rollback.js +94 -22
  23. package/dist/deploy/rollback.js.map +1 -1
  24. package/dist/deploy/sync.d.ts +10 -7
  25. package/dist/deploy/sync.d.ts.map +1 -1
  26. package/dist/deploy/sync.js +13 -9
  27. package/dist/deploy/sync.js.map +1 -1
  28. package/dist/index.js +269 -23
  29. package/dist/index.js.map +1 -1
  30. package/dist/inventory.d.ts +144 -0
  31. package/dist/inventory.d.ts.map +1 -0
  32. package/dist/inventory.js +1980 -0
  33. package/dist/inventory.js.map +1 -0
  34. package/dist/overview.d.ts +101 -0
  35. package/dist/overview.d.ts.map +1 -0
  36. package/dist/overview.js +852 -0
  37. package/dist/overview.js.map +1 -0
  38. package/dist/prompts.d.ts +22 -7
  39. package/dist/prompts.d.ts.map +1 -1
  40. package/dist/prompts.js +240 -40
  41. package/dist/prompts.js.map +1 -1
  42. package/dist/scaffold/app.js +1 -1
  43. package/dist/scaffold/app.js.map +1 -1
  44. package/dist/scaffold/infra.d.ts.map +1 -1
  45. package/dist/scaffold/infra.js +8 -2
  46. package/dist/scaffold/infra.js.map +1 -1
  47. package/dist/scaffold/manifest.d.ts +6 -0
  48. package/dist/scaffold/manifest.d.ts.map +1 -1
  49. package/dist/scaffold/manifest.js +1 -0
  50. package/dist/scaffold/manifest.js.map +1 -1
  51. package/dist/scaffold/pages-heuristics.d.ts +17 -0
  52. package/dist/scaffold/pages-heuristics.d.ts.map +1 -0
  53. package/dist/scaffold/pages-heuristics.js +344 -0
  54. package/dist/scaffold/pages-heuristics.js.map +1 -0
  55. package/dist/scaffold/pages-mode.d.ts +10 -0
  56. package/dist/scaffold/pages-mode.d.ts.map +1 -0
  57. package/dist/scaffold/pages-mode.js +109 -0
  58. package/dist/scaffold/pages-mode.js.map +1 -0
  59. package/dist/scaffold/surfaces.d.ts.map +1 -1
  60. package/dist/scaffold/surfaces.js +12 -1
  61. package/dist/scaffold/surfaces.js.map +1 -1
  62. package/dist/utils/cloudflare-api.d.ts +19 -0
  63. package/dist/utils/cloudflare-api.d.ts.map +1 -1
  64. package/dist/utils/cloudflare-api.js +16 -0
  65. package/dist/utils/cloudflare-api.js.map +1 -1
  66. package/dist/utils/coolify-api.d.ts +20 -0
  67. package/dist/utils/coolify-api.d.ts.map +1 -1
  68. package/dist/utils/coolify-api.js +51 -0
  69. package/dist/utils/coolify-api.js.map +1 -1
  70. package/dist/utils/coolify-server-ips.d.ts +6 -12
  71. package/dist/utils/coolify-server-ips.d.ts.map +1 -1
  72. package/dist/utils/coolify-server-ips.js +26 -81
  73. package/dist/utils/coolify-server-ips.js.map +1 -1
  74. package/dist/utils/run-ledger.d.ts +20 -0
  75. package/dist/utils/run-ledger.d.ts.map +1 -1
  76. package/dist/utils/run-ledger.js.map +1 -1
  77. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -166,6 +166,36 @@ async function main() {
166
166
  await runDoctor({ json: isJson });
167
167
  break;
168
168
  }
169
+ case "overview": {
170
+ if (args.includes("--help"))
171
+ return printHelp("overview");
172
+ const { runOverview } = await import("./overview.js");
173
+ await runOverview({ json: isJson, all: args.includes("--all") });
174
+ break;
175
+ }
176
+ case "inventory": {
177
+ if (args.includes("--help"))
178
+ return printHelp("inventory");
179
+ const { runInventory } = await import("./inventory.js");
180
+ const nameFlag = flagValue("--name");
181
+ const domainFlag = flagValue("--domain");
182
+ const repoFlag = flagValue("--repo");
183
+ const inputOverride = {};
184
+ if (nameFlag)
185
+ inputOverride.name = nameFlag;
186
+ if (domainFlag)
187
+ inputOverride.domain = domainFlag;
188
+ if (repoFlag)
189
+ inputOverride.repo = repoFlag;
190
+ await runInventory(resolve("."), {
191
+ json: isJson,
192
+ yes: args.includes("--yes") || args.includes("-y"),
193
+ save: args.includes("--save"),
194
+ noSave: args.includes("--no-save"),
195
+ input: Object.keys(inputOverride).length > 0 ? inputOverride : undefined,
196
+ });
197
+ break;
198
+ }
169
199
  case "provision": {
170
200
  const sub = args[1];
171
201
  if (sub === "s3") {
@@ -212,6 +242,14 @@ async function main() {
212
242
  if (command === "pages") {
213
243
  console.log(chalk.yellow(" Note: `hatchkit pages` has been renamed to `hatchkit gh-pages`."));
214
244
  }
245
+ if (args.includes("--undo")) {
246
+ const { runPagesUndo } = await import("./deploy/pages.js");
247
+ await runPagesUndo(resolve("."), {
248
+ dryRun: args.includes("--dry-run"),
249
+ yes: args.includes("--yes") || args.includes("-y"),
250
+ });
251
+ break;
252
+ }
215
253
  const { runPagesSetup } = await import("./deploy/pages.js");
216
254
  await runPagesSetup(resolve("."));
217
255
  break;
@@ -828,17 +866,22 @@ async function handleCreate() {
828
866
  if (forceNoInstall)
829
867
  config.installDeps = false;
830
868
  // Ensure needed providers are configured (lazy prompting).
831
- // Coolify + Hetzner are only needed when actually deploying
832
- // scaffold-only + --no-deploy runs skip their setup prompts.
833
- if (config.deployTarget === "existing" || config.runDeployment) {
869
+ // Coolify + Hetzner only matter for the coolify deployment mode.
870
+ // gh-pages skips them entirely (no server, no Docker registry).
871
+ if (config.deploymentMode === "coolify" &&
872
+ (config.deployTarget === "existing" || config.runDeployment)) {
834
873
  await ensureCoolify();
835
874
  }
836
875
  // GitHub is checked here so auth failures surface before scaffold
837
- // (not deep inside `setupGitHub` after files are on disk).
838
- if (config.createGithubRepo) {
876
+ // (not deep inside `setupGitHub` after files are on disk). Pages
877
+ // also needs GitHub auth for the API calls that enable Pages and
878
+ // set the cname — so we require it whenever gh-pages is involved.
879
+ if (config.createGithubRepo || config.deploymentMode === "gh-pages") {
839
880
  await ensureGitHub();
840
881
  }
841
- if (config.deployTarget === "new" && config.runDeployment) {
882
+ if (config.deploymentMode === "coolify" &&
883
+ config.deployTarget === "new" &&
884
+ config.runDeployment) {
842
885
  await ensureHetzner();
843
886
  }
844
887
  if (config.features.includes("s3") &&
@@ -872,9 +915,14 @@ async function handleCreate() {
872
915
  console.log(chalk.bold("\n ── Summary ───────────────────────────────────────────────\n"));
873
916
  console.log(` Project: ${chalk.cyan(config.name)}`);
874
917
  console.log(` Domain: ${chalk.cyan(config.domain)}`);
875
- console.log(` Deploy to: ${config.deployTarget === "existing" ? `existing server (${config.serverIpv4 ?? config.serverIp ?? "?"}${config.serverIpv6 ? ` · ${config.serverIpv6}` : ""})` : `new Hetzner ${config.serverSize}`}`);
876
- if (config.serverIpMismatchWarning) {
877
- console.log(chalk.yellow(` ⚠ ${config.serverIpMismatchWarning}`));
918
+ if (config.deploymentMode === "gh-pages") {
919
+ console.log(` Deploy to: ${chalk.cyan("GitHub Pages (static)")}`);
920
+ }
921
+ else if (config.deploymentMode === "scaffold-only") {
922
+ console.log(` Deploy to: ${chalk.dim("scaffold only (no deploy)")}`);
923
+ }
924
+ else {
925
+ console.log(` Deploy to: ${config.deployTarget === "existing" ? `existing server (${config.serverIpv4 ?? config.serverIp ?? "?"}${config.serverIpv6 ? ` · ${config.serverIpv6}` : ""})` : `new Hetzner ${config.serverSize}`}`);
878
926
  }
879
927
  console.log(` Features: ${config.features.length > 0 ? config.features.join(", ") : "none"}`);
880
928
  console.log(` ML: ${config.mlServices.length > 0 ? config.mlServices.join(", ") : "none"}`);
@@ -1010,10 +1058,19 @@ async function handleCreate() {
1010
1058
  }
1011
1059
  }
1012
1060
  if (config.dryRun) {
1013
- scaffoldInfra(config, INFRA_ROOT, {
1014
- serverPort: scaffoldResult?.ports.server,
1015
- clientPort: scaffoldResult?.ports.client,
1016
- });
1061
+ // Coolify mode previews the Terraform tfvars + Coolify env that
1062
+ // would be written. gh-pages and scaffold-only have nothing
1063
+ // equivalent — Pages reads no env, scaffold-only writes no infra.
1064
+ if (config.deploymentMode === "coolify") {
1065
+ scaffoldInfra(config, INFRA_ROOT, {
1066
+ serverPort: scaffoldResult?.ports.server,
1067
+ clientPort: scaffoldResult?.ports.client,
1068
+ });
1069
+ }
1070
+ else if (config.deploymentMode === "gh-pages") {
1071
+ console.log(chalk.dim(" · gh-pages mode — would write `.github/workflows/gh-pages.yml`, patch `next.config`,\n" +
1072
+ " write CNAME, enable Pages, configure DNS, and wait for the Let's Encrypt cert."));
1073
+ }
1017
1074
  console.log(chalk.green("\n ✓ Dry run complete. No changes were made.\n"));
1018
1075
  return;
1019
1076
  }
@@ -1063,8 +1120,10 @@ async function handleCreate() {
1063
1120
  if (infraResult.coolifyEnvPath) {
1064
1121
  ledger?.record({ kind: "coolifyEnv", path: infraResult.coolifyEnvPath });
1065
1122
  }
1066
- // Step 5: Terraform (DNS + optionally server)
1067
- if (config.runDeployment) {
1123
+ // Step 5: Terraform (DNS + optionally server). Coolify-only —
1124
+ // gh-pages handles its own DNS via `runPagesSetupProgrammatic`
1125
+ // a few steps down, and `scaffold-only` skips deploy entirely.
1126
+ if (config.runDeployment && config.deploymentMode === "coolify") {
1068
1127
  const tfResult = await runTerraform(config, INFRA_ROOT);
1069
1128
  if (tfResult.applied) {
1070
1129
  ledger?.record({
@@ -1074,8 +1133,9 @@ async function handleCreate() {
1074
1133
  });
1075
1134
  }
1076
1135
  }
1077
- // Step 6: Coolify setup
1078
- if (config.runDeployment) {
1136
+ // Step 6: Coolify setup. Only runs in coolify mode; gh-pages has
1137
+ // no Coolify app to provision (the site lives on GitHub's CDN).
1138
+ if (config.runDeployment && config.deploymentMode === "coolify") {
1079
1139
  const coolifyResult = await runCoolifySetup(config, {
1080
1140
  repoUrl: repoUrl ?? undefined,
1081
1141
  serverPort: scaffoldResult?.ports.server,
@@ -1113,7 +1173,11 @@ async function handleCreate() {
1113
1173
  // print the manual command instead of failing the whole flow.
1114
1174
  if (scaffoldResult?.dotenvx) {
1115
1175
  try {
1116
- await pushProjectKeyToCoolify(config.name);
1176
+ // App name matches the project name (the dockercompose
1177
+ // wrapper). The candidate-list fallback in
1178
+ // `pushProjectKeyToCoolify` still catches legacy `-web`
1179
+ // projects.
1180
+ await pushProjectKeyToCoolify(config.name, { appName: config.name });
1117
1181
  }
1118
1182
  catch (err) {
1119
1183
  console.log(chalk.yellow(` Couldn't auto-push dotenvx key: ${err.message}`));
@@ -1148,6 +1212,63 @@ async function handleCreate() {
1148
1212
  }
1149
1213
  }
1150
1214
  }
1215
+ // Step 6.25 (gh-pages only): run Pages setup. Writes the
1216
+ // .github/workflows/gh-pages.yml + CNAME file locally and wires
1217
+ // the remote side (enable Pages, register cname, configure DNS,
1218
+ // poll for the Let's Encrypt cert, flip https_enforced). Must
1219
+ // happen BEFORE push so the new files land in the first push and
1220
+ // the workflow runs immediately.
1221
+ if (config.deploymentMode === "gh-pages" &&
1222
+ config.scaffoldRepo &&
1223
+ config.runDeployment &&
1224
+ repoUrl) {
1225
+ const { runPagesSetupProgrammatic } = await import("./deploy/pages.js");
1226
+ const { exec: bashExec } = await import("./utils/exec.js");
1227
+ // The scaffold's `pruneToClientOnly` rewrites the root build
1228
+ // script to `pnpm --filter @starter/shared run build && pnpm
1229
+ // --filter @starter/client run build` — runs from the repo
1230
+ // root, outputs to `packages/client/out/` (after the Pages-
1231
+ // mode Next config patch sets `output: "export"`).
1232
+ const detected = {
1233
+ kind: "node-build",
1234
+ publishDir: "packages/client/out",
1235
+ packageManager: "pnpm",
1236
+ buildScript: "build",
1237
+ workDir: "",
1238
+ };
1239
+ const slug = repoUrl.replace(/^https?:\/\/github\.com\//, "");
1240
+ try {
1241
+ const { pageUrl } = await runPagesSetupProgrammatic(appDir, {
1242
+ detected,
1243
+ domain: config.domain,
1244
+ });
1245
+ ledger?.record({
1246
+ kind: "ghPages",
1247
+ repo: slug,
1248
+ projectDir: appDir,
1249
+ cname: config.domain,
1250
+ });
1251
+ // Commit the workflow + CNAME file before the push step
1252
+ // below picks up the staged changes. Empty diffs (e.g. re-
1253
+ // running on an idempotent state) just produce a no-op commit.
1254
+ await bashExec("git", ["add", "-A"], { cwd: appDir, silent: true });
1255
+ const status = await bashExec("git", ["status", "--porcelain"], {
1256
+ cwd: appDir,
1257
+ silent: true,
1258
+ });
1259
+ if (status.stdout.trim()) {
1260
+ await bashExec("git", ["commit", "-m", "ci: GitHub Pages setup"], {
1261
+ cwd: appDir,
1262
+ silent: true,
1263
+ });
1264
+ }
1265
+ console.log(chalk.green(` ✓ GitHub Pages will publish at ${pageUrl}`));
1266
+ }
1267
+ catch (err) {
1268
+ console.log(chalk.yellow(` Couldn't auto-wire GitHub Pages: ${err.message}`));
1269
+ console.log(chalk.dim(` Run \`hatchkit gh-pages\` from ${appDir} once the issue is resolved.`));
1270
+ }
1271
+ }
1151
1272
  // Step 6.5: push the working branch to origin. Done AFTER Coolify
1152
1273
  // wiring + Actions-secret upserts so the workflow's first run
1153
1274
  // already has the secrets it needs to deploy. setupGitHub above
@@ -1220,6 +1341,9 @@ async function handleCreate() {
1220
1341
  if (config.surfaces !== "client-only") {
1221
1342
  console.log(` API: ${chalk.cyan(`https://${config.domain}/api`)}`);
1222
1343
  }
1344
+ if (config.deploymentMode === "gh-pages") {
1345
+ console.log(chalk.dim(` Hosting: GitHub Pages — first build kicks off on push, https cert provisions over the next few minutes.`));
1346
+ }
1223
1347
  console.log(` App dir: ${chalk.dim(appDir)}`);
1224
1348
  console.log(` Config: ${chalk.dim(getConfigPath())}`);
1225
1349
  if (config.scaffoldRepo) {
@@ -1369,13 +1493,20 @@ function printHelp(topic) {
1369
1493
  hatchkit create [--dry-run]
1370
1494
 
1371
1495
  ${chalk.bold("What it does (interactively):")}
1372
- 1. Prompts for project name, domain, deploy target, features, ML services
1496
+ 1. Prompts for project name, domain, surfaces, deployment mode, features, ML
1373
1497
  2. Copies the starter template and strips unselected features
1374
1498
  3. Assigns unique ports per project (server, client, native HMR)
1375
1499
  4. Runs \`pnpm install\` (if pnpm is present and you opt in)
1376
1500
  5. Initializes git, optionally creates a GitHub repo
1377
- 6. Generates Terraform tfvars + Coolify .env
1378
- 7. Optionally deploys: Terraform → Coolify → ML services
1501
+ 6. Generates Terraform tfvars + Coolify .env (Coolify mode)
1502
+ 7. Deploys: Terraform → Coolify → ML ${chalk.dim("OR")} GitHub Pages setup
1503
+
1504
+ ${chalk.bold("Deployment modes:")}
1505
+ ${chalk.cyan("coolify")} Full-stack on Hetzner — DB, providers, Docker. Default.
1506
+ ${chalk.cyan("gh-pages")} Static-only on GitHub Pages. Only offered when surfaces
1507
+ is ${chalk.dim("client-only")}; the scaffold's Next config is patched to
1508
+ ${chalk.dim('`output: "export"`')} and the gh-pages workflow is written.
1509
+ ${chalk.cyan("scaffold-only")} Write files, skip deploy. Pick this to defer setup.
1379
1510
 
1380
1511
  ${chalk.bold("Options:")}
1381
1512
  --dry-run Show the plan without writing anything
@@ -1463,6 +1594,7 @@ function printHelp(topic) {
1463
1594
 
1464
1595
  ${chalk.bold("Usage:")}
1465
1596
  cd <project-dir> && hatchkit gh-pages
1597
+ cd <project-dir> && hatchkit gh-pages --undo [--dry-run] [--yes]
1466
1598
 
1467
1599
  ${chalk.bold("What it does:")}
1468
1600
  1. Reads the repo via \`gh repo view\` (must be a GitHub repo you own).
@@ -1484,6 +1616,17 @@ function printHelp(topic) {
1484
1616
  ${chalk.bold("After running:")}
1485
1617
  git add -A && git commit -m "ci: deploy to GitHub Pages" && git push
1486
1618
 
1619
+ ${chalk.bold("Undo (--undo):")}
1620
+ Reverses what the command put in place:
1621
+ - Disables Pages via ${chalk.dim("DELETE /repos/<owner>/<repo>/pages")} (clears the cname too).
1622
+ - Deletes Cloudflare records that point at GitHub's Pages IPs / ${chalk.dim("<user>.github.io")}
1623
+ for the registered domain (only when a Cloudflare token is configured + the
1624
+ zone is in this account).
1625
+ - Removes ${chalk.cyan(".github/workflows/gh-pages.yml")} (only the file hatchkit writes
1626
+ — hand-written Pages workflows are left untouched).
1627
+ - Removes any ${chalk.cyan("CNAME")} files whose content matches the registered domain.
1628
+ ${chalk.dim("--dry-run")} prints the plan without changing anything. ${chalk.dim("--yes")} skips the confirm.
1629
+
1487
1630
  ${chalk.bold("Notes:")}
1488
1631
  - Private repos need a paid GitHub plan for Pages. Free-tier repos
1489
1632
  must be made public first.
@@ -1523,6 +1666,105 @@ function printHelp(topic) {
1523
1666
  stored (Coolify /version, Hetzner /servers, Cloudflare /tokens/verify,
1524
1667
  Resend /domains, …). Reports ok / fail / not-configured per provider
1525
1668
  and exits non-zero if any check fails. Safe to run repeatedly.
1669
+ `);
1670
+ return;
1671
+ }
1672
+ if (topic === "overview") {
1673
+ console.log(`
1674
+ ${chalk.bold("hatchkit overview")} — fleet-level view of every configured provider
1675
+
1676
+ Distinct from ${chalk.cyan("status")} (which providers do I have credentials for?),
1677
+ ${chalk.cyan("doctor")} (are those credentials valid?), and ${chalk.cyan("inventory")} (what does THIS
1678
+ project have?). ${chalk.cyan("overview")} answers "what does my whole hatchkit
1679
+ footprint look like, across every configured provider?" — no name or
1680
+ domain filter, just a roll-up of top-level resources.
1681
+
1682
+ ${chalk.bold("What it lists:")}
1683
+ · Coolify applications, projects, databases
1684
+ · Cloudflare DNS zones
1685
+ · R2 buckets (whole account)
1686
+ · Hetzner S3 / AWS S3 credential presence (bucket listing not implemented)
1687
+ · Resend verified domains
1688
+ · GlitchTip projects in the configured org
1689
+ · OpenPanel projects
1690
+ · Stripe webhook endpoints (test + live)
1691
+
1692
+ ${chalk.bold("Cross-references:")}
1693
+ After listing every provider, ${chalk.cyan("overview")} cross-references the
1694
+ raw data to flag fleet-level inconsistencies — the kind of bitrot
1695
+ that a single-provider lens can't see:
1696
+
1697
+ · Coolify app deploys from a repo \`gh\` can't find (deleted/renamed)
1698
+ · App fqdn references an apex with no Cloudflare zone
1699
+ · R2 bucket follows the \`<project>-<role>\` convention but has no
1700
+ matching Coolify app (orphan from a destroyed project)
1701
+ · GlitchTip / OpenPanel project with no Coolify app counterpart
1702
+ · Cloudflare zone with no Coolify app pointing into it
1703
+
1704
+ ${chalk.bold("Flags:")}
1705
+ --all Print every resource per provider (default: 6-line preview)
1706
+ --json Machine-readable OverviewReport (non-interactive)
1707
+
1708
+ Read-only — every call is a GET. Safe to run repeatedly.
1709
+ `);
1710
+ return;
1711
+ }
1712
+ if (topic === "inventory") {
1713
+ console.log(`
1714
+ ${chalk.bold("hatchkit inventory")} — survey what already exists for this project
1715
+
1716
+ Inverse of ${chalk.cyan("doctor")}: instead of "are my credentials valid?", asks
1717
+ "given THIS project (cwd / name / domain / repo), what resources
1718
+ already exist across every configured provider — and is anything
1719
+ out of sync?"
1720
+
1721
+ ${chalk.bold("Inference (cwd → identity):")}
1722
+ · .hatchkit.json — name, domain
1723
+ · package.json — name, description
1724
+ · git remote — GitHub owner/repo
1725
+ · CNAME file — gh-pages custom domain
1726
+ · .env.production dotenvx — encryption state
1727
+ · .github/workflows/ — gh-pages / Coolify deploy workflows
1728
+
1729
+ Asks interactively for anything that couldn't be inferred. Confirms
1730
+ inferred values unless ${chalk.cyan("--yes")} is passed.
1731
+
1732
+ ${chalk.bold("Scans (parallel, read-only):")}
1733
+ · Coolify — projects, applications (with fqdn + git source)
1734
+ · DNS — Cloudflare zone + relevant records (apex/www/api/s3/…)
1735
+ · R2 — buckets (manifest + naming-convention candidates) + CORS
1736
+ · GitHub — repo visibility, Pages status, relevant repo secrets
1737
+ · Resend — verified-domain match
1738
+ · GlitchTip — projects in the configured org
1739
+ · OpenPanel — projects
1740
+ · Stripe — webhook endpoints whose URL contains the project domain
1741
+
1742
+ ${chalk.bold("Drift detection (cross-references):")}
1743
+ · Coolify app fqdn vs DNS A record (and the linked server's public IP)
1744
+ · Coolify app git source vs local git remote (renamed repo gotcha)
1745
+ · Manifest s3Buckets entries vs live R2 buckets
1746
+ · Bucket CORS — manifest origins vs live policy
1747
+ · gh-pages workflow on disk vs Pages enabled on repo (and CNAME ↔ Pages cname)
1748
+ · dotenvx encrypted locally but no DOTENV_PRIVATE_KEY_PRODUCTION secret in GH
1749
+
1750
+ ${chalk.bold("Flags:")}
1751
+ --name <project> Override inferred project name
1752
+ --domain <domain> Override inferred domain
1753
+ --repo <owner/name> Override inferred GitHub repo
1754
+ --yes, -y Skip confirm-inferred-value prompts
1755
+ --save Write a minimal .hatchkit.json without prompting
1756
+ --no-save Suppress the end-of-run save prompt
1757
+ --json Machine-readable InventoryReport (non-interactive)
1758
+
1759
+ ${chalk.bold("Persisting identity:")}
1760
+ After an interactive run, when ${chalk.cyan(".hatchkit.json")} doesn't yet exist
1761
+ and both name + domain are inferred, hatchkit offers to write a
1762
+ minimal manifest. The manifest carries the right schema for every
1763
+ other command (adopt, update, sync, keys), with conservative defaults
1764
+ for fields inventory can't infer (features=[], s3Provider="none",
1765
+ deployTarget="existing", ports={server:3000,client:5173}). Run
1766
+ ${chalk.cyan("hatchkit adopt --resume")} afterwards to flesh out the rest via
1767
+ the adopt stepper.
1526
1768
  `);
1527
1769
  return;
1528
1770
  }
@@ -1958,6 +2200,8 @@ function printHelp(topic) {
1958
2200
  setup One-time onboarding — wires up all credentials (alias: init)
1959
2201
  status Show what's configured and what's next
1960
2202
  doctor Health-check every provider with contextual fix hints
2203
+ inventory Survey what already exists for this project (and flag drift)
2204
+ overview Fleet-level survey — every resource across all configured providers
1961
2205
  explain One-page mental model of the CLI
1962
2206
 
1963
2207
  ${chalk.bold("Projects:")}
@@ -1983,8 +2227,10 @@ function printHelp(topic) {
1983
2227
  config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
1984
2228
 
1985
2229
  ${chalk.bold("For agents / scripts:")}
1986
- status --json StatusSnapshot as JSON
1987
- doctor --json Per-provider health with fix hints as JSON
2230
+ status --json StatusSnapshot as JSON
2231
+ doctor --json Per-provider health with fix hints as JSON
2232
+ inventory --json InventoryReport — resources found per provider + drift
2233
+ overview --json OverviewReport — fleet-level resource counts + names
1988
2234
  completion <shell> Print a zsh/bash/fish completion script
1989
2235
 
1990
2236
  ${chalk.bold("Options:")}