dsh-git-worktree 0.9.2 → 0.9.3

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +405 -386
  2. package/LICENSE +21 -21
  3. package/NOTICE +16 -14
  4. package/README.en.md +169 -169
  5. package/README.md +169 -169
  6. package/cordis.patch.yml +11 -11
  7. package/docs/i18n.md +63 -63
  8. package/lib/adapters/initial-commit.js +3 -3
  9. package/lib/client/actions.d.ts +21 -3
  10. package/lib/client/actions.js +23 -3
  11. package/lib/client/actions.js.map +1 -1
  12. package/lib/client/console-remote/index.js +36 -8
  13. package/lib/client/console-remote/index.js.map +1 -1
  14. package/lib/client/index.js +1 -0
  15. package/lib/client/index.js.map +1 -1
  16. package/lib/client/pre-session/controller.d.ts +1 -1
  17. package/lib/client/pre-session/controller.js +33 -18
  18. package/lib/client/pre-session/controller.js.map +1 -1
  19. package/lib/client/review-console/recovery-continuation.d.ts +1 -1
  20. package/lib/client/review-console/recovery-continuation.js +9 -4
  21. package/lib/client/review-console/recovery-continuation.js.map +1 -1
  22. package/lib/client/review-console/review-console.styles.js +190 -190
  23. package/lib/client/styles.js +146 -140
  24. package/lib/client/styles.js.map +1 -1
  25. package/lib/client/target-console/target-console.styles.js +155 -155
  26. package/lib/client/workspace-sidebar/index.d.ts +11 -2
  27. package/lib/client/workspace-sidebar/index.js +50 -8
  28. package/lib/client/workspace-sidebar/index.js.map +1 -1
  29. package/lib/client.js +7624 -161
  30. package/lib/console-contract.d.ts +2 -0
  31. package/lib/console-contract.js.map +1 -1
  32. package/lib/console-host/service.js +4 -2
  33. package/lib/console-host/service.js.map +1 -1
  34. package/lib/console-remote/descriptors.js +8 -3
  35. package/lib/console-remote/descriptors.js.map +1 -1
  36. package/lib/console-remote/schemas.js +1 -0
  37. package/lib/console-remote/schemas.js.map +1 -1
  38. package/lib/host-compat.d.ts +4 -0
  39. package/lib/host-compat.js +23 -0
  40. package/lib/host-compat.js.map +1 -0
  41. package/lib/i18n/client-messages.d.ts +4 -0
  42. package/lib/i18n/client-messages.js +4 -0
  43. package/lib/i18n/client-messages.js.map +1 -1
  44. package/lib/workspace-provider-lifecycle.d.ts +2 -1
  45. package/lib/workspace-provider-lifecycle.js +6 -1
  46. package/lib/workspace-provider-lifecycle.js.map +1 -1
  47. package/package.json +151 -149
  48. package/scripts/check-publish.mjs +238 -238
  49. package/scripts/check-workspace-sidebar-version.mjs +18 -14
  50. package/scripts/clean-lib.mjs +7 -7
  51. package/scripts/dev-dsh-lib.mjs +547 -509
  52. package/scripts/dev-dsh.mjs +98 -95
  53. package/scripts/dsh-source-runner.mjs +20 -14
  54. package/scripts/workspace-sidebar-upstream-alpha.mjs +102 -0
  55. package/scripts/workspace-sidebar-upstream-next.mjs +134 -0
  56. package/scripts/workspace-sidebar-upstream.d.mts +8 -8
  57. package/scripts/workspace-sidebar-upstream.mjs +162 -162
@@ -1,509 +1,547 @@
1
- import { execFileSync, spawnSync } from 'node:child_process'
2
- import {
3
- copyFileSync,
4
- existsSync,
5
- lstatSync,
6
- mkdirSync,
7
- readFileSync,
8
- readdirSync,
9
- realpathSync,
10
- rmSync,
11
- statSync,
12
- writeFileSync,
13
- } from 'node:fs'
14
- import { homedir } from 'node:os'
15
- import { dirname, join, resolve } from 'node:path'
16
-
17
- const FIXTURE_MARKER = '.dsh-git-worktree-fixture.json'
18
- const FIXTURE_IDENTITY = Object.freeze({
19
- owner: 'dsh-git-worktree',
20
- schemaVersion: 1,
21
- })
22
- const MODES = new Set(['run', 'install', 'remove', 'smoke'])
23
-
24
- export function createDefaultOptions(projectRoot, cacheRoot) {
25
- return {
26
- projectRoot: resolve(projectRoot),
27
- cacheRoot: resolve(cacheRoot),
28
- profile: 'web',
29
- port: 3081,
30
- repo: resolve(cacheRoot, 'fixture'),
31
- }
32
- }
33
-
34
- export function parseDevDshArgs(argv, defaults) {
35
- const tokens = [...argv]
36
- const candidateMode = tokens[0] && !tokens[0].startsWith('-') ? tokens.shift() : 'run'
37
- if (!MODES.has(candidateMode)) {
38
- throw new Error(`Unknown mode ${candidateMode}. Expected run, install, remove, or smoke.`)
39
- }
40
-
41
- let profile = defaults.profile
42
- let port = defaults.port
43
- let repo = defaults.repo
44
- let repoExplicit = false
45
- let harnessRoot = defaults.harnessRoot
46
-
47
- for (let index = 0; index < tokens.length; index += 1) {
48
- const token = tokens[index]
49
- if (token === '--') continue
50
- if (token === '--profile' || token === '--repo' || token === '--port' || token === '--harness') {
51
- const value = tokens[index + 1]
52
- if (!value || value.startsWith('--')) throw new Error(`${token} requires a value.`)
53
- index += 1
54
- if (token === '--profile') profile = value
55
- if (token === '--repo') {
56
- repo = resolve(defaults.projectRoot, value)
57
- repoExplicit = true
58
- }
59
- if (token === '--port') port = parsePort(value)
60
- if (token === '--harness') harnessRoot = resolve(defaults.projectRoot, value)
61
- continue
62
- }
63
- if (token?.startsWith('--profile=')) {
64
- profile = token.slice('--profile='.length)
65
- continue
66
- }
67
- if (token?.startsWith('--repo=')) {
68
- repo = resolve(defaults.projectRoot, token.slice('--repo='.length))
69
- repoExplicit = true
70
- continue
71
- }
72
- if (token?.startsWith('--port=')) {
73
- port = parsePort(token.slice('--port='.length))
74
- continue
75
- }
76
- if (token?.startsWith('--harness=')) {
77
- harnessRoot = resolve(defaults.projectRoot, token.slice('--harness='.length))
78
- continue
79
- }
80
- throw new Error(`Unknown option ${token}.`)
81
- }
82
-
83
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile)) {
84
- throw new Error('profile must contain only letters, numbers, dot, underscore, or dash.')
85
- }
86
- return {
87
- mode: candidateMode,
88
- profile,
89
- port,
90
- repo,
91
- repoExplicit,
92
- ...(harnessRoot ? { harnessRoot } : {}),
93
- }
94
- }
95
-
96
- function parsePort(value) {
97
- const parsed = Number(value)
98
- if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65_535) {
99
- throw new Error('port must be an integer between 1 and 65535.')
100
- }
101
- return parsed
102
- }
103
-
104
- function normalizedRealPath(path) {
105
- const value = realpathSync(path).replaceAll('\\', '/')
106
- return process.platform === 'win32' ? value.toLowerCase() : value
107
- }
108
-
109
- function git(cwd, args) {
110
- return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
111
- }
112
-
113
- function validateGitRoot(repoPath) {
114
- let root
115
- try {
116
- root = git(repoPath, ['rev-parse', '--show-toplevel'])
117
- } catch {
118
- throw new Error(`Repository ${repoPath} is not a usable Git worktree.`)
119
- }
120
- if (normalizedRealPath(root) !== normalizedRealPath(repoPath)) {
121
- throw new Error(`Repository ${repoPath} is nested inside another Git worktree; pass its actual root instead.`)
122
- }
123
- }
124
-
125
- /**
126
- * Create or validate the disposable repository used by the one-command workflow.
127
- * Existing explicit repositories are read-only here. The default path requires our marker.
128
- */
129
- export function ensureDevFixture(repoPath, options) {
130
- const absolute = resolve(repoPath)
131
- if (existsSync(absolute) && lstatSync(absolute).isSymbolicLink()) {
132
- throw new Error(`Fixture path ${absolute} is a symbolic link; refusing to manage it.`)
133
- }
134
-
135
- if (options.explicit) {
136
- if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
137
- throw new Error(`Explicit repository ${absolute} must already exist.`)
138
- }
139
- validateGitRoot(absolute)
140
- return { path: absolute, created: false, managed: false }
141
- }
142
-
143
- let initialize = false
144
- if (!existsSync(absolute)) {
145
- mkdirSync(absolute, { recursive: true })
146
- initialize = true
147
- } else if (!statSync(absolute).isDirectory()) {
148
- throw new Error(`Fixture path ${absolute} is not a directory.`)
149
- } else {
150
- const entries = readdirSync(absolute)
151
- if (entries.length === 0) {
152
- initialize = true
153
- } else {
154
- const markerPath = join(absolute, FIXTURE_MARKER)
155
- if (!existsSync(markerPath)) {
156
- throw new Error(`Fixture path ${absolute} is not managed by dsh-git-worktree; existing bytes were left untouched.`)
157
- }
158
- let marker
159
- try {
160
- marker = JSON.parse(readFileSync(markerPath, 'utf8'))
161
- } catch {
162
- throw new Error(`Fixture marker at ${markerPath} is invalid; refusing to modify the directory.`)
163
- }
164
- if (marker.owner !== FIXTURE_IDENTITY.owner || marker.schemaVersion !== FIXTURE_IDENTITY.schemaVersion) {
165
- throw new Error(`Fixture marker at ${markerPath} has an unexpected identity.`)
166
- }
167
- }
168
- }
169
-
170
- if (initialize) {
171
- writeFileSync(join(absolute, FIXTURE_MARKER), `${JSON.stringify(FIXTURE_IDENTITY, null, 2)}\n`)
172
- writeFileSync(join(absolute, 'tracked.txt'), 'base\n')
173
- git(absolute, ['init'])
174
- git(absolute, ['add', FIXTURE_MARKER, 'tracked.txt'])
175
- git(absolute, [
176
- '-c', 'user.name=DSH Local Test',
177
- '-c', 'user.email=dsh-test@example.local',
178
- 'commit', '-m', 'test: initialize disposable DSH worktree fixture',
179
- ])
180
- }
181
-
182
- validateGitRoot(absolute)
183
- const status = git(absolute, ['status', '--porcelain=v1', '--untracked-files=all'])
184
- if (status !== '') {
185
- throw new Error(`Managed fixture ${absolute} is not clean. Review it manually; the workflow will never reset or delete it.`)
186
- }
187
- const worktreeCount = git(absolute, ['worktree', 'list', '--porcelain'])
188
- .split(/\r?\n/)
189
- .filter((line) => line.startsWith('worktree '))
190
- .length
191
- if (worktreeCount !== 1) {
192
- throw new Error(`Managed fixture ${absolute} still has ${worktreeCount - 1} linked Worktree(s). Finalize or remove them before reusing it.`)
193
- }
194
- return { path: absolute, created: initialize, managed: true }
195
- }
196
-
197
- function isHarnessRoot(path) {
198
- return existsSync(join(path, 'package.json'))
199
- && existsSync(join(path, 'apps', 'cli', 'src', 'bin.ts'))
200
- }
201
-
202
- function isHarnessSourceReady(path) {
203
- return isHarnessRoot(path)
204
- && existsSync(join(path, 'node_modules', 'tsx', 'package.json'))
205
- }
206
-
207
- export function discoverHarnessRoot(projectRoot, environment = process.env) {
208
- if (environment.DSH_HARNESS_ROOT) {
209
- const configured = resolve(environment.DSH_HARNESS_ROOT)
210
- if (!isHarnessRoot(configured)) {
211
- throw new Error(`DSH_HARNESS_ROOT does not point to a DeepSeek Harness checkout: ${configured}`)
212
- }
213
- return configured
214
- }
215
- try {
216
- const commonDir = git(projectRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'])
217
- const localProjectRoot = dirname(resolve(projectRoot, commonDir))
218
- const parent = dirname(localProjectRoot)
219
- const candidates = [
220
- join(parent, 'deepseek-harness'),
221
- join(dirname(parent), 'deepseek-harness'),
222
- join(parent, 'DeepSeek', 'deepseek-harness'),
223
- ]
224
- const validCandidates = candidates.filter(isHarnessRoot)
225
- return validCandidates.find(isHarnessSourceReady) ?? validCandidates[0]
226
- } catch {
227
- return undefined
228
- }
229
- }
230
-
231
- export function executable(name, platform = process.platform) {
232
- return platform === 'win32' ? `${name}.cmd` : name
233
- }
234
-
235
- export function createDshInvocation(options) {
236
- const cwd = options.cwd ?? options.projectRoot
237
- const args = options.args ?? []
238
- if (!options.harnessRoot) {
239
- return {
240
- command: executable('dsh', options.platform),
241
- args,
242
- cwd,
243
- source: false,
244
- }
245
- }
246
- const harnessRoot = resolve(options.harnessRoot)
247
- if (!isHarnessRoot(harnessRoot)) {
248
- throw new Error(`Harness source checkout is invalid: ${harnessRoot}`)
249
- }
250
- if (!isHarnessSourceReady(harnessRoot)) {
251
- throw new Error(
252
- `Harness source checkout is not prepared: ${harnessRoot}. Run pnpm --dir "${harnessRoot}" install before dev preview.`,
253
- )
254
- }
255
- return {
256
- command: executable('pnpm', options.platform),
257
- args: [
258
- '--dir', harnessRoot,
259
- 'exec', 'node', '--import', 'tsx/esm',
260
- join(options.projectRoot, 'scripts', 'dsh-source-runner.mjs'),
261
- cwd,
262
- join(harnessRoot, 'apps', 'cli', 'src', 'bin.ts'),
263
- ...args,
264
- ],
265
- cwd: options.projectRoot,
266
- source: true,
267
- }
268
- }
269
-
270
- export function createDshLaunch(options) {
271
- return createDshInvocation({
272
- projectRoot: options.projectRoot,
273
- harnessRoot: options.harnessRoot,
274
- platform: options.platform,
275
- cwd: options.workspaceRoot,
276
- args: ['--profile', options.profile, '--port', String(options.port)],
277
- })
278
- }
279
-
280
- function assertWindowsCommandSafe(command, args) {
281
- for (const value of [command, ...args]) {
282
- if (/[\0\r\n"&|<>^%!]/.test(value)) {
283
- throw new Error(`Windows command argument contains unsupported shell metacharacters: ${value}`)
284
- }
285
- }
286
- }
287
-
288
- export function runProcess(command, args, options = {}) {
289
- const capture = options.capture === true
290
- const windows = process.platform === 'win32'
291
- if (windows) assertWindowsCommandSafe(command, args)
292
- const actualCommand = windows ? process.env.ComSpec || 'cmd.exe' : command
293
- const actualArgs = windows
294
- ? ['/d', '/v:off', '/c', command, ...args]
295
- : args
296
- const result = spawnSync(actualCommand, actualArgs, {
297
- cwd: options.cwd,
298
- env: options.env,
299
- encoding: 'utf8',
300
- stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
301
- })
302
- if (result.error) throw result.error
303
- if (result.status !== 0) {
304
- const detail = capture ? `\n${result.stderr || result.stdout || ''}` : ''
305
- throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}.${detail}`)
306
- }
307
- return { stdout: capture ? result.stdout ?? '' : '' }
308
- }
309
-
310
- function copyPackageEntry(source, destination) {
311
- const info = lstatSync(source)
312
- if (info.isSymbolicLink()) throw new Error(`Refusing to package symbolic link ${source}.`)
313
- if (info.isDirectory()) {
314
- mkdirSync(destination, { recursive: true })
315
- for (const entry of readdirSync(source)) {
316
- copyPackageEntry(join(source, entry), join(destination, entry))
317
- }
318
- return
319
- }
320
- if (!info.isFile()) throw new Error(`Unsupported package entry ${source}.`)
321
- mkdirSync(resolve(destination, '..'), { recursive: true })
322
- copyFileSync(source, destination)
323
- }
324
-
325
- function dshHomeOf(options) {
326
- return options.environment?.DSH_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')
327
- }
328
-
329
- function profileManifestOf(options) {
330
- return options.profileManifestPath ?? join(dshHomeOf(options), 'profiles', options.profile, 'package.json')
331
- }
332
-
333
- function archiveReferenceFromManifest(manifestPath, required = false) {
334
- if (!existsSync(manifestPath)) {
335
- if (required) throw new Error(`DSH profile manifest is missing: ${manifestPath}`)
336
- return undefined
337
- }
338
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
339
- const specifier = manifest.dependencies?.['dsh-git-worktree']
340
- if (typeof specifier !== 'string' || !specifier.startsWith('file:')) {
341
- if (required) throw new Error('DSH profile did not record dsh-git-worktree as a local file dependency.')
342
- return undefined
343
- }
344
- let filePath
345
- try {
346
- filePath = decodeURIComponent(specifier.slice('file:'.length))
347
- } catch {
348
- if (required) throw new Error(`DSH profile recorded an invalid local file dependency: ${specifier}`)
349
- return undefined
350
- }
351
- return resolve(dirname(manifestPath), filePath)
352
- }
353
-
354
- function profileArchiveReference(options) {
355
- return archiveReferenceFromManifest(profileManifestOf(options), true)
356
- }
357
-
358
- function comparablePath(path) {
359
- const normalized = resolve(path).replaceAll('\\', '/')
360
- return process.platform === 'win32' ? normalized.toLowerCase() : normalized
361
- }
362
-
363
- function referencedProfileArchives(options) {
364
- const references = new Set()
365
- const addManifest = (manifestPath) => {
366
- try {
367
- const reference = archiveReferenceFromManifest(manifestPath)
368
- if (reference !== undefined) references.add(comparablePath(reference))
369
- } catch {
370
- // A malformed unrelated profile must not make local snapshot cleanup delete more files.
371
- }
372
- }
373
- addManifest(profileManifestOf(options))
374
- const profilesRoot = join(dshHomeOf(options), 'profiles')
375
- if (existsSync(profilesRoot)) {
376
- for (const entry of readdirSync(profilesRoot)) addManifest(join(profilesRoot, entry, 'package.json'))
377
- }
378
- return references
379
- }
380
-
381
- function cleanSupersededArchives(cacheRoot, keepPath, options) {
382
- const preserved = referencedProfileArchives(options)
383
- preserved.add(comparablePath(keepPath))
384
- for (const entry of readdirSync(cacheRoot)) {
385
- if (!/^dsh-git-worktree-\d+\.tgz$/.test(entry)) continue
386
- const candidate = join(cacheRoot, entry)
387
- if (!preserved.has(comparablePath(candidate))) rmSync(candidate, { force: true })
388
- }
389
- }
390
-
391
- function repairMissingCurrentProfileArchive(options, archivePath) {
392
- const previous = archiveReferenceFromManifest(profileManifestOf(options))
393
- if (previous === undefined || existsSync(previous)) return
394
- const managedCache = comparablePath(dirname(previous)) === comparablePath(options.cacheRoot)
395
- && /^dsh-git-worktree-\d+\.tgz$/.test(previous.split(/[\\/]/u).at(-1) ?? '')
396
- if (!managedCache) {
397
- throw new Error(`DSH profile references missing local archive outside the managed cache: ${previous}`)
398
- }
399
- copyFileSync(archivePath, previous)
400
- }
401
-
402
- function assertDshRuntimeAvailable(options) {
403
- if (options.harnessRoot) {
404
- createDshInvocation({
405
- projectRoot: options.projectRoot,
406
- harnessRoot: options.harnessRoot,
407
- platform: options.platform,
408
- args: [],
409
- })
410
- return
411
- }
412
- if (options.run) return
413
- try {
414
- runProcess(executable('dsh', options.platform), ['--version'], { capture: true })
415
- } catch (cause) {
416
- throw new Error(
417
- 'No Harness source checkout was selected and the installed dsh CLI is unavailable. Pass --harness <path>, set DSH_HARNESS_ROOT, or install dsh on PATH.',
418
- { cause },
419
- )
420
- }
421
- }
422
-
423
- function runDsh(options, args, runOptions = {}) {
424
- const invocation = createDshInvocation({
425
- projectRoot: options.projectRoot,
426
- harnessRoot: options.harnessRoot,
427
- platform: options.platform,
428
- cwd: runOptions.cwd ?? options.projectRoot,
429
- args,
430
- })
431
- return (options.run ?? runProcess)(invocation.command, invocation.args, {
432
- cwd: invocation.cwd,
433
- ...(runOptions.capture === true ? { capture: true } : {}),
434
- })
435
- }
436
-
437
- /** Build, pack and install only the current local snapshot. No registry publish occurs. */
438
- export function installLocalSnapshot(options) {
439
- const run = options.run ?? runProcess
440
- const pnpm = executable('pnpm', options.platform)
441
- assertDshRuntimeAvailable(options)
442
- mkdirSync(options.cacheRoot, { recursive: true })
443
-
444
- if (!existsSync(join(options.projectRoot, 'node_modules'))) {
445
- run(pnpm, ['install', '--frozen-lockfile'], { cwd: options.projectRoot })
446
- }
447
- run(pnpm, ['run', 'typecheck'], { cwd: options.projectRoot })
448
- run(pnpm, ['run', 'build'], { cwd: options.projectRoot })
449
-
450
- const snapshotId = options.now?.() ?? Date.now()
451
- const archivePath = join(options.cacheRoot, `dsh-git-worktree-${snapshotId}.tgz`)
452
- const stagingRoot = join(options.cacheRoot, `.staging-${snapshotId}`)
453
- for (const entry of readdirSync(options.cacheRoot)) {
454
- if (/^\.staging-\d+$/.test(entry)) rmSync(join(options.cacheRoot, entry), { recursive: true, force: true })
455
- }
456
- mkdirSync(stagingRoot, { recursive: true })
457
- try {
458
- const manifest = JSON.parse(readFileSync(join(options.projectRoot, 'package.json'), 'utf8'))
459
- const stagedManifest = {
460
- ...manifest,
461
- version: `${manifest.version}-dev.${snapshotId}`,
462
- scripts: {},
463
- }
464
- writeFileSync(join(stagingRoot, 'package.json'), `${JSON.stringify(stagedManifest, null, 2)}\n`)
465
- for (const entry of ['lib', 'cordis.patch.yml', 'README.md', 'README.en.md', 'docs/i18n.md', 'LICENSE', 'NOTICE']) {
466
- const source = join(options.projectRoot, entry)
467
- if (existsSync(source)) copyPackageEntry(source, join(stagingRoot, entry))
468
- }
469
- run(pnpm, ['pack', '--out', archivePath, '--json'], { cwd: stagingRoot })
470
- } finally {
471
- rmSync(stagingRoot, { recursive: true, force: true })
472
- }
473
- if (!existsSync(archivePath)) throw new Error(`pnpm pack did not create ${archivePath}.`)
474
-
475
- // pnpm resolves the profile's existing direct file dependency before it can
476
- // replace it. Repair only our own missing cache path with the new snapshot;
477
- // after the add succeeds the obsolete recovery copy becomes collectible.
478
- repairMissingCurrentProfileArchive(options, archivePath)
479
- runDsh(options, ['plugin', '--profile', options.profile, 'add', archivePath])
480
- const referencedArchive = profileArchiveReference(options)
481
- if (comparablePath(referencedArchive) !== comparablePath(archivePath)) {
482
- throw new Error(`DSH profile ${options.profile} still references ${referencedArchive}; preserving old and new archives for recovery.`)
483
- }
484
- const dump = runDsh(options, ['--profile', options.profile, '--dump-config'], { capture: true }).stdout
485
- if (!dump.includes('dsh-git-worktree')) {
486
- throw new Error(`Profile ${options.profile} installed the tarball but did not compose dsh-git-worktree.`)
487
- }
488
- cleanSupersededArchives(options.cacheRoot, archivePath, options)
489
- return { archivePath, dumpConfig: dump }
490
- }
491
-
492
- export function removeLocalSnapshot(options) {
493
- assertDshRuntimeAvailable(options)
494
- runDsh(options, ['plugin', '--profile', options.profile, 'remove', 'dsh-git-worktree'])
495
- if (existsSync(options.cacheRoot)) {
496
- for (const entry of readdirSync(options.cacheRoot)) {
497
- if (/^dsh-git-worktree-\d+\.tgz$/.test(entry)) rmSync(join(options.cacheRoot, entry), { force: true })
498
- }
499
- }
500
- }
501
-
502
- export function smokeLocalSnapshot(options) {
503
- assertDshRuntimeAvailable(options)
504
- const dump = runDsh(options, ['--profile', options.profile, '--dump-config'], { capture: true }).stdout
505
- if (!dump.includes('dsh-git-worktree')) {
506
- throw new Error(`Profile ${options.profile} does not compose dsh-git-worktree.`)
507
- }
508
- return dump
509
- }
1
+ import { execFileSync, spawnSync } from 'node:child_process'
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ realpathSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from 'node:fs'
14
+ import { homedir, tmpdir } from 'node:os'
15
+ import { dirname, join, resolve } from 'node:path'
16
+
17
+ const FIXTURE_MARKER = '.dsh-git-worktree-fixture.json'
18
+ const FIXTURE_IDENTITY = Object.freeze({
19
+ owner: 'dsh-git-worktree',
20
+ schemaVersion: 1,
21
+ })
22
+ const MODES = new Set(['run', 'install', 'remove', 'smoke'])
23
+
24
+ /** Isolated Profile runs must not prune archives belonging to other DSH homes. */
25
+ export function devCacheRoot(environment = process.env) {
26
+ return environment.DSH_HOME
27
+ ? join(resolve(environment.DSH_HOME), 'dev-dsh-cache')
28
+ : join(tmpdir(), 'dsh-git-worktree-dev')
29
+ }
30
+
31
+ export function createDefaultOptions(projectRoot, cacheRoot, isolated = false) {
32
+ return {
33
+ projectRoot: resolve(projectRoot),
34
+ cacheRoot: resolve(cacheRoot),
35
+ profile: 'web',
36
+ port: 3081,
37
+ // Match the Host's own first-use Workspace path when DSH_HOME is isolated.
38
+ repo: isolated
39
+ ? resolve(cacheRoot, 'Documents', 'deepseek-harness', 'default-workspace')
40
+ : resolve(cacheRoot, 'fixture'),
41
+ }
42
+ }
43
+
44
+ export function parseDevDshArgs(argv, defaults) {
45
+ const tokens = [...argv]
46
+ const candidateMode = tokens[0] && !tokens[0].startsWith('-') ? tokens.shift() : 'run'
47
+ if (!MODES.has(candidateMode)) {
48
+ throw new Error(`Unknown mode ${candidateMode}. Expected run, install, remove, or smoke.`)
49
+ }
50
+
51
+ let profile = defaults.profile
52
+ let port = defaults.port
53
+ let repo = defaults.repo
54
+ let repoExplicit = false
55
+ let harnessRoot = defaults.harnessRoot
56
+
57
+ for (let index = 0; index < tokens.length; index += 1) {
58
+ const token = tokens[index]
59
+ if (token === '--') continue
60
+ if (token === '--profile' || token === '--repo' || token === '--port' || token === '--harness') {
61
+ const value = tokens[index + 1]
62
+ if (!value || value.startsWith('--')) throw new Error(`${token} requires a value.`)
63
+ index += 1
64
+ if (token === '--profile') profile = value
65
+ if (token === '--repo') {
66
+ repo = resolve(defaults.projectRoot, value)
67
+ repoExplicit = true
68
+ }
69
+ if (token === '--port') port = parsePort(value)
70
+ if (token === '--harness') harnessRoot = resolve(defaults.projectRoot, value)
71
+ continue
72
+ }
73
+ if (token?.startsWith('--profile=')) {
74
+ profile = token.slice('--profile='.length)
75
+ continue
76
+ }
77
+ if (token?.startsWith('--repo=')) {
78
+ repo = resolve(defaults.projectRoot, token.slice('--repo='.length))
79
+ repoExplicit = true
80
+ continue
81
+ }
82
+ if (token?.startsWith('--port=')) {
83
+ port = parsePort(token.slice('--port='.length))
84
+ continue
85
+ }
86
+ if (token?.startsWith('--harness=')) {
87
+ harnessRoot = resolve(defaults.projectRoot, token.slice('--harness='.length))
88
+ continue
89
+ }
90
+ throw new Error(`Unknown option ${token}.`)
91
+ }
92
+
93
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile)) {
94
+ throw new Error('profile must contain only letters, numbers, dot, underscore, or dash.')
95
+ }
96
+ return {
97
+ mode: candidateMode,
98
+ profile,
99
+ port,
100
+ repo,
101
+ repoExplicit,
102
+ ...(harnessRoot ? { harnessRoot } : {}),
103
+ }
104
+ }
105
+
106
+ function parsePort(value) {
107
+ const parsed = Number(value)
108
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65_535) {
109
+ throw new Error('port must be an integer between 1 and 65535.')
110
+ }
111
+ return parsed
112
+ }
113
+
114
+ function normalizedRealPath(path) {
115
+ const value = realpathSync(path).replaceAll('\\', '/')
116
+ return process.platform === 'win32' ? value.toLowerCase() : value
117
+ }
118
+
119
+ function git(cwd, args) {
120
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
121
+ }
122
+
123
+ function validateGitRoot(repoPath) {
124
+ let root
125
+ try {
126
+ root = git(repoPath, ['rev-parse', '--show-toplevel'])
127
+ } catch {
128
+ throw new Error(`Repository ${repoPath} is not a usable Git worktree.`)
129
+ }
130
+ if (normalizedRealPath(root) !== normalizedRealPath(repoPath)) {
131
+ throw new Error(`Repository ${repoPath} is nested inside another Git worktree; pass its actual root instead.`)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Create or validate the disposable repository used by the one-command workflow.
137
+ * Existing explicit repositories are read-only here. The default path requires our marker.
138
+ */
139
+ export function ensureDevFixture(repoPath, options) {
140
+ const absolute = resolve(repoPath)
141
+ if (existsSync(absolute) && lstatSync(absolute).isSymbolicLink()) {
142
+ throw new Error(`Fixture path ${absolute} is a symbolic link; refusing to manage it.`)
143
+ }
144
+
145
+ if (options.explicit) {
146
+ if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
147
+ throw new Error(`Explicit repository ${absolute} must already exist.`)
148
+ }
149
+ validateGitRoot(absolute)
150
+ return { path: absolute, created: false, managed: false }
151
+ }
152
+
153
+ let initialize = false
154
+ if (!existsSync(absolute)) {
155
+ mkdirSync(absolute, { recursive: true })
156
+ initialize = true
157
+ } else if (!statSync(absolute).isDirectory()) {
158
+ throw new Error(`Fixture path ${absolute} is not a directory.`)
159
+ } else {
160
+ const entries = readdirSync(absolute)
161
+ if (entries.length === 0) {
162
+ initialize = true
163
+ } else {
164
+ const markerPath = join(absolute, FIXTURE_MARKER)
165
+ if (!existsSync(markerPath)) {
166
+ throw new Error(`Fixture path ${absolute} is not managed by dsh-git-worktree; existing bytes were left untouched.`)
167
+ }
168
+ let marker
169
+ try {
170
+ marker = JSON.parse(readFileSync(markerPath, 'utf8'))
171
+ } catch {
172
+ throw new Error(`Fixture marker at ${markerPath} is invalid; refusing to modify the directory.`)
173
+ }
174
+ if (marker.owner !== FIXTURE_IDENTITY.owner || marker.schemaVersion !== FIXTURE_IDENTITY.schemaVersion) {
175
+ throw new Error(`Fixture marker at ${markerPath} has an unexpected identity.`)
176
+ }
177
+ }
178
+ }
179
+
180
+ if (initialize) {
181
+ writeFileSync(join(absolute, FIXTURE_MARKER), `${JSON.stringify(FIXTURE_IDENTITY, null, 2)}\n`)
182
+ writeFileSync(join(absolute, 'tracked.txt'), 'base\n')
183
+ git(absolute, ['init'])
184
+ git(absolute, ['add', FIXTURE_MARKER, 'tracked.txt'])
185
+ git(absolute, [
186
+ '-c', 'user.name=DSH Local Test',
187
+ '-c', 'user.email=dsh-test@example.local',
188
+ 'commit', '-m', 'test: initialize disposable DSH worktree fixture',
189
+ ])
190
+ }
191
+
192
+ validateGitRoot(absolute)
193
+ const status = git(absolute, ['status', '--porcelain=v1', '--untracked-files=all'])
194
+ if (status !== '') {
195
+ throw new Error(`Managed fixture ${absolute} is not clean. Review it manually; the workflow will never reset or delete it.`)
196
+ }
197
+ const worktreeCount = git(absolute, ['worktree', 'list', '--porcelain'])
198
+ .split(/\r?\n/)
199
+ .filter((line) => line.startsWith('worktree '))
200
+ .length
201
+ if (worktreeCount !== 1) {
202
+ throw new Error(`Managed fixture ${absolute} still has ${worktreeCount - 1} linked Worktree(s). Finalize or remove them before reusing it.`)
203
+ }
204
+ return { path: absolute, created: initialize, managed: true }
205
+ }
206
+
207
+ function isHarnessRoot(path) {
208
+ return existsSync(join(path, 'package.json'))
209
+ && existsSync(join(path, 'apps', 'cli', 'src', 'bin.ts'))
210
+ }
211
+
212
+ function isHarnessSourceReady(path) {
213
+ return isHarnessRoot(path)
214
+ && existsSync(join(path, 'node_modules', 'tsx', 'package.json'))
215
+ }
216
+
217
+ export function discoverHarnessRoot(projectRoot, environment = process.env) {
218
+ if (environment.DSH_HARNESS_ROOT) {
219
+ const configured = resolve(environment.DSH_HARNESS_ROOT)
220
+ if (!isHarnessRoot(configured)) {
221
+ throw new Error(`DSH_HARNESS_ROOT does not point to a DeepSeek Harness checkout: ${configured}`)
222
+ }
223
+ return configured
224
+ }
225
+ try {
226
+ const commonDir = git(projectRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'])
227
+ const localProjectRoot = dirname(resolve(projectRoot, commonDir))
228
+ const parent = dirname(localProjectRoot)
229
+ const candidates = [
230
+ join(parent, 'deepseek-harness'),
231
+ join(dirname(parent), 'deepseek-harness'),
232
+ join(parent, 'DeepSeek', 'deepseek-harness'),
233
+ ]
234
+ const validCandidates = candidates.filter(isHarnessRoot)
235
+ return validCandidates.find(isHarnessSourceReady) ?? validCandidates[0]
236
+ } catch {
237
+ return undefined
238
+ }
239
+ }
240
+
241
+ export function executable(name, platform = process.platform) {
242
+ return platform === 'win32' ? `${name}.cmd` : name
243
+ }
244
+
245
+ export function createDshInvocation(options) {
246
+ const cwd = options.cwd ?? options.projectRoot
247
+ const args = options.args ?? []
248
+ if (!options.harnessRoot) {
249
+ return {
250
+ command: executable('dsh', options.platform),
251
+ args,
252
+ cwd,
253
+ source: false,
254
+ }
255
+ }
256
+ const harnessRoot = resolve(options.harnessRoot)
257
+ if (!isHarnessRoot(harnessRoot)) {
258
+ throw new Error(`Harness source checkout is invalid: ${harnessRoot}`)
259
+ }
260
+ if (!isHarnessSourceReady(harnessRoot)) {
261
+ throw new Error(
262
+ `Harness source checkout is not prepared: ${harnessRoot}. Run pnpm --dir "${harnessRoot}" install before dev preview.`,
263
+ )
264
+ }
265
+ const pinnedPnpm = join(harnessRoot, 'node_modules', '.bin', executable('pnpm', options.platform))
266
+ if (!existsSync(pinnedPnpm)) {
267
+ throw new Error(`Harness source checkout is missing its pinned pnpm: ${pinnedPnpm}. Install the checkout's development dependencies before dev preview.`)
268
+ }
269
+ return {
270
+ command: pinnedPnpm,
271
+ args: [
272
+ '--dir', harnessRoot,
273
+ 'exec', 'node', '--import', 'tsx/esm',
274
+ join(options.projectRoot, 'scripts', 'dsh-source-runner.mjs'),
275
+ cwd,
276
+ join(harnessRoot, 'apps', 'cli', 'src', 'bin.ts'),
277
+ ...args,
278
+ ],
279
+ cwd: options.projectRoot,
280
+ source: true,
281
+ }
282
+ }
283
+
284
+ export function createDshLaunch(options) {
285
+ return createDshInvocation({
286
+ projectRoot: options.projectRoot,
287
+ harnessRoot: options.harnessRoot,
288
+ platform: options.platform,
289
+ cwd: options.workspaceRoot,
290
+ args: [
291
+ '--profile', options.profile,
292
+ ...(options.isolationPatchPath ? ['--patch', options.isolationPatchPath] : []),
293
+ '--port', String(options.port),
294
+ ...(options.isolationPatchPath ? ['--no-open'] : []),
295
+ ],
296
+ })
297
+ }
298
+
299
+ /** Confine Host first-use Documents workspaces as well as its Profile to DSH_HOME. */
300
+ export function prepareIsolatedWorkspacePatch(cacheRoot) {
301
+ const documents = join(resolve(cacheRoot), 'Documents')
302
+ mkdirSync(documents, { recursive: true })
303
+ const patchPath = join(resolve(cacheRoot), 'workspace-isolation.patch.yml')
304
+ writeFileSync(patchPath, `- id: workspace-controller\n config:\n documentsDirectory: ${JSON.stringify(documents)}\n`)
305
+ return patchPath
306
+ }
307
+
308
+ function assertWindowsCommandSafe(command, args) {
309
+ for (const value of [command, ...args]) {
310
+ if (/[\0\r\n"&|<>^%!]/.test(value)) {
311
+ throw new Error(`Windows command argument contains unsupported shell metacharacters: ${value}`)
312
+ }
313
+ }
314
+ }
315
+
316
+ export function runProcess(command, args, options = {}) {
317
+ const capture = options.capture === true
318
+ const windows = process.platform === 'win32'
319
+ if (windows) assertWindowsCommandSafe(command, args)
320
+ const actualCommand = windows ? process.env.ComSpec || 'cmd.exe' : command
321
+ const actualArgs = windows
322
+ ? ['/d', '/v:off', '/c', command, ...args]
323
+ : args
324
+ const result = spawnSync(actualCommand, actualArgs, {
325
+ cwd: options.cwd,
326
+ env: options.env,
327
+ encoding: 'utf8',
328
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
329
+ })
330
+ if (result.error) throw result.error
331
+ if (result.status !== 0) {
332
+ const detail = capture ? `\n${result.stderr || result.stdout || ''}` : ''
333
+ throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}.${detail}`)
334
+ }
335
+ return { stdout: capture ? result.stdout ?? '' : '' }
336
+ }
337
+
338
+ function copyPackageEntry(source, destination) {
339
+ const info = lstatSync(source)
340
+ if (info.isSymbolicLink()) throw new Error(`Refusing to package symbolic link ${source}.`)
341
+ if (info.isDirectory()) {
342
+ mkdirSync(destination, { recursive: true })
343
+ for (const entry of readdirSync(source)) {
344
+ copyPackageEntry(join(source, entry), join(destination, entry))
345
+ }
346
+ return
347
+ }
348
+ if (!info.isFile()) throw new Error(`Unsupported package entry ${source}.`)
349
+ mkdirSync(resolve(destination, '..'), { recursive: true })
350
+ copyFileSync(source, destination)
351
+ }
352
+
353
+ function dshHomeOf(options) {
354
+ return options.environment?.DSH_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')
355
+ }
356
+
357
+ function profileManifestOf(options) {
358
+ return options.profileManifestPath ?? join(dshHomeOf(options), 'profiles', options.profile, 'package.json')
359
+ }
360
+
361
+ function archiveReferenceFromManifest(manifestPath, required = false) {
362
+ if (!existsSync(manifestPath)) {
363
+ if (required) throw new Error(`DSH profile manifest is missing: ${manifestPath}`)
364
+ return undefined
365
+ }
366
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
367
+ const specifier = manifest.dependencies?.['dsh-git-worktree']
368
+ if (typeof specifier !== 'string' || !specifier.startsWith('file:')) {
369
+ if (required) throw new Error('DSH profile did not record dsh-git-worktree as a local file dependency.')
370
+ return undefined
371
+ }
372
+ let filePath
373
+ try {
374
+ filePath = decodeURIComponent(specifier.slice('file:'.length))
375
+ } catch {
376
+ if (required) throw new Error(`DSH profile recorded an invalid local file dependency: ${specifier}`)
377
+ return undefined
378
+ }
379
+ return resolve(dirname(manifestPath), filePath)
380
+ }
381
+
382
+ function profileArchiveReference(options) {
383
+ return archiveReferenceFromManifest(profileManifestOf(options), true)
384
+ }
385
+
386
+ function comparablePath(path) {
387
+ const normalized = resolve(path).replaceAll('\\', '/')
388
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized
389
+ }
390
+
391
+ function referencedProfileArchives(options) {
392
+ const references = new Set()
393
+ const addManifest = (manifestPath) => {
394
+ try {
395
+ const reference = archiveReferenceFromManifest(manifestPath)
396
+ if (reference !== undefined) references.add(comparablePath(reference))
397
+ } catch {
398
+ // A malformed unrelated profile must not make local snapshot cleanup delete more files.
399
+ }
400
+ }
401
+ addManifest(profileManifestOf(options))
402
+ const profilesRoot = join(dshHomeOf(options), 'profiles')
403
+ if (existsSync(profilesRoot)) {
404
+ for (const entry of readdirSync(profilesRoot)) addManifest(join(profilesRoot, entry, 'package.json'))
405
+ }
406
+ return references
407
+ }
408
+
409
+ function cleanSupersededArchives(cacheRoot, keepPath, options) {
410
+ const preserved = referencedProfileArchives(options)
411
+ preserved.add(comparablePath(keepPath))
412
+ for (const entry of readdirSync(cacheRoot)) {
413
+ if (!/^dsh-git-worktree-\d+\.tgz$/.test(entry)) continue
414
+ const candidate = join(cacheRoot, entry)
415
+ if (!preserved.has(comparablePath(candidate))) rmSync(candidate, { force: true })
416
+ }
417
+ }
418
+
419
+ function repairMissingCurrentProfileArchive(options, archivePath) {
420
+ const previous = archiveReferenceFromManifest(profileManifestOf(options))
421
+ if (previous === undefined || existsSync(previous)) return
422
+ const managedCache = comparablePath(dirname(previous)) === comparablePath(options.cacheRoot)
423
+ && /^dsh-git-worktree-\d+\.tgz$/.test(previous.split(/[\\/]/u).at(-1) ?? '')
424
+ if (!managedCache) {
425
+ throw new Error(`DSH profile references missing local archive outside the managed cache: ${previous}`)
426
+ }
427
+ copyFileSync(archivePath, previous)
428
+ }
429
+
430
+ function assertDshRuntimeAvailable(options) {
431
+ if (options.harnessRoot) {
432
+ createDshInvocation({
433
+ projectRoot: options.projectRoot,
434
+ harnessRoot: options.harnessRoot,
435
+ platform: options.platform,
436
+ args: [],
437
+ })
438
+ return
439
+ }
440
+ if (options.run) return
441
+ try {
442
+ runProcess(executable('dsh', options.platform), ['--version'], { capture: true })
443
+ } catch (cause) {
444
+ throw new Error(
445
+ 'No Harness source checkout was selected and the installed dsh CLI is unavailable. Pass --harness <path>, set DSH_HARNESS_ROOT, or install dsh on PATH.',
446
+ { cause },
447
+ )
448
+ }
449
+ }
450
+
451
+ function runDsh(options, args, runOptions = {}) {
452
+ const invocation = createDshInvocation({
453
+ projectRoot: options.projectRoot,
454
+ harnessRoot: options.harnessRoot,
455
+ platform: options.platform,
456
+ cwd: runOptions.cwd ?? options.projectRoot,
457
+ args,
458
+ })
459
+ return (options.run ?? runProcess)(invocation.command, invocation.args, {
460
+ cwd: invocation.cwd,
461
+ ...(runOptions.capture === true ? { capture: true } : {}),
462
+ })
463
+ }
464
+
465
+ /** Build, pack and install only the current local snapshot. No registry publish occurs. */
466
+ export function installLocalSnapshot(options) {
467
+ const run = options.run ?? runProcess
468
+ const pnpm = executable('pnpm', options.platform)
469
+ assertDshRuntimeAvailable(options)
470
+ mkdirSync(options.cacheRoot, { recursive: true })
471
+
472
+ if (!existsSync(join(options.projectRoot, 'node_modules'))) {
473
+ run(pnpm, ['install', '--frozen-lockfile'], { cwd: options.projectRoot })
474
+ }
475
+ run(pnpm, ['run', 'typecheck'], { cwd: options.projectRoot })
476
+ run(pnpm, ['run', 'build'], { cwd: options.projectRoot })
477
+
478
+ const snapshotId = options.now?.() ?? Date.now()
479
+ const archivePath = join(options.cacheRoot, `dsh-git-worktree-${snapshotId}.tgz`)
480
+ const stagingRoot = join(options.cacheRoot, `.staging-${snapshotId}`)
481
+ for (const entry of readdirSync(options.cacheRoot)) {
482
+ if (/^\.staging-\d+$/.test(entry)) rmSync(join(options.cacheRoot, entry), { recursive: true, force: true })
483
+ }
484
+ mkdirSync(stagingRoot, { recursive: true })
485
+ try {
486
+ const manifest = JSON.parse(readFileSync(join(options.projectRoot, 'package.json'), 'utf8'))
487
+ const stagedManifest = {
488
+ ...manifest,
489
+ version: `${manifest.version}-dev.${snapshotId}`,
490
+ scripts: {},
491
+ }
492
+ writeFileSync(join(stagingRoot, 'package.json'), `${JSON.stringify(stagedManifest, null, 2)}\n`)
493
+ for (const entry of ['lib', 'cordis.patch.yml', 'README.md', 'README.en.md', 'docs/i18n.md', 'LICENSE', 'NOTICE']) {
494
+ const source = join(options.projectRoot, entry)
495
+ if (existsSync(source)) copyPackageEntry(source, join(stagingRoot, entry))
496
+ }
497
+ run(pnpm, ['pack', '--out', archivePath, '--json'], { cwd: stagingRoot })
498
+ } finally {
499
+ rmSync(stagingRoot, { recursive: true, force: true })
500
+ }
501
+ if (!existsSync(archivePath)) throw new Error(`pnpm pack did not create ${archivePath}.`)
502
+
503
+ // The official config-dump entry initializes a missing named Profile using
504
+ // DSH's template. Plugin add alone may leave a fresh profile without its
505
+ // manifest; never create or overwrite that Host-owned file ourselves.
506
+ if (!existsSync(profileManifestOf(options))) {
507
+ runDsh(options, ['--profile', options.profile, '--dump-config'], { capture: true })
508
+ if (!existsSync(profileManifestOf(options))) {
509
+ throw new Error(`DSH did not initialize profile ${options.profile} before plugin install.`)
510
+ }
511
+ }
512
+
513
+ // pnpm resolves the profile's existing direct file dependency before it can
514
+ // replace it. Repair only our own missing cache path with the new snapshot;
515
+ // after the add succeeds the obsolete recovery copy becomes collectible.
516
+ repairMissingCurrentProfileArchive(options, archivePath)
517
+ runDsh(options, ['plugin', '--profile', options.profile, 'add', archivePath])
518
+ const referencedArchive = profileArchiveReference(options)
519
+ if (comparablePath(referencedArchive) !== comparablePath(archivePath)) {
520
+ throw new Error(`DSH profile ${options.profile} still references ${referencedArchive}; preserving old and new archives for recovery.`)
521
+ }
522
+ const dump = runDsh(options, ['--profile', options.profile, '--dump-config'], { capture: true }).stdout
523
+ if (!dump.includes('dsh-git-worktree')) {
524
+ throw new Error(`Profile ${options.profile} installed the tarball but did not compose dsh-git-worktree.`)
525
+ }
526
+ cleanSupersededArchives(options.cacheRoot, archivePath, options)
527
+ return { archivePath, dumpConfig: dump }
528
+ }
529
+
530
+ export function removeLocalSnapshot(options) {
531
+ assertDshRuntimeAvailable(options)
532
+ runDsh(options, ['plugin', '--profile', options.profile, 'remove', 'dsh-git-worktree'])
533
+ if (existsSync(options.cacheRoot)) {
534
+ for (const entry of readdirSync(options.cacheRoot)) {
535
+ if (/^dsh-git-worktree-\d+\.tgz$/.test(entry)) rmSync(join(options.cacheRoot, entry), { force: true })
536
+ }
537
+ }
538
+ }
539
+
540
+ export function smokeLocalSnapshot(options) {
541
+ assertDshRuntimeAvailable(options)
542
+ const dump = runDsh(options, ['--profile', options.profile, '--dump-config'], { capture: true }).stdout
543
+ if (!dump.includes('dsh-git-worktree')) {
544
+ throw new Error(`Profile ${options.profile} does not compose dsh-git-worktree.`)
545
+ }
546
+ return dump
547
+ }