hatchkit 0.1.47 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/adopt.d.ts +61 -1
  2. package/dist/adopt.d.ts.map +1 -1
  3. package/dist/adopt.js +90 -86
  4. package/dist/adopt.js.map +1 -1
  5. package/dist/assets/env.d.ts +2 -2
  6. package/dist/assets/env.d.ts.map +1 -1
  7. package/dist/assets/index.js +11 -11
  8. package/dist/assets/index.js.map +1 -1
  9. package/dist/assets/mirror.js +1 -1
  10. package/dist/completion.d.ts.map +1 -1
  11. package/dist/completion.js +20 -2
  12. package/dist/completion.js.map +1 -1
  13. package/dist/config.d.ts +32 -1
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +364 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/deploy/coolify.d.ts +5 -0
  18. package/dist/deploy/coolify.d.ts.map +1 -1
  19. package/dist/deploy/coolify.js +67 -4
  20. package/dist/deploy/coolify.js.map +1 -1
  21. package/dist/deploy/ghcr.d.ts +1 -0
  22. package/dist/deploy/ghcr.d.ts.map +1 -1
  23. package/dist/deploy/ghcr.js +2 -2
  24. package/dist/deploy/ghcr.js.map +1 -1
  25. package/dist/deploy/github.d.ts.map +1 -1
  26. package/dist/deploy/github.js +3 -2
  27. package/dist/deploy/github.js.map +1 -1
  28. package/dist/deploy/rollback.d.ts.map +1 -1
  29. package/dist/deploy/rollback.js +9 -0
  30. package/dist/deploy/rollback.js.map +1 -1
  31. package/dist/dev-setup.d.ts +13 -5
  32. package/dist/dev-setup.d.ts.map +1 -1
  33. package/dist/dev-setup.js +268 -59
  34. package/dist/dev-setup.js.map +1 -1
  35. package/dist/doctor.d.ts.map +1 -1
  36. package/dist/doctor.js +65 -1
  37. package/dist/doctor.js.map +1 -1
  38. package/dist/email/index.js +5 -5
  39. package/dist/email/index.js.map +1 -1
  40. package/dist/email/setup.d.ts +1 -1
  41. package/dist/email/setup.d.ts.map +1 -1
  42. package/dist/email/setup.js +3 -3
  43. package/dist/email/setup.js.map +1 -1
  44. package/dist/explain.d.ts.map +1 -1
  45. package/dist/explain.js +9 -8
  46. package/dist/explain.js.map +1 -1
  47. package/dist/index.js +523 -91
  48. package/dist/index.js.map +1 -1
  49. package/dist/inventory.d.ts +1 -0
  50. package/dist/inventory.d.ts.map +1 -1
  51. package/dist/inventory.js +2 -0
  52. package/dist/inventory.js.map +1 -1
  53. package/dist/onboarding/plan.d.ts +54 -0
  54. package/dist/onboarding/plan.d.ts.map +1 -0
  55. package/dist/onboarding/plan.js +143 -0
  56. package/dist/onboarding/plan.js.map +1 -0
  57. package/dist/onboarding/review.d.ts +27 -0
  58. package/dist/onboarding/review.d.ts.map +1 -0
  59. package/dist/onboarding/review.js +55 -0
  60. package/dist/onboarding/review.js.map +1 -0
  61. package/dist/prompts.d.ts +13 -0
  62. package/dist/prompts.d.ts.map +1 -1
  63. package/dist/prompts.js +107 -89
  64. package/dist/prompts.js.map +1 -1
  65. package/dist/provision/glitchtip.d.ts +1 -0
  66. package/dist/provision/glitchtip.d.ts.map +1 -1
  67. package/dist/provision/glitchtip.js +16 -0
  68. package/dist/provision/glitchtip.js.map +1 -1
  69. package/dist/provision/index.d.ts +26 -3
  70. package/dist/provision/index.d.ts.map +1 -1
  71. package/dist/provision/index.js +215 -11
  72. package/dist/provision/index.js.map +1 -1
  73. package/dist/provision/openpanel.d.ts +1 -0
  74. package/dist/provision/openpanel.d.ts.map +1 -1
  75. package/dist/provision/openpanel.js +21 -0
  76. package/dist/provision/openpanel.js.map +1 -1
  77. package/dist/provision/plausible.d.ts +11 -0
  78. package/dist/provision/plausible.d.ts.map +1 -0
  79. package/dist/provision/plausible.js +108 -0
  80. package/dist/provision/plausible.js.map +1 -0
  81. package/dist/provision/resend.d.ts +4 -0
  82. package/dist/provision/resend.d.ts.map +1 -1
  83. package/dist/provision/resend.js +11 -6
  84. package/dist/provision/resend.js.map +1 -1
  85. package/dist/provision/search-console.d.ts +17 -0
  86. package/dist/provision/search-console.d.ts.map +1 -0
  87. package/dist/provision/search-console.js +142 -0
  88. package/dist/provision/search-console.js.map +1 -0
  89. package/dist/scaffold/app.d.ts +1 -0
  90. package/dist/scaffold/app.d.ts.map +1 -1
  91. package/dist/scaffold/app.js +6 -3
  92. package/dist/scaffold/app.js.map +1 -1
  93. package/dist/scaffold/infra.js +2 -0
  94. package/dist/scaffold/infra.js.map +1 -1
  95. package/dist/scaffold/manifest.d.ts +18 -2
  96. package/dist/scaffold/manifest.d.ts.map +1 -1
  97. package/dist/scaffold/manifest.js +7 -1
  98. package/dist/scaffold/manifest.js.map +1 -1
  99. package/dist/scaffold/server-add.d.ts +21 -0
  100. package/dist/scaffold/server-add.d.ts.map +1 -0
  101. package/dist/scaffold/server-add.js +275 -0
  102. package/dist/scaffold/server-add.js.map +1 -0
  103. package/dist/scaffold/starter-files.d.ts +3 -3
  104. package/dist/scaffold/starter-files.js +3 -3
  105. package/dist/scaffold/update.d.ts +1 -0
  106. package/dist/scaffold/update.d.ts.map +1 -1
  107. package/dist/scaffold/update.js +8 -5
  108. package/dist/scaffold/update.js.map +1 -1
  109. package/dist/status.d.ts.map +1 -1
  110. package/dist/status.js +27 -1
  111. package/dist/status.js.map +1 -1
  112. package/dist/templates/base/env.example.hbs +3 -0
  113. package/dist/utils/cloudflare-api.d.ts +5 -0
  114. package/dist/utils/cloudflare-api.d.ts.map +1 -1
  115. package/dist/utils/cloudflare-api.js +19 -0
  116. package/dist/utils/cloudflare-api.js.map +1 -1
  117. package/dist/utils/coolify-api.d.ts +3 -2
  118. package/dist/utils/coolify-api.d.ts.map +1 -1
  119. package/dist/utils/coolify-api.js +19 -5
  120. package/dist/utils/coolify-api.js.map +1 -1
  121. package/dist/utils/flags.d.ts.map +1 -1
  122. package/dist/utils/flags.js +16 -0
  123. package/dist/utils/flags.js.map +1 -1
  124. package/dist/utils/run-ledger.d.ts +3 -0
  125. package/dist/utils/run-ledger.d.ts.map +1 -1
  126. package/dist/utils/run-ledger.js.map +1 -1
  127. package/dist/utils/secrets.d.ts +5 -0
  128. package/dist/utils/secrets.d.ts.map +1 -1
  129. package/dist/utils/secrets.js +5 -0
  130. package/dist/utils/secrets.js.map +1 -1
  131. package/package.json +24 -3
package/dist/adopt.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Feature } from "./prompts.js";
1
+ import type { DeploymentMode, Feature, Surface } from "./prompts.js";
2
+ import { type ProvisionService } from "./provision/index.js";
2
3
  import { type ProjectManifest } from "./scaffold/manifest.js";
3
4
  export interface DetectedState {
4
5
  /** Absolute path to the project root. */
@@ -74,9 +75,68 @@ export interface DetectedState {
74
75
  * the user already chose, instead of starting from blank. */
75
76
  existingManifest?: ProjectManifest;
76
77
  }
78
+ type AdoptSurface = Surface;
79
+ type AdoptDeploymentMode = DeploymentMode;
80
+ export interface AdoptPlan {
81
+ name: string;
82
+ domain: string;
83
+ /** One-liner that ends up on the Coolify project + application
84
+ * pages (instead of the generic "Adopted by hatchkit" blurb).
85
+ * Empty string means "no override" — wireProjectIntoCoolify will
86
+ * use the default on first create and leave any existing
87
+ * description untouched on reconcile. */
88
+ description: string;
89
+ features: Feature[];
90
+ /** What kind of project this is — drives where env files go, which
91
+ * Coolify build-pack we ask for, and which surfaces `hatchkit add`
92
+ * provisions clients into. */
93
+ surfaces: AdoptSurface;
94
+ /** Where this project will run. Coolify is the default for full-
95
+ * stack and server-only adopts; gh-pages is only offered when
96
+ * surfaces is client-only (Pages can't host a backend). Switches
97
+ * the second half of executePlan from Coolify wiring to
98
+ * `runPagesSetupProgrammatic`. */
99
+ deploymentMode: AdoptDeploymentMode;
100
+ /** Required when `surfaces !== "client-only"`. */
101
+ serverDir?: string;
102
+ /** Required when `surfaces !== "server-only"`. */
103
+ clientDir?: string;
104
+ /** Always-on side effect — initialize dotenvx encryption if the
105
+ * server dir doesn't already have an encrypted .env.production. */
106
+ bootstrapDotenvx: boolean;
107
+ /** Initialize git + create a GitHub remote (private repo). Skipped
108
+ * silently when a remote already exists. */
109
+ setupGitHub: boolean;
110
+ /** Wire the repo into the user's existing Coolify + DNS via direct
111
+ * API calls (no Terraform / no submodule). Defaults ON when there's
112
+ * no Coolify app match yet. */
113
+ wireCoolify: boolean;
114
+ /** Whether to treat the GitHub repo as private when wiring Coolify.
115
+ * Private → Coolify clones via the configured GitHub App's SSH
116
+ * deploy key. Public → Coolify clones via HTTPS, no auth needed.
117
+ * Defaulted from `gh repo view --json visibility` when the remote
118
+ * exists; defaults to true when adopt is creating a fresh
119
+ * `--private` repo. Picking the wrong value is the #1 cause of
120
+ * "Permission denied (publickey)" deploy failures, hence its own
121
+ * stepper row instead of being silently inferred from setupGitHub. */
122
+ isPrivate: boolean;
123
+ /** Container port the app exposes — defaults to "3000". */
124
+ appPort: string;
125
+ /** Scaffold the build pipeline (Dockerfile + docker-compose.yml +
126
+ * .github/workflows/deploy.yml) when those files don't exist yet.
127
+ * Always coupled with wireCoolify since the compose file is what
128
+ * Coolify reads. The detection step ensures this is a no-op when
129
+ * the user already has their own. */
130
+ scaffoldBuildPipeline: boolean;
131
+ /** Provisioning to run after manifest write. */
132
+ services: ProvisionService[];
133
+ /** Push dotenvx key to Coolify after everything's written. */
134
+ pushKey: boolean;
135
+ }
77
136
  export declare function runAdopt(cwd: string, opts?: {
78
137
  resume?: boolean;
79
138
  regeneratePipeline?: boolean;
80
139
  }): Promise<void>;
81
140
  export declare function detectProject(projectDir: string): Promise<DetectedState>;
141
+ export {};
82
142
  //# sourceMappingURL=adopt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adopt.d.ts","sourceRoot":"","sources":["../src/adopt.ts"],"names":[],"mappings":"AAkDA,OAAO,KAAK,EAAE,OAAO,EAAc,MAAM,cAAc,CAAC;AAIxD,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,wBAAwB,CAAC;AAoBhC,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;iEAE6D;IAC7D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;uEACmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;oCAWgC;IAChC,sBAAsB,EAAE,OAAO,CAAC;IAChC;;;;;qDAKiD;IACjD,yBAAyB,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC/E,yEAAyE;IACzE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;8DAC0D;IAC1D,kBAAkB,EAAE,OAAO,CAAC;IAC5B,4DAA4D;IAC5D,UAAU,EAAE,OAAO,CAAC;IACpB,sCAAsC;IACtC,eAAe,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD;;oCAEgC;IAChC,iBAAiB,EAAE,OAAO,CAAC;IAC3B;;;6BAGyB;IACzB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,0CAA0C;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;sDAGkD;IAClD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;kEAE8D;IAC9D,gBAAgB,CAAC,EAAE,eAAe,CAAC;CACpC;AA+DD,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5D,OAAO,CAAC,IAAI,CAAC,CA6Lf;AAMD,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAkM9E"}
1
+ {"version":3,"file":"adopt.d.ts","sourceRoot":"","sources":["../src/adopt.ts"],"names":[],"mappings":"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"}
package/dist/adopt.js CHANGED
@@ -35,13 +35,15 @@
35
35
  */
36
36
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
37
37
  import { join, relative } from "node:path";
38
- import { Separator, confirm, input, select } from "@inquirer/prompts";
38
+ import { confirm, input, select } from "@inquirer/prompts";
39
39
  import chalk from "chalk";
40
40
  import { ensureGitHub, getCoolifyConfig, getGhcrConfig } from "./config.js";
41
41
  import { ghSecretExists, ownerFromRemote, repoSlugFromRemote, setCoolifyDeploySecrets, } from "./deploy/gh-actions-secrets.js";
42
42
  import { pushInitialBranch } from "./deploy/github.js";
43
43
  import { pushProjectKeyToCoolify, pushProjectKeyToGh } from "./deploy/keys.js";
44
44
  import { handleAdoptFailure } from "./deploy/rollback.js";
45
+ import { adoptPlanToOnboardingPlan, onboardingPlanToAdoptPlan, renderOnboardingDeploymentModeSummary, renderOnboardingSurfaceSummary, summarizeOnboardingDomain, summarizeOnboardingFeatures, } from "./onboarding/plan.js";
46
+ import { runProjectOnboardingReview, } from "./onboarding/review.js";
45
47
  import { runProvision } from "./provision/index.js";
46
48
  import { readEnvKeys } from "./provision/write-env.js";
47
49
  import { detectBuildPipeline, scaffoldBuildPipeline } from "./scaffold/build-pipeline.js";
@@ -152,7 +154,7 @@ export async function runAdopt(cwd, opts = {}) {
152
154
  // the cursor on the row in this case so the choice is explicit.
153
155
  scaffoldBuildPipeline: !state.unknownWorkspaceLayout,
154
156
  // Provisioning is opt-in. Each service mints real resources on a
155
- // third-party (GlitchTip project, OpenPanel project, Resend API
157
+ // third-party (GlitchTip project, OpenPanel project, Plausible site, Resend API
156
158
  // key) and cleaning those up after the fact is a chore — better
157
159
  // to require an explicit tick than to surprise the user with three
158
160
  // new clients they didn't ask for. The user opens the "Provision
@@ -517,7 +519,9 @@ function detectFeatures(projectDir, serverDir) {
517
519
  "@sentry/nextjs" in deps ||
518
520
  "@openpanel/web" in deps ||
519
521
  "@openpanel/sdk" in deps ||
520
- "@openpanel/nextjs" in deps) {
522
+ "@openpanel/nextjs" in deps ||
523
+ "plausible-tracker" in deps ||
524
+ "next-plausible" in deps) {
521
525
  found.add("analytics");
522
526
  }
523
527
  if ("@aws-sdk/client-s3" in deps || "minio" in deps)
@@ -541,7 +545,7 @@ function detectFeatures(projectDir, serverDir) {
541
545
  found.add("stripe");
542
546
  if (/REDIS_URL/.test(text))
543
547
  found.add("websocket");
544
- if (/GLITCHTIP_DSN|SENTRY_DSN|OPENPANEL_/.test(text))
548
+ if (/GLITCHTIP_DSN|SENTRY_DSN|OPENPANEL_|PLAUSIBLE_/.test(text))
545
549
  found.add("analytics");
546
550
  if (/S3_BUCKET|S3_ENDPOINT/.test(text))
547
551
  found.add("s3");
@@ -610,66 +614,46 @@ function printDetected(state) {
610
614
  console.log();
611
615
  }
612
616
  async function reviewLoop(state, initial) {
613
- let plan = initial;
614
- console.log(chalk.dim(" Step through each row to confirm or change. Choose 'Adopt' when ready.\n"));
615
- for (;;) {
616
- const groups = buildAdoptGroups(state, plan);
617
- const allSteps = groups.flatMap((g) => g.steps);
618
- const firstUnset = allSteps.find((s) => !s.set);
619
- const defaultKey = firstUnset?.key ?? "__adopt__";
620
- const choices = [];
621
- for (const group of groups) {
622
- choices.push(new Separator(chalk.bold(`── ${group.title} ──`)));
623
- for (const step of group.steps) {
624
- const mark = step.set ? chalk.green("✓") : chalk.dim("·");
625
- choices.push({
626
- name: `${mark} ${step.label.padEnd(18)}${chalk.dim(` — ${step.summary}`)}`,
627
- value: step.key,
628
- });
629
- }
630
- }
631
- choices.push(new Separator(" "));
632
- choices.push({
633
- name: chalk.bold(chalk.green("✓ Adopt — apply changes")),
634
- value: "__adopt__",
635
- });
636
- choices.push({ name: chalk.dim("✗ Cancel"), value: "__cancel__" });
637
- const picked = await select({
638
- message: "Next step:",
639
- default: defaultKey,
640
- pageSize: Math.min(30, choices.length),
641
- choices,
642
- });
643
- if (picked === "__adopt__")
644
- return plan;
645
- if (picked === "__cancel__") {
646
- console.log(chalk.dim("\n Cancelled. Nothing was changed.\n"));
647
- throw new Error("Adopt cancelled by user");
648
- }
649
- plan = await editAdoptStep(state, plan, picked);
650
- }
617
+ let latestPlan = initial;
618
+ const reviewedPlan = await runProjectOnboardingReview({
619
+ initial: adoptPlanToOnboardingPlan(initial, state),
620
+ intro: chalk.dim(" Step through each row to confirm or change. Choose 'Adopt' when ready.\n"),
621
+ proceedLabel: "Adopt apply changes",
622
+ cancelLabel: "Cancel",
623
+ cancelMessage: chalk.dim("\n Cancelled. Nothing was changed.\n"),
624
+ buildGroups: (plan) => buildAdoptGroups(state, plan, onboardingPlanToAdoptPlan(plan, latestPlan, state)),
625
+ editStep: async (plan, picked) => {
626
+ const currentPlan = onboardingPlanToAdoptPlan(plan, latestPlan, state);
627
+ latestPlan = await editAdoptStep(state, currentPlan, picked);
628
+ return adoptPlanToOnboardingPlan(latestPlan, state);
629
+ },
630
+ });
631
+ return onboardingPlanToAdoptPlan(reviewedPlan, latestPlan, state);
651
632
  }
652
- function buildAdoptGroups(state, plan) {
633
+ function buildAdoptGroups(state, onboarding, plan) {
653
634
  return [
654
635
  {
655
636
  title: "Project",
656
637
  steps: [
657
- { key: "name", label: "Project name", set: !!plan.name, summary: plan.name || "(unset)" },
638
+ {
639
+ key: "name",
640
+ label: "Project name",
641
+ set: !!onboarding.identity.name,
642
+ summary: onboarding.identity.name || "(unset)",
643
+ },
658
644
  {
659
645
  key: "domain",
660
646
  label: "Domain",
661
- set: !!plan.domain,
662
- summary: plan.domain
663
- ? `${plan.domain} ${chalk.dim("→")} https://${plan.domain}`
664
- : "(unset)",
647
+ set: !!onboarding.identity.domain,
648
+ summary: summarizeOnboardingDomain(onboarding),
665
649
  },
666
650
  (() => {
667
651
  // Description is optional — empty is a valid choice that
668
652
  // falls back to the generic "Adopted by hatchkit" blurb.
669
653
  // We always render it as `set: true` so the cursor doesn't
670
654
  // park on it; the user opens it explicitly when they care.
671
- const summary = plan.description
672
- ? truncate(plan.description, 60)
655
+ const summary = onboarding.identity.description
656
+ ? truncate(onboarding.identity.description, 60)
673
657
  : state.packageDescription
674
658
  ? chalk.dim(`(empty — defaults from package.json: "${truncate(state.packageDescription, 40)}")`)
675
659
  : chalk.dim('(empty — defaults to "Adopted by hatchkit")');
@@ -694,11 +678,7 @@ function buildAdoptGroups(state, plan) {
694
678
  const hasManifestSurfaces = !!state.existingManifest?.surfaces;
695
679
  const detectionWasDefinitive = !!(state.serverDir || state.clientDir);
696
680
  const inferred = !hasManifestSurfaces && !detectionWasDefinitive;
697
- const baseSummary = plan.surfaces === "server-only"
698
- ? "server only (backend / API)"
699
- : plan.surfaces === "client-only"
700
- ? "client only (static / SPA — no backend)"
701
- : "server + client";
681
+ const baseSummary = renderOnboardingSurfaceSummary(onboarding.layout.surfaces);
702
682
  return {
703
683
  key: "surfaces",
704
684
  label: "Surfaces",
@@ -710,23 +690,27 @@ function buildAdoptGroups(state, plan) {
710
690
  // the chosen surface. Hiding instead of greying-out keeps the
711
691
  // stepper consistent with the surfaces choice and avoids the
712
692
  // "checkmark on a thing I can't unset" UX trap.
713
- ...(plan.surfaces !== "client-only"
693
+ ...(onboarding.layout.surfaces !== "client-only"
714
694
  ? [
715
695
  {
716
696
  key: "serverDir",
717
697
  label: "Server env dir",
718
- set: !!plan.serverDir,
719
- summary: plan.serverDir ? relativeTo(plan.serverDir) : "(unset)",
698
+ set: !!onboarding.layout.serverDir,
699
+ summary: onboarding.layout.serverDir
700
+ ? relativeTo(onboarding.layout.serverDir)
701
+ : "(unset)",
720
702
  },
721
703
  ]
722
704
  : []),
723
- ...(plan.surfaces !== "server-only"
705
+ ...(onboarding.layout.surfaces !== "server-only"
724
706
  ? [
725
707
  {
726
708
  key: "clientDir",
727
709
  label: "Client env dir",
728
- set: !!plan.clientDir,
729
- summary: plan.clientDir ? relativeTo(plan.clientDir) : "(unset)",
710
+ set: !!onboarding.layout.clientDir,
711
+ summary: onboarding.layout.clientDir
712
+ ? relativeTo(onboarding.layout.clientDir)
713
+ : "(unset)",
730
714
  },
731
715
  ]
732
716
  : []),
@@ -739,7 +723,7 @@ function buildAdoptGroups(state, plan) {
739
723
  key: "features",
740
724
  label: "Features",
741
725
  set: true,
742
- summary: plan.features.length > 0 ? plan.features.join(", ") : chalk.dim("none"),
726
+ summary: summarizeOnboardingFeatures(onboarding.provisioning.features),
743
727
  },
744
728
  ],
745
729
  },
@@ -750,7 +734,7 @@ function buildAdoptGroups(state, plan) {
750
734
  key: "bootstrapDotenvx",
751
735
  label: "Initialize dotenvx",
752
736
  set: true,
753
- summary: plan.bootstrapDotenvx
737
+ summary: onboarding.env.bootstrapDotenvx
754
738
  ? state.prodEnvIsEncrypted
755
739
  ? chalk.dim("already encrypted — will skip")
756
740
  : "yes — generate keypair + encrypt .env.production"
@@ -760,7 +744,7 @@ function buildAdoptGroups(state, plan) {
760
744
  key: "setupGitHub",
761
745
  label: "GitHub remote",
762
746
  set: true,
763
- summary: plan.setupGitHub
747
+ summary: onboarding.repo.setupGitHub
764
748
  ? state.gitRemoteUrl
765
749
  ? chalk.dim("already set — will skip")
766
750
  : "yes — `gh repo create` + push"
@@ -774,7 +758,7 @@ function buildAdoptGroups(state, plan) {
774
758
  // configures the redeploy webhook + builds an image). gh-pages
775
759
  // ships its own workflow that uploads to Pages instead, so we
776
760
  // hide this group when the user picks Pages.
777
- ...(plan.deploymentMode === "coolify"
761
+ ...(onboarding.deployment.mode === "coolify"
778
762
  ? [
779
763
  {
780
764
  title: "Build pipeline",
@@ -801,11 +785,11 @@ function buildAdoptGroups(state, plan) {
801
785
  key: "deploymentMode",
802
786
  label: "Deployment mode",
803
787
  set: true,
804
- summary: renderAdoptDeploymentModeSummary(plan.deploymentMode, plan.surfaces),
788
+ summary: renderOnboardingDeploymentModeSummary(onboarding.deployment.mode, onboarding.layout.surfaces),
805
789
  },
806
790
  // Coolify-specific rows — only shown when actually deploying
807
791
  // to Coolify. gh-pages skips this branch entirely.
808
- ...(plan.deploymentMode === "coolify"
792
+ ...(onboarding.deployment.mode === "coolify"
809
793
  ? [
810
794
  (() => {
811
795
  // Visibility row. Picking the wrong path here is the #1
@@ -861,12 +845,14 @@ function buildAdoptGroups(state, plan) {
861
845
  key: "services",
862
846
  label: "Provision clients",
863
847
  set: true,
864
- summary: plan.services.length > 0 ? plan.services.join(", ") : chalk.dim("skip provisioning"),
848
+ summary: onboarding.provisioning.services.length > 0
849
+ ? onboarding.provisioning.services.join(", ")
850
+ : chalk.dim("skip provisioning"),
865
851
  },
866
852
  // `pushKey` only matters when a Coolify app is the deploy
867
853
  // target — Pages reads no secrets from a Coolify env, so the
868
854
  // row would just be noise on the gh-pages path.
869
- ...(plan.deploymentMode === "coolify"
855
+ ...(onboarding.deployment.mode === "coolify"
870
856
  ? [
871
857
  {
872
858
  key: "pushKey",
@@ -884,18 +870,6 @@ function buildAdoptGroups(state, plan) {
884
870
  },
885
871
  ];
886
872
  }
887
- function renderAdoptDeploymentModeSummary(mode, surfaces) {
888
- switch (mode) {
889
- case "coolify":
890
- return "Coolify (full-stack on Hetzner)";
891
- case "gh-pages":
892
- return surfaces === "client-only"
893
- ? "GitHub Pages (static)"
894
- : chalk.yellow("GitHub Pages — needs surfaces=client-only");
895
- case "scaffold-only":
896
- return "scaffold only (no deploy)";
897
- }
898
- }
899
873
  async function editAdoptStep(state, plan, step) {
900
874
  if (step === "name") {
901
875
  const name = (await input({
@@ -1067,6 +1041,11 @@ async function editAdoptStep(state, plan, step) {
1067
1041
  value: "openpanel",
1068
1042
  checked: plan.services.includes("openpanel"),
1069
1043
  },
1044
+ {
1045
+ name: "Plausible (web analytics)",
1046
+ value: "plausible",
1047
+ checked: plan.services.includes("plausible"),
1048
+ },
1070
1049
  {
1071
1050
  name: "Resend (transactional email)",
1072
1051
  value: "resend",
@@ -1077,6 +1056,11 @@ async function editAdoptStep(state, plan, step) {
1077
1056
  value: "email",
1078
1057
  checked: plan.services.includes("email"),
1079
1058
  },
1059
+ {
1060
+ name: "Google Search Console (DNS verification + domain property)",
1061
+ value: "search-console",
1062
+ checked: plan.services.includes("search-console"),
1063
+ },
1080
1064
  ],
1081
1065
  });
1082
1066
  return { ...plan, services };
@@ -1167,9 +1151,11 @@ async function editAdoptStep(state, plan, step) {
1167
1151
  const RESUME_SERVICE_ENV_KEY = {
1168
1152
  glitchtip: { server: "GLITCHTIP_DSN", client: "PUBLIC_GLITCHTIP_DSN" },
1169
1153
  openpanel: { server: "OPENPANEL_CLIENT_ID", client: "PUBLIC_OPENPANEL_CLIENT_ID" },
1154
+ plausible: { client: "NEXT_PUBLIC_PLAUSIBLE_DOMAIN" },
1170
1155
  resend: { server: "RESEND_API_KEY" },
1171
1156
  s3: { server: "R2_ENDPOINT" },
1172
1157
  email: {},
1158
+ "search-console": {},
1173
1159
  };
1174
1160
  /** Filter the services list for `runProvision` on `--resume`: drop
1175
1161
  * every service whose canonical env keys are already in the target
@@ -1660,12 +1646,14 @@ async function executePlan(state, plan, opts = { resume: false }) {
1660
1646
  username: ghcrConfig?.username,
1661
1647
  });
1662
1648
  if (r.kind === "private-registered") {
1663
- ledger.record({
1664
- kind: "coolifyPrivateRegistry",
1665
- uuid: r.registryUuid,
1666
- });
1649
+ if (r.created) {
1650
+ ledger.record({
1651
+ kind: "coolifyPrivateRegistry",
1652
+ uuid: r.registryUuid,
1653
+ });
1654
+ }
1667
1655
  }
1668
- else if (r.kind !== "public-set") {
1656
+ else if (r.kind === "skipped" || r.kind === "failed") {
1669
1657
  caveats.push({
1670
1658
  title: "GHCR pull credentials not configured",
1671
1659
  reason: r.reason,
@@ -1719,9 +1707,11 @@ async function executePlan(state, plan, opts = { resume: false }) {
1719
1707
  services: resumeServices,
1720
1708
  surfaces: {
1721
1709
  mode: provisionMode,
1710
+ projectDir: state.projectDir,
1722
1711
  serverEnvDir: plan.serverDir,
1723
1712
  clientEnvDir: plan.clientDir,
1724
1713
  },
1714
+ domain: plan.domain,
1725
1715
  // Record per-resource as runProvision creates them. Done via
1726
1716
  // callback so a mid-loop failure (e.g. Resend after GlitchTip
1727
1717
  // already succeeded) still leaves a complete trail of what
@@ -1733,9 +1723,23 @@ async function executePlan(state, plan, opts = { resume: false }) {
1733
1723
  else if (event.service === "openpanel") {
1734
1724
  ledger.record({ kind: "openpanel", project: event.project });
1735
1725
  }
1726
+ else if (event.service === "plausible") {
1727
+ ledger.record({ kind: "plausible", project: event.project });
1728
+ }
1736
1729
  else if (event.service === "resend") {
1737
1730
  ledger.record({ kind: "resend", client: event.client });
1738
1731
  }
1732
+ else if (event.service === "search-console") {
1733
+ if (event.dnsRecord?.created) {
1734
+ ledger.record({
1735
+ kind: "cloudflareDnsRecord",
1736
+ zoneId: event.dnsRecord.zoneId,
1737
+ recordId: event.dnsRecord.id,
1738
+ name: event.dnsRecord.name,
1739
+ type: event.dnsRecord.type,
1740
+ });
1741
+ }
1742
+ }
1739
1743
  else if (event.service === "email") {
1740
1744
  // Email setup creates three kinds of mutable state on
1741
1745
  // Cloudflare: the destination address (account-scoped), the