dsh-gitbash-shell 0.31.1 → 0.32.1

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
@@ -868,14 +408,12 @@ export function installRegisterShim(reg) {
868
408
  // ── unified path dialect: settings namespace + gate (v0.9.0) ───────────────
869
409
  //
870
410
  // The dialect (directive + translation wrapper) is OFF by default and gated
871
- // by the posixPaths boolean in the 'gitbash-shell' settings namespace,
872
- // flipped from the Settings → Plugins card (src/client.js). The directive's
873
- // text closure returns '' while off — empty context contributions are
874
- // dropped at assembly, so a disabled dialect adds zero prompt noise; the
411
+ // by the posixPaths boolean on this plugin's row Config (volatile, read live
412
+ // per dispatch), flipped from the plugin's settings card (src/client.js). The
413
+ // directive's text closure returns '' while off — empty context contributions
414
+ // are dropped at assembly, so a disabled dialect adds zero prompt noise; the
875
415
  // wrapper reads the same value per dispatch and passes calls through.
876
416
 
877
- const SETTINGS_NAMESPACE = 'gitbash-shell'
878
-
879
417
  /**
880
418
  * dsh >= 0.1.7 marks a Config field live-editable without remount; a
881
419
  * 0.1.6-era schemastery predates the method and the guard keeps this module
@@ -953,14 +491,16 @@ export function valueOf(value) {
953
491
  * @returns {{ dialect: () => object, posix: () => boolean, adoptSidebar: () => boolean, suppressPeerCordis: () => boolean }}
954
492
  */
955
493
  function makeLiveReader(ctx, config) {
956
- let legacy = false
957
- try {
958
- const settings = ctx.get('settings')
959
- legacy = !!(settings && typeof settings.register === 'function')
960
- } catch { legacy = false }
961
494
  const hasConfig = !!(config && typeof config === 'object')
962
495
  const dialect = () => {
963
- if (legacy || !hasConfig) return readDialectSettings(ctx)
496
+ if (!hasConfig) {
497
+ // No row Config (schemastery unresolvable): everything off except the
498
+ // opt-out booleans, the exact shape the old fallback returned.
499
+ return {
500
+ posixPaths: false, virtualMounts: false, globSplit: false, errorDialect: false,
501
+ codePaths: false, gitAutocrlf: false, bashPath: '', subagentDialect: true, autoTerminalShell: true,
502
+ }
503
+ }
964
504
  const bash = valueOf(config.bashPath)
965
505
  return {
966
506
  posixPaths: valueOf(config.posixPaths) === true,
@@ -982,8 +522,8 @@ function makeLiveReader(ctx, config) {
982
522
  posix: () => dialect().posixPaths === true,
983
523
  adoptSidebar: () => dialect().adoptSidebar !== false,
984
524
  autoTerminalShell: () => dialect().autoTerminalShell !== false,
985
- // Default OFF on both eras: only an explicit true asks for deduplication.
986
- suppressPeerCordis: () => (legacy || !hasConfig ? readSuppressPeerCordis(ctx) : valueOf(config.suppressPeerCordis) === true),
525
+ // Default OFF: only an explicit true asks for deduplication.
526
+ suppressPeerCordis: () => (hasConfig ? valueOf(config.suppressPeerCordis) === true : false),
987
527
  }
988
528
  }
989
529
 
@@ -1028,109 +568,6 @@ const PATH_DIALECT_DESCRIPTION = 'Path dialect for tool calls and tool results:
1028
568
  // They cannot ride the shell-env registry below: that registry accepts DSH_*
1029
569
  // facts only, and a non-DSH key throws the whole contribution away.
1030
570
 
1031
- /** Read the posixPaths switch from the live settings service; never throws. */
1032
- export function readPosixPaths(ctxLike) {
1033
- try {
1034
- const settings = ctxLike && typeof ctxLike.get === 'function' ? ctxLike.get('settings') : undefined
1035
- const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
1036
- return !!(value && value.posixPaths === true)
1037
- } catch {
1038
- return false
1039
- }
1040
- }
1041
-
1042
- /**
1043
- * Read every dialect switch at once (v0.19.0): posixPaths gates the whole
1044
- * dialect; virtualMounts gates the bash virtual mounts + ~ + $VAR expansion
1045
- * (off = strict drive-root-only translation); globSplit gates the absolute
1046
- * glob-pattern rewrite; errorDialect gates error-message translation and the
1047
- * NUL guidance block; bashPath (optional) joins the Git-root probe candidates
1048
- * first. All default ON (schema defaults); never throws.
1049
- */
1050
- export function readDialectSettings(ctxLike) {
1051
- const fallback = { posixPaths: false, virtualMounts: false, globSplit: false, errorDialect: false, codePaths: false, gitAutocrlf: false, bashPath: '', subagentDialect: true, autoTerminalShell: true }
1052
- try {
1053
- const settings = ctxLike && typeof ctxLike.get === 'function' ? ctxLike.get('settings') : undefined
1054
- const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
1055
- const v = value && typeof value === 'object' ? value : {}
1056
- return {
1057
- posixPaths: v.posixPaths === true,
1058
- virtualMounts: v.virtualMounts === true,
1059
- globSplit: v.globSplit === true,
1060
- errorDialect: v.errorDialect === true,
1061
- codePaths: v.codePaths === true,
1062
- // An old host without the key keeps the default (delegated agents covered).
1063
- subagentDialect: v.subagentDialect !== false,
1064
- // Default ON: a host without the key still gets the terminal adoption.
1065
- autoTerminalShell: v.autoTerminalShell !== false,
1066
- gitAutocrlf: v.gitAutocrlf === true,
1067
- bashPath: typeof v.bashPath === 'string' ? v.bashPath : '',
1068
- }
1069
- } catch {
1070
- return fallback
1071
- }
1072
- }
1073
-
1074
- /**
1075
- * Read the adoptSidebar switch (v0.15.0, default ON). Unlike posixPaths the
1076
- * default is on, so only an explicit false disables the takeover.
1077
- */
1078
- export function readAdoptSidebar(ctxLike) {
1079
- try {
1080
- const settings = ctxLike && typeof ctxLike.get === 'function' ? ctxLike.get('settings') : undefined
1081
- const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
1082
- return !(value && value.adoptSidebar === false)
1083
- } catch {
1084
- return true
1085
- }
1086
- }
1087
-
1088
- /**
1089
- * Read the peer-dedupe switch (v0.25.0, default OFF) from the OLD era's
1090
- * registered namespace — on dsh >= 0.1.7 the row Config is the surface and
1091
- * `makeLiveReader` reads it directly. Never throws.
1092
- */
1093
- export function readSuppressPeerCordis(ctxLike) {
1094
- try {
1095
- const settings = ctxLike && typeof ctxLike.get === 'function' ? ctxLike.get('settings') : undefined
1096
- const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
1097
- return !!(value && value.suppressPeerCordis === true)
1098
- } catch {
1099
- return false
1100
- }
1101
- }
1102
-
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
-
1134
571
  // Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
1135
572
  // (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
1136
573
  // path stops at a closing punctuation and crosses a space ONLY into a token that
@@ -1960,10 +1397,6 @@ const SIDEBAR_NS = 'dsh-better-sidebar'
1960
1397
  const SIDEBAR_TRIES = 12
1961
1398
  const SIDEBAR_RETRY_MS = 1500
1962
1399
 
1963
- // Owner scope of the gitbash-shell namespace once registered (shared with
1964
- // the register callback so a late registration can reconcile + watch).
1965
- let sidebarScope = undefined
1966
-
1967
1400
  // Reconciler installed by adoptSidebarShell; a no-op until then.
1968
1401
  let reconcileSidebar = () => {}
1969
1402
 
@@ -1992,14 +1425,9 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
1992
1425
  const settings = ctx.get('settings')
1993
1426
  if (!settings || typeof settings.update !== 'function') return null
1994
1427
  try {
1995
- // <=0.1.6: the sidebar registers a settings namespace and `get(ns)` reads
1996
- // it. >=0.1.7: that API is gone (the service exposes describe/update
1997
- // only), so the value comes from the row Config's projected form —
1998
- // without this branch the adoption was silently skipped on the new host.
1999
- if (typeof settings.get === 'function') {
2000
- const value = settings.get(SIDEBAR_NS)
2001
- return value && typeof value === 'object' && typeof value.terminalShell === 'string' ? value.terminalShell : ''
2002
- }
1428
+ // The sidebar's value lives in ITS row Config, projected into the
1429
+ // settings describe() list; the service exposes describe/update only
1430
+ // (the old `get(ns)` reader died with dsh 0.1.6).
2003
1431
  if (typeof settings.describe === 'function') {
2004
1432
  const entry = settings.describe().find((row) => row !== null && typeof row === 'object' && row.ns === SIDEBAR_NS)
2005
1433
  const value = entry === undefined ? undefined : entry.value
@@ -2050,18 +1478,7 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
2050
1478
  const run = () => {
2051
1479
  tried += 1
2052
1480
  const ok = reconcileSidebar(readAdopt())
2053
- if (ok) {
2054
- // The namespace scope may register after the settings service; hook
2055
- // the live watch once it exists (idempotent via the effect disposer).
2056
- if (sidebarScope && typeof sidebarScope.watch === 'function' && !run.watched) {
2057
- run.watched = true
2058
- try {
2059
- const off = sidebarScope.watch((next) => { reconcileSidebar(next && next.adoptSidebar !== false) })
2060
- ctx.effect(() => off, 'dsh-gitbash-shell: adoptSidebar watch (polling path)')
2061
- } catch { /* best effort */ }
2062
- }
2063
- return
2064
- }
1481
+ if (ok) return
2065
1482
  if (tried < SIDEBAR_TRIES) {
2066
1483
  timer = setTimeout(run, SIDEBAR_RETRY_MS)
2067
1484
  return
@@ -2096,10 +1513,6 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
2096
1513
  // ── plugin ──────────────────────────────────────────────────────────────────
2097
1514
 
2098
1515
  /** 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
1516
 
2104
1517
  /** Directory of skills inside the installed shipped `cordis` preset, if any. */
2105
1518
  async function findSkillsSource(agentPresets) {
@@ -2416,82 +1829,11 @@ export async function apply(ctx, config = {}) {
2416
1829
  const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
2417
1830
  ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
2418
1831
 
2419
- // Era-aware live settings: NEW hosts read this plugin's own Config refs
2420
- // (volatile, re-read per dispatch/assembly), OLD hosts keep the registered
2421
- // namespace. Every consumer below already re-reads on every use, so a
2422
- // flipped knob lands on the next dispatch with zero wiring.
1832
+ // Era-aware live settings: every consumer below re-reads the row Config
1833
+ // refs on every use (volatile), so a flipped knob lands on the next
1834
+ // dispatch with zero wiring.
2423
1835
  const liveSettings = makeLiveReader(ctx, config)
2424
1836
 
2425
- // ── settings namespace: the posixPaths switch (default ON, v0.1.0) — OLD
2426
- // hosts only; on dsh >= 0.1.7 the row Config above IS the surface.
2427
-
2428
- // Served on the host so the Plugins tab pairs it with the browser card
2429
- // (the tab dispatches Host-served namespaces ∩ registered cards). Dynamic
2430
- // imports keep the zero-dependency smoke path importable; the schema MUST
2431
- // be a callable schemastery object (dsh-settings calls schema(merged)).
2432
- const legacySettings = (() => {
2433
- try {
2434
- const settings = ctx.get('settings')
2435
- return !!(settings && typeof settings.register === 'function')
2436
- } catch {
2437
- return false
2438
- }
2439
- })()
2440
- if (legacySettings) try {
2441
- ctx.inject(['settings'], (sctx) => {
2442
- Promise.resolve(import('@deepseek-ai/dsh-settings'))
2443
- .then((ds) => {
2444
- const settings = sctx && sctx.settings
2445
- if (!settings || typeof settings.register !== 'function') return
2446
- // The SAME copy the executor and the row Config were built from
2447
- // (issue #12): one resolution per process, so the two faces of this
2448
- // plugin can never disagree about the schema vocabulary.
2449
- if (!Schema) return
2450
- // Era probe: newer dsh register() takes a plain string; the older
2451
- // one accepted the branded helper — one call satisfies both.
2452
- const ns = typeof ds.settingsNamespace === 'function'
2453
- ? ds.settingsNamespace(SETTINGS_NAMESPACE)
2454
- : SETTINGS_NAMESPACE
2455
- const scope = settings.register(ns, Schema.object({
2456
- posixPaths: Schema.boolean().default(true),
2457
- virtualMounts: Schema.boolean().default(true),
2458
- globSplit: Schema.boolean().default(true),
2459
- errorDialect: Schema.boolean().default(true),
2460
- codePaths: Schema.boolean().default(true),
2461
- subagentDialect: Schema.boolean().default(true),
2462
- gitAutocrlf: Schema.boolean().default(true),
2463
- bashPath: Schema.string().default(''),
2464
- adoptSidebar: Schema.boolean().default(true),
2465
- // v0.29.0: official sidebar terminal adoption (default ON).
2466
- autoTerminalShell: Schema.boolean().default(true),
2467
- // v0.25.0: same switch as the row Config carries on dsh >= 0.1.7
2468
- // (the OLD era has no cross-namespace edits, so this namespace is
2469
- // the only side that can change it here).
2470
- suppressPeerCordis: Schema.boolean().default(false),
2471
- }))
2472
- sidebarScope = scope
2473
- console.log(`${TAG} settings namespace registered: ${SETTINGS_NAMESPACE} (posixPaths default on, adoptSidebar default on)`)
2474
- // A late register (settings service up after the adoption polling
2475
- // finished) still reconciles once, and the live watch flips the
2476
- // takeover when the user toggles the card switch.
2477
- try {
2478
- if (typeof scope?.watch === 'function') {
2479
- const off = scope.watch((next) => { reconcileSidebar(next && next.adoptSidebar !== false) })
2480
- ctx.effect(() => off, 'dsh-gitbash-shell: adoptSidebar watch')
2481
- }
2482
- reconcileSidebar(readAdoptSidebar(ctx))
2483
- } catch (error) {
2484
- console.log(`${TAG} adoptSidebar watch wiring failed: ${error?.message ?? error}`)
2485
- }
2486
- })
2487
- .catch((error) => {
2488
- console.log(`${TAG} settings namespace registration FAILED: ${error && error.stack || String(error)}`)
2489
- })
2490
- })
2491
- } catch (error) {
2492
- console.log(`${TAG} settings inject wiring failed: ${error?.message ?? error}`)
2493
- }
2494
-
2495
1837
  // ── official shell-env fact: DSH_PATH_DIALECT (Windows only, gated; v0.11.0) ──
2496
1838
  // dsh-shell-env owns the model-visible $DSH_* facts (host-plane service;
2497
1839
  // the web composition injects it and publishes DSH_WEB_URL beside the
@@ -3008,92 +2350,6 @@ export async function apply(ctx, config = {}) {
3008
2350
  return
3009
2351
  }
3010
2352
 
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
2353
  }
3097
-
3098
2354
  // 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 }
2355
+ export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, translateDispatch, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, hashTree, installRegisterShim }