@openparachute/vault 0.2.1 → 0.2.2

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
@@ -4,6 +4,16 @@ All notable changes to Parachute Vault are documented here.
4
4
 
5
5
  This project loosely follows [Keep a Changelog](https://keepachangelog.com) and [Semantic Versioning](https://semver.org).
6
6
 
7
+ ## [0.2.2] — 2026-04-17
8
+
9
+ ### Fixed
10
+
11
+ - **`start.sh` daemon wrapper no longer crashes on user shell profiles that reference unbound variables.** The generated wrapper ran `source ~/.zprofile` and `source ~/.zshrc` under `set -u`, so a zsh plugin framework or any conditional profile setup that touched an unset variable would abort the wrapper with exit 1. The `2>/dev/null` redirect swallowed the error, launchd saw repeated exit 1s, and the daemon silently refused to start with an empty `vault.err`. The wrapper now brackets the profile-source lines with `set +u` / `set -u` so -u is only active for code the wrapper owns. Run `parachute vault init` once on 0.2.2 to rewrite `~/.parachute/start.sh` — the rewrite is idempotent.
12
+
13
+ ### Added
14
+
15
+ - **`parachute --version` / `parachute -v` / `parachute version`** print the installed package version to stdout. Works at the root and with the `vault` prefix (`parachute vault --version`, etc.). Reads from the installed `package.json` at module load, not a hardcoded string.
16
+
7
17
  ## [0.2.1] — 2026-04-17
8
18
 
9
19
  ### Fixed
@@ -83,5 +93,6 @@ First tagged public release. Ships the auth, backup, and onboarding surface the
83
93
  - **`core/src/test-preload.ts`** isolates `PARACHUTE_HOME` for tests so `bun test` never touches a user's real `~/.parachute/`.
84
94
  - Test suite at release cut: **538 passing / 0 failing / 3 skipped** across 22 files (541 tests total).
85
95
 
96
+ [0.2.2]: https://github.com/ParachuteComputer/parachute-vault/releases/tag/v0.2.2
86
97
  [0.2.1]: https://github.com/ParachuteComputer/parachute-vault/releases/tag/v0.2.1
87
98
  [0.2.0]: https://github.com/ParachuteComputer/parachute-vault/releases/tag/v0.2.0
package/README.md CHANGED
@@ -172,6 +172,7 @@ parachute vault init # one-command setup (idempotent — s
172
172
  parachute vault status # check what's running
173
173
  parachute vault doctor # diagnose install/config issues (see Troubleshooting)
174
174
  parachute vault url # print the local server URL (for scripts)
175
+ parachute --version # print the installed version (aliases: -v, version)
175
176
  parachute vault uninstall # remove daemon + MCP entry; keeps user data
176
177
  parachute vault uninstall --wipe # ...and also remove vaults, .env, config.yaml, logs
177
178
  parachute vault uninstall --yes --wipe # scripted destructive wipe (prints an audit line)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -19,6 +19,10 @@
19
19
  import { resolve } from "path";
20
20
  import { homedir } from "os";
21
21
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "fs";
22
+ // JSON import — resolved at module load, works for both dev runs
23
+ // (`bun src/cli.ts …`) and the published package (`bunx @openparachute/vault`)
24
+ // because package.json ships at the root next to src/.
25
+ import pkg from "../package.json" with { type: "json" };
22
26
  import {
23
27
  ensureConfigDirSync,
24
28
  readVaultConfig,
@@ -180,6 +184,14 @@ switch (command) {
180
184
  case "-h":
181
185
  usage();
182
186
  break;
187
+ case "version":
188
+ case "--version":
189
+ case "-v":
190
+ // Intentionally minimal — just the version string on stdout. Scripts
191
+ // (and `parachute vault doctor` in a future check) rely on this being
192
+ // a bare-number line; anything else belongs in `vault status`.
193
+ console.log(pkg.version);
194
+ break;
183
195
  default:
184
196
  console.error(`Unknown command: ${command}`);
185
197
  usage();
@@ -1976,6 +1988,7 @@ Setup:
1976
1988
  config.yaml, and daemon logs (vault.log, vault.err).
1977
1989
  --yes skips prompts (DANGEROUS with --wipe: no confirmation).
1978
1990
  parachute vault url Print the local server URL (for scripts)
1991
+ parachute --version Print the installed version (alias: -v, version)
1979
1992
 
1980
1993
  Vaults:
1981
1994
  parachute vault create <name> Create a new vault
package/src/daemon.ts CHANGED
@@ -55,8 +55,17 @@ export function generateWrapper(opts: {
55
55
  set -u
56
56
 
57
57
  # Source user shell profile for PATH (needed for parakeet-mlx, ffmpeg, etc.)
58
+ # Temporarily disable -u around these: user rc files routinely reference
59
+ # unbound variables (zsh plugin frameworks, conditional setups), and a bare
60
+ # \`set -u\` source would crash the wrapper with exit 1 and leave vault.err
61
+ # empty because of the 2>/dev/null below — launchd would respawn silently
62
+ # until it gave up. Keep the stderr redirect so expected "command not found"
63
+ # noise from incomplete setups doesn't fill vault.err; to debug silent
64
+ # wrapper failures, run \`bash -x ~/.parachute/start.sh\` by hand.
65
+ set +u
58
66
  [ -f "$HOME/.zprofile" ] && source "$HOME/.zprofile" 2>/dev/null
59
67
  [ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc" 2>/dev/null
68
+ set -u
60
69
 
61
70
  if [ -f "${envPath}" ]; then
62
71
  set -a
@@ -78,6 +78,84 @@ describe("generateWrapper", () => {
78
78
  rmSync(dir, { recursive: true, force: true });
79
79
  }
80
80
  });
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // The incident this guards against (0.2.2): sourcing the user's ~/.zshrc or
84
+ // ~/.zprofile under `set -u` crashes the wrapper if any line in the rc file
85
+ // references an unbound variable — which is routine in zsh plugin frameworks
86
+ // and half-configured setups. The 2>/dev/null redirect swallowed the error
87
+ // so vault.err stayed empty and launchd silently gave up after repeated
88
+ // exit 1s. The fix brackets the profile-source lines with `set +u` / `set -u`
89
+ // so strict-unset-vars only applies to code the wrapper itself owns.
90
+ // ---------------------------------------------------------------------------
91
+
92
+ test("brackets profile sourcing with set +u / set -u to survive user rc files", () => {
93
+ const wrapper = generateWrapper({ bunPath: "/bin/bun" });
94
+ // Textual shape check — quickest canary.
95
+ const lines = wrapper.split("\n");
96
+ const zprofileIdx = lines.findIndex((l) => l.includes(".zprofile"));
97
+ const zshrcIdx = lines.findIndex((l) => l.includes(".zshrc"));
98
+ expect(zprofileIdx).toBeGreaterThan(-1);
99
+ expect(zshrcIdx).toBeGreaterThan(-1);
100
+ // Both source lines must be sandwiched between a `set +u` and a `set -u`.
101
+ // Walk back for `set +u` and forward for `set -u` from whichever source
102
+ // line comes first / last so ordering stays flexible.
103
+ const firstIdx = Math.min(zprofileIdx, zshrcIdx);
104
+ const lastIdx = Math.max(zprofileIdx, zshrcIdx);
105
+ const preceding = lines.slice(0, firstIdx).reverse().find((l) => l.trim().startsWith("set "));
106
+ const following = lines.slice(lastIdx + 1).find((l) => l.trim().startsWith("set "));
107
+ expect(preceding?.trim()).toBe("set +u");
108
+ expect(following?.trim()).toBe("set -u");
109
+ });
110
+
111
+ test("surviving profile source under set -u: running the generated wrapper with a rc file that trips set -u does not abort before reaching the pointer-file logic", async () => {
112
+ // The integration proof. Build a fake HOME where ~/.zshrc expands an
113
+ // unbound variable ($UNSET_IN_TEST), point the wrapper at it via HOME,
114
+ // and expect the wrapper to exit on the "server path not configured"
115
+ // path (exit 1 from the explicit check) rather than on the zshrc crash
116
+ // (exit 1 from set -u). The signal we compare on is the stderr message:
117
+ // the pointer-missing branch prints a specific error; a set-u crash
118
+ // prints zsh's own "parameter not set" message and no vault-branded
119
+ // text at all.
120
+ //
121
+ // Skipping stderr here would let a regressed wrapper silently exit 1
122
+ // and still pass the test, so we assert on the presence of the
123
+ // pointer-missing message.
124
+ const dir = mkdtempSync(join(tmpdir(), "vault-wrapper-setu-"));
125
+ try {
126
+ const fakeHome = join(dir, "home");
127
+ writeFileSync(join(dir, "mkdir.marker"), ""); // ensure dir exists
128
+ await $`mkdir -p ${fakeHome}`.quiet();
129
+ // A ~/.zshrc that blows up under set -u.
130
+ writeFileSync(join(fakeHome, ".zshrc"), 'echo "$UNSET_IN_TEST"\n');
131
+ // Wrapper with explicit env/pointer paths that do NOT exist. We do not
132
+ // pass PARACHUTE_VAULT_SERVER_PATH either. So the wrapper should take
133
+ // the "no server path configured" branch and exit 1 with the branded
134
+ // message — but only if it survives the zshrc source.
135
+ const wrapper = generateWrapper({
136
+ bunPath: "/bin/echo", // won't be reached; a safe no-op if it is
137
+ serverPathFile: join(dir, "nonexistent-pointer"),
138
+ envPath: join(dir, "nonexistent.env"),
139
+ });
140
+ const path = join(dir, "start.sh");
141
+ writeFileSync(path, wrapper);
142
+ // Override HOME so the wrapper sources our crafted zshrc.
143
+ // Clear PARACHUTE_VAULT_SERVER_PATH in case the test runner has it set.
144
+ const result = await $`HOME=${fakeHome} PARACHUTE_VAULT_SERVER_PATH= bash ${path}`
145
+ .quiet()
146
+ .nothrow();
147
+ const stderr = result.stderr.toString();
148
+ // Positive: we reached the branded pointer-missing branch.
149
+ expect(stderr).toMatch(/parachute-vault: server path not configured/);
150
+ // Negative: we did NOT crash in zshrc with a zsh/bash unbound-variable
151
+ // message. Catches a future regression where someone drops the
152
+ // `set +u` bracket.
153
+ expect(stderr).not.toMatch(/UNSET_IN_TEST: unbound variable/);
154
+ expect(result.exitCode).toBe(1); // the branded branch
155
+ } finally {
156
+ rmSync(dir, { recursive: true, force: true });
157
+ }
158
+ });
81
159
  });
82
160
 
83
161
  describe("generatePlist", () => {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Integration tests for `parachute --version` and its aliases.
3
+ *
4
+ * Spawns the real CLI as a subprocess so the argv-dispatch path is exercised
5
+ * end-to-end. Every accepted spelling must produce the exact version string
6
+ * from package.json on stdout, with exit code 0, and nothing else.
7
+ */
8
+
9
+ import { describe, test, expect } from "bun:test";
10
+ import { resolve } from "path";
11
+ import pkg from "../package.json" with { type: "json" };
12
+
13
+ const CLI = resolve(import.meta.dir, "cli.ts");
14
+
15
+ function runCli(args: string[]): {
16
+ exitCode: number;
17
+ stdout: string;
18
+ stderr: string;
19
+ } {
20
+ const proc = Bun.spawnSync({
21
+ cmd: ["bun", CLI, ...args],
22
+ stdout: "pipe",
23
+ stderr: "pipe",
24
+ });
25
+ return {
26
+ exitCode: proc.exitCode ?? -1,
27
+ stdout: new TextDecoder().decode(proc.stdout),
28
+ stderr: new TextDecoder().decode(proc.stderr),
29
+ };
30
+ }
31
+
32
+ describe("parachute version", () => {
33
+ // Every spelling must print the package's version and nothing else.
34
+ // `parachute vault --version` works via the argv parser's existing
35
+ // `args[0] === "vault"` branch, which shifts "vault" off and treats
36
+ // `--version` as the command — so the same switch case handles both
37
+ // root and vault-prefixed invocations.
38
+ for (const form of [
39
+ ["--version"],
40
+ ["-v"],
41
+ ["version"],
42
+ ["vault", "--version"],
43
+ ["vault", "-v"],
44
+ ["vault", "version"],
45
+ ]) {
46
+ test(`parachute ${form.join(" ")} prints the package version`, () => {
47
+ const { exitCode, stdout, stderr } = runCli(form);
48
+ expect(exitCode).toBe(0);
49
+ // Exact match — no banner, no trailing whitespace other than the single
50
+ // trailing newline from console.log. Scripts will pipe this through
51
+ // things like `$(parachute --version)`.
52
+ expect(stdout).toBe(`${pkg.version}\n`);
53
+ // Stderr must be empty. If the dispatcher drops into the default branch
54
+ // it would print "Unknown command:" plus the full usage() block to
55
+ // stderr, which is exactly the regression this test catches.
56
+ expect(stderr).toBe("");
57
+ });
58
+ }
59
+
60
+ test("version string looks like semver (sanity check)", () => {
61
+ // Defense-in-depth: if someone ever replaces the JSON import with a
62
+ // hardcoded string, a malformed value still won't slip through.
63
+ expect(pkg.version).toMatch(/^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$/);
64
+ });
65
+ });