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,179 @@
|
|
|
1
|
+
import { describe, expect, mock, spyOn, test } from 'bun:test';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { normalize } from 'node:path';
|
|
4
|
+
import { platform } from 'node:os';
|
|
5
|
+
|
|
6
|
+
// Mock logger to avoid noise
|
|
7
|
+
mock.module('../../utils/logger', () => ({
|
|
8
|
+
log: mock(() => {}),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
mock.module('../../cli/config-manager', () => ({
|
|
12
|
+
stripJsonComments: (s: string) => s,
|
|
13
|
+
getOpenCodeConfigPaths: () => [
|
|
14
|
+
normalize('/mock/config/opencode.json'),
|
|
15
|
+
normalize('/mock/config/opencode.jsonc'),
|
|
16
|
+
],
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Platform-normalize a Unix-style path for the current OS
|
|
20
|
+
function norm(path: string): string {
|
|
21
|
+
return normalize(path);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Cache buster for dynamic imports
|
|
25
|
+
let importCounter = 0;
|
|
26
|
+
|
|
27
|
+
const onWindows = platform() === 'win32';
|
|
28
|
+
|
|
29
|
+
const RUNTIME_PKG_JSON = norm(
|
|
30
|
+
'/home/user/.cache/opencode/packages/opencode-dux@latest/node_modules/opencode-dux/package.json',
|
|
31
|
+
);
|
|
32
|
+
const WRAPPER_PKG_JSON = norm(
|
|
33
|
+
'/home/user/.cache/opencode/packages/opencode-dux@latest/package.json',
|
|
34
|
+
);
|
|
35
|
+
const PKG_DIR = norm(
|
|
36
|
+
'/home/user/.cache/opencode/packages/opencode-dux@latest/node_modules/opencode-dux',
|
|
37
|
+
);
|
|
38
|
+
const INSTALL_DIR = norm(
|
|
39
|
+
'/home/user/.cache/opencode/packages/opencode-dux@latest',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function existsSync(path: unknown): boolean {
|
|
43
|
+
return (
|
|
44
|
+
path === WRAPPER_PKG_JSON || path === PKG_DIR
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readFileSync(path: unknown): string | Buffer {
|
|
49
|
+
if (path === WRAPPER_PKG_JSON) {
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
dependencies: { 'opencode-dux': '0.9.1' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('auto-update-checker/cache', () => {
|
|
58
|
+
describe('resolveInstallContext', () => {
|
|
59
|
+
test('detects OpenCode packages install root from runtime package path', async () => {
|
|
60
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
61
|
+
(path: unknown) => path === WRAPPER_PKG_JSON,
|
|
62
|
+
);
|
|
63
|
+
const { resolveInstallContext } = await import(
|
|
64
|
+
`./cache?test=${importCounter++}`
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const context = resolveInstallContext(RUNTIME_PKG_JSON);
|
|
68
|
+
|
|
69
|
+
expect(context).toEqual({
|
|
70
|
+
installDir: INSTALL_DIR,
|
|
71
|
+
packageJsonPath: WRAPPER_PKG_JSON,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
existsSpy.mockRestore();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('does not fall back to legacy cache when runtime path is active but wrapper root is invalid', async () => {
|
|
78
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
79
|
+
() => false,
|
|
80
|
+
);
|
|
81
|
+
const { resolveInstallContext } = await import(
|
|
82
|
+
`./cache?test=${importCounter++}`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const context = resolveInstallContext(RUNTIME_PKG_JSON);
|
|
86
|
+
|
|
87
|
+
expect(context).toBeNull();
|
|
88
|
+
|
|
89
|
+
existsSpy.mockRestore();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('preparePackageUpdate', () => {
|
|
94
|
+
test('returns null when no install context is available', async () => {
|
|
95
|
+
const existsSpy = spyOn(fs, 'existsSync').mockReturnValue(false);
|
|
96
|
+
const { preparePackageUpdate } = await import(
|
|
97
|
+
`./cache?test=${importCounter++}`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const result = preparePackageUpdate('1.0.1');
|
|
101
|
+
expect(result).toBeNull();
|
|
102
|
+
|
|
103
|
+
existsSpy.mockRestore();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('updates packages wrapper dependency and removes installed package', async () => {
|
|
107
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
108
|
+
(path: unknown) => path === WRAPPER_PKG_JSON || path === PKG_DIR,
|
|
109
|
+
);
|
|
110
|
+
const readSpy = spyOn(fs, 'readFileSync').mockReturnValue(
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
dependencies: { 'opencode-dux': '0.9.1' },
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
const writtenData: string[] = [];
|
|
116
|
+
const writeSpy = spyOn(fs, 'writeFileSync').mockImplementation(
|
|
117
|
+
(_path: unknown, data: unknown) => {
|
|
118
|
+
writtenData.push(data as string);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
const rmSyncSpy = spyOn(fs, 'rmSync').mockReturnValue(undefined);
|
|
122
|
+
const { preparePackageUpdate } = await import(
|
|
123
|
+
`./cache?test=${importCounter++}`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const result = preparePackageUpdate(
|
|
127
|
+
'0.9.11',
|
|
128
|
+
'opencode-dux',
|
|
129
|
+
RUNTIME_PKG_JSON,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(INSTALL_DIR);
|
|
133
|
+
expect(rmSyncSpy).toHaveBeenCalledWith(PKG_DIR, {
|
|
134
|
+
recursive: true,
|
|
135
|
+
force: true,
|
|
136
|
+
});
|
|
137
|
+
expect(writtenData.length).toBeGreaterThan(0);
|
|
138
|
+
expect(JSON.parse(writtenData[0])).toEqual({
|
|
139
|
+
dependencies: { 'opencode-dux': '0.9.11' },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
existsSpy.mockRestore();
|
|
143
|
+
readSpy.mockRestore();
|
|
144
|
+
writeSpy.mockRestore();
|
|
145
|
+
rmSyncSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const testLegacyCache = onWindows ? test.skip : test;
|
|
149
|
+
testLegacyCache('keeps working when dependency is already on target version', async () => {
|
|
150
|
+
const legacyCache = norm('/.cache/opencode');
|
|
151
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
152
|
+
(p: unknown) =>
|
|
153
|
+
(typeof p === 'string' && p.endsWith(norm('/.cache/opencode/package.json'))) ||
|
|
154
|
+
(typeof p === 'string' && p.endsWith(norm('/.cache/opencode/node_modules/opencode-dux'))),
|
|
155
|
+
);
|
|
156
|
+
const readSpy = spyOn(fs, 'readFileSync').mockReturnValue(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
dependencies: { 'opencode-dux': '1.0.1' },
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
const writeSpy = spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
|
162
|
+
const rmSyncSpy = spyOn(fs, 'rmSync').mockReturnValue(undefined);
|
|
163
|
+
const { preparePackageUpdate } = await import(
|
|
164
|
+
`./cache?test=${importCounter++}`
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const result = preparePackageUpdate('1.0.1', 'opencode-dux', null);
|
|
168
|
+
|
|
169
|
+
expect(typeof result === 'string' && result.endsWith(legacyCache)).toBe(true);
|
|
170
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
171
|
+
expect(rmSyncSpy).toHaveBeenCalled();
|
|
172
|
+
|
|
173
|
+
existsSpy.mockRestore();
|
|
174
|
+
readSpy.mockRestore();
|
|
175
|
+
writeSpy.mockRestore();
|
|
176
|
+
rmSyncSpy.mockRestore();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { stripJsonComments } from '../../cli/config-manager';
|
|
4
|
+
import { log } from '../../utils/logger';
|
|
5
|
+
import { getCurrentRuntimePackageJsonPath } from './checker';
|
|
6
|
+
import { CACHE_DIR, PACKAGE_NAME } from './constants';
|
|
7
|
+
|
|
8
|
+
interface BunLockfile {
|
|
9
|
+
workspaces?: {
|
|
10
|
+
''?: {
|
|
11
|
+
dependencies?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
packages?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AutoUpdateInstallContext {
|
|
18
|
+
installDir: string;
|
|
19
|
+
packageJsonPath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Removes a package from the bun.lock file if it's in JSON format.
|
|
24
|
+
* Note: Newer Bun versions (1.1+) use a custom text format for bun.lock.
|
|
25
|
+
* This function handles JSON-based lockfiles gracefully.
|
|
26
|
+
*/
|
|
27
|
+
function removeFromBunLock(installDir: string, packageName: string): boolean {
|
|
28
|
+
const lockPath = path.join(installDir, 'bun.lock');
|
|
29
|
+
if (!fs.existsSync(lockPath)) return false;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
33
|
+
let lock: BunLockfile;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
lock = JSON.parse(stripJsonComments(content)) as BunLockfile;
|
|
37
|
+
} catch {
|
|
38
|
+
// If it's not valid JSON(C), it might be the new Bun text format or binary format.
|
|
39
|
+
// For now, we only support JSON-based lockfile manipulation.
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let modified = false;
|
|
44
|
+
|
|
45
|
+
if (lock.workspaces?.['']?.dependencies?.[packageName]) {
|
|
46
|
+
delete lock.workspaces[''].dependencies[packageName];
|
|
47
|
+
modified = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (lock.packages?.[packageName]) {
|
|
51
|
+
delete lock.packages[packageName];
|
|
52
|
+
modified = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (modified) {
|
|
56
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
57
|
+
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return modified;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log(`[auto-update-checker] Failed to process bun.lock:`, err);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureDependencyVersion(
|
|
68
|
+
packageJsonPath: string,
|
|
69
|
+
packageName: string,
|
|
70
|
+
version: string,
|
|
71
|
+
): boolean {
|
|
72
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const content = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
76
|
+
const pkgJson = JSON.parse(stripJsonComments(content)) as {
|
|
77
|
+
dependencies?: Record<string, string>;
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const dependencies = { ...(pkgJson.dependencies ?? {}) };
|
|
82
|
+
if (dependencies[packageName] === version) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
dependencies[packageName] = version;
|
|
87
|
+
pkgJson.dependencies = dependencies;
|
|
88
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
89
|
+
log(
|
|
90
|
+
`[auto-update-checker] Updated dependency in package.json: ${packageName} → ${version}`,
|
|
91
|
+
);
|
|
92
|
+
return true;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
log(
|
|
95
|
+
`[auto-update-checker] Failed to update package.json dependency for auto-update:`,
|
|
96
|
+
err,
|
|
97
|
+
);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function removeInstalledPackage(
|
|
103
|
+
installDir: string,
|
|
104
|
+
packageName: string,
|
|
105
|
+
): boolean {
|
|
106
|
+
const pkgDir = path.join(installDir, 'node_modules', packageName);
|
|
107
|
+
if (!fs.existsSync(pkgDir)) return false;
|
|
108
|
+
|
|
109
|
+
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
110
|
+
log(`[auto-update-checker] Package removed: ${pkgDir}`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveInstallContext(
|
|
115
|
+
runtimePackageJsonPath: string | null = getCurrentRuntimePackageJsonPath(),
|
|
116
|
+
): AutoUpdateInstallContext | null {
|
|
117
|
+
if (runtimePackageJsonPath) {
|
|
118
|
+
const packageDir = path.dirname(runtimePackageJsonPath);
|
|
119
|
+
const nodeModulesDir = path.dirname(packageDir);
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
path.basename(packageDir) === PACKAGE_NAME &&
|
|
123
|
+
path.basename(nodeModulesDir) === 'node_modules'
|
|
124
|
+
) {
|
|
125
|
+
const installDir = path.dirname(nodeModulesDir);
|
|
126
|
+
const packageJsonPath = path.join(installDir, 'package.json');
|
|
127
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
128
|
+
return { installDir, packageJsonPath };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const legacyPackageJsonPath = path.join(CACHE_DIR, 'package.json');
|
|
136
|
+
if (fs.existsSync(legacyPackageJsonPath)) {
|
|
137
|
+
return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Prepares the current install root for a clean re-install of the target version.
|
|
145
|
+
* Returns the install directory to run `bun install` in.
|
|
146
|
+
*/
|
|
147
|
+
export function preparePackageUpdate(
|
|
148
|
+
version: string,
|
|
149
|
+
packageName: string = PACKAGE_NAME,
|
|
150
|
+
runtimePackageJsonPath: string | null = getCurrentRuntimePackageJsonPath(),
|
|
151
|
+
): string | null {
|
|
152
|
+
try {
|
|
153
|
+
const installContext = resolveInstallContext(runtimePackageJsonPath);
|
|
154
|
+
if (!installContext) {
|
|
155
|
+
log('[auto-update-checker] No install context found for auto-update');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const dependencyReady = ensureDependencyVersion(
|
|
160
|
+
installContext.packageJsonPath,
|
|
161
|
+
packageName,
|
|
162
|
+
version,
|
|
163
|
+
);
|
|
164
|
+
if (!dependencyReady) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const packageRemoved = removeInstalledPackage(
|
|
169
|
+
installContext.installDir,
|
|
170
|
+
packageName,
|
|
171
|
+
);
|
|
172
|
+
const lockRemoved = removeFromBunLock(
|
|
173
|
+
installContext.installDir,
|
|
174
|
+
packageName,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!packageRemoved && !lockRemoved) {
|
|
178
|
+
log(
|
|
179
|
+
`[auto-update-checker] No cached package artifacts removed for ${packageName}; continuing with updated dependency spec`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return installContext.installDir;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
log('[auto-update-checker] Failed to prepare package update:', err);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, mock, spyOn, test } from 'bun:test';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Mock logger to avoid noise
|
|
5
|
+
mock.module('../../utils/logger', () => ({
|
|
6
|
+
log: mock(() => {}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
mock.module('../../cli/config-manager', () => ({
|
|
10
|
+
stripJsonComments: (s: string) => s,
|
|
11
|
+
getOpenCodeConfigPaths: () => [
|
|
12
|
+
'/mock/config/opencode.json',
|
|
13
|
+
'/mock/config/opencode.jsonc',
|
|
14
|
+
],
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Cache buster for dynamic imports
|
|
18
|
+
let importCounter = 0;
|
|
19
|
+
|
|
20
|
+
describe('auto-update-checker/checker', () => {
|
|
21
|
+
describe('extractChannel', () => {
|
|
22
|
+
test('returns latest for null or empty', async () => {
|
|
23
|
+
const { extractChannel } = await import(
|
|
24
|
+
`./checker?test=${importCounter++}`
|
|
25
|
+
);
|
|
26
|
+
expect(extractChannel(null)).toBe('latest');
|
|
27
|
+
expect(extractChannel('')).toBe('latest');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('returns tag if version starts with non-digit', async () => {
|
|
31
|
+
const { extractChannel } = await import(
|
|
32
|
+
`./checker?test=${importCounter++}`
|
|
33
|
+
);
|
|
34
|
+
expect(extractChannel('beta')).toBe('beta');
|
|
35
|
+
expect(extractChannel('next')).toBe('next');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('extracts channel from prerelease version', async () => {
|
|
39
|
+
const { extractChannel } = await import(
|
|
40
|
+
`./checker?test=${importCounter++}`
|
|
41
|
+
);
|
|
42
|
+
expect(extractChannel('1.0.0-alpha.1')).toBe('alpha');
|
|
43
|
+
expect(extractChannel('2.3.4-beta.5')).toBe('beta');
|
|
44
|
+
expect(extractChannel('0.1.0-rc.1')).toBe('rc');
|
|
45
|
+
expect(extractChannel('1.0.0-canary.0')).toBe('canary');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('returns latest for standard versions', async () => {
|
|
49
|
+
const { extractChannel } = await import(
|
|
50
|
+
`./checker?test=${importCounter++}`
|
|
51
|
+
);
|
|
52
|
+
expect(extractChannel('1.0.0')).toBe('latest');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getLocalDevVersion', () => {
|
|
57
|
+
test('returns null if no local dev path in config', async () => {
|
|
58
|
+
const existsSpy = spyOn(fs, 'existsSync').mockReturnValue(false);
|
|
59
|
+
const { getLocalDevVersion } = await import(
|
|
60
|
+
`./checker?test=${importCounter++}`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(getLocalDevVersion('/test')).toBeNull();
|
|
64
|
+
|
|
65
|
+
existsSpy.mockRestore();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('returns version from local package.json if path exists', async () => {
|
|
69
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
70
|
+
(p: string) => {
|
|
71
|
+
if (p.includes('opencode.json')) return true;
|
|
72
|
+
if (p.includes('package.json')) return true;
|
|
73
|
+
return false;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
const statSpy = spyOn(fs, 'statSync').mockImplementation(
|
|
77
|
+
() =>
|
|
78
|
+
({
|
|
79
|
+
isDirectory: () => true,
|
|
80
|
+
}) as unknown as fs.Stats,
|
|
81
|
+
);
|
|
82
|
+
const readSpy = spyOn(fs, 'readFileSync').mockImplementation(
|
|
83
|
+
(p: string) => {
|
|
84
|
+
if (p.includes('opencode.json')) {
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
plugin: ['file:///dev/opencode-dux'],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (p.includes('package.json')) {
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
name: 'opencode-dux',
|
|
92
|
+
version: '1.2.3-dev',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const { getLocalDevVersion } = await import(
|
|
100
|
+
`./checker?test=${importCounter++}`
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(getLocalDevVersion('/test')).toBe('1.2.3-dev');
|
|
104
|
+
|
|
105
|
+
existsSpy.mockRestore();
|
|
106
|
+
statSpy.mockRestore();
|
|
107
|
+
readSpy.mockRestore();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('findPluginEntry', () => {
|
|
112
|
+
test('detects latest version entry', async () => {
|
|
113
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
114
|
+
(p: string) => p.includes('opencode.json'),
|
|
115
|
+
);
|
|
116
|
+
const readSpy = spyOn(fs, 'readFileSync').mockReturnValue(
|
|
117
|
+
JSON.stringify({
|
|
118
|
+
plugin: ['opencode-dux'],
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const { findPluginEntry } = await import(
|
|
123
|
+
`./checker?test=${importCounter++}`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const entry = findPluginEntry('/test');
|
|
127
|
+
expect(entry).not.toBeNull();
|
|
128
|
+
expect(entry?.entry).toBe('opencode-dux');
|
|
129
|
+
expect(entry?.isPinned).toBe(false);
|
|
130
|
+
expect(entry?.pinnedVersion).toBeNull();
|
|
131
|
+
|
|
132
|
+
existsSpy.mockRestore();
|
|
133
|
+
readSpy.mockRestore();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('detects pinned version entry', async () => {
|
|
137
|
+
const existsSpy = spyOn(fs, 'existsSync').mockImplementation(
|
|
138
|
+
(p: string) => p.includes('opencode.json'),
|
|
139
|
+
);
|
|
140
|
+
const readSpy = spyOn(fs, 'readFileSync').mockReturnValue(
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
plugin: ['opencode-dux@1.0.0'],
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const { findPluginEntry } = await import(
|
|
147
|
+
`./checker?test=${importCounter++}`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const entry = findPluginEntry('/test');
|
|
151
|
+
expect(entry).not.toBeNull();
|
|
152
|
+
expect(entry?.isPinned).toBe(true);
|
|
153
|
+
expect(entry?.pinnedVersion).toBe('1.0.0');
|
|
154
|
+
|
|
155
|
+
existsSpy.mockRestore();
|
|
156
|
+
readSpy.mockRestore();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|