hatchkit 0.1.40 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adopt.d.ts.map +1 -1
- package/dist/adopt.js +663 -82
- package/dist/adopt.js.map +1 -1
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -38
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify-app.d.ts.map +1 -1
- package/dist/deploy/coolify-app.js +0 -7
- package/dist/deploy/coolify-app.js.map +1 -1
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +20 -1
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/ghcr.d.ts +4 -2
- package/dist/deploy/ghcr.d.ts.map +1 -1
- package/dist/deploy/ghcr.js +1 -1
- package/dist/deploy/ghcr.js.map +1 -1
- package/dist/deploy/github.d.ts +4 -3
- package/dist/deploy/github.d.ts.map +1 -1
- package/dist/deploy/github.js +5 -2
- package/dist/deploy/github.js.map +1 -1
- package/dist/deploy/pages.d.ts +41 -0
- package/dist/deploy/pages.d.ts.map +1 -1
- package/dist/deploy/pages.js +363 -22
- package/dist/deploy/pages.js.map +1 -1
- package/dist/deploy/regen-infra.d.ts.map +1 -1
- package/dist/deploy/regen-infra.js +5 -11
- package/dist/deploy/regen-infra.js.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +44 -6
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/deploy/terraform.d.ts.map +1 -1
- package/dist/deploy/terraform.js +20 -37
- package/dist/deploy/terraform.js.map +1 -1
- package/dist/dns.d.ts.map +1 -1
- package/dist/dns.js +4 -5
- package/dist/dns.js.map +1 -1
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +110 -36
- package/dist/doctor.js.map +1 -1
- package/dist/email/index.d.ts +31 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/presets.d.ts +14 -0
- package/dist/email/presets.d.ts.map +1 -0
- package/dist/email/presets.js +33 -0
- package/dist/email/presets.js.map +1 -0
- package/dist/email/setup.d.ts +93 -0
- package/dist/email/setup.d.ts.map +1 -0
- package/dist/email/setup.js +263 -0
- package/dist/email/setup.js.map +1 -0
- package/dist/email/spf.d.ts +56 -0
- package/dist/email/spf.d.ts.map +1 -0
- package/dist/email/spf.js +102 -0
- package/dist/email/spf.js.map +1 -0
- package/dist/index.js +306 -22
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +37 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +536 -55
- package/dist/inventory.js.map +1 -1
- package/dist/overview.d.ts +101 -0
- package/dist/overview.d.ts.map +1 -0
- package/dist/overview.js +880 -0
- package/dist/overview.js.map +1 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +262 -34
- package/dist/prompts.js.map +1 -1
- package/dist/provision/index.d.ts +20 -1
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +115 -0
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/s3-buckets.js +1 -1
- package/dist/provision/s3-buckets.js.map +1 -1
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +15 -7
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/build-pipeline.d.ts +16 -0
- package/dist/scaffold/build-pipeline.d.ts.map +1 -1
- package/dist/scaffold/build-pipeline.js +47 -4
- package/dist/scaffold/build-pipeline.js.map +1 -1
- package/dist/scaffold/infra.d.ts +4 -5
- package/dist/scaffold/infra.d.ts.map +1 -1
- package/dist/scaffold/infra.js +18 -57
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts +6 -0
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +2 -0
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/pages-heuristics.d.ts +17 -0
- package/dist/scaffold/pages-heuristics.d.ts.map +1 -0
- package/dist/scaffold/pages-heuristics.js +344 -0
- package/dist/scaffold/pages-heuristics.js.map +1 -0
- package/dist/scaffold/pages-mode.d.ts +10 -0
- package/dist/scaffold/pages-mode.d.ts.map +1 -0
- package/dist/scaffold/pages-mode.js +107 -0
- package/dist/scaffold/pages-mode.js.map +1 -0
- package/dist/scaffold/pkg-json.d.ts +4 -0
- package/dist/scaffold/pkg-json.d.ts.map +1 -1
- package/dist/scaffold/pkg-json.js +17 -0
- package/dist/scaffold/pkg-json.js.map +1 -1
- package/dist/scaffold/surfaces.d.ts.map +1 -1
- package/dist/scaffold/surfaces.js +12 -1
- package/dist/scaffold/surfaces.js.map +1 -1
- package/dist/scaffold/update.js +1 -1
- package/dist/scaffold/update.js.map +1 -1
- package/dist/templates/build-pipeline/Dockerfile.nextjs.hbs +103 -0
- package/dist/templates/build-pipeline/docker-compose.yml.hbs +23 -6
- package/dist/utils/cloudflare-api.d.ts +158 -13
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +219 -11
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/coolify-api.d.ts +9 -0
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +26 -0
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +42 -1
- package/dist/utils/run-ledger.d.ts.map +1 -1
- package/dist/utils/run-ledger.js.map +1 -1
- package/dist/utils/s3-admin.d.ts +9 -0
- package/dist/utils/s3-admin.d.ts.map +1 -0
- package/dist/utils/s3-admin.js +46 -0
- package/dist/utils/s3-admin.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -166,6 +166,13 @@ 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
|
+
}
|
|
169
176
|
case "inventory": {
|
|
170
177
|
if (args.includes("--help"))
|
|
171
178
|
return printHelp("inventory");
|
|
@@ -183,6 +190,8 @@ async function main() {
|
|
|
183
190
|
await runInventory(resolve("."), {
|
|
184
191
|
json: isJson,
|
|
185
192
|
yes: args.includes("--yes") || args.includes("-y"),
|
|
193
|
+
save: args.includes("--save"),
|
|
194
|
+
noSave: args.includes("--no-save"),
|
|
186
195
|
input: Object.keys(inputOverride).length > 0 ? inputOverride : undefined,
|
|
187
196
|
});
|
|
188
197
|
break;
|
|
@@ -226,6 +235,13 @@ async function main() {
|
|
|
226
235
|
await handleDns();
|
|
227
236
|
break;
|
|
228
237
|
}
|
|
238
|
+
case "email": {
|
|
239
|
+
if (args.includes("--help") && args.length === 2)
|
|
240
|
+
return printHelp("email");
|
|
241
|
+
const { handleEmailCommand } = await import("./email/index.js");
|
|
242
|
+
await handleEmailCommand(args.slice(1));
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
229
245
|
case "gh-pages":
|
|
230
246
|
case "pages": {
|
|
231
247
|
if (args.includes("--help"))
|
|
@@ -233,6 +249,14 @@ async function main() {
|
|
|
233
249
|
if (command === "pages") {
|
|
234
250
|
console.log(chalk.yellow(" Note: `hatchkit pages` has been renamed to `hatchkit gh-pages`."));
|
|
235
251
|
}
|
|
252
|
+
if (args.includes("--undo")) {
|
|
253
|
+
const { runPagesUndo } = await import("./deploy/pages.js");
|
|
254
|
+
await runPagesUndo(resolve("."), {
|
|
255
|
+
dryRun: args.includes("--dry-run"),
|
|
256
|
+
yes: args.includes("--yes") || args.includes("-y"),
|
|
257
|
+
});
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
236
260
|
const { runPagesSetup } = await import("./deploy/pages.js");
|
|
237
261
|
await runPagesSetup(resolve("."));
|
|
238
262
|
break;
|
|
@@ -425,7 +449,7 @@ async function handleAdd() {
|
|
|
425
449
|
const positional = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
426
450
|
let baseName = positional[0];
|
|
427
451
|
const rawService = positional[1];
|
|
428
|
-
const allServices = ["glitchtip", "openpanel", "resend", "s3"];
|
|
452
|
+
const allServices = ["glitchtip", "openpanel", "resend", "s3", "email"];
|
|
429
453
|
if (!baseName) {
|
|
430
454
|
const { input } = await import("@inquirer/prompts");
|
|
431
455
|
const { validateProjectName } = await import("./utils/validate.js");
|
|
@@ -448,6 +472,11 @@ async function handleAdd() {
|
|
|
448
472
|
value: "s3",
|
|
449
473
|
checked: false,
|
|
450
474
|
},
|
|
475
|
+
{
|
|
476
|
+
name: "Email forwarding (Cloudflare Email Routing — MX/SPF/DMARC + rules)",
|
|
477
|
+
value: "email",
|
|
478
|
+
checked: false,
|
|
479
|
+
},
|
|
451
480
|
],
|
|
452
481
|
required: true,
|
|
453
482
|
});
|
|
@@ -747,7 +776,7 @@ async function handleRemove() {
|
|
|
747
776
|
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
748
777
|
let baseName = positional[0];
|
|
749
778
|
const rawService = positional[1];
|
|
750
|
-
const allServices = ["glitchtip", "openpanel", "resend", "s3"];
|
|
779
|
+
const allServices = ["glitchtip", "openpanel", "resend", "s3", "email"];
|
|
751
780
|
if (!baseName) {
|
|
752
781
|
const { input } = await import("@inquirer/prompts");
|
|
753
782
|
const { validateProjectName } = await import("./utils/validate.js");
|
|
@@ -766,6 +795,11 @@ async function handleRemove() {
|
|
|
766
795
|
{ name: "OpenPanel (deletes the project)", value: "openpanel", checked: true },
|
|
767
796
|
{ name: "Resend (deletes the API key)", value: "resend", checked: true },
|
|
768
797
|
{ name: "S3 / R2 (deletes per-bucket scoped tokens)", value: "s3", checked: false },
|
|
798
|
+
{
|
|
799
|
+
name: "Email forwarding (deletes routing rules + DNS records; keeps destination)",
|
|
800
|
+
value: "email",
|
|
801
|
+
checked: false,
|
|
802
|
+
},
|
|
769
803
|
],
|
|
770
804
|
required: true,
|
|
771
805
|
});
|
|
@@ -849,17 +883,22 @@ async function handleCreate() {
|
|
|
849
883
|
if (forceNoInstall)
|
|
850
884
|
config.installDeps = false;
|
|
851
885
|
// Ensure needed providers are configured (lazy prompting).
|
|
852
|
-
// Coolify + Hetzner
|
|
853
|
-
//
|
|
854
|
-
if (config.
|
|
886
|
+
// Coolify + Hetzner only matter for the coolify deployment mode.
|
|
887
|
+
// gh-pages skips them entirely (no server, no Docker registry).
|
|
888
|
+
if (config.deploymentMode === "coolify" &&
|
|
889
|
+
(config.deployTarget === "existing" || config.runDeployment)) {
|
|
855
890
|
await ensureCoolify();
|
|
856
891
|
}
|
|
857
892
|
// GitHub is checked here so auth failures surface before scaffold
|
|
858
|
-
// (not deep inside `setupGitHub` after files are on disk).
|
|
859
|
-
|
|
893
|
+
// (not deep inside `setupGitHub` after files are on disk). Pages
|
|
894
|
+
// also needs GitHub auth for the API calls that enable Pages and
|
|
895
|
+
// set the cname — so we require it whenever gh-pages is involved.
|
|
896
|
+
if (config.createGithubRepo || config.deploymentMode === "gh-pages") {
|
|
860
897
|
await ensureGitHub();
|
|
861
898
|
}
|
|
862
|
-
if (config.
|
|
899
|
+
if (config.deploymentMode === "coolify" &&
|
|
900
|
+
config.deployTarget === "new" &&
|
|
901
|
+
config.runDeployment) {
|
|
863
902
|
await ensureHetzner();
|
|
864
903
|
}
|
|
865
904
|
if (config.features.includes("s3") &&
|
|
@@ -892,8 +931,19 @@ async function handleCreate() {
|
|
|
892
931
|
// Summary before execution
|
|
893
932
|
console.log(chalk.bold("\n ── Summary ───────────────────────────────────────────────\n"));
|
|
894
933
|
console.log(` Project: ${chalk.cyan(config.name)}`);
|
|
934
|
+
if (config.description) {
|
|
935
|
+
console.log(` Descr.: ${chalk.cyan(config.description)}`);
|
|
936
|
+
}
|
|
895
937
|
console.log(` Domain: ${chalk.cyan(config.domain)}`);
|
|
896
|
-
|
|
938
|
+
if (config.deploymentMode === "gh-pages") {
|
|
939
|
+
console.log(` Deploy to: ${chalk.cyan("GitHub Pages (static)")}`);
|
|
940
|
+
}
|
|
941
|
+
else if (config.deploymentMode === "scaffold-only") {
|
|
942
|
+
console.log(` Deploy to: ${chalk.dim("scaffold only (no deploy)")}`);
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
console.log(` Deploy to: ${config.deployTarget === "existing" ? `existing server (${config.serverIpv4 ?? config.serverIp ?? "?"}${config.serverIpv6 ? ` · ${config.serverIpv6}` : ""})` : `new Hetzner ${config.serverSize}`}`);
|
|
946
|
+
}
|
|
897
947
|
console.log(` Features: ${config.features.length > 0 ? config.features.join(", ") : "none"}`);
|
|
898
948
|
console.log(` ML: ${config.mlServices.length > 0 ? config.mlServices.join(", ") : "none"}`);
|
|
899
949
|
console.log(` Scaffold: ${config.scaffoldRepo ? "yes" : "no"}`);
|
|
@@ -1028,10 +1078,19 @@ async function handleCreate() {
|
|
|
1028
1078
|
}
|
|
1029
1079
|
}
|
|
1030
1080
|
if (config.dryRun) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1081
|
+
// Coolify mode previews the Terraform tfvars + Coolify env that
|
|
1082
|
+
// would be written. gh-pages and scaffold-only have nothing
|
|
1083
|
+
// equivalent — Pages reads no env, scaffold-only writes no infra.
|
|
1084
|
+
if (config.deploymentMode === "coolify") {
|
|
1085
|
+
scaffoldInfra(config, INFRA_ROOT, {
|
|
1086
|
+
serverPort: scaffoldResult?.ports.server,
|
|
1087
|
+
clientPort: scaffoldResult?.ports.client,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
else if (config.deploymentMode === "gh-pages") {
|
|
1091
|
+
console.log(chalk.dim(" · gh-pages mode — would write `.github/workflows/gh-pages.yml`, patch `next.config`,\n" +
|
|
1092
|
+
" write CNAME, enable Pages, configure DNS, and wait for the Let's Encrypt cert."));
|
|
1093
|
+
}
|
|
1035
1094
|
console.log(chalk.green("\n ✓ Dry run complete. No changes were made.\n"));
|
|
1036
1095
|
return;
|
|
1037
1096
|
}
|
|
@@ -1081,8 +1140,10 @@ async function handleCreate() {
|
|
|
1081
1140
|
if (infraResult.coolifyEnvPath) {
|
|
1082
1141
|
ledger?.record({ kind: "coolifyEnv", path: infraResult.coolifyEnvPath });
|
|
1083
1142
|
}
|
|
1084
|
-
// Step 5: Terraform (DNS + optionally server)
|
|
1085
|
-
|
|
1143
|
+
// Step 5: Terraform (DNS + optionally server). Coolify-only —
|
|
1144
|
+
// gh-pages handles its own DNS via `runPagesSetupProgrammatic`
|
|
1145
|
+
// a few steps down, and `scaffold-only` skips deploy entirely.
|
|
1146
|
+
if (config.runDeployment && config.deploymentMode === "coolify") {
|
|
1086
1147
|
const tfResult = await runTerraform(config, INFRA_ROOT);
|
|
1087
1148
|
if (tfResult.applied) {
|
|
1088
1149
|
ledger?.record({
|
|
@@ -1092,8 +1153,9 @@ async function handleCreate() {
|
|
|
1092
1153
|
});
|
|
1093
1154
|
}
|
|
1094
1155
|
}
|
|
1095
|
-
// Step 6: Coolify setup
|
|
1096
|
-
|
|
1156
|
+
// Step 6: Coolify setup. Only runs in coolify mode; gh-pages has
|
|
1157
|
+
// no Coolify app to provision (the site lives on GitHub's CDN).
|
|
1158
|
+
if (config.runDeployment && config.deploymentMode === "coolify") {
|
|
1097
1159
|
const coolifyResult = await runCoolifySetup(config, {
|
|
1098
1160
|
repoUrl: repoUrl ?? undefined,
|
|
1099
1161
|
serverPort: scaffoldResult?.ports.server,
|
|
@@ -1170,6 +1232,63 @@ async function handleCreate() {
|
|
|
1170
1232
|
}
|
|
1171
1233
|
}
|
|
1172
1234
|
}
|
|
1235
|
+
// Step 6.25 (gh-pages only): run Pages setup. Writes the
|
|
1236
|
+
// .github/workflows/gh-pages.yml + CNAME file locally and wires
|
|
1237
|
+
// the remote side (enable Pages, register cname, configure DNS,
|
|
1238
|
+
// poll for the Let's Encrypt cert, flip https_enforced). Must
|
|
1239
|
+
// happen BEFORE push so the new files land in the first push and
|
|
1240
|
+
// the workflow runs immediately.
|
|
1241
|
+
if (config.deploymentMode === "gh-pages" &&
|
|
1242
|
+
config.scaffoldRepo &&
|
|
1243
|
+
config.runDeployment &&
|
|
1244
|
+
repoUrl) {
|
|
1245
|
+
const { runPagesSetupProgrammatic } = await import("./deploy/pages.js");
|
|
1246
|
+
const { exec: bashExec } = await import("./utils/exec.js");
|
|
1247
|
+
// The scaffold's `pruneToClientOnly` rewrites the root build
|
|
1248
|
+
// script to `pnpm --filter @starter/shared run build && pnpm
|
|
1249
|
+
// --filter @starter/client run build` — runs from the repo
|
|
1250
|
+
// root, outputs to `packages/client/out/` (after the Pages-
|
|
1251
|
+
// mode Next config patch sets `output: "export"`).
|
|
1252
|
+
const detected = {
|
|
1253
|
+
kind: "node-build",
|
|
1254
|
+
publishDir: "packages/client/out",
|
|
1255
|
+
packageManager: "pnpm",
|
|
1256
|
+
buildScript: "build",
|
|
1257
|
+
workDir: "",
|
|
1258
|
+
};
|
|
1259
|
+
const slug = repoUrl.replace(/^https?:\/\/github\.com\//, "");
|
|
1260
|
+
try {
|
|
1261
|
+
const { pageUrl } = await runPagesSetupProgrammatic(appDir, {
|
|
1262
|
+
detected,
|
|
1263
|
+
domain: config.domain,
|
|
1264
|
+
});
|
|
1265
|
+
ledger?.record({
|
|
1266
|
+
kind: "ghPages",
|
|
1267
|
+
repo: slug,
|
|
1268
|
+
projectDir: appDir,
|
|
1269
|
+
cname: config.domain,
|
|
1270
|
+
});
|
|
1271
|
+
// Commit the workflow + CNAME file before the push step
|
|
1272
|
+
// below picks up the staged changes. Empty diffs (e.g. re-
|
|
1273
|
+
// running on an idempotent state) just produce a no-op commit.
|
|
1274
|
+
await bashExec("git", ["add", "-A"], { cwd: appDir, silent: true });
|
|
1275
|
+
const status = await bashExec("git", ["status", "--porcelain"], {
|
|
1276
|
+
cwd: appDir,
|
|
1277
|
+
silent: true,
|
|
1278
|
+
});
|
|
1279
|
+
if (status.stdout.trim()) {
|
|
1280
|
+
await bashExec("git", ["commit", "-m", "ci: GitHub Pages setup"], {
|
|
1281
|
+
cwd: appDir,
|
|
1282
|
+
silent: true,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
console.log(chalk.green(` ✓ GitHub Pages will publish at ${pageUrl}`));
|
|
1286
|
+
}
|
|
1287
|
+
catch (err) {
|
|
1288
|
+
console.log(chalk.yellow(` Couldn't auto-wire GitHub Pages: ${err.message}`));
|
|
1289
|
+
console.log(chalk.dim(` Run \`hatchkit gh-pages\` from ${appDir} once the issue is resolved.`));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1173
1292
|
// Step 6.5: push the working branch to origin. Done AFTER Coolify
|
|
1174
1293
|
// wiring + Actions-secret upserts so the workflow's first run
|
|
1175
1294
|
// already has the secrets it needs to deploy. setupGitHub above
|
|
@@ -1178,6 +1297,61 @@ async function handleCreate() {
|
|
|
1178
1297
|
const { pushInitialBranch } = await import("./deploy/github.js");
|
|
1179
1298
|
await pushInitialBranch(appDir);
|
|
1180
1299
|
}
|
|
1300
|
+
// Step 6.6: optional email forwarding setup (Cloudflare Email
|
|
1301
|
+
// Routing). Opt-in prompt — most projects want it but a scripted
|
|
1302
|
+
// / non-interactive create shouldn't pay the latency cost or sink
|
|
1303
|
+
// on a missing accountId without explicit consent.
|
|
1304
|
+
if (config.scaffoldRepo && !config.dryRun && !nonInteractive && process.stdin.isTTY) {
|
|
1305
|
+
try {
|
|
1306
|
+
const wantsEmail = await confirm({
|
|
1307
|
+
message: `Set up email forwarding for ${chalk.cyan(config.domain)} (Cloudflare Email Routing)?`,
|
|
1308
|
+
default: true,
|
|
1309
|
+
});
|
|
1310
|
+
if (wantsEmail) {
|
|
1311
|
+
const { runEmailSetupForDomain } = await import("./email/index.js");
|
|
1312
|
+
const result = await runEmailSetupForDomain({ domain: config.domain }, appDir);
|
|
1313
|
+
// Mirror adopt's ledger plumbing so `hatchkit destroy <project>`
|
|
1314
|
+
// can roll back the email-routing state we just created.
|
|
1315
|
+
if (ledger && result.destination.createdThisRun) {
|
|
1316
|
+
ledger.record({
|
|
1317
|
+
kind: "cloudflareEmailDestination",
|
|
1318
|
+
accountId: result.accountId,
|
|
1319
|
+
destinationId: result.destination.record.id,
|
|
1320
|
+
email: result.destination.record.email,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
for (const dns of result.dnsRecords) {
|
|
1324
|
+
if (!dns.created)
|
|
1325
|
+
continue;
|
|
1326
|
+
ledger?.record({
|
|
1327
|
+
kind: "cloudflareDnsRecord",
|
|
1328
|
+
zoneId: result.zoneId,
|
|
1329
|
+
recordId: dns.id,
|
|
1330
|
+
name: dns.name,
|
|
1331
|
+
type: dns.type,
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
for (const rule of result.rules) {
|
|
1335
|
+
if (!rule.created)
|
|
1336
|
+
continue;
|
|
1337
|
+
ledger?.record({
|
|
1338
|
+
kind: "cloudflareEmailRoutingRule",
|
|
1339
|
+
zoneId: result.zoneId,
|
|
1340
|
+
ruleId: rule.id,
|
|
1341
|
+
address: rule.address,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
catch (err) {
|
|
1347
|
+
// Soft-fail: email forwarding is a follow-up convenience, not a
|
|
1348
|
+
// gating step for the rest of `create`. The user can re-run
|
|
1349
|
+
// `hatchkit email setup` once any underlying issue (e.g. zone
|
|
1350
|
+
// not yet in Cloudflare) is fixed.
|
|
1351
|
+
console.log(chalk.yellow(` ⚠ Email forwarding setup skipped: ${err.message}`));
|
|
1352
|
+
console.log(chalk.dim(` Re-run with \`hatchkit email setup --domain ${config.domain}\`.`));
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1181
1355
|
// Step 7: Deploy ML services
|
|
1182
1356
|
if (config.runDeployment &&
|
|
1183
1357
|
deploy.length > 0 &&
|
|
@@ -1242,6 +1416,9 @@ async function handleCreate() {
|
|
|
1242
1416
|
if (config.surfaces !== "client-only") {
|
|
1243
1417
|
console.log(` API: ${chalk.cyan(`https://${config.domain}/api`)}`);
|
|
1244
1418
|
}
|
|
1419
|
+
if (config.deploymentMode === "gh-pages") {
|
|
1420
|
+
console.log(chalk.dim(` Hosting: GitHub Pages — first build kicks off on push, https cert provisions over the next few minutes.`));
|
|
1421
|
+
}
|
|
1245
1422
|
console.log(` App dir: ${chalk.dim(appDir)}`);
|
|
1246
1423
|
console.log(` Config: ${chalk.dim(getConfigPath())}`);
|
|
1247
1424
|
if (config.scaffoldRepo) {
|
|
@@ -1259,7 +1436,7 @@ async function handleCreate() {
|
|
|
1259
1436
|
}
|
|
1260
1437
|
if (config.features.includes("desktop")) {
|
|
1261
1438
|
console.log(chalk.yellow("\n Next (desktop): replace build/icon.png with a 512×512 logo, then:"));
|
|
1262
|
-
console.log(chalk.dim(" pnpm icons:desktop # cross-platform (
|
|
1439
|
+
console.log(chalk.dim(" pnpm icons:desktop # cross-platform (icon-gen)"));
|
|
1263
1440
|
}
|
|
1264
1441
|
if (config.features.includes("desktop") || config.features.includes("mobile")) {
|
|
1265
1442
|
console.log(chalk.yellow("\n Server CORS: TRUSTED_ORIGINS is already set in .env.example for native clients."));
|
|
@@ -1391,13 +1568,20 @@ function printHelp(topic) {
|
|
|
1391
1568
|
hatchkit create [--dry-run]
|
|
1392
1569
|
|
|
1393
1570
|
${chalk.bold("What it does (interactively):")}
|
|
1394
|
-
1. Prompts for project name, domain,
|
|
1571
|
+
1. Prompts for project name, domain, surfaces, deployment mode, features, ML
|
|
1395
1572
|
2. Copies the starter template and strips unselected features
|
|
1396
1573
|
3. Assigns unique ports per project (server, client, native HMR)
|
|
1397
1574
|
4. Runs \`pnpm install\` (if pnpm is present and you opt in)
|
|
1398
1575
|
5. Initializes git, optionally creates a GitHub repo
|
|
1399
|
-
6. Generates Terraform tfvars + Coolify .env
|
|
1400
|
-
7.
|
|
1576
|
+
6. Generates Terraform tfvars + Coolify .env (Coolify mode)
|
|
1577
|
+
7. Deploys: Terraform → Coolify → ML ${chalk.dim("OR")} GitHub Pages setup
|
|
1578
|
+
|
|
1579
|
+
${chalk.bold("Deployment modes:")}
|
|
1580
|
+
${chalk.cyan("coolify")} Full-stack on Hetzner — DB, providers, Docker. Default.
|
|
1581
|
+
${chalk.cyan("gh-pages")} Static-only on GitHub Pages. Only offered when surfaces
|
|
1582
|
+
is ${chalk.dim("client-only")}; the scaffold's Next config is patched to
|
|
1583
|
+
${chalk.dim('`output: "export"`')} and the gh-pages workflow is written.
|
|
1584
|
+
${chalk.cyan("scaffold-only")} Write files, skip deploy. Pick this to defer setup.
|
|
1401
1585
|
|
|
1402
1586
|
${chalk.bold("Options:")}
|
|
1403
1587
|
--dry-run Show the plan without writing anything
|
|
@@ -1485,6 +1669,7 @@ function printHelp(topic) {
|
|
|
1485
1669
|
|
|
1486
1670
|
${chalk.bold("Usage:")}
|
|
1487
1671
|
cd <project-dir> && hatchkit gh-pages
|
|
1672
|
+
cd <project-dir> && hatchkit gh-pages --undo [--dry-run] [--yes]
|
|
1488
1673
|
|
|
1489
1674
|
${chalk.bold("What it does:")}
|
|
1490
1675
|
1. Reads the repo via \`gh repo view\` (must be a GitHub repo you own).
|
|
@@ -1506,6 +1691,17 @@ function printHelp(topic) {
|
|
|
1506
1691
|
${chalk.bold("After running:")}
|
|
1507
1692
|
git add -A && git commit -m "ci: deploy to GitHub Pages" && git push
|
|
1508
1693
|
|
|
1694
|
+
${chalk.bold("Undo (--undo):")}
|
|
1695
|
+
Reverses what the command put in place:
|
|
1696
|
+
- Disables Pages via ${chalk.dim("DELETE /repos/<owner>/<repo>/pages")} (clears the cname too).
|
|
1697
|
+
- Deletes Cloudflare records that point at GitHub's Pages IPs / ${chalk.dim("<user>.github.io")}
|
|
1698
|
+
for the registered domain (only when a Cloudflare token is configured + the
|
|
1699
|
+
zone is in this account).
|
|
1700
|
+
- Removes ${chalk.cyan(".github/workflows/gh-pages.yml")} (only the file hatchkit writes
|
|
1701
|
+
— hand-written Pages workflows are left untouched).
|
|
1702
|
+
- Removes any ${chalk.cyan("CNAME")} files whose content matches the registered domain.
|
|
1703
|
+
${chalk.dim("--dry-run")} prints the plan without changing anything. ${chalk.dim("--yes")} skips the confirm.
|
|
1704
|
+
|
|
1509
1705
|
${chalk.bold("Notes:")}
|
|
1510
1706
|
- Private repos need a paid GitHub plan for Pages. Free-tier repos
|
|
1511
1707
|
must be made public first.
|
|
@@ -1532,8 +1728,41 @@ function printHelp(topic) {
|
|
|
1532
1728
|
${chalk.dim("INWX_SANDBOX=1")} → use the OTE sandbox instead of production.
|
|
1533
1729
|
|
|
1534
1730
|
${chalk.bold("Prerequisites:")}
|
|
1535
|
-
Run ${chalk.cyan("hatchkit config add dns")}
|
|
1731
|
+
Run ${chalk.cyan("hatchkit config add dns")} (Cloudflare-only), then answer
|
|
1536
1732
|
${chalk.cyan("yes")} to "Is INWX your domain registrar?" when prompted.
|
|
1733
|
+
`);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (topic === "email") {
|
|
1737
|
+
console.log(`
|
|
1738
|
+
${chalk.bold("hatchkit email")} — Cloudflare Email Routing setup
|
|
1739
|
+
|
|
1740
|
+
${chalk.bold("Subcommands:")}
|
|
1741
|
+
setup Configure Email Routing + DNS (MX, SPF, DMARC) for a domain
|
|
1742
|
+
status Print current routing state (read-only)
|
|
1743
|
+
|
|
1744
|
+
${chalk.bold("Flags (setup):")}
|
|
1745
|
+
--domain <fqdn> Override the project domain
|
|
1746
|
+
--to <email> Forwarding destination (saved globally on first use)
|
|
1747
|
+
--addresses <list> Comma-separated local parts (skips picker)
|
|
1748
|
+
--all-defaults Use every default preset; skip picker
|
|
1749
|
+
--no-catch-all Don't set the *@domain catch-all rule
|
|
1750
|
+
--dmarc <none|quarantine|reject> DMARC policy (default: quarantine)
|
|
1751
|
+
--no-resend-spf Skip auto-merging _spf.resend.com
|
|
1752
|
+
|
|
1753
|
+
${chalk.bold("What it sets:")}
|
|
1754
|
+
· Email Routing enabled on the zone
|
|
1755
|
+
· Destination address verified at Cloudflare (verification email sent)
|
|
1756
|
+
· MX records → route1/route2/route3.mx.cloudflare.net
|
|
1757
|
+
· SPF TXT (single record, merged with Resend if detected)
|
|
1758
|
+
· DMARC TXT at _dmarc.<domain> (default p=quarantine sp=none)
|
|
1759
|
+
· One forwarding rule per picked address
|
|
1760
|
+
· Optional catch-all rule (*@<domain>)
|
|
1761
|
+
|
|
1762
|
+
${chalk.bold("Prerequisites:")}
|
|
1763
|
+
DNS must be on Cloudflare (${chalk.cyan("hatchkit config add dns")}). The token
|
|
1764
|
+
needs Zone:DNS:Edit + Zone:Email Routing Rules:Edit +
|
|
1765
|
+
Account:Email Routing Addresses:Edit.
|
|
1537
1766
|
`);
|
|
1538
1767
|
return;
|
|
1539
1768
|
}
|
|
@@ -1545,6 +1774,46 @@ function printHelp(topic) {
|
|
|
1545
1774
|
stored (Coolify /version, Hetzner /servers, Cloudflare /tokens/verify,
|
|
1546
1775
|
Resend /domains, …). Reports ok / fail / not-configured per provider
|
|
1547
1776
|
and exits non-zero if any check fails. Safe to run repeatedly.
|
|
1777
|
+
`);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (topic === "overview") {
|
|
1781
|
+
console.log(`
|
|
1782
|
+
${chalk.bold("hatchkit overview")} — fleet-level view of every configured provider
|
|
1783
|
+
|
|
1784
|
+
Distinct from ${chalk.cyan("status")} (which providers do I have credentials for?),
|
|
1785
|
+
${chalk.cyan("doctor")} (are those credentials valid?), and ${chalk.cyan("inventory")} (what does THIS
|
|
1786
|
+
project have?). ${chalk.cyan("overview")} answers "what does my whole hatchkit
|
|
1787
|
+
footprint look like, across every configured provider?" — no name or
|
|
1788
|
+
domain filter, just a roll-up of top-level resources.
|
|
1789
|
+
|
|
1790
|
+
${chalk.bold("What it lists:")}
|
|
1791
|
+
· Coolify applications, projects, databases
|
|
1792
|
+
· Cloudflare DNS zones
|
|
1793
|
+
· R2 buckets (whole account)
|
|
1794
|
+
· Hetzner S3 / AWS S3 credential presence (bucket listing not implemented)
|
|
1795
|
+
· Resend verified domains
|
|
1796
|
+
· GlitchTip projects in the configured org
|
|
1797
|
+
· OpenPanel projects
|
|
1798
|
+
· Stripe webhook endpoints (test + live)
|
|
1799
|
+
|
|
1800
|
+
${chalk.bold("Cross-references:")}
|
|
1801
|
+
After listing every provider, ${chalk.cyan("overview")} cross-references the
|
|
1802
|
+
raw data to flag fleet-level inconsistencies — the kind of bitrot
|
|
1803
|
+
that a single-provider lens can't see:
|
|
1804
|
+
|
|
1805
|
+
· Coolify app deploys from a repo \`gh\` can't find (deleted/renamed)
|
|
1806
|
+
· App fqdn references an apex with no Cloudflare zone
|
|
1807
|
+
· R2 bucket follows the \`<project>-<role>\` convention but has no
|
|
1808
|
+
matching Coolify app (orphan from a destroyed project)
|
|
1809
|
+
· GlitchTip / OpenPanel project with no Coolify app counterpart
|
|
1810
|
+
· Cloudflare zone with no Coolify app pointing into it
|
|
1811
|
+
|
|
1812
|
+
${chalk.bold("Flags:")}
|
|
1813
|
+
--all Print every resource per provider (default: 6-line preview)
|
|
1814
|
+
--json Machine-readable OverviewReport (non-interactive)
|
|
1815
|
+
|
|
1816
|
+
Read-only — every call is a GET. Safe to run repeatedly.
|
|
1548
1817
|
`);
|
|
1549
1818
|
return;
|
|
1550
1819
|
}
|
|
@@ -1591,7 +1860,19 @@ function printHelp(topic) {
|
|
|
1591
1860
|
--domain <domain> Override inferred domain
|
|
1592
1861
|
--repo <owner/name> Override inferred GitHub repo
|
|
1593
1862
|
--yes, -y Skip confirm-inferred-value prompts
|
|
1863
|
+
--save Write a minimal .hatchkit.json without prompting
|
|
1864
|
+
--no-save Suppress the end-of-run save prompt
|
|
1594
1865
|
--json Machine-readable InventoryReport (non-interactive)
|
|
1866
|
+
|
|
1867
|
+
${chalk.bold("Persisting identity:")}
|
|
1868
|
+
After an interactive run, when ${chalk.cyan(".hatchkit.json")} doesn't yet exist
|
|
1869
|
+
and both name + domain are inferred, hatchkit offers to write a
|
|
1870
|
+
minimal manifest. The manifest carries the right schema for every
|
|
1871
|
+
other command (adopt, update, sync, keys), with conservative defaults
|
|
1872
|
+
for fields inventory can't infer (features=[], s3Provider="none",
|
|
1873
|
+
deployTarget="existing", ports={server:3000,client:5173}). Run
|
|
1874
|
+
${chalk.cyan("hatchkit adopt --resume")} afterwards to flesh out the rest via
|
|
1875
|
+
the adopt stepper.
|
|
1595
1876
|
`);
|
|
1596
1877
|
return;
|
|
1597
1878
|
}
|
|
@@ -2028,6 +2309,7 @@ function printHelp(topic) {
|
|
|
2028
2309
|
status Show what's configured and what's next
|
|
2029
2310
|
doctor Health-check every provider with contextual fix hints
|
|
2030
2311
|
inventory Survey what already exists for this project (and flag drift)
|
|
2312
|
+
overview Fleet-level survey — every resource across all configured providers
|
|
2031
2313
|
explain One-page mental model of the CLI
|
|
2032
2314
|
|
|
2033
2315
|
${chalk.bold("Projects:")}
|
|
@@ -2042,6 +2324,7 @@ function printHelp(topic) {
|
|
|
2042
2324
|
sync Push the manifest's domain/ports onto the matching Coolify app(s)
|
|
2043
2325
|
gh-pages Wire GitHub Pages for the current repo (static / Vite / Jekyll — with DNS)
|
|
2044
2326
|
dns DNS reconciliation helpers (link-to-cloudflare, …)
|
|
2327
|
+
email Set up Cloudflare Email Routing + MX/SPF/DMARC (setup/status)
|
|
2045
2328
|
keys show <p> Print the dotenvx private key for a project
|
|
2046
2329
|
keys set <p> Upsert the key into the OS keychain (after \`dotenvx rotate\`)
|
|
2047
2330
|
keys rotate <p> Rotate the dotenvx keypair, mirror to keychain + (optional) deploy targets
|
|
@@ -2056,6 +2339,7 @@ function printHelp(topic) {
|
|
|
2056
2339
|
status --json StatusSnapshot as JSON
|
|
2057
2340
|
doctor --json Per-provider health with fix hints as JSON
|
|
2058
2341
|
inventory --json InventoryReport — resources found per provider + drift
|
|
2342
|
+
overview --json OverviewReport — fleet-level resource counts + names
|
|
2059
2343
|
completion <shell> Print a zsh/bash/fish completion script
|
|
2060
2344
|
|
|
2061
2345
|
${chalk.bold("Options:")}
|