@dsh-cc/cli 0.7.0 → 0.7.1-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.
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, devStoreRestoreDecision, dshUnavailableMessage, existingWorktreeDecision, formatVersionLabel, interceptResume, parseLocalConfig, parseWorktreeFlag, parseWorktreeRef, planWorktree, planWorktreeRef, prFetchRefs, remoteHost, PROFILE, readBuildInfo, repoRootFromCommonDir, runStoreRestore, sanitizeInheritedEnv, slugRetryDecision, spawnEnv, symlinkedPath, versionGate, worktreeAddArgv, worktreeEnv, worktreeIdentityRefusal } from '../bootstrap.mjs'
12
+ import { bootstrapCommand, BOOTSTRAP_STAMP, devStoreRestoreDecision, dshUnavailableMessage, existingWorktreeDecision, formatVersionLabel, healDecision, interceptResume, parseLocalConfig, parseWorktreeFlag, parseWorktreeRef, planWorktree, planWorktreeRef, prFetchRefs, readBootstrapVersion, remoteHost, PROFILE, readBuildInfo, repoRootFromCommonDir, runStoreHeal, runStoreRestore, sanitizeInheritedEnv, slugRetryDecision, spawnEnv, symlinkedPath, versionGate, worktreeAddArgv, worktreeEnv, worktreeIdentityRefusal, writeBootstrapStamp } from '../bootstrap.mjs'
13
13
  import { readWorktreeSettings, resolveBaseRef, sweepWorktrees, SWEEP_CAP_MS, worktreeReuseReset, worktreeSettingsPaths } from '../worktree-lifecycle.mjs'
14
14
 
15
15
  const here = dirname(fileURLToPath(import.meta.url))
@@ -27,7 +27,8 @@ if (process.argv.includes('--version') || process.argv.includes('-V')) {
27
27
  console.log(formatVersionLabel(ownVersion, buildInfo))
28
28
  process.exit(0)
29
29
  }
30
- const add = bootstrapCommand(existsSync(join(profileDir, 'package.json')), ownVersion)
30
+ const profileExisted = existsSync(join(profileDir, 'package.json'))
31
+ const add = bootstrapCommand(profileExisted, ownVersion)
31
32
  if (add !== undefined) {
32
33
  console.error(`dsh-cc: initializing profile "${PROFILE}"…`)
33
34
  // Minimum-version gate runs ONLY here (bootstrap/install path), never on
@@ -51,13 +52,33 @@ if (add !== undefined) {
51
52
  console.error(`dsh-cc: plugin install failed. Retry:\n dsh ${add.join(' ')}`)
52
53
  process.exit(installed.status ?? 1)
53
54
  }
55
+ // First install of this launcher version: record it so later launches can
56
+ // tell "converged" from "installed before the heal mechanism existed".
57
+ writeBootstrapStamp(join(profileDir, BOOTSTRAP_STAMP), ownVersion)
54
58
  }
55
59
 
56
60
  // A dev-synced profile paired with a DIFFERENT launcher version converges
57
61
  // back to store bundles before the session starts (plan 2026-09-13 §3.4).
58
62
  // A fresh profile has no stamp, so this can never fire on the bootstrap path.
63
+ // A successful restore IS this version's install: stamp afterwards.
59
64
  const restorePlan = devStoreRestoreDecision(buildInfo, ownVersion)
60
- if (restorePlan !== null) runStoreRestore(profileDir, ownVersion)
65
+ if (restorePlan !== null) {
66
+ const restored = runStoreRestore(profileDir, ownVersion)
67
+ if (restored.restored) writeBootstrapStamp(join(profileDir, BOOTSTRAP_STAMP), ownVersion)
68
+ } else {
69
+ // A pre-existing store profile converges to this launcher's version on
70
+ // version change (or on a missing stamp — installs made before the heal
71
+ // existed, including every 0.7.0-broken profile). Never fires on a
72
+ // dev-synced profile (that pairing belongs to the restore path) nor on a
73
+ // launch that just bootstrapped.
74
+ const heal = healDecision({
75
+ profileExists: profileExisted && add === undefined,
76
+ stampVersion: readBootstrapVersion(join(profileDir, BOOTSTRAP_STAMP)),
77
+ ownVersion,
78
+ buildInfo,
79
+ })
80
+ if (heal !== null) runStoreHeal(profileDir, ownVersion, { from: heal.from })
81
+ }
61
82
 
62
83
  // A parent dsh-cc TUI process leaks DSH_CC_RESUME_SESSION / DSH_CC_AUTO_RESUME
63
84
  // / DSH_CC_CONTINUE into a child launcher's environment. Strip them up front —
package/bootstrap.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { spawnSync } from 'node:child_process'
7
- import { closeSync, existsSync, lstatSync, openSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs'
7
+ import { closeSync, existsSync, lstatSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
9
9
 
10
10
  export const PROFILE = 'tui'
@@ -815,3 +815,137 @@ export function runStoreRestore(profileDir, ownVersion, deps = {}) {
815
815
  if (locked) tryRm(lock, false)
816
816
  }
817
817
  }
818
+
819
+ // --- store heal: converge an existing profile after a launcher update --------
820
+
821
+ /** Marker file recording the launcher version that last installed the bundles. */
822
+ export const BOOTSTRAP_STAMP = '.dsh-cc-bootstrap.json'
823
+ /** Exclusive lock for the heal path (same freshness semantics as the restore lock). */
824
+ export const HEAL_LOCK = '.dsh-cc-heal.lock'
825
+
826
+ /**
827
+ * Read the launcher version recorded by the last successful bundle install.
828
+ * Missing/malformed stamp -> null (pre-heal profiles, i.e. every install made
829
+ * before this mechanism existed) — that is a HEAL, not a skip: those profiles
830
+ * carry whatever the first bootstrap pinned, forever.
831
+ *
832
+ * @param {string} stampPath
833
+ * @returns {string | null}
834
+ */
835
+ export function readBootstrapVersion(stampPath) {
836
+ try {
837
+ const value = JSON.parse(readFileSync(stampPath, 'utf8'))
838
+ return typeof value.launcherVersion === 'string' && value.launcherVersion.length > 0
839
+ ? value.launcherVersion
840
+ : null
841
+ } catch {
842
+ return null
843
+ }
844
+ }
845
+
846
+ /**
847
+ * Decide whether an EXISTING profile needs its store bundles re-added at this
848
+ * launcher's version. Skipped for dev pairings (channel `dev`, regardless of
849
+ * version match): the registry `plugin add` here would clobber the
850
+ * workspace-symlinked dev scope — dev convergence is `runStoreRestore`'s job
851
+ * alone. The first-install path is separate (`bootstrapCommand`), so callers
852
+ * must not heal on a launch that just bootstrapped.
853
+ *
854
+ * @param {{ profileExists: boolean, stampVersion: string | null, ownVersion: string, buildInfo?: Record<string, unknown> | null }} input
855
+ * @returns {{ from: string } | null}
856
+ */
857
+ export function healDecision({ profileExists, stampVersion, ownVersion, buildInfo = null }) {
858
+ if (!profileExists) return null
859
+ if (buildInfo?.channel === 'dev') return null
860
+ if (stampVersion === ownVersion) return null
861
+ return { from: stampVersion ?? 'unknown (pre-heal profile)' }
862
+ }
863
+
864
+ /**
865
+ * Write the bootstrap stamp. A failure here is reported LOUDLY, not swallowed:
866
+ * a launcher that heals but cannot record it re-runs a full network install on
867
+ * every single launch — silent degradation is unacceptable.
868
+ *
869
+ * @param {string} stampPath
870
+ * @param {string} ownVersion
871
+ * @param {(...a: unknown[]) => void} [log]
872
+ */
873
+ export function writeBootstrapStamp(stampPath, ownVersion, log = console.error) {
874
+ try {
875
+ writeFileSync(stampPath, `${JSON.stringify({ launcherVersion: ownVersion }, null, 2)}\n`)
876
+ } catch (error) {
877
+ log(`dsh-cc: CRITICAL: profile healed to ${ownVersion} but the stamp could not be written `
878
+ + `(${/** @type {Error} */ (error).message}); the full plugin install will repeat on every launch until this is fixed`)
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Re-run the bundle add against an existing profile (launcher update path).
884
+ * `pnpm add` upserts the pinned ranges and `dsh plugin add` reconciles
885
+ * `dsh.profile.bundles` idempotently — the same command surface the first
886
+ * bootstrap and `runStoreRestore` rely on. A failed add warns and continues
887
+ * launching (the stale stamp retries next launch); a failed stamp write is
888
+ * loud (see writeBootstrapStamp).
889
+ *
890
+ * @param {string} profileDir
891
+ * @param {string} ownVersion
892
+ * @param {{ from?: string, spawnSyncImpl?: typeof import('node:child_process').spawnSync, log?: (...a: unknown[]) => void, now?: () => number }} [options]
893
+ * @returns {{ healed: boolean, reason?: 'locked' | 'plugin-add-failed' }}
894
+ */
895
+ export function runStoreHeal(profileDir, ownVersion, options = {}) {
896
+ const { from, spawnSyncImpl = spawnSync, log = console.error, now = Date.now } = options
897
+ const lock = join(profileDir, HEAL_LOCK)
898
+ const stamp = join(profileDir, BOOTSTRAP_STAMP)
899
+ const home = dirname(dirname(profileDir))
900
+ const profileName = basename(profileDir)
901
+ let locked = false
902
+
903
+ try {
904
+ // Exclusive create; a lock younger than ten minutes means another launch
905
+ // is mid-heal — launch as-is rather than race pnpm on one profile dir.
906
+ try {
907
+ closeSync(openSync(lock, 'wx'))
908
+ locked = true
909
+ } catch (error) {
910
+ if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'EEXIST') throw error
911
+ let fresh = true
912
+ try {
913
+ fresh = now() - statSync(lock).mtimeMs < 10 * 60 * 1000
914
+ } catch { /* lock vanished between EEXIST and stat: proceed */ }
915
+ if (fresh) {
916
+ log('dsh-cc: profile heal already in progress, skipping')
917
+ return { healed: false, reason: 'locked' }
918
+ }
919
+ try {
920
+ rmSync(lock, { force: true })
921
+ closeSync(openSync(lock, 'wx'))
922
+ locked = true
923
+ } catch {
924
+ log('dsh-cc: profile heal already in progress, skipping')
925
+ return { healed: false, reason: 'locked' }
926
+ }
927
+ }
928
+
929
+ const result = spawnSyncImpl('dsh', ['plugin', '--profile', profileName, 'add', ...BUNDLES.map(n => `${n}@${ownVersion}`)], {
930
+ stdio: 'inherit',
931
+ env: spawnEnv(sanitizeInheritedEnv(process.env), home),
932
+ })
933
+ const success = !result.error && result.status === 0
934
+ if (!success) {
935
+ log(`dsh-cc: profile heal to ${ownVersion} failed (will retry on next launch; if the new dsh-cc version was just published, npm/pnpm's minimum-release-age window may still be hiding it)`)
936
+ return { healed: false, reason: 'plugin-add-failed' }
937
+ }
938
+ writeBootstrapStamp(stamp, ownVersion, log)
939
+ log(`dsh-cc: launcher updated ${from ?? 'unknown'} → ${ownVersion}; re-installed store bundles in profile "${profileName}"`)
940
+ return { healed: true }
941
+ } catch (error) {
942
+ log(`dsh-cc: profile heal failed unexpectedly: ${/** @type {Error} */ (error).message}`)
943
+ return { healed: false, reason: 'plugin-add-failed' }
944
+ } finally {
945
+ if (locked) {
946
+ try {
947
+ rmSync(lock, { force: true })
948
+ } catch { /* best-effort; a stale lock is taken over next launch */ }
949
+ }
950
+ }
951
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-cc/cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1-rc.1",
4
4
  "description": "Optional dsh-cc shortcut: bootstraps the tui profile and runs dsh --profile tui",
5
5
  "type": "module",
6
6
  "bin": {