hatchkit 0.1.4 → 0.1.5
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 +552 -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/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 +356 -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/coolify-api.d.ts +12 -0
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +33 -0
- 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 +56 -98
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,
|
|
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),
|
|
466
514
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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);
|
|
666
|
+
}
|
|
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\`).`));
|
|
672
|
+
}
|
|
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);
|
|
557
681
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
console.log(chalk.dim(` ${mlEnvVarName(svc)}=${defaultUrl}`));
|
|
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}`));
|
|
562
685
|
}
|
|
563
686
|
}
|
|
564
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`));
|
|
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,40 @@ 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, feature flags
|
|
1066
|
+
inferable from deps), then runs a stepper-style review where you
|
|
1067
|
+
confirm or change each detected value. On confirm:
|
|
1068
|
+
|
|
1069
|
+
· If \`.env.production\` isn't dotenvx-encrypted yet, encrypts it
|
|
1070
|
+
(generates \`.env.keys\` with the private key).
|
|
1071
|
+
· Imports DOTENV_PRIVATE_KEY_PRODUCTION into the OS keychain.
|
|
1072
|
+
· Writes \`.hatchkit.json\` so \`update\`, \`add\`, \`keys\` recognise
|
|
1073
|
+
the project.
|
|
1074
|
+
· Optionally provisions GlitchTip / OpenPanel / Resend clients
|
|
1075
|
+
(same machinery as \`hatchkit add\`).
|
|
1076
|
+
· Optionally pushes the dotenvx private key to Coolify.
|
|
1077
|
+
|
|
1078
|
+
${chalk.bold("When to use:")}
|
|
1079
|
+
The project wasn't created by hatchkit but you want it to be
|
|
1080
|
+
managed going forward.
|
|
1081
|
+
|
|
1082
|
+
${chalk.bold("Refuses to run twice:")}
|
|
1083
|
+
If \`.hatchkit.json\` already exists, exits with a hint to use
|
|
1084
|
+
\`hatchkit update\` (add features) or \`hatchkit add\` (re-provision
|
|
1085
|
+
clients) instead.
|
|
885
1086
|
`);
|
|
886
1087
|
return;
|
|
887
1088
|
}
|
|
@@ -918,6 +1119,37 @@ function printHelp(topic) {
|
|
|
918
1119
|
hatchkit remove raptor-runner all
|
|
919
1120
|
hatchkit remove raptor-runner glitchtip,resend --dry-run
|
|
920
1121
|
hatchkit remove raptor-runner all --yes
|
|
1122
|
+
`);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (topic === "destroy") {
|
|
1126
|
+
console.log(`
|
|
1127
|
+
${chalk.bold("hatchkit destroy")} — undo a project that ${chalk.cyan("hatchkit create")} set up
|
|
1128
|
+
|
|
1129
|
+
${chalk.bold("Usage:")}
|
|
1130
|
+
hatchkit destroy [<project-name>] [--yes] [--recipe]
|
|
1131
|
+
|
|
1132
|
+
Reads the run ledger written by ${chalk.cyan("hatchkit create")} and reverses the
|
|
1133
|
+
recorded steps in reverse order. Destructive operations
|
|
1134
|
+
(rm -rf the local repo, gh repo delete, terraform destroy) prompt
|
|
1135
|
+
per-step unless ${chalk.dim("--yes")} is passed.
|
|
1136
|
+
|
|
1137
|
+
${chalk.bold("What it can undo:")}
|
|
1138
|
+
- local project directory ${chalk.dim("rm -rf")}
|
|
1139
|
+
- GitHub repo ${chalk.dim("gh repo delete")}
|
|
1140
|
+
- GlitchTip project ${chalk.dim("DELETE /api/0/projects")}
|
|
1141
|
+
- generated tfvars + Coolify .env files ${chalk.dim("rm")}
|
|
1142
|
+
- dotenvx private key in keychain ${chalk.dim("keytar deletePassword")}
|
|
1143
|
+
- Terraform-applied resources ${chalk.dim("terraform destroy")}
|
|
1144
|
+
|
|
1145
|
+
${chalk.bold("Options:")}
|
|
1146
|
+
--yes, -y Skip per-step confirmation on destructive operations.
|
|
1147
|
+
--recipe Print the rollback recipe (bash commands) and exit. No execution.
|
|
1148
|
+
|
|
1149
|
+
${chalk.bold("Examples:")}
|
|
1150
|
+
hatchkit destroy ai-playground
|
|
1151
|
+
hatchkit destroy ai-playground --recipe ${chalk.dim("# just print the recipe")}
|
|
1152
|
+
hatchkit destroy ai-playground --yes ${chalk.dim("# no confirmations")}
|
|
921
1153
|
`);
|
|
922
1154
|
return;
|
|
923
1155
|
}
|
|
@@ -1020,9 +1252,11 @@ function printHelp(topic) {
|
|
|
1020
1252
|
|
|
1021
1253
|
${chalk.bold("Projects:")}
|
|
1022
1254
|
create Scaffold a new project (interactive)
|
|
1255
|
+
adopt Bring an existing project under hatchkit management (run in project dir)
|
|
1023
1256
|
update Add features to an already-scaffolded project (run in project dir)
|
|
1024
1257
|
add Create GlitchTip / OpenPanel / Resend clients for an existing project
|
|
1025
1258
|
remove Delete the -dev/-prod clients created by 'add' (inverse of add)
|
|
1259
|
+
destroy Roll back everything ${chalk.cyan("hatchkit create")} did for a project
|
|
1026
1260
|
rename-domain Move a scaffolded project to a new domain (rewrites tfvars/env/manifest)
|
|
1027
1261
|
gh-pages Wire GitHub Pages for the current repo (static / Vite / Jekyll — with DNS)
|
|
1028
1262
|
dns DNS reconciliation helpers (link-to-cloudflare, …)
|