wyvrnpm 1.3.2 → 2.0.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.
package/src/manifest.js CHANGED
@@ -3,6 +3,9 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
+ /** Current manifest schema version emitted by v2 tooling. */
7
+ const SCHEMA_VERSION = 2;
8
+
6
9
  /**
7
10
  * Valid project kind values, matching the C# serialisation enum.
8
11
  * @type {string[]}
@@ -19,21 +22,72 @@ const VALID_KINDS = [
19
22
  ];
20
23
 
21
24
  /**
22
- * Returns a blank manifest object populated with sensible defaults.
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
+ * }
23
38
  *
24
39
  * @param {string} name - Project name (typically the directory basename).
25
40
  * @returns {object}
26
41
  */
27
42
  function defaultManifest(name) {
28
43
  return {
29
- name: name,
44
+ name,
30
45
  version: '1.0.0.0',
46
+ schemaVersion: SCHEMA_VERSION,
31
47
  description: '',
32
48
  kind: 'ConsoleApp',
33
49
  dependencies: {},
34
50
  };
35
51
  }
36
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
+
37
91
  /**
38
92
  * Reads and parses a wyvrn.json manifest from disk.
39
93
  *
@@ -70,8 +124,10 @@ function writeManifest(manifestPath, data) {
70
124
  }
71
125
 
72
126
  module.exports = {
127
+ SCHEMA_VERSION,
73
128
  VALID_KINDS,
74
129
  defaultManifest,
130
+ normalizeDependencies,
75
131
  readManifest,
76
132
  writeManifest,
77
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
+ */
@@ -6,6 +6,19 @@
6
6
  * To add a new provider:
7
7
  * 1. Create src/providers/<name>.js that extends BaseProvider
8
8
  * 2. Register it in src/providers/index.js
9
+ *
10
+ * ─── Schema versions ────────────────────────────────────────────────────────
11
+ *
12
+ * v1 (legacy): {source}/{platform}/{name}/{version}/wyvrn.{json,zip}
13
+ * {source}/{platform}/{name}/latest.json
14
+ *
15
+ * v2: {source}/v2/{name}/{version}/{profileHash}/wyvrn.{json,zip}
16
+ * {source}/v2/{name}/{version}/source.json
17
+ * {source}/v2/{name}/versions.json
18
+ * {source}/v2/{name}/latest.json
19
+ *
20
+ * Publishing with v2 tooling writes both v2 AND v1 paths by default so that
21
+ * v1 clients can still consume the packages.
9
22
  */
10
23
  class BaseProvider {
11
24
  /**
@@ -18,18 +31,13 @@ class BaseProvider {
18
31
  return false;
19
32
  }
20
33
 
34
+ // ── v1 (legacy) API ────────────────────────────────────────────────────────
35
+
21
36
  /**
22
- * Publish package files to the destination.
37
+ * Publish package files to the v1 destination path.
23
38
  *
24
39
  * @param {{ manifest: string, zip: string }} files
25
- * Absolute paths to the local wyvrn.json and wyvrn.zip to upload.
26
- * @param {{ source: string, platform: string, name: string, version: string, profile?: string, token?: string }} options
27
- * `source` – the raw --source value (URL / URI)
28
- * `platform` – target platform string (e.g. win_x64)
29
- * `name` – package name from manifest
30
- * `version` – package version from manifest
31
- * `profile` – AWS SSO profile (S3 provider)
32
- * `token` – Bearer token (HTTP provider)
40
+ * @param {{ source: string, platform: string, name: string, version: string, awsProfile?: string, token?: string }} options
33
41
  * @returns {Promise<void>}
34
42
  */
35
43
  async publish(_files, _options) {
@@ -37,16 +45,85 @@ class BaseProvider {
37
45
  }
38
46
 
39
47
  /**
40
- * Returns true if the given package version already exists at the destination.
41
- * Providers should override this; the default assumes it does not exist.
48
+ * Returns true if the given package version already exists at the v1 destination.
42
49
  *
43
- * @param {{ source: string, platform: string, name: string, version: string, profile?: string, token?: string }} _options
50
+ * @param {{ source: string, platform: string, name: string, version: string, awsProfile?: string, token?: string }} _options
44
51
  * @returns {Promise<boolean>}
45
52
  */
46
53
  async exists(_options) {
47
54
  return false;
48
55
  }
49
56
 
57
+ // ── v2 API ─────────────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Returns true if a prebuilt binary matching the given profile hash already
61
+ * exists at the v2 destination.
62
+ *
63
+ * @param {{ source: string, name: string, version: string, profileHash: string, awsProfile?: string, token?: string }} _options
64
+ * @returns {Promise<boolean>}
65
+ */
66
+ async v2Exists(_options) {
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Publish to the v2 path.
72
+ * Writes wyvrn.json + wyvrn.zip under profileHash, updates versions.json and latest.json.
73
+ *
74
+ * @param {{ manifest: string, zip: string }} files - local file paths
75
+ * @param {{
76
+ * source: string,
77
+ * name: string,
78
+ * version: string,
79
+ * profileHash: string,
80
+ * buildSettings: import('../profile').WyvrnProfile, // stored in metadata for debugging
81
+ * contentSha256: string,
82
+ * gitSha: string|null,
83
+ * gitRepo: string|null,
84
+ * awsProfile?: string,
85
+ * token?: string,
86
+ * }} options
87
+ * @returns {Promise<void>}
88
+ */
89
+ async v2Publish(_files, _options) {
90
+ throw new Error(`v2Publish() is not implemented in ${this.constructor.name}`);
91
+ }
92
+
93
+ /**
94
+ * Fetch the wyvrn.json metadata object for a specific v2 build.
95
+ * Returns null if the build does not exist.
96
+ *
97
+ * @param {{ source: string, name: string, version: string, profileHash: string, awsProfile?: string, token?: string }} _options
98
+ * @returns {Promise<object|null>}
99
+ */
100
+ async v2GetMeta(_options) {
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Fetch and return the parsed versions.json index for a package.
106
+ * Returns null if no index exists yet.
107
+ *
108
+ * @param {{ source: string, name: string, awsProfile?: string, token?: string }} _options
109
+ * @returns {Promise<object|null>}
110
+ */
111
+ async v2GetVersionsIndex(_options) {
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Download the prebuilt binary zip for a specific v2 build to destPath.
117
+ *
118
+ * @param {{ source: string, name: string, version: string, profileHash: string, awsProfile?: string, token?: string }} options
119
+ * @param {string} destPath - absolute path to write the zip to
120
+ * @param {number} timeoutMs
121
+ * @returns {Promise<boolean>} true on success
122
+ */
123
+ async v2DownloadZip(_options, _destPath, _timeoutMs) {
124
+ return false;
125
+ }
126
+
50
127
  /** Human-readable provider name shown in log output. */
51
128
  static get providerName() {
52
129
  return 'BaseProvider';