dsh-gitbash-shell 0.42.2 → 0.43.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/CHANGELOG.md +183 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -3
- package/src/bash-render.js +1 -1
- package/src/bash-tool.js +1 -1
- package/src/client.js +210 -22
- package/src/fastctx-bridge.js +124 -54
- package/src/index.js +46 -11
- package/src/native-executor.js +2 -1
- package/src/native-tool.js +5 -2
- package/src/tool-runner.js +245 -44
- package/src/toolchain.js +16 -10
package/src/fastctx-bridge.js
CHANGED
|
@@ -117,6 +117,8 @@ export class McpStdio {
|
|
|
117
117
|
/** Spawn the server and run the MCP initialize handshake. */
|
|
118
118
|
async start() {
|
|
119
119
|
const { spawn } = await import('node:child_process')
|
|
120
|
+
// close() can run while the dynamic import is pending; never resurrect it.
|
|
121
|
+
if (this.dead) throw new Error('the FastCtx bridge was stopped')
|
|
120
122
|
this.child = spawn(this.file, this.args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
|
121
123
|
this.child.stdout.setEncoding('utf8')
|
|
122
124
|
this.child.stderr.setEncoding('utf8')
|
|
@@ -323,43 +325,102 @@ const FASTCTX_SANDBOX_NOTE = 'FastCtx reads the real filesystem with no workspac
|
|
|
323
325
|
* - echo(text) -> result dialect echo, or undefined (off)
|
|
324
326
|
* - log(message) -> the plugin's tagged logger
|
|
325
327
|
*
|
|
326
|
-
* The surface publishes at the
|
|
327
|
-
*
|
|
328
|
-
* the rest of the process
|
|
329
|
-
*
|
|
328
|
+
* The surface publishes at the first agent creation or live switch update.
|
|
329
|
+
* User OFF/ON is reversible; dsh-ops appearing (ops_* tools in the face)
|
|
330
|
+
* independently withdraws gb_* for the rest of the process. Every call also
|
|
331
|
+
* checks the live switch and its publication generation, including old SDKs.
|
|
330
332
|
*/
|
|
331
333
|
export function mountFastctx(ctx, deps) {
|
|
332
334
|
const { platform, enabled, createRequireFrom, tools, msysToWin, echo, log } = deps
|
|
333
335
|
if (platform !== 'win32') return
|
|
334
336
|
let disposer = undefined
|
|
335
337
|
let client = undefined
|
|
336
|
-
let
|
|
338
|
+
let unavailable = false
|
|
337
339
|
let withdrawnByOps = false
|
|
340
|
+
let switchOn
|
|
341
|
+
let generation = 0
|
|
338
342
|
let closed = false
|
|
339
343
|
let publishing
|
|
340
344
|
let connecting
|
|
341
345
|
const starting = new Set()
|
|
346
|
+
const pending = new Set()
|
|
347
|
+
const agents = new Set()
|
|
348
|
+
|
|
349
|
+
const track = (promise) => {
|
|
350
|
+
pending.add(promise)
|
|
351
|
+
promise.then(() => pending.delete(promise), () => pending.delete(promise))
|
|
352
|
+
return promise
|
|
353
|
+
}
|
|
342
354
|
|
|
343
355
|
const unpublish = (reason) => {
|
|
344
356
|
if (disposer) {
|
|
345
|
-
|
|
357
|
+
const dispose = disposer
|
|
346
358
|
disposer = undefined
|
|
359
|
+
try { dispose() } catch { /* already gone */ }
|
|
347
360
|
}
|
|
348
361
|
if (reason) log(reason)
|
|
349
362
|
}
|
|
350
363
|
|
|
351
|
-
const
|
|
352
|
-
|
|
364
|
+
const stopConnections = () => {
|
|
365
|
+
const owned = new Set([...starting, client].filter(Boolean))
|
|
366
|
+
client = undefined
|
|
367
|
+
// Calling close immediately rejects pending requests and kills the child;
|
|
368
|
+
// track completion as well so disposal owns even superseded generations.
|
|
369
|
+
track(Promise.allSettled([...owned].map((connection) => connection.close())))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const live = (agent) => {
|
|
373
|
+
if (closed) return false
|
|
374
|
+
const next = !!enabled()
|
|
375
|
+
if (switchOn !== next) {
|
|
376
|
+
switchOn = next
|
|
377
|
+
generation++
|
|
378
|
+
unavailable = false
|
|
379
|
+
if (!next) {
|
|
380
|
+
unpublish('FastCtx bridge disabled by the fastctxTools switch')
|
|
381
|
+
stopConnections()
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!withdrawnByOps) {
|
|
385
|
+
const registry = tools(ctx)
|
|
386
|
+
const faces = new Set(agent ? [agent, ...agents] : agents.size ? agents : [undefined])
|
|
387
|
+
for (const face of faces) {
|
|
388
|
+
const names = (registry?.schemas?.(face) ?? []).map((schema) => schema?.name)
|
|
389
|
+
// Any ops_* name means dsh-ops owns the file surface. Matching the
|
|
390
|
+
// prefix (rather than only the listed markers) covers the 0.4.0
|
|
391
|
+
// progressive residents AND the names the marker list does not carry
|
|
392
|
+
// yet (ops_batch / ops_glob / ops_replace / ops_run): a later caller
|
|
393
|
+
// can hand the face a name that was never resident in face 1.
|
|
394
|
+
if (names.some((name) => typeof name === 'string'
|
|
395
|
+
&& (name.startsWith('ops_') || OPS_PRESENCE_MARKERS.includes(name)))) {
|
|
396
|
+
withdrawnByOps = true
|
|
397
|
+
generation++
|
|
398
|
+
unpublish('dsh-ops detected (ops_* tools present): the gb_* surface stands down')
|
|
399
|
+
stopConnections()
|
|
400
|
+
break
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return switchOn && !withdrawnByOps
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const assertLive = (owner, agent) => {
|
|
408
|
+
if (!live(agent) || owner !== generation || !disposer) throw new Error('the FastCtx bridge was stopped')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const callWithReconnect = async (rawName, args, owner, agent) => {
|
|
412
|
+
assertLive(owner, agent)
|
|
353
413
|
if (client === undefined || client.isDead) {
|
|
354
|
-
if (!connecting) {
|
|
355
|
-
|
|
414
|
+
if (!connecting || connecting.owner !== owner) {
|
|
415
|
+
const task = { owner }
|
|
416
|
+
task.promise = track((async () => {
|
|
356
417
|
const { file } = resolveFastctx(ctx, createRequireFrom)
|
|
357
418
|
if (file === '') throw new Error('the FastCtx payload is not installed; repair the profile dependencies (frozen install) and retry')
|
|
358
419
|
const fresh = new McpStdio({ file, log })
|
|
359
420
|
starting.add(fresh)
|
|
360
421
|
try {
|
|
361
422
|
await fresh.start()
|
|
362
|
-
|
|
423
|
+
assertLive(owner, agent)
|
|
363
424
|
client = fresh
|
|
364
425
|
} catch (error) {
|
|
365
426
|
await fresh.close()
|
|
@@ -367,23 +428,25 @@ export function mountFastctx(ctx, deps) {
|
|
|
367
428
|
} finally {
|
|
368
429
|
starting.delete(fresh)
|
|
369
430
|
}
|
|
370
|
-
})().finally(() => { connecting = undefined })
|
|
431
|
+
})().finally(() => { if (connecting === task) connecting = undefined }))
|
|
432
|
+
connecting = task
|
|
371
433
|
}
|
|
372
|
-
await connecting
|
|
434
|
+
await connecting.promise
|
|
373
435
|
}
|
|
374
|
-
|
|
436
|
+
assertLive(owner, agent)
|
|
437
|
+
// Reconnect only BEFORE dispatch. Never replay a failed tools/call: it may
|
|
438
|
+
// already have written files even when its response is an ordinary error.
|
|
375
439
|
return client.callTool(rawName, args)
|
|
376
440
|
}
|
|
377
441
|
|
|
378
|
-
const publishOnce = async () => {
|
|
379
|
-
if (
|
|
442
|
+
const publishOnce = async (owner) => {
|
|
443
|
+
if (!live() || owner !== generation || disposer || unavailable) return
|
|
380
444
|
const registry = tools(ctx)
|
|
381
445
|
if (!registry || typeof registry.register !== 'function') return
|
|
382
446
|
const { file, source } = resolveFastctx(ctx, createRequireFrom)
|
|
383
447
|
if (file === '') {
|
|
384
448
|
log('FastCtx payload not found (probed ' + PAYLOAD_PACKAGES.map((p) => p.pkg).join(', ') + '); gb_* tools stay unpublished')
|
|
385
|
-
|
|
386
|
-
withdrawnByOps = true // nothing to publish either way; stop re-deciding
|
|
449
|
+
unavailable = true
|
|
387
450
|
return
|
|
388
451
|
}
|
|
389
452
|
const fresh = new McpStdio({ file, log })
|
|
@@ -391,23 +454,30 @@ export function mountFastctx(ctx, deps) {
|
|
|
391
454
|
let toolList
|
|
392
455
|
try {
|
|
393
456
|
await fresh.start()
|
|
457
|
+
if (!live() || owner !== generation) return
|
|
394
458
|
toolList = await fresh.listTools()
|
|
395
459
|
} catch (error) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
460
|
+
if (live() && owner === generation) {
|
|
461
|
+
log(`FastCtx server start failed (${source}): ${error?.message ?? error}; gb_* tools stay unpublished`)
|
|
462
|
+
unavailable = true
|
|
463
|
+
}
|
|
400
464
|
return
|
|
401
465
|
} finally {
|
|
466
|
+
try { await fresh.close() } catch { /* best effort */ }
|
|
402
467
|
starting.delete(fresh)
|
|
403
468
|
}
|
|
404
|
-
|
|
405
|
-
if (closed || withdrawnByOps) return
|
|
469
|
+
if (!live() || owner !== generation) return
|
|
406
470
|
// Discovery never overwrites the independently owned lazy call client.
|
|
407
471
|
const wanted = toolList.filter((tool) => FASTCTX_FILE_TOOLS.includes(String(tool?.name)))
|
|
408
472
|
const registered = []
|
|
409
473
|
const registeredNames = []
|
|
474
|
+
const release = () => {
|
|
475
|
+
for (const dispose of registered) {
|
|
476
|
+
try { dispose() } catch (error) { log(`FastCtx unregister failed: ${error?.message ?? error}`) }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
410
479
|
for (const tool of wanted) {
|
|
480
|
+
if (!live() || owner !== generation) break
|
|
411
481
|
const rawName = String(tool.name)
|
|
412
482
|
const schema = tool.inputSchema && typeof tool.inputSchema === 'object' ? tool.inputSchema : { type: 'object', properties: {} }
|
|
413
483
|
// Ignore-rule transparency (v0.38.5, the field smoke report): the three
|
|
@@ -438,6 +508,7 @@ export function mountFastctx(ctx, deps) {
|
|
|
438
508
|
render: (_args, value) => renderTextContent(value),
|
|
439
509
|
},
|
|
440
510
|
async execute(args, execution) {
|
|
511
|
+
assertLive(owner, execution?.agent)
|
|
441
512
|
// Preserve v0.39.1 / #21: glob accepts real string arrays,
|
|
442
513
|
// while stringified arrays and malformed patterns fail loudly.
|
|
443
514
|
if (args !== null && typeof args === 'object' && 'pattern' in args) {
|
|
@@ -445,7 +516,7 @@ export function mountFastctx(ctx, deps) {
|
|
|
445
516
|
if (invalid) throw new Error(invalid)
|
|
446
517
|
}
|
|
447
518
|
const translated = translateFastctxArgs(args, msysToWin ? (path) => msysToWin(path, execution) : undefined)
|
|
448
|
-
const result = await callWithReconnect(rawName, translated)
|
|
519
|
+
const result = await callWithReconnect(rawName, translated, owner, execution?.agent)
|
|
449
520
|
const text = renderFastctxResult(rawName, result, echo ? (value) => echo(value, execution) : undefined)
|
|
450
521
|
// The host treats a returned value as success; MCP failures must
|
|
451
522
|
// throw, just as its official MCP client does.
|
|
@@ -461,52 +532,51 @@ export function mountFastctx(ctx, deps) {
|
|
|
461
532
|
log(`${gbName(rawName)} registration failed: ${error?.message ?? error}`)
|
|
462
533
|
}
|
|
463
534
|
}
|
|
535
|
+
if (!live() || owner !== generation) {
|
|
536
|
+
release()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
464
539
|
if (registered.length === 0) {
|
|
465
540
|
log('FastCtx published none of the known file tools; nothing registered')
|
|
466
|
-
|
|
467
|
-
withdrawnByOps = true
|
|
541
|
+
unavailable = true
|
|
468
542
|
return
|
|
469
543
|
}
|
|
470
|
-
disposer =
|
|
544
|
+
disposer = release
|
|
471
545
|
log(`FastCtx bridge active (${source}): ${registeredNames.join(', ')}`)
|
|
472
546
|
}
|
|
473
547
|
|
|
474
548
|
const publish = () => {
|
|
475
|
-
if (!publishing
|
|
476
|
-
|
|
549
|
+
if (!publishing || publishing.owner !== generation) {
|
|
550
|
+
const task = { owner: generation }
|
|
551
|
+
task.promise = track(publishOnce(task.owner).finally(() => { if (publishing === task) publishing = undefined }))
|
|
552
|
+
publishing = task
|
|
553
|
+
}
|
|
554
|
+
return publishing.promise
|
|
477
555
|
}
|
|
478
556
|
|
|
479
557
|
const reconcile = (agent) => {
|
|
480
|
-
if (closed) return
|
|
481
|
-
if (!enabled()) { unpublish('FastCtx bridge disabled by the fastctxTools switch'); decided = true; withdrawnByOps = true; return }
|
|
482
|
-
if (decided && withdrawnByOps) return
|
|
483
|
-
let names = []
|
|
484
558
|
try {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
withdrawnByOps = true
|
|
490
|
-
decided = true
|
|
491
|
-
unpublish('dsh-ops detected (ops_* tools present): the gb_* surface stands down')
|
|
492
|
-
}
|
|
493
|
-
return
|
|
559
|
+
if (!live(agent)) return
|
|
560
|
+
void publish().catch((error) => log(`FastCtx bridge mount failed: ${error?.message ?? error}`))
|
|
561
|
+
} catch (error) {
|
|
562
|
+
log(`FastCtx bridge reconcile failed: ${error?.message ?? error}`)
|
|
494
563
|
}
|
|
495
|
-
void publish().catch((error) => log(`FastCtx bridge mount failed: ${error?.message ?? error}`))
|
|
496
564
|
}
|
|
497
565
|
|
|
498
|
-
|
|
499
|
-
ctx.on('agent/created', ({ agent } = {}) => { try { reconcile(agent) } catch { /* never break agent creation */ } })
|
|
500
|
-
} catch (error) {
|
|
501
|
-
log(`FastCtx bridge wiring failed: ${error?.message ?? error}`)
|
|
502
|
-
return
|
|
503
|
-
}
|
|
566
|
+
// Install disposal before listeners: partial wiring still owns every child.
|
|
504
567
|
ctx.effect(() => async () => {
|
|
505
568
|
closed = true
|
|
569
|
+
generation++
|
|
570
|
+
agents.clear()
|
|
506
571
|
unpublish()
|
|
507
|
-
|
|
508
|
-
await Promise.allSettled([...
|
|
509
|
-
await Promise.allSettled([publishing, connecting].filter(Boolean))
|
|
510
|
-
client = undefined
|
|
572
|
+
stopConnections()
|
|
573
|
+
await Promise.allSettled([...pending])
|
|
511
574
|
}, 'dsh-gitbash-shell: FastCtx bridge')
|
|
575
|
+
try {
|
|
576
|
+
ctx.on('agent/created', ({ agent } = {}) => { if (!closed && agent) agents.add(agent); reconcile(agent) })
|
|
577
|
+
ctx.on('agent/disposed', ({ agent } = {}) => { agents.delete(agent) })
|
|
578
|
+
ctx.on('loader/volatile-update', () => { reconcile() })
|
|
579
|
+
} catch (error) {
|
|
580
|
+
log(`FastCtx bridge wiring failed: ${error?.message ?? error}`)
|
|
581
|
+
}
|
|
512
582
|
}
|
package/src/index.js
CHANGED
|
@@ -308,7 +308,7 @@ import {
|
|
|
308
308
|
restoreOpsBash,
|
|
309
309
|
} from './ops-adopt.js'
|
|
310
310
|
import {
|
|
311
|
-
clearToolJob, currentToolJob, defaultRunnerIo, fenceToolRequest,
|
|
311
|
+
cancelToolJob, clearToolJob, currentToolJob, defaultRunnerIo, fenceToolRequest,
|
|
312
312
|
probeToolchain, readJsonBody, startToolJob,
|
|
313
313
|
} from './tool-runner.js'
|
|
314
314
|
|
|
@@ -766,7 +766,12 @@ function pathEcho(value, env) {
|
|
|
766
766
|
const out = driveToMsys(value)
|
|
767
767
|
if (typeof out !== 'string' || out.length === 0) return out
|
|
768
768
|
const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
|
|
769
|
-
|
|
769
|
+
// A whole path has exactly one root; an embedded copy in a backup tree is data.
|
|
770
|
+
if (tempRoot && out.slice(0, tempRoot.length).toLowerCase() === tempRoot.toLowerCase()
|
|
771
|
+
&& (out.length === tempRoot.length || out[tempRoot.length] === '/')) {
|
|
772
|
+
return '/tmp' + out.slice(tempRoot.length)
|
|
773
|
+
}
|
|
774
|
+
return out
|
|
770
775
|
}
|
|
771
776
|
|
|
772
777
|
/**
|
|
@@ -1251,6 +1256,12 @@ function regexCanStart(previous) {
|
|
|
1251
1256
|
return previous.length === 1 && '(,=:[!&|?{;+-*%<>~^'.includes(previous)
|
|
1252
1257
|
}
|
|
1253
1258
|
|
|
1259
|
+
/** A newline may end a declaration before a statement regexp; do not guess ASI. */
|
|
1260
|
+
function ambiguousSlash(code, index, previous, previousEnd) {
|
|
1261
|
+
return previous === ')' || previous === '}'
|
|
1262
|
+
|| (!regexCanStart(previous) && /[\r\n\u2028\u2029]/.test(code.slice(previousEnd, index)))
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1254
1265
|
/** Read an identifier as ONE token: keyword-led regexes are not division. */
|
|
1255
1266
|
function codeWordAt(code, index) {
|
|
1256
1267
|
return /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(code.slice(index))?.[0]
|
|
@@ -1263,7 +1274,7 @@ function skipQuotedLiteral(code, index) {
|
|
|
1263
1274
|
while (i < code.length) {
|
|
1264
1275
|
const c = code[i]
|
|
1265
1276
|
if (c === '\\') { i += 2; continue }
|
|
1266
|
-
if (
|
|
1277
|
+
if (/[\r\n\u2028\u2029]/.test(c)) return -1
|
|
1267
1278
|
if (c === quote) return i + 1
|
|
1268
1279
|
i += 1
|
|
1269
1280
|
}
|
|
@@ -1277,7 +1288,7 @@ function skipRegexLiteral(code, index) {
|
|
|
1277
1288
|
while (i < code.length) {
|
|
1278
1289
|
const c = code[i]
|
|
1279
1290
|
if (c === '\\') { i += 2; continue }
|
|
1280
|
-
if (
|
|
1291
|
+
if (/[\r\n\u2028\u2029]/.test(c)) return -1
|
|
1281
1292
|
if (c === '[') inClass = true
|
|
1282
1293
|
else if (c === ']') inClass = false
|
|
1283
1294
|
else if (c === '/' && inClass === false) {
|
|
@@ -1295,11 +1306,13 @@ function skipBracedExpression(code, index) {
|
|
|
1295
1306
|
let depth = 1
|
|
1296
1307
|
let i = index
|
|
1297
1308
|
let last = ''
|
|
1309
|
+
let previousEnd = 0
|
|
1298
1310
|
while (i < code.length) {
|
|
1299
1311
|
const c = code[i]
|
|
1300
1312
|
const next = code[i + 1]
|
|
1301
1313
|
if (c === '/' && next === '/') {
|
|
1302
|
-
const
|
|
1314
|
+
const lineEnd = /[\r\n\u2028\u2029]/.exec(code.slice(i))
|
|
1315
|
+
const nl = lineEnd ? i + lineEnd.index : -1
|
|
1303
1316
|
if (nl === -1) return -1
|
|
1304
1317
|
i = nl
|
|
1305
1318
|
continue
|
|
@@ -1315,6 +1328,7 @@ function skipBracedExpression(code, index) {
|
|
|
1315
1328
|
if (end === -1) return -1
|
|
1316
1329
|
i = end
|
|
1317
1330
|
last = 'x'
|
|
1331
|
+
previousEnd = i
|
|
1318
1332
|
continue
|
|
1319
1333
|
}
|
|
1320
1334
|
if (c === '`') {
|
|
@@ -1322,15 +1336,17 @@ function skipBracedExpression(code, index) {
|
|
|
1322
1336
|
if (inner === null) return -1
|
|
1323
1337
|
i = inner.end
|
|
1324
1338
|
last = 'x'
|
|
1339
|
+
previousEnd = i
|
|
1325
1340
|
continue
|
|
1326
1341
|
}
|
|
1327
1342
|
// A slash after a closing group may start a statement regex OR division.
|
|
1328
1343
|
// Without a parser that distinction is uncertain: abort the WHOLE scan.
|
|
1329
|
-
if (c === '/' && (
|
|
1344
|
+
if (c === '/' && ambiguousSlash(code, i, last, previousEnd)) return -1
|
|
1330
1345
|
const word = codeWordAt(code, i)
|
|
1331
1346
|
if (word) {
|
|
1332
1347
|
last = last === '.' ? 'x' : word
|
|
1333
1348
|
i += word.length
|
|
1349
|
+
previousEnd = i
|
|
1334
1350
|
continue
|
|
1335
1351
|
}
|
|
1336
1352
|
if (c === '/' && regexCanStart(last)) {
|
|
@@ -1338,6 +1354,7 @@ function skipBracedExpression(code, index) {
|
|
|
1338
1354
|
if (end === -1) return -1
|
|
1339
1355
|
i = end
|
|
1340
1356
|
last = 'x'
|
|
1357
|
+
previousEnd = i
|
|
1341
1358
|
continue
|
|
1342
1359
|
}
|
|
1343
1360
|
if (c === '{') depth += 1
|
|
@@ -1345,7 +1362,7 @@ function skipBracedExpression(code, index) {
|
|
|
1345
1362
|
depth -= 1
|
|
1346
1363
|
if (depth === 0) return i + 1
|
|
1347
1364
|
}
|
|
1348
|
-
if (c.trim() !== '') last = c
|
|
1365
|
+
if (c.trim() !== '') { last = c; previousEnd = i + 1 }
|
|
1349
1366
|
i += 1
|
|
1350
1367
|
}
|
|
1351
1368
|
return -1
|
|
@@ -1383,11 +1400,13 @@ export function scanCodeLiterals(code) {
|
|
|
1383
1400
|
const spans = []
|
|
1384
1401
|
let i = 0
|
|
1385
1402
|
let last = ''
|
|
1403
|
+
let previousEnd = 0
|
|
1386
1404
|
while (i < code.length) {
|
|
1387
1405
|
const c = code[i]
|
|
1388
1406
|
const next = code[i + 1]
|
|
1389
1407
|
if (c === '/' && next === '/') {
|
|
1390
|
-
const
|
|
1408
|
+
const lineEnd = /[\r\n\u2028\u2029]/.exec(code.slice(i))
|
|
1409
|
+
const nl = lineEnd ? i + lineEnd.index : -1
|
|
1391
1410
|
// A line comment may legally terminate at EOF (outside interpolation).
|
|
1392
1411
|
if (nl === -1) break
|
|
1393
1412
|
i = nl
|
|
@@ -1405,6 +1424,7 @@ export function scanCodeLiterals(code) {
|
|
|
1405
1424
|
spans.push({ start: i + 1, end: end - 1 })
|
|
1406
1425
|
i = end
|
|
1407
1426
|
last = 'x'
|
|
1427
|
+
previousEnd = i
|
|
1408
1428
|
continue
|
|
1409
1429
|
}
|
|
1410
1430
|
if (c === '`') {
|
|
@@ -1418,15 +1438,17 @@ export function scanCodeLiterals(code) {
|
|
|
1418
1438
|
}
|
|
1419
1439
|
i = template.end
|
|
1420
1440
|
last = 'x'
|
|
1441
|
+
previousEnd = i
|
|
1421
1442
|
continue
|
|
1422
1443
|
}
|
|
1423
1444
|
// A slash after a closing group may start a statement regex OR division.
|
|
1424
1445
|
// Without a parser that distinction is uncertain: abort the WHOLE scan.
|
|
1425
|
-
if (c === '/' && (
|
|
1446
|
+
if (c === '/' && ambiguousSlash(code, i, last, previousEnd)) return null
|
|
1426
1447
|
const word = codeWordAt(code, i)
|
|
1427
1448
|
if (word) {
|
|
1428
1449
|
last = last === '.' ? 'x' : word
|
|
1429
1450
|
i += word.length
|
|
1451
|
+
previousEnd = i
|
|
1430
1452
|
continue
|
|
1431
1453
|
}
|
|
1432
1454
|
if (c === '/' && regexCanStart(last)) {
|
|
@@ -1434,9 +1456,10 @@ export function scanCodeLiterals(code) {
|
|
|
1434
1456
|
if (end === -1) return null
|
|
1435
1457
|
i = end
|
|
1436
1458
|
last = 'x'
|
|
1459
|
+
previousEnd = i
|
|
1437
1460
|
continue
|
|
1438
1461
|
}
|
|
1439
|
-
if (c.trim() !== '') last = c
|
|
1462
|
+
if (c.trim() !== '') { last = c; previousEnd = i + 1 }
|
|
1440
1463
|
i += 1
|
|
1441
1464
|
}
|
|
1442
1465
|
return spans
|
|
@@ -1512,7 +1535,7 @@ export function rewriteCodePaths(code, env) {
|
|
|
1512
1535
|
// path WITHOUT wildcards splits into directory + basename. Runs only for
|
|
1513
1536
|
// the glob tool and only for patterns that start absolute.
|
|
1514
1537
|
|
|
1515
|
-
const GLOB_META = /[*?\[\]]/
|
|
1538
|
+
const GLOB_META = /[*?\[\]{}]/
|
|
1516
1539
|
|
|
1517
1540
|
/** Split an absolute glob pattern into { prefix, rest }; null when relative. */
|
|
1518
1541
|
function splitGlobPrefix(pattern) {
|
|
@@ -2637,6 +2660,10 @@ export async function apply(ctx, config = {}) {
|
|
|
2637
2660
|
return {
|
|
2638
2661
|
id: job.id, action: job.action, total: job.total, done: job.done,
|
|
2639
2662
|
current: job.current, finished: job.finished, results: job.results.slice(),
|
|
2663
|
+
// v0.43.0 (issue #35): elapsed time, the freshest winget line,
|
|
2664
|
+
// and the cancelled state, so the card can show them.
|
|
2665
|
+
startedAt: job.startedAt, currentSince: job.currentSince,
|
|
2666
|
+
lastOutput: job.lastOutput, cancelled: job.cancelled === true,
|
|
2640
2667
|
}
|
|
2641
2668
|
}
|
|
2642
2669
|
const handler = async (req, res) => {
|
|
@@ -2652,6 +2679,14 @@ export async function apply(ctx, config = {}) {
|
|
|
2652
2679
|
try {
|
|
2653
2680
|
if (method === 'POST') {
|
|
2654
2681
|
const action = query.get('action') || ''
|
|
2682
|
+
if (action === 'cancel') {
|
|
2683
|
+
// v0.43.0 (issue #35): kill the in-flight tree, mark the rest
|
|
2684
|
+
// skipped. The cancelled job stays current until finished so
|
|
2685
|
+
// the card can show what happened; `clear` then forgets it.
|
|
2686
|
+
cancelToolJob('')
|
|
2687
|
+
send(res, 200, { ok: true, job: snapshot() })
|
|
2688
|
+
return
|
|
2689
|
+
}
|
|
2655
2690
|
if (action === 'clear') {
|
|
2656
2691
|
clearToolJob('')
|
|
2657
2692
|
send(res, 200, { ok: true, job: null })
|
package/src/native-executor.js
CHANGED
|
@@ -25,7 +25,8 @@ export function nativePath(input, translateMsysPath, platform = process.platform
|
|
|
25
25
|
object(input, label, new Set(['kind', 'value']))
|
|
26
26
|
if (input.kind !== 'path') throw new TypeError(`${label}: kind must be path`)
|
|
27
27
|
const value = string(input.value, label)
|
|
28
|
-
|
|
28
|
+
// Reject expansion prefixes, not literal characters inside absolute paths.
|
|
29
|
+
if (/^(?:[~$]|%[^%]+%)/.test(value)) throw new TypeError(`${label}: variable and home expansion are not supported; use an absolute path`)
|
|
29
30
|
if (platform !== 'win32') throw new Error('gitbash_native currently supports only the Windows local execution world')
|
|
30
31
|
// A rooted Windows path without a drive is not absolute in this contract.
|
|
31
32
|
const absolute = v => /^[a-z]:[\\/]/i.test(v) || /^\\\\[^\\]+\\[^\\]+/.test(v) || /^\/\/[^/]+\/[^/]+/.test(v)
|
package/src/native-tool.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerExperimentalTool } from './experimental-tools.js'
|
|
2
2
|
import { createNativeExecutor } from './native-executor.js'
|
|
3
|
-
import { processJob, processSources, ringDelta } from './bash-render.js'
|
|
3
|
+
import { processJob, processSources, ringDelta, renderJobRead } from './bash-render.js'
|
|
4
4
|
|
|
5
5
|
const typedPath = () => ({ type: 'object', additionalProperties: false, properties: {
|
|
6
6
|
kind: { type: 'string', const: 'path', required: true }, value: { type: 'string', required: true },
|
|
@@ -165,8 +165,11 @@ export function createNativeToolModule(deps) {
|
|
|
165
165
|
throw new Error(`native preparation timed out; job ${attached.id} was cancelled, inspect job_output before retrying`)
|
|
166
166
|
}
|
|
167
167
|
const read = jobs.read(attached.id, owner)
|
|
168
|
+
// A ring eviction can report gapBefore even when its pull source
|
|
169
|
+
// was lossless. This consuming read must preserve both loss facts.
|
|
170
|
+
const lostOutput = read.lossy || read.chunks.some(chunk => chunk.gapBefore)
|
|
168
171
|
return { kind: 'promoted', jobId: attached.id, cwd: request.cwd, timeoutMs: request.timeoutMs,
|
|
169
|
-
output: ringDelta(read.chunks), encoding: request.output }
|
|
172
|
+
output: renderJobRead(ringDelta(read.chunks), lostOutput, read.job?.output?.spillPaths), encoding: request.output }
|
|
170
173
|
}
|
|
171
174
|
jobs.remove(attached.id, owner)
|
|
172
175
|
if (!attached.process()) throw new Error(view.detail ?? 'native preparation failed before a process handle was available')
|