flowviant 0.102.0 → 0.102.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/bin/cli.mjs CHANGED
@@ -170,6 +170,8 @@ if (process.argv[2] === 'login') {
170
170
  }
171
171
 
172
172
  if (process.argv[2] === 'status' && process.argv.includes('--json')) {
173
+ // The tray polls this every 30 s: the fixed per-user bin dirs only, no shell.
174
+ await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess({ shell: false });
173
175
  const { desktopStatus, desktopStatusRemote } = await import('./lib/desktopContract.mjs');
174
176
  const status = process.argv.includes('--remote') ? await desktopStatusRemote() : desktopStatus();
175
177
  process.stdout.write(`${JSON.stringify(status)}\n`);
@@ -178,6 +180,7 @@ if (process.argv[2] === 'status' && process.argv.includes('--json')) {
178
180
 
179
181
  // `flowviant status` — the same facts, for a person.
180
182
  if (process.argv[2] === 'status') {
183
+ await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess();
181
184
  const { runStatus } = await import('./lib/views.mjs');
182
185
  await runStatus();
183
186
  process.exit(0);
@@ -199,6 +202,7 @@ if (process.argv[2] === 'open') {
199
202
 
200
203
  // `flowviant doctor` — what this computer needs, checked.
201
204
  if (process.argv[2] === 'doctor') {
205
+ await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess();
202
206
  const { runDoctor } = await import('./lib/views.mjs');
203
207
  process.exit((await runDoctor()) > 0 ? 1 : 0);
204
208
  }
@@ -561,6 +565,10 @@ if (process.argv[2] && !process.argv[2].startsWith('-')) {
561
565
  process.exit(1);
562
566
  }
563
567
 
568
+ // Find the CLIs where the person's own terminal would: the Windows tray starts
569
+ // this through `wsl.exe --`, which reads no shell startup file (loginPath.mjs).
570
+ await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess();
571
+
564
572
  // ── WHICH PROJECT THIS START SERVES — said, asked, or refused; never guessed.
565
573
  //
566
574
  // The store holds many projects since 0.55.0 and resolution is BY REPO
@@ -0,0 +1,116 @@
1
+ /**
2
+ * THE PATH A PERSON'S TERMINAL HAS, for a daemon nobody started from one.
3
+ *
4
+ * Every CLI is found by name on PATH (`claude --version`, `codex`, `agy`,
5
+ * `gh`). A terminal's PATH comes from the shell's startup files; the Windows
6
+ * tray starts the daemon with `wsl.exe -d <distro> -- <binary>`, which reads
7
+ * none of them. That PATH is the distro's bare default, and it misses exactly
8
+ * where the CLIs usually live: `~/.local/bin` (Claude Code's own installer,
9
+ * put on PATH by ~/.profile) and version managers set up in ~/.bashrc (nvm,
10
+ * volta, bun). So a machine whose `claude` works in the owner's WSL terminal
11
+ * measured "not installed" under the tray (2026-09-25).
12
+ *
13
+ * So the daemon asks the person's login shell for its PATH once, and keeps
14
+ * every entry it already had FIRST (a terminal start is unchanged) with the
15
+ * shell's additions after it; then the usual per-user bin directories that
16
+ * exist. Best-effort and bounded: a shell that hangs or prints nothing costs a
17
+ * few seconds once, and nothing is ever removed from PATH.
18
+ */
19
+
20
+ import { execFileSync, spawn } from 'node:child_process';
21
+ import { existsSync, readdirSync } from 'node:fs';
22
+ import { homedir, platform } from 'node:os';
23
+ import { join } from 'node:path';
24
+
25
+ const MARK = '__FLOWVIANT_PATH__=';
26
+
27
+ /** The PATH an interactive login shell ends up with, or null. */
28
+ export function loginShellPath({ env = process.env, exec = execFileSync, timeoutMs = 4000 } = {}) {
29
+ const shell = env.SHELL && env.SHELL.startsWith('/') ? env.SHELL : '/bin/bash';
30
+ try {
31
+ // -i so ~/.bashrc runs (nvm lives there, behind the "not interactive?
32
+ // return" guard); -l so ~/.profile runs (~/.local/bin lives there). The
33
+ // marker line survives whatever a startup file prints.
34
+ const out = String(exec(shell, ['-ilc', `printf '\\n${MARK}%s\\n' "$PATH"`], {
35
+ env: { ...env, TERM: 'dumb' },
36
+ timeout: timeoutMs,
37
+ stdio: ['ignore', 'pipe', 'ignore'],
38
+ }));
39
+ const line = out.split('\n').reverse().find((l) => l.startsWith(MARK));
40
+ const value = line?.slice(MARK.length).trim();
41
+ return value ? value : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** Per-user bin directories CLIs install into, that exist on this box. */
48
+ export function userBinDirs({ home = homedir(), exists = existsSync, list = readdirSync } = {}) {
49
+ const dirs = ['.local/bin', '.npm-global/bin', '.bun/bin', '.volta/bin', '.cargo/bin'].map((d) => join(home, d));
50
+ // nvm: the newest installed node's bin (nvm's own "default" needs its shell
51
+ // function to resolve; the newest is the usual answer and only a fallback).
52
+ try {
53
+ const versions = list(join(home, '.nvm/versions/node'))
54
+ .filter((v) => /^v\d+\.\d+\.\d+$/.test(v))
55
+ .sort((a, b) => {
56
+ const pa = a.slice(1).split('.').map(Number);
57
+ const pb = b.slice(1).split('.').map(Number);
58
+ return pb[0] - pa[0] || pb[1] - pa[1] || pb[2] - pa[2];
59
+ });
60
+ if (versions[0]) dirs.push(join(home, '.nvm/versions/node', versions[0], 'bin'));
61
+ } catch { /* no nvm */ }
62
+ return dirs.filter((d) => exists(d));
63
+ }
64
+
65
+ /** Current entries first, then any the others add. Order-preserving, deduped. */
66
+ export function mergePath(current, ...more) {
67
+ const seen = new Set();
68
+ const out = [];
69
+ for (const part of [current, ...more].flatMap((p) => (Array.isArray(p) ? p : String(p ?? '').split(':')))) {
70
+ if (!part || seen.has(part)) continue;
71
+ seen.add(part);
72
+ out.push(part);
73
+ }
74
+ return out.join(':');
75
+ }
76
+
77
+ /**
78
+ * Widen process.env.PATH in place. `shell: false` skips the login shell and
79
+ * only adds the fixed per-user directories (for the tray's frequent status
80
+ * polls, where a shell start every 30 s is not worth it).
81
+ */
82
+ export function adoptLoginPath({ shell = true, env = process.env, ...deps } = {}) {
83
+ if (platform() === 'win32' || env.FLOWVIANT_KEEP_PATH === '1') return env.PATH;
84
+ const login = shell ? loginShellPath({ env, ...deps }) : null;
85
+ env.PATH = mergePath(env.PATH, login, userBinDirs(deps));
86
+ return env.PATH;
87
+ }
88
+
89
+ /**
90
+ * adoptLoginPath, for THIS process and every child it will start.
91
+ *
92
+ * Under Bun (the compiled binary the curl install and the tray ship) child
93
+ * processes are looked up and started with the environment the process
94
+ * STARTED with unless a spawn passes `env` — a later `process.env.PATH = …` is
95
+ * invisible to them (measured 2026-09-25: node found ~/.local/bin/claude after
96
+ * the change, the compiled binary did not). So when the PATH grew, the
97
+ * binary restarts itself once with it: a thin proxy, the same shape as the
98
+ * self-update re-exec, stdio inherited, exit code and signals passed through.
99
+ * Node needs none of that and adopts in place.
100
+ */
101
+ export async function adoptLoginPathForProcess(opts = {}) {
102
+ if (process.env.FLOWVIANT_PATH_ADOPTED === '1') return;
103
+ const before = process.env.PATH;
104
+ const after = adoptLoginPath(opts);
105
+ if (!process.versions.bun || after === before) return;
106
+ const compiled = process.argv[1]?.startsWith('/$bunfs/');
107
+ const child = spawn(process.execPath, process.argv.slice(compiled ? 2 : 1), {
108
+ stdio: 'inherit',
109
+ env: { ...process.env, FLOWVIANT_PATH_ADOPTED: '1' },
110
+ });
111
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
112
+ process.on(signal, () => { try { child.kill(signal); } catch { /* gone */ } });
113
+ }
114
+ const code = await new Promise((resolve) => child.on('exit', (status, signal) => resolve(status ?? (signal ? 1 : 0))));
115
+ process.exit(code);
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.102.0",
3
+ "version": "0.102.2",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
5
5
  "type": "module",
6
6
  "bin": {