@paths.design/caws-cli 2.0.1 → 3.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/index.d.ts.map +1 -1
- package/dist/index.js +1463 -121
- package/package.json +3 -2
- package/templates/agents.md +820 -0
- package/templates/apps/tools/caws/COMPLETION_REPORT.md +331 -0
- package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +360 -0
- package/templates/apps/tools/caws/README.md +463 -0
- package/templates/apps/tools/caws/TEST_STATUS.md +365 -0
- package/templates/apps/tools/caws/attest.js +357 -0
- package/templates/apps/tools/caws/ci-optimizer.js +642 -0
- package/templates/apps/tools/caws/config.ts +245 -0
- package/templates/apps/tools/caws/cross-functional.js +876 -0
- package/templates/apps/tools/caws/dashboard.js +1112 -0
- package/templates/apps/tools/caws/flake-detector.ts +362 -0
- package/templates/apps/tools/caws/gates.js +198 -0
- package/templates/apps/tools/caws/gates.ts +237 -0
- package/templates/apps/tools/caws/language-adapters.ts +381 -0
- package/templates/apps/tools/caws/language-support.d.ts +367 -0
- package/templates/apps/tools/caws/language-support.d.ts.map +1 -0
- package/templates/apps/tools/caws/language-support.js +585 -0
- package/templates/apps/tools/caws/legacy-assessment.ts +408 -0
- package/templates/apps/tools/caws/legacy-assessor.js +764 -0
- package/templates/apps/tools/caws/mutant-analyzer.js +734 -0
- package/templates/apps/tools/caws/perf-budgets.ts +349 -0
- package/templates/apps/tools/caws/prompt-lint.js.backup +274 -0
- package/templates/apps/tools/caws/property-testing.js +707 -0
- package/templates/apps/tools/caws/provenance.d.ts +14 -0
- package/templates/apps/tools/caws/provenance.d.ts.map +1 -0
- package/templates/apps/tools/caws/provenance.js +132 -0
- package/templates/apps/tools/caws/provenance.js.backup +73 -0
- package/templates/apps/tools/caws/provenance.ts +211 -0
- package/templates/apps/tools/caws/schemas/waivers.schema.json +30 -0
- package/templates/apps/tools/caws/schemas/working-spec.schema.json +115 -0
- package/templates/apps/tools/caws/scope-guard.js +208 -0
- package/templates/apps/tools/caws/security-provenance.ts +483 -0
- package/templates/apps/tools/caws/shared/base-tool.ts +281 -0
- package/templates/apps/tools/caws/shared/config-manager.ts +366 -0
- package/templates/apps/tools/caws/shared/gate-checker.ts +597 -0
- package/templates/apps/tools/caws/shared/types.ts +444 -0
- package/templates/apps/tools/caws/shared/validator.ts +305 -0
- package/templates/apps/tools/caws/shared/waivers-manager.ts +174 -0
- package/templates/apps/tools/caws/spec-test-mapper.ts +391 -0
- package/templates/apps/tools/caws/templates/working-spec.template.yml +60 -0
- package/templates/apps/tools/caws/test-quality.js +578 -0
- package/templates/apps/tools/caws/tools-allow.json +331 -0
- package/templates/apps/tools/caws/validate.js +76 -0
- package/templates/apps/tools/caws/validate.ts +228 -0
- package/templates/apps/tools/caws/waivers.js +344 -0
- package/templates/apps/tools/caws/waivers.yml +19 -0
- package/templates/codemod/README.md +1 -0
- package/templates/codemod/test.js +1 -0
- package/templates/docs/README.md +150 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAWS Validator
|
|
3
|
+
* Shared validation utilities for working specs, provenance, and other data
|
|
4
|
+
*
|
|
5
|
+
* @author @darianrosebrook
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import Ajv from 'ajv';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
12
|
+
import { ValidationResult, ContractValidationResult } from './types.js';
|
|
13
|
+
import { CawsBaseTool } from './base-tool.js';
|
|
14
|
+
|
|
15
|
+
export class CawsValidator extends CawsBaseTool {
|
|
16
|
+
private ajv: Ajv;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.ajv = new Ajv({
|
|
21
|
+
allErrors: true,
|
|
22
|
+
strict: false,
|
|
23
|
+
allowUnionTypes: true,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate a working spec file
|
|
29
|
+
*/
|
|
30
|
+
validateWorkingSpec(specPath: string): ValidationResult {
|
|
31
|
+
try {
|
|
32
|
+
// Read the working spec file
|
|
33
|
+
const specContent = fs.readFileSync(specPath, 'utf-8');
|
|
34
|
+
let spec: any;
|
|
35
|
+
|
|
36
|
+
// Try to parse as YAML first, then JSON
|
|
37
|
+
try {
|
|
38
|
+
spec = yaml.load(specContent);
|
|
39
|
+
} catch {
|
|
40
|
+
try {
|
|
41
|
+
spec = JSON.parse(specContent);
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
passed: false,
|
|
45
|
+
score: 0,
|
|
46
|
+
details: {},
|
|
47
|
+
errors: ['Invalid JSON/YAML format in working spec'],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load schema if available
|
|
53
|
+
const schemaPath = path.join(this.getCawsDirectory(), 'schemas/working-spec.schema.json');
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(schemaPath)) {
|
|
56
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
57
|
+
const schema = JSON.parse(schemaContent);
|
|
58
|
+
|
|
59
|
+
// Validate against schema
|
|
60
|
+
const validate = this.ajv.compile(schema);
|
|
61
|
+
const valid = validate(spec);
|
|
62
|
+
|
|
63
|
+
if (!valid) {
|
|
64
|
+
return {
|
|
65
|
+
passed: false,
|
|
66
|
+
errors: validate.errors?.map((err) => `${err.instancePath}: ${err.message}`) || [],
|
|
67
|
+
score: 0,
|
|
68
|
+
details: {},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Additional business logic validations
|
|
74
|
+
const warnings: string[] = [];
|
|
75
|
+
|
|
76
|
+
// Check risk tier thresholds
|
|
77
|
+
if (spec.risk_tier === 1 && spec.acceptance?.length < 5) {
|
|
78
|
+
warnings.push('Tier 1 specs should have at least 5 acceptance criteria');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (spec.risk_tier === 2 && spec.contracts?.length === 0) {
|
|
82
|
+
warnings.push('Tier 2 specs should have contract definitions');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for required non-functional requirements
|
|
86
|
+
const requiredNonFunctional = ['perf'];
|
|
87
|
+
const missingNonFunctional = requiredNonFunctional.filter(
|
|
88
|
+
(req) => !spec.non_functional?.[req]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (missingNonFunctional.length > 0) {
|
|
92
|
+
warnings.push(`Missing non-functional requirements: ${missingNonFunctional.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
passed: true,
|
|
97
|
+
score: 1,
|
|
98
|
+
details: {},
|
|
99
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
passed: false,
|
|
104
|
+
score: 0,
|
|
105
|
+
details: {},
|
|
106
|
+
errors: [`Validation failed: ${error}`],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validate a provenance file
|
|
113
|
+
*/
|
|
114
|
+
validateProvenance(provenancePath: string): ValidationResult {
|
|
115
|
+
try {
|
|
116
|
+
const provenanceContent = fs.readFileSync(provenancePath, 'utf-8');
|
|
117
|
+
const provenance = JSON.parse(provenanceContent);
|
|
118
|
+
|
|
119
|
+
// Basic structure validation
|
|
120
|
+
const requiredFields = ['agent', 'model', 'commit', 'artifacts', 'results', 'approvals'];
|
|
121
|
+
const missingFields = requiredFields.filter((field) => !provenance[field]);
|
|
122
|
+
|
|
123
|
+
if (missingFields.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
passed: false,
|
|
126
|
+
score: 0,
|
|
127
|
+
details: {},
|
|
128
|
+
errors: [`Missing required fields: ${missingFields.join(', ')}`],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate results structure
|
|
133
|
+
const requiredResults = ['coverage_branch', 'mutation_score', 'tests_passed'];
|
|
134
|
+
const missingResults = requiredResults.filter(
|
|
135
|
+
(field) => typeof provenance.results[field] !== 'number'
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (missingResults.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
passed: false,
|
|
141
|
+
score: 0,
|
|
142
|
+
details: {},
|
|
143
|
+
errors: [`Missing numeric results: ${missingResults.join(', ')}`],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
passed: true,
|
|
149
|
+
score: 1,
|
|
150
|
+
details: {},
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
passed: false,
|
|
155
|
+
score: 0,
|
|
156
|
+
details: {},
|
|
157
|
+
errors: [`Provenance validation failed: ${error}`],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate a JSON file against a schema
|
|
164
|
+
*/
|
|
165
|
+
validateJsonAgainstSchema(jsonPath: string, schemaPath: string): ValidationResult {
|
|
166
|
+
try {
|
|
167
|
+
// Read JSON file
|
|
168
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
|
|
169
|
+
const jsonData = JSON.parse(jsonContent);
|
|
170
|
+
|
|
171
|
+
// Read schema file
|
|
172
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
173
|
+
const schema = JSON.parse(schemaContent);
|
|
174
|
+
|
|
175
|
+
// Validate
|
|
176
|
+
const validate = this.ajv.compile(schema);
|
|
177
|
+
const valid = validate(jsonData);
|
|
178
|
+
|
|
179
|
+
if (!valid) {
|
|
180
|
+
return {
|
|
181
|
+
passed: false,
|
|
182
|
+
score: 0,
|
|
183
|
+
details: {},
|
|
184
|
+
errors: validate.errors?.map((err) => `${err.instancePath}: ${err.message}`) || [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
passed: true,
|
|
190
|
+
score: 1,
|
|
191
|
+
details: {},
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
passed: false,
|
|
196
|
+
score: 0,
|
|
197
|
+
details: {},
|
|
198
|
+
errors: [`Schema validation failed: ${error}`],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Validate a YAML file against a schema
|
|
205
|
+
*/
|
|
206
|
+
validateYamlAgainstSchema(yamlPath: string, schemaPath: string): ValidationResult {
|
|
207
|
+
try {
|
|
208
|
+
// Read YAML file
|
|
209
|
+
const yamlContent = fs.readFileSync(yamlPath, 'utf-8');
|
|
210
|
+
const yamlData = yaml.load(yamlContent);
|
|
211
|
+
|
|
212
|
+
// Read schema file
|
|
213
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
214
|
+
const schema = JSON.parse(schemaContent);
|
|
215
|
+
|
|
216
|
+
// Validate
|
|
217
|
+
const validate = this.ajv.compile(schema);
|
|
218
|
+
const valid = validate(yamlData);
|
|
219
|
+
|
|
220
|
+
if (!valid) {
|
|
221
|
+
return {
|
|
222
|
+
passed: false,
|
|
223
|
+
score: 0,
|
|
224
|
+
details: {},
|
|
225
|
+
errors: validate.errors?.map((err) => `${err.instancePath}: ${err.message}`) || [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
passed: true,
|
|
231
|
+
score: 1,
|
|
232
|
+
details: {},
|
|
233
|
+
};
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return {
|
|
236
|
+
passed: false,
|
|
237
|
+
score: 0,
|
|
238
|
+
details: {},
|
|
239
|
+
errors: [`YAML schema validation failed: ${error}`],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validate file exists and is readable
|
|
246
|
+
*/
|
|
247
|
+
validateFileExists(filePath: string): ValidationResult {
|
|
248
|
+
try {
|
|
249
|
+
if (!fs.existsSync(filePath)) {
|
|
250
|
+
return {
|
|
251
|
+
passed: false,
|
|
252
|
+
score: 0,
|
|
253
|
+
details: {},
|
|
254
|
+
errors: [`File not found: ${filePath}`],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try to read the file
|
|
259
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
260
|
+
return {
|
|
261
|
+
passed: true,
|
|
262
|
+
score: 1,
|
|
263
|
+
details: {},
|
|
264
|
+
};
|
|
265
|
+
} catch {
|
|
266
|
+
return {
|
|
267
|
+
passed: false,
|
|
268
|
+
score: 0,
|
|
269
|
+
details: {},
|
|
270
|
+
errors: [`File not readable: ${filePath}`],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validate directory exists and is writable
|
|
277
|
+
*/
|
|
278
|
+
validateDirectoryExists(dirPath: string): ValidationResult {
|
|
279
|
+
try {
|
|
280
|
+
if (!fs.existsSync(dirPath)) {
|
|
281
|
+
return {
|
|
282
|
+
passed: false,
|
|
283
|
+
score: 0,
|
|
284
|
+
details: {},
|
|
285
|
+
errors: [`Directory not found: ${dirPath}`],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Try to write to the directory
|
|
290
|
+
fs.accessSync(dirPath, fs.constants.W_OK);
|
|
291
|
+
return {
|
|
292
|
+
passed: true,
|
|
293
|
+
score: 1,
|
|
294
|
+
details: {},
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return {
|
|
298
|
+
passed: false,
|
|
299
|
+
score: 0,
|
|
300
|
+
details: {},
|
|
301
|
+
errors: [`Directory not writable: ${dirPath}`],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAWS Waivers Manager
|
|
3
|
+
* TypeScript wrapper for waivers management functionality
|
|
4
|
+
*
|
|
5
|
+
* @author @darianrosebrook
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
import { WaiverConfig } from './types.js';
|
|
12
|
+
import { CawsBaseTool } from './base-tool.js';
|
|
13
|
+
|
|
14
|
+
export class WaiversManager extends CawsBaseTool {
|
|
15
|
+
private readonly waiversPath: string;
|
|
16
|
+
|
|
17
|
+
constructor(waiversPath?: string) {
|
|
18
|
+
super();
|
|
19
|
+
this.waiversPath = waiversPath || path.join(this.getCawsDirectory(), 'waivers.yml');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load waivers configuration
|
|
24
|
+
*/
|
|
25
|
+
private loadWaiversConfig(): { waivers: WaiverConfig[] } {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(this.waiversPath)) {
|
|
28
|
+
return { waivers: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = fs.readFileSync(this.waiversPath, 'utf8');
|
|
32
|
+
return yaml.load(content) as { waivers: WaiverConfig[] };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.logError(`Error loading waivers config: ${error}`);
|
|
35
|
+
return { waivers: [] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Save waivers configuration
|
|
41
|
+
*/
|
|
42
|
+
private saveWaiversConfig(config: { waivers: WaiverConfig[] }): void {
|
|
43
|
+
try {
|
|
44
|
+
const yamlContent = yaml.dump(config, { indent: 2 });
|
|
45
|
+
fs.writeFileSync(this.waiversPath, yamlContent);
|
|
46
|
+
this.logSuccess(`Waivers configuration saved to ${this.waiversPath}`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
this.logError(`Error saving waivers config: ${error}`);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all waivers for a specific gate
|
|
55
|
+
*/
|
|
56
|
+
async getWaiversByGate(gate: string): Promise<WaiverConfig[]> {
|
|
57
|
+
const config = this.loadWaiversConfig();
|
|
58
|
+
const now = new Date();
|
|
59
|
+
|
|
60
|
+
return config.waivers.filter((waiver) => {
|
|
61
|
+
// Check if waiver covers this gate
|
|
62
|
+
if (waiver.gate !== gate) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if waiver is still active
|
|
67
|
+
const expiresAt = new Date(waiver.expiry);
|
|
68
|
+
if (now > expiresAt) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return waiver.status === 'active';
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check waiver status
|
|
78
|
+
*/
|
|
79
|
+
async checkWaiverStatus(waiverId: string): Promise<{
|
|
80
|
+
active: boolean;
|
|
81
|
+
waiver?: WaiverConfig;
|
|
82
|
+
reason?: string;
|
|
83
|
+
}> {
|
|
84
|
+
const config = this.loadWaiversConfig();
|
|
85
|
+
const now = new Date();
|
|
86
|
+
|
|
87
|
+
const waiver = config.waivers.find((w) => w.created_at === waiverId);
|
|
88
|
+
|
|
89
|
+
if (!waiver) {
|
|
90
|
+
return { active: false, reason: 'Waiver not found' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const expiresAt = new Date(waiver.expiry);
|
|
94
|
+
if (now > expiresAt) {
|
|
95
|
+
return { active: false, waiver, reason: 'Waiver expired' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (waiver.status !== 'active') {
|
|
99
|
+
return { active: false, waiver, reason: `Waiver status: ${waiver.status}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { active: true, waiver };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a new waiver
|
|
107
|
+
*/
|
|
108
|
+
async createWaiver(waiver: Omit<WaiverConfig, 'created_at'>): Promise<void> {
|
|
109
|
+
const config = this.loadWaiversConfig();
|
|
110
|
+
|
|
111
|
+
const newWaiver: WaiverConfig = {
|
|
112
|
+
...waiver,
|
|
113
|
+
created_at: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
config.waivers.push(newWaiver);
|
|
117
|
+
this.saveWaiversConfig(config);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Revoke a waiver
|
|
122
|
+
*/
|
|
123
|
+
async revokeWaiver(gate: string, owner: string): Promise<void> {
|
|
124
|
+
const config = this.loadWaiversConfig();
|
|
125
|
+
|
|
126
|
+
const waiver = config.waivers.find(
|
|
127
|
+
(w) => w.gate === gate && w.owner === owner && w.status === 'active'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (waiver) {
|
|
131
|
+
waiver.status = 'revoked';
|
|
132
|
+
this.saveWaiversConfig(config);
|
|
133
|
+
this.logSuccess(`Revoked waiver for gate: ${gate}`);
|
|
134
|
+
} else {
|
|
135
|
+
this.logWarning(`No active waiver found for gate: ${gate}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Cleanup expired waivers
|
|
141
|
+
*/
|
|
142
|
+
async cleanupExpiredWaivers(): Promise<number> {
|
|
143
|
+
const config = this.loadWaiversConfig();
|
|
144
|
+
const now = new Date();
|
|
145
|
+
|
|
146
|
+
const activeWaivers = config.waivers.filter((waiver) => {
|
|
147
|
+
const expiresAt = new Date(waiver.expiry);
|
|
148
|
+
return now <= expiresAt;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const removedCount = config.waivers.length - activeWaivers.length;
|
|
152
|
+
|
|
153
|
+
if (removedCount > 0) {
|
|
154
|
+
config.waivers = activeWaivers;
|
|
155
|
+
this.saveWaiversConfig(config);
|
|
156
|
+
this.logSuccess(`Cleaned up ${removedCount} expired waiver(s)`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return removedCount;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* List all active waivers
|
|
164
|
+
*/
|
|
165
|
+
async listActiveWaivers(): Promise<WaiverConfig[]> {
|
|
166
|
+
const config = this.loadWaiversConfig();
|
|
167
|
+
const now = new Date();
|
|
168
|
+
|
|
169
|
+
return config.waivers.filter((waiver) => {
|
|
170
|
+
const expiresAt = new Date(waiver.expiry);
|
|
171
|
+
return now <= expiresAt && waiver.status === 'active';
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|