@fluid-app/fluid-cli-mist 0.1.47 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Command, InvalidArgumentError, Option } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { getAuthToken, getConfigDir, gitSyncActorFromMe, gitSyncCommitSubject, parseNameStatusZ, resolveGitSyncActor, summarizeChanges } from "@fluid-app/fluid-cli";
4
+ import { randomUUID } from "node:crypto";
4
5
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
6
  import path, { join, resolve } from "node:path";
6
7
  import os from "node:os";
7
8
  import { execa } from "execa";
8
9
  import ora from "ora";
9
- import { randomUUID } from "node:crypto";
10
10
  import prompts from "prompts";
11
11
  import open from "open";
12
12
  //#region src/api/client.ts
@@ -166,6 +166,181 @@ async function restoreMist(client, slug) {
166
166
  return client.post(`${BASE$4}/${id}/restore`);
167
167
  }
168
168
  //#endregion
169
+ //#region src/api/branch_environments.ts
170
+ /**
171
+ * Branch environments and branch operations for a Mist project.
172
+ *
173
+ * Server contracts (Rails, internal API v2026-04):
174
+ * apps/rails/docs/openapi/internal/mist-branch-environments-v2026-04.yaml
175
+ * apps/rails/docs/openapi/internal/mist-branch-operations-v2026-04.yaml
176
+ *
177
+ * Every response is checked at runtime before a caller sees it. A server
178
+ * whose listing has no `capability` block predates branch environments and
179
+ * reads as unsupported. A response that does not match the contract throws
180
+ * `BranchContractError`; neither case is ever a reason to fall back to a
181
+ * production publish.
182
+ */
183
+ const ENVIRONMENT_STATES = [
184
+ "provisioning",
185
+ "ready",
186
+ "error",
187
+ "expired",
188
+ "deleting"
189
+ ];
190
+ const OPERATION_KINDS = [
191
+ "ensure_staging",
192
+ "create",
193
+ "delete",
194
+ "promote",
195
+ "publish"
196
+ ];
197
+ const OPERATION_STATES = [
198
+ "queued",
199
+ "running",
200
+ "awaiting_content",
201
+ "awaiting_resolution",
202
+ "retryable_failure",
203
+ "succeeded",
204
+ "failed",
205
+ "cancelled",
206
+ "stale"
207
+ ];
208
+ /** States after which the server will not advance the operation again. */
209
+ const TERMINAL_OPERATION_STATES = [
210
+ "succeeded",
211
+ "failed",
212
+ "cancelled",
213
+ "stale"
214
+ ];
215
+ /** A server response that does not match the branch environments contract. */
216
+ var BranchContractError = class extends Error {
217
+ constructor(message) {
218
+ super(`Unexpected branch environments response: ${message}`);
219
+ this.name = "BranchContractError";
220
+ }
221
+ };
222
+ const SOURCE_REVISION = /^[0-9a-f]{40}$/;
223
+ const OPERATION_KEY = /^[A-Za-z0-9._:-]{1,128}$/;
224
+ function base(mistId) {
225
+ return `/api/v202604/mists/${mistId}`;
226
+ }
227
+ /** List the project's branch environments and what the server offers. */
228
+ async function listBranchEnvironments(client, slug) {
229
+ const id = await resolveMistId(client, slug);
230
+ return parseListing(await client.get(`${base(id)}/branch-environments`));
231
+ }
232
+ /**
233
+ * Request the staging branch at `sourceRevision`. Reusing `operationKey`
234
+ * with the same revision returns the same operation, so a caller persists
235
+ * the key before calling and retries with it.
236
+ */
237
+ async function requestStaging(client, slug, request) {
238
+ if (!SOURCE_REVISION.test(request.sourceRevision)) throw new Error("Source revision must be a full 40-character commit SHA");
239
+ if (!OPERATION_KEY.test(request.operationKey)) throw new Error("Operation key must be 1-128 of A-Z a-z 0-9 . _ : -");
240
+ const id = await resolveMistId(client, slug);
241
+ return parseOperationEnvelope(await client.post(`${base(id)}/branch-environments/staging`, { source_revision: request.sourceRevision }, { "X-Operation-Key": request.operationKey }));
242
+ }
243
+ /** Read one branch operation of the project. Never advances it. */
244
+ async function fetchBranchOperation(client, slug, operationId) {
245
+ const id = await resolveMistId(client, slug);
246
+ return parseOperationEnvelope(await client.get(`${base(id)}/branch-operations/${encodeURIComponent(operationId)}`));
247
+ }
248
+ /** A fresh key for a new staging request. */
249
+ function newOperationKey() {
250
+ return `staging-${randomUUID()}`;
251
+ }
252
+ function isTerminal(operation) {
253
+ return TERMINAL_OPERATION_STATES.includes(operation.state);
254
+ }
255
+ function parseListing(body) {
256
+ const envelope = record(body, "response");
257
+ return {
258
+ environments: array(envelope["environments"], "environments").map((item, index) => parseEnvironment(item, `environments[${index}]`)),
259
+ capability: parseCapability(envelope["capability"])
260
+ };
261
+ }
262
+ function parseCapability(raw) {
263
+ if (raw === void 0) return { supported: false };
264
+ const capability = record(raw, "capability");
265
+ const version = capability["version"];
266
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) throw new BranchContractError("capability.version is not a positive integer");
267
+ const staging = record(capability["staging"], "capability.staging");
268
+ return {
269
+ supported: true,
270
+ version,
271
+ staging: {
272
+ available: boolean(staging["available"], "capability.staging.available"),
273
+ reason: nullableString(staging["reason"], "capability.staging.reason"),
274
+ active_operation_id: nullableString(staging["active_operation_id"], "capability.staging.active_operation_id")
275
+ }
276
+ };
277
+ }
278
+ function parseEnvironment(raw, path) {
279
+ const environment = record(raw, path);
280
+ const url = nullableString(environment["url"], `${path}.url`);
281
+ if (url !== null && !url.startsWith("https://")) throw new BranchContractError(`${path}.url is not an https address`);
282
+ return {
283
+ id: string(environment["id"], `${path}.id`),
284
+ slug: string(environment["slug"], `${path}.slug`),
285
+ state: member(environment["state"], ENVIRONMENT_STATES, `${path}.state`),
286
+ seeded: boolean(environment["seeded"], `${path}.seeded`),
287
+ url,
288
+ created_at: string(environment["created_at"], `${path}.created_at`),
289
+ updated_at: string(environment["updated_at"], `${path}.updated_at`)
290
+ };
291
+ }
292
+ function parseOperationEnvelope(body) {
293
+ return parseOperation(record(body, "response")["operation"]);
294
+ }
295
+ function parseOperation(raw) {
296
+ const operation = record(raw, "operation");
297
+ const progress = record(operation["progress"], "operation.progress");
298
+ const sourceRevision = string(operation["source_revision"], "operation.source_revision");
299
+ if (!SOURCE_REVISION.test(sourceRevision)) throw new BranchContractError("operation.source_revision is not a commit SHA");
300
+ return {
301
+ id: string(operation["id"], "operation.id"),
302
+ kind: member(operation["kind"], OPERATION_KINDS, "operation.kind"),
303
+ state: member(operation["state"], OPERATION_STATES, "operation.state"),
304
+ source_revision: sourceRevision,
305
+ branch_id: nullableString(operation["branch_id"], "operation.branch_id"),
306
+ progress: {
307
+ database_isolated: boolean(progress["database_isolated"], "operation.progress.database_isolated"),
308
+ branch_published: boolean(progress["branch_published"], "operation.progress.branch_published"),
309
+ build_ready: boolean(progress["build_ready"], "operation.progress.build_ready")
310
+ },
311
+ failure: parseFailure(operation["failure"]),
312
+ created_at: string(operation["created_at"], "operation.created_at"),
313
+ updated_at: string(operation["updated_at"], "operation.updated_at")
314
+ };
315
+ }
316
+ function parseFailure(raw) {
317
+ if (raw === null) return null;
318
+ return { type: string(record(raw, "operation.failure")["type"], "operation.failure.type") };
319
+ }
320
+ function record(raw, path) {
321
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new BranchContractError(`${path} is not an object`);
322
+ return raw;
323
+ }
324
+ function array(raw, path) {
325
+ if (!Array.isArray(raw)) throw new BranchContractError(`${path} is not a list`);
326
+ return raw;
327
+ }
328
+ function string(raw, path) {
329
+ if (typeof raw !== "string") throw new BranchContractError(`${path} is not a string`);
330
+ return raw;
331
+ }
332
+ function nullableString(raw, path) {
333
+ return raw === null ? null : string(raw, path);
334
+ }
335
+ function boolean(raw, path) {
336
+ if (typeof raw !== "boolean") throw new BranchContractError(`${path} is not a boolean`);
337
+ return raw;
338
+ }
339
+ function member(raw, allowed, path) {
340
+ if (typeof raw !== "string" || !allowed.includes(raw)) throw new BranchContractError(`${path} has an unknown value`);
341
+ return raw;
342
+ }
343
+ //#endregion
169
344
  //#region src/lib/hostable.ts
170
345
  const SHORTHAND = {
171
346
  droplet: "Droplet::Application",
@@ -830,6 +1005,133 @@ function readMistFileFrom(dir = process.cwd()) {
830
1005
  return readMistFile(dir);
831
1006
  }
832
1007
  //#endregion
1008
+ //#region src/lib/staging.ts
1009
+ /** A staging step the user can act on, as opposed to a server fault. */
1010
+ var StagingUnavailableError = class extends Error {
1011
+ reason;
1012
+ activeOperationId;
1013
+ constructor(reason, message, activeOperationId = null) {
1014
+ super(message);
1015
+ this.name = "StagingUnavailableError";
1016
+ this.reason = reason;
1017
+ this.activeOperationId = activeOperationId;
1018
+ }
1019
+ };
1020
+ /** The operation stopped without succeeding. */
1021
+ var StagingOperationFailedError = class extends Error {
1022
+ operation;
1023
+ constructor(operation) {
1024
+ const cause = operation.failure ? ` (${operation.failure.type})` : "";
1025
+ super(`Staging ${operation.state}${cause}. Operation ${operation.id}.`);
1026
+ this.name = "StagingOperationFailedError";
1027
+ this.operation = operation;
1028
+ }
1029
+ };
1030
+ /** The local wait ran out; the server keeps working on the operation. */
1031
+ var StagingWaitTimeoutError = class extends Error {
1032
+ operation;
1033
+ constructor(operation) {
1034
+ super(`Still ${operation.state} after the wait limit. The server keeps working on it; follow it with \`fluid mist staging wait --operation ${operation.id}\`.`);
1035
+ this.name = "StagingWaitTimeoutError";
1036
+ this.operation = operation;
1037
+ }
1038
+ };
1039
+ const UNSUPPORTED = "This Fluid server doesn't support branch environments yet, so there is no staging to use. Nothing was published.";
1040
+ /** Why staging can't be requested now, in words a user can act on. */
1041
+ function describeUnavailable(staging) {
1042
+ switch (staging.reason) {
1043
+ case "feature_disabled": return "Branch environments aren't enabled for this company.";
1044
+ case "execution_unavailable": return "Staging isn't running on the server right now. Try again later.";
1045
+ case "hosting_unavailable": return "This mist isn't live, so it can't have a staging environment.";
1046
+ case "credentials_not_retired": return "This mist's production credentials still reach preview builds. A Fluid operator has to retire them before staging can be used.";
1047
+ case "operation_in_progress": return `Staging is already being prepared (operation ${staging.active_operation_id ?? "unknown"}). Follow it with \`fluid mist staging wait\`.`;
1048
+ case "environment_deleting": return "Staging is being torn down. Request it again once it's removed.";
1049
+ case "staging_exists": return "Staging already exists. It can't be requested again until it's torn down.";
1050
+ case "quota_exceeded": return "This company has reached its branch environment limit.";
1051
+ default: return `Staging isn't available right now (${staging.reason ?? "no reason given"}).`;
1052
+ }
1053
+ }
1054
+ /** Where staging stands: capability, the staging environment and any run. */
1055
+ async function readStagingStatus(client, slug) {
1056
+ const listing = await listBranchEnvironments(client, slug);
1057
+ if (!listing.capability.supported) throw new StagingUnavailableError("unsupported", UNSUPPORTED);
1058
+ const { staging } = listing.capability;
1059
+ const activeOperation = staging.active_operation_id ? await fetchBranchOperation(client, slug, staging.active_operation_id) : null;
1060
+ return {
1061
+ capability: {
1062
+ version: listing.capability.version,
1063
+ staging
1064
+ },
1065
+ environment: listing.environments.find((env) => env.seeded) ?? null,
1066
+ activeOperation
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Request staging at a revision. A key the caller passes is retried even
1071
+ * while the capability reports a run in progress, because that run may be
1072
+ * the one this key started. Throws `StagingOperationFailedError` when the
1073
+ * key returns an operation that already failed, was cancelled or went stale.
1074
+ */
1075
+ async function admitStaging(client, slug, request) {
1076
+ if (!request.operationKey) {
1077
+ const listing = await listBranchEnvironments(client, slug);
1078
+ if (!listing.capability.supported) throw new StagingUnavailableError("unsupported", UNSUPPORTED);
1079
+ const { staging } = listing.capability;
1080
+ if (!staging.available) throw new StagingUnavailableError(staging.reason ?? "unavailable", describeUnavailable(staging), staging.active_operation_id);
1081
+ }
1082
+ const operationKey = request.operationKey ?? newOperationKey();
1083
+ request.onOperationKey?.(operationKey);
1084
+ const operation = await requestStaging(client, slug, {
1085
+ sourceRevision: request.sourceRevision,
1086
+ operationKey
1087
+ });
1088
+ if (isTerminal(operation) && operation.state !== "succeeded") throw new StagingOperationFailedError(operation);
1089
+ return operation;
1090
+ }
1091
+ /**
1092
+ * Poll the staging operation until it finishes. Resolves on success;
1093
+ * throws `StagingUnavailableError` for an operation that isn't a staging
1094
+ * request, `StagingOperationFailedError` on any other terminal state and
1095
+ * `StagingWaitTimeoutError` when the local wait runs out, which never
1096
+ * cancels the server operation.
1097
+ */
1098
+ async function waitForOperation(client, slug, operationId, options) {
1099
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1100
+ const now = options.now ?? Date.now;
1101
+ const deadline = now() + options.timeoutMs;
1102
+ let lastSeen = "";
1103
+ for (;;) {
1104
+ const operation = await fetchBranchOperation(client, slug, operationId);
1105
+ if (operation.kind !== "ensure_staging") throw new StagingUnavailableError("not_a_staging_operation", `Operation ${operation.id} is a ${operation.kind} operation, not a staging request. Follow staging with \`fluid mist staging wait\`.`);
1106
+ const seen = JSON.stringify([
1107
+ operation.state,
1108
+ operation.progress,
1109
+ operation.failure
1110
+ ]);
1111
+ if (seen !== lastSeen) {
1112
+ lastSeen = seen;
1113
+ options.onChange?.(operation);
1114
+ }
1115
+ if (isTerminal(operation)) {
1116
+ if (operation.state === "succeeded") return operation;
1117
+ throw new StagingOperationFailedError(operation);
1118
+ }
1119
+ if (now() >= deadline) throw new StagingWaitTimeoutError(operation);
1120
+ await sleep(options.intervalMs);
1121
+ }
1122
+ }
1123
+ /** One line describing an operation's progress, for stderr. */
1124
+ function describeProgress(operation) {
1125
+ const { progress } = operation;
1126
+ const steps = [
1127
+ `database ${progress.database_isolated ? "isolated" : "pending"}`,
1128
+ `branch ${progress.branch_published ? "published" : "pending"}`,
1129
+ `build ${progress.build_ready ? "ready" : "pending"}`
1130
+ ].join(", ");
1131
+ const failure = operation.failure ? `, last error ${operation.failure.type}` : "";
1132
+ return `${operation.state}: ${steps}${failure}`;
1133
+ }
1134
+ //#endregion
833
1135
  //#region src/lib/error-handler.ts
834
1136
  /**
835
1137
  * Centralized error handling — maps thrown errors to spec'd exit codes
@@ -856,6 +1158,14 @@ function handle(err) {
856
1158
  console.error(chalk.red(err.message));
857
1159
  process.exit(1);
858
1160
  }
1161
+ if (err instanceof StagingUnavailableError || err instanceof StagingWaitTimeoutError) {
1162
+ console.error(chalk.yellow(err.message));
1163
+ process.exit(1);
1164
+ }
1165
+ if (err instanceof StagingOperationFailedError || err instanceof BranchContractError) {
1166
+ console.error(chalk.red(err.message));
1167
+ process.exit(3);
1168
+ }
859
1169
  if (err instanceof MistApiError) {
860
1170
  if (err.status === 403) {
861
1171
  const action = err.details?.["permission"];
@@ -3838,6 +4148,70 @@ function printSummary(summary, options = {}) {
3838
4148
  /** `fluid mist droplet` — sub-command aggregator for the attached Droplet. */
3839
4149
  const dropletCommand = new Command("droplet").description("Repair, reinstall, or set egress for the Mist's attached Droplet").addCommand(dropletRepairCommand).addCommand(dropletReinstallCommand).addCommand(dropletEgressProxyCommand);
3840
4150
  //#endregion
4151
+ //#region src/commands/staging/status.ts
4152
+ /** `fluid mist staging status` — where staging stands. */
4153
+ const stagingStatusCommand = new Command("status").description("Show the staging environment and whether it can be requested").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--json", "Emit the status as JSON").action((slug, opts) => runAction(async () => {
4154
+ const status = await readStagingStatus(createMistClient(), resolveSlug(slug));
4155
+ if (opts.json) return printJson({
4156
+ capability: status.capability,
4157
+ environment: status.environment,
4158
+ active_operation: status.activeOperation
4159
+ });
4160
+ const { environment, activeOperation, capability } = status;
4161
+ if (environment) {
4162
+ console.log(`Staging: ${chalk.bold(environment.state)}`);
4163
+ if (environment.url) console.log(`URL: ${chalk.cyan(environment.url)}`);
4164
+ } else console.log(chalk.dim("No staging environment yet."));
4165
+ if (activeOperation) console.log(`Operation ${activeOperation.id}: ${describeProgress(activeOperation)}`);
4166
+ console.log(capability.staging.available ? "Staging can be requested with `fluid mist staging request --revision <sha>`." : chalk.yellow(describeUnavailable(capability.staging)));
4167
+ }));
4168
+ //#endregion
4169
+ //#region src/commands/staging/wait.ts
4170
+ /** `fluid mist staging wait` — follow a staging operation to the end. */
4171
+ const POLL_INTERVAL_MS = 5e3;
4172
+ const DEFAULT_TIMEOUT_MINUTES = 30;
4173
+ /** Polling options for a `--timeout <minutes>` flag, progress on stderr. */
4174
+ function waitOptions(timeout) {
4175
+ const minutes = timeout ? Number(timeout) : DEFAULT_TIMEOUT_MINUTES;
4176
+ if (!Number.isFinite(minutes) || minutes <= 0) throw new Error("--timeout must be a positive number of minutes");
4177
+ return {
4178
+ intervalMs: POLL_INTERVAL_MS,
4179
+ timeoutMs: minutes * 6e4,
4180
+ onChange: (operation) => process.stderr.write(`${describeProgress(operation)}\n`)
4181
+ };
4182
+ }
4183
+ const stagingWaitCommand = new Command("wait").description("Follow a staging operation until it finishes").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--operation <id>", "Operation to follow (defaults to the one preparing staging)").option("--timeout <minutes>", "How long to follow it (default 30)").option("--json", "Emit the finished operation as JSON").action((slug, opts) => runAction(async () => {
4184
+ const resolved = resolveSlug(slug);
4185
+ const client = createMistClient();
4186
+ const options = waitOptions(opts.timeout);
4187
+ const operationId = opts.operation ?? (await readStagingStatus(client, resolved)).activeOperation?.id;
4188
+ if (!operationId) throw new Error("No staging operation is running. Check `fluid mist staging status`.");
4189
+ const operation = await waitForOperation(client, resolved, operationId, options);
4190
+ if (opts.json) return printJson({ operation });
4191
+ console.log("✔ Staging is ready. Run `fluid mist staging status` for its URL.");
4192
+ }));
4193
+ //#endregion
4194
+ //#region src/commands/staging/request.ts
4195
+ /** `fluid mist staging request` — request staging at a revision. */
4196
+ const stagingRequestCommand = new Command("request").description("Request the staging environment at a pushed commit").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").requiredOption("--revision <sha>", "Full 40-character SHA of a commit already on the mist's repository").option("--operation-key <key>", "Resume an interrupted request with the key it printed").option("--wait", "Follow the operation until staging is ready").option("--timeout <minutes>", "How long --wait follows it (default 30)").option("--json", "Emit the operation as JSON").action((slug, opts) => runAction(async () => {
4197
+ const resolved = resolveSlug(slug);
4198
+ const client = createMistClient();
4199
+ let operation = await admitStaging(client, resolved, {
4200
+ sourceRevision: opts.revision,
4201
+ operationKey: opts.operationKey,
4202
+ onOperationKey: (key) => process.stderr.write(chalk.dim(`Operation key ${key}. If this is interrupted, rerun with --operation-key ${key}.\n`))
4203
+ });
4204
+ process.stderr.write(`Staging operation ${operation.id}: ${describeProgress(operation)}\n`);
4205
+ if (opts.wait) operation = await waitForOperation(client, resolved, operation.id, waitOptions(opts.timeout));
4206
+ if (opts.json) return printJson({ operation });
4207
+ if (operation.state === "succeeded") console.log(`✔ Staging is ready. Run \`fluid mist staging status\` for its URL.`);
4208
+ else console.log(`Requested. Follow it with \`fluid mist staging wait --operation ${operation.id}\`.`);
4209
+ }));
4210
+ //#endregion
4211
+ //#region src/commands/staging/index.ts
4212
+ /** `fluid mist staging` — sub-command aggregator. */
4213
+ const stagingCommand = new Command("staging").description("Use a mist's isolated staging environment").addCommand(stagingStatusCommand).addCommand(stagingRequestCommand).addCommand(stagingWaitCommand);
4214
+ //#endregion
3841
4215
  //#region src/index.ts
3842
4216
  /**
3843
4217
  * @fluid-app/fluid-cli-mist
@@ -3869,6 +4243,7 @@ const plugin = {
3869
4243
  mist.addCommand(logsCommand);
3870
4244
  mist.addCommand(envCommand);
3871
4245
  mist.addCommand(dropletCommand);
4246
+ mist.addCommand(stagingCommand);
3872
4247
  ctx.program.addCommand(mist);
3873
4248
  }
3874
4249
  };