agent-gear 2.4.0 → 2.5.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.
Files changed (93) hide show
  1. package/AGENTS.md +64 -5
  2. package/dist/cloud-host/supervisor.d.ts +92 -0
  3. package/dist/cloud-host/supervisor.d.ts.map +1 -0
  4. package/dist/cloud-host/supervisor.js +216 -0
  5. package/dist/cloud-host/supervisor.js.map +1 -0
  6. package/dist/cloud-host/supervisor.test.d.ts +2 -0
  7. package/dist/cloud-host/supervisor.test.d.ts.map +1 -0
  8. package/dist/cloud-host/supervisor.test.js +296 -0
  9. package/dist/cloud-host/supervisor.test.js.map +1 -0
  10. package/dist/commands/harness.d.ts +17 -0
  11. package/dist/commands/harness.d.ts.map +1 -0
  12. package/dist/commands/harness.js +35 -0
  13. package/dist/commands/harness.js.map +1 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +10 -3
  16. package/dist/config.js.map +1 -1
  17. package/dist/harness/env.d.ts +36 -0
  18. package/dist/harness/env.d.ts.map +1 -0
  19. package/dist/harness/env.js +78 -0
  20. package/dist/harness/env.js.map +1 -0
  21. package/dist/harness/env.test.d.ts +2 -0
  22. package/dist/harness/env.test.d.ts.map +1 -0
  23. package/dist/harness/env.test.js +70 -0
  24. package/dist/harness/env.test.js.map +1 -0
  25. package/dist/harness/opencode-cli.d.ts +24 -0
  26. package/dist/harness/opencode-cli.d.ts.map +1 -0
  27. package/dist/harness/opencode-cli.js +74 -0
  28. package/dist/harness/opencode-cli.js.map +1 -0
  29. package/dist/harness/opencode-cli.test.d.ts +2 -0
  30. package/dist/harness/opencode-cli.test.d.ts.map +1 -0
  31. package/dist/harness/opencode-cli.test.js +62 -0
  32. package/dist/harness/opencode-cli.test.js.map +1 -0
  33. package/dist/harness/opencode-config.atomic-failure.test.d.ts +2 -0
  34. package/dist/harness/opencode-config.atomic-failure.test.d.ts.map +1 -0
  35. package/dist/harness/opencode-config.atomic-failure.test.js +35 -0
  36. package/dist/harness/opencode-config.atomic-failure.test.js.map +1 -0
  37. package/dist/harness/opencode-config.d.ts +54 -0
  38. package/dist/harness/opencode-config.d.ts.map +1 -0
  39. package/dist/harness/opencode-config.js +130 -0
  40. package/dist/harness/opencode-config.js.map +1 -0
  41. package/dist/harness/opencode-config.test.d.ts +2 -0
  42. package/dist/harness/opencode-config.test.d.ts.map +1 -0
  43. package/dist/harness/opencode-config.test.js +183 -0
  44. package/dist/harness/opencode-config.test.js.map +1 -0
  45. package/dist/harness/prepare.d.ts +34 -0
  46. package/dist/harness/prepare.d.ts.map +1 -0
  47. package/dist/harness/prepare.js +46 -0
  48. package/dist/harness/prepare.js.map +1 -0
  49. package/dist/harness/prepare.test.d.ts +2 -0
  50. package/dist/harness/prepare.test.d.ts.map +1 -0
  51. package/dist/harness/prepare.test.js +96 -0
  52. package/dist/harness/prepare.test.js.map +1 -0
  53. package/dist/heartbeat.d.ts.map +1 -1
  54. package/dist/heartbeat.js +2 -0
  55. package/dist/heartbeat.js.map +1 -1
  56. package/dist/index.js +21 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/socketio-client.d.ts +62 -3
  59. package/dist/socketio-client.d.ts.map +1 -1
  60. package/dist/socketio-client.js +155 -16
  61. package/dist/socketio-client.js.map +1 -1
  62. package/dist/socketio-client.test.d.ts +2 -0
  63. package/dist/socketio-client.test.d.ts.map +1 -0
  64. package/dist/socketio-client.test.js +132 -0
  65. package/dist/socketio-client.test.js.map +1 -0
  66. package/dist/workload-executor.d.ts +24 -1
  67. package/dist/workload-executor.d.ts.map +1 -1
  68. package/dist/workload-executor.js +78 -16
  69. package/dist/workload-executor.js.map +1 -1
  70. package/dist/workload-executor.test.js +99 -1
  71. package/dist/workload-executor.test.js.map +1 -1
  72. package/package.json +2 -1
  73. package/src/cloud-host/supervisor.test.ts +367 -0
  74. package/src/cloud-host/supervisor.ts +289 -0
  75. package/src/commands/harness.ts +44 -0
  76. package/src/config.ts +9 -3
  77. package/src/harness/env.test.ts +79 -0
  78. package/src/harness/env.ts +89 -0
  79. package/src/harness/opencode-cli.test.ts +82 -0
  80. package/src/harness/opencode-cli.ts +99 -0
  81. package/src/harness/opencode-config.atomic-failure.test.ts +51 -0
  82. package/src/harness/opencode-config.test.ts +238 -0
  83. package/src/harness/opencode-config.ts +176 -0
  84. package/src/harness/prepare.test.ts +117 -0
  85. package/src/harness/prepare.ts +69 -0
  86. package/src/heartbeat.ts +2 -0
  87. package/src/index.ts +22 -0
  88. package/src/socketio-client.test.ts +155 -0
  89. package/src/socketio-client.ts +204 -18
  90. package/src/workload-executor.test.ts +120 -0
  91. package/src/workload-executor.ts +100 -17
  92. package/tests/commands/harness.test.ts +90 -0
  93. package/tests/socketio-client.test.ts +140 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.test.js","sourceRoot":"","sources":["../../src/harness/env.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /** 一次外部命令执行结果(stdout/stderr 均为完整文本)。 */
2
+ export interface CommandResult {
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ /** 可注入的命令执行器(测试使用;生产走 `execFile`)。 */
7
+ export type CommandRunner = (command: string, args: string[]) => Promise<CommandResult>;
8
+ /** `opencode --version` 硬校验失败。 */
9
+ export declare class OpencodeCliError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ /** 从版本输出中取第一行非空文本(去掉前后空白与可能的 `v` 前缀)。 */
13
+ export declare function extractVersion(stdout: string, stderr?: string): string | undefined;
14
+ /**
15
+ * `opencode --version` 硬校验(`opencode -v` 仅作旧版 CLI 的兼容探测)。
16
+ *
17
+ * `expectedVersion` 非空时要求实际版本严格一致(去掉 `v` 前缀后比较),不一致立即失败,
18
+ * 用于 init 脚本已固定安装版本的二次校验(C-5)。
19
+ *
20
+ * 失败时抛 {@link OpencodeCliError},消息包含「版本校验」提示与命令级错误摘要
21
+ * (不包含任何 env/secret)。调用方(init 脚本 / `harness prepare`)必须以非零退出。
22
+ */
23
+ export declare function verifyOpencodeCli(runner?: CommandRunner, expectedVersion?: string): Promise<string>;
24
+ //# sourceMappingURL=opencode-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-cli.d.ts","sourceRoot":"","sources":["../../src/harness/opencode-cli.ts"],"names":[],"mappings":"AAEA,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sCAAsC;AACtC,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAExF,kCAAkC;AAClC,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAoBD,yCAAyC;AACzC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,GAAG,SAAS,CAO9E;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,GAAE,aAA6B,EACrC,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,CAAC,CAuCjB"}
@@ -0,0 +1,74 @@
1
+ import { execFile } from 'node:child_process';
2
+ /** `opencode --version` 硬校验失败。 */
3
+ export class OpencodeCliError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'OpencodeCliError';
7
+ }
8
+ }
9
+ const DEFAULT_TIMEOUT_MS = 15_000;
10
+ const defaultRunner = (command, args) => new Promise((resolve, reject) => {
11
+ execFile(command, args, { timeout: DEFAULT_TIMEOUT_MS, maxBuffer: 1024 * 1024, encoding: 'utf8' }, (error, stdout, stderr) => {
12
+ if (error) {
13
+ reject(error);
14
+ return;
15
+ }
16
+ resolve({ stdout: stdout ?? '', stderr: stderr ?? '' });
17
+ });
18
+ });
19
+ /** 从版本输出中取第一行非空文本(去掉前后空白与可能的 `v` 前缀)。 */
20
+ export function extractVersion(stdout, stderr = '') {
21
+ const line = `${stdout}\n${stderr}`
22
+ .split(/\r?\n/)
23
+ .map((value) => value.trim())
24
+ .find((value) => value.length > 0);
25
+ if (!line)
26
+ return undefined;
27
+ return line.replace(/^v/i, '');
28
+ }
29
+ /**
30
+ * `opencode --version` 硬校验(`opencode -v` 仅作旧版 CLI 的兼容探测)。
31
+ *
32
+ * `expectedVersion` 非空时要求实际版本严格一致(去掉 `v` 前缀后比较),不一致立即失败,
33
+ * 用于 init 脚本已固定安装版本的二次校验(C-5)。
34
+ *
35
+ * 失败时抛 {@link OpencodeCliError},消息包含「版本校验」提示与命令级错误摘要
36
+ * (不包含任何 env/secret)。调用方(init 脚本 / `harness prepare`)必须以非零退出。
37
+ */
38
+ export async function verifyOpencodeCli(runner = defaultRunner, expectedVersion) {
39
+ const failures = [];
40
+ const normalizeVersion = (version) => (version.trim().split(/\s+/).pop() ?? '').replace(/^v/i, '');
41
+ const ensureExpected = (version) => {
42
+ if (expectedVersion && normalizeVersion(version) !== normalizeVersion(expectedVersion)) {
43
+ throw new OpencodeCliError(`OpenCode CLI version mismatch: expected ${expectedVersion}, got ${version}`);
44
+ }
45
+ return version;
46
+ };
47
+ try {
48
+ const result = await runner('opencode', ['--version']);
49
+ const version = extractVersion(result.stdout, result.stderr);
50
+ if (version)
51
+ return ensureExpected(version);
52
+ failures.push('`opencode --version` returned no version output');
53
+ }
54
+ catch (error) {
55
+ if (error instanceof OpencodeCliError)
56
+ throw error;
57
+ failures.push(`\`opencode --version\` failed: ${error.message}`);
58
+ }
59
+ // 兼容旧版 CLI:仅当 `--version` 不可用时探测 `-v`。
60
+ try {
61
+ const result = await runner('opencode', ['-v']);
62
+ const version = extractVersion(result.stdout, result.stderr);
63
+ if (version)
64
+ return ensureExpected(version);
65
+ failures.push('`opencode -v` returned no version output');
66
+ }
67
+ catch (error) {
68
+ if (error instanceof OpencodeCliError)
69
+ throw error;
70
+ failures.push(`\`opencode -v\` failed: ${error.message}`);
71
+ }
72
+ throw new OpencodeCliError(`OpenCode CLI version check failed; OpenCode is required by the harness workload (${failures.join('; ')})`);
73
+ }
74
+ //# sourceMappingURL=opencode-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-cli.js","sourceRoot":"","sources":["../../src/harness/opencode-cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=opencode-cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-cli.test.d.ts","sourceRoot":"","sources":["../../src/harness/opencode-cli.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { OpencodeCliError, extractVersion, verifyOpencodeCli, } from './opencode-cli.js';
3
+ describe('verifyOpencodeCli (T-AG8)', () => {
4
+ it('runs `opencode --version` and returns the version', async () => {
5
+ const runner = vi.fn(async () => ({ stdout: '1.2.3\n', stderr: '' }));
6
+ await expect(verifyOpencodeCli(runner)).resolves.toBe('1.2.3');
7
+ expect(runner).toHaveBeenCalledWith('opencode', ['--version']);
8
+ expect(runner).toHaveBeenCalledTimes(1);
9
+ });
10
+ it('falls back to `opencode -v` for legacy CLIs', async () => {
11
+ const runner = vi.fn(async (_command, args) => {
12
+ if (args[0] === '--version')
13
+ throw new Error('unknown option');
14
+ return { stdout: 'v0.9.1\n', stderr: '' };
15
+ });
16
+ await expect(verifyOpencodeCli(runner)).resolves.toBe('0.9.1');
17
+ expect(runner).toHaveBeenNthCalledWith(1, 'opencode', ['--version']);
18
+ expect(runner).toHaveBeenNthCalledWith(2, 'opencode', ['-v']);
19
+ });
20
+ it('falls back to `-v` when `--version` prints no version', async () => {
21
+ const runner = vi.fn(async (_command, args) => args[0] === '--version'
22
+ ? { stdout: ' \n', stderr: '' }
23
+ : { stdout: '2.0.0\n', stderr: '' });
24
+ await expect(verifyOpencodeCli(runner)).resolves.toBe('2.0.0');
25
+ });
26
+ it('throws a version-check error when both commands fail', async () => {
27
+ const runner = vi.fn(async () => {
28
+ throw new Error('spawn opencode ENOENT');
29
+ });
30
+ let error;
31
+ try {
32
+ await verifyOpencodeCli(runner);
33
+ }
34
+ catch (caught) {
35
+ error = caught;
36
+ }
37
+ expect(error).toBeInstanceOf(OpencodeCliError);
38
+ const message = error.message;
39
+ expect(message).toContain('version check failed');
40
+ expect(message).toContain('opencode --version');
41
+ expect(message).toContain('opencode -v');
42
+ expect(message).toContain('ENOENT');
43
+ });
44
+ it('C-5: validates the expected pinned version (tolerates v prefix and command output prefix)', async () => {
45
+ const runner = vi.fn(async () => ({ stdout: 'v2.1.1\n', stderr: '' }));
46
+ await expect(verifyOpencodeCli(runner, '2.1.1')).resolves.toBe('2.1.1');
47
+ const prefixed = vi.fn(async () => ({ stdout: 'opencode 2.1.1\n', stderr: '' }));
48
+ await expect(verifyOpencodeCli(prefixed, '2.1.1')).resolves.toBe('opencode 2.1.1');
49
+ });
50
+ it('C-5: throws immediately on a pinned version mismatch', async () => {
51
+ const runner = vi.fn(async () => ({ stdout: '2.0.0\n', stderr: '' }));
52
+ await expect(verifyOpencodeCli(runner, '2.1.1')).rejects.toThrow(/version mismatch: expected 2\.1\.1, got 2\.0\.0/);
53
+ // 不匹配即失败,不再退回 `-v`
54
+ expect(runner).toHaveBeenCalledTimes(1);
55
+ });
56
+ it('reads the version from stderr when stdout is empty', () => {
57
+ expect(extractVersion('', 'opencode 1.0.0\n')).toBe('opencode 1.0.0');
58
+ expect(extractVersion('\n\n3.2.1\n')).toBe('3.2.1');
59
+ expect(extractVersion(' ')).toBeUndefined();
60
+ });
61
+ });
62
+ //# sourceMappingURL=opencode-cli.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-cli.test.js","sourceRoot":"","sources":["../../src/harness/opencode-cli.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=opencode-config.atomic-failure.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.atomic-failure.test.d.ts","sourceRoot":"","sources":["../../src/harness/opencode-config.atomic-failure.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ const fs = vi.hoisted(() => ({
3
+ chmod: vi.fn(),
4
+ mkdir: vi.fn(),
5
+ readFile: vi.fn(),
6
+ rename: vi.fn(),
7
+ rm: vi.fn(),
8
+ stat: vi.fn(),
9
+ writeFile: vi.fn(),
10
+ }));
11
+ vi.mock('node:fs/promises', () => fs);
12
+ import { OpencodeConfigError, writeOpencodeConfig } from './opencode-config.js';
13
+ const ENV = {
14
+ modelBaseUrl: 'http://admin-api:8002/v1',
15
+ modelApiKey: 'zag_sk_super_secret',
16
+ modelIds: ['model-a'],
17
+ };
18
+ describe('R2-16 opencode config atomic write failure cleanup', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ fs.mkdir.mockResolvedValue(undefined);
22
+ fs.chmod.mockResolvedValue(undefined);
23
+ fs.writeFile.mockResolvedValue(undefined);
24
+ fs.rm.mockResolvedValue(undefined);
25
+ // 目标文件不存在 → 走新建分支
26
+ fs.stat.mockRejectedValue(new Error('ENOENT'));
27
+ });
28
+ it('removes the temp file when rename fails and preserves the original error surface', async () => {
29
+ fs.rename.mockRejectedValue(new Error('rename failed'));
30
+ await expect(writeOpencodeConfig({ env: ENV, configDir: '/tmp/opencode-r2-16' })).rejects.toBeInstanceOf(OpencodeConfigError);
31
+ expect(fs.writeFile).toHaveBeenCalledWith(expect.stringMatching(/\/tmp\/opencode-r2-16\/\.opencode-config\..*\.tmp$/), expect.any(String), { encoding: 'utf-8', mode: 0o600 });
32
+ expect(fs.rm).toHaveBeenCalledWith(expect.stringMatching(/\/tmp\/opencode-r2-16\/\.opencode-config\..*\.tmp$/), { force: true });
33
+ });
34
+ });
35
+ //# sourceMappingURL=opencode-config.atomic-failure.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.atomic-failure.test.js","sourceRoot":"","sources":["../../src/harness/opencode-config.atomic-failure.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE9D,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3B,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE;IACd,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE;IACd,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE;IACf,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE;IACX,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;IACb,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE;CACnB,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;AAEtC,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,MAAM,GAAG,GAAG;IACV,YAAY,EAAE,0BAA0B;IACxC,WAAW,EAAE,qBAAqB;IAClC,QAAQ,EAAE,CAAC,SAAS,CAAC;CACtB,CAAC;AAEF,QAAQ,CAAC,oDAAoD,EAAE,GAAG,EAAE;IAClE,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACtC,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACtC,EAAE,CAAC,SAAS,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAC1C,EAAE,CAAC,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACnC,kBAAkB;QAClB,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;QAChG,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;QAExD,MAAM,MAAM,CACV,mBAAmB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,qBAAqB,EAAE,CAAC,CACpE,CAAC,OAAO,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC;QAE9C,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,oBAAoB,CACvC,MAAM,CAAC,cAAc,CAAC,oDAAoD,CAAC,EAC3E,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAClB,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CACnC,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,oBAAoB,CAChC,MAAM,CAAC,cAAc,CAAC,oDAAoD,CAAC,EAC3E,EAAE,KAAK,EAAE,IAAI,EAAE,CAChB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { HarnessEnv } from './env.js';
2
+ /** OpenCode 配置中 `provider.zag` 的结构(OpenAI-compatible 自定义 provider)。 */
3
+ export interface ZagProviderConfig {
4
+ npm: string;
5
+ name: string;
6
+ options: {
7
+ baseURL: string;
8
+ apiKey: string;
9
+ };
10
+ models: Record<string, {
11
+ name: string;
12
+ }>;
13
+ }
14
+ /** 写入 OpenCode 配置失败(消息只含路径/变量名,不含 key 等 secret)。 */
15
+ export declare class OpencodeConfigError extends Error {
16
+ constructor(message: string);
17
+ }
18
+ /** `provider.zag` 固定使用 OpenAI compatible provider 包。 */
19
+ export declare const ZAG_PROVIDER_NPM = "@ai-sdk/openai-compatible";
20
+ /** 由 harness env 构造 `provider.zag`(模型列表为空时 `models: {}`)。 */
21
+ export declare function buildZagProvider(env: Pick<HarnessEnv, 'modelBaseUrl' | 'modelApiKey' | 'modelIds'>): ZagProviderConfig;
22
+ /** 默认配置目录:`~/.config/opencode`。 */
23
+ export declare function defaultConfigDir(): string;
24
+ /**
25
+ * 选择配置目标文件(A13):
26
+ *
27
+ * 1. 同目录存在 `opencode.jsonc` → 用它;
28
+ * 2. 否则同目录只存在 `opencode.json` → 合并该文件(不另建 `.jsonc`);
29
+ * 3. 都不存在 → 新建 `opencode.jsonc`。
30
+ */
31
+ export declare function resolveOpencodeConfigPath(configDir?: string): Promise<string>;
32
+ export interface WriteOpencodeConfigOptions {
33
+ env: Pick<HarnessEnv, 'modelBaseUrl' | 'modelApiKey' | 'modelIds'>;
34
+ /** 覆盖配置目录(测试/定制场景;缺省 `~/.config/opencode`)。 */
35
+ configDir?: string;
36
+ }
37
+ export interface WriteOpencodeConfigResult {
38
+ /** 实际写入的配置文件路径。 */
39
+ path: string;
40
+ /** 是否为新建文件。 */
41
+ created: boolean;
42
+ /** 写入的模型数量。 */
43
+ modelCount: number;
44
+ }
45
+ /**
46
+ * 写入/合并 OpenCode 配置:
47
+ *
48
+ * - 用 `jsonc-parser` 的 `modify` 仅替换 `provider.zag`,保留注释与既有 provider;
49
+ * - 文件权限固定 `0600`(含已存在文件),目录权限收紧为 `0700`(含已存在目录);
50
+ * - 同目录临时文件 + `rename` 原子替换(避免半写文件,C-7);
51
+ * - 非法 JSONC 直接报错(不覆盖用户配置,避免静默丢数据)。
52
+ */
53
+ export declare function writeOpencodeConfig(options: WriteOpencodeConfigOptions): Promise<WriteOpencodeConfigResult>;
54
+ //# sourceMappingURL=opencode-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.d.ts","sourceRoot":"","sources":["../../src/harness/opencode-config.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED,oDAAoD;AACpD,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAE5D,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,cAAc,GAAG,aAAa,GAAG,UAAU,CAAC,GACjE,iBAAiB,CAenB;AAED,mCAAmC;AACnC,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAWD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,SAAS,SAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAM/F;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,cAAc,GAAG,aAAa,GAAG,UAAU,CAAC,CAAC;IACnE,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,yBAAyB,CAAC,CAiEpC"}
@@ -0,0 +1,130 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { applyEdits, modify, parse, printParseErrorCode, } from 'jsonc-parser';
6
+ /** 写入 OpenCode 配置失败(消息只含路径/变量名,不含 key 等 secret)。 */
7
+ export class OpencodeConfigError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'OpencodeConfigError';
11
+ }
12
+ }
13
+ /** `provider.zag` 固定使用 OpenAI compatible provider 包。 */
14
+ export const ZAG_PROVIDER_NPM = '@ai-sdk/openai-compatible';
15
+ /** 由 harness env 构造 `provider.zag`(模型列表为空时 `models: {}`)。 */
16
+ export function buildZagProvider(env) {
17
+ const models = {};
18
+ for (const id of env.modelIds) {
19
+ if (!id)
20
+ continue;
21
+ models[id] = { name: id };
22
+ }
23
+ return {
24
+ npm: ZAG_PROVIDER_NPM,
25
+ name: 'ZAG',
26
+ options: {
27
+ baseURL: env.modelBaseUrl,
28
+ apiKey: env.modelApiKey,
29
+ },
30
+ models,
31
+ };
32
+ }
33
+ /** 默认配置目录:`~/.config/opencode`。 */
34
+ export function defaultConfigDir() {
35
+ return join(homedir(), '.config', 'opencode');
36
+ }
37
+ async function exists(path) {
38
+ try {
39
+ await stat(path);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * 选择配置目标文件(A13):
48
+ *
49
+ * 1. 同目录存在 `opencode.jsonc` → 用它;
50
+ * 2. 否则同目录只存在 `opencode.json` → 合并该文件(不另建 `.jsonc`);
51
+ * 3. 都不存在 → 新建 `opencode.jsonc`。
52
+ */
53
+ export async function resolveOpencodeConfigPath(configDir = defaultConfigDir()) {
54
+ const jsoncPath = join(configDir, 'opencode.jsonc');
55
+ if (await exists(jsoncPath))
56
+ return jsoncPath;
57
+ const jsonPath = join(configDir, 'opencode.json');
58
+ if (await exists(jsonPath))
59
+ return jsonPath;
60
+ return jsoncPath;
61
+ }
62
+ /**
63
+ * 写入/合并 OpenCode 配置:
64
+ *
65
+ * - 用 `jsonc-parser` 的 `modify` 仅替换 `provider.zag`,保留注释与既有 provider;
66
+ * - 文件权限固定 `0600`(含已存在文件),目录权限收紧为 `0700`(含已存在目录);
67
+ * - 同目录临时文件 + `rename` 原子替换(避免半写文件,C-7);
68
+ * - 非法 JSONC 直接报错(不覆盖用户配置,避免静默丢数据)。
69
+ */
70
+ export async function writeOpencodeConfig(options) {
71
+ const dir = options.configDir ?? defaultConfigDir();
72
+ await mkdir(dir, { recursive: true, mode: 0o700 });
73
+ // `mkdir` 的 mode 只对新建目录生效:已存在目录显式收紧到 0700(可能含模型 Key)。
74
+ try {
75
+ await chmod(dir, 0o700);
76
+ }
77
+ catch (error) {
78
+ throw new OpencodeConfigError(`cannot set permissions on OpenCode config directory ${dir}: ${error.message}`);
79
+ }
80
+ const target = await resolveOpencodeConfigPath(dir);
81
+ const created = !(await exists(target));
82
+ let text = '';
83
+ if (!created) {
84
+ try {
85
+ text = await readFile(target, 'utf-8');
86
+ }
87
+ catch (error) {
88
+ throw new OpencodeConfigError(`cannot read OpenCode config at ${target}: ${error.message}`);
89
+ }
90
+ }
91
+ if (text.trim().length === 0) {
92
+ text = '{}\n';
93
+ }
94
+ const parseErrors = [];
95
+ const parsed = parse(text, parseErrors, { allowTrailingComma: true });
96
+ if (parseErrors.length > 0) {
97
+ const first = parseErrors[0];
98
+ throw new OpencodeConfigError(`cannot parse OpenCode config at ${target}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
99
+ }
100
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
101
+ throw new OpencodeConfigError(`invalid OpenCode config at ${target}: expected a JSON object`);
102
+ }
103
+ const formattingOptions = {
104
+ insertSpaces: true,
105
+ tabSize: 2,
106
+ eol: text.includes('\r\n') ? '\r\n' : '\n',
107
+ };
108
+ const provider = buildZagProvider(options.env);
109
+ const edits = modify(text, ['provider', 'zag'], provider, { formattingOptions });
110
+ let content = applyEdits(text, edits);
111
+ if (!content.endsWith('\n'))
112
+ content += '\n';
113
+ const tempPath = join(dir, `.opencode-config.${process.pid}.${randomUUID()}.tmp`);
114
+ try {
115
+ // 先写同目录临时文件(0600)再 rename:失败时原配置保持完整,不产生半写文件。
116
+ await writeFile(tempPath, content, { encoding: 'utf-8', mode: 0o600 });
117
+ await chmod(tempPath, 0o600);
118
+ await rename(tempPath, target);
119
+ }
120
+ catch (error) {
121
+ await rm(tempPath, { force: true }).catch(() => { });
122
+ throw new OpencodeConfigError(`cannot write OpenCode config at ${target}: ${error.message}`);
123
+ }
124
+ return {
125
+ path: target,
126
+ created,
127
+ modelCount: Object.keys(provider.models).length,
128
+ };
129
+ }
130
+ //# sourceMappingURL=opencode-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.js","sourceRoot":"","sources":["../../src/harness/opencode-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=opencode-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.test.d.ts","sourceRoot":"","sources":["../../src/harness/opencode-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { parse } from 'jsonc-parser';
5
+ import { OpencodeConfigError, ZAG_PROVIDER_NPM, buildZagProvider, resolveOpencodeConfigPath, writeOpencodeConfig, } from './opencode-config.js';
6
+ const TMP_BASE = '/tmp/opencode';
7
+ const ENV = {
8
+ modelBaseUrl: 'http://admin-api:8002/v1',
9
+ modelApiKey: 'zag_sk_super_secret',
10
+ modelIds: ['model-a', 'model-b'],
11
+ };
12
+ async function makeDir() {
13
+ await mkdir(TMP_BASE, { recursive: true });
14
+ return mkdtemp(join(TMP_BASE, 'opencode-config-'));
15
+ }
16
+ async function fileMode(path) {
17
+ return (await stat(path)).mode & 0o777;
18
+ }
19
+ describe('buildZagProvider (T-AG9/T-AG11)', () => {
20
+ it('builds an OpenAI-compatible provider with baseURL/apiKey/models', () => {
21
+ expect(buildZagProvider(ENV)).toEqual({
22
+ npm: '@ai-sdk/openai-compatible',
23
+ name: 'ZAG',
24
+ options: {
25
+ baseURL: 'http://admin-api:8002/v1',
26
+ apiKey: 'zag_sk_super_secret',
27
+ },
28
+ models: {
29
+ 'model-a': { name: 'model-a' },
30
+ 'model-b': { name: 'model-b' },
31
+ },
32
+ });
33
+ expect(ZAG_PROVIDER_NPM).toBe('@ai-sdk/openai-compatible');
34
+ });
35
+ it('writes models:{} for an empty model list', () => {
36
+ expect(buildZagProvider({ ...ENV, modelIds: [] }).models).toEqual({});
37
+ });
38
+ });
39
+ describe('writeOpencodeConfig (T-AG9..T-AG12)', () => {
40
+ const dirs = [];
41
+ afterEach(async () => {
42
+ await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
43
+ });
44
+ it('T-AG9: creates opencode.jsonc with provider.zag and 0600 permissions', async () => {
45
+ const dir = await makeDir();
46
+ dirs.push(dir);
47
+ const result = await writeOpencodeConfig({ env: ENV, configDir: dir });
48
+ expect(result.path).toBe(join(dir, 'opencode.jsonc'));
49
+ expect(result.created).toBe(true);
50
+ expect(result.modelCount).toBe(2);
51
+ expect(await fileMode(result.path)).toBe(0o600);
52
+ const config = parse(await readFile(result.path, 'utf-8'));
53
+ expect(config.provider.zag.options.baseURL).toBe('http://admin-api:8002/v1');
54
+ expect(config.provider.zag.options.apiKey).toBe('zag_sk_super_secret');
55
+ expect(config.provider.zag.models).toEqual({
56
+ 'model-a': { name: 'model-a' },
57
+ 'model-b': { name: 'model-b' },
58
+ });
59
+ });
60
+ it('T-AG10: merges an existing config, preserving comments and other providers and replacing provider.zag', async () => {
61
+ const dir = await makeDir();
62
+ dirs.push(dir);
63
+ const target = join(dir, 'opencode.jsonc');
64
+ await writeFile(target, `{
65
+ // keep this comment
66
+ "$schema": "https://opencode.ai/config.json",
67
+ "provider": {
68
+ // existing provider must survive
69
+ "other": {
70
+ "npm": "@example/other",
71
+ "name": "Other",
72
+ "options": { "baseURL": "http://other" },
73
+ "models": { "x": { "name": "x" } }
74
+ },
75
+ "zag": {
76
+ "npm": "old-npm",
77
+ "name": "Old ZAG",
78
+ "options": { "baseURL": "http://old", "apiKey": "old-key" },
79
+ "models": {}
80
+ }
81
+ }
82
+ }
83
+ `, 'utf-8');
84
+ await writeOpencodeConfig({ env: ENV, configDir: dir });
85
+ const text = await readFile(target, 'utf-8');
86
+ expect(text).toContain('// keep this comment');
87
+ expect(text).toContain('// existing provider must survive');
88
+ const config = parse(text);
89
+ expect(config.$schema).toBe('https://opencode.ai/config.json');
90
+ expect(config.provider.other).toMatchObject({ npm: '@example/other', name: 'Other' });
91
+ expect(config.provider.zag).toMatchObject({
92
+ npm: ZAG_PROVIDER_NPM,
93
+ name: 'ZAG',
94
+ options: { baseURL: 'http://admin-api:8002/v1', apiKey: 'zag_sk_super_secret' },
95
+ });
96
+ // 旧 apiKey/baseURL 已被替换
97
+ expect(text).not.toContain('old-key');
98
+ expect(text).not.toContain('http://old');
99
+ });
100
+ it('T-AG11: writes models:{} when ZAG_MODEL_IDS is an empty array', async () => {
101
+ const dir = await makeDir();
102
+ dirs.push(dir);
103
+ await writeOpencodeConfig({ env: { ...ENV, modelIds: [] }, configDir: dir });
104
+ const config = parse(await readFile(join(dir, 'opencode.jsonc'), 'utf-8'));
105
+ expect(config.provider.zag.models).toEqual({});
106
+ });
107
+ it('T-AG12: merges an existing opencode.json instead of creating a new .jsonc', async () => {
108
+ const dir = await makeDir();
109
+ dirs.push(dir);
110
+ const jsonPath = join(dir, 'opencode.json');
111
+ await writeFile(jsonPath, '{\n "theme": "dark"\n}\n', 'utf-8');
112
+ const result = await writeOpencodeConfig({ env: ENV, configDir: dir });
113
+ expect(result.path).toBe(jsonPath);
114
+ expect(result.created).toBe(false);
115
+ expect(await fileMode(jsonPath)).toBe(0o600);
116
+ const config = parse(await readFile(jsonPath, 'utf-8'));
117
+ expect(config.theme).toBe('dark');
118
+ expect(config.provider.zag.name).toBe('ZAG');
119
+ // 不创建并存的 .jsonc
120
+ await expect(stat(join(dir, 'opencode.jsonc'))).rejects.toThrow();
121
+ });
122
+ it('T-AG9b: tightens permissions of an existing config to 0600', async () => {
123
+ const dir = await makeDir();
124
+ dirs.push(dir);
125
+ const target = join(dir, 'opencode.jsonc');
126
+ await writeFile(target, '{}\n', 'utf-8');
127
+ await chmod(target, 0o644);
128
+ await writeOpencodeConfig({ env: ENV, configDir: dir });
129
+ expect(await fileMode(target)).toBe(0o600);
130
+ });
131
+ it('C-7: tightens an existing config directory to 0700', async () => {
132
+ const dir = await makeDir();
133
+ dirs.push(dir);
134
+ await chmod(dir, 0o755);
135
+ await writeOpencodeConfig({ env: ENV, configDir: dir });
136
+ expect(await fileMode(dir)).toBe(0o700);
137
+ });
138
+ it('C-7: writes atomically via a temp file and leaves no partial/temp files', async () => {
139
+ const dir = await makeDir();
140
+ dirs.push(dir);
141
+ const target = join(dir, 'opencode.jsonc');
142
+ await writeFile(target, '{\n "theme": "dark"\n}\n', 'utf-8');
143
+ await writeOpencodeConfig({ env: ENV, configDir: dir });
144
+ // 同目录只应有目标文件(临时文件已 rename 掉)
145
+ expect(await readdir(dir)).toEqual(['opencode.jsonc']);
146
+ expect(await fileMode(target)).toBe(0o600);
147
+ const config = parse(await readFile(target, 'utf-8'));
148
+ expect(config.theme).toBe('dark');
149
+ expect(config.provider.zag.name).toBe('ZAG');
150
+ });
151
+ it('C-7: keeps the original file intact when the write path fails', async () => {
152
+ const dir = await makeDir();
153
+ dirs.push(dir);
154
+ const target = join(dir, 'opencode.jsonc');
155
+ const original = '{\n "theme": "dark"\n}\n';
156
+ await writeFile(target, original, 'utf-8');
157
+ // 目标文件不可读(解析阶段失败):不得产生临时文件,原文件保持原样。
158
+ await chmod(target, 0o200);
159
+ await expect(writeOpencodeConfig({ env: ENV, configDir: dir })).rejects.toBeInstanceOf(OpencodeConfigError);
160
+ expect(await readdir(dir)).toEqual(['opencode.jsonc']);
161
+ await chmod(target, 0o600);
162
+ expect(await readFile(target, 'utf-8')).toBe(original);
163
+ });
164
+ it('rejects invalid JSONC and leaves the file untouched', async () => {
165
+ const dir = await makeDir();
166
+ dirs.push(dir);
167
+ const target = join(dir, 'opencode.jsonc');
168
+ const original = '{ "provider": ';
169
+ await writeFile(target, original, 'utf-8');
170
+ await expect(writeOpencodeConfig({ env: ENV, configDir: dir })).rejects.toBeInstanceOf(OpencodeConfigError);
171
+ expect(await readFile(target, 'utf-8')).toBe(original);
172
+ });
173
+ it('T-AG12b: resolves the target file by precedence', async () => {
174
+ const dir = await makeDir();
175
+ dirs.push(dir);
176
+ expect(await resolveOpencodeConfigPath(dir)).toBe(join(dir, 'opencode.jsonc'));
177
+ await writeFile(join(dir, 'opencode.json'), '{}\n', 'utf-8');
178
+ expect(await resolveOpencodeConfigPath(dir)).toBe(join(dir, 'opencode.json'));
179
+ await writeFile(join(dir, 'opencode.jsonc'), '{}\n', 'utf-8');
180
+ expect(await resolveOpencodeConfigPath(dir)).toBe(join(dir, 'opencode.jsonc'));
181
+ });
182
+ });
183
+ //# sourceMappingURL=opencode-config.test.js.map