@kb-labs/devkit 1.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.
Files changed (295) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/CODEOWNERS +2 -0
  3. package/.github/actions/setup-node-pnpm/action.yml +47 -0
  4. package/.github/workflow-templates/ci.yml +13 -0
  5. package/.github/workflow-templates/drift-check.yml +10 -0
  6. package/.github/workflow-templates/profiles-validate.yml +16 -0
  7. package/.github/workflow-templates/release.yml +8 -0
  8. package/.github/workflows/ci-reusable.yml +131 -0
  9. package/.github/workflows/drift-check-reusable.yml +23 -0
  10. package/.github/workflows/fixtures.yml +74 -0
  11. package/.github/workflows/profiles-validate-reusable.yml +67 -0
  12. package/.github/workflows/release-reusable.yml +50 -0
  13. package/.vscode/settings.json +23 -0
  14. package/AGENTS.md +130 -0
  15. package/LICENSE +21 -0
  16. package/README.md +1542 -0
  17. package/agents/devkit-maintainer/context.globs +15 -0
  18. package/agents/devkit-maintainer/permissions.yml +17 -0
  19. package/agents/devkit-maintainer/prompt.md +28 -0
  20. package/agents/devkit-maintainer/runbook.md +31 -0
  21. package/agents/docs-crafter/prompt.md +24 -0
  22. package/agents/docs-crafter/runbook.md +18 -0
  23. package/agents/release-manager/context.globs +7 -0
  24. package/agents/release-manager/prompt.md +27 -0
  25. package/agents/release-manager/runbook.md +17 -0
  26. package/agents/test-generator/context.globs +7 -0
  27. package/agents/test-generator/prompt.md +27 -0
  28. package/agents/test-generator/runbook.md +18 -0
  29. package/bin/devkit-architecture.mjs +1225 -0
  30. package/bin/devkit-build-order.mjs +500 -0
  31. package/bin/devkit-check-build-readiness.mjs +222 -0
  32. package/bin/devkit-check-commands.mjs +394 -0
  33. package/bin/devkit-check-configs.mjs +461 -0
  34. package/bin/devkit-check-deprecated.mjs +532 -0
  35. package/bin/devkit-check-duplicates.mjs +431 -0
  36. package/bin/devkit-check-exports.mjs +561 -0
  37. package/bin/devkit-check-imports.mjs +712 -0
  38. package/bin/devkit-check-paths.mjs +670 -0
  39. package/bin/devkit-check-scripts.mjs +335 -0
  40. package/bin/devkit-check-structure.mjs +495 -0
  41. package/bin/devkit-check-types.mjs +450 -0
  42. package/bin/devkit-ci.mjs +261 -0
  43. package/bin/devkit-core-gate.mjs +225 -0
  44. package/bin/devkit-fix-deps.mjs +1169 -0
  45. package/bin/devkit-freshness.mjs +199 -0
  46. package/bin/devkit-health.mjs +489 -0
  47. package/bin/devkit-migrate-configs.mjs +370 -0
  48. package/bin/devkit-paths.mjs +192 -0
  49. package/bin/devkit-stats.mjs +453 -0
  50. package/bin/devkit-sync.mjs +12 -0
  51. package/bin/devkit-tsup-external.mjs +148 -0
  52. package/bin/devkit-types-audit.mjs +615 -0
  53. package/bin/devkit-types-order.mjs +637 -0
  54. package/bin/devkit-validate-naming.mjs +203 -0
  55. package/bin/devkit-visualize.mjs +452 -0
  56. package/bin/kb-devkit-qa-history.mjs +453 -0
  57. package/bin/kb-devkit-qa.mjs +915 -0
  58. package/eslint/node.js +134 -0
  59. package/eslint/react.js +260 -0
  60. package/package.json +186 -0
  61. package/prettier/index.json +10 -0
  62. package/sync/index.mjs +693 -0
  63. package/templates/configs/README.md +306 -0
  64. package/templates/configs/eslint.config.js +27 -0
  65. package/templates/configs/package.json.bin +25 -0
  66. package/templates/configs/package.json.lib +30 -0
  67. package/templates/configs/tsconfig.build.json +15 -0
  68. package/templates/configs/tsconfig.json +9 -0
  69. package/templates/configs/tsup.config.bin.ts +34 -0
  70. package/templates/configs/tsup.config.cli.ts +41 -0
  71. package/templates/configs/tsup.config.dual.ts +46 -0
  72. package/templates/configs/tsup.config.ts +36 -0
  73. package/templates/package-json/README.md +290 -0
  74. package/templates/package-json/package.json.bin.template +40 -0
  75. package/templates/package-json/package.json.template +45 -0
  76. package/tsconfig/base.json +21 -0
  77. package/tsconfig/cli.json +12 -0
  78. package/tsconfig/dist/__tests__/cache.spec.d.ts +6 -0
  79. package/tsconfig/dist/__tests__/cache.spec.d.ts.map +1 -0
  80. package/tsconfig/dist/__tests__/cache.spec.js +85 -0
  81. package/tsconfig/dist/__tests__/cache.spec.js.map +1 -0
  82. package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts +6 -0
  83. package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts.map +1 -0
  84. package/tsconfig/dist/__tests__/fs-atomic.spec.js +153 -0
  85. package/tsconfig/dist/__tests__/fs-atomic.spec.js.map +1 -0
  86. package/tsconfig/dist/__tests__/init-workspace.spec.d.ts +6 -0
  87. package/tsconfig/dist/__tests__/init-workspace.spec.d.ts.map +1 -0
  88. package/tsconfig/dist/__tests__/init-workspace.spec.js +99 -0
  89. package/tsconfig/dist/__tests__/init-workspace.spec.js.map +1 -0
  90. package/tsconfig/dist/__tests__/kb-error.spec.d.ts +6 -0
  91. package/tsconfig/dist/__tests__/kb-error.spec.d.ts.map +1 -0
  92. package/tsconfig/dist/__tests__/kb-error.spec.js +190 -0
  93. package/tsconfig/dist/__tests__/kb-error.spec.js.map +1 -0
  94. package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts +6 -0
  95. package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts.map +1 -0
  96. package/tsconfig/dist/__tests__/preset-lockfile.spec.js +142 -0
  97. package/tsconfig/dist/__tests__/preset-lockfile.spec.js.map +1 -0
  98. package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts +6 -0
  99. package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts.map +1 -0
  100. package/tsconfig/dist/__tests__/product-config-profiles.spec.js +100 -0
  101. package/tsconfig/dist/__tests__/product-config-profiles.spec.js.map +1 -0
  102. package/tsconfig/dist/__tests__/product-config.spec.d.ts +6 -0
  103. package/tsconfig/dist/__tests__/product-config.spec.d.ts.map +1 -0
  104. package/tsconfig/dist/__tests__/product-config.spec.js +298 -0
  105. package/tsconfig/dist/__tests__/product-config.spec.js.map +1 -0
  106. package/tsconfig/dist/__tests__/runtime.spec.d.ts +2 -0
  107. package/tsconfig/dist/__tests__/runtime.spec.d.ts.map +1 -0
  108. package/tsconfig/dist/__tests__/runtime.spec.js +127 -0
  109. package/tsconfig/dist/__tests__/runtime.spec.js.map +1 -0
  110. package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts +6 -0
  111. package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts.map +1 -0
  112. package/tsconfig/dist/__tests__/upsert-lockfile.spec.js +251 -0
  113. package/tsconfig/dist/__tests__/upsert-lockfile.spec.js.map +1 -0
  114. package/tsconfig/dist/__tests__/validate-config.spec.d.ts +2 -0
  115. package/tsconfig/dist/__tests__/validate-config.spec.d.ts.map +1 -0
  116. package/tsconfig/dist/__tests__/validate-config.spec.js +14 -0
  117. package/tsconfig/dist/__tests__/validate-config.spec.js.map +1 -0
  118. package/tsconfig/dist/api/init-workspace.d.ts +10 -0
  119. package/tsconfig/dist/api/init-workspace.d.ts.map +1 -0
  120. package/tsconfig/dist/api/init-workspace.js +191 -0
  121. package/tsconfig/dist/api/init-workspace.js.map +1 -0
  122. package/tsconfig/dist/api/product-config.d.ts +21 -0
  123. package/tsconfig/dist/api/product-config.d.ts.map +1 -0
  124. package/tsconfig/dist/api/product-config.js +192 -0
  125. package/tsconfig/dist/api/product-config.js.map +1 -0
  126. package/tsconfig/dist/api/read-config.d.ts +22 -0
  127. package/tsconfig/dist/api/read-config.d.ts.map +1 -0
  128. package/tsconfig/dist/api/read-config.js +105 -0
  129. package/tsconfig/dist/api/read-config.js.map +1 -0
  130. package/tsconfig/dist/api/upsert-lockfile.d.ts +10 -0
  131. package/tsconfig/dist/api/upsert-lockfile.d.ts.map +1 -0
  132. package/tsconfig/dist/api/upsert-lockfile.js +63 -0
  133. package/tsconfig/dist/api/upsert-lockfile.js.map +1 -0
  134. package/tsconfig/dist/cache/fs-cache.d.ts +38 -0
  135. package/tsconfig/dist/cache/fs-cache.d.ts.map +1 -0
  136. package/tsconfig/dist/cache/fs-cache.js +142 -0
  137. package/tsconfig/dist/cache/fs-cache.js.map +1 -0
  138. package/tsconfig/dist/errors/kb-error.d.ts +32 -0
  139. package/tsconfig/dist/errors/kb-error.d.ts.map +1 -0
  140. package/tsconfig/dist/errors/kb-error.js +54 -0
  141. package/tsconfig/dist/errors/kb-error.js.map +1 -0
  142. package/tsconfig/dist/fs/__tests__/fs.spec.d.ts +2 -0
  143. package/tsconfig/dist/fs/__tests__/fs.spec.d.ts.map +1 -0
  144. package/tsconfig/dist/fs/__tests__/fs.spec.js +22 -0
  145. package/tsconfig/dist/fs/__tests__/fs.spec.js.map +1 -0
  146. package/tsconfig/dist/fs/fs.d.ts +6 -0
  147. package/tsconfig/dist/fs/fs.d.ts.map +1 -0
  148. package/tsconfig/dist/fs/fs.js +12 -0
  149. package/tsconfig/dist/fs/fs.js.map +1 -0
  150. package/tsconfig/dist/fs/index.d.ts +2 -0
  151. package/tsconfig/dist/fs/index.d.ts.map +1 -0
  152. package/tsconfig/dist/fs/index.js +2 -0
  153. package/tsconfig/dist/fs/index.js.map +1 -0
  154. package/tsconfig/dist/hash/config-hash.d.ts +17 -0
  155. package/tsconfig/dist/hash/config-hash.d.ts.map +1 -0
  156. package/tsconfig/dist/hash/config-hash.js +55 -0
  157. package/tsconfig/dist/hash/config-hash.js.map +1 -0
  158. package/tsconfig/dist/index.d.ts +5 -0
  159. package/tsconfig/dist/index.d.ts.map +1 -0
  160. package/tsconfig/dist/index.js +5 -0
  161. package/tsconfig/dist/index.js.map +1 -0
  162. package/tsconfig/dist/lockfile/lockfile.d.ts +54 -0
  163. package/tsconfig/dist/lockfile/lockfile.d.ts.map +1 -0
  164. package/tsconfig/dist/lockfile/lockfile.js +141 -0
  165. package/tsconfig/dist/lockfile/lockfile.js.map +1 -0
  166. package/tsconfig/dist/logging/__tests__/logger.spec.d.ts +2 -0
  167. package/tsconfig/dist/logging/__tests__/logger.spec.d.ts.map +1 -0
  168. package/tsconfig/dist/logging/__tests__/logger.spec.js +65 -0
  169. package/tsconfig/dist/logging/__tests__/logger.spec.js.map +1 -0
  170. package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts +2 -0
  171. package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts.map +1 -0
  172. package/tsconfig/dist/logging/__tests__/redaction.spec.js +34 -0
  173. package/tsconfig/dist/logging/__tests__/redaction.spec.js.map +1 -0
  174. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts +2 -0
  175. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts.map +1 -0
  176. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js +90 -0
  177. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js.map +1 -0
  178. package/tsconfig/dist/logging/index.d.ts +6 -0
  179. package/tsconfig/dist/logging/index.d.ts.map +1 -0
  180. package/tsconfig/dist/logging/index.js +6 -0
  181. package/tsconfig/dist/logging/index.js.map +1 -0
  182. package/tsconfig/dist/logging/logger.d.ts +9 -0
  183. package/tsconfig/dist/logging/logger.d.ts.map +1 -0
  184. package/tsconfig/dist/logging/logger.js +101 -0
  185. package/tsconfig/dist/logging/logger.js.map +1 -0
  186. package/tsconfig/dist/logging/redaction.d.ts +7 -0
  187. package/tsconfig/dist/logging/redaction.d.ts.map +1 -0
  188. package/tsconfig/dist/logging/redaction.js +23 -0
  189. package/tsconfig/dist/logging/redaction.js.map +1 -0
  190. package/tsconfig/dist/logging/sinks/json.d.ts +4 -0
  191. package/tsconfig/dist/logging/sinks/json.d.ts.map +1 -0
  192. package/tsconfig/dist/logging/sinks/json.js +23 -0
  193. package/tsconfig/dist/logging/sinks/json.js.map +1 -0
  194. package/tsconfig/dist/logging/sinks/stdout.d.ts +3 -0
  195. package/tsconfig/dist/logging/sinks/stdout.d.ts.map +1 -0
  196. package/tsconfig/dist/logging/sinks/stdout.js +24 -0
  197. package/tsconfig/dist/logging/sinks/stdout.js.map +1 -0
  198. package/tsconfig/dist/logging/types/index.d.ts +2 -0
  199. package/tsconfig/dist/logging/types/index.d.ts.map +1 -0
  200. package/tsconfig/dist/logging/types/index.js +2 -0
  201. package/tsconfig/dist/logging/types/index.js.map +1 -0
  202. package/tsconfig/dist/logging/types/types.d.ts +37 -0
  203. package/tsconfig/dist/logging/types/types.d.ts.map +1 -0
  204. package/tsconfig/dist/logging/types/types.js +2 -0
  205. package/tsconfig/dist/logging/types/types.js.map +1 -0
  206. package/tsconfig/dist/merge/layered-merge.d.ts +16 -0
  207. package/tsconfig/dist/merge/layered-merge.d.ts.map +1 -0
  208. package/tsconfig/dist/merge/layered-merge.js +97 -0
  209. package/tsconfig/dist/merge/layered-merge.js.map +1 -0
  210. package/tsconfig/dist/preset/resolve-preset.d.ts +29 -0
  211. package/tsconfig/dist/preset/resolve-preset.d.ts.map +1 -0
  212. package/tsconfig/dist/preset/resolve-preset.js +104 -0
  213. package/tsconfig/dist/preset/resolve-preset.js.map +1 -0
  214. package/tsconfig/dist/repo/__tests__/repo.spec.d.ts +2 -0
  215. package/tsconfig/dist/repo/__tests__/repo.spec.d.ts.map +1 -0
  216. package/tsconfig/dist/repo/__tests__/repo.spec.js +25 -0
  217. package/tsconfig/dist/repo/__tests__/repo.spec.js.map +1 -0
  218. package/tsconfig/dist/repo/index.d.ts +2 -0
  219. package/tsconfig/dist/repo/index.d.ts.map +1 -0
  220. package/tsconfig/dist/repo/index.js +2 -0
  221. package/tsconfig/dist/repo/index.js.map +1 -0
  222. package/tsconfig/dist/repo/repo.d.ts +6 -0
  223. package/tsconfig/dist/repo/repo.d.ts.map +1 -0
  224. package/tsconfig/dist/repo/repo.js +25 -0
  225. package/tsconfig/dist/repo/repo.js.map +1 -0
  226. package/tsconfig/dist/runtime/index.d.ts +2 -0
  227. package/tsconfig/dist/runtime/index.d.ts.map +1 -0
  228. package/tsconfig/dist/runtime/index.js +2 -0
  229. package/tsconfig/dist/runtime/index.js.map +1 -0
  230. package/tsconfig/dist/runtime/runtime.d.ts +46 -0
  231. package/tsconfig/dist/runtime/runtime.d.ts.map +1 -0
  232. package/tsconfig/dist/runtime/runtime.js +126 -0
  233. package/tsconfig/dist/runtime/runtime.js.map +1 -0
  234. package/tsconfig/dist/tsconfig.tools.tsbuildinfo +1 -0
  235. package/tsconfig/dist/tsconfig.tsbuildinfo +1 -0
  236. package/tsconfig/dist/types/index.d.ts +2 -0
  237. package/tsconfig/dist/types/index.d.ts.map +1 -0
  238. package/tsconfig/dist/types/index.js +2 -0
  239. package/tsconfig/dist/types/index.js.map +1 -0
  240. package/tsconfig/dist/types/init.d.ts +34 -0
  241. package/tsconfig/dist/types/init.d.ts.map +1 -0
  242. package/tsconfig/dist/types/init.js +6 -0
  243. package/tsconfig/dist/types/init.js.map +1 -0
  244. package/tsconfig/dist/types/preset.d.ts +27 -0
  245. package/tsconfig/dist/types/preset.d.ts.map +1 -0
  246. package/tsconfig/dist/types/preset.js +6 -0
  247. package/tsconfig/dist/types/preset.js.map +1 -0
  248. package/tsconfig/dist/types/types.d.ts +6 -0
  249. package/tsconfig/dist/types/types.d.ts.map +1 -0
  250. package/tsconfig/dist/types/types.js +2 -0
  251. package/tsconfig/dist/types/types.js.map +1 -0
  252. package/tsconfig/dist/utils/__tests__/env.spec.d.ts +2 -0
  253. package/tsconfig/dist/utils/__tests__/env.spec.d.ts.map +1 -0
  254. package/tsconfig/dist/utils/__tests__/env.spec.js +33 -0
  255. package/tsconfig/dist/utils/__tests__/env.spec.js.map +1 -0
  256. package/tsconfig/dist/utils/env.d.ts +7 -0
  257. package/tsconfig/dist/utils/env.d.ts.map +1 -0
  258. package/tsconfig/dist/utils/env.js +25 -0
  259. package/tsconfig/dist/utils/env.js.map +1 -0
  260. package/tsconfig/dist/utils/fs-atomic.d.ts +15 -0
  261. package/tsconfig/dist/utils/fs-atomic.d.ts.map +1 -0
  262. package/tsconfig/dist/utils/fs-atomic.js +45 -0
  263. package/tsconfig/dist/utils/fs-atomic.js.map +1 -0
  264. package/tsconfig/dist/utils/index.d.ts +3 -0
  265. package/tsconfig/dist/utils/index.d.ts.map +1 -0
  266. package/tsconfig/dist/utils/index.js +3 -0
  267. package/tsconfig/dist/utils/index.js.map +1 -0
  268. package/tsconfig/dist/utils/paths.d.ts +21 -0
  269. package/tsconfig/dist/utils/paths.d.ts.map +1 -0
  270. package/tsconfig/dist/utils/paths.js +32 -0
  271. package/tsconfig/dist/utils/paths.js.map +1 -0
  272. package/tsconfig/dist/utils/product-normalize.d.ts +27 -0
  273. package/tsconfig/dist/utils/product-normalize.d.ts.map +1 -0
  274. package/tsconfig/dist/utils/product-normalize.js +45 -0
  275. package/tsconfig/dist/utils/product-normalize.js.map +1 -0
  276. package/tsconfig/dist/validation/validate-config.d.ts +7 -0
  277. package/tsconfig/dist/validation/validate-config.d.ts.map +1 -0
  278. package/tsconfig/dist/validation/validate-config.js +22 -0
  279. package/tsconfig/dist/validation/validate-config.js.map +1 -0
  280. package/tsconfig/lib.json +13 -0
  281. package/tsconfig/node.json +12 -0
  282. package/tsconfig/react-app.json +8 -0
  283. package/tsconfig/react-lib.json +8 -0
  284. package/tsconfig/test.json +15 -0
  285. package/tsup/bin.js +172 -0
  286. package/tsup/dual.js +155 -0
  287. package/tsup/external-sync.mjs +41 -0
  288. package/tsup/external.mjs +109 -0
  289. package/tsup/node.js +65 -0
  290. package/tsup/react-lib.js +18 -0
  291. package/tsup/sdk.js +118 -0
  292. package/vite/react-app.js +50 -0
  293. package/vitest/node.js +37 -0
  294. package/vitest/react.js +39 -0
  295. package/vitest/vitest-setup.ts +9 -0
@@ -0,0 +1,915 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * KB Labs Quality Assurance Runner
5
+ *
6
+ * Runs comprehensive checks across entire monorepo:
7
+ * 1. Build all packages in correct layer order (incremental)
8
+ * 2. Run linter on all packages (with smart caching)
9
+ * 3. Run type-check on all packages (with smart caching)
10
+ * 4. Run tests on all packages (with smart caching)
11
+ *
12
+ * Features:
13
+ * - Incremental builds (skips packages where src/ hasn't changed)
14
+ * - Smart caching (skips lint/type-check/test for unchanged packages)
15
+ * - Per-package and per-repo filtering
16
+ * - Baseline regression detection for all 4 check types
17
+ * - JSON output for CI/CD and agents
18
+ *
19
+ * Usage:
20
+ * npx kb-devkit-qa # Run on all packages (with caching)
21
+ * npx kb-devkit-qa --json # JSON output for agents
22
+ * npx kb-devkit-qa --package=@kb-labs/cli-core # Run on specific package
23
+ * npx kb-devkit-qa --repo=kb-labs-core # Run on entire repo
24
+ * npx kb-devkit-qa --scope=workflow # Run on packages matching scope
25
+ * npx kb-devkit-qa --no-cache # Disable smart caching
26
+ * npx kb-devkit-qa --skip-build # Skip build phase
27
+ * npx kb-devkit-qa --skip-lint # Skip lint phase
28
+ * npx kb-devkit-qa --skip-types # Skip type-check phase
29
+ * npx kb-devkit-qa --skip-tests # Skip test phase
30
+ */
31
+
32
+ import { execSync } from 'child_process'
33
+ import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
34
+ import { join } from 'path'
35
+ import { createHash } from 'crypto'
36
+
37
+ // Parse args
38
+ const args = process.argv.slice(2)
39
+ const jsonMode = args.includes('--json')
40
+ const skipBuild = args.includes('--skip-build')
41
+ const skipLint = args.includes('--skip-lint')
42
+ const skipTypes = args.includes('--skip-types')
43
+ const skipTests = args.includes('--skip-tests')
44
+ const noCache = args.includes('--no-cache') // NEW: Disable caching
45
+
46
+ // NEW: Parse filter options
47
+ const packageFilter = args.find(arg => arg.startsWith('--package='))?.split('=')[1]
48
+ const repoFilter = args.find(arg => arg.startsWith('--repo='))?.split('=')[1]
49
+ const scopeFilter = args.find(arg => arg.startsWith('--scope='))?.split('=')[1]
50
+
51
+ // Cache directory
52
+ const CACHE_DIR = '.qa-cache'
53
+ const CACHE_FILE = join(CACHE_DIR, 'package-hashes.json')
54
+
55
+ // ANSI colors (disabled in JSON mode)
56
+ const colors = jsonMode ? {
57
+ reset: '', bright: '', red: '', green: '', yellow: '', blue: '', cyan: '', gray: '',
58
+ } : {
59
+ reset: '\x1b[0m',
60
+ bright: '\x1b[1m',
61
+ red: '\x1b[31m',
62
+ green: '\x1b[32m',
63
+ yellow: '\x1b[33m',
64
+ blue: '\x1b[34m',
65
+ cyan: '\x1b[36m',
66
+ gray: '\x1b[90m',
67
+ }
68
+
69
+ function log(msg, color = 'reset') {
70
+ if (!jsonMode) {
71
+ console.log(`${colors[color]}${msg}${colors.reset}`)
72
+ }
73
+ }
74
+
75
+ function header(msg) {
76
+ if (!jsonMode) {
77
+ log(`\n${colors.bright}${colors.cyan}${msg}${colors.reset}\n`)
78
+ }
79
+ }
80
+
81
+ const results = {
82
+ build: { passed: [], failed: [], skipped: [], errors: {} },
83
+ lint: { passed: [], failed: [], skipped: [], errors: {} },
84
+ typeCheck: { passed: [], failed: [], skipped: [], errors: {} },
85
+ test: { passed: [], failed: [], skipped: [], errors: {} },
86
+ }
87
+
88
+ // Get repo name from package path
89
+ function getRepoFromPackage(pkg) {
90
+ // Extract repo from path: /path/to/kb-labs-REPO/packages/...
91
+ const match = pkg.path.match(/\/(kb-labs-[^/]+)/)
92
+ return match ? match[1] : null
93
+ }
94
+
95
+ // Load package hash cache
96
+ function loadCache() {
97
+ if (!existsSync(CACHE_FILE)) {
98
+ return {}
99
+ }
100
+ try {
101
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'))
102
+ } catch (err) {
103
+ return {}
104
+ }
105
+ }
106
+
107
+ // Save package hash cache
108
+ function saveCache(cache) {
109
+ if (!existsSync(CACHE_DIR)) {
110
+ mkdirSync(CACHE_DIR, { recursive: true })
111
+ }
112
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2))
113
+ }
114
+
115
+ // Calculate hash for package sources
116
+ function calculatePackageHash(pkgPath) {
117
+ const srcDir = join(pkgPath, 'src')
118
+ const pkgJson = join(pkgPath, 'package.json')
119
+
120
+ const hash = createHash('sha256')
121
+
122
+ // Hash all files in src/ if it exists
123
+ if (existsSync(srcDir)) {
124
+ function hashDirectory(dir) {
125
+ try {
126
+ const files = readdirSync(dir, { withFileTypes: true })
127
+ for (const file of files) {
128
+ const fullPath = join(dir, file.name)
129
+ if (file.isDirectory()) {
130
+ hashDirectory(fullPath)
131
+ } else {
132
+ const content = readFileSync(fullPath, 'utf-8')
133
+ hash.update(content)
134
+ }
135
+ }
136
+ } catch (err) {
137
+ // Ignore permission errors
138
+ }
139
+ }
140
+ hashDirectory(srcDir)
141
+ }
142
+
143
+ // Hash package.json (dependencies/scripts)
144
+ if (existsSync(pkgJson)) {
145
+ const content = readFileSync(pkgJson, 'utf-8')
146
+ hash.update(content)
147
+ }
148
+
149
+ // Return null only if neither src nor package.json exist
150
+ if (!existsSync(srcDir) && !existsSync(pkgJson)) {
151
+ return null
152
+ }
153
+
154
+ return hash.digest('hex')
155
+ }
156
+
157
+ // Check if package sources have changed since last QA run
158
+ function hasPackageChanged(pkgName, pkgPath, cache) {
159
+ if (noCache) {return true} // Skip cache if --no-cache flag
160
+
161
+ const currentHash = calculatePackageHash(pkgPath)
162
+ if (!currentHash) {return true} // Nothing to hash, always run
163
+
164
+ const cachedHash = cache[pkgName]
165
+ return currentHash !== cachedHash
166
+ }
167
+
168
+ // Get all workspace packages (with optional filtering)
169
+ function getWorkspacePackages() {
170
+ try {
171
+ const output = execSync('pnpm list -r --depth -1 --json', {
172
+ encoding: 'utf-8',
173
+ stdio: ['pipe', 'pipe', 'ignore'],
174
+ })
175
+ let packages = JSON.parse(output)
176
+ .filter(pkg => pkg.name?.startsWith('@kb-labs/'))
177
+ .map(pkg => ({ name: pkg.name, path: pkg.path }))
178
+
179
+ // Apply filters
180
+ if (packageFilter) {
181
+ packages = packages.filter(p => p.name === packageFilter)
182
+ if (packages.length === 0) {
183
+ log(`โŒ Package not found: ${packageFilter}`, 'red')
184
+ process.exit(1)
185
+ }
186
+ }
187
+
188
+ if (repoFilter) {
189
+ packages = packages.filter(p => getRepoFromPackage(p) === repoFilter)
190
+ if (packages.length === 0) {
191
+ log(`โŒ No packages found in repo: ${repoFilter}`, 'red')
192
+ process.exit(1)
193
+ }
194
+ }
195
+
196
+ if (scopeFilter) {
197
+ packages = packages.filter(p => p.name.includes(scopeFilter))
198
+ if (packages.length === 0) {
199
+ log(`โŒ No packages found matching scope: ${scopeFilter}`, 'red')
200
+ process.exit(1)
201
+ }
202
+ }
203
+
204
+ return packages
205
+ } catch (err) {
206
+ log('Failed to get workspace packages', 'red')
207
+ process.exit(1)
208
+ }
209
+ }
210
+
211
+ // Get latest modification time in directory recursively
212
+ function getLatestMtime(dir) {
213
+ if (!existsSync(dir)) {return 0}
214
+
215
+ let latest = 0
216
+ try {
217
+ const files = readdirSync(dir, { withFileTypes: true })
218
+ for (const file of files) {
219
+ const fullPath = join(dir, file.name)
220
+ if (file.isDirectory()) {
221
+ const dirMtime = getLatestMtime(fullPath)
222
+ if (dirMtime > latest) {latest = dirMtime}
223
+ } else {
224
+ const stat = statSync(fullPath)
225
+ if (stat.mtimeMs > latest) {latest = stat.mtimeMs}
226
+ }
227
+ }
228
+ } catch (err) {
229
+ // Ignore permission errors
230
+ }
231
+ return latest
232
+ }
233
+
234
+ // Check if package needs rebuild
235
+ function needsRebuild(pkgPath) {
236
+ const srcDir = join(pkgPath, 'src')
237
+ const distDir = join(pkgPath, 'dist')
238
+
239
+ // If dist doesn't exist, needs build
240
+ if (!existsSync(distDir)) {return true}
241
+
242
+ // If src doesn't exist, skip (might be types-only package)
243
+ if (!existsSync(srcDir)) {return false}
244
+
245
+ // Compare modification times
246
+ const srcMtime = getLatestMtime(srcDir)
247
+ const distMtime = getLatestMtime(distDir)
248
+
249
+ // If src is newer than dist, needs rebuild
250
+ return srcMtime > distMtime
251
+ }
252
+
253
+ // Build all packages in layer order
254
+ async function buildAllLayers() {
255
+ header('๐Ÿ”จ Building all packages in correct dependency order...')
256
+
257
+ try {
258
+ // Generate build order
259
+ log('๐Ÿ“Š Calculating build order...', 'gray')
260
+ let buildOrder = execSync('npx kb-devkit-build-order --layers', {
261
+ encoding: 'utf-8',
262
+ stdio: ['pipe', 'pipe', 'ignore'],
263
+ })
264
+
265
+ // Strip ANSI codes
266
+ buildOrder = buildOrder.replace(/\x1b\[[0-9;]*m/g, '')
267
+
268
+ // Parse layers
269
+ const layerMatches = buildOrder.matchAll(/Layer (\d+).*?:\s+((?:@kb-labs\/[\w-]+\s*)+)/gs)
270
+ const layers = []
271
+
272
+ for (const match of layerMatches) {
273
+ const layerNum = parseInt(match[1])
274
+ const packages = match[2]
275
+ .trim()
276
+ .split(/\s+/)
277
+ .filter(pkg => pkg.startsWith('@kb-labs/'))
278
+ layers.push({ num: layerNum, packages })
279
+ }
280
+
281
+ log(`Found ${layers.length} layers to build\n`, 'gray')
282
+
283
+ // Get package paths for timestamp checking
284
+ const allPackages = getWorkspacePackages()
285
+ const pkgPathMap = new Map(allPackages.map(p => [p.name, p.path]))
286
+ const filteredPackageNames = new Set(allPackages.map(p => p.name))
287
+
288
+ // Build each layer
289
+ for (const layer of layers) {
290
+ // Filter layer packages based on current filters
291
+ const layerPackagesToBuild = layer.packages.filter(pkg => filteredPackageNames.has(pkg))
292
+
293
+ if (layerPackagesToBuild.length === 0) {
294
+ continue // Skip empty layers
295
+ }
296
+
297
+ log(`๐Ÿ”จ Building Layer ${layer.num}/${layers.length} (${layerPackagesToBuild.length} packages)...`, 'cyan')
298
+
299
+ for (const pkg of layerPackagesToBuild) {
300
+ const pkgPath = pkgPathMap.get(pkg)
301
+
302
+ // Check if rebuild is needed
303
+ if (pkgPath && !needsRebuild(pkgPath)) {
304
+ results.build.skipped.push(pkg)
305
+ if (!jsonMode) {process.stdout.write(`${colors.gray}-${colors.reset}`)}
306
+ continue
307
+ }
308
+
309
+ try {
310
+ execSync(`pnpm --filter ${pkg} run build`, {
311
+ encoding: 'utf-8',
312
+ stdio: 'pipe',
313
+ })
314
+ results.build.passed.push(pkg)
315
+ if (!jsonMode) {process.stdout.write(`${colors.green}.${colors.reset}`)}
316
+ } catch (err) {
317
+ results.build.failed.push(pkg)
318
+ results.build.errors[pkg] = err.stderr || err.stdout || err.message
319
+ if (!jsonMode) {process.stdout.write(`${colors.red}F${colors.reset}`)}
320
+ }
321
+ }
322
+ if (!jsonMode) {console.log('')}
323
+ }
324
+
325
+ log(`\nโœ… Build complete: ${results.build.passed.length} passed, ${results.build.failed.length} failed, ${results.build.skipped.length} skipped (up-to-date)\n`, 'green')
326
+
327
+ } catch (err) {
328
+ log('โŒ Build order calculation failed', 'red')
329
+ throw err
330
+ }
331
+ }
332
+
333
+ // Run command on all packages
334
+ async function runOnAllPackages(command, label, resultKey, cache) {
335
+ header(`${label}...`)
336
+
337
+ const packages = getWorkspacePackages()
338
+ log(`Running on ${packages.length} packages\n`, 'gray')
339
+
340
+ let cacheHits = 0
341
+
342
+ for (const pkg of packages) {
343
+ // NEW: Check if package has changed
344
+ const changed = hasPackageChanged(pkg.name, pkg.path, cache)
345
+
346
+ if (!changed && !noCache) {
347
+ // Skip unchanged package
348
+ results[resultKey].skipped.push(pkg.name)
349
+ cacheHits++
350
+ if (!jsonMode) {process.stdout.write(`${colors.gray}-${colors.reset}`)}
351
+ continue
352
+ }
353
+
354
+ try {
355
+ execSync(`pnpm --filter ${pkg.name} ${command}`, {
356
+ encoding: 'utf-8',
357
+ stdio: 'pipe',
358
+ })
359
+ results[resultKey].passed.push(pkg.name)
360
+ if (!jsonMode) {process.stdout.write(`${colors.green}.${colors.reset}`)}
361
+
362
+ // NEW: Update cache with current hash
363
+ const currentHash = calculatePackageHash(pkg.path)
364
+ if (currentHash) {
365
+ cache[pkg.name] = currentHash
366
+ }
367
+ } catch (err) {
368
+ // Check if script doesn't exist
369
+ if (err.message.includes('missing script')) {
370
+ results[resultKey].skipped.push(pkg.name)
371
+ if (!jsonMode) {process.stdout.write(`${colors.gray}-${colors.reset}`)}
372
+ } else {
373
+ results[resultKey].failed.push(pkg.name)
374
+ results[resultKey].errors[pkg.name] = err.stderr || err.stdout || err.message
375
+ if (!jsonMode) {process.stdout.write(`${colors.red}F${colors.reset}`)}
376
+
377
+ // Cache hash even for failed packages โ€” if code hasn't changed, no point re-running
378
+ const currentHash = calculatePackageHash(pkg.path)
379
+ if (currentHash) {
380
+ cache[pkg.name] = currentHash
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ if (!jsonMode) {console.log('')}
387
+
388
+ const skippedByCache = cacheHits
389
+ const totalSkipped = results[resultKey].skipped.length
390
+
391
+ if (skippedByCache > 0) {
392
+ log(`\nโœ… ${label} complete: ${results[resultKey].passed.length} passed, ${results[resultKey].failed.length} failed, ${totalSkipped} skipped (${skippedByCache} unchanged)\n`, 'green')
393
+ } else {
394
+ log(`\nโœ… ${label} complete: ${results[resultKey].passed.length} passed, ${results[resultKey].failed.length} failed, ${totalSkipped} skipped\n`, 'green')
395
+ }
396
+ }
397
+
398
+ // Load all baselines if they exist
399
+ function loadBaselines() {
400
+ const baselines = {
401
+ build: null,
402
+ lint: null,
403
+ typeCheck: null,
404
+ test: null,
405
+ }
406
+
407
+ // Load build baseline
408
+ const buildPath = '.baseline/build-failures.json'
409
+ if (existsSync(buildPath)) {
410
+ try {
411
+ baselines.build = JSON.parse(readFileSync(buildPath, 'utf-8'))
412
+ } catch (err) {
413
+ // Ignore parsing errors
414
+ }
415
+ }
416
+
417
+ // Load lint baseline (aggregated)
418
+ const lintPath = '.baseline/lint-errors-aggregated.json'
419
+ if (existsSync(lintPath)) {
420
+ try {
421
+ baselines.lint = JSON.parse(readFileSync(lintPath, 'utf-8'))
422
+ } catch (err) {
423
+ // Ignore parsing errors
424
+ }
425
+ }
426
+
427
+ // Load type-check baseline
428
+ const typesPath = '.baseline/type-errors.json'
429
+ if (existsSync(typesPath)) {
430
+ try {
431
+ baselines.typeCheck = JSON.parse(readFileSync(typesPath, 'utf-8'))
432
+ } catch (err) {
433
+ // Ignore parsing errors
434
+ }
435
+ }
436
+
437
+ // Load test baseline
438
+ const testPath = '.baseline/test-results.json'
439
+ if (existsSync(testPath)) {
440
+ try {
441
+ baselines.test = JSON.parse(readFileSync(testPath, 'utf-8'))
442
+ } catch (err) {
443
+ // Ignore parsing errors
444
+ }
445
+ }
446
+
447
+ return baselines
448
+ }
449
+
450
+ // Calculate diff with baselines for all check types
451
+ function calculateDiff(baselines) {
452
+ if (!baselines) {return null}
453
+
454
+ return {
455
+ build: calculateCheckDiff('build', baselines.build),
456
+ lint: calculateCheckDiff('lint', baselines.lint),
457
+ typeCheck: calculateCheckDiff('typeCheck', baselines.typeCheck),
458
+ test: calculateCheckDiff('test', baselines.test),
459
+ }
460
+ }
461
+
462
+ // Calculate diff for a specific check type
463
+ function calculateCheckDiff(checkKey, baseline) {
464
+ if (!baseline) {return null}
465
+
466
+ const currentFailed = new Set(results[checkKey].failed)
467
+ let baselineFailed = new Set()
468
+ const perPackage = []
469
+
470
+ // Extract baseline failures based on format
471
+ if (checkKey === 'build' && baseline.summary) {
472
+ // build-failures.json format: { packages: [{name, path, reason, errors}], summary: {...} }
473
+ if (baseline.packages && Array.isArray(baseline.packages)) {
474
+ // New format: has per-package data
475
+ baselineFailed = new Set(baseline.packages.map(pkg => pkg.name))
476
+ } else if (baseline.failures) {
477
+ // Old format: just array of package names
478
+ baselineFailed = new Set(baseline.failures || [])
479
+ }
480
+ } else if (checkKey === 'lint' && baseline.packages) {
481
+ // lint-errors-aggregated.json format: { packages: [{name, errors, warnings}] }
482
+ baselineFailed = new Set(
483
+ baseline.packages
484
+ .filter(pkg => pkg.errors > 0 || pkg.warnings > 0)
485
+ .map(pkg => pkg.name)
486
+ )
487
+
488
+ // Per-package comparison for lint
489
+ const allPackages = new Set([...currentFailed, ...baselineFailed])
490
+ for (const pkgName of allPackages) {
491
+ const baselinePkg = baseline.packages.find(p => p.name === pkgName)
492
+ const baselineErrors = baselinePkg ? (baselinePkg.errors + baselinePkg.warnings) : 0
493
+ const currentFailing = currentFailed.has(pkgName) ? 1 : 0
494
+
495
+ if (baselineErrors > 0 || currentFailing > 0) {
496
+ perPackage.push({
497
+ package: pkgName,
498
+ baseline: baselineErrors,
499
+ current: currentFailing ? '1+' : 0, // We don't have exact counts from current run
500
+ isRegression: currentFailing > 0 && baselineErrors === 0
501
+ })
502
+ }
503
+ }
504
+ } else if (checkKey === 'typeCheck' && baseline.packages) {
505
+ // type-errors.json format: { packages: [{name, errors, warnings, coverage}] }
506
+ baselineFailed = new Set(
507
+ baseline.packages
508
+ .filter(pkg => pkg.errors > 0)
509
+ .map(pkg => pkg.name)
510
+ )
511
+
512
+ // Per-package comparison for type-check
513
+ const allPackages = new Set([...currentFailed, ...baselineFailed])
514
+ for (const pkgName of allPackages) {
515
+ const baselinePkg = baseline.packages.find(p => p.name === pkgName)
516
+ const baselineErrors = baselinePkg ? baselinePkg.errors : 0
517
+ const currentFailing = currentFailed.has(pkgName) ? 1 : 0
518
+
519
+ if (baselineErrors > 0 || currentFailing > 0) {
520
+ perPackage.push({
521
+ package: pkgName,
522
+ baseline: baselineErrors,
523
+ current: currentFailing ? '1+' : 0,
524
+ isRegression: currentFailing > 0 && baselineErrors === 0
525
+ })
526
+ }
527
+ }
528
+ } else if (checkKey === 'test' && baseline.summary) {
529
+ // test-results.json format: { summary: {total, passed, failed}, packages: [{name, tests, passed, failed, status}] }
530
+ if (baseline.packages && Array.isArray(baseline.packages)) {
531
+ // New format: has per-package data
532
+ baselineFailed = new Set(
533
+ baseline.packages
534
+ .filter(pkg => pkg.failed > 0)
535
+ .map(pkg => pkg.name)
536
+ )
537
+
538
+ // Per-package comparison for tests
539
+ const allPackages = new Set([...currentFailed, ...baselineFailed])
540
+ for (const pkgName of allPackages) {
541
+ const baselinePkg = baseline.packages.find(p => p.name === pkgName)
542
+ const baselineFailed = baselinePkg ? baselinePkg.failed : 0
543
+ const currentFailing = currentFailed.has(pkgName) ? 1 : 0
544
+
545
+ if (baselineFailed > 0 || currentFailing > 0) {
546
+ perPackage.push({
547
+ package: pkgName,
548
+ baseline: baselineFailed,
549
+ current: currentFailing ? '1+' : 0,
550
+ isRegression: currentFailing > 0 && baselineFailed === 0
551
+ })
552
+ }
553
+ }
554
+ } else {
555
+ // Old format: no per-package data
556
+ baselineFailed = new Set() // No per-package data
557
+ }
558
+ }
559
+
560
+ const newFailures = [...currentFailed].filter(pkg => !baselineFailed.has(pkg))
561
+ const fixed = [...baselineFailed].filter(pkg => !currentFailed.has(pkg))
562
+ const stillFailing = [...currentFailed].filter(pkg => baselineFailed.has(pkg))
563
+
564
+ return {
565
+ newFailures,
566
+ fixed,
567
+ stillFailing,
568
+ summary: {
569
+ baseline: baselineFailed.size,
570
+ current: currentFailed.size,
571
+ delta: currentFailed.size - baselineFailed.size,
572
+ },
573
+ perPackage: perPackage.sort((a, b) => {
574
+ // Sort regressions first, then by baseline errors
575
+ if (a.isRegression !== b.isRegression) {return a.isRegression ? -1 : 1}
576
+ return (typeof b.baseline === 'number' ? b.baseline : 0) - (typeof a.baseline === 'number' ? a.baseline : 0)
577
+ })
578
+ }
579
+ }
580
+
581
+ // Group results by repo
582
+ function groupByRepo() {
583
+ const packages = getWorkspacePackages()
584
+ const byRepo = {}
585
+
586
+ // Helper to get status for a package in a check
587
+ const getStatus = (checkKey, pkgName) => {
588
+ if (results[checkKey].passed.includes(pkgName)) {return 'passed'}
589
+ if (results[checkKey].failed.includes(pkgName)) {return 'failed'}
590
+ if (results[checkKey].skipped.includes(pkgName)) {return 'skipped'}
591
+ return 'not-run'
592
+ }
593
+
594
+ // Process all packages
595
+ for (const pkg of packages) {
596
+ const repo = getRepoFromPackage(pkg)
597
+ if (!repo) {continue}
598
+
599
+ // Initialize repo if needed
600
+ if (!byRepo[repo]) {
601
+ byRepo[repo] = {
602
+ repo,
603
+ packages: [],
604
+ summary: {
605
+ build: { passed: 0, failed: 0, skipped: 0 },
606
+ lint: { passed: 0, failed: 0, skipped: 0 },
607
+ typeCheck: { passed: 0, failed: 0, skipped: 0 },
608
+ test: { passed: 0, failed: 0, skipped: 0 },
609
+ }
610
+ }
611
+ }
612
+
613
+ // Add package with statuses
614
+ const pkgData = {
615
+ name: pkg.name,
616
+ build: getStatus('build', pkg.name),
617
+ lint: getStatus('lint', pkg.name),
618
+ typeCheck: getStatus('typeCheck', pkg.name),
619
+ test: getStatus('test', pkg.name),
620
+ }
621
+ byRepo[repo].packages.push(pkgData)
622
+
623
+ // Update summary counts
624
+ for (const checkKey of ['build', 'lint', 'typeCheck', 'test']) {
625
+ const status = pkgData[checkKey]
626
+ if (status === 'passed') {byRepo[repo].summary[checkKey].passed++}
627
+ else if (status === 'failed') {byRepo[repo].summary[checkKey].failed++}
628
+ else if (status === 'skipped') {byRepo[repo].summary[checkKey].skipped++}
629
+ }
630
+ }
631
+
632
+ return byRepo
633
+ }
634
+
635
+ // Print JSON report for agents
636
+ function printJsonReport(baselines, diff) {
637
+ // Group by repo if filtering is active
638
+ const byRepo = (packageFilter || repoFilter || scopeFilter) ? groupByRepo() : null
639
+
640
+ const report = {
641
+ status: (results.build.failed.length + results.lint.failed.length +
642
+ results.typeCheck.failed.length + results.test.failed.length) === 0 ? 'passed' : 'failed',
643
+ timestamp: new Date().toISOString(),
644
+ filter: {
645
+ package: packageFilter || null,
646
+ repo: repoFilter || null,
647
+ scope: scopeFilter || null,
648
+ },
649
+ summary: {
650
+ build: {
651
+ passed: results.build.passed.length,
652
+ failed: results.build.failed.length,
653
+ skipped: results.build.skipped.length,
654
+ },
655
+ lint: {
656
+ passed: results.lint.passed.length,
657
+ failed: results.lint.failed.length,
658
+ skipped: results.lint.skipped.length,
659
+ },
660
+ typeCheck: {
661
+ passed: results.typeCheck.passed.length,
662
+ failed: results.typeCheck.failed.length,
663
+ skipped: results.typeCheck.skipped.length,
664
+ },
665
+ test: {
666
+ passed: results.test.passed.length,
667
+ failed: results.test.failed.length,
668
+ skipped: results.test.skipped.length,
669
+ },
670
+ },
671
+ failures: {
672
+ build: results.build.failed,
673
+ lint: results.lint.failed,
674
+ typeCheck: results.typeCheck.failed,
675
+ test: results.test.failed,
676
+ },
677
+ errors: {
678
+ build: results.build.errors,
679
+ lint: results.lint.errors,
680
+ typeCheck: results.typeCheck.errors,
681
+ test: results.test.errors,
682
+ },
683
+ baseline: diff ? {
684
+ build: diff.build,
685
+ lint: diff.lint,
686
+ typeCheck: diff.typeCheck,
687
+ test: diff.test,
688
+ } : null,
689
+ byRepo: byRepo || null,
690
+ }
691
+
692
+ console.log(JSON.stringify(report, null, 2))
693
+ }
694
+
695
+ // Print human-readable report
696
+ function printHumanReport(baselines, diff) {
697
+ header('๐Ÿ“Š QA Summary Report')
698
+
699
+ const checks = [
700
+ { name: 'Build', key: 'build', icon: '๐Ÿ”จ' },
701
+ { name: 'Lint', key: 'lint', icon: '๐Ÿ”' },
702
+ { name: 'Type Check', key: 'typeCheck', icon: '๐Ÿ“˜' },
703
+ { name: 'Tests', key: 'test', icon: '๐Ÿงช' },
704
+ ]
705
+
706
+ let totalPassed = 0
707
+ let totalFailed = 0
708
+ let totalSkipped = 0
709
+ let hasFailures = false
710
+
711
+ for (const check of checks) {
712
+ const result = results[check.key]
713
+ const passed = result.passed.length
714
+ const failed = result.failed.length
715
+ const skipped = result.skipped.length
716
+ const total = passed + failed + skipped
717
+
718
+ totalPassed += passed
719
+ totalFailed += failed
720
+ totalSkipped += skipped
721
+
722
+ if (failed > 0) {hasFailures = true}
723
+
724
+ const status = failed > 0 ? `${colors.red}โŒ` : `${colors.green}โœ…`
725
+ const percentage = total > 0 ? Math.round((passed / (total - skipped)) * 100) : 100
726
+
727
+ log(`${status} ${check.icon} ${check.name.padEnd(12)} ${passed}/${total - skipped} passed (${percentage}%)${colors.reset}`)
728
+
729
+ // Show failed packages
730
+ if (failed > 0) {
731
+ log(` ${colors.red}Failed packages:${colors.reset}`, 'red')
732
+ for (const pkg of result.failed.slice(0, 10)) {
733
+ log(` - ${pkg}`, 'red')
734
+ }
735
+ if (failed > 10) {
736
+ log(` ... and ${failed - 10} more`, 'gray')
737
+ }
738
+ }
739
+ }
740
+
741
+ // Show baseline diff if available
742
+ if (diff) {
743
+ log('')
744
+ log('๐Ÿ“ˆ Baseline Comparison:', 'cyan')
745
+
746
+ const checkTypes = [
747
+ { key: 'build', label: 'Build', icon: '๐Ÿ”จ' },
748
+ { key: 'lint', label: 'Lint', icon: '๐Ÿ”' },
749
+ { key: 'typeCheck', label: 'Type Check', icon: '๐Ÿ“˜' },
750
+ { key: 'test', label: 'Tests', icon: '๐Ÿงช' },
751
+ ]
752
+
753
+ for (const checkType of checkTypes) {
754
+ const checkDiff = diff[checkType.key]
755
+ if (!checkDiff) {continue}
756
+
757
+ log(`\n${checkType.icon} ${checkType.label}:`, 'cyan')
758
+ log(` Baseline: ${checkDiff.summary.baseline} failed`, 'gray')
759
+ log(` Current: ${checkDiff.summary.current} failed`, 'gray')
760
+
761
+ if (checkDiff.summary.delta > 0) {
762
+ log(` โŒ +${checkDiff.summary.delta} NEW failures (regression!)`, 'red')
763
+ } else if (checkDiff.summary.delta < 0) {
764
+ log(` โœ… ${Math.abs(checkDiff.summary.delta)} failures FIXED (improvement!)`, 'green')
765
+ } else {
766
+ log(` โžก๏ธ No change`, 'yellow')
767
+ }
768
+
769
+ if (checkDiff.newFailures.length > 0) {
770
+ log(` ๐Ÿ†• New failures (${checkDiff.newFailures.length}):`, 'red')
771
+ for (const pkg of checkDiff.newFailures.slice(0, 3)) {
772
+ log(` - ${pkg}`, 'red')
773
+ }
774
+ if (checkDiff.newFailures.length > 3) {
775
+ log(` ... and ${checkDiff.newFailures.length - 3} more`, 'gray')
776
+ }
777
+ }
778
+
779
+ if (checkDiff.fixed.length > 0) {
780
+ log(` โœ… Fixed (${checkDiff.fixed.length}):`, 'green')
781
+ for (const pkg of checkDiff.fixed.slice(0, 3)) {
782
+ log(` - ${pkg}`, 'green')
783
+ }
784
+ if (checkDiff.fixed.length > 3) {
785
+ log(` ... and ${checkDiff.fixed.length - 3} more`, 'gray')
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ log('')
792
+ log('โ”€'.repeat(60), 'gray')
793
+ log(`Total: ${totalPassed} passed, ${totalFailed} failed, ${totalSkipped} skipped`, 'bright')
794
+ log('')
795
+
796
+ if (hasFailures) {
797
+ log('โŒ QA checks failed. See details above.', 'red')
798
+ process.exit(1)
799
+ } else {
800
+ log('โœ… All QA checks passed!', 'green')
801
+ process.exit(0)
802
+ }
803
+ }
804
+
805
+ // Main
806
+ async function main() {
807
+ if (!jsonMode) {
808
+ let header = `\n${colors.bright}${colors.blue}๐Ÿš€ KB Labs QA Runner${colors.reset}`
809
+
810
+ // Show filter info
811
+ if (packageFilter) {
812
+ header += `\n${colors.yellow}๐Ÿ“ฆ Filtered: --package=${packageFilter}${colors.reset}`
813
+ } else if (repoFilter) {
814
+ header += `\n${colors.yellow}๐Ÿ“ Filtered: --repo=${repoFilter}${colors.reset}`
815
+ } else if (scopeFilter) {
816
+ header += `\n${colors.yellow}๐Ÿ” Filtered: --scope=${scopeFilter}${colors.reset}`
817
+ }
818
+
819
+ if (noCache) {
820
+ header += `\n${colors.yellow}๐Ÿšซ Cache disabled (--no-cache)${colors.reset}`
821
+ }
822
+
823
+ log(header + '\n')
824
+ }
825
+
826
+ try {
827
+ // NEW: Load cache at the start
828
+ const cache = loadCache()
829
+
830
+ // 1. Build (in correct order)
831
+ if (!skipBuild) {
832
+ await buildAllLayers()
833
+ } else {
834
+ log('โญ๏ธ Skipping build (--skip-build)', 'yellow')
835
+ }
836
+
837
+ // 2. Lint
838
+ if (!skipLint) {
839
+ await runOnAllPackages('run lint', '๐Ÿ” Running linter', 'lint', cache)
840
+ } else {
841
+ log('โญ๏ธ Skipping lint (--skip-lint)', 'yellow')
842
+ }
843
+
844
+ // 3. Type check
845
+ if (!skipTypes) {
846
+ await runOnAllPackages('run type-check', '๐Ÿ“˜ Running type check', 'typeCheck', cache)
847
+ } else {
848
+ log('โญ๏ธ Skipping type-check (--skip-types)', 'yellow')
849
+ }
850
+
851
+ // 4. Tests
852
+ if (!skipTests) {
853
+ await runOnAllPackages('run test', '๐Ÿงช Running tests', 'test', cache)
854
+ } else {
855
+ log('โญ๏ธ Skipping tests (--skip-tests)', 'yellow')
856
+ }
857
+
858
+ // NEW: Save cache at the end
859
+ if (!noCache) {
860
+ saveCache(cache)
861
+ }
862
+
863
+ // Save last run results so core:gate can read fresh data without re-running
864
+ try {
865
+ const lastRunPath = join(CACHE_DIR, 'last-run.json')
866
+ writeFileSync(lastRunPath, JSON.stringify({
867
+ timestamp: new Date().toISOString(),
868
+ git: (() => {
869
+ try {
870
+ const commit = execSync('git rev-parse HEAD', { encoding: 'utf-8', stdio: ['pipe','pipe','ignore'] }).trim()
871
+ return { commit }
872
+ } catch { return {} }
873
+ })(),
874
+ failedPackages: {
875
+ build: results.build.failed,
876
+ lint: results.lint.failed,
877
+ typeCheck: results.typeCheck.failed,
878
+ test: results.test.failed,
879
+ }
880
+ }, null, 2))
881
+ } catch { /* non-critical */ }
882
+
883
+ // Load baselines and calculate diff
884
+ const baselines = loadBaselines()
885
+ const diff = calculateDiff(baselines)
886
+
887
+ // Print report
888
+ if (jsonMode) {
889
+ printJsonReport(baselines, diff)
890
+ } else {
891
+ printHumanReport(baselines, diff)
892
+ }
893
+
894
+ // Auto-save QA results to history
895
+ if (!jsonMode) {
896
+ log('\n๐Ÿ’พ Saving QA results to history...', 'cyan')
897
+ try {
898
+ execSync('pnpm qa:save', { stdio: 'inherit' })
899
+ log('โœ… Results saved successfully\n', 'green')
900
+ } catch (err) {
901
+ log('โš ๏ธ Failed to save results (this is non-critical)', 'yellow')
902
+ }
903
+ }
904
+
905
+ } catch (err) {
906
+ if (jsonMode) {
907
+ console.log(JSON.stringify({ status: 'error', message: err.message }, null, 2))
908
+ } else {
909
+ log(`\nโŒ QA runner failed: ${err.message}`, 'red')
910
+ }
911
+ process.exit(1)
912
+ }
913
+ }
914
+
915
+ main()