@captainsafia/burrow 1.0.0-preview.ddd1e9d → 1.0.0-preview.f26ef28

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
@@ -30,7 +30,7 @@ burrow set DATABASE_URL=postgres://localhost/mydb --path ~/projects
30
30
  ### Get a secret
31
31
 
32
32
  ```bash
33
- burrow get API_KEY --show
33
+ burrow get API_KEY
34
34
  burrow get API_KEY --format json
35
35
  ```
36
36
 
@@ -44,8 +44,14 @@ burrow list --format json
44
44
  ### Export to your shell
45
45
 
46
46
  ```bash
47
+ # Auto-detects your shell (bash, fish, powershell, cmd)
47
48
  eval "$(burrow export)"
48
- eval "$(burrow export --format shell)" && npm start
49
+
50
+ # Or specify a format explicitly
51
+ burrow export --format fish
52
+ burrow export --format powershell
53
+ burrow export --format dotenv
54
+ burrow export --format json
49
55
  ```
50
56
 
51
57
  ### Block inheritance
@@ -54,6 +60,14 @@ eval "$(burrow export --format shell)" && npm start
54
60
  burrow unset API_KEY --path ~/projects/app/tests
55
61
  ```
56
62
 
63
+ ### Remove a secret
64
+
65
+ ```bash
66
+ burrow remove API_KEY --path ~/projects/app
67
+ ```
68
+
69
+ Unlike `unset` which blocks inheritance, `remove` deletes the entry entirely, restoring inheritance from parent directories.
70
+
57
71
  ## How It Works
58
72
 
59
73
  Secrets are stored in your user profile:
@@ -76,13 +90,26 @@ import { BurrowClient } from '@captainsafia/burrow';
76
90
 
77
91
  const client = new BurrowClient();
78
92
 
79
- await client.set('API_KEY', 'secret123', { path: '/my/project' });
93
+ try {
94
+ await client.set('API_KEY', 'secret123', { path: '/my/project' });
95
+
96
+ const secret = await client.get('API_KEY', { cwd: '/my/project/subdir' });
97
+ console.log(secret?.value); // 'secret123'
98
+ console.log(secret?.sourcePath); // '/my/project'
80
99
 
81
- const secret = await client.get('API_KEY', { cwd: '/my/project/subdir' });
82
- console.log(secret?.value); // 'secret123'
83
- console.log(secret?.sourcePath); // '/my/project'
100
+ const allSecrets = await client.list({ cwd: '/my/project' });
101
+ } finally {
102
+ client.close(); // Clean up database connection
103
+ }
104
+ ```
84
105
 
85
- const allSecrets = await client.list({ cwd: '/my/project' });
106
+ Or with TypeScript's `using` declarations for automatic cleanup:
107
+
108
+ ```typescript
109
+ {
110
+ using client = new BurrowClient();
111
+ await client.set('API_KEY', 'secret123');
112
+ } // Automatically cleaned up
86
113
  ```
87
114
 
88
115
  ## Contributing
package/dist/api.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface ResolvedSecret {
5
5
  value: string;
6
6
  sourcePath: string;
7
7
  }
8
- export type ExportFormat = "shell" | "dotenv" | "json";
8
+ export type ExportFormat = "shell" | "bash" | "fish" | "powershell" | "cmd" | "dotenv" | "json";
9
9
  /**
10
10
  * Configuration options for creating a BurrowClient instance.
11
11
  */
@@ -21,7 +21,7 @@ export interface BurrowClientOptions {
21
21
  configDir?: string;
22
22
  /**
23
23
  * Custom filename for the secrets store.
24
- * Defaults to `store.json`.
24
+ * Defaults to `store.db`.
25
25
  */
26
26
  storeFileName?: string;
27
27
  /**
@@ -72,6 +72,16 @@ export interface BlockOptions {
72
72
  */
73
73
  path?: string;
74
74
  }
75
+ /**
76
+ * Options for the `remove` method.
77
+ */
78
+ export interface RemoveOptions {
79
+ /**
80
+ * Directory path to remove the secret from.
81
+ * Defaults to the current working directory.
82
+ */
83
+ path?: string;
84
+ }
75
85
  /**
76
86
  * Options for the `export` method.
77
87
  */
@@ -89,10 +99,6 @@ export interface ExportOptions {
89
99
  * - `json`: Exports as a JSON object
90
100
  */
91
101
  format?: ExportFormat;
92
- /**
93
- * Whether to show actual values (currently unused, reserved for future use).
94
- */
95
- showValues?: boolean;
96
102
  /**
97
103
  * Whether to include source paths in JSON output.
98
104
  * When true, JSON output includes `{ key: { value, sourcePath } }` format.
@@ -141,7 +147,7 @@ export declare class BurrowClient {
141
147
  * The secret will be available to the specified directory and all its
142
148
  * subdirectories, unless overridden or blocked at a deeper level.
143
149
  *
144
- * @param key - Environment variable name. Must match `^[A-Z_][A-Z0-9_]*$`
150
+ * @param key - Environment variable name. Must match `^[A-Za-z_][A-Za-z0-9_]*$`
145
151
  * @param value - Secret value to store
146
152
  * @param options - Set options including target path
147
153
  * @throws Error if the key format is invalid
@@ -205,7 +211,7 @@ export declare class BurrowClient {
205
211
  *
206
212
  * A blocked key can be re-enabled by calling `set` at the same or deeper path.
207
213
  *
208
- * @param key - Environment variable name to block. Must match `^[A-Z_][A-Z0-9_]*$`
214
+ * @param key - Environment variable name to block. Must match `^[A-Za-z_][A-Za-z0-9_]*$`
209
215
  * @param options - Block options including target path
210
216
  * @throws Error if the key format is invalid
211
217
  *
@@ -223,6 +229,33 @@ export declare class BurrowClient {
223
229
  * ```
224
230
  */
225
231
  block(key: string, options?: BlockOptions): Promise<void>;
232
+ /**
233
+ * Removes a secret entry entirely from the specified path.
234
+ *
235
+ * Unlike `block`, which creates a tombstone to prevent inheritance,
236
+ * `remove` completely deletes the secret entry. After removal, the key
237
+ * may still be inherited from parent directories if defined there.
238
+ *
239
+ * @param key - Environment variable name to remove. Must match `^[A-Za-z_][A-Za-z0-9_]*$`
240
+ * @param options - Remove options including target path
241
+ * @returns true if the secret was found and removed, false if it didn't exist
242
+ * @throws Error if the key format is invalid
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * // Set a secret
247
+ * await client.set('API_KEY', 'secret', { path: '/projects/myapp' });
248
+ *
249
+ * // Remove it entirely
250
+ * const removed = await client.remove('API_KEY', { path: '/projects/myapp' });
251
+ * console.log(removed); // true
252
+ *
253
+ * // Trying to remove again returns false
254
+ * const removedAgain = await client.remove('API_KEY', { path: '/projects/myapp' });
255
+ * console.log(removedAgain); // false
256
+ * ```
257
+ */
258
+ remove(key: string, options?: RemoveOptions): Promise<boolean>;
226
259
  /**
227
260
  * Exports resolved secrets in various formats.
228
261
  *
@@ -274,6 +307,36 @@ export declare class BurrowClient {
274
307
  * ```
275
308
  */
276
309
  resolve(cwd?: string): Promise<Map<string, ResolvedSecret>>;
310
+ /**
311
+ * Closes the database connection and releases resources.
312
+ * After calling this method, the client instance should not be used.
313
+ *
314
+ * This method is safe to call multiple times.
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const client = new BurrowClient();
319
+ * try {
320
+ * await client.set('API_KEY', 'value');
321
+ * // ... do work
322
+ * } finally {
323
+ * client.close();
324
+ * }
325
+ * ```
326
+ */
327
+ close(): void;
328
+ /**
329
+ * Allows using the BurrowClient with `using` declarations for automatic cleanup.
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * {
334
+ * using client = new BurrowClient();
335
+ * await client.set('API_KEY', 'value');
336
+ * } // client.close() is called automatically
337
+ * ```
338
+ */
339
+ [Symbol.dispose](): void;
277
340
  }
278
341
  /**
279
342
  * Creates a new BurrowClient instance.
package/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/storage/index.ts
2
2
  import { Database } from "bun:sqlite";
3
- import { mkdir } from "node:fs/promises";
3
+ import { chmod, mkdir } from "node:fs/promises";
4
4
  import { join as join2 } from "node:path";
5
5
 
6
6
  // src/platform/index.ts
@@ -62,7 +62,13 @@ class Storage {
62
62
  return this.db;
63
63
  }
64
64
  await mkdir(this.configDir, { recursive: true });
65
+ if (!isWindows()) {
66
+ await chmod(this.configDir, 448);
67
+ }
65
68
  this.db = new Database(this.storePath);
69
+ if (!isWindows()) {
70
+ await chmod(this.storePath, 384);
71
+ }
66
72
  this.db.run("PRAGMA journal_mode = WAL");
67
73
  this.db.run(`
68
74
  CREATE TABLE IF NOT EXISTS secrets (
@@ -114,6 +120,16 @@ class Storage {
114
120
  const rows = db.query("SELECT DISTINCT path FROM secrets").all();
115
121
  return rows.map((row) => row.path);
116
122
  }
123
+ async getAncestorPaths(canonicalPath) {
124
+ const db = await this.ensureDb();
125
+ let rows;
126
+ if (isWindows()) {
127
+ rows = db.query("SELECT DISTINCT path FROM secrets WHERE ? = path OR ? LIKE path || '\\' || '%' OR (length(path) = 3 AND path LIKE '_:\\' AND ? LIKE path || '%')").all(canonicalPath, canonicalPath, canonicalPath);
128
+ } else {
129
+ rows = db.query("SELECT DISTINCT path FROM secrets WHERE ? = path OR ? LIKE path || '/' || '%' OR path = '/'").all(canonicalPath, canonicalPath);
130
+ }
131
+ return rows.map((row) => row.path);
132
+ }
117
133
  async removeKey(canonicalPath, key) {
118
134
  const db = await this.ensureDb();
119
135
  const existing = db.query("SELECT path FROM secrets WHERE path = ? AND key = ?").get(canonicalPath, key);
@@ -123,6 +139,15 @@ class Storage {
123
139
  db.query("DELETE FROM secrets WHERE path = ? AND key = ?").run(canonicalPath, key);
124
140
  return true;
125
141
  }
142
+ close() {
143
+ if (this.db) {
144
+ this.db.close();
145
+ this.db = null;
146
+ }
147
+ }
148
+ [Symbol.dispose]() {
149
+ this.close();
150
+ }
126
151
  }
127
152
 
128
153
  // src/core/path.ts
@@ -159,16 +184,6 @@ function normalizePath(path) {
159
184
  }
160
185
  return normalized;
161
186
  }
162
- function isAncestorOf(ancestorPath, descendantPath) {
163
- if (ancestorPath === descendantPath) {
164
- return true;
165
- }
166
- const ancestorWithSep = ancestorPath.endsWith(sep) ? ancestorPath : ancestorPath + sep;
167
- if (isWindows()) {
168
- return descendantPath.toLowerCase().startsWith(ancestorWithSep.toLowerCase());
169
- }
170
- return descendantPath.startsWith(ancestorWithSep);
171
- }
172
187
  // src/core/resolver.ts
173
188
  class Resolver {
174
189
  storage;
@@ -182,8 +197,7 @@ class Resolver {
182
197
  async resolve(cwd) {
183
198
  const workingDir = cwd ?? process.cwd();
184
199
  const canonicalCwd = await canonicalize(workingDir, this.pathOptions);
185
- const allPaths = await this.storage.getAllPaths();
186
- const ancestorPaths = allPaths.filter((storedPath) => isAncestorOf(storedPath, canonicalCwd));
200
+ const ancestorPaths = await this.storage.getAncestorPaths(canonicalCwd);
187
201
  ancestorPaths.sort((a, b) => {
188
202
  if (isWindows()) {
189
203
  return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -222,7 +236,7 @@ class Resolver {
222
236
  }
223
237
  }
224
238
  // src/core/formatter.ts
225
- var ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
239
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
226
240
  function validateEnvKey(key) {
227
241
  return ENV_KEY_PATTERN.test(key);
228
242
  }
@@ -237,6 +251,9 @@ function escapeShellValue(value) {
237
251
  function escapeDoubleQuotes(value) {
238
252
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
239
253
  }
254
+ function escapePowerShellValue(value) {
255
+ return value.replace(/'/g, "''");
256
+ }
240
257
  function formatShell(secrets) {
241
258
  const lines = [];
242
259
  const sortedKeys = Array.from(secrets.keys()).sort();
@@ -249,6 +266,42 @@ function formatShell(secrets) {
249
266
  return lines.join(`
250
267
  `);
251
268
  }
269
+ function formatFish(secrets) {
270
+ const lines = [];
271
+ const sortedKeys = Array.from(secrets.keys()).sort();
272
+ for (const key of sortedKeys) {
273
+ const secret = secrets.get(key);
274
+ assertValidEnvKey(key);
275
+ const escapedValue = escapeShellValue(secret.value);
276
+ lines.push(`set -gx ${key} '${escapedValue}'`);
277
+ }
278
+ return lines.join(`
279
+ `);
280
+ }
281
+ function formatPowerShell(secrets) {
282
+ const lines = [];
283
+ const sortedKeys = Array.from(secrets.keys()).sort();
284
+ for (const key of sortedKeys) {
285
+ const secret = secrets.get(key);
286
+ assertValidEnvKey(key);
287
+ const escapedValue = escapePowerShellValue(secret.value);
288
+ lines.push(`$env:${key} = '${escapedValue}'`);
289
+ }
290
+ return lines.join(`
291
+ `);
292
+ }
293
+ function formatCmd(secrets) {
294
+ const lines = [];
295
+ const sortedKeys = Array.from(secrets.keys()).sort();
296
+ for (const key of sortedKeys) {
297
+ const secret = secrets.get(key);
298
+ assertValidEnvKey(key);
299
+ const escapedValue = secret.value.replace(/([&|<>^])/g, "^$1");
300
+ lines.push(`set ${key}=${escapedValue}`);
301
+ }
302
+ return lines.join(`
303
+ `);
304
+ }
252
305
  function formatDotenv(secrets) {
253
306
  const lines = [];
254
307
  const sortedKeys = Array.from(secrets.keys()).sort();
@@ -286,7 +339,14 @@ function formatJson(secrets, includeSources = false) {
286
339
  function format(secrets, fmt, options = {}) {
287
340
  switch (fmt) {
288
341
  case "shell":
342
+ case "bash":
289
343
  return formatShell(secrets);
344
+ case "fish":
345
+ return formatFish(secrets);
346
+ case "powershell":
347
+ return formatPowerShell(secrets);
348
+ case "cmd":
349
+ return formatCmd(secrets);
290
350
  case "dotenv":
291
351
  return formatDotenv(secrets);
292
352
  case "json":
@@ -331,6 +391,12 @@ class BurrowClient {
331
391
  const canonicalPath = await canonicalize(targetPath, this.pathOptions);
332
392
  await this.storage.setSecret(canonicalPath, key, null);
333
393
  }
394
+ async remove(key, options = {}) {
395
+ assertValidEnvKey(key);
396
+ const targetPath = options.path ?? process.cwd();
397
+ const canonicalPath = await canonicalize(targetPath, this.pathOptions);
398
+ return this.storage.removeKey(canonicalPath, key);
399
+ }
334
400
  async export(options = {}) {
335
401
  const secrets = await this.resolver.resolve(options.cwd);
336
402
  const fmt = options.format ?? "shell";
@@ -341,6 +407,12 @@ class BurrowClient {
341
407
  async resolve(cwd) {
342
408
  return this.resolver.resolve(cwd);
343
409
  }
410
+ close() {
411
+ this.storage.close();
412
+ }
413
+ [Symbol.dispose]() {
414
+ this.close();
415
+ }
344
416
  }
345
417
  function createClient(options) {
346
418
  return new BurrowClient(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@captainsafia/burrow",
3
- "version": "1.0.0-preview.ddd1e9d",
3
+ "version": "1.0.0-preview.f26ef28",
4
4
  "description": "Platform-agnostic, directory-scoped secrets manager",
5
5
  "type": "module",
6
6
  "main": "dist/api.js",