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/CHANGELOG.md +18 -0
- package/dsh.plugin.json +3 -3
- package/package.json +3 -4
- package/src/client.js +12 -44
- package/src/index.js +22 -766
- package/src/shell.js +4 -7
- package/assets/code-gitbash/agent.cordis.ptc.ps.yml +0 -282
- package/assets/code-gitbash/agent.cordis.ptc.yml +0 -276
- package/assets/code-gitbash/agent.cordis.yml +0 -268
- package/assets/code-gitbash/preset.yml +0 -3
- package/assets/cordis-gitbash/agent.cordis.ptc.ps.yml +0 -283
- package/assets/cordis-gitbash/agent.cordis.ptc.yml +0 -267
- package/assets/cordis-gitbash/agent.cordis.yml +0 -267
- package/assets/cordis-gitbash/preset.yml +0 -3
- package/assets/minimal-gitbash/agent.cordis.ps.yml +0 -79
- package/assets/minimal-gitbash/agent.cordis.yml +0 -74
- package/assets/minimal-gitbash/preset.yml +0 -3
- package/assets/standard-gitbash/agent.cordis.ptc.ps.yml +0 -262
- package/assets/standard-gitbash/agent.cordis.ptc.yml +0 -256
- package/assets/standard-gitbash/agent.cordis.yml +0 -256
- package/assets/standard-gitbash/preset.yml +0 -3
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
|
|
872
|
-
// flipped from the
|
|
873
|
-
// text closure returns '' while off — empty context contributions
|
|
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 (
|
|
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
|
|
986
|
-
suppressPeerCordis: () => (
|
|
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
|
-
//
|
|
1996
|
-
//
|
|
1997
|
-
//
|
|
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:
|
|
2420
|
-
// (volatile,
|
|
2421
|
-
//
|
|
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,
|
|
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 }
|