wyvrnpm 2.10.2 → 2.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1914 -1860
  2. package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
  3. package/cmake/cpp.cmake +9 -9
  4. package/cmake/functions.cmake +224 -224
  5. package/cmake/macros.cmake +284 -284
  6. package/package.json +3 -2
  7. package/src/auth.js +66 -66
  8. package/src/binary-dir.js +95 -0
  9. package/src/bootstrap/cookbook.js +196 -196
  10. package/src/bootstrap/detect.js +150 -150
  11. package/src/bootstrap/index.js +220 -220
  12. package/src/bootstrap/version.js +72 -72
  13. package/src/build/cache.js +344 -344
  14. package/src/build/clone.js +170 -170
  15. package/src/build/cmake.js +342 -342
  16. package/src/build/index.js +299 -297
  17. package/src/build/msvc-env.js +260 -260
  18. package/src/build/recipe.js +188 -188
  19. package/src/commands/add.js +141 -141
  20. package/src/commands/bootstrap.js +96 -96
  21. package/src/commands/build.js +482 -452
  22. package/src/commands/cache.js +189 -189
  23. package/src/commands/clean.js +80 -80
  24. package/src/commands/configure.js +92 -92
  25. package/src/commands/init.js +70 -70
  26. package/src/commands/install-skill.js +115 -115
  27. package/src/commands/install.js +730 -674
  28. package/src/commands/link.js +320 -320
  29. package/src/commands/profile.js +237 -237
  30. package/src/commands/publish.js +584 -555
  31. package/src/commands/show.js +252 -252
  32. package/src/commands/version.js +187 -0
  33. package/src/compat.js +273 -273
  34. package/src/conf/index.js +415 -391
  35. package/src/conf/namespaces.js +94 -94
  36. package/src/context.js +230 -230
  37. package/src/http-fetch.js +53 -53
  38. package/src/ignore.js +118 -118
  39. package/src/logger.js +122 -122
  40. package/src/options.js +303 -303
  41. package/src/settings-overrides.js +152 -152
  42. package/src/toolchain/deps.js +164 -164
  43. package/src/toolchain/index.js +212 -212
  44. package/src/toolchain/presets.js +356 -340
  45. package/src/toolchain/template.cmake +77 -77
  46. package/src/upload-built.js +265 -265
  47. package/src/version-range.js +301 -301
  48. package/src/zip-safe.js +52 -52
  49. package/src/zip-stream.js +126 -0
@@ -1,340 +1,356 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const VENDOR_KEY = 'wyvrnpm/generated';
7
-
8
- // Local copy of the arch→VS-platform mapping. Duplicated here rather than
9
- // imported from ./index so preset generation can run standalone (keeps
10
- // the dependency graph simple presets.js doesn't otherwise need the
11
- // toolchain generator). Keep in sync with `archToVsPlatform` in index.js.
12
- const ARCH_VS_PLATFORM = {
13
- x86_64: 'x64',
14
- x86: 'Win32',
15
- armv8: 'ARM64',
16
- armv7: 'ARM',
17
- };
18
-
19
- function isVsGenerator(name) {
20
- return typeof name === 'string' && /^Visual Studio \d/.test(name);
21
- }
22
- const SCHEMA_VERSION = 3; // requires CMake 3.21
23
-
24
- /**
25
- * Safely parse a JSON file; returns null on any error (missing, malformed, …).
26
- * @param {string} p
27
- * @returns {object|null}
28
- */
29
- function safeReadJson(p) {
30
- try {
31
- return JSON.parse(fs.readFileSync(p, 'utf8'));
32
- } catch {
33
- return null;
34
- }
35
- }
36
-
37
- /**
38
- * True if the parsed presets object was produced by wyvrnpm (i.e. carries our
39
- * vendor marker). Hand-edited or user-owned files return false.
40
- *
41
- * @param {object|null} presets
42
- * @returns {boolean}
43
- */
44
- function isWyvrnGenerated(presets) {
45
- return !!presets && typeof presets.vendor === 'object' && !!presets.vendor[VENDOR_KEY];
46
- }
47
-
48
- /**
49
- * Build the configure preset object for the active profile.
50
- *
51
- * When the project has its own `build` recipe (i.e. it's a publishable
52
- * library with `build.generator` / `build.configure` declared), those are
53
- * propagated into the generated preset so `wyvrnpm build` locally honours
54
- * the same knobs the source-build + publish paths use. Without this, a
55
- * publisher's recipe is effectively advisory at install-time (F15 fix).
56
- *
57
- * @param {object} args
58
- * @param {string} args.profileName
59
- * @param {string} args.profileHash
60
- * @param {string} args.toolchainRelPath Relative from project root, CMake-style
61
- * forward slashes.
62
- * @param {string|null} [args.generator] Single generator name to pin on the
63
- * preset (e.g. "Ninja Multi-Config").
64
- * Null / undefined let CMake pick.
65
- * @param {Record<string,string>|null} [args.extraCacheVariables]
66
- * Merged into cacheVariables alongside
67
- * `CMAKE_TOOLCHAIN_FILE`. Usually
68
- * derived from the project's own
69
- * `build.configure` -D args.
70
- * @param {string|null} [args.profileArch] Active profile arch — used to pin
71
- * `architecture` for VS generators
72
- * so `cmake --preset` produces
73
- * binaries matching the profile
74
- * (VS otherwise defaults to x64).
75
- * @param {string[]|null} [args.configs] Build recipe's `configs` list
76
- * (e.g. ['Debug','Release','MinSizeRel']).
77
- * When provided, drives
78
- * `CMAKE_CONFIGURATION_TYPES` so
79
- * multi-config generators (Ninja
80
- * Multi-Config, VS) actually
81
- * produce every config the recipe
82
- * declares. Null/empty → omitted
83
- * (let CMake use its generator
84
- * default). `extraCacheVariables`
85
- * wins on explicit collision so
86
- * authors that already bake the
87
- * list into `build.configure`
88
- * aren't overridden.
89
- */
90
- function buildConfigurePreset({
91
- profileName,
92
- profileHash,
93
- toolchainRelPath,
94
- generator = null,
95
- extraCacheVariables = null,
96
- installDir = null,
97
- profileArch = null,
98
- configs = null,
99
- }) {
100
- const binaryDir = `\${sourceDir}/build/wyvrn-${profileName}`;
101
- // CMAKE_CONFIGURATION_TYPES is a defensible default from the recipe's
102
- // declared `configs` list. Applied FIRST (lowest precedence) so an
103
- // explicit `-DCMAKE_CONFIGURATION_TYPES=...` in `build.configure`
104
- // still wins — authors who already pin the list shouldn't silently
105
- // lose their override. The wyvrn-owned entries (toolchain, install
106
- // prefix) still win over both below.
107
- const configsDefault = (Array.isArray(configs) && configs.length > 0)
108
- ? { CMAKE_CONFIGURATION_TYPES: configs.join(';') }
109
- : {};
110
- const cacheVariables = {
111
- ...configsDefault,
112
- ...(extraCacheVariables ?? {}),
113
- CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
114
- };
115
- if (installDir) {
116
- // `installDir` in wyvrn.json is defined as relative to the binary dir
117
- // (same semantics as the source-build path, which passes
118
- // -DCMAKE_INSTALL_PREFIX=<buildDir>/<installDir>). Without this, a
119
- // `wyvrnpm build --install` falls back to CMake's system default —
120
- // on Windows that's `C:/Program Files (x86)/<Project>`, which needs
121
- // admin rights and almost always fails.
122
- cacheVariables.CMAKE_INSTALL_PREFIX = `${binaryDir}/${installDir}`;
123
- }
124
- const preset = {
125
- name: `wyvrn-${profileName}`,
126
- displayName: `wyvrnpm (${profileName})`,
127
- description: `Generated by wyvrnpm install — profile hash ${profileHash}`,
128
- binaryDir,
129
- cacheVariables,
130
- };
131
- if (generator) preset.generator = generator;
132
-
133
- // Visual Studio generators default to x64 when `architecture` is
134
- // unspecified which silently overrides the profile arch and makes
135
- // find_package() reject 32-bit installed packages with a pointer-size
136
- // mismatch. Pin `architecture` from the profile so `cmake --preset`
137
- // produces the right artefacts end-to-end.
138
- if (isVsGenerator(generator) && profileArch && ARCH_VS_PLATFORM[profileArch]) {
139
- preset.architecture = {
140
- value: ARCH_VS_PLATFORM[profileArch],
141
- strategy: 'set',
142
- };
143
- }
144
-
145
- return preset;
146
- }
147
-
148
- /**
149
- * One build preset per configuration the recipe declares (or the
150
- * legacy default pair `Debug` + `Release` when no list is provided).
151
- *
152
- * The preset name suffix is the configuration name lower-cased — matches
153
- * the previous Debug/Release naming for those two, and extends to
154
- * `wyvrn-<profile>-relwithdebinfo` / `-minsizerel` for the multi-config
155
- * cases. Authors whose recipes include `MinSizeRel` in `build.configs`
156
- * can now `cmake --build --preset wyvrn-<profile>-minsizerel` directly.
157
- *
158
- * @param {string} profileName
159
- * @param {string[]|null} [configs] Recipe's declared configurations. When
160
- * null/empty, emits the pre-2.8.1 `Debug` + `Release` pair so existing
161
- * CMakePresets.json files remain byte-identical.
162
- * @returns {Array<object>}
163
- */
164
- function buildBuildPresets(profileName, configs = null) {
165
- const base = `wyvrn-${profileName}`;
166
- const list = (Array.isArray(configs) && configs.length > 0)
167
- ? configs
168
- : ['Debug', 'Release'];
169
- return list.map((cfg) => ({
170
- name: `${base}-${cfg.toLowerCase()}`,
171
- displayName: `wyvrn ${profileName} (${cfg})`,
172
- configurePreset: base,
173
- configuration: cfg,
174
- }));
175
- }
176
-
177
- /**
178
- * Build a fresh presets file from scratch (no prior content to merge).
179
- */
180
- function buildFreshPresetsFile({
181
- profileName,
182
- profileHash,
183
- toolchainRelPath,
184
- generator = null,
185
- extraCacheVariables = null,
186
- installDir = null,
187
- profileArch = null,
188
- configs = null,
189
- }) {
190
- return {
191
- version: SCHEMA_VERSION,
192
- vendor: {
193
- [VENDOR_KEY]: {
194
- profileName,
195
- profileHash,
196
- generatedAt: new Date().toISOString(),
197
- },
198
- },
199
- configurePresets: [buildConfigurePreset({
200
- profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs,
201
- })],
202
- buildPresets: buildBuildPresets(profileName, configs),
203
- };
204
- }
205
-
206
- /**
207
- * Merge the current profile's presets into an existing wyvrnpm-generated
208
- * file. Presets with matching `name` are replaced; others are preserved so
209
- * `wyvrnpm install --profile release` followed by `wyvrnpm install --profile
210
- * debug` leaves both profiles available in the file.
211
- *
212
- * @param {object} existing
213
- * @param {object} args forwarded to the preset builders
214
- * @returns {object}
215
- */
216
- function mergeIntoExisting(existing, {
217
- profileName,
218
- profileHash,
219
- toolchainRelPath,
220
- generator = null,
221
- extraCacheVariables = null,
222
- installDir = null,
223
- profileArch = null,
224
- configs = null,
225
- }) {
226
- const newConfigure = buildConfigurePreset({
227
- profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs,
228
- });
229
- const newBuilds = buildBuildPresets(profileName, configs);
230
- const newConfigureNames = new Set([newConfigure.name]);
231
- // When replacing THIS profile's build presets, drop every pre-existing
232
- // one that targets the same configurePreset — the fresh `newBuilds`
233
- // list fully replaces them. Scoping to configurePreset avoids stepping
234
- // on presets for OTHER profiles in the same file, which is the whole
235
- // point of the merge path.
236
- //
237
- // Without this, editing `build.configs` from 4 configs down to 2 would
238
- // leave orphan build presets pointing at configurations CMake won't
239
- // generate.
240
- const myBase = `wyvrn-${profileName}`;
241
- const isMyBuildPreset = (p) => p && p.configurePreset === myBase;
242
-
243
- const configurePresets = (existing.configurePresets ?? []).filter((p) => !newConfigureNames.has(p.name));
244
- configurePresets.push(newConfigure);
245
-
246
- // Drop every pre-existing build preset that targets THIS profile's
247
- // configurePreset — the fresh `newBuilds` list (one per current
248
- // `configs` entry) fully replaces them. Presets for OTHER profiles
249
- // stay untouched so the multi-profile merge story is preserved.
250
- const buildPresets = (existing.buildPresets ?? []).filter((p) => !isMyBuildPreset(p));
251
- buildPresets.push(...newBuilds);
252
-
253
- return {
254
- ...existing,
255
- version: existing.version ?? SCHEMA_VERSION,
256
- vendor: {
257
- ...(existing.vendor ?? {}),
258
- [VENDOR_KEY]: {
259
- profileName,
260
- profileHash,
261
- generatedAt: new Date().toISOString(),
262
- },
263
- },
264
- configurePresets,
265
- buildPresets,
266
- };
267
- }
268
-
269
- /**
270
- * Write a CMakePresets.json (or CMakeUserPresets.json, as fallback) under
271
- * `projectRoot`. Never overwrites a user-owned file.
272
- *
273
- * Resolution order:
274
- * 1. CMakePresets.json
275
- * - Does not exist → create it.
276
- * - Exists, wyvrn-gen → merge into it.
277
- * - Exists, user-owned → leave alone, fall through.
278
- * 2. CMakeUserPresets.json
279
- * - Does not exist → create it.
280
- * - Exists, wyvrn-gen → merge into it.
281
- * - Exists, user-owned → skip (action: "skipped").
282
- *
283
- * @param {object} args
284
- * @param {string} args.projectRoot Directory containing top-level CMakeLists.txt.
285
- * @param {string} args.profileName
286
- * @param {string} args.profileHash
287
- * @param {string} args.toolchainRelPath CMake-style forward slashes, relative to projectRoot.
288
- * @param {string|null} [args.generator] Propagated onto the configure preset's `generator` field.
289
- * @param {Record<string,string>|null} [args.extraCacheVariables]
290
- * Merged into the configure preset's `cacheVariables`.
291
- * @returns {{ path: string|null, action: 'created'|'updated'|'skipped', isUser: boolean }}
292
- */
293
- function generateCMakePresets({
294
- projectRoot,
295
- profileName,
296
- profileHash,
297
- toolchainRelPath,
298
- generator = null,
299
- extraCacheVariables = null,
300
- installDir = null,
301
- profileArch = null,
302
- configs = null,
303
- }) {
304
- const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs };
305
- const candidates = [
306
- { filePath: path.join(projectRoot, 'CMakePresets.json'), isUser: false },
307
- { filePath: path.join(projectRoot, 'CMakeUserPresets.json'), isUser: true },
308
- ];
309
-
310
- for (const { filePath, isUser } of candidates) {
311
- if (!fs.existsSync(filePath)) {
312
- const content = buildFreshPresetsFile(args);
313
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf8');
314
- return { path: filePath, action: 'created', isUser };
315
- }
316
-
317
- const existing = safeReadJson(filePath);
318
- if (isWyvrnGenerated(existing)) {
319
- const merged = mergeIntoExisting(existing, args);
320
- fs.writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
321
- return { path: filePath, action: 'updated', isUser };
322
- }
323
-
324
- // User-owned — do not touch, try next candidate.
325
- }
326
-
327
- return { path: null, action: 'skipped', isUser: false };
328
- }
329
-
330
- module.exports = {
331
- generateCMakePresets,
332
- // Exported for tests
333
- isWyvrnGenerated,
334
- buildConfigurePreset,
335
- buildBuildPresets,
336
- buildFreshPresetsFile,
337
- mergeIntoExisting,
338
- VENDOR_KEY,
339
- SCHEMA_VERSION,
340
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { DEFAULT_BINARY_DIR_ROOT } = require('../binary-dir');
7
+
8
+ const VENDOR_KEY = 'wyvrnpm/generated';
9
+
10
+ // Local copy of the arch→VS-platform mapping. Duplicated here rather than
11
+ // imported from ./index so preset generation can run standalone (keeps
12
+ // the dependency graph simple — presets.js doesn't otherwise need the
13
+ // toolchain generator). Keep in sync with `archToVsPlatform` in index.js.
14
+ const ARCH_VS_PLATFORM = {
15
+ x86_64: 'x64',
16
+ x86: 'Win32',
17
+ armv8: 'ARM64',
18
+ armv7: 'ARM',
19
+ };
20
+
21
+ function isVsGenerator(name) {
22
+ return typeof name === 'string' && /^Visual Studio \d/.test(name);
23
+ }
24
+ const SCHEMA_VERSION = 3; // requires CMake ≥ 3.21
25
+
26
+ /**
27
+ * Safely parse a JSON file; returns null on any error (missing, malformed, …).
28
+ * @param {string} p
29
+ * @returns {object|null}
30
+ */
31
+ function safeReadJson(p) {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * True if the parsed presets object was produced by wyvrnpm (i.e. carries our
41
+ * vendor marker). Hand-edited or user-owned files return false.
42
+ *
43
+ * @param {object|null} presets
44
+ * @returns {boolean}
45
+ */
46
+ function isWyvrnGenerated(presets) {
47
+ return !!presets && typeof presets.vendor === 'object' && !!presets.vendor[VENDOR_KEY];
48
+ }
49
+
50
+ /**
51
+ * Build the configure preset object for the active profile.
52
+ *
53
+ * When the project has its own `build` recipe (i.e. it's a publishable
54
+ * library with `build.generator` / `build.configure` declared), those are
55
+ * propagated into the generated preset so `wyvrnpm build` locally honours
56
+ * the same knobs the source-build + publish paths use. Without this, a
57
+ * publisher's recipe is effectively advisory at install-time (F15 fix).
58
+ *
59
+ * @param {object} args
60
+ * @param {string} args.profileName
61
+ * @param {string} args.profileHash
62
+ * @param {string} args.toolchainRelPath Relative from project root, CMake-style
63
+ * forward slashes.
64
+ * @param {string|null} [args.generator] Single generator name to pin on the
65
+ * preset (e.g. "Ninja Multi-Config").
66
+ * Null / undefined → let CMake pick.
67
+ * @param {Record<string,string>|null} [args.extraCacheVariables]
68
+ * Merged into cacheVariables alongside
69
+ * `CMAKE_TOOLCHAIN_FILE`. Usually
70
+ * derived from the project's own
71
+ * `build.configure` -D args.
72
+ * @param {string|null} [args.profileArch] Active profile arch — used to pin
73
+ * `architecture` for VS generators
74
+ * so `cmake --preset` produces
75
+ * binaries matching the profile
76
+ * (VS otherwise defaults to x64).
77
+ * @param {string[]|null} [args.configs] Build recipe's `configs` list
78
+ * (e.g. ['Debug','Release','MinSizeRel']).
79
+ * When provided, drives
80
+ * `CMAKE_CONFIGURATION_TYPES` so
81
+ * multi-config generators (Ninja
82
+ * Multi-Config, VS) actually
83
+ * produce every config the recipe
84
+ * declares. Null/empty → omitted
85
+ * (let CMake use its generator
86
+ * default). `extraCacheVariables`
87
+ * wins on explicit collision so
88
+ * authors that already bake the
89
+ * list into `build.configure`
90
+ * aren't overridden.
91
+ * @param {string|null} [args.binaryDirRoot] Project-relative subdir for the
92
+ * preset's binaryDir. Defaults to
93
+ * `"build"`. Override via
94
+ * `--build-dir` or
95
+ * `wyvrn.local.json:binaryDirRoot`
96
+ * when the repo has a `BUILD`
97
+ * Bazel/Buck file at root that
98
+ * collides with `build/` on
99
+ * case-insensitive filesystems.
100
+ */
101
+ function buildConfigurePreset({
102
+ profileName,
103
+ profileHash,
104
+ toolchainRelPath,
105
+ generator = null,
106
+ extraCacheVariables = null,
107
+ installDir = null,
108
+ profileArch = null,
109
+ configs = null,
110
+ binaryDirRoot = DEFAULT_BINARY_DIR_ROOT,
111
+ }) {
112
+ const root = binaryDirRoot || DEFAULT_BINARY_DIR_ROOT;
113
+ const binaryDir = `\${sourceDir}/${root}/wyvrn-${profileName}`;
114
+ // CMAKE_CONFIGURATION_TYPES is a defensible default from the recipe's
115
+ // declared `configs` list. Applied FIRST (lowest precedence) so an
116
+ // explicit `-DCMAKE_CONFIGURATION_TYPES=...` in `build.configure`
117
+ // still wins authors who already pin the list shouldn't silently
118
+ // lose their override. The wyvrn-owned entries (toolchain, install
119
+ // prefix) still win over both below.
120
+ const configsDefault = (Array.isArray(configs) && configs.length > 0)
121
+ ? { CMAKE_CONFIGURATION_TYPES: configs.join(';') }
122
+ : {};
123
+ const cacheVariables = {
124
+ ...configsDefault,
125
+ ...(extraCacheVariables ?? {}),
126
+ CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
127
+ };
128
+ if (installDir) {
129
+ // `installDir` in wyvrn.json is defined as relative to the binary dir
130
+ // (same semantics as the source-build path, which passes
131
+ // -DCMAKE_INSTALL_PREFIX=<buildDir>/<installDir>). Without this, a
132
+ // `wyvrnpm build --install` falls back to CMake's system default —
133
+ // on Windows that's `C:/Program Files (x86)/<Project>`, which needs
134
+ // admin rights and almost always fails.
135
+ cacheVariables.CMAKE_INSTALL_PREFIX = `${binaryDir}/${installDir}`;
136
+ }
137
+ const preset = {
138
+ name: `wyvrn-${profileName}`,
139
+ displayName: `wyvrnpm (${profileName})`,
140
+ description: `Generated by wyvrnpm install — profile hash ${profileHash}`,
141
+ binaryDir,
142
+ cacheVariables,
143
+ };
144
+ if (generator) preset.generator = generator;
145
+
146
+ // Visual Studio generators default to x64 when `architecture` is
147
+ // unspecified — which silently overrides the profile arch and makes
148
+ // find_package() reject 32-bit installed packages with a pointer-size
149
+ // mismatch. Pin `architecture` from the profile so `cmake --preset`
150
+ // produces the right artefacts end-to-end.
151
+ if (isVsGenerator(generator) && profileArch && ARCH_VS_PLATFORM[profileArch]) {
152
+ preset.architecture = {
153
+ value: ARCH_VS_PLATFORM[profileArch],
154
+ strategy: 'set',
155
+ };
156
+ }
157
+
158
+ return preset;
159
+ }
160
+
161
+ /**
162
+ * One build preset per configuration the recipe declares (or the
163
+ * legacy default pair `Debug` + `Release` when no list is provided).
164
+ *
165
+ * The preset name suffix is the configuration name lower-cased — matches
166
+ * the previous Debug/Release naming for those two, and extends to
167
+ * `wyvrn-<profile>-relwithdebinfo` / `-minsizerel` for the multi-config
168
+ * cases. Authors whose recipes include `MinSizeRel` in `build.configs`
169
+ * can now `cmake --build --preset wyvrn-<profile>-minsizerel` directly.
170
+ *
171
+ * @param {string} profileName
172
+ * @param {string[]|null} [configs] Recipe's declared configurations. When
173
+ * null/empty, emits the pre-2.8.1 `Debug` + `Release` pair so existing
174
+ * CMakePresets.json files remain byte-identical.
175
+ * @returns {Array<object>}
176
+ */
177
+ function buildBuildPresets(profileName, configs = null) {
178
+ const base = `wyvrn-${profileName}`;
179
+ const list = (Array.isArray(configs) && configs.length > 0)
180
+ ? configs
181
+ : ['Debug', 'Release'];
182
+ return list.map((cfg) => ({
183
+ name: `${base}-${cfg.toLowerCase()}`,
184
+ displayName: `wyvrn ${profileName} (${cfg})`,
185
+ configurePreset: base,
186
+ configuration: cfg,
187
+ }));
188
+ }
189
+
190
+ /**
191
+ * Build a fresh presets file from scratch (no prior content to merge).
192
+ */
193
+ function buildFreshPresetsFile({
194
+ profileName,
195
+ profileHash,
196
+ toolchainRelPath,
197
+ generator = null,
198
+ extraCacheVariables = null,
199
+ installDir = null,
200
+ profileArch = null,
201
+ configs = null,
202
+ binaryDirRoot = DEFAULT_BINARY_DIR_ROOT,
203
+ }) {
204
+ return {
205
+ version: SCHEMA_VERSION,
206
+ vendor: {
207
+ [VENDOR_KEY]: {
208
+ profileName,
209
+ profileHash,
210
+ generatedAt: new Date().toISOString(),
211
+ },
212
+ },
213
+ configurePresets: [buildConfigurePreset({
214
+ profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs, binaryDirRoot,
215
+ })],
216
+ buildPresets: buildBuildPresets(profileName, configs),
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Merge the current profile's presets into an existing wyvrnpm-generated
222
+ * file. Presets with matching `name` are replaced; others are preserved so
223
+ * `wyvrnpm install --profile release` followed by `wyvrnpm install --profile
224
+ * debug` leaves both profiles available in the file.
225
+ *
226
+ * @param {object} existing
227
+ * @param {object} args forwarded to the preset builders
228
+ * @returns {object}
229
+ */
230
+ function mergeIntoExisting(existing, {
231
+ profileName,
232
+ profileHash,
233
+ toolchainRelPath,
234
+ generator = null,
235
+ extraCacheVariables = null,
236
+ installDir = null,
237
+ profileArch = null,
238
+ configs = null,
239
+ binaryDirRoot = DEFAULT_BINARY_DIR_ROOT,
240
+ }) {
241
+ const newConfigure = buildConfigurePreset({
242
+ profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs, binaryDirRoot,
243
+ });
244
+ const newBuilds = buildBuildPresets(profileName, configs);
245
+ const newConfigureNames = new Set([newConfigure.name]);
246
+ // When replacing THIS profile's build presets, drop every pre-existing
247
+ // one that targets the same configurePreset — the fresh `newBuilds`
248
+ // list fully replaces them. Scoping to configurePreset avoids stepping
249
+ // on presets for OTHER profiles in the same file, which is the whole
250
+ // point of the merge path.
251
+ //
252
+ // Without this, editing `build.configs` from 4 configs down to 2 would
253
+ // leave orphan build presets pointing at configurations CMake won't
254
+ // generate.
255
+ const myBase = `wyvrn-${profileName}`;
256
+ const isMyBuildPreset = (p) => p && p.configurePreset === myBase;
257
+
258
+ const configurePresets = (existing.configurePresets ?? []).filter((p) => !newConfigureNames.has(p.name));
259
+ configurePresets.push(newConfigure);
260
+
261
+ // Drop every pre-existing build preset that targets THIS profile's
262
+ // configurePreset — the fresh `newBuilds` list (one per current
263
+ // `configs` entry) fully replaces them. Presets for OTHER profiles
264
+ // stay untouched so the multi-profile merge story is preserved.
265
+ const buildPresets = (existing.buildPresets ?? []).filter((p) => !isMyBuildPreset(p));
266
+ buildPresets.push(...newBuilds);
267
+
268
+ return {
269
+ ...existing,
270
+ version: existing.version ?? SCHEMA_VERSION,
271
+ vendor: {
272
+ ...(existing.vendor ?? {}),
273
+ [VENDOR_KEY]: {
274
+ profileName,
275
+ profileHash,
276
+ generatedAt: new Date().toISOString(),
277
+ },
278
+ },
279
+ configurePresets,
280
+ buildPresets,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Write a CMakePresets.json (or CMakeUserPresets.json, as fallback) under
286
+ * `projectRoot`. Never overwrites a user-owned file.
287
+ *
288
+ * Resolution order:
289
+ * 1. CMakePresets.json
290
+ * - Does not exist → create it.
291
+ * - Exists, wyvrn-gen → merge into it.
292
+ * - Exists, user-owned → leave alone, fall through.
293
+ * 2. CMakeUserPresets.json
294
+ * - Does not exist → create it.
295
+ * - Exists, wyvrn-gen → merge into it.
296
+ * - Exists, user-owned → skip (action: "skipped").
297
+ *
298
+ * @param {object} args
299
+ * @param {string} args.projectRoot Directory containing top-level CMakeLists.txt.
300
+ * @param {string} args.profileName
301
+ * @param {string} args.profileHash
302
+ * @param {string} args.toolchainRelPath CMake-style forward slashes, relative to projectRoot.
303
+ * @param {string|null} [args.generator] Propagated onto the configure preset's `generator` field.
304
+ * @param {Record<string,string>|null} [args.extraCacheVariables]
305
+ * Merged into the configure preset's `cacheVariables`.
306
+ * @returns {{ path: string|null, action: 'created'|'updated'|'skipped', isUser: boolean }}
307
+ */
308
+ function generateCMakePresets({
309
+ projectRoot,
310
+ profileName,
311
+ profileHash,
312
+ toolchainRelPath,
313
+ generator = null,
314
+ extraCacheVariables = null,
315
+ installDir = null,
316
+ profileArch = null,
317
+ configs = null,
318
+ binaryDirRoot = DEFAULT_BINARY_DIR_ROOT,
319
+ }) {
320
+ const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs, binaryDirRoot };
321
+ const candidates = [
322
+ { filePath: path.join(projectRoot, 'CMakePresets.json'), isUser: false },
323
+ { filePath: path.join(projectRoot, 'CMakeUserPresets.json'), isUser: true },
324
+ ];
325
+
326
+ for (const { filePath, isUser } of candidates) {
327
+ if (!fs.existsSync(filePath)) {
328
+ const content = buildFreshPresetsFile(args);
329
+ fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf8');
330
+ return { path: filePath, action: 'created', isUser };
331
+ }
332
+
333
+ const existing = safeReadJson(filePath);
334
+ if (isWyvrnGenerated(existing)) {
335
+ const merged = mergeIntoExisting(existing, args);
336
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
337
+ return { path: filePath, action: 'updated', isUser };
338
+ }
339
+
340
+ // User-owned — do not touch, try next candidate.
341
+ }
342
+
343
+ return { path: null, action: 'skipped', isUser: false };
344
+ }
345
+
346
+ module.exports = {
347
+ generateCMakePresets,
348
+ // Exported for tests
349
+ isWyvrnGenerated,
350
+ buildConfigurePreset,
351
+ buildBuildPresets,
352
+ buildFreshPresetsFile,
353
+ mergeIntoExisting,
354
+ VENDOR_KEY,
355
+ SCHEMA_VERSION,
356
+ };