@mpgd/cli 0.38.0 → 0.40.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/asset-pack-build.js +4 -1
- package/dist/asset-pack-native-audit.d.ts +5 -0
- package/dist/asset-pack-native-audit.js +137 -0
- package/dist/asset-pack-publish.d.ts +43 -0
- package/dist/asset-pack-publish.js +309 -0
- package/dist/asset-pack-target.d.ts +42 -0
- package/dist/asset-pack-target.js +251 -0
- package/dist/build-targets.d.ts +1 -1
- package/dist/build-targets.js +2 -0
- package/dist/index.js +85 -3
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +3501 -633
- package/package.json +19 -14
- package/templates/phaser-game/agent/acceptance.md +6 -0
- package/templates/phaser-game/agent/brief.md +4 -0
- package/templates/phaser-game/agent/game-manifest.json +21 -0
- package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +6 -0
- package/templates/phaser-game/mpgd.targets.json +11 -0
- package/templates/phaser-game/src/main.ts +2 -0
- package/templates/phaser-game/src/platform/buildGateways/oneplay.ts +32 -0
- package/templates/phaser-game/src/platform/runtimeDetector.ts +1 -0
- package/templates/phaser-game/src/runtime/createGame.ts +3 -3
- package/templates/phaser-game/src/scenes/BootScene.ts +7 -1
- package/templates/phaser-game/src/scenes/PlayScene.ts +16 -0
- package/templates/phaser-game/vite.runtime-target-config.ts +2 -0
- package/templates/phaser-game/vite.shared.ts +2 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { copyFile, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, writeFile, } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { validatePhaserPackBuildConfig, validatePhaserPackDeliveryManifest, } from '@mpgd/phaser-assets/pack-format';
|
|
7
|
+
import { assertAssetPackTargetPolicy, resolveAssetPackLocations, } from '@mpgd/target-config/asset-packs';
|
|
8
|
+
import { buildAssetPacks } from './asset-pack-build.js';
|
|
9
|
+
import { verifyAssetPackDelivery } from './asset-pack-verify.js';
|
|
10
|
+
export const assetPackTargetNamespace = 'mpgd-asset-packs';
|
|
11
|
+
export const assetPackTargetManifestName = 'asset-pack-delivery.json';
|
|
12
|
+
export const assetPackTargetPolicyName = 'target-policy.json';
|
|
13
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
async function fileDigest(path) {
|
|
15
|
+
const hash = createHash('sha256');
|
|
16
|
+
for await (const chunk of createReadStream(path)) {
|
|
17
|
+
hash.update(chunk);
|
|
18
|
+
}
|
|
19
|
+
return hash.digest('hex');
|
|
20
|
+
}
|
|
21
|
+
function inside(parent, child) {
|
|
22
|
+
const step = relative(parent, child);
|
|
23
|
+
return step === '' || (!isAbsolute(step) && step !== '..' && !step.startsWith(`..${sep}`));
|
|
24
|
+
}
|
|
25
|
+
async function noSymlink(path) {
|
|
26
|
+
let current = resolve(path);
|
|
27
|
+
while (true) {
|
|
28
|
+
const stat = await lstat(current).catch((error) => {
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
});
|
|
34
|
+
if (stat?.isSymbolicLink()) {
|
|
35
|
+
// macOS exposes its system temporary directories through these fixed
|
|
36
|
+
// aliases. Accept only their canonical OS targets, never user links.
|
|
37
|
+
const systemAlias = process.platform === 'darwin'
|
|
38
|
+
&& ((current === '/var' && await realpath(current) === '/private/var')
|
|
39
|
+
|| (current === '/tmp' && await realpath(current) === '/private/tmp'));
|
|
40
|
+
if (!systemAlias) {
|
|
41
|
+
throw new Error(`Asset target path must not traverse a symbolic link: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const parent = dirname(current);
|
|
45
|
+
if (parent === current) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function canonicalOutput(path) {
|
|
52
|
+
let ancestor = path;
|
|
53
|
+
while (true) {
|
|
54
|
+
try {
|
|
55
|
+
const canonical = await realpath(ancestor);
|
|
56
|
+
return resolve(canonical, relative(ancestor, path));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== 'ENOENT') {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(ancestor);
|
|
63
|
+
if (parent === ancestor) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
ancestor = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Stage verified immutable objects into disjoint packaged/remote roots.
|
|
71
|
+
* The output must not exist: a failed attempt never replaces a deployment. */
|
|
72
|
+
export async function stageAssetPackTarget(options) {
|
|
73
|
+
await noSymlink(options.root);
|
|
74
|
+
await noSymlink(options.manifestPath);
|
|
75
|
+
const root = await realpath(resolve(options.root));
|
|
76
|
+
await noSymlink(options.outDir);
|
|
77
|
+
const out = await canonicalOutput(resolve(options.outDir));
|
|
78
|
+
if (inside(root, out) || inside(out, root)) {
|
|
79
|
+
throw new Error('Asset target output must be disjoint from the delivery source root');
|
|
80
|
+
}
|
|
81
|
+
if (await lstat(out).catch((error) => {
|
|
82
|
+
if (error.code === 'ENOENT') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
})) {
|
|
87
|
+
throw new Error('Asset target output already exists; choose a fresh deployment directory');
|
|
88
|
+
}
|
|
89
|
+
const verified = await verifyAssetPackDelivery({
|
|
90
|
+
manifestPath: options.manifestPath,
|
|
91
|
+
root,
|
|
92
|
+
hostLimits: { maxFiles: 100_000, maxTotalBytes: 1024 ** 3 },
|
|
93
|
+
});
|
|
94
|
+
if (!verified.ok) {
|
|
95
|
+
throw new Error(`Asset target source verification failed: ${verified.failures.map((failure) => failure.code).join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
const manifestBytes = await readFile(options.manifestPath);
|
|
98
|
+
if (digest(manifestBytes) !== verified.manifest.sha256) {
|
|
99
|
+
throw new Error('Delivery manifest changed during target staging');
|
|
100
|
+
}
|
|
101
|
+
const manifest = validatePhaserPackDeliveryManifest(JSON.parse(manifestBytes.toString('utf8')));
|
|
102
|
+
const locations = resolveAssetPackLocations(options.policy, manifest.packs);
|
|
103
|
+
const runtimePolicy = {
|
|
104
|
+
defaultLocation: 'packaged',
|
|
105
|
+
packs: locations,
|
|
106
|
+
...(options.policy.offlineRequired === undefined ? {} : { offlineRequired: [...options.policy.offlineRequired] }),
|
|
107
|
+
...(options.policy.remoteBaseUrl === undefined ? {} : { remoteBaseUrl: options.policy.remoteBaseUrl }),
|
|
108
|
+
...(options.policy.maxPackagedBytes === undefined ? {} : { maxPackagedBytes: options.policy.maxPackagedBytes }),
|
|
109
|
+
};
|
|
110
|
+
const policyBytes = Buffer.from(JSON.stringify(runtimePolicy, null, 2) + '\n');
|
|
111
|
+
const objects = new Map();
|
|
112
|
+
for (const pack of manifest.packs) {
|
|
113
|
+
const files = pack.delivery === 'zip'
|
|
114
|
+
? [pack.archive]
|
|
115
|
+
: pack.assets.flatMap((asset) => asset.files);
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const location = locations[pack.packId];
|
|
118
|
+
objects.set(`${location}:${file.path}`, {
|
|
119
|
+
path: file.path,
|
|
120
|
+
bytes: file.bytes,
|
|
121
|
+
sha256: file.sha256,
|
|
122
|
+
location,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const planned = [...objects.values()].sort((a, b) => a.location.localeCompare(b.location) || a.path.localeCompare(b.path));
|
|
127
|
+
const packagedAssetBytes = planned.filter((object) => object.location === 'packaged').reduce((sum, object) => sum + object.bytes, 0);
|
|
128
|
+
const packagedBytes = packagedAssetBytes + manifestBytes.length + policyBytes.length;
|
|
129
|
+
if (options.policy.maxPackagedBytes !== undefined && packagedBytes > options.policy.maxPackagedBytes) {
|
|
130
|
+
throw new Error(`Packaged asset namespace exceeds its budget: ${packagedBytes} > ${options.policy.maxPackagedBytes}`);
|
|
131
|
+
}
|
|
132
|
+
await mkdir(dirname(out), { recursive: true });
|
|
133
|
+
const temporary = await mkdtemp(join(dirname(out), '.mpgd-asset-target-'));
|
|
134
|
+
try {
|
|
135
|
+
await mkdir(join(temporary, 'packaged'));
|
|
136
|
+
await mkdir(join(temporary, 'remote'));
|
|
137
|
+
for (const object of planned) {
|
|
138
|
+
const source = join(root, object.path);
|
|
139
|
+
await noSymlink(source);
|
|
140
|
+
const destination = join(temporary, object.location, object.path);
|
|
141
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
142
|
+
await copyFile(source, destination);
|
|
143
|
+
const stat = await lstat(destination);
|
|
144
|
+
if (stat.size !== object.bytes || await fileDigest(destination) !== object.sha256) {
|
|
145
|
+
throw new Error(`Asset object changed during target staging: ${object.path}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
await writeFile(join(temporary, 'packaged', assetPackTargetManifestName), manifestBytes);
|
|
149
|
+
await writeFile(join(temporary, 'packaged', assetPackTargetPolicyName), policyBytes);
|
|
150
|
+
await rename(temporary, out);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await rm(temporary, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
outDir: out,
|
|
157
|
+
packagedDir: join(out, 'packaged'),
|
|
158
|
+
remoteDir: join(out, 'remote'),
|
|
159
|
+
manifestSha256: verified.manifest.sha256,
|
|
160
|
+
locations,
|
|
161
|
+
packagedAssetBytes,
|
|
162
|
+
packagedBytes,
|
|
163
|
+
remoteBytes: planned.filter((object) => object.location === 'remote').reduce((sum, object) => sum + object.bytes, 0),
|
|
164
|
+
objects: planned,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Build from one target's policy without requiring a kit checkout. */
|
|
168
|
+
export async function assertAssetPackTargetOutput(options) {
|
|
169
|
+
assertAssetPackTargetPolicy(options.policy);
|
|
170
|
+
const configPath = resolve(options.baseDir, options.policy.buildConfig);
|
|
171
|
+
const config = validatePhaserPackBuildConfig(JSON.parse(await readFile(configPath, 'utf8')));
|
|
172
|
+
const sourceRoot = await realpath(resolve(dirname(configPath), config.root));
|
|
173
|
+
await noSymlink(options.outDir);
|
|
174
|
+
const output = await canonicalOutput(resolve(options.outDir));
|
|
175
|
+
if (inside(sourceRoot, output) || inside(output, sourceRoot)) {
|
|
176
|
+
throw new Error('Asset target output must be disjoint from the original asset source root');
|
|
177
|
+
}
|
|
178
|
+
if (inside(output, await realpath(configPath))) {
|
|
179
|
+
throw new Error('Asset target output must not replace its build config');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export async function buildAssetPackTarget(options) {
|
|
183
|
+
await assertAssetPackTargetOutput(options);
|
|
184
|
+
const configPath = resolve(options.baseDir, options.policy.buildConfig);
|
|
185
|
+
const temporary = await mkdtemp(join(tmpdir(), 'mpgd-asset-source-'));
|
|
186
|
+
try {
|
|
187
|
+
const built = buildAssetPacks({
|
|
188
|
+
configPath,
|
|
189
|
+
outDir: join(temporary, 'delivery'),
|
|
190
|
+
});
|
|
191
|
+
return await stageAssetPackTarget({
|
|
192
|
+
manifestPath: built.manifestPath,
|
|
193
|
+
root: built.outDir,
|
|
194
|
+
policy: options.policy,
|
|
195
|
+
outDir: options.outDir,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await rm(temporary, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Verify the real bundle's asset namespace and reject byte-identical remote
|
|
203
|
+
* objects copied elsewhere. This does not inspect transformed/embedded data. */
|
|
204
|
+
export async function assertAssetPackTargetArtifact(report, artifactRoot, namespacePath = assetPackTargetNamespace) {
|
|
205
|
+
const root = resolve(artifactRoot);
|
|
206
|
+
const namespace = resolve(root, namespacePath);
|
|
207
|
+
if (!inside(root, namespace) || namespace === root) {
|
|
208
|
+
throw new Error('Asset namespace must be inside the target artifact');
|
|
209
|
+
}
|
|
210
|
+
const localHashes = new Set(report.objects.filter((object) => object.location === 'packaged').map((object) => object.sha256));
|
|
211
|
+
const remoteHashes = new Set(report.objects.filter((object) => object.location === 'remote' && !localHashes.has(object.sha256)).map((object) => object.sha256));
|
|
212
|
+
let count = 0;
|
|
213
|
+
let namespaceBytes = 0;
|
|
214
|
+
const pending = [root];
|
|
215
|
+
while (pending.length) {
|
|
216
|
+
const directory = pending.pop();
|
|
217
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
218
|
+
const path = join(directory, entry.name);
|
|
219
|
+
if (++count > 100_000 || entry.isSymbolicLink()) {
|
|
220
|
+
throw new Error('Asset artifact inventory exceeds its limit or contains a symbolic link');
|
|
221
|
+
}
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
pending.push(path);
|
|
224
|
+
}
|
|
225
|
+
else if (entry.isFile()) {
|
|
226
|
+
if (inside(namespace, path)) {
|
|
227
|
+
namespaceBytes += (await lstat(path)).size;
|
|
228
|
+
}
|
|
229
|
+
if (remoteHashes.has(await fileDigest(path))) {
|
|
230
|
+
throw new Error(`Remote-only asset object is present in the packaged artifact: ${relative(root, path)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
throw new Error('Asset artifact contains a non-regular entry');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (namespaceBytes !== report.packagedBytes) {
|
|
239
|
+
throw new Error('Packaged asset namespace has extra, missing or modified bytes');
|
|
240
|
+
}
|
|
241
|
+
for (const name of [assetPackTargetManifestName, assetPackTargetPolicyName]) {
|
|
242
|
+
if (await fileDigest(join(namespace, name)) !== await fileDigest(join(report.packagedDir, name))) {
|
|
243
|
+
throw new Error(`Packaged asset metadata changed: ${name}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const object of report.objects.filter((object) => object.location === 'packaged')) {
|
|
247
|
+
if (await fileDigest(join(namespace, object.path)) !== object.sha256) {
|
|
248
|
+
throw new Error(`Packaged asset object changed: ${object.path}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
package/dist/build-targets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type ConfiguredBuildTargets = Readonly<Record<string, unknown>>;
|
|
2
|
-
export declare const supportedBuildTargets: readonly ['browser', 'web', 'web-preview', 'microsoft-store', 'msstore', 'verse8', 'crazygames', 'android', 'ios', 'ait', 'devvit', 'reddit'];
|
|
2
|
+
export declare const supportedBuildTargets: readonly ['browser', 'web', 'web-preview', 'microsoft-store', 'msstore', 'verse8', 'crazygames', 'oneplay', 'android', 'ios', 'ait', 'devvit', 'reddit'];
|
|
3
3
|
export declare function normalizeBuildTarget(target: string, configuredTargets?: ConfiguredBuildTargets): string;
|
|
4
4
|
export declare function normalizeConfiguredBuildTargets(configuredTargets: ConfiguredBuildTargets): readonly string[];
|
package/dist/build-targets.js
CHANGED
|
@@ -7,6 +7,7 @@ export const supportedBuildTargets = [
|
|
|
7
7
|
'msstore',
|
|
8
8
|
'verse8',
|
|
9
9
|
'crazygames',
|
|
10
|
+
'oneplay',
|
|
10
11
|
'android',
|
|
11
12
|
'ios',
|
|
12
13
|
'ait',
|
|
@@ -18,6 +19,7 @@ const canonicalBuildTargets = new Set([
|
|
|
18
19
|
'microsoft-store',
|
|
19
20
|
'verse8',
|
|
20
21
|
'crazygames',
|
|
22
|
+
'oneplay',
|
|
21
23
|
'android',
|
|
22
24
|
'ios',
|
|
23
25
|
'ait',
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ import { cli } from 'gunshi';
|
|
|
9
9
|
import { assertPlatformVersionLedger } from '@mpgd/target-config';
|
|
10
10
|
import { buildAssetPacks } from './asset-pack-build.js';
|
|
11
11
|
import { verifyAssetPackDelivery } from './asset-pack-verify.js';
|
|
12
|
+
import { buildAssetPackTarget } from './asset-pack-target.js';
|
|
13
|
+
import { publishAssetPacks } from './asset-pack-publish.js';
|
|
14
|
+
import { assertAssetPackTargetPolicy } from '@mpgd/target-config/asset-packs';
|
|
12
15
|
import { materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
|
|
13
16
|
import { doctorNativeDeployment, initializeDeployConfig, parseDeployTargets, planNativeDeployment, readNativeDeploymentPlan, writeNativeDeploymentPlan, } from './deploy-planning.js';
|
|
14
17
|
import { initializeNativeDeployWorkflow } from './native-deploy-workflow.js';
|
|
@@ -83,6 +86,7 @@ const recommendedMatrixTargetOrder = [
|
|
|
83
86
|
'microsoft-store',
|
|
84
87
|
'verse8',
|
|
85
88
|
'crazygames',
|
|
89
|
+
'oneplay',
|
|
86
90
|
'ait',
|
|
87
91
|
'reddit',
|
|
88
92
|
];
|
|
@@ -91,6 +95,7 @@ const allMatrixTargetOrder = [
|
|
|
91
95
|
'microsoft-store',
|
|
92
96
|
'verse8',
|
|
93
97
|
'crazygames',
|
|
98
|
+
'oneplay',
|
|
94
99
|
'android',
|
|
95
100
|
'ios',
|
|
96
101
|
'ait',
|
|
@@ -144,7 +149,7 @@ export async function runMpgdCli(args) {
|
|
|
144
149
|
await cli([...args], entryCommand, {
|
|
145
150
|
name: 'mpgd',
|
|
146
151
|
version: cliVersion,
|
|
147
|
-
// Machine-readable
|
|
152
|
+
// Machine-readable asset reports and upgrade plans with --json
|
|
148
153
|
// owns stdout entirely — exactly one JSON document, parseable with
|
|
149
154
|
// JSON.parse(stdout) and nothing else. The framework's banner is
|
|
150
155
|
// suppressed for that command, and framework-level argument errors
|
|
@@ -153,7 +158,7 @@ export async function runMpgdCli(args) {
|
|
|
153
158
|
// partial document. Every other command keeps the default rendering.
|
|
154
159
|
renderHeader: async (ctx) => {
|
|
155
160
|
const values = ctx.values;
|
|
156
|
-
if ((ctx.name === 'verify-delivery' || ctx.name === 'upgrade') && values.json === true) {
|
|
161
|
+
if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade') && values.json === true) {
|
|
157
162
|
return '';
|
|
158
163
|
}
|
|
159
164
|
const title = ctx.env.description || ctx.env.name || '';
|
|
@@ -164,7 +169,7 @@ export async function runMpgdCli(args) {
|
|
|
164
169
|
renderValidationErrors: async (ctx, error) => {
|
|
165
170
|
const messages = error.errors.map((entry) => String(entry.message)).join('\n');
|
|
166
171
|
const values = ctx.values;
|
|
167
|
-
if ((ctx.name === 'verify-delivery' || ctx.name === 'upgrade') && values.json === true) {
|
|
172
|
+
if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade') && values.json === true) {
|
|
168
173
|
process.stderr.write(`${messages}\n`);
|
|
169
174
|
return '';
|
|
170
175
|
}
|
|
@@ -1193,6 +1198,83 @@ const assetsCommand = defineI18n({
|
|
|
1193
1198
|
ko: '결정적인 에셋 팩 전달 산출물을 빌드합니다.',
|
|
1194
1199
|
}),
|
|
1195
1200
|
subCommands: {
|
|
1201
|
+
'publish-s3': defineI18n({
|
|
1202
|
+
name: 'publish-s3',
|
|
1203
|
+
description: 'Publish immutable remote objects from a staged asset deployment.',
|
|
1204
|
+
resource: commandResource({ en: 'Publish immutable remote objects from a staged asset deployment.', ko: '배치된 에셋 배포의 원격 객체를 변경 불가능한 경로로 게시합니다.' }),
|
|
1205
|
+
args: {
|
|
1206
|
+
deployment: { type: 'string', required: true, description: 'stage-target output with packaged/ and remote/ children.' },
|
|
1207
|
+
endpoint: { type: 'string', required: true, description: 'HTTPS S3 API root endpoint without credentials or query.' },
|
|
1208
|
+
bucket: { type: 'string', required: true, description: 'Existing S3 bucket.' },
|
|
1209
|
+
prefix: { type: 'string', required: true, description: 'Versioned object-key prefix matching the public asset origin.' },
|
|
1210
|
+
region: { type: 'string', default: 'us-east-1', description: 'S3 signing region.' },
|
|
1211
|
+
'dry-run': { type: 'boolean', default: false, description: 'Verify and report without credentials or network calls.' },
|
|
1212
|
+
'allow-http-loopback': { type: 'boolean', default: false, description: 'Explicit development-only HTTP loopback endpoint opt-in.' },
|
|
1213
|
+
'access-key-env': { type: 'string', default: 'MPGD_ASSETS_S3_ACCESS_KEY_ID', description: 'Access-key environment variable name, never its value.' },
|
|
1214
|
+
'secret-key-env': { type: 'string', default: 'MPGD_ASSETS_S3_SECRET_ACCESS_KEY', description: 'Secret-key environment variable name, never its value.' },
|
|
1215
|
+
'session-token-env': { type: 'string', default: 'MPGD_ASSETS_S3_SESSION_TOKEN', description: 'Optional session-token environment variable name.' },
|
|
1216
|
+
'max-object-bytes': { type: 'number', description: 'Object-body cap; default64 MiB, maximum512 MiB.' },
|
|
1217
|
+
'max-total-bytes': { type: 'number', description: 'Total planned object bodies; default1 GiB.' },
|
|
1218
|
+
'max-objects': { type: 'number', description: 'Maximum objects including the immutable manifest snapshot; default10000.' },
|
|
1219
|
+
'request-timeout-ms': { type: 'number', description: 'Per HTTP attempt deadline; default10000 ms.' },
|
|
1220
|
+
'timeout-ms': { type: 'number', description: 'Whole preparation/publication deadline; default120000 ms.' },
|
|
1221
|
+
json: { type: 'boolean', default: false, description: 'Print a single JSON report without sensitive headers or URLs.' },
|
|
1222
|
+
},
|
|
1223
|
+
run: async (ctx) => {
|
|
1224
|
+
const report = await publishAssetPacks({
|
|
1225
|
+
deployment: ctx.values.deployment, endpoint: ctx.values.endpoint, bucket: ctx.values.bucket, prefix: ctx.values.prefix,
|
|
1226
|
+
region: ctx.values.region, dryRun: ctx.values['dry-run'], allowHttpLoopback: ctx.values['allow-http-loopback'],
|
|
1227
|
+
accessKeyEnv: ctx.values['access-key-env'], secretKeyEnv: ctx.values['secret-key-env'], sessionTokenEnv: ctx.values['session-token-env'],
|
|
1228
|
+
...(ctx.values['max-object-bytes'] === undefined ? {} : { maxObjectBytes: ctx.values['max-object-bytes'] }),
|
|
1229
|
+
...(ctx.values['max-total-bytes'] === undefined ? {} : { maxTotalBytes: ctx.values['max-total-bytes'] }),
|
|
1230
|
+
...(ctx.values['max-objects'] === undefined ? {} : { maxObjects: ctx.values['max-objects'] }),
|
|
1231
|
+
...(ctx.values['request-timeout-ms'] === undefined ? {} : { requestTimeoutMs: ctx.values['request-timeout-ms'] }),
|
|
1232
|
+
...(ctx.values['timeout-ms'] === undefined ? {} : { timeoutMs: ctx.values['timeout-ms'] }),
|
|
1233
|
+
});
|
|
1234
|
+
if (ctx.values.json) {
|
|
1235
|
+
console.info(JSON.stringify(report, null, 2));
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
console.info(`${report.dryRun ? 'Planned' : 'Published'} ${report.objects.length} immutable objects; ${report.plannedBytes} planned body bytes, ${report.uploadedBytes} newly uploaded body bytes.`);
|
|
1239
|
+
console.info(`Manifest snapshot: ${report.objects.at(-1)?.key}`);
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
}),
|
|
1243
|
+
'stage-target': defineI18n({
|
|
1244
|
+
name: 'stage-target',
|
|
1245
|
+
description: "Build and stage one target's packaged and remote asset packs.",
|
|
1246
|
+
resource: commandResource({
|
|
1247
|
+
en: "Build and stage one target's packaged and remote asset packs.",
|
|
1248
|
+
ko: '한 타깃의 로컬·원격 에셋 팩을 빌드하고 배치합니다.',
|
|
1249
|
+
}),
|
|
1250
|
+
args: {
|
|
1251
|
+
'targets-file': { type: 'string', default: 'mpgd.targets.json', description: 'Game target configuration file.' },
|
|
1252
|
+
target: { type: 'string', required: true, description: 'Deployment target name.' },
|
|
1253
|
+
out: { type: 'string', required: true, description: 'Fresh output directory with packaged/remote children.' },
|
|
1254
|
+
json: { type: 'boolean', default: false, description: 'Print the actual byte and location report as JSON.' },
|
|
1255
|
+
},
|
|
1256
|
+
run: async (ctx) => {
|
|
1257
|
+
const targetsPath = path.resolve(ctx.values['targets-file']);
|
|
1258
|
+
const document = JSON.parse(readFileSync(targetsPath, 'utf8'));
|
|
1259
|
+
if (!Object.hasOwn(document.targets ?? {}, ctx.values.target)) {
|
|
1260
|
+
throw new Error(`Target ${ctx.values.target} is not defined in the target configuration.`);
|
|
1261
|
+
}
|
|
1262
|
+
const policy = document.targets?.[ctx.values.target]?.assetPacks;
|
|
1263
|
+
if (policy === undefined) {
|
|
1264
|
+
throw new Error(`Target ${ctx.values.target} has no assetPacks policy.`);
|
|
1265
|
+
}
|
|
1266
|
+
assertAssetPackTargetPolicy(policy);
|
|
1267
|
+
const report = await buildAssetPackTarget({ policy, baseDir: path.dirname(targetsPath), outDir: ctx.values.out });
|
|
1268
|
+
if (ctx.values.json) {
|
|
1269
|
+
console.info(JSON.stringify(report, null, 2));
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
console.info(`Packaged assets: ${report.packagedAssetBytes} bytes (${report.packagedBytes} including metadata)`);
|
|
1273
|
+
console.info(`Remote objects: ${report.remoteBytes} bytes; manifest sha256:${report.manifestSha256}`);
|
|
1274
|
+
console.info(`Asset pack deployment: ${report.outDir}`);
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
}),
|
|
1196
1278
|
'build-packs': defineI18n({
|
|
1197
1279
|
name: 'build-packs',
|
|
1198
1280
|
description: 'Build files or ZIP delivery artifacts for configured packs.',
|