@paths.design/caws-cli 3.4.0 → 4.0.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/budget-derivation.d.ts +41 -2
- package/dist/budget-derivation.d.ts.map +1 -1
- package/dist/budget-derivation.js +417 -30
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +105 -28
- package/dist/index.js +2 -0
- package/dist/policy/PolicyManager.d.ts +104 -0
- package/dist/policy/PolicyManager.d.ts.map +1 -0
- package/dist/policy/PolicyManager.js +399 -0
- package/dist/scaffold/cursor-hooks.d.ts.map +1 -1
- package/dist/scaffold/cursor-hooks.js +15 -0
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +27 -6
- package/dist/spec/SpecFileManager.d.ts +146 -0
- package/dist/spec/SpecFileManager.d.ts.map +1 -0
- package/dist/spec/SpecFileManager.js +419 -0
- package/dist/validation/spec-validation.d.ts +14 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +225 -13
- package/package.json +1 -1
- package/templates/.cursor/rules/01-claims-verification.mdc +144 -0
- package/templates/.cursor/rules/02-testing-standards.mdc +315 -0
- package/templates/.cursor/rules/03-infrastructure-standards.mdc +251 -0
- package/templates/.cursor/rules/04-documentation-integrity.mdc +291 -0
- package/templates/.cursor/rules/05-production-readiness-checklist.mdc +214 -0
- package/templates/.cursor/rules/README.md +64 -0
- package/templates/agents.md +6 -5
|
@@ -138,6 +138,21 @@ async function scaffoldCursorHooks(projectDir, levels = ['safety', 'quality', 's
|
|
|
138
138
|
await fs.copy(readmePath, path.join(cursorDir, 'README.md'));
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// Copy rules directory if it exists
|
|
142
|
+
const rulesTemplateDir = path.join(cursorTemplateDir, 'rules');
|
|
143
|
+
const rulesDestDir = path.join(cursorDir, 'rules');
|
|
144
|
+
if (fs.existsSync(rulesTemplateDir)) {
|
|
145
|
+
try {
|
|
146
|
+
await fs.ensureDir(rulesDestDir);
|
|
147
|
+
await fs.copy(rulesTemplateDir, rulesDestDir);
|
|
148
|
+
const ruleFiles = fs.readdirSync(rulesTemplateDir).filter((file) => file.endsWith('.mdc'));
|
|
149
|
+
console.log(chalk.green('✅ Cursor rules configured'));
|
|
150
|
+
console.log(chalk.gray(` Rules: ${ruleFiles.length} rule files installed`));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(chalk.yellow('⚠️ Failed to copy Cursor rules:'), error.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
console.log(chalk.green('✅ Cursor hooks configured'));
|
|
142
157
|
console.log(chalk.gray(` Enabled: ${levels.join(', ')}`));
|
|
143
158
|
console.log(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../src/scaffold/git-hooks.js"],"names":[],"mappings":"AASA;;;;GAIG;AACH,6CAHW,MAAM;;;
|
|
1
|
+
{"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../src/scaffold/git-hooks.js"],"names":[],"mappings":"AASA;;;;GAIG;AACH,6CAHW,MAAM;;;GAwGhB;AAoOD;;;GAGG;AACH,2CAFW,MAAM,iBAkChB;AAED;;;GAGG;AACH,gDAFW,MAAM,iBAgDhB"}
|
|
@@ -108,7 +108,8 @@ async function scaffoldGitHooks(projectDir, options = {}) {
|
|
|
108
108
|
if (addedCount > 0) {
|
|
109
109
|
console.log(`\n🔗 Git hooks configured: ${addedCount} hooks active`);
|
|
110
110
|
console.log('💡 Hooks will run automatically on git operations');
|
|
111
|
-
console.log('💡 Use --no-verify to skip hooks: git commit --no-verify');
|
|
111
|
+
console.log('💡 Use --no-verify to skip commit hooks: git commit --no-verify');
|
|
112
|
+
console.log('⚠️ Note: --no-verify is BLOCKED on git push for safety');
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
return { added: addedCount, skipped: skippedCount };
|
|
@@ -142,7 +143,7 @@ if command -v caws >/dev/null 2>&1; then
|
|
|
142
143
|
echo "✅ CAWS validation passed"
|
|
143
144
|
else
|
|
144
145
|
echo "❌ CAWS validation failed"
|
|
145
|
-
echo "💡 Fix issues or
|
|
146
|
+
echo "💡 Fix issues or skip with: git commit --no-verify (allowed)"
|
|
146
147
|
exit 1
|
|
147
148
|
fi
|
|
148
149
|
else
|
|
@@ -161,7 +162,8 @@ if [ -f "package.json" ]; then
|
|
|
161
162
|
if npx eslint . --quiet; then
|
|
162
163
|
echo "✅ ESLint passed"
|
|
163
164
|
else
|
|
164
|
-
echo "❌ ESLint failed
|
|
165
|
+
echo "❌ ESLint failed"
|
|
166
|
+
echo "💡 Fix issues or skip with: git commit --no-verify (allowed)"
|
|
165
167
|
exit 1
|
|
166
168
|
fi
|
|
167
169
|
fi
|
|
@@ -172,7 +174,8 @@ if [ -f "package.json" ]; then
|
|
|
172
174
|
if npm test; then
|
|
173
175
|
echo "✅ Tests passed"
|
|
174
176
|
else
|
|
175
|
-
echo "❌ Tests failed
|
|
177
|
+
echo "❌ Tests failed"
|
|
178
|
+
echo "💡 Fix issues or skip with: git commit --no-verify (allowed)"
|
|
176
179
|
exit 1
|
|
177
180
|
fi
|
|
178
181
|
fi
|
|
@@ -227,14 +230,32 @@ function generatePostCommitHook() {
|
|
|
227
230
|
|
|
228
231
|
/**
|
|
229
232
|
* Generate pre-push hook content
|
|
233
|
+
* Blocks --no-verify to enforce quality gates before pushing
|
|
230
234
|
*/
|
|
231
235
|
function generatePrePushHook() {
|
|
232
236
|
return `#!/bin/bash
|
|
233
237
|
# CAWS Pre-push Hook
|
|
234
238
|
# Runs comprehensive checks before pushing
|
|
239
|
+
# BLOCKS --no-verify for safety
|
|
235
240
|
|
|
236
241
|
set -e
|
|
237
242
|
|
|
243
|
+
# Block --no-verify on push operations
|
|
244
|
+
for arg in "$@"; do
|
|
245
|
+
if [[ "$arg" == "--no-verify" ]] || [[ "$arg" == "-n" ]]; then
|
|
246
|
+
echo "❌ Error: --no-verify is BLOCKED on git push"
|
|
247
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
248
|
+
echo "Push operations must pass all quality gates."
|
|
249
|
+
echo ""
|
|
250
|
+
echo "💡 To fix issues locally:"
|
|
251
|
+
echo " 1. Run: caws validate"
|
|
252
|
+
echo " 2. Fix reported issues"
|
|
253
|
+
echo " 3. Commit fixes: git commit --no-verify (allowed)"
|
|
254
|
+
echo " 4. Push again: git push (no --no-verify)"
|
|
255
|
+
exit 1
|
|
256
|
+
fi
|
|
257
|
+
done
|
|
258
|
+
|
|
238
259
|
echo "🚀 CAWS Pre-push Validation"
|
|
239
260
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
240
261
|
|
|
@@ -251,7 +272,8 @@ if command -v caws >/dev/null 2>&1; then
|
|
|
251
272
|
echo "✅ CAWS validation passed"
|
|
252
273
|
else
|
|
253
274
|
echo "❌ CAWS validation failed"
|
|
254
|
-
echo "💡 Fix issues
|
|
275
|
+
echo "💡 Fix issues locally, then push again"
|
|
276
|
+
echo "💡 You can commit fixes with: git commit --no-verify"
|
|
255
277
|
exit 1
|
|
256
278
|
fi
|
|
257
279
|
fi
|
|
@@ -267,7 +289,6 @@ if [ -f "package.json" ]; then
|
|
|
267
289
|
else
|
|
268
290
|
echo "⚠️ Security vulnerabilities found"
|
|
269
291
|
echo "💡 Review with: npm audit"
|
|
270
|
-
echo "💡 Use --no-verify to push anyway"
|
|
271
292
|
# Don't fail on warnings, just warn
|
|
272
293
|
fi
|
|
273
294
|
fi
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec File Manager - Handles WorkingSpec file operations and YAML conversion
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Bidirectional WorkingSpec ↔ YAML conversion
|
|
6
|
+
* - Temporary file support for validation workflows
|
|
7
|
+
* - Backup/restore capabilities
|
|
8
|
+
* - Automatic cleanup of old temporary files
|
|
9
|
+
*/
|
|
10
|
+
export class SpecFileManager {
|
|
11
|
+
constructor(config?: {});
|
|
12
|
+
projectRoot: any;
|
|
13
|
+
useTemporaryFiles: any;
|
|
14
|
+
tempDir: any;
|
|
15
|
+
/**
|
|
16
|
+
* Convert WorkingSpec object to YAML string
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} spec - WorkingSpec to convert
|
|
19
|
+
* @returns {string} YAML string representation
|
|
20
|
+
*/
|
|
21
|
+
specToYaml(spec: any): string;
|
|
22
|
+
/**
|
|
23
|
+
* Parse YAML string to WorkingSpec object
|
|
24
|
+
*
|
|
25
|
+
* @param {string} yamlContent - YAML string to parse
|
|
26
|
+
* @returns {Object} Parsed WorkingSpec object
|
|
27
|
+
* @throws {Error} If YAML is invalid or doesn't match WorkingSpec schema
|
|
28
|
+
*/
|
|
29
|
+
yamlToSpec(yamlContent: string): any;
|
|
30
|
+
/**
|
|
31
|
+
* Get path to .caws/working-spec.yaml in project
|
|
32
|
+
*
|
|
33
|
+
* @returns {string} Absolute path to working spec file
|
|
34
|
+
*/
|
|
35
|
+
getSpecFilePath(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Check if working spec file exists
|
|
38
|
+
*
|
|
39
|
+
* @returns {Promise<boolean>} True if file exists
|
|
40
|
+
*/
|
|
41
|
+
specFileExists(): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Read working spec from .caws/working-spec.yaml
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<Object>} Parsed WorkingSpec object
|
|
46
|
+
* @throws {Error} If file doesn't exist or is invalid
|
|
47
|
+
*/
|
|
48
|
+
readSpecFile(): Promise<any>;
|
|
49
|
+
/**
|
|
50
|
+
* Write WorkingSpec to file
|
|
51
|
+
*
|
|
52
|
+
* Writes to .caws/working-spec.yaml or a temporary file based on configuration.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} spec - WorkingSpec to write
|
|
55
|
+
* @param {Object} options - Write options
|
|
56
|
+
* @param {boolean} options.useTemp - Override temp file usage
|
|
57
|
+
* @param {boolean} options.backup - Create backup before writing
|
|
58
|
+
* @returns {Promise<Object>} Write result with file path and cleanup function
|
|
59
|
+
*/
|
|
60
|
+
writeSpecFile(spec: any, options?: {
|
|
61
|
+
useTemp: boolean;
|
|
62
|
+
backup: boolean;
|
|
63
|
+
}): Promise<any>;
|
|
64
|
+
/**
|
|
65
|
+
* Update existing working spec file
|
|
66
|
+
*
|
|
67
|
+
* Reads current spec, merges changes, and writes back.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} updates - Partial WorkingSpec with fields to update
|
|
70
|
+
* @returns {Promise<Object>} Updated WorkingSpec
|
|
71
|
+
*/
|
|
72
|
+
updateSpecFile(updates: any): Promise<any>;
|
|
73
|
+
/**
|
|
74
|
+
* Create backup of working spec
|
|
75
|
+
*
|
|
76
|
+
* @returns {Promise<string>} Path to backup file
|
|
77
|
+
*/
|
|
78
|
+
backupSpecFile(): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Restore working spec from backup
|
|
81
|
+
*
|
|
82
|
+
* @param {string} backupPath - Path to backup file
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
restoreSpecFile(backupPath: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* List all backup files
|
|
88
|
+
*
|
|
89
|
+
* @returns {Promise<string[]>} Array of backup file paths
|
|
90
|
+
*/
|
|
91
|
+
listBackups(): Promise<string[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Delete old backup files
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} options - Cleanup options
|
|
96
|
+
* @param {number} options.maxAge - Maximum age in milliseconds (default: 7 days)
|
|
97
|
+
* @param {number} options.keep - Minimum number of backups to keep (default: 5)
|
|
98
|
+
* @returns {Promise<number>} Number of backups deleted
|
|
99
|
+
*/
|
|
100
|
+
cleanupBackups(options?: {
|
|
101
|
+
maxAge: number;
|
|
102
|
+
keep: number;
|
|
103
|
+
}): Promise<number>;
|
|
104
|
+
/**
|
|
105
|
+
* Validate spec file exists and is parseable
|
|
106
|
+
*
|
|
107
|
+
* @returns {Promise<Object>} Validation result
|
|
108
|
+
*/
|
|
109
|
+
validateSpecFile(): Promise<any>;
|
|
110
|
+
/**
|
|
111
|
+
* Clean up old temporary spec files
|
|
112
|
+
*
|
|
113
|
+
* Removes temp files older than specified age.
|
|
114
|
+
*
|
|
115
|
+
* @param {number} maxAge - Maximum age in milliseconds (default: 1 hour)
|
|
116
|
+
* @returns {Promise<number>} Number of files cleaned up
|
|
117
|
+
*/
|
|
118
|
+
cleanupTempFiles(maxAge?: number): Promise<number>;
|
|
119
|
+
/**
|
|
120
|
+
* Get spec file stats (size, modified date, etc.)
|
|
121
|
+
*
|
|
122
|
+
* @returns {Promise<Object>} File stats
|
|
123
|
+
*/
|
|
124
|
+
getSpecFileStats(): Promise<any>;
|
|
125
|
+
/**
|
|
126
|
+
* Create a new SpecFileManager instance with different configuration
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} config - New configuration
|
|
129
|
+
* @returns {SpecFileManager} New instance
|
|
130
|
+
*/
|
|
131
|
+
withConfig(config: any): SpecFileManager;
|
|
132
|
+
}
|
|
133
|
+
export const defaultSpecFileManager: SpecFileManager;
|
|
134
|
+
/**
|
|
135
|
+
* Create a SpecFileManager instance with default configuration
|
|
136
|
+
*
|
|
137
|
+
* @param {string} projectRoot - Project root directory
|
|
138
|
+
* @param {Object} options - Additional options
|
|
139
|
+
* @returns {SpecFileManager} SpecFileManager instance
|
|
140
|
+
*/
|
|
141
|
+
export function createSpecFileManager(projectRoot: string, options?: any): SpecFileManager;
|
|
142
|
+
export declare function specToYaml(spec: any): string;
|
|
143
|
+
export declare function yamlToSpec(yaml: any): any;
|
|
144
|
+
export declare function readSpecFile(projectRoot: any): Promise<any>;
|
|
145
|
+
export declare function writeSpecFile(spec: any, projectRoot: any, options: any): Promise<any>;
|
|
146
|
+
//# sourceMappingURL=SpecFileManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpecFileManager.d.ts","sourceRoot":"","sources":["../../src/spec/SpecFileManager.js"],"names":[],"mappings":"AAaA;;;;;;;;GAQG;AACH;IACE,yBAIC;IAHC,iBAAsD;IACtD,uBAA0D;IAC1D,aAA4C;IAG9C;;;;;OAKG;IACH,uBAFa,MAAM,CASlB;IAED;;;;;;OAMG;IACH,wBAJW,MAAM,OAqBhB;IAED;;;;OAIG;IACH,mBAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,kBAFa,OAAO,CAAC,OAAO,CAAC,CAS5B;IAED;;;;;OAKG;IACH,gBAHa,OAAO,KAAQ,CAe3B;IAED;;;;;;;;;;OAUG;IACH,mCAJG;QAAyB,OAAO,EAAxB,OAAO;QACU,MAAM,EAAvB,OAAO;KACf,GAAU,OAAO,KAAQ,CA2C3B;IAED;;;;;;;OAOG;IACH,8BAFa,OAAO,KAAQ,CAa3B;IAED;;;;OAIG;IACH,kBAFa,OAAO,CAAC,MAAM,CAAC,CAS3B;IAED;;;;;OAKG;IACH,4BAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAKzB;IAED;;;;OAIG;IACH,eAFa,OAAO,CAAC,MAAM,EAAE,CAAC,CAoB7B;IAED;;;;;;;OAOG;IACH,yBAJG;QAAwB,MAAM,EAAtB,MAAM;QACU,IAAI,EAApB,MAAM;KACd,GAAU,OAAO,CAAC,MAAM,CAAC,CA+B3B;IAED;;;;OAIG;IACH,oBAFa,OAAO,KAAQ,CAe3B;IAED;;;;;;;OAOG;IACH,0BAHW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CA6B3B;IAED;;;;OAIG;IACH,oBAFa,OAAO,KAAQ,CA0B3B;IAED;;;;;OAKG;IACH,yBAFa,eAAe,CAS3B;CACF;AAiBD,qDAAqD;AAfrD;;;;;;GAMG;AACH,mDAJW,MAAM,kBAEJ,eAAe,CAO3B;AAWa,sDAAiD;AACjD,mDAAiD;AAC/C,qEAKb;AACc,+FAKd"}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Spec File Manager - WorkingSpec ↔ YAML conversion and file management
|
|
3
|
+
* Handles conversion between JavaScript WorkingSpec objects and YAML files,
|
|
4
|
+
* manages .caws/working-spec.yaml lifecycle, and provides temporary file utilities.
|
|
5
|
+
* Ported from agent-agency v2 CAWS integration patterns.
|
|
6
|
+
* @author @darianrosebrook
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Spec File Manager - Handles WorkingSpec file operations and YAML conversion
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Bidirectional WorkingSpec ↔ YAML conversion
|
|
19
|
+
* - Temporary file support for validation workflows
|
|
20
|
+
* - Backup/restore capabilities
|
|
21
|
+
* - Automatic cleanup of old temporary files
|
|
22
|
+
*/
|
|
23
|
+
class SpecFileManager {
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.projectRoot = config.projectRoot || process.cwd();
|
|
26
|
+
this.useTemporaryFiles = config.useTemporaryFiles ?? false;
|
|
27
|
+
this.tempDir = config.tempDir || os.tmpdir();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert WorkingSpec object to YAML string
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} spec - WorkingSpec to convert
|
|
34
|
+
* @returns {string} YAML string representation
|
|
35
|
+
*/
|
|
36
|
+
specToYaml(spec) {
|
|
37
|
+
return yaml.dump(spec, {
|
|
38
|
+
indent: 2,
|
|
39
|
+
lineWidth: 100,
|
|
40
|
+
noRefs: true,
|
|
41
|
+
sortKeys: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse YAML string to WorkingSpec object
|
|
47
|
+
*
|
|
48
|
+
* @param {string} yamlContent - YAML string to parse
|
|
49
|
+
* @returns {Object} Parsed WorkingSpec object
|
|
50
|
+
* @throws {Error} If YAML is invalid or doesn't match WorkingSpec schema
|
|
51
|
+
*/
|
|
52
|
+
yamlToSpec(yamlContent) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = yaml.load(yamlContent);
|
|
55
|
+
|
|
56
|
+
// Basic validation
|
|
57
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
58
|
+
throw new Error('Invalid YAML: not an object');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!parsed.id || !parsed.title || !parsed.risk_tier) {
|
|
62
|
+
throw new Error('Invalid WorkingSpec: missing required fields (id, title, risk_tier)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return parsed;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`Failed to parse YAML: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get path to .caws/working-spec.yaml in project
|
|
73
|
+
*
|
|
74
|
+
* @returns {string} Absolute path to working spec file
|
|
75
|
+
*/
|
|
76
|
+
getSpecFilePath() {
|
|
77
|
+
return path.join(this.projectRoot, '.caws', 'working-spec.yaml');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if working spec file exists
|
|
82
|
+
*
|
|
83
|
+
* @returns {Promise<boolean>} True if file exists
|
|
84
|
+
*/
|
|
85
|
+
async specFileExists() {
|
|
86
|
+
try {
|
|
87
|
+
await fs.access(this.getSpecFilePath());
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read working spec from .caws/working-spec.yaml
|
|
96
|
+
*
|
|
97
|
+
* @returns {Promise<Object>} Parsed WorkingSpec object
|
|
98
|
+
* @throws {Error} If file doesn't exist or is invalid
|
|
99
|
+
*/
|
|
100
|
+
async readSpecFile() {
|
|
101
|
+
const specPath = this.getSpecFilePath();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const content = await fs.readFile(specPath, 'utf-8');
|
|
105
|
+
return this.yamlToSpec(content);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code === 'ENOENT') {
|
|
108
|
+
throw new Error(`Working spec not found: ${specPath}\nRun 'caws init' to create it`);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Write WorkingSpec to file
|
|
116
|
+
*
|
|
117
|
+
* Writes to .caws/working-spec.yaml or a temporary file based on configuration.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} spec - WorkingSpec to write
|
|
120
|
+
* @param {Object} options - Write options
|
|
121
|
+
* @param {boolean} options.useTemp - Override temp file usage
|
|
122
|
+
* @param {boolean} options.backup - Create backup before writing
|
|
123
|
+
* @returns {Promise<Object>} Write result with file path and cleanup function
|
|
124
|
+
*/
|
|
125
|
+
async writeSpecFile(spec, options = {}) {
|
|
126
|
+
const yamlContent = this.specToYaml(spec);
|
|
127
|
+
const useTemp = options.useTemp ?? this.useTemporaryFiles;
|
|
128
|
+
|
|
129
|
+
if (useTemp) {
|
|
130
|
+
// Write to temporary file
|
|
131
|
+
const tempPath = path.join(this.tempDir, `caws-spec-${spec.id || 'temp'}-${Date.now()}.yaml`);
|
|
132
|
+
|
|
133
|
+
await fs.writeFile(tempPath, yamlContent, 'utf-8');
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
filePath: tempPath,
|
|
137
|
+
isTemporary: true,
|
|
138
|
+
cleanup: async () => {
|
|
139
|
+
try {
|
|
140
|
+
await fs.unlink(tempPath);
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore cleanup errors (file may already be deleted)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
// Write to project .caws directory
|
|
148
|
+
const specPath = this.getSpecFilePath();
|
|
149
|
+
const cawsDir = path.dirname(specPath);
|
|
150
|
+
|
|
151
|
+
// Create backup if requested
|
|
152
|
+
if (options.backup && (await this.specFileExists())) {
|
|
153
|
+
await this.backupSpecFile();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Ensure .caws directory exists
|
|
157
|
+
await fs.mkdir(cawsDir, { recursive: true });
|
|
158
|
+
|
|
159
|
+
await fs.writeFile(specPath, yamlContent, 'utf-8');
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
filePath: specPath,
|
|
163
|
+
isTemporary: false,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Update existing working spec file
|
|
170
|
+
*
|
|
171
|
+
* Reads current spec, merges changes, and writes back.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} updates - Partial WorkingSpec with fields to update
|
|
174
|
+
* @returns {Promise<Object>} Updated WorkingSpec
|
|
175
|
+
*/
|
|
176
|
+
async updateSpecFile(updates) {
|
|
177
|
+
const currentSpec = await this.readSpecFile();
|
|
178
|
+
const updatedSpec = {
|
|
179
|
+
...currentSpec,
|
|
180
|
+
...updates,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Always write to permanent location for updates
|
|
184
|
+
await this.writeSpecFile(updatedSpec, { useTemp: false });
|
|
185
|
+
|
|
186
|
+
return updatedSpec;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create backup of working spec
|
|
191
|
+
*
|
|
192
|
+
* @returns {Promise<string>} Path to backup file
|
|
193
|
+
*/
|
|
194
|
+
async backupSpecFile() {
|
|
195
|
+
const specPath = this.getSpecFilePath();
|
|
196
|
+
const backupPath = `${specPath}.backup-${Date.now()}`;
|
|
197
|
+
|
|
198
|
+
await fs.copyFile(specPath, backupPath);
|
|
199
|
+
|
|
200
|
+
return backupPath;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Restore working spec from backup
|
|
205
|
+
*
|
|
206
|
+
* @param {string} backupPath - Path to backup file
|
|
207
|
+
* @returns {Promise<void>}
|
|
208
|
+
*/
|
|
209
|
+
async restoreSpecFile(backupPath) {
|
|
210
|
+
const specPath = this.getSpecFilePath();
|
|
211
|
+
await fs.copyFile(backupPath, specPath);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* List all backup files
|
|
216
|
+
*
|
|
217
|
+
* @returns {Promise<string[]>} Array of backup file paths
|
|
218
|
+
*/
|
|
219
|
+
async listBackups() {
|
|
220
|
+
const specPath = this.getSpecFilePath();
|
|
221
|
+
const cawsDir = path.dirname(specPath);
|
|
222
|
+
const specName = path.basename(specPath);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const files = await fs.readdir(cawsDir);
|
|
226
|
+
const backups = files
|
|
227
|
+
.filter((f) => f.startsWith(`${specName}.backup-`))
|
|
228
|
+
.map((f) => path.join(cawsDir, f));
|
|
229
|
+
|
|
230
|
+
// Sort by timestamp (newest first)
|
|
231
|
+
backups.sort().reverse();
|
|
232
|
+
|
|
233
|
+
return backups;
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Delete old backup files
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} options - Cleanup options
|
|
243
|
+
* @param {number} options.maxAge - Maximum age in milliseconds (default: 7 days)
|
|
244
|
+
* @param {number} options.keep - Minimum number of backups to keep (default: 5)
|
|
245
|
+
* @returns {Promise<number>} Number of backups deleted
|
|
246
|
+
*/
|
|
247
|
+
async cleanupBackups(options = {}) {
|
|
248
|
+
const maxAge = options.maxAge ?? 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
249
|
+
const keepCount = options.keep ?? 5;
|
|
250
|
+
|
|
251
|
+
const backups = await this.listBackups();
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
let deleted = 0;
|
|
254
|
+
|
|
255
|
+
// Delete old backups beyond the keep count
|
|
256
|
+
for (let i = 0; i < backups.length; i++) {
|
|
257
|
+
const backupPath = backups[i];
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const stats = await fs.stat(backupPath);
|
|
261
|
+
const age = now - stats.mtimeMs;
|
|
262
|
+
|
|
263
|
+
// Keep the most recent N backups, or delete if too old
|
|
264
|
+
const shouldDelete = i >= keepCount && age > maxAge;
|
|
265
|
+
|
|
266
|
+
if (shouldDelete) {
|
|
267
|
+
await fs.unlink(backupPath);
|
|
268
|
+
deleted++;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Skip files that can't be accessed
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return deleted;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Validate spec file exists and is parseable
|
|
280
|
+
*
|
|
281
|
+
* @returns {Promise<Object>} Validation result
|
|
282
|
+
*/
|
|
283
|
+
async validateSpecFile() {
|
|
284
|
+
try {
|
|
285
|
+
const spec = await this.readSpecFile();
|
|
286
|
+
return {
|
|
287
|
+
valid: true,
|
|
288
|
+
spec,
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return {
|
|
292
|
+
valid: false,
|
|
293
|
+
error: error.message,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Clean up old temporary spec files
|
|
300
|
+
*
|
|
301
|
+
* Removes temp files older than specified age.
|
|
302
|
+
*
|
|
303
|
+
* @param {number} maxAge - Maximum age in milliseconds (default: 1 hour)
|
|
304
|
+
* @returns {Promise<number>} Number of files cleaned up
|
|
305
|
+
*/
|
|
306
|
+
async cleanupTempFiles(maxAge = 3600000) {
|
|
307
|
+
try {
|
|
308
|
+
const files = await fs.readdir(this.tempDir);
|
|
309
|
+
const specFiles = files.filter((f) => f.startsWith('caws-spec-'));
|
|
310
|
+
|
|
311
|
+
let cleaned = 0;
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
|
|
314
|
+
for (const file of specFiles) {
|
|
315
|
+
const filePath = path.join(this.tempDir, file);
|
|
316
|
+
try {
|
|
317
|
+
const stats = await fs.stat(filePath);
|
|
318
|
+
const age = now - stats.mtimeMs;
|
|
319
|
+
|
|
320
|
+
if (age > maxAge) {
|
|
321
|
+
await fs.unlink(filePath);
|
|
322
|
+
cleaned++;
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Skip files that can't be accessed
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return cleaned;
|
|
330
|
+
} catch {
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get spec file stats (size, modified date, etc.)
|
|
337
|
+
*
|
|
338
|
+
* @returns {Promise<Object>} File stats
|
|
339
|
+
*/
|
|
340
|
+
async getSpecFileStats() {
|
|
341
|
+
const specPath = this.getSpecFilePath();
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const stats = await fs.stat(specPath);
|
|
345
|
+
const content = await fs.readFile(specPath, 'utf-8');
|
|
346
|
+
const lines = content.split('\n').length;
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
exists: true,
|
|
350
|
+
size: stats.size,
|
|
351
|
+
sizeKB: Math.round((stats.size / 1024) * 10) / 10,
|
|
352
|
+
lines,
|
|
353
|
+
modified: stats.mtime,
|
|
354
|
+
created: stats.birthtime,
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error.code === 'ENOENT') {
|
|
358
|
+
return {
|
|
359
|
+
exists: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Create a new SpecFileManager instance with different configuration
|
|
368
|
+
*
|
|
369
|
+
* @param {Object} config - New configuration
|
|
370
|
+
* @returns {SpecFileManager} New instance
|
|
371
|
+
*/
|
|
372
|
+
withConfig(config) {
|
|
373
|
+
return new SpecFileManager({
|
|
374
|
+
projectRoot: this.projectRoot,
|
|
375
|
+
useTemporaryFiles: this.useTemporaryFiles,
|
|
376
|
+
tempDir: this.tempDir,
|
|
377
|
+
...config,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a SpecFileManager instance with default configuration
|
|
384
|
+
*
|
|
385
|
+
* @param {string} projectRoot - Project root directory
|
|
386
|
+
* @param {Object} options - Additional options
|
|
387
|
+
* @returns {SpecFileManager} SpecFileManager instance
|
|
388
|
+
*/
|
|
389
|
+
function createSpecFileManager(projectRoot, options = {}) {
|
|
390
|
+
return new SpecFileManager({
|
|
391
|
+
projectRoot,
|
|
392
|
+
...options,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Export singleton instance for convenience
|
|
397
|
+
const defaultSpecFileManager = new SpecFileManager();
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
SpecFileManager,
|
|
401
|
+
defaultSpecFileManager,
|
|
402
|
+
createSpecFileManager,
|
|
403
|
+
|
|
404
|
+
// Convenience exports for backward compatibility
|
|
405
|
+
specToYaml: (spec) => defaultSpecFileManager.specToYaml(spec),
|
|
406
|
+
yamlToSpec: (yaml) => defaultSpecFileManager.yamlToSpec(yaml),
|
|
407
|
+
readSpecFile: (projectRoot) => {
|
|
408
|
+
if (projectRoot) {
|
|
409
|
+
return createSpecFileManager(projectRoot).readSpecFile();
|
|
410
|
+
}
|
|
411
|
+
return defaultSpecFileManager.readSpecFile();
|
|
412
|
+
},
|
|
413
|
+
writeSpecFile: (spec, projectRoot, options) => {
|
|
414
|
+
if (projectRoot) {
|
|
415
|
+
return createSpecFileManager(projectRoot).writeSpecFile(spec, options);
|
|
416
|
+
}
|
|
417
|
+
return defaultSpecFileManager.writeSpecFile(spec, options);
|
|
418
|
+
},
|
|
419
|
+
};
|
|
@@ -26,4 +26,18 @@ export function getFieldSuggestion(field: string, _spec: any): string;
|
|
|
26
26
|
* @returns {boolean} Whether field can be auto-fixed
|
|
27
27
|
*/
|
|
28
28
|
export function canAutoFixField(field: string, _spec: any): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Calculate compliance score based on errors and warnings
|
|
31
|
+
* Score ranges from 0 (many issues) to 1 (perfect)
|
|
32
|
+
* @param {Array} errors - Validation errors
|
|
33
|
+
* @param {Array} warnings - Validation warnings
|
|
34
|
+
* @returns {number} Compliance score (0-1)
|
|
35
|
+
*/
|
|
36
|
+
export function calculateComplianceScore(errors: any[], warnings: any[]): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get compliance grade from score
|
|
39
|
+
* @param {number} score - Compliance score (0-1)
|
|
40
|
+
* @returns {string} Grade (A, B, C, D, F)
|
|
41
|
+
*/
|
|
42
|
+
export function getComplianceGrade(score: number): string;
|
|
29
43
|
//# sourceMappingURL=spec-validation.d.ts.map
|