@captainsafia/burrow 1.3.0-preview.4437197 → 1.3.0-preview.8fe41bb

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
@@ -19,7 +19,7 @@ Burrow is a platform-agnostic, directory-scoped secrets manager. Secrets are sto
19
19
  **Linux/macOS:**
20
20
 
21
21
  ```bash
22
- curl -fsSL https://safia.rocks/burrow/install.sh | sh
22
+ curl -fsSL https://i.captainsafia.sh/captainsafia/burrow | sh
23
23
  ```
24
24
 
25
25
  ## Usage
package/dist/api.d.ts CHANGED
@@ -21,25 +21,11 @@ export interface LoadedSecret {
21
21
  value: string;
22
22
  sourcePath: string;
23
23
  }
24
- export interface HookState {
25
- /** Currently loaded secret keys and their values */
26
- secrets: LoadedSecret[];
27
- /** The last directory that triggered loading */
28
- lastDir: string;
29
- /** Timestamp when secrets were loaded */
30
- loadedAt: string;
31
- /** Keys that were skipped due to conflicts */
32
- skippedKeys: string[];
33
- }
34
24
  export interface HookDiff {
35
25
  /** Keys to unset from the environment */
36
- unset: string[];
26
+ toUnset: string[];
37
27
  /** Secrets to set in the environment */
38
- set: LoadedSecret[];
39
- /** Keys that already exist in the environment and were skipped */
40
- skipped: string[];
41
- /** Keys that remain unchanged */
42
- unchanged: string[];
28
+ toSet: LoadedSecret[];
43
29
  }
44
30
  /**
45
31
  * Configuration options for creating a BurrowClient instance.
@@ -173,6 +159,11 @@ export interface HookOptions {
173
159
  * Defaults to respecting NO_COLOR environment variable.
174
160
  */
175
161
  useColor?: boolean;
162
+ /**
163
+ * Keys that were previously loaded by the hook.
164
+ * Used to compute which keys need to be unset.
165
+ */
166
+ previousKeys?: string[];
176
167
  }
177
168
  /**
178
169
  * Result of the `hook` method.
@@ -187,9 +178,13 @@ export interface HookResult {
187
178
  */
188
179
  message?: string;
189
180
  /**
190
- * The diff that was computed.
181
+ * The secrets that were loaded.
182
+ */
183
+ secrets: LoadedSecret[];
184
+ /**
185
+ * The keys that were unloaded.
191
186
  */
192
- diff: HookDiff;
187
+ unloadedKeys: string[];
193
188
  /**
194
189
  * Whether the directory is trusted.
195
190
  */
@@ -253,7 +248,6 @@ export declare class BurrowClient {
253
248
  private readonly resolver;
254
249
  private readonly pathOptions;
255
250
  private readonly trustManager;
256
- private readonly hookStateManager;
257
251
  /**
258
252
  * Creates a new BurrowClient instance.
259
253
  *
@@ -498,41 +492,23 @@ export declare class BurrowClient {
498
492
  * ```
499
493
  */
500
494
  listTrusted(): Promise<TrustedPath[]>;
501
- /**
502
- * Gets the current hook state (loaded secrets).
503
- *
504
- * @returns The current hook state or undefined if no secrets are loaded
505
- *
506
- * @example
507
- * ```typescript
508
- * const state = await client.getHookState();
509
- * if (state) {
510
- * console.log(`${state.secrets.length} secrets loaded from ${state.lastDir}`);
511
- * }
512
- * ```
513
- */
514
- getHookState(): Promise<HookState | undefined>;
515
- /**
516
- * Clears the hook state (unloads all secrets tracking).
517
- *
518
- * Note: This only clears the state file. The actual environment variables
519
- * remain set until the shell exits or they are explicitly unset.
520
- */
521
- clearHookState(): Promise<void>;
522
495
  /**
523
496
  * Processes a directory change for the shell hook.
524
497
  *
525
498
  * This is the main method called by shell hooks on directory change.
526
- * It checks trust, resolves secrets, computes the diff, and returns
527
- * the commands needed to update the environment.
499
+ * It checks trust, resolves secrets, computes the diff from previous
500
+ * state, and returns the commands needed to update the environment.
528
501
  *
529
502
  * @param cwd - The new working directory
530
- * @param options - Hook options including shell type
503
+ * @param options - Hook options including shell type and previous keys
531
504
  * @returns Hook result with commands to execute and optional message
532
505
  *
533
506
  * @example
534
507
  * ```typescript
535
- * const result = await client.hook('/projects/myapp', { shell: 'bash' });
508
+ * const result = await client.hook('/projects/myapp', {
509
+ * shell: 'bash',
510
+ * previousKeys: ['OLD_KEY'] // Keys from last hook call
511
+ * });
536
512
  * if (result.trusted) {
537
513
  * for (const cmd of result.commands) {
538
514
  * console.log(cmd); // Execute in shell
package/dist/api.js CHANGED
@@ -471,107 +471,33 @@ class TrustManager {
471
471
  }
472
472
  }
473
473
  // src/core/hook.ts
474
- import { readFile, writeFile, rm, mkdir as mkdir2 } from "node:fs/promises";
475
- import { join as join3 } from "node:path";
476
- var HOOK_STATE_FILE = "hook-state.json";
477
-
478
- class HookStateManager {
479
- configDir;
480
- stateFilePath;
481
- constructor(options = {}) {
482
- this.configDir = options.configDir ?? getConfigDir();
483
- this.stateFilePath = join3(this.configDir, HOOK_STATE_FILE);
484
- }
485
- async load() {
486
- try {
487
- const content = await readFile(this.stateFilePath, "utf-8");
488
- return JSON.parse(content);
489
- } catch {
490
- return;
491
- }
492
- }
493
- async save(state) {
494
- await mkdir2(this.configDir, { recursive: true });
495
- await writeFile(this.stateFilePath, JSON.stringify(state, null, 2));
496
- }
497
- async clear() {
498
- try {
499
- await rm(this.stateFilePath, { force: true });
500
- } catch {}
501
- }
502
- computeDiff(currentState, newSecrets, env = process.env) {
503
- const currentKeys = new Map;
504
- if (currentState) {
505
- for (const secret of currentState.secrets) {
506
- currentKeys.set(secret.key, secret);
507
- }
508
- }
509
- const skippedFromState = new Set(currentState?.skippedKeys ?? []);
510
- const unset = [];
511
- const set = [];
512
- const skipped = [];
513
- const unchanged = [];
514
- for (const [key] of currentKeys) {
515
- if (!newSecrets.has(key)) {
516
- unset.push(key);
517
- }
518
- }
519
- for (const [key, secret] of newSecrets) {
520
- const loadedSecret = {
521
- key: secret.key,
522
- value: secret.value,
523
- sourcePath: secret.sourcePath
524
- };
525
- const current = currentKeys.get(key);
526
- if (current) {
527
- if (current.value === secret.value && current.sourcePath === secret.sourcePath) {
528
- unchanged.push(key);
529
- } else {
530
- set.push(loadedSecret);
531
- }
532
- } else {
533
- const envValue = env[key];
534
- if (envValue !== undefined && !skippedFromState.has(key)) {
535
- skipped.push(key);
536
- } else {
537
- set.push(loadedSecret);
538
- }
539
- }
540
- }
541
- return { unset, set, skipped, unchanged };
542
- }
543
- async applyDiff(currentState, diff, newDir) {
544
- const secretsMap = new Map;
545
- if (currentState) {
546
- for (const secret of currentState.secrets) {
547
- if (!diff.unset.includes(secret.key)) {
548
- secretsMap.set(secret.key, secret);
549
- }
550
- }
551
- }
552
- for (const secret of diff.set) {
553
- secretsMap.set(secret.key, secret);
554
- }
555
- const skippedKeys = [...currentState?.skippedKeys ?? []];
556
- for (const key of diff.skipped) {
557
- if (!skippedKeys.includes(key)) {
558
- skippedKeys.push(key);
559
- }
560
- }
561
- const newState = {
562
- secrets: Array.from(secretsMap.values()),
563
- lastDir: newDir,
564
- loadedAt: new Date().toISOString(),
565
- skippedKeys
566
- };
567
- await this.save(newState);
568
- return newState;
474
+ function resolveToLoadedSecrets(secrets) {
475
+ const result = [];
476
+ for (const [, secret] of secrets) {
477
+ result.push({
478
+ key: secret.key,
479
+ value: secret.value,
480
+ sourcePath: secret.sourcePath
481
+ });
569
482
  }
483
+ return result;
484
+ }
485
+ function computeHookDiff(previousKeys, newSecrets) {
486
+ const newKeys = new Set(newSecrets.map((s) => s.key));
487
+ const toUnset = [];
488
+ for (const key of previousKeys) {
489
+ if (!newKeys.has(key)) {
490
+ toUnset.push(key);
491
+ }
492
+ }
493
+ return {
494
+ toUnset,
495
+ toSet: newSecrets
496
+ };
570
497
  }
571
498
  function formatHookMessage(diff, useColor = true) {
572
- const loaded = diff.set.length;
573
- const unloaded = diff.unset.length;
574
- const skippedCount = diff.skipped.length;
499
+ const loaded = diff.toSet.length;
500
+ const unloaded = diff.toUnset.length;
575
501
  if (loaded === 0 && unloaded === 0) {
576
502
  return;
577
503
  }
@@ -585,22 +511,19 @@ function formatHookMessage(diff, useColor = true) {
585
511
  } else {
586
512
  message += `unloaded ${unloaded} secret${unloaded === 1 ? "" : "s"}`;
587
513
  }
588
- if (skippedCount > 0) {
589
- message += ` (${skippedCount} skipped)`;
590
- }
591
514
  message += reset;
592
515
  return message;
593
516
  }
594
517
  function generateShellCommands(diff, shell) {
595
518
  const commands = [];
596
- for (const key of diff.unset) {
519
+ for (const key of diff.toUnset) {
597
520
  if (shell === "fish") {
598
521
  commands.push(`set -e ${key}`);
599
522
  } else {
600
523
  commands.push(`unset ${key}`);
601
524
  }
602
525
  }
603
- for (const secret of diff.set) {
526
+ for (const secret of diff.toSet) {
604
527
  const escapedValue = escapeShellValue2(secret.value, shell);
605
528
  if (shell === "fish") {
606
529
  commands.push(`set -gx ${secret.key} ${escapedValue}`);
@@ -624,7 +547,6 @@ class BurrowClient {
624
547
  resolver;
625
548
  pathOptions;
626
549
  trustManager;
627
- hookStateManager;
628
550
  constructor(options = {}) {
629
551
  this.storage = new Storage({
630
552
  configDir: options.configDir,
@@ -641,9 +563,6 @@ class BurrowClient {
641
563
  storage: this.storage,
642
564
  followSymlinks: options.followSymlinks
643
565
  });
644
- this.hookStateManager = new HookStateManager({
645
- configDir: options.configDir
646
- });
647
566
  }
648
567
  async set(key, value, options = {}) {
649
568
  assertValidEnvKey(key);
@@ -694,43 +613,48 @@ class BurrowClient {
694
613
  async listTrusted() {
695
614
  return this.trustManager.list();
696
615
  }
697
- async getHookState() {
698
- return this.hookStateManager.load();
699
- }
700
- async clearHookState() {
701
- return this.hookStateManager.clear();
702
- }
703
616
  async hook(cwd, options) {
617
+ const previousKeys = new Set(options.previousKeys ?? []);
704
618
  if (process.env["BURROW_AUTOLOAD"] === "0") {
705
- const emptyDiff = { unset: [], set: [], skipped: [], unchanged: [] };
619
+ const diff2 = computeHookDiff(previousKeys, []);
620
+ const commands2 = generateShellCommands(diff2, options.shell);
621
+ const useColor2 = options.useColor ?? !process.env["NO_COLOR"];
622
+ const message2 = diff2.toUnset.length > 0 ? formatHookMessage(diff2, useColor2) : undefined;
706
623
  return {
707
- commands: [],
708
- diff: emptyDiff,
624
+ commands: commands2,
625
+ message: message2,
626
+ secrets: [],
627
+ unloadedKeys: diff2.toUnset,
709
628
  trusted: false,
710
629
  notTrustedReason: "autoload-disabled"
711
630
  };
712
631
  }
713
632
  const trustResult = await this.isTrusted({ path: cwd });
714
633
  if (!trustResult.trusted) {
715
- const emptyDiff = { unset: [], set: [], skipped: [], unchanged: [] };
634
+ const diff2 = computeHookDiff(previousKeys, []);
635
+ const commands2 = generateShellCommands(diff2, options.shell);
636
+ const useColor2 = options.useColor ?? !process.env["NO_COLOR"];
637
+ const message2 = diff2.toUnset.length > 0 ? formatHookMessage(diff2, useColor2) : undefined;
716
638
  return {
717
- commands: [],
718
- diff: emptyDiff,
639
+ commands: commands2,
640
+ message: message2,
641
+ secrets: [],
642
+ unloadedKeys: diff2.toUnset,
719
643
  trusted: false,
720
644
  notTrustedReason: trustResult.reason
721
645
  };
722
646
  }
723
- const newSecrets = await this.resolve(cwd);
724
- const currentState = await this.hookStateManager.load();
725
- const diff = this.hookStateManager.computeDiff(currentState, newSecrets);
647
+ const resolvedSecrets = await this.resolve(cwd);
648
+ const secrets = resolveToLoadedSecrets(resolvedSecrets);
649
+ const diff = computeHookDiff(previousKeys, secrets);
726
650
  const commands = generateShellCommands(diff, options.shell);
727
- await this.hookStateManager.applyDiff(currentState, diff, cwd);
728
651
  const useColor = options.useColor ?? !process.env["NO_COLOR"];
729
652
  const message = formatHookMessage(diff, useColor);
730
653
  return {
731
654
  commands,
732
655
  message,
733
- diff,
656
+ secrets,
657
+ unloadedKeys: diff.toUnset,
734
658
  trusted: true
735
659
  };
736
660
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@captainsafia/burrow",
3
- "version": "1.3.0-preview.4437197",
3
+ "version": "1.3.0-preview.8fe41bb",
4
4
  "description": "Platform-agnostic, directory-scoped secrets manager",
5
5
  "type": "module",
6
6
  "main": "dist/api.js",
@@ -54,6 +54,6 @@
54
54
  "@inquirer/password": "^5.0.3",
55
55
  "clipboardy": "^5.0.2",
56
56
  "commander": "^14.0.2",
57
- "gh-release-update-notifier": "^1.0.0"
57
+ "gh-release-update-notifier": "^1.0.1"
58
58
  }
59
59
  }