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.
- 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/completion.d.ts.map +1 -1
- package/dist/completion.js +19 -1
- 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 +10 -4
- package/dist/dev-setup.d.ts.map +1 -1
- package/dist/dev-setup.js +166 -57
- 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 +8 -7
- package/dist/explain.js.map +1 -1
- package/dist/index.js +277 -60
- 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/index.d.ts +21 -3
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +112 -5
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/plausible.d.ts +10 -0
- package/dist/provision/plausible.d.ts.map +1 -0
- package/dist/provision/plausible.js +103 -0
- package/dist/provision/plausible.js.map +1 -0
- 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 +4 -1
- 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 +4 -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 +273 -0
- package/dist/scaffold/server-add.js.map +1 -0
- 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/dev-setup.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* `hatchkit dev-setup` — opt-in Tailscale-served dev URLs.
|
|
3
3
|
*
|
|
4
4
|
* Goal: every scaffolded project reachable from any Tailscale peer at
|
|
5
|
-
* https://<slug>.local
|
|
5
|
+
* https://<slug>.local.<project-domain>/ with no per-project DNS work,
|
|
6
6
|
* no port juggling, no app-side base/basePath config, and zero
|
|
7
7
|
* collisions between projects.
|
|
8
8
|
*
|
|
9
9
|
* Architecture (host-wide one-time setup):
|
|
10
10
|
*
|
|
11
|
-
* phone ──HTTPS──▶ <slug>.local
|
|
12
|
-
* │ DNS
|
|
11
|
+
* phone ──HTTPS──▶ <slug>.local.<project-domain>:443
|
|
12
|
+
* │ DNS A → laptop's 100.x tailnet IP
|
|
13
13
|
* ▼
|
|
14
14
|
* tailscale serve --tcp=443 (raw TCP passthrough)
|
|
15
15
|
* ▼
|
|
@@ -29,9 +29,9 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
29
29
|
import { createServer } from "node:net";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { join } from "node:path";
|
|
32
|
-
import { CADDYFILE_PATH, DEV_CONFIG_DIR,
|
|
32
|
+
import { CADDYFILE_PATH, DEV_CONFIG_DIR, LOCAL_DEV_DOMAIN, LOCAL_DEV_DOMAIN_WILDCARD, MANAGED_MARKER, PROJECTS_DIR, isLocalDevActive, localDevDomainFromProjectDomain, localDevUrl, normaliseLocalDevDomain, projectFragmentPath, readCaddyLocalDevDomain, readCaddyPort, removeProjectFragment, tailscaleIdentity, tailscaleServeTcpTarget, writeProjectFragment, } from "@hatchkit/dev-shared";
|
|
33
33
|
import { exec, execOk } from "./utils/exec.js";
|
|
34
|
-
export { CADDYFILE_PATH, DEV_CONFIG_DIR, LOCAL_DEV_DOMAIN, LOCAL_DEV_DOMAIN_WILDCARD, MANAGED_MARKER, PROJECTS_DIR, isLocalDevActive, readCaddyPort, tailscaleIdentity, tailscaleServeTcpTarget, };
|
|
34
|
+
export { CADDYFILE_PATH, DEV_CONFIG_DIR, LOCAL_DEV_DOMAIN, LOCAL_DEV_DOMAIN_WILDCARD, MANAGED_MARKER, PROJECTS_DIR, isLocalDevActive, readCaddyLocalDevDomain, readCaddyPort, tailscaleIdentity, tailscaleServeTcpTarget, };
|
|
35
35
|
export const CADDY_LOG_PATH = join(DEV_CONFIG_DIR, "caddy.log");
|
|
36
36
|
export const CADDY_ERR_LOG_PATH = join(DEV_CONFIG_DIR, "caddy.err.log");
|
|
37
37
|
export const CADDY_WRAPPER_PATH = join(DEV_CONFIG_DIR, "caddy-wrapper.sh");
|
|
@@ -50,7 +50,9 @@ const CADDY_PORT_BUMP_LIMIT = 50;
|
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// Caddyfile + launchd plist contents
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
|
-
export function caddyfileContents(caddyPort) {
|
|
53
|
+
export function caddyfileContents(caddyPort, localDevDomain = LOCAL_DEV_DOMAIN) {
|
|
54
|
+
const domain = normaliseLocalDevDomain(localDevDomain) ?? LOCAL_DEV_DOMAIN;
|
|
55
|
+
const wildcard = `*.${domain}`;
|
|
54
56
|
return `${MANAGED_MARKER}. Edit at your own risk — re-running
|
|
55
57
|
# \`hatchkit dev-setup init\` will overwrite this file (delete the marker
|
|
56
58
|
# line above to keep your edits across re-runs; doctor will then skip
|
|
@@ -66,11 +68,11 @@ export function caddyfileContents(caddyPort) {
|
|
|
66
68
|
auto_https disable_redirects
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
https://${
|
|
71
|
+
https://${wildcard}:${caddyPort} {
|
|
70
72
|
bind 127.0.0.1
|
|
71
73
|
# No explicit \`tls\` directive: the site address already names the
|
|
72
74
|
# wildcard subject, and the global \`acme_dns cloudflare\` block
|
|
73
|
-
# drives DNS-01 issuance. Adding \`tls
|
|
75
|
+
# drives DNS-01 issuance. Adding an explicit wildcard \`tls\` arg here
|
|
74
76
|
# would be parsed as the email-or-keyword form and Caddy 2 rejects
|
|
75
77
|
# it ("single argument must either be 'internal', 'force_automate',
|
|
76
78
|
# or an email address").
|
|
@@ -416,6 +418,8 @@ export async function checkLocalDevHost() {
|
|
|
416
418
|
export async function runDevSetupInit(opts = {}) {
|
|
417
419
|
if (!existsSync(PROJECTS_DIR))
|
|
418
420
|
mkdirSync(PROJECTS_DIR, { recursive: true });
|
|
421
|
+
const localDevDomain = normaliseLocalDevDomain(opts.localDevDomain) ??
|
|
422
|
+
(isLocalDevActive() ? readCaddyLocalDevDomain() : LOCAL_DEV_DOMAIN);
|
|
419
423
|
let caddyPort = opts.force ? null : readCaddyPort();
|
|
420
424
|
if (caddyPort === null) {
|
|
421
425
|
caddyPort = await pickFreeCaddyPort();
|
|
@@ -428,7 +432,7 @@ export async function runDevSetupInit(opts = {}) {
|
|
|
428
432
|
// unless --force is set; the user may be running their own dev domain
|
|
429
433
|
// setup we shouldn't trample on first encounter.
|
|
430
434
|
let wroteCaddyfile = false;
|
|
431
|
-
const nextCaddyfile = caddyfileContents(caddyPort);
|
|
435
|
+
const nextCaddyfile = caddyfileContents(caddyPort, localDevDomain);
|
|
432
436
|
if (existsSync(CADDYFILE_PATH)) {
|
|
433
437
|
const existing = readFileSync(CADDYFILE_PATH, "utf-8");
|
|
434
438
|
if (!existing.includes(MANAGED_MARKER) && !opts.force) {
|
|
@@ -483,7 +487,8 @@ export async function runDevSetupInit(opts = {}) {
|
|
|
483
487
|
wroteWrapper = true;
|
|
484
488
|
}
|
|
485
489
|
const nextPlist = launchdPlistContentsWrapped(CADDY_WRAPPER_PATH);
|
|
486
|
-
if (!existsSync(LAUNCHD_PLIST_PATH) ||
|
|
490
|
+
if (!existsSync(LAUNCHD_PLIST_PATH) ||
|
|
491
|
+
readFileSync(LAUNCHD_PLIST_PATH, "utf-8") !== nextPlist) {
|
|
487
492
|
writeFileSync(LAUNCHD_PLIST_PATH, nextPlist);
|
|
488
493
|
wrotePlist = true;
|
|
489
494
|
}
|
|
@@ -501,7 +506,8 @@ export async function runDevSetupInit(opts = {}) {
|
|
|
501
506
|
notes.push(`For keychain-backed storage: \`security add-generic-password -s ${DEFAULT_CADDY_KEYCHAIN_SERVICE} -a ${DEFAULT_CADDY_KEYCHAIN_ACCOUNT} -w '<token>' -U\` then re-run \`dev-setup init\`.`);
|
|
502
507
|
}
|
|
503
508
|
const nextPlist = launchdPlistContents(caddyBinPath, token ?? null);
|
|
504
|
-
if (!existsSync(LAUNCHD_PLIST_PATH) ||
|
|
509
|
+
if (!existsSync(LAUNCHD_PLIST_PATH) ||
|
|
510
|
+
readFileSync(LAUNCHD_PLIST_PATH, "utf-8") !== nextPlist) {
|
|
505
511
|
writeFileSync(LAUNCHD_PLIST_PATH, nextPlist);
|
|
506
512
|
wrotePlist = true;
|
|
507
513
|
}
|
|
@@ -528,7 +534,7 @@ export async function runDevSetupInit(opts = {}) {
|
|
|
528
534
|
registeredServe = true;
|
|
529
535
|
}
|
|
530
536
|
}
|
|
531
|
-
// DNS: ensure
|
|
537
|
+
// DNS: ensure the local-dev wildcard → laptop's tailnet IP (A record,
|
|
532
538
|
// DNS-only). Without this every project URL hits whatever the parent
|
|
533
539
|
// zone's wildcard says (typically the Coolify host IP, proxied — which
|
|
534
540
|
// doesn't reach the tailnet) and phone requests die at the TLS
|
|
@@ -536,8 +542,9 @@ export async function runDevSetupInit(opts = {}) {
|
|
|
536
542
|
// but only when each peer has Tailscale's resolver in front of its
|
|
537
543
|
// public DNS — fragile on iOS, where stub resolvers cache the
|
|
538
544
|
// intermediate NXDOMAIN. Direct A record is the bulletproof shape.
|
|
539
|
-
const dnsRecord = await ensureLocalDevDnsRecord(opts, notes);
|
|
545
|
+
const dnsRecord = await ensureLocalDevDnsRecord({ ...opts, localDevDomain }, notes);
|
|
540
546
|
return {
|
|
547
|
+
localDevDomain,
|
|
541
548
|
caddyPort,
|
|
542
549
|
wroteCaddyfile,
|
|
543
550
|
wrotePlist,
|
|
@@ -548,11 +555,13 @@ export async function runDevSetupInit(opts = {}) {
|
|
|
548
555
|
notes,
|
|
549
556
|
};
|
|
550
557
|
}
|
|
551
|
-
/** Upsert the
|
|
558
|
+
/** Upsert the local-dev wildcard A record to the laptop's current
|
|
552
559
|
* tailnet IP. Idempotent. Returns the action taken (or a reason it
|
|
553
560
|
* was skipped). All failures are non-fatal — they only nudge the
|
|
554
561
|
* user toward the manual command. */
|
|
555
562
|
async function ensureLocalDevDnsRecord(opts, notes) {
|
|
563
|
+
const localDevDomain = normaliseLocalDevDomain(opts.localDevDomain) ?? LOCAL_DEV_DOMAIN;
|
|
564
|
+
const wildcard = `*.${localDevDomain}`;
|
|
556
565
|
const identity = await tailscaleIdentity();
|
|
557
566
|
if (!identity) {
|
|
558
567
|
notes.push("DNS record skipped: tailscale daemon offline (no IP to point the record at).");
|
|
@@ -577,14 +586,14 @@ async function ensureLocalDevDnsRecord(opts, notes) {
|
|
|
577
586
|
try {
|
|
578
587
|
const { CloudflareApi } = await import("./utils/cloudflare-api.js");
|
|
579
588
|
const cf = new CloudflareApi({ token });
|
|
580
|
-
const zone = await resolveZoneForName(cf,
|
|
589
|
+
const zone = await resolveZoneForName(cf, wildcard);
|
|
581
590
|
if (!zone) {
|
|
582
|
-
notes.push(`DNS record skipped: no Cloudflare zone found for ${
|
|
591
|
+
notes.push(`DNS record skipped: no Cloudflare zone found for ${localDevDomain}. The token may lack Zone:Zone:Read scope.`);
|
|
583
592
|
return "failed";
|
|
584
593
|
}
|
|
585
594
|
const upsert = await cf.upsertRecord(zone.id, {
|
|
586
595
|
type: "A",
|
|
587
|
-
name:
|
|
596
|
+
name: wildcard,
|
|
588
597
|
content: identity.ip,
|
|
589
598
|
proxied: false,
|
|
590
599
|
ttl: 60,
|
|
@@ -601,9 +610,9 @@ async function ensureLocalDevDnsRecord(opts, notes) {
|
|
|
601
610
|
}
|
|
602
611
|
}
|
|
603
612
|
/** Walk the candidate label chain looking for a Cloudflare zone we can
|
|
604
|
-
* manage. For `*.local.
|
|
613
|
+
* manage. For `*.local.example.com` this tries `local.example.com`
|
|
605
614
|
* first (in case the user has it delegated as its own zone), then
|
|
606
|
-
* `
|
|
615
|
+
* `example.com`, then `com` (which won't be ours but completes the
|
|
607
616
|
* chain symmetrically). Returns the first hit. */
|
|
608
617
|
async function resolveZoneForName(cf, name) {
|
|
609
618
|
const labels = name.replace(/^\*\./, "").split(".");
|
|
@@ -615,39 +624,39 @@ async function resolveZoneForName(cf, name) {
|
|
|
615
624
|
}
|
|
616
625
|
return null;
|
|
617
626
|
}
|
|
618
|
-
/** Probe the live
|
|
627
|
+
/** Probe the live local-dev wildcard Cloudflare record. Returns
|
|
619
628
|
* null when there's no token to check with — that's a configuration
|
|
620
629
|
* state, not a failure. Otherwise reports drift between the record
|
|
621
630
|
* and the laptop's current tailnet IP. */
|
|
622
631
|
async function checkLocalDevDnsRecord(currentIp) {
|
|
632
|
+
const localDevDomain = readCaddyLocalDevDomain();
|
|
633
|
+
const wildcard = `*.${localDevDomain}`;
|
|
623
634
|
const token = (await readCloudflareTokenFromConfig()) ?? (await readCloudflareTokenFromKeychain());
|
|
624
635
|
if (!token)
|
|
625
636
|
return null;
|
|
626
637
|
try {
|
|
627
638
|
const { CloudflareApi } = await import("./utils/cloudflare-api.js");
|
|
628
639
|
const cf = new CloudflareApi({ token });
|
|
629
|
-
const zone = await resolveZoneForName(cf,
|
|
640
|
+
const zone = await resolveZoneForName(cf, wildcard);
|
|
630
641
|
if (!zone) {
|
|
631
642
|
return {
|
|
632
643
|
name: `Local-dev / DNS A record`,
|
|
633
644
|
status: "fail",
|
|
634
|
-
detail: `no Cloudflare zone found for ${
|
|
645
|
+
detail: `no Cloudflare zone found for ${localDevDomain}`,
|
|
635
646
|
hint: [
|
|
636
647
|
"Token may lack Zone:Zone:Read on the parent zone, or the zone isn't on Cloudflare.",
|
|
637
648
|
"Add the record manually if you're using a different DNS provider:",
|
|
638
|
-
`
|
|
649
|
+
` ${wildcard} A ${currentIp} (DNS-only / unproxied, TTL 60)`,
|
|
639
650
|
],
|
|
640
651
|
};
|
|
641
652
|
}
|
|
642
|
-
const record = await cf.findRecord(zone.id,
|
|
653
|
+
const record = await cf.findRecord(zone.id, wildcard, "A");
|
|
643
654
|
if (!record) {
|
|
644
655
|
return {
|
|
645
656
|
name: "Local-dev / DNS A record",
|
|
646
657
|
status: "fail",
|
|
647
|
-
detail: `no A record for ${
|
|
648
|
-
hint: [
|
|
649
|
-
"Run `hatchkit dev-setup init` to create it automatically.",
|
|
650
|
-
],
|
|
658
|
+
detail: `no A record for ${wildcard} in zone ${zone.name}`,
|
|
659
|
+
hint: ["Run `hatchkit dev-setup init` to create it automatically."],
|
|
651
660
|
};
|
|
652
661
|
}
|
|
653
662
|
if (record.content !== currentIp) {
|
|
@@ -674,7 +683,7 @@ async function checkLocalDevDnsRecord(currentIp) {
|
|
|
674
683
|
return {
|
|
675
684
|
name: "Local-dev / DNS A record",
|
|
676
685
|
status: "ok",
|
|
677
|
-
detail: `${
|
|
686
|
+
detail: `${wildcard} → ${currentIp} (DNS-only)`,
|
|
678
687
|
};
|
|
679
688
|
}
|
|
680
689
|
catch (err) {
|
|
@@ -747,10 +756,24 @@ async function pluginVersionRange() {
|
|
|
747
756
|
* safe to call from scaffold (first run) AND from `dev-setup enable`
|
|
748
757
|
* on an already-wired project. */
|
|
749
758
|
export async function enableProjectLocalDev(input) {
|
|
750
|
-
const
|
|
759
|
+
const manifest = await (async () => {
|
|
760
|
+
try {
|
|
761
|
+
const { readManifest } = await import("./scaffold/manifest.js");
|
|
762
|
+
return readManifest(input.projectDir);
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
})();
|
|
768
|
+
const localDevDomain = normaliseLocalDevDomain(input.localDevDomain) ??
|
|
769
|
+
normaliseLocalDevDomain(manifest?.localDev?.domain) ??
|
|
770
|
+
localDevDomainFromProjectDomain(manifest?.domain) ??
|
|
771
|
+
LOCAL_DEV_DOMAIN;
|
|
772
|
+
const wroteFragment = writeProjectFragment(input.slug, input.devPort, localDevDomain);
|
|
751
773
|
const docsPath = join(input.projectDir, "docs", "dev-setup.md");
|
|
752
774
|
const docsContent = renderDevSetupDocs({
|
|
753
775
|
slug: input.slug,
|
|
776
|
+
localDevDomain,
|
|
754
777
|
tailscale: await tailscaleIdentity(),
|
|
755
778
|
});
|
|
756
779
|
let wroteDocs = false;
|
|
@@ -954,8 +977,10 @@ function patchPluginPackageJsonDep(projectDir, versionRange, framework) {
|
|
|
954
977
|
return "added";
|
|
955
978
|
}
|
|
956
979
|
export function renderDevSetupDocs(input) {
|
|
980
|
+
const localDevDomain = normaliseLocalDevDomain(input.localDevDomain) ?? LOCAL_DEV_DOMAIN;
|
|
981
|
+
const wildcard = `*.${localDevDomain}`;
|
|
957
982
|
const tailnetHostname = input.tailscale?.fullName ?? "<your-machine>.<tailnet>.ts.net";
|
|
958
|
-
const url =
|
|
983
|
+
const url = localDevUrl(input.slug, localDevDomain);
|
|
959
984
|
return `# Dev URL setup (\`${url}\`)
|
|
960
985
|
|
|
961
986
|
This project ships with the **hatchkit local-dev** integration: when you run
|
|
@@ -973,20 +998,25 @@ framework \`base\` / \`basePath\` config.
|
|
|
973
998
|
|
|
974
999
|
## One-time host setup
|
|
975
1000
|
|
|
976
|
-
Do this **once per machine**, not per project.
|
|
977
|
-
|
|
1001
|
+
Do this **once per machine**, not per project. New hatchkit projects opt in by
|
|
1002
|
+
default, so after the host is wired they just work.
|
|
1003
|
+
|
|
1004
|
+
\`\`\`
|
|
1005
|
+
hatchkit dev-setup init --domain ${localDevDomain}
|
|
1006
|
+
\`\`\`
|
|
978
1007
|
|
|
979
1008
|
### 1. Cloudflare DNS — auto-managed
|
|
980
1009
|
|
|
981
1010
|
\`hatchkit dev-setup init\` creates a DNS-only A record:
|
|
982
1011
|
|
|
983
1012
|
\`\`\`
|
|
984
|
-
|
|
1013
|
+
${wildcard} A <your-tailnet-ip> (DNS-only, TTL 60)
|
|
985
1014
|
\`\`\`
|
|
986
1015
|
|
|
987
1016
|
It uses your hatchkit DNS token (or the \`caddy-dev/cloudflare-acme\`
|
|
988
1017
|
keychain entry as a fallback) — the same token Caddy already needs for
|
|
989
|
-
DNS-01 ACME. \`Zone:DNS:Edit\` + \`Zone:Zone:Read\`
|
|
1018
|
+
DNS-01 ACME. Required permissions: \`Zone:DNS:Edit\` + \`Zone:Zone:Read\`
|
|
1019
|
+
on the parent zone.
|
|
990
1020
|
|
|
991
1021
|
**Why a direct A record instead of a CNAME to ${tailnetHostname}?**
|
|
992
1022
|
A CNAME to a \`.ts.net\` name only resolves when each peer has
|
|
@@ -999,7 +1029,7 @@ else gets a useless 100.x address (intended).
|
|
|
999
1029
|
If you're using a non-Cloudflare DNS provider, add the record yourself:
|
|
1000
1030
|
|
|
1001
1031
|
\`\`\`
|
|
1002
|
-
|
|
1032
|
+
${wildcard} A <your-tailnet-ip> (DNS-only)
|
|
1003
1033
|
\`\`\`
|
|
1004
1034
|
|
|
1005
1035
|
### 2. Cloudflare API token
|
|
@@ -1013,8 +1043,16 @@ hatchkit config add dns
|
|
|
1013
1043
|
\`\`\`
|
|
1014
1044
|
|
|
1015
1045
|
Permissions: \`Zone:DNS:Edit\` + \`Zone:Zone:Read\` scoped to
|
|
1016
|
-
|
|
1017
|
-
|
|
1046
|
+
\`${localDevDomain}\`. For the cleanest setup, keep the token in Keychain:
|
|
1047
|
+
|
|
1048
|
+
\`\`\`
|
|
1049
|
+
security add-generic-password -s caddy-dev -a cloudflare-acme -w '<token>' -U
|
|
1050
|
+
\`\`\`
|
|
1051
|
+
|
|
1052
|
+
When that keychain entry exists, \`dev-setup init\` writes a tiny Caddy
|
|
1053
|
+
wrapper that reads the token at startup, so the launchd plist never stores
|
|
1054
|
+
the token in plaintext. Without the keychain entry, hatchkit falls back to
|
|
1055
|
+
embedding the DNS token from \`hatchkit config add dns\` in the plist.
|
|
1018
1056
|
|
|
1019
1057
|
### 3. Caddy with the Cloudflare DNS plugin
|
|
1020
1058
|
|
|
@@ -1037,10 +1075,11 @@ xcaddy build --with github.com/caddy-dns/cloudflare
|
|
|
1037
1075
|
hatchkit dev-setup init
|
|
1038
1076
|
\`\`\`
|
|
1039
1077
|
|
|
1040
|
-
This writes \`~/.config/dev/Caddyfile\`, a launchd
|
|
1041
|
-
on a free port (default 9443, auto-bumps if taken),
|
|
1042
|
-
|
|
1043
|
-
|
|
1078
|
+
This writes \`~/.config/dev/Caddyfile\`, writes/loads a launchd job that runs
|
|
1079
|
+
Caddy on a free port (default 9443, auto-bumps if taken), registers
|
|
1080
|
+
\`tailscale serve --tcp=443 → localhost:<caddyPort>\`, and upserts the
|
|
1081
|
+
\`${wildcard}\` DNS-only A record when Cloudflare credentials are
|
|
1082
|
+
available. Idempotent — safe to re-run.
|
|
1044
1083
|
|
|
1045
1084
|
### 5. Verify
|
|
1046
1085
|
|
|
@@ -1048,13 +1087,14 @@ Idempotent — safe to re-run.
|
|
|
1048
1087
|
hatchkit doctor
|
|
1049
1088
|
\`\`\`
|
|
1050
1089
|
|
|
1051
|
-
Look for the **Local-dev** rows.
|
|
1090
|
+
Look for the **Local-dev** rows. They should be green:
|
|
1052
1091
|
|
|
1053
1092
|
- Tailscale daemon
|
|
1054
1093
|
- Caddy installed
|
|
1055
1094
|
- Caddy cloudflare plugin
|
|
1056
|
-
- Cloudflare API token in plist
|
|
1095
|
+
- Cloudflare ACME token in keychain, or Cloudflare API token in plist
|
|
1057
1096
|
- Caddy launchd job
|
|
1097
|
+
- DNS A record
|
|
1058
1098
|
- Tailscale serve bridge
|
|
1059
1099
|
|
|
1060
1100
|
## Per-project bits
|
|
@@ -1077,6 +1117,35 @@ hatchkit dev plugin:
|
|
|
1077
1117
|
\`HATCHKIT_LOCAL_DEV=0\` in the environment disables the plugin entirely;
|
|
1078
1118
|
the dev server falls back to its default banner.
|
|
1079
1119
|
|
|
1120
|
+
## Mobile/devices
|
|
1121
|
+
|
|
1122
|
+
For browser testing on a phone or tablet, install Tailscale on the device,
|
|
1123
|
+
sign in to the same tailnet, and open:
|
|
1124
|
+
|
|
1125
|
+
\`\`\`
|
|
1126
|
+
${url}
|
|
1127
|
+
\`\`\`
|
|
1128
|
+
|
|
1129
|
+
For the native Capacitor loop, the scaffolded \`pnpm dev:ios\` and
|
|
1130
|
+
\`pnpm dev:android\` scripts run the WebView against \`CAP_DEV_URL\`.
|
|
1131
|
+
The iOS script targets the Simulator; the Android script targets an emulator
|
|
1132
|
+
or attached device. Simulators/emulators use local host routes automatically.
|
|
1133
|
+
Android physical devices auto-pick this Tailscale URL when \`.hatchkit.json\`
|
|
1134
|
+
has \`localDev.slug\`; otherwise Android devices can use either:
|
|
1135
|
+
|
|
1136
|
+
\`\`\`
|
|
1137
|
+
LAN_IP=<your-lan-ip> pnpm dev:android
|
|
1138
|
+
\`\`\`
|
|
1139
|
+
|
|
1140
|
+
or, when the device is on Tailscale and this host setup is green:
|
|
1141
|
+
|
|
1142
|
+
\`\`\`
|
|
1143
|
+
CAP_DEV_URL=${url} pnpm dev:android
|
|
1144
|
+
\`\`\`
|
|
1145
|
+
|
|
1146
|
+
For a real iPhone, set \`CAP_DEV_URL=${url}\`, run \`npx cap sync ios\`,
|
|
1147
|
+
then launch from Xcode via \`npx cap open ios\`.
|
|
1148
|
+
|
|
1080
1149
|
## Cleanup
|
|
1081
1150
|
|
|
1082
1151
|
If you tear down this project:
|
|
@@ -1104,6 +1173,26 @@ export async function runDevSetupCli(args) {
|
|
|
1104
1173
|
}
|
|
1105
1174
|
if (sub === "init") {
|
|
1106
1175
|
const force = args.includes("--force");
|
|
1176
|
+
const domainFlagIdx = args.findIndex((a) => a === "--domain" || a.startsWith("--domain="));
|
|
1177
|
+
const rawDomain = domainFlagIdx === -1
|
|
1178
|
+
? undefined
|
|
1179
|
+
: args[domainFlagIdx].includes("=")
|
|
1180
|
+
? args[domainFlagIdx].slice("--domain=".length)
|
|
1181
|
+
: args[domainFlagIdx + 1];
|
|
1182
|
+
let localDevDomain = rawDomain ? normaliseLocalDevDomain(rawDomain) : undefined;
|
|
1183
|
+
if (rawDomain && !localDevDomain) {
|
|
1184
|
+
console.log("Usage: --domain <local-dev-domain> (for example: local.example.com)");
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
if (!localDevDomain) {
|
|
1188
|
+
const { resolve } = await import("node:path");
|
|
1189
|
+
const { readManifest } = await import("./scaffold/manifest.js");
|
|
1190
|
+
const manifest = readManifest(resolve("."));
|
|
1191
|
+
localDevDomain =
|
|
1192
|
+
normaliseLocalDevDomain(manifest?.localDev?.domain) ??
|
|
1193
|
+
localDevDomainFromProjectDomain(manifest?.domain) ??
|
|
1194
|
+
undefined;
|
|
1195
|
+
}
|
|
1107
1196
|
// --caddy-token-keychain <service>:<account> → wrapper mode with custom pair
|
|
1108
1197
|
// --no-caddy-token-keychain → force inline-env mode
|
|
1109
1198
|
// (default) → auto-detect default pair
|
|
@@ -1125,9 +1214,10 @@ export async function runDevSetupCli(args) {
|
|
|
1125
1214
|
caddyTokenKeychain = { service, account };
|
|
1126
1215
|
}
|
|
1127
1216
|
}
|
|
1128
|
-
const result = await runDevSetupInit({ force, caddyTokenKeychain });
|
|
1217
|
+
const result = await runDevSetupInit({ force, caddyTokenKeychain, localDevDomain });
|
|
1129
1218
|
const chalk = (await import("chalk")).default;
|
|
1130
1219
|
console.log(chalk.bold("\n hatchkit dev-setup init\n"));
|
|
1220
|
+
console.log(` Local-dev domain: ${chalk.cyan(result.localDevDomain)}`);
|
|
1131
1221
|
console.log(` Caddy port: ${chalk.cyan(result.caddyPort)}`);
|
|
1132
1222
|
console.log(` Caddyfile: ${result.wroteCaddyfile ? chalk.green("wrote") : chalk.dim("unchanged")} ${chalk.dim(CADDYFILE_PATH)}`);
|
|
1133
1223
|
if (result.wroteWrapper) {
|
|
@@ -1144,7 +1234,7 @@ export async function runDevSetupCli(args) {
|
|
|
1144
1234
|
: result.dnsRecord === "failed"
|
|
1145
1235
|
? chalk.red("failed")
|
|
1146
1236
|
: chalk.dim("skipped (no CF token)");
|
|
1147
|
-
console.log(` DNS A record: ${dnsLabel} ${chalk.dim(`*.${
|
|
1237
|
+
console.log(` DNS A record: ${dnsLabel} ${chalk.dim(`*.${result.localDevDomain}`)}`);
|
|
1148
1238
|
}
|
|
1149
1239
|
if (result.notes.length > 0) {
|
|
1150
1240
|
console.log(chalk.bold("\n Notes:"));
|
|
@@ -1162,13 +1252,17 @@ export async function runDevSetupCli(args) {
|
|
|
1162
1252
|
}
|
|
1163
1253
|
const chalk = (await import("chalk")).default;
|
|
1164
1254
|
for (const r of checks) {
|
|
1165
|
-
const icon = r.status === "ok"
|
|
1255
|
+
const icon = r.status === "ok"
|
|
1256
|
+
? chalk.green("✓")
|
|
1257
|
+
: r.status === "fail"
|
|
1258
|
+
? chalk.red("✗")
|
|
1259
|
+
: chalk.dim("·");
|
|
1166
1260
|
console.log(` ${icon} ${r.name}${r.detail ? chalk.dim(` — ${r.detail}`) : ""}`);
|
|
1167
1261
|
}
|
|
1168
1262
|
return;
|
|
1169
1263
|
}
|
|
1170
1264
|
console.log("Usage: hatchkit dev-setup <init|status|enable|disable> [flags]");
|
|
1171
|
-
console.log("\n init
|
|
1265
|
+
console.log("\n init [--domain] Auto-write ~/.config/dev/Caddyfile, launchd plist, register tailscale TCP bridge.");
|
|
1172
1266
|
console.log(" status Run the same checks doctor runs, but only the Local-dev rows.");
|
|
1173
1267
|
console.log(" enable [--slug] Wire the project in cwd for Tailscale dev URLs (writes Caddy fragment,");
|
|
1174
1268
|
console.log(" docs/dev-setup.md, patches next.config, adds plugin dep).");
|
|
@@ -1176,7 +1270,7 @@ export async function runDevSetupCli(args) {
|
|
|
1176
1270
|
}
|
|
1177
1271
|
async function runDevSetupEnableCli(args) {
|
|
1178
1272
|
const chalk = (await import("chalk")).default;
|
|
1179
|
-
const { resolve
|
|
1273
|
+
const { resolve } = await import("node:path");
|
|
1180
1274
|
const { readManifest, writeManifest } = await import("./scaffold/manifest.js");
|
|
1181
1275
|
const { sanitiseSlug } = await import("@hatchkit/dev-shared");
|
|
1182
1276
|
const { input, confirm: askConfirm } = await import("@inquirer/prompts");
|
|
@@ -1198,6 +1292,12 @@ async function runDevSetupEnableCli(args) {
|
|
|
1198
1292
|
: args[projectDirFlagIdx].includes("=")
|
|
1199
1293
|
? args[projectDirFlagIdx].slice("--project-dir=".length)
|
|
1200
1294
|
: args[projectDirFlagIdx + 1];
|
|
1295
|
+
const domainFlagIdx = args.findIndex((a) => a === "--domain" || a.startsWith("--domain="));
|
|
1296
|
+
const rawLocalDevDomain = domainFlagIdx === -1
|
|
1297
|
+
? undefined
|
|
1298
|
+
: args[domainFlagIdx].includes("=")
|
|
1299
|
+
? args[domainFlagIdx].slice("--domain=".length)
|
|
1300
|
+
: args[domainFlagIdx + 1];
|
|
1201
1301
|
const projectDir = projectDirFlag ? resolve(projectDirFlag) : resolve(".");
|
|
1202
1302
|
const manifest = readManifest(projectDir);
|
|
1203
1303
|
if (!manifest) {
|
|
@@ -1205,12 +1305,20 @@ async function runDevSetupEnableCli(args) {
|
|
|
1205
1305
|
console.log(chalk.dim(` Run from a hatchkit-managed project root, or pass --project-dir <path>.`));
|
|
1206
1306
|
process.exit(1);
|
|
1207
1307
|
}
|
|
1308
|
+
const localDevDomain = normaliseLocalDevDomain(rawLocalDevDomain) ??
|
|
1309
|
+
normaliseLocalDevDomain(manifest.localDev?.domain) ??
|
|
1310
|
+
localDevDomainFromProjectDomain(manifest.domain) ??
|
|
1311
|
+
LOCAL_DEV_DOMAIN;
|
|
1312
|
+
if (rawLocalDevDomain && !normaliseLocalDevDomain(rawLocalDevDomain)) {
|
|
1313
|
+
console.log(chalk.red(` --domain ${rawLocalDevDomain} is not a valid local-dev domain.`));
|
|
1314
|
+
process.exit(1);
|
|
1315
|
+
}
|
|
1208
1316
|
// Slug: flag → manifest.localDev → manifest.name → prompt.
|
|
1209
1317
|
let slug = slugFlag ? sanitiseSlug(slugFlag) : manifest.localDev?.slug;
|
|
1210
1318
|
if (!slug) {
|
|
1211
1319
|
const defaultSlug = sanitiseSlug(manifest.name);
|
|
1212
1320
|
slug = await input({
|
|
1213
|
-
message:
|
|
1321
|
+
message: `Slug for this project (${localDevUrl("<slug>", localDevDomain)}):`,
|
|
1214
1322
|
default: defaultSlug,
|
|
1215
1323
|
validate: (v) => {
|
|
1216
1324
|
const s = sanitiseSlug(v);
|
|
@@ -1237,8 +1345,9 @@ async function runDevSetupEnableCli(args) {
|
|
|
1237
1345
|
}
|
|
1238
1346
|
console.log(chalk.bold(`\n Enabling local-dev for ${chalk.cyan(manifest.name)}\n`));
|
|
1239
1347
|
console.log(` Slug: ${chalk.cyan(slug)}`);
|
|
1348
|
+
console.log(` Domain: ${chalk.cyan(localDevDomain)}`);
|
|
1240
1349
|
console.log(` Dev port: ${chalk.cyan(devPort)}`);
|
|
1241
|
-
console.log(` URL: ${chalk.cyan(
|
|
1350
|
+
console.log(` URL: ${chalk.cyan(localDevUrl(slug, localDevDomain))}`);
|
|
1242
1351
|
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
1243
1352
|
if (!skipConfirm) {
|
|
1244
1353
|
const ok = await askConfirm({ message: "Proceed?", default: true });
|
|
@@ -1247,11 +1356,11 @@ async function runDevSetupEnableCli(args) {
|
|
|
1247
1356
|
return;
|
|
1248
1357
|
}
|
|
1249
1358
|
}
|
|
1250
|
-
const result = await enableProjectLocalDev({ projectDir, slug, devPort });
|
|
1359
|
+
const result = await enableProjectLocalDev({ projectDir, slug, localDevDomain, devPort });
|
|
1251
1360
|
// Persist the slug in the manifest so subsequent runs (plugin, doctor,
|
|
1252
1361
|
// future `dev-setup disable`) all converge on the same identity.
|
|
1253
|
-
if (manifest.localDev?.slug !== slug) {
|
|
1254
|
-
const updated = { ...manifest, localDev: { slug } };
|
|
1362
|
+
if (manifest.localDev?.slug !== slug || manifest.localDev?.domain !== localDevDomain) {
|
|
1363
|
+
const updated = { ...manifest, localDev: { slug, domain: localDevDomain } };
|
|
1255
1364
|
writeManifest(projectDir, updated);
|
|
1256
1365
|
}
|
|
1257
1366
|
console.log(`\n Framework: ${chalk.cyan(result.framework)}`);
|
|
@@ -1272,14 +1381,14 @@ async function runDevSetupEnableCli(args) {
|
|
|
1272
1381
|
console.log(chalk.dim(" // …"));
|
|
1273
1382
|
console.log(chalk.dim(" plugins: ["));
|
|
1274
1383
|
console.log(chalk.dim(" // …your existing plugins"));
|
|
1275
|
-
console.log(chalk.dim(` localDev({ slug: "${slug}" }),`));
|
|
1384
|
+
console.log(chalk.dim(` localDev({ slug: "${slug}", localDevDomain: "${localDevDomain}" }),`));
|
|
1276
1385
|
console.log(chalk.dim(" ],"));
|
|
1277
|
-
console.log(chalk.dim(
|
|
1386
|
+
console.log(chalk.dim(` server: { allowedHosts: [".${localDevDomain}", ".ts.net", ".local"] },`));
|
|
1278
1387
|
}
|
|
1279
1388
|
if (result.patchedConfig === "unsupported-shape") {
|
|
1280
1389
|
console.log(chalk.bold("\n Next config wiring (CJS / non-standard shape):"));
|
|
1281
1390
|
console.log(chalk.dim(' const { withLocalDev } = require("@hatchkit/dev-plugin-next");'));
|
|
1282
|
-
console.log(chalk.dim(` module.exports = withLocalDev(nextConfig, { slug: "${slug}" });`));
|
|
1391
|
+
console.log(chalk.dim(` module.exports = withLocalDev(nextConfig, { slug: "${slug}", localDevDomain: "${localDevDomain}" });`));
|
|
1283
1392
|
}
|
|
1284
1393
|
console.log(chalk.dim(`\n Verify with: \`hatchkit doctor\` (or \`hatchkit dev-setup status\`).`));
|
|
1285
1394
|
}
|