@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
package/sync/index.mjs ADDED
@@ -0,0 +1,693 @@
1
+ // Public API: sync runner used by bin and by consumers via import('@kb-labs/devkit/sync')
2
+ import { createHash } from 'node:crypto';
3
+ import { cp, mkdir, readFile, readdir, writeFile, access } from 'node:fs/promises';
4
+ import { dirname, join, resolve, relative } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import process from 'node:process';
7
+ import { glob } from 'glob';
8
+
9
+ // helpers
10
+ async function readDevkitMeta() {
11
+ try {
12
+ const pkgJson = JSON.parse(await readFile(resolve(DEVKIT_ROOT, 'package.json'), 'utf8'));
13
+ return { name: pkgJson.name ?? '@kb-labs/devkit', version: pkgJson.version ?? null, description: pkgJson.description ?? null, commit: null };
14
+ } catch {
15
+ return { name: '@kb-labs/devkit', version: null, description: null, commit: null };
16
+ }
17
+ }
18
+ function nowIso() { return new Date().toISOString(); }
19
+ function relFromDevkit(absPath) {
20
+ const p = absPath && absPath.startsWith(DEVKIT_ROOT) ? absPath.slice(DEVKIT_ROOT.length + 1) : absPath;
21
+ return (p ?? '').replaceAll('\\', '/');
22
+ }
23
+ function relFromRepo(root, absPath) {
24
+ const p = absPath && absPath.startsWith(root) ? absPath.slice(root.length + 1) : absPath;
25
+ return (p ?? '').replaceAll('\\', '/');
26
+ }
27
+ function cryptoRandomId() {
28
+ return createHash('sha256').update(String(Math.random()) + String(Date.now())).digest('hex').slice(0, 32);
29
+ }
30
+
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+ const DEVKIT_ROOT = resolve(__dirname, '..');
34
+
35
+ const BASE_MAP = {
36
+ agents: {
37
+ from: resolve(DEVKIT_ROOT, 'agents'),
38
+ to: (root) => resolve(root, '.kb/devkit/agents'),
39
+ type: 'dir',
40
+ },
41
+ cursorrules: {
42
+ from: resolve(DEVKIT_ROOT, '.cursorrules'),
43
+ to: (root) => resolve(root, '.cursorrules'),
44
+ type: 'file',
45
+ },
46
+ vscode: {
47
+ from: resolve(DEVKIT_ROOT, '.vscode/settings.json'),
48
+ to: (root) => resolve(root, '.vscode/settings.json'),
49
+ type: 'file',
50
+ },
51
+ ci: {
52
+ from: resolve(DEVKIT_ROOT, '.github/workflow-templates'),
53
+ to: (root) => resolve(root, '.github/workflows'),
54
+ type: 'dir',
55
+ },
56
+ configs: {
57
+ from: resolve(DEVKIT_ROOT, 'templates/configs'),
58
+ to: (root) => root, // Root of package
59
+ type: 'configs', // Special type for config drift checking
60
+ }
61
+ };
62
+
63
+ function resolveFromDevkit(p) {
64
+ return p && p.startsWith('/') ? p : resolve(DEVKIT_ROOT, p);
65
+ }
66
+
67
+ function buildEffectiveMap(projectCfg) {
68
+ const map = Object.fromEntries(Object.entries(BASE_MAP).map(([k, v]) => [k, { ...v }]));
69
+ const syncCfg = projectCfg?.sync || {};
70
+
71
+ if (syncCfg.overrides && typeof syncCfg.overrides === 'object') {
72
+ for (const [key, ov] of Object.entries(syncCfg.overrides)) {
73
+ if (!map[key]) {continue;}
74
+ if (ov.from) {map[key].from = resolveFromDevkit(String(ov.from));}
75
+ if (ov.to) {map[key].to = (root) => resolve(root, String(ov.to));}
76
+ if (ov.type) {map[key].type = String(ov.type);}
77
+ }
78
+ }
79
+
80
+ if (syncCfg.targets && typeof syncCfg.targets === 'object') {
81
+ for (const [key, t] of Object.entries(syncCfg.targets)) {
82
+ if (!t?.from || !t?.to) {continue;}
83
+ map[key] = {
84
+ from: resolveFromDevkit(String(t.from)),
85
+ to: (root) => resolve(root, String(t.to)),
86
+ type: String(t.type || 'dir'),
87
+ };
88
+ }
89
+ }
90
+ return map;
91
+ }
92
+
93
+ const log = (...a) => console.log('[devkit-sync]', ...a);
94
+ const warn = (...a) => console.warn('[devkit-sync]', ...a);
95
+
96
+ async function exists(p) { try { await access(p); return true; } catch { return false; } }
97
+ async function ensureDirForFile(p) { await mkdir(dirname(p), { recursive: true }); }
98
+
99
+ async function sha256File(p) {
100
+ const buf = await readFile(p);
101
+ return createHash('sha256').update(buf).digest('hex');
102
+ }
103
+
104
+ async function listFilesRec(root) {
105
+ const out = [];
106
+ async function walk(dir) {
107
+ const entries = await readdir(dir, { withFileTypes: true });
108
+ for (const e of entries) {
109
+ const p = join(dir, e.name);
110
+ if (e.isDirectory()) {await walk(p);} else {out.push(p);}
111
+ }
112
+ }
113
+ await walk(root);
114
+ return out;
115
+ }
116
+
117
+ async function comparePaths(src, dst, type) {
118
+ const diffs = [];
119
+ const onlySrc = [];
120
+ const onlyDst = [];
121
+
122
+ if (type === 'file') {
123
+ const srcOk = await exists(src);
124
+ const dstOk = await exists(dst);
125
+ if (!srcOk && !dstOk) {return { diffs, onlySrc, onlyDst };}
126
+ if (srcOk && !dstOk) { onlySrc.push(dst); return { diffs, onlySrc, onlyDst }; }
127
+ if (!srcOk && dstOk) { onlyDst.push(dst); return { diffs, onlySrc, onlyDst }; }
128
+ const [a, b] = await Promise.all([sha256File(src), sha256File(dst)]);
129
+ if (a !== b) {diffs.push(dst);}
130
+ return { diffs, onlySrc, onlyDst };
131
+ }
132
+
133
+ const srcOk = await exists(src);
134
+ const dstOk = await exists(dst);
135
+ if (!srcOk && !dstOk) {return { diffs, onlySrc, onlyDst };}
136
+ if (srcOk && !dstOk) {
137
+ const files = await listFilesRec(src);
138
+ onlySrc.push(...files.map(f => f.replace(src, dst)));
139
+ return { diffs, onlySrc, onlyDst };
140
+ }
141
+ if (!srcOk && dstOk) {
142
+ const files = await listFilesRec(dst);
143
+ onlyDst.push(...files);
144
+ return { diffs, onlySrc, onlyDst };
145
+ }
146
+
147
+ const srcFiles = await listFilesRec(src);
148
+ const dstFiles = await listFilesRec(dst);
149
+ const srcRel = new Map(srcFiles.map(p => [p.slice(src.length + 1), p]));
150
+ const dstRel = new Map(dstFiles.map(p => [p.slice(dst.length + 1), p]));
151
+
152
+ for (const [rel, pSrc] of srcRel) {
153
+ const pDst = dstRel.get(rel);
154
+ if (!pDst) { onlySrc.push(join(dst, rel)); continue; }
155
+ const [a, b] = await Promise.all([sha256File(pSrc), sha256File(pDst)]);
156
+ if (a !== b) {diffs.push(pDst);}
157
+ }
158
+ for (const [rel, pDst] of dstRel) {
159
+ if (!srcRel.has(rel)) {onlyDst.push(pDst);}
160
+ }
161
+ return { diffs, onlySrc, onlyDst };
162
+ }
163
+
164
+ function parseArgs(argv) {
165
+ const args = [...argv];
166
+ const flags = new Set();
167
+ const kv = new Map();
168
+ const positional = [];
169
+
170
+ for (const a of args) {
171
+ if (a === '--') {break;}
172
+ if (a.startsWith('--')) {
173
+ const [k, v] = a.split('=', 2);
174
+ if (v === undefined) {flags.add(k);}
175
+ else {kv.set(k, v);}
176
+ } else {
177
+ positional.push(a);
178
+ }
179
+ }
180
+
181
+ const help = flags.has('--help') || flags.has('-h');
182
+ const version = flags.has('--version') || flags.has('-v');
183
+ const check = flags.has('--check');
184
+ const force = flags.has('--force');
185
+ const verbose = flags.has('--verbose');
186
+ const json = flags.has('--json');
187
+ const dryRun = flags.has('--dry-run');
188
+ const ciOnly = flags.has('--ci-only');
189
+ const list = flags.has('--list');
190
+ const timeoutMs = Number(kv.get('--timeout') ?? process.env.KB_DEVKIT_SYNC_TIMEOUT_MS ?? 30000);
191
+ const onlyList = kv.get('--only')?.split(',').map(s => s.trim()).filter(Boolean) ?? [];
192
+
193
+ const scopeRaw = (kv.get('--scope') ?? process.env.KB_DEVKIT_SYNC_SCOPE ?? '').toString();
194
+ const scope = ['managed-only', 'strict', 'all'].includes(scopeRaw) ? scopeRaw : 'managed-only';
195
+
196
+ return { help, version, check, force, verbose, json, dryRun, ciOnly, list, timeoutMs, onlyList, positional, scope };
197
+ }
198
+
199
+ async function readProjectConfig(root) {
200
+ const p = resolve(root, 'kb-labs.config.json');
201
+ try { return JSON.parse(await readFile(p, 'utf8')); } catch { return {}; }
202
+ }
203
+
204
+ function resolveTargets(effectiveMap, { onlyList, positional, disabledSet }) {
205
+ const all = Object.keys(effectiveMap).filter(k => !disabledSet.has(k));
206
+ if (onlyList.length === 0 && positional.length === 0) {return all;}
207
+ const requested = [...onlyList, ...positional].filter(Boolean);
208
+ return requested.filter(k => effectiveMap[k] && !disabledSet.has(k));
209
+ }
210
+
211
+ async function writeProvenance(root, { items = [], scope = 'managed-only', report = null, fileName = 'DEVKIT_SYNC.json' } = {}) {
212
+ const meta = await readDevkitMeta();
213
+ await mkdir(resolve(root, '.kb/devkit/tmp'), { recursive: true });
214
+ const payload = {
215
+ source: meta.name,
216
+ version: meta.version,
217
+ when: nowIso(),
218
+ scope,
219
+ items,
220
+ };
221
+ if (report) {payload.report = report;}
222
+ await writeFile(resolve(root, `.kb/devkit/tmp/${fileName}`), JSON.stringify(payload, null, 2));
223
+ }
224
+
225
+ async function runCheck(root, effectiveMap, targets, { verbose, scope }) {
226
+ const details = [];
227
+ const summary = { ok: 0, drift: 0 };
228
+ const considerOnlyDst = scope !== 'managed-only';
229
+ const checkTargets = [];
230
+
231
+ for (const key of targets) {
232
+ const { from, to, type } = effectiveMap[key];
233
+ const dst = to(root);
234
+ const res = await comparePaths(from, dst, type);
235
+
236
+ const changed = (res.diffs.length + res.onlySrc.length + (considerOnlyDst ? res.onlyDst.length : 0)) > 0;
237
+ const item = {
238
+ key,
239
+ type,
240
+ dst,
241
+ diffs: res.diffs,
242
+ onlySrc: res.onlySrc,
243
+ onlyDst: considerOnlyDst ? res.onlyDst : [],
244
+ };
245
+ if (!considerOnlyDst && res.onlyDst.length) {
246
+ item.ignored = { onlyDst: res.onlyDst.length };
247
+ }
248
+
249
+ const files = [];
250
+ async function addChangedWithHashes(pDstAbs) {
251
+ const rel = pDstAbs.startsWith(dst) ? pDstAbs.slice(dst.length + 1) : null;
252
+ const pSrcAbs = rel ? join(from, rel) : null;
253
+ let hashSrc = null, hashDst = null;
254
+ try { hashDst = await sha256File(pDstAbs); } catch { }
255
+ if (pSrcAbs) { try { hashSrc = await sha256File(pSrcAbs); } catch { } }
256
+ files.push({
257
+ fromPath: rel ? relFromDevkit(pSrcAbs) : null,
258
+ toPath: relFromRepo(root, pDstAbs),
259
+ action: 'changed',
260
+ hashBefore: hashDst,
261
+ hashAfter: hashSrc
262
+ });
263
+ }
264
+ for (const p of res.diffs) { await addChangedWithHashes(p); }
265
+ for (const p of res.onlySrc) {
266
+ const rel = p.startsWith(dst) ? p.slice(dst.length + 1) : null;
267
+ const pSrcAbs = rel ? join(from, rel) : null;
268
+ let hashSrc = null; try { if (pSrcAbs) {hashSrc = await sha256File(pSrcAbs);} } catch { }
269
+ files.push({
270
+ fromPath: rel ? relFromDevkit(pSrcAbs) : null,
271
+ toPath: relFromRepo(root, p),
272
+ action: 'missing-dst',
273
+ hashBefore: null,
274
+ hashAfter: hashSrc
275
+ });
276
+ }
277
+ if (considerOnlyDst) {
278
+ for (const p of res.onlyDst) {
279
+ let hashDst = null; try { hashDst = await sha256File(p); } catch { }
280
+ files.push({
281
+ fromPath: null,
282
+ toPath: relFromRepo(root, p),
283
+ action: 'foreign',
284
+ hashBefore: hashDst,
285
+ hashAfter: null
286
+ });
287
+ }
288
+ }
289
+ const tStatus = (res.diffs.length || res.onlySrc.length || (considerOnlyDst ? res.onlyDst.length : 0)) ? 'drift' : 'ok';
290
+ checkTargets.push({ id: key, status: tStatus, files });
291
+
292
+ details.push(item);
293
+ if (changed) {
294
+ summary.drift++;
295
+ log(`drift ${key}:`, JSON.stringify(item, null, 2));
296
+ } else {
297
+ summary.ok++;
298
+ if (verbose) {log(`ok ${key}: no drift`);}
299
+ }
300
+ }
301
+ log('check done', summary);
302
+
303
+ const meta = await readDevkitMeta();
304
+ const report = {
305
+ schemaVersion: '2-min',
306
+ devkit: { version: meta.version, commit: meta.commit },
307
+ repo: {},
308
+ run: { id: cryptoRandomId(), startedAt: null, finishedAt: null },
309
+ summary: { filesChanged: summary.drift, kept: summary.ok, skipped: 0, conflicts: 0, mode: 'check', driftCount: summary.drift },
310
+ targets: checkTargets
311
+ };
312
+ await writeProvenance(root, { items: checkTargets.map(t => t.id), scope, report, fileName: 'DEVKIT_CHECK.json' });
313
+ log('check report written to', '.kb/devkit/tmp/DEVKIT_CHECK.json');
314
+
315
+ return { code: summary.drift > 0 ? 2 : 0, summary, details };
316
+ }
317
+
318
+ async function runSync(root, effectiveMap, targets, { force, verbose, dryRun }) {
319
+ const details = [];
320
+ const startedAt = Date.now();
321
+ const reportTargets = [];
322
+ let filesChanged = 0, keptCount = 0, skippedCount = 0;
323
+ const summary = { synced: 0, kept: 0, skipped: 0 };
324
+
325
+ for (const key of targets) {
326
+ const { from, to, type } = effectiveMap[key];
327
+ const dst = to(root);
328
+ const tReport = { id: key, status: 'pending', why: [], files: [] };
329
+
330
+ const srcOk = await exists(from);
331
+ if (!srcOk) {
332
+ summary.skipped++;
333
+ details.push({ key, action: 'skip', reason: 'source-missing', from, dst });
334
+ warn(`skip ${key} — source not found: ${from}`);
335
+ tReport.status = 'skipped';
336
+ reportTargets.push(tReport);
337
+ continue;
338
+ }
339
+ if (!force && await exists(dst)) {
340
+ summary.kept++;
341
+ details.push({ key, action: 'keep', from, dst });
342
+ tReport.status = 'kept';
343
+ if (type === 'file') {
344
+ const h = await sha256File(dst).catch(() => null);
345
+ tReport.files.push({
346
+ fromPath: relFromDevkit(from),
347
+ toPath: relFromRepo(root, dst),
348
+ action: 'keep',
349
+ hashBefore: h,
350
+ hashAfter: h
351
+ });
352
+ }
353
+ keptCount++;
354
+ reportTargets.push(tReport);
355
+ if (verbose) {log(`keep ${key} — exists: ${dst}`);}
356
+ continue;
357
+ }
358
+ if (dryRun) {
359
+ summary.synced++;
360
+ details.push({ key, action: 'plan-sync', from, dst, type });
361
+ tReport.status = 'planned';
362
+ reportTargets.push(tReport);
363
+ log(`[dry-run] ${key} -> ${dst}`);
364
+ continue;
365
+ }
366
+ let filePairs = [];
367
+ if (type === 'file') {
368
+ filePairs = [[from, dst]];
369
+ } else {
370
+ const srcFiles = await listFilesRec(from);
371
+ filePairs = srcFiles.map(sf => {
372
+ const rel = sf.slice(from.length + 1);
373
+ return [sf, join(dst, rel)];
374
+ });
375
+ }
376
+ const beforeState = await Promise.all(filePairs.map(async ([srcFile, dstFile]) => {
377
+ const existed = await exists(dstFile);
378
+ const hb = existed ? await sha256File(dstFile).catch(() => null) : null;
379
+ return { srcFile, dstFile, existed, hashBefore: hb };
380
+ }));
381
+
382
+ if (type === 'file') {await ensureDirForFile(dst);} else {await mkdir(dst, { recursive: true });}
383
+ await cp(from, dst, { recursive: true, force: true });
384
+
385
+ const afterState = await Promise.all(beforeState.map(async (s) => {
386
+ const ha = await sha256File(s.dstFile).catch(() => null);
387
+ const action = s.existed ? ((s.hashBefore && ha && s.hashBefore === ha) ? 'keep' : 'update') : 'create';
388
+ if (action !== 'keep') {filesChanged++;}
389
+ tReport.files.push({
390
+ fromPath: relFromDevkit(s.srcFile),
391
+ toPath: relFromRepo(root, s.dstFile),
392
+ action,
393
+ hashBefore: s.hashBefore ?? null,
394
+ hashAfter: ha ?? null
395
+ });
396
+ return { ...s, hashAfter: ha, action };
397
+ }));
398
+ const created = afterState.filter(s => s.action === 'create').length;
399
+ const updated = afterState.filter(s => s.action === 'update').length;
400
+ const keptF = afterState.filter(s => s.action === 'keep').length;
401
+ tReport.status = 'applied';
402
+ reportTargets.push(tReport);
403
+ summary.synced++;
404
+ details.push({ key, action: 'synced', from, dst, type });
405
+ log(`→ ${key}: ${created} created, ${updated} updated, ${keptF} kept`);
406
+ log(`synced ${key} -> ${dst}`);
407
+ }
408
+ const finishedAt = Date.now();
409
+ log('sync done', summary, `(force=${force}, dry-run=${!!dryRun})`);
410
+ return {
411
+ code: 0,
412
+ summary,
413
+ details,
414
+ _report: {
415
+ targets: reportTargets,
416
+ filesChanged,
417
+ keptCount,
418
+ skippedCount,
419
+ startedAt,
420
+ finishedAt
421
+ }
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Generates tsconfig.build.json for all packages with tsup.config.ts
427
+ * This ensures tsup uses a tsconfig without paths to prevent bundling workspace packages
428
+ */
429
+ async function generateTsconfigBuild(root, { dryRun, verbose }) {
430
+ const tsupConfigs = await glob('**/tsup.config.ts', {
431
+ cwd: root,
432
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.kb/**'],
433
+ absolute: false,
434
+ });
435
+
436
+ if (tsupConfigs.length === 0) {
437
+ if (verbose) {log('No tsup.config.ts files found, skipping tsconfig.build.json generation');}
438
+ return { generated: 0, skipped: 0 };
439
+ }
440
+
441
+ let generated = 0;
442
+ let skipped = 0;
443
+
444
+ for (const tsupConfigPath of tsupConfigs) {
445
+ const pkgDir = dirname(resolve(root, tsupConfigPath));
446
+ const tsconfigPath = join(pkgDir, 'tsconfig.json');
447
+ const tsconfigBuildPath = join(pkgDir, 'tsconfig.build.json');
448
+
449
+ // Check if tsconfig.json exists
450
+ const tsconfigExists = await exists(tsconfigPath);
451
+ if (!tsconfigExists) {
452
+ if (verbose) {log(`Skipping ${tsupConfigPath}: no tsconfig.json found`);}
453
+ skipped++;
454
+ continue;
455
+ }
456
+
457
+ // Read existing tsconfig.json to determine base path
458
+ let tsconfigBase;
459
+ try {
460
+ const tsconfigContent = await readFile(tsconfigPath, 'utf8');
461
+ tsconfigBase = JSON.parse(tsconfigContent);
462
+ } catch (error) {
463
+ if (verbose) {warn(`Failed to parse ${tsconfigPath}:`, error.message);}
464
+ skipped++;
465
+ continue;
466
+ }
467
+
468
+ // Determine relative path to tsconfig.base.json
469
+ // Common patterns: "../../tsconfig.base.json", "../tsconfig.base.json", "./tsconfig.base.json"
470
+ let basePath = tsconfigBase.extends;
471
+ if (Array.isArray(basePath)) {
472
+ // Find the first non-devkit extends (usually tsconfig.base.json)
473
+ basePath = basePath.find(p => typeof p === 'string' && !p.includes('@kb-labs/devkit')) || basePath[0];
474
+ }
475
+ if (typeof basePath !== 'string') {
476
+ // Default: search up for tsconfig.base.json
477
+ let current = pkgDir;
478
+ let found = false;
479
+ for (let i = 0; i < 10; i++) {
480
+ const candidate = join(current, 'tsconfig.base.json');
481
+ if (await exists(candidate)) {
482
+ basePath = relative(pkgDir, candidate);
483
+ found = true;
484
+ break;
485
+ }
486
+ const parent = dirname(current);
487
+ if (parent === current) {break;}
488
+ current = parent;
489
+ }
490
+ if (!found) {
491
+ // Fallback to common pattern
492
+ const depth = relative(root, pkgDir).split(/[/\\]/).filter(Boolean).length;
493
+ basePath = '../'.repeat(depth) + 'tsconfig.base.json';
494
+ }
495
+ }
496
+
497
+ // Normalize basePath to relative path
498
+ if (basePath.includes('@kb-labs/devkit')) {
499
+ // If extends devkit, try to find tsconfig.base.json in repo root
500
+ let current = pkgDir;
501
+ for (let i = 0; i < 10; i++) {
502
+ const candidate = join(current, 'tsconfig.base.json');
503
+ if (await exists(candidate)) {
504
+ basePath = relative(pkgDir, candidate);
505
+ break;
506
+ }
507
+ const parent = dirname(current);
508
+ if (parent === current) {break;}
509
+ current = parent;
510
+ }
511
+ }
512
+
513
+ // Generate tsconfig.build.json content
514
+ const buildConfig = {
515
+ extends: basePath,
516
+ compilerOptions: {
517
+ outDir: 'dist',
518
+ baseUrl: '.',
519
+ paths: {},
520
+ },
521
+ include: ['src/**/*'],
522
+ exclude: ['dist', 'node_modules'],
523
+ };
524
+
525
+ // Check if file already exists and is identical
526
+ const existingExists = await exists(tsconfigBuildPath);
527
+ if (existingExists && !dryRun) {
528
+ try {
529
+ const existingContent = await readFile(tsconfigBuildPath, 'utf8');
530
+ const existing = JSON.parse(existingContent);
531
+ // Compare key fields
532
+ if (
533
+ existing.extends === buildConfig.extends &&
534
+ JSON.stringify(existing.compilerOptions?.paths) === JSON.stringify(buildConfig.compilerOptions.paths)
535
+ ) {
536
+ if (verbose) {log(`Keep ${tsconfigBuildPath}: already up to date`);}
537
+ skipped++;
538
+ continue;
539
+ }
540
+ } catch {
541
+ // If parse fails, regenerate
542
+ }
543
+ }
544
+
545
+ if (dryRun) {
546
+ log(`[dry-run] Would generate ${tsconfigBuildPath}`);
547
+ generated++;
548
+ continue;
549
+ }
550
+
551
+ // Write tsconfig.build.json
552
+ try {
553
+ await writeFile(tsconfigBuildPath, JSON.stringify(buildConfig, null, 2) + '\n');
554
+ log(`Generated ${tsconfigBuildPath}`);
555
+ generated++;
556
+ } catch (error) {
557
+ warn(`Failed to write ${tsconfigBuildPath}:`, error.message);
558
+ skipped++;
559
+ }
560
+ }
561
+
562
+ return { generated, skipped };
563
+ }
564
+
565
+ function printHelp(effectiveMap = BASE_MAP) {
566
+ const keys = Object.keys(effectiveMap).join(', ');
567
+ console.log(`kb-devkit-sync
568
+ Synchronize DevKit assets into a consumer repository.
569
+
570
+ Usage:
571
+ kb-devkit-sync [--check] [--force] [--dry-run] [--verbose] [--json] [--timeout=ms] [--scope=managed-only|strict|all] [--only=a,b] [targets...]
572
+
573
+ Targets:
574
+ ${keys}
575
+ (Override/add via kb-labs.config.json → { "sync": { "overrides": {..}, "targets": {..}, "only": ["ci"], "disabled": ["sbom"], "scope": "managed-only" } })
576
+
577
+ Flags:
578
+ --help, -h Show this help and exit
579
+ --version, -v Print devkit version and exit
580
+ --list Print available target keys and exit
581
+ --check Compare and exit with 0 (no drift) or 2 (drift found)
582
+ --force Overwrite destination files/directories
583
+ --dry-run Do not write files; print planned actions
584
+ --ci-only Limit scope to CI templates (alias for --only=ci)
585
+ --scope=... Set drift scope: 'managed-only' (ignore foreign files, default), 'strict' (flag foreign files), 'all'
586
+ --verbose Print per-target details
587
+ --json Emit machine-readable JSON result to stdout
588
+ --timeout=ms Optional timeout guard (default 30000)
589
+ --only=a,b Limit sync/check to specific targets
590
+ `);
591
+ }
592
+
593
+ async function printVersion() {
594
+ try {
595
+ const pkgJson = JSON.parse(await readFile(resolve(DEVKIT_ROOT, 'package.json'), 'utf8'));
596
+ console.log(pkgJson.version ?? 'unknown');
597
+ } catch {
598
+ console.log('unknown');
599
+ }
600
+ }
601
+
602
+ export async function run({ args = [] } = {}) {
603
+ const { help, version, check, force, verbose, json, dryRun, ciOnly, list, timeoutMs, onlyList, positional, scope: scopeFromCli } = parseArgs(args);
604
+ const devkitMeta = await readDevkitMeta();
605
+ log(`DevKit ${devkitMeta.version ?? 'unknown'} — scope=${(process.env.KB_DEVKIT_SYNC_SCOPE || '').toString() || 'managed-only'}`);
606
+ const root = process.cwd();
607
+ const cfg = await readProjectConfig(root);
608
+
609
+ if (cfg?.sync?.enabled === false) {
610
+ log('sync disabled by kb-labs.config.json');
611
+ return 0;
612
+ }
613
+
614
+ const disabledSet = new Set(cfg?.sync?.disabled ?? []);
615
+ const map = buildEffectiveMap(cfg);
616
+
617
+ if (list) {
618
+ console.log('[devkit-sync] available targets:', Object.keys(map).join(', '));
619
+ return 0;
620
+ }
621
+
622
+ const cfgOnly = Array.isArray(cfg?.sync?.only) ? cfg.sync.only.filter(s => typeof s === 'string' && s.length > 0) : [];
623
+ let select = onlyList.length ? onlyList.slice() : cfgOnly.slice();
624
+
625
+ if (ciOnly) {
626
+ const hasCi = select.includes('ci') || positional.includes('ci');
627
+ if (!hasCi) {
628
+ if (select.length === 0 && positional.length === 0) {select = ['ci'];}
629
+ else {select.push('ci');}
630
+ }
631
+ }
632
+
633
+ const seen = new Set();
634
+ select = select.filter(k => (k && !seen.has(k) && seen.add(k)));
635
+
636
+ const pos = positional.filter(Boolean);
637
+ const targets = resolveTargets(map, { onlyList: select, positional: pos, disabledSet });
638
+
639
+ log(`Targets: [${targets.join(', ')}]`);
640
+
641
+ let scope = scopeFromCli || cfg?.sync?.scope || 'managed-only';
642
+ if (!['managed-only', 'strict', 'all'].includes(scope)) {scope = 'managed-only';}
643
+
644
+ if (help) { printHelp(map); return 0; }
645
+ if (version) { await printVersion(); return 0; }
646
+
647
+ const controller = new AbortController();
648
+ const t = setTimeout(() => { controller.abort(); }, Math.max(0, timeoutMs));
649
+
650
+ try {
651
+ log('Starting devkit sync...');
652
+ if (check) {
653
+ const res = await runCheck(root, map, targets, { verbose, scope });
654
+ if (json) {console.log(JSON.stringify({ mode: 'check', ...res }, null, 2));}
655
+ return res.code;
656
+ } else {
657
+ const res = await runSync(root, map, targets, { force: force || !!cfg?.sync?.force, verbose, dryRun });
658
+
659
+ // Generate tsconfig.build.json for all packages with tsup.config.ts
660
+ // This is done after sync to ensure proper bundling configuration
661
+ if (!dryRun || verbose) {
662
+ const buildResult = await generateTsconfigBuild(root, { dryRun, verbose });
663
+ if (buildResult.generated > 0 || buildResult.skipped > 0) {
664
+ log(`tsconfig.build.json: ${buildResult.generated} generated, ${buildResult.skipped} skipped`);
665
+ }
666
+ }
667
+
668
+ const report = {
669
+ schemaVersion: '2-min',
670
+ devkit: { version: devkitMeta.version, commit: devkitMeta.commit },
671
+ repo: {},
672
+ run: {
673
+ id: cryptoRandomId(),
674
+ startedAt: new Date(res?._report?.startedAt ?? Date.now()).toISOString(),
675
+ finishedAt: new Date(res?._report?.finishedAt ?? Date.now()).toISOString()
676
+ },
677
+ summary: {
678
+ filesChanged: res?._report?.filesChanged ?? 0,
679
+ kept: res.summary.kept,
680
+ skipped: res.summary.skipped,
681
+ conflicts: 0,
682
+ mode: 'sync'
683
+ },
684
+ targets: (res?._report?.targets ?? [])
685
+ };
686
+ await writeProvenance(root, { items: targets, scope, report, fileName: 'DEVKIT_SYNC.json' });
687
+ if (json) {console.log(JSON.stringify({ mode: 'sync', ...res }, null, 2));}
688
+ return res.code;
689
+ }
690
+ } finally {
691
+ clearTimeout(t);
692
+ }
693
+ }