hatchkit 0.1.47 → 0.2.1

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 (112) 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/completion.d.ts.map +1 -1
  6. package/dist/completion.js +19 -1
  7. package/dist/completion.js.map +1 -1
  8. package/dist/config.d.ts +32 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +364 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/deploy/coolify.d.ts +5 -0
  13. package/dist/deploy/coolify.d.ts.map +1 -1
  14. package/dist/deploy/coolify.js +67 -4
  15. package/dist/deploy/coolify.js.map +1 -1
  16. package/dist/deploy/ghcr.d.ts +1 -0
  17. package/dist/deploy/ghcr.d.ts.map +1 -1
  18. package/dist/deploy/ghcr.js +2 -2
  19. package/dist/deploy/ghcr.js.map +1 -1
  20. package/dist/deploy/github.d.ts.map +1 -1
  21. package/dist/deploy/github.js +3 -2
  22. package/dist/deploy/github.js.map +1 -1
  23. package/dist/deploy/rollback.d.ts.map +1 -1
  24. package/dist/deploy/rollback.js +9 -0
  25. package/dist/deploy/rollback.js.map +1 -1
  26. package/dist/dev-setup.d.ts +10 -4
  27. package/dist/dev-setup.d.ts.map +1 -1
  28. package/dist/dev-setup.js +166 -57
  29. package/dist/dev-setup.js.map +1 -1
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/doctor.js +65 -1
  32. package/dist/doctor.js.map +1 -1
  33. package/dist/email/index.js +5 -5
  34. package/dist/email/index.js.map +1 -1
  35. package/dist/email/setup.d.ts +1 -1
  36. package/dist/email/setup.d.ts.map +1 -1
  37. package/dist/email/setup.js +3 -3
  38. package/dist/email/setup.js.map +1 -1
  39. package/dist/explain.d.ts.map +1 -1
  40. package/dist/explain.js +8 -7
  41. package/dist/explain.js.map +1 -1
  42. package/dist/index.js +277 -60
  43. package/dist/index.js.map +1 -1
  44. package/dist/inventory.d.ts +1 -0
  45. package/dist/inventory.d.ts.map +1 -1
  46. package/dist/inventory.js +2 -0
  47. package/dist/inventory.js.map +1 -1
  48. package/dist/onboarding/plan.d.ts +54 -0
  49. package/dist/onboarding/plan.d.ts.map +1 -0
  50. package/dist/onboarding/plan.js +143 -0
  51. package/dist/onboarding/plan.js.map +1 -0
  52. package/dist/onboarding/review.d.ts +27 -0
  53. package/dist/onboarding/review.d.ts.map +1 -0
  54. package/dist/onboarding/review.js +55 -0
  55. package/dist/onboarding/review.js.map +1 -0
  56. package/dist/prompts.d.ts +13 -0
  57. package/dist/prompts.d.ts.map +1 -1
  58. package/dist/prompts.js +107 -89
  59. package/dist/prompts.js.map +1 -1
  60. package/dist/provision/index.d.ts +21 -3
  61. package/dist/provision/index.d.ts.map +1 -1
  62. package/dist/provision/index.js +112 -5
  63. package/dist/provision/index.js.map +1 -1
  64. package/dist/provision/plausible.d.ts +10 -0
  65. package/dist/provision/plausible.d.ts.map +1 -0
  66. package/dist/provision/plausible.js +103 -0
  67. package/dist/provision/plausible.js.map +1 -0
  68. package/dist/provision/search-console.d.ts +17 -0
  69. package/dist/provision/search-console.d.ts.map +1 -0
  70. package/dist/provision/search-console.js +142 -0
  71. package/dist/provision/search-console.js.map +1 -0
  72. package/dist/scaffold/app.d.ts +1 -0
  73. package/dist/scaffold/app.d.ts.map +1 -1
  74. package/dist/scaffold/app.js +4 -1
  75. package/dist/scaffold/app.js.map +1 -1
  76. package/dist/scaffold/infra.js +2 -0
  77. package/dist/scaffold/infra.js.map +1 -1
  78. package/dist/scaffold/manifest.d.ts +4 -2
  79. package/dist/scaffold/manifest.d.ts.map +1 -1
  80. package/dist/scaffold/manifest.js +7 -1
  81. package/dist/scaffold/manifest.js.map +1 -1
  82. package/dist/scaffold/server-add.d.ts +21 -0
  83. package/dist/scaffold/server-add.d.ts.map +1 -0
  84. package/dist/scaffold/server-add.js +273 -0
  85. package/dist/scaffold/server-add.js.map +1 -0
  86. package/dist/scaffold/update.d.ts +1 -0
  87. package/dist/scaffold/update.d.ts.map +1 -1
  88. package/dist/scaffold/update.js +8 -5
  89. package/dist/scaffold/update.js.map +1 -1
  90. package/dist/status.d.ts.map +1 -1
  91. package/dist/status.js +27 -1
  92. package/dist/status.js.map +1 -1
  93. package/dist/templates/base/env.example.hbs +3 -0
  94. package/dist/utils/cloudflare-api.d.ts +5 -0
  95. package/dist/utils/cloudflare-api.d.ts.map +1 -1
  96. package/dist/utils/cloudflare-api.js +19 -0
  97. package/dist/utils/cloudflare-api.js.map +1 -1
  98. package/dist/utils/coolify-api.d.ts +3 -2
  99. package/dist/utils/coolify-api.d.ts.map +1 -1
  100. package/dist/utils/coolify-api.js +19 -5
  101. package/dist/utils/coolify-api.js.map +1 -1
  102. package/dist/utils/flags.d.ts.map +1 -1
  103. package/dist/utils/flags.js +16 -0
  104. package/dist/utils/flags.js.map +1 -1
  105. package/dist/utils/run-ledger.d.ts +3 -0
  106. package/dist/utils/run-ledger.d.ts.map +1 -1
  107. package/dist/utils/run-ledger.js.map +1 -1
  108. package/dist/utils/secrets.d.ts +5 -0
  109. package/dist/utils/secrets.d.ts.map +1 -1
  110. package/dist/utils/secrets.js +5 -0
  111. package/dist/utils/secrets.js.map +1 -1
  112. package/package.json +24 -3
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { confirm } from "@inquirer/prompts";
5
5
  import chalk from "chalk";
6
- import { ensureCoolify, ensureGitHub, ensureHetzner, ensureS3, getConfig, getConfigPath, getMlServices, isFirstRun, reconfigureProvider, resetConfig, runOnboarding, } from "./config.js";
6
+ import { ensureCoolify, ensureGitHub, ensureHetzner, ensureS3, getConfig, getConfigPath, getCoolifyConfig, getGhcrConfig, getMlServices, isFirstRun, reconfigureProvider, resetConfig, runOnboarding, } from "./config.js";
7
7
  import { runCoolifySetup } from "./deploy/coolify.js";
8
8
  import { setupGitHub } from "./deploy/github.js";
9
9
  import { deployMlServices } from "./deploy/gpu.js";
@@ -108,6 +108,11 @@ async function main() {
108
108
  return printHelp("update");
109
109
  await handleUpdate();
110
110
  break;
111
+ case "server":
112
+ if (args.includes("--help") && args.length === 2)
113
+ return printHelp("server");
114
+ await handleServer();
115
+ break;
111
116
  case "keys":
112
117
  if (args.includes("--help") && args.length === 2)
113
118
  return printHelp("keys");
@@ -456,7 +461,15 @@ async function handleAdd() {
456
461
  const positional = args.slice(1).filter((a) => !a.startsWith("--"));
457
462
  let baseName = positional[0];
458
463
  const rawService = positional[1];
459
- const allServices = ["glitchtip", "openpanel", "resend", "s3", "email"];
464
+ const allServices = [
465
+ "glitchtip",
466
+ "openpanel",
467
+ "plausible",
468
+ "resend",
469
+ "s3",
470
+ "email",
471
+ "search-console",
472
+ ];
460
473
  if (!baseName) {
461
474
  const { input } = await import("@inquirer/prompts");
462
475
  const { validateProjectName } = await import("./utils/validate.js");
@@ -473,6 +486,7 @@ async function handleAdd() {
473
486
  choices: [
474
487
  { name: "GlitchTip (error tracking)", value: "glitchtip", checked: true },
475
488
  { name: "OpenPanel (product analytics)", value: "openpanel", checked: true },
489
+ { name: "Plausible (web analytics)", value: "plausible", checked: false },
476
490
  { name: "Resend (transactional email)", value: "resend", checked: true },
477
491
  {
478
492
  name: "S3 / R2 (per-bucket scoped credentials from .hatchkit.json)",
@@ -484,6 +498,11 @@ async function handleAdd() {
484
498
  value: "email",
485
499
  checked: false,
486
500
  },
501
+ {
502
+ name: "Google Search Console (DNS verification + domain property)",
503
+ value: "search-console",
504
+ checked: false,
505
+ },
487
506
  ],
488
507
  required: true,
489
508
  });
@@ -503,7 +522,7 @@ async function handleAdd() {
503
522
  }
504
523
  // Flag parsing:
505
524
  // --no-write → never write; print a cache summary only
506
- // --enable-dev-obs → also populate .env.development with GlitchTip/OpenPanel creds
525
+ // --enable-dev-obs → also populate .env.development with observability creds
507
526
  // --surfaces=<shared|separate|server-only|client-only>
508
527
  // --server-dir <path> → absolute or project-relative env dir for the server
509
528
  // --client-dir <path> → same for the client
@@ -783,7 +802,15 @@ async function handleRemove() {
783
802
  const skipConfirm = args.includes("--yes") || args.includes("-y");
784
803
  let baseName = positional[0];
785
804
  const rawService = positional[1];
786
- const allServices = ["glitchtip", "openpanel", "resend", "s3", "email"];
805
+ const allServices = [
806
+ "glitchtip",
807
+ "openpanel",
808
+ "plausible",
809
+ "resend",
810
+ "s3",
811
+ "email",
812
+ "search-console",
813
+ ];
787
814
  if (!baseName) {
788
815
  const { input } = await import("@inquirer/prompts");
789
816
  const { validateProjectName } = await import("./utils/validate.js");
@@ -800,6 +827,7 @@ async function handleRemove() {
800
827
  choices: [
801
828
  { name: "GlitchTip (deletes the project)", value: "glitchtip", checked: true },
802
829
  { name: "OpenPanel (deletes the project)", value: "openpanel", checked: true },
830
+ { name: "Plausible (deletes the site)", value: "plausible", checked: false },
803
831
  { name: "Resend (deletes the API key)", value: "resend", checked: true },
804
832
  { name: "S3 / R2 (deletes per-bucket scoped tokens)", value: "s3", checked: false },
805
833
  {
@@ -807,6 +835,11 @@ async function handleRemove() {
807
835
  value: "email",
808
836
  checked: false,
809
837
  },
838
+ {
839
+ name: "Google Search Console (removes property; keeps verification token)",
840
+ value: "search-console",
841
+ checked: false,
842
+ },
810
843
  ],
811
844
  required: true,
812
845
  });
@@ -841,7 +874,7 @@ async function handleRemove() {
841
874
  // project directory if it exists; the s3 unprovision falls back to
842
875
  // a keychain sweep when the manifest can't be found.
843
876
  let projectDir;
844
- if (services.includes("s3")) {
877
+ if (services.includes("s3") || services.includes("search-console")) {
845
878
  const guess = resolve(baseName);
846
879
  if (existsSync(join(guess, ".hatchkit.json"))) {
847
880
  projectDir = guess;
@@ -867,6 +900,52 @@ async function handleDns() {
867
900
  printHelp("dns");
868
901
  }
869
902
  }
903
+ async function configureGhcrForCreate(repoUrl, isPrivateRepo, ledger) {
904
+ const { repoSlugFromRemote } = await import("./deploy/gh-actions-secrets.js");
905
+ const slug = repoSlugFromRemote(repoUrl);
906
+ if (!slug) {
907
+ console.log(chalk.dim(" · Couldn't resolve owner/repo from GitHub URL — skipping GHCR pull setup."));
908
+ return;
909
+ }
910
+ const coolify = await getCoolifyConfig();
911
+ if (!coolify) {
912
+ console.log(chalk.dim(" · Coolify not configured — skipping GHCR pull setup."));
913
+ return;
914
+ }
915
+ const { CoolifyApi } = await import("./utils/coolify-api.js");
916
+ const { makeGhcrPackagePublic, registerGhcrCredsWithCoolify } = await import("./deploy/ghcr.js");
917
+ if (!isPrivateRepo) {
918
+ const result = await makeGhcrPackagePublic({ repoSlug: slug });
919
+ if (result.kind === "public-set")
920
+ return;
921
+ if (result.kind === "skipped" || result.kind === "failed") {
922
+ console.log(chalk.yellow(` GHCR public-image setup skipped: ${result.reason}`));
923
+ console.log(chalk.dim(result.recovery.map((line) => ` ${line}`).join("\n")));
924
+ }
925
+ return;
926
+ }
927
+ const ghcrConfig = await getGhcrConfig();
928
+ const api = new CoolifyApi({ url: coolify.url, token: coolify.token });
929
+ const result = await registerGhcrCredsWithCoolify({
930
+ api,
931
+ repoSlug: slug,
932
+ pullToken: ghcrConfig?.pullToken,
933
+ username: ghcrConfig?.username,
934
+ });
935
+ if (result.kind === "private-registered") {
936
+ if (result.created) {
937
+ ledger?.record({ kind: "coolifyPrivateRegistry", uuid: result.registryUuid });
938
+ }
939
+ return;
940
+ }
941
+ if (result.kind === "skipped" || result.kind === "failed") {
942
+ console.log(chalk.yellow(` GHCR private-image pull setup skipped: ${result.reason}`));
943
+ console.log(chalk.dim(result.recovery.map((line) => ` ${line}`).join("\n")));
944
+ }
945
+ }
946
+ function isCreatedGithubRepoPrivate(config) {
947
+ return config.createGithubRepo && (config.githubRepoVisibility ?? "private") === "private";
948
+ }
870
949
  // ---------------------------------------------------------------------------
871
950
  // Commands
872
951
  // ---------------------------------------------------------------------------
@@ -875,20 +954,22 @@ async function handleCreate() {
875
954
  // the flow non-interactive; otherwise we still prompt for anything
876
955
  // not supplied via flags / config file.
877
956
  const flags = parseCreateFlags(args);
878
- const { yes: nonInteractive, dryRun, presets, forceNoGithub, forceNoDeploy, forceNoInstall, } = flags;
957
+ const { yes: nonInteractive, dryRun, presets, forceNoGithub, forceNoDeploy, forceNoInstall, forceNoLocalDev, } = flags;
879
958
  // Check if first run (skip onboarding when non-interactive — the
880
959
  // onboarding prompts would stall automation).
881
960
  if (!nonInteractive && (await isFirstRun())) {
882
961
  await runOnboarding();
883
962
  }
884
963
  // Collect project config via interactive prompts (or presets).
885
- const config = await collectProjectConfig({ dryRun, presets, nonInteractive });
964
+ const config = await collectProjectConfig({ dryRun, presets, nonInteractive, forceNoLocalDev });
886
965
  if (forceNoGithub)
887
966
  config.createGithubRepo = false;
888
967
  if (forceNoDeploy)
889
968
  config.runDeployment = false;
890
969
  if (forceNoInstall)
891
970
  config.installDeps = false;
971
+ if (forceNoLocalDev)
972
+ config.localDev = undefined;
892
973
  // Ensure needed providers are configured (lazy prompting).
893
974
  // Coolify + Hetzner only matter for the coolify deployment mode.
894
975
  // gh-pages skips them entirely (no server, no Docker registry).
@@ -917,13 +998,19 @@ async function handleCreate() {
917
998
  await ensureS3(config.s3Provider);
918
999
  }
919
1000
  }
920
- // Pre-flight observability + email + Stripe providers used by `hatchkit
921
- // create` directly (not just `add`): if the user picked the analytics
922
- // feature, GlitchTip needs to be configured before we can mint a DSN
923
- // for them. Same for Stripe webhook auto-provisioning.
1001
+ // Pre-flight observability + Stripe providers used by `hatchkit
1002
+ // create` directly (not just `add`): if the user picked analytics
1003
+ // providers, make sure they are configured before we can mint
1004
+ // project-scoped resources. Same for Stripe webhook auto-provisioning.
924
1005
  if (config.features.includes("analytics")) {
925
- const { ensureGlitchtip } = await import("./config.js");
926
- await ensureGlitchtip();
1006
+ const providers = config.analyticsProviders ?? ["glitchtip"];
1007
+ const { ensureGlitchtip, ensureOpenpanel, ensurePlausible } = await import("./config.js");
1008
+ if (providers.includes("glitchtip"))
1009
+ await ensureGlitchtip();
1010
+ if (providers.includes("openpanel"))
1011
+ await ensureOpenpanel();
1012
+ if (providers.includes("plausible"))
1013
+ await ensurePlausible();
927
1014
  }
928
1015
  if (config.features.includes("stripe")) {
929
1016
  const { ensureStripe } = await import("./config.js");
@@ -954,7 +1041,7 @@ async function handleCreate() {
954
1041
  console.log(` Features: ${config.features.length > 0 ? config.features.join(", ") : "none"}`);
955
1042
  console.log(` ML: ${config.mlServices.length > 0 ? config.mlServices.join(", ") : "none"}`);
956
1043
  console.log(` Scaffold: ${config.scaffoldRepo ? "yes" : "no"}`);
957
- console.log(` GitHub: ${config.createGithubRepo ? "yes" : "no"}`);
1044
+ console.log(` GitHub: ${config.createGithubRepo ? `yes (${config.githubRepoVisibility ?? "private"})` : "no"}`);
958
1045
  console.log(` Install: ${config.installDeps ? "yes (pnpm install)" : "no"}`);
959
1046
  console.log(` Deploy now: ${config.runDeployment ? "yes" : "no"}`);
960
1047
  if (config.dryRun) {
@@ -996,28 +1083,47 @@ async function handleCreate() {
996
1083
  const { printDotenvxSummary } = await import("./scaffold/dotenvx.js");
997
1084
  printDotenvxSummary(scaffoldResult.dotenvx, config.name);
998
1085
  }
999
- // Auto-provision GlitchTip + write its DSN encrypted into
1000
- // .env.production. The user picked the `analytics` feature; we
1001
- // already verified GlitchTip is configured during pre-flight.
1002
- // Skipped for client-only — the encrypt target lives in
1003
- // packages/server/, which doesn't exist post-prune. The client
1004
- // side of analytics (OpenPanel via NEXT_PUBLIC_*) still works
1005
- // without any provisioning.
1006
- if (config.features.includes("analytics") && config.surfaces !== "client-only") {
1086
+ // Auto-provision selected observability/analytics providers
1087
+ // through the same machinery used by `hatchkit add`, so create,
1088
+ // adopt, and existing-project provisioning stay aligned.
1089
+ if (config.features.includes("analytics")) {
1090
+ const analyticsServices = [
1091
+ ...(config.analyticsProviders ?? ["glitchtip"]),
1092
+ ];
1007
1093
  try {
1008
- const { provisionGlitchtipClient } = await import("./provision/glitchtip.js");
1009
- const { set: dotenvxSet } = await import("@dotenvx/dotenvx");
1010
- const ora = (await import("ora")).default;
1011
- const spinner = ora(`GlitchTip: creating project ${config.name}`).start();
1012
- const res = await provisionGlitchtipClient(config.name);
1013
- ledger?.record({ kind: "glitchtip", project: config.name });
1014
- spinner.succeed(`GlitchTip project ready (DSN encrypted into .env.production)`);
1015
- const prodEnvPath = join(appDir, "packages/server/.env.production");
1016
- dotenvxSet("GLITCHTIP_DSN", res.dsn, { path: prodEnvPath, encrypt: true });
1094
+ if (analyticsServices.length > 0) {
1095
+ const provisionMode = config.surfaces === "both"
1096
+ ? "shared"
1097
+ : config.surfaces === "server-only"
1098
+ ? "server-only"
1099
+ : "client-only";
1100
+ await runProvision({
1101
+ baseName: config.name,
1102
+ services: analyticsServices,
1103
+ domain: config.domain,
1104
+ surfaces: {
1105
+ mode: provisionMode,
1106
+ projectDir: appDir,
1107
+ serverEnvDir: config.surfaces === "client-only" ? undefined : join(appDir, "packages/server"),
1108
+ clientEnvDir: config.surfaces === "server-only" ? undefined : join(appDir, "packages/client"),
1109
+ },
1110
+ onProvisioned: (event) => {
1111
+ if (event.service === "glitchtip") {
1112
+ ledger?.record({ kind: "glitchtip", project: event.project });
1113
+ }
1114
+ else if (event.service === "openpanel") {
1115
+ ledger?.record({ kind: "openpanel", project: event.project });
1116
+ }
1117
+ else if (event.service === "plausible") {
1118
+ ledger?.record({ kind: "plausible", project: event.project });
1119
+ }
1120
+ },
1121
+ });
1122
+ }
1017
1123
  }
1018
1124
  catch (err) {
1019
- console.log(chalk.yellow(` Couldn't auto-provision GlitchTip: ${err.message}`));
1020
- console.log(chalk.dim(` Run \`hatchkit add ${config.name} glitchtip\` once GlitchTip is reachable.`));
1125
+ console.log(chalk.yellow(` Couldn't auto-provision analytics: ${err.message}`));
1126
+ console.log(chalk.dim(` Run \`hatchkit add ${config.name} ${analyticsServices.join(",")}\` once providers are reachable.`));
1021
1127
  }
1022
1128
  }
1023
1129
  // Stripe: walk the user through pasting per-project keys (sk + pk
@@ -1170,6 +1276,7 @@ async function handleCreate() {
1170
1276
  repoUrl: repoUrl ?? undefined,
1171
1277
  serverPort: scaffoldResult?.ports.server,
1172
1278
  clientPort: scaffoldResult?.ports.client,
1279
+ isPrivateRepo: isCreatedGithubRepoPrivate(config),
1173
1280
  });
1174
1281
  // Order matters: rollback iterates the ledger in REVERSE, so we
1175
1282
  // record parent-before-child (project before app). Otherwise
@@ -1223,18 +1330,26 @@ async function handleCreate() {
1223
1330
  if (repoUrl && config.scaffoldRepo) {
1224
1331
  try {
1225
1332
  const { findCoolifyAppsForProject } = await import("./deploy/coolify-app.js");
1226
- const { repoSlugFromRemote, setCoolifyDeploySecrets } = await import("./deploy/gh-actions-secrets.js");
1333
+ const { ghSecretExists, repoSlugFromRemote, setCoolifyDeploySecrets } = await import("./deploy/gh-actions-secrets.js");
1227
1334
  const slug = repoSlugFromRemote(repoUrl);
1228
1335
  const apps = await findCoolifyAppsForProject(config.name);
1229
- if (slug && apps.length > 0) {
1230
- await setCoolifyDeploySecrets({
1231
- projectDir: appDir,
1232
- repoSlug: slug,
1233
- apps,
1234
- });
1235
- }
1236
- else if (apps.length === 0) {
1237
- console.log(chalk.dim(` · No Coolify app named "${config.name}" / "${config.name}-server" / "${config.name}-client" / "${config.name}-web" found — skipping Actions secret push.`));
1336
+ if (slug) {
1337
+ if (apps.length > 0) {
1338
+ await setCoolifyDeploySecrets({
1339
+ projectDir: appDir,
1340
+ repoSlug: slug,
1341
+ apps,
1342
+ });
1343
+ }
1344
+ else {
1345
+ console.log(chalk.dim(` · No Coolify app named "${config.name}" / "${config.name}-server" / "${config.name}-client" / "${config.name}-web" found — skipping Coolify deploy secret push.`));
1346
+ }
1347
+ const secretName = "DOTENV_PRIVATE_KEY_PRODUCTION";
1348
+ const preExisted = await ghSecretExists(appDir, slug, secretName);
1349
+ await pushProjectKeyToGh(config.name, slug);
1350
+ if (!preExisted) {
1351
+ ledger?.record({ kind: "ghActionsSecret", repo: slug, name: secretName });
1352
+ }
1238
1353
  }
1239
1354
  }
1240
1355
  catch (err) {
@@ -1305,7 +1420,10 @@ async function handleCreate() {
1305
1420
  // created the repo + `origin` but deliberately skipped the push.
1306
1421
  if (config.scaffoldRepo && config.createGithubRepo && repoUrl) {
1307
1422
  const { pushInitialBranch } = await import("./deploy/github.js");
1308
- await pushInitialBranch(appDir);
1423
+ const pushed = await pushInitialBranch(appDir);
1424
+ if (pushed && config.deploymentMode === "coolify") {
1425
+ await configureGhcrForCreate(repoUrl, isCreatedGithubRepoPrivate(config), ledger);
1426
+ }
1309
1427
  }
1310
1428
  // Step 6.6: optional email forwarding setup (Cloudflare Email
1311
1429
  // Routing). Opt-in prompt — most projects want it but a scripted
@@ -1470,6 +1588,48 @@ async function handleUpdate() {
1470
1588
  console.log(chalk.yellow(" Run `pnpm install` to pick up @hatchkit/dev-plugin-next, then `hatchkit doctor` to confirm host plumbing."));
1471
1589
  }
1472
1590
  }
1591
+ async function handleServer() {
1592
+ const sub = args[1];
1593
+ if (sub !== "add") {
1594
+ console.log("Usage: hatchkit server add [--yes] [--dry-run] [--server-dir <path>]");
1595
+ console.log("Run `hatchkit help server` for details.");
1596
+ process.exit(1);
1597
+ }
1598
+ const { runServerAdd } = await import("./scaffold/server-add.js");
1599
+ const result = await runServerAdd(resolve("."), {
1600
+ yes: args.includes("--yes") || args.includes("-y"),
1601
+ dryRun: args.includes("--dry-run"),
1602
+ serverDir: flagValue("--server-dir"),
1603
+ sharedDir: flagValue("--shared-dir"),
1604
+ });
1605
+ if (args.includes("--json")) {
1606
+ console.log(JSON.stringify(result, null, 2));
1607
+ return;
1608
+ }
1609
+ if (result.dryRun) {
1610
+ console.log(chalk.yellow(" --dry-run — no files were changed."));
1611
+ }
1612
+ if (result.created.length > 0) {
1613
+ console.log(chalk.green(` ✓ Created: ${result.created.join(", ")}`));
1614
+ }
1615
+ if (result.updated.length > 0) {
1616
+ console.log(chalk.green(` ✓ Updated: ${result.updated.join(", ")}`));
1617
+ }
1618
+ if (result.reused.length > 0) {
1619
+ console.log(chalk.dim(` · Reused existing: ${result.reused.join(", ")}`));
1620
+ }
1621
+ for (const warning of result.warnings) {
1622
+ console.log(chalk.yellow(` ! ${warning}`));
1623
+ }
1624
+ if (result.skipped.length > 0 && !result.changed) {
1625
+ console.log(chalk.dim(` · ${result.skipped.join(", ")}`));
1626
+ }
1627
+ if (result.nextSteps.length > 0) {
1628
+ console.log(chalk.bold("\n Next:"));
1629
+ for (const step of result.nextSteps)
1630
+ console.log(` ${step}`);
1631
+ }
1632
+ }
1473
1633
  async function handleConfig() {
1474
1634
  const subcommand = args[1];
1475
1635
  switch (subcommand) {
@@ -1477,7 +1637,7 @@ async function handleConfig() {
1477
1637
  const provider = args[2];
1478
1638
  if (!provider) {
1479
1639
  console.log("Usage: hatchkit config add <provider>");
1480
- console.log("Providers: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, resend, stripe");
1640
+ console.log("Providers: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, plausible, resend, search-console, stripe");
1481
1641
  return;
1482
1642
  }
1483
1643
  // Handle provider setup based on name
@@ -1489,7 +1649,9 @@ async function handleConfig() {
1489
1649
  case "dns":
1490
1650
  case "glitchtip":
1491
1651
  case "openpanel":
1652
+ case "plausible":
1492
1653
  case "resend":
1654
+ case "search-console":
1493
1655
  case "stripe":
1494
1656
  case "ghcr":
1495
1657
  await reconfigureProvider(provider);
@@ -1528,7 +1690,7 @@ async function handleConfig() {
1528
1690
  default:
1529
1691
  if (!isGpuPlatform(provider)) {
1530
1692
  console.log(chalk.red(` Unknown provider: ${provider}`));
1531
- console.log(chalk.dim(" Valid: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, resend, stripe"));
1693
+ console.log(chalk.dim(" Valid: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, plausible, resend, search-console, stripe"));
1532
1694
  return;
1533
1695
  }
1534
1696
  await reconfigureProvider(`gpu.${provider}`);
@@ -1673,6 +1835,37 @@ function printHelp(topic) {
1673
1835
 
1674
1836
  ${chalk.bold("Removal is not supported.")} Removing features could delete
1675
1837
  user code — remove manually + edit the manifest.
1838
+ `);
1839
+ return;
1840
+ }
1841
+ if (topic === "server") {
1842
+ console.log(`
1843
+ ${chalk.bold("hatchkit server add")} — retrofit a server into a client-only project
1844
+
1845
+ ${chalk.bold("Usage:")}
1846
+ cd <project-dir> && hatchkit server add
1847
+ cd <project-dir> && hatchkit server add --yes
1848
+
1849
+ ${chalk.bold("What it does:")}
1850
+ Reads .hatchkit.json, copies the Hatchkit server package from the
1851
+ starter, restores shared server types, updates root scripts/workspace
1852
+ files, flips manifest surfaces from ${chalk.cyan("client-only")} to
1853
+ ${chalk.cyan("both")}, and switches gh-pages projects back to coolify.
1854
+
1855
+ ${chalk.bold("What it does not do:")}
1856
+ No provider calls. No Coolify, DNS, GitHub, keychain, or Terraform
1857
+ mutation. To wire deploy infra after the local scaffold:
1858
+
1859
+ hatchkit adopt --resume --regenerate-pipeline
1860
+
1861
+ ${chalk.bold("Options:")}
1862
+ --server-dir <path> Destination for the server package. Default:
1863
+ ${chalk.dim("packages/server")}.
1864
+ --shared-dir <path> Destination for the shared package. Default:
1865
+ ${chalk.dim("packages/shared")}.
1866
+ --yes, -y Skip confirmation.
1867
+ --dry-run Show planned local changes without writing.
1868
+ --json Machine-readable result.
1676
1869
  `);
1677
1870
  return;
1678
1871
  }
@@ -1792,20 +1985,22 @@ function printHelp(topic) {
1792
1985
  }
1793
1986
  if (topic === "dev-setup") {
1794
1987
  console.log(`
1795
- ${chalk.bold("hatchkit dev-setup")} — opt-in Tailscale-served dev URLs
1988
+ ${chalk.bold("hatchkit dev-setup")} — Tailscale-served dev URLs
1796
1989
 
1797
1990
  Wires up the host-wide plumbing that makes every scaffolded project
1798
1991
  reachable from any Tailscale peer at:
1799
1992
 
1800
- ${chalk.cyan("https://<slug>.local.ricoslabs.com/")}
1993
+ ${chalk.cyan("https://<slug>.local.<your-domain>/")}
1801
1994
 
1802
1995
  …without per-project DNS, port juggling, or framework basePath config.
1803
1996
 
1804
1997
  ${chalk.bold("Host-wide subcommands (run once per machine):")}
1805
1998
  dev-setup init [--force] Auto-write ~/.config/dev/Caddyfile, register
1806
1999
  a launchd job to run Caddy on a free port
1807
- (default 9443, auto-bumps on collision), and
1808
- register a tailscale serve TCP=443 bridge.
2000
+ (default 9443, auto-bumps on collision),
2001
+ register a tailscale serve TCP=443 bridge,
2002
+ and auto-upsert the wildcard DNS A record
2003
+ when Cloudflare credentials are available.
1809
2004
  Idempotent — safe to re-run.
1810
2005
  dev-setup status Print the same Local-dev rows that
1811
2006
  ${chalk.cyan("hatchkit doctor")} would show.
@@ -1825,8 +2020,11 @@ function printHelp(topic) {
1825
2020
  next.config + dep in place (they're inert
1826
2021
  without the fragment).
1827
2022
 
1828
- ${chalk.bold("One-time DNS bit you do yourself (per machine, not per project):")}
1829
- *.local.ricoslabs.com CNAME <your-machine>.<tailnet>.ts.net.
2023
+ ${chalk.bold("DNS:")}
2024
+ ${chalk.cyan("dev-setup init")} auto-upserts a DNS-only A record:
2025
+ *.local.<your-domain> A <your-tailnet-ip> (DNS-only)
2026
+
2027
+ If Cloudflare credentials are unavailable, add that record manually.
1830
2028
 
1831
2029
  This feature is fully optional: until you run ${chalk.cyan("dev-setup init")},
1832
2030
  ${chalk.cyan("hatchkit doctor")} surfaces zero Local-dev rows. Within a project,
@@ -1864,7 +2062,7 @@ function printHelp(topic) {
1864
2062
  · App fqdn references an apex with no Cloudflare zone
1865
2063
  · R2 bucket follows the \`<project>-<role>\` convention but has no
1866
2064
  matching Coolify app (orphan from a destroyed project)
1867
- · GlitchTip / OpenPanel project with no Coolify app counterpart
2065
+ · GlitchTip / OpenPanel / Plausible project/site with no Coolify app counterpart
1868
2066
  · Cloudflare zone with no Coolify app pointing into it
1869
2067
 
1870
2068
  ${chalk.bold("Flags:")}
@@ -1944,10 +2142,14 @@ function printHelp(topic) {
1944
2142
  ${chalk.bold("What it does:")}
1945
2143
  · GlitchTip / OpenPanel: ${chalk.bold("one project per product")}, events tagged by
1946
2144
  \`environment\` so dev / staging / prod share the same dashboard.
1947
- Written to ${chalk.cyan(".env.production")} only dev noise pollutes real metrics.
2145
+ · Plausible: one site for the public project domain, with browser tracker env.
2146
+ Observability values are written to ${chalk.cyan(".env.production")} only — dev noise pollutes real metrics.
1948
2147
  Pass ${chalk.cyan("--enable-dev-obs")} to populate ${chalk.cyan(".env.development")} too.
1949
2148
  · Resend: separate ${chalk.cyan("-dev")} and ${chalk.cyan("-prod")} API keys (audience
1950
2149
  safety). Written to the server's dev + prod env respectively.
2150
+ · Search Console: verifies the project domain via Cloudflare DNS TXT,
2151
+ then adds the ${chalk.cyan("sc-domain:<domain>")} property to your Google account.
2152
+ No runtime env is written.
1951
2153
  · ${chalk.cyan(".env.production")} is dotenvx-encrypted — commit-safe.
1952
2154
  ${chalk.cyan(".env.development")} is plaintext — gitignored, not encrypted.
1953
2155
  · A 0600 cache of every value is saved under
@@ -1967,7 +2169,10 @@ function printHelp(topic) {
1967
2169
  ${chalk.bold("Services:")}
1968
2170
  glitchtip GLITCHTIP_DSN (server) / PUBLIC_GLITCHTIP_DSN (client)
1969
2171
  openpanel OPENPANEL_* (server) / PUBLIC_OPENPANEL_* (client)
2172
+ plausible NEXT_PUBLIC_PLAUSIBLE_DOMAIN / *_SCRIPT_URL (client only)
1970
2173
  resend RESEND_API_KEY (server only)
2174
+ search-console
2175
+ Google Search Console domain property (DNS verification; no env)
1971
2176
  s3 R2_<BUCKET>_ACCESS_KEY_ID / *_SECRET_ACCESS_KEY / *_BUCKET / R2_ENDPOINT
1972
2177
  — mints a per-bucket scoped Cloudflare R2 API token for every
1973
2178
  bucket declared in .hatchkit.json (s3Buckets). Single-bucket
@@ -2016,8 +2221,10 @@ function printHelp(topic) {
2016
2221
  · Writes \`.hatchkit.json\` so \`update\`, \`add\`, \`keys\` recognise
2017
2222
  the project.
2018
2223
  · ${chalk.cyan("GitHub remote")} — \`git init\` (if needed),
2019
- commit, \`gh repo create --private --source=. --push\`. Skipped
2020
- when an \`origin\` is already set.
2224
+ commit, \`gh repo create --private|--public --source=. --push\`.
2225
+ Visibility is prompted (default private) or set with
2226
+ \`--github-visibility private|public\`. Skipped when an \`origin\`
2227
+ is already set.
2021
2228
  · ${chalk.cyan("Coolify + DNS")} — direct REST-API calls into the
2022
2229
  Coolify and Cloudflare you already configured (no Terraform,
2023
2230
  no submodule). Finds or creates the Coolify project, picks
@@ -2027,7 +2234,7 @@ function printHelp(topic) {
2027
2234
  (DOTENV_PRIVATE_KEY_PRODUCTION + GITHUB_REPO_URL), upserts an
2028
2235
  A record \`<domain> → <server-ip>\` on Cloudflare, and triggers
2029
2236
  the first deploy. Defaults ON when no matching app exists.
2030
- · Optionally provisions GlitchTip / OpenPanel / Resend clients
2237
+ · Optionally provisions GlitchTip / OpenPanel / Plausible / Resend clients
2031
2238
  (same machinery as \`hatchkit add\`).
2032
2239
  · Optionally pushes the dotenvx private key to Coolify
2033
2240
  (redundant when the Coolify+DNS step ran — it already does).
@@ -2086,7 +2293,11 @@ function printHelp(topic) {
2086
2293
  ${chalk.bold("Services:")}
2087
2294
  glitchtip Deletes the GlitchTip project
2088
2295
  openpanel Deletes the OpenPanel project (and clears cached creds)
2296
+ plausible Deletes the Plausible site cached for this project
2089
2297
  resend Finds API keys by name and deletes them
2298
+ search-console
2299
+ Removes the Search Console property from your Google account
2300
+ (keeps DNS verification token / ownership state)
2090
2301
  s3 Deletes per-bucket scoped Cloudflare R2 API tokens
2091
2302
  (clears the keychain entries and DELETEs upstream)
2092
2303
 
@@ -2124,7 +2335,7 @@ function printHelp(topic) {
2124
2335
  create + adopt:
2125
2336
  - GitHub repo ${chalk.dim("gh repo delete")}
2126
2337
  - dotenvx private key in keychain ${chalk.dim("keytar deletePassword")}
2127
- - GlitchTip / OpenPanel / Resend ${chalk.dim("DELETE")} per-vendor
2338
+ - GlitchTip / OpenPanel / Plausible / Resend ${chalk.dim("DELETE")} per-vendor
2128
2339
  - Coolify app / project / database ${chalk.dim("DELETE /api/v1/...")}
2129
2340
 
2130
2341
  adopt-only (fine-grained, never wider than what adopt itself wrote):
@@ -2269,7 +2480,7 @@ function printHelp(topic) {
2269
2480
  config Show status of every configured provider (alias: \`status\`)
2270
2481
  config add <p> Configure a provider
2271
2482
  (coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate,
2272
- glitchtip, openpanel, resend, stripe)
2483
+ glitchtip, openpanel, plausible, resend, search-console, stripe)
2273
2484
  config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
2274
2485
  `);
2275
2486
  return;
@@ -2374,7 +2585,8 @@ function printHelp(topic) {
2374
2585
  create Scaffold a new project (interactive)
2375
2586
  adopt Bring an existing project under hatchkit management (run in project dir)
2376
2587
  update Add features to an already-scaffolded project (run in project dir)
2377
- add Create GlitchTip / OpenPanel / Resend clients for an existing project
2588
+ server add Retrofit a server into a client-only project
2589
+ add Create GlitchTip / OpenPanel / Plausible / Resend clients for an existing project
2378
2590
  assets Move bytes between local MinIO and prod buckets (seed/push/pull/migrate)
2379
2591
  remove Delete the -dev/-prod clients created by 'add' (inverse of add)
2380
2592
  destroy Roll back everything ${chalk.cyan("hatchkit create")} did for a project
@@ -2408,7 +2620,12 @@ function printHelp(topic) {
2408
2620
  --yes, -y (with \`create\`) skip prompts, use defaults / --config values
2409
2621
  --config <path> (with \`create\`) load JSON overrides for ProjectConfig fields
2410
2622
  --name <name> (with \`create\`) set project name without prompting
2623
+ --local-dev[=<slug>] (with \`create\`) enable Tailscale dev URL, optionally with slug
2624
+ --no-local-dev (with \`create\`) skip local-dev wiring
2411
2625
  --no-github (with \`create\`) skip GitHub repo creation
2626
+ --github-visibility {private|public}
2627
+ (with \`create\`) visibility for a newly-created GitHub repo.
2628
+ Default: private. Shorthands: \`--private\`, \`--public\`.
2412
2629
  --no-deploy (with \`create\`) skip Terraform/Coolify/ML deployment
2413
2630
 
2414
2631
  ${chalk.bold("Environment:")}