@bedrock-rbx/core 0.1.0-beta.1
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/LICENSE +21 -0
- package/dist/cli/run.d.mts +1 -0
- package/dist/cli/run.mjs +1476 -0
- package/dist/cli/run.mjs.map +1 -0
- package/dist/config.d.mts +3 -0
- package/dist/config.mjs +2 -0
- package/dist/define-config-CroC96-C.mjs +42 -0
- package/dist/define-config-CroC96-C.mjs.map +1 -0
- package/dist/define-config-D-LAhfSJ.d.mts +1731 -0
- package/dist/define-config-D-LAhfSJ.d.mts.map +1 -0
- package/dist/index.d.mts +3449 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +67 -0
- package/dist/index.mjs.map +1 -0
- package/dist/migrate-mantle-state-DqbJ1TLq.mjs +5789 -0
- package/dist/migrate-mantle-state-DqbJ1TLq.mjs.map +1 -0
- package/package.json +80 -0
package/dist/cli/run.mjs
ADDED
|
@@ -0,0 +1,1476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { T as serializeStateFile, a as buildStatePort, d as resolveStateConfig, f as flattenConfig, h as diff, i as loadConfig, l as validatePlan, n as serializeConfig, o as buildDesired, r as deploy, t as migrateMantleState, u as selectEnvironment } from "../migrate-mantle-state-DqbJ1TLq.mjs";
|
|
3
|
+
import { PermissionError } from "@bedrock-rbx/ocale";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import sade from "sade";
|
|
8
|
+
import { cancel, intro, isCancel, log, outro, path, select, text } from "@clack/prompts";
|
|
9
|
+
//#region package.json
|
|
10
|
+
var version = "0.1.0-beta.1";
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/cli/parse-options.ts
|
|
13
|
+
const RECOGNIZED_FLAGS$1 = new Set([
|
|
14
|
+
"api-key",
|
|
15
|
+
"apiKey",
|
|
16
|
+
"config",
|
|
17
|
+
"env",
|
|
18
|
+
"github-token",
|
|
19
|
+
"githubToken"
|
|
20
|
+
]);
|
|
21
|
+
const SADE_RESERVED$1 = new Set([
|
|
22
|
+
"--",
|
|
23
|
+
"_",
|
|
24
|
+
"h",
|
|
25
|
+
"help",
|
|
26
|
+
"v",
|
|
27
|
+
"version"
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Translate the raw sade options POJO into a typed `CommonOptions`. Reads
|
|
31
|
+
* `BEDROCK_ENVIRONMENT` from `process.env` as a fallback when `--env` is
|
|
32
|
+
* absent; inject `getEnvironment` to redirect or isolate the lookup. No
|
|
33
|
+
* clack, no sade types. Reused by `bedrock deploy` and `bedrock diff`.
|
|
34
|
+
* @param rawOptions - The options object sade hands the action callback.
|
|
35
|
+
* @param getEnvironment - Reads an environment variable; consulted as a
|
|
36
|
+
* fallback for `--env` when no flag was supplied. Defaults to a
|
|
37
|
+
* `process.env` reader.
|
|
38
|
+
* @returns `Ok(CommonOptions)` on success, or `Err(ParseOptionsError)` when a
|
|
39
|
+
* required flag is missing or an unrecognized flag was supplied.
|
|
40
|
+
*/
|
|
41
|
+
function parseCommonOptions(rawOptions, getEnvironment = readProcessEnvironment$1) {
|
|
42
|
+
for (const key of Object.keys(rawOptions)) if (!RECOGNIZED_FLAGS$1.has(key) && !SADE_RESERVED$1.has(key)) return {
|
|
43
|
+
err: {
|
|
44
|
+
flag: key,
|
|
45
|
+
kind: "unknownFlag"
|
|
46
|
+
},
|
|
47
|
+
success: false
|
|
48
|
+
};
|
|
49
|
+
const environments = resolveEnvironments(rawOptions["env"], getEnvironment);
|
|
50
|
+
if (!environments.success) return environments;
|
|
51
|
+
const apiKey = pickString(rawOptions, "apiKey", "api-key");
|
|
52
|
+
const configFile = pickString(rawOptions, "config");
|
|
53
|
+
const githubToken = pickString(rawOptions, "githubToken", "github-token");
|
|
54
|
+
return {
|
|
55
|
+
data: {
|
|
56
|
+
environments: environments.data,
|
|
57
|
+
...apiKey === void 0 ? {} : { apiKey },
|
|
58
|
+
...configFile === void 0 ? {} : { configFile },
|
|
59
|
+
...githubToken === void 0 ? {} : { githubToken }
|
|
60
|
+
},
|
|
61
|
+
success: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function resolveEnvironments(raw, getEnvironment) {
|
|
65
|
+
if (raw === void 0) {
|
|
66
|
+
const fallback = getEnvironment("BEDROCK_ENVIRONMENT");
|
|
67
|
+
if (fallback === void 0) return {
|
|
68
|
+
err: {
|
|
69
|
+
flag: "env",
|
|
70
|
+
kind: "missingRequired"
|
|
71
|
+
},
|
|
72
|
+
success: false
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
data: [fallback],
|
|
76
|
+
success: true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const candidates = Array.isArray(raw) ? raw : [raw];
|
|
80
|
+
const environments = [];
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
if (typeof candidate !== "string") return {
|
|
83
|
+
err: {
|
|
84
|
+
flag: "env",
|
|
85
|
+
kind: "invalidValue"
|
|
86
|
+
},
|
|
87
|
+
success: false
|
|
88
|
+
};
|
|
89
|
+
environments.push(candidate);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
data: environments,
|
|
93
|
+
success: true
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function readProcessEnvironment$1(name) {
|
|
97
|
+
return process.env[name];
|
|
98
|
+
}
|
|
99
|
+
function pickString(rawOptions, ...keys) {
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const value = rawOptions[key];
|
|
102
|
+
if (typeof value === "string") return value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/cli/render.ts
|
|
107
|
+
/**
|
|
108
|
+
* Render a `DeployError` to the supplied `ClackPort` as a single error line.
|
|
109
|
+
* Each variant produces a distinct, terse diagnostic; wrapped variants
|
|
110
|
+
* (`applyFailed`, `buildDesiredFailed`, `configLoadFailed`, `stateReadFailed`,
|
|
111
|
+
* `stateWriteFailed`) surface the inner cause's actionable detail (file path,
|
|
112
|
+
* resource key, parser message, HTTP failure, validator issue) so the reader
|
|
113
|
+
* does not have to inspect the full cause to act.
|
|
114
|
+
* @param err - The deploy error to describe.
|
|
115
|
+
* @param port - The output port the diagnostic is written to.
|
|
116
|
+
*/
|
|
117
|
+
function renderDeployError(err, port) {
|
|
118
|
+
port.logError(deployErrorMessage(err));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Render a `ParseOptionsError` to the supplied `ClackPort` as a single
|
|
122
|
+
* error line. Each variant names the offending flag so the diagnostic
|
|
123
|
+
* pinpoints what the caller needs to change.
|
|
124
|
+
* @param err - The parse error to describe.
|
|
125
|
+
* @param port - The output port the diagnostic is written to.
|
|
126
|
+
*/
|
|
127
|
+
function renderParseError(err, port) {
|
|
128
|
+
port.logError(parseErrorMessage(err));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Construct a `ClackPort` whose methods delegate to `@clack/prompts`. The
|
|
132
|
+
* resulting port writes to `process.stdout` via clack's defaults.
|
|
133
|
+
* @returns A port whose six methods each invoke the matching clack helper.
|
|
134
|
+
*/
|
|
135
|
+
function createClackPort() {
|
|
136
|
+
return {
|
|
137
|
+
cancel: (message) => {
|
|
138
|
+
cancel(message);
|
|
139
|
+
},
|
|
140
|
+
intro: (message) => {
|
|
141
|
+
intro(message);
|
|
142
|
+
},
|
|
143
|
+
logError: (message) => {
|
|
144
|
+
log.error(message);
|
|
145
|
+
},
|
|
146
|
+
logMessage: (message) => {
|
|
147
|
+
log.message(message);
|
|
148
|
+
},
|
|
149
|
+
logSuccess: (message) => {
|
|
150
|
+
log.success(message);
|
|
151
|
+
},
|
|
152
|
+
outro: (message) => {
|
|
153
|
+
outro(message);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Render a `ParseMigrateError` to the supplied `ClackPort`. Reuses
|
|
159
|
+
* `parseErrorMessage` for the three flag-shape variants and adds a
|
|
160
|
+
* dedicated message for `unknownSource` listing the supported sources.
|
|
161
|
+
* @param err - The parse error to describe.
|
|
162
|
+
* @param port - The output port the diagnostic is written to.
|
|
163
|
+
*/
|
|
164
|
+
function renderMigrateParseError(err, port) {
|
|
165
|
+
port.logError(migrateParseErrorMessage(err));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Render a `MigrateError` to the supplied `ClackPort` as a single error
|
|
169
|
+
* line. Each variant points at the offending Mantle state file path,
|
|
170
|
+
* primary-environment input, or wrapped `ConfigError` so the reader can
|
|
171
|
+
* act without inspecting the raw error object.
|
|
172
|
+
* @param err - The migrate error to describe.
|
|
173
|
+
* @param port - The output port the diagnostic is written to.
|
|
174
|
+
*/
|
|
175
|
+
function renderMigrateError(err, port) {
|
|
176
|
+
port.logError(migrateErrorMessage(err));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Render a `MissingCredentialError` or `UnsupportedBackendError`
|
|
180
|
+
* surfaced when the migrate command tried to default-construct the
|
|
181
|
+
* configured `StatePort` and was missing its inputs.
|
|
182
|
+
* @param err - The error returned by `buildStatePort`.
|
|
183
|
+
* @param port - The output port the diagnostic is written to.
|
|
184
|
+
*/
|
|
185
|
+
function renderBuildStatePortError(err, port) {
|
|
186
|
+
port.logError(buildStatePortErrorMessage(err));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Render the post-migrate review prompt to the supplied `ClackPort`.
|
|
190
|
+
* Three outcomes:
|
|
191
|
+
*
|
|
192
|
+
* - Any `ambiguous` warnings exist: emit a single error line directing
|
|
193
|
+
* the user to the report. The migration ran but there are decisions
|
|
194
|
+
* the user still needs to make before deploy will be meaningful.
|
|
195
|
+
* - No `ambiguous` warnings but non-zero `blocked` / `deferred` /
|
|
196
|
+
* `interpretive`: emit a single success line pointing at the report
|
|
197
|
+
* for auditing.
|
|
198
|
+
* - All counts zero: silent. The closing `outro("migrate succeeded")`
|
|
199
|
+
* already speaks for the run.
|
|
200
|
+
* @param input - Counts plus the path of the Markdown report.
|
|
201
|
+
* @param port - The output port the line is written to.
|
|
202
|
+
*/
|
|
203
|
+
function renderMigrationSummary(input, port) {
|
|
204
|
+
const { ambiguousCount, blockedCount, deferredCount, interpretiveCount } = input.summary;
|
|
205
|
+
if (ambiguousCount > 0) {
|
|
206
|
+
port.logError(`action required: ${String(ambiguousCount)} fields need your input. See ${input.reportPath}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const reviewable = blockedCount + deferredCount + interpretiveCount;
|
|
210
|
+
if (reviewable > 0) port.logSuccess(`migration complete; see ${input.reportPath} for ${String(reviewable)} auto-mapped or skipped fields`);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Render a `StateError` produced when the migrator wrote a per-environment
|
|
214
|
+
* state through the `StatePort`. Names the environment alongside the
|
|
215
|
+
* adapter's failure reason so the reader knows which write failed.
|
|
216
|
+
* @param input - Environment + state-error to describe.
|
|
217
|
+
* @param port - The output port the diagnostic is written to.
|
|
218
|
+
*/
|
|
219
|
+
function renderStateWriteError(input, port) {
|
|
220
|
+
port.logError(`state write failed for '${input.environment}' (${input.err.file}): ${input.err.reason}`);
|
|
221
|
+
}
|
|
222
|
+
function permissionDetail(err) {
|
|
223
|
+
const isPlural = err.requiredScopes.length > 1;
|
|
224
|
+
const label = isPlural ? "scopes" : "scope";
|
|
225
|
+
const pronoun = isPlural ? "them" : "it";
|
|
226
|
+
const scopeList = err.requiredScopes.map((scope) => `'${scope}'`).join(", ");
|
|
227
|
+
return `${err.message} on ${err.operationKey}: missing required ${label} ${scopeList}. Grant ${pronoun} on the API key at https://create.roblox.com/credentials`;
|
|
228
|
+
}
|
|
229
|
+
function applyCauseDetail(cause) {
|
|
230
|
+
switch (cause.kind) {
|
|
231
|
+
case "driverFailure":
|
|
232
|
+
if (cause.cause instanceof PermissionError) return permissionDetail(cause.cause);
|
|
233
|
+
return cause.cause.message;
|
|
234
|
+
case "updateUnsupported": return "update not supported";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function buildDesiredDetail(cause) {
|
|
238
|
+
switch (cause.kind) {
|
|
239
|
+
case "fileReadFailed": return `(${cause.filePath}): ${cause.reason}`;
|
|
240
|
+
case "iconRemovalRejected": return `: ${cause.message}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function configErrorDetail(err) {
|
|
244
|
+
switch (err.kind) {
|
|
245
|
+
case "configFunctionFailed": return `${err.sourceFile}: config function threw: ${err.message}`;
|
|
246
|
+
case "fileNotFound": return `no bedrock config under ${err.searchedFrom}`;
|
|
247
|
+
case "luauRuntimeMissing": return `${err.sourceFile}: ${err.hint}`;
|
|
248
|
+
case "parseFailed": return `${err.sourceFile}: ${err.message}`;
|
|
249
|
+
case "validationFailed": {
|
|
250
|
+
const first = err.issues[0];
|
|
251
|
+
return first === void 0 ? `${err.sourceFile}: invalid` : `${err.sourceFile}: ${first.path.join(".")} ${first.message}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function stateErrorDetail(cause) {
|
|
256
|
+
return `(${cause.file}): ${cause.reason}`;
|
|
257
|
+
}
|
|
258
|
+
function deployErrorMessage(err) {
|
|
259
|
+
switch (err.kind) {
|
|
260
|
+
case "applyFailed": return `apply failed for '${err.cause.key}': ${applyCauseDetail(err.cause)}`;
|
|
261
|
+
case "buildDesiredFailed": return `build desired state failed for '${err.cause.key}' ${buildDesiredDetail(err.cause)}`;
|
|
262
|
+
case "configLoadFailed": return `config load failed: ${configErrorDetail(err.cause)}`;
|
|
263
|
+
case "incompletePlaceEntry": return `place '${err.key}' is missing '${err.missingField}' under environment '${err.environment}'`;
|
|
264
|
+
case "incompleteUniverseEntry": return `universe is missing '${err.missingField}' under environment '${err.environment}'`;
|
|
265
|
+
case "missingCredential": return `missing credential: environment variable ${err.variable} is not set`;
|
|
266
|
+
case "registryConfigMissing": return `registry config missing '${err.missing}' (${err.hint})`;
|
|
267
|
+
case "stateNotConfigured": return `state not configured for environment '${err.environment}'`;
|
|
268
|
+
case "stateReadFailed": return `state read failed ${stateErrorDetail(err.cause)}`;
|
|
269
|
+
case "stateWriteFailed": return `state write failed ${stateErrorDetail(err.cause)}`;
|
|
270
|
+
case "unknownEnvironment": return `unknown environment '${err.environment}' (declared: ${err.declared.join(", ")})`;
|
|
271
|
+
case "unsupportedBackend": return `unsupported state backend '${err.backend}' (${err.hint})`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function parseErrorMessage(err) {
|
|
275
|
+
switch (err.kind) {
|
|
276
|
+
case "invalidValue": return `invalid value for flag '--${err.flag}' (expected a string)`;
|
|
277
|
+
case "missingRequired": return `missing required flag --${err.flag}`;
|
|
278
|
+
case "unknownFlag": return `unknown flag '--${err.flag}'`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function migrateParseErrorMessage(err) {
|
|
282
|
+
if (err.kind === "unknownSource") return `unknown migration source '${err.received}' (supported: ${err.supported.join(", ")})`;
|
|
283
|
+
return parseErrorMessage(err);
|
|
284
|
+
}
|
|
285
|
+
function migrateErrorMessage(err) {
|
|
286
|
+
switch (err.kind) {
|
|
287
|
+
case "internalError": return `migrate internal error: ${err.reason} (${configErrorDetail(err.cause)})`;
|
|
288
|
+
case "primaryEnvironmentNotFound": return `primary environment '${err.primary}' not found (available: ${err.available.join(", ")})`;
|
|
289
|
+
case "primaryEnvironmentRequired": return `primary environment required (available: ${err.available.join(", ")})`;
|
|
290
|
+
case "stateFileNotFound": return `Mantle state file not found at '${err.path}'`;
|
|
291
|
+
case "stateParseFailed": return `Mantle state file at '${err.path}' could not be parsed: ${err.reason}`;
|
|
292
|
+
case "unsupportedMantleStateVersion": return `unsupported Mantle state version '${err.found}' (supported: ${err.supported.join(", ")})`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function buildStatePortErrorMessage(err) {
|
|
296
|
+
switch (err.kind) {
|
|
297
|
+
case "missingCredential": return `missing credential: environment variable ${err.variable} is not set`;
|
|
298
|
+
case "unsupportedBackend": return `unsupported state backend '${err.backend}' (${err.hint})`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/cli/commands/deploy.ts
|
|
303
|
+
/**
|
|
304
|
+
* Build the sade action for `bedrock deploy`. The returned function consumes
|
|
305
|
+
* the raw options object sade hands the action callback, parses it via
|
|
306
|
+
* `parseCommonOptions`, loads the project config once, and dispatches
|
|
307
|
+
* `deploy()` for each `--env` value in order. Per-env successes and failures
|
|
308
|
+
* render through clack as a single line each; the aggregated exit code is
|
|
309
|
+
* `EXIT_OK` only when every env succeeded.
|
|
310
|
+
* @param deps - Dependency overrides; missing slots are default-constructed
|
|
311
|
+
* from real implementations.
|
|
312
|
+
* @returns An async sade action that returns once `deps.exit` was invoked.
|
|
313
|
+
*/
|
|
314
|
+
function deployCommand(deps) {
|
|
315
|
+
const resolved = resolveDeploy(deps);
|
|
316
|
+
return async (rawOptions) => {
|
|
317
|
+
const code = await runDeploy(rawOptions, resolved);
|
|
318
|
+
resolved.exit(code);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function resolveDeploy(deps) {
|
|
322
|
+
return {
|
|
323
|
+
clack: deps.clack ?? createClackPort(),
|
|
324
|
+
deploy: deps.deploy ?? deploy,
|
|
325
|
+
exit: deps.exit ?? ((code) => process.exit(code)),
|
|
326
|
+
loadConfig: deps.loadConfig ?? loadConfig
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function loadOptionsFor$1(parsed) {
|
|
330
|
+
return parsed.configFile === void 0 ? void 0 : { configFile: parsed.configFile };
|
|
331
|
+
}
|
|
332
|
+
async function dispatchEnvironments$1(inputs) {
|
|
333
|
+
const { config, environments, getEnv, resolved } = inputs;
|
|
334
|
+
const failed = [];
|
|
335
|
+
for (const environment of environments) {
|
|
336
|
+
const result = await resolved.deploy({
|
|
337
|
+
config,
|
|
338
|
+
environment,
|
|
339
|
+
getEnv
|
|
340
|
+
});
|
|
341
|
+
if (result.success) resolved.clack.logSuccess(`${environment}: ${result.data.resources.length} resources reconciled`);
|
|
342
|
+
else {
|
|
343
|
+
renderDeployError(result.err, resolved.clack);
|
|
344
|
+
failed.push(environment);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return failed;
|
|
348
|
+
}
|
|
349
|
+
function buildGetEnvironment$1(parsed) {
|
|
350
|
+
return (name) => {
|
|
351
|
+
if (name === "ROBLOX_API_KEY" && parsed.apiKey !== void 0) return parsed.apiKey;
|
|
352
|
+
if (name === "GITHUB_TOKEN" && parsed.githubToken !== void 0) return parsed.githubToken;
|
|
353
|
+
return process.env[name];
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function cancelAsFailed$1(clack) {
|
|
357
|
+
clack.cancel("deploy failed");
|
|
358
|
+
}
|
|
359
|
+
async function runDeploy(rawOptions, resolved) {
|
|
360
|
+
resolved.clack.intro("bedrock deploy");
|
|
361
|
+
const parsed = parseCommonOptions(rawOptions);
|
|
362
|
+
if (!parsed.success) {
|
|
363
|
+
renderParseError(parsed.err, resolved.clack);
|
|
364
|
+
cancelAsFailed$1(resolved.clack);
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
const loaded = await resolved.loadConfig(loadOptionsFor$1(parsed.data));
|
|
368
|
+
if (!loaded.success) {
|
|
369
|
+
renderDeployError({
|
|
370
|
+
cause: loaded.err,
|
|
371
|
+
kind: "configLoadFailed"
|
|
372
|
+
}, resolved.clack);
|
|
373
|
+
cancelAsFailed$1(resolved.clack);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
if ((await dispatchEnvironments$1({
|
|
377
|
+
config: loaded.data,
|
|
378
|
+
environments: parsed.data.environments,
|
|
379
|
+
getEnv: buildGetEnvironment$1(parsed.data),
|
|
380
|
+
resolved
|
|
381
|
+
})).length > 0) {
|
|
382
|
+
cancelAsFailed$1(resolved.clack);
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
resolved.clack.outro("deploy succeeded");
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/shell/preview-diff.ts
|
|
390
|
+
/**
|
|
391
|
+
* Compute the operations `deploy` would apply for a target environment
|
|
392
|
+
* without writing state. Default-constructs missing deps from the project
|
|
393
|
+
* config and `GITHUB_TOKEN`; never reads `process.env` when `statePort`
|
|
394
|
+
* and `config` are both supplied explicitly.
|
|
395
|
+
*
|
|
396
|
+
* @param options - Target environment plus optional overrides.
|
|
397
|
+
* @returns The computed operations on success, or a stage-tagged
|
|
398
|
+
* `PreviewDiffError` on failure.
|
|
399
|
+
*/
|
|
400
|
+
async function previewDiff(options) {
|
|
401
|
+
const resolved = await resolveDeps(options);
|
|
402
|
+
if (!resolved.success) return resolved;
|
|
403
|
+
return runPreview(options.environment, resolved.data);
|
|
404
|
+
}
|
|
405
|
+
async function pickConfig(options) {
|
|
406
|
+
if (options.config !== void 0) return {
|
|
407
|
+
data: options.config,
|
|
408
|
+
success: true
|
|
409
|
+
};
|
|
410
|
+
const loaded = await (options.loadConfig ?? loadConfig)();
|
|
411
|
+
if (!loaded.success) return {
|
|
412
|
+
err: {
|
|
413
|
+
cause: loaded.err,
|
|
414
|
+
kind: "configLoadFailed"
|
|
415
|
+
},
|
|
416
|
+
success: false
|
|
417
|
+
};
|
|
418
|
+
return {
|
|
419
|
+
data: loaded.data,
|
|
420
|
+
success: true
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function readProcessEnvironment(name) {
|
|
424
|
+
return process.env[name];
|
|
425
|
+
}
|
|
426
|
+
function getEnvironmentOf(options) {
|
|
427
|
+
return options.getEnv ?? readProcessEnvironment;
|
|
428
|
+
}
|
|
429
|
+
function pickStatePort(options, config) {
|
|
430
|
+
if (options.statePort !== void 0) return {
|
|
431
|
+
data: options.statePort,
|
|
432
|
+
success: true
|
|
433
|
+
};
|
|
434
|
+
const stateConfig = resolveStateConfig(config, options.environment);
|
|
435
|
+
if (!stateConfig.success) return {
|
|
436
|
+
err: stateConfig.err,
|
|
437
|
+
success: false
|
|
438
|
+
};
|
|
439
|
+
return buildStatePort({
|
|
440
|
+
fetch: options.fetch,
|
|
441
|
+
getEnv: getEnvironmentOf(options),
|
|
442
|
+
stateConfig: stateConfig.data
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
async function resolveDeps(options) {
|
|
446
|
+
const config = await pickConfig(options);
|
|
447
|
+
if (!config.success) return config;
|
|
448
|
+
const selected = selectEnvironment(config.data, options.environment);
|
|
449
|
+
if (!selected.success) return {
|
|
450
|
+
err: selected.err,
|
|
451
|
+
success: false
|
|
452
|
+
};
|
|
453
|
+
const effective = selected.data;
|
|
454
|
+
const readFile$1 = options.readFile ?? readFile;
|
|
455
|
+
const statePort = pickStatePort(options, effective);
|
|
456
|
+
if (!statePort.success) return statePort;
|
|
457
|
+
return {
|
|
458
|
+
data: {
|
|
459
|
+
config: effective,
|
|
460
|
+
readFile: readFile$1,
|
|
461
|
+
statePort: statePort.data
|
|
462
|
+
},
|
|
463
|
+
success: true
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function runPreview(environment, deps) {
|
|
467
|
+
const desired = await buildDesired(flattenConfig(deps.config), deps.readFile);
|
|
468
|
+
if (!desired.success) return {
|
|
469
|
+
err: {
|
|
470
|
+
cause: desired.err,
|
|
471
|
+
kind: "buildDesiredFailed"
|
|
472
|
+
},
|
|
473
|
+
success: false
|
|
474
|
+
};
|
|
475
|
+
const prior = await deps.statePort.read(environment);
|
|
476
|
+
if (!prior.success) return {
|
|
477
|
+
err: {
|
|
478
|
+
cause: prior.err,
|
|
479
|
+
kind: "stateReadFailed"
|
|
480
|
+
},
|
|
481
|
+
success: false
|
|
482
|
+
};
|
|
483
|
+
const priorResources = prior.data?.resources ?? [];
|
|
484
|
+
const validated = validatePlan(desired.data, priorResources);
|
|
485
|
+
if (!validated.success) return {
|
|
486
|
+
err: {
|
|
487
|
+
cause: validated.err,
|
|
488
|
+
kind: "buildDesiredFailed"
|
|
489
|
+
},
|
|
490
|
+
success: false
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
data: {
|
|
494
|
+
environment,
|
|
495
|
+
ops: diff(desired.data, priorResources)
|
|
496
|
+
},
|
|
497
|
+
success: true
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/cli/commands/diff.ts
|
|
502
|
+
/**
|
|
503
|
+
* Build the sade action for `bedrock diff`. The returned function consumes
|
|
504
|
+
* the raw options object sade hands the action callback, parses it via
|
|
505
|
+
* `parseCommonOptions`, loads the project config once, and dispatches
|
|
506
|
+
* `previewDiff()` for each `--env` value in order. Per-env successes render
|
|
507
|
+
* the operations list (or a `No drift` line when every op is a noop);
|
|
508
|
+
* failures render via `renderDeployError`. The aggregated exit code is
|
|
509
|
+
* `EXIT_OK` only when every env succeeded.
|
|
510
|
+
* @param deps - Dependency overrides; missing slots are default-constructed
|
|
511
|
+
* from real implementations.
|
|
512
|
+
* @returns An async sade action that returns once `deps.exit` was invoked.
|
|
513
|
+
*/
|
|
514
|
+
function diffCommand(deps) {
|
|
515
|
+
const resolved = resolveDiff(deps);
|
|
516
|
+
return async (rawOptions) => {
|
|
517
|
+
const code = await runDiff(rawOptions, resolved);
|
|
518
|
+
resolved.exit(code);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function resolveDiff(deps) {
|
|
522
|
+
return {
|
|
523
|
+
clack: deps.clack ?? createClackPort(),
|
|
524
|
+
exit: deps.exit ?? ((code) => process.exit(code)),
|
|
525
|
+
loadConfig: deps.loadConfig ?? loadConfig,
|
|
526
|
+
previewDiff: deps.previewDiff ?? previewDiff
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function loadOptionsFor(parsed) {
|
|
530
|
+
return parsed.configFile === void 0 ? void 0 : { configFile: parsed.configFile };
|
|
531
|
+
}
|
|
532
|
+
function buildGetEnvironment(parsed) {
|
|
533
|
+
return (name) => {
|
|
534
|
+
if (name === "ROBLOX_API_KEY" && parsed.apiKey !== void 0) return parsed.apiKey;
|
|
535
|
+
if (name === "GITHUB_TOKEN" && parsed.githubToken !== void 0) return parsed.githubToken;
|
|
536
|
+
return process.env[name];
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function cancelAsFailed(clack) {
|
|
540
|
+
clack.cancel("diff failed");
|
|
541
|
+
}
|
|
542
|
+
function describeOp(op) {
|
|
543
|
+
switch (op.type) {
|
|
544
|
+
case "create": return `+ ${op.desired.kind}:${op.key}`;
|
|
545
|
+
case "update": return `~ ${op.desired.kind}:${op.key}`;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function isDriftOp(op) {
|
|
549
|
+
return op.type !== "noop";
|
|
550
|
+
}
|
|
551
|
+
function renderPreview(preview, clack) {
|
|
552
|
+
const drift = preview.ops.filter(isDriftOp);
|
|
553
|
+
if (drift.length === 0) {
|
|
554
|
+
clack.logSuccess(`No drift for "${preview.environment}"`);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
clack.logMessage(`Pending changes for "${preview.environment}":`);
|
|
558
|
+
for (const op of drift) clack.logMessage(describeOp(op));
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
async function dispatchEnvironments(inputs) {
|
|
562
|
+
const { config, environments, getEnv, resolved } = inputs;
|
|
563
|
+
const failed = [];
|
|
564
|
+
let hasDrift = false;
|
|
565
|
+
for (const environment of environments) {
|
|
566
|
+
const result = await resolved.previewDiff({
|
|
567
|
+
config,
|
|
568
|
+
environment,
|
|
569
|
+
getEnv
|
|
570
|
+
});
|
|
571
|
+
if (result.success) {
|
|
572
|
+
if (renderPreview(result.data, resolved.clack)) hasDrift = true;
|
|
573
|
+
} else {
|
|
574
|
+
renderDeployError(result.err, resolved.clack);
|
|
575
|
+
failed.push(environment);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
failed,
|
|
580
|
+
hasDrift
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function outroFor(hasDrift) {
|
|
584
|
+
return hasDrift ? "run bedrock deploy to apply pending changes" : "all environments are up to date";
|
|
585
|
+
}
|
|
586
|
+
async function runDiff(rawOptions, resolved) {
|
|
587
|
+
resolved.clack.intro("bedrock diff");
|
|
588
|
+
const parsed = parseCommonOptions(rawOptions);
|
|
589
|
+
if (!parsed.success) {
|
|
590
|
+
renderParseError(parsed.err, resolved.clack);
|
|
591
|
+
cancelAsFailed(resolved.clack);
|
|
592
|
+
return 1;
|
|
593
|
+
}
|
|
594
|
+
const loaded = await resolved.loadConfig(loadOptionsFor(parsed.data));
|
|
595
|
+
if (!loaded.success) {
|
|
596
|
+
renderDeployError({
|
|
597
|
+
cause: loaded.err,
|
|
598
|
+
kind: "configLoadFailed"
|
|
599
|
+
}, resolved.clack);
|
|
600
|
+
cancelAsFailed(resolved.clack);
|
|
601
|
+
return 1;
|
|
602
|
+
}
|
|
603
|
+
const outcome = await dispatchEnvironments({
|
|
604
|
+
config: loaded.data,
|
|
605
|
+
environments: parsed.data.environments,
|
|
606
|
+
getEnv: buildGetEnvironment(parsed.data),
|
|
607
|
+
resolved
|
|
608
|
+
});
|
|
609
|
+
if (outcome.failed.length > 0) {
|
|
610
|
+
cancelAsFailed(resolved.clack);
|
|
611
|
+
return 1;
|
|
612
|
+
}
|
|
613
|
+
resolved.clack.outro(outroFor(outcome.hasDrift));
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/cli/default-migrate-prompt-port.ts
|
|
618
|
+
const FORMAT_OPTIONS = [{
|
|
619
|
+
hint: "recommended",
|
|
620
|
+
label: "TypeScript",
|
|
621
|
+
value: "typescript"
|
|
622
|
+
}, {
|
|
623
|
+
label: "YAML",
|
|
624
|
+
value: "yaml"
|
|
625
|
+
}];
|
|
626
|
+
const BACKEND_OPTIONS = [{
|
|
627
|
+
label: "GitHub Gist",
|
|
628
|
+
value: "gist"
|
|
629
|
+
}, {
|
|
630
|
+
hint: "writes .bedrock/state/<env>.json next to bedrock.config",
|
|
631
|
+
label: "Local files",
|
|
632
|
+
value: "local"
|
|
633
|
+
}];
|
|
634
|
+
const SOURCE_LABELS = { mantle: "Mantle" };
|
|
635
|
+
const defaultHelpers = {
|
|
636
|
+
isCancel,
|
|
637
|
+
path,
|
|
638
|
+
select,
|
|
639
|
+
text
|
|
640
|
+
};
|
|
641
|
+
/**
|
|
642
|
+
* Construct a `MigratePromptPort` whose methods delegate to
|
|
643
|
+
* `@clack/prompts`. Each prompt translates clack's cancel sentinel into
|
|
644
|
+
* a typed `Err({ kind: "cancelled" })` so the migrate command branches
|
|
645
|
+
* on `Result` like every other shell call.
|
|
646
|
+
*
|
|
647
|
+
* @param helpers - Test-only seam for swapping the three clack
|
|
648
|
+
* primitives. Production callers omit this argument.
|
|
649
|
+
* @returns A live `MigratePromptPort` ready to drive interactively.
|
|
650
|
+
*/
|
|
651
|
+
function createDefaultMigratePromptPort(helpers = defaultHelpers) {
|
|
652
|
+
return {
|
|
653
|
+
promptConfigFormat: async () => promptConfigFormatFrom(helpers),
|
|
654
|
+
promptGistId: async () => promptGistIdFrom(helpers),
|
|
655
|
+
promptMigrationSource: async (sources) => selectMigrationSource(helpers, sources),
|
|
656
|
+
promptPrimaryEnvironment: async (environments) => selectPrimaryEnvironment(helpers, environments),
|
|
657
|
+
promptStateBackend: async () => promptStateBackendFrom(helpers),
|
|
658
|
+
promptStateFilePath: async () => promptStateFilePathFrom(helpers)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async function fromSelect(helpers, inputs) {
|
|
662
|
+
const result = await helpers.select({
|
|
663
|
+
message: inputs.message,
|
|
664
|
+
options: inputs.options.map((option) => {
|
|
665
|
+
return {
|
|
666
|
+
...option.hint === void 0 ? {} : { hint: option.hint },
|
|
667
|
+
label: option.label,
|
|
668
|
+
value: option.value
|
|
669
|
+
};
|
|
670
|
+
}),
|
|
671
|
+
...inputs.initialValue === void 0 ? {} : { initialValue: inputs.initialValue }
|
|
672
|
+
});
|
|
673
|
+
if (helpers.isCancel(result)) return {
|
|
674
|
+
err: { kind: "cancelled" },
|
|
675
|
+
success: false
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
data: result,
|
|
679
|
+
success: true
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function selectMigrationSource(helpers, sources) {
|
|
683
|
+
return fromSelect(helpers, {
|
|
684
|
+
initialValue: sources[0],
|
|
685
|
+
message: "Migrate from?",
|
|
686
|
+
options: sources.map((source) => ({
|
|
687
|
+
label: SOURCE_LABELS[source],
|
|
688
|
+
value: source
|
|
689
|
+
}))
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
async function promptConfigFormatFrom(helpers) {
|
|
693
|
+
return fromSelect(helpers, {
|
|
694
|
+
initialValue: "typescript",
|
|
695
|
+
message: "Output config format?",
|
|
696
|
+
options: FORMAT_OPTIONS
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
function validateNonEmpty(value) {
|
|
700
|
+
if (value === void 0 || value.trim() === "") return "Required";
|
|
701
|
+
}
|
|
702
|
+
async function fromText(helpers, options) {
|
|
703
|
+
const result = await helpers.text(options);
|
|
704
|
+
if (helpers.isCancel(result)) return {
|
|
705
|
+
err: { kind: "cancelled" },
|
|
706
|
+
success: false
|
|
707
|
+
};
|
|
708
|
+
return {
|
|
709
|
+
data: result,
|
|
710
|
+
success: true
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
async function promptGistIdFrom(helpers) {
|
|
714
|
+
return fromText(helpers, {
|
|
715
|
+
message: "Gist ID for state storage?",
|
|
716
|
+
placeholder: "abc123",
|
|
717
|
+
validate: validateNonEmpty
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
async function selectPrimaryEnvironment(helpers, environments) {
|
|
721
|
+
return fromSelect(helpers, {
|
|
722
|
+
message: "Which environment should be the primary?\nThe migrator uses it as the baseline for the generated config.",
|
|
723
|
+
options: environments.map((name) => ({
|
|
724
|
+
label: name,
|
|
725
|
+
value: name
|
|
726
|
+
}))
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
async function promptStateBackendFrom(helpers) {
|
|
730
|
+
return fromSelect(helpers, {
|
|
731
|
+
initialValue: "gist",
|
|
732
|
+
message: "State backend?",
|
|
733
|
+
options: BACKEND_OPTIONS
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
async function fromPath(helpers, options) {
|
|
737
|
+
const result = await helpers.path(options);
|
|
738
|
+
if (helpers.isCancel(result)) return {
|
|
739
|
+
err: { kind: "cancelled" },
|
|
740
|
+
success: false
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
data: result,
|
|
744
|
+
success: true
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async function promptStateFilePathFrom(helpers) {
|
|
748
|
+
return fromPath(helpers, {
|
|
749
|
+
initialValue: ".mantle-state.yml",
|
|
750
|
+
message: "Path to the Mantle state file?",
|
|
751
|
+
validate: validateNonEmpty
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region src/cli/parse-migrate-options.ts
|
|
756
|
+
/**
|
|
757
|
+
* Sources `bedrock migrate --from <source>` accepts. Today only `"mantle"`
|
|
758
|
+
* is wired up; widening this tuple turns on additional sources without
|
|
759
|
+
* touching the parser.
|
|
760
|
+
*/
|
|
761
|
+
const SUPPORTED_MIGRATION_SOURCES = ["mantle"];
|
|
762
|
+
const RECOGNIZED_FLAGS = new Set(["from"]);
|
|
763
|
+
const SADE_RESERVED = new Set([
|
|
764
|
+
"--",
|
|
765
|
+
"_",
|
|
766
|
+
"h",
|
|
767
|
+
"help",
|
|
768
|
+
"v",
|
|
769
|
+
"version"
|
|
770
|
+
]);
|
|
771
|
+
/**
|
|
772
|
+
* Translate the raw sade options POJO into a typed `MigrateOptions`. The
|
|
773
|
+
* positional `<stateFilePath>` argument is not handled here: sade hands
|
|
774
|
+
* positional values to the action callback ahead of the options object,
|
|
775
|
+
* and the migrate command falls back to an interactive prompt when it
|
|
776
|
+
* is absent. This parser only covers the `--from` flag, which is also
|
|
777
|
+
* optional and prompted for when omitted.
|
|
778
|
+
*
|
|
779
|
+
* @param rawOptions - The options object sade hands the action callback.
|
|
780
|
+
* @returns `Ok(MigrateOptions)` on success, or `Err(ParseMigrateError)`
|
|
781
|
+
* describing the offending flag or value.
|
|
782
|
+
*/
|
|
783
|
+
function parseMigrateOptions(rawOptions) {
|
|
784
|
+
for (const key of Object.keys(rawOptions)) if (!RECOGNIZED_FLAGS.has(key) && !SADE_RESERVED.has(key)) return {
|
|
785
|
+
err: {
|
|
786
|
+
flag: key,
|
|
787
|
+
kind: "unknownFlag"
|
|
788
|
+
},
|
|
789
|
+
success: false
|
|
790
|
+
};
|
|
791
|
+
const fromRaw = rawOptions["from"];
|
|
792
|
+
if (fromRaw === void 0) return {
|
|
793
|
+
data: { from: void 0 },
|
|
794
|
+
success: true
|
|
795
|
+
};
|
|
796
|
+
if (typeof fromRaw !== "string") return {
|
|
797
|
+
err: {
|
|
798
|
+
flag: "from",
|
|
799
|
+
kind: "invalidValue"
|
|
800
|
+
},
|
|
801
|
+
success: false
|
|
802
|
+
};
|
|
803
|
+
if (!isMigrationSource(fromRaw)) return {
|
|
804
|
+
err: {
|
|
805
|
+
kind: "unknownSource",
|
|
806
|
+
received: fromRaw,
|
|
807
|
+
supported: SUPPORTED_MIGRATION_SOURCES
|
|
808
|
+
},
|
|
809
|
+
success: false
|
|
810
|
+
};
|
|
811
|
+
return {
|
|
812
|
+
data: { from: fromRaw },
|
|
813
|
+
success: true
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function isMigrationSource(value) {
|
|
817
|
+
return SUPPORTED_MIGRATION_SOURCES.includes(value);
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/cli/commands/describe-unknown.ts
|
|
821
|
+
/**
|
|
822
|
+
* Format a thrown value as a human-readable string for clack diagnostic
|
|
823
|
+
* lines. Native `Error` instances surface their `message`; everything
|
|
824
|
+
* else is coerced via `String()` so non-Error throws (rejection of a
|
|
825
|
+
* primitive, for example) still render legibly.
|
|
826
|
+
*
|
|
827
|
+
* @param value - The caught value to describe.
|
|
828
|
+
* @returns A short string suitable for inlining into a CLI error line.
|
|
829
|
+
*/
|
|
830
|
+
function describeUnknown(value) {
|
|
831
|
+
return value instanceof Error ? value.message : String(value);
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/cli/commands/write-migrated-states.ts
|
|
835
|
+
/**
|
|
836
|
+
* Persist every per-environment state in the migration report to the
|
|
837
|
+
* resolved target. Dispatches to the GitHub Gist `StatePort` adapter or
|
|
838
|
+
* to a local-file dump under `target.outputDir`. On failure the writer
|
|
839
|
+
* has already rendered the error to `deps.clack`; the caller only needs
|
|
840
|
+
* to translate the Err into an exit code.
|
|
841
|
+
*
|
|
842
|
+
* @param inputs - Resolved deps, the migration report, and the target
|
|
843
|
+
* the writer should dispatch on.
|
|
844
|
+
* @returns `Ok(void)` once every environment has been written; `Err(void)`
|
|
845
|
+
* on the first failure (already rendered to clack).
|
|
846
|
+
*/
|
|
847
|
+
async function writeMigratedStates(inputs) {
|
|
848
|
+
if (inputs.target.backend === "local") return writeStatesToLocal({
|
|
849
|
+
...inputs,
|
|
850
|
+
target: inputs.target
|
|
851
|
+
});
|
|
852
|
+
return writeStatesToGist({
|
|
853
|
+
...inputs,
|
|
854
|
+
target: inputs.target
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
async function writeStatesToGist(inputs) {
|
|
858
|
+
const { deps, report, target } = inputs;
|
|
859
|
+
const portResult = deps.buildStatePort({
|
|
860
|
+
getEnv: (name) => process.env[name],
|
|
861
|
+
stateConfig: target.stateConfig
|
|
862
|
+
});
|
|
863
|
+
if (!portResult.success) {
|
|
864
|
+
renderBuildStatePortError(portResult.err, deps.clack);
|
|
865
|
+
return {
|
|
866
|
+
err: void 0,
|
|
867
|
+
success: false
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
for (const [environment, state] of Object.entries(report.statesByEnvironment)) {
|
|
871
|
+
const writeResult = await portResult.data.write(state);
|
|
872
|
+
if (!writeResult.success) {
|
|
873
|
+
renderStateWriteError({
|
|
874
|
+
environment,
|
|
875
|
+
err: writeResult.err
|
|
876
|
+
}, deps.clack);
|
|
877
|
+
return {
|
|
878
|
+
err: void 0,
|
|
879
|
+
success: false
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
deps.clack.logSuccess(`${environment}: ${state.resources.length} resources migrated`);
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
data: void 0,
|
|
886
|
+
success: true
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
async function writeStatesToLocal(inputs) {
|
|
890
|
+
const { deps, report, target } = inputs;
|
|
891
|
+
try {
|
|
892
|
+
await deps.mkdir(target.outputDir);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
deps.clack.logError(`local state directory create failed (${target.outputDir}): ${describeUnknown(err)}`);
|
|
895
|
+
return {
|
|
896
|
+
err: void 0,
|
|
897
|
+
success: false
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
for (const [environment, state] of Object.entries(report.statesByEnvironment)) {
|
|
901
|
+
const filePath = join(target.outputDir, `${environment}.json`);
|
|
902
|
+
try {
|
|
903
|
+
await deps.writeFile(filePath, serializeStateFile(state));
|
|
904
|
+
} catch (err) {
|
|
905
|
+
deps.clack.logError(`local state write failed (${filePath}): ${describeUnknown(err)}`);
|
|
906
|
+
return {
|
|
907
|
+
err: void 0,
|
|
908
|
+
success: false
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
deps.clack.logSuccess(`${environment}: ${state.resources.length} resources migrated`);
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
data: void 0,
|
|
915
|
+
success: true
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/core/migrate/render-migration-report-md.ts
|
|
920
|
+
/**
|
|
921
|
+
* Render a {@link MigrationReportFile} as the Markdown body written to
|
|
922
|
+
* `.bedrock/migration-report.md`. Pure derivation: the same input shape
|
|
923
|
+
* the JSON serializer consumes feeds this renderer, so the Markdown view
|
|
924
|
+
* round-trips through the JSON file.
|
|
925
|
+
*
|
|
926
|
+
* Output structure:
|
|
927
|
+
* - Header with the four counts.
|
|
928
|
+
* - Sections in user-action priority order (Action required, Blocked,
|
|
929
|
+
* Deferred, Interpretive). Sections with no matching warnings are
|
|
930
|
+
* omitted entirely.
|
|
931
|
+
* - Within each section, warnings are grouped by their non-path
|
|
932
|
+
* discriminator (`hint` for ambiguous, `reason` for blocked/deferred,
|
|
933
|
+
* `rule` for interpretive). First-appearance order is preserved.
|
|
934
|
+
* - Interpretive entries render as `mantlePath -> bedrockPath` so the
|
|
935
|
+
* user can verify the auto-applied mapping at a glance.
|
|
936
|
+
*
|
|
937
|
+
* @param file - The summary plus warnings to render.
|
|
938
|
+
* @returns Markdown source ending with a trailing newline.
|
|
939
|
+
*/
|
|
940
|
+
function renderMigrationReportMarkdown(file) {
|
|
941
|
+
const sections = [
|
|
942
|
+
renderActionRequired(file.warnings),
|
|
943
|
+
renderBlocked(file.warnings),
|
|
944
|
+
renderDeferred(file.warnings),
|
|
945
|
+
renderInterpretive(file.warnings)
|
|
946
|
+
].filter((section) => section !== "");
|
|
947
|
+
return [renderHeader(file), ...sections].join("\n");
|
|
948
|
+
}
|
|
949
|
+
function renderHeader(file) {
|
|
950
|
+
return [
|
|
951
|
+
"# Migration report",
|
|
952
|
+
"",
|
|
953
|
+
`ambiguous: ${String(file.summary.ambiguousCount)}`,
|
|
954
|
+
`blocked: ${String(file.summary.blockedCount)}`,
|
|
955
|
+
`deferred: ${String(file.summary.deferredCount)}`,
|
|
956
|
+
`interpretive: ${String(file.summary.interpretiveCount)}`,
|
|
957
|
+
""
|
|
958
|
+
].join("\n");
|
|
959
|
+
}
|
|
960
|
+
function suffixOf(mantlePath) {
|
|
961
|
+
return mantlePath.slice(mantlePath.indexOf(".") + 1);
|
|
962
|
+
}
|
|
963
|
+
function groupByKey(warnings, key) {
|
|
964
|
+
const orderedKeys = [...new Set(warnings.map(key))];
|
|
965
|
+
return new Map(orderedKeys.map((groupKey) => [groupKey, warnings.filter((warning) => key(warning) === groupKey)]));
|
|
966
|
+
}
|
|
967
|
+
function environmentOf(mantlePath) {
|
|
968
|
+
const dot = mantlePath.indexOf(".");
|
|
969
|
+
return dot === -1 ? void 0 : mantlePath.slice(0, dot);
|
|
970
|
+
}
|
|
971
|
+
function environmentsOf(members) {
|
|
972
|
+
const environments = members.map((warning) => environmentOf(warning.mantlePath)).filter((environment) => environment !== void 0);
|
|
973
|
+
return [...new Set(environments)];
|
|
974
|
+
}
|
|
975
|
+
function renderBullet(subject, members) {
|
|
976
|
+
const environments = environmentsOf(members);
|
|
977
|
+
return environments.length === 0 ? `- ${subject}` : `- ${subject} (${environments.join(", ")})`;
|
|
978
|
+
}
|
|
979
|
+
function renderSection(input) {
|
|
980
|
+
if (input.warnings.length === 0) return "";
|
|
981
|
+
const blocks = [...groupByKey(input.warnings, input.groupKey)].map(([heading, members]) => {
|
|
982
|
+
const lines = [...groupByKey(members, input.subject)].map(([key, subjectMembers]) => renderBullet(key, subjectMembers));
|
|
983
|
+
return [
|
|
984
|
+
`### ${heading}`,
|
|
985
|
+
"",
|
|
986
|
+
...lines,
|
|
987
|
+
""
|
|
988
|
+
].join("\n");
|
|
989
|
+
});
|
|
990
|
+
return [
|
|
991
|
+
`## ${input.title}`,
|
|
992
|
+
"",
|
|
993
|
+
...blocks
|
|
994
|
+
].join("\n");
|
|
995
|
+
}
|
|
996
|
+
function renderActionRequired(warnings) {
|
|
997
|
+
return renderSection({
|
|
998
|
+
groupKey: (warning) => warning.hint,
|
|
999
|
+
subject: (warning) => suffixOf(warning.mantlePath),
|
|
1000
|
+
title: "Action required",
|
|
1001
|
+
warnings: warnings.filter((warning) => {
|
|
1002
|
+
return warning.kind === "ambiguous";
|
|
1003
|
+
})
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function renderBlocked(warnings) {
|
|
1007
|
+
return renderSection({
|
|
1008
|
+
groupKey: (warning) => warning.reason,
|
|
1009
|
+
subject: (warning) => suffixOf(warning.mantlePath),
|
|
1010
|
+
title: "Won't migrate (no Open Cloud equivalent)",
|
|
1011
|
+
warnings: warnings.filter((warning) => {
|
|
1012
|
+
return warning.kind === "blocked";
|
|
1013
|
+
})
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
function renderDeferred(warnings) {
|
|
1017
|
+
return renderSection({
|
|
1018
|
+
groupKey: (warning) => warning.reason,
|
|
1019
|
+
subject: (warning) => suffixOf(warning.mantlePath),
|
|
1020
|
+
title: "Coming later (skipped for now)",
|
|
1021
|
+
warnings: warnings.filter((warning) => {
|
|
1022
|
+
return warning.kind === "deferred";
|
|
1023
|
+
})
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
function renderInterpretive(warnings) {
|
|
1027
|
+
return renderSection({
|
|
1028
|
+
groupKey: (warning) => warning.rule,
|
|
1029
|
+
subject: (warning) => `${suffixOf(warning.mantlePath)} -> ${warning.bedrockPath}`,
|
|
1030
|
+
title: "Auto-mapped (please verify)",
|
|
1031
|
+
warnings: warnings.filter((warning) => {
|
|
1032
|
+
return warning.kind === "interpretive";
|
|
1033
|
+
})
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
//#endregion
|
|
1037
|
+
//#region src/core/migrate/serialize-migration-report.ts
|
|
1038
|
+
/**
|
|
1039
|
+
* Serialize a {@link MigrationReportFile} as the bytes written to
|
|
1040
|
+
* `.bedrock/migration-report.json`. The output is canonical JSON:
|
|
1041
|
+
* pretty-printed with two-space indentation, terminated with a trailing newline.
|
|
1042
|
+
*
|
|
1043
|
+
* @param file - The summary plus warnings to serialize.
|
|
1044
|
+
* @returns A UTF-8 source string ending with a trailing newline.
|
|
1045
|
+
*/
|
|
1046
|
+
function serializeMigrationReport(file) {
|
|
1047
|
+
return `${JSON.stringify(file, void 0, 2)}\n`;
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/cli/commands/write-migration-report.ts
|
|
1051
|
+
const REPORT_DIR_NAME = ".bedrock";
|
|
1052
|
+
const JSON_FILE_NAME = "migration-report.json";
|
|
1053
|
+
const MD_FILE_NAME = "migration-report.md";
|
|
1054
|
+
/**
|
|
1055
|
+
* Persist the migration report as `.bedrock/migration-report.json` and a
|
|
1056
|
+
* pure derivation `.bedrock/migration-report.md` next to the per-environment
|
|
1057
|
+
* state files. Always written so the user can compare runs even when no
|
|
1058
|
+
* warnings were emitted; the CLI summary line that points at the Markdown
|
|
1059
|
+
* file is gated on warning count separately.
|
|
1060
|
+
*
|
|
1061
|
+
* @param input - Resolved deps, the migration report, and the path of the
|
|
1062
|
+
* Mantle state file the report describes.
|
|
1063
|
+
* @returns `Ok` with both file paths once both writes succeed; `Err` on the
|
|
1064
|
+
* first failure (already rendered to clack).
|
|
1065
|
+
*/
|
|
1066
|
+
async function writeMigrationReport(input) {
|
|
1067
|
+
const { deps, report, stateFilePath } = input;
|
|
1068
|
+
const reportDirectory = join(dirname(stateFilePath), REPORT_DIR_NAME);
|
|
1069
|
+
const jsonPath = join(reportDirectory, JSON_FILE_NAME);
|
|
1070
|
+
const mdPath = join(reportDirectory, MD_FILE_NAME);
|
|
1071
|
+
const file = {
|
|
1072
|
+
summary: report.summary,
|
|
1073
|
+
warnings: report.warnings
|
|
1074
|
+
};
|
|
1075
|
+
try {
|
|
1076
|
+
await deps.mkdir(reportDirectory);
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
deps.clack.logError(`migration report directory create failed (${reportDirectory}): ${describeUnknown(err)}`);
|
|
1079
|
+
return {
|
|
1080
|
+
err: void 0,
|
|
1081
|
+
success: false
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
await deps.writeFile(jsonPath, serializeMigrationReport(file));
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
deps.clack.logError(`migration report write failed (${jsonPath}): ${describeUnknown(err)}`);
|
|
1088
|
+
return {
|
|
1089
|
+
err: void 0,
|
|
1090
|
+
success: false
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
await deps.writeFile(mdPath, renderMigrationReportMarkdown(file));
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
deps.clack.logError(`migration report write failed (${mdPath}): ${describeUnknown(err)}`);
|
|
1097
|
+
return {
|
|
1098
|
+
err: void 0,
|
|
1099
|
+
success: false
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
data: {
|
|
1104
|
+
jsonPath,
|
|
1105
|
+
mdPath
|
|
1106
|
+
},
|
|
1107
|
+
success: true
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/cli/commands/finalize-migration.ts
|
|
1112
|
+
/**
|
|
1113
|
+
* Persist the migration's per-environment state, the bedrock config, and
|
|
1114
|
+
* the migration report (JSON + Markdown) in that order. Each failure
|
|
1115
|
+
* surface has already been rendered to clack by its respective writer; on
|
|
1116
|
+
* the first failure the function returns `Err` so the caller can exit
|
|
1117
|
+
* after a single shared `cancel("migrate failed")` line.
|
|
1118
|
+
*
|
|
1119
|
+
* @param inputs - Resolved deps, the migration report, and the resolved
|
|
1120
|
+
* state-target plus paths the writers consume.
|
|
1121
|
+
* @returns `Ok` carrying the on-disk Markdown report path (used by the
|
|
1122
|
+
* terminal summary line) once every writer succeeded; `Err(void)` once
|
|
1123
|
+
* any writer reported failure.
|
|
1124
|
+
*/
|
|
1125
|
+
async function persistMigration(inputs) {
|
|
1126
|
+
if (!(await persistStateAndConfig(inputs)).success) return {
|
|
1127
|
+
err: void 0,
|
|
1128
|
+
success: false
|
|
1129
|
+
};
|
|
1130
|
+
const reportPaths = await writeMigrationReport({
|
|
1131
|
+
deps: {
|
|
1132
|
+
clack: inputs.deps.clack,
|
|
1133
|
+
mkdir: inputs.deps.mkdir,
|
|
1134
|
+
writeFile: inputs.deps.writeFile
|
|
1135
|
+
},
|
|
1136
|
+
report: inputs.report,
|
|
1137
|
+
stateFilePath: inputs.stateFilePath
|
|
1138
|
+
});
|
|
1139
|
+
return reportPaths.success ? {
|
|
1140
|
+
data: reportPaths.data.mdPath,
|
|
1141
|
+
success: true
|
|
1142
|
+
} : {
|
|
1143
|
+
err: void 0,
|
|
1144
|
+
success: false
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async function writeBedrockConfig(inputs) {
|
|
1148
|
+
const { configFilePath, configFormat, deps, report, target } = inputs;
|
|
1149
|
+
const { state: _ignoredState, ...configWithoutState } = report.config;
|
|
1150
|
+
const bytes = serializeConfig({
|
|
1151
|
+
config: target.backend === "gist" ? {
|
|
1152
|
+
...configWithoutState,
|
|
1153
|
+
state: target.stateConfig
|
|
1154
|
+
} : configWithoutState,
|
|
1155
|
+
configFormat
|
|
1156
|
+
});
|
|
1157
|
+
try {
|
|
1158
|
+
await deps.writeFile(configFilePath, bytes);
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
deps.clack.logError(`config file write failed (${configFilePath}): ${describeUnknown(err)}`);
|
|
1161
|
+
return {
|
|
1162
|
+
err: void 0,
|
|
1163
|
+
success: false
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
deps.clack.logSuccess(`wrote ${configFilePath}`);
|
|
1167
|
+
return {
|
|
1168
|
+
data: void 0,
|
|
1169
|
+
success: true
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async function persistStateAndConfig(inputs) {
|
|
1173
|
+
if (!(await writeMigratedStates({
|
|
1174
|
+
deps: inputs.deps,
|
|
1175
|
+
report: inputs.report,
|
|
1176
|
+
target: inputs.target
|
|
1177
|
+
})).success) return {
|
|
1178
|
+
err: void 0,
|
|
1179
|
+
success: false
|
|
1180
|
+
};
|
|
1181
|
+
return (await writeBedrockConfig(inputs)).success ? {
|
|
1182
|
+
data: void 0,
|
|
1183
|
+
success: true
|
|
1184
|
+
} : {
|
|
1185
|
+
err: void 0,
|
|
1186
|
+
success: false
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/cli/commands/resolve-migrate-inputs.ts
|
|
1191
|
+
/**
|
|
1192
|
+
* Resolve the path to the input Mantle state file. The positional CLI
|
|
1193
|
+
* argument wins; when it is absent the user is asked through the prompt
|
|
1194
|
+
* port. Cancelling the prompt surfaces as `Err("cancelled")`.
|
|
1195
|
+
*
|
|
1196
|
+
* @param pathArgument - The positional `<stateFilePath>` value sade
|
|
1197
|
+
* handed the action callback, or `undefined` when omitted.
|
|
1198
|
+
* @param promptPort - The migrate prompt port whose `promptStateFilePath`
|
|
1199
|
+
* is used as the interactive fallback.
|
|
1200
|
+
* @returns `Ok(path)` on success, or `Err("cancelled")` if the user
|
|
1201
|
+
* aborted the prompt.
|
|
1202
|
+
*/
|
|
1203
|
+
async function resolveStateFilePath(pathArgument, promptPort) {
|
|
1204
|
+
if (pathArgument !== void 0) return {
|
|
1205
|
+
data: pathArgument,
|
|
1206
|
+
success: true
|
|
1207
|
+
};
|
|
1208
|
+
const promptResult = await promptPort.promptStateFilePath();
|
|
1209
|
+
if (!promptResult.success) return {
|
|
1210
|
+
err: "cancelled",
|
|
1211
|
+
success: false
|
|
1212
|
+
};
|
|
1213
|
+
return {
|
|
1214
|
+
data: promptResult.data,
|
|
1215
|
+
success: true
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Resolve which source format to migrate from. A validated `--from`
|
|
1220
|
+
* value wins; when it is absent the user picks from
|
|
1221
|
+
* {@link SUPPORTED_MIGRATION_SOURCES} through the prompt port.
|
|
1222
|
+
* Cancelling the prompt surfaces as `Err("cancelled")`.
|
|
1223
|
+
*
|
|
1224
|
+
* @param from - The validated `--from` value, or `undefined` when the
|
|
1225
|
+
* flag was omitted.
|
|
1226
|
+
* @param promptPort - The migrate prompt port whose
|
|
1227
|
+
* `promptMigrationSource` is used as the interactive fallback.
|
|
1228
|
+
* @returns `Ok(source)` on success, or `Err("cancelled")` if the user
|
|
1229
|
+
* aborted the prompt.
|
|
1230
|
+
*/
|
|
1231
|
+
async function resolveMigrationSource(from, promptPort) {
|
|
1232
|
+
if (from !== void 0) return {
|
|
1233
|
+
data: from,
|
|
1234
|
+
success: true
|
|
1235
|
+
};
|
|
1236
|
+
const promptResult = await promptPort.promptMigrationSource(SUPPORTED_MIGRATION_SOURCES);
|
|
1237
|
+
if (!promptResult.success) return {
|
|
1238
|
+
err: "cancelled",
|
|
1239
|
+
success: false
|
|
1240
|
+
};
|
|
1241
|
+
return {
|
|
1242
|
+
data: promptResult.data,
|
|
1243
|
+
success: true
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
//#endregion
|
|
1247
|
+
//#region src/cli/commands/migrate.ts
|
|
1248
|
+
const FAILED_OUTRO = "migrate failed";
|
|
1249
|
+
const CANCELLED_OUTRO = "migrate cancelled";
|
|
1250
|
+
/**
|
|
1251
|
+
* Build the sade action for `bedrock migrate`. The returned function
|
|
1252
|
+
* consumes the optional positional path argument and the raw options
|
|
1253
|
+
* object sade hands the action callback. The command parses `--from`,
|
|
1254
|
+
* resolves the state file path (positional or interactive), prompts for
|
|
1255
|
+
* the output config format, runs the migrator, prompts for the state
|
|
1256
|
+
* backend coordinates, writes the per-environment states through the
|
|
1257
|
+
* configured `StatePort`, and emits an enriched bedrock config to disk.
|
|
1258
|
+
*
|
|
1259
|
+
* @param deps - Dependency overrides; missing slots are default-constructed
|
|
1260
|
+
* from real implementations.
|
|
1261
|
+
* @returns An async sade action that returns once `deps.exit` was invoked.
|
|
1262
|
+
*/
|
|
1263
|
+
function migrateCommand(deps) {
|
|
1264
|
+
const resolved = resolveMigrate(deps);
|
|
1265
|
+
return async (pathArgument, rawOptions) => {
|
|
1266
|
+
const code = await runMigrate({
|
|
1267
|
+
pathArg: pathArgument,
|
|
1268
|
+
rawOptions,
|
|
1269
|
+
resolved
|
|
1270
|
+
});
|
|
1271
|
+
resolved.exit(code);
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function resolveMigrate(deps) {
|
|
1275
|
+
return {
|
|
1276
|
+
buildStatePort: deps.buildStatePort ?? buildStatePort,
|
|
1277
|
+
clack: deps.clack ?? createClackPort(),
|
|
1278
|
+
exit: deps.exit ?? ((code) => process.exit(code)),
|
|
1279
|
+
migrateMantleState: deps.migrateMantleState ?? migrateMantleState,
|
|
1280
|
+
mkdir: deps.mkdir ?? (async (path) => void await mkdir(path, { recursive: true })),
|
|
1281
|
+
promptPort: deps.migratePromptPort ?? createDefaultMigratePromptPort(),
|
|
1282
|
+
writeFile: deps.writeFile ?? (async (path, contents) => writeFile(path, contents, "utf8"))
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function cancel$1(resolved) {
|
|
1286
|
+
resolved.clack.cancel(CANCELLED_OUTRO);
|
|
1287
|
+
return 1;
|
|
1288
|
+
}
|
|
1289
|
+
function failAfterRender(resolved) {
|
|
1290
|
+
resolved.clack.cancel(FAILED_OUTRO);
|
|
1291
|
+
return 1;
|
|
1292
|
+
}
|
|
1293
|
+
function renderedFailure(err, resolved) {
|
|
1294
|
+
renderMigrateError(err, resolved.clack);
|
|
1295
|
+
resolved.clack.cancel(FAILED_OUTRO);
|
|
1296
|
+
return {
|
|
1297
|
+
err: "rendered",
|
|
1298
|
+
success: false
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
async function callMigrator(inputs) {
|
|
1302
|
+
const callDeps = {
|
|
1303
|
+
configFormat: inputs.configFormat,
|
|
1304
|
+
stateFilePath: inputs.stateFilePath,
|
|
1305
|
+
...inputs.primaryEnvironment === void 0 ? {} : { primaryEnvironment: inputs.primaryEnvironment }
|
|
1306
|
+
};
|
|
1307
|
+
try {
|
|
1308
|
+
return await inputs.resolved.migrateMantleState(callDeps);
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
return {
|
|
1311
|
+
err: {
|
|
1312
|
+
cause: err,
|
|
1313
|
+
kind: "ioError",
|
|
1314
|
+
path: inputs.stateFilePath
|
|
1315
|
+
},
|
|
1316
|
+
success: false
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function renderIoFailure(err, resolved) {
|
|
1321
|
+
resolved.clack.logError(`failed to read Mantle state file '${err.path}': ${describeUnknown(err.cause)}`);
|
|
1322
|
+
resolved.clack.cancel(FAILED_OUTRO);
|
|
1323
|
+
return {
|
|
1324
|
+
err: "rendered",
|
|
1325
|
+
success: false
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
async function runMigratorWithPrompt(inputs) {
|
|
1329
|
+
const first = await callMigrator(inputs);
|
|
1330
|
+
if (first.success) return {
|
|
1331
|
+
data: first.data,
|
|
1332
|
+
success: true
|
|
1333
|
+
};
|
|
1334
|
+
if (first.err.kind === "ioError") return renderIoFailure(first.err, inputs.resolved);
|
|
1335
|
+
if (first.err.kind !== "primaryEnvironmentRequired") return renderedFailure(first.err, inputs.resolved);
|
|
1336
|
+
const primary = await inputs.resolved.promptPort.promptPrimaryEnvironment(first.err.available);
|
|
1337
|
+
if (!primary.success) return {
|
|
1338
|
+
err: "cancelled",
|
|
1339
|
+
success: false
|
|
1340
|
+
};
|
|
1341
|
+
const second = await callMigrator({
|
|
1342
|
+
...inputs,
|
|
1343
|
+
primaryEnvironment: primary.data
|
|
1344
|
+
});
|
|
1345
|
+
if (second.success) return {
|
|
1346
|
+
data: second.data,
|
|
1347
|
+
success: true
|
|
1348
|
+
};
|
|
1349
|
+
if (second.err.kind === "ioError") return renderIoFailure(second.err, inputs.resolved);
|
|
1350
|
+
return renderedFailure(second.err, inputs.resolved);
|
|
1351
|
+
}
|
|
1352
|
+
async function finalize(inputs) {
|
|
1353
|
+
const persisted = await persistMigration(inputs);
|
|
1354
|
+
if (!persisted.success) {
|
|
1355
|
+
inputs.deps.clack.cancel(FAILED_OUTRO);
|
|
1356
|
+
return 1;
|
|
1357
|
+
}
|
|
1358
|
+
renderMigrationSummary({
|
|
1359
|
+
reportPath: persisted.data,
|
|
1360
|
+
summary: inputs.report.summary
|
|
1361
|
+
}, inputs.deps.clack);
|
|
1362
|
+
inputs.deps.clack.outro("migrate succeeded");
|
|
1363
|
+
return 0;
|
|
1364
|
+
}
|
|
1365
|
+
function configFileFor(stateFilePath, format) {
|
|
1366
|
+
const extension = format === "typescript" ? "ts" : "yaml";
|
|
1367
|
+
return join(dirname(stateFilePath), `bedrock.config.${extension}`);
|
|
1368
|
+
}
|
|
1369
|
+
async function promptForStateTarget(resolved, stateFilePath) {
|
|
1370
|
+
const backend = await resolved.promptPort.promptStateBackend();
|
|
1371
|
+
if (!backend.success) return {
|
|
1372
|
+
err: "cancelled",
|
|
1373
|
+
success: false
|
|
1374
|
+
};
|
|
1375
|
+
if (backend.data === "local") return {
|
|
1376
|
+
data: {
|
|
1377
|
+
backend: "local",
|
|
1378
|
+
outputDir: join(dirname(stateFilePath), ".bedrock", "state")
|
|
1379
|
+
},
|
|
1380
|
+
success: true
|
|
1381
|
+
};
|
|
1382
|
+
const gistId = await resolved.promptPort.promptGistId();
|
|
1383
|
+
if (!gistId.success) return {
|
|
1384
|
+
err: "cancelled",
|
|
1385
|
+
success: false
|
|
1386
|
+
};
|
|
1387
|
+
return {
|
|
1388
|
+
data: {
|
|
1389
|
+
backend: "gist",
|
|
1390
|
+
stateConfig: {
|
|
1391
|
+
backend: "gist",
|
|
1392
|
+
gistId: gistId.data
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
success: true
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function finalizeDeps(resolved) {
|
|
1399
|
+
return {
|
|
1400
|
+
buildStatePort: resolved.buildStatePort,
|
|
1401
|
+
clack: resolved.clack,
|
|
1402
|
+
mkdir: resolved.mkdir,
|
|
1403
|
+
writeFile: resolved.writeFile
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
async function runWithStateFilePath(stateFilePath, resolved) {
|
|
1407
|
+
const formatResult = await resolved.promptPort.promptConfigFormat();
|
|
1408
|
+
if (!formatResult.success) return cancel$1(resolved);
|
|
1409
|
+
const reportResult = await runMigratorWithPrompt({
|
|
1410
|
+
configFormat: formatResult.data,
|
|
1411
|
+
resolved,
|
|
1412
|
+
stateFilePath
|
|
1413
|
+
});
|
|
1414
|
+
if (!reportResult.success) return reportResult.err === "cancelled" ? cancel$1(resolved) : 1;
|
|
1415
|
+
const targetResult = await promptForStateTarget(resolved, stateFilePath);
|
|
1416
|
+
if (!targetResult.success) return cancel$1(resolved);
|
|
1417
|
+
return finalize({
|
|
1418
|
+
configFilePath: configFileFor(stateFilePath, formatResult.data),
|
|
1419
|
+
configFormat: formatResult.data,
|
|
1420
|
+
deps: finalizeDeps(resolved),
|
|
1421
|
+
report: reportResult.data,
|
|
1422
|
+
stateFilePath,
|
|
1423
|
+
target: targetResult.data
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
async function dispatchBySource(inputs) {
|
|
1427
|
+
const { resolved, source, stateFilePath } = inputs;
|
|
1428
|
+
const handler = { mantle: async () => runWithStateFilePath(stateFilePath, resolved) }[source];
|
|
1429
|
+
return handler();
|
|
1430
|
+
}
|
|
1431
|
+
async function runMigrate(inputs) {
|
|
1432
|
+
const { pathArg, rawOptions, resolved } = inputs;
|
|
1433
|
+
resolved.clack.intro("bedrock migrate");
|
|
1434
|
+
const parsed = parseMigrateOptions(rawOptions);
|
|
1435
|
+
if (!parsed.success) {
|
|
1436
|
+
renderMigrateParseError(parsed.err, resolved.clack);
|
|
1437
|
+
return failAfterRender(resolved);
|
|
1438
|
+
}
|
|
1439
|
+
const source = await resolveMigrationSource(parsed.data.from, resolved.promptPort);
|
|
1440
|
+
if (!source.success) return cancel$1(resolved);
|
|
1441
|
+
const stateFilePath = await resolveStateFilePath(pathArg, resolved.promptPort);
|
|
1442
|
+
if (!stateFilePath.success) return cancel$1(resolved);
|
|
1443
|
+
return dispatchBySource({
|
|
1444
|
+
resolved,
|
|
1445
|
+
source: source.data,
|
|
1446
|
+
stateFilePath: stateFilePath.data
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
//#endregion
|
|
1450
|
+
//#region src/cli/index.ts
|
|
1451
|
+
const PROGRAM_NAME = "bedrock";
|
|
1452
|
+
const PROGRAM_DESCRIBE = "Infrastructure-as-Code deployment tool for Roblox";
|
|
1453
|
+
/**
|
|
1454
|
+
* Construct the bedrock CLI program. Pure factory: no `process.argv` parsing,
|
|
1455
|
+
* no clack output, no exits. Callers (the `run.ts` shim, integration tests)
|
|
1456
|
+
* call `.parse()` on the returned sade instance.
|
|
1457
|
+
* @param deps - Dependency overrides for command actions. Each command
|
|
1458
|
+
* resolves its own defaults from any omitted slots.
|
|
1459
|
+
* @returns A configured sade program with the bedrock name, description, and
|
|
1460
|
+
* the currently installed `@bedrock-rbx/core` version, plus the registered
|
|
1461
|
+
* `deploy`, `diff`, and `migrate` commands.
|
|
1462
|
+
*/
|
|
1463
|
+
function createProg(deps = {}) {
|
|
1464
|
+
const prog = sade(PROGRAM_NAME).describe(PROGRAM_DESCRIBE).version(version);
|
|
1465
|
+
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 ROBLOX_API_KEY environment variable").option("--github-token", "Override the GITHUB_TOKEN environment variable").action(deployCommand(deps));
|
|
1466
|
+
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 ROBLOX_API_KEY environment variable").option("--github-token", "Override the GITHUB_TOKEN environment variable").action(diffCommand(deps));
|
|
1467
|
+
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));
|
|
1468
|
+
return prog;
|
|
1469
|
+
}
|
|
1470
|
+
//#endregion
|
|
1471
|
+
//#region src/cli/run.ts
|
|
1472
|
+
createProg().parse(process.argv);
|
|
1473
|
+
//#endregion
|
|
1474
|
+
export {};
|
|
1475
|
+
|
|
1476
|
+
//# sourceMappingURL=run.mjs.map
|