flowviant 0.52.0 → 0.54.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 +25 -31
- package/bin/cli.mjs +28 -22
- package/bin/lib/authproxy.mjs +131 -32
- package/bin/lib/config.mjs +15 -20
- package/bin/lib/fleet.mjs +141 -63
- package/bin/lib/git.mjs +1 -1
- package/bin/lib/instance.mjs +296 -17
- package/bin/lib/listeners.mjs +269 -0
- package/bin/lib/login.mjs +5 -1
- package/bin/lib/mcp-cli.mjs +12 -11
- package/bin/lib/preflight.mjs +8 -5
- package/bin/lib/preview.mjs +294 -390
- package/bin/lib/runtimes.mjs +12 -3
- package/bin/lib/stream.mjs +10 -8
- package/bin/lib/update.mjs +3 -2
- package/bin/lib/work.mjs +249 -5
- package/package.json +1 -1
package/bin/lib/fleet.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
USER_AGENT,
|
|
24
24
|
MCP_URL,
|
|
25
25
|
SAFE,
|
|
26
|
+
DAEMON_INSTANCE,
|
|
26
27
|
POLL_SECONDS,
|
|
27
28
|
MAX_CONCURRENT,
|
|
28
29
|
IDLE_SECONDS,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
REFRESH_BEFORE_SECONDS,
|
|
31
32
|
LIVE,
|
|
32
33
|
AUTO_UPDATE,
|
|
33
|
-
ALLOW_PATCHES,
|
|
34
34
|
} from './config.mjs';
|
|
35
35
|
import { handleVersionSignal } from './update.mjs';
|
|
36
36
|
import {
|
|
@@ -77,7 +77,7 @@ import { detectRuntimes, knownSkills, pickRuntimeFor, RUNTIMES } from './runtime
|
|
|
77
77
|
import { createWorkManager } from './work.mjs';
|
|
78
78
|
import { scanLocalSessions } from './localSessions.mjs';
|
|
79
79
|
|
|
80
|
-
async function fetchRoster(haveIds) {
|
|
80
|
+
async function fetchRoster(haveIds, livePreviewSessionIds = [], heldSessionIds = []) {
|
|
81
81
|
const url = new URL(FLEET_URL);
|
|
82
82
|
if (haveIds.length) url.searchParams.set('have', haveIds.join(','));
|
|
83
83
|
// What this machine will run at once. The server grows lanes to meet waiting
|
|
@@ -98,6 +98,20 @@ async function fetchRoster(haveIds) {
|
|
|
98
98
|
// so a team can see whether the shared box runs wide open, and enforces
|
|
99
99
|
// nothing (membership is the consent boundary). Older servers ignore it.
|
|
100
100
|
url.searchParams.set('safe', SAFE ? '1' : '0');
|
|
101
|
+
// WHICH PROCESS, so the server can lease preview work to exactly one of two
|
|
102
|
+
// daemons on one credential. Older servers ignore unknown params.
|
|
103
|
+
url.searchParams.set('di', DAEMON_INSTANCE);
|
|
104
|
+
// Which shares this machine is still serving. It rides the poll rather than
|
|
105
|
+
// taking an endpoint of its own: one beat, no floor, and the stale window is
|
|
106
|
+
// the reconcile interval instead of minutes — which matters, because a share
|
|
107
|
+
// the server still calls live is a 530 on somebody's phone. Always set, even
|
|
108
|
+
// empty: '' means "serving none", absent would mean "an older daemon".
|
|
109
|
+
url.searchParams.set('pv', livePreviewSessionIds.join(','));
|
|
110
|
+
// The sessions this daemon holds a worktree for. Its LEASE on each renews
|
|
111
|
+
// here — one beat, no extra endpoint, and the server can tell "this daemon is
|
|
112
|
+
// still serving that tab" from "it went away" within a reconcile interval
|
|
113
|
+
// instead of minutes. Always set, even empty: '' means "holding none".
|
|
114
|
+
url.searchParams.set('ws', heldSessionIds.join(','));
|
|
101
115
|
// WHICH CLIs this machine actually has, so the app can stop guessing.
|
|
102
116
|
//
|
|
103
117
|
// Until now every surface that listed Gemini or Codex said "not wired up yet"
|
|
@@ -261,20 +275,13 @@ async function maybeReportLocalSessions({ repoRoot, excludeDirs }) {
|
|
|
261
275
|
|
|
262
276
|
export async function runFleetDaemon() {
|
|
263
277
|
console.log('');
|
|
264
|
-
console.log(` ${c.bold(c.cyan('◣ flowviant'))} ${c.dim(`
|
|
278
|
+
console.log(` ${c.bold(c.cyan('◣ flowviant'))} ${c.dim(`machine daemon · v${VERSION}`)}`);
|
|
265
279
|
console.log(` ${c.dim('──────────────────────────────────────────────')}`);
|
|
266
280
|
const repoRoot = repoRootOrDie();
|
|
267
281
|
const baseRef = detectBaseRef(repoRoot);
|
|
268
282
|
info(SAFE ? 'mode · safe (restricted toolset)' : 'mode · unattended (skips permission prompts)');
|
|
269
283
|
info(`repo · ${repoRoot}`);
|
|
270
284
|
info(`base · ${baseRef}`);
|
|
271
|
-
// Stated out loud because it is the one setting that lets something else write
|
|
272
|
-
// into the checkout you are sitting in.
|
|
273
|
-
info(
|
|
274
|
-
ALLOW_PATCHES
|
|
275
|
-
? 'patches· accepted — small changes land in your checkout for Keep/Revert (--no-patches to refuse)'
|
|
276
|
-
: 'patches· refused — everything arrives as a branch + PR'
|
|
277
|
-
);
|
|
278
285
|
info(`server · ${FLEET_URL}`);
|
|
279
286
|
console.log('');
|
|
280
287
|
|
|
@@ -285,18 +292,42 @@ export async function runFleetDaemon() {
|
|
|
285
292
|
// the SAME project served twice, and the worst version of this: their session
|
|
286
293
|
// worktrees are in different directories, so the per-turn lock cannot even see
|
|
287
294
|
// across them. See instance.mjs for why that lock is not enough on its own.
|
|
288
|
-
|
|
295
|
+
// Same repo -> this run replaces whatever was serving it. Different repo ->
|
|
296
|
+
// refused, and nothing is signalled. See instance.mjs's header for the rule.
|
|
297
|
+
const instance = acquireInstanceLock(FLEET_TOKEN, repoRoot, {
|
|
298
|
+
takeover:
|
|
299
|
+
process.argv.includes('--takeover') || process.argv.includes('--takeover-downgrade'),
|
|
300
|
+
noTakeover:
|
|
301
|
+
process.argv.includes('--no-takeover') || process.env.FLOWVIANT_NO_TAKEOVER === '1',
|
|
302
|
+
allowDowngrade: process.argv.includes('--takeover-downgrade'),
|
|
303
|
+
log: (m) => info(m),
|
|
304
|
+
});
|
|
289
305
|
if (!instance.ok) {
|
|
290
306
|
const h = instance.holder;
|
|
291
307
|
console.log('');
|
|
292
|
-
|
|
308
|
+
// Two different refusals, because they are two different mistakes and the
|
|
309
|
+
// fix is not the same. Same CREDENTIAL: one project is being served twice.
|
|
310
|
+
// Same REPO under another credential: two daemons in one working tree,
|
|
311
|
+
// which the credential-keyed lock cannot see on its own.
|
|
312
|
+
if (instance.takeoverFailed) {
|
|
313
|
+
fail(`could not replace the running daemon: ${instance.takeoverFailed}`);
|
|
314
|
+
} else if (instance.sameRepo) {
|
|
315
|
+
fail('a flowviant daemon is already running in this repo.');
|
|
316
|
+
} else {
|
|
317
|
+
fail('a flowviant daemon is already running for this credential.');
|
|
318
|
+
}
|
|
293
319
|
if (h?.pid) info(`holder · pid ${h.pid}${h.repoRoot ? ` in ${h.repoRoot}` : ''}`);
|
|
294
320
|
// The two-checkouts case is the one nobody spots on their own: both tabs
|
|
295
321
|
// look healthy, and the damage is doubled cards and doubled edits in a repo
|
|
296
322
|
// you are not looking at. Name the other repo when it is a different one.
|
|
297
|
-
if (h?.repoRoot && h.repoRoot !== repoRoot)
|
|
323
|
+
if (!instance.sameRepo && h?.repoRoot && h.repoRoot !== repoRoot) {
|
|
298
324
|
warn('that is a DIFFERENT checkout — one credential serves one project, so both would answer the same tabs.');
|
|
299
|
-
|
|
325
|
+
// Not offered lightly: that daemon is serving other work, and this
|
|
326
|
+
// command was run somewhere else. Replacing it is a decision, not a
|
|
327
|
+
// restart, so it takes a word.
|
|
328
|
+
note('run with --takeover to stop it and serve this repo instead.');
|
|
329
|
+
}
|
|
330
|
+
note('or run this one with FLOWVIANT_ALLOW_MULTI=1 if you know what you are doing.');
|
|
300
331
|
console.log('');
|
|
301
332
|
process.exit(1);
|
|
302
333
|
}
|
|
@@ -374,6 +405,14 @@ export async function runFleetDaemon() {
|
|
|
374
405
|
const nextByAgent = new Map();
|
|
375
406
|
let leaseTtlSeconds = 24 * 60 * 60; // updated from each roster response
|
|
376
407
|
let mcpUrl = MCP_URL;
|
|
408
|
+
// DEAD, and kept only because unpicking it is a rewire rather than a
|
|
409
|
+
// deletion: nothing calls `workers.set` anywhere in this tree. It held
|
|
410
|
+
// DISPATCH lanes, and the server has sent `agents: []` permanently since
|
|
411
|
+
// 2026-08-19, so every loop below iterates nothing. Two `stopPreview` calls
|
|
412
|
+
// hung off it until 2026-08-21 and read as live preview wiring; they were
|
|
413
|
+
// deleted, not rewired. When the Workbench preview lands, its teardown is
|
|
414
|
+
// keyed on sessionId and belongs beside retireWorkSessions in work.mjs —
|
|
415
|
+
// NOT here.
|
|
377
416
|
const workers = new Map(); // agentId -> { state, promise, wt, label }
|
|
378
417
|
let daemonAlive = true; // flipped false on shutdown so the stream stops reconnecting
|
|
379
418
|
let stream = null; // push channel handle (set once the loop is set up)
|
|
@@ -413,15 +452,10 @@ export async function runFleetDaemon() {
|
|
|
413
452
|
} catch {
|
|
414
453
|
/* best-effort */
|
|
415
454
|
}
|
|
416
|
-
// Stop the detached preview (dev server + cloudflared tunnel) — it's its
|
|
417
|
-
// own process group and survives our exit, otherwise leaking a port-bound
|
|
418
|
-
// server + a live tunnel serving a stale branch until reboot.
|
|
419
|
-
try {
|
|
420
|
-
w.state.stopPreview?.();
|
|
421
|
-
} catch {
|
|
422
|
-
/* best-effort */
|
|
423
|
-
}
|
|
424
455
|
}
|
|
456
|
+
// Detached tunnels survive our exit by design, so leaving them would strand
|
|
457
|
+
// a public hostname until the box rebooted.
|
|
458
|
+
shutdownPreviews();
|
|
425
459
|
};
|
|
426
460
|
process.on('SIGINT', () => {
|
|
427
461
|
console.log('');
|
|
@@ -593,6 +627,11 @@ export async function runFleetDaemon() {
|
|
|
593
627
|
processWorkTurns,
|
|
594
628
|
processShipJobs,
|
|
595
629
|
processDiffJobs,
|
|
630
|
+
heldSessionIds,
|
|
631
|
+
processPreviewJobs,
|
|
632
|
+
livePreviewIds,
|
|
633
|
+
retirePreviews,
|
|
634
|
+
shutdownPreviews,
|
|
596
635
|
retireWorkSessions,
|
|
597
636
|
reportWorktrees,
|
|
598
637
|
shutdownWork,
|
|
@@ -856,20 +895,42 @@ export async function runFleetDaemon() {
|
|
|
856
895
|
|
|
857
896
|
// Changed files of a (merged) PR, for the re-ground prompt. Capped so a huge
|
|
858
897
|
// PR can't blow up the prompt. prUrl was already validated before the merge.
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
898
|
+
// WHICH FILES A SHIP CHANGED, read from the commits it landed.
|
|
899
|
+
//
|
|
900
|
+
// This asked `gh pr view <prUrl> --json files` until 2026-08-22, and `prUrl`
|
|
901
|
+
// has been null by construction since dispatch was deleted on 2026-08-19 —
|
|
902
|
+
// the server writes null and says so in a comment. Node threw on the null
|
|
903
|
+
// argument, the catch below read it as "gh failed", and the re-ground retried
|
|
904
|
+
// three times and gave up. Every post-ship re-ground for three months did
|
|
905
|
+
// that silently, while the spec said ship re-grounds the wiki.
|
|
906
|
+
//
|
|
907
|
+
// Returns null when it learned NOTHING (no shas, or none of them resolvable),
|
|
908
|
+
// which the caller still treats as retryable — distinct from a ship that
|
|
909
|
+
// genuinely changed no files.
|
|
910
|
+
const changedFilesForShas = (shas) => {
|
|
911
|
+
if (!Array.isArray(shas) || shas.length === 0) return null;
|
|
912
|
+
const files = new Set();
|
|
913
|
+
for (const sha of shas.slice(0, 50)) {
|
|
914
|
+
if (!/^[0-9a-f]{7,40}$/i.test(String(sha))) continue;
|
|
915
|
+
try {
|
|
916
|
+
const out = execFileSync(
|
|
917
|
+
'git',
|
|
918
|
+
['show', '--name-only', '--pretty=format:', String(sha)],
|
|
919
|
+
{ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
|
|
920
|
+
);
|
|
921
|
+
for (const line of out.split('\n')) {
|
|
922
|
+
const f = line.trim();
|
|
923
|
+
if (f) files.add(f);
|
|
924
|
+
if (files.size >= 60) break;
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
// One unreachable commit is not a failed re-ground — the ship merged
|
|
928
|
+
// to main and the rest of the shas still name real files. Only an
|
|
929
|
+
// EMPTY result is treated as "we learned nothing".
|
|
930
|
+
}
|
|
931
|
+
if (files.size >= 60) break;
|
|
872
932
|
}
|
|
933
|
+
return files.size ? [...files] : null;
|
|
873
934
|
};
|
|
874
935
|
const regroundAttempts = new Map(); // intentId -> gh-failure count
|
|
875
936
|
|
|
@@ -1025,7 +1086,7 @@ export async function runFleetDaemon() {
|
|
|
1025
1086
|
warn('wiki sweep ended without WIKI_DONE — partial pages synced; retry from the app.');
|
|
1026
1087
|
await runSync(complete);
|
|
1027
1088
|
} else {
|
|
1028
|
-
const files =
|
|
1089
|
+
const files = changedFilesForShas(task.shas);
|
|
1029
1090
|
if (files === null) {
|
|
1030
1091
|
// gh failed (network/auth) — retry via the durable job a couple
|
|
1031
1092
|
// of times before consuming it, so a transient outage doesn't
|
|
@@ -1033,11 +1094,11 @@ export async function runFleetDaemon() {
|
|
|
1033
1094
|
const n = (regroundAttempts.get(task.intentId) ?? 0) + 1;
|
|
1034
1095
|
regroundAttempts.set(task.intentId, n);
|
|
1035
1096
|
if (n < 3) {
|
|
1036
|
-
warn(`wiki re-ground for "${task.title}":
|
|
1097
|
+
warn(`wiki re-ground for "${task.title}": no changed files resolved — will retry (${n}/3)`);
|
|
1037
1098
|
groundedIntents.delete(task.intentId); // let the roster re-offer it
|
|
1038
1099
|
continue;
|
|
1039
1100
|
}
|
|
1040
|
-
warn(`wiki re-ground for "${task.title}":
|
|
1101
|
+
warn(`wiki re-ground for "${task.title}": could not resolve changed files ${n} times — giving up (heals on the next full sweep)`);
|
|
1041
1102
|
} else if (files.length === 0) {
|
|
1042
1103
|
note(`${c.cyan('wiki')} ${c.dim(`— "${task.title}": no changed files to re-ground`)}`);
|
|
1043
1104
|
} else {
|
|
@@ -1153,7 +1214,7 @@ export async function runFleetDaemon() {
|
|
|
1153
1214
|
for (;;) {
|
|
1154
1215
|
let roster;
|
|
1155
1216
|
try {
|
|
1156
|
-
roster = await fetchRoster(buildHave());
|
|
1217
|
+
roster = await fetchRoster(buildHave(), livePreviewIds(), heldSessionIds());
|
|
1157
1218
|
} catch (e) {
|
|
1158
1219
|
if (e.auth) {
|
|
1159
1220
|
fail(`${e.message} — credential revoked or invalid. Shutting down.`);
|
|
@@ -1215,11 +1276,29 @@ export async function runFleetDaemon() {
|
|
|
1215
1276
|
// AFTER the work/ship intake: retirement is the server saying which
|
|
1216
1277
|
// sessions are LIVE, and the guards above (chains, shipping) are populated
|
|
1217
1278
|
// by the intake this same tick.
|
|
1218
|
-
|
|
1279
|
+
// BEFORE retirement, and the order is load-bearing: `git worktree remove`
|
|
1280
|
+
// under a running dev server leaves it serving bytes from open file handles
|
|
1281
|
+
// in a directory that no longer exists — a human is shown the wrong thing
|
|
1282
|
+
// and nothing errors anywhere.
|
|
1283
|
+
retirePreviews(roster.activeWorkSessions);
|
|
1284
|
+
// A session another daemon on this credential is serving is NOT a closed
|
|
1285
|
+
// tab. Without this the daemon that lost the lease removes the worktree the
|
|
1286
|
+
// winner is working in — absence would mean "somebody else won" instead of
|
|
1287
|
+
// "the tab closed".
|
|
1288
|
+
retireWorkSessions(
|
|
1289
|
+
Array.isArray(roster.activeWorkSessions)
|
|
1290
|
+
? roster.activeWorkSessions
|
|
1291
|
+
: roster.activeWorkSessions,
|
|
1292
|
+
roster.sessionsHeldElsewhere
|
|
1293
|
+
);
|
|
1219
1294
|
// Diffs somebody has open and is waiting on. Project-scoped rather than
|
|
1220
1295
|
// per-session: `git show` runs from the repo ROOT, which can see a closed
|
|
1221
1296
|
// tab's branch and a shipped commit on main alike.
|
|
1222
1297
|
processDiffJobs(roster.diffJobs);
|
|
1298
|
+
// Shares to open or tear down. CLAIMED before acted on — two daemons on one
|
|
1299
|
+
// credential are both handed this array, and both opening a tunnel strands
|
|
1300
|
+
// a public hostname nobody can settle.
|
|
1301
|
+
processPreviewJobs(roster.previewJobs);
|
|
1223
1302
|
// …and what the SURVIVING ones hold: branch, ahead-of-base, diffstat.
|
|
1224
1303
|
// Throttled inside, never awaited — a `git status` the human cannot run
|
|
1225
1304
|
// themselves from a browser, relayed. After retirement so a directory that
|
|
@@ -1236,24 +1315,23 @@ export async function runFleetDaemon() {
|
|
|
1236
1315
|
const sig = [...rosterIds].sort().join(',');
|
|
1237
1316
|
if (sig !== rosterSig) {
|
|
1238
1317
|
rosterSig = sig;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
info('Machine online. Open a tab in Flowviant → Workbench to start working.');
|
|
1246
|
-
} else {
|
|
1247
|
-
note(`Roster: ${c.bold(String(rosterIds.size))} agent${rosterIds.size === 1 ? '' : 's'}.`);
|
|
1248
|
-
}
|
|
1318
|
+
// `agents` is permanently [] — the lanes it counted died with dispatch
|
|
1319
|
+
// and the array survives only as wire compat, so this runs once, on the
|
|
1320
|
+
// first poll. It used to point at the Cockpit, a surface deleted
|
|
1321
|
+
// 2026-08-04 that now redirects to the Board. Say what is actually true
|
|
1322
|
+
// instead: the machine is up, and work starts in a tab.
|
|
1323
|
+
info('Machine online. Open a tab in Flowviant → Workbench to start working.');
|
|
1249
1324
|
}
|
|
1250
|
-
// Heartbeat so a quiet
|
|
1251
|
-
|
|
1325
|
+
// Heartbeat so a quiet daemon visibly stays alive. Gated on REAL work —
|
|
1326
|
+
// `rosterIds` is built from `roster.agents`, which the server sends
|
|
1327
|
+
// permanently empty, so gating on it printed "waiting" once a minute even
|
|
1328
|
+
// while a tab's turn was running. `workBusy()` is the honest question: are
|
|
1329
|
+
// there session turns, ships or unsettled reports in flight?
|
|
1330
|
+
if (!workBusy() && Date.now() - idleBeatAt > 60_000) {
|
|
1252
1331
|
idleBeatAt = Date.now();
|
|
1253
|
-
info('
|
|
1332
|
+
info('machine online — nothing running right now.');
|
|
1254
1333
|
}
|
|
1255
1334
|
|
|
1256
|
-
|
|
1257
1335
|
// Living-wiki work (runs under its own minted wiki token — no agent
|
|
1258
1336
|
// needed). enqueueSweep queues a Regenerate; regroundJobs re-offers merged
|
|
1259
1337
|
// deliveries whose re-ground never ran (e.g. we restarted between merge and
|
|
@@ -1269,13 +1347,18 @@ export async function runFleetDaemon() {
|
|
|
1269
1347
|
|
|
1270
1348
|
// Env sync tick: register/bootstrap/wrap/rotate/sync as the roster block
|
|
1271
1349
|
// dictates (self-guarded — one operation at a time, errors retry next
|
|
1272
|
-
// poll). A fresh bundle rematerializes every
|
|
1273
|
-
//
|
|
1350
|
+
// poll). A fresh bundle rematerializes every SESSION worktree this daemon
|
|
1351
|
+
// holds — read off the sessions directory, the same fact retirement acts
|
|
1352
|
+
// on; the wiki worktree NEVER gets env (the cartographer doesn't need
|
|
1353
|
+
// secrets). This used to iterate the dispatch-era `workers` map, which
|
|
1354
|
+
// nothing has ever `.set()`, so a rotation reached no worktree at all.
|
|
1355
|
+
// Safe mid-turn by construction: materializeInto refuses to write anything
|
|
1356
|
+
// git does not ignore, so it cannot dirty a tree and block a ship.
|
|
1274
1357
|
void handleRosterEnv(roster.env, { projectId: roster.project?.id }).then(({ changed }) => {
|
|
1275
1358
|
if (!changed) return;
|
|
1276
|
-
for (const
|
|
1359
|
+
for (const id of heldSessionIds()) {
|
|
1277
1360
|
try {
|
|
1278
|
-
materializeInto(
|
|
1361
|
+
materializeInto(join(baseDir, 'sessions', id));
|
|
1279
1362
|
} catch {
|
|
1280
1363
|
/* best-effort */
|
|
1281
1364
|
}
|
|
@@ -1323,11 +1406,6 @@ export async function runFleetDaemon() {
|
|
|
1323
1406
|
} catch {
|
|
1324
1407
|
/* best-effort */
|
|
1325
1408
|
}
|
|
1326
|
-
try {
|
|
1327
|
-
w.state.stopPreview?.();
|
|
1328
|
-
} catch {
|
|
1329
|
-
/* best-effort */
|
|
1330
|
-
}
|
|
1331
1409
|
// Only poll mode's per-lane tree dies with the lane. A live lane owns no
|
|
1332
1410
|
// checkout: the task it was building has its own, which must SURVIVE —
|
|
1333
1411
|
// removing a lane requeues its task, and the next lane to pick that task
|
package/bin/lib/git.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export function repoRootOrDie() {
|
|
|
35
35
|
try {
|
|
36
36
|
return git(['rev-parse', '--show-toplevel'], process.cwd());
|
|
37
37
|
} catch {
|
|
38
|
-
console.error('error:
|
|
38
|
+
console.error('error: the flowviant daemon must run inside a git repo.');
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
41
|
}
|