@bedrock-rbx/core 0.1.0-beta.14 → 0.1.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -0
- package/dist/cli/run.mjs +135 -37
- package/dist/cli/run.mjs.map +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/{define-config-C2cOtDpP.d.mts → define-config-B4GZRPj-.d.mts} +3 -3
- package/dist/{define-config-C2cOtDpP.d.mts.map → define-config-B4GZRPj-.d.mts.map} +1 -1
- package/dist/index.d.mts +251 -95
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -26
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-mantle-state-qejWFAR0.mjs → migrate-mantle-state-F4zdhxV4.mjs} +513 -178
- package/dist/migrate-mantle-state-F4zdhxV4.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/migrate-mantle-state-qejWFAR0.mjs.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { cancel, intro, log, outro } from "@clack/prompts";
|
|
1
2
|
import { ApiError, PermissionError } from "@bedrock-rbx/ocale";
|
|
2
3
|
import { ArkErrors, type } from "arktype";
|
|
3
|
-
import {
|
|
4
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import { defu } from "defu";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
@@ -12,9 +13,51 @@ import { readFile } from "node:fs/promises";
|
|
|
12
13
|
import { loadConfig } from "c12";
|
|
13
14
|
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
|
-
import { execFile } from "node:child_process";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { parseYAML, stringifyYAML } from "confbox";
|
|
18
|
+
//#region src/cli/clack-port.ts
|
|
19
|
+
/**
|
|
20
|
+
* Construct a `ClackPort` whose methods delegate to `@clack/prompts`. The
|
|
21
|
+
* resulting port writes to `process.stdout` via clack's defaults. Kept in
|
|
22
|
+
* its own module so consumers that never need the clack-backed rendering
|
|
23
|
+
* (programmatic deploys, custom adapters) do not pull `@clack/prompts`
|
|
24
|
+
* into their bundle.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { createClackPort } from "@bedrock-rbx/core";
|
|
30
|
+
*
|
|
31
|
+
* const port = createClackPort();
|
|
32
|
+
*
|
|
33
|
+
* expect(typeof port.logSuccess).toBe("function");
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @returns A port whose six methods each invoke the matching clack helper.
|
|
37
|
+
*/
|
|
38
|
+
function createClackPort() {
|
|
39
|
+
return {
|
|
40
|
+
cancel: (message) => {
|
|
41
|
+
cancel(message);
|
|
42
|
+
},
|
|
43
|
+
intro: (message) => {
|
|
44
|
+
intro(message);
|
|
45
|
+
},
|
|
46
|
+
logError: (message) => {
|
|
47
|
+
log.error(message);
|
|
48
|
+
},
|
|
49
|
+
logMessage: (message) => {
|
|
50
|
+
log.message(message);
|
|
51
|
+
},
|
|
52
|
+
logSuccess: (message) => {
|
|
53
|
+
log.success(message);
|
|
54
|
+
},
|
|
55
|
+
outro: (message) => {
|
|
56
|
+
outro(message);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
18
61
|
//#region src/cli/render.ts
|
|
19
62
|
/**
|
|
20
63
|
* Render a `DeployError` to the supplied `ClackPort`. Most variants emit a
|
|
@@ -46,6 +89,30 @@ function renderParseError(err, port) {
|
|
|
46
89
|
port.logError(parseErrorMessage(err));
|
|
47
90
|
}
|
|
48
91
|
/**
|
|
92
|
+
* Render a `SpawnOverrideError` to the supplied `ClackPort` as a single
|
|
93
|
+
* error line that names the environment alongside the failure mode. On
|
|
94
|
+
* `launchFailed` the child never produced output of its own, so the parent
|
|
95
|
+
* carries the diagnostic; on `nonZeroExit` the parent's line attributes the
|
|
96
|
+
* exit code to a specific environment when several spawns are running.
|
|
97
|
+
* @param input - Environment + spawn-override error to describe.
|
|
98
|
+
* @param port - The output port the diagnostic is written to.
|
|
99
|
+
*/
|
|
100
|
+
function renderOverrideError(input, port) {
|
|
101
|
+
port.logError(overrideErrorMessage(input));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Render the failure surfaced when override discovery throws a non-absence
|
|
105
|
+
* filesystem error (for example `EACCES` on a `.bedrock/<command>.ts` that
|
|
106
|
+
* exists but cannot be read). Discovery refuses to fall through to the
|
|
107
|
+
* built-in path in that case, so the CLI reports the cause and exits rather
|
|
108
|
+
* than crashing on the unhandled throw.
|
|
109
|
+
* @param error - The value thrown during override discovery.
|
|
110
|
+
* @param port - The output port the diagnostic is written to.
|
|
111
|
+
*/
|
|
112
|
+
function renderOverrideDiscoveryError(error, port) {
|
|
113
|
+
port.logError(`override discovery failed: ${safeStringify(error)}`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
49
116
|
* Render a `ParseMigrateError` to the supplied `ClackPort`. Reuses
|
|
50
117
|
* `parseErrorMessage` for the three flag-shape variants and adds a
|
|
51
118
|
* dedicated message for `unknownSource` listing the supported sources.
|
|
@@ -182,6 +249,11 @@ function parseErrorMessage(err) {
|
|
|
182
249
|
case "unknownFlag": return `unknown flag '--${err.flag}'`;
|
|
183
250
|
}
|
|
184
251
|
}
|
|
252
|
+
function overrideErrorMessage(input) {
|
|
253
|
+
const { environment, err } = input;
|
|
254
|
+
if (err.kind === "launchFailed") return `${environment}: failed to launch override - ${err.cause.message}`;
|
|
255
|
+
return `${environment}: override exited with code ${String(err.exitCode)}`;
|
|
256
|
+
}
|
|
185
257
|
function migrateParseErrorMessage(err) {
|
|
186
258
|
if (err.kind === "unknownSource") return `unknown migration source '${err.received}' (supported: ${err.supported.join(", ")})`;
|
|
187
259
|
return parseErrorMessage(err);
|
|
@@ -1235,6 +1307,27 @@ function createClackProgressAdapter(deps) {
|
|
|
1235
1307
|
renderEvent(event, deps);
|
|
1236
1308
|
} };
|
|
1237
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Build a {@link ProgressPort} for the default CLI rendering path: wires a
|
|
1312
|
+
* fresh {@link createClackPort} into {@link createClackProgressAdapter}. The
|
|
1313
|
+
* `config` argument (raw `Config` or env-resolved `ResolvedConfig`) is
|
|
1314
|
+
* forwarded so `stateWritten` events can name the persistence backend; pass
|
|
1315
|
+
* `undefined` when the config has not yet loaded.
|
|
1316
|
+
*
|
|
1317
|
+
* Internal: used by `deploy()`'s default-port resolver when callers omit
|
|
1318
|
+
* `progress` and `BEDROCK_CLI` is set.
|
|
1319
|
+
*
|
|
1320
|
+
* @param config - Pre-loaded or env-resolved config used to format the
|
|
1321
|
+
* state-backend label, or `undefined` to render the generic placeholder.
|
|
1322
|
+
* @returns A clack-backed `ProgressPort` that writes to `process.stdout`.
|
|
1323
|
+
*/
|
|
1324
|
+
function createDefaultProgressAdapter(config) {
|
|
1325
|
+
const clack = createClackPort();
|
|
1326
|
+
return config === void 0 ? createClackProgressAdapter({ clack }) : createClackProgressAdapter({
|
|
1327
|
+
clack,
|
|
1328
|
+
config
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1238
1331
|
function applySummaryLine(event) {
|
|
1239
1332
|
return `Succeeded in ${(event.durationMs / 1e3).toFixed(1)}s: ${[
|
|
1240
1333
|
`${event.created} create`,
|
|
@@ -1800,7 +1893,9 @@ const GITHUB_API_BASE = "https://api.github.com";
|
|
|
1800
1893
|
const GITHUB_API_VERSION = "2026-03-10";
|
|
1801
1894
|
const USER_AGENT = "bedrock";
|
|
1802
1895
|
const MAX_INLINE_BYTES = 1e7;
|
|
1803
|
-
const MAX_RETRIES =
|
|
1896
|
+
const MAX_RETRIES = 6;
|
|
1897
|
+
const BASE_BACKOFF_MS = 500;
|
|
1898
|
+
const MAX_BACKOFF_MS = 16e3;
|
|
1804
1899
|
const RETRYABLE_STATUSES = new Set([
|
|
1805
1900
|
409,
|
|
1806
1901
|
502,
|
|
@@ -1843,6 +1938,7 @@ function createGistStateAdapter(deps) {
|
|
|
1843
1938
|
const ctx = {
|
|
1844
1939
|
fetchFn: deps.fetch ?? globalThis.fetch.bind(globalThis),
|
|
1845
1940
|
gistId: deps.gistId,
|
|
1941
|
+
random: deps.random ?? Math.random,
|
|
1846
1942
|
sleep: deps.sleep ?? defaultSleep,
|
|
1847
1943
|
token: deps.token
|
|
1848
1944
|
};
|
|
@@ -1938,14 +2034,15 @@ async function sendGet(ctx) {
|
|
|
1938
2034
|
function isRetryableStatus(status) {
|
|
1939
2035
|
return RETRYABLE_STATUSES.has(status);
|
|
1940
2036
|
}
|
|
1941
|
-
function backoffMs(attempt) {
|
|
1942
|
-
|
|
2037
|
+
function backoffMs(attempt, random) {
|
|
2038
|
+
const half = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** attempt) / 2;
|
|
2039
|
+
return half + random() * half;
|
|
1943
2040
|
}
|
|
1944
|
-
async function withRetry(
|
|
2041
|
+
async function withRetry(retry, operation) {
|
|
1945
2042
|
let response = await operation();
|
|
1946
2043
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
|
|
1947
2044
|
if (response.ok || !isRetryableStatus(response.status)) return response;
|
|
1948
|
-
await sleep(backoffMs(attempt));
|
|
2045
|
+
await retry.sleep(backoffMs(attempt, retry.random));
|
|
1949
2046
|
response = await operation();
|
|
1950
2047
|
}
|
|
1951
2048
|
return response;
|
|
@@ -1953,7 +2050,7 @@ async function withRetry(sleep, operation) {
|
|
|
1953
2050
|
async function fetchGistBody(ctx, file) {
|
|
1954
2051
|
let response;
|
|
1955
2052
|
try {
|
|
1956
|
-
response = await withRetry(ctx
|
|
2053
|
+
response = await withRetry(ctx, async () => sendGet(ctx));
|
|
1957
2054
|
} catch (err) {
|
|
1958
2055
|
return {
|
|
1959
2056
|
err: networkError(err, file),
|
|
@@ -1983,14 +2080,14 @@ function stateErr(file, reason) {
|
|
|
1983
2080
|
success: false
|
|
1984
2081
|
};
|
|
1985
2082
|
}
|
|
1986
|
-
async function readGistContent({ entry, fetchFn, file,
|
|
2083
|
+
async function readGistContent({ entry, fetchFn, file, retry }) {
|
|
1987
2084
|
if (entry.size > MAX_INLINE_BYTES) return stateErr(file, `state file too large: ${entry.size} bytes`);
|
|
1988
2085
|
if (entry.isTruncated) {
|
|
1989
2086
|
if (entry.rawUrl === void 0) return stateErr(file, "truncated gist file missing raw_url");
|
|
1990
2087
|
const { rawUrl } = entry;
|
|
1991
2088
|
let rawResponse;
|
|
1992
2089
|
try {
|
|
1993
|
-
rawResponse = await withRetry(
|
|
2090
|
+
rawResponse = await withRetry(retry, async () => fetchFn(rawUrl));
|
|
1994
2091
|
} catch (err) {
|
|
1995
2092
|
return {
|
|
1996
2093
|
err: networkError(err, file),
|
|
@@ -2016,7 +2113,7 @@ async function readPath(ctx, environment) {
|
|
|
2016
2113
|
entry,
|
|
2017
2114
|
fetchFn: ctx.fetchFn,
|
|
2018
2115
|
file,
|
|
2019
|
-
|
|
2116
|
+
retry: ctx
|
|
2020
2117
|
});
|
|
2021
2118
|
}
|
|
2022
2119
|
async function sendPatch(ctx, body) {
|
|
@@ -2065,7 +2162,7 @@ async function writePath(ctx, state) {
|
|
|
2065
2162
|
const body = JSON.stringify({ files: { [fileName(state.environment)]: { content: serializeStateFile(state) } } });
|
|
2066
2163
|
let response;
|
|
2067
2164
|
try {
|
|
2068
|
-
response = await withRetry(ctx
|
|
2165
|
+
response = await withRetry(ctx, async () => sendPatch(ctx, body));
|
|
2069
2166
|
} catch (err) {
|
|
2070
2167
|
return {
|
|
2071
2168
|
err: networkError(err, file),
|
|
@@ -2092,6 +2189,30 @@ async function writePath(ctx, state) {
|
|
|
2092
2189
|
};
|
|
2093
2190
|
}
|
|
2094
2191
|
//#endregion
|
|
2192
|
+
//#region src/adapters/no-op-progress-adapter.ts
|
|
2193
|
+
/**
|
|
2194
|
+
* Build a {@link ProgressPort} that silently drops every event. Useful for
|
|
2195
|
+
* tests and programmatic callers who want to invoke deploy logic without
|
|
2196
|
+
* any rendering.
|
|
2197
|
+
*
|
|
2198
|
+
* @example
|
|
2199
|
+
*
|
|
2200
|
+
* ```ts
|
|
2201
|
+
* import { createNoOpProgressAdapter } from "@bedrock-rbx/core";
|
|
2202
|
+
*
|
|
2203
|
+
* const port = createNoOpProgressAdapter();
|
|
2204
|
+
*
|
|
2205
|
+
* expect(() =>
|
|
2206
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 }),
|
|
2207
|
+
* ).not.toThrow();
|
|
2208
|
+
* ```
|
|
2209
|
+
*
|
|
2210
|
+
* @returns A `ProgressPort` whose `emit` method is a no-op.
|
|
2211
|
+
*/
|
|
2212
|
+
function createNoOpProgressAdapter() {
|
|
2213
|
+
return { emit() {} };
|
|
2214
|
+
}
|
|
2215
|
+
//#endregion
|
|
2095
2216
|
//#region src/core/resources.ts
|
|
2096
2217
|
/**
|
|
2097
2218
|
* Ordered list of optional metadata fields the driver routes through
|
|
@@ -2470,46 +2591,192 @@ async function reconcileUniverse(inputs) {
|
|
|
2470
2591
|
};
|
|
2471
2592
|
}
|
|
2472
2593
|
//#endregion
|
|
2473
|
-
//#region src/cli/
|
|
2594
|
+
//#region src/cli/default-spawner.ts
|
|
2474
2595
|
/**
|
|
2475
|
-
*
|
|
2476
|
-
*
|
|
2477
|
-
*
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2480
|
-
*
|
|
2596
|
+
* Translate a `child.on("close", code, signal)` payload into the
|
|
2597
|
+
* {@link Spawner.spawn} return shape. Extracted from the adapter so the
|
|
2598
|
+
* signal-terminated branch can be exercised without launching a real
|
|
2599
|
+
* process. The caller normalizes node's `null` to `undefined` at the
|
|
2600
|
+
* boundary so this helper never sees `null`.
|
|
2601
|
+
* @param code - Exit code reported by the child, or `undefined` if the
|
|
2602
|
+
* child was terminated by a signal before exiting.
|
|
2603
|
+
* @param signal - Signal name reported by the child, or `undefined` when
|
|
2604
|
+
* no signal terminated it.
|
|
2605
|
+
* @returns `Ok(code)` for a clean exit (including `0`); otherwise
|
|
2606
|
+
* `Err(launchFailed)` carrying a synthetic Error whose message names
|
|
2607
|
+
* the signal.
|
|
2608
|
+
*/
|
|
2609
|
+
function classifySpawnClose(code, signal) {
|
|
2610
|
+
if (code !== void 0) return {
|
|
2611
|
+
data: code,
|
|
2612
|
+
success: true
|
|
2613
|
+
};
|
|
2614
|
+
return {
|
|
2615
|
+
err: {
|
|
2616
|
+
cause: /* @__PURE__ */ new Error(`spawned process terminated by signal ${signal ?? "unknown"}`),
|
|
2617
|
+
kind: "launchFailed"
|
|
2618
|
+
},
|
|
2619
|
+
success: false
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Construct a {@link Spawner} backed by `node:child_process.spawn` with
|
|
2624
|
+
* `stdio` inherited from the parent process. The child's environment is
|
|
2625
|
+
* `process.env` overlaid with {@link SpawnInvocation.envOverrides} (overrides
|
|
2626
|
+
* win on key collision).
|
|
2627
|
+
*
|
|
2628
|
+
* - Exit codes resolve as `Ok(exitCode)` (including `0`).
|
|
2629
|
+
* - `ENOENT` and other launch-time errors resolve as `Err(launchFailed)`
|
|
2630
|
+
* with the original error in `cause` (its `code` field carries the
|
|
2631
|
+
* errno where present).
|
|
2632
|
+
* - Children terminated by signal before producing an exit code collapse
|
|
2633
|
+
* into `launchFailed` with a synthetic `Error` whose message names the
|
|
2634
|
+
* signal; a distinct variant lands the day a caller needs to act on the
|
|
2635
|
+
* difference.
|
|
2636
|
+
*
|
|
2637
|
+
* @returns A `Spawner` whose `spawn` settles once the child closes.
|
|
2481
2638
|
* @example
|
|
2482
2639
|
*
|
|
2483
2640
|
* ```ts
|
|
2484
|
-
* import {
|
|
2641
|
+
* import { createDefaultSpawner } from "@bedrock-rbx/core";
|
|
2642
|
+
* import process from "node:process";
|
|
2485
2643
|
*
|
|
2486
|
-
* const
|
|
2644
|
+
* const spawner = createDefaultSpawner();
|
|
2487
2645
|
*
|
|
2488
|
-
*
|
|
2646
|
+
* return spawner
|
|
2647
|
+
* .spawn({
|
|
2648
|
+
* args: ["-e", "process.exit(0)"],
|
|
2649
|
+
* command: process.execPath,
|
|
2650
|
+
* envOverrides: {},
|
|
2651
|
+
* })
|
|
2652
|
+
* .then((result) => {
|
|
2653
|
+
* expect(result.success).toBeTrue();
|
|
2654
|
+
* if (result.success) {
|
|
2655
|
+
* expect(result.data).toBe(0);
|
|
2656
|
+
* }
|
|
2657
|
+
* });
|
|
2489
2658
|
* ```
|
|
2659
|
+
*/
|
|
2660
|
+
function createDefaultSpawner() {
|
|
2661
|
+
return { spawn: spawnViaChildProcess };
|
|
2662
|
+
}
|
|
2663
|
+
async function spawnViaChildProcess(invocation) {
|
|
2664
|
+
return new Promise((resolve) => {
|
|
2665
|
+
const child = spawn(invocation.command, [...invocation.args], {
|
|
2666
|
+
env: {
|
|
2667
|
+
...process.env,
|
|
2668
|
+
...invocation.envOverrides
|
|
2669
|
+
},
|
|
2670
|
+
stdio: "inherit"
|
|
2671
|
+
});
|
|
2672
|
+
child.once("error", (error) => {
|
|
2673
|
+
resolve({
|
|
2674
|
+
err: {
|
|
2675
|
+
cause: error,
|
|
2676
|
+
kind: "launchFailed"
|
|
2677
|
+
},
|
|
2678
|
+
success: false
|
|
2679
|
+
});
|
|
2680
|
+
});
|
|
2681
|
+
child.once("close", (code, signal) => {
|
|
2682
|
+
resolve(classifySpawnClose(code ?? void 0, signal ?? void 0));
|
|
2683
|
+
});
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
//#endregion
|
|
2687
|
+
//#region src/cli/credential-environment-overrides.ts
|
|
2688
|
+
/**
|
|
2689
|
+
* Map CLI credential flags to their corresponding env-var names, omitting
|
|
2690
|
+
* entries whose flag is `undefined`.
|
|
2691
|
+
* @param flags - CLI credential flag values to translate.
|
|
2692
|
+
* @returns An immutable record of env-var names to their override values.
|
|
2693
|
+
*/
|
|
2694
|
+
function buildCredentialOverrides(flags) {
|
|
2695
|
+
const overrides = {};
|
|
2696
|
+
if (flags.apiKey !== void 0) overrides["BEDROCK_API_KEY"] = flags.apiKey;
|
|
2697
|
+
if (flags.githubToken !== void 0) overrides["BEDROCK_GITHUB_TOKEN"] = flags.githubToken;
|
|
2698
|
+
return overrides;
|
|
2699
|
+
}
|
|
2700
|
+
//#endregion
|
|
2701
|
+
//#region src/cli/dispatch-override.ts
|
|
2702
|
+
/**
|
|
2703
|
+
* Dispatch a single `.bedrock/<command>.ts` override invocation through the
|
|
2704
|
+
* supplied {@link Spawner}. Encapsulates the spawn protocol:
|
|
2705
|
+
*
|
|
2706
|
+
* - argv = `[overridePath, "--env", environment]`, with `"--config", configFile`
|
|
2707
|
+
* appended when supplied.
|
|
2708
|
+
* - `apiKey` becomes the `BEDROCK_API_KEY` env-var override; `githubToken`
|
|
2709
|
+
* becomes `BEDROCK_GITHUB_TOKEN`. Neither value appears in argv.
|
|
2710
|
+
* - `BEDROCK_CLI=1` is always set in the env. The override's `deploy()`
|
|
2711
|
+
* reads this on the `getEnv` seam to default to the clack progress
|
|
2712
|
+
* adapter; absent that downstream wiring, the variable is a forward-
|
|
2713
|
+
* compatible signal a future caller can act on.
|
|
2714
|
+
*
|
|
2715
|
+
* The dispatcher itself reads no ambient state: every input arrives via the
|
|
2716
|
+
* `invocation` argument and the `Spawner` port is the only side-effect seam.
|
|
2717
|
+
*
|
|
2718
|
+
* @param invocation - Path, environment, and parsed deploy-flag inputs.
|
|
2719
|
+
* @param spawner - Port the dispatcher hands the resolved
|
|
2720
|
+
* {@link SpawnInvocation} to.
|
|
2721
|
+
* @returns `Ok(undefined)` when the child exited zero; otherwise an
|
|
2722
|
+
* {@link SpawnOverrideError} discriminating launch vs non-zero exit.
|
|
2490
2723
|
*
|
|
2491
|
-
* @
|
|
2724
|
+
* @example
|
|
2725
|
+
*
|
|
2726
|
+
* ```ts
|
|
2727
|
+
* import { dispatchOverride, type Spawner } from "@bedrock-rbx/core";
|
|
2728
|
+
*
|
|
2729
|
+
* const spawner: Spawner = {
|
|
2730
|
+
* async spawn() {
|
|
2731
|
+
* return { data: 0, success: true };
|
|
2732
|
+
* },
|
|
2733
|
+
* };
|
|
2734
|
+
*
|
|
2735
|
+
* return dispatchOverride(
|
|
2736
|
+
* {
|
|
2737
|
+
* environment: "production",
|
|
2738
|
+
* overridePath: "/abs/.bedrock/deploy.ts",
|
|
2739
|
+
* },
|
|
2740
|
+
* spawner,
|
|
2741
|
+
* ).then((result) => {
|
|
2742
|
+
* expect(result.success).toBeTrue();
|
|
2743
|
+
* });
|
|
2744
|
+
* ```
|
|
2492
2745
|
*/
|
|
2493
|
-
function
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2746
|
+
async function dispatchOverride(invocation, spawner) {
|
|
2747
|
+
const args = [
|
|
2748
|
+
invocation.overridePath,
|
|
2749
|
+
"--env",
|
|
2750
|
+
invocation.environment
|
|
2751
|
+
];
|
|
2752
|
+
if (invocation.configFile !== void 0) args.push("--config", invocation.configFile);
|
|
2753
|
+
const credentialOverrides = buildCredentialOverrides(invocation);
|
|
2754
|
+
const launched = await spawner.spawn({
|
|
2755
|
+
args,
|
|
2756
|
+
command: "bun",
|
|
2757
|
+
envOverrides: {
|
|
2758
|
+
...credentialOverrides,
|
|
2759
|
+
BEDROCK_CLI: "1"
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
if (!launched.success) return {
|
|
2763
|
+
err: {
|
|
2764
|
+
cause: launched.err.cause,
|
|
2765
|
+
kind: "launchFailed"
|
|
2506
2766
|
},
|
|
2507
|
-
|
|
2508
|
-
|
|
2767
|
+
success: false
|
|
2768
|
+
};
|
|
2769
|
+
const exitCode = launched.data;
|
|
2770
|
+
if (exitCode !== 0) return {
|
|
2771
|
+
err: {
|
|
2772
|
+
exitCode,
|
|
2773
|
+
kind: "nonZeroExit"
|
|
2509
2774
|
},
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2775
|
+
success: false
|
|
2776
|
+
};
|
|
2777
|
+
return {
|
|
2778
|
+
data: void 0,
|
|
2779
|
+
success: true
|
|
2513
2780
|
};
|
|
2514
2781
|
}
|
|
2515
2782
|
//#endregion
|
|
@@ -2600,7 +2867,7 @@ function assertReconcilable(current, desired) {
|
|
|
2600
2867
|
/**
|
|
2601
2868
|
* Resource-kind module for Roblox developer products. Owns the entry
|
|
2602
2869
|
* schema, flattening, icon-hash normalization, drift-equality, and the
|
|
2603
|
-
*
|
|
2870
|
+
* pre-reconcile icon-removal rejection for the `developerProduct` kind.
|
|
2604
2871
|
*/
|
|
2605
2872
|
const developerProductKind = {
|
|
2606
2873
|
assertReconcilable,
|
|
@@ -2890,7 +3157,7 @@ const defaultKindRegistry = {
|
|
|
2890
3157
|
*
|
|
2891
3158
|
* @param desired - Declared desired state from user config, already normalized
|
|
2892
3159
|
* (file hashes computed, nullable wire values mapped to `undefined`).
|
|
2893
|
-
* @param current - Last-known
|
|
3160
|
+
* @param current - Last-known current state from the state file.
|
|
2894
3161
|
* @returns Operations to reconcile the two snapshots.
|
|
2895
3162
|
*
|
|
2896
3163
|
* @example
|
|
@@ -3100,7 +3367,7 @@ const PLACE_ENV_FIELDS = ["description", "displayName"];
|
|
|
3100
3367
|
* disambiguating suffix on a redacted developer-product's default `name`.
|
|
3101
3368
|
* Stable across config edits (driven only by the bedrock resource key, not
|
|
3102
3369
|
* declaration order) and opaque to a Roblox player browsing the marketplace.
|
|
3103
|
-
* A natural collision is caught
|
|
3370
|
+
* A natural collision is caught before any apply-side driver I/O by `assertAllReconcilable`.
|
|
3104
3371
|
*
|
|
3105
3372
|
* @param key - Bedrock resource key for the developer product being redacted.
|
|
3106
3373
|
* @returns The first six lowercase hex characters of the SHA-256 digest of `key`.
|
|
@@ -3617,105 +3884,6 @@ function redactAndPrefix(inputs) {
|
|
|
3617
3884
|
};
|
|
3618
3885
|
}
|
|
3619
3886
|
//#endregion
|
|
3620
|
-
//#region src/core/validate-plan.ts
|
|
3621
|
-
/**
|
|
3622
|
-
* Plan-time invariant check that runs after `buildDesired` and before
|
|
3623
|
-
* `diff`. Walks paired `(kind, key)` entries and dispatches to each
|
|
3624
|
-
* kind module's optional `assertReconcilable` hook so kind-specific
|
|
3625
|
-
* rejections (e.g. Removing a developer-product icon, which the upstream
|
|
3626
|
-
* API has no documented unset path for) surface as typed errors before
|
|
3627
|
-
* `diff` runs and before any apply-side driver I/O is attempted.
|
|
3628
|
-
*
|
|
3629
|
-
* Pure and synchronous. Current-only entries (no matching desired) are
|
|
3630
|
-
* ignored: their reconciliation is `diff`'s concern, not this seam's.
|
|
3631
|
-
*
|
|
3632
|
-
* @param desired - Desired state from `buildDesired`.
|
|
3633
|
-
* @param current - Prior current state from the state port.
|
|
3634
|
-
* @returns `Ok(undefined)` when every paired entry passes its kind-level
|
|
3635
|
-
* reconcilability check, or the first `Err` returned by a hook.
|
|
3636
|
-
*
|
|
3637
|
-
* @example
|
|
3638
|
-
*
|
|
3639
|
-
* ```ts
|
|
3640
|
-
* import { asResourceKey, asRobloxAssetId, asSha256Hex, validatePlan } from "@bedrock-rbx/core";
|
|
3641
|
-
*
|
|
3642
|
-
* const result = validatePlan(
|
|
3643
|
-
* [
|
|
3644
|
-
* {
|
|
3645
|
-
* description: "Stocks the player up with 1,000 premium gems.",
|
|
3646
|
-
* isRegionalPricingEnabled: undefined,
|
|
3647
|
-
* key: asResourceKey("gem-pack"),
|
|
3648
|
-
* kind: "developerProduct",
|
|
3649
|
-
* name: "Gem Pack",
|
|
3650
|
-
* price: undefined,
|
|
3651
|
-
* storePageEnabled: undefined,
|
|
3652
|
-
* },
|
|
3653
|
-
* ],
|
|
3654
|
-
* [
|
|
3655
|
-
* {
|
|
3656
|
-
* description: "Stocks the player up with 1,000 premium gems.",
|
|
3657
|
-
* icon: { "en-us": "assets/gem-pack.png" },
|
|
3658
|
-
* iconFileHashes: {
|
|
3659
|
-
* "en-us": asSha256Hex(
|
|
3660
|
-
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3661
|
-
* ),
|
|
3662
|
-
* },
|
|
3663
|
-
* isRegionalPricingEnabled: undefined,
|
|
3664
|
-
* key: asResourceKey("gem-pack"),
|
|
3665
|
-
* kind: "developerProduct",
|
|
3666
|
-
* name: "Gem Pack",
|
|
3667
|
-
* outputs: { productId: asRobloxAssetId("9876543210") },
|
|
3668
|
-
* price: undefined,
|
|
3669
|
-
* storePageEnabled: undefined,
|
|
3670
|
-
* },
|
|
3671
|
-
* ],
|
|
3672
|
-
* );
|
|
3673
|
-
*
|
|
3674
|
-
* expect(result.success).toBeFalse();
|
|
3675
|
-
* if (!result.success) {
|
|
3676
|
-
* expect(result.err.kind).toBe("iconRemovalRejected");
|
|
3677
|
-
* }
|
|
3678
|
-
* ```
|
|
3679
|
-
*/
|
|
3680
|
-
function validatePlan(desired, current) {
|
|
3681
|
-
const collision = detectProductNameCollision(desired);
|
|
3682
|
-
if (collision !== void 0) return collision;
|
|
3683
|
-
const currentByKey = new Map(current.map((entry) => [compositeKey(entry), entry]));
|
|
3684
|
-
for (const entry of desired) {
|
|
3685
|
-
const matched = currentByKey.get(compositeKey(entry));
|
|
3686
|
-
if (matched === void 0) continue;
|
|
3687
|
-
const check = defaultKindRegistry[entry.kind].assertReconcilable?.(matched, entry);
|
|
3688
|
-
if (check !== void 0 && !check.success) return check;
|
|
3689
|
-
}
|
|
3690
|
-
return {
|
|
3691
|
-
data: void 0,
|
|
3692
|
-
success: true
|
|
3693
|
-
};
|
|
3694
|
-
}
|
|
3695
|
-
function compositeKey(resource) {
|
|
3696
|
-
return `${resource.kind}:${resource.key}`;
|
|
3697
|
-
}
|
|
3698
|
-
function detectProductNameCollision(desired) {
|
|
3699
|
-
const seenByName = /* @__PURE__ */ new Map();
|
|
3700
|
-
for (const entry of desired) {
|
|
3701
|
-
if (entry.kind !== "developerProduct") continue;
|
|
3702
|
-
const prior = seenByName.get(entry.name);
|
|
3703
|
-
if (prior === void 0) {
|
|
3704
|
-
seenByName.set(entry.name, entry.key);
|
|
3705
|
-
continue;
|
|
3706
|
-
}
|
|
3707
|
-
return {
|
|
3708
|
-
err: {
|
|
3709
|
-
keys: [prior, entry.key],
|
|
3710
|
-
kind: "redactedNameCollision",
|
|
3711
|
-
message: `developer products '${prior}' and '${entry.key}' both resolve to the wire name '${entry.name}'. Roblox enforces per-universe uniqueness on developer-product names, so the second update would be rejected as DuplicateProductName. Set 'redacted: { name: "<unique>" }' on one of them to disambiguate.`,
|
|
3712
|
-
resolvedName: entry.name
|
|
3713
|
-
},
|
|
3714
|
-
success: false
|
|
3715
|
-
};
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
//#endregion
|
|
3719
3887
|
//#region src/shell/apply-ops.ts
|
|
3720
3888
|
/**
|
|
3721
3889
|
* Dispatch reconciliation operations to their matching drivers in two phases
|
|
@@ -4169,7 +4337,7 @@ const STATE_PORT_HINT = "pass a custom statePort via opts.statePort";
|
|
|
4169
4337
|
* const port = buildStatePort({
|
|
4170
4338
|
* fetch: async () =>
|
|
4171
4339
|
* new Response(JSON.stringify({ files: {} }), { status: 200 }),
|
|
4172
|
-
* getEnv: (name) => (name === "
|
|
4340
|
+
* getEnv: (name) => (name === "BEDROCK_GITHUB_TOKEN" ? "ghp_example" : undefined),
|
|
4173
4341
|
* stateConfig: { backend: "gist", gistId: "abc123" },
|
|
4174
4342
|
* });
|
|
4175
4343
|
*
|
|
@@ -4192,12 +4360,12 @@ function buildStatePort(deps) {
|
|
|
4192
4360
|
};
|
|
4193
4361
|
}
|
|
4194
4362
|
function buildGistStatePort(stateConfig, deps) {
|
|
4195
|
-
const token = deps.getEnv("GITHUB_TOKEN");
|
|
4363
|
+
const token = deps.getEnv("BEDROCK_GITHUB_TOKEN") ?? deps.getEnv("GITHUB_TOKEN");
|
|
4196
4364
|
if (token === void 0) return {
|
|
4197
4365
|
err: {
|
|
4198
4366
|
kind: "missingCredential",
|
|
4199
4367
|
purpose: "stateBackend",
|
|
4200
|
-
variable: "
|
|
4368
|
+
variable: "BEDROCK_GITHUB_TOKEN"
|
|
4201
4369
|
},
|
|
4202
4370
|
success: false
|
|
4203
4371
|
};
|
|
@@ -4211,6 +4379,62 @@ function buildGistStatePort(stateConfig, deps) {
|
|
|
4211
4379
|
};
|
|
4212
4380
|
}
|
|
4213
4381
|
//#endregion
|
|
4382
|
+
//#region src/core/assert-all-reconcilable.ts
|
|
4383
|
+
/**
|
|
4384
|
+
* Batch reconcilability check that runs after `buildDesired` and before
|
|
4385
|
+
* `diff`. Walks paired `(kind, key)` entries and dispatches to each
|
|
4386
|
+
* kind module's optional `assertReconcilable` hook so kind-specific
|
|
4387
|
+
* rejections (e.g. Removing a developer-product icon, which the upstream
|
|
4388
|
+
* API has no documented unset path for) surface as typed errors before
|
|
4389
|
+
* `diff` runs and before any apply-side driver I/O is attempted.
|
|
4390
|
+
*
|
|
4391
|
+
* Pure and synchronous. Current-only entries (no matching desired) are
|
|
4392
|
+
* ignored: their reconciliation is `diff`'s concern, not this seam's.
|
|
4393
|
+
*
|
|
4394
|
+
* @param desired - Desired state from `buildDesired`.
|
|
4395
|
+
* @param current - Prior current state from the state port.
|
|
4396
|
+
* @returns `Ok(undefined)` when every paired entry passes its kind-level
|
|
4397
|
+
* reconcilability check, or the first `Err` returned by a hook.
|
|
4398
|
+
*/
|
|
4399
|
+
function assertAllReconcilable(desired, current) {
|
|
4400
|
+
const collision = detectProductNameCollision(desired);
|
|
4401
|
+
if (collision !== void 0) return collision;
|
|
4402
|
+
const currentByKey = new Map(current.map((entry) => [compositeKey(entry), entry]));
|
|
4403
|
+
for (const entry of desired) {
|
|
4404
|
+
const matched = currentByKey.get(compositeKey(entry));
|
|
4405
|
+
if (matched === void 0) continue;
|
|
4406
|
+
const check = defaultKindRegistry[entry.kind].assertReconcilable?.(matched, entry);
|
|
4407
|
+
if (check !== void 0 && !check.success) return check;
|
|
4408
|
+
}
|
|
4409
|
+
return {
|
|
4410
|
+
data: void 0,
|
|
4411
|
+
success: true
|
|
4412
|
+
};
|
|
4413
|
+
}
|
|
4414
|
+
function compositeKey(resource) {
|
|
4415
|
+
return `${resource.kind}:${resource.key}`;
|
|
4416
|
+
}
|
|
4417
|
+
function detectProductNameCollision(desired) {
|
|
4418
|
+
const seenByName = /* @__PURE__ */ new Map();
|
|
4419
|
+
for (const entry of desired) {
|
|
4420
|
+
if (entry.kind !== "developerProduct") continue;
|
|
4421
|
+
const prior = seenByName.get(entry.name);
|
|
4422
|
+
if (prior === void 0) {
|
|
4423
|
+
seenByName.set(entry.name, entry.key);
|
|
4424
|
+
continue;
|
|
4425
|
+
}
|
|
4426
|
+
return {
|
|
4427
|
+
err: {
|
|
4428
|
+
keys: [prior, entry.key],
|
|
4429
|
+
kind: "redactedNameCollision",
|
|
4430
|
+
message: `developer products '${prior}' and '${entry.key}' both resolve to the wire name '${entry.name}'. Roblox enforces per-universe uniqueness on developer-product names, so the second update would be rejected as DuplicateProductName. Set 'redacted: { name: "<unique>" }' on one of them to disambiguate.`,
|
|
4431
|
+
resolvedName: entry.name
|
|
4432
|
+
},
|
|
4433
|
+
success: false
|
|
4434
|
+
};
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
//#endregion
|
|
4214
4438
|
//#region src/shell/load-config-internal.ts
|
|
4215
4439
|
const LUAU_BOOTSTRAP_TEMP_PREFIX = "bedrock-luau-bootstrap-";
|
|
4216
4440
|
/**
|
|
@@ -4356,14 +4580,9 @@ async function evaluateLuauWithLute(absPath) {
|
|
|
4356
4580
|
*/
|
|
4357
4581
|
async function loadConfigWith(deps, options) {
|
|
4358
4582
|
const cwd = options?.cwd ?? process.cwd();
|
|
4359
|
-
const
|
|
4360
|
-
if (
|
|
4361
|
-
|
|
4362
|
-
kind: "fileNotFound",
|
|
4363
|
-
searchedFrom: cwd
|
|
4364
|
-
},
|
|
4365
|
-
success: false
|
|
4366
|
-
};
|
|
4583
|
+
const explicit = resolveExplicitConfigFile(cwd, options?.configFile);
|
|
4584
|
+
if (!explicit.success) return explicit;
|
|
4585
|
+
const configFile = explicit.data ?? discoverConfigFallback(cwd);
|
|
4367
4586
|
let resolved;
|
|
4368
4587
|
try {
|
|
4369
4588
|
resolved = await loadConfig({
|
|
@@ -4394,9 +4613,12 @@ async function loadConfigWith(deps, options) {
|
|
|
4394
4613
|
/**
|
|
4395
4614
|
* Discover, parse, and validate the project config.
|
|
4396
4615
|
*
|
|
4397
|
-
* Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json}`,
|
|
4398
|
-
* and `package.json#bedrock` starting at `options.cwd` (or the
|
|
4399
|
-
* working directory).
|
|
4616
|
+
* Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json,luau}`,
|
|
4617
|
+
* `.bedrockrc*`, and `package.json#bedrock` starting at `options.cwd` (or the
|
|
4618
|
+
* current working directory). When no config sits at the project root, the
|
|
4619
|
+
* loader also probes `.bedrock/bedrock.config.*` so users can colocate the
|
|
4620
|
+
* file with their other `.bedrock/` artifacts. The project root always wins
|
|
4621
|
+
* on collision. Returns a fresh, mutable `Config` on every call so
|
|
4400
4622
|
* long-running scripts see up-to-date values.
|
|
4401
4623
|
*
|
|
4402
4624
|
* When the exported default is a function (sync or async), `loadConfig`
|
|
@@ -4483,6 +4705,8 @@ const NATIVE_CONFIG_EXTENSIONS = [
|
|
|
4483
4705
|
"yaml",
|
|
4484
4706
|
"yml"
|
|
4485
4707
|
];
|
|
4708
|
+
const DISCOVERY_EXTENSIONS = [...NATIVE_CONFIG_EXTENSIONS, "luau"];
|
|
4709
|
+
const BEDROCK_CONFIG_DIRECTORY = ".bedrock";
|
|
4486
4710
|
/**
|
|
4487
4711
|
* Internal-only wrapper used at the c12 boundary: makeLuauResolver maps an
|
|
4488
4712
|
* evaluator `Err` into this throwable, which `attributeLoadError` unwraps
|
|
@@ -4496,6 +4720,44 @@ var EvaluatorThrow = class extends Error {
|
|
|
4496
4720
|
this.configError = configError;
|
|
4497
4721
|
}
|
|
4498
4722
|
};
|
|
4723
|
+
function resolveExplicitConfigFile(cwd, configFile) {
|
|
4724
|
+
if (configFile === void 0) return {
|
|
4725
|
+
data: void 0,
|
|
4726
|
+
success: true
|
|
4727
|
+
};
|
|
4728
|
+
const resolved = resolveConfigPath(cwd, configFile);
|
|
4729
|
+
if (!isExistingFile(resolved)) return {
|
|
4730
|
+
err: {
|
|
4731
|
+
kind: "fileNotFound",
|
|
4732
|
+
searchedFrom: cwd
|
|
4733
|
+
},
|
|
4734
|
+
success: false
|
|
4735
|
+
};
|
|
4736
|
+
return {
|
|
4737
|
+
data: resolved,
|
|
4738
|
+
success: true
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
function findConfigInDirectory(directory) {
|
|
4742
|
+
for (const extension of DISCOVERY_EXTENSIONS) {
|
|
4743
|
+
const candidate = join(directory, `bedrock.config.${extension}`);
|
|
4744
|
+
if (isExistingFile(candidate)) return candidate;
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
/**
|
|
4748
|
+
* Pick the `.bedrock/bedrock.config.*` fallback only when no `bedrock.config.*`
|
|
4749
|
+
* exists at the project root, letting c12 run its own discovery so a root file
|
|
4750
|
+
* always wins. Other c12 sources (`.bedrockrc`, `package.json#bedrock`) are
|
|
4751
|
+
* still merged in by c12 either way; the configFile we hand back wins
|
|
4752
|
+
* overlapping keys per c12's standard layering precedence.
|
|
4753
|
+
* @param cwd - The directory to search.
|
|
4754
|
+
* @returns Absolute path of the `.bedrock/` candidate, or `undefined` to defer
|
|
4755
|
+
* to c12's own discovery on the project root.
|
|
4756
|
+
*/
|
|
4757
|
+
function discoverConfigFallback(cwd) {
|
|
4758
|
+
if (findConfigInDirectory(cwd) !== void 0) return;
|
|
4759
|
+
return findConfigInDirectory(join(cwd, BEDROCK_CONFIG_DIRECTORY));
|
|
4760
|
+
}
|
|
4499
4761
|
/**
|
|
4500
4762
|
* Decide which Luau file the resolver should evaluate for a given c12 source,
|
|
4501
4763
|
* or `undefined` to defer to c12's built-in loaders.
|
|
@@ -4556,15 +4818,18 @@ function extractConfigFileFromStack(err) {
|
|
|
4556
4818
|
if (match !== null) return match[0];
|
|
4557
4819
|
}
|
|
4558
4820
|
}
|
|
4559
|
-
function
|
|
4821
|
+
function findConfigEntry(directory) {
|
|
4560
4822
|
let entries;
|
|
4561
4823
|
try {
|
|
4562
|
-
entries = readdirSync(
|
|
4824
|
+
entries = readdirSync(directory);
|
|
4563
4825
|
} catch {
|
|
4564
4826
|
return;
|
|
4565
4827
|
}
|
|
4566
4828
|
const match = entries.toSorted().find((entry) => entry.startsWith("bedrock.config."));
|
|
4567
|
-
return match === void 0 ? void 0 : join(
|
|
4829
|
+
return match === void 0 ? void 0 : join(directory, match);
|
|
4830
|
+
}
|
|
4831
|
+
function discoverConfigFile(cwd) {
|
|
4832
|
+
return findConfigEntry(cwd) ?? findConfigEntry(join(cwd, BEDROCK_CONFIG_DIRECTORY));
|
|
4568
4833
|
}
|
|
4569
4834
|
function attributeLoadError(err, cwd) {
|
|
4570
4835
|
if (err instanceof EvaluatorThrow) return err.configError;
|
|
@@ -4584,10 +4849,26 @@ function attributeLoadError(err, cwd) {
|
|
|
4584
4849
|
//#endregion
|
|
4585
4850
|
//#region src/shell/deploy.ts
|
|
4586
4851
|
/**
|
|
4852
|
+
* Decide whether `BEDROCK_CLI` should select the clack-backed default
|
|
4853
|
+
* progress adapter. Exported for direct unit coverage of the boundary
|
|
4854
|
+
* (`undefined` and empty string both flip to no-op; any non-empty value
|
|
4855
|
+
* picks clack).
|
|
4856
|
+
*
|
|
4857
|
+
* @param value - Raw `BEDROCK_CLI` value as returned by `getEnv`.
|
|
4858
|
+
* @returns `true` if the clack adapter should be the default.
|
|
4859
|
+
*/
|
|
4860
|
+
function isCliEnvironmentFlagSet(value) {
|
|
4861
|
+
return value !== void 0 && value !== "";
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4587
4864
|
* Run a full reconcile end-to-end. Default-constructs missing deps from
|
|
4588
|
-
* the project config and the environment variables `
|
|
4589
|
-
* `BEDROCK_API_KEY`;
|
|
4590
|
-
*
|
|
4865
|
+
* the project config and the environment variables `BEDROCK_GITHUB_TOKEN`
|
|
4866
|
+
* and `BEDROCK_API_KEY`; emits a terminal `deploySuccess` or `deployFailure`
|
|
4867
|
+
* event through the resolved `progress` port. When `progress` is omitted,
|
|
4868
|
+
* the default port comes from `BEDROCK_CLI`: a non-empty value selects the
|
|
4869
|
+
* clack-backed adapter, any other reading selects the no-op adapter. No
|
|
4870
|
+
* environment lookups happen when `statePort`, `registry`, `config`, and
|
|
4871
|
+
* `progress` are all supplied explicitly.
|
|
4591
4872
|
*
|
|
4592
4873
|
* @param options - Target environment plus optional overrides.
|
|
4593
4874
|
* @returns The persisted `BedrockState` on success, or a stage-tagged
|
|
@@ -4648,9 +4929,15 @@ function attributeLoadError(err, cwd) {
|
|
|
4648
4929
|
* ```
|
|
4649
4930
|
*/
|
|
4650
4931
|
async function deploy(options) {
|
|
4651
|
-
|
|
4652
|
-
if (!
|
|
4653
|
-
return
|
|
4932
|
+
if (options.progress !== void 0) return runAndEmit(options, options.progress);
|
|
4933
|
+
if (!isCliEnvironmentFlagSet(getEnvironmentOf(options)("BEDROCK_CLI"))) return runAndEmit(options, createNoOpProgressAdapter());
|
|
4934
|
+
return runWithDeferredClackProgress(options);
|
|
4935
|
+
}
|
|
4936
|
+
function readProcessEnvironment(name) {
|
|
4937
|
+
return process.env[name];
|
|
4938
|
+
}
|
|
4939
|
+
function getEnvironmentOf(options) {
|
|
4940
|
+
return options.getEnv ?? readProcessEnvironment;
|
|
4654
4941
|
}
|
|
4655
4942
|
async function pickConfig(options) {
|
|
4656
4943
|
if (options.config !== void 0) return {
|
|
@@ -4670,12 +4957,6 @@ async function pickConfig(options) {
|
|
|
4670
4957
|
success: true
|
|
4671
4958
|
};
|
|
4672
4959
|
}
|
|
4673
|
-
function readProcessEnvironment(name) {
|
|
4674
|
-
return process.env[name];
|
|
4675
|
-
}
|
|
4676
|
-
function getEnvironmentOf(options) {
|
|
4677
|
-
return options.getEnv ?? readProcessEnvironment;
|
|
4678
|
-
}
|
|
4679
4960
|
function pickStatePort(options, config) {
|
|
4680
4961
|
if (options.statePort !== void 0) return {
|
|
4681
4962
|
data: options.statePort,
|
|
@@ -4724,7 +5005,6 @@ async function resolveDeps(options) {
|
|
|
4724
5005
|
return {
|
|
4725
5006
|
data: {
|
|
4726
5007
|
config: effective,
|
|
4727
|
-
progress: options.progress,
|
|
4728
5008
|
readFile: readFile$2,
|
|
4729
5009
|
registry: registry.data,
|
|
4730
5010
|
statePort: statePort.data
|
|
@@ -4785,7 +5065,7 @@ async function runReconcile(environment, deps) {
|
|
|
4785
5065
|
success: false
|
|
4786
5066
|
};
|
|
4787
5067
|
const priorResources = prior.data?.resources ?? [];
|
|
4788
|
-
const validated =
|
|
5068
|
+
const validated = assertAllReconcilable(desired.data, priorResources);
|
|
4789
5069
|
if (!validated.success) return {
|
|
4790
5070
|
err: {
|
|
4791
5071
|
cause: validated.err,
|
|
@@ -4793,7 +5073,7 @@ async function runReconcile(environment, deps) {
|
|
|
4793
5073
|
},
|
|
4794
5074
|
success: false
|
|
4795
5075
|
};
|
|
4796
|
-
const applied = await applyOps(diff(desired.data, priorResources), deps.registry,
|
|
5076
|
+
const applied = await applyOps(diff(desired.data, priorResources), deps.registry, {
|
|
4797
5077
|
environment,
|
|
4798
5078
|
progress: deps.progress
|
|
4799
5079
|
});
|
|
@@ -4803,7 +5083,7 @@ async function runReconcile(environment, deps) {
|
|
|
4803
5083
|
priorResources
|
|
4804
5084
|
});
|
|
4805
5085
|
const written = await deps.statePort.write(merged);
|
|
4806
|
-
if (written.success) deps.progress
|
|
5086
|
+
if (written.success) deps.progress.emit({
|
|
4807
5087
|
environment,
|
|
4808
5088
|
kind: "stateWritten"
|
|
4809
5089
|
});
|
|
@@ -4813,6 +5093,61 @@ async function runReconcile(environment, deps) {
|
|
|
4813
5093
|
written
|
|
4814
5094
|
});
|
|
4815
5095
|
}
|
|
5096
|
+
async function runDeploy(options, progress) {
|
|
5097
|
+
const resolved = await resolveDeps(options);
|
|
5098
|
+
if (!resolved.success) return resolved;
|
|
5099
|
+
return runReconcile(options.environment, {
|
|
5100
|
+
...resolved.data,
|
|
5101
|
+
progress
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
function emitTerminalEvent(inputs) {
|
|
5105
|
+
const { environment, progress, result } = inputs;
|
|
5106
|
+
if (result.success) {
|
|
5107
|
+
progress.emit({
|
|
5108
|
+
environment,
|
|
5109
|
+
kind: "deploySuccess",
|
|
5110
|
+
resourceCount: result.data.resources.length
|
|
5111
|
+
});
|
|
5112
|
+
return;
|
|
5113
|
+
}
|
|
5114
|
+
progress.emit({
|
|
5115
|
+
environment,
|
|
5116
|
+
error: result.err,
|
|
5117
|
+
kind: "deployFailure"
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
async function runAndEmit(options, progress) {
|
|
5121
|
+
const result = await runDeploy(options, progress);
|
|
5122
|
+
emitTerminalEvent({
|
|
5123
|
+
environment: options.environment,
|
|
5124
|
+
progress,
|
|
5125
|
+
result
|
|
5126
|
+
});
|
|
5127
|
+
return result;
|
|
5128
|
+
}
|
|
5129
|
+
async function runWithDeferredClackProgress(options) {
|
|
5130
|
+
const resolved = await resolveDeps(options);
|
|
5131
|
+
const progress = createDefaultProgressAdapter(resolved.success ? resolved.data.config : options.config);
|
|
5132
|
+
if (!resolved.success) {
|
|
5133
|
+
emitTerminalEvent({
|
|
5134
|
+
environment: options.environment,
|
|
5135
|
+
progress,
|
|
5136
|
+
result: resolved
|
|
5137
|
+
});
|
|
5138
|
+
return resolved;
|
|
5139
|
+
}
|
|
5140
|
+
const result = await runReconcile(options.environment, {
|
|
5141
|
+
...resolved.data,
|
|
5142
|
+
progress
|
|
5143
|
+
});
|
|
5144
|
+
emitTerminalEvent({
|
|
5145
|
+
environment: options.environment,
|
|
5146
|
+
progress,
|
|
5147
|
+
result
|
|
5148
|
+
});
|
|
5149
|
+
return result;
|
|
5150
|
+
}
|
|
4816
5151
|
//#endregion
|
|
4817
5152
|
//#region src/core/migrate/build-state.ts
|
|
4818
5153
|
/**
|
|
@@ -6963,6 +7298,6 @@ function isFileMissing(err) {
|
|
|
6963
7298
|
return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && FILE_MISSING_CODES.has(err.code);
|
|
6964
7299
|
}
|
|
6965
7300
|
//#endregion
|
|
6966
|
-
export {
|
|
7301
|
+
export { renderStateWriteError as $, createGamePassDriver as A, asSha256Hex as B, createPlaceDriver as C, createGistStateAdapter as D, createNoOpProgressAdapter as E, validateConfig as F, renderBuildStatePortError as G, isRobloxAssetId as H, shouldReuploadIcon as I, renderMigrateParseError as J, renderDeployError as K, validateEnvironmentName as L, derivePriceFields as M, createClackProgressAdapter as N, parseStateFile as O, isGistStateConfig as P, renderParseError as Q, asResourceKey as R, createUniverseDriver as S, UNIVERSE_SINGLETON_KEY as T, isSha256Hex as U, isResourceKey as V, resolveStateConfig as W, renderOverrideDiscoveryError as X, renderMigrationSummary as Y, renderOverrideError as Z, diff as _, assertAllReconcilable as a, buildCredentialOverrides as b, buildDefaultRegistry as c, selectEnvironment as d, createClackPort as et, selectMergedEnvironment as f, renderDisplayNamePrefix as g, DEFAULT_PREFIX_FORMAT as h, loadConfig$1 as i, createDeveloperProductDriver as j, serializeStateFile as k, applyOps as l, flattenConfig as m, serializeConfig as n, buildStatePort as o, collectRedactionAnnotations as p, renderMigrateError as q, deploy as r, buildDesired as s, migrateMantleState as t, extractResourceRedaction as u, defaultKindRegistry as v, SOCIAL_LINK_FIELDS as w, createDefaultSpawner as x, dispatchOverride as y, asRobloxAssetId as z };
|
|
6967
7302
|
|
|
6968
|
-
//# sourceMappingURL=migrate-mantle-state-
|
|
7303
|
+
//# sourceMappingURL=migrate-mantle-state-F4zdhxV4.mjs.map
|