firevault 0.1.1-beta.3 → 0.2.0-beta.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/CHANGELOG.md +19 -1
- package/README.md +89 -15
- package/dist/commands/backup.js +1 -1
- package/dist/commands/changes.js +3 -3
- package/dist/commands/commit.js +4 -4
- package/dist/commands/doctor.js +225 -0
- package/dist/commands/history.js +2 -2
- package/dist/commands/init.js +50 -30
- package/dist/commands/restoreFirestore.js +6 -5
- package/dist/commands/restoreLocal.js +8 -7
- package/dist/commands/restorePreview.js +6 -5
- package/dist/commands/setupGithubAction.js +120 -0
- package/dist/commands/status.js +124 -0
- package/dist/config/loadConfig.js +37 -9
- package/dist/firestore/firebase.js +2 -2
- package/dist/git/git.js +89 -23
- package/dist/index.js +7 -1
- package/dist/init/detectServiceAccount.js +21 -2
- package/docs/architecture.md +51 -27
- package/docs/doctor-design.md +418 -0
- package/docs/github-actions-design.md +317 -0
- package/docs/roadmap.md +8 -1
- package/docs/status-design.md +218 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.2.0-beta.1 - Unreleased
|
|
4
|
+
|
|
5
|
+
- Added `firevault status` for compact local recovery health checks.
|
|
6
|
+
- Added `firevault doctor` for actionable local setup validation.
|
|
7
|
+
- Added `firevault setup-github-action` to generate a scheduled GitHub Actions workflow for offsite snapshots.
|
|
8
|
+
- Added local workflow detection for generated GitHub Actions snapshot automation.
|
|
9
|
+
|
|
10
|
+
## 0.2.0-beta.0
|
|
11
|
+
|
|
12
|
+
Breaking prerelease change:
|
|
13
|
+
|
|
14
|
+
- Firevault now uses `.firevault/config.json` and a dedicated `.firevault` recovery workspace.
|
|
15
|
+
- Firevault backup history now lives in the `.firevault` Git repository instead of the parent app repo.
|
|
16
|
+
- Config-relative paths now resolve from `.firevault/`.
|
|
17
|
+
- Operational commands discover the nearest `.firevault/config.json` from the app root or from inside `.firevault/`.
|
|
18
|
+
- `firevault init` no longer creates root `firevault.config.json`.
|
|
19
|
+
- `firestore-backups/` is no longer ignored inside `.firevault/` by default.
|
|
20
|
+
|
|
21
|
+
## 0.1.1-beta.1
|
|
4
22
|
|
|
5
23
|
- Added guided `firevault init` setup with prompts for project ID, service account path, output directory, and collections.
|
|
6
24
|
- Added init Git safety checks, `--force`, and `--yes`.
|
package/README.md
CHANGED
|
@@ -25,10 +25,12 @@ Current scope:
|
|
|
25
25
|
Current export shape:
|
|
26
26
|
|
|
27
27
|
```txt
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
.firevault/
|
|
29
|
+
config.json
|
|
30
|
+
firestore-backups/
|
|
31
|
+
users/
|
|
32
|
+
abc123.json
|
|
33
|
+
def456.json
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
The immediate priority is trustworthy document-level recovery: clear previews, explicit confirmation, and no broad destructive restore flows.
|
|
@@ -36,10 +38,11 @@ The immediate priority is trustworthy document-level recovery: clear previews, e
|
|
|
36
38
|
## Quick Start
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
npm install -g firevault
|
|
41
|
+
npm install -g firevault@next
|
|
42
|
+
cd my-app
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
Firevault 0.2 uses `.firevault/config.json` and a dedicated `.firevault` recovery workspace. The app repo stays focused on application source code; `.firevault/` contains Firevault config, backup JSON, credentials, and its own Git history.
|
|
43
46
|
|
|
44
47
|
Run guided setup:
|
|
45
48
|
|
|
@@ -47,7 +50,7 @@ Run guided setup:
|
|
|
47
50
|
firevault init
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
`firevault init` asks for your Firebase project ID, service account path, output directory, and collections. It
|
|
53
|
+
`firevault init` asks for your Firebase project ID, service account path, output directory, and collections. It creates `.firevault/`, writes `.firevault/config.json`, writes `.firevault/.gitignore`, can initialize Git inside `.firevault/`, and adds `.firevault/` to the parent app repo `.gitignore` when the parent is a Git repo.
|
|
51
54
|
|
|
52
55
|
During setup, Firevault looks for likely Firebase project IDs in local files such as `.env.local`, `.env.development`, `firebase.json`, and common Firebase config files. Detection is best-effort and transparent: if Firevault finds candidates, it shows where they came from and lets you accept one or enter a value manually.
|
|
53
56
|
|
|
@@ -62,14 +65,14 @@ https://console.firebase.google.com/project/your-project-id/settings/serviceacco
|
|
|
62
65
|
|
|
63
66
|
Download the JSON key and save it as:
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
.firevault/serviceAccountKey.json
|
|
66
69
|
```
|
|
67
70
|
|
|
68
71
|
Firevault does not create service accounts, open a browser, run `gcloud`, or authenticate against Firebase during setup.
|
|
69
72
|
|
|
70
73
|
If the selected service account file already exists, Firevault can optionally connect to Firestore and list top-level collections so you can choose which ones to back up. If the file is missing or Firebase access fails, init continues and you can enter collections manually.
|
|
71
74
|
|
|
72
|
-
Generated
|
|
75
|
+
Generated `.firevault/config.json`:
|
|
73
76
|
|
|
74
77
|
```json
|
|
75
78
|
{
|
|
@@ -86,6 +89,8 @@ Take a snapshot:
|
|
|
86
89
|
firevault snapshot
|
|
87
90
|
```
|
|
88
91
|
|
|
92
|
+
Operational commands discover the nearest `.firevault/config.json`, so they work from the app root or from inside `.firevault/`.
|
|
93
|
+
|
|
89
94
|
Example output:
|
|
90
95
|
|
|
91
96
|
```txt
|
|
@@ -179,6 +184,12 @@ firevault snapshot
|
|
|
179
184
|
|
|
180
185
|
Firevault operates against an existing Firebase project using a service account.
|
|
181
186
|
|
|
187
|
+
Expected config path:
|
|
188
|
+
|
|
189
|
+
```txt
|
|
190
|
+
.firevault/config.json
|
|
191
|
+
```
|
|
192
|
+
|
|
182
193
|
Expected config shape:
|
|
183
194
|
|
|
184
195
|
```json
|
|
@@ -192,19 +203,28 @@ Expected config shape:
|
|
|
192
203
|
|
|
193
204
|
Notes:
|
|
194
205
|
|
|
195
|
-
-
|
|
196
|
-
- `
|
|
206
|
+
- paths are relative to `.firevault/`,
|
|
207
|
+
- `serviceAccountPath` points to a local Firebase service account JSON file,
|
|
208
|
+
- `outputDir` is where Firestore documents are written inside `.firevault/`,
|
|
197
209
|
- `collections` controls which top-level Firestore collections are exported.
|
|
198
210
|
- Service account files must not be committed.
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
Parent app repo `.gitignore`:
|
|
213
|
+
|
|
214
|
+
```gitignore
|
|
215
|
+
.firevault/
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`.firevault/.gitignore`:
|
|
201
219
|
|
|
202
220
|
```gitignore
|
|
203
221
|
serviceAccountKey.json
|
|
204
|
-
firestore-
|
|
222
|
+
firestore-debug.log
|
|
223
|
+
.env
|
|
224
|
+
.env.*
|
|
205
225
|
```
|
|
206
226
|
|
|
207
|
-
`firevault init` adds these safety entries automatically. `firestore-backups/` is
|
|
227
|
+
`firevault init` adds these safety entries automatically. In the 0.2 workspace model, `firestore-backups/` is not ignored inside `.firevault/` because the `.firevault` Git repo exists to track backup history.
|
|
208
228
|
|
|
209
229
|
## Commands
|
|
210
230
|
|
|
@@ -213,6 +233,9 @@ firevault init
|
|
|
213
233
|
firevault backup
|
|
214
234
|
firevault commit
|
|
215
235
|
firevault snapshot
|
|
236
|
+
firevault status
|
|
237
|
+
firevault doctor
|
|
238
|
+
firevault setup-github-action
|
|
216
239
|
firevault changes
|
|
217
240
|
firevault changes --last 24h
|
|
218
241
|
firevault history users/abc123
|
|
@@ -262,7 +285,7 @@ JSON output is stable:
|
|
|
262
285
|
|
|
263
286
|
`firevault backup` exports configured Firestore collections to deterministic local JSON files. It does not stage or commit anything.
|
|
264
287
|
|
|
265
|
-
`firevault commit`
|
|
288
|
+
`firevault commit` commits inside the `.firevault` Git repository.
|
|
266
289
|
|
|
267
290
|
Behavior:
|
|
268
291
|
|
|
@@ -270,6 +293,7 @@ Behavior:
|
|
|
270
293
|
- exits successfully if no backup changes exist,
|
|
271
294
|
- stages only the configured `outputDir`,
|
|
272
295
|
- creates a local commit with message `backup: <ISO timestamp>`,
|
|
296
|
+
- never stages app source files from the parent repo,
|
|
273
297
|
- never pushes.
|
|
274
298
|
|
|
275
299
|
Keep `serviceAccountKey.json` ignored so credentials cannot be committed by this workflow or by manual Git usage.
|
|
@@ -282,6 +306,56 @@ Keep `serviceAccountKey.json` ignored so credentials cannot be committed by this
|
|
|
282
306
|
- exits successfully when backup succeeds but no Git changes exist,
|
|
283
307
|
- never pushes.
|
|
284
308
|
|
|
309
|
+
`firevault status` shows a compact local recovery health overview. It does not contact Firebase, call GitHub APIs, fetch from remotes, write files, stage, commit, or push.
|
|
310
|
+
|
|
311
|
+
Example output:
|
|
312
|
+
|
|
313
|
+
```txt
|
|
314
|
+
Firevault status
|
|
315
|
+
|
|
316
|
+
Workspace:
|
|
317
|
+
Path: .firevault
|
|
318
|
+
Config: OK
|
|
319
|
+
|
|
320
|
+
Firestore:
|
|
321
|
+
Project: my-project
|
|
322
|
+
Collections configured: 4
|
|
323
|
+
|
|
324
|
+
Backups:
|
|
325
|
+
Output directory: firestore-backups
|
|
326
|
+
Output exists: yes
|
|
327
|
+
Last snapshot: 2026-05-17T14:22:10Z
|
|
328
|
+
Uncommitted backup changes: none
|
|
329
|
+
|
|
330
|
+
Git:
|
|
331
|
+
Repository: OK
|
|
332
|
+
Branch: main
|
|
333
|
+
Working tree: clean
|
|
334
|
+
Remote origin: configured
|
|
335
|
+
Remote sync: unknown
|
|
336
|
+
|
|
337
|
+
Automation:
|
|
338
|
+
GitHub Actions workflow: not configured
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`firevault doctor` validates the local Firevault setup and prints actionable fixes. It checks workspace discovery, config validity, service account file presence, backup output state, `.firevault` Git setup, remote origin, GitHub Actions workflow contents, `.gitignore` safety, tracked secret-looking files, backup directory trackability, and working tree state.
|
|
342
|
+
|
|
343
|
+
Doctor is local-only. It does not contact Firebase, call GitHub APIs, write files, stage, commit, push, or print secrets.
|
|
344
|
+
|
|
345
|
+
Exit codes:
|
|
346
|
+
|
|
347
|
+
- `0`: all checks OK,
|
|
348
|
+
- `1`: warnings only,
|
|
349
|
+
- `2`: one or more failures.
|
|
350
|
+
|
|
351
|
+
`firevault setup-github-action` creates a local scheduled workflow at `.firevault/.github/workflows/firevault-snapshot.yml`.
|
|
352
|
+
|
|
353
|
+
The workflow is intended for a private GitHub repository containing the `.firevault` recovery workspace. It runs daily by default, supports manual dispatch, installs `firevault@next`, writes the Firebase service account JSON from the GitHub secret `FIREVAULT_SERVICE_ACCOUNT_JSON`, runs `firevault snapshot`, and pushes only when a backup commit was created.
|
|
354
|
+
|
|
355
|
+
This command only writes the workflow file. It does not create GitHub repositories, call GitHub APIs, create secrets, push, stage, commit, store credentials, or install GitHub CLI dependencies.
|
|
356
|
+
|
|
357
|
+
After generation, push the `.firevault` repo to GitHub, create the `FIREVAULT_SERVICE_ACCOUNT_JSON` repository secret with the full service account JSON, review the workflow, and commit it yourself.
|
|
358
|
+
|
|
285
359
|
`firevault changes` shows a file-level Git summary for the configured `outputDir` only:
|
|
286
360
|
|
|
287
361
|
```txt
|
package/dist/commands/backup.js
CHANGED
|
@@ -4,7 +4,7 @@ import { exportCollection } from "../firestore/exportFirestore.js";
|
|
|
4
4
|
export async function runBackup() {
|
|
5
5
|
const config = loadConfig();
|
|
6
6
|
for (const collection of config.collections) {
|
|
7
|
-
await exportCollection(config.
|
|
7
|
+
await exportCollection(config.outputDirPath, collection);
|
|
8
8
|
}
|
|
9
9
|
console.log("Backup complete.");
|
|
10
10
|
}
|
package/dist/commands/changes.js
CHANGED
|
@@ -31,10 +31,10 @@ function normalizeLastWindow(last) {
|
|
|
31
31
|
}
|
|
32
32
|
export function runChanges(last) {
|
|
33
33
|
const config = loadConfig();
|
|
34
|
-
assertInsideGitRepository();
|
|
34
|
+
assertInsideGitRepository(config.workspaceRoot);
|
|
35
35
|
const changes = last
|
|
36
|
-
? getHistoricalChanges(config.outputDir, normalizeLastWindow(last))
|
|
37
|
-
: getWorkingTreeChanges(config.outputDir);
|
|
36
|
+
? getHistoricalChanges(config.outputDir, normalizeLastWindow(last), config.workspaceRoot)
|
|
37
|
+
: getWorkingTreeChanges(config.outputDir, config.workspaceRoot);
|
|
38
38
|
printChanges(changes);
|
|
39
39
|
}
|
|
40
40
|
export const changesCommand = new Command("changes")
|
package/dist/commands/commit.js
CHANGED
|
@@ -3,14 +3,14 @@ import { ConfigError, loadConfig } from "../config/loadConfig.js";
|
|
|
3
3
|
import { GitError, assertInsideGitRepository, commitPath, hasChangesUnder, stagePath, } from "../git/git.js";
|
|
4
4
|
export function runCommit() {
|
|
5
5
|
const config = loadConfig();
|
|
6
|
-
assertInsideGitRepository();
|
|
7
|
-
if (!hasChangesUnder(config.outputDir)) {
|
|
6
|
+
assertInsideGitRepository(config.workspaceRoot);
|
|
7
|
+
if (!hasChangesUnder(config.outputDir, config.workspaceRoot)) {
|
|
8
8
|
console.log(`No changes found under ${config.outputDir}.`);
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
stagePath(config.outputDir);
|
|
11
|
+
stagePath(config.outputDir, config.workspaceRoot);
|
|
12
12
|
const message = `backup: ${new Date().toISOString()}`;
|
|
13
|
-
commitPath(message, config.outputDir);
|
|
13
|
+
commitPath(message, config.outputDir, config.workspaceRoot);
|
|
14
14
|
console.log(`Created commit: ${message}`);
|
|
15
15
|
}
|
|
16
16
|
export const commitCommand = new Command("commit")
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { ConfigError, findWorkspaceRoot, loadConfig } from "../config/loadConfig.js";
|
|
5
|
+
import { getRemoteUrl, getTrackedFiles, hasChangesUnder, hasWorkingTreeChanges, isInsideGitRepository, isPathIgnored, } from "../git/git.js";
|
|
6
|
+
function findNearestWorkspaceDir(startDir = process.cwd()) {
|
|
7
|
+
let currentDir = path.resolve(startDir);
|
|
8
|
+
while (true) {
|
|
9
|
+
const candidate = path.join(currentDir, ".firevault");
|
|
10
|
+
if (existsSync(candidate)) {
|
|
11
|
+
return candidate;
|
|
12
|
+
}
|
|
13
|
+
const parent = path.dirname(currentDir);
|
|
14
|
+
if (parent === currentDir) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
currentDir = parent;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function addCheck(checks, severity, label, fix) {
|
|
21
|
+
checks.push({ severity, label, fix });
|
|
22
|
+
}
|
|
23
|
+
function displayPathFromApp(workspaceRoot, targetPath) {
|
|
24
|
+
return path.relative(path.dirname(workspaceRoot), targetPath).replaceAll("\\", "/");
|
|
25
|
+
}
|
|
26
|
+
function isInside(parent, child) {
|
|
27
|
+
const relativePath = path.relative(parent, child);
|
|
28
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
29
|
+
}
|
|
30
|
+
function gitignoreContains(workspaceRoot, value) {
|
|
31
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
32
|
+
if (!existsSync(gitignorePath)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const normalizedValue = value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
36
|
+
const basename = path.posix.basename(normalizedValue);
|
|
37
|
+
const lines = readFileSync(gitignorePath, "utf-8")
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map((line) => line.trim().replace(/\/+$/, ""))
|
|
40
|
+
.filter((line) => line !== "" && !line.startsWith("#"));
|
|
41
|
+
return lines.includes(normalizedValue) || lines.includes(`./${normalizedValue}`) || lines.includes(basename);
|
|
42
|
+
}
|
|
43
|
+
function workflowChecks(checks, workspaceRoot) {
|
|
44
|
+
const workflowPath = path.join(workspaceRoot, ".github", "workflows", "firevault-snapshot.yml");
|
|
45
|
+
if (!existsSync(workflowPath)) {
|
|
46
|
+
addCheck(checks, "WARN", "GitHub Actions workflow missing", "Run `firevault setup-github-action`");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const workflow = readFileSync(workflowPath, "utf-8");
|
|
50
|
+
const missing = [];
|
|
51
|
+
if (!workflow.includes("schedule:") || !workflow.includes("cron:")) {
|
|
52
|
+
missing.push("schedule trigger");
|
|
53
|
+
}
|
|
54
|
+
if (!workflow.includes("workflow_dispatch:")) {
|
|
55
|
+
missing.push("workflow_dispatch");
|
|
56
|
+
}
|
|
57
|
+
if (!workflow.includes("FIREVAULT_SERVICE_ACCOUNT_JSON")) {
|
|
58
|
+
missing.push("FIREVAULT_SERVICE_ACCOUNT_JSON");
|
|
59
|
+
}
|
|
60
|
+
if (!workflow.includes("firevault snapshot")) {
|
|
61
|
+
missing.push("firevault snapshot");
|
|
62
|
+
}
|
|
63
|
+
if (missing.length === 0) {
|
|
64
|
+
addCheck(checks, "OK", "GitHub Actions workflow configured");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
addCheck(checks, "WARN", `GitHub Actions workflow incomplete: missing ${missing.join(", ")}`, "Review .firevault/.github/workflows/firevault-snapshot.yml or rerun `firevault setup-github-action --force`");
|
|
68
|
+
}
|
|
69
|
+
function isObviousSecretPath(filePath, serviceAccountPath) {
|
|
70
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
71
|
+
const basename = path.posix.basename(normalized);
|
|
72
|
+
return (normalized === serviceAccountPath ||
|
|
73
|
+
basename === "serviceAccountKey.json" ||
|
|
74
|
+
basename === "service-account.json" ||
|
|
75
|
+
basename === "firebase-service-account.json" ||
|
|
76
|
+
normalized === "credentials/firebase.json" ||
|
|
77
|
+
basename === ".env" ||
|
|
78
|
+
basename.startsWith(".env.") ||
|
|
79
|
+
basename.endsWith(".pem") ||
|
|
80
|
+
basename.endsWith(".key"));
|
|
81
|
+
}
|
|
82
|
+
function printChecks(checks) {
|
|
83
|
+
console.log("Firevault doctor");
|
|
84
|
+
console.log("");
|
|
85
|
+
for (const check of checks) {
|
|
86
|
+
console.log(`${check.severity.padEnd(5)} ${check.label}`);
|
|
87
|
+
}
|
|
88
|
+
const fixes = checks
|
|
89
|
+
.map((check) => check.fix)
|
|
90
|
+
.filter((fix) => Boolean(fix));
|
|
91
|
+
const uniqueFixes = [...new Set(fixes)];
|
|
92
|
+
if (uniqueFixes.length > 0) {
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log("Next fixes:");
|
|
95
|
+
uniqueFixes.forEach((fix, index) => {
|
|
96
|
+
const [firstLine, ...rest] = fix.split("\n");
|
|
97
|
+
console.log(`${index + 1}. ${firstLine}`);
|
|
98
|
+
for (const line of rest) {
|
|
99
|
+
console.log(` ${line}`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function runDoctor() {
|
|
105
|
+
const checks = [];
|
|
106
|
+
const workspaceRoot = findWorkspaceRoot() ?? findNearestWorkspaceDir();
|
|
107
|
+
if (!workspaceRoot) {
|
|
108
|
+
addCheck(checks, "FAIL", "Workspace not found", "Run `firevault init`");
|
|
109
|
+
addCheck(checks, "FAIL", "Config missing", "Run `firevault init`");
|
|
110
|
+
printChecks(checks);
|
|
111
|
+
process.exitCode = 2;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
addCheck(checks, "OK", "Workspace found");
|
|
115
|
+
const configPath = path.join(workspaceRoot, "config.json");
|
|
116
|
+
if (!existsSync(configPath)) {
|
|
117
|
+
addCheck(checks, "FAIL", "Config missing", "Run `firevault init` or create .firevault/config.json");
|
|
118
|
+
printChecks(checks);
|
|
119
|
+
process.exitCode = 2;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let config;
|
|
123
|
+
try {
|
|
124
|
+
config = loadConfig();
|
|
125
|
+
addCheck(checks, "OK", "Config valid");
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof ConfigError ? error.message : "Config invalid";
|
|
129
|
+
addCheck(checks, "FAIL", message, "Edit .firevault/config.json or rerun `firevault init --force`");
|
|
130
|
+
printChecks(checks);
|
|
131
|
+
process.exitCode = 2;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const serviceAccountDisplayPath = displayPathFromApp(config.workspaceRoot, config.serviceAccountPathAbsolute);
|
|
135
|
+
if (!isInside(config.workspaceRoot, config.serviceAccountPathAbsolute)) {
|
|
136
|
+
addCheck(checks, "FAIL", "Service account path is outside .firevault", "Set serviceAccountPath to a path inside .firevault, such as ./serviceAccountKey.json");
|
|
137
|
+
}
|
|
138
|
+
else if (existsSync(config.serviceAccountPathAbsolute)) {
|
|
139
|
+
addCheck(checks, "OK", "Service account file present");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
addCheck(checks, "FAIL", "Service account file missing", `Save your Firebase service account JSON to:\n${serviceAccountDisplayPath}`);
|
|
143
|
+
}
|
|
144
|
+
if (!isInside(config.workspaceRoot, config.outputDirPath)) {
|
|
145
|
+
addCheck(checks, "FAIL", "Backup output path is outside .firevault", "Set outputDir to a path inside .firevault, such as firestore-backups");
|
|
146
|
+
}
|
|
147
|
+
else if (existsSync(config.outputDirPath)) {
|
|
148
|
+
addCheck(checks, "OK", "Backup output directory exists");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
addCheck(checks, "WARN", "Backup output directory has not been created yet", "Run `firevault snapshot`");
|
|
152
|
+
}
|
|
153
|
+
const workspaceIsGitRepo = isInsideGitRepository(config.workspaceRoot);
|
|
154
|
+
if (workspaceIsGitRepo) {
|
|
155
|
+
addCheck(checks, "OK", ".firevault Git repository found");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
addCheck(checks, "FAIL", ".firevault is not a Git repository", "git -C .firevault init");
|
|
159
|
+
}
|
|
160
|
+
if (workspaceIsGitRepo && getRemoteUrl("origin", config.workspaceRoot)) {
|
|
161
|
+
addCheck(checks, "OK", "Git remote origin configured");
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
addCheck(checks, "WARN", "No Git remote origin configured", "git -C .firevault remote add origin <private-repo-url>");
|
|
165
|
+
}
|
|
166
|
+
workflowChecks(checks, config.workspaceRoot);
|
|
167
|
+
const serviceAccountIgnored = workspaceIsGitRepo
|
|
168
|
+
? isPathIgnored(config.serviceAccountPath, config.workspaceRoot)
|
|
169
|
+
: undefined;
|
|
170
|
+
if (serviceAccountIgnored === true || gitignoreContains(config.workspaceRoot, config.serviceAccountPath)) {
|
|
171
|
+
addCheck(checks, "OK", "Service account file ignored");
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
addCheck(checks, "FAIL", "Service account file is not ignored", `Add ${config.serviceAccountPath} to .firevault/.gitignore`);
|
|
175
|
+
}
|
|
176
|
+
const appRoot = path.dirname(config.workspaceRoot);
|
|
177
|
+
if (!isInsideGitRepository(appRoot)) {
|
|
178
|
+
addCheck(checks, "WARN", "Parent app directory is not a Git repository");
|
|
179
|
+
}
|
|
180
|
+
else if (isPathIgnored(".firevault", appRoot)) {
|
|
181
|
+
addCheck(checks, "OK", "Parent app repo ignores .firevault/");
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
addCheck(checks, "FAIL", "Parent app repo does not ignore .firevault/", "Add .firevault/ to .gitignore");
|
|
185
|
+
}
|
|
186
|
+
if (workspaceIsGitRepo) {
|
|
187
|
+
const trackedFiles = getTrackedFiles(config.workspaceRoot) ?? [];
|
|
188
|
+
const trackedSecretFiles = trackedFiles.filter((filePath) => isObviousSecretPath(filePath, config.serviceAccountPath));
|
|
189
|
+
if (trackedSecretFiles.length === 0) {
|
|
190
|
+
addCheck(checks, "OK", "No obvious secret files tracked");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
addCheck(checks, "FAIL", `Possible secret files tracked: ${trackedSecretFiles.join(", ")}`, "Remove tracked secret files from Git history and rotate exposed credentials.");
|
|
194
|
+
}
|
|
195
|
+
if (isPathIgnored(config.outputDir, config.workspaceRoot) ||
|
|
196
|
+
gitignoreContains(config.workspaceRoot, config.outputDir)) {
|
|
197
|
+
addCheck(checks, "FAIL", "Backup directory is ignored", `Remove ${config.outputDir}/ from .firevault/.gitignore`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
addCheck(checks, "OK", "Backup directory is trackable");
|
|
201
|
+
}
|
|
202
|
+
if (hasWorkingTreeChanges(config.workspaceRoot)) {
|
|
203
|
+
addCheck(checks, "WARN", "Working tree has uncommitted changes", "Review changes, then run `firevault commit` when appropriate");
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
addCheck(checks, "OK", "Working tree clean");
|
|
207
|
+
}
|
|
208
|
+
if (hasChangesUnder(config.outputDir, config.workspaceRoot)) {
|
|
209
|
+
addCheck(checks, "WARN", "Backup output has uncommitted changes", "Run `firevault commit` after reviewing changes");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
printChecks(checks);
|
|
213
|
+
if (checks.some((check) => check.severity === "FAIL")) {
|
|
214
|
+
process.exitCode = 2;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (checks.some((check) => check.severity === "WARN")) {
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export const doctorCommand = new Command("doctor")
|
|
222
|
+
.description("Validate local Firevault recovery setup")
|
|
223
|
+
.action(() => {
|
|
224
|
+
runDoctor();
|
|
225
|
+
});
|
package/dist/commands/history.js
CHANGED
|
@@ -15,8 +15,8 @@ function printHistory(entries) {
|
|
|
15
15
|
export function runHistory(inputPath) {
|
|
16
16
|
const config = loadConfig();
|
|
17
17
|
const normalizedPath = normalizeHistoryPath(inputPath, config.outputDir);
|
|
18
|
-
assertInsideGitRepository();
|
|
19
|
-
const entries = getHistory(normalizedPath.path, normalizedPath.isCollection);
|
|
18
|
+
assertInsideGitRepository(config.workspaceRoot);
|
|
19
|
+
const entries = getHistory(normalizedPath.path, normalizedPath.isCollection, config.workspaceRoot);
|
|
20
20
|
if (entries.length === 0) {
|
|
21
21
|
console.log(`No history found for ${normalizedPath.path}.`);
|
|
22
22
|
return;
|