flowviant 0.95.0 → 0.96.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/README.md CHANGED
@@ -54,7 +54,7 @@ Launch with `@latest` so each start pulls the newest published version — a bar
54
54
 
55
55
  ## Machines on this box
56
56
 
57
- One box can serve several projects — one daemon per repository directory, each connected with its own `flowviant login` run inside that repo. `flowviant machines` lists every project connected on this box with the id and date it was connected, and under each one every computer that has polled it (the app's Home lists the same across your whole account). Two projects bound to one repository are named out loud at the foot of the listing, because that is the one shape that reads as "the same project connected twice" and is not: every `npx flowviant` in that directory has to ask which of them to serve.
57
+ One box can serve several projects — one daemon per repository directory, each connected with its own `flowviant login` run inside that repo. `flowviant machines` lists every project connected on this box with the id and date it was connected, and under each one every computer that has polled it (the app's Home lists the same across your whole account). Two projects bound to one repository are named out loud at the foot of the listing, because that is the one shape that reads as "the same project connected twice" and is not — and a directory serves one project, so `npx flowviant` there refuses to start until one of them goes.
58
58
 
59
59
  On a terminal the listing is a menu: ↑/↓ over the projects, enter for what you can do about one, esc to leave. Two verbs, each acting on **this box's own connection** and nothing else:
60
60
 
@@ -63,7 +63,7 @@ On a terminal the listing is a menu: ↑/↓ over the projects, enter for what y
63
63
 
64
64
  Stopping or removing a daemon on **another** computer is done in the app (project settings → Machines), where the person pressing it can see what they are changing.
65
65
 
66
- `flowviant login` asks before it binds a second project to a repository that is already connected to one: keep both (every start there asks which to serve), replace (the repo serves the new project and this box is disconnected from the old one), or cancel.
66
+ **A directory serves one project.** `flowviant login` refuses to bind a second project to a repository that is already connected to one, and a store that already holds two projects for one repository refuses to start there — both name the projects and the remedy: delete the project you do not mean in Flowviant (project settings → General → Delete project), or disconnect this box from it with `flowviant machines`, then try again.
67
67
 
68
68
  ## Sessions
69
69
 
package/bin/cli.mjs CHANGED
@@ -46,7 +46,10 @@
46
46
  * a daemon on another computer is invisible from here; since 0.95.0 it says
47
47
  * out loud when two projects are bound to one repo, and on a terminal it is a
48
48
  * menu (↑/↓, enter) whose verbs disconnect THIS box from a project or forget
49
- * a credential here — `--remove <id>` / `--forget <id>` for a script.
49
+ * a credential here — `--remove <id>` / `--forget <id>` for a script. A
50
+ * DIRECTORY SERVES ONE PROJECT: login refuses to bind a second project to a
51
+ * repo, and a store that already holds two for one repo refuses to start
52
+ * until one is deleted in the app or disconnected here.
50
53
  *
51
54
  * Env:
52
55
  * FLOWVIANT_MACHINE_TOKEN the machine credential (or use `flowviant login`);
@@ -89,7 +92,8 @@ if (process.argv[2] === 'login') {
89
92
  // A login the person CANCELLED at the second-project question saved nothing,
90
93
  // so there is no credential for the child to serve — starting it would end
91
94
  // in "no credential found" over a choice they just made on purpose.
92
- if (noStart || !login?.saved) process.exit(0);
95
+ if (!login?.saved) process.exit(1); // refused: the directory already serves another project
96
+ if (noStart) process.exit(0);
93
97
  // Re-exec as a plain `flowviant` rather than falling through. config.mjs reads
94
98
  // the credential at IMPORT time — which was before the login we just did — so
95
99
  // this process still has an empty FLEET_TOKEN and would exit with "no
@@ -460,7 +464,7 @@ const externalToken =
460
464
  * existed, so this process cannot serve; the child can. */
461
465
  async function reexecAfterLogin() {
462
466
  const login = await runLogin({ thenStart: false });
463
- if (!login?.saved) process.exit(0); // cancelled at the second-project question — nothing to serve
467
+ if (!login?.saved) process.exit(1); // refused: the directory already serves another project
464
468
  const { spawn } = await import('node:child_process');
465
469
  const child = spawn(process.execPath, [process.argv[1]], { stdio: 'inherit', env: process.env });
466
470
  process.exit(await new Promise((resolve) => child.on('exit', (code) => resolve(code ?? 0))));
@@ -470,7 +474,9 @@ async function reexecAfterLogin() {
470
474
  // the connected date to the COLLIDING rows only — production held two projects
471
475
  // both called "BRIF AI", both bound to the same checkout, so this picker
472
476
  // offered two identical lines and the only way to answer was to guess. See
473
- // credentials.mjs for why the suffix is not on every row.
477
+ // credentials.mjs for why the suffix is not on every row. (That exact case is
478
+ // a refusal now rather than a picker — a directory serves one project — but
479
+ // two same-named projects can still meet in the no-match picker.)
474
480
  function listLines(entries, { projectRowLabel }) {
475
481
  return entries
476
482
  .map(
@@ -486,6 +492,20 @@ if (!FLEET_TOKEN) {
486
492
  console.error(`error: ${CREDENTIAL.error}. \`flowviant projects\` lists what is stored.`);
487
493
  process.exit(1);
488
494
  }
495
+ // A DIRECTORY SERVES ONE PROJECT (2026-09-23, the owner, verbatim: "a
496
+ // directory cannot have more than one project on flowviant. if one already
497
+ // exists, it would warn the user and ask them to delete the project on
498
+ // flowviant first. because 2 projects shouldnt be able to edit a directory
499
+ // at the same time."). This used to be a PICKER — two projects bound to one
500
+ // checkout, choose which this daemon serves — which is a control offering to
501
+ // do the forbidden thing politely. It is a refusal now, on a TTY and
502
+ // headless alike, naming both projects and the remedy. `credentialRefusal`
503
+ // is the same sentence login prints when it declines to bind a second one.
504
+ if (CREDENTIAL.reason === 'multiple-bound') {
505
+ const { directoryTakenRefusal } = await import('./lib/credentials.mjs');
506
+ console.error(directoryTakenRefusal(CREDENTIAL.choices, CREDENTIAL.repoRoot));
507
+ process.exit(1);
508
+ }
489
509
  if (CREDENTIAL.choices?.length && interactive) {
490
510
  const creds = await import('./lib/credentials.mjs');
491
511
  const { originSlug } = await import('./lib/git.mjs');
@@ -494,9 +514,7 @@ if (!FLEET_TOKEN) {
494
514
  console.log(
495
515
  CREDENTIAL.reason === 'outside-repo'
496
516
  ? 'flowviant is not inside a git repo, and more than one project is connected here.'
497
- : CREDENTIAL.reason === 'multiple-bound'
498
- ? `More than one connected project names this repo (${repoRoot}) — pick which one this daemon serves:`
499
- : `This repo (${repoRoot}) is not connected to any project yet. Connected on this machine:`
517
+ : `This repo (${repoRoot}) is not connected to any project yet. Connected on this machine:`
500
518
  );
501
519
 
502
520
  const loginLabel = `connect ${repoRoot ? 'this repo' : 'a repo'} to a different project (flowviant login)`;
@@ -505,16 +523,13 @@ if (!FLEET_TOKEN) {
505
523
  // this only decides which row the cursor starts on, using the repo's folder
506
524
  // name and its github repo-name against the stored project names. A unique
507
525
  // match becomes ONE keypress; a wrong guess costs nothing, because the human
508
- // still confirms. `multiple-bound` gets no hint — every choice already names
509
- // this repo, so nothing distinguishes them.
526
+ // still confirms. (Two projects bound to THIS repo never reach here: that
527
+ // is a refusal above, since 2026-09-23.)
510
528
  const slug = repoRoot ? originSlug(repoRoot) : null;
511
- const likely =
512
- CREDENTIAL.reason === 'multiple-bound'
513
- ? -1
514
- : creds.likelyChoiceIndex(choices, {
515
- repoBasename: repoRoot ? basename(repoRoot) : null,
516
- repoSlugName: slug ? slug.split('/')[1] : null,
517
- });
529
+ const likely = creds.likelyChoiceIndex(choices, {
530
+ repoBasename: repoRoot ? basename(repoRoot) : null,
531
+ repoSlugName: slug ? slug.split('/')[1] : null,
532
+ });
518
533
 
519
534
  // Bounded like the confirm below, and for the same reason — but silence
520
535
  // means something DIFFERENT here and the difference is load-bearing. There
@@ -227,6 +227,42 @@ export function boundElsewhere(entries, repoRoot, projectId) {
227
227
  return entries.filter((e) => e.projectId !== projectId && samePath(e.repoRoot, repoRoot));
228
228
  }
229
229
 
230
+ /**
231
+ * THE REFUSAL, in one place, because two commands print it (2026-09-23).
232
+ *
233
+ * The owner, verbatim: "a directory cannot have more than one project on
234
+ * flowviant. if one already exists, it would warn the user and ask them to
235
+ * delete the project on flowviant first. because 2 projects shouldnt be able
236
+ * to edit a directory at the same time." So `flowviant login` refuses to bind
237
+ * a second project to a repo, and a start in a repo the store already holds
238
+ * two projects for refuses to serve either — the picker that used to stand
239
+ * there was a control offering to do the forbidden thing politely.
240
+ *
241
+ * It names every project on the directory, the rule, and the remedy in the
242
+ * owner's own order: delete the project in Flowviant first. The disconnect
243
+ * this box can do on its own (`flowviant machines`) is named second, for the
244
+ * case where the project must survive somewhere else — it satisfies the same
245
+ * rule, because a project this box no longer serves cannot edit the directory
246
+ * from here.
247
+ */
248
+ export function directoryTakenRefusal(bound, repoRoot, { incoming } = {}) {
249
+ // Every project in the sentence is labelled against the WHOLE set, the
250
+ // incoming one included: a refusal reading "already connected to BRIF AI,
251
+ // so BRIF AI was not connected" is the puzzle it exists to end.
252
+ const all = [...bound, ...(incoming ? [incoming] : [])];
253
+ const names = bound.map((e) => projectRowLabel(e, all));
254
+ const list = names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}` : names[0];
255
+ const head = incoming
256
+ ? `${repoRoot} is already connected to ${list}, so ${projectRowLabel(incoming, all)} was not connected here.`
257
+ : `${repoRoot} is connected to ${list}.`;
258
+ return (
259
+ `${head}\n` +
260
+ 'A directory serves one project — two projects must not edit it at the same time.\n' +
261
+ 'Delete the project you do not mean in Flowviant first (project settings → General → Delete project),\n' +
262
+ 'or disconnect this box from it with `flowviant machines`, then run this again.'
263
+ );
264
+ }
265
+
230
266
  /** Collapse a name or slug for loose comparison: "My Project", "my-project"
231
267
  * and "myproject" all become "myproject". */
232
268
  function normalizeName(s) {
@@ -387,6 +423,9 @@ export function matchStoredProject(ref) {
387
423
  * this returns. Shapes:
388
424
  * { entry, source: 'project-flag' | 'repo' | 'only', needsConfirm? }
389
425
  * { choices, reason: 'no-match' | 'multiple-bound' | 'outside-repo', repoRoot }
426
+ * (`multiple-bound` is a REFUSAL in cli.mjs since 2026-09-23 — a directory
427
+ * serves one project — and never a picker; the shape is kept because it
428
+ * names the projects the refusal has to print)
390
429
  * { none: true }
391
430
  * { error }
392
431
  *
package/bin/lib/login.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { FLEET_URL, USER_AGENT, VERSION } from './config.mjs';
10
- import { saveLogin, detectRepoRoot, projectLabel, listStoredProjects, boundElsewhere } from './credentials.mjs';
10
+ import { saveLogin, detectRepoRoot, projectLabel, listStoredProjects, boundElsewhere, directoryTakenRefusal } from './credentials.mjs';
11
11
  import { c, info, ok, warn, fail } from './ui.mjs';
12
12
  import { sleep } from './claude.mjs';
13
13
 
@@ -27,58 +27,6 @@ async function post(url, body) {
27
27
  return j.data ?? j;
28
28
  }
29
29
 
30
- /**
31
- * THE THREE ANSWERS to "this repo is already connected to another project",
32
- * as menu rows — pure, so the wording is provable. Each row says its whole
33
- * consequence, the same rule the `machines` menu keeps: a verb whose outcome
34
- * lives in a manual is one you have to be sure about before you can use it.
35
- *
36
- * keep both stay; `npx flowviant` in this repo asks which to serve.
37
- * replace this repo serves the NEW project; this box is disconnected from
38
- * the old one (its daemon here stopped, its row left in the app,
39
- * its credential forgotten here) — the whole disconnect, because a
40
- * half of it leaves a daemon running on a credential the store no
41
- * longer names.
42
- * cancel nothing is saved. The approval in the app minted or reused the
43
- * project's credential either way; a credential nobody holds is a
44
- * row, not a machine.
45
- */
46
- export function secondProjectOptions(clash, entry, repoRoot) {
47
- const olds = clash.map(projectLabel).join(' and ');
48
- const now = projectLabel(entry);
49
- return [
50
- `keep both — every \`npx flowviant\` in ${repoRoot} asks whether to serve ${olds} or ${now}`,
51
- `replace — ${repoRoot} serves ${now} from now on; this box is disconnected from ${olds} (its daemon here stopped, its credential forgotten here)`,
52
- `cancel — save nothing; ${repoRoot} stays connected to ${olds}`,
53
- ];
54
- }
55
-
56
- const DECISIONS = ['keep', 'replace', 'cancel'];
57
-
58
- /** Ask, on whatever this terminal can draw. No terminal to ask on: KEEP, and
59
- * say so — a headless login must not hang, and refusing would strand a
60
- * runner that had just been approved in the app. */
61
- async function secondProjectDecision({ clash, entry, repoRoot }) {
62
- const { canPrompt, menuSupported, selectMenu, askWithTimeout } = await import('./tty.mjs');
63
- const olds = clash.map(projectLabel).join(' and ');
64
- console.log('');
65
- warn(`${repoRoot} is already connected to ${olds} on this box.`);
66
- if (!canPrompt()) {
67
- console.log(` keeping both — every \`npx flowviant\` there will ask which to serve; \`flowviant machines\` removes one.\n`);
68
- return 'keep';
69
- }
70
- const options = secondProjectOptions(clash, entry, repoRoot);
71
- if (menuSupported()) {
72
- const res = await selectMenu({ options });
73
- if (res.cancelled) return 'cancel';
74
- if (!res.unsupported) return DECISIONS[res.index];
75
- }
76
- console.log(options.map((o, i) => ` ${i + 1}. ${o}`).join('\n'));
77
- const raw = await askWithTimeout(' Which? [1-3] ');
78
- const n = Number.parseInt(raw ?? '', 10);
79
- return DECISIONS[n - 1] ?? 'cancel';
80
- }
81
-
82
30
  export async function runLogin({ thenStart = false } = {}) {
83
31
  console.log(`\n ${c.bold(c.cyan('◣ flowviant'))} ${c.dim(`login · v${VERSION}`)}\n`);
84
32
  let start;
@@ -125,28 +73,23 @@ export async function runLogin({ thenStart = false } = {}) {
125
73
  name: typeof poll.projectName === 'string' && poll.projectName ? poll.projectName : null,
126
74
  repoRoot,
127
75
  };
128
- // A SECOND PROJECT ON A REPO IS A QUESTION, NEVER A SILENT SAVE
129
- // (2026-09-23). The owner: "im not sure how it even allowed me to run
130
- // npx flowviant login twice and init a daemon twice on the same
131
- // project/repository/directory in the first place." It allowed it
132
- // because it never looked: the approval names a project, this repo was
133
- // already bound to a different one, and both landed in the store — after
134
- // which every start here was a picker between two rows that read alike.
135
- // Asked while the person is still at the terminal; headless, the save
136
- // goes ahead and the fact is printed, because a CI runner cannot answer
137
- // and a login that silently refused would be worse than one that said.
76
+ // A DIRECTORY SERVES ONE PROJECT (2026-09-23). The owner, verbatim: "a
77
+ // directory cannot have more than one project on flowviant. if one
78
+ // already exists, it would warn the user and ask them to delete the
79
+ // project on flowviant first. because 2 projects shouldnt be able to
80
+ // edit a directory at the same time." And his earlier question — "im not
81
+ // sure how it even allowed me to run npx flowviant login twice … on the
82
+ // same repository" — was answered by the fact that login never looked.
83
+ // It looks now, and REFUSES: the approval in the app minted or reused
84
+ // the project's credential either way, and a credential nobody holds is
85
+ // a row, not a machine. The refusal names both projects and the remedy
86
+ // (`directoryTakenRefusal`, shared with the daemon's own start).
138
87
  const clash = boundElsewhere(listStoredProjects(), repoRoot, entry.projectId);
139
88
  if (clash.length > 0) {
140
- const decision = await secondProjectDecision({ clash, entry, repoRoot });
141
- if (decision === 'cancel') {
142
- console.log(`\n nothing saved — ${repoRoot} stays connected to ${clash.map(projectLabel).join(' and ')}.\n`);
143
- return { saved: false };
144
- }
145
- if (decision === 'replace') {
146
- const { disconnectHere, realDisconnectDeps, leaveUrlFrom } = await import('./machines.mjs');
147
- const deps = await realDisconnectDeps({ url: leaveUrlFrom(FLEET_URL), log: (m) => console.log(` ${m}`) });
148
- for (const old of clash) await disconnectHere(old, deps, { log: (m) => console.log(` ${m}`) });
149
- }
89
+ console.log('');
90
+ warn(directoryTakenRefusal(clash, repoRoot, { incoming: entry }));
91
+ console.log('');
92
+ return { saved: false };
150
93
  }
151
94
  saveLogin(entry);
152
95
  ok(
@@ -277,10 +277,10 @@ export function projectPhrase(e) {
277
277
  * daemons for one project. The id on each row said they were different
278
278
  * projects; nothing said they were bound to the SAME checkout, which is the
279
279
  * fact that makes it a duplicate rather than two rows. So the listing ends
280
- * with one line per such repo, naming every project on it in full, and the
281
- * consequence the store already enforces: every start in that directory asks
282
- * which project to serve. It states no remedy of its own — the menu below it
283
- * (or the footer, when there is no menu) is where the verbs are.
280
+ * with one line per such repo, naming every project on it in full, the rule
281
+ * (a directory serves one project — the owner's 2026-09-23 ruling, which made
282
+ * the start there a refusal), and the remedy in his order: delete the project
283
+ * in Flowviant, or disconnect this box from it (the menu below, or the flags).
284
284
  *
285
285
  * Pure over the entries. An unbound project collides with nothing.
286
286
  */
@@ -290,8 +290,8 @@ export function renderCollisions(entries) {
290
290
  const list = `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
291
291
  return (
292
292
  ` ! ${g.entries.length} projects are connected for ${g.repoRoot}: ${list} — ` +
293
- 'one repo serves one project, so every `npx flowviant` there asks which. ' +
294
- 'If one of them is a duplicate, disconnect this box from it.'
293
+ 'a directory serves one project, and `npx flowviant` there refuses to start until it does. ' +
294
+ 'Delete the one you do not mean in Flowviant (project settings → General → Delete project), or disconnect this box from it.'
295
295
  );
296
296
  });
297
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
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": {