@kb-labs/core-config 1.2.0 → 1.4.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/dist/index.d.ts +1016 -25
- package/dist/index.js +1533 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/__tests__/cache.spec.d.ts +0 -6
- package/dist/__tests__/cache.spec.d.ts.map +0 -1
- package/dist/__tests__/cache.spec.js +0 -87
- package/dist/__tests__/cache.spec.js.map +0 -1
- package/dist/__tests__/fs-atomic.spec.d.ts +0 -6
- package/dist/__tests__/fs-atomic.spec.d.ts.map +0 -1
- package/dist/__tests__/fs-atomic.spec.js +0 -153
- package/dist/__tests__/fs-atomic.spec.js.map +0 -1
- package/dist/__tests__/get-product-config-edge-cases.spec.d.ts +0 -6
- package/dist/__tests__/get-product-config-edge-cases.spec.d.ts.map +0 -1
- package/dist/__tests__/get-product-config-edge-cases.spec.js +0 -334
- package/dist/__tests__/get-product-config-edge-cases.spec.js.map +0 -1
- package/dist/__tests__/init-workspace.spec.d.ts +0 -6
- package/dist/__tests__/init-workspace.spec.d.ts.map +0 -1
- package/dist/__tests__/init-workspace.spec.js +0 -101
- package/dist/__tests__/init-workspace.spec.js.map +0 -1
- package/dist/__tests__/kb-error.spec.d.ts +0 -6
- package/dist/__tests__/kb-error.spec.d.ts.map +0 -1
- package/dist/__tests__/kb-error.spec.js +0 -193
- package/dist/__tests__/kb-error.spec.js.map +0 -1
- package/dist/__tests__/preset-lockfile.spec.d.ts +0 -6
- package/dist/__tests__/preset-lockfile.spec.d.ts.map +0 -1
- package/dist/__tests__/preset-lockfile.spec.js +0 -142
- package/dist/__tests__/preset-lockfile.spec.js.map +0 -1
- package/dist/__tests__/product-config-profiles.spec.d.ts +0 -6
- package/dist/__tests__/product-config-profiles.spec.d.ts.map +0 -1
- package/dist/__tests__/product-config-profiles.spec.js +0 -82
- package/dist/__tests__/product-config-profiles.spec.js.map +0 -1
- package/dist/__tests__/product-config.spec.d.ts +0 -6
- package/dist/__tests__/product-config.spec.d.ts.map +0 -1
- package/dist/__tests__/product-config.spec.js +0 -304
- package/dist/__tests__/product-config.spec.js.map +0 -1
- package/dist/__tests__/profiles-demo.d.ts +0 -11
- package/dist/__tests__/profiles-demo.d.ts.map +0 -1
- package/dist/__tests__/profiles-demo.js +0 -173
- package/dist/__tests__/profiles-demo.js.map +0 -1
- package/dist/__tests__/profiles-resolver.spec.d.ts +0 -2
- package/dist/__tests__/profiles-resolver.spec.d.ts.map +0 -1
- package/dist/__tests__/profiles-resolver.spec.js +0 -114
- package/dist/__tests__/profiles-resolver.spec.js.map +0 -1
- package/dist/__tests__/profiles-section.spec.d.ts +0 -2
- package/dist/__tests__/profiles-section.spec.d.ts.map +0 -1
- package/dist/__tests__/profiles-section.spec.js +0 -50
- package/dist/__tests__/profiles-section.spec.js.map +0 -1
- package/dist/__tests__/runtime.spec.d.ts +0 -2
- package/dist/__tests__/runtime.spec.d.ts.map +0 -1
- package/dist/__tests__/runtime.spec.js +0 -170
- package/dist/__tests__/runtime.spec.js.map +0 -1
- package/dist/__tests__/upsert-lockfile.spec.d.ts +0 -1
- package/dist/__tests__/upsert-lockfile.spec.d.ts.map +0 -1
- package/dist/__tests__/upsert-lockfile.spec.js +0 -2
- package/dist/__tests__/upsert-lockfile.spec.js.map +0 -1
- package/dist/__tests__/validate-config.spec.d.ts +0 -2
- package/dist/__tests__/validate-config.spec.d.ts.map +0 -1
- package/dist/__tests__/validate-config.spec.js +0 -14
- package/dist/__tests__/validate-config.spec.js.map +0 -1
- package/dist/api/init-workspace.d.ts +0 -10
- package/dist/api/init-workspace.d.ts.map +0 -1
- package/dist/api/init-workspace.js +0 -186
- package/dist/api/init-workspace.js.map +0 -1
- package/dist/api/product-config.d.ts +0 -20
- package/dist/api/product-config.d.ts.map +0 -1
- package/dist/api/product-config.js +0 -236
- package/dist/api/product-config.js.map +0 -1
- package/dist/api/read-config.d.ts +0 -22
- package/dist/api/read-config.d.ts.map +0 -1
- package/dist/api/read-config.js +0 -112
- package/dist/api/read-config.js.map +0 -1
- package/dist/api/read-kb-config.d.ts +0 -12
- package/dist/api/read-kb-config.d.ts.map +0 -1
- package/dist/api/read-kb-config.js +0 -47
- package/dist/api/read-kb-config.js.map +0 -1
- package/dist/api/upsert-lockfile.d.ts +0 -10
- package/dist/api/upsert-lockfile.d.ts.map +0 -1
- package/dist/api/upsert-lockfile.js +0 -63
- package/dist/api/upsert-lockfile.js.map +0 -1
- package/dist/cache/fs-cache.d.ts +0 -38
- package/dist/cache/fs-cache.d.ts.map +0 -1
- package/dist/cache/fs-cache.js +0 -142
- package/dist/cache/fs-cache.js.map +0 -1
- package/dist/errors/kb-error.d.ts +0 -35
- package/dist/errors/kb-error.d.ts.map +0 -1
- package/dist/errors/kb-error.js +0 -57
- package/dist/errors/kb-error.js.map +0 -1
- package/dist/hash/config-hash.d.ts +0 -17
- package/dist/hash/config-hash.d.ts.map +0 -1
- package/dist/hash/config-hash.js +0 -55
- package/dist/hash/config-hash.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lockfile/lockfile.d.ts +0 -53
- package/dist/lockfile/lockfile.d.ts.map +0 -1
- package/dist/lockfile/lockfile.js +0 -142
- package/dist/lockfile/lockfile.js.map +0 -1
- package/dist/merge/layered-merge.d.ts +0 -16
- package/dist/merge/layered-merge.d.ts.map +0 -1
- package/dist/merge/layered-merge.js +0 -87
- package/dist/merge/layered-merge.js.map +0 -1
- package/dist/preset/resolve-preset.d.ts +0 -28
- package/dist/preset/resolve-preset.d.ts.map +0 -1
- package/dist/preset/resolve-preset.js +0 -104
- package/dist/preset/resolve-preset.js.map +0 -1
- package/dist/profiles/loader.d.ts +0 -12
- package/dist/profiles/loader.d.ts.map +0 -1
- package/dist/profiles/loader.js +0 -34
- package/dist/profiles/loader.js.map +0 -1
- package/dist/profiles/resolver.d.ts +0 -11
- package/dist/profiles/resolver.d.ts.map +0 -1
- package/dist/profiles/resolver.js +0 -252
- package/dist/profiles/resolver.js.map +0 -1
- package/dist/profiles/scope-selector.d.ts +0 -19
- package/dist/profiles/scope-selector.d.ts.map +0 -1
- package/dist/profiles/scope-selector.js +0 -64
- package/dist/profiles/scope-selector.js.map +0 -1
- package/dist/profiles/types.d.ts +0 -462
- package/dist/profiles/types.d.ts.map +0 -1
- package/dist/profiles/types.js +0 -68
- package/dist/profiles/types.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -2
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -2
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/runtime.d.ts +0 -46
- package/dist/runtime/runtime.d.ts.map +0 -1
- package/dist/runtime/runtime.js +0 -126
- package/dist/runtime/runtime.js.map +0 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -2
- package/dist/types/index.js.map +0 -1
- package/dist/types/init.d.ts +0 -33
- package/dist/types/init.d.ts.map +0 -1
- package/dist/types/init.js +0 -6
- package/dist/types/init.js.map +0 -1
- package/dist/types/preset.d.ts +0 -27
- package/dist/types/preset.d.ts.map +0 -1
- package/dist/types/preset.js +0 -6
- package/dist/types/preset.js.map +0 -1
- package/dist/types/types.d.ts +0 -70
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/types.js +0 -2
- package/dist/types/types.js.map +0 -1
- package/dist/utils/__tests__/env.spec.d.ts +0 -2
- package/dist/utils/__tests__/env.spec.d.ts.map +0 -1
- package/dist/utils/__tests__/env.spec.js +0 -33
- package/dist/utils/__tests__/env.spec.js.map +0 -1
- package/dist/utils/env.d.ts +0 -7
- package/dist/utils/env.d.ts.map +0 -1
- package/dist/utils/env.js +0 -25
- package/dist/utils/env.js.map +0 -1
- package/dist/utils/fs-atomic.d.ts +0 -15
- package/dist/utils/fs-atomic.d.ts.map +0 -1
- package/dist/utils/fs-atomic.js +0 -46
- package/dist/utils/fs-atomic.js.map +0 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -3
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/paths.d.ts +0 -21
- package/dist/utils/paths.d.ts.map +0 -1
- package/dist/utils/paths.js +0 -32
- package/dist/utils/paths.js.map +0 -1
- package/dist/utils/product-normalize.d.ts +0 -25
- package/dist/utils/product-normalize.d.ts.map +0 -1
- package/dist/utils/product-normalize.js +0 -48
- package/dist/utils/product-normalize.js.map +0 -1
- package/dist/validation/validate-config.d.ts +0 -10
- package/dist/validation/validate-config.d.ts.map +0 -1
- package/dist/validation/validate-config.js +0 -21
- package/dist/validation/validate-config.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,30 +1,1534 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { KbError, ERROR_HINTS, ensureWithinWorkspace, writeFileAtomic } from './chunk-A2WE2JZW.js';
|
|
2
|
+
export { ERROR_HINTS, KbError, ensureWithinWorkspace, getExitCode, writeFileAtomic } from './chunk-A2WE2JZW.js';
|
|
3
|
+
import { promises } from 'fs';
|
|
4
|
+
import path3, { extname } from 'path';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import YAML2 from 'yaml';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import picomatch from 'picomatch';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import Ajv from 'ajv';
|
|
11
|
+
import addFormats from 'ajv-formats';
|
|
12
|
+
|
|
13
|
+
async function findNearestConfig(opts) {
|
|
14
|
+
const start = path3.resolve(opts.startDir ?? process.cwd());
|
|
15
|
+
const stop = opts.stopDir ? path3.resolve(opts.stopDir) : null;
|
|
16
|
+
const tried = [];
|
|
17
|
+
let dir = start;
|
|
18
|
+
while (true) {
|
|
19
|
+
for (const name of opts.filenames) {
|
|
20
|
+
const candidate = path3.join(dir, name);
|
|
21
|
+
tried.push(candidate);
|
|
22
|
+
try {
|
|
23
|
+
await promises.access(candidate);
|
|
24
|
+
return { path: candidate, tried };
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const parent = path3.dirname(dir);
|
|
29
|
+
if (parent === dir) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
if (stop && (dir === stop || parent === stop)) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
dir = parent;
|
|
36
|
+
}
|
|
37
|
+
return { path: null, tried };
|
|
38
|
+
}
|
|
39
|
+
async function readJsonWithDiagnostics(p) {
|
|
40
|
+
const diagnostics = [];
|
|
41
|
+
try {
|
|
42
|
+
const raw = await promises.readFile(p, "utf8");
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(raw);
|
|
45
|
+
return { ok: true, data, diagnostics };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
diagnostics.push({ level: "error", code: "JSON_PARSE_FAILED", message: `Failed to parse JSON: ${p}`, detail: String(e) });
|
|
48
|
+
return { ok: false, diagnostics };
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
diagnostics.push({ level: "error", code: "FILE_READ_FAILED", message: `Failed to read file: ${p}`, detail: String(e) });
|
|
52
|
+
return { ok: false, diagnostics };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function pickDefined(obj) {
|
|
56
|
+
if (!obj) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
61
|
+
if (v !== void 0) {
|
|
62
|
+
out[k] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function mergeDefined(base, over) {
|
|
68
|
+
if (!over) {
|
|
69
|
+
return base;
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(base) && Array.isArray(over)) {
|
|
72
|
+
return [...base, ...over.filter((v) => v !== void 0)];
|
|
73
|
+
}
|
|
74
|
+
if (isPlainObject(base) && isPlainObject(over)) {
|
|
75
|
+
const out = { ...base };
|
|
76
|
+
for (const [k, v] of Object.entries(over)) {
|
|
77
|
+
if (v === void 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (isPlainObject(base[k]) && isPlainObject(v)) {
|
|
81
|
+
out[k] = mergeDefined(base[k], v);
|
|
82
|
+
} else if (Array.isArray(base[k]) && Array.isArray(v)) {
|
|
83
|
+
out[k] = mergeDefined(base[k], v);
|
|
84
|
+
} else {
|
|
85
|
+
out[k] = v;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
return over ?? base;
|
|
91
|
+
}
|
|
92
|
+
function isPlainObject(v) {
|
|
93
|
+
return typeof v === "object" && v !== null && Object.getPrototypeOf(v) === Object.prototype;
|
|
94
|
+
}
|
|
95
|
+
var SYSTEM_DEFAULTS = {
|
|
96
|
+
profiles: { rootDir: ".kb/profiles", defaultName: "default", strict: true }
|
|
97
|
+
};
|
|
98
|
+
function resolveConfig(args) {
|
|
99
|
+
const diagnostics = [];
|
|
100
|
+
const envPart = args.envMapper?.(process.env) ?? {};
|
|
101
|
+
const merged = mergeDefined(
|
|
102
|
+
mergeDefined(
|
|
103
|
+
mergeDefined(mergeDefined(structuredClone(SYSTEM_DEFAULTS), structuredClone(args.defaults)), args.fileConfig),
|
|
104
|
+
envPart
|
|
105
|
+
),
|
|
106
|
+
args.cliOverrides
|
|
107
|
+
);
|
|
108
|
+
if (args.validate) {
|
|
109
|
+
const res = args.validate(merged);
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
diagnostics.push({ level: "error", code: "CONFIG_VALIDATION_FAILED", message: "Configuration did not pass validation." });
|
|
112
|
+
if (res.diagnostics) {
|
|
113
|
+
diagnostics.push(...res.diagnostics);
|
|
114
|
+
}
|
|
115
|
+
} else if (res.diagnostics?.length) {
|
|
116
|
+
diagnostics.push(...res.diagnostics);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { value: merged, diagnostics };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/utils/env.ts
|
|
123
|
+
function toBool(v) {
|
|
124
|
+
if (v === void 0) {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
const s = v.trim().toLowerCase();
|
|
128
|
+
if (s === "1" || s === "true" || s === "yes" || s === "on") {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (s === "0" || s === "false" || s === "no" || s === "off") {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
function toInt(v) {
|
|
137
|
+
if (v === void 0) {
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
const n = Number(v);
|
|
141
|
+
return Number.isFinite(n) ? n : void 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/utils/product-normalize.ts
|
|
145
|
+
var FS_MAP = /* @__PURE__ */ new Map([
|
|
146
|
+
["aiReview", "ai-review"],
|
|
147
|
+
["aiDocs", "ai-docs"],
|
|
148
|
+
["devlink", "devlink"],
|
|
149
|
+
["release", "release"],
|
|
150
|
+
["devkit", "devkit"],
|
|
151
|
+
["mind", "mind"],
|
|
152
|
+
["workflow", "workflow"],
|
|
153
|
+
["analytics", "analytics"]
|
|
154
|
+
]);
|
|
155
|
+
function toFsProduct(id) {
|
|
156
|
+
return FS_MAP.get(id) ?? id;
|
|
157
|
+
}
|
|
158
|
+
function toConfigProduct(fsKey) {
|
|
159
|
+
for (const [k, v] of FS_MAP) {
|
|
160
|
+
if (v === fsKey) {
|
|
161
|
+
return k;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return fsKey;
|
|
165
|
+
}
|
|
166
|
+
function isValidProductId(value) {
|
|
167
|
+
return FS_MAP.has(value);
|
|
168
|
+
}
|
|
169
|
+
function getAllProductIds() {
|
|
170
|
+
return Array.from(FS_MAP.keys());
|
|
171
|
+
}
|
|
172
|
+
function getProfilesRootDir(cwd) {
|
|
173
|
+
return path3.join(cwd, ".kb", "profiles");
|
|
174
|
+
}
|
|
175
|
+
function getProductConfigDir(cwd, fsProduct) {
|
|
176
|
+
return path3.join(cwd, ".kb", fsProduct);
|
|
177
|
+
}
|
|
178
|
+
function getProductConfigPath(cwd, fsProduct) {
|
|
179
|
+
return path3.join(getProductConfigDir(cwd, fsProduct), `${fsProduct}.config.json`);
|
|
180
|
+
}
|
|
181
|
+
function getKbRootDir(cwd) {
|
|
182
|
+
return path3.join(cwd, ".kb");
|
|
183
|
+
}
|
|
184
|
+
function getLockfilePath(cwd) {
|
|
185
|
+
return path3.join(getKbRootDir(cwd), "lock.json");
|
|
186
|
+
}
|
|
187
|
+
var LRUCache = class {
|
|
188
|
+
cache = /* @__PURE__ */ new Map();
|
|
189
|
+
maxSize;
|
|
190
|
+
constructor(maxSize = 100) {
|
|
191
|
+
this.maxSize = maxSize;
|
|
192
|
+
}
|
|
193
|
+
get(key) {
|
|
194
|
+
const value = this.cache.get(key);
|
|
195
|
+
if (value !== void 0) {
|
|
196
|
+
this.cache.delete(key);
|
|
197
|
+
this.cache.set(key, value);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
set(key, value) {
|
|
202
|
+
if (this.cache.has(key)) {
|
|
203
|
+
this.cache.delete(key);
|
|
204
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
205
|
+
const firstKey = this.cache.keys().next().value;
|
|
206
|
+
if (firstKey !== void 0) {
|
|
207
|
+
this.cache.delete(firstKey);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.cache.set(key, value);
|
|
211
|
+
}
|
|
212
|
+
delete(key) {
|
|
213
|
+
return this.cache.delete(key);
|
|
214
|
+
}
|
|
215
|
+
clear() {
|
|
216
|
+
this.cache.clear();
|
|
217
|
+
}
|
|
218
|
+
size() {
|
|
219
|
+
return this.cache.size;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var FSCache = class {
|
|
223
|
+
cache = new LRUCache(100);
|
|
224
|
+
keyCache = new LRUCache(100);
|
|
225
|
+
/**
|
|
226
|
+
* Generate cache key from file path, mtime, and size
|
|
227
|
+
*/
|
|
228
|
+
async getCacheKey(absPath) {
|
|
229
|
+
const keyStr = `key:${absPath}`;
|
|
230
|
+
let key = this.keyCache.get(keyStr);
|
|
231
|
+
if (!key) {
|
|
232
|
+
try {
|
|
233
|
+
const stats = await promises.stat(absPath);
|
|
234
|
+
key = {
|
|
235
|
+
absPath,
|
|
236
|
+
mtime: stats.mtime.getTime(),
|
|
237
|
+
size: stats.size
|
|
238
|
+
};
|
|
239
|
+
this.keyCache.set(keyStr, key);
|
|
240
|
+
} catch {
|
|
241
|
+
return { absPath, mtime: 0, size: 0 };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return key;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get cached file data if available and not stale
|
|
248
|
+
*/
|
|
249
|
+
async get(absPath) {
|
|
250
|
+
const key = await this.getCacheKey(absPath);
|
|
251
|
+
const cacheKey = `${key.absPath}|${key.mtime}|${key.size}`;
|
|
252
|
+
const entry = this.cache.get(cacheKey);
|
|
253
|
+
if (!entry) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const stats = await promises.stat(absPath);
|
|
258
|
+
if (stats.mtime.getTime() !== key.mtime || stats.size !== key.size) {
|
|
259
|
+
this.cache.delete(cacheKey);
|
|
260
|
+
const newKey = {
|
|
261
|
+
absPath,
|
|
262
|
+
mtime: stats.mtime.getTime(),
|
|
263
|
+
size: stats.size
|
|
264
|
+
};
|
|
265
|
+
this.keyCache.set(`key:${absPath}`, newKey);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
this.cache.delete(cacheKey);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return entry.data;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Cache file data
|
|
276
|
+
*/
|
|
277
|
+
async set(absPath, data) {
|
|
278
|
+
const key = await this.getCacheKey(absPath);
|
|
279
|
+
const cacheKey = `${key.absPath}|${key.mtime}|${key.size}`;
|
|
280
|
+
this.cache.set(cacheKey, {
|
|
281
|
+
data,
|
|
282
|
+
mtime: key.mtime,
|
|
283
|
+
size: key.size,
|
|
284
|
+
timestamp: Date.now()
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Clear all caches
|
|
289
|
+
*/
|
|
290
|
+
clearCaches() {
|
|
291
|
+
this.cache.clear();
|
|
292
|
+
this.keyCache.clear();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get cache statistics
|
|
296
|
+
*/
|
|
297
|
+
getStats() {
|
|
298
|
+
return {
|
|
299
|
+
size: this.cache.size(),
|
|
300
|
+
keySize: this.keyCache.size()
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var fsCache = new FSCache();
|
|
305
|
+
function clearCaches() {
|
|
306
|
+
fsCache.clearCaches();
|
|
307
|
+
}
|
|
308
|
+
function computeConfigHash(obj) {
|
|
309
|
+
const normalized = normalizeForHash(obj);
|
|
310
|
+
const json = JSON.stringify(normalized, null, 0);
|
|
311
|
+
return createHash("sha256").update(json, "utf8").digest("hex");
|
|
312
|
+
}
|
|
313
|
+
function normalizeForHash(obj) {
|
|
314
|
+
if (obj === null || obj === void 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (typeof obj !== "object") {
|
|
318
|
+
return obj;
|
|
319
|
+
}
|
|
320
|
+
if (Array.isArray(obj)) {
|
|
321
|
+
return obj.map(normalizeForHash);
|
|
322
|
+
}
|
|
323
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
324
|
+
const normalized = {};
|
|
325
|
+
for (const key of sortedKeys) {
|
|
326
|
+
const value = obj[key];
|
|
327
|
+
if (value !== void 0) {
|
|
328
|
+
normalized[key] = normalizeForHash(value);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return normalized;
|
|
332
|
+
}
|
|
333
|
+
function computeConfigsHash(configs) {
|
|
334
|
+
const combined = configs.reduce((acc, config) => {
|
|
335
|
+
return { ...acc, ...config };
|
|
336
|
+
}, {});
|
|
337
|
+
return computeConfigHash(combined);
|
|
338
|
+
}
|
|
339
|
+
async function readConfigFile(filePath) {
|
|
340
|
+
const absPath = path3.resolve(filePath);
|
|
341
|
+
const cached = await fsCache.get(absPath);
|
|
342
|
+
if (cached !== null) {
|
|
343
|
+
return cached;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const raw = await promises.readFile(absPath, "utf-8");
|
|
347
|
+
const ext = extname(absPath).toLowerCase();
|
|
348
|
+
let data;
|
|
349
|
+
let format;
|
|
350
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
351
|
+
data = YAML2.parse(raw);
|
|
352
|
+
format = "yaml";
|
|
353
|
+
} else {
|
|
354
|
+
data = JSON.parse(raw);
|
|
355
|
+
format = "json";
|
|
356
|
+
}
|
|
357
|
+
const result = { data, format, path: absPath };
|
|
358
|
+
await fsCache.set(absPath, result);
|
|
359
|
+
return result;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof SyntaxError) {
|
|
362
|
+
throw new KbError(
|
|
363
|
+
"ERR_CONFIG_INVALID",
|
|
364
|
+
`Failed to parse config file: ${filePath}`,
|
|
365
|
+
ERROR_HINTS.ERR_CONFIG_INVALID,
|
|
366
|
+
{ filePath, error: error.message }
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
throw new KbError(
|
|
370
|
+
"ERR_CONFIG_NOT_FOUND",
|
|
371
|
+
`Config file not found: ${filePath}`,
|
|
372
|
+
ERROR_HINTS.ERR_CONFIG_NOT_FOUND,
|
|
373
|
+
{ filePath, error: error instanceof Error ? error.message : String(error) }
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function findNearestConfig2(startDir, filenames = []) {
|
|
378
|
+
const start = path3.resolve(startDir);
|
|
379
|
+
const tried = [];
|
|
380
|
+
let dir = start;
|
|
381
|
+
const defaultFilenames = [
|
|
382
|
+
".kb/kb.config.yaml",
|
|
383
|
+
".kb/kb.config.yml",
|
|
384
|
+
".kb/kb.config.json"
|
|
385
|
+
];
|
|
386
|
+
const searchFilenames = filenames.length > 0 ? filenames : defaultFilenames;
|
|
387
|
+
while (true) {
|
|
388
|
+
for (const filename of searchFilenames) {
|
|
389
|
+
const candidate = path3.join(dir, filename);
|
|
390
|
+
tried.push(candidate);
|
|
391
|
+
try {
|
|
392
|
+
await promises.access(candidate);
|
|
393
|
+
return { path: candidate, tried };
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const parent = path3.dirname(dir);
|
|
398
|
+
if (parent === dir) {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
dir = parent;
|
|
402
|
+
}
|
|
403
|
+
return { path: null, tried };
|
|
404
|
+
}
|
|
405
|
+
async function findGitRoot(startDir) {
|
|
406
|
+
let dir = path3.resolve(startDir);
|
|
407
|
+
while (true) {
|
|
408
|
+
try {
|
|
409
|
+
await promises.access(path3.join(dir, ".git"));
|
|
410
|
+
return dir;
|
|
411
|
+
} catch {
|
|
412
|
+
const parent = path3.dirname(dir);
|
|
413
|
+
if (parent === dir) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
dir = parent;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
async function readWorkspaceConfig(cwd) {
|
|
422
|
+
const { path: configPath } = await findNearestConfig2(cwd);
|
|
423
|
+
if (!configPath) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return readConfigFile(configPath);
|
|
427
|
+
}
|
|
428
|
+
var KB_CONFIG_FILENAMES = [
|
|
429
|
+
"kb.config.json",
|
|
430
|
+
"kb.config.yaml",
|
|
431
|
+
"kb.config.yml",
|
|
432
|
+
".kb/kb.config.json",
|
|
433
|
+
".kb/kb.config.yaml",
|
|
434
|
+
".kb/kb.config.yml"
|
|
435
|
+
];
|
|
436
|
+
async function findKbConfig(cwd, filenames = KB_CONFIG_FILENAMES) {
|
|
437
|
+
const tried = [];
|
|
438
|
+
const gitRoot = await findGitRoot(cwd);
|
|
439
|
+
const stopDir = gitRoot || path3.parse(cwd).root;
|
|
440
|
+
let dir = path3.resolve(cwd);
|
|
441
|
+
while (true) {
|
|
442
|
+
for (const filename of filenames) {
|
|
443
|
+
const candidate = path3.join(dir, filename);
|
|
444
|
+
tried.push(candidate);
|
|
445
|
+
try {
|
|
446
|
+
await promises.access(candidate);
|
|
447
|
+
return { path: candidate, tried };
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (dir === stopDir || dir === path3.dirname(dir)) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
dir = path3.dirname(dir);
|
|
455
|
+
}
|
|
456
|
+
return { path: null, tried };
|
|
457
|
+
}
|
|
458
|
+
async function readKbConfig(cwd) {
|
|
459
|
+
const { path: path12 } = await findKbConfig(cwd);
|
|
460
|
+
if (!path12) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
return readConfigFile(path12);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/merge/layered-merge.ts
|
|
467
|
+
function layeredMergeWithTrace(layers) {
|
|
468
|
+
const trace = [];
|
|
469
|
+
const isPlainObject2 = (v) => v && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
470
|
+
const merge = (base, overlay, layer, path12 = []) => {
|
|
471
|
+
if (Array.isArray(base) && Array.isArray(overlay)) {
|
|
472
|
+
trace.push({
|
|
473
|
+
path: path12.join("."),
|
|
474
|
+
source: layer.source,
|
|
475
|
+
type: "overwriteArray",
|
|
476
|
+
layer: layer.label,
|
|
477
|
+
profileRef: layer.source.includes("profile") ? extractProfileRef(layer.source) : void 0,
|
|
478
|
+
presetRef: layer.source.includes("preset") ? extractPresetRef(layer.source) : void 0,
|
|
479
|
+
version: extractVersion(layer.source)
|
|
480
|
+
});
|
|
481
|
+
return overlay;
|
|
482
|
+
}
|
|
483
|
+
if (isPlainObject2(base) && isPlainObject2(overlay)) {
|
|
484
|
+
const result = { ...base };
|
|
485
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
486
|
+
if (key in base) {
|
|
487
|
+
result[key] = merge(base[key], value, layer, [...path12, key]);
|
|
488
|
+
} else {
|
|
489
|
+
trace.push({
|
|
490
|
+
path: [...path12, key].join("."),
|
|
491
|
+
source: layer.source,
|
|
492
|
+
type: "set",
|
|
493
|
+
layer: layer.label,
|
|
494
|
+
profileRef: layer.source.includes("profile") ? extractProfileRef(layer.source) : void 0,
|
|
495
|
+
presetRef: layer.source.includes("preset") ? extractPresetRef(layer.source) : void 0,
|
|
496
|
+
version: extractVersion(layer.source)
|
|
497
|
+
});
|
|
498
|
+
result[key] = value;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
trace.push({
|
|
504
|
+
path: path12.join("."),
|
|
505
|
+
source: layer.source,
|
|
506
|
+
type: "set",
|
|
507
|
+
layer: layer.label,
|
|
508
|
+
profileRef: layer.source.includes("profile") ? extractProfileRef(layer.source) : void 0,
|
|
509
|
+
presetRef: layer.source.includes("preset") ? extractPresetRef(layer.source) : void 0,
|
|
510
|
+
version: extractVersion(layer.source)
|
|
511
|
+
});
|
|
512
|
+
return overlay ?? base;
|
|
513
|
+
};
|
|
514
|
+
const merged = layers.reduce((acc, layer) => {
|
|
515
|
+
return merge(acc, layer.value, layer);
|
|
516
|
+
}, {});
|
|
517
|
+
return { merged, trace };
|
|
518
|
+
}
|
|
519
|
+
function extractProfileRef(source) {
|
|
520
|
+
const match = source.match(/profile[^:]*:[^@]+@(.+)/);
|
|
521
|
+
return match?.[1];
|
|
522
|
+
}
|
|
523
|
+
function extractPresetRef(source) {
|
|
524
|
+
const match = source.match(/preset[^:]*:@(.+)/);
|
|
525
|
+
return match?.[1];
|
|
526
|
+
}
|
|
527
|
+
function extractVersion(source) {
|
|
528
|
+
const match = source.match(/@([^:]+)/);
|
|
529
|
+
return match?.[1];
|
|
530
|
+
}
|
|
531
|
+
async function resolvePreset(presetRef, cwd) {
|
|
532
|
+
const [name, version] = parsePresetRef(presetRef);
|
|
533
|
+
const localPath = path3.join(cwd, "node_modules", name);
|
|
534
|
+
try {
|
|
535
|
+
const presetInfo = await loadPresetFromPath(localPath, name, version);
|
|
536
|
+
if (presetInfo) {
|
|
537
|
+
return presetInfo;
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
throw new KbError(
|
|
542
|
+
"ERR_PRESET_NOT_RESOLVED",
|
|
543
|
+
`Preset not found: ${presetRef}`,
|
|
544
|
+
ERROR_HINTS.ERR_PRESET_NOT_RESOLVED,
|
|
545
|
+
{ presetRef, cwd }
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
function parsePresetRef(presetRef) {
|
|
549
|
+
const atIndex = presetRef.lastIndexOf("@");
|
|
550
|
+
if (atIndex === -1) {
|
|
551
|
+
throw new KbError(
|
|
552
|
+
"ERR_PRESET_REF_INVALID",
|
|
553
|
+
`Invalid preset reference: ${presetRef}`,
|
|
554
|
+
"Preset references must include version (e.g., @kb-labs/org-preset@1.3.2)",
|
|
555
|
+
{ presetRef }
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const name = presetRef.substring(0, atIndex);
|
|
559
|
+
const version = presetRef.substring(atIndex + 1);
|
|
560
|
+
if (!name || !version) {
|
|
561
|
+
throw new KbError(
|
|
562
|
+
"ERR_PRESET_REF_INVALID",
|
|
563
|
+
`Invalid preset reference: ${presetRef}`,
|
|
564
|
+
"Preset references must include both name and version",
|
|
565
|
+
{ presetRef }
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return [name, version];
|
|
569
|
+
}
|
|
570
|
+
async function loadPresetFromPath(presetPath, name, version) {
|
|
571
|
+
try {
|
|
572
|
+
await promises.access(presetPath);
|
|
573
|
+
const packageJsonPath = path3.join(presetPath, "package.json");
|
|
574
|
+
const packageJson = JSON.parse(await promises.readFile(packageJsonPath, "utf-8"));
|
|
575
|
+
if (!versionMatches(packageJson.version, version)) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const configPath = path3.join(presetPath, "config.defaults.json");
|
|
579
|
+
const config = JSON.parse(await promises.readFile(configPath, "utf-8"));
|
|
580
|
+
return {
|
|
581
|
+
name,
|
|
582
|
+
version: packageJson.version,
|
|
583
|
+
path: presetPath,
|
|
584
|
+
config
|
|
585
|
+
};
|
|
586
|
+
} catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function versionMatches(actualVersion, requiredVersion) {
|
|
591
|
+
if (requiredVersion.startsWith("^")) {
|
|
592
|
+
const major = requiredVersion.substring(1).split(".")[0];
|
|
593
|
+
const actualMajor = actualVersion.split(".")[0];
|
|
594
|
+
return major === actualMajor;
|
|
595
|
+
}
|
|
596
|
+
return actualVersion === requiredVersion;
|
|
597
|
+
}
|
|
598
|
+
function getPresetConfigForProduct(preset, product) {
|
|
599
|
+
const fsProduct = toFsProduct(product);
|
|
600
|
+
return preset.config.products?.[fsProduct] || {};
|
|
601
|
+
}
|
|
602
|
+
function getPresetMetadata(preset) {
|
|
603
|
+
return {
|
|
604
|
+
name: preset.name,
|
|
605
|
+
version: preset.version,
|
|
606
|
+
path: preset.path,
|
|
607
|
+
products: Object.keys(preset.config.products || {})
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function readLockfile(cwd) {
|
|
611
|
+
const lockfilePath = path3.join(cwd, ".kb", "lock.json");
|
|
612
|
+
try {
|
|
613
|
+
const lockfileData = await promises.readFile(lockfilePath, "utf-8");
|
|
614
|
+
return JSON.parse(lockfileData);
|
|
615
|
+
} catch {
|
|
616
|
+
const oldLockfilePath = path3.join(cwd, ".kb", "lockfile.json");
|
|
617
|
+
try {
|
|
618
|
+
const lockfileData = await promises.readFile(oldLockfilePath, "utf-8");
|
|
619
|
+
return JSON.parse(lockfileData);
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function stableStringify(obj, space = 2) {
|
|
626
|
+
return JSON.stringify(obj, (key, value) => {
|
|
627
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
628
|
+
return Object.keys(value).sort().reduce((sorted, key2) => {
|
|
629
|
+
sorted[key2] = value[key2];
|
|
630
|
+
return sorted;
|
|
631
|
+
}, {});
|
|
632
|
+
}
|
|
633
|
+
return value;
|
|
634
|
+
}, space);
|
|
635
|
+
}
|
|
636
|
+
async function writeLockfile(cwd, lockfileData) {
|
|
637
|
+
const lockfileDir = path3.join(cwd, ".kb");
|
|
638
|
+
const lockfilePath = path3.join(lockfileDir, "lock.json");
|
|
639
|
+
await promises.mkdir(lockfileDir, { recursive: true });
|
|
640
|
+
if (!lockfileData.$schema) {
|
|
641
|
+
lockfileData.$schema = "https://schemas.kb-labs.dev/lockfile.schema.json";
|
|
642
|
+
}
|
|
643
|
+
const { writeFileAtomic: writeFileAtomic2 } = await import('./fs-atomic-EKSZ6G5A.js');
|
|
644
|
+
await writeFileAtomic2(lockfilePath, stableStringify(lockfileData, 2) + "\n");
|
|
645
|
+
}
|
|
646
|
+
async function updateLockfile(cwd, updates) {
|
|
647
|
+
const existing = await readLockfile(cwd);
|
|
648
|
+
const lockfileData = {
|
|
649
|
+
$schema: "https://schemas.kb-labs.dev/lockfile.schema.json",
|
|
650
|
+
schemaVersion: "1.0",
|
|
651
|
+
orgPreset: updates.orgPreset || existing?.orgPreset,
|
|
652
|
+
profile: updates.profile || existing?.profile,
|
|
653
|
+
policyBundle: updates.policyBundle || existing?.policyBundle,
|
|
654
|
+
hashes: { ...existing?.hashes },
|
|
655
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
656
|
+
};
|
|
657
|
+
if (updates.configHashes) {
|
|
658
|
+
for (const [product, config] of Object.entries(updates.configHashes)) {
|
|
659
|
+
const fsProduct = toFsProduct(product);
|
|
660
|
+
const hash = computeConfigHash(config);
|
|
661
|
+
lockfileData.hashes[fsProduct] = hash;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
await writeLockfile(cwd, lockfileData);
|
|
665
|
+
return lockfileData;
|
|
666
|
+
}
|
|
667
|
+
function getLockfileConfigHash(product, config) {
|
|
668
|
+
return computeConfigHash(config);
|
|
669
|
+
}
|
|
670
|
+
async function isLockfileUpToDate(cwd, configs) {
|
|
671
|
+
const lockfile = await readLockfile(cwd);
|
|
672
|
+
if (!lockfile) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
for (const [product, config] of Object.entries(configs)) {
|
|
676
|
+
const fsProduct = toFsProduct(product);
|
|
677
|
+
const currentHash = computeConfigHash(config);
|
|
678
|
+
const lockfileHash = lockfile.hashes[fsProduct];
|
|
679
|
+
if (currentHash !== lockfileHash) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
function validateLockfile(lockfileData) {
|
|
686
|
+
if (!lockfileData || typeof lockfileData !== "object") {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
if (lockfileData.schemaVersion !== "1.0") {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
if (!lockfileData.hashes || typeof lockfileData.hashes !== "object") {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
if (!lockfileData.generatedAt || typeof lockfileData.generatedAt !== "string") {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
function getLockfileStats(lockfileData) {
|
|
701
|
+
return {
|
|
702
|
+
productCount: Object.keys(lockfileData.hashes).length,
|
|
703
|
+
hasOrgPreset: !!lockfileData.orgPreset,
|
|
704
|
+
hasProfile: !!lockfileData.profile,
|
|
705
|
+
hasPolicyBundle: !!lockfileData.policyBundle,
|
|
706
|
+
generatedAt: new Date(lockfileData.generatedAt)
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/api/product-config.ts
|
|
711
|
+
async function getProductConfig(opts, schema) {
|
|
712
|
+
const { cwd, product, cli = {}, writeFinal = false, profileLayer } = opts;
|
|
713
|
+
const fsProduct = toFsProduct(product);
|
|
714
|
+
const workspaceConfig = await readWorkspaceConfig(cwd);
|
|
715
|
+
const layers = [];
|
|
716
|
+
layers.push({
|
|
717
|
+
label: "runtime",
|
|
718
|
+
value: getRuntimeDefaults(product),
|
|
719
|
+
source: "runtime:defaults"
|
|
720
|
+
});
|
|
721
|
+
if (profileLayer && profileLayer.products) {
|
|
722
|
+
const profileOverlay = profileLayer.products;
|
|
723
|
+
layers.push({
|
|
724
|
+
label: "profile",
|
|
725
|
+
value: profileOverlay,
|
|
726
|
+
source: profileLayer.source
|
|
727
|
+
});
|
|
728
|
+
if (profileLayer.scope && profileLayer.scope.products) {
|
|
729
|
+
const scopeOverlay = profileLayer.scope.products;
|
|
730
|
+
layers.push({
|
|
731
|
+
label: "profile-scope",
|
|
732
|
+
value: scopeOverlay,
|
|
733
|
+
source: profileLayer.scope.source
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
layers.push({
|
|
738
|
+
label: "profile",
|
|
739
|
+
value: {},
|
|
740
|
+
source: "profile:none"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
let presetConfig = {};
|
|
744
|
+
if (workspaceConfig?.data) {
|
|
745
|
+
const workspaceData = workspaceConfig.data;
|
|
746
|
+
if (workspaceData.$extends) {
|
|
747
|
+
try {
|
|
748
|
+
const preset = await resolvePreset(workspaceData.$extends, cwd);
|
|
749
|
+
presetConfig = getPresetConfigForProduct(preset, product);
|
|
750
|
+
layers.push({
|
|
751
|
+
label: "preset",
|
|
752
|
+
value: presetConfig,
|
|
753
|
+
source: `preset:${workspaceData.$extends}`
|
|
754
|
+
});
|
|
755
|
+
} catch (error) {
|
|
756
|
+
console.warn(`Warning: Could not resolve preset ${workspaceData.$extends}:`, error);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (Object.keys(presetConfig).length === 0) {
|
|
761
|
+
layers.push({
|
|
762
|
+
label: "preset",
|
|
763
|
+
value: {},
|
|
764
|
+
source: "preset:none"
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (workspaceConfig?.data) {
|
|
768
|
+
const workspaceData = workspaceConfig.data?.products?.[fsProduct] || {};
|
|
769
|
+
layers.push({
|
|
770
|
+
label: "workspace",
|
|
771
|
+
value: workspaceData,
|
|
772
|
+
source: "workspace:kb-labs.config"
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const localConfigPath = path3.join(cwd, ".kb", fsProduct, `${fsProduct}.config.json`);
|
|
776
|
+
try {
|
|
777
|
+
const localConfig = await promises.readFile(localConfigPath, "utf-8");
|
|
778
|
+
const localData = JSON.parse(localConfig);
|
|
779
|
+
layers.push({
|
|
780
|
+
label: "local",
|
|
781
|
+
value: localData,
|
|
782
|
+
source: `local:.kb/${fsProduct}/${fsProduct}.config.json`
|
|
783
|
+
});
|
|
784
|
+
} catch {
|
|
785
|
+
}
|
|
786
|
+
if (Object.keys(cli).length > 0) {
|
|
787
|
+
layers.push({
|
|
788
|
+
label: "cli",
|
|
789
|
+
value: cli,
|
|
790
|
+
source: "cli:overrides"
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const { merged, trace } = layeredMergeWithTrace(layers);
|
|
794
|
+
if (writeFinal) {
|
|
795
|
+
await writeFinalConfig(cwd, fsProduct, merged);
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const configHash = computeConfigHash(merged);
|
|
799
|
+
const configHashes = {};
|
|
800
|
+
configHashes[product] = configHash;
|
|
801
|
+
await updateLockfile(cwd, {
|
|
802
|
+
configHashes
|
|
803
|
+
});
|
|
804
|
+
} catch (error) {
|
|
805
|
+
console.warn("Warning: Could not update lockfile:", error);
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
config: merged,
|
|
809
|
+
trace
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
async function explainProductConfig(opts, schema) {
|
|
813
|
+
const result = await getProductConfig(opts);
|
|
814
|
+
return { trace: result.trace };
|
|
815
|
+
}
|
|
816
|
+
function getRuntimeDefaults(product) {
|
|
817
|
+
const defaults = {
|
|
818
|
+
devlink: {
|
|
819
|
+
watch: true,
|
|
820
|
+
build: true
|
|
821
|
+
},
|
|
822
|
+
release: {
|
|
823
|
+
registry: "https://registry.npmjs.org",
|
|
824
|
+
strategy: "semver",
|
|
825
|
+
bump: "auto",
|
|
826
|
+
strict: true,
|
|
827
|
+
verify: ["audit", "build", "tests"],
|
|
828
|
+
publish: { npm: true, github: false },
|
|
829
|
+
rollback: { enabled: true, maxHistory: 5 },
|
|
830
|
+
output: { json: true, md: true, text: true },
|
|
831
|
+
changelog: {
|
|
832
|
+
enabled: true,
|
|
833
|
+
includeTypes: ["feat", "fix", "perf", "refactor", "revert"],
|
|
834
|
+
excludeTypes: ["chore", "ci"],
|
|
835
|
+
ignoreAuthors: ["dependabot", "renovate", "github-actions[bot]", "*[bot]"],
|
|
836
|
+
collapseMerges: true,
|
|
837
|
+
collapseReverts: true,
|
|
838
|
+
preferMergeSummary: true,
|
|
839
|
+
bumpStrategy: "independent",
|
|
840
|
+
workspace: true,
|
|
841
|
+
perPackage: true,
|
|
842
|
+
format: "both",
|
|
843
|
+
level: "standard",
|
|
844
|
+
locale: "en",
|
|
845
|
+
cache: true,
|
|
846
|
+
requireAudit: true,
|
|
847
|
+
requireSignedTags: false,
|
|
848
|
+
redactPatterns: ["(?i)token=\\w+", "(?i)apikey=\\w+", "(https?://[^\\s]+@[^\\s]+)", "(gh[ps]_[A-Za-z0-9_]+)"],
|
|
849
|
+
maxBodyLength: 500,
|
|
850
|
+
ignoreSubmodules: true
|
|
851
|
+
},
|
|
852
|
+
git: {
|
|
853
|
+
provider: "auto",
|
|
854
|
+
autoUnshallow: false
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
aiReview: {
|
|
858
|
+
enabled: true,
|
|
859
|
+
rules: []
|
|
860
|
+
},
|
|
861
|
+
aiDocs: {
|
|
862
|
+
enabled: true,
|
|
863
|
+
templates: []
|
|
864
|
+
},
|
|
865
|
+
devkit: {
|
|
866
|
+
sync: true,
|
|
867
|
+
check: true
|
|
868
|
+
},
|
|
869
|
+
analytics: {
|
|
870
|
+
enabled: true,
|
|
871
|
+
sinks: [],
|
|
872
|
+
buffer: {}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
return defaults[product] || {};
|
|
876
|
+
}
|
|
877
|
+
async function writeFinalConfig(cwd, fsProduct, config) {
|
|
878
|
+
const configDir = path3.join(cwd, ".kb", fsProduct);
|
|
879
|
+
const configPath = path3.join(configDir, `${fsProduct}.config.json`);
|
|
880
|
+
await promises.mkdir(configDir, { recursive: true });
|
|
881
|
+
const finalConfig = {
|
|
882
|
+
$schema: "https://schemas.kb-labs.dev/config.schema.json",
|
|
883
|
+
schemaVersion: "1.0",
|
|
884
|
+
...config
|
|
885
|
+
};
|
|
886
|
+
await promises.writeFile(configPath, JSON.stringify(finalConfig, null, 2));
|
|
887
|
+
}
|
|
888
|
+
function getConfigHash(config) {
|
|
889
|
+
return computeConfigHash(config);
|
|
890
|
+
}
|
|
891
|
+
var GlobSchema = z.string().min(1, "Glob pattern must be a non-empty string");
|
|
892
|
+
var ProductOverridesSchema = z.record(z.string(), z.unknown());
|
|
893
|
+
var ProductsMapSchema = z.record(z.string(), ProductOverridesSchema);
|
|
894
|
+
var ScopeV2Schema = z.object({
|
|
895
|
+
id: z.string().min(1, "Scope id is required"),
|
|
896
|
+
label: z.string().optional(),
|
|
897
|
+
description: z.string().optional(),
|
|
898
|
+
include: z.array(GlobSchema).min(1, "include requires at least one glob"),
|
|
899
|
+
exclude: z.array(GlobSchema).optional(),
|
|
900
|
+
products: ProductsMapSchema.optional(),
|
|
901
|
+
default: z.boolean().optional()
|
|
902
|
+
});
|
|
903
|
+
var ProfileMetaSchema = z.object({
|
|
904
|
+
version: z.string().optional(),
|
|
905
|
+
tags: z.array(z.string()).optional(),
|
|
906
|
+
deprecated: z.boolean().optional(),
|
|
907
|
+
owner: z.string().optional()
|
|
908
|
+
}).strict();
|
|
909
|
+
var ProfileV2Schema = z.object({
|
|
910
|
+
id: z.string().min(1, "Profile id is required"),
|
|
911
|
+
label: z.string().optional(),
|
|
912
|
+
description: z.string().optional(),
|
|
913
|
+
extends: z.string().min(1, "extends must reference another profile id or preset").optional(),
|
|
914
|
+
scopes: z.array(ScopeV2Schema).optional(),
|
|
915
|
+
products: ProductsMapSchema.optional(),
|
|
916
|
+
meta: ProfileMetaSchema.optional()
|
|
917
|
+
});
|
|
918
|
+
var ProfilesV2Schema = z.array(ProfileV2Schema);
|
|
919
|
+
var ResolvedScopeSchema = z.object({
|
|
920
|
+
id: z.string(),
|
|
921
|
+
label: z.string().optional(),
|
|
922
|
+
description: z.string().optional(),
|
|
923
|
+
include: z.array(z.string()),
|
|
924
|
+
exclude: z.array(z.string()).optional(),
|
|
925
|
+
isDefault: z.boolean(),
|
|
926
|
+
products: ProductsMapSchema.optional()
|
|
927
|
+
});
|
|
928
|
+
var BundleProfileSourceSchema = z.enum(["workspace", "preset", "npm", "implicit"]);
|
|
929
|
+
var ProfileTraceSchema = z.object({
|
|
930
|
+
extends: z.array(z.string()).optional(),
|
|
931
|
+
overrides: z.array(z.string()).optional()
|
|
932
|
+
});
|
|
933
|
+
var ScopeSelectionSchema = z.object({
|
|
934
|
+
strategy: z.enum(["explicit", "default", "auto", "none"]),
|
|
935
|
+
path: z.string().optional()
|
|
936
|
+
});
|
|
937
|
+
var BundleProfileSchema = z.object({
|
|
938
|
+
id: z.string(),
|
|
939
|
+
key: z.string().optional(),
|
|
940
|
+
label: z.string().optional(),
|
|
941
|
+
name: z.string().optional(),
|
|
942
|
+
version: z.string().optional(),
|
|
943
|
+
source: BundleProfileSourceSchema,
|
|
944
|
+
scopes: z.array(ResolvedScopeSchema),
|
|
945
|
+
products: ProductsMapSchema.optional(),
|
|
946
|
+
trace: ProfileTraceSchema.optional(),
|
|
947
|
+
meta: ProfileMetaSchema.optional(),
|
|
948
|
+
productsByScope: z.record(z.string(), ProductsMapSchema).optional(),
|
|
949
|
+
activeScopeId: z.string().optional(),
|
|
950
|
+
activeScope: ResolvedScopeSchema.optional(),
|
|
951
|
+
scopeSelection: ScopeSelectionSchema.optional()
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// src/profiles/loader.ts
|
|
955
|
+
async function readProfilesSection(cwd) {
|
|
956
|
+
const kbConfig = await readKbConfig(cwd);
|
|
957
|
+
if (!kbConfig?.data) {
|
|
958
|
+
return { profiles: [] };
|
|
959
|
+
}
|
|
960
|
+
const rawProfiles = kbConfig.data?.profiles;
|
|
961
|
+
if (rawProfiles === void 0) {
|
|
962
|
+
return {
|
|
963
|
+
profiles: [],
|
|
964
|
+
sourcePath: kbConfig.path,
|
|
965
|
+
format: kbConfig.format
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
const parsed = ProfilesV2Schema.safeParse(rawProfiles);
|
|
969
|
+
if (!parsed.success) {
|
|
970
|
+
throw new KbError(
|
|
971
|
+
"ERR_PROFILE_INVALID_FORMAT",
|
|
972
|
+
"Invalid profiles[] section in kb.config.",
|
|
973
|
+
ERROR_HINTS.ERR_PROFILE_INVALID_FORMAT,
|
|
974
|
+
{
|
|
975
|
+
issues: parsed.error.issues,
|
|
976
|
+
sourcePath: kbConfig.path
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
profiles: parsed.data,
|
|
982
|
+
sourcePath: kbConfig.path,
|
|
983
|
+
format: kbConfig.format
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function selectProfileScope(opts) {
|
|
987
|
+
const scopes = opts.bundleProfile.scopes || [];
|
|
988
|
+
if (scopes.length === 0) {
|
|
989
|
+
return { strategy: "none" };
|
|
990
|
+
}
|
|
991
|
+
if (opts.scopeId) {
|
|
992
|
+
const scope = scopes.find((s) => s.id === opts.scopeId);
|
|
993
|
+
if (!scope) {
|
|
994
|
+
throw new KbError(
|
|
995
|
+
"ERR_PROFILE_SCOPE_NOT_FOUND",
|
|
996
|
+
`Scope "${opts.scopeId}" not found in profile "${opts.bundleProfile.id}"`,
|
|
997
|
+
ERROR_HINTS.ERR_PROFILE_SCOPE_NOT_FOUND,
|
|
998
|
+
{ scopeId: opts.scopeId, available: scopes.map((s) => s.id) }
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
return { scope, strategy: "explicit" };
|
|
1002
|
+
}
|
|
1003
|
+
const defaults = scopes.filter((s) => s.isDefault);
|
|
1004
|
+
if (defaults.length > 1) {
|
|
1005
|
+
throw new KbError(
|
|
1006
|
+
"ERR_PROFILE_SCOPE_CONFLICT",
|
|
1007
|
+
`Multiple default scopes found in profile "${opts.bundleProfile.id}"`,
|
|
1008
|
+
ERROR_HINTS.ERR_PROFILE_SCOPE_CONFLICT,
|
|
1009
|
+
{ scopes: defaults.map((s) => s.id) }
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (defaults.length === 1) {
|
|
1013
|
+
return { scope: defaults[0], strategy: "default" };
|
|
1014
|
+
}
|
|
1015
|
+
const relativePath = normalizePath(
|
|
1016
|
+
path3.relative(opts.cwd, opts.executionPath)
|
|
1017
|
+
);
|
|
1018
|
+
const matches = scopes.filter(
|
|
1019
|
+
(scope) => scopeMatchesPath(scope, relativePath)
|
|
1020
|
+
);
|
|
1021
|
+
if (matches.length > 1) {
|
|
1022
|
+
throw new KbError(
|
|
1023
|
+
"ERR_PROFILE_SCOPE_CONFLICT",
|
|
1024
|
+
`Multiple scopes match path "${relativePath}"`,
|
|
1025
|
+
ERROR_HINTS.ERR_PROFILE_SCOPE_CONFLICT,
|
|
1026
|
+
{ scopes: matches.map((s) => s.id), path: relativePath }
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
if (matches.length === 1) {
|
|
1030
|
+
return { scope: matches[0], strategy: "auto", matchedPath: relativePath };
|
|
1031
|
+
}
|
|
1032
|
+
return { strategy: "none" };
|
|
1033
|
+
}
|
|
1034
|
+
function scopeMatchesPath(scope, candidate) {
|
|
1035
|
+
const includes = scope.include && scope.include.length > 0 ? scope.include : ["**/*"];
|
|
1036
|
+
const excludeGlobs = scope.exclude || [];
|
|
1037
|
+
const tester = (glob) => picomatch(glob, { dot: true, nocase: false })(candidate);
|
|
1038
|
+
const includeMatched = candidate === "." || includes.some((glob) => tester(adjustGlob(glob)));
|
|
1039
|
+
if (!includeMatched) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
const excludeMatched = excludeGlobs.some(
|
|
1043
|
+
(glob) => tester(adjustGlob(glob))
|
|
1044
|
+
);
|
|
1045
|
+
return !excludeMatched;
|
|
1046
|
+
}
|
|
1047
|
+
function normalizePath(p) {
|
|
1048
|
+
if (!p || p === ".") {
|
|
1049
|
+
return ".";
|
|
1050
|
+
}
|
|
1051
|
+
const normalized = p.split(path3.sep).join("/");
|
|
1052
|
+
return normalized || ".";
|
|
1053
|
+
}
|
|
1054
|
+
function adjustGlob(glob) {
|
|
1055
|
+
if (!glob) {
|
|
1056
|
+
return "**/*";
|
|
1057
|
+
}
|
|
1058
|
+
return glob;
|
|
1059
|
+
}
|
|
1060
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
1061
|
+
function clone(value) {
|
|
1062
|
+
if (typeof structuredClone === "function") {
|
|
1063
|
+
return structuredClone(value);
|
|
1064
|
+
}
|
|
1065
|
+
return JSON.parse(JSON.stringify(value));
|
|
1066
|
+
}
|
|
1067
|
+
async function resolveProfile(opts) {
|
|
1068
|
+
const localProfilesResult = await readProfilesSection(opts.cwd);
|
|
1069
|
+
const localProfiles = localProfilesResult.profiles;
|
|
1070
|
+
const localPath = localProfilesResult.sourcePath;
|
|
1071
|
+
const profileMap = new Map(localProfiles.map((p) => [p.id, p]));
|
|
1072
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1073
|
+
if (localPath) {
|
|
1074
|
+
for (const profile of localProfiles) {
|
|
1075
|
+
cache.set(profile.id, {
|
|
1076
|
+
profile: clone(profile),
|
|
1077
|
+
source: "workspace",
|
|
1078
|
+
sourcePath: localPath
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const chain = await resolveProfileChain(
|
|
1083
|
+
opts.profileId,
|
|
1084
|
+
cache,
|
|
1085
|
+
profileMap,
|
|
1086
|
+
opts.cwd,
|
|
1087
|
+
/* @__PURE__ */ new Set()
|
|
1088
|
+
);
|
|
1089
|
+
if (chain.length === 0) {
|
|
1090
|
+
throw new KbError(
|
|
1091
|
+
"ERR_PROFILE_NOT_DEFINED",
|
|
1092
|
+
`Profile "${opts.profileId}" not found`,
|
|
1093
|
+
ERROR_HINTS.ERR_PROFILE_NOT_DEFINED,
|
|
1094
|
+
{
|
|
1095
|
+
profileId: opts.profileId,
|
|
1096
|
+
available: Array.from(profileMap.keys())
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
const bundleProfile = mergeProfileChain(chain);
|
|
1101
|
+
const productsByScope = buildScopeProductMap(bundleProfile, chain);
|
|
1102
|
+
bundleProfile.productsByScope = productsByScope;
|
|
1103
|
+
return bundleProfile;
|
|
1104
|
+
}
|
|
1105
|
+
async function resolveProfileChain(reference, cache, localProfiles, cwd, seen) {
|
|
1106
|
+
const key = reference;
|
|
1107
|
+
if (seen.has(key)) {
|
|
1108
|
+
throw new KbError(
|
|
1109
|
+
"ERR_PROFILE_EXTENDS_FAILED",
|
|
1110
|
+
`Circular profile extends detected at "${reference}"`,
|
|
1111
|
+
ERROR_HINTS.ERR_PROFILE_EXTENDS_FAILED,
|
|
1112
|
+
{ reference, chain: Array.from(seen) }
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
seen.add(key);
|
|
1116
|
+
let layer = cache.get(reference);
|
|
1117
|
+
if (!layer) {
|
|
1118
|
+
if (localProfiles.has(reference)) {
|
|
1119
|
+
layer = {
|
|
1120
|
+
profile: clone(localProfiles.get(reference)),
|
|
1121
|
+
source: "workspace"
|
|
1122
|
+
};
|
|
1123
|
+
} else if (isPackageReference(reference)) {
|
|
1124
|
+
layer = await loadPackageProfile(reference, cwd);
|
|
1125
|
+
} else {
|
|
1126
|
+
throw new KbError(
|
|
1127
|
+
"ERR_PROFILE_NOT_DEFINED",
|
|
1128
|
+
`Profile "${reference}" not found`,
|
|
1129
|
+
ERROR_HINTS.ERR_PROFILE_NOT_DEFINED,
|
|
1130
|
+
{ reference, available: Array.from(localProfiles.keys()) }
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
cache.set(reference, layer);
|
|
1134
|
+
}
|
|
1135
|
+
const result = [];
|
|
1136
|
+
if (layer.profile.extends) {
|
|
1137
|
+
const parentChain = await resolveProfileChain(
|
|
1138
|
+
layer.profile.extends,
|
|
1139
|
+
cache,
|
|
1140
|
+
localProfiles,
|
|
1141
|
+
cwd,
|
|
1142
|
+
seen
|
|
1143
|
+
);
|
|
1144
|
+
result.push(...parentChain);
|
|
1145
|
+
}
|
|
1146
|
+
result.push(layer);
|
|
1147
|
+
seen.delete(key);
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
function mergeProfileChain(chain) {
|
|
1151
|
+
const mergedProducts = {};
|
|
1152
|
+
let mergedMeta;
|
|
1153
|
+
let mergedLabel;
|
|
1154
|
+
let mergedDescription;
|
|
1155
|
+
let mergedVersion;
|
|
1156
|
+
let scopes = [];
|
|
1157
|
+
let scopesDefined = false;
|
|
1158
|
+
for (const layer of chain) {
|
|
1159
|
+
const profile = layer.profile;
|
|
1160
|
+
mergedLabel = profile.label ?? mergedLabel;
|
|
1161
|
+
mergedDescription = profile.description ?? mergedDescription;
|
|
1162
|
+
mergedVersion = profile.meta?.version ?? mergedVersion;
|
|
1163
|
+
mergedMeta = mergeMeta(mergedMeta, profile.meta);
|
|
1164
|
+
if (profile.products) {
|
|
1165
|
+
for (const [productKey, overrides] of Object.entries(profile.products)) {
|
|
1166
|
+
if (!matchedProductOverrides(overrides)) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const target = mergedProducts[productKey] ??= {};
|
|
1170
|
+
Object.assign(target, clone(overrides));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (profile.scopes && profile.scopes.length > 0) {
|
|
1174
|
+
scopes = clone(profile.scopes);
|
|
1175
|
+
scopesDefined = true;
|
|
1176
|
+
} else if (!scopesDefined && profile.scopes) {
|
|
1177
|
+
scopes = clone(profile.scopes);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const finalLayer = chain[chain.length - 1];
|
|
1181
|
+
if (!finalLayer) {
|
|
1182
|
+
throw new KbError(
|
|
1183
|
+
"ERR_PROFILE_NOT_DEFINED",
|
|
1184
|
+
"Profile chain is empty",
|
|
1185
|
+
ERROR_HINTS.ERR_PROFILE_NOT_DEFINED
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const finalId = finalLayer.profile.id;
|
|
1189
|
+
const finalSource = finalLayer.source;
|
|
1190
|
+
const bundleProfile = {
|
|
1191
|
+
id: finalId,
|
|
1192
|
+
key: finalId,
|
|
1193
|
+
label: mergedLabel,
|
|
1194
|
+
name: mergedLabel ?? finalId,
|
|
1195
|
+
version: mergedVersion,
|
|
1196
|
+
source: finalSource,
|
|
1197
|
+
scopes: scopes.map((scope) => ({
|
|
1198
|
+
id: scope.id,
|
|
1199
|
+
label: scope.label,
|
|
1200
|
+
description: scope.description,
|
|
1201
|
+
include: scope.include,
|
|
1202
|
+
exclude: scope.exclude,
|
|
1203
|
+
isDefault: !!scope.default,
|
|
1204
|
+
products: scope.products ? clone(scope.products) : void 0
|
|
1205
|
+
})),
|
|
1206
|
+
products: Object.keys(mergedProducts).length ? mergedProducts : void 0,
|
|
1207
|
+
trace: buildTrace(chain),
|
|
1208
|
+
meta: mergedMeta,
|
|
1209
|
+
productsByScope: {}
|
|
1210
|
+
};
|
|
1211
|
+
return bundleProfile;
|
|
1212
|
+
}
|
|
1213
|
+
function buildScopeProductMap(bundleProfile, chain) {
|
|
1214
|
+
const map = {};
|
|
1215
|
+
for (const scope of bundleProfile.scopes) {
|
|
1216
|
+
map[scope.id] = {};
|
|
1217
|
+
for (const layer of chain) {
|
|
1218
|
+
const matchingScope = layer.profile.scopes?.find((s) => s.id === scope.id);
|
|
1219
|
+
if (!matchingScope?.products) {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
for (const [productKey, overrides] of Object.entries(matchingScope.products)) {
|
|
1223
|
+
if (!matchedProductOverrides(overrides)) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const scopeMap = map[scope.id];
|
|
1227
|
+
if (!scopeMap) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const target = scopeMap[productKey];
|
|
1231
|
+
if (!target) {
|
|
1232
|
+
scopeMap[productKey] = clone(overrides);
|
|
1233
|
+
} else {
|
|
1234
|
+
Object.assign(target, clone(overrides));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return map;
|
|
1240
|
+
}
|
|
1241
|
+
function matchedProductOverrides(overrides) {
|
|
1242
|
+
return overrides !== null && typeof overrides === "object";
|
|
1243
|
+
}
|
|
1244
|
+
function buildTrace(chain) {
|
|
1245
|
+
if (chain.length <= 1) {
|
|
1246
|
+
return void 0;
|
|
1247
|
+
}
|
|
1248
|
+
const extendsList = chain.slice(0, -1).map((layer) => {
|
|
1249
|
+
if (typeof layer.source === "string" && layer.source.startsWith("npm:")) {
|
|
1250
|
+
return layer.source;
|
|
1251
|
+
}
|
|
1252
|
+
return `${layer.source}:${layer.profile.id}`;
|
|
1253
|
+
});
|
|
1254
|
+
return {
|
|
1255
|
+
extends: extendsList
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
function mergeMeta(existing, next) {
|
|
1259
|
+
if (!next) {
|
|
1260
|
+
return existing ? structuredClone(existing) : void 0;
|
|
1261
|
+
}
|
|
1262
|
+
if (!existing) {
|
|
1263
|
+
return structuredClone(next);
|
|
1264
|
+
}
|
|
1265
|
+
return { ...existing, ...clone(next) };
|
|
1266
|
+
}
|
|
1267
|
+
function isPackageReference(ref) {
|
|
1268
|
+
return ref.includes("/") || ref.startsWith("@");
|
|
1269
|
+
}
|
|
1270
|
+
function parsePackageReference(ref) {
|
|
1271
|
+
const [pkg, profileId] = ref.split("#");
|
|
1272
|
+
return {
|
|
1273
|
+
packageName: pkg || "",
|
|
1274
|
+
profileId: profileId || "default"
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
async function loadPackageProfile(ref, cwd) {
|
|
1278
|
+
const { packageName, profileId } = parsePackageReference(ref);
|
|
1279
|
+
let pkgJsonPath;
|
|
1280
|
+
try {
|
|
1281
|
+
pkgJsonPath = requireFromHere.resolve(`${packageName}/package.json`, {
|
|
1282
|
+
paths: [cwd]
|
|
1283
|
+
});
|
|
1284
|
+
} catch {
|
|
1285
|
+
throw new KbError(
|
|
1286
|
+
"ERR_PROFILE_EXTENDS_FAILED",
|
|
1287
|
+
`Cannot resolve package "${packageName}" for profile extends`,
|
|
1288
|
+
ERROR_HINTS.ERR_PROFILE_EXTENDS_FAILED,
|
|
1289
|
+
{ reference: ref }
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const pkgDir = path3.dirname(pkgJsonPath);
|
|
1293
|
+
const candidateConfigs = [
|
|
1294
|
+
"kb.config.json",
|
|
1295
|
+
"kb.config.yaml",
|
|
1296
|
+
"kb.config.yml",
|
|
1297
|
+
path3.join("config", "kb.config.json"),
|
|
1298
|
+
path3.join("config", "kb.config.yaml"),
|
|
1299
|
+
path3.join("config", "kb.config.yml")
|
|
1300
|
+
];
|
|
1301
|
+
for (const candidate of candidateConfigs) {
|
|
1302
|
+
const fullPath = path3.join(pkgDir, candidate);
|
|
1303
|
+
try {
|
|
1304
|
+
await promises.access(fullPath);
|
|
1305
|
+
} catch {
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const config = await readConfigFile(fullPath);
|
|
1309
|
+
const profiles = config.data?.profiles;
|
|
1310
|
+
const parsed = ProfilesV2Schema.safeParse(profiles);
|
|
1311
|
+
if (!parsed.success) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const target = parsed.data.find((p) => p.id === profileId);
|
|
1315
|
+
if (!target) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
profile: clone(target),
|
|
1320
|
+
source: `npm:${packageName}#${profileId}`,
|
|
1321
|
+
sourcePath: fullPath
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
throw new KbError(
|
|
1325
|
+
"ERR_PROFILE_EXTENDS_FAILED",
|
|
1326
|
+
`Profile "${profileId}" not found in package "${packageName}"`,
|
|
1327
|
+
ERROR_HINTS.ERR_PROFILE_EXTENDS_FAILED,
|
|
1328
|
+
{ reference: ref }
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
function generateDiffPreview(oldContent, newContent) {
|
|
1332
|
+
const oldLines = oldContent.split("\n");
|
|
1333
|
+
const newLines = newContent.split("\n");
|
|
1334
|
+
const maxLines = 20;
|
|
1335
|
+
const diff = [];
|
|
1336
|
+
for (let i = 0; i < Math.min(maxLines, Math.max(oldLines.length, newLines.length)); i++) {
|
|
1337
|
+
const oldLine = oldLines[i] || "";
|
|
1338
|
+
const newLine = newLines[i] || "";
|
|
1339
|
+
if (oldLine !== newLine) {
|
|
1340
|
+
if (oldLine) {
|
|
1341
|
+
diff.push(`- ${oldLine}`);
|
|
1342
|
+
}
|
|
1343
|
+
if (newLine) {
|
|
1344
|
+
diff.push(`+ ${newLine}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (oldLines.length > maxLines || newLines.length > maxLines) {
|
|
1349
|
+
diff.push("... truncated");
|
|
1350
|
+
}
|
|
1351
|
+
return diff.slice(0, 20).join("\n");
|
|
1352
|
+
}
|
|
1353
|
+
async function findWorkspaceConfig(cwd) {
|
|
1354
|
+
const filenames = [
|
|
1355
|
+
".kb/kb.config.yaml",
|
|
1356
|
+
".kb/kb.config.yml",
|
|
1357
|
+
".kb/kb.config.json"
|
|
1358
|
+
];
|
|
1359
|
+
const gitRoot = await findGitRoot(cwd);
|
|
1360
|
+
const stopDir = gitRoot || path3.parse(cwd).root;
|
|
1361
|
+
let dir = path3.resolve(cwd);
|
|
1362
|
+
while (true) {
|
|
1363
|
+
for (const filename of filenames) {
|
|
1364
|
+
const candidate = path3.join(dir, filename);
|
|
1365
|
+
try {
|
|
1366
|
+
await promises.access(candidate);
|
|
1367
|
+
const format = filename.endsWith(".json") ? "json" : "yaml";
|
|
1368
|
+
return { path: candidate, format };
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (dir === stopDir || dir === path3.dirname(dir)) {
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
dir = path3.dirname(dir);
|
|
1376
|
+
}
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
async function initWorkspaceConfig(opts) {
|
|
1380
|
+
const cwd = path3.resolve(opts.cwd);
|
|
1381
|
+
const result = {
|
|
1382
|
+
actions: [],
|
|
1383
|
+
created: [],
|
|
1384
|
+
updated: [],
|
|
1385
|
+
skipped: [],
|
|
1386
|
+
warnings: []
|
|
1387
|
+
};
|
|
1388
|
+
if (opts.profiles && Object.keys(opts.profiles).length > 0) {
|
|
1389
|
+
result.warnings.push(
|
|
1390
|
+
"profiles option is deprecated. Define profiles[] inside kb.config.json (Profiles v2)."
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
const existing = await findWorkspaceConfig(cwd);
|
|
1394
|
+
const format = existing?.format || opts.format || "yaml";
|
|
1395
|
+
const configFileName = format === "json" ? "kb.config.json" : "kb.config.yaml";
|
|
1396
|
+
const configPath = existing?.path || path3.join(cwd, ".kb", configFileName);
|
|
1397
|
+
ensureWithinWorkspace(configPath, cwd);
|
|
1398
|
+
const newConfig = {
|
|
1399
|
+
schemaVersion: "1.0",
|
|
1400
|
+
products: {}
|
|
1401
|
+
};
|
|
1402
|
+
if (opts.products && opts.products.length > 0) {
|
|
1403
|
+
for (const product of opts.products) {
|
|
1404
|
+
toFsProduct(product);
|
|
1405
|
+
newConfig.products[product] = {};
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
if (opts.presetRef !== void 0) {
|
|
1409
|
+
newConfig.$extends = opts.presetRef;
|
|
1410
|
+
}
|
|
1411
|
+
if (existing) {
|
|
1412
|
+
try {
|
|
1413
|
+
const existingContent = await promises.readFile(existing.path, "utf-8");
|
|
1414
|
+
const existingData = format === "json" ? JSON.parse(existingContent) : YAML2.parse(existingContent);
|
|
1415
|
+
let modified = false;
|
|
1416
|
+
if (!existingData.schemaVersion) {
|
|
1417
|
+
existingData.schemaVersion = "1.0";
|
|
1418
|
+
modified = true;
|
|
1419
|
+
}
|
|
1420
|
+
if (opts.products) {
|
|
1421
|
+
if (!existingData.products) {
|
|
1422
|
+
existingData.products = {};
|
|
1423
|
+
modified = true;
|
|
1424
|
+
}
|
|
1425
|
+
for (const product of opts.products) {
|
|
1426
|
+
if (!existingData.products[product]) {
|
|
1427
|
+
existingData.products[product] = {};
|
|
1428
|
+
modified = true;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (opts.presetRef !== void 0 && existingData.$extends !== opts.presetRef) {
|
|
1433
|
+
existingData.$extends = opts.presetRef;
|
|
1434
|
+
modified = true;
|
|
1435
|
+
}
|
|
1436
|
+
if (!modified) {
|
|
1437
|
+
result.actions.push({ kind: "skip", path: configPath });
|
|
1438
|
+
result.skipped.push(configPath);
|
|
1439
|
+
return result;
|
|
1440
|
+
}
|
|
1441
|
+
const newContent = format === "json" ? JSON.stringify(existingData, null, 2) + "\n" : YAML2.stringify(existingData);
|
|
1442
|
+
if (!opts.force && existingContent !== newContent) {
|
|
1443
|
+
const action = {
|
|
1444
|
+
kind: "conflict",
|
|
1445
|
+
path: configPath,
|
|
1446
|
+
previewDiff: generateDiffPreview(existingContent, newContent)
|
|
1447
|
+
};
|
|
1448
|
+
result.actions.push(action);
|
|
1449
|
+
result.warnings.push(`Config file has changes, use --force to overwrite: ${configPath}`);
|
|
1450
|
+
return result;
|
|
1451
|
+
}
|
|
1452
|
+
if (!opts.dryRun) {
|
|
1453
|
+
await writeFileAtomic(configPath, newContent);
|
|
1454
|
+
}
|
|
1455
|
+
result.actions.push({ kind: "update", path: configPath });
|
|
1456
|
+
result.updated.push(configPath);
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
throw new KbError(
|
|
1459
|
+
"ERR_CONFIG_INVALID",
|
|
1460
|
+
`Failed to read or parse existing config: ${existing.path}`,
|
|
1461
|
+
ERROR_HINTS.ERR_CONFIG_INVALID,
|
|
1462
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
} else {
|
|
1466
|
+
const content = format === "json" ? JSON.stringify(newConfig, null, 2) + "\n" : YAML2.stringify(newConfig);
|
|
1467
|
+
if (!opts.dryRun) {
|
|
1468
|
+
await writeFileAtomic(configPath, content);
|
|
1469
|
+
}
|
|
1470
|
+
result.actions.push({ kind: "write", path: configPath });
|
|
1471
|
+
result.created.push(configPath);
|
|
1472
|
+
}
|
|
1473
|
+
return result;
|
|
1474
|
+
}
|
|
1475
|
+
async function upsertLockfile(opts) {
|
|
1476
|
+
const cwd = path3.resolve(opts.cwd);
|
|
1477
|
+
const lockfilePath = path3.join(cwd, ".kb", "lock.json");
|
|
1478
|
+
const oldLockfilePath = path3.join(cwd, ".kb", "lockfile.json");
|
|
1479
|
+
const result = {
|
|
1480
|
+
actions: [],
|
|
1481
|
+
created: [],
|
|
1482
|
+
updated: [],
|
|
1483
|
+
skipped: [],
|
|
1484
|
+
warnings: []
|
|
1485
|
+
};
|
|
1486
|
+
ensureWithinWorkspace(lockfilePath, cwd);
|
|
1487
|
+
try {
|
|
1488
|
+
await promises.access(oldLockfilePath);
|
|
1489
|
+
result.warnings.push(
|
|
1490
|
+
"Deprecated lockfile found at .kb/lockfile.json, please migrate to .kb/lock.json"
|
|
1491
|
+
);
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1494
|
+
const existing = await readLockfile(cwd);
|
|
1495
|
+
const lockfileData = {
|
|
1496
|
+
$schema: "https://schemas.kb-labs.dev/lockfile.schema.json",
|
|
1497
|
+
schemaVersion: "1.0",
|
|
1498
|
+
orgPreset: opts.presetRef !== void 0 ? opts.presetRef || void 0 : existing?.orgPreset,
|
|
1499
|
+
profile: opts.profileRef !== void 0 ? opts.profileRef || void 0 : existing?.profile,
|
|
1500
|
+
policyBundle: opts.policyBundle !== void 0 ? opts.policyBundle || void 0 : existing?.policyBundle,
|
|
1501
|
+
hashes: existing?.hashes || {},
|
|
1502
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1503
|
+
};
|
|
1504
|
+
if (!existing) {
|
|
1505
|
+
if (!opts.dryRun) {
|
|
1506
|
+
await writeLockfile(cwd, lockfileData);
|
|
1507
|
+
}
|
|
1508
|
+
result.actions.push({ kind: "write", path: lockfilePath });
|
|
1509
|
+
result.created.push(lockfilePath);
|
|
1510
|
+
} else {
|
|
1511
|
+
if (!opts.dryRun) {
|
|
1512
|
+
await writeLockfile(cwd, lockfileData);
|
|
1513
|
+
}
|
|
1514
|
+
result.actions.push({ kind: "update", path: lockfilePath });
|
|
1515
|
+
result.updated.push(lockfilePath);
|
|
1516
|
+
}
|
|
1517
|
+
return result;
|
|
1518
|
+
}
|
|
1519
|
+
var ajv = new Ajv({ allErrors: true, strict: false });
|
|
1520
|
+
addFormats(ajv);
|
|
1521
|
+
var schemaMap = {};
|
|
1522
|
+
function validateProductConfig(product, config) {
|
|
1523
|
+
const schema = schemaMap[product];
|
|
1524
|
+
if (!schema) {
|
|
1525
|
+
return { ok: true, errors: null };
|
|
1526
|
+
}
|
|
1527
|
+
const validate = ajv.compile(schema);
|
|
1528
|
+
const ok = validate(config);
|
|
1529
|
+
return { ok: !!ok, errors: validate.errors || null };
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
export { BundleProfileSchema, BundleProfileSourceSchema, FSCache, ProfileMetaSchema, ProfileTraceSchema, ProfileV2Schema, ProfilesV2Schema, ResolvedScopeSchema, SYSTEM_DEFAULTS, ScopeSelectionSchema, ScopeV2Schema, clearCaches, computeConfigHash, computeConfigsHash, explainProductConfig, findGitRoot, findKbConfig, findNearestConfig, fsCache, getAllProductIds, getConfigHash, getKbRootDir, getLockfileConfigHash, getLockfilePath, getLockfileStats, getPresetConfigForProduct, getPresetMetadata, getProductConfig, getProductConfigDir, getProductConfigPath, getProfilesRootDir, initWorkspaceConfig, isLockfileUpToDate, isValidProductId, layeredMergeWithTrace, mergeDefined, pickDefined, readConfigFile, readJsonWithDiagnostics, readKbConfig, readLockfile, readProfilesSection, readWorkspaceConfig, resolveConfig, resolvePreset, resolveProfile, selectProfileScope, toBool, toConfigProduct, toFsProduct, toInt, updateLockfile, upsertLockfile, validateLockfile, validateProductConfig, writeLockfile };
|
|
1533
|
+
//# sourceMappingURL=index.js.map
|
|
30
1534
|
//# sourceMappingURL=index.js.map
|