@supacloud/cli 0.23.0 → 0.25.0

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.
Files changed (3) hide show
  1. package/README.md +32 -2
  2. package/dist/index.js +65 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -111,18 +111,48 @@ loopback development origins, with the default `:80` likewise omitted. Use
111
111
  `release` is an official CLI entry point for the existing Management API
112
112
  logical-backup and PostgREST lifecycle capabilities. It requires the Management
113
113
  API context above; it does not promote an application `service_role` key to
114
- Management authority. Restore remains an admin-only operation and is not
115
- exposed by this command group.
114
+ Management authority.
115
+
116
+ Project pause and restore are explicit lifecycle commands. A logical backup
117
+ restore requires an operator to pause the selected project first and then use
118
+ `project get` to confirm that its status is `paused`. `project restore` resumes
119
+ the paused project lifecycle when its database exists; if the database is
120
+ missing, the platform begins project re-provisioning instead. It does not
121
+ restore a database backup and is never invoked automatically by the CLI.
122
+
123
+ ```bash
124
+ supacloud-cli project pause --ref abc123
125
+ supacloud-cli release logical_backup_restore --ref abc123 \
126
+ --backup_id logical-full_abc123_<backup-id-suffix> \
127
+ --expected_sha256 <64-lowercase-hex> \
128
+ --restore_confirmation RESTORE_PROJECT:abc123:logical-full_abc123_<backup-id-suffix>:<64-lowercase-hex>
129
+ supacloud-cli project restore --ref abc123
130
+ ```
116
131
 
117
132
  ```bash
118
133
  supacloud-cli release logical_backup_list --ref abc123
119
134
  supacloud-cli release logical_backup_create --ref abc123
135
+ supacloud-cli release logical_backup_restore --ref abc123 \
136
+ --backup_id logical-full_abc123_<backup-id-suffix> \
137
+ --expected_sha256 <64-lowercase-hex> \
138
+ --restore_confirmation RESTORE_PROJECT:abc123:logical-full_abc123_<backup-id-suffix>:<64-lowercase-hex>
120
139
  supacloud-cli release postgrest_status --ref abc123
121
140
  supacloud-cli release postgrest_restart --ref abc123
122
141
  ```
123
142
 
124
143
  Backup creation reports success only after the CLI verifies exactly one new
125
144
  logical-backup receipt against the inventory before and after the mutation.
145
+ Logical restore is project-scoped: pause the selected project first, obtain the
146
+ backup ID and SHA-256 from `logical_backup_list`, and supply both the normal
147
+ production `--confirm-production <ref>` value (for production profiles) and
148
+ the exact `--restore_confirmation
149
+ RESTORE_PROJECT:<ref>:<backup_id>:<sha256>`. Before POST, the CLI re-reads
150
+ that same project's inventory and binds the request to the complete verified
151
+ backup identity; it then verifies both the server receipt and a fresh inventory
152
+ read. It never retries a restore. A transport, server, or unreadable-response
153
+ failure is reported as `OUTCOME_UNKNOWN`; read the inventory and investigate
154
+ before any new restore decision.
155
+
126
156
  PostgREST restart reports success only after it receives a matching restart
127
157
  receipt and reads back `desired=running`, `actual=running`, and
128
158
  `health=healthy`. Both mutating controls follow the normal production
package/dist/index.js CHANGED
@@ -6459,7 +6459,7 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
6459
6459
  var ACTION_POLICY = {
6460
6460
  project: {
6461
6461
  read: ["get", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
6462
- write: ["task_cancel", "task_retry", "update_background_settings"]
6462
+ write: ["pause", "restore", "task_cancel", "task_retry", "update_background_settings"]
6463
6463
  },
6464
6464
  database: {
6465
6465
  read: ["list_tables", "describe_columns", "list_indexes", "list_constraints", "list_extensions", "rls_status", "rls_policies", "list_auth_users", "get_auth_user", "connections", "stats", "slow_queries", "list_migrations", "migration_inventory", "project_url", "generate_types"],
@@ -6486,7 +6486,7 @@ var ACTION_POLICY = {
6486
6486
  mutations: { read: ["status"] },
6487
6487
  release: {
6488
6488
  read: ["logical_backup_list", "postgrest_status"],
6489
- write: ["logical_backup_create", "postgrest_restart"]
6489
+ write: ["logical_backup_create", "logical_backup_restore", "postgrest_restart"]
6490
6490
  },
6491
6491
  secrets: { read: ["list"], write: ["upsert", "delete"] },
6492
6492
  frontend: {
@@ -10098,6 +10098,7 @@ var formatTaskStats = (data) => {
10098
10098
  `);
10099
10099
  };
10100
10100
  var ok = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
10101
+ var simple = (res, msg) => res.ok ? `✅ ${msg}` : `❌ Failed (${res.status})`;
10101
10102
  function buildProjectLogsPath(ref, logType) {
10102
10103
  const params = new URLSearchParams({ limit: "200" });
10103
10104
  if (logType && logType !== "all") {
@@ -10114,9 +10115,11 @@ function resolveRef(refFromArgs, defaultRef) {
10114
10115
  function registerUserProjectCliTools(server, http, options = {}) {
10115
10116
  const { projectRef } = options;
10116
10117
  server.tool("project", `Project-scoped inspection and developer operations.
10117
- Actions: get, health, logs, api_keys, settings, tasks, task_detail, task_cancel, task_retry, task_stats, dlq, background_settings, update_background_settings`, {
10118
+ Actions: get, pause, restore, health, logs, api_keys, settings, tasks, task_detail, task_cancel, task_retry, task_stats, dlq, background_settings, update_background_settings`, {
10118
10119
  action: withDescription(stringEnum([
10119
10120
  "get",
10121
+ "pause",
10122
+ "restore",
10120
10123
  "health",
10121
10124
  "logs",
10122
10125
  "api_keys",
@@ -10144,6 +10147,12 @@ Actions: get, health, logs, api_keys, settings, tasks, task_detail, task_cancel,
10144
10147
  return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
10145
10148
  maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
10146
10149
  }), resolvedRef));
10150
+ case "pause":
10151
+ text = simple(await http.post(`/v1/projects/${resolvedRef}/pause`), `Project ${resolvedRef} paused`);
10152
+ break;
10153
+ case "restore":
10154
+ text = simple(await http.post(`/v1/projects/${resolvedRef}/restore`), `Project ${resolvedRef} restored`);
10155
+ break;
10147
10156
  case "health":
10148
10157
  text = ok(await http.get(`/v1/projects/${resolvedRef}/health`));
10149
10158
  break;
@@ -10540,7 +10549,7 @@ var redirectStatus = Type.Optional(Type.Union([
10540
10549
  Type.Literal(308)
10541
10550
  ]));
10542
10551
  var ok2 = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
10543
- var simple = (res, msg) => res.ok ? `✅ ${msg}` : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
10552
+ var simple2 = (res, msg) => res.ok ? `✅ ${msg}` : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
10544
10553
  function registerGatewayTools(server, http, options = {}) {
10545
10554
  const { projectRef } = options;
10546
10555
  server.tool("gateway", `Gateway / Caddy 配置(通过 JSON Admin API 注入)。要求 admin 权限。
@@ -10802,11 +10811,11 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
10802
10811
  break;
10803
10812
  case "set_custom_hostname": {
10804
10813
  need("custom_hostname", custom_hostname);
10805
- text = simple(await http.post(`/v1/projects/${projectRefValue}/custom-hostname`, { custom_hostname }), `Custom hostname ${custom_hostname} requested`);
10814
+ text = simple2(await http.post(`/v1/projects/${projectRefValue}/custom-hostname`, { custom_hostname }), `Custom hostname ${custom_hostname} requested`);
10806
10815
  break;
10807
10816
  }
10808
10817
  case "delete_custom_hostname":
10809
- text = simple(await http.delete(`/v1/projects/${projectRefValue}/custom-hostname`), "Custom hostname removed");
10818
+ text = simple2(await http.delete(`/v1/projects/${projectRefValue}/custom-hostname`), "Custom hostname removed");
10810
10819
  break;
10811
10820
  case "verify_custom_hostname":
10812
10821
  text = ok2(await http.post(`/v1/projects/${projectRefValue}/custom-hostname/verify`));
@@ -12277,6 +12286,19 @@ function newlyCreatedBackup(before, after) {
12277
12286
  const additions = after.filter((backup) => !known.has(backup.backup_id));
12278
12287
  return additions.length === 1 ? additions[0] : null;
12279
12288
  }
12289
+ function restoreRequest(projectRef, backupId, expectedSha256, restoreConfirmation) {
12290
+ if (typeof backupId !== "string" || !backupBelongsToProject(backupId, projectRef)) {
12291
+ throw new Error("'backup_id' must identify a logical-full backup for 'ref'");
12292
+ }
12293
+ if (typeof expectedSha256 !== "string" || !SHA256.test(expectedSha256)) {
12294
+ throw new Error("'expected_sha256' must be a lowercase SHA-256 digest");
12295
+ }
12296
+ const confirmation = `RESTORE_PROJECT:${projectRef}:${backupId}:${expectedSha256}`;
12297
+ if (restoreConfirmation !== confirmation) {
12298
+ throw new Error("'restore_confirmation' must exactly confirm the selected logical backup restore");
12299
+ }
12300
+ return { backup_id: backupId, expected_sha256: expectedSha256, confirmation };
12301
+ }
12280
12302
  function endpoint(projectRef) {
12281
12303
  if (!validProjectRef(projectRef))
12282
12304
  throw new Error("'ref' is invalid for release controls");
@@ -12334,15 +12356,19 @@ function isRestartReceipt(value) {
12334
12356
  return isRecord(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
12335
12357
  }
12336
12358
  function registerReleaseTools(server, http, options = {}) {
12337
- server.tool("release", "Verified release controls using a Management API credential. Actions: logical_backup_list, logical_backup_create, postgrest_status, postgrest_restart", {
12359
+ server.tool("release", "Verified release controls using a Management API credential. Actions: logical_backup_list, logical_backup_create, logical_backup_restore, postgrest_status, postgrest_restart", {
12338
12360
  action: withDescription(stringEnum([
12339
12361
  "logical_backup_list",
12340
12362
  "logical_backup_create",
12363
+ "logical_backup_restore",
12341
12364
  "postgrest_status",
12342
12365
  "postgrest_restart"
12343
12366
  ]), "Release control action"),
12344
- ref: optional(Type.String(), options.projectRef ? "Optional override when not auto-linked" : "Project ref")
12345
- }, async ({ action, ref }) => {
12367
+ ref: optional(Type.String(), options.projectRef ? "Optional override when not auto-linked" : "Project ref"),
12368
+ backup_id: optional(Type.String(), "[logical_backup_restore] Exact verified logical-full backup ID from the selected project inventory"),
12369
+ expected_sha256: optional(Type.String(), "[logical_backup_restore] Exact lowercase SHA-256 from the selected project inventory"),
12370
+ restore_confirmation: optional(Type.String(), "[logical_backup_restore] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation")
12371
+ }, async ({ action, ref, backup_id, expected_sha256, restore_confirmation }) => {
12346
12372
  const projectRef = typeof ref === "string" && ref || options.projectRef;
12347
12373
  if (!projectRef)
12348
12374
  throw new Error("'ref' is required for release controls");
@@ -12379,6 +12405,32 @@ function registerReleaseTools(server, http, options = {}) {
12379
12405
  backup: publicBackup(addedBackup)
12380
12406
  });
12381
12407
  }
12408
+ if (action === "logical_backup_restore") {
12409
+ const request = restoreRequest(projectRef, backup_id, expected_sha256, restore_confirmation);
12410
+ const before = await readInventory(http, projectRef);
12411
+ const beforeFailure = readInventoryFailure("release.logical_backup.restore", before);
12412
+ if (beforeFailure)
12413
+ return beforeFailure;
12414
+ const selectedBackup = before.inventory.find((backup) => backup.backup_id === request.backup_id && backup.sha256 === request.expected_sha256);
12415
+ if (!selectedBackup) {
12416
+ return releaseControlFailure("release.logical_backup.restore", "MUTATION_NOT_SUCCEEDED", null);
12417
+ }
12418
+ const mutation2 = await http.postReleaseMutation(`${endpoint(projectRef)}/database/backups/logical/restore`, request, { timeoutMs: BACKUP_TIMEOUT_MS });
12419
+ if (!mutation2.ok || mutation2.status !== 200) {
12420
+ return mutationFailure("release.logical_backup.restore", mutation2);
12421
+ }
12422
+ const responseBackup = isRecord(mutation2.data) ? verifiedBackup(mutation2.data.restored_backup, projectRef) : null;
12423
+ const after = await readInventory(http, projectRef);
12424
+ const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
12425
+ const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
12426
+ if (!responseBackup || !equalBackup(responseBackup, selectedBackup) || afterFailure || !restoredInventoryBackup || !equalBackup(restoredInventoryBackup, selectedBackup)) {
12427
+ return releaseControlFailure("release.logical_backup.restore", "OUTCOME_UNKNOWN", mutation2.status);
12428
+ }
12429
+ return releaseControlSuccess("release.logical_backup.restore", {
12430
+ project_ref: projectRef,
12431
+ backup: publicBackup(selectedBackup)
12432
+ });
12433
+ }
12382
12434
  if (action === "postgrest_status") {
12383
12435
  const read2 = await readPostgrestStatus(http, projectRef);
12384
12436
  const failure = readPostgrestFailure("release.postgrest.status", read2);
@@ -12407,7 +12459,7 @@ function registerReleaseTools(server, http, options = {}) {
12407
12459
  // package.json
12408
12460
  var package_default = {
12409
12461
  name: "@supacloud/cli",
12410
- version: "0.23.0",
12462
+ version: "0.25.0",
12411
12463
  description: "Project-scoped CLI for SupaCloud users",
12412
12464
  type: "module",
12413
12465
  main: "./dist/index.js",
@@ -12451,6 +12503,8 @@ var commandName = "supacloud-cli";
12451
12503
  var preferredCommand = commandName;
12452
12504
  var projectActionSchema = stringEnum([
12453
12505
  "get",
12506
+ "pause",
12507
+ "restore",
12454
12508
  "health",
12455
12509
  "logs",
12456
12510
  "api_keys",
@@ -12652,6 +12706,7 @@ EXAMPLES
12652
12706
  ${preferredCommand} project logs --log_type database
12653
12707
  ${preferredCommand} project task_stats
12654
12708
  ${preferredCommand} release logical_backup_create --ref abc123
12709
+ ${preferredCommand} release logical_backup_restore --ref abc123 --backup_id <backup_id> --expected_sha256 <sha256> --restore_confirmation RESTORE_PROJECT:abc123:<backup_id>:<sha256>
12655
12710
  ${preferredCommand} release postgrest_status --ref abc123
12656
12711
  ${preferredCommand} release postgrest_restart --ref abc123
12657
12712
  ${preferredCommand} queue stats --queue emails
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",