@mpgd/cli 0.35.0 → 0.36.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.
Files changed (53) hide show
  1. package/dist/android-bundle-signer.d.ts +2 -0
  2. package/dist/android-bundle-signer.js +38 -0
  3. package/dist/android-signing-session.d.ts +22 -0
  4. package/dist/android-signing-session.js +198 -0
  5. package/dist/artifact-inspection-environment.d.ts +2 -0
  6. package/dist/artifact-inspection-environment.js +31 -0
  7. package/dist/asc-pin.json +26 -0
  8. package/dist/capacitor-shell-starter.d.ts +22 -0
  9. package/dist/capacitor-shell-starter.js +130 -7
  10. package/dist/deploy-planning.d.ts +64 -0
  11. package/dist/deploy-planning.js +471 -0
  12. package/dist/deploy-process.d.ts +28 -0
  13. package/dist/deploy-process.js +197 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.js +409 -10
  16. package/dist/ios-ipa-inspection.js +151 -0
  17. package/dist/ios-keychain-import.swift +92 -0
  18. package/dist/ios-plist.d.ts +2 -0
  19. package/dist/ios-plist.js +75 -0
  20. package/dist/ios-signing-profile.d.ts +12 -0
  21. package/dist/ios-signing-profile.js +60 -0
  22. package/dist/ios-signing-session.d.ts +22 -0
  23. package/dist/ios-signing-session.js +256 -0
  24. package/dist/kit-upgrade-target-readiness.d.ts +8 -0
  25. package/dist/kit-upgrade-target-readiness.js +119 -0
  26. package/dist/kit-upgrade.d.ts +2 -0
  27. package/dist/kit-upgrade.js +3 -0
  28. package/dist/native-build-info.json +5 -0
  29. package/dist/native-build-target.js +12121 -0
  30. package/dist/native-deploy-run.d.ts +35 -0
  31. package/dist/native-deploy-run.js +462 -0
  32. package/dist/native-deploy-submission.d.ts +40 -0
  33. package/dist/native-deploy-submission.js +244 -0
  34. package/dist/native-deploy-workflow.d.ts +11 -0
  35. package/dist/native-deploy-workflow.js +328 -0
  36. package/dist/play-internal-submission.d.ts +32 -0
  37. package/dist/play-internal-submission.js +247 -0
  38. package/dist/play-publisher-port.d.ts +30 -0
  39. package/dist/play-publisher-port.js +1 -0
  40. package/dist/play-sdk-adapter.js +78 -0
  41. package/dist/release-state.d.ts +140 -0
  42. package/dist/release-state.js +1046 -0
  43. package/dist/release-workspace.d.ts +61 -0
  44. package/dist/release-workspace.js +504 -0
  45. package/dist/testflight-submission.d.ts +49 -0
  46. package/dist/testflight-submission.js +462 -0
  47. package/package.json +18 -13
  48. package/templates/phaser-game/README.md +30 -8
  49. package/templates/phaser-game/apps/target-devvit/README.md +12 -0
  50. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +513 -0
  51. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +8 -291
  52. package/templates/phaser-game/src/main.ts +34 -33
  53. package/templates/phaser-game/vite.shared.ts +5 -1
@@ -0,0 +1,471 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { isDeepStrictEqual } from 'node:util';
6
+ import { assertCapacitorAppId } from './capacitor-shell-starter.js';
7
+ const deploymentConfigName = 'mpgd.deploy.json';
8
+ const targetsConfigName = 'mpgd.targets.json';
9
+ const environmentName = /^[A-Z_][A-Z0-9_]*$/u;
10
+ const ascGroupIdPattern = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
11
+ export function initializeDeployConfig(game) {
12
+ const gameRoot = resolve(game);
13
+ const targets = readNativeTargets(gameRoot);
14
+ const profiles = {
15
+ beta: {
16
+ buildProfile: 'production',
17
+ approval: 'manual',
18
+ targets: {
19
+ ...(targets.android === undefined ? {} : {
20
+ android: {
21
+ destination: 'play-internal',
22
+ signingCredential: { env: 'MPGD_ANDROID_UPLOAD_KEYSTORE' },
23
+ submissionCredential: { env: 'MPGD_GOOGLE_PLAY_SERVICE_ACCOUNT' },
24
+ },
25
+ }),
26
+ ...(targets.ios === undefined ? {} : {
27
+ ios: {
28
+ destination: 'testflight',
29
+ signingCredential: { env: 'MPGD_IOS_SIGNING_P12' },
30
+ submissionCredential: { env: 'MPGD_ASC_API_KEY' },
31
+ },
32
+ }),
33
+ },
34
+ },
35
+ };
36
+ if (targets.android === undefined && targets.ios === undefined) {
37
+ throw new Error('Deploy init requires an Android or iOS target in mpgd.targets.json.');
38
+ }
39
+ const file = join(gameRoot, deploymentConfigName);
40
+ writeFileSync(file, `${JSON.stringify({ schemaVersion: 1, profiles }, null, 2)}\n`, {
41
+ flag: 'wx',
42
+ mode: 0o600,
43
+ });
44
+ return file;
45
+ }
46
+ export function planNativeDeployment(input) {
47
+ const gameRoot = resolve(input.game);
48
+ const deployFile = join(gameRoot, deploymentConfigName);
49
+ const targetFile = join(gameRoot, targetsConfigName);
50
+ const deployBytes = readFileSync(deployFile);
51
+ const targetBytes = readFileSync(targetFile);
52
+ const config = readDeployConfig(deployFile, deployBytes.toString('utf8'));
53
+ if (!Object.hasOwn(config.profiles, input.profile)) {
54
+ throw new Error(`Unknown deployment profile: ${input.profile}`);
55
+ }
56
+ const profile = config.profiles[input.profile];
57
+ if (profile === undefined) {
58
+ throw new Error(`Unknown deployment profile: ${input.profile}`);
59
+ }
60
+ const nativeTargets = readNativeTargets(gameRoot, targetBytes.toString('utf8'));
61
+ const selected = input.targets ?? Object.keys(profile.targets);
62
+ if (selected.length === 0 || new Set(selected).size !== selected.length) {
63
+ throw new Error('Select at least one unique Android or iOS deployment target.');
64
+ }
65
+ const targets = selected.map((target) => {
66
+ if (target !== 'android' && target !== 'ios') {
67
+ throw new Error(`Unsupported deployment target: ${target}`);
68
+ }
69
+ const targetProfile = profile.targets[target];
70
+ const targetConfig = nativeTargets[target];
71
+ if (targetProfile === undefined || targetConfig === undefined) {
72
+ throw new Error(`Deployment profile ${input.profile} does not configure ${target}.`);
73
+ }
74
+ assertGameOwnedNativePaths(gameRoot, target, targetConfig);
75
+ const appId = target === 'android'
76
+ ? targetConfig.metadata?.packageId
77
+ : targetConfig.metadata?.bundleId;
78
+ if (appId === undefined || appId.trim() === '') {
79
+ throw new Error(`${target} must have its app ID in mpgd.targets.json metadata.`);
80
+ }
81
+ assertCapacitorAppId(appId);
82
+ if (target === 'ios' && targetProfile.testGroup === undefined) {
83
+ throw new Error('TestFlight deployment requires a testGroup in mpgd.deploy.json.');
84
+ }
85
+ if (target === 'ios' && !ascGroupIdPattern.test(targetProfile.testGroup ?? '')) {
86
+ throw new Error('TestFlight testGroup must be an internal group ID, not a name.');
87
+ }
88
+ return {
89
+ target,
90
+ destination: targetProfile.destination,
91
+ appId,
92
+ ...(targetProfile.testGroup === undefined ? {} : { testGroup: targetProfile.testGroup }),
93
+ };
94
+ });
95
+ return {
96
+ schemaVersion: 1,
97
+ gameRoot: realpathSync(gameRoot),
98
+ profile: input.profile,
99
+ buildProfile: profile.buildProfile,
100
+ approval: profile.approval,
101
+ targetConfigSha256: createHash('sha256').update(targetBytes).digest('hex'),
102
+ deployConfigSha256: createHash('sha256').update(deployBytes).digest('hex'),
103
+ targets,
104
+ };
105
+ }
106
+ export function writeNativeDeploymentPlan(file, plan) {
107
+ writeFileSync(resolve(file), `${JSON.stringify(plan, null, 2)}\n`, {
108
+ flag: 'wx',
109
+ mode: 0o600,
110
+ });
111
+ }
112
+ /** Load a saved plan only if it still equals the game-owned current configuration. */
113
+ export function readNativeDeploymentPlan(file, gameRootOverride) {
114
+ const raw = parseObject(readFileSync(resolve(file), 'utf8'), file);
115
+ if (raw.schemaVersion !== 1 || typeof raw.gameRoot !== 'string'
116
+ || !isAbsolute(raw.gameRoot) || typeof raw.profile !== 'string'
117
+ || !Array.isArray(raw.targets) || raw.targets.some((entry) => !isObject(entry)
118
+ || (entry.target !== 'android' && entry.target !== 'ios'))
119
+ || !hasOnlyKeys(raw, [
120
+ 'schemaVersion', 'gameRoot', 'profile', 'buildProfile', 'approval',
121
+ 'targetConfigSha256', 'deployConfigSha256', 'targets',
122
+ ])) {
123
+ throw new Error('Saved native deployment plan is malformed.');
124
+ }
125
+ const plan = planNativeDeployment({
126
+ game: gameRootOverride ?? raw.gameRoot,
127
+ profile: raw.profile,
128
+ targets: raw.targets.map((entry) => entry.target),
129
+ });
130
+ if (!isDeepStrictEqual({ ...raw, gameRoot: plan.gameRoot }, plan)) {
131
+ throw new Error('Saved native deployment plan differs from current game configuration.');
132
+ }
133
+ return plan;
134
+ }
135
+ /** Resolve credential references from the one authoritative game-owned profile. */
136
+ export function readNativeDeployTargetProfile(plan, target) {
137
+ const current = planNativeDeployment({
138
+ game: plan.gameRoot,
139
+ profile: plan.profile,
140
+ targets: plan.targets.map((entry) => entry.target),
141
+ });
142
+ if (!isDeepStrictEqual(current, plan)) {
143
+ throw new Error('Native deployment configuration changed after the plan was written.');
144
+ }
145
+ const deployFile = join(plan.gameRoot, deploymentConfigName);
146
+ const deployBytes = readFileSync(deployFile);
147
+ if (createHash('sha256').update(deployBytes).digest('hex') !== plan.deployConfigSha256) {
148
+ throw new Error('Native deployment configuration changed after the plan was written.');
149
+ }
150
+ const config = readDeployConfig(deployFile, deployBytes.toString('utf8'));
151
+ const profile = config.profiles[plan.profile]?.targets[target];
152
+ if (profile === undefined || !plan.targets.some((entry) => entry.target === target)) {
153
+ throw new Error(`Native deployment plan has no ${target} target profile.`);
154
+ }
155
+ return profile;
156
+ }
157
+ /** Credential names across every configured profile, including unselected targets. */
158
+ export function readNativeDeployCredentialNames(plan) {
159
+ const deployFile = join(plan.gameRoot, deploymentConfigName);
160
+ const deployBytes = readFileSync(deployFile);
161
+ if (createHash('sha256').update(deployBytes).digest('hex') !== plan.deployConfigSha256) {
162
+ throw new Error('Native deployment configuration changed after the plan was written.');
163
+ }
164
+ const config = readDeployConfig(deployFile, deployBytes.toString('utf8'));
165
+ const names = [];
166
+ for (const profile of Object.values(config.profiles)) {
167
+ for (const entry of Object.values(profile.targets)) {
168
+ if (entry !== undefined) {
169
+ names.push(entry.signingCredential.env, entry.submissionCredential.env);
170
+ }
171
+ }
172
+ }
173
+ return names;
174
+ }
175
+ export function doctorNativeDeployment(input) {
176
+ const environment = input.environment ?? process.env;
177
+ const checks = [];
178
+ let plan;
179
+ try {
180
+ plan = planNativeDeployment(input);
181
+ checks.push({ name: 'deployment plan', status: 'ok', detail: 'Configuration is valid.' });
182
+ }
183
+ catch (error) {
184
+ checks.push({
185
+ name: 'deployment plan',
186
+ status: 'missing',
187
+ detail: error instanceof Error ? error.message : String(error),
188
+ });
189
+ return { checks, healthy: false };
190
+ }
191
+ const config = readDeployConfig(join(resolve(input.game), deploymentConfigName));
192
+ const profile = config.profiles[input.profile];
193
+ if (profile === undefined) {
194
+ throw new Error(`Unknown deployment profile: ${input.profile}`);
195
+ }
196
+ checks.push({
197
+ name: 'Node.js',
198
+ status: Number(process.versions.node.split('.')[0]) >= 24 ? 'ok' : 'missing',
199
+ detail: `Running Node.js ${process.versions.node}; version 24 or later is required.`,
200
+ });
201
+ for (const target of plan.targets) {
202
+ const entry = profile.targets[target.target];
203
+ if (entry === undefined) {
204
+ throw new Error(`Missing deployment profile target: ${target.target}`);
205
+ }
206
+ if (target.target === 'android') {
207
+ checks.push(checkSelectedJdk(environment));
208
+ const sdk = environment.ANDROID_HOME ?? environment.ANDROID_SDK_ROOT;
209
+ const conflictingSdkRoots = environment.ANDROID_HOME !== undefined
210
+ && environment.ANDROID_SDK_ROOT !== undefined
211
+ && canonicalSdkRoot(environment.ANDROID_HOME)
212
+ !== canonicalSdkRoot(environment.ANDROID_SDK_ROOT);
213
+ checks.push({
214
+ name: 'Android SDK',
215
+ status: !conflictingSdkRoots && sdk !== undefined
216
+ && existsSync(join(sdk, 'platform-tools'))
217
+ && hasDirectoryEntries(join(sdk, 'platforms'))
218
+ && hasDirectoryEntries(join(sdk, 'build-tools')) ? 'ok' : 'missing',
219
+ detail: conflictingSdkRoots
220
+ ? 'ANDROID_HOME and ANDROID_SDK_ROOT point to different directories.'
221
+ : sdk === undefined
222
+ ? 'Set ANDROID_HOME or ANDROID_SDK_ROOT.'
223
+ : `SDK path: ${sdk}; platform-tools, platforms and build-tools are required.`,
224
+ });
225
+ }
226
+ else {
227
+ checks.push(process.platform === 'darwin'
228
+ ? commandCheck('Xcode', 'xcodebuild', ['-version'])
229
+ : { name: 'Xcode', status: 'missing', detail: 'iOS builds require macOS and Xcode.' });
230
+ }
231
+ for (const [purpose, credential] of [
232
+ ['signing', entry.signingCredential],
233
+ ['submission', entry.submissionCredential],
234
+ ]) {
235
+ checks.push({
236
+ name: `${target.target} ${purpose} credential`,
237
+ status: environment[credential.env] ? 'ok' : 'missing',
238
+ detail: environment[credential.env]
239
+ ? `Environment reference ${credential.env} is present; content is not validated.`
240
+ : `Set the ${credential.env} environment reference.`,
241
+ });
242
+ }
243
+ const additional = target.target === 'android'
244
+ ? [
245
+ 'MPGD_ANDROID_UPLOAD_STORE_PASSWORD',
246
+ 'MPGD_ANDROID_UPLOAD_KEY_ALIAS',
247
+ 'MPGD_ANDROID_UPLOAD_KEY_PASSWORD',
248
+ 'MPGD_ANDROID_UPLOAD_CERT_SHA256',
249
+ ]
250
+ : [
251
+ 'MPGD_IOS_SIGNING_P12_PASSWORD',
252
+ 'MPGD_IOS_PROVISIONING_PROFILE',
253
+ 'MPGD_IOS_TEAM_ID',
254
+ 'MPGD_ASC_BINARY',
255
+ 'MPGD_ASC_APP_ID',
256
+ 'MPGD_ASC_KEY_ID',
257
+ 'MPGD_ASC_ISSUER_ID',
258
+ ];
259
+ for (const name of additional) {
260
+ checks.push({
261
+ name: `${target.target} ${name}`,
262
+ status: environment[name]?.trim() ? 'ok' : 'missing',
263
+ detail: environment[name]?.trim()
264
+ ? `${name} is present; content is not validated.`
265
+ : `Set ${name} for native test deployment.`,
266
+ });
267
+ }
268
+ }
269
+ return { checks, healthy: checks.every((check) => check.status !== 'missing') };
270
+ }
271
+ export function parseDeployTargets(value) {
272
+ if (value === undefined) {
273
+ return undefined;
274
+ }
275
+ const targets = value.split(',').map((target) => target.trim());
276
+ if (targets.length === 0 || targets.some((target) => target !== 'android' && target !== 'ios')) {
277
+ throw new Error('--targets must be android, ios, or android,ios.');
278
+ }
279
+ if (new Set(targets).size !== targets.length) {
280
+ throw new Error('--targets must not contain duplicates.');
281
+ }
282
+ return targets;
283
+ }
284
+ function readDeployConfig(file, text) {
285
+ const raw = text === undefined ? readObject(file) : parseObject(text, file);
286
+ if (raw.schemaVersion !== 1 || !isObject(raw.profiles)
287
+ || !hasOnlyKeys(raw, ['schemaVersion', 'profiles'])) {
288
+ throw new Error('mpgd.deploy.json must use schemaVersion 1 and contain profiles.');
289
+ }
290
+ for (const [name, value] of Object.entries(raw.profiles)) {
291
+ if (!/^[a-z][a-z0-9-]*$/u.test(name) || !isObject(value)
292
+ || value.buildProfile !== 'production'
293
+ || (value.approval !== 'manual' && value.approval !== 'preapproved-internal-test')
294
+ || !isObject(value.targets)
295
+ || !hasOnlyKeys(value, ['buildProfile', 'approval', 'targets'])) {
296
+ throw new Error(`Invalid deployment profile: ${name}`);
297
+ }
298
+ for (const [target, entry] of Object.entries(value.targets)) {
299
+ const destination = target === 'android' ? 'play-internal' : 'testflight';
300
+ if ((target !== 'android' && target !== 'ios') || !isObject(entry)
301
+ || entry.destination !== destination
302
+ || !isCredentialReference(entry.signingCredential)
303
+ || !isCredentialReference(entry.submissionCredential)
304
+ || !hasOnlyKeys(entry, [
305
+ 'destination', 'signingCredential', 'submissionCredential', 'testGroup',
306
+ ])
307
+ || (entry.testGroup !== undefined
308
+ && (target !== 'ios' || typeof entry.testGroup !== 'string'
309
+ || entry.testGroup.trim() === ''))) {
310
+ throw new Error(`Invalid deployment target ${target} in profile ${name}.`);
311
+ }
312
+ }
313
+ }
314
+ return raw;
315
+ }
316
+ function readNativeTargets(gameRoot, text) {
317
+ const file = join(gameRoot, targetsConfigName);
318
+ const raw = text === undefined ? readObject(file) : parseObject(text, file);
319
+ if (!isObject(raw.targets)) {
320
+ throw new Error('mpgd.targets.json must contain a targets object.');
321
+ }
322
+ const result = {};
323
+ for (const [target, kind] of [
324
+ ['android', 'capacitor-android'],
325
+ ['ios', 'capacitor-ios'],
326
+ ]) {
327
+ const value = raw.targets[target];
328
+ if (value === undefined) {
329
+ continue;
330
+ }
331
+ if (!isObject(value) || value.kind !== kind
332
+ || value.adapter !== 'capacitor'
333
+ || value.artifact !== (target === 'android' ? 'aab' : 'ipa')
334
+ || typeof value.gameApp !== 'string'
335
+ || typeof value.shellApp !== 'string'
336
+ || typeof value.webDir !== 'string'
337
+ || (value.metadata !== undefined && !isObject(value.metadata))) {
338
+ throw new Error(`Invalid ${target} target in mpgd.targets.json.`);
339
+ }
340
+ if (isObject(value.metadata)) {
341
+ const appId = target === 'android' ? value.metadata.packageId : value.metadata.bundleId;
342
+ if (appId !== undefined && typeof appId !== 'string') {
343
+ throw new Error(`Invalid ${target} app ID in mpgd.targets.json metadata.`);
344
+ }
345
+ }
346
+ result[target] = value;
347
+ }
348
+ return result;
349
+ }
350
+ function assertGameOwnedNativePaths(gameRoot, target, config) {
351
+ const root = realpathSync(gameRoot);
352
+ const canonicalPaths = {};
353
+ for (const field of ['gameApp', 'shellApp', 'webDir']) {
354
+ const value = config[field];
355
+ if (value.includes('${MPGD_KIT_PATH}')) {
356
+ throw new Error(`${target} ${field} still points to a Kit checkout; initialize a game-owned shell.`);
357
+ }
358
+ const expanded = value
359
+ .replaceAll('${MPGD_GAME_ROOT}', root)
360
+ .replaceAll('${MPGD_GAME_APP_ROOT}', root);
361
+ const candidate = resolve(root, expanded);
362
+ let ancestor = candidate;
363
+ while (!existsSync(ancestor)) {
364
+ const parent = dirname(ancestor);
365
+ if (parent === ancestor) {
366
+ throw new Error(`Cannot resolve ${target} ${field}: ${value}`);
367
+ }
368
+ ancestor = parent;
369
+ }
370
+ const canonical = resolve(realpathSync(ancestor), relative(ancestor, candidate));
371
+ if (field !== 'gameApp') {
372
+ canonicalPaths[field] = canonical;
373
+ }
374
+ const fromRoot = relative(root, canonical);
375
+ if (fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
376
+ throw new Error(`${target} ${field} must stay inside the game project.`);
377
+ }
378
+ if (field === 'shellApp' && !existsSync(join(candidate, 'package.json'))) {
379
+ throw new Error(`${target} shellApp is missing its package.json; run mpgd target init capacitor.`);
380
+ }
381
+ }
382
+ const shell = canonicalPaths.shellApp;
383
+ const web = canonicalPaths.webDir;
384
+ if (shell === undefined || web === undefined) {
385
+ throw new Error(`${target} native shell paths are missing.`);
386
+ }
387
+ const webWithinShell = relative(shell, web);
388
+ if (webWithinShell === '' || webWithinShell === '..'
389
+ || webWithinShell.startsWith(`..${sep}`) || isAbsolute(webWithinShell)) {
390
+ throw new Error(`${target} webDir must be inside its shellApp.`);
391
+ }
392
+ }
393
+ function readObject(file) {
394
+ let text;
395
+ try {
396
+ text = readFileSync(file, 'utf8');
397
+ }
398
+ catch (error) {
399
+ throw new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
400
+ }
401
+ return parseObject(text, file);
402
+ }
403
+ function parseObject(text, file) {
404
+ let value;
405
+ try {
406
+ value = JSON.parse(text);
407
+ }
408
+ catch (error) {
409
+ throw new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
410
+ }
411
+ if (!isObject(value)) {
412
+ throw new Error(`Expected a JSON object in ${file}.`);
413
+ }
414
+ return value;
415
+ }
416
+ function isObject(value) {
417
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
418
+ }
419
+ function isCredentialReference(value) {
420
+ return isObject(value) && typeof value.env === 'string'
421
+ && environmentName.test(value.env) && hasOnlyKeys(value, ['env']);
422
+ }
423
+ function hasOnlyKeys(value, allowed) {
424
+ return Object.keys(value).every((key) => allowed.includes(key));
425
+ }
426
+ function hasDirectoryEntries(directory) {
427
+ try {
428
+ return readdirSync(directory).length > 0;
429
+ }
430
+ catch {
431
+ return false;
432
+ }
433
+ }
434
+ function canonicalSdkRoot(directory) {
435
+ let canonical;
436
+ try {
437
+ canonical = realpathSync(directory);
438
+ }
439
+ catch {
440
+ canonical = resolve(directory);
441
+ }
442
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
443
+ }
444
+ function checkSelectedJdk(environment) {
445
+ const javaHome = environment.JAVA_HOME?.trim();
446
+ const binaryName = process.platform === 'win32' ? '.exe' : '';
447
+ const java = javaHome ? join(javaHome, 'bin', `java${binaryName}`) : 'java';
448
+ const javac = javaHome ? join(javaHome, 'bin', `javac${binaryName}`) : 'javac';
449
+ const javaCheck = commandCheck('Java', java, ['-version']);
450
+ const javacCheck = commandCheck('Javac', javac, ['-version']);
451
+ return {
452
+ name: 'JDK',
453
+ status: javaCheck.status === 'ok' && javacCheck.status === 'ok' ? 'ok' : 'missing',
454
+ detail: javaHome === undefined || javaHome === ''
455
+ ? 'java and javac must be available on PATH.'
456
+ : `JAVA_HOME must contain usable bin/java and bin/javac: ${javaHome}`,
457
+ };
458
+ }
459
+ function commandCheck(name, command, args) {
460
+ const result = spawnSync(command, [...args], {
461
+ encoding: 'utf8',
462
+ timeout: 5_000,
463
+ maxBuffer: 64 * 1024,
464
+ stdio: ['ignore', 'pipe', 'pipe'],
465
+ });
466
+ return {
467
+ name,
468
+ status: result.status === 0 ? 'ok' : 'missing',
469
+ detail: result.status === 0 ? `${command} is available.` : `${command} is unavailable.`,
470
+ };
471
+ }
@@ -0,0 +1,28 @@
1
+ export interface ReleaseProcessInput {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ readonly cwd: string;
5
+ readonly environment?: NodeJS.ProcessEnv | undefined;
6
+ readonly timeoutMs: number;
7
+ readonly maxOutputBytes?: number | undefined;
8
+ /** Opt in only for machine parsing; never write this unredacted stdout to logs. */
9
+ readonly captureMachineStdout?: boolean | undefined;
10
+ /** Secret input is piped to stdin, never included in child arguments. */
11
+ readonly stdin?: string | undefined;
12
+ readonly secretValues?: readonly string[] | undefined;
13
+ readonly signal?: AbortSignal | undefined;
14
+ }
15
+ export interface ReleaseProcessResult {
16
+ readonly output: string;
17
+ readonly truncated: boolean;
18
+ /** Present only when requested and successful; may contain secrets. */
19
+ readonly machineStdout?: string;
20
+ }
21
+ export declare class ReleaseProcessError extends Error {
22
+ readonly reason: 'abort' | 'exit' | 'spawn' | 'timeout';
23
+ readonly output: string;
24
+ readonly exitCode?: number;
25
+ constructor(reason: ReleaseProcessError['reason'], output: string, exitCode?: number);
26
+ }
27
+ /** Execute a release step without streaming unredacted child output to logs. */
28
+ export declare function runReleaseProcess(input: ReleaseProcessInput): Promise<ReleaseProcessResult>;