@tapestry-mud/cli 0.3.7 → 0.3.8

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.8",
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,109 @@
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 buildManifest(name, deps, engineVersion, engineChannel) {
8
+ const depLines = Object.entries(deps)
9
+ .map(([pkg, range]) => ` '${pkg}': '${range}'`)
10
+ .join('\n');
9
11
  return [
10
12
  `name: ${name}`,
11
13
  `engine:`,
12
- ` version: '0.0.1'`,
14
+ ` version: '${engineVersion}'`,
15
+ ` channel: ${engineChannel}`,
13
16
  ` mode: docker`,
14
17
  ` image: ghcr.io/tapestry-mud/tapestry`,
15
18
  `dependencies:`,
16
19
  depLines,
17
20
  ` # Add more packs here. Run: tapestry install @scope/pack-name`,
18
- `packs: []`,
19
21
  `validation: strict`,
20
22
  ``,
21
23
  `# Server port, admin seed account, and engine settings are in server.yaml`,
22
24
  ].join('\n');
23
25
  }
24
26
 
25
- async function init(cwd, { registryUrl = DEFAULT_REGISTRY } = {}) {
27
+ function buildServerYaml({ serverName, adminHandle, adminPassword, telemetry }) {
28
+ const telemetryBlock = telemetry
29
+ ? [
30
+ `telemetry:`,
31
+ ` enabled: true`,
32
+ ` endpoint: "http://localhost:4317"`,
33
+ ` protocol: grpc`,
34
+ ` service_name: tapestry`,
35
+ ].join('\n')
36
+ : [
37
+ `# telemetry:`,
38
+ `# enabled: true`,
39
+ `# endpoint: "http://localhost:4317"`,
40
+ `# protocol: grpc`,
41
+ `# service_name: tapestry`,
42
+ ].join('\n');
43
+
44
+ return [
45
+ `# Tapestry server configuration`,
46
+ `# Uncomment and modify sections as needed.`,
47
+ `# Docs: https://tapestryengine.com/docs/config`,
48
+ ``,
49
+ `server:`,
50
+ ` name: "${serverName}"`,
51
+ ` telnet_port: 4000`,
52
+ ` websocket_port: 4001`,
53
+ ` max_connections: 200`,
54
+ ` tick_rate_ms: 100`,
55
+ ``,
56
+ `admin:`,
57
+ ` handle: ${adminHandle}`,
58
+ ` password: ${adminPassword}`,
59
+ ``,
60
+ `# --- Telemetry (OpenTelemetry) ---`,
61
+ `# Requires the observability stack. In 0.4.0: tapestry telemetry start`,
62
+ telemetryBlock,
63
+ ``,
64
+ `# --- Logging ---`,
65
+ `# logging:`,
66
+ `# level: Information`,
67
+ ``,
68
+ `# --- Persistence ---`,
69
+ `# persistence:`,
70
+ `# save_path: "./data/saves"`,
71
+ `# connections_path: "./data/connections"`,
72
+ `# autosave_interval: 3000`,
73
+ `# password_min_length: 6`,
74
+ `# max_login_attempts: 5`,
75
+ ``,
76
+ `# --- Idle Timeouts ---`,
77
+ `# idle:`,
78
+ `# pre_login_timeout_seconds: 120`,
79
+ `# phase_timeouts:`,
80
+ `# name: 60`,
81
+ `# password: 30`,
82
+ `# session_takeover: 15`,
83
+ `# creating: 300`,
84
+ ``,
85
+ `# --- MSSP (MUD Server Status Protocol) ---`,
86
+ `# mssp:`,
87
+ `# name: "${serverName}"`,
88
+ `# codebase: "Tapestry"`,
89
+ `# hostname: ""`,
90
+ `# port: 4000`,
91
+ ``,
92
+ `# --- LLM ---`,
93
+ `# llm:`,
94
+ `# provider: none`,
95
+ ``,
96
+ `# --- Networking ---`,
97
+ `# networking:`,
98
+ `# negotiation_timeout_ms: 500`,
99
+ ``,
100
+ `# --- Pre-Auth (web client token auth) ---`,
101
+ `# pre_auth:`,
102
+ `# enabled: false`,
103
+ `# token_expiry_seconds: 60`,
104
+ ].join('\n');
105
+ }
106
+
107
+ async function init(cwd, { registryUrl = DEFAULT_REGISTRY, yes = false, prompter = null } = {}) {
26
108
  if (cwd === undefined) {
27
109
  cwd = process.cwd();
28
110
  }
@@ -34,38 +116,135 @@ async function init(cwd, { registryUrl = DEFAULT_REGISTRY } = {}) {
34
116
 
35
117
  let preset;
36
118
  try {
37
- preset = await fetchPreset('starter', registryUrl);
119
+ const presets = await fetchPresetList(registryUrl);
120
+ if (presets === null) {
121
+ preset = await fetchPreset('starter', registryUrl);
122
+ console.log(`Using preset: starter (engine v${preset.version}, ${preset.engine_channel} channel)`);
123
+ } else if (presets.length === 1) {
124
+ console.log(`Using preset: ${presets[0].name} (engine v${presets[0].version}, ${presets[0].engine_channel} channel)`);
125
+ preset = await fetchPreset(presets[0].name, registryUrl);
126
+ } else {
127
+ const doPrompt = prompter || require('inquirer').prompt;
128
+ const { selectedPreset } = await doPrompt([{
129
+ type: 'list',
130
+ name: 'selectedPreset',
131
+ message: 'Select a preset:',
132
+ choices: presets.map(p => ({
133
+ name: `${p.name} (engine v${p.version}, ${p.engine_channel} channel)`,
134
+ value: p.name,
135
+ })),
136
+ }]);
137
+ preset = await fetchPreset(selectedPreset, registryUrl);
138
+ console.log(`Using preset: ${selectedPreset} (engine v${preset.version}, ${preset.engine_channel} channel)`);
139
+ }
38
140
  } catch (e) {
39
- throw new Error(`Failed to fetch starter preset from registry: ${e.message}. Check your connection and try again.`);
141
+ throw new Error(`Failed to fetch presets from registry: ${e.message}. Check your connection and try again.`);
40
142
  }
41
143
 
42
- console.log(`Initializing Tapestry Starter v${preset.version}`);
43
-
44
144
  const deps = {};
45
145
  for (const [pkg, ver] of Object.entries(preset.packs)) {
46
146
  deps[pkg] = `^${ver}`;
47
147
  }
48
148
 
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');
149
+ const dirName = path.basename(cwd);
150
+ let answers;
151
+
152
+ if (yes) {
153
+ answers = {
154
+ gameName: dirName,
155
+ adminHandle: 'admin',
156
+ adminPassword: 'changeme',
157
+ serverName: dirName,
158
+ telemetry: false,
159
+ };
160
+ console.warn('Default admin credentials -- change in server.yaml before production use.');
161
+ } else {
162
+ const doPrompt = prompter || require('inquirer').prompt;
163
+ answers = await doPrompt([
164
+ {
165
+ type: 'input',
166
+ name: 'gameName',
167
+ message: 'Game name:',
168
+ default: dirName,
169
+ validate: (v) => (v.trim().length > 0 && /^[a-zA-Z0-9_.-]+$/.test(v.trim())) || 'Must be non-empty and filesystem-safe',
170
+ },
171
+ {
172
+ type: 'input',
173
+ name: 'adminHandle',
174
+ message: 'Admin handle:',
175
+ validate: (v) => (v.trim().length > 0 && !/\s/.test(v)) || 'Required, no spaces',
176
+ },
177
+ {
178
+ type: 'password',
179
+ name: 'adminPassword',
180
+ message: 'Admin password:',
181
+ mask: '*',
182
+ validate: (v) => v.length >= 6 || 'Minimum 6 characters',
183
+ },
184
+ {
185
+ type: 'password',
186
+ name: 'adminPasswordConfirm',
187
+ message: 'Confirm admin password:',
188
+ mask: '*',
189
+ validate: (v, a) => v === a.adminPassword || 'Passwords do not match',
190
+ },
191
+ {
192
+ type: 'input',
193
+ name: 'serverName',
194
+ message: 'Server name:',
195
+ default: (a) => a.gameName,
196
+ validate: (v) => v.trim().length > 0 || 'Required',
197
+ },
198
+ {
199
+ type: 'confirm',
200
+ name: 'telemetry',
201
+ message: 'Enable telemetry?',
202
+ default: false,
203
+ },
204
+ ]);
205
+ }
206
+
207
+ const name = answers.gameName;
208
+
209
+ fs.writeFileSync(manifestPath, buildManifest(name, deps, preset.version, preset.engine_channel));
210
+ fs.writeFileSync(
211
+ path.join(cwd, 'server.yaml'),
212
+ buildServerYaml({
213
+ serverName: answers.serverName,
214
+ adminHandle: answers.adminHandle,
215
+ adminPassword: answers.adminPassword,
216
+ telemetry: answers.telemetry,
217
+ })
218
+ );
52
219
  fs.mkdirSync(path.join(cwd, 'packs'), { recursive: true });
53
220
  fs.writeFileSync(
54
221
  path.join(cwd, '.gitignore'),
55
222
  '# 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
223
  );
57
224
 
225
+ console.log('');
58
226
  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');
227
+ console.log(` tapestry.yaml project manifest (engine v${preset.version})`);
228
+ console.log(` server.yaml engine config`);
229
+ console.log(` packs/ installed packages`);
230
+ console.log(` .gitignore excludes packs/ and .tapestry-engine/ from git`);
231
+ console.log('');
232
+ console.log('Next steps:');
233
+ console.log(' tapestry install install packs');
234
+ console.log(' tapestry engine install pull the engine image');
235
+ console.log(' tapestry start boot the server');
63
236
 
64
- if (!fs.existsSync(path.join(cwd, '.git'))) {
65
- console.log('\nHint: no git repo detected. Run: git init');
237
+ if (answers.telemetry) {
238
+ console.log('');
239
+ console.log(' Telemetry is enabled in server.yaml.');
240
+ console.log(' The observability stack (Grafana, Prometheus, Loki, Jaeger) must be');
241
+ console.log(' running for telemetry data to be collected. See docs for setup.');
66
242
  }
67
243
 
68
- console.log('\nNext: run tapestry install, then tapestry engine install, then tapestry start');
244
+ if (!fs.existsSync(path.join(cwd, '.git'))) {
245
+ console.log('');
246
+ console.log('Hint: no git repo detected. Run: git init');
247
+ }
69
248
  }
70
249
 
71
- module.exports = { init };
250
+ module.exports = { init, buildManifest, buildServerYaml };
@@ -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
  };