dsh-gitbash-shell 0.31.1 → 0.32.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/src/index.js CHANGED
@@ -285,7 +285,6 @@ import {
285
285
  } from './tool-runner.js'
286
286
 
287
287
  /** The shipped preset whose skills/ dir seeds cordis-gitbash. */
288
- const SKILLS_SOURCE_PRESET = 'cordis'
289
288
 
290
289
  const here = dirname(fileURLToPath(import.meta.url))
291
290
  const pkgDir = join(here, '..')
@@ -353,465 +352,6 @@ function skillsHashes(source) {
353
352
  return out
354
353
  }
355
354
 
356
- // ── built-in era detection (dsh 0.1.2 renamed the `code` preset to `ptc`) ──
357
-
358
- /**
359
- * Which built-in PTC preset does the installed dsh ship? dsh 0.1.2 renamed
360
- * the preset id `code` to `ptc` with no compatibility alias, so the variant
361
- * compositions are era-specific where the built-in changed. The roster probe
362
- * is the version-agnostic signal. Pure companion of `detectBase`; 'ptc' wins
363
- * if both ids somehow exist, and an unknown roster conservatively maps to
364
- * 'code' — the era text the stable release accepted.
365
- */
366
- function baseForRoster(ids) {
367
- const set = new Set(ids)
368
- if (set.has('ptc')) return 'ptc'
369
- return 'code'
370
- }
371
-
372
- /** Async probe against the live roster; never throws. */
373
- async function detectBase(agentPresets) {
374
- try {
375
- const list = await agentPresets.list()
376
- return baseForRoster((Array.isArray(list) ? list : []).map((p) => p && p.id))
377
- } catch (error) {
378
- console.log(`${TAG} built-in preset probe failed (${error?.message ?? error}) — assuming the 'code' era`)
379
- return 'code'
380
- }
381
- }
382
-
383
- /** Era suffix for committed composition assets: ptc-era files carry `.ptc`. */
384
- function eraSuffix(base) {
385
- return base === 'ptc' ? '.ptc' : ''
386
- }
387
-
388
- // ── persona-form detection (dsh 0.1.3-alpha.2 split the persona row) ──────
389
-
390
- /**
391
- * Which persona config shape does one composition text use? 0.1.3-alpha.2
392
- * (40792330c0) split dsh-persona's single `text` key into `prefix:` +
393
- * `suffix:` with no compatibility alias, so compositions carrying a persona
394
- * row are persona-form-specific. Pure companion of `detectPersonaEra`.
395
- */
396
- export function personaEraForText(text) {
397
- return /- id:\s*persona[\s\S]{0,600}?\bprefix:/.test(text) ? 'split' : 'text'
398
- }
399
-
400
- /**
401
- * Async probe of a SHIPPED preset's composition through the live roster;
402
- * never throws. Built-in ids only — this plugin's own variants sit on the
403
- * roster too and would echo whichever form they were materialized with. The
404
- * roster entry path is the preset file (or its directory); either way we
405
- * read agent.cordis.yml. A missing probe conservatively maps to 'text'.
406
- */
407
- export async function detectPersonaEra(agentPresets) {
408
- try {
409
- const list = await agentPresets.list()
410
- const entries = Array.isArray(list) ? list : []
411
- const entry = ['ptc', 'standard', 'cordis', 'minimal']
412
- .map((id) => entries.find((p) => p && p.id === id && typeof p.path === 'string'))
413
- .find(Boolean)
414
- if (!entry) return 'text'
415
- const file = /\.yml$/.test(entry.path) ? entry.path : join(entry.path, 'agent.cordis.yml')
416
- return personaEraForText(readFileSync(file, 'utf8'))
417
- } catch (error) {
418
- console.log(`${TAG} persona-form probe failed (${error?.message ?? error}) — assuming the pre-split 'text' form`)
419
- return 'text'
420
- }
421
- }
422
-
423
- /**
424
- * Does the SHIPPED composition already carry the official `present` row? The
425
- * row (`@deepseek-ai/dsh-tool-present`, immutable file-delivery download
426
- * cards) first shipped with dsh 0.1.5-alpha.2, and a composition row that
427
- * cannot be imported rejects the WHOLE preset mount — so the row is injected
428
- * only on hosts whose own shipped presets have it. The live roster is the
429
- * authority: package resolution alone would lie on CLI installs (first-party
430
- * packages live outside the profile) and on linked development trees.
431
- * `minimal` never gains the row (single-tool preset). Never throws.
432
- */
433
- async function detectPresentSupport(agentPresets) {
434
- try {
435
- const list = await agentPresets.list()
436
- const entries = Array.isArray(list) ? list : []
437
- const entry = ['ptc', 'standard', 'cordis']
438
- .map((id) => entries.find((p) => p && p.id === id && typeof p.path === 'string'))
439
- .find(Boolean)
440
- if (!entry) return false
441
- const file = /\.yml$/.test(entry.path) ? entry.path : join(entry.path, 'agent.cordis.yml')
442
- return readFileSync(file, 'utf8').includes("'@deepseek-ai/dsh-tool-present'")
443
- } catch (error) {
444
- console.log(`${TAG} present-row probe failed (${error?.message ?? error}) — not injecting the row`)
445
- return false
446
- }
447
- }
448
-
449
- /** Row ids the shipped workflow-engine row has answered to (dsh 0.1.6 renamed it). */
450
- const ENGINE_ROW_IDS = ['workflow-worker-thread', 'workflow-ptc']
451
-
452
- /** Which built-in preset each of this plugin's variants mirrors, for row-form lookup. */
453
- export const ROW_SOURCE = {
454
- 'standard-gitbash': 'standard',
455
- 'cordis-gitbash': 'cordis',
456
- 'code-gitbash': 'ptc',
457
- }
458
-
459
- /** Leading whitespace of one line (pure). */
460
- function indentOf(line) {
461
- return line.slice(0, line.length - line.trimStart().length)
462
- }
463
-
464
- /** The unquoted value of a name line, or undefined when the line is not one (pure). */
465
- function quotedName(line) {
466
- const trimmed = line.trim()
467
- if (!trimmed.startsWith('name: ')) return undefined
468
- const raw = trimmed.slice(6).trim()
469
- if (raw.length > 1 && raw.charAt(0) === "'" && raw.charAt(raw.length - 1) === "'") return raw.slice(1, -1)
470
- return raw
471
- }
472
-
473
- /**
474
- * Read ONE row's spelling out of a composition text (pure): the name value
475
- * that follows its id line and whether the row's own block turns it off before
476
- * the next row starts. Returns undefined when the row is absent, and never
477
- * throws, so a composition that legitimately lacks the row (minimal) is simply
478
- * left alone.
479
- * @param text - composition text.
480
- * @param rowId - exact id value to locate.
481
- * @returns the row's spelling, or undefined.
482
- */
483
- export function rowFormOf(text, rowId) {
484
- const lines = text.split('\n')
485
- for (let i = 0; i < lines.length; i += 1) {
486
- if (lines[i].trim() !== '- id: ' + rowId) continue
487
- const nameLine = i + 1 < lines.length ? lines[i + 1] : ''
488
- const name = quotedName(nameLine)
489
- if (name === undefined) continue
490
- let disabled = false
491
- let disabledIndent = indentOf(nameLine)
492
- for (let j = i + 2; j < lines.length; j += 1) {
493
- if (lines[j].trim().startsWith('- id: ')) break
494
- if (lines[j].trim() === 'disabled: true') {
495
- disabled = true
496
- disabledIndent = indentOf(lines[j])
497
- break
498
- }
499
- }
500
- return { id: rowId, name, indent: indentOf(lines[i]), nameIndent: indentOf(nameLine), disabled, disabledIndent }
501
- }
502
- return undefined
503
- }
504
-
505
- /**
506
- * Read the two rows this plugin's committed compositions must track out of a
507
- * SHIPPED composition text (pure). The built-in preset is the authority: it
508
- * mounts on this host by construction, so whatever it spells is mountable
509
- * here, on every era, without shipping one asset variant per rename.
510
- * @param text - a shipped agent.cordis.yml.
511
- * @returns an object with the engine row and the tool-ralph row; either may be undefined.
512
- */
513
- export function rowFormsOf(text) {
514
- let engine
515
- for (const id of ENGINE_ROW_IDS) {
516
- engine = rowFormOf(text, id)
517
- if (engine !== undefined) break
518
- }
519
- return { engine, ralph: rowFormOf(text, 'tool-ralph') }
520
- }
521
-
522
- /**
523
- * Align the workflow-engine row with the host's own spelling (pure,
524
- * idempotent, plain string surgery — never a YAML round-trip, so !!js literals
525
- * survive).
526
- *
527
- * dsh 0.1.6-alpha.1 renamed this row workflow-worker-thread to workflow-ptc
528
- * AND deleted the old package. A composition row whose module fails to import
529
- * rejects the WHOLE preset mount (agent-presets mount.ts), so a committed old
530
- * spelling breaks every Git Bash preset on the new host while the new spelling
531
- * breaks them on hosts that still ship only the old package. The materializer
532
- * therefore copies the row out of the host's own built-in preset instead of
533
- * pinning either spelling.
534
- * @param text - composition text.
535
- * @param form - the host's engine row from rowFormsOf, or undefined to leave the text alone.
536
- * @param options - engineEnabled forces the row on regardless of the host's own state.
537
- * @returns the aligned text.
538
- */
539
- export function alignEngineRow(text, form, options) {
540
- if (!form) return text
541
- const engineEnabled = Boolean(options && options.engineEnabled)
542
- let current
543
- for (const id of ENGINE_ROW_IDS) {
544
- current = rowFormOf(text, id)
545
- if (current !== undefined) break
546
- }
547
- if (current === undefined) return text
548
- const head = '- id: ' + current.id + '\n' + current.nameIndent + "name: '" + current.name + "'"
549
- const wanted = '- id: ' + form.id + '\n' + current.nameIndent + "name: '" + form.name + "'"
550
- const wantOff = engineEnabled ? false : form.disabled
551
- let out = text.replace(head, wanted)
552
- if (wantOff && !current.disabled) {
553
- out = out.replace(wanted + '\n', wanted + '\n' + current.nameIndent + 'disabled: true\n')
554
- } else if (!wantOff && current.disabled) {
555
- out = out.replace(wanted + '\n' + current.disabledIndent + 'disabled: true\n', wanted + '\n')
556
- }
557
- return out
558
- }
559
-
560
- /**
561
- * Align the tool-ralph row with the host's own default (pure, idempotent).
562
- * dsh 0.1.6-alpha.1 ships ralph disabled in every built-in preset: its tool
563
- * description restricts it to runs the human explicitly asked for. The row
564
- * still imports on older hosts, so this is a semantics alignment rather than a
565
- * mount fix — but a composition claiming to mirror the built-in preset should
566
- * not silently re-enable a tool the deployment turned off.
567
- * @param text - composition text.
568
- * @param form - the host's tool-ralph row from rowFormsOf, or undefined.
569
- * @returns the aligned text.
570
- */
571
- export function alignRalphRow(text, form) {
572
- if (!form) return text
573
- const current = rowFormOf(text, 'tool-ralph')
574
- if (current === undefined) return text
575
- const head = '- id: tool-ralph\n' + current.nameIndent + "name: '" + current.name + "'"
576
- if (form.disabled && !current.disabled) {
577
- return text.replace(head + '\n', head + '\n' + current.nameIndent + 'disabled: true\n')
578
- }
579
- if (!form.disabled && current.disabled) {
580
- return text.replace(head + '\n' + current.disabledIndent + 'disabled: true\n', head + '\n')
581
- }
582
- return text
583
- }
584
-
585
- /**
586
- * Probe every built-in preset this plugin's variants mirror for those row
587
- * spellings (never throws). The live roster is the authority — package
588
- * resolution alone would lie on linked development trees and on CLI installs
589
- * whose first-party packages live outside the profile.
590
- * @param agentPresets - the roster service.
591
- * @returns a map of row forms per available built-in id, or undefined.
592
- */
593
- async function detectRowForms(agentPresets) {
594
- try {
595
- const list = await agentPresets.list()
596
- const entries = Array.isArray(list) ? list : []
597
- const out = {}
598
- for (const id of ['ptc', 'standard', 'cordis']) {
599
- const entry = entries.find((p) => p && p.id === id && typeof p.path === 'string')
600
- if (!entry) continue
601
- const file = /\.yml$/.test(entry.path) ? entry.path : join(entry.path, 'agent.cordis.yml')
602
- out[id] = rowFormsOf(readFileSync(file, 'utf8'))
603
- }
604
- return Object.keys(out).length > 0 ? out : undefined
605
- } catch (error) {
606
- console.log(TAG + ' row-form probe failed (' + (error && error.message ? error.message : error) + ') — keeping the committed spellings')
607
- return undefined
608
- }
609
- }
610
-
611
- /**
612
- * Marker fingerprint of the row spellings one variant is aligned to, so a host
613
- * that renames (or re-defaults) them re-materializes on the next boot.
614
- * @param rows - detectRowForms result.
615
- * @param presetId - the variant being materialized.
616
- * @returns a short stable string, '' when nothing was probed.
617
- */
618
- function rowFingerprint(rows, presetId) {
619
- const source = ROW_SOURCE[presetId]
620
- const form = rows && source ? rows[source] : undefined
621
- if (!form || !form.engine) return ''
622
- const ralph = form.ralph === undefined ? 'na' : (form.ralph.disabled ? 'off' : 'on')
623
- return form.engine.id + ':' + (form.engine.disabled ? 'off' : 'on') + ':' + ralph
624
- }
625
-
626
- /**
627
- * Pick the committed composition asset inside one variant directory (pure).
628
- * The ptc-era twin is preferred when the roster says `ptc`; the persona-split
629
- * twin (`.ps`, v0.12.0) is preferred when the shipped persona row carries the
630
- * split keys; variants without a twin fall back down the chain (minimal never
631
- * had a ptc twin and gained its `.ps` twin instead). Every candidate is a
632
- * committed file — no runtime text synthesis.
633
- */
634
- function pickComposition(base, persona, available) {
635
- const era = eraSuffix(base)
636
- const ps = persona === 'split' ? '.ps' : ''
637
- const candidates = base === 'ptc'
638
- ? [`agent.cordis${era}${ps}.yml`, `agent.cordis${era}.yml`, ...(ps ? ['agent.cordis.ps.yml'] : []), 'agent.cordis.yml']
639
- : ['agent.cordis.yml']
640
- for (const file of candidates) if (available.includes(file)) return file
641
- return 'agent.cordis.yml'
642
- }
643
-
644
- /** Startup decision for an existing unmodified tree: 'refresh' or 'idle'. */
645
- function syncDecision({ state, marker, version, sourceHashes, base = 'code', persona = 'text', present = false, pluginManager = false, rows = '' }) {
646
- if (state !== 'unmodified' || !marker) return 'refresh'
647
- if (marker.version !== version) return 'refresh'
648
- if (marker.base !== base) return 'refresh'
649
- if ((marker.persona ?? 'text') !== persona) return 'refresh'
650
- // Host row-form flip: dsh 0.1.6-alpha.1 renamed the workflow-engine row and
651
- // deleted the package behind the old spelling. A composition row that cannot
652
- // be imported rejects the WHOLE preset mount, so a host reporting a
653
- // different spelling must re-materialize (same marker-dimension pattern as
654
- // persona and present).
655
- if ((marker.rows ?? '') !== rows) return 'refresh'
656
- // Host-capability flip: @deepseek-ai/dsh-tool-present first shipped with dsh
657
- // 0.1.5-alpha.2. A preset composed with the row on a host that lacks the
658
- // package REJECTS THE WHOLE MOUNT (mount.ts: a row whose module failed to
659
- // import already rejects the mount), so the row is injected only when the
660
- // host resolves it — and a host upgrade past that point must re-materialize
661
- // to gain it (same marker-dimension pattern as persona).
662
- if ((marker.present ?? false) !== present) return 'refresh'
663
- // Host-capability flip: @deepseek-ai/dsh-plugin-manager/tools first shipped
664
- // with dsh 0.1.6-alpha.2 — same mount-rejection stakes and the same marker
665
- // dimension pattern as present above.
666
- if ((marker.pluginManager ?? false) !== pluginManager) return 'refresh'
667
- const recorded = {}
668
- for (const k of Object.keys(marker.files)) if (k.startsWith('skills/')) recorded[k] = marker.files[k]
669
- if (sourceHashes === null) return Object.keys(recorded).length === 0 ? 'idle' : 'refresh'
670
- const live = Object.keys(sourceHashes)
671
- const seen = Object.keys(recorded)
672
- if (live.length !== seen.length) return 'refresh'
673
- for (const k of live) if (recorded[k] !== sourceHashes[k]) return 'refresh'
674
- return 'idle'
675
- }
676
-
677
- // ── materialization ─────────────────────────────────────────────────────────
678
-
679
- /*
680
- * Conditional present-row injection (v0.13.0, dsh 0.1.5-alpha.2 sync). The
681
- * official ptc/standard/cordis compositions appended `- id: present / name:
682
- * '@deepseek-ai/dsh-tool-present'` (immutable file-delivery download cards).
683
- * The row is NOT committed into the ptc-era twin files: a host older than
684
- * 0.1.5-alpha.2 lacks the package, and a composition row that fails to
685
- * import rejects the WHOLE preset mount. Instead the materializer probes the
686
- * host (hostHasToolPresent) and splices the row in as PLAIN TEXT — anchor
687
- * string surgery only, never a YAML round-trip, so !!js literals survive.
688
- * Placement mirrors the official compositions: right after the
689
- * tool-presentation block when the composition has one (all ptc-derived
690
- * twins), otherwise appended at the tail (cordis/standard twins). Only
691
- * `.ptc.` files are ever touched: code-era (<= 0.1.1) snapshots and the
692
- * minimal variants are frozen history (official minimal never gained the
693
- * row — it is the single-tool preset).
694
- */
695
- const PRESENT_ROW = "\n- id: present\n name: '@deepseek-ai/dsh-tool-present'\n"
696
- const PRESENT_ANCHOR = " name: '@deepseek-ai/dsh-agent-tool-presentation'\n config:\n mode: ptc\n"
697
-
698
- function injectPresentRow(text) {
699
- if (text.includes("'@deepseek-ai/dsh-tool-present'")) return text
700
- const at = text.indexOf(PRESENT_ANCHOR)
701
- if (at !== -1) return text.slice(0, at + PRESENT_ANCHOR.length) + PRESENT_ROW + text.slice(at + PRESENT_ANCHOR.length)
702
- return text.endsWith('\n') ? text + PRESENT_ROW : text + '\n' + PRESENT_ROW
703
- }
704
-
705
- let toolPresentCache
706
- /** Probe whether THIS host can resolve the present package (cached per boot). */
707
- async function hostHasToolPresent() {
708
- if (toolPresentCache !== undefined) return toolPresentCache
709
- try {
710
- const { createRequire } = await import('node:module')
711
- createRequire(import.meta.url).resolve('@deepseek-ai/dsh-tool-present')
712
- toolPresentCache = true
713
- } catch {
714
- toolPresentCache = false
715
- }
716
- return toolPresentCache
717
- }
718
-
719
- /*
720
- * Conditional plugin-manager-row injection (v0.15.0, dsh 0.1.6-alpha.2 sync).
721
- * Official ptc/standard/cordis gained `- id: tool-plugin-manager / name:
722
- * '@deepseek-ai/dsh-plugin-manager/tools'` (persistent plugin management;
723
- * the official ptc preset carries it DISABLED, Creation keeps it on). The
724
- * package only exists from 0.1.6-alpha.2 on, so the row is injected as plain
725
- * text behind a host probe — never committed into the assets — anchored
726
- * after the present row when there is one, else at the tail, mirroring the
727
- * official order (tool-presentation → present → tool-plugin-manager).
728
- * Per-variant shape mirrors each variant's official base: code-gitbash (ptc
729
- * base) DISABLED, standard/cordis-gitbash ENABLED, minimal untouched
730
- * (official minimal never gained the row), code-era files frozen history.
731
- */
732
- const PLUGIN_MANAGER_ROW_ON = "- id: tool-plugin-manager\n name: '@deepseek-ai/dsh-plugin-manager/tools'\n"
733
- const PLUGIN_MANAGER_ROW_OFF = "- id: tool-plugin-manager\n name: '@deepseek-ai/dsh-plugin-manager/tools'\n disabled: true\n"
734
- const PRESENT_ROW_FULL = "\n- id: present\n name: '@deepseek-ai/dsh-tool-present'\n"
735
-
736
- function injectPluginManagerRow(text, { enabled }) {
737
- if (text.includes("'@deepseek-ai/dsh-plugin-manager/tools'")) return text
738
- const body = enabled ? PLUGIN_MANAGER_ROW_ON : PLUGIN_MANAGER_ROW_OFF
739
- const at = text.indexOf(PRESENT_ROW_FULL)
740
- if (at !== -1) return text.slice(0, at + PRESENT_ROW_FULL.length) + body + text.slice(at + PRESENT_ROW_FULL.length)
741
- return text.endsWith('\n') ? text + body : text + '\n' + body
742
- }
743
-
744
-
745
- let pluginManagerToolsCache
746
- /** Probe whether THIS host can resolve the plugin-manager tools package (cached per boot). */
747
- async function hostHasPluginManagerTools() {
748
- if (pluginManagerToolsCache !== undefined) return pluginManagerToolsCache
749
- try {
750
- const { createRequire } = await import('node:module')
751
- createRequire(import.meta.url).resolve('@deepseek-ai/dsh-plugin-manager/tools')
752
- pluginManagerToolsCache = true
753
- } catch {
754
- pluginManagerToolsCache = false
755
- }
756
- return pluginManagerToolsCache
757
- }
758
-
759
- /** Write one preset directory from scratch. Returns 'ok' or 'no-skills-source'. */
760
- function materialize({ target, presetId, skillsSource, version, base = 'code', persona = 'text', present = false, pluginManager = false, rows }) {
761
- rmSync(target, { recursive: true, force: true })
762
- mkdirSync(target, { recursive: true })
763
-
764
- const available = readdirSync(join(pkgDir, 'assets', presetId)).filter((f) => f.endsWith('.yml'))
765
- const compositionFile = pickComposition(base, persona, available)
766
- let composition = readFileSync(join(pkgDir, 'assets', presetId, compositionFile), 'utf8')
767
- if (present && compositionFile.includes('.ptc.')) composition = injectPresentRow(composition)
768
- // v0.15.0: ptc-era files only; the ptc-based variant mirrors the official
769
- // ptc DISABLED form, standard/cordis mirror their ENABLED bases, minimal is
770
- // untouched (official minimal never gained the row).
771
- if (pluginManager && compositionFile.includes('.ptc.') && presetId !== 'minimal-gitbash') {
772
- composition = injectPluginManagerRow(composition, { enabled: presetId !== 'code-gitbash' })
773
- }
774
- // Row-form alignment (v0.14.0): the committed assets pin one spelling of the
775
- // workflow-engine row, but a host renames it out from under them (dsh
776
- // 0.1.6-alpha.1) and a row whose module fails to import rejects the whole
777
- // mount. Copy the host's own spelling rather than shipping a third asset
778
- // variant per era. Absent probe = leave the frozen history untouched.
779
- const rowSource = ROW_SOURCE[presetId]
780
- const rowForm = rows && rowSource ? rows[rowSource] : undefined
781
- if (rowForm) {
782
- composition = alignEngineRow(composition, rowForm.engine)
783
- composition = alignRalphRow(composition, rowForm.ralph)
784
- }
785
- writeFileSync(join(target, 'agent.cordis.yml'), composition)
786
- writeFileSync(join(target, 'preset.yml'), readFileSync(join(pkgDir, 'assets', presetId, 'preset.yml')))
787
-
788
- let skills = 'none'
789
- if (presetId === 'cordis-gitbash') {
790
- if (skillsSource && existsSync(skillsSource)) {
791
- cpSync(skillsSource, join(target, 'skills'), { recursive: true, force: true, dereference: true })
792
- skills = 'copied'
793
- } else {
794
- mkdirSync(join(target, 'skills'), { recursive: true })
795
- skills = 'missing-source'
796
- }
797
- }
798
-
799
- const marker = { managedBy: MANAGED_BY, version, presetId, base, persona, present, pluginManager, rows: rowFingerprint(rows, presetId), files: hashTree(target) }
800
- writeFileSync(join(target, MARKER_FILE), JSON.stringify(marker, null, 2) + '\n')
801
- return skills
802
- }
803
-
804
- /** Disposal-time cleanup for one preset. */
805
- function cleanupOnDispose({ target, packageJsonExists }) {
806
- if (packageJsonExists) return 'kept-package-intact'
807
- const state = classify(target)
808
- if (state === 'unmodified') {
809
- rmSync(target, { recursive: true, force: true })
810
- return 'removed'
811
- }
812
- return `kept-${state}`
813
- }
814
-
815
355
  // ── inspect-registry compatibility shim ─────────────────────────────────────
816
356
  //
817
357
  // The host-plane runner's inspect registry is a process-global singleton and
@@ -1100,36 +640,6 @@ export function readSuppressPeerCordis(ctxLike) {
1100
640
  }
1101
641
  }
1102
642
 
1103
- /**
1104
- * Bounded probe for dsh-ptc-cordis-preset's coverage capability. The OLD era
1105
- * has no registration registry to watch, and rows mount concurrently, so poll
1106
- * briefly when the service is not there yet; a short absence means the peer is
1107
- * not installed (or is older than 0.14.0) and every configured variant stays.
1108
- * @param {object} ctx - the plugin's mounting context.
1109
- * @param {number} [timeoutMs] - total wait before giving up.
1110
- * @param {number} [intervalMs] - poll interval.
1111
- * @returns {Promise<boolean>} the peer's `gitBashActive`, false when unknown.
1112
- */
1113
- export async function detectPeerCoverage(ctx, timeoutMs = 1000, intervalMs = 25) {
1114
- try {
1115
- const probe = () => {
1116
- const capability = ctx.get(PEER_CAPABILITY)
1117
- return capability === null || capability === undefined ? undefined : capability
1118
- }
1119
- let capability = probe()
1120
- if (capability === undefined) {
1121
- const deadline = Date.now() + timeoutMs
1122
- while (Date.now() < deadline) {
1123
- await new Promise((resolve) => setTimeout(resolve, intervalMs))
1124
- capability = probe()
1125
- if (capability !== undefined) break
1126
- }
1127
- }
1128
- return capability?.gitBashActive === true
1129
- } catch {
1130
- return false
1131
- }
1132
- }
1133
643
 
1134
644
  // Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
1135
645
  // (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
@@ -2096,10 +1606,6 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
2096
1606
  // ── plugin ──────────────────────────────────────────────────────────────────
2097
1607
 
2098
1608
  /** First user-trust root: the roster's authoring target. */
2099
- function firstUserRoot(roots) {
2100
- for (const r of roots) if (r && r.trust === 'user' && typeof r.path === 'string') return r
2101
- return undefined
2102
- }
2103
1609
 
2104
1610
  /** Directory of skills inside the installed shipped `cordis` preset, if any. */
2105
1611
  async function findSkillsSource(agentPresets) {
@@ -3008,92 +2514,6 @@ export async function apply(ctx, config = {}) {
3008
2514
  return
3009
2515
  }
3010
2516
 
3011
- const roots = ctx.agentPresets?.roots ?? []
3012
- const userRoot = firstUserRoot(roots)
3013
- if (!userRoot) {
3014
- console.log(`${TAG} no user-trust preset root configured — nothing to materialize`)
3015
- return
3016
- }
3017
-
3018
- let version = '0.0.0'
3019
- try {
3020
- version = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')).version ?? version
3021
- } catch {
3022
- /* fall back to the placeholder */
3023
- }
3024
-
3025
- const skillsSource = await findSkillsSource(ctx.agentPresets)
3026
- const base = await detectBase(ctx.agentPresets)
3027
- const persona = await detectPersonaEra(ctx.agentPresets)
3028
- // Two independent signals, either of which is sufficient: the shipped
3029
- // composition text (authoritative on every install layout) and the package
3030
- // resolving from this plugin's own tree (covers hosts where the roster
3031
- // probe is unavailable).
3032
- const present = (await detectPresentSupport(ctx.agentPresets)) || (await hostHasToolPresent())
3033
- // v0.15.0 (dsh 0.1.6-alpha.2): official ptc/standard/cordis gained the
3034
- // tool-plugin-manager row; inject behind the same host-package probe as
3035
- // present (resolve-only is sufficient — the package cannot exist on an
3036
- // older host, so a roster text probe adds nothing).
3037
- const pluginManager = await hostHasPluginManagerTools()
3038
- // Row spellings of the built-in presets these variants mirror: the
3039
- // workflow-engine row was renamed and its old package deleted in dsh
3040
- // 0.1.6-alpha.1, so the materializer copies whatever the host itself ships.
3041
- const rows = await detectRowForms(ctx.agentPresets)
3042
- const userRootPath = userRoot.path
3043
- // Same dedupe decision as the declarative era, taken once at startup here:
3044
- // this host has no roster registry to watch and no live Config writes, so the
3045
- // switch is read per boot (documented in the README). `purgeOrphans` retires
3046
- // a copy an earlier boot materialized, and flipping the switch back
3047
- // re-materializes it on the next start.
3048
- const suppressPeer = liveSettings.suppressPeerCordis()
3049
- const peerGitBash = suppressPeer ? await detectPeerCoverage(ctx) : false
3050
- const effectiveIds = effectivePresetIds(presetIds, { suppress: suppressPeer, peerGitBash })
3051
- if (effectiveIds.length !== presetIds.length) {
3052
- const skipped = presetIds.filter((presetId) => !effectiveIds.includes(presetId))
3053
- console.log(`${TAG} dsh-ptc-cordis-preset covers Creation mode on Git Bash — not materializing: ${skipped.join(', ')}`)
3054
- }
3055
- purgeOrphans(userRootPath, effectiveIds)
3056
-
3057
- for (const presetId of effectiveIds) {
3058
- const target = join(userRootPath, presetId)
3059
- const state = classify(target)
3060
- if (state === 'foreign') {
3061
- console.log(`${TAG} a preset not written by this plugin already exists at ${target} — leaving it alone`)
3062
- continue
3063
- }
3064
- if (state === 'user-modified') {
3065
- console.log(`${TAG} preset at ${target} was modified after materialization — keeping the user's version (delete the directory to re-materialize)`)
3066
- continue
3067
- }
3068
-
3069
- // Reversible side effect, registered BEFORE the idle check so a quiet
3070
- // startup keeps uninstall hygiene.
3071
- ctx.effect(() => () => {
3072
- try {
3073
- const result = cleanupOnDispose({ target, packageJsonExists: existsSync(join(pkgDir, 'package.json')) })
3074
- if (result === 'removed') console.log(`${TAG} package uninstalled — removed the unmodified '${presetId}' preset`)
3075
- else if (result !== 'kept-package-intact' && result !== 'kept-absent') console.log(`${TAG} preset ${result} on disposal — kept`)
3076
- } catch (error) {
3077
- console.log(`${TAG} cleanup skipped: ${error?.message ?? error}`)
3078
- }
3079
- }, `dsh-gitbash-shell: preset materialization (${presetId})`)
3080
-
3081
- const sourceHashes = presetId === 'cordis-gitbash' ? skillsHashes(skillsSource) : null
3082
- const marker = readMarker(target)
3083
- const markerRows = rowFingerprint(rows, presetId)
3084
- if (state === 'unmodified' && syncDecision({ state, marker, version, sourceHashes, base, persona, present, pluginManager, rows: markerRows }) === 'idle') {
3085
- ctx.logger?.('gitbash-shell')?.debug?.( `preset '${presetId}' up to date (v${version}, ${base}-era${persona === 'split' ? ', persona-split' : ''}) — idle`)
3086
- continue
3087
- }
3088
-
3089
- const skills = materialize({ target, presetId, skillsSource, version, base, persona, present, pluginManager, rows })
3090
- const verb = state === 'absent' ? 'materialized' : 'refreshed'
3091
- console.log(
3092
- `${TAG} ${verb} preset '${presetId}' into ${userRootPath} (v${version}, ${base}-era composition${persona === 'split' ? ', persona-split' : ''})` +
3093
- (skills === 'copied' ? " (skills copied from the installed 'cordis' preset)" : '')
3094
- )
3095
- }
3096
2517
  }
3097
-
3098
2518
  // Test surface: pure helpers, no Cordis context required.
3099
- export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, detectPeerCoverage, readSuppressPeerCordis, translateDispatch, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, injectPluginManagerRow, hostHasPluginManagerTools, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }
2519
+ export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, readSuppressPeerCordis, translateDispatch, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, hashTree, installRegisterShim }