@afokapu/atdd-bun 0.10.90 → 0.10.91

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/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.90",
2
+ "version": "0.10.91",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -1261,7 +1261,7 @@
1261
1261
  "src/cli.ts": "d93c3aebb78e752fd96347a5b177c0ed0697010c6e2533e116de93e772cd65a8",
1262
1262
  "src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
1263
1263
  "src/enforce.ts": "e0c10519aeecd7080f2900ea230fad2f09482af47c1cee885d5de71573e29732",
1264
- "src/hooks.ts": "ed1550a02ed9d61828e540ce8d7f1e01d0baea8dcebc74b4ea53d4a49532856b",
1264
+ "src/hooks.ts": "b96b5fee3a03c2f4c317d6725d6d21ed3133db18d0cdcdda72183cf05c7b6639",
1265
1265
  "src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
1266
1266
  "src/integrity.ts": "19066185f99839c9e95c57f9bfbcd34df58cf8f4c33de86264c89e03b1085abf",
1267
1267
  "src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.90",
3
+ "version": "0.10.91",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/hooks.ts CHANGED
@@ -336,7 +336,9 @@ async function ratchetPushGate(root: string, remoteBase: string, candidate: stri
336
336
 
337
337
  const inside = (parent: string, child: string) => { const path = relative(parent, child); return path === "" || (path !== ".." && !path.startsWith("../") && !path.startsWith("..\\")); };
338
338
  async function primaryRoot(root: string): Promise<string | null> { const result = await git(root, ["rev-parse", "--git-common-dir"]); if (result.code) return null; return dirname(resolve(root, result.out)); }
339
- async function primaryPolicy(root: string): Promise<HookPolicy> { const primary = await primaryRoot(await realpath(resolve(root))); if (!primary) return defaultHookPolicy; const committed = await git(primary, ["show", "HEAD:atdd-bun.yaml"]); return committed.code ? defaultHookPolicy : parsePolicy(committed.out); }
339
+ type PrimaryPolicy = { policy: HookPolicy; source: { path: string; commit: string } | null };
340
+ async function primaryPolicy(root: string): Promise<PrimaryPolicy> { const found = await primaryRoot(await realpath(resolve(root))); if (!found) return { policy: defaultHookPolicy, source: null }; const primary = await realpath(found), revision = await git(primary, ["rev-parse", "--verify", "HEAD^{commit}"]); if (revision.code || !revision.out) return { policy: defaultHookPolicy, source: null }; const source = { path: primary, commit: revision.out }, committed = await git(primary, ["show", `${source.commit}:atdd-bun.yaml`]); return { policy: committed.code ? defaultHookPolicy : parsePolicy(committed.out), source }; }
341
+ const disabledWorktreePolicy = (source: PrimaryPolicy["source"]) => bad(source ? `worktree policy is disabled in primary checkout ${source.path} at ${source.commit}; update the primary checkout and try again` : "worktree policy is disabled");
340
342
  export type WorktreeLayout = { primary: string; worktreeRoot: string; current: string; branch: string };
341
343
  async function layout(root: string, cfg: WorktreePolicy): Promise<WorktreeLayout | { error: string }> { const current = await realpath(resolve(root)), commonRoot = await primaryRoot(current); if (!commonRoot) return { error: "not inside a Git worktree" }; const primary = await realpath(commonRoot), expectedPrimary = cfg.primary_directory === "." ? primary : join(dirname(primary), cfg.primary_directory); if (primary !== expectedPrimary) return { error: `primary checkout must be ${expectedPrimary}, not ${primary}` }; const branch = (await git(primary, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out; if (branch !== cfg.primary_branch) return { error: `primary checkout ${primary} must be on ${cfg.primary_branch}, not ${branch || "a detached HEAD"}` }; return { primary, worktreeRoot: resolve(primary, cfg.root.replaceAll("{repo}", basename(primary))), current, branch: (await git(current, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out }; }
342
344
  async function worktreeCommitPolicy(root: string, cfg: WorktreePolicy): Promise<string | null> { if (!cfg.enabled || !cfg.require_linked_worktree) return null; const state = await layout(root, cfg); if ("error" in state) return state.error; if (state.current === state.primary) return `commits must use a linked worktree beneath ${state.worktreeRoot}; run: atdd-bun worktree start <branch>`; if (!inside(state.worktreeRoot, state.current)) return `linked worktree ${state.current} is outside required root ${state.worktreeRoot}`; if (!state.branch) return "commits require a named branch, not a detached HEAD"; return null; }
@@ -376,8 +378,8 @@ export async function hooksStatus(root = process.cwd()) {
376
378
  const branchPath = (branch: string) => branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
377
379
  export async function startWorktree(root = process.cwd(), branch = "") { const cfg = await policy(root); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled; set worktrees.enabled: true in atdd-bun.yaml"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current !== state.primary) return bad(`start must run from the primary checkout ${state.primary}`); if (!branch) return bad("branch is required: atdd-bun worktree start <branch>"); if ((await git(root, ["check-ref-format", "--branch", branch])).code) return bad(`invalid branch name ${JSON.stringify(branch)}`); if (cfg.protected_branches.includes(branch)) return bad(`cannot create a worktree for protected branch ${branch}`); const leaf = branchPath(branch); if (!leaf) return bad(`branch ${JSON.stringify(branch)} cannot form a safe worktree directory`); const target = join(state.worktreeRoot, leaf); if (existsSync(target)) return bad(`worktree path already exists: ${target}`); await mkdir(state.worktreeRoot, { recursive: true }); const created = await git(state.primary, ["worktree", "add", target, "-b", branch]); if (created.code) return bad(created.err || created.out); const hooks = await installHooks(target); if (!hooks.ok) return bad(`created ${target}, but could not install hooks: ${hooks.message}`); return { ok: true, message: `created linked worktree ${target} on ${branch}` }; }
378
380
 
379
- export async function worktreeStatus(root = process.cwd()) { const cfg = await primaryPolicy(root); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); const result = await git(state.primary, ["worktree", "list", "--porcelain"]); return result.code ? bad(result.err) : { ok: true, message: result.out }; }
381
+ export async function worktreeStatus(root = process.cwd()) { const { policy: cfg, source } = await primaryPolicy(root); if (!cfg.worktrees.enabled) return disabledWorktreePolicy(source); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); const result = await git(state.primary, ["worktree", "list", "--porcelain"]); return result.code ? bad(result.err) : { ok: true, message: result.out }; }
380
382
 
381
383
  async function generatedHooksOnly(root: string): Promise<boolean> { const directory = join(root, ".githooks"); if (!existsSync(directory)) return false; const names = (await readdir(directory)).sort(); if (names.join("\n") !== [...hookEvents].sort().join("\n")) return false; return (await Promise.all(hookEvents.map(async event => (await readFile(join(directory, event), "utf8")) === await renderHook(event, root)))).every(Boolean); }
382
384
 
383
- export async function finishWorktree(root = process.cwd(), deleteBranch = false) { const cfg = await primaryPolicy(root); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current === state.primary || !inside(state.worktreeRoot, state.current)) return bad(`finish must run from a linked worktree beneath ${state.worktreeRoot}`); if (!state.branch || cfg.protected_branches.includes(state.branch)) return bad("finish requires a non-protected named branch"); const status = (await git(state.current, ["status", "--porcelain"])).out, generatedHooks = await generatedHooksOnly(state.current), onlyGeneratedHooks = Boolean(status) && status.split("\n").every(line => line.endsWith(" .githooks/") || line.endsWith(" .githooks")); if (status && !onlyGeneratedHooks) return bad("worktree is dirty; commit, stash, or discard changes before finish"); const tip = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (!tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`branch ${state.branch} is not merged into ${cfg.worktrees.primary_branch} in the primary checkout`); const removed = await git(state.primary, ["worktree", "remove", ...(onlyGeneratedHooks && generatedHooks ? ["--force"] : []), state.current]); if (removed.code) return bad(removed.err || removed.out); if (deleteBranch) { const now = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (now !== tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`worktree removed, but branch ${state.branch} was not deleted: its tip changed or is no longer merged into ${cfg.worktrees.primary_branch}`); const deleted = await git(state.primary, ["branch", "-D", state.branch]); if (deleted.code) return bad(`worktree removed, but branch deletion failed: ${deleted.err || deleted.out}`); } return { ok: true, message: `removed merged worktree ${state.current}${deleteBranch ? ` and branch ${state.branch}` : ""}` }; }
385
+ export async function finishWorktree(root = process.cwd(), deleteBranch = false) { const { policy: cfg, source } = await primaryPolicy(root); if (!cfg.worktrees.enabled) return disabledWorktreePolicy(source); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current === state.primary || !inside(state.worktreeRoot, state.current)) return bad(`finish must run from a linked worktree beneath ${state.worktreeRoot}`); if (!state.branch || cfg.protected_branches.includes(state.branch)) return bad("finish requires a non-protected named branch"); const status = (await git(state.current, ["status", "--porcelain"])).out, generatedHooks = await generatedHooksOnly(state.current), onlyGeneratedHooks = Boolean(status) && status.split("\n").every(line => line.endsWith(" .githooks/") || line.endsWith(" .githooks")); if (status && !onlyGeneratedHooks) return bad("worktree is dirty; commit, stash, or discard changes before finish"); const tip = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (!tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`branch ${state.branch} is not merged into ${cfg.worktrees.primary_branch} in the primary checkout`); const removed = await git(state.primary, ["worktree", "remove", ...(onlyGeneratedHooks && generatedHooks ? ["--force"] : []), state.current]); if (removed.code) return bad(removed.err || removed.out); if (deleteBranch) { const now = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (now !== tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`worktree removed, but branch ${state.branch} was not deleted: its tip changed or is no longer merged into ${cfg.worktrees.primary_branch}`); const deleted = await git(state.primary, ["branch", "-D", state.branch]); if (deleted.code) return bad(`worktree removed, but branch deletion failed: ${deleted.err || deleted.out}`); } return { ok: true, message: `removed merged worktree ${state.current}${deleteBranch ? ` and branch ${state.branch}` : ""}` }; }