@dsh-cc/cli 0.5.0 → 0.6.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.
- package/bin/dsh-cc.js +11 -1
- package/bootstrap.mjs +115 -0
- package/package.json +1 -1
package/bin/dsh-cc.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { dirname, join } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
|
-
import { bootstrapCommand, dshUnavailableMessage, existingWorktreeDecision, interceptResume, parseWorktreeFlag, planWorktree, PROFILE, sanitizeInheritedEnv, slugRetryDecision, spawnEnv, worktreeAddArgv, worktreeEnv } from '../bootstrap.mjs'
|
|
12
|
+
import { bootstrapCommand, dshUnavailableMessage, existingWorktreeDecision, interceptResume, parseWorktreeFlag, planWorktree, PROFILE, sanitizeInheritedEnv, slugRetryDecision, spawnEnv, versionGate, worktreeAddArgv, worktreeEnv } from '../bootstrap.mjs'
|
|
13
13
|
|
|
14
14
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
15
15
|
const ownVersion = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version
|
|
@@ -24,6 +24,16 @@ const profileDir = join(home, 'profiles', PROFILE)
|
|
|
24
24
|
const add = bootstrapCommand(existsSync(join(profileDir, 'package.json')), ownVersion)
|
|
25
25
|
if (add !== undefined) {
|
|
26
26
|
console.error(`dsh-cc: initializing profile "${PROFILE}"…`)
|
|
27
|
+
// Minimum-version gate runs ONLY here (bootstrap/install path), never on
|
|
28
|
+
// every launch — docs/plans/2026-09-05-startup-boot-first-frame.md W2
|
|
29
|
+
// removed the per-launch `dsh --version` probe for cold-start latency.
|
|
30
|
+
// Unparseable output fails open: the check must never brick the launcher.
|
|
31
|
+
const gate = versionGate(() => spawnSync('dsh', ['--version'], { encoding: 'utf8' }))
|
|
32
|
+
if (gate.warning) console.error(gate.warning)
|
|
33
|
+
if (!gate.ok) {
|
|
34
|
+
console.error(gate.message)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
27
37
|
const installed = spawnSync('dsh', add, { encoding: 'utf8', stdio: 'inherit' })
|
|
28
38
|
// A spawn error (e.g. dsh not on PATH) leaves status null — that is a
|
|
29
39
|
// missing-CLI problem, not an install failure.
|
package/bootstrap.mjs
CHANGED
|
@@ -315,3 +315,118 @@ export function existingWorktreeDecision({ named, pathExists }) {
|
|
|
315
315
|
export function slugRetryDecision({ named, attempt, maxAttempts = 5 }) {
|
|
316
316
|
return !named && attempt < maxAttempts ? 'retry' : 'fail'
|
|
317
317
|
}
|
|
318
|
+
|
|
319
|
+
// --- minimum harness version gate --------------------------------------------
|
|
320
|
+
// This gate runs ONLY on the bootstrap/install path (when bootstrapCommand
|
|
321
|
+
// returns non-undefined), never on every launch: docs/plans/
|
|
322
|
+
// 2026-09-05-startup-boot-first-frame.md W2 deliberately removed the
|
|
323
|
+
// launcher's per-launch `dsh --version` probe (~60 ms of extra Node cold
|
|
324
|
+
// start before the first frame). A version check on the install path is
|
|
325
|
+
// acceptable — it happens once, before the profile exists.
|
|
326
|
+
|
|
327
|
+
/** Lowest harness version the published bundles are known to work with. */
|
|
328
|
+
export const MIN_DSH_VERSION = '0.1.2-rc.1'
|
|
329
|
+
|
|
330
|
+
const VERSION_RE = /\d+\.\d+\.\d+(?:-[\w.+-]+)?/
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Extract the first `x.y.z[-pre][+build]` version string from `dsh --version`
|
|
334
|
+
* output. Lenient on purpose: returns null when nothing matches so callers
|
|
335
|
+
* can fail open.
|
|
336
|
+
* @param {string | undefined} output - Raw stdout (may be undefined).
|
|
337
|
+
* @returns {string | null}
|
|
338
|
+
*/
|
|
339
|
+
export function extractDshVersion(output) {
|
|
340
|
+
if (typeof output !== 'string') return null
|
|
341
|
+
const match = output.match(VERSION_RE)
|
|
342
|
+
return match === null ? null : match[0]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Prerelease-aware semver compare (hand-rolled — the launcher has zero
|
|
347
|
+
* dependencies). Returns <0, 0, >0 as a sorts before/equal/after b.
|
|
348
|
+
* Numeric identifiers compare numerically; absence of a prerelease outranks
|
|
349
|
+
* any prerelease; alphanumeric identifiers compare lexically (alpha < rc).
|
|
350
|
+
* @param {string} a
|
|
351
|
+
* @param {string} b
|
|
352
|
+
* @returns {number}
|
|
353
|
+
*/
|
|
354
|
+
export function compareSemver(a, b) {
|
|
355
|
+
const parse = (v) => {
|
|
356
|
+
const [core, pre = ''] = v.split('-')
|
|
357
|
+
const [numbers, build = ''] = pre.split('+')
|
|
358
|
+
return {
|
|
359
|
+
core: core.split('.').map(Number),
|
|
360
|
+
pre: numbers.length === 0 ? [] : numbers.split('.'),
|
|
361
|
+
build,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const pa = parse(a)
|
|
365
|
+
const pb = parse(b)
|
|
366
|
+
for (let i = 0; i < 3; i += 1) {
|
|
367
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
368
|
+
}
|
|
369
|
+
if (pa.pre.length !== pb.pre.length) {
|
|
370
|
+
return pa.pre.length === 0 ? 1 : pb.pre.length === 0 ? -1 : 0
|
|
371
|
+
}
|
|
372
|
+
for (let i = 0; i < pa.pre.length; i += 1) {
|
|
373
|
+
const x = pa.pre[i]
|
|
374
|
+
const y = pb.pre[i]
|
|
375
|
+
const nx = /^\d+$/.test(x)
|
|
376
|
+
const ny = /^\d+$/.test(y)
|
|
377
|
+
if (nx && ny) {
|
|
378
|
+
if (Number(x) !== Number(y)) return Number(x) - Number(y)
|
|
379
|
+
} else if (x !== y) {
|
|
380
|
+
return x < y ? -1 : 1
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return 0
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* True when the found version is strictly below {@link MIN_DSH_VERSION}.
|
|
388
|
+
* @param {string} version
|
|
389
|
+
* @returns {boolean}
|
|
390
|
+
*/
|
|
391
|
+
export function belowMinimumVersion(version) {
|
|
392
|
+
return compareSemver(version, MIN_DSH_VERSION) < 0
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Actionable below-minimum error: what was found, what is required, how to
|
|
397
|
+
* fix it.
|
|
398
|
+
* @param {string} found
|
|
399
|
+
* @returns {string}
|
|
400
|
+
*/
|
|
401
|
+
export function belowMinimumMessage(found) {
|
|
402
|
+
return `dsh-cc: dsh version ${found} is too old; this launcher requires >= ${MIN_DSH_VERSION}.\n`
|
|
403
|
+
+ 'Upgrade the harness first, e.g.: npm install -g @deepseek-ai/dsh@latest'
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Full gate decision for the bootstrap path: run `dsh --version` (via the
|
|
408
|
+
* injectable runner, spawnSync in production), parse, compare.
|
|
409
|
+
* Garbage/unparseable output fails OPEN with a one-line warning — a parse
|
|
410
|
+
* failure must never brick the launcher.
|
|
411
|
+
* @param {() => { stdout?: string | Buffer } | undefined} runVersion
|
|
412
|
+
* @returns {{ ok: boolean, message?: string, warning?: string }}
|
|
413
|
+
*/
|
|
414
|
+
export function versionGate(runVersion) {
|
|
415
|
+
let result
|
|
416
|
+
try {
|
|
417
|
+
result = runVersion()
|
|
418
|
+
} catch {
|
|
419
|
+
result = undefined
|
|
420
|
+
}
|
|
421
|
+
const found = extractDshVersion(result?.stdout?.toString())
|
|
422
|
+
if (found === null) {
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
warning: 'dsh-cc: could not parse `dsh --version` output; skipping the minimum-version check.',
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (belowMinimumVersion(found)) {
|
|
429
|
+
return { ok: false, message: belowMinimumMessage(found) }
|
|
430
|
+
}
|
|
431
|
+
return { ok: true }
|
|
432
|
+
}
|