@paths.design/caws-cli 8.0.1 → 8.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/README.md +5 -6
- package/dist/commands/archive.d.ts +1 -0
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +114 -6
- package/dist/commands/burnup.d.ts.map +1 -1
- package/dist/commands/burnup.js +109 -10
- package/dist/commands/diagnose.js +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +185 -39
- package/dist/commands/mode.d.ts +2 -1
- package/dist/commands/mode.d.ts.map +1 -1
- package/dist/commands/mode.js +24 -14
- package/dist/commands/provenance.d.ts.map +1 -1
- package/dist/commands/provenance.js +216 -93
- package/dist/commands/quality-gates.d.ts.map +1 -1
- package/dist/commands/quality-gates.js +3 -1
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/specs.js +184 -6
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +134 -10
- package/dist/commands/templates.js +2 -2
- package/dist/commands/worktree.d.ts +7 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +136 -0
- package/dist/config/lite-scope.d.ts +33 -0
- package/dist/config/lite-scope.d.ts.map +1 -0
- package/dist/config/lite-scope.js +158 -0
- package/dist/config/modes.d.ts +90 -51
- package/dist/config/modes.d.ts.map +1 -1
- package/dist/config/modes.js +26 -0
- package/dist/error-handler.d.ts +3 -16
- package/dist/error-handler.d.ts.map +1 -1
- package/dist/error-handler.js +6 -98
- package/dist/generators/jest-config-generator.d.ts +32 -0
- package/dist/generators/jest-config-generator.d.ts.map +1 -0
- package/dist/generators/jest-config-generator.js +242 -0
- package/dist/index.js +40 -7
- package/dist/minimal-cli.js +3 -1
- package/dist/scaffold/claude-hooks.d.ts +28 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -0
- package/dist/scaffold/claude-hooks.js +344 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +96 -76
- package/dist/templates/.caws/schemas/scope.schema.json +52 -0
- package/dist/templates/.caws/schemas/working-spec.schema.json +1 -1
- package/dist/templates/.caws/schemas/worktrees.schema.json +36 -0
- package/dist/templates/.claude/README.md +190 -0
- package/dist/templates/.claude/hooks/audit.sh +96 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +123 -0
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/dist/templates/.claude/hooks/naming-check.sh +97 -0
- package/dist/templates/.claude/hooks/quality-check.sh +68 -0
- package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +192 -0
- package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
- package/dist/templates/.claude/settings.json +95 -0
- package/dist/templates/.cursor/README.md +0 -3
- package/dist/templates/.github/copilot-instructions.md +82 -0
- package/dist/templates/.junie/guidelines.md +73 -0
- package/dist/templates/.vscode/launch.json +0 -27
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +54 -0
- package/dist/templates/CLAUDE.md +101 -0
- package/dist/templates/agents.md +73 -1016
- package/dist/templates/docs/README.md +5 -5
- package/dist/test-analysis.d.ts +50 -1
- package/dist/test-analysis.d.ts.map +1 -1
- package/dist/test-analysis.js +203 -10
- package/dist/utils/error-categories.d.ts +52 -0
- package/dist/utils/error-categories.d.ts.map +1 -0
- package/dist/utils/error-categories.js +210 -0
- package/dist/utils/gitignore-updater.d.ts +1 -1
- package/dist/utils/gitignore-updater.d.ts.map +1 -1
- package/dist/utils/gitignore-updater.js +4 -0
- package/dist/utils/ide-detection.js +133 -0
- package/dist/utils/quality-gates-utils.d.ts +49 -0
- package/dist/utils/quality-gates-utils.d.ts.map +1 -0
- package/dist/utils/quality-gates-utils.js +402 -0
- package/dist/utils/typescript-detector.d.ts +8 -5
- package/dist/utils/typescript-detector.d.ts.map +1 -1
- package/dist/utils/typescript-detector.js +36 -90
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +59 -6
- package/dist/worktree/worktree-manager.d.ts +54 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.js +378 -0
- package/package.json +9 -3
- package/templates/.caws/schemas/scope.schema.json +52 -0
- package/templates/.caws/schemas/working-spec.schema.json +1 -1
- package/templates/.caws/schemas/worktrees.schema.json +36 -0
- package/templates/.claude/README.md +190 -0
- package/templates/.claude/hooks/audit.sh +96 -0
- package/templates/.claude/hooks/block-dangerous.sh +123 -0
- package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/templates/.claude/hooks/naming-check.sh +97 -0
- package/templates/.claude/hooks/quality-check.sh +68 -0
- package/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/templates/.claude/hooks/scope-guard.sh +192 -0
- package/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/templates/.claude/hooks/validate-spec.sh +76 -0
- package/templates/.claude/settings.json +95 -0
- package/templates/.cursor/README.md +0 -3
- package/templates/.github/copilot-instructions.md +82 -0
- package/templates/.junie/guidelines.md +73 -0
- package/templates/.vscode/launch.json +0 -27
- package/templates/.windsurf/rules/caws-quality-standards.md +54 -0
- package/templates/AGENTS.md +104 -0
- package/templates/CLAUDE.md +101 -0
- package/templates/docs/README.md +5 -5
- package/templates/.github/copilot/instructions.md +0 -311
- package/templates/agents.md +0 -1047
|
@@ -101,10 +101,40 @@ function detectTestFramework(projectDir = process.cwd(), packageJson = null) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Expand workspace glob patterns to actual directories
|
|
105
|
+
* Shared helper for npm, pnpm, and lerna workspace resolution
|
|
106
|
+
* @param {string[]} patterns - Workspace patterns (may include globs like "packages/*")
|
|
105
107
|
* @param {string} projectDir - Project directory path
|
|
106
|
-
* @returns {string[]} Array of workspace
|
|
108
|
+
* @returns {string[]} Array of resolved workspace directory paths
|
|
107
109
|
*/
|
|
110
|
+
function expandWorkspacePatterns(patterns, projectDir) {
|
|
111
|
+
const workspaceDirs = [];
|
|
112
|
+
for (const pattern of patterns) {
|
|
113
|
+
if (pattern.includes('*')) {
|
|
114
|
+
const baseDir = pattern.split('*')[0];
|
|
115
|
+
const fullBaseDir = path.join(projectDir, baseDir);
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(fullBaseDir)) {
|
|
118
|
+
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
const wsPath = path.join(fullBaseDir, entry.name);
|
|
122
|
+
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
123
|
+
workspaceDirs.push(wsPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const wsPath = path.join(projectDir, pattern);
|
|
130
|
+
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
131
|
+
workspaceDirs.push(wsPath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return workspaceDirs;
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
/**
|
|
109
139
|
* Get workspace directories from npm/yarn package.json workspaces
|
|
110
140
|
* @param {string} projectDir - Project directory path
|
|
@@ -120,36 +150,7 @@ function getNpmWorkspaces(projectDir) {
|
|
|
120
150
|
try {
|
|
121
151
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
122
152
|
const workspaces = packageJson.workspaces || [];
|
|
123
|
-
|
|
124
|
-
// Convert glob patterns to actual directories (simple implementation)
|
|
125
|
-
const workspaceDirs = [];
|
|
126
|
-
for (const ws of workspaces) {
|
|
127
|
-
// Handle simple patterns like "packages/*" or "iterations/*"
|
|
128
|
-
if (ws.includes('*')) {
|
|
129
|
-
const baseDir = ws.split('*')[0];
|
|
130
|
-
const fullBaseDir = path.join(projectDir, baseDir);
|
|
131
|
-
|
|
132
|
-
if (fs.existsSync(fullBaseDir)) {
|
|
133
|
-
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
134
|
-
for (const entry of entries) {
|
|
135
|
-
if (entry.isDirectory()) {
|
|
136
|
-
const wsPath = path.join(fullBaseDir, entry.name);
|
|
137
|
-
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
138
|
-
workspaceDirs.push(wsPath);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
// Direct path
|
|
145
|
-
const wsPath = path.join(projectDir, ws);
|
|
146
|
-
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
147
|
-
workspaceDirs.push(wsPath);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return workspaceDirs;
|
|
153
|
+
return expandWorkspacePatterns(workspaces, projectDir);
|
|
153
154
|
} catch (error) {
|
|
154
155
|
return [];
|
|
155
156
|
}
|
|
@@ -171,35 +172,7 @@ function getPnpmWorkspaces(projectDir) {
|
|
|
171
172
|
const yaml = require('js-yaml');
|
|
172
173
|
const config = yaml.load(fs.readFileSync(pnpmFile, 'utf8'));
|
|
173
174
|
const workspacePatterns = config.packages || [];
|
|
174
|
-
|
|
175
|
-
// Convert glob patterns to actual directories
|
|
176
|
-
const workspaceDirs = [];
|
|
177
|
-
for (const pattern of workspacePatterns) {
|
|
178
|
-
if (pattern.includes('*')) {
|
|
179
|
-
const baseDir = pattern.split('*')[0];
|
|
180
|
-
const fullBaseDir = path.join(projectDir, baseDir);
|
|
181
|
-
|
|
182
|
-
if (fs.existsSync(fullBaseDir)) {
|
|
183
|
-
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
184
|
-
for (const entry of entries) {
|
|
185
|
-
if (entry.isDirectory()) {
|
|
186
|
-
const wsPath = path.join(fullBaseDir, entry.name);
|
|
187
|
-
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
188
|
-
workspaceDirs.push(wsPath);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
// Direct path
|
|
195
|
-
const wsPath = path.join(projectDir, pattern);
|
|
196
|
-
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
197
|
-
workspaceDirs.push(wsPath);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return workspaceDirs;
|
|
175
|
+
return expandWorkspacePatterns(workspacePatterns, projectDir);
|
|
203
176
|
} catch (error) {
|
|
204
177
|
return [];
|
|
205
178
|
}
|
|
@@ -220,35 +193,7 @@ function getLernaWorkspaces(projectDir) {
|
|
|
220
193
|
try {
|
|
221
194
|
const config = JSON.parse(fs.readFileSync(lernaFile, 'utf8'));
|
|
222
195
|
const workspacePatterns = config.packages || ['packages/*'];
|
|
223
|
-
|
|
224
|
-
// Convert glob patterns to actual directories
|
|
225
|
-
const workspaceDirs = [];
|
|
226
|
-
for (const pattern of workspacePatterns) {
|
|
227
|
-
if (pattern.includes('*')) {
|
|
228
|
-
const baseDir = pattern.split('*')[0];
|
|
229
|
-
const fullBaseDir = path.join(projectDir, baseDir);
|
|
230
|
-
|
|
231
|
-
if (fs.existsSync(fullBaseDir)) {
|
|
232
|
-
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
233
|
-
for (const entry of entries) {
|
|
234
|
-
if (entry.isDirectory()) {
|
|
235
|
-
const wsPath = path.join(fullBaseDir, entry.name);
|
|
236
|
-
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
237
|
-
workspaceDirs.push(wsPath);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
// Direct path
|
|
244
|
-
const wsPath = path.join(projectDir, pattern);
|
|
245
|
-
if (fs.existsSync(path.join(wsPath, 'package.json'))) {
|
|
246
|
-
workspaceDirs.push(wsPath);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return workspaceDirs;
|
|
196
|
+
return expandWorkspacePatterns(workspacePatterns, projectDir);
|
|
252
197
|
} catch (error) {
|
|
253
198
|
return [];
|
|
254
199
|
}
|
|
@@ -416,6 +361,7 @@ module.exports = {
|
|
|
416
361
|
getNpmWorkspaces,
|
|
417
362
|
getPnpmWorkspaces,
|
|
418
363
|
getLernaWorkspaces,
|
|
364
|
+
expandWorkspacePatterns,
|
|
419
365
|
checkHoistedDependency,
|
|
420
366
|
checkTypeScriptTestConfig,
|
|
421
367
|
generateRecommendations,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,mEA8HC;AAED;;;;;GAKG;AACH,kFAgdC;AAoCD;;;;;GAKG;AACH,0CAJW,MAAM,eAEJ,MAAM,CAkBlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,eAEJ,OAAO,CAKnB;AAnED;;;;;;GAMG;AACH,0EAFa,MAAM,CAclB;AAED;;;;GAIG;AACH,0CAHW,MAAM,GACJ,MAAM,CAQlB"}
|
|
@@ -5,6 +5,59 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { deriveBudget, checkBudgetCompliance } = require('../budget-derivation');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get actual budget statistics from git history
|
|
12
|
+
* Analyzes changes since last tag or initial commit
|
|
13
|
+
* @param {string} specDir - Project directory
|
|
14
|
+
* @returns {Object|null} Budget stats or null on failure
|
|
15
|
+
*/
|
|
16
|
+
function getActualBudgetStats(specDir) {
|
|
17
|
+
const cwd = specDir || process.cwd();
|
|
18
|
+
try {
|
|
19
|
+
// Get base ref (last tag or initial commit)
|
|
20
|
+
let baseRef;
|
|
21
|
+
try {
|
|
22
|
+
baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
|
|
23
|
+
cwd,
|
|
24
|
+
encoding: 'utf8'
|
|
25
|
+
}).trim();
|
|
26
|
+
} catch {
|
|
27
|
+
// No tags found, use initial commit
|
|
28
|
+
baseRef = execSync('git rev-list --max-parents=0 HEAD', {
|
|
29
|
+
cwd,
|
|
30
|
+
encoding: 'utf8'
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Count files changed since base ref
|
|
35
|
+
const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf8'
|
|
38
|
+
});
|
|
39
|
+
const files_changed = filesOutput.trim().split('\n').filter(Boolean).length;
|
|
40
|
+
|
|
41
|
+
// Count lines changed (added + removed)
|
|
42
|
+
const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: 'utf8'
|
|
45
|
+
});
|
|
46
|
+
let lines_changed = 0;
|
|
47
|
+
for (const line of numstatOutput.trim().split('\n').filter(Boolean)) {
|
|
48
|
+
const [added, removed] = line.split('\t');
|
|
49
|
+
// Handle binary files (shown as '-')
|
|
50
|
+
const addedNum = added === '-' ? 0 : parseInt(added, 10) || 0;
|
|
51
|
+
const removedNum = removed === '-' ? 0 : parseInt(removed, 10) || 0;
|
|
52
|
+
lines_changed += addedNum + removedNum;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { files_changed, lines_changed };
|
|
56
|
+
} catch {
|
|
57
|
+
// Git not available or not a repository
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
8
61
|
|
|
9
62
|
/**
|
|
10
63
|
* Basic validation of working spec
|
|
@@ -511,14 +564,14 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
511
564
|
try {
|
|
512
565
|
const derivedBudget = deriveBudget(spec, projectRoot);
|
|
513
566
|
|
|
514
|
-
//
|
|
515
|
-
const
|
|
516
|
-
files_changed:
|
|
517
|
-
lines_changed:
|
|
518
|
-
risk_tier: spec.risk_tier,
|
|
567
|
+
// Get actual stats from git history
|
|
568
|
+
const actualStats = getActualBudgetStats(projectRoot) || {
|
|
569
|
+
files_changed: 0,
|
|
570
|
+
lines_changed: 0,
|
|
519
571
|
};
|
|
572
|
+
actualStats.risk_tier = spec.risk_tier;
|
|
520
573
|
|
|
521
|
-
budgetCheck = checkBudgetCompliance(derivedBudget,
|
|
574
|
+
budgetCheck = checkBudgetCompliance(derivedBudget, actualStats);
|
|
522
575
|
|
|
523
576
|
if (!budgetCheck.compliant) {
|
|
524
577
|
for (const violation of budgetCheck.violations) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a new git worktree with scope isolation
|
|
3
|
+
* @param {string} name - Worktree name
|
|
4
|
+
* @param {Object} options - Creation options
|
|
5
|
+
* @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
|
|
6
|
+
* @param {string} [options.baseBranch] - Base branch to create from
|
|
7
|
+
* @param {string} [options.specId] - Associated spec ID for standard+ modes
|
|
8
|
+
* @returns {Object} Created worktree info
|
|
9
|
+
*/
|
|
10
|
+
export function createWorktree(name: string, options?: {
|
|
11
|
+
scope?: string;
|
|
12
|
+
baseBranch?: string;
|
|
13
|
+
specId?: string;
|
|
14
|
+
}): any;
|
|
15
|
+
/**
|
|
16
|
+
* List all registered worktrees with filesystem validation
|
|
17
|
+
* @returns {Array} Worktree entries with status
|
|
18
|
+
*/
|
|
19
|
+
export function listWorktrees(): any[];
|
|
20
|
+
/**
|
|
21
|
+
* Destroy a worktree
|
|
22
|
+
* @param {string} name - Worktree name
|
|
23
|
+
* @param {Object} options - Destruction options
|
|
24
|
+
* @param {boolean} [options.deleteBranch] - Also delete the branch
|
|
25
|
+
* @param {boolean} [options.force] - Force removal even if dirty
|
|
26
|
+
*/
|
|
27
|
+
export function destroyWorktree(name: string, options?: {
|
|
28
|
+
deleteBranch?: boolean;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}): void;
|
|
31
|
+
/**
|
|
32
|
+
* Prune stale worktree entries
|
|
33
|
+
* @param {Object} options - Prune options
|
|
34
|
+
* @param {number} [options.maxAgeDays] - Remove entries older than this many days
|
|
35
|
+
* @returns {Array} Pruned entries
|
|
36
|
+
*/
|
|
37
|
+
export function pruneWorktrees(options?: {
|
|
38
|
+
maxAgeDays?: number;
|
|
39
|
+
}): any[];
|
|
40
|
+
/**
|
|
41
|
+
* Load the worktree registry
|
|
42
|
+
* @param {string} root - Repository root
|
|
43
|
+
* @returns {Object} Registry object
|
|
44
|
+
*/
|
|
45
|
+
export function loadRegistry(root: string): any;
|
|
46
|
+
/**
|
|
47
|
+
* Get the git repository root
|
|
48
|
+
* @returns {string} Absolute path to repo root
|
|
49
|
+
*/
|
|
50
|
+
export function getRepoRoot(): string;
|
|
51
|
+
export const WORKTREES_DIR: ".caws/worktrees";
|
|
52
|
+
export const REGISTRY_FILE: ".caws/worktrees.json";
|
|
53
|
+
export const BRANCH_PREFIX: "caws/";
|
|
54
|
+
//# sourceMappingURL=worktree-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-manager.d.ts","sourceRoot":"","sources":["../../src/worktree/worktree-manager.js"],"names":[],"mappings":"AA+DA;;;;;;;;GAQG;AACH,qCAPW,MAAM,YAEd;IAAyB,KAAK,GAAtB,MAAM;IACW,UAAU,GAA3B,MAAM;IACW,MAAM,GAAvB,MAAM;CACd,OA4IF;AAED;;;GAGG;AACH,uCAqCC;AAED;;;;;;GAMG;AACH,sCALW,MAAM,YAEd;IAA0B,YAAY,GAA9B,OAAO;IACW,KAAK,GAAvB,OAAO;CACjB,QAqDA;AAED;;;;;GAKG;AACH,yCAHG;IAAyB,UAAU,GAA3B,MAAM;CACd,SA6CF;AA1UD;;;;GAIG;AACH,mCAHW,MAAM,OAahB;AAnCD;;;GAGG;AACH,+BAFa,MAAM,CAMlB;AAZD,4BAAsB,iBAAiB,CAAC;AACxC,4BAAsB,sBAAsB,CAAC;AAC7C,4BAAsB,OAAO,CAAC"}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWS Git Worktree Manager
|
|
3
|
+
* Provides CRUD operations for git worktrees with scope isolation
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
const WORKTREES_DIR = '.caws/worktrees';
|
|
13
|
+
const REGISTRY_FILE = '.caws/worktrees.json';
|
|
14
|
+
const BRANCH_PREFIX = 'caws/';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the git repository root
|
|
18
|
+
* @returns {string} Absolute path to repo root
|
|
19
|
+
*/
|
|
20
|
+
function getRepoRoot() {
|
|
21
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
}).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get current branch name
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function getCurrentBranch() {
|
|
31
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load the worktree registry
|
|
38
|
+
* @param {string} root - Repository root
|
|
39
|
+
* @returns {Object} Registry object
|
|
40
|
+
*/
|
|
41
|
+
function loadRegistry(root) {
|
|
42
|
+
const registryPath = path.join(root, REGISTRY_FILE);
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(registryPath)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Corrupted registry, start fresh
|
|
49
|
+
}
|
|
50
|
+
return { version: 1, worktrees: {} };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save the worktree registry
|
|
55
|
+
* @param {string} root - Repository root
|
|
56
|
+
* @param {Object} registry - Registry object
|
|
57
|
+
*/
|
|
58
|
+
function saveRegistry(root, registry) {
|
|
59
|
+
const registryPath = path.join(root, REGISTRY_FILE);
|
|
60
|
+
fs.ensureDirSync(path.dirname(registryPath));
|
|
61
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a new git worktree with scope isolation
|
|
66
|
+
* @param {string} name - Worktree name
|
|
67
|
+
* @param {Object} options - Creation options
|
|
68
|
+
* @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
|
|
69
|
+
* @param {string} [options.baseBranch] - Base branch to create from
|
|
70
|
+
* @param {string} [options.specId] - Associated spec ID for standard+ modes
|
|
71
|
+
* @returns {Object} Created worktree info
|
|
72
|
+
*/
|
|
73
|
+
function createWorktree(name, options = {}) {
|
|
74
|
+
const root = getRepoRoot();
|
|
75
|
+
const { scope, baseBranch, specId } = options;
|
|
76
|
+
|
|
77
|
+
// Validate name
|
|
78
|
+
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
79
|
+
throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const registry = loadRegistry(root);
|
|
83
|
+
|
|
84
|
+
// Check for duplicate
|
|
85
|
+
if (registry.worktrees[name]) {
|
|
86
|
+
throw new Error(`Worktree '${name}' already exists. Use 'caws worktree destroy ${name}' first.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const worktreePath = path.join(root, WORKTREES_DIR, name);
|
|
90
|
+
const branchName = BRANCH_PREFIX + name;
|
|
91
|
+
const base = baseBranch || getCurrentBranch();
|
|
92
|
+
|
|
93
|
+
// Create the worktree directory
|
|
94
|
+
fs.ensureDirSync(path.dirname(worktreePath));
|
|
95
|
+
|
|
96
|
+
// Create git worktree with new branch
|
|
97
|
+
try {
|
|
98
|
+
execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
|
|
99
|
+
cwd: root,
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Branch might already exist
|
|
104
|
+
if (error.message.includes('already exists')) {
|
|
105
|
+
execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
|
|
106
|
+
cwd: root,
|
|
107
|
+
stdio: 'pipe',
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Failed to create worktree: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Set up sparse checkout if scope is provided
|
|
115
|
+
if (scope) {
|
|
116
|
+
try {
|
|
117
|
+
execFileSync('git', ['sparse-checkout', 'init', '--cone'], {
|
|
118
|
+
cwd: worktreePath,
|
|
119
|
+
stdio: 'pipe',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Parse scope patterns (comma-separated)
|
|
123
|
+
const patterns = scope.split(',').map((p) => p.trim());
|
|
124
|
+
execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
|
|
125
|
+
cwd: worktreePath,
|
|
126
|
+
stdio: 'pipe',
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn(chalk.yellow(`⚠️ Sparse checkout setup failed: ${error.message}`));
|
|
130
|
+
console.warn(chalk.blue('💡 Worktree created but without sparse checkout'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Copy .caws/ config into worktree
|
|
135
|
+
const cawsSource = path.join(root, '.caws');
|
|
136
|
+
const cawsDest = path.join(worktreePath, '.caws');
|
|
137
|
+
if (fs.existsSync(cawsSource)) {
|
|
138
|
+
try {
|
|
139
|
+
fs.copySync(cawsSource, cawsDest, {
|
|
140
|
+
filter: (src) => {
|
|
141
|
+
// Don't copy worktrees directory or registry into the worktree
|
|
142
|
+
const rel = path.relative(cawsSource, src);
|
|
143
|
+
return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
// Non-fatal
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Generate working spec if in standard+ mode and specId provided
|
|
152
|
+
if (specId) {
|
|
153
|
+
try {
|
|
154
|
+
const { generateWorkingSpec } = require('../generators/working-spec');
|
|
155
|
+
const specContent = generateWorkingSpec({
|
|
156
|
+
projectId: specId,
|
|
157
|
+
projectTitle: `Worktree: ${name}`,
|
|
158
|
+
projectDescription: `Isolated worktree for ${name}`,
|
|
159
|
+
riskTier: 3,
|
|
160
|
+
projectMode: 'feature',
|
|
161
|
+
scopeIn: scope || 'src/',
|
|
162
|
+
scopeOut: 'node_modules/, dist/, build/',
|
|
163
|
+
maxFiles: 25,
|
|
164
|
+
maxLoc: 1000,
|
|
165
|
+
blastModules: scope || 'src',
|
|
166
|
+
dataMigration: false,
|
|
167
|
+
rollbackSlo: '5m',
|
|
168
|
+
projectThreats: '',
|
|
169
|
+
projectInvariants: 'System maintains data consistency',
|
|
170
|
+
acceptanceCriteria: 'Given current state, when action occurs, then expected result',
|
|
171
|
+
a11yRequirements: 'keyboard',
|
|
172
|
+
perfBudget: 250,
|
|
173
|
+
securityRequirements: 'validation',
|
|
174
|
+
contractType: '',
|
|
175
|
+
contractPath: '',
|
|
176
|
+
observabilityLogs: '',
|
|
177
|
+
observabilityMetrics: '',
|
|
178
|
+
observabilityTraces: '',
|
|
179
|
+
migrationPlan: '',
|
|
180
|
+
rollbackPlan: '',
|
|
181
|
+
needsOverride: false,
|
|
182
|
+
isExperimental: false,
|
|
183
|
+
aiConfidence: 0.8,
|
|
184
|
+
uncertaintyAreas: '',
|
|
185
|
+
complexityFactors: '',
|
|
186
|
+
});
|
|
187
|
+
const specPath = path.join(cawsDest, 'working-spec.yaml');
|
|
188
|
+
fs.ensureDirSync(path.dirname(specPath));
|
|
189
|
+
fs.writeFileSync(specPath, specContent);
|
|
190
|
+
} catch {
|
|
191
|
+
// Non-fatal: spec generation is optional
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Register worktree
|
|
196
|
+
const entry = {
|
|
197
|
+
name,
|
|
198
|
+
path: worktreePath,
|
|
199
|
+
branch: branchName,
|
|
200
|
+
baseBranch: base,
|
|
201
|
+
scope: scope || null,
|
|
202
|
+
specId: specId || null,
|
|
203
|
+
createdAt: new Date().toISOString(),
|
|
204
|
+
status: 'active',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
registry.worktrees[name] = entry;
|
|
208
|
+
saveRegistry(root, registry);
|
|
209
|
+
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* List all registered worktrees with filesystem validation
|
|
215
|
+
* @returns {Array} Worktree entries with status
|
|
216
|
+
*/
|
|
217
|
+
function listWorktrees() {
|
|
218
|
+
const root = getRepoRoot();
|
|
219
|
+
const registry = loadRegistry(root);
|
|
220
|
+
|
|
221
|
+
// Get actual git worktrees for validation
|
|
222
|
+
let gitWorktrees = [];
|
|
223
|
+
try {
|
|
224
|
+
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
225
|
+
cwd: root,
|
|
226
|
+
encoding: 'utf8',
|
|
227
|
+
});
|
|
228
|
+
gitWorktrees = output
|
|
229
|
+
.split('\n\n')
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.map((block) => {
|
|
232
|
+
const lines = block.split('\n');
|
|
233
|
+
const worktreeLine = lines.find((l) => l.startsWith('worktree '));
|
|
234
|
+
return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
|
|
235
|
+
})
|
|
236
|
+
.filter(Boolean);
|
|
237
|
+
} catch {
|
|
238
|
+
// Git worktree list failed
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const entries = Object.values(registry.worktrees).map((entry) => {
|
|
242
|
+
const exists = fs.existsSync(entry.path);
|
|
243
|
+
const inGit = gitWorktrees.some(
|
|
244
|
+
(wt) => path.resolve(wt) === path.resolve(entry.path)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...entry,
|
|
249
|
+
status: exists && inGit ? 'active' : exists ? 'orphaned' : 'missing',
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Destroy a worktree
|
|
258
|
+
* @param {string} name - Worktree name
|
|
259
|
+
* @param {Object} options - Destruction options
|
|
260
|
+
* @param {boolean} [options.deleteBranch] - Also delete the branch
|
|
261
|
+
* @param {boolean} [options.force] - Force removal even if dirty
|
|
262
|
+
*/
|
|
263
|
+
function destroyWorktree(name, options = {}) {
|
|
264
|
+
const root = getRepoRoot();
|
|
265
|
+
const registry = loadRegistry(root);
|
|
266
|
+
const { deleteBranch = false, force = false } = options;
|
|
267
|
+
|
|
268
|
+
const entry = registry.worktrees[name];
|
|
269
|
+
if (!entry) {
|
|
270
|
+
throw new Error(`Worktree '${name}' not found in registry`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Remove git worktree
|
|
274
|
+
try {
|
|
275
|
+
const args = ['worktree', 'remove'];
|
|
276
|
+
if (force) args.push('--force');
|
|
277
|
+
args.push(entry.path);
|
|
278
|
+
execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (force) {
|
|
281
|
+
// Force cleanup: remove directory manually
|
|
282
|
+
if (fs.existsSync(entry.path)) {
|
|
283
|
+
fs.removeSync(entry.path);
|
|
284
|
+
}
|
|
285
|
+
// Prune git worktree list
|
|
286
|
+
try {
|
|
287
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
288
|
+
} catch {
|
|
289
|
+
// Non-fatal
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Optionally delete branch
|
|
297
|
+
if (deleteBranch && entry.branch) {
|
|
298
|
+
try {
|
|
299
|
+
execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
300
|
+
} catch {
|
|
301
|
+
if (force) {
|
|
302
|
+
try {
|
|
303
|
+
execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
304
|
+
} catch {
|
|
305
|
+
// Non-fatal
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update registry
|
|
312
|
+
registry.worktrees[name].status = 'destroyed';
|
|
313
|
+
registry.worktrees[name].destroyedAt = new Date().toISOString();
|
|
314
|
+
saveRegistry(root, registry);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Prune stale worktree entries
|
|
319
|
+
* @param {Object} options - Prune options
|
|
320
|
+
* @param {number} [options.maxAgeDays] - Remove entries older than this many days
|
|
321
|
+
* @returns {Array} Pruned entries
|
|
322
|
+
*/
|
|
323
|
+
function pruneWorktrees(options = {}) {
|
|
324
|
+
const root = getRepoRoot();
|
|
325
|
+
const registry = loadRegistry(root);
|
|
326
|
+
const { maxAgeDays = 30 } = options;
|
|
327
|
+
|
|
328
|
+
const now = new Date();
|
|
329
|
+
const pruned = [];
|
|
330
|
+
|
|
331
|
+
for (const [name, entry] of Object.entries(registry.worktrees)) {
|
|
332
|
+
const created = new Date(entry.createdAt);
|
|
333
|
+
const ageDays = (now - created) / (1000 * 60 * 60 * 24);
|
|
334
|
+
|
|
335
|
+
const shouldPrune =
|
|
336
|
+
entry.status === 'destroyed' ||
|
|
337
|
+
(!fs.existsSync(entry.path) && ageDays > maxAgeDays) ||
|
|
338
|
+
(maxAgeDays === 0 && entry.status === 'destroyed');
|
|
339
|
+
|
|
340
|
+
if (shouldPrune) {
|
|
341
|
+
// Clean up filesystem if still exists
|
|
342
|
+
if (fs.existsSync(entry.path)) {
|
|
343
|
+
try {
|
|
344
|
+
execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
|
|
345
|
+
cwd: root,
|
|
346
|
+
stdio: 'pipe',
|
|
347
|
+
});
|
|
348
|
+
} catch {
|
|
349
|
+
fs.removeSync(entry.path);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
pruned.push(entry);
|
|
353
|
+
delete registry.worktrees[name];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Prune git's worktree list
|
|
358
|
+
try {
|
|
359
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
360
|
+
} catch {
|
|
361
|
+
// Non-fatal
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
saveRegistry(root, registry);
|
|
365
|
+
return pruned;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = {
|
|
369
|
+
createWorktree,
|
|
370
|
+
listWorktrees,
|
|
371
|
+
destroyWorktree,
|
|
372
|
+
pruneWorktrees,
|
|
373
|
+
loadRegistry,
|
|
374
|
+
getRepoRoot,
|
|
375
|
+
WORKTREES_DIR,
|
|
376
|
+
REGISTRY_FILE,
|
|
377
|
+
BRANCH_PREFIX,
|
|
378
|
+
};
|