@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.
@@ -1,5 +1,6 @@
1
+ import { ApiError, PermissionError } from "@bedrock-rbx/ocale";
1
2
  import { ArkErrors, type } from "arktype";
2
- import { ApiError } from "@bedrock-rbx/ocale";
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(deps, desired);
944
+ return createOne(effective, desired);
488
945
  },
489
946
  async update(current, desired) {
490
- return updateOne(deps, {
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
- return { async create(desired) {
643
- const imageFile = await deps.readFile(desired.icon["en-us"]);
644
- const result = await deps.client.create({
645
- name: desired.name,
646
- description: desired.description,
647
- imageFile,
648
- universeId: deps.universeId,
649
- ...desired.price !== void 0 ? { price: desired.price } : {}
650
- });
651
- if (!result.success) return result;
652
- return toCurrentState$1(desired, result.data);
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?": type({ [`[/${RESOURCE_KEY_PATTERN_SOURCE}/]`]: placeOverlay }).onUndeclaredKey("reject"),
2266
+ "places?": placesOverlayCollection,
1684
2267
  "products?": productsOverlayCollection,
2268
+ [REDACTED_KEY]: OPTIONAL_BOOLEAN$2,
1685
2269
  "state?": stateConfig,
1686
- "universe?": universeEntry
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 entry = config.environments[environment];
2428
- if (entry === void 0) return {
2429
- err: unknownEnvironment(config, environment),
2430
- success: false
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
- const projected = projectConfig({
2433
- config,
2434
- entry
2435
- });
2436
- const incompletePlace = findIncompletePlace(projected, environment);
2437
- if (incompletePlace !== void 0) return {
2438
- err: incompletePlace,
2439
- success: false
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
- const incompleteUniverse = findIncompleteUniverse(projected, environment);
2442
- if (incompleteUniverse !== void 0) return {
2443
- err: incompleteUniverse,
2444
- success: false
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
- data: projected,
2448
- success: true
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 projectConfig(inputs) {
2519
- const { config, entry } = inputs;
2520
- const passes = mergeKeyedRecord(entry.passes, config.passes);
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 universe = applyUniversePrefix(merged, prefix);
2526
- const places = applyPlacesPrefix(mergedPlaces, prefix);
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
- ...rest,
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 ? { ...entry } : buildPassOverlayEntry(entry, primaryEntry);
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 { asResourceKey as A, createGistStateAdapter as C, createGamePassDriver as D, validateEnvironmentName as E, isSha256Hex as F, derivePriceFields as I, asSha256Hex as M, isResourceKey as N, createDeveloperProductDriver as O, isRobloxAssetId as P, UNIVERSE_SINGLETON_KEY as S, serializeStateFile as T, isGistStateConfig as _, buildStatePort as a, createPlaceDriver as b, applyOps as c, resolveStateConfig as d, flattenConfig as f, defaultKindRegistry as g, diff as h, loadConfig$1 as i, asRobloxAssetId as j, shouldReuploadIcon as k, validatePlan as l, renderDisplayNamePrefix as m, serializeConfig as n, buildDesired as o, DEFAULT_PREFIX_FORMAT as p, deploy as r, buildDefaultRegistry as s, migrateMantleState as t, selectEnvironment as u, validateConfig as v, parseStateFile as w, SOCIAL_LINK_FIELDS as x, createUniverseDriver as y };
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-DjAU-I39.mjs.map
6518
+ //# sourceMappingURL=migrate-mantle-state-CQjWBZwT.mjs.map