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.
- package/dist/adopt.d.ts +61 -1
- package/dist/adopt.d.ts.map +1 -1
- package/dist/adopt.js +90 -86
- package/dist/adopt.js.map +1 -1
- package/dist/assets/env.d.ts +2 -2
- package/dist/assets/env.d.ts.map +1 -1
- package/dist/assets/index.js +11 -11
- package/dist/assets/index.js.map +1 -1
- package/dist/assets/mirror.js +1 -1
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +20 -2
- package/dist/completion.js.map +1 -1
- package/dist/config.d.ts +32 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +364 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify.d.ts +5 -0
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +67 -4
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/ghcr.d.ts +1 -0
- package/dist/deploy/ghcr.d.ts.map +1 -1
- package/dist/deploy/ghcr.js +2 -2
- package/dist/deploy/ghcr.js.map +1 -1
- package/dist/deploy/github.d.ts.map +1 -1
- package/dist/deploy/github.js +3 -2
- package/dist/deploy/github.js.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +9 -0
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/dev-setup.d.ts +13 -5
- package/dist/dev-setup.d.ts.map +1 -1
- package/dist/dev-setup.js +268 -59
- package/dist/dev-setup.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +65 -1
- package/dist/doctor.js.map +1 -1
- package/dist/email/index.js +5 -5
- package/dist/email/index.js.map +1 -1
- package/dist/email/setup.d.ts +1 -1
- package/dist/email/setup.d.ts.map +1 -1
- package/dist/email/setup.js +3 -3
- package/dist/email/setup.js.map +1 -1
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +9 -8
- package/dist/explain.js.map +1 -1
- package/dist/index.js +523 -91
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +1 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +2 -0
- package/dist/inventory.js.map +1 -1
- package/dist/onboarding/plan.d.ts +54 -0
- package/dist/onboarding/plan.d.ts.map +1 -0
- package/dist/onboarding/plan.js +143 -0
- package/dist/onboarding/plan.js.map +1 -0
- package/dist/onboarding/review.d.ts +27 -0
- package/dist/onboarding/review.d.ts.map +1 -0
- package/dist/onboarding/review.js +55 -0
- package/dist/onboarding/review.js.map +1 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +107 -89
- package/dist/prompts.js.map +1 -1
- package/dist/provision/glitchtip.d.ts +1 -0
- package/dist/provision/glitchtip.d.ts.map +1 -1
- package/dist/provision/glitchtip.js +16 -0
- package/dist/provision/glitchtip.js.map +1 -1
- package/dist/provision/index.d.ts +26 -3
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +215 -11
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/openpanel.d.ts +1 -0
- package/dist/provision/openpanel.d.ts.map +1 -1
- package/dist/provision/openpanel.js +21 -0
- package/dist/provision/openpanel.js.map +1 -1
- package/dist/provision/plausible.d.ts +11 -0
- package/dist/provision/plausible.d.ts.map +1 -0
- package/dist/provision/plausible.js +108 -0
- package/dist/provision/plausible.js.map +1 -0
- package/dist/provision/resend.d.ts +4 -0
- package/dist/provision/resend.d.ts.map +1 -1
- package/dist/provision/resend.js +11 -6
- package/dist/provision/resend.js.map +1 -1
- package/dist/provision/search-console.d.ts +17 -0
- package/dist/provision/search-console.d.ts.map +1 -0
- package/dist/provision/search-console.js +142 -0
- package/dist/provision/search-console.js.map +1 -0
- package/dist/scaffold/app.d.ts +1 -0
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +6 -3
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/infra.js +2 -0
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts +18 -2
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +7 -1
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/server-add.d.ts +21 -0
- package/dist/scaffold/server-add.d.ts.map +1 -0
- package/dist/scaffold/server-add.js +275 -0
- package/dist/scaffold/server-add.js.map +1 -0
- package/dist/scaffold/starter-files.d.ts +3 -3
- package/dist/scaffold/starter-files.js +3 -3
- package/dist/scaffold/update.d.ts +1 -0
- package/dist/scaffold/update.d.ts.map +1 -1
- package/dist/scaffold/update.js +8 -5
- package/dist/scaffold/update.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +27 -1
- package/dist/status.js.map +1 -1
- package/dist/templates/base/env.example.hbs +3 -0
- package/dist/utils/cloudflare-api.d.ts +5 -0
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +19 -0
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/coolify-api.d.ts +3 -2
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +19 -5
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/flags.d.ts.map +1 -1
- package/dist/utils/flags.js +16 -0
- package/dist/utils/flags.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +3 -0
- package/dist/utils/run-ledger.d.ts.map +1 -1
- package/dist/utils/run-ledger.js.map +1 -1
- package/dist/utils/secrets.d.ts +5 -0
- package/dist/utils/secrets.d.ts.map +1 -1
- package/dist/utils/secrets.js +5 -0
- package/dist/utils/secrets.js.map +1 -1
- package/package.json +24 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } 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";
|
|
@@ -11,9 +11,10 @@ import { pushProjectKeyToCoolify, pushProjectKeyToGh, rotateProjectKey, setProje
|
|
|
11
11
|
import { handleCreateFailure, runRollback } from "./deploy/rollback.js";
|
|
12
12
|
import { runTerraform } from "./deploy/terraform.js";
|
|
13
13
|
import { collectProjectConfig } from "./prompts.js";
|
|
14
|
-
import { runProvision, runUnprovision } from "./provision/index.js";
|
|
14
|
+
import { runProvision, runUnprovision, } from "./provision/index.js";
|
|
15
15
|
import { scaffoldApp } from "./scaffold/app.js";
|
|
16
16
|
import { scaffoldInfra } from "./scaffold/infra.js";
|
|
17
|
+
import { readManifest } from "./scaffold/manifest.js";
|
|
17
18
|
import { mlEnvVarName, printMlSummary, resolveMlServices } from "./scaffold/ml-client.js";
|
|
18
19
|
import { runUpdate } from "./scaffold/update.js";
|
|
19
20
|
import { installCancelHandler, isCancelInProgress, uninstallCancelHandler, } from "./utils/cancel-handler.js";
|
|
@@ -108,6 +109,11 @@ async function main() {
|
|
|
108
109
|
return printHelp("update");
|
|
109
110
|
await handleUpdate();
|
|
110
111
|
break;
|
|
112
|
+
case "server":
|
|
113
|
+
if (args.includes("--help") && args.length === 2)
|
|
114
|
+
return printHelp("server");
|
|
115
|
+
await handleServer();
|
|
116
|
+
break;
|
|
111
117
|
case "keys":
|
|
112
118
|
if (args.includes("--help") && args.length === 2)
|
|
113
119
|
return printHelp("keys");
|
|
@@ -433,10 +439,10 @@ function opts(result) {
|
|
|
433
439
|
* dir already lives inside the project (`packages/server`,
|
|
434
440
|
* `apps/web`, etc.). Returns undefined when no manifest is found —
|
|
435
441
|
* callers fall back to "skip s3" with a hint. */
|
|
436
|
-
function inferProjectDir(
|
|
437
|
-
if (!
|
|
442
|
+
function inferProjectDir(startDir) {
|
|
443
|
+
if (!startDir)
|
|
438
444
|
return undefined;
|
|
439
|
-
let cur =
|
|
445
|
+
let cur = startDir;
|
|
440
446
|
for (let i = 0; i < 4; i++) {
|
|
441
447
|
if (existsSync(join(cur, ".hatchkit.json")))
|
|
442
448
|
return cur;
|
|
@@ -447,16 +453,191 @@ function inferProjectDir(serverEnvDir) {
|
|
|
447
453
|
}
|
|
448
454
|
return undefined;
|
|
449
455
|
}
|
|
456
|
+
function manifestBucketEntries(manifest) {
|
|
457
|
+
const buckets = manifest?.s3Buckets;
|
|
458
|
+
if (!buckets)
|
|
459
|
+
return [];
|
|
460
|
+
const out = [];
|
|
461
|
+
for (const [key, value] of Object.entries(buckets)) {
|
|
462
|
+
if (key === "tokenId" || key === "accountId")
|
|
463
|
+
continue;
|
|
464
|
+
if (value && typeof value === "object" && "name" in value) {
|
|
465
|
+
out.push(value);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
function readIfExists(path) {
|
|
471
|
+
if (!existsSync(path))
|
|
472
|
+
return "";
|
|
473
|
+
try {
|
|
474
|
+
return readFileSync(path, "utf-8");
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return "";
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function readProjectEnvText(projectDir, baseName) {
|
|
481
|
+
const chunks = [];
|
|
482
|
+
if (projectDir) {
|
|
483
|
+
for (const dir of [
|
|
484
|
+
".",
|
|
485
|
+
"packages/server",
|
|
486
|
+
"packages/client",
|
|
487
|
+
"packages/web",
|
|
488
|
+
"apps/server",
|
|
489
|
+
"apps/api",
|
|
490
|
+
"apps/web",
|
|
491
|
+
"apps/client",
|
|
492
|
+
"server",
|
|
493
|
+
"client",
|
|
494
|
+
"web",
|
|
495
|
+
]) {
|
|
496
|
+
const abs = resolve(projectDir, dir);
|
|
497
|
+
chunks.push(readIfExists(join(abs, ".env.production")));
|
|
498
|
+
chunks.push(readIfExists(join(abs, ".env.development")));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (baseName) {
|
|
502
|
+
const provisionedDir = join(dirname(getConfigPath()), "provisioned");
|
|
503
|
+
if (existsSync(provisionedDir)) {
|
|
504
|
+
for (const file of readdirSync(provisionedDir)) {
|
|
505
|
+
if (file.startsWith(`${baseName}.`) && file.endsWith(".env")) {
|
|
506
|
+
chunks.push(readIfExists(join(provisionedDir, file)));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return chunks.join("\n");
|
|
512
|
+
}
|
|
513
|
+
function servicesAlreadyAdded(args) {
|
|
514
|
+
const text = readProjectEnvText(args.projectDir, args.baseName);
|
|
515
|
+
const added = new Set();
|
|
516
|
+
if (/(^|\n)(PUBLIC_)?GLITCHTIP_DSN=/m.test(text))
|
|
517
|
+
added.add("glitchtip");
|
|
518
|
+
if (/(^|\n)(PUBLIC_)?OPENPANEL_CLIENT_ID=/m.test(text))
|
|
519
|
+
added.add("openpanel");
|
|
520
|
+
if (/(^|\n)(NEXT_PUBLIC_|PUBLIC_)?PLAUSIBLE_DOMAIN=/m.test(text))
|
|
521
|
+
added.add("plausible");
|
|
522
|
+
if (/(^|\n)RESEND_API_KEY=/m.test(text))
|
|
523
|
+
added.add("resend");
|
|
524
|
+
if (/(^|\n)R2(_[A-Z0-9]+)?_ACCESS_KEY_ID=/m.test(text))
|
|
525
|
+
added.add("s3");
|
|
526
|
+
if (manifestBucketEntries(args.manifest).some((bucket) => bucket.tokenId))
|
|
527
|
+
added.add("s3");
|
|
528
|
+
if (args.manifest?.integrations?.email)
|
|
529
|
+
added.add("email");
|
|
530
|
+
if (args.manifest?.integrations?.searchConsole)
|
|
531
|
+
added.add("search-console");
|
|
532
|
+
return added;
|
|
533
|
+
}
|
|
534
|
+
function servicesImpossibleForProject(manifest) {
|
|
535
|
+
const blocked = new Set();
|
|
536
|
+
if (!manifest)
|
|
537
|
+
return blocked;
|
|
538
|
+
if (!manifest.domain) {
|
|
539
|
+
blocked.add("email");
|
|
540
|
+
blocked.add("search-console");
|
|
541
|
+
}
|
|
542
|
+
if (manifest.surfaces === "server-only")
|
|
543
|
+
blocked.add("plausible");
|
|
544
|
+
if (manifest.surfaces === "client-only") {
|
|
545
|
+
blocked.add("resend");
|
|
546
|
+
blocked.add("s3");
|
|
547
|
+
}
|
|
548
|
+
if (manifestBucketEntries(manifest).length === 0)
|
|
549
|
+
blocked.add("s3");
|
|
550
|
+
return blocked;
|
|
551
|
+
}
|
|
552
|
+
function recordProvisionedEvent(ledger, event) {
|
|
553
|
+
if (event.service === "glitchtip")
|
|
554
|
+
ledger.record({ kind: "glitchtip", project: event.project });
|
|
555
|
+
if (event.service === "openpanel")
|
|
556
|
+
ledger.record({ kind: "openpanel", project: event.project });
|
|
557
|
+
if (event.service === "plausible")
|
|
558
|
+
ledger.record({ kind: "plausible", project: event.project });
|
|
559
|
+
if (event.service === "resend")
|
|
560
|
+
ledger.record({ kind: "resend", client: event.client });
|
|
561
|
+
if (event.service === "s3" && event.minted) {
|
|
562
|
+
ledger.record({
|
|
563
|
+
kind: "r2Token",
|
|
564
|
+
tokenId: event.tokenId,
|
|
565
|
+
accountId: event.accountId,
|
|
566
|
+
audience: "account",
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (event.service === "search-console" && event.dnsRecord?.created) {
|
|
570
|
+
ledger.record({
|
|
571
|
+
kind: "cloudflareDnsRecord",
|
|
572
|
+
zoneId: event.dnsRecord.zoneId,
|
|
573
|
+
recordId: event.dnsRecord.id,
|
|
574
|
+
name: event.dnsRecord.name,
|
|
575
|
+
type: event.dnsRecord.type,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (event.service === "email") {
|
|
579
|
+
if (event.destinationCreatedThisRun) {
|
|
580
|
+
ledger.record({
|
|
581
|
+
kind: "cloudflareEmailDestination",
|
|
582
|
+
accountId: event.accountId,
|
|
583
|
+
destinationId: event.destinationId,
|
|
584
|
+
email: event.destinationEmail,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
for (const dns of event.dnsRecords) {
|
|
588
|
+
ledger.record({
|
|
589
|
+
kind: "cloudflareDnsRecord",
|
|
590
|
+
zoneId: event.zoneId,
|
|
591
|
+
recordId: dns.id,
|
|
592
|
+
name: dns.name,
|
|
593
|
+
type: dns.type,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
for (const rule of event.rules) {
|
|
597
|
+
if (!rule.created)
|
|
598
|
+
continue;
|
|
599
|
+
ledger.record({
|
|
600
|
+
kind: "cloudflareEmailRoutingRule",
|
|
601
|
+
zoneId: event.zoneId,
|
|
602
|
+
ruleId: rule.id,
|
|
603
|
+
address: rule.address,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
450
608
|
async function handleAdd() {
|
|
451
609
|
// Positional args are optional — anything missing is prompted for.
|
|
452
610
|
// hatchkit add (fully interactive)
|
|
453
611
|
// hatchkit add raptor-runner (prompts for services)
|
|
454
612
|
// hatchkit add raptor-runner all
|
|
455
613
|
// hatchkit add raptor-runner glitchtip,resend
|
|
614
|
+
const allServices = [
|
|
615
|
+
"glitchtip",
|
|
616
|
+
"openpanel",
|
|
617
|
+
"plausible",
|
|
618
|
+
"resend",
|
|
619
|
+
"s3",
|
|
620
|
+
"email",
|
|
621
|
+
"search-console",
|
|
622
|
+
];
|
|
623
|
+
const isServiceExpr = (value) => {
|
|
624
|
+
if (!value)
|
|
625
|
+
return false;
|
|
626
|
+
if (value === "all")
|
|
627
|
+
return true;
|
|
628
|
+
return value
|
|
629
|
+
.split(",")
|
|
630
|
+
.map((s) => s.trim().toLowerCase())
|
|
631
|
+
.every((s) => allServices.includes(s));
|
|
632
|
+
};
|
|
456
633
|
const positional = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
const
|
|
634
|
+
const inferredProjectDir = inferProjectDir(process.cwd());
|
|
635
|
+
const inferredManifest = inferredProjectDir ? readManifest(inferredProjectDir) : null;
|
|
636
|
+
const firstArgIsService = isServiceExpr(positional[0]);
|
|
637
|
+
let baseName = firstArgIsService
|
|
638
|
+
? inferredManifest?.name
|
|
639
|
+
: (positional[0] ?? inferredManifest?.name);
|
|
640
|
+
const rawService = firstArgIsService ? positional[0] : positional[1];
|
|
460
641
|
if (!baseName) {
|
|
461
642
|
const { input } = await import("@inquirer/prompts");
|
|
462
643
|
const { validateProjectName } = await import("./utils/validate.js");
|
|
@@ -465,31 +646,54 @@ async function handleAdd() {
|
|
|
465
646
|
validate: validateProjectName,
|
|
466
647
|
});
|
|
467
648
|
}
|
|
649
|
+
const alreadyAdded = servicesAlreadyAdded({
|
|
650
|
+
baseName,
|
|
651
|
+
projectDir: inferredProjectDir,
|
|
652
|
+
manifest: inferredManifest,
|
|
653
|
+
});
|
|
654
|
+
const impossible = servicesImpossibleForProject(inferredManifest);
|
|
655
|
+
const hiddenServices = new Set([...alreadyAdded, ...impossible]);
|
|
656
|
+
const addableServices = allServices.filter((service) => !hiddenServices.has(service));
|
|
468
657
|
let services;
|
|
469
658
|
if (!rawService) {
|
|
659
|
+
if (addableServices.length === 0) {
|
|
660
|
+
console.log(chalk.green(` Nothing to add — ${baseName} already has every supported service.`));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
470
663
|
const { multiselect } = await import("./utils/multiselect.js");
|
|
664
|
+
const serviceChoices = [
|
|
665
|
+
{ name: "GlitchTip (error tracking)", value: "glitchtip", checked: false },
|
|
666
|
+
{ name: "OpenPanel (product analytics)", value: "openpanel", checked: false },
|
|
667
|
+
{ name: "Plausible (web analytics)", value: "plausible", checked: false },
|
|
668
|
+
{ name: "Resend (transactional email)", value: "resend", checked: false },
|
|
669
|
+
{
|
|
670
|
+
name: "S3 / R2 (per-bucket scoped credentials from .hatchkit.json)",
|
|
671
|
+
value: "s3",
|
|
672
|
+
checked: false,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "Email forwarding (Cloudflare Email Routing — MX/SPF/DMARC + rules)",
|
|
676
|
+
value: "email",
|
|
677
|
+
checked: false,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "Google Search Console (DNS verification + domain property)",
|
|
681
|
+
value: "search-console",
|
|
682
|
+
checked: false,
|
|
683
|
+
},
|
|
684
|
+
];
|
|
471
685
|
services = await multiselect({
|
|
472
686
|
message: "Which services to add?",
|
|
473
|
-
choices:
|
|
474
|
-
{ name: "GlitchTip (error tracking)", value: "glitchtip", checked: true },
|
|
475
|
-
{ name: "OpenPanel (product analytics)", value: "openpanel", checked: true },
|
|
476
|
-
{ name: "Resend (transactional email)", value: "resend", checked: true },
|
|
477
|
-
{
|
|
478
|
-
name: "S3 / R2 (per-bucket scoped credentials from .hatchkit.json)",
|
|
479
|
-
value: "s3",
|
|
480
|
-
checked: false,
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
name: "Email forwarding (Cloudflare Email Routing — MX/SPF/DMARC + rules)",
|
|
484
|
-
value: "email",
|
|
485
|
-
checked: false,
|
|
486
|
-
},
|
|
487
|
-
],
|
|
687
|
+
choices: serviceChoices.filter((choice) => addableServices.includes(choice.value)),
|
|
488
688
|
required: true,
|
|
489
689
|
});
|
|
490
690
|
}
|
|
491
691
|
else if (rawService === "all") {
|
|
492
|
-
services =
|
|
692
|
+
services = addableServices;
|
|
693
|
+
if (services.length === 0) {
|
|
694
|
+
console.log(chalk.green(` Nothing to add — ${baseName} already has every supported service.`));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
493
697
|
}
|
|
494
698
|
else {
|
|
495
699
|
const requested = rawService.split(",").map((s) => s.trim().toLowerCase());
|
|
@@ -499,11 +703,21 @@ async function handleAdd() {
|
|
|
499
703
|
console.log(chalk.dim(` Valid: ${allServices.join(", ")}, or 'all'`));
|
|
500
704
|
process.exit(1);
|
|
501
705
|
}
|
|
502
|
-
|
|
706
|
+
const skipped = requested.filter((service) => hiddenServices.has(service));
|
|
707
|
+
if (skipped.length > 0) {
|
|
708
|
+
console.log(chalk.red(` Refusing to add already-present/unavailable service(s): ${skipped.join(", ")}`));
|
|
709
|
+
console.log(chalk.dim(" Run `hatchkit remove` first if you want Hatchkit to recreate them."));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
services = requested.filter((service) => !hiddenServices.has(service));
|
|
713
|
+
if (services.length === 0) {
|
|
714
|
+
console.log(chalk.green(` Nothing to add — requested service(s) are already present or unavailable.`));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
503
717
|
}
|
|
504
718
|
// Flag parsing:
|
|
505
719
|
// --no-write → never write; print a cache summary only
|
|
506
|
-
// --enable-dev-obs → also populate .env.development with
|
|
720
|
+
// --enable-dev-obs → also populate .env.development with observability creds
|
|
507
721
|
// --surfaces=<shared|separate|server-only|client-only>
|
|
508
722
|
// --server-dir <path> → absolute or project-relative env dir for the server
|
|
509
723
|
// --client-dir <path> → same for the client
|
|
@@ -554,7 +768,16 @@ async function handleAdd() {
|
|
|
554
768
|
: inferProjectDir(needsServer ? resolvePath(serverDirFlag) : undefined),
|
|
555
769
|
};
|
|
556
770
|
}
|
|
557
|
-
|
|
771
|
+
const ledger = RunLedger.resumeOrStart(baseName);
|
|
772
|
+
await runProvision({
|
|
773
|
+
baseName,
|
|
774
|
+
services,
|
|
775
|
+
surfaces,
|
|
776
|
+
enableDevObs,
|
|
777
|
+
failIfExists: true,
|
|
778
|
+
onProvisioned: (event) => recordProvisionedEvent(ledger, event),
|
|
779
|
+
});
|
|
780
|
+
ledger.complete();
|
|
558
781
|
}
|
|
559
782
|
async function handleProvisionS3() {
|
|
560
783
|
// `hatchkit provision s3` — create the public+private bucket pair
|
|
@@ -783,7 +1006,15 @@ async function handleRemove() {
|
|
|
783
1006
|
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
784
1007
|
let baseName = positional[0];
|
|
785
1008
|
const rawService = positional[1];
|
|
786
|
-
const allServices = [
|
|
1009
|
+
const allServices = [
|
|
1010
|
+
"glitchtip",
|
|
1011
|
+
"openpanel",
|
|
1012
|
+
"plausible",
|
|
1013
|
+
"resend",
|
|
1014
|
+
"s3",
|
|
1015
|
+
"email",
|
|
1016
|
+
"search-console",
|
|
1017
|
+
];
|
|
787
1018
|
if (!baseName) {
|
|
788
1019
|
const { input } = await import("@inquirer/prompts");
|
|
789
1020
|
const { validateProjectName } = await import("./utils/validate.js");
|
|
@@ -800,6 +1031,7 @@ async function handleRemove() {
|
|
|
800
1031
|
choices: [
|
|
801
1032
|
{ name: "GlitchTip (deletes the project)", value: "glitchtip", checked: true },
|
|
802
1033
|
{ name: "OpenPanel (deletes the project)", value: "openpanel", checked: true },
|
|
1034
|
+
{ name: "Plausible (deletes the site)", value: "plausible", checked: false },
|
|
803
1035
|
{ name: "Resend (deletes the API key)", value: "resend", checked: true },
|
|
804
1036
|
{ name: "S3 / R2 (deletes per-bucket scoped tokens)", value: "s3", checked: false },
|
|
805
1037
|
{
|
|
@@ -807,6 +1039,11 @@ async function handleRemove() {
|
|
|
807
1039
|
value: "email",
|
|
808
1040
|
checked: false,
|
|
809
1041
|
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "Google Search Console (removes property; keeps verification token)",
|
|
1044
|
+
value: "search-console",
|
|
1045
|
+
checked: false,
|
|
1046
|
+
},
|
|
810
1047
|
],
|
|
811
1048
|
required: true,
|
|
812
1049
|
});
|
|
@@ -841,7 +1078,7 @@ async function handleRemove() {
|
|
|
841
1078
|
// project directory if it exists; the s3 unprovision falls back to
|
|
842
1079
|
// a keychain sweep when the manifest can't be found.
|
|
843
1080
|
let projectDir;
|
|
844
|
-
if (services.includes("s3")) {
|
|
1081
|
+
if (services.includes("s3") || services.includes("search-console")) {
|
|
845
1082
|
const guess = resolve(baseName);
|
|
846
1083
|
if (existsSync(join(guess, ".hatchkit.json"))) {
|
|
847
1084
|
projectDir = guess;
|
|
@@ -867,6 +1104,52 @@ async function handleDns() {
|
|
|
867
1104
|
printHelp("dns");
|
|
868
1105
|
}
|
|
869
1106
|
}
|
|
1107
|
+
async function configureGhcrForCreate(repoUrl, isPrivateRepo, ledger) {
|
|
1108
|
+
const { repoSlugFromRemote } = await import("./deploy/gh-actions-secrets.js");
|
|
1109
|
+
const slug = repoSlugFromRemote(repoUrl);
|
|
1110
|
+
if (!slug) {
|
|
1111
|
+
console.log(chalk.dim(" · Couldn't resolve owner/repo from GitHub URL — skipping GHCR pull setup."));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const coolify = await getCoolifyConfig();
|
|
1115
|
+
if (!coolify) {
|
|
1116
|
+
console.log(chalk.dim(" · Coolify not configured — skipping GHCR pull setup."));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const { CoolifyApi } = await import("./utils/coolify-api.js");
|
|
1120
|
+
const { makeGhcrPackagePublic, registerGhcrCredsWithCoolify } = await import("./deploy/ghcr.js");
|
|
1121
|
+
if (!isPrivateRepo) {
|
|
1122
|
+
const result = await makeGhcrPackagePublic({ repoSlug: slug });
|
|
1123
|
+
if (result.kind === "public-set")
|
|
1124
|
+
return;
|
|
1125
|
+
if (result.kind === "skipped" || result.kind === "failed") {
|
|
1126
|
+
console.log(chalk.yellow(` GHCR public-image setup skipped: ${result.reason}`));
|
|
1127
|
+
console.log(chalk.dim(result.recovery.map((line) => ` ${line}`).join("\n")));
|
|
1128
|
+
}
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const ghcrConfig = await getGhcrConfig();
|
|
1132
|
+
const api = new CoolifyApi({ url: coolify.url, token: coolify.token });
|
|
1133
|
+
const result = await registerGhcrCredsWithCoolify({
|
|
1134
|
+
api,
|
|
1135
|
+
repoSlug: slug,
|
|
1136
|
+
pullToken: ghcrConfig?.pullToken,
|
|
1137
|
+
username: ghcrConfig?.username,
|
|
1138
|
+
});
|
|
1139
|
+
if (result.kind === "private-registered") {
|
|
1140
|
+
if (result.created) {
|
|
1141
|
+
ledger?.record({ kind: "coolifyPrivateRegistry", uuid: result.registryUuid });
|
|
1142
|
+
}
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (result.kind === "skipped" || result.kind === "failed") {
|
|
1146
|
+
console.log(chalk.yellow(` GHCR private-image pull setup skipped: ${result.reason}`));
|
|
1147
|
+
console.log(chalk.dim(result.recovery.map((line) => ` ${line}`).join("\n")));
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function isCreatedGithubRepoPrivate(config) {
|
|
1151
|
+
return config.createGithubRepo && (config.githubRepoVisibility ?? "private") === "private";
|
|
1152
|
+
}
|
|
870
1153
|
// ---------------------------------------------------------------------------
|
|
871
1154
|
// Commands
|
|
872
1155
|
// ---------------------------------------------------------------------------
|
|
@@ -875,20 +1158,22 @@ async function handleCreate() {
|
|
|
875
1158
|
// the flow non-interactive; otherwise we still prompt for anything
|
|
876
1159
|
// not supplied via flags / config file.
|
|
877
1160
|
const flags = parseCreateFlags(args);
|
|
878
|
-
const { yes: nonInteractive, dryRun, presets, forceNoGithub, forceNoDeploy, forceNoInstall, } = flags;
|
|
1161
|
+
const { yes: nonInteractive, dryRun, presets, forceNoGithub, forceNoDeploy, forceNoInstall, forceNoLocalDev, } = flags;
|
|
879
1162
|
// Check if first run (skip onboarding when non-interactive — the
|
|
880
1163
|
// onboarding prompts would stall automation).
|
|
881
1164
|
if (!nonInteractive && (await isFirstRun())) {
|
|
882
1165
|
await runOnboarding();
|
|
883
1166
|
}
|
|
884
1167
|
// Collect project config via interactive prompts (or presets).
|
|
885
|
-
const config = await collectProjectConfig({ dryRun, presets, nonInteractive });
|
|
1168
|
+
const config = await collectProjectConfig({ dryRun, presets, nonInteractive, forceNoLocalDev });
|
|
886
1169
|
if (forceNoGithub)
|
|
887
1170
|
config.createGithubRepo = false;
|
|
888
1171
|
if (forceNoDeploy)
|
|
889
1172
|
config.runDeployment = false;
|
|
890
1173
|
if (forceNoInstall)
|
|
891
1174
|
config.installDeps = false;
|
|
1175
|
+
if (forceNoLocalDev)
|
|
1176
|
+
config.localDev = undefined;
|
|
892
1177
|
// Ensure needed providers are configured (lazy prompting).
|
|
893
1178
|
// Coolify + Hetzner only matter for the coolify deployment mode.
|
|
894
1179
|
// gh-pages skips them entirely (no server, no Docker registry).
|
|
@@ -917,13 +1202,19 @@ async function handleCreate() {
|
|
|
917
1202
|
await ensureS3(config.s3Provider);
|
|
918
1203
|
}
|
|
919
1204
|
}
|
|
920
|
-
// Pre-flight observability +
|
|
921
|
-
// create` directly (not just `add`): if the user picked
|
|
922
|
-
//
|
|
923
|
-
//
|
|
1205
|
+
// Pre-flight observability + Stripe providers used by `hatchkit
|
|
1206
|
+
// create` directly (not just `add`): if the user picked analytics
|
|
1207
|
+
// providers, make sure they are configured before we can mint
|
|
1208
|
+
// project-scoped resources. Same for Stripe webhook auto-provisioning.
|
|
924
1209
|
if (config.features.includes("analytics")) {
|
|
925
|
-
const
|
|
926
|
-
await
|
|
1210
|
+
const providers = config.analyticsProviders ?? ["glitchtip"];
|
|
1211
|
+
const { ensureGlitchtip, ensureOpenpanel, ensurePlausible } = await import("./config.js");
|
|
1212
|
+
if (providers.includes("glitchtip"))
|
|
1213
|
+
await ensureGlitchtip();
|
|
1214
|
+
if (providers.includes("openpanel"))
|
|
1215
|
+
await ensureOpenpanel();
|
|
1216
|
+
if (providers.includes("plausible"))
|
|
1217
|
+
await ensurePlausible();
|
|
927
1218
|
}
|
|
928
1219
|
if (config.features.includes("stripe")) {
|
|
929
1220
|
const { ensureStripe } = await import("./config.js");
|
|
@@ -954,7 +1245,7 @@ async function handleCreate() {
|
|
|
954
1245
|
console.log(` Features: ${config.features.length > 0 ? config.features.join(", ") : "none"}`);
|
|
955
1246
|
console.log(` ML: ${config.mlServices.length > 0 ? config.mlServices.join(", ") : "none"}`);
|
|
956
1247
|
console.log(` Scaffold: ${config.scaffoldRepo ? "yes" : "no"}`);
|
|
957
|
-
console.log(` GitHub: ${config.createGithubRepo ?
|
|
1248
|
+
console.log(` GitHub: ${config.createGithubRepo ? `yes (${config.githubRepoVisibility ?? "private"})` : "no"}`);
|
|
958
1249
|
console.log(` Install: ${config.installDeps ? "yes (pnpm install)" : "no"}`);
|
|
959
1250
|
console.log(` Deploy now: ${config.runDeployment ? "yes" : "no"}`);
|
|
960
1251
|
if (config.dryRun) {
|
|
@@ -996,28 +1287,47 @@ async function handleCreate() {
|
|
|
996
1287
|
const { printDotenvxSummary } = await import("./scaffold/dotenvx.js");
|
|
997
1288
|
printDotenvxSummary(scaffoldResult.dotenvx, config.name);
|
|
998
1289
|
}
|
|
999
|
-
// Auto-provision
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (config.features.includes("analytics") && config.surfaces !== "client-only") {
|
|
1290
|
+
// Auto-provision selected observability/analytics providers
|
|
1291
|
+
// through the same machinery used by `hatchkit add`, so create,
|
|
1292
|
+
// adopt, and existing-project provisioning stay aligned.
|
|
1293
|
+
if (config.features.includes("analytics")) {
|
|
1294
|
+
const analyticsServices = [
|
|
1295
|
+
...(config.analyticsProviders ?? ["glitchtip"]),
|
|
1296
|
+
];
|
|
1007
1297
|
try {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1298
|
+
if (analyticsServices.length > 0) {
|
|
1299
|
+
const provisionMode = config.surfaces === "both"
|
|
1300
|
+
? "shared"
|
|
1301
|
+
: config.surfaces === "server-only"
|
|
1302
|
+
? "server-only"
|
|
1303
|
+
: "client-only";
|
|
1304
|
+
await runProvision({
|
|
1305
|
+
baseName: config.name,
|
|
1306
|
+
services: analyticsServices,
|
|
1307
|
+
domain: config.domain,
|
|
1308
|
+
surfaces: {
|
|
1309
|
+
mode: provisionMode,
|
|
1310
|
+
projectDir: appDir,
|
|
1311
|
+
serverEnvDir: config.surfaces === "client-only" ? undefined : join(appDir, "packages/server"),
|
|
1312
|
+
clientEnvDir: config.surfaces === "server-only" ? undefined : join(appDir, "packages/client"),
|
|
1313
|
+
},
|
|
1314
|
+
onProvisioned: (event) => {
|
|
1315
|
+
if (event.service === "glitchtip") {
|
|
1316
|
+
ledger?.record({ kind: "glitchtip", project: event.project });
|
|
1317
|
+
}
|
|
1318
|
+
else if (event.service === "openpanel") {
|
|
1319
|
+
ledger?.record({ kind: "openpanel", project: event.project });
|
|
1320
|
+
}
|
|
1321
|
+
else if (event.service === "plausible") {
|
|
1322
|
+
ledger?.record({ kind: "plausible", project: event.project });
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1017
1327
|
}
|
|
1018
1328
|
catch (err) {
|
|
1019
|
-
console.log(chalk.yellow(` Couldn't auto-provision
|
|
1020
|
-
console.log(chalk.dim(` Run \`hatchkit add ${config.name}
|
|
1329
|
+
console.log(chalk.yellow(` Couldn't auto-provision analytics: ${err.message}`));
|
|
1330
|
+
console.log(chalk.dim(` Run \`hatchkit add ${config.name} ${analyticsServices.join(",")}\` once providers are reachable.`));
|
|
1021
1331
|
}
|
|
1022
1332
|
}
|
|
1023
1333
|
// Stripe: walk the user through pasting per-project keys (sk + pk
|
|
@@ -1170,6 +1480,7 @@ async function handleCreate() {
|
|
|
1170
1480
|
repoUrl: repoUrl ?? undefined,
|
|
1171
1481
|
serverPort: scaffoldResult?.ports.server,
|
|
1172
1482
|
clientPort: scaffoldResult?.ports.client,
|
|
1483
|
+
isPrivateRepo: isCreatedGithubRepoPrivate(config),
|
|
1173
1484
|
});
|
|
1174
1485
|
// Order matters: rollback iterates the ledger in REVERSE, so we
|
|
1175
1486
|
// record parent-before-child (project before app). Otherwise
|
|
@@ -1223,18 +1534,26 @@ async function handleCreate() {
|
|
|
1223
1534
|
if (repoUrl && config.scaffoldRepo) {
|
|
1224
1535
|
try {
|
|
1225
1536
|
const { findCoolifyAppsForProject } = await import("./deploy/coolify-app.js");
|
|
1226
|
-
const { repoSlugFromRemote, setCoolifyDeploySecrets } = await import("./deploy/gh-actions-secrets.js");
|
|
1537
|
+
const { ghSecretExists, repoSlugFromRemote, setCoolifyDeploySecrets } = await import("./deploy/gh-actions-secrets.js");
|
|
1227
1538
|
const slug = repoSlugFromRemote(repoUrl);
|
|
1228
1539
|
const apps = await findCoolifyAppsForProject(config.name);
|
|
1229
|
-
if (slug
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1540
|
+
if (slug) {
|
|
1541
|
+
if (apps.length > 0) {
|
|
1542
|
+
await setCoolifyDeploySecrets({
|
|
1543
|
+
projectDir: appDir,
|
|
1544
|
+
repoSlug: slug,
|
|
1545
|
+
apps,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
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.`));
|
|
1550
|
+
}
|
|
1551
|
+
const secretName = "DOTENV_PRIVATE_KEY_PRODUCTION";
|
|
1552
|
+
const preExisted = await ghSecretExists(appDir, slug, secretName);
|
|
1553
|
+
await pushProjectKeyToGh(config.name, slug);
|
|
1554
|
+
if (!preExisted) {
|
|
1555
|
+
ledger?.record({ kind: "ghActionsSecret", repo: slug, name: secretName });
|
|
1556
|
+
}
|
|
1238
1557
|
}
|
|
1239
1558
|
}
|
|
1240
1559
|
catch (err) {
|
|
@@ -1305,7 +1624,10 @@ async function handleCreate() {
|
|
|
1305
1624
|
// created the repo + `origin` but deliberately skipped the push.
|
|
1306
1625
|
if (config.scaffoldRepo && config.createGithubRepo && repoUrl) {
|
|
1307
1626
|
const { pushInitialBranch } = await import("./deploy/github.js");
|
|
1308
|
-
await pushInitialBranch(appDir);
|
|
1627
|
+
const pushed = await pushInitialBranch(appDir);
|
|
1628
|
+
if (pushed && config.deploymentMode === "coolify") {
|
|
1629
|
+
await configureGhcrForCreate(repoUrl, isCreatedGithubRepoPrivate(config), ledger);
|
|
1630
|
+
}
|
|
1309
1631
|
}
|
|
1310
1632
|
// Step 6.6: optional email forwarding setup (Cloudflare Email
|
|
1311
1633
|
// Routing). Opt-in prompt — most projects want it but a scripted
|
|
@@ -1470,6 +1792,48 @@ async function handleUpdate() {
|
|
|
1470
1792
|
console.log(chalk.yellow(" Run `pnpm install` to pick up @hatchkit/dev-plugin-next, then `hatchkit doctor` to confirm host plumbing."));
|
|
1471
1793
|
}
|
|
1472
1794
|
}
|
|
1795
|
+
async function handleServer() {
|
|
1796
|
+
const sub = args[1];
|
|
1797
|
+
if (sub !== "add") {
|
|
1798
|
+
console.log("Usage: hatchkit server add [--yes] [--dry-run] [--server-dir <path>]");
|
|
1799
|
+
console.log("Run `hatchkit help server` for details.");
|
|
1800
|
+
process.exit(1);
|
|
1801
|
+
}
|
|
1802
|
+
const { runServerAdd } = await import("./scaffold/server-add.js");
|
|
1803
|
+
const result = await runServerAdd(resolve("."), {
|
|
1804
|
+
yes: args.includes("--yes") || args.includes("-y"),
|
|
1805
|
+
dryRun: args.includes("--dry-run"),
|
|
1806
|
+
serverDir: flagValue("--server-dir"),
|
|
1807
|
+
sharedDir: flagValue("--shared-dir"),
|
|
1808
|
+
});
|
|
1809
|
+
if (args.includes("--json")) {
|
|
1810
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (result.dryRun) {
|
|
1814
|
+
console.log(chalk.yellow(" --dry-run — no files were changed."));
|
|
1815
|
+
}
|
|
1816
|
+
if (result.created.length > 0) {
|
|
1817
|
+
console.log(chalk.green(` ✓ Created: ${result.created.join(", ")}`));
|
|
1818
|
+
}
|
|
1819
|
+
if (result.updated.length > 0) {
|
|
1820
|
+
console.log(chalk.green(` ✓ Updated: ${result.updated.join(", ")}`));
|
|
1821
|
+
}
|
|
1822
|
+
if (result.reused.length > 0) {
|
|
1823
|
+
console.log(chalk.dim(` · Reused existing: ${result.reused.join(", ")}`));
|
|
1824
|
+
}
|
|
1825
|
+
for (const warning of result.warnings) {
|
|
1826
|
+
console.log(chalk.yellow(` ! ${warning}`));
|
|
1827
|
+
}
|
|
1828
|
+
if (result.skipped.length > 0 && !result.changed) {
|
|
1829
|
+
console.log(chalk.dim(` · ${result.skipped.join(", ")}`));
|
|
1830
|
+
}
|
|
1831
|
+
if (result.nextSteps.length > 0) {
|
|
1832
|
+
console.log(chalk.bold("\n Next:"));
|
|
1833
|
+
for (const step of result.nextSteps)
|
|
1834
|
+
console.log(` ${step}`);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1473
1837
|
async function handleConfig() {
|
|
1474
1838
|
const subcommand = args[1];
|
|
1475
1839
|
switch (subcommand) {
|
|
@@ -1477,7 +1841,7 @@ async function handleConfig() {
|
|
|
1477
1841
|
const provider = args[2];
|
|
1478
1842
|
if (!provider) {
|
|
1479
1843
|
console.log("Usage: hatchkit config add <provider>");
|
|
1480
|
-
console.log("Providers: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, resend, stripe");
|
|
1844
|
+
console.log("Providers: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, plausible, resend, search-console, stripe");
|
|
1481
1845
|
return;
|
|
1482
1846
|
}
|
|
1483
1847
|
// Handle provider setup based on name
|
|
@@ -1489,7 +1853,9 @@ async function handleConfig() {
|
|
|
1489
1853
|
case "dns":
|
|
1490
1854
|
case "glitchtip":
|
|
1491
1855
|
case "openpanel":
|
|
1856
|
+
case "plausible":
|
|
1492
1857
|
case "resend":
|
|
1858
|
+
case "search-console":
|
|
1493
1859
|
case "stripe":
|
|
1494
1860
|
case "ghcr":
|
|
1495
1861
|
await reconfigureProvider(provider);
|
|
@@ -1528,7 +1894,7 @@ async function handleConfig() {
|
|
|
1528
1894
|
default:
|
|
1529
1895
|
if (!isGpuPlatform(provider)) {
|
|
1530
1896
|
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"));
|
|
1897
|
+
console.log(chalk.dim(" Valid: coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate, glitchtip, openpanel, plausible, resend, search-console, stripe"));
|
|
1532
1898
|
return;
|
|
1533
1899
|
}
|
|
1534
1900
|
await reconfigureProvider(`gpu.${provider}`);
|
|
@@ -1673,6 +2039,37 @@ function printHelp(topic) {
|
|
|
1673
2039
|
|
|
1674
2040
|
${chalk.bold("Removal is not supported.")} Removing features could delete
|
|
1675
2041
|
user code — remove manually + edit the manifest.
|
|
2042
|
+
`);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (topic === "server") {
|
|
2046
|
+
console.log(`
|
|
2047
|
+
${chalk.bold("hatchkit server add")} — retrofit a server into a client-only project
|
|
2048
|
+
|
|
2049
|
+
${chalk.bold("Usage:")}
|
|
2050
|
+
cd <project-dir> && hatchkit server add
|
|
2051
|
+
cd <project-dir> && hatchkit server add --yes
|
|
2052
|
+
|
|
2053
|
+
${chalk.bold("What it does:")}
|
|
2054
|
+
Reads .hatchkit.json, copies the Hatchkit server package from the
|
|
2055
|
+
starter, restores shared server types, updates root scripts/workspace
|
|
2056
|
+
files, flips manifest surfaces from ${chalk.cyan("client-only")} to
|
|
2057
|
+
${chalk.cyan("both")}, and switches gh-pages projects back to coolify.
|
|
2058
|
+
|
|
2059
|
+
${chalk.bold("What it does not do:")}
|
|
2060
|
+
No provider calls. No Coolify, DNS, GitHub, keychain, or Terraform
|
|
2061
|
+
mutation. To wire deploy infra after the local scaffold:
|
|
2062
|
+
|
|
2063
|
+
hatchkit adopt --resume --regenerate-pipeline
|
|
2064
|
+
|
|
2065
|
+
${chalk.bold("Options:")}
|
|
2066
|
+
--server-dir <path> Destination for the server package. Default:
|
|
2067
|
+
${chalk.dim("packages/server")}.
|
|
2068
|
+
--shared-dir <path> Destination for the shared package. Default:
|
|
2069
|
+
${chalk.dim("packages/shared")}.
|
|
2070
|
+
--yes, -y Skip confirmation.
|
|
2071
|
+
--dry-run Show planned local changes without writing.
|
|
2072
|
+
--json Machine-readable result.
|
|
1676
2073
|
`);
|
|
1677
2074
|
return;
|
|
1678
2075
|
}
|
|
@@ -1792,20 +2189,22 @@ function printHelp(topic) {
|
|
|
1792
2189
|
}
|
|
1793
2190
|
if (topic === "dev-setup") {
|
|
1794
2191
|
console.log(`
|
|
1795
|
-
${chalk.bold("hatchkit dev-setup")} —
|
|
2192
|
+
${chalk.bold("hatchkit dev-setup")} — Tailscale-served dev URLs
|
|
1796
2193
|
|
|
1797
2194
|
Wires up the host-wide plumbing that makes every scaffolded project
|
|
1798
2195
|
reachable from any Tailscale peer at:
|
|
1799
2196
|
|
|
1800
|
-
${chalk.cyan("https://<slug>.local
|
|
2197
|
+
${chalk.cyan("https://<slug>.local.<your-domain>/")}
|
|
1801
2198
|
|
|
1802
2199
|
…without per-project DNS, port juggling, or framework basePath config.
|
|
1803
2200
|
|
|
1804
2201
|
${chalk.bold("Host-wide subcommands (run once per machine):")}
|
|
1805
2202
|
dev-setup init [--force] Auto-write ~/.config/dev/Caddyfile, register
|
|
1806
2203
|
a launchd job to run Caddy on a free port
|
|
1807
|
-
(default 9443, auto-bumps on collision),
|
|
1808
|
-
register a tailscale serve TCP=443 bridge
|
|
2204
|
+
(default 9443, auto-bumps on collision),
|
|
2205
|
+
register a tailscale serve TCP=443 bridge,
|
|
2206
|
+
and auto-upsert the wildcard DNS A record
|
|
2207
|
+
when Cloudflare credentials are available.
|
|
1809
2208
|
Idempotent — safe to re-run.
|
|
1810
2209
|
dev-setup status Print the same Local-dev rows that
|
|
1811
2210
|
${chalk.cyan("hatchkit doctor")} would show.
|
|
@@ -1825,8 +2224,15 @@ function printHelp(topic) {
|
|
|
1825
2224
|
next.config + dep in place (they're inert
|
|
1826
2225
|
without the fragment).
|
|
1827
2226
|
|
|
1828
|
-
${chalk.bold("
|
|
1829
|
-
|
|
2227
|
+
${chalk.bold("DNS:")}
|
|
2228
|
+
${chalk.cyan("dev-setup init")} auto-upserts a DNS-only A record on a
|
|
2229
|
+
dedicated ${chalk.cyan("local.")} subdomain:
|
|
2230
|
+
*.local.<your-domain> A <your-tailnet-ip> (DNS-only)
|
|
2231
|
+
|
|
2232
|
+
Custom ${chalk.cyan("--domain")} values must use that ${chalk.cyan("local.")} prefix so
|
|
2233
|
+
Hatchkit never overwrites a production wildcard such as *.example.com.
|
|
2234
|
+
|
|
2235
|
+
If Cloudflare credentials are unavailable, add that record manually.
|
|
1830
2236
|
|
|
1831
2237
|
This feature is fully optional: until you run ${chalk.cyan("dev-setup init")},
|
|
1832
2238
|
${chalk.cyan("hatchkit doctor")} surfaces zero Local-dev rows. Within a project,
|
|
@@ -1864,7 +2270,7 @@ function printHelp(topic) {
|
|
|
1864
2270
|
· App fqdn references an apex with no Cloudflare zone
|
|
1865
2271
|
· R2 bucket follows the \`<project>-<role>\` convention but has no
|
|
1866
2272
|
matching Coolify app (orphan from a destroyed project)
|
|
1867
|
-
· GlitchTip / OpenPanel project with no Coolify app counterpart
|
|
2273
|
+
· GlitchTip / OpenPanel / Plausible project/site with no Coolify app counterpart
|
|
1868
2274
|
· Cloudflare zone with no Coolify app pointing into it
|
|
1869
2275
|
|
|
1870
2276
|
${chalk.bold("Flags:")}
|
|
@@ -1940,19 +2346,29 @@ function printHelp(topic) {
|
|
|
1940
2346
|
|
|
1941
2347
|
${chalk.bold("Usage:")}
|
|
1942
2348
|
hatchkit add [<project-name>] [<services>] [flags]
|
|
2349
|
+
hatchkit add [<services>] [flags] ${chalk.dim("(inside a project with .hatchkit.json)")}
|
|
1943
2350
|
|
|
1944
2351
|
${chalk.bold("What it does:")}
|
|
1945
2352
|
· GlitchTip / OpenPanel: ${chalk.bold("one project per product")}, events tagged by
|
|
1946
2353
|
\`environment\` so dev / staging / prod share the same dashboard.
|
|
1947
|
-
|
|
2354
|
+
· Plausible: one site for the public project domain, with browser tracker env.
|
|
2355
|
+
Observability values are written to ${chalk.cyan(".env.production")} only — dev noise pollutes real metrics.
|
|
1948
2356
|
Pass ${chalk.cyan("--enable-dev-obs")} to populate ${chalk.cyan(".env.development")} too.
|
|
1949
2357
|
· Resend: separate ${chalk.cyan("-dev")} and ${chalk.cyan("-prod")} API keys (audience
|
|
1950
2358
|
safety). Written to the server's dev + prod env respectively.
|
|
2359
|
+
· Search Console: verifies the project domain via Cloudflare DNS TXT,
|
|
2360
|
+
then adds the ${chalk.cyan("sc-domain:<domain>")} property to your Google account.
|
|
2361
|
+
No runtime env is written.
|
|
1951
2362
|
· ${chalk.cyan(".env.production")} is dotenvx-encrypted — commit-safe.
|
|
1952
2363
|
${chalk.cyan(".env.development")} is plaintext — gitignored, not encrypted.
|
|
1953
2364
|
· A 0600 cache of every value is saved under
|
|
1954
2365
|
${chalk.dim("<config-dir>/provisioned/<project>.*.env")} for recoverability.
|
|
1955
2366
|
${chalk.dim("Secret values never hit stdout.")}
|
|
2367
|
+
· The interactive menu only shows services not already present for the
|
|
2368
|
+
current project, starts with nothing selected, and refuses explicit
|
|
2369
|
+
requests that would recreate known resources.
|
|
2370
|
+
· Before creating provider resources, add runs read-only existence probes
|
|
2371
|
+
for the selected services and stops on conflicts so cleanup stays safe.
|
|
1956
2372
|
|
|
1957
2373
|
${chalk.bold("Surfaces:")}
|
|
1958
2374
|
hatchkit asks which surfaces your project has. Options:
|
|
@@ -1967,7 +2383,10 @@ function printHelp(topic) {
|
|
|
1967
2383
|
${chalk.bold("Services:")}
|
|
1968
2384
|
glitchtip GLITCHTIP_DSN (server) / PUBLIC_GLITCHTIP_DSN (client)
|
|
1969
2385
|
openpanel OPENPANEL_* (server) / PUBLIC_OPENPANEL_* (client)
|
|
2386
|
+
plausible NEXT_PUBLIC_PLAUSIBLE_DOMAIN / *_SCRIPT_URL (client only)
|
|
1970
2387
|
resend RESEND_API_KEY (server only)
|
|
2388
|
+
search-console
|
|
2389
|
+
Google Search Console domain property (DNS verification; no env)
|
|
1971
2390
|
s3 R2_<BUCKET>_ACCESS_KEY_ID / *_SECRET_ACCESS_KEY / *_BUCKET / R2_ENDPOINT
|
|
1972
2391
|
— mints a per-bucket scoped Cloudflare R2 API token for every
|
|
1973
2392
|
bucket declared in .hatchkit.json (s3Buckets). Single-bucket
|
|
@@ -1985,6 +2404,7 @@ function printHelp(topic) {
|
|
|
1985
2404
|
|
|
1986
2405
|
${chalk.bold("Examples:")}
|
|
1987
2406
|
hatchkit add
|
|
2407
|
+
hatchkit add search-console
|
|
1988
2408
|
hatchkit add raptor-runner
|
|
1989
2409
|
hatchkit add raptor-runner all --enable-dev-obs
|
|
1990
2410
|
hatchkit add raptor-runner glitchtip,resend --no-write
|
|
@@ -2016,8 +2436,10 @@ function printHelp(topic) {
|
|
|
2016
2436
|
· Writes \`.hatchkit.json\` so \`update\`, \`add\`, \`keys\` recognise
|
|
2017
2437
|
the project.
|
|
2018
2438
|
· ${chalk.cyan("GitHub remote")} — \`git init\` (if needed),
|
|
2019
|
-
commit, \`gh repo create --private --source=. --push\`.
|
|
2020
|
-
|
|
2439
|
+
commit, \`gh repo create --private|--public --source=. --push\`.
|
|
2440
|
+
Visibility is prompted (default private) or set with
|
|
2441
|
+
\`--github-visibility private|public\`. Skipped when an \`origin\`
|
|
2442
|
+
is already set.
|
|
2021
2443
|
· ${chalk.cyan("Coolify + DNS")} — direct REST-API calls into the
|
|
2022
2444
|
Coolify and Cloudflare you already configured (no Terraform,
|
|
2023
2445
|
no submodule). Finds or creates the Coolify project, picks
|
|
@@ -2027,7 +2449,7 @@ function printHelp(topic) {
|
|
|
2027
2449
|
(DOTENV_PRIVATE_KEY_PRODUCTION + GITHUB_REPO_URL), upserts an
|
|
2028
2450
|
A record \`<domain> → <server-ip>\` on Cloudflare, and triggers
|
|
2029
2451
|
the first deploy. Defaults ON when no matching app exists.
|
|
2030
|
-
· Optionally provisions GlitchTip / OpenPanel / Resend clients
|
|
2452
|
+
· Optionally provisions GlitchTip / OpenPanel / Plausible / Resend clients
|
|
2031
2453
|
(same machinery as \`hatchkit add\`).
|
|
2032
2454
|
· Optionally pushes the dotenvx private key to Coolify
|
|
2033
2455
|
(redundant when the Coolify+DNS step ran — it already does).
|
|
@@ -2086,7 +2508,11 @@ function printHelp(topic) {
|
|
|
2086
2508
|
${chalk.bold("Services:")}
|
|
2087
2509
|
glitchtip Deletes the GlitchTip project
|
|
2088
2510
|
openpanel Deletes the OpenPanel project (and clears cached creds)
|
|
2511
|
+
plausible Deletes the Plausible site cached for this project
|
|
2089
2512
|
resend Finds API keys by name and deletes them
|
|
2513
|
+
search-console
|
|
2514
|
+
Removes the Search Console property from your Google account
|
|
2515
|
+
(keeps DNS verification token / ownership state)
|
|
2090
2516
|
s3 Deletes per-bucket scoped Cloudflare R2 API tokens
|
|
2091
2517
|
(clears the keychain entries and DELETEs upstream)
|
|
2092
2518
|
|
|
@@ -2124,7 +2550,7 @@ function printHelp(topic) {
|
|
|
2124
2550
|
create + adopt:
|
|
2125
2551
|
- GitHub repo ${chalk.dim("gh repo delete")}
|
|
2126
2552
|
- dotenvx private key in keychain ${chalk.dim("keytar deletePassword")}
|
|
2127
|
-
- GlitchTip / OpenPanel / Resend
|
|
2553
|
+
- GlitchTip / OpenPanel / Plausible / Resend ${chalk.dim("DELETE")} per-vendor
|
|
2128
2554
|
- Coolify app / project / database ${chalk.dim("DELETE /api/v1/...")}
|
|
2129
2555
|
|
|
2130
2556
|
adopt-only (fine-grained, never wider than what adopt itself wrote):
|
|
@@ -2269,7 +2695,7 @@ function printHelp(topic) {
|
|
|
2269
2695
|
config Show status of every configured provider (alias: \`status\`)
|
|
2270
2696
|
config add <p> Configure a provider
|
|
2271
2697
|
(coolify, ghcr, hetzner, dns, s3, modal, runpod, hf, replicate,
|
|
2272
|
-
glitchtip, openpanel, resend, stripe)
|
|
2698
|
+
glitchtip, openpanel, plausible, resend, search-console, stripe)
|
|
2273
2699
|
config reset Clear ALL CLI config (providers, tokens, ML registry, ports)
|
|
2274
2700
|
`);
|
|
2275
2701
|
return;
|
|
@@ -2302,13 +2728,13 @@ function printHelp(topic) {
|
|
|
2302
2728
|
}
|
|
2303
2729
|
if (topic === "assets") {
|
|
2304
2730
|
console.log(`
|
|
2305
|
-
${chalk.bold("hatchkit assets")} — move bytes between local
|
|
2731
|
+
${chalk.bold("hatchkit assets")} — move bytes between local S3 and prod buckets
|
|
2306
2732
|
|
|
2307
2733
|
${chalk.bold("Subcommands:")}
|
|
2308
|
-
assets seed [--from <dir>] Local dir → local
|
|
2734
|
+
assets seed [--from <dir>] Local dir → local S3 bucket.
|
|
2309
2735
|
Defaults to ./seed/assets.
|
|
2310
|
-
assets push [--bucket assets|state] Local
|
|
2311
|
-
assets pull [--bucket assets|state] Prod bucket → local
|
|
2736
|
+
assets push [--bucket assets|state] Local S3 → prod bucket.
|
|
2737
|
+
assets pull [--bucket assets|state] Prod bucket → local S3.
|
|
2312
2738
|
Caution: prod data may include PII.
|
|
2313
2739
|
assets migrate --from-endpoint=URL External S3 → prod bucket.
|
|
2314
2740
|
--from-bucket=NAME The adoption escape hatch — copy
|
|
@@ -2335,7 +2761,7 @@ function printHelp(topic) {
|
|
|
2335
2761
|
the env doesn't carry them (R2's URL-driven assets bucket).
|
|
2336
2762
|
|
|
2337
2763
|
${chalk.bold("Examples:")}
|
|
2338
|
-
hatchkit assets seed # ./seed/assets/ → local
|
|
2764
|
+
hatchkit assets seed # ./seed/assets/ → local S3
|
|
2339
2765
|
hatchkit assets push --dry-run # see what would ship to prod
|
|
2340
2766
|
hatchkit assets push # actually ship it
|
|
2341
2767
|
hatchkit assets migrate --from-endpoint https://nyc3.digitaloceanspaces.com \\
|
|
@@ -2374,8 +2800,9 @@ function printHelp(topic) {
|
|
|
2374
2800
|
create Scaffold a new project (interactive)
|
|
2375
2801
|
adopt Bring an existing project under hatchkit management (run in project dir)
|
|
2376
2802
|
update Add features to an already-scaffolded project (run in project dir)
|
|
2377
|
-
add
|
|
2378
|
-
|
|
2803
|
+
server add Retrofit a server into a client-only project
|
|
2804
|
+
add Create GlitchTip / OpenPanel / Plausible / Resend clients for an existing project
|
|
2805
|
+
assets Move bytes between local S3 and prod buckets (seed/push/pull/migrate)
|
|
2379
2806
|
remove Delete the -dev/-prod clients created by 'add' (inverse of add)
|
|
2380
2807
|
destroy Roll back everything ${chalk.cyan("hatchkit create")} did for a project
|
|
2381
2808
|
rename-domain Move a scaffolded project to a new domain (rewrites tfvars/env/manifest)
|
|
@@ -2408,7 +2835,12 @@ function printHelp(topic) {
|
|
|
2408
2835
|
--yes, -y (with \`create\`) skip prompts, use defaults / --config values
|
|
2409
2836
|
--config <path> (with \`create\`) load JSON overrides for ProjectConfig fields
|
|
2410
2837
|
--name <name> (with \`create\`) set project name without prompting
|
|
2838
|
+
--local-dev[=<slug>] (with \`create\`) enable Tailscale dev URL, optionally with slug
|
|
2839
|
+
--no-local-dev (with \`create\`) skip local-dev wiring
|
|
2411
2840
|
--no-github (with \`create\`) skip GitHub repo creation
|
|
2841
|
+
--github-visibility {private|public}
|
|
2842
|
+
(with \`create\`) visibility for a newly-created GitHub repo.
|
|
2843
|
+
Default: private. Shorthands: \`--private\`, \`--public\`.
|
|
2412
2844
|
--no-deploy (with \`create\`) skip Terraform/Coolify/ML deployment
|
|
2413
2845
|
|
|
2414
2846
|
${chalk.bold("Environment:")}
|