kiro-kit 0.2.4 → 0.3.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 +35 -0
- package/dist/index.js +764 -112
- package/dist/index.js.map +1 -1
- package/dist/presets/backend/hooks/api-schema-validate.js +75 -0
- package/dist/presets/backend/hooks/endpoint-test-coverage.js +77 -0
- package/dist/presets/backend/hooks/migration-safety-check.js +68 -0
- package/dist/presets/backend/manifest.json +23 -0
- package/dist/presets/backend/powers.json +40 -0
- package/dist/presets/data-ai/hooks/data-drift-check.js +77 -0
- package/dist/presets/data-ai/hooks/experiment-log.js +87 -0
- package/dist/presets/data-ai/hooks/model-card-update.js +99 -0
- package/dist/presets/data-ai/manifest.json +23 -0
- package/dist/presets/data-ai/powers.json +34 -0
- package/dist/presets/devops/hooks/container-scan.js +73 -0
- package/dist/presets/devops/hooks/cost-estimation.js +69 -0
- package/dist/presets/devops/hooks/terraform-plan-review.js +67 -0
- package/dist/presets/devops/manifest.json +23 -0
- package/dist/presets/devops/powers.json +40 -0
- package/dist/presets/frontend/hooks/accessibility-check.js +76 -0
- package/dist/presets/frontend/hooks/bundle-size-guard.js +71 -0
- package/dist/presets/frontend/hooks/component-test-reminder.js +71 -0
- package/dist/presets/frontend/manifest.json +23 -0
- package/dist/presets/frontend/powers.json +34 -0
- package/dist/presets/fullstack/hooks/api-client-gen.js +69 -0
- package/dist/presets/fullstack/hooks/deployment-readiness.js +73 -0
- package/dist/presets/fullstack/hooks/type-sync-check.js +69 -0
- package/dist/presets/fullstack/manifest.json +23 -0
- package/dist/presets/fullstack/powers.json +46 -0
- package/dist/presets/mobile/hooks/asset-optimization.js +58 -0
- package/dist/presets/mobile/hooks/platform-parity-check.js +73 -0
- package/dist/presets/mobile/hooks/release-checklist.js +83 -0
- package/dist/presets/mobile/manifest.json +23 -0
- package/dist/presets/mobile/powers.json +34 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validates that route handler files export proper HTTP method handlers
|
|
4
|
+
* and use input validation (checks for zod/joi imports in route files).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const ROUTE_DIRS = ['src/app/api', 'src/routes', 'routes', 'api'];
|
|
11
|
+
const ROUTE_FILES = ['route.ts', 'route.js', 'index.ts', 'index.js'];
|
|
12
|
+
const VALIDATION_PATTERNS = [/from\s+['"]zod['"]/, /from\s+['"]joi['"]/, /require\(['"]zod['"]\)/, /require\(['"]joi['"]\)/];
|
|
13
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
14
|
+
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const issues = [];
|
|
17
|
+
|
|
18
|
+
function findRouteFiles(dir) {
|
|
19
|
+
const results = [];
|
|
20
|
+
if (!fs.existsSync(dir)) return results;
|
|
21
|
+
try {
|
|
22
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const full = path.join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
results.push(...findRouteFiles(full));
|
|
27
|
+
} else if (ROUTE_FILES.includes(entry.name)) {
|
|
28
|
+
results.push(full);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (e) { /* skip */ }
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let routeFiles = [];
|
|
36
|
+
for (const dir of ROUTE_DIRS) {
|
|
37
|
+
routeFiles.push(...findRouteFiles(path.resolve(cwd, dir)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (routeFiles.length === 0) {
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const file of routeFiles) {
|
|
45
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
46
|
+
const rel = path.relative(cwd, file);
|
|
47
|
+
|
|
48
|
+
// Check for exported HTTP method handlers
|
|
49
|
+
const hasMethod = HTTP_METHODS.some((m) =>
|
|
50
|
+
new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`).test(content)
|
|
51
|
+
);
|
|
52
|
+
if (!hasMethod) {
|
|
53
|
+
issues.push(`${rel}: No exported HTTP method handler found (GET, POST, etc.)`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for input validation on POST/PUT/PATCH handlers
|
|
58
|
+
const hasMutation = ['POST', 'PUT', 'PATCH'].some((m) =>
|
|
59
|
+
new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`).test(content)
|
|
60
|
+
);
|
|
61
|
+
if (hasMutation) {
|
|
62
|
+
const hasValidation = VALIDATION_PATTERNS.some((p) => p.test(content));
|
|
63
|
+
if (!hasValidation) {
|
|
64
|
+
issues.push(`${rel}: Mutation handler without input validation (zod/joi not imported)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (issues.length > 0) {
|
|
70
|
+
process.stdout.write('[api-schema-validate] Issues found:\n');
|
|
71
|
+
issues.forEach((i) => process.stdout.write(` - ${i}\n`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.exit(0);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks that API route files in src/app/api/ or routes/ have corresponding test files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const ROUTE_DIRS = ['src/app/api', 'src/routes', 'routes'];
|
|
10
|
+
const TEST_DIRS = ['__tests__', 'tests', 'test'];
|
|
11
|
+
const ROUTE_FILES = ['route.ts', 'route.js', 'index.ts', 'index.js'];
|
|
12
|
+
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
function findRoutes(dir) {
|
|
16
|
+
const results = [];
|
|
17
|
+
if (!fs.existsSync(dir)) return results;
|
|
18
|
+
try {
|
|
19
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const full = path.join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
results.push(...findRoutes(full));
|
|
24
|
+
} else if (ROUTE_FILES.includes(entry.name)) {
|
|
25
|
+
results.push({ file: full, dir: path.dirname(full) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch (e) { /* skip */ }
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let routes = [];
|
|
33
|
+
for (const dir of ROUTE_DIRS) {
|
|
34
|
+
routes.push(...findRoutes(path.resolve(cwd, dir)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (routes.length === 0) {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const missing = [];
|
|
42
|
+
|
|
43
|
+
for (const route of routes) {
|
|
44
|
+
const routeDir = route.dir;
|
|
45
|
+
const routeRel = path.relative(cwd, routeDir);
|
|
46
|
+
const routeName = path.basename(routeDir);
|
|
47
|
+
|
|
48
|
+
// Look for test files in various locations
|
|
49
|
+
const testPatterns = [
|
|
50
|
+
path.join(routeDir, `route.test.ts`),
|
|
51
|
+
path.join(routeDir, `route.test.js`),
|
|
52
|
+
path.join(routeDir, `${routeName}.test.ts`),
|
|
53
|
+
path.join(routeDir, `${routeName}.test.js`),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Also check parent __tests__ directories
|
|
57
|
+
for (const testDir of TEST_DIRS) {
|
|
58
|
+
testPatterns.push(path.join(routeDir, '..', testDir, `${routeName}.test.ts`));
|
|
59
|
+
testPatterns.push(path.join(routeDir, '..', testDir, `${routeName}.test.js`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hasTest = testPatterns.some((t) => fs.existsSync(t));
|
|
63
|
+
if (!hasTest) {
|
|
64
|
+
missing.push(routeRel);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
process.stdout.write(`[endpoint-test-coverage] ${missing.length} API route(s) without tests:\n`);
|
|
70
|
+
missing.slice(0, 10).forEach((m) => process.stdout.write(` - ${m}\n`));
|
|
71
|
+
if (missing.length > 10) {
|
|
72
|
+
process.stdout.write(` ... and ${missing.length - 10} more\n`);
|
|
73
|
+
}
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
process.exit(0);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scans migration files for dangerous operations (DROP TABLE, DROP COLUMN, TRUNCATE)
|
|
4
|
+
* and warns before execution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const MIGRATION_DIRS = ['prisma/migrations', 'drizzle', 'migrations', 'db/migrations', 'src/db/migrations'];
|
|
11
|
+
const DANGEROUS_PATTERNS = [
|
|
12
|
+
{ pattern: /DROP\s+TABLE/gi, label: 'DROP TABLE' },
|
|
13
|
+
{ pattern: /DROP\s+COLUMN/gi, label: 'DROP COLUMN' },
|
|
14
|
+
{ pattern: /ALTER\s+TABLE\s+\S+\s+DROP/gi, label: 'ALTER TABLE DROP' },
|
|
15
|
+
{ pattern: /TRUNCATE/gi, label: 'TRUNCATE' },
|
|
16
|
+
{ pattern: /DELETE\s+FROM\s+\S+\s*;/gi, label: 'DELETE without WHERE' },
|
|
17
|
+
];
|
|
18
|
+
const SQL_EXTENSIONS = ['.sql', '.ts', '.js'];
|
|
19
|
+
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
const warnings = [];
|
|
22
|
+
|
|
23
|
+
function findMigrationFiles(dir) {
|
|
24
|
+
const results = [];
|
|
25
|
+
if (!fs.existsSync(dir)) return results;
|
|
26
|
+
try {
|
|
27
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const full = path.join(dir, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
results.push(...findMigrationFiles(full));
|
|
32
|
+
} else if (SQL_EXTENSIONS.includes(path.extname(entry.name).toLowerCase())) {
|
|
33
|
+
results.push(full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (e) { /* skip */ }
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let migrationFiles = [];
|
|
41
|
+
for (const dir of MIGRATION_DIRS) {
|
|
42
|
+
migrationFiles.push(...findMigrationFiles(path.resolve(cwd, dir)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (migrationFiles.length === 0) {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const file of migrationFiles) {
|
|
50
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
51
|
+
const rel = path.relative(cwd, file);
|
|
52
|
+
|
|
53
|
+
for (const { pattern, label } of DANGEROUS_PATTERNS) {
|
|
54
|
+
const matches = content.match(pattern);
|
|
55
|
+
if (matches) {
|
|
56
|
+
warnings.push(`${rel}: Contains ${label} (${matches.length} occurrence(s))`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (warnings.length > 0) {
|
|
62
|
+
process.stdout.write('[migration-safety-check] Dangerous operations detected:\n');
|
|
63
|
+
warnings.forEach((w) => process.stdout.write(` - ${w}\n`));
|
|
64
|
+
process.stdout.write(' Review these changes carefully before applying.\n');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(0);
|
|
@@ -523,6 +523,29 @@
|
|
|
523
523
|
"type": "hook",
|
|
524
524
|
"executable": true
|
|
525
525
|
},
|
|
526
|
+
{
|
|
527
|
+
"source": "hooks/api-schema-validate.js",
|
|
528
|
+
"target": ".kiro/hooks/api-schema-validate.js",
|
|
529
|
+
"type": "hook",
|
|
530
|
+
"executable": true
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
"source": "hooks/migration-safety-check.js",
|
|
534
|
+
"target": ".kiro/hooks/migration-safety-check.js",
|
|
535
|
+
"type": "hook",
|
|
536
|
+
"executable": true
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"source": "hooks/endpoint-test-coverage.js",
|
|
540
|
+
"target": ".kiro/hooks/endpoint-test-coverage.js",
|
|
541
|
+
"type": "hook",
|
|
542
|
+
"executable": true
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"source": "powers.json",
|
|
546
|
+
"target": ".kiro/powers.json",
|
|
547
|
+
"type": "powers"
|
|
548
|
+
},
|
|
526
549
|
{
|
|
527
550
|
"source": "metadata.json",
|
|
528
551
|
"target": ".kiro/metadata.json",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"powers": [
|
|
3
|
+
{
|
|
4
|
+
"name": "Supabase",
|
|
5
|
+
"url": "https://kiro.dev/powers/supabase",
|
|
6
|
+
"description": "Backend-as-a-service with Postgres, auth, and real-time subscriptions",
|
|
7
|
+
"tier": "essential"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "Neon",
|
|
11
|
+
"url": "https://kiro.dev/powers/neon",
|
|
12
|
+
"description": "Serverless Postgres with branching and autoscaling",
|
|
13
|
+
"tier": "recommended"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Postman",
|
|
17
|
+
"url": "https://kiro.dev/powers/postman",
|
|
18
|
+
"description": "API testing and collection management",
|
|
19
|
+
"tier": "recommended"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "Context7",
|
|
23
|
+
"url": "https://kiro.dev/powers/context7",
|
|
24
|
+
"description": "Up-to-date documentation lookup for libraries and frameworks",
|
|
25
|
+
"tier": "recommended"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "Stripe",
|
|
29
|
+
"url": "https://kiro.dev/powers/stripe",
|
|
30
|
+
"description": "Payment processing, subscriptions, and billing integration",
|
|
31
|
+
"tier": "optional"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "Snyk",
|
|
35
|
+
"url": "https://kiro.dev/powers/snyk",
|
|
36
|
+
"description": "Security scanning and vulnerability detection for dependencies",
|
|
37
|
+
"tier": "optional"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks for schema changes in data models that could indicate data drift.
|
|
4
|
+
* Compares current vs baseline schema files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const SCHEMA_DIRS = ['src/data', 'data/schemas', 'schemas', 'src/models'];
|
|
11
|
+
const BASELINE_DIR = 'data/baseline';
|
|
12
|
+
const SCHEMA_EXTENSIONS = ['.json', '.yaml', '.yml', '.avsc', '.proto'];
|
|
13
|
+
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
function findSchemaFiles(dir) {
|
|
17
|
+
const results = [];
|
|
18
|
+
if (!fs.existsSync(dir)) return results;
|
|
19
|
+
try {
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = path.join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
results.push(...findSchemaFiles(full));
|
|
25
|
+
} else if (entry.isFile()) {
|
|
26
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
27
|
+
if (SCHEMA_EXTENSIONS.includes(ext) && entry.name.includes('schema')) {
|
|
28
|
+
results.push(full);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (e) { /* skip */ }
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let schemaFiles = [];
|
|
37
|
+
for (const dir of SCHEMA_DIRS) {
|
|
38
|
+
schemaFiles.push(...findSchemaFiles(path.resolve(cwd, dir)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (schemaFiles.length === 0) {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const baselineDir = path.resolve(cwd, BASELINE_DIR);
|
|
46
|
+
if (!fs.existsSync(baselineDir)) {
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
'[data-drift-check] No baseline directory found at data/baseline/.\n' +
|
|
49
|
+
' Create baseline schemas to enable drift detection.\n'
|
|
50
|
+
);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const drifted = [];
|
|
55
|
+
|
|
56
|
+
for (const schemaFile of schemaFiles) {
|
|
57
|
+
const name = path.basename(schemaFile);
|
|
58
|
+
const baselinePath = path.join(baselineDir, name);
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(baselinePath)) continue;
|
|
61
|
+
|
|
62
|
+
const current = fs.readFileSync(schemaFile, 'utf8').trim();
|
|
63
|
+
const baseline = fs.readFileSync(baselinePath, 'utf8').trim();
|
|
64
|
+
|
|
65
|
+
if (current !== baseline) {
|
|
66
|
+
drifted.push(path.relative(cwd, schemaFile));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (drifted.length > 0) {
|
|
71
|
+
process.stdout.write(`[data-drift-check] ${drifted.length} schema(s) differ from baseline:\n`);
|
|
72
|
+
drifted.forEach((d) => process.stdout.write(` - ${d}\n`));
|
|
73
|
+
process.stdout.write(' Verify changes are intentional and update baseline if so.\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
process.exit(0);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks that experiment configs have corresponding log entries
|
|
4
|
+
* in the experiments/ directory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIRS = ['configs', 'config', 'experiments/configs', 'src/configs'];
|
|
11
|
+
const LOG_DIRS = ['experiments', 'experiments/logs', 'logs/experiments', 'mlruns'];
|
|
12
|
+
const CONFIG_EXTENSIONS = ['.yaml', '.yml', '.toml', '.json'];
|
|
13
|
+
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
function findConfigs(dir) {
|
|
17
|
+
const results = [];
|
|
18
|
+
if (!fs.existsSync(dir)) return results;
|
|
19
|
+
try {
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = path.join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
results.push(...findConfigs(full));
|
|
25
|
+
} else if (entry.isFile()) {
|
|
26
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
27
|
+
if (CONFIG_EXTENSIONS.includes(ext) && /experiment|exp|run/i.test(entry.name)) {
|
|
28
|
+
results.push(full);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (e) { /* skip */ }
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let configFiles = [];
|
|
37
|
+
for (const dir of CONFIG_DIRS) {
|
|
38
|
+
configFiles.push(...findConfigs(path.resolve(cwd, dir)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (configFiles.length === 0) {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Find log directories
|
|
46
|
+
let logDir = null;
|
|
47
|
+
for (const dir of LOG_DIRS) {
|
|
48
|
+
const full = path.resolve(cwd, dir);
|
|
49
|
+
if (fs.existsSync(full)) {
|
|
50
|
+
logDir = full;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!logDir) {
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
'[experiment-log] Experiment configs found but no experiments/ log directory.\n' +
|
|
58
|
+
' Create an experiments/ directory to track experiment results.\n'
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check each config has a corresponding log entry
|
|
64
|
+
const logEntries = new Set();
|
|
65
|
+
try {
|
|
66
|
+
const entries = fs.readdirSync(logDir, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
logEntries.add(path.basename(entry.name, path.extname(entry.name)).toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
} catch (e) { /* skip */ }
|
|
71
|
+
|
|
72
|
+
const unlogged = [];
|
|
73
|
+
for (const config of configFiles) {
|
|
74
|
+
const baseName = path.basename(config, path.extname(config)).toLowerCase();
|
|
75
|
+
if (!logEntries.has(baseName)) {
|
|
76
|
+
unlogged.push(path.relative(cwd, config));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (unlogged.length > 0) {
|
|
81
|
+
process.stdout.write(`[experiment-log] ${unlogged.length} experiment config(s) without log entries:\n`);
|
|
82
|
+
unlogged.forEach((u) => process.stdout.write(` - ${u}\n`));
|
|
83
|
+
process.stdout.write(' Run experiments and log results before committing configs.\n');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.exit(0);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reminds to update model card when model files (.pkl, .pt, .h5, saved_model/)
|
|
4
|
+
* are modified.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const MODEL_DIRS = ['models', 'src/models', 'artifacts', 'saved_models'];
|
|
11
|
+
const MODEL_EXTENSIONS = ['.pkl', '.pt', '.pth', '.h5', '.onnx', '.pb', '.safetensors'];
|
|
12
|
+
const MODEL_CARD_NAMES = ['MODEL_CARD.md', 'model_card.md', 'model-card.md', 'README.md'];
|
|
13
|
+
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
function findModelFiles(dir) {
|
|
17
|
+
const results = [];
|
|
18
|
+
if (!fs.existsSync(dir)) return results;
|
|
19
|
+
try {
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = path.join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory() && entry.name !== '__pycache__') {
|
|
24
|
+
// saved_model/ directories count as model artifacts
|
|
25
|
+
if (entry.name === 'saved_model' || entry.name === 'checkpoint') {
|
|
26
|
+
results.push(full);
|
|
27
|
+
} else {
|
|
28
|
+
results.push(...findModelFiles(full));
|
|
29
|
+
}
|
|
30
|
+
} else if (entry.isFile()) {
|
|
31
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
32
|
+
if (MODEL_EXTENSIONS.includes(ext)) {
|
|
33
|
+
results.push(full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (e) { /* skip */ }
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let modelFiles = [];
|
|
42
|
+
for (const dir of MODEL_DIRS) {
|
|
43
|
+
modelFiles.push(...findModelFiles(path.resolve(cwd, dir)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (modelFiles.length === 0) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if model card exists
|
|
51
|
+
let modelCardPath = null;
|
|
52
|
+
for (const dir of MODEL_DIRS) {
|
|
53
|
+
for (const name of MODEL_CARD_NAMES) {
|
|
54
|
+
const full = path.resolve(cwd, dir, name);
|
|
55
|
+
if (fs.existsSync(full)) {
|
|
56
|
+
modelCardPath = full;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (modelCardPath) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Also check project root
|
|
64
|
+
if (!modelCardPath) {
|
|
65
|
+
for (const name of MODEL_CARD_NAMES.slice(0, -1)) {
|
|
66
|
+
const full = path.resolve(cwd, name);
|
|
67
|
+
if (fs.existsSync(full)) {
|
|
68
|
+
modelCardPath = full;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!modelCardPath) {
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
'[model-card-update] Model artifacts found but no model card exists.\n' +
|
|
77
|
+
' Create a MODEL_CARD.md documenting model details, metrics, and limitations.\n'
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if model files are newer than model card
|
|
83
|
+
const cardStat = fs.statSync(modelCardPath);
|
|
84
|
+
const newerModels = modelFiles.filter((f) => {
|
|
85
|
+
try {
|
|
86
|
+
const stat = fs.statSync(f);
|
|
87
|
+
return stat.mtimeMs > cardStat.mtimeMs;
|
|
88
|
+
} catch (e) { return false; }
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (newerModels.length > 0) {
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
`[model-card-update] ${newerModels.length} model file(s) updated after model card.\n` +
|
|
94
|
+
` Update ${path.relative(cwd, modelCardPath)} with latest model information.\n`
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.exit(0);
|
|
@@ -553,6 +553,29 @@
|
|
|
553
553
|
"type": "hook",
|
|
554
554
|
"executable": true
|
|
555
555
|
},
|
|
556
|
+
{
|
|
557
|
+
"source": "hooks/data-drift-check.js",
|
|
558
|
+
"target": ".kiro/hooks/data-drift-check.js",
|
|
559
|
+
"type": "hook",
|
|
560
|
+
"executable": true
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
"source": "hooks/model-card-update.js",
|
|
564
|
+
"target": ".kiro/hooks/model-card-update.js",
|
|
565
|
+
"type": "hook",
|
|
566
|
+
"executable": true
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"source": "hooks/experiment-log.js",
|
|
570
|
+
"target": ".kiro/hooks/experiment-log.js",
|
|
571
|
+
"type": "hook",
|
|
572
|
+
"executable": true
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"source": "powers.json",
|
|
576
|
+
"target": ".kiro/powers.json",
|
|
577
|
+
"type": "powers"
|
|
578
|
+
},
|
|
556
579
|
{
|
|
557
580
|
"source": "metadata.json",
|
|
558
581
|
"target": ".kiro/metadata.json",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"powers": [
|
|
3
|
+
{
|
|
4
|
+
"name": "ClickHouse",
|
|
5
|
+
"url": "https://kiro.dev/powers/click-house",
|
|
6
|
+
"description": "High-performance analytics database for data pipelines",
|
|
7
|
+
"tier": "essential"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "Context7",
|
|
11
|
+
"url": "https://kiro.dev/powers/context7",
|
|
12
|
+
"description": "Up-to-date documentation lookup for libraries and frameworks",
|
|
13
|
+
"tier": "recommended"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Exa",
|
|
17
|
+
"url": "https://kiro.dev/powers/exa",
|
|
18
|
+
"description": "Web search and research for finding datasets and papers",
|
|
19
|
+
"tier": "recommended"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "Neon",
|
|
23
|
+
"url": "https://kiro.dev/powers/neon",
|
|
24
|
+
"description": "Serverless Postgres for ML feature stores",
|
|
25
|
+
"tier": "optional"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "New Relic",
|
|
29
|
+
"url": "https://kiro.dev/powers/new-relic",
|
|
30
|
+
"description": "Observability and performance monitoring for ML services",
|
|
31
|
+
"tier": "optional"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks Dockerfile for security best practices: non-root user,
|
|
4
|
+
* no latest tag, and multi-stage build usage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const DOCKERFILE_NAMES = ['Dockerfile', 'dockerfile', 'Containerfile'];
|
|
12
|
+
|
|
13
|
+
function findDockerfiles(dir) {
|
|
14
|
+
const results = [];
|
|
15
|
+
try {
|
|
16
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.isFile() && (DOCKERFILE_NAMES.includes(entry.name) || entry.name.startsWith('Dockerfile.'))) {
|
|
19
|
+
results.push(path.join(dir, entry.name));
|
|
20
|
+
} else if (entry.isDirectory() && !['node_modules', '.git', '.terraform'].includes(entry.name)) {
|
|
21
|
+
results.push(...findDockerfiles(path.join(dir, entry.name)));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch (e) { /* skip */ }
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const dockerfiles = findDockerfiles(cwd);
|
|
29
|
+
|
|
30
|
+
if (dockerfiles.length === 0) {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const issues = [];
|
|
35
|
+
|
|
36
|
+
for (const df of dockerfiles) {
|
|
37
|
+
const content = fs.readFileSync(df, 'utf8');
|
|
38
|
+
const rel = path.relative(cwd, df);
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
|
|
41
|
+
// Check for latest tag
|
|
42
|
+
const fromLines = lines.filter((l) => /^\s*FROM\s+/i.test(l));
|
|
43
|
+
for (const line of fromLines) {
|
|
44
|
+
if (/:latest\b/.test(line) || (/^\s*FROM\s+\S+\s*/i.test(line) && !/:/.test(line) && !/\s+AS\s+/i.test(line.split(/\s+/).slice(2).join(' ')))) {
|
|
45
|
+
if (!/:/.test(line.split(/\s+/)[1] || '')) {
|
|
46
|
+
issues.push(`${rel}: FROM without pinned version tag`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (/:latest/.test(line)) {
|
|
50
|
+
issues.push(`${rel}: Uses :latest tag (pin a specific version)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for non-root user
|
|
55
|
+
const hasUser = /^\s*USER\s+(?!root)/im.test(content);
|
|
56
|
+
if (!hasUser) {
|
|
57
|
+
issues.push(`${rel}: No non-root USER directive found`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for multi-stage build
|
|
61
|
+
if (fromLines.length < 2) {
|
|
62
|
+
issues.push(`${rel}: Single-stage build (consider multi-stage for smaller images)`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (issues.length > 0) {
|
|
67
|
+
process.stdout.write('[container-scan] Dockerfile security issues:\n');
|
|
68
|
+
issues.forEach((i) => process.stdout.write(` - ${i}\n`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.stdout.write('[container-scan] All Dockerfiles pass security checks.\n');
|
|
73
|
+
process.exit(0);
|