@tapestry-mud/cli 0.3.6 → 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 +3 -2
- package/package.json +2 -1
- package/src/commands/init.js +201 -22
- package/src/lib/registry-client.js +11 -1
- package/src/scaffold/templates.js +4 -4
- package/src/schema/manifest.js +2 -2
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.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",
|
package/src/commands/init.js
CHANGED
|
@@ -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)
|
|
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: '
|
|
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
|
-
`
|
|
19
|
-
`tag_validation: strict`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
62
|
-
console.log(
|
|
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 (
|
|
65
|
-
console.log('
|
|
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
|
-
|
|
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
|
};
|
|
@@ -30,9 +30,9 @@ engine: ">=3.0.0"
|
|
|
30
30
|
provides:
|
|
31
31
|
- example
|
|
32
32
|
|
|
33
|
-
# strict: undeclared tags
|
|
34
|
-
# lenient:
|
|
35
|
-
|
|
33
|
+
# strict: undeclared tags and unregistered properties cause load failure
|
|
34
|
+
# lenient: logs warnings, pack still loads
|
|
35
|
+
validation: strict
|
|
36
36
|
|
|
37
37
|
# Path to tag declarations file
|
|
38
38
|
tags: "tags.yml"
|
|
@@ -56,7 +56,7 @@ meta:
|
|
|
56
56
|
function tagsTemplate() {
|
|
57
57
|
return `# Tag declarations for this pack.
|
|
58
58
|
# Tags listed here can be used on entities (items, npcs, rooms, areas).
|
|
59
|
-
# Undeclared tags cause load failure when
|
|
59
|
+
# Undeclared tags cause load failure when validation is strict.
|
|
60
60
|
#
|
|
61
61
|
# Convention: always snake_case (e.g., safe_recall, not safe-recall)
|
|
62
62
|
# applies_to: which entity types accept this tag
|
package/src/schema/manifest.js
CHANGED
|
@@ -19,7 +19,7 @@ const PackageManifestSchema = z.object({
|
|
|
19
19
|
]),
|
|
20
20
|
license: z.string().min(1),
|
|
21
21
|
engine: z.string().min(1),
|
|
22
|
-
|
|
22
|
+
validation: z.enum(['strict', 'lenient']),
|
|
23
23
|
dependencies: z.record(z.string()).optional(),
|
|
24
24
|
peerDependencies: z.record(z.string()).optional(),
|
|
25
25
|
provides: z.array(z.string()).optional(),
|
|
@@ -56,7 +56,7 @@ const ProjectManifestSchema = z.object({
|
|
|
56
56
|
]),
|
|
57
57
|
dependencies: z.record(z.string()).optional(),
|
|
58
58
|
packs: z.array(z.string()).optional(),
|
|
59
|
-
|
|
59
|
+
validation: z.enum(['strict', 'lenient']).optional(),
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
function validatePackageManifest(data) {
|