dsh-gitbash-shell 0.39.1 → 0.39.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/src/client.js CHANGED
@@ -2063,6 +2063,42 @@ window.__ModuleLoader__.load({
2063
2063
  if (scope && typeof scope.getSnapshot === "function") snap = scope.getSnapshot();
2064
2064
  } catch (error_) { /* keep unavailable */ }
2065
2065
 
2066
+ var pageView = props.view === "page";
2067
+
2068
+ // Hooks run for every snapshot state; readiness only gates their work.
2069
+ useEffect(function () {
2070
+ if (snap.status !== "ready" || (!open && !pageView)) return undefined;
2071
+ if (toolsLoaded || toolBusy) return undefined;
2072
+ loadTools();
2073
+ return undefined;
2074
+ }, [open, pageView, snap.status]);
2075
+
2076
+ /* Poll ONLY while a job is unfinished, and through the cheap ?job=1
2077
+ form. A transient failure retries without discarding progress. */
2078
+ useEffect(function () {
2079
+ if (snap.status !== "ready" || toolJob === null || toolJob.finished) return undefined;
2080
+ var alive = true;
2081
+ var timer;
2082
+ function schedule() {
2083
+ timer = setTimeout(function () {
2084
+ fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
2085
+ .then(function (res) {
2086
+ if (!res.ok) throw new Error("HTTP " + res.status);
2087
+ return res.json();
2088
+ })
2089
+ .then(function (data) {
2090
+ if (!alive) return;
2091
+ var next = data !== null && typeof data === "object" && data.job ? data.job : null;
2092
+ setToolJob(next);
2093
+ if (next !== null && next.finished) loadTools();
2094
+ })
2095
+ .catch(function () { if (alive) schedule(); });
2096
+ }, 1200);
2097
+ }
2098
+ schedule();
2099
+ return function () { alive = false; clearTimeout(timer); };
2100
+ }, [toolJob, snap.status]);
2101
+
2066
2102
  if (snap.status !== "ready") return null;
2067
2103
 
2068
2104
  var value = snap.value || {};
@@ -2135,7 +2171,6 @@ window.__ModuleLoader__.load({
2135
2171
  // dsh 0.1.6-alpha.2: the Plugins page renders this card through the
2136
2172
  // `plugins.bundle.config` slot with view="page" — the page draws the
2137
2173
  // title itself; the collapsible shell only shows on the popup seat.
2138
- var pageView = props.view === "page";
2139
2174
 
2140
2175
  var mounts = snap.value.virtualMounts !== false;
2141
2176
  var errs = snap.value.errorDialect !== false;
@@ -2469,30 +2504,6 @@ window.__ModuleLoader__.load({
2469
2504
  .then(function () { setToolBusy(false); });
2470
2505
  }
2471
2506
 
2472
- useEffect(function () {
2473
- if (!open && !pageView) return undefined;
2474
- if (toolsLoaded || toolBusy) return undefined;
2475
- loadTools();
2476
- return undefined;
2477
- }, [open]);
2478
-
2479
- /* Poll ONLY while a job is unfinished, and through the cheap ?job=1
2480
- form: a full probe re-runs three winget commands per second. */
2481
- useEffect(function () {
2482
- if (toolJob === null || toolJob.finished) return undefined;
2483
- var timer = setTimeout(function () {
2484
- fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
2485
- .then(function (res) { return res.json(); })
2486
- .then(function (data) {
2487
- var next = data !== null && typeof data === "object" && data.job ? data.job : null;
2488
- setToolJob(next);
2489
- if (next !== null && next.finished) loadTools();
2490
- })
2491
- .catch(function () { /* keep the last known progress */ });
2492
- }, 1200);
2493
- return function () { clearTimeout(timer); };
2494
- }, [toolJob]);
2495
-
2496
2507
  var toolsSection = fold("sec.tools",
2497
2508
  toolsData === null && toolErr === "" ? E("p", { className: "gb-hint" }, t("tools.loading")) : null,
2498
2509
  toolErr !== "" ? E("p", { className: "gb-error" }, toolErr) : null,
@@ -9,10 +9,10 @@
9
9
  *
10
10
  * Every variant mirrors the official 0.1.7 preset it replaces (packages/
11
11
  * bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
12
- * one Git Bash delta: the bash rows are always on and the pwsh rows are
13
- * always off (the plugin ships its OWN bash tool over a private executor —
14
- * on non-Windows hosts the official stack is native anyway, which is exactly
15
- * what the old materialized variants encoded). A second, conditional delta exists while
12
+ * Git Bash routing: full variants keep the official platform-gated shell
13
+ * rows, and the inline patch adds OUR bash over a private executor on Windows.
14
+ * The minimal variant instead keeps its persistent Git Bash terminal rows.
15
+ * On non-Windows hosts the official stack stays native. A conditional delta exists while
16
16
  * dsh-ptc-cordis-preset reports its experimental-CPython backend ACTIVE: the
17
17
  * two workflow rows follow the official Python composition and go off (see
18
18
  * `pythonActive` below). While that backend is off — the default, and also
@@ -56,7 +56,8 @@ const planSectionFor = (hostEra021) => hostEra021
56
56
  * @param {'standard'|'ptc'|'cordis'} input.kind - which official preset this
57
57
  * variant mirrors (workflow rows follow it: enabled on standard/cordis,
58
58
  * disabled on ptc).
59
- * @param {boolean} input.gitBash - bash rows always on, pwsh rows always off.
59
+ * @param {boolean} input.gitBash - retained for composition-call compatibility;
60
+ * full variants obtain Git Bash from the per-agent inline patch on Windows.
60
61
  * @param {string|undefined} input.skillsDir - Creation authoring skills
61
62
  * directory (cordis variant only; resolved beside the agent-preset package).
62
63
  * @param {boolean} [input.pythonActive] - whether the experimental CPython
@@ -75,8 +76,11 @@ const planSectionFor = (hostEra021) => hostEra021
75
76
  */
76
77
  export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hostExtras = {} }) {
77
78
  const win = typeof process !== 'undefined' && process.platform === 'win32'
78
- const bashDisabled = gitBash ? false : win
79
- const pwshDisabled = gitBash ? true : !win
79
+ // Route B preserves the root PowerShell executor. On Windows the official
80
+ // tool-bash would execute through that executor, so full variants retain the
81
+ // official pwsh face and let the same per-agent inline patch add OUR bash.
82
+ const bashDisabled = win
83
+ const pwshDisabled = !win
80
84
  const workflowOn = kind !== 'ptc' && pythonActive !== true
81
85
  // dsh 0.2.1 added two rows to every full-tool official preset — time-context
82
86
  // (durable clock readings) and tool-schedule (the reminder tools) — plus a
@@ -106,20 +106,21 @@ export class McpStdio {
106
106
  this.child.stderr.on('data', (chunk) => {
107
107
  this.stderrTail = (this.stderrTail + chunk).slice(-2000)
108
108
  })
109
- this.child.on('exit', (code) => {
110
- this.dead = true
111
- for (const [, { reject }] of [...this.pending]) {
112
- reject(new Error(`the FastCtx server exited (code ${code ?? 'null'})`))
113
- }
114
- this.pending.clear()
115
- })
116
- await this.#request('initialize', {
117
- protocolVersion: '2024-11-05',
118
- capabilities: {},
119
- clientInfo: { name: 'dsh-gitbash-shell', version: '0.35.0' },
120
- })
121
- this.#notify('notifications/initialized')
122
- return this
109
+ this.child.on('error', (error) => this.#fail(error))
110
+ this.child.stdin.on('error', (error) => this.#fail(error))
111
+ this.child.on('exit', (code) => this.#fail(new Error(`the FastCtx server exited (code ${code ?? 'null'})`)))
112
+ try {
113
+ await this.#request('initialize', {
114
+ protocolVersion: '2024-11-05',
115
+ capabilities: {},
116
+ clientInfo: { name: 'dsh-gitbash-shell', version: '0.35.0' },
117
+ })
118
+ this.#notify('notifications/initialized')
119
+ return this
120
+ } catch (error) {
121
+ await this.close()
122
+ throw error
123
+ }
123
124
  }
124
125
 
125
126
  /** List the server's tools: [{ name, description, inputSchema }]. */
@@ -140,15 +141,21 @@ export class McpStdio {
140
141
  this.dead = true
141
142
  const child = this.child
142
143
  this.child = undefined
143
- for (const [, { reject }] of [...this.pending]) {
144
- reject(new Error('the FastCtx bridge was stopped'))
145
- }
146
- this.pending.clear()
144
+ this.#fail(new Error('the FastCtx bridge was stopped'))
147
145
  if (child && child.exitCode === null && !child.killed) {
148
146
  try { child.kill() } catch { /* already gone */ }
149
147
  }
150
148
  }
151
149
 
150
+ #fail(error) {
151
+ this.dead = true
152
+ for (const { reject, timer } of this.pending.values()) {
153
+ clearTimeout(timer)
154
+ reject(error)
155
+ }
156
+ this.pending.clear()
157
+ }
158
+
152
159
  #onChunk(chunk) {
153
160
  this.buffer += chunk
154
161
  let at = this.buffer.indexOf('\n')
@@ -310,6 +317,10 @@ export function mountFastctx(ctx, deps) {
310
317
  let client = undefined
311
318
  let decided = false
312
319
  let withdrawnByOps = false
320
+ let closed = false
321
+ let publishing
322
+ let connecting
323
+ const starting = new Set()
313
324
 
314
325
  const unpublish = (reason) => {
315
326
  if (disposer) {
@@ -320,16 +331,34 @@ export function mountFastctx(ctx, deps) {
320
331
  }
321
332
 
322
333
  const callWithReconnect = async (rawName, args) => {
334
+ if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
323
335
  if (client === undefined || client.isDead) {
324
- const { file } = resolveFastctx(ctx, createRequireFrom)
325
- if (file === '') throw new Error('the FastCtx payload is not installed; repair the profile dependencies (frozen install) and retry')
326
- client = await new McpStdio({ file, log }).start()
336
+ if (!connecting) {
337
+ connecting = (async () => {
338
+ const { file } = resolveFastctx(ctx, createRequireFrom)
339
+ if (file === '') throw new Error('the FastCtx payload is not installed; repair the profile dependencies (frozen install) and retry')
340
+ const fresh = new McpStdio({ file, log })
341
+ starting.add(fresh)
342
+ try {
343
+ await fresh.start()
344
+ if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
345
+ client = fresh
346
+ } catch (error) {
347
+ await fresh.close()
348
+ throw error
349
+ } finally {
350
+ starting.delete(fresh)
351
+ }
352
+ })().finally(() => { connecting = undefined })
353
+ }
354
+ await connecting
327
355
  }
356
+ if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
328
357
  return client.callTool(rawName, args)
329
358
  }
330
359
 
331
- const publish = async () => {
332
- if (disposer || decided && withdrawnByOps) return
360
+ const publishOnce = async () => {
361
+ if (closed || disposer || decided && withdrawnByOps) return
333
362
  const registry = tools(ctx)
334
363
  if (!registry || typeof registry.register !== 'function') return
335
364
  const { file, source } = resolveFastctx(ctx, createRequireFrom)
@@ -340,6 +369,7 @@ export function mountFastctx(ctx, deps) {
340
369
  return
341
370
  }
342
371
  const fresh = new McpStdio({ file, log })
372
+ starting.add(fresh)
343
373
  let toolList
344
374
  try {
345
375
  await fresh.start()
@@ -350,9 +380,12 @@ export function mountFastctx(ctx, deps) {
350
380
  decided = true
351
381
  withdrawnByOps = true
352
382
  return
383
+ } finally {
384
+ starting.delete(fresh)
353
385
  }
354
386
  await fresh.close()
355
- client = undefined // the first call re-launches lazily; boot stays cheap
387
+ if (closed || withdrawnByOps) return
388
+ // Discovery never overwrites the independently owned lazy call client.
356
389
  const wanted = toolList.filter((tool) => FASTCTX_FILE_TOOLS.includes(String(tool?.name)))
357
390
  const registered = []
358
391
  const registeredNames = []
@@ -386,16 +419,20 @@ export function mountFastctx(ctx, deps) {
386
419
  schema: TOOL_OUTPUT_SCHEMA,
387
420
  render: (_args, value) => renderTextContent(value),
388
421
  },
389
- async execute(args) {
390
- // Fail LOUD on malformed patterns (v0.38.5; reshaped v0.39.1,
391
- // issue #21 — see validateFastctxPattern).
422
+ async execute(args, execution) {
423
+ // Preserve v0.39.1 / #21: glob accepts real string arrays,
424
+ // while stringified arrays and malformed patterns fail loudly.
392
425
  if (args !== null && typeof args === 'object' && 'pattern' in args) {
393
426
  const invalid = validateFastctxPattern(rawName, args.pattern)
394
427
  if (invalid) throw new Error(invalid)
395
428
  }
396
- const translated = translateFastctxArgs(args, msysToWin)
429
+ const translated = translateFastctxArgs(args, msysToWin ? (path) => msysToWin(path, execution) : undefined)
397
430
  const result = await callWithReconnect(rawName, translated)
398
- return { content: [{ type: 'text', text: renderFastctxResult(rawName, result, echo) }] }
431
+ const text = renderFastctxResult(rawName, result, echo ? (value) => echo(value, execution) : undefined)
432
+ // The host treats a returned value as success; MCP failures must
433
+ // throw, just as its official MCP client does.
434
+ if (result?.isError === true) throw new Error(text)
435
+ return { content: [{ type: 'text', text }] }
399
436
  },
400
437
  })
401
438
  if (typeof dispose === 'function') {
@@ -416,7 +453,13 @@ export function mountFastctx(ctx, deps) {
416
453
  log(`FastCtx bridge active (${source}): ${registeredNames.join(', ')}`)
417
454
  }
418
455
 
456
+ const publish = () => {
457
+ if (!publishing) publishing = publishOnce().finally(() => { publishing = undefined })
458
+ return publishing
459
+ }
460
+
419
461
  const reconcile = (agent) => {
462
+ if (closed) return
420
463
  if (!enabled()) { unpublish('FastCtx bridge disabled by the fastctxTools switch'); decided = true; withdrawnByOps = true; return }
421
464
  if (decided && withdrawnByOps) return
422
465
  let names = []
@@ -440,8 +483,12 @@ export function mountFastctx(ctx, deps) {
440
483
  log(`FastCtx bridge wiring failed: ${error?.message ?? error}`)
441
484
  return
442
485
  }
443
- ctx.effect(() => () => {
486
+ ctx.effect(() => async () => {
487
+ closed = true
444
488
  unpublish()
445
- if (client) { void client.close().catch(() => {}) }
489
+ const owned = new Set([...starting, client].filter(Boolean))
490
+ await Promise.allSettled([...owned].map((connection) => connection.close()))
491
+ await Promise.allSettled([publishing, connecting].filter(Boolean))
492
+ client = undefined
446
493
  }, 'dsh-gitbash-shell: FastCtx bridge')
447
494
  }