@dsh-cc/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-cc/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Optional dsh-cc shortcut: bootstraps the tui profile and runs dsh --profile tui",
5
5
  "type": "module",
6
6
  "bin": {