miaoda-game-devkit 0.9.0 → 0.10.0
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/README.md
CHANGED
|
@@ -107,12 +107,19 @@ Devkit 发布统一的 `miaoda` 命令。裸 `miaoda-game-*` 包名默认从公
|
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
pnpm exec miaoda mechanics --help
|
|
110
|
+
pnpm exec miaoda mechanics list
|
|
111
|
+
pnpm exec miaoda mechanics list --domain=grid --engine=react
|
|
110
112
|
pnpm exec miaoda mechanics add miaoda-game-beam-core
|
|
111
113
|
pnpm exec miaoda mechanics add miaoda-game-beam-core@1.2.3 \
|
|
112
114
|
--source-index=https://public.example.com/game-mechanics/stable.json
|
|
113
115
|
pnpm exec miaoda mechanics status
|
|
114
116
|
```
|
|
115
117
|
|
|
118
|
+
`list` 只列出稳定索引中实际可安装的包,并从 Devkit 自带的 capabilities JSON 动态汇总
|
|
119
|
+
domain 的 `owns` 能力;带筛选时会显示包负责和不负责的边界、使用指引及可测试性。它不会安装
|
|
120
|
+
源码,也不会维护另一份容易过时的文字目录。先选择覆盖需求的最小包集合,再单独运行 `add`,
|
|
121
|
+
成功后阅读生成的 `src/game-mechanics/README.md`。
|
|
122
|
+
|
|
116
123
|
默认索引是 `https://resource-static.bj.bcebos.com/miaoda-game/stable.json`。只有调试、测试或
|
|
117
124
|
私有镜像场景才需要通过 `--source-index` 或 `MIAODA_MECHANICS_INDEX_URL` 覆盖。
|
|
118
125
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { listIndexedMechanics } from './resolve-game-mechanics-source-index.mjs';
|
|
3
|
+
|
|
4
|
+
const ENGINES = new Set(['neutral', 'react', 'phaser', 'cocos']);
|
|
5
|
+
const FILTER_NAMES = ['domain', 'engine', 'owns'];
|
|
6
|
+
|
|
7
|
+
function readCapabilityDocument(path) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
12
|
+
throw new Error(`miaoda mechanics list: cannot read capabilities JSON: ${detail}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function optionValue(argv, name) {
|
|
17
|
+
const prefix = `--${name}=`;
|
|
18
|
+
const argument = argv.find((value) => value.startsWith(prefix));
|
|
19
|
+
return argument?.slice(prefix.length).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseMechanicListArguments(argv, defaultSourceIndexUrl) {
|
|
23
|
+
const allowed = new Set(['--json', '--help', '-h']);
|
|
24
|
+
for (const argument of argv.slice(1)) {
|
|
25
|
+
if (allowed.has(argument)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (['--domain=', '--engine=', '--owns=', '--source-index='].some((prefix) => argument.startsWith(prefix))) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`miaoda mechanics list: unsupported argument ${JSON.stringify(argument)}. ` +
|
|
33
|
+
'Use --domain=<domain>, --engine=<engine>, --owns=<capability>, or --json.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const filters = Object.fromEntries(
|
|
37
|
+
FILTER_NAMES.map((name) => [name, optionValue(argv, name)]).filter(([, value]) => Boolean(value)),
|
|
38
|
+
);
|
|
39
|
+
if (filters.engine && !ENGINES.has(filters.engine)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`miaoda mechanics list: engine must be neutral, react, phaser, or cocos; received ${filters.engine}.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
filters,
|
|
46
|
+
json: argv.includes('--json'),
|
|
47
|
+
sourceIndexUrl: optionValue(argv, 'source-index') || defaultSourceIndexUrl,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function packageMatches(entry, filters) {
|
|
52
|
+
if (!entry.annotated) {
|
|
53
|
+
return Object.keys(filters).length === 0;
|
|
54
|
+
}
|
|
55
|
+
if (filters.domain && !entry.domains.includes(filters.domain)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (filters.owns && !entry.provides.includes(filters.owns)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (filters.engine) {
|
|
62
|
+
const compatible =
|
|
63
|
+
filters.engine === 'neutral'
|
|
64
|
+
? entry.engine === 'neutral'
|
|
65
|
+
: entry.engine === 'neutral' || entry.engine === filters.engine;
|
|
66
|
+
if (!compatible) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function packageEntry(name, version, capability, installableNames) {
|
|
74
|
+
if (!capability) {
|
|
75
|
+
return { name, version, annotated: false };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
version,
|
|
80
|
+
annotated: true,
|
|
81
|
+
engine: capability.engine,
|
|
82
|
+
domains: capability.domains,
|
|
83
|
+
provides: capability.owns,
|
|
84
|
+
leavesOutside: capability.doesNotOwn,
|
|
85
|
+
compatibleWith: capability.compatibleWith.filter((candidate) => installableNames.has(candidate)),
|
|
86
|
+
useFor: capability.guidance?.useFor ?? [],
|
|
87
|
+
keepOutside: capability.guidance?.keepOutside ?? [],
|
|
88
|
+
rules: capability.guidance?.rules ?? [],
|
|
89
|
+
useInsteadWhen: capability.useInsteadWhen,
|
|
90
|
+
testability: capability.testability,
|
|
91
|
+
persistence: capability.persistence,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function summarizeDomains(packages) {
|
|
96
|
+
const domains = new Map();
|
|
97
|
+
for (const entry of packages) {
|
|
98
|
+
if (!entry.annotated) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const domain of entry.domains) {
|
|
102
|
+
const summary = domains.get(domain) ?? { name: domain, packageNames: new Set(), provides: new Set() };
|
|
103
|
+
summary.packageNames.add(entry.name);
|
|
104
|
+
for (const capability of entry.provides) {
|
|
105
|
+
summary.provides.add(capability);
|
|
106
|
+
}
|
|
107
|
+
domains.set(domain, summary);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...domains.values()]
|
|
111
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
112
|
+
.map((summary) => ({
|
|
113
|
+
name: summary.name,
|
|
114
|
+
packageCount: summary.packageNames.size,
|
|
115
|
+
provides: [...summary.provides].sort(),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createMechanicCatalog(indexedPackages, capabilityDocument, filters = {}, sourceIndexUrl) {
|
|
120
|
+
const installableNames = new Set(indexedPackages.map((entry) => entry.name));
|
|
121
|
+
const allPackages = indexedPackages
|
|
122
|
+
.map(({ name, version }) =>
|
|
123
|
+
packageEntry(name, version, capabilityDocument.packages?.[name], installableNames),
|
|
124
|
+
)
|
|
125
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
126
|
+
const matchedPackages = allPackages.filter((entry) => packageMatches(entry, filters));
|
|
127
|
+
const matchedNames = new Set(matchedPackages.map((entry) => entry.name));
|
|
128
|
+
const packages = matchedPackages.map((entry) => {
|
|
129
|
+
if (!entry.annotated) {
|
|
130
|
+
return entry;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...entry,
|
|
134
|
+
compatibleWith: entry.compatibleWith.filter((candidate) => matchedNames.has(candidate)),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const filtersActive = Object.keys(filters).length > 0;
|
|
138
|
+
return {
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
sourceIndexUrl,
|
|
141
|
+
filters,
|
|
142
|
+
installablePackageCount: allPackages.length,
|
|
143
|
+
unannotatedPackageCount: allPackages.filter((entry) => !entry.annotated).length,
|
|
144
|
+
domains: summarizeDomains(filtersActive ? packages : allPackages),
|
|
145
|
+
packages,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function listMechanics({ sourceIndexUrl, capabilityPath, filters }) {
|
|
150
|
+
const indexed = await listIndexedMechanics(sourceIndexUrl);
|
|
151
|
+
const capabilityDocument = readCapabilityDocument(capabilityPath);
|
|
152
|
+
return createMechanicCatalog(indexed.packages, capabilityDocument, filters, indexed.sourceIndexUrl);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatList(values) {
|
|
156
|
+
return values.length > 0 ? values.join(', ') : 'none declared';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTestability(testability) {
|
|
160
|
+
if (!testability) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return `observation=${testability.observation}; advance=${testability.advance}; ` +
|
|
164
|
+
`observe: ${testability.methods.observe}; exercise: ${testability.methods.advance}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatPackage(entry) {
|
|
168
|
+
if (!entry.annotated) {
|
|
169
|
+
return `${entry.name}@${entry.version}\n Capability metadata: unavailable; inspect its README after add.`;
|
|
170
|
+
}
|
|
171
|
+
const lines = [
|
|
172
|
+
`${entry.name}@${entry.version} [${entry.engine}]`,
|
|
173
|
+
` Provides: ${formatList(entry.provides)}`,
|
|
174
|
+
` Leaves outside: ${formatList(entry.leavesOutside)}`,
|
|
175
|
+
];
|
|
176
|
+
if (entry.useFor.length > 0) {
|
|
177
|
+
lines.push(` Use for: ${entry.useFor.join(' ')}`);
|
|
178
|
+
}
|
|
179
|
+
if (entry.keepOutside.length > 0) {
|
|
180
|
+
lines.push(` Keep outside: ${entry.keepOutside.join(' ')}`);
|
|
181
|
+
}
|
|
182
|
+
if (entry.rules.length > 0) {
|
|
183
|
+
lines.push(` Rules: ${entry.rules.join(' ')}`);
|
|
184
|
+
}
|
|
185
|
+
if (entry.compatibleWith.length > 0) {
|
|
186
|
+
lines.push(` Works with: ${entry.compatibleWith.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
for (const alternative of entry.useInsteadWhen) {
|
|
189
|
+
lines.push(` Use ${alternative.package} instead when: ${alternative.condition}`);
|
|
190
|
+
}
|
|
191
|
+
const testability = formatTestability(entry.testability);
|
|
192
|
+
if (testability) {
|
|
193
|
+
lines.push(` Testability: ${testability}`);
|
|
194
|
+
}
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatFilters(filters) {
|
|
199
|
+
const values = Object.entries(filters).map(([name, value]) => `${name}=${value}`);
|
|
200
|
+
return values.length > 0 ? values.join(', ') : 'none';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatMechanicList(catalog, { json = false } = {}) {
|
|
204
|
+
if (json) {
|
|
205
|
+
return JSON.stringify(catalog, null, 2);
|
|
206
|
+
}
|
|
207
|
+
const filtersActive = Object.keys(catalog.filters).length > 0;
|
|
208
|
+
if (!filtersActive) {
|
|
209
|
+
const domainLines = catalog.domains.map((domain) => {
|
|
210
|
+
const preview = domain.provides.slice(0, 3).join(', ');
|
|
211
|
+
const remaining = Math.max(0, domain.provides.length - 3);
|
|
212
|
+
const suffix = remaining > 0 ? `, +${remaining} more` : '';
|
|
213
|
+
return ` ${domain.name} (${domain.packageCount}) — can provide: ${preview}${suffix}`;
|
|
214
|
+
});
|
|
215
|
+
const annotationNote =
|
|
216
|
+
catalog.unannotatedPackageCount > 0
|
|
217
|
+
? `\n${catalog.unannotatedPackageCount} installable packages lack capability annotations.`
|
|
218
|
+
: '';
|
|
219
|
+
return `Installable Miaoda mechanic domains (${catalog.domains.length} domains, ` +
|
|
220
|
+
`${catalog.installablePackageCount} packages):\n${domainLines.join('\n')}${annotationNote}\n\n` +
|
|
221
|
+
'Next:\n' +
|
|
222
|
+
' Rerun with --domain=<domain>, --engine=<react|phaser|cocos|neutral>, or --owns=<capability>.\n' +
|
|
223
|
+
' Use --json for complete machine-readable facts. Do not install every match.';
|
|
224
|
+
}
|
|
225
|
+
if (catalog.packages.length === 0) {
|
|
226
|
+
return `No installable mechanics matched ${formatFilters(catalog.filters)}.\n` +
|
|
227
|
+
'Run "pnpm exec miaoda mechanics list" to inspect available domains and exact capability names.';
|
|
228
|
+
}
|
|
229
|
+
return `Matched ${catalog.packages.length} installable mechanic packages (${formatFilters(catalog.filters)}):\n\n` +
|
|
230
|
+
`${catalog.packages.map(formatPackage).join('\n\n')}\n\n` +
|
|
231
|
+
'Next:\n' +
|
|
232
|
+
' Select the smallest set whose Provides entries own required mechanics, then run:\n' +
|
|
233
|
+
' pnpm exec miaoda mechanics add <full miaoda-game-* names>\n' +
|
|
234
|
+
' Run add by itself, then read src/game-mechanics/README.md before gameplay edits.';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const LIST_HELP = `Usage: miaoda mechanics list [filters]
|
|
238
|
+
|
|
239
|
+
Discover packages that are currently installable through the source index. Package
|
|
240
|
+
meaning comes from the bundled capabilities JSON: owns becomes Provides, doesNotOwn
|
|
241
|
+
becomes Leaves outside, and guidance/testability are shown when annotated. This command
|
|
242
|
+
is read-only and never installs source.
|
|
243
|
+
|
|
244
|
+
Without filters, list prints domain summaries with real owned-capability previews.
|
|
245
|
+
With filters, it prints package versions and ownership boundaries. An engine filter
|
|
246
|
+
includes engine-neutral cores plus adapters for that engine.
|
|
247
|
+
|
|
248
|
+
Filters:
|
|
249
|
+
--domain=<domain> Match one exact domain from the summary
|
|
250
|
+
--engine=<engine> neutral, react, phaser, or cocos
|
|
251
|
+
--owns=<capability> Match one exact owned-capability token
|
|
252
|
+
--json Emit complete machine-readable JSON
|
|
253
|
+
--source-index=<url> Override the public stable.json URL
|
|
254
|
+
-h, --help Show this help`;
|
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
} from 'node:fs';
|
|
17
17
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import {
|
|
20
|
+
formatMechanicList,
|
|
21
|
+
LIST_HELP,
|
|
22
|
+
listMechanics,
|
|
23
|
+
parseMechanicListArguments,
|
|
24
|
+
} from './list-game-mechanics-source.mjs';
|
|
19
25
|
import {
|
|
20
26
|
areIndexedPackageSpecs,
|
|
21
27
|
cleanupIndexedMechanics,
|
|
@@ -657,6 +663,7 @@ Manage editable Miaoda game-mechanic TypeScript source in src/game-mechanics.
|
|
|
657
663
|
|
|
658
664
|
Commands:
|
|
659
665
|
add <package...> Resolve packages from the source index and add editable source
|
|
666
|
+
list Discover installable packages and their ownership boundaries
|
|
660
667
|
status Show clean, modified, or missing source packages
|
|
661
668
|
help Show this help
|
|
662
669
|
|
|
@@ -860,10 +867,24 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
860
867
|
const mechanicsArguments = argv[0] === 'mechanics' ? argv.slice(1) : argv;
|
|
861
868
|
const command = mechanicsArguments[0];
|
|
862
869
|
const wantsHelp = mechanicsArguments.includes('--help') || mechanicsArguments.includes('-h');
|
|
863
|
-
if (!command || command === 'help' || (wantsHelp && !['add', 'status'].includes(command))) {
|
|
870
|
+
if (!command || command === 'help' || (wantsHelp && !['add', 'list', 'status'].includes(command))) {
|
|
864
871
|
console.log(ROOT_HELP);
|
|
865
872
|
return;
|
|
866
873
|
}
|
|
874
|
+
if (command === 'list') {
|
|
875
|
+
if (wantsHelp) {
|
|
876
|
+
console.log(LIST_HELP);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const options = parseMechanicListArguments(mechanicsArguments, DEFAULT_SOURCE_INDEX_URL);
|
|
880
|
+
const catalog = await listMechanics({
|
|
881
|
+
sourceIndexUrl: options.sourceIndexUrl,
|
|
882
|
+
capabilityPath,
|
|
883
|
+
filters: options.filters,
|
|
884
|
+
});
|
|
885
|
+
console.log(formatMechanicList(catalog, { json: options.json }));
|
|
886
|
+
return catalog;
|
|
887
|
+
}
|
|
867
888
|
if (command === 'add') {
|
|
868
889
|
if (wantsHelp) {
|
|
869
890
|
console.log(ADD_HELP);
|
|
@@ -312,6 +312,20 @@ export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, sp
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
export async function listIndexedMechanics(indexUrl) {
|
|
316
|
+
const loaded = await readIndex(indexUrl);
|
|
317
|
+
const packages = Object.keys(loaded.index.packages)
|
|
318
|
+
.sort()
|
|
319
|
+
.map((name) => {
|
|
320
|
+
const selected = selectVersion(loaded.index, name, undefined, loaded.indexUrl);
|
|
321
|
+
return { name: selected.name, version: selected.version };
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
sourceIndexUrl: loaded.indexUrl,
|
|
325
|
+
packages,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
315
329
|
export function cleanupIndexedMechanics(resolution) {
|
|
316
330
|
if (resolution?.temporaryRoot) {
|
|
317
331
|
rmSync(resolution.temporaryRoot, { recursive: true, force: true });
|