@tutti-os/nexight-devops 0.6.0 → 0.6.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/README.md CHANGED
@@ -48,6 +48,10 @@ nexight-devops release hotfix
48
48
  nexight-devops release approve
49
49
  nexight-devops release rollback
50
50
  nexight-devops release rollback tutti-server
51
+ nexight-devops release rollback platform-web
52
+ nexight-devops release rollback platform-web --previous --yes --reason "entry failure"
53
+ nexight-devops release rollback platform-web --version-id <uuid> --yes
54
+ nexight-devops release rollback platform-web --probe
51
55
  ```
52
56
 
53
57
  ### Function overview
@@ -66,7 +70,8 @@ nexight-devops release rollback tutti-server
66
70
  - `ppe-lane destroy` checks existing lane instances and triggers destroy workflows after a lane-name confirmation. `tutti-server` must be selected explicitly for destruction.
67
71
  - `ppe-lane status` queries AWS Cloud Map for one lane, or all lanes with `--all`, including `tutti-server`.
68
72
  - `release prepare`, `release hotfix`, and `release approve` run the controlled release lifecycle through GitHub Actions and approval issues.
69
- - `release rollback` lets you choose `tutti-server` interactively; `release rollback tutti-server` selects it directly. The command reads the current production image and up to ten recent Helm deployments directly when local read access to the production cluster is available. Otherwise it retrieves the verified list through GitHub Actions. The selected image is always revalidated and restored by GitHub Actions, followed by health checks. Use `--revision <number> --yes` with the explicit repository for scripted selection and confirmation.
73
+ - `release rollback` lets you choose `tutti-server` or `platform-web` interactively. For `tutti-server`, the command reads the current production image and up to ten recent Helm deployments directly when local cluster read access is available, otherwise through GitHub Actions. The selected image is revalidated and restored by GitHub Actions, followed by health checks. Use `--revision <number> --yes` for scripted selection.
74
+ - `release rollback platform-web` restores a retained, previously deployed Cloudflare Worker version through Tutti's lightweight `platform-web-rollback.yml`. It does not build, test, install packages, upload assets, or roll back ingress/storage. The interactive flow binds your confirmation to the displayed deployment ID; if production changes, execution refuses the stale selection. `--previous --yes` starts only one workflow and selects the latest eligible previous version from live history under the writer lock, excluding versions withdrawn by earlier recorded rollbacks. `--version-id <uuid> --yes` explicitly selects a previously deployed version, including a withdrawn version. `--yes` skips confirmation only. `--reason` records up to 200 UTF-8 bytes; missing historical SHA/run mappings remain unknown. Results distinguish deployment switching, public entry/assets, version/content identity, and unverified Room behavior, and include queue/setup, switch, verification, and total timings. `--probe` measures the same read-only production checks without changing traffic. The CLI needs GitHub access; Cloudflare credentials remain in the production environment's GitHub Secrets.
70
75
 
71
76
  ## Controlled Release
72
77
 
package/dist/cli.js CHANGED
@@ -3142,6 +3142,9 @@ var matchesWorkflowRunCorrelation = (run, correlation) => {
3142
3142
  if (correlation.actorLogin && run.actor?.login !== correlation.actorLogin) {
3143
3143
  return false;
3144
3144
  }
3145
+ if (correlation.runName && run.display_title !== correlation.runName) {
3146
+ return false;
3147
+ }
3145
3148
  if (!isRunCreatedAtOrAfter(run.created_at, correlation.createdAt)) {
3146
3149
  return false;
3147
3150
  }
@@ -4148,9 +4151,9 @@ var registerReleasePrepareCommand = (program) => {
4148
4151
  };
4149
4152
 
4150
4153
  // src/commands/release/rollback.ts
4151
- import { mkdtemp as mkdtemp2, readFile as readFile2, rm } from "fs/promises";
4152
- import { tmpdir } from "os";
4153
- import { join as join5 } from "path";
4154
+ import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm2 } from "fs/promises";
4155
+ import { tmpdir as tmpdir2 } from "os";
4156
+ import { join as join6 } from "path";
4154
4157
 
4155
4158
  // src/constants/rollback-targets.ts
4156
4159
  var tuttiServerRollbackTarget = {
@@ -4158,7 +4161,12 @@ var tuttiServerRollbackTarget = {
4158
4161
  label: "tutti-server",
4159
4162
  repoFullName: "tutti-lab/tutti-server"
4160
4163
  };
4161
- var rollbackTargets = [tuttiServerRollbackTarget];
4164
+ var platformWebRollbackTarget = {
4165
+ key: "platform-web",
4166
+ label: "platform-web",
4167
+ repoFullName: "tutti-lab/tutti"
4168
+ };
4169
+ var rollbackTargets = [tuttiServerRollbackTarget, platformWebRollbackTarget];
4162
4170
 
4163
4171
  // src/commands/release/rollback-local.ts
4164
4172
  var CLUSTER = "arn:aws:eks:us-east-1:250509935467:cluster/nexight-ojo-us-east-1";
@@ -4246,9 +4254,137 @@ var fetchLocalRollbackCatalog = async () => {
4246
4254
  return buildLocalRollbackCatalog(history, deployment, manifests);
4247
4255
  };
4248
4256
 
4257
+ // src/commands/release/rollback-web.ts
4258
+ import { randomUUID as randomUUID3 } from "crypto";
4259
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm } from "fs/promises";
4260
+ import { tmpdir } from "os";
4261
+ import { join as join5 } from "path";
4262
+ var REPO = platformWebRollbackTarget.repoFullName;
4263
+ var WORKFLOW = "platform-web-rollback.yml";
4264
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
4265
+ var SHA = /^[0-9a-f]{40}$/;
4266
+ var choiceTime = new Intl.DateTimeFormat("sv-SE", { timeZone: "Asia/Singapore", dateStyle: "short", timeStyle: "short" });
4267
+ var validDeployment = (value) => UUID.test(value?.id) && Array.isArray(value.versions) && value.versions.length > 0 && value.versions.every((item) => UUID.test(item.version_id) && Number.isFinite(item.percentage) && item.percentage > 0 && item.percentage <= 100) && Math.abs(value.versions.reduce((sum, item) => sum + item.percentage, 0) - 100) < 1e-3;
4268
+ var parseWebRollbackCatalog = (raw) => {
4269
+ const data = JSON.parse(raw);
4270
+ if (data.schema !== "tutti.web.rollback.catalog.v1" || data.environment !== "production" || !validDeployment(data.currentDeployment) || !Array.isArray(data.candidates) || data.candidates.length > 100) {
4271
+ throw new CliError("Invalid Platform Web rollback catalog", 1);
4272
+ }
4273
+ const seen = /* @__PURE__ */ new Set();
4274
+ const active = new Set(data.currentDeployment.versions.map((item) => item.version_id));
4275
+ for (const candidate of data.candidates) {
4276
+ if (!UUID.test(candidate.versionId) || !UUID.test(candidate.deploymentId) || !Number.isFinite(Date.parse(candidate.deployedAt)) || typeof candidate.current !== "boolean" || candidate.current !== active.has(candidate.versionId) || typeof candidate.withdrawn !== "boolean" || typeof candidate.fullTraffic !== "boolean" || !(candidate.sha === null || SHA.test(candidate.sha)) || !(candidate.runId === null || /^\d+$/.test(candidate.runId)) || seen.has(candidate.versionId)) {
4277
+ throw new CliError("Invalid Platform Web deployment in rollback catalog", 1);
4278
+ }
4279
+ seen.add(candidate.versionId);
4280
+ }
4281
+ return data;
4282
+ };
4283
+ var buildWebRollbackChoices = (catalog) => catalog.candidates.filter((candidate) => !candidate.current && !candidate.withdrawn && candidate.fullTraffic).map((candidate) => ({
4284
+ name: `${choiceTime.format(new Date(candidate.deployedAt))} SGT ${candidate.versionId.slice(0, 8)} ${candidate.sha?.slice(0, 12) ?? "SHA unknown"}${candidate.runId ? ` run ${candidate.runId}` : ""}`,
4285
+ value: candidate.versionId
4286
+ }));
4287
+ var gh = async (args) => {
4288
+ const result = await runCommand("gh", args);
4289
+ if (result.exitCode !== 0) throw new CliError(result.stderr.trim() || result.stdout.trim() || "GitHub command failed", 1);
4290
+ return result.stdout;
4291
+ };
4292
+ var dispatch = async (operation, inputs = {}) => {
4293
+ const operationId = randomUUID3();
4294
+ const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
4295
+ const knownRunIds = await listWorkflowRunIds({ repoFullName: REPO, workflowFile: WORKFLOW, ref: "main" });
4296
+ const fields = { operation, operation_id: operationId, requested_at: requestedAt, ...inputs };
4297
+ await gh(["workflow", "run", WORKFLOW, "--repo", REPO, "--ref", "main", ...Object.entries(fields).flatMap(([key, value]) => ["-f", `${key}=${value}`])]);
4298
+ const progress = ui.spinner(operation === "execute" ? "\u7B49\u5F85 Platform Web \u56DE\u6EDA\u548C\u5165\u53E3\u9A8C\u8BC1..." : "\u8BFB\u53D6 Platform Web \u751F\u4EA7\u72B6\u6001...").start();
4299
+ try {
4300
+ const run = await pollWorkflowRun({
4301
+ knownRunIds,
4302
+ repoFullName: REPO,
4303
+ workflowFile: WORKFLOW,
4304
+ ref: "main",
4305
+ intervalMs: 1e3,
4306
+ maxAttempts: 600,
4307
+ correlation: { runName: `Platform Web Rollback / ${operation} / ${operationId}`, createdAt: requestedAt },
4308
+ onUpdate: (update) => {
4309
+ if ("htmlUrl" in update) progress.text = update.htmlUrl;
4310
+ }
4311
+ });
4312
+ const directory = await mkdtemp2(join5(tmpdir(), "platform-web-rollback-"));
4313
+ try {
4314
+ try {
4315
+ await gh(["run", "download", String(run.runId), "--repo", REPO, "--name", "platform-web-rollback", "--dir", directory]);
4316
+ } catch (error) {
4317
+ throw new CliError(`Could not retrieve operation outcome; inspect actual production state at ${run.htmlUrl} before retrying: ${error instanceof Error ? error.message : String(error)}`, 1);
4318
+ }
4319
+ const result = JSON.parse(await readFile2(join5(directory, "rollback-result.json"), "utf8"));
4320
+ if (result.schema !== "tutti.web.rollback.result.v1" || result.operationId !== operationId || !["success", "failure", "pending_verification"].includes(result.status) || !["unchanged", "switched", "unknown"].includes(result.deployment) || !result.verification || !result.timings || Object.values(result.timings).some((value) => !Number.isFinite(value) || value < 0) || result.actual && !validDeployment(result.actual)) {
4321
+ throw new CliError(`Invalid rollback result: ${run.htmlUrl}`, 1);
4322
+ }
4323
+ if (run.conclusion !== "success" || result.status !== "success") {
4324
+ const state = result.actual ? `${result.actual.id} / ${JSON.stringify(result.actual.versions)}` : "unknown";
4325
+ throw new CliError(`Platform Web operation failed; deployment ${result.deployment}
4326
+ Actual production: ${state}
4327
+ ${result.error ?? run.failureMessage ?? run.conclusion}
4328
+ ${run.htmlUrl}`, 1);
4329
+ }
4330
+ if (operation === "execute" && (result.deployment !== "switched" || !result.actual || !UUID.test(result.targetVersionId ?? "") || result.actual.versions.length !== 1 || result.actual.versions[0]?.version_id !== result.targetVersionId || result.actual.versions[0]?.percentage !== 100)) {
4331
+ throw new CliError(`Rollback did not confirm target version at 100%: ${run.htmlUrl}`, 1);
4332
+ }
4333
+ const catalog = operation === "list" ? parseWebRollbackCatalog(await readFile2(join5(directory, "rollback-catalog.json"), "utf8")) : void 0;
4334
+ return { run, result, catalog };
4335
+ } catch (error) {
4336
+ if (error instanceof CliError) throw error;
4337
+ throw new CliError(`Could not read actual operation state; inspect ${run.htmlUrl} before retrying: ${error instanceof Error ? error.message : String(error)}`, 1);
4338
+ } finally {
4339
+ await rm(directory, { recursive: true, force: true });
4340
+ }
4341
+ } finally {
4342
+ progress.stop();
4343
+ }
4344
+ };
4345
+ var runPlatformWebRollback = async (options) => {
4346
+ if (options.previous && options.versionId || options.probe && (options.previous || options.versionId)) throw new CliError("Choose only one of --previous, --version-id, or --probe", 1);
4347
+ if (options.versionId && !UUID.test(options.versionId)) throw new CliError("--version-id must be a Worker version UUID", 1);
4348
+ if (options.reason && Buffer.byteLength(options.reason) > 200) throw new CliError("--reason must be at most 200 UTF-8 bytes", 1);
4349
+ await ensurePrerequisites({ needsGh: true });
4350
+ let inputs = { reason: options.reason ?? "operator rollback" };
4351
+ if (options.probe) {
4352
+ const { run: run2, result: result2 } = await dispatch("probe");
4353
+ logger.info(`Read-only production probe: ${JSON.stringify(result2.timings)}`);
4354
+ logger.info(`Verification: ${JSON.stringify(result2.verification)}`);
4355
+ logger.info(run2.htmlUrl);
4356
+ return;
4357
+ }
4358
+ if (options.yes && (options.previous || options.versionId)) {
4359
+ inputs = { ...inputs, previous: String(Boolean(options.previous)), version_id: options.versionId ?? "" };
4360
+ } else {
4361
+ const { catalog } = await dispatch("list", options.versionId ? { version_id: options.versionId } : {});
4362
+ if (!catalog) throw new CliError("Missing Platform Web rollback catalog", 1);
4363
+ const choices = buildWebRollbackChoices(catalog);
4364
+ if (!options.versionId && choices.length === 0) throw new CliError("No eligible recent production Web version is available", 1);
4365
+ const versionId = options.versionId ?? (options.previous ? choices[0]?.value : await ui.select({ message: "Select a production Web version to restore", choices, pageSize: Math.min(choices.length, 15) }));
4366
+ const selected = catalog.candidates.find((candidate) => candidate.versionId === versionId && !candidate.current);
4367
+ if (!selected) throw new CliError("No eligible recent production Web version is available", 1);
4368
+ logger.info(`Current deployment: ${catalog.currentDeployment.id}`);
4369
+ logger.info(`Target version: ${selected.versionId} / SHA ${selected.sha ?? "unknown"}`);
4370
+ if (selected.runId) logger.info(`Published by: https://github.com/${REPO}/actions/runs/${selected.runId}`);
4371
+ if (selected.withdrawn) logger.warn("This version was previously withdrawn by a rollback.");
4372
+ if (!options.yes && !await ui.confirm({ message: "Restore this Web version to production?", default: false })) {
4373
+ logger.warn("Rollback cancelled");
4374
+ return;
4375
+ }
4376
+ inputs = { ...inputs, previous: "false", version_id: selected.versionId, expected_deployment_id: catalog.currentDeployment.id };
4377
+ }
4378
+ const { run, result } = await dispatch("execute", inputs);
4379
+ logger.success(`Platform Web switched to ${result.targetVersionId} at 100% traffic`);
4380
+ logger.info(`Verification: ${JSON.stringify(result.verification)}`);
4381
+ logger.info(`Timing (ms): ${JSON.stringify(result.timings)}`);
4382
+ logger.info(run.htmlUrl);
4383
+ };
4384
+
4249
4385
  // src/commands/release/rollback.ts
4250
- var REPO = tuttiServerRollbackTarget.repoFullName;
4251
- var WORKFLOW = "rollback.yml";
4386
+ var REPO2 = tuttiServerRollbackTarget.repoFullName;
4387
+ var WORKFLOW2 = "rollback.yml";
4252
4388
  var SHA256 = /^sha256:[0-9a-f]{64}$/;
4253
4389
  var SINGAPORE_TIME = new Intl.DateTimeFormat("sv-SE", {
4254
4390
  timeZone: "Asia/Singapore",
@@ -4272,7 +4408,7 @@ var buildRollbackChoices = (catalog) => catalog.candidates.filter((candidate) =>
4272
4408
  name: `${displayChoiceTime(candidate.deployedAt)} SGT #${candidate.revision} ${candidate.version}`,
4273
4409
  value: candidate.revision
4274
4410
  }));
4275
- var gh = async (args) => {
4411
+ var gh2 = async (args) => {
4276
4412
  const result = await runCommand("gh", args);
4277
4413
  if (result.exitCode !== 0) {
4278
4414
  throw new CliError(result.stderr.trim() || result.stdout.trim() || `gh ${args[0]} failed`, 1);
@@ -4292,8 +4428,8 @@ var parseRollbackCatalog = (raw) => {
4292
4428
  return data;
4293
4429
  };
4294
4430
  var listActiveDeployments = async () => {
4295
- const runs = await Promise.all(["deploy.yml", WORKFLOW].map(async (workflow) => {
4296
- const raw = await gh(["api", `repos/${REPO}/actions/workflows/${workflow}/runs?per_page=30`]);
4431
+ const runs = await Promise.all(["deploy.yml", WORKFLOW2].map(async (workflow) => {
4432
+ const raw = await gh2(["api", `repos/${REPO2}/actions/workflows/${workflow}/runs?per_page=30`]);
4297
4433
  return JSON.parse(raw).workflow_runs;
4298
4434
  }));
4299
4435
  return runs.flat().filter(
@@ -4306,19 +4442,19 @@ var ensureNoActiveDeployment = async () => {
4306
4442
  throw new CliError(`Production deployment is already running: ${active[0]?.html_url}`, 1);
4307
4443
  }
4308
4444
  };
4309
- var dispatch = async (operation, inputs = {}) => {
4310
- const knownRunIds = await listWorkflowRunIds({ repoFullName: REPO, workflowFile: WORKFLOW, ref: "main" });
4311
- const args = ["workflow", "run", WORKFLOW, "--repo", REPO, "--ref", "main", "-f", `operation=${operation}`];
4445
+ var dispatch2 = async (operation, inputs = {}) => {
4446
+ const knownRunIds = await listWorkflowRunIds({ repoFullName: REPO2, workflowFile: WORKFLOW2, ref: "main" });
4447
+ const args = ["workflow", "run", WORKFLOW2, "--repo", REPO2, "--ref", "main", "-f", `operation=${operation}`];
4312
4448
  for (const [key, value] of Object.entries(inputs)) {
4313
4449
  args.push("-f", `${key}=${value}`);
4314
4450
  }
4315
- await gh(args);
4451
+ await gh2(args);
4316
4452
  const progress = ui.spinner(operation === "list" ? "\u8BFB\u53D6\u751F\u4EA7\u90E8\u7F72\u5386\u53F2..." : "\u7B49\u5F85\u751F\u4EA7\u56DE\u6EDA\u548C\u5065\u5EB7\u68C0\u67E5...").start();
4317
4453
  try {
4318
4454
  const run = await pollWorkflowRun({
4319
4455
  knownRunIds,
4320
- repoFullName: REPO,
4321
- workflowFile: WORKFLOW,
4456
+ repoFullName: REPO2,
4457
+ workflowFile: WORKFLOW2,
4322
4458
  ref: "main",
4323
4459
  maxAttempts: operation === "execute" ? 420 : 180,
4324
4460
  onUpdate: (update) => {
@@ -4328,15 +4464,15 @@ var dispatch = async (operation, inputs = {}) => {
4328
4464
  if (run.conclusion !== "success") {
4329
4465
  let state = "";
4330
4466
  if (operation === "execute") {
4331
- const dir = await mkdtemp2(join5(tmpdir(), "tutti-rollback-state-"));
4467
+ const dir = await mkdtemp3(join6(tmpdir2(), "tutti-rollback-state-"));
4332
4468
  try {
4333
- await gh(["run", "download", String(run.runId), "--repo", REPO, "--name", "rollback-state", "--dir", dir]);
4469
+ await gh2(["run", "download", String(run.runId), "--repo", REPO2, "--name", "rollback-state", "--dir", dir]);
4334
4470
  state = `
4335
- ${await readFile2(join5(dir, "rollback-state.txt"), "utf8")}`;
4471
+ ${await readFile3(join6(dir, "rollback-state.txt"), "utf8")}`;
4336
4472
  } catch {
4337
4473
  state = "\nActual production state is available in the workflow logs.";
4338
4474
  } finally {
4339
- await rm(dir, { recursive: true, force: true });
4475
+ await rm2(dir, { recursive: true, force: true });
4340
4476
  }
4341
4477
  }
4342
4478
  throw new CliError(`Workflow failed: ${run.failureMessage ?? run.conclusion}
@@ -4348,12 +4484,12 @@ ${run.htmlUrl}${state}`, 1);
4348
4484
  }
4349
4485
  };
4350
4486
  var fetchCatalog = async (runId) => {
4351
- const dir = await mkdtemp2(join5(tmpdir(), "tutti-rollback-"));
4487
+ const dir = await mkdtemp3(join6(tmpdir2(), "tutti-rollback-"));
4352
4488
  try {
4353
- await gh(["run", "download", String(runId), "--repo", REPO, "--name", "rollback-catalog", "--dir", dir]);
4354
- return parseRollbackCatalog(await readFile2(join5(dir, "rollback-catalog.json"), "utf8"));
4489
+ await gh2(["run", "download", String(runId), "--repo", REPO2, "--name", "rollback-catalog", "--dir", dir]);
4490
+ return parseRollbackCatalog(await readFile3(join6(dir, "rollback-catalog.json"), "utf8"));
4355
4491
  } finally {
4356
- await rm(dir, { recursive: true, force: true });
4492
+ await rm2(dir, { recursive: true, force: true });
4357
4493
  }
4358
4494
  };
4359
4495
  var readCatalog = async () => {
@@ -4365,7 +4501,7 @@ var readCatalog = async () => {
4365
4501
  progress.stop();
4366
4502
  }
4367
4503
  logger.warn("\u672C\u673A\u65E0\u6CD5\u5B8C\u6574\u8BFB\u53D6\u751F\u4EA7\u90E8\u7F72\u5386\u53F2\uFF0C\u6539\u7528 GitHub Actions \u67E5\u8BE2\u3002");
4368
- const listRun = await dispatch("list");
4504
+ const listRun = await dispatch2("list");
4369
4505
  return fetchCatalog(listRun.runId);
4370
4506
  };
4371
4507
  var runTuttiRollback = async (options) => {
@@ -4405,7 +4541,7 @@ var runTuttiRollback = async (options) => {
4405
4541
  return;
4406
4542
  }
4407
4543
  await ensureNoActiveDeployment();
4408
- const run = await dispatch("execute", {
4544
+ const run = await dispatch2("execute", {
4409
4545
  revision: String(selected.revision),
4410
4546
  digest: selected.digest,
4411
4547
  expected_current_digest: catalog.currentDigest,
@@ -4415,14 +4551,18 @@ var runTuttiRollback = async (options) => {
4415
4551
  logger.info(run.htmlUrl);
4416
4552
  };
4417
4553
  var registerReleaseRollbackCommand = (program) => {
4418
- program.command("rollback").description("Restore a recent production image").argument("[repository]", "Rollback target; omit to choose interactively").option("--revision <number>", "Select a recent Helm revision without the chooser").option("--yes", "Skip the final confirmation").action(async (repository, options) => {
4554
+ program.command("rollback").description("Restore a recent production deployment").argument("[repository]", "Rollback target; omit to choose interactively").option("--revision <number>", "Select a recent Helm revision without the chooser").option("--previous", "Restore the latest eligible previous Platform Web version").option("--version-id <uuid>", "Select a previously deployed Platform Web Worker version").option("--reason <text>", "Record a Platform Web rollback reason").option("--probe", "Measure a read-only Platform Web production probe").option("--yes", "Skip the final confirmation").action(async (repository, options) => {
4419
4555
  const selected = repository ?? await ui.select({
4420
4556
  message: "Select a repository to roll back",
4421
4557
  choices: rollbackTargets.map((target) => ({ name: target.label, value: target.key }))
4422
4558
  });
4423
- if (selected !== tuttiServerRollbackTarget.key) {
4424
- throw new CliError("Only tutti-server is currently supported", 1);
4559
+ if (selected === platformWebRollbackTarget.key) {
4560
+ if (options.revision) throw new CliError("--revision applies only to tutti-server", 1);
4561
+ await runPlatformWebRollback(options);
4562
+ return;
4425
4563
  }
4564
+ if (selected !== tuttiServerRollbackTarget.key) throw new CliError(`Unsupported rollback target: ${selected}`, 1);
4565
+ if (options.previous || options.versionId || options.reason || options.probe) throw new CliError("--previous, --version-id, --reason and --probe apply only to platform-web", 1);
4426
4566
  await runTuttiRollback(options);
4427
4567
  });
4428
4568
  };
@@ -4452,7 +4592,7 @@ var readCliVersion = () => {
4452
4592
 
4453
4593
  // src/lib/version-check.ts
4454
4594
  import { realpath } from "fs/promises";
4455
- import { join as join6, resolve as resolve3, sep } from "path";
4595
+ import { join as join7, resolve as resolve3, sep } from "path";
4456
4596
  var PACKAGE_NAME = "@tutti-os/nexight-devops";
4457
4597
  var AUTO_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
4458
4598
  var DEFAULT_LOCALE = "zh-CN";
@@ -4521,7 +4661,7 @@ var isInsidePackageRoot = (entryPath, root) => {
4521
4661
  if (!entryPath || !root) {
4522
4662
  return false;
4523
4663
  }
4524
- const packageRoot = resolve3(join6(root, "@tutti-os", "nexight-devops"));
4664
+ const packageRoot = resolve3(join7(root, "@tutti-os", "nexight-devops"));
4525
4665
  const targetPath = resolve3(entryPath);
4526
4666
  return targetPath === packageRoot || targetPath.startsWith(`${packageRoot}${sep}`);
4527
4667
  };
@@ -4531,7 +4671,7 @@ var isInsideBunGlobalBin = (entryPath, bunGlobalBin) => {
4531
4671
  }
4532
4672
  const targetPath = resolve3(entryPath);
4533
4673
  const binPath = resolve3(bunGlobalBin);
4534
- const expectedEntryPath = resolve3(join6(binPath, "nexight-devops"));
4674
+ const expectedEntryPath = resolve3(join7(binPath, "nexight-devops"));
4535
4675
  return targetPath === expectedEntryPath;
4536
4676
  };
4537
4677
  var readGlobalPath = async (command, args) => {