@a2f0/agent-tool 0.1.46 → 0.1.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2f0/agent-tool",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "packageManager": "bun@1.4.4",
5
5
  "type": "module",
6
6
  "description": "Portable cross-agent review, PR workflows, and shared skills",
package/src/cli.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { readFileSync, writeFileSync, writeSync } from "node:fs";
3
2
  import path from "node:path";
4
3
  import { DEFAULT_CONFIG, loadConfig } from "./config";
@@ -28,6 +27,7 @@ import { hooksCommand } from "./workflows/hooks";
28
27
  import { repoCommand, policyCommand, toolchainCommand } from "./workflows/repository";
29
28
  import { prWorkflowCommand } from "./workflows/pr";
30
29
  import { releaseCommand } from "./release/commands";
30
+ import { checkoutRoot } from "./workflows/core";
31
31
  import { procCommand, teardownCommand } from "./processes/commands";
32
32
 
33
33
  export const HELP = `agent-tool ${metadata.version}
@@ -96,9 +96,7 @@ returned; inspect VERDICT before shipping. Skill install previews unless --apply
96
96
 
97
97
  function output(value: unknown): void { writeSync(1, `${JSON.stringify(value, null, 2)}\n`); }
98
98
  function usage(condition: boolean, message: string): void { if (!condition) throw new Error(message); }
99
- function gitRoot(): string {
100
- return execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
101
- }
99
+ function gitRoot(): string { return checkoutRoot(); }
102
100
 
103
101
  export function main(argv = process.argv.slice(2)): number | Promise<number> {
104
102
  const args = [...argv];
@@ -1,5 +1,6 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
- import { writeSync } from "node:fs";
2
+ import { lstatSync, realpathSync, writeSync } from "node:fs";
3
+ import path from "node:path";
3
4
  import { gitEnvironment } from "../git/environment";
4
5
 
5
6
  export function output(value: unknown): void { writeSync(1, `${JSON.stringify(value, null, 2)}\n`); }
@@ -23,8 +24,37 @@ export function currentBranch(): string {
23
24
  assert(branch, "Detached HEAD; supply a branch name to checks or attach a branch before mutations.");
24
25
  return branch;
25
26
  }
27
+ /**
28
+ * Git skips a `.git` it cannot open (unreadable, or without HEAD while it is
29
+ * rewritten) and keeps searching upward, so a checkout nested inside another
30
+ * repository, such as a workspace's `checkouts/<name>`, would silently resolve
31
+ * to the outer one. Refuse that instead of acting on another repository.
32
+ */
33
+ export function assertOwnCheckout(root: string, cwd = process.cwd()): void {
34
+ const top = realpathSync(root);
35
+ const relative = path.relative(top, realpathSync(cwd));
36
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return;
37
+ let current = top;
38
+ for (const part of relative.split(path.sep)) {
39
+ current = path.join(current, part);
40
+ assert(!hasEntry(path.join(current, ".git")), `${current} has its own .git, but Git resolved the enclosing repository ${top}; the nested checkout's Git directory is unreadable or incomplete (mid-checkout?). Retry once it is readable.`);
41
+ }
42
+ }
43
+ /** Whether anything, even a dangling link, exists at `file`; an entry that cannot be inspected counts. */
44
+ function hasEntry(file: string): boolean {
45
+ try { lstatSync(file); return true; } catch (error) { return (error as NodeJS.ErrnoException).code !== "ENOENT"; }
46
+ }
47
+ /** The top level of the checkout Git resolves from `cwd`, never an enclosing repository. */
48
+ export function checkoutRoot(cwd = process.cwd()): string {
49
+ const root = git(["rev-parse", "--show-toplevel"], cwd);
50
+ assertOwnCheckout(root, cwd);
51
+ return root;
52
+ }
26
53
  export interface Repository { nameWithOwner: string; defaultBranchRef: { name: string }; url: string; sshUrl: string }
27
54
  export function repository(): Repository {
55
+ // gh resolves the repository from the same Git discovery; outside any checkout it may still use GH_REPO.
56
+ const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], env: gitEnvironment() });
57
+ if (top.status === 0) assertOwnCheckout(top.stdout.trim());
28
58
  const repo = json<Repository>(["repo", "view", "--json", "nameWithOwner,defaultBranchRef,url,sshUrl"]);
29
59
  assert(/^[^/\s]+\/[^/\s]+$/.test(repo.nameWithOwner) && repo.defaultBranchRef?.name, "Could not resolve repository identity and default branch.");
30
60
  return repo;