dsh-runnpu 0.1.0-rc.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.
Potentially problematic release.
This version of dsh-runnpu might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/bin/cli-deps.mjs +384 -0
- package/bin/dsh-runnpu.mjs +255 -0
- package/bin/presets.mjs +167 -0
- package/compute-mode/node/index.mjs +527 -0
- package/compute-mode/node/tools.mjs +1440 -0
- package/compute-mode/preset/agent.cordis.yml +366 -0
- package/compute-mode/preset/preset.yml +3 -0
- package/compute-mode/preset/skills/runnpu/SKILL.md +233 -0
- package/compute-mode/preset/skills/runnpu/VENDOR.txt +17 -0
- package/compute-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
- package/compute-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
- package/compute-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
- package/compute-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
- package/compute-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
- package/compute-mode/preset/skills/runnpu/references/admin.md +60 -0
- package/compute-mode/preset/skills/runnpu/references/data-sources.md +35 -0
- package/compute-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
- package/compute-mode/preset/skills/runnpu/references/training-runs.md +56 -0
- package/compute-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
- package/compute-mode/preset/skills/runnpu/references/workload-types.md +47 -0
- package/cordis.patch.yml +80 -0
- package/lib/client.js +189 -0
- package/lib/client.js.map +7 -0
- package/package.json +106 -0
- package/shared/config.mjs +496 -0
- package/shared/connectors.mjs +322 -0
- package/shared/http.mjs +179 -0
- package/shared/insights.mjs +250 -0
- package/shared/json.mjs +28 -0
- package/shared/nonce.mjs +109 -0
- package/shared/operations.mjs +427 -0
- package/shared/pins.mjs +570 -0
- package/shared/reports.mjs +187 -0
- package/shared/research.mjs +91 -0
- package/shared/runnpu.mjs +527 -0
- package/shared/series.mjs +208 -0
- package/shared/sse-host.mjs +172 -0
- package/shared/swanlab.mjs +523 -0
- package/shared/ui-host.mjs +21 -0
- package/shared/upload.mjs +928 -0
- package/shared/views.mjs +520 -0
- package/training-mode/node/index.mjs +750 -0
- package/training-mode/node/tools.mjs +1268 -0
- package/training-mode/preset/agent.cordis.yml +341 -0
- package/training-mode/preset/preset.yml +3 -0
- package/training-mode/preset/skills/runnpu/SKILL.md +233 -0
- package/training-mode/preset/skills/runnpu/VENDOR.txt +17 -0
- package/training-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
- package/training-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
- package/training-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
- package/training-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
- package/training-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
- package/training-mode/preset/skills/runnpu/references/admin.md +60 -0
- package/training-mode/preset/skills/runnpu/references/data-sources.md +35 -0
- package/training-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
- package/training-mode/preset/skills/runnpu/references/training-runs.md +56 -0
- package/training-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
- package/training-mode/preset/skills/runnpu/references/workload-types.md +47 -0
- package/training-mode/preset/skills/swanlab-skill/LICENSE +21 -0
- package/training-mode/preset/skills/swanlab-skill/SKILL.md +163 -0
- package/training-mode/preset/skills/swanlab-skill/VENDOR.txt +9 -0
- package/training-mode/preset/skills/swanlab-skill/references/ANALYSIS_GUIDE.md +83 -0
- package/training-mode/preset/skills/swanlab-skill/references/CLI_REFERENCE.md +510 -0
- package/training-mode/preset/skills/swanlab-skill/references/SDK_QUICKSTART.md +341 -0
- package/training-mode/preset/skills/swanlab-skill/references/SWANLAB_CONCEPTS.md +523 -0
- package/training-mode/preset/skills/swanlab-skill/scripts/plot_metrics.py +353 -0
- package/training-mode/preset/skills/swanlab-skill/scripts/runs_benchmark.py +550 -0
- package/training-mode/preset/skills/training-research/SKILL.md +33 -0
- package/training-mode/preset/skills/training-research/references/pipeline.md +45 -0
- package/training-mode/preset/skills/training-research/references/research.md +76 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `dsh-runnpu` — the parts of installing this package that npm cannot do:
|
|
4
|
+
* the agent presets (real directories under $DSH_HOME/.agent-presets) and the
|
|
5
|
+
* two host CLIs the modes drive.
|
|
6
|
+
*
|
|
7
|
+
* dsh-runnpu install <compute|training|all> [--force] [--profile <name|dir>] [--with-cli]
|
|
8
|
+
* dsh-runnpu uninstall <compute|training|all> [--force]
|
|
9
|
+
* dsh-runnpu status
|
|
10
|
+
* dsh-runnpu doctor
|
|
11
|
+
* dsh-runnpu cli <runnpu|swanlab|all> [--from <archive|dir|url>] [--bin-dir <dir>] [--python <exe>] [--dry-run]
|
|
12
|
+
*
|
|
13
|
+
* `--profile web` additionally adds "dsh-runnpu" to that profile's
|
|
14
|
+
* `dsh.profile.bundles` (a name resolves under $DSH_HOME/profiles/, a path is
|
|
15
|
+
* used as is). The dependency itself is `pnpm add dsh-runnpu`'s job.
|
|
16
|
+
*
|
|
17
|
+
* `--with-cli` runs `cli` for what the mode needs after the presets:
|
|
18
|
+
* compute → runnpu; training → runnpu + swanlab. `doctor` only reports.
|
|
19
|
+
*
|
|
20
|
+
* DSH_HOME overrides ~/.dsh. RUNNPU_BIN_DIR overrides ~/.local/bin.
|
|
21
|
+
* GITHUB_TOKEN / GH_TOKEN is sent when downloading from a private GitHub mirror.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { delimiter, join } from 'node:path'
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
defaultBinDir,
|
|
28
|
+
doctor,
|
|
29
|
+
installRunnpu,
|
|
30
|
+
installSwanlab,
|
|
31
|
+
onPath,
|
|
32
|
+
platformKey,
|
|
33
|
+
readPins,
|
|
34
|
+
runnpuDownloadUrl,
|
|
35
|
+
runnpuReleaseDir,
|
|
36
|
+
} from './cli-deps.mjs'
|
|
37
|
+
import {
|
|
38
|
+
MODES,
|
|
39
|
+
PACKAGE_NAME,
|
|
40
|
+
dshHome,
|
|
41
|
+
inspectTarget,
|
|
42
|
+
installPreset,
|
|
43
|
+
mountInProfile,
|
|
44
|
+
presetRoot,
|
|
45
|
+
profileDir,
|
|
46
|
+
selectModes,
|
|
47
|
+
uninstallPreset,
|
|
48
|
+
} from './presets.mjs'
|
|
49
|
+
|
|
50
|
+
const USAGE = `usage:
|
|
51
|
+
${PACKAGE_NAME} install <compute|training|all> [--force] [--link] [--profile <name|dir>] [--with-cli]
|
|
52
|
+
${PACKAGE_NAME} uninstall <compute|training|all> [--force]
|
|
53
|
+
${PACKAGE_NAME} status
|
|
54
|
+
${PACKAGE_NAME} doctor
|
|
55
|
+
${PACKAGE_NAME} cli <runnpu|swanlab|all> [--from <archive|dir|url>] [--bin-dir <dir>] [--python <exe>] [--dry-run]
|
|
56
|
+
|
|
57
|
+
install copy the preset(s) into $DSH_HOME/.agent-presets/<id> (DSH_HOME defaults to ~/.dsh)
|
|
58
|
+
--force replace / remove a preset directory this package did not install
|
|
59
|
+
--profile also add "${PACKAGE_NAME}" to <profile>/package.json → dsh.profile.bundles
|
|
60
|
+
--with-cli then install the host CLIs the mode needs (compute: runnpu; training: runnpu + swanlab)
|
|
61
|
+
--link dev only: symlink instead of copy (a symlinked preset is NOT discovered by dsh)
|
|
62
|
+
doctor check runnpu / swanlab on PATH against the versions this package was built for
|
|
63
|
+
cli install runnpu (pinned build, from the release or --from) and/or swanlab (uv > pipx > pip)
|
|
64
|
+
--from runnpu source: a local runnpu_<ver>_<os>_<arch>.tar.gz/.zip, an extracted directory, or a URL
|
|
65
|
+
--bin-dir where runnpu goes (default $RUNNPU_BIN_DIR or ~/.local/bin)
|
|
66
|
+
--python interpreter for swanlab (default: uv tool, else pipx, else python3 -m pip --user)
|
|
67
|
+
`
|
|
68
|
+
|
|
69
|
+
/** @param {string[]} argv */
|
|
70
|
+
function parse(argv) {
|
|
71
|
+
const flags = {
|
|
72
|
+
force: false,
|
|
73
|
+
link: false,
|
|
74
|
+
withCli: false,
|
|
75
|
+
dryRun: false,
|
|
76
|
+
profile: /** @type {string | undefined} */ (undefined),
|
|
77
|
+
from: /** @type {string | undefined} */ (undefined),
|
|
78
|
+
binDir: /** @type {string | undefined} */ (undefined),
|
|
79
|
+
python: /** @type {string | undefined} */ (undefined),
|
|
80
|
+
}
|
|
81
|
+
const valued = { '--profile': 'profile', '--from': 'from', '--bin-dir': 'binDir', '--python': 'python' }
|
|
82
|
+
const positional = []
|
|
83
|
+
for (let i = 0; i < argv.length; i++) {
|
|
84
|
+
const arg = argv[i]
|
|
85
|
+
const eq = arg.indexOf('=')
|
|
86
|
+
const key = eq > 0 ? arg.slice(0, eq) : arg
|
|
87
|
+
if (key in valued) {
|
|
88
|
+
const value = eq > 0 ? arg.slice(eq + 1) : argv[++i]
|
|
89
|
+
if (!value) throw new Error(`${key} needs a value`)
|
|
90
|
+
flags[valued[key]] = value
|
|
91
|
+
} else if (arg === '--force') flags.force = true
|
|
92
|
+
else if (arg === '--link') flags.link = true
|
|
93
|
+
else if (arg === '--with-cli') flags.withCli = true
|
|
94
|
+
else if (arg === '--dry-run') flags.dryRun = true
|
|
95
|
+
else if (arg === '-h' || arg === '--help') positional.push('help')
|
|
96
|
+
else if (arg.startsWith('-')) throw new Error(`unknown flag ${arg}`)
|
|
97
|
+
else positional.push(arg)
|
|
98
|
+
}
|
|
99
|
+
return { command: positional[0], selector: positional[1], flags }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {string} home */
|
|
103
|
+
async function status(home) {
|
|
104
|
+
console.log(`preset root: ${presetRoot(home)}`)
|
|
105
|
+
for (const mode of MODES) {
|
|
106
|
+
const target = join(presetRoot(home), mode.id)
|
|
107
|
+
const state = await inspectTarget(target, mode.from)
|
|
108
|
+
const label = { absent: 'not installed', ours: 'installed', 'our-link': 'symlinked (dev only, not discovered)', foreign: 'present, not ours' }[state]
|
|
109
|
+
console.log(` ${mode.id.padEnd(9)} ${mode.display} ${label}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Print the doctor report; returns whether everything checked out.
|
|
115
|
+
* @param {NodeJS.ProcessEnv} [env] PATH to probe (default: this process's)
|
|
116
|
+
*/
|
|
117
|
+
async function runDoctor(env = process.env) {
|
|
118
|
+
const pins = await readPins()
|
|
119
|
+
const findings = await doctor(pins, env)
|
|
120
|
+
for (const f of findings) {
|
|
121
|
+
const mark = f.ok ? '✓' : '✗'
|
|
122
|
+
const where = f.path ? `${f.path}` : '(not found)'
|
|
123
|
+
console.log(`${mark} ${f.name.padEnd(8)} ${(f.version ?? '-').padEnd(16)} want ${f.want.padEnd(10)} ${where}`)
|
|
124
|
+
if (!f.ok) console.log(` ${f.detail}`)
|
|
125
|
+
}
|
|
126
|
+
const bad = findings.filter(f => !f.ok)
|
|
127
|
+
if (bad.length) {
|
|
128
|
+
console.log(`\nfix: ${PACKAGE_NAME} cli ${bad.length === 2 ? 'all' : bad[0].name}`)
|
|
129
|
+
}
|
|
130
|
+
return bad.length === 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {('runnpu' | 'swanlab')[]} which
|
|
135
|
+
* @param {ReturnType<typeof parse>['flags']} flags
|
|
136
|
+
*/
|
|
137
|
+
async function runCli(which, flags) {
|
|
138
|
+
const pins = await readPins()
|
|
139
|
+
// The verification pass after installing looks in the install dir first, so
|
|
140
|
+
// it reports the binary just installed even when that dir is not on PATH yet.
|
|
141
|
+
let checkEnv = process.env
|
|
142
|
+
if (which.includes('runnpu')) {
|
|
143
|
+
const plat = platformKey()
|
|
144
|
+
const binDir = flags.binDir ?? defaultBinDir()
|
|
145
|
+
checkEnv = { ...process.env, PATH: [binDir, process.env.PATH ?? ''].filter(Boolean).join(delimiter) }
|
|
146
|
+
const source = flags.from ?? runnpuDownloadUrl(pins.runnpu, plat)
|
|
147
|
+
if (flags.dryRun) {
|
|
148
|
+
console.log(`runnpu: would install build ${pins.runnpu.version} from ${source} → ${join(binDir, `runnpu${plat.exe}`)}`)
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`runnpu: installing build ${pins.runnpu.version} (${plat.os}/${plat.arch}) from ${source}`)
|
|
151
|
+
try {
|
|
152
|
+
const result = await installRunnpu({ pin: pins.runnpu, from: flags.from, binDir, plat })
|
|
153
|
+
console.log(`✓ runnpu ${result.version ?? ''} → ${result.path}`)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
156
|
+
throw new Error(
|
|
157
|
+
`${message}\n` +
|
|
158
|
+
` The runnpu build must match the vendored skill (${pins.runnpu.version}). If the release asset is not\n` +
|
|
159
|
+
` reachable (offline cluster → use the archive the RunNPU console ships, a mirror → its URL),\n` +
|
|
160
|
+
` pass it with --from <archive|dir|url>. Release: ${runnpuReleaseDir(pins.runnpu)}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
if (!onPath(binDir)) {
|
|
164
|
+
console.log(`\n⚠ ${binDir} is not on PATH — add it, e.g.\n fish: fish_add_path ${binDir}\n bash/zsh: echo 'export PATH="${binDir}:$PATH"' >> ~/.profile`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (which.includes('swanlab')) {
|
|
169
|
+
const result = await installSwanlab({ pin: pins.swanlab, python: flags.python, dryRun: flags.dryRun })
|
|
170
|
+
const cmd = result.plan.argv.join(' ')
|
|
171
|
+
if (flags.dryRun) console.log(`swanlab: would run (${result.plan.tool}): ${cmd}`)
|
|
172
|
+
else if (result.ok) console.log(`✓ swanlab installed via ${result.plan.tool}: ${cmd}`)
|
|
173
|
+
else throw new Error(`swanlab install failed (${cmd}):\n${result.output}`)
|
|
174
|
+
}
|
|
175
|
+
if (!flags.dryRun) {
|
|
176
|
+
console.log('')
|
|
177
|
+
await runDoctor(checkEnv)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const { command, selector, flags } = parse(process.argv.slice(2))
|
|
183
|
+
const home = dshHome()
|
|
184
|
+
|
|
185
|
+
if (!command || command === 'help') {
|
|
186
|
+
console.log(USAGE)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (command === 'status') {
|
|
190
|
+
await status(home)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (command === 'doctor') {
|
|
194
|
+
process.exitCode = (await runDoctor()) ? 0 : 1
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
if (command === 'cli') {
|
|
198
|
+
const which = selector === 'all' ? ['runnpu', 'swanlab'] : selector === 'runnpu' || selector === 'swanlab' ? [selector] : undefined
|
|
199
|
+
if (!which) throw new Error(`cli: expected runnpu | swanlab | all, got ${JSON.stringify(selector ?? '')}`)
|
|
200
|
+
await runCli(/** @type {('runnpu' | 'swanlab')[]} */ (which), flags)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
if (command !== 'install' && command !== 'uninstall') throw new Error(`unknown command ${command}\n${USAGE}`)
|
|
204
|
+
|
|
205
|
+
const modes = selectModes(selector)
|
|
206
|
+
|
|
207
|
+
if (command === 'uninstall') {
|
|
208
|
+
for (const mode of modes) {
|
|
209
|
+
const result = await uninstallPreset(mode, { home, force: flags.force })
|
|
210
|
+
console.log(`${result === 'removed' ? 'removed' : 'not installed'}: ${mode.id} (${mode.display})`)
|
|
211
|
+
}
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const mode of modes) {
|
|
216
|
+
const target = await installPreset(mode, { home, force: flags.force, link: flags.link })
|
|
217
|
+
console.log(`installed ${mode.id} (${mode.display}) → ${target}${flags.link ? ' (symlink, dev only)' : ''}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (flags.profile) {
|
|
221
|
+
const dir = profileDir(flags.profile, home)
|
|
222
|
+
const changed = await mountInProfile(dir)
|
|
223
|
+
console.log(`${changed ? 'added' : 'already in'} dsh.profile.bundles: ${join(dir, 'package.json')}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (flags.withCli) {
|
|
227
|
+
const needSwanlab = modes.some(mode => mode.id === 'training')
|
|
228
|
+
console.log('')
|
|
229
|
+
await runCli(needSwanlab ? ['runnpu', 'swanlab'] : ['runnpu'], flags)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (flags.profile) {
|
|
233
|
+
const dir = profileDir(flags.profile, home)
|
|
234
|
+
console.log(`
|
|
235
|
+
Next:
|
|
236
|
+
cd ${dir} && pnpm install # if "${PACKAGE_NAME}" is not yet in dependencies: pnpm add ${PACKAGE_NAME}
|
|
237
|
+
restart dsh, then start a NEW session and pick the mode in the picker.${flags.withCli ? '' : `\n host CLIs: ${PACKAGE_NAME} doctor (install with: ${PACKAGE_NAME} cli all)`}
|
|
238
|
+
`)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`
|
|
243
|
+
Next, mount the plugin bundle in the dsh profile you run (usually $DSH_HOME/profiles/web):
|
|
244
|
+
|
|
245
|
+
1. dependencies: pnpm add ${PACKAGE_NAME} (already done if you are running this via npx from that profile)
|
|
246
|
+
2. dsh.profile.bundles: add "${PACKAGE_NAME}" (or re-run with --profile <name|dir>)
|
|
247
|
+
3. host CLIs: ${PACKAGE_NAME} doctor (install with: ${PACKAGE_NAME} cli all)
|
|
248
|
+
4. restart dsh, then start a NEW session and pick the mode in the picker.
|
|
249
|
+
`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main().catch(error => {
|
|
253
|
+
console.error(`${PACKAGE_NAME}: ${error instanceof Error ? error.message : String(error)}`)
|
|
254
|
+
process.exit(1)
|
|
255
|
+
})
|
package/bin/presets.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset installation — the logic behind the `dsh-runnpu` bin and
|
|
3
|
+
* `scripts/install-presets.mjs`.
|
|
4
|
+
*
|
|
5
|
+
* A dsh agent preset lives at `<dshHome>/.agent-presets/<id>` and MUST be a
|
|
6
|
+
* real directory: a symlinked preset is silently not discovered by the roster
|
|
7
|
+
* (the picker just lists the shipped presets, no error anywhere). npm cannot
|
|
8
|
+
* put files there, which is the whole reason this installer exists.
|
|
9
|
+
*
|
|
10
|
+
* The preset yml cannot carry an absolute skill path from the author's
|
|
11
|
+
* machine, so it carries `__DSH_PRESET_ROOT__` and the real root is resolved
|
|
12
|
+
* here, on the machine the preset is installed onto.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-runnpu/bin/presets
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { cp, lstat, mkdir, readFile, readlink, rm, symlink, writeFile } from 'node:fs/promises'
|
|
18
|
+
import { existsSync } from 'node:fs'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
export const PACKAGE_NAME = 'dsh-runnpu'
|
|
24
|
+
|
|
25
|
+
/** Repo / installed-package root: this file sits in `<root>/bin/`. */
|
|
26
|
+
export const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
27
|
+
|
|
28
|
+
/** @typedef {{ id: 'compute' | 'training', display: string, from: string }} Mode */
|
|
29
|
+
|
|
30
|
+
/** @type {readonly Mode[]} */
|
|
31
|
+
export const MODES = Object.freeze([
|
|
32
|
+
{ id: 'compute', display: '算力管理模式', from: join(packageRoot, 'compute-mode', 'preset') },
|
|
33
|
+
{ id: 'training', display: '训练模式', from: join(packageRoot, 'training-mode', 'preset') },
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
/** Marks a copy as ours, so a re-install replaces it without --force. */
|
|
37
|
+
export const MARKER = `.installed-by-${PACKAGE_NAME}`
|
|
38
|
+
/** Markers written by earlier releases of this package, still recognised as ours. */
|
|
39
|
+
const LEGACY_MARKERS = ['.installed-by-dsh-runnpu-modes']
|
|
40
|
+
|
|
41
|
+
export const PLACEHOLDER = '__DSH_PRESET_ROOT__'
|
|
42
|
+
|
|
43
|
+
/** @param {NodeJS.ProcessEnv} [env] */
|
|
44
|
+
export function dshHome(env = process.env) {
|
|
45
|
+
return env.DSH_HOME || join(homedir(), '.dsh')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {string} home */
|
|
49
|
+
export function presetRoot(home) {
|
|
50
|
+
return join(home, '.agent-presets')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve `compute` / `training` / `all` to modes; throws on anything else.
|
|
55
|
+
* @param {string | undefined} selector
|
|
56
|
+
* @returns {Mode[]}
|
|
57
|
+
*/
|
|
58
|
+
export function selectModes(selector) {
|
|
59
|
+
if (selector === 'all') return [...MODES]
|
|
60
|
+
const mode = MODES.find(m => m.id === selector)
|
|
61
|
+
if (!mode) {
|
|
62
|
+
throw new Error(`unknown mode ${JSON.stringify(selector ?? '')}: expected ${MODES.map(m => m.id).join(' | ')} | all`)
|
|
63
|
+
}
|
|
64
|
+
return [mode]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What sits at the preset's target path, if anything.
|
|
69
|
+
* @param {string} target
|
|
70
|
+
* @param {string} from
|
|
71
|
+
* @returns {Promise<'absent' | 'ours' | 'our-link' | 'foreign'>}
|
|
72
|
+
*/
|
|
73
|
+
export async function inspectTarget(target, from) {
|
|
74
|
+
let existing
|
|
75
|
+
try {
|
|
76
|
+
existing = await lstat(target)
|
|
77
|
+
} catch {
|
|
78
|
+
return 'absent'
|
|
79
|
+
}
|
|
80
|
+
if (existing.isSymbolicLink()) return (await readlink(target)) === from ? 'our-link' : 'foreign'
|
|
81
|
+
if ([MARKER, ...LEGACY_MARKERS].some(marker => existsSync(join(target, marker)))) return 'ours'
|
|
82
|
+
return 'foreign'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Install one mode's preset. Returns the target path.
|
|
87
|
+
*
|
|
88
|
+
* `link` symlinks instead of copying — dev-only: the roster does NOT discover
|
|
89
|
+
* a symlinked preset, and the placeholder is served verbatim. Kept for
|
|
90
|
+
* experiments, never for a working install.
|
|
91
|
+
*
|
|
92
|
+
* @param {Mode} mode
|
|
93
|
+
* @param {{ home?: string, force?: boolean, link?: boolean }} [options]
|
|
94
|
+
*/
|
|
95
|
+
export async function installPreset(mode, { home = dshHome(), force = false, link = false } = {}) {
|
|
96
|
+
const root = presetRoot(home)
|
|
97
|
+
const target = join(root, mode.id)
|
|
98
|
+
await mkdir(root, { recursive: true })
|
|
99
|
+
|
|
100
|
+
const state = await inspectTarget(target, mode.from)
|
|
101
|
+
if (state === 'foreign' && !force) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`refusing to replace ${target}: it exists and was not installed by ${PACKAGE_NAME} (pass --force to replace)`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (state !== 'absent') await rm(target, { recursive: true, force: true })
|
|
107
|
+
|
|
108
|
+
if (link) {
|
|
109
|
+
await symlink(mode.from, target, 'dir')
|
|
110
|
+
return target
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await cp(mode.from, target, { recursive: true, filter: src => !src.endsWith('.DS_Store') })
|
|
114
|
+
const cordis = join(target, 'agent.cordis.yml')
|
|
115
|
+
await writeFile(cordis, (await readFile(cordis, 'utf8')).replaceAll(PLACEHOLDER, root), 'utf8')
|
|
116
|
+
await writeFile(join(target, MARKER), `installed from ${mode.from}\n`, 'utf8')
|
|
117
|
+
return target
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove one mode's preset. Only removes what we installed unless forced.
|
|
122
|
+
* @param {Mode} mode
|
|
123
|
+
* @param {{ home?: string, force?: boolean }} [options]
|
|
124
|
+
* @returns {Promise<'removed' | 'absent'>}
|
|
125
|
+
*/
|
|
126
|
+
export async function uninstallPreset(mode, { home = dshHome(), force = false } = {}) {
|
|
127
|
+
const target = join(presetRoot(home), mode.id)
|
|
128
|
+
const state = await inspectTarget(target, mode.from)
|
|
129
|
+
if (state === 'absent') return 'absent'
|
|
130
|
+
if (state === 'foreign' && !force) {
|
|
131
|
+
throw new Error(`refusing to remove ${target}: it was not installed by ${PACKAGE_NAME} (pass --force to remove)`)
|
|
132
|
+
}
|
|
133
|
+
await rm(target, { recursive: true, force: true })
|
|
134
|
+
return 'removed'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve `--profile web` (a name under `<dshHome>/profiles/`) or a path to
|
|
139
|
+
* the profile directory.
|
|
140
|
+
* @param {string} profile
|
|
141
|
+
* @param {string} home
|
|
142
|
+
*/
|
|
143
|
+
export function profileDir(profile, home) {
|
|
144
|
+
if (isAbsolute(profile) || profile.startsWith('.') || profile.includes('/')) return resolve(profile)
|
|
145
|
+
return join(home, 'profiles', profile)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Add this package to a profile's `dsh.profile.bundles`. Does NOT touch
|
|
150
|
+
* `dependencies` — that is `pnpm add`'s job — and does not run the package
|
|
151
|
+
* manager. Returns whether the file changed.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} dir profile directory (holds package.json)
|
|
154
|
+
*/
|
|
155
|
+
export async function mountInProfile(dir) {
|
|
156
|
+
const file = join(dir, 'package.json')
|
|
157
|
+
if (!existsSync(file)) throw new Error(`no package.json in ${dir}: is this a dsh profile directory?`)
|
|
158
|
+
const text = await readFile(file, 'utf8')
|
|
159
|
+
const pkg = JSON.parse(text)
|
|
160
|
+
const profile = { ...(pkg.dsh?.profile ?? {}) }
|
|
161
|
+
const bundles = Array.isArray(profile.bundles) ? [...profile.bundles] : []
|
|
162
|
+
if (bundles.includes(PACKAGE_NAME)) return false
|
|
163
|
+
bundles.push(PACKAGE_NAME)
|
|
164
|
+
pkg.dsh = { ...(pkg.dsh ?? {}), profile: { ...profile, bundles } }
|
|
165
|
+
await writeFile(file, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
166
|
+
return true
|
|
167
|
+
}
|