@paths.design/caws-cli 9.3.0 → 9.3.1
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.
|
@@ -39,7 +39,7 @@ function detectTestRunner(projectRoot) {
|
|
|
39
39
|
if (fs.existsSync(fp)) {
|
|
40
40
|
try {
|
|
41
41
|
if (fs.readFileSync(fp, 'utf8').includes(needle)) return check.runner;
|
|
42
|
-
} catch (_) {}
|
|
42
|
+
} catch (_) { /* ignore unreadable config */ }
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -123,7 +123,7 @@ function runNodeid(nodeid, runner, projectRoot) {
|
|
|
123
123
|
try {
|
|
124
124
|
switch (runner) {
|
|
125
125
|
case 'pytest': {
|
|
126
|
-
|
|
126
|
+
execFileSync('python3', ['-m', 'pytest', '-x', '--tb=short', nodeid], {
|
|
127
127
|
cwd: projectRoot, encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
128
128
|
});
|
|
129
129
|
return { passed: true, detail: 'tests passed' };
|
|
@@ -215,7 +215,7 @@ function checkEvidence(evidence, projectRoot) {
|
|
|
215
215
|
if (files) {
|
|
216
216
|
return { found: true, detail: `found: ${files.split('\n')[0]}` };
|
|
217
217
|
}
|
|
218
|
-
} catch (_) {}
|
|
218
|
+
} catch (_) { /* ignore unreadable config */ }
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
@@ -350,7 +350,7 @@ function loadSpecs(projectRoot, targetSpecId) {
|
|
|
350
350
|
if (s && !TERMINAL_STATUSES.has(s.status)) {
|
|
351
351
|
specs.push({ path: workingSpec, spec: s });
|
|
352
352
|
}
|
|
353
|
-
} catch (_) {}
|
|
353
|
+
} catch (_) { /* ignore unreadable config */ }
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
if (fs.existsSync(specsDir)) {
|
|
@@ -360,7 +360,7 @@ function loadSpecs(projectRoot, targetSpecId) {
|
|
|
360
360
|
if (s && !TERMINAL_STATUSES.has(s.status)) {
|
|
361
361
|
specs.push({ path: path.join(specsDir, f), spec: s });
|
|
362
362
|
}
|
|
363
|
-
} catch (_) {}
|
|
363
|
+
} catch (_) { /* ignore unreadable config */ }
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
package/dist/utils/detection.js
CHANGED
|
@@ -203,17 +203,8 @@ function detectCAWSSetup(cwd = process.cwd()) {
|
|
|
203
203
|
* @returns {string} Project root directory path
|
|
204
204
|
*/
|
|
205
205
|
function findProjectRoot(startDir = process.cwd()) {
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
const root = path.parse(dir).root;
|
|
209
|
-
while (dir !== root) {
|
|
210
|
-
if (fs.existsSync(path.join(dir, '.caws'))) {
|
|
211
|
-
return dir;
|
|
212
|
-
}
|
|
213
|
-
dir = path.dirname(dir);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Fallback: try git root
|
|
206
|
+
// In a monorepo, nested packages may have their own .caws/ (scaffold debris).
|
|
207
|
+
// The git root's .caws/ is authoritative — check it first.
|
|
217
208
|
try {
|
|
218
209
|
const { execFileSync } = require('child_process');
|
|
219
210
|
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
@@ -225,7 +216,17 @@ function findProjectRoot(startDir = process.cwd()) {
|
|
|
225
216
|
return gitRoot;
|
|
226
217
|
}
|
|
227
218
|
} catch {
|
|
228
|
-
// Not a git repo or git not available
|
|
219
|
+
// Not a git repo or git not available — fall through
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Walk up looking for .caws/ directory (non-git projects)
|
|
223
|
+
let dir = path.resolve(startDir);
|
|
224
|
+
const root = path.parse(dir).root;
|
|
225
|
+
while (dir !== root) {
|
|
226
|
+
if (fs.existsSync(path.join(dir, '.caws'))) {
|
|
227
|
+
return dir;
|
|
228
|
+
}
|
|
229
|
+
dir = path.dirname(dir);
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
// Final fallback: cwd
|
|
@@ -101,6 +101,106 @@ function saveRegistry(root, registry) {
|
|
|
101
101
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Discover git worktrees under .caws/worktrees/ that are not in the registry.
|
|
106
|
+
* @param {string} root - Repository root
|
|
107
|
+
* @param {Object} registry - Current registry object
|
|
108
|
+
* @returns {Array<{ name: string, path: string, branch: string }>}
|
|
109
|
+
*/
|
|
110
|
+
function discoverUnregisteredWorktrees(root, registry) {
|
|
111
|
+
const unregistered = [];
|
|
112
|
+
try {
|
|
113
|
+
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
114
|
+
cwd: root,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
stdio: 'pipe',
|
|
117
|
+
});
|
|
118
|
+
let worktreesDir;
|
|
119
|
+
try {
|
|
120
|
+
worktreesDir = fs.realpathSync(path.resolve(root, WORKTREES_DIR));
|
|
121
|
+
} catch {
|
|
122
|
+
// Directory might not exist yet
|
|
123
|
+
worktreesDir = path.resolve(root, WORKTREES_DIR);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const blocks = output.split('\n\n').filter(Boolean);
|
|
127
|
+
for (const block of blocks) {
|
|
128
|
+
const lines = block.split('\n');
|
|
129
|
+
const wtLine = lines.find((l) => l.startsWith('worktree '));
|
|
130
|
+
const branchLine = lines.find((l) => l.startsWith('branch '));
|
|
131
|
+
if (!wtLine) continue;
|
|
132
|
+
|
|
133
|
+
const wtPath = wtLine.replace('worktree ', '');
|
|
134
|
+
let resolvedPath;
|
|
135
|
+
try {
|
|
136
|
+
resolvedPath = fs.realpathSync(wtPath);
|
|
137
|
+
} catch {
|
|
138
|
+
resolvedPath = path.resolve(wtPath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Only consider worktrees under .caws/worktrees/
|
|
142
|
+
if (!resolvedPath.startsWith(worktreesDir + path.sep)) continue;
|
|
143
|
+
|
|
144
|
+
const name = path.basename(resolvedPath);
|
|
145
|
+
if (registry.worktrees[name]) continue;
|
|
146
|
+
|
|
147
|
+
const branch = branchLine
|
|
148
|
+
? branchLine.replace('branch refs/heads/', '')
|
|
149
|
+
: `${BRANCH_PREFIX}${name}`;
|
|
150
|
+
unregistered.push({ name, path: resolvedPath, branch });
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// git worktree list failed
|
|
154
|
+
}
|
|
155
|
+
return unregistered;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Auto-register an unregistered worktree. Infers baseBranch via merge-base.
|
|
160
|
+
* @param {string} root - Repository root
|
|
161
|
+
* @param {Object} registry - Registry object (mutated in place)
|
|
162
|
+
* @param {{ name: string, path: string, branch: string }} discovered
|
|
163
|
+
* @returns {Object} The registered entry
|
|
164
|
+
*/
|
|
165
|
+
function autoRegisterWorktree(root, registry, discovered) {
|
|
166
|
+
let baseBranch = 'main';
|
|
167
|
+
try {
|
|
168
|
+
execFileSync(
|
|
169
|
+
'git',
|
|
170
|
+
['merge-base', discovered.branch, 'main'],
|
|
171
|
+
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
172
|
+
);
|
|
173
|
+
} catch {
|
|
174
|
+
try {
|
|
175
|
+
execFileSync(
|
|
176
|
+
'git',
|
|
177
|
+
['merge-base', discovered.branch, 'master'],
|
|
178
|
+
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
179
|
+
);
|
|
180
|
+
baseBranch = 'master';
|
|
181
|
+
} catch {
|
|
182
|
+
// Keep 'main' as default
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const entry = {
|
|
187
|
+
name: discovered.name,
|
|
188
|
+
path: discovered.path,
|
|
189
|
+
branch: discovered.branch,
|
|
190
|
+
baseBranch,
|
|
191
|
+
scope: null,
|
|
192
|
+
specId: null,
|
|
193
|
+
owner: null,
|
|
194
|
+
createdAt: new Date().toISOString(),
|
|
195
|
+
status: 'active',
|
|
196
|
+
autoRegistered: true,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
registry.worktrees[discovered.name] = entry;
|
|
200
|
+
saveRegistry(root, registry);
|
|
201
|
+
return entry;
|
|
202
|
+
}
|
|
203
|
+
|
|
104
204
|
/**
|
|
105
205
|
* Create a new git worktree with scope isolation
|
|
106
206
|
* @param {string} name - Worktree name
|
|
@@ -308,6 +408,25 @@ function listWorktrees() {
|
|
|
308
408
|
};
|
|
309
409
|
});
|
|
310
410
|
|
|
411
|
+
// Append unregistered worktrees discovered from git
|
|
412
|
+
const unregistered = discoverUnregisteredWorktrees(root, registry);
|
|
413
|
+
for (const discovered of unregistered) {
|
|
414
|
+
const lastCommit = getLastCommitInfo(discovered.branch, root);
|
|
415
|
+
entries.push({
|
|
416
|
+
name: discovered.name,
|
|
417
|
+
path: discovered.path,
|
|
418
|
+
branch: discovered.branch,
|
|
419
|
+
baseBranch: null,
|
|
420
|
+
scope: null,
|
|
421
|
+
specId: null,
|
|
422
|
+
owner: null,
|
|
423
|
+
createdAt: null,
|
|
424
|
+
status: 'unregistered',
|
|
425
|
+
lastCommit,
|
|
426
|
+
merged: false,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
311
430
|
return entries;
|
|
312
431
|
}
|
|
313
432
|
|
|
@@ -323,9 +442,17 @@ function destroyWorktree(name, options = {}) {
|
|
|
323
442
|
const registry = loadRegistry(root);
|
|
324
443
|
const { deleteBranch = false, force = false } = options;
|
|
325
444
|
|
|
326
|
-
|
|
445
|
+
let entry = registry.worktrees[name];
|
|
327
446
|
if (!entry) {
|
|
328
|
-
|
|
447
|
+
// Fallback: scan git for unregistered worktree and auto-register
|
|
448
|
+
const unregistered = discoverUnregisteredWorktrees(root, registry);
|
|
449
|
+
const discovered = unregistered.find((u) => u.name === name);
|
|
450
|
+
if (discovered) {
|
|
451
|
+
console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
|
|
452
|
+
entry = autoRegisterWorktree(root, registry, discovered);
|
|
453
|
+
} else {
|
|
454
|
+
throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
|
|
455
|
+
}
|
|
329
456
|
}
|
|
330
457
|
|
|
331
458
|
// Ownership check: refuse to destroy another agent's active worktree without --force
|
|
@@ -438,9 +565,17 @@ function mergeWorktree(name, options = {}) {
|
|
|
438
565
|
const registry = loadRegistry(root);
|
|
439
566
|
const { dryRun = false, deleteBranch = true, message } = options;
|
|
440
567
|
|
|
441
|
-
|
|
568
|
+
let entry = registry.worktrees[name];
|
|
442
569
|
if (!entry) {
|
|
443
|
-
|
|
570
|
+
// Fallback: scan git for unregistered worktree and auto-register
|
|
571
|
+
const unregistered = discoverUnregisteredWorktrees(root, registry);
|
|
572
|
+
const discovered = unregistered.find((u) => u.name === name);
|
|
573
|
+
if (discovered) {
|
|
574
|
+
console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
|
|
575
|
+
entry = autoRegisterWorktree(root, registry, discovered);
|
|
576
|
+
} else {
|
|
577
|
+
throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
|
|
578
|
+
}
|
|
444
579
|
}
|
|
445
580
|
|
|
446
581
|
const baseBranch = entry.baseBranch || 'main';
|
|
@@ -469,7 +604,7 @@ function mergeWorktree(name, options = {}) {
|
|
|
469
604
|
let conflicts = [];
|
|
470
605
|
try {
|
|
471
606
|
// New-style merge-tree: takes two branches, computes merge-base automatically
|
|
472
|
-
|
|
607
|
+
execFileSync(
|
|
473
608
|
'git',
|
|
474
609
|
['merge-tree', '--write-tree', baseBranch, entry.branch],
|
|
475
610
|
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
@@ -624,6 +759,8 @@ module.exports = {
|
|
|
624
759
|
getRepoRoot,
|
|
625
760
|
getLastCommitInfo,
|
|
626
761
|
isBranchMerged,
|
|
762
|
+
discoverUnregisteredWorktrees,
|
|
763
|
+
autoRegisterWorktree,
|
|
627
764
|
WORKTREES_DIR,
|
|
628
765
|
REGISTRY_FILE,
|
|
629
766
|
BRANCH_PREFIX,
|
package/package.json
CHANGED