flowviant 0.91.0 → 0.91.1

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
@@ -350,15 +350,14 @@ if (process.argv[2] === 'env') {
350
350
  const { canPrompt, askWithTimeout, selectMenu, menuSupported } = await import('./lib/tty.mjs');
351
351
  const interactive = canPrompt() && process.env.FLOWVIANT_REEXEC !== '1';
352
352
 
353
- /** How long the one-time binding confirm waits before serving unbound. A person
354
- * who just typed `flowviant` answers in seconds; anything longer is a restart
355
- * nobody is watching, and the machine must not sit dark for it. */
356
- const CONFIRM_TIMEOUT_MS = 20_000;
357
-
358
- /** The picker's own budget. Longer than the confirm: this one asks you to READ
359
- * a list before answering, and its fallback costs you a start rather than
360
- * costing you a binding. */
361
- const PICK_TIMEOUT_MS = 60_000;
353
+ /** NO ANSWER TIME LIMIT on the start path (2026-09-20, the owner: "why is
354
+ * there an answer time limit, remove that"). The confirm below carried 20s
355
+ * and the picker 60s, each arguing that a restart nobody is watching must
356
+ * not sit dark on a prompt — but `interactive` already excludes exactly that
357
+ * case (no foreground terminal, or a self-update re-exec, and nothing is
358
+ * asked). What was left was a person reading a list being told "no answer
359
+ * in 60s — nothing started". A prompt a person can see waits for the
360
+ * person; tty.mjs's header carries the argument. */
362
361
  const externalToken = process.argv.includes('--fleet') || Boolean(process.env.FLOWVIANT_FLEET);
363
362
 
364
363
  /** Re-exec a plain `flowviant` after an inline login — the login command's own
@@ -437,15 +436,7 @@ if (!FLEET_TOKEN) {
437
436
  const res = await selectMenu({
438
437
  options,
439
438
  defaultIndex: likely >= 0 ? likely : 0,
440
- timeoutMs: PICK_TIMEOUT_MS,
441
439
  });
442
- if (res.timedOut) {
443
- console.error(
444
- `\nno answer in ${Math.round(PICK_TIMEOUT_MS / 1000)}s — nothing started. ` +
445
- `Name one with \`--project <name|id>\`, or run \`flowviant\` here in the foreground and pick.`
446
- );
447
- process.exit(1);
448
- }
449
440
  if (res.cancelled) {
450
441
  console.error('nothing chosen — nothing started.');
451
442
  process.exit(1);
@@ -460,12 +451,13 @@ if (!FLEET_TOKEN) {
460
451
  console.log(` ${choices.length + 1}. ${loginLabel}`);
461
452
  const hint = likely >= 0 ? ` (enter for ${creds.projectLabel(choices[likely])})` : '';
462
453
  const raw = await askWithTimeout(
463
- `Which project should this daemon serve? [1-${choices.length + 1}]${hint} `,
464
- PICK_TIMEOUT_MS
454
+ `Which project should this daemon serve? [1-${choices.length + 1}]${hint} `
465
455
  );
466
456
  if (raw === null) {
457
+ // The read itself failed — stdin closed, or the terminal went away
458
+ // mid-question. Not a timeout: there is none.
467
459
  console.error(
468
- `\nno answer in ${Math.round(PICK_TIMEOUT_MS / 1000)}s — nothing started. ` +
460
+ `\ncould not read an answer — nothing started. ` +
469
461
  `Name one with \`--project <name|id>\`, or run \`flowviant\` here in the foreground and pick.`
470
462
  );
471
463
  process.exit(1);
@@ -519,7 +511,10 @@ if (!FLEET_TOKEN) {
519
511
  // stop here with the machine serving nothing. A guard that only works once
520
512
  // everyone already has it is not a guard.
521
513
  //
522
- // ON TIMEOUT WE SERVE, AND WE DO NOT BIND. Those are two decisions:
514
+ // IF THE READ FAILS WE SERVE, AND WE DO NOT BIND. (This used to say "on
515
+ // timeout"; there is no timeout since 2026-09-20 — a person at the prompt
516
+ // is waited for. `null` now means stdin closed under the question.) Those
517
+ // are two decisions:
523
518
  // · SERVE, because it is what every version before 0.55.0 did with this
524
519
  // exact store, so the silent path is the status quo rather than a new
525
520
  // risk — and a daemon that answers is strictly better than one that does
@@ -531,13 +526,12 @@ if (!FLEET_TOKEN) {
531
526
  const creds = await import('./lib/credentials.mjs');
532
527
  const label = creds.projectLabel(CREDENTIAL.entry);
533
528
  const answered = await askWithTimeout(
534
- `This machine's one connected project is ${label}. Serve this repo (${CREDENTIAL.repoRoot}) as ${label}? [Y/n] `,
535
- CONFIRM_TIMEOUT_MS
529
+ `This machine's one connected project is ${label}. Serve this repo (${CREDENTIAL.repoRoot}) as ${label}? [Y/n] `
536
530
  );
537
- const raw = answered === null ? null : answered.toLowerCase(); // null = nobody answered
531
+ const raw = answered === null ? null : answered.toLowerCase(); // null = the read failed
538
532
  if (raw === null) {
539
533
  console.log(
540
- `\n no answer in ${Math.round(CONFIRM_TIMEOUT_MS / 1000)}s — serving ${label} for this run ` +
534
+ `\n could not read an answer — serving ${label} for this run ` +
541
535
  `without tying it to this repo. Run \`flowviant\` here and answer to make it stick.`
542
536
  );
543
537
  } else if (raw === '' || raw === 'y' || raw === 'yes') {
package/bin/lib/tty.mjs CHANGED
@@ -19,13 +19,29 @@
19
19
  *
20
20
  * 2. `askWithTimeout()` — while asking, install no-op SIGTTIN/SIGTTOU handlers.
21
21
  * A handler (even an empty one) replaces the default STOP, so a misjudged
22
- * foreground check degrades to a read that fails or hangs — and a hang is
23
- * something the timer can now actually interrupt, because the process is
24
- * still running.
22
+ * foreground check degrades to a read that fails rather than a process
23
+ * that is frozen.
25
24
  *
26
25
  * The detection is best-effort by design and FAILS TOWARDS ASKING: an unknown
27
26
  * platform returns true, because refusing to prompt a human who IS there is a
28
- * worse failure than a prompt that times out on its own.
27
+ * worse failure than a prompt that waits.
28
+ *
29
+ * A PROMPT A PERSON IS LOOKING AT WAITS FOR THE PERSON (2026-09-20). Both
30
+ * helpers used to carry a mandatory timer — 20s on the binding confirm, 60s
31
+ * on the project picker — on the argument that "anything longer is a restart
32
+ * nobody is watching, and the machine must not sit dark for it". That
33
+ * argument was already answered one layer up: the start path never asks at
34
+ * all unless `canPrompt()` says a foreground terminal is attached AND the
35
+ * process is not a self-update re-exec, so the unattended restart the timer
36
+ * was defending against cannot reach a prompt. What the timer actually did
37
+ * was refuse a person who went to read the list, make a coffee, or answer a
38
+ * message — "no answer in 60s — nothing started" — on the one path where
39
+ * nothing has been started yet and waiting costs nothing. The owner's ruling,
40
+ * verbatim: "why is there an answer time limit, remove that." So `timeoutMs`
41
+ * is OPTIONAL now and the start path passes none; a caller that has a real
42
+ * reason to bound a wait (the install prompt, where silence is "no") may
43
+ * still pass one. The signal handlers stay — they were never about the timer,
44
+ * they are what keeps a backgrounded job from freezing.
29
45
  */
30
46
 
31
47
  import { readFileSync } from 'node:fs';
@@ -80,11 +96,21 @@ export function canPrompt() {
80
96
  return Boolean(process.stdin.isTTY && process.stdout.isTTY) && inForeground();
81
97
  }
82
98
 
99
+ /** A timer only when a caller asked for one. `clearTimeout(null)` is a no-op,
100
+ * so the finally blocks need no branch. Non-finite or non-positive means
101
+ * "wait for the person". */
102
+ export function boundedTimer(timeoutMs, fire) {
103
+ return Number.isFinite(timeoutMs) && timeoutMs > 0 ? setTimeout(fire, timeoutMs) : null;
104
+ }
105
+
83
106
  /**
84
- * Ask, and come back no matter what. Resolves the trimmed answer, or `null`
85
- * when nobody answered within `timeoutMs` — the caller decides what silence
86
- * means, because it is not the same answer everywhere (the binding confirm
87
- * serves unbound; the project picker refuses, exactly as it does headless).
107
+ * Ask, and come back with an answer or a refusal. Resolves the trimmed
108
+ * answer, or `null` when the read failed (stdin closed under us, the terminal
109
+ * went away) or — only when a caller passed a finite `timeoutMs` — when nobody
110
+ * answered in time. With no `timeoutMs` it waits for the person; see the
111
+ * header. The caller decides what `null` means, because it is not the same
112
+ * answer everywhere (the binding confirm serves unbound; the project picker
113
+ * refuses, exactly as it does headless).
88
114
  */
89
115
  export async function askWithTimeout(query, timeoutMs) {
90
116
  const noop = () => {};
@@ -97,7 +123,7 @@ export async function askWithTimeout(query, timeoutMs) {
97
123
  output: process.stdout,
98
124
  });
99
125
  const ac = new AbortController();
100
- const timer = setTimeout(() => ac.abort(), timeoutMs);
126
+ const timer = boundedTimer(timeoutMs, () => ac.abort());
101
127
  try {
102
128
  return (await rl.question(query, { signal: ac.signal })).trim();
103
129
  } catch {
@@ -153,15 +179,15 @@ const MENU_FOOTER = '↑/↓ move · enter select · 1–9 jump · esc cancel';
153
179
  * An arrow-navigable picker for the start path. Resolves one of:
154
180
  * { index } the row taken
155
181
  * { cancelled } Esc/q/Ctrl-C
156
- * { timedOut } nobody drove it within timeoutMs
182
+ * { timedOut } a caller passed `timeoutMs` and nobody drove it in time
183
+ * (the start path passes none — see the header)
157
184
  * { unsupported } no raw mode — the caller uses the numeric prompt instead
158
185
  *
159
- * Held to this file's one law — no start-path prompt may hang the daemon — the
160
- * same way `askWithTimeout` is: it keeps the timeout, installs the
161
- * SIGTTIN/SIGTTOU no-ops that keep that timer alive, restores the terminal in
162
- * every exit, and refuses (returns `unsupported`) rather than half-drawing when
163
- * raw mode is not really there. The caller still gates on `canPrompt()` before
164
- * ever reaching here.
186
+ * Held to this file's one law — no start-path prompt may STOP the daemon — the
187
+ * same way `askWithTimeout` is: it installs the SIGTTIN/SIGTTOU no-ops,
188
+ * restores the terminal in every exit, and refuses (returns `unsupported`)
189
+ * rather than half-drawing when raw mode is not really there. The caller still
190
+ * gates on `canPrompt()` before ever reaching here.
165
191
  */
166
192
  export async function selectMenu({ options, defaultIndex = 0, timeoutMs }) {
167
193
  if (!menuSupported()) return { unsupported: true };
@@ -220,7 +246,7 @@ export async function selectMenu({ options, defaultIndex = 0, timeoutMs }) {
220
246
  };
221
247
  process.on('SIGTTIN', noop);
222
248
  process.on('SIGTTOU', noop);
223
- const timer = setTimeout(() => finish({ timedOut: true }), timeoutMs);
249
+ const timer = boundedTimer(timeoutMs, () => finish({ timedOut: true }));
224
250
  try {
225
251
  stdin.setRawMode(true);
226
252
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.91.0",
3
+ "version": "0.91.1",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant \u2014 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": {