@bedrock-rbx/core 0.1.0-beta.13 → 0.1.0-beta.15

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 ADDED
@@ -0,0 +1,180 @@
1
+ # @bedrock-rbx/core
2
+
3
+ Infrastructure-as-Code for Roblox, with a bundled `bedrock` CLI.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@bedrock-rbx/core.svg)](https://npmx.dev/package/@bedrock-rbx/core)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/christopher-buss/bedrock/blob/main/LICENSE)
7
+ [![CI](https://github.com/christopher-buss/bedrock/actions/workflows/ci.yaml/badge.svg)](https://github.com/christopher-buss/bedrock/actions/workflows/ci.yaml)
8
+
9
+ > **Status: 0.1 beta.** The public API is stabilizing. Breaking changes may land in minor releases (0.1 to 0.2) until 1.0.
10
+
11
+ ## What is `@bedrock-rbx/core`?
12
+
13
+ Bedrock defines a Roblox experience's setup, including the universe, places, game passes, and developer products, in a single config file you keep alongside your game code. Each deploy diffs that config against the live state in Roblox and applies the create or update operations needed to bring them into sync. Adding a new place (lobby, mini-game, hub world) means editing the config instead of clicking through Creator Hub.
14
+
15
+ It is a spiritual successor to [Mantle](https://github.com/blake-mealey/mantle) (no longer maintained), rebuilt on top of [Roblox Open Cloud](https://create.roblox.com/docs/cloud). That means API-key authentication only, no `ROBLOSECURITY` cookies or legacy endpoints.
16
+
17
+ Bedrock ships two paths to the same engine. The `bedrock` CLI reconciles a config file (TypeScript, JavaScript, YAML, JSON, or Luau) against live Roblox state. The programmatic API exposes the same `deploy()`, `diff()`, and `applyOps()` functions for direct use in TypeScript, so deploys can be triggered from a webhook handler, a chat bot, or any other service in your stack. Both surfaces run identical code below the entry point.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pnpm add -D @bedrock-rbx/core
23
+ # or: npm install --save-dev @bedrock-rbx/core
24
+ # or: bun add -d @bedrock-rbx/core
25
+ ```
26
+
27
+ The `bedrock` binary is then available via your package manager (`pnpm bedrock`, `npx bedrock`, `bunx bedrock`).
28
+
29
+ **Runtime:** Node >= 24.12 or Bun >= 1.3.
30
+
31
+ **Authentication:** bedrock reads two environment variables.
32
+
33
+ | Variable | Purpose | Where to get it |
34
+ |---|---|---|
35
+ | `BEDROCK_API_KEY` | Authenticates Open Cloud calls. | [Creator Hub > Credentials > API Keys](https://create.roblox.com/dashboard/credentials). Needs the scopes for the resources you manage (universe-place, universe-place-config, universe-passes, universe-developer-products, asset-create). |
36
+ | `BEDROCK_GITHUB_TOKEN` | Reads and writes the state gist (the default state backend). | A GitHub personal access token with the `gist` scope. The gist itself starts empty: create one at [gist.github.com](https://gist.github.com), then put its ID in your config's `state.gistId`. |
37
+
38
+ Both can be overridden per environment in your config (or via `--api-key` / `--github-token` flags on the CLI).
39
+
40
+ ## Quick start (programmatic)
41
+
42
+ A bedrock config is a plain `Config` object. The `defineConfig` helper is identity at runtime; it exists only to give TypeScript users full type inference and autocomplete.
43
+
44
+ ```ts
45
+ // bedrock.config.ts
46
+ import { defineConfig } from "@bedrock-rbx/core/config";
47
+
48
+ export default defineConfig({
49
+ environments: {
50
+ production: { universe: { universeId: 1234567890 } },
51
+ },
52
+ passes: {
53
+ "vip-pass": {
54
+ name: "VIP Pass",
55
+ description: "Grants VIP perks.",
56
+ icon: { "en-us": "assets/vip-icon.png" },
57
+ price: 500,
58
+ },
59
+ },
60
+ places: {
61
+ "start-place": {
62
+ name: "Start Place",
63
+ file: "places/start.rbxl",
64
+ },
65
+ },
66
+ state: { backend: "gist", gistId: "abc123def456" },
67
+ universe: {
68
+ name: "My Game",
69
+ description: "An adventure.",
70
+ },
71
+ });
72
+ ```
73
+
74
+ Place your custom deploy orchestration at `.bedrock/deploy.ts`. The `bedrock deploy` CLI auto-discovers and invokes this file when present, and the same exports can be imported from a webhook handler, chat bot, or other service that wants to trigger deploys directly.
75
+
76
+ ```ts
77
+ // .bedrock/deploy.ts
78
+ import { deploy } from "@bedrock-rbx/core";
79
+
80
+ /**
81
+ * Triggered from a webhook handler whenever a release tag is pushed.
82
+ * Wraps `deploy` with custom orchestration: structured logging on
83
+ * failure, success reporting back to the caller.
84
+ *
85
+ * @param environment - Target environment from the webhook payload.
86
+ * @returns Whether the deploy completed successfully.
87
+ */
88
+ export async function deployFromWebhook(environment: string): Promise<boolean> {
89
+ const result = await deploy({ environment });
90
+ if (!result.success) {
91
+ console.error("bedrock deploy failed", { environment, err: result.err });
92
+ return false;
93
+ }
94
+
95
+ console.log("bedrock deploy succeeded", { environment });
96
+ return true;
97
+ }
98
+ ```
99
+
100
+ `deploy()` returns a `Result<BedrockState, DeployError>` rather than throwing on failure. `Result` is a discriminated union: `result.success` is `true` with the value on `result.data`, or `false` with the error on `result.err`. `DeployError` is itself stage-tagged (`configLoadFailed`, `stateReadFailed`, `applyFailed`, and similar) so callers can branch on `kind` to distinguish what went wrong without parsing error messages.
101
+
102
+ ## Quick start (CLI)
103
+
104
+ Using the same `bedrock.config.ts` from above:
105
+
106
+ ```bash
107
+ export BEDROCK_API_KEY=...
108
+ export BEDROCK_GITHUB_TOKEN=...
109
+
110
+ pnpm bedrock deploy --env production
111
+ ```
112
+
113
+ `bedrock deploy` discovers `bedrock.config.{ts,js,mjs,yaml,yml,json,luau}` in the current directory, loads it, and runs the same reconcile as the programmatic path. Output is rendered via `@clack/prompts` (interactive progress, summary, and error reporting).
114
+
115
+ `--env` may be repeated to deploy to multiple environments in one invocation; each environment is reconciled with its own merged config and its own state slot.
116
+
117
+ ## What bedrock manages today
118
+
119
+ | Kind | Key example | What bedrock manages | Notes |
120
+ |---|---|---|---|
121
+ | `universe` | `"main"` (singleton) | Name, description, social links, age guidelines, opt-in studio access. | Adopted by ID. Bedrock does not create new universes. |
122
+ | `place` | `"start-place"` | Place metadata (name, description, server-fill mode, max-player count), `.rbxl` file uploads. | The root place is adopted; secondary places are provisioned by bedrock. |
123
+ | `gamePass` | `"vip-pass"` | Name, description, icon upload, price, on-sale state. | Provisioned by bedrock; the asset ID is recorded in outputs. |
124
+ | `developerProduct` | `"gem-pack-100"` | Name, description, icon upload, price. | Provisioned by bedrock; the asset ID is recorded in outputs. |
125
+
126
+ Bedrock does not delete resources. Removing an entry from your config leaves the upstream entity in place; delete it from Creator Hub directly if you want it gone. Additional kinds (badges, place settings, more) are on the [roadmap](https://github.com/christopher-buss/bedrock/projects).
127
+
128
+ ## CLI reference
129
+
130
+ ```text
131
+ bedrock <command> [options]
132
+ ```
133
+
134
+ **`bedrock deploy`** - reconciles every configured environment (or only those passed via `--env`) against live Roblox state, writing the new state snapshot on success.
135
+
136
+ Options: `--env <name>` (repeatable), `--config <path>`, `--api-key <value>`, `--github-token <value>`.
137
+
138
+ **`bedrock diff`** - previews the operations a deploy *would* apply, without writing state or hitting any mutating Roblox endpoints. Suitable for code review and CI.
139
+
140
+ Options: same as `deploy`.
141
+
142
+ **`bedrock migrate [stateFilePath]`** - translates a state file from another deployment tool into a bedrock project. Currently supports Mantle (`--from mantle`); other sources will be added as needed. The maintainer-prompt UI walks through naming the project, choosing a backend, and resolving any unrecognized fields.
143
+
144
+ Options: `--from <tool>`.
145
+
146
+ Pass `--help` to any command for the full option list at the version you have installed.
147
+
148
+ ## Programmatic API surface
149
+
150
+ The most commonly imported entry points:
151
+
152
+ | Symbol | Module | Role |
153
+ |---|---|---|
154
+ | `deploy()` | `@bedrock-rbx/core` | Full reconcile end-to-end. |
155
+ | `diff()` | `@bedrock-rbx/core` | Pure function from desired and current state to an operation list. |
156
+ | `applyOps()` | `@bedrock-rbx/core` | Dispatch an operation list to its drivers and collect outputs. |
157
+ | `loadConfig()` | `@bedrock-rbx/core` | Discover and load a `bedrock.config.*` file. |
158
+ | `buildDesired()` | `@bedrock-rbx/core` | Compute the desired-state list from a resolved config. |
159
+ | `defineConfig()` | `@bedrock-rbx/core/config` | Type-helper for `bedrock.config.ts`. |
160
+ | `ResourceDriver<K>` | `@bedrock-rbx/core` | Plugin contract for resource kinds. |
161
+ | `StatePort` | `@bedrock-rbx/core` | Plugin contract for state backends. |
162
+ | `ProgressPort` | `@bedrock-rbx/core` | Listener interface for per-resource and aggregate deploy events. |
163
+
164
+ `ResourceDriver<K>`, `StatePort`, and `ProgressPort` are published contracts today; the plugin runtime that lets you register custom implementations against a real deploy ships in v0.3.
165
+
166
+ See the [full reference on the docs site](https://bedrock-livid.vercel.app/) for the complete list of exports.
167
+
168
+ ## Status, docs, and contributing
169
+
170
+ Bedrock is in active development ahead of a first public release. Track scope and timing on the [project board](https://github.com/christopher-buss/bedrock/projects).
171
+
172
+ - [Documentation site](https://bedrock-livid.vercel.app/) (work in progress)
173
+ - [Source repository](https://github.com/christopher-buss/bedrock)
174
+ - [Issues](https://github.com/christopher-buss/bedrock/issues) (maintainer-only; external feedback runs through [Discussions](https://github.com/christopher-buss/bedrock/discussions) as prompt requests)
175
+ - [Contributing guide](https://github.com/christopher-buss/bedrock/blob/main/CONTRIBUTING.md)
176
+ - [Security policy](https://github.com/christopher-buss/bedrock/blob/main/SECURITY.md)
177
+
178
+ ## License
179
+
180
+ [MIT](https://github.com/christopher-buss/bedrock/blob/main/LICENSE) (c) Christopher Buss.
package/dist/cli/run.mjs CHANGED
@@ -1,25 +1,94 @@
1
1
  #!/usr/bin/env node
2
- import { B as createClackProgressAdapter, G as renderMigrationSummary, H as renderDeployError, K as renderParseError, O as serializeStateFile, U as renderMigrateError, V as renderBuildStatePortError, W as renderMigrateParseError, _ as diff, a as buildStatePort, d as selectMergedEnvironment, f as collectRedactionAnnotations, i as loadConfig, l as validatePlan, m as flattenConfig, n as serializeConfig, o as buildDesired, p as resolveStateConfig, q as renderStateWriteError, r as deploy, t as migrateMantleState, u as selectEnvironment, x as createClackPort } from "../migrate-mantle-state-CQjWBZwT.mjs";
2
+ import { $ as renderStateWriteError, G as renderBuildStatePortError, J as renderMigrateParseError, K as renderDeployError, N as createClackProgressAdapter, Q as renderParseError, W as resolveStateConfig, X as renderOverrideDiscoveryError, Y as renderMigrationSummary, Z as renderOverrideError, _ as diff, a as assertAllReconcilable, b as buildCredentialOverrides, d as selectEnvironment, et as createClackPort, f as selectMergedEnvironment, i as loadConfig, k as serializeStateFile, m as flattenConfig, n as serializeConfig, o as buildStatePort, p as collectRedactionAnnotations, q as renderMigrateError, r as deploy, s as buildDesired, t as migrateMantleState, u as extractResourceRedaction, x as createDefaultSpawner, y as dispatchOverride } from "../migrate-mantle-state-ClQ40EFD.mjs";
3
3
  import { isCancel, path, select, text } from "@clack/prompts";
4
4
  import process from "node:process";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
- import { dirname, join } from "node:path";
6
+ import { statSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
7
8
  import sade from "sade";
8
9
  //#region package.json
9
- var version = "0.1.0-beta.13";
10
+ var version = "0.1.0-beta.15";
10
11
  //#endregion
11
- //#region src/cli/credential-environment-overrides.ts
12
+ //#region src/cli/build-override-invocation.ts
12
13
  /**
13
- * Map CLI credential flags to their corresponding env-var names, omitting
14
- * entries whose flag is `undefined`.
15
- * @param flags - CLI credential flag values to translate.
16
- * @returns An immutable record of env-var names to their override values.
14
+ * Translate the deploy command's parsed inputs into a single-environment
15
+ * {@link OverrideInvocation}. Optional flags (`apiKey`, `configFile`,
16
+ * `githubToken`) are *omitted* from the returned object when their parsed
17
+ * value is `undefined`, rather than included with an `undefined` value: the
18
+ * spawn protocol's downstream argv and env-var routing relies on field
19
+ * presence, not just defined-ness.
20
+ * @param inputs - {@link BuildOverrideInvocationInputs}.
21
+ * @returns A single-environment {@link OverrideInvocation}.
17
22
  */
18
- function buildCredentialOverrides(flags) {
19
- const overrides = {};
20
- if (flags.apiKey !== void 0) overrides["BEDROCK_API_KEY"] = flags.apiKey;
21
- if (flags.githubToken !== void 0) overrides["GITHUB_TOKEN"] = flags.githubToken;
22
- return overrides;
23
+ function buildOverrideInvocation(inputs) {
24
+ const { environment, overridePath, parsed } = inputs;
25
+ return {
26
+ ...parsed.apiKey === void 0 ? {} : { apiKey: parsed.apiKey },
27
+ ...parsed.configFile === void 0 ? {} : { configFile: parsed.configFile },
28
+ environment,
29
+ ...parsed.githubToken === void 0 ? {} : { githubToken: parsed.githubToken },
30
+ overridePath
31
+ };
32
+ }
33
+ //#endregion
34
+ //#region src/cli/discover-override.ts
35
+ const OVERRIDE_DIR_NAME = ".bedrock";
36
+ const OVERRIDE_EXTENSION = ".ts";
37
+ const VALID_COMMAND = /^[a-z][a-z0-9-]*$/;
38
+ /**
39
+ * Stat-injectable variant of {@link discoverOverride}. Exported so tests can
40
+ * drive `EACCES`-class errors and malformed-throw cases that real fs fixtures
41
+ * cannot reliably produce on every supported OS.
42
+ * @param inputs - {@link DiscoverOverrideInputs}.
43
+ * @returns The absolute path to the override file when it exists, otherwise
44
+ * `undefined`.
45
+ */
46
+ function discoverOverrideWith(inputs) {
47
+ const { command, projectRoot, stat } = inputs;
48
+ if (!VALID_COMMAND.test(command)) return;
49
+ const candidate = resolve(projectRoot, OVERRIDE_DIR_NAME, `${command}${OVERRIDE_EXTENSION}`);
50
+ try {
51
+ return stat(candidate).isFile() ? candidate : void 0;
52
+ } catch (err) {
53
+ if (isAbsenceError(err)) return;
54
+ throw err;
55
+ }
56
+ }
57
+ /**
58
+ * Resolve the path of a user-authored `.bedrock/<command>.ts` override for
59
+ * the given command, or return `undefined` when no such file exists.
60
+ *
61
+ * The CLI uses this primitive to decide whether a subcommand invocation
62
+ * should be handed to an override script or run through the built-in path.
63
+ * `undefined` therefore means "fall through to the built-in implementation",
64
+ * so only absence-style stat failures (`ENOENT`, `ENOTDIR`) are swallowed.
65
+ * Permission and other errors propagate; the dispatcher must refuse to
66
+ * silently route a `deploy` (or any other destructive command) through the
67
+ * built-in path when the override file demonstrably exists but is
68
+ * unreadable.
69
+ *
70
+ * `command` is validated against the subcommand grammar before any path
71
+ * construction. An out-of-shape input cannot be a real subcommand and
72
+ * returns `undefined` without touching the filesystem.
73
+ * @param projectRoot - Absolute path of the directory `bedrock` was invoked
74
+ * from. Relative inputs are resolved against `process.cwd()`.
75
+ * @param command - The CLI subcommand name to look up (`deploy`, `diff`,
76
+ * ...). Must match `/^[a-z][a-z0-9-]*$/`; anything else returns `undefined`.
77
+ * @returns The absolute path to the override file when it exists, otherwise
78
+ * `undefined`.
79
+ * @throws When the stat call fails with anything other than `ENOENT` or
80
+ * `ENOTDIR` (e.g. `EACCES`, `EPERM`).
81
+ */
82
+ function discoverOverride(projectRoot, command) {
83
+ return discoverOverrideWith({
84
+ command,
85
+ projectRoot,
86
+ stat: statSync
87
+ });
88
+ }
89
+ function isAbsenceError(error) {
90
+ if (!(error instanceof Error) || !("code" in error)) return false;
91
+ return error.code === "ENOENT" || error.code === "ENOTDIR";
23
92
  }
24
93
  //#endregion
25
94
  //#region src/cli/parse-options.ts
@@ -124,6 +193,14 @@ function pickString(rawOptions, ...keys) {
124
193
  * `deploy()` for each `--env` value in order. Per-env successes and failures
125
194
  * render through clack as a single line each; the aggregated exit code is
126
195
  * `EXIT_OK` only when every env succeeded.
196
+ *
197
+ * When a `.bedrock/deploy.ts` override is discovered under the resolved
198
+ * project root, each `--env` is handed to the spawner via
199
+ * {@link dispatchOverride} instead of the in-process `deploy()` call. The
200
+ * aggregation rule is identical: every env still runs and the exit code is
201
+ * `EXIT_OK` only when every spawn returned a zero exit code. Unlike the
202
+ * in-process path, only failures emit a per-env line here; a successful
203
+ * spawn's output comes from the override script's own inherited stdout.
127
204
  * @param deps - Dependency overrides; missing slots are default-constructed
128
205
  * from real implementations.
129
206
  * @returns An async sade action that returns once `deps.exit` was invoked.
@@ -136,40 +213,48 @@ function deployCommand(deps) {
136
213
  };
137
214
  }
138
215
  function resolveDeploy(deps) {
139
- const clack = deps.clack ?? createClackPort();
140
216
  return {
141
- clack,
217
+ clack: deps.clack ?? createClackPort(),
142
218
  deploy: deps.deploy ?? deploy,
219
+ discoverOverride: deps.discoverOverride ?? discoverOverride,
143
220
  exit: deps.exit ?? ((code) => process.exit(code)),
144
221
  loadConfig: deps.loadConfig ?? loadConfig,
145
- progress: deps.progress ?? createClackProgressAdapter({ clack })
222
+ progressOverride: deps.progress,
223
+ projectRoot: deps.projectRoot ?? process.cwd(),
224
+ spawner: deps.spawner ?? createDefaultSpawner()
146
225
  };
147
226
  }
148
227
  function loadOptionsFor$1(parsed) {
149
228
  return parsed.configFile === void 0 ? void 0 : { configFile: parsed.configFile };
150
229
  }
230
+ function cancelAsFailed$1(clack) {
231
+ clack.cancel("deploy failed");
232
+ }
151
233
  async function dispatchEnvironments$1(inputs) {
152
- const { config, environments, getEnv, resolved } = inputs;
234
+ const { config, getEnv, overridePath, parsed, progress, resolved } = inputs;
153
235
  const failed = [];
154
- for (const environment of environments) {
155
- const result = await resolved.deploy({
156
- config,
157
- environment,
158
- getEnv
159
- });
160
- if (result.success) resolved.progress.emit({
161
- environment,
162
- kind: "deploySuccess",
163
- resourceCount: result.data.resources.length
164
- });
165
- else {
166
- resolved.progress.emit({
236
+ for (const environment of parsed.environments) {
237
+ if (overridePath !== void 0) {
238
+ const result = await dispatchOverride(buildOverrideInvocation({
167
239
  environment,
168
- error: result.err,
169
- kind: "deployFailure"
170
- });
171
- failed.push(environment);
240
+ overridePath,
241
+ parsed
242
+ }), resolved.spawner);
243
+ if (!result.success) {
244
+ renderOverrideError({
245
+ environment,
246
+ err: result.err
247
+ }, resolved.clack);
248
+ failed.push(environment);
249
+ }
250
+ continue;
172
251
  }
252
+ if (!(await resolved.deploy({
253
+ config,
254
+ environment,
255
+ getEnv,
256
+ progress
257
+ })).success) failed.push(environment);
173
258
  }
174
259
  return failed;
175
260
  }
@@ -177,8 +262,37 @@ function buildGetEnvironment$1(parsed) {
177
262
  const overrides = buildCredentialOverrides(parsed);
178
263
  return (name) => overrides[name] ?? process.env[name];
179
264
  }
180
- function cancelAsFailed$1(clack) {
181
- clack.cancel("deploy failed");
265
+ async function dispatchAndReport(input) {
266
+ const { loaded, overridePath, parsed, resolved } = input;
267
+ const progress = resolved.progressOverride ?? createClackProgressAdapter({
268
+ clack: resolved.clack,
269
+ config: loaded
270
+ });
271
+ if ((await dispatchEnvironments$1({
272
+ config: loaded,
273
+ getEnv: buildGetEnvironment$1(parsed),
274
+ overridePath,
275
+ parsed,
276
+ progress,
277
+ resolved
278
+ })).length > 0) {
279
+ cancelAsFailed$1(resolved.clack);
280
+ return 1;
281
+ }
282
+ resolved.clack.outro("deploy succeeded");
283
+ return 0;
284
+ }
285
+ function discoverOverridePath(resolved) {
286
+ try {
287
+ return {
288
+ kind: "discovered",
289
+ overridePath: resolved.discoverOverride(resolved.projectRoot, "deploy")
290
+ };
291
+ } catch (err) {
292
+ renderOverrideDiscoveryError(err, resolved.clack);
293
+ cancelAsFailed$1(resolved.clack);
294
+ return { kind: "failed" };
295
+ }
182
296
  }
183
297
  async function runDeploy(rawOptions, resolved) {
184
298
  resolved.clack.intro("bedrock deploy");
@@ -197,24 +311,21 @@ async function runDeploy(rawOptions, resolved) {
197
311
  cancelAsFailed$1(resolved.clack);
198
312
  return 1;
199
313
  }
200
- if ((await dispatchEnvironments$1({
201
- config: loaded.data,
202
- environments: parsed.data.environments,
203
- getEnv: buildGetEnvironment$1(parsed.data),
314
+ const discovery = discoverOverridePath(resolved);
315
+ if (discovery.kind === "failed") return 1;
316
+ return dispatchAndReport({
317
+ loaded: loaded.data,
318
+ overridePath: discovery.overridePath,
319
+ parsed: parsed.data,
204
320
  resolved
205
- })).length > 0) {
206
- cancelAsFailed$1(resolved.clack);
207
- return 1;
208
- }
209
- resolved.clack.outro("deploy succeeded");
210
- return 0;
321
+ });
211
322
  }
212
323
  //#endregion
213
324
  //#region src/shell/preview-diff.ts
214
325
  /**
215
326
  * Compute the operations `deploy` would apply for a target environment
216
327
  * without writing state. Default-constructs missing deps from the project
217
- * config and `GITHUB_TOKEN`; never reads `process.env` when `statePort`
328
+ * config and `BEDROCK_GITHUB_TOKEN`; never reads `process.env` when `statePort`
218
329
  * and `config` are both supplied explicitly.
219
330
  *
220
331
  * @param options - Target environment plus optional overrides.
@@ -277,10 +388,11 @@ function resolveEnvironmentView(config, environment) {
277
388
  err: selected.err,
278
389
  success: false
279
390
  };
391
+ const environmentResource = extractResourceRedaction(merged.data.entry);
280
392
  return {
281
393
  data: {
282
394
  effective: selected.data,
283
- redactions: collectRedactionAnnotations(merged.data.merged)
395
+ redactions: collectRedactionAnnotations(merged.data.merged, environmentResource)
284
396
  },
285
397
  success: true
286
398
  };
@@ -322,7 +434,7 @@ async function runPreview(environment, deps) {
322
434
  success: false
323
435
  };
324
436
  const priorResources = prior.data?.resources ?? [];
325
- const validated = validatePlan(desired.data, priorResources);
437
+ const validated = assertAllReconcilable(desired.data, priorResources);
326
438
  if (!validated.success) return {
327
439
  err: {
328
440
  cause: validated.err,
@@ -381,7 +493,7 @@ function cancelAsFailed(clack) {
381
493
  function describeOp(op) {
382
494
  switch (op.type) {
383
495
  case "create": return `+ ${op.desired.kind}:${op.key}`;
384
- case "update": return `~ ${op.desired.kind}:${op.key}`;
496
+ case "update": return `~ ${op.desired.kind}:${op.key} ${op.changedFields.join(" + ")} updated`;
385
497
  }
386
498
  }
387
499
  function isDriftOp(op) {
@@ -1314,8 +1426,8 @@ const PROGRAM_DESCRIBE = "Infrastructure-as-Code deployment tool for Roblox";
1314
1426
  */
1315
1427
  function createProg(deps = {}) {
1316
1428
  const prog = sade(PROGRAM_NAME).describe(PROGRAM_DESCRIBE).version(version);
1317
- prog.command("deploy").describe("Reconcile a project's resources against the configured environment(s)").option("--env", "Target environment (repeat for multiple)").option("--config", "Config file path (overrides discovery)").option("--api-key", "Override the BEDROCK_API_KEY environment variable").option("--github-token", "Override the GITHUB_TOKEN environment variable").action(deployCommand(deps));
1318
- prog.command("diff").describe("Preview the operations a deploy would apply, without writing state").option("--env", "Target environment (repeat for multiple)").option("--config", "Config file path (overrides discovery)").option("--api-key", "Override the BEDROCK_API_KEY environment variable").option("--github-token", "Override the GITHUB_TOKEN environment variable").action(diffCommand(deps));
1429
+ prog.command("deploy").describe("Reconcile a project's resources against the configured environment(s)").option("--env", "Target environment (repeat for multiple)").option("--config", "Config file path (overrides discovery)").option("--api-key", "Override the BEDROCK_API_KEY environment variable").option("--github-token", "Override the BEDROCK_GITHUB_TOKEN environment variable").action(deployCommand(deps));
1430
+ prog.command("diff").describe("Preview the operations a deploy would apply, without writing state").option("--env", "Target environment (repeat for multiple)").option("--config", "Config file path (overrides discovery)").option("--api-key", "Override the BEDROCK_API_KEY environment variable").option("--github-token", "Override the BEDROCK_GITHUB_TOKEN environment variable").action(diffCommand(deps));
1319
1431
  prog.command("migrate [stateFilePath]").describe("Translate a state file from another tool into a bedrock project").option("--from", "Source format to migrate from (mantle; prompted if omitted)").action(migrateCommand(deps));
1320
1432
  return prog;
1321
1433
  }