dsh-ops 0.0.0-stage → 0.2.2
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 +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +998 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- 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
|
+
}
|