kiro-kit 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +35 -0
  2. package/dist/index.js +772 -130
  3. package/dist/index.js.map +1 -1
  4. package/dist/presets/backend/hooks/api-schema-validate.js +75 -0
  5. package/dist/presets/backend/hooks/endpoint-test-coverage.js +77 -0
  6. package/dist/presets/backend/hooks/migration-safety-check.js +68 -0
  7. package/dist/presets/backend/manifest.json +23 -0
  8. package/dist/presets/backend/powers.json +40 -0
  9. package/dist/presets/data-ai/hooks/data-drift-check.js +77 -0
  10. package/dist/presets/data-ai/hooks/experiment-log.js +87 -0
  11. package/dist/presets/data-ai/hooks/model-card-update.js +99 -0
  12. package/dist/presets/data-ai/manifest.json +23 -0
  13. package/dist/presets/data-ai/powers.json +34 -0
  14. package/dist/presets/devops/hooks/container-scan.js +73 -0
  15. package/dist/presets/devops/hooks/cost-estimation.js +69 -0
  16. package/dist/presets/devops/hooks/terraform-plan-review.js +67 -0
  17. package/dist/presets/devops/manifest.json +23 -0
  18. package/dist/presets/devops/powers.json +40 -0
  19. package/dist/presets/frontend/hooks/accessibility-check.js +76 -0
  20. package/dist/presets/frontend/hooks/bundle-size-guard.js +71 -0
  21. package/dist/presets/frontend/hooks/component-test-reminder.js +71 -0
  22. package/dist/presets/frontend/manifest.json +23 -0
  23. package/dist/presets/frontend/powers.json +34 -0
  24. package/dist/presets/fullstack/hooks/api-client-gen.js +69 -0
  25. package/dist/presets/fullstack/hooks/deployment-readiness.js +73 -0
  26. package/dist/presets/fullstack/hooks/type-sync-check.js +69 -0
  27. package/dist/presets/fullstack/manifest.json +23 -0
  28. package/dist/presets/fullstack/powers.json +46 -0
  29. package/dist/presets/mobile/hooks/asset-optimization.js +58 -0
  30. package/dist/presets/mobile/hooks/platform-parity-check.js +73 -0
  31. package/dist/presets/mobile/hooks/release-checklist.js +83 -0
  32. package/dist/presets/mobile/manifest.json +23 -0
  33. package/dist/presets/mobile/powers.json +34 -0
  34. package/package.json +73 -65
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scans terraform files for expensive resource types (RDS, NAT Gateway, EKS)
4
+ * and flags them for cost review.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const EXPENSIVE_RESOURCES = [
11
+ { pattern: /resource\s+"aws_db_instance"/, label: 'RDS Instance', note: 'Consider Aurora Serverless for variable workloads' },
12
+ { pattern: /resource\s+"aws_nat_gateway"/, label: 'NAT Gateway', note: 'Costs ~$32/month per AZ plus data transfer' },
13
+ { pattern: /resource\s+"aws_eks_cluster"/, label: 'EKS Cluster', note: 'Costs $0.10/hour (~$73/month) per cluster' },
14
+ { pattern: /resource\s+"aws_elasticsearch_domain"/, label: 'Elasticsearch', note: 'Consider OpenSearch Serverless for variable usage' },
15
+ { pattern: /resource\s+"aws_redshift_cluster"/, label: 'Redshift Cluster', note: 'Consider Redshift Serverless for intermittent queries' },
16
+ { pattern: /resource\s+"aws_msk_cluster"/, label: 'MSK Cluster', note: 'Minimum 3 brokers required, consider MSK Serverless' },
17
+ { pattern: /resource\s+"aws_fsx_/, label: 'FSx File System', note: 'High baseline cost, verify storage requirements' },
18
+ { pattern: /resource\s+"google_container_cluster"/, label: 'GKE Cluster', note: 'Management fee applies per cluster' },
19
+ { pattern: /resource\s+"azurerm_kubernetes_cluster"/, label: 'AKS Cluster', note: 'Node pool costs can escalate quickly' },
20
+ ];
21
+
22
+ const TF_EXTENSIONS = ['.tf', '.tf.json'];
23
+ const cwd = process.cwd();
24
+
25
+ function findTfFiles(dir, depth) {
26
+ const results = [];
27
+ if (depth > 3 || !fs.existsSync(dir)) return results;
28
+ try {
29
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
30
+ for (const entry of entries) {
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory() && !['.terraform', 'node_modules', '.git'].includes(entry.name)) {
33
+ results.push(...findTfFiles(full, depth + 1));
34
+ } else if (entry.isFile() && TF_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
35
+ results.push(full);
36
+ }
37
+ }
38
+ } catch (e) { /* skip */ }
39
+ return results;
40
+ }
41
+
42
+ const tfFiles = findTfFiles(cwd, 0);
43
+
44
+ if (tfFiles.length === 0) {
45
+ process.exit(0);
46
+ }
47
+
48
+ const findings = [];
49
+
50
+ for (const file of tfFiles) {
51
+ const content = fs.readFileSync(file, 'utf8');
52
+ const rel = path.relative(cwd, file);
53
+
54
+ for (const { pattern, label, note } of EXPENSIVE_RESOURCES) {
55
+ if (pattern.test(content)) {
56
+ findings.push({ file: rel, label, note });
57
+ }
58
+ }
59
+ }
60
+
61
+ if (findings.length > 0) {
62
+ process.stdout.write('[cost-estimation] Expensive resources detected for cost review:\n');
63
+ findings.forEach((f) => {
64
+ process.stdout.write(` - ${f.file}: ${f.label}\n ${f.note}\n`);
65
+ });
66
+ process.exit(1);
67
+ }
68
+
69
+ process.exit(0);
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Parses terraform plan output for destructive changes (destroy, replace)
4
+ * and highlights them for review.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ const cwd = process.cwd();
12
+ const PLAN_FILES = ['tfplan.txt', 'plan.txt', 'terraform.plan.txt'];
13
+
14
+ let planContent = '';
15
+
16
+ // Try to read a saved plan output file
17
+ for (const pf of PLAN_FILES) {
18
+ const full = path.resolve(cwd, pf);
19
+ if (fs.existsSync(full)) {
20
+ planContent = fs.readFileSync(full, 'utf8');
21
+ break;
22
+ }
23
+ }
24
+
25
+ // If no plan file, try to run terraform show on binary plan
26
+ if (!planContent) {
27
+ const binaryPlan = path.resolve(cwd, 'tfplan');
28
+ if (fs.existsSync(binaryPlan)) {
29
+ try {
30
+ planContent = execSync('terraform show -no-color tfplan', {
31
+ cwd,
32
+ encoding: 'utf8',
33
+ timeout: 30000,
34
+ });
35
+ } catch (e) { /* skip */ }
36
+ }
37
+ }
38
+
39
+ if (!planContent) {
40
+ process.exit(0);
41
+ }
42
+
43
+ const DESTRUCTIVE_PATTERNS = [
44
+ { pattern: /will be destroyed/g, label: 'DESTROY' },
45
+ { pattern: /must be replaced/g, label: 'REPLACE' },
46
+ { pattern: /forces replacement/g, label: 'FORCE REPLACE' },
47
+ ];
48
+
49
+ const findings = [];
50
+
51
+ for (const { pattern, label } of DESTRUCTIVE_PATTERNS) {
52
+ const matches = planContent.match(pattern);
53
+ if (matches) {
54
+ findings.push({ label, count: matches.length });
55
+ }
56
+ }
57
+
58
+ if (findings.length > 0) {
59
+ process.stdout.write('[terraform-plan-review] Destructive changes detected:\n');
60
+ findings.forEach((f) => {
61
+ process.stdout.write(` - ${f.label}: ${f.count} resource(s)\n`);
62
+ });
63
+ process.stdout.write(' Review plan carefully before applying.\n');
64
+ process.exit(1);
65
+ }
66
+
67
+ process.exit(0);
@@ -535,6 +535,29 @@
535
535
  "type": "hook",
536
536
  "executable": true
537
537
  },
538
+ {
539
+ "source": "hooks/terraform-plan-review.js",
540
+ "target": ".kiro/hooks/terraform-plan-review.js",
541
+ "type": "hook",
542
+ "executable": true
543
+ },
544
+ {
545
+ "source": "hooks/container-scan.js",
546
+ "target": ".kiro/hooks/container-scan.js",
547
+ "type": "hook",
548
+ "executable": true
549
+ },
550
+ {
551
+ "source": "hooks/cost-estimation.js",
552
+ "target": ".kiro/hooks/cost-estimation.js",
553
+ "type": "hook",
554
+ "executable": true
555
+ },
556
+ {
557
+ "source": "powers.json",
558
+ "target": ".kiro/powers.json",
559
+ "type": "powers"
560
+ },
538
561
  {
539
562
  "source": "metadata.json",
540
563
  "target": ".kiro/metadata.json",
@@ -0,0 +1,40 @@
1
+ {
2
+ "powers": [
3
+ {
4
+ "name": "Terraform",
5
+ "url": "https://kiro.dev/powers/terraform",
6
+ "description": "Infrastructure as Code with registry providers and modules",
7
+ "tier": "essential"
8
+ },
9
+ {
10
+ "name": "Datadog",
11
+ "url": "https://kiro.dev/powers/datadog",
12
+ "description": "Production monitoring, logging, and APM",
13
+ "tier": "recommended"
14
+ },
15
+ {
16
+ "name": "Snyk",
17
+ "url": "https://kiro.dev/powers/snyk",
18
+ "description": "Security scanning for containers and infrastructure",
19
+ "tier": "recommended"
20
+ },
21
+ {
22
+ "name": "Depot",
23
+ "url": "https://kiro.dev/powers/depot",
24
+ "description": "Optimized container builds and CI/CD pipelines",
25
+ "tier": "recommended"
26
+ },
27
+ {
28
+ "name": "Harness",
29
+ "url": "https://kiro.dev/powers/harness",
30
+ "description": "CI/CD pipelines, cloud cost management, and feature flags",
31
+ "tier": "optional"
32
+ },
33
+ {
34
+ "name": "AWS CDK",
35
+ "url": "https://kiro.dev/powers/aws-cdk",
36
+ "description": "AWS infrastructure with CDK using best practices",
37
+ "tier": "optional"
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks for common accessibility issues in .tsx/.jsx files.
4
+ * Scans for missing alt attributes, missing aria-labels, and missing lang attribute.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const SRC_DIR = path.resolve(process.cwd(), 'src');
11
+ const EXTENSIONS = ['.tsx', '.jsx'];
12
+
13
+ if (!fs.existsSync(SRC_DIR)) {
14
+ process.exit(0);
15
+ }
16
+
17
+ function getFiles(dir) {
18
+ const 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 !== 'node_modules') {
24
+ results.push(...getFiles(full));
25
+ } else if (EXTENSIONS.includes(path.extname(entry.name).toLowerCase())) {
26
+ results.push(full);
27
+ }
28
+ }
29
+ } catch (e) { /* skip unreadable dirs */ }
30
+ return results;
31
+ }
32
+
33
+ const issues = [];
34
+ const files = getFiles(SRC_DIR);
35
+
36
+ for (const file of files) {
37
+ const content = fs.readFileSync(file, 'utf8');
38
+ const rel = path.relative(process.cwd(), file);
39
+
40
+ // Check for <img> without alt
41
+ const imgNoAlt = content.match(/<img(?![^>]*\balt\b)[^>]*>/g);
42
+ if (imgNoAlt) {
43
+ issues.push(`${rel}: ${imgNoAlt.length} <img> tag(s) missing alt attribute`);
44
+ }
45
+
46
+ // Check for interactive elements without aria-label
47
+ const buttons = content.match(/<button(?![^>]*\baria-label\b)(?![^>]*>[^<]+<\/button>)[^>]*>\s*<\//g);
48
+ if (buttons) {
49
+ issues.push(`${rel}: ${buttons.length} empty <button> without aria-label`);
50
+ }
51
+ }
52
+
53
+ // Check for lang attribute in root layout/html
54
+ const layoutFiles = ['src/app/layout.tsx', 'src/app/layout.jsx', 'index.html'];
55
+ let hasLang = false;
56
+ for (const lf of layoutFiles) {
57
+ const full = path.resolve(process.cwd(), lf);
58
+ if (fs.existsSync(full)) {
59
+ const content = fs.readFileSync(full, 'utf8');
60
+ if (/<html[^>]*\blang\b/.test(content)) {
61
+ hasLang = true;
62
+ }
63
+ break;
64
+ }
65
+ }
66
+ if (!hasLang && files.length > 0) {
67
+ issues.push('Root layout/html: missing lang attribute on <html> element');
68
+ }
69
+
70
+ if (issues.length > 0) {
71
+ process.stdout.write('[accessibility-check] Issues found:\n');
72
+ issues.forEach((i) => process.stdout.write(` - ${i}\n`));
73
+ process.exit(1);
74
+ }
75
+
76
+ process.exit(0);
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks if bundle analysis data exists and warns if total size exceeds threshold.
4
+ * Looks for .next/analyze/ or build stats files.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const THRESHOLD_KB = 500;
11
+ const ANALYZE_PATHS = [
12
+ '.next/analyze/client.html',
13
+ '.next/analyze/nodejs.html',
14
+ 'build/bundle-stats.json',
15
+ 'dist/stats.json',
16
+ 'stats.json',
17
+ ];
18
+
19
+ const cwd = process.cwd();
20
+ let found = false;
21
+
22
+ for (const rel of ANALYZE_PATHS) {
23
+ const full = path.resolve(cwd, rel);
24
+ if (fs.existsSync(full)) {
25
+ found = true;
26
+ break;
27
+ }
28
+ }
29
+
30
+ if (!found) {
31
+ // Check .next build manifest for size info
32
+ const buildManifest = path.resolve(cwd, '.next/build-manifest.json');
33
+ if (!fs.existsSync(buildManifest)) {
34
+ process.exit(0);
35
+ }
36
+ }
37
+
38
+ // Check total size of JS output
39
+ const outputDirs = ['.next/static/chunks', 'dist', 'build/static/js'];
40
+ let totalBytes = 0;
41
+
42
+ for (const dir of outputDirs) {
43
+ const full = path.resolve(cwd, dir);
44
+ if (!fs.existsSync(full)) continue;
45
+ try {
46
+ const files = fs.readdirSync(full);
47
+ for (const file of files) {
48
+ if (file.endsWith('.js')) {
49
+ const stat = fs.statSync(path.join(full, file));
50
+ totalBytes += stat.size;
51
+ }
52
+ }
53
+ } catch (e) { /* skip */ }
54
+ }
55
+
56
+ if (totalBytes === 0) {
57
+ process.exit(0);
58
+ }
59
+
60
+ const totalKB = Math.round(totalBytes / 1024);
61
+
62
+ if (totalKB > THRESHOLD_KB) {
63
+ process.stdout.write(
64
+ `[bundle-size-guard] Warning: Total JS bundle size is ${totalKB}KB (threshold: ${THRESHOLD_KB}KB).\n` +
65
+ ' Consider code splitting, lazy loading, or removing unused dependencies.\n'
66
+ );
67
+ process.exit(1);
68
+ }
69
+
70
+ process.stdout.write(`[bundle-size-guard] Bundle size OK: ${totalKB}KB (threshold: ${THRESHOLD_KB}KB).\n`);
71
+ process.exit(0);
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks if components in src/components/ have corresponding .test.tsx files.
4
+ * Warns about components missing test coverage.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const COMPONENTS_DIR = path.resolve(process.cwd(), 'src/components');
11
+ const COMPONENT_EXTENSIONS = ['.tsx', '.jsx'];
12
+ const IGNORE_DIRS = ['ui', 'node_modules', '__tests__'];
13
+
14
+ if (!fs.existsSync(COMPONENTS_DIR)) {
15
+ process.exit(0);
16
+ }
17
+
18
+ function getComponentFiles(dir) {
19
+ const results = [];
20
+ try {
21
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ const full = path.join(dir, entry.name);
24
+ if (entry.isDirectory() && !IGNORE_DIRS.includes(entry.name)) {
25
+ results.push(...getComponentFiles(full));
26
+ } else if (entry.isFile()) {
27
+ const ext = path.extname(entry.name).toLowerCase();
28
+ const base = path.basename(entry.name, ext);
29
+ if (COMPONENT_EXTENSIONS.includes(ext) && !base.endsWith('.test') && !base.endsWith('.spec')) {
30
+ // Only count PascalCase files as components
31
+ if (/^[A-Z]/.test(base)) {
32
+ results.push(full);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ } catch (e) { /* skip */ }
38
+ return results;
39
+ }
40
+
41
+ const components = getComponentFiles(COMPONENTS_DIR);
42
+ const missing = [];
43
+
44
+ for (const comp of components) {
45
+ const dir = path.dirname(comp);
46
+ const ext = path.extname(comp);
47
+ const base = path.basename(comp, ext);
48
+
49
+ const testPatterns = [
50
+ path.join(dir, `${base}.test.tsx`),
51
+ path.join(dir, `${base}.test.jsx`),
52
+ path.join(dir, `${base}.spec.tsx`),
53
+ path.join(dir, '__tests__', `${base}.test.tsx`),
54
+ ];
55
+
56
+ const hasTest = testPatterns.some((t) => fs.existsSync(t));
57
+ if (!hasTest) {
58
+ missing.push(path.relative(process.cwd(), comp));
59
+ }
60
+ }
61
+
62
+ if (missing.length > 0) {
63
+ process.stdout.write(`[component-test-reminder] ${missing.length} component(s) without tests:\n`);
64
+ missing.slice(0, 10).forEach((m) => process.stdout.write(` - ${m}\n`));
65
+ if (missing.length > 10) {
66
+ process.stdout.write(` ... and ${missing.length - 10} more\n`);
67
+ }
68
+ process.exit(1);
69
+ }
70
+
71
+ process.exit(0);
@@ -553,6 +553,29 @@
553
553
  "type": "hook",
554
554
  "executable": true
555
555
  },
556
+ {
557
+ "source": "hooks/accessibility-check.js",
558
+ "target": ".kiro/hooks/accessibility-check.js",
559
+ "type": "hook",
560
+ "executable": true
561
+ },
562
+ {
563
+ "source": "hooks/bundle-size-guard.js",
564
+ "target": ".kiro/hooks/bundle-size-guard.js",
565
+ "type": "hook",
566
+ "executable": true
567
+ },
568
+ {
569
+ "source": "hooks/component-test-reminder.js",
570
+ "target": ".kiro/hooks/component-test-reminder.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": "Figma",
5
+ "url": "https://kiro.dev/powers/figma",
6
+ "description": "Design-to-code integration with Figma files",
7
+ "tier": "essential"
8
+ },
9
+ {
10
+ "name": "Netlify",
11
+ "url": "https://kiro.dev/powers/netlify",
12
+ "description": "Deploy React, Next.js, and modern web apps to global CDN",
13
+ "tier": "recommended"
14
+ },
15
+ {
16
+ "name": "Context7",
17
+ "url": "https://kiro.dev/powers/context7",
18
+ "description": "Up-to-date documentation lookup for libraries and frameworks",
19
+ "tier": "recommended"
20
+ },
21
+ {
22
+ "name": "Snyk",
23
+ "url": "https://kiro.dev/powers/snyk",
24
+ "description": "Security scanning and vulnerability detection for dependencies",
25
+ "tier": "optional"
26
+ },
27
+ {
28
+ "name": "ScoutQA",
29
+ "url": "https://kiro.dev/powers/scout-qa",
30
+ "description": "AI-powered exploratory testing for web applications",
31
+ "tier": "optional"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks if API client types are in sync with route handlers.
4
+ * Warns if route files changed more recently than client type definitions.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const ROUTE_DIRS = ['src/app/api', 'src/routes'];
11
+ const CLIENT_DIRS = ['src/lib/api', 'src/client', 'src/api', 'src/services'];
12
+
13
+ const cwd = process.cwd();
14
+
15
+ function getLatestMtime(dir) {
16
+ let latest = 0;
17
+ if (!fs.existsSync(dir)) return 0;
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
+ const sub = getLatestMtime(full);
24
+ if (sub > latest) latest = sub;
25
+ } else if (/\.(ts|js)$/.test(entry.name)) {
26
+ const stat = fs.statSync(full);
27
+ if (stat.mtimeMs > latest) latest = stat.mtimeMs;
28
+ }
29
+ }
30
+ } catch (e) { /* skip */ }
31
+ return latest;
32
+ }
33
+
34
+ let routeMtime = 0;
35
+ for (const dir of ROUTE_DIRS) {
36
+ const t = getLatestMtime(path.resolve(cwd, dir));
37
+ if (t > routeMtime) routeMtime = t;
38
+ }
39
+
40
+ if (routeMtime === 0) {
41
+ process.exit(0);
42
+ }
43
+
44
+ let clientMtime = 0;
45
+ let clientDir = null;
46
+ for (const dir of CLIENT_DIRS) {
47
+ const full = path.resolve(cwd, dir);
48
+ if (fs.existsSync(full)) {
49
+ clientDir = dir;
50
+ clientMtime = getLatestMtime(full);
51
+ break;
52
+ }
53
+ }
54
+
55
+ if (!clientDir) {
56
+ process.exit(0);
57
+ }
58
+
59
+ if (routeMtime > clientMtime) {
60
+ const diff = Math.round((routeMtime - clientMtime) / 1000 / 60);
61
+ process.stdout.write(
62
+ `[api-client-gen] API routes changed ${diff}min after client types.\n` +
63
+ ` Route handlers may be out of sync with ${clientDir}/.\n` +
64
+ ' Consider regenerating API client types.\n'
65
+ );
66
+ process.exit(1);
67
+ }
68
+
69
+ process.exit(0);
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pre-deployment checklist: checks for .env.example completeness,
4
+ * build success indicators, and no TODO/FIXME in production code.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const cwd = process.cwd();
11
+ const issues = [];
12
+
13
+ // Check .env.example exists
14
+ const envExample = path.resolve(cwd, '.env.example');
15
+ const envLocal = path.resolve(cwd, '.env.local');
16
+ if (!fs.existsSync(envExample)) {
17
+ issues.push('Missing .env.example file');
18
+ } else if (fs.existsSync(envLocal)) {
19
+ // Check all keys in .env.local are documented in .env.example
20
+ const exampleKeys = fs.readFileSync(envExample, 'utf8')
21
+ .split('\n')
22
+ .filter((l) => l.match(/^[A-Z_]+=/) )
23
+ .map((l) => l.split('=')[0]);
24
+ const localKeys = fs.readFileSync(envLocal, 'utf8')
25
+ .split('\n')
26
+ .filter((l) => l.match(/^[A-Z_]+=/))
27
+ .map((l) => l.split('=')[0]);
28
+ const undocumented = localKeys.filter((k) => !exampleKeys.includes(k));
29
+ if (undocumented.length > 0) {
30
+ issues.push(`${undocumented.length} env var(s) in .env.local not in .env.example`);
31
+ }
32
+ }
33
+
34
+ // Check for TODO/FIXME in src/
35
+ function scanTodos(dir, depth) {
36
+ let count = 0;
37
+ if (depth > 5 || !fs.existsSync(dir)) return 0;
38
+ try {
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ const full = path.join(dir, entry.name);
42
+ if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.next') {
43
+ count += scanTodos(full, depth + 1);
44
+ } else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
45
+ const content = fs.readFileSync(full, 'utf8');
46
+ const matches = content.match(/\b(TODO|FIXME)\b/g);
47
+ if (matches) count += matches.length;
48
+ }
49
+ }
50
+ } catch (e) { /* skip */ }
51
+ return count;
52
+ }
53
+
54
+ const todoCount = scanTodos(path.resolve(cwd, 'src'), 0);
55
+ if (todoCount > 0) {
56
+ issues.push(`${todoCount} TODO/FIXME comment(s) found in src/`);
57
+ }
58
+
59
+ // Check build output exists
60
+ const buildDirs = ['.next', 'dist', 'build'];
61
+ const hasBuild = buildDirs.some((d) => fs.existsSync(path.resolve(cwd, d)));
62
+ if (!hasBuild) {
63
+ issues.push('No build output found. Run build before deploying.');
64
+ }
65
+
66
+ if (issues.length > 0) {
67
+ process.stdout.write('[deployment-readiness] Pre-deployment issues:\n');
68
+ issues.forEach((i) => process.stdout.write(` - ${i}\n`));
69
+ process.exit(1);
70
+ }
71
+
72
+ process.stdout.write('[deployment-readiness] All checks passed.\n');
73
+ process.exit(0);