jules-orchestrator-kit 0.72.2 → 0.73.0
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/.agent/prompts/{Overseer.md → Auditor.md} +3 -3
- package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
- package/.agent/prompts/Debugger.md +25 -0
- package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
- package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
- package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
- package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
- package/.agent/prompts/Resilience.md +20 -0
- package/.agent/prompts/Security.md +21 -0
- package/.agent/prompts/Testing.md +30 -0
- package/.agent/prompts/Types.md +19 -0
- package/.agent/rules/jules-protocol.md +4 -3
- package/AGENTS.md +78 -96
- package/CHANGELOG.md +193 -0
- package/JULES_RULES_TEMPLATE.md +83 -96
- package/LICENSE +1 -1
- package/README.md +77 -439
- package/ROADMAP_V1.md +22 -132
- package/bin/agentctl.mjs +443 -144
- package/bin/init.js +6 -3
- package/index.mjs +9 -6
- package/package.json +1 -1
- package/scripts/asset-integrity-check.mjs +1 -1
- package/scripts/doc-sync-check.mjs +47 -0
- package/scripts/generate-command-reference.mjs +39 -0
- package/scripts/jules-dispatch.mjs +12 -113
- package/scripts/jules-merge-swarm.mjs +8 -196
- package/scripts/jules-patch.mjs +7 -8
- package/scripts/jules-queue-runner.mjs +6 -8
- package/scripts/jules-scan-todos.mjs +10 -38
- package/scripts/jules-self-audit.mjs +8 -139
- package/scripts/jules-status.mjs +32 -38
- package/scripts/jules-webhook-receiver.mjs +1 -1
- package/src/assertions.mjs +5 -50
- package/src/bidi-guard.mjs +36 -0
- package/src/budget.mjs +3 -14
- package/src/config.mjs +2 -6
- package/src/dashboard.mjs +7 -9
- package/src/dispatch.mjs +212 -0
- package/src/engine.mjs +40 -16
- package/src/evidence.mjs +10 -41
- package/src/execution-envelope.mjs +13 -1
- package/src/flaky-ledger.mjs +1 -1
- package/src/fs-atomic.mjs +72 -0
- package/src/git.mjs +298 -27
- package/src/mcp.mjs +296 -7
- package/src/memory.mjs +0 -0
- package/src/merge-swarm.mjs +202 -0
- package/src/ops/cli-intent.mjs +1 -0
- package/src/ops/command-registry.mjs +796 -70
- package/src/ops/doctor-registry.mjs +134 -47
- package/src/ops/handover.mjs +3 -27
- package/src/ops/pr-harvest.mjs +1 -1
- package/src/prompt-guard.mjs +33 -3
- package/src/provider.mjs +51 -7
- package/src/remediation.mjs +2 -2
- package/src/role-resolver.mjs +113 -3
- package/src/router.mjs +19 -11
- package/src/runtime-env.mjs +67 -0
- package/src/scaffold.mjs +3 -1
- package/src/scope-guard.mjs +249 -0
- package/src/secret-scanner.mjs +530 -0
- package/src/security.mjs +81 -2972
- package/src/self-audit.mjs +140 -0
- package/src/session-ops.mjs +29 -0
- package/src/stability.mjs +8 -1
- package/src/stack-detector.mjs +5 -2
- package/src/state.mjs +45 -0
- package/src/swarm.mjs +76 -0
- package/src/task-optimizer.mjs +34 -7
- package/src/telemetry.mjs +23 -0
- package/src/test-tamper-guard.mjs +2173 -0
- package/src/todo-scanner.mjs +129 -0
- package/src/web-templates.mjs +3 -3
- package/src/webhook.mjs +10 -3
- package/src/wizard-init.mjs +12 -20
- package/src/wizard-oracle.mjs +4 -3
- package/src/wizard-task.mjs +37 -9
- package/.agent/prompts/Sentinel.md +0 -18
- package/scripts/utils.mjs +0 -241
package/src/git.mjs
CHANGED
|
@@ -392,8 +392,40 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
|
392
392
|
);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
export function getHeadCommit(root = process.cwd()) {
|
|
396
|
+
try {
|
|
397
|
+
const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], {
|
|
398
|
+
cwd: root,
|
|
399
|
+
encoding: "utf-8",
|
|
400
|
+
shell: false,
|
|
401
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
402
|
+
});
|
|
403
|
+
return res && res.trim() ? res.trim() : null;
|
|
404
|
+
} catch (_) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function getHeadParent(root = process.cwd()) {
|
|
410
|
+
try {
|
|
411
|
+
const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", "HEAD~1^{commit}"], {
|
|
412
|
+
cwd: root,
|
|
413
|
+
encoding: "utf-8",
|
|
414
|
+
shell: false,
|
|
415
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
416
|
+
});
|
|
417
|
+
return res && res.trim() ? res.trim() : null;
|
|
418
|
+
} catch (_) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
395
423
|
export function changedFiles(root = process.cwd(), base = "main", mode = "committed") {
|
|
396
424
|
const resolvedRef = resolveBase(root, base);
|
|
425
|
+
const headCommit = getHeadCommit(root);
|
|
426
|
+
const isHeadRef = typeof base === "string" && (base === "HEAD" || base.startsWith("HEAD~") || base.startsWith("HEAD^") || base.startsWith("HEAD@"));
|
|
427
|
+
const isBaseAtHead = Boolean(!isHeadRef && headCommit && resolvedRef === headCommit);
|
|
428
|
+
|
|
397
429
|
if (mode === "working-tree" || mode === "working") {
|
|
398
430
|
const committedRaw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
399
431
|
const uncommittedRaw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", "HEAD"], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
@@ -409,6 +441,16 @@ export function changedFiles(root = process.cwd(), base = "main", mode = "commit
|
|
|
409
441
|
const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", "--cached", resolvedRef], { cwd: root, raw: true });
|
|
410
442
|
return raw.split("\0").map(normalizePath).filter(Boolean);
|
|
411
443
|
} else {
|
|
444
|
+
if (isBaseAtHead) {
|
|
445
|
+
const headParent = getHeadParent(root);
|
|
446
|
+
if (headParent) {
|
|
447
|
+
const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${headParent}...HEAD`], { cwd: root, raw: true });
|
|
448
|
+
return raw.split("\0").map(normalizePath).filter(Boolean);
|
|
449
|
+
} else {
|
|
450
|
+
const raw = git(["-c", "core.quotePath=false", "diff-tree", "--root", "--no-commit-id", "-r", "-z", "--name-only", headCommit], { cwd: root, raw: true });
|
|
451
|
+
return raw.split("\0").map(normalizePath).filter(Boolean);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
412
454
|
const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
|
|
413
455
|
return raw.split("\0").map(normalizePath).filter(Boolean);
|
|
414
456
|
}
|
|
@@ -416,6 +458,10 @@ export function changedFiles(root = process.cwd(), base = "main", mode = "commit
|
|
|
416
458
|
|
|
417
459
|
export function diffText(root = process.cwd(), base = "main", mode = "committed") {
|
|
418
460
|
const resolvedRef = resolveBase(root, base);
|
|
461
|
+
const headCommit = getHeadCommit(root);
|
|
462
|
+
const isHeadRef = typeof base === "string" && (base === "HEAD" || base.startsWith("HEAD~") || base.startsWith("HEAD^") || base.startsWith("HEAD@"));
|
|
463
|
+
const isBaseAtHead = Boolean(!isHeadRef && headCommit && resolvedRef === headCommit);
|
|
464
|
+
|
|
419
465
|
if (mode === "working-tree" || mode === "working") {
|
|
420
466
|
const committed = git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
421
467
|
const uncommitted = git(["diff", "HEAD"], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
@@ -465,6 +511,15 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
|
|
|
465
511
|
} else if (mode === "staged" || mode === "index") {
|
|
466
512
|
return git(["diff", "--cached", resolvedRef], { cwd: root, raw: true });
|
|
467
513
|
}
|
|
514
|
+
|
|
515
|
+
if (isBaseAtHead) {
|
|
516
|
+
const headParent = getHeadParent(root);
|
|
517
|
+
if (headParent) {
|
|
518
|
+
return git(["diff", `${headParent}...HEAD`], { cwd: root, raw: true });
|
|
519
|
+
} else {
|
|
520
|
+
return git(["diff-tree", "-p", "--no-commit-id", "--root", headCommit], { cwd: root, raw: true });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
468
523
|
return git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
|
|
469
524
|
}
|
|
470
525
|
|
|
@@ -486,38 +541,72 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
|
|
|
486
541
|
* @param {string} mode
|
|
487
542
|
* @returns {Array<{ file: string, bytes: number }>}
|
|
488
543
|
*/
|
|
544
|
+
function parseRawDiffOutput(raw, out) {
|
|
545
|
+
// `--raw -z` emits ":<srcmode> <dstmode> <srcsha> <dstsha> <status>\0<path>\0".
|
|
546
|
+
const fields = raw.split("\0").filter(Boolean);
|
|
547
|
+
for (let i = 0; i < fields.length; i++) {
|
|
548
|
+
const meta = fields[i];
|
|
549
|
+
if (!meta.startsWith(":")) continue;
|
|
550
|
+
const parts = meta.slice(1).split(/\s+/);
|
|
551
|
+
const file = fields[i + 1];
|
|
552
|
+
i += 1;
|
|
553
|
+
if (!file) continue;
|
|
554
|
+
out.push({
|
|
555
|
+
file,
|
|
556
|
+
srcMode: parts[0] || "",
|
|
557
|
+
dstMode: parts[1] || "",
|
|
558
|
+
srcSha: parts[2] || "",
|
|
559
|
+
dstSha: parts[3] || "",
|
|
560
|
+
status: (parts[4] || "").charAt(0),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
489
565
|
export function parseRawDiff(root = process.cwd(), base = "main", mode = "committed") {
|
|
490
566
|
const resolvedRef = resolveBase(root, base);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
: [[`${resolvedRef}...HEAD`]];
|
|
567
|
+
const headCommit = getHeadCommit(root);
|
|
568
|
+
const isHeadRef = typeof base === "string" && (base === "HEAD" || base.startsWith("HEAD~") || base.startsWith("HEAD^") || base.startsWith("HEAD@"));
|
|
569
|
+
const isBaseAtHead = Boolean(!isHeadRef && headCommit && resolvedRef === headCommit);
|
|
495
570
|
|
|
496
571
|
const out = [];
|
|
497
|
-
|
|
572
|
+
if (mode === "working-tree" || mode === "working") {
|
|
573
|
+
for (const range of [[`${resolvedRef}...HEAD`], ["HEAD"]]) {
|
|
574
|
+
let raw = "";
|
|
575
|
+
try {
|
|
576
|
+
raw = git(["diff", "--raw", "--no-renames", "-z", ...range], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
577
|
+
} catch (_) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
parseRawDiffOutput(raw, out);
|
|
581
|
+
}
|
|
582
|
+
} else if (mode === "staged" || mode === "index") {
|
|
498
583
|
let raw = "";
|
|
499
584
|
try {
|
|
500
|
-
raw = git(["diff", "--raw", "--no-renames", "-z",
|
|
501
|
-
} catch (_) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
585
|
+
raw = git(["diff", "--raw", "--no-renames", "-z", "--cached", resolvedRef], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
586
|
+
} catch (_) {}
|
|
587
|
+
parseRawDiffOutput(raw, out);
|
|
588
|
+
} else {
|
|
589
|
+
if (isBaseAtHead) {
|
|
590
|
+
const headParent = getHeadParent(root);
|
|
591
|
+
if (headParent) {
|
|
592
|
+
let raw = "";
|
|
593
|
+
try {
|
|
594
|
+
raw = git(["diff", "--raw", "--no-renames", "-z", `${headParent}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
595
|
+
} catch (_) {}
|
|
596
|
+
parseRawDiffOutput(raw, out);
|
|
597
|
+
} else {
|
|
598
|
+
let raw = "";
|
|
599
|
+
try {
|
|
600
|
+
raw = git(["diff-tree", "--root", "--no-commit-id", "-r", "--raw", "--no-renames", "-z", headCommit], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
601
|
+
} catch (_) {}
|
|
602
|
+
parseRawDiffOutput(raw, out);
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
let raw = "";
|
|
606
|
+
try {
|
|
607
|
+
raw = git(["diff", "--raw", "--no-renames", "-z", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
608
|
+
} catch (_) {}
|
|
609
|
+
parseRawDiffOutput(raw, out);
|
|
521
610
|
}
|
|
522
611
|
}
|
|
523
612
|
return out;
|
|
@@ -878,6 +967,79 @@ export function partitionTracked(root = process.cwd(), files = []) {
|
|
|
878
967
|
};
|
|
879
968
|
}
|
|
880
969
|
|
|
970
|
+
// Root-level package-manager manifests a just-run `npm install` (or equivalent)
|
|
971
|
+
// writes while onboarding a cold-start repository. `npm init -y` creates a
|
|
972
|
+
// brand-new package.json; `npm install` adds/updates the lockfile. The scope
|
|
973
|
+
// rules legitimately `protect` these — but a tool's *own* install step must not
|
|
974
|
+
// brick the first interaction a newcomer has with it. The two helpers below
|
|
975
|
+
// give callers a way to tell "fresh onboarding artifact" (untracked) apart from
|
|
976
|
+
// "a dependency change the maintainer actually shipped" (tracked+modified), so
|
|
977
|
+
// the gate keeps its teeth while task creation and the init commit hint stop
|
|
978
|
+
// tripping over scaffolding the installer just laid down.
|
|
979
|
+
const ONBOARDING_ROOT_ARTIFACTS = [
|
|
980
|
+
"package.json",
|
|
981
|
+
"package-lock.json",
|
|
982
|
+
"npm-shrinkwrap.json",
|
|
983
|
+
"yarn.lock",
|
|
984
|
+
"pnpm-lock.yaml",
|
|
985
|
+
"bun.lock",
|
|
986
|
+
"bun.lockb",
|
|
987
|
+
];
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Among the given repo-relative paths, which are *untracked* root-level
|
|
991
|
+
* package-manager artifacts — i.e. manifests a fresh `npm init -y`/install just
|
|
992
|
+
* created rather than files the maintainer committed and an agent then rewrote?
|
|
993
|
+
*
|
|
994
|
+
* The scope rules flag a brand-new package.json as `protect`, which is the
|
|
995
|
+
* right call for the gate. But a task-create preflight that rejects a brand-new
|
|
996
|
+
* repository because its own onboarding just produced package.json is a cold
|
|
997
|
+
* start deadlock: the user has not committed anything yet, so they cannot make
|
|
998
|
+
* the preflight pass without guessing the secret incantation. Modified
|
|
999
|
+
* *tracked* lockfiles are deliberately NOT returned here — those stay protected
|
|
1000
|
+
* as supply-chain tampering.
|
|
1001
|
+
*
|
|
1002
|
+
* @param {string} root
|
|
1003
|
+
* @param {string[]} files repo-relative paths (as reported by a scope phase)
|
|
1004
|
+
* @returns {string[]}
|
|
1005
|
+
*/
|
|
1006
|
+
export function untrackedOnboardingArtifacts(root = process.cwd(), files = []) {
|
|
1007
|
+
const { untracked } = partitionTracked(root, files);
|
|
1008
|
+
if (untracked.length === 0) return [];
|
|
1009
|
+
return untracked.filter((f) => ONBOARDING_ROOT_ARTIFACTS.includes(f));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Root-level package-manager artifacts a `git add` in this repo would actually
|
|
1014
|
+
* stage *right now* — present on disk and not gitignored — for a commit hint.
|
|
1015
|
+
*
|
|
1016
|
+
* A cold-start repo has these as brand-new untracked files; an existing repo
|
|
1017
|
+
* that just ran `npm install -D` may have a modified (tracked) package.json
|
|
1018
|
+
* plus a new lockfile. Gitignored lockfiles (a repo that chooses not to track
|
|
1019
|
+
* them) are excluded, so the suggested `git add …` never errors on a path git
|
|
1020
|
+
* refuses to add.
|
|
1021
|
+
*
|
|
1022
|
+
* @param {string} root
|
|
1023
|
+
* @returns {string[]}
|
|
1024
|
+
*/
|
|
1025
|
+
export function addableOnboardingArtifacts(root = process.cwd()) {
|
|
1026
|
+
let porcelain = "";
|
|
1027
|
+
try {
|
|
1028
|
+
// `--untracked-files=all` so a lone new file appears verbatim, not as a
|
|
1029
|
+
// collapsed `dir/` entry that would not match a root manifest name.
|
|
1030
|
+
porcelain = git(["status", "--porcelain", "--untracked-files=all"], { cwd: root, ignoreError: true }) || "";
|
|
1031
|
+
} catch (_) {
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
const changed = new Set();
|
|
1035
|
+
for (const line of porcelain.split("\n")) {
|
|
1036
|
+
// Porcelain v1: two status chars, one space, then the path.
|
|
1037
|
+
const path = line.length > 3 ? line.slice(3) : "";
|
|
1038
|
+
if (path && !path.startsWith('"')) changed.add(path);
|
|
1039
|
+
}
|
|
1040
|
+
return ONBOARDING_ROOT_ARTIFACTS.filter((a) => changed.has(a) && existsSync(join(root, a)));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
881
1043
|
export function detectDefaultBranch(root = process.cwd()) {
|
|
882
1044
|
// `git()` returns trimmed stdout, and "" for a non-zero exit under
|
|
883
1045
|
// ignoreError — there is no status field to read.
|
|
@@ -896,7 +1058,12 @@ export function detectDefaultBranch(root = process.cwd()) {
|
|
|
896
1058
|
}
|
|
897
1059
|
|
|
898
1060
|
for (const candidate of ["main", "master"]) {
|
|
899
|
-
if (
|
|
1061
|
+
if (
|
|
1062
|
+
ask(["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`]) ||
|
|
1063
|
+
ask(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${candidate}`])
|
|
1064
|
+
) {
|
|
1065
|
+
return candidate;
|
|
1066
|
+
}
|
|
900
1067
|
}
|
|
901
1068
|
|
|
902
1069
|
const current = ask(["branch", "--show-current"]);
|
|
@@ -914,3 +1081,107 @@ export function resolveGitRemoteOrigin(root = process.cwd()) {
|
|
|
914
1081
|
}
|
|
915
1082
|
}
|
|
916
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Default globs for generated, bundled, and lockfile paths that exhaust the diff budget.
|
|
1086
|
+
*/
|
|
1087
|
+
export const DEFAULT_GENERATED_GLOBS = [
|
|
1088
|
+
"dist/**",
|
|
1089
|
+
"**/dist/**",
|
|
1090
|
+
"**/*.map",
|
|
1091
|
+
"**/*-lock.yaml",
|
|
1092
|
+
"**/*-lock.json",
|
|
1093
|
+
"**/package-lock.json",
|
|
1094
|
+
"**/pnpm-lock.yaml",
|
|
1095
|
+
"**/yarn.lock",
|
|
1096
|
+
"**/Cargo.lock",
|
|
1097
|
+
"**/generated/**",
|
|
1098
|
+
];
|
|
1099
|
+
|
|
1100
|
+
const REGEXP_SPECIAL = ".+?^${}()|[]\\";
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Convert a glob with * and ** into an anchored RegExp.
|
|
1104
|
+
* @param {string} glob
|
|
1105
|
+
* @returns {RegExp}
|
|
1106
|
+
*/
|
|
1107
|
+
export function globToRegExp(glob = "") {
|
|
1108
|
+
let re = "";
|
|
1109
|
+
for (let i = 0; i < glob.length; i++) {
|
|
1110
|
+
const c = glob[i];
|
|
1111
|
+
if (c === "*") {
|
|
1112
|
+
if (glob[i + 1] === "*") {
|
|
1113
|
+
if (glob[i + 2] === "/") {
|
|
1114
|
+
re += "(?:.*/)?";
|
|
1115
|
+
i += 2;
|
|
1116
|
+
} else {
|
|
1117
|
+
re += ".*";
|
|
1118
|
+
i += 1;
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
re += "[^/]*";
|
|
1122
|
+
}
|
|
1123
|
+
} else if (REGEXP_SPECIAL.includes(c)) {
|
|
1124
|
+
re += "\\" + c;
|
|
1125
|
+
} else {
|
|
1126
|
+
re += c;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return new RegExp("^" + re + "$");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Filter out per-file sections whose target path matches any ignore glob from a unified git diff.
|
|
1134
|
+
* Returns the filtered diff text and the array of excluded file paths.
|
|
1135
|
+
* If filtering would remove all file sections, the original diff is preserved.
|
|
1136
|
+
*
|
|
1137
|
+
* @param {string} diff - Unified git diff
|
|
1138
|
+
* @param {string[]} [ignoreGlobs=DEFAULT_GENERATED_GLOBS] - List of glob patterns
|
|
1139
|
+
* @returns {{ diff: string, excludedPaths: string[] }}
|
|
1140
|
+
*/
|
|
1141
|
+
export function filterDiffByPaths(diff = "", ignoreGlobs = DEFAULT_GENERATED_GLOBS) {
|
|
1142
|
+
if (!diff || typeof diff !== "string" || !ignoreGlobs || ignoreGlobs.length === 0) {
|
|
1143
|
+
return { diff: diff || "", excludedPaths: [] };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const matchers = ignoreGlobs.map(globToRegExp);
|
|
1147
|
+
const sections = diff.split(/(?=^diff --git )/m);
|
|
1148
|
+
const kept = [];
|
|
1149
|
+
const excludedPaths = [];
|
|
1150
|
+
let fileSectionsCount = 0;
|
|
1151
|
+
|
|
1152
|
+
for (const section of sections) {
|
|
1153
|
+
if (!section.startsWith("diff --git ")) {
|
|
1154
|
+
if (section.length > 0) kept.push(section);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
fileSectionsCount++;
|
|
1159
|
+
const match = section.match(/^diff --git a\/.*? b\/(.+)$/m);
|
|
1160
|
+
let path = match ? match[1].trim() : undefined;
|
|
1161
|
+
if (path) {
|
|
1162
|
+
path = path.replace(/^["']|["']$/g, "");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (path && matchers.some((re) => re.test(path))) {
|
|
1166
|
+
excludedPaths.push(path);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
kept.push(section);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (fileSectionsCount > 0 && excludedPaths.length === fileSectionsCount) {
|
|
1173
|
+
return { diff, excludedPaths: [] };
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return { diff: kept.join(""), excludedPaths };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Compute byte length of diff after pruning generated files and lockfiles.
|
|
1181
|
+
*/
|
|
1182
|
+
export function prunedDiffBytes(root = process.cwd(), base = "main", mode = "committed", ignoreGlobs = DEFAULT_GENERATED_GLOBS) {
|
|
1183
|
+
const text = diffText(root, base, mode);
|
|
1184
|
+
const filtered = filterDiffByPaths(text, ignoreGlobs);
|
|
1185
|
+
return Buffer.byteLength(filtered.diff, "utf-8");
|
|
1186
|
+
}
|
|
1187
|
+
|