wyvrnpm 1.0.1 → 1.1.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # wyvrnpm
2
2
 
3
- A simple, private C++ package manager that works with any static file hosting provider — Amazon S3, Azure Blob Storage, a plain nginx server, or anything that can serve files over HTTPS.
3
+ A simple, private C++ package manager that works with any static file hosting provider — Amazon S3, a plain HTTP/HTTPS server, a local directory, or an SMB/UNC network share.
4
4
 
5
5
  There is no central registry. You control where packages are hosted.
6
6
 
@@ -14,47 +14,121 @@ npm install -g wyvrnpm
14
14
 
15
15
  Requires Node.js >= 18.
16
16
 
17
+ For S3 publishing, also install the AWS SDK:
18
+
19
+ ```bash
20
+ npm install -g @aws-sdk/client-s3 @aws-sdk/credential-providers
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # 1. Initialise a manifest in your project
29
+ wyvrnpm init
30
+
31
+ # 2. Configure your package sources once
32
+ wyvrnpm configure add-source --kind install --name corp-s3 --url s3://my-bucket/packages --profile my-sso
33
+ wyvrnpm configure add-source --kind publish --name default --url s3://my-bucket/packages --profile my-sso
34
+
35
+ # 3. Install dependencies (uses configured sources automatically)
36
+ wyvrnpm install --platform win_x64
37
+
38
+ # 4. Publish your package
39
+ wyvrnpm publish --platform win_x64
40
+ ```
41
+
17
42
  ---
18
43
 
19
44
  ## Commands
20
45
 
21
46
  ### `wyvrnpm init`
22
47
 
23
- Creates a `wyvrn.json` manifest in the current directory (or the directory given by `--root`).
48
+ Creates a `wyvrn.json` manifest in the current directory.
24
49
 
25
50
  ```bash
26
51
  wyvrnpm init
27
52
  wyvrnpm init --root ./my-project
28
53
  ```
29
54
 
55
+ ---
56
+
30
57
  ### `wyvrnpm install`
31
58
 
32
59
  Resolves the full dependency graph and downloads all packages.
33
60
 
61
+ **Source priority:** CLI `--source` values are used if provided. If omitted, sources are loaded from the [configuration file](#configuration-file).
62
+
34
63
  ```bash
35
- wyvrnpm install --source https://my-bucket.s3.amazonaws.com/cpp --platform win_x64
64
+ # Use configured sources (set up via wyvrnpm configure)
65
+ wyvrnpm install --platform win_x64
66
+
67
+ # Override with explicit sources — ignores config
68
+ wyvrnpm install --source https://pkg.example.com/cpp --platform win_x64
36
69
 
37
70
  # Multiple sources — tried in order, first to respond wins
38
71
  wyvrnpm install \
39
72
  --source https://primary.example.com/cpp \
40
73
  --source https://mirror.example.com/cpp \
41
74
  --platform linux_x64
42
-
43
- # Aliases
44
- wyvrnpm install -s https://pkg.example.com/cpp -s https://mirror.example.com/cpp
45
75
  ```
46
76
 
47
77
  **Options**
48
78
 
49
79
  | Option | Default | Description |
50
80
  |---|---|---|
51
- | `--source` / `-s` | | Base URL of a package source. Repeat for multiple sources. |
52
- | `--platform` | `win_x64` | Target platform. One of: `win_x64`, `win_x86`, `linux_x64`, `linux_x86`, `osx_x64`, `osx_arm64` |
81
+ | `--source` / `-s` | *(config)* | Base URL of a package source. Repeat for multiple. Overrides config when provided. |
82
+ | `--platform` | `win_x64` | Target platform: `win_x64` `win_x86` `linux_x64` `linux_x86` `osx_x64` `osx_arm64` |
53
83
  | `--timeout` | `300` | HTTP request timeout in seconds |
54
84
  | `--manifest` | `./wyvrn.json` | Path to the manifest file |
55
85
  | `--root` | `./` | Project root (packages extracted to `{root}/wyvrn_internal/`) |
56
86
 
57
- Packages are extracted to `wyvrn_internal/{name}/` and a `wyvrn.lock` file is written alongside the manifest recording the exact resolved versions. On subsequent installs the lock pins all previously resolved versions — only newly added dependencies are resolved fresh.
87
+ Packages are extracted to `wyvrn_internal/{name}/`. A `wyvrn.lock` file is written alongside the manifest recording the exact resolved versions. On subsequent installs the lock file pins all previously resolved versions — only newly added dependencies are resolved fresh.
88
+
89
+ ---
90
+
91
+ ### `wyvrnpm publish`
92
+
93
+ Zips the project directory and uploads `wyvrn.json` + `wyvrn.zip` to the destination source.
94
+
95
+ **Source priority:** CLI `--source` is used if provided (as a URL/URI or a configured source name). If omitted, the first configured publish source is used.
96
+
97
+ ```bash
98
+ # Use the first configured publish source
99
+ wyvrnpm publish --platform win_x64
100
+
101
+ # Reference a configured source by name
102
+ wyvrnpm publish --source default --platform win_x64
103
+
104
+ # Explicit destination URL — ignores config
105
+ wyvrnpm publish --source s3://my-bucket/packages --profile my-sso --platform win_x64
106
+
107
+ # HTTP server with token auth
108
+ wyvrnpm publish --source https://pkg.corp.com --token mytoken --platform linux_x64
109
+
110
+ # Local directory or SMB share
111
+ wyvrnpm publish --source \\fileserver\packages --platform win_x64
112
+ wyvrnpm publish --source /mnt/packages --platform linux_x64
113
+
114
+ # Publish build artifacts from a specific directory
115
+ wyvrnpm publish --path ./dist --platform win_x64
116
+ ```
117
+
118
+ **Options**
119
+
120
+ | Option | Default | Description |
121
+ |---|---|---|
122
+ | `--source` / `-s` | *(config)* | Destination URL/URI or configured source name. Overrides config when provided. |
123
+ | `--profile` / `-p` | *(config)* | AWS SSO profile for S3 authentication. Overrides config when provided. |
124
+ | `--token` | *(config)* | Bearer token for HTTP server authentication. Overrides config when provided. |
125
+ | `--platform` | `common` | Target platform sub-path: `win_x64` `win_x86` `linux_x64` `linux_x86` `osx_x64` `osx_arm64` `common` |
126
+ | `--path` | `.` | Directory to zip and publish |
127
+ | `--manifest` | `./wyvrn.json` | Path to the manifest file |
128
+
129
+ Files are uploaded to `{source}/{platform}/{name}/{version}/wyvrn.json` and `wyvrn.zip`.
130
+
131
+ ---
58
132
 
59
133
  ### `wyvrnpm clean`
60
134
 
@@ -66,25 +140,225 @@ wyvrnpm clean
66
140
 
67
141
  ---
68
142
 
143
+ ### `wyvrnpm configure`
144
+
145
+ Manages the [configuration file](#configuration-file). All sub-commands read and write `config.json` in the platform config directory.
146
+
147
+ #### `wyvrnpm configure list`
148
+
149
+ Prints all configured sources.
150
+
151
+ ```
152
+ Config: C:\Users\you\AppData\Local\wyvrnpm\config.json
153
+
154
+ Install Sources:
155
+ corp-s3 s3://my-bucket/packages [profile: my-sso]
156
+ fallback https://pkg.corp.com [token: ***]
157
+
158
+ Publish Sources:
159
+ default s3://my-bucket/packages [profile: my-sso]
160
+ ```
161
+
162
+ #### `wyvrnpm configure add-source`
163
+
164
+ Adds a new source or updates an existing one with the same name.
165
+
166
+ ```bash
167
+ # S3 with AWS SSO profile (install)
168
+ wyvrnpm configure add-source \
169
+ --kind install \
170
+ --name corp-s3 \
171
+ --url s3://my-bucket/packages \
172
+ --profile my-sso-profile
173
+
174
+ # HTTPS server with token auth (install)
175
+ wyvrnpm configure add-source \
176
+ --kind install \
177
+ --name fallback \
178
+ --url https://pkg.corp.com \
179
+ --token mytoken
180
+
181
+ # Local or SMB path (no auth required)
182
+ wyvrnpm configure add-source \
183
+ --kind install \
184
+ --name local \
185
+ --url \\fileserver\packages
186
+
187
+ # Publish destination
188
+ wyvrnpm configure add-source \
189
+ --kind publish \
190
+ --name default \
191
+ --url s3://my-bucket/packages \
192
+ --profile my-sso-profile
193
+ ```
194
+
195
+ **Options**
196
+
197
+ | Option | Required | Description |
198
+ |---|---|---|
199
+ | `--kind` | Yes | `install` or `publish` |
200
+ | `--name` | Yes | Unique name for this source |
201
+ | `--url` | Yes | Destination URL, URI, or file path |
202
+ | `--profile` | No | AWS SSO profile (S3 sources) |
203
+ | `--token` | No | Bearer token (HTTP sources) |
204
+
205
+ #### `wyvrnpm configure remove-source`
206
+
207
+ Removes a source by name.
208
+
209
+ ```bash
210
+ wyvrnpm configure remove-source --kind install --name fallback
211
+ wyvrnpm configure remove-source --kind publish --name default
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Configuration File
217
+
218
+ wyvrnpm stores persistent configuration in a `config.json` file:
219
+
220
+ | Platform | Path |
221
+ |---|---|
222
+ | Windows | `%LOCALAPPDATA%\wyvrnpm\config.json` |
223
+ | Linux / macOS | `~/.config/wyvrnpm/config.json` |
224
+
225
+ **Example config.json**
226
+
227
+ ```json
228
+ {
229
+ "installSources": [
230
+ {
231
+ "name": "corp-s3",
232
+ "url": "s3://my-bucket/packages",
233
+ "profile": "my-sso-profile"
234
+ },
235
+ {
236
+ "name": "fallback",
237
+ "url": "https://pkg.corp.com",
238
+ "token": "mytoken"
239
+ }
240
+ ],
241
+ "publishSources": [
242
+ {
243
+ "name": "default",
244
+ "url": "s3://my-bucket/packages",
245
+ "profile": "my-sso-profile"
246
+ }
247
+ ]
248
+ }
249
+ ```
250
+
251
+ **CLI options always take priority over config.** Config values are only used when the corresponding CLI option is not provided.
252
+
253
+ ---
254
+
255
+ ## Publish Providers
256
+
257
+ wyvrnpm uses a provider architecture to support different destination types. The correct provider is selected automatically based on the source URL format.
258
+
259
+ | Provider | Detected from | Auth |
260
+ |---|---|---|
261
+ | **S3** | `s3://` URI, or S3 HTTPS endpoint | `--profile` (AWS SSO) |
262
+ | **HTTP** | `http://` or `https://` | `--token` (Bearer) |
263
+ | **File** | Local path or UNC/SMB share | none |
264
+
265
+ ### S3 Provider
266
+
267
+ Accepts any of these source formats:
268
+
269
+ ```
270
+ s3://bucket-name/optional/prefix
271
+ https://bucket-name.s3.amazonaws.com/prefix
272
+ https://bucket-name.s3.us-east-1.amazonaws.com/prefix
273
+ https://s3.amazonaws.com/bucket-name/prefix
274
+ https://s3.us-east-1.amazonaws.com/bucket-name/prefix
275
+ ```
276
+
277
+ Authentication uses AWS SSO via `--profile`. If no profile is given, the default AWS credential chain is used.
278
+
279
+ ### HTTP Provider
280
+
281
+ Accepts `http://` and `https://` URLs. Uploads files via HTTP `PUT`. Optionally authenticates with a Bearer token via `--token`.
282
+
283
+ ### File Provider
284
+
285
+ Accepts local directory paths and SMB/UNC network shares. Files are copied with no authentication required.
286
+
287
+ ```
288
+ /absolute/unix/path
289
+ ./relative/path
290
+ C:\Windows\path
291
+ \\server\share\path
292
+ //server/share/path
293
+ file:///absolute/path
294
+ ```
295
+
296
+ ### Adding a Custom Provider
297
+
298
+ 1. Create `src/providers/myprovider.js` extending `BaseProvider`:
299
+
300
+ ```js
301
+ 'use strict';
302
+ const BaseProvider = require('./base');
303
+
304
+ class MyProvider extends BaseProvider {
305
+ static canHandle(source) {
306
+ return source.startsWith('myscheme://');
307
+ }
308
+
309
+ static get providerName() { return 'MyProvider'; }
310
+
311
+ async publish(files, options) {
312
+ // files.manifest — path to wyvrn.json
313
+ // files.zip — path to wyvrn.zip
314
+ // options.source, options.platform, options.name, options.version
315
+ }
316
+ }
317
+
318
+ module.exports = MyProvider;
319
+ ```
320
+
321
+ 2. Register it in `src/providers/index.js`:
322
+
323
+ ```js
324
+ const MyProvider = require('./myprovider');
325
+
326
+ const PROVIDERS = [
327
+ S3Provider,
328
+ HttpProvider,
329
+ FileProvider,
330
+ MyProvider, // add here
331
+ ];
332
+ ```
333
+
334
+ ---
335
+
69
336
  ## Registry Layout
70
337
 
71
- Packages must be hosted at the following URL structure:
338
+ Packages must be hosted at the following path structure relative to the source base:
72
339
 
73
340
  ```
74
- {source}/{platform}/{name}/{version}/wyvrn.json <- package manifest (preferred)
75
- {source}/{platform}/{name}/{version}/wyvrn.zip <- package archive (preferred)
341
+ {source}/{platform}/{name}/{version}/wyvrn.json <- package manifest
342
+ {source}/{platform}/{name}/{version}/wyvrn.zip <- package archive
76
343
  ```
77
344
 
78
- The tool tries `wyvrn.json` / `wyvrn.zip` first and falls back to `razer.json` / `razer.zip` if not found, for backwards compatibility.
345
+ The tool also falls back to `razer.json` / `razer.zip` and a `common/` platform path for backwards compatibility.
346
+
347
+ **Example (S3):**
348
+
349
+ ```
350
+ s3://my-bucket/packages/win_x64/OpenSSL/3.0.0.0/wyvrn.json
351
+ s3://my-bucket/packages/win_x64/OpenSSL/3.0.0.0/wyvrn.zip
352
+ ```
79
353
 
80
- **Example:**
354
+ **Example (HTTP):**
81
355
 
82
356
  ```
83
- https://my-bucket.s3.amazonaws.com/cpp/win_x64/OpenSSL/3.0.0.0/wyvrn.json
84
- https://my-bucket.s3.amazonaws.com/cpp/win_x64/OpenSSL/3.0.0.0/wyvrn.zip
357
+ https://pkg.corp.com/win_x64/OpenSSL/3.0.0.0/wyvrn.json
358
+ https://pkg.corp.com/win_x64/OpenSSL/3.0.0.0/wyvrn.zip
85
359
  ```
86
360
 
87
- ### Package manifest format (wyvrn.json / razer.json)
361
+ ### Package manifest (wyvrn.json hosted on source)
88
362
 
89
363
  ```json
90
364
  {
package/bin/wyvrn.js CHANGED
@@ -6,6 +6,8 @@ const yargs = require('yargs');
6
6
  const init = require('../src/commands/init');
7
7
  const install = require('../src/commands/install');
8
8
  const clean = require('../src/commands/clean');
9
+ const publish = require('../src/commands/publish');
10
+ const configure = require('../src/commands/configure');
9
11
 
10
12
  yargs
11
13
  .option('manifest', {
@@ -55,6 +57,101 @@ yargs
55
57
  () => {},
56
58
  (argv) => clean(argv),
57
59
  )
60
+ .command(
61
+ 'publish',
62
+ 'Package and publish the project to a source (S3 or HTTP server)',
63
+ (yargs) => {
64
+ yargs
65
+ .option('source', {
66
+ alias: 's',
67
+ type: 'string',
68
+ description: 'Publish destination URL/URI or a configured source name (falls back to config)',
69
+ })
70
+ .option('profile', {
71
+ alias: 'p',
72
+ type: 'string',
73
+ description: 'AWS SSO profile to use for authentication (S3 only)',
74
+ })
75
+ .option('platform', {
76
+ type: 'string',
77
+ description: 'Target platform sub-path (e.g. win_x64). Defaults to "common"',
78
+ choices: ['win_x64', 'win_x86', 'linux_x64', 'linux_x86', 'osx_x64', 'osx_arm64', 'common'],
79
+ default: 'common',
80
+ })
81
+ .option('path', {
82
+ type: 'string',
83
+ description: 'Directory to zip and publish (defaults to current directory)',
84
+ default: '.',
85
+ })
86
+ .option('token', {
87
+ type: 'string',
88
+ description: 'Bearer token for HTTP server authentication (HTTP only)',
89
+ });
90
+ },
91
+ (argv) => publish(argv),
92
+ )
93
+ .command(
94
+ 'configure',
95
+ 'Manage wyvrnpm configuration (sources, authentication)',
96
+ (yargs) => {
97
+ yargs
98
+ .command('list', 'List all configured sources', () => {}, () => configure.list())
99
+ .command(
100
+ 'add-source',
101
+ 'Add or update an install/publish source',
102
+ (yargs) => {
103
+ yargs
104
+ .option('kind', {
105
+ type: 'string',
106
+ description: 'Source kind',
107
+ choices: ['install', 'publish'],
108
+ demandOption: true,
109
+ })
110
+ .option('name', {
111
+ type: 'string',
112
+ description: 'Unique name for this source',
113
+ demandOption: true,
114
+ })
115
+ .option('url', {
116
+ type: 'string',
117
+ description: 'Source URL or URI (s3://, https://, UNC path, etc.)',
118
+ demandOption: true,
119
+ })
120
+ .option('profile', {
121
+ type: 'string',
122
+ description: 'AWS SSO profile for this source (S3 only)',
123
+ })
124
+ .option('token', {
125
+ type: 'string',
126
+ description: 'Bearer token for this source (HTTP only)',
127
+ });
128
+ },
129
+ (argv) => configure.addSource(argv),
130
+ )
131
+ .command(
132
+ 'remove-source',
133
+ 'Remove a configured source by name',
134
+ (yargs) => {
135
+ yargs
136
+ .option('kind', {
137
+ type: 'string',
138
+ description: 'Source kind',
139
+ choices: ['install', 'publish'],
140
+ demandOption: true,
141
+ })
142
+ .option('name', {
143
+ type: 'string',
144
+ description: 'Name of the source to remove',
145
+ demandOption: true,
146
+ });
147
+ },
148
+ (argv) => configure.removeSource(argv),
149
+ )
150
+ .demandCommand(1)
151
+ .help();
152
+ },
153
+ () => {},
154
+ )
58
155
  .demandCommand(1)
59
156
  .strict()
60
157
  .help()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyvrnpm",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
@@ -26,7 +26,10 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "adm-zip": "^0.5.16",
29
- "yargs": "^17.7.2"
29
+ "node-stream-zip": "^1.15.0",
30
+ "yargs": "^17.7.2",
31
+ "@aws-sdk/client-s3": "^3.0.0",
32
+ "@aws-sdk/credential-providers": "^3.0.0"
30
33
  },
31
34
  "engines": {
32
35
  "node": ">=18.0.0"
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const { readConfig, writeConfig, getConfigPath } = require('../config');
4
+
5
+ /** wyvrnpm configure list */
6
+ function list() {
7
+ const config = readConfig();
8
+
9
+ console.log(`Config: ${getConfigPath()}\n`);
10
+
11
+ printSection('Install Sources', config.installSources);
12
+ console.log('');
13
+ printSection('Publish Sources', config.publishSources);
14
+ }
15
+
16
+ function printSection(title, sources) {
17
+ console.log(`${title}:`);
18
+ if (sources.length === 0) {
19
+ console.log(' (none)');
20
+ return;
21
+ }
22
+ for (const src of sources) {
23
+ const auth = authLabel(src);
24
+ console.log(` ${src.name.padEnd(20)} ${src.url}${auth ? ' ' + auth : ''}`);
25
+ }
26
+ }
27
+
28
+ function authLabel(src) {
29
+ if (src.profile) return `[profile: ${src.profile}]`;
30
+ if (src.token) return '[token: ***]';
31
+ return '';
32
+ }
33
+
34
+ /**
35
+ * wyvrnpm configure add-source --kind install|publish --name <name> --url <url>
36
+ * [--profile <profile>] [--token <token>]
37
+ */
38
+ function addSource(argv) {
39
+ const { kind, name, url, profile, token } = argv;
40
+ const config = readConfig();
41
+ const key = kind === 'install' ? 'installSources' : 'publishSources';
42
+
43
+ const entry = { name, url };
44
+ if (profile) entry.profile = profile;
45
+ if (token) entry.token = token;
46
+
47
+ const idx = config[key].findIndex((s) => s.name === name);
48
+ if (idx !== -1) {
49
+ config[key][idx] = entry;
50
+ console.log(`[wyvrn] Updated ${kind} source "${name}"`);
51
+ } else {
52
+ config[key].push(entry);
53
+ console.log(`[wyvrn] Added ${kind} source "${name}"`);
54
+ }
55
+
56
+ writeConfig(config);
57
+ console.log(`[wyvrn] Saved to ${getConfigPath()}`);
58
+ }
59
+
60
+ /**
61
+ * wyvrnpm configure remove-source --kind install|publish --name <name>
62
+ */
63
+ function removeSource(argv) {
64
+ const { kind, name } = argv;
65
+ const config = readConfig();
66
+ const key = kind === 'install' ? 'installSources' : 'publishSources';
67
+
68
+ const before = config[key].length;
69
+ config[key] = config[key].filter((s) => s.name !== name);
70
+
71
+ if (config[key].length === before) {
72
+ console.error(`[wyvrn] Error: No ${kind} source named "${name}"`);
73
+ process.exit(1);
74
+ }
75
+
76
+ writeConfig(config);
77
+ console.log(`[wyvrn] Removed ${kind} source "${name}"`);
78
+ }
79
+
80
+ module.exports = { list, addSource, removeSource };
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const { readManifest } = require('../manifest');
6
6
  const { resolveDependencies } = require('../resolve');
7
7
  const { downloadDependencies } = require('../download');
8
+ const { readConfig } = require('../config');
8
9
 
9
10
  /**
10
11
  * Resolves and downloads all dependencies declared in the manifest, then writes
@@ -22,12 +23,22 @@ async function install(argv) {
22
23
  const manifestPath = path.resolve(argv.manifest);
23
24
  const rootDir = path.resolve(argv.root);
24
25
 
25
- // Validate that at least one source is provided.
26
- const packageSources = argv.source && argv.source.length > 0 ? argv.source : [];
26
+ // CLI --source takes priority; fall back to config installSources.
27
+ let packageSources;
28
+ if (argv.source && argv.source.length > 0) {
29
+ packageSources = argv.source;
30
+ } else {
31
+ const config = readConfig();
32
+ packageSources = config.installSources.map((s) => s.url);
33
+ if (packageSources.length > 0) {
34
+ console.log(`[wyvrn] Using ${packageSources.length} source(s) from config`);
35
+ }
36
+ }
37
+
27
38
  if (packageSources.length === 0) {
28
39
  console.warn(
29
- '[wyvrn] Warning: no --source URLs provided. ' +
30
- 'Pass at least one with --source <url> or -s <url>.',
40
+ '[wyvrn] Warning: no sources configured. ' +
41
+ 'Pass --source <url> or run: wyvrnpm configure add-source --kind install ...',
31
42
  );
32
43
  }
33
44
 
@@ -0,0 +1,96 @@
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
+ async function publish(argv) {
12
+ let { source, profile, platform, path: publishPath, token, manifest: manifestArg } = argv;
13
+
14
+ // Resolve source + auth: CLI > config entry by name > first config entry > raw URL
15
+ if (source) {
16
+ // Check if --source matches a named config entry rather than a raw URL/URI
17
+ const config = readConfig();
18
+ const named = config.publishSources.find((s) => s.name === source);
19
+ if (named) {
20
+ console.log(`[wyvrn] Using publish source "${named.name}" from config`);
21
+ source = named.url;
22
+ profile = profile ?? named.profile;
23
+ token = token ?? named.token;
24
+ }
25
+ } else {
26
+ // No --source given — fall back to first configured publish source
27
+ const config = readConfig();
28
+ if (config.publishSources.length === 0) {
29
+ console.error(
30
+ '[wyvrn] Error: --source is required (or configure one with: ' +
31
+ 'wyvrnpm configure add-source --kind publish ...)',
32
+ );
33
+ process.exit(1);
34
+ }
35
+ const first = config.publishSources[0];
36
+ console.log(`[wyvrn] Using publish source "${first.name}" from config`);
37
+ source = first.url;
38
+ profile = profile ?? first.profile;
39
+ token = token ?? first.token;
40
+ }
41
+
42
+ // Resolve and read the manifest
43
+ const manifestPath = path.resolve(manifestArg || './wyvrn.json');
44
+ const manifest = await readManifest(manifestPath);
45
+ if (!manifest) {
46
+ console.error(`[wyvrn] Error: Could not read manifest at ${manifestPath}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ const { name, version } = manifest;
51
+ if (!name || !version) {
52
+ console.error('[wyvrn] Error: Manifest must have "name" and "version" fields');
53
+ process.exit(1);
54
+ }
55
+
56
+ const targetPlatform = platform || 'common';
57
+ const srcDir = path.resolve(publishPath || '.');
58
+
59
+ if (!fs.existsSync(srcDir)) {
60
+ console.error(`[wyvrn] Error: Publish path does not exist: ${srcDir}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ // Resolve provider from source URL/URI
65
+ let provider;
66
+ try {
67
+ provider = getProvider(source);
68
+ } catch (err) {
69
+ console.error(`[wyvrn] Error: ${err.message}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ console.log(
74
+ `[wyvrn] Publishing ${name}@${version} (${targetPlatform}) via ${provider.constructor.providerName} provider`,
75
+ );
76
+
77
+ // Build a temporary zip of the publish directory
78
+ const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
79
+ try {
80
+ console.log(`[wyvrn] Zipping ${srcDir} ...`);
81
+ const zip = new AdmZip();
82
+ zip.addLocalFolder(srcDir);
83
+ zip.writeZip(tmpZipPath);
84
+
85
+ await provider.publish(
86
+ { manifest: manifestPath, zip: tmpZipPath },
87
+ { source, platform: targetPlatform, name, version, profile, token },
88
+ );
89
+
90
+ console.log(`[wyvrn] Successfully published ${name}@${version} to ${source}`);
91
+ } finally {
92
+ if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
93
+ }
94
+ }
95
+
96
+ module.exports = publish;
package/src/config.js ADDED
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ /**
8
+ * Returns the directory where wyvrnpm stores its config.
9
+ * Windows : %LOCALAPPDATA%\wyvrnpm
10
+ * Other : ~/.config/wyvrnpm
11
+ */
12
+ function getConfigDir() {
13
+ if (process.env.LOCALAPPDATA) {
14
+ return path.join(process.env.LOCALAPPDATA, 'wyvrnpm');
15
+ }
16
+ return path.join(os.homedir(), '.config', 'wyvrnpm');
17
+ }
18
+
19
+ /** Full path to config.json. */
20
+ function getConfigPath() {
21
+ return path.join(getConfigDir(), 'config.json');
22
+ }
23
+
24
+ /**
25
+ * Read and parse config.json.
26
+ * Returns a default empty config if the file doesn't exist yet.
27
+ * @returns {{ installSources: SourceEntry[], publishSources: SourceEntry[] }}
28
+ */
29
+ function readConfig() {
30
+ const configPath = getConfigPath();
31
+ if (!fs.existsSync(configPath)) {
32
+ return { installSources: [], publishSources: [] };
33
+ }
34
+ try {
35
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
+ return {
37
+ installSources: raw.installSources ?? [],
38
+ publishSources: raw.publishSources ?? [],
39
+ };
40
+ } catch {
41
+ console.warn('[wyvrn] Warning: could not parse config.json — using defaults');
42
+ return { installSources: [], publishSources: [] };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Persist a config object to disk.
48
+ * @param {{ installSources: SourceEntry[], publishSources: SourceEntry[] }} config
49
+ */
50
+ function writeConfig(config) {
51
+ const dir = getConfigDir();
52
+ fs.mkdirSync(dir, { recursive: true });
53
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n', 'utf8');
54
+ }
55
+
56
+ module.exports = { getConfigDir, getConfigPath, readConfig, writeConfig };
57
+
58
+ /**
59
+ * @typedef {{ name: string, url: string, profile?: string, token?: string }} SourceEntry
60
+ */
package/src/download.js CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const AdmZip = require('adm-zip');
5
+ const { pipeline } = require('stream/promises');
6
+ const { Readable } = require('stream');
7
+ const StreamZip = require('node-stream-zip');
6
8
 
7
9
  /**
8
10
  * Downloads a single URL to a local file path, respecting the given timeout.
@@ -29,8 +31,9 @@ async function downloadFile(url, destPath, httpClient, timeoutMs) {
29
31
  throw new Error(`HTTP ${response.status} for ${url}`);
30
32
  }
31
33
 
32
- const arrayBuffer = await response.arrayBuffer();
33
- fs.writeFileSync(destPath, Buffer.from(arrayBuffer));
34
+ // Stream the response body directly to disk — avoids loading large files into RAM.
35
+ const fileStream = fs.createWriteStream(destPath);
36
+ await pipeline(Readable.fromWeb(response.body), fileStream);
34
37
  }
35
38
 
36
39
  /**
@@ -137,8 +140,10 @@ async function downloadDependencies(
137
140
  }
138
141
 
139
142
  try {
140
- const zip = new AdmZip(destZipPath);
141
- zip.extractAllTo(extractDir, /* overwrite */ true);
143
+ fs.mkdirSync(extractDir, { recursive: true });
144
+ const zip = new StreamZip.async({ file: destZipPath });
145
+ await zip.extract(null, extractDir);
146
+ await zip.close();
142
147
  fs.writeFileSync(versionFile, version, 'utf8');
143
148
  console.log(`[wyvrn] Extracted: ${name}@${version} → ${extractDir}`);
144
149
  } catch (err) {
package/src/index.js CHANGED
@@ -4,4 +4,6 @@ module.exports = {
4
4
  init: require('./commands/init'),
5
5
  install: require('./commands/install'),
6
6
  clean: require('./commands/clean'),
7
+ publish: require('./commands/publish'),
8
+ configure: require('./commands/configure'),
7
9
  };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base provider class. All publish providers must extend this.
5
+ *
6
+ * To add a new provider:
7
+ * 1. Create src/providers/<name>.js that extends BaseProvider
8
+ * 2. Register it in src/providers/index.js
9
+ */
10
+ class BaseProvider {
11
+ /**
12
+ * Returns true if this provider can handle the given source string.
13
+ * Checked in registration order; first match wins.
14
+ * @param {string} _source
15
+ * @returns {boolean}
16
+ */
17
+ static canHandle(_source) {
18
+ return false;
19
+ }
20
+
21
+ /**
22
+ * Publish package files to the destination.
23
+ *
24
+ * @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)
33
+ * @returns {Promise<void>}
34
+ */
35
+ async publish(_files, _options) {
36
+ throw new Error(`publish() is not implemented in ${this.constructor.name}`);
37
+ }
38
+
39
+ /** Human-readable provider name shown in log output. */
40
+ static get providerName() {
41
+ return 'BaseProvider';
42
+ }
43
+ }
44
+
45
+ module.exports = BaseProvider;
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const BaseProvider = require('./base');
6
+
7
+ /**
8
+ * File system provider — handles local directories and SMB/UNC shares.
9
+ *
10
+ * Recognised source formats:
11
+ * /abs/unix/path
12
+ * ./relative/path ../relative/path
13
+ * C:\Windows\path C:/Windows/path (drive-letter)
14
+ * \\server\share\path (SMB UNC — backslash)
15
+ * //server/share/path (SMB UNC — forward slash)
16
+ * file:///path (file URI)
17
+ */
18
+ class FileProvider extends BaseProvider {
19
+ static canHandle(source) {
20
+ return (
21
+ source.startsWith('file://') ||
22
+ source.startsWith('//') ||
23
+ source.startsWith('\\\\') ||
24
+ source.startsWith('/') ||
25
+ source.startsWith('./') ||
26
+ source.startsWith('../') ||
27
+ /^[a-zA-Z]:[/\\]/.test(source) // Windows drive letter
28
+ );
29
+ }
30
+
31
+ static get providerName() {
32
+ return 'File';
33
+ }
34
+
35
+ /** Normalise a source string to an absolute file system path. */
36
+ _resolvePath(source) {
37
+ if (source.startsWith('file://')) {
38
+ return path.resolve(new URL(source).pathname);
39
+ }
40
+ return path.resolve(source);
41
+ }
42
+
43
+ async publish(files, options) {
44
+ const { source, platform, name, version } = options;
45
+
46
+ const destDir = path.join(this._resolvePath(source), platform, name, version);
47
+ fs.mkdirSync(destDir, { recursive: true });
48
+
49
+ this._copy(files.manifest, path.join(destDir, 'wyvrn.json'));
50
+ this._copy(files.zip, path.join(destDir, 'wyvrn.zip'));
51
+ }
52
+
53
+ _copy(src, dest) {
54
+ console.log(`[wyvrn] → ${dest}`);
55
+ fs.copyFileSync(src, dest);
56
+ }
57
+ }
58
+
59
+ module.exports = FileProvider;
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const BaseProvider = require('./base');
7
+
8
+ class HttpProvider extends BaseProvider {
9
+ static canHandle(source) {
10
+ return /^https?:\/\//.test(source);
11
+ }
12
+
13
+ static get providerName() {
14
+ return 'HTTP';
15
+ }
16
+
17
+ async publish(files, options) {
18
+ const { source, platform, name, version, token } = options;
19
+ const base = source.replace(/\/$/, '');
20
+ const urlBase = `${base}/${platform}/${name}/${version}`;
21
+
22
+ await this._put(`${urlBase}/wyvrn.json`, files.manifest, 'application/json', token);
23
+ await this._put(`${urlBase}/wyvrn.zip`, files.zip, 'application/zip', token);
24
+ }
25
+
26
+ _put(url, filePath, contentType, token) {
27
+ return new Promise((resolve, reject) => {
28
+ const body = fs.readFileSync(filePath);
29
+ const parsed = new URL(url);
30
+ const lib = parsed.protocol === 'https:' ? https : http;
31
+
32
+ const headers = {
33
+ 'Content-Type': contentType,
34
+ 'Content-Length': body.length,
35
+ };
36
+ if (token) headers['Authorization'] = `Bearer ${token}`;
37
+
38
+ console.log(`[wyvrn] → PUT ${url}`);
39
+ const req = lib.request(
40
+ {
41
+ hostname: parsed.hostname,
42
+ port: parsed.port || undefined,
43
+ path: parsed.pathname,
44
+ method: 'PUT',
45
+ headers,
46
+ },
47
+ (res) => {
48
+ res.resume(); // drain the response
49
+ if (res.statusCode >= 200 && res.statusCode < 300) {
50
+ resolve();
51
+ } else {
52
+ reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode} ${res.statusMessage}`));
53
+ }
54
+ },
55
+ );
56
+ req.on('error', reject);
57
+ req.write(body);
58
+ req.end();
59
+ });
60
+ }
61
+ }
62
+
63
+ module.exports = HttpProvider;
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const S3Provider = require('./s3');
4
+ const HttpProvider = require('./http');
5
+ const FileProvider = require('./file');
6
+
7
+ /**
8
+ * Ordered list of providers. Detection runs top-to-bottom; first match wins.
9
+ * Add new providers here (or call registerProvider() at runtime).
10
+ */
11
+ const PROVIDERS = [
12
+ S3Provider, // must come before HttpProvider — S3 HTTPS URLs also match http://
13
+ HttpProvider,
14
+ FileProvider, // local paths and SMB UNC shares (\\server\share or //server/share)
15
+ ];
16
+
17
+ /**
18
+ * Return an instantiated provider that can handle the given source string.
19
+ * @param {string} source
20
+ * @returns {import('./base')}
21
+ */
22
+ function getProvider(source) {
23
+ for (const Provider of PROVIDERS) {
24
+ if (Provider.canHandle(source)) {
25
+ return new Provider();
26
+ }
27
+ }
28
+ throw new Error(
29
+ `No provider found for source: "${source}"\n` +
30
+ 'Supported schemes: s3://, http://, https://, file://, local path, \\\\server\\share (SMB)',
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Register a custom provider at the front of the detection list
36
+ * (giving it higher priority than built-ins).
37
+ * @param {typeof import('./base')} Provider
38
+ */
39
+ function registerProvider(Provider) {
40
+ PROVIDERS.unshift(Provider);
41
+ }
42
+
43
+ module.exports = { getProvider, registerProvider, PROVIDERS };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const BaseProvider = require('./base');
5
+
6
+ // Matches: s3://bucket/...
7
+ const RE_S3_URI = /^s3:\/\//;
8
+ // Matches: https://bucket.s3.amazonaws.com/... or https://bucket.s3.us-east-1.amazonaws.com/...
9
+ const RE_S3_VIRTUAL_HOSTED = /^https?:\/\/([^.]+)\.s3[.-][^/]*\.amazonaws\.com/;
10
+ // Matches: https://s3.amazonaws.com/bucket/... or https://s3.us-east-1.amazonaws.com/bucket/...
11
+ const RE_S3_PATH_STYLE = /^https?:\/\/s3[.-][^/]*\.amazonaws\.com\/([^/]+)/;
12
+
13
+ class S3Provider extends BaseProvider {
14
+ static canHandle(source) {
15
+ return (
16
+ RE_S3_URI.test(source) ||
17
+ RE_S3_VIRTUAL_HOSTED.test(source) ||
18
+ RE_S3_PATH_STYLE.test(source)
19
+ );
20
+ }
21
+
22
+ static get providerName() {
23
+ return 'S3';
24
+ }
25
+
26
+ /** Parse source into { bucket, prefix }. prefix has no leading/trailing slashes. */
27
+ _parseSource(source) {
28
+ if (RE_S3_URI.test(source)) {
29
+ const rest = source.slice(5); // strip "s3://"
30
+ const slash = rest.indexOf('/');
31
+ if (slash === -1) return { bucket: rest, prefix: '' };
32
+ return {
33
+ bucket: rest.slice(0, slash),
34
+ prefix: rest.slice(slash + 1).replace(/\/$/, ''),
35
+ };
36
+ }
37
+
38
+ const url = new URL(source);
39
+ const vhMatch = url.hostname.match(/^([^.]+)\.s3/);
40
+ if (vhMatch) {
41
+ return { bucket: vhMatch[1], prefix: url.pathname.slice(1).replace(/\/$/, '') };
42
+ }
43
+
44
+ // path-style: first path segment is the bucket
45
+ const parts = url.pathname.slice(1).split('/');
46
+ return {
47
+ bucket: parts[0],
48
+ prefix: parts.slice(1).join('/').replace(/\/$/, ''),
49
+ };
50
+ }
51
+
52
+ async publish(files, options) {
53
+ // Lazy-load AWS SDK so the tool works without it when only HTTP is used.
54
+ let S3Client, PutObjectCommand, fromSSO;
55
+ try {
56
+ ({ S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'));
57
+ ({ fromSSO } = require('@aws-sdk/credential-providers'));
58
+ } catch {
59
+ throw new Error(
60
+ 'AWS SDK packages are required for S3 publishing.\n' +
61
+ 'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
62
+ );
63
+ }
64
+
65
+ const { source, platform, name, version, profile } = options;
66
+ const { bucket, prefix } = this._parseSource(source);
67
+
68
+ // Destination key base: [prefix/]platform/name/version
69
+ const keyBase = [prefix, platform, name, version].filter(Boolean).join('/');
70
+
71
+ const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
72
+ const client = new S3Client(clientConfig);
73
+
74
+ await this._put(client, bucket, `${keyBase}/wyvrn.json`, files.manifest, 'application/json', PutObjectCommand);
75
+ await this._put(client, bucket, `${keyBase}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
76
+ }
77
+
78
+ async _put(client, bucket, key, filePath, contentType, PutObjectCommand) {
79
+ console.log(`[wyvrn] → s3://${bucket}/${key}`);
80
+ await client.send(
81
+ new PutObjectCommand({
82
+ Bucket: bucket,
83
+ Key: key,
84
+ Body: fs.readFileSync(filePath),
85
+ ContentType: contentType,
86
+ }),
87
+ );
88
+ }
89
+ }
90
+
91
+ module.exports = S3Provider;