wyvrnpm 1.2.0 → 1.3.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,316 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readConfig, writeConfig } = require('../config');
6
+ const { readManifest } = require('../manifest');
7
+ const { createLink, isLink, removeLink, getLinkTarget } = require('../link-utils');
8
+ const { downloadDependencies } = require('../download');
9
+ const { resolveDependencies } = require('../resolve');
10
+
11
+ /**
12
+ * Normalizes a linked package entry to object form.
13
+ * Supports both legacy string format and new object format.
14
+ *
15
+ * @param {string|object} entry - Either a path string or {path, subdir} object.
16
+ * @returns {{path: string, subdir?: string}}
17
+ */
18
+ function normalizeLinkedEntry(entry) {
19
+ if (typeof entry === 'string') {
20
+ return { path: entry };
21
+ }
22
+ return entry;
23
+ }
24
+
25
+ /**
26
+ * Gets the effective link target path (combining path + subdir).
27
+ *
28
+ * @param {{path: string, subdir?: string}} entry - Linked package entry.
29
+ * @returns {string} The full path to link to.
30
+ */
31
+ function getEffectivePath(entry) {
32
+ if (entry.subdir) {
33
+ return path.join(entry.path, entry.subdir);
34
+ }
35
+ return entry.path;
36
+ }
37
+
38
+ /**
39
+ * Register the current directory's package globally for linking.
40
+ * Reads wyvrn.json to get the package name, stores {name: {path, subdir}} in config.
41
+ *
42
+ * @param {string} manifestPath - Path to wyvrn.json
43
+ * @param {string} [subdir] - Optional subdirectory within the package to link.
44
+ */
45
+ function registerGlobal(manifestPath, subdir) {
46
+ const manifest = readManifest(manifestPath);
47
+ const packageName = manifest.name || manifest.Name;
48
+
49
+ if (!packageName) {
50
+ console.error('[wyvrn] Error: wyvrn.json must have a "name" field');
51
+ process.exit(1);
52
+ }
53
+
54
+ const packagePath = path.dirname(path.resolve(manifestPath));
55
+ const config = readConfig();
56
+ config.linkedPackages = config.linkedPackages || {};
57
+
58
+ const entry = { path: packagePath };
59
+ if (subdir) {
60
+ entry.subdir = subdir;
61
+ }
62
+ config.linkedPackages[packageName] = entry;
63
+ writeConfig(config);
64
+
65
+ const effectivePath = getEffectivePath(entry);
66
+ console.log(`[wyvrn] Registered "${packageName}" → ${effectivePath}`);
67
+ }
68
+
69
+ /**
70
+ * Link a package into the current project's wyvrn_internal directory.
71
+ *
72
+ * @param {string} name - Package name to link.
73
+ * @param {string} sourcePath - Source directory path (absolute or relative).
74
+ * @param {string} rootDir - Project root directory.
75
+ */
76
+ function linkToProject(name, sourcePath, rootDir) {
77
+ const absoluteSource = path.resolve(sourcePath);
78
+
79
+ if (!fs.existsSync(absoluteSource)) {
80
+ console.error(`[wyvrn] Error: source path does not exist: ${absoluteSource}`);
81
+ process.exit(1);
82
+ }
83
+
84
+ const razerDir = path.join(rootDir, 'wyvrn_internal');
85
+ const linkPath = path.join(razerDir, name);
86
+
87
+ // Create the link
88
+ createLink(absoluteSource, linkPath);
89
+
90
+ // Write a .wyvrn-version file to indicate it's linked
91
+ const versionFile = path.join(linkPath, '.wyvrn-version');
92
+ fs.writeFileSync(versionFile, 'linked', 'utf8');
93
+
94
+ // Update the lock file to record the link
95
+ const lockPath = path.join(rootDir, 'wyvrn.lock');
96
+ let lockData = { packages: {} };
97
+ if (fs.existsSync(lockPath)) {
98
+ try {
99
+ lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
100
+ } catch {
101
+ // Use default if parsing fails
102
+ }
103
+ }
104
+ lockData.packages = lockData.packages || {};
105
+ lockData.packages[name] = `linked:${absoluteSource}`;
106
+ lockData.generatedAt = new Date().toISOString();
107
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + '\n', 'utf8');
108
+
109
+ console.log(`[wyvrn] Linked: ${name} → ${absoluteSource}`);
110
+ }
111
+
112
+ /**
113
+ * Unlink a package from the current project.
114
+ *
115
+ * @param {string} name - Package name to unlink.
116
+ * @param {string} rootDir - Project root directory.
117
+ * @param {boolean} restore - Whether to re-download from registry.
118
+ * @param {object} argv - Original argv for re-download options.
119
+ */
120
+ async function unlinkFromProject(name, rootDir, restore, argv) {
121
+ const razerDir = path.join(rootDir, 'wyvrn_internal');
122
+ const linkPath = path.join(razerDir, name);
123
+
124
+ if (!fs.existsSync(linkPath)) {
125
+ console.error(`[wyvrn] Error: package "${name}" is not installed`);
126
+ process.exit(1);
127
+ }
128
+
129
+ if (!isLink(linkPath)) {
130
+ console.error(`[wyvrn] Error: package "${name}" is not a linked package`);
131
+ process.exit(1);
132
+ }
133
+
134
+ // Remove the symlink
135
+ removeLink(linkPath);
136
+ console.log(`[wyvrn] Unlinked: ${name}`);
137
+
138
+ // Update the lock file
139
+ const lockPath = path.join(rootDir, 'wyvrn.lock');
140
+ if (fs.existsSync(lockPath)) {
141
+ try {
142
+ const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
143
+ if (lockData.packages && lockData.packages[name]) {
144
+ delete lockData.packages[name];
145
+ lockData.generatedAt = new Date().toISOString();
146
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + '\n', 'utf8');
147
+ }
148
+ } catch {
149
+ // Ignore lock file errors
150
+ }
151
+ }
152
+
153
+ // Re-download from registry if requested
154
+ if (restore) {
155
+ const manifestPath = path.resolve(argv.manifest);
156
+ const manifest = readManifest(manifestPath);
157
+
158
+ const rawDeps = manifest.dependencies || manifest.Dependencies || {};
159
+ const deps = Array.isArray(rawDeps)
160
+ ? Object.fromEntries(rawDeps.map((d) => [d.Name ?? d.name, d.Version ?? d.version]))
161
+ : rawDeps;
162
+
163
+ if (!deps[name]) {
164
+ console.warn(`[wyvrn] Warning: "${name}" is not in manifest dependencies, cannot restore`);
165
+ return;
166
+ }
167
+
168
+ // Get package sources
169
+ let packageSources;
170
+ if (argv.source && argv.source.length > 0) {
171
+ packageSources = argv.source;
172
+ } else {
173
+ const config = readConfig();
174
+ packageSources = config.installSources.map((s) => s.url);
175
+ }
176
+
177
+ if (packageSources.length === 0) {
178
+ console.warn('[wyvrn] Warning: no sources configured, cannot restore package');
179
+ return;
180
+ }
181
+
182
+ // Resolve just this one dependency
183
+ const resolvedDeps = await resolveDependencies(
184
+ { [name]: deps[name] },
185
+ packageSources,
186
+ argv.platform || 'win_x64',
187
+ fetch,
188
+ new Map(),
189
+ );
190
+
191
+ // Download it
192
+ await downloadDependencies(
193
+ resolvedDeps,
194
+ packageSources,
195
+ argv.platform || 'win_x64',
196
+ razerDir,
197
+ fetch,
198
+ argv.timeout || 300,
199
+ );
200
+
201
+ // Update lock file with restored version
202
+ if (fs.existsSync(lockPath)) {
203
+ try {
204
+ const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
205
+ for (const [depName, version] of resolvedDeps) {
206
+ lockData.packages[depName] = version;
207
+ }
208
+ lockData.generatedAt = new Date().toISOString();
209
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + '\n', 'utf8');
210
+ } catch {
211
+ // Ignore lock file errors
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * List all globally registered packages.
219
+ */
220
+ function listLinked() {
221
+ const config = readConfig();
222
+ const linked = config.linkedPackages || {};
223
+ const entries = Object.entries(linked);
224
+
225
+ if (entries.length === 0) {
226
+ console.log('[wyvrn] No globally registered packages');
227
+ return;
228
+ }
229
+
230
+ console.log('[wyvrn] Globally registered packages:');
231
+ for (const [name, rawEntry] of entries) {
232
+ const entry = normalizeLinkedEntry(rawEntry);
233
+ const effectivePath = getEffectivePath(entry);
234
+ const exists = fs.existsSync(effectivePath);
235
+ const status = exists ? '' : ' (path not found)';
236
+ const subdirInfo = entry.subdir ? ` [subdir: ${entry.subdir}]` : '';
237
+ console.log(` ${name} → ${effectivePath}${subdirInfo}${status}`);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Main link command handler.
243
+ *
244
+ * @param {object} argv - Parsed yargs arguments.
245
+ */
246
+ async function link(argv) {
247
+ const rootDir = path.resolve(argv.root);
248
+ const manifestPath = path.resolve(argv.manifest);
249
+
250
+ // wyvrnpm link --list
251
+ if (argv.list) {
252
+ listLinked();
253
+ return;
254
+ }
255
+
256
+ // wyvrnpm link (no args) - register current package globally
257
+ if (!argv.name) {
258
+ if (!fs.existsSync(manifestPath)) {
259
+ console.error(`[wyvrn] Error: no wyvrn.json found at ${manifestPath}`);
260
+ process.exit(1);
261
+ }
262
+ registerGlobal(manifestPath, argv.subdir);
263
+ return;
264
+ }
265
+
266
+ // wyvrnpm link <name> <path> - direct path link
267
+ if (argv.name && argv.path) {
268
+ let targetPath = argv.path;
269
+ if (argv.subdir) {
270
+ targetPath = path.join(argv.path, argv.subdir);
271
+ }
272
+ linkToProject(argv.name, targetPath, rootDir);
273
+ return;
274
+ }
275
+
276
+ // wyvrnpm link <name> - link from global registry
277
+ const config = readConfig();
278
+ const linked = config.linkedPackages || {};
279
+
280
+ if (!linked[argv.name]) {
281
+ console.error(`[wyvrn] Error: "${argv.name}" is not registered globally`);
282
+ console.error('[wyvrn] Run "wyvrnpm link" in the package directory first, or use:');
283
+ console.error(`[wyvrn] wyvrnpm link ${argv.name} <path>`);
284
+ process.exit(1);
285
+ }
286
+
287
+ const entry = normalizeLinkedEntry(linked[argv.name]);
288
+ // Command-line --subdir overrides the registered subdir
289
+ let effectivePath;
290
+ if (argv.subdir) {
291
+ effectivePath = path.join(entry.path, argv.subdir);
292
+ } else {
293
+ effectivePath = getEffectivePath(entry);
294
+ }
295
+
296
+ linkToProject(argv.name, effectivePath, rootDir);
297
+ }
298
+
299
+ /**
300
+ * Unlink command handler.
301
+ *
302
+ * @param {object} argv - Parsed yargs arguments.
303
+ */
304
+ async function unlink(argv) {
305
+ const rootDir = path.resolve(argv.root);
306
+ await unlinkFromProject(argv.name, rootDir, argv.restore, argv);
307
+ }
308
+
309
+ module.exports = {
310
+ link,
311
+ unlink,
312
+ listLinked,
313
+ registerGlobal,
314
+ linkToProject,
315
+ unlinkFromProject,
316
+ };
@@ -1,167 +1,180 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
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
- */
15
- function loadIgnorePatterns(ignoreFile) {
16
- if (!fs.existsSync(ignoreFile)) return [];
17
- return fs
18
- .readFileSync(ignoreFile, 'utf8')
19
- .split(/\r?\n/)
20
- .map((l) => l.trim())
21
- .filter((l) => l.length > 0 && !l.startsWith('#'));
22
- }
23
-
24
- /**
25
- * Convert a glob-style ignore pattern to a RegExp.
26
- * Supports '*', '**', '?' wildcards and leading '/' to anchor to root.
27
- */
28
- function patternToRegex(pattern) {
29
- // A leading slash means "anchored to root"; strip it and keep the rest
30
- const anchored = pattern.startsWith('/');
31
- const p = anchored ? pattern.slice(1) : pattern;
32
-
33
- 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
41
- return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
42
- }
43
-
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
- function addFolderFiltered(zip, dir, base, ignoreRe) {
54
- for (const entry of fs.readdirSync(dir)) {
55
- 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
-
59
- if (ignoreRe.some((re) => re.test(relPath))) {
60
- console.log(`[wyvrn] Ignoring: ${relPath}`);
61
- continue;
62
- }
63
-
64
- const stat = fs.statSync(fullPath);
65
- if (stat.isDirectory()) {
66
- addFolderFiltered(zip, fullPath, base, ignoreRe);
67
- } else {
68
- const zipDir = path.dirname(relPath).replace(/\\/g, '/');
69
- zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
70
- }
71
- }
72
- }
73
-
74
- async function publish(argv) {
75
- let { source, profile, platform, path: publishPath, token, manifest: manifestArg } = argv;
76
-
77
- // Resolve source + auth: CLI > config entry by name > first config entry > raw URL
78
- if (source) {
79
- // Check if --source matches a named config entry rather than a raw URL/URI
80
- const config = readConfig();
81
- const named = config.publishSources.find((s) => s.name === source);
82
- if (named) {
83
- console.log(`[wyvrn] Using publish source "${named.name}" from config`);
84
- source = named.url;
85
- profile = profile ?? named.profile;
86
- token = token ?? named.token;
87
- }
88
- } else {
89
- // No --source given — fall back to first configured publish source
90
- const config = readConfig();
91
- if (config.publishSources.length === 0) {
92
- console.error(
93
- '[wyvrn] Error: --source is required (or configure one with: ' +
94
- 'wyvrnpm configure add-source --kind publish ...)',
95
- );
96
- process.exit(1);
97
- }
98
- const first = config.publishSources[0];
99
- console.log(`[wyvrn] Using publish source "${first.name}" from config`);
100
- source = first.url;
101
- profile = profile ?? first.profile;
102
- token = token ?? first.token;
103
- }
104
-
105
- // Resolve and read the manifest
106
- const manifestPath = path.resolve(manifestArg || './wyvrn.json');
107
- const manifest = await readManifest(manifestPath);
108
- if (!manifest) {
109
- console.error(`[wyvrn] Error: Could not read manifest at ${manifestPath}`);
110
- process.exit(1);
111
- }
112
-
113
- const { name, version } = manifest;
114
- if (!name || !version) {
115
- console.error('[wyvrn] Error: Manifest must have "name" and "version" fields');
116
- process.exit(1);
117
- }
118
-
119
- const targetPlatform = platform || 'common';
120
- const srcDir = path.resolve(publishPath || '.');
121
-
122
- if (!fs.existsSync(srcDir)) {
123
- console.error(`[wyvrn] Error: Publish path does not exist: ${srcDir}`);
124
- process.exit(1);
125
- }
126
-
127
- // Resolve provider from source URL/URI
128
- let provider;
129
- try {
130
- provider = getProvider(source);
131
- } catch (err) {
132
- console.error(`[wyvrn] Error: ${err.message}`);
133
- process.exit(1);
134
- }
135
-
136
- console.log(
137
- `[wyvrn] Publishing ${name}@${version} (${targetPlatform}) via ${provider.constructor.providerName} provider`,
138
- );
139
-
140
- // Load .wyvrnignore patterns from the publish directory
141
- const ignoreFile = path.join(srcDir, '.wyvrnignore');
142
- const ignorePatterns = loadIgnorePatterns(ignoreFile);
143
- const ignoreRe = ignorePatterns.map(patternToRegex);
144
- if (ignorePatterns.length > 0) {
145
- console.log(`[wyvrn] Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
146
- }
147
-
148
- // Build a temporary zip of the publish directory
149
- const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
150
- try {
151
- console.log(`[wyvrn] Zipping ${srcDir} ...`);
152
- const zip = new AdmZip();
153
- addFolderFiltered(zip, srcDir, srcDir, ignoreRe);
154
- zip.writeZip(tmpZipPath);
155
-
156
- await provider.publish(
157
- { manifest: manifestPath, zip: tmpZipPath },
158
- { source, platform: targetPlatform, name, version, profile, token },
159
- );
160
-
161
- console.log(`[wyvrn] Successfully published ${name}@${version} to ${source}`);
162
- } finally {
163
- if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
164
- }
165
- }
166
-
167
- module.exports = publish;
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
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
+ */
15
+ function loadIgnorePatterns(ignoreFile) {
16
+ if (!fs.existsSync(ignoreFile)) return [];
17
+ return fs
18
+ .readFileSync(ignoreFile, 'utf8')
19
+ .split(/\r?\n/)
20
+ .map((l) => l.trim())
21
+ .filter((l) => l.length > 0 && !l.startsWith('#'));
22
+ }
23
+
24
+ /**
25
+ * Convert a glob-style ignore pattern to a RegExp.
26
+ * Supports '*', '**', '?' wildcards and leading '/' to anchor to root.
27
+ */
28
+ function patternToRegex(pattern) {
29
+ // A leading slash means "anchored to root"; strip it and keep the rest
30
+ const anchored = pattern.startsWith('/');
31
+ const p = anchored ? pattern.slice(1) : pattern;
32
+
33
+ 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
41
+ return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
42
+ }
43
+
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
+ function addFolderFiltered(zip, dir, base, ignoreRe) {
54
+ for (const entry of fs.readdirSync(dir)) {
55
+ 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
+
59
+ if (ignoreRe.some((re) => re.test(relPath))) {
60
+ console.log(`[wyvrn] Ignoring: ${relPath}`);
61
+ continue;
62
+ }
63
+
64
+ const stat = fs.statSync(fullPath);
65
+ if (stat.isDirectory()) {
66
+ addFolderFiltered(zip, fullPath, base, ignoreRe);
67
+ } else {
68
+ const zipDir = path.dirname(relPath).replace(/\\/g, '/');
69
+ zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
70
+ }
71
+ }
72
+ }
73
+
74
+ async function publish(argv) {
75
+ let { source, profile, platform, path: publishPath, token, manifest: manifestArg, force } = argv;
76
+
77
+ // Resolve source + auth: CLI > config entry by name > first config entry > raw URL
78
+ if (source) {
79
+ // Check if --source matches a named config entry rather than a raw URL/URI
80
+ const config = readConfig();
81
+ const named = config.publishSources.find((s) => s.name === source);
82
+ if (named) {
83
+ console.log(`[wyvrn] Using publish source "${named.name}" from config`);
84
+ source = named.url;
85
+ profile = profile ?? named.profile;
86
+ token = token ?? named.token;
87
+ }
88
+ } else {
89
+ // No --source given — fall back to first configured publish source
90
+ const config = readConfig();
91
+ if (config.publishSources.length === 0) {
92
+ console.error(
93
+ '[wyvrn] Error: --source is required (or configure one with: ' +
94
+ 'wyvrnpm configure add-source --kind publish ...)',
95
+ );
96
+ process.exit(1);
97
+ }
98
+ const first = config.publishSources[0];
99
+ console.log(`[wyvrn] Using publish source "${first.name}" from config`);
100
+ source = first.url;
101
+ profile = profile ?? first.profile;
102
+ token = token ?? first.token;
103
+ }
104
+
105
+ // Resolve and read the manifest
106
+ const manifestPath = path.resolve(manifestArg || './wyvrn.json');
107
+ const manifest = await readManifest(manifestPath);
108
+ if (!manifest) {
109
+ console.error(`[wyvrn] Error: Could not read manifest at ${manifestPath}`);
110
+ process.exit(1);
111
+ }
112
+
113
+ const { name, version } = manifest;
114
+ if (!name || !version) {
115
+ console.error('[wyvrn] Error: Manifest must have "name" and "version" fields');
116
+ process.exit(1);
117
+ }
118
+
119
+ const targetPlatform = platform || 'common';
120
+ const srcDir = path.resolve(publishPath || '.');
121
+
122
+ if (!fs.existsSync(srcDir)) {
123
+ console.error(`[wyvrn] Error: Publish path does not exist: ${srcDir}`);
124
+ process.exit(1);
125
+ }
126
+
127
+ // Resolve provider from source URL/URI
128
+ let provider;
129
+ try {
130
+ provider = getProvider(source);
131
+ } catch (err) {
132
+ console.error(`[wyvrn] Error: ${err.message}`);
133
+ process.exit(1);
134
+ }
135
+
136
+ console.log(
137
+ `[wyvrn] Publishing ${name}@${version} (${targetPlatform}) via ${provider.constructor.providerName} provider`,
138
+ );
139
+
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) {
143
+ if (force) {
144
+ console.warn(`[wyvrn] Warning: ${name}@${version} already exists — overwriting (--force)`);
145
+ } else {
146
+ console.error(
147
+ `[wyvrn] Error: ${name}@${version} already exists. Bump the version or use --force to overwrite.`,
148
+ );
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ // Load .wyvrnignore patterns from the publish directory
154
+ const ignoreFile = path.join(srcDir, '.wyvrnignore');
155
+ const ignorePatterns = loadIgnorePatterns(ignoreFile);
156
+ const ignoreRe = ignorePatterns.map(patternToRegex);
157
+ if (ignorePatterns.length > 0) {
158
+ console.log(`[wyvrn] Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
159
+ }
160
+
161
+ // Build a temporary zip of the publish directory
162
+ const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
163
+ try {
164
+ console.log(`[wyvrn] Zipping ${srcDir} ...`);
165
+ const zip = new AdmZip();
166
+ addFolderFiltered(zip, srcDir, srcDir, ignoreRe);
167
+ zip.writeZip(tmpZipPath);
168
+
169
+ await provider.publish(
170
+ { manifest: manifestPath, zip: tmpZipPath },
171
+ { source, platform: targetPlatform, name, version, profile, token },
172
+ );
173
+
174
+ console.log(`[wyvrn] Successfully published ${name}@${version} to ${source}`);
175
+ } finally {
176
+ if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
177
+ }
178
+ }
179
+
180
+ module.exports = publish;