dsh-ops 0.0.0-stage → 0.2.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +417 -0
  5. package/README.en.md +121 -0
  6. package/README.md +113 -2
  7. package/README.zh.md +114 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/schema-baseline.json +64 -0
  12. package/docs/schema-current.json +84 -0
  13. package/docs/schema-measurement.md +17 -0
  14. package/dsh-plugin.json +88 -0
  15. package/icon.svg +12 -0
  16. package/lib/binary.js +409 -0
  17. package/lib/config.js +198 -0
  18. package/lib/handshake.js +252 -0
  19. package/lib/index.js +108 -0
  20. package/lib/jobs.js +42 -0
  21. package/lib/policy.js +64 -0
  22. package/lib/presentation.js +63 -0
  23. package/lib/profile-install.js +61 -0
  24. package/lib/rust.js +194 -0
  25. package/lib/session-shells.js +78 -0
  26. package/lib/shells.js +993 -0
  27. package/lib/tools.js +657 -0
  28. package/locale/en.json +6 -0
  29. package/locale/zh.json +6 -0
  30. package/package.json +114 -4
  31. package/vendor/fastctx/Cargo.lock +3210 -0
  32. package/vendor/fastctx/Cargo.toml +94 -0
  33. package/vendor/fastctx/FORK.md +119 -0
  34. package/vendor/fastctx/LICENSE-APACHE +201 -0
  35. package/vendor/fastctx/NOTICE +40 -0
  36. package/vendor/fastctx/README.md +439 -0
  37. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  39. package/vendor/fastctx/UPSTREAM.md +49 -0
  40. package/vendor/fastctx/build.rs +413 -0
  41. package/vendor/fastctx/src/background_status.rs +403 -0
  42. package/vendor/fastctx/src/binary.rs +75 -0
  43. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  44. package/vendor/fastctx/src/budget.rs +781 -0
  45. package/vendor/fastctx/src/cli/mod.rs +110 -0
  46. package/vendor/fastctx/src/context_guard.rs +289 -0
  47. package/vendor/fastctx/src/control/mod.rs +6 -0
  48. package/vendor/fastctx/src/control/paths.rs +49 -0
  49. package/vendor/fastctx/src/control/settings.rs +753 -0
  50. package/vendor/fastctx/src/control/transaction.rs +531 -0
  51. package/vendor/fastctx/src/edit/document.rs +535 -0
  52. package/vendor/fastctx/src/edit/locks.rs +371 -0
  53. package/vendor/fastctx/src/edit/mod.rs +213 -0
  54. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  55. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  56. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  57. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  58. package/vendor/fastctx/src/edit_server.rs +53 -0
  59. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  60. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  61. package/vendor/fastctx/src/encoding.rs +1118 -0
  62. package/vendor/fastctx/src/file_executor.rs +1151 -0
  63. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  64. package/vendor/fastctx/src/glob_filter.rs +98 -0
  65. package/vendor/fastctx/src/glob_tool.rs +653 -0
  66. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  67. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  68. package/vendor/fastctx/src/lib.rs +45 -0
  69. package/vendor/fastctx/src/main.rs +15 -0
  70. package/vendor/fastctx/src/model.rs +51 -0
  71. package/vendor/fastctx/src/model_guidance.rs +62 -0
  72. package/vendor/fastctx/src/operation.rs +356 -0
  73. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  74. package/vendor/fastctx/src/os_environment.rs +414 -0
  75. package/vendor/fastctx/src/path_codec.rs +850 -0
  76. package/vendor/fastctx/src/paths.rs +244 -0
  77. package/vendor/fastctx/src/process_identity.rs +763 -0
  78. package/vendor/fastctx/src/process_policy.rs +74 -0
  79. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  80. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  81. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  82. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  83. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  84. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  85. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  86. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  87. package/vendor/fastctx/src/render_plan.rs +468 -0
  88. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  89. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  90. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  91. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  92. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  93. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  94. package/vendor/fastctx/src/runtime/session.rs +536 -0
  95. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  96. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  97. package/vendor/fastctx/src/search_text.rs +227 -0
  98. package/vendor/fastctx/src/server.rs +359 -0
  99. package/vendor/fastctx/src/server_manifest.rs +468 -0
  100. package/vendor/fastctx/src/server_support.rs +826 -0
  101. package/vendor/fastctx/src/session.rs +629 -0
  102. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  103. package/vendor/fastctx/src/shell/bash.rs +263 -0
  104. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  105. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  106. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  107. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  108. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  109. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  110. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  111. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  112. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  113. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  114. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  115. package/vendor/fastctx/src/shell/mod.rs +345 -0
  116. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  117. package/vendor/fastctx/src/shell/output.rs +406 -0
  118. package/vendor/fastctx/src/shell/process.rs +493 -0
  119. package/vendor/fastctx/src/shell_server.rs +156 -0
  120. package/vendor/fastctx/src/skip_report.rs +83 -0
  121. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  122. package/vendor/fastctx/src/tool_schema.rs +204 -0
  123. package/vendor/fastctx/src/traversal.rs +846 -0
  124. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
package/lib/binary.js ADDED
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Locating the FastCtx executable this plugin hosts.
3
+ *
4
+ * FastCtx is a Rust runtime; the plugin never bundles it. Resolution is an
5
+ * ordered search from the most explicit, operator-owned answer to the least,
6
+ * and every step records why it did or did not match, so a failure reports the
7
+ * whole search instead of "not found".
8
+ *
9
+ * @module dsh-ops/binary
10
+ */
11
+
12
+ import { spawnSync } from 'node:child_process'
13
+ import fs from 'node:fs'
14
+ import os from 'node:os'
15
+ import path from 'node:path'
16
+ import { createRequire } from 'node:module'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ /** Repository root (`lib/`'s parent), used for the vendored source build path. */
20
+ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
21
+
22
+ /** Environment variable that overrides the search with one explicit path. */
23
+ export const BINARY_ENV = 'DSH_OPS_FASTCTX_BIN'
24
+
25
+ /**
26
+ * The upstream FastCtx platform packages and the executable each carries.
27
+ *
28
+ * This is the chain's last package step, not a default: this distribution's own
29
+ * build (`@dsh-ops/fastctx-<platform>-<arch>`) is preferred, and an upstream
30
+ * package the deployment does have is still honoured — installed by hand, by a
31
+ * profile, or by an install from before these names stopped riding along as
32
+ * `optionalDependencies`.
33
+ */
34
+ export const PLATFORM_TARGETS = Object.freeze({
35
+ 'win32-x64': { package: '@fastctx/win32-x64', executable: 'fastctx.exe' },
36
+ 'win32-arm64': { package: '@fastctx/win32-arm64', executable: 'fastctx.exe' },
37
+ 'linux-x64': { package: '@fastctx/linux-x64', executable: 'fastctx' },
38
+ 'darwin-x64': { package: '@fastctx/darwin-x64', executable: 'fastctx' },
39
+ 'darwin-arm64': { package: '@fastctx/darwin-arm64', executable: 'fastctx' },
40
+ })
41
+
42
+ /**
43
+ * The plugin's own FastCtx platform package for one platform/architecture pair.
44
+ *
45
+ * A frozen name constructor rather than a lookup table: the release build
46
+ * publishes `@dsh-ops/fastctx-<platform>-<arch>` — this fork's trimmed runtime —
47
+ * and imports this function to name what it produced, so the name has exactly
48
+ * one definition. The package carries the executable at
49
+ * `bin/<executableName(platform)>`, the same shape as `@fastctx/<platform>-<arch>`.
50
+ * @param {string} [platform] - `process.platform`.
51
+ * @param {string} [arch] - `process.arch`.
52
+ * @returns {string} the package name.
53
+ */
54
+ export const opsFastctxPackage = Object.freeze(
55
+ (platform = process.platform, arch = process.arch) => `@dsh-ops/fastctx-${platform}-${arch}`,
56
+ )
57
+
58
+ /** Error raised when no FastCtx executable can be resolved, carrying the search log. */
59
+ export class BinaryNotFoundError extends Error {
60
+ /**
61
+ * @param {string} message - the failure summary.
62
+ * @param {{file: string, source: string, detail: string}[]} tried - every candidate considered.
63
+ */
64
+ constructor(message, tried) {
65
+ super(message)
66
+ this.name = 'BinaryNotFoundError'
67
+ this.tried = tried
68
+ }
69
+
70
+ /**
71
+ * The search log as an indented, one-line-per-candidate report.
72
+ * @returns {string} the report.
73
+ */
74
+ report() {
75
+ return this.tried
76
+ .map((candidate) => ` - ${candidate.source}: ${candidate.file} (${candidate.detail})`)
77
+ .join('\n')
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The package target for one platform/architecture pair.
83
+ * @param {string} [platform] - `process.platform`.
84
+ * @param {string} [arch] - `process.arch`.
85
+ * @returns {{package: string, executable: string}|undefined} the target, or undefined when unsupported.
86
+ */
87
+ export function platformTarget(platform = process.platform, arch = process.arch) {
88
+ return PLATFORM_TARGETS[`${platform}-${arch}`]
89
+ }
90
+
91
+ /**
92
+ * The executable's file name on a platform.
93
+ * @param {string} [platform] - `process.platform`.
94
+ * @returns {string} `fastctx.exe` on Windows, `fastctx` elsewhere.
95
+ */
96
+ export function executableName(platform = process.platform) {
97
+ return platform === 'win32' ? 'fastctx.exe' : 'fastctx'
98
+ }
99
+
100
+ /**
101
+ * The DSH home directory this deployment uses.
102
+ * @param {NodeJS.ProcessEnv} [env] - the environment to read.
103
+ * @returns {string} the absolute DSH home.
104
+ */
105
+ export function dshHome(env = process.env) {
106
+ return env.DSH_HOME && env.DSH_HOME.trim() !== ''
107
+ ? path.resolve(env.DSH_HOME)
108
+ : path.join(os.homedir(), '.dsh')
109
+ }
110
+
111
+ /**
112
+ * The managed runtime directory: where `dsh-ops provision` installs the copy
113
+ * this plugin prefers. Keeping it under the DSH home means it survives npm
114
+ * cache cleanup and package upgrades.
115
+ * @param {object} [options] - resolution inputs.
116
+ * @param {NodeJS.ProcessEnv} [options.env] - the environment to read.
117
+ * @returns {string} the absolute directory.
118
+ */
119
+ export function managedRuntimeDir({ env = process.env } = {}) {
120
+ return path.join(dshHome(env), 'dsh-ops', 'bin')
121
+ }
122
+
123
+ /**
124
+ * The managed executable path.
125
+ * @param {object} [options] - resolution inputs.
126
+ * @param {NodeJS.ProcessEnv} [options.env] - the environment to read.
127
+ * @param {string} [options.platform] - `process.platform`.
128
+ * @returns {string} the absolute path.
129
+ */
130
+ export function managedBinaryFile({ env = process.env, platform = process.platform } = {}) {
131
+ return path.join(managedRuntimeDir({ env }), executableName(platform))
132
+ }
133
+
134
+ /**
135
+ * The in-repository release build produced by `cargo build --release` over the
136
+ * vendored FastCtx source. Present only in a source checkout whose owner ran
137
+ * the build.
138
+ * @param {object} [options] - resolution inputs.
139
+ * @param {string} [options.packageRoot] - the plugin package root.
140
+ * @param {string} [options.platform] - `process.platform`.
141
+ * @returns {string} the absolute path.
142
+ */
143
+ export function repoBuildFile({ packageRoot = PACKAGE_ROOT, platform = process.platform } = {}) {
144
+ return path.join(packageRoot, 'vendor', 'fastctx', 'target', 'release', executableName(platform))
145
+ }
146
+
147
+ /**
148
+ * Resolve the upstream package for this platform to its executable.
149
+ *
150
+ * Looked up under `root` — the plugin package root in production, a temporary
151
+ * tree in a test — exactly as {@link opsFastctxPackageBinary} is, so both steps
152
+ * of the chain answer "is this package installed here" from one place. A
153
+ * platform with no upstream target, a package that is not installed, and an
154
+ * installed package without its executable are each reported, never raised.
155
+ * @param {object} options - resolution inputs.
156
+ * @param {string} [options.platform] - `process.platform`.
157
+ * @param {string} [options.arch] - `process.arch`.
158
+ * @param {string} [options.root] - the package root to resolve from.
159
+ * @returns {{file: string}|{error: string}} the executable, or why it is unusable.
160
+ */
161
+ export function platformPackageBinary({
162
+ platform = process.platform,
163
+ arch = process.arch,
164
+ root = PACKAGE_ROOT,
165
+ } = {}) {
166
+ const target = platformTarget(platform, arch)
167
+ if (target === undefined) return { error: `no FastCtx platform package for ${platform}-${arch}` }
168
+ return packageBinaryAt({ name: target.package, executable: target.executable, root })
169
+ }
170
+
171
+ /**
172
+ * Whether a package root's manifest declares one dependency name.
173
+ *
174
+ * Read only to make a search miss say which kind of miss it is; a manifest that
175
+ * is absent, unreadable, or not JSON answers `false`, which is the plain "not
176
+ * installed".
177
+ * @param {string} root - the package root.
178
+ * @param {string} name - the package name.
179
+ * @returns {boolean} whether the manifest names it in `dependencies` or `optionalDependencies`.
180
+ */
181
+ function declaresPackage(root, name) {
182
+ try {
183
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
184
+ return name in { ...manifest.optionalDependencies, ...manifest.dependencies }
185
+ } catch {
186
+ return false
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Resolve one platform package installed below a package root to the executable
192
+ * in its `bin/`.
193
+ *
194
+ * Resolution starts at `root` rather than at this module, so a caller can point
195
+ * the search at another tree, and every way of missing says which one it was:
196
+ * absent, declared but not installed, or installed without its executable.
197
+ * Nothing here throws — this is one step of a search, not a decision.
198
+ * @param {object} options - resolution inputs.
199
+ * @param {string} options.name - the package name.
200
+ * @param {string} options.executable - the file name the package carries in `bin/`.
201
+ * @param {string} options.root - the package root to resolve from.
202
+ * @returns {{file: string}|{error: string}} the executable, or why it is unusable.
203
+ */
204
+ function packageBinaryAt({ name, executable, root }) {
205
+ let manifest
206
+ try {
207
+ const require = createRequire(path.join(root, 'package.json'))
208
+ manifest = require.resolve(`${name}/package.json`)
209
+ } catch {
210
+ return {
211
+ error: declaresPackage(root, name)
212
+ ? `${name} is declared but not installed`
213
+ : `${name} is not installed`,
214
+ }
215
+ }
216
+ const file = path.join(path.dirname(manifest), 'bin', executable)
217
+ return fs.existsSync(file) ? { file } : { error: `${name} is installed without bin/${executable}` }
218
+ }
219
+
220
+ /**
221
+ * Resolve this plugin's own FastCtx platform package.
222
+ *
223
+ * The rung sits directly below the vendored source build: the package is this
224
+ * fork's own trimmed runtime, so it is preferred over the upstream
225
+ * `@fastctx/<platform>-<arch>` prebuild. A platform the release never published
226
+ * for, a package that is not installed, and an installed package without its
227
+ * executable are each reported and searched past, never raised.
228
+ * @param {object} [options] - resolution inputs.
229
+ * @param {string} [options.platform] - `process.platform`.
230
+ * @param {string} [options.arch] - `process.arch`.
231
+ * @param {string} [options.packageRoot] - the plugin package root to resolve from.
232
+ * @returns {{file: string}|{error: string}} the executable, or why it is unusable.
233
+ */
234
+ export function opsFastctxPackageBinary({
235
+ platform = process.platform,
236
+ arch = process.arch,
237
+ packageRoot = PACKAGE_ROOT,
238
+ } = {}) {
239
+ return packageBinaryAt({
240
+ name: opsFastctxPackage(platform, arch),
241
+ executable: executableName(platform),
242
+ root: packageRoot,
243
+ })
244
+ }
245
+
246
+ /**
247
+ * Find an executable on `PATH`.
248
+ * @param {object} [options] - resolution inputs.
249
+ * @param {NodeJS.ProcessEnv} [options.env] - the environment to read.
250
+ * @param {string} [options.platform] - `process.platform`.
251
+ * @returns {{file: string}|{error: string}} the executable, or why none was found.
252
+ */
253
+ export function pathBinary({ env = process.env, platform = process.platform } = {}) {
254
+ const rawPath = env.PATH ?? env.Path ?? ''
255
+ if (rawPath.trim() === '') return { error: 'PATH is empty' }
256
+ const suffixes = platform === 'win32'
257
+ ? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map((entry) => entry.trim().toLowerCase()).filter(Boolean)
258
+ : ['']
259
+ const names = platform === 'win32'
260
+ ? ['fastctx', ...suffixes.map((suffix) => `fastctx${suffix}`)]
261
+ : ['fastctx']
262
+ for (const entry of rawPath.split(path.delimiter)) {
263
+ const directory = entry.trim().replace(/^"(.*)"$/, '$1')
264
+ if (directory === '' || !path.isAbsolute(directory)) continue
265
+ for (const name of names) {
266
+ const file = path.join(directory, name)
267
+ if (fs.existsSync(file)) return { file }
268
+ }
269
+ }
270
+ return { error: 'fastctx is not on PATH' }
271
+ }
272
+
273
+ /**
274
+ * Describe one candidate path without executing it.
275
+ * @param {string} file - the candidate.
276
+ * @returns {string} a short reason why it is or is not usable.
277
+ */
278
+ function describeFile(file) {
279
+ try {
280
+ const stat = fs.statSync(file)
281
+ return stat.isFile() ? 'exists' : 'not a regular file'
282
+ } catch (error) {
283
+ return /** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT' ? 'missing' : String(error?.message ?? error)
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Resolve the FastCtx executable to host.
289
+ *
290
+ * Order: explicit config, explicit environment, the managed copy installed by
291
+ * `dsh-ops provision`, the in-repository release build, this plugin's own
292
+ * FastCtx platform package (`@dsh-ops/fastctx-<platform>-<arch>`, the trimmed
293
+ * fork), the upstream `@fastctx/<platform>-<arch>` prebuild, then `PATH`. An
294
+ * explicitly configured path is authoritative: when it is unusable the search
295
+ * fails instead of quietly hosting a different binary.
296
+ *
297
+ * @param {object} [options] - resolution inputs.
298
+ * @param {string} [options.binaryPath] - the configured path, when one was given.
299
+ * @param {string} [options.packageRoot] - the plugin package root.
300
+ * @param {NodeJS.ProcessEnv} [options.env] - the environment to read.
301
+ * @param {string} [options.platform] - `process.platform`.
302
+ * @param {string} [options.arch] - `process.arch`.
303
+ * @returns {{file: string, source: string, tried: {file: string, source: string, detail: string}[]}} the resolved executable.
304
+ * @throws {BinaryNotFoundError} when nothing usable was found.
305
+ */
306
+ export function resolveBinary({
307
+ binaryPath,
308
+ packageRoot = PACKAGE_ROOT,
309
+ env = process.env,
310
+ platform = process.platform,
311
+ arch = process.arch,
312
+ } = {}) {
313
+ /** @type {{file: string, source: string, detail: string}[]} */
314
+ const tried = []
315
+
316
+ if (binaryPath !== undefined) {
317
+ const detail = describeFile(binaryPath)
318
+ tried.push({ file: binaryPath, source: 'config.binaryPath', detail })
319
+ if (detail !== 'exists') {
320
+ throw new BinaryNotFoundError(
321
+ `dsh-ops config.binaryPath does not name an existing file: ${binaryPath}`,
322
+ tried,
323
+ )
324
+ }
325
+ return { file: binaryPath, source: 'config.binaryPath', tried }
326
+ }
327
+
328
+ const fromEnv = env[BINARY_ENV]
329
+ if (fromEnv !== undefined && fromEnv.trim() !== '') {
330
+ const file = path.resolve(fromEnv)
331
+ const detail = describeFile(file)
332
+ tried.push({ file, source: `${BINARY_ENV}`, detail })
333
+ if (detail !== 'exists') {
334
+ throw new BinaryNotFoundError(`${BINARY_ENV} does not name an existing file: ${file}`, tried)
335
+ }
336
+ return { file, source: BINARY_ENV, tried }
337
+ }
338
+
339
+ const managed = managedBinaryFile({ env, platform })
340
+ const managedDetail = describeFile(managed)
341
+ tried.push({ file: managed, source: 'managed runtime', detail: managedDetail })
342
+ if (managedDetail === 'exists') return { file: managed, source: 'managed runtime', tried }
343
+
344
+ const built = repoBuildFile({ packageRoot, platform })
345
+ const builtDetail = describeFile(built)
346
+ tried.push({ file: built, source: 'vendored source build', detail: builtDetail })
347
+ if (builtDetail === 'exists') return { file: built, source: 'vendored source build', tried }
348
+
349
+ const ownPackage = opsFastctxPackageBinary({ platform, arch, packageRoot })
350
+ if ('file' in ownPackage) {
351
+ tried.push({ file: ownPackage.file, source: 'bundled fastctx package', detail: 'exists' })
352
+ return { file: ownPackage.file, source: 'bundled fastctx package', tried }
353
+ }
354
+ tried.push({
355
+ file: opsFastctxPackage(platform, arch),
356
+ source: 'bundled fastctx package',
357
+ detail: ownPackage.error,
358
+ })
359
+
360
+ const published = platformPackageBinary({ platform, arch, root: packageRoot })
361
+ if ('file' in published) {
362
+ tried.push({ file: published.file, source: 'published platform package', detail: 'exists' })
363
+ return { file: published.file, source: 'published platform package', tried }
364
+ }
365
+ tried.push({
366
+ file: platformTarget(platform, arch)?.package ?? `@fastctx/${platform}-${arch}`,
367
+ source: 'published platform package',
368
+ detail: published.error,
369
+ })
370
+
371
+ const onPath = pathBinary({ env, platform })
372
+ if ('file' in onPath) {
373
+ tried.push({ file: onPath.file, source: 'PATH', detail: 'exists' })
374
+ return { file: onPath.file, source: 'PATH', tried }
375
+ }
376
+ tried.push({ file: 'fastctx', source: 'PATH', detail: onPath.error })
377
+
378
+ throw new BinaryNotFoundError(
379
+ 'no FastCtx executable found; run `dsh-ops provision` from a source checkout, install the '
380
+ + 'matching @dsh-ops/fastctx or @fastctx platform package, or set config.binaryPath',
381
+ tried,
382
+ )
383
+ }
384
+
385
+ /**
386
+ * Run `--version` against a candidate to prove it is a working FastCtx build
387
+ * before the plugin spawns it as its own MCP stdio server (`lib/tools.js`).
388
+ * @param {string} file - the executable.
389
+ * @param {object} [options] - probe inputs.
390
+ * @param {number} [options.timeoutMs] - how long to wait.
391
+ * @param {NodeJS.ProcessEnv} [options.env] - the environment to run with.
392
+ * @returns {{ok: true, version: string}|{ok: false, detail: string}} the probe result.
393
+ */
394
+ export function probeBinary(file, { timeoutMs = 15_000, env = process.env } = {}) {
395
+ const result = spawnSync(file, ['--version'], {
396
+ encoding: 'utf8',
397
+ timeout: timeoutMs,
398
+ windowsHide: true,
399
+ env,
400
+ })
401
+ if (result.error !== undefined && result.error !== null) {
402
+ return { ok: false, detail: String(result.error.message ?? result.error) }
403
+ }
404
+ if (result.status !== 0) {
405
+ const stderr = (result.stderr ?? '').trim().split('\n')[0] ?? ''
406
+ return { ok: false, detail: `exited ${result.status}${stderr === '' ? '' : `: ${stderr}`}` }
407
+ }
408
+ return { ok: true, version: (result.stdout ?? '').trim().split('\n')[0] ?? '' }
409
+ }
package/lib/config.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Plugin configuration: one plain object read from the `dsh-ops` row in
3
+ * `cordis.yml`, validated here.
4
+ *
5
+ * The plugin deliberately exports no Schemastery `Config`: this host half must
6
+ * resolve with no `@deepseek-ai/*` value import of its own (the profile loader
7
+ * supplies those names, and every extra one is another way for the plugin to
8
+ * fail to load). Validation is therefore explicit and fails loud, naming the
9
+ * offending key, which is what the loader's own schema would have done.
10
+ *
11
+ * @module dsh-ops/config
12
+ */
13
+
14
+ /**
15
+ * The legacy FastCtx server identity. Accepted for existing deployment rows;
16
+ * server instructions are no longer published as a separate prompt section.
17
+ */
18
+ export const DEFAULT_SERVER_NAME = 'fastctx'
19
+
20
+ /**
21
+ * Per-call timeout for FastCtx tools, in milliseconds. FastCtx serves reads,
22
+ * searches, and replacements from a persistent process, so a call is normally
23
+ * fast; the ceiling is generous because `run` executes a real command and a
24
+ * build or test run legitimately takes minutes.
25
+ */
26
+ export const DEFAULT_TOOL_CALL_TIMEOUT_MS = 300_000
27
+
28
+ /** Upper bound accepted for `toolCallTimeoutMs`, matching the loader's timer ceiling. */
29
+ const MAX_TOOL_CALL_TIMEOUT_MS = 2_147_483_647
30
+
31
+ /** How the plugin treats the host's own shell tools. */
32
+ export const SHELL_POLICIES = ['advise', 'deny-host-shell']
33
+
34
+ /** Host tool names the `deny-host-shell` policy refuses by default. */
35
+ export const DEFAULT_DENIED_HOST_TOOLS = ['pwsh', 'bash', 'pwsh_persistent']
36
+
37
+ /** Error raised for a configuration value this plugin cannot honour. */
38
+ export class ConfigError extends Error {
39
+ /**
40
+ * @param {string} message - what is wrong and what is accepted.
41
+ */
42
+ constructor(message) {
43
+ super(`dsh-ops config: ${message}`)
44
+ this.name = 'ConfigError'
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Reject a key the plugin does not implement instead of silently ignoring it.
50
+ * @param {Record<string, unknown>} raw - the whole row config.
51
+ * @param {string[]} known - accepted keys.
52
+ * @returns {void}
53
+ */
54
+ function rejectUnknownKeys(raw, known) {
55
+ const unknown = Object.keys(raw).filter((key) => !known.includes(key))
56
+ if (unknown.length > 0) {
57
+ throw new ConfigError(
58
+ `unknown key(s) ${unknown.map((key) => JSON.stringify(key)).join(', ')}; `
59
+ + `accepted keys are ${known.map((key) => JSON.stringify(key)).join(', ')}`,
60
+ )
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Read an optional string.
66
+ * @param {Record<string, unknown>} raw - the whole row config.
67
+ * @param {string} key - the key to read.
68
+ * @returns {string|undefined} the value, or undefined when absent.
69
+ */
70
+ function optionalString(raw, key) {
71
+ const value = raw[key]
72
+ if (value === undefined || value === null) return undefined
73
+ if (typeof value !== 'string' || value.trim() === '') {
74
+ throw new ConfigError(`${key} must be a non-empty string, got ${JSON.stringify(value)}`)
75
+ }
76
+ return value
77
+ }
78
+
79
+ /**
80
+ * Read an optional boolean.
81
+ * @param {Record<string, unknown>} raw - the whole row config.
82
+ * @param {string} key - the key to read.
83
+ * @param {boolean} fallback - the value when the key is absent.
84
+ * @returns {boolean} the value.
85
+ */
86
+ function optionalBoolean(raw, key, fallback) {
87
+ const value = raw[key]
88
+ if (value === undefined || value === null) return fallback
89
+ if (typeof value !== 'boolean') {
90
+ throw new ConfigError(`${key} must be a boolean, got ${JSON.stringify(value)}`)
91
+ }
92
+ return value
93
+ }
94
+
95
+ /**
96
+ * Read an optional list of non-empty strings.
97
+ * @param {Record<string, unknown>} raw - the whole row config.
98
+ * @param {string} key - the key to read.
99
+ * @param {string[]} fallback - the value when the key is absent.
100
+ * @returns {string[]} the value.
101
+ */
102
+ function optionalStringList(raw, key, fallback) {
103
+ const value = raw[key]
104
+ if (value === undefined || value === null) return fallback
105
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.trim() === '')) {
106
+ throw new ConfigError(`${key} must be an array of non-empty strings, got ${JSON.stringify(value)}`)
107
+ }
108
+ return [...value]
109
+ }
110
+
111
+ /**
112
+ * The resolved configuration, with every default applied.
113
+ * @typedef {object} ResolvedConfig
114
+ * @property {string|undefined} binaryPath - explicit FastCtx executable.
115
+ * @property {string} serverName - tool namespace owner.
116
+ * @property {boolean} enableShellTools - opt into run/job tools, additionally gated by session authority.
117
+ * @property {number} toolCallTimeoutMs - per-call deadline for one FastCtx tool call.
118
+ * @property {boolean} required - fail activation when the server cannot start.
119
+ * @property {'advise'|'deny-host-shell'} shellPolicy - treatment of host shell tools.
120
+ * @property {string[]} deniedHostTools - tool names refused by `deny-host-shell`.
121
+ * @property {boolean} promptPolicy - inject the repository-tooling prompt section.
122
+ * @property {string} extraGuidance - extra text appended to that section.
123
+ * @property {string|undefined} bashPath - explicit executable for the plugin's bash layer.
124
+ * @property {boolean} publishBashTool - publish ops_bash in full-access sessions with a usable bash and subprocess service.
125
+ * @property {boolean} allowSystemShellFallback - let the bash rung use a
126
+ * system-installed bash when the plugin carries no copy of its own.
127
+ */
128
+
129
+ /**
130
+ * Validate one plugin row's config.
131
+ * @param {unknown} rawConfig - the `config` value from the loader row.
132
+ * @returns {ResolvedConfig} the resolved configuration.
133
+ */
134
+ export function resolveConfig(rawConfig) {
135
+ if (rawConfig === undefined || rawConfig === null) rawConfig = {}
136
+ if (typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
137
+ throw new ConfigError(`config must be an object, got ${JSON.stringify(rawConfig)}`)
138
+ }
139
+ const raw = /** @type {Record<string, unknown>} */ (rawConfig)
140
+ rejectUnknownKeys(raw, [
141
+ 'binaryPath',
142
+ 'serverName',
143
+ 'enableShellTools',
144
+ 'toolCallTimeoutMs',
145
+ 'required',
146
+ 'shellPolicy',
147
+ 'deniedHostTools',
148
+ 'promptPolicy',
149
+ 'extraGuidance',
150
+ 'bashPath',
151
+ 'publishBashTool',
152
+ 'allowSystemShellFallback',
153
+ ])
154
+
155
+ const serverName = optionalString(raw, 'serverName') ?? DEFAULT_SERVER_NAME
156
+ if (!/^[A-Za-z0-9_-]{1,32}$/.test(serverName)) {
157
+ throw new ConfigError(`serverName must match [A-Za-z0-9_-]{1,32}, got ${JSON.stringify(serverName)}`)
158
+ }
159
+
160
+ const timeout = raw.toolCallTimeoutMs
161
+ if (timeout !== undefined && timeout !== null
162
+ && (!Number.isFinite(timeout) || Number(timeout) <= 0 || Number(timeout) > MAX_TOOL_CALL_TIMEOUT_MS)) {
163
+ throw new ConfigError(
164
+ `toolCallTimeoutMs must be a positive finite number of milliseconds, got ${JSON.stringify(timeout)}`,
165
+ )
166
+ }
167
+
168
+ const shellPolicy = optionalString(raw, 'shellPolicy') ?? 'advise'
169
+ if (!SHELL_POLICIES.includes(shellPolicy)) {
170
+ throw new ConfigError(
171
+ `shellPolicy must be one of ${SHELL_POLICIES.map((value) => JSON.stringify(value)).join(', ')}, `
172
+ + `got ${JSON.stringify(shellPolicy)}`,
173
+ )
174
+ }
175
+
176
+ return {
177
+ binaryPath: optionalString(raw, 'binaryPath'),
178
+ serverName,
179
+ enableShellTools: optionalBoolean(raw, 'enableShellTools', true),
180
+ toolCallTimeoutMs: timeout === undefined || timeout === null
181
+ ? DEFAULT_TOOL_CALL_TIMEOUT_MS
182
+ : Number(timeout),
183
+ required: optionalBoolean(raw, 'required', false),
184
+ shellPolicy: /** @type {'advise'|'deny-host-shell'} */ (shellPolicy),
185
+ deniedHostTools: optionalStringList(raw, 'deniedHostTools', DEFAULT_DENIED_HOST_TOOLS),
186
+ promptPolicy: optionalBoolean(raw, 'promptPolicy', true),
187
+ extraGuidance: optionalString(raw, 'extraGuidance') ?? '',
188
+ bashPath: optionalString(raw, 'bashPath'),
189
+ publishBashTool: optionalBoolean(raw, 'publishBashTool', true),
190
+ // There is deliberately no `pwshPath` and no switch for the host's pwsh
191
+ // row. A bundle patch is evaluated before any dsh-ops row is mounted, so a
192
+ // config key here could never reach the `pwsh-sandbox` override in
193
+ // `cordis.patch.yml`; a key that pretended to would make the prompt ladder
194
+ // claim an L3 the host row never runs. L3 follows the executable instead:
195
+ // the plugin carries a pwsh, or the host's own pwsh tool is the last rung.
196
+ allowSystemShellFallback: optionalBoolean(raw, 'allowSystemShellFallback', true),
197
+ }
198
+ }