firevault 0.1.1-beta.0 → 0.1.1-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 CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 - Unreleased
3
+ ## 0.1.1-beta.1 - Unreleased
4
+
5
+ - Added guided `firevault init` setup with prompts for project ID, service account path, output directory, and collections.
6
+ - Added init Git safety checks, `--force`, and `--yes`.
7
+ - Added safe `.gitignore` updates for service account keys, backup output, and emulator logs.
8
+ - Ensured Firevault can still explicitly commit the configured backup directory even when it is ignored by default.
9
+
10
+ ## 0.1.0
4
11
 
5
12
  Initial prerelease candidate for Firevault, an undo button for Firestore.
6
13
 
package/README.md CHANGED
@@ -41,7 +41,15 @@ npm install -g firevault
41
41
 
42
42
  Create a Firebase service account key for your Firestore project, save it as `serviceAccountKey.json`, and keep it out of Git.
43
43
 
44
- Create `firevault.config.json`:
44
+ Run guided setup:
45
+
46
+ ```bash
47
+ firevault init
48
+ ```
49
+
50
+ `firevault init` asks for your Firebase project ID, service account path, output directory, and collections. It also checks Git state before writing files and appends safety entries to `.gitignore`.
51
+
52
+ Generated `firevault.config.json`:
45
53
 
46
54
  ```json
47
55
  {
@@ -176,6 +184,8 @@ serviceAccountKey.json
176
184
  firestore-backups/
177
185
  ```
178
186
 
187
+ `firevault init` adds these safety entries automatically. `firestore-backups/` is ignored by default so exported Firestore data is not committed accidentally with normal Git commands. Firevault can still commit the configured backup directory explicitly through `firevault commit` or `firevault snapshot`, and it stages only that directory.
188
+
179
189
  ## Commands
180
190
 
181
191
  ```bash
@@ -191,6 +201,8 @@ firevault restore-local users/abc123 --from HEAD~3 --confirm
191
201
  firevault restore-firestore users/abc123 --from HEAD~3 --confirm
192
202
  ```
193
203
 
204
+ `firevault init --yes` uses default values for non-interactive setup. `firevault init --force` allows setup with a dirty Git working tree and overwrites an existing config after warning.
205
+
194
206
  ## Local Development
195
207
 
196
208
  Install dependencies and run commands through the TypeScript entrypoint:
@@ -1,19 +1,142 @@
1
1
  import { Command } from "commander";
2
- import { writeFileSync, existsSync } from "node:fs";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { appendFileSync, existsSync, readFileSync, writeFileSync, } from "node:fs";
5
+ import { GitError, hasWorkingTreeChanges, initGitRepository, isInsideGitRepository, } from "../git/git.js";
3
6
  const defaultConfig = {
4
7
  projectId: "your-firebase-project-id",
5
8
  serviceAccountPath: "./serviceAccountKey.json",
6
9
  outputDir: "firestore-backups",
7
- collections: [],
10
+ collections: ["users"],
8
11
  };
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.");
12
+ function validateConfig(config) {
13
+ if (config.projectId.trim() === "") {
14
+ throw new Error("Firebase project ID is required.");
15
+ }
16
+ if (config.serviceAccountPath.trim() === "") {
17
+ throw new Error("Service account path is required.");
18
+ }
19
+ if (config.outputDir.trim() === "") {
20
+ throw new Error("Output directory is required.");
21
+ }
22
+ if (config.collections.length === 0) {
23
+ throw new Error("At least one collection is required.");
24
+ }
25
+ if (config.collections.some((collection) => collection.trim() === "")) {
26
+ throw new Error("Collection names must not be empty.");
27
+ }
28
+ }
29
+ function parseCollections(value) {
30
+ return value
31
+ .split(",")
32
+ .map((collection) => collection.trim());
33
+ }
34
+ async function promptForConfig(options) {
35
+ if (options.yes) {
36
+ return defaultConfig;
37
+ }
38
+ const rl = createInterface({ input, output });
39
+ try {
40
+ const projectId = (await rl.question("Firebase project ID: ")).trim();
41
+ const serviceAccountPath = (await rl.question(`Service account path (${defaultConfig.serviceAccountPath}): `)).trim() || defaultConfig.serviceAccountPath;
42
+ const outputDir = (await rl.question(`Output directory (${defaultConfig.outputDir}): `)).trim() || defaultConfig.outputDir;
43
+ const collectionsInput = (await rl.question("Collections, comma-separated: ")).trim();
44
+ return {
45
+ projectId,
46
+ serviceAccountPath,
47
+ outputDir,
48
+ collections: parseCollections(collectionsInput),
49
+ };
50
+ }
51
+ finally {
52
+ rl.close();
53
+ }
54
+ }
55
+ async function promptForGitInit(options) {
56
+ if (options.yes) {
57
+ return true;
58
+ }
59
+ const rl = createInterface({ input, output });
60
+ try {
61
+ const answer = (await rl.question("This directory is not a Git repository. Run git init? (Y/n): "))
62
+ .trim()
63
+ .toLowerCase();
64
+ return answer === "" || answer === "y" || answer === "yes";
65
+ }
66
+ finally {
67
+ rl.close();
68
+ }
69
+ }
70
+ function ensureGitignoreEntries(entries) {
71
+ const gitignorePath = ".gitignore";
72
+ const existing = existsSync(gitignorePath)
73
+ ? readFileSync(gitignorePath, "utf-8")
74
+ : "";
75
+ const existingLines = new Set(existing
76
+ .split("\n")
77
+ .map((line) => line.trim())
78
+ .filter((line) => line !== ""));
79
+ const missingEntries = entries.filter((entry) => !existingLines.has(entry));
80
+ if (missingEntries.length === 0) {
15
81
  return;
16
82
  }
17
- writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
18
- console.log("Created firevault.config.json");
83
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
84
+ appendFileSync(gitignorePath, `${prefix}${missingEntries.join("\n")}\n`);
85
+ }
86
+ export async function runInit(options) {
87
+ console.log("Firevault");
88
+ console.log("Undo button for Firestore.");
89
+ console.log("");
90
+ const configPath = "firevault.config.json";
91
+ const alreadyInGitRepository = isInsideGitRepository();
92
+ if (alreadyInGitRepository && hasWorkingTreeChanges() && !options.force) {
93
+ throw new Error("Git working tree has changes. Commit, stash, or rerun with --force before init writes files.");
94
+ }
95
+ if (existsSync(configPath) && !options.force) {
96
+ throw new Error("firevault.config.json already exists. Rerun with --force to overwrite it.");
97
+ }
98
+ const shouldInitGit = alreadyInGitRepository
99
+ ? false
100
+ : await promptForGitInit(options);
101
+ const config = await promptForConfig(options);
102
+ config.collections = config.collections.map((collection) => collection.trim());
103
+ validateConfig(config);
104
+ if (shouldInitGit) {
105
+ initGitRepository();
106
+ console.log("Initialized Git repository.");
107
+ }
108
+ if (existsSync(configPath) && options.force) {
109
+ console.log("Warning: overwriting existing firevault.config.json.");
110
+ }
111
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
112
+ ensureGitignoreEntries([
113
+ "serviceAccountKey.json",
114
+ "firestore-backups/",
115
+ "firestore-debug.log",
116
+ ]);
117
+ console.log("Created firevault.config.json.");
118
+ console.log("Updated .gitignore safety entries.");
119
+ console.log("");
120
+ console.log("Next steps:");
121
+ console.log(`1. Save your service account key at ${config.serviceAccountPath}`);
122
+ console.log("2. Run `firevault snapshot`");
123
+ console.log("3. Run `firevault changes`");
124
+ console.log("4. Run `firevault restore-preview <path> --from <commit>` before a real restore");
125
+ }
126
+ export const initCommand = new Command("init")
127
+ .description("Create a guided Firevault configuration")
128
+ .option("--force", "Allow init with a dirty Git tree and overwrite existing config")
129
+ .option("--yes", "Use defaults and skip prompts")
130
+ .action(async (options) => {
131
+ try {
132
+ await runInit(options);
133
+ }
134
+ catch (error) {
135
+ if (error instanceof GitError || error instanceof Error) {
136
+ console.error(error.message);
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+ throw error;
141
+ }
19
142
  });
package/dist/git/git.js CHANGED
@@ -23,20 +23,27 @@ function runGit(args) {
23
23
  throw new GitError("Git command failed.");
24
24
  }
25
25
  }
26
- export function assertInsideGitRepository() {
27
- let result;
26
+ export function isInsideGitRepository() {
28
27
  try {
29
- result = runGit(["rev-parse", "--is-inside-work-tree"]).trim();
28
+ return runGit(["rev-parse", "--is-inside-work-tree"]).trim() === "true";
30
29
  }
31
30
  catch {
32
- throw new GitError("Current directory is not inside a Git repository.");
31
+ return false;
33
32
  }
34
- if (result !== "true") {
33
+ }
34
+ export function assertInsideGitRepository() {
35
+ if (!isInsideGitRepository()) {
35
36
  throw new GitError("Current directory is not inside a Git repository.");
36
37
  }
37
38
  }
39
+ export function initGitRepository() {
40
+ runGit(["init"]);
41
+ }
42
+ export function hasWorkingTreeChanges() {
43
+ return runGit(["status", "--porcelain"]).trim() !== "";
44
+ }
38
45
  export function hasChangesUnder(path) {
39
- return runGit(["status", "--porcelain", "--", path]).trim() !== "";
46
+ return runGit(["status", "--porcelain", "--ignored", "--", path]).trim() !== "";
40
47
  }
41
48
  function emptyFileChanges() {
42
49
  return {
@@ -148,7 +155,7 @@ export function showFileAtCommit(commit, path) {
148
155
  }
149
156
  }
150
157
  export function stagePath(path) {
151
- runGit(["add", "--", path]);
158
+ runGit(["add", "-f", "--", path]);
152
159
  }
153
160
  export function commitPath(message, path) {
154
161
  runGit(["commit", "-m", message, "--", path]);
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { restoreFirestoreCommand } from "./commands/restoreFirestore.js";
12
12
  const program = new Command();
13
13
  program
14
14
  .name("firevault")
15
- .description("Git-native backup, diff, and recovery tooling for Firestore.")
15
+ .description("Undo button for Firestore.")
16
16
  .version("0.1.0");
17
17
  program.addCommand(initCommand);
18
18
  program.addCommand(backupCommand);
@@ -81,7 +81,23 @@ Current implementation notes:
81
81
 
82
82
  - `loadConfig` reads and parses `firevault.config.json`.
83
83
  - Firebase initialization expects `serviceAccountPath`.
84
- - The current `init` template should be kept aligned with the expected config shape.
84
+ - `firevault init` guides setup, validates required fields, checks Git state before writing, and updates `.gitignore` without overwriting existing entries.
85
+ - `firevault init --force` allows dirty Git state and config overwrite with a warning.
86
+ - `firevault init --yes` provides a non-interactive path for tests and automation.
87
+
88
+ ## Init Safety
89
+
90
+ `firevault init` is intentionally conservative because setup writes local project files.
91
+
92
+ Behavior:
93
+
94
+ - prints the Firevault identity before prompting,
95
+ - detects whether the current directory is inside a Git repository,
96
+ - offers `git init` when no repository exists,
97
+ - refuses to run in a dirty Git working tree unless `--force` is provided,
98
+ - refuses to overwrite `firevault.config.json` unless `--force` is provided,
99
+ - appends `.gitignore` safety entries without duplicating existing lines,
100
+ - never commits, pushes, creates GitHub repositories, contacts Firebase, or writes secrets.
85
101
 
86
102
  ## Firebase Access
87
103
 
package/docs/roadmap.md CHANGED
@@ -25,6 +25,7 @@ Status:
25
25
  - CLI packaging runs from compiled `dist/index.js`.
26
26
  - npm prerelease packaging is guarded by package file whitelisting and pack verification.
27
27
  - Public GitHub release docs cover quick start, security, contributing, and issue triage.
28
+ - Guided `firevault init` validates setup input, checks Git state, and applies `.gitignore` safety entries.
28
29
 
29
30
  Next work:
30
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firevault",
3
- "version": "0.1.1-beta.0",
3
+ "version": "0.1.1-beta.1",
4
4
  "description": "Undo button for Firestore. Git-style history, rollback, and recovery for Firestore projects.",
5
5
  "keywords": [
6
6
  "firebase",