hatchkit 0.1.3 → 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/coolify-mongo.d.ts +12 -0
- package/dist/deploy/coolify-mongo.d.ts.map +1 -0
- package/dist/deploy/coolify-mongo.js +109 -0
- package/dist/deploy/coolify-mongo.js.map +1 -0
- package/dist/deploy/gpu.d.ts +9 -2
- package/dist/deploy/gpu.d.ts.map +1 -1
- package/dist/deploy/gpu.js +63 -39
- package/dist/deploy/gpu.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 +351 -88
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +15 -2
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +334 -17
- 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/app.js +8 -0
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/dotenvx.d.ts.map +1 -1
- package/dist/scaffold/dotenvx.js +41 -10
- 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/scaffold/manifest.d.ts +4 -2
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +1 -1
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/ml-client.d.ts +9 -2
- package/dist/scaffold/ml-client.d.ts.map +1 -1
- package/dist/scaffold/ml-client.js +11 -1
- package/dist/scaffold/ml-client.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/templates/base/env.example.hbs +10 -0
- package/dist/templates/base/src/config.ts.hbs +24 -4
- package/dist/utils/coolify-api.d.ts +56 -0
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +79 -0
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/flags.d.ts.map +1 -1
- package/dist/utils/flags.js +4 -0
- package/dist/utils/flags.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 +6 -5
- package/scripts/release-prep.mjs +88 -0
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,100 +492,208 @@ 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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
504
|
+
let installedDeps = false;
|
|
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),
|
|
514
|
+
});
|
|
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 });
|
|
537
|
+
}
|
|
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\`.`));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
440
581
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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."));
|
|
596
|
+
}
|
|
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
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
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 });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Step 4: Generate infra configs (with repo URL + ports baked in).
|
|
633
|
+
const infraResult = scaffoldInfra(config, INFRA_ROOT, {
|
|
634
|
+
repoUrl: repoUrl ?? undefined,
|
|
444
635
|
serverPort: scaffoldResult?.ports.server,
|
|
445
636
|
clientPort: scaffoldResult?.ports.client,
|
|
446
637
|
});
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
let installedDeps = false;
|
|
453
|
-
if (config.scaffoldRepo) {
|
|
454
|
-
const hasPnpm = await execOk("pnpm", ["--version"]);
|
|
455
|
-
if (!hasPnpm) {
|
|
456
|
-
console.log(chalk.yellow(" pnpm not found on PATH — skipping install step. Install deps with your preferred tool once available."));
|
|
638
|
+
if (infraResult.tfvarsPath) {
|
|
639
|
+
ledger?.record({ kind: "tfvars", path: infraResult.tfvarsPath });
|
|
457
640
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
cwd: appDir,
|
|
470
|
-
spinner: "Installing dependencies...",
|
|
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,
|
|
471
652
|
});
|
|
472
|
-
if (res.exitCode === 0) {
|
|
473
|
-
installedDeps = true;
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
console.log(chalk.yellow(" pnpm install failed — continuing anyway."));
|
|
477
|
-
}
|
|
478
653
|
}
|
|
479
654
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (config.runDeployment) {
|
|
499
|
-
await runCoolifySetup(config, INFRA_ROOT);
|
|
500
|
-
// Push the dotenvx private key to Coolify so the starter's server
|
|
501
|
-
// can decrypt .env.production at runtime. Best-effort — if the
|
|
502
|
-
// Coolify app doesn't exist yet (race with the stack script), we
|
|
503
|
-
// print the manual command instead of failing the whole flow.
|
|
504
|
-
if (scaffoldResult?.dotenvx) {
|
|
505
|
-
try {
|
|
506
|
-
await pushProjectKeyToCoolify(config.name);
|
|
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
|
+
}
|
|
507
673
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
+
}
|
|
511
686
|
}
|
|
512
687
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
// cast is sound, but narrow via the literal-array check so a
|
|
523
|
-
// stray unknown slips don't silently format wrong.
|
|
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");
|
|
524
697
|
const knownServices = [
|
|
525
698
|
"3d-extraction",
|
|
526
699
|
"subtitles",
|
|
@@ -528,16 +701,41 @@ async function handleCreate() {
|
|
|
528
701
|
"background-removal",
|
|
529
702
|
"custom-hf",
|
|
530
703
|
];
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
}
|
|
533
721
|
}
|
|
534
722
|
}
|
|
535
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);
|
|
536
734
|
}
|
|
537
735
|
// Final summary
|
|
538
736
|
console.log(chalk.bold("\n ── Done! ─────────────────────────────────────────────────\n"));
|
|
539
737
|
console.log(` App: ${chalk.cyan(`https://${config.domain}`)}`);
|
|
540
|
-
console.log(` API: ${chalk.cyan(`https
|
|
738
|
+
console.log(` API: ${chalk.cyan(`https://${config.domain}/api`)}`);
|
|
541
739
|
console.log(` App dir: ${chalk.dim(appDir)}`);
|
|
542
740
|
console.log(` Config: ${chalk.dim(getConfigPath())}`);
|
|
543
741
|
if (config.scaffoldRepo) {
|
|
@@ -548,9 +746,6 @@ async function handleCreate() {
|
|
|
548
746
|
console.log(chalk.yellow(`\n Next: cd ${config.name} && pnpm install && pnpm dev`));
|
|
549
747
|
}
|
|
550
748
|
}
|
|
551
|
-
if (config.features.includes("stripe")) {
|
|
552
|
-
console.log(chalk.yellow("\n Next: Set STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET in Coolify"));
|
|
553
|
-
}
|
|
554
749
|
if (config.features.includes("mobile")) {
|
|
555
750
|
console.log(chalk.yellow("\n Next (mobile): generate native projects once:"));
|
|
556
751
|
console.log(chalk.dim(" pnpm cap:add:ios # requires Xcode"));
|
|
@@ -599,6 +794,7 @@ async function handleConfig() {
|
|
|
599
794
|
case "glitchtip":
|
|
600
795
|
case "openpanel":
|
|
601
796
|
case "resend":
|
|
797
|
+
case "stripe":
|
|
602
798
|
await reconfigureProvider(provider);
|
|
603
799
|
break;
|
|
604
800
|
case "s3": {
|
|
@@ -853,6 +1049,40 @@ function printHelp(topic) {
|
|
|
853
1049
|
hatchkit add raptor-runner all --surfaces=shared \\
|
|
854
1050
|
--server-dir ./raptor-runner/packages/server \\
|
|
855
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.
|
|
856
1086
|
`);
|
|
857
1087
|
return;
|
|
858
1088
|
}
|
|
@@ -889,6 +1119,37 @@ function printHelp(topic) {
|
|
|
889
1119
|
hatchkit remove raptor-runner all
|
|
890
1120
|
hatchkit remove raptor-runner glitchtip,resend --dry-run
|
|
891
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")}
|
|
892
1153
|
`);
|
|
893
1154
|
return;
|
|
894
1155
|
}
|
|
@@ -991,9 +1252,11 @@ function printHelp(topic) {
|
|
|
991
1252
|
|
|
992
1253
|
${chalk.bold("Projects:")}
|
|
993
1254
|
create Scaffold a new project (interactive)
|
|
1255
|
+
adopt Bring an existing project under hatchkit management (run in project dir)
|
|
994
1256
|
update Add features to an already-scaffolded project (run in project dir)
|
|
995
1257
|
add Create GlitchTip / OpenPanel / Resend clients for an existing project
|
|
996
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
|
|
997
1260
|
rename-domain Move a scaffolded project to a new domain (rewrites tfvars/env/manifest)
|
|
998
1261
|
gh-pages Wire GitHub Pages for the current repo (static / Vite / Jekyll — with DNS)
|
|
999
1262
|
dns DNS reconciliation helpers (link-to-cloudflare, …)
|