@tapestry-mud/cli 0.3.7 → 0.3.9

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/bin/tapestry.js CHANGED
@@ -103,9 +103,10 @@ program.configureHelp({
103
103
  program
104
104
  .command('init')
105
105
  .description('Initialize a new Tapestry game project in the current directory')
106
- .action(async () => {
106
+ .option('-y, --yes', 'Skip prompts and use defaults (for CI and scripting)')
107
+ .action(async (options) => {
107
108
  try {
108
- await init();
109
+ await init(undefined, { yes: !!options.yes });
109
110
  } catch (e) {
110
111
  console.error(`error: ${e.message}`);
111
112
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapestry-mud/cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "CLI for the Tapestry MUD engine",
5
5
  "bin": {
6
6
  "tapestry": "./bin/tapestry.js"
@@ -14,6 +14,7 @@
14
14
  "dependencies": {
15
15
  "commander": "^11.1.0",
16
16
  "form-data": "^4.0.5",
17
+ "inquirer": "^8.2.6",
17
18
  "js-yaml": "^4.1.0",
18
19
  "node-fetch": "^2.7.0",
19
20
  "semver": "^7.6.2",
@@ -2,27 +2,113 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { fetchPreset, DEFAULT_REGISTRY } = require('../lib/registry-client');
5
+ const { fetchPreset, fetchPresetList, DEFAULT_REGISTRY } = require('../lib/registry-client');
6
6
 
7
- function buildManifest(name, deps) {
8
- const depLines = Object.entries(deps).map(([pkg, range]) => ` '${pkg}': '${range}'`).join('\n');
7
+ function slugify(str) {
8
+ return str.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9_.-]/g, '');
9
+ }
10
+
11
+ function buildManifest(name, deps, engineVersion, engineChannel) {
12
+ const depLines = Object.entries(deps)
13
+ .map(([pkg, range]) => ` '${pkg}': '${range}'`)
14
+ .join('\n');
9
15
  return [
10
16
  `name: ${name}`,
11
17
  `engine:`,
12
- ` version: '0.0.1'`,
18
+ ` version: '${engineVersion}'`,
19
+ ` channel: ${engineChannel}`,
13
20
  ` mode: docker`,
14
21
  ` image: ghcr.io/tapestry-mud/tapestry`,
15
22
  `dependencies:`,
16
23
  depLines,
17
24
  ` # Add more packs here. Run: tapestry install @scope/pack-name`,
18
- `packs: []`,
19
25
  `validation: strict`,
20
26
  ``,
21
27
  `# Server port, admin seed account, and engine settings are in server.yaml`,
22
28
  ].join('\n');
23
29
  }
24
30
 
25
- async function init(cwd, { registryUrl = DEFAULT_REGISTRY } = {}) {
31
+ function buildServerYaml({ serverName, adminHandle, adminPassword, telemetry }) {
32
+ const telemetryBlock = telemetry
33
+ ? [
34
+ `telemetry:`,
35
+ ` enabled: true`,
36
+ ` endpoint: "http://localhost:4317"`,
37
+ ` protocol: grpc`,
38
+ ` service_name: tapestry`,
39
+ ].join('\n')
40
+ : [
41
+ `# telemetry:`,
42
+ `# enabled: true`,
43
+ `# endpoint: "http://localhost:4317"`,
44
+ `# protocol: grpc`,
45
+ `# service_name: tapestry`,
46
+ ].join('\n');
47
+
48
+ return [
49
+ `# Tapestry server configuration`,
50
+ `# Uncomment and modify sections as needed.`,
51
+ `# Docs: https://tapestryengine.com/docs/config`,
52
+ ``,
53
+ `server:`,
54
+ ` name: "${serverName}"`,
55
+ ` telnet_port: 4000`,
56
+ ` websocket_port: 4001`,
57
+ ` max_connections: 200`,
58
+ ` tick_rate_ms: 100`,
59
+ ``,
60
+ `admin:`,
61
+ ` handle: ${adminHandle}`,
62
+ ` password: ${adminPassword}`,
63
+ ``,
64
+ `# --- Telemetry (OpenTelemetry) ---`,
65
+ `# Requires the observability stack. In 0.4.0: tapestry telemetry start`,
66
+ telemetryBlock,
67
+ ``,
68
+ `# --- Logging ---`,
69
+ `# logging:`,
70
+ `# level: Information`,
71
+ ``,
72
+ `# --- Persistence ---`,
73
+ `# persistence:`,
74
+ `# save_path: "./data/saves"`,
75
+ `# connections_path: "./data/connections"`,
76
+ `# autosave_interval: 3000`,
77
+ `# password_min_length: 6`,
78
+ `# max_login_attempts: 5`,
79
+ ``,
80
+ `# --- Idle Timeouts ---`,
81
+ `# idle:`,
82
+ `# pre_login_timeout_seconds: 120`,
83
+ `# phase_timeouts:`,
84
+ `# name: 60`,
85
+ `# password: 30`,
86
+ `# session_takeover: 15`,
87
+ `# creating: 300`,
88
+ ``,
89
+ `# --- MSSP (MUD Server Status Protocol) ---`,
90
+ `# mssp:`,
91
+ `# name: "${serverName}"`,
92
+ `# codebase: "Tapestry"`,
93
+ `# hostname: ""`,
94
+ `# port: 4000`,
95
+ ``,
96
+ `# --- LLM ---`,
97
+ `# llm:`,
98
+ `# provider: none`,
99
+ ``,
100
+ `# --- Networking ---`,
101
+ `# networking:`,
102
+ `# negotiation_timeout_ms: 500`,
103
+ ``,
104
+ `# --- Pre-Auth (web client token auth) ---`,
105
+ `# pre_auth:`,
106
+ `# enabled: false`,
107
+ `# token_expiry_seconds: 60`,
108
+ ].join('\n');
109
+ }
110
+
111
+ async function init(cwd, { registryUrl = DEFAULT_REGISTRY, yes = false, prompter = null } = {}) {
26
112
  if (cwd === undefined) {
27
113
  cwd = process.cwd();
28
114
  }
@@ -34,38 +120,127 @@ async function init(cwd, { registryUrl = DEFAULT_REGISTRY } = {}) {
34
120
 
35
121
  let preset;
36
122
  try {
37
- preset = await fetchPreset('starter', registryUrl);
123
+ const presets = await fetchPresetList(registryUrl);
124
+ if (presets === null) {
125
+ preset = await fetchPreset('starter', registryUrl);
126
+ console.log(`Using preset: starter (engine v${preset.version}, ${preset.engine_channel} channel)`);
127
+ } else if (presets.length === 1) {
128
+ console.log(`Using preset: ${presets[0].name} (engine v${presets[0].version}, ${presets[0].engine_channel} channel)`);
129
+ preset = await fetchPreset(presets[0].name, registryUrl);
130
+ } else {
131
+ const doPrompt = prompter || require('inquirer').prompt;
132
+ const { selectedPreset } = await doPrompt([{
133
+ type: 'list',
134
+ name: 'selectedPreset',
135
+ message: 'Select a preset:',
136
+ choices: presets.map(p => ({
137
+ name: `${p.name} (engine v${p.version}, ${p.engine_channel} channel)`,
138
+ value: p.name,
139
+ })),
140
+ }]);
141
+ preset = await fetchPreset(selectedPreset, registryUrl);
142
+ console.log(`Using preset: ${selectedPreset} (engine v${preset.version}, ${preset.engine_channel} channel)`);
143
+ }
38
144
  } catch (e) {
39
- throw new Error(`Failed to fetch starter preset from registry: ${e.message}. Check your connection and try again.`);
145
+ throw new Error(`Failed to fetch presets from registry: ${e.message}. Check your connection and try again.`);
40
146
  }
41
147
 
42
- console.log(`Initializing Tapestry Starter v${preset.version}`);
43
-
44
148
  const deps = {};
45
149
  for (const [pkg, ver] of Object.entries(preset.packs)) {
46
150
  deps[pkg] = `^${ver}`;
47
151
  }
48
152
 
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');
153
+ const dirName = path.basename(cwd);
154
+ let answers;
155
+
156
+ if (yes) {
157
+ answers = {
158
+ gameName: dirName,
159
+ adminHandle: 'admin',
160
+ adminPassword: 'changeme',
161
+ telemetry: false,
162
+ };
163
+ console.warn('Default admin credentials -- change in server.yaml before production use.');
164
+ } else {
165
+ const doPrompt = prompter || require('inquirer').prompt;
166
+ answers = await doPrompt([
167
+ {
168
+ type: 'input',
169
+ name: 'gameName',
170
+ message: 'Game name:',
171
+ default: dirName,
172
+ validate: (v) => v.trim().length > 0 || 'Required',
173
+ },
174
+ {
175
+ type: 'input',
176
+ name: 'adminHandle',
177
+ message: 'Admin handle:',
178
+ validate: (v) => (v.trim().length > 0 && !/\s/.test(v)) || 'Required, no spaces',
179
+ },
180
+ {
181
+ type: 'password',
182
+ name: 'adminPassword',
183
+ message: 'Admin password:',
184
+ mask: '*',
185
+ validate: (v) => v.length >= 6 || 'Minimum 6 characters',
186
+ },
187
+ {
188
+ type: 'password',
189
+ name: 'adminPasswordConfirm',
190
+ message: 'Confirm admin password:',
191
+ mask: '*',
192
+ validate: (v, a) => v === a.adminPassword || 'Passwords do not match',
193
+ },
194
+ {
195
+ type: 'confirm',
196
+ name: 'telemetry',
197
+ message: 'Enable telemetry?',
198
+ default: false,
199
+ },
200
+ ]);
201
+ }
202
+
203
+ const name = slugify(answers.gameName);
204
+
205
+ fs.writeFileSync(manifestPath, buildManifest(name, deps, preset.version, preset.engine_channel));
206
+ fs.writeFileSync(
207
+ path.join(cwd, 'server.yaml'),
208
+ buildServerYaml({
209
+ serverName: answers.gameName,
210
+ adminHandle: answers.adminHandle,
211
+ adminPassword: answers.adminPassword,
212
+ telemetry: answers.telemetry,
213
+ })
214
+ );
52
215
  fs.mkdirSync(path.join(cwd, 'packs'), { recursive: true });
53
216
  fs.writeFileSync(
54
217
  path.join(cwd, '.gitignore'),
55
218
  '# 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
219
  );
57
220
 
221
+ console.log('');
58
222
  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');
223
+ console.log(` tapestry.yaml project manifest (engine v${preset.version})`);
224
+ console.log(` server.yaml engine config`);
225
+ console.log(` packs/ installed packages`);
226
+ console.log(` .gitignore excludes packs/ and .tapestry-engine/ from git`);
227
+ console.log('');
228
+ console.log('Next steps:');
229
+ console.log(' tapestry install install packs');
230
+ console.log(' tapestry engine install pull the engine image');
231
+ console.log(' tapestry start boot the server');
63
232
 
64
- if (!fs.existsSync(path.join(cwd, '.git'))) {
65
- console.log('\nHint: no git repo detected. Run: git init');
233
+ if (answers.telemetry) {
234
+ console.log('');
235
+ console.log(' Telemetry is enabled in server.yaml.');
236
+ console.log(' The observability stack (Grafana, Prometheus, Loki, Jaeger) must be');
237
+ console.log(' running for telemetry data to be collected. See docs for setup.');
66
238
  }
67
239
 
68
- console.log('\nNext: run tapestry install, then tapestry engine install, then tapestry start');
240
+ if (!fs.existsSync(path.join(cwd, '.git'))) {
241
+ console.log('');
242
+ console.log('Hint: no git repo detected. Run: git init');
243
+ }
69
244
  }
70
245
 
71
- module.exports = { init };
246
+ module.exports = { init, buildManifest, buildServerYaml, slugify };
@@ -65,11 +65,11 @@ function dockerEnsureImage(image, version) {
65
65
  }
66
66
  }
67
67
 
68
- function dockerStart(projectName, image, version, packsDir, serverYamlPath, dataDir) {
68
+ function dockerStart(projectName, image, version, packsDir, serverYamlPath, dataDir, network) {
69
69
  const containerName = `tapestry-${projectName}`;
70
70
  dockerEnsureImage(image, version);
71
71
  spawnSync('docker', ['rm', '-f', containerName], { stdio: 'ignore' });
72
- const result = spawnSync('docker', [
72
+ const args = [
73
73
  'run', '--detach',
74
74
  '--name', containerName,
75
75
  '-p', '4000:4000',
@@ -77,8 +77,12 @@ function dockerStart(projectName, image, version, packsDir, serverYamlPath, data
77
77
  '-v', `${packsDir}:/app/packs`,
78
78
  '-v', `${serverYamlPath}:/app/server.yaml`,
79
79
  '-v', `${dataDir}:/app/data`,
80
- `${image}:${version}`,
81
- ], { stdio: 'inherit' });
80
+ ];
81
+ if (network) {
82
+ args.push('--network', network);
83
+ }
84
+ args.push(`${image}:${version}`);
85
+ const result = spawnSync('docker', args, { stdio: 'inherit' });
82
86
  if (result.status !== 0) {
83
87
  throw new Error(
84
88
  `docker run failed. Ensure the image exists and no container named '${containerName}' is already running.`
@@ -261,6 +265,7 @@ function readEngineConfig(cwd) {
261
265
  version: engine.version,
262
266
  mode: engine.mode,
263
267
  image: engine.image || DEFAULT_IMAGE,
268
+ network: engine.network || null,
264
269
  installDir: path.join(cwd, '.tapestry-engine'),
265
270
  projectName: (manifest.name || 'tapestry').toLowerCase().replace(/[^a-z0-9-]+/g, '-'),
266
271
  };
@@ -317,7 +322,7 @@ async function startEngine(cwd) {
317
322
  fs.mkdirSync(dataDir, { recursive: true });
318
323
  if (config.mode === 'docker') {
319
324
  const tag = await resolveDockerTag(config);
320
- dockerStart(config.projectName, config.image, tag, packsDir, serverYamlPath, dataDir);
325
+ dockerStart(config.projectName, config.image, tag, packsDir, serverYamlPath, dataDir, config.network);
321
326
  } else if (config.mode === 'binary') {
322
327
  binaryStart(config.version, config.installDir, packsDir, serverYamlPath, cwd);
323
328
  } else {
@@ -69,6 +69,16 @@ async function fetchPreset(name, registryUrl = DEFAULT_REGISTRY) {
69
69
  return res.json();
70
70
  }
71
71
 
72
+ async function fetchPresetList(registryUrl = DEFAULT_REGISTRY) {
73
+ const url = `${registryUrl.replace(/\/$/, '')}/v1/presets`;
74
+ const res = await fetch(url);
75
+ if (res.status === 404) {
76
+ return null;
77
+ }
78
+ await throwIfError(res, 'Failed to fetch preset list');
79
+ return res.json();
80
+ }
81
+
72
82
  async function patchDistTag(packName, tag, version, token, registryUrl = DEFAULT_REGISTRY) {
73
83
  validatePackageName(packName);
74
84
  const url = `${registryUrl.replace(/\/$/, '')}/v1/packages/${packName}/dist-tags/${tag}`;
@@ -108,5 +118,5 @@ async function patchPreset(name, payload, token, registryUrl = DEFAULT_REGISTRY)
108
118
 
109
119
  module.exports = {
110
120
  fetchPackageMetadata, fetchTarball, throwIfError, DEFAULT_REGISTRY,
111
- fetchPreset, patchDistTag, listDistTags, patchPreset,
121
+ fetchPreset, fetchPresetList, patchDistTag, listDistTags, patchPreset,
112
122
  };