@great-arrow/cli 0.1.8 → 0.1.10

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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/dist/index.js +1321 -63
  3. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -5253,7 +5253,7 @@ var require_minimist = __commonJS({
5253
5253
  var require_rc = __commonJS({
5254
5254
  "node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
5255
5255
  var cc = require_utils();
5256
- var join3 = __require("path").join;
5256
+ var join5 = __require("path").join;
5257
5257
  var deepExtend = require_deep_extend();
5258
5258
  var etc = "/etc";
5259
5259
  var win = process.platform === "win32";
@@ -5278,15 +5278,15 @@ var require_rc = __commonJS({
5278
5278
  }
5279
5279
  if (!win)
5280
5280
  [
5281
- join3(etc, name, "config"),
5282
- join3(etc, name + "rc")
5281
+ join5(etc, name, "config"),
5282
+ join5(etc, name + "rc")
5283
5283
  ].forEach(addConfigFile);
5284
5284
  if (home)
5285
5285
  [
5286
- join3(home, ".config", name, "config"),
5287
- join3(home, ".config", name),
5288
- join3(home, "." + name, "config"),
5289
- join3(home, "." + name + "rc")
5286
+ join5(home, ".config", name, "config"),
5287
+ join5(home, ".config", name),
5288
+ join5(home, "." + name, "config"),
5289
+ join5(home, "." + name + "rc")
5290
5290
  ].forEach(addConfigFile);
5291
5291
  addConfigFile(cc.find("." + name + "rc"));
5292
5292
  if (env4.config) addConfigFile(env4.config);
@@ -6624,11 +6624,11 @@ var require_util = __commonJS({
6624
6624
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
6625
6625
  return name2;
6626
6626
  }
6627
- const dirname3 = path9.dirname(name2);
6628
- if (dirname3 === name2) {
6627
+ const dirname4 = path9.dirname(name2);
6628
+ if (dirname4 === name2) {
6629
6629
  return original;
6630
6630
  }
6631
- return find(dirname3, original);
6631
+ return find(dirname4, original);
6632
6632
  } catch (error) {
6633
6633
  if (name2 === original) {
6634
6634
  if (error.code === "ENOENT") {
@@ -9278,6 +9278,13 @@ function networkError(message, hint) {
9278
9278
  hint ?? "Check your internet connection or run 'gad doctor'."
9279
9279
  );
9280
9280
  }
9281
+ function validationError(message, hint) {
9282
+ return new CliError(
9283
+ message,
9284
+ 3 /* VALIDATION_ERROR */,
9285
+ hint ?? "Check the command usage with --help."
9286
+ );
9287
+ }
9281
9288
  function mapStatusToExitCode(status) {
9282
9289
  if (status === 401 || status === 403) {
9283
9290
  return 1 /* AUTH_ERROR */;
@@ -9302,8 +9309,9 @@ function isNodeError(value) {
9302
9309
 
9303
9310
  // src/lib/version.ts
9304
9311
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9305
- var CLI_VERSION = "0.1.8";
9306
- var GAD_INSTALL_VERSION = "2026.06.22";
9312
+ var CLI_VERSION = "0.1.10";
9313
+ var GAD_INSTALL_VERSION = "2026.07.06";
9314
+ var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9307
9315
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
9308
9316
  var GAD_CLI_VERSION_HEADER = "X-GAD-CLI-Version";
9309
9317
 
@@ -9543,12 +9551,12 @@ h1{font-size:1.25rem;margin:0 0 .5rem}p{color:#9a9aa3;margin:0}</style></head>
9543
9551
  <body><div class="card"><h1>${ok ? "\u2713 Signed in to Great Arrow" : "\u2717 Sign-in failed"}</h1>
9544
9552
  <p>${escapeHtml(detail)}</p></div></body></html>`;
9545
9553
  function listen(port) {
9546
- return new Promise((resolve, reject) => {
9554
+ return new Promise((resolve3, reject) => {
9547
9555
  const server = createServer();
9548
9556
  server.on("error", reject);
9549
9557
  server.listen(port, LOOPBACK_HOST, () => {
9550
9558
  const addr = server.address();
9551
- resolve({ server, port: addr.port });
9559
+ resolve3({ server, port: addr.port });
9552
9560
  });
9553
9561
  });
9554
9562
  }
@@ -9573,7 +9581,7 @@ async function startLoopbackServer(opts) {
9573
9581
  port,
9574
9582
  close: () => server.close(),
9575
9583
  waitForCallback({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
9576
- return new Promise((resolve, reject) => {
9584
+ return new Promise((resolve3, reject) => {
9577
9585
  const timer = setTimeout(() => {
9578
9586
  server.close();
9579
9587
  reject(new Error("Timed out waiting for the browser callback."));
@@ -9613,7 +9621,7 @@ async function startLoopbackServer(opts) {
9613
9621
  "You can close this tab and return to your terminal."
9614
9622
  )
9615
9623
  );
9616
- settle(() => resolve({ code, state }));
9624
+ settle(() => resolve3({ code, state }));
9617
9625
  });
9618
9626
  });
9619
9627
  }
@@ -9867,11 +9875,11 @@ async function confirm(message) {
9867
9875
  input: process.stdin,
9868
9876
  output: process.stdout
9869
9877
  });
9870
- return new Promise((resolve) => {
9878
+ return new Promise((resolve3) => {
9871
9879
  rl.question(`${message} [y/N] `, (answer) => {
9872
9880
  rl.close();
9873
9881
  const normalized = answer.trim().toLowerCase();
9874
- resolve(normalized === "y" || normalized === "yes");
9882
+ resolve3(normalized === "y" || normalized === "yes");
9875
9883
  });
9876
9884
  });
9877
9885
  }
@@ -10038,10 +10046,10 @@ function maskToken(token) {
10038
10046
  }
10039
10047
  function openBrowser(url) {
10040
10048
  if (!/^https?:\/\//i.test(url)) return Promise.resolve(false);
10041
- return new Promise((resolve) => {
10049
+ return new Promise((resolve3) => {
10042
10050
  const platform = process.platform;
10043
10051
  const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
10044
- execFile(cmd, args, (error) => resolve(!error));
10052
+ execFile(cmd, args, (error) => resolve3(!error));
10045
10053
  });
10046
10054
  }
10047
10055
  var loginCommand = new Command("login").description("Authenticate the CLI with your Great Arrow Digital account").option(
@@ -10314,7 +10322,7 @@ function describeError(error) {
10314
10322
  return String(error);
10315
10323
  }
10316
10324
  function sleep(ms) {
10317
- return new Promise((resolve) => setTimeout(resolve, ms));
10325
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
10318
10326
  }
10319
10327
  var GadClient = class {
10320
10328
  token;
@@ -10603,10 +10611,10 @@ function whoamiCommand() {
10603
10611
 
10604
10612
  // src/lib/stdin.ts
10605
10613
  function readStream(stream) {
10606
- return new Promise((resolve, reject) => {
10614
+ return new Promise((resolve3, reject) => {
10607
10615
  const chunks = [];
10608
10616
  stream.on("data", (chunk) => chunks.push(chunk));
10609
- stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
10617
+ stream.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
10610
10618
  stream.on("error", reject);
10611
10619
  });
10612
10620
  }
@@ -11228,14 +11236,928 @@ var claimCommand = new Command("claim").description("Preflight + atomically clai
11228
11236
  );
11229
11237
  var claim_default = claimCommand;
11230
11238
 
11239
+ // src/commands/work.ts
11240
+ import { readFileSync } from "node:fs";
11241
+
11242
+ // src/lib/work-status.ts
11243
+ import { execFileSync as execFileSync2 } from "node:child_process";
11244
+ function run(command, args, cwd) {
11245
+ try {
11246
+ const stdout = execFileSync2(command, args, {
11247
+ cwd,
11248
+ encoding: "utf8",
11249
+ stdio: ["ignore", "pipe", "pipe"],
11250
+ maxBuffer: 10 * 1024 * 1024
11251
+ });
11252
+ return { status: 0, stdout, stderr: "" };
11253
+ } catch (err) {
11254
+ const e = err;
11255
+ return {
11256
+ status: e.status ?? 1,
11257
+ stdout: e.stdout ?? "",
11258
+ stderr: e.stderr ?? ""
11259
+ };
11260
+ }
11261
+ }
11262
+ function git(args, cwd) {
11263
+ return run("git", args, cwd);
11264
+ }
11265
+ function requireGit(args, cwd) {
11266
+ const result = git(args, cwd);
11267
+ if (result.status !== 0) {
11268
+ throw new Error(
11269
+ `git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`
11270
+ );
11271
+ }
11272
+ return result.stdout.trim();
11273
+ }
11274
+ function isAncestor(ancestor, ref, cwd) {
11275
+ return git(["merge-base", "--is-ancestor", ancestor, ref], cwd).status === 0;
11276
+ }
11277
+ function revListCounts(head, cwd) {
11278
+ const result = git(
11279
+ ["rev-list", "--left-right", "--count", `origin/main...${head}`],
11280
+ cwd
11281
+ );
11282
+ if (result.status !== 0) return { behindMain: 0, aheadMain: 0 };
11283
+ const [behindRaw, aheadRaw] = result.stdout.trim().split(/\s+/);
11284
+ const behind = Number(behindRaw ?? 0);
11285
+ const ahead = Number(aheadRaw ?? 0);
11286
+ return {
11287
+ behindMain: Number.isFinite(behind) ? behind : 0,
11288
+ aheadMain: Number.isFinite(ahead) ? ahead : 0
11289
+ };
11290
+ }
11291
+ function parseWorktrees(raw) {
11292
+ const worktrees = [];
11293
+ let current = {};
11294
+ function flush() {
11295
+ if (current.path && current.head) {
11296
+ worktrees.push({
11297
+ path: current.path,
11298
+ branch: current.branch ?? "(detached)",
11299
+ head: current.head
11300
+ });
11301
+ }
11302
+ current = {};
11303
+ }
11304
+ for (const line of raw.split("\n")) {
11305
+ if (line === "") {
11306
+ flush();
11307
+ } else if (line.startsWith("worktree ")) {
11308
+ current.path = line.slice("worktree ".length);
11309
+ } else if (line.startsWith("HEAD ")) {
11310
+ current.head = line.slice("HEAD ".length);
11311
+ } else if (line.startsWith("branch refs/heads/")) {
11312
+ current.branch = line.slice("branch refs/heads/".length);
11313
+ } else if (line === "detached") {
11314
+ current.branch = "(detached)";
11315
+ }
11316
+ }
11317
+ flush();
11318
+ return worktrees;
11319
+ }
11320
+ function classifyWorktree(audit) {
11321
+ if (audit.path.includes("/.claude/worktrees/")) return "agent-harness";
11322
+ if (audit.dirty) return "review-dirty";
11323
+ if (audit.mergedToMain) return "cleanup-ready";
11324
+ if (audit.upstreamGone) return "upstream-gone";
11325
+ if (!audit.upstream) return "local-rescue";
11326
+ return "active-unmerged";
11327
+ }
11328
+ function auditWorktrees(root) {
11329
+ const raw = requireGit(["worktree", "list", "--porcelain"], root);
11330
+ return parseWorktrees(raw).map((worktree) => {
11331
+ const status = git(["status", "--porcelain"], worktree.path).stdout.trim();
11332
+ const branchStatus = git(["status", "--short", "--branch"], worktree.path).stdout.split("\n")[0]?.trim();
11333
+ const upstream = git(
11334
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
11335
+ worktree.path
11336
+ );
11337
+ const counts = revListCounts(worktree.head, root);
11338
+ const baseAudit = {
11339
+ path: worktree.path,
11340
+ branch: worktree.branch,
11341
+ head: worktree.head,
11342
+ dirty: status.length > 0,
11343
+ mergedToMain: isAncestor(worktree.head, "origin/main", root),
11344
+ upstream: upstream.status === 0 ? upstream.stdout.trim() : null,
11345
+ upstreamGone: branchStatus?.includes("[gone]") ?? false,
11346
+ ...counts
11347
+ };
11348
+ return { ...baseAudit, classification: classifyWorktree(baseAudit) };
11349
+ });
11350
+ }
11351
+ function auditDetachedLocalBranches(root, checkedOutBranches) {
11352
+ const raw = requireGit(
11353
+ ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
11354
+ root
11355
+ );
11356
+ return raw.split("\n").map((branch) => branch.trim()).filter(Boolean).filter((branch) => !checkedOutBranches.has(branch)).map((branch) => {
11357
+ const head = requireGit(["rev-parse", branch], root);
11358
+ const counts = revListCounts(head, root);
11359
+ return {
11360
+ branch,
11361
+ head,
11362
+ mergedToMain: isAncestor(head, "origin/main", root),
11363
+ ...counts
11364
+ };
11365
+ });
11366
+ }
11367
+ function readOpenPullRequests(root) {
11368
+ const result = run(
11369
+ "gh",
11370
+ [
11371
+ "pr",
11372
+ "list",
11373
+ "--state",
11374
+ "open",
11375
+ "--limit",
11376
+ "50",
11377
+ "--json",
11378
+ "number,title,headRefName,mergeStateStatus,isDraft,url"
11379
+ ],
11380
+ root
11381
+ );
11382
+ if (result.status !== 0) return [];
11383
+ try {
11384
+ return JSON.parse(result.stdout);
11385
+ } catch {
11386
+ return [];
11387
+ }
11388
+ }
11389
+ async function readProductionVersion(apiUrl) {
11390
+ const controller = new AbortController();
11391
+ const timeout2 = setTimeout(() => controller.abort(), 8e3);
11392
+ try {
11393
+ const response = await fetch(`${apiUrl.replace(/\/$/, "")}/api/version`, {
11394
+ signal: controller.signal
11395
+ });
11396
+ if (!response.ok) return null;
11397
+ return await response.json();
11398
+ } catch {
11399
+ return null;
11400
+ } finally {
11401
+ clearTimeout(timeout2);
11402
+ }
11403
+ }
11404
+ function countBy(values) {
11405
+ return values.reduce(
11406
+ (counts, value) => {
11407
+ counts[value] = (counts[value] ?? 0) + 1;
11408
+ return counts;
11409
+ },
11410
+ {}
11411
+ );
11412
+ }
11413
+ async function collectWorkStatus(opts) {
11414
+ const root = requireGit(["rev-parse", "--show-toplevel"]);
11415
+ if (opts.fetchRemote !== false) {
11416
+ git(["fetch", "origin", "main", "--prune"], root);
11417
+ }
11418
+ const originMain = requireGit(["rev-parse", "origin/main"], root);
11419
+ const originMainSubject = requireGit(
11420
+ ["log", "-1", "--pretty=%s", "origin/main"],
11421
+ root
11422
+ );
11423
+ const currentBranch = requireGit(["branch", "--show-current"], root);
11424
+ const currentHead = requireGit(["rev-parse", "HEAD"], root);
11425
+ const currentClean = git(["status", "--porcelain"], root).stdout.trim().length === 0;
11426
+ const stashes = git(["stash", "list"], root).stdout.split("\n").map((line) => line.trim()).filter(Boolean);
11427
+ const worktrees = auditWorktrees(root);
11428
+ const checkedOutBranches = new Set(
11429
+ worktrees.map((worktree) => worktree.branch).filter((branch) => branch !== "(detached)")
11430
+ );
11431
+ const detachedBranches = auditDetachedLocalBranches(root, checkedOutBranches);
11432
+ const openPullRequests = readOpenPullRequests(root);
11433
+ const production = opts.includeProduction === false ? null : await readProductionVersion(opts.apiUrl);
11434
+ const productionSha = production?.sha ?? null;
11435
+ const productionContainedByMain = productionSha ? isAncestor(productionSha, "origin/main", root) : null;
11436
+ const productionAtMain = productionSha === originMain;
11437
+ const detachedUnmerged = detachedBranches.filter(
11438
+ (branch) => !branch.mergedToMain
11439
+ );
11440
+ const blockedPrs = openPullRequests.filter(
11441
+ (pr) => pr.isDraft || !["CLEAN", "HAS_HOOKS"].includes(pr.mergeStateStatus)
11442
+ );
11443
+ return {
11444
+ root,
11445
+ current: {
11446
+ branch: currentBranch,
11447
+ head: currentHead,
11448
+ clean: currentClean,
11449
+ containedByMain: isAncestor(currentHead, "origin/main", root)
11450
+ },
11451
+ originMain: { sha: originMain, subject: originMainSubject },
11452
+ production: production ? {
11453
+ ...production,
11454
+ containedByMain: productionContainedByMain,
11455
+ atOriginMain: productionAtMain
11456
+ } : null,
11457
+ summary: {
11458
+ worktrees: worktrees.length,
11459
+ worktreeClassifications: countBy(
11460
+ worktrees.map((worktree) => worktree.classification)
11461
+ ),
11462
+ detachedLocalBranches: detachedBranches.length,
11463
+ detachedUnmergedBranches: detachedUnmerged.length,
11464
+ stashes: stashes.length,
11465
+ openPullRequests: openPullRequests.length,
11466
+ blockedPullRequests: blockedPrs.length
11467
+ },
11468
+ worktrees,
11469
+ detachedBranches,
11470
+ openPullRequests,
11471
+ strictFailures: []
11472
+ };
11473
+ }
11474
+
11475
+ // src/commands/work.ts
11476
+ function writeJson(value) {
11477
+ process.stdout.write(`${JSON.stringify(value)}
11478
+ `);
11479
+ }
11480
+ function shortSha(value) {
11481
+ return typeof value === "string" ? value.slice(0, 12) : "";
11482
+ }
11483
+ function collectOptions(opts) {
11484
+ const result = { apiUrl: opts.apiUrl };
11485
+ if (opts.fetch !== void 0) result.fetchRemote = opts.fetch;
11486
+ if (opts.production !== void 0) result.includeProduction = opts.production;
11487
+ return result;
11488
+ }
11489
+ function workCommand() {
11490
+ const cmd = new Command("work").description(
11491
+ "Inspect and publish unshipped work / work-at-risk state"
11492
+ );
11493
+ cmd.command("status").description("Show canonical work-at-risk state for the active workspace").option("--local", "Run a local git/worktree audit instead of app state").option("--no-fetch", "Skip git fetch when using --local").option(
11494
+ "--no-production",
11495
+ "Skip production version probe when using --local"
11496
+ ).option("--limit <n>", "Maximum app rows to show", "20").action(
11497
+ async (options, actionCmd) => {
11498
+ const output = globalOutputOpts(actionCmd);
11499
+ const fmt = createFormatter({
11500
+ ...output,
11501
+ isTTY: isTTY()
11502
+ });
11503
+ const auth = await resolveAuth();
11504
+ if (options.local) {
11505
+ const report = await collectWorkStatus({
11506
+ ...collectOptions({
11507
+ apiUrl: auth.apiUrl,
11508
+ fetch: options.fetch,
11509
+ production: options.production
11510
+ })
11511
+ });
11512
+ if (output.json) return writeJson(report);
11513
+ fmt.detail(
11514
+ {
11515
+ current: `${report.current.branch || "(detached)"} @ ${shortSha(
11516
+ report.current.head
11517
+ )}`,
11518
+ clean: report.current.clean,
11519
+ worktrees: report.summary.worktrees,
11520
+ dirty: report.summary.worktreeClassifications["review-dirty"] ?? 0,
11521
+ activeUnmerged: report.summary.worktreeClassifications["active-unmerged"] ?? 0,
11522
+ detachedUnmerged: report.summary.detachedUnmergedBranches,
11523
+ blockedPrs: report.summary.blockedPullRequests,
11524
+ stashes: report.summary.stashes
11525
+ },
11526
+ [
11527
+ { key: "current", header: "Current" },
11528
+ { key: "clean", header: "Clean" },
11529
+ { key: "worktrees", header: "Worktrees" },
11530
+ { key: "dirty", header: "Dirty" },
11531
+ { key: "activeUnmerged", header: "Unmerged" },
11532
+ { key: "detachedUnmerged", header: "Detached" },
11533
+ { key: "blockedPrs", header: "Blocked PRs" },
11534
+ { key: "stashes", header: "Stashes" }
11535
+ ]
11536
+ );
11537
+ return;
11538
+ }
11539
+ const client = new GadClient({
11540
+ token: auth.token,
11541
+ apiUrl: auth.apiUrl
11542
+ });
11543
+ const data = await client.get("/api/work/status", {
11544
+ limit: String(Number.parseInt(options.limit ?? "20", 10) || 20)
11545
+ });
11546
+ if (output.json) return writeJson(data);
11547
+ const items = Array.isArray(data.items) ? data.items : [];
11548
+ fmt.table(items, [
11549
+ { key: "riskLevel", header: "Risk" },
11550
+ { key: "title", header: "Item" },
11551
+ { key: "recommendedAction", header: "Action" }
11552
+ ]);
11553
+ }
11554
+ );
11555
+ cmd.command("publish").description("Publish the local release audit into the active workspace").option("--file <path>", "Publish a precomputed release-audit JSON file").option("--no-fetch", "Skip git fetch before collecting local status").option("--no-production", "Skip production version probe").option("--machine-id <id>", "Stable machine/session identifier").action(
11556
+ async (options, actionCmd) => {
11557
+ const output = globalOutputOpts(actionCmd);
11558
+ const fmt = createFormatter({
11559
+ ...output,
11560
+ isTTY: isTTY()
11561
+ });
11562
+ const auth = await resolveAuth();
11563
+ const client = new GadClient({
11564
+ token: auth.token,
11565
+ apiUrl: auth.apiUrl
11566
+ });
11567
+ const report = options.file ? JSON.parse(readFileSync(options.file, "utf8")) : await collectWorkStatus({
11568
+ ...collectOptions({
11569
+ apiUrl: auth.apiUrl,
11570
+ fetch: options.fetch,
11571
+ production: options.production
11572
+ })
11573
+ });
11574
+ const result = await client.post("/api/work/publish", {
11575
+ schemaVersion: 1,
11576
+ kind: "release-logjam",
11577
+ clientSource: "gad-cli",
11578
+ machineId: options.machineId,
11579
+ report
11580
+ });
11581
+ if (output.json) return writeJson(result);
11582
+ const r = result;
11583
+ fmt.success(
11584
+ `Published work-at-risk snapshot (${r.itemCount ?? 0} item${r.itemCount === 1 ? "" : "s"}, ${r.resolvedCount ?? 0} resolved).`
11585
+ );
11586
+ if (Array.isArray(r.topItems) && r.topItems.length > 0) {
11587
+ fmt.table(r.topItems, [
11588
+ { key: "riskLevel", header: "Risk" },
11589
+ { key: "title", header: "Item" },
11590
+ { key: "recommendedAction", header: "Action" }
11591
+ ]);
11592
+ }
11593
+ }
11594
+ );
11595
+ return cmd;
11596
+ }
11597
+ var work_default = workCommand;
11598
+
11599
+ // src/commands/worktree.ts
11600
+ import { execFileSync as execFileSync3 } from "node:child_process";
11601
+ import { existsSync, readFileSync as readFileSync2, statSync, writeFileSync } from "node:fs";
11602
+ import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
11603
+ var realExec = (cmd, args, opts) => {
11604
+ const out = execFileSync3(cmd, args, {
11605
+ cwd: opts?.cwd,
11606
+ encoding: "utf8",
11607
+ stdio: opts?.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
11608
+ });
11609
+ return typeof out === "string" ? out : "";
11610
+ };
11611
+ function assertCleanTree(statusPorcelain) {
11612
+ if (statusPorcelain.trim().length === 0) return;
11613
+ throw validationError(
11614
+ "Working tree is dirty \u2014 commit, stash, or clean up uncommitted changes first.",
11615
+ "A new task worktree must start from a clean state so another task's in-flight files can't leak into your first commit."
11616
+ );
11617
+ }
11618
+ function parseDefaultBranch(symbolicRef) {
11619
+ if (!symbolicRef) return null;
11620
+ const match = symbolicRef.trim().match(/^refs\/remotes\/origin\/(.+)$/);
11621
+ return match ? match[1] : null;
11622
+ }
11623
+ function detectPackageManager(repoRoot) {
11624
+ if (existsSync(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
11625
+ if (existsSync(join2(repoRoot, "bun.lockb"))) return "bun";
11626
+ if (existsSync(join2(repoRoot, "yarn.lock"))) return "yarn";
11627
+ return "npm";
11628
+ }
11629
+ function defaultWorktreeDir(repoRoot, branch) {
11630
+ const slug = (branch.split("/").pop() ?? branch).replace(
11631
+ /[^A-Za-z0-9._-]+/g,
11632
+ "-"
11633
+ );
11634
+ return join2(dirname2(repoRoot), `${basename(repoRoot)}-${slug}`);
11635
+ }
11636
+ var AGENT_PROCESS_RE = /(^|\/)(claude|kiro|cursor|codex)/i;
11637
+ function psField(pid, field) {
11638
+ try {
11639
+ return execFileSync3("ps", ["-o", `${field}=`, "-p", String(pid)], {
11640
+ encoding: "utf8"
11641
+ }).trim();
11642
+ } catch {
11643
+ return "";
11644
+ }
11645
+ }
11646
+ function findAgentPid(startPid) {
11647
+ let pid = Number(startPid);
11648
+ for (let i = 0; i < 40 && pid > 1; i++) {
11649
+ if (AGENT_PROCESS_RE.test(basename(psField(pid, "comm")))) return pid;
11650
+ const ppid = Number(psField(pid, "ppid"));
11651
+ if (!ppid || ppid === pid) break;
11652
+ pid = ppid;
11653
+ }
11654
+ return null;
11655
+ }
11656
+ function ownGitDir(worktreeDir) {
11657
+ const dotGit = join2(worktreeDir, ".git");
11658
+ if (!existsSync(dotGit)) return null;
11659
+ if (statSync(dotGit).isDirectory()) return dotGit;
11660
+ const match = readFileSync2(dotGit, "utf8").trim().match(/^gitdir:\s*(.+)$/m);
11661
+ if (!match) return null;
11662
+ return isAbsolute(match[1]) ? match[1] : resolve(worktreeDir, match[1]);
11663
+ }
11664
+ function stampWorktreeOwner(worktreeDir) {
11665
+ try {
11666
+ if (process.platform === "win32") return null;
11667
+ const gitDir = ownGitDir(worktreeDir);
11668
+ if (!gitDir) return null;
11669
+ const ownerPid = findAgentPid(process.ppid);
11670
+ if (!ownerPid) return null;
11671
+ writeFileSync(
11672
+ join2(gitDir, "agent-owner.json"),
11673
+ JSON.stringify(
11674
+ {
11675
+ ownerPid,
11676
+ comm: basename(psField(ownerPid, "comm")),
11677
+ stampedAt: (/* @__PURE__ */ new Date()).toISOString()
11678
+ },
11679
+ null,
11680
+ 2
11681
+ )
11682
+ );
11683
+ return ownerPid;
11684
+ } catch {
11685
+ return null;
11686
+ }
11687
+ }
11688
+ function createTaskWorktree(opts) {
11689
+ const exec2 = opts.exec ?? realExec;
11690
+ const cwd = opts.cwd ?? process.cwd();
11691
+ const { branch } = opts;
11692
+ let repoRoot;
11693
+ try {
11694
+ repoRoot = exec2("git", ["rev-parse", "--show-toplevel"], { cwd }).trim();
11695
+ } catch {
11696
+ throw validationError(
11697
+ "Not inside a git repository.",
11698
+ "Run this from the repo you want a task worktree for."
11699
+ );
11700
+ }
11701
+ assertCleanTree(exec2("git", ["status", "--porcelain"], { cwd: repoRoot }));
11702
+ let fetched = true;
11703
+ try {
11704
+ exec2("git", ["fetch", "origin", "--quiet"], { cwd: repoRoot });
11705
+ } catch {
11706
+ fetched = false;
11707
+ }
11708
+ let base = null;
11709
+ try {
11710
+ base = parseDefaultBranch(
11711
+ exec2("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], {
11712
+ cwd: repoRoot
11713
+ })
11714
+ );
11715
+ } catch {
11716
+ base = null;
11717
+ }
11718
+ if (!base) {
11719
+ for (const candidate of ["main", "master"]) {
11720
+ try {
11721
+ exec2(
11722
+ "git",
11723
+ [
11724
+ "show-ref",
11725
+ "--verify",
11726
+ "--quiet",
11727
+ `refs/remotes/origin/${candidate}`
11728
+ ],
11729
+ { cwd: repoRoot }
11730
+ );
11731
+ base = candidate;
11732
+ break;
11733
+ } catch {
11734
+ }
11735
+ }
11736
+ }
11737
+ if (!base) {
11738
+ throw validationError(
11739
+ "Could not determine the origin default branch.",
11740
+ "Ensure the repo has an 'origin' remote with a main or master branch (git fetch origin)."
11741
+ );
11742
+ }
11743
+ let branchExists = false;
11744
+ try {
11745
+ exec2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
11746
+ cwd: repoRoot
11747
+ });
11748
+ branchExists = true;
11749
+ } catch {
11750
+ branchExists = false;
11751
+ }
11752
+ if (branchExists) {
11753
+ throw validationError(
11754
+ `Branch '${branch}' already exists locally \u2014 pick a new name or remove it.`
11755
+ );
11756
+ }
11757
+ const dir = opts.dir ? resolve(cwd, opts.dir) : defaultWorktreeDir(repoRoot, branch);
11758
+ const baseRef = `origin/${base}`;
11759
+ exec2("git", ["worktree", "add", dir, "-b", branch, baseRef], {
11760
+ cwd: repoRoot
11761
+ });
11762
+ const stampedPid = opts.stamp === false ? null : stampWorktreeOwner(dir);
11763
+ const packageManager = detectPackageManager(repoRoot);
11764
+ let installed = false;
11765
+ if (opts.install !== false && existsSync(join2(repoRoot, "package.json"))) {
11766
+ exec2(packageManager, ["install"], { cwd: dir, inherit: true });
11767
+ installed = true;
11768
+ }
11769
+ return {
11770
+ dir,
11771
+ branch,
11772
+ baseRef,
11773
+ packageManager,
11774
+ installed,
11775
+ fetched,
11776
+ stampedPid
11777
+ };
11778
+ }
11779
+ var worktreeCommand = new Command("worktree").description(
11780
+ "Isolated task worktrees for parallel agents"
11781
+ );
11782
+ worktreeCommand.command("new").description(
11783
+ "Create a fresh worktree on a new branch off the origin default branch, with a real install"
11784
+ ).argument("<branch>", "New branch name (e.g. feat/fix-login)").argument("[dir]", "Target directory (default: sibling <repo>-<slug>)").option("--no-install", "Skip the dependency install step").action((branch, dir, _options, cmd) => {
11785
+ const parentOpts = cmd.optsWithGlobals();
11786
+ const fmt = createFormatter({
11787
+ json: parentOpts.json || false,
11788
+ quiet: parentOpts.quiet || false,
11789
+ isTTY: isTTY(),
11790
+ noColor: parentOpts.noColor || false
11791
+ });
11792
+ const localOpts = cmd.opts();
11793
+ const result = createTaskWorktree({
11794
+ branch,
11795
+ ...dir ? { dir } : {},
11796
+ install: localOpts.install
11797
+ });
11798
+ if (parentOpts.json) {
11799
+ process.stdout.write(JSON.stringify(result) + "\n");
11800
+ return;
11801
+ }
11802
+ if (!result.fetched) {
11803
+ fmt.warn(
11804
+ "Could not fetch origin \u2014 branched off the last locally known " + result.baseRef + "."
11805
+ );
11806
+ }
11807
+ fmt.success(
11808
+ `Worktree ready: ${result.dir} on ${result.branch} (based on ${result.baseRef}).`
11809
+ );
11810
+ if (result.stampedPid) {
11811
+ fmt.info(
11812
+ "Stamped this agent session as the worktree owner (pairs with 'gad install-hooks')."
11813
+ );
11814
+ }
11815
+ if (!result.installed) {
11816
+ fmt.info(
11817
+ `Install skipped \u2014 run '${result.packageManager} install' inside the worktree before building.`
11818
+ );
11819
+ }
11820
+ fmt.info(`Next: cd ${result.dir}`);
11821
+ fmt.info(`Done: git worktree remove ${result.dir}`);
11822
+ });
11823
+ var worktree_default = worktreeCommand;
11824
+
11825
+ // src/commands/install-hooks.ts
11826
+ import { execFileSync as execFileSync4 } from "node:child_process";
11827
+ import {
11828
+ chmodSync,
11829
+ existsSync as existsSync2,
11830
+ mkdirSync,
11831
+ readFileSync as readFileSync3,
11832
+ writeFileSync as writeFileSync2
11833
+ } from "node:fs";
11834
+ import { isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
11835
+ var GUARD_MARKER = "# gad-agent-guard v1";
11836
+ var SCHEMA_AUDIT_MARKER = "# gad-schema-audit v1";
11837
+ var GUARD_FILE = "gad-commit-guard.mjs";
11838
+ var realExec2 = (cmd, args, opts) => execFileSync4(cmd, args, {
11839
+ cwd: opts?.cwd,
11840
+ encoding: "utf8",
11841
+ stdio: ["ignore", "pipe", "pipe"]
11842
+ });
11843
+ function buildGuardScript() {
11844
+ return `#!/usr/bin/env node
11845
+ // gad-agent-guard v1 \u2014 pre-commit backstop against cross-worktree clobber.
11846
+ // Installed by \`gad install-hooks\`. Blocks a commit made inside a worktree
11847
+ // owned by a DIFFERENT still-live agent session. FAILS OPEN on every
11848
+ // ambiguity (no marker, dead owner, human committer, unsupported platform).
11849
+ // Deliberate override: ALLOW_FOREIGN_WORKTREE=1.
11850
+ import { existsSync, statSync, readFileSync } from "node:fs";
11851
+ import { join, basename, isAbsolute, resolve } from "node:path";
11852
+ import { execFileSync } from "node:child_process";
11853
+
11854
+ const OWNER_FILE = "agent-owner.json";
11855
+ const AGENT_RE = /(^|\\/)(claude|kiro|cursor|codex)/i;
11856
+
11857
+ function psField(pid, field) {
11858
+ try {
11859
+ return execFileSync("ps", ["-o", field + "=", "-p", String(pid)], {
11860
+ encoding: "utf8",
11861
+ }).trim();
11862
+ } catch {
11863
+ return "";
11864
+ }
11865
+ }
11866
+
11867
+ function ancestry(startPid) {
11868
+ const pids = new Set();
11869
+ let pid = Number(startPid);
11870
+ for (let i = 0; i < 40 && pid > 1; i++) {
11871
+ pids.add(pid);
11872
+ const ppid = Number(psField(pid, "ppid"));
11873
+ if (!ppid || ppid === pid) break;
11874
+ pid = ppid;
11875
+ }
11876
+ return pids;
11877
+ }
11878
+
11879
+ function isAlive(pid) {
11880
+ try {
11881
+ process.kill(pid, 0);
11882
+ return true;
11883
+ } catch {
11884
+ return false;
11885
+ }
11886
+ }
11887
+
11888
+ function ownGitDir(cwd) {
11889
+ const dotGit = join(cwd, ".git");
11890
+ if (!existsSync(dotGit)) return null;
11891
+ if (statSync(dotGit).isDirectory()) return dotGit;
11892
+ const m = readFileSync(dotGit, "utf8").trim().match(/^gitdir:\\s*(.+)$/m);
11893
+ if (!m) return null;
11894
+ return isAbsolute(m[1]) ? m[1] : resolve(cwd, m[1]);
11895
+ }
11896
+
11897
+ function evaluate(cwd, env) {
11898
+ if (env.ALLOW_FOREIGN_WORKTREE) return { block: false, reason: "override" };
11899
+ if (process.platform === "win32")
11900
+ return { block: false, reason: "unsupported-platform" };
11901
+ const isAgent =
11902
+ env.CLAUDECODE ||
11903
+ env.CLAUDE_CODE ||
11904
+ env.KIRO_AGENT ||
11905
+ env.CURSOR_AGENT ||
11906
+ env.CODEX_AGENT ||
11907
+ env.AGENT_SESSION;
11908
+ if (!isAgent) return { block: false, reason: "not-an-agent" };
11909
+ const gd = ownGitDir(cwd);
11910
+ if (!gd) return { block: false, reason: "no-gitdir" };
11911
+ const markerPath = join(gd, OWNER_FILE);
11912
+ if (!existsSync(markerPath)) return { block: false, reason: "no-marker" };
11913
+ let owner;
11914
+ try {
11915
+ owner = JSON.parse(readFileSync(markerPath, "utf8"));
11916
+ } catch {
11917
+ return { block: false, reason: "marker-unreadable" };
11918
+ }
11919
+ if (!owner || !owner.ownerPid)
11920
+ return { block: false, reason: "marker-no-owner" };
11921
+ const anc = ancestry(process.ppid);
11922
+ if (anc.has(Number(owner.ownerPid)))
11923
+ return { block: false, reason: "owner-match" };
11924
+ if (!isAlive(Number(owner.ownerPid)))
11925
+ return { block: false, reason: "owner-dead" };
11926
+ return { block: true, reason: "foreign-owner ownerPid=" + owner.ownerPid };
11927
+ }
11928
+
11929
+ const { block, reason } = evaluate(process.cwd(), process.env);
11930
+ if (block) {
11931
+ console.error("");
11932
+ console.error(
11933
+ "gad-agent-guard: commit blocked \u2014 this worktree belongs to a DIFFERENT live agent session.",
11934
+ );
11935
+ console.error(" " + reason);
11936
+ console.error(
11937
+ " You are about to commit another agent's in-flight work. Use your own worktree:",
11938
+ );
11939
+ console.error(" gad worktree new <branch>");
11940
+ console.error(" Deliberate override: ALLOW_FOREIGN_WORKTREE=1");
11941
+ console.error("");
11942
+ process.exit(1);
11943
+ }
11944
+ `;
11945
+ }
11946
+ function buildHookBlock() {
11947
+ return `${GUARD_MARKER} (managed by \`gad install-hooks\` \u2014 do not edit this block)
11948
+ if command -v node >/dev/null 2>&1; then
11949
+ gad_guard_dir="$(git rev-parse --git-common-dir 2>/dev/null)"
11950
+ if [ -n "$gad_guard_dir" ] && [ -f "$gad_guard_dir/hooks/${GUARD_FILE}" ]; then
11951
+ node "$gad_guard_dir/hooks/${GUARD_FILE}" || exit 1
11952
+ fi
11953
+ fi
11954
+ `;
11955
+ }
11956
+ function buildSchemaAuditHookBlock() {
11957
+ return `${SCHEMA_AUDIT_MARKER} (managed by \`gad install-hooks\` \u2014 do not edit this block)
11958
+ if [ -f package.json ] && grep -q '"schema:audit"' package.json; then
11959
+ if command -v pnpm >/dev/null 2>&1; then
11960
+ pnpm schema:audit --pre-push || exit 1
11961
+ else
11962
+ echo "gad-schema-audit: pnpm not found; skipping schema audit" >&2
11963
+ fi
11964
+ fi
11965
+ `;
11966
+ }
11967
+ function planHookInstall(existing) {
11968
+ if (existing !== null && existing.includes(GUARD_MARKER)) {
11969
+ return { action: "noop", content: existing };
11970
+ }
11971
+ if (existing === null) {
11972
+ return { action: "create", content: `#!/bin/sh
11973
+ ${buildHookBlock()}` };
11974
+ }
11975
+ return {
11976
+ action: "append",
11977
+ content: `${existing.trimEnd()}
11978
+
11979
+ ${buildHookBlock()}`
11980
+ };
11981
+ }
11982
+ function planSchemaAuditHookInstall(existing) {
11983
+ if (existing !== null && existing.includes(SCHEMA_AUDIT_MARKER)) {
11984
+ return { action: "noop", content: existing };
11985
+ }
11986
+ if (existing === null) {
11987
+ return {
11988
+ action: "create",
11989
+ content: `#!/bin/sh
11990
+ ${buildSchemaAuditHookBlock()}`
11991
+ };
11992
+ }
11993
+ return {
11994
+ action: "append",
11995
+ content: `${existing.trimEnd()}
11996
+
11997
+ ${buildSchemaAuditHookBlock()}`
11998
+ };
11999
+ }
12000
+ function installCommitGuard(opts) {
12001
+ const exec2 = opts?.exec ?? realExec2;
12002
+ const cwd = opts?.cwd ?? process.cwd();
12003
+ let repoRoot;
12004
+ let commonDir;
12005
+ try {
12006
+ repoRoot = exec2("git", ["rev-parse", "--show-toplevel"], { cwd }).trim();
12007
+ commonDir = exec2("git", ["rev-parse", "--git-common-dir"], {
12008
+ cwd: repoRoot
12009
+ }).trim();
12010
+ } catch {
12011
+ throw validationError(
12012
+ "Not inside a git repository.",
12013
+ "Run this from the repo whose commits you want guarded."
12014
+ );
12015
+ }
12016
+ if (!isAbsolute2(commonDir)) commonDir = resolve2(repoRoot, commonDir);
12017
+ const hooksDir = join3(commonDir, "hooks");
12018
+ mkdirSync(hooksDir, { recursive: true });
12019
+ const guardPath = join3(hooksDir, GUARD_FILE);
12020
+ writeFileSync2(guardPath, buildGuardScript());
12021
+ chmodSync(guardPath, 493);
12022
+ const huskyHook = join3(repoRoot, ".husky", "pre-commit");
12023
+ const hookPath = existsSync2(huskyHook) ? huskyHook : join3(hooksDir, "pre-commit");
12024
+ const existing = existsSync2(hookPath) ? readFileSync3(hookPath, "utf8") : null;
12025
+ const plan = planHookInstall(existing);
12026
+ if (plan.action !== "noop") {
12027
+ writeFileSync2(hookPath, plan.content);
12028
+ }
12029
+ chmodSync(hookPath, 493);
12030
+ const huskyDir = join3(repoRoot, ".husky");
12031
+ const huskyPrePush = join3(huskyDir, "pre-push");
12032
+ const schemaAuditHookPath = existsSync2(huskyPrePush) || existsSync2(huskyDir) ? huskyPrePush : join3(hooksDir, "pre-push");
12033
+ const existingSchemaAuditHook = existsSync2(schemaAuditHookPath) ? readFileSync3(schemaAuditHookPath, "utf8") : null;
12034
+ const schemaAuditPlan = planSchemaAuditHookInstall(existingSchemaAuditHook);
12035
+ if (schemaAuditPlan.action !== "noop") {
12036
+ writeFileSync2(schemaAuditHookPath, schemaAuditPlan.content);
12037
+ }
12038
+ chmodSync(schemaAuditHookPath, 493);
12039
+ return {
12040
+ guardPath,
12041
+ hookPath,
12042
+ action: plan.action,
12043
+ schemaAuditHookPath,
12044
+ schemaAuditAction: schemaAuditPlan.action
12045
+ };
12046
+ }
12047
+ var installHooksCommand = new Command("install-hooks").description(
12048
+ "Install the fail-open agent commit guard into this repo's pre-commit hook (idempotent)"
12049
+ ).action((_options, cmd) => {
12050
+ const parentOpts = cmd.optsWithGlobals();
12051
+ const fmt = createFormatter({
12052
+ json: parentOpts.json || false,
12053
+ quiet: parentOpts.quiet || false,
12054
+ isTTY: isTTY(),
12055
+ noColor: parentOpts.noColor || false
12056
+ });
12057
+ const result = installCommitGuard();
12058
+ if (parentOpts.json) {
12059
+ process.stdout.write(JSON.stringify(result) + "\n");
12060
+ return;
12061
+ }
12062
+ if (result.action === "noop") {
12063
+ fmt.success(`Commit guard already installed at ${result.hookPath}.`);
12064
+ } else {
12065
+ fmt.success(
12066
+ `Commit guard ${result.action === "append" ? "chained into" : "installed at"} ${result.hookPath}.`
12067
+ );
12068
+ }
12069
+ if (result.schemaAuditAction === "noop") {
12070
+ fmt.success(
12071
+ `Schema audit pre-push hook already installed at ${result.schemaAuditHookPath}.`
12072
+ );
12073
+ } else {
12074
+ fmt.success(
12075
+ `Schema audit pre-push hook ${result.schemaAuditAction === "append" ? "chained into" : "installed at"} ${result.schemaAuditHookPath}.`
12076
+ );
12077
+ }
12078
+ fmt.info(
12079
+ "The guard fails open for humans and dead sessions; override deliberately with ALLOW_FOREIGN_WORKTREE=1."
12080
+ );
12081
+ fmt.info(
12082
+ "Pair with 'gad worktree new <branch>' so each agent session owns its worktree."
12083
+ );
12084
+ });
12085
+ var install_hooks_default = installHooksCommand;
12086
+
12087
+ // src/commands/guardrails.ts
12088
+ function writeJson2(value) {
12089
+ process.stdout.write(`${JSON.stringify(value)}
12090
+ `);
12091
+ }
12092
+ function normalizeResponse(value) {
12093
+ return typeof value === "object" && value !== null ? value : {};
12094
+ }
12095
+ async function createClient2() {
12096
+ const auth = await resolveAuth();
12097
+ return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12098
+ }
12099
+ function renderGuardrails(data, cmd) {
12100
+ const output = globalOutputOpts(cmd);
12101
+ const fmt = createFormatter({ ...output, isTTY: isTTY() });
12102
+ if (output.json) return writeJson2(data);
12103
+ const guardrails = Array.isArray(data.guardrails) ? data.guardrails : [];
12104
+ fmt.table(
12105
+ guardrails.map((guardrail) => ({
12106
+ ...guardrail,
12107
+ state: guardrail.enabled ? "on" : "off",
12108
+ surfacesLabel: guardrail.surfaces.join(", ")
12109
+ })),
12110
+ [
12111
+ { key: "state", header: "State" },
12112
+ { key: "id", header: "ID" },
12113
+ { key: "title", header: "Guardrail" },
12114
+ { key: "command", header: "Command" },
12115
+ { key: "surfacesLabel", header: "Surfaces" }
12116
+ ]
12117
+ );
12118
+ }
12119
+ function guardrailsCommand() {
12120
+ const cmd = new Command("guardrails").description(
12121
+ "List and manage optional engineering guardrails for the active workspace"
12122
+ );
12123
+ cmd.command("list").description("List guardrails, explanations, commands, and enabled state").action(async (_options, actionCmd) => {
12124
+ const client = await createClient2();
12125
+ const result = normalizeResponse(
12126
+ await client.mcpCall("engineering_guardrails_list", {})
12127
+ );
12128
+ renderGuardrails(result, actionCmd);
12129
+ });
12130
+ cmd.command("enable").description("Enable a guardrail for the active workspace").argument("<id>", "Guardrail id from `gad guardrails list`").action(async (id, _options, actionCmd) => {
12131
+ const client = await createClient2();
12132
+ const result = normalizeResponse(
12133
+ await client.mcpCall("engineering_guardrails_update", {
12134
+ id,
12135
+ enabled: true
12136
+ })
12137
+ );
12138
+ renderGuardrails(result, actionCmd);
12139
+ });
12140
+ cmd.command("disable").description("Disable a guardrail for the active workspace").argument("<id>", "Guardrail id from `gad guardrails list`").action(async (id, _options, actionCmd) => {
12141
+ const client = await createClient2();
12142
+ const result = normalizeResponse(
12143
+ await client.mcpCall("engineering_guardrails_update", {
12144
+ id,
12145
+ enabled: false
12146
+ })
12147
+ );
12148
+ renderGuardrails(result, actionCmd);
12149
+ });
12150
+ return cmd;
12151
+ }
12152
+
11231
12153
  // src/commands/doctor.ts
11232
- import { existsSync } from "node:fs";
12154
+ import { existsSync as existsSync3 } from "node:fs";
11233
12155
  import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
11234
12156
  import { homedir as homedir2 } from "node:os";
11235
- import { dirname as dirname2, join as join2 } from "node:path";
12157
+ import { dirname as dirname3, join as join4 } from "node:path";
11236
12158
 
11237
12159
  // src/lib/codex-config.ts
11238
- import { execFileSync as execFileSync2 } from "node:child_process";
12160
+ import { execFileSync as execFileSync5 } from "node:child_process";
11239
12161
  var CODEX_SERVER_NAME = "great-arrow";
11240
12162
  var CODEX_CLIENT_ID = "codex-cli";
11241
12163
  var SAFE_GLOBAL_MCP_SERVERS = /* @__PURE__ */ new Set([
@@ -11258,13 +12180,31 @@ function mcpEndpoint(apiUrl) {
11258
12180
  function tomlEscape(value) {
11259
12181
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
11260
12182
  }
12183
+ function compareVersion(a, b) {
12184
+ if (!a && !b) return 0;
12185
+ if (!a) return -1;
12186
+ if (!b) return 1;
12187
+ const left = a.match(/\d+|[a-z]+/gi) ?? [];
12188
+ const right = b.match(/\d+|[a-z]+/gi) ?? [];
12189
+ const length = Math.max(left.length, right.length);
12190
+ for (let i = 0; i < length; i += 1) {
12191
+ const l = left[i] ?? "0";
12192
+ const r = right[i] ?? "0";
12193
+ const ln = Number(l);
12194
+ const rn = Number(r);
12195
+ const bothNumeric = Number.isFinite(ln) && Number.isFinite(rn);
12196
+ if (bothNumeric && ln !== rn) return ln > rn ? 1 : -1;
12197
+ if (!bothNumeric && l !== r) return l > r ? 1 : -1;
12198
+ }
12199
+ return 0;
12200
+ }
11261
12201
  function detectGitOrigin2(cwd = process.cwd()) {
11262
12202
  try {
11263
12203
  const env4 = { ...process.env };
11264
12204
  for (const key of Object.keys(env4)) {
11265
12205
  if (key.startsWith("GIT_")) delete env4[key];
11266
12206
  }
11267
- const url = execFileSync2(
12207
+ const url = execFileSync5(
11268
12208
  "git",
11269
12209
  ["-C", cwd, "remote", "get-url", "origin"],
11270
12210
  {
@@ -11394,9 +12334,29 @@ function extractConfiguredRepoKeys(text) {
11394
12334
  }
11395
12335
  return [...keys];
11396
12336
  }
11397
- function hasJobbyJobMarker(server, text) {
11398
- return /jobby\s*job|jobbyjob|jobbyjob\.ai/i.test(`${server}
11399
- ${text}`);
12337
+ function extractInstallVersion(text) {
12338
+ const escapedHeader = GAD_INSTALL_HEADER.replace(
12339
+ /[.*+?^${}()|[\]\\]/g,
12340
+ "\\$&"
12341
+ );
12342
+ const match = text.match(
12343
+ new RegExp(`^\\s*${escapedHeader}\\s*=\\s*"([^"]+)"\\s*$`, "m")
12344
+ );
12345
+ return match?.[1]?.trim() || null;
12346
+ }
12347
+ function localPathArgs(text) {
12348
+ return [...text.matchAll(/"((?:\/|~\/)[^"]+)"/g)].map((m) => m[1] ?? "");
12349
+ }
12350
+ function hasForeignRepoMarker(text, expectedRepoKey) {
12351
+ if (!expectedRepoKey) return false;
12352
+ const repoName = expectedRepoKey.split("/").pop()?.toLowerCase() ?? "";
12353
+ if (!repoName) return false;
12354
+ for (const p of localPathArgs(text)) {
12355
+ const lower = p.toLowerCase();
12356
+ const looksLikeCheckout = /server-filesystem/.test(text) || /\.(c?js|mjs|ts|py|sh)$/.test(lower);
12357
+ if (looksLikeCheckout && !lower.includes(repoName)) return true;
12358
+ }
12359
+ return false;
11400
12360
  }
11401
12361
  function isUnmanagedServer(server) {
11402
12362
  return !SAFE_GLOBAL_MCP_SERVERS.has(server);
@@ -11407,6 +12367,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11407
12367
  const serverNames = [...grouped.keys()].sort();
11408
12368
  const greatArrowText = grouped.get(CODEX_SERVER_NAME) ?? "";
11409
12369
  const hasGreatArrowServer = grouped.has(CODEX_SERVER_NAME);
12370
+ const greatArrowInstallVersion = extractInstallVersion(greatArrowText);
11410
12371
  const greatArrowRepoKeys = extractConfiguredRepoKeys(greatArrowText);
11411
12372
  const greatArrowHasRepoHeader = Boolean(
11412
12373
  expectedRepoKey && greatArrowRepoKeys.includes(expectedRepoKey)
@@ -11417,7 +12378,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11417
12378
  if (server === CODEX_SERVER_NAME) continue;
11418
12379
  const keys = extractConfiguredRepoKeys(text);
11419
12380
  const mismatchedExplicitRepo = expectedRepoKey && keys.length > 0 && !keys.some((key) => key === expectedRepoKey);
11420
- if (mismatchedExplicitRepo || hasJobbyJobMarker(server, text)) {
12381
+ if (mismatchedExplicitRepo || hasForeignRepoMarker(text, expectedRepoKey)) {
11421
12382
  staleRepoServers.push(server);
11422
12383
  }
11423
12384
  }
@@ -11449,6 +12410,36 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11449
12410
  }
11450
12411
  );
11451
12412
  }
12413
+ if (hasGreatArrowServer) {
12414
+ if (!greatArrowInstallVersion) {
12415
+ issues.push({
12416
+ code: "install_version_missing",
12417
+ level: "warn",
12418
+ message: "Great Arrow is missing the install-version header.",
12419
+ hint: "Run `gad mcp install codex` to refresh Codex update tracking."
12420
+ });
12421
+ } else if (compareVersion(greatArrowInstallVersion, GAD_INSTALL_MIN_VERSION) < 0) {
12422
+ issues.push({
12423
+ code: "install_version_required",
12424
+ level: "fail",
12425
+ message: `Great Arrow install ${greatArrowInstallVersion} is below the supported minimum ${GAD_INSTALL_MIN_VERSION}.`,
12426
+ hint: "Update the CLI with `npm install -g @great-arrow/cli@latest`, then run `gad mcp install codex`."
12427
+ });
12428
+ } else if (compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) < 0) {
12429
+ issues.push({
12430
+ code: "install_version_stale",
12431
+ level: "warn",
12432
+ message: `Great Arrow install ${greatArrowInstallVersion} can be refreshed to ${GAD_INSTALL_VERSION}.`,
12433
+ hint: "Run `gad mcp install codex` to rewrite the Codex block with the latest self-healing headers."
12434
+ });
12435
+ } else {
12436
+ issues.push({
12437
+ code: "install_version_current",
12438
+ level: "pass",
12439
+ message: `Great Arrow install version ${greatArrowInstallVersion} is current.`
12440
+ });
12441
+ }
12442
+ }
11452
12443
  if (staleRepoServers.length > 0) {
11453
12444
  issues.push({
11454
12445
  code: "stale_repo_servers",
@@ -11476,6 +12467,8 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11476
12467
  expectedRepoKey,
11477
12468
  hasGreatArrowServer,
11478
12469
  greatArrowHasRepoHeader,
12470
+ greatArrowInstallVersion,
12471
+ greatArrowInstallVersionCurrent: Boolean(greatArrowInstallVersion) && compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) >= 0,
11479
12472
  unmanagedServers,
11480
12473
  staleRepoServers,
11481
12474
  issues
@@ -11494,8 +12487,134 @@ function repairCodexConfig(content, staleRepoServers) {
11494
12487
  };
11495
12488
  }
11496
12489
 
12490
+ // src/lib/json-mcp-config.ts
12491
+ var JSON_SERVER_NAME = "great-arrow";
12492
+ function stripBom(content) {
12493
+ return content.codePointAt(0) === 65279 ? content.slice(1) : content;
12494
+ }
12495
+ function asString(v) {
12496
+ return typeof v === "string" ? v : null;
12497
+ }
12498
+ function explicitRepoKeys(entry) {
12499
+ const keys = /* @__PURE__ */ new Set();
12500
+ const headerVal = asString(entry.headers?.["X-Repo-URL"]) ?? asString(entry.headers?.["x-repo-url"]);
12501
+ const envVals = [
12502
+ asString(entry.env?.MEMORY_REPO_URL),
12503
+ asString(entry.env?.GAD_REPO_URL)
12504
+ ];
12505
+ for (const v of [headerVal, ...envVals]) {
12506
+ const k = repoKey(v);
12507
+ if (k) keys.add(k);
12508
+ }
12509
+ const text = JSON.stringify([entry.url, entry.args]);
12510
+ const gitUrls = /(?:https:\/\/|ssh:\/\/git@|git@)(?:github\.com|gitlab\.com|bitbucket\.org)[:/][^\s"',\]]+/gi;
12511
+ for (const m of text.matchAll(gitUrls)) {
12512
+ const k = repoKey(m[0]);
12513
+ if (k) keys.add(k);
12514
+ }
12515
+ return [...keys];
12516
+ }
12517
+ function inspectJsonMcpConfig(content, expectedRepoUrl) {
12518
+ const expectedRepoKey = repoKey(expectedRepoUrl);
12519
+ const base = {
12520
+ expectedRepoUrl: expectedRepoUrl ?? null,
12521
+ expectedRepoKey
12522
+ };
12523
+ if (!content.trim()) {
12524
+ return {
12525
+ ...base,
12526
+ parseError: null,
12527
+ staleRepoServers: [],
12528
+ issues: [
12529
+ { code: "no_config", level: "warn", message: "No MCP config found." }
12530
+ ]
12531
+ };
12532
+ }
12533
+ let parsed;
12534
+ try {
12535
+ parsed = JSON.parse(stripBom(content));
12536
+ } catch (e) {
12537
+ return {
12538
+ ...base,
12539
+ parseError: e instanceof Error ? e.message : String(e),
12540
+ staleRepoServers: [],
12541
+ issues: [
12542
+ {
12543
+ code: "parse_error",
12544
+ level: "warn",
12545
+ message: "Could not parse MCP config (left untouched)."
12546
+ }
12547
+ ]
12548
+ };
12549
+ }
12550
+ const serversRaw = parsed.mcpServers;
12551
+ const servers = serversRaw && typeof serversRaw === "object" && !Array.isArray(serversRaw) ? serversRaw : {};
12552
+ const stale = [];
12553
+ for (const [name, entry] of Object.entries(servers)) {
12554
+ if (name === JSON_SERVER_NAME) continue;
12555
+ if (!entry || typeof entry !== "object") continue;
12556
+ const keys = explicitRepoKeys(entry);
12557
+ const mismatchedExplicit = expectedRepoKey && keys.length > 0 && !keys.some((k) => k === expectedRepoKey);
12558
+ if (mismatchedExplicit || hasForeignRepoMarker(JSON.stringify(entry), expectedRepoKey)) {
12559
+ stale.push(name);
12560
+ }
12561
+ }
12562
+ stale.sort();
12563
+ const issues = [
12564
+ JSON_SERVER_NAME in servers ? {
12565
+ code: "great_arrow_present",
12566
+ level: "pass",
12567
+ message: "Great Arrow MCP server present."
12568
+ } : {
12569
+ code: "great_arrow_missing",
12570
+ level: "warn",
12571
+ message: "Great Arrow MCP server not found in this config.",
12572
+ hint: "Run `gad mcp install` from the repo root."
12573
+ },
12574
+ stale.length > 0 ? {
12575
+ code: "stale_repo_servers",
12576
+ level: "fail",
12577
+ message: `Found stale repo-scoped MCP server(s): ${stale.join(", ")}.`,
12578
+ hint: "Run `gad doctor clients --enforce` to remove the stale blocks."
12579
+ } : {
12580
+ code: "no_stale_repo_servers",
12581
+ level: "pass",
12582
+ message: "No stale repo-scoped MCP server blocks found."
12583
+ }
12584
+ ];
12585
+ return { ...base, parseError: null, staleRepoServers: stale, issues };
12586
+ }
12587
+ function repairJsonMcpConfig(content, staleRepoServers) {
12588
+ if (!content.trim() || staleRepoServers.length === 0) {
12589
+ return { content, removedServers: [] };
12590
+ }
12591
+ let parsed;
12592
+ try {
12593
+ parsed = JSON.parse(stripBom(content));
12594
+ } catch {
12595
+ return { content, removedServers: [] };
12596
+ }
12597
+ const servers = parsed.mcpServers;
12598
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
12599
+ return { content, removedServers: [] };
12600
+ }
12601
+ const removed = [];
12602
+ for (const name of new Set(staleRepoServers)) {
12603
+ if (name in servers) {
12604
+ delete servers[name];
12605
+ removed.push(name);
12606
+ }
12607
+ }
12608
+ removed.sort();
12609
+ if (removed.length === 0) return { content, removedServers: [] };
12610
+ return {
12611
+ content: JSON.stringify(parsed, null, 2) + "\n",
12612
+ removedServers: removed
12613
+ };
12614
+ }
12615
+
11497
12616
  // src/commands/doctor.ts
11498
- function compareVersion(a, b) {
12617
+ function compareVersion2(a, b) {
11499
12618
  const left = a.match(/\d+|[a-z]+/gi) ?? [];
11500
12619
  const right = b.match(/\d+|[a-z]+/gi) ?? [];
11501
12620
  const length = Math.max(left.length, right.length);
@@ -11597,7 +12716,7 @@ async function checkCliVersion(auth) {
11597
12716
  const latest = cli.latest ?? CLI_VERSION;
11598
12717
  const minimum = cli.minimum ?? CLI_VERSION;
11599
12718
  const updateCommand2 = cli.updateCommand ?? `npm install -g ${CLI_PACKAGE_NAME}@latest`;
11600
- if (compareVersion(CLI_VERSION, minimum) < 0) {
12719
+ if (compareVersion2(CLI_VERSION, minimum) < 0) {
11601
12720
  return {
11602
12721
  name: "CLI Version",
11603
12722
  status: "fail",
@@ -11605,7 +12724,7 @@ async function checkCliVersion(auth) {
11605
12724
  hint: `Update with: ${updateCommand2}`
11606
12725
  };
11607
12726
  }
11608
- if (compareVersion(CLI_VERSION, latest) < 0) {
12727
+ if (compareVersion2(CLI_VERSION, latest) < 0) {
11609
12728
  return {
11610
12729
  name: "CLI Version",
11611
12730
  status: "warn",
@@ -11695,7 +12814,7 @@ async function checkMcp(auth) {
11695
12814
  }
11696
12815
  }
11697
12816
  function defaultCodexConfigPath() {
11698
- return join2(homedir2(), ".codex", "config.toml");
12817
+ return join4(homedir2(), ".codex", "config.toml");
11699
12818
  }
11700
12819
  function codexIssuesToChecks(issues) {
11701
12820
  return issues.map((issue) => {
@@ -11733,13 +12852,13 @@ function codexCommand() {
11733
12852
  });
11734
12853
  const configPath = options.config ?? defaultCodexConfigPath();
11735
12854
  const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
11736
- let content = existsSync(configPath) ? await readFile2(configPath, "utf8") : "";
12855
+ let content = existsSync3(configPath) ? await readFile2(configPath, "utf8") : "";
11737
12856
  let backupPath = null;
11738
12857
  let removedServers = [];
11739
12858
  if (options.enforce) {
11740
12859
  const apiUrl = extractCodexGreatArrowApiUrl(content) ?? (await resolveAuth()).apiUrl;
11741
12860
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11742
- await mkdir2(dirname2(configPath), { recursive: true });
12861
+ await mkdir2(dirname3(configPath), { recursive: true });
11743
12862
  if (content.trim()) {
11744
12863
  backupPath = `${configPath}.bak.${timestamp}`;
11745
12864
  await copyFile(configPath, backupPath);
@@ -11821,6 +12940,137 @@ function codexCommand() {
11821
12940
  }
11822
12941
  );
11823
12942
  }
12943
+ function defaultJsonClientConfigs() {
12944
+ const home = homedir2();
12945
+ return [
12946
+ { name: "Claude Code", path: join4(home, ".claude.json") },
12947
+ { name: "Cursor", path: join4(home, ".cursor", "mcp.json") },
12948
+ { name: "Kiro", path: join4(home, ".kiro", "settings", "mcp.json") },
12949
+ { name: "Gemini CLI", path: join4(home, ".gemini", "settings.json") }
12950
+ ];
12951
+ }
12952
+ function clientsCommand() {
12953
+ return new Command("clients").description(
12954
+ "Check JSON MCP client configs (Claude Code / Cursor / Kiro / Gemini) for wrong-repo server entries and optionally repair"
12955
+ ).option(
12956
+ "--expected-repo <url>",
12957
+ "Expected repo URL (defaults to current git origin)"
12958
+ ).option(
12959
+ "--enforce",
12960
+ "Back up each config and remove stale repo-scoped MCP servers"
12961
+ ).option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(
12962
+ async (options, command) => {
12963
+ const g = globalOutputOpts(command);
12964
+ const color2 = createColor(g.noColor);
12965
+ const fmt = createFormatter({
12966
+ json: g.json,
12967
+ quiet: g.quiet,
12968
+ isTTY: isTTY(),
12969
+ noColor: g.noColor
12970
+ });
12971
+ const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
12972
+ const checks = [];
12973
+ const results = [];
12974
+ for (const { name, path: path9 } of defaultJsonClientConfigs()) {
12975
+ if (!existsSync3(path9)) continue;
12976
+ let content = await readFile2(path9, "utf8");
12977
+ let backupPath = null;
12978
+ let removedServers = [];
12979
+ if (options.enforce) {
12980
+ const pre = inspectJsonMcpConfig(content, expectedRepoUrl);
12981
+ if (!pre.parseError && pre.staleRepoServers.length > 0) {
12982
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12983
+ backupPath = `${path9}.bak.${timestamp}`;
12984
+ await copyFile(path9, backupPath);
12985
+ const repaired = repairJsonMcpConfig(
12986
+ content,
12987
+ pre.staleRepoServers
12988
+ );
12989
+ content = repaired.content;
12990
+ removedServers = repaired.removedServers;
12991
+ await writeFile2(path9, content, "utf8");
12992
+ }
12993
+ }
12994
+ const inspection = inspectJsonMcpConfig(content, expectedRepoUrl);
12995
+ for (const issue of inspection.issues) {
12996
+ const check = {
12997
+ name: `${name}: ${issue.code}`,
12998
+ status: issue.level,
12999
+ message: issue.message
13000
+ };
13001
+ if (issue.hint) check.hint = issue.hint;
13002
+ checks.push(check);
13003
+ }
13004
+ if (options.enforce && removedServers.length > 0) {
13005
+ const enforcementCheck = {
13006
+ name: `${name}: enforcement`,
13007
+ status: "pass",
13008
+ message: `Removed stale server(s): ${removedServers.join(", ")}`
13009
+ };
13010
+ if (backupPath) enforcementCheck.hint = `Backup: ${backupPath}`;
13011
+ checks.push(enforcementCheck);
13012
+ }
13013
+ results.push({
13014
+ name,
13015
+ path: path9,
13016
+ backupPath,
13017
+ removedServers,
13018
+ inspection
13019
+ });
13020
+ }
13021
+ if (results.length === 0) {
13022
+ checks.push({
13023
+ name: "JSON clients",
13024
+ status: "warn",
13025
+ message: "No JSON MCP client configs found on this machine."
13026
+ });
13027
+ }
13028
+ if (g.json) {
13029
+ process.stdout.write(
13030
+ JSON.stringify(
13031
+ { expectedRepoUrl, enforce: Boolean(options.enforce), results },
13032
+ null,
13033
+ 2
13034
+ ) + "\n"
13035
+ );
13036
+ } else {
13037
+ if (!expectedRepoUrl) {
13038
+ process.stdout.write(
13039
+ renderCheck(
13040
+ {
13041
+ name: "JSON clients: repo detection",
13042
+ status: "warn",
13043
+ message: "No git origin detected for this directory",
13044
+ hint: "Run from a repo root or pass --expected-repo."
13045
+ },
13046
+ color2
13047
+ ) + "\n"
13048
+ );
13049
+ }
13050
+ for (const check of checks) {
13051
+ process.stdout.write(renderCheck(check, color2) + "\n");
13052
+ }
13053
+ if (!g.quiet) {
13054
+ const failed = checks.filter((c) => c.status === "fail").length;
13055
+ const warned = checks.filter((c) => c.status === "warn").length;
13056
+ process.stdout.write("\n");
13057
+ if (failed === 0 && warned === 0) {
13058
+ fmt.success("JSON client config guard passed");
13059
+ } else if (failed === 0) {
13060
+ fmt.warn(`JSON client guard passed with ${warned} warning(s)`);
13061
+ } else {
13062
+ fmt.error(
13063
+ `JSON client guard found ${failed} failure(s), ${warned} warning(s)`
13064
+ );
13065
+ }
13066
+ }
13067
+ }
13068
+ if (checks.some((check) => check.status === "fail")) {
13069
+ process.exitCode = 1;
13070
+ }
13071
+ }
13072
+ );
13073
+ }
11824
13074
  function doctorCommand() {
11825
13075
  const cmd = new Command("doctor").description("Run a comprehensive health check").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(async (options, command) => {
11826
13076
  const g = globalOutputOpts(command);
@@ -11875,6 +13125,7 @@ function doctorCommand() {
11875
13125
  }
11876
13126
  });
11877
13127
  cmd.addCommand(codexCommand());
13128
+ cmd.addCommand(clientsCommand());
11878
13129
  return cmd;
11879
13130
  }
11880
13131
 
@@ -11920,7 +13171,7 @@ var integrationListColumns = [
11920
13171
  }
11921
13172
  }
11922
13173
  ];
11923
- async function createClient2() {
13174
+ async function createClient3() {
11924
13175
  const auth = await resolveAuth();
11925
13176
  return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11926
13177
  }
@@ -11982,7 +13233,7 @@ var listCommand3 = new Command("list").description("List all connected integrati
11982
13233
  isTTY: isTTY(),
11983
13234
  noColor: false
11984
13235
  });
11985
- const client = await createClient2();
13236
+ const client = await createClient3();
11986
13237
  const result = await client.mcpCall("integrations_list", {});
11987
13238
  const data = result;
11988
13239
  let integrations = [];
@@ -12007,7 +13258,7 @@ var syncCommand = new Command("sync").description("Trigger a manual sync for the
12007
13258
  isTTY: isTTY(),
12008
13259
  noColor: false
12009
13260
  });
12010
- const client = await createClient2();
13261
+ const client = await createClient3();
12011
13262
  fmt.info(`Triggering sync for "${kind}"...`);
12012
13263
  try {
12013
13264
  const result = await client.mcpCall("integration_sync", { kind });
@@ -12045,7 +13296,7 @@ var CLIENT_IDS = {
12045
13296
  codex: CODEX_CLIENT_ID,
12046
13297
  gemini: "gemini-cli"
12047
13298
  };
12048
- async function createClient3() {
13299
+ async function createClient4() {
12049
13300
  const auth = await resolveAuth();
12050
13301
  const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12051
13302
  return { client, apiUrl: auth.apiUrl };
@@ -12107,11 +13358,11 @@ async function pathExists(path9) {
12107
13358
  }
12108
13359
  async function installCodexConfig(apiUrl, token, repoUrl) {
12109
13360
  const { homedir: homedir3 } = await import("node:os");
12110
- const { join: join3 } = await import("node:path");
13361
+ const { join: join5 } = await import("node:path");
12111
13362
  const { chmod, copyFile: copyFile2, mkdir: mkdir3, readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
12112
- const codexDir = join3(homedir3(), ".codex");
12113
- const configPath = join3(codexDir, "config.toml");
12114
- const envPath = join3(codexDir, ".env");
13363
+ const codexDir = join5(homedir3(), ".codex");
13364
+ const configPath = join5(codexDir, "config.toml");
13365
+ const envPath = join5(codexDir, ".env");
12115
13366
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12116
13367
  await mkdir3(codexDir, { recursive: true });
12117
13368
  const existingConfig = await pathExists(configPath) ? await readFile3(configPath, "utf8") : "";
@@ -12153,7 +13404,7 @@ var statusCommand = new Command("status").description(
12153
13404
  isTTY: isTTY(),
12154
13405
  noColor: false
12155
13406
  });
12156
- const { client, apiUrl } = await createClient3();
13407
+ const { client, apiUrl } = await createClient4();
12157
13408
  let statusData = {
12158
13409
  status: "unknown",
12159
13410
  proxyVersion: "\u2014",
@@ -12212,7 +13463,7 @@ var logsCommand = new Command("logs").description("Tail MCP connection log with
12212
13463
  });
12213
13464
  const c = createColor(false);
12214
13465
  const lineCount = Number.parseInt(options.lines ?? "20", 10);
12215
- const { client } = await createClient3();
13466
+ const { client } = await createClient4();
12216
13467
  try {
12217
13468
  const result = await client.mcpCall("integration_sync_logs", {
12218
13469
  limit: lineCount
@@ -12378,6 +13629,9 @@ var installCommand = new Command("install").description("Generate MCP configurat
12378
13629
  repoUrl
12379
13630
  );
12380
13631
  fmt.success(`Wrote Codex MCP config to ${configPath} and ${envPath}`);
13632
+ fmt.info(
13633
+ "Restart Codex, start a fresh chat, then call tools_discover; tools_discover and tools_enable should be visible before enabling more Great Arrow tools."
13634
+ );
12381
13635
  return;
12382
13636
  }
12383
13637
  const config = generateMcpConfig(
@@ -12392,8 +13646,8 @@ var installCommand = new Command("install").description("Generate MCP configurat
12392
13646
  return;
12393
13647
  }
12394
13648
  const { writeFile: writeFile3 } = await import("node:fs/promises");
12395
- const { join: join3 } = await import("node:path");
12396
- const outputPath = join3(process.cwd(), ".mcp.json");
13649
+ const { join: join5 } = await import("node:path");
13650
+ const outputPath = join5(process.cwd(), ".mcp.json");
12397
13651
  await writeFile3(outputPath, configJson + "\n", "utf-8");
12398
13652
  fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
12399
13653
  }
@@ -13086,7 +14340,7 @@ var retryifyAsync = (fn, options) => {
13086
14340
  throw error;
13087
14341
  const delay2 = Math.round(interval * Math.random());
13088
14342
  if (delay2 > 0) {
13089
- const delayPromise = new Promise((resolve) => setTimeout(resolve, delay2));
14343
+ const delayPromise = new Promise((resolve3) => setTimeout(resolve3, delay2));
13090
14344
  return delayPromise.then(() => attempt.apply(void 0, args));
13091
14345
  } else {
13092
14346
  return attempt.apply(void 0, args);
@@ -13345,23 +14599,23 @@ var Temp = {
13345
14599
  }
13346
14600
  },
13347
14601
  truncate: (filePath) => {
13348
- const basename = path2.basename(filePath);
13349
- if (basename.length <= LIMIT_BASENAME_LENGTH)
14602
+ const basename2 = path2.basename(filePath);
14603
+ if (basename2.length <= LIMIT_BASENAME_LENGTH)
13350
14604
  return filePath;
13351
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename);
14605
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename2);
13352
14606
  if (!truncable)
13353
14607
  return filePath;
13354
- const truncationLength = basename.length - LIMIT_BASENAME_LENGTH;
13355
- return `${filePath.slice(0, -basename.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
14608
+ const truncationLength = basename2.length - LIMIT_BASENAME_LENGTH;
14609
+ return `${filePath.slice(0, -basename2.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
13356
14610
  }
13357
14611
  };
13358
14612
  node_default(Temp.purgeSyncAll);
13359
14613
  var temp_default = Temp;
13360
14614
 
13361
14615
  // node_modules/.pnpm/atomically@2.1.1/node_modules/atomically/dist/index.js
13362
- function writeFileSync(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
14616
+ function writeFileSync3(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
13363
14617
  if (isString(options))
13364
- return writeFileSync(filePath, data, { encoding: options });
14618
+ return writeFileSync3(filePath, data, { encoding: options });
13365
14619
  const timeout2 = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
13366
14620
  const retryOptions = { timeout: timeout2 };
13367
14621
  let tempDisposer = null;
@@ -13688,7 +14942,7 @@ var Configstore = class {
13688
14942
  }
13689
14943
  if (error.name === "SyntaxError") {
13690
14944
  if (this._clearInvalidConfig) {
13691
- writeFileSync(this._path, "", writeFileOptions);
14945
+ writeFileSync3(this._path, "", writeFileOptions);
13692
14946
  return {};
13693
14947
  }
13694
14948
  throw error;
@@ -13700,7 +14954,7 @@ var Configstore = class {
13700
14954
  set all(value) {
13701
14955
  try {
13702
14956
  import_graceful_fs.default.mkdirSync(path4.dirname(this._path), mkdirOptions);
13703
- writeFileSync(this._path, JSON.stringify(value, void 0, " "), writeFileOptions);
14957
+ writeFileSync3(this._path, JSON.stringify(value, void 0, " "), writeFileOptions);
13704
14958
  } catch (error) {
13705
14959
  handlePermissionError(error);
13706
14960
  }
@@ -14652,14 +15906,14 @@ var TimeoutError = class extends Error {
14652
15906
 
14653
15907
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
14654
15908
  async function timeout(request, init, abortController, options) {
14655
- return new Promise((resolve, reject) => {
15909
+ return new Promise((resolve3, reject) => {
14656
15910
  const timeoutId = setTimeout(() => {
14657
15911
  if (abortController) {
14658
15912
  abortController.abort();
14659
15913
  }
14660
15914
  reject(new TimeoutError(request));
14661
15915
  }, options.timeout);
14662
- void options.fetch(request, init).then(resolve).catch(reject).then(() => {
15916
+ void options.fetch(request, init).then(resolve3).catch(reject).then(() => {
14663
15917
  clearTimeout(timeoutId);
14664
15918
  });
14665
15919
  });
@@ -14667,7 +15921,7 @@ async function timeout(request, init, abortController, options) {
14667
15921
 
14668
15922
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
14669
15923
  async function delay(ms, { signal }) {
14670
- return new Promise((resolve, reject) => {
15924
+ return new Promise((resolve3, reject) => {
14671
15925
  if (signal) {
14672
15926
  signal.throwIfAborted();
14673
15927
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -14678,7 +15932,7 @@ async function delay(ms, { signal }) {
14678
15932
  }
14679
15933
  const timeoutId = setTimeout(() => {
14680
15934
  signal?.removeEventListener("abort", abortHandler);
14681
- resolve();
15935
+ resolve3();
14682
15936
  }, ms);
14683
15937
  });
14684
15938
  }
@@ -16396,6 +17650,10 @@ program2.addCommand(memoryCommand);
16396
17650
  program2.addCommand(workspace_default);
16397
17651
  program2.addCommand(todo_default);
16398
17652
  program2.addCommand(claim_default);
17653
+ program2.addCommand(work_default());
17654
+ program2.addCommand(worktree_default);
17655
+ program2.addCommand(install_hooks_default);
17656
+ program2.addCommand(guardrailsCommand());
16399
17657
  program2.addCommand(doctorCommand());
16400
17658
  program2.addCommand(integrations_default);
16401
17659
  program2.addCommand(mcp_default);