opencode-dux 1.0.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/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/agents/descriptions.d.ts +6 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/interpreter.d.ts +2 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +27 -0
- package/dist/agents/overrides.d.ts +18 -0
- package/dist/agents/prompt-blocks.d.ts +97 -0
- package/dist/agents/steward.d.ts +3 -0
- package/dist/cli/config-io.d.ts +24 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1006 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/mcps.d.ts +13 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +137 -0
- package/dist/cli/skills.d.ts +22 -0
- package/dist/cli/system.d.ts +5 -0
- package/dist/cli/types.d.ts +38 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/discovery/local/types.d.ts +79 -0
- package/dist/discovery/local.d.ts +73 -0
- package/dist/discovery/mcp-servers.d.ts +88 -0
- package/dist/discovery/skills.d.ts +94 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/types.d.ts +22 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +32 -0
- package/dist/hooks/foreground-fallback/index.d.ts +72 -0
- package/dist/hooks/image-hook.d.ts +5 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
- package/dist/hooks/task-session-manager/index.d.ts +52 -0
- package/dist/hooks/todo-continuation/index.d.ts +53 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31782 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/skills/registry.d.ts +29 -0
- package/dist/subscriptions/accounts-store.d.ts +57 -0
- package/dist/subscriptions/index.d.ts +13 -0
- package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
- package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
- package/dist/subscriptions/types.d.ts +115 -0
- package/dist/subscriptions/usage-service.d.ts +74 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/delegate.d.ts +14 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +18 -0
- package/dist/tui-state.d.ts +168 -0
- package/dist/tui.d.ts +37 -0
- package/dist/tui.js +1896 -0
- package/dist/utils/agent-variant.d.ts +63 -0
- package/dist/utils/compat.d.ts +30 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session-manager.d.ts +55 -0
- package/dist/utils/session.d.ts +90 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/index.ts +1 -0
- package/opencode-dux.schema.json +634 -0
- package/package.json +103 -0
- package/src/agents/descriptions.ts +55 -0
- package/src/agents/designer.test.ts +86 -0
- package/src/agents/designer.ts +154 -0
- package/src/agents/display-name.test.ts +186 -0
- package/src/agents/explorer.test.ts +79 -0
- package/src/agents/explorer.ts +144 -0
- package/src/agents/fixer.test.ts +79 -0
- package/src/agents/fixer.ts +145 -0
- package/src/agents/index.test.ts +472 -0
- package/src/agents/index.ts +248 -0
- package/src/agents/interpreter.ts +136 -0
- package/src/agents/librarian.test.ts +80 -0
- package/src/agents/librarian.ts +145 -0
- package/src/agents/oracle.test.ts +89 -0
- package/src/agents/oracle.ts +184 -0
- package/src/agents/orchestrator.test.ts +116 -0
- package/src/agents/orchestrator.ts +574 -0
- package/src/agents/overrides.ts +95 -0
- package/src/agents/prompt-blocks.test.ts +114 -0
- package/src/agents/prompt-blocks.ts +640 -0
- package/src/agents/steward.ts +146 -0
- package/src/cli/config-io.test.ts +536 -0
- package/src/cli/config-io.ts +473 -0
- package/src/cli/config-manager.test.ts +141 -0
- package/src/cli/config-manager.ts +4 -0
- package/src/cli/index.ts +88 -0
- package/src/cli/install.ts +282 -0
- package/src/cli/mcps.test.ts +62 -0
- package/src/cli/mcps.ts +39 -0
- package/src/cli/model-key-normalization.test.ts +21 -0
- package/src/cli/model-key-normalization.ts +60 -0
- package/src/cli/paths.test.ts +167 -0
- package/src/cli/paths.ts +144 -0
- package/src/cli/providers.test.ts +118 -0
- package/src/cli/providers.ts +141 -0
- package/src/cli/skills.test.ts +111 -0
- package/src/cli/skills.ts +103 -0
- package/src/cli/system.test.ts +91 -0
- package/src/cli/system.ts +180 -0
- package/src/cli/types.ts +43 -0
- package/src/config/constants.ts +58 -0
- package/src/config/index.ts +4 -0
- package/src/config/loader.test.ts +1194 -0
- package/src/config/loader.ts +269 -0
- package/src/config/model-resolution.test.ts +176 -0
- package/src/config/runtime-preset.test.ts +61 -0
- package/src/config/runtime-preset.ts +37 -0
- package/src/config/schema.ts +248 -0
- package/src/config/utils.test.ts +41 -0
- package/src/config/utils.ts +23 -0
- package/src/discovery/local/types.ts +85 -0
- package/src/discovery/local.ts +322 -0
- package/src/discovery/mcp-servers.ts +804 -0
- package/src/discovery/skills.ts +959 -0
- package/src/hooks/apply-patch/codec.test.ts +184 -0
- package/src/hooks/apply-patch/codec.ts +352 -0
- package/src/hooks/apply-patch/errors.ts +117 -0
- package/src/hooks/apply-patch/execution-context.ts +432 -0
- package/src/hooks/apply-patch/hook.test.ts +768 -0
- package/src/hooks/apply-patch/index.ts +126 -0
- package/src/hooks/apply-patch/matching.test.ts +215 -0
- package/src/hooks/apply-patch/matching.ts +586 -0
- package/src/hooks/apply-patch/operations.test.ts +1535 -0
- package/src/hooks/apply-patch/operations.ts +3 -0
- package/src/hooks/apply-patch/patch.ts +9 -0
- package/src/hooks/apply-patch/prepared-changes.ts +400 -0
- package/src/hooks/apply-patch/resolution.test.ts +420 -0
- package/src/hooks/apply-patch/resolution.ts +437 -0
- package/src/hooks/apply-patch/rewrite.ts +496 -0
- package/src/hooks/apply-patch/test-helpers.ts +52 -0
- package/src/hooks/apply-patch/types.ts +111 -0
- package/src/hooks/auto-update-checker/cache.test.ts +179 -0
- package/src/hooks/auto-update-checker/cache.ts +188 -0
- package/src/hooks/auto-update-checker/checker.test.ts +159 -0
- package/src/hooks/auto-update-checker/checker.ts +308 -0
- package/src/hooks/auto-update-checker/constants.ts +33 -0
- package/src/hooks/auto-update-checker/index.test.ts +282 -0
- package/src/hooks/auto-update-checker/index.ts +225 -0
- package/src/hooks/auto-update-checker/types.ts +26 -0
- package/src/hooks/chat-headers.test.ts +236 -0
- package/src/hooks/chat-headers.ts +97 -0
- package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
- package/src/hooks/context-pressure-reminder/index.ts +137 -0
- package/src/hooks/delegate-task-retry/guidance.ts +41 -0
- package/src/hooks/delegate-task-retry/hook.ts +23 -0
- package/src/hooks/delegate-task-retry/index.test.ts +38 -0
- package/src/hooks/delegate-task-retry/index.ts +7 -0
- package/src/hooks/delegate-task-retry/patterns.ts +79 -0
- package/src/hooks/filter-available-skills/index.test.ts +297 -0
- package/src/hooks/filter-available-skills/index.ts +160 -0
- package/src/hooks/foreground-fallback/index.test.ts +624 -0
- package/src/hooks/foreground-fallback/index.ts +374 -0
- package/src/hooks/image-hook.ts +6 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/json-error-recovery/hook.ts +73 -0
- package/src/hooks/json-error-recovery/index.test.ts +111 -0
- package/src/hooks/json-error-recovery/index.ts +6 -0
- package/src/hooks/phase-reminder/index.test.ts +74 -0
- package/src/hooks/phase-reminder/index.ts +85 -0
- package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
- package/src/hooks/post-file-tool-nudge/index.ts +63 -0
- package/src/hooks/task-session-manager/index.test.ts +833 -0
- package/src/hooks/task-session-manager/index.ts +434 -0
- package/src/hooks/todo-continuation/index.test.ts +3026 -0
- package/src/hooks/todo-continuation/index.ts +878 -0
- package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
- package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
- package/src/index.ts +1672 -0
- package/src/mcp/context7.ts +14 -0
- package/src/mcp/grep-app.ts +11 -0
- package/src/mcp/index.test.ts +96 -0
- package/src/mcp/index.ts +66 -0
- package/src/mcp/types.ts +16 -0
- package/src/mcp/websearch.ts +47 -0
- package/src/skills/codemap/README.md +60 -0
- package/src/skills/codemap/SKILL.md +174 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/registry.ts +218 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/subscriptions/accounts-store.test.ts +236 -0
- package/src/subscriptions/accounts-store.ts +184 -0
- package/src/subscriptions/index.ts +30 -0
- package/src/subscriptions/neuralwatt-scraper.ts +108 -0
- package/src/subscriptions/opencode-go-scraper.ts +301 -0
- package/src/subscriptions/types.ts +145 -0
- package/src/subscriptions/usage-service.test.ts +202 -0
- package/src/subscriptions/usage-service.ts +651 -0
- package/src/tools/ast-grep/cli.ts +257 -0
- package/src/tools/ast-grep/constants.ts +214 -0
- package/src/tools/ast-grep/downloader.ts +131 -0
- package/src/tools/ast-grep/index.ts +24 -0
- package/src/tools/ast-grep/tools.ts +117 -0
- package/src/tools/ast-grep/types.ts +51 -0
- package/src/tools/ast-grep/utils.ts +126 -0
- package/src/tools/delegate-handoff.test.ts +18 -0
- package/src/tools/delegate.ts +508 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/preset-manager.test.ts +795 -0
- package/src/tools/preset-manager.ts +332 -0
- package/src/tools/smartfetch/binary.ts +58 -0
- package/src/tools/smartfetch/cache.test.ts +34 -0
- package/src/tools/smartfetch/cache.ts +112 -0
- package/src/tools/smartfetch/constants.ts +29 -0
- package/src/tools/smartfetch/index.ts +8 -0
- package/src/tools/smartfetch/network.test.ts +178 -0
- package/src/tools/smartfetch/network.ts +614 -0
- package/src/tools/smartfetch/secondary-model.test.ts +85 -0
- package/src/tools/smartfetch/secondary-model.ts +276 -0
- package/src/tools/smartfetch/tool.test.ts +60 -0
- package/src/tools/smartfetch/tool.ts +832 -0
- package/src/tools/smartfetch/types.ts +135 -0
- package/src/tools/smartfetch/utils.test.ts +24 -0
- package/src/tools/smartfetch/utils.ts +456 -0
- package/src/tui-state.test.ts +867 -0
- package/src/tui-state.ts +1255 -0
- package/src/tui.test.ts +336 -0
- package/src/tui.ts +1539 -0
- package/src/utils/agent-variant.test.ts +244 -0
- package/src/utils/agent-variant.ts +187 -0
- package/src/utils/compat.ts +91 -0
- package/src/utils/env.ts +12 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/internal-initiator.ts +28 -0
- package/src/utils/logger.test.ts +220 -0
- package/src/utils/logger.ts +136 -0
- package/src/utils/polling.test.ts +191 -0
- package/src/utils/polling.ts +67 -0
- package/src/utils/session-manager.test.ts +173 -0
- package/src/utils/session-manager.ts +356 -0
- package/src/utils/session.test.ts +110 -0
- package/src/utils/session.ts +389 -0
- package/src/utils/subagent-depth.test.ts +170 -0
- package/src/utils/subagent-depth.ts +75 -0
- package/src/utils/system-collapse.test.ts +86 -0
- package/src/utils/system-collapse.ts +24 -0
- package/src/utils/task.test.ts +24 -0
- package/src/utils/task.ts +20 -0
- package/src/utils/zip-extractor.ts +102 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Compatibility shim for local deep imports; keep only the stable runtime
|
|
2
|
+
// surface here. `applyPreparedChanges()` remains in operations.ts as an
|
|
3
|
+
// internal best-effort helper for local tests/helpers.
|
|
4
|
+
export { parsePatch } from './codec';
|
|
5
|
+
export {
|
|
6
|
+
preparePatchChanges,
|
|
7
|
+
rewritePatch,
|
|
8
|
+
rewritePatchText,
|
|
9
|
+
} from './operations';
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createApplyPatchInternalError,
|
|
7
|
+
createApplyPatchValidationError,
|
|
8
|
+
createApplyPatchVerificationError,
|
|
9
|
+
ensureApplyPatchError,
|
|
10
|
+
getErrorMessage,
|
|
11
|
+
} from './errors';
|
|
12
|
+
import {
|
|
13
|
+
createPatchExecutionContext,
|
|
14
|
+
isMissingPathError,
|
|
15
|
+
resolvePreparedUpdate,
|
|
16
|
+
stageAddedText,
|
|
17
|
+
} from './execution-context';
|
|
18
|
+
import type { ApplyPatchRuntimeOptions, PreparedChange } from './types';
|
|
19
|
+
|
|
20
|
+
function isNormalizedAbsolutePath(filePath: string): boolean {
|
|
21
|
+
return path.isAbsolute(filePath) && path.normalize(filePath) === filePath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertPreparedChangePath(
|
|
25
|
+
value: unknown,
|
|
26
|
+
field: 'file' | 'move',
|
|
27
|
+
index: number,
|
|
28
|
+
): asserts value is string {
|
|
29
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
30
|
+
throw createApplyPatchValidationError(
|
|
31
|
+
`Prepared changes require a non-empty string ${field} at index ${index}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isNormalizedAbsolutePath(value)) {
|
|
36
|
+
throw createApplyPatchValidationError(
|
|
37
|
+
`Prepared changes require absolute normalized ${field} paths at index ${index}: ${value}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertPreparedChangesContract(
|
|
43
|
+
changes: readonly PreparedChange[],
|
|
44
|
+
): void {
|
|
45
|
+
for (const [index, change] of changes.entries()) {
|
|
46
|
+
if (!change || typeof change !== 'object') {
|
|
47
|
+
throw createApplyPatchValidationError(
|
|
48
|
+
`Prepared change at index ${index} must be an object`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!('type' in change)) {
|
|
53
|
+
throw createApplyPatchValidationError(
|
|
54
|
+
`Prepared change at index ${index} is missing type`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assertPreparedChangePath(change.file, 'file', index);
|
|
59
|
+
|
|
60
|
+
if (change.type === 'add') {
|
|
61
|
+
if (typeof change.text !== 'string') {
|
|
62
|
+
throw createApplyPatchValidationError(
|
|
63
|
+
`Prepared add at index ${index} is missing text`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (change.type === 'delete') {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (change.type === 'update') {
|
|
74
|
+
if (typeof change.text !== 'string') {
|
|
75
|
+
throw createApplyPatchValidationError(
|
|
76
|
+
`Prepared update at index ${index} is missing text`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (change.move !== undefined) {
|
|
81
|
+
assertPreparedChangePath(change.move, 'move', index);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw createApplyPatchValidationError(
|
|
88
|
+
`Prepared change at index ${index} has unsupported type`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function preparePatchChanges(
|
|
94
|
+
root: string,
|
|
95
|
+
patchText: string,
|
|
96
|
+
cfg: ApplyPatchRuntimeOptions,
|
|
97
|
+
worktree?: string,
|
|
98
|
+
): Promise<PreparedChange[]> {
|
|
99
|
+
try {
|
|
100
|
+
const { hunks, staged, getPreparedFileState, assertPreparedPathMissing } =
|
|
101
|
+
await createPatchExecutionContext(root, patchText, worktree);
|
|
102
|
+
const changes: PreparedChange[] = [];
|
|
103
|
+
|
|
104
|
+
for (const hunk of hunks) {
|
|
105
|
+
const filePath = path.resolve(root, hunk.path);
|
|
106
|
+
|
|
107
|
+
if (hunk.type === 'add') {
|
|
108
|
+
await assertPreparedPathMissing(filePath, 'add');
|
|
109
|
+
const text = stageAddedText(hunk.contents);
|
|
110
|
+
changes.push({
|
|
111
|
+
type: 'add',
|
|
112
|
+
file: filePath,
|
|
113
|
+
text,
|
|
114
|
+
});
|
|
115
|
+
staged.set(filePath, { exists: true, text, derived: true });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (hunk.type === 'delete') {
|
|
120
|
+
await getPreparedFileState(filePath, 'delete');
|
|
121
|
+
|
|
122
|
+
changes.push({ type: 'delete', file: filePath });
|
|
123
|
+
staged.set(filePath, { exists: false, derived: true });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const current = await getPreparedFileState(filePath, 'update');
|
|
128
|
+
if (!current.exists) {
|
|
129
|
+
throw createApplyPatchVerificationError(
|
|
130
|
+
`Failed to read file to update: ${filePath}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const move = hunk.move_path
|
|
135
|
+
? path.resolve(root, hunk.move_path)
|
|
136
|
+
: undefined;
|
|
137
|
+
if (move && move !== filePath) {
|
|
138
|
+
await assertPreparedPathMissing(move, 'move');
|
|
139
|
+
}
|
|
140
|
+
const { nextText } = resolvePreparedUpdate(
|
|
141
|
+
filePath,
|
|
142
|
+
current.text,
|
|
143
|
+
hunk,
|
|
144
|
+
cfg,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
changes.push({
|
|
148
|
+
type: 'update',
|
|
149
|
+
file: filePath,
|
|
150
|
+
move,
|
|
151
|
+
text: nextText,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (move && move !== filePath) {
|
|
155
|
+
staged.set(filePath, { exists: false, derived: true });
|
|
156
|
+
staged.set(move, {
|
|
157
|
+
exists: true,
|
|
158
|
+
text: nextText,
|
|
159
|
+
mode: current.mode,
|
|
160
|
+
derived: true,
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
staged.set(filePath, {
|
|
166
|
+
exists: true,
|
|
167
|
+
text: nextText,
|
|
168
|
+
mode: current.mode,
|
|
169
|
+
derived: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return changes;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw ensureApplyPatchError(error, 'Unexpected prepare failure');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type FileSnapshot =
|
|
180
|
+
| { type: 'missing' }
|
|
181
|
+
| {
|
|
182
|
+
type: 'file';
|
|
183
|
+
text: string;
|
|
184
|
+
mode: number;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
async function readSnapshot(filePath: string): Promise<FileSnapshot> {
|
|
188
|
+
try {
|
|
189
|
+
const stat = await fs.stat(filePath);
|
|
190
|
+
if (stat.isDirectory()) {
|
|
191
|
+
throw createApplyPatchInternalError(
|
|
192
|
+
`Refusing to overwrite directory while applying prepared changes: ${filePath}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
type: 'file',
|
|
198
|
+
text: await fs.readFile(filePath, 'utf-8'),
|
|
199
|
+
mode: stat.mode & 0o7777,
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (isMissingPathError(error)) {
|
|
203
|
+
return { type: 'missing' };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw createApplyPatchInternalError(
|
|
207
|
+
`Failed to snapshot file before apply: ${filePath}`,
|
|
208
|
+
error,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function restoreSnapshot(
|
|
214
|
+
filePath: string,
|
|
215
|
+
snapshot: FileSnapshot,
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
if (snapshot.type === 'missing') {
|
|
218
|
+
await fs.rm(filePath, { force: true });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
223
|
+
await writeFileAtomically(filePath, snapshot.text, snapshot.mode);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function createTempSiblingPath(target: string): string {
|
|
227
|
+
return path.join(
|
|
228
|
+
path.dirname(target),
|
|
229
|
+
`.${path.basename(target)}.apply-patch-${randomUUID()}.tmp`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function writeFileAtomically(
|
|
234
|
+
target: string,
|
|
235
|
+
text: string,
|
|
236
|
+
mode?: number,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
const tempPath = createTempSiblingPath(target);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
242
|
+
await fs.writeFile(tempPath, text, 'utf-8');
|
|
243
|
+
if (mode !== undefined) {
|
|
244
|
+
await fs.chmod(tempPath, mode);
|
|
245
|
+
}
|
|
246
|
+
await fs.rename(tempPath, target);
|
|
247
|
+
} finally {
|
|
248
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getSnapshotMode(snapshot: FileSnapshot): number | undefined {
|
|
253
|
+
return snapshot.type === 'file' ? snapshot.mode : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function assertPreparedApplyPreconditions(
|
|
257
|
+
changes: PreparedChange[],
|
|
258
|
+
snapshots: Map<string, FileSnapshot>,
|
|
259
|
+
): void {
|
|
260
|
+
const staged = new Map<string, FileSnapshot['type']>();
|
|
261
|
+
|
|
262
|
+
function pathState(filePath: string): FileSnapshot['type'] {
|
|
263
|
+
if (staged.has(filePath)) {
|
|
264
|
+
return staged.get(filePath) ?? 'missing';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return snapshots.get(filePath)?.type ?? 'missing';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const change of changes) {
|
|
271
|
+
if (change.type === 'add') {
|
|
272
|
+
if (pathState(change.file) !== 'missing') {
|
|
273
|
+
throw createApplyPatchVerificationError(
|
|
274
|
+
`Prepared add target already exists: ${change.file}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
staged.set(change.file, 'file');
|
|
279
|
+
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (change.type === 'delete') {
|
|
284
|
+
if (pathState(change.file) !== 'file') {
|
|
285
|
+
throw createApplyPatchVerificationError(
|
|
286
|
+
`Prepared delete source does not exist: ${change.file}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
staged.set(change.file, 'missing');
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (pathState(change.file) !== 'file') {
|
|
295
|
+
throw createApplyPatchVerificationError(
|
|
296
|
+
change.move && change.move !== change.file
|
|
297
|
+
? `Prepared move source does not exist: ${change.file}`
|
|
298
|
+
: `Prepared update source does not exist: ${change.file}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (change.move && change.move !== change.file) {
|
|
303
|
+
if (pathState(change.move) !== 'missing') {
|
|
304
|
+
throw createApplyPatchVerificationError(
|
|
305
|
+
`Prepared move destination already exists: ${change.move}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
staged.set(change.file, 'missing');
|
|
310
|
+
staged.set(change.move, 'file');
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
staged.set(change.file, 'file');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Internal best-effort helper that applies the output of
|
|
320
|
+
* `preparePatchChanges()`: it snapshots all touched paths first and uses
|
|
321
|
+
* temp + rename for writes to regular files. It is not a universal multi-file
|
|
322
|
+
* transaction and is not perfect against concurrent external interference,
|
|
323
|
+
* but it avoids leaving silent partial states on normal apply failures.
|
|
324
|
+
*
|
|
325
|
+
* Contract: although it is exported for local tests/helpers, its expected
|
|
326
|
+
* input is the already prepared output of `preparePatchChanges()`. If it
|
|
327
|
+
* receives manual arrays, it revalidates the basic shape
|
|
328
|
+
* (types/text/normalized absolute paths) and filesystem invariants: it
|
|
329
|
+
* rejects updates/deletes/moves whose source does not exist, and add/move
|
|
330
|
+
* operations whose destination is already occupied.
|
|
331
|
+
*/
|
|
332
|
+
export async function applyPreparedChanges(
|
|
333
|
+
changes: PreparedChange[],
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
assertPreparedChangesContract(changes);
|
|
336
|
+
|
|
337
|
+
const snapshots = new Map<string, FileSnapshot>();
|
|
338
|
+
|
|
339
|
+
for (const change of changes) {
|
|
340
|
+
if (!snapshots.has(change.file)) {
|
|
341
|
+
snapshots.set(change.file, await readSnapshot(change.file));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
change.type === 'update' &&
|
|
346
|
+
change.move &&
|
|
347
|
+
!snapshots.has(change.move)
|
|
348
|
+
) {
|
|
349
|
+
snapshots.set(change.move, await readSnapshot(change.move));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
assertPreparedApplyPreconditions(changes, snapshots);
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
for (const change of changes) {
|
|
357
|
+
if (change.type === 'add') {
|
|
358
|
+
await writeFileAtomically(change.file, change.text);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (change.type === 'delete') {
|
|
363
|
+
await fs.unlink(change.file);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (change.move && change.move !== change.file) {
|
|
368
|
+
await writeFileAtomically(
|
|
369
|
+
change.move,
|
|
370
|
+
change.text,
|
|
371
|
+
getSnapshotMode(snapshots.get(change.file) ?? { type: 'missing' }),
|
|
372
|
+
);
|
|
373
|
+
await fs.unlink(change.file);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await writeFileAtomically(
|
|
378
|
+
change.file,
|
|
379
|
+
change.text,
|
|
380
|
+
getSnapshotMode(snapshots.get(change.file) ?? { type: 'missing' }),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const rollbackFailures: string[] = [];
|
|
385
|
+
|
|
386
|
+
for (const [filePath, snapshot] of [...snapshots.entries()].reverse()) {
|
|
387
|
+
try {
|
|
388
|
+
await restoreSnapshot(filePath, snapshot);
|
|
389
|
+
} catch (rollbackError) {
|
|
390
|
+
rollbackFailures.push(`${filePath}: ${getErrorMessage(rollbackError)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const message = rollbackFailures.length
|
|
395
|
+
? `Failed to apply prepared changes and rollback was incomplete: ${getErrorMessage(error)}; rollback issues: ${rollbackFailures.join('; ')}`
|
|
396
|
+
: `Failed to apply prepared changes; rolled back touched files: ${getErrorMessage(error)}`;
|
|
397
|
+
|
|
398
|
+
throw createApplyPatchInternalError(message, error);
|
|
399
|
+
}
|
|
400
|
+
}
|