@tapestry-mud/cli 0.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.
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { Command } = require('commander');
5
+ const { init } = require('../src/commands/init');
6
+ const { createPack } = require('../src/commands/create-pack');
7
+ const { install } = require('../src/commands/install');
8
+ const { uninstall } = require('../src/commands/uninstall');
9
+ const { update } = require('../src/commands/update');
10
+ const { enable } = require('../src/commands/enable');
11
+ const { disable } = require('../src/commands/disable');
12
+ const { login } = require('../src/commands/login');
13
+ const { register } = require('../src/commands/register');
14
+ const { validate } = require('../src/commands/validate');
15
+ const { pack } = require('../src/commands/pack');
16
+ const { publish } = require('../src/commands/publish');
17
+ const { search } = require('../src/commands/search');
18
+ const { info } = require('../src/commands/info');
19
+ const { list } = require('../src/commands/list');
20
+ const { outdated } = require('../src/commands/outdated');
21
+ const { engineInstall, engineUpdate, engineInfo } = require('../src/commands/engine');
22
+ const { startCmd } = require('../src/commands/start');
23
+ const { stopCmd } = require('../src/commands/stop');
24
+
25
+ const program = new Command();
26
+
27
+ program
28
+ .name('tapestry')
29
+ .description('Tapestry Package Manager')
30
+ .version('0.1.0');
31
+
32
+ program
33
+ .command('init')
34
+ .description('Initialize a new Tapestry game project in the current directory')
35
+ .action(() => {
36
+ try {
37
+ init();
38
+ } catch (e) {
39
+ console.error(`error: ${e.message}`);
40
+ process.exit(1);
41
+ }
42
+ });
43
+
44
+ const createCmd = program.command('create');
45
+
46
+ createCmd
47
+ .command('pack <name>')
48
+ .description('Scaffold a new pack with annotated example content')
49
+ .action((name) => {
50
+ try {
51
+ createPack(name);
52
+ } catch (e) {
53
+ console.error(`error: ${e.message}`);
54
+ process.exit(1);
55
+ }
56
+ });
57
+
58
+ program
59
+ .command('install [package]')
60
+ .description('Install a package or all dependencies from tapestry.yaml')
61
+ .action(async (pkg) => {
62
+ try {
63
+ await install(pkg || undefined);
64
+ } catch (e) {
65
+ console.error(`error: ${e.message}`);
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ program
71
+ .command('uninstall <package>')
72
+ .description('Remove an installed package')
73
+ .action(async (pkg) => {
74
+ try {
75
+ await uninstall(pkg);
76
+ } catch (e) {
77
+ console.error(`error: ${e.message}`);
78
+ process.exit(1);
79
+ }
80
+ });
81
+
82
+ program
83
+ .command('update [package]')
84
+ .description('Update a package or all packages to latest compatible versions')
85
+ .action(async (pkg) => {
86
+ try {
87
+ await update(pkg || undefined);
88
+ } catch (e) {
89
+ console.error(`error: ${e.message}`);
90
+ process.exit(1);
91
+ }
92
+ });
93
+
94
+ program
95
+ .command('enable <package>')
96
+ .description('Activate a package in the engine boot order')
97
+ .action(async (pkg) => {
98
+ try {
99
+ await enable(pkg);
100
+ } catch (e) {
101
+ console.error(`error: ${e.message}`);
102
+ process.exit(1);
103
+ }
104
+ });
105
+
106
+ program
107
+ .command('disable <package>')
108
+ .description('Remove a package from the engine boot order without deleting files')
109
+ .action(async (pkg) => {
110
+ try {
111
+ await disable(pkg);
112
+ } catch (e) {
113
+ console.error(`error: ${e.message}`);
114
+ process.exit(1);
115
+ }
116
+ });
117
+
118
+ program
119
+ .command('login')
120
+ .description('Authenticate with the registry and store token in ~/.tapestryrc')
121
+ .action(async () => {
122
+ try {
123
+ await login();
124
+ } catch (e) {
125
+ console.error(`error: ${e.message}`);
126
+ process.exit(1);
127
+ }
128
+ });
129
+
130
+ program
131
+ .command('register')
132
+ .description('Create an account on the registry')
133
+ .action(async () => {
134
+ try {
135
+ await register();
136
+ } catch (e) {
137
+ console.error(`error: ${e.message}`);
138
+ process.exit(1);
139
+ }
140
+ });
141
+
142
+ program
143
+ .command('validate')
144
+ .description('Validate tapestry.yaml in the current directory')
145
+ .action(() => {
146
+ try {
147
+ validate();
148
+ } catch (e) {
149
+ console.error(`error: ${e.message}`);
150
+ process.exit(1);
151
+ }
152
+ });
153
+
154
+ program
155
+ .command('pack')
156
+ .description('Build a tarball from the current pack directory for local inspection')
157
+ .action(async () => {
158
+ try {
159
+ await pack();
160
+ } catch (e) {
161
+ console.error(`error: ${e.message}`);
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ program
167
+ .command('publish')
168
+ .description('Build and upload the current pack to the registry')
169
+ .action(async () => {
170
+ try {
171
+ await publish();
172
+ } catch (e) {
173
+ console.error(`error: ${e.message}`);
174
+ process.exit(1);
175
+ }
176
+ });
177
+
178
+ program
179
+ .command('search <query>')
180
+ .description('Search the registry by keyword')
181
+ .action(async (query) => {
182
+ try {
183
+ await search(query);
184
+ } catch (e) {
185
+ console.error(`error: ${e.message}`);
186
+ process.exit(1);
187
+ }
188
+ });
189
+
190
+ program
191
+ .command('info <package>')
192
+ .description('Show details for a registry package')
193
+ .action(async (pkg) => {
194
+ try {
195
+ await info(pkg);
196
+ } catch (e) {
197
+ console.error(`error: ${e.message}`);
198
+ process.exit(1);
199
+ }
200
+ });
201
+
202
+ program
203
+ .command('list')
204
+ .description('Show installed packages with version, type, and enabled/disabled status')
205
+ .action(async () => {
206
+ try {
207
+ await list();
208
+ } catch (e) {
209
+ console.error(`error: ${e.message}`);
210
+ process.exit(1);
211
+ }
212
+ });
213
+
214
+ program
215
+ .command('outdated')
216
+ .description('Show installed packages with newer versions available in the registry')
217
+ .action(async () => {
218
+ try {
219
+ await outdated();
220
+ } catch (e) {
221
+ console.error(`error: ${e.message}`);
222
+ process.exit(1);
223
+ }
224
+ });
225
+
226
+ const engineCmd = program.command('engine').description('Manage the Tapestry engine');
227
+
228
+ engineCmd
229
+ .command('install')
230
+ .description('Fetch the engine artifact for the configured mode (docker/binary/source)')
231
+ .action(async () => {
232
+ try {
233
+ await engineInstall();
234
+ } catch (e) {
235
+ console.error(`error: ${e.message}`);
236
+ process.exit(1);
237
+ }
238
+ });
239
+
240
+ engineCmd
241
+ .command('update')
242
+ .description('Update the engine to the configured version')
243
+ .action(async () => {
244
+ try {
245
+ await engineUpdate();
246
+ } catch (e) {
247
+ console.error(`error: ${e.message}`);
248
+ process.exit(1);
249
+ }
250
+ });
251
+
252
+ engineCmd
253
+ .command('info')
254
+ .description('Show installed engine version, mode, and image or path')
255
+ .action(() => {
256
+ try {
257
+ engineInfo();
258
+ } catch (e) {
259
+ console.error(`error: ${e.message}`);
260
+ process.exit(1);
261
+ }
262
+ });
263
+
264
+ program
265
+ .command('start')
266
+ .description('Launch the Tapestry engine')
267
+ .action(async () => {
268
+ try {
269
+ await startCmd();
270
+ } catch (e) {
271
+ console.error(`error: ${e.message}`);
272
+ process.exit(1);
273
+ }
274
+ });
275
+
276
+ program
277
+ .command('stop')
278
+ .description('Stop the running Tapestry engine')
279
+ .action(async () => {
280
+ try {
281
+ await stopCmd();
282
+ } catch (e) {
283
+ console.error(`error: ${e.message}`);
284
+ process.exit(1);
285
+ }
286
+ });
287
+
288
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@tapestry-mud/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for the Tapestry MUD engine",
5
+ "bin": {
6
+ "tapestry": "./bin/tapestry.js"
7
+ },
8
+ "scripts": {
9
+ "test": "jest"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^11.1.0",
16
+ "form-data": "^4.0.5",
17
+ "js-yaml": "^4.1.0",
18
+ "node-fetch": "^2.7.0",
19
+ "semver": "^7.6.2",
20
+ "tar": "^6.2.1",
21
+ "zod": "^3.22.4"
22
+ },
23
+ "devDependencies": {
24
+ "jest": "^29.7.0"
25
+ }
26
+ }
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { generatePackFiles } = require('../scaffold/templates');
6
+
7
+ function parseName(name) {
8
+ const scopedMatch = name.match(/^@([a-z0-9-]+)\/([a-z0-9-]+)$/);
9
+ if (scopedMatch) {
10
+ {
11
+ return { scopedName: name, shortName: scopedMatch[2], scope: scopedMatch[1] };
12
+ }
13
+ }
14
+ const plainMatch = name.match(/^[a-z0-9-]+$/);
15
+ if (plainMatch) {
16
+ {
17
+ return { scopedName: `@todo/${name}`, shortName: name, scope: 'todo' };
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function createPack(name, cwd) {
24
+ {
25
+ if (cwd === undefined) {
26
+ {
27
+ cwd = process.cwd();
28
+ }
29
+ }
30
+ const parsed = parseName(name);
31
+ if (!parsed) {
32
+ {
33
+ throw new Error(
34
+ `Invalid pack name: ${name}\n` +
35
+ 'Expected @scope/name or plain-name (lowercase letters and hyphens only)'
36
+ );
37
+ }
38
+ }
39
+
40
+ const packDir = path.join(cwd, parsed.shortName);
41
+ if (fs.existsSync(packDir)) {
42
+ {
43
+ throw new Error(`Directory already exists: ${packDir}`);
44
+ }
45
+ }
46
+
47
+ const files = generatePackFiles(parsed);
48
+ for (const file of files) {
49
+ {
50
+ const filePath = path.join(packDir, file.path);
51
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
52
+ fs.writeFileSync(filePath, file.content);
53
+ }
54
+ }
55
+
56
+ console.log(`Created pack: ${parsed.scopedName}`);
57
+ for (const file of files) {
58
+ {
59
+ console.log(` ${file.path}`);
60
+ }
61
+ }
62
+ console.log('\nEdit tapestry.yaml, then run: tapestry validate');
63
+ }
64
+ }
65
+
66
+ module.exports = { createPack, parseName };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { disablePackage } = require('../lib/boot');
6
+
7
+ async function disable(packageName, { cwd = process.cwd() } = {}) {
8
+ const manifestPath = path.join(cwd, 'tapestry.yaml');
9
+ if (!fs.existsSync(manifestPath)) {
10
+ throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
11
+ }
12
+ disablePackage(cwd, packageName);
13
+ console.log(`Disabled ${packageName}. Files remain on disk. Run \`tapestry enable ${packageName}\` to re-activate.`);
14
+ }
15
+
16
+ module.exports = { disable };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { enablePackage } = require('../lib/boot');
6
+
7
+ async function enable(packageName, { cwd = process.cwd() } = {}) {
8
+ const manifestPath = path.join(cwd, 'tapestry.yaml');
9
+ if (!fs.existsSync(manifestPath)) {
10
+ throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
11
+ }
12
+ enablePackage(cwd, packageName);
13
+ console.log(`Enabled ${packageName}.`);
14
+ }
15
+
16
+ module.exports = { enable };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { installEngine, updateEngine, getEngineInfo } = require('../lib/engine-manager');
4
+
5
+ async function engineInstall({ cwd = process.cwd() } = {}) {
6
+ await installEngine(cwd);
7
+ }
8
+
9
+ async function engineUpdate({ cwd = process.cwd() } = {}) {
10
+ await updateEngine(cwd);
11
+ }
12
+
13
+ function engineInfo({ cwd = process.cwd() } = {}) {
14
+ const info = getEngineInfo(cwd);
15
+ console.log(`Mode: ${info.mode}`);
16
+ console.log(`Version: ${info.version}`);
17
+ if (info.mode === 'docker') {
18
+ console.log(`Image: ${info.image}`);
19
+ } else {
20
+ console.log(`Path: ${info.path}`);
21
+ console.log(`Status: ${info.installed ? 'installed' : 'not installed'}`);
22
+ }
23
+ }
24
+
25
+ module.exports = { engineInstall, engineUpdate, engineInfo };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { fetchPackageMetadata } = require('../lib/registry-client');
4
+
5
+ async function info(packageName, { registryUrl } = {}) {
6
+ if (!packageName) {
7
+ throw new Error('Usage: tapestry info <package>');
8
+ }
9
+
10
+ const data = await fetchPackageMetadata(packageName, registryUrl);
11
+ const latest = data.versions?.[0];
12
+ const m = latest?.manifest || {};
13
+
14
+ console.log(data.name);
15
+ if (m.description) {
16
+ console.log(m.description);
17
+ }
18
+ console.log('');
19
+ if (m.author) {
20
+ console.log(` Author: ${typeof m.author === 'string' ? m.author : m.author.handle}`);
21
+ }
22
+ if (m.license) {
23
+ console.log(` License: ${m.license}`);
24
+ }
25
+ if (m.type) {
26
+ console.log(` Type: ${m.type}`);
27
+ }
28
+ if (latest?.version) {
29
+ console.log(` Latest: ${latest.version}`);
30
+ }
31
+
32
+ if (data.versions?.length > 1) {
33
+ console.log(` Versions: ${data.versions.map((v) => v.version).join(' ')}`);
34
+ }
35
+
36
+ const deps = m.dependencies ? Object.entries(m.dependencies) : [];
37
+ if (deps.length) {
38
+ console.log('');
39
+ console.log(' Dependencies:');
40
+ for (const [dep, range] of deps) {
41
+ console.log(` ${dep} ${range}`);
42
+ }
43
+ }
44
+
45
+ if (m.meta?.keywords?.length) {
46
+ console.log('');
47
+ console.log(` Keywords: ${m.meta.keywords.join(', ')}`);
48
+ }
49
+ }
50
+
51
+ module.exports = { info };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function buildManifest(name) {
7
+ return [
8
+ `name: ${name}`,
9
+ `engine:`,
10
+ ` version: '0.0.1'`,
11
+ ` mode: docker`,
12
+ ` image: ghcr.io/tapestry-mud/tapestry`,
13
+ `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'`,
17
+ `packs: []`,
18
+ `tag_validation: strict`,
19
+ ``,
20
+ ].join('\n');
21
+ }
22
+
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');
60
+ }
61
+ }
62
+
63
+ module.exports = { init };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { readYaml, writeYaml } = require('../util/yaml');
7
+ const { resolve } = require('../lib/semver-resolver');
8
+ const { readLock, writeLock } = require('../lib/lock-file');
9
+ const { fetchTarball, DEFAULT_REGISTRY } = require('../lib/registry-client');
10
+ const { verifyIntegrity, saveTarball, extractTarball } = require('../lib/tarball');
11
+ const { addPackageToBoot } = require('../lib/boot');
12
+
13
+ function packInstallPath(cwd, packageName) {
14
+ const parts = packageName.split('/');
15
+ return path.join(cwd, 'packs', ...parts);
16
+ }
17
+
18
+ function parsePackageArg(arg) {
19
+ const match = arg.match(/^(@[^@/]+\/[^@]+)(?:@(.+))?$/);
20
+ if (!match) {
21
+ throw new Error(`Invalid package name: ${arg}. Expected @scope/name or @scope/name@range`);
22
+ }
23
+ return { name: match[1], rawRange: match[2] || null };
24
+ }
25
+
26
+ function isLockCurrent(manifestDeps, lock) {
27
+ const lockResolved = lock.resolved || {};
28
+ return Object.keys(manifestDeps).every((name) => lockResolved[name]);
29
+ }
30
+
31
+ async function installResolved(cwd, resolved) {
32
+ for (const [packageName, info] of Object.entries(resolved)) {
33
+ const destDir = packInstallPath(cwd, packageName);
34
+
35
+ if (fs.existsSync(destDir)) {
36
+ console.log(` already installed ${packageName}@${info.version}`);
37
+ continue;
38
+ }
39
+
40
+ console.log(` installing ${packageName}@${info.version}`);
41
+
42
+ const safeId = packageName.replace('@', '').replace('/', '-');
43
+ const tmpPath = path.join(os.tmpdir(), `tapestry-${safeId}-${info.version}.tgz`);
44
+
45
+ try {
46
+ const buffer = await fetchTarball(info.tarball);
47
+ verifyIntegrity(buffer, info.integrity);
48
+ saveTarball(buffer, tmpPath);
49
+ await extractTarball(tmpPath, destDir);
50
+ } finally {
51
+ if (fs.existsSync(tmpPath)) {
52
+ fs.unlinkSync(tmpPath);
53
+ }
54
+ }
55
+
56
+ const packManifest = readYaml(path.join(destDir, 'tapestry.yaml'));
57
+ addPackageToBoot(cwd, packageName, packManifest);
58
+ }
59
+ }
60
+
61
+ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_REGISTRY } = {}) {
62
+ const manifestPath = path.join(cwd, 'tapestry.yaml');
63
+ if (!fs.existsSync(manifestPath)) {
64
+ throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
65
+ }
66
+
67
+ const manifest = readYaml(manifestPath);
68
+ let resolved;
69
+
70
+ if (packageArg) {
71
+ const { name, rawRange } = parsePackageArg(packageArg);
72
+ manifest.dependencies = manifest.dependencies || {};
73
+ const tempRange = rawRange || '*';
74
+ manifest.dependencies[name] = tempRange;
75
+
76
+ console.log('Resolving dependencies...');
77
+ resolved = await resolve(manifest.dependencies, registryUrl);
78
+
79
+ if (!rawRange) {
80
+ manifest.dependencies[name] = `^${resolved[name].version}`;
81
+ }
82
+
83
+ writeYaml(manifestPath, manifest);
84
+ } else {
85
+ const lock = readLock(cwd);
86
+ if (lock && isLockCurrent(manifest.dependencies || {}, lock)) {
87
+ console.log('Installing from lock file...');
88
+ resolved = lock.resolved;
89
+ } else {
90
+ console.log('Resolving dependencies...');
91
+ resolved = await resolve(manifest.dependencies || {}, registryUrl);
92
+ }
93
+ }
94
+
95
+ await installResolved(cwd, resolved);
96
+ writeLock(cwd, { lockfile_version: 1, resolved });
97
+ console.log('Done.');
98
+ }
99
+
100
+ module.exports = { install };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readLock } = require('../lib/lock-file');
6
+ const { readBoot } = require('../lib/boot');
7
+ const { readYaml } = require('../util/yaml');
8
+
9
+ function packInstallPath(cwd, packageName) {
10
+ const parts = packageName.split('/');
11
+ return path.join(cwd, 'packs', ...parts);
12
+ }
13
+
14
+ async function list({ cwd = process.cwd() } = {}) {
15
+ const lock = readLock(cwd);
16
+ if (!lock || !lock.resolved || !Object.keys(lock.resolved).length) {
17
+ console.log('No packages installed.');
18
+ return;
19
+ }
20
+
21
+ const boot = readBoot(cwd);
22
+ const packages = Object.entries(lock.resolved);
23
+
24
+ const nameWidth = Math.max(7, ...packages.map(([n]) => n.length));
25
+ const verWidth = Math.max(7, ...packages.map(([, r]) => r.version.length));
26
+
27
+ console.log(
28
+ `${'PACKAGE'.padEnd(nameWidth)} ${'VERSION'.padEnd(verWidth)} TYPE STATUS`
29
+ );
30
+
31
+ for (const [pkgName, resolved] of packages) {
32
+ const enabled = boot.packs[pkgName]?.enabled !== false ? 'enabled' : 'disabled';
33
+
34
+ let type = '';
35
+ const packManifestPath = path.join(packInstallPath(cwd, pkgName), 'tapestry.yaml');
36
+ if (fs.existsSync(packManifestPath)) {
37
+ try {
38
+ type = readYaml(packManifestPath).type || '';
39
+ } catch {
40
+ //
41
+ }
42
+ }
43
+
44
+ console.log(
45
+ `${pkgName.padEnd(nameWidth)} ${resolved.version.padEnd(verWidth)} ${type.padEnd(8)} ${enabled}`
46
+ );
47
+ }
48
+ }
49
+
50
+ module.exports = { list };