@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.
- package/.cursorrules +32 -0
- package/.github/CODEOWNERS +2 -0
- package/.github/actions/setup-node-pnpm/action.yml +47 -0
- package/.github/workflow-templates/ci.yml +13 -0
- package/.github/workflow-templates/drift-check.yml +10 -0
- package/.github/workflow-templates/profiles-validate.yml +16 -0
- package/.github/workflow-templates/release.yml +8 -0
- package/.github/workflows/ci-reusable.yml +131 -0
- package/.github/workflows/drift-check-reusable.yml +23 -0
- package/.github/workflows/fixtures.yml +74 -0
- package/.github/workflows/profiles-validate-reusable.yml +67 -0
- package/.github/workflows/release-reusable.yml +50 -0
- package/.vscode/settings.json +23 -0
- package/AGENTS.md +130 -0
- package/LICENSE +21 -0
- package/README.md +1542 -0
- package/agents/devkit-maintainer/context.globs +15 -0
- package/agents/devkit-maintainer/permissions.yml +17 -0
- package/agents/devkit-maintainer/prompt.md +28 -0
- package/agents/devkit-maintainer/runbook.md +31 -0
- package/agents/docs-crafter/prompt.md +24 -0
- package/agents/docs-crafter/runbook.md +18 -0
- package/agents/release-manager/context.globs +7 -0
- package/agents/release-manager/prompt.md +27 -0
- package/agents/release-manager/runbook.md +17 -0
- package/agents/test-generator/context.globs +7 -0
- package/agents/test-generator/prompt.md +27 -0
- package/agents/test-generator/runbook.md +18 -0
- package/bin/devkit-architecture.mjs +1225 -0
- package/bin/devkit-build-order.mjs +500 -0
- package/bin/devkit-check-build-readiness.mjs +222 -0
- package/bin/devkit-check-commands.mjs +394 -0
- package/bin/devkit-check-configs.mjs +461 -0
- package/bin/devkit-check-deprecated.mjs +532 -0
- package/bin/devkit-check-duplicates.mjs +431 -0
- package/bin/devkit-check-exports.mjs +561 -0
- package/bin/devkit-check-imports.mjs +712 -0
- package/bin/devkit-check-paths.mjs +670 -0
- package/bin/devkit-check-scripts.mjs +335 -0
- package/bin/devkit-check-structure.mjs +495 -0
- package/bin/devkit-check-types.mjs +450 -0
- package/bin/devkit-ci.mjs +261 -0
- package/bin/devkit-core-gate.mjs +225 -0
- package/bin/devkit-fix-deps.mjs +1169 -0
- package/bin/devkit-freshness.mjs +199 -0
- package/bin/devkit-health.mjs +489 -0
- package/bin/devkit-migrate-configs.mjs +370 -0
- package/bin/devkit-paths.mjs +192 -0
- package/bin/devkit-stats.mjs +453 -0
- package/bin/devkit-sync.mjs +12 -0
- package/bin/devkit-tsup-external.mjs +148 -0
- package/bin/devkit-types-audit.mjs +615 -0
- package/bin/devkit-types-order.mjs +637 -0
- package/bin/devkit-validate-naming.mjs +203 -0
- package/bin/devkit-visualize.mjs +452 -0
- package/bin/kb-devkit-qa-history.mjs +453 -0
- package/bin/kb-devkit-qa.mjs +915 -0
- package/eslint/node.js +134 -0
- package/eslint/react.js +260 -0
- package/package.json +186 -0
- package/prettier/index.json +10 -0
- package/sync/index.mjs +693 -0
- package/templates/configs/README.md +306 -0
- package/templates/configs/eslint.config.js +27 -0
- package/templates/configs/package.json.bin +25 -0
- package/templates/configs/package.json.lib +30 -0
- package/templates/configs/tsconfig.build.json +15 -0
- package/templates/configs/tsconfig.json +9 -0
- package/templates/configs/tsup.config.bin.ts +34 -0
- package/templates/configs/tsup.config.cli.ts +41 -0
- package/templates/configs/tsup.config.dual.ts +46 -0
- package/templates/configs/tsup.config.ts +36 -0
- package/templates/package-json/README.md +290 -0
- package/templates/package-json/package.json.bin.template +40 -0
- package/templates/package-json/package.json.template +45 -0
- package/tsconfig/base.json +21 -0
- package/tsconfig/cli.json +12 -0
- package/tsconfig/dist/__tests__/cache.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/cache.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/cache.spec.js +85 -0
- package/tsconfig/dist/__tests__/cache.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.js +153 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.js +99 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/kb-error.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/kb-error.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/kb-error.spec.js +190 -0
- package/tsconfig/dist/__tests__/kb-error.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.js +142 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.js +100 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/product-config.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/product-config.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/product-config.spec.js +298 -0
- package/tsconfig/dist/__tests__/product-config.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/runtime.spec.d.ts +2 -0
- package/tsconfig/dist/__tests__/runtime.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/runtime.spec.js +127 -0
- package/tsconfig/dist/__tests__/runtime.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.js +251 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/validate-config.spec.d.ts +2 -0
- package/tsconfig/dist/__tests__/validate-config.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/validate-config.spec.js +14 -0
- package/tsconfig/dist/__tests__/validate-config.spec.js.map +1 -0
- package/tsconfig/dist/api/init-workspace.d.ts +10 -0
- package/tsconfig/dist/api/init-workspace.d.ts.map +1 -0
- package/tsconfig/dist/api/init-workspace.js +191 -0
- package/tsconfig/dist/api/init-workspace.js.map +1 -0
- package/tsconfig/dist/api/product-config.d.ts +21 -0
- package/tsconfig/dist/api/product-config.d.ts.map +1 -0
- package/tsconfig/dist/api/product-config.js +192 -0
- package/tsconfig/dist/api/product-config.js.map +1 -0
- package/tsconfig/dist/api/read-config.d.ts +22 -0
- package/tsconfig/dist/api/read-config.d.ts.map +1 -0
- package/tsconfig/dist/api/read-config.js +105 -0
- package/tsconfig/dist/api/read-config.js.map +1 -0
- package/tsconfig/dist/api/upsert-lockfile.d.ts +10 -0
- package/tsconfig/dist/api/upsert-lockfile.d.ts.map +1 -0
- package/tsconfig/dist/api/upsert-lockfile.js +63 -0
- package/tsconfig/dist/api/upsert-lockfile.js.map +1 -0
- package/tsconfig/dist/cache/fs-cache.d.ts +38 -0
- package/tsconfig/dist/cache/fs-cache.d.ts.map +1 -0
- package/tsconfig/dist/cache/fs-cache.js +142 -0
- package/tsconfig/dist/cache/fs-cache.js.map +1 -0
- package/tsconfig/dist/errors/kb-error.d.ts +32 -0
- package/tsconfig/dist/errors/kb-error.d.ts.map +1 -0
- package/tsconfig/dist/errors/kb-error.js +54 -0
- package/tsconfig/dist/errors/kb-error.js.map +1 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.d.ts +2 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.d.ts.map +1 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.js +22 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.js.map +1 -0
- package/tsconfig/dist/fs/fs.d.ts +6 -0
- package/tsconfig/dist/fs/fs.d.ts.map +1 -0
- package/tsconfig/dist/fs/fs.js +12 -0
- package/tsconfig/dist/fs/fs.js.map +1 -0
- package/tsconfig/dist/fs/index.d.ts +2 -0
- package/tsconfig/dist/fs/index.d.ts.map +1 -0
- package/tsconfig/dist/fs/index.js +2 -0
- package/tsconfig/dist/fs/index.js.map +1 -0
- package/tsconfig/dist/hash/config-hash.d.ts +17 -0
- package/tsconfig/dist/hash/config-hash.d.ts.map +1 -0
- package/tsconfig/dist/hash/config-hash.js +55 -0
- package/tsconfig/dist/hash/config-hash.js.map +1 -0
- package/tsconfig/dist/index.d.ts +5 -0
- package/tsconfig/dist/index.d.ts.map +1 -0
- package/tsconfig/dist/index.js +5 -0
- package/tsconfig/dist/index.js.map +1 -0
- package/tsconfig/dist/lockfile/lockfile.d.ts +54 -0
- package/tsconfig/dist/lockfile/lockfile.d.ts.map +1 -0
- package/tsconfig/dist/lockfile/lockfile.js +141 -0
- package/tsconfig/dist/lockfile/lockfile.js.map +1 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.d.ts +2 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.d.ts.map +1 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.js +65 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.js.map +1 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts +2 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts.map +1 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.js +34 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.js.map +1 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts +2 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts.map +1 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js +90 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js.map +1 -0
- package/tsconfig/dist/logging/index.d.ts +6 -0
- package/tsconfig/dist/logging/index.d.ts.map +1 -0
- package/tsconfig/dist/logging/index.js +6 -0
- package/tsconfig/dist/logging/index.js.map +1 -0
- package/tsconfig/dist/logging/logger.d.ts +9 -0
- package/tsconfig/dist/logging/logger.d.ts.map +1 -0
- package/tsconfig/dist/logging/logger.js +101 -0
- package/tsconfig/dist/logging/logger.js.map +1 -0
- package/tsconfig/dist/logging/redaction.d.ts +7 -0
- package/tsconfig/dist/logging/redaction.d.ts.map +1 -0
- package/tsconfig/dist/logging/redaction.js +23 -0
- package/tsconfig/dist/logging/redaction.js.map +1 -0
- package/tsconfig/dist/logging/sinks/json.d.ts +4 -0
- package/tsconfig/dist/logging/sinks/json.d.ts.map +1 -0
- package/tsconfig/dist/logging/sinks/json.js +23 -0
- package/tsconfig/dist/logging/sinks/json.js.map +1 -0
- package/tsconfig/dist/logging/sinks/stdout.d.ts +3 -0
- package/tsconfig/dist/logging/sinks/stdout.d.ts.map +1 -0
- package/tsconfig/dist/logging/sinks/stdout.js +24 -0
- package/tsconfig/dist/logging/sinks/stdout.js.map +1 -0
- package/tsconfig/dist/logging/types/index.d.ts +2 -0
- package/tsconfig/dist/logging/types/index.d.ts.map +1 -0
- package/tsconfig/dist/logging/types/index.js +2 -0
- package/tsconfig/dist/logging/types/index.js.map +1 -0
- package/tsconfig/dist/logging/types/types.d.ts +37 -0
- package/tsconfig/dist/logging/types/types.d.ts.map +1 -0
- package/tsconfig/dist/logging/types/types.js +2 -0
- package/tsconfig/dist/logging/types/types.js.map +1 -0
- package/tsconfig/dist/merge/layered-merge.d.ts +16 -0
- package/tsconfig/dist/merge/layered-merge.d.ts.map +1 -0
- package/tsconfig/dist/merge/layered-merge.js +97 -0
- package/tsconfig/dist/merge/layered-merge.js.map +1 -0
- package/tsconfig/dist/preset/resolve-preset.d.ts +29 -0
- package/tsconfig/dist/preset/resolve-preset.d.ts.map +1 -0
- package/tsconfig/dist/preset/resolve-preset.js +104 -0
- package/tsconfig/dist/preset/resolve-preset.js.map +1 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.d.ts +2 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.d.ts.map +1 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.js +25 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.js.map +1 -0
- package/tsconfig/dist/repo/index.d.ts +2 -0
- package/tsconfig/dist/repo/index.d.ts.map +1 -0
- package/tsconfig/dist/repo/index.js +2 -0
- package/tsconfig/dist/repo/index.js.map +1 -0
- package/tsconfig/dist/repo/repo.d.ts +6 -0
- package/tsconfig/dist/repo/repo.d.ts.map +1 -0
- package/tsconfig/dist/repo/repo.js +25 -0
- package/tsconfig/dist/repo/repo.js.map +1 -0
- package/tsconfig/dist/runtime/index.d.ts +2 -0
- package/tsconfig/dist/runtime/index.d.ts.map +1 -0
- package/tsconfig/dist/runtime/index.js +2 -0
- package/tsconfig/dist/runtime/index.js.map +1 -0
- package/tsconfig/dist/runtime/runtime.d.ts +46 -0
- package/tsconfig/dist/runtime/runtime.d.ts.map +1 -0
- package/tsconfig/dist/runtime/runtime.js +126 -0
- package/tsconfig/dist/runtime/runtime.js.map +1 -0
- package/tsconfig/dist/tsconfig.tools.tsbuildinfo +1 -0
- package/tsconfig/dist/tsconfig.tsbuildinfo +1 -0
- package/tsconfig/dist/types/index.d.ts +2 -0
- package/tsconfig/dist/types/index.d.ts.map +1 -0
- package/tsconfig/dist/types/index.js +2 -0
- package/tsconfig/dist/types/index.js.map +1 -0
- package/tsconfig/dist/types/init.d.ts +34 -0
- package/tsconfig/dist/types/init.d.ts.map +1 -0
- package/tsconfig/dist/types/init.js +6 -0
- package/tsconfig/dist/types/init.js.map +1 -0
- package/tsconfig/dist/types/preset.d.ts +27 -0
- package/tsconfig/dist/types/preset.d.ts.map +1 -0
- package/tsconfig/dist/types/preset.js +6 -0
- package/tsconfig/dist/types/preset.js.map +1 -0
- package/tsconfig/dist/types/types.d.ts +6 -0
- package/tsconfig/dist/types/types.d.ts.map +1 -0
- package/tsconfig/dist/types/types.js +2 -0
- package/tsconfig/dist/types/types.js.map +1 -0
- package/tsconfig/dist/utils/__tests__/env.spec.d.ts +2 -0
- package/tsconfig/dist/utils/__tests__/env.spec.d.ts.map +1 -0
- package/tsconfig/dist/utils/__tests__/env.spec.js +33 -0
- package/tsconfig/dist/utils/__tests__/env.spec.js.map +1 -0
- package/tsconfig/dist/utils/env.d.ts +7 -0
- package/tsconfig/dist/utils/env.d.ts.map +1 -0
- package/tsconfig/dist/utils/env.js +25 -0
- package/tsconfig/dist/utils/env.js.map +1 -0
- package/tsconfig/dist/utils/fs-atomic.d.ts +15 -0
- package/tsconfig/dist/utils/fs-atomic.d.ts.map +1 -0
- package/tsconfig/dist/utils/fs-atomic.js +45 -0
- package/tsconfig/dist/utils/fs-atomic.js.map +1 -0
- package/tsconfig/dist/utils/index.d.ts +3 -0
- package/tsconfig/dist/utils/index.d.ts.map +1 -0
- package/tsconfig/dist/utils/index.js +3 -0
- package/tsconfig/dist/utils/index.js.map +1 -0
- package/tsconfig/dist/utils/paths.d.ts +21 -0
- package/tsconfig/dist/utils/paths.d.ts.map +1 -0
- package/tsconfig/dist/utils/paths.js +32 -0
- package/tsconfig/dist/utils/paths.js.map +1 -0
- package/tsconfig/dist/utils/product-normalize.d.ts +27 -0
- package/tsconfig/dist/utils/product-normalize.d.ts.map +1 -0
- package/tsconfig/dist/utils/product-normalize.js +45 -0
- package/tsconfig/dist/utils/product-normalize.js.map +1 -0
- package/tsconfig/dist/validation/validate-config.d.ts +7 -0
- package/tsconfig/dist/validation/validate-config.d.ts.map +1 -0
- package/tsconfig/dist/validation/validate-config.js +22 -0
- package/tsconfig/dist/validation/validate-config.js.map +1 -0
- package/tsconfig/lib.json +13 -0
- package/tsconfig/node.json +12 -0
- package/tsconfig/react-app.json +8 -0
- package/tsconfig/react-lib.json +8 -0
- package/tsconfig/test.json +15 -0
- package/tsup/bin.js +172 -0
- package/tsup/dual.js +155 -0
- package/tsup/external-sync.mjs +41 -0
- package/tsup/external.mjs +109 -0
- package/tsup/node.js +65 -0
- package/tsup/react-lib.js +18 -0
- package/tsup/sdk.js +118 -0
- package/vite/react-app.js +50 -0
- package/vitest/node.js +37 -0
- package/vitest/react.js +39 -0
- 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
|
+
}
|