flowviant 0.10.0 → 0.11.0

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
@@ -4,9 +4,14 @@
4
4
  *
5
5
  * Three modes, picked by which env var is set:
6
6
  *
7
- * FLOWVIANT_TOKEN=fva_… npx flowviant # 1 worker, current checkout
8
- * FLOWVIANT_TOKENS=a,b,c npx flowviant # static fleet, 1 worktree each
9
- * FLOWVIANT_FLEET=fft_… npx flowviant # FLEET DAEMON (recommended)
7
+ * FLOWVIANT_TOKEN=fva_… npx flowviant@latest # 1 worker, current checkout
8
+ * FLOWVIANT_TOKENS=a,b,c npx flowviant@latest # static fleet, 1 worktree each
9
+ * FLOWVIANT_FLEET=fft_… npx flowviant@latest # FLEET DAEMON (recommended)
10
+ *
11
+ * Launch with `@latest` so each start pulls the newest published version (bare
12
+ * `npx flowviant` can reuse a stale cache). A running daemon also self-updates
13
+ * on its own — at startup and when idle — so it stays current without restarts
14
+ * (FLOWVIANT_NO_UPDATE=1 makes it nag-only; `flowviant update` updates now).
10
15
  *
11
16
  * Fleet daemon: install ONCE with a fleet credential, then manage everything
12
17
  * from Flowviant. The daemon polls GET /api/v2/fleet/agents, reconciles one
@@ -44,6 +49,14 @@ if (process.argv[2] === 'login') {
44
49
  process.exit(0);
45
50
  }
46
51
 
52
+ // `flowviant update` — install the latest published version now. The daemon also
53
+ // self-updates on its own (at startup + when idle); this is the manual path.
54
+ if (process.argv[2] === 'update') {
55
+ const { runUpdateCommand } = await import('./lib/update.mjs');
56
+ runUpdateCommand();
57
+ process.exit(0);
58
+ }
59
+
47
60
  // `flowviant clean` — reclaim the persistent worktrees (~/.flowviant/worktrees).
48
61
  // They're kept across runs so in-flight work survives Ctrl+C; this is the drain.
49
62
  // Repos self-heal: the daemon runs `git worktree prune` if a stale registration
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
 
7
- export const VERSION = '0.10.0';
7
+ export const VERSION = '0.11.0';
8
8
 
9
9
  // Credential stored by `flowviant login` (device auth) — the no-token,
10
10
  // no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
@@ -35,6 +35,11 @@ export const RECONCILE_SECONDS = Number(process.env.RECONCILE_SECONDS || 10);
35
35
  // so a long-lived daemon never silently 401s on an expired token.
36
36
  export const REFRESH_BEFORE_SECONDS = Number(process.env.REFRESH_BEFORE_SECONDS || 3600);
37
37
  export const SAFE = process.env.FLOWVIANT_SAFE === '1';
38
+ // Self-update: a running daemon updates itself to the latest published version
39
+ // (at startup + when idle) and re-execs. On by default; FLOWVIANT_NO_UPDATE=1
40
+ // keeps it nag-only (it still tells you to update, never installs). Below the
41
+ // server's MIN version it updates regardless, since live mode won't work.
42
+ export const AUTO_UPDATE = process.env.FLOWVIANT_NO_UPDATE !== '1';
38
43
  // Live mode (DEFAULT since 0.8.0): persistent Agent-SDK session per task —
39
44
  // streams into the task channel, injectable mid-task, blocker-parks in place,
40
45
  // delivery card on complete, branch preview tunnels. The legacy poll/sentinel
package/bin/lib/fleet.mjs CHANGED
@@ -22,7 +22,9 @@ import {
22
22
  RECONCILE_SECONDS,
23
23
  REFRESH_BEFORE_SECONDS,
24
24
  LIVE,
25
+ AUTO_UPDATE,
25
26
  } from './config.mjs';
27
+ import { handleVersionSignal } from './update.mjs';
26
28
  import {
27
29
  git,
28
30
  resetWorktree,
@@ -411,6 +413,20 @@ export async function runFleetDaemon() {
411
413
  }
412
414
  if (roster.mcpUrl) mcpUrl = roster.mcpUrl;
413
415
  if (roster.leaseTtlSeconds) leaseTtlSeconds = roster.leaseTtlSeconds;
416
+ // Keep the daemon current. Safe = no worker mid-task (true at startup, since
417
+ // no workers are spawned yet). If it self-updates it re-execs into the new
418
+ // version and this process becomes a proxy — stop the loop.
419
+ if (roster.daemon) {
420
+ const safeToUpdate = [...workers.values()].every((w) => w.state.child == null);
421
+ const updating = handleVersionSignal({
422
+ latest: roster.daemon.latest,
423
+ min: roster.daemon.min,
424
+ autoUpdate: AUTO_UPDATE,
425
+ safeToUpdate,
426
+ teardown,
427
+ });
428
+ if (updating) return;
429
+ }
414
430
  processMergeJobs(roster.mergeJobs);
415
431
  processCleanupJobs(roster.cleanupJobs);
416
432
  const rosterIds = new Set(roster.agents.map((a) => a.agentId));
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Self-update — keep a long-running daemon current without babysitting it.
3
+ *
4
+ * A daemon runs for hours/days from one launch, so "latest at launch" (even with
5
+ * `npx flowviant@latest`) doesn't help a process that's already up when a new
6
+ * version ships. The server reports {latest, min} on every roster poll; the
7
+ * daemon compares its own VERSION and, at a SAFE boundary (startup or idle —
8
+ * never mid-task), self-updates + re-execs. Below `min` it updates regardless
9
+ * (older protocol is known-broken); otherwise it honors AUTO_UPDATE. When it
10
+ * can't install (npx cache, or auto off) it nags with the exact command.
11
+ */
12
+
13
+ import { execFileSync, spawn } from 'node:child_process';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { VERSION } from './config.mjs';
16
+ import { note, ok, warn } from './ui.mjs';
17
+
18
+ /** Compare x.y.z version strings → -1 | 0 | 1 (missing parts read as 0). */
19
+ export function cmpVersion(a, b) {
20
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
21
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
22
+ for (let i = 0; i < 3; i++) {
23
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
24
+ if (d !== 0) return d > 0 ? 1 : -1;
25
+ }
26
+ return 0;
27
+ }
28
+
29
+ /**
30
+ * npx runs from a per-invocation cache dir. `npm i -g` would install to a
31
+ * DIFFERENT location than the one executing, so re-execing our own path would
32
+ * loop on the stale cached copy. Detect npx and skip the install (nag instead —
33
+ * relaunching with `npx flowviant@latest` is the npx-native update).
34
+ */
35
+ export function runningViaNpx() {
36
+ const ua = process.env.npm_config_user_agent || '';
37
+ const argv1 = process.argv[1] || '';
38
+ let self = '';
39
+ try {
40
+ self = fileURLToPath(import.meta.url);
41
+ } catch {
42
+ /* non-file URL — ignore */
43
+ }
44
+ return /\bnpx\b/.test(ua) || /[\\/]_npx[\\/]/.test(argv1) || /[\\/]_npx[\\/]/.test(self);
45
+ }
46
+
47
+ /**
48
+ * Replace this process with a fresh one running the just-installed version.
49
+ * `npm i -g` overwrote the global package in place, so re-running argv[1] loads
50
+ * the NEW code. We tear down first (idle-gated, so nothing's mid-task) and keep
51
+ * this process alive only as a thin proxy waiting on the child, so the user's
52
+ * shell stays attached to one foreground process.
53
+ */
54
+ function reexec(teardown) {
55
+ try {
56
+ teardown?.();
57
+ } catch {
58
+ /* best-effort */
59
+ }
60
+ const child = spawn(process.execPath, process.argv.slice(1), {
61
+ stdio: 'inherit',
62
+ env: process.env,
63
+ });
64
+ child.on('exit', (code) => process.exit(code ?? 0));
65
+ }
66
+
67
+ /** Install @latest globally. Throws on failure (EACCES without sudo, offline…). */
68
+ function installLatest() {
69
+ execFileSync('npm', ['install', '-g', 'flowviant@latest'], { stdio: 'inherit' });
70
+ }
71
+
72
+ /** `flowviant update` — explicit, manual update. Does not re-exec into a daemon
73
+ * (the user ran a one-shot command); it installs and tells them to relaunch. */
74
+ export function runUpdateCommand() {
75
+ if (runningViaNpx()) {
76
+ note('running via npx — just relaunch with `npx flowviant@latest` to get the newest.');
77
+ return;
78
+ }
79
+ try {
80
+ note(`updating flowviant (currently ${VERSION})…`);
81
+ installLatest();
82
+ ok('updated. Relaunch `flowviant` to run the new version.');
83
+ } catch (e) {
84
+ warn(`update failed (${e?.message ?? e}). Try: npm i -g flowviant@latest`);
85
+ }
86
+ }
87
+
88
+ // Nag at most once per target version, so a poll every ~10s doesn't spam.
89
+ let naggedFor = null;
90
+
91
+ /**
92
+ * React to the server's {latest, min} signal from a roster poll.
93
+ * @returns true if it kicked off a self-update + re-exec (caller must stop).
94
+ */
95
+ export function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, teardown }) {
96
+ const cur = VERSION;
97
+ const belowMin = min && cmpVersion(cur, min) < 0;
98
+ const belowLatest = latest && cmpVersion(cur, latest) < 0;
99
+ if (!belowMin && !belowLatest) return false; // current — nothing to do
100
+ const target = latest || min;
101
+ const npx = runningViaNpx();
102
+ const wantInstall = belowMin || autoUpdate;
103
+
104
+ if (wantInstall && !npx) {
105
+ if (!safeToUpdate) {
106
+ // Outdated but an agent is mid-task — wait for idle. Nag once meanwhile.
107
+ if (naggedFor !== target) {
108
+ naggedFor = target;
109
+ note(`flowviant ${cur} → ${target} available — self-updating once agents go idle.`);
110
+ }
111
+ return false;
112
+ }
113
+ try {
114
+ note(`flowviant ${cur} → ${target}: self-updating…`);
115
+ installLatest();
116
+ ok('updated — restarting into the new version.');
117
+ reexec(teardown);
118
+ return true;
119
+ } catch (e) {
120
+ warn(`self-update failed (${e?.message ?? e}) — update manually: npm i -g flowviant@latest`);
121
+ naggedFor = target; // don't retry-spam a failing install every poll
122
+ return false;
123
+ }
124
+ }
125
+
126
+ // Can't or won't auto-install → nag once per target version.
127
+ if (naggedFor !== target) {
128
+ naggedFor = target;
129
+ const how = npx ? 'relaunch with `npx flowviant@latest`' : 'run `npm i -g flowviant@latest`';
130
+ if (belowMin) {
131
+ warn(`flowviant ${cur} is below the minimum ${min} — live mode may not work. Update: ${how}.`);
132
+ } else {
133
+ note(`flowviant ${cur} → ${latest} available. Update: ${how}.`);
134
+ }
135
+ }
136
+ return false;
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
5
5
  "type": "module",
6
6
  "bin": {