@cad0p/pi-steering 0.1.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/CHANGELOG.md +36 -0
- package/LICENSE +29 -0
- package/README.md +920 -0
- package/dist/__test-helpers__.d.ts +95 -0
- package/dist/__test-helpers__.d.ts.map +1 -0
- package/dist/__test-helpers__.js +174 -0
- package/dist/__test-helpers__.js.map +1 -0
- package/dist/bin/pi-steering.d.ts +9 -0
- package/dist/bin/pi-steering.d.ts.map +1 -0
- package/dist/bin/pi-steering.js +506 -0
- package/dist/bin/pi-steering.js.map +1 -0
- package/dist/compat.d.ts +50 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +180 -0
- package/dist/compat.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +106 -0
- package/dist/defaults.js.map +1 -0
- package/dist/define-config.d.ts +235 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +118 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define-predicate.d.ts +49 -0
- package/dist/define-predicate.d.ts.map +1 -0
- package/dist/define-predicate.js +35 -0
- package/dist/define-predicate.js.map +1 -0
- package/dist/evaluator-internals/context.d.ts +186 -0
- package/dist/evaluator-internals/context.d.ts.map +1 -0
- package/dist/evaluator-internals/context.js +219 -0
- package/dist/evaluator-internals/context.js.map +1 -0
- package/dist/evaluator-internals/override.d.ts +19 -0
- package/dist/evaluator-internals/override.d.ts.map +1 -0
- package/dist/evaluator-internals/override.js +79 -0
- package/dist/evaluator-internals/override.js.map +1 -0
- package/dist/evaluator-internals/predicates.d.ts +152 -0
- package/dist/evaluator-internals/predicates.d.ts.map +1 -0
- package/dist/evaluator-internals/predicates.js +849 -0
- package/dist/evaluator-internals/predicates.js.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts +102 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.js +101 -0
- package/dist/evaluator-internals/speculative-synthesis.js.map +1 -0
- package/dist/evaluator.d.ts +101 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +714 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts +68 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.js +73 -0
- package/dist/helpers/walker-unknown-cwd-reason.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/drop-unused-observers.d.ts +43 -0
- package/dist/internal/drop-unused-observers.d.ts.map +1 -0
- package/dist/internal/drop-unused-observers.js +50 -0
- package/dist/internal/drop-unused-observers.js.map +1 -0
- package/dist/internal/finalize-plugin-state.d.ts +22 -0
- package/dist/internal/finalize-plugin-state.d.ts.map +1 -0
- package/dist/internal/finalize-plugin-state.js +14 -0
- package/dist/internal/finalize-plugin-state.js.map +1 -0
- package/dist/internal/merge-observers.d.ts +30 -0
- package/dist/internal/merge-observers.d.ts.map +1 -0
- package/dist/internal/merge-observers.js +27 -0
- package/dist/internal/merge-observers.js.map +1 -0
- package/dist/internal/pattern-utils.d.ts +33 -0
- package/dist/internal/pattern-utils.d.ts.map +1 -0
- package/dist/internal/pattern-utils.js +20 -0
- package/dist/internal/pattern-utils.js.map +1 -0
- package/dist/internal/ref-text.d.ts +11 -0
- package/dist/internal/ref-text.d.ts.map +1 -0
- package/dist/internal/ref-text.js +15 -0
- package/dist/internal/ref-text.js.map +1 -0
- package/dist/internal/session-runtime.d.ts +44 -0
- package/dist/internal/session-runtime.d.ts.map +1 -0
- package/dist/internal/session-runtime.js +143 -0
- package/dist/internal/session-runtime.js.map +1 -0
- package/dist/internal/watch-matcher.d.ts +56 -0
- package/dist/internal/watch-matcher.d.ts.map +1 -0
- package/dist/internal/watch-matcher.js +179 -0
- package/dist/internal/watch-matcher.js.map +1 -0
- package/dist/loader.d.ts +97 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +527 -0
- package/dist/loader.js.map +1 -0
- package/dist/observer-dispatcher.d.ts +69 -0
- package/dist/observer-dispatcher.d.ts.map +1 -0
- package/dist/observer-dispatcher.js +184 -0
- package/dist/observer-dispatcher.js.map +1 -0
- package/dist/plugin-merger.d.ts +175 -0
- package/dist/plugin-merger.d.ts.map +1 -0
- package/dist/plugin-merger.js +450 -0
- package/dist/plugin-merger.js.map +1 -0
- package/dist/plugins/git/branch-tracker.d.ts +85 -0
- package/dist/plugins/git/branch-tracker.d.ts.map +1 -0
- package/dist/plugins/git/branch-tracker.js +181 -0
- package/dist/plugins/git/branch-tracker.js.map +1 -0
- package/dist/plugins/git/cwd-extensions.d.ts +10 -0
- package/dist/plugins/git/cwd-extensions.d.ts.map +1 -0
- package/dist/plugins/git/cwd-extensions.js +155 -0
- package/dist/plugins/git/cwd-extensions.js.map +1 -0
- package/dist/plugins/git/git-ops.d.ts +89 -0
- package/dist/plugins/git/git-ops.d.ts.map +1 -0
- package/dist/plugins/git/git-ops.js +107 -0
- package/dist/plugins/git/git-ops.js.map +1 -0
- package/dist/plugins/git/index.d.ts +225 -0
- package/dist/plugins/git/index.d.ts.map +1 -0
- package/dist/plugins/git/index.js +52 -0
- package/dist/plugins/git/index.js.map +1 -0
- package/dist/plugins/git/predicates.d.ts +312 -0
- package/dist/plugins/git/predicates.d.ts.map +1 -0
- package/dist/plugins/git/predicates.js +580 -0
- package/dist/plugins/git/predicates.js.map +1 -0
- package/dist/plugins/git/rules.d.ts +209 -0
- package/dist/plugins/git/rules.d.ts.map +1 -0
- package/dist/plugins/git/rules.js +267 -0
- package/dist/plugins/git/rules.js.map +1 -0
- package/dist/schema.d.ts +1799 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/schema.js.map +1 -0
- package/dist/testing/index.d.ts +621 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +747 -0
- package/dist/testing/index.js.map +1 -0
- package/examples/README.md +83 -0
- package/examples/combined-git-discipline/README.md +52 -0
- package/examples/combined-git-discipline/node_modules/.bin/pi-steering +43 -0
- package/examples/combined-git-discipline/package.json +18 -0
- package/examples/combined-git-discipline/steering.json +27 -0
- package/examples/combined-git-discipline/steering.test.ts +42 -0
- package/examples/combined-git-discipline/steering.ts +64 -0
- package/examples/combined-git-discipline/tsconfig.json +7 -0
- package/examples/draft-prs-only/README.md +42 -0
- package/examples/draft-prs-only/node_modules/.bin/pi-steering +43 -0
- package/examples/draft-prs-only/package.json +18 -0
- package/examples/draft-prs-only/steering.json +12 -0
- package/examples/draft-prs-only/steering.test.ts +48 -0
- package/examples/draft-prs-only/steering.ts +36 -0
- package/examples/draft-prs-only/tsconfig.json +7 -0
- package/examples/dynamic-reason-runtime-cwd/README.md +148 -0
- package/examples/dynamic-reason-runtime-cwd/node_modules/.bin/pi-steering +43 -0
- package/examples/dynamic-reason-runtime-cwd/package.json +18 -0
- package/examples/dynamic-reason-runtime-cwd/steering.test.ts +130 -0
- package/examples/dynamic-reason-runtime-cwd/steering.ts +82 -0
- package/examples/dynamic-reason-runtime-cwd/tsconfig.json +7 -0
- package/examples/force-push-strict/README.md +75 -0
- package/examples/force-push-strict/node_modules/.bin/pi-steering +43 -0
- package/examples/force-push-strict/package.json +18 -0
- package/examples/force-push-strict/steering.json +12 -0
- package/examples/force-push-strict/steering.test.ts +51 -0
- package/examples/force-push-strict/steering.ts +54 -0
- package/examples/force-push-strict/tsconfig.json +7 -0
- package/examples/no-amend/README.md +52 -0
- package/examples/no-amend/node_modules/.bin/pi-steering +43 -0
- package/examples/no-amend/package.json +18 -0
- package/examples/no-amend/steering.cwd-scoped.json +12 -0
- package/examples/no-amend/steering.json +11 -0
- package/examples/no-amend/steering.test.ts +49 -0
- package/examples/no-amend/steering.ts +38 -0
- package/examples/no-amend/tsconfig.json +7 -0
- package/examples/work-item-plugin/README.md +103 -0
- package/examples/work-item-plugin/node_modules/.bin/pi-steering +43 -0
- package/examples/work-item-plugin/package.json +19 -0
- package/examples/work-item-plugin/src/index.test.ts +168 -0
- package/examples/work-item-plugin/src/index.ts +122 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.test.ts +95 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.ts +88 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.test.ts +81 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.ts +81 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.test.ts +157 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.ts +99 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.test.ts +107 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.ts +83 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.test.ts +79 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.ts +56 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.test.ts +245 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.ts +62 -0
- package/examples/work-item-plugin/tsconfig.json +8 -0
- package/package.json +88 -0
- package/skills/steering-authoring/SKILL.md +102 -0
- package/src/INVARIANTS.md +116 -0
- package/src/__test-helpers__.ts +243 -0
- package/src/bin/pi-steering.test.ts +920 -0
- package/src/bin/pi-steering.ts +584 -0
- package/src/compat.test.ts +431 -0
- package/src/compat.ts +282 -0
- package/src/defaults.test.ts +585 -0
- package/src/defaults.ts +148 -0
- package/src/define-config.test.ts +888 -0
- package/src/define-config.ts +375 -0
- package/src/define-predicate.test.ts +70 -0
- package/src/define-predicate.ts +57 -0
- package/src/evaluator-internals/context.ts +290 -0
- package/src/evaluator-internals/override.test.ts +99 -0
- package/src/evaluator-internals/override.ts +83 -0
- package/src/evaluator-internals/predicates.ts +1037 -0
- package/src/evaluator-internals/speculative-synthesis.test.ts +411 -0
- package/src/evaluator-internals/speculative-synthesis.ts +199 -0
- package/src/evaluator.test.ts +6296 -0
- package/src/evaluator.ts +1106 -0
- package/src/factory-time-load.test.ts +630 -0
- package/src/helpers/walker-unknown-cwd-reason.test.ts +73 -0
- package/src/helpers/walker-unknown-cwd-reason.ts +83 -0
- package/src/index.test.ts +836 -0
- package/src/index.ts +229 -0
- package/src/internal/drop-unused-observers.test.ts +141 -0
- package/src/internal/drop-unused-observers.ts +81 -0
- package/src/internal/finalize-plugin-state.ts +43 -0
- package/src/internal/merge-observers.ts +51 -0
- package/src/internal/pattern-utils.test.ts +52 -0
- package/src/internal/pattern-utils.ts +38 -0
- package/src/internal/ref-text.test.ts +54 -0
- package/src/internal/ref-text.ts +17 -0
- package/src/internal/session-runtime.test.ts +629 -0
- package/src/internal/session-runtime.ts +201 -0
- package/src/internal/watch-matcher.test.ts +326 -0
- package/src/internal/watch-matcher.ts +200 -0
- package/src/jsdoc-propagation.test.ts +277 -0
- package/src/loader.test.ts +1098 -0
- package/src/loader.ts +581 -0
- package/src/not-block-onunknown.test.ts +554 -0
- package/src/observer-dispatcher.test.ts +939 -0
- package/src/observer-dispatcher.ts +290 -0
- package/src/per-predicate-typing.test.ts +361 -0
- package/src/plugin-merger.test.ts +790 -0
- package/src/plugin-merger.ts +598 -0
- package/src/plugins/git/README.md +485 -0
- package/src/plugins/git/branch-tracker.test.ts +281 -0
- package/src/plugins/git/branch-tracker.ts +187 -0
- package/src/plugins/git/cwd-extensions.test.ts +230 -0
- package/src/plugins/git/cwd-extensions.ts +160 -0
- package/src/plugins/git/git-ops.test.ts +397 -0
- package/src/plugins/git/git-ops.ts +173 -0
- package/src/plugins/git/index.ts +253 -0
- package/src/plugins/git/integration.test.ts +705 -0
- package/src/plugins/git/predicates.test.ts +1401 -0
- package/src/plugins/git/predicates.ts +693 -0
- package/src/plugins/git/rules.test.ts +1001 -0
- package/src/plugins/git/rules.ts +307 -0
- package/src/schema.test.ts +259 -0
- package/src/schema.ts +1963 -0
- package/src/testing/index.test.ts +1696 -0
- package/src/testing/index.ts +1472 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the git plugin's cwd tracker extension
|
|
6
|
+
* (`./cwd-extensions.ts`).
|
|
7
|
+
*
|
|
8
|
+
* These tests walk scripts against a MANUALLY composed cwd tracker -
|
|
9
|
+
* the core `cwdTracker` with the plugin's `gitCwdExtensions` appended
|
|
10
|
+
* under basename `git`. This mirrors exactly what the plugin merger
|
|
11
|
+
* produces at runtime, without pulling the merger into every test.
|
|
12
|
+
* See `./integration.test.ts` for end-to-end behavior through the
|
|
13
|
+
* evaluator.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { describe, it } from "node:test";
|
|
18
|
+
import {
|
|
19
|
+
type CommandRef,
|
|
20
|
+
cwdTracker,
|
|
21
|
+
extractAllCommandsFromAST,
|
|
22
|
+
getBasename,
|
|
23
|
+
getCommandArgs,
|
|
24
|
+
type Modifier,
|
|
25
|
+
parse as parseBash,
|
|
26
|
+
type Tracker,
|
|
27
|
+
walk,
|
|
28
|
+
} from "@cad0p/unbash-walker";
|
|
29
|
+
import { gitCwdExtensions } from "./cwd-extensions.ts";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Harness
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a cwd tracker with the plugin's extensions appended to the
|
|
37
|
+
* core `git` basename slot. The same shape the plugin merger produces
|
|
38
|
+
* at runtime via `composeTracker`.
|
|
39
|
+
*/
|
|
40
|
+
function composeCwdWithExtensions(): Tracker<string> {
|
|
41
|
+
const existing = cwdTracker.modifiers["git"];
|
|
42
|
+
const existingList: Modifier<string>[] = Array.isArray(existing)
|
|
43
|
+
? ([...existing] as Modifier<string>[])
|
|
44
|
+
: existing !== undefined
|
|
45
|
+
? [existing as Modifier<string>]
|
|
46
|
+
: [];
|
|
47
|
+
return {
|
|
48
|
+
...cwdTracker,
|
|
49
|
+
modifiers: {
|
|
50
|
+
...cwdTracker.modifiers,
|
|
51
|
+
git: [
|
|
52
|
+
...existingList,
|
|
53
|
+
...(gitCwdExtensions as readonly Modifier<string>[]),
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface WalkedCommand {
|
|
60
|
+
ref: CommandRef;
|
|
61
|
+
text: string;
|
|
62
|
+
cwd: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function walkCwd(script: string, initialCwd = "/initial"): WalkedCommand[] {
|
|
66
|
+
const tracker = composeCwdWithExtensions();
|
|
67
|
+
const ast = parseBash(script);
|
|
68
|
+
const refs = extractAllCommandsFromAST(ast, script);
|
|
69
|
+
const result = walk(ast, { cwd: initialCwd }, { cwd: tracker }, refs);
|
|
70
|
+
return refs.map((ref) => {
|
|
71
|
+
const snap = result.get(ref);
|
|
72
|
+
return {
|
|
73
|
+
ref,
|
|
74
|
+
text: `${getBasename(ref)} ${getCommandArgs(ref).join(" ")}`.trim(),
|
|
75
|
+
cwd: (snap?.cwd as string | undefined) ?? "<missing>",
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cwdOf(walked: WalkedCommand[], prefix: string): string {
|
|
81
|
+
const hit = walked.find((w) => w.text.startsWith(prefix));
|
|
82
|
+
if (!hit) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`no command starting with "${prefix}" in walked set: ` +
|
|
85
|
+
walked.map((w) => w.text).join(" | "),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return hit.cwd;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// --git-dir=
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
describe("gitCwdExtensions: --git-dir=PATH", () => {
|
|
96
|
+
it("absolute path replaces the effective cwd", () => {
|
|
97
|
+
const walked = walkCwd("git --git-dir=/b status");
|
|
98
|
+
assert.equal(cwdOf(walked, "git --git-dir"), "/b");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("relative path joins against the current cwd", () => {
|
|
102
|
+
const walked = walkCwd("git --git-dir=subrepo status", "/work");
|
|
103
|
+
assert.equal(cwdOf(walked, "git --git-dir"), "/work/subrepo");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("per-command semantics - does not propagate to the next command", () => {
|
|
107
|
+
// Per-command scope: the modifier applies to THIS command only.
|
|
108
|
+
// The next sibling command sees the pre-modifier cwd.
|
|
109
|
+
const walked = walkCwd("git --git-dir=/b status && git log", "/initial");
|
|
110
|
+
assert.equal(cwdOf(walked, "git --git-dir"), "/b");
|
|
111
|
+
assert.equal(cwdOf(walked, "git log"), "/initial");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("non-static target collapses cwd to unknown", () => {
|
|
115
|
+
// `--git-dir=$V` - the overall Word is non-static via
|
|
116
|
+
// SimpleExpansion. Extension returns undefined; walker emits
|
|
117
|
+
// the cwd tracker's `"unknown"` sentinel.
|
|
118
|
+
const walked = walkCwd("git --git-dir=$V status");
|
|
119
|
+
assert.equal(cwdOf(walked, "git --git-dir"), "unknown");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// --work-tree=
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe("gitCwdExtensions: --work-tree=PATH", () => {
|
|
128
|
+
it("absolute path replaces the effective cwd", () => {
|
|
129
|
+
const walked = walkCwd("git --work-tree=/c status");
|
|
130
|
+
assert.equal(cwdOf(walked, "git --work-tree"), "/c");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("relative path joins against the current cwd", () => {
|
|
134
|
+
const walked = walkCwd("git --work-tree=tree status", "/repo");
|
|
135
|
+
assert.equal(cwdOf(walked, "git --work-tree"), "/repo/tree");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("`git --work-tree=$V status` collapses cwd to unknown", () => {
|
|
139
|
+
const walked = walkCwd("git --work-tree=$V status");
|
|
140
|
+
assert.equal(cwdOf(walked, "git --work-tree"), "unknown");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("`--work-tree=/t` is per-command (doesn't propagate)", () => {
|
|
144
|
+
const walked = walkCwd("git --work-tree=/t status && git log", "/initial");
|
|
145
|
+
assert.equal(cwdOf(walked, "git --work-tree"), "/t");
|
|
146
|
+
assert.equal(cwdOf(walked, "git log"), "/initial");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("multiple --git-dir= / --work-tree= flags compose (last absolute wins)", () => {
|
|
150
|
+
const walked = walkCwd("git --git-dir=/a --work-tree=/b status");
|
|
151
|
+
assert.equal(cwdOf(walked, "git --git-dir"), "/b");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Composition with the core `-C` modifier
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
describe("gitCwdExtensions: composition with core -C", () => {
|
|
160
|
+
// Pins the "extension wins" precedence. The walker runs per-command
|
|
161
|
+
// modifiers left-to-right; the plugin merger registers the extension
|
|
162
|
+
// AFTER the core modifier, so the extension's value is applied last.
|
|
163
|
+
// This matches git's documented precedence for --git-dir / --work-tree
|
|
164
|
+
// over -C.
|
|
165
|
+
it("`git -C /a --git-dir=/b status` -> /b (extension wins on absolute)", () => {
|
|
166
|
+
const walked = walkCwd("git -C /a --git-dir=/b status");
|
|
167
|
+
assert.equal(cwdOf(walked, "git -C"), "/b");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("`git --git-dir=/b -C /a status` -> /b (same verdict regardless of source order)", () => {
|
|
171
|
+
// Token order in the source doesn't change the modifier
|
|
172
|
+
// composition order - that's determined by registration in the
|
|
173
|
+
// plugin merger. Both orders produce the same final cwd here.
|
|
174
|
+
const walked = walkCwd("git --git-dir=/b -C /a status");
|
|
175
|
+
assert.equal(cwdOf(walked, "git --git-dir"), "/b");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("`git -C /a --git-dir=b status` -> /a/b (extension joins relative against -C result)", () => {
|
|
179
|
+
// The extension's modifier runs SECOND with the core modifier's
|
|
180
|
+
// output (/a) as its running cwd. A relative --git-dir joins
|
|
181
|
+
// against that, producing /a/b.
|
|
182
|
+
const walked = walkCwd("git -C /a --git-dir=b status");
|
|
183
|
+
assert.equal(cwdOf(walked, "git -C"), "/a/b");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("`git -C /a status` (no extension flag) still routes through the core -C", () => {
|
|
187
|
+
// Regression guard: adding the extension must not disturb
|
|
188
|
+
// bare `-C` behavior.
|
|
189
|
+
const walked = walkCwd("git -C /a status");
|
|
190
|
+
assert.equal(cwdOf(walked, "git -C"), "/a");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Non-interference
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
describe("gitCwdExtensions: non-interference", () => {
|
|
199
|
+
it("`git status` (no cwd flags) preserves the current cwd", () => {
|
|
200
|
+
const walked = walkCwd("git status", "/work");
|
|
201
|
+
assert.equal(cwdOf(walked, "git status"), "/work");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("`git log --grep='--git-dir=foo'` does NOT match inside a quoted arg", () => {
|
|
205
|
+
// The grep value is a SINGLE token whose text starts with
|
|
206
|
+
// `--grep=`, not `--git-dir=`. Regression guard against the
|
|
207
|
+
// naive scan-all-args trap.
|
|
208
|
+
const walked = walkCwd("git log --grep='--git-dir=foo'", "/initial");
|
|
209
|
+
assert.equal(cwdOf(walked, "git log"), "/initial");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Accepted false-positives
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
describe("cwd extensions: accepted false-positives", () => {
|
|
218
|
+
it("`git log --git-dir=/repo` over-matches (post-subcommand flag)", () => {
|
|
219
|
+
// Documented accepted false-positive: we scan all args; real git
|
|
220
|
+
// treats post-subcommand --git-dir= as a pathspec. Over-match rate
|
|
221
|
+
// is low; stopping at subcommand would break -C composition.
|
|
222
|
+
const walked = walkCwd("git log --git-dir=/repo", "/start");
|
|
223
|
+
assert.equal(cwdOf(walked, "git log"), "/repo");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("`git diff -- --git-dir=/x` over-matches (after `--` should be pathspec)", () => {
|
|
227
|
+
const walked = walkCwd("git diff -- --git-dir=/x", "/start");
|
|
228
|
+
assert.equal(cwdOf(walked, "git diff"), "/x");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `cwd` tracker extensions for the git plugin.
|
|
6
|
+
*
|
|
7
|
+
* The built-in `cwdTracker` (in unbash-walker) handles `git -C DIR
|
|
8
|
+
* subcmd`. Two narrower git flags - `--git-dir=/path` and
|
|
9
|
+
* `--work-tree=/path` - express the same "run this command as if the
|
|
10
|
+
* working directory were /path" intent, but weren't modelled in the
|
|
11
|
+
* walker's core to keep its git knowledge minimal. Parsing them lives
|
|
12
|
+
* in the git plugin as a tracker extension: registered under
|
|
13
|
+
* `trackerExtensions.cwd.git`, the modifier layers on top of the core
|
|
14
|
+
* cwd tracker's own `git` per-command modifier.
|
|
15
|
+
*
|
|
16
|
+
* Scope is `per-command` - consistent with `-C` and git's own runtime
|
|
17
|
+
* behavior: these flags affect only the command they're attached to.
|
|
18
|
+
*
|
|
19
|
+
* Composition with `-C`:
|
|
20
|
+
*
|
|
21
|
+
* `git -C /a --git-dir=/b status`
|
|
22
|
+
*
|
|
23
|
+
* The plugin-merger appends this extension's modifier AFTER the core
|
|
24
|
+
* `-C` modifier under the same basename. The walker then runs BOTH
|
|
25
|
+
* modifiers left-to-right against the same arg list, each layering
|
|
26
|
+
* its own contribution on top of the running recorded value:
|
|
27
|
+
*
|
|
28
|
+
* 1. Core `-C /a` modifier (scans pre-subcommand flags only):
|
|
29
|
+
* running cwd "/initial" -> "/a"
|
|
30
|
+
* 2. This `--git-dir=/b` modifier (scans all args for the two
|
|
31
|
+
* specific flag forms):
|
|
32
|
+
* running cwd "/a" -> "/b"
|
|
33
|
+
*
|
|
34
|
+
* The LATER modifier wins on absolute paths; for relative paths it
|
|
35
|
+
* joins against the running value from the previous step. Test
|
|
36
|
+
* expectation for `git -C /a --git-dir=b status` is `/a/b`.
|
|
37
|
+
*
|
|
38
|
+
* Order in the source matters too:
|
|
39
|
+
*
|
|
40
|
+
* - `git --git-dir=/b -C /a status` -> `/b` (core `-C` runs on
|
|
41
|
+
* the pre-sequential value `/initial`, producing `/a`; then
|
|
42
|
+
* this modifier runs against the same args, overriding to
|
|
43
|
+
* `/b`).
|
|
44
|
+
* - `git -C /a --git-dir=/b status` -> `/b` (same left-to-right
|
|
45
|
+
* modifier order; the core scanner produces `/a`, this one
|
|
46
|
+
* overrides to `/b`).
|
|
47
|
+
*
|
|
48
|
+
* In both cases the `--git-dir=` / `--work-tree=` flags win, matching
|
|
49
|
+
* git's documented precedence.
|
|
50
|
+
*
|
|
51
|
+
* Known limitations:
|
|
52
|
+
*
|
|
53
|
+
* - `--git-dir` without an `=` (space-separated form:
|
|
54
|
+
* `git --git-dir /path ...`) is NOT handled. Both git and shell
|
|
55
|
+
* tokenize this as two words; walker modifiers today only see the
|
|
56
|
+
* single `--git-dir` token. Wiring a two-token form is possible
|
|
57
|
+
* but agents overwhelmingly emit the `=` form; follow-up.
|
|
58
|
+
* - Non-static targets (`--git-dir=$VAR`) return `undefined`,
|
|
59
|
+
* collapsing the per-command cwd to `"unknown"`. Predicates then
|
|
60
|
+
* apply `onUnknown`.
|
|
61
|
+
* - `--work-tree` without `--git-dir` is semantically incomplete in
|
|
62
|
+
* real git (the gitdir still points at the default), but for cwd
|
|
63
|
+
* tracking purposes we treat them identically - they both mean
|
|
64
|
+
* "the command's effective cwd is this path". If a rule author
|
|
65
|
+
* needs the distinction (e.g. a repo-path vs tree-path
|
|
66
|
+
* predicate), they can split it in their own plugin.
|
|
67
|
+
*
|
|
68
|
+
* Known accepted false-positives:
|
|
69
|
+
*
|
|
70
|
+
* - `git log --git-dir=/repo` -> cwd recorded as /repo. Real git
|
|
71
|
+
* treats post-subcommand `--git-dir=` as a pathspec, not a
|
|
72
|
+
* global flag. Over-match rate is low in practice (agents emit
|
|
73
|
+
* `git --git-dir=/x log`, not the inverse), and stopping the
|
|
74
|
+
* scan at the first non-flag token would break composition with
|
|
75
|
+
* the core `-C` modifier which iterates the same args list.
|
|
76
|
+
* Accepted as a Phase-4 corner.
|
|
77
|
+
* - `git diff -- --git-dir=/x` -> cwd recorded as /x. Arguments
|
|
78
|
+
* after `--` are always pathspecs in real git. Same mitigation
|
|
79
|
+
* rationale as above; stopping the scan at `--` would still
|
|
80
|
+
* need to run after the core `-C` scanner has finished, not
|
|
81
|
+
* before, to avoid the composition breakage.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import * as path from "node:path";
|
|
85
|
+
import type { Word } from "unbash";
|
|
86
|
+
import { isStaticallyResolvable, type Modifier } from "@cad0p/unbash-walker";
|
|
87
|
+
|
|
88
|
+
/** Regex matched against each `git`-command argument token. */
|
|
89
|
+
const GIT_DIR_RE = /^--git-dir=(.+)$/;
|
|
90
|
+
const WORK_TREE_RE = /^--work-tree=(.+)$/;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read a word's static value. Falls back to `text` when `value`
|
|
94
|
+
* isn't materialised (pure literal fast path in unbash).
|
|
95
|
+
*/
|
|
96
|
+
function wordValue(w: Word | undefined): string | undefined {
|
|
97
|
+
return w?.value ?? w?.text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Scan git's pre-subcommand flags for `--git-dir=DIR` / `--work-tree=DIR`
|
|
102
|
+
* and layer the target on top of the current cwd.
|
|
103
|
+
*
|
|
104
|
+
* Stops at the first non-flag token (the subcommand) - identical shape
|
|
105
|
+
* to the core `-C` scanner. Multiple occurrences compose left-to-right
|
|
106
|
+
* (an absolute path replaces, a relative path joins to the running
|
|
107
|
+
* cwd). If we encounter a target we can't statically resolve, we bail
|
|
108
|
+
* returning `undefined`; the walker translates that to the cwd
|
|
109
|
+
* tracker's `"unknown"` sentinel.
|
|
110
|
+
*/
|
|
111
|
+
const gitCwdExtension: Modifier<string> = {
|
|
112
|
+
scope: "per-command",
|
|
113
|
+
apply: (args, current) => {
|
|
114
|
+
let cwd = current;
|
|
115
|
+
// Scan ALL args (not just pre-subcommand): we match only the two
|
|
116
|
+
// specific flag forms `--git-dir=X` / `--work-tree=X`, so there's
|
|
117
|
+
// no ambiguity with subcommand arguments. `git log
|
|
118
|
+
// --grep="--git-dir=foo"` is safe because the quoted string is a
|
|
119
|
+
// SINGLE argument whose text starts with `--grep`, not
|
|
120
|
+
// `--git-dir`, so the regex doesn't match.
|
|
121
|
+
//
|
|
122
|
+
// A stop-at-non-flag scan (mirroring the core `-C` modifier)
|
|
123
|
+
// would break composition with `-C`: the walker runs this
|
|
124
|
+
// modifier AGAINST THE SAME args the core `-C` modifier saw, not
|
|
125
|
+
// a reduced arg list. On `git -C /a --git-dir=/b status`, the
|
|
126
|
+
// args are still `[-C, /a, --git-dir=/b, status]`; a stop-at-
|
|
127
|
+
// non-flag scan would terminate at `/a` (the value of `-C`,
|
|
128
|
+
// which doesn't start with `-`) and miss the `--git-dir=` flag.
|
|
129
|
+
for (const w of args) {
|
|
130
|
+
const tok = wordValue(w);
|
|
131
|
+
if (tok === undefined) continue;
|
|
132
|
+
|
|
133
|
+
const gitDirMatch = tok.match(GIT_DIR_RE);
|
|
134
|
+
const workTreeMatch = tok.match(WORK_TREE_RE);
|
|
135
|
+
const match = gitDirMatch ?? workTreeMatch;
|
|
136
|
+
if (!match) continue;
|
|
137
|
+
|
|
138
|
+
// `--git-dir=$VAR` / `--work-tree=$(...)` - the captured
|
|
139
|
+
// piece is there in the raw text, but the overall Word is
|
|
140
|
+
// non-static. Refuse to invent a value.
|
|
141
|
+
if (!isStaticallyResolvable(w)) return undefined;
|
|
142
|
+
|
|
143
|
+
const target = match[1];
|
|
144
|
+
if (target === undefined || target.length === 0) continue;
|
|
145
|
+
cwd = path.isAbsolute(target) ? target : path.join(cwd, target);
|
|
146
|
+
}
|
|
147
|
+
return cwd;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Modifiers the git plugin registers under `cwd.git`. Exposed as an
|
|
153
|
+
* array so the plugin merger sees each entry distinctly; the runtime
|
|
154
|
+
* could today also accept a single modifier, but the array form
|
|
155
|
+
* leaves room for future git-specific cwd tweaks without changing
|
|
156
|
+
* the plugin manifest.
|
|
157
|
+
*/
|
|
158
|
+
export const gitCwdExtensions: readonly Modifier<unknown>[] = [
|
|
159
|
+
gitCwdExtension as Modifier<unknown>,
|
|
160
|
+
];
|