dsh-gitbash-shell 0.24.3 → 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/CHANGELOG.md +483 -0
- package/README.md +46 -3
- package/README_EN.md +62 -0
- package/dsh.plugin.json +1 -1
- package/package.json +2 -1
- package/src/client.js +123 -3
- package/src/index.js +221 -24
- package/src/shell.js +149 -17
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
|
|
@@ -1692,10 +1780,22 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
|
|
|
1692
1780
|
|
|
1693
1781
|
const readShell = () => {
|
|
1694
1782
|
const settings = ctx.get('settings')
|
|
1695
|
-
if (!settings || typeof settings.
|
|
1783
|
+
if (!settings || typeof settings.update !== 'function') return null
|
|
1696
1784
|
try {
|
|
1697
|
-
|
|
1698
|
-
|
|
1785
|
+
// <=0.1.6: the sidebar registers a settings namespace and `get(ns)` reads
|
|
1786
|
+
// it. >=0.1.7: that API is gone (the service exposes describe/update
|
|
1787
|
+
// only), so the value comes from the row Config's projected form —
|
|
1788
|
+
// without this branch the adoption was silently skipped on the new host.
|
|
1789
|
+
if (typeof settings.get === 'function') {
|
|
1790
|
+
const value = settings.get(SIDEBAR_NS)
|
|
1791
|
+
return value && typeof value === 'object' && typeof value.terminalShell === 'string' ? value.terminalShell : ''
|
|
1792
|
+
}
|
|
1793
|
+
if (typeof settings.describe === 'function') {
|
|
1794
|
+
const entry = settings.describe().find((row) => row !== null && typeof row === 'object' && row.ns === SIDEBAR_NS)
|
|
1795
|
+
const value = entry === undefined ? undefined : entry.value
|
|
1796
|
+
return value && typeof value === 'object' && typeof value.terminalShell === 'string' ? value.terminalShell : ''
|
|
1797
|
+
}
|
|
1798
|
+
return null
|
|
1699
1799
|
} catch {
|
|
1700
1800
|
return null // namespace not registered yet / sidebar absent
|
|
1701
1801
|
}
|
|
@@ -1908,26 +2008,107 @@ async function registerVariant(ctx, presetId, { gitBashActive, skillsDir }) {
|
|
|
1908
2008
|
}
|
|
1909
2009
|
|
|
1910
2010
|
/**
|
|
1911
|
-
* Declarative-era wiring:
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
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.
|
|
1915
2025
|
*/
|
|
1916
|
-
async function runDeclarativeEra(ctx, presetIds) {
|
|
2026
|
+
async function runDeclarativeEra(ctx, presetIds, readSuppress) {
|
|
1917
2027
|
cleanupLegacyTrees(presetIds)
|
|
1918
2028
|
const gitBashActive = process.platform === 'win32'
|
|
1919
2029
|
const skillsDir = await resolveSkillsDir()
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
|
2066
|
+
}
|
|
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))
|
|
1928
2094
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
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')
|
|
1931
2112
|
}
|
|
1932
2113
|
|
|
1933
2114
|
export async function apply(ctx, config = {}) {
|
|
@@ -2007,6 +2188,10 @@ export async function apply(ctx, config = {}) {
|
|
|
2007
2188
|
gitAutocrlf: Schema.boolean().default(true),
|
|
2008
2189
|
bashPath: Schema.string().default(''),
|
|
2009
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),
|
|
2010
2195
|
}))
|
|
2011
2196
|
sidebarScope = scope
|
|
2012
2197
|
console.log(`${TAG} settings namespace registered: ${SETTINGS_NAMESPACE} (posixPaths default on, adoptSidebar default on)`)
|
|
@@ -2292,7 +2477,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2292
2477
|
// more), so this branch serves the variants and returns.
|
|
2293
2478
|
if (ctx.agentPresets && typeof ctx.agentPresets.register === 'function') {
|
|
2294
2479
|
try {
|
|
2295
|
-
await runDeclarativeEra(ctx, presetIds)
|
|
2480
|
+
await runDeclarativeEra(ctx, presetIds, () => liveSettings.suppressPeerCordis())
|
|
2296
2481
|
} catch (error) {
|
|
2297
2482
|
console.log(TAG + ' declarative registration failed: ' + (error && error.message ? error.message : error))
|
|
2298
2483
|
}
|
|
@@ -2331,9 +2516,21 @@ export async function apply(ctx, config = {}) {
|
|
|
2331
2516
|
// 0.1.6-alpha.1, so the materializer copies whatever the host itself ships.
|
|
2332
2517
|
const rows = await detectRowForms(ctx.agentPresets)
|
|
2333
2518
|
const userRootPath = userRoot.path
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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) {
|
|
2337
2534
|
const target = join(userRootPath, presetId)
|
|
2338
2535
|
const state = classify(target)
|
|
2339
2536
|
if (state === 'foreign') {
|
|
@@ -2375,4 +2572,4 @@ export async function apply(ctx, config = {}) {
|
|
|
2375
2572
|
}
|
|
2376
2573
|
|
|
2377
2574
|
// Test surface: pure helpers, no Cordis context required.
|
|
2378
|
-
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, 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 }
|
package/src/shell.js
CHANGED
|
@@ -46,16 +46,61 @@ export const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
|
|
|
46
46
|
/** Log prefix, matching src/index.js. */
|
|
47
47
|
const TAG = '[gitbash-shell]'
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The plugin row's id, which doubles as the settings namespace on <= 0.1.6 and
|
|
51
|
+
* as the row-config namespace on 0.1.7+ (same string by design).
|
|
52
|
+
*/
|
|
53
|
+
const SETTINGS_NAMESPACE = 'gitbash-shell'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mark one schema field live-editable when the HOST's schemastery supports it.
|
|
57
|
+
*
|
|
58
|
+
* dsh 0.1.7-alpha.1 (`feat(settings): project volatile Config through
|
|
59
|
+
* profile-backed forms`, #4587) made `LocalBashExecutor.Config` declare
|
|
60
|
+
* `cwd`/`timeoutMs`/`maxTimeoutMs`/`maxOutputBytes`/`maxSpillBytes`/`graceMs`
|
|
61
|
+
* with `.volatile()` and read every one of them through `.get()`
|
|
62
|
+
* (`packages/shell/bash-local/src/index.ts:100-107` and its
|
|
63
|
+
* `assertServiceableBashConfig`/`resolve`). A subclass that REDECLARES those
|
|
64
|
+
* fields as plain values therefore throws
|
|
65
|
+
* `TypeError: config.timeoutMs.get is not a function` on the first shell call
|
|
66
|
+
* and takes the whole `ctx.shell` down with it (issue #6, 0.24.2).
|
|
67
|
+
* `SandboxBashExecutor` — our parent — declares no Config of its own: it
|
|
68
|
+
* inherits `LocalBashExecutor`'s verbatim, so this redeclaration IS the
|
|
69
|
+
* executor's contract.
|
|
70
|
+
*
|
|
71
|
+
* The probe keeps one build serving both eras: hosts whose schemastery has
|
|
72
|
+
* `volatile()` (0.1.7+) get the Volatile refs their base class calls `.get()`
|
|
73
|
+
* on, while older hosts — whose base class reads plain values — keep plain
|
|
74
|
+
* ones. Published schemastery 3.18.2 has no `volatile()`, which is exactly why
|
|
75
|
+
* the probe (not an unconditional call) is required.
|
|
76
|
+
*/
|
|
77
|
+
function live(schema) {
|
|
78
|
+
return typeof schema.volatile === 'function' ? schema.volatile() : schema
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the executor's Config against a given schemastery module. Exported so
|
|
83
|
+
* the smoke test can drive BOTH eras with a recording stand-in instead of
|
|
84
|
+
* depending on which schemastery happens to be installed here.
|
|
85
|
+
* @param z schemastery module (real one, or the test's recorder).
|
|
86
|
+
* @returns the object schema for the `gitbash-executor` row.
|
|
87
|
+
*/
|
|
88
|
+
export function gitBashShellConfig(z) {
|
|
89
|
+
return z.object({
|
|
90
|
+
cwd: live(z.string()),
|
|
91
|
+
timeoutMs: live(z.number().default(120000)),
|
|
92
|
+
maxTimeoutMs: live(z.number().default(600000)),
|
|
93
|
+
maxOutputBytes: live(z.number().default(64000)),
|
|
94
|
+
maxSpillBytes: live(z.number().default(64 * 1024 * 1024)),
|
|
95
|
+
graceMs: live(z.number().default(3000)),
|
|
96
|
+
// Our own knob: the base class neither declares nor `.get()`s it, and
|
|
97
|
+
// `get bashPath()` below reads it as a plain value, so it stays plain.
|
|
98
|
+
bashPath: z.string().default(DEFAULT_GIT_BASH),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
49
102
|
/** Resolved configuration: the local executor's knobs, plus the Git Bash path. */
|
|
50
|
-
export const Config = z
|
|
51
|
-
cwd: z.string(),
|
|
52
|
-
timeoutMs: z.number().default(120000),
|
|
53
|
-
maxTimeoutMs: z.number().default(600000),
|
|
54
|
-
maxOutputBytes: z.number().default(64000),
|
|
55
|
-
maxSpillBytes: z.number().default(64 * 1024 * 1024),
|
|
56
|
-
graceMs: z.number().default(3000),
|
|
57
|
-
bashPath: z.string().default(DEFAULT_GIT_BASH),
|
|
58
|
-
})
|
|
103
|
+
export const Config = gitBashShellConfig(z)
|
|
59
104
|
|
|
60
105
|
/** Git Bash executor — mirrors the shipped bash/pwsh sandbox executors. */
|
|
61
106
|
export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
@@ -119,15 +164,72 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
119
164
|
}
|
|
120
165
|
|
|
121
166
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* name
|
|
167
|
+
* Decide whether one spec takes the Git Bash argv path, and how to label it
|
|
168
|
+
* (pure). Full access always does — the shipped executor hardcodes the bare
|
|
169
|
+
* `bash` name there. A Windows confined call does too (issue #1: MSYS2 cannot
|
|
170
|
+
* start under the restricted-token runner). Everything else inherits the
|
|
171
|
+
* parent's sandbox path, which already runs Git Bash because `confine` below
|
|
172
|
+
* substitutes the argv.
|
|
173
|
+
* @param spec the shell request.
|
|
174
|
+
* @returns `{ mode, unconfined }`, or undefined to inherit the parent path.
|
|
125
175
|
*/
|
|
176
|
+
gitBashRoute(spec) {
|
|
177
|
+
const policy = spec.sandboxPolicy
|
|
178
|
+
const mode = policy === undefined ? undefined : policy.mode
|
|
179
|
+
if (mode === undefined) return undefined
|
|
180
|
+
if (mode === 'danger-full-access') return { mode, unconfined: false }
|
|
181
|
+
if (process.platform === 'win32') return { mode, unconfined: true }
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
126
185
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
186
|
+
* Label one execution handle's foreground result without changing its
|
|
187
|
+
* identity (the parent's `decorateResult` is private to the shipped class, so
|
|
188
|
+
* the same memoized in-place wrapper lives here).
|
|
189
|
+
* @param ex the handle returned by `executeArgv`.
|
|
190
|
+
* @param sandbox sandbox facts to stamp onto the result.
|
|
191
|
+
* @returns the same handle, with `result()` mapped.
|
|
130
192
|
*/
|
|
193
|
+
static decorateExecution(ex, sandbox) {
|
|
194
|
+
const base = ex.result.bind(ex)
|
|
195
|
+
let decorated
|
|
196
|
+
ex.result = () => {
|
|
197
|
+
decorated ??= base().then((result) => ({ ...result, sandbox }))
|
|
198
|
+
return decorated
|
|
199
|
+
}
|
|
200
|
+
return ex
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* dsh 0.1.7 execution entry point (the shipped `ShellExecutor` now declares
|
|
205
|
+
* `execute(spec): Promise<ShellExecution>`; `run`/`start` were removed and
|
|
206
|
+
* background work is an `onExpiry: 'none'` execution the caller stops
|
|
207
|
+
* waiting on). Overriding it is what keeps the two Git Bash substitutions
|
|
208
|
+
* alive on the new host: without it the full-access branch spawns the bare
|
|
209
|
+
* `bash` name hardcoded in `LocalBashExecutor.execute`, and a Windows
|
|
210
|
+
* confined call goes back through the restricted-token runner MSYS2 cannot
|
|
211
|
+
* survive.
|
|
212
|
+
* On hosts without `execute` (<= 0.1.6) this method is never called by the
|
|
213
|
+
* runtime; it delegates to `run` so the class still behaves if it is.
|
|
214
|
+
* @param spec resolved execution settings and caller-owned command metadata.
|
|
215
|
+
* @returns the live execution handle.
|
|
216
|
+
*/
|
|
217
|
+
execute(spec) {
|
|
218
|
+
if (typeof super.execute !== 'function') return this.run(spec)
|
|
219
|
+
const routed = this.withParityEnv(spec)
|
|
220
|
+
const route = this.gitBashRoute(routed)
|
|
221
|
+
if (route === undefined) return super.execute(routed)
|
|
222
|
+
const argv = [this.bashPath, '-c', routed.command]
|
|
223
|
+
if (route.unconfined) this.warnConfinedUnconfined(route.mode)
|
|
224
|
+
const sandbox = route.unconfined
|
|
225
|
+
? { mode: route.mode, denied: false, enforcement: 'unconfined' }
|
|
226
|
+
: { mode: route.mode, denied: false }
|
|
227
|
+
// `executeArgv` is the shipped helper for exactly this: run an explicit
|
|
228
|
+
// argv with the executor's own lifecycle, output and deadline semantics.
|
|
229
|
+
return Promise.resolve(this.executeArgv(routed, argv))
|
|
230
|
+
.then((ex) => GitBashSandboxExecutor.decorateExecution(ex, sandbox))
|
|
231
|
+
}
|
|
232
|
+
|
|
131
233
|
/**
|
|
132
234
|
* Linux-parity environment for the MODEL's shell (v0.21.1). The official
|
|
133
235
|
* shell-env registry accepts DSH_* facts only — registering anything else
|
|
@@ -146,10 +248,33 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
146
248
|
* @param {object} spec the shell request
|
|
147
249
|
* @returns {object} the request with the parity facts merged in
|
|
148
250
|
*/
|
|
149
|
-
|
|
251
|
+
/**
|
|
252
|
+
* The plugin row's live dialect settings, across eras. dsh 0.1.7 replaced the
|
|
253
|
+
* settings namespace API: the service still exists but exposes only
|
|
254
|
+
* `describe()` / `update()` — `get(ns)` is gone — so the old read silently
|
|
255
|
+
* returned undefined on the new host and the Linux-parity env below never
|
|
256
|
+
* applied. The new era reads the row Config's projected values from the form
|
|
257
|
+
* descriptors (the same read dsh-agent-lang uses for the `locale` namespace).
|
|
258
|
+
* @returns the row's settings object, or undefined when unavailable.
|
|
259
|
+
*/
|
|
260
|
+
dialectSettings() {
|
|
150
261
|
try {
|
|
151
262
|
const settings = this.ctx && typeof this.ctx.get === 'function' ? this.ctx.get('settings') : undefined
|
|
152
|
-
|
|
263
|
+
if (settings === undefined || settings === null) return undefined
|
|
264
|
+
if (typeof settings.get === 'function') return settings.get(SETTINGS_NAMESPACE)
|
|
265
|
+
if (typeof settings.describe === 'function') {
|
|
266
|
+
const entry = settings.describe().find((row) => row !== null && typeof row === 'object' && row.ns === SETTINGS_NAMESPACE)
|
|
267
|
+
return entry === undefined ? undefined : entry.value
|
|
268
|
+
}
|
|
269
|
+
return undefined
|
|
270
|
+
} catch {
|
|
271
|
+
return undefined
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
withParityEnv(spec) {
|
|
276
|
+
try {
|
|
277
|
+
const value = this.dialectSettings()
|
|
153
278
|
if (!value || value.posixPaths !== true || value.gitAutocrlf === false) return spec
|
|
154
279
|
const dshEnv = {
|
|
155
280
|
...(spec.dshEnv === undefined ? {} : spec.dshEnv),
|
|
@@ -165,6 +290,13 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
165
290
|
}
|
|
166
291
|
}
|
|
167
292
|
|
|
293
|
+
/**
|
|
294
|
+
* 0.1.6-era full-access path with Git Bash argv: the parent's full-access
|
|
295
|
+
* branch delegates to LocalBashExecutor.run, which hardcodes the bare `bash`
|
|
296
|
+
* name, so override that branch here and keep everything else inherited.
|
|
297
|
+
* On 0.1.7+ the runtime calls `execute` above instead; this stays as the
|
|
298
|
+
* old-era entry point.
|
|
299
|
+
*/
|
|
168
300
|
async run(spec) {
|
|
169
301
|
spec = this.withParityEnv(spec)
|
|
170
302
|
const policy = spec.sandboxPolicy
|