@mpgd/cli 0.34.0 → 0.35.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/dist/index.js +74 -3
- package/dist/kit-upgrade.d.ts +39 -0
- package/dist/kit-upgrade.js +486 -0
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, verifyHost
|
|
|
17
17
|
import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
|
|
18
18
|
import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
|
|
19
19
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
|
|
20
|
+
import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
|
|
20
21
|
import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
|
|
21
22
|
import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
|
|
22
23
|
import { targetConfigExtensionsFileEnv } from './target-config-env.js';
|
|
@@ -135,7 +136,7 @@ export async function runMpgdCli(args) {
|
|
|
135
136
|
// partial document. Every other command keeps the default rendering.
|
|
136
137
|
renderHeader: async (ctx) => {
|
|
137
138
|
const values = ctx.values;
|
|
138
|
-
if (ctx.name === 'verify-delivery' && values.json === true) {
|
|
139
|
+
if ((ctx.name === 'verify-delivery' || ctx.name === 'upgrade') && values.json === true) {
|
|
139
140
|
return '';
|
|
140
141
|
}
|
|
141
142
|
const title = ctx.env.description || ctx.env.name || '';
|
|
@@ -146,7 +147,7 @@ export async function runMpgdCli(args) {
|
|
|
146
147
|
renderValidationErrors: async (ctx, error) => {
|
|
147
148
|
const messages = error.errors.map((entry) => String(entry.message)).join('\n');
|
|
148
149
|
const values = ctx.values;
|
|
149
|
-
if (ctx.name === 'verify-delivery' && values.json === true) {
|
|
150
|
+
if ((ctx.name === 'verify-delivery' || ctx.name === 'upgrade') && values.json === true) {
|
|
150
151
|
process.stderr.write(`${messages}\n`);
|
|
151
152
|
return '';
|
|
152
153
|
}
|
|
@@ -2042,6 +2043,76 @@ const kitCommand = defineI18n({
|
|
|
2042
2043
|
ko: '현재 mpgd-kit 체크아웃을 점검합니다.',
|
|
2043
2044
|
}),
|
|
2044
2045
|
subCommands: {
|
|
2046
|
+
upgrade: defineI18n({
|
|
2047
|
+
name: 'upgrade',
|
|
2048
|
+
description: 'Plan or apply the latest published Kit package versions for a game.',
|
|
2049
|
+
resource: commandResource({
|
|
2050
|
+
base: {
|
|
2051
|
+
en: 'Plan or apply the latest published Kit package versions for a game.',
|
|
2052
|
+
ko: '게임에 배포된 최신 Kit 패키지 버전 적용을 계획하거나 실행합니다.',
|
|
2053
|
+
},
|
|
2054
|
+
args: {
|
|
2055
|
+
game: {
|
|
2056
|
+
en: 'Game directory containing package.json and optional mpgd.targets.json.',
|
|
2057
|
+
ko: 'package.json과 선택적 mpgd.targets.json이 있는 게임 디렉터리.',
|
|
2058
|
+
},
|
|
2059
|
+
'targets-file': {
|
|
2060
|
+
en: 'Optional game-owned target configuration file.',
|
|
2061
|
+
ko: '선택적 게임 소유 타깃 구성 파일.',
|
|
2062
|
+
},
|
|
2063
|
+
apply: {
|
|
2064
|
+
en: 'Write the planned manifests and pnpm lockfiles.',
|
|
2065
|
+
ko: '계획된 의존성 선언과 pnpm 락파일을 갱신합니다.',
|
|
2066
|
+
},
|
|
2067
|
+
json: {
|
|
2068
|
+
en: 'Write one machine-readable JSON document to stdout.',
|
|
2069
|
+
ko: '표준 출력에 기계 판독용 JSON 문서 하나를 출력합니다.',
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
2072
|
+
}),
|
|
2073
|
+
args: {
|
|
2074
|
+
game: { type: 'string', required: false, default: '.', description: 'Game directory.' },
|
|
2075
|
+
'targets-file': { type: 'string', required: false, description: 'Target config file.' },
|
|
2076
|
+
apply: { type: 'boolean', required: false, description: 'Apply the upgrade plan.' },
|
|
2077
|
+
json: { type: 'boolean', required: false, description: 'Print JSON.' },
|
|
2078
|
+
},
|
|
2079
|
+
run: async (ctx) => {
|
|
2080
|
+
const game = readOptionalString(ctx.values.game) ?? '.';
|
|
2081
|
+
const plan = await planKitUpgrade(game, undefined, readOptionalString(ctx.values['targets-file']));
|
|
2082
|
+
const result = ctx.values.apply === true && plan.blockers.length === 0
|
|
2083
|
+
? applyKitUpgrade(plan)
|
|
2084
|
+
: undefined;
|
|
2085
|
+
if (ctx.values.json === true) {
|
|
2086
|
+
process.stdout.write(JSON.stringify({ plan, result: result ?? null }, null, 2) + '\n');
|
|
2087
|
+
}
|
|
2088
|
+
else {
|
|
2089
|
+
console.info('Kit upgrade plan for ' + plan.gameRoot);
|
|
2090
|
+
for (const update of plan.updates) {
|
|
2091
|
+
console.info(' ' + path.relative(plan.gameRoot, update.manifest)
|
|
2092
|
+
+ ': ' + update.packageName + ' ' + update.current + ' -> ' + update.next);
|
|
2093
|
+
}
|
|
2094
|
+
if (plan.updates.length === 0) {
|
|
2095
|
+
console.info(' Already current for published npm latest tags.');
|
|
2096
|
+
}
|
|
2097
|
+
for (const note of plan.notes) {
|
|
2098
|
+
console.info('Note: ' + note);
|
|
2099
|
+
}
|
|
2100
|
+
for (const blocker of plan.blockers) {
|
|
2101
|
+
console.error('Blocked: ' + blocker);
|
|
2102
|
+
}
|
|
2103
|
+
if (result !== undefined) {
|
|
2104
|
+
console.info('Updated ' + result.manifests.length + ' manifest(s) and '
|
|
2105
|
+
+ result.lockfiles.length + ' lockfile(s). Run target checks before release.');
|
|
2106
|
+
}
|
|
2107
|
+
else if (plan.updates.length > 0 && plan.blockers.length === 0) {
|
|
2108
|
+
console.info('Run again with --apply to update manifests and pnpm lockfiles.');
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (plan.blockers.length > 0) {
|
|
2112
|
+
process.exitCode = 1;
|
|
2113
|
+
}
|
|
2114
|
+
},
|
|
2115
|
+
}),
|
|
2045
2116
|
doctor: defineI18n({
|
|
2046
2117
|
name: 'doctor',
|
|
2047
2118
|
description: 'Print CLI, kit, template, and target-wrapper status.',
|
|
@@ -2060,7 +2131,7 @@ const kitCommand = defineI18n({
|
|
|
2060
2131
|
}),
|
|
2061
2132
|
},
|
|
2062
2133
|
run: () => {
|
|
2063
|
-
console.info('Use "mpgd kit doctor".');
|
|
2134
|
+
console.info('Use "mpgd kit doctor" or "mpgd kit upgrade --game <directory> [--apply]".');
|
|
2064
2135
|
},
|
|
2065
2136
|
});
|
|
2066
2137
|
function commandResource(description, args = {}) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
declare const dependencySections: readonly ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
2
|
+
type DependencySection = (typeof dependencySections)[number];
|
|
3
|
+
export interface PublishedKitPackage {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly peerDependencies?: Readonly<Record<string, string>>;
|
|
6
|
+
readonly optionalPeerDependencies?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export type LatestKitPackageLookup = (name: string, gameRoot: string) => Promise<PublishedKitPackage>;
|
|
9
|
+
export interface KitUpgradeUpdate {
|
|
10
|
+
readonly manifest: string;
|
|
11
|
+
readonly section: DependencySection;
|
|
12
|
+
readonly packageName: string;
|
|
13
|
+
readonly current: string;
|
|
14
|
+
readonly next: string;
|
|
15
|
+
readonly latestVersion: string;
|
|
16
|
+
}
|
|
17
|
+
export interface KitUpgradePlan {
|
|
18
|
+
readonly gameRoot: string;
|
|
19
|
+
readonly updates: readonly KitUpgradeUpdate[];
|
|
20
|
+
readonly blockers: readonly string[];
|
|
21
|
+
readonly notes: readonly string[];
|
|
22
|
+
readonly manifestDigests: Readonly<Record<string, string>>;
|
|
23
|
+
readonly targetConfigDigest: {
|
|
24
|
+
readonly file: string;
|
|
25
|
+
readonly sha256: string | null;
|
|
26
|
+
};
|
|
27
|
+
readonly lockfileDigests: Readonly<Record<string, string>>;
|
|
28
|
+
readonly workspaceRoots: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export interface KitUpgradeApplyResult {
|
|
31
|
+
readonly manifests: readonly string[];
|
|
32
|
+
readonly lockfiles: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export type KitUpgradeLockfileRunner = (workspaceRoot: string) => void;
|
|
35
|
+
export declare function planKitUpgrade(gameDirectory: string, lookup?: LatestKitPackageLookup, targetsFileName?: string): Promise<KitUpgradePlan>;
|
|
36
|
+
export declare function applyKitUpgrade(plan: KitUpgradePlan, runLockfileUpdate?: KitUpgradeLockfileRunner): KitUpgradeApplyResult;
|
|
37
|
+
export declare function lookupPublishedKitPackage(packageName: string, gameRoot: string): Promise<PublishedKitPackage>;
|
|
38
|
+
export declare function parsePublishedKitPackageResponse(source: string, packageName: string): PublishedKitPackage;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { execFile as execFileCallback, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { compare, intersects, minVersion, satisfies, valid, validRange } from 'semver';
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
8
|
+
const dependencySections = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
9
|
+
export async function planKitUpgrade(gameDirectory, lookup = lookupPublishedKitPackage, targetsFileName) {
|
|
10
|
+
const gameRoot = realpathSync(path.resolve(gameDirectory));
|
|
11
|
+
const gameManifest = path.join(gameRoot, 'package.json');
|
|
12
|
+
if (!existsSync(gameManifest)) {
|
|
13
|
+
throw new Error('Game package.json is missing: ' + gameManifest);
|
|
14
|
+
}
|
|
15
|
+
if (lstatSync(gameManifest).isSymbolicLink() || !inside(gameRoot, realpathSync(gameManifest))) {
|
|
16
|
+
throw new Error('Refusing to update a linked game package.json: ' + gameManifest);
|
|
17
|
+
}
|
|
18
|
+
const notes = [];
|
|
19
|
+
const blockers = [];
|
|
20
|
+
const { manifests, targetConfigDigest } = discoverGameManifests(gameRoot, notes, blockers, targetsFileName);
|
|
21
|
+
const manifestDigests = {};
|
|
22
|
+
const requested = new Set();
|
|
23
|
+
const parsedManifests = new Map();
|
|
24
|
+
for (const manifest of manifests) {
|
|
25
|
+
const source = readFileSync(manifest, 'utf8');
|
|
26
|
+
manifestDigests[manifest] = digest(source);
|
|
27
|
+
const parsed = parseJsonObject(source, manifest);
|
|
28
|
+
parsedManifests.set(manifest, parsed);
|
|
29
|
+
for (const section of dependencySections) {
|
|
30
|
+
for (const [name, value] of Object.entries(asObject(parsed[section]))) {
|
|
31
|
+
if (!name.startsWith('@mpgd/') || typeof value !== 'string') {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!isKitPackageName(name)) {
|
|
35
|
+
blockers.push('Invalid Kit package name in ' + manifest + ': ' + name);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (parseVersionSpecifier(value) === undefined) {
|
|
39
|
+
if (validRange(value) !== null) {
|
|
40
|
+
blockers.push('Unsupported Kit version range for ' + name + ' in ' + manifest
|
|
41
|
+
+ ': ' + value + '. Pin an exact version, ^version, or ~version before upgrading.');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
notes.push('Skipped non-SemVer dependency ' + name + ' in ' + manifest + ': ' + value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
requested.add(name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const packages = new Map();
|
|
54
|
+
const names = [...requested].sort();
|
|
55
|
+
for (let index = 0; index < names.length; index += 4) {
|
|
56
|
+
await Promise.all(names.slice(index, index + 4).map(async (name) => {
|
|
57
|
+
try {
|
|
58
|
+
const published = await lookup(name, gameRoot);
|
|
59
|
+
if (valid(published.version) === null) {
|
|
60
|
+
throw new Error('registry returned a non-SemVer latest version');
|
|
61
|
+
}
|
|
62
|
+
packages.set(name, published);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
blockers.push('Cannot resolve npm latest for ' + name + ': ' + formatError(error));
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
const updates = [];
|
|
70
|
+
const peerSubjects = [];
|
|
71
|
+
for (const manifest of manifests) {
|
|
72
|
+
const parsed = parsedManifests.get(manifest);
|
|
73
|
+
if (parsed === undefined) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const section of dependencySections) {
|
|
77
|
+
for (const [name, value] of Object.entries(asObject(parsed[section]))) {
|
|
78
|
+
if (!name.startsWith('@mpgd/') || typeof value !== 'string') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const current = parseVersionSpecifier(value);
|
|
82
|
+
const published = packages.get(name);
|
|
83
|
+
if (current === undefined || published === undefined) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (compare(published.version, current.version) < 0) {
|
|
87
|
+
notes.push('Kept ' + name + ' in ' + manifest + ': current version is newer than npm latest.');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const next = compare(published.version, current.version) > 0
|
|
91
|
+
? current.prefix + published.version
|
|
92
|
+
: value;
|
|
93
|
+
const subject = {
|
|
94
|
+
manifest,
|
|
95
|
+
section,
|
|
96
|
+
packageName: name,
|
|
97
|
+
current: value,
|
|
98
|
+
next,
|
|
99
|
+
latestVersion: published.version,
|
|
100
|
+
};
|
|
101
|
+
peerSubjects.push(subject);
|
|
102
|
+
if (next !== value) {
|
|
103
|
+
updates.push(subject);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
validatePeers(peerSubjects, updates, packages, parsedManifests, gameManifest, blockers, notes);
|
|
110
|
+
const { lockfileDigests, workspaceRoots } = discoverLockfiles(gameRoot, manifests, blockers, notes);
|
|
111
|
+
const cliUpdate = updates.find((update) => update.packageName === '@mpgd/cli');
|
|
112
|
+
if (process.env.MPGD_KIT_PATH !== undefined && cliUpdate !== undefined) {
|
|
113
|
+
const kitPackageFile = path.resolve(process.env.MPGD_KIT_PATH, 'packages/cli/package.json');
|
|
114
|
+
if (existsSync(kitPackageFile)) {
|
|
115
|
+
const kitPackage = parseJsonObject(readFileSync(kitPackageFile, 'utf8'), kitPackageFile);
|
|
116
|
+
if (kitPackage.version !== cliUpdate.latestVersion) {
|
|
117
|
+
notes.push('MPGD_KIT_PATH still has CLI ' + String(kitPackage.version)
|
|
118
|
+
+ '; select the published ' + cliUpdate.latestVersion + ' Kit tag before a release.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
gameRoot,
|
|
124
|
+
updates,
|
|
125
|
+
blockers,
|
|
126
|
+
notes: [...new Set(notes)],
|
|
127
|
+
manifestDigests,
|
|
128
|
+
targetConfigDigest,
|
|
129
|
+
lockfileDigests,
|
|
130
|
+
workspaceRoots,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function applyKitUpgrade(plan, runLockfileUpdate = updatePnpmLockfile) {
|
|
134
|
+
if (plan.blockers.length > 0) {
|
|
135
|
+
throw new Error('Kit upgrade is blocked:\n' + plan.blockers.join('\n'));
|
|
136
|
+
}
|
|
137
|
+
if (plan.updates.length === 0) {
|
|
138
|
+
return { manifests: [], lockfiles: [] };
|
|
139
|
+
}
|
|
140
|
+
const { file: targetsFile, sha256: targetsDigest } = plan.targetConfigDigest;
|
|
141
|
+
if (targetsDigest === null) {
|
|
142
|
+
if (existsSync(targetsFile)) {
|
|
143
|
+
throw new Error('File changed after upgrade planning: ' + targetsFile);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (!existsSync(targetsFile) || digest(readFileSync(targetsFile, 'utf8')) !== targetsDigest) {
|
|
147
|
+
throw new Error('File changed after upgrade planning: ' + targetsFile);
|
|
148
|
+
}
|
|
149
|
+
const snapshots = new Map();
|
|
150
|
+
for (const [file, expected] of Object.entries({
|
|
151
|
+
...plan.manifestDigests,
|
|
152
|
+
...plan.lockfileDigests,
|
|
153
|
+
})) {
|
|
154
|
+
const current = readFileSync(file, 'utf8');
|
|
155
|
+
if (digest(current) !== expected) {
|
|
156
|
+
throw new Error('File changed after upgrade planning: ' + file);
|
|
157
|
+
}
|
|
158
|
+
snapshots.set(file, current);
|
|
159
|
+
}
|
|
160
|
+
const changedManifests = [...new Set(plan.updates.map((update) => update.manifest))];
|
|
161
|
+
try {
|
|
162
|
+
for (const manifest of changedManifests) {
|
|
163
|
+
const source = snapshots.get(manifest);
|
|
164
|
+
if (source === undefined) {
|
|
165
|
+
throw new Error('Missing manifest snapshot: ' + manifest);
|
|
166
|
+
}
|
|
167
|
+
const parsed = parseJsonObject(source, manifest);
|
|
168
|
+
for (const update of plan.updates.filter((item) => item.manifest === manifest)) {
|
|
169
|
+
const section = asObject(parsed[update.section]);
|
|
170
|
+
if (section[update.packageName] !== update.current) {
|
|
171
|
+
throw new Error('Dependency changed after planning: ' + update.packageName);
|
|
172
|
+
}
|
|
173
|
+
section[update.packageName] = update.next;
|
|
174
|
+
parsed[update.section] = section;
|
|
175
|
+
}
|
|
176
|
+
writeFileSync(manifest, stringifyLike(source, parsed));
|
|
177
|
+
}
|
|
178
|
+
for (const root of plan.workspaceRoots) {
|
|
179
|
+
runLockfileUpdate(root);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const restoreFailures = [];
|
|
184
|
+
for (const [file, original] of snapshots) {
|
|
185
|
+
try {
|
|
186
|
+
writeFileSync(file, original);
|
|
187
|
+
}
|
|
188
|
+
catch (restoreError) {
|
|
189
|
+
restoreFailures.push(file + ': ' + formatError(restoreError));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const restoration = restoreFailures.length === 0
|
|
193
|
+
? 'Original manifests and lockfiles were rolled back.'
|
|
194
|
+
: 'Rollback incomplete:\n' + restoreFailures.join('\n');
|
|
195
|
+
throw new Error('Kit upgrade failed during lockfile update: ' + formatError(error)
|
|
196
|
+
+ '\n' + restoration);
|
|
197
|
+
}
|
|
198
|
+
return { manifests: changedManifests, lockfiles: Object.keys(plan.lockfileDigests) };
|
|
199
|
+
}
|
|
200
|
+
export async function lookupPublishedKitPackage(packageName, gameRoot) {
|
|
201
|
+
if (!isKitPackageName(packageName)) {
|
|
202
|
+
throw new Error('Invalid Kit package name: ' + packageName);
|
|
203
|
+
}
|
|
204
|
+
const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
205
|
+
const { stdout } = await execFile(executable, [
|
|
206
|
+
'view',
|
|
207
|
+
packageName + '@latest',
|
|
208
|
+
'version',
|
|
209
|
+
'peerDependencies',
|
|
210
|
+
'peerDependenciesMeta',
|
|
211
|
+
'--json',
|
|
212
|
+
], {
|
|
213
|
+
cwd: gameRoot,
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
maxBuffer: 512 * 1024,
|
|
216
|
+
timeout: 20_000,
|
|
217
|
+
shell: process.platform === 'win32',
|
|
218
|
+
});
|
|
219
|
+
return parsePublishedKitPackageResponse(String(stdout), packageName);
|
|
220
|
+
}
|
|
221
|
+
export function parsePublishedKitPackageResponse(source, packageName) {
|
|
222
|
+
let parsed;
|
|
223
|
+
try {
|
|
224
|
+
parsed = JSON.parse(source);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
throw new Error('Invalid npm view JSON for ' + packageName + ': ' + formatError(error));
|
|
228
|
+
}
|
|
229
|
+
const response = typeof parsed === 'string' ? { version: parsed } : asObject(parsed);
|
|
230
|
+
if (typeof response.version !== 'string') {
|
|
231
|
+
throw new Error('Registry did not return a latest version for ' + packageName);
|
|
232
|
+
}
|
|
233
|
+
const peers = asObject(response.peerDependencies);
|
|
234
|
+
const peerMetadata = asObject(response.peerDependenciesMeta);
|
|
235
|
+
const declaredPeers = Object.entries(peers).filter((entry) => typeof entry[1] === 'string');
|
|
236
|
+
const optionalPeerDependencies = declaredPeers
|
|
237
|
+
.filter(([name]) => asObject(peerMetadata[name]).optional === true)
|
|
238
|
+
.map(([name]) => name);
|
|
239
|
+
return {
|
|
240
|
+
version: response.version,
|
|
241
|
+
peerDependencies: Object.fromEntries(declaredPeers),
|
|
242
|
+
...(optionalPeerDependencies.length > 0 ? { optionalPeerDependencies } : {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function discoverGameManifests(gameRoot, notes, blockers, targetsFileName) {
|
|
246
|
+
const manifests = new Set([path.join(gameRoot, 'package.json')]);
|
|
247
|
+
const targetsFile = path.resolve(gameRoot, targetsFileName ?? 'mpgd.targets.json');
|
|
248
|
+
if (!inside(gameRoot, targetsFile)) {
|
|
249
|
+
throw new Error('Targets file must be inside the game directory: ' + targetsFile);
|
|
250
|
+
}
|
|
251
|
+
if (!existsSync(targetsFile)) {
|
|
252
|
+
if (targetsFileName === undefined) {
|
|
253
|
+
notes.push('No mpgd.targets.json found; only the game manifest will be updated.');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
blockers.push('Requested targets file is missing: ' + targetsFile);
|
|
257
|
+
}
|
|
258
|
+
return { manifests: [...manifests], targetConfigDigest: { file: targetsFile, sha256: null } };
|
|
259
|
+
}
|
|
260
|
+
if (lstatSync(targetsFile).isSymbolicLink() || !inside(gameRoot, realpathSync(targetsFile))) {
|
|
261
|
+
throw new Error('Refusing to read a linked targets file: ' + targetsFile);
|
|
262
|
+
}
|
|
263
|
+
const targetsSource = readFileSync(targetsFile, 'utf8');
|
|
264
|
+
const config = parseJsonObject(targetsSource, targetsFile);
|
|
265
|
+
const targetsDirectory = path.dirname(targetsFile);
|
|
266
|
+
for (const value of Object.values(asObject(config.targets))) {
|
|
267
|
+
const target = asObject(value);
|
|
268
|
+
for (const key of ['wrapperApp', 'shellApp']) {
|
|
269
|
+
const location = target[key];
|
|
270
|
+
if (typeof location !== 'string') {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (location.includes('${MPGD_KIT_PATH}')) {
|
|
274
|
+
notes.push('Skipped external target ' + key + ': ' + location);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const resolvedLocation = location
|
|
278
|
+
.replaceAll('${MPGD_GAME_ROOT}', targetsDirectory)
|
|
279
|
+
.replaceAll('${MPGD_GAME_APP_ROOT}', targetsDirectory);
|
|
280
|
+
if (resolvedLocation.includes('$' + '{')) {
|
|
281
|
+
notes.push('Skipped target with unresolved path token: ' + location);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const directory = path.resolve(targetsDirectory, resolvedLocation);
|
|
285
|
+
if (!inside(gameRoot, directory)) {
|
|
286
|
+
notes.push('Skipped target outside the game directory: ' + location);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const manifest = path.join(directory, 'package.json');
|
|
290
|
+
if (!existsSync(manifest)) {
|
|
291
|
+
notes.push('Skipped target without package.json: ' + location);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (lstatSync(manifest).isSymbolicLink() || !inside(gameRoot, realpathSync(manifest))) {
|
|
295
|
+
notes.push('Skipped linked target manifest: ' + location);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
manifests.add(manifest);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
manifests: [...manifests].sort(),
|
|
303
|
+
targetConfigDigest: { file: targetsFile, sha256: digest(targetsSource) },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function discoverLockfiles(gameRoot, manifests, blockers, notes) {
|
|
307
|
+
const roots = new Set();
|
|
308
|
+
let boundary = gameRoot;
|
|
309
|
+
for (;;) {
|
|
310
|
+
if (existsSync(path.join(boundary, '.git'))) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
const parent = path.dirname(boundary);
|
|
314
|
+
if (parent === boundary) {
|
|
315
|
+
boundary = gameRoot;
|
|
316
|
+
notes.push('No git boundary found; lockfile discovery is limited to the game directory.');
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
boundary = parent;
|
|
320
|
+
}
|
|
321
|
+
for (const manifest of manifests) {
|
|
322
|
+
let directory = path.dirname(manifest);
|
|
323
|
+
while (inside(boundary, directory)) {
|
|
324
|
+
const lockfile = path.join(directory, 'pnpm-lock.yaml');
|
|
325
|
+
if (existsSync(lockfile)) {
|
|
326
|
+
if (lstatSync(lockfile).isSymbolicLink()) {
|
|
327
|
+
blockers.push('Refusing to rewrite a linked pnpm lockfile: ' + lockfile);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
roots.add(directory);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (directory === boundary) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
directory = path.dirname(directory);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (roots.size === 0) {
|
|
340
|
+
blockers.push('No pnpm-lock.yaml found for the game and its target manifests.');
|
|
341
|
+
}
|
|
342
|
+
const workspaceRoots = [...roots].sort((left, right) => left.length - right.length);
|
|
343
|
+
return {
|
|
344
|
+
workspaceRoots,
|
|
345
|
+
lockfileDigests: Object.fromEntries(workspaceRoots.map((root) => {
|
|
346
|
+
const file = path.join(root, 'pnpm-lock.yaml');
|
|
347
|
+
return [file, digest(readFileSync(file, 'utf8'))];
|
|
348
|
+
})),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function validatePeers(subjects, updates, packages, manifests, gameManifest, blockers, notes) {
|
|
352
|
+
const replacements = new Map(updates.map((update) => [update.manifest + '\0' + update.packageName, update.next]));
|
|
353
|
+
for (const subject of subjects) {
|
|
354
|
+
const published = packages.get(subject.packageName);
|
|
355
|
+
const peers = published?.peerDependencies ?? {};
|
|
356
|
+
const optionalPeers = new Set(published?.optionalPeerDependencies ?? []);
|
|
357
|
+
for (const [peerName, range] of Object.entries(peers)) {
|
|
358
|
+
if (validRange(range) === null) {
|
|
359
|
+
blockers.push('Invalid published peer range for ' + subject.packageName + ': '
|
|
360
|
+
+ peerName + ' ' + range);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const owner = findPeerOwner(manifests, subject.manifest, gameManifest, peerName);
|
|
364
|
+
if (owner === undefined) {
|
|
365
|
+
if (!optionalPeers.has(peerName)) {
|
|
366
|
+
notes.push(subject.packageName + ' requires peer ' + peerName + ' ' + range
|
|
367
|
+
+ '; no direct version was declared. Check the resolved lockfile.');
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const specifier = replacements.get(owner.manifest + '\0' + peerName) ?? owner.specifier;
|
|
372
|
+
const parsed = parseVersionSpecifier(specifier);
|
|
373
|
+
if (parsed !== undefined) {
|
|
374
|
+
if (!intersects(specifier, range)) {
|
|
375
|
+
blockers.push(subject.packageName + '@' + subject.latestVersion + ' requires '
|
|
376
|
+
+ peerName + ' ' + range + ', incompatible with ' + specifier + '.');
|
|
377
|
+
}
|
|
378
|
+
else if (!satisfies(parsed.version, range)) {
|
|
379
|
+
notes.push(subject.packageName + ' peer ' + peerName + ' requires ' + range
|
|
380
|
+
+ '; confirm the installed version selected by ' + specifier + '.');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (validRange(specifier) !== null && !intersects(specifier, range)) {
|
|
384
|
+
blockers.push(subject.packageName + '@' + subject.latestVersion + ' requires '
|
|
385
|
+
+ peerName + ' ' + range + ', incompatible with ' + specifier + '.');
|
|
386
|
+
}
|
|
387
|
+
else if (validRange(specifier) !== null) {
|
|
388
|
+
const minimum = minVersion(specifier);
|
|
389
|
+
if (minimum !== null && !satisfies(minimum, range)) {
|
|
390
|
+
notes.push(subject.packageName + ' peer ' + peerName + ' requires ' + range
|
|
391
|
+
+ '; verify the resolved version inside ' + specifier + '.');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
notes.push('Could not verify peer ' + peerName + ' for ' + subject.packageName
|
|
396
|
+
+ ' from specifier ' + specifier + '.');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function findPeerOwner(manifests, packageManifest, gameManifest, peerName) {
|
|
402
|
+
for (const manifest of [packageManifest, gameManifest]) {
|
|
403
|
+
const specifier = dependencySpecifier(manifests.get(manifest), peerName);
|
|
404
|
+
if (specifier !== undefined) {
|
|
405
|
+
return { manifest, specifier };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const candidates = [...manifests].flatMap(([manifest, content]) => {
|
|
409
|
+
const specifier = dependencySpecifier(content, peerName);
|
|
410
|
+
return specifier === undefined ? [] : [{ manifest, specifier }];
|
|
411
|
+
});
|
|
412
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
413
|
+
}
|
|
414
|
+
function dependencySpecifier(manifest, name) {
|
|
415
|
+
if (manifest === undefined) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
for (const section of dependencySections) {
|
|
419
|
+
const value = asObject(manifest[section])[name];
|
|
420
|
+
if (typeof value === 'string') {
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
function parseVersionSpecifier(value) {
|
|
427
|
+
if (valid(value) !== null) {
|
|
428
|
+
return { prefix: '', version: value };
|
|
429
|
+
}
|
|
430
|
+
const prefix = value[0];
|
|
431
|
+
if ((prefix === '^' || prefix === '~') && valid(value.slice(1)) !== null) {
|
|
432
|
+
return { prefix, version: value.slice(1) };
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
function updatePnpmLockfile(workspaceRoot) {
|
|
437
|
+
const result = spawnSync(process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm', ['install', '--lockfile-only', '--ignore-scripts'], {
|
|
438
|
+
cwd: workspaceRoot,
|
|
439
|
+
encoding: 'utf8',
|
|
440
|
+
timeout: 180_000,
|
|
441
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
442
|
+
shell: process.platform === 'win32',
|
|
443
|
+
});
|
|
444
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
445
|
+
throw new Error('pnpm lockfile update failed in ' + workspaceRoot + ': '
|
|
446
|
+
+ formatError(result.error ?? (result.stderr || result.stdout)));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function parseJsonObject(source, label) {
|
|
450
|
+
let parsed;
|
|
451
|
+
try {
|
|
452
|
+
parsed = JSON.parse(source);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
throw new Error('Invalid JSON in ' + label + ': ' + formatError(error));
|
|
456
|
+
}
|
|
457
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
458
|
+
throw new Error('Expected a JSON object in ' + label);
|
|
459
|
+
}
|
|
460
|
+
return parsed;
|
|
461
|
+
}
|
|
462
|
+
function asObject(value) {
|
|
463
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
464
|
+
? value
|
|
465
|
+
: {};
|
|
466
|
+
}
|
|
467
|
+
function stringifyLike(original, value) {
|
|
468
|
+
const indent = original.match(/\r?\n([ \t]+)"/u)?.[1] ?? ' ';
|
|
469
|
+
const newline = original.includes('\r\n') ? '\r\n' : '\n';
|
|
470
|
+
return JSON.stringify(value, null, indent).replaceAll('\n', newline)
|
|
471
|
+
+ (original.endsWith('\n') ? newline : '');
|
|
472
|
+
}
|
|
473
|
+
function inside(root, candidate) {
|
|
474
|
+
const relative = path.relative(root, candidate);
|
|
475
|
+
return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..'
|
|
476
|
+
&& !path.isAbsolute(relative));
|
|
477
|
+
}
|
|
478
|
+
function digest(source) {
|
|
479
|
+
return createHash('sha256').update(source).digest('hex');
|
|
480
|
+
}
|
|
481
|
+
function isKitPackageName(name) {
|
|
482
|
+
return /^@mpgd\/[a-z0-9][a-z0-9._-]*$/u.test(name);
|
|
483
|
+
}
|
|
484
|
+
function formatError(error) {
|
|
485
|
+
return error instanceof Error ? error.message : String(error);
|
|
486
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -47,25 +47,27 @@
|
|
|
47
47
|
"entities": "^8.0.0",
|
|
48
48
|
"esbuild": "^0.28.1",
|
|
49
49
|
"gunshi": "^0.35.1",
|
|
50
|
+
"semver": "^7.8.5",
|
|
50
51
|
"undici": "^7.28.0",
|
|
51
52
|
"@mpgd/phaser-assets": "0.7.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@types/node": "^24.0.0",
|
|
56
|
+
"@types/semver": "^7.8.0",
|
|
55
57
|
"ttsc": "0.30.4",
|
|
56
58
|
"typescript": "7.0.2",
|
|
57
59
|
"@mpgd/adapter-ait": "0.12.5",
|
|
60
|
+
"@mpgd/adapter-capacitor": "0.5.0",
|
|
58
61
|
"@mpgd/adapter-browser": "0.7.6",
|
|
59
62
|
"@mpgd/adapter-devvit": "0.9.11",
|
|
60
|
-
"@mpgd/adapter-capacitor": "0.5.0",
|
|
61
63
|
"@mpgd/adapter-verse8": "0.3.9",
|
|
64
|
+
"@mpgd/analytics": "0.3.15",
|
|
62
65
|
"@mpgd/bridge": "0.10.0",
|
|
63
66
|
"@mpgd/catalog": "0.7.5",
|
|
67
|
+
"@mpgd/game-services": "0.16.0",
|
|
64
68
|
"@mpgd/i18n": "0.6.4",
|
|
65
69
|
"@mpgd/platform": "0.13.0",
|
|
66
|
-
"@mpgd/target-config": "0.16.0"
|
|
67
|
-
"@mpgd/analytics": "0.3.15",
|
|
68
|
-
"@mpgd/game-services": "0.16.0"
|
|
70
|
+
"@mpgd/target-config": "0.16.0"
|
|
69
71
|
},
|
|
70
72
|
"main": "./dist/index.js",
|
|
71
73
|
"types": "./dist/index.d.ts",
|