dsh-gitbash-shell 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dsh.plugin.json +3 -3
- package/package.json +3 -4
- package/src/index.js +1 -581
- 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
|
|
@@ -1100,36 +640,6 @@ export function readSuppressPeerCordis(ctxLike) {
|
|
|
1100
640
|
}
|
|
1101
641
|
}
|
|
1102
642
|
|
|
1103
|
-
/**
|
|
1104
|
-
* Bounded probe for dsh-ptc-cordis-preset's coverage capability. The OLD era
|
|
1105
|
-
* has no registration registry to watch, and rows mount concurrently, so poll
|
|
1106
|
-
* briefly when the service is not there yet; a short absence means the peer is
|
|
1107
|
-
* not installed (or is older than 0.14.0) and every configured variant stays.
|
|
1108
|
-
* @param {object} ctx - the plugin's mounting context.
|
|
1109
|
-
* @param {number} [timeoutMs] - total wait before giving up.
|
|
1110
|
-
* @param {number} [intervalMs] - poll interval.
|
|
1111
|
-
* @returns {Promise<boolean>} the peer's `gitBashActive`, false when unknown.
|
|
1112
|
-
*/
|
|
1113
|
-
export async function detectPeerCoverage(ctx, timeoutMs = 1000, intervalMs = 25) {
|
|
1114
|
-
try {
|
|
1115
|
-
const probe = () => {
|
|
1116
|
-
const capability = ctx.get(PEER_CAPABILITY)
|
|
1117
|
-
return capability === null || capability === undefined ? undefined : capability
|
|
1118
|
-
}
|
|
1119
|
-
let capability = probe()
|
|
1120
|
-
if (capability === undefined) {
|
|
1121
|
-
const deadline = Date.now() + timeoutMs
|
|
1122
|
-
while (Date.now() < deadline) {
|
|
1123
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
1124
|
-
capability = probe()
|
|
1125
|
-
if (capability !== undefined) break
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
return capability?.gitBashActive === true
|
|
1129
|
-
} catch {
|
|
1130
|
-
return false
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
643
|
|
|
1134
644
|
// Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
|
|
1135
645
|
// (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
|
|
@@ -2096,10 +1606,6 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
|
|
|
2096
1606
|
// ── plugin ──────────────────────────────────────────────────────────────────
|
|
2097
1607
|
|
|
2098
1608
|
/** First user-trust root: the roster's authoring target. */
|
|
2099
|
-
function firstUserRoot(roots) {
|
|
2100
|
-
for (const r of roots) if (r && r.trust === 'user' && typeof r.path === 'string') return r
|
|
2101
|
-
return undefined
|
|
2102
|
-
}
|
|
2103
1609
|
|
|
2104
1610
|
/** Directory of skills inside the installed shipped `cordis` preset, if any. */
|
|
2105
1611
|
async function findSkillsSource(agentPresets) {
|
|
@@ -3008,92 +2514,6 @@ export async function apply(ctx, config = {}) {
|
|
|
3008
2514
|
return
|
|
3009
2515
|
}
|
|
3010
2516
|
|
|
3011
|
-
const roots = ctx.agentPresets?.roots ?? []
|
|
3012
|
-
const userRoot = firstUserRoot(roots)
|
|
3013
|
-
if (!userRoot) {
|
|
3014
|
-
console.log(`${TAG} no user-trust preset root configured — nothing to materialize`)
|
|
3015
|
-
return
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
let version = '0.0.0'
|
|
3019
|
-
try {
|
|
3020
|
-
version = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')).version ?? version
|
|
3021
|
-
} catch {
|
|
3022
|
-
/* fall back to the placeholder */
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
const skillsSource = await findSkillsSource(ctx.agentPresets)
|
|
3026
|
-
const base = await detectBase(ctx.agentPresets)
|
|
3027
|
-
const persona = await detectPersonaEra(ctx.agentPresets)
|
|
3028
|
-
// Two independent signals, either of which is sufficient: the shipped
|
|
3029
|
-
// composition text (authoritative on every install layout) and the package
|
|
3030
|
-
// resolving from this plugin's own tree (covers hosts where the roster
|
|
3031
|
-
// probe is unavailable).
|
|
3032
|
-
const present = (await detectPresentSupport(ctx.agentPresets)) || (await hostHasToolPresent())
|
|
3033
|
-
// v0.15.0 (dsh 0.1.6-alpha.2): official ptc/standard/cordis gained the
|
|
3034
|
-
// tool-plugin-manager row; inject behind the same host-package probe as
|
|
3035
|
-
// present (resolve-only is sufficient — the package cannot exist on an
|
|
3036
|
-
// older host, so a roster text probe adds nothing).
|
|
3037
|
-
const pluginManager = await hostHasPluginManagerTools()
|
|
3038
|
-
// Row spellings of the built-in presets these variants mirror: the
|
|
3039
|
-
// workflow-engine row was renamed and its old package deleted in dsh
|
|
3040
|
-
// 0.1.6-alpha.1, so the materializer copies whatever the host itself ships.
|
|
3041
|
-
const rows = await detectRowForms(ctx.agentPresets)
|
|
3042
|
-
const userRootPath = userRoot.path
|
|
3043
|
-
// Same dedupe decision as the declarative era, taken once at startup here:
|
|
3044
|
-
// this host has no roster registry to watch and no live Config writes, so the
|
|
3045
|
-
// switch is read per boot (documented in the README). `purgeOrphans` retires
|
|
3046
|
-
// a copy an earlier boot materialized, and flipping the switch back
|
|
3047
|
-
// re-materializes it on the next start.
|
|
3048
|
-
const suppressPeer = liveSettings.suppressPeerCordis()
|
|
3049
|
-
const peerGitBash = suppressPeer ? await detectPeerCoverage(ctx) : false
|
|
3050
|
-
const effectiveIds = effectivePresetIds(presetIds, { suppress: suppressPeer, peerGitBash })
|
|
3051
|
-
if (effectiveIds.length !== presetIds.length) {
|
|
3052
|
-
const skipped = presetIds.filter((presetId) => !effectiveIds.includes(presetId))
|
|
3053
|
-
console.log(`${TAG} dsh-ptc-cordis-preset covers Creation mode on Git Bash — not materializing: ${skipped.join(', ')}`)
|
|
3054
|
-
}
|
|
3055
|
-
purgeOrphans(userRootPath, effectiveIds)
|
|
3056
|
-
|
|
3057
|
-
for (const presetId of effectiveIds) {
|
|
3058
|
-
const target = join(userRootPath, presetId)
|
|
3059
|
-
const state = classify(target)
|
|
3060
|
-
if (state === 'foreign') {
|
|
3061
|
-
console.log(`${TAG} a preset not written by this plugin already exists at ${target} — leaving it alone`)
|
|
3062
|
-
continue
|
|
3063
|
-
}
|
|
3064
|
-
if (state === 'user-modified') {
|
|
3065
|
-
console.log(`${TAG} preset at ${target} was modified after materialization — keeping the user's version (delete the directory to re-materialize)`)
|
|
3066
|
-
continue
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
// Reversible side effect, registered BEFORE the idle check so a quiet
|
|
3070
|
-
// startup keeps uninstall hygiene.
|
|
3071
|
-
ctx.effect(() => () => {
|
|
3072
|
-
try {
|
|
3073
|
-
const result = cleanupOnDispose({ target, packageJsonExists: existsSync(join(pkgDir, 'package.json')) })
|
|
3074
|
-
if (result === 'removed') console.log(`${TAG} package uninstalled — removed the unmodified '${presetId}' preset`)
|
|
3075
|
-
else if (result !== 'kept-package-intact' && result !== 'kept-absent') console.log(`${TAG} preset ${result} on disposal — kept`)
|
|
3076
|
-
} catch (error) {
|
|
3077
|
-
console.log(`${TAG} cleanup skipped: ${error?.message ?? error}`)
|
|
3078
|
-
}
|
|
3079
|
-
}, `dsh-gitbash-shell: preset materialization (${presetId})`)
|
|
3080
|
-
|
|
3081
|
-
const sourceHashes = presetId === 'cordis-gitbash' ? skillsHashes(skillsSource) : null
|
|
3082
|
-
const marker = readMarker(target)
|
|
3083
|
-
const markerRows = rowFingerprint(rows, presetId)
|
|
3084
|
-
if (state === 'unmodified' && syncDecision({ state, marker, version, sourceHashes, base, persona, present, pluginManager, rows: markerRows }) === 'idle') {
|
|
3085
|
-
ctx.logger?.('gitbash-shell')?.debug?.( `preset '${presetId}' up to date (v${version}, ${base}-era${persona === 'split' ? ', persona-split' : ''}) — idle`)
|
|
3086
|
-
continue
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
const skills = materialize({ target, presetId, skillsSource, version, base, persona, present, pluginManager, rows })
|
|
3090
|
-
const verb = state === 'absent' ? 'materialized' : 'refreshed'
|
|
3091
|
-
console.log(
|
|
3092
|
-
`${TAG} ${verb} preset '${presetId}' into ${userRootPath} (v${version}, ${base}-era composition${persona === 'split' ? ', persona-split' : ''})` +
|
|
3093
|
-
(skills === 'copied' ? " (skills copied from the installed 'cordis' preset)" : '')
|
|
3094
|
-
)
|
|
3095
|
-
}
|
|
3096
2517
|
}
|
|
3097
|
-
|
|
3098
2518
|
// Test surface: pure helpers, no Cordis context required.
|
|
3099
|
-
export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds,
|
|
2519
|
+
export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, readSuppressPeerCordis, translateDispatch, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, hashTree, installRegisterShim }
|