wyvrnpm 1.0.0 → 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 +291 -17
- package/bin/wyvrn.js +97 -0
- package/package.json +5 -2
- package/src/commands/configure.js +80 -0
- package/src/commands/install.js +15 -4
- package/src/commands/publish.js +96 -0
- package/src/config.js +60 -0
- package/src/download.js +12 -5
- package/src/index.js +2 -0
- package/src/providers/base.js +45 -0
- package/src/providers/file.js +59 -0
- package/src/providers/http.js +63 -0
- package/src/providers/index.js +43 -0
- package/src/providers/s3.js +91 -0
- package/src/resolve.js +2 -0
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,
|
|
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
|
|
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
|
-
|
|
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` |
|
|
52
|
-
| `--platform` | `win_x64` | Target platform
|
|
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}
|
|
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
|
|
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
|
|
75
|
-
{source}/{platform}/{name}/{version}/wyvrn.zip <- package archive
|
|
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
|
|
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://
|
|
84
|
-
https://
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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 };
|
package/src/commands/install.js
CHANGED
|
@@ -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
|
-
//
|
|
26
|
-
|
|
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
|
|
30
|
-
'Pass
|
|
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
|
|
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
|
-
|
|
33
|
-
fs.
|
|
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
|
/**
|
|
@@ -59,6 +62,8 @@ async function tryDownloadFromSources(
|
|
|
59
62
|
const candidates = [
|
|
60
63
|
`${base}/${platform}/${name}/${version}/wyvrn.zip`,
|
|
61
64
|
`${base}/${platform}/${name}/${version}/razer.zip`,
|
|
65
|
+
`${base}/common/${name}/${version}/wyvrn.zip`,
|
|
66
|
+
`${base}/common/${name}/${version}/razer.zip`,
|
|
62
67
|
];
|
|
63
68
|
for (const url of candidates) {
|
|
64
69
|
try {
|
|
@@ -135,8 +140,10 @@ async function downloadDependencies(
|
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
try {
|
|
138
|
-
|
|
139
|
-
zip.
|
|
143
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
144
|
+
const zip = new StreamZip.async({ file: destZipPath });
|
|
145
|
+
await zip.extract(null, extractDir);
|
|
146
|
+
await zip.close();
|
|
140
147
|
fs.writeFileSync(versionFile, version, 'utf8');
|
|
141
148
|
console.log(`[wyvrn] Extracted: ${name}@${version} → ${extractDir}`);
|
|
142
149
|
} catch (err) {
|
package/src/index.js
CHANGED
|
@@ -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;
|
package/src/resolve.js
CHANGED
|
@@ -44,6 +44,8 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
|
|
|
44
44
|
const candidates = [
|
|
45
45
|
`${base}/${platform}/${name}/${version}/wyvrn.json`,
|
|
46
46
|
`${base}/${platform}/${name}/${version}/razer.json`,
|
|
47
|
+
`${base}/common/${name}/${version}/wyvrn.json`,
|
|
48
|
+
`${base}/common/${name}/${version}/razer.json`,
|
|
47
49
|
];
|
|
48
50
|
for (const url of candidates) {
|
|
49
51
|
try {
|