backupdrill 2.2.4 → 2.2.5

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
@@ -31,6 +31,17 @@ npm install -g backupdrill
31
31
  npx backupdrill backup
32
32
  ```
33
33
 
34
+ ## Use it from Claude Code or Cursor
35
+
36
+ The [`plugin/`](plugin/) folder is a plugin for Claude Code and Cursor. It teaches the agent to run `estimate`, `backup`, `drill` and `restore` for you, keeps your keys in a gitignored `.env.backupdrill` that the agent never opens, and always does a dry run before a restore. In Claude Code:
37
+
38
+ ```
39
+ /plugin marketplace add backupdrill/cli
40
+ /plugin install backupdrill@backupdrill
41
+ ```
42
+
43
+ Details are in [plugin/README.md](plugin/README.md).
44
+
34
45
  ## Usage
35
46
 
36
47
  Configure via **env vars**, a **config file**, or **flags** (precedence: flags > env > file).
package/dist/restore.d.ts CHANGED
@@ -61,6 +61,12 @@ export declare function assertRequestApplies(manifest: Manifest, wantsDatabase:
61
61
  * 这道门要求人把目标身份亲手打一遍。
62
62
  */
63
63
  export declare function assertConfirmedTarget(confirmTarget: string | undefined, targetDatabaseUrl?: string, targetSupabaseUrl?: string): void;
64
+ /**
65
+ * 空目标门的拒绝文案。"只重试 Storage"那句只对同时恢复 Storage 的运行有意义:库已写完、
66
+ * Storage 中途断了,重跑时才会在这道门上被拦。纯数据库恢复(托管恢复只做数据库)带上它,
67
+ * 用户会以为自己漏了一步,其实只需要换一个空项目。
68
+ */
69
+ export declare function nonEmptyTargetMessage(schemas: string[], objectCount: number, alsoRestoringStorage: boolean): string;
64
70
  /**
65
71
  * 把一份快照恢复出来:数据库经统一引擎还原进目标(与演练同一条 pg_restore 路径与
66
72
  * 错误分类),恢复后跑同一套表级验证;Storage 提供目标凭据时回传+对账(P0-D),
package/dist/restore.js CHANGED
@@ -155,6 +155,20 @@ export function assertConfirmedTarget(confirmTarget, targetDatabaseUrl, targetSu
155
155
  `(use --dry-run first to preview all checks without writing).`);
156
156
  }
157
157
  }
158
+ /**
159
+ * 空目标门的拒绝文案。"只重试 Storage"那句只对同时恢复 Storage 的运行有意义:库已写完、
160
+ * Storage 中途断了,重跑时才会在这道门上被拦。纯数据库恢复(托管恢复只做数据库)带上它,
161
+ * 用户会以为自己漏了一步,其实只需要换一个空项目。
162
+ */
163
+ export function nonEmptyTargetMessage(schemas, objectCount, alsoRestoringStorage) {
164
+ const message = `target schema(s) ${schemas.join(", ")} contain ${objectCount} existing object(s) — ` +
165
+ `restore only writes into an empty target. Create a fresh Supabase project ` +
166
+ `(or empty these schemas) and retry.`;
167
+ if (!alsoRestoringStorage)
168
+ return message;
169
+ return (`${message} If a previous run already restored the database and only Storage was ` +
170
+ `interrupted, re-run WITHOUT the database target to retry Storage alone.`);
171
+ }
158
172
  /**
159
173
  * 空目标门(PRD §5.3.3 / 决策 D5):目标 schema 里存在任何用户对象即拒绝写入。
160
174
  * "空"的定义来自 R0 spike:不是"schema 不存在"(Supabase 恒有 public),而是
@@ -162,7 +176,7 @@ export function assertConfirmedTarget(confirmTarget, targetDatabaseUrl, targetSu
162
176
  * 全部计入:只查关系时,"目标里只有函数"会放行,然后在 pre-data 中途撞冲突,
163
177
  * 留下半截目标(评审第 7 轮)。
164
178
  */
165
- async function assertEmptyTarget(targetUrl, schemas) {
179
+ async function assertEmptyTarget(targetUrl, schemas, alsoRestoringStorage) {
166
180
  const client = await connectPg(targetUrl);
167
181
  try {
168
182
  const res = await client.query(`select
@@ -175,11 +189,7 @@ async function assertEmptyTarget(targetUrl, schemas) {
175
189
  as n`, [schemas]);
176
190
  const objectCount = Number(res.rows[0].n);
177
191
  if (objectCount > 0) {
178
- throw new Error(`target schema(s) ${schemas.join(", ")} contain ${objectCount} existing object(s) — ` +
179
- `restore only writes into an empty target. Create a fresh Supabase project ` +
180
- `(or empty these schemas) and retry. If a previous run already restored the ` +
181
- `database and only Storage was interrupted, re-run WITHOUT the database target ` +
182
- `to retry Storage alone.`);
192
+ throw new Error(nonEmptyTargetMessage(schemas, objectCount, alsoRestoringStorage));
183
193
  }
184
194
  }
185
195
  finally {
@@ -190,7 +200,7 @@ async function assertEmptyTarget(targetUrl, schemas) {
190
200
  * dry-run 只读预检(PRD §5.3.2):零写入,把正式执行会撞的墙全部提前撞一遍。
191
201
  * 逐项打印,任何 blocker 汇总后抛出(退出码非零 = "现在执行不会成功")。
192
202
  */
193
- async function dryRunPreflight(config, manifest, targetUrl, storageTarget) {
203
+ async function dryRunPreflight(config, manifest, targetUrl, storageTarget, alsoRestoringStorage) {
194
204
  const blockers = [];
195
205
  const db = manifest.database;
196
206
  const mb = (n) => (n / 1024 / 1024).toFixed(1);
@@ -221,7 +231,7 @@ async function dryRunPreflight(config, manifest, targetUrl, storageTarget) {
221
231
  log.ok("target identity: distinct from source");
222
232
  }
223
233
  try {
224
- await assertEmptyTarget(targetUrl, db.schemas);
234
+ await assertEmptyTarget(targetUrl, db.schemas, alsoRestoringStorage);
225
235
  log.ok(`target schemas (${db.schemas.join(", ")}) are empty`);
226
236
  }
227
237
  catch (error) {
@@ -376,10 +386,13 @@ export async function runRestore(config, opts) {
376
386
  storageFilesWritten: 0,
377
387
  };
378
388
  assertRequestApplies(manifest, !!opts.targetDatabaseUrl, !!storageTarget);
389
+ // 这次运行会不会碰 Storage(回传到目标,或下载到本地)。只有会碰时,空目标门才该
390
+ // 提示"库已恢复、只重试 Storage";skipStorage 的纯数据库运行(托管恢复)不该看到这句。
391
+ const alsoRestoringStorage = !!manifest.storage && !opts.skipStorage;
379
392
  // dry-run:只读预检后直接返回,零写入(PRD §5.3.2)
380
393
  if (opts.dryRun) {
381
394
  log.step(`Dry run for snapshot ${snapshot} — nothing will be written`);
382
- await dryRunPreflight(config, manifest, opts.targetDatabaseUrl, storageTarget);
395
+ await dryRunPreflight(config, manifest, opts.targetDatabaseUrl, storageTarget, alsoRestoringStorage);
383
396
  result.dryRun = true;
384
397
  return result;
385
398
  }
@@ -408,7 +421,7 @@ export async function runRestore(config, opts) {
408
421
  "at a fresh Supabase project.");
409
422
  }
410
423
  log.step("Checking target is empty…");
411
- await assertEmptyTarget(targetUrl, manifest.database.schemas);
424
+ await assertEmptyTarget(targetUrl, manifest.database.schemas, alsoRestoringStorage);
412
425
  // 版本预检不只属于 dry-run:跳过 dry-run 直接执行时,目标比源旧照样会
413
426
  // 半途炸掉或部分恢复(评审第 8 轮)——写入前必须拦住
414
427
  const versionClient = await connectPg(targetUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backupdrill",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "description": "Open-source CLI to back up a Supabase project — Postgres database and Storage files — to your own S3/R2/B2 bucket, with a checksummed manifest.",
5
5
  "type": "module",
6
6
  "bin": {