dsh-gitbash-shell 0.24.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -95,6 +95,36 @@ const MARKER_FILE = '.plugin-managed.json'
95
95
  /** Default preset ids this plugin materializes, in roster order (configurable). */
96
96
  export const PRESET_IDS = ['standard-gitbash', 'minimal-gitbash', 'code-gitbash', 'cordis-gitbash']
97
97
 
98
+ /**
99
+ * The variant dsh-ptc-cordis-preset already covers while cooperating: its
100
+ * `PTC 创造模式` is materialized against Git Bash too, so our own
101
+ * `创造模式 · Git Bash` describes the same mode (their issue #7).
102
+ */
103
+ export const PEER_COVERED_PRESET_ID = 'cordis-gitbash'
104
+
105
+ /** Service dsh-ptc-cordis-preset publishes: `{ id, gitBashActive }`. */
106
+ export const PEER_CAPABILITY = 'ptcCordisPreset'
107
+
108
+ /**
109
+ * The roster this plugin serves: the configured list, minus the variant a peer
110
+ * already covers. Two independent facts must BOTH hold before anything is
111
+ * dropped — the user's `suppressPeerCordis` switch (row Config, default OFF)
112
+ * and the peer actually reporting a Git Bash-materialized preset. A peer that
113
+ * is absent, inactive, or mounted later than this row therefore never costs
114
+ * the user a variant, and the default keeps the historical four-variant roster
115
+ * byte for byte.
116
+ * @param {string[]} configured - the row's `presets` list (empty/absent = default).
117
+ * @param {object} [options] - live decision inputs.
118
+ * @param {boolean} [options.suppress] - the live `suppressPeerCordis` switch.
119
+ * @param {boolean} [options.peerGitBash] - the peer capability's `gitBashActive`.
120
+ * @returns {string[]} the ids to register (or materialize), in configured order.
121
+ */
122
+ export function effectivePresetIds(configured, { suppress, peerGitBash } = {}) {
123
+ const list = Array.isArray(configured) && configured.length > 0 ? [...configured] : [...PRESET_IDS]
124
+ if (suppress !== true || peerGitBash !== true) return list
125
+ return list.filter((id) => id !== PEER_COVERED_PRESET_ID)
126
+ }
127
+
98
128
  /** Git Bash binary default — must match src/shell.js. */
99
129
  const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
100
130
 
@@ -721,6 +751,16 @@ export const Config = Schema === null ? undefined : Schema.object({
721
751
  gitAutocrlf: live(Schema.boolean().default(true)),
722
752
  bashPath: live(Schema.string().default('')),
723
753
  adoptSidebar: live(Schema.boolean().default(true)),
754
+ /**
755
+ * Dedupe switch against dsh-ptc-cordis-preset (their issue #7, v0.25.0):
756
+ * ON while that plugin is installed AND materializing its `PTC 创造模式`
757
+ * against Git Bash drops OUR `创造模式 · Git Bash` from the roster, because
758
+ * the two describe the same mode. Default OFF — the historical four-variant
759
+ * roster is unchanged until asked for. The same state is rendered on their
760
+ * settings card too (their card binds this row through configForms), so
761
+ * changing it on either side changes it here.
762
+ */
763
+ suppressPeerCordis: live(Schema.boolean().default(false)),
724
764
  })
725
765
 
726
766
  /** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
@@ -735,7 +775,7 @@ export function valueOf(value) {
735
775
  * reading the registered namespace through the settings service.
736
776
  * @param {object} ctx - the plugin's mounting context.
737
777
  * @param {object|undefined} config - apply()'s resolved Config.
738
- * @returns {{ dialect: () => object, posix: () => boolean, adoptSidebar: () => boolean }}
778
+ * @returns {{ dialect: () => object, posix: () => boolean, adoptSidebar: () => boolean, suppressPeerCordis: () => boolean }}
739
779
  */
740
780
  function makeLiveReader(ctx, config) {
741
781
  let legacy = false
@@ -762,6 +802,8 @@ function makeLiveReader(ctx, config) {
762
802
  dialect,
763
803
  posix: () => dialect().posixPaths === true,
764
804
  adoptSidebar: () => dialect().adoptSidebar !== false,
805
+ // Default OFF on both eras: only an explicit true asks for deduplication.
806
+ suppressPeerCordis: () => (legacy || !hasConfig ? readSuppressPeerCordis(ctx) : valueOf(config.suppressPeerCordis) === true),
765
807
  }
766
808
  }
767
809
 
@@ -859,6 +901,52 @@ export function readAdoptSidebar(ctxLike) {
859
901
  }
860
902
  }
861
903
 
904
+ /**
905
+ * Read the peer-dedupe switch (v0.25.0, default OFF) from the OLD era's
906
+ * registered namespace — on dsh >= 0.1.7 the row Config is the surface and
907
+ * `makeLiveReader` reads it directly. Never throws.
908
+ */
909
+ export function readSuppressPeerCordis(ctxLike) {
910
+ try {
911
+ const settings = ctxLike && typeof ctxLike.get === 'function' ? ctxLike.get('settings') : undefined
912
+ const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
913
+ return !!(value && value.suppressPeerCordis === true)
914
+ } catch {
915
+ return false
916
+ }
917
+ }
918
+
919
+ /**
920
+ * Bounded probe for dsh-ptc-cordis-preset's coverage capability. The OLD era
921
+ * has no registration registry to watch, and rows mount concurrently, so poll
922
+ * briefly when the service is not there yet; a short absence means the peer is
923
+ * not installed (or is older than 0.14.0) and every configured variant stays.
924
+ * @param {object} ctx - the plugin's mounting context.
925
+ * @param {number} [timeoutMs] - total wait before giving up.
926
+ * @param {number} [intervalMs] - poll interval.
927
+ * @returns {Promise<boolean>} the peer's `gitBashActive`, false when unknown.
928
+ */
929
+ export async function detectPeerCoverage(ctx, timeoutMs = 1000, intervalMs = 25) {
930
+ try {
931
+ const probe = () => {
932
+ const capability = ctx.get(PEER_CAPABILITY)
933
+ return capability === null || capability === undefined ? undefined : capability
934
+ }
935
+ let capability = probe()
936
+ if (capability === undefined) {
937
+ const deadline = Date.now() + timeoutMs
938
+ while (Date.now() < deadline) {
939
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
940
+ capability = probe()
941
+ if (capability !== undefined) break
942
+ }
943
+ }
944
+ return capability?.gitBashActive === true
945
+ } catch {
946
+ return false
947
+ }
948
+ }
949
+
862
950
  // Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
863
951
  // (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
864
952
  // path stops at a closing punctuation and crosses a space ONLY into a token that
@@ -1920,26 +2008,107 @@ async function registerVariant(ctx, presetId, { gitBashActive, skillsDir }) {
1920
2008
  }
1921
2009
 
1922
2010
  /**
1923
- * Declarative-era wiring: register every configured variant once at startup
1924
- * and keep exactly one registration per variant for the plugin's lifetime.
1925
- * The roster picks the entries up immediately; sessions already pinned to a
1926
- * variant keep their revision until recomposed.
2011
+ * Declarative-era wiring: keep exactly one registration per VARIANT THIS HOST
2012
+ * SHOULD SERVE for the plugin's lifetime, and re-reconcile whenever that set
2013
+ * changes. Two inputs drive it — the live `suppressPeerCordis` switch and the
2014
+ * peer capability appearing (or leaving) via `ctx.inject`, which fires
2015
+ * independent of row activation order — and the peer's own coverage only
2016
+ * counts while it reports a Git Bash-materialized preset.
2017
+ *
2018
+ * The roster picks entries up immediately; sessions already pinned to a
2019
+ * variant keep their revision until recomposed, so suppressing a variant never
2020
+ * rewrites a running session.
2021
+ * @param {object} ctx - the plugin's mounting context.
2022
+ * @param {string[]} presetIds - the row's configured variants.
2023
+ * @param {() => boolean} readSuppress - live dedupe switch reader.
2024
+ * @returns {Promise<void>} resolves once the first reconcile finished.
1927
2025
  */
1928
- async function runDeclarativeEra(ctx, presetIds) {
2026
+ async function runDeclarativeEra(ctx, presetIds, readSuppress) {
1929
2027
  cleanupLegacyTrees(presetIds)
1930
2028
  const gitBashActive = process.platform === 'win32'
1931
2029
  const skillsDir = await resolveSkillsDir()
1932
- const unregisters = []
1933
- for (const presetId of presetIds) {
1934
- try {
1935
- const unregister = await registerVariant(ctx, presetId, { gitBashActive, skillsDir })
1936
- if (unregister) unregisters.push(unregister)
1937
- } catch (error) {
1938
- console.log(TAG + " preset '" + presetId + "' registration failed: " + (error && error.message ? error.message : error))
1939
- }
2030
+ /** presetId -> unregister, i.e. exactly what is live right now. */
2031
+ const live = new Map()
2032
+ let peerGitBash = false
2033
+ let serial = Promise.resolve()
2034
+
2035
+ const desired = () => effectivePresetIds(presetIds, { suppress: readSuppress(), peerGitBash })
2036
+
2037
+ const reconcile = () => {
2038
+ serial = serial.then(async () => {
2039
+ const want = desired()
2040
+ for (const [presetId, off] of [...live]) {
2041
+ if (want.includes(presetId)) continue
2042
+ live.delete(presetId)
2043
+ try {
2044
+ await off()
2045
+ console.log(TAG + " preset '" + presetId + "' retired (dsh-ptc-cordis-preset covers Creation mode on Git Bash)")
2046
+ } catch (error) {
2047
+ console.log(TAG + " preset '" + presetId + "' retirement failed: " + (error && error.message ? error.message : error))
2048
+ }
2049
+ }
2050
+ for (const presetId of want) {
2051
+ if (live.has(presetId)) continue
2052
+ try {
2053
+ const unregister = await registerVariant(ctx, presetId, { gitBashActive, skillsDir })
2054
+ if (unregister) {
2055
+ live.set(presetId, unregister)
2056
+ console.log(TAG + " preset '" + presetId + "' registered declaratively")
2057
+ }
2058
+ } catch (error) {
2059
+ console.log(TAG + " preset '" + presetId + "' registration failed: " + (error && error.message ? error.message : error))
2060
+ }
2061
+ }
2062
+ }).catch((error) => {
2063
+ console.log(TAG + ' preset reconcile failed: ' + (error && error.message ? error.message : error))
2064
+ })
2065
+ return serial
1940
2066
  }
1941
- console.log(TAG + ' registered ' + unregisters.length + ' preset(s) declaratively (' + presetIds.join(', ') + ')')
1942
- ctx.effect(() => () => { for (const off of unregisters) void off() }, 'dsh-gitbash-shell: declarative preset registrations')
2067
+
2068
+ await reconcile()
2069
+ console.log(TAG + ' registered ' + live.size + ' preset(s) declaratively (' + [...live.keys()].join(', ') + ')')
2070
+
2071
+ // The peer's capability: `ctx.inject` makes this independent of row order,
2072
+ // and the child scope's disposer runs when the peer unmounts (or its row is
2073
+ // disabled), which restores the variant.
2074
+ try {
2075
+ ctx.inject([PEER_CAPABILITY], (peerCtx) => {
2076
+ try {
2077
+ const coverage = peerCtx[PEER_CAPABILITY] ?? peerCtx.get(PEER_CAPABILITY)
2078
+ const next = coverage?.gitBashActive === true
2079
+ if (next !== peerGitBash) {
2080
+ peerGitBash = next
2081
+ void reconcile()
2082
+ }
2083
+ } catch (error) {
2084
+ console.log(TAG + ' peer coverage read failed: ' + (error && error.message ? error.message : error))
2085
+ }
2086
+ peerCtx.effect(() => () => {
2087
+ if (!peerGitBash) return
2088
+ peerGitBash = false
2089
+ void reconcile()
2090
+ }, 'dsh-gitbash-shell: peer coverage release')
2091
+ })
2092
+ } catch (error) {
2093
+ console.log(TAG + ' peer capability wiring failed: ' + (error && error.message ? error.message : error))
2094
+ }
2095
+
2096
+ // The switch itself: a volatile Config edit on this row lands as a
2097
+ // loader/volatile-update event on this fiber.
2098
+ try {
2099
+ ctx.on('loader/volatile-update', (paths) => {
2100
+ try {
2101
+ if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && path[0] === 'suppressPeerCordis')) return
2102
+ void reconcile()
2103
+ } catch (error) {
2104
+ console.log(TAG + ' dedupe switch handling failed: ' + (error && error.message ? error.message : error))
2105
+ }
2106
+ })
2107
+ } catch (error) {
2108
+ console.log(TAG + ' dedupe switch wiring failed: ' + (error && error.message ? error.message : error))
2109
+ }
2110
+
2111
+ ctx.effect(() => () => { for (const off of live.values()) void off() }, 'dsh-gitbash-shell: declarative preset registrations')
1943
2112
  }
1944
2113
 
1945
2114
  export async function apply(ctx, config = {}) {
@@ -2019,6 +2188,10 @@ export async function apply(ctx, config = {}) {
2019
2188
  gitAutocrlf: Schema.boolean().default(true),
2020
2189
  bashPath: Schema.string().default(''),
2021
2190
  adoptSidebar: Schema.boolean().default(true),
2191
+ // v0.25.0: same switch as the row Config carries on dsh >= 0.1.7
2192
+ // (the OLD era has no cross-namespace edits, so this namespace is
2193
+ // the only side that can change it here).
2194
+ suppressPeerCordis: Schema.boolean().default(false),
2022
2195
  }))
2023
2196
  sidebarScope = scope
2024
2197
  console.log(`${TAG} settings namespace registered: ${SETTINGS_NAMESPACE} (posixPaths default on, adoptSidebar default on)`)
@@ -2304,7 +2477,7 @@ export async function apply(ctx, config = {}) {
2304
2477
  // more), so this branch serves the variants and returns.
2305
2478
  if (ctx.agentPresets && typeof ctx.agentPresets.register === 'function') {
2306
2479
  try {
2307
- await runDeclarativeEra(ctx, presetIds)
2480
+ await runDeclarativeEra(ctx, presetIds, () => liveSettings.suppressPeerCordis())
2308
2481
  } catch (error) {
2309
2482
  console.log(TAG + ' declarative registration failed: ' + (error && error.message ? error.message : error))
2310
2483
  }
@@ -2343,9 +2516,21 @@ export async function apply(ctx, config = {}) {
2343
2516
  // 0.1.6-alpha.1, so the materializer copies whatever the host itself ships.
2344
2517
  const rows = await detectRowForms(ctx.agentPresets)
2345
2518
  const userRootPath = userRoot.path
2346
- purgeOrphans(userRootPath, presetIds)
2347
-
2348
- for (const presetId of presetIds) {
2519
+ // Same dedupe decision as the declarative era, taken once at startup here:
2520
+ // this host has no roster registry to watch and no live Config writes, so the
2521
+ // switch is read per boot (documented in the README). `purgeOrphans` retires
2522
+ // a copy an earlier boot materialized, and flipping the switch back
2523
+ // re-materializes it on the next start.
2524
+ const suppressPeer = liveSettings.suppressPeerCordis()
2525
+ const peerGitBash = suppressPeer ? await detectPeerCoverage(ctx) : false
2526
+ const effectiveIds = effectivePresetIds(presetIds, { suppress: suppressPeer, peerGitBash })
2527
+ if (effectiveIds.length !== presetIds.length) {
2528
+ const skipped = presetIds.filter((presetId) => !effectiveIds.includes(presetId))
2529
+ console.log(`${TAG} dsh-ptc-cordis-preset covers Creation mode on Git Bash — not materializing: ${skipped.join(', ')}`)
2530
+ }
2531
+ purgeOrphans(userRootPath, effectiveIds)
2532
+
2533
+ for (const presetId of effectiveIds) {
2349
2534
  const target = join(userRootPath, presetId)
2350
2535
  const state = classify(target)
2351
2536
  if (state === 'foreign') {
@@ -2387,4 +2572,4 @@ export async function apply(ctx, config = {}) {
2387
2572
  }
2388
2573
 
2389
2574
  // Test surface: pure helpers, no Cordis context required.
2390
- export const _internal = { PRESET_IDS, 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 }
2575
+ export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, effectivePresetIds, detectPeerCoverage, readSuppressPeerCordis, 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 }