wyvrnpm 1.2.1 → 2.0.1

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.
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Creates a symlink/junction from linkPath pointing to targetPath.
8
+ * Uses junctions on Windows (no admin rights required), symlinks on Unix.
9
+ *
10
+ * @param {string} targetPath - The actual directory to link to.
11
+ * @param {string} linkPath - The path where the link will be created.
12
+ */
13
+ function createLink(targetPath, linkPath) {
14
+ const absoluteTarget = path.resolve(targetPath);
15
+ const absoluteLink = path.resolve(linkPath);
16
+
17
+ // Ensure parent directory exists
18
+ const parentDir = path.dirname(absoluteLink);
19
+ if (!fs.existsSync(parentDir)) {
20
+ fs.mkdirSync(parentDir, { recursive: true });
21
+ }
22
+
23
+ // Remove existing link/directory if present
24
+ if (fs.existsSync(absoluteLink)) {
25
+ const stats = fs.lstatSync(absoluteLink);
26
+ if (stats.isSymbolicLink()) {
27
+ fs.unlinkSync(absoluteLink);
28
+ } else {
29
+ fs.rmSync(absoluteLink, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ // Use 'junction' on Windows (no admin required), 'dir' on Unix
34
+ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
35
+ fs.symlinkSync(absoluteTarget, absoluteLink, linkType);
36
+ }
37
+
38
+ /**
39
+ * Checks if a path is a symlink or junction.
40
+ *
41
+ * @param {string} linkPath - Path to check.
42
+ * @returns {boolean} True if it's a symlink/junction.
43
+ */
44
+ function isLink(linkPath) {
45
+ try {
46
+ const stats = fs.lstatSync(linkPath);
47
+ return stats.isSymbolicLink();
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Removes a symlink/junction without affecting the target directory.
55
+ *
56
+ * @param {string} linkPath - Path to the symlink/junction to remove.
57
+ * @returns {boolean} True if removed, false if it didn't exist or wasn't a link.
58
+ */
59
+ function removeLink(linkPath) {
60
+ try {
61
+ const stats = fs.lstatSync(linkPath);
62
+ if (stats.isSymbolicLink()) {
63
+ fs.unlinkSync(linkPath);
64
+ return true;
65
+ }
66
+ return false;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Gets the target path of a symlink/junction.
74
+ *
75
+ * @param {string} linkPath - Path to the symlink/junction.
76
+ * @returns {string|null} The target path, or null if not a link.
77
+ */
78
+ function getLinkTarget(linkPath) {
79
+ try {
80
+ const stats = fs.lstatSync(linkPath);
81
+ if (stats.isSymbolicLink()) {
82
+ return fs.readlinkSync(linkPath);
83
+ }
84
+ return null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ module.exports = {
91
+ createLink,
92
+ isLink,
93
+ removeLink,
94
+ getLinkTarget,
95
+ };
package/src/manifest.js CHANGED
@@ -1,77 +1,133 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- /**
7
- * Valid project kind values, matching the C# serialisation enum.
8
- * @type {string[]}
9
- */
10
- const VALID_KINDS = [
11
- 'ConsoleApp',
12
- 'StaticLib',
13
- 'DynamicLib',
14
- 'HeaderOnlyLib',
15
- 'WebApp',
16
- 'Utility',
17
- 'Service',
18
- 'TestProject',
19
- ];
20
-
21
- /**
22
- * Returns a blank manifest object populated with sensible defaults.
23
- *
24
- * @param {string} name - Project name (typically the directory basename).
25
- * @returns {object}
26
- */
27
- function defaultManifest(name) {
28
- return {
29
- name: name,
30
- version: '1.0.0.0',
31
- description: '',
32
- kind: 'ConsoleApp',
33
- dependencies: {},
34
- };
35
- }
36
-
37
- /**
38
- * Reads and parses a wyvrn.json manifest from disk.
39
- *
40
- * @param {string} manifestPath - Absolute or relative path to wyvrn.json.
41
- * @returns {object} Parsed manifest data.
42
- * @throws {Error} If the file does not exist or contains invalid JSON.
43
- */
44
- function readManifest(manifestPath) {
45
- const resolved = path.resolve(manifestPath);
46
- if (!fs.existsSync(resolved)) {
47
- throw new Error(`Manifest not found: ${resolved}`);
48
- }
49
- const raw = fs.readFileSync(resolved, 'utf8');
50
- try {
51
- return JSON.parse(raw);
52
- } catch (err) {
53
- throw new Error(`Failed to parse manifest at ${resolved}: ${err.message}`);
54
- }
55
- }
56
-
57
- /**
58
- * Serialises and writes manifest data to disk with 2-space indentation.
59
- *
60
- * @param {string} manifestPath - Absolute or relative path to wyvrn.json.
61
- * @param {object} data - Manifest object to serialise.
62
- */
63
- function writeManifest(manifestPath, data) {
64
- const resolved = path.resolve(manifestPath);
65
- const dir = path.dirname(resolved);
66
- if (!fs.existsSync(dir)) {
67
- fs.mkdirSync(dir, { recursive: true });
68
- }
69
- fs.writeFileSync(resolved, JSON.stringify(data, null, 2) + '\n', 'utf8');
70
- }
71
-
72
- module.exports = {
73
- VALID_KINDS,
74
- defaultManifest,
75
- readManifest,
76
- writeManifest,
77
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /** Current manifest schema version emitted by v2 tooling. */
7
+ const SCHEMA_VERSION = 2;
8
+
9
+ /**
10
+ * Valid project kind values, matching the C# serialisation enum.
11
+ * @type {string[]}
12
+ */
13
+ const VALID_KINDS = [
14
+ 'ConsoleApp',
15
+ 'StaticLib',
16
+ 'DynamicLib',
17
+ 'HeaderOnlyLib',
18
+ 'WebApp',
19
+ 'Utility',
20
+ 'Service',
21
+ 'TestProject',
22
+ ];
23
+
24
+ /**
25
+ * Returns a blank v2 manifest object populated with sensible defaults.
26
+ *
27
+ * Build profile settings are NOT stored in wyvrn.json — they live in named
28
+ * profile files under the wyvrnpm config directory
29
+ * (%LOCALAPPDATA%\wyvrnpm\profiles\ on Windows).
30
+ *
31
+ * Dependencies can optionally carry per-package settings overrides that are
32
+ * merged on top of the active machine profile at install/publish time:
33
+ *
34
+ * "dependencies": {
35
+ * "zlib": "1.2.11",
36
+ * "openssl": { "version": "3.0.0", "settings": { "compiler.runtime": "static" } }
37
+ * }
38
+ *
39
+ * @param {string} name - Project name (typically the directory basename).
40
+ * @returns {object}
41
+ */
42
+ function defaultManifest(name) {
43
+ return {
44
+ name,
45
+ version: '1.0.0.0',
46
+ schemaVersion: SCHEMA_VERSION,
47
+ description: '',
48
+ kind: 'ConsoleApp',
49
+ dependencies: {},
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Normalise the raw `dependencies` field from a manifest into a consistent
55
+ * `{ name → { version, settings } }` map, regardless of the input format:
56
+ *
57
+ * • String value : "1.0.0" → { version: "1.0.0", settings: {} }
58
+ * Object value : { version, settings? } kept (settings defaults to {})
59
+ * • Array format : [{ Name, Version }] → converted
60
+ *
61
+ * @param {object|Array|undefined} rawDeps
62
+ * @returns {Record<string, { version: string, settings: object }>}
63
+ */
64
+ function normalizeDependencies(rawDeps) {
65
+ if (!rawDeps || typeof rawDeps !== 'object') return {};
66
+
67
+ if (Array.isArray(rawDeps)) {
68
+ const result = {};
69
+ for (const d of rawDeps) {
70
+ const n = d.Name ?? d.name;
71
+ const v = d.Version ?? d.version;
72
+ if (n && v) result[n] = { version: v, settings: {} };
73
+ }
74
+ return result;
75
+ }
76
+
77
+ const result = {};
78
+ for (const [n, value] of Object.entries(rawDeps)) {
79
+ if (typeof value === 'string') {
80
+ result[n] = { version: value, settings: {} };
81
+ } else if (value && typeof value === 'object') {
82
+ result[n] = {
83
+ version: value.version ?? value.Version ?? '',
84
+ settings: value.settings ?? {},
85
+ };
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+
91
+ /**
92
+ * Reads and parses a wyvrn.json manifest from disk.
93
+ *
94
+ * @param {string} manifestPath - Absolute or relative path to wyvrn.json.
95
+ * @returns {object} Parsed manifest data.
96
+ * @throws {Error} If the file does not exist or contains invalid JSON.
97
+ */
98
+ function readManifest(manifestPath) {
99
+ const resolved = path.resolve(manifestPath);
100
+ if (!fs.existsSync(resolved)) {
101
+ throw new Error(`Manifest not found: ${resolved}`);
102
+ }
103
+ const raw = fs.readFileSync(resolved, 'utf8');
104
+ try {
105
+ return JSON.parse(raw);
106
+ } catch (err) {
107
+ throw new Error(`Failed to parse manifest at ${resolved}: ${err.message}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Serialises and writes manifest data to disk with 2-space indentation.
113
+ *
114
+ * @param {string} manifestPath - Absolute or relative path to wyvrn.json.
115
+ * @param {object} data - Manifest object to serialise.
116
+ */
117
+ function writeManifest(manifestPath, data) {
118
+ const resolved = path.resolve(manifestPath);
119
+ const dir = path.dirname(resolved);
120
+ if (!fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ }
123
+ fs.writeFileSync(resolved, JSON.stringify(data, null, 2) + '\n', 'utf8');
124
+ }
125
+
126
+ module.exports = {
127
+ SCHEMA_VERSION,
128
+ VALID_KINDS,
129
+ defaultManifest,
130
+ normalizeDependencies,
131
+ readManifest,
132
+ writeManifest,
133
+ };
package/src/profile.js ADDED
@@ -0,0 +1,377 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { execSync } = require('child_process');
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // OS / Arch mapping (Node → Conan-style names)
10
+ // ---------------------------------------------------------------------------
11
+ const OS_MAP = {
12
+ win32: 'Windows',
13
+ linux: 'Linux',
14
+ darwin: 'Macos',
15
+ };
16
+
17
+ const ARCH_MAP = {
18
+ x64: 'x86_64',
19
+ ia32: 'x86',
20
+ arm: 'armv7',
21
+ arm64: 'armv8',
22
+ ppc64: 'ppc64le',
23
+ s390x: 's390x',
24
+ mips: 'mips',
25
+ mipsel: 'mipsel',
26
+ };
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Helpers
30
+ // ---------------------------------------------------------------------------
31
+ function tryExec(cmd) {
32
+ try {
33
+ return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 })
34
+ .toString()
35
+ .trim();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function detectOs() {
42
+ return OS_MAP[process.platform] ?? process.platform;
43
+ }
44
+
45
+ function detectArch() {
46
+ return ARCH_MAP[process.arch] ?? process.arch;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Compiler detection (per platform)
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function detectCompilerWindows() {
54
+ // 1) MSVC via vswhere
55
+ const vswhere = tryExec(
56
+ '"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"' +
57
+ ' -latest -property installationVersion',
58
+ );
59
+ if (vswhere) {
60
+ const major = parseInt(vswhere.split('.')[0], 10);
61
+ // VS17 → MSVC 193x, VS16 → 192x, VS15 → 191x, VS14 → 190x
62
+ const vsToMsvc = { 17: '193', 16: '192', 15: '191', 14: '190' };
63
+ return {
64
+ compiler: 'msvc',
65
+ 'compiler.version': vsToMsvc[major] ?? String(major),
66
+ 'compiler.runtime': 'dynamic',
67
+ };
68
+ }
69
+
70
+ // 2) Clang / clang-cl
71
+ const clang = tryExec('clang --version');
72
+ if (clang) {
73
+ const m = clang.match(/version (\d+)/);
74
+ return {
75
+ compiler: 'clang',
76
+ 'compiler.version': m ? m[1] : 'unknown',
77
+ 'compiler.runtime': null,
78
+ };
79
+ }
80
+
81
+ // 3) MinGW GCC
82
+ const gcc = tryExec('gcc --version');
83
+ if (gcc) {
84
+ const m = gcc.match(/(\d+\.\d+\.\d+)/);
85
+ return {
86
+ compiler: 'gcc',
87
+ 'compiler.version': m ? m[1].split('.')[0] : 'unknown',
88
+ 'compiler.runtime': null,
89
+ };
90
+ }
91
+
92
+ return { compiler: 'unknown', 'compiler.version': 'unknown', 'compiler.runtime': null };
93
+ }
94
+
95
+ function detectCompilerMacos() {
96
+ const clang = tryExec('clang --version');
97
+ if (clang) {
98
+ const appleMatch = clang.match(/Apple clang version (\d+)/i);
99
+ if (appleMatch) {
100
+ return { compiler: 'apple-clang', 'compiler.version': appleMatch[1], 'compiler.runtime': null };
101
+ }
102
+ const llvmMatch = clang.match(/version (\d+)/);
103
+ return {
104
+ compiler: 'clang',
105
+ 'compiler.version': llvmMatch ? llvmMatch[1] : 'unknown',
106
+ 'compiler.runtime': null,
107
+ };
108
+ }
109
+ return { compiler: 'unknown', 'compiler.version': 'unknown', 'compiler.runtime': null };
110
+ }
111
+
112
+ function detectCompilerLinux() {
113
+ // Prefer GCC on Linux
114
+ const gcc = tryExec('gcc --version');
115
+ if (gcc) {
116
+ const m = gcc.match(/(\d+\.\d+\.\d+)/);
117
+ if (m) {
118
+ return {
119
+ compiler: 'gcc',
120
+ 'compiler.version': m[1].split('.')[0],
121
+ 'compiler.runtime': null,
122
+ };
123
+ }
124
+ }
125
+
126
+ const clang = tryExec('clang --version');
127
+ if (clang) {
128
+ const m = clang.match(/version (\d+)/);
129
+ return {
130
+ compiler: 'clang',
131
+ 'compiler.version': m ? m[1] : 'unknown',
132
+ 'compiler.runtime': null,
133
+ };
134
+ }
135
+
136
+ return { compiler: 'unknown', 'compiler.version': 'unknown', 'compiler.runtime': null };
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Public API
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Auto-detect the current build environment profile.
145
+ * Mirrors Conan 2.0 profile structure.
146
+ *
147
+ * @returns {WyvrnProfile}
148
+ */
149
+ function detectProfile() {
150
+ const compilerInfo =
151
+ process.platform === 'win32' ? detectCompilerWindows()
152
+ : process.platform === 'darwin' ? detectCompilerMacos()
153
+ : detectCompilerLinux();
154
+
155
+ return {
156
+ os: detectOs(),
157
+ arch: detectArch(),
158
+ compiler: compilerInfo.compiler,
159
+ 'compiler.version': compilerInfo['compiler.version'],
160
+ 'compiler.cppstd': '20',
161
+ 'compiler.runtime': compilerInfo['compiler.runtime'],
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Compute a stable 16-char SHA256 prefix that identifies a unique build profile.
167
+ * Only non-null, non-wildcard fields contribute to the hash.
168
+ *
169
+ * @param {WyvrnProfile} profile
170
+ * @returns {string} 16-character lowercase hex string
171
+ */
172
+ function hashProfile(profile) {
173
+ const significant = Object.fromEntries(
174
+ Object.entries(profile)
175
+ .filter(([, v]) => v !== null && v !== undefined && v !== '*' && v !== '')
176
+ .sort(([a], [b]) => a.localeCompare(b)),
177
+ );
178
+ return crypto
179
+ .createHash('sha256')
180
+ .update(JSON.stringify(significant))
181
+ .digest('hex')
182
+ .slice(0, 16);
183
+ }
184
+
185
+ /**
186
+ * Compute the SHA256 hash of a file or Buffer.
187
+ *
188
+ * @param {string|Buffer} source - Absolute path to a file, or a Buffer.
189
+ * @returns {string} Full 64-character lowercase hex SHA256.
190
+ */
191
+ function sha256Of(source) {
192
+ const buf = Buffer.isBuffer(source) ? source : fs.readFileSync(source);
193
+ return crypto.createHash('sha256').update(buf).digest('hex');
194
+ }
195
+
196
+ /**
197
+ * Try to get the current git commit SHA in the given directory.
198
+ *
199
+ * @param {string} [cwd] - Directory to run git in (defaults to process.cwd()).
200
+ * @returns {string|null}
201
+ */
202
+ function getGitSha(cwd) {
203
+ const result = tryExec(`git -C "${cwd || '.'}" rev-parse HEAD`);
204
+ return result && /^[0-9a-f]{40}$/i.test(result) ? result : null;
205
+ }
206
+
207
+ /**
208
+ * Try to get the remote origin URL for the current git repo.
209
+ *
210
+ * @param {string} [cwd]
211
+ * @returns {string|null}
212
+ */
213
+ function getGitRepo(cwd) {
214
+ return tryExec(`git -C "${cwd || '.'}" remote get-url origin`) || null;
215
+ }
216
+
217
+ /**
218
+ * Format a profile for human-readable display (Conan-style).
219
+ *
220
+ * @param {WyvrnProfile} profile
221
+ * @returns {string}
222
+ */
223
+ function formatProfile(profile) {
224
+ const fieldOrder = [
225
+ 'os', 'arch', 'compiler', 'compiler.version', 'compiler.cppstd', 'compiler.runtime',
226
+ ];
227
+ const lines = ['[settings]'];
228
+ for (const key of fieldOrder) {
229
+ const val = profile[key];
230
+ if (val !== null && val !== undefined) {
231
+ lines.push(`${key}=${val}`);
232
+ }
233
+ }
234
+ return lines.join('\n');
235
+ }
236
+
237
+ /**
238
+ * Merge a partial override into a base profile, ignoring null/undefined overrides.
239
+ *
240
+ * @param {WyvrnProfile} base
241
+ * @param {Partial<WyvrnProfile>} overrides
242
+ * @returns {WyvrnProfile}
243
+ */
244
+ function mergeProfile(base, overrides) {
245
+ return {
246
+ ...base,
247
+ ...Object.fromEntries(Object.entries(overrides).filter(([, v]) => v != null)),
248
+ };
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Profile file management
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /**
256
+ * Returns the directory where named build profiles are stored.
257
+ * Windows : %LOCALAPPDATA%\wyvrnpm\profiles
258
+ * Other : ~/.config/wyvrnpm/profiles
259
+ *
260
+ * @returns {string}
261
+ */
262
+ function getProfilesDir() {
263
+ const base = process.env.LOCALAPPDATA
264
+ ? path.join(process.env.LOCALAPPDATA, 'wyvrnpm')
265
+ : path.join(require('os').homedir(), '.config', 'wyvrnpm');
266
+ return path.join(base, 'profiles');
267
+ }
268
+
269
+ /**
270
+ * Read a named profile from the profiles directory.
271
+ * Looks for `{name}.json`, then `{name}` without extension.
272
+ *
273
+ * @param {string} name - Profile name (e.g. "default", "msvc_release").
274
+ * @returns {WyvrnProfile|null} Parsed profile, or null if not found.
275
+ */
276
+ function readProfile(name) {
277
+ const dir = getProfilesDir();
278
+ for (const filename of [`${name}.json`, name]) {
279
+ const p = path.join(dir, filename);
280
+ if (fs.existsSync(p)) {
281
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
282
+ catch { return null; }
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+
288
+ /**
289
+ * Write a named profile to the profiles directory as `{name}.json`.
290
+ *
291
+ * @param {string} name
292
+ * @param {WyvrnProfile} profile
293
+ */
294
+ function writeProfile(name, profile) {
295
+ const dir = getProfilesDir();
296
+ fs.mkdirSync(dir, { recursive: true });
297
+ fs.writeFileSync(
298
+ path.join(dir, `${name}.json`),
299
+ JSON.stringify(profile, null, 2) + '\n',
300
+ 'utf8',
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Delete a named profile file. Does nothing if the profile does not exist.
306
+ *
307
+ * @param {string} name
308
+ * @returns {boolean} true if a file was deleted.
309
+ */
310
+ function deleteProfile(name) {
311
+ const dir = getProfilesDir();
312
+ for (const filename of [`${name}.json`, name]) {
313
+ const p = path.join(dir, filename);
314
+ if (fs.existsSync(p)) {
315
+ fs.unlinkSync(p);
316
+ return true;
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
322
+ /**
323
+ * List all profile names available in the profiles directory.
324
+ *
325
+ * @returns {string[]} Sorted list of profile names (without `.json` extension).
326
+ */
327
+ function listProfiles() {
328
+ const dir = getProfilesDir();
329
+ if (!fs.existsSync(dir)) return [];
330
+ return fs
331
+ .readdirSync(dir)
332
+ .filter((f) => f.endsWith('.json'))
333
+ .map((f) => f.slice(0, -5))
334
+ .sort();
335
+ }
336
+
337
+ /**
338
+ * Load the active build profile by name. Falls back to auto-detection if the
339
+ * named profile file does not exist yet.
340
+ *
341
+ * @param {string} [name='default']
342
+ * @returns {{ profile: WyvrnProfile, fromFile: boolean }}
343
+ */
344
+ function loadProfile(name = 'default') {
345
+ const stored = readProfile(name);
346
+ if (stored) return { profile: stored, fromFile: true };
347
+ return { profile: detectProfile(), fromFile: false };
348
+ }
349
+
350
+ module.exports = {
351
+ // Detection
352
+ detectProfile,
353
+ hashProfile,
354
+ sha256Of,
355
+ getGitSha,
356
+ getGitRepo,
357
+ formatProfile,
358
+ mergeProfile,
359
+ // Profile file management
360
+ getProfilesDir,
361
+ readProfile,
362
+ writeProfile,
363
+ deleteProfile,
364
+ listProfiles,
365
+ loadProfile,
366
+ };
367
+
368
+ /**
369
+ * @typedef {{
370
+ * os: string,
371
+ * arch: string,
372
+ * compiler: string,
373
+ * 'compiler.version': string,
374
+ * 'compiler.cppstd': string,
375
+ * 'compiler.runtime': string|null,
376
+ * }} WyvrnProfile
377
+ */