@great-arrow/cli 0.1.7 → 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.
- package/README.md +23 -0
- package/dist/index.js +1336 -65
- 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
|
|
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
|
-
|
|
5282
|
-
|
|
5281
|
+
join5(etc, name, "config"),
|
|
5282
|
+
join5(etc, name + "rc")
|
|
5283
5283
|
].forEach(addConfigFile);
|
|
5284
5284
|
if (home)
|
|
5285
5285
|
[
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
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
|
|
6628
|
-
if (
|
|
6627
|
+
const dirname4 = path9.dirname(name2);
|
|
6628
|
+
if (dirname4 === name2) {
|
|
6629
6629
|
return original;
|
|
6630
6630
|
}
|
|
6631
|
-
return find(
|
|
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.
|
|
9306
|
-
var GAD_INSTALL_VERSION = "2026.06
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(() =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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) =>
|
|
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(
|
|
@@ -10270,7 +10278,7 @@ function parseMcpPayload(result) {
|
|
|
10270
10278
|
|
|
10271
10279
|
// src/lib/client.ts
|
|
10272
10280
|
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
10273
|
-
var MAX_RETRIES =
|
|
10281
|
+
var MAX_RETRIES = 5;
|
|
10274
10282
|
var BASE_DELAY_MS = 1e3;
|
|
10275
10283
|
function detectGitOrigin() {
|
|
10276
10284
|
try {
|
|
@@ -10300,8 +10308,21 @@ function isRetryable(error) {
|
|
|
10300
10308
|
function isRetryableStatus(status) {
|
|
10301
10309
|
return status >= 500 && status < 600;
|
|
10302
10310
|
}
|
|
10311
|
+
function describeError(error) {
|
|
10312
|
+
if (error instanceof Error) {
|
|
10313
|
+
const cause = error.cause;
|
|
10314
|
+
if (cause instanceof Error) {
|
|
10315
|
+
return `${error.name}: ${error.message}; cause=${cause.name}: ${cause.message}`;
|
|
10316
|
+
}
|
|
10317
|
+
if (cause) {
|
|
10318
|
+
return `${error.name}: ${error.message}; cause=${String(cause)}`;
|
|
10319
|
+
}
|
|
10320
|
+
return `${error.name}: ${error.message}`;
|
|
10321
|
+
}
|
|
10322
|
+
return String(error);
|
|
10323
|
+
}
|
|
10303
10324
|
function sleep(ms) {
|
|
10304
|
-
return new Promise((
|
|
10325
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
10305
10326
|
}
|
|
10306
10327
|
var GadClient = class {
|
|
10307
10328
|
token;
|
|
@@ -10432,7 +10453,7 @@ var GadClient = class {
|
|
|
10432
10453
|
} catch (error) {
|
|
10433
10454
|
if (error instanceof CliError) throw error;
|
|
10434
10455
|
throw networkError(
|
|
10435
|
-
`Network error calling MCP tool '${toolName}'.`,
|
|
10456
|
+
`Network error calling MCP tool '${toolName}' (${describeError(error)}).`,
|
|
10436
10457
|
"Check your internet connection or run 'gad doctor'."
|
|
10437
10458
|
);
|
|
10438
10459
|
}
|
|
@@ -10590,10 +10611,10 @@ function whoamiCommand() {
|
|
|
10590
10611
|
|
|
10591
10612
|
// src/lib/stdin.ts
|
|
10592
10613
|
function readStream(stream) {
|
|
10593
|
-
return new Promise((
|
|
10614
|
+
return new Promise((resolve3, reject) => {
|
|
10594
10615
|
const chunks = [];
|
|
10595
10616
|
stream.on("data", (chunk) => chunks.push(chunk));
|
|
10596
|
-
stream.on("end", () =>
|
|
10617
|
+
stream.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
10597
10618
|
stream.on("error", reject);
|
|
10598
10619
|
});
|
|
10599
10620
|
}
|
|
@@ -11215,14 +11236,928 @@ var claimCommand = new Command("claim").description("Preflight + atomically clai
|
|
|
11215
11236
|
);
|
|
11216
11237
|
var claim_default = claimCommand;
|
|
11217
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
|
+
|
|
11218
12153
|
// src/commands/doctor.ts
|
|
11219
|
-
import { existsSync } from "node:fs";
|
|
12154
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
11220
12155
|
import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
11221
12156
|
import { homedir as homedir2 } from "node:os";
|
|
11222
|
-
import { dirname as
|
|
12157
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
11223
12158
|
|
|
11224
12159
|
// src/lib/codex-config.ts
|
|
11225
|
-
import { execFileSync as
|
|
12160
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
11226
12161
|
var CODEX_SERVER_NAME = "great-arrow";
|
|
11227
12162
|
var CODEX_CLIENT_ID = "codex-cli";
|
|
11228
12163
|
var SAFE_GLOBAL_MCP_SERVERS = /* @__PURE__ */ new Set([
|
|
@@ -11245,13 +12180,31 @@ function mcpEndpoint(apiUrl) {
|
|
|
11245
12180
|
function tomlEscape(value) {
|
|
11246
12181
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
11247
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
|
+
}
|
|
11248
12201
|
function detectGitOrigin2(cwd = process.cwd()) {
|
|
11249
12202
|
try {
|
|
11250
12203
|
const env4 = { ...process.env };
|
|
11251
12204
|
for (const key of Object.keys(env4)) {
|
|
11252
12205
|
if (key.startsWith("GIT_")) delete env4[key];
|
|
11253
12206
|
}
|
|
11254
|
-
const url =
|
|
12207
|
+
const url = execFileSync5(
|
|
11255
12208
|
"git",
|
|
11256
12209
|
["-C", cwd, "remote", "get-url", "origin"],
|
|
11257
12210
|
{
|
|
@@ -11381,9 +12334,29 @@ function extractConfiguredRepoKeys(text) {
|
|
|
11381
12334
|
}
|
|
11382
12335
|
return [...keys];
|
|
11383
12336
|
}
|
|
11384
|
-
function
|
|
11385
|
-
|
|
11386
|
-
|
|
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;
|
|
11387
12360
|
}
|
|
11388
12361
|
function isUnmanagedServer(server) {
|
|
11389
12362
|
return !SAFE_GLOBAL_MCP_SERVERS.has(server);
|
|
@@ -11394,6 +12367,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11394
12367
|
const serverNames = [...grouped.keys()].sort();
|
|
11395
12368
|
const greatArrowText = grouped.get(CODEX_SERVER_NAME) ?? "";
|
|
11396
12369
|
const hasGreatArrowServer = grouped.has(CODEX_SERVER_NAME);
|
|
12370
|
+
const greatArrowInstallVersion = extractInstallVersion(greatArrowText);
|
|
11397
12371
|
const greatArrowRepoKeys = extractConfiguredRepoKeys(greatArrowText);
|
|
11398
12372
|
const greatArrowHasRepoHeader = Boolean(
|
|
11399
12373
|
expectedRepoKey && greatArrowRepoKeys.includes(expectedRepoKey)
|
|
@@ -11404,7 +12378,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11404
12378
|
if (server === CODEX_SERVER_NAME) continue;
|
|
11405
12379
|
const keys = extractConfiguredRepoKeys(text);
|
|
11406
12380
|
const mismatchedExplicitRepo = expectedRepoKey && keys.length > 0 && !keys.some((key) => key === expectedRepoKey);
|
|
11407
|
-
if (mismatchedExplicitRepo ||
|
|
12381
|
+
if (mismatchedExplicitRepo || hasForeignRepoMarker(text, expectedRepoKey)) {
|
|
11408
12382
|
staleRepoServers.push(server);
|
|
11409
12383
|
}
|
|
11410
12384
|
}
|
|
@@ -11436,6 +12410,36 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11436
12410
|
}
|
|
11437
12411
|
);
|
|
11438
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
|
+
}
|
|
11439
12443
|
if (staleRepoServers.length > 0) {
|
|
11440
12444
|
issues.push({
|
|
11441
12445
|
code: "stale_repo_servers",
|
|
@@ -11463,6 +12467,8 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11463
12467
|
expectedRepoKey,
|
|
11464
12468
|
hasGreatArrowServer,
|
|
11465
12469
|
greatArrowHasRepoHeader,
|
|
12470
|
+
greatArrowInstallVersion,
|
|
12471
|
+
greatArrowInstallVersionCurrent: Boolean(greatArrowInstallVersion) && compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) >= 0,
|
|
11466
12472
|
unmanagedServers,
|
|
11467
12473
|
staleRepoServers,
|
|
11468
12474
|
issues
|
|
@@ -11481,8 +12487,134 @@ function repairCodexConfig(content, staleRepoServers) {
|
|
|
11481
12487
|
};
|
|
11482
12488
|
}
|
|
11483
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
|
+
|
|
11484
12616
|
// src/commands/doctor.ts
|
|
11485
|
-
function
|
|
12617
|
+
function compareVersion2(a, b) {
|
|
11486
12618
|
const left = a.match(/\d+|[a-z]+/gi) ?? [];
|
|
11487
12619
|
const right = b.match(/\d+|[a-z]+/gi) ?? [];
|
|
11488
12620
|
const length = Math.max(left.length, right.length);
|
|
@@ -11584,7 +12716,7 @@ async function checkCliVersion(auth) {
|
|
|
11584
12716
|
const latest = cli.latest ?? CLI_VERSION;
|
|
11585
12717
|
const minimum = cli.minimum ?? CLI_VERSION;
|
|
11586
12718
|
const updateCommand2 = cli.updateCommand ?? `npm install -g ${CLI_PACKAGE_NAME}@latest`;
|
|
11587
|
-
if (
|
|
12719
|
+
if (compareVersion2(CLI_VERSION, minimum) < 0) {
|
|
11588
12720
|
return {
|
|
11589
12721
|
name: "CLI Version",
|
|
11590
12722
|
status: "fail",
|
|
@@ -11592,7 +12724,7 @@ async function checkCliVersion(auth) {
|
|
|
11592
12724
|
hint: `Update with: ${updateCommand2}`
|
|
11593
12725
|
};
|
|
11594
12726
|
}
|
|
11595
|
-
if (
|
|
12727
|
+
if (compareVersion2(CLI_VERSION, latest) < 0) {
|
|
11596
12728
|
return {
|
|
11597
12729
|
name: "CLI Version",
|
|
11598
12730
|
status: "warn",
|
|
@@ -11682,7 +12814,7 @@ async function checkMcp(auth) {
|
|
|
11682
12814
|
}
|
|
11683
12815
|
}
|
|
11684
12816
|
function defaultCodexConfigPath() {
|
|
11685
|
-
return
|
|
12817
|
+
return join4(homedir2(), ".codex", "config.toml");
|
|
11686
12818
|
}
|
|
11687
12819
|
function codexIssuesToChecks(issues) {
|
|
11688
12820
|
return issues.map((issue) => {
|
|
@@ -11720,13 +12852,13 @@ function codexCommand() {
|
|
|
11720
12852
|
});
|
|
11721
12853
|
const configPath = options.config ?? defaultCodexConfigPath();
|
|
11722
12854
|
const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
|
|
11723
|
-
let content =
|
|
12855
|
+
let content = existsSync3(configPath) ? await readFile2(configPath, "utf8") : "";
|
|
11724
12856
|
let backupPath = null;
|
|
11725
12857
|
let removedServers = [];
|
|
11726
12858
|
if (options.enforce) {
|
|
11727
12859
|
const apiUrl = extractCodexGreatArrowApiUrl(content) ?? (await resolveAuth()).apiUrl;
|
|
11728
12860
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11729
|
-
await mkdir2(
|
|
12861
|
+
await mkdir2(dirname3(configPath), { recursive: true });
|
|
11730
12862
|
if (content.trim()) {
|
|
11731
12863
|
backupPath = `${configPath}.bak.${timestamp}`;
|
|
11732
12864
|
await copyFile(configPath, backupPath);
|
|
@@ -11808,6 +12940,137 @@ function codexCommand() {
|
|
|
11808
12940
|
}
|
|
11809
12941
|
);
|
|
11810
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
|
+
}
|
|
11811
13074
|
function doctorCommand() {
|
|
11812
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) => {
|
|
11813
13076
|
const g = globalOutputOpts(command);
|
|
@@ -11862,6 +13125,7 @@ function doctorCommand() {
|
|
|
11862
13125
|
}
|
|
11863
13126
|
});
|
|
11864
13127
|
cmd.addCommand(codexCommand());
|
|
13128
|
+
cmd.addCommand(clientsCommand());
|
|
11865
13129
|
return cmd;
|
|
11866
13130
|
}
|
|
11867
13131
|
|
|
@@ -11907,7 +13171,7 @@ var integrationListColumns = [
|
|
|
11907
13171
|
}
|
|
11908
13172
|
}
|
|
11909
13173
|
];
|
|
11910
|
-
async function
|
|
13174
|
+
async function createClient3() {
|
|
11911
13175
|
const auth = await resolveAuth();
|
|
11912
13176
|
return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
11913
13177
|
}
|
|
@@ -11969,7 +13233,7 @@ var listCommand3 = new Command("list").description("List all connected integrati
|
|
|
11969
13233
|
isTTY: isTTY(),
|
|
11970
13234
|
noColor: false
|
|
11971
13235
|
});
|
|
11972
|
-
const client = await
|
|
13236
|
+
const client = await createClient3();
|
|
11973
13237
|
const result = await client.mcpCall("integrations_list", {});
|
|
11974
13238
|
const data = result;
|
|
11975
13239
|
let integrations = [];
|
|
@@ -11994,7 +13258,7 @@ var syncCommand = new Command("sync").description("Trigger a manual sync for the
|
|
|
11994
13258
|
isTTY: isTTY(),
|
|
11995
13259
|
noColor: false
|
|
11996
13260
|
});
|
|
11997
|
-
const client = await
|
|
13261
|
+
const client = await createClient3();
|
|
11998
13262
|
fmt.info(`Triggering sync for "${kind}"...`);
|
|
11999
13263
|
try {
|
|
12000
13264
|
const result = await client.mcpCall("integration_sync", { kind });
|
|
@@ -12032,7 +13296,7 @@ var CLIENT_IDS = {
|
|
|
12032
13296
|
codex: CODEX_CLIENT_ID,
|
|
12033
13297
|
gemini: "gemini-cli"
|
|
12034
13298
|
};
|
|
12035
|
-
async function
|
|
13299
|
+
async function createClient4() {
|
|
12036
13300
|
const auth = await resolveAuth();
|
|
12037
13301
|
const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
12038
13302
|
return { client, apiUrl: auth.apiUrl };
|
|
@@ -12094,11 +13358,11 @@ async function pathExists(path9) {
|
|
|
12094
13358
|
}
|
|
12095
13359
|
async function installCodexConfig(apiUrl, token, repoUrl) {
|
|
12096
13360
|
const { homedir: homedir3 } = await import("node:os");
|
|
12097
|
-
const { join:
|
|
13361
|
+
const { join: join5 } = await import("node:path");
|
|
12098
13362
|
const { chmod, copyFile: copyFile2, mkdir: mkdir3, readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
|
|
12099
|
-
const codexDir =
|
|
12100
|
-
const configPath =
|
|
12101
|
-
const envPath =
|
|
13363
|
+
const codexDir = join5(homedir3(), ".codex");
|
|
13364
|
+
const configPath = join5(codexDir, "config.toml");
|
|
13365
|
+
const envPath = join5(codexDir, ".env");
|
|
12102
13366
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12103
13367
|
await mkdir3(codexDir, { recursive: true });
|
|
12104
13368
|
const existingConfig = await pathExists(configPath) ? await readFile3(configPath, "utf8") : "";
|
|
@@ -12140,7 +13404,7 @@ var statusCommand = new Command("status").description(
|
|
|
12140
13404
|
isTTY: isTTY(),
|
|
12141
13405
|
noColor: false
|
|
12142
13406
|
});
|
|
12143
|
-
const { client, apiUrl } = await
|
|
13407
|
+
const { client, apiUrl } = await createClient4();
|
|
12144
13408
|
let statusData = {
|
|
12145
13409
|
status: "unknown",
|
|
12146
13410
|
proxyVersion: "\u2014",
|
|
@@ -12199,7 +13463,7 @@ var logsCommand = new Command("logs").description("Tail MCP connection log with
|
|
|
12199
13463
|
});
|
|
12200
13464
|
const c = createColor(false);
|
|
12201
13465
|
const lineCount = Number.parseInt(options.lines ?? "20", 10);
|
|
12202
|
-
const { client } = await
|
|
13466
|
+
const { client } = await createClient4();
|
|
12203
13467
|
try {
|
|
12204
13468
|
const result = await client.mcpCall("integration_sync_logs", {
|
|
12205
13469
|
limit: lineCount
|
|
@@ -12365,6 +13629,9 @@ var installCommand = new Command("install").description("Generate MCP configurat
|
|
|
12365
13629
|
repoUrl
|
|
12366
13630
|
);
|
|
12367
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
|
+
);
|
|
12368
13635
|
return;
|
|
12369
13636
|
}
|
|
12370
13637
|
const config = generateMcpConfig(
|
|
@@ -12379,8 +13646,8 @@ var installCommand = new Command("install").description("Generate MCP configurat
|
|
|
12379
13646
|
return;
|
|
12380
13647
|
}
|
|
12381
13648
|
const { writeFile: writeFile3 } = await import("node:fs/promises");
|
|
12382
|
-
const { join:
|
|
12383
|
-
const outputPath =
|
|
13649
|
+
const { join: join5 } = await import("node:path");
|
|
13650
|
+
const outputPath = join5(process.cwd(), ".mcp.json");
|
|
12384
13651
|
await writeFile3(outputPath, configJson + "\n", "utf-8");
|
|
12385
13652
|
fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
|
|
12386
13653
|
}
|
|
@@ -13073,7 +14340,7 @@ var retryifyAsync = (fn, options) => {
|
|
|
13073
14340
|
throw error;
|
|
13074
14341
|
const delay2 = Math.round(interval * Math.random());
|
|
13075
14342
|
if (delay2 > 0) {
|
|
13076
|
-
const delayPromise = new Promise((
|
|
14343
|
+
const delayPromise = new Promise((resolve3) => setTimeout(resolve3, delay2));
|
|
13077
14344
|
return delayPromise.then(() => attempt.apply(void 0, args));
|
|
13078
14345
|
} else {
|
|
13079
14346
|
return attempt.apply(void 0, args);
|
|
@@ -13332,23 +14599,23 @@ var Temp = {
|
|
|
13332
14599
|
}
|
|
13333
14600
|
},
|
|
13334
14601
|
truncate: (filePath) => {
|
|
13335
|
-
const
|
|
13336
|
-
if (
|
|
14602
|
+
const basename2 = path2.basename(filePath);
|
|
14603
|
+
if (basename2.length <= LIMIT_BASENAME_LENGTH)
|
|
13337
14604
|
return filePath;
|
|
13338
|
-
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(
|
|
14605
|
+
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename2);
|
|
13339
14606
|
if (!truncable)
|
|
13340
14607
|
return filePath;
|
|
13341
|
-
const truncationLength =
|
|
13342
|
-
return `${filePath.slice(0, -
|
|
14608
|
+
const truncationLength = basename2.length - LIMIT_BASENAME_LENGTH;
|
|
14609
|
+
return `${filePath.slice(0, -basename2.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
|
|
13343
14610
|
}
|
|
13344
14611
|
};
|
|
13345
14612
|
node_default(Temp.purgeSyncAll);
|
|
13346
14613
|
var temp_default = Temp;
|
|
13347
14614
|
|
|
13348
14615
|
// node_modules/.pnpm/atomically@2.1.1/node_modules/atomically/dist/index.js
|
|
13349
|
-
function
|
|
14616
|
+
function writeFileSync3(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
|
|
13350
14617
|
if (isString(options))
|
|
13351
|
-
return
|
|
14618
|
+
return writeFileSync3(filePath, data, { encoding: options });
|
|
13352
14619
|
const timeout2 = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
|
|
13353
14620
|
const retryOptions = { timeout: timeout2 };
|
|
13354
14621
|
let tempDisposer = null;
|
|
@@ -13675,7 +14942,7 @@ var Configstore = class {
|
|
|
13675
14942
|
}
|
|
13676
14943
|
if (error.name === "SyntaxError") {
|
|
13677
14944
|
if (this._clearInvalidConfig) {
|
|
13678
|
-
|
|
14945
|
+
writeFileSync3(this._path, "", writeFileOptions);
|
|
13679
14946
|
return {};
|
|
13680
14947
|
}
|
|
13681
14948
|
throw error;
|
|
@@ -13687,7 +14954,7 @@ var Configstore = class {
|
|
|
13687
14954
|
set all(value) {
|
|
13688
14955
|
try {
|
|
13689
14956
|
import_graceful_fs.default.mkdirSync(path4.dirname(this._path), mkdirOptions);
|
|
13690
|
-
|
|
14957
|
+
writeFileSync3(this._path, JSON.stringify(value, void 0, " "), writeFileOptions);
|
|
13691
14958
|
} catch (error) {
|
|
13692
14959
|
handlePermissionError(error);
|
|
13693
14960
|
}
|
|
@@ -14639,14 +15906,14 @@ var TimeoutError = class extends Error {
|
|
|
14639
15906
|
|
|
14640
15907
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
|
|
14641
15908
|
async function timeout(request, init, abortController, options) {
|
|
14642
|
-
return new Promise((
|
|
15909
|
+
return new Promise((resolve3, reject) => {
|
|
14643
15910
|
const timeoutId = setTimeout(() => {
|
|
14644
15911
|
if (abortController) {
|
|
14645
15912
|
abortController.abort();
|
|
14646
15913
|
}
|
|
14647
15914
|
reject(new TimeoutError(request));
|
|
14648
15915
|
}, options.timeout);
|
|
14649
|
-
void options.fetch(request, init).then(
|
|
15916
|
+
void options.fetch(request, init).then(resolve3).catch(reject).then(() => {
|
|
14650
15917
|
clearTimeout(timeoutId);
|
|
14651
15918
|
});
|
|
14652
15919
|
});
|
|
@@ -14654,7 +15921,7 @@ async function timeout(request, init, abortController, options) {
|
|
|
14654
15921
|
|
|
14655
15922
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
|
|
14656
15923
|
async function delay(ms, { signal }) {
|
|
14657
|
-
return new Promise((
|
|
15924
|
+
return new Promise((resolve3, reject) => {
|
|
14658
15925
|
if (signal) {
|
|
14659
15926
|
signal.throwIfAborted();
|
|
14660
15927
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -14665,7 +15932,7 @@ async function delay(ms, { signal }) {
|
|
|
14665
15932
|
}
|
|
14666
15933
|
const timeoutId = setTimeout(() => {
|
|
14667
15934
|
signal?.removeEventListener("abort", abortHandler);
|
|
14668
|
-
|
|
15935
|
+
resolve3();
|
|
14669
15936
|
}, ms);
|
|
14670
15937
|
});
|
|
14671
15938
|
}
|
|
@@ -16383,6 +17650,10 @@ program2.addCommand(memoryCommand);
|
|
|
16383
17650
|
program2.addCommand(workspace_default);
|
|
16384
17651
|
program2.addCommand(todo_default);
|
|
16385
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());
|
|
16386
17657
|
program2.addCommand(doctorCommand());
|
|
16387
17658
|
program2.addCommand(integrations_default);
|
|
16388
17659
|
program2.addCommand(mcp_default);
|