@tapestry-mud/cli 0.3.3 → 0.3.5
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 +125 -25
- package/bin/tapestry.js +127 -5
- package/package.json +1 -1
- package/src/commands/dist-tag.js +25 -0
- package/src/commands/engine-versions.js +33 -0
- package/src/commands/init.js +49 -41
- package/src/commands/install.js +7 -5
- package/src/commands/login.js +8 -3
- package/src/commands/preset.js +13 -0
- package/src/lib/engine-manager.js +47 -4
- package/src/lib/registry-client.js +59 -5
- package/src/lib/semver-resolver.js +14 -4
- package/src/scaffold/templates.js +3 -0
- package/src/schema/manifest.js +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tapestry-mud/cli
|
|
2
2
|
|
|
3
|
-
CLI for the [Tapestry MUD engine](https://github.com/tapestry-mud/tapestry). Create, manage, and publish
|
|
3
|
+
CLI for the [Tapestry MUD engine](https://github.com/tapestry-mud/tapestry). Create game projects, install content packs, manage the engine, and publish packs to the registry.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,52 +8,152 @@ CLI for the [Tapestry MUD engine](https://github.com/tapestry-mud/tapestry). Cre
|
|
|
8
8
|
npm install -g @tapestry-mud/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Three commands from zero to a running game:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
```bash
|
|
16
|
+
tapestry init
|
|
17
|
+
tapestry install
|
|
18
|
+
tapestry start
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`tapestry init` scaffolds a project directory with a manifest, server config, and starter packs. `tapestry install` resolves dependencies from the registry and downloads them. `tapestry start` pulls the engine (if needed) and launches the server.
|
|
22
|
+
|
|
23
|
+
Connect with `telnet localhost 4000`.
|
|
24
|
+
|
|
25
|
+
**Requirements:** [Docker](https://www.docker.com/) must be installed and running. The default engine mode pulls a Docker image. Binary and source modes are also available (.NET runtime required for source mode -- coming soon).
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
## Admin Account
|
|
28
|
+
|
|
29
|
+
`tapestry init` generates a `server.yaml` with a seed admin block:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
admin:
|
|
33
|
+
handle: TODO # your admin character name
|
|
34
|
+
password: changeme
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Set your handle before starting the server. On first boot, the engine creates the admin account with the `admin` role. Log in and change your password immediately.
|
|
38
|
+
|
|
39
|
+
## Project Structure
|
|
40
|
+
|
|
41
|
+
After `tapestry init`, your project looks like this:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
my-game/
|
|
45
|
+
tapestry.yaml # project manifest (dependencies, engine config)
|
|
46
|
+
server.yaml # engine config (port, admin seed, settings)
|
|
47
|
+
packs/ # installed packs (managed by tapestry install)
|
|
48
|
+
data/ # game data -- players, saves (persists across restarts)
|
|
49
|
+
.tapestry-engine/ # engine artifacts (docker images, binaries, source)
|
|
50
|
+
.gitignore # excludes packs/, data/, .tapestry-engine/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### Game Project
|
|
23
56
|
|
|
24
57
|
| Command | Description |
|
|
25
58
|
|---------|-------------|
|
|
26
|
-
| `tapestry
|
|
59
|
+
| `tapestry init` | Scaffold a new game project from the starter preset |
|
|
60
|
+
| `tapestry install [pack]` | Install all dependencies, or add a specific pack |
|
|
27
61
|
| `tapestry uninstall [pack]` | Remove an installed pack |
|
|
28
|
-
| `tapestry update [pack]` | Update
|
|
29
|
-
| `tapestry list` | List installed packs |
|
|
30
|
-
| `tapestry enable [pack]` |
|
|
31
|
-
| `tapestry disable [pack]` | Disable a pack without removing
|
|
62
|
+
| `tapestry update [pack]` | Update one or all packs to latest compatible versions |
|
|
63
|
+
| `tapestry list` | List installed packs with version and status |
|
|
64
|
+
| `tapestry enable [pack]` | Activate a disabled pack |
|
|
65
|
+
| `tapestry disable [pack]` | Disable a pack without removing files |
|
|
32
66
|
| `tapestry outdated` | Check for newer versions of installed packs |
|
|
33
|
-
| `tapestry info [pack]` | Show pack metadata from the registry |
|
|
34
|
-
| `tapestry search [query]` | Search the registry for packs |
|
|
35
67
|
|
|
36
68
|
### Engine
|
|
37
69
|
|
|
38
70
|
| Command | Description |
|
|
39
71
|
|---------|-------------|
|
|
40
|
-
| `tapestry start` |
|
|
41
|
-
| `tapestry stop` | Stop the
|
|
42
|
-
| `tapestry engine` |
|
|
72
|
+
| `tapestry start` | Launch the engine (auto-pulls Docker image if needed) |
|
|
73
|
+
| `tapestry stop` | Stop the running engine |
|
|
74
|
+
| `tapestry engine install` | Explicitly pull/download the engine artifact |
|
|
75
|
+
| `tapestry engine update` | Update the engine to the configured version |
|
|
76
|
+
| `tapestry engine info` | Show engine version, mode, and image/path |
|
|
77
|
+
| `tapestry engine versions` | List available engine channels from the registry |
|
|
43
78
|
|
|
44
|
-
### Registry
|
|
79
|
+
### Registry
|
|
45
80
|
|
|
46
81
|
| Command | Description |
|
|
47
82
|
|---------|-------------|
|
|
83
|
+
| `tapestry search [query]` | Search the registry by keyword |
|
|
84
|
+
| `tapestry info [pack]` | Show pack metadata from the registry |
|
|
48
85
|
| `tapestry register` | Create a registry account |
|
|
49
|
-
| `tapestry login` |
|
|
50
|
-
| `tapestry publish` | Publish a pack to the registry |
|
|
51
|
-
| `tapestry unpublish [pack]` | Remove a pack from the registry |
|
|
86
|
+
| `tapestry login` | Authenticate with the registry |
|
|
52
87
|
| `tapestry change-password` | Change your registry password |
|
|
53
88
|
|
|
89
|
+
### Pack Authoring
|
|
90
|
+
|
|
91
|
+
| Command | Description |
|
|
92
|
+
|---------|-------------|
|
|
93
|
+
| `tapestry create pack [name]` | Scaffold a new pack with annotated examples |
|
|
94
|
+
| `tapestry validate` | Validate the pack manifest and content files |
|
|
95
|
+
| `tapestry pack` | Build a tarball for local inspection |
|
|
96
|
+
| `tapestry publish` | Build and upload the pack to the registry |
|
|
97
|
+
| `tapestry unpublish [pack]` | Remove a pack from the registry |
|
|
98
|
+
|
|
99
|
+
### Admin
|
|
100
|
+
|
|
101
|
+
| Command | Description |
|
|
102
|
+
|---------|-------------|
|
|
103
|
+
| `tapestry dist-tag set [pack] [tag] [version]` | Set a dist-tag on a pack version |
|
|
104
|
+
| `tapestry dist-tag list [pack]` | List dist-tags for a pack |
|
|
105
|
+
| `tapestry preset set [name] [version] [channel] [packs]` | Update a registry preset |
|
|
106
|
+
|
|
107
|
+
## Publishing a Pack
|
|
108
|
+
|
|
109
|
+
### 1. Create an account
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
tapestry register
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2. Scaffold and build your pack
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
tapestry create pack @yourscope/my-pack
|
|
119
|
+
cd my-pack
|
|
120
|
+
# edit areas, mobs, items, scripts...
|
|
121
|
+
tapestry validate
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 3. Publish
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
tapestry login
|
|
128
|
+
tapestry publish
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The registry validates your manifest, bundles the content, and makes it available for `tapestry install`.
|
|
132
|
+
|
|
133
|
+
### 4. Tag a stable release (admin)
|
|
134
|
+
|
|
135
|
+
After verifying a published version works:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
tapestry dist-tag set @yourscope/my-pack stable 0.1.0
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Players using `tapestry init` with a preset that references your pack will resolve to the tagged version.
|
|
142
|
+
|
|
143
|
+
### CI Publishing
|
|
144
|
+
|
|
145
|
+
For automated pipelines, use token-based auth:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
tapestry login --token $REGISTRY_CI_TOKEN
|
|
149
|
+
tapestry publish
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The CI token is a JWT issued by the registry bootstrap script. Store it as a secret in your CI environment.
|
|
153
|
+
|
|
54
154
|
## Registry
|
|
55
155
|
|
|
56
|
-
|
|
156
|
+
Browse published packs at [tapestryengine.com/packages.html](https://tapestryengine.com/packages.html).
|
|
57
157
|
|
|
58
158
|
## Development
|
|
59
159
|
|
package/bin/tapestry.js
CHANGED
|
@@ -20,10 +20,13 @@ const { info } = require('../src/commands/info');
|
|
|
20
20
|
const { list } = require('../src/commands/list');
|
|
21
21
|
const { outdated } = require('../src/commands/outdated');
|
|
22
22
|
const { engineInstall, engineUpdate, engineInfo } = require('../src/commands/engine');
|
|
23
|
+
const { engineVersions } = require('../src/commands/engine-versions');
|
|
23
24
|
const { startCmd } = require('../src/commands/start');
|
|
24
25
|
const { stopCmd } = require('../src/commands/stop');
|
|
25
26
|
const { changePassword } = require('../src/commands/change-password');
|
|
26
27
|
const { unpublish } = require('../src/commands/unpublish');
|
|
28
|
+
const { distTagSet, distTagList } = require('../src/commands/dist-tag');
|
|
29
|
+
const { presetSet } = require('../src/commands/preset');
|
|
27
30
|
|
|
28
31
|
const program = new Command();
|
|
29
32
|
|
|
@@ -32,19 +35,84 @@ program
|
|
|
32
35
|
.description('Tapestry Package Manager')
|
|
33
36
|
.version(version);
|
|
34
37
|
|
|
38
|
+
program.configureHelp({
|
|
39
|
+
formatHelp(cmd, helper) {
|
|
40
|
+
const groups = [
|
|
41
|
+
{
|
|
42
|
+
title: 'Pack Management',
|
|
43
|
+
commands: ['uninstall', 'update', 'list', 'enable', 'disable', 'outdated'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: 'Engine',
|
|
47
|
+
commands: ['engine'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: 'Registry',
|
|
51
|
+
commands: ['search', 'info'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Account',
|
|
55
|
+
commands: ['register', 'login', 'change-password'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: 'Pack Authoring',
|
|
59
|
+
commands: ['create', 'validate', 'pack', 'publish', 'unpublish'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: 'Admin',
|
|
63
|
+
commands: ['dist-tag', 'preset'],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const cmdMap = new Map();
|
|
68
|
+
for (const sub of cmd.commands) {
|
|
69
|
+
cmdMap.set(sub.name(), sub);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pad = 28;
|
|
73
|
+
let out = `Usage: ${helper.commandUsage(cmd)}\n\n`;
|
|
74
|
+
out += 'Tapestry Package Manager\n\n';
|
|
75
|
+
out += 'Quick start:\n';
|
|
76
|
+
out += ` ${'init'.padEnd(pad)}Scaffold a new game project\n`;
|
|
77
|
+
out += ` ${'install'.padEnd(pad)}Install packs from the registry\n`;
|
|
78
|
+
out += ` ${'start'.padEnd(pad)}Launch the engine (auto-pulls if needed)\n`;
|
|
79
|
+
out += ` ${'stop'.padEnd(pad)}Stop the running engine\n`;
|
|
80
|
+
out += ' telnet localhost 4000\n\n';
|
|
81
|
+
|
|
82
|
+
for (const group of groups) {
|
|
83
|
+
out += `${group.title}:\n`;
|
|
84
|
+
for (const name of group.commands) {
|
|
85
|
+
const sub = cmdMap.get(name);
|
|
86
|
+
if (sub) {
|
|
87
|
+
const usage = sub.options.length ? `${name} [options]` : name;
|
|
88
|
+
out += ` ${usage.padEnd(pad)}${sub.description()}\n`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
out += '\n';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
out += 'Options:\n';
|
|
95
|
+
for (const opt of helper.visibleOptions(cmd)) {
|
|
96
|
+
out += ` ${helper.optionTerm(opt).padEnd(pad)}${helper.optionDescription(opt)}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return out;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
35
103
|
program
|
|
36
104
|
.command('init')
|
|
37
105
|
.description('Initialize a new Tapestry game project in the current directory')
|
|
38
|
-
.action(() => {
|
|
106
|
+
.action(async () => {
|
|
39
107
|
try {
|
|
40
|
-
init();
|
|
108
|
+
await init();
|
|
41
109
|
} catch (e) {
|
|
42
110
|
console.error(`error: ${e.message}`);
|
|
43
111
|
process.exit(1);
|
|
44
112
|
}
|
|
45
113
|
});
|
|
46
114
|
|
|
47
|
-
const createCmd = program.command('create');
|
|
115
|
+
const createCmd = program.command('create').description('Scaffold new content');
|
|
48
116
|
|
|
49
117
|
createCmd
|
|
50
118
|
.command('pack <name>')
|
|
@@ -58,6 +126,47 @@ createCmd
|
|
|
58
126
|
}
|
|
59
127
|
});
|
|
60
128
|
|
|
129
|
+
const distTagCmd = program.command('dist-tag').description('Manage dist-tags for a registry package');
|
|
130
|
+
|
|
131
|
+
distTagCmd
|
|
132
|
+
.command('set <pack> <tag> <version>')
|
|
133
|
+
.description('Set a dist-tag on a pack version (owner or admin only)')
|
|
134
|
+
.action(async (pack, tag, version) => {
|
|
135
|
+
try {
|
|
136
|
+
await distTagSet(pack, tag, version);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(`error: ${e.message}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
distTagCmd
|
|
144
|
+
.command('list <pack>')
|
|
145
|
+
.description('List all dist-tags for a pack')
|
|
146
|
+
.action(async (pack) => {
|
|
147
|
+
try {
|
|
148
|
+
await distTagList(pack);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error(`error: ${e.message}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const presetCmd = program.command('preset').description('Manage registry presets (admin only)');
|
|
156
|
+
|
|
157
|
+
presetCmd
|
|
158
|
+
.command('set <name> <version> <engine-channel> <packs>')
|
|
159
|
+
.description('Update a preset with pinned pack versions (packs: JSON string)')
|
|
160
|
+
.action(async (name, version, engineChannel, packsJson) => {
|
|
161
|
+
try {
|
|
162
|
+
const packs = JSON.parse(packsJson);
|
|
163
|
+
await presetSet(name, version, engineChannel, packs);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error(`error: ${e.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
61
170
|
program
|
|
62
171
|
.command('install [package]')
|
|
63
172
|
.description('Install a package or all dependencies from tapestry.yaml')
|
|
@@ -121,9 +230,10 @@ program
|
|
|
121
230
|
program
|
|
122
231
|
.command('login')
|
|
123
232
|
.description('Authenticate with the registry and store token in ~/.tapestryrc')
|
|
124
|
-
.
|
|
233
|
+
.option('--token <token>', 'Save a raw token directly (for CI use, skips interactive login)')
|
|
234
|
+
.action(async (options) => {
|
|
125
235
|
try {
|
|
126
|
-
await login();
|
|
236
|
+
await login({}, { token: options.token });
|
|
127
237
|
} catch (e) {
|
|
128
238
|
console.error(`error: ${e.message}`);
|
|
129
239
|
process.exit(1);
|
|
@@ -264,6 +374,18 @@ engineCmd
|
|
|
264
374
|
}
|
|
265
375
|
});
|
|
266
376
|
|
|
377
|
+
engineCmd
|
|
378
|
+
.command('versions')
|
|
379
|
+
.description('List available engine channels from the registry')
|
|
380
|
+
.action(async () => {
|
|
381
|
+
try {
|
|
382
|
+
await engineVersions();
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.error(`error: ${e.message}`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
267
389
|
program
|
|
268
390
|
.command('start')
|
|
269
391
|
.description('Launch the Tapestry engine')
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireToken } = require('../lib/auth');
|
|
4
|
+
const { patchDistTag, listDistTags, DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
5
|
+
|
|
6
|
+
async function distTagSet(packName, tag, version, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
7
|
+
const token = requireToken();
|
|
8
|
+
await patchDistTag(packName, tag, version, token, registryUrl);
|
|
9
|
+
console.log(` ${packName} ${tag} -> ${version}`);
|
|
10
|
+
console.log('Done.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function distTagList(packName, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
14
|
+
const tags = await listDistTags(packName, registryUrl);
|
|
15
|
+
const entries = Object.entries(tags);
|
|
16
|
+
if (entries.length === 0) {
|
|
17
|
+
console.log(`No tags set for ${packName}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const [tag, version] of entries) {
|
|
21
|
+
console.log(` ${tag}: ${version}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { distTagSet, distTagList };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
const { DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
5
|
+
|
|
6
|
+
async function engineVersions() {
|
|
7
|
+
const res = await fetch(`${DEFAULT_REGISTRY}/v1/engine-channels`);
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
throw new Error(`Registry error ${res.status}`);
|
|
10
|
+
}
|
|
11
|
+
const channels = await res.json();
|
|
12
|
+
|
|
13
|
+
if (channels.length === 0) {
|
|
14
|
+
console.log('No engine channels registered.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const COL = [10, 10, 22];
|
|
19
|
+
const pad = (s, w) => String(s).padEnd(w);
|
|
20
|
+
|
|
21
|
+
console.log([pad('Channel', COL[0]), pad('Version', COL[1]), pad('Updated', COL[2])].join(' '));
|
|
22
|
+
console.log(COL.map(w => '-'.repeat(w)).join(' '));
|
|
23
|
+
|
|
24
|
+
for (const ch of channels) {
|
|
25
|
+
const date = new Date(ch.updated_at).toLocaleString('en-US', {
|
|
26
|
+
year: 'numeric', month: 'numeric', day: 'numeric',
|
|
27
|
+
hour: '2-digit', minute: '2-digit',
|
|
28
|
+
});
|
|
29
|
+
console.log([pad(ch.channel, COL[0]), pad(ch.version, COL[1]), pad(date, COL[2])].join(' '));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { engineVersions };
|
package/src/commands/init.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { fetchPreset, DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
5
6
|
|
|
6
|
-
function buildManifest(name) {
|
|
7
|
+
function buildManifest(name, deps) {
|
|
8
|
+
const depLines = Object.entries(deps).map(([pkg, range]) => ` '${pkg}': '${range}'`).join('\n');
|
|
7
9
|
return [
|
|
8
10
|
`name: ${name}`,
|
|
9
11
|
`engine:`,
|
|
@@ -11,53 +13,59 @@ function buildManifest(name) {
|
|
|
11
13
|
` mode: docker`,
|
|
12
14
|
` image: ghcr.io/tapestry-mud/tapestry`,
|
|
13
15
|
`dependencies:`,
|
|
14
|
-
|
|
15
|
-
` #
|
|
16
|
-
` '@tapestry/example-pack': '^0.0.1'`,
|
|
16
|
+
depLines,
|
|
17
|
+
` # Add more packs here. Run: tapestry install @scope/pack-name`,
|
|
17
18
|
`packs: []`,
|
|
18
19
|
`tag_validation: strict`,
|
|
19
20
|
``,
|
|
21
|
+
`# Server port, admin seed account, and engine settings are in server.yaml`,
|
|
20
22
|
].join('\n');
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
function init(cwd) {
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
cwd = process.cwd();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const manifestPath = path.join(cwd, 'tapestry.yaml');
|
|
32
|
-
if (fs.existsSync(manifestPath)) {
|
|
33
|
-
{
|
|
34
|
-
throw new Error('tapestry.yaml already exists. Run tapestry install to install dependencies.');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const name = path.basename(cwd);
|
|
39
|
-
fs.writeFileSync(manifestPath, buildManifest(name));
|
|
40
|
-
fs.writeFileSync(path.join(cwd, 'server.yaml'), '# Tapestry server configuration\n# See https://tapestryengine.com/docs/config for full options\nport: 4000\n');
|
|
41
|
-
fs.mkdirSync(path.join(cwd, 'packs'), { recursive: true });
|
|
42
|
-
fs.writeFileSync(
|
|
43
|
-
path.join(cwd, '.gitignore'),
|
|
44
|
-
'# Installed packages (managed by tapestry install)\npacks/\n\n# Engine artifacts (managed by tapestry engine install)\n.tapestry-engine/\n'
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
console.log(`Initialized: ${name}`);
|
|
48
|
-
console.log(' tapestry.yaml project manifest');
|
|
49
|
-
console.log(' server.yaml engine config');
|
|
50
|
-
console.log(' packs/ installed packages');
|
|
51
|
-
console.log(' .gitignore excludes packs/ and .tapestry-engine/ from git');
|
|
52
|
-
|
|
53
|
-
if (!fs.existsSync(path.join(cwd, '.git'))) {
|
|
54
|
-
{
|
|
55
|
-
console.log('\nHint: no git repo detected. Run: git init');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log('\nNext: run tapestry install, then tapestry engine install, then tapestry start');
|
|
25
|
+
async function init(cwd, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
26
|
+
if (cwd === undefined) {
|
|
27
|
+
cwd = process.cwd();
|
|
60
28
|
}
|
|
29
|
+
|
|
30
|
+
const manifestPath = path.join(cwd, 'tapestry.yaml');
|
|
31
|
+
if (fs.existsSync(manifestPath)) {
|
|
32
|
+
throw new Error('tapestry.yaml already exists. Run tapestry install to install dependencies.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let preset;
|
|
36
|
+
try {
|
|
37
|
+
preset = await fetchPreset('starter', registryUrl);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new Error(`Failed to fetch starter preset from registry: ${e.message}. Check your connection and try again.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Initializing Tapestry Starter v${preset.version}`);
|
|
43
|
+
|
|
44
|
+
const deps = {};
|
|
45
|
+
for (const [pkg, ver] of Object.entries(preset.packs)) {
|
|
46
|
+
deps[pkg] = `^${ver}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const name = path.basename(cwd);
|
|
50
|
+
fs.writeFileSync(manifestPath, buildManifest(name, deps));
|
|
51
|
+
fs.writeFileSync(path.join(cwd, 'server.yaml'), '# Tapestry server configuration\n# See https://tapestryengine.com/docs/config for full options\nport: 4000\n\n# Admin account created on first boot (change password after login)\nadmin:\n handle: TODO # your admin character name\n password: changeme\n');
|
|
52
|
+
fs.mkdirSync(path.join(cwd, 'packs'), { recursive: true });
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(cwd, '.gitignore'),
|
|
55
|
+
'# Installed packages (managed by tapestry install)\npacks/\n\n# Engine artifacts (managed by tapestry engine install)\n.tapestry-engine/\n\n# Game data (players, saves)\ndata/\n'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
console.log(`Initialized: ${name}`);
|
|
59
|
+
console.log(' tapestry.yaml project manifest');
|
|
60
|
+
console.log(' server.yaml engine config');
|
|
61
|
+
console.log(' packs/ installed packages');
|
|
62
|
+
console.log(' .gitignore excludes packs/ and .tapestry-engine/ from git');
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(path.join(cwd, '.git'))) {
|
|
65
|
+
console.log('\nHint: no git repo detected. Run: git init');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('\nNext: run tapestry install, then tapestry engine install, then tapestry start');
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
module.exports = { init };
|
package/src/commands/install.js
CHANGED
|
@@ -9,6 +9,7 @@ const { readLock, writeLock } = require('../lib/lock-file');
|
|
|
9
9
|
const { fetchTarball, DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
10
10
|
const { verifyIntegrity, saveTarball, extractTarball } = require('../lib/tarball');
|
|
11
11
|
const { addPackageToBoot } = require('../lib/boot');
|
|
12
|
+
const { loadToken } = require('../lib/auth');
|
|
12
13
|
|
|
13
14
|
function packInstallPath(cwd, packageName) {
|
|
14
15
|
const parts = packageName.split('/');
|
|
@@ -28,7 +29,7 @@ function isLockCurrent(manifestDeps, lock) {
|
|
|
28
29
|
return Object.keys(manifestDeps).every((name) => lockResolved[name]);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
async function installResolved(cwd, resolved) {
|
|
32
|
+
async function installResolved(cwd, resolved, token) {
|
|
32
33
|
for (const [packageName, info] of Object.entries(resolved)) {
|
|
33
34
|
const destDir = packInstallPath(cwd, packageName);
|
|
34
35
|
|
|
@@ -43,7 +44,7 @@ async function installResolved(cwd, resolved) {
|
|
|
43
44
|
const tmpPath = path.join(os.tmpdir(), `tapestry-${safeId}-${info.version}.tgz`);
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
|
-
const buffer = await fetchTarball(info.tarball);
|
|
47
|
+
const buffer = await fetchTarball(info.tarball, token);
|
|
47
48
|
verifyIntegrity(buffer, info.integrity);
|
|
48
49
|
saveTarball(buffer, tmpPath);
|
|
49
50
|
await extractTarball(tmpPath, destDir);
|
|
@@ -64,6 +65,7 @@ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_
|
|
|
64
65
|
throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
const token = loadToken();
|
|
67
69
|
const manifest = readYaml(manifestPath);
|
|
68
70
|
let resolved;
|
|
69
71
|
|
|
@@ -74,7 +76,7 @@ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_
|
|
|
74
76
|
manifest.dependencies[name] = tempRange;
|
|
75
77
|
|
|
76
78
|
console.log('Resolving dependencies...');
|
|
77
|
-
resolved = await resolve(manifest.dependencies, registryUrl);
|
|
79
|
+
resolved = await resolve(manifest.dependencies, registryUrl, token);
|
|
78
80
|
|
|
79
81
|
if (!rawRange) {
|
|
80
82
|
manifest.dependencies[name] = `^${resolved[name].version}`;
|
|
@@ -88,11 +90,11 @@ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_
|
|
|
88
90
|
resolved = lock.resolved;
|
|
89
91
|
} else {
|
|
90
92
|
console.log('Resolving dependencies...');
|
|
91
|
-
resolved = await resolve(manifest.dependencies || {}, registryUrl);
|
|
93
|
+
resolved = await resolve(manifest.dependencies || {}, registryUrl, token);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
await installResolved(cwd, resolved);
|
|
97
|
+
await installResolved(cwd, resolved, token);
|
|
96
98
|
writeLock(cwd, { lockfile_version: 1, resolved });
|
|
97
99
|
console.log('Done.');
|
|
98
100
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -16,7 +16,12 @@ async function promptCredentials() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
async function login({ email, password } = {}, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
19
|
+
async function login({ email, password } = {}, { registryUrl = DEFAULT_REGISTRY, token = null } = {}) {
|
|
20
|
+
if (token) {
|
|
21
|
+
saveToken(token);
|
|
22
|
+
console.log('Token saved.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
20
25
|
if (!email || !password) {
|
|
21
26
|
({ email, password } = await promptCredentials());
|
|
22
27
|
}
|
|
@@ -29,8 +34,8 @@ async function login({ email, password } = {}, { registryUrl = DEFAULT_REGISTRY
|
|
|
29
34
|
|
|
30
35
|
await throwIfError(res, 'Login failed');
|
|
31
36
|
|
|
32
|
-
const { token } = await res.json();
|
|
33
|
-
saveToken(
|
|
37
|
+
const { token: authToken } = await res.json();
|
|
38
|
+
saveToken(authToken);
|
|
34
39
|
console.log('Logged in.');
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requireToken } = require('../lib/auth');
|
|
4
|
+
const { patchPreset, DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
5
|
+
|
|
6
|
+
async function presetSet(name, version, engineChannel, packs, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
7
|
+
const token = requireToken();
|
|
8
|
+
await patchPreset(name, { version, engine_channel: engineChannel, packs }, token, registryUrl);
|
|
9
|
+
console.log(` Updated preset '${name}' to v${version}`);
|
|
10
|
+
console.log('Done.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { presetSet };
|
|
@@ -5,6 +5,34 @@ const path = require('path');
|
|
|
5
5
|
const { spawnSync, spawn } = require('child_process');
|
|
6
6
|
const { readYaml } = require('../util/yaml');
|
|
7
7
|
const { writePid, readPid, clearPid } = require('./process-tracker');
|
|
8
|
+
const fetch = require('node-fetch');
|
|
9
|
+
const { DEFAULT_REGISTRY } = require('./registry-client');
|
|
10
|
+
|
|
11
|
+
const NAMED_CHANNELS = ['nightly', 'stable'];
|
|
12
|
+
|
|
13
|
+
async function resolveDockerTag(config) {
|
|
14
|
+
if (!NAMED_CHANNELS.includes(config.version)) {
|
|
15
|
+
return config.version;
|
|
16
|
+
}
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(`${DEFAULT_REGISTRY}/v1/engine-channels/${config.version}`);
|
|
20
|
+
} catch {
|
|
21
|
+
console.warn('Could not reach registry to resolve channel, using version string directly.');
|
|
22
|
+
return config.version;
|
|
23
|
+
}
|
|
24
|
+
if (res.status === 404) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Channel '${config.version}' not found in registry. Run \`tapestry engine versions\` to see available channels.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
console.warn(`Registry returned ${res.status} resolving channel, using version string directly.`);
|
|
31
|
+
return config.version;
|
|
32
|
+
}
|
|
33
|
+
const { docker_tag } = await res.json();
|
|
34
|
+
return docker_tag;
|
|
35
|
+
}
|
|
8
36
|
|
|
9
37
|
const ENGINE_REPO = 'https://github.com/tapestry-mud/tapestry.git';
|
|
10
38
|
const DEFAULT_IMAGE = 'ghcr.io/tapestry-mud/tapestry';
|
|
@@ -30,8 +58,17 @@ function dockerPull(image, version) {
|
|
|
30
58
|
console.log(`Engine image ready: ${image}:${version}`);
|
|
31
59
|
}
|
|
32
60
|
|
|
33
|
-
function
|
|
61
|
+
function dockerEnsureImage(image, version) {
|
|
62
|
+
const check = spawnSync('docker', ['image', 'inspect', `${image}:${version}`], { stdio: 'ignore' });
|
|
63
|
+
if (check.status !== 0) {
|
|
64
|
+
dockerPull(image, version);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dockerStart(projectName, image, version, packsDir, serverYamlPath, dataDir) {
|
|
34
69
|
const containerName = `tapestry-${projectName}`;
|
|
70
|
+
dockerEnsureImage(image, version);
|
|
71
|
+
spawnSync('docker', ['rm', '-f', containerName], { stdio: 'ignore' });
|
|
35
72
|
const result = spawnSync('docker', [
|
|
36
73
|
'run', '--detach',
|
|
37
74
|
'--name', containerName,
|
|
@@ -39,6 +76,7 @@ function dockerStart(projectName, image, version, packsDir, serverYamlPath) {
|
|
|
39
76
|
'-p', '4001:4001',
|
|
40
77
|
'-v', `${packsDir}:/app/packs`,
|
|
41
78
|
'-v', `${serverYamlPath}:/app/server.yaml`,
|
|
79
|
+
'-v', `${dataDir}:/app/data`,
|
|
42
80
|
`${image}:${version}`,
|
|
43
81
|
], { stdio: 'inherit' });
|
|
44
82
|
if (result.status !== 0) {
|
|
@@ -233,7 +271,8 @@ function readEngineConfig(cwd) {
|
|
|
233
271
|
async function installEngine(cwd) {
|
|
234
272
|
const config = readEngineConfig(cwd);
|
|
235
273
|
if (config.mode === 'docker') {
|
|
236
|
-
|
|
274
|
+
const tag = await resolveDockerTag(config);
|
|
275
|
+
dockerPull(config.image, tag);
|
|
237
276
|
} else if (config.mode === 'binary') {
|
|
238
277
|
binaryInstall(config.version, config.installDir);
|
|
239
278
|
} else {
|
|
@@ -244,7 +283,8 @@ async function installEngine(cwd) {
|
|
|
244
283
|
async function updateEngine(cwd) {
|
|
245
284
|
const config = readEngineConfig(cwd);
|
|
246
285
|
if (config.mode === 'docker') {
|
|
247
|
-
|
|
286
|
+
const tag = await resolveDockerTag(config);
|
|
287
|
+
dockerPull(config.image, tag);
|
|
248
288
|
} else if (config.mode === 'binary') {
|
|
249
289
|
binaryInstall(config.version, config.installDir);
|
|
250
290
|
} else {
|
|
@@ -266,6 +306,7 @@ function getEngineInfo(cwd) {
|
|
|
266
306
|
async function startEngine(cwd) {
|
|
267
307
|
const config = readEngineConfig(cwd);
|
|
268
308
|
const packsDir = path.resolve(cwd, 'packs');
|
|
309
|
+
const dataDir = path.resolve(cwd, 'data');
|
|
269
310
|
const serverYamlPath = path.resolve(cwd, 'server.yaml');
|
|
270
311
|
if (!fs.existsSync(packsDir)) {
|
|
271
312
|
throw new Error('packs/ directory not found. Run tapestry install first.');
|
|
@@ -273,8 +314,10 @@ async function startEngine(cwd) {
|
|
|
273
314
|
if (!fs.existsSync(serverYamlPath)) {
|
|
274
315
|
throw new Error('server.yaml not found in the current directory.');
|
|
275
316
|
}
|
|
317
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
276
318
|
if (config.mode === 'docker') {
|
|
277
|
-
|
|
319
|
+
const tag = await resolveDockerTag(config);
|
|
320
|
+
dockerStart(config.projectName, config.image, tag, packsDir, serverYamlPath, dataDir);
|
|
278
321
|
} else if (config.mode === 'binary') {
|
|
279
322
|
binaryStart(config.version, config.installDir, packsDir, serverYamlPath, cwd);
|
|
280
323
|
} else {
|
|
@@ -28,10 +28,12 @@ async function throwIfError(res, context) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async function fetchPackageMetadata(name, registryUrl = DEFAULT_REGISTRY) {
|
|
31
|
+
async function fetchPackageMetadata(name, registryUrl = DEFAULT_REGISTRY, token = null) {
|
|
32
32
|
validatePackageName(name);
|
|
33
33
|
const url = `${registryUrl}/v1/packages/${name}`;
|
|
34
|
-
const
|
|
34
|
+
const headers = {};
|
|
35
|
+
if (token) { headers['Authorization'] = `Bearer ${token}`; }
|
|
36
|
+
const res = await fetch(url, { headers });
|
|
35
37
|
if (!res.ok) {
|
|
36
38
|
if (res.status === 404) {
|
|
37
39
|
throw new Error(`Package ${name} not found in registry`);
|
|
@@ -46,8 +48,13 @@ async function fetchPackageMetadata(name, registryUrl = DEFAULT_REGISTRY) {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
async function fetchTarball(url) {
|
|
50
|
-
const
|
|
51
|
+
async function fetchTarball(url, token = null) {
|
|
52
|
+
const headers = {};
|
|
53
|
+
if (token) { headers['Authorization'] = `Bearer ${token}`; }
|
|
54
|
+
const res = await fetch(url, { headers });
|
|
55
|
+
if (res.status === 401) {
|
|
56
|
+
throw new Error('pack is private - run tapestry login first');
|
|
57
|
+
}
|
|
51
58
|
if (!res.ok) {
|
|
52
59
|
const body = await res.text();
|
|
53
60
|
throw new Error(`Tarball download failed: ${res.status}: ${body}`);
|
|
@@ -55,4 +62,51 @@ async function fetchTarball(url) {
|
|
|
55
62
|
return res.buffer();
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
async function fetchPreset(name, registryUrl = DEFAULT_REGISTRY) {
|
|
66
|
+
const url = `${registryUrl.replace(/\/$/, '')}/v1/presets/${name}`;
|
|
67
|
+
const res = await fetch(url);
|
|
68
|
+
await throwIfError(res, `Failed to fetch preset '${name}'`);
|
|
69
|
+
return res.json();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function patchDistTag(packName, tag, version, token, registryUrl = DEFAULT_REGISTRY) {
|
|
73
|
+
validatePackageName(packName);
|
|
74
|
+
const url = `${registryUrl.replace(/\/$/, '')}/v1/packages/${packName}/dist-tags/${tag}`;
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: 'PATCH',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({ version }),
|
|
82
|
+
});
|
|
83
|
+
await throwIfError(res, `Failed to set dist-tag ${tag} on ${packName}`);
|
|
84
|
+
return res.json();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function listDistTags(packName, registryUrl = DEFAULT_REGISTRY) {
|
|
88
|
+
validatePackageName(packName);
|
|
89
|
+
const url = `${registryUrl.replace(/\/$/, '')}/v1/packages/${packName}/dist-tags`;
|
|
90
|
+
const res = await fetch(url);
|
|
91
|
+
await throwIfError(res, `Failed to fetch dist-tags for ${packName}`);
|
|
92
|
+
return res.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function patchPreset(name, payload, token, registryUrl = DEFAULT_REGISTRY) {
|
|
96
|
+
const url = `${registryUrl.replace(/\/$/, '')}/v1/admin/presets/${name}`;
|
|
97
|
+
const res = await fetch(url, {
|
|
98
|
+
method: 'PATCH',
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
Authorization: `Bearer ${token}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(payload),
|
|
104
|
+
});
|
|
105
|
+
await throwIfError(res, `Failed to update preset '${name}'`);
|
|
106
|
+
return res.json();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
fetchPackageMetadata, fetchTarball, throwIfError, DEFAULT_REGISTRY,
|
|
111
|
+
fetchPreset, patchDistTag, listDistTags, patchPreset,
|
|
112
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const semver = require('semver');
|
|
4
4
|
const { fetchPackageMetadata } = require('./registry-client');
|
|
5
5
|
|
|
6
|
-
async function resolve(dependencies, registryUrl) {
|
|
6
|
+
async function resolve(dependencies, registryUrl, token = null) {
|
|
7
7
|
if (!dependencies || Object.keys(dependencies).length === 0) {
|
|
8
8
|
return {};
|
|
9
9
|
}
|
|
@@ -34,13 +34,23 @@ async function resolve(dependencies, registryUrl) {
|
|
|
34
34
|
|
|
35
35
|
resolvedBy[name] = { range, requiredBy };
|
|
36
36
|
|
|
37
|
-
const meta = await fetchPackageMetadata(name, baseUrl);
|
|
37
|
+
const meta = await fetchPackageMetadata(name, baseUrl, token);
|
|
38
|
+
|
|
39
|
+
let resolvedRange = range;
|
|
40
|
+
if (/^[a-z]+$/.test(range)) {
|
|
41
|
+
const distTags = meta.dist_tags || {};
|
|
42
|
+
if (!distTags[range]) {
|
|
43
|
+
throw new Error(`Tag '${range}' not found for ${name}. Available tags: ${Object.keys(distTags).join(', ') || 'none'}`);
|
|
44
|
+
}
|
|
45
|
+
resolvedRange = distTags[range];
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
const versions = meta.versions.map((v) => v.version);
|
|
39
|
-
const best = semver.maxSatisfying(versions,
|
|
49
|
+
const best = semver.maxSatisfying(versions, resolvedRange);
|
|
40
50
|
|
|
41
51
|
if (!best) {
|
|
42
52
|
throw new Error(
|
|
43
|
-
`No version of ${name} satisfies ${
|
|
53
|
+
`No version of ${name} satisfies ${resolvedRange}. Available: ${versions.join(', ') || 'none'}`
|
|
44
54
|
);
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -12,6 +12,9 @@ author:
|
|
|
12
12
|
handle: "TODO: your-registry-handle"
|
|
13
13
|
license: "MIT"
|
|
14
14
|
|
|
15
|
+
# Packs default to public. Add \`private: true\` to restrict access to your
|
|
16
|
+
# account and registry admins only.
|
|
17
|
+
|
|
15
18
|
# Semver range: >=3.0.0 means any engine version at or above this.
|
|
16
19
|
engine: ">=3.0.0"
|
|
17
20
|
|
package/src/schema/manifest.js
CHANGED