@mpgd/cli 0.31.0 → 0.32.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.d.ts +32 -0
- package/dist/asset-pack-build.js +411 -0
- package/dist/asset-pack-verify.d.ts +76 -0
- package/dist/asset-pack-verify.js +920 -0
- package/dist/asset-pack-zip.d.ts +19 -0
- package/dist/asset-pack-zip.js +81 -0
- package/dist/bin.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +283 -1
- package/package.json +10 -10
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic ZIP writer for pack delivery archives. The builder targets
|
|
3
|
+
* this limited format only: STORE and DEFLATE entries, no ZIP64, no split
|
|
4
|
+
* archives, no encryption, no directory or symlink entries, no comments.
|
|
5
|
+
* Fixed metadata: DOS timestamp 1980-01-01 00:00:00, UNIX permissions 0644,
|
|
6
|
+
* UTF-8 name flag, no extra fields. Given the same entries in the same order
|
|
7
|
+
* and the same Node/zlib build, the produced bytes are identical.
|
|
8
|
+
*/
|
|
9
|
+
export interface DeterministicZipEntry {
|
|
10
|
+
/** Normalized relative entry path; validated by the pack format module. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** Uncompressed entry bytes. */
|
|
13
|
+
readonly data: Buffer;
|
|
14
|
+
readonly method: 'store' | 'deflate';
|
|
15
|
+
/** Precomputed stored bytes for the entry method, when the builder already
|
|
16
|
+
* produced them (for example while choosing the method). */
|
|
17
|
+
readonly stored?: Buffer | undefined;
|
|
18
|
+
}
|
|
19
|
+
export declare function createDeterministicZip(entries: readonly DeterministicZipEntry[]): Buffer;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { crc32, deflateRawSync } from 'node:zlib';
|
|
2
|
+
const DOS_TIME = 0;
|
|
3
|
+
const DOS_DATE = 0x0021;
|
|
4
|
+
const VERSION_NEEDED = 20;
|
|
5
|
+
const VERSION_MADE_BY = 0x0300;
|
|
6
|
+
const UTF8_FLAG = 0x0800;
|
|
7
|
+
const EXTERNAL_ATTRIBUTES = (0o100644 << 16) >>> 0;
|
|
8
|
+
const MAX_ENTRIES = 0xffff;
|
|
9
|
+
const MAX_ENTRY_BYTES = 0xffffffff;
|
|
10
|
+
export function createDeterministicZip(entries) {
|
|
11
|
+
if (entries.length === 0 || entries.length > MAX_ENTRIES) {
|
|
12
|
+
throw new Error(`Deterministic ZIP requires 1..${MAX_ENTRIES} entries`);
|
|
13
|
+
}
|
|
14
|
+
const localChunks = [];
|
|
15
|
+
const centralChunks = [];
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const name = Buffer.from(entry.path, 'utf8');
|
|
19
|
+
if (name.length > MAX_ENTRIES) {
|
|
20
|
+
throw new Error(`Deterministic ZIP entry name exceeds 65535 bytes: ${entry.path}`);
|
|
21
|
+
}
|
|
22
|
+
if (entry.data.length > MAX_ENTRY_BYTES) {
|
|
23
|
+
throw new Error(`Deterministic ZIP entry exceeds 4 GiB: ${entry.path}`);
|
|
24
|
+
}
|
|
25
|
+
const compressed = entry.method === 'deflate'
|
|
26
|
+
? (entry.stored ?? deflateRawSync(entry.data, { level: 9 }))
|
|
27
|
+
: entry.data;
|
|
28
|
+
if (compressed.length > MAX_ENTRY_BYTES) {
|
|
29
|
+
throw new Error(`Deterministic ZIP entry exceeds 4 GiB: ${entry.path}`);
|
|
30
|
+
}
|
|
31
|
+
const methodCode = entry.method === 'deflate' ? 8 : 0;
|
|
32
|
+
const digest = crc32(entry.data) >>> 0;
|
|
33
|
+
const local = Buffer.alloc(30);
|
|
34
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
35
|
+
local.writeUInt16LE(VERSION_NEEDED, 4);
|
|
36
|
+
local.writeUInt16LE(UTF8_FLAG, 6);
|
|
37
|
+
local.writeUInt16LE(methodCode, 8);
|
|
38
|
+
local.writeUInt16LE(DOS_TIME, 10);
|
|
39
|
+
local.writeUInt16LE(DOS_DATE, 12);
|
|
40
|
+
local.writeUInt32LE(digest, 14);
|
|
41
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
42
|
+
local.writeUInt32LE(entry.data.length, 22);
|
|
43
|
+
local.writeUInt16LE(name.length, 26);
|
|
44
|
+
local.writeUInt16LE(0, 28);
|
|
45
|
+
localChunks.push(local, name, compressed);
|
|
46
|
+
const central = Buffer.alloc(46);
|
|
47
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
48
|
+
central.writeUInt16LE(VERSION_MADE_BY, 4);
|
|
49
|
+
central.writeUInt16LE(VERSION_NEEDED, 6);
|
|
50
|
+
central.writeUInt16LE(UTF8_FLAG, 8);
|
|
51
|
+
central.writeUInt16LE(methodCode, 10);
|
|
52
|
+
central.writeUInt16LE(DOS_TIME, 12);
|
|
53
|
+
central.writeUInt16LE(DOS_DATE, 14);
|
|
54
|
+
central.writeUInt32LE(digest, 16);
|
|
55
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
56
|
+
central.writeUInt32LE(entry.data.length, 24);
|
|
57
|
+
central.writeUInt16LE(name.length, 28);
|
|
58
|
+
central.writeUInt16LE(0, 30);
|
|
59
|
+
central.writeUInt16LE(0, 32);
|
|
60
|
+
central.writeUInt16LE(0, 34);
|
|
61
|
+
central.writeUInt16LE(0, 36);
|
|
62
|
+
central.writeUInt32LE(EXTERNAL_ATTRIBUTES, 38);
|
|
63
|
+
central.writeUInt32LE(offset, 42);
|
|
64
|
+
centralChunks.push(central, name);
|
|
65
|
+
offset += local.length + name.length + compressed.length;
|
|
66
|
+
}
|
|
67
|
+
const centralDirectory = Buffer.concat(centralChunks);
|
|
68
|
+
if (offset > MAX_ENTRY_BYTES || centralDirectory.length > MAX_ENTRY_BYTES) {
|
|
69
|
+
throw new Error('Deterministic ZIP archive exceeds 4 GiB');
|
|
70
|
+
}
|
|
71
|
+
const end = Buffer.alloc(22);
|
|
72
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
73
|
+
end.writeUInt16LE(0, 4);
|
|
74
|
+
end.writeUInt16LE(0, 6);
|
|
75
|
+
end.writeUInt16LE(entries.length, 8);
|
|
76
|
+
end.writeUInt16LE(entries.length, 10);
|
|
77
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
78
|
+
end.writeUInt32LE(offset, 16);
|
|
79
|
+
end.writeUInt16LE(0, 20);
|
|
80
|
+
return Buffer.concat([...localChunks, centralDirectory, end]);
|
|
81
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readCliArgs, runMpgdCli } from './index.js';
|
|
2
|
+
import { mpgdCliNeedsForcedExit, readCliArgs, runMpgdCli } from './index.js';
|
|
3
3
|
await runMpgdCli(readCliArgs());
|
|
4
|
+
if (mpgdCliNeedsForcedExit()) {
|
|
5
|
+
// Stalled threadpool filesystem work cannot be cancelled; only the
|
|
6
|
+
// standalone binary may terminate the process to outrun it.
|
|
7
|
+
process.exit(process.exitCode ?? 1);
|
|
8
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcce
|
|
|
8
8
|
export { createMicrosoftStorePackageGenerationRuntime, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, microsoftStorePackageGenerationSchemaVersion, renderMicrosoftStorePackageGenerationMarkdown, runMicrosoftStorePackageGeneration, type CreateMicrosoftStorePackageGenerationRuntimeInput, type MicrosoftStoreAddressResolver, type MicrosoftStorePackageGenerationEvidence, type MicrosoftStorePackageGenerationRuntime, type MicrosoftStoreResolvedAddress, type RunMicrosoftStorePackageGenerationInput, } from './microsoft-store-package-generation.js';
|
|
9
9
|
export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, type MicrosoftStoreSubmissionConfig, type MicrosoftStoreSubmissionCommerce, type MicrosoftStoreSubmissionCommerceProduct, type MicrosoftStoreSubmissionEvidence, type RunMicrosoftStoreSubmissionPreflightInput, } from './microsoft-store-submission.js';
|
|
10
10
|
export { assertProductionTargetReadiness, type ProductionTargetReadinessInput, } from './production-target-readiness.js';
|
|
11
|
+
export declare function mpgdCliNeedsForcedExit(): boolean;
|
|
11
12
|
export declare function runMpgdCli(args: readonly string[]): Promise<void>;
|
|
12
13
|
export declare function runCreateGameCli(args: readonly string[]): Promise<void>;
|
|
13
14
|
export declare function readCliArgs(): readonly string[];
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import completion from '@gunshi/plugin-completion';
|
|
|
6
6
|
import i18n, { defineI18n } from '@gunshi/plugin-i18n';
|
|
7
7
|
import resources from '@gunshi/resources';
|
|
8
8
|
import { cli } from 'gunshi';
|
|
9
|
+
import { buildAssetPacks } from './asset-pack-build.js';
|
|
10
|
+
import { verifyAssetPackDelivery } from './asset-pack-verify.js';
|
|
9
11
|
import { normalizeConfiguredBuildTargets, normalizeBuildTarget as normalizeConfiguredTargetName, supportedBuildTargets, } from './build-targets.js';
|
|
10
12
|
import { escapeMarkdownInline as escapeEvidenceMarkdownInline, writeEvidenceReportFiles, } from './evidence-io.js';
|
|
11
13
|
import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
|
|
@@ -85,11 +87,71 @@ const acceptanceStepIds = {
|
|
|
85
87
|
targetSmoke: 'target-smoke',
|
|
86
88
|
gameplayE2E: 'gameplay-e2e',
|
|
87
89
|
};
|
|
90
|
+
/** Whether the last run reported a deadline failure that leaves
|
|
91
|
+
* uncancellable filesystem work pending: the standalone binary forces its
|
|
92
|
+
* exit (see bin.ts); in-process library callers handle it themselves. */
|
|
93
|
+
let forcedExitPending = false;
|
|
94
|
+
/** A blocked pipe must not prevent the standalone binary from reaching its
|
|
95
|
+
* forced-exit path after a deadline report. The callback may remain queued
|
|
96
|
+
* behind a stalled reader, so the drain is best effort and bounded. */
|
|
97
|
+
const CLI_OUTPUT_DRAIN_TIMEOUT_MS = 250;
|
|
98
|
+
const drainOutput = (stream) => new Promise((resolve) => {
|
|
99
|
+
let settled = false;
|
|
100
|
+
const finish = () => {
|
|
101
|
+
if (settled) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
settled = true;
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(finish, CLI_OUTPUT_DRAIN_TIMEOUT_MS);
|
|
108
|
+
try {
|
|
109
|
+
stream.write('', () => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
finish();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
finish();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
export function mpgdCliNeedsForcedExit() {
|
|
120
|
+
return forcedExitPending;
|
|
121
|
+
}
|
|
88
122
|
export async function runMpgdCli(args) {
|
|
123
|
+
forcedExitPending = false;
|
|
89
124
|
await cli([...args], entryCommand, {
|
|
90
125
|
name: 'mpgd',
|
|
91
126
|
version: cliVersion,
|
|
127
|
+
// Machine-readable mode contract: `assets verify-delivery --json`
|
|
128
|
+
// owns stdout entirely — exactly one JSON document, parseable with
|
|
129
|
+
// JSON.parse(stdout) and nothing else. The framework's banner is
|
|
130
|
+
// suppressed for that command, and framework-level argument errors
|
|
131
|
+
// (bad value types, missing required arguments — rejected before the
|
|
132
|
+
// command handler runs) move to stderr so stdout never carries a
|
|
133
|
+
// partial document. Every other command keeps the default rendering.
|
|
134
|
+
renderHeader: async (ctx) => {
|
|
135
|
+
const values = ctx.values;
|
|
136
|
+
if (ctx.name === 'verify-delivery' && values.json === true) {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
const title = ctx.env.description || ctx.env.name || '';
|
|
140
|
+
return title
|
|
141
|
+
? `${title} (${ctx.env.name || ''}${ctx.env.version ? ` v${ctx.env.version}` : ''})`
|
|
142
|
+
: title;
|
|
143
|
+
},
|
|
144
|
+
renderValidationErrors: async (ctx, error) => {
|
|
145
|
+
const messages = error.errors.map((entry) => String(entry.message)).join('\n');
|
|
146
|
+
const values = ctx.values;
|
|
147
|
+
if (ctx.name === 'verify-delivery' && values.json === true) {
|
|
148
|
+
process.stderr.write(`${messages}\n`);
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
return messages;
|
|
152
|
+
},
|
|
92
153
|
subCommands: {
|
|
154
|
+
assets: assetsCommand,
|
|
93
155
|
game: gameCommand,
|
|
94
156
|
legal: legalCommand,
|
|
95
157
|
target: targetCommand,
|
|
@@ -143,7 +205,7 @@ const entryCommand = defineI18n({
|
|
|
143
205
|
ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
|
|
144
206
|
}),
|
|
145
207
|
run: () => {
|
|
146
|
-
console.info('Use a sub-command: game, target, kit.');
|
|
208
|
+
console.info('Use a sub-command: assets, game, legal, target, kit.');
|
|
147
209
|
console.info('Run "pnpm mpgd --help" for available commands.');
|
|
148
210
|
},
|
|
149
211
|
});
|
|
@@ -774,6 +836,226 @@ function readPositiveInteger(value, label) {
|
|
|
774
836
|
}
|
|
775
837
|
return parsed;
|
|
776
838
|
}
|
|
839
|
+
function assetPackVerifyArgs() {
|
|
840
|
+
return {
|
|
841
|
+
manifest: {
|
|
842
|
+
type: 'string',
|
|
843
|
+
required: true,
|
|
844
|
+
description: 'Path to the delivery manifest JSON document.',
|
|
845
|
+
},
|
|
846
|
+
root: {
|
|
847
|
+
type: 'string',
|
|
848
|
+
required: true,
|
|
849
|
+
description: 'Deployment root the manifest artifact paths resolve against.',
|
|
850
|
+
},
|
|
851
|
+
json: {
|
|
852
|
+
type: 'boolean',
|
|
853
|
+
required: false,
|
|
854
|
+
description: 'Print a single machine-readable JSON report to stdout.',
|
|
855
|
+
},
|
|
856
|
+
'max-object-bytes': {
|
|
857
|
+
type: 'number',
|
|
858
|
+
required: false,
|
|
859
|
+
description: 'Optional static-host limit: largest allowed deployed object in bytes.',
|
|
860
|
+
},
|
|
861
|
+
'max-files': {
|
|
862
|
+
type: 'number',
|
|
863
|
+
required: false,
|
|
864
|
+
description: 'Optional static-host limit: total regular files allowed under the root.',
|
|
865
|
+
},
|
|
866
|
+
'max-total-bytes': {
|
|
867
|
+
type: 'number',
|
|
868
|
+
required: false,
|
|
869
|
+
description: 'Optional static-host limit: total deployed bytes allowed under the root.',
|
|
870
|
+
},
|
|
871
|
+
'max-archive-bytes': {
|
|
872
|
+
type: 'number',
|
|
873
|
+
required: false,
|
|
874
|
+
description: 'Optional cap: largest zip archive read for verification, in bytes.',
|
|
875
|
+
},
|
|
876
|
+
'max-entry-bytes': {
|
|
877
|
+
type: 'number',
|
|
878
|
+
required: false,
|
|
879
|
+
description: 'Optional cap: largest expanded zip entry allowed, in bytes.',
|
|
880
|
+
},
|
|
881
|
+
'max-expanded-bytes': {
|
|
882
|
+
type: 'number',
|
|
883
|
+
required: false,
|
|
884
|
+
description: 'Optional cap: total expanded bytes allowed per archive, in bytes.',
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function assetPackBuildArgs() {
|
|
889
|
+
return {
|
|
890
|
+
config: {
|
|
891
|
+
type: 'string',
|
|
892
|
+
required: true,
|
|
893
|
+
description: 'Path to the asset pack build config JSON file.',
|
|
894
|
+
},
|
|
895
|
+
out: {
|
|
896
|
+
type: 'string',
|
|
897
|
+
required: true,
|
|
898
|
+
description: 'Output directory for delivery artifacts, outside the source root.',
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const assetsCommand = defineI18n({
|
|
903
|
+
name: 'assets',
|
|
904
|
+
description: 'Build deterministic asset pack delivery artifacts.',
|
|
905
|
+
resource: commandResource({
|
|
906
|
+
en: 'Build deterministic asset pack delivery artifacts.',
|
|
907
|
+
ko: '결정적인 에셋 팩 전달 산출물을 빌드합니다.',
|
|
908
|
+
}),
|
|
909
|
+
subCommands: {
|
|
910
|
+
'build-packs': defineI18n({
|
|
911
|
+
name: 'build-packs',
|
|
912
|
+
description: 'Build files or ZIP delivery artifacts for configured packs.',
|
|
913
|
+
resource: commandResource({
|
|
914
|
+
en: 'Build files or ZIP delivery artifacts for configured packs.',
|
|
915
|
+
ko: '설정된 팩의 files 또는 ZIP 전달 산출물을 빌드합니다.',
|
|
916
|
+
}, {
|
|
917
|
+
config: {
|
|
918
|
+
en: 'Path to the asset pack build config JSON file.',
|
|
919
|
+
ko: '에셋 팩 빌드 설정 JSON 파일 경로.',
|
|
920
|
+
},
|
|
921
|
+
out: {
|
|
922
|
+
en: 'Output directory for delivery artifacts, outside the source root.',
|
|
923
|
+
ko: '전달 산출물 출력 디렉터리. 소스 루트 밖이어야 합니다.',
|
|
924
|
+
},
|
|
925
|
+
}),
|
|
926
|
+
args: assetPackBuildArgs(),
|
|
927
|
+
run: (ctx) => {
|
|
928
|
+
const report = buildAssetPacks({
|
|
929
|
+
configPath: ctx.values.config,
|
|
930
|
+
outDir: ctx.values.out,
|
|
931
|
+
});
|
|
932
|
+
for (const output of report.outputs) {
|
|
933
|
+
console.info(`${output.action === 'written' ? 'wrote' : 'unchanged'} ${output.path} `
|
|
934
|
+
+ `(${output.bytes} bytes, sha256:${output.sha256})`);
|
|
935
|
+
}
|
|
936
|
+
for (const archive of report.archives) {
|
|
937
|
+
console.info(`archive ${archive.path}: ${archive.entryCount} entries `
|
|
938
|
+
+ `(${archive.storeEntries} store, ${archive.deflateEntries} deflate), `
|
|
939
|
+
+ `sources ${archive.sourceBytes} bytes, archive ${archive.archiveBytes} bytes, `
|
|
940
|
+
+ `sha256:${archive.sha256}`);
|
|
941
|
+
}
|
|
942
|
+
console.info(`Asset pack delivery manifest: ${report.manifestPath}`);
|
|
943
|
+
},
|
|
944
|
+
}),
|
|
945
|
+
'verify-delivery': defineI18n({
|
|
946
|
+
name: 'verify-delivery',
|
|
947
|
+
description: 'Verify delivery artifacts read-only against the manifest.',
|
|
948
|
+
resource: commandResource({
|
|
949
|
+
en: 'Verify delivery artifacts read-only against the manifest.',
|
|
950
|
+
ko: '전달 산출물을 manifest 기준으로 읽기 전용으로 검증합니다.',
|
|
951
|
+
}, {
|
|
952
|
+
manifest: {
|
|
953
|
+
en: 'Path to the delivery manifest JSON document.',
|
|
954
|
+
ko: '전달 manifest JSON 문서 경로.',
|
|
955
|
+
},
|
|
956
|
+
root: {
|
|
957
|
+
en: 'Deployment root the manifest artifact paths resolve against.',
|
|
958
|
+
ko: 'manifest 산출물 경로의 기준이 되는 배포 루트 디렉터리.',
|
|
959
|
+
},
|
|
960
|
+
json: {
|
|
961
|
+
en: 'Print a single machine-readable JSON report to stdout.',
|
|
962
|
+
ko: '기계 판독 가능한 JSON 보고서 하나를 stdout에 출력합니다.',
|
|
963
|
+
},
|
|
964
|
+
'max-object-bytes': {
|
|
965
|
+
en: 'Optional static-host limit: largest allowed deployed object in bytes.',
|
|
966
|
+
ko: '선택적 정적 호스트 한도: 허용되는 최대 배포 object 바이트 수.',
|
|
967
|
+
},
|
|
968
|
+
'max-files': {
|
|
969
|
+
en: 'Optional static-host limit: total regular files allowed under the root.',
|
|
970
|
+
ko: '선택적 정적 호스트 한도: 루트 아래 허용되는 일반 파일 총 개수.',
|
|
971
|
+
},
|
|
972
|
+
'max-total-bytes': {
|
|
973
|
+
en: 'Optional static-host limit: total deployed bytes allowed under the root.',
|
|
974
|
+
ko: '선택적 정적 호스트 한도: 루트 아래 허용되는 총 배포 바이트 수.',
|
|
975
|
+
},
|
|
976
|
+
'max-archive-bytes': {
|
|
977
|
+
en: 'Optional cap: largest zip archive read for verification, in bytes.',
|
|
978
|
+
ko: '선택적 상한: 검증 시 읽는 zip 아카이브의 최대 바이트 수.',
|
|
979
|
+
},
|
|
980
|
+
'max-entry-bytes': {
|
|
981
|
+
en: 'Optional cap: largest expanded zip entry allowed, in bytes.',
|
|
982
|
+
ko: '선택적 상한: 허용되는 확장(zip 해제) 엔트리 최대 바이트 수.',
|
|
983
|
+
},
|
|
984
|
+
'max-expanded-bytes': {
|
|
985
|
+
en: 'Optional cap: total expanded bytes allowed per archive, in bytes.',
|
|
986
|
+
ko: '선택적 상한: 아카이브별 허용되는 총 확장 바이트 수.',
|
|
987
|
+
},
|
|
988
|
+
}),
|
|
989
|
+
args: assetPackVerifyArgs(),
|
|
990
|
+
run: async (ctx) => {
|
|
991
|
+
const hostLimits = {
|
|
992
|
+
...(ctx.values['max-object-bytes'] === undefined
|
|
993
|
+
? {}
|
|
994
|
+
: { maxObjectBytes: ctx.values['max-object-bytes'] }),
|
|
995
|
+
...(ctx.values['max-files'] === undefined
|
|
996
|
+
? {}
|
|
997
|
+
: { maxFiles: ctx.values['max-files'] }),
|
|
998
|
+
...(ctx.values['max-total-bytes'] === undefined
|
|
999
|
+
? {}
|
|
1000
|
+
: { maxTotalBytes: ctx.values['max-total-bytes'] }),
|
|
1001
|
+
};
|
|
1002
|
+
const report = await verifyAssetPackDelivery({
|
|
1003
|
+
manifestPath: ctx.values.manifest,
|
|
1004
|
+
root: ctx.values.root,
|
|
1005
|
+
...(Object.keys(hostLimits).length > 0 ? { hostLimits } : {}),
|
|
1006
|
+
...(ctx.values['max-archive-bytes'] === undefined
|
|
1007
|
+
? {}
|
|
1008
|
+
: { maxArchiveBytes: ctx.values['max-archive-bytes'] }),
|
|
1009
|
+
...(ctx.values['max-entry-bytes'] === undefined
|
|
1010
|
+
? {}
|
|
1011
|
+
: { maxEntryBytes: ctx.values['max-entry-bytes'] }),
|
|
1012
|
+
...(ctx.values['max-expanded-bytes'] === undefined
|
|
1013
|
+
? {}
|
|
1014
|
+
: { maxExpandedBytes: ctx.values['max-expanded-bytes'] }),
|
|
1015
|
+
});
|
|
1016
|
+
if (ctx.values.json === true) {
|
|
1017
|
+
console.info(JSON.stringify(report, null, 2));
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
for (const archive of report.archives) {
|
|
1021
|
+
console.info(`verified archive ${archive.packId}: ${archive.entries} entries, ${archive.expandedBytes} expanded bytes`);
|
|
1022
|
+
}
|
|
1023
|
+
console.info(`manifest ${report.manifest.sha256.slice(0, 12)}… (${report.manifest.bytes} bytes, ${report.manifest.packs} packs)`);
|
|
1024
|
+
console.info(`referenced objects: ${report.referenced.files} files, ${report.referenced.bytes} bytes`);
|
|
1025
|
+
if (report.inventory !== undefined) {
|
|
1026
|
+
console.info(`root inventory: ${report.inventory.files} files, ${report.inventory.bytes} bytes${report.inventory.truncated ? ' (truncated)' : ''}`);
|
|
1027
|
+
}
|
|
1028
|
+
for (const failure of report.failures) {
|
|
1029
|
+
console.error(`[${failure.stage}/${failure.code}] ${failure.message}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (report.ok) {
|
|
1032
|
+
console.info('Delivery verification passed.');
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
console.error('Delivery verification failed; see the failures above.');
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (report.failures.some((failure) => failure.code === 'deadline')) {
|
|
1039
|
+
// Piped console output is asynchronous; drain both streams so a
|
|
1040
|
+
// large report is not truncated by the binary's forced exit.
|
|
1041
|
+
await Promise.all([drainOutput(process.stdout), drainOutput(process.stderr)]);
|
|
1042
|
+
// Stalled filesystem requests cannot be cancelled once the
|
|
1043
|
+
// deadline fires; the standalone binary forces its exit so a
|
|
1044
|
+
// pending threadpool call cannot keep the process (and its
|
|
1045
|
+
// leaked handle) alive past the reported budget. In-process
|
|
1046
|
+
// callers of runMpgdCli observe mpgdCliNeedsForcedExit instead.
|
|
1047
|
+
forcedExitPending = true;
|
|
1048
|
+
}
|
|
1049
|
+
if (!report.ok) {
|
|
1050
|
+
process.exitCode = 1;
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
}),
|
|
1054
|
+
},
|
|
1055
|
+
run: () => {
|
|
1056
|
+
console.info('Use "mpgd assets build-packs" or "mpgd assets verify-delivery".');
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
777
1059
|
const legalCommand = defineI18n({
|
|
778
1060
|
name: 'legal',
|
|
779
1061
|
description: 'Build static legal/support pages for store and release evidence.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,25 +46,25 @@
|
|
|
46
46
|
"entities": "^8.0.0",
|
|
47
47
|
"esbuild": "^0.28.1",
|
|
48
48
|
"gunshi": "^0.35.1",
|
|
49
|
-
"undici": "^7.28.0"
|
|
49
|
+
"undici": "^7.28.0",
|
|
50
|
+
"@mpgd/phaser-assets": "0.6.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "^24.0.0",
|
|
53
54
|
"ttsc": "0.18.4",
|
|
54
55
|
"typescript": "7.0.2",
|
|
55
|
-
"@mpgd/adapter-
|
|
56
|
+
"@mpgd/adapter-browser": "0.7.3",
|
|
56
57
|
"@mpgd/adapter-capacitor": "0.4.10",
|
|
57
|
-
"@mpgd/adapter-verse8": "0.3.6",
|
|
58
58
|
"@mpgd/adapter-devvit": "0.9.8",
|
|
59
|
-
"@mpgd/adapter-
|
|
60
|
-
"@mpgd/
|
|
61
|
-
"@mpgd/catalog": "0.7.2",
|
|
62
|
-
"@mpgd/game-services": "0.15.1",
|
|
59
|
+
"@mpgd/adapter-ait": "0.12.1",
|
|
60
|
+
"@mpgd/adapter-verse8": "0.3.6",
|
|
63
61
|
"@mpgd/bridge": "0.9.0",
|
|
64
|
-
"@mpgd/
|
|
62
|
+
"@mpgd/catalog": "0.7.2",
|
|
63
|
+
"@mpgd/i18n": "0.6.1",
|
|
65
64
|
"@mpgd/platform": "0.12.0",
|
|
65
|
+
"@mpgd/game-services": "0.15.1",
|
|
66
66
|
"@mpgd/target-config": "0.15.0",
|
|
67
|
-
"@mpgd/
|
|
67
|
+
"@mpgd/analytics": "0.3.12"
|
|
68
68
|
},
|
|
69
69
|
"main": "./dist/index.js",
|
|
70
70
|
"types": "./dist/index.d.ts",
|