flowviant 0.99.0 → 0.101.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 +18 -12
- package/bin/cli.mjs +108 -25
- package/bin/lib/config.mjs +12 -8
- package/bin/lib/credentials.mjs +2 -1
- package/bin/lib/desktopContract.mjs +227 -0
- package/bin/lib/fleet.mjs +22 -2
- package/bin/lib/instance.mjs +14 -0
- package/bin/lib/launchCommand.mjs +23 -0
- package/bin/lib/login.mjs +34 -10
- package/bin/lib/machines.mjs +8 -7
- package/bin/lib/mcp-cli.mjs +4 -3
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/tty.mjs +2 -2
- package/bin/lib/uninstall.mjs +286 -0
- package/bin/lib/update.mjs +116 -25
- package/bin/lib/workAgentTurns.mjs +4 -1
- package/package.json +1 -2
package/bin/lib/fleet.mjs
CHANGED
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
} from './config.mjs';
|
|
40
40
|
import { projectLabel, safeName, setStoredProjectName } from './credentials.mjs';
|
|
41
41
|
import { credentialRejected } from './authReject.mjs';
|
|
42
|
-
import { handleVersionSignal } from './update.mjs';
|
|
42
|
+
import { handleVersionSignal, cmpVersion } from './update.mjs';
|
|
43
|
+
import { emitMachineEvent, writeDaemonState } from './desktopContract.mjs';
|
|
43
44
|
import {
|
|
44
45
|
git,
|
|
45
46
|
gitNetAsync,
|
|
@@ -1128,6 +1129,8 @@ export async function standDownDisplaced({
|
|
|
1128
1129
|
? `this project's machine moved to ${name} — standing down.`
|
|
1129
1130
|
: "this project's machine moved to another box — standing down."
|
|
1130
1131
|
);
|
|
1132
|
+
if (removed) emitMachineEvent({ event: 'stopped', reason: 'removed' });
|
|
1133
|
+
else emitMachineEvent({ event: 'displaced', message: displacedTurnSentence(by) });
|
|
1131
1134
|
// FIRST, and awaited: an unsettled turn is the one thing here that no later
|
|
1132
1135
|
// poll from anybody can fix — this process holds the only copy of the fact
|
|
1133
1136
|
// that it was running.
|
|
@@ -1160,6 +1163,10 @@ export async function standDownDisplaced({
|
|
|
1160
1163
|
// base between tasks (fresh conversation), resume in place while on a blocker.
|
|
1161
1164
|
|
|
1162
1165
|
export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
1166
|
+
let desktopProjectId = CREDENTIAL?.entry?.projectId ?? null;
|
|
1167
|
+
let wasServing = false;
|
|
1168
|
+
if (process.env.FLOWVIANT_UPDATE_TARGET && cmpVersion(VERSION, process.env.FLOWVIANT_UPDATE_TARGET) >= 0)
|
|
1169
|
+
emitMachineEvent({ event: 'update-applied', from: process.env.FLOWVIANT_UPDATE_FROM ?? null, to: VERSION });
|
|
1163
1170
|
console.log('');
|
|
1164
1171
|
console.log(` ${c.bold(c.cyan('◣ flowviant'))} ${c.dim(`machine daemon · v${VERSION}`)}`);
|
|
1165
1172
|
console.log(` ${c.dim('──────────────────────────────────────────────')}`);
|
|
@@ -1279,6 +1286,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
1279
1286
|
} catch {
|
|
1280
1287
|
/* the binding is asked again next time */
|
|
1281
1288
|
}
|
|
1289
|
+
writeDaemonState(desktopProjectId, { limits: {}, holder: null });
|
|
1282
1290
|
|
|
1283
1291
|
await preflight({ needGit: true });
|
|
1284
1292
|
|
|
@@ -1443,6 +1451,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
1443
1451
|
// A SAME-REPO TAKEOVER IS DELIBERATELY NOT DISTINGUISHED, and that is a
|
|
1444
1452
|
};
|
|
1445
1453
|
process.on('SIGINT', () => {
|
|
1454
|
+
emitMachineEvent({ event: 'stopped', reason: 'signal' });
|
|
1446
1455
|
console.log('');
|
|
1447
1456
|
note('shutting down — stopping workers. Worktrees are kept: in-flight work resumes next run.');
|
|
1448
1457
|
teardown();
|
|
@@ -1452,6 +1461,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
1452
1461
|
// handler every child survived a `systemctl stop` — the exact orphaning the
|
|
1453
1462
|
// teardown exists to prevent.
|
|
1454
1463
|
process.on('SIGTERM', () => {
|
|
1464
|
+
emitMachineEvent({ event: 'stopped', reason: 'signal' });
|
|
1455
1465
|
console.log('');
|
|
1456
1466
|
note('shutting down (SIGTERM) — stopping workers. Worktrees are kept: in-flight work resumes next run.');
|
|
1457
1467
|
teardown();
|
|
@@ -2407,6 +2417,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
2407
2417
|
);
|
|
2408
2418
|
} catch (e) {
|
|
2409
2419
|
if (e.auth) {
|
|
2420
|
+
emitMachineEvent({ event: 'stopped', reason: 'credential-revoked' });
|
|
2410
2421
|
fail(`${e.message} — credential revoked or invalid. Shutting down.`);
|
|
2411
2422
|
teardown();
|
|
2412
2423
|
// EXIT 0, for the same reason the commanded-stop path does: a revoked
|
|
@@ -2467,6 +2478,8 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
2467
2478
|
}
|
|
2468
2479
|
})();
|
|
2469
2480
|
}
|
|
2481
|
+
if (roster.project?.id) desktopProjectId = roster.project.id;
|
|
2482
|
+
writeDaemonState(desktopProjectId, { lastPoll: new Date().toISOString() });
|
|
2470
2483
|
if (roster.mcpUrl) mcpUrl = roster.mcpUrl;
|
|
2471
2484
|
/**
|
|
2472
2485
|
* HOW MANY TURNS THE APP SAYS THIS MACHINE MAY RUN (2026-09-17).
|
|
@@ -2495,6 +2508,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
2495
2508
|
// going away.
|
|
2496
2509
|
const stopSignal = shouldStop(roster.daemon);
|
|
2497
2510
|
if (stopSignal) {
|
|
2511
|
+
emitMachineEvent({ event: 'stopped', reason: stopSignal.reason ?? null });
|
|
2498
2512
|
warn(
|
|
2499
2513
|
stopSignal.reason
|
|
2500
2514
|
? `stopped by Flowviant — ${stopSignal.reason}`
|
|
@@ -2599,6 +2613,12 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
2599
2613
|
* would start deleting worktrees it cannot see the tabs for.
|
|
2600
2614
|
*/
|
|
2601
2615
|
holderState = holderWatch.observe(roster.holder);
|
|
2616
|
+
writeDaemonState(desktopProjectId, {
|
|
2617
|
+
lastPoll: new Date().toISOString(),
|
|
2618
|
+
holder: ({ mine: 'serving', standby: 'inactive' })[holderState] ?? null,
|
|
2619
|
+
});
|
|
2620
|
+
if (holderState === 'mine' && !wasServing) emitMachineEvent({ event: 'serving' });
|
|
2621
|
+
wasServing = holderState === 'mine';
|
|
2602
2622
|
// Keep the daemon current. Safe = no worker mid-task (true at startup, since
|
|
2603
2623
|
// no workers are spawned yet). If it self-updates it re-execs into the new
|
|
2604
2624
|
// version and this process becomes a proxy — stop the loop.
|
|
@@ -2620,7 +2640,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
|
|
|
2620
2640
|
safeToUpdate,
|
|
2621
2641
|
teardown,
|
|
2622
2642
|
});
|
|
2623
|
-
if (updating) return;
|
|
2643
|
+
if (await updating) return;
|
|
2624
2644
|
}
|
|
2625
2645
|
// Settle any turn/ship answers whose earlier report POST failed BEFORE
|
|
2626
2646
|
// taking new work — the skip-if-pending guards make the ordering safe, but
|
package/bin/lib/instance.mjs
CHANGED
|
@@ -139,6 +139,20 @@ function readHolder(path) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** Read-only desktop measurement. No lock is unknown because an unguarded or
|
|
143
|
+
* older daemon may still run; a stale lock with a dead pid measures empty. */
|
|
144
|
+
export function daemonRunningFor(fleetToken) {
|
|
145
|
+
const path = instanceLockPath(fleetToken);
|
|
146
|
+
let raw;
|
|
147
|
+
try { raw = readFileSync(path, 'utf8'); }
|
|
148
|
+
catch { return null; }
|
|
149
|
+
let holder;
|
|
150
|
+
try { holder = JSON.parse(raw); } catch { return null; }
|
|
151
|
+
if (!Number.isInteger(holder?.pid) || holder.pid <= 0) return null;
|
|
152
|
+
if (!alive(holder.pid)) return false;
|
|
153
|
+
return stillTheHolder(holder);
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
/**
|
|
143
157
|
* argv[1] AS IT WAS TYPED, when that differs from the resolved form.
|
|
144
158
|
*
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
|
|
3
|
+
/** The updater and terminal advice share one detection of the launch channel. */
|
|
4
|
+
export function runningViaNpx() {
|
|
5
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
6
|
+
const argv1 = process.argv[1] || '';
|
|
7
|
+
let self = '';
|
|
8
|
+
try {
|
|
9
|
+
self = fileURLToPath(import.meta.url);
|
|
10
|
+
} catch {
|
|
11
|
+
/* non-file URL — ignore */
|
|
12
|
+
}
|
|
13
|
+
return /\bnpx\b/.test(ua) || /[\\/]_npx[\\/]/.test(argv1) || /[\\/]_npx[\\/]/.test(self);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Name the command that launched this process when giving a person a next step. */
|
|
17
|
+
export function launchCommand() {
|
|
18
|
+
return 'flowviant';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function terminalCommand(args = '') {
|
|
22
|
+
return `${launchCommand()}${args ? ` ${args}` : ''}`;
|
|
23
|
+
}
|
package/bin/lib/login.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { FLEET_URL, USER_AGENT, VERSION } from './config.mjs';
|
|
|
10
10
|
import { saveLogin, detectRepoRoot, projectLabel, safeName, 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
|
+
import { launchCommand, terminalCommand } from './launchCommand.mjs';
|
|
13
14
|
|
|
14
15
|
const DEVICE_START = FLEET_URL.replace(/\/agents\/?$/, '/device/start');
|
|
15
16
|
const DEVICE_POLL = FLEET_URL.replace(/\/agents\/?$/, '/device/poll');
|
|
@@ -27,24 +28,35 @@ async function post(url, body) {
|
|
|
27
28
|
return j.data ?? j;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export async function runLogin({ thenStart = false } = {}) {
|
|
31
|
-
|
|
31
|
+
export async function runLogin({ thenStart = false, json = false, dir = process.cwd() } = {}) {
|
|
32
|
+
const event = (value) => { if (json) process.stdout.write(`${JSON.stringify(value)}\n`); };
|
|
33
|
+
const selectedRepo = json ? detectRepoRoot(dir) : null;
|
|
34
|
+
if (json && !selectedRepo) { event({ event: 'error', message: `No git repository found in ${dir}.` }); return { saved: false }; }
|
|
35
|
+
if (!json) console.log(`\n ${c.bold(c.cyan('◣ flowviant'))} ${c.dim(`login · v${VERSION}`)}\n`);
|
|
32
36
|
let start;
|
|
33
37
|
try {
|
|
34
38
|
start = await post(DEVICE_START, {});
|
|
35
39
|
} catch (e) {
|
|
40
|
+
if (json) { event({ event: 'error', message: `couldn't reach Flowviant (${e.message}).` }); return { saved: false }; }
|
|
36
41
|
fail(`couldn't reach Flowviant (${e.message}).`);
|
|
37
42
|
process.exit(1);
|
|
38
43
|
}
|
|
39
44
|
const { deviceCode, userCode, intervalSeconds = 5, expiresInSeconds = 600 } = start;
|
|
45
|
+
if (json && (typeof deviceCode !== 'string' || typeof userCode !== 'string' || userCode.length < 8)) {
|
|
46
|
+
event({ event: 'error', message: 'Flowviant returned an invalid login code.' });
|
|
47
|
+
return { saved: false };
|
|
48
|
+
}
|
|
40
49
|
const pretty = `${userCode.slice(0, 4)}-${userCode.slice(4)}`;
|
|
41
50
|
// Where the control ACTUALLY is. It was "the Agents panel", a settings
|
|
42
51
|
// section deleted 2026-08-17; connecting a machine is offered on the surface
|
|
43
52
|
// you are on when it matters, and for a new operator that is the Workbench —
|
|
44
53
|
// the project's empty state says so before it can show you any sessions.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
if (json) event({ event: 'open_url', url: APP_URL, code: pretty });
|
|
55
|
+
else {
|
|
56
|
+
console.log(` 1. Open ${c.cyan(APP_URL)} → your project → the ${c.bold('Workbench')} → ${c.bold('Connect a machine')}.`);
|
|
57
|
+
console.log(` 2. Enter this code: ${c.bold(c.green(pretty))}\n`);
|
|
58
|
+
info('waiting for you to approve…');
|
|
59
|
+
}
|
|
48
60
|
|
|
49
61
|
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
50
62
|
while (Date.now() < deadline) {
|
|
@@ -56,6 +68,10 @@ export async function runLogin({ thenStart = false } = {}) {
|
|
|
56
68
|
continue; // transient — keep polling
|
|
57
69
|
}
|
|
58
70
|
if (poll.status === 'approved') {
|
|
71
|
+
if (json && (!poll.projectId || !(poll.machineToken ?? poll.fleetToken))) {
|
|
72
|
+
event({ event: 'error', message: 'Flowviant did not return a project credential.' });
|
|
73
|
+
return { saved: false };
|
|
74
|
+
}
|
|
59
75
|
// `machineToken` is the wire's new name; `fleetToken` is the one every
|
|
60
76
|
// published daemon reads. The server dual-sends until DAEMON_MIN clears
|
|
61
77
|
// the release that reads the new one (0.54.2+) — reading both here is
|
|
@@ -65,7 +81,7 @@ export async function runLogin({ thenStart = false } = {}) {
|
|
|
65
81
|
// know for certain which checkout this project means, and the binding is
|
|
66
82
|
// what lets a multi-project VM resolve `npx flowviant` by DIRECTORY
|
|
67
83
|
// instead of by whichever login happened last.
|
|
68
|
-
const repoRoot = detectRepoRoot();
|
|
84
|
+
const repoRoot = selectedRepo ?? detectRepoRoot(dir);
|
|
69
85
|
const entry = {
|
|
70
86
|
fleetToken: poll.machineToken ?? poll.fleetToken,
|
|
71
87
|
projectId: poll.projectId,
|
|
@@ -86,12 +102,18 @@ export async function runLogin({ thenStart = false } = {}) {
|
|
|
86
102
|
// (`directoryTakenRefusal`, shared with the daemon's own start).
|
|
87
103
|
const clash = boundElsewhere(listStoredProjects(), repoRoot, entry.projectId);
|
|
88
104
|
if (clash.length > 0) {
|
|
105
|
+
if (json) { event({ event: 'error', message: directoryTakenRefusal(clash, repoRoot, { incoming: entry }) }); return { saved: false }; }
|
|
89
106
|
console.log('');
|
|
90
107
|
warn(directoryTakenRefusal(clash, repoRoot, { incoming: entry }));
|
|
91
108
|
console.log('');
|
|
92
109
|
return { saved: false };
|
|
93
110
|
}
|
|
94
|
-
saveLogin(entry);
|
|
111
|
+
try { saveLogin(entry); }
|
|
112
|
+
catch (e) {
|
|
113
|
+
if (json) { event({ event: 'error', message: `Could not save this connection: ${e.message}` }); return { saved: false }; }
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
if (json) { event({ event: 'bound', projectId: entry.projectId, name: entry.name, dir: repoRoot }); return { saved: true }; }
|
|
95
117
|
ok(
|
|
96
118
|
`connected to ${c.bold(projectLabel(entry))}` +
|
|
97
119
|
`${repoRoot ? ` for ${c.dim(repoRoot)}` : ''} — saved to ~/.flowviant/credentials.json`
|
|
@@ -102,15 +124,17 @@ export async function runLogin({ thenStart = false } = {}) {
|
|
|
102
124
|
console.log(
|
|
103
125
|
thenStart
|
|
104
126
|
? `\n ${c.dim('starting your agent — leave this running')}\n`
|
|
105
|
-
: `\n Now run: ${c.bold(
|
|
127
|
+
: `\n Now run: ${c.bold(launchCommand())}\n`
|
|
106
128
|
);
|
|
107
129
|
return { saved: true };
|
|
108
130
|
}
|
|
109
131
|
if (poll.status === 'expired') {
|
|
110
|
-
|
|
132
|
+
if (json) { event({ event: 'error', message: 'Login code expired.' }); return { saved: false }; }
|
|
133
|
+
warn(`that code expired — run \`${terminalCommand('login')}\` again.`);
|
|
111
134
|
process.exit(1);
|
|
112
135
|
}
|
|
113
136
|
}
|
|
114
|
-
|
|
137
|
+
if (json) { event({ event: 'error', message: 'Login timed out.' }); return { saved: false }; }
|
|
138
|
+
warn(`login timed out — run \`${terminalCommand('login')}\` again.`);
|
|
115
139
|
process.exit(1);
|
|
116
140
|
}
|
package/bin/lib/machines.mjs
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
import { USER_AGENT } from './config.mjs';
|
|
75
75
|
import { projectLabel, repoCollisions, safeName } from './credentials.mjs';
|
|
76
76
|
import { credentialRejected } from './authReject.mjs';
|
|
77
|
+
import { launchCommand, terminalCommand } from './launchCommand.mjs';
|
|
77
78
|
|
|
78
79
|
/** The boxes read, derived from the roster URL the way the diffstat post is —
|
|
79
80
|
* one place configures the API base and everything else is a suffix swap. */
|
|
@@ -246,7 +247,7 @@ export function renderMachines(entries, results, { now = Date.now() } = {}) {
|
|
|
246
247
|
if (r.rejected) {
|
|
247
248
|
lines.push(
|
|
248
249
|
' credential rejected — this project was disconnected or deleted. ' +
|
|
249
|
-
|
|
250
|
+
`\`${terminalCommand(`machines --forget ${e.projectId.slice(0, 8)}`)}\` removes it here.`
|
|
250
251
|
);
|
|
251
252
|
continue;
|
|
252
253
|
}
|
|
@@ -297,7 +298,7 @@ export function renderCollisions(entries) {
|
|
|
297
298
|
const list = `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
298
299
|
return (
|
|
299
300
|
` ! ${g.entries.length} projects are connected for ${g.repoRoot}: ${list} — ` +
|
|
300
|
-
|
|
301
|
+
`a directory serves one project, and \`${launchCommand()}\` there refuses to start until it does. ` +
|
|
301
302
|
'Delete the one you do not mean in Flowviant (project settings → General → Delete project), or disconnect this box from it.'
|
|
302
303
|
);
|
|
303
304
|
});
|
|
@@ -317,7 +318,7 @@ export function renderCollisions(entries) {
|
|
|
317
318
|
*/
|
|
318
319
|
export const MACHINES_FOOTER = [
|
|
319
320
|
' stop a machine from the app: project settings → Machine → Disconnect;',
|
|
320
|
-
|
|
321
|
+
` a new \`${launchCommand()}\` on another box takes the project over.`,
|
|
321
322
|
' this lists the projects connected on THIS box; the app’s Home lists every machine on your account.',
|
|
322
323
|
];
|
|
323
324
|
|
|
@@ -325,7 +326,7 @@ export const MACHINES_FOOTER = [
|
|
|
325
326
|
* terminal that could draw the menu was offered the same two things by name,
|
|
326
327
|
* and repeating the flags under it would be a manual under a control. */
|
|
327
328
|
export const MACHINES_FLAGS_FOOTER = [
|
|
328
|
-
'
|
|
329
|
+
` \`${terminalCommand('machines --remove <id>')}\` disconnects this box from a project: stops its daemon here,`,
|
|
329
330
|
' removes this box from that project’s machines list in the app, and forgets its credential here.',
|
|
330
331
|
' `--forget <id>` forgets the credential here only.',
|
|
331
332
|
];
|
|
@@ -339,13 +340,13 @@ export const MACHINES_FLAGS_FOOTER = [
|
|
|
339
340
|
* probably forget. Two different next moves; collapsing them is how somebody
|
|
340
341
|
* deletes a working credential.
|
|
341
342
|
*/
|
|
342
|
-
export async function fetchBoxesFor(entry, { url, envpub, fetchImpl = fetch } = {}) {
|
|
343
|
+
export async function fetchBoxesFor(entry, { url, envpub, fetchImpl = fetch, signal = AbortSignal.timeout(15_000) } = {}) {
|
|
343
344
|
const target = new URL(url);
|
|
344
345
|
if (envpub) target.searchParams.set('envpub', envpub);
|
|
345
346
|
try {
|
|
346
347
|
const res = await fetchImpl(target, {
|
|
347
348
|
headers: { Authorization: `Bearer ${entry.fleetToken}`, 'User-Agent': USER_AGENT },
|
|
348
|
-
signal
|
|
349
|
+
signal,
|
|
349
350
|
});
|
|
350
351
|
if (await credentialRejected(res)) return { rejected: true };
|
|
351
352
|
if (res.status === 401 || res.status === 403) return { error: `HTTP ${res.status} from something in front of the app` };
|
|
@@ -487,7 +488,7 @@ export async function disconnectHere(entry, deps, { log = (m) => console.log(m)
|
|
|
487
488
|
log(`could not forget the credential here: ${gone.error}`);
|
|
488
489
|
return { ok: false };
|
|
489
490
|
}
|
|
490
|
-
log(`forgot ${who}’s credential on this box.
|
|
491
|
+
log(`forgot ${who}’s credential on this box. \`${terminalCommand('login')}\` in its repo connects it again.`);
|
|
491
492
|
return { ok: true };
|
|
492
493
|
}
|
|
493
494
|
|
package/bin/lib/mcp-cli.mjs
CHANGED
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { FLEET_TOKEN, USER_AGENT, MCP_URL, FLEET_URL } from './config.mjs';
|
|
21
|
+
import { terminalCommand } from './launchCommand.mjs';
|
|
21
22
|
|
|
22
23
|
const CLI_TOKEN_URL = FLEET_URL.replace(/\/agents\/?$/, '/cli-token');
|
|
23
24
|
|
|
24
25
|
export async function runMcpCommand(args = []) {
|
|
25
26
|
if (!FLEET_TOKEN) {
|
|
26
27
|
console.error(
|
|
27
|
-
|
|
28
|
+
`error: no credential resolves here. Run \`${terminalCommand('login')}\` first — or, with\n` +
|
|
28
29
|
'several projects connected on this box, run this inside the project\'s own\n' +
|
|
29
|
-
|
|
30
|
+
`repo or pass \`--project <name|id>\` (\`${terminalCommand('projects')}\` lists them).`
|
|
30
31
|
);
|
|
31
32
|
process.exit(1);
|
|
32
33
|
}
|
|
@@ -50,7 +51,7 @@ export async function runMcpCommand(args = []) {
|
|
|
50
51
|
console.error(
|
|
51
52
|
`error: could not mint a CLI credential (${res.status}). ` +
|
|
52
53
|
(res.status === 401 || res.status === 403
|
|
53
|
-
?
|
|
54
|
+
? `Your credential may have been revoked — try \`${terminalCommand('login')}\` again.`
|
|
54
55
|
: 'Try again in a moment.')
|
|
55
56
|
);
|
|
56
57
|
process.exit(1);
|
package/bin/lib/preflight.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
8
8
|
import { ok, warn, info, c } from './ui.mjs';
|
|
9
9
|
import { addLocalBinToPath, promptYesNo, installClaude, installGh } from './install.mjs';
|
|
10
10
|
import { RUNTIMES, detectRuntimes, mediatedSafeGap } from './runtimes.mjs';
|
|
11
|
+
import { terminalCommand } from './launchCommand.mjs';
|
|
11
12
|
|
|
12
13
|
function present(cmd) {
|
|
13
14
|
try {
|
|
@@ -106,7 +107,7 @@ export async function preflight({ needGit = true } = {}) {
|
|
|
106
107
|
if (await installGh((m) => info(m))) gh = present('gh');
|
|
107
108
|
}
|
|
108
109
|
gh
|
|
109
|
-
? ok(
|
|
110
|
+
? ok(`gh installed to ~/.flowviant/bin — authenticate with: ${terminalCommand('gh-auth')}`)
|
|
110
111
|
: info(c.dim('or install it later: https://cli.github.com, then: gh auth login'));
|
|
111
112
|
}
|
|
112
113
|
|
package/bin/lib/tty.mjs
CHANGED
|
@@ -112,7 +112,7 @@ export function boundedTimer(timeoutMs, fire) {
|
|
|
112
112
|
* answer everywhere (the binding confirm serves unbound; the project picker
|
|
113
113
|
* refuses, exactly as it does headless).
|
|
114
114
|
*/
|
|
115
|
-
export async function askWithTimeout(query, timeoutMs) {
|
|
115
|
+
export async function askWithTimeout(query, timeoutMs, output = process.stdout) {
|
|
116
116
|
const noop = () => {};
|
|
117
117
|
// Replacing the DEFAULT disposition is the whole point — an empty handler is
|
|
118
118
|
// enough, and it is what keeps the timer below able to run at all.
|
|
@@ -120,7 +120,7 @@ export async function askWithTimeout(query, timeoutMs) {
|
|
|
120
120
|
process.on('SIGTTOU', noop);
|
|
121
121
|
const rl = (await import('node:readline/promises')).createInterface({
|
|
122
122
|
input: process.stdin,
|
|
123
|
-
output
|
|
123
|
+
output,
|
|
124
124
|
});
|
|
125
125
|
const ac = new AbortController();
|
|
126
126
|
const timer = boundedTimer(timeoutMs, () => ac.abort());
|