hatchkit 0.1.4 → 0.1.6
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 +2 -0
- package/dist/adopt.d.ts.map +1 -0
- package/dist/adopt.js +819 -0
- package/dist/adopt.js.map +1 -0
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +3 -0
- package/dist/completion.js.map +1 -1
- package/dist/config.d.ts +30 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +108 -0
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify-app.d.ts +35 -0
- package/dist/deploy/coolify-app.d.ts.map +1 -0
- package/dist/deploy/coolify-app.js +238 -0
- package/dist/deploy/coolify-app.js.map +1 -0
- package/dist/deploy/pages.js +50 -9
- package/dist/deploy/pages.js.map +1 -1
- package/dist/deploy/rename-domain.d.ts.map +1 -1
- package/dist/deploy/rename-domain.js +26 -6
- package/dist/deploy/rename-domain.js.map +1 -1
- package/dist/deploy/rollback.d.ts +10 -0
- package/dist/deploy/rollback.d.ts.map +1 -0
- package/dist/deploy/rollback.js +295 -0
- package/dist/deploy/rollback.js.map +1 -0
- package/dist/deploy/terraform.d.ts +10 -1
- package/dist/deploy/terraform.d.ts.map +1 -1
- package/dist/deploy/terraform.js +177 -42
- package/dist/deploy/terraform.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +25 -0
- package/dist/doctor.js.map +1 -1
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +5 -0
- package/dist/explain.js.map +1 -1
- package/dist/index.js +377 -122
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +283 -11
- package/dist/prompts.js.map +1 -1
- package/dist/provision/stripe.d.ts +19 -0
- package/dist/provision/stripe.d.ts.map +1 -0
- package/dist/provision/stripe.js +58 -0
- package/dist/provision/stripe.js.map +1 -0
- package/dist/scaffold/dotenvx.d.ts.map +1 -1
- package/dist/scaffold/dotenvx.js +35 -11
- package/dist/scaffold/dotenvx.js.map +1 -1
- package/dist/scaffold/infra.d.ts +21 -1
- package/dist/scaffold/infra.d.ts.map +1 -1
- package/dist/scaffold/infra.js +66 -20
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +7 -0
- package/dist/status.js.map +1 -1
- package/dist/utils/cloudflare-api.d.ts +23 -0
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +31 -0
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/coolify-api.d.ts +64 -3
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +99 -3
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +68 -0
- package/dist/utils/run-ledger.d.ts.map +1 -0
- package/dist/utils/run-ledger.js +99 -0
- package/dist/utils/run-ledger.js.map +1 -0
- package/dist/utils/secrets.d.ts +2 -0
- package/dist/utils/secrets.d.ts.map +1 -1
- package/dist/utils/secrets.js +2 -0
- package/dist/utils/secrets.js.map +1 -1
- package/package.json +2 -2
- package/scripts/release-prep.mjs +130 -95
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
|
+
import { handleCreateFailure, runRollback } from "./deploy/rollback.js";
|
|
10
11
|
import { runTerraform } from "./deploy/terraform.js";
|
|
11
12
|
import { collectProjectConfig } from "./prompts.js";
|
|
12
13
|
import { runProvision, runUnprovision } from "./provision/index.js";
|
|
@@ -16,6 +17,8 @@ import { mlEnvVarName, printMlSummary, resolveMlServices } from "./scaffold/ml-c
|
|
|
16
17
|
import { runUpdate } from "./scaffold/update.js";
|
|
17
18
|
import { exec, execOk } from "./utils/exec.js";
|
|
18
19
|
import { parseCreateFlags } from "./utils/flags.js";
|
|
20
|
+
import { RunLedger } from "./utils/run-ledger.js";
|
|
21
|
+
import { SECRET_KEYS } from "./utils/secrets.js";
|
|
19
22
|
import { getCliVersion } from "./utils/version.js";
|
|
20
23
|
// ---------------------------------------------------------------------------
|
|
21
24
|
// CLI
|
|
@@ -118,6 +121,18 @@ async function main() {
|
|
|
118
121
|
return printHelp("remove");
|
|
119
122
|
await handleRemove();
|
|
120
123
|
break;
|
|
124
|
+
case "adopt": {
|
|
125
|
+
if (args.includes("--help"))
|
|
126
|
+
return printHelp("adopt");
|
|
127
|
+
const { runAdopt } = await import("./adopt.js");
|
|
128
|
+
await runAdopt(resolve("."));
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "destroy":
|
|
132
|
+
if (args.includes("--help"))
|
|
133
|
+
return printHelp("destroy");
|
|
134
|
+
await handleDestroy();
|
|
135
|
+
break;
|
|
121
136
|
case "rename-domain": {
|
|
122
137
|
if (args.includes("--help"))
|
|
123
138
|
return printHelp("rename-domain");
|
|
@@ -289,6 +304,44 @@ async function handleAdd() {
|
|
|
289
304
|
}
|
|
290
305
|
await runProvision({ baseName, services, surfaces, enableDevObs });
|
|
291
306
|
}
|
|
307
|
+
async function handleDestroy() {
|
|
308
|
+
// `hatchkit destroy <project> [--yes] [--recipe]`
|
|
309
|
+
// destroy reads the run ledger written by `hatchkit create` and
|
|
310
|
+
// reverses the recorded steps. With --recipe it just prints the
|
|
311
|
+
// shell-command rollback recipe and exits (no execution).
|
|
312
|
+
const positional = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
313
|
+
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
314
|
+
const recipeOnly = args.includes("--recipe");
|
|
315
|
+
let name = positional[0];
|
|
316
|
+
if (!name) {
|
|
317
|
+
const { input } = await import("@inquirer/prompts");
|
|
318
|
+
const { validateProjectName } = await import("./utils/validate.js");
|
|
319
|
+
name = await input({
|
|
320
|
+
message: "Project name to destroy:",
|
|
321
|
+
validate: validateProjectName,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const ledger = RunLedger.load(name);
|
|
325
|
+
if (!ledger) {
|
|
326
|
+
console.log(chalk.yellow(` No run ledger found for "${name}". Either it was never created via \`hatchkit create\` or the ledger was already cleaned up.`));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
const { printRecipe } = await import("./deploy/rollback.js");
|
|
330
|
+
printRecipe(ledger);
|
|
331
|
+
if (recipeOnly)
|
|
332
|
+
return;
|
|
333
|
+
if (!skipConfirm) {
|
|
334
|
+
const ok = await confirm({
|
|
335
|
+
message: `Roll back ${ledger.steps.length} step(s) for ${chalk.cyan(name)}?`,
|
|
336
|
+
default: false,
|
|
337
|
+
});
|
|
338
|
+
if (!ok) {
|
|
339
|
+
console.log(chalk.dim(" Aborted. Ledger left in place."));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
await runRollback(ledger, { yes: skipConfirm });
|
|
344
|
+
}
|
|
292
345
|
async function handleRemove() {
|
|
293
346
|
// Mirrors handleAdd: `hatchkit remove [<name>] [<services>] [--dry-run] [--yes]`
|
|
294
347
|
// hatchkit remove (fully interactive)
|
|
@@ -408,6 +461,18 @@ async function handleCreate() {
|
|
|
408
461
|
await ensureS3(config.s3Provider);
|
|
409
462
|
}
|
|
410
463
|
}
|
|
464
|
+
// Pre-flight observability + email + Stripe providers used by `hatchkit
|
|
465
|
+
// create` directly (not just `add`): if the user picked the analytics
|
|
466
|
+
// feature, GlitchTip needs to be configured before we can mint a DSN
|
|
467
|
+
// for them. Same for Stripe webhook auto-provisioning.
|
|
468
|
+
if (config.features.includes("analytics")) {
|
|
469
|
+
const { ensureGlitchtip } = await import("./config.js");
|
|
470
|
+
await ensureGlitchtip();
|
|
471
|
+
}
|
|
472
|
+
if (config.features.includes("stripe")) {
|
|
473
|
+
const { ensureStripe } = await import("./config.js");
|
|
474
|
+
await ensureStripe();
|
|
475
|
+
}
|
|
411
476
|
const appDir = resolve(config.name);
|
|
412
477
|
// Resolve ML services (reuse or deploy)
|
|
413
478
|
const { reuse, deploy } = await resolveMlServices(config);
|
|
@@ -427,146 +492,250 @@ async function handleCreate() {
|
|
|
427
492
|
if (config.dryRun) {
|
|
428
493
|
console.log(chalk.yellow("\n [dry-run mode — no changes will be made]\n"));
|
|
429
494
|
}
|
|
430
|
-
//
|
|
495
|
+
// Run ledger — append-only record of what each step accomplished, so
|
|
496
|
+
// a mid-run failure can offer a tailored cleanup recipe + auto-undo.
|
|
497
|
+
// Skipped for dry-run (nothing to undo) and when the user opted out of
|
|
498
|
+
// both scaffolding and deployment (also nothing to undo).
|
|
499
|
+
const useLedger = !config.dryRun && (config.scaffoldRepo || config.runDeployment);
|
|
500
|
+
const ledger = useLedger ? RunLedger.start(config.name) : null;
|
|
501
|
+
// Hoisted across the try-block boundary so the success summary below
|
|
502
|
+
// can read them. Declared with let so they can be reassigned inside.
|
|
431
503
|
let scaffoldResult;
|
|
432
|
-
if (config.scaffoldRepo) {
|
|
433
|
-
scaffoldResult = await scaffoldApp(config, appDir);
|
|
434
|
-
const { ports } = scaffoldResult;
|
|
435
|
-
console.log(chalk.dim(` Ports: server=${ports.server}, client=${ports.client}` +
|
|
436
|
-
(ports.nativeHmr ? `, native HMR=${ports.nativeHmr}` : "")));
|
|
437
|
-
if (scaffoldResult.dotenvx) {
|
|
438
|
-
const { printDotenvxSummary } = await import("./scaffold/dotenvx.js");
|
|
439
|
-
printDotenvxSummary(scaffoldResult.dotenvx, config.name);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (config.dryRun) {
|
|
443
|
-
scaffoldInfra(config, INFRA_ROOT, {
|
|
444
|
-
serverPort: scaffoldResult?.ports.server,
|
|
445
|
-
clientPort: scaffoldResult?.ports.client,
|
|
446
|
-
});
|
|
447
|
-
console.log(chalk.green("\n ✓ Dry run complete. No changes were made.\n"));
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
// Step 2: Install deps. Required for the initial commit to pick up
|
|
451
|
-
// the lockfile delta and for the user to `pnpm dev` immediately.
|
|
452
504
|
let installedDeps = false;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
? true
|
|
463
|
-
: await confirm({
|
|
464
|
-
message: "Install dependencies now (pnpm install)?",
|
|
465
|
-
default: true,
|
|
466
|
-
});
|
|
467
|
-
if (shouldInstall) {
|
|
468
|
-
const res = await exec("pnpm", ["install"], {
|
|
469
|
-
cwd: appDir,
|
|
470
|
-
spinner: "Installing dependencies...",
|
|
505
|
+
try {
|
|
506
|
+
// Step 1: Scaffold app repo
|
|
507
|
+
if (config.scaffoldRepo) {
|
|
508
|
+
scaffoldResult = await scaffoldApp(config, appDir);
|
|
509
|
+
ledger?.record({ kind: "scaffold", path: appDir });
|
|
510
|
+
if (scaffoldResult.dotenvx) {
|
|
511
|
+
ledger?.record({
|
|
512
|
+
kind: "keychain",
|
|
513
|
+
account: SECRET_KEYS.dotenvxPrivateKey(config.name),
|
|
471
514
|
});
|
|
472
|
-
|
|
473
|
-
|
|
515
|
+
}
|
|
516
|
+
const { ports } = scaffoldResult;
|
|
517
|
+
console.log(chalk.dim(` Ports: server=${ports.server}, client=${ports.client}` +
|
|
518
|
+
(ports.nativeHmr ? `, native HMR=${ports.nativeHmr}` : "")));
|
|
519
|
+
if (scaffoldResult.dotenvx) {
|
|
520
|
+
const { printDotenvxSummary } = await import("./scaffold/dotenvx.js");
|
|
521
|
+
printDotenvxSummary(scaffoldResult.dotenvx, config.name);
|
|
522
|
+
}
|
|
523
|
+
// Auto-provision GlitchTip + write its DSN encrypted into
|
|
524
|
+
// .env.production. The user picked the `analytics` feature; we
|
|
525
|
+
// already verified GlitchTip is configured during pre-flight.
|
|
526
|
+
if (config.features.includes("analytics")) {
|
|
527
|
+
try {
|
|
528
|
+
const { provisionGlitchtipClient } = await import("./provision/glitchtip.js");
|
|
529
|
+
const { set: dotenvxSet } = await import("@dotenvx/dotenvx");
|
|
530
|
+
const ora = (await import("ora")).default;
|
|
531
|
+
const spinner = ora(`GlitchTip: creating project ${config.name}`).start();
|
|
532
|
+
const res = await provisionGlitchtipClient(config.name);
|
|
533
|
+
ledger?.record({ kind: "glitchtip", project: config.name });
|
|
534
|
+
spinner.succeed(`GlitchTip project ready (DSN encrypted into .env.production)`);
|
|
535
|
+
const prodEnvPath = join(appDir, "packages/server/.env.production");
|
|
536
|
+
dotenvxSet("GLITCHTIP_DSN", res.dsn, { path: prodEnvPath, encrypt: true });
|
|
474
537
|
}
|
|
475
|
-
|
|
476
|
-
console.log(chalk.yellow(
|
|
538
|
+
catch (err) {
|
|
539
|
+
console.log(chalk.yellow(` Couldn't auto-provision GlitchTip: ${err.message}`));
|
|
540
|
+
console.log(chalk.dim(` Run \`hatchkit add ${config.name} glitchtip\` once GlitchTip is reachable.`));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Stripe: register a webhook endpoint at https://<domain>/api/stripe/webhook
|
|
544
|
+
// and write STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET encrypted
|
|
545
|
+
// into .env.production. The publishable key is non-secret (it ships
|
|
546
|
+
// in the browser bundle); we still encrypt-write it so the same env
|
|
547
|
+
// file is the single source of truth per environment.
|
|
548
|
+
if (config.features.includes("stripe")) {
|
|
549
|
+
try {
|
|
550
|
+
const { provisionStripeWebhook } = await import("./provision/stripe.js");
|
|
551
|
+
const { getStripeConfig } = await import("./config.js");
|
|
552
|
+
const { set: dotenvxSet } = await import("@dotenvx/dotenvx");
|
|
553
|
+
const ora = (await import("ora")).default;
|
|
554
|
+
const spinner = ora(`Stripe: registering webhook for ${config.domain}`).start();
|
|
555
|
+
const stripeCfg = await getStripeConfig();
|
|
556
|
+
const webhook = await provisionStripeWebhook(config.name, config.domain);
|
|
557
|
+
spinner.succeed(`Stripe webhook ready (${webhook.mode} mode → https://${config.domain}/api/stripe/webhook)`);
|
|
558
|
+
const prodEnvPath = join(appDir, "packages/server/.env.production");
|
|
559
|
+
if (stripeCfg) {
|
|
560
|
+
dotenvxSet("STRIPE_SECRET_KEY", stripeCfg.secretKey, {
|
|
561
|
+
path: prodEnvPath,
|
|
562
|
+
encrypt: true,
|
|
563
|
+
});
|
|
564
|
+
dotenvxSet("STRIPE_PUBLISHABLE_KEY", stripeCfg.publishableKey, {
|
|
565
|
+
path: prodEnvPath,
|
|
566
|
+
encrypt: true,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
dotenvxSet("STRIPE_WEBHOOK_SECRET", webhook.signingSecret, {
|
|
570
|
+
path: prodEnvPath,
|
|
571
|
+
encrypt: true,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
console.log(chalk.yellow(` Couldn't auto-provision Stripe webhook: ${err.message}`));
|
|
576
|
+
console.log(chalk.dim(` Create one manually: dashboard.stripe.com → Developers → Webhooks,\n` +
|
|
577
|
+
` point at https://${config.domain}/api/stripe/webhook, then\n` +
|
|
578
|
+
` \`dotenvx set STRIPE_WEBHOOK_SECRET <whsec_…> -f packages/server/.env.production\`.`));
|
|
477
579
|
}
|
|
478
580
|
}
|
|
479
581
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (config.runDeployment) {
|
|
495
|
-
await runTerraform(config, INFRA_ROOT);
|
|
496
|
-
}
|
|
497
|
-
// Step 6: Coolify setup
|
|
498
|
-
if (config.runDeployment) {
|
|
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);
|
|
582
|
+
if (config.dryRun) {
|
|
583
|
+
scaffoldInfra(config, INFRA_ROOT, {
|
|
584
|
+
serverPort: scaffoldResult?.ports.server,
|
|
585
|
+
clientPort: scaffoldResult?.ports.client,
|
|
586
|
+
});
|
|
587
|
+
console.log(chalk.green("\n ✓ Dry run complete. No changes were made.\n"));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Step 2: Install deps. Required for the initial commit to pick up
|
|
591
|
+
// the lockfile delta and for the user to `pnpm dev` immediately.
|
|
592
|
+
if (config.scaffoldRepo) {
|
|
593
|
+
const hasPnpm = await execOk("pnpm", ["--version"]);
|
|
594
|
+
if (!hasPnpm) {
|
|
595
|
+
console.log(chalk.yellow(" pnpm not found on PATH — skipping install step. Install deps with your preferred tool once available."));
|
|
508
596
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
597
|
+
else {
|
|
598
|
+
// In non-interactive mode, auto-accept the install so `--yes`
|
|
599
|
+
// doesn't stall waiting on a y/n prompt.
|
|
600
|
+
const shouldInstall = nonInteractive
|
|
601
|
+
? true
|
|
602
|
+
: await confirm({
|
|
603
|
+
message: "Install dependencies now (pnpm install)?",
|
|
604
|
+
default: true,
|
|
605
|
+
});
|
|
606
|
+
if (shouldInstall) {
|
|
607
|
+
const res = await exec("pnpm", ["install"], {
|
|
608
|
+
cwd: appDir,
|
|
609
|
+
spinner: "Installing dependencies...",
|
|
610
|
+
});
|
|
611
|
+
if (res.exitCode === 0) {
|
|
612
|
+
installedDeps = true;
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
console.log(chalk.yellow(" pnpm install failed — continuing anyway."));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
514
618
|
}
|
|
515
619
|
}
|
|
516
|
-
//
|
|
517
|
-
// can
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
620
|
+
// Step 3: Git + GitHub — must run BEFORE scaffoldInfra so the repo
|
|
621
|
+
// URL can be threaded into the Coolify env (GITHUB_REPO_URL).
|
|
622
|
+
let repoUrl = null;
|
|
623
|
+
if (config.scaffoldRepo) {
|
|
624
|
+
repoUrl = await setupGitHub(config, appDir);
|
|
625
|
+
if (repoUrl) {
|
|
626
|
+
// Strip the https://github.com/ prefix so the recorded value is
|
|
627
|
+
// gh-CLI-friendly (`gh repo delete <owner>/<repo>`).
|
|
628
|
+
const slug = repoUrl.replace(/^https?:\/\/github\.com\//, "");
|
|
629
|
+
ledger?.record({ kind: "github", repo: slug });
|
|
523
630
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
631
|
+
}
|
|
632
|
+
// Step 4: Generate infra configs (with repo URL + ports baked in).
|
|
633
|
+
const infraResult = scaffoldInfra(config, INFRA_ROOT, {
|
|
634
|
+
repoUrl: repoUrl ?? undefined,
|
|
635
|
+
serverPort: scaffoldResult?.ports.server,
|
|
636
|
+
clientPort: scaffoldResult?.ports.client,
|
|
637
|
+
});
|
|
638
|
+
if (infraResult.tfvarsPath) {
|
|
639
|
+
ledger?.record({ kind: "tfvars", path: infraResult.tfvarsPath });
|
|
640
|
+
}
|
|
641
|
+
if (infraResult.coolifyEnvPath) {
|
|
642
|
+
ledger?.record({ kind: "coolifyEnv", path: infraResult.coolifyEnvPath });
|
|
643
|
+
}
|
|
644
|
+
// Step 5: Terraform (DNS + optionally server)
|
|
645
|
+
if (config.runDeployment) {
|
|
646
|
+
const tfResult = await runTerraform(config, INFRA_ROOT);
|
|
647
|
+
if (tfResult.applied) {
|
|
648
|
+
ledger?.record({
|
|
649
|
+
kind: "terraformApplied",
|
|
650
|
+
stackDir: tfResult.applied.stackDir,
|
|
651
|
+
tfvarsPath: tfResult.applied.tfvarsPath,
|
|
652
|
+
});
|
|
527
653
|
}
|
|
528
654
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
"3d-extraction",
|
|
541
|
-
"subtitles",
|
|
542
|
-
"image-recognition",
|
|
543
|
-
"background-removal",
|
|
544
|
-
"custom-hf",
|
|
545
|
-
];
|
|
546
|
-
console.log(chalk.bold("\n ML service endpoints (add to Coolify env):"));
|
|
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}`));
|
|
655
|
+
// Step 6: Coolify setup
|
|
656
|
+
if (config.runDeployment) {
|
|
657
|
+
await runCoolifySetup(config, INFRA_ROOT);
|
|
658
|
+
// Provision a per-project MongoDB container on Coolify when the
|
|
659
|
+
// user picked that path. Best-effort: a failure here doesn't undo
|
|
660
|
+
// the app deploy — we surface clear instructions instead.
|
|
661
|
+
if (config.mongodbProvider === "coolify" && config.scaffoldRepo) {
|
|
662
|
+
try {
|
|
663
|
+
const { provisionCoolifyMongo } = await import("./deploy/coolify-mongo.js");
|
|
664
|
+
const serverEnvDir = join(appDir, "packages/server");
|
|
665
|
+
await provisionCoolifyMongo(config, serverEnvDir);
|
|
557
666
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
667
|
+
catch (err) {
|
|
668
|
+
console.log(chalk.yellow(` Couldn't auto-provision MongoDB: ${err.message}`));
|
|
669
|
+
console.log(chalk.dim(` Create one manually in Coolify: New → Database → MongoDB,\n` +
|
|
670
|
+
` then set MONGODB_URI on the app's env (or run\n` +
|
|
671
|
+
` \`dotenvx set MONGODB_URI <url> -f packages/server/.env.production\`).`));
|
|
562
672
|
}
|
|
563
673
|
}
|
|
674
|
+
// Push the dotenvx private key to Coolify so the starter's server
|
|
675
|
+
// can decrypt .env.production at runtime. Best-effort — if the
|
|
676
|
+
// Coolify app doesn't exist yet (race with the stack script), we
|
|
677
|
+
// print the manual command instead of failing the whole flow.
|
|
678
|
+
if (scaffoldResult?.dotenvx) {
|
|
679
|
+
try {
|
|
680
|
+
await pushProjectKeyToCoolify(config.name);
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
console.log(chalk.yellow(` Couldn't auto-push dotenvx key: ${err.message}`));
|
|
684
|
+
console.log(chalk.dim(` Push manually once the Coolify app exists: hatchkit keys push ${config.name}`));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Step 7: Deploy ML services
|
|
689
|
+
if (config.runDeployment &&
|
|
690
|
+
deploy.length > 0 &&
|
|
691
|
+
config.gpuPlatforms &&
|
|
692
|
+
config.gpuPlatforms.length > 0) {
|
|
693
|
+
const endpoints = await deployMlServices(deploy, config.gpuPlatforms, SERVICES_ROOT, config.customHfModelId);
|
|
694
|
+
// Print env vars to set
|
|
695
|
+
if (Object.keys(endpoints).length > 0) {
|
|
696
|
+
const { mlPlatformUrlEnv } = await import("./scaffold/ml-client.js");
|
|
697
|
+
const knownServices = [
|
|
698
|
+
"3d-extraction",
|
|
699
|
+
"subtitles",
|
|
700
|
+
"image-recognition",
|
|
701
|
+
"background-removal",
|
|
702
|
+
"custom-hf",
|
|
703
|
+
];
|
|
704
|
+
console.log(chalk.bold("\n ML service endpoints (add to Coolify env):"));
|
|
705
|
+
console.log(chalk.dim(` ML_BACKEND=${config.gpuPlatforms[0]}`));
|
|
706
|
+
for (const [service, byPlatform] of Object.entries(endpoints)) {
|
|
707
|
+
if (!knownServices.includes(service))
|
|
708
|
+
continue;
|
|
709
|
+
const svc = service;
|
|
710
|
+
// Per-platform URL — the runtime config picks one based on ML_BACKEND.
|
|
711
|
+
for (const [platform, url] of Object.entries(byPlatform)) {
|
|
712
|
+
if (!url)
|
|
713
|
+
continue;
|
|
714
|
+
console.log(chalk.dim(` ${mlPlatformUrlEnv(svc, platform)}=${url}`));
|
|
715
|
+
}
|
|
716
|
+
// Legacy ENDPOINT for back-compat — points at the default platform.
|
|
717
|
+
const defaultUrl = byPlatform[config.gpuPlatforms[0]];
|
|
718
|
+
if (defaultUrl) {
|
|
719
|
+
console.log(chalk.dim(` ${mlEnvVarName(svc)}=${defaultUrl}`));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
ledger?.complete();
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
if (ledger) {
|
|
728
|
+
await handleCreateFailure(ledger, err);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
console.log(chalk.red(`\n ✗ ${err instanceof Error ? err.message : String(err)}\n`));
|
|
564
732
|
}
|
|
733
|
+
process.exit(1);
|
|
565
734
|
}
|
|
566
735
|
// Final summary
|
|
567
736
|
console.log(chalk.bold("\n ── Done! ─────────────────────────────────────────────────\n"));
|
|
568
737
|
console.log(` App: ${chalk.cyan(`https://${config.domain}`)}`);
|
|
569
|
-
console.log(` API: ${chalk.cyan(`https
|
|
738
|
+
console.log(` API: ${chalk.cyan(`https://${config.domain}/api`)}`);
|
|
570
739
|
console.log(` App dir: ${chalk.dim(appDir)}`);
|
|
571
740
|
console.log(` Config: ${chalk.dim(getConfigPath())}`);
|
|
572
741
|
if (config.scaffoldRepo) {
|
|
@@ -577,9 +746,6 @@ async function handleCreate() {
|
|
|
577
746
|
console.log(chalk.yellow(`\n Next: cd ${config.name} && pnpm install && pnpm dev`));
|
|
578
747
|
}
|
|
579
748
|
}
|
|
580
|
-
if (config.features.includes("stripe")) {
|
|
581
|
-
console.log(chalk.yellow("\n Next: Set STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET in Coolify"));
|
|
582
|
-
}
|
|
583
749
|
if (config.features.includes("mobile")) {
|
|
584
750
|
console.log(chalk.yellow("\n Next (mobile): generate native projects once:"));
|
|
585
751
|
console.log(chalk.dim(" pnpm cap:add:ios # requires Xcode"));
|
|
@@ -628,6 +794,7 @@ async function handleConfig() {
|
|
|
628
794
|
case "glitchtip":
|
|
629
795
|
case "openpanel":
|
|
630
796
|
case "resend":
|
|
797
|
+
case "stripe":
|
|
631
798
|
await reconfigureProvider(provider);
|
|
632
799
|
break;
|
|
633
800
|
case "s3": {
|
|
@@ -882,6 +1049,61 @@ function printHelp(topic) {
|
|
|
882
1049
|
hatchkit add raptor-runner all --surfaces=shared \\
|
|
883
1050
|
--server-dir ./raptor-runner/packages/server \\
|
|
884
1051
|
--client-dir ./raptor-runner/packages/client
|
|
1052
|
+
`);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (topic === "adopt") {
|
|
1056
|
+
console.log(`
|
|
1057
|
+
${chalk.bold("hatchkit adopt")} — bring an existing project under hatchkit management
|
|
1058
|
+
|
|
1059
|
+
${chalk.bold("Usage:")}
|
|
1060
|
+
cd <project-dir> && hatchkit adopt
|
|
1061
|
+
|
|
1062
|
+
${chalk.bold("What it does:")}
|
|
1063
|
+
Inverse of \`hatchkit create\`. Inspects the current directory,
|
|
1064
|
+
detects what's already there (package.json name, repo layout,
|
|
1065
|
+
.env state, dotenvx encryption, Coolify app match, git origin,
|
|
1066
|
+
feature flags inferable from deps), then runs a stepper-style
|
|
1067
|
+
review. On confirm:
|
|
1068
|
+
|
|
1069
|
+
· ${chalk.cyan("Initialize dotenvx")} — generates an encrypted
|
|
1070
|
+
\`.env.production\` + \`.env.keys\` if missing, or re-encrypts
|
|
1071
|
+
an existing plain file. The keypair is what every other
|
|
1072
|
+
hatchkit step depends on, so this defaults to ON.
|
|
1073
|
+
· Imports DOTENV_PRIVATE_KEY_PRODUCTION into the OS keychain.
|
|
1074
|
+
· Writes \`.hatchkit.json\` so \`update\`, \`add\`, \`keys\` recognise
|
|
1075
|
+
the project.
|
|
1076
|
+
· ${chalk.cyan("GitHub remote")} — \`git init\` (if needed),
|
|
1077
|
+
commit, \`gh repo create --private --source=. --push\`. Skipped
|
|
1078
|
+
when an \`origin\` is already set.
|
|
1079
|
+
· ${chalk.cyan("Coolify + DNS")} — direct REST-API calls into the
|
|
1080
|
+
Coolify and Cloudflare you already configured (no Terraform,
|
|
1081
|
+
no submodule). Finds or creates the Coolify project, picks
|
|
1082
|
+
the server (single-server setups auto-resolve), creates the
|
|
1083
|
+
application from the GitHub repo (private repos use a
|
|
1084
|
+
Coolify GitHub App source), pushes the baseline env
|
|
1085
|
+
(DOTENV_PRIVATE_KEY_PRODUCTION + GITHUB_REPO_URL), upserts an
|
|
1086
|
+
A record \`<domain> → <server-ip>\` on Cloudflare, and triggers
|
|
1087
|
+
the first deploy. Defaults ON when no matching app exists.
|
|
1088
|
+
· Optionally provisions GlitchTip / OpenPanel / Resend clients
|
|
1089
|
+
(same machinery as \`hatchkit add\`).
|
|
1090
|
+
· Optionally pushes the dotenvx private key to Coolify
|
|
1091
|
+
(redundant when the Coolify+DNS step ran — it already does).
|
|
1092
|
+
|
|
1093
|
+
${chalk.bold("Limitations:")}
|
|
1094
|
+
· Cloudflare only for DNS automation. INWX / manual users get a
|
|
1095
|
+
"create the A record yourself" hint with the exact target IP.
|
|
1096
|
+
· Doesn't provision new Hetzner servers — the Coolify wiring
|
|
1097
|
+
assumes the server is already in your Coolify dashboard.
|
|
1098
|
+
|
|
1099
|
+
${chalk.bold("When to use:")}
|
|
1100
|
+
The project wasn't created by hatchkit but you want it managed
|
|
1101
|
+
going forward.
|
|
1102
|
+
|
|
1103
|
+
${chalk.bold("Refuses to run twice:")}
|
|
1104
|
+
If \`.hatchkit.json\` already exists, exits with a hint to use
|
|
1105
|
+
\`hatchkit update\` (add features) or \`hatchkit add\` (re-provision
|
|
1106
|
+
clients) instead.
|
|
885
1107
|
`);
|
|
886
1108
|
return;
|
|
887
1109
|
}
|
|
@@ -918,6 +1140,37 @@ function printHelp(topic) {
|
|
|
918
1140
|
hatchkit remove raptor-runner all
|
|
919
1141
|
hatchkit remove raptor-runner glitchtip,resend --dry-run
|
|
920
1142
|
hatchkit remove raptor-runner all --yes
|
|
1143
|
+
`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (topic === "destroy") {
|
|
1147
|
+
console.log(`
|
|
1148
|
+
${chalk.bold("hatchkit destroy")} — undo a project that ${chalk.cyan("hatchkit create")} set up
|
|
1149
|
+
|
|
1150
|
+
${chalk.bold("Usage:")}
|
|
1151
|
+
hatchkit destroy [<project-name>] [--yes] [--recipe]
|
|
1152
|
+
|
|
1153
|
+
Reads the run ledger written by ${chalk.cyan("hatchkit create")} and reverses the
|
|
1154
|
+
recorded steps in reverse order. Destructive operations
|
|
1155
|
+
(rm -rf the local repo, gh repo delete, terraform destroy) prompt
|
|
1156
|
+
per-step unless ${chalk.dim("--yes")} is passed.
|
|
1157
|
+
|
|
1158
|
+
${chalk.bold("What it can undo:")}
|
|
1159
|
+
- local project directory ${chalk.dim("rm -rf")}
|
|
1160
|
+
- GitHub repo ${chalk.dim("gh repo delete")}
|
|
1161
|
+
- GlitchTip project ${chalk.dim("DELETE /api/0/projects")}
|
|
1162
|
+
- generated tfvars + Coolify .env files ${chalk.dim("rm")}
|
|
1163
|
+
- dotenvx private key in keychain ${chalk.dim("keytar deletePassword")}
|
|
1164
|
+
- Terraform-applied resources ${chalk.dim("terraform destroy")}
|
|
1165
|
+
|
|
1166
|
+
${chalk.bold("Options:")}
|
|
1167
|
+
--yes, -y Skip per-step confirmation on destructive operations.
|
|
1168
|
+
--recipe Print the rollback recipe (bash commands) and exit. No execution.
|
|
1169
|
+
|
|
1170
|
+
${chalk.bold("Examples:")}
|
|
1171
|
+
hatchkit destroy ai-playground
|
|
1172
|
+
hatchkit destroy ai-playground --recipe ${chalk.dim("# just print the recipe")}
|
|
1173
|
+
hatchkit destroy ai-playground --yes ${chalk.dim("# no confirmations")}
|
|
921
1174
|
`);
|
|
922
1175
|
return;
|
|
923
1176
|
}
|
|
@@ -1020,9 +1273,11 @@ function printHelp(topic) {
|
|
|
1020
1273
|
|
|
1021
1274
|
${chalk.bold("Projects:")}
|
|
1022
1275
|
create Scaffold a new project (interactive)
|
|
1276
|
+
adopt Bring an existing project under hatchkit management (run in project dir)
|
|
1023
1277
|
update Add features to an already-scaffolded project (run in project dir)
|
|
1024
1278
|
add Create GlitchTip / OpenPanel / Resend clients for an existing project
|
|
1025
1279
|
remove Delete the -dev/-prod clients created by 'add' (inverse of add)
|
|
1280
|
+
destroy Roll back everything ${chalk.cyan("hatchkit create")} did for a project
|
|
1026
1281
|
rename-domain Move a scaffolded project to a new domain (rewrites tfvars/env/manifest)
|
|
1027
1282
|
gh-pages Wire GitHub Pages for the current repo (static / Vite / Jekyll — with DNS)
|
|
1028
1283
|
dns DNS reconciliation helpers (link-to-cloudflare, …)
|