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.
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -21
- package/README.md +115 -43
- package/README.zh.md +115 -41
- package/cordis.patch.yml +7 -7
- package/lib/adapters/registry.js +40 -0
- package/lib/adapters/registry.js.map +1 -1
- package/lib/adapters/safe-file.js +34 -4
- package/lib/adapters/safe-file.js.map +1 -1
- package/lib/client/WorktreeCreateRow.d.ts +7 -0
- package/lib/client/WorktreeCreateRow.js +26 -0
- package/lib/client/WorktreeCreateRow.js.map +1 -0
- package/lib/client/WorktreeReviewRow.d.ts +11 -0
- package/lib/client/WorktreeReviewRow.js +67 -0
- package/lib/client/WorktreeReviewRow.js.map +1 -0
- package/lib/client/actions.d.ts +86 -0
- package/lib/client/actions.js +35 -0
- package/lib/client/actions.js.map +1 -0
- package/lib/client/console-remote/adapter.d.ts +3 -0
- package/lib/client/console-remote/adapter.js +59 -0
- package/lib/client/console-remote/adapter.js.map +1 -0
- package/lib/client/console-remote/index.d.ts +18 -0
- package/lib/client/console-remote/index.js +17 -0
- package/lib/client/console-remote/index.js.map +1 -0
- package/lib/client/index.d.ts +17 -0
- package/lib/client/index.js +44 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/model.d.ts +68 -0
- package/lib/client/model.js +127 -0
- package/lib/client/model.js.map +1 -0
- package/lib/client/pre-session/PreSessionWorktreeToggle.d.ts +16 -0
- package/lib/client/pre-session/PreSessionWorktreeToggle.js +128 -0
- package/lib/client/pre-session/PreSessionWorktreeToggle.js.map +1 -0
- package/lib/client/pre-session/controller.d.ts +42 -0
- package/lib/client/pre-session/controller.js +149 -0
- package/lib/client/pre-session/controller.js.map +1 -0
- package/lib/client/pre-session/index.d.ts +15 -0
- package/lib/client/pre-session/index.js +16 -0
- package/lib/client/pre-session/index.js.map +1 -0
- package/lib/client/review-console/ReviewActions.d.ts +15 -0
- package/lib/client/review-console/ReviewActions.js +219 -0
- package/lib/client/review-console/ReviewActions.js.map +1 -0
- package/lib/client/review-console/WorktreeReviewPanel.d.ts +20 -0
- package/lib/client/review-console/WorktreeReviewPanel.js +75 -0
- package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -0
- package/lib/client/review-console/index.d.ts +8 -0
- package/lib/client/review-console/index.js +22 -0
- package/lib/client/review-console/index.js.map +1 -0
- package/lib/client/review-console/review-console.styles.d.ts +1 -0
- package/lib/client/review-console/review-console.styles.js +81 -0
- package/lib/client/review-console/review-console.styles.js.map +1 -0
- package/lib/client/review-console/status-events.d.ts +3 -0
- package/lib/client/review-console/status-events.js +6 -0
- package/lib/client/review-console/status-events.js.map +1 -0
- package/lib/client/styles.d.ts +1 -0
- package/lib/client/styles.js +78 -0
- package/lib/client/styles.js.map +1 -0
- package/lib/client/target-console/TargetStatusAction.d.ts +7 -0
- package/lib/client/target-console/TargetStatusAction.js +46 -0
- package/lib/client/target-console/TargetStatusAction.js.map +1 -0
- package/lib/client/target-console/WorktreeConsoleView.d.ts +9 -0
- package/lib/client/target-console/WorktreeConsoleView.js +362 -0
- package/lib/client/target-console/WorktreeConsoleView.js.map +1 -0
- package/lib/client/target-console/WorktreeReviewStatus.d.ts +9 -0
- package/lib/client/target-console/WorktreeReviewStatus.js +91 -0
- package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -0
- package/lib/client/target-console/index.d.ts +14 -0
- package/lib/client/target-console/index.js +23 -0
- package/lib/client/target-console/index.js.map +1 -0
- package/lib/client/target-console/target-console.styles.d.ts +1 -0
- package/lib/client/target-console/target-console.styles.js +130 -0
- package/lib/client/target-console/target-console.styles.js.map +1 -0
- package/lib/client.js +6813 -0
- package/lib/commands.d.ts +2 -4
- package/lib/commands.js +54 -28
- package/lib/commands.js.map +1 -1
- package/lib/console-contract.d.ts +236 -0
- package/lib/console-contract.js +86 -0
- package/lib/console-contract.js.map +1 -0
- package/lib/console-host/control-plane.d.ts +29 -0
- package/lib/console-host/control-plane.js +443 -0
- package/lib/console-host/control-plane.js.map +1 -0
- package/lib/console-host/errors.d.ts +6 -0
- package/lib/console-host/errors.js +31 -0
- package/lib/console-host/errors.js.map +1 -0
- package/lib/console-host/projection.d.ts +12 -0
- package/lib/console-host/projection.js +131 -0
- package/lib/console-host/projection.js.map +1 -0
- package/lib/console-host/review-diff.d.ts +18 -0
- package/lib/console-host/review-diff.js +176 -0
- package/lib/console-host/review-diff.js.map +1 -0
- package/lib/console-host/service.d.ts +24 -0
- package/lib/console-host/service.js +131 -0
- package/lib/console-host/service.js.map +1 -0
- package/lib/console-remote/descriptors.d.ts +3 -0
- package/lib/console-remote/descriptors.js +96 -0
- package/lib/console-remote/descriptors.js.map +1 -0
- package/lib/console-remote/remote.d.ts +27 -0
- package/lib/console-remote/remote.js +4 -0
- package/lib/console-remote/remote.js.map +1 -0
- package/lib/console-remote/schemas.d.ts +29 -0
- package/lib/console-remote/schemas.js +143 -0
- package/lib/console-remote/schemas.js.map +1 -0
- package/lib/console-remote/typert.d.ts +3 -0
- package/lib/console-remote/typert.js +14 -0
- package/lib/console-remote/typert.js.map +1 -0
- package/lib/index.d.ts +22 -6
- package/lib/index.js +28 -10
- package/lib/index.js.map +1 -1
- package/lib/managed-worktree-path.d.ts +14 -13
- package/lib/managed-worktree-path.js +32 -22
- package/lib/managed-worktree-path.js.map +1 -1
- package/lib/ports.d.ts +37 -1
- package/lib/session-checkout-apply.d.ts +54 -1
- package/lib/session-checkout-apply.js +318 -28
- package/lib/session-checkout-apply.js.map +1 -1
- package/lib/session-checkout-module.js +853 -33
- package/lib/session-checkout-module.js.map +1 -1
- package/lib/session-target-context.d.ts +6 -0
- package/lib/session-target-context.js +16 -0
- package/lib/session-target-context.js.map +1 -0
- package/lib/tools.d.ts +5 -6
- package/lib/tools.js +43 -233
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +57 -11
- package/lib/types.js +6 -6
- package/lib/types.js.map +1 -1
- package/package.json +116 -69
- package/scripts/check-publish.mjs +193 -0
- package/scripts/clean-lib.mjs +7 -0
- package/scripts/dev-dsh-lib.mjs +453 -0
- package/scripts/dev-dsh.mjs +89 -0
- 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)
|