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 +2 -2
- package/bin/cli.mjs +31 -16
- package/bin/lib/credentials.mjs +39 -0
- package/bin/lib/login.mjs +16 -73
- package/bin/lib/machines.mjs +6 -6
- package/package.json +1 -1
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
|
|
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`
|
|
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 (
|
|
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(
|
|
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
|
-
:
|
|
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.
|
|
509
|
-
//
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
package/bin/lib/credentials.mjs
CHANGED
|
@@ -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
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// project
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
package/bin/lib/machines.mjs
CHANGED
|
@@ -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,
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
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
|
-
'
|
|
294
|
-
'
|
|
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.
|
|
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": {
|