@bedrock-rbx/core 0.1.0-beta.11 → 0.1.0-beta.13
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/cli/run.mjs +54 -210
- package/dist/cli/run.mjs.map +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/{define-config-87u2jqjM.d.mts → define-config-Bd0XIiSX.d.mts} +155 -8
- package/dist/{define-config-87u2jqjM.d.mts.map → define-config-Bd0XIiSX.d.mts.map} +1 -1
- package/dist/index.d.mts +1847 -1637
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +26 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-mantle-state-DjAU-I39.mjs → migrate-mantle-state-CQjWBZwT.mjs} +857 -79
- package/dist/migrate-mantle-state-CQjWBZwT.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/migrate-mantle-state-DjAU-I39.mjs.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { ApiError, PermissionError } from "@bedrock-rbx/ocale";
|
|
1
2
|
import { ArkErrors, type } from "arktype";
|
|
2
|
-
import {
|
|
3
|
+
import { cancel, intro, log, outro } from "@clack/prompts";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import { defu } from "defu";
|
|
5
6
|
import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
@@ -13,6 +14,220 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
13
14
|
import { execFile } from "node:child_process";
|
|
14
15
|
import { tmpdir } from "node:os";
|
|
15
16
|
import { parseYAML, stringifyYAML } from "confbox";
|
|
17
|
+
//#region src/cli/render.ts
|
|
18
|
+
/**
|
|
19
|
+
* Render a `DeployError` to the supplied `ClackPort` as a single error line.
|
|
20
|
+
* Each variant produces a distinct, terse diagnostic; wrapped variants
|
|
21
|
+
* (`applyFailed`, `buildDesiredFailed`, `configLoadFailed`, `stateReadFailed`,
|
|
22
|
+
* `stateWriteFailed`) surface the inner cause's actionable detail (file path,
|
|
23
|
+
* resource key, parser message, HTTP failure, validator issue) so the reader
|
|
24
|
+
* does not have to inspect the full cause to act.
|
|
25
|
+
* @param err - The deploy error to describe.
|
|
26
|
+
* @param port - The output port the diagnostic is written to.
|
|
27
|
+
*/
|
|
28
|
+
function renderDeployError(err, port) {
|
|
29
|
+
port.logError(deployErrorMessage(err));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render a `ParseOptionsError` to the supplied `ClackPort` as a single
|
|
33
|
+
* error line. Each variant names the offending flag so the diagnostic
|
|
34
|
+
* pinpoints what the caller needs to change.
|
|
35
|
+
* @param err - The parse error to describe.
|
|
36
|
+
* @param port - The output port the diagnostic is written to.
|
|
37
|
+
*/
|
|
38
|
+
function renderParseError(err, port) {
|
|
39
|
+
port.logError(parseErrorMessage(err));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Render a `ParseMigrateError` to the supplied `ClackPort`. Reuses
|
|
43
|
+
* `parseErrorMessage` for the three flag-shape variants and adds a
|
|
44
|
+
* dedicated message for `unknownSource` listing the supported sources.
|
|
45
|
+
* @param err - The parse error to describe.
|
|
46
|
+
* @param port - The output port the diagnostic is written to.
|
|
47
|
+
*/
|
|
48
|
+
function renderMigrateParseError(err, port) {
|
|
49
|
+
port.logError(migrateParseErrorMessage(err));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Render a `MigrateError` to the supplied `ClackPort` as a single error
|
|
53
|
+
* line. Each variant points at the offending Mantle state file path,
|
|
54
|
+
* primary-environment input, or wrapped `ConfigError` so the reader can
|
|
55
|
+
* act without inspecting the raw error object.
|
|
56
|
+
* @param err - The migrate error to describe.
|
|
57
|
+
* @param port - The output port the diagnostic is written to.
|
|
58
|
+
*/
|
|
59
|
+
function renderMigrateError(err, port) {
|
|
60
|
+
port.logError(migrateErrorMessage(err));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Render a `MissingCredentialError` or `UnsupportedBackendError`
|
|
64
|
+
* surfaced when the migrate command tried to default-construct the
|
|
65
|
+
* configured `StatePort` and was missing its inputs.
|
|
66
|
+
* @param err - The error returned by `buildStatePort`.
|
|
67
|
+
* @param port - The output port the diagnostic is written to.
|
|
68
|
+
*/
|
|
69
|
+
function renderBuildStatePortError(err, port) {
|
|
70
|
+
port.logError(buildStatePortErrorMessage(err));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Render the post-migrate review prompt to the supplied `ClackPort`.
|
|
74
|
+
* Three outcomes:
|
|
75
|
+
*
|
|
76
|
+
* - Any `ambiguous` warnings exist: emit a single error line directing
|
|
77
|
+
* the user to the report. The migration ran but there are decisions
|
|
78
|
+
* the user still needs to make before deploy will be meaningful.
|
|
79
|
+
* - No `ambiguous` warnings but non-zero `blocked` / `deferred` /
|
|
80
|
+
* `interpretive`: emit a single success line pointing at the report
|
|
81
|
+
* for auditing.
|
|
82
|
+
* - All counts zero: silent. The closing `outro("migrate succeeded")`
|
|
83
|
+
* already speaks for the run.
|
|
84
|
+
* @param input - Counts plus the path of the Markdown report.
|
|
85
|
+
* @param port - The output port the line is written to.
|
|
86
|
+
*/
|
|
87
|
+
function renderMigrationSummary(input, port) {
|
|
88
|
+
const { ambiguousCount, blockedCount, deferredCount, interpretiveCount } = input.summary;
|
|
89
|
+
if (ambiguousCount > 0) {
|
|
90
|
+
port.logError(`action required: ${String(ambiguousCount)} fields need your input. See ${input.reportPath}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const reviewable = blockedCount + deferredCount + interpretiveCount;
|
|
94
|
+
if (reviewable > 0) port.logSuccess(`migration complete; see ${input.reportPath} for ${String(reviewable)} auto-mapped or skipped fields`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Render a `StateError` produced when the migrator wrote a per-environment
|
|
98
|
+
* state through the `StatePort`. Names the environment alongside the
|
|
99
|
+
* adapter's failure reason so the reader knows which write failed.
|
|
100
|
+
* @param input - Environment + state-error to describe.
|
|
101
|
+
* @param port - The output port the diagnostic is written to.
|
|
102
|
+
*/
|
|
103
|
+
function renderStateWriteError(input, port) {
|
|
104
|
+
port.logError(`state write failed for '${input.environment}' (${input.err.file}): ${input.err.reason}`);
|
|
105
|
+
}
|
|
106
|
+
function permissionDetail(err) {
|
|
107
|
+
const isPlural = err.requiredScopes.length > 1;
|
|
108
|
+
const label = isPlural ? "scopes" : "scope";
|
|
109
|
+
const pronoun = isPlural ? "them" : "it";
|
|
110
|
+
const scopeList = err.requiredScopes.map((scope) => `'${scope}'`).join(", ");
|
|
111
|
+
return `${err.message} on ${err.operationKey}: missing required ${label} ${scopeList}. Grant ${pronoun} on the API key at https://create.roblox.com/credentials`;
|
|
112
|
+
}
|
|
113
|
+
function applyCauseDetail(cause) {
|
|
114
|
+
switch (cause.kind) {
|
|
115
|
+
case "driverFailure":
|
|
116
|
+
if (cause.cause instanceof PermissionError) return permissionDetail(cause.cause);
|
|
117
|
+
return cause.cause.message;
|
|
118
|
+
case "updateUnsupported": return "update not supported";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function buildDesiredDetail(cause) {
|
|
122
|
+
switch (cause.kind) {
|
|
123
|
+
case "fileReadFailed": return `(${cause.filePath}): ${cause.reason}`;
|
|
124
|
+
case "iconRemovalRejected": return `: ${cause.message}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function configErrorDetail(err) {
|
|
128
|
+
switch (err.kind) {
|
|
129
|
+
case "configFunctionFailed": return `${err.sourceFile}: config function threw: ${err.message}`;
|
|
130
|
+
case "fileNotFound": return `no bedrock config under ${err.searchedFrom}`;
|
|
131
|
+
case "luauRuntimeMissing": return `${err.sourceFile}: ${err.hint}`;
|
|
132
|
+
case "parseFailed": return `${err.sourceFile}: ${err.message}`;
|
|
133
|
+
case "validationFailed": {
|
|
134
|
+
const first = err.issues[0];
|
|
135
|
+
return first === void 0 ? `${err.sourceFile}: invalid` : `${err.sourceFile}: ${first.path.join(".")} ${first.message}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function stateErrorDetail(cause) {
|
|
140
|
+
return `(${cause.file}): ${cause.reason}`;
|
|
141
|
+
}
|
|
142
|
+
function deployErrorMessage(err) {
|
|
143
|
+
switch (err.kind) {
|
|
144
|
+
case "applyFailed": return `apply failed for '${err.cause.key}': ${applyCauseDetail(err.cause)}`;
|
|
145
|
+
case "buildDesiredFailed": return `build desired state failed for '${err.cause.key}' ${buildDesiredDetail(err.cause)}`;
|
|
146
|
+
case "configLoadFailed": return `config load failed: ${configErrorDetail(err.cause)}`;
|
|
147
|
+
case "incompletePassEntry": return `pass '${err.key}' is missing '${err.missingField}' under environment '${err.environment}'`;
|
|
148
|
+
case "incompletePlaceEntry": return `place '${err.key}' is missing '${err.missingField}' under environment '${err.environment}'`;
|
|
149
|
+
case "incompleteUniverseEntry": return `universe is missing '${err.missingField}' under environment '${err.environment}'`;
|
|
150
|
+
case "missingCredential": return `missing credential: environment variable ${err.variable} is not set`;
|
|
151
|
+
case "registryConfigMissing": return `registry config missing '${err.missing}' (${err.hint})`;
|
|
152
|
+
case "stateNotConfigured": return `state not configured for environment '${err.environment}'`;
|
|
153
|
+
case "stateReadFailed": return `state read failed ${stateErrorDetail(err.cause)}`;
|
|
154
|
+
case "stateWriteFailed": return `state write failed ${stateErrorDetail(err.cause)}`;
|
|
155
|
+
case "unknownEnvironment": return `unknown environment '${err.environment}' (declared: ${err.declared.join(", ")})`;
|
|
156
|
+
case "unsupportedBackend": return `unsupported state backend '${err.backend}' (${err.hint})`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function parseErrorMessage(err) {
|
|
160
|
+
switch (err.kind) {
|
|
161
|
+
case "invalidValue": return `invalid value for flag '--${err.flag}' (expected a string)`;
|
|
162
|
+
case "missingRequired": return `missing required flag --${err.flag}`;
|
|
163
|
+
case "unknownFlag": return `unknown flag '--${err.flag}'`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function migrateParseErrorMessage(err) {
|
|
167
|
+
if (err.kind === "unknownSource") return `unknown migration source '${err.received}' (supported: ${err.supported.join(", ")})`;
|
|
168
|
+
return parseErrorMessage(err);
|
|
169
|
+
}
|
|
170
|
+
function migrateErrorMessage(err) {
|
|
171
|
+
switch (err.kind) {
|
|
172
|
+
case "internalError": return `migrate internal error: ${err.reason} (${configErrorDetail(err.cause)})`;
|
|
173
|
+
case "primaryEnvironmentNotFound": return `primary environment '${err.primary}' not found (available: ${err.available.join(", ")})`;
|
|
174
|
+
case "primaryEnvironmentRequired": return `primary environment required (available: ${err.available.join(", ")})`;
|
|
175
|
+
case "stateFileNotFound": return `Mantle state file not found at '${err.path}'`;
|
|
176
|
+
case "stateParseFailed": return `Mantle state file at '${err.path}' could not be parsed: ${err.reason}`;
|
|
177
|
+
case "unsupportedMantleStateVersion": return `unsupported Mantle state version '${err.found}' (supported: ${err.supported.join(", ")})`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function buildStatePortErrorMessage(err) {
|
|
181
|
+
switch (err.kind) {
|
|
182
|
+
case "missingCredential": return `missing credential: environment variable ${err.variable} is not set`;
|
|
183
|
+
case "unsupportedBackend": return `unsupported state backend '${err.backend}' (${err.hint})`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/adapters/clack-progress-adapter.ts
|
|
188
|
+
/**
|
|
189
|
+
* Build a {@link ProgressPort} that renders events through a `ClackPort`.
|
|
190
|
+
* Pattern-matches on the event `kind`: `deploySuccess` becomes a single
|
|
191
|
+
* success line and `deployFailure` delegates to the package's deploy-error
|
|
192
|
+
* rendering helper.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* import { createClackProgressAdapter, type ClackPort } from "@bedrock-rbx/core";
|
|
198
|
+
*
|
|
199
|
+
* const lines: Array<string> = [];
|
|
200
|
+
* const clack: ClackPort = {
|
|
201
|
+
* cancel: (message) => lines.push(`cancel: ${message}`),
|
|
202
|
+
* intro: (message) => lines.push(`intro: ${message}`),
|
|
203
|
+
* logError: (message) => lines.push(`error: ${message}`),
|
|
204
|
+
* logMessage: (message) => lines.push(`log: ${message}`),
|
|
205
|
+
* logSuccess: (message) => lines.push(`ok: ${message}`),
|
|
206
|
+
* outro: (message) => lines.push(`outro: ${message}`),
|
|
207
|
+
* };
|
|
208
|
+
*
|
|
209
|
+
* const port = createClackProgressAdapter({ clack });
|
|
210
|
+
*
|
|
211
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
|
|
212
|
+
*
|
|
213
|
+
* expect(lines).toEqual(["ok: production: 3 resources reconciled"]);
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @param deps - The clack port the adapter renders through.
|
|
217
|
+
* @returns A `ProgressPort` that renders via clack.
|
|
218
|
+
*/
|
|
219
|
+
function createClackProgressAdapter(deps) {
|
|
220
|
+
const { clack } = deps;
|
|
221
|
+
return { emit(event) {
|
|
222
|
+
switch (event.kind) {
|
|
223
|
+
case "deployFailure":
|
|
224
|
+
renderDeployError(event.error, clack);
|
|
225
|
+
return;
|
|
226
|
+
case "deploySuccess": clack.logSuccess(`${event.environment}: ${event.resourceCount} resources reconciled`);
|
|
227
|
+
}
|
|
228
|
+
} };
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
16
231
|
//#region src/core/derive-price-fields.ts
|
|
17
232
|
/**
|
|
18
233
|
* Translate a Mantle-style optional price into the Open Cloud wire shape.
|
|
@@ -240,17 +455,255 @@ async function sha256Hex(bytes) {
|
|
|
240
455
|
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
241
456
|
}
|
|
242
457
|
//#endregion
|
|
458
|
+
//#region src/core/redacted-icon.ts
|
|
459
|
+
const REDACTED_ICON_BYTES = new Uint8Array([
|
|
460
|
+
137,
|
|
461
|
+
80,
|
|
462
|
+
78,
|
|
463
|
+
71,
|
|
464
|
+
13,
|
|
465
|
+
10,
|
|
466
|
+
26,
|
|
467
|
+
10,
|
|
468
|
+
0,
|
|
469
|
+
0,
|
|
470
|
+
0,
|
|
471
|
+
13,
|
|
472
|
+
73,
|
|
473
|
+
72,
|
|
474
|
+
68,
|
|
475
|
+
82,
|
|
476
|
+
0,
|
|
477
|
+
0,
|
|
478
|
+
0,
|
|
479
|
+
64,
|
|
480
|
+
0,
|
|
481
|
+
0,
|
|
482
|
+
0,
|
|
483
|
+
64,
|
|
484
|
+
8,
|
|
485
|
+
0,
|
|
486
|
+
0,
|
|
487
|
+
0,
|
|
488
|
+
0,
|
|
489
|
+
143,
|
|
490
|
+
2,
|
|
491
|
+
46,
|
|
492
|
+
2,
|
|
493
|
+
0,
|
|
494
|
+
0,
|
|
495
|
+
0,
|
|
496
|
+
137,
|
|
497
|
+
73,
|
|
498
|
+
68,
|
|
499
|
+
65,
|
|
500
|
+
84,
|
|
501
|
+
120,
|
|
502
|
+
156,
|
|
503
|
+
237,
|
|
504
|
+
86,
|
|
505
|
+
209,
|
|
506
|
+
14,
|
|
507
|
+
128,
|
|
508
|
+
32,
|
|
509
|
+
8,
|
|
510
|
+
244,
|
|
511
|
+
83,
|
|
512
|
+
252,
|
|
513
|
+
148,
|
|
514
|
+
254,
|
|
515
|
+
255,
|
|
516
|
+
167,
|
|
517
|
+
174,
|
|
518
|
+
37,
|
|
519
|
+
98,
|
|
520
|
+
130,
|
|
521
|
+
189,
|
|
522
|
+
20,
|
|
523
|
+
110,
|
|
524
|
+
57,
|
|
525
|
+
119,
|
|
526
|
+
108,
|
|
527
|
+
26,
|
|
528
|
+
194,
|
|
529
|
+
188,
|
|
530
|
+
64,
|
|
531
|
+
15,
|
|
532
|
+
42,
|
|
533
|
+
229,
|
|
534
|
+
160,
|
|
535
|
+
164,
|
|
536
|
+
13,
|
|
537
|
+
0,
|
|
538
|
+
142,
|
|
539
|
+
160,
|
|
540
|
+
44,
|
|
541
|
+
144,
|
|
542
|
+
2,
|
|
543
|
+
1,
|
|
544
|
+
200,
|
|
545
|
+
3,
|
|
546
|
+
242,
|
|
547
|
+
96,
|
|
548
|
+
82,
|
|
549
|
+
45,
|
|
550
|
+
176,
|
|
551
|
+
31,
|
|
552
|
+
176,
|
|
553
|
+
161,
|
|
554
|
+
244,
|
|
555
|
+
60,
|
|
556
|
+
0,
|
|
557
|
+
0,
|
|
558
|
+
153,
|
|
559
|
+
81,
|
|
560
|
+
245,
|
|
561
|
+
235,
|
|
562
|
+
161,
|
|
563
|
+
142,
|
|
564
|
+
219,
|
|
565
|
+
222,
|
|
566
|
+
188,
|
|
567
|
+
254,
|
|
568
|
+
187,
|
|
569
|
+
128,
|
|
570
|
+
50,
|
|
571
|
+
224,
|
|
572
|
+
116,
|
|
573
|
+
181,
|
|
574
|
+
168,
|
|
575
|
+
205,
|
|
576
|
+
106,
|
|
577
|
+
134,
|
|
578
|
+
202,
|
|
579
|
+
113,
|
|
580
|
+
0,
|
|
581
|
+
0,
|
|
582
|
+
109,
|
|
583
|
+
150,
|
|
584
|
+
173,
|
|
585
|
+
101,
|
|
586
|
+
97,
|
|
587
|
+
0,
|
|
588
|
+
58,
|
|
589
|
+
239,
|
|
590
|
+
67,
|
|
591
|
+
4,
|
|
592
|
+
254,
|
|
593
|
+
29,
|
|
594
|
+
239,
|
|
595
|
+
83,
|
|
596
|
+
168,
|
|
597
|
+
232,
|
|
598
|
+
177,
|
|
599
|
+
51,
|
|
600
|
+
144,
|
|
601
|
+
176,
|
|
602
|
+
251,
|
|
603
|
+
80,
|
|
604
|
+
101,
|
|
605
|
+
209,
|
|
606
|
+
82,
|
|
607
|
+
80,
|
|
608
|
+
239,
|
|
609
|
+
0,
|
|
610
|
+
240,
|
|
611
|
+
85,
|
|
612
|
+
216,
|
|
613
|
+
15,
|
|
614
|
+
216,
|
|
615
|
+
15,
|
|
616
|
+
200,
|
|
617
|
+
131,
|
|
618
|
+
89,
|
|
619
|
+
197,
|
|
620
|
+
180,
|
|
621
|
+
1,
|
|
622
|
+
0,
|
|
623
|
+
255,
|
|
624
|
+
15,
|
|
625
|
+
86,
|
|
626
|
+
184,
|
|
627
|
+
5,
|
|
628
|
+
2,
|
|
629
|
+
228,
|
|
630
|
+
255,
|
|
631
|
+
207,
|
|
632
|
+
224,
|
|
633
|
+
4,
|
|
634
|
+
233,
|
|
635
|
+
243,
|
|
636
|
+
166,
|
|
637
|
+
219,
|
|
638
|
+
234,
|
|
639
|
+
149,
|
|
640
|
+
21,
|
|
641
|
+
116,
|
|
642
|
+
0,
|
|
643
|
+
0,
|
|
644
|
+
0,
|
|
645
|
+
0,
|
|
646
|
+
73,
|
|
647
|
+
69,
|
|
648
|
+
78,
|
|
649
|
+
68,
|
|
650
|
+
174,
|
|
651
|
+
66,
|
|
652
|
+
96,
|
|
653
|
+
130
|
|
654
|
+
]);
|
|
655
|
+
/**
|
|
656
|
+
* Sentinel path written into a resource's `icon["en-us"]` field when
|
|
657
|
+
* redaction substitutes the bedrock-supplied placeholder image. Callers
|
|
658
|
+
* route this path through {@link withRedactedIcon} or through the
|
|
659
|
+
* `readBytes` short-circuit; neither touches the filesystem.
|
|
660
|
+
*/
|
|
661
|
+
const REDACTED_ICON_PATH = "<bedrock:redacted-icon.png>";
|
|
662
|
+
/**
|
|
663
|
+
* `true` when `path` is the redacted-icon sentinel. `readBytes` and
|
|
664
|
+
* {@link withRedactedIcon} both use this predicate to decide whether to
|
|
665
|
+
* bypass the injected file reader.
|
|
666
|
+
*
|
|
667
|
+
* @param path - Icon path supplied by a flattened or normalized resource entry.
|
|
668
|
+
* @returns `true` for {@link REDACTED_ICON_PATH}; otherwise `false`.
|
|
669
|
+
*/
|
|
670
|
+
function isRedactedIconPath(path) {
|
|
671
|
+
return path === REDACTED_ICON_PATH;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Wrap a `readFile` so the sentinel resolves to {@link REDACTED_ICON_BYTES}
|
|
675
|
+
* without touching the inner reader. Applied once at the shell deploy /
|
|
676
|
+
* preview boundary; the wrapped reader flows to every consumer (normalize,
|
|
677
|
+
* registry drivers) unchanged.
|
|
678
|
+
*
|
|
679
|
+
* @param readFile - Inner reader that handles every non-sentinel path.
|
|
680
|
+
* @returns Sentinel-aware reader with the same callable shape as `readFile`.
|
|
681
|
+
*/
|
|
682
|
+
function withRedactedIcon(readFile) {
|
|
683
|
+
return async (path) => {
|
|
684
|
+
if (isRedactedIconPath(path)) return new Uint8Array(REDACTED_ICON_BYTES);
|
|
685
|
+
return readFile(path);
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
243
689
|
//#region src/core/kinds/read-bytes.ts
|
|
244
690
|
/**
|
|
245
691
|
* Read file bytes via the injected reader, translating rejections into a
|
|
246
692
|
* `fileReadFailed` `BuildDesiredError`. Shared by kind modules whose
|
|
247
|
-
* pre-I/O normalization hashes a file the user declared by path.
|
|
693
|
+
* pre-I/O normalization hashes a file the user declared by path. The
|
|
694
|
+
* redacted-icon sentinel short-circuits to the embedded placeholder
|
|
695
|
+
* bytes without invoking the injected reader, so a redaction-substituted
|
|
696
|
+
* icon path produces a deterministic hash on every deploy.
|
|
248
697
|
*
|
|
249
698
|
* @param target - Path to read plus the resource key blamed on failure.
|
|
250
699
|
* @param io - I/O surface carrying the injected `readFile` function.
|
|
251
700
|
* @returns `Ok` with the bytes, or `Err` with a `fileReadFailed` error.
|
|
252
701
|
*/
|
|
253
702
|
async function readBytes(target, io) {
|
|
703
|
+
if (isRedactedIconPath(target.filePath)) return {
|
|
704
|
+
data: new Uint8Array(REDACTED_ICON_BYTES),
|
|
705
|
+
success: true
|
|
706
|
+
};
|
|
254
707
|
try {
|
|
255
708
|
return {
|
|
256
709
|
data: await io.readFile(target.filePath),
|
|
@@ -482,12 +935,16 @@ function planFollowUpPatch(desired, createResponse) {
|
|
|
482
935
|
* ```
|
|
483
936
|
*/
|
|
484
937
|
function createDeveloperProductDriver(deps) {
|
|
938
|
+
const effective = {
|
|
939
|
+
...deps,
|
|
940
|
+
readFile: withRedactedIcon(deps.readFile)
|
|
941
|
+
};
|
|
485
942
|
return {
|
|
486
943
|
async create(desired) {
|
|
487
|
-
return createOne(
|
|
944
|
+
return createOne(effective, desired);
|
|
488
945
|
},
|
|
489
946
|
async update(current, desired) {
|
|
490
|
-
return updateOne(
|
|
947
|
+
return updateOne(effective, {
|
|
491
948
|
current,
|
|
492
949
|
desired
|
|
493
950
|
});
|
|
@@ -639,18 +1096,21 @@ async function updateOne(deps, { current, desired }) {
|
|
|
639
1096
|
* ```
|
|
640
1097
|
*/
|
|
641
1098
|
function createGamePassDriver(deps) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1099
|
+
const effective = {
|
|
1100
|
+
...deps,
|
|
1101
|
+
readFile: withRedactedIcon(deps.readFile)
|
|
1102
|
+
};
|
|
1103
|
+
return {
|
|
1104
|
+
async create(desired) {
|
|
1105
|
+
return createGamePass(effective, desired);
|
|
1106
|
+
},
|
|
1107
|
+
async update(current, desired) {
|
|
1108
|
+
return updateGamePass(effective, {
|
|
1109
|
+
current,
|
|
1110
|
+
desired
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
654
1114
|
}
|
|
655
1115
|
function toCurrentState$1(desired, data) {
|
|
656
1116
|
const { id, iconAssetId } = data;
|
|
@@ -669,6 +1129,53 @@ function toCurrentState$1(desired, data) {
|
|
|
669
1129
|
success: true
|
|
670
1130
|
};
|
|
671
1131
|
}
|
|
1132
|
+
async function createGamePass(deps, desired) {
|
|
1133
|
+
const imageFile = await deps.readFile(desired.icon["en-us"]);
|
|
1134
|
+
const result = await deps.client.create({
|
|
1135
|
+
name: desired.name,
|
|
1136
|
+
description: desired.description,
|
|
1137
|
+
imageFile,
|
|
1138
|
+
universeId: deps.universeId,
|
|
1139
|
+
...desired.price !== void 0 ? { price: desired.price } : {}
|
|
1140
|
+
});
|
|
1141
|
+
if (!result.success) return result;
|
|
1142
|
+
return toCurrentState$1(desired, result.data);
|
|
1143
|
+
}
|
|
1144
|
+
async function resolveUpdatedState(deps, context) {
|
|
1145
|
+
const { current, desired, hasIconChanged } = context;
|
|
1146
|
+
if (!hasIconChanged) return {
|
|
1147
|
+
data: {
|
|
1148
|
+
...desired,
|
|
1149
|
+
outputs: current.outputs
|
|
1150
|
+
},
|
|
1151
|
+
success: true
|
|
1152
|
+
};
|
|
1153
|
+
const fetched = await deps.client.get({
|
|
1154
|
+
gamePassId: current.outputs.assetId,
|
|
1155
|
+
universeId: deps.universeId
|
|
1156
|
+
});
|
|
1157
|
+
if (!fetched.success) return fetched;
|
|
1158
|
+
return toCurrentState$1(desired, fetched.data);
|
|
1159
|
+
}
|
|
1160
|
+
async function updateGamePass(deps, states) {
|
|
1161
|
+
const { current, desired } = states;
|
|
1162
|
+
const hasIconChanged = shouldReuploadIcon(current.iconFileHashes, desired.iconFileHashes);
|
|
1163
|
+
const imageFile = hasIconChanged ? await deps.readFile(desired.icon["en-us"]) : void 0;
|
|
1164
|
+
const result = await deps.client.update({
|
|
1165
|
+
name: desired.name,
|
|
1166
|
+
description: desired.description,
|
|
1167
|
+
gamePassId: current.outputs.assetId,
|
|
1168
|
+
universeId: deps.universeId,
|
|
1169
|
+
...derivePriceFields(desired),
|
|
1170
|
+
...imageFile !== void 0 ? { imageFile } : {}
|
|
1171
|
+
});
|
|
1172
|
+
if (!result.success) return result;
|
|
1173
|
+
return resolveUpdatedState(deps, {
|
|
1174
|
+
current,
|
|
1175
|
+
desired,
|
|
1176
|
+
hasIconChanged
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
672
1179
|
//#endregion
|
|
673
1180
|
//#region src/core/environment.ts
|
|
674
1181
|
/**
|
|
@@ -1510,6 +2017,49 @@ async function reconcileUniverse(inputs) {
|
|
|
1510
2017
|
};
|
|
1511
2018
|
}
|
|
1512
2019
|
//#endregion
|
|
2020
|
+
//#region src/cli/clack-port.ts
|
|
2021
|
+
/**
|
|
2022
|
+
* Construct a `ClackPort` whose methods delegate to `@clack/prompts`. The
|
|
2023
|
+
* resulting port writes to `process.stdout` via clack's defaults. Kept in
|
|
2024
|
+
* its own module so consumers that never need the clack-backed rendering
|
|
2025
|
+
* (programmatic deploys, custom adapters) do not pull `@clack/prompts`
|
|
2026
|
+
* into their bundle.
|
|
2027
|
+
*
|
|
2028
|
+
* @example
|
|
2029
|
+
*
|
|
2030
|
+
* ```ts
|
|
2031
|
+
* import { createClackPort } from "@bedrock-rbx/core";
|
|
2032
|
+
*
|
|
2033
|
+
* const port = createClackPort();
|
|
2034
|
+
*
|
|
2035
|
+
* expect(typeof port.logSuccess).toBe("function");
|
|
2036
|
+
* ```
|
|
2037
|
+
*
|
|
2038
|
+
* @returns A port whose six methods each invoke the matching clack helper.
|
|
2039
|
+
*/
|
|
2040
|
+
function createClackPort() {
|
|
2041
|
+
return {
|
|
2042
|
+
cancel: (message) => {
|
|
2043
|
+
cancel(message);
|
|
2044
|
+
},
|
|
2045
|
+
intro: (message) => {
|
|
2046
|
+
intro(message);
|
|
2047
|
+
},
|
|
2048
|
+
logError: (message) => {
|
|
2049
|
+
log.error(message);
|
|
2050
|
+
},
|
|
2051
|
+
logMessage: (message) => {
|
|
2052
|
+
log.message(message);
|
|
2053
|
+
},
|
|
2054
|
+
logSuccess: (message) => {
|
|
2055
|
+
log.success(message);
|
|
2056
|
+
},
|
|
2057
|
+
outro: (message) => {
|
|
2058
|
+
outro(message);
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
//#endregion
|
|
1513
2063
|
//#region src/core/validate-universe-xor.ts
|
|
1514
2064
|
/**
|
|
1515
2065
|
* Walk the loose authored-shape and surface every place the
|
|
@@ -1588,6 +2138,8 @@ function isGistStateConfig(config) {
|
|
|
1588
2138
|
}
|
|
1589
2139
|
const OPTIONAL_BOOLEAN$2 = "boolean | undefined";
|
|
1590
2140
|
const OPTIONAL_STRING = "string | undefined";
|
|
2141
|
+
const REDACTED_KEY = "redacted?";
|
|
2142
|
+
const NON_EMPTY_OVERRIDE_MESSAGE = "a non-empty override object; use `redacted: true` for default placeholders";
|
|
1591
2143
|
/**
|
|
1592
2144
|
* Shared arktype constraint for any optional positive-integer field.
|
|
1593
2145
|
* Reused by per-kind entry schemas so positive-integer fields validate
|
|
@@ -1604,11 +2156,35 @@ const OPTIONAL_POSITIVE_INTEGER = "(number.integer >= 1) | undefined";
|
|
|
1604
2156
|
* identically.
|
|
1605
2157
|
*/
|
|
1606
2158
|
const OPTIONAL_ROBUX_PRICE = "number.integer >= 0 | undefined";
|
|
2159
|
+
const gamePassRedacted = type({
|
|
2160
|
+
"description?": "string",
|
|
2161
|
+
"icon?": iconMap,
|
|
2162
|
+
"name?": "string"
|
|
2163
|
+
}).onUndeclaredKey("reject").narrow((value, ctx) => {
|
|
2164
|
+
if (Object.keys(value).length === 0) return ctx.mustBe(NON_EMPTY_OVERRIDE_MESSAGE);
|
|
2165
|
+
return true;
|
|
2166
|
+
}).or(OPTIONAL_BOOLEAN$2);
|
|
2167
|
+
const placeRedacted = type({
|
|
2168
|
+
"description?": "string",
|
|
2169
|
+
"displayName?": "string"
|
|
2170
|
+
}).onUndeclaredKey("reject").narrow((value, ctx) => {
|
|
2171
|
+
if (Object.keys(value).length === 0) return ctx.mustBe(NON_EMPTY_OVERRIDE_MESSAGE);
|
|
2172
|
+
return true;
|
|
2173
|
+
}).or(OPTIONAL_BOOLEAN$2);
|
|
2174
|
+
const productRedacted = type({
|
|
2175
|
+
"description?": "string",
|
|
2176
|
+
"icon?": iconMap,
|
|
2177
|
+
"name?": "string"
|
|
2178
|
+
}).onUndeclaredKey("reject").narrow((value, ctx) => {
|
|
2179
|
+
if (Object.keys(value).length === 0) return ctx.mustBe(NON_EMPTY_OVERRIDE_MESSAGE);
|
|
2180
|
+
return true;
|
|
2181
|
+
}).or(OPTIONAL_BOOLEAN$2);
|
|
1607
2182
|
const gamePassEntry = type({
|
|
1608
2183
|
"name": "string",
|
|
1609
2184
|
"description": "string",
|
|
1610
2185
|
"icon": iconMap,
|
|
1611
|
-
"price?": OPTIONAL_ROBUX_PRICE
|
|
2186
|
+
"price?": OPTIONAL_ROBUX_PRICE,
|
|
2187
|
+
[REDACTED_KEY]: gamePassRedacted
|
|
1612
2188
|
});
|
|
1613
2189
|
const passesCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: gamePassEntry }).onUndeclaredKey("reject");
|
|
1614
2190
|
const developerProductEntry = type({
|
|
@@ -1617,6 +2193,7 @@ const developerProductEntry = type({
|
|
|
1617
2193
|
"icon?": iconMap,
|
|
1618
2194
|
"isRegionalPricingEnabled?": OPTIONAL_BOOLEAN$2,
|
|
1619
2195
|
"price?": OPTIONAL_ROBUX_PRICE,
|
|
2196
|
+
[REDACTED_KEY]: productRedacted,
|
|
1620
2197
|
"storePageEnabled?": OPTIONAL_BOOLEAN$2
|
|
1621
2198
|
}).onUndeclaredKey("reject");
|
|
1622
2199
|
const productsCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: developerProductEntry }).onUndeclaredKey("reject");
|
|
@@ -1625,6 +2202,7 @@ const placeEntry = type({
|
|
|
1625
2202
|
"description?": OPTIONAL_STRING,
|
|
1626
2203
|
"displayName?": OPTIONAL_STRING,
|
|
1627
2204
|
"filePath": "string",
|
|
2205
|
+
[REDACTED_KEY]: placeRedacted,
|
|
1628
2206
|
"serverSize?": OPTIONAL_POSITIVE_INTEGER
|
|
1629
2207
|
}).onUndeclaredKey("reject");
|
|
1630
2208
|
const placesCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: placeEntry }).onUndeclaredKey("reject");
|
|
@@ -1658,7 +2236,8 @@ const gamePassOverlay = type({
|
|
|
1658
2236
|
"description?": "string",
|
|
1659
2237
|
"icon?": iconMap,
|
|
1660
2238
|
"name?": "string",
|
|
1661
|
-
"price?": OPTIONAL_ROBUX_PRICE
|
|
2239
|
+
"price?": OPTIONAL_ROBUX_PRICE,
|
|
2240
|
+
[REDACTED_KEY]: OPTIONAL_BOOLEAN$2
|
|
1662
2241
|
}).onUndeclaredKey("reject");
|
|
1663
2242
|
const passesOverlayCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: gamePassOverlay }).onUndeclaredKey("reject");
|
|
1664
2243
|
const developerProductOverlay = type({
|
|
@@ -1667,6 +2246,7 @@ const developerProductOverlay = type({
|
|
|
1667
2246
|
"isRegionalPricingEnabled?": OPTIONAL_BOOLEAN$2,
|
|
1668
2247
|
"name?": "string",
|
|
1669
2248
|
"price?": OPTIONAL_ROBUX_PRICE,
|
|
2249
|
+
[REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
|
|
1670
2250
|
"storePageEnabled?": OPTIONAL_BOOLEAN$2
|
|
1671
2251
|
}).onUndeclaredKey("reject");
|
|
1672
2252
|
const productsOverlayCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: developerProductOverlay }).onUndeclaredKey("reject");
|
|
@@ -1675,15 +2255,19 @@ const placeOverlay = type({
|
|
|
1675
2255
|
"displayName?": OPTIONAL_STRING,
|
|
1676
2256
|
"filePath?": "string",
|
|
1677
2257
|
"placeId": ROBLOX_ID_DIGITS,
|
|
2258
|
+
[REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
|
|
1678
2259
|
"serverSize?": OPTIONAL_POSITIVE_INTEGER
|
|
1679
2260
|
}).onUndeclaredKey("reject");
|
|
2261
|
+
const placesOverlayCollection = type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: placeOverlay }).onUndeclaredKey("reject");
|
|
2262
|
+
const universeOverlay = universeEntry;
|
|
1680
2263
|
const environmentEntry = type({
|
|
1681
2264
|
"label?": OPTIONAL_STRING,
|
|
1682
2265
|
"passes?": passesOverlayCollection,
|
|
1683
|
-
"places?":
|
|
2266
|
+
"places?": placesOverlayCollection,
|
|
1684
2267
|
"products?": productsOverlayCollection,
|
|
2268
|
+
[REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
|
|
1685
2269
|
"state?": stateConfig,
|
|
1686
|
-
"universe?":
|
|
2270
|
+
"universe?": universeOverlay
|
|
1687
2271
|
}).onUndeclaredKey("reject");
|
|
1688
2272
|
const rootSchema = type({
|
|
1689
2273
|
"displayNamePrefix?": type({
|
|
@@ -1780,6 +2364,7 @@ const entrySchema$3 = type({
|
|
|
1780
2364
|
"icon?": iconMap,
|
|
1781
2365
|
"isRegionalPricingEnabled?": OPTIONAL_BOOLEAN$1,
|
|
1782
2366
|
"price?": OPTIONAL_ROBUX_PRICE,
|
|
2367
|
+
"redacted?": "boolean | undefined",
|
|
1783
2368
|
"storePageEnabled?": OPTIONAL_BOOLEAN$1
|
|
1784
2369
|
});
|
|
1785
2370
|
function flatten$3(config) {
|
|
@@ -1863,7 +2448,8 @@ const entrySchema$2 = type({
|
|
|
1863
2448
|
"name": "string",
|
|
1864
2449
|
"description": "string",
|
|
1865
2450
|
"icon": iconMap,
|
|
1866
|
-
"price?": OPTIONAL_ROBUX_PRICE
|
|
2451
|
+
"price?": OPTIONAL_ROBUX_PRICE,
|
|
2452
|
+
"redacted?": "boolean | undefined"
|
|
1867
2453
|
});
|
|
1868
2454
|
function flatten$2(config) {
|
|
1869
2455
|
return Object.entries(config.passes ?? {}).map(([key, entry]) => {
|
|
@@ -2344,9 +2930,172 @@ function resolveStateConfig(config, environment) {
|
|
|
2344
2930
|
success: false
|
|
2345
2931
|
};
|
|
2346
2932
|
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Pure transform that substitutes bedrock-supplied placeholder content for
|
|
2935
|
+
* every resource whose effective `redacted` flag is truthy. The effective
|
|
2936
|
+
* flag is the per-resource `redacted` value when set, otherwise the
|
|
2937
|
+
* `environmentRedacted` fallback. A `redacted` object form replaces
|
|
2938
|
+
* matching fields with the supplied values and falls back to the bedrock
|
|
2939
|
+
* defaults for the rest. Runs between env-overlay merge and display-name
|
|
2940
|
+
* prefix render so the rest of the pipeline (flatten, normalize, diff,
|
|
2941
|
+
* apply) operates on already-redacted values and needs no special-case
|
|
2942
|
+
* redaction logic.
|
|
2943
|
+
*
|
|
2944
|
+
* @param config - Post-merge `ResolvedConfig` produced by `selectEnvironment`.
|
|
2945
|
+
* @param environmentRedacted - Environment-level redaction toggle. Resources
|
|
2946
|
+
* that omit a per-resource `redacted` flag inherit this value.
|
|
2947
|
+
* @returns A `ResolvedConfig` whose redacted entries carry placeholder
|
|
2948
|
+
* values; non-redacted entries pass through verbatim, and the input is
|
|
2949
|
+
* not mutated.
|
|
2950
|
+
*/
|
|
2951
|
+
function applyRedaction(config, environmentRedacted = false) {
|
|
2952
|
+
const passes = redactPasses(config.passes, environmentRedacted);
|
|
2953
|
+
const places = redactPlaces(config.places, environmentRedacted);
|
|
2954
|
+
const products = redactProducts(config.products, environmentRedacted);
|
|
2955
|
+
if (passes === config.passes && places === config.places && products === config.products) return config;
|
|
2956
|
+
return {
|
|
2957
|
+
...config,
|
|
2958
|
+
...passes === void 0 ? {} : { passes },
|
|
2959
|
+
...places === void 0 ? {} : { places },
|
|
2960
|
+
...products === void 0 ? {} : { products }
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Inspect the pre-redaction merged config and produce one annotation per
|
|
2965
|
+
* resource flagged `redacted: true`. Callers thread the result into plan
|
|
2966
|
+
* output so authors can see which resources are redacted in the active
|
|
2967
|
+
* environment and whether their real-value edits are being suppressed.
|
|
2968
|
+
*
|
|
2969
|
+
* Operates on the pre-redaction view because the post-redaction config no
|
|
2970
|
+
* longer carries the real `name`/`description`/`icon` values needed to
|
|
2971
|
+
* detect divergence from the placeholder defaults.
|
|
2972
|
+
*
|
|
2973
|
+
* @param merged - `ResolvedConfig` produced by environment overlay merge,
|
|
2974
|
+
* before `applyRedaction` has substituted placeholders.
|
|
2975
|
+
* @returns Zero or more annotations, one per redacted resource. Empty when
|
|
2976
|
+
* the config declares no redacted resources.
|
|
2977
|
+
*/
|
|
2978
|
+
function collectRedactionAnnotations(merged) {
|
|
2979
|
+
const passes = Object.entries(merged.passes ?? {}).filter(([, entry]) => entry.redacted === true).map(([key, entry]) => {
|
|
2980
|
+
return {
|
|
2981
|
+
key: asResourceKey(key),
|
|
2982
|
+
hasRealValueEdits: passHasRealValueEdits(entry),
|
|
2983
|
+
kind: "gamePass"
|
|
2984
|
+
};
|
|
2985
|
+
});
|
|
2986
|
+
const products = Object.entries(merged.products ?? {}).filter(([, entry]) => entry.redacted === true).map(([key, entry]) => {
|
|
2987
|
+
return {
|
|
2988
|
+
key: asResourceKey(key),
|
|
2989
|
+
hasRealValueEdits: productHasRealValueEdits(entry),
|
|
2990
|
+
kind: "developerProduct"
|
|
2991
|
+
};
|
|
2992
|
+
});
|
|
2993
|
+
return [...passes, ...products];
|
|
2994
|
+
}
|
|
2995
|
+
function redactPass(entry, override) {
|
|
2996
|
+
return {
|
|
2997
|
+
...entry,
|
|
2998
|
+
name: override.name ?? "Redacted Pass",
|
|
2999
|
+
description: override.description ?? "",
|
|
3000
|
+
icon: override.icon ?? { "en-us": "<bedrock:redacted-icon.png>" }
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
function redactPasses(passes, environmentRedacted) {
|
|
3004
|
+
if (passes === void 0) return;
|
|
3005
|
+
if (!Object.values(passes).some((entry) => (entry.redacted ?? environmentRedacted) !== false)) return passes;
|
|
3006
|
+
return Object.fromEntries(Object.entries(passes).map(([key, entry]) => {
|
|
3007
|
+
const effective = entry.redacted ?? environmentRedacted;
|
|
3008
|
+
if (effective === false) return [key, entry];
|
|
3009
|
+
return [key, redactPass(entry, typeof effective === "object" ? effective : {})];
|
|
3010
|
+
}));
|
|
3011
|
+
}
|
|
3012
|
+
function redactPlace(entry, override) {
|
|
3013
|
+
return {
|
|
3014
|
+
...entry,
|
|
3015
|
+
description: override.description ?? "",
|
|
3016
|
+
displayName: override.displayName ?? entry.displayName
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
function redactPlaces(places, environmentRedacted) {
|
|
3020
|
+
if (places === void 0) return;
|
|
3021
|
+
if (!Object.values(places).some((entry) => (entry.redacted ?? environmentRedacted) !== false)) return places;
|
|
3022
|
+
return Object.fromEntries(Object.entries(places).map(([key, entry]) => {
|
|
3023
|
+
const effective = entry.redacted ?? environmentRedacted;
|
|
3024
|
+
if (effective === false) return [key, entry];
|
|
3025
|
+
return [key, redactPlace(entry, typeof effective === "object" ? effective : {})];
|
|
3026
|
+
}));
|
|
3027
|
+
}
|
|
3028
|
+
function redactProduct(entry, override) {
|
|
3029
|
+
return {
|
|
3030
|
+
...entry,
|
|
3031
|
+
name: override.name ?? "Redacted Product",
|
|
3032
|
+
description: override.description ?? "",
|
|
3033
|
+
icon: override.icon ?? { "en-us": "<bedrock:redacted-icon.png>" }
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
function redactProducts(products, environmentRedacted) {
|
|
3037
|
+
if (products === void 0) return;
|
|
3038
|
+
if (!Object.values(products).some((entry) => (entry.redacted ?? environmentRedacted) !== false)) return products;
|
|
3039
|
+
return Object.fromEntries(Object.entries(products).map(([key, entry]) => {
|
|
3040
|
+
const effective = entry.redacted ?? environmentRedacted;
|
|
3041
|
+
if (effective === false) return [key, entry];
|
|
3042
|
+
return [key, redactProduct(entry, typeof effective === "object" ? effective : {})];
|
|
3043
|
+
}));
|
|
3044
|
+
}
|
|
3045
|
+
function passHasRealValueEdits(entry) {
|
|
3046
|
+
return entry.name !== "Redacted Pass" || entry.description !== "" || entry.icon["en-us"] !== "<bedrock:redacted-icon.png>";
|
|
3047
|
+
}
|
|
3048
|
+
function productHasRealValueEdits(entry) {
|
|
3049
|
+
return entry.name !== "Redacted Product" || entry.description !== "" || entry.icon !== void 0 && entry.icon["en-us"] !== "<bedrock:redacted-icon.png>";
|
|
3050
|
+
}
|
|
2347
3051
|
//#endregion
|
|
2348
3052
|
//#region src/core/select-environment.ts
|
|
2349
3053
|
/**
|
|
3054
|
+
* Project a `Config` onto a single environment up to the pre-redaction
|
|
3055
|
+
* merge boundary. Looks up the env entry, deep-merges its resource overlay
|
|
3056
|
+
* over the root config, and runs the same pass, place, and universe
|
|
3057
|
+
* completeness checks {@link selectEnvironment} runs, so the returned
|
|
3058
|
+
* `merged` config honours the full `ResolvedConfig` contract. Real
|
|
3059
|
+
* `name`, `description`, and `icon` values on redacted resources stay
|
|
3060
|
+
* intact, letting callers inspect divergence from placeholder defaults
|
|
3061
|
+
* before {@link selectEnvironment} substitutes them.
|
|
3062
|
+
*
|
|
3063
|
+
* @param config - Validated project config.
|
|
3064
|
+
* @param environment - Environment name to project onto.
|
|
3065
|
+
* @returns The matched env entry plus the merged config, or any of the
|
|
3066
|
+
* `SelectEnvironmentError` failure modes.
|
|
3067
|
+
*/
|
|
3068
|
+
function selectMergedEnvironment(config, environment) {
|
|
3069
|
+
const entry = config.environments[environment];
|
|
3070
|
+
if (entry === void 0) return {
|
|
3071
|
+
err: unknownEnvironment(config, environment),
|
|
3072
|
+
success: false
|
|
3073
|
+
};
|
|
3074
|
+
const merged = mergeOverlays(config, entry);
|
|
3075
|
+
const incompletePass = findIncompletePass(merged, environment);
|
|
3076
|
+
if (incompletePass !== void 0) return {
|
|
3077
|
+
err: incompletePass,
|
|
3078
|
+
success: false
|
|
3079
|
+
};
|
|
3080
|
+
const incompletePlace = findIncompletePlace(merged, environment);
|
|
3081
|
+
if (incompletePlace !== void 0) return {
|
|
3082
|
+
err: incompletePlace,
|
|
3083
|
+
success: false
|
|
3084
|
+
};
|
|
3085
|
+
const incompleteUniverse = findIncompleteUniverse(merged, environment);
|
|
3086
|
+
if (incompleteUniverse !== void 0) return {
|
|
3087
|
+
err: incompleteUniverse,
|
|
3088
|
+
success: false
|
|
3089
|
+
};
|
|
3090
|
+
return {
|
|
3091
|
+
data: {
|
|
3092
|
+
entry,
|
|
3093
|
+
merged
|
|
3094
|
+
},
|
|
3095
|
+
success: true
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
2350
3099
|
* Project a validated `Config` onto a single environment. Looks up the
|
|
2351
3100
|
* matching `environments[environment]` entry, deep-merges its resource
|
|
2352
3101
|
* overlay (`passes`, `places`, `universe`) over the root config via defu,
|
|
@@ -2424,28 +3173,80 @@ function resolveStateConfig(config, environment) {
|
|
|
2424
3173
|
* projection failed.
|
|
2425
3174
|
*/
|
|
2426
3175
|
function selectEnvironment(config, environment) {
|
|
2427
|
-
const
|
|
2428
|
-
if (
|
|
2429
|
-
|
|
2430
|
-
|
|
3176
|
+
const mergedResult = selectMergedEnvironment(config, environment);
|
|
3177
|
+
if (!mergedResult.success) return mergedResult;
|
|
3178
|
+
const { entry, merged } = mergedResult.data;
|
|
3179
|
+
return {
|
|
3180
|
+
data: redactAndPrefix({
|
|
3181
|
+
config,
|
|
3182
|
+
entry,
|
|
3183
|
+
merged
|
|
3184
|
+
}),
|
|
3185
|
+
success: true
|
|
2431
3186
|
};
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
3187
|
+
}
|
|
3188
|
+
function findIncompletePass(merged, environment) {
|
|
3189
|
+
const { passes } = merged;
|
|
3190
|
+
if (passes === void 0) return;
|
|
3191
|
+
const candidates = passes;
|
|
3192
|
+
for (const [key, entry] of Object.entries(candidates)) {
|
|
3193
|
+
if (entry.name === void 0) return {
|
|
3194
|
+
key,
|
|
3195
|
+
environment,
|
|
3196
|
+
kind: "incompletePassEntry",
|
|
3197
|
+
missingField: "name"
|
|
3198
|
+
};
|
|
3199
|
+
if (entry.description === void 0) return {
|
|
3200
|
+
key,
|
|
3201
|
+
environment,
|
|
3202
|
+
kind: "incompletePassEntry",
|
|
3203
|
+
missingField: "description"
|
|
3204
|
+
};
|
|
3205
|
+
if (entry.icon === void 0) return {
|
|
3206
|
+
key,
|
|
3207
|
+
environment,
|
|
3208
|
+
kind: "incompletePassEntry",
|
|
3209
|
+
missingField: "icon"
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
function mergeEntry(overlay, base) {
|
|
3214
|
+
return defu(overlay, base ?? {});
|
|
3215
|
+
}
|
|
3216
|
+
function mergeKeyedRecord(overlay, base) {
|
|
3217
|
+
if (overlay === void 0) return base;
|
|
3218
|
+
return {
|
|
3219
|
+
...base ?? {},
|
|
3220
|
+
...Object.fromEntries(Object.entries(overlay).map(([key, partial]) => {
|
|
3221
|
+
return [key, mergeEntry(partial, base?.[key])];
|
|
3222
|
+
}))
|
|
2440
3223
|
};
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
3224
|
+
}
|
|
3225
|
+
function mergeUniverse(overlay, base) {
|
|
3226
|
+
if (overlay === void 0 && base === void 0) return;
|
|
3227
|
+
return defu(overlay ?? {}, base ?? {});
|
|
3228
|
+
}
|
|
3229
|
+
function mergeOverlays(config, entry) {
|
|
3230
|
+
const passes = mergeKeyedRecord(entry.passes, config.passes);
|
|
3231
|
+
const places = mergeKeyedRecord(entry.places, config.places);
|
|
3232
|
+
const products = mergeKeyedRecord(entry.products, config.products);
|
|
3233
|
+
const universe = mergeUniverse(entry.universe, config.universe);
|
|
3234
|
+
const state = entry.state ?? config.state;
|
|
3235
|
+
const { places: _placesRoot, products: _productsRoot, universe: _universeRoot, ...rest } = config;
|
|
3236
|
+
return {
|
|
3237
|
+
...rest,
|
|
3238
|
+
...passes === void 0 ? {} : { passes },
|
|
3239
|
+
...places === void 0 ? {} : { places },
|
|
3240
|
+
...products === void 0 ? {} : { products },
|
|
3241
|
+
...state === void 0 ? {} : { state },
|
|
3242
|
+
...universe === void 0 ? {} : { universe }
|
|
2445
3243
|
};
|
|
3244
|
+
}
|
|
3245
|
+
function unknownEnvironment(config, environment) {
|
|
2446
3246
|
return {
|
|
2447
|
-
|
|
2448
|
-
|
|
3247
|
+
declared: Object.keys(config.environments),
|
|
3248
|
+
environment,
|
|
3249
|
+
kind: "unknownEnvironment"
|
|
2449
3250
|
};
|
|
2450
3251
|
}
|
|
2451
3252
|
function findIncompleteUniverse(projected, environment) {
|
|
@@ -2476,22 +3277,6 @@ function findIncompletePlace(projected, environment) {
|
|
|
2476
3277
|
};
|
|
2477
3278
|
}
|
|
2478
3279
|
}
|
|
2479
|
-
function mergeEntry(overlay, base) {
|
|
2480
|
-
return defu(overlay, base ?? {});
|
|
2481
|
-
}
|
|
2482
|
-
function mergeKeyedRecord(overlay, base) {
|
|
2483
|
-
if (overlay === void 0) return base;
|
|
2484
|
-
return {
|
|
2485
|
-
...base ?? {},
|
|
2486
|
-
...Object.fromEntries(Object.entries(overlay).map(([key, partial]) => {
|
|
2487
|
-
return [key, mergeEntry(partial, base?.[key])];
|
|
2488
|
-
}))
|
|
2489
|
-
};
|
|
2490
|
-
}
|
|
2491
|
-
function mergeUniverse(overlay, base) {
|
|
2492
|
-
if (overlay === void 0 && base === void 0) return;
|
|
2493
|
-
return defu(overlay ?? {}, base ?? {});
|
|
2494
|
-
}
|
|
2495
3280
|
function resolvePrefix(config, entry) {
|
|
2496
3281
|
if (config.displayNamePrefix?.enabled === false) return;
|
|
2497
3282
|
const { label } = entry;
|
|
@@ -2515,33 +3300,18 @@ function applyPlacesPrefix(places, prefix) {
|
|
|
2515
3300
|
}];
|
|
2516
3301
|
}));
|
|
2517
3302
|
}
|
|
2518
|
-
function
|
|
2519
|
-
const { config, entry } = inputs;
|
|
2520
|
-
const
|
|
2521
|
-
const mergedPlaces = mergeKeyedRecord(entry.places, config.places);
|
|
2522
|
-
const products = mergeKeyedRecord(entry.products, config.products);
|
|
2523
|
-
const merged = mergeUniverse(entry.universe, config.universe);
|
|
3303
|
+
function redactAndPrefix(inputs) {
|
|
3304
|
+
const { config, entry, merged } = inputs;
|
|
3305
|
+
const redacted = applyRedaction(merged, entry.redacted);
|
|
2524
3306
|
const prefix = resolvePrefix(config, entry);
|
|
2525
|
-
const
|
|
2526
|
-
const
|
|
2527
|
-
const state = entry.state ?? config.state;
|
|
2528
|
-
const { places: _placesRoot, products: _productsRoot, universe: _universeRoot, ...rest } = config;
|
|
3307
|
+
const places = applyPlacesPrefix(redacted.places, prefix);
|
|
3308
|
+
const universe = applyUniversePrefix(redacted.universe, prefix);
|
|
2529
3309
|
return {
|
|
2530
|
-
...
|
|
2531
|
-
...passes === void 0 ? {} : { passes },
|
|
3310
|
+
...redacted,
|
|
2532
3311
|
...places === void 0 ? {} : { places },
|
|
2533
|
-
...products === void 0 ? {} : { products },
|
|
2534
|
-
...state === void 0 ? {} : { state },
|
|
2535
3312
|
...universe === void 0 ? {} : { universe }
|
|
2536
3313
|
};
|
|
2537
3314
|
}
|
|
2538
|
-
function unknownEnvironment(config, environment) {
|
|
2539
|
-
return {
|
|
2540
|
-
declared: Object.keys(config.environments),
|
|
2541
|
-
environment,
|
|
2542
|
-
kind: "unknownEnvironment"
|
|
2543
|
-
};
|
|
2544
|
-
}
|
|
2545
3315
|
//#endregion
|
|
2546
3316
|
//#region src/core/validate-plan.ts
|
|
2547
3317
|
/**
|
|
@@ -4159,6 +4929,14 @@ function buildRootPasses(primaryFold) {
|
|
|
4159
4929
|
if (primaryFold.passes.length === 0) return;
|
|
4160
4930
|
return Object.fromEntries(primaryFold.passes.map(({ key, entry }) => [key, entry]));
|
|
4161
4931
|
}
|
|
4932
|
+
function buildFullPassOverlay(entry) {
|
|
4933
|
+
return {
|
|
4934
|
+
name: entry.name,
|
|
4935
|
+
description: entry.description,
|
|
4936
|
+
icon: entry.icon,
|
|
4937
|
+
...entry.price !== void 0 && { price: entry.price }
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4162
4940
|
function buildPassOverlayEntry(entry, primary) {
|
|
4163
4941
|
const overlay = {};
|
|
4164
4942
|
if (!Object.is(primary.name, entry.name)) overlay.name = entry.name;
|
|
@@ -4172,7 +4950,7 @@ function buildPassesOverlay(fold, primary) {
|
|
|
4172
4950
|
const overlay = {};
|
|
4173
4951
|
for (const { key, entry } of fold.passes) {
|
|
4174
4952
|
const primaryEntry = primaryByKey.get(key);
|
|
4175
|
-
const passOverlay = primaryEntry === void 0 ?
|
|
4953
|
+
const passOverlay = primaryEntry === void 0 ? buildFullPassOverlay(entry) : buildPassOverlayEntry(entry, primaryEntry);
|
|
4176
4954
|
if (passOverlay !== void 0) overlay[key] = passOverlay;
|
|
4177
4955
|
}
|
|
4178
4956
|
return Object.keys(overlay).length === 0 ? void 0 : overlay;
|
|
@@ -5735,6 +6513,6 @@ function isFileMissing(err) {
|
|
|
5735
6513
|
return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && FILE_MISSING_CODES.has(err.code);
|
|
5736
6514
|
}
|
|
5737
6515
|
//#endregion
|
|
5738
|
-
export {
|
|
6516
|
+
export { createGamePassDriver as A, createClackProgressAdapter as B, createPlaceDriver as C, parseStateFile as D, createGistStateAdapter as E, asSha256Hex as F, renderMigrationSummary as G, renderDeployError as H, isResourceKey as I, renderParseError as K, isRobloxAssetId as L, shouldReuploadIcon as M, asResourceKey as N, serializeStateFile as O, asRobloxAssetId as P, isSha256Hex as R, createUniverseDriver as S, UNIVERSE_SINGLETON_KEY as T, renderMigrateError as U, renderBuildStatePortError as V, renderMigrateParseError as W, diff as _, buildStatePort as a, validateConfig as b, applyOps as c, selectMergedEnvironment as d, collectRedactionAnnotations as f, renderDisplayNamePrefix as g, DEFAULT_PREFIX_FORMAT as h, loadConfig$1 as i, createDeveloperProductDriver as j, validateEnvironmentName as k, validatePlan as l, flattenConfig as m, serializeConfig as n, buildDesired as o, resolveStateConfig as p, renderStateWriteError as q, deploy as r, buildDefaultRegistry as s, migrateMantleState as t, selectEnvironment as u, defaultKindRegistry as v, SOCIAL_LINK_FIELDS as w, createClackPort as x, isGistStateConfig as y, derivePriceFields as z };
|
|
5739
6517
|
|
|
5740
|
-
//# sourceMappingURL=migrate-mantle-state-
|
|
6518
|
+
//# sourceMappingURL=migrate-mantle-state-CQjWBZwT.mjs.map
|