@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 +3 -2
- package/package.json +2 -1
- package/src/commands/init.js +196 -21
- package/src/lib/engine-manager.js +10 -5
- package/src/lib/registry-client.js +11 -1
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
|
-
.
|
|
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.
|
|
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",
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
8
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
62
|
-
console.log(
|
|
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 (
|
|
65
|
-
console.log('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
};
|