@auraindustry/aurajs 0.0.5 → 0.0.6
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/README.md +4 -2
- package/benchmarks/perf-thresholds.json +27 -0
- package/package.json +2 -1
- package/src/build-contract.mjs +1644 -56
- package/src/cli.mjs +1048 -321
- package/src/conformance.mjs +356 -11
- package/src/cutscene.mjs +205 -0
- package/src/game-state-runtime.mjs +260 -0
- package/src/headless-test.mjs +92 -9
- package/src/perf-benchmark.mjs +103 -0
- package/src/scaffold.mjs +413 -13
- package/src/state-artifacts.mjs +321 -0
- package/src/state-dev-reload.mjs +120 -0
- package/templates/create/2d-survivor/aura.config.json +28 -0
- package/templates/create/2d-survivor/src/main.js +344 -0
- package/templates/create/3d-collectathon/aura.config.json +28 -0
- package/templates/create/3d-collectathon/src/main.js +367 -0
- package/templates/skills/aurajs/api-contract-3d.md +1 -1
- package/templates/skills/aurajs/api-contract.md +1 -1
- package/src/.gitkeep +0 -0
package/src/cli.mjs
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
formatBundleError,
|
|
10
10
|
isBundleError,
|
|
11
11
|
} from './bundler.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
scaffold,
|
|
14
|
+
scaffoldGame,
|
|
15
|
+
listCreateTemplates,
|
|
16
|
+
listCreateTemplateAliasHints,
|
|
17
|
+
normalizeCreateTemplate,
|
|
18
|
+
} from './scaffold.mjs';
|
|
13
19
|
import { packageAssets } from './asset-pack.mjs';
|
|
14
20
|
import { writeBuildManifest, writeWebBuildArtifacts } from './build-contract.mjs';
|
|
15
21
|
import {
|
|
@@ -29,6 +35,16 @@ import {
|
|
|
29
35
|
DEFAULT_STATE_MUTATION_GUARDRAILS,
|
|
30
36
|
STATE_MUTATION_ALLOWLIST_PREFIXES,
|
|
31
37
|
} from './game-state-runtime.mjs';
|
|
38
|
+
import {
|
|
39
|
+
StateArtifactError,
|
|
40
|
+
createStateArtifactEnvelope,
|
|
41
|
+
listStateArtifacts,
|
|
42
|
+
loadStateArtifact,
|
|
43
|
+
normalizeStateArtifactName,
|
|
44
|
+
resolveStateArtifactPath,
|
|
45
|
+
writeStateArtifactEnvelope,
|
|
46
|
+
} from './state-artifacts.mjs';
|
|
47
|
+
import { prepareDevStateRestoreEntry } from './state-dev-reload.mjs';
|
|
32
48
|
|
|
33
49
|
// ---------------------------------------------------------------------------
|
|
34
50
|
// Version
|
|
@@ -36,6 +52,9 @@ import {
|
|
|
36
52
|
|
|
37
53
|
const PKG_PATH = new URL('../package.json', import.meta.url);
|
|
38
54
|
const VERSION = JSON.parse(readFileSync(PKG_PATH, 'utf8')).version;
|
|
55
|
+
const CREATE_TEMPLATE_OPTION_LIST = listCreateTemplates().join('|');
|
|
56
|
+
const CREATE_TEMPLATE_ALIAS_HINT_LIST = listCreateTemplateAliasHints().join(', ');
|
|
57
|
+
const CREATE_USAGE = `aura create <name> [--template <${CREATE_TEMPLATE_OPTION_LIST}>] [--skip-install]`;
|
|
39
58
|
|
|
40
59
|
// ---------------------------------------------------------------------------
|
|
41
60
|
// Command registry
|
|
@@ -45,10 +64,10 @@ const COMMANDS = {
|
|
|
45
64
|
init: { description: 'Scaffold a new AuraJS project', usage: 'aura init <name>' },
|
|
46
65
|
create: {
|
|
47
66
|
description: 'Scaffold a full npm-ready AuraJS game',
|
|
48
|
-
usage:
|
|
67
|
+
usage: CREATE_USAGE,
|
|
49
68
|
details: [
|
|
50
69
|
' Options:',
|
|
51
|
-
` --template <${
|
|
70
|
+
` --template <${CREATE_TEMPLATE_OPTION_LIST}> Choose starting game template (aliases: ${CREATE_TEMPLATE_ALIAS_HINT_LIST})`,
|
|
52
71
|
' --skip-install Skip npm install after scaffold',
|
|
53
72
|
],
|
|
54
73
|
},
|
|
@@ -57,11 +76,14 @@ const COMMANDS = {
|
|
|
57
76
|
usage: 'aura dev [--target web]',
|
|
58
77
|
details: [
|
|
59
78
|
' Options:',
|
|
60
|
-
' --target web Run browser web-build
|
|
79
|
+
' --target web Run browser web-build rebuild loop for the current browser-backed subset',
|
|
80
|
+
' --restore-slot <name> Reapply a saved slot after each native dev restart',
|
|
81
|
+
' --restore-checkpoint <name> Reapply a saved checkpoint after each native dev restart',
|
|
61
82
|
'',
|
|
62
83
|
' Notes:',
|
|
63
84
|
' - default `aura dev` preserves native host hot-reload semantics.',
|
|
64
|
-
' -
|
|
85
|
+
' - restore flags are native-only and reapply a persisted state artifact after setup.',
|
|
86
|
+
' - `aura dev --target web` rebuilds `build/web` and emits refresh diagnostics; serve those artifacts in a browser.',
|
|
65
87
|
],
|
|
66
88
|
},
|
|
67
89
|
build: {
|
|
@@ -73,24 +95,39 @@ const COMMANDS = {
|
|
|
73
95
|
' --asset-mode <embed|sibling> Package assets in pak or sibling assets/',
|
|
74
96
|
'',
|
|
75
97
|
' Notes:',
|
|
76
|
-
' - `--target web` emits browser artifacts under `build/web
|
|
98
|
+
' - `--target web` emits browser artifacts under `build/web` for the current browser-backed subset.',
|
|
77
99
|
' - Native targets are current host only in v1.',
|
|
78
100
|
' - `--target all` keeps current host output; use CI matrix for all platforms.',
|
|
101
|
+
' - Browser runtime truth today: current 2D/input/assets/storage path is supported; `aura.state` and `aura.net` remain reason-coded unsupported.',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
run: {
|
|
105
|
+
description: 'Build and run the game',
|
|
106
|
+
usage: 'aura run',
|
|
107
|
+
details: [
|
|
108
|
+
' Notes:',
|
|
109
|
+
' - `aura run` is native-only in v1.',
|
|
110
|
+
' - For web, use `aura build --target web` and serve `build/web` in a browser.',
|
|
79
111
|
],
|
|
80
112
|
},
|
|
81
|
-
run: { description: 'Build and run the game', usage: 'aura run' },
|
|
82
113
|
clean: { description: 'Delete build artifacts', usage: 'aura clean' },
|
|
83
114
|
test: { description: 'Run game logic in headless mode', usage: 'aura test [file] [--width N] [--height N] [--frames N]' },
|
|
84
115
|
conformance: { description: 'Run API conformance suites in headless mode', usage: 'aura conformance [--mode shim|native|both] [--json]' },
|
|
85
116
|
state: {
|
|
86
117
|
description: 'Export, diff, and patch canonical game-state snapshots',
|
|
87
|
-
usage: 'aura state <export|diff|patch|apply> [options]',
|
|
118
|
+
usage: 'aura state <export|diff|patch|apply|slot|checkpoint> [options]',
|
|
88
119
|
details: [
|
|
89
120
|
' Subcommands:',
|
|
90
121
|
' export Emit canonical game-state-v1 JSON snapshot',
|
|
91
122
|
' diff <before.json> <after.json> Emit deterministic patch payload',
|
|
92
123
|
' patch <state.json> <patch.json> Apply patch payload to a state snapshot',
|
|
93
124
|
' apply <state.json> Apply state payload via runtime hooks',
|
|
125
|
+
' slot save <name> Persist named save slot under .aura/state/slots/',
|
|
126
|
+
' slot restore <name> Reapply named save slot via runtime hooks',
|
|
127
|
+
' slot list List named save slots with metadata',
|
|
128
|
+
' checkpoint save <name> Persist named checkpoint under .aura/state/checkpoints/',
|
|
129
|
+
' checkpoint restore <name> Reapply named checkpoint via runtime hooks',
|
|
130
|
+
' checkpoint list List named checkpoints with metadata',
|
|
94
131
|
'',
|
|
95
132
|
' Options:',
|
|
96
133
|
' export:',
|
|
@@ -127,6 +164,20 @@ const COMMANDS = {
|
|
|
127
164
|
' --max-bytes <N> Max payload bytes per input (default: 262144)',
|
|
128
165
|
' --max-runtime-ms <N> Max mutation runtime budget in ms (default: 200)',
|
|
129
166
|
' --audit <path|-> Audit artifact path (default: .aura/state/state-apply-audit.json)',
|
|
167
|
+
'',
|
|
168
|
+
' slot/checkpoint save:',
|
|
169
|
+
' --report <path|-> Write command report JSON to file (or "-" for stdout)',
|
|
170
|
+
' --artifact <path> Override saved artifact path',
|
|
171
|
+
' --note <text> Optional metadata note stored with the artifact',
|
|
172
|
+
'',
|
|
173
|
+
' slot/checkpoint restore:',
|
|
174
|
+
' --report <path|-> Write command report JSON to file (or "-" for stdout)',
|
|
175
|
+
' --artifact <path> Override artifact path to restore from',
|
|
176
|
+
'',
|
|
177
|
+
' slot/checkpoint list:',
|
|
178
|
+
' --report <path|-> Write listing JSON to file (or "-" for stdout)',
|
|
179
|
+
' --compact Compact JSON output (single line)',
|
|
180
|
+
' --pretty Pretty JSON output (default)',
|
|
130
181
|
],
|
|
131
182
|
},
|
|
132
183
|
};
|
|
@@ -270,14 +321,14 @@ function parseCreateArgs(args) {
|
|
|
270
321
|
throw new Error(`Unknown option for create: ${arg}`);
|
|
271
322
|
}
|
|
272
323
|
if (name !== null) {
|
|
273
|
-
throw new Error(`Unexpected extra argument "${arg}". Usage:
|
|
324
|
+
throw new Error(`Unexpected extra argument "${arg}". Usage: ${CREATE_USAGE}`);
|
|
274
325
|
}
|
|
275
326
|
name = arg;
|
|
276
327
|
}
|
|
277
328
|
|
|
278
329
|
const normalizedTemplate = normalizeCreateTemplate(template);
|
|
279
330
|
if (!normalizedTemplate || !allowedTemplates.has(normalizedTemplate)) {
|
|
280
|
-
throw new Error(`Invalid --template "${template}". Supported templates: ${[...allowedTemplates].join(', ')} (aliases:
|
|
331
|
+
throw new Error(`Invalid --template "${template}". Supported templates: ${[...allowedTemplates].join(', ')} (aliases: ${CREATE_TEMPLATE_ALIAS_HINT_LIST}).`);
|
|
281
332
|
}
|
|
282
333
|
|
|
283
334
|
return { name, template: normalizedTemplate, skipInstall };
|
|
@@ -902,121 +953,640 @@ function parseStateApplyArgs(args) {
|
|
|
902
953
|
return parsed;
|
|
903
954
|
}
|
|
904
955
|
|
|
905
|
-
function
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
956
|
+
function parseDevArgs(args) {
|
|
957
|
+
const parsed = {
|
|
958
|
+
target: null,
|
|
959
|
+
restoreKind: null,
|
|
960
|
+
restoreName: null,
|
|
961
|
+
};
|
|
912
962
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (normalized.length === 0) {
|
|
916
|
-
throw new StateExportError(reasonCode, '--output requires a non-empty path or "-".');
|
|
917
|
-
}
|
|
918
|
-
if (normalized === '-') {
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
return resolve(process.cwd(), normalized);
|
|
922
|
-
}
|
|
963
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
964
|
+
const token = args[i];
|
|
923
965
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
966
|
+
if (token === '--target') {
|
|
967
|
+
const value = readRequiredOptionValue(args, i, '--target');
|
|
968
|
+
parsed.target = normalizeDevTarget(value);
|
|
969
|
+
i += 1;
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (token.startsWith('--target=')) {
|
|
973
|
+
parsed.target = normalizeDevTarget(token.slice('--target='.length));
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (token === '--restore-slot') {
|
|
978
|
+
const value = readRequiredOptionValue(args, i, '--restore-slot');
|
|
979
|
+
if (parsed.restoreKind) {
|
|
980
|
+
throw new StateExportError(
|
|
981
|
+
'invalid_dev_args',
|
|
982
|
+
'Use only one of --restore-slot or --restore-checkpoint.',
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
parsed.restoreKind = 'slot';
|
|
986
|
+
parsed.restoreName = normalizeStateArtifactName(value, { kind: 'slot' });
|
|
987
|
+
i += 1;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (token.startsWith('--restore-slot=')) {
|
|
991
|
+
if (parsed.restoreKind) {
|
|
992
|
+
throw new StateExportError(
|
|
993
|
+
'invalid_dev_args',
|
|
994
|
+
'Use only one of --restore-slot or --restore-checkpoint.',
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
parsed.restoreKind = 'slot';
|
|
998
|
+
parsed.restoreName = normalizeStateArtifactName(token.slice('--restore-slot='.length), { kind: 'slot' });
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (token === '--restore-checkpoint') {
|
|
1003
|
+
const value = readRequiredOptionValue(args, i, '--restore-checkpoint');
|
|
1004
|
+
if (parsed.restoreKind) {
|
|
1005
|
+
throw new StateExportError(
|
|
1006
|
+
'invalid_dev_args',
|
|
1007
|
+
'Use only one of --restore-slot or --restore-checkpoint.',
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
parsed.restoreKind = 'checkpoint';
|
|
1011
|
+
parsed.restoreName = normalizeStateArtifactName(value, { kind: 'checkpoint' });
|
|
1012
|
+
i += 1;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (token.startsWith('--restore-checkpoint=')) {
|
|
1016
|
+
if (parsed.restoreKind) {
|
|
1017
|
+
throw new StateExportError(
|
|
1018
|
+
'invalid_dev_args',
|
|
1019
|
+
'Use only one of --restore-slot or --restore-checkpoint.',
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
parsed.restoreKind = 'checkpoint';
|
|
1023
|
+
parsed.restoreName = normalizeStateArtifactName(
|
|
1024
|
+
token.slice('--restore-checkpoint='.length),
|
|
1025
|
+
{ kind: 'checkpoint' },
|
|
1026
|
+
);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (token.startsWith('--')) {
|
|
1031
|
+
throw new StateExportError('invalid_dev_args', `Unknown dev option: ${token}`);
|
|
1032
|
+
}
|
|
931
1033
|
}
|
|
932
|
-
return resolve(process.cwd(), normalized);
|
|
933
|
-
}
|
|
934
1034
|
|
|
935
|
-
|
|
936
|
-
return resolve(projectRoot, '.aura/state/state-patch-audit.json');
|
|
1035
|
+
return parsed;
|
|
937
1036
|
}
|
|
938
1037
|
|
|
939
|
-
function
|
|
940
|
-
|
|
1038
|
+
function normalizeDevTarget(value) {
|
|
1039
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
1040
|
+
if (normalized === 'macos' || normalized === 'darwin' || normalized === 'mac') return 'mac';
|
|
1041
|
+
if (normalized === 'win' || normalized === 'win32' || normalized === 'windows') return 'windows';
|
|
1042
|
+
if (normalized === 'browser' || normalized === 'web') return 'web';
|
|
1043
|
+
if (normalized === 'linux') return 'linux';
|
|
1044
|
+
throw new StateExportError(
|
|
1045
|
+
'invalid_dev_args',
|
|
1046
|
+
`Unsupported dev target "${value}". Supported values: web.`,
|
|
1047
|
+
);
|
|
941
1048
|
}
|
|
942
1049
|
|
|
943
|
-
function
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
`${label} "${filePath}" is not valid JSON: ${error.message}`,
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
1050
|
+
function parseStateArtifactSaveArgs(kind, args) {
|
|
1051
|
+
const normalizedKind = kind === 'slot' ? 'slot' : 'checkpoint';
|
|
1052
|
+
const reasonCode = `invalid_state_${normalizedKind}_save_args`;
|
|
1053
|
+
const parsed = {
|
|
1054
|
+
kind: normalizedKind,
|
|
1055
|
+
name: null,
|
|
1056
|
+
artifactPath: null,
|
|
1057
|
+
reportPath: null,
|
|
1058
|
+
note: null,
|
|
1059
|
+
file: null,
|
|
1060
|
+
mode: 'headless',
|
|
1061
|
+
frames: 1,
|
|
1062
|
+
schemaVersion: GAME_STATE_SCHEMA_VERSION,
|
|
1063
|
+
compact: false,
|
|
1064
|
+
};
|
|
962
1065
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1066
|
+
let formattingFlag = 'pretty';
|
|
1067
|
+
|
|
1068
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1069
|
+
const token = args[i];
|
|
1070
|
+
|
|
1071
|
+
if (token === '--artifact') {
|
|
1072
|
+
parsed.artifactPath = normalizeStateArtifactFilePath(
|
|
1073
|
+
readRequiredOptionValue(args, i, '--artifact'),
|
|
1074
|
+
'--artifact',
|
|
1075
|
+
reasonCode,
|
|
1076
|
+
);
|
|
1077
|
+
i += 1;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (token.startsWith('--artifact=')) {
|
|
1081
|
+
parsed.artifactPath = normalizeStateArtifactFilePath(
|
|
1082
|
+
token.slice('--artifact='.length),
|
|
1083
|
+
'--artifact',
|
|
1084
|
+
reasonCode,
|
|
1085
|
+
);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (token === '--report') {
|
|
1090
|
+
parsed.reportPath = normalizeStateCommandOutput(
|
|
1091
|
+
readRequiredOptionValue(args, i, '--report'),
|
|
1092
|
+
reasonCode,
|
|
1093
|
+
);
|
|
1094
|
+
i += 1;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (token.startsWith('--report=')) {
|
|
1098
|
+
parsed.reportPath = normalizeStateCommandOutput(token.slice('--report='.length), reasonCode);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (token === '--note') {
|
|
1103
|
+
parsed.note = normalizeOptionalNote(readRequiredOptionValue(args, i, '--note'));
|
|
1104
|
+
i += 1;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (token.startsWith('--note=')) {
|
|
1108
|
+
parsed.note = normalizeOptionalNote(token.slice('--note='.length));
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (token === '--mode') {
|
|
1113
|
+
parsed.mode = normalizeStateExportMode(readRequiredOptionValue(args, i, '--mode'));
|
|
1114
|
+
i += 1;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (token.startsWith('--mode=')) {
|
|
1118
|
+
parsed.mode = normalizeStateExportMode(token.slice('--mode='.length));
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (token === '--frames') {
|
|
1123
|
+
parsed.frames = parseNonNegativeIntegerArg(readRequiredOptionValue(args, i, '--frames'), '--frames', reasonCode);
|
|
1124
|
+
i += 1;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (token.startsWith('--frames=')) {
|
|
1128
|
+
parsed.frames = parseNonNegativeIntegerArg(token.slice('--frames='.length), '--frames', reasonCode);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (token === '--schema' || token === '--schema-version') {
|
|
1133
|
+
parsed.schemaVersion = normalizeStateExportSchemaVersion(readRequiredOptionValue(args, i, token));
|
|
1134
|
+
i += 1;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (token.startsWith('--schema=')) {
|
|
1138
|
+
parsed.schemaVersion = normalizeStateExportSchemaVersion(token.slice('--schema='.length));
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (token.startsWith('--schema-version=')) {
|
|
1142
|
+
parsed.schemaVersion = normalizeStateExportSchemaVersion(token.slice('--schema-version='.length));
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (token === '--compact') {
|
|
1147
|
+
if (formattingFlag === 'pretty') {
|
|
1148
|
+
parsed.compact = true;
|
|
1149
|
+
formattingFlag = 'compact';
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (formattingFlag === 'compact') continue;
|
|
1153
|
+
throw new StateExportError(reasonCode, 'Cannot use --compact and --pretty together.');
|
|
1154
|
+
}
|
|
1155
|
+
if (token === '--pretty') {
|
|
1156
|
+
if (formattingFlag === 'compact') {
|
|
1157
|
+
throw new StateExportError(reasonCode, 'Cannot use --compact and --pretty together.');
|
|
1158
|
+
}
|
|
1159
|
+
parsed.compact = false;
|
|
1160
|
+
formattingFlag = 'pretty';
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (token === '--file') {
|
|
1165
|
+
parsed.file = normalizeStateExportFile(readRequiredOptionValue(args, i, '--file'));
|
|
1166
|
+
i += 1;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (token.startsWith('--file=')) {
|
|
1170
|
+
parsed.file = normalizeStateExportFile(token.slice('--file='.length));
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (token.startsWith('--')) {
|
|
1175
|
+
throw new StateExportError(reasonCode, `Unknown ${normalizedKind} save option: ${token}`);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (parsed.name !== null) {
|
|
1179
|
+
throw new StateExportError(
|
|
1180
|
+
reasonCode,
|
|
1181
|
+
`Unexpected extra argument "${token}". Usage: aura state ${normalizedKind} save <name> [options]`,
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
parsed.name = normalizeStateArtifactName(token, { kind: normalizedKind });
|
|
967
1185
|
}
|
|
968
1186
|
|
|
969
|
-
|
|
970
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
971
|
-
writeFileSync(filePath, jsonText, 'utf8');
|
|
972
|
-
} catch (error) {
|
|
1187
|
+
if (!parsed.name) {
|
|
973
1188
|
throw new StateExportError(
|
|
974
1189
|
reasonCode,
|
|
975
|
-
`
|
|
1190
|
+
`Missing ${normalizedKind} name. Usage: aura state ${normalizedKind} save <name> [options]`,
|
|
976
1191
|
);
|
|
977
1192
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
function writeStateMutationAudit(auditPath, payload) {
|
|
981
|
-
const text = `${JSON.stringify(payload, null, 2)}\n`;
|
|
982
|
-
if (auditPath === null) {
|
|
983
|
-
process.stdout.write(text);
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
try {
|
|
987
|
-
mkdirSync(dirname(auditPath), { recursive: true });
|
|
988
|
-
writeFileSync(auditPath, text, 'utf8');
|
|
989
|
-
} catch (error) {
|
|
1193
|
+
if (parsed.schemaVersion !== GAME_STATE_SCHEMA_VERSION) {
|
|
990
1194
|
throw new StateExportError(
|
|
991
|
-
'
|
|
992
|
-
`
|
|
1195
|
+
'schema_version_mismatch',
|
|
1196
|
+
`Unsupported schema version "${parsed.schemaVersion}". Expected "${GAME_STATE_SCHEMA_VERSION}".`,
|
|
993
1197
|
);
|
|
994
1198
|
}
|
|
995
|
-
}
|
|
996
1199
|
|
|
997
|
-
|
|
998
|
-
let text = '';
|
|
999
|
-
try {
|
|
1000
|
-
text = JSON.stringify(value);
|
|
1001
|
-
} catch {
|
|
1002
|
-
text = '';
|
|
1003
|
-
}
|
|
1004
|
-
return Buffer.byteLength(text || '', 'utf8');
|
|
1200
|
+
return parsed;
|
|
1005
1201
|
}
|
|
1006
1202
|
|
|
1007
|
-
function
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1203
|
+
function parseStateArtifactRestoreArgs(kind, args) {
|
|
1204
|
+
const normalizedKind = kind === 'slot' ? 'slot' : 'checkpoint';
|
|
1205
|
+
const reasonCode = `invalid_state_${normalizedKind}_restore_args`;
|
|
1206
|
+
const parsed = {
|
|
1207
|
+
kind: normalizedKind,
|
|
1208
|
+
name: null,
|
|
1209
|
+
artifactPath: null,
|
|
1210
|
+
reportPath: null,
|
|
1211
|
+
file: null,
|
|
1212
|
+
mode: 'headless',
|
|
1213
|
+
frames: 1,
|
|
1214
|
+
schemaVersion: GAME_STATE_SCHEMA_VERSION,
|
|
1215
|
+
compact: false,
|
|
1216
|
+
dryRun: false,
|
|
1217
|
+
verify: false,
|
|
1218
|
+
rollbackOnFail: false,
|
|
1219
|
+
maxOps: DEFAULT_STATE_MUTATION_GUARDRAILS.maxMutations,
|
|
1220
|
+
maxBytes: DEFAULT_STATE_MUTATION_GUARDRAILS.maxPayloadBytes,
|
|
1221
|
+
maxRuntimeMs: DEFAULT_STATE_MUTATION_GUARDRAILS.maxRuntimeMs,
|
|
1222
|
+
auditPath: defaultStateApplyAuditPath(process.cwd()),
|
|
1013
1223
|
};
|
|
1014
|
-
}
|
|
1015
1224
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1225
|
+
let formattingFlag = 'pretty';
|
|
1226
|
+
|
|
1227
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1228
|
+
const token = args[i];
|
|
1229
|
+
|
|
1230
|
+
if (token === '--artifact') {
|
|
1231
|
+
parsed.artifactPath = normalizeStateArtifactFilePath(
|
|
1232
|
+
readRequiredOptionValue(args, i, '--artifact'),
|
|
1233
|
+
'--artifact',
|
|
1234
|
+
reasonCode,
|
|
1235
|
+
);
|
|
1236
|
+
i += 1;
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
if (token.startsWith('--artifact=')) {
|
|
1240
|
+
parsed.artifactPath = normalizeStateArtifactFilePath(
|
|
1241
|
+
token.slice('--artifact='.length),
|
|
1242
|
+
'--artifact',
|
|
1243
|
+
reasonCode,
|
|
1244
|
+
);
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (token === '--report') {
|
|
1249
|
+
parsed.reportPath = normalizeStateCommandOutput(
|
|
1250
|
+
readRequiredOptionValue(args, i, '--report'),
|
|
1251
|
+
reasonCode,
|
|
1252
|
+
);
|
|
1253
|
+
i += 1;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (token.startsWith('--report=')) {
|
|
1257
|
+
parsed.reportPath = normalizeStateCommandOutput(token.slice('--report='.length), reasonCode);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (token === '--file') {
|
|
1262
|
+
parsed.file = normalizeStateExportFile(readRequiredOptionValue(args, i, '--file'));
|
|
1263
|
+
i += 1;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (token.startsWith('--file=')) {
|
|
1267
|
+
parsed.file = normalizeStateExportFile(token.slice('--file='.length));
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (token === '--mode') {
|
|
1272
|
+
parsed.mode = normalizeStateExportMode(readRequiredOptionValue(args, i, '--mode'));
|
|
1273
|
+
i += 1;
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (token.startsWith('--mode=')) {
|
|
1277
|
+
parsed.mode = normalizeStateExportMode(token.slice('--mode='.length));
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (token === '--frames') {
|
|
1282
|
+
parsed.frames = parseNonNegativeIntegerArg(readRequiredOptionValue(args, i, '--frames'), '--frames', reasonCode);
|
|
1283
|
+
i += 1;
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (token.startsWith('--frames=')) {
|
|
1287
|
+
parsed.frames = parseNonNegativeIntegerArg(token.slice('--frames='.length), '--frames', reasonCode);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (token === '--schema' || token === '--schema-version') {
|
|
1292
|
+
parsed.schemaVersion = normalizeStateExportSchemaVersion(readRequiredOptionValue(args, i, token));
|
|
1293
|
+
i += 1;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (token.startsWith('--schema=')) {
|
|
1297
|
+
parsed.schemaVersion = normalizeStateExportSchemaVersion(token.slice('--schema='.length));
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (token.startsWith('--schema-version=')) {
|
|
1301
|
+
parsed.schemaVersion = normalizeStateExportSchemaVersion(token.slice('--schema-version='.length));
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (token === '--dry-run') {
|
|
1306
|
+
parsed.dryRun = true;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (token === '--verify') {
|
|
1310
|
+
parsed.verify = true;
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (token === '--rollback-on-fail') {
|
|
1314
|
+
parsed.rollbackOnFail = true;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (token === '--max-ops') {
|
|
1319
|
+
parsed.maxOps = parseNonNegativeIntegerArg(readRequiredOptionValue(args, i, '--max-ops'), '--max-ops', reasonCode);
|
|
1320
|
+
i += 1;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (token.startsWith('--max-ops=')) {
|
|
1324
|
+
parsed.maxOps = parseNonNegativeIntegerArg(token.slice('--max-ops='.length), '--max-ops', reasonCode);
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (token === '--max-bytes') {
|
|
1329
|
+
parsed.maxBytes = parseNonNegativeIntegerArg(readRequiredOptionValue(args, i, '--max-bytes'), '--max-bytes', reasonCode);
|
|
1330
|
+
i += 1;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (token.startsWith('--max-bytes=')) {
|
|
1334
|
+
parsed.maxBytes = parseNonNegativeIntegerArg(token.slice('--max-bytes='.length), '--max-bytes', reasonCode);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (token === '--max-runtime-ms') {
|
|
1339
|
+
parsed.maxRuntimeMs = parseNonNegativeIntegerArg(readRequiredOptionValue(args, i, '--max-runtime-ms'), '--max-runtime-ms', reasonCode);
|
|
1340
|
+
i += 1;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (token.startsWith('--max-runtime-ms=')) {
|
|
1344
|
+
parsed.maxRuntimeMs = parseNonNegativeIntegerArg(token.slice('--max-runtime-ms='.length), '--max-runtime-ms', reasonCode);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (token === '--audit') {
|
|
1349
|
+
parsed.auditPath = normalizeStateAuditOutput(readRequiredOptionValue(args, i, '--audit'), reasonCode);
|
|
1350
|
+
i += 1;
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (token.startsWith('--audit=')) {
|
|
1354
|
+
parsed.auditPath = normalizeStateAuditOutput(token.slice('--audit='.length), reasonCode);
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (token === '--compact') {
|
|
1359
|
+
if (formattingFlag === 'pretty') {
|
|
1360
|
+
parsed.compact = true;
|
|
1361
|
+
formattingFlag = 'compact';
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (formattingFlag === 'compact') continue;
|
|
1365
|
+
throw new StateExportError(reasonCode, 'Cannot use --compact and --pretty together.');
|
|
1366
|
+
}
|
|
1367
|
+
if (token === '--pretty') {
|
|
1368
|
+
if (formattingFlag === 'compact') {
|
|
1369
|
+
throw new StateExportError(reasonCode, 'Cannot use --compact and --pretty together.');
|
|
1370
|
+
}
|
|
1371
|
+
parsed.compact = false;
|
|
1372
|
+
formattingFlag = 'pretty';
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (token.startsWith('--')) {
|
|
1377
|
+
throw new StateExportError(reasonCode, `Unknown ${normalizedKind} restore option: ${token}`);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (parsed.name !== null) {
|
|
1381
|
+
throw new StateExportError(
|
|
1382
|
+
reasonCode,
|
|
1383
|
+
`Unexpected extra argument "${token}". Usage: aura state ${normalizedKind} restore <name> [options]`,
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
parsed.name = normalizeStateArtifactName(token, { kind: normalizedKind });
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (!parsed.name && !parsed.artifactPath) {
|
|
1390
|
+
throw new StateExportError(
|
|
1391
|
+
reasonCode,
|
|
1392
|
+
`Missing ${normalizedKind} name. Usage: aura state ${normalizedKind} restore <name> [options]`,
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
if (parsed.schemaVersion !== GAME_STATE_SCHEMA_VERSION) {
|
|
1396
|
+
throw new StateExportError(
|
|
1397
|
+
'schema_version_mismatch',
|
|
1398
|
+
`Unsupported schema version "${parsed.schemaVersion}". Expected "${GAME_STATE_SCHEMA_VERSION}".`,
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
if (parsed.dryRun && parsed.rollbackOnFail) {
|
|
1402
|
+
throw new StateExportError(reasonCode, '--rollback-on-fail cannot be combined with --dry-run.');
|
|
1403
|
+
}
|
|
1404
|
+
if (!parsed.reportPath && parsed.auditPath === null) {
|
|
1405
|
+
throw new StateExportError(
|
|
1406
|
+
reasonCode,
|
|
1407
|
+
'--audit - cannot be combined with report output to stdout; set --report <file> or --audit <file>.',
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return parsed;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function parseStateArtifactListArgs(kind, args) {
|
|
1415
|
+
const normalizedKind = kind === 'slot' ? 'slot' : 'checkpoint';
|
|
1416
|
+
const reasonCode = `invalid_state_${normalizedKind}_list_args`;
|
|
1417
|
+
const parsed = {
|
|
1418
|
+
kind: normalizedKind,
|
|
1419
|
+
reportPath: null,
|
|
1420
|
+
compact: false,
|
|
1421
|
+
};
|
|
1422
|
+
let formattingFlag = 'pretty';
|
|
1423
|
+
|
|
1424
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1425
|
+
const token = args[i];
|
|
1426
|
+
|
|
1427
|
+
if (token === '--report') {
|
|
1428
|
+
parsed.reportPath = normalizeStateCommandOutput(
|
|
1429
|
+
readRequiredOptionValue(args, i, '--report'),
|
|
1430
|
+
reasonCode,
|
|
1431
|
+
);
|
|
1432
|
+
i += 1;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (token.startsWith('--report=')) {
|
|
1436
|
+
parsed.reportPath = normalizeStateCommandOutput(token.slice('--report='.length), reasonCode);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (token === '--compact') {
|
|
1440
|
+
if (formattingFlag === 'pretty') {
|
|
1441
|
+
parsed.compact = true;
|
|
1442
|
+
formattingFlag = 'compact';
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (formattingFlag === 'compact') continue;
|
|
1446
|
+
throw new StateExportError(reasonCode, 'Cannot use --compact and --pretty together.');
|
|
1447
|
+
}
|
|
1448
|
+
if (token === '--pretty') {
|
|
1449
|
+
if (formattingFlag === 'compact') {
|
|
1450
|
+
throw new StateExportError(reasonCode, 'Cannot use --compact and --pretty together.');
|
|
1451
|
+
}
|
|
1452
|
+
parsed.compact = false;
|
|
1453
|
+
formattingFlag = 'pretty';
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
throw new StateExportError(reasonCode, `Unknown ${normalizedKind} list option: ${token}`);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
return parsed;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function normalizeStateCommandInputPath(value, flagName, reasonCode) {
|
|
1463
|
+
const normalized = String(value || '').trim();
|
|
1464
|
+
if (normalized.length === 0) {
|
|
1465
|
+
throw new StateExportError(reasonCode, `${flagName} requires a non-empty file path.`);
|
|
1466
|
+
}
|
|
1467
|
+
return resolve(process.cwd(), normalized);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function normalizeStateCommandOutput(value, reasonCode) {
|
|
1471
|
+
const normalized = String(value || '').trim();
|
|
1472
|
+
if (normalized.length === 0) {
|
|
1473
|
+
throw new StateExportError(reasonCode, '--output requires a non-empty path or "-".');
|
|
1474
|
+
}
|
|
1475
|
+
if (normalized === '-') {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
return resolve(process.cwd(), normalized);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function normalizeStateAuditOutput(value, reasonCode) {
|
|
1482
|
+
const normalized = String(value || '').trim();
|
|
1483
|
+
if (normalized.length === 0) {
|
|
1484
|
+
throw new StateExportError(reasonCode, '--audit requires a non-empty path or "-".');
|
|
1485
|
+
}
|
|
1486
|
+
if (normalized === '-') {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
return resolve(process.cwd(), normalized);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function normalizeStateArtifactFilePath(value, flagName, reasonCode) {
|
|
1493
|
+
const normalized = String(value || '').trim();
|
|
1494
|
+
if (normalized.length === 0 || normalized === '-') {
|
|
1495
|
+
throw new StateExportError(reasonCode, `${flagName} requires a writable file path.`);
|
|
1496
|
+
}
|
|
1497
|
+
return resolve(process.cwd(), normalized);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function normalizeOptionalNote(value) {
|
|
1501
|
+
const normalized = String(value || '').trim();
|
|
1502
|
+
return normalized.length > 0 ? normalized : null;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function defaultStatePatchAuditPath(projectRoot) {
|
|
1506
|
+
return resolve(projectRoot, '.aura/state/state-patch-audit.json');
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function defaultStateApplyAuditPath(projectRoot) {
|
|
1510
|
+
return resolve(projectRoot, '.aura/state/state-apply-audit.json');
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function readJsonInputFile(filePath, label, reasonCodePrefix) {
|
|
1514
|
+
let text;
|
|
1515
|
+
try {
|
|
1516
|
+
text = readFileSync(filePath, 'utf8');
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
throw new StateExportError(
|
|
1519
|
+
`${reasonCodePrefix}_read_failed`,
|
|
1520
|
+
`Failed to read ${label} "${filePath}": ${error.message}`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
return JSON.parse(text);
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
throw new StateExportError(
|
|
1527
|
+
`${reasonCodePrefix}_invalid_json`,
|
|
1528
|
+
`${label} "${filePath}" is not valid JSON: ${error.message}`,
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function writeStateJsonOutput(filePath, jsonText, reasonCode) {
|
|
1534
|
+
if (!filePath) {
|
|
1535
|
+
process.stdout.write(jsonText);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
try {
|
|
1540
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
1541
|
+
writeFileSync(filePath, jsonText, 'utf8');
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
throw new StateExportError(
|
|
1544
|
+
reasonCode,
|
|
1545
|
+
`Failed to write state output "${filePath}": ${error.message}`,
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function writeStateMutationAudit(auditPath, payload) {
|
|
1551
|
+
const text = `${JSON.stringify(payload, null, 2)}\n`;
|
|
1552
|
+
if (auditPath === null) {
|
|
1553
|
+
process.stdout.write(text);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
mkdirSync(dirname(auditPath), { recursive: true });
|
|
1558
|
+
writeFileSync(auditPath, text, 'utf8');
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
throw new StateExportError(
|
|
1561
|
+
'state_mutation_audit_write_failed',
|
|
1562
|
+
`Failed to write state mutation audit "${auditPath}": ${error.message}`,
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function jsonByteSize(value) {
|
|
1568
|
+
let text = '';
|
|
1569
|
+
try {
|
|
1570
|
+
text = JSON.stringify(value);
|
|
1571
|
+
} catch {
|
|
1572
|
+
text = '';
|
|
1573
|
+
}
|
|
1574
|
+
return Buffer.byteLength(text || '', 'utf8');
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function mutationGuardrailsFromParsed(parsed) {
|
|
1578
|
+
return {
|
|
1579
|
+
maxMutations: parsed.maxOps,
|
|
1580
|
+
maxPayloadBytes: parsed.maxBytes,
|
|
1581
|
+
maxRuntimeMs: parsed.maxRuntimeMs,
|
|
1582
|
+
allowlistPrefixes: STATE_MUTATION_ALLOWLIST_PREFIXES,
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function readRequiredOptionValue(args, index, flagName) {
|
|
1587
|
+
const value = args[index + 1];
|
|
1588
|
+
if (!value) {
|
|
1589
|
+
throw new StateExportError('invalid_state_export_args', `Missing value for ${flagName}.`);
|
|
1020
1590
|
}
|
|
1021
1591
|
return value;
|
|
1022
1592
|
}
|
|
@@ -1199,7 +1769,7 @@ async function cmdInit(args) {
|
|
|
1199
1769
|
async function cmdCreate(args) {
|
|
1200
1770
|
const parsed = parseCreateArgs(args);
|
|
1201
1771
|
if (!parsed.name) {
|
|
1202
|
-
error(
|
|
1772
|
+
error(`Missing project name. Usage: ${CREATE_USAGE}`);
|
|
1203
1773
|
}
|
|
1204
1774
|
|
|
1205
1775
|
const name = parsed.name;
|
|
@@ -1372,14 +1942,22 @@ async function cmdDevWeb(_args) {
|
|
|
1372
1942
|
});
|
|
1373
1943
|
}
|
|
1374
1944
|
|
|
1375
|
-
async function cmdDevNative() {
|
|
1945
|
+
async function cmdDevNative(devArgs = {}) {
|
|
1376
1946
|
const projectRoot = process.cwd();
|
|
1377
1947
|
const config = await loadConfig({ projectRoot, mode: 'dev' });
|
|
1378
1948
|
const identity = resolveBuildIdentity(config);
|
|
1379
1949
|
const hostBinary = resolveAndCacheHostBinary();
|
|
1950
|
+
const devEntry = devArgs.restoreKind
|
|
1951
|
+
? prepareDevStateRestoreEntry({
|
|
1952
|
+
projectRoot,
|
|
1953
|
+
entryFile: config.build.entry,
|
|
1954
|
+
artifactKind: devArgs.restoreKind,
|
|
1955
|
+
artifactName: devArgs.restoreName,
|
|
1956
|
+
})
|
|
1957
|
+
: null;
|
|
1380
1958
|
const incremental = createIncrementalBundler({
|
|
1381
1959
|
projectRoot,
|
|
1382
|
-
entryFile: config.build.entry,
|
|
1960
|
+
entryFile: devEntry ? devEntry.entryFile : config.build.entry,
|
|
1383
1961
|
});
|
|
1384
1962
|
|
|
1385
1963
|
let shuttingDown = false;
|
|
@@ -1508,6 +2086,11 @@ async function cmdDevNative() {
|
|
|
1508
2086
|
for (const diagnostic of hostBinary.diagnostics || []) {
|
|
1509
2087
|
console.log(` Host diagnostic: ${diagnostic}`);
|
|
1510
2088
|
}
|
|
2089
|
+
if (devEntry) {
|
|
2090
|
+
console.log(` Dev restore: ${devEntry.artifact.kind} "${devEntry.artifact.name}"`);
|
|
2091
|
+
console.log(` Dev restore artifact: ${devEntry.artifactPath}`);
|
|
2092
|
+
console.log(` Dev restore wrapper: ${devEntry.wrapperPath}`);
|
|
2093
|
+
}
|
|
1511
2094
|
startHost(initial.outFile, 'initial');
|
|
1512
2095
|
console.log('\n Watching src/ for changes... (Ctrl+C to stop)');
|
|
1513
2096
|
|
|
@@ -1553,12 +2136,19 @@ async function cmdDevNative() {
|
|
|
1553
2136
|
}
|
|
1554
2137
|
|
|
1555
2138
|
async function cmdDev(args) {
|
|
1556
|
-
const
|
|
2139
|
+
const parsed = parseDevArgs(args);
|
|
2140
|
+
const target = parsed.target || parseTarget(args);
|
|
1557
2141
|
if (target === 'web') {
|
|
2142
|
+
if (parsed.restoreKind) {
|
|
2143
|
+
throw new StateExportError(
|
|
2144
|
+
'invalid_dev_args',
|
|
2145
|
+
'--restore-slot and --restore-checkpoint are only supported for native `aura dev`.',
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
1558
2148
|
await cmdDevWeb(args);
|
|
1559
2149
|
return;
|
|
1560
2150
|
}
|
|
1561
|
-
await cmdDevNative(
|
|
2151
|
+
await cmdDevNative(parsed);
|
|
1562
2152
|
}
|
|
1563
2153
|
|
|
1564
2154
|
async function cmdBuild(args) {
|
|
@@ -1805,8 +2395,7 @@ async function cmdTest(args) {
|
|
|
1805
2395
|
console.log('');
|
|
1806
2396
|
}
|
|
1807
2397
|
|
|
1808
|
-
async function
|
|
1809
|
-
const parsed = parseStateExportArgs(args);
|
|
2398
|
+
async function runStateExportPayload(parsed) {
|
|
1810
2399
|
const projectRoot = process.cwd();
|
|
1811
2400
|
const config = await loadConfig({ projectRoot, mode: 'build' });
|
|
1812
2401
|
const file = parsed.file || config.build.entry;
|
|
@@ -1819,210 +2408,87 @@ async function cmdStateExport(args) {
|
|
|
1819
2408
|
frames: parsed.frames,
|
|
1820
2409
|
mode: parsed.mode,
|
|
1821
2410
|
frameIndex: parsed.frames,
|
|
1822
|
-
});
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
if (error instanceof HeadlessTestError) {
|
|
1825
|
-
throw new StateExportError(
|
|
1826
|
-
'state_export_runtime_failed',
|
|
1827
|
-
error.message,
|
|
1828
|
-
);
|
|
1829
|
-
}
|
|
1830
|
-
throw error;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
const exportResult = runResult.exportResult;
|
|
1834
|
-
if (!exportResult || typeof exportResult !== 'object') {
|
|
1835
|
-
throw new StateExportError(
|
|
1836
|
-
'state_export_runtime_failed',
|
|
1837
|
-
'State export runtime returned an invalid export result.',
|
|
1838
|
-
);
|
|
1839
|
-
}
|
|
1840
|
-
if (exportResult.ok !== true) {
|
|
1841
|
-
throw new StateExportError(
|
|
1842
|
-
typeof exportResult.reasonCode === 'string' ? exportResult.reasonCode : 'state_export_failed',
|
|
1843
|
-
'State export hook returned a failure result.',
|
|
1844
|
-
{ exportResult },
|
|
1845
|
-
);
|
|
1846
|
-
}
|
|
1847
|
-
if (!exportResult.payload || typeof exportResult.payload !== 'object') {
|
|
1848
|
-
throw new StateExportError(
|
|
1849
|
-
'state_export_invalid_payload',
|
|
1850
|
-
'State export hook returned an invalid payload.',
|
|
1851
|
-
{ exportResult },
|
|
1852
|
-
);
|
|
1853
|
-
}
|
|
1854
|
-
if (exportResult.payload.schemaVersion !== parsed.schemaVersion) {
|
|
1855
|
-
throw new StateExportError(
|
|
1856
|
-
'schema_version_mismatch',
|
|
1857
|
-
`Exported schema "${exportResult.payload.schemaVersion}" does not match requested schema "${parsed.schemaVersion}".`,
|
|
1858
|
-
{ exported: exportResult.payload.schemaVersion, requested: parsed.schemaVersion },
|
|
1859
|
-
);
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
const jsonText = parsed.compact
|
|
1863
|
-
? `${JSON.stringify(exportResult.payload)}\n`
|
|
1864
|
-
: `${JSON.stringify(exportResult.payload, null, 2)}\n`;
|
|
1865
|
-
|
|
1866
|
-
writeStateJsonOutput(parsed.outputPath, jsonText, 'state_export_output_write_failed');
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
async function cmdStateDiff(args) {
|
|
1870
|
-
const parsed = parseStateDiffArgs(args);
|
|
1871
|
-
const beforePayload = readJsonInputFile(parsed.beforePath, 'state diff before payload', 'state_diff_before');
|
|
1872
|
-
const afterPayload = readJsonInputFile(parsed.afterPath, 'state diff after payload', 'state_diff_after');
|
|
1873
|
-
|
|
1874
|
-
const diffResult = diffCanonicalGameState(beforePayload, afterPayload);
|
|
1875
|
-
if (!diffResult.ok) {
|
|
1876
|
-
throw new StateExportError(
|
|
1877
|
-
typeof diffResult.reasonCode === 'string' ? diffResult.reasonCode : 'state_diff_failed',
|
|
1878
|
-
'State diff operation failed.',
|
|
1879
|
-
{ diffResult },
|
|
1880
|
-
);
|
|
1881
|
-
}
|
|
1882
|
-
if (!diffResult.patch || typeof diffResult.patch !== 'object') {
|
|
1883
|
-
throw new StateExportError('state_diff_failed', 'State diff returned an invalid patch payload.');
|
|
1884
|
-
}
|
|
1885
|
-
if (diffResult.patch.schemaVersion !== parsed.schemaVersion) {
|
|
1886
|
-
throw new StateExportError(
|
|
1887
|
-
'schema_version_mismatch',
|
|
1888
|
-
`Diff schema "${diffResult.patch.schemaVersion}" does not match requested schema "${parsed.schemaVersion}".`,
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
const jsonText = parsed.compact
|
|
1893
|
-
? `${JSON.stringify(diffResult.patch)}\n`
|
|
1894
|
-
: `${JSON.stringify(diffResult.patch, null, 2)}\n`;
|
|
1895
|
-
writeStateJsonOutput(parsed.outputPath, jsonText, 'state_diff_output_write_failed');
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
async function cmdStatePatch(args) {
|
|
1899
|
-
const projectRoot = process.cwd();
|
|
1900
|
-
let parsed = null;
|
|
1901
|
-
let auditPath = defaultStatePatchAuditPath(projectRoot);
|
|
1902
|
-
let pendingError = null;
|
|
1903
|
-
const audit = {
|
|
1904
|
-
schemaVersion: STATE_MUTATION_AUDIT_SCHEMA_VERSION,
|
|
1905
|
-
command: 'state.patch',
|
|
1906
|
-
ok: false,
|
|
1907
|
-
reasonCode: 'state_patch_failed',
|
|
1908
|
-
limits: {
|
|
1909
|
-
maxOps: DEFAULT_STATE_MUTATION_GUARDRAILS.maxMutations,
|
|
1910
|
-
maxBytes: DEFAULT_STATE_MUTATION_GUARDRAILS.maxPayloadBytes,
|
|
1911
|
-
maxRuntimeMs: DEFAULT_STATE_MUTATION_GUARDRAILS.maxRuntimeMs,
|
|
1912
|
-
},
|
|
1913
|
-
allowlistPrefixes: [...STATE_MUTATION_ALLOWLIST_PREFIXES],
|
|
1914
|
-
io: {
|
|
1915
|
-
statePath: null,
|
|
1916
|
-
patchPath: null,
|
|
1917
|
-
outputPath: '-',
|
|
1918
|
-
auditPath: auditPath,
|
|
1919
|
-
},
|
|
1920
|
-
metrics: {
|
|
1921
|
-
statePayloadBytes: 0,
|
|
1922
|
-
patchPayloadBytes: 0,
|
|
1923
|
-
outputPayloadBytes: 0,
|
|
1924
|
-
mutationCount: 0,
|
|
1925
|
-
appliedMutations: 0,
|
|
1926
|
-
},
|
|
1927
|
-
detail: null,
|
|
1928
|
-
};
|
|
1929
|
-
|
|
1930
|
-
try {
|
|
1931
|
-
parsed = parseStatePatchArgs(args);
|
|
1932
|
-
auditPath = parsed.auditPath;
|
|
1933
|
-
audit.io.statePath = parsed.statePath;
|
|
1934
|
-
audit.io.patchPath = parsed.patchPath;
|
|
1935
|
-
audit.io.outputPath = parsed.outputPath || '-';
|
|
1936
|
-
audit.io.auditPath = parsed.auditPath || '-';
|
|
1937
|
-
audit.limits = {
|
|
1938
|
-
maxOps: parsed.maxOps,
|
|
1939
|
-
maxBytes: parsed.maxBytes,
|
|
1940
|
-
maxRuntimeMs: parsed.maxRuntimeMs,
|
|
1941
|
-
};
|
|
1942
|
-
|
|
1943
|
-
const statePayload = readJsonInputFile(parsed.statePath, 'state payload', 'state_patch_state');
|
|
1944
|
-
const patchPayload = readJsonInputFile(parsed.patchPath, 'state patch payload', 'state_patch_patch');
|
|
1945
|
-
audit.metrics.statePayloadBytes = jsonByteSize(statePayload);
|
|
1946
|
-
audit.metrics.patchPayloadBytes = jsonByteSize(patchPayload);
|
|
1947
|
-
audit.metrics.mutationCount = Array.isArray(patchPayload?.mutations) ? patchPayload.mutations.length : 0;
|
|
1948
|
-
|
|
1949
|
-
const patchResult = applyCanonicalStatePatch(
|
|
1950
|
-
statePayload,
|
|
1951
|
-
patchPayload,
|
|
1952
|
-
mutationGuardrailsFromParsed(parsed),
|
|
1953
|
-
);
|
|
1954
|
-
if (!patchResult.ok) {
|
|
1955
|
-
throw new StateExportError(
|
|
1956
|
-
typeof patchResult.reasonCode === 'string' ? patchResult.reasonCode : 'state_patch_failed',
|
|
1957
|
-
'State patch operation failed.',
|
|
1958
|
-
{ patchResult },
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
if (!patchResult.payload || typeof patchResult.payload !== 'object') {
|
|
1962
|
-
throw new StateExportError('state_patch_failed', 'State patch returned an invalid payload.');
|
|
1963
|
-
}
|
|
1964
|
-
if (patchResult.payload.schemaVersion !== parsed.schemaVersion) {
|
|
1965
|
-
throw new StateExportError(
|
|
1966
|
-
'schema_version_mismatch',
|
|
1967
|
-
`Patched schema "${patchResult.payload.schemaVersion}" does not match requested schema "${parsed.schemaVersion}".`,
|
|
1968
|
-
);
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
const jsonText = parsed.compact
|
|
1972
|
-
? `${JSON.stringify(patchResult.payload)}\n`
|
|
1973
|
-
: `${JSON.stringify(patchResult.payload, null, 2)}\n`;
|
|
1974
|
-
writeStateJsonOutput(parsed.outputPath, jsonText, 'state_patch_output_write_failed');
|
|
1975
|
-
|
|
1976
|
-
audit.ok = true;
|
|
1977
|
-
audit.reasonCode = 'state_patch_ok';
|
|
1978
|
-
audit.metrics.outputPayloadBytes = jsonByteSize(patchResult.payload);
|
|
1979
|
-
audit.metrics.appliedMutations = Number.isInteger(patchResult.appliedMutations) ? patchResult.appliedMutations : 0;
|
|
2411
|
+
});
|
|
1980
2412
|
} catch (error) {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
: 'state_patch_failed';
|
|
1986
|
-
audit.detail = error instanceof Error ? error.message : String(error);
|
|
2413
|
+
if (error instanceof HeadlessTestError) {
|
|
2414
|
+
throw new StateExportError('state_export_runtime_failed', error.message);
|
|
2415
|
+
}
|
|
2416
|
+
throw error;
|
|
1987
2417
|
}
|
|
1988
2418
|
|
|
1989
|
-
|
|
1990
|
-
if (
|
|
1991
|
-
throw
|
|
2419
|
+
const exportResult = runResult.exportResult;
|
|
2420
|
+
if (!exportResult || typeof exportResult !== 'object') {
|
|
2421
|
+
throw new StateExportError(
|
|
2422
|
+
'state_export_runtime_failed',
|
|
2423
|
+
'State export runtime returned an invalid export result.',
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
if (exportResult.ok !== true) {
|
|
2427
|
+
throw new StateExportError(
|
|
2428
|
+
typeof exportResult.reasonCode === 'string' ? exportResult.reasonCode : 'state_export_failed',
|
|
2429
|
+
'State export hook returned a failure result.',
|
|
2430
|
+
{ exportResult },
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
if (!exportResult.payload || typeof exportResult.payload !== 'object') {
|
|
2434
|
+
throw new StateExportError(
|
|
2435
|
+
'state_export_invalid_payload',
|
|
2436
|
+
'State export hook returned an invalid payload.',
|
|
2437
|
+
{ exportResult },
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
if (exportResult.payload.schemaVersion !== parsed.schemaVersion) {
|
|
2441
|
+
throw new StateExportError(
|
|
2442
|
+
'schema_version_mismatch',
|
|
2443
|
+
`Exported schema "${exportResult.payload.schemaVersion}" does not match requested schema "${parsed.schemaVersion}".`,
|
|
2444
|
+
{ exported: exportResult.payload.schemaVersion, requested: parsed.schemaVersion },
|
|
2445
|
+
);
|
|
1992
2446
|
}
|
|
2447
|
+
|
|
2448
|
+
return {
|
|
2449
|
+
projectRoot,
|
|
2450
|
+
file,
|
|
2451
|
+
payload: exportResult.payload,
|
|
2452
|
+
};
|
|
1993
2453
|
}
|
|
1994
2454
|
|
|
1995
|
-
async function
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2455
|
+
async function executeStateApplyOperation({
|
|
2456
|
+
parsed,
|
|
2457
|
+
payload,
|
|
2458
|
+
projectRoot = process.cwd(),
|
|
2459
|
+
file,
|
|
2460
|
+
outputPath = null,
|
|
2461
|
+
auditPath = defaultStateApplyAuditPath(projectRoot),
|
|
2462
|
+
inputPathLabel = null,
|
|
2463
|
+
commandName = 'state.apply',
|
|
2464
|
+
artifactMetadata = null,
|
|
2465
|
+
}) {
|
|
2000
2466
|
const audit = {
|
|
2001
2467
|
schemaVersion: STATE_MUTATION_AUDIT_SCHEMA_VERSION,
|
|
2002
|
-
command:
|
|
2468
|
+
command: commandName,
|
|
2003
2469
|
ok: false,
|
|
2004
2470
|
reasonCode: 'state_apply_failed',
|
|
2005
2471
|
limits: {
|
|
2006
|
-
maxOps:
|
|
2007
|
-
maxBytes:
|
|
2008
|
-
maxRuntimeMs:
|
|
2472
|
+
maxOps: parsed.maxOps,
|
|
2473
|
+
maxBytes: parsed.maxBytes,
|
|
2474
|
+
maxRuntimeMs: parsed.maxRuntimeMs,
|
|
2009
2475
|
},
|
|
2010
2476
|
allowlistPrefixes: [...STATE_MUTATION_ALLOWLIST_PREFIXES],
|
|
2011
2477
|
io: {
|
|
2012
|
-
statePath:
|
|
2013
|
-
file
|
|
2014
|
-
outputPath: '-',
|
|
2015
|
-
auditPath: auditPath,
|
|
2478
|
+
statePath: inputPathLabel || '-',
|
|
2479
|
+
file,
|
|
2480
|
+
outputPath: outputPath || '-',
|
|
2481
|
+
auditPath: auditPath || '-',
|
|
2016
2482
|
},
|
|
2017
2483
|
metrics: {
|
|
2018
|
-
payloadBytes:
|
|
2484
|
+
payloadBytes: jsonByteSize(payload),
|
|
2019
2485
|
outputPayloadBytes: 0,
|
|
2020
2486
|
mutationCount: 0,
|
|
2021
2487
|
},
|
|
2022
2488
|
result: {
|
|
2023
|
-
dryRun:
|
|
2024
|
-
verify:
|
|
2025
|
-
rollbackOnFail:
|
|
2489
|
+
dryRun: parsed.dryRun === true,
|
|
2490
|
+
verify: parsed.verify === true,
|
|
2491
|
+
rollbackOnFail: parsed.rollbackOnFail === true,
|
|
2026
2492
|
applied: false,
|
|
2027
2493
|
verified: false,
|
|
2028
2494
|
rolledBack: false,
|
|
@@ -2033,27 +2499,16 @@ async function cmdStateApply(args) {
|
|
|
2033
2499
|
detail: null,
|
|
2034
2500
|
};
|
|
2035
2501
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
audit.limits = {
|
|
2043
|
-
maxOps: parsed.maxOps,
|
|
2044
|
-
maxBytes: parsed.maxBytes,
|
|
2045
|
-
maxRuntimeMs: parsed.maxRuntimeMs,
|
|
2502
|
+
if (artifactMetadata) {
|
|
2503
|
+
audit.artifact = {
|
|
2504
|
+
kind: artifactMetadata.kind,
|
|
2505
|
+
name: artifactMetadata.name,
|
|
2506
|
+
path: artifactMetadata.path,
|
|
2507
|
+
payloadFingerprint: artifactMetadata.payloadFingerprint || null,
|
|
2046
2508
|
};
|
|
2047
|
-
|
|
2048
|
-
audit.result.verify = parsed.verify === true;
|
|
2049
|
-
audit.result.rollbackOnFail = parsed.rollbackOnFail === true;
|
|
2050
|
-
|
|
2051
|
-
const config = await loadConfig({ projectRoot, mode: 'build' });
|
|
2052
|
-
const file = parsed.file || config.build.entry;
|
|
2053
|
-
audit.io.file = file;
|
|
2054
|
-
const payload = readJsonInputFile(parsed.statePath, 'state apply payload', 'state_apply_input');
|
|
2055
|
-
audit.metrics.payloadBytes = jsonByteSize(payload);
|
|
2509
|
+
}
|
|
2056
2510
|
|
|
2511
|
+
try {
|
|
2057
2512
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
2058
2513
|
throw new StateExportError('invalid_schema_payload', 'State apply payload must be a JSON object.');
|
|
2059
2514
|
}
|
|
@@ -2088,6 +2543,7 @@ async function cmdStateApply(args) {
|
|
|
2088
2543
|
if (!runResult || typeof runResult !== 'object') {
|
|
2089
2544
|
throw new StateExportError('state_apply_runtime_failed', 'State apply runtime returned an invalid result.');
|
|
2090
2545
|
}
|
|
2546
|
+
|
|
2091
2547
|
if (runResult.ok !== true) {
|
|
2092
2548
|
const failureReport = {
|
|
2093
2549
|
ok: false,
|
|
@@ -2103,12 +2559,16 @@ async function cmdStateApply(args) {
|
|
|
2103
2559
|
baselineFingerprint: runResult.baselineFingerprint || null,
|
|
2104
2560
|
finalFingerprint: runResult.finalFingerprint || null,
|
|
2105
2561
|
};
|
|
2106
|
-
if (
|
|
2562
|
+
if (artifactMetadata) {
|
|
2563
|
+
failureReport.artifact = artifactMetadata;
|
|
2564
|
+
}
|
|
2565
|
+
if (outputPath) {
|
|
2107
2566
|
const failureText = parsed.compact
|
|
2108
2567
|
? `${JSON.stringify(failureReport)}\n`
|
|
2109
2568
|
: `${JSON.stringify(failureReport, null, 2)}\n`;
|
|
2110
|
-
writeStateJsonOutput(
|
|
2569
|
+
writeStateJsonOutput(outputPath, failureText, 'state_apply_output_write_failed');
|
|
2111
2570
|
}
|
|
2571
|
+
|
|
2112
2572
|
audit.ok = false;
|
|
2113
2573
|
audit.reasonCode = failureReport.reasonCode;
|
|
2114
2574
|
audit.metrics.mutationCount = failureReport.mutationCount;
|
|
@@ -2123,6 +2583,7 @@ async function cmdStateApply(args) {
|
|
|
2123
2583
|
baselineFingerprint: failureReport.baselineFingerprint,
|
|
2124
2584
|
finalFingerprint: failureReport.finalFingerprint,
|
|
2125
2585
|
};
|
|
2586
|
+
|
|
2126
2587
|
throw new StateExportError(
|
|
2127
2588
|
failureReport.reasonCode,
|
|
2128
2589
|
'State apply operation failed.',
|
|
@@ -2143,15 +2604,17 @@ async function cmdStateApply(args) {
|
|
|
2143
2604
|
baselineFingerprint: runResult.baselineFingerprint || null,
|
|
2144
2605
|
finalFingerprint: runResult.finalFingerprint || null,
|
|
2145
2606
|
};
|
|
2146
|
-
|
|
2147
2607
|
if (runResult.previewPatch && typeof runResult.previewPatch === 'object') {
|
|
2148
2608
|
report.previewPatch = runResult.previewPatch;
|
|
2149
2609
|
}
|
|
2610
|
+
if (artifactMetadata) {
|
|
2611
|
+
report.artifact = artifactMetadata;
|
|
2612
|
+
}
|
|
2150
2613
|
|
|
2151
2614
|
const jsonText = parsed.compact
|
|
2152
2615
|
? `${JSON.stringify(report)}\n`
|
|
2153
2616
|
: `${JSON.stringify(report, null, 2)}\n`;
|
|
2154
|
-
writeStateJsonOutput(
|
|
2617
|
+
writeStateJsonOutput(outputPath, jsonText, 'state_apply_output_write_failed');
|
|
2155
2618
|
|
|
2156
2619
|
audit.ok = true;
|
|
2157
2620
|
audit.reasonCode = report.reasonCode;
|
|
@@ -2168,12 +2631,144 @@ async function cmdStateApply(args) {
|
|
|
2168
2631
|
baselineFingerprint: report.baselineFingerprint,
|
|
2169
2632
|
finalFingerprint: report.finalFingerprint,
|
|
2170
2633
|
};
|
|
2634
|
+
|
|
2635
|
+
return report;
|
|
2171
2636
|
} catch (error) {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2637
|
+
audit.reasonCode = error instanceof StateExportError ? error.reasonCode : 'state_apply_failed';
|
|
2638
|
+
audit.detail = error instanceof Error ? error.message : String(error);
|
|
2639
|
+
throw error;
|
|
2640
|
+
} finally {
|
|
2641
|
+
writeStateMutationAudit(auditPath, audit);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
async function cmdStateExport(args) {
|
|
2646
|
+
const parsed = parseStateExportArgs(args);
|
|
2647
|
+
const { payload } = await runStateExportPayload(parsed);
|
|
2648
|
+
const jsonText = parsed.compact
|
|
2649
|
+
? `${JSON.stringify(payload)}\n`
|
|
2650
|
+
: `${JSON.stringify(payload, null, 2)}\n`;
|
|
2651
|
+
|
|
2652
|
+
writeStateJsonOutput(parsed.outputPath, jsonText, 'state_export_output_write_failed');
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
async function cmdStateDiff(args) {
|
|
2656
|
+
const parsed = parseStateDiffArgs(args);
|
|
2657
|
+
const beforePayload = readJsonInputFile(parsed.beforePath, 'state diff before payload', 'state_diff_before');
|
|
2658
|
+
const afterPayload = readJsonInputFile(parsed.afterPath, 'state diff after payload', 'state_diff_after');
|
|
2659
|
+
|
|
2660
|
+
const diffResult = diffCanonicalGameState(beforePayload, afterPayload);
|
|
2661
|
+
if (!diffResult.ok) {
|
|
2662
|
+
throw new StateExportError(
|
|
2663
|
+
typeof diffResult.reasonCode === 'string' ? diffResult.reasonCode : 'state_diff_failed',
|
|
2664
|
+
'State diff operation failed.',
|
|
2665
|
+
{ diffResult },
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
if (!diffResult.patch || typeof diffResult.patch !== 'object') {
|
|
2669
|
+
throw new StateExportError('state_diff_failed', 'State diff returned an invalid patch payload.');
|
|
2670
|
+
}
|
|
2671
|
+
if (diffResult.patch.schemaVersion !== parsed.schemaVersion) {
|
|
2672
|
+
throw new StateExportError(
|
|
2673
|
+
'schema_version_mismatch',
|
|
2674
|
+
`Diff schema "${diffResult.patch.schemaVersion}" does not match requested schema "${parsed.schemaVersion}".`,
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
const jsonText = parsed.compact
|
|
2679
|
+
? `${JSON.stringify(diffResult.patch)}\n`
|
|
2680
|
+
: `${JSON.stringify(diffResult.patch, null, 2)}\n`;
|
|
2681
|
+
writeStateJsonOutput(parsed.outputPath, jsonText, 'state_diff_output_write_failed');
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
async function cmdStatePatch(args) {
|
|
2685
|
+
const projectRoot = process.cwd();
|
|
2686
|
+
let parsed = null;
|
|
2687
|
+
let auditPath = defaultStatePatchAuditPath(projectRoot);
|
|
2688
|
+
let pendingError = null;
|
|
2689
|
+
const audit = {
|
|
2690
|
+
schemaVersion: STATE_MUTATION_AUDIT_SCHEMA_VERSION,
|
|
2691
|
+
command: 'state.patch',
|
|
2692
|
+
ok: false,
|
|
2693
|
+
reasonCode: 'state_patch_failed',
|
|
2694
|
+
limits: {
|
|
2695
|
+
maxOps: DEFAULT_STATE_MUTATION_GUARDRAILS.maxMutations,
|
|
2696
|
+
maxBytes: DEFAULT_STATE_MUTATION_GUARDRAILS.maxPayloadBytes,
|
|
2697
|
+
maxRuntimeMs: DEFAULT_STATE_MUTATION_GUARDRAILS.maxRuntimeMs,
|
|
2698
|
+
},
|
|
2699
|
+
allowlistPrefixes: [...STATE_MUTATION_ALLOWLIST_PREFIXES],
|
|
2700
|
+
io: {
|
|
2701
|
+
statePath: null,
|
|
2702
|
+
patchPath: null,
|
|
2703
|
+
outputPath: '-',
|
|
2704
|
+
auditPath: auditPath,
|
|
2705
|
+
},
|
|
2706
|
+
metrics: {
|
|
2707
|
+
statePayloadBytes: 0,
|
|
2708
|
+
patchPayloadBytes: 0,
|
|
2709
|
+
outputPayloadBytes: 0,
|
|
2710
|
+
mutationCount: 0,
|
|
2711
|
+
appliedMutations: 0,
|
|
2712
|
+
},
|
|
2713
|
+
detail: null,
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
try {
|
|
2717
|
+
parsed = parseStatePatchArgs(args);
|
|
2718
|
+
auditPath = parsed.auditPath;
|
|
2719
|
+
audit.io.statePath = parsed.statePath;
|
|
2720
|
+
audit.io.patchPath = parsed.patchPath;
|
|
2721
|
+
audit.io.outputPath = parsed.outputPath || '-';
|
|
2722
|
+
audit.io.auditPath = parsed.auditPath || '-';
|
|
2723
|
+
audit.limits = {
|
|
2724
|
+
maxOps: parsed.maxOps,
|
|
2725
|
+
maxBytes: parsed.maxBytes,
|
|
2726
|
+
maxRuntimeMs: parsed.maxRuntimeMs,
|
|
2727
|
+
};
|
|
2728
|
+
|
|
2729
|
+
const statePayload = readJsonInputFile(parsed.statePath, 'state payload', 'state_patch_state');
|
|
2730
|
+
const patchPayload = readJsonInputFile(parsed.patchPath, 'state patch payload', 'state_patch_patch');
|
|
2731
|
+
audit.metrics.statePayloadBytes = jsonByteSize(statePayload);
|
|
2732
|
+
audit.metrics.patchPayloadBytes = jsonByteSize(patchPayload);
|
|
2733
|
+
audit.metrics.mutationCount = Array.isArray(patchPayload?.mutations) ? patchPayload.mutations.length : 0;
|
|
2734
|
+
|
|
2735
|
+
const patchResult = applyCanonicalStatePatch(
|
|
2736
|
+
statePayload,
|
|
2737
|
+
patchPayload,
|
|
2738
|
+
mutationGuardrailsFromParsed(parsed),
|
|
2739
|
+
);
|
|
2740
|
+
if (!patchResult.ok) {
|
|
2741
|
+
throw new StateExportError(
|
|
2742
|
+
typeof patchResult.reasonCode === 'string' ? patchResult.reasonCode : 'state_patch_failed',
|
|
2743
|
+
'State patch operation failed.',
|
|
2744
|
+
{ patchResult },
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
if (!patchResult.payload || typeof patchResult.payload !== 'object') {
|
|
2748
|
+
throw new StateExportError('state_patch_failed', 'State patch returned an invalid payload.');
|
|
2749
|
+
}
|
|
2750
|
+
if (patchResult.payload.schemaVersion !== parsed.schemaVersion) {
|
|
2751
|
+
throw new StateExportError(
|
|
2752
|
+
'schema_version_mismatch',
|
|
2753
|
+
`Patched schema "${patchResult.payload.schemaVersion}" does not match requested schema "${parsed.schemaVersion}".`,
|
|
2754
|
+
);
|
|
2175
2755
|
}
|
|
2756
|
+
|
|
2757
|
+
const jsonText = parsed.compact
|
|
2758
|
+
? `${JSON.stringify(patchResult.payload)}\n`
|
|
2759
|
+
: `${JSON.stringify(patchResult.payload, null, 2)}\n`;
|
|
2760
|
+
writeStateJsonOutput(parsed.outputPath, jsonText, 'state_patch_output_write_failed');
|
|
2761
|
+
|
|
2762
|
+
audit.ok = true;
|
|
2763
|
+
audit.reasonCode = 'state_patch_ok';
|
|
2764
|
+
audit.metrics.outputPayloadBytes = jsonByteSize(patchResult.payload);
|
|
2765
|
+
audit.metrics.appliedMutations = Number.isInteger(patchResult.appliedMutations) ? patchResult.appliedMutations : 0;
|
|
2766
|
+
} catch (error) {
|
|
2767
|
+
pendingError = error;
|
|
2176
2768
|
audit.ok = false;
|
|
2769
|
+
audit.reasonCode = error instanceof StateExportError
|
|
2770
|
+
? error.reasonCode
|
|
2771
|
+
: 'state_patch_failed';
|
|
2177
2772
|
audit.detail = error instanceof Error ? error.message : String(error);
|
|
2178
2773
|
}
|
|
2179
2774
|
|
|
@@ -2183,12 +2778,115 @@ async function cmdStateApply(args) {
|
|
|
2183
2778
|
}
|
|
2184
2779
|
}
|
|
2185
2780
|
|
|
2781
|
+
async function cmdStateApply(args) {
|
|
2782
|
+
const projectRoot = process.cwd();
|
|
2783
|
+
const parsed = parseStateApplyArgs(args);
|
|
2784
|
+
const config = await loadConfig({ projectRoot, mode: 'build' });
|
|
2785
|
+
const file = parsed.file || config.build.entry;
|
|
2786
|
+
const payload = readJsonInputFile(parsed.statePath, 'state apply payload', 'state_apply_input');
|
|
2787
|
+
|
|
2788
|
+
await executeStateApplyOperation({
|
|
2789
|
+
parsed,
|
|
2790
|
+
payload,
|
|
2791
|
+
projectRoot,
|
|
2792
|
+
file,
|
|
2793
|
+
outputPath: parsed.outputPath,
|
|
2794
|
+
auditPath: parsed.auditPath,
|
|
2795
|
+
inputPathLabel: parsed.statePath,
|
|
2796
|
+
commandName: 'state.apply',
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
async function cmdStateArtifactSave(kind, args) {
|
|
2801
|
+
const parsed = parseStateArtifactSaveArgs(kind, args);
|
|
2802
|
+
const { projectRoot, file, payload } = await runStateExportPayload(parsed);
|
|
2803
|
+
const artifactPath = parsed.artifactPath || resolveStateArtifactPath(projectRoot, parsed.kind, parsed.name);
|
|
2804
|
+
const envelope = createStateArtifactEnvelope({
|
|
2805
|
+
kind: parsed.kind,
|
|
2806
|
+
name: parsed.name,
|
|
2807
|
+
payload,
|
|
2808
|
+
note: parsed.note,
|
|
2809
|
+
source: {
|
|
2810
|
+
mode: parsed.mode,
|
|
2811
|
+
frames: parsed.frames,
|
|
2812
|
+
file,
|
|
2813
|
+
reportPath: parsed.reportPath || '-',
|
|
2814
|
+
},
|
|
2815
|
+
});
|
|
2816
|
+
writeStateArtifactEnvelope(artifactPath, envelope);
|
|
2817
|
+
|
|
2818
|
+
const report = {
|
|
2819
|
+
ok: true,
|
|
2820
|
+
reasonCode: parsed.kind === 'slot' ? 'state_slot_saved' : 'state_checkpoint_saved',
|
|
2821
|
+
artifact: {
|
|
2822
|
+
kind: parsed.kind,
|
|
2823
|
+
name: parsed.name,
|
|
2824
|
+
path: artifactPath,
|
|
2825
|
+
createdAt: envelope.createdAt,
|
|
2826
|
+
note: envelope.note || null,
|
|
2827
|
+
stateSchemaVersion: envelope.stateSchemaVersion,
|
|
2828
|
+
payloadFingerprint: envelope.payloadFingerprint,
|
|
2829
|
+
mode: payload?.export?.mode || null,
|
|
2830
|
+
frameIndex: payload?.export?.frameIndex ?? null,
|
|
2831
|
+
elapsedSeconds: payload?.export?.elapsedSeconds ?? null,
|
|
2832
|
+
},
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
const jsonText = parsed.compact
|
|
2836
|
+
? `${JSON.stringify(report)}\n`
|
|
2837
|
+
: `${JSON.stringify(report, null, 2)}\n`;
|
|
2838
|
+
writeStateJsonOutput(parsed.reportPath, jsonText, 'state_artifact_report_write_failed');
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
async function cmdStateArtifactRestore(kind, args) {
|
|
2842
|
+
const projectRoot = process.cwd();
|
|
2843
|
+
const parsed = parseStateArtifactRestoreArgs(kind, args);
|
|
2844
|
+
const artifact = loadStateArtifact({
|
|
2845
|
+
projectRoot,
|
|
2846
|
+
kind: parsed.kind,
|
|
2847
|
+
name: parsed.name,
|
|
2848
|
+
artifactPath: parsed.artifactPath,
|
|
2849
|
+
});
|
|
2850
|
+
const config = await loadConfig({ projectRoot, mode: 'build' });
|
|
2851
|
+
const file = parsed.file || config.build.entry;
|
|
2852
|
+
|
|
2853
|
+
await executeStateApplyOperation({
|
|
2854
|
+
parsed,
|
|
2855
|
+
payload: artifact.envelope.payload,
|
|
2856
|
+
projectRoot,
|
|
2857
|
+
file,
|
|
2858
|
+
outputPath: parsed.reportPath,
|
|
2859
|
+
auditPath: parsed.auditPath,
|
|
2860
|
+
inputPathLabel: artifact.filePath,
|
|
2861
|
+
commandName: parsed.kind === 'slot' ? 'state.slot.restore' : 'state.checkpoint.restore',
|
|
2862
|
+
artifactMetadata: artifact.metadata,
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
async function cmdStateArtifactList(kind, args) {
|
|
2867
|
+
const projectRoot = process.cwd();
|
|
2868
|
+
const parsed = parseStateArtifactListArgs(kind, args);
|
|
2869
|
+
const listing = listStateArtifacts(projectRoot, parsed.kind);
|
|
2870
|
+
const report = {
|
|
2871
|
+
ok: true,
|
|
2872
|
+
reasonCode: parsed.kind === 'slot' ? 'state_slot_list_ok' : 'state_checkpoint_list_ok',
|
|
2873
|
+
kind: parsed.kind,
|
|
2874
|
+
directory: listing.directory,
|
|
2875
|
+
artifacts: listing.artifacts,
|
|
2876
|
+
invalidArtifacts: listing.invalidArtifacts,
|
|
2877
|
+
};
|
|
2878
|
+
const jsonText = parsed.compact
|
|
2879
|
+
? `${JSON.stringify(report)}\n`
|
|
2880
|
+
: `${JSON.stringify(report, null, 2)}\n`;
|
|
2881
|
+
writeStateJsonOutput(parsed.reportPath, jsonText, 'state_artifact_report_write_failed');
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2186
2884
|
async function cmdState(args) {
|
|
2187
2885
|
const subcommand = args[0];
|
|
2188
2886
|
if (!subcommand) {
|
|
2189
2887
|
throw new StateExportError(
|
|
2190
2888
|
'state_subcommand_required',
|
|
2191
|
-
'Missing state subcommand. Usage: aura state <export|diff|patch> [options]',
|
|
2889
|
+
'Missing state subcommand. Usage: aura state <export|diff|patch|apply|slot|checkpoint> [options]',
|
|
2192
2890
|
);
|
|
2193
2891
|
}
|
|
2194
2892
|
|
|
@@ -2212,9 +2910,35 @@ async function cmdState(args) {
|
|
|
2212
2910
|
return;
|
|
2213
2911
|
}
|
|
2214
2912
|
|
|
2913
|
+
if (subcommand === 'slot' || subcommand === 'checkpoint') {
|
|
2914
|
+
const artifactAction = args[1];
|
|
2915
|
+
if (!artifactAction) {
|
|
2916
|
+
throw new StateExportError(
|
|
2917
|
+
'state_subcommand_unsupported',
|
|
2918
|
+
`Missing ${subcommand} action. Supported actions: save, restore, list.`,
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
if (artifactAction === 'save') {
|
|
2922
|
+
await cmdStateArtifactSave(subcommand, args.slice(2));
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (artifactAction === 'restore') {
|
|
2926
|
+
await cmdStateArtifactRestore(subcommand, args.slice(2));
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (artifactAction === 'list') {
|
|
2930
|
+
await cmdStateArtifactList(subcommand, args.slice(2));
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
throw new StateExportError(
|
|
2934
|
+
'state_subcommand_unsupported',
|
|
2935
|
+
`Unknown ${subcommand} action "${artifactAction}". Supported actions: save, restore, list.`,
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2215
2939
|
throw new StateExportError(
|
|
2216
2940
|
'state_subcommand_unsupported',
|
|
2217
|
-
`Unknown state subcommand "${subcommand}". Supported subcommands: export, diff, patch, apply.`,
|
|
2941
|
+
`Unknown state subcommand "${subcommand}". Supported subcommands: export, diff, patch, apply, slot, checkpoint.`,
|
|
2218
2942
|
);
|
|
2219
2943
|
}
|
|
2220
2944
|
|
|
@@ -2280,6 +3004,9 @@ async function main() {
|
|
|
2280
3004
|
if (err instanceof StateExportError) {
|
|
2281
3005
|
error(`${err.message} (reasonCode=${err.reasonCode})`, 6);
|
|
2282
3006
|
}
|
|
3007
|
+
if (err instanceof StateArtifactError) {
|
|
3008
|
+
error(`${err.message} (reasonCode=${err.reasonCode})`, 6);
|
|
3009
|
+
}
|
|
2283
3010
|
if (err instanceof HeadlessTestError) {
|
|
2284
3011
|
if (Array.isArray(err.details?.failures) && err.details.failures.length > 0) {
|
|
2285
3012
|
console.error('\n aura test failures:');
|