firevault 0.1.1-beta.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - Unreleased
4
+
5
+ Initial prerelease candidate for Firevault, an undo button for Firestore.
6
+
7
+ - Firestore backup to deterministic JSON files.
8
+ - Git-scoped local snapshot workflow.
9
+ - File-level change inspection.
10
+ - Document and collection history inspection.
11
+ - Restore preview from Git.
12
+ - Local backup-file restore with explicit confirmation.
13
+ - Single-document Firestore restore with explicit confirmation.
14
+ - Firestore emulator integration tests for backup and restore safety paths.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 henskjold73
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,363 @@
1
+ # Firevault
2
+
3
+ Undo button for Firestore.
4
+
5
+ Firevault gives Firestore projects Git-style history, change inspection, and document-level rollback so teams can recover from accidental writes, bad migrations, and destructive scripts.
6
+
7
+ Supporting line: Git-style history, rollback, and recovery for Firestore projects.
8
+
9
+ Firevault is focused operational recovery tooling for existing Firestore projects. It is not a hosted database platform, Firebase replacement, generic backup vendor, SaaS product, or dashboard.
10
+
11
+ ## Current Status
12
+
13
+ Firevault is in Foundation / Phase 0.
14
+
15
+ This is an experimental prerelease CLI. Use it against test or non-critical Firestore projects until recovery behavior has been reviewed for your project.
16
+
17
+ Current scope:
18
+
19
+ - snapshot Firestore into Git-friendly JSON,
20
+ - inspect changes,
21
+ - view document history,
22
+ - preview rollback,
23
+ - restore one document back to Firestore.
24
+
25
+ Current export shape:
26
+
27
+ ```txt
28
+ firestore-backups/
29
+ users/
30
+ abc123.json
31
+ def456.json
32
+ ```
33
+
34
+ The immediate priority is trustworthy document-level recovery: clear previews, explicit confirmation, and no broad destructive restore flows.
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ npm install -g firevault
40
+ ```
41
+
42
+ Create a Firebase service account key for your Firestore project, save it as `serviceAccountKey.json`, and keep it out of Git.
43
+
44
+ Create `firevault.config.json`:
45
+
46
+ ```json
47
+ {
48
+ "projectId": "your-project-id",
49
+ "serviceAccountPath": "./serviceAccountKey.json",
50
+ "outputDir": "firestore-backups",
51
+ "collections": ["users"]
52
+ }
53
+ ```
54
+
55
+ Take a snapshot:
56
+
57
+ ```bash
58
+ firevault snapshot
59
+ ```
60
+
61
+ Example output:
62
+
63
+ ```txt
64
+ Exported 2 docs from users
65
+ Backup complete.
66
+ Created commit: backup: 2026-05-16T17:00:00.000Z
67
+ ```
68
+
69
+ Inspect what changed:
70
+
71
+ ```bash
72
+ firevault changes
73
+ ```
74
+
75
+ Example output:
76
+
77
+ ```txt
78
+ Added:
79
+
80
+ * firestore-backups/users/abc123.json
81
+
82
+ Modified:
83
+
84
+ Deleted:
85
+ ```
86
+
87
+ Preview a document rollback:
88
+
89
+ ```bash
90
+ firevault restore-preview users/abc123 --from HEAD~1
91
+ ```
92
+
93
+ Example output:
94
+
95
+ ```txt
96
+ Target: firestore-backups/users/abc123.json
97
+ Source commit: HEAD~1
98
+ Current file exists: yes
99
+
100
+ Diff:
101
+
102
+ {
103
+ - "name": "Ada Lovelace"
104
+ + "name": "Ada"
105
+ }
106
+ ```
107
+
108
+ Restore one document to Firestore after reviewing the preview:
109
+
110
+ ```bash
111
+ firevault restore-firestore users/abc123 --from HEAD~1 --confirm
112
+ ```
113
+
114
+ `restore-firestore` overwrites one Firestore document with the JSON from Git. It does not support collection restore, merge, or patch restore yet.
115
+
116
+ ## Recovery Workflow
117
+
118
+ Scenario: a script accidentally overwrites `users/abc123`.
119
+
120
+ 1. Inspect recent snapshot changes:
121
+
122
+ ```bash
123
+ firevault changes --last 24h
124
+ ```
125
+
126
+ 2. Find the document history:
127
+
128
+ ```bash
129
+ firevault history users/abc123
130
+ ```
131
+
132
+ 3. Preview the rollback:
133
+
134
+ ```bash
135
+ firevault restore-preview users/abc123 --from HEAD~3
136
+ ```
137
+
138
+ 4. Restore only that document:
139
+
140
+ ```bash
141
+ firevault restore-firestore users/abc123 --from HEAD~3 --confirm
142
+ ```
143
+
144
+ 5. Take a new snapshot after recovery:
145
+
146
+ ```bash
147
+ firevault snapshot
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ Firevault operates against an existing Firebase project using a service account.
153
+
154
+ Expected config shape:
155
+
156
+ ```json
157
+ {
158
+ "projectId": "your-project-id",
159
+ "serviceAccountPath": "./serviceAccountKey.json",
160
+ "outputDir": "firestore-backups",
161
+ "collections": ["users"]
162
+ }
163
+ ```
164
+
165
+ Notes:
166
+
167
+ - `serviceAccountPath` points to a local Firebase service account JSON file.
168
+ - `outputDir` is where Firestore documents are written.
169
+ - `collections` controls which top-level Firestore collections are exported.
170
+ - Service account files must not be committed.
171
+
172
+ Recommended `.gitignore` entries for local development:
173
+
174
+ ```gitignore
175
+ serviceAccountKey.json
176
+ firestore-backups/
177
+ ```
178
+
179
+ ## Commands
180
+
181
+ ```bash
182
+ firevault init
183
+ firevault backup
184
+ firevault commit
185
+ firevault snapshot
186
+ firevault changes
187
+ firevault changes --last 24h
188
+ firevault history users/abc123
189
+ firevault restore-preview users/abc123 --from HEAD~3
190
+ firevault restore-local users/abc123 --from HEAD~3 --confirm
191
+ firevault restore-firestore users/abc123 --from HEAD~3 --confirm
192
+ ```
193
+
194
+ ## Local Development
195
+
196
+ Install dependencies and run commands through the TypeScript entrypoint:
197
+
198
+ ```bash
199
+ npm install
200
+ npm run dev -- --help
201
+ npm run dev -- backup
202
+ npm run dev -- changes
203
+ ```
204
+
205
+ Build and link the compiled CLI:
206
+
207
+ ```bash
208
+ npm run build
209
+ npm link
210
+ firevault --help
211
+ ```
212
+
213
+ The installed `firevault` binary runs from `dist/index.js`.
214
+
215
+ ## Backup Model
216
+
217
+ Firevault writes one document per file:
218
+
219
+ ```txt
220
+ <outputDir>/<collection>/<documentId>.json
221
+ ```
222
+
223
+ JSON output is stable:
224
+
225
+ - object keys are sorted recursively,
226
+ - formatting is deterministic,
227
+ - files are intended to produce readable Git diffs.
228
+
229
+ ## Git Commit Flow
230
+
231
+ `firevault backup` exports configured Firestore collections to deterministic local JSON files. It does not stage or commit anything.
232
+
233
+ `firevault commit` expects to run inside a Git repository.
234
+
235
+ Behavior:
236
+
237
+ - checks for changes under the configured `outputDir`,
238
+ - exits successfully if no backup changes exist,
239
+ - stages only the configured `outputDir`,
240
+ - creates a local commit with message `backup: <ISO timestamp>`,
241
+ - never pushes.
242
+
243
+ Keep `serviceAccountKey.json` ignored so credentials cannot be committed by this workflow or by manual Git usage.
244
+
245
+ `firevault snapshot` is the safe local recovery snapshot workflow:
246
+
247
+ - runs backup,
248
+ - stops immediately if backup fails,
249
+ - commits backup changes when files changed,
250
+ - exits successfully when backup succeeds but no Git changes exist,
251
+ - never pushes.
252
+
253
+ `firevault changes` shows a file-level Git summary for the configured `outputDir` only:
254
+
255
+ ```txt
256
+ Added:
257
+
258
+ * firestore-backups/users/abc123.json
259
+
260
+ Modified:
261
+
262
+ * firestore-backups/users/def456.json
263
+
264
+ Deleted:
265
+
266
+ * firestore-backups/users/old-user.json
267
+ ```
268
+
269
+ Without options it inspects working tree changes. With `--last 24h`, it uses Git history and lists files changed under `outputDir` in commits since that time window. It does not contact Firebase.
270
+
271
+ `firevault history <path>` shows commit history for one backed-up document or collection. It accepts logical paths like `users/abc123`, full backup file paths like `firestore-backups/users/abc123.json`, and collection paths like `users`.
272
+
273
+ Output includes commit short SHA, commit date, and commit message. For collection paths, it also includes the number of files changed by each commit under that collection. It uses Git history only and does not contact Firebase.
274
+
275
+ `firevault restore-preview <path> --from <commit>` shows what would be restored for one backed-up document without writing anything. It accepts logical document paths and full backup file paths, reads the source JSON from Git, compares it to the current local backup file if present, and prints a readable line diff.
276
+
277
+ Restore preview is intentionally dry-run only. It does not write to Firestore, does not overwrite local files, does not push, and does not contact Firebase.
278
+
279
+ `firevault restore-local <path> --from <commit> --confirm` restores one backed-up document from Git into the local backup directory. It prints the same preview information before writing, creates parent directories if needed, and requires `--confirm`.
280
+
281
+ Restore local does not write to Firestore, does not stage, does not commit, does not push, and does not contact Firebase.
282
+
283
+ `firevault restore-firestore <path> --from <commit> --confirm` restores one backed-up document from Git directly into Firestore. It prints target backup path, Firestore collection, document ID, source commit, and a local JSON diff before writing.
284
+
285
+ Firestore restore overwrites the target document with the parsed JSON from Git. It does not support collection restore, merge, or patch restore yet. It does not modify local backup files, stage, commit, push, or contact GitHub.
286
+
287
+ Manual Firestore restore verification:
288
+
289
+ 1. Point `serviceAccountPath` at a valid service account for a test Firebase project.
290
+ 2. Run `npm run restore-preview -- users/abc123 --from <commit>` and inspect the diff.
291
+ 3. Run `npm run restore-firestore -- users/abc123 --from <commit> --confirm`.
292
+ 4. Verify the document in Firestore was overwritten with the JSON from Git.
293
+ 5. Run `git status` to confirm no local files were changed by `restore-firestore`.
294
+
295
+ ## Testing
296
+
297
+ Run the TypeScript build:
298
+
299
+ ```bash
300
+ npm run build
301
+ ```
302
+
303
+ Run Firestore emulator integration tests:
304
+
305
+ ```bash
306
+ npm run test:emulator
307
+ ```
308
+
309
+ The emulator tests require dependencies installed through `npm install`, including the `firebase-tools` dev dependency. The test runner starts the local Firestore emulator with demo project `demo-firevault-test`; it does not require `serviceAccountKey.json` and does not contact a real Firebase project.
310
+
311
+ Covered emulator flows:
312
+
313
+ - `backup` exports a known Firestore document,
314
+ - `backup` writes deterministic JSON,
315
+ - `restore-firestore` overwrites one emulator document from a Git commit,
316
+ - `restore-firestore` rejects collection paths,
317
+ - `restore-firestore` requires `--confirm`.
318
+
319
+ ## Publishing
320
+
321
+ Before publishing:
322
+
323
+ - run `npm run build`,
324
+ - run `npm run test:emulator`,
325
+ - run `npm pack --dry-run` and review the file list,
326
+ - verify `dist/index.js` exists and starts with `#!/usr/bin/env node`,
327
+ - do not publish `serviceAccountKey.json`,
328
+ - do not publish local `firestore-backups/` output,
329
+ - verify logs such as `firestore-debug.log` are not included.
330
+
331
+ The package `bin` points to `./dist/index.js`, so a published or linked package must include compiled output. `prepublishOnly` currently runs clean, build, and emulator tests.
332
+
333
+ ## Product Principles
334
+
335
+ Firevault should stay:
336
+
337
+ - small,
338
+ - operational,
339
+ - trustworthy,
340
+ - CLI-first,
341
+ - Git-backed.
342
+
343
+ Avoid adding SaaS features, hosted infrastructure, auth systems, collaboration features, dashboards, billing, or broad multi-cloud abstractions before the core Firestore to stable JSON to Git workflow is robust.
344
+
345
+ ## Safety
346
+
347
+ Firestore restore is document-only and overwrite-only for now. Future restore flows should:
348
+
349
+ - default to dry-run,
350
+ - require explicit confirmation for writes,
351
+ - start with document-level recovery,
352
+ - avoid early whole-database destructive workflows.
353
+
354
+ ## Documentation
355
+
356
+ - [Architecture](docs/architecture.md)
357
+ - [Roadmap](docs/roadmap.md)
358
+ - [GitHub labels](docs/github-labels.md)
359
+ - [Contributing](CONTRIBUTING.md)
360
+ - [Security](SECURITY.md)
361
+ - [AI review ledger](AI_REVIEW.md)
362
+
363
+ AI agents must never create git commits automatically. Human review and commits are required.
@@ -0,0 +1,25 @@
1
+ import { Command } from "commander";
2
+ import { ConfigError, loadConfig } from "../config/loadConfig.js";
3
+ import { exportCollection } from "../firestore/exportFirestore.js";
4
+ export async function runBackup() {
5
+ const config = loadConfig();
6
+ for (const collection of config.collections) {
7
+ await exportCollection(config.outputDir, collection);
8
+ }
9
+ console.log("Backup complete.");
10
+ }
11
+ export const backupCommand = new Command("backup")
12
+ .description("Back up configured Firestore collections")
13
+ .action(async () => {
14
+ try {
15
+ await runBackup();
16
+ }
17
+ catch (error) {
18
+ if (error instanceof ConfigError) {
19
+ console.error(error.message);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ throw error;
24
+ }
25
+ });
@@ -0,0 +1,55 @@
1
+ import { Command } from "commander";
2
+ import { ConfigError, loadConfig } from "../config/loadConfig.js";
3
+ import { GitError, assertInsideGitRepository, getHistoricalChanges, getWorkingTreeChanges, } from "../git/git.js";
4
+ function printSection(title, paths) {
5
+ console.log(`${title}:`);
6
+ console.log("");
7
+ for (const path of paths) {
8
+ console.log(`* ${path}`);
9
+ }
10
+ console.log("");
11
+ }
12
+ function printChanges(changes) {
13
+ printSection("Added", changes.added);
14
+ printSection("Modified", changes.modified);
15
+ printSection("Deleted", changes.deleted);
16
+ }
17
+ function normalizeLastWindow(last) {
18
+ const match = last.match(/^(\d+)([hdw])$/);
19
+ if (!match) {
20
+ return last;
21
+ }
22
+ const amount = match[1];
23
+ const unit = match[2];
24
+ if (unit === "h") {
25
+ return `${amount} hours ago`;
26
+ }
27
+ if (unit === "d") {
28
+ return `${amount} days ago`;
29
+ }
30
+ return `${amount} weeks ago`;
31
+ }
32
+ export function runChanges(last) {
33
+ const config = loadConfig();
34
+ assertInsideGitRepository();
35
+ const changes = last
36
+ ? getHistoricalChanges(config.outputDir, normalizeLastWindow(last))
37
+ : getWorkingTreeChanges(config.outputDir);
38
+ printChanges(changes);
39
+ }
40
+ export const changesCommand = new Command("changes")
41
+ .description("Show file-level Git changes under the configured backup directory")
42
+ .option("--last <window>", "Show committed changes since a time window, such as 24h")
43
+ .action((options) => {
44
+ try {
45
+ runChanges(options.last);
46
+ }
47
+ catch (error) {
48
+ if (error instanceof ConfigError || error instanceof GitError) {
49
+ console.error(error.message);
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+ throw error;
54
+ }
55
+ });
@@ -0,0 +1,30 @@
1
+ import { Command } from "commander";
2
+ import { ConfigError, loadConfig } from "../config/loadConfig.js";
3
+ import { GitError, assertInsideGitRepository, commitPath, hasChangesUnder, stagePath, } from "../git/git.js";
4
+ export function runCommit() {
5
+ const config = loadConfig();
6
+ assertInsideGitRepository();
7
+ if (!hasChangesUnder(config.outputDir)) {
8
+ console.log(`No changes found under ${config.outputDir}.`);
9
+ return;
10
+ }
11
+ stagePath(config.outputDir);
12
+ const message = `backup: ${new Date().toISOString()}`;
13
+ commitPath(message, config.outputDir);
14
+ console.log(`Created commit: ${message}`);
15
+ }
16
+ export const commitCommand = new Command("commit")
17
+ .description("Commit changes under the configured Firestore backup directory")
18
+ .action(() => {
19
+ try {
20
+ runCommit();
21
+ }
22
+ catch (error) {
23
+ if (error instanceof ConfigError || error instanceof GitError) {
24
+ console.error(error.message);
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ throw error;
29
+ }
30
+ });
@@ -0,0 +1,41 @@
1
+ import { Command } from "commander";
2
+ import { ConfigError, loadConfig } from "../config/loadConfig.js";
3
+ import { GitError, assertInsideGitRepository, getHistory, } from "../git/git.js";
4
+ import { normalizeHistoryPath } from "../paths/backupPaths.js";
5
+ function printHistory(entries) {
6
+ for (const entry of entries) {
7
+ const parts = [entry.shortSha, entry.date, entry.message];
8
+ if (entry.changedFileCount !== undefined) {
9
+ const label = entry.changedFileCount === 1 ? "file" : "files";
10
+ parts.push(`${entry.changedFileCount} ${label}`);
11
+ }
12
+ console.log(parts.join(" "));
13
+ }
14
+ }
15
+ export function runHistory(inputPath) {
16
+ const config = loadConfig();
17
+ const normalizedPath = normalizeHistoryPath(inputPath, config.outputDir);
18
+ assertInsideGitRepository();
19
+ const entries = getHistory(normalizedPath.path, normalizedPath.isCollection);
20
+ if (entries.length === 0) {
21
+ console.log(`No history found for ${normalizedPath.path}.`);
22
+ return;
23
+ }
24
+ printHistory(entries);
25
+ }
26
+ export const historyCommand = new Command("history")
27
+ .description("Show Git history for a backed-up document or collection")
28
+ .argument("<path>", "Logical path or backup file path")
29
+ .action((inputPath) => {
30
+ try {
31
+ runHistory(inputPath);
32
+ }
33
+ catch (error) {
34
+ if (error instanceof ConfigError || error instanceof GitError) {
35
+ console.error(error.message);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ throw error;
40
+ }
41
+ });
@@ -0,0 +1,19 @@
1
+ import { Command } from "commander";
2
+ import { writeFileSync, existsSync } from "node:fs";
3
+ const defaultConfig = {
4
+ projectId: "your-firebase-project-id",
5
+ serviceAccountPath: "./serviceAccountKey.json",
6
+ outputDir: "firestore-backups",
7
+ collections: [],
8
+ };
9
+ export const initCommand = new Command("init")
10
+ .description("Create a firevault.config.json file")
11
+ .action(() => {
12
+ const configPath = "firevault.config.json";
13
+ if (existsSync(configPath)) {
14
+ console.log("firevault.config.json already exists.");
15
+ return;
16
+ }
17
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
18
+ console.log("Created firevault.config.json");
19
+ });
@@ -0,0 +1,100 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Command } from "commander";
4
+ import { ConfigError, loadConfig } from "../config/loadConfig.js";
5
+ import { FirestoreError, writeDocument } from "../firestore/writeDocument.js";
6
+ import { GitError, assertInsideGitRepository, showFileAtCommit, } from "../git/git.js";
7
+ import { normalizeDocumentPath, normalizeSlashes, } from "../paths/backupPaths.js";
8
+ import { buildLineDiff } from "./restorePreview.js";
9
+ function getFirestoreTarget(inputPath, outputDir) {
10
+ const normalizedInput = normalizeSlashes(inputPath);
11
+ const normalizedOutputDir = normalizeSlashes(outputDir);
12
+ if (normalizedInput === normalizedOutputDir ||
13
+ normalizedInput === normalizedOutputDir.split("/")[0] ||
14
+ (!normalizedInput.includes("/") && !normalizedInput.endsWith(".json"))) {
15
+ throw new ConfigError("Collection restore is not supported yet. Provide a document path such as users/abc123.");
16
+ }
17
+ const backupPath = normalizeDocumentPath(inputPath, outputDir);
18
+ const relativePath = path.posix.relative(normalizedOutputDir, backupPath);
19
+ const parts = relativePath.split("/");
20
+ if (parts.length !== 2 || !parts[1].endsWith(".json")) {
21
+ throw new ConfigError("Only top-level document restore is supported. Provide a path such as users/abc123.");
22
+ }
23
+ return {
24
+ backupPath,
25
+ collection: parts[0],
26
+ documentId: parts[1].slice(0, -".json".length),
27
+ };
28
+ }
29
+ function parseRestoreJson(content, backupPath) {
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(content);
33
+ }
34
+ catch {
35
+ throw new ConfigError(`Malformed JSON at source commit for ${backupPath}.`);
36
+ }
37
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
38
+ throw new ConfigError(`Malformed JSON at source commit for ${backupPath}: expected a JSON object.`);
39
+ }
40
+ return parsed;
41
+ }
42
+ function printFirestorePreview(target, sourceCommit, currentExists, diff) {
43
+ console.log(`Target backup path: ${target.backupPath}`);
44
+ console.log(`Firestore collection: ${target.collection}`);
45
+ console.log(`Firestore document ID: ${target.documentId}`);
46
+ console.log(`Source commit: ${sourceCommit}`);
47
+ console.log(`Current local backup file exists: ${currentExists ? "yes" : "no"}`);
48
+ console.log("");
49
+ console.log("Diff:");
50
+ console.log("");
51
+ if (diff.length === 0) {
52
+ console.log("No changes.");
53
+ return;
54
+ }
55
+ for (const line of diff) {
56
+ console.log(line);
57
+ }
58
+ }
59
+ export async function runRestoreFirestore(inputPath, options) {
60
+ if (!options.from) {
61
+ throw new ConfigError("Missing required option: --from <commit>");
62
+ }
63
+ if (!options.confirm) {
64
+ throw new ConfigError("Missing required option: --confirm. Run restore-preview first, then rerun with --confirm to overwrite the Firestore document.");
65
+ }
66
+ const config = loadConfig();
67
+ const target = getFirestoreTarget(inputPath, config.outputDir);
68
+ assertInsideGitRepository();
69
+ const restoredContent = showFileAtCommit(options.from, target.backupPath);
70
+ const restoredData = parseRestoreJson(restoredContent, target.backupPath);
71
+ const currentExists = existsSync(target.backupPath);
72
+ const currentContent = currentExists
73
+ ? readFileSync(target.backupPath, "utf-8")
74
+ : undefined;
75
+ const diff = buildLineDiff(currentContent, restoredContent);
76
+ printFirestorePreview(target, options.from, currentExists, diff);
77
+ await writeDocument(target.collection, target.documentId, restoredData);
78
+ console.log("");
79
+ console.log(`Restored Firestore document: ${target.collection}/${target.documentId}`);
80
+ }
81
+ export const restoreFirestoreCommand = new Command("restore-firestore")
82
+ .description("Restore a backed-up document from Git directly into Firestore")
83
+ .argument("<path>", "Logical document path or backup file path")
84
+ .option("--from <commit>", "Git commit to restore from")
85
+ .option("--confirm", "Confirm overwriting the Firestore document")
86
+ .action(async (inputPath, options) => {
87
+ try {
88
+ await runRestoreFirestore(inputPath, options);
89
+ }
90
+ catch (error) {
91
+ if (error instanceof ConfigError ||
92
+ error instanceof GitError ||
93
+ error instanceof FirestoreError) {
94
+ console.error(error.message);
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ throw error;
99
+ }
100
+ });