flowviant 0.102.2 → 0.104.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/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +184 -2396
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +34 -3
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/shipMerge.mjs +33 -2
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +35 -12
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3037
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +92 -897
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
package/bin/cli.mjs
CHANGED
|
@@ -64,9 +64,12 @@
|
|
|
64
64
|
* inside the git repo you want worked. `gh` is optional.
|
|
65
65
|
*
|
|
66
66
|
* Implementation lives in ./lib/: config, ui, preflight, install, update,
|
|
67
|
-
* instance, login, mcp-cli;
|
|
67
|
+
* instance, login, mcp-cli; startProject (which project a start serves) and
|
|
68
|
+
* machinesCommand (the `machines` listing and menu); fleet (the roster loop)
|
|
69
|
+
* and work (session turns);
|
|
68
70
|
* claude + runtimes + prompts + stream (spawning a CLI and reading its events);
|
|
69
|
-
* git + worktreeDiff + patch; localSessions, listeners, preview + authproxy
|
|
71
|
+
* git + worktreeDiff + patch; localSessions (+ claudeSessions, agySessions), listeners, preview + authproxy
|
|
72
|
+
* (+ previewGatePolicy, previewFramePolicy);
|
|
70
73
|
* env (the box keypair, the uplink scrubber, the env-comparison scan) + vault
|
|
71
74
|
* (the knowledge wiki, not secrets), resources, deploy, shot.
|
|
72
75
|
*/
|
|
@@ -81,9 +84,11 @@ if (process.argv[2] === 'start' && !process.argv.slice(3).some((arg) => arg ===
|
|
|
81
84
|
|
|
82
85
|
// Resolve --dir before config reads the project bound to cwd. A tray process
|
|
83
86
|
// starts outside the checkout; its explicit folder is the daemon's checkout.
|
|
87
|
+
// Which words work on the checkout is the command table's (commandSpecs.mjs,
|
|
88
|
+
// import-free, so it is safe to read before config.mjs loads).
|
|
89
|
+
const { commandUsesCheckout, findCommand } = await import('./lib/commandSpecs.mjs');
|
|
84
90
|
const dirAt = process.argv.indexOf('--dir');
|
|
85
|
-
|
|
86
|
-
if (dirAt >= 0 && !noCheckoutCommand.has(process.argv[2])) {
|
|
91
|
+
if (dirAt >= 0 && commandUsesCheckout(process.argv[2])) {
|
|
87
92
|
const dir = process.argv[dirAt + 1];
|
|
88
93
|
try {
|
|
89
94
|
if (!dir || dir.startsWith('--')) throw new Error('missing directory');
|
|
@@ -96,7 +101,7 @@ if (dirAt >= 0 && !noCheckoutCommand.has(process.argv[2])) {
|
|
|
96
101
|
process.exit(1);
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
|
-
const {
|
|
104
|
+
const { CREDENTIAL, VERSION } = await import('./lib/config.mjs');
|
|
100
105
|
const { runFleetDaemon } = await import('./lib/fleet.mjs');
|
|
101
106
|
const { runLogin } = await import('./lib/login.mjs');
|
|
102
107
|
const { launchCommand, terminalCommand } = await import('./lib/launchCommand.mjs');
|
|
@@ -120,18 +125,18 @@ if (process.argv.includes('--remote') && !(process.argv[2] === 'status' && proce
|
|
|
120
125
|
// long-running process.
|
|
121
126
|
// `flowviant --version` prints and exits — the first thing a person runs after
|
|
122
127
|
// a curl install, and it must never start a daemon.
|
|
123
|
-
if (process.argv[2] === '
|
|
128
|
+
if (findCommand(process.argv[2])?.name === 'version') {
|
|
124
129
|
console.log(VERSION);
|
|
125
130
|
process.exit(0);
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
// `flowviant help`, `--help`/`-h`, and `<command> --help`. Before any command
|
|
129
|
-
// runs, so asking about one never runs it (bin/lib/
|
|
134
|
+
// runs, so asking about one never runs it (bin/lib/commandSpecs.mjs has the table).
|
|
130
135
|
{
|
|
131
136
|
const asksHelp = (arg) => arg === '--help' || arg === '-h';
|
|
132
137
|
const first = process.argv[2];
|
|
133
138
|
if (first === 'help' || asksHelp(first) || process.argv.slice(3).some(asksHelp)) {
|
|
134
|
-
const {
|
|
139
|
+
const { renderCommandHelp, renderHelp, unknownCommandMessage } = await import('./lib/help.mjs');
|
|
135
140
|
const topic = first === 'help' ? process.argv[3] : asksHelp(first) ? undefined : first?.startsWith('-') ? 'start' : first;
|
|
136
141
|
if (!topic) { console.log(renderHelp(VERSION)); process.exit(0); }
|
|
137
142
|
const cmd = findCommand(topic);
|
|
@@ -141,7 +146,7 @@ if (process.argv[2] === '--version' || process.argv[2] === '-v' || process.argv[
|
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
|
|
149
|
+
async function loginCommand() {
|
|
145
150
|
const noStart = process.argv.includes('--no-start');
|
|
146
151
|
const json = process.argv.includes('--json');
|
|
147
152
|
const login = await runLogin({ thenStart: !noStart, json, dir: process.cwd() });
|
|
@@ -169,17 +174,18 @@ if (process.argv[2] === 'login') {
|
|
|
169
174
|
process.exit(await new Promise((resolve) => child.on('exit', (code) => resolve(code ?? 0))));
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
// `flowviant status` — each project connected here. `--json` is the tray's
|
|
178
|
+
// form (and `--remote` adds the server's answer); plain is the same facts, for
|
|
179
|
+
// a person.
|
|
180
|
+
async function statusCommand() {
|
|
181
|
+
if (process.argv.includes('--json')) {
|
|
182
|
+
// The tray polls this every 30 s: the fixed per-user bin dirs only, no shell.
|
|
183
|
+
await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess({ shell: false });
|
|
184
|
+
const { desktopStatus, desktopStatusRemote } = await import('./lib/desktopContract.mjs');
|
|
185
|
+
const status = process.argv.includes('--remote') ? await desktopStatusRemote() : desktopStatus();
|
|
186
|
+
process.stdout.write(`${JSON.stringify(status)}\n`);
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
183
189
|
await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess();
|
|
184
190
|
const { runStatus } = await import('./lib/views.mjs');
|
|
185
191
|
await runStatus();
|
|
@@ -187,21 +193,21 @@ if (process.argv[2] === 'status') {
|
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
// `flowviant logs [-f]` — this repo's daemon log.
|
|
190
|
-
|
|
196
|
+
async function logsCommand() {
|
|
191
197
|
const { runLogs } = await import('./lib/views.mjs');
|
|
192
198
|
await runLogs(process.argv);
|
|
193
199
|
process.exit(0);
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
// `flowviant open` — this repo's board, in the browser.
|
|
197
|
-
|
|
203
|
+
async function openCommand() {
|
|
198
204
|
const { runOpen } = await import('./lib/views.mjs');
|
|
199
205
|
runOpen(process.argv);
|
|
200
206
|
process.exit(0);
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
// `flowviant doctor` — what this computer needs, checked.
|
|
204
|
-
|
|
210
|
+
async function doctorCommand() {
|
|
205
211
|
await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess();
|
|
206
212
|
const { runDoctor } = await import('./lib/views.mjs');
|
|
207
213
|
process.exit((await runDoctor()) > 0 ? 1 : 0);
|
|
@@ -209,7 +215,7 @@ if (process.argv[2] === 'doctor') {
|
|
|
209
215
|
|
|
210
216
|
// `flowviant update` — install the latest published version now. The daemon also
|
|
211
217
|
// self-updates on its own (at startup + when idle); this is the manual path.
|
|
212
|
-
|
|
218
|
+
async function updateCommand() {
|
|
213
219
|
const { runUpdateCommand } = await import('./lib/update.mjs');
|
|
214
220
|
await runUpdateCommand();
|
|
215
221
|
process.exit(0);
|
|
@@ -217,7 +223,7 @@ if (process.argv[2] === 'update') {
|
|
|
217
223
|
|
|
218
224
|
// `flowviant gh-auth` — sign in the gh CLI (incl. a copy we bundled into
|
|
219
225
|
// ~/.flowviant/bin), so the isolated install doesn't need gh on your global PATH.
|
|
220
|
-
|
|
226
|
+
async function ghAuthCommand() {
|
|
221
227
|
const { addLocalBinToPath } = await import('./lib/install.mjs');
|
|
222
228
|
const { execFileSync } = await import('node:child_process');
|
|
223
229
|
addLocalBinToPath();
|
|
@@ -233,13 +239,13 @@ if (process.argv[2] === 'gh-auth') {
|
|
|
233
239
|
// They're kept across runs so in-flight work survives Ctrl+C; this is the drain.
|
|
234
240
|
// Repos self-heal: the daemon runs `git worktree prune` if a stale registration
|
|
235
241
|
// blocks re-adding a path.
|
|
236
|
-
|
|
242
|
+
async function cleanCommand() {
|
|
237
243
|
const { rmSync, existsSync } = await import('node:fs');
|
|
238
244
|
const { join } = await import('node:path');
|
|
239
245
|
const { homedir } = await import('node:os');
|
|
240
246
|
const { execFileSync } = await import('node:child_process');
|
|
241
247
|
// Also reap any preview dev-server/tunnel groups a crashed daemon left running.
|
|
242
|
-
const { reapOrphanPreviews } = await import('./lib/
|
|
248
|
+
const { reapOrphanPreviews } = await import('./lib/previewRegistry.mjs');
|
|
243
249
|
reapOrphanPreviews((m) => console.log(m));
|
|
244
250
|
const dir = join(homedir(), '.flowviant', 'worktrees');
|
|
245
251
|
if (!existsSync(dir)) {
|
|
@@ -263,7 +269,7 @@ if (process.argv[2] === 'clean') {
|
|
|
263
269
|
// page. A session's agent shells out to this to SEE the change it just made.
|
|
264
270
|
// Self-contained + graceful (no browser → exit 1, and the agent carries on in
|
|
265
271
|
// text); needs no credential, so it runs before the auth gate.
|
|
266
|
-
|
|
272
|
+
async function shotCommand() {
|
|
267
273
|
const { runShot } = await import('./lib/shot.mjs');
|
|
268
274
|
await runShot(process.argv.slice(3));
|
|
269
275
|
process.exit(0);
|
|
@@ -287,7 +293,7 @@ if (process.argv[2] === 'shot') {
|
|
|
287
293
|
// EXIT 0 when it stopped something AND when it found nothing: "no flowviant
|
|
288
294
|
// daemon is running on this machine." is the answer the asker came for, not an
|
|
289
295
|
// error. Non-zero only when something was alive and could not be stopped.
|
|
290
|
-
|
|
296
|
+
async function stopCommand() {
|
|
291
297
|
const { stopAllDaemons, stopDaemonFor } = await import('./lib/instance.mjs');
|
|
292
298
|
let result;
|
|
293
299
|
if (process.argv.includes('--project')) {
|
|
@@ -301,7 +307,7 @@ if (process.argv[2] === 'stop') {
|
|
|
301
307
|
process.exit(failed > 0 ? 1 : 0);
|
|
302
308
|
}
|
|
303
309
|
|
|
304
|
-
|
|
310
|
+
async function uninstallCommand() {
|
|
305
311
|
const { runUninstall } = await import('./lib/uninstall.mjs');
|
|
306
312
|
const { askWithTimeout } = await import('./lib/tty.mjs');
|
|
307
313
|
const result = await runUninstall({
|
|
@@ -319,8 +325,9 @@ if (process.argv[2] === 'uninstall') {
|
|
|
319
325
|
// network: it reads the store, which is the exact thing a confused person is
|
|
320
326
|
// trying to see. The remedies are named because this listing IS the moment of
|
|
321
327
|
// confusion ("why did it say skadooble?"), not documentation.
|
|
322
|
-
|
|
328
|
+
async function projectsCommand() {
|
|
323
329
|
const { listStoredProjects, projectLabel } = await import('./lib/credentials.mjs');
|
|
330
|
+
const { connectedOn } = await import('./lib/credentialDate.mjs');
|
|
324
331
|
const entries = listStoredProjects();
|
|
325
332
|
if (entries.length === 0) {
|
|
326
333
|
console.log(`no projects connected on this machine yet — run \`${terminalCommand('login')}\` inside a repo.`);
|
|
@@ -332,11 +339,8 @@ if (process.argv[2] === 'projects') {
|
|
|
332
339
|
// did I set up last month" is the same question one step on. Absent when
|
|
333
340
|
// nothing recorded one (a credential stored before the field existed);
|
|
334
341
|
// nothing is invented.
|
|
335
|
-
const
|
|
336
|
-
const connected =
|
|
337
|
-
savedOn && !Number.isNaN(savedOn.getTime())
|
|
338
|
-
? `, connected ${savedOn.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`
|
|
339
|
-
: '';
|
|
342
|
+
const on = connectedOn(e.savedAt);
|
|
343
|
+
const connected = on ? `, connected ${on}` : '';
|
|
340
344
|
console.log(
|
|
341
345
|
` ${projectLabel(e)} (${e.projectId.slice(0, 8)}…${connected})` +
|
|
342
346
|
`${e.repoRoot ? `\n repo · ${e.repoRoot}` : '\n repo · not bound yet — first start or login in its repo binds it'}` +
|
|
@@ -350,192 +354,12 @@ if (process.argv[2] === 'projects') {
|
|
|
350
354
|
process.exit(0);
|
|
351
355
|
}
|
|
352
356
|
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
// this one directly ("the CLI gets a command to list connections"). It shipped
|
|
360
|
-
// VIEW-ONLY, and on 2026-09-23 he asked for the rest: "it seems to be only a
|
|
361
|
-
// read cmd but I want to be able to interact with it and use arrow keys to
|
|
362
|
-
// select the machines or to remove them." So: the listing, then — only where a
|
|
363
|
-
// person can drive one — a menu. Its verbs act on THIS BOX'S OWN CONNECTIONS
|
|
364
|
-
// (stop the daemon here, leave the project's list in the app, forget the
|
|
365
|
-
// credential here) and never on another computer's daemon; machines.mjs
|
|
366
|
-
// carries the argument.
|
|
367
|
-
//
|
|
368
|
-
// The question is the owner's, verbatim: "is there a way to view ALL the
|
|
369
|
-
// connected flowviants? because im not sure if i have any duplicate or
|
|
370
|
-
// redundant daemons running". It needs the NETWORK, unlike `projects` next
|
|
371
|
-
// door, because the boxes are the server's answer — this box cannot see a
|
|
372
|
-
// daemon running on somebody else's computer, and guessing would be worse than
|
|
373
|
-
// asking.
|
|
374
|
-
//
|
|
375
|
-
// ONE CALL PER STORED CREDENTIAL, because the CLI has NO PERSON IDENTITY: every
|
|
376
|
-
// request it can make is `Bearer <machine token>`, so "all my machines" has to
|
|
377
|
-
// be assembled from the credentials this box holds. That is also the limit the
|
|
378
|
-
// footer states out loud — the app's Home is where the whole account's answer
|
|
379
|
-
// lives.
|
|
380
|
-
//
|
|
381
|
-
// EXIT 0 whatever it finds, including nothing: the listing is the answer the
|
|
382
|
-
// asker came for, and a non-zero code over "you have no projects connected"
|
|
383
|
-
// would make this command unusable in anything that checks one. `--remove`
|
|
384
|
-
// is the one exit that can be 1, when the daemon it had to stop would not.
|
|
385
|
-
if (process.argv[2] === 'machines') {
|
|
386
|
-
const creds = await import('./lib/credentials.mjs');
|
|
387
|
-
const machineJson = process.argv.includes('--json');
|
|
388
|
-
const forgetAt = process.argv.indexOf('--forget');
|
|
389
|
-
if (forgetAt >= 0) {
|
|
390
|
-
if (machineJson && !process.argv.includes('--yes')) {
|
|
391
|
-
process.stdout.write(`${JSON.stringify({ ok: false, error: '--yes is required with --json' })}\n`);
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
// THE LOCAL-ONLY WRITE. Named explicitly, matched by full id or a ≥6
|
|
395
|
-
// prefix, and AMBIGUITY REFUSES rather than guessing — this deletes a
|
|
396
|
-
// credential, and two projects that look alike is the case that produced
|
|
397
|
-
// the command.
|
|
398
|
-
const res = creds.forgetStoredProject(process.argv[forgetAt + 1]);
|
|
399
|
-
if (res.error) {
|
|
400
|
-
if (machineJson) { process.stdout.write(`${JSON.stringify({ ok: false, error: res.error })}\n`); process.exit(1); }
|
|
401
|
-
console.error(`error: ${res.error}`);
|
|
402
|
-
process.exit(1);
|
|
403
|
-
}
|
|
404
|
-
if (machineJson) { process.stdout.write(`${JSON.stringify({ ok: true, action: 'forget', projectId: res.entry.projectId })}\n`); process.exit(0); }
|
|
405
|
-
console.log(
|
|
406
|
-
`forgot ${creds.projectLabel(res.entry)} (${res.entry.projectId.slice(0, 8)}…) on this box.\n` +
|
|
407
|
-
` Nothing was stopped or deleted anywhere else — \`${terminalCommand('login')}\` connects it again.`
|
|
408
|
-
);
|
|
409
|
-
process.exit(0);
|
|
410
|
-
}
|
|
411
|
-
const { boxesUrlFrom, leaveUrlFrom, fetchBoxesFor, renderMachines, disconnectHere, realDisconnectDeps, MACHINES_FOOTER, MACHINES_FLAGS_FOOTER } =
|
|
412
|
-
await import('./lib/machines.mjs');
|
|
413
|
-
const { FLEET_URL } = await import('./lib/config.mjs');
|
|
414
|
-
const removeAt = process.argv.indexOf('--remove');
|
|
415
|
-
if (removeAt >= 0) {
|
|
416
|
-
if (machineJson && !process.argv.includes('--yes')) {
|
|
417
|
-
process.stdout.write(`${JSON.stringify({ ok: false, error: '--yes is required with --json' })}\n`);
|
|
418
|
-
process.exit(1);
|
|
419
|
-
}
|
|
420
|
-
// THE SCRIPTED DISCONNECT — the menu's first verb, named by id for a
|
|
421
|
-
// terminal nobody is sitting at. Same matcher and the same refusal of an
|
|
422
|
-
// ambiguous prefix as --forget: a name two projects share is exactly the
|
|
423
|
-
// input this command exists to untangle, and it must not pick one.
|
|
424
|
-
const m = creds.matchStoredProject(process.argv[removeAt + 1]);
|
|
425
|
-
if (m.error) {
|
|
426
|
-
if (machineJson) { process.stdout.write(`${JSON.stringify({ ok: false, error: m.error })}\n`); process.exit(1); }
|
|
427
|
-
console.error(`error: ${m.error}. \`${terminalCommand('machines')}\` lists what is stored.`);
|
|
428
|
-
process.exit(1);
|
|
429
|
-
}
|
|
430
|
-
const res = await disconnectHere(m.entry, await realDisconnectDeps({ url: leaveUrlFrom(FLEET_URL), log: machineJson ? () => {} : undefined }), { log: machineJson ? () => {} : undefined });
|
|
431
|
-
if (machineJson) process.stdout.write(`${JSON.stringify({ ok: res.ok, action: 'disconnect', projectId: m.entry.projectId })}\n`);
|
|
432
|
-
process.exit(res.ok ? 0 : 1);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
let entries = creds.listStoredProjects();
|
|
436
|
-
if (entries.length === 0) {
|
|
437
|
-
console.log(`no projects connected on this machine yet — run \`${terminalCommand('login')}\` inside a repo.`);
|
|
438
|
-
process.exit(0);
|
|
439
|
-
}
|
|
440
|
-
// OUR OWN BOX ID, so the listing can mark "← this box".
|
|
441
|
-
//
|
|
442
|
-
// READ, NEVER CREATED. This called `ensureKeypair()` until 2026-09-19, which
|
|
443
|
-
// MINTS a keypair when there is no file — a 0600 write establishing this
|
|
444
|
-
// machine's durable identity, performed by a command whose entire job is to
|
|
445
|
-
// print a list. A view-only command is what let a third terminal command
|
|
446
|
-
// exist at all, and enrolling a box as a side effect of looking at one is not
|
|
447
|
-
// that. `readStoredPubB64` reads or answers null.
|
|
448
|
-
//
|
|
449
|
-
// NULL COSTS ONLY THE MARK, and that is the honest outcome: a box that has
|
|
450
|
-
// never run a daemon has never polled, so it is not in this listing to be
|
|
451
|
-
// marked in the first place.
|
|
452
|
-
let me;
|
|
453
|
-
try {
|
|
454
|
-
const env = await import('./lib/env.mjs');
|
|
455
|
-
me = env.readStoredPubB64() ?? undefined;
|
|
456
|
-
} catch {
|
|
457
|
-
/* unreadable keypair — nothing is marked, and nothing is claimed */
|
|
458
|
-
}
|
|
459
|
-
const url = boxesUrlFrom(FLEET_URL);
|
|
460
|
-
const listing = async () => {
|
|
461
|
-
const results = {};
|
|
462
|
-
// SEQUENTIAL, not a fan-out: this is a handful of credentials on somebody's
|
|
463
|
-
// laptop, and a parallel burst against the API buys nothing a person
|
|
464
|
-
// waiting two seconds can perceive.
|
|
465
|
-
for (const e of entries) {
|
|
466
|
-
results[e.projectId] = await fetchBoxesFor(e, { url, envpub: me });
|
|
467
|
-
}
|
|
468
|
-
console.log('');
|
|
469
|
-
for (const line of renderMachines(entries, results)) console.log(line);
|
|
470
|
-
console.log('');
|
|
471
|
-
};
|
|
472
|
-
await listing();
|
|
473
|
-
|
|
474
|
-
// THE MENU, only where a person can drive one. `canPrompt()` is the same
|
|
475
|
-
// gate the start path keeps — a backgrounded job or a pipe gets the listing
|
|
476
|
-
// and the flags, never a prompt that stops the process — and `menuSupported`
|
|
477
|
-
// is whether ↑/↓ can be read at all. Without both this is the command it was.
|
|
478
|
-
const { canPrompt, menuSupported, selectMenu, askWithTimeout } = await import('./lib/tty.mjs');
|
|
479
|
-
if (!(canPrompt() && menuSupported())) {
|
|
480
|
-
for (const line of MACHINES_FOOTER) console.log(line);
|
|
481
|
-
for (const line of MACHINES_FLAGS_FOOTER) console.log(line);
|
|
482
|
-
process.exit(0);
|
|
483
|
-
}
|
|
484
|
-
const deps = await realDisconnectDeps({ url: leaveUrlFrom(FLEET_URL) });
|
|
485
|
-
for (;;) {
|
|
486
|
-
// A ROW PER PROJECT connected on this box — the things this box can act on.
|
|
487
|
-
// The other computers under each project are printed above for the
|
|
488
|
-
// duplicate to be visible, and are not rows here: stopping or removing
|
|
489
|
-
// THEM is the app's verb, and a menu row that answers "not from here" is
|
|
490
|
-
// a control wired to a refusal.
|
|
491
|
-
console.log(' projects connected on this box — enter one for what you can do about it:');
|
|
492
|
-
const rowLabel = (e) => creds.projectRowLabel(e, entries) + (e.repoRoot ? ` — ${e.repoRoot}` : ' — not tied to a repo');
|
|
493
|
-
const pick = await selectMenu({ options: [...entries.map(rowLabel), 'done'] });
|
|
494
|
-
if (pick.cancelled || pick.unsupported || pick.index === entries.length) break;
|
|
495
|
-
const picked = entries[pick.index];
|
|
496
|
-
const who = creds.projectRowLabel(picked, entries);
|
|
497
|
-
// TWO VERBS, each row saying its whole consequence — the app's ⋯ menu on a
|
|
498
|
-
// machine row says the same two things, and a menu whose rows are verbs
|
|
499
|
-
// with the outcome in a manual somewhere is one you have to be sure about
|
|
500
|
-
// before you can use it.
|
|
501
|
-
const action = await selectMenu({
|
|
502
|
-
options: [
|
|
503
|
-
`disconnect this box from ${who} — stops its daemon here, removes this box from its machines list in the app, forgets its credential here`,
|
|
504
|
-
`forget ${who} here only — nothing is stopped; the app keeps listing this box until it goes quiet`,
|
|
505
|
-
'back',
|
|
506
|
-
],
|
|
507
|
-
});
|
|
508
|
-
if (action.cancelled || action.unsupported || action.index === 2) continue;
|
|
509
|
-
// ONE CONFIRM, in words, because both verbs delete a credential and the
|
|
510
|
-
// arrow keys make a slip cheap. A read that failed (stdin gone) is "no".
|
|
511
|
-
const verb = action.index === 0 ? 'Disconnect this box from' : 'Forget';
|
|
512
|
-
const answer = await askWithTimeout(` ${verb} ${who}? [y/N] `);
|
|
513
|
-
if (!/^y(es)?$/i.test(answer ?? '')) {
|
|
514
|
-
console.log(' left as it was.\n');
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (action.index === 0) {
|
|
518
|
-
await disconnectHere(picked, deps, { log: (m) => console.log(` ${m}`) });
|
|
519
|
-
} else {
|
|
520
|
-
const res = creds.forgetStoredProject(picked.projectId);
|
|
521
|
-
console.log(
|
|
522
|
-
res.error
|
|
523
|
-
? ` ${res.error}`
|
|
524
|
-
: ` forgot ${creds.projectLabel(res.entry)} (${res.entry.projectId.slice(0, 8)}…) on this box. Nothing was stopped or deleted anywhere else — \`${terminalCommand('login')}\` connects it again.`
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
entries = creds.listStoredProjects();
|
|
528
|
-
if (entries.length === 0) {
|
|
529
|
-
console.log('\n no projects are connected on this box now.\n');
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
// THE LISTING AGAIN, re-asked: the server's rows are the only true account
|
|
533
|
-
// of what the leave did, and a menu redrawn over a stale listing would be
|
|
534
|
-
// this command asserting the outcome it hoped for.
|
|
535
|
-
await listing();
|
|
536
|
-
}
|
|
537
|
-
for (const line of MACHINES_FOOTER) console.log(line);
|
|
538
|
-
process.exit(0);
|
|
357
|
+
// `flowviant machines` — every project connected on THIS box, every box that
|
|
358
|
+
// has polled each of them, and on a terminal a menu over them. The command's
|
|
359
|
+
// body and its argument live in machinesCommand.mjs.
|
|
360
|
+
async function machinesCommand() {
|
|
361
|
+
const { runMachinesCommand } = await import('./lib/machinesCommand.mjs');
|
|
362
|
+
await runMachinesCommand();
|
|
539
363
|
}
|
|
540
364
|
|
|
541
365
|
// `flowviant env <import|set|show>` IS DELETED (2026-09-21), with the
|
|
@@ -550,12 +374,41 @@ if (process.argv[2] === 'machines') {
|
|
|
550
374
|
// the terminal. Mints a `cli` credential: a separate principal from the
|
|
551
375
|
// per-session tokens, with only the management tools and no way to work or ship
|
|
552
376
|
// a card.
|
|
553
|
-
|
|
377
|
+
async function mcpCommand() {
|
|
554
378
|
const { runMcpCommand } = await import('./lib/mcp-cli.mjs');
|
|
555
379
|
await runMcpCommand(process.argv.slice(3));
|
|
556
380
|
process.exit(0);
|
|
557
381
|
}
|
|
558
382
|
|
|
383
|
+
// ── DISPATCH: one handler per row of the command table (commandSpecs.mjs).
|
|
384
|
+
// `version` and `help` were answered above and `start` is everything below;
|
|
385
|
+
// help.test.mjs pins that every other row has exactly one handler here.
|
|
386
|
+
const HANDLERS = {
|
|
387
|
+
login: loginCommand,
|
|
388
|
+
status: statusCommand,
|
|
389
|
+
logs: logsCommand,
|
|
390
|
+
open: openCommand,
|
|
391
|
+
doctor: doctorCommand,
|
|
392
|
+
update: updateCommand,
|
|
393
|
+
'gh-auth': ghAuthCommand,
|
|
394
|
+
clean: cleanCommand,
|
|
395
|
+
shot: shotCommand,
|
|
396
|
+
stop: stopCommand,
|
|
397
|
+
uninstall: uninstallCommand,
|
|
398
|
+
projects: projectsCommand,
|
|
399
|
+
machines: machinesCommand,
|
|
400
|
+
mcp: mcpCommand,
|
|
401
|
+
};
|
|
402
|
+
{
|
|
403
|
+
const handler = HANDLERS[findCommand(process.argv[2])?.name];
|
|
404
|
+
if (handler) {
|
|
405
|
+
await handler();
|
|
406
|
+
// Every handler exits on its own; a command never falls through into
|
|
407
|
+
// starting the daemon.
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
559
412
|
// A WORD THAT IS NOT A COMMAND IS REFUSED, not served. Every word used to fall
|
|
560
413
|
// through to here, so `flowviant hlep` started the daemon. Flags still reach
|
|
561
414
|
// the start path (`--project`, `--dir`, `--json-events`, …).
|
|
@@ -569,255 +422,15 @@ if (process.argv[2] && !process.argv[2].startsWith('-')) {
|
|
|
569
422
|
// this through `wsl.exe --`, which reads no shell startup file (loginPath.mjs).
|
|
570
423
|
await (await import('./lib/loginPath.mjs')).adoptLoginPathForProcess();
|
|
571
424
|
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// binding confirm below and the machine would stay dark until somebody typed a
|
|
584
|
-
// key. Same reasoning as the headless case, and the same answer.
|
|
585
|
-
// `canPrompt()`, not a bare isTTY pair: a BACKGROUNDED job (`flowviant &`) has
|
|
586
|
-
// two TTYs and cannot be asked anything — the first read raises SIGTTIN and the
|
|
587
|
-
// kernel STOPS the process, which is why 0.55.2's timeout did not save it (a
|
|
588
|
-
// stopped process runs no timers). See tty.mjs.
|
|
589
|
-
const { canPrompt, askWithTimeout, selectMenu, menuSupported } = await import('./lib/tty.mjs');
|
|
590
|
-
const interactive = canPrompt() && process.env.FLOWVIANT_REEXEC !== '1';
|
|
591
|
-
|
|
592
|
-
/** NO ANSWER TIME LIMIT on the start path (2026-09-20, the owner: "why is
|
|
593
|
-
* there an answer time limit, remove that"). The confirm below carried 20s
|
|
594
|
-
* and the picker 60s, each arguing that a restart nobody is watching must
|
|
595
|
-
* not sit dark on a prompt — but `interactive` already excludes exactly that
|
|
596
|
-
* case (no foreground terminal, or a self-update re-exec, and nothing is
|
|
597
|
-
* asked). What was left was a person reading a list being told "no answer
|
|
598
|
-
* in 60s — nothing started". A prompt a person can see waits for the
|
|
599
|
-
* person; tty.mjs's header carries the argument. */
|
|
600
|
-
const externalToken =
|
|
601
|
-
process.argv.includes('--fleet') ||
|
|
602
|
-
Boolean(process.env.FLOWVIANT_MACHINE_TOKEN) ||
|
|
603
|
-
Boolean(process.env.FLOWVIANT_FLEET);
|
|
604
|
-
|
|
605
|
-
/** Re-exec a plain `flowviant` after an inline login — the login command's own
|
|
606
|
-
* pattern: config.mjs read the store at IMPORT time, before the credential
|
|
607
|
-
* existed, so this process cannot serve; the child can. */
|
|
608
|
-
async function reexecAfterLogin() {
|
|
609
|
-
const login = await runLogin({ thenStart: false });
|
|
610
|
-
if (!login?.saved) process.exit(1); // refused: the directory already serves another project
|
|
611
|
-
const { spawn } = await import('node:child_process');
|
|
612
|
-
const child = spawn(process.execPath, process.argv[1]?.startsWith('/$bunfs/') ? [] : [process.argv[1]], { stdio: 'inherit', env: process.env });
|
|
613
|
-
process.exit(await new Promise((resolve) => child.on('exit', (code) => resolve(code ?? 0))));
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// TWO ROWS THAT READ ALIKE ARE NOT A CHOICE. `projectRowLabel` adds the id and
|
|
617
|
-
// the connected date to the COLLIDING rows only — production held two projects
|
|
618
|
-
// both called "BRIF AI", both bound to the same checkout, so this picker
|
|
619
|
-
// offered two identical lines and the only way to answer was to guess. See
|
|
620
|
-
// credentials.mjs for why the suffix is not on every row. (That exact case is
|
|
621
|
-
// a refusal now rather than a picker — a directory serves one project — but
|
|
622
|
-
// two same-named projects can still meet in the no-match picker.)
|
|
623
|
-
function listLines(entries, { projectRowLabel }) {
|
|
624
|
-
return entries
|
|
625
|
-
.map(
|
|
626
|
-
(e, i) =>
|
|
627
|
-
` ${i + 1}. ${projectRowLabel(e, entries)}` +
|
|
628
|
-
(e.repoRoot ? ` — connected for ${e.repoRoot}` : ' — not tied to a repo yet')
|
|
629
|
-
)
|
|
630
|
-
.join('\n');
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/** A repo binding the person just consented to, persisted by runFleetDaemon
|
|
634
|
-
* only after the instance lock is taken — never before a refusal. */
|
|
635
|
-
let afterLock = null;
|
|
636
|
-
|
|
637
|
-
if (!FLEET_TOKEN) {
|
|
638
|
-
if (CREDENTIAL.error) {
|
|
639
|
-
console.error(`error: ${CREDENTIAL.error}. \`${terminalCommand('projects')}\` lists what is stored.`);
|
|
640
|
-
process.exit(1);
|
|
641
|
-
}
|
|
642
|
-
// A DIRECTORY SERVES ONE PROJECT (2026-09-23, the owner, verbatim: "a
|
|
643
|
-
// directory cannot have more than one project on flowviant. if one already
|
|
644
|
-
// exists, it would warn the user and ask them to delete the project on
|
|
645
|
-
// flowviant first. because 2 projects shouldnt be able to edit a directory
|
|
646
|
-
// at the same time."). This used to be a PICKER — two projects bound to one
|
|
647
|
-
// checkout, choose which this daemon serves — which is a control offering to
|
|
648
|
-
// do the forbidden thing politely. It is a refusal now, on a TTY and
|
|
649
|
-
// headless alike, naming both projects and the remedy. `credentialRefusal`
|
|
650
|
-
// is the same sentence login prints when it declines to bind a second one.
|
|
651
|
-
if (CREDENTIAL.reason === 'multiple-bound') {
|
|
652
|
-
const { directoryTakenRefusal } = await import('./lib/credentials.mjs');
|
|
653
|
-
console.error(directoryTakenRefusal(CREDENTIAL.choices, CREDENTIAL.repoRoot));
|
|
654
|
-
process.exit(1);
|
|
655
|
-
}
|
|
656
|
-
if (CREDENTIAL.choices?.length && interactive) {
|
|
657
|
-
const creds = await import('./lib/credentials.mjs');
|
|
658
|
-
const { originSlug } = await import('./lib/git.mjs');
|
|
659
|
-
const { basename } = await import('node:path');
|
|
660
|
-
const { choices, repoRoot } = CREDENTIAL;
|
|
661
|
-
console.log(
|
|
662
|
-
CREDENTIAL.reason === 'outside-repo'
|
|
663
|
-
? 'flowviant is not inside a git repo, and more than one project is connected here.'
|
|
664
|
-
: `This repo (${repoRoot}) is not connected to any project yet. Connected on this machine:`
|
|
665
|
-
);
|
|
666
|
-
|
|
667
|
-
const loginLabel = `connect ${repoRoot ? 'this repo' : 'a repo'} to a different project (${terminalCommand('login')})`;
|
|
668
|
-
// WHICH ONE LOOKS RIGHT — a pre-selection, never an auto-serve. The resolver
|
|
669
|
-
// refuses to serve a project the repo PATH did not name (the skadooble law);
|
|
670
|
-
// this only decides which row the cursor starts on, using the repo's folder
|
|
671
|
-
// name and its github repo-name against the stored project names. A unique
|
|
672
|
-
// match becomes ONE keypress; a wrong guess costs nothing, because the human
|
|
673
|
-
// still confirms. (Two projects bound to THIS repo never reach here: that
|
|
674
|
-
// is a refusal above, since 2026-09-23.)
|
|
675
|
-
const slug = repoRoot ? originSlug(repoRoot) : null;
|
|
676
|
-
const likely = creds.likelyChoiceIndex(choices, {
|
|
677
|
-
repoBasename: repoRoot ? basename(repoRoot) : null,
|
|
678
|
-
repoSlugName: slug ? slug.split('/')[1] : null,
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Bounded like the confirm below, and for the same reason — but silence
|
|
682
|
-
// means something DIFFERENT here and the difference is load-bearing. There
|
|
683
|
-
// is a real ambiguity to resolve; serving a guess is the skadooble bug.
|
|
684
|
-
// So no answer REFUSES, which is exactly what this branch already does
|
|
685
|
-
// headless, and the message says how to answer without being present.
|
|
686
|
-
let chosen = null; // 0-based into [...choices, login]
|
|
687
|
-
if (menuSupported()) {
|
|
688
|
-
const rowLabel = (e) =>
|
|
689
|
-
creds.projectRowLabel(e, choices) +
|
|
690
|
-
(e.repoRoot ? ` — connected for ${e.repoRoot}` : ' — not tied to a repo yet');
|
|
691
|
-
const options = [...choices.map(rowLabel), loginLabel];
|
|
692
|
-
// Say WHY the cursor starts where it does — "intuitive" made visible.
|
|
693
|
-
if (likely >= 0) options[likely] += ' ← looks like this repo';
|
|
694
|
-
const res = await selectMenu({
|
|
695
|
-
options,
|
|
696
|
-
defaultIndex: likely >= 0 ? likely : 0,
|
|
697
|
-
});
|
|
698
|
-
if (res.cancelled) {
|
|
699
|
-
console.error('nothing chosen — nothing started.');
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
if (!res.unsupported) chosen = res.index;
|
|
703
|
-
}
|
|
704
|
-
if (chosen === null) {
|
|
705
|
-
// Numeric fallback — a pipe, or a terminal without raw mode. Empty Enter
|
|
706
|
-
// takes the likely default when there is one, so it is one keystroke here
|
|
707
|
-
// too.
|
|
708
|
-
console.log(listLines(choices, creds));
|
|
709
|
-
console.log(` ${choices.length + 1}. ${loginLabel}`);
|
|
710
|
-
const hint = likely >= 0 ? ` (enter for ${creds.projectLabel(choices[likely])})` : '';
|
|
711
|
-
const raw = await askWithTimeout(
|
|
712
|
-
`Which project should this daemon serve? [1-${choices.length + 1}]${hint} `
|
|
713
|
-
);
|
|
714
|
-
if (raw === null) {
|
|
715
|
-
// The read itself failed — stdin closed, or the terminal went away
|
|
716
|
-
// mid-question. Not a timeout: there is none.
|
|
717
|
-
console.error(
|
|
718
|
-
`\ncould not read an answer — nothing started. ` +
|
|
719
|
-
`Name one with \`--project <name|id>\`, or run \`flowviant\` here in the foreground and pick.`
|
|
720
|
-
);
|
|
721
|
-
process.exit(1);
|
|
722
|
-
}
|
|
723
|
-
const n = raw === '' && likely >= 0 ? likely + 1 : Number.parseInt(raw, 10);
|
|
724
|
-
chosen = Number.isInteger(n) ? n - 1 : -1;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (chosen === choices.length) await reexecAfterLogin();
|
|
728
|
-
const picked = chosen >= 0 ? choices[chosen] : undefined;
|
|
729
|
-
if (!picked) {
|
|
730
|
-
console.error('nothing chosen — nothing started.');
|
|
731
|
-
process.exit(1);
|
|
732
|
-
}
|
|
733
|
-
// An answered question is consent: adopt it, and BIND it to this repo so
|
|
734
|
-
// the next start needs no prompt. Repointing is legitimate and said aloud.
|
|
735
|
-
if (repoRoot && picked.repoRoot && picked.repoRoot !== repoRoot) {
|
|
736
|
-
console.log(`note: ${creds.projectLabel(picked)} was connected for ${picked.repoRoot} — now serving ${repoRoot} instead.`);
|
|
737
|
-
}
|
|
738
|
-
adoptStoredCredential(picked);
|
|
739
|
-
// BOUND ONLY ONCE THE START HAS THE LOCK. Written here, the answer moved
|
|
740
|
-
// the binding even when the instance lock then refused the start — a live
|
|
741
|
-
// daemon for the same project in its own checkout — and that daemon's next
|
|
742
|
-
// unattended self-update re-exec'd into a store that no longer bound its
|
|
743
|
-
// repo, found no match headless, and exited with the machine dark.
|
|
744
|
-
afterLock = () => creds.selectStoredProject(picked.projectId, { bindRepoRoot: repoRoot ?? undefined });
|
|
745
|
-
console.log(`serving ${creds.projectLabel(picked)}${repoRoot ? ` from ${repoRoot}` : ''}.`);
|
|
746
|
-
} else if (CREDENTIAL.choices?.length) {
|
|
747
|
-
const creds = await import('./lib/credentials.mjs');
|
|
748
|
-
console.error(
|
|
749
|
-
'error: more than one project is connected on this machine and this repo is not bound to any of them:\n' +
|
|
750
|
-
listLines(CREDENTIAL.choices, creds) +
|
|
751
|
-
`\nPick one with \`--project <name|id>\`, bind this repo by running \`${terminalCommand()}\` here in a terminal once,\n` +
|
|
752
|
-
`or connect this repo to its own project with \`${terminalCommand('login')}\`.`
|
|
753
|
-
);
|
|
754
|
-
process.exit(1);
|
|
755
|
-
} else {
|
|
756
|
-
console.error(
|
|
757
|
-
'error: no credential found. Easiest:\n' +
|
|
758
|
-
` ${terminalCommand('login')} (approve in the app — recommended)\n` +
|
|
759
|
-
'Or set:\n' +
|
|
760
|
-
' FLOWVIANT_FLEET=fva_… (machine token, from the app)'
|
|
761
|
-
);
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
} else if (!externalToken && CREDENTIAL.needsConfirm && interactive) {
|
|
765
|
-
// ONE stored project, never tied to a repo — the pre-0.55.0 world. Ask once;
|
|
766
|
-
// yes binds and every later start is silent. This is the exact question
|
|
767
|
-
// whose absence had a calendar checkout serving skadooble.
|
|
768
|
-
//
|
|
769
|
-
// AND IT TIMES OUT, because a prompt on a start path is a way for a machine
|
|
770
|
-
// to go dark. `FLOWVIANT_REEXEC` above covers the restart THIS version
|
|
771
|
-
// performs, but it cannot cover the one that matters most: the hop that
|
|
772
|
-
// installs a fixed daemon is spawned by the OLD one, which never sets it.
|
|
773
|
-
// 0.55.0 → 0.55.1 was exactly that — an auto-update landing unattended would
|
|
774
|
-
// stop here with the machine serving nothing. A guard that only works once
|
|
775
|
-
// everyone already has it is not a guard.
|
|
776
|
-
//
|
|
777
|
-
// IF THE READ FAILS WE SERVE, AND WE DO NOT BIND. (This used to say "on
|
|
778
|
-
// timeout"; there is no timeout since 2026-09-20 — a person at the prompt
|
|
779
|
-
// is waited for. `null` now means stdin closed under the question.) Those
|
|
780
|
-
// are two decisions:
|
|
781
|
-
// · SERVE, because it is what every version before 0.55.0 did with this
|
|
782
|
-
// exact store, so the silent path is the status quo rather than a new
|
|
783
|
-
// risk — and a daemon that answers is strictly better than one that does
|
|
784
|
-
// not, which is the whole reason this product has exactly one refusal.
|
|
785
|
-
// · DO NOT BIND, because binding is the thing the question was FOR. Nobody
|
|
786
|
-
// answered, so nothing is cemented; the next human start asks again. That
|
|
787
|
-
// keeps the skadooble case fixed for the person who is actually looking,
|
|
788
|
-
// which is the only person it could ever have been fixed for.
|
|
789
|
-
const creds = await import('./lib/credentials.mjs');
|
|
790
|
-
const label = creds.projectLabel(CREDENTIAL.entry);
|
|
791
|
-
const answered = await askWithTimeout(
|
|
792
|
-
`This machine's one connected project is ${label}. Serve this repo (${CREDENTIAL.repoRoot}) as ${label}? [Y/n] `
|
|
793
|
-
);
|
|
794
|
-
const raw = answered === null ? null : answered.toLowerCase(); // null = the read failed
|
|
795
|
-
if (raw === null) {
|
|
796
|
-
console.log(
|
|
797
|
-
`\n could not read an answer — serving ${label} for this run ` +
|
|
798
|
-
`without tying it to this repo. Run \`${terminalCommand()}\` here and answer to make it stick.`
|
|
799
|
-
);
|
|
800
|
-
} else if (raw === '' || raw === 'y' || raw === 'yes') {
|
|
801
|
-
// After the lock, for the reason the picker's bind gives above.
|
|
802
|
-
afterLock = () => creds.bindStoredRepo(CREDENTIAL.entry.projectId, CREDENTIAL.repoRoot);
|
|
803
|
-
} else {
|
|
804
|
-
console.error(
|
|
805
|
-
`nothing started. Connect this repo to its own project with \`${terminalCommand('login')}\`, ` +
|
|
806
|
-
`or see what is stored with \`${terminalCommand('projects')}\`.`
|
|
807
|
-
);
|
|
808
|
-
process.exit(1);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (process.argv.includes('--json-events') && dirAt >= 0 && CREDENTIAL?.entry) {
|
|
813
|
-
const { detectRepoRoot, listStoredProjects, boundElsewhere } = await import('./lib/credentials.mjs');
|
|
814
|
-
const repo = detectRepoRoot();
|
|
815
|
-
const entry = CREDENTIAL.entry;
|
|
816
|
-
if (!repo || entry.repoRoot !== repo || boundElsewhere(listStoredProjects(), repo, entry.projectId).length) {
|
|
817
|
-
console.error('error: this project is not connected to this repository. Run `flowviant login` for the selected folder.');
|
|
818
|
-
process.exit(1);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
const { installDaemonLogging } = await import('./lib/desktopContract.mjs');
|
|
822
|
-
installDaemonLogging(CREDENTIAL?.entry?.projectId, { jsonEvents: process.argv.includes('--json-events') });
|
|
425
|
+
// WHICH PROJECT THIS START SERVES — said, asked, or refused; never guessed.
|
|
426
|
+
// startProject.mjs owns the picker, the one binding confirm and the refusals;
|
|
427
|
+
// what it hands back is the binding the person consented to, deferred until
|
|
428
|
+
// runFleetDaemon holds the instance lock (a refused start moves nothing).
|
|
429
|
+
const { resolveStartProject } = await import('./lib/startProject.mjs');
|
|
430
|
+
const afterLock = await resolveStartProject({ dirGiven: dirAt >= 0 });
|
|
431
|
+
|
|
432
|
+
const { installDaemonLogging } = await import('./lib/daemonLogging.mjs');
|
|
433
|
+
const { daemonLogPath } = await import('./lib/daemonState.mjs');
|
|
434
|
+
const logProjectId = CREDENTIAL?.entry?.projectId;
|
|
435
|
+
installDaemonLogging(logProjectId ? daemonLogPath(logProjectId) : null, { jsonEvents: process.argv.includes('--json-events') });
|
|
823
436
|
await runFleetDaemon({ afterLock });
|