hatchkit 0.1.41 → 0.1.42
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.js +362 -13
- package/dist/adopt.js.map +1 -1
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -38
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify-app.d.ts.map +1 -1
- package/dist/deploy/coolify-app.js +0 -7
- package/dist/deploy/coolify-app.js.map +1 -1
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +20 -1
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/ghcr.d.ts +4 -2
- package/dist/deploy/ghcr.d.ts.map +1 -1
- package/dist/deploy/ghcr.js +1 -1
- package/dist/deploy/ghcr.js.map +1 -1
- package/dist/deploy/github.d.ts +4 -3
- package/dist/deploy/github.d.ts.map +1 -1
- package/dist/deploy/github.js +5 -2
- package/dist/deploy/github.js.map +1 -1
- package/dist/deploy/pages.d.ts.map +1 -1
- package/dist/deploy/pages.js +8 -14
- package/dist/deploy/pages.js.map +1 -1
- package/dist/deploy/regen-infra.d.ts.map +1 -1
- package/dist/deploy/regen-infra.js +1 -11
- package/dist/deploy/regen-infra.js.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +30 -6
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/deploy/terraform.d.ts.map +1 -1
- package/dist/deploy/terraform.js +20 -37
- package/dist/deploy/terraform.js.map +1 -1
- package/dist/dns.d.ts.map +1 -1
- package/dist/dns.js +4 -5
- package/dist/dns.js.map +1 -1
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +110 -36
- package/dist/doctor.js.map +1 -1
- package/dist/email/index.d.ts +31 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/presets.d.ts +14 -0
- package/dist/email/presets.d.ts.map +1 -0
- package/dist/email/presets.js +33 -0
- package/dist/email/presets.js.map +1 -0
- package/dist/email/setup.d.ts +93 -0
- package/dist/email/setup.d.ts.map +1 -0
- package/dist/email/setup.js +263 -0
- package/dist/email/setup.js.map +1 -0
- package/dist/email/spf.d.ts +56 -0
- package/dist/email/spf.d.ts.map +1 -0
- package/dist/email/spf.js +102 -0
- package/dist/email/spf.js.map +1 -0
- package/dist/index.js +113 -4
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +34 -11
- package/dist/inventory.js.map +1 -1
- package/dist/overview.d.ts.map +1 -1
- package/dist/overview.js +43 -15
- package/dist/overview.js.map +1 -1
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +29 -7
- package/dist/prompts.js.map +1 -1
- package/dist/provision/index.d.ts +20 -1
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +115 -0
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/s3-buckets.js +1 -1
- package/dist/provision/s3-buckets.js.map +1 -1
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +15 -7
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/build-pipeline.d.ts +16 -0
- package/dist/scaffold/build-pipeline.d.ts.map +1 -1
- package/dist/scaffold/build-pipeline.js +47 -4
- package/dist/scaffold/build-pipeline.js.map +1 -1
- package/dist/scaffold/infra.d.ts +4 -5
- package/dist/scaffold/infra.d.ts.map +1 -1
- package/dist/scaffold/infra.js +11 -56
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +1 -0
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/pages-heuristics.d.ts.map +1 -1
- package/dist/scaffold/pages-heuristics.js +10 -10
- package/dist/scaffold/pages-heuristics.js.map +1 -1
- package/dist/scaffold/pages-mode.js +2 -4
- package/dist/scaffold/pages-mode.js.map +1 -1
- package/dist/scaffold/pkg-json.d.ts +4 -0
- package/dist/scaffold/pkg-json.d.ts.map +1 -1
- package/dist/scaffold/pkg-json.js +17 -0
- package/dist/scaffold/pkg-json.js.map +1 -1
- package/dist/scaffold/update.js +1 -1
- package/dist/scaffold/update.js.map +1 -1
- package/dist/templates/build-pipeline/Dockerfile.nextjs.hbs +103 -0
- package/dist/templates/build-pipeline/docker-compose.yml.hbs +23 -6
- package/dist/utils/cloudflare-api.d.ts +146 -20
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +203 -11
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +22 -1
- package/dist/utils/run-ledger.d.ts.map +1 -1
- package/dist/utils/run-ledger.js.map +1 -1
- package/dist/utils/s3-admin.d.ts +9 -0
- package/dist/utils/s3-admin.d.ts.map +1 -0
- package/dist/utils/s3-admin.js +46 -0
- package/dist/utils/s3-admin.js.map +1 -0
- package/package.json +1 -1
package/dist/adopt.js
CHANGED
|
@@ -834,7 +834,9 @@ async function editAdoptStep(state, plan, step) {
|
|
|
834
834
|
// Also: switching away from client-only invalidates gh-pages
|
|
835
835
|
// (Pages can't host a backend). Snap deploymentMode back to
|
|
836
836
|
// coolify in that case so the user doesn't keep an invalid combo.
|
|
837
|
-
const nextDeploymentMode = plan.deploymentMode === "gh-pages" && next !== "client-only"
|
|
837
|
+
const nextDeploymentMode = plan.deploymentMode === "gh-pages" && next !== "client-only"
|
|
838
|
+
? "coolify"
|
|
839
|
+
: plan.deploymentMode;
|
|
838
840
|
if (plan.deploymentMode === "gh-pages" && next !== "client-only") {
|
|
839
841
|
console.log(chalk.yellow(" ⚠ gh-pages requires client-only surfaces — switched deployment mode back to coolify."));
|
|
840
842
|
}
|
|
@@ -958,10 +960,15 @@ async function editAdoptStep(state, plan, step) {
|
|
|
958
960
|
checked: plan.services.includes("openpanel"),
|
|
959
961
|
},
|
|
960
962
|
{
|
|
961
|
-
name: "Resend (email)",
|
|
963
|
+
name: "Resend (transactional email)",
|
|
962
964
|
value: "resend",
|
|
963
965
|
checked: plan.services.includes("resend"),
|
|
964
966
|
},
|
|
967
|
+
{
|
|
968
|
+
name: "Email forwarding (Cloudflare Email Routing → your inbox)",
|
|
969
|
+
value: "email",
|
|
970
|
+
checked: plan.services.includes("email"),
|
|
971
|
+
},
|
|
965
972
|
],
|
|
966
973
|
});
|
|
967
974
|
return { ...plan, services };
|
|
@@ -1127,10 +1134,14 @@ async function executePlan(state, plan, opts = { resume: false }) {
|
|
|
1127
1134
|
// Gated on coolify mode — the Coolify-targeted Dockerfile + deploy
|
|
1128
1135
|
// webhook workflow aren't useful for gh-pages, which uses its own
|
|
1129
1136
|
// `gh-pages.yml` workflow written later in step 3c-pages.
|
|
1137
|
+
let scaffoldedAbsPaths = [];
|
|
1138
|
+
let overwrittenAbsPaths = [];
|
|
1130
1139
|
if (plan.scaffoldBuildPipeline && plan.deploymentMode === "coolify") {
|
|
1131
1140
|
const pipeResult = await scaffoldBuildPipelineNow(state, plan, remoteUrl, {
|
|
1132
1141
|
force: !!opts.regeneratePipeline,
|
|
1133
1142
|
});
|
|
1143
|
+
scaffoldedAbsPaths = pipeResult.createdAbsPaths;
|
|
1144
|
+
overwrittenAbsPaths = pipeResult.overwrittenAbsPaths;
|
|
1134
1145
|
// Record only files we *created*. The `overwritten` list is
|
|
1135
1146
|
// deliberately not recorded — those files existed before this
|
|
1136
1147
|
// run (the user's), and a later `hatchkit destroy` must never
|
|
@@ -1258,9 +1269,7 @@ async function executePlan(state, plan, opts = { resume: false }) {
|
|
|
1258
1269
|
//
|
|
1259
1270
|
// Skipped for gh-pages — there's no Coolify webhook to hit.
|
|
1260
1271
|
const appUuidForSecrets = coolifyResult?.appUuid ?? state.coolifyAppMatch?.uuid;
|
|
1261
|
-
if (plan.scaffoldBuildPipeline &&
|
|
1262
|
-
plan.deploymentMode === "coolify" &&
|
|
1263
|
-
appUuidForSecrets) {
|
|
1272
|
+
if (plan.scaffoldBuildPipeline && plan.deploymentMode === "coolify" && appUuidForSecrets) {
|
|
1264
1273
|
const slug = repoSlugFromRemote(remoteUrl);
|
|
1265
1274
|
if (slug) {
|
|
1266
1275
|
await setCoolifyDeploySecrets({
|
|
@@ -1385,13 +1394,36 @@ async function executePlan(state, plan, opts = { resume: false }) {
|
|
|
1385
1394
|
});
|
|
1386
1395
|
}
|
|
1387
1396
|
}
|
|
1388
|
-
// Step 3d: push
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
|
|
1394
|
-
|
|
1397
|
+
// Step 3d: push to origin so GitHub Actions builds + pushes the
|
|
1398
|
+
// GHCR image. Done AFTER Coolify wiring + secrets so the workflow's
|
|
1399
|
+
// first run can hit the redeploy webhook on its own.
|
|
1400
|
+
//
|
|
1401
|
+
// Two paths:
|
|
1402
|
+
// · Brand-new remote (this run just ran `gh repo create`) →
|
|
1403
|
+
// pushInitialBranch pushes the whole tree.
|
|
1404
|
+
// · Pre-existing remote → commitAndPushScaffold makes a
|
|
1405
|
+
// pathspec-scoped commit of just the files hatchkit wrote
|
|
1406
|
+
// (manifest + build-pipeline scaffold) and pushes it. Without
|
|
1407
|
+
// this push the workflow file lives only in the working tree,
|
|
1408
|
+
// Actions never fires, and the GHCR-wait below times out.
|
|
1409
|
+
//
|
|
1410
|
+
// `pushedThisRun` gates the GHCR step below — we only wait for a
|
|
1411
|
+
// new image when a push actually went out.
|
|
1412
|
+
let pushedThisRun = false;
|
|
1413
|
+
if (remoteUrl) {
|
|
1414
|
+
if (plan.setupGitHub && !state.gitRemoteUrl) {
|
|
1415
|
+
pushedThisRun = await pushInitialBranch(state.projectDir);
|
|
1416
|
+
}
|
|
1417
|
+
else if (state.gitRemoteUrl) {
|
|
1418
|
+
const result = await commitAndPushScaffold(state, {
|
|
1419
|
+
scaffoldedAbsPaths,
|
|
1420
|
+
overwrittenAbsPaths,
|
|
1421
|
+
manifestPath,
|
|
1422
|
+
});
|
|
1423
|
+
pushedThisRun = result.pushed;
|
|
1424
|
+
if (result.caveat)
|
|
1425
|
+
caveats.push(result.caveat);
|
|
1426
|
+
}
|
|
1395
1427
|
}
|
|
1396
1428
|
// Step 3e: GHCR setup. Two paths, gated on the user's earlier
|
|
1397
1429
|
// public/private choice:
|
|
@@ -1414,7 +1446,25 @@ async function executePlan(state, plan, opts = { resume: false }) {
|
|
|
1414
1446
|
remoteUrl &&
|
|
1415
1447
|
coolifyResult !== undefined) {
|
|
1416
1448
|
const slug = repoSlugFromRemote(remoteUrl);
|
|
1417
|
-
if (slug) {
|
|
1449
|
+
if (slug && !pushedThisRun && !plan.isPrivate) {
|
|
1450
|
+
// No push went out (either nothing changed on disk this run,
|
|
1451
|
+
// or the auto-commit-push failed). Without a push the
|
|
1452
|
+
// build-and-deploy workflow doesn't run, so polling GHCR for
|
|
1453
|
+
// a brand-new image would just time out. Defer the visibility
|
|
1454
|
+
// PATCH to the next `--resume` once the user has pushed.
|
|
1455
|
+
caveats.push({
|
|
1456
|
+
title: "GHCR visibility not set — no push triggered",
|
|
1457
|
+
reason: "Adopt didn't push to origin this run, so the build-and-deploy workflow hasn't been triggered to publish the GHCR image.",
|
|
1458
|
+
recovery: [
|
|
1459
|
+
"Commit + push so the workflow runs:",
|
|
1460
|
+
` cd ${state.projectDir}`,
|
|
1461
|
+
` git add . && git commit -m "chore: adopt hatchkit"`,
|
|
1462
|
+
` git push`,
|
|
1463
|
+
"Then re-run: hatchkit adopt --resume",
|
|
1464
|
+
],
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
else if (slug) {
|
|
1418
1468
|
const { makeGhcrPackagePublic, registerGhcrCredsWithCoolify } = await import("./deploy/ghcr.js");
|
|
1419
1469
|
if (plan.isPrivate) {
|
|
1420
1470
|
// Read the full GHCR config (token + the PAT owner's GitHub
|
|
@@ -1492,6 +1542,44 @@ async function executePlan(state, plan, opts = { resume: false }) {
|
|
|
1492
1542
|
else if (event.service === "resend") {
|
|
1493
1543
|
ledger.record({ kind: "resend", client: event.client });
|
|
1494
1544
|
}
|
|
1545
|
+
else if (event.service === "email") {
|
|
1546
|
+
// Email setup creates three kinds of mutable state on
|
|
1547
|
+
// Cloudflare: the destination address (account-scoped), the
|
|
1548
|
+
// forwarding rules (zone-scoped), and the apex MX/SPF/DMARC
|
|
1549
|
+
// records (also zone-scoped). We only record what THIS run
|
|
1550
|
+
// created — `destinationCreatedThisRun` and `r.created` /
|
|
1551
|
+
// `dnsRecords` (which the provision orchestrator already
|
|
1552
|
+
// pre-filtered to `created: true` entries). MX/SPF/DMARC
|
|
1553
|
+
// upserts on a zone that already had them stay out of the
|
|
1554
|
+
// ledger so destroy never yanks pre-existing records.
|
|
1555
|
+
if (event.destinationCreatedThisRun) {
|
|
1556
|
+
ledger.record({
|
|
1557
|
+
kind: "cloudflareEmailDestination",
|
|
1558
|
+
accountId: event.accountId,
|
|
1559
|
+
destinationId: event.destinationId,
|
|
1560
|
+
email: event.destinationEmail,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
for (const dns of event.dnsRecords) {
|
|
1564
|
+
ledger.record({
|
|
1565
|
+
kind: "cloudflareDnsRecord",
|
|
1566
|
+
zoneId: event.zoneId,
|
|
1567
|
+
recordId: dns.id,
|
|
1568
|
+
name: dns.name,
|
|
1569
|
+
type: dns.type,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
for (const rule of event.rules) {
|
|
1573
|
+
if (!rule.created)
|
|
1574
|
+
continue;
|
|
1575
|
+
ledger.record({
|
|
1576
|
+
kind: "cloudflareEmailRoutingRule",
|
|
1577
|
+
zoneId: event.zoneId,
|
|
1578
|
+
ruleId: rule.id,
|
|
1579
|
+
address: rule.address,
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1495
1583
|
},
|
|
1496
1584
|
});
|
|
1497
1585
|
}
|
|
@@ -1942,6 +2030,267 @@ async function ensureEnvProductionCommitted(state, plan) {
|
|
|
1942
2030
|
` git -C ${relativeTo(state.projectDir)} commit -m "chore(dotenvx): commit encrypted .env.production" -- ${relativeTo(prodPath, state.projectDir)}`));
|
|
1943
2031
|
}
|
|
1944
2032
|
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Sniff the working tree for state that would make auto-commit + push
|
|
2035
|
+
* surprising or destructive. We refuse to touch git when:
|
|
2036
|
+
*
|
|
2037
|
+
* · A merge / rebase / cherry-pick / revert / bisect is in progress
|
|
2038
|
+
* — adopt isn't allowed to add commits on top of half-resolved
|
|
2039
|
+
* conflicts.
|
|
2040
|
+
* · Any tracked file *outside* hatchkit's path list has unstaged or
|
|
2041
|
+
* staged changes. Even though the commit itself is pathspec-scoped
|
|
2042
|
+
* (so the user's modifications wouldn't be swept in), the push
|
|
2043
|
+
* would still land hatchkit's commit on top of the user's WIP on
|
|
2044
|
+
* the same branch — entangling their unpushed work with the
|
|
2045
|
+
* hatchkit commit on origin. Make them park or commit it first.
|
|
2046
|
+
*
|
|
2047
|
+
* Untracked files (status `??`) are deliberately ignored — they're
|
|
2048
|
+
* common debris (editor swaps, build artifacts not in gitignore, etc.)
|
|
2049
|
+
* and never end up in our pathspec commit anyway.
|
|
2050
|
+
*/
|
|
2051
|
+
async function detectUserWip(projectDir, hatchkitAbsPaths) {
|
|
2052
|
+
// In-progress operations: probe the .git dir for marker files. We
|
|
2053
|
+
// resolve --git-dir via git itself so this works in worktrees (where
|
|
2054
|
+
// .git is a file, not a directory) and submodules.
|
|
2055
|
+
const gitDirRes = await exec("git", ["rev-parse", "--git-dir"], {
|
|
2056
|
+
cwd: projectDir,
|
|
2057
|
+
silent: true,
|
|
2058
|
+
});
|
|
2059
|
+
if (gitDirRes.exitCode === 0) {
|
|
2060
|
+
const raw = gitDirRes.stdout.trim();
|
|
2061
|
+
const gitDir = raw.startsWith("/") ? raw : join(projectDir, raw);
|
|
2062
|
+
const markers = [
|
|
2063
|
+
["MERGE_HEAD", "merge"],
|
|
2064
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
2065
|
+
["REVERT_HEAD", "revert"],
|
|
2066
|
+
["rebase-merge", "rebase"],
|
|
2067
|
+
["rebase-apply", "rebase"],
|
|
2068
|
+
["BISECT_LOG", "bisect"],
|
|
2069
|
+
];
|
|
2070
|
+
for (const [marker, op] of markers) {
|
|
2071
|
+
if (existsSync(join(gitDir, marker)))
|
|
2072
|
+
return { kind: "in-progress", op };
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
// Repo-root-relative path matching. `git status --porcelain` emits
|
|
2076
|
+
// paths relative to the repo root, not necessarily our cwd, so we
|
|
2077
|
+
// normalize hatchkit's absolute paths the same way before comparing.
|
|
2078
|
+
const rootRes = await exec("git", ["rev-parse", "--show-toplevel"], {
|
|
2079
|
+
cwd: projectDir,
|
|
2080
|
+
silent: true,
|
|
2081
|
+
});
|
|
2082
|
+
if (rootRes.exitCode !== 0) {
|
|
2083
|
+
return { kind: "error", reason: "git rev-parse --show-toplevel failed" };
|
|
2084
|
+
}
|
|
2085
|
+
const repoRoot = rootRes.stdout.trim();
|
|
2086
|
+
const hatchkitRelToRoot = new Set(hatchkitAbsPaths.map((p) => relative(repoRoot, p)));
|
|
2087
|
+
const status = await exec("git", ["status", "--porcelain", "--untracked-files=no"], {
|
|
2088
|
+
cwd: projectDir,
|
|
2089
|
+
silent: true,
|
|
2090
|
+
});
|
|
2091
|
+
if (status.exitCode !== 0) {
|
|
2092
|
+
return { kind: "error", reason: "git status failed" };
|
|
2093
|
+
}
|
|
2094
|
+
const userFiles = [];
|
|
2095
|
+
for (const line of status.stdout.split("\n")) {
|
|
2096
|
+
if (line.length < 4)
|
|
2097
|
+
continue;
|
|
2098
|
+
const code = line.slice(0, 2);
|
|
2099
|
+
let rest = line.slice(3);
|
|
2100
|
+
// Renames/copies show up as "OLD -> NEW". We want the new path.
|
|
2101
|
+
if (rest.includes(" -> "))
|
|
2102
|
+
rest = rest.split(" -> ").pop() ?? rest;
|
|
2103
|
+
// Git quotes paths with special chars in C-style. If a path is
|
|
2104
|
+
// quoted we conservatively treat it as user WIP rather than try
|
|
2105
|
+
// to unquote and risk a false negative.
|
|
2106
|
+
const path = rest.startsWith('"') && rest.endsWith('"') ? rest.slice(1, -1) : rest;
|
|
2107
|
+
if (!hatchkitRelToRoot.has(path)) {
|
|
2108
|
+
userFiles.push({ status: code, path });
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (userFiles.length > 0)
|
|
2112
|
+
return { kind: "user-changes", files: userFiles };
|
|
2113
|
+
return { kind: "ok" };
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Commit + push the files hatchkit wrote this run (manifest +
|
|
2117
|
+
* scaffolded build pipeline) to a pre-existing remote so the
|
|
2118
|
+
* build-and-deploy workflow fires.
|
|
2119
|
+
*
|
|
2120
|
+
* Pathspec-scoped on purpose: a plain `git add -A` would sweep up
|
|
2121
|
+
* whatever WIP the user happened to have in the working tree —
|
|
2122
|
+
* surprising behavior for an adopt. By listing only the paths
|
|
2123
|
+
* hatchkit just wrote, the resulting commit is exactly "the adopt
|
|
2124
|
+
* step", and anything else stays staged in the user's hands.
|
|
2125
|
+
*
|
|
2126
|
+
* Hard-stops with a caveat when `detectUserWip` finds unrelated user
|
|
2127
|
+
* changes or an in-progress git operation. The push would otherwise
|
|
2128
|
+
* land hatchkit's commit on top of WIP that isn't part of adopt — a
|
|
2129
|
+
* surprise we explicitly refuse to do.
|
|
2130
|
+
*
|
|
2131
|
+
* Returns `{ pushed: false }` (no caveat) for the idempotent case —
|
|
2132
|
+
* everything hatchkit wrote was already byte-identical to HEAD, so
|
|
2133
|
+
* there was nothing to push. Failures during commit/push surface as
|
|
2134
|
+
* a caveat with a copy-pasteable manual recipe.
|
|
2135
|
+
*/
|
|
2136
|
+
async function commitAndPushScaffold(state, paths) {
|
|
2137
|
+
const all = [
|
|
2138
|
+
...paths.scaffoldedAbsPaths,
|
|
2139
|
+
...paths.overwrittenAbsPaths,
|
|
2140
|
+
paths.manifestPath,
|
|
2141
|
+
].filter((p, i, arr) => arr.indexOf(p) === i && existsSync(p));
|
|
2142
|
+
if (all.length === 0)
|
|
2143
|
+
return { pushed: false };
|
|
2144
|
+
// Hard stop: refuse to auto-commit on top of in-progress git ops
|
|
2145
|
+
// or unrelated user changes. See detectUserWip docstring for the
|
|
2146
|
+
// exact policy.
|
|
2147
|
+
const wip = await detectUserWip(state.projectDir, all);
|
|
2148
|
+
if (wip.kind === "in-progress") {
|
|
2149
|
+
return {
|
|
2150
|
+
pushed: false,
|
|
2151
|
+
caveat: {
|
|
2152
|
+
title: `Refusing to auto-commit — git ${wip.op} in progress`,
|
|
2153
|
+
reason: `A ${wip.op} is in progress in ${state.projectDir}. Adopt won't stack commits on top of half-resolved git state.`,
|
|
2154
|
+
recovery: [
|
|
2155
|
+
`Finish or abort the ${wip.op}, then re-run adopt:`,
|
|
2156
|
+
wip.op === "rebase"
|
|
2157
|
+
? ` git rebase --continue # or: git rebase --abort`
|
|
2158
|
+
: ` git ${wip.op} --abort # or finish it manually and commit`,
|
|
2159
|
+
"Then: hatchkit adopt --resume",
|
|
2160
|
+
],
|
|
2161
|
+
},
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
if (wip.kind === "user-changes") {
|
|
2165
|
+
const preview = wip.files.slice(0, 8).map((f) => ` ${f.status} ${f.path}`);
|
|
2166
|
+
const extra = wip.files.length > 8 ? [` ... and ${wip.files.length - 8} more`] : [];
|
|
2167
|
+
return {
|
|
2168
|
+
pushed: false,
|
|
2169
|
+
caveat: {
|
|
2170
|
+
title: "Refusing to auto-commit — working tree has unrelated changes",
|
|
2171
|
+
reason: `Found ${wip.files.length} modified file(s) outside the hatchkit scaffold. Auto-committing + pushing now would land the adopt commit on top of WIP that isn't part of the adopt step.`,
|
|
2172
|
+
recovery: [
|
|
2173
|
+
"Hatchkit wanted to commit + push these files:",
|
|
2174
|
+
...all.map((p) => ` + ${relativeTo(p, state.projectDir)}`),
|
|
2175
|
+
"",
|
|
2176
|
+
"Your working tree also has changes to:",
|
|
2177
|
+
...preview,
|
|
2178
|
+
...extra,
|
|
2179
|
+
"",
|
|
2180
|
+
"Park, commit, or discard your WIP first — whichever fits:",
|
|
2181
|
+
` git stash push -u -m "pre-hatchkit-adopt" # park on the side`,
|
|
2182
|
+
` # or: git add . && git commit -m "..." # keep in history`,
|
|
2183
|
+
` # or: git checkout -- <file> # discard a file`,
|
|
2184
|
+
"Then re-run: hatchkit adopt --resume",
|
|
2185
|
+
],
|
|
2186
|
+
},
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
if (wip.kind === "error") {
|
|
2190
|
+
return {
|
|
2191
|
+
pushed: false,
|
|
2192
|
+
caveat: {
|
|
2193
|
+
title: "Refusing to auto-commit — couldn't verify a clean working tree",
|
|
2194
|
+
reason: `Working-tree detection failed: ${wip.reason}. Adopt won't auto-commit without knowing what else is in the tree.`,
|
|
2195
|
+
recovery: [
|
|
2196
|
+
"Commit + push the scaffold manually:",
|
|
2197
|
+
` cd ${state.projectDir}`,
|
|
2198
|
+
` git add ${all.map((p) => relativeTo(p, state.projectDir)).join(" ")}`,
|
|
2199
|
+
` git commit -m "chore(hatchkit): adopt scaffold + manifest"`,
|
|
2200
|
+
` git push`,
|
|
2201
|
+
"Then re-run: hatchkit adopt --resume",
|
|
2202
|
+
],
|
|
2203
|
+
},
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
// Definitive pre-commit notice. The user opted into adopt, but an
|
|
2207
|
+
// auto-commit-and-push on a pre-existing remote is a meaningful
|
|
2208
|
+
// side effect — they should see exactly what's happening before it
|
|
2209
|
+
// lands on origin.
|
|
2210
|
+
console.log();
|
|
2211
|
+
console.log(chalk.bold.yellow(" ⚠ hatchkit is about to commit + push to origin:"));
|
|
2212
|
+
for (const p of all) {
|
|
2213
|
+
console.log(chalk.yellow(` + ${relativeTo(p, state.projectDir)}`));
|
|
2214
|
+
}
|
|
2215
|
+
console.log(chalk.dim(" (working tree verified clean of unrelated changes — auto-commit is safe)"));
|
|
2216
|
+
console.log();
|
|
2217
|
+
// Pathspec stage: only the hatchkit-owned files. `--` separates
|
|
2218
|
+
// pathspecs from refs so a file named "main" doesn't get confused
|
|
2219
|
+
// for a branch.
|
|
2220
|
+
const stage = await exec("git", ["add", "--", ...all], {
|
|
2221
|
+
cwd: state.projectDir,
|
|
2222
|
+
silent: true,
|
|
2223
|
+
});
|
|
2224
|
+
if (stage.exitCode !== 0) {
|
|
2225
|
+
return {
|
|
2226
|
+
pushed: false,
|
|
2227
|
+
caveat: {
|
|
2228
|
+
title: "Couldn't stage hatchkit scaffold for commit",
|
|
2229
|
+
reason: (stage.stderr || stage.stdout).split(/\r?\n/)[0] || "git add failed",
|
|
2230
|
+
recovery: [
|
|
2231
|
+
"Stage + commit + push manually so the workflow runs:",
|
|
2232
|
+
` cd ${state.projectDir}`,
|
|
2233
|
+
` git add ${all.map((p) => relativeTo(p, state.projectDir)).join(" ")}`,
|
|
2234
|
+
` git commit -m "chore(hatchkit): adopt scaffold + manifest"`,
|
|
2235
|
+
` git push`,
|
|
2236
|
+
"Then re-run: hatchkit adopt --resume",
|
|
2237
|
+
],
|
|
2238
|
+
},
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
// Nothing in the staged index means every file was byte-identical
|
|
2242
|
+
// to HEAD — this is the idempotent re-run case. No push needed.
|
|
2243
|
+
const cleanStaged = await execOk("git", ["diff", "--cached", "--quiet"], {
|
|
2244
|
+
cwd: state.projectDir,
|
|
2245
|
+
});
|
|
2246
|
+
if (cleanStaged)
|
|
2247
|
+
return { pushed: false };
|
|
2248
|
+
// Pathspec on the commit too — anything else the user happened to
|
|
2249
|
+
// stage themselves before running adopt stays out of this commit.
|
|
2250
|
+
const commit = await exec("git", ["commit", "-m", "chore(hatchkit): adopt scaffold + manifest", "--", ...all], { cwd: state.projectDir, silent: true });
|
|
2251
|
+
if (commit.exitCode !== 0) {
|
|
2252
|
+
return {
|
|
2253
|
+
pushed: false,
|
|
2254
|
+
caveat: {
|
|
2255
|
+
title: "Couldn't commit hatchkit scaffold automatically",
|
|
2256
|
+
reason: (commit.stderr || commit.stdout).split(/\r?\n/)[0] || "git commit failed",
|
|
2257
|
+
recovery: [
|
|
2258
|
+
"Commit + push the scaffold manually:",
|
|
2259
|
+
` cd ${state.projectDir}`,
|
|
2260
|
+
` git commit -m "chore(hatchkit): adopt scaffold + manifest" -- ${all.map((p) => relativeTo(p, state.projectDir)).join(" ")}`,
|
|
2261
|
+
` git push`,
|
|
2262
|
+
"Then re-run: hatchkit adopt --resume",
|
|
2263
|
+
],
|
|
2264
|
+
},
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
console.log(chalk.green(` ✓ Committed hatchkit scaffold (${all.length} files)`));
|
|
2268
|
+
const headRes = await exec("git", ["symbolic-ref", "--short", "HEAD"], {
|
|
2269
|
+
cwd: state.projectDir,
|
|
2270
|
+
silent: true,
|
|
2271
|
+
});
|
|
2272
|
+
const branch = headRes.exitCode === 0 ? headRes.stdout.trim() : "main";
|
|
2273
|
+
const push = await exec("git", ["push", "origin", branch], {
|
|
2274
|
+
cwd: state.projectDir,
|
|
2275
|
+
spinner: `Pushing ${branch} to origin...`,
|
|
2276
|
+
});
|
|
2277
|
+
if (push.exitCode !== 0) {
|
|
2278
|
+
return {
|
|
2279
|
+
pushed: false,
|
|
2280
|
+
caveat: {
|
|
2281
|
+
title: `Couldn't push ${branch} to origin`,
|
|
2282
|
+
reason: (push.stderr || push.stdout).split(/\r?\n/)[0] || `git push exited ${push.exitCode}`,
|
|
2283
|
+
recovery: [
|
|
2284
|
+
"Push the new commit so Actions can build the image:",
|
|
2285
|
+
` cd ${state.projectDir}`,
|
|
2286
|
+
` git push origin ${branch}`,
|
|
2287
|
+
"Then re-run: hatchkit adopt --resume",
|
|
2288
|
+
],
|
|
2289
|
+
},
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
return { pushed: true };
|
|
2293
|
+
}
|
|
1945
2294
|
async function setupGitHubRemote(state, plan) {
|
|
1946
2295
|
// Pre-flight gh CLI auth. ensureGitHub prompts the user to log in
|
|
1947
2296
|
// when needed; if they cancel, surface a clear "you can do this
|