w-dispatch-ai 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/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-dispatch-ai.umd.js +7 -0
- package/dist/w-dispatch-ai.umd.js.map +1 -0
- package/docs/WDispatchAi.mjs.html +110 -0
- package/docs/adapters.mjs.html +100 -0
- package/docs/dispatchAi.mjs.html +133 -0
- package/docs/dispatchClaude.mjs.html +190 -0
- package/docs/dispatchCodex.mjs.html +191 -0
- package/docs/dispatchOpencode.mjs.html +260 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/getCliArgs.mjs.html +126 -0
- package/docs/getErrorResult.mjs.html +115 -0
- package/docs/global.html +2924 -0
- package/docs/index.html +84 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/g.mjs +94 -0
- package/package.json +38 -0
- package/script.txt +17 -0
- package/src/WDispatchAi.mjs +38 -0
- package/src/adapters.mjs +28 -0
- package/src/dispatchAi.mjs +61 -0
- package/src/dispatchClaude.mjs +118 -0
- package/src/dispatchCodex.mjs +119 -0
- package/src/dispatchOpencode.mjs +188 -0
- package/src/getCliArgs.mjs +54 -0
- package/src/getErrorResult.mjs +43 -0
- package/test/tools/fakeCliForTest.mjs +139 -0
- package/test/unit-WDispatchAi.test.mjs +79 -0
- package/test/unit-adapters.test.mjs +34 -0
- package/test/unit-dispatchAi.test.mjs +104 -0
- package/test/unit-dispatchClaude.test.mjs +144 -0
- package/test/unit-dispatchCodex.test.mjs +113 -0
- package/test/unit-dispatchOpencode.test.mjs +176 -0
- package/test/unit-getCliArgs.test.mjs +55 -0
- package/test/unit-getErrorResult.test.mjs +45 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +5 -0
- package/toolg/gDistRollup.mjs +24 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import dispatchClaude from '../src/dispatchClaude.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('dispatchClaude', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
|
|
10
|
+
before(function() {
|
|
11
|
+
fake = createFakeCli('fake-claude')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(function() {
|
|
15
|
+
if (fake) {
|
|
16
|
+
fake.clean()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('prompt非有效字串時回傳錯誤結果物件且不reject', async function() {
|
|
21
|
+
let r = []
|
|
22
|
+
for (let prompt of [null, undefined, '', 123, {}, []]) {
|
|
23
|
+
let t = await dispatchClaude(prompt)
|
|
24
|
+
r.push([t.ok, t.error, t.attempts])
|
|
25
|
+
}
|
|
26
|
+
let rr = [
|
|
27
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
28
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
29
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
30
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
31
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
32
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
33
|
+
]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('固定帶-p與--dangerously-skip-permissions旗標', async function() {
|
|
38
|
+
let t = await dispatchClaude('abc', { exe: fake.exe })
|
|
39
|
+
let o = JSON.parse(t.stdout)
|
|
40
|
+
let r = [t.ok, o.args]
|
|
41
|
+
let rr = [true, ['-p', '--dangerously-skip-permissions']]
|
|
42
|
+
assert.strict.deepEqual(r, rr)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('有給model時帶--model旗標', async function() {
|
|
46
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, model: 'sonnet' })
|
|
47
|
+
let o = JSON.parse(t.stdout)
|
|
48
|
+
let r = o.args
|
|
49
|
+
let rr = ['-p', '--dangerously-skip-permissions', '--model', 'sonnet']
|
|
50
|
+
assert.strict.deepEqual(r, rr)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('model非有效字串時整段--model旗標不出現', async function() {
|
|
54
|
+
let r = []
|
|
55
|
+
for (let model of [null, undefined, '', 123, {}]) {
|
|
56
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, model })
|
|
57
|
+
let o = JSON.parse(t.stdout)
|
|
58
|
+
r.push(o.args.includes('--model'))
|
|
59
|
+
}
|
|
60
|
+
let rr = [false, false, false, false, false]
|
|
61
|
+
assert.strict.deepEqual(r, rr)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('skipPermissions為false時不帶--dangerously-skip-permissions旗標', async function() {
|
|
65
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, model: 'sonnet', skipPermissions: false })
|
|
66
|
+
let o = JSON.parse(t.stdout)
|
|
67
|
+
let r = [t.ok, o.args]
|
|
68
|
+
let rr = [true, ['-p', '--model', 'sonnet']]
|
|
69
|
+
assert.strict.deepEqual(r, rr)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('skipPermissions非布林值時回退預設true', async function() {
|
|
73
|
+
let r = []
|
|
74
|
+
for (let skipPermissions of [null, undefined, 1, 'false', {}]) {
|
|
75
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, skipPermissions })
|
|
76
|
+
let o = JSON.parse(t.stdout)
|
|
77
|
+
r.push(o.args.includes('--dangerously-skip-permissions'))
|
|
78
|
+
}
|
|
79
|
+
let rr = [true, true, true, true, true]
|
|
80
|
+
assert.strict.deepEqual(r, rr)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('extraArgs接於固定旗標之後', async function() {
|
|
84
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, model: 'sonnet', extraArgs: ['--verbose', '--fake-stderr=warn'] })
|
|
85
|
+
let o = JSON.parse(t.stdout)
|
|
86
|
+
let r = o.args
|
|
87
|
+
let rr = ['-p', '--dangerously-skip-permissions', '--model', 'sonnet', '--verbose', '--fake-stderr=warn']
|
|
88
|
+
assert.strict.deepEqual(r, rr)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('prompt以stdin傳入, 中文與多行皆完整保留', async function() {
|
|
92
|
+
let prompt = '第一行中文\n第二行 with "quote" & <sym>\n第三行'
|
|
93
|
+
let t = await dispatchClaude(prompt, { exe: fake.exe })
|
|
94
|
+
let o = JSON.parse(t.stdout)
|
|
95
|
+
let r = [o.stdin === prompt, o.args.includes(prompt)]
|
|
96
|
+
let rr = [true, false]
|
|
97
|
+
assert.strict.deepEqual(r, rr)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('執行檔不存在時回傳ENOENT且不重試', async function() {
|
|
101
|
+
let t = await dispatchClaude('abc', { exe: 'claude-not-exist-for-test', maxRetries: 3 })
|
|
102
|
+
let r = [t.ok, t.error.includes('ENOENT'), t.attempts]
|
|
103
|
+
let rr = [false, true, 1]
|
|
104
|
+
assert.strict.deepEqual(r, rr)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('離開碼非0時回傳失敗', async function() {
|
|
108
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, extraArgs: ['--fake-exit=1'] })
|
|
109
|
+
let r = [t.ok, t.code, t.error]
|
|
110
|
+
let rr = [false, 1, 'Exit code 1']
|
|
111
|
+
assert.strict.deepEqual(r, rr)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('validate驗證失敗時回傳OUTPUT_VALIDATION_FAILED', async function() {
|
|
115
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, validate: 'min:100000' })
|
|
116
|
+
let r = [t.ok, t.code, t.error]
|
|
117
|
+
let rr = [false, 0, 'OUTPUT_VALIDATION_FAILED']
|
|
118
|
+
assert.strict.deepEqual(r, rr)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('逾時時強制關閉子進程並回傳TIMEOUT', async function() {
|
|
122
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, extraArgs: ['--fake-sleep=10000'], timeoutMs: 1000 })
|
|
123
|
+
let r = [t.ok, t.error.includes('TIMEOUT')]
|
|
124
|
+
let rr = [false, true]
|
|
125
|
+
assert.strict.deepEqual(r, rr)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('cwd等execCli設定原樣轉傳', async function() {
|
|
129
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, cwd: process.cwd() })
|
|
130
|
+
let o = JSON.parse(t.stdout)
|
|
131
|
+
let r = [t.ok, o.cwd === process.cwd()]
|
|
132
|
+
let rr = [true, true]
|
|
133
|
+
assert.strict.deepEqual(r, rr)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('無key概念, 給予key與provider亦不注入OPENCODE_AUTH_CONTENT', async function() {
|
|
137
|
+
let t = await dispatchClaude('abc', { exe: fake.exe, key: 'sk-abc', provider: 'opencode' })
|
|
138
|
+
let o = JSON.parse(t.stdout)
|
|
139
|
+
let r = [t.ok, o.env.OPENCODE_AUTH_CONTENT]
|
|
140
|
+
let rr = [true, '']
|
|
141
|
+
assert.strict.deepEqual(r, rr)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import dispatchCodex from '../src/dispatchCodex.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('dispatchCodex', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
|
|
10
|
+
before(function() {
|
|
11
|
+
fake = createFakeCli('fake-codex')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(function() {
|
|
15
|
+
if (fake) {
|
|
16
|
+
fake.clean()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('prompt非有效字串時回傳錯誤結果物件且不reject', async function() {
|
|
21
|
+
let r = []
|
|
22
|
+
for (let prompt of [null, undefined, '', 123, {}, []]) {
|
|
23
|
+
let t = await dispatchCodex(prompt)
|
|
24
|
+
r.push([t.ok, t.error, t.attempts])
|
|
25
|
+
}
|
|
26
|
+
let rr = [
|
|
27
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
28
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
29
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
30
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
31
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
32
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
33
|
+
]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('固定帶exec與--skip-git-repo-check旗標, sandbox預設workspace-write', async function() {
|
|
38
|
+
let t = await dispatchCodex('abc', { exe: fake.exe })
|
|
39
|
+
let o = JSON.parse(t.stdout)
|
|
40
|
+
let r = [t.ok, o.args]
|
|
41
|
+
let rr = [true, ['exec', '--sandbox', 'workspace-write', '--skip-git-repo-check']]
|
|
42
|
+
assert.strict.deepEqual(r, rr)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('有給model時帶-m旗標', async function() {
|
|
46
|
+
let t = await dispatchCodex('abc', { exe: fake.exe, model: 'gpt-5.6-luna' })
|
|
47
|
+
let o = JSON.parse(t.stdout)
|
|
48
|
+
let r = o.args
|
|
49
|
+
let rr = ['exec', '--sandbox', 'workspace-write', '--skip-git-repo-check', '-m', 'gpt-5.6-luna']
|
|
50
|
+
assert.strict.deepEqual(r, rr)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('model非有效字串時整段-m旗標不出現', async function() {
|
|
54
|
+
let r = []
|
|
55
|
+
for (let model of [null, undefined, '', 123, {}]) {
|
|
56
|
+
let t = await dispatchCodex('abc', { exe: fake.exe, model })
|
|
57
|
+
let o = JSON.parse(t.stdout)
|
|
58
|
+
r.push(o.args.includes('-m'))
|
|
59
|
+
}
|
|
60
|
+
let rr = [false, false, false, false, false]
|
|
61
|
+
assert.strict.deepEqual(r, rr)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('sandbox可自訂, 非有效字串則回退workspace-write', async function() {
|
|
65
|
+
let t1 = await dispatchCodex('abc', { exe: fake.exe, sandbox: 'read-only' })
|
|
66
|
+
let t2 = await dispatchCodex('abc', { exe: fake.exe, sandbox: 123 })
|
|
67
|
+
let r = [
|
|
68
|
+
JSON.parse(t1.stdout).args[2],
|
|
69
|
+
JSON.parse(t2.stdout).args[2],
|
|
70
|
+
]
|
|
71
|
+
let rr = ['read-only', 'workspace-write']
|
|
72
|
+
assert.strict.deepEqual(r, rr)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('extraArgs接於固定旗標之後', async function() {
|
|
76
|
+
let t = await dispatchCodex('abc', { exe: fake.exe, model: 'gpt-5.6-luna', extraArgs: ['--config', 'model_reasoning_effort="max"'] })
|
|
77
|
+
let o = JSON.parse(t.stdout)
|
|
78
|
+
let r = o.args
|
|
79
|
+
let rr = ['exec', '--sandbox', 'workspace-write', '--skip-git-repo-check', '-m', 'gpt-5.6-luna', '--config', 'model_reasoning_effort="max"']
|
|
80
|
+
assert.strict.deepEqual(r, rr)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('prompt以stdin傳入, 中文與多行皆完整保留', async function() {
|
|
84
|
+
let prompt = '第一行中文\n第二行 with "quote" & <sym>\n第三行'
|
|
85
|
+
let t = await dispatchCodex(prompt, { exe: fake.exe })
|
|
86
|
+
let o = JSON.parse(t.stdout)
|
|
87
|
+
let r = [o.stdin === prompt, o.args.includes(prompt)]
|
|
88
|
+
let rr = [true, false]
|
|
89
|
+
assert.strict.deepEqual(r, rr)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('執行檔不存在時回傳ENOENT且不重試', async function() {
|
|
93
|
+
let t = await dispatchCodex('abc', { exe: 'codex-not-exist-for-test', maxRetries: 3 })
|
|
94
|
+
let r = [t.ok, t.error.includes('ENOENT'), t.attempts]
|
|
95
|
+
let rr = [false, true, 1]
|
|
96
|
+
assert.strict.deepEqual(r, rr)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('失敗時依maxRetries重試, 重試次數含初始共maxRetries+1次', async function() {
|
|
100
|
+
let t = await dispatchCodex('abc', { exe: fake.exe, extraArgs: ['--fake-exit=1'], maxRetries: 1, retryDelayMs: 100 })
|
|
101
|
+
let r = [t.ok, t.code, t.attempts]
|
|
102
|
+
let rr = [false, 1, 2]
|
|
103
|
+
assert.strict.deepEqual(r, rr)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('stderr內容原樣回傳', async function() {
|
|
107
|
+
let t = await dispatchCodex('abc', { exe: fake.exe, extraArgs: ['--fake-stderr=some warning'] })
|
|
108
|
+
let r = [t.ok, t.stderr]
|
|
109
|
+
let rr = [true, 'some warning']
|
|
110
|
+
assert.strict.deepEqual(r, rr)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import dispatchOpencode from '../src/dispatchOpencode.mjs'
|
|
3
|
+
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('dispatchOpencode', function() {
|
|
7
|
+
|
|
8
|
+
let fake = null
|
|
9
|
+
|
|
10
|
+
before(function() {
|
|
11
|
+
fake = createFakeCli('fake-opencode')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(function() {
|
|
15
|
+
if (fake) {
|
|
16
|
+
fake.clean()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('prompt非有效字串時回傳錯誤結果物件且不reject', async function() {
|
|
21
|
+
let r = []
|
|
22
|
+
for (let prompt of [null, undefined, '', 123, {}, []]) {
|
|
23
|
+
let t = await dispatchOpencode(prompt)
|
|
24
|
+
r.push([t.ok, t.error, t.attempts])
|
|
25
|
+
}
|
|
26
|
+
let rr = [
|
|
27
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
28
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
29
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
30
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
31
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
32
|
+
[false, 'prompt must be a non-empty string', 0],
|
|
33
|
+
]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('固定帶run與--agent旗標, agent預設build', async function() {
|
|
38
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe })
|
|
39
|
+
let o = JSON.parse(t.stdout)
|
|
40
|
+
let r = [t.ok, o.args]
|
|
41
|
+
let rr = [true, ['run', '--agent', 'build']]
|
|
42
|
+
assert.strict.deepEqual(r, rr)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('有給model時帶-m旗標', async function() {
|
|
46
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, model: 'opencode/deepseek-v4-flash-free' })
|
|
47
|
+
let o = JSON.parse(t.stdout)
|
|
48
|
+
let r = o.args
|
|
49
|
+
let rr = ['run', '--agent', 'build', '-m', 'opencode/deepseek-v4-flash-free']
|
|
50
|
+
assert.strict.deepEqual(r, rr)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('agent可自訂, 非有效字串則回退build', async function() {
|
|
54
|
+
let t1 = await dispatchOpencode('abc', { exe: fake.exe, agent: 'plan' })
|
|
55
|
+
let t2 = await dispatchOpencode('abc', { exe: fake.exe, agent: 123 })
|
|
56
|
+
let r = [
|
|
57
|
+
JSON.parse(t1.stdout).args[2],
|
|
58
|
+
JSON.parse(t2.stdout).args[2],
|
|
59
|
+
]
|
|
60
|
+
let rr = ['plan', 'build']
|
|
61
|
+
assert.strict.deepEqual(r, rr)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('同時給予key與provider時注入OPENCODE_AUTH_CONTENT', async function() {
|
|
65
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, provider: 'opencode', key: 'sk-abc123' })
|
|
66
|
+
let o = JSON.parse(t.stdout)
|
|
67
|
+
let r = [t.ok, JSON.parse(o.env.OPENCODE_AUTH_CONTENT)]
|
|
68
|
+
let rr = [true, { opencode: { type: 'api', key: 'sk-abc123' } }]
|
|
69
|
+
assert.strict.deepEqual(r, rr)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('僅給key或僅給provider時不注入OPENCODE_AUTH_CONTENT', async function() {
|
|
73
|
+
let t1 = await dispatchOpencode('abc', { exe: fake.exe, key: 'sk-abc123' })
|
|
74
|
+
let t2 = await dispatchOpencode('abc', { exe: fake.exe, provider: 'opencode' })
|
|
75
|
+
let t3 = await dispatchOpencode('abc', { exe: fake.exe })
|
|
76
|
+
let r = [
|
|
77
|
+
JSON.parse(t1.stdout).env.OPENCODE_AUTH_CONTENT,
|
|
78
|
+
JSON.parse(t2.stdout).env.OPENCODE_AUTH_CONTENT,
|
|
79
|
+
JSON.parse(t3.stdout).env.OPENCODE_AUTH_CONTENT,
|
|
80
|
+
]
|
|
81
|
+
let rr = ['', '', '']
|
|
82
|
+
assert.strict.deepEqual(r, rr)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('金鑰注入僅作用於當次子進程, 不影響本進程之process.env', async function() {
|
|
86
|
+
let before = process.env.OPENCODE_AUTH_CONTENT
|
|
87
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, provider: 'opencode', key: 'sk-abc123' })
|
|
88
|
+
let o = JSON.parse(t.stdout)
|
|
89
|
+
let after = process.env.OPENCODE_AUTH_CONTENT
|
|
90
|
+
let r = [o.env.OPENCODE_AUTH_CONTENT !== '', before === after]
|
|
91
|
+
let rr = [true, true]
|
|
92
|
+
assert.strict.deepEqual(r, rr)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('逐次可帶不同金鑰, 供多把金鑰輪替', async function() {
|
|
96
|
+
let r = []
|
|
97
|
+
for (let key of ['sk-key1', 'sk-key2', 'sk-key3']) {
|
|
98
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, provider: 'opencode', key })
|
|
99
|
+
let o = JSON.parse(t.stdout)
|
|
100
|
+
r.push(JSON.parse(o.env.OPENCODE_AUTH_CONTENT).opencode.key)
|
|
101
|
+
}
|
|
102
|
+
let rr = ['sk-key1', 'sk-key2', 'sk-key3']
|
|
103
|
+
assert.strict.deepEqual(r, rr)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('config為物件時序列化後以OPENCODE_CONFIG_CONTENT注入', async function() {
|
|
107
|
+
let config = {
|
|
108
|
+
provider: {
|
|
109
|
+
'agnes-ai': {
|
|
110
|
+
npm: '@ai-sdk/openai-compatible',
|
|
111
|
+
name: 'Agnes',
|
|
112
|
+
options: { baseURL: 'https://apihub.agnes-ai.com/v1' },
|
|
113
|
+
models: { 'agnes-2.0-flash': { name: 'Agnes 2.0 Flash' } },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, model: 'agnes-ai/agnes-2.0-flash', provider: 'agnes-ai', key: 'sk-abc123', config })
|
|
118
|
+
let o = JSON.parse(t.stdout)
|
|
119
|
+
let r = [
|
|
120
|
+
JSON.parse(o.env.OPENCODE_CONFIG_CONTENT),
|
|
121
|
+
JSON.parse(o.env.OPENCODE_AUTH_CONTENT),
|
|
122
|
+
o.args,
|
|
123
|
+
]
|
|
124
|
+
let rr = [
|
|
125
|
+
config,
|
|
126
|
+
{ 'agnes-ai': { type: 'api', key: 'sk-abc123' } },
|
|
127
|
+
['run', '--agent', 'build', '-m', 'agnes-ai/agnes-2.0-flash'],
|
|
128
|
+
]
|
|
129
|
+
assert.strict.deepEqual(r, rr)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('config為JSON字串時原樣注入', async function() {
|
|
133
|
+
let config = '{"provider":{"agnes-ai":{"npm":"@ai-sdk/openai-compatible"}}}'
|
|
134
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, config })
|
|
135
|
+
let o = JSON.parse(t.stdout)
|
|
136
|
+
let r = o.env.OPENCODE_CONFIG_CONTENT
|
|
137
|
+
let rr = config
|
|
138
|
+
assert.strict.deepEqual(r, rr)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('config非有效物件或字串時不注入OPENCODE_CONFIG_CONTENT', async function() {
|
|
142
|
+
let r = []
|
|
143
|
+
for (let config of [null, undefined, '', 123, []]) {
|
|
144
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, config })
|
|
145
|
+
let o = JSON.parse(t.stdout)
|
|
146
|
+
r.push(o.env.OPENCODE_CONFIG_CONTENT)
|
|
147
|
+
}
|
|
148
|
+
let rr = ['', '', '', '', '']
|
|
149
|
+
assert.strict.deepEqual(r, rr)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('自帶env可與金鑰注入併存', async function() {
|
|
153
|
+
let t = await dispatchOpencode('abc', { exe: fake.exe, provider: 'opencode', key: 'sk-abc123', env: { FAKE_ENV: 'hello' } })
|
|
154
|
+
let o = JSON.parse(t.stdout)
|
|
155
|
+
let r = [o.env.FAKE_ENV, JSON.parse(o.env.OPENCODE_AUTH_CONTENT).opencode.key]
|
|
156
|
+
let rr = ['hello', 'sk-abc123']
|
|
157
|
+
assert.strict.deepEqual(r, rr)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('prompt以stdin傳入, 中文與多行皆完整保留', async function() {
|
|
161
|
+
let prompt = '第一行中文\n第二行 with "quote" & <sym>\n第三行'
|
|
162
|
+
let t = await dispatchOpencode(prompt, { exe: fake.exe })
|
|
163
|
+
let o = JSON.parse(t.stdout)
|
|
164
|
+
let r = [o.stdin === prompt, o.args.includes(prompt)]
|
|
165
|
+
let rr = [true, false]
|
|
166
|
+
assert.strict.deepEqual(r, rr)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('執行檔不存在時回傳ENOENT且不重試', async function() {
|
|
170
|
+
let t = await dispatchOpencode('abc', { exe: 'opencode-not-exist-for-test', maxRetries: 3 })
|
|
171
|
+
let r = [t.ok, t.error.includes('ENOENT'), t.attempts]
|
|
172
|
+
let rr = [false, true, 1]
|
|
173
|
+
assert.strict.deepEqual(r, rr)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import getCliArgs from '../src/getCliArgs.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('getCliArgs', function() {
|
|
6
|
+
|
|
7
|
+
it('字串與陣列混合輸入可展平為一維陣列', function() {
|
|
8
|
+
let r = getCliArgs('run', ['--agent', 'build'], ['-m', 'opencode/abc'])
|
|
9
|
+
let rr = ['run', '--agent', 'build', '-m', 'opencode/abc']
|
|
10
|
+
assert.strict.deepEqual(r, rr)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('空陣列不產生任何參數', function() {
|
|
14
|
+
let r = getCliArgs('-p', [], '--verbose')
|
|
15
|
+
let rr = ['-p', '--verbose']
|
|
16
|
+
assert.strict.deepEqual(r, rr)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('非有效字串之參數一律濾除', function() {
|
|
20
|
+
let r = getCliArgs('-p', undefined, null, 123, true, {}, '', '--verbose')
|
|
21
|
+
let rr = ['-p', '--verbose']
|
|
22
|
+
assert.strict.deepEqual(r, rr)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('僅濾除非有效字串, 不判斷旗標配對, 故可選旗標須由呼叫端整段給或不給', function() {
|
|
26
|
+
let r = [
|
|
27
|
+
getCliArgs('-p', ['--model', undefined]),
|
|
28
|
+
getCliArgs('-p', []),
|
|
29
|
+
]
|
|
30
|
+
let rr = [
|
|
31
|
+
['-p', '--model'],
|
|
32
|
+
['-p'],
|
|
33
|
+
]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('僅展開一層, 巢狀陣列內之陣列視為非字串而濾除', function() {
|
|
38
|
+
let r = getCliArgs('a', ['b', ['c', 'd'], 'e'])
|
|
39
|
+
let rr = ['a', 'b', 'e']
|
|
40
|
+
assert.strict.deepEqual(r, rr)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('未給任何參數回傳空陣列', function() {
|
|
44
|
+
let r = getCliArgs()
|
|
45
|
+
let rr = []
|
|
46
|
+
assert.strict.deepEqual(r, rr)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('保留參數原有順序與重複值', function() {
|
|
50
|
+
let r = getCliArgs('--config', 'a=1', ['--config', 'a=1'])
|
|
51
|
+
let rr = ['--config', 'a=1', '--config', 'a=1']
|
|
52
|
+
assert.strict.deepEqual(r, rr)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import getErrorResult from '../src/getErrorResult.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('getErrorResult', function() {
|
|
6
|
+
|
|
7
|
+
it('回傳與execCli同結構之錯誤結果物件', function() {
|
|
8
|
+
let r = getErrorResult('something wrong')
|
|
9
|
+
let rr = {
|
|
10
|
+
ok: false,
|
|
11
|
+
stdout: '',
|
|
12
|
+
stderr: '',
|
|
13
|
+
code: null,
|
|
14
|
+
error: 'something wrong',
|
|
15
|
+
durationMs: 0,
|
|
16
|
+
attempts: 0,
|
|
17
|
+
}
|
|
18
|
+
assert.strict.deepEqual(r, rr)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('鍵名與順序固定', function() {
|
|
22
|
+
let r = Object.keys(getErrorResult('abc'))
|
|
23
|
+
let rr = ['ok', 'stdout', 'stderr', 'code', 'error', 'durationMs', 'attempts']
|
|
24
|
+
assert.strict.deepEqual(r, rr)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('非有效字串之錯誤訊息回退為unknown error', function() {
|
|
28
|
+
let r = []
|
|
29
|
+
for (let v of [null, undefined, '', 123, {}, []]) {
|
|
30
|
+
r.push(getErrorResult(v).error)
|
|
31
|
+
}
|
|
32
|
+
let rr = ['unknown error', 'unknown error', 'unknown error', 'unknown error', 'unknown error', 'unknown error']
|
|
33
|
+
assert.strict.deepEqual(r, rr)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('各次呼叫回傳互相獨立之物件', function() {
|
|
37
|
+
let a = getErrorResult('a')
|
|
38
|
+
let b = getErrorResult('b')
|
|
39
|
+
a.stdout = 'modified'
|
|
40
|
+
let r = [a === b, b.stdout, b.error]
|
|
41
|
+
let rr = [false, '', 'b']
|
|
42
|
+
assert.strict.deepEqual(r, rr)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import rollupFiles from 'w-package-tools/src/rollupFiles.mjs'
|
|
2
|
+
import getFiles from 'w-package-tools/src/getFiles.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
let fdSrc = './src'
|
|
6
|
+
let fdTar = './dist'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
rollupFiles({
|
|
10
|
+
fns: 'WDispatchAi.mjs',
|
|
11
|
+
fdSrc,
|
|
12
|
+
fdTar,
|
|
13
|
+
nameDistType: 'kebabCase',
|
|
14
|
+
globals: {
|
|
15
|
+
'path': 'path',
|
|
16
|
+
'fs': 'fs',
|
|
17
|
+
'child_process': 'child_process',
|
|
18
|
+
},
|
|
19
|
+
external: [
|
|
20
|
+
'path',
|
|
21
|
+
'fs',
|
|
22
|
+
'child_process',
|
|
23
|
+
],
|
|
24
|
+
})
|