dsh-git-worktree 0.1.2 → 0.3.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 (136) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +21 -21
  3. package/README.md +187 -43
  4. package/README.zh.md +187 -41
  5. package/cordis.patch.yml +7 -7
  6. package/lib/adapters/lookup.d.ts +4 -2
  7. package/lib/adapters/lookup.js +31 -3
  8. package/lib/adapters/lookup.js.map +1 -1
  9. package/lib/adapters/registry.js +41 -0
  10. package/lib/adapters/registry.js.map +1 -1
  11. package/lib/adapters/safe-file.js +34 -4
  12. package/lib/adapters/safe-file.js.map +1 -1
  13. package/lib/client/WorktreeCreateRow.d.ts +7 -0
  14. package/lib/client/WorktreeCreateRow.js +26 -0
  15. package/lib/client/WorktreeCreateRow.js.map +1 -0
  16. package/lib/client/WorktreeReviewRow.d.ts +11 -0
  17. package/lib/client/WorktreeReviewRow.js +67 -0
  18. package/lib/client/WorktreeReviewRow.js.map +1 -0
  19. package/lib/client/actions.d.ts +86 -0
  20. package/lib/client/actions.js +35 -0
  21. package/lib/client/actions.js.map +1 -0
  22. package/lib/client/console-remote/adapter.d.ts +3 -0
  23. package/lib/client/console-remote/adapter.js +61 -0
  24. package/lib/client/console-remote/adapter.js.map +1 -0
  25. package/lib/client/console-remote/index.d.ts +18 -0
  26. package/lib/client/console-remote/index.js +17 -0
  27. package/lib/client/console-remote/index.js.map +1 -0
  28. package/lib/client/index.d.ts +17 -0
  29. package/lib/client/index.js +44 -0
  30. package/lib/client/index.js.map +1 -0
  31. package/lib/client/model.d.ts +68 -0
  32. package/lib/client/model.js +127 -0
  33. package/lib/client/model.js.map +1 -0
  34. package/lib/client/pre-session/PreSessionWorktreeToggle.d.ts +16 -0
  35. package/lib/client/pre-session/PreSessionWorktreeToggle.js +128 -0
  36. package/lib/client/pre-session/PreSessionWorktreeToggle.js.map +1 -0
  37. package/lib/client/pre-session/controller.d.ts +42 -0
  38. package/lib/client/pre-session/controller.js +149 -0
  39. package/lib/client/pre-session/controller.js.map +1 -0
  40. package/lib/client/pre-session/index.d.ts +15 -0
  41. package/lib/client/pre-session/index.js +16 -0
  42. package/lib/client/pre-session/index.js.map +1 -0
  43. package/lib/client/review-console/ReviewActions.d.ts +15 -0
  44. package/lib/client/review-console/ReviewActions.js +234 -0
  45. package/lib/client/review-console/ReviewActions.js.map +1 -0
  46. package/lib/client/review-console/WorktreeReviewPanel.d.ts +20 -0
  47. package/lib/client/review-console/WorktreeReviewPanel.js +75 -0
  48. package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -0
  49. package/lib/client/review-console/index.d.ts +8 -0
  50. package/lib/client/review-console/index.js +22 -0
  51. package/lib/client/review-console/index.js.map +1 -0
  52. package/lib/client/review-console/review-console.styles.d.ts +1 -0
  53. package/lib/client/review-console/review-console.styles.js +81 -0
  54. package/lib/client/review-console/review-console.styles.js.map +1 -0
  55. package/lib/client/review-console/status-events.d.ts +3 -0
  56. package/lib/client/review-console/status-events.js +6 -0
  57. package/lib/client/review-console/status-events.js.map +1 -0
  58. package/lib/client/styles.d.ts +1 -0
  59. package/lib/client/styles.js +78 -0
  60. package/lib/client/styles.js.map +1 -0
  61. package/lib/client/target-console/TargetStatusAction.d.ts +7 -0
  62. package/lib/client/target-console/TargetStatusAction.js +46 -0
  63. package/lib/client/target-console/TargetStatusAction.js.map +1 -0
  64. package/lib/client/target-console/WorktreeConsoleView.d.ts +9 -0
  65. package/lib/client/target-console/WorktreeConsoleView.js +362 -0
  66. package/lib/client/target-console/WorktreeConsoleView.js.map +1 -0
  67. package/lib/client/target-console/WorktreeReviewStatus.d.ts +9 -0
  68. package/lib/client/target-console/WorktreeReviewStatus.js +125 -0
  69. package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -0
  70. package/lib/client/target-console/index.d.ts +14 -0
  71. package/lib/client/target-console/index.js +23 -0
  72. package/lib/client/target-console/index.js.map +1 -0
  73. package/lib/client/target-console/target-console.styles.d.ts +1 -0
  74. package/lib/client/target-console/target-console.styles.js +130 -0
  75. package/lib/client/target-console/target-console.styles.js.map +1 -0
  76. package/lib/client.js +6902 -0
  77. package/lib/commands.d.ts +2 -4
  78. package/lib/commands.js +69 -28
  79. package/lib/commands.js.map +1 -1
  80. package/lib/console-contract.d.ts +253 -0
  81. package/lib/console-contract.js +86 -0
  82. package/lib/console-contract.js.map +1 -0
  83. package/lib/console-host/control-plane.d.ts +31 -0
  84. package/lib/console-host/control-plane.js +502 -0
  85. package/lib/console-host/control-plane.js.map +1 -0
  86. package/lib/console-host/errors.d.ts +6 -0
  87. package/lib/console-host/errors.js +31 -0
  88. package/lib/console-host/errors.js.map +1 -0
  89. package/lib/console-host/projection.d.ts +12 -0
  90. package/lib/console-host/projection.js +136 -0
  91. package/lib/console-host/projection.js.map +1 -0
  92. package/lib/console-host/review-diff.d.ts +18 -0
  93. package/lib/console-host/review-diff.js +176 -0
  94. package/lib/console-host/review-diff.js.map +1 -0
  95. package/lib/console-host/service.d.ts +26 -0
  96. package/lib/console-host/service.js +143 -0
  97. package/lib/console-host/service.js.map +1 -0
  98. package/lib/console-remote/descriptors.d.ts +3 -0
  99. package/lib/console-remote/descriptors.js +105 -0
  100. package/lib/console-remote/descriptors.js.map +1 -0
  101. package/lib/console-remote/remote.d.ts +29 -0
  102. package/lib/console-remote/remote.js +4 -0
  103. package/lib/console-remote/remote.js.map +1 -0
  104. package/lib/console-remote/schemas.d.ts +29 -0
  105. package/lib/console-remote/schemas.js +145 -0
  106. package/lib/console-remote/schemas.js.map +1 -0
  107. package/lib/console-remote/typert.d.ts +3 -0
  108. package/lib/console-remote/typert.js +14 -0
  109. package/lib/console-remote/typert.js.map +1 -0
  110. package/lib/index.d.ts +26 -8
  111. package/lib/index.js +28 -10
  112. package/lib/index.js.map +1 -1
  113. package/lib/managed-worktree-path.d.ts +14 -13
  114. package/lib/managed-worktree-path.js +32 -22
  115. package/lib/managed-worktree-path.js.map +1 -1
  116. package/lib/ports.d.ts +39 -1
  117. package/lib/session-checkout-apply.d.ts +54 -1
  118. package/lib/session-checkout-apply.js +318 -28
  119. package/lib/session-checkout-apply.js.map +1 -1
  120. package/lib/session-checkout-module.js +1098 -36
  121. package/lib/session-checkout-module.js.map +1 -1
  122. package/lib/session-target-context.d.ts +6 -0
  123. package/lib/session-target-context.js +16 -0
  124. package/lib/session-target-context.js.map +1 -0
  125. package/lib/tools.d.ts +6 -6
  126. package/lib/tools.js +107 -218
  127. package/lib/tools.js.map +1 -1
  128. package/lib/types.d.ts +57 -11
  129. package/lib/types.js +6 -6
  130. package/lib/types.js.map +1 -1
  131. package/package.json +116 -69
  132. package/scripts/check-publish.mjs +193 -0
  133. package/scripts/clean-lib.mjs +7 -0
  134. package/scripts/dev-dsh-lib.mjs +509 -0
  135. package/scripts/dev-dsh.mjs +95 -0
  136. package/scripts/dsh-source-runner.mjs +14 -0
@@ -0,0 +1,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 } 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.zh.md', 'LICENSE']) {
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
+ }
@@ -0,0 +1,95 @@
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] [--harness <path>]
31
+ pnpm run dev:dsh:smoke -- [--profile web] [--harness <path>]
32
+ pnpm run dev:dsh:remove -- [--profile web] [--harness <path>]
33
+
34
+ The install path is a local tarball under the OS temporary directory. Nothing is
35
+ published to npm or pushed to Git. Nearby Harness source checkouts are scanned
36
+ and a candidate with node_modules/tsx is preferred; an arbitrary checkout can be
37
+ selected with DSH_HARNESS_ROOT or --harness. Every profile and launch command
38
+ then uses that source CLI instead of a globally installed dsh executable. An
39
+ explicit checkout without dependencies fails early with its pnpm install command.
40
+ Without --repo, a marker-protected disposable Git repository is created at:
41
+ ${defaults.repo}
42
+ `)
43
+ }
44
+
45
+ try {
46
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
47
+ printUsage()
48
+ process.exitCode = 0
49
+ } else {
50
+ const options = parseDevDshArgs(process.argv.slice(2), defaults)
51
+ const runtime = {
52
+ projectRoot,
53
+ profile: options.profile,
54
+ harnessRoot: options.harnessRoot,
55
+ }
56
+ if (options.mode === 'install') {
57
+ const installed = installLocalSnapshot({
58
+ ...runtime,
59
+ cacheRoot,
60
+ })
61
+ console.log(`\nLocal snapshot installed in profile "${options.profile}".`)
62
+ console.log(`Archive retained for profile reproducibility: ${installed.archivePath}`)
63
+ } else if (options.mode === 'smoke') {
64
+ smokeLocalSnapshot(runtime)
65
+ console.log(`Profile "${options.profile}" composes dsh-git-worktree.`)
66
+ } else if (options.mode === 'remove') {
67
+ removeLocalSnapshot({ ...runtime, cacheRoot })
68
+ console.log(`Removed dsh-git-worktree from profile "${options.profile}" and cleared local archives.`)
69
+ } else {
70
+ const installed = installLocalSnapshot({
71
+ ...runtime,
72
+ cacheRoot,
73
+ })
74
+ const fixture = ensureDevFixture(options.repo, { explicit: options.repoExplicit })
75
+ console.log(`\nLocal snapshot: ${installed.archivePath}`)
76
+ console.log(`${fixture.created ? 'Created' : 'Using'} test repository: ${fixture.path}`)
77
+ const launch = createDshLaunch({
78
+ projectRoot,
79
+ harnessRoot: options.harnessRoot,
80
+ workspaceRoot: fixture.path,
81
+ profile: options.profile,
82
+ port: options.port,
83
+ })
84
+ console.log(`Starting DSH at http://127.0.0.1:${options.port}`)
85
+ console.log(launch.source
86
+ ? `Using Harness source checkout: ${options.harnessRoot}`
87
+ : 'Using the installed dsh executable.')
88
+ console.log('Press Ctrl+C to stop. No npm/GitHub publication is performed.\n')
89
+ runProcess(launch.command, launch.args, { cwd: launch.cwd })
90
+ }
91
+ }
92
+ } catch (error) {
93
+ console.error(`dev:dsh failed: ${error instanceof Error ? error.message : String(error)}`)
94
+ process.exitCode = 1
95
+ }
@@ -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)