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.
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ detectProfile,
5
+ hashProfile,
6
+ formatProfile,
7
+ readProfile,
8
+ writeProfile,
9
+ deleteProfile,
10
+ listProfiles,
11
+ getProfilesDir,
12
+ mergeProfile,
13
+ } = require('../profile');
14
+ const { readConfig, writeConfig } = require('../config');
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // list
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * wyvrnpm configure profile list
22
+ * Show all saved profiles and which one is the default.
23
+ */
24
+ function list() {
25
+ const config = readConfig();
26
+ const names = listProfiles();
27
+ const dir = getProfilesDir();
28
+ const defName = config.defaultProfile ?? 'default';
29
+
30
+ console.log(`[wyvrn] Profiles directory: ${dir}`);
31
+ console.log(`[wyvrn] Default profile : ${defName}\n`);
32
+
33
+ if (names.length === 0) {
34
+ console.log(' (no profiles saved — run `wyvrnpm configure profile detect` to create one)');
35
+ return;
36
+ }
37
+
38
+ for (const name of names) {
39
+ const marker = name === defName ? ' *' : '';
40
+ const p = readProfile(name);
41
+ const summary = p
42
+ ? `${p.os}/${p.arch} ${p.compiler} ${p['compiler.version']} C++${p['compiler.cppstd']}` +
43
+ (p['compiler.runtime'] ? ` [${p['compiler.runtime']}]` : '')
44
+ : '(unreadable)';
45
+ console.log(` ${(name + marker).padEnd(24)} ${summary}`);
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // show
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * wyvrnpm configure profile show [--name <name>]
55
+ * Display a specific profile (defaults to the configured default).
56
+ */
57
+ function show(argv) {
58
+ const config = readConfig();
59
+ const name = argv.name ?? config.defaultProfile ?? 'default';
60
+ const stored = readProfile(name);
61
+
62
+ if (stored) {
63
+ console.log(`[wyvrn] Profile "${name}":`);
64
+ console.log(formatProfile(stored));
65
+ console.log(`\nProfile hash : ${hashProfile(stored)}`);
66
+ } else {
67
+ const detected = detectProfile();
68
+ console.log(`[wyvrn] Profile "${name}" not found — showing auto-detected environment:`);
69
+ console.log(formatProfile(detected));
70
+ console.log(`\nProfile hash : ${hashProfile(detected)}`);
71
+ console.log(`\nRun \`wyvrnpm configure profile detect --name ${name}\` to save this.`);
72
+ }
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // detect
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /**
80
+ * wyvrnpm configure profile detect [--name <name>] [--dry-run]
81
+ * Auto-detect the current build environment and save it as a named profile.
82
+ */
83
+ function detect(argv) {
84
+ const config = readConfig();
85
+ const name = argv.name ?? config.defaultProfile ?? 'default';
86
+ const p = detectProfile();
87
+
88
+ console.log('[wyvrn] Detected profile:');
89
+ console.log(formatProfile(p));
90
+ console.log(`\nProfile hash : ${hashProfile(p)}`);
91
+
92
+ if (!argv.dryRun) {
93
+ writeProfile(name, p);
94
+ console.log(`\n[wyvrn] Saved as profile "${name}".`);
95
+ }
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // set
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * wyvrnpm configure profile set [--name <name>] [field options]
104
+ * Manually override individual fields in a named profile.
105
+ * Starts from the existing saved profile, or auto-detects if none exists yet.
106
+ */
107
+ function set(argv) {
108
+ const config = readConfig();
109
+ const name = argv.name ?? config.defaultProfile ?? 'default';
110
+ const base = readProfile(name) ?? detectProfile();
111
+
112
+ const updated = mergeProfile(base, {
113
+ os: argv.os ?? null,
114
+ arch: argv.arch ?? null,
115
+ compiler: argv.compiler ?? null,
116
+ 'compiler.version': argv.compilerVersion ?? null,
117
+ 'compiler.cppstd': argv.cppstd ?? null,
118
+ 'compiler.runtime': argv.runtime ?? null,
119
+ });
120
+
121
+ writeProfile(name, updated);
122
+
123
+ console.log(`[wyvrn] Profile "${name}" updated:`);
124
+ console.log(formatProfile(updated));
125
+ console.log(`\nProfile hash : ${hashProfile(updated)}`);
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // setDefault
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * wyvrnpm configure profile set-default <name>
134
+ * Change which named profile is used by default.
135
+ */
136
+ function setDefault(argv) {
137
+ const name = argv.name;
138
+ if (!readProfile(name)) {
139
+ console.error(`[wyvrn] Error: profile "${name}" does not exist. Create it first.`);
140
+ process.exit(1);
141
+ }
142
+ const config = readConfig();
143
+ config.defaultProfile = name;
144
+ writeConfig(config);
145
+ console.log(`[wyvrn] Default profile set to "${name}".`);
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // del
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * wyvrnpm configure profile delete <name>
154
+ * Remove a saved profile file.
155
+ */
156
+ function del(argv) {
157
+ const name = argv.name;
158
+ if (name === 'default') {
159
+ console.error('[wyvrn] Error: cannot delete the "default" profile.');
160
+ process.exit(1);
161
+ }
162
+ const removed = deleteProfile(name);
163
+ if (removed) {
164
+ console.log(`[wyvrn] Profile "${name}" deleted.`);
165
+ } else {
166
+ console.error(`[wyvrn] Error: profile "${name}" not found.`);
167
+ process.exit(1);
168
+ }
169
+ }
170
+
171
+ module.exports = { list, show, detect, set, setDefault, del };
@@ -1,17 +1,19 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
3
+ const fs = require('fs');
4
4
  const path = require('path');
5
- const os = require('os');
5
+ const os = require('os');
6
6
  const AdmZip = require('adm-zip');
7
- const { readManifest } = require('../manifest');
8
- const { getProvider } = require('../providers');
9
- const { readConfig } = require('../config');
10
-
11
- /**
12
- * Parse a .wyvrnignore file and return an array of pattern strings.
13
- * Lines starting with '#' and blank lines are ignored.
14
- */
7
+
8
+ const { readManifest } = require('../manifest');
9
+ const { getProvider } = require('../providers');
10
+ const { readConfig } = require('../config');
11
+ const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo } = require('../profile');
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // .wyvrnignore helpers (unchanged from v1)
15
+ // ---------------------------------------------------------------------------
16
+
15
17
  function loadIgnorePatterns(ignoreFile) {
16
18
  if (!fs.existsSync(ignoreFile)) return [];
17
19
  return fs
@@ -21,46 +23,26 @@ function loadIgnorePatterns(ignoreFile) {
21
23
  .filter((l) => l.length > 0 && !l.startsWith('#'));
22
24
  }
23
25
 
24
- /**
25
- * Convert a glob-style ignore pattern to a RegExp.
26
- * Supports '*', '**', '?' wildcards and leading '/' to anchor to root.
27
- */
28
26
  function patternToRegex(pattern) {
29
- // A leading slash means "anchored to root"; strip it and keep the rest
30
27
  const anchored = pattern.startsWith('/');
31
28
  const p = anchored ? pattern.slice(1) : pattern;
32
-
33
29
  let re = p
34
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars
35
- .replace(/\*\*/g, '\x00') // placeholder for **
36
- .replace(/\*/g, '[^/]*') // * → match within segment
37
- .replace(/\?/g, '[^/]') // ? → single non-slash char
38
- .replace(/\x00/g, '.*'); // ** → match across segments
39
-
40
- // If anchored, match from the start; otherwise match any path segment
30
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
31
+ .replace(/\*\*/g, '\x00')
32
+ .replace(/\*/g, '[^/]*')
33
+ .replace(/\?/g, '[^/]')
34
+ .replace(/\x00/g, '.*');
41
35
  return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
42
36
  }
43
37
 
44
- /**
45
- * Recursively add files from `dir` into `zip`, skipping any path that
46
- * matches one of the compiled ignore regexes.
47
- *
48
- * @param {AdmZip} zip
49
- * @param {string} dir Absolute path of the directory being zipped
50
- * @param {string} base Absolute path of the root publish directory
51
- * @param {RegExp[]} ignoreRe Compiled ignore patterns
52
- */
53
38
  function addFolderFiltered(zip, dir, base, ignoreRe) {
54
39
  for (const entry of fs.readdirSync(dir)) {
55
40
  const fullPath = path.join(dir, entry);
56
- // Relative path from publish root, always using forward slashes
57
- const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
58
-
41
+ const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
59
42
  if (ignoreRe.some((re) => re.test(relPath))) {
60
43
  console.log(`[wyvrn] Ignoring: ${relPath}`);
61
44
  continue;
62
45
  }
63
-
64
46
  const stat = fs.statSync(fullPath);
65
47
  if (stat.isDirectory()) {
66
48
  addFolderFiltered(zip, fullPath, base, ignoreRe);
@@ -71,23 +53,23 @@ function addFolderFiltered(zip, dir, base, ignoreRe) {
71
53
  }
72
54
  }
73
55
 
74
- async function publish(argv) {
75
- let { source, profile, platform, path: publishPath, token, manifest: manifestArg, force } = argv;
56
+ // ---------------------------------------------------------------------------
57
+ // Source resolution
58
+ // ---------------------------------------------------------------------------
59
+
60
+ function resolveSource(argv) {
61
+ let { source, awsProfile, token } = argv;
62
+ const config = readConfig();
76
63
 
77
- // Resolve source + auth: CLI > config entry by name > first config entry > raw URL
78
64
  if (source) {
79
- // Check if --source matches a named config entry rather than a raw URL/URI
80
- const config = readConfig();
81
65
  const named = config.publishSources.find((s) => s.name === source);
82
66
  if (named) {
83
67
  console.log(`[wyvrn] Using publish source "${named.name}" from config`);
84
- source = named.url;
85
- profile = profile ?? named.profile;
86
- token = token ?? named.token;
68
+ source = named.url;
69
+ awsProfile = awsProfile ?? named.profile;
70
+ token = token ?? named.token;
87
71
  }
88
72
  } else {
89
- // No --source given — fall back to first configured publish source
90
- const config = readConfig();
91
73
  if (config.publishSources.length === 0) {
92
74
  console.error(
93
75
  '[wyvrn] Error: --source is required (or configure one with: ' +
@@ -97,12 +79,28 @@ async function publish(argv) {
97
79
  }
98
80
  const first = config.publishSources[0];
99
81
  console.log(`[wyvrn] Using publish source "${first.name}" from config`);
100
- source = first.url;
101
- profile = profile ?? first.profile;
102
- token = token ?? first.token;
82
+ source = first.url;
83
+ awsProfile = awsProfile ?? first.profile;
84
+ token = token ?? first.token;
103
85
  }
104
86
 
105
- // Resolve and read the manifest
87
+ return { source, awsProfile, token };
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Main publish command
92
+ // ---------------------------------------------------------------------------
93
+
94
+ async function publish(argv) {
95
+ const { source, awsProfile, token } = resolveSource(argv);
96
+ const {
97
+ profile: profileName,
98
+ path: publishPath,
99
+ force,
100
+ manifest: manifestArg,
101
+ } = argv;
102
+
103
+ // ── Load manifest ─────────────────────────────────────────────────────────
106
104
  const manifestPath = path.resolve(manifestArg || './wyvrn.json');
107
105
  const manifest = await readManifest(manifestPath);
108
106
  if (!manifest) {
@@ -116,15 +114,13 @@ async function publish(argv) {
116
114
  process.exit(1);
117
115
  }
118
116
 
119
- const targetPlatform = platform || 'common';
120
117
  const srcDir = path.resolve(publishPath || '.');
121
-
122
118
  if (!fs.existsSync(srcDir)) {
123
119
  console.error(`[wyvrn] Error: Publish path does not exist: ${srcDir}`);
124
120
  process.exit(1);
125
121
  }
126
122
 
127
- // Resolve provider from source URL/URI
123
+ // ── Resolve provider ──────────────────────────────────────────────────────
128
124
  let provider;
129
125
  try {
130
126
  provider = getProvider(source);
@@ -133,24 +129,62 @@ async function publish(argv) {
133
129
  process.exit(1);
134
130
  }
135
131
 
132
+ // ── Load build profile ────────────────────────────────────────────────────
133
+ const config = readConfig();
134
+ const activeProfileName = profileName ?? config.defaultProfile ?? 'default';
135
+ const { profile: buildProfile, fromFile } = loadProfile(activeProfileName);
136
+ const profileHash = hashProfile(buildProfile);
137
+
136
138
  console.log(
137
- `[wyvrn] Publishing ${name}@${version} (${targetPlatform}) via ${provider.constructor.providerName} provider`,
139
+ `[wyvrn] Publishing ${name}@${version}\n` +
140
+ `[wyvrn] Profile : "${activeProfileName}"${fromFile ? '' : ' (auto-detected — save with `wyvrnpm configure profile detect`)'}\n` +
141
+ `[wyvrn] ${buildProfile.os}/${buildProfile.arch} ` +
142
+ `${buildProfile.compiler} ${buildProfile['compiler.version']} ` +
143
+ `C++${buildProfile['compiler.cppstd']}` +
144
+ (buildProfile['compiler.runtime'] ? ` [${buildProfile['compiler.runtime']}]` : '') + '\n' +
145
+ `[wyvrn] Profile hash: ${profileHash}\n` +
146
+ `[wyvrn] Provider : ${provider.constructor.providerName}`,
138
147
  );
139
148
 
140
- // Prevent overwriting an already-published version unless --force is passed
141
- const alreadyExists = await provider.exists({ source, platform: targetPlatform, name, version, profile, token });
142
- if (alreadyExists) {
149
+ // ── Check v2 existence ────────────────────────────────────────────────────
150
+ const v2AlreadyExists = await provider.v2Exists({ source, name, version, profileHash, awsProfile, token });
151
+ if (v2AlreadyExists) {
143
152
  if (force) {
144
- console.warn(`[wyvrn] Warning: ${name}@${version} already exists — overwriting (--force)`);
153
+ console.warn(`[wyvrn] Warning: ${name}@${version} [${profileHash}] already exists — overwriting (--force)`);
145
154
  } else {
146
155
  console.error(
147
- `[wyvrn] Error: ${name}@${version} already exists. Bump the version or use --force to overwrite.`,
156
+ `[wyvrn] Error: ${name}@${version} [${profileHash}] already exists.\n` +
157
+ ' Bump the version, use a different profile, or pass --force to overwrite.',
148
158
  );
149
159
  process.exit(1);
150
160
  }
151
161
  }
152
162
 
153
- // Load .wyvrnignore patterns from the publish directory
163
+ // ── Read locked dependency versions from wyvrn.lock ──────────────────────
164
+ const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
165
+ const lockedDependencies = {};
166
+ if (fs.existsSync(lockPath)) {
167
+ try {
168
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
169
+ for (const [pkgName, entry] of Object.entries(lock.packages ?? {})) {
170
+ const ver = typeof entry === 'string' ? entry : entry.version;
171
+ if (ver) lockedDependencies[pkgName] = ver;
172
+ }
173
+ console.log(`[wyvrn] Lock file : ${Object.keys(lockedDependencies).length} pinned package(s)`);
174
+ } catch {
175
+ console.warn('[wyvrn] Warning: could not read wyvrn.lock — dependency versions will be published as-is from wyvrn.json');
176
+ }
177
+ } else {
178
+ console.warn('[wyvrn] Warning: no wyvrn.lock found — run `wyvrnpm install` before publishing to pin dependency versions');
179
+ }
180
+
181
+ // ── Git metadata ──────────────────────────────────────────────────────────
182
+ const gitSha = getGitSha(srcDir);
183
+ const gitRepo = getGitRepo(srcDir);
184
+ if (gitSha) console.log(`[wyvrn] Git commit : ${gitSha}`);
185
+ if (gitRepo) console.log(`[wyvrn] Git remote : ${gitRepo}`);
186
+
187
+ // ── .wyvrnignore ──────────────────────────────────────────────────────────
154
188
  const ignoreFile = path.join(srcDir, '.wyvrnignore');
155
189
  const ignorePatterns = loadIgnorePatterns(ignoreFile);
156
190
  const ignoreRe = ignorePatterns.map(patternToRegex);
@@ -158,7 +192,7 @@ async function publish(argv) {
158
192
  console.log(`[wyvrn] Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
159
193
  }
160
194
 
161
- // Build a temporary zip of the publish directory
195
+ // ── Build zip ─────────────────────────────────────────────────────────────
162
196
  const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
163
197
  try {
164
198
  console.log(`[wyvrn] Zipping ${srcDir} ...`);
@@ -166,12 +200,30 @@ async function publish(argv) {
166
200
  addFolderFiltered(zip, srcDir, srcDir, ignoreRe);
167
201
  zip.writeZip(tmpZipPath);
168
202
 
169
- await provider.publish(
203
+ const contentSha256 = sha256Of(tmpZipPath);
204
+ console.log(`[wyvrn] Content SHA256 : ${contentSha256}`);
205
+
206
+ // ── v2 publish ────────────────────────────────────────────────────────
207
+ console.log('[wyvrn] Publishing (v2) ...');
208
+ await provider.v2Publish(
170
209
  { manifest: manifestPath, zip: tmpZipPath },
171
- { source, platform: targetPlatform, name, version, profile, token },
210
+ {
211
+ source,
212
+ name,
213
+ version,
214
+ profileHash,
215
+ // buildSettings stored in metadata for debugging — NOT wyvrn.json
216
+ buildSettings: buildProfile,
217
+ contentSha256,
218
+ gitSha,
219
+ gitRepo,
220
+ lockedDependencies,
221
+ awsProfile,
222
+ token,
223
+ },
172
224
  );
173
225
 
174
- console.log(`[wyvrn] Successfully published ${name}@${version} to ${source}`);
226
+ console.log(`[wyvrn] Successfully published ${name}@${version} [${profileHash}] to ${source}`);
175
227
  } finally {
176
228
  if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
177
229
  }
package/src/config.js CHANGED
@@ -24,23 +24,25 @@ function getConfigPath() {
24
24
  /**
25
25
  * Read and parse config.json.
26
26
  * Returns a default empty config if the file doesn't exist yet.
27
- * @returns {{ installSources: SourceEntry[], publishSources: SourceEntry[], linkedPackages: Object<string, string> }}
27
+ * @returns {{ installSources: SourceEntry[], publishSources: SourceEntry[], linkedPackages: Object<string, string>, defaultProfile: string }}
28
28
  */
29
29
  function readConfig() {
30
30
  const configPath = getConfigPath();
31
31
  if (!fs.existsSync(configPath)) {
32
- return { installSources: [], publishSources: [], linkedPackages: {} };
32
+ return { installSources: [], publishSources: [], linkedPackages: {}, defaultProfile: 'default' };
33
33
  }
34
34
  try {
35
35
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
36
  return {
37
- installSources: raw.installSources ?? [],
38
- publishSources: raw.publishSources ?? [],
39
- linkedPackages: raw.linkedPackages ?? {},
37
+ installSources: raw.installSources ?? [],
38
+ publishSources: raw.publishSources ?? [],
39
+ linkedPackages: raw.linkedPackages ?? {},
40
+ // Migrate: if old config had an inline `profile` object, drop it; use file-based profiles.
41
+ defaultProfile: raw.defaultProfile ?? 'default',
40
42
  };
41
43
  } catch {
42
44
  console.warn('[wyvrn] Warning: could not parse config.json — using defaults');
43
- return { installSources: [], publishSources: [], linkedPackages: {} };
45
+ return { installSources: [], publishSources: [], linkedPackages: {}, defaultProfile: 'default' };
44
46
  }
45
47
  }
46
48