miaoda-game-devkit 0.8.0 → 0.8.1
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.
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
existsSync,
|
|
8
8
|
lstatSync,
|
|
9
9
|
mkdirSync,
|
|
10
|
-
readFileSync,
|
|
11
10
|
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
12
|
renameSync,
|
|
13
13
|
rmSync,
|
|
14
14
|
statSync,
|
|
@@ -29,8 +29,7 @@ const SOURCE_ROOT = 'src/game-mechanics';
|
|
|
29
29
|
const STATE_FILENAME = 'mechanics-source.json';
|
|
30
30
|
const WORKSPACE_PATTERN = 'src/game-mechanics/*';
|
|
31
31
|
const STATE_SCHEMA_VERSION = 2;
|
|
32
|
-
const DEFAULT_SOURCE_INDEX_URL =
|
|
33
|
-
'https://resource-static.bj.bcebos.com/miaoda-game/stable.json';
|
|
32
|
+
const DEFAULT_SOURCE_INDEX_URL = 'https://resource-static.bj.bcebos.com/miaoda-game/stable.json';
|
|
34
33
|
|
|
35
34
|
function fail(api, field, expected, repair) {
|
|
36
35
|
throw new Error(`miaoda mechanics source ${api}: ${field} must ${expected}. ${repair}`);
|
|
@@ -41,9 +40,8 @@ function readJson(path, label = path) {
|
|
|
41
40
|
try {
|
|
42
41
|
value = JSON.parse(readFileSync(path, 'utf8'));
|
|
43
42
|
} catch (error) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
43
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
44
|
+
throw new Error(`miaoda mechanics source: cannot read ${label} as JSON: ${detail}`);
|
|
47
45
|
}
|
|
48
46
|
return value;
|
|
49
47
|
}
|
|
@@ -53,15 +51,25 @@ function writeJson(path, value) {
|
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
function sourcePath(value) {
|
|
56
|
-
if (typeof value !== 'string' || !value.startsWith('./dist/'))
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (typeof value !== 'string' || !value.startsWith('./dist/')) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (value.endsWith('.d.ts')) {
|
|
58
|
+
return `./src/${value.slice('./dist/'.length, -'.d.ts'.length)}.ts`;
|
|
59
|
+
}
|
|
60
|
+
if (value.endsWith('.js')) {
|
|
61
|
+
return `./src/${value.slice('./dist/'.length, -'.js'.length)}.ts`;
|
|
62
|
+
}
|
|
59
63
|
return value.replace('./dist/', './src/');
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
function rewriteExportPaths(value) {
|
|
63
|
-
if (typeof value === 'string')
|
|
64
|
-
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
return sourcePath(value);
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.map(rewriteExportPaths);
|
|
72
|
+
}
|
|
65
73
|
if (value && typeof value === 'object') {
|
|
66
74
|
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, rewriteExportPaths(child)]));
|
|
67
75
|
}
|
|
@@ -69,9 +77,15 @@ function rewriteExportPaths(value) {
|
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
function stringValues(value) {
|
|
72
|
-
if (typeof value === 'string')
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
if (typeof value === 'string') {
|
|
81
|
+
return [value];
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.flatMap(stringValues);
|
|
85
|
+
}
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
return Object.values(value).flatMap(stringValues);
|
|
88
|
+
}
|
|
75
89
|
return [];
|
|
76
90
|
}
|
|
77
91
|
|
|
@@ -83,15 +97,23 @@ export function createSourceManifest(manifest, resolvedPackages) {
|
|
|
83
97
|
delete generated.files;
|
|
84
98
|
delete generated.scripts;
|
|
85
99
|
for (const field of ['main', 'module', 'types']) {
|
|
86
|
-
if (field in generated)
|
|
100
|
+
if (field in generated) {
|
|
101
|
+
generated[field] = sourcePath(generated[field]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (generated.exports) {
|
|
105
|
+
generated.exports = rewriteExportPaths(generated.exports);
|
|
87
106
|
}
|
|
88
|
-
if (generated.exports) generated.exports = rewriteExportPaths(generated.exports);
|
|
89
107
|
|
|
90
108
|
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
91
|
-
if (!generated[field])
|
|
109
|
+
if (!generated[field]) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
92
112
|
for (const dependencyName of Object.keys(generated[field])) {
|
|
93
113
|
const dependency = resolvedPackages.get(dependencyName);
|
|
94
|
-
if (dependency)
|
|
114
|
+
if (dependency) {
|
|
115
|
+
generated[field][dependencyName] = `workspace:${dependency.version}`;
|
|
116
|
+
}
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
return generated;
|
|
@@ -99,25 +121,52 @@ export function createSourceManifest(manifest, resolvedPackages) {
|
|
|
99
121
|
|
|
100
122
|
export function validateMechanicSelection(projectManifest, resolvedPackages, capabilityDocument) {
|
|
101
123
|
const config = projectManifest.miaodaGame;
|
|
102
|
-
if (config === undefined)
|
|
124
|
+
if (config === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
103
127
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
104
|
-
fail(
|
|
128
|
+
fail(
|
|
129
|
+
'add',
|
|
130
|
+
'package.json miaodaGame',
|
|
131
|
+
'be an object when defined',
|
|
132
|
+
'Remove it or declare schemaVersion, engine, uses, and blockedPackages.',
|
|
133
|
+
);
|
|
105
134
|
}
|
|
106
135
|
const engines = new Set(['neutral', 'react', 'phaser', 'cocos']);
|
|
107
136
|
if (config.schemaVersion !== 1 || !engines.has(config.engine)) {
|
|
108
|
-
fail(
|
|
137
|
+
fail(
|
|
138
|
+
'add',
|
|
139
|
+
'package.json miaodaGame',
|
|
140
|
+
'use schemaVersion 1 and a neutral, react, phaser, or cocos engine',
|
|
141
|
+
'Correct the project mechanic selection metadata.',
|
|
142
|
+
);
|
|
109
143
|
}
|
|
110
144
|
if (!config.uses || typeof config.uses !== 'object' || Array.isArray(config.uses)) {
|
|
111
|
-
fail(
|
|
145
|
+
fail(
|
|
146
|
+
'add',
|
|
147
|
+
'package.json miaodaGame.uses',
|
|
148
|
+
'be an object',
|
|
149
|
+
'Map selected package names to their intended owned capabilities.',
|
|
150
|
+
);
|
|
112
151
|
}
|
|
113
152
|
if (!Array.isArray(config.blockedPackages)) {
|
|
114
|
-
fail(
|
|
153
|
+
fail(
|
|
154
|
+
'add',
|
|
155
|
+
'package.json miaodaGame.blockedPackages',
|
|
156
|
+
'be an array',
|
|
157
|
+
'Use an empty array when no package is blocked.',
|
|
158
|
+
);
|
|
115
159
|
}
|
|
116
160
|
const capabilities = capabilityDocument?.packages ?? {};
|
|
117
161
|
const owners = new Map();
|
|
118
162
|
for (const [name, resolvedPackage] of resolvedPackages) {
|
|
119
163
|
if (config.blockedPackages.includes(name)) {
|
|
120
|
-
fail(
|
|
164
|
+
fail(
|
|
165
|
+
'add',
|
|
166
|
+
name,
|
|
167
|
+
'not be listed in miaodaGame.blockedPackages',
|
|
168
|
+
'Remove the package request or unblock it explicitly.',
|
|
169
|
+
);
|
|
121
170
|
}
|
|
122
171
|
const capability = capabilities[name];
|
|
123
172
|
if (
|
|
@@ -126,27 +175,56 @@ export function validateMechanicSelection(projectManifest, resolvedPackages, cap
|
|
|
126
175
|
config.engine !== 'neutral' &&
|
|
127
176
|
capability.engine !== config.engine
|
|
128
177
|
) {
|
|
129
|
-
fail(
|
|
178
|
+
fail(
|
|
179
|
+
'add',
|
|
180
|
+
`${name} engine`,
|
|
181
|
+
`match project engine ${config.engine}`,
|
|
182
|
+
`Choose a ${config.engine} adapter or an engine-neutral core package.`,
|
|
183
|
+
);
|
|
130
184
|
}
|
|
131
185
|
const uses = config.uses[name];
|
|
132
|
-
if (uses === undefined)
|
|
186
|
+
if (uses === undefined) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
133
189
|
if (!capability) {
|
|
134
|
-
fail(
|
|
190
|
+
fail(
|
|
191
|
+
'add',
|
|
192
|
+
`${name} capability metadata`,
|
|
193
|
+
'exist before declaring miaodaGame.uses',
|
|
194
|
+
'Update the Devkit capability snapshot or remove the declaration.',
|
|
195
|
+
);
|
|
135
196
|
}
|
|
136
197
|
if (!Array.isArray(uses) || uses.length === 0 || new Set(uses).size !== uses.length) {
|
|
137
|
-
fail(
|
|
198
|
+
fail(
|
|
199
|
+
'add',
|
|
200
|
+
`miaodaGame.uses.${name}`,
|
|
201
|
+
'be a non-empty array without duplicates',
|
|
202
|
+
'Declare each intended owned capability once.',
|
|
203
|
+
);
|
|
138
204
|
}
|
|
139
205
|
for (const use of uses) {
|
|
140
206
|
if (!capability.owns.includes(use)) {
|
|
141
|
-
fail(
|
|
207
|
+
fail(
|
|
208
|
+
'add',
|
|
209
|
+
`${name} use ${use}`,
|
|
210
|
+
`be one of its owned capabilities: ${capability.owns.join(', ')}`,
|
|
211
|
+
'Select the correct package or capability name.',
|
|
212
|
+
);
|
|
142
213
|
}
|
|
143
214
|
const existing = owners.get(use);
|
|
144
215
|
if (existing && existing !== name) {
|
|
145
|
-
fail(
|
|
216
|
+
fail(
|
|
217
|
+
'add',
|
|
218
|
+
`capability ${use}`,
|
|
219
|
+
`have one owner, but both ${existing} and ${name} are selected`,
|
|
220
|
+
'Remove the duplicate ownership assignment.',
|
|
221
|
+
);
|
|
146
222
|
}
|
|
147
223
|
owners.set(use, name);
|
|
148
224
|
}
|
|
149
|
-
if (resolvedPackage.version.length === 0)
|
|
225
|
+
if (resolvedPackage.version.length === 0) {
|
|
226
|
+
fail('add', `${name} version`, 'be non-empty', 'Repack the package.');
|
|
227
|
+
}
|
|
150
228
|
}
|
|
151
229
|
}
|
|
152
230
|
|
|
@@ -165,14 +243,20 @@ function validateSourceEntrypoints(packageDirectory, manifest) {
|
|
|
165
243
|
}
|
|
166
244
|
|
|
167
245
|
function listFiles(root, current = root) {
|
|
168
|
-
const entries = readdirSync(current, { withFileTypes: true }).sort((left, right) =>
|
|
246
|
+
const entries = readdirSync(current, { withFileTypes: true }).sort((left, right) =>
|
|
247
|
+
left.name.localeCompare(right.name),
|
|
248
|
+
);
|
|
169
249
|
const files = [];
|
|
170
250
|
for (const entry of entries) {
|
|
171
|
-
if (entry.name === 'node_modules')
|
|
251
|
+
if (entry.name === 'node_modules') {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
172
254
|
const path = join(current, entry.name);
|
|
173
|
-
if (entry.isDirectory())
|
|
174
|
-
|
|
175
|
-
else if (entry.
|
|
255
|
+
if (entry.isDirectory()) {
|
|
256
|
+
files.push(...listFiles(root, path));
|
|
257
|
+
} else if (entry.isFile()) {
|
|
258
|
+
files.push(path);
|
|
259
|
+
} else if (entry.isSymbolicLink()) {
|
|
176
260
|
fail(
|
|
177
261
|
'add',
|
|
178
262
|
`${relative(root, path)} in ${root}`,
|
|
@@ -197,10 +281,17 @@ function hashTree(root) {
|
|
|
197
281
|
|
|
198
282
|
function readState(metadataRoot) {
|
|
199
283
|
const statePath = join(metadataRoot, STATE_FILENAME);
|
|
200
|
-
if (!existsSync(statePath))
|
|
284
|
+
if (!existsSync(statePath)) {
|
|
285
|
+
return { schemaVersion: STATE_SCHEMA_VERSION, roots: {}, packages: {} };
|
|
286
|
+
}
|
|
201
287
|
const state = readJson(statePath, STATE_FILENAME);
|
|
202
288
|
if (state.schemaVersion !== STATE_SCHEMA_VERSION || !state.roots || !state.packages) {
|
|
203
|
-
fail(
|
|
289
|
+
fail(
|
|
290
|
+
'add',
|
|
291
|
+
STATE_FILENAME,
|
|
292
|
+
`use schemaVersion ${STATE_SCHEMA_VERSION}`,
|
|
293
|
+
`Preserve src/game-mechanics, then remove ${STATE_FILENAME} to adopt it again.`,
|
|
294
|
+
);
|
|
204
295
|
}
|
|
205
296
|
return state;
|
|
206
297
|
}
|
|
@@ -209,12 +300,22 @@ function inspectExistingPackages(sourceRoot, state) {
|
|
|
209
300
|
const statuses = {};
|
|
210
301
|
if (!existsSync(sourceRoot)) {
|
|
211
302
|
if (Object.keys(state.packages).length > 0) {
|
|
212
|
-
fail(
|
|
303
|
+
fail(
|
|
304
|
+
'add',
|
|
305
|
+
sourceRoot,
|
|
306
|
+
'exist for the recorded state',
|
|
307
|
+
`Restore it or preserve intentional changes and remove ${STATE_FILENAME} to reinstall.`,
|
|
308
|
+
);
|
|
213
309
|
}
|
|
214
310
|
return statuses;
|
|
215
311
|
}
|
|
216
312
|
if (lstatSync(sourceRoot).isSymbolicLink()) {
|
|
217
|
-
fail(
|
|
313
|
+
fail(
|
|
314
|
+
'add',
|
|
315
|
+
sourceRoot,
|
|
316
|
+
'be a real project directory, not a symbolic link',
|
|
317
|
+
'Replace the link after preserving its contents.',
|
|
318
|
+
);
|
|
218
319
|
}
|
|
219
320
|
for (const [name, record] of Object.entries(state.packages)) {
|
|
220
321
|
const directory = join(sourceRoot, name);
|
|
@@ -223,7 +324,12 @@ function inspectExistingPackages(sourceRoot, state) {
|
|
|
223
324
|
continue;
|
|
224
325
|
}
|
|
225
326
|
if (lstatSync(directory).isSymbolicLink()) {
|
|
226
|
-
fail(
|
|
327
|
+
fail(
|
|
328
|
+
'add',
|
|
329
|
+
`${name} source`,
|
|
330
|
+
'be a real directory, not a symbolic link',
|
|
331
|
+
'Replace the link with the editable source directory.',
|
|
332
|
+
);
|
|
227
333
|
}
|
|
228
334
|
statuses[name] = hashTree(directory) === record.sha256 ? 'clean' : 'modified';
|
|
229
335
|
}
|
|
@@ -257,15 +363,20 @@ function ensureWorkspacePattern(projectRoot) {
|
|
|
257
363
|
return { path: workspacePath, previous };
|
|
258
364
|
}
|
|
259
365
|
let insertAt = packagesIndex + 1;
|
|
260
|
-
while (insertAt < lines.length && (lines[insertAt].trim() === '' || /^\s+/.test(lines[insertAt])))
|
|
366
|
+
while (insertAt < lines.length && (lines[insertAt].trim() === '' || /^\s+/.test(lines[insertAt]))) {
|
|
367
|
+
insertAt += 1;
|
|
368
|
+
}
|
|
261
369
|
lines.splice(insertAt, 0, ` - ${quotedPattern}`);
|
|
262
370
|
writeFileSync(workspacePath, lines.join('\n'));
|
|
263
371
|
return { path: workspacePath, previous };
|
|
264
372
|
}
|
|
265
373
|
|
|
266
374
|
function restoreFile(path, previous) {
|
|
267
|
-
if (previous === undefined)
|
|
268
|
-
|
|
375
|
+
if (previous === undefined) {
|
|
376
|
+
rmSync(path, { force: true });
|
|
377
|
+
} else {
|
|
378
|
+
writeFileSync(path, previous);
|
|
379
|
+
}
|
|
269
380
|
}
|
|
270
381
|
|
|
271
382
|
function updateProjectManifest(projectRoot, roots, resolvedPackages) {
|
|
@@ -276,7 +387,12 @@ function updateProjectManifest(projectRoot, roots, resolvedPackages) {
|
|
|
276
387
|
for (const name of Object.keys(roots).sort()) {
|
|
277
388
|
const resolvedPackage = resolvedPackages.get(name);
|
|
278
389
|
if (!resolvedPackage) {
|
|
279
|
-
fail(
|
|
390
|
+
fail(
|
|
391
|
+
'add',
|
|
392
|
+
`direct package ${name}`,
|
|
393
|
+
'be present in the resolved Miaoda graph',
|
|
394
|
+
'Check the source package name and registry metadata.',
|
|
395
|
+
);
|
|
280
396
|
}
|
|
281
397
|
manifest.dependencies[name] = `workspace:${resolvedPackage.version}`;
|
|
282
398
|
}
|
|
@@ -288,7 +404,12 @@ function pnpmVersion(pnpmCommand, cwd) {
|
|
|
288
404
|
const result = spawnSync(pnpmCommand, ['--version'], { cwd, encoding: 'utf8' });
|
|
289
405
|
if (result.status !== 0) {
|
|
290
406
|
const detail = result.error?.message || result.stderr?.trim() || result.stdout?.trim();
|
|
291
|
-
fail(
|
|
407
|
+
fail(
|
|
408
|
+
'add',
|
|
409
|
+
'pnpm command',
|
|
410
|
+
'run successfully',
|
|
411
|
+
detail || `Install pnpm 10 or 11 and ensure ${pnpmCommand} is on PATH.`,
|
|
412
|
+
);
|
|
292
413
|
}
|
|
293
414
|
const version = result.stdout.trim();
|
|
294
415
|
const major = Number.parseInt(version.split('.')[0], 10);
|
|
@@ -323,7 +444,9 @@ function copyResolvedPackage(target, resolvedPackage, resolvedPackages) {
|
|
|
323
444
|
recursive: true,
|
|
324
445
|
filter: (source) => {
|
|
325
446
|
const sourceName = basename(source);
|
|
326
|
-
if (['node_modules', 'dist', 'coverage', '.nx', '__tests__'].includes(sourceName))
|
|
447
|
+
if (['node_modules', 'dist', 'coverage', '.nx', '__tests__'].includes(sourceName)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
327
450
|
if (/\.(?:spec|test)\.[cm]?[jt]sx?$/.test(sourceName) || /^vitest\.config\.[cm]?[jt]s$/.test(sourceName)) {
|
|
328
451
|
return false;
|
|
329
452
|
}
|
|
@@ -337,19 +460,56 @@ function copyResolvedPackage(target, resolvedPackage, resolvedPackages) {
|
|
|
337
460
|
return hashTree(target);
|
|
338
461
|
}
|
|
339
462
|
|
|
340
|
-
const legacyAgentsSource =
|
|
463
|
+
const legacyAgentsSource = [
|
|
464
|
+
'# Editable game mechanics',
|
|
465
|
+
'',
|
|
466
|
+
'- This directory contains editable TypeScript source, not generated build output.',
|
|
467
|
+
'- Preserve package boundaries and import packages by their `miaoda-game-*` names.',
|
|
468
|
+
'- Modify each package under `<package>/src/`; do not edit `node_modules`.',
|
|
469
|
+
'- Run `pnpm exec miaoda mechanics status` before updating a locally modified package.',
|
|
470
|
+
'',
|
|
471
|
+
].join('\n');
|
|
341
472
|
const generatedReadmeMarker = '<!-- Generated by miaoda mechanics. -->';
|
|
342
473
|
|
|
343
|
-
function
|
|
474
|
+
function readmeDetail(label, value, separator = ', ') {
|
|
475
|
+
if (Array.isArray(value)) {
|
|
476
|
+
return value.length > 0 ? [`- ${label}: ${value.join(separator)}`] : [];
|
|
477
|
+
}
|
|
478
|
+
return typeof value === 'string' && value.length > 0 ? [`- ${label}: ${value}`] : [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function capabilityReadme(capability) {
|
|
482
|
+
if (!capability) {
|
|
483
|
+
return ['- Capability metadata: unavailable; read the package README and source before assigning ownership.'];
|
|
484
|
+
}
|
|
485
|
+
return [
|
|
486
|
+
...readmeDetail('Engine', capability.engine),
|
|
487
|
+
...readmeDetail('Domains', capability.domains),
|
|
488
|
+
...readmeDetail('Owns', capability.owns),
|
|
489
|
+
...readmeDetail('Keep outside', capability.doesNotOwn),
|
|
490
|
+
...readmeDetail('Compatible with', capability.compatibleWith),
|
|
491
|
+
...readmeDetail('Use for', capability.guidance?.useFor, ' '),
|
|
492
|
+
...readmeDetail('Boundary guidance', capability.guidance?.keepOutside, ' '),
|
|
493
|
+
...readmeDetail('Rules', capability.guidance?.rules, ' '),
|
|
494
|
+
...(capability.useInsteadWhen ?? []).map(
|
|
495
|
+
({ package: packageName, condition }) => `- Use ${packageName} instead when: ${condition}`,
|
|
496
|
+
),
|
|
497
|
+
...readmeDetail('Persistence', capability.persistence),
|
|
498
|
+
...readmeDetail('Observe', capability.testability?.methods?.observe),
|
|
499
|
+
...readmeDetail('Advance', capability.testability?.methods?.advance),
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function createSourceReadme(resolvedPackages, capabilityDocument) {
|
|
344
504
|
const packages = [...resolvedPackages]
|
|
345
505
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
346
506
|
.map(([name, resolvedPackage]) => {
|
|
347
507
|
const capability = capabilityDocument?.packages?.[name];
|
|
348
508
|
const description = resolvedPackage.manifest.description || 'Miaoda game mechanic package.';
|
|
349
|
-
const
|
|
350
|
-
return
|
|
509
|
+
const details = [`- Source: \`./${name}/\``, ...capabilityReadme(capability)].join('\n');
|
|
510
|
+
return `### \`${name}@${resolvedPackage.version}\`\n\n${description}\n\n${details}`;
|
|
351
511
|
})
|
|
352
|
-
.join('\n');
|
|
512
|
+
.join('\n\n');
|
|
353
513
|
return `${generatedReadmeMarker}
|
|
354
514
|
# Game mechanics
|
|
355
515
|
|
|
@@ -377,7 +537,7 @@ function ensureSourceGuidance(sourceRoot, resolvedPackages, capabilityDocument)
|
|
|
377
537
|
existingReadme.startsWith(generatedReadmeMarker) ||
|
|
378
538
|
existingReadme.startsWith('# Game mechanics source\n')
|
|
379
539
|
) {
|
|
380
|
-
writeFileSync(readmePath,
|
|
540
|
+
writeFileSync(readmePath, createSourceReadme(resolvedPackages, capabilityDocument));
|
|
381
541
|
}
|
|
382
542
|
const agentsPath = join(sourceRoot, 'AGENTS.md');
|
|
383
543
|
if (existsSync(agentsPath) && readFileSync(agentsPath, 'utf8') === legacyAgentsSource) {
|
|
@@ -407,7 +567,12 @@ function materializePackages(
|
|
|
407
567
|
const previous = previousState.packages[name];
|
|
408
568
|
if (previous && previous.version === resolvedPackage.version) {
|
|
409
569
|
if (statuses[name] === 'missing') {
|
|
410
|
-
fail(
|
|
570
|
+
fail(
|
|
571
|
+
'add',
|
|
572
|
+
`${name} source`,
|
|
573
|
+
'still exist',
|
|
574
|
+
`Restore ${join(sourceRoot, name)} or remove its state entry before reinstalling.`,
|
|
575
|
+
);
|
|
411
576
|
}
|
|
412
577
|
records[name] = previous;
|
|
413
578
|
continue;
|
|
@@ -417,16 +582,25 @@ function materializePackages(
|
|
|
417
582
|
'add',
|
|
418
583
|
`${name} ${previous.version} source`,
|
|
419
584
|
`be clean before updating to ${resolvedPackage.version}`,
|
|
420
|
-
`Preserve or commit ${join(sourceRoot, name)}, then explicitly restore it before retrying
|
|
585
|
+
`Preserve or commit ${join(sourceRoot, name)}, then explicitly restore it before retrying ` +
|
|
586
|
+
'the update. Nothing was overwritten.',
|
|
421
587
|
);
|
|
422
588
|
}
|
|
423
589
|
if (existsSync(target)) {
|
|
424
590
|
if (!previous) {
|
|
425
|
-
fail(
|
|
591
|
+
fail(
|
|
592
|
+
'add',
|
|
593
|
+
`${name} destination`,
|
|
594
|
+
'not contain an unmanaged package',
|
|
595
|
+
`Move ${join(sourceRoot, name)} elsewhere or adopt it explicitly.`,
|
|
596
|
+
);
|
|
426
597
|
}
|
|
427
598
|
rmSync(target, { recursive: true, force: true });
|
|
428
599
|
}
|
|
429
|
-
records[name] = {
|
|
600
|
+
records[name] = {
|
|
601
|
+
version: resolvedPackage.version,
|
|
602
|
+
sha256: copyResolvedPackage(target, resolvedPackage, resolvedPackages),
|
|
603
|
+
};
|
|
430
604
|
}
|
|
431
605
|
return records;
|
|
432
606
|
}
|
|
@@ -434,11 +608,15 @@ function materializePackages(
|
|
|
434
608
|
function switchManagedDirectory(sourceRoot, stagedSourceRoot, metadataRoot) {
|
|
435
609
|
const backupRoot = join(metadataRoot, `.game-mechanics-backup-${randomUUID()}`);
|
|
436
610
|
mkdirSync(dirname(sourceRoot), { recursive: true });
|
|
437
|
-
if (existsSync(sourceRoot))
|
|
611
|
+
if (existsSync(sourceRoot)) {
|
|
612
|
+
renameSync(sourceRoot, backupRoot);
|
|
613
|
+
}
|
|
438
614
|
try {
|
|
439
615
|
renameSync(stagedSourceRoot, sourceRoot);
|
|
440
616
|
} catch (error) {
|
|
441
|
-
if (existsSync(backupRoot))
|
|
617
|
+
if (existsSync(backupRoot)) {
|
|
618
|
+
renameSync(backupRoot, sourceRoot);
|
|
619
|
+
}
|
|
442
620
|
throw error;
|
|
443
621
|
}
|
|
444
622
|
return { packagesRoot: sourceRoot, backupRoot: existsSync(backupRoot) ? backupRoot : undefined };
|
|
@@ -446,7 +624,9 @@ function switchManagedDirectory(sourceRoot, stagedSourceRoot, metadataRoot) {
|
|
|
446
624
|
|
|
447
625
|
function restoreManagedDirectory({ packagesRoot, backupRoot }) {
|
|
448
626
|
rmSync(packagesRoot, { recursive: true, force: true });
|
|
449
|
-
if (backupRoot)
|
|
627
|
+
if (backupRoot) {
|
|
628
|
+
renameSync(backupRoot, packagesRoot);
|
|
629
|
+
}
|
|
450
630
|
}
|
|
451
631
|
|
|
452
632
|
function parseArguments(argv) {
|
|
@@ -454,7 +634,9 @@ function parseArguments(argv) {
|
|
|
454
634
|
const projectArgument = argv.find((argument) => argument.startsWith('--project='));
|
|
455
635
|
const pnpmArgument = argv.find((argument) => argument.startsWith('--pnpm='));
|
|
456
636
|
const sourceIndexArgument = argv.find((argument) => argument.startsWith('--source-index='));
|
|
457
|
-
const options = new Set(
|
|
637
|
+
const options = new Set(
|
|
638
|
+
argv.filter((argument) => argument.startsWith('--')).map((argument) => argument.split('=')[0]),
|
|
639
|
+
);
|
|
458
640
|
const specs = argv.slice(1).filter((argument) => !argument.startsWith('--'));
|
|
459
641
|
return {
|
|
460
642
|
command,
|
|
@@ -514,7 +696,12 @@ function validateProject(projectRoot) {
|
|
|
514
696
|
|
|
515
697
|
function validateMetadataRoot(metadataRoot) {
|
|
516
698
|
if (existsSync(metadataRoot) && lstatSync(metadataRoot).isSymbolicLink()) {
|
|
517
|
-
fail(
|
|
699
|
+
fail(
|
|
700
|
+
'add',
|
|
701
|
+
metadataRoot,
|
|
702
|
+
'be a real directory, not a symbolic link',
|
|
703
|
+
`Replace ${METADATA_ROOT} with a project-local directory.`,
|
|
704
|
+
);
|
|
518
705
|
}
|
|
519
706
|
}
|
|
520
707
|
|
|
@@ -526,12 +713,14 @@ export function mechanicSourceStatus({ projectRoot }) {
|
|
|
526
713
|
const state = readState(metadataRoot);
|
|
527
714
|
const sourceRoot = join(projectRoot, SOURCE_ROOT);
|
|
528
715
|
const statuses = inspectExistingPackages(sourceRoot, state);
|
|
529
|
-
return Object.keys(state.packages)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
716
|
+
return Object.keys(state.packages)
|
|
717
|
+
.sort()
|
|
718
|
+
.map((name) => ({
|
|
719
|
+
name,
|
|
720
|
+
version: state.packages[name].version,
|
|
721
|
+
status: statuses[name],
|
|
722
|
+
path: join(sourceRoot, name),
|
|
723
|
+
}));
|
|
535
724
|
}
|
|
536
725
|
|
|
537
726
|
export async function addMechanicSources({
|
|
@@ -544,7 +733,12 @@ export async function addMechanicSources({
|
|
|
544
733
|
projectRoot = resolve(projectRoot);
|
|
545
734
|
validateProject(projectRoot);
|
|
546
735
|
if (!Array.isArray(specs) || specs.length === 0) {
|
|
547
|
-
fail(
|
|
736
|
+
fail(
|
|
737
|
+
'add',
|
|
738
|
+
'package specs',
|
|
739
|
+
'contain at least one miaoda-game-* package',
|
|
740
|
+
'Pass a package name with an optional @version.',
|
|
741
|
+
);
|
|
548
742
|
}
|
|
549
743
|
if (!sourceIndexUrl) {
|
|
550
744
|
fail(
|
|
@@ -628,8 +822,12 @@ export async function addMechanicSources({
|
|
|
628
822
|
sourceIndexUrl: indexedResolution.sourceIndexUrl,
|
|
629
823
|
});
|
|
630
824
|
|
|
631
|
-
if (!skipInstall)
|
|
632
|
-
|
|
825
|
+
if (!skipInstall) {
|
|
826
|
+
runPnpm(pnpmCommand, ['install', '--ignore-scripts', '--no-frozen-lockfile'], projectRoot);
|
|
827
|
+
}
|
|
828
|
+
if (switched.backupRoot) {
|
|
829
|
+
rmSync(switched.backupRoot, { recursive: true, force: true });
|
|
830
|
+
}
|
|
633
831
|
return {
|
|
634
832
|
directPackages: directNames,
|
|
635
833
|
packageNames: [...resolvedPackages.keys()].sort(),
|
|
@@ -637,12 +835,20 @@ export async function addMechanicSources({
|
|
|
637
835
|
pnpmVersion: version,
|
|
638
836
|
};
|
|
639
837
|
} catch (error) {
|
|
640
|
-
if (projectManifestChange)
|
|
641
|
-
|
|
642
|
-
|
|
838
|
+
if (projectManifestChange) {
|
|
839
|
+
restoreFile(projectManifestChange.path, projectManifestChange.previous);
|
|
840
|
+
}
|
|
841
|
+
if (workspaceChange) {
|
|
842
|
+
restoreFile(workspaceChange.path, workspaceChange.previous);
|
|
843
|
+
}
|
|
844
|
+
if (switched) {
|
|
845
|
+
restoreManagedDirectory(switched);
|
|
846
|
+
}
|
|
643
847
|
restoreFile(statePath, previousStateText);
|
|
644
848
|
restoreFile(lockfilePath, previousLockfileText);
|
|
645
|
-
if (!nodeModulesExisted)
|
|
849
|
+
if (!nodeModulesExisted) {
|
|
850
|
+
rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
851
|
+
}
|
|
646
852
|
throw error;
|
|
647
853
|
} finally {
|
|
648
854
|
cleanupIndexedMechanics(indexedResolution);
|
|
@@ -654,7 +860,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
654
860
|
const mechanicsArguments = argv[0] === 'mechanics' ? argv.slice(1) : argv;
|
|
655
861
|
const command = mechanicsArguments[0];
|
|
656
862
|
const wantsHelp = mechanicsArguments.includes('--help') || mechanicsArguments.includes('-h');
|
|
657
|
-
if (!command || command === 'help' || wantsHelp && !['add', 'status'].includes(command)) {
|
|
863
|
+
if (!command || command === 'help' || (wantsHelp && !['add', 'status'].includes(command))) {
|
|
658
864
|
console.log(ROOT_HELP);
|
|
659
865
|
return;
|
|
660
866
|
}
|
|
@@ -675,17 +881,20 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
675
881
|
}
|
|
676
882
|
const options = parseArguments(mechanicsArguments);
|
|
677
883
|
const statuses = mechanicSourceStatus(options);
|
|
678
|
-
if (statuses.length === 0)
|
|
679
|
-
|
|
884
|
+
if (statuses.length === 0) {
|
|
885
|
+
console.log('No Miaoda game-mechanic source packages are installed.');
|
|
886
|
+
} else {
|
|
887
|
+
for (const entry of statuses) {
|
|
888
|
+
console.log(`${entry.name}@${entry.version} ${entry.status} ${entry.path}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
680
891
|
return statuses;
|
|
681
892
|
}
|
|
682
893
|
throw new Error(`Unknown miaoda mechanics command: ${command}\n\n${ROOT_HELP}`);
|
|
683
894
|
}
|
|
684
895
|
|
|
685
896
|
export function formatAddResult(result) {
|
|
686
|
-
const packagePaths = result.packageNames
|
|
687
|
-
.map((name) => ` - src/game-mechanics/${name}/`)
|
|
688
|
-
.join('\n');
|
|
897
|
+
const packagePaths = result.packageNames.map((name) => ` - src/game-mechanics/${name}/`).join('\n');
|
|
689
898
|
return `✓ Added ${result.packageNames.length} editable game-mechanic source packages
|
|
690
899
|
|
|
691
900
|
Added source:
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync,
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { x as extractTarball } from 'tar';
|
|
6
6
|
|
|
7
|
-
const MECHANIC_PREFIX = 'miaoda-game-';
|
|
8
7
|
const INDEX_SCHEMA_VERSION = 1;
|
|
9
8
|
const MAX_INDEX_BYTES = 5 * 1024 * 1024;
|
|
10
9
|
const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
|
|
@@ -18,9 +17,13 @@ function isMechanicName(value) {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
function parsePackageSpec(spec) {
|
|
21
|
-
if (typeof spec !== 'string')
|
|
20
|
+
if (typeof spec !== 'string') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
22
23
|
const match = /^(miaoda-game-[a-z0-9][a-z0-9._-]*?)(?:@([^/]+))?$/.exec(spec);
|
|
23
|
-
if (!match)
|
|
24
|
+
if (!match) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
24
27
|
return { name: match[1], version: match[2] };
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -30,13 +33,19 @@ export function areIndexedPackageSpecs(specs) {
|
|
|
30
33
|
|
|
31
34
|
async function readResponse(response, limit, label) {
|
|
32
35
|
if (!response.ok) {
|
|
33
|
-
fail(
|
|
36
|
+
fail(
|
|
37
|
+
label,
|
|
38
|
+
`download successfully, but received HTTP ${response.status}`,
|
|
39
|
+
'Check the public source URL and object permissions.',
|
|
40
|
+
);
|
|
34
41
|
}
|
|
35
42
|
const declaredLength = Number(response.headers.get('content-length'));
|
|
36
43
|
if (Number.isFinite(declaredLength) && declaredLength > limit) {
|
|
37
44
|
fail(label, `be at most ${limit} bytes`, 'Publish a smaller source artifact.');
|
|
38
45
|
}
|
|
39
|
-
if (!response.body)
|
|
46
|
+
if (!response.body) {
|
|
47
|
+
fail(label, 'contain a response body', 'Upload the source object again.');
|
|
48
|
+
}
|
|
40
49
|
const chunks = [];
|
|
41
50
|
let total = 0;
|
|
42
51
|
for await (const chunk of response.body) {
|
|
@@ -76,7 +85,11 @@ function validateVersionRecord(name, version, record, indexUrl) {
|
|
|
76
85
|
fail(`${name}@${version} url`, 'use HTTP or HTTPS', 'Upload the source archive to the configured public storage.');
|
|
77
86
|
}
|
|
78
87
|
if (typeof record.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(record.sha256)) {
|
|
79
|
-
fail(
|
|
88
|
+
fail(
|
|
89
|
+
`${name}@${version} sha256`,
|
|
90
|
+
'contain 64 hexadecimal characters',
|
|
91
|
+
'Regenerate the source index after packing.',
|
|
92
|
+
);
|
|
80
93
|
}
|
|
81
94
|
const dependencies = record.dependencies ?? {};
|
|
82
95
|
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
|
@@ -113,11 +126,19 @@ function selectVersion(index, name, requestedVersion, indexUrl) {
|
|
|
113
126
|
function resolveGraph(index, indexUrl, previousRoots, specs) {
|
|
114
127
|
const roots = new Map();
|
|
115
128
|
for (const [name, version] of Object.entries(previousRoots ?? {})) {
|
|
116
|
-
if (isMechanicName(name) && typeof version === 'string' && version.length > 0)
|
|
129
|
+
if (isMechanicName(name) && typeof version === 'string' && version.length > 0) {
|
|
130
|
+
roots.set(name, version);
|
|
131
|
+
}
|
|
117
132
|
}
|
|
118
133
|
for (const spec of specs) {
|
|
119
134
|
const parsed = parsePackageSpec(spec);
|
|
120
|
-
if (!parsed)
|
|
135
|
+
if (!parsed) {
|
|
136
|
+
fail(
|
|
137
|
+
`package spec ${spec}`,
|
|
138
|
+
'be a miaoda-game-* name with an optional @version',
|
|
139
|
+
'Pass TGZ URLs without --source-index.',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
121
142
|
const selected = selectVersion(index, parsed.name, parsed.version, indexUrl);
|
|
122
143
|
roots.set(parsed.name, selected.version);
|
|
123
144
|
}
|
|
@@ -140,7 +161,11 @@ function resolveGraph(index, indexUrl, previousRoots, specs) {
|
|
|
140
161
|
const selected = selectVersion(index, request.name, request.version, indexUrl);
|
|
141
162
|
selectedPackages.set(request.name, selected);
|
|
142
163
|
for (const [dependencyName, dependencyVersion] of Object.entries(selected.dependencies).sort()) {
|
|
143
|
-
queue.push({
|
|
164
|
+
queue.push({
|
|
165
|
+
name: dependencyName,
|
|
166
|
+
version: dependencyVersion,
|
|
167
|
+
requestedBy: `${selected.name}@${selected.version}`,
|
|
168
|
+
});
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
return { roots: Object.fromEntries([...roots].sort()), selectedPackages };
|
|
@@ -164,7 +189,11 @@ async function readIndex(indexUrl) {
|
|
|
164
189
|
fail('stable.json', 'contain valid JSON', error instanceof Error ? error.message : String(error));
|
|
165
190
|
}
|
|
166
191
|
if (index?.schemaVersion !== INDEX_SCHEMA_VERSION || !index.packages || typeof index.packages !== 'object') {
|
|
167
|
-
fail(
|
|
192
|
+
fail(
|
|
193
|
+
'stable.json',
|
|
194
|
+
`use schemaVersion ${INDEX_SCHEMA_VERSION} and a packages object`,
|
|
195
|
+
'Regenerate the source index.',
|
|
196
|
+
);
|
|
168
197
|
}
|
|
169
198
|
return { index, indexUrl: normalizedUrl };
|
|
170
199
|
}
|
|
@@ -175,9 +204,15 @@ function assertExtractedTree(root, name) {
|
|
|
175
204
|
const current = stack.pop();
|
|
176
205
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
177
206
|
if (entry.isSymbolicLink()) {
|
|
178
|
-
fail(
|
|
207
|
+
fail(
|
|
208
|
+
`${name} ${relative(root, join(current, entry.name))}`,
|
|
209
|
+
'not be a symbolic link',
|
|
210
|
+
'Repack the source archive without links.',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
stack.push(join(current, entry.name));
|
|
179
215
|
}
|
|
180
|
-
if (entry.isDirectory()) stack.push(join(current, entry.name));
|
|
181
216
|
}
|
|
182
217
|
}
|
|
183
218
|
}
|
|
@@ -202,16 +237,24 @@ async function extractPackage(targetRoot, selected, bytes) {
|
|
|
202
237
|
strict: true,
|
|
203
238
|
preservePaths: false,
|
|
204
239
|
filter(path, entry) {
|
|
205
|
-
if (!path.startsWith('package/'))
|
|
240
|
+
if (!path.startsWith('package/')) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
206
243
|
if (entry.type === 'SymbolicLink' || entry.type === 'Link') {
|
|
207
|
-
fail(
|
|
244
|
+
fail(
|
|
245
|
+
`${selected.name}@${selected.version} archive`,
|
|
246
|
+
'not contain links',
|
|
247
|
+
'Repack the source archive with regular files only.',
|
|
248
|
+
);
|
|
208
249
|
}
|
|
209
250
|
return true;
|
|
210
251
|
},
|
|
211
252
|
});
|
|
212
253
|
assertExtractedTree(packageDirectory, selected.name);
|
|
213
254
|
const manifestPath = join(packageDirectory, 'package.json');
|
|
214
|
-
if (!existsSync(manifestPath))
|
|
255
|
+
if (!existsSync(manifestPath)) {
|
|
256
|
+
fail(`${selected.name} package.json`, 'exist in the TGZ', 'Repack the source package.');
|
|
257
|
+
}
|
|
215
258
|
let manifest;
|
|
216
259
|
try {
|
|
217
260
|
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
@@ -230,9 +273,19 @@ async function extractPackage(targetRoot, selected, bytes) {
|
|
|
230
273
|
);
|
|
231
274
|
}
|
|
232
275
|
if (!existsSync(join(packageDirectory, 'src'))) {
|
|
233
|
-
fail(
|
|
276
|
+
fail(
|
|
277
|
+
`${selected.name}@${selected.version} src`,
|
|
278
|
+
'exist in the TGZ',
|
|
279
|
+
'Pack production TypeScript source before uploading.',
|
|
280
|
+
);
|
|
234
281
|
}
|
|
235
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
directory: packageDirectory,
|
|
284
|
+
manifest,
|
|
285
|
+
version: selected.version,
|
|
286
|
+
sourceUrl: selected.url,
|
|
287
|
+
artifactSha256: actualHash,
|
|
288
|
+
};
|
|
236
289
|
}
|
|
237
290
|
|
|
238
291
|
export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, specs }) {
|
|
@@ -241,7 +294,9 @@ export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, sp
|
|
|
241
294
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'miaoda-mechanics-source-index-'));
|
|
242
295
|
const resolvedPackages = new Map();
|
|
243
296
|
try {
|
|
244
|
-
for (const selected of [...graph.selectedPackages.values()].sort((left, right) =>
|
|
297
|
+
for (const selected of [...graph.selectedPackages.values()].sort((left, right) =>
|
|
298
|
+
left.name.localeCompare(right.name),
|
|
299
|
+
)) {
|
|
245
300
|
const bytes = await download(selected.url, MAX_TARBALL_BYTES, `${selected.name}@${selected.version}`);
|
|
246
301
|
resolvedPackages.set(selected.name, await extractPackage(temporaryRoot, selected, bytes));
|
|
247
302
|
}
|
|
@@ -258,5 +313,7 @@ export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, sp
|
|
|
258
313
|
}
|
|
259
314
|
|
|
260
315
|
export function cleanupIndexedMechanics(resolution) {
|
|
261
|
-
if (resolution?.temporaryRoot)
|
|
316
|
+
if (resolution?.temporaryRoot) {
|
|
317
|
+
rmSync(resolution.temporaryRoot, { recursive: true, force: true });
|
|
318
|
+
}
|
|
262
319
|
}
|