@supacloud/admin 0.19.0 → 0.20.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.
Files changed (3) hide show
  1. package/README.md +26 -0
  2. package/dist/index.js +94 -5
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -101,6 +101,9 @@ npx @supacloud/admin ssh ping
101
101
  npx @supacloud/admin ssh versions
102
102
  npx @supacloud/admin ssh diagnose
103
103
  npx @supacloud/admin ssh upgrade_status --transaction_id 11111111-1111-4111-8111-111111111111
104
+ npx @supacloud/admin ssh backup_cleanup_plan --before 2026-09-03 --keep_latest 2
105
+ npx @supacloud/admin ssh backup_cleanup_apply --before 2026-09-03 --keep_latest 2 \
106
+ --plan_sha256 <plan_sha256>
104
107
  npx @supacloud/admin project create --name my-app --domain example.com \
105
108
  --env_file /secure/path/.env.project-credentials.test --environment test
106
109
  npx @supacloud/admin project list
@@ -118,6 +121,29 @@ plus an explicit `tree_sha256` digest. An `unknown` component remains a valid
118
121
  inventory result, while any `error` component makes the CLI exit non-zero after
119
122
  printing the structured report.
120
123
 
124
+ ## Managed rollback backup cleanup
125
+
126
+ Use the two-step SSH cleanup flow when old platform rollback directories consume
127
+ disk space:
128
+
129
+ ```bash
130
+ npx @supacloud/admin ssh backup_cleanup_plan \
131
+ --before 2026-09-03 --keep_latest 2
132
+ # review plan_sha256 and the candidate list
133
+ npx @supacloud/admin ssh backup_cleanup_apply \
134
+ --before 2026-09-03 --keep_latest 2 \
135
+ --plan_sha256 <plan_sha256>
136
+ ```
137
+
138
+ The plan is read-only. Apply requires the exact digest from the plan and
139
+ recomputes it immediately before deletion. Only completed `committed` or
140
+ `rolled_back` transaction directories directly under
141
+ `/opt/supacloud/backups` are eligible; the newest `keep_latest` directories
142
+ are retained, and candidates must be older than `before`. Symlinks,
143
+ mountpoints, malformed transaction names, missing status files, and every path
144
+ outside the managed backup root are refused. Production runs also require the
145
+ normal `--confirm-production host:<ssh-host>[:port]` confirmation.
146
+
121
147
  ## Verified platform upgrades
122
148
 
123
149
  Use one explicit Admin command to upgrade Management, Web Console, and an
package/dist/index.js CHANGED
@@ -25886,12 +25886,13 @@ var ACTION_POLICY = {
25886
25886
  "exec",
25887
25887
  "troubleshoot",
25888
25888
  "container_logs",
25889
+ "backup_cleanup_plan",
25889
25890
  "tenant_list",
25890
25891
  "tenant_inspect",
25891
25892
  "tenant_diagnose",
25892
25893
  "upgrade_status"
25893
25894
  ],
25894
- write: ["setup", "install", "upgrade", "tenant_migrate"]
25895
+ write: ["setup", "install", "upgrade", "backup_cleanup_apply", "tenant_migrate"]
25895
25896
  }
25896
25897
  };
25897
25898
  var DYNAMIC_ACTION_FIELDS = {
@@ -25902,7 +25903,8 @@ var REFLESS_WRITE_ACTIONS = new Set([
25902
25903
  "project.create",
25903
25904
  "ssh.setup",
25904
25905
  "ssh.install",
25905
- "ssh.upgrade"
25906
+ "ssh.upgrade",
25907
+ "ssh.backup_cleanup_apply"
25906
25908
  ]);
25907
25909
  var SSH_PROJECT_REF_ACTIONS = new Set(["tenant_manage", "tenant_inspect", "tenant_diagnose"]);
25908
25910
  var PLATFORM_PROJECT_REF_ACTIONS = new Set([
@@ -28506,6 +28508,9 @@ var WEB_TREE_AFTER_LABEL = "SUPACLOUD_WEB_TREE_SHA256_AFTER=";
28506
28508
  var WEB_MARKER_AFTER_LABEL = "SUPACLOUD_WEB_MARKER_BASE64_AFTER=";
28507
28509
  var WEB_ROOT_REAL_AFTER_LABEL = "SUPACLOUD_WEB_ROOT_REAL_AFTER=";
28508
28510
  var WEB_ROOT_ID_AFTER_LABEL = "SUPACLOUD_WEB_ROOT_ID_AFTER=";
28511
+ var BACKUP_CLEANUP_ROOT = "/opt/supacloud/backups";
28512
+ var BACKUP_CLEANUP_PLAN_SHA256 = /^[a-f0-9]{64}$/;
28513
+ var BACKUP_CLEANUP_DATE = /^\d{4}-\d{2}-\d{2}$/;
28509
28514
  function fixedBinaryProbeCommand(executablePath, versionArgument) {
28510
28515
  const executable = quoteEnvValue(executablePath);
28511
28516
  return [
@@ -28617,6 +28622,66 @@ function assertSafeReleaseTag(value) {
28617
28622
  }
28618
28623
  return value;
28619
28624
  }
28625
+ function assertBackupCleanupDate(value) {
28626
+ if (!BACKUP_CLEANUP_DATE.test(value))
28627
+ throw new Error("before must be an ISO date (YYYY-MM-DD)");
28628
+ const parsed = Date.parse(`${value}T00:00:00Z`);
28629
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString().slice(0, 10) !== value) {
28630
+ throw new Error("before must be a valid ISO date (YYYY-MM-DD)");
28631
+ }
28632
+ return value;
28633
+ }
28634
+ function assertBackupCleanupKeepLatest(value) {
28635
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 100) {
28636
+ throw new Error("keep_latest must be an integer between 1 and 100");
28637
+ }
28638
+ return value;
28639
+ }
28640
+ function assertBackupCleanupPlanSha256(value) {
28641
+ if (!BACKUP_CLEANUP_PLAN_SHA256.test(value))
28642
+ throw new Error("plan_sha256 must be a lowercase SHA-256 digest");
28643
+ return value;
28644
+ }
28645
+ function backupCleanupCommand(args) {
28646
+ const before = quoteEnvValue(args.before);
28647
+ const keepLatest = String(args.keepLatest);
28648
+ const expectedPlan = args.planSha256 ? quoteEnvValue(args.planSha256) : "";
28649
+ const manifest = [
28650
+ "set -euo pipefail",
28651
+ "export TMPDIR=/dev/shm",
28652
+ ...args.apply ? [buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH)] : [],
28653
+ `ROOT=${quoteEnvValue(BACKUP_CLEANUP_ROOT)}`,
28654
+ `BEFORE=${before}`,
28655
+ `KEEP_LATEST=${keepLatest}`,
28656
+ "collect() {",
28657
+ ` find -P "$ROOT" -mindepth 1 -maxdepth 1 -type d -name 'supacloud-*' -print0 |`,
28658
+ " while IFS= read -r -d '' path; do",
28659
+ " base=${path##*/}",
28660
+ ' case "$base" in supacloud-[A-Za-z0-9._-]*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z-[A-Za-z0-9._-]*) ;; *) continue ;; esac',
28661
+ ' status_file="$path/transaction-status.json"',
28662
+ ' [ -f "$status_file" ] && [ ! -L "$status_file" ] || continue',
28663
+ ` status=$(sed -nE 's/^[[:space:]]*"status"[[:space:]]*:[[:space:]]*"([^"]+)".*$/\\1/p' "$status_file" | head -1)`,
28664
+ ' case "$status" in committed|rolled_back) ;; *) continue ;; esac',
28665
+ ` finished=$(sed -nE 's/^[[:space:]]*"finishedAt"[[:space:]]*:[[:space:]]*"([^"]+)".*$/\\1/p' "$status_file" | head -1)`,
28666
+ " finished_date=${finished:0:10}",
28667
+ ' [[ "$finished_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || continue',
28668
+ ` bytes=$(du -sb -- "$path" | awk '{print $1}')`,
28669
+ ` printf '%s\\t%s\\t%s\\t%s\\n' "$finished" "$status" "$bytes" "$path"`,
28670
+ " done",
28671
+ "}",
28672
+ `manifest=$(collect | sort -t $'\\t' -k1,1r | awk -F '\\t' -v keep="$KEEP_LATEST" -v before="$BEFORE" 'NR > keep && substr($1,1,10) < before')`,
28673
+ `plan_sha256=$(printf '%s' "$manifest" | sha256sum | awk '{print $1}')`,
28674
+ 'echo "plan_sha256=$plan_sha256"',
28675
+ 'echo "before=$BEFORE keep_latest=$KEEP_LATEST"',
28676
+ `if [ -n "$manifest" ]; then printf '%s\\n' "$manifest"; else echo 'candidates=0'; fi`
28677
+ ];
28678
+ if (!args.apply)
28679
+ return manifest.join(`
28680
+ `);
28681
+ manifest.push(`expected=${expectedPlan}`, `[ "$plan_sha256" = "$expected" ] || { echo 'plan_sha256 mismatch; refusing cleanup' >&2; exit 65; }`, "freed=0", "deleted=0", 'if [ -n "$manifest" ]; then', " while IFS=$'\\t' read -r finished status bytes path; do", ' [ -n "$path" ] || continue', ` case "$path" in "$ROOT"/supacloud-*) ;; *) echo 'candidate escaped backup root' >&2; exit 66 ;; esac`, ' [ -d "$path" ] && [ ! -L "$path" ] || { echo "candidate changed: $path" >&2; exit 67; }', ' mountpoint -q -- "$path" && { echo "mountpoint candidate: $path" >&2; exit 68; }', ' find -P -- "$path" -xdev -depth -delete', ' [ ! -e "$path" ] || { echo "candidate was not removed: $path" >&2; exit 69; }', " freed=$((freed + bytes))", " deleted=$((deleted + 1))", ' done <<< "$manifest"', "fi", 'echo "deleted=$deleted freed_bytes=$freed"', "df -B1 / | tail -1");
28682
+ return manifest.join(`
28683
+ `);
28684
+ }
28620
28685
  function assertExactStableVersion2(value, fieldName) {
28621
28686
  const normalized = value.startsWith("v") ? value.slice(1) : value;
28622
28687
  if (!EXACT_STABLE_SEMVER.test(normalized)) {
@@ -29349,7 +29414,7 @@ function platformVersionsToolResult(report) {
29349
29414
  }
29350
29415
  function registerSshTools(server, ssh) {
29351
29416
  server.tool("ssh", `Server management via SSH. Available before & after SupaCloud installation.
29352
- Actions: ping, setup, install, upgrade, upgrade_status, versions, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
29417
+ Actions: ping, setup, install, upgrade, upgrade_status, versions, diagnose, exec, troubleshoot, container_logs, backup_cleanup_plan, backup_cleanup_apply, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
29353
29418
  action: withDescription(stringEnum([
29354
29419
  "ping",
29355
29420
  "setup",
@@ -29361,6 +29426,8 @@ Actions: ping, setup, install, upgrade, upgrade_status, versions, diagnose, exec
29361
29426
  "exec",
29362
29427
  "troubleshoot",
29363
29428
  "container_logs",
29429
+ "backup_cleanup_plan",
29430
+ "backup_cleanup_apply",
29364
29431
  "tenant_manage",
29365
29432
  "tenant_list",
29366
29433
  "tenant_inspect",
@@ -29381,6 +29448,9 @@ Actions: ping, setup, install, upgrade, upgrade_status, versions, diagnose, exec
29381
29448
  artifact_transport: optional(stringEnum(["local", "remote"]), "[upgrade] Download verified release assets locally or on the server (default: remote)"),
29382
29449
  github_proxy: optional(Type.String(), "[install/upgrade] Explicit GitHub proxy prefix, or direct/none"),
29383
29450
  focus: optional(stringEnum(["all", "containers", "database", "network", "disk", "logs"]), "[troubleshoot] Focus area"),
29451
+ before: optional(Type.String(), "[backup_cleanup_*] Delete only completed rollback directories finished before YYYY-MM-DD"),
29452
+ keep_latest: optional(Type.Number(), "[backup_cleanup_*] Keep this many newest completed rollback directories (default: 2)"),
29453
+ plan_sha256: optional(Type.String(), "[backup_cleanup_apply] Exact plan_sha256 returned by backup_cleanup_plan"),
29384
29454
  container: optional(Type.String(), "[container_logs] Container name"),
29385
29455
  lines: optional(Type.Number(), "[container_logs] Number of log lines (default: 100)"),
29386
29456
  project_ref: optional(Type.String(), "[tenant_*] Project reference ID"),
@@ -29545,6 +29615,25 @@ ${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
29545
29615
  case "versions": {
29546
29616
  return platformVersionsToolResult(await platformVersions(ssh));
29547
29617
  }
29618
+ case "backup_cleanup_plan":
29619
+ case "backup_cleanup_apply": {
29620
+ if (typeof args.before !== "string")
29621
+ throw new Error("'before' required");
29622
+ const before = assertBackupCleanupDate(args.before);
29623
+ const keepLatest = args.keep_latest === undefined ? 2 : assertBackupCleanupKeepLatest(args.keep_latest);
29624
+ const apply = action === "backup_cleanup_apply";
29625
+ const planSha256 = args.plan_sha256 === undefined ? undefined : assertBackupCleanupPlanSha256(String(args.plan_sha256));
29626
+ if (apply && !planSha256)
29627
+ throw new Error("'plan_sha256' required for backup_cleanup_apply");
29628
+ if (!apply && planSha256)
29629
+ throw new Error("plan_sha256 is only valid for backup_cleanup_apply");
29630
+ const r = await ssh.exec(backupCleanupCommand({ before, keepLatest, planSha256, apply }), 120000);
29631
+ if (!r.success) {
29632
+ throw new Error(`Backup cleanup ${apply ? "apply" : "plan"} failed (exit ${r.code}): ${(r.stderr || r.stdout).slice(-1000)}`);
29633
+ }
29634
+ text = r.stdout;
29635
+ break;
29636
+ }
29548
29637
  case "diagnose": {
29549
29638
  const cmds = [
29550
29639
  "set -o pipefail",
@@ -32571,7 +32660,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
32571
32660
  // package.json
32572
32661
  var package_default = {
32573
32662
  name: "@supacloud/admin",
32574
- version: "0.19.0",
32663
+ version: "0.20.1",
32575
32664
  description: "Platform administration CLI for SupaCloud operators",
32576
32665
  type: "module",
32577
32666
  main: "./dist/index.js",
@@ -32606,7 +32695,7 @@ var package_default = {
32606
32695
  },
32607
32696
  devDependencies: {
32608
32697
  "@types/bun": "^1.4.0",
32609
- "@types/ssh2": "^1.15.5",
32698
+ "@types/ssh2": "^1.15.6",
32610
32699
  typescript: "^7.0.2"
32611
32700
  }
32612
32701
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/bun": "^1.4.0",
38
- "@types/ssh2": "^1.15.5",
38
+ "@types/ssh2": "^1.15.6",
39
39
  "typescript": "^7.0.2"
40
40
  }
41
41
  }