martty 0.2.33 → 0.2.35

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.
@@ -64,6 +64,7 @@ class AcpSessionConfigService extends Service {
64
64
  /** Install the `ctx.acpSessionConfig` standard ACP state service. */
65
65
  export function installAcpSessionConfig(ctx) {
66
66
  let sessionId
67
+ let sessionGeneration = 0
67
68
  let selectionKnown = false
68
69
  const optionsBySession = new Map()
69
70
  let requestTui
@@ -142,6 +143,15 @@ export function installAcpSessionConfig(ctx) {
142
143
 
143
144
  function observeClient(message) {
144
145
  if (!isObject(message) || message.id === undefined || typeof message.method !== 'string') return
146
+ if (message.method === 'initialize') {
147
+ sessionGeneration++
148
+ pending.clear()
149
+ sessionId = undefined
150
+ selectionKnown = false
151
+ optionsBySession.clear()
152
+ publish()
153
+ return
154
+ }
145
155
  if (SETUP_METHODS.has(message.method)) {
146
156
  const requested = message.method !== 'session/new'
147
157
  ? readString(message.params, 'sessionId', 'session_id')
@@ -209,12 +219,15 @@ export function installAcpSessionConfig(ctx) {
209
219
  throw new Error(`acpSessionConfig.set: option "${id}" is not advertised by the current Session`)
210
220
  }
211
221
  validateValue(option, value)
222
+ const requestedSession = targetSessionId
223
+ const requestedGeneration = sessionGeneration
212
224
  const result = await requestTui(CORDIS_METHODS.sessionConfigSet, {
213
225
  protocol: CORDIS_PROTOCOL,
214
226
  sessionId: targetSessionId,
215
227
  configId: id,
216
228
  value,
217
229
  })
230
+ assertSession(requestedSession, requestedGeneration)
218
231
  if (!isObject(result)) {
219
232
  throw new Error('acpSessionConfig.set: native ACP client returned an invalid response')
220
233
  }
@@ -227,6 +240,12 @@ export function installAcpSessionConfig(ctx) {
227
240
  return setForSession(sessionId, id, value)
228
241
  }
229
242
 
243
+ function assertSession(expectedSession, expectedGeneration) {
244
+ if (sessionGeneration !== expectedGeneration || !optionsBySession.has(expectedSession)) {
245
+ throw new Error('acpSessionConfig: Session changed while configuring options')
246
+ }
247
+ }
248
+
230
249
  function transaction(selector) {
231
250
  const transactionSessionId = sessionId
232
251
  if (transactionSessionId === undefined) {
@@ -234,6 +253,8 @@ export function installAcpSessionConfig(ctx) {
234
253
  }
235
254
  const transactionOptions = optionsBySession.get(transactionSessionId) ?? []
236
255
  const option = resolveTransactionOption(selector, transactionOptions)
256
+ const originalSession = transactionSessionId
257
+ const originalGeneration = sessionGeneration
237
258
  const original = cloneJson(option.currentValue ?? option.current_value)
238
259
  if (original === undefined) {
239
260
  throw new Error(
@@ -248,6 +269,7 @@ export function installAcpSessionConfig(ctx) {
248
269
  let settled = false
249
270
 
250
271
  const write = async (value) => {
272
+ assertSession(originalSession, originalGeneration)
251
273
  const result = await setForSession(transactionSessionId, option.id, value)
252
274
  applied = cloneJson(value)
253
275
  return result
@@ -269,6 +291,7 @@ export function installAcpSessionConfig(ctx) {
269
291
  if (finalizing) return finalizing
270
292
  desired = cloneJson(value)
271
293
  const settle = async () => {
294
+ assertSession(originalSession, originalGeneration)
272
295
  if (!Object.is(applied, desired)) await write(desired)
273
296
  settled = true
274
297
  return listFor(transactionSessionId)
@@ -101,12 +101,22 @@ export function installAcpSessionPlan(ctx) {
101
101
  }
102
102
 
103
103
  function observeClient(message) {
104
- if (!isObject(message) || message.id === undefined || !SETUP_METHODS.has(message.method)) return
104
+ if (!isObject(message) || message.id === undefined) return
105
+ if (message.method === 'initialize') {
106
+ pending.clear()
107
+ plansBySession.clear()
108
+ sessionId = undefined
109
+ selectionKnown = false
110
+ publish()
111
+ return
112
+ }
113
+ if (!SETUP_METHODS.has(message.method)) return
105
114
  pending.set(message.id, {
106
115
  sessionId: message.method !== 'session/new'
107
116
  ? readString(message.params, 'sessionId', 'session_id')
108
117
  : undefined,
109
118
  })
119
+ if (message.method === 'session/load') reset(readString(message.params, 'sessionId', 'session_id'))
110
120
  }
111
121
 
112
122
  function observeAgent(message) {
@@ -117,7 +127,9 @@ export function installAcpSessionPlan(ctx) {
117
127
  if (message.error !== undefined || !isObject(message.result)) return
118
128
  const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
119
129
  if (!selectionKnown) sessionId = bound
120
- reset(bound)
130
+ // Replay notifications precede the setup response. Preserve their state.
131
+ if (!plansBySession.has(bound)) reset(bound)
132
+ else if (bound === sessionId) publish()
121
133
  return
122
134
  }
123
135
  if (message.method !== 'session/update' || !isObject(message.params)) return
@@ -114,12 +114,24 @@ export function installAcpSessionStats(ctx, options = {}) {
114
114
 
115
115
  function observeClient(message) {
116
116
  if (!object(message) || message.id === undefined || typeof message.method !== 'string') return
117
+ if (message.method === 'initialize') {
118
+ pendingSetup.clear()
119
+ pendingPrompts.clear()
120
+ activePrompts.clear()
121
+ toolStarts.clear()
122
+ values.clear()
123
+ sessionId = undefined
124
+ selectionKnown = false
125
+ publish()
126
+ return
127
+ }
117
128
  if (SETUP_METHODS.has(message.method)) {
118
129
  pendingSetup.set(message.id, {
119
130
  sessionId: message.method !== 'session/new'
120
131
  ? readString(message.params, 'sessionId', 'session_id')
121
132
  : undefined,
122
133
  })
134
+ if (message.method === 'session/load') reset(readString(message.params, 'sessionId', 'session_id'))
123
135
  return
124
136
  }
125
137
  if (message.method !== 'session/prompt') return
@@ -128,13 +140,16 @@ export function installAcpSessionStats(ctx, options = {}) {
128
140
  if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
129
141
  const prompt = {
130
142
  sessionId: promptSessionId,
143
+ primary: !activePrompts.has(promptSessionId),
131
144
  started: now(),
132
145
  firstToken: undefined,
133
146
  toolMillis: 0,
134
147
  }
135
148
  pendingPrompts.set(message.id, prompt)
136
- activePrompts.set(promptSessionId, prompt)
137
- stateFor(promptSessionId).stats.turns += 1
149
+ if (prompt.primary) {
150
+ activePrompts.set(promptSessionId, prompt)
151
+ stateFor(promptSessionId).stats.turns += 1
152
+ }
138
153
  publishIf(promptSessionId)
139
154
  }
140
155
 
@@ -146,19 +161,21 @@ export function installAcpSessionStats(ctx, options = {}) {
146
161
  if (message.error !== undefined || !object(message.result)) return
147
162
  const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
148
163
  if (!selectionKnown) sessionId = bound
149
- reset(bound)
164
+ // Replay notifications precede the setup response. Preserve their state.
165
+ if (!values.has(bound)) reset(bound)
166
+ else if (bound === sessionId) publish()
150
167
  return
151
168
  }
152
169
  if (message.id !== undefined && pendingPrompts.has(message.id)) {
153
170
  const prompt = pendingPrompts.get(message.id)
154
171
  pendingPrompts.delete(message.id)
155
- activePrompts.delete(prompt.sessionId)
172
+ if (activePrompts.get(prompt.sessionId) === prompt) activePrompts.delete(prompt.sessionId)
156
173
  const value = stateFor(prompt.sessionId)
157
174
  if (message.error === undefined && object(message.result)) {
158
175
  addUsage(value, message.result.usage)
159
176
  }
160
177
  const elapsed = Math.max(0, now() - prompt.started)
161
- value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
178
+ if (prompt.primary) value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
162
179
  publishIf(prompt.sessionId)
163
180
  return
164
181
  }
@@ -2,7 +2,7 @@
2
2
  * Standard-ACP-backed run-state projection for the Client tree.
3
3
  *
4
4
  * Folds the non-statistics facts `/status` needs — connection, server,
5
- * authenticate state, session binding, model, effort, permission, plan,
5
+ * authenticate state, session binding/use, model, effort, permission, plan,
6
6
  * agent preset, and the running/idle state — from messages the mux already
7
7
  * observes. Statistics stay in `acpSessionStats`; this service never
8
8
  * counts tokens or timings, so there is exactly one stats source.
@@ -43,7 +43,7 @@ class AcpSessionStatusService extends Service {
43
43
  function zeroSession(sessionId, bound = sessionId !== undefined) {
44
44
  return {
45
45
  state: 'idle',
46
- session: { sessionId, bound },
46
+ session: { sessionId, bound, started: false },
47
47
  model: undefined,
48
48
  effort: undefined,
49
49
  permission: undefined,
@@ -68,6 +68,8 @@ export function installAcpSessionStatus(ctx, options = {}) {
68
68
  let server
69
69
  const auth = { status: undefined, method: undefined }
70
70
  let initializeId
71
+ let authMethods = []
72
+ const pendingRequests = new Set()
71
73
  const pendingAuthenticate = new Set()
72
74
  const pendingSetup = new Map()
73
75
  const pendingPrompts = new Map()
@@ -131,22 +133,43 @@ export function installAcpSessionStatus(ctx, options = {}) {
131
133
  function observeClient(message) {
132
134
  if (!object(message) || typeof message.method !== 'string') return
133
135
  if (message.method === 'initialize' && message.id !== undefined) {
136
+ pendingRequests.clear()
137
+ sessions.clear()
138
+ Object.assign(fallback, zeroSession(undefined, false))
139
+ delete fallback.error
140
+ sessionId = undefined
141
+ selectionKnown = false
142
+ connection = 'connecting'
143
+ server = undefined
144
+ auth.status = undefined
145
+ auth.method = undefined
146
+ delete auth.message
147
+ authMethods = []
148
+ pendingAuthenticate.clear()
149
+ pendingSetup.clear()
150
+ pendingPrompts.clear()
134
151
  initializeId = message.id
152
+ publish()
135
153
  return
136
154
  }
155
+ if (message.id !== undefined) pendingRequests.add(message.id)
137
156
  if (message.method === 'authenticate' && message.id !== undefined) {
138
157
  pendingAuthenticate.add(message.id)
158
+ const methodId = readString(message.params, 'methodId')
159
+ const method = authMethods.find((candidate) => candidate?.id === methodId)
160
+ if (methodId !== undefined) auth.method = readString(method, 'name', 'label') ?? methodId
161
+ delete auth.message
139
162
  auth.status = 'signing in'
140
163
  publish()
141
164
  return
142
165
  }
143
166
  if (SETUP_METHODS.has(message.method) && message.id !== undefined) {
144
- pendingSetup.set(
145
- message.id,
146
- message.method !== 'session/new'
167
+ pendingSetup.set(message.id, {
168
+ sessionId: message.method !== 'session/new'
147
169
  ? readString(message.params, 'sessionId', 'session_id')
148
170
  : undefined,
149
- )
171
+ started: message.method !== 'session/new',
172
+ })
150
173
  return
151
174
  }
152
175
  if (message.method === 'session/prompt' && message.id !== undefined) {
@@ -155,6 +178,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
155
178
  if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
156
179
  pendingPrompts.set(message.id, promptSessionId)
157
180
  const value = stateFor(promptSessionId)
181
+ value.session.started = true
158
182
  if (value.state === 'idle') {
159
183
  value.state = 'starting'
160
184
  publishIf(promptSessionId)
@@ -166,42 +190,61 @@ export function installAcpSessionStatus(ctx, options = {}) {
166
190
  if (!object(message)) return
167
191
  if (message.id !== undefined && message.id === initializeId) {
168
192
  initializeId = undefined
169
- connection = 'attached'
193
+ connection = message.error === undefined ? 'attached' : 'failed'
194
+ fallback.error = message.error?.message
170
195
  const result = object(message.result) ? message.result : undefined
171
196
  server = readString(result?.agentInfo, 'name')
172
197
  const methods = Array.isArray(result?.authMethods) ? result.authMethods : []
173
- const method = methods.find((candidate) => object(candidate)
174
- && typeof candidate.id === 'string' && candidate.id.length > 0)
175
- if (method !== undefined && auth.status === undefined) {
176
- auth.method = readString(method, 'name', 'label') ?? method.id
177
- }
198
+ authMethods = methods
199
+ // initialize advertises choices, not the credential used by this process.
178
200
  publish()
179
201
  return
180
202
  }
203
+ // Unknown replies can belong to a replaced Agent. Never let them restore
204
+ // its authentication or run state in the new connection.
205
+ if (response(message) && !pendingRequests.delete(message.id)) return
181
206
  if (message.id !== undefined && pendingAuthenticate.has(message.id)) {
182
207
  pendingAuthenticate.delete(message.id)
183
208
  if (message.error === undefined) {
184
209
  auth.status = 'configured'
185
- } else if (isAuthRequired(message.error)) {
186
- auth.status = 'needs sign-in'
210
+ delete auth.message
187
211
  } else {
188
- auth.status = undefined
212
+ auth.status = 'sign-in failed'
213
+ auth.message = readString(message.error?.data, 'details', 'message')
214
+ ?? readString(message.error, 'message') ?? 'Authentication failed'
189
215
  }
190
216
  publish()
191
217
  return
192
218
  }
193
219
  if (message.id !== undefined && pendingSetup.has(message.id)) {
194
- const requested = pendingSetup.get(message.id)
220
+ const setup = pendingSetup.get(message.id)
195
221
  pendingSetup.delete(message.id)
196
222
  if (message.error !== undefined) {
197
223
  if (isAuthRequired(message.error)) auth.status = 'needs sign-in'
198
- if (requested !== undefined) {
199
- stateFor(requested, false).session.bound = false
224
+ else {
225
+ connection = 'failed'
226
+ const value = stateFor(setup.sessionId, false)
227
+ value.error = readString(message.error, 'message') ?? 'Session setup failed'
228
+ value.state = 'idle'
229
+ value.model = undefined
230
+ value.effort = undefined
200
231
  }
201
- if (!selectionKnown) sessionId = requested
232
+ stateFor(setup.sessionId, false).session = {
233
+ sessionId: setup.sessionId, bound: false, started: false,
234
+ }
235
+ if (!selectionKnown) sessionId = setup.sessionId
202
236
  } else {
203
- const bound = readString(message.result, 'sessionId', 'session_id') ?? requested
204
- if (bound !== undefined) stateFor(bound).session = { sessionId: bound, bound: true }
237
+ connection = 'attached'
238
+ delete fallback.error
239
+ const bound = readString(message.result, 'sessionId', 'session_id') ?? setup.sessionId
240
+ if (bound !== undefined) {
241
+ stateFor(bound).session = { sessionId: bound, bound: true, started: setup.started }
242
+ delete stateFor(bound).error
243
+ if (authMethods.length > 0) {
244
+ auth.status = 'configured'
245
+ delete auth.message
246
+ }
247
+ }
205
248
  if (!selectionKnown) sessionId = bound
206
249
  }
207
250
  publish()
@@ -304,14 +347,16 @@ export function installAcpSessionStatus(ctx, options = {}) {
304
347
  ? snapshot.sessionId
305
348
  : sessionId
306
349
  const value = stateFor(configSessionId)
307
- value.model = optionValue(snapshot.options, 'model') ?? value.model
308
- value.effort = optionValue(snapshot.options, 'effort') ?? value.effort
350
+ if (!selectionKnown && sessionId === undefined) sessionId = configSessionId
351
+ value.model = optionValue(snapshot.options, 'model', 'model')
352
+ value.effort = optionValue(snapshot.options, 'thought_level', 'effort')
309
353
  publishIf(configSessionId)
310
354
  }
311
355
 
312
- function optionValue(options, id) {
356
+ function optionValue(options, category, id) {
313
357
  if (!Array.isArray(options)) return undefined
314
- const option = options.find((candidate) => candidate?.id === id)
358
+ const option = options.find((candidate) => candidate?.category === category)
359
+ ?? options.find((candidate) => candidate?.id === id)
315
360
  if (option === undefined) return undefined
316
361
  const raw = option.currentValue ?? option.current_value
317
362
  return typeof raw === 'string' ? raw : undefined
package/lib/agent.js CHANGED
@@ -6,11 +6,12 @@ import { homedir } from 'node:os'
6
6
  import { dirname, join, resolve } from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
8
  import { selectedHarness } from './harnesses.js'
9
+ import { tokenizeCommandArgs } from './command-args.js'
9
10
 
10
11
  /**
11
12
  * Resolve the ACP package and TUI's internal Creator overlay bundle.
12
13
  * @param {string | URL} [anchor]
13
- * @returns {{ command: string, args: string[] }}
14
+ * @returns {{ command: string, args: string[], env?: Record<string, string> }}
14
15
  */
15
16
  export function resolveDependencyStack(anchor = import.meta.url) {
16
17
  const req = createRequire(anchor)
@@ -31,20 +32,39 @@ export function resolveDependencyStack(anchor = import.meta.url) {
31
32
 
32
33
  /**
33
34
  * Resolve the standalone ACP command without affecting the profile path.
35
+ * `forcedHarness` is an internal product initialization value, not a CLI
36
+ * argument; null/omitted means that no product Harness is forced.
34
37
  * @param {string | URL} [anchor]
35
- * @param {{ settingsPath?: string }} [options]
38
+ * @param {{ settingsPath?: string, forcedHarness?: { id: string, label: string, command: string, args?: string[] } | null }} [options]
36
39
  * @returns {{ command: string, args: string[] }}
37
40
  */
38
41
  export function resolveStackedAgent(anchor = import.meta.url, options = {}) {
39
42
  const envCmd = process.env.DSH_TUI_AGENT
40
43
  if (typeof envCmd === 'string' && envCmd.trim().length > 0) {
41
- const tokens = envCmd.trim().split(/\s+/)
44
+ const tokens = tokenizeCommandArgs(envCmd)
45
+ if (!tokens[0]) throw new Error('DSH_TUI_AGENT needs a non-empty command')
42
46
  return { command: tokens[0], args: tokens.slice(1) }
43
47
  }
48
+ if (options.forcedHarness !== undefined && options.forcedHarness !== null) {
49
+ const configured = options.forcedHarness
50
+ if (typeof configured !== 'object' || Array.isArray(configured)
51
+ || typeof configured.command !== 'string' || configured.command.trim().length === 0) {
52
+ throw new Error('forcedHarness must declare a non-empty command')
53
+ }
54
+ return {
55
+ command: configured.command,
56
+ args: Array.isArray(configured.args) ? configured.args.map(String) : [],
57
+ ...(configured.env !== undefined ? { env: { ...configured.env } } : {}),
58
+ }
59
+ }
44
60
  if (typeof options.settingsPath === 'string') {
45
61
  const selected = selectedHarness(options.settingsPath)
46
62
  if (selected !== undefined) {
47
- return { command: selected.command, args: selected.args }
63
+ return {
64
+ command: selected.command,
65
+ args: selected.args,
66
+ ...(selected.env !== undefined ? { env: { ...selected.env } } : {}),
67
+ }
48
68
  }
49
69
  }
50
70
  try {
package/lib/boot.js CHANGED
@@ -67,6 +67,8 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
67
67
  * @param {{ stdin: import('node:stream').Writable, stdout: import('node:stream').Readable, child?: import('node:child_process').ChildProcess }} [options.stream]
68
68
  * @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
69
69
  * @param {string} [options.settingsPath]
70
+ * Internal product-only forced Harness; null means no pinned Harness.
71
+ * @param {{ id: string, label: string, command: string, args?: string[] } | null} [options.forcedHarness]
70
72
  * @param {string} [options.artifactRoot]
71
73
  * @param {Array<{ id: string, label: string, command: string, args?: string[], source?: string }>} [options.harnessDefaults]
72
74
  * @param {string} [options.harnessPathValue]
@@ -75,10 +77,16 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
75
77
  export async function bootClient(options = {}) {
76
78
  const { Context } = await import('@deepseek-ai/cordis')
77
79
  const ctx = new Context()
80
+ const settingsPath = options.settingsPath ?? uiSettingsPath(options.extraArgs ?? [])
81
+ const forcedHarness = options.forcedHarness ?? null
78
82
  const acpConfig = options.stream !== undefined
79
83
  ? { stream: options.stream }
80
- : { agent: options.agent ?? resolveStackedAgent() }
81
- const settingsPath = options.settingsPath ?? uiSettingsPath(options.extraArgs ?? [])
84
+ : {
85
+ agent: options.agent ?? resolveStackedAgent(import.meta.url, {
86
+ settingsPath,
87
+ forcedHarness,
88
+ }),
89
+ }
82
90
  if (options.settingsPath === undefined) {
83
91
  migrateLegacyUiSettings(settingsPath, legacyUiSettingsPaths(options.extraArgs ?? []))
84
92
  }
@@ -96,9 +104,16 @@ export async function bootClient(options = {}) {
96
104
  harnessDefaults = []
97
105
  }
98
106
  }
107
+ if (forcedHarness !== null) {
108
+ const forcedEntry = { ...forcedHarness, source: 'forced' }
109
+ const forcedIndex = harnessDefaults.findIndex(({ id }) => id === forcedHarness.id)
110
+ if (forcedIndex === -1) harnessDefaults = [forcedEntry, ...harnessDefaults]
111
+ else harnessDefaults = harnessDefaults.map((entry, index) => index === forcedIndex ? forcedEntry : entry)
112
+ }
99
113
  const harnessConfig = {
100
114
  settingsPath,
101
115
  defaults: harnessDefaults,
116
+ forcedHarness,
102
117
  pathValue: options.harnessPathValue,
103
118
  hostOwned: options.stream !== undefined,
104
119
  }
@@ -292,10 +307,12 @@ export function migrateLegacyUiSettings(settingsPath, legacyPaths) {
292
307
 
293
308
  /**
294
309
  * Parse `--agent` / `--agent-arg` from argv. Remaining flags pass through to Rust.
310
+ * `forcedHarness` is an internal product initialization value, not a CLI
311
+ * argument; null/omitted means that no product Harness is forced.
295
312
  * `--agent` is also forwarded via {@link painterArgs} so Terminal Auth can
296
313
  * re-exec the same command.
297
314
  * @param {string[]} argv
298
- * @param {{ settingsPath?: string }} [options]
315
+ * @param {{ settingsPath?: string, forcedHarness?: { id: string, label: string, command: string, args?: string[] } | null }} [options]
299
316
  * @returns {{ agent: { command: string, args: string[] }, rustArgs: string[] }}
300
317
  */
301
318
  export function parseClientArgv(argv, options = {}) {
@@ -305,12 +322,16 @@ export function parseClientArgv(argv, options = {}) {
305
322
  for (let i = 0; i < argv.length; i += 1) {
306
323
  const token = argv[i]
307
324
  if (token === '--agent') {
325
+ if (!argv[i + 1]?.trim() || argv[i + 1].startsWith('--')) {
326
+ throw Object.assign(new Error('--agent needs a value'), { exitCode: 2 })
327
+ }
308
328
  command = argv[i + 1]
309
329
  i += 1
310
330
  continue
311
331
  }
312
332
  if (token === '--agent-arg') {
313
- args.push(argv[i + 1] ?? '')
333
+ if (argv[i + 1] === undefined) throw Object.assign(new Error('--agent-arg needs a value'), { exitCode: 2 })
334
+ args.push(argv[i + 1])
314
335
  i += 1
315
336
  continue
316
337
  }
@@ -319,7 +340,10 @@ export function parseClientArgv(argv, options = {}) {
319
340
  if (command === undefined) {
320
341
  const settingsPath = options.settingsPath ?? uiSettingsPath(rustArgs)
321
342
  return {
322
- agent: resolveStackedAgent(import.meta.url, { settingsPath }),
343
+ agent: resolveStackedAgent(import.meta.url, {
344
+ settingsPath,
345
+ forcedHarness: options.forcedHarness,
346
+ }),
323
347
  rustArgs,
324
348
  }
325
349
  }
@@ -0,0 +1,28 @@
1
+ /** Split an argv string without shell execution, expansion, or substitution. */
2
+ export function tokenizeCommandArgs(value) {
3
+ const tokens = []
4
+ let token = '', quote
5
+ let escaped = false, started = false
6
+ for (const char of String(value ?? '')) {
7
+ if (escaped) {
8
+ if (quote === '"' && !['\\', '"', '$', '`', '\n'].includes(char)) token += '\\'
9
+ token += char
10
+ escaped = false
11
+ } else if (char === '\\' && quote !== "'") {
12
+ escaped = true; started = true
13
+ } else if (quote !== undefined) {
14
+ if (char === quote) quote = undefined
15
+ else token += char
16
+ } else if (char === '"' || char === "'") {
17
+ quote = char; started = true
18
+ } else if (/\s/.test(char)) {
19
+ if (started) { tokens.push(token); token = ''; started = false }
20
+ } else {
21
+ token += char; started = true
22
+ }
23
+ }
24
+ if (escaped) token += '\\'
25
+ if (quote !== undefined) throw Object.assign(new Error(`Unclosed ${quote} quote in command`), { exitCode: 2 })
26
+ if (started) tokens.push(token)
27
+ return tokens
28
+ }