mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -22,7 +22,7 @@ test('server setup is not charged to the search budget', () => {
|
|
|
22
22
|
assert.equal(_searchRemainingMsForTest(deadline, startedAt, 0, 9_000), 1);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test('a fuzzy deadline
|
|
25
|
+
test('a fuzzy deadline returns promptly without starting another filesystem walk', async () => {
|
|
26
26
|
const root = mkdtempSync(join(tmpdir(), 'mixdog-find-budget-'));
|
|
27
27
|
try {
|
|
28
28
|
writeFileSync(join(root, 'anchor.txt'), 'anchor\n');
|
|
@@ -30,28 +30,19 @@ test('a fuzzy deadline recovers a real candidate through one targeted path scan'
|
|
|
30
30
|
new Error('native fuzzy search timed out after 1500ms.'),
|
|
31
31
|
{ code: 'NATIVE_SEARCH_TIMEOUT' },
|
|
32
32
|
);
|
|
33
|
-
let targetedEnumerationCalls = 0;
|
|
34
|
-
let targetedArgs = null;
|
|
35
33
|
const telemetry = {};
|
|
36
34
|
const out = String(await runWithLocalSearchTelemetry(telemetry, () => executeFuzzyFindTool(
|
|
37
35
|
{ query: 'needle-that-never-ranks', path: '.', head_limit: 5 },
|
|
38
36
|
root,
|
|
39
37
|
{
|
|
40
38
|
__tryServeFuzzySearch: async () => { throw timeout; },
|
|
41
|
-
__runRg: async (args) => {
|
|
42
|
-
targetedEnumerationCalls += 1;
|
|
43
|
-
targetedArgs = args;
|
|
44
|
-
return 'src/needle-that-never-ranks.ts\n';
|
|
45
|
-
},
|
|
46
39
|
},
|
|
47
40
|
)));
|
|
48
41
|
assert.ok(!/^Error[\s:[]/.test(out.trimStart()), `must not surface as a tool failure:\n${out}`);
|
|
49
|
-
assert.match(out, /
|
|
50
|
-
assert.match(out, /
|
|
51
|
-
assert.equal(targetedEnumerationCalls, 1);
|
|
52
|
-
assert.ok(targetedArgs.includes('--iglob'), `expected a targeted --iglob scan: ${JSON.stringify(targetedArgs)}`);
|
|
42
|
+
assert.match(out, /no fuzzy match yet/);
|
|
43
|
+
assert.match(out, /inventory is still building/);
|
|
53
44
|
assert.equal(telemetry.native_fuzzy_partials, 1);
|
|
54
|
-
assert.equal(telemetry.native_fuzzy_targeted_hits,
|
|
45
|
+
assert.equal(telemetry.native_fuzzy_targeted_hits, undefined);
|
|
55
46
|
assert.equal(telemetry.native_fuzzy_errors, undefined);
|
|
56
47
|
} finally {
|
|
57
48
|
rmSync(root, { recursive: true, force: true });
|
|
@@ -76,7 +67,6 @@ test('a served partial still reports its ranked matches', async () => {
|
|
|
76
67
|
scanErrors: 0,
|
|
77
68
|
walkErrorDetails: [],
|
|
78
69
|
}),
|
|
79
|
-
__runRg: async () => '',
|
|
80
70
|
},
|
|
81
71
|
));
|
|
82
72
|
assert.match(out, /src\/alpha\.ts/);
|
|
@@ -85,3 +75,29 @@ test('a served partial still reports its ranked matches', async () => {
|
|
|
85
75
|
rmSync(root, { recursive: true, force: true });
|
|
86
76
|
}
|
|
87
77
|
});
|
|
78
|
+
|
|
79
|
+
test('an empty served partial is returned without another filesystem walk', async () => {
|
|
80
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-find-empty-partial-'));
|
|
81
|
+
try {
|
|
82
|
+
const out = String(await executeFuzzyFindTool(
|
|
83
|
+
{ query: 'missing', path: '.', head_limit: 5 },
|
|
84
|
+
root,
|
|
85
|
+
{
|
|
86
|
+
__tryServeFuzzySearch: async () => ({
|
|
87
|
+
matches: [],
|
|
88
|
+
hasMore: false,
|
|
89
|
+
totalMatches: 0,
|
|
90
|
+
totalSeen: 1,
|
|
91
|
+
complete: false,
|
|
92
|
+
partial: true,
|
|
93
|
+
timeout: true,
|
|
94
|
+
scanErrors: 0,
|
|
95
|
+
walkErrorDetails: [],
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
));
|
|
99
|
+
assert.match(out, /inventory is still building/);
|
|
100
|
+
} finally {
|
|
101
|
+
rmSync(root, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -11,6 +11,12 @@ const ALWAYS_READ = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
13
|
const SHELL_OPERATOR_CHARS = '|&;<>()';
|
|
14
|
+
const OPERATION_ALIASES = new Map([
|
|
15
|
+
['--version', 'version'],
|
|
16
|
+
['-v', 'version'],
|
|
17
|
+
['--help', 'help'],
|
|
18
|
+
['-h', 'help'],
|
|
19
|
+
]);
|
|
14
20
|
|
|
15
21
|
function atTokenBoundary(char) {
|
|
16
22
|
return char === undefined || /\s/.test(char);
|
|
@@ -61,19 +67,23 @@ export function gitActionOf(operation, args) {
|
|
|
61
67
|
|
|
62
68
|
export function gitPlanIsReadOnly(plan) {
|
|
63
69
|
const { operation, args } = plan;
|
|
70
|
+
if (args.some((value) => value === '--output' || value.startsWith('--output='))) return false;
|
|
64
71
|
if (operation === 'fsck' && args.includes('--lost-found')) return false;
|
|
65
72
|
if (ALWAYS_READ.has(operation)) return true;
|
|
66
|
-
if (operation === 'reflog') return
|
|
73
|
+
if (operation === 'reflog') return ['show', 'list', 'exists'].includes(gitActionOf(operation, args));
|
|
67
74
|
if (operation === 'stash') return ['list', 'show'].includes(gitActionOf(operation, args));
|
|
68
75
|
if (operation === 'worktree') return gitActionOf(operation, args) === 'list';
|
|
69
|
-
if (operation === 'remote') return
|
|
76
|
+
if (operation === 'remote') return ['list', 'get-url', 'show'].includes(gitActionOf(operation, args));
|
|
70
77
|
if (operation === 'clean') return args.some((value) => value === '-n' || value === '--dry-run' || /^-[^-]*n/.test(value));
|
|
71
78
|
if (operation === 'bundle') return ['list-heads', 'verify'].includes(gitActionOf(operation, args));
|
|
72
79
|
if (operation === 'notes') return ['', 'list', 'show'].includes(gitActionOf(operation, args));
|
|
73
80
|
if (operation === 'replace') return args.length === 0 || args.includes('--list');
|
|
74
81
|
if (operation === 'sparse-checkout') return gitActionOf(operation, args) === 'list';
|
|
75
82
|
if (operation === 'submodule') return ['', 'status', 'summary'].includes(gitActionOf(operation, args));
|
|
76
|
-
if (operation === 'symbolic-ref')
|
|
83
|
+
if (operation === 'symbolic-ref') {
|
|
84
|
+
if (args.some((value) => value === '-d' || value === '--delete')) return false;
|
|
85
|
+
return args.filter((value) => !value.startsWith('-')).length <= 1;
|
|
86
|
+
}
|
|
77
87
|
if (operation === 'hash-object') return !args.includes('-w') && !args.includes('--stdin-paths');
|
|
78
88
|
if (operation === 'branch' || operation === 'tag') {
|
|
79
89
|
return args.length === 0 || args.some((value) => ['--list', '-l', '-a', '--all', '-r', '--remotes'].includes(value));
|
|
@@ -112,7 +122,8 @@ function parsedGitOperation(command) {
|
|
|
112
122
|
}
|
|
113
123
|
break;
|
|
114
124
|
}
|
|
115
|
-
const
|
|
125
|
+
const rawOperation = String(tokens[index] || '').toLowerCase();
|
|
126
|
+
const operation = OPERATION_ALIASES.get(rawOperation) ?? rawOperation;
|
|
116
127
|
return operation ? { operation, args: tokens.slice(index + 1) } : null;
|
|
117
128
|
}
|
|
118
129
|
|
|
@@ -208,13 +208,19 @@ function parseCommand(command, workDir) {
|
|
|
208
208
|
const rawOperation = String(tokens[index] || '').toLowerCase();
|
|
209
209
|
const operation = OPERATION_ALIASES.get(rawOperation) ?? rawOperation;
|
|
210
210
|
if (!operation) throw new Error('git requires a subcommand');
|
|
211
|
+
const args = tokens.slice(index + 1);
|
|
212
|
+
if (args.includes('--help')
|
|
213
|
+
|| (operation === 'help' && args.some((value) => !value.startsWith('-')))
|
|
214
|
+
|| (operation === 'help' && args.some((value) => ['-w', '--web', '-m', '--man', '-i', '--info'].includes(value)))) {
|
|
215
|
+
throw new Error('git manual help can open an external viewer; use the subcommand -h form for terminal usage');
|
|
216
|
+
}
|
|
211
217
|
if (GUI_OPERATIONS.has(operation)) {
|
|
212
218
|
throw new Error(`git ${operation} opens an interactive GUI and would never return here; use its non-interactive form (diff, merge, add -p)`);
|
|
213
219
|
}
|
|
214
220
|
if (SERVER_OPERATIONS.has(operation)) {
|
|
215
221
|
throw new Error(`git ${operation} is a long-running server; start it with the shell tool so it becomes a background task`);
|
|
216
222
|
}
|
|
217
|
-
return { command: String(command), cwd, globalArgs, operation, args
|
|
223
|
+
return { command: String(command), cwd, globalArgs, operation, args };
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_BYTES } = {}) {
|
|
@@ -327,12 +333,25 @@ function semanticExit(plan, result) {
|
|
|
327
333
|
if (result.exitCode === 0) return { exists: true };
|
|
328
334
|
return result.exitCode === 1 || result.exitCode === 128 ? { exists: false } : null;
|
|
329
335
|
}
|
|
336
|
+
if (operation === 'reflog' && actionOf(operation, args) === 'exists') {
|
|
337
|
+
if (result.exitCode === 0) return { exists: true };
|
|
338
|
+
return result.exitCode === 1 ? { exists: false } : null;
|
|
339
|
+
}
|
|
340
|
+
if ((operation === 'show-ref' && args.includes('--verify') && args.includes('--quiet'))
|
|
341
|
+
|| (operation === 'rev-parse' && args.includes('--verify') && args.includes('--quiet'))) {
|
|
342
|
+
if (result.exitCode === 0) return { exists: true };
|
|
343
|
+
return result.exitCode === 1 ? { exists: false } : null;
|
|
344
|
+
}
|
|
345
|
+
if (operation === 'merge-base' && args.includes('--is-ancestor')) {
|
|
346
|
+
if (result.exitCode === 0) return { ancestor: true };
|
|
347
|
+
return result.exitCode === 1 ? { ancestor: false } : null;
|
|
348
|
+
}
|
|
330
349
|
if (!['diff', 'diff-files', 'diff-index', 'diff-tree'].includes(operation)) return null;
|
|
331
350
|
if (args.includes('--check')) {
|
|
332
351
|
if (result.exitCode === 0) return { problems: false };
|
|
333
352
|
return result.exitCode === 1 || result.exitCode === 2 ? { problems: true } : null;
|
|
334
353
|
}
|
|
335
|
-
if (args.some((value) => value === '--exit-code' || value === '--quiet')) {
|
|
354
|
+
if (args.some((value) => value === '--exit-code' || value === '--quiet' || value === '--no-index')) {
|
|
336
355
|
if (result.exitCode === 0) return { changed: false };
|
|
337
356
|
return result.exitCode === 1 ? { changed: true } : null;
|
|
338
357
|
}
|
|
@@ -379,8 +398,40 @@ function hasOutputFormat(args) {
|
|
|
379
398
|
return args.some((value) => /^(?:--format|--pretty)(?:=|$)/.test(value) || value === '--oneline');
|
|
380
399
|
}
|
|
381
400
|
|
|
401
|
+
function hasPatchOutput(args) {
|
|
402
|
+
return args.some((value) => ['-p', '-u', '--patch', '--binary'].includes(value)
|
|
403
|
+
|| /^-U\d*$/.test(value)
|
|
404
|
+
|| /^--(?:patch|unified|word-diff|color-words)(?:=|$)/.test(value));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function needsTextDiffOutput(args) {
|
|
408
|
+
return args.some((value) => [
|
|
409
|
+
'-s', '--no-patch', '--raw', '--patch-with-raw', '--patch-with-stat',
|
|
410
|
+
'--numstat', '--shortstat', '--summary', '--compact-summary',
|
|
411
|
+
'--name-only', '--name-status', '--check', '--cumulative',
|
|
412
|
+
'--no-prefix', '--default-prefix',
|
|
413
|
+
].includes(value)
|
|
414
|
+
|| /^--(?:stat|dirstat|dirstat-by-file)(?:-|=|$)/.test(value)
|
|
415
|
+
|| /^--(?:line-prefix|src-prefix|dst-prefix|output-indicator-(?:new|old|context))(?:=|$)/.test(value)
|
|
416
|
+
|| /^--submodule(?:=|$)/.test(value)
|
|
417
|
+
|| /^--(?:word-diff|color-words)(?:=|$)/.test(value));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function hasDiffPresentation(args) {
|
|
421
|
+
return hasPatchOutput(args) || needsTextDiffOutput(args);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function hasSpecialHistoryPresentation(args) {
|
|
425
|
+
return hasDiffPresentation(args) || args.some((value) => [
|
|
426
|
+
'-g', '--graph', '--source', '--show-signature', '--show-notes',
|
|
427
|
+
'--notes', '--no-notes', '--walk-reflogs', '--left-right',
|
|
428
|
+
'--cherry-mark', '--boundary', '--parents', '--children',
|
|
429
|
+
].includes(value)
|
|
430
|
+
|| /^--(?:decorate|decorate-refs|decorate-refs-exclude|show-notes|notes|date)(?:=|$)/.test(value));
|
|
431
|
+
}
|
|
432
|
+
|
|
382
433
|
function hasLogLimit(args) {
|
|
383
|
-
return args.some((value) =>
|
|
434
|
+
return args.some((value) => /^-(?:n)?\d+$/.test(value) || value === '-n' || value === '--max-count' || value.startsWith('--max-count='));
|
|
384
435
|
}
|
|
385
436
|
|
|
386
437
|
function prepare(plan, limit) {
|
|
@@ -390,22 +441,26 @@ function prepare(plan, limit) {
|
|
|
390
441
|
return { argv: ['status', '--porcelain=v1', '-b', '--untracked-files=normal'], format: 'status', action: 'list' };
|
|
391
442
|
}
|
|
392
443
|
if (['diff', 'diff-files', 'diff-index', 'diff-tree'].includes(operation)) {
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
const
|
|
444
|
+
// Plumbing diff commands default to raw records, and presentation flags
|
|
445
|
+
// can replace or prefix patch markers. Only compact a standard patch.
|
|
446
|
+
const patch = !needsTextDiffOutput(args) && (operation === 'diff' || hasPatchOutput(args));
|
|
447
|
+
const format = patch ? 'diff' : 'text';
|
|
396
448
|
return { argv: [operation, '--no-ext-diff', '--no-color', ...args], format, action: 'list' };
|
|
397
449
|
}
|
|
398
|
-
if (operation === 'log'
|
|
450
|
+
if (operation === 'log') {
|
|
399
451
|
const limitArgs = hasLogLimit(args) ? [] : [`-n${limit}`];
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
452
|
+
if (!hasOutputFormat(args) && !hasSpecialHistoryPresentation(args)) {
|
|
453
|
+
return {
|
|
454
|
+
argv: ['log', ...limitArgs, '--date=iso-strict', '--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s%x1f%b%x1e', ...args],
|
|
455
|
+
format: 'log',
|
|
456
|
+
action: 'list',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return { argv: ['log', ...limitArgs, '--no-color', ...args], format: 'text', action: 'list' };
|
|
405
460
|
}
|
|
406
461
|
if (operation === 'show') {
|
|
407
462
|
const blob = args.some((value) => !value.startsWith('-') && value.includes(':'));
|
|
408
|
-
const explicit = hasOutputFormat(args) || args
|
|
463
|
+
const explicit = hasOutputFormat(args) || hasSpecialHistoryPresentation(args);
|
|
409
464
|
if (!blob && !explicit) {
|
|
410
465
|
return { argv: ['show', '--no-color', '--format=%x1e%H%x1f%h%x1f%an%x1f%aI%x1f%s%n', ...args], format: 'show', action: 'list' };
|
|
411
466
|
}
|
|
@@ -417,10 +472,13 @@ function prepare(plan, limit) {
|
|
|
417
472
|
action: 'list',
|
|
418
473
|
};
|
|
419
474
|
}
|
|
420
|
-
if (operation === 'reflog' && isReadOnly(plan) &&
|
|
475
|
+
if (operation === 'reflog' && isReadOnly(plan) && actionOf(operation, args) === 'show') {
|
|
421
476
|
const action = actionOf(operation, args);
|
|
422
477
|
const rest = action === 'show' && args[0] === 'show' ? args.slice(1) : args;
|
|
423
478
|
const limitArgs = hasLogLimit(rest) ? [] : [`-n${limit}`];
|
|
479
|
+
if (rest.some((value) => value.startsWith('--format'))) {
|
|
480
|
+
return { argv: ['reflog', 'show', ...limitArgs, ...rest], format: 'text', action: 'show' };
|
|
481
|
+
}
|
|
424
482
|
return {
|
|
425
483
|
argv: ['reflog', 'show', ...limitArgs, '--date=iso-strict', '--format=%gD%x00%H%x00%gs', ...rest],
|
|
426
484
|
format: 'reflog',
|
|
@@ -915,4 +973,4 @@ export async function executeGitTool(input, workDir, options = {}) {
|
|
|
915
973
|
})), { signal });
|
|
916
974
|
}
|
|
917
975
|
|
|
918
|
-
export const _gitCommandInternals = { creationTarget, failureText, foldProgressFrames, localizeConfigPlan, parseCommand };
|
|
976
|
+
export const _gitCommandInternals = { creationTarget, failureText, foldProgressFrames, localizeConfigPlan, parseCommand, prepare };
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
GIT_TOOL_DEF,
|
|
12
12
|
_gitCommandInternals,
|
|
13
13
|
} from './git-command-tool.mjs';
|
|
14
|
-
import { commandHasShellSyntax } from './git-command-policy.mjs';
|
|
14
|
+
import { commandHasShellSyntax, gitCommandMutates } from './git-command-policy.mjs';
|
|
15
15
|
|
|
16
16
|
function parseOk(result) {
|
|
17
17
|
assert.doesNotMatch(String(result), /^Error:/, String(result));
|
|
@@ -163,6 +163,11 @@ test('git tool answers semantic exits and keeps literal operator characters', as
|
|
|
163
163
|
// diff reports through the exit code for --quiet and --check.
|
|
164
164
|
assert.equal(parseOk(await git(repo, 'diff --quiet')).changed, false);
|
|
165
165
|
assert.equal(parseOk(await git(repo, 'diff --check')).problems, false);
|
|
166
|
+
assert.equal(parseOk(await git(repo, 'reflog exists HEAD')).exists, true);
|
|
167
|
+
assert.equal(parseOk(await git(repo, 'reflog exists refs/heads/missing')).exists, false);
|
|
168
|
+
assert.equal(parseOk(await git(repo, 'show-ref --verify --quiet refs/heads/missing')).exists, false);
|
|
169
|
+
assert.equal(parseOk(await git(repo, 'rev-parse --verify --quiet refs/heads/missing')).exists, false);
|
|
170
|
+
assert.equal(parseOk(await git(repo, 'merge-base --is-ancestor HEAD HEAD')).ancestor, true);
|
|
166
171
|
writeFileSync(join(repo, 'base.txt'), 'base\ntrailing \n');
|
|
167
172
|
assert.equal(parseOk(await git(repo, 'diff --quiet')).changed, true);
|
|
168
173
|
const check = parseOk(await git(repo, 'diff --check'));
|
|
@@ -170,6 +175,73 @@ test('git tool answers semantic exits and keeps literal operator characters', as
|
|
|
170
175
|
assert.match(JSON.stringify(check), /trailing whitespace/);
|
|
171
176
|
});
|
|
172
177
|
|
|
178
|
+
test('git preserves non-patch diff and history presentations', async (t) => {
|
|
179
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-presentations-'));
|
|
180
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
181
|
+
const repo = join(root, 'repo');
|
|
182
|
+
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
183
|
+
parseOk(await git(repo, 'config user.name "Mixdog Test"'));
|
|
184
|
+
parseOk(await git(repo, 'config user.email mixdog@example.invalid'));
|
|
185
|
+
writeFileSync(join(repo, 'base.txt'), 'base\n');
|
|
186
|
+
parseOk(await git(repo, 'add -- base.txt'));
|
|
187
|
+
parseOk(await git(repo, 'commit -m base'));
|
|
188
|
+
writeFileSync(join(repo, 'base.txt'), 'changed\n');
|
|
189
|
+
|
|
190
|
+
for (const command of [
|
|
191
|
+
'diff --stat -- base.txt',
|
|
192
|
+
'diff --raw -- base.txt',
|
|
193
|
+
'diff --numstat -- base.txt',
|
|
194
|
+
'diff --name-only -- base.txt',
|
|
195
|
+
'diff --name-status -- base.txt',
|
|
196
|
+
'diff-files -- base.txt',
|
|
197
|
+
'diff-index HEAD -- base.txt',
|
|
198
|
+
]) {
|
|
199
|
+
const result = parseOk(await git(repo, command));
|
|
200
|
+
assert.match(JSON.stringify(result), /base\.txt/, command);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
parseOk(await git(repo, 'add -- base.txt'));
|
|
204
|
+
parseOk(await git(repo, 'commit -m changed'));
|
|
205
|
+
assert.match(JSON.stringify(parseOk(await git(repo, 'diff-tree --no-commit-id HEAD^ HEAD -- base.txt'))), /base\.txt/);
|
|
206
|
+
for (const command of ['log --stat -1', 'log --raw -1', 'log -p -1', 'show --raw HEAD']) {
|
|
207
|
+
const result = parseOk(await git(repo, command));
|
|
208
|
+
assert.equal(result.commits, undefined, command);
|
|
209
|
+
assert.match(JSON.stringify(result), /base\.txt/, command);
|
|
210
|
+
}
|
|
211
|
+
assert.match(JSON.stringify(parseOk(await git(repo, 'reflog list'))), /refs\/heads\//);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('git policy distinguishes probes from output and ref mutations', () => {
|
|
215
|
+
for (const command of [
|
|
216
|
+
'git --version',
|
|
217
|
+
'git remote -v',
|
|
218
|
+
'git reflog exists HEAD',
|
|
219
|
+
'git symbolic-ref HEAD',
|
|
220
|
+
]) {
|
|
221
|
+
assert.equal(gitCommandMutates({ command }), false, command);
|
|
222
|
+
}
|
|
223
|
+
for (const command of [
|
|
224
|
+
'git remote -v update',
|
|
225
|
+
'git reflog delete HEAD@{0}',
|
|
226
|
+
'git symbolic-ref --delete HEAD',
|
|
227
|
+
'git diff --output=outside.patch',
|
|
228
|
+
]) {
|
|
229
|
+
assert.equal(gitCommandMutates({ command }), true, command);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('git plans patch, plumbing, and custom history output without over-reading', () => {
|
|
234
|
+
const plan = (operation, args, limit = 7) => _gitCommandInternals.prepare({ operation, args }, limit);
|
|
235
|
+
assert.equal(plan('diff', []).format, 'diff');
|
|
236
|
+
assert.equal(plan('diff', ['--stat']).format, 'text');
|
|
237
|
+
assert.equal(plan('diff-files', []).format, 'text');
|
|
238
|
+
assert.equal(plan('diff-files', ['-p']).format, 'diff');
|
|
239
|
+
assert.deepEqual(plan('log', ['--oneline']).argv.slice(0, 3), ['log', '-n7', '--no-color']);
|
|
240
|
+
assert.deepEqual(plan('log', ['--stat']).argv.slice(0, 3), ['log', '-n7', '--no-color']);
|
|
241
|
+
assert.equal(plan('reflog', ['list']).argv[0], 'reflog');
|
|
242
|
+
assert.notEqual(plan('reflog', ['list']).argv[1], 'show');
|
|
243
|
+
});
|
|
244
|
+
|
|
173
245
|
test('git failure detail folds progress frames and keeps the fatal tail', () => {
|
|
174
246
|
const { failureText, foldProgressFrames } = _gitCommandInternals;
|
|
175
247
|
assert.equal(
|
|
@@ -238,7 +310,7 @@ test('git forwards unknown subcommands to git instead of pre-rejecting them', as
|
|
|
238
310
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
239
311
|
const repo = join(root, 'repo');
|
|
240
312
|
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
241
|
-
for (const command of ['git filter-repo --version', 'git fast-export --
|
|
313
|
+
for (const command of ['git filter-repo --version', 'git fast-export --no-data HEAD']) {
|
|
242
314
|
assert.doesNotMatch(String(await executeGitTool({ command }, repo)), /unsupported git subcommand/);
|
|
243
315
|
}
|
|
244
316
|
});
|
|
@@ -254,6 +326,8 @@ test('git refuses only non-returning subcommands and routes servers to shell', a
|
|
|
254
326
|
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
255
327
|
assert.match(String(await executeGitTool({ command: 'git mergetool' }, repo)), /interactive GUI/i);
|
|
256
328
|
assert.match(String(await executeGitTool({ command: 'git daemon --export-all' }, repo)), /shell tool/i);
|
|
329
|
+
assert.match(String(await executeGitTool({ command: 'git fast-export --help' }, repo)), /external viewer/i);
|
|
330
|
+
assert.match(String(await executeGitTool({ command: 'git help fast-export' }, repo)), /external viewer/i);
|
|
257
331
|
assert.match(String(await executeGitTool({ command: 'git' }, repo)), /requires a subcommand/i);
|
|
258
332
|
});
|
|
259
333
|
|