@speclife/core 0.1.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/dist/adapters/claude-cli-adapter.d.ts +57 -0
- package/dist/adapters/claude-cli-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-cli-adapter.js +161 -0
- package/dist/adapters/claude-cli-adapter.js.map +1 -0
- package/dist/adapters/claude-sdk-adapter.d.ts +49 -0
- package/dist/adapters/claude-sdk-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-sdk-adapter.js +278 -0
- package/dist/adapters/claude-sdk-adapter.js.map +1 -0
- package/dist/adapters/cursor-adapter.d.ts +26 -0
- package/dist/adapters/cursor-adapter.d.ts.map +1 -0
- package/dist/adapters/cursor-adapter.js +54 -0
- package/dist/adapters/cursor-adapter.js.map +1 -0
- package/dist/adapters/environment-adapter.d.ts +153 -0
- package/dist/adapters/environment-adapter.d.ts.map +1 -0
- package/dist/adapters/environment-adapter.js +690 -0
- package/dist/adapters/environment-adapter.js.map +1 -0
- package/dist/adapters/git-adapter.d.ts +41 -0
- package/dist/adapters/git-adapter.d.ts.map +1 -0
- package/dist/adapters/git-adapter.js +95 -0
- package/dist/adapters/git-adapter.js.map +1 -0
- package/dist/adapters/github-adapter.d.ts +39 -0
- package/dist/adapters/github-adapter.d.ts.map +1 -0
- package/dist/adapters/github-adapter.js +129 -0
- package/dist/adapters/github-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openspec-adapter.d.ts +36 -0
- package/dist/adapters/openspec-adapter.d.ts.map +1 -0
- package/dist/adapters/openspec-adapter.js +182 -0
- package/dist/adapters/openspec-adapter.js.map +1 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +112 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +28 -0
- package/dist/types.js.map +1 -0
- package/dist/workflows/implement.d.ts +28 -0
- package/dist/workflows/implement.d.ts.map +1 -0
- package/dist/workflows/implement.js +277 -0
- package/dist/workflows/implement.js.map +1 -0
- package/dist/workflows/index.d.ts +9 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +9 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/init.d.ts +55 -0
- package/dist/workflows/init.d.ts.map +1 -0
- package/dist/workflows/init.js +195 -0
- package/dist/workflows/init.js.map +1 -0
- package/dist/workflows/merge.d.ts +40 -0
- package/dist/workflows/merge.d.ts.map +1 -0
- package/dist/workflows/merge.js +90 -0
- package/dist/workflows/merge.js.map +1 -0
- package/dist/workflows/status.d.ts +34 -0
- package/dist/workflows/status.d.ts.map +1 -0
- package/dist/workflows/status.js +53 -0
- package/dist/workflows/status.js.map +1 -0
- package/dist/workflows/submit.d.ts +44 -0
- package/dist/workflows/submit.d.ts.map +1 -0
- package/dist/workflows/submit.js +143 -0
- package/dist/workflows/submit.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment adapters for language-specific worktree setup
|
|
3
|
+
*
|
|
4
|
+
* These adapters handle dependency bootstrapping when creating worktrees,
|
|
5
|
+
* ensuring the worktree is ready to build without manual intervention.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, lstatSync, symlinkSync, rmSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { join, dirname, relative } from 'node:path';
|
|
9
|
+
import { mkdir } from 'node:fs/promises';
|
|
10
|
+
/**
|
|
11
|
+
* Create an environment registry with optional initial adapters
|
|
12
|
+
*/
|
|
13
|
+
export function createEnvironmentRegistry(initialAdapters) {
|
|
14
|
+
const adapters = new Map();
|
|
15
|
+
// Register initial adapters
|
|
16
|
+
if (initialAdapters) {
|
|
17
|
+
for (const adapter of initialAdapters) {
|
|
18
|
+
adapters.set(adapter.name, adapter);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
register(adapter) {
|
|
23
|
+
adapters.set(adapter.name, adapter);
|
|
24
|
+
},
|
|
25
|
+
getAdapters() {
|
|
26
|
+
return Array.from(adapters.values())
|
|
27
|
+
.sort((a, b) => b.priority - a.priority);
|
|
28
|
+
},
|
|
29
|
+
getAdapter(name) {
|
|
30
|
+
return adapters.get(name);
|
|
31
|
+
},
|
|
32
|
+
async detectEnvironments(projectRoot) {
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const adapter of this.getAdapters()) {
|
|
35
|
+
const detection = await adapter.detect(projectRoot);
|
|
36
|
+
if (detection) {
|
|
37
|
+
results.push(detection);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results.sort((a, b) => b.confidence - a.confidence);
|
|
41
|
+
},
|
|
42
|
+
async bootstrapAll(worktreePath, sourceRoot, strategy, onProgress) {
|
|
43
|
+
const results = [];
|
|
44
|
+
const detected = await this.detectEnvironments(sourceRoot);
|
|
45
|
+
for (const detection of detected) {
|
|
46
|
+
const adapter = adapters.get(detection.name);
|
|
47
|
+
if (adapter) {
|
|
48
|
+
onProgress?.({
|
|
49
|
+
type: 'step_completed',
|
|
50
|
+
message: `Bootstrapping ${adapter.displayName} environment`,
|
|
51
|
+
data: { environment: detection.name, strategy },
|
|
52
|
+
});
|
|
53
|
+
const result = await adapter.bootstrap(worktreePath, sourceRoot, strategy, onProgress);
|
|
54
|
+
results.push(result);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
},
|
|
59
|
+
async cleanupAll(worktreePath) {
|
|
60
|
+
for (const adapter of this.getAdapters()) {
|
|
61
|
+
await adapter.cleanup(worktreePath);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Built-in Adapters
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/**
|
|
70
|
+
* Node.js environment adapter
|
|
71
|
+
* Detects: package.json, package-lock.json, yarn.lock, pnpm-lock.yaml
|
|
72
|
+
* Bootstraps: symlinks node_modules from source to worktree
|
|
73
|
+
*
|
|
74
|
+
* For monorepos: Detects workspace packages and auto-patches tsconfig.json
|
|
75
|
+
* with paths mappings to ensure TypeScript resolves to worktree source.
|
|
76
|
+
*/
|
|
77
|
+
export function createNodejsAdapter() {
|
|
78
|
+
return {
|
|
79
|
+
name: 'nodejs',
|
|
80
|
+
displayName: 'Node.js',
|
|
81
|
+
priority: 100,
|
|
82
|
+
async detect(projectRoot) {
|
|
83
|
+
const markerFiles = [];
|
|
84
|
+
let packageManager;
|
|
85
|
+
// Check for package.json (required)
|
|
86
|
+
if (existsSync(join(projectRoot, 'package.json'))) {
|
|
87
|
+
markerFiles.push('package.json');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// Detect package manager from lock files
|
|
93
|
+
if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) {
|
|
94
|
+
packageManager = 'pnpm';
|
|
95
|
+
markerFiles.push('pnpm-lock.yaml');
|
|
96
|
+
}
|
|
97
|
+
else if (existsSync(join(projectRoot, 'yarn.lock'))) {
|
|
98
|
+
packageManager = 'yarn';
|
|
99
|
+
markerFiles.push('yarn.lock');
|
|
100
|
+
}
|
|
101
|
+
else if (existsSync(join(projectRoot, 'package-lock.json'))) {
|
|
102
|
+
packageManager = 'npm';
|
|
103
|
+
markerFiles.push('package-lock.json');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
packageManager = 'npm'; // Default to npm
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
name: 'nodejs',
|
|
110
|
+
confidence: 1.0,
|
|
111
|
+
packageManager,
|
|
112
|
+
markerFiles,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
async bootstrap(worktreePath, sourceRoot, strategy, onProgress) {
|
|
116
|
+
const sourceModules = join(sourceRoot, 'node_modules');
|
|
117
|
+
const targetModules = join(worktreePath, 'node_modules');
|
|
118
|
+
// Detect monorepo structure
|
|
119
|
+
const monorepo = detectMonorepo(sourceRoot);
|
|
120
|
+
if (strategy === 'none') {
|
|
121
|
+
return {
|
|
122
|
+
environment: 'nodejs',
|
|
123
|
+
strategy: 'none',
|
|
124
|
+
success: true,
|
|
125
|
+
message: 'Node.js bootstrap skipped (strategy: none)',
|
|
126
|
+
monorepo,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Check if source node_modules exists
|
|
130
|
+
if (!existsSync(sourceModules)) {
|
|
131
|
+
return {
|
|
132
|
+
environment: 'nodejs',
|
|
133
|
+
strategy,
|
|
134
|
+
success: false,
|
|
135
|
+
message: 'Source node_modules not found. Run npm install in the main project first.',
|
|
136
|
+
monorepo,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (strategy === 'symlink') {
|
|
140
|
+
// Remove existing target if present
|
|
141
|
+
if (existsSync(targetModules)) {
|
|
142
|
+
rmSync(targetModules, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
// Create parent directory if needed
|
|
145
|
+
await mkdir(dirname(targetModules), { recursive: true });
|
|
146
|
+
// Create symlink
|
|
147
|
+
symlinkSync(sourceModules, targetModules, 'junction');
|
|
148
|
+
onProgress?.({
|
|
149
|
+
type: 'file_written',
|
|
150
|
+
message: `Symlinked node_modules`,
|
|
151
|
+
data: { source: sourceModules, target: targetModules },
|
|
152
|
+
});
|
|
153
|
+
// For monorepos with symlink strategy, patch tsconfig to fix local package resolution
|
|
154
|
+
let tsconfigPatched = false;
|
|
155
|
+
if (monorepo.isMonorepo && monorepo.workspacePackages.length > 0) {
|
|
156
|
+
onProgress?.({
|
|
157
|
+
type: 'step_completed',
|
|
158
|
+
message: `Detected ${monorepo.type} monorepo with ${monorepo.workspacePackages.length} local packages`,
|
|
159
|
+
data: { monorepo },
|
|
160
|
+
});
|
|
161
|
+
tsconfigPatched = patchTsconfigForMonorepo(worktreePath, monorepo, onProgress);
|
|
162
|
+
if (tsconfigPatched) {
|
|
163
|
+
onProgress?.({
|
|
164
|
+
type: 'step_completed',
|
|
165
|
+
message: 'Patched tsconfig.json files with local package paths',
|
|
166
|
+
data: { packages: monorepo.workspacePackages.map(p => p.name) },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
environment: 'nodejs',
|
|
172
|
+
strategy: 'symlink',
|
|
173
|
+
success: true,
|
|
174
|
+
message: tsconfigPatched
|
|
175
|
+
? `Symlinked node_modules and patched tsconfig for ${monorepo.workspacePackages.length} local packages`
|
|
176
|
+
: `Symlinked node_modules from ${sourceRoot}`,
|
|
177
|
+
path: targetModules,
|
|
178
|
+
tsconfigPatched,
|
|
179
|
+
monorepo,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// strategy === 'install'
|
|
183
|
+
// For install strategy, we'd run the package manager
|
|
184
|
+
// This requires executing shell commands which we'll leave as a TODO
|
|
185
|
+
return {
|
|
186
|
+
environment: 'nodejs',
|
|
187
|
+
strategy: 'install',
|
|
188
|
+
success: false,
|
|
189
|
+
message: 'Install strategy not yet implemented. Use symlink for now.',
|
|
190
|
+
monorepo,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
async cleanup(worktreePath) {
|
|
194
|
+
const targetModules = join(worktreePath, 'node_modules');
|
|
195
|
+
// Only remove if it's a symlink (don't delete real node_modules)
|
|
196
|
+
if (existsSync(targetModules)) {
|
|
197
|
+
try {
|
|
198
|
+
const stats = lstatSync(targetModules);
|
|
199
|
+
if (stats.isSymbolicLink()) {
|
|
200
|
+
rmSync(targetModules);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Ignore cleanup errors
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Python environment adapter
|
|
212
|
+
* Detects: requirements.txt, pyproject.toml, setup.py, Pipfile
|
|
213
|
+
* Bootstraps: symlinks .venv from source to worktree
|
|
214
|
+
*/
|
|
215
|
+
export function createPythonAdapter() {
|
|
216
|
+
return {
|
|
217
|
+
name: 'python',
|
|
218
|
+
displayName: 'Python',
|
|
219
|
+
priority: 90,
|
|
220
|
+
async detect(projectRoot) {
|
|
221
|
+
const markerFiles = [];
|
|
222
|
+
let packageManager;
|
|
223
|
+
// Check for Python project indicators
|
|
224
|
+
if (existsSync(join(projectRoot, 'pyproject.toml'))) {
|
|
225
|
+
markerFiles.push('pyproject.toml');
|
|
226
|
+
// Could be poetry, uv, or standard setuptools
|
|
227
|
+
if (existsSync(join(projectRoot, 'poetry.lock'))) {
|
|
228
|
+
packageManager = 'poetry';
|
|
229
|
+
markerFiles.push('poetry.lock');
|
|
230
|
+
}
|
|
231
|
+
else if (existsSync(join(projectRoot, 'uv.lock'))) {
|
|
232
|
+
packageManager = 'uv';
|
|
233
|
+
markerFiles.push('uv.lock');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
packageManager = 'pip';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (existsSync(join(projectRoot, 'requirements.txt'))) {
|
|
240
|
+
markerFiles.push('requirements.txt');
|
|
241
|
+
packageManager = 'pip';
|
|
242
|
+
}
|
|
243
|
+
else if (existsSync(join(projectRoot, 'Pipfile'))) {
|
|
244
|
+
markerFiles.push('Pipfile');
|
|
245
|
+
packageManager = 'pipenv';
|
|
246
|
+
}
|
|
247
|
+
else if (existsSync(join(projectRoot, 'setup.py'))) {
|
|
248
|
+
markerFiles.push('setup.py');
|
|
249
|
+
packageManager = 'pip';
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
name: 'python',
|
|
256
|
+
confidence: 1.0,
|
|
257
|
+
packageManager,
|
|
258
|
+
markerFiles,
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
async bootstrap(worktreePath, sourceRoot, strategy, onProgress) {
|
|
262
|
+
const sourceVenv = join(sourceRoot, '.venv');
|
|
263
|
+
const targetVenv = join(worktreePath, '.venv');
|
|
264
|
+
if (strategy === 'none') {
|
|
265
|
+
return {
|
|
266
|
+
environment: 'python',
|
|
267
|
+
strategy: 'none',
|
|
268
|
+
success: true,
|
|
269
|
+
message: 'Python bootstrap skipped (strategy: none)',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// Check if source .venv exists
|
|
273
|
+
if (!existsSync(sourceVenv)) {
|
|
274
|
+
return {
|
|
275
|
+
environment: 'python',
|
|
276
|
+
strategy,
|
|
277
|
+
success: true, // Not a failure - just no venv to symlink
|
|
278
|
+
message: 'No .venv found in source. Python environment not bootstrapped.',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (strategy === 'symlink') {
|
|
282
|
+
// Remove existing target if present
|
|
283
|
+
if (existsSync(targetVenv)) {
|
|
284
|
+
rmSync(targetVenv, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
// Create parent directory if needed
|
|
287
|
+
await mkdir(dirname(targetVenv), { recursive: true });
|
|
288
|
+
// Create symlink
|
|
289
|
+
symlinkSync(sourceVenv, targetVenv, 'junction');
|
|
290
|
+
onProgress?.({
|
|
291
|
+
type: 'file_written',
|
|
292
|
+
message: `Symlinked .venv`,
|
|
293
|
+
data: { source: sourceVenv, target: targetVenv },
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
environment: 'python',
|
|
297
|
+
strategy: 'symlink',
|
|
298
|
+
success: true,
|
|
299
|
+
message: `Symlinked .venv from ${sourceRoot}`,
|
|
300
|
+
path: targetVenv,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// strategy === 'install'
|
|
304
|
+
return {
|
|
305
|
+
environment: 'python',
|
|
306
|
+
strategy: 'install',
|
|
307
|
+
success: false,
|
|
308
|
+
message: 'Install strategy not yet implemented. Use symlink for now.',
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
async cleanup(worktreePath) {
|
|
312
|
+
const targetVenv = join(worktreePath, '.venv');
|
|
313
|
+
if (existsSync(targetVenv)) {
|
|
314
|
+
try {
|
|
315
|
+
const stats = lstatSync(targetVenv);
|
|
316
|
+
if (stats.isSymbolicLink()) {
|
|
317
|
+
rmSync(targetVenv);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Ignore cleanup errors
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Go environment adapter
|
|
329
|
+
* Detects: go.mod
|
|
330
|
+
* Bootstraps: no-op (Go uses global module cache)
|
|
331
|
+
*/
|
|
332
|
+
export function createGoAdapter() {
|
|
333
|
+
return {
|
|
334
|
+
name: 'go',
|
|
335
|
+
displayName: 'Go',
|
|
336
|
+
priority: 80,
|
|
337
|
+
async detect(projectRoot) {
|
|
338
|
+
if (existsSync(join(projectRoot, 'go.mod'))) {
|
|
339
|
+
return {
|
|
340
|
+
name: 'go',
|
|
341
|
+
confidence: 1.0,
|
|
342
|
+
packageManager: 'go',
|
|
343
|
+
markerFiles: ['go.mod'],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
},
|
|
348
|
+
async bootstrap(_worktreePath, _sourceRoot, strategy, _onProgress) {
|
|
349
|
+
// Go uses a global module cache, no per-project setup needed
|
|
350
|
+
return {
|
|
351
|
+
environment: 'go',
|
|
352
|
+
strategy,
|
|
353
|
+
success: true,
|
|
354
|
+
message: 'Go uses global module cache. No worktree setup needed.',
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
async cleanup(_worktreePath) {
|
|
358
|
+
// Nothing to clean up for Go
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Rust environment adapter
|
|
364
|
+
* Detects: Cargo.toml
|
|
365
|
+
* Bootstraps: no-op (Rust uses global cargo cache)
|
|
366
|
+
*/
|
|
367
|
+
export function createRustAdapter() {
|
|
368
|
+
return {
|
|
369
|
+
name: 'rust',
|
|
370
|
+
displayName: 'Rust',
|
|
371
|
+
priority: 80,
|
|
372
|
+
async detect(projectRoot) {
|
|
373
|
+
if (existsSync(join(projectRoot, 'Cargo.toml'))) {
|
|
374
|
+
return {
|
|
375
|
+
name: 'rust',
|
|
376
|
+
confidence: 1.0,
|
|
377
|
+
packageManager: 'cargo',
|
|
378
|
+
markerFiles: ['Cargo.toml'],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
},
|
|
383
|
+
async bootstrap(_worktreePath, _sourceRoot, strategy, _onProgress) {
|
|
384
|
+
// Rust uses a global cargo cache, no per-project setup needed
|
|
385
|
+
return {
|
|
386
|
+
environment: 'rust',
|
|
387
|
+
strategy,
|
|
388
|
+
success: true,
|
|
389
|
+
message: 'Rust uses global cargo cache. No worktree setup needed.',
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
async cleanup(_worktreePath) {
|
|
393
|
+
// Nothing to clean up for Rust
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Create the default environment registry with all built-in adapters
|
|
399
|
+
*/
|
|
400
|
+
export function createDefaultEnvironmentRegistry() {
|
|
401
|
+
return createEnvironmentRegistry([
|
|
402
|
+
createNodejsAdapter(),
|
|
403
|
+
createPythonAdapter(),
|
|
404
|
+
createGoAdapter(),
|
|
405
|
+
createRustAdapter(),
|
|
406
|
+
]);
|
|
407
|
+
}
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// Monorepo Detection and TypeScript Patching
|
|
410
|
+
// ============================================================================
|
|
411
|
+
/**
|
|
412
|
+
* Detect if a project is a monorepo and identify workspace packages
|
|
413
|
+
*/
|
|
414
|
+
export function detectMonorepo(projectRoot) {
|
|
415
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
416
|
+
if (!existsSync(packageJsonPath)) {
|
|
417
|
+
return { isMonorepo: false, rootPackageJson: packageJsonPath, workspacePackages: [] };
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
421
|
+
// Check for npm/yarn workspaces
|
|
422
|
+
if (packageJson.workspaces) {
|
|
423
|
+
const workspacePatterns = Array.isArray(packageJson.workspaces)
|
|
424
|
+
? packageJson.workspaces
|
|
425
|
+
: packageJson.workspaces.packages ?? [];
|
|
426
|
+
const packages = resolveWorkspacePackages(projectRoot, workspacePatterns);
|
|
427
|
+
return {
|
|
428
|
+
isMonorepo: true,
|
|
429
|
+
type: 'npm-workspaces',
|
|
430
|
+
rootPackageJson: packageJsonPath,
|
|
431
|
+
workspacePackages: packages,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
// Check for pnpm workspaces
|
|
435
|
+
const pnpmWorkspacePath = join(projectRoot, 'pnpm-workspace.yaml');
|
|
436
|
+
if (existsSync(pnpmWorkspacePath)) {
|
|
437
|
+
// Simple YAML parsing for packages array
|
|
438
|
+
const content = readFileSync(pnpmWorkspacePath, 'utf-8');
|
|
439
|
+
const patterns = parsePnpmWorkspaceYaml(content);
|
|
440
|
+
const packages = resolveWorkspacePackages(projectRoot, patterns);
|
|
441
|
+
return {
|
|
442
|
+
isMonorepo: true,
|
|
443
|
+
type: 'pnpm-workspaces',
|
|
444
|
+
rootPackageJson: packageJsonPath,
|
|
445
|
+
workspacePackages: packages,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// Check for lerna
|
|
449
|
+
const lernaPath = join(projectRoot, 'lerna.json');
|
|
450
|
+
if (existsSync(lernaPath)) {
|
|
451
|
+
const lernaConfig = JSON.parse(readFileSync(lernaPath, 'utf-8'));
|
|
452
|
+
const patterns = lernaConfig.packages ?? ['packages/*'];
|
|
453
|
+
const packages = resolveWorkspacePackages(projectRoot, patterns);
|
|
454
|
+
return {
|
|
455
|
+
isMonorepo: true,
|
|
456
|
+
type: 'lerna',
|
|
457
|
+
rootPackageJson: packageJsonPath,
|
|
458
|
+
workspacePackages: packages,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Ignore JSON parse errors
|
|
464
|
+
}
|
|
465
|
+
return { isMonorepo: false, rootPackageJson: packageJsonPath, workspacePackages: [] };
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Parse pnpm-workspace.yaml to extract package patterns
|
|
469
|
+
* Simple parser that handles common cases
|
|
470
|
+
*/
|
|
471
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
472
|
+
const patterns = [];
|
|
473
|
+
const lines = content.split('\n');
|
|
474
|
+
let inPackages = false;
|
|
475
|
+
for (const line of lines) {
|
|
476
|
+
const trimmed = line.trim();
|
|
477
|
+
if (trimmed === 'packages:') {
|
|
478
|
+
inPackages = true;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (inPackages) {
|
|
482
|
+
if (trimmed.startsWith('-')) {
|
|
483
|
+
// Extract pattern from "- packages/*" or "- 'packages/*'"
|
|
484
|
+
let pattern = trimmed.slice(1).trim();
|
|
485
|
+
pattern = pattern.replace(/^['"]|['"]$/g, '');
|
|
486
|
+
if (pattern) {
|
|
487
|
+
patterns.push(pattern);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (!trimmed.startsWith('#') && trimmed.length > 0 && !trimmed.startsWith('-')) {
|
|
491
|
+
// End of packages section
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return patterns;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Resolve workspace patterns to actual package directories
|
|
500
|
+
*/
|
|
501
|
+
function resolveWorkspacePackages(projectRoot, patterns) {
|
|
502
|
+
const packages = [];
|
|
503
|
+
for (const pattern of patterns) {
|
|
504
|
+
// Handle simple glob patterns like "packages/*"
|
|
505
|
+
if (pattern.endsWith('/*')) {
|
|
506
|
+
const baseDir = pattern.slice(0, -2);
|
|
507
|
+
const fullPath = join(projectRoot, baseDir);
|
|
508
|
+
if (existsSync(fullPath)) {
|
|
509
|
+
try {
|
|
510
|
+
const entries = readdirSync(fullPath, { withFileTypes: true });
|
|
511
|
+
for (const entry of entries) {
|
|
512
|
+
if (entry.isDirectory()) {
|
|
513
|
+
const pkgPath = join(fullPath, entry.name);
|
|
514
|
+
const pkgJsonPath = join(pkgPath, 'package.json');
|
|
515
|
+
if (existsSync(pkgJsonPath)) {
|
|
516
|
+
try {
|
|
517
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
518
|
+
const relativePath = relative(projectRoot, pkgPath);
|
|
519
|
+
// Find TypeScript entry point
|
|
520
|
+
const entryPoint = findTypeScriptEntryPoint(pkgPath);
|
|
521
|
+
packages.push({
|
|
522
|
+
name: pkgJson.name,
|
|
523
|
+
path: relativePath,
|
|
524
|
+
absolutePath: pkgPath,
|
|
525
|
+
entryPoint,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
// Skip packages with invalid package.json
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// Ignore directory read errors
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else if (!pattern.includes('*')) {
|
|
541
|
+
// Direct path like "packages/core"
|
|
542
|
+
const fullPath = join(projectRoot, pattern);
|
|
543
|
+
const pkgJsonPath = join(fullPath, 'package.json');
|
|
544
|
+
if (existsSync(pkgJsonPath)) {
|
|
545
|
+
try {
|
|
546
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
547
|
+
const entryPoint = findTypeScriptEntryPoint(fullPath);
|
|
548
|
+
packages.push({
|
|
549
|
+
name: pkgJson.name,
|
|
550
|
+
path: pattern,
|
|
551
|
+
absolutePath: fullPath,
|
|
552
|
+
entryPoint,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// Skip packages with invalid package.json
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return packages;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Find the TypeScript entry point for a package
|
|
565
|
+
*/
|
|
566
|
+
function findTypeScriptEntryPoint(packagePath) {
|
|
567
|
+
// Common entry points in order of preference
|
|
568
|
+
const candidates = [
|
|
569
|
+
'src/index.ts',
|
|
570
|
+
'src/index.tsx',
|
|
571
|
+
'lib/index.ts',
|
|
572
|
+
'index.ts',
|
|
573
|
+
];
|
|
574
|
+
for (const candidate of candidates) {
|
|
575
|
+
if (existsSync(join(packagePath, candidate))) {
|
|
576
|
+
return candidate;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Patch tsconfig files in a worktree to add paths for local workspace packages
|
|
583
|
+
* This ensures TypeScript resolves to worktree source, not the symlinked main repo
|
|
584
|
+
*/
|
|
585
|
+
export function patchTsconfigForMonorepo(worktreePath, monorepo, onProgress) {
|
|
586
|
+
if (!monorepo.isMonorepo || monorepo.workspacePackages.length === 0) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
let patched = false;
|
|
590
|
+
// Find all tsconfig files in the worktree
|
|
591
|
+
const tsconfigPaths = findTsconfigFiles(worktreePath);
|
|
592
|
+
for (const tsconfigPath of tsconfigPaths) {
|
|
593
|
+
try {
|
|
594
|
+
const pathedPackages = patchSingleTsconfig(tsconfigPath, worktreePath, monorepo);
|
|
595
|
+
if (pathedPackages > 0) {
|
|
596
|
+
patched = true;
|
|
597
|
+
onProgress?.({
|
|
598
|
+
type: 'file_written',
|
|
599
|
+
message: `Patched ${tsconfigPath} with ${pathedPackages} local package paths`,
|
|
600
|
+
data: { tsconfigPath, packageCount: pathedPackages },
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// Skip files that can't be patched
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return patched;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Find all tsconfig.json files in a directory tree
|
|
612
|
+
*/
|
|
613
|
+
function findTsconfigFiles(rootPath) {
|
|
614
|
+
const tsconfigs = [];
|
|
615
|
+
function scan(dirPath, depth) {
|
|
616
|
+
if (depth > 5)
|
|
617
|
+
return; // Limit depth to avoid traversing too deep
|
|
618
|
+
try {
|
|
619
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
620
|
+
for (const entry of entries) {
|
|
621
|
+
const fullPath = join(dirPath, entry.name);
|
|
622
|
+
if (entry.isFile() && entry.name === 'tsconfig.json') {
|
|
623
|
+
tsconfigs.push(fullPath);
|
|
624
|
+
}
|
|
625
|
+
else if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
626
|
+
scan(fullPath, depth + 1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// Ignore directory read errors
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
scan(rootPath, 0);
|
|
635
|
+
return tsconfigs;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Patch a single tsconfig.json file with paths for local packages
|
|
639
|
+
*/
|
|
640
|
+
function patchSingleTsconfig(tsconfigPath, worktreePath, monorepo) {
|
|
641
|
+
const content = readFileSync(tsconfigPath, 'utf-8');
|
|
642
|
+
// Parse tsconfig (handle comments by stripping them)
|
|
643
|
+
const jsonContent = stripJsonComments(content);
|
|
644
|
+
const tsconfig = JSON.parse(jsonContent);
|
|
645
|
+
// Ensure compilerOptions exists
|
|
646
|
+
if (!tsconfig.compilerOptions) {
|
|
647
|
+
tsconfig.compilerOptions = {};
|
|
648
|
+
}
|
|
649
|
+
// Calculate relative path from tsconfig location to worktree root
|
|
650
|
+
const tsconfigDir = dirname(tsconfigPath);
|
|
651
|
+
const relativeToRoot = relative(tsconfigDir, worktreePath);
|
|
652
|
+
// Set baseUrl if not already set
|
|
653
|
+
if (!tsconfig.compilerOptions.baseUrl) {
|
|
654
|
+
tsconfig.compilerOptions.baseUrl = '.';
|
|
655
|
+
}
|
|
656
|
+
// Build paths mapping for local packages
|
|
657
|
+
if (!tsconfig.compilerOptions.paths) {
|
|
658
|
+
tsconfig.compilerOptions.paths = {};
|
|
659
|
+
}
|
|
660
|
+
let addedCount = 0;
|
|
661
|
+
for (const pkg of monorepo.workspacePackages) {
|
|
662
|
+
// Skip if path already exists for this package
|
|
663
|
+
if (tsconfig.compilerOptions.paths[pkg.name]) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
// Calculate relative path from tsconfig to package source
|
|
667
|
+
const packageSrcPath = pkg.entryPoint
|
|
668
|
+
? join(relativeToRoot, pkg.path, pkg.entryPoint)
|
|
669
|
+
: join(relativeToRoot, pkg.path, 'src/index.ts');
|
|
670
|
+
// Add path mapping
|
|
671
|
+
tsconfig.compilerOptions.paths[pkg.name] = [packageSrcPath];
|
|
672
|
+
addedCount++;
|
|
673
|
+
}
|
|
674
|
+
if (addedCount > 0) {
|
|
675
|
+
// Write back with pretty formatting
|
|
676
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n', 'utf-8');
|
|
677
|
+
}
|
|
678
|
+
return addedCount;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Strip JSON comments for parsing (single-line // and multi-line block comments)
|
|
682
|
+
*/
|
|
683
|
+
function stripJsonComments(content) {
|
|
684
|
+
// Remove single-line comments
|
|
685
|
+
let result = content.replace(/\/\/.*$/gm, '');
|
|
686
|
+
// Remove multi-line comments
|
|
687
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
//# sourceMappingURL=environment-adapter.js.map
|