dsh-git-worktree 0.1.2 → 0.2.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -21
  3. package/README.md +115 -43
  4. package/README.zh.md +115 -41
  5. package/cordis.patch.yml +7 -7
  6. package/lib/adapters/registry.js +40 -0
  7. package/lib/adapters/registry.js.map +1 -1
  8. package/lib/adapters/safe-file.js +34 -4
  9. package/lib/adapters/safe-file.js.map +1 -1
  10. package/lib/client/WorktreeCreateRow.d.ts +7 -0
  11. package/lib/client/WorktreeCreateRow.js +26 -0
  12. package/lib/client/WorktreeCreateRow.js.map +1 -0
  13. package/lib/client/WorktreeReviewRow.d.ts +11 -0
  14. package/lib/client/WorktreeReviewRow.js +67 -0
  15. package/lib/client/WorktreeReviewRow.js.map +1 -0
  16. package/lib/client/actions.d.ts +86 -0
  17. package/lib/client/actions.js +35 -0
  18. package/lib/client/actions.js.map +1 -0
  19. package/lib/client/console-remote/adapter.d.ts +3 -0
  20. package/lib/client/console-remote/adapter.js +59 -0
  21. package/lib/client/console-remote/adapter.js.map +1 -0
  22. package/lib/client/console-remote/index.d.ts +18 -0
  23. package/lib/client/console-remote/index.js +17 -0
  24. package/lib/client/console-remote/index.js.map +1 -0
  25. package/lib/client/index.d.ts +17 -0
  26. package/lib/client/index.js +44 -0
  27. package/lib/client/index.js.map +1 -0
  28. package/lib/client/model.d.ts +68 -0
  29. package/lib/client/model.js +127 -0
  30. package/lib/client/model.js.map +1 -0
  31. package/lib/client/pre-session/PreSessionWorktreeToggle.d.ts +16 -0
  32. package/lib/client/pre-session/PreSessionWorktreeToggle.js +128 -0
  33. package/lib/client/pre-session/PreSessionWorktreeToggle.js.map +1 -0
  34. package/lib/client/pre-session/controller.d.ts +42 -0
  35. package/lib/client/pre-session/controller.js +149 -0
  36. package/lib/client/pre-session/controller.js.map +1 -0
  37. package/lib/client/pre-session/index.d.ts +15 -0
  38. package/lib/client/pre-session/index.js +16 -0
  39. package/lib/client/pre-session/index.js.map +1 -0
  40. package/lib/client/review-console/ReviewActions.d.ts +15 -0
  41. package/lib/client/review-console/ReviewActions.js +219 -0
  42. package/lib/client/review-console/ReviewActions.js.map +1 -0
  43. package/lib/client/review-console/WorktreeReviewPanel.d.ts +20 -0
  44. package/lib/client/review-console/WorktreeReviewPanel.js +75 -0
  45. package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -0
  46. package/lib/client/review-console/index.d.ts +8 -0
  47. package/lib/client/review-console/index.js +22 -0
  48. package/lib/client/review-console/index.js.map +1 -0
  49. package/lib/client/review-console/review-console.styles.d.ts +1 -0
  50. package/lib/client/review-console/review-console.styles.js +81 -0
  51. package/lib/client/review-console/review-console.styles.js.map +1 -0
  52. package/lib/client/review-console/status-events.d.ts +3 -0
  53. package/lib/client/review-console/status-events.js +6 -0
  54. package/lib/client/review-console/status-events.js.map +1 -0
  55. package/lib/client/styles.d.ts +1 -0
  56. package/lib/client/styles.js +78 -0
  57. package/lib/client/styles.js.map +1 -0
  58. package/lib/client/target-console/TargetStatusAction.d.ts +7 -0
  59. package/lib/client/target-console/TargetStatusAction.js +46 -0
  60. package/lib/client/target-console/TargetStatusAction.js.map +1 -0
  61. package/lib/client/target-console/WorktreeConsoleView.d.ts +9 -0
  62. package/lib/client/target-console/WorktreeConsoleView.js +362 -0
  63. package/lib/client/target-console/WorktreeConsoleView.js.map +1 -0
  64. package/lib/client/target-console/WorktreeReviewStatus.d.ts +9 -0
  65. package/lib/client/target-console/WorktreeReviewStatus.js +91 -0
  66. package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -0
  67. package/lib/client/target-console/index.d.ts +14 -0
  68. package/lib/client/target-console/index.js +23 -0
  69. package/lib/client/target-console/index.js.map +1 -0
  70. package/lib/client/target-console/target-console.styles.d.ts +1 -0
  71. package/lib/client/target-console/target-console.styles.js +130 -0
  72. package/lib/client/target-console/target-console.styles.js.map +1 -0
  73. package/lib/client.js +6813 -0
  74. package/lib/commands.d.ts +2 -4
  75. package/lib/commands.js +54 -28
  76. package/lib/commands.js.map +1 -1
  77. package/lib/console-contract.d.ts +236 -0
  78. package/lib/console-contract.js +86 -0
  79. package/lib/console-contract.js.map +1 -0
  80. package/lib/console-host/control-plane.d.ts +29 -0
  81. package/lib/console-host/control-plane.js +443 -0
  82. package/lib/console-host/control-plane.js.map +1 -0
  83. package/lib/console-host/errors.d.ts +6 -0
  84. package/lib/console-host/errors.js +31 -0
  85. package/lib/console-host/errors.js.map +1 -0
  86. package/lib/console-host/projection.d.ts +12 -0
  87. package/lib/console-host/projection.js +131 -0
  88. package/lib/console-host/projection.js.map +1 -0
  89. package/lib/console-host/review-diff.d.ts +18 -0
  90. package/lib/console-host/review-diff.js +176 -0
  91. package/lib/console-host/review-diff.js.map +1 -0
  92. package/lib/console-host/service.d.ts +24 -0
  93. package/lib/console-host/service.js +131 -0
  94. package/lib/console-host/service.js.map +1 -0
  95. package/lib/console-remote/descriptors.d.ts +3 -0
  96. package/lib/console-remote/descriptors.js +96 -0
  97. package/lib/console-remote/descriptors.js.map +1 -0
  98. package/lib/console-remote/remote.d.ts +27 -0
  99. package/lib/console-remote/remote.js +4 -0
  100. package/lib/console-remote/remote.js.map +1 -0
  101. package/lib/console-remote/schemas.d.ts +29 -0
  102. package/lib/console-remote/schemas.js +143 -0
  103. package/lib/console-remote/schemas.js.map +1 -0
  104. package/lib/console-remote/typert.d.ts +3 -0
  105. package/lib/console-remote/typert.js +14 -0
  106. package/lib/console-remote/typert.js.map +1 -0
  107. package/lib/index.d.ts +22 -6
  108. package/lib/index.js +28 -10
  109. package/lib/index.js.map +1 -1
  110. package/lib/managed-worktree-path.d.ts +14 -13
  111. package/lib/managed-worktree-path.js +32 -22
  112. package/lib/managed-worktree-path.js.map +1 -1
  113. package/lib/ports.d.ts +37 -1
  114. package/lib/session-checkout-apply.d.ts +54 -1
  115. package/lib/session-checkout-apply.js +318 -28
  116. package/lib/session-checkout-apply.js.map +1 -1
  117. package/lib/session-checkout-module.js +853 -33
  118. package/lib/session-checkout-module.js.map +1 -1
  119. package/lib/session-target-context.d.ts +6 -0
  120. package/lib/session-target-context.js +16 -0
  121. package/lib/session-target-context.js.map +1 -0
  122. package/lib/tools.d.ts +5 -6
  123. package/lib/tools.js +43 -233
  124. package/lib/tools.js.map +1 -1
  125. package/lib/types.d.ts +57 -11
  126. package/lib/types.js +6 -6
  127. package/lib/types.js.map +1 -1
  128. package/package.json +116 -69
  129. package/scripts/check-publish.mjs +193 -0
  130. package/scripts/clean-lib.mjs +7 -0
  131. package/scripts/dev-dsh-lib.mjs +453 -0
  132. package/scripts/dev-dsh.mjs +89 -0
  133. package/scripts/dsh-source-runner.mjs +14 -0
@@ -0,0 +1,453 @@
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
+ export function discoverHarnessRoot(projectRoot, environment = process.env) {
203
+ if (environment.DSH_HARNESS_ROOT) {
204
+ const configured = resolve(environment.DSH_HARNESS_ROOT)
205
+ if (!isHarnessRoot(configured)) {
206
+ throw new Error(`DSH_HARNESS_ROOT does not point to a DeepSeek Harness checkout: ${configured}`)
207
+ }
208
+ return configured
209
+ }
210
+ try {
211
+ const commonDir = git(projectRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'])
212
+ const localProjectRoot = dirname(resolve(projectRoot, commonDir))
213
+ const candidate = join(dirname(localProjectRoot), 'DeepSeek', 'deepseek-harness')
214
+ return isHarnessRoot(candidate) ? candidate : undefined
215
+ } catch {
216
+ return undefined
217
+ }
218
+ }
219
+
220
+ export function executable(name, platform = process.platform) {
221
+ return platform === 'win32' ? `${name}.cmd` : name
222
+ }
223
+
224
+ export function createDshLaunch(options) {
225
+ if (!options.harnessRoot) {
226
+ return {
227
+ command: executable('dsh', options.platform),
228
+ args: ['--profile', options.profile, '--port', String(options.port)],
229
+ cwd: options.workspaceRoot,
230
+ source: false,
231
+ }
232
+ }
233
+ if (!isHarnessRoot(options.harnessRoot)) {
234
+ throw new Error(`Harness source checkout is invalid: ${options.harnessRoot}`)
235
+ }
236
+ return {
237
+ command: executable('pnpm', options.platform),
238
+ args: [
239
+ '--dir', options.harnessRoot,
240
+ 'exec', 'node', '--import', 'tsx/esm',
241
+ join(options.projectRoot, 'scripts', 'dsh-source-runner.mjs'),
242
+ options.workspaceRoot,
243
+ join(options.harnessRoot, 'apps', 'cli', 'src', 'bin.ts'),
244
+ '--profile', options.profile,
245
+ '--port', String(options.port),
246
+ ],
247
+ cwd: options.projectRoot,
248
+ source: true,
249
+ }
250
+ }
251
+
252
+ function assertWindowsCommandSafe(command, args) {
253
+ for (const value of [command, ...args]) {
254
+ if (/[\0\r\n"&|<>^%!]/.test(value)) {
255
+ throw new Error(`Windows command argument contains unsupported shell metacharacters: ${value}`)
256
+ }
257
+ }
258
+ }
259
+
260
+ export function runProcess(command, args, options = {}) {
261
+ const capture = options.capture === true
262
+ const windows = process.platform === 'win32'
263
+ if (windows) assertWindowsCommandSafe(command, args)
264
+ const actualCommand = windows ? process.env.ComSpec || 'cmd.exe' : command
265
+ const actualArgs = windows
266
+ ? ['/d', '/v:off', '/c', command, ...args]
267
+ : args
268
+ const result = spawnSync(actualCommand, actualArgs, {
269
+ cwd: options.cwd,
270
+ env: options.env,
271
+ encoding: 'utf8',
272
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
273
+ })
274
+ if (result.error) throw result.error
275
+ if (result.status !== 0) {
276
+ const detail = capture ? `\n${result.stderr || result.stdout || ''}` : ''
277
+ throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}.${detail}`)
278
+ }
279
+ return { stdout: capture ? result.stdout ?? '' : '' }
280
+ }
281
+
282
+ function copyPackageEntry(source, destination) {
283
+ const info = lstatSync(source)
284
+ if (info.isSymbolicLink()) throw new Error(`Refusing to package symbolic link ${source}.`)
285
+ if (info.isDirectory()) {
286
+ mkdirSync(destination, { recursive: true })
287
+ for (const entry of readdirSync(source)) {
288
+ copyPackageEntry(join(source, entry), join(destination, entry))
289
+ }
290
+ return
291
+ }
292
+ if (!info.isFile()) throw new Error(`Unsupported package entry ${source}.`)
293
+ mkdirSync(resolve(destination, '..'), { recursive: true })
294
+ copyFileSync(source, destination)
295
+ }
296
+
297
+ function dshHomeOf(options) {
298
+ return options.environment?.DSH_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')
299
+ }
300
+
301
+ function profileManifestOf(options) {
302
+ return options.profileManifestPath ?? join(dshHomeOf(options), 'profiles', options.profile, 'package.json')
303
+ }
304
+
305
+ function archiveReferenceFromManifest(manifestPath, required = false) {
306
+ if (!existsSync(manifestPath)) {
307
+ if (required) throw new Error(`DSH profile manifest is missing: ${manifestPath}`)
308
+ return undefined
309
+ }
310
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
311
+ const specifier = manifest.dependencies?.['dsh-git-worktree']
312
+ if (typeof specifier !== 'string' || !specifier.startsWith('file:')) {
313
+ if (required) throw new Error('DSH profile did not record dsh-git-worktree as a local file dependency.')
314
+ return undefined
315
+ }
316
+ let filePath
317
+ try {
318
+ filePath = decodeURIComponent(specifier.slice('file:'.length))
319
+ } catch {
320
+ if (required) throw new Error(`DSH profile recorded an invalid local file dependency: ${specifier}`)
321
+ return undefined
322
+ }
323
+ return resolve(dirname(manifestPath), filePath)
324
+ }
325
+
326
+ function profileArchiveReference(options) {
327
+ return archiveReferenceFromManifest(profileManifestOf(options), true)
328
+ }
329
+
330
+ function comparablePath(path) {
331
+ const normalized = resolve(path).replaceAll('\\', '/')
332
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized
333
+ }
334
+
335
+ function referencedProfileArchives(options) {
336
+ const references = new Set()
337
+ const addManifest = (manifestPath) => {
338
+ try {
339
+ const reference = archiveReferenceFromManifest(manifestPath)
340
+ if (reference !== undefined) references.add(comparablePath(reference))
341
+ } catch {
342
+ // A malformed unrelated profile must not make local snapshot cleanup delete more files.
343
+ }
344
+ }
345
+ addManifest(profileManifestOf(options))
346
+ const profilesRoot = join(dshHomeOf(options), 'profiles')
347
+ if (existsSync(profilesRoot)) {
348
+ for (const entry of readdirSync(profilesRoot)) addManifest(join(profilesRoot, entry, 'package.json'))
349
+ }
350
+ return references
351
+ }
352
+
353
+ function cleanSupersededArchives(cacheRoot, keepPath, options) {
354
+ const preserved = referencedProfileArchives(options)
355
+ preserved.add(comparablePath(keepPath))
356
+ for (const entry of readdirSync(cacheRoot)) {
357
+ if (!/^dsh-git-worktree-\d+\.tgz$/.test(entry)) continue
358
+ const candidate = join(cacheRoot, entry)
359
+ if (!preserved.has(comparablePath(candidate))) rmSync(candidate, { force: true })
360
+ }
361
+ }
362
+
363
+ function repairMissingCurrentProfileArchive(options, archivePath) {
364
+ const previous = archiveReferenceFromManifest(profileManifestOf(options))
365
+ if (previous === undefined || existsSync(previous)) return
366
+ const managedCache = comparablePath(dirname(previous)) === comparablePath(options.cacheRoot)
367
+ && /^dsh-git-worktree-\d+\.tgz$/.test(previous.split(/[\\/]/u).at(-1) ?? '')
368
+ if (!managedCache) {
369
+ throw new Error(`DSH profile references missing local archive outside the managed cache: ${previous}`)
370
+ }
371
+ copyFileSync(archivePath, previous)
372
+ }
373
+
374
+ /** Build, pack and install only the current local snapshot. No registry publish occurs. */
375
+ export function installLocalSnapshot(options) {
376
+ const run = options.run ?? runProcess
377
+ const pnpm = executable('pnpm', options.platform)
378
+ const dsh = executable('dsh', options.platform)
379
+ mkdirSync(options.cacheRoot, { recursive: true })
380
+
381
+ if (!existsSync(join(options.projectRoot, 'node_modules'))) {
382
+ run(pnpm, ['install', '--frozen-lockfile'], { cwd: options.projectRoot })
383
+ }
384
+ run(pnpm, ['run', 'typecheck'], { cwd: options.projectRoot })
385
+ run(pnpm, ['run', 'build'], { cwd: options.projectRoot })
386
+
387
+ const snapshotId = options.now?.() ?? Date.now()
388
+ const archivePath = join(options.cacheRoot, `dsh-git-worktree-${snapshotId}.tgz`)
389
+ const stagingRoot = join(options.cacheRoot, `.staging-${snapshotId}`)
390
+ for (const entry of readdirSync(options.cacheRoot)) {
391
+ if (/^\.staging-\d+$/.test(entry)) rmSync(join(options.cacheRoot, entry), { recursive: true, force: true })
392
+ }
393
+ mkdirSync(stagingRoot, { recursive: true })
394
+ try {
395
+ const manifest = JSON.parse(readFileSync(join(options.projectRoot, 'package.json'), 'utf8'))
396
+ const stagedManifest = {
397
+ ...manifest,
398
+ version: `${manifest.version}-dev.${snapshotId}`,
399
+ scripts: {},
400
+ }
401
+ writeFileSync(join(stagingRoot, 'package.json'), `${JSON.stringify(stagedManifest, null, 2)}\n`)
402
+ for (const entry of ['lib', 'cordis.patch.yml', 'README.md', 'README.zh.md', 'LICENSE']) {
403
+ const source = join(options.projectRoot, entry)
404
+ if (existsSync(source)) copyPackageEntry(source, join(stagingRoot, entry))
405
+ }
406
+ run(pnpm, ['pack', '--out', archivePath, '--json'], { cwd: stagingRoot })
407
+ } finally {
408
+ rmSync(stagingRoot, { recursive: true, force: true })
409
+ }
410
+ if (!existsSync(archivePath)) throw new Error(`pnpm pack did not create ${archivePath}.`)
411
+
412
+ // pnpm resolves the profile's existing direct file dependency before it can
413
+ // replace it. Repair only our own missing cache path with the new snapshot;
414
+ // after the add succeeds the obsolete recovery copy becomes collectible.
415
+ repairMissingCurrentProfileArchive(options, archivePath)
416
+ run(dsh, ['plugin', '--profile', options.profile, 'add', archivePath], { cwd: options.projectRoot })
417
+ const referencedArchive = profileArchiveReference(options)
418
+ if (comparablePath(referencedArchive) !== comparablePath(archivePath)) {
419
+ throw new Error(`DSH profile ${options.profile} still references ${referencedArchive}; preserving old and new archives for recovery.`)
420
+ }
421
+ const dump = run(dsh, ['--profile', options.profile, '--dump-config'], {
422
+ cwd: options.projectRoot,
423
+ capture: true,
424
+ }).stdout
425
+ if (!dump.includes('dsh-git-worktree')) {
426
+ throw new Error(`Profile ${options.profile} installed the tarball but did not compose dsh-git-worktree.`)
427
+ }
428
+ cleanSupersededArchives(options.cacheRoot, archivePath, options)
429
+ return { archivePath, dumpConfig: dump }
430
+ }
431
+
432
+ export function removeLocalSnapshot(options) {
433
+ const run = options.run ?? runProcess
434
+ run(executable('dsh', options.platform), [
435
+ 'plugin', '--profile', options.profile, 'remove', 'dsh-git-worktree',
436
+ ], { cwd: options.projectRoot })
437
+ if (existsSync(options.cacheRoot)) {
438
+ for (const entry of readdirSync(options.cacheRoot)) {
439
+ if (/^dsh-git-worktree-\d+\.tgz$/.test(entry)) rmSync(join(options.cacheRoot, entry), { force: true })
440
+ }
441
+ }
442
+ }
443
+
444
+ export function smokeLocalSnapshot(options) {
445
+ const run = options.run ?? runProcess
446
+ const dump = run(executable('dsh', options.platform), [
447
+ '--profile', options.profile, '--dump-config',
448
+ ], { cwd: options.projectRoot, capture: true }).stdout
449
+ if (!dump.includes('dsh-git-worktree')) {
450
+ throw new Error(`Profile ${options.profile} does not compose dsh-git-worktree.`)
451
+ }
452
+ return dump
453
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import {
7
+ createDefaultOptions,
8
+ createDshLaunch,
9
+ discoverHarnessRoot,
10
+ ensureDevFixture,
11
+ installLocalSnapshot,
12
+ parseDevDshArgs,
13
+ removeLocalSnapshot,
14
+ runProcess,
15
+ smokeLocalSnapshot,
16
+ } from './dev-dsh-lib.mjs'
17
+
18
+ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
19
+ const cacheRoot = join(tmpdir(), 'dsh-git-worktree-dev')
20
+ const defaults = {
21
+ ...createDefaultOptions(projectRoot, cacheRoot),
22
+ harnessRoot: discoverHarnessRoot(projectRoot),
23
+ }
24
+
25
+ function printUsage() {
26
+ console.log(`dsh-git-worktree local development
27
+
28
+ Usage:
29
+ pnpm run dev:dsh -- [--profile web] [--repo <git-root>] [--port 3081] [--harness <path>]
30
+ pnpm run dev:dsh:install -- [--profile web]
31
+ pnpm run dev:dsh:smoke -- [--profile web]
32
+ pnpm run dev:dsh:remove -- [--profile web]
33
+
34
+ The install path is a local tarball under the OS temporary directory. Nothing is
35
+ published to npm or pushed to Git. A sibling Harness source checkout is used
36
+ when available; override with DSH_HARNESS_ROOT or --harness. Without --repo, a
37
+ marker-protected disposable Git repository is created at:
38
+ ${defaults.repo}
39
+ `)
40
+ }
41
+
42
+ try {
43
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
44
+ printUsage()
45
+ process.exitCode = 0
46
+ } else {
47
+ const options = parseDevDshArgs(process.argv.slice(2), defaults)
48
+ if (options.mode === 'install') {
49
+ const installed = installLocalSnapshot({
50
+ projectRoot,
51
+ profile: options.profile,
52
+ cacheRoot,
53
+ })
54
+ console.log(`\nLocal snapshot installed in profile "${options.profile}".`)
55
+ console.log(`Archive retained for profile reproducibility: ${installed.archivePath}`)
56
+ } else if (options.mode === 'smoke') {
57
+ smokeLocalSnapshot({ projectRoot, profile: options.profile })
58
+ console.log(`Profile "${options.profile}" composes dsh-git-worktree.`)
59
+ } else if (options.mode === 'remove') {
60
+ removeLocalSnapshot({ projectRoot, profile: options.profile, cacheRoot })
61
+ console.log(`Removed dsh-git-worktree from profile "${options.profile}" and cleared local archives.`)
62
+ } else {
63
+ const installed = installLocalSnapshot({
64
+ projectRoot,
65
+ profile: options.profile,
66
+ cacheRoot,
67
+ })
68
+ const fixture = ensureDevFixture(options.repo, { explicit: options.repoExplicit })
69
+ console.log(`\nLocal snapshot: ${installed.archivePath}`)
70
+ console.log(`${fixture.created ? 'Created' : 'Using'} test repository: ${fixture.path}`)
71
+ const launch = createDshLaunch({
72
+ projectRoot,
73
+ harnessRoot: options.harnessRoot,
74
+ workspaceRoot: fixture.path,
75
+ profile: options.profile,
76
+ port: options.port,
77
+ })
78
+ console.log(`Starting DSH at http://127.0.0.1:${options.port}`)
79
+ console.log(launch.source
80
+ ? `Using Harness source checkout: ${options.harnessRoot}`
81
+ : 'Using the installed dsh executable.')
82
+ console.log('Press Ctrl+C to stop. No npm/GitHub publication is performed.\n')
83
+ runProcess(launch.command, launch.args, { cwd: launch.cwd })
84
+ }
85
+ }
86
+ } catch (error) {
87
+ console.error(`dev:dsh failed: ${error instanceof Error ? error.message : String(error)}`)
88
+ process.exitCode = 1
89
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ const [workspaceRoot, cliEntry, ...args] = process.argv.slice(2)
6
+ if (!workspaceRoot || !cliEntry) {
7
+ throw new Error('dsh-source-runner requires <workspaceRoot> <cliEntry> [...args].')
8
+ }
9
+
10
+ // pnpm resolves tsx and the Harness workspace before this point. Switch only
11
+ // the application cwd so newly created DSH Sessions use the intended fixture.
12
+ process.chdir(workspaceRoot)
13
+ process.argv = [process.execPath, cliEntry, ...args]
14
+ await import(pathToFileURL(cliEntry).href)