@larktask/aamp-feishu-task-agent 0.1.0-dev.174 → 0.1.1-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -18
- package/bin/agent-metadata.mjs +54 -0
- package/bin/feishu-task-agent-controller.mjs +1947 -359
- package/bin/runtime-concurrency.mjs +71 -0
- package/bin/runtime-network.mjs +13 -0
- package/bin/runtime-package-executable.mjs +254 -0
- package/bin/traecode-readiness.mjs +173 -0
- package/bootstrap/aamp-feishu-task-agent-bootstrap.sh +1477 -56
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export async function settleWithConcurrency(items, limit, worker) {
|
|
2
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error('concurrency limit must be a positive integer')
|
|
3
|
+
const results = new Array(items.length)
|
|
4
|
+
let cursor = 0
|
|
5
|
+
const run = async () => {
|
|
6
|
+
while (cursor < items.length) {
|
|
7
|
+
const index = cursor
|
|
8
|
+
cursor += 1
|
|
9
|
+
try {
|
|
10
|
+
results[index] = { status: 'fulfilled', value: await worker(items[index], index) }
|
|
11
|
+
} catch (reason) {
|
|
12
|
+
results[index] = { status: 'rejected', reason }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
await Promise.all(Array.from(
|
|
17
|
+
{ length: Math.min(limit, items.length) },
|
|
18
|
+
() => run(),
|
|
19
|
+
))
|
|
20
|
+
return results
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runLayeredStarts(items, { prepare, start, concurrency }) {
|
|
24
|
+
const outcomes = new Array(items.length)
|
|
25
|
+
const prepared = []
|
|
26
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
27
|
+
try {
|
|
28
|
+
prepared.push({ index, value: await prepare(items[index], index) })
|
|
29
|
+
} catch (reason) {
|
|
30
|
+
outcomes[index] = { status: 'rejected', phase: 'prepare', reason, item: items[index], index }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const started = await settleWithConcurrency(
|
|
34
|
+
prepared,
|
|
35
|
+
concurrency,
|
|
36
|
+
({ index, value }) => start(value, index, items[index]),
|
|
37
|
+
)
|
|
38
|
+
started.forEach((result, preparedIndex) => {
|
|
39
|
+
const { index } = prepared[preparedIndex]
|
|
40
|
+
outcomes[index] = { ...result, phase: 'start', item: items[index], index }
|
|
41
|
+
})
|
|
42
|
+
return outcomes
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createKeyedSerialExecutor() {
|
|
46
|
+
const tails = new Map()
|
|
47
|
+
return async (key, operation) => {
|
|
48
|
+
const previous = tails.get(key) || Promise.resolve()
|
|
49
|
+
const current = previous.catch(() => {}).then(operation)
|
|
50
|
+
tails.set(key, current)
|
|
51
|
+
try {
|
|
52
|
+
return await current
|
|
53
|
+
} finally {
|
|
54
|
+
if (tails.get(key) === current) tails.delete(key)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createSerializedRunner(operation) {
|
|
60
|
+
let tail = Promise.resolve()
|
|
61
|
+
return {
|
|
62
|
+
run() {
|
|
63
|
+
const current = tail.catch(() => {}).then(operation)
|
|
64
|
+
tail = current
|
|
65
|
+
return current
|
|
66
|
+
},
|
|
67
|
+
flush() {
|
|
68
|
+
return tail
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
package/bin/runtime-network.mjs
CHANGED
|
@@ -190,6 +190,19 @@ export function agentStartRetryError(events, expectedAgentNames, attempt, maxAtt
|
|
|
190
190
|
return undefined;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
export function preserveAgentStartFailure(error, events) {
|
|
194
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
195
|
+
failure.agentStartEvents = [...(events || [])];
|
|
196
|
+
return failure;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function agentStartFailureMessage(events, agentName, fallbackMessage) {
|
|
200
|
+
const failed = [...(events || [])]
|
|
201
|
+
.reverse()
|
|
202
|
+
.find((event) => event?.type === 'agent.failed' && event.agent === agentName);
|
|
203
|
+
return String(failed?.message || fallbackMessage || `${agentName} Agent Bridge 启动失败`);
|
|
204
|
+
}
|
|
205
|
+
|
|
193
206
|
export function safeDiagnosticUrl(rawUrl) {
|
|
194
207
|
const parsed = new URL(rawUrl);
|
|
195
208
|
parsed.username = '';
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
const NPM_EXEC_ENVIRONMENT_KEYS = [
|
|
4
|
+
'npm_lifecycle_event',
|
|
5
|
+
'npm_package_json',
|
|
6
|
+
'npm_command',
|
|
7
|
+
'npm_execpath',
|
|
8
|
+
'npm_node_execpath',
|
|
9
|
+
'INIT_CWD',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const RESOLVE_EXECUTABLE_SOURCE = String.raw`
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
const executable = process.argv[1]
|
|
17
|
+
const environmentKeys = ${JSON.stringify(NPM_EXEC_ENVIRONMENT_KEYS)}
|
|
18
|
+
|
|
19
|
+
function environmentValue(name) {
|
|
20
|
+
return Object.entries(process.env)
|
|
21
|
+
.find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1] || ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pathValue = environmentValue('PATH')
|
|
25
|
+
const pathEntries = pathValue.split(path.delimiter).filter(Boolean)
|
|
26
|
+
const environment = {}
|
|
27
|
+
for (const key of environmentKeys) {
|
|
28
|
+
if (Object.hasOwn(process.env, key) && typeof process.env[key] === 'string') {
|
|
29
|
+
environment[key] = process.env[key]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const windowsExtensions = (environmentValue('PATHEXT') || '.COM;.EXE;.BAT;.CMD')
|
|
33
|
+
.split(';')
|
|
34
|
+
.map((extension) => extension.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
|
|
37
|
+
function executableCandidates(binDir) {
|
|
38
|
+
const candidate = path.join(binDir, executable)
|
|
39
|
+
if (process.platform !== 'win32') return [candidate]
|
|
40
|
+
if (windowsExtensions.some((extension) => executable.toLowerCase().endsWith(extension.toLowerCase()))) {
|
|
41
|
+
return [candidate]
|
|
42
|
+
}
|
|
43
|
+
return windowsExtensions.map((extension) => candidate + extension.toLowerCase())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const binDir of pathEntries) {
|
|
47
|
+
for (const candidate of executableCandidates(binDir)) {
|
|
48
|
+
try {
|
|
49
|
+
const stat = fs.statSync(candidate)
|
|
50
|
+
if (!stat.isFile()) continue
|
|
51
|
+
if (process.platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK)
|
|
52
|
+
const extension = path.extname(candidate).toLowerCase()
|
|
53
|
+
const kind = process.platform === 'win32' && (extension === '.cmd' || extension === '.bat')
|
|
54
|
+
? 'cmd'
|
|
55
|
+
: 'direct'
|
|
56
|
+
process.stdout.write(JSON.stringify({
|
|
57
|
+
executable,
|
|
58
|
+
kind,
|
|
59
|
+
command: candidate,
|
|
60
|
+
pathValue,
|
|
61
|
+
environment,
|
|
62
|
+
}))
|
|
63
|
+
process.exit(0)
|
|
64
|
+
} catch {
|
|
65
|
+
// Keep following npm's PATH order until its executable shim is found.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.stderr.write('unable to resolve npm executable shim: ' + executable + '\\n')
|
|
71
|
+
process.exit(1)
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
export function npmExecutableResolverArgs(executable) {
|
|
75
|
+
return ['--input-type=module', '--eval', RESOLVE_EXECUTABLE_SOURCE, executable]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validatePreparedExecutable(value, executable = value?.executable) {
|
|
79
|
+
if (!value || typeof value !== 'object') {
|
|
80
|
+
throw new Error(`${executable || 'package'} resolved an invalid npm executable descriptor`)
|
|
81
|
+
}
|
|
82
|
+
if (value.executable !== executable || !['direct', 'cmd'].includes(value.kind)) {
|
|
83
|
+
throw new Error(`${executable} resolved an invalid npm executable descriptor`)
|
|
84
|
+
}
|
|
85
|
+
if (typeof value.command !== 'string' || !value.command) {
|
|
86
|
+
throw new Error(`${executable} resolved an invalid npm executable command`)
|
|
87
|
+
}
|
|
88
|
+
if (typeof value.pathValue !== 'string' || !value.pathValue) {
|
|
89
|
+
throw new Error(`${executable} resolved an invalid npm executable PATH`)
|
|
90
|
+
}
|
|
91
|
+
if (!value.environment || typeof value.environment !== 'object' || Array.isArray(value.environment)) {
|
|
92
|
+
throw new Error(`${executable} resolved an invalid npm executable environment`)
|
|
93
|
+
}
|
|
94
|
+
const environment = {}
|
|
95
|
+
for (const [key, environmentValue] of Object.entries(value.environment)) {
|
|
96
|
+
if (!NPM_EXEC_ENVIRONMENT_KEYS.includes(key) || typeof environmentValue !== 'string') {
|
|
97
|
+
throw new Error(`${executable} resolved an invalid npm executable environment`)
|
|
98
|
+
}
|
|
99
|
+
environment[key] = environmentValue
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
executable: value.executable,
|
|
103
|
+
kind: value.kind,
|
|
104
|
+
command: value.command,
|
|
105
|
+
pathValue: value.pathValue,
|
|
106
|
+
environment,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseResolvedPackageExecutable(stdout, executable) {
|
|
111
|
+
const lines = String(stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
112
|
+
for (const line of lines.reverse()) {
|
|
113
|
+
try {
|
|
114
|
+
const result = JSON.parse(line)
|
|
115
|
+
if (result?.executable === executable) return validatePreparedExecutable(result, executable)
|
|
116
|
+
} catch {
|
|
117
|
+
// npm can print unrelated output; continue looking for the resolver record.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`${executable} package did not expose a runnable npm executable shim`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function replacePath(environment, pathValue, platform) {
|
|
124
|
+
const next = { ...(environment || process.env) }
|
|
125
|
+
const existingKeys = Object.keys(next).filter((key) => key.toLowerCase() === 'path')
|
|
126
|
+
const pathKey = existingKeys[0] || (platform === 'win32' ? 'Path' : 'PATH')
|
|
127
|
+
for (const key of existingKeys) {
|
|
128
|
+
if (key !== pathKey) delete next[key]
|
|
129
|
+
}
|
|
130
|
+
next[pathKey] = pathValue
|
|
131
|
+
return next
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mergePreparedEnvironment(environment, descriptor, platform) {
|
|
135
|
+
const next = replacePath(environment, descriptor.pathValue, platform)
|
|
136
|
+
for (const [key, value] of Object.entries(descriptor.environment)) {
|
|
137
|
+
for (const existingKey of Object.keys(next)) {
|
|
138
|
+
if (existingKey.toLowerCase() === key.toLowerCase()) delete next[existingKey]
|
|
139
|
+
}
|
|
140
|
+
next[key] = value
|
|
141
|
+
}
|
|
142
|
+
return next
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Adapted from the escaping used by @npmcli/promise-spawn for cmd.exe. User
|
|
146
|
+
// arguments need two escaping passes because a generated .cmd shim adds a
|
|
147
|
+
// second cmd.exe parsing layer.
|
|
148
|
+
function escapeCmdArgument(input, doubleEscape = false) {
|
|
149
|
+
const value = String(input)
|
|
150
|
+
if (!value.length) return '""'
|
|
151
|
+
let result
|
|
152
|
+
if (!/[ \t\n\v"]/.test(value)) {
|
|
153
|
+
result = value
|
|
154
|
+
} else {
|
|
155
|
+
result = '"'
|
|
156
|
+
for (let index = 0; index <= value.length; index += 1) {
|
|
157
|
+
let slashCount = 0
|
|
158
|
+
while (value[index] === '\\') {
|
|
159
|
+
index += 1
|
|
160
|
+
slashCount += 1
|
|
161
|
+
}
|
|
162
|
+
if (index === value.length) {
|
|
163
|
+
result += '\\'.repeat(slashCount * 2)
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
if (value[index] === '"') {
|
|
167
|
+
result += '\\'.repeat(slashCount * 2 + 1)
|
|
168
|
+
result += value[index]
|
|
169
|
+
} else {
|
|
170
|
+
result += '\\'.repeat(slashCount)
|
|
171
|
+
result += value[index]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
result += '"'
|
|
175
|
+
}
|
|
176
|
+
result = result.replace(/[ !%^&()<>|"]/g, '^$&')
|
|
177
|
+
if (doubleEscape) result = result.replace(/[ !%^&()<>|"]/g, '^$&')
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createPackageExecutableLauncher({
|
|
182
|
+
materialize,
|
|
183
|
+
spawnProcess = spawn,
|
|
184
|
+
platform = process.platform,
|
|
185
|
+
} = {}) {
|
|
186
|
+
if (typeof materialize !== 'function') throw new Error('materialize must be a function')
|
|
187
|
+
const resolved = new Map()
|
|
188
|
+
const packageTails = new Map()
|
|
189
|
+
|
|
190
|
+
function resolve(packageSpec, executable, context = {}) {
|
|
191
|
+
const key = JSON.stringify([packageSpec, executable])
|
|
192
|
+
const existing = resolved.get(key)
|
|
193
|
+
if (existing) return existing
|
|
194
|
+
|
|
195
|
+
const previous = packageTails.get(packageSpec) || Promise.resolve()
|
|
196
|
+
const pending = previous.catch(() => {}).then(async () => (
|
|
197
|
+
validatePreparedExecutable(
|
|
198
|
+
await materialize(packageSpec, executable, context),
|
|
199
|
+
executable,
|
|
200
|
+
)
|
|
201
|
+
))
|
|
202
|
+
packageTails.set(packageSpec, pending)
|
|
203
|
+
resolved.set(key, pending)
|
|
204
|
+
pending.then(
|
|
205
|
+
() => {
|
|
206
|
+
if (packageTails.get(packageSpec) === pending) packageTails.delete(packageSpec)
|
|
207
|
+
},
|
|
208
|
+
() => {
|
|
209
|
+
if (resolved.get(key) === pending) resolved.delete(key)
|
|
210
|
+
if (packageTails.get(packageSpec) === pending) packageTails.delete(packageSpec)
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
return pending
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function launchPrepared({ preparedExecutable, args = [], spawnOptions = {} }) {
|
|
217
|
+
const descriptor = validatePreparedExecutable(preparedExecutable)
|
|
218
|
+
const options = {
|
|
219
|
+
...spawnOptions,
|
|
220
|
+
env: mergePreparedEnvironment(spawnOptions.env, descriptor, platform),
|
|
221
|
+
shell: false,
|
|
222
|
+
}
|
|
223
|
+
if (descriptor.kind === 'direct') {
|
|
224
|
+
return spawnProcess(descriptor.command, args, options)
|
|
225
|
+
}
|
|
226
|
+
if (platform !== 'win32') {
|
|
227
|
+
throw new Error(`${descriptor.executable} resolved a Windows command shim on ${platform}`)
|
|
228
|
+
}
|
|
229
|
+
const commandShell = Object.entries(options.env)
|
|
230
|
+
.find(([key]) => key.toLowerCase() === 'comspec')?.[1] || 'cmd.exe'
|
|
231
|
+
const script = [
|
|
232
|
+
escapeCmdArgument(descriptor.command),
|
|
233
|
+
...args.map((argument) => escapeCmdArgument(argument, true)),
|
|
234
|
+
].join(' ')
|
|
235
|
+
return spawnProcess(commandShell, ['/d', '/s', '/c', script], {
|
|
236
|
+
...options,
|
|
237
|
+
windowsVerbatimArguments: true,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function launch({
|
|
242
|
+
packageSpec,
|
|
243
|
+
executable,
|
|
244
|
+
args = [],
|
|
245
|
+
context = {},
|
|
246
|
+
spawnOptions = {},
|
|
247
|
+
preparedExecutable,
|
|
248
|
+
}) {
|
|
249
|
+
const descriptor = preparedExecutable || await resolve(packageSpec, executable, context)
|
|
250
|
+
return launchPrepared({ preparedExecutable: descriptor, args, spawnOptions })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { launch, launchPrepared, resolve }
|
|
254
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024
|
|
6
|
+
const EXIT = Object.freeze({ unsupported: 3, model: 10, blocked: 11, invalid: 65, execution: 70, timeout: 124, missing: 127 })
|
|
7
|
+
|
|
8
|
+
function cleanText(value, homeDir = '') {
|
|
9
|
+
if (typeof value !== 'string') return undefined
|
|
10
|
+
let text = value.replace(/\s+/g, ' ').trim()
|
|
11
|
+
if (homeDir) text = text.split(homeDir).join('~')
|
|
12
|
+
if (!text) return undefined
|
|
13
|
+
return text.slice(0, 400)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function supportsTraeCodeAcpHelp(output) {
|
|
17
|
+
const text = String(output || '')
|
|
18
|
+
return /\bacp\s+serve\b/i.test(text) && /Start the ACP server/i.test(text)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseTraeCodeDoctor(raw, homeDir = '') {
|
|
22
|
+
let document
|
|
23
|
+
try {
|
|
24
|
+
document = JSON.parse(String(raw || ''))
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error('TraeCode doctor did not return valid JSON')
|
|
27
|
+
}
|
|
28
|
+
if (!document || typeof document !== 'object' || !Array.isArray(document.checks)) {
|
|
29
|
+
throw new Error('TraeCode doctor JSON is missing a checks array')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const checks = document.checks.map((value, index) => {
|
|
33
|
+
if (!value || typeof value !== 'object') {
|
|
34
|
+
throw new Error(`TraeCode doctor check ${index} is invalid`)
|
|
35
|
+
}
|
|
36
|
+
const name = cleanText(value.name, homeDir)
|
|
37
|
+
const severity = cleanText(value.severity, homeDir)?.toLowerCase()
|
|
38
|
+
const message = cleanText(value.message, homeDir)
|
|
39
|
+
const fix = cleanText(value.fix, homeDir)
|
|
40
|
+
if (!name || !['info', 'warning', 'error'].includes(severity) || !message) {
|
|
41
|
+
throw new Error(`TraeCode doctor check ${index} is invalid`)
|
|
42
|
+
}
|
|
43
|
+
return { name, severity, message, ...(fix ? { fix } : {}) }
|
|
44
|
+
})
|
|
45
|
+
const warnings = checks.filter((check) => check.severity === 'warning')
|
|
46
|
+
const errors = checks.filter((check) => check.severity === 'error')
|
|
47
|
+
return {
|
|
48
|
+
status: errors.length === 0
|
|
49
|
+
? 'ready'
|
|
50
|
+
: errors.some((check) => check.name.toLowerCase() === 'model')
|
|
51
|
+
? 'model_required'
|
|
52
|
+
: 'blocked',
|
|
53
|
+
warnings,
|
|
54
|
+
errors,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatChecks(checks) {
|
|
59
|
+
return checks.map((check) => {
|
|
60
|
+
const suffix = check.fix ? `;建议:${check.fix}` : ''
|
|
61
|
+
return `${check.name}: ${check.message}${suffix}`
|
|
62
|
+
}).join('\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runBounded(command, args, timeoutSeconds) {
|
|
66
|
+
const timeoutMs = Math.max(1, Number.isFinite(timeoutSeconds) ? timeoutSeconds : 10) * 1000
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let stdout = ''
|
|
69
|
+
let stderr = ''
|
|
70
|
+
let total = 0
|
|
71
|
+
let timedOut = false
|
|
72
|
+
let outputLimited = false
|
|
73
|
+
let settled = false
|
|
74
|
+
let timer
|
|
75
|
+
const child = spawn(command, args, {
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
})
|
|
79
|
+
let killTimer
|
|
80
|
+
const finish = (result) => {
|
|
81
|
+
if (settled) return
|
|
82
|
+
settled = true
|
|
83
|
+
clearTimeout(timer)
|
|
84
|
+
clearTimeout(killTimer)
|
|
85
|
+
resolve(result)
|
|
86
|
+
}
|
|
87
|
+
const signal = (name) => {
|
|
88
|
+
try {
|
|
89
|
+
child.kill(name)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code === 'ESRCH') return
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const terminate = () => {
|
|
96
|
+
signal('SIGTERM')
|
|
97
|
+
if (!killTimer) killTimer = setTimeout(() => {
|
|
98
|
+
signal('SIGKILL')
|
|
99
|
+
child.stdout.destroy()
|
|
100
|
+
child.stderr.destroy()
|
|
101
|
+
finish({ code: child.exitCode, signal: child.signalCode, stdout, stderr, timedOut, outputLimited })
|
|
102
|
+
}, 500)
|
|
103
|
+
killTimer.unref()
|
|
104
|
+
}
|
|
105
|
+
const append = (target, chunk) => {
|
|
106
|
+
const text = chunk.toString('utf8')
|
|
107
|
+
total += Buffer.byteLength(text)
|
|
108
|
+
if (total > MAX_OUTPUT_BYTES) {
|
|
109
|
+
if (!outputLimited) {
|
|
110
|
+
outputLimited = true
|
|
111
|
+
clearTimeout(timer)
|
|
112
|
+
terminate()
|
|
113
|
+
}
|
|
114
|
+
return target
|
|
115
|
+
}
|
|
116
|
+
return target + text
|
|
117
|
+
}
|
|
118
|
+
child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk) })
|
|
119
|
+
child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk) })
|
|
120
|
+
timer = setTimeout(() => {
|
|
121
|
+
timedOut = true
|
|
122
|
+
terminate()
|
|
123
|
+
}, timeoutMs)
|
|
124
|
+
child.once('error', (error) => {
|
|
125
|
+
finish({ spawnError: error, stdout, stderr, timedOut, outputLimited })
|
|
126
|
+
})
|
|
127
|
+
child.once('close', (code, signal) => {
|
|
128
|
+
finish({ code, signal, stdout, stderr, timedOut, outputLimited })
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
const [action, command, timeoutRaw] = process.argv.slice(2)
|
|
135
|
+
if (!['probe-acp', 'doctor'].includes(action) || !command) process.exit(64)
|
|
136
|
+
const result = await runBounded(command, action === 'probe-acp'
|
|
137
|
+
? ['acp', 'serve', '--help']
|
|
138
|
+
: ['doctor', '--json'], Number(timeoutRaw || '10'))
|
|
139
|
+
if (result.timedOut) process.exit(EXIT.timeout)
|
|
140
|
+
if (result.outputLimited) process.exit(EXIT.execution)
|
|
141
|
+
if (result.spawnError) process.exit(result.spawnError.code === 'ENOENT' ? EXIT.missing : EXIT.execution)
|
|
142
|
+
|
|
143
|
+
if (action === 'probe-acp') {
|
|
144
|
+
process.exit(result.code === 0 && supportsTraeCodeAcpHelp(`${result.stdout}\n${result.stderr}`)
|
|
145
|
+
? 0
|
|
146
|
+
: EXIT.unsupported)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (![0, 1, 2].includes(result.code)) {
|
|
150
|
+
process.stderr.write('TraeCode doctor failed with an unsupported exit code')
|
|
151
|
+
process.exit(EXIT.execution)
|
|
152
|
+
}
|
|
153
|
+
let parsed
|
|
154
|
+
try {
|
|
155
|
+
parsed = parseTraeCodeDoctor(result.stdout, process.env.HOME || '')
|
|
156
|
+
} catch (error) {
|
|
157
|
+
process.stderr.write(error.message)
|
|
158
|
+
process.exit(EXIT.invalid)
|
|
159
|
+
}
|
|
160
|
+
if (parsed.status === 'ready') {
|
|
161
|
+
if (parsed.warnings.length) process.stdout.write(formatChecks(parsed.warnings))
|
|
162
|
+
process.exit(0)
|
|
163
|
+
}
|
|
164
|
+
process.stdout.write(formatChecks(parsed.errors))
|
|
165
|
+
process.exit(parsed.status === 'model_required' ? EXIT.model : EXIT.blocked)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
169
|
+
main().catch((error) => {
|
|
170
|
+
process.stderr.write(cleanText(error?.message || error, process.env.HOME) || 'TraeCode readiness check failed')
|
|
171
|
+
process.exit(EXIT.execution)
|
|
172
|
+
})
|
|
173
|
+
}
|