flowviant 0.116.0 → 0.124.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 (161) hide show
  1. package/README.md +66 -12
  2. package/bin/cli.mjs +50 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentAsk.mjs +123 -0
  5. package/bin/lib/agentBrief.mjs +36 -0
  6. package/bin/lib/agentFootprint.mjs +76 -0
  7. package/bin/lib/agentInternalWait.mjs +83 -0
  8. package/bin/lib/agentPlan.mjs +134 -31
  9. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  10. package/bin/lib/agentPublish.mjs +12 -13
  11. package/bin/lib/agentTaskKinds.mjs +5 -8
  12. package/bin/lib/agySessions.mjs +3 -3
  13. package/bin/lib/artifactContracts.mjs +91 -20
  14. package/bin/lib/artifactIteration.mjs +182 -0
  15. package/bin/lib/artifactPreview.mjs +92 -18
  16. package/bin/lib/artifactRecovery.mjs +36 -0
  17. package/bin/lib/artifactReporter.mjs +194 -22
  18. package/bin/lib/artifacts.mjs +70 -63
  19. package/bin/lib/authproxy.mjs +2 -1
  20. package/bin/lib/childEnv.mjs +9 -4
  21. package/bin/lib/claude.mjs +10 -20
  22. package/bin/lib/claudeAuth.mjs +15 -5
  23. package/bin/lib/claudePersonal.mjs +71 -6
  24. package/bin/lib/claudePosture.mjs +227 -54
  25. package/bin/lib/claudeSessions.mjs +7 -7
  26. package/bin/lib/claudeStream.mjs +27 -13
  27. package/bin/lib/cliLimit.mjs +2 -2
  28. package/bin/lib/codexHome.mjs +17 -0
  29. package/bin/lib/codexPersonal.mjs +44 -3
  30. package/bin/lib/codexProjectRead.mjs +63 -0
  31. package/bin/lib/codexRelay.mjs +21 -5
  32. package/bin/lib/codexResearch.mjs +193 -0
  33. package/bin/lib/commandSpecs.mjs +6 -0
  34. package/bin/lib/config.mjs +37 -24
  35. package/bin/lib/conversationIds.mjs +51 -0
  36. package/bin/lib/credentials.mjs +2 -7
  37. package/bin/lib/daemonLocks.mjs +90 -0
  38. package/bin/lib/daemonState.mjs +5 -5
  39. package/bin/lib/deploy.mjs +6 -9
  40. package/bin/lib/deployCheckout.mjs +26 -22
  41. package/bin/lib/deployRunner.mjs +54 -18
  42. package/bin/lib/deployWire.mjs +32 -23
  43. package/bin/lib/desktopContract.mjs +6 -5
  44. package/bin/lib/discoveryContract.mjs +19 -0
  45. package/bin/lib/env.mjs +104 -35
  46. package/bin/lib/fleet.mjs +53 -13
  47. package/bin/lib/fleetPost.mjs +172 -25
  48. package/bin/lib/fleetReports.mjs +172 -171
  49. package/bin/lib/fleetStartup.mjs +6 -6
  50. package/bin/lib/fleetWire.mjs +42 -0
  51. package/bin/lib/git.mjs +111 -61
  52. package/bin/lib/gitMergeIdentity.mjs +2 -8
  53. package/bin/lib/grant.mjs +23 -7
  54. package/bin/lib/holder.mjs +108 -81
  55. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  56. package/bin/lib/hooks/readGuard.mjs +54 -11
  57. package/bin/lib/imageArtifact.mjs +74 -0
  58. package/bin/lib/install.mjs +80 -33
  59. package/bin/lib/instance.mjs +36 -83
  60. package/bin/lib/intakeDraft.mjs +4 -1
  61. package/bin/lib/knowledgeFileSync.mjs +31 -0
  62. package/bin/lib/knowledgeFiles.mjs +28 -83
  63. package/bin/lib/landed.mjs +22 -29
  64. package/bin/lib/launchCommand.mjs +91 -0
  65. package/bin/lib/listeners.mjs +0 -5
  66. package/bin/lib/localSessions.mjs +52 -39
  67. package/bin/lib/login.mjs +22 -12
  68. package/bin/lib/loginPath.mjs +22 -17
  69. package/bin/lib/machineListing.mjs +6 -3
  70. package/bin/lib/maintenance.mjs +47 -0
  71. package/bin/lib/maintenanceInventory.mjs +39 -0
  72. package/bin/lib/mcp-cli.mjs +9 -10
  73. package/bin/lib/mcpInstall.mjs +163 -0
  74. package/bin/lib/mcpInventory.mjs +119 -0
  75. package/bin/lib/noWindowEnv.mjs +23 -3
  76. package/bin/lib/ownedDirs.mjs +99 -0
  77. package/bin/lib/pinnedCapture.mjs +383 -0
  78. package/bin/lib/planDependencies.mjs +70 -0
  79. package/bin/lib/postureTools.mjs +85 -0
  80. package/bin/lib/prWorkflow.mjs +94 -17
  81. package/bin/lib/preflight.mjs +8 -25
  82. package/bin/lib/preview.mjs +19 -25
  83. package/bin/lib/previewGatePolicy.mjs +34 -9
  84. package/bin/lib/previewRegistry.mjs +4 -11
  85. package/bin/lib/printable.mjs +70 -7
  86. package/bin/lib/procRegistry.mjs +33 -13
  87. package/bin/lib/procScan.mjs +2 -3
  88. package/bin/lib/procStat.mjs +76 -0
  89. package/bin/lib/processGroup.mjs +48 -0
  90. package/bin/lib/processes.mjs +10 -24
  91. package/bin/lib/projectToolRuntimes.mjs +328 -19
  92. package/bin/lib/projectTools.mjs +109 -34
  93. package/bin/lib/prompts.mjs +205 -60
  94. package/bin/lib/repoPath.mjs +19 -0
  95. package/bin/lib/repoState.mjs +17 -19
  96. package/bin/lib/repositoryBaseline.mjs +68 -0
  97. package/bin/lib/runTurn.mjs +286 -209
  98. package/bin/lib/runtimeAntigravity.mjs +16 -5
  99. package/bin/lib/runtimeCapabilities.mjs +215 -21
  100. package/bin/lib/runtimeClaude.mjs +78 -10
  101. package/bin/lib/runtimeCodex.mjs +217 -209
  102. package/bin/lib/runtimeDetection.mjs +8 -2
  103. package/bin/lib/runtimeEvents.mjs +28 -11
  104. package/bin/lib/runtimeLimits.mjs +5 -6
  105. package/bin/lib/runtimeOfferings.mjs +99 -14
  106. package/bin/lib/runtimes.mjs +10 -0
  107. package/bin/lib/shipSweep.mjs +5 -3
  108. package/bin/lib/shot.mjs +91 -17
  109. package/bin/lib/sleep.mjs +11 -0
  110. package/bin/lib/startProject.mjs +5 -5
  111. package/bin/lib/statePaths.mjs +40 -0
  112. package/bin/lib/tty.mjs +25 -15
  113. package/bin/lib/turnDiagnostics.mjs +93 -0
  114. package/bin/lib/turnLock.mjs +7 -14
  115. package/bin/lib/turnProfile.mjs +16 -4
  116. package/bin/lib/ui.mjs +0 -3
  117. package/bin/lib/uninstall.mjs +30 -26
  118. package/bin/lib/update.mjs +79 -27
  119. package/bin/lib/uplinkScrub.mjs +296 -31
  120. package/bin/lib/vault.mjs +78 -10
  121. package/bin/lib/vaultDiff.mjs +5 -2
  122. package/bin/lib/views.mjs +37 -12
  123. package/bin/lib/wikiRunner.mjs +40 -61
  124. package/bin/lib/work.mjs +44 -13
  125. package/bin/lib/workAdoptCarry.mjs +2 -4
  126. package/bin/lib/workAgentBrief.mjs +92 -0
  127. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  128. package/bin/lib/workAgentCheck.mjs +104 -57
  129. package/bin/lib/workAgentMerges.mjs +62 -57
  130. package/bin/lib/workAgentPlans.mjs +82 -199
  131. package/bin/lib/workAgentPrecheck.mjs +76 -138
  132. package/bin/lib/workAgentPublish.mjs +6 -8
  133. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  134. package/bin/lib/workAgentTurnExecution.mjs +131 -67
  135. package/bin/lib/workAgentTurnOutcome.mjs +11 -2
  136. package/bin/lib/workAgentTurnReports.mjs +9 -37
  137. package/bin/lib/workAgentTurns.mjs +64 -7
  138. package/bin/lib/workArtifactReconcile.mjs +79 -0
  139. package/bin/lib/workAttachments.mjs +66 -11
  140. package/bin/lib/workBrain.mjs +9 -4
  141. package/bin/lib/workCappedTurn.mjs +119 -0
  142. package/bin/lib/workChildren.mjs +5 -5
  143. package/bin/lib/workCommandAudit.mjs +18 -2
  144. package/bin/lib/workDiffs.mjs +52 -6
  145. package/bin/lib/workIntake.mjs +35 -60
  146. package/bin/lib/workPlaceReports.mjs +7 -4
  147. package/bin/lib/workPlaces.mjs +113 -31
  148. package/bin/lib/workPreviews.mjs +47 -112
  149. package/bin/lib/workProcesses.mjs +8 -35
  150. package/bin/lib/workPullRequests.mjs +37 -56
  151. package/bin/lib/workReportQueue.mjs +100 -148
  152. package/bin/lib/workRetire.mjs +2 -1
  153. package/bin/lib/workSessionRuntime.mjs +38 -37
  154. package/bin/lib/workSessionTurns.mjs +256 -199
  155. package/bin/lib/workShip.mjs +43 -23
  156. package/bin/lib/workToolLog.mjs +70 -3
  157. package/bin/lib/workTurnShape.mjs +165 -0
  158. package/npm-shrinkwrap.json +69 -0
  159. package/package.json +9 -4
  160. package/bin/lib/fleetJobs.mjs +0 -120
  161. package/bin/lib/patchRevert.mjs +0 -60
@@ -10,7 +10,7 @@
10
10
  * means is this module's.
11
11
  */
12
12
  import { existsSync, readFileSync, rmSync } from 'node:fs';
13
- import { processStartTime } from './procRegistry.mjs';
13
+ import { aliveAndOurs, processStartTime } from './procRegistry.mjs';
14
14
 
15
15
  /**
16
16
  * The spawn lock: the pid of the CLI currently live in this worktree. A
@@ -45,19 +45,12 @@ export const turnLockedByLivePid = (lockPath) => {
45
45
  } catch {
46
46
  /* unreadable — treat as stale */
47
47
  }
48
- if (Number.isInteger(pid) && pid > 0) {
49
- let alive = false;
50
- try {
51
- process.kill(pid, 0);
52
- alive = true; // signal 0 delivered — a process of OUR uid holds the pid
53
- } catch {
54
- /* ESRCH: gone. EPERM: another uid's process — never our CLI. Stale. */
55
- }
56
- if (alive) {
57
- const now = start ? processStartTime(pid) : null;
58
- // Unmeasurable start time is "cannot tell", which keeps the lock.
59
- if (!start || !now || now === start) return true;
60
- }
48
+ // Signal 0 delivered to a process of OUR uid; EPERM is another uid's process,
49
+ // never our CLI, so stale (procRegistry.aliveAndOurs).
50
+ if (aliveAndOurs(pid)) {
51
+ const now = start ? processStartTime(pid) : null;
52
+ // Unmeasurable start time is "cannot tell", which keeps the lock.
53
+ if (!start || !now || now === start) return true;
61
54
  }
62
55
  try {
63
56
  rmSync(lockPath, { force: true }); // dead holder — clear the stale lock
@@ -22,10 +22,18 @@
22
22
  * - design / research / image — a non-code card's posture
23
23
  * (agentTaskKinds.mjs), refused on any runtime that does not
24
24
  * DECLARE it, because every other adapter's fallback is its
25
- * build branch. research is Claude Code's; design is
25
+ * build branch. research is Claude Code's and isolated Codex's; design is
26
26
  * Claude Code's and, since 0.115.0, Codex's; image (0.114.0)
27
27
  * is Codex's.
28
28
  *
29
+ * `projectMcp` (2026-10-10, the owner): whether an agent's turn under this
30
+ * profile mounts MCP servers — the project's reviewed base `.mcp.json` copy
31
+ * and, since 2026-10-11 ("the server that is running flowviant's claude HAS
32
+ * these MCPs yet the agents are not using them"), this machine's own CLI
33
+ * servers and claude.ai connectors. Every card's posture: the code card's
34
+ * build, and the write-up's, mockup/3D-model/deck's and image's fences. The
35
+ * consult, plan, wiki and plan-mode turns are not a card's and mount none.
36
+ *
29
37
  * Claude's permission list for each name lives beside the lists themselves
30
38
  * (`claudePermFor`, claudePosture.mjs); this module owns the vocabulary and the
31
39
  * refusals, and imports the card-kind table and the runtime registry (whose
@@ -44,10 +52,12 @@ const declaredBy = (name) =>
44
52
  .join(' or ') || 'no CLI this daemon knows';
45
53
  const article = (word) => (/^[aeiou]/i.test(word) ? 'an' : 'a');
46
54
 
47
- const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false });
55
+ const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false, projectMcp: false });
56
+ /** The kind postures that mount MCP servers (2026-10-10; image 2026-10-11). */
57
+ const KIND_PROJECT_MCP = new Set(['design', 'research', 'image']);
48
58
 
49
59
  export const TURN_PROFILES = Object.freeze({
50
- build: plain('build'),
60
+ build: Object.freeze({ ...plain('build'), projectMcp: true }),
51
61
  wiki: plain('wiki'),
52
62
  consult: plain('consult'),
53
63
  plan: plain('plan'),
@@ -58,6 +68,7 @@ export const TURN_PROFILES = Object.freeze({
58
68
  // wearing the word "plan".
59
69
  onlyOn: (rt) => (rt.id === 'claude' ? null : `plan mode runs on Claude Code only — not '${rt.label}'`),
60
70
  strictMcp: true,
71
+ projectMcp: false,
61
72
  }),
62
73
  // The kind postures: fenced to "write only the artifacts directory", and a
63
74
  // runtime that does not declare one would run its build branch instead.
@@ -73,6 +84,7 @@ export const TURN_PROFILES = Object.freeze({
73
84
  ? null
74
85
  : `${article(name)} ${name} card runs on ${declaredBy(name)} only — not '${rt.label}'`,
75
86
  strictMcp: false,
87
+ projectMcp: KIND_PROJECT_MCP.has(name),
76
88
  }),
77
89
  ])
78
90
  ),
@@ -83,7 +95,7 @@ export const TURN_PROFILES = Object.freeze({
83
95
  export const RETIRED_POSTURE_KEYS = Object.freeze(['wikiPerm', 'readOnly', 'planPerm', 'planMode', 'posture']);
84
96
 
85
97
  /**
86
- * `{ name, adapterProfile, onlyOn, strictMcp }` for a turn's options, or
98
+ * `{ name, adapterProfile, onlyOn, strictMcp, projectMcp }` for a turn's options, or
87
99
  * `{ error }` in words. Absent `profile` is build.
88
100
  */
89
101
  export function resolveTurnProfile(opts = {}) {
package/bin/lib/ui.mjs CHANGED
@@ -9,9 +9,6 @@ export const c = {
9
9
  blue: wrap(34), magenta: wrap(35), cyan: wrap(36), gray: wrap(90),
10
10
  };
11
11
 
12
- // Stable, cycled colours so each agent's label is easy to scan in a fleet.
13
- export const LABEL_COLORS = [c.cyan, c.magenta, c.blue, c.yellow, c.green, c.red];
14
-
15
12
  const stamp = () => {
16
13
  const d = new Date();
17
14
  const p = (n) => String(n).padStart(2, '0');
@@ -3,7 +3,9 @@ import { execFile as nodeExecFile } from 'node:child_process';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
- import { runningCompiledBinary } from './update.mjs';
6
+ import { runningCompiledBinary } from './launchCommand.mjs';
7
+ import { listDaemonLocks } from './daemonLocks.mjs';
8
+ import { processAlive } from './procRegistry.mjs';
7
9
  import { VERSION } from './config.mjs';
8
10
  import { parseVersion } from './versionOrder.mjs';
9
11
  import { copyKind, exactBlock, inside } from './uninstallCopies.mjs';
@@ -18,7 +20,9 @@ function defaults(options = {}) {
18
20
  home: homedir(),
19
21
  path: process.env.PATH || '',
20
22
  execFile: (file, args, opts) => execFileAsync(file, args, opts),
21
- pidLive: (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } },
23
+ // EPERM is alive (2026-10-10, F090/F123): a conservative alignment with the
24
+ // one probe — a pid we may not signal still protects the copy it may run.
25
+ pidLive: processAlive,
22
26
  procReader: (pid, file) => file === 'exe' || file === 'cwd'
23
27
  ? nodeFs.readlinkSync(`/proc/${pid}/${file}`) : nodeFs.readFileSync(`/proc/${pid}/${file}`),
24
28
  procAvailable: nodeFs.existsSync('/proc/self/cmdline'),
@@ -58,32 +62,32 @@ function compiledExecutable(fs, path) {
58
62
  } catch { return false; }
59
63
  }
60
64
 
65
+ // The lock files and their records come from the one scan instance.mjs uses
66
+ // (daemonLocks.mjs), through the injected fs and home; an unreadable lock is
67
+ // not evidence of a live daemon.
61
68
  function liveDaemons(o) {
62
69
  const found = [];
63
- for (const name of files(o.fs, join(o.home, '.flowviant'))) {
64
- if (!/^daemon-[0-9a-f]{12}\.lock$/.test(name)) continue;
65
- try {
66
- const pid = JSON.parse(o.fs.readFileSync(join(o.home, '.flowviant', name), 'utf8')).pid;
67
- if (!Number.isInteger(pid) || pid <= 0 || !o.pidLive(pid)) continue;
68
- let paths = [];
69
- if (o.procAvailable) {
70
- try {
71
- const exe = o.procReader(pid, 'exe');
72
- const command = o.procReader(pid, 'cmdline');
73
- const args = Buffer.from(command).toString('utf8').split('\0').filter(Boolean);
74
- let scripts = args.filter((arg) => isAbsolute(arg));
75
- const relative = args.filter((arg) => !arg.startsWith('-') && arg.includes('/') && !isAbsolute(arg));
76
- if (relative.length) {
77
- const cwd = String(o.procReader(pid, 'cwd'));
78
- scripts.push(...relative.map((arg) => resolve(cwd, arg)));
79
- }
80
- const executable = String(exe).replace(/ \(deleted\)$/, '');
81
- paths = basename(executable).startsWith('node') && !scripts.length ? [] :
82
- [executable, ...scripts].map((path) => real(o.fs, path));
83
- } catch { /* The live pid is still protected when its command is hidden. */ }
84
- }
85
- found.push({ pid, paths });
86
- } catch { /* An unreadable lock is not evidence of a live daemon. */ }
70
+ for (const { holder } of listDaemonLocks({ fs: o.fs, home: o.home })) {
71
+ if (!holder || !o.pidLive(holder.pid)) continue;
72
+ const pid = holder.pid;
73
+ let paths = [];
74
+ if (o.procAvailable) {
75
+ try {
76
+ const exe = o.procReader(pid, 'exe');
77
+ const command = o.procReader(pid, 'cmdline');
78
+ const args = Buffer.from(command).toString('utf8').split('\0').filter(Boolean);
79
+ let scripts = args.filter((arg) => isAbsolute(arg));
80
+ const relative = args.filter((arg) => !arg.startsWith('-') && arg.includes('/') && !isAbsolute(arg));
81
+ if (relative.length) {
82
+ const cwd = String(o.procReader(pid, 'cwd'));
83
+ scripts.push(...relative.map((arg) => resolve(cwd, arg)));
84
+ }
85
+ const executable = String(exe).replace(/ \(deleted\)$/, '');
86
+ paths = basename(executable).startsWith('node') && !scripts.length ? [] :
87
+ [executable, ...scripts].map((path) => real(o.fs, path));
88
+ } catch { /* The live pid is still protected when its command is hidden. */ }
89
+ }
90
+ found.push({ pid, paths });
87
91
  }
88
92
  return found;
89
93
  }
@@ -33,11 +33,12 @@ import { execFileSync, spawn } from 'node:child_process';
33
33
  import { createHash } from 'node:crypto';
34
34
  import { createReadStream, createWriteStream } from 'node:fs';
35
35
  import { chmod, mkdtemp, open, rename, rm } from 'node:fs/promises';
36
+ import { homedir } from 'node:os';
36
37
  import { dirname, join } from 'node:path';
37
38
  import { platform, arch } from 'node:process';
38
39
  import { pipeline } from 'node:stream/promises';
39
40
  import { Readable } from 'node:stream';
40
- import { runningViaNpx, terminalCommand } from './launchCommand.mjs';
41
+ import { relaunchSelf, runningCompiledBinary, runningViaNpx, selfArgv, terminalCommand } from './launchCommand.mjs';
41
42
  import { VERSION } from './config.mjs';
42
43
  import { compareVersions, versionBelow } from './versionOrder.mjs';
43
44
  import { note, ok, warn } from './ui.mjs';
@@ -48,9 +49,40 @@ import { note, ok, warn } from './ui.mjs';
48
49
  * never installs, never restarts and never nags on a version it cannot read.
49
50
  */
50
51
 
51
- /** Bun marks compiled entrypoints with a virtual /$bunfs/ script path. */
52
- export function runningCompiledBinary() {
53
- return Boolean(process.versions.bun && process.argv[1]?.startsWith('/$bunfs/'));
52
+ // Channel detection lives in launchCommand.mjs; re-exported for this module's callers.
53
+ export { runningCompiledBinary };
54
+
55
+ /**
56
+ * NPM RUNS FROM OUTSIDE THE CHECKOUT (2026-10-10, F131). The daemon's working
57
+ * directory IS the repository (the person launched there, or `--dir` chdir'd
58
+ * to it), and npm loads a project's `.npmrc` from the nearest package root —
59
+ * its `registry=` and `node-options=` included — for `npm exec` and `npm view`.
60
+ * A change that reached base could then steer the unattended self-update to
61
+ * another registry or preload code into it, outside any agent posture. From
62
+ * the home directory npm reads only the operator's own user and global config,
63
+ * which is honoured as it is: no registry is forced.
64
+ */
65
+ export function npmOutsideCheckout() {
66
+ return { cwd: homedir() };
67
+ }
68
+
69
+ /**
70
+ * The npx relaunch's argv: the same flags, with the checkout named explicitly
71
+ * and ABSOLUTELY, because the relaunch no longer starts inside it. Any `--dir`
72
+ * pair already there is replaced — a relative one would now resolve against
73
+ * the home directory. `cwd` is this process's, which cli.mjs already moved to
74
+ * the `--dir` it was given; a bare start honours `--dir` (commandSpecs.mjs).
75
+ */
76
+ export function npxRelaunchArgs(args = process.argv.slice(2), cwd = process.cwd()) {
77
+ const kept = [];
78
+ for (let i = 0; i < args.length; i++) {
79
+ if (args[i] === '--dir') {
80
+ i++; // and its value
81
+ continue;
82
+ }
83
+ kept.push(args[i]);
84
+ }
85
+ return ['-y', 'flowviant@latest', ...kept, '--dir', cwd];
54
86
  }
55
87
 
56
88
  const RELEASE_ORIGIN = 'https://api.flowviant.com';
@@ -115,9 +147,15 @@ export async function installBinaryUpdate({
115
147
  * `npm i -g` overwrote the global package in place, so re-running argv[1] loads
116
148
  * the NEW code. We tear down first (idle-gated, so nothing's mid-task) and keep
117
149
  * this process alive only as a thin proxy waiting on the child, so the user's
118
- * shell stays attached to one foreground process.
150
+ * shell stays attached to one foreground process — the one relaunch proxy
151
+ * (launchCommand.mjs `relaunchSelf`), which passes signals on and reads a
152
+ * successor killed by one (the OOM killer) as a failure, so `Restart=on-failure`
153
+ * relaunches the machine instead of leaving it dark.
154
+ *
155
+ * `deps` (spawn, exit, signals) are relaunchSelf's test seams; the daemon
156
+ * passes none.
119
157
  */
120
- function reexec(teardown, { viaNpx = false, viaBinary = false, target = null } = {}) {
158
+ export function reexec(teardown, { viaNpx = false, target = null } = {}, deps = {}) {
121
159
  try {
122
160
  teardown?.();
123
161
  } catch {
@@ -128,12 +166,16 @@ function reexec(teardown, { viaNpx = false, viaBinary = false, target = null } =
128
166
  // to leave, and re-running it would reload the stale copy forever. Going back
129
167
  // through `npx -y flowviant@latest` is what makes npx resolve `latest` against
130
168
  // the registry and fetch the new one. `-y` because a restart must never stop
131
- // on npx's install prompt — the same rule FLOWVIANT_REEXEC keeps below.
132
- const [cmd, args] = viaNpx
133
- ? ['npx', ['-y', 'flowviant@latest', ...process.argv.slice(2)]]
134
- : [process.execPath, process.argv.slice(viaBinary ? 2 : 1)];
135
- const child = spawn(cmd, args, {
136
- stdio: 'inherit',
169
+ // on npx's install prompt — the same rule FLOWVIANT_REEXEC keeps below. It
170
+ // runs from outside the checkout and names it with `--dir` (npmOutsideCheckout).
171
+ const [cmd, argv, where] = viaNpx
172
+ ? ['npx', npxRelaunchArgs(), npmOutsideCheckout()]
173
+ : [process.execPath, selfArgv(process.argv.slice(2)), {}];
174
+ return relaunchSelf({
175
+ ...deps,
176
+ cmd,
177
+ argv,
178
+ ...where,
137
179
  // MARK THE CHILD AS A RESTART, not as a person typing `flowviant`.
138
180
  // stdio is inherited, so the child sees two TTYs and believes a human is
139
181
  // watching — and 0.55.0 asks a one-time binding question on exactly that
@@ -163,7 +205,6 @@ function reexec(teardown, { viaNpx = false, viaBinary = false, target = null } =
163
205
  FLOWVIANT_UPDATE_FROM: VERSION,
164
206
  },
165
207
  });
166
- child.on('exit', (code) => process.exit(code ?? 0));
167
208
  }
168
209
 
169
210
  /**
@@ -184,7 +225,29 @@ export function updateRestartFailed(target) {
184
225
 
185
226
  /** Install @latest globally. Throws on failure (EACCES without sudo, offline…). */
186
227
  function installLatest() {
187
- execFileSync('npm', ['install', '-g', 'flowviant@latest'], { stdio: 'inherit' });
228
+ execFileSync('npm', ['install', '-g', 'flowviant@latest'], { ...npmOutsideCheckout(), stdio: 'inherit' });
229
+ }
230
+
231
+ /**
232
+ * What npm says `flowviant@latest` is, or null when it cannot say (offline, an
233
+ * npm hiccup, a hung registry) — read from outside the checkout like every
234
+ * other npm call here. `exec` is the test seam.
235
+ */
236
+ export function publishedVersion({ exec = execFileSync } = {}) {
237
+ try {
238
+ return String(
239
+ exec('npm', ['view', 'flowviant', 'version'], {
240
+ ...npmOutsideCheckout(),
241
+ encoding: 'utf8',
242
+ stdio: ['ignore', 'pipe', 'ignore'],
243
+ // Synchronous, so it holds the event loop hostage for however long it
244
+ // runs — a hung registry must not become a hung daemon.
245
+ timeout: 10_000,
246
+ })
247
+ ).trim();
248
+ } catch {
249
+ return null; // offline / npm hiccup — "can't confirm", skip this poll
250
+ }
188
251
  }
189
252
 
190
253
  /** `flowviant update` — explicit, manual update. Does not re-exec into a daemon
@@ -289,7 +352,7 @@ export async function handleVersionSignal({ latest, min, autoUpdate, safeToUpdat
289
352
  try {
290
353
  spawn(process.execPath, ['uninstall', '--others', '--yes', '--json'], { detached: true, stdio: 'ignore' }).unref();
291
354
  } catch (e) { warn(`older-copy cleanup: ${e?.message ?? e}`); }
292
- reexec(teardown, { viaBinary: true, target: installed });
355
+ reexec(teardown, { target: installed });
293
356
  return true;
294
357
  } catch (e) {
295
358
  lastInstallFailAt = Date.now();
@@ -339,18 +402,7 @@ export async function handleVersionSignal({ latest, min, autoUpdate, safeToUpdat
339
402
  // the source of truth — only install if npm ACTUALLY has something newer than
340
403
  // us, else `npm i -g @latest` reinstalls our own version and we'd re-exec
341
404
  // forever.
342
- let published = null;
343
- try {
344
- published = execFileSync('npm', ['view', 'flowviant', 'version'], {
345
- encoding: 'utf8',
346
- stdio: ['ignore', 'pipe', 'ignore'],
347
- // Synchronous, so it holds the event loop hostage for however long it
348
- // runs — a hung registry must not become a hung daemon.
349
- timeout: 10_000,
350
- }).trim();
351
- } catch {
352
- /* offline / npm hiccup — treat as "can't confirm", skip this poll */
353
- }
405
+ const published = publishedVersion();
354
406
  // An unreadable answer from npm is "can't confirm", like no answer at all.
355
407
  const publishedOrder = published ? compareVersions(published, cur) : null;
356
408
  if (publishedOrder === null || publishedOrder <= 0) {