flowviant 0.97.0 → 0.100.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.
Files changed (50) hide show
  1. package/README.md +15 -13
  2. package/bin/cli.mjs +108 -28
  3. package/bin/lib/agentPlan.mjs +159 -37
  4. package/bin/lib/artifacts.mjs +136 -26
  5. package/bin/lib/authReject.mjs +32 -0
  6. package/bin/lib/authproxy.mjs +52 -16
  7. package/bin/lib/childEnv.mjs +33 -1
  8. package/bin/lib/claude.mjs +166 -71
  9. package/bin/lib/config.mjs +36 -9
  10. package/bin/lib/credentials.mjs +29 -5
  11. package/bin/lib/deploy.mjs +364 -30
  12. package/bin/lib/desktopContract.mjs +227 -0
  13. package/bin/lib/env.mjs +130 -13
  14. package/bin/lib/fleet.mjs +121 -30
  15. package/bin/lib/git.mjs +131 -3
  16. package/bin/lib/hooks/readGuard.mjs +24 -0
  17. package/bin/lib/instance.mjs +129 -14
  18. package/bin/lib/knowledge.mjs +180 -60
  19. package/bin/lib/landed.mjs +38 -30
  20. package/bin/lib/launchCommand.mjs +23 -0
  21. package/bin/lib/listeners.mjs +211 -0
  22. package/bin/lib/localSessions.mjs +29 -4
  23. package/bin/lib/login.mjs +36 -12
  24. package/bin/lib/machines.mjs +24 -14
  25. package/bin/lib/mcp-cli.mjs +4 -3
  26. package/bin/lib/preflight.mjs +2 -1
  27. package/bin/lib/preview.mjs +95 -6
  28. package/bin/lib/printable.mjs +42 -0
  29. package/bin/lib/procRegistry.mjs +65 -0
  30. package/bin/lib/processes.mjs +20 -7
  31. package/bin/lib/projectTools.mjs +248 -0
  32. package/bin/lib/prompts.mjs +16 -11
  33. package/bin/lib/resources.mjs +46 -4
  34. package/bin/lib/runtimeOfferings.mjs +75 -0
  35. package/bin/lib/runtimes.mjs +71 -9
  36. package/bin/lib/shipMerge.mjs +8 -4
  37. package/bin/lib/trace.mjs +16 -6
  38. package/bin/lib/update.mjs +114 -25
  39. package/bin/lib/work.mjs +1192 -4584
  40. package/bin/lib/workAgentMerges.mjs +504 -0
  41. package/bin/lib/workAgentPlans.mjs +447 -0
  42. package/bin/lib/workAgentReview.mjs +702 -0
  43. package/bin/lib/workAgentTurns.mjs +1151 -0
  44. package/bin/lib/workDiffs.mjs +107 -0
  45. package/bin/lib/workPreviews.mjs +351 -0
  46. package/bin/lib/workProcesses.mjs +236 -0
  47. package/bin/lib/workPullRequests.mjs +313 -0
  48. package/bin/lib/workShip.mjs +385 -0
  49. package/bin/lib/worktreeDiff.mjs +99 -43
  50. package/package.json +2 -3
@@ -30,7 +30,14 @@
30
30
  */
31
31
 
32
32
  import { execFileSync, spawn } from 'node:child_process';
33
- import { fileURLToPath } from 'node:url';
33
+ import { createHash } from 'node:crypto';
34
+ import { createReadStream, createWriteStream } from 'node:fs';
35
+ import { chmod, mkdtemp, open, rename, rm } from 'node:fs/promises';
36
+ import { dirname, join } from 'node:path';
37
+ import { platform, arch } from 'node:process';
38
+ import { pipeline } from 'node:stream/promises';
39
+ import { Readable } from 'node:stream';
40
+ import { runningViaNpx, terminalCommand } from './launchCommand.mjs';
34
41
  import { VERSION } from './config.mjs';
35
42
  import { note, ok, warn } from './ui.mjs';
36
43
 
@@ -45,22 +52,66 @@ export function cmpVersion(a, b) {
45
52
  return 0;
46
53
  }
47
54
 
48
- /**
49
- * npx runs from a per-invocation cache dir. `npm i -g` would install to a
50
- * DIFFERENT location than the one executing, so re-execing our own path would
51
- * loop on the stale cached copy. Detect npx and skip the install (nag instead —
52
- * relaunching with `npx flowviant@latest` is the npx-native update).
53
- */
54
- export function runningViaNpx() {
55
- const ua = process.env.npm_config_user_agent || '';
56
- const argv1 = process.argv[1] || '';
57
- let self = '';
55
+ /** Bun marks compiled entrypoints with a virtual /$bunfs/ script path. */
56
+ export function runningCompiledBinary() {
57
+ return Boolean(process.versions.bun && process.argv[1]?.startsWith('/$bunfs/'));
58
+ }
59
+
60
+ const RELEASE_ORIGIN = 'https://api.flowviant.com';
61
+
62
+ /** Download and verify before replacing the executable. Exposed for temp-dir tests. */
63
+ export async function installBinaryUpdate({
64
+ executable = process.execPath,
65
+ target = `${platform === 'darwin' ? 'darwin' : platform}-${arch}`,
66
+ fetchImpl = fetch,
67
+ origin = RELEASE_ORIGIN,
68
+ minimumVersion = null,
69
+ } = {}) {
70
+ if (!/^(linux|darwin)-(x64|arm64)$/.test(target)) throw new Error(`unsupported binary target ${target}`);
71
+ const manifestResponse = await fetchImpl(`${origin}/dl/latest.json`);
72
+ if (!manifestResponse.ok) throw new Error(`release manifest: HTTP ${manifestResponse.status}`);
73
+ const manifest = await manifestResponse.json();
74
+ const version = manifest?.version;
75
+ const file = manifest?.files?.[target];
76
+ if (!/^\d+\.\d+\.\d+$/.test(version) ||
77
+ file?.name !== `flowviant-${version}-${target}` ||
78
+ !/^[a-f0-9]{64}$/.test(file?.sha256) ||
79
+ !Number.isSafeInteger(file?.bytes) || file.bytes <= 0) {
80
+ throw new Error('invalid release manifest');
81
+ }
82
+ if (minimumVersion && cmpVersion(version, minimumVersion) < 0) {
83
+ throw new Error(`release manifest still serves ${version}; waiting for ${minimumVersion}`);
84
+ }
85
+ const binaryResponse = await fetchImpl(`${origin}/dl/${version}/${file.name}`);
86
+ if (!binaryResponse.ok || !binaryResponse.body) throw new Error(`binary download: HTTP ${binaryResponse.status}`);
87
+ const parent = dirname(executable);
88
+ const temporaryDir = await mkdtemp(join(parent, '.flowviant-update-'));
89
+ const temporaryFile = join(temporaryDir, file.name);
58
90
  try {
59
- self = fileURLToPath(import.meta.url);
60
- } catch {
61
- /* non-file URL — ignore */
91
+ await pipeline(Readable.fromWeb(binaryResponse.body), createWriteStream(temporaryFile, { mode: 0o755 }));
92
+ const digest = createHash('sha256');
93
+ let bytes = 0;
94
+ for await (const chunk of createReadStream(temporaryFile)) {
95
+ digest.update(chunk);
96
+ bytes += chunk.length;
97
+ }
98
+ if (bytes !== file.bytes || digest.digest('hex') !== file.sha256) {
99
+ throw new Error('downloaded binary failed SHA-256 or size verification');
100
+ }
101
+ await chmod(temporaryFile, 0o755);
102
+ const handle = await open(temporaryFile, 'r');
103
+ try { await handle.sync(); } finally { await handle.close(); }
104
+ await rename(temporaryFile, executable);
105
+ // The rename has committed. Directory fsync is best effort on filesystems
106
+ // that do not support it; a failure here must not claim the binary survived.
107
+ try {
108
+ const dirHandle = await open(parent, 'r');
109
+ try { await dirHandle.sync(); } finally { await dirHandle.close(); }
110
+ } catch { /* rename already succeeded */ }
111
+ return version;
112
+ } finally {
113
+ await rm(temporaryDir, { recursive: true, force: true }).catch(() => {});
62
114
  }
63
- return /\bnpx\b/.test(ua) || /[\\/]_npx[\\/]/.test(argv1) || /[\\/]_npx[\\/]/.test(self);
64
115
  }
65
116
 
66
117
  /**
@@ -70,7 +121,7 @@ export function runningViaNpx() {
70
121
  * this process alive only as a thin proxy waiting on the child, so the user's
71
122
  * shell stays attached to one foreground process.
72
123
  */
73
- function reexec(teardown, { viaNpx = false, target = null } = {}) {
124
+ function reexec(teardown, { viaNpx = false, viaBinary = false, target = null } = {}) {
74
125
  try {
75
126
  teardown?.();
76
127
  } catch {
@@ -84,7 +135,7 @@ function reexec(teardown, { viaNpx = false, target = null } = {}) {
84
135
  // on npx's install prompt — the same rule FLOWVIANT_REEXEC keeps below.
85
136
  const [cmd, args] = viaNpx
86
137
  ? ['npx', ['-y', 'flowviant@latest', ...process.argv.slice(2)]]
87
- : [process.execPath, process.argv.slice(1)];
138
+ : [process.execPath, process.argv.slice(viaBinary ? 2 : 1)];
88
139
  const child = spawn(cmd, args, {
89
140
  stdio: 'inherit',
90
141
  // MARK THE CHILD AS A RESTART, not as a person typing `flowviant`.
@@ -100,6 +151,11 @@ function reexec(teardown, { viaNpx = false, target = null } = {}) {
100
151
  env: {
101
152
  ...process.env,
102
153
  FLOWVIANT_REEXEC: '1',
154
+ // WHICH DAEMON THE SUCCESSOR IS REPLACING. Under npx the successor's
155
+ // parent is `npm exec`, not us, so the instance lock's ppid check read us
156
+ // as a rival and the new daemon SIGTERMed this proxy. instance.mjs adopts
157
+ // the lock from this pid when it is really an ancestor of the successor.
158
+ FLOWVIANT_REEXEC_FROM: String(process.pid),
103
159
  // WHAT WE RESTARTED IN ORDER TO BECOME. The successor compares its own
104
160
  // VERSION against this: if it came back still short, the update did not
105
161
  // take (a registry serving a stale `latest`, an npx cache that refused to
@@ -108,6 +164,7 @@ function reexec(teardown, { viaNpx = false, target = null } = {}) {
108
164
  // the global path there is no install step whose failure would throw and
109
165
  // stop it.
110
166
  ...(target ? { FLOWVIANT_UPDATE_TARGET: target } : {}),
167
+ FLOWVIANT_UPDATE_FROM: VERSION,
111
168
  },
112
169
  });
113
170
  child.on('exit', (code) => process.exit(code ?? 0));
@@ -133,9 +190,18 @@ function installLatest() {
133
190
 
134
191
  /** `flowviant update` — explicit, manual update. Does not re-exec into a daemon
135
192
  * (the user ran a one-shot command); it installs and tells them to relaunch. */
136
- export function runUpdateCommand() {
193
+ export async function runUpdateCommand() {
194
+ if (runningCompiledBinary()) {
195
+ try {
196
+ const version = await installBinaryUpdate();
197
+ ok(`updated to ${version}. Relaunch \`flowviant\` to run the new version.`);
198
+ } catch (e) {
199
+ warn(`update failed (${e?.message ?? e}) — running binary untouched.`);
200
+ }
201
+ return;
202
+ }
137
203
  if (runningViaNpx()) {
138
- note('running via npx — just relaunch with `npx flowviant@latest` to get the newest.');
204
+ note('running via npx — relaunch `flowviant` to get the newest.');
139
205
  return;
140
206
  }
141
207
  try {
@@ -143,7 +209,7 @@ export function runUpdateCommand() {
143
209
  installLatest();
144
210
  ok('updated. Relaunch `flowviant` to run the new version.');
145
211
  } catch (e) {
146
- warn(`update failed (${e?.message ?? e}). Try: npm i -g flowviant@latest`);
212
+ warn(`update failed (${e?.message ?? e}). Try: ${terminalCommand('update')}`);
147
213
  }
148
214
  }
149
215
 
@@ -165,13 +231,14 @@ const INSTALL_RETRY_MS = 15 * 60_000;
165
231
  * React to the server's {latest, min} signal from a roster poll.
166
232
  * @returns true if it kicked off a self-update + re-exec (caller must stop).
167
233
  */
168
- export function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, teardown }) {
234
+ export async function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, teardown }) {
169
235
  const cur = VERSION;
170
236
  const belowMin = min && cmpVersion(cur, min) < 0;
171
237
  const belowLatest = latest && cmpVersion(cur, latest) < 0;
172
238
  if (!belowMin && !belowLatest) return false; // current — nothing to do
173
239
  const target = latest || min;
174
240
  const npx = runningViaNpx();
241
+ const binary = runningCompiledBinary();
175
242
  const wantInstall = belowMin || autoUpdate;
176
243
 
177
244
  // A RESTART THAT DID NOT TAKE must not be tried again on the next poll. The
@@ -184,13 +251,35 @@ export function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, tea
184
251
  naggedFor = target;
185
252
  warn(
186
253
  `restarted to pick up ${target} but came back as ${cur} — staying put. Update by hand: ${
187
- npx ? 'relaunch with `npx flowviant@latest`' : 'npm i -g flowviant@latest'
254
+ binary ? 'run `flowviant update`' : npx ? 'relaunch `flowviant`' : `run \`${terminalCommand('update')}\``
188
255
  }.`
189
256
  );
190
257
  }
191
258
  return false;
192
259
  }
193
260
 
261
+ if (wantInstall && binary) {
262
+ if (!safeToUpdate) {
263
+ if (naggedFor !== target) {
264
+ naggedFor = target;
265
+ note(`flowviant ${cur} → ${target} available — self-updating once no turn is running.`);
266
+ }
267
+ return false;
268
+ }
269
+ if (Date.now() - lastInstallFailAt < INSTALL_RETRY_MS) return false;
270
+ try {
271
+ note(`flowviant ${cur} → ${target}: downloading binary…`);
272
+ const installed = await installBinaryUpdate({ minimumVersion: target });
273
+ ok('updated — restarting into the new version.');
274
+ reexec(teardown, { viaBinary: true, target: installed });
275
+ return true;
276
+ } catch (e) {
277
+ lastInstallFailAt = Date.now();
278
+ warn(`binary self-update failed (${e?.message ?? e}) — running binary untouched; retrying in 15m.`);
279
+ return false;
280
+ }
281
+ }
282
+
194
283
  // UNDER NPX THERE IS NOTHING TO INSTALL — the relaunch IS the update, because
195
284
  // `npx -y flowviant@latest` resolves `latest` against the registry. Same two
196
285
  // gates as the global path: the operator's AUTO_UPDATE choice, and an idle
@@ -210,7 +299,7 @@ export function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, tea
210
299
  return true;
211
300
  }
212
301
 
213
- if (wantInstall && !npx) {
302
+ if (wantInstall && !npx && !binary) {
214
303
  if (!safeToUpdate) {
215
304
  // Outdated but a turn is running — wait until the machine is quiet. Nag
216
305
  // once meanwhile.
@@ -260,7 +349,7 @@ export function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, tea
260
349
  } catch (e) {
261
350
  lastInstallFailAt = Date.now();
262
351
  warn(
263
- `self-update failed (${e?.message ?? e}) — retrying in 15m; to fix it now: npm i -g flowviant@latest`
352
+ `self-update failed (${e?.message ?? e}) — retrying in 15m; to fix it now: ${terminalCommand('update')}`
264
353
  );
265
354
  return false;
266
355
  }
@@ -269,7 +358,7 @@ export function handleVersionSignal({ latest, min, autoUpdate, safeToUpdate, tea
269
358
  // Can't or won't auto-install → nag once per target version.
270
359
  if (naggedFor !== target) {
271
360
  naggedFor = target;
272
- const how = npx ? 'relaunch with `npx flowviant@latest`' : 'run `npm i -g flowviant@latest`';
361
+ const how = binary ? 'run `flowviant update`' : npx ? 'relaunch `flowviant`' : `run \`${terminalCommand('update')}\``;
273
362
  if (belowMin) {
274
363
  warn(`flowviant ${cur} is below the minimum ${min} — live mode may not work. Update: ${how}.`);
275
364
  } else {