@mpgd/cli 0.1.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/LICENSE +21 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +930 -0
- package/package.json +64 -0
- package/templates/phaser-game/README.md +66 -0
- package/templates/phaser-game/agent/acceptance.md +16 -0
- package/templates/phaser-game/agent/brief.md +20 -0
- package/templates/phaser-game/agent/game-manifest.json +41 -0
- package/templates/phaser-game/index.html +12 -0
- package/templates/phaser-game/mpgd.targets.json +42 -0
- package/templates/phaser-game/package.json +35 -0
- package/templates/phaser-game/src/env.d.ts +12 -0
- package/templates/phaser-game/src/game/state.ts +29 -0
- package/templates/phaser-game/src/i18n/messages.ts +64 -0
- package/templates/phaser-game/src/main.ts +78 -0
- package/templates/phaser-game/src/platform/gameServices.ts +70 -0
- package/templates/phaser-game/src/platform/installPlatform.ts +96 -0
- package/templates/phaser-game/src/platform/runtimeDetector.ts +30 -0
- package/templates/phaser-game/src/runtime/createGame.ts +28 -0
- package/templates/phaser-game/src/runtime/gameContext.ts +18 -0
- package/templates/phaser-game/src/runtime/id.ts +6 -0
- package/templates/phaser-game/src/scenes/BootScene.ts +11 -0
- package/templates/phaser-game/src/scenes/LobbyScene.ts +65 -0
- package/templates/phaser-game/src/scenes/PlayScene.ts +121 -0
- package/templates/phaser-game/src/styles.css +31 -0
- package/templates/phaser-game/tsconfig.json +27 -0
- package/templates/phaser-game/vite.config.ts +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import completion from '@gunshi/plugin-completion';
|
|
6
|
+
import i18n, { defineI18n } from '@gunshi/plugin-i18n';
|
|
7
|
+
import resources from '@gunshi/resources';
|
|
8
|
+
import { cli } from 'gunshi';
|
|
9
|
+
const sourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageRoot = resolvePackageRoot(sourceDir);
|
|
11
|
+
const detectedKitRoot = resolveDefaultKitRoot();
|
|
12
|
+
const gameTemplateDir = path.resolve(packageRoot, 'templates/phaser-game');
|
|
13
|
+
const cliVersion = readPackageVersion(packageRoot);
|
|
14
|
+
const defaultMatrixTargets = 'web,ait';
|
|
15
|
+
const defaultDependencyVersion = '^0.1.0';
|
|
16
|
+
const supportedBuildTargets = [
|
|
17
|
+
'browser',
|
|
18
|
+
'web',
|
|
19
|
+
'web-preview',
|
|
20
|
+
'android',
|
|
21
|
+
'ios',
|
|
22
|
+
'ait',
|
|
23
|
+
'devvit',
|
|
24
|
+
'reddit',
|
|
25
|
+
];
|
|
26
|
+
const supportedVariants = ['wrapper', 'sync', 'simulator', 'archive'];
|
|
27
|
+
export async function runMpgdCli(args) {
|
|
28
|
+
await cli([...args], entryCommand, {
|
|
29
|
+
name: 'mpgd',
|
|
30
|
+
version: cliVersion,
|
|
31
|
+
subCommands: {
|
|
32
|
+
game: gameCommand,
|
|
33
|
+
target: targetCommand,
|
|
34
|
+
kit: kitCommand,
|
|
35
|
+
},
|
|
36
|
+
plugins: [
|
|
37
|
+
i18n({
|
|
38
|
+
locale: readCliLocale(),
|
|
39
|
+
builtinResources: resources,
|
|
40
|
+
}),
|
|
41
|
+
completion({
|
|
42
|
+
config: {
|
|
43
|
+
subCommands: {
|
|
44
|
+
game: {
|
|
45
|
+
handler: () => listTemplateCompletionItems(),
|
|
46
|
+
},
|
|
47
|
+
target: {
|
|
48
|
+
handler: () => listTargetCompletionItems(),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function runCreateGameCli(args) {
|
|
57
|
+
await runMpgdCli(['game', 'create', ...args]);
|
|
58
|
+
}
|
|
59
|
+
export function readCliArgs() {
|
|
60
|
+
const encoded = process.env.MPGD_CLI_ARGV;
|
|
61
|
+
if (encoded === undefined) {
|
|
62
|
+
return process.argv.slice(2);
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(encoded);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error('Invalid MPGD_CLI_ARGV payload.');
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(parsed) || !parsed.every((value) => typeof value === 'string')) {
|
|
72
|
+
throw new Error('Invalid MPGD_CLI_ARGV payload.');
|
|
73
|
+
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
const entryCommand = defineI18n({
|
|
77
|
+
name: 'mpgd',
|
|
78
|
+
description: 'Manage mpgd-kit starter and target workflows.',
|
|
79
|
+
resource: commandResource({
|
|
80
|
+
en: 'Manage mpgd-kit starter and target workflows.',
|
|
81
|
+
ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
|
|
82
|
+
}),
|
|
83
|
+
run: () => {
|
|
84
|
+
console.log('Use a sub-command: game, target, kit.');
|
|
85
|
+
console.log('Run "pnpm mpgd --help" for available commands.');
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const gameCommand = defineI18n({
|
|
89
|
+
name: 'game',
|
|
90
|
+
description: 'Create standalone Phaser game starters.',
|
|
91
|
+
resource: commandResource({
|
|
92
|
+
en: 'Create standalone Phaser game starters.',
|
|
93
|
+
ko: '독립 Phaser 게임 스타터를 생성합니다.',
|
|
94
|
+
}),
|
|
95
|
+
subCommands: {
|
|
96
|
+
create: defineI18n({
|
|
97
|
+
name: 'create',
|
|
98
|
+
description: 'Create a Phaser game starter in a directory.',
|
|
99
|
+
resource: commandResource({
|
|
100
|
+
en: 'Create a Phaser game starter in a directory.',
|
|
101
|
+
ko: '지정한 디렉터리에 Phaser 게임 스타터를 생성합니다.',
|
|
102
|
+
}, {
|
|
103
|
+
directory: {
|
|
104
|
+
en: 'Directory to create.',
|
|
105
|
+
ko: '생성할 디렉터리.',
|
|
106
|
+
},
|
|
107
|
+
title: {
|
|
108
|
+
en: 'Display title. Defaults to the directory name.',
|
|
109
|
+
ko: '표시 제목. 기본값은 디렉터리 이름입니다.',
|
|
110
|
+
},
|
|
111
|
+
'package-name': {
|
|
112
|
+
en: 'npm package name. Defaults to the directory basename.',
|
|
113
|
+
ko: 'npm 패키지명. 기본값은 디렉터리 마지막 이름입니다.',
|
|
114
|
+
},
|
|
115
|
+
'dependency-version': {
|
|
116
|
+
en: 'Version range for @mpgd packages.',
|
|
117
|
+
ko: '@mpgd 패키지에 사용할 버전 범위.',
|
|
118
|
+
},
|
|
119
|
+
workspace: {
|
|
120
|
+
en: 'Use workspace:* for @mpgd dependencies.',
|
|
121
|
+
ko: '@mpgd 의존성에 workspace:*를 사용합니다.',
|
|
122
|
+
},
|
|
123
|
+
'kit-path': {
|
|
124
|
+
en: 'Path to an mpgd-kit checkout when using --workspace.',
|
|
125
|
+
ko: '--workspace 사용 시 mpgd-kit 체크아웃 경로.',
|
|
126
|
+
},
|
|
127
|
+
'dry-run': {
|
|
128
|
+
en: 'Print files that would be generated without writing them.',
|
|
129
|
+
ko: '파일을 쓰지 않고 생성 예정 목록만 출력합니다.',
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
args: {
|
|
133
|
+
directory: {
|
|
134
|
+
type: 'positional',
|
|
135
|
+
required: true,
|
|
136
|
+
description: 'Directory to create.',
|
|
137
|
+
},
|
|
138
|
+
title: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
required: false,
|
|
141
|
+
description: 'Display title. Defaults to the directory name.',
|
|
142
|
+
},
|
|
143
|
+
'package-name': {
|
|
144
|
+
type: 'string',
|
|
145
|
+
required: false,
|
|
146
|
+
description: 'npm package name. Defaults to the directory basename.',
|
|
147
|
+
},
|
|
148
|
+
'dependency-version': {
|
|
149
|
+
type: 'string',
|
|
150
|
+
required: false,
|
|
151
|
+
default: defaultDependencyVersion,
|
|
152
|
+
description: 'Version range for @mpgd packages.',
|
|
153
|
+
},
|
|
154
|
+
workspace: {
|
|
155
|
+
type: 'boolean',
|
|
156
|
+
required: false,
|
|
157
|
+
description: 'Use workspace:* for @mpgd dependencies.',
|
|
158
|
+
},
|
|
159
|
+
'kit-path': {
|
|
160
|
+
type: 'string',
|
|
161
|
+
required: false,
|
|
162
|
+
description: 'Path to an mpgd-kit checkout when using --workspace.',
|
|
163
|
+
},
|
|
164
|
+
'dry-run': {
|
|
165
|
+
type: 'boolean',
|
|
166
|
+
required: false,
|
|
167
|
+
description: 'Print files that would be generated without writing them.',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
run: (ctx) => {
|
|
171
|
+
const positionals = readLocalPositionals(ctx.positionals, ['game', 'create']);
|
|
172
|
+
const directory = readRequiredPositional(positionals, 0, 'directory');
|
|
173
|
+
const title = readOptionalString(ctx.values.title);
|
|
174
|
+
const packageName = readOptionalString(ctx.values['package-name']);
|
|
175
|
+
const dependencyVersion = ctx.values.workspace === true
|
|
176
|
+
? 'workspace:*'
|
|
177
|
+
: (readOptionalString(ctx.values['dependency-version']) ?? defaultDependencyVersion);
|
|
178
|
+
const kitPath = readGameCreateKitPath(ctx.values, ctx.values.workspace === true);
|
|
179
|
+
createGameApp({
|
|
180
|
+
directory,
|
|
181
|
+
...(title === undefined ? {} : { title }),
|
|
182
|
+
...(packageName === undefined ? {} : { packageName }),
|
|
183
|
+
dependencyVersion,
|
|
184
|
+
workspace: ctx.values.workspace === true,
|
|
185
|
+
...(kitPath === undefined ? {} : { kitPath }),
|
|
186
|
+
dryRun: ctx.values['dry-run'] === true,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
run: () => {
|
|
192
|
+
console.log('Use "mpgd game create <directory>".');
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
const targetCommand = defineI18n({
|
|
196
|
+
name: 'target',
|
|
197
|
+
description: 'Build and smoke target artifacts with generated game target config.',
|
|
198
|
+
resource: commandResource({
|
|
199
|
+
en: 'Build and smoke target artifacts with generated game target config.',
|
|
200
|
+
ko: '게임 타깃 설정으로 타깃 산출물을 빌드하고 스모크 검증합니다.',
|
|
201
|
+
}),
|
|
202
|
+
subCommands: {
|
|
203
|
+
build: defineI18n({
|
|
204
|
+
name: 'build',
|
|
205
|
+
description: 'Build one target artifact.',
|
|
206
|
+
resource: commandResource({
|
|
207
|
+
en: 'Build one target artifact.',
|
|
208
|
+
ko: '단일 타깃 산출물을 빌드합니다.',
|
|
209
|
+
}, {
|
|
210
|
+
target: {
|
|
211
|
+
en: `Target (${supportedBuildTargets.join(', ')})`,
|
|
212
|
+
ko: `타깃 (${supportedBuildTargets.join(', ')})`,
|
|
213
|
+
},
|
|
214
|
+
profile: {
|
|
215
|
+
en: 'Vite build mode/profile.',
|
|
216
|
+
ko: 'Vite 빌드 모드/프로필.',
|
|
217
|
+
},
|
|
218
|
+
'targets-file': {
|
|
219
|
+
en: 'Game target config file.',
|
|
220
|
+
ko: '게임 타깃 설정 파일.',
|
|
221
|
+
},
|
|
222
|
+
'kit-path': {
|
|
223
|
+
en: 'Path to the mpgd-kit checkout used by target wrappers.',
|
|
224
|
+
ko: '타깃 wrapper에 사용할 mpgd-kit 체크아웃 경로.',
|
|
225
|
+
},
|
|
226
|
+
variant: {
|
|
227
|
+
en: `Target variant (${supportedVariants.join(', ')})`,
|
|
228
|
+
ko: `타깃 변형 (${supportedVariants.join(', ')})`,
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
args: {
|
|
232
|
+
target: {
|
|
233
|
+
type: 'positional',
|
|
234
|
+
required: true,
|
|
235
|
+
description: `Target (${supportedBuildTargets.join(', ')})`,
|
|
236
|
+
},
|
|
237
|
+
profile: {
|
|
238
|
+
type: 'positional',
|
|
239
|
+
required: false,
|
|
240
|
+
default: 'production',
|
|
241
|
+
description: 'Vite build mode/profile.',
|
|
242
|
+
},
|
|
243
|
+
...targetConfigArgs(),
|
|
244
|
+
variant: {
|
|
245
|
+
type: 'enum',
|
|
246
|
+
choices: supportedVariants,
|
|
247
|
+
required: false,
|
|
248
|
+
description: `Target variant (${supportedVariants.join(', ')})`,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
run: (ctx) => {
|
|
252
|
+
const positionals = readLocalPositionals(ctx.positionals, ['target', 'build']);
|
|
253
|
+
const target = readRequiredPositional(positionals, 0, 'target');
|
|
254
|
+
const profile = positionals[1] ?? 'production';
|
|
255
|
+
const variant = readOptionalString(ctx.values.variant);
|
|
256
|
+
runTargetCommand({
|
|
257
|
+
action: 'build',
|
|
258
|
+
target,
|
|
259
|
+
profile,
|
|
260
|
+
env: createTargetCommandEnv(ctx.values),
|
|
261
|
+
...(variant === undefined ? {} : { variant }),
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
smoke: defineI18n({
|
|
266
|
+
name: 'smoke',
|
|
267
|
+
description: 'Verify one existing target artifact.',
|
|
268
|
+
resource: commandResource({
|
|
269
|
+
en: 'Verify one existing target artifact.',
|
|
270
|
+
ko: '기존 단일 타깃 산출물을 스모크 검증합니다.',
|
|
271
|
+
}, {
|
|
272
|
+
target: {
|
|
273
|
+
en: `Target (${supportedBuildTargets.join(', ')})`,
|
|
274
|
+
ko: `타깃 (${supportedBuildTargets.join(', ')})`,
|
|
275
|
+
},
|
|
276
|
+
'targets-file': {
|
|
277
|
+
en: 'Game target config file.',
|
|
278
|
+
ko: '게임 타깃 설정 파일.',
|
|
279
|
+
},
|
|
280
|
+
'kit-path': {
|
|
281
|
+
en: 'Path to the mpgd-kit checkout used by target wrappers.',
|
|
282
|
+
ko: '타깃 wrapper에 사용할 mpgd-kit 체크아웃 경로.',
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
args: {
|
|
286
|
+
target: {
|
|
287
|
+
type: 'positional',
|
|
288
|
+
required: true,
|
|
289
|
+
description: `Target (${supportedBuildTargets.join(', ')})`,
|
|
290
|
+
},
|
|
291
|
+
...targetConfigArgs(),
|
|
292
|
+
},
|
|
293
|
+
run: (ctx) => {
|
|
294
|
+
const positionals = readLocalPositionals(ctx.positionals, ['target', 'smoke']);
|
|
295
|
+
const target = readRequiredPositional(positionals, 0, 'target');
|
|
296
|
+
runTargetCommand({
|
|
297
|
+
action: 'smoke',
|
|
298
|
+
target,
|
|
299
|
+
env: createTargetCommandEnv(ctx.values),
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
'build-all': defineI18n({
|
|
304
|
+
name: 'build-all',
|
|
305
|
+
description: 'Build multiple target artifacts.',
|
|
306
|
+
resource: commandResource({
|
|
307
|
+
en: 'Build multiple target artifacts.',
|
|
308
|
+
ko: '여러 타깃 산출물을 빌드합니다.',
|
|
309
|
+
}, matrixArgResources()),
|
|
310
|
+
args: {
|
|
311
|
+
targets: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
required: false,
|
|
314
|
+
default: defaultMatrixTargets,
|
|
315
|
+
description: 'Comma-separated target list, or all.',
|
|
316
|
+
},
|
|
317
|
+
profile: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
required: false,
|
|
320
|
+
default: 'production',
|
|
321
|
+
description: 'Build profile passed to target builds.',
|
|
322
|
+
},
|
|
323
|
+
'ait-variant': {
|
|
324
|
+
type: 'enum',
|
|
325
|
+
choices: supportedVariants,
|
|
326
|
+
required: false,
|
|
327
|
+
description: 'Variant for the ait target, such as wrapper.',
|
|
328
|
+
},
|
|
329
|
+
'ios-variant': {
|
|
330
|
+
type: 'enum',
|
|
331
|
+
choices: supportedVariants,
|
|
332
|
+
required: false,
|
|
333
|
+
description: 'Variant for the ios target, such as sync or archive.',
|
|
334
|
+
},
|
|
335
|
+
...targetConfigArgs(),
|
|
336
|
+
},
|
|
337
|
+
run: (ctx) => {
|
|
338
|
+
const aitVariant = readOptionalString(ctx.values['ait-variant']);
|
|
339
|
+
const iosVariant = readOptionalString(ctx.values['ios-variant']);
|
|
340
|
+
runTargetMatrix({
|
|
341
|
+
action: 'build',
|
|
342
|
+
targets: parseTargetList(readOptionalString(ctx.values.targets)),
|
|
343
|
+
profile: readOptionalString(ctx.values.profile) ?? 'production',
|
|
344
|
+
env: createTargetCommandEnv(ctx.values),
|
|
345
|
+
...(aitVariant === undefined ? {} : { aitVariant }),
|
|
346
|
+
...(iosVariant === undefined ? {} : { iosVariant }),
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
350
|
+
'smoke-all': defineI18n({
|
|
351
|
+
name: 'smoke-all',
|
|
352
|
+
description: 'Verify existing artifacts for multiple targets.',
|
|
353
|
+
resource: commandResource({
|
|
354
|
+
en: 'Verify existing artifacts for multiple targets.',
|
|
355
|
+
ko: '여러 타깃의 기존 산출물을 스모크 검증합니다.',
|
|
356
|
+
}, matrixArgResources()),
|
|
357
|
+
args: {
|
|
358
|
+
targets: {
|
|
359
|
+
type: 'string',
|
|
360
|
+
required: false,
|
|
361
|
+
default: defaultMatrixTargets,
|
|
362
|
+
description: 'Comma-separated target list, or all.',
|
|
363
|
+
},
|
|
364
|
+
...targetConfigArgs(),
|
|
365
|
+
},
|
|
366
|
+
run: (ctx) => {
|
|
367
|
+
runTargetMatrix({
|
|
368
|
+
action: 'smoke',
|
|
369
|
+
targets: parseTargetList(readOptionalString(ctx.values.targets)),
|
|
370
|
+
env: createTargetCommandEnv(ctx.values),
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
},
|
|
375
|
+
run: () => {
|
|
376
|
+
console.log('Use "mpgd target build <target>" or "mpgd target smoke <target>".');
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
const kitCommand = defineI18n({
|
|
380
|
+
name: 'kit',
|
|
381
|
+
description: 'Inspect this mpgd-kit checkout.',
|
|
382
|
+
resource: commandResource({
|
|
383
|
+
en: 'Inspect this mpgd-kit checkout.',
|
|
384
|
+
ko: '현재 mpgd-kit 체크아웃을 점검합니다.',
|
|
385
|
+
}),
|
|
386
|
+
subCommands: {
|
|
387
|
+
doctor: defineI18n({
|
|
388
|
+
name: 'doctor',
|
|
389
|
+
description: 'Print CLI, kit, template, and target-wrapper status.',
|
|
390
|
+
resource: commandResource({
|
|
391
|
+
en: 'Print CLI, kit, template, and target-wrapper status.',
|
|
392
|
+
ko: 'CLI, kit, template, target wrapper 상태를 출력합니다.',
|
|
393
|
+
}),
|
|
394
|
+
run: () => {
|
|
395
|
+
console.log(`cli package: ${packageRoot}`);
|
|
396
|
+
console.log(`mpgd-kit: ${detectedKitRoot ?? 'not detected'}`);
|
|
397
|
+
console.log(`cli template: ${existsSync(gameTemplateDir) ? 'ok' : 'missing'}`);
|
|
398
|
+
console.log(`mobile wrapper: ${kitFileStatus('apps/mobile-capacitor/package.json')}`);
|
|
399
|
+
console.log(`ait wrapper: ${kitFileStatus('apps/target-ait/package.json')}`);
|
|
400
|
+
console.log(`devvit wrapper: ${kitFileStatus('apps/target-devvit/package.json')}`);
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
403
|
+
},
|
|
404
|
+
run: () => {
|
|
405
|
+
console.log('Use "mpgd kit doctor".');
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
function commandResource(description, args = {}) {
|
|
409
|
+
const normalized = 'base' in description
|
|
410
|
+
? description
|
|
411
|
+
: {
|
|
412
|
+
base: description,
|
|
413
|
+
args,
|
|
414
|
+
};
|
|
415
|
+
return (locale) => {
|
|
416
|
+
const resource = {
|
|
417
|
+
description: pickLocalizedText(locale, normalized.base),
|
|
418
|
+
};
|
|
419
|
+
for (const [name, text] of Object.entries(normalized.args)) {
|
|
420
|
+
resource[`arg:${name}`] = pickLocalizedText(locale, text);
|
|
421
|
+
}
|
|
422
|
+
return resource;
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function targetConfigArgs() {
|
|
426
|
+
return {
|
|
427
|
+
'targets-file': {
|
|
428
|
+
type: 'string',
|
|
429
|
+
required: false,
|
|
430
|
+
default: 'mpgd.targets.json',
|
|
431
|
+
description: 'Game target config file.',
|
|
432
|
+
},
|
|
433
|
+
'kit-path': {
|
|
434
|
+
type: 'string',
|
|
435
|
+
required: false,
|
|
436
|
+
description: 'Path to the mpgd-kit checkout used by target wrappers.',
|
|
437
|
+
},
|
|
438
|
+
'resolved-targets-file': {
|
|
439
|
+
type: 'string',
|
|
440
|
+
required: false,
|
|
441
|
+
description: 'Output path for the resolved target config file.',
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function matrixArgResources() {
|
|
446
|
+
return {
|
|
447
|
+
targets: {
|
|
448
|
+
en: 'Comma-separated target list, or all.',
|
|
449
|
+
ko: '쉼표로 구분한 타깃 목록 또는 all.',
|
|
450
|
+
},
|
|
451
|
+
profile: {
|
|
452
|
+
en: 'Build profile passed to target builds.',
|
|
453
|
+
ko: '타깃 빌드에 전달할 빌드 프로필.',
|
|
454
|
+
},
|
|
455
|
+
'targets-file': {
|
|
456
|
+
en: 'Game target config file.',
|
|
457
|
+
ko: '게임 타깃 설정 파일.',
|
|
458
|
+
},
|
|
459
|
+
'kit-path': {
|
|
460
|
+
en: 'Path to the mpgd-kit checkout used by target wrappers.',
|
|
461
|
+
ko: '타깃 wrapper에 사용할 mpgd-kit 체크아웃 경로.',
|
|
462
|
+
},
|
|
463
|
+
'ait-variant': {
|
|
464
|
+
en: 'Variant for the ait target, such as wrapper.',
|
|
465
|
+
ko: 'wrapper 같은 ait 타깃 변형.',
|
|
466
|
+
},
|
|
467
|
+
'ios-variant': {
|
|
468
|
+
en: 'Variant for the ios target, such as sync, simulator, or archive.',
|
|
469
|
+
ko: 'sync, simulator, archive 같은 ios 타깃 변형.',
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function createGameApp(input) {
|
|
474
|
+
const appDir = path.resolve(process.cwd(), input.directory);
|
|
475
|
+
const gameName = path.basename(appDir);
|
|
476
|
+
assertValidGameName(gameName);
|
|
477
|
+
const packageName = input.packageName ?? gameName;
|
|
478
|
+
assertValidPackageName(packageName);
|
|
479
|
+
assertValidDependencyVersion(input.dependencyVersion);
|
|
480
|
+
if (existsSync(appDir)) {
|
|
481
|
+
throw new Error(`Game directory already exists: ${appDir}`);
|
|
482
|
+
}
|
|
483
|
+
const context = createTemplateContext({
|
|
484
|
+
gameName,
|
|
485
|
+
packageName,
|
|
486
|
+
dependencyVersion: input.dependencyVersion,
|
|
487
|
+
appDir,
|
|
488
|
+
workspace: input.workspace,
|
|
489
|
+
...(input.kitPath === undefined ? {} : { kitPath: input.kitPath }),
|
|
490
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
491
|
+
});
|
|
492
|
+
const files = collectTemplateFiles(gameTemplateDir);
|
|
493
|
+
if (input.dryRun) {
|
|
494
|
+
console.log(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
|
|
495
|
+
for (const file of files) {
|
|
496
|
+
console.log(`- ${path.join(path.relative(process.cwd(), appDir), file.relativePath)}`);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
for (const file of files) {
|
|
501
|
+
const outputFile = path.join(appDir, file.relativePath);
|
|
502
|
+
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
503
|
+
writeFileSync(outputFile, renderTemplate(file.content, context));
|
|
504
|
+
}
|
|
505
|
+
console.log(`Created ${appDir}`);
|
|
506
|
+
console.log('Next steps:');
|
|
507
|
+
console.log(` pnpm --dir ${appDir} install`);
|
|
508
|
+
console.log(` pnpm --dir ${appDir} dev`);
|
|
509
|
+
console.log('Target builds require an mpgd-kit checkout:');
|
|
510
|
+
console.log([
|
|
511
|
+
' mpgd target build-all',
|
|
512
|
+
`--targets-file ${path.join(appDir, 'mpgd.targets.json')}`,
|
|
513
|
+
'--targets web,ait',
|
|
514
|
+
'--ait-variant wrapper',
|
|
515
|
+
'--kit-path <path-to-mpgd-kit>',
|
|
516
|
+
].join(' '));
|
|
517
|
+
}
|
|
518
|
+
function assertValidGameName(name) {
|
|
519
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
520
|
+
throw new Error('Game directory basename must use kebab-case, for example: puzzle-one');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function assertValidPackageName(name) {
|
|
524
|
+
if (!/^(?:@[a-z][a-z0-9-]*\/)?[a-z][a-z0-9-]*$/.test(name)) {
|
|
525
|
+
throw new Error('Package name must be kebab-case, optionally scoped, for example: @scope/puzzle-one');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function assertValidDependencyVersion(version) {
|
|
529
|
+
if (!/^(?:workspace:\*|[~^]?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.test(version)) {
|
|
530
|
+
throw new Error('Dependency version must be workspace:* or a semver version/range such as ^0.1.0.');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function createTemplateContext(input) {
|
|
534
|
+
const title = input.title === undefined || input.title.length === 0 ? input.gameName : input.title;
|
|
535
|
+
assertValidGameTitle(title);
|
|
536
|
+
const kitPath = input.kitPath ?? detectedKitRoot;
|
|
537
|
+
if (input.workspace && kitPath === undefined) {
|
|
538
|
+
throw new Error('Could not detect an mpgd-kit checkout for --workspace. Pass --kit-path or set MPGD_KIT_PATH.');
|
|
539
|
+
}
|
|
540
|
+
const workspaceRoot = toTemplatePath(path.relative(input.appDir, kitPath ?? input.appDir) || '.');
|
|
541
|
+
const workspacePrefix = workspaceRoot;
|
|
542
|
+
return {
|
|
543
|
+
gameName: input.gameName,
|
|
544
|
+
gameTitle: title,
|
|
545
|
+
packageName: input.packageName,
|
|
546
|
+
dependencyVersion: input.dependencyVersion,
|
|
547
|
+
tsconfigExtendsLine: input.workspace
|
|
548
|
+
? ` "extends": "${workspacePrefix}/tsconfig.base.json",`
|
|
549
|
+
: '',
|
|
550
|
+
tsconfigWorkspaceIncludes: input.workspace
|
|
551
|
+
? [
|
|
552
|
+
`,\n "${workspacePrefix}/packages/**/*.ts"`,
|
|
553
|
+
`,\n "${workspacePrefix}/adapters/**/*.ts"`,
|
|
554
|
+
`,\n "${workspacePrefix}/native-plugins/*/src/**/*.ts"`,
|
|
555
|
+
].join('')
|
|
556
|
+
: '',
|
|
557
|
+
tsconfigWorkspaceExcludes: input.workspace
|
|
558
|
+
? [
|
|
559
|
+
`,\n "${workspacePrefix}/packages/**/dist/**"`,
|
|
560
|
+
`,\n "${workspacePrefix}/adapters/**/dist/**"`,
|
|
561
|
+
`,\n "${workspacePrefix}/native-plugins/**/dist/**"`,
|
|
562
|
+
].join('')
|
|
563
|
+
: '',
|
|
564
|
+
workspaceI18nBuildPrefix: input.workspace
|
|
565
|
+
? `pnpm --dir ${workspacePrefix} i18n:build && `
|
|
566
|
+
: '',
|
|
567
|
+
pascalName: toPascalCase(input.gameName),
|
|
568
|
+
camelName: toCamelCase(input.gameName),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function assertValidGameTitle(title) {
|
|
572
|
+
if (title.trim().length === 0) {
|
|
573
|
+
throw new Error('Game title cannot be empty.');
|
|
574
|
+
}
|
|
575
|
+
if (/[\u0000-\u001f<>"'\\]/u.test(title)) {
|
|
576
|
+
throw new Error('Game title cannot contain control characters, quotes, backslashes, or angle brackets.');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function collectTemplateFiles(templateDir) {
|
|
580
|
+
const files = [];
|
|
581
|
+
walkTemplateDir(templateDir, '', files);
|
|
582
|
+
return files;
|
|
583
|
+
}
|
|
584
|
+
function walkTemplateDir(templateDir, relativeDir, files) {
|
|
585
|
+
const currentDir = path.join(templateDir, relativeDir);
|
|
586
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
587
|
+
const relativePath = path.join(relativeDir, entry.name);
|
|
588
|
+
if (entry.isDirectory()) {
|
|
589
|
+
walkTemplateDir(templateDir, relativePath, files);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
files.push({
|
|
593
|
+
relativePath,
|
|
594
|
+
content: readFileSync(path.join(templateDir, relativePath), 'utf8'),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function renderTemplate(content, context) {
|
|
599
|
+
return content
|
|
600
|
+
.replaceAll('__GAME_NAME__', context.gameName)
|
|
601
|
+
.replaceAll('__GAME_TITLE__', context.gameTitle)
|
|
602
|
+
.replaceAll('__PACKAGE_NAME__', context.packageName)
|
|
603
|
+
.replaceAll('__MPGD_DEPENDENCY_VERSION__', context.dependencyVersion)
|
|
604
|
+
.replaceAll('__TSCONFIG_EXTENDS_LINE__', context.tsconfigExtendsLine)
|
|
605
|
+
.replaceAll('__TSCONFIG_WORKSPACE_INCLUDES__', context.tsconfigWorkspaceIncludes)
|
|
606
|
+
.replaceAll('__TSCONFIG_WORKSPACE_EXCLUDES__', context.tsconfigWorkspaceExcludes)
|
|
607
|
+
.replaceAll('__WORKSPACE_I18N_BUILD_PREFIX__', context.workspaceI18nBuildPrefix)
|
|
608
|
+
.replaceAll('__PASCAL_NAME__', context.pascalName)
|
|
609
|
+
.replaceAll('__CAMEL_NAME__', context.camelName);
|
|
610
|
+
}
|
|
611
|
+
function toPascalCase(value) {
|
|
612
|
+
return value
|
|
613
|
+
.split('-')
|
|
614
|
+
.filter((part) => part.length > 0)
|
|
615
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
|
|
616
|
+
.join('');
|
|
617
|
+
}
|
|
618
|
+
function toCamelCase(value) {
|
|
619
|
+
const pascal = toPascalCase(value);
|
|
620
|
+
return `${pascal[0]?.toLowerCase() ?? ''}${pascal.slice(1)}`;
|
|
621
|
+
}
|
|
622
|
+
function toTemplatePath(value) {
|
|
623
|
+
return value.split(path.sep).join('/');
|
|
624
|
+
}
|
|
625
|
+
function createTargetCommandEnv(values) {
|
|
626
|
+
const targetsFile = path.resolve(readOptionalString(values['targets-file']) ?? 'mpgd.targets.json');
|
|
627
|
+
const kitPath = resolveKitPathForTarget(values);
|
|
628
|
+
const resolvedTargetsFile = readOptionalString(values['resolved-targets-file']);
|
|
629
|
+
const preparedTargetsFile = preparePlatformTargetsFile({
|
|
630
|
+
targetsFile,
|
|
631
|
+
kitPath,
|
|
632
|
+
...(resolvedTargetsFile === undefined
|
|
633
|
+
? {}
|
|
634
|
+
: { outputFile: path.resolve(resolvedTargetsFile) }),
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
...process.env,
|
|
638
|
+
MPGD_KIT_PATH: kitPath,
|
|
639
|
+
MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function preparePlatformTargetsFile(input) {
|
|
643
|
+
const parsed = readJsonForCli(input.targetsFile);
|
|
644
|
+
const gameRoot = path.dirname(input.targetsFile);
|
|
645
|
+
const outputFile = input.outputFile ?? path.join(gameRoot, '.mpgd.targets.generated.json');
|
|
646
|
+
const outputDir = path.dirname(outputFile);
|
|
647
|
+
if (path.resolve(outputDir) !== path.resolve(gameRoot)) {
|
|
648
|
+
throw new Error('--resolved-targets-file must stay beside the source targets file so relative target paths keep their meaning.');
|
|
649
|
+
}
|
|
650
|
+
const rendered = replaceTargetTokens(parsed, {
|
|
651
|
+
gameRoot,
|
|
652
|
+
kitPath: input.kitPath,
|
|
653
|
+
});
|
|
654
|
+
writeFileSync(outputFile, `${JSON.stringify(rendered, null, 2)}\n`);
|
|
655
|
+
return outputFile;
|
|
656
|
+
}
|
|
657
|
+
function readJsonForCli(file) {
|
|
658
|
+
let raw;
|
|
659
|
+
try {
|
|
660
|
+
raw = readFileSync(file, 'utf8');
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
throw new Error(`Failed to read targets file ${file}: ${formatError(error)}`);
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
return JSON.parse(raw);
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
throw new Error(`Failed to parse targets file ${file}: ${formatError(error)}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function formatError(error) {
|
|
673
|
+
return error instanceof Error ? error.message : String(error);
|
|
674
|
+
}
|
|
675
|
+
function replaceTargetTokens(value, context) {
|
|
676
|
+
if (typeof value === 'string') {
|
|
677
|
+
return value
|
|
678
|
+
.replaceAll('${MPGD_KIT_PATH}', context.kitPath)
|
|
679
|
+
.replaceAll('${MPGD_GAME_ROOT}', context.gameRoot)
|
|
680
|
+
.replaceAll('${MPGD_GAME_APP_ROOT}', context.gameRoot);
|
|
681
|
+
}
|
|
682
|
+
if (Array.isArray(value)) {
|
|
683
|
+
return value.map((entry) => replaceTargetTokens(entry, context));
|
|
684
|
+
}
|
|
685
|
+
if (typeof value === 'object' && value !== null) {
|
|
686
|
+
const output = Object.create(null);
|
|
687
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
688
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
output[key] = replaceTargetTokens(entry, context);
|
|
692
|
+
}
|
|
693
|
+
return output;
|
|
694
|
+
}
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
function runTargetCommand(input) {
|
|
698
|
+
const target = normalizeBuildTarget(input.target);
|
|
699
|
+
const env = withTargetVariantEnv(target, input.variant, input.env);
|
|
700
|
+
const args = input.action === 'build'
|
|
701
|
+
? ['build:target', target, input.profile ?? 'production']
|
|
702
|
+
: ['smoke:target', target];
|
|
703
|
+
console.log(`[mpgd] ${input.action} ${target}`);
|
|
704
|
+
runPnpm(args, env);
|
|
705
|
+
}
|
|
706
|
+
function runTargetMatrix(input) {
|
|
707
|
+
for (const target of input.targets) {
|
|
708
|
+
const variant = variantForTarget(target, input);
|
|
709
|
+
runTargetCommand({
|
|
710
|
+
action: input.action,
|
|
711
|
+
target,
|
|
712
|
+
env: input.env,
|
|
713
|
+
...(input.profile === undefined ? {} : { profile: input.profile }),
|
|
714
|
+
...(variant === undefined ? {} : { variant }),
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function variantForTarget(target, input) {
|
|
719
|
+
const normalized = normalizeBuildTarget(target);
|
|
720
|
+
if (normalized === 'ait') {
|
|
721
|
+
return input.aitVariant;
|
|
722
|
+
}
|
|
723
|
+
if (normalized === 'ios') {
|
|
724
|
+
return input.iosVariant;
|
|
725
|
+
}
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
function withTargetVariantEnv(target, variant, env) {
|
|
729
|
+
if (target === 'ait' && variant === 'wrapper') {
|
|
730
|
+
return {
|
|
731
|
+
...env,
|
|
732
|
+
MPGD_AIT_PACKAGE_MODE: 'skip',
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (target === 'ios' && variant === 'archive') {
|
|
736
|
+
return {
|
|
737
|
+
...env,
|
|
738
|
+
MPGD_RUN_IOS_ARCHIVE: '1',
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (target === 'ios' && variant === 'simulator') {
|
|
742
|
+
return {
|
|
743
|
+
...env,
|
|
744
|
+
MPGD_RUN_IOS_SIMULATOR_BUILD: '1',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return env;
|
|
748
|
+
}
|
|
749
|
+
function parseTargetList(value) {
|
|
750
|
+
const raw = value ?? defaultMatrixTargets;
|
|
751
|
+
if (raw === 'all') {
|
|
752
|
+
return ['web-preview', 'android', 'ios', 'ait', 'reddit'];
|
|
753
|
+
}
|
|
754
|
+
const targets = raw
|
|
755
|
+
.split(',')
|
|
756
|
+
.map((target) => target.trim())
|
|
757
|
+
.filter((target) => target.length > 0)
|
|
758
|
+
.map((target) => normalizeBuildTarget(target));
|
|
759
|
+
if (targets.length === 0) {
|
|
760
|
+
throw new Error('Target list cannot be empty.');
|
|
761
|
+
}
|
|
762
|
+
return [...new Set(targets)];
|
|
763
|
+
}
|
|
764
|
+
function normalizeBuildTarget(target) {
|
|
765
|
+
if (target === 'browser' || target === 'web') {
|
|
766
|
+
return 'web-preview';
|
|
767
|
+
}
|
|
768
|
+
if (target === 'devvit') {
|
|
769
|
+
return 'reddit';
|
|
770
|
+
}
|
|
771
|
+
if (!supportedBuildTargets.includes(target)) {
|
|
772
|
+
throw new Error(`Unsupported target: ${target}`);
|
|
773
|
+
}
|
|
774
|
+
return target;
|
|
775
|
+
}
|
|
776
|
+
function runPnpm(args, commandEnv) {
|
|
777
|
+
const kitPath = commandEnv.MPGD_KIT_PATH;
|
|
778
|
+
if (kitPath === undefined || kitPath.length === 0) {
|
|
779
|
+
throw new Error('Missing MPGD_KIT_PATH. Pass --kit-path or set MPGD_KIT_PATH.');
|
|
780
|
+
}
|
|
781
|
+
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
782
|
+
const result = spawnSync(command, [...args], {
|
|
783
|
+
cwd: kitPath,
|
|
784
|
+
env: commandEnv,
|
|
785
|
+
stdio: 'inherit',
|
|
786
|
+
shell: process.platform === 'win32',
|
|
787
|
+
});
|
|
788
|
+
if (result.error !== undefined) {
|
|
789
|
+
throw result.error;
|
|
790
|
+
}
|
|
791
|
+
if (result.status !== 0) {
|
|
792
|
+
throw new Error(`pnpm ${args.join(' ')} failed with exit code ${result.status}.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function readRequiredPositional(positionals, index, label) {
|
|
796
|
+
const value = positionals[index];
|
|
797
|
+
if (value === undefined || value.length === 0) {
|
|
798
|
+
throw new Error(`Missing ${label}.`);
|
|
799
|
+
}
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
function readLocalPositionals(positionals, commandPath) {
|
|
803
|
+
const commandPathMatches = commandPath.every((segment, index) => positionals[index] === segment);
|
|
804
|
+
if (!commandPathMatches) {
|
|
805
|
+
return positionals;
|
|
806
|
+
}
|
|
807
|
+
return positionals.slice(commandPath.length);
|
|
808
|
+
}
|
|
809
|
+
function readOptionalString(value) {
|
|
810
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
811
|
+
}
|
|
812
|
+
function readGameCreateKitPath(values, workspace) {
|
|
813
|
+
const explicitKitPath = readOptionalString(values['kit-path']);
|
|
814
|
+
if (explicitKitPath !== undefined) {
|
|
815
|
+
return assertKitRoot(path.resolve(explicitKitPath));
|
|
816
|
+
}
|
|
817
|
+
if (!workspace) {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
if (process.env.MPGD_KIT_PATH === undefined || process.env.MPGD_KIT_PATH.length === 0) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
return isKitRoot(path.resolve(process.env.MPGD_KIT_PATH))
|
|
824
|
+
? path.resolve(process.env.MPGD_KIT_PATH)
|
|
825
|
+
: undefined;
|
|
826
|
+
}
|
|
827
|
+
function resolveKitPathForTarget(values) {
|
|
828
|
+
const rawKitPath = readOptionalString(values['kit-path']) ?? process.env.MPGD_KIT_PATH ?? detectedKitRoot;
|
|
829
|
+
if (rawKitPath === undefined) {
|
|
830
|
+
throw new Error('Could not detect an mpgd-kit checkout. Pass --kit-path or set MPGD_KIT_PATH before running target commands.');
|
|
831
|
+
}
|
|
832
|
+
return assertKitRoot(path.resolve(rawKitPath));
|
|
833
|
+
}
|
|
834
|
+
function resolvePackageRoot(startDir) {
|
|
835
|
+
const packageRoot = findAncestor(startDir, (dir) => readPackageJsonOrUndefined(dir)?.name === '@mpgd/cli');
|
|
836
|
+
if (packageRoot === undefined) {
|
|
837
|
+
throw new Error(`Could not find @mpgd/cli package root from ${startDir}`);
|
|
838
|
+
}
|
|
839
|
+
return packageRoot;
|
|
840
|
+
}
|
|
841
|
+
function resolveDefaultKitRoot() {
|
|
842
|
+
const envKitPath = process.env.MPGD_KIT_PATH;
|
|
843
|
+
if (envKitPath !== undefined && envKitPath.length > 0) {
|
|
844
|
+
const resolved = path.resolve(envKitPath);
|
|
845
|
+
if (isKitRoot(resolved)) {
|
|
846
|
+
return resolved;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return findKitRoot(packageRoot) ?? findKitRoot(process.cwd());
|
|
850
|
+
}
|
|
851
|
+
function findKitRoot(startDir) {
|
|
852
|
+
return findAncestor(startDir, isKitRoot);
|
|
853
|
+
}
|
|
854
|
+
function assertKitRoot(kitPath) {
|
|
855
|
+
if (!isKitRoot(kitPath, { strictParse: true })) {
|
|
856
|
+
throw new Error(`Expected an mpgd-kit checkout at ${kitPath}. Pass a directory containing the mpgd-kit root package.json and build:target script.`);
|
|
857
|
+
}
|
|
858
|
+
return kitPath;
|
|
859
|
+
}
|
|
860
|
+
function isKitRoot(dir, options = {}) {
|
|
861
|
+
const packageJson = readPackageJsonOrUndefined(dir, options);
|
|
862
|
+
return packageJson?.name === 'mpgd-kit' && packageJson.scripts?.['build:target'] !== undefined;
|
|
863
|
+
}
|
|
864
|
+
function kitFileStatus(relativePath) {
|
|
865
|
+
if (detectedKitRoot === undefined) {
|
|
866
|
+
return 'unknown';
|
|
867
|
+
}
|
|
868
|
+
return existsSync(path.join(detectedKitRoot, relativePath)) ? 'ok' : 'missing';
|
|
869
|
+
}
|
|
870
|
+
function readPackageVersion(packageRoot) {
|
|
871
|
+
return readPackageJsonOrUndefined(packageRoot)?.version ?? '0.0.0';
|
|
872
|
+
}
|
|
873
|
+
function readPackageJsonOrUndefined(dir, options = {}) {
|
|
874
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
875
|
+
if (!existsSync(packageJsonPath)) {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
return JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (options.strictParse === true) {
|
|
883
|
+
throw new Error(`Failed to parse ${packageJsonPath}: ${formatError(error)}`);
|
|
884
|
+
}
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function findAncestor(startDir, predicate) {
|
|
889
|
+
let currentDir = path.resolve(startDir);
|
|
890
|
+
while (true) {
|
|
891
|
+
if (predicate(currentDir)) {
|
|
892
|
+
return currentDir;
|
|
893
|
+
}
|
|
894
|
+
const parentDir = path.dirname(currentDir);
|
|
895
|
+
if (parentDir === currentDir) {
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
898
|
+
currentDir = parentDir;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function listTemplateCompletionItems() {
|
|
902
|
+
return [
|
|
903
|
+
{
|
|
904
|
+
value: 'create',
|
|
905
|
+
description: 'Create a Phaser starter',
|
|
906
|
+
},
|
|
907
|
+
];
|
|
908
|
+
}
|
|
909
|
+
function listTargetCompletionItems() {
|
|
910
|
+
return [...supportedBuildTargets].map((target) => ({
|
|
911
|
+
value: target,
|
|
912
|
+
description: `Target ${target}`,
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
function readCliLocale() {
|
|
916
|
+
const rawLocale = process.env.MPGD_LOCALE ?? process.env.LANG ?? 'en-US';
|
|
917
|
+
const normalized = rawLocale.split('.')[0]?.replace('_', '-');
|
|
918
|
+
if (normalized === undefined || normalized.length === 0) {
|
|
919
|
+
return 'en-US';
|
|
920
|
+
}
|
|
921
|
+
try {
|
|
922
|
+
return new Intl.Locale(normalized).toString();
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
return 'en-US';
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function pickLocalizedText(locale, text) {
|
|
929
|
+
return locale.language === 'ko' ? text.ko : text.en;
|
|
930
|
+
}
|