@tapestry-mud/cli 0.3.3 → 0.3.6

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
  # @tapestry-mud/cli
2
2
 
3
- CLI for the [Tapestry MUD engine](https://github.com/tapestry-mud/tapestry). Create, manage, and publish content packs.
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
- ## Commands
11
+ ## Quick Start
12
12
 
13
- ### Pack Development
13
+ Three commands from zero to a running game:
14
14
 
15
- | Command | Description |
16
- |---------|-------------|
17
- | `tapestry init` | Create a new pack project with scaffold |
18
- | `tapestry create-pack [name]` | Generate a pack from a template |
19
- | `tapestry validate` | Validate pack.yaml and content files |
20
- | `tapestry pack` | Bundle a pack into a tarball for publishing |
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
- ### Pack Management
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 install [pack]` | Install a pack from the registry |
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 a pack to the latest version |
29
- | `tapestry list` | List installed packs |
30
- | `tapestry enable [pack]` | Enable a disabled pack |
31
- | `tapestry disable [pack]` | Disable a pack without removing it |
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` | Start the Tapestry server |
41
- | `tapestry stop` | Stop the Tapestry server |
42
- | `tapestry engine` | Show engine version and status |
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 Account
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` | Log in to the registry |
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
- Packs are published to [registry.tapestryengine.com](https://registry.tapestryengine.com).
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
- .action(async () => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapestry-mud/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.6",
4
4
  "description": "CLI for the Tapestry MUD engine",
5
5
  "bin": {
6
6
  "tapestry": "./bin/tapestry.js"
@@ -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 };
@@ -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
- ` '@tapestry/core': '^0.0.1'`,
15
- ` # Starter races, classes, and tutorial area. Remove or replace with your own content pack.`,
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
- if (cwd === undefined) {
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 };
@@ -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
  }
@@ -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(token);
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 dockerStart(projectName, image, version, packsDir, serverYamlPath) {
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
- dockerPull(config.image, config.version);
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
- dockerPull(config.image, config.version);
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
- dockerStart(config.projectName, config.image, config.version, packsDir, serverYamlPath);
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 res = await fetch(url);
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 res = await fetch(url);
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
- module.exports = { fetchPackageMetadata, fetchTarball, throwIfError, DEFAULT_REGISTRY };
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, range);
49
+ const best = semver.maxSatisfying(versions, resolvedRange);
40
50
 
41
51
  if (!best) {
42
52
  throw new Error(
43
- `No version of ${name} satisfies ${range}. Available: ${versions.join(', ') || 'none'}`
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
 
@@ -41,6 +41,7 @@ const PackageManifestSchema = z.object({
41
41
  properties: z.number().optional(),
42
42
  keywords: z.array(z.string()).optional(),
43
43
  }).optional(),
44
+ private: z.boolean().optional(),
44
45
  });
45
46
 
46
47
  const ProjectManifestSchema = z.object({