@the-open-engine/zeroshot 6.2.0 → 6.4.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 +141 -474
- package/cli/event-copy.js +12 -0
- package/cli/index.js +314 -96
- package/cli/message-formatters-normal.js +14 -2
- package/cli/message-formatters-watch.js +9 -2
- package/cluster-hooks/block-ask-user-question.py +53 -0
- package/cluster-hooks/block-dangerous-git.py +145 -0
- package/lib/clusters-registry.js +48 -0
- package/lib/compose-utils.js +101 -0
- package/lib/detached-startup.js +12 -1
- package/lib/id-detector.js +8 -9
- package/lib/path-check.js +63 -0
- package/lib/run-mode.js +34 -0
- package/lib/run-plan.js +32 -0
- package/lib/start-cluster.js +58 -28
- package/package.json +7 -6
- package/scripts/check-path.js +19 -0
- package/scripts/validate-templates.js +11 -29
- package/src/agent/agent-hook-executor.js +1 -1
- package/src/agent/agent-lifecycle.js +2 -0
- package/src/agent/agent-task-executor.js +4 -3
- package/src/agent/pr-verification.js +27 -1
- package/src/agents/git-pusher-template.js +121 -4
- package/src/attach/socket-discovery.js +2 -3
- package/src/claude-credentials.js +75 -0
- package/src/claude-task-runner.js +1 -0
- package/src/isolation-manager.js +12 -9
- package/src/issue-providers/README.md +45 -15
- package/src/issue-providers/index.js +2 -0
- package/src/issue-providers/jira-provider.js +8 -1
- package/src/issue-providers/linear-provider.js +405 -0
- package/src/ledger.js +45 -3
- package/src/lib/gc.js +2 -3
- package/src/message-bus.js +22 -2
- package/src/orchestrator.js +271 -144
- package/src/preflight.js +12 -16
- package/src/providers/base-provider.js +7 -6
- package/src/status-footer.js +13 -1
- package/src/template-validation/index.js +3 -0
- package/src/template-validation/report-formatter.js +55 -0
- package/src/worktree-claude-config.js +2 -13
- package/task-lib/runner.js +1 -0
- package/task-lib/watcher.js +1 -0
package/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const { URL } = require('url');
|
|
|
22
22
|
const chalk = require('chalk');
|
|
23
23
|
const Orchestrator = require('../src/orchestrator');
|
|
24
24
|
const { setupCompletion } = require('../lib/completion');
|
|
25
|
+
const { resolveRunMode, describeRunMode } = require('../lib/run-mode');
|
|
25
26
|
const { formatWatchMode } = require('./message-formatters-watch');
|
|
26
27
|
const {
|
|
27
28
|
formatAgentLifecycle,
|
|
@@ -48,10 +49,16 @@ const {
|
|
|
48
49
|
} = require('../lib/settings');
|
|
49
50
|
const { normalizeProviderName } = require('../lib/provider-names');
|
|
50
51
|
const { getProvider, parseProviderChunk } = require('../src/providers');
|
|
52
|
+
const { readClustersFileSync } = require('../lib/clusters-registry');
|
|
51
53
|
const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
|
|
52
54
|
const {
|
|
53
55
|
detectGitRepoRoot,
|
|
54
56
|
detectRunInput,
|
|
57
|
+
isStdinInput,
|
|
58
|
+
readStdinText,
|
|
59
|
+
encodeStdinEnv,
|
|
60
|
+
decodeStdinEnv,
|
|
61
|
+
buildTextInput,
|
|
55
62
|
loadClusterConfig,
|
|
56
63
|
resolveConfigPath,
|
|
57
64
|
resolveProviderOverride,
|
|
@@ -66,10 +73,13 @@ const { runCmdproof } = require('./commands/cmdproof');
|
|
|
66
73
|
const {
|
|
67
74
|
markDetachedSetupFailed,
|
|
68
75
|
registerDetachedSetupCluster,
|
|
76
|
+
removeDetachedSetupCluster,
|
|
69
77
|
} = require('../lib/detached-startup');
|
|
70
78
|
// Setup wizard removed - use: zeroshot settings set <key> <value>
|
|
71
79
|
const { checkForUpdates, printLegacyDistroNotice } = require('./lib/update-checker');
|
|
80
|
+
const { checkBinDirOnPath, printPathWarning } = require('../lib/path-check');
|
|
72
81
|
const { StatusFooter, AGENT_STATE, ACTIVE_STATES } = require('../src/status-footer');
|
|
82
|
+
const { EVENT_COPY, formatMergeStatus } = require('./event-copy');
|
|
73
83
|
|
|
74
84
|
// =============================================================================
|
|
75
85
|
// GLOBAL ERROR HANDLERS - Prevent silent process death
|
|
@@ -179,9 +189,12 @@ function normalizeRunOptions(options) {
|
|
|
179
189
|
if (options.docker) {
|
|
180
190
|
options.worktree = false;
|
|
181
191
|
}
|
|
192
|
+
// autoMerge is NOT stored here — it is derived from the run plan (delivery ===
|
|
193
|
+
// 'ship') at every consumer. Writing it here unconditionally would clobber an
|
|
194
|
+
// explicit autoMerge intent (e.g. a future `--auto-merge` flag) back to false.
|
|
182
195
|
}
|
|
183
196
|
|
|
184
|
-
function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
|
|
197
|
+
async function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
|
|
185
198
|
// Detect which issue provider tool is needed
|
|
186
199
|
let issueProvider = null;
|
|
187
200
|
let targetHost = null;
|
|
@@ -205,7 +218,7 @@ function runClusterPreflight({ input, options, providerOverride, settings, force
|
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
requirePreflight({
|
|
221
|
+
await requirePreflight({
|
|
209
222
|
requireGh: issueProvider === 'github', // gh CLI required for GitHub
|
|
210
223
|
requireDocker: options.docker,
|
|
211
224
|
requireGit: options.worktree,
|
|
@@ -221,11 +234,8 @@ function shouldRunDetached(options) {
|
|
|
221
234
|
}
|
|
222
235
|
|
|
223
236
|
function printDetachedClusterStart(options, clusterId, logPath) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} else {
|
|
227
|
-
console.log(`Started ${clusterId}`);
|
|
228
|
-
}
|
|
237
|
+
const runMode = resolveRunMode(options);
|
|
238
|
+
console.log(runMode ? `Started ${clusterId} (${runMode})` : `Started ${clusterId}`);
|
|
229
239
|
if (logPath) {
|
|
230
240
|
console.log(`Setup log: ${logPath}`);
|
|
231
241
|
}
|
|
@@ -256,7 +266,7 @@ function resolveMergeQueueEnv(value) {
|
|
|
256
266
|
return '';
|
|
257
267
|
}
|
|
258
268
|
|
|
259
|
-
function buildDaemonEnv(options, clusterId, targetCwd) {
|
|
269
|
+
function buildDaemonEnv(options, clusterId, targetCwd, stdinText) {
|
|
260
270
|
const mergeQueueEnv = resolveMergeQueueEnv(options.mergeQueue);
|
|
261
271
|
const runOptionsEnv = serializeRunOptions(options);
|
|
262
272
|
return {
|
|
@@ -275,10 +285,11 @@ function buildDaemonEnv(options, clusterId, targetCwd) {
|
|
|
275
285
|
ZEROSHOT_MERGE_QUEUE: mergeQueueEnv,
|
|
276
286
|
ZEROSHOT_CLOSE_ISSUE: options.closeIssue || '',
|
|
277
287
|
ZEROSHOT_CWD: targetCwd,
|
|
288
|
+
ZEROSHOT_STDIN_TASK: stdinText !== undefined ? encodeStdinEnv(stdinText) : '',
|
|
278
289
|
};
|
|
279
290
|
}
|
|
280
291
|
|
|
281
|
-
async function spawnDetachedCluster(options, clusterId) {
|
|
292
|
+
async function spawnDetachedCluster(options, clusterId, stdinText) {
|
|
282
293
|
const { spawn } = require('child_process');
|
|
283
294
|
const logFd = createDaemonLogFile(clusterId);
|
|
284
295
|
const targetCwd = detectGitRepoRoot();
|
|
@@ -287,7 +298,7 @@ async function spawnDetachedCluster(options, clusterId) {
|
|
|
287
298
|
detached: true,
|
|
288
299
|
stdio: ['ignore', logFd, logFd],
|
|
289
300
|
cwd: targetCwd,
|
|
290
|
-
env: buildDaemonEnv(options, clusterId, targetCwd),
|
|
301
|
+
env: buildDaemonEnv(options, clusterId, targetCwd, stdinText),
|
|
291
302
|
});
|
|
292
303
|
await registerDetachedSetupCluster({
|
|
293
304
|
clusterId,
|
|
@@ -321,13 +332,8 @@ function printForegroundStartInfo(options, clusterId, configName) {
|
|
|
321
332
|
if (process.env.ZEROSHOT_DAEMON) {
|
|
322
333
|
return;
|
|
323
334
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
} else if (options.worktree) {
|
|
327
|
-
console.log(`Starting ${clusterId} (worktree)`);
|
|
328
|
-
} else {
|
|
329
|
-
console.log(`Starting ${clusterId}`);
|
|
330
|
-
}
|
|
335
|
+
const runMode = resolveRunMode(options);
|
|
336
|
+
console.log(runMode ? `Starting ${clusterId} (${runMode})` : `Starting ${clusterId}`);
|
|
331
337
|
console.log(chalk.dim(`Config: ${configName}`));
|
|
332
338
|
console.log(chalk.dim('Ctrl+C to stop following (cluster keeps running)\n'));
|
|
333
339
|
}
|
|
@@ -389,13 +395,14 @@ function applyModelOverrideToConfig(config, modelOverride, providerOverride, set
|
|
|
389
395
|
console.log(chalk.dim(`Model override: ${modelOverride} (all agents)`));
|
|
390
396
|
}
|
|
391
397
|
|
|
392
|
-
function createStatusFooter(clusterId, messageBus) {
|
|
398
|
+
function createStatusFooter(clusterId, messageBus, runMode) {
|
|
393
399
|
const statusFooter = new StatusFooter({
|
|
394
400
|
refreshInterval: 1000,
|
|
395
401
|
enabled: process.stdout.isTTY,
|
|
396
402
|
});
|
|
397
403
|
statusFooter.setCluster(clusterId);
|
|
398
404
|
statusFooter.setClusterState('running');
|
|
405
|
+
statusFooter.setRunMode(runMode);
|
|
399
406
|
statusFooter.setMessageBus(messageBus);
|
|
400
407
|
activeStatusFooter = statusFooter;
|
|
401
408
|
return statusFooter;
|
|
@@ -571,11 +578,11 @@ function waitForClusterCompletion(orchestrator, clusterId, cleanup) {
|
|
|
571
578
|
});
|
|
572
579
|
}
|
|
573
580
|
|
|
574
|
-
async function streamClusterInForeground(cluster, orchestrator, clusterId) {
|
|
581
|
+
async function streamClusterInForeground(cluster, orchestrator, clusterId, options) {
|
|
575
582
|
const sendersWithOutput = new Set();
|
|
576
583
|
const processedMessageIds = new Set();
|
|
577
584
|
|
|
578
|
-
const statusFooter = createStatusFooter(clusterId, cluster.messageBus);
|
|
585
|
+
const statusFooter = createStatusFooter(clusterId, cluster.messageBus, resolveRunMode(options));
|
|
579
586
|
const handleLifecycleMessage = createLifecycleHandler(statusFooter);
|
|
580
587
|
const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic(
|
|
581
588
|
'AGENT_LIFECYCLE',
|
|
@@ -629,22 +636,28 @@ function setupDaemonCleanup(orchestrator, clusterId) {
|
|
|
629
636
|
}
|
|
630
637
|
|
|
631
638
|
function readClusterTokenTotals(orchestrator, clusterId) {
|
|
632
|
-
let totalTokens = 0;
|
|
633
|
-
let totalCostUsd = 0;
|
|
634
639
|
try {
|
|
635
640
|
const clusterObj = orchestrator.getCluster(clusterId);
|
|
636
|
-
if (clusterObj?.messageBus) {
|
|
637
|
-
|
|
638
|
-
if (tokensByRole?._total?.count > 0) {
|
|
639
|
-
const total = tokensByRole._total;
|
|
640
|
-
totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
|
|
641
|
-
totalCostUsd = total.totalCostUsd || 0;
|
|
642
|
-
}
|
|
641
|
+
if (!clusterObj?.messageBus) {
|
|
642
|
+
return { totalTokens: 0, totalCostUsd: 0 };
|
|
643
643
|
}
|
|
644
|
-
|
|
645
|
-
|
|
644
|
+
const { tokensByRole } = clusterObj.messageBus.readSnapshot(clusterId);
|
|
645
|
+
const total = tokensByRole?._total;
|
|
646
|
+
if (!total || total.count === 0) {
|
|
647
|
+
return { totalTokens: 0, totalCostUsd: 0 };
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
totalTokens: (total.inputTokens || 0) + (total.outputTokens || 0),
|
|
651
|
+
totalCostUsd: total.totalCostUsd || 0,
|
|
652
|
+
};
|
|
653
|
+
} catch (error) {
|
|
654
|
+
// A confirmed-empty ledger (0) and a failed read must stay distinguishable -
|
|
655
|
+
// fabricating 0 here is what produced phantom "msgs=0 $0" rows in `list --json`.
|
|
656
|
+
console.error(
|
|
657
|
+
`[cli] Failed to read token totals for cluster ${clusterId}: ${error.message || String(error)}`
|
|
658
|
+
);
|
|
659
|
+
return { totalTokens: null, totalCostUsd: null };
|
|
646
660
|
}
|
|
647
|
-
return { totalTokens, totalCostUsd };
|
|
648
661
|
}
|
|
649
662
|
|
|
650
663
|
function enrichClustersWithTokens(clusters, orchestrator) {
|
|
@@ -672,8 +685,9 @@ function formatClusterRow(cluster) {
|
|
|
672
685
|
const stateDisplay =
|
|
673
686
|
cluster.state === 'zombie' ? chalk.red(cluster.state.padEnd(12)) : cluster.state.padEnd(12);
|
|
674
687
|
const rowColor = cluster.state === 'zombie' ? chalk.red : (text) => text;
|
|
688
|
+
const modeDisplay = (cluster.runMode || '-').padEnd(10);
|
|
675
689
|
|
|
676
|
-
return `${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${cluster.agentCount
|
|
690
|
+
return `${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${modeDisplay} ${cluster.agentCount
|
|
677
691
|
.toString()
|
|
678
692
|
.padEnd(8)} ${tokenDisplay.padEnd(12)} ${costDisplay.padEnd(8)} ${created}`;
|
|
679
693
|
}
|
|
@@ -687,11 +701,11 @@ function printClusterTable(enrichedClusters) {
|
|
|
687
701
|
|
|
688
702
|
console.log(chalk.bold('\n=== Clusters ==='));
|
|
689
703
|
console.log(
|
|
690
|
-
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'
|
|
691
|
-
|
|
692
|
-
)} ${'Cost'.padEnd(8)} Created`
|
|
704
|
+
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Mode'.padEnd(10)} ${'Agents'.padEnd(
|
|
705
|
+
8
|
|
706
|
+
)} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
|
|
693
707
|
);
|
|
694
|
-
console.log('-'.repeat(
|
|
708
|
+
console.log('-'.repeat(110));
|
|
695
709
|
for (const cluster of enrichedClusters) {
|
|
696
710
|
console.log(formatClusterRow(cluster));
|
|
697
711
|
}
|
|
@@ -734,13 +748,16 @@ function reportMissingId(id, options) {
|
|
|
734
748
|
function getClusterTokensByRole(orchestrator, clusterId) {
|
|
735
749
|
try {
|
|
736
750
|
const cluster = orchestrator.getCluster(clusterId);
|
|
737
|
-
if (cluster?.messageBus) {
|
|
738
|
-
return
|
|
751
|
+
if (!cluster?.messageBus) {
|
|
752
|
+
return null;
|
|
739
753
|
}
|
|
740
|
-
|
|
741
|
-
|
|
754
|
+
return cluster.messageBus.readSnapshot(clusterId).tokensByRole;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
console.error(
|
|
757
|
+
`[cli] Failed to read tokens by role for cluster ${clusterId}: ${error.message || String(error)}`
|
|
758
|
+
);
|
|
759
|
+
return null;
|
|
742
760
|
}
|
|
743
|
-
return null;
|
|
744
761
|
}
|
|
745
762
|
|
|
746
763
|
function printClusterStatusJson(status, tokensByRole) {
|
|
@@ -774,6 +791,9 @@ function printClusterStatusHeader(status, clusterId) {
|
|
|
774
791
|
} else {
|
|
775
792
|
console.log(`State: ${status.state}`);
|
|
776
793
|
}
|
|
794
|
+
if (status.runMode) {
|
|
795
|
+
console.log(`Mode: ${describeRunMode(status.runMode)}`);
|
|
796
|
+
}
|
|
777
797
|
if (status.pid) {
|
|
778
798
|
console.log(`PID: ${status.pid}`);
|
|
779
799
|
}
|
|
@@ -1216,9 +1236,26 @@ function followSetupLog(logPath) {
|
|
|
1216
1236
|
}
|
|
1217
1237
|
|
|
1218
1238
|
function printRecentMessages(messages, limit, isActive, options) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1239
|
+
if (options.watch) {
|
|
1240
|
+
const recentMessages = selectRecentPrintableMessages(messages, limit);
|
|
1241
|
+
for (const msg of recentMessages) {
|
|
1242
|
+
printMessage(msg, true, true, isActive);
|
|
1243
|
+
}
|
|
1244
|
+
if (recentMessages.length === 0 && messages.length > 0) {
|
|
1245
|
+
console.log(chalk.dim(`No printable history in ${messages.length} stored messages.`));
|
|
1246
|
+
}
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const output = renderRecentMessagesToTerminal(messages, limit);
|
|
1251
|
+
|
|
1252
|
+
if (output) {
|
|
1253
|
+
console.log(output);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (messages.length > 0) {
|
|
1258
|
+
console.log(chalk.dim(`No printable history in ${messages.length} stored messages.`));
|
|
1222
1259
|
}
|
|
1223
1260
|
}
|
|
1224
1261
|
|
|
@@ -1461,9 +1498,8 @@ async function printAttachableClusters(clusters, socketDiscovery) {
|
|
|
1461
1498
|
}
|
|
1462
1499
|
|
|
1463
1500
|
function readClusterFromDisk(id) {
|
|
1464
|
-
const clustersFile = path.join(os.homedir(), '.zeroshot', 'clusters.json');
|
|
1465
1501
|
try {
|
|
1466
|
-
const clusters =
|
|
1502
|
+
const clusters = readClustersFileSync(path.join(os.homedir(), '.zeroshot'));
|
|
1467
1503
|
return clusters[id] || null;
|
|
1468
1504
|
} catch {
|
|
1469
1505
|
return null;
|
|
@@ -1554,7 +1590,7 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
|
|
|
1554
1590
|
const cluster = readClusterFromDisk(id);
|
|
1555
1591
|
ensureClusterRunning(cluster, id);
|
|
1556
1592
|
|
|
1557
|
-
const orchestrator = await Orchestrator.create({ quiet: true });
|
|
1593
|
+
const orchestrator = await Orchestrator.create({ quiet: true, readonly: true });
|
|
1558
1594
|
try {
|
|
1559
1595
|
const status = orchestrator.getStatus(id);
|
|
1560
1596
|
const activeAgents = getActiveAgents(status);
|
|
@@ -2028,6 +2064,31 @@ function getOrchestrator() {
|
|
|
2028
2064
|
return _orchestratorPromise;
|
|
2029
2065
|
}
|
|
2030
2066
|
|
|
2067
|
+
// Separate lazy-loaded read-only orchestrator for commands that only ever read
|
|
2068
|
+
// cluster state (list/status/logs). Read-only ledgers can never contend with a
|
|
2069
|
+
// live daemon's writer connection or mutate clusters.json as a side effect.
|
|
2070
|
+
/** @type {import('../src/orchestrator') | null} */
|
|
2071
|
+
let _readonlyOrchestrator = null;
|
|
2072
|
+
/** @type {Promise<import('../src/orchestrator')> | null} */
|
|
2073
|
+
let _readonlyOrchestratorPromise = null;
|
|
2074
|
+
/**
|
|
2075
|
+
* @returns {Promise<import('../src/orchestrator')>}
|
|
2076
|
+
*/
|
|
2077
|
+
function getReadonlyOrchestrator() {
|
|
2078
|
+
if (_readonlyOrchestrator) {
|
|
2079
|
+
return Promise.resolve(_readonlyOrchestrator);
|
|
2080
|
+
}
|
|
2081
|
+
if (!_readonlyOrchestratorPromise) {
|
|
2082
|
+
_readonlyOrchestratorPromise = Orchestrator.create({ quiet: true, readonly: true }).then(
|
|
2083
|
+
(orch) => {
|
|
2084
|
+
_readonlyOrchestrator = orch;
|
|
2085
|
+
return orch;
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
return _readonlyOrchestratorPromise;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2031
2092
|
/**
|
|
2032
2093
|
* @typedef {Object} TaskLogMessage
|
|
2033
2094
|
* @property {string} topic
|
|
@@ -2323,7 +2384,9 @@ Shell completion:
|
|
|
2323
2384
|
// Run command - CLUSTER with auto-detection
|
|
2324
2385
|
program
|
|
2325
2386
|
.command('run <input>')
|
|
2326
|
-
.description(
|
|
2387
|
+
.description(
|
|
2388
|
+
'Start a multi-agent cluster (GitHub issue, markdown file, plain text, or "-" for stdin)'
|
|
2389
|
+
)
|
|
2327
2390
|
.option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
|
|
2328
2391
|
.option('--docker', 'Run cluster inside Docker container (full isolation)')
|
|
2329
2392
|
.option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
|
|
@@ -2337,7 +2400,7 @@ program
|
|
|
2337
2400
|
)
|
|
2338
2401
|
.option(
|
|
2339
2402
|
'--pr',
|
|
2340
|
-
'Create PR for human review (uses worktree isolation by default, use --docker for Docker)'
|
|
2403
|
+
'Create PR for human review (uses worktree isolation by default, use --docker for Docker). Never auto-merges itself; a repo-side branch-protection auto-merge rule or merge queue may still merge the PR independently of zeroshot.'
|
|
2341
2404
|
)
|
|
2342
2405
|
.option(
|
|
2343
2406
|
'--ship',
|
|
@@ -2364,6 +2427,7 @@ program
|
|
|
2364
2427
|
.option('-L, --gitlab', 'Force GitLab as issue source')
|
|
2365
2428
|
.option('-J, --jira', 'Force Jira as issue source')
|
|
2366
2429
|
.option('-D, --devops', 'Force Azure DevOps as issue source')
|
|
2430
|
+
.option('-N, --linear', 'Force Linear as issue source')
|
|
2367
2431
|
.option('-d, --detach', 'Run in background (default: attach to first agent)')
|
|
2368
2432
|
.option('--mount <spec...>', 'Add Docker mount (host:container[:ro]). Repeatable.')
|
|
2369
2433
|
.option('--no-mounts', 'Disable all Docker credential mounts')
|
|
@@ -2391,11 +2455,20 @@ Input formats:
|
|
|
2391
2455
|
Azure DevOps:
|
|
2392
2456
|
https://dev.azure.com/org/proj/_workitems/edit/123
|
|
2393
2457
|
|
|
2458
|
+
Linear:
|
|
2459
|
+
ENG-42 Issue key
|
|
2460
|
+
https://linear.app/<workspace>/issue/ENG-42/...
|
|
2461
|
+
|
|
2394
2462
|
File/Text:
|
|
2395
2463
|
feature.md Markdown file
|
|
2396
2464
|
"Implement feature X" Plain text task
|
|
2397
2465
|
|
|
2398
|
-
|
|
2466
|
+
Stdin:
|
|
2467
|
+
- Read task body from stdin (pipe/redirect)
|
|
2468
|
+
pbpaste | zeroshot run -
|
|
2469
|
+
zeroshot run - < issue.md
|
|
2470
|
+
|
|
2471
|
+
Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Linear)
|
|
2399
2472
|
`
|
|
2400
2473
|
)
|
|
2401
2474
|
.action(async (inputArg, options) => {
|
|
@@ -2409,14 +2482,41 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2409
2482
|
else if (options.gitlab) forceProvider = 'gitlab';
|
|
2410
2483
|
else if (options.jira) forceProvider = 'jira';
|
|
2411
2484
|
else if (options.devops) forceProvider = 'azure-devops';
|
|
2485
|
+
else if (options.linear) forceProvider = 'linear';
|
|
2486
|
+
|
|
2487
|
+
// Stdin input ('-'): read task body from stdin to avoid shell-quoting breakage
|
|
2488
|
+
let stdinText;
|
|
2489
|
+
if (isStdinInput(inputArg)) {
|
|
2490
|
+
if (process.env.ZEROSHOT_DAEMON === '1') {
|
|
2491
|
+
const encoded = process.env.ZEROSHOT_STDIN_TASK;
|
|
2492
|
+
if (!encoded) {
|
|
2493
|
+
throw new Error('zeroshot run -: daemon mode missing forwarded stdin content');
|
|
2494
|
+
}
|
|
2495
|
+
stdinText = decodeStdinEnv(encoded);
|
|
2496
|
+
} else if (process.stdin.isTTY) {
|
|
2497
|
+
throw new Error(
|
|
2498
|
+
'zeroshot run -: no piped input detected. Pipe or redirect a task body, e.g.\n' +
|
|
2499
|
+
' pbpaste | zeroshot run -\n' +
|
|
2500
|
+
' zeroshot run - < issue.md'
|
|
2501
|
+
);
|
|
2502
|
+
} else {
|
|
2503
|
+
stdinText = await readStdinText();
|
|
2504
|
+
}
|
|
2505
|
+
if (!stdinText) {
|
|
2506
|
+
throw new Error('zeroshot run -: stdin was empty, no task content received');
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2412
2509
|
|
|
2413
2510
|
// Auto-detect input type
|
|
2414
|
-
const input = detectRunInput(inputArg);
|
|
2415
2511
|
const settings = loadSettings();
|
|
2512
|
+
const input =
|
|
2513
|
+
stdinText !== undefined
|
|
2514
|
+
? buildTextInput(stdinText)
|
|
2515
|
+
: detectRunInput(inputArg, settings, forceProvider);
|
|
2416
2516
|
const providerOverride = resolveProviderOverride(options);
|
|
2417
2517
|
|
|
2418
2518
|
// Preflight checks
|
|
2419
|
-
runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
|
|
2519
|
+
await runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
|
|
2420
2520
|
|
|
2421
2521
|
// Secondary preflight: token-free template simulation/validation
|
|
2422
2522
|
const simMode = String(options.sim || 'fast').toLowerCase();
|
|
@@ -2446,7 +2546,7 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2446
2546
|
|
|
2447
2547
|
if (shouldRunDetached(options)) {
|
|
2448
2548
|
const clusterId = generateName('cluster');
|
|
2449
|
-
await spawnDetachedCluster(options, clusterId);
|
|
2549
|
+
await spawnDetachedCluster(options, clusterId, stdinText);
|
|
2450
2550
|
return;
|
|
2451
2551
|
}
|
|
2452
2552
|
|
|
@@ -2511,11 +2611,29 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2511
2611
|
}
|
|
2512
2612
|
|
|
2513
2613
|
if (!process.env.ZEROSHOT_DAEMON) {
|
|
2514
|
-
await streamClusterInForeground(cluster, orchestrator, clusterId);
|
|
2614
|
+
await streamClusterInForeground(cluster, orchestrator, clusterId, options);
|
|
2615
|
+
orchestrator.close();
|
|
2515
2616
|
}
|
|
2516
2617
|
|
|
2517
2618
|
setupDaemonCleanup(orchestrator, clusterId);
|
|
2518
2619
|
} catch (error) {
|
|
2620
|
+
if (error.code === 'DUPLICATE_CLUSTER') {
|
|
2621
|
+
// Benign guard rejection, not a crash: nothing was allocated, so there is
|
|
2622
|
+
// nothing to roll back. No stack trace, no "Error:" framing.
|
|
2623
|
+
if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
|
|
2624
|
+
try {
|
|
2625
|
+
await removeDetachedSetupCluster({
|
|
2626
|
+
clusterId: process.env.ZEROSHOT_CLUSTER_ID,
|
|
2627
|
+
storageDir: path.join(os.homedir(), '.zeroshot'),
|
|
2628
|
+
});
|
|
2629
|
+
} catch (removeError) {
|
|
2630
|
+
console.error('Failed to remove provisional setup cluster:', removeError.message);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
console.log(chalk.yellow(error.message));
|
|
2634
|
+
process.exit(1);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2519
2637
|
if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
|
|
2520
2638
|
try {
|
|
2521
2639
|
await markDetachedSetupFailed({
|
|
@@ -2585,7 +2703,7 @@ taskCmd
|
|
|
2585
2703
|
const providerOverride = normalizeProviderName(
|
|
2586
2704
|
options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
|
|
2587
2705
|
);
|
|
2588
|
-
requirePreflight({
|
|
2706
|
+
await requirePreflight({
|
|
2589
2707
|
requireGh: false, // gh not needed for plain tasks
|
|
2590
2708
|
requireDocker: false, // Docker not needed for plain tasks
|
|
2591
2709
|
quiet: false,
|
|
@@ -2629,7 +2747,7 @@ program
|
|
|
2629
2747
|
.option('--json', 'Output as JSON')
|
|
2630
2748
|
.action(async (options) => {
|
|
2631
2749
|
try {
|
|
2632
|
-
const orchestrator = await
|
|
2750
|
+
const orchestrator = await getReadonlyOrchestrator();
|
|
2633
2751
|
const clusters = filterClustersByStatus(orchestrator.listClusters(), options.status);
|
|
2634
2752
|
const enrichedClusters = enrichClustersWithTokens(clusters, orchestrator);
|
|
2635
2753
|
|
|
@@ -2667,7 +2785,7 @@ program
|
|
|
2667
2785
|
}
|
|
2668
2786
|
|
|
2669
2787
|
if (type === 'cluster') {
|
|
2670
|
-
const orchestrator = await
|
|
2788
|
+
const orchestrator = await getReadonlyOrchestrator();
|
|
2671
2789
|
const status = orchestrator.getStatus(id);
|
|
2672
2790
|
const tokensByRole = getClusterTokensByRole(orchestrator, id);
|
|
2673
2791
|
if (options.json) {
|
|
@@ -2725,7 +2843,7 @@ program
|
|
|
2725
2843
|
}
|
|
2726
2844
|
|
|
2727
2845
|
const limit = parseLogLimit(options);
|
|
2728
|
-
const quietOrchestrator = await Orchestrator.create({ quiet: true });
|
|
2846
|
+
const quietOrchestrator = await Orchestrator.create({ quiet: true, readonly: true });
|
|
2729
2847
|
|
|
2730
2848
|
if (!id) {
|
|
2731
2849
|
showAllClusterLogs(quietOrchestrator, options, limit);
|
|
@@ -2949,9 +3067,9 @@ program
|
|
|
2949
3067
|
program
|
|
2950
3068
|
.command('export <cluster-id>')
|
|
2951
3069
|
.description('Export cluster conversation')
|
|
2952
|
-
.option('-f, --format <format>', 'Export format: json, markdown,
|
|
2953
|
-
.option('-o, --output <file>', 'Output file (auto-generated for
|
|
2954
|
-
.action(
|
|
3070
|
+
.option('-f, --format <format>', 'Export format: json, markdown, html', 'html')
|
|
3071
|
+
.option('-o, --output <file>', 'Output file (auto-generated for html)')
|
|
3072
|
+
.action((clusterId, options) => {
|
|
2955
3073
|
try {
|
|
2956
3074
|
// Get messages from DB
|
|
2957
3075
|
const Ledger = require('../src/ledger');
|
|
@@ -2993,10 +3111,12 @@ program
|
|
|
2993
3111
|
return;
|
|
2994
3112
|
}
|
|
2995
3113
|
|
|
2996
|
-
//
|
|
2997
|
-
|
|
3114
|
+
// HTML export - convert ANSI to a self-contained, print-ready HTML file.
|
|
3115
|
+
// Open it in any browser and use Print -> Save as PDF for a PDF copy.
|
|
3116
|
+
// `pdf` is accepted as a migration alias for the previous puppeteer-based export.
|
|
3117
|
+
const isLegacyPdfFormat = options.format === 'pdf';
|
|
3118
|
+
const outputFile = options.output || `${clusterId}.html`;
|
|
2998
3119
|
const AnsiToHtml = require('ansi-to-html');
|
|
2999
|
-
const { default: puppeteer } = await import('puppeteer');
|
|
3000
3120
|
|
|
3001
3121
|
const ansiConverter = new AnsiToHtml({
|
|
3002
3122
|
fg: '#d4d4d4',
|
|
@@ -3042,27 +3162,13 @@ program
|
|
|
3042
3162
|
</body>
|
|
3043
3163
|
</html>`;
|
|
3044
3164
|
|
|
3045
|
-
|
|
3046
|
-
try {
|
|
3047
|
-
const page = await browser.newPage();
|
|
3048
|
-
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
|
3049
|
-
const pdf = await page.pdf({
|
|
3050
|
-
format: 'A4',
|
|
3051
|
-
landscape: true,
|
|
3052
|
-
margin: {
|
|
3053
|
-
top: '10mm',
|
|
3054
|
-
right: '10mm',
|
|
3055
|
-
bottom: '10mm',
|
|
3056
|
-
left: '10mm',
|
|
3057
|
-
},
|
|
3058
|
-
printBackground: true,
|
|
3059
|
-
});
|
|
3060
|
-
|
|
3061
|
-
require('fs').writeFileSync(outputFile, pdf);
|
|
3062
|
-
} finally {
|
|
3063
|
-
await browser.close();
|
|
3064
|
-
}
|
|
3165
|
+
require('fs').writeFileSync(outputFile, fullHtml, 'utf8');
|
|
3065
3166
|
console.log(`Exported to ${outputFile}`);
|
|
3167
|
+
if (isLegacyPdfFormat) {
|
|
3168
|
+
console.log(
|
|
3169
|
+
'Note: PDF export now produces print-ready HTML. Open it in a browser and use Print -> Save as PDF.'
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3066
3172
|
} catch (error) {
|
|
3067
3173
|
console.error('Error exporting cluster:', error.message);
|
|
3068
3174
|
process.exit(1);
|
|
@@ -3096,7 +3202,7 @@ program
|
|
|
3096
3202
|
cluster.config?.defaultProvider ||
|
|
3097
3203
|
settings.defaultProvider;
|
|
3098
3204
|
|
|
3099
|
-
requirePreflight({
|
|
3205
|
+
await requirePreflight({
|
|
3100
3206
|
requireGh: false, // Resume doesn't fetch new issues
|
|
3101
3207
|
requireDocker: requiresDocker,
|
|
3102
3208
|
quiet: false,
|
|
@@ -3228,7 +3334,7 @@ program
|
|
|
3228
3334
|
// If task store is unavailable, fall back to default provider
|
|
3229
3335
|
}
|
|
3230
3336
|
|
|
3231
|
-
requirePreflight({
|
|
3337
|
+
await requirePreflight({
|
|
3232
3338
|
requireGh: false,
|
|
3233
3339
|
requireDocker: false,
|
|
3234
3340
|
quiet: false,
|
|
@@ -3334,6 +3440,23 @@ async function runGc(dryRun) {
|
|
|
3334
3440
|
}
|
|
3335
3441
|
}
|
|
3336
3442
|
|
|
3443
|
+
async function detectAndReportCorruptedClusters(dryRun) {
|
|
3444
|
+
if (dryRun) {
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
try {
|
|
3448
|
+
const orchestrator = await getOrchestrator();
|
|
3449
|
+
const corrupted = await orchestrator.detectCorruptedClusters();
|
|
3450
|
+
if (corrupted.length > 0) {
|
|
3451
|
+
console.log(chalk.yellow(`\nFound ${corrupted.length} corrupted cluster(s):`));
|
|
3452
|
+
corrupted.forEach((id) => console.log(chalk.yellow(` ${id}`)));
|
|
3453
|
+
console.log(chalk.dim(`Run 'zeroshot kill <id>' to remove them.`));
|
|
3454
|
+
}
|
|
3455
|
+
} catch (error) {
|
|
3456
|
+
console.error(`Error detecting corrupted clusters: ${error.message}`);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3337
3460
|
program
|
|
3338
3461
|
.command('gc')
|
|
3339
3462
|
.description('Clean up orphaned worktree directories and stale database files')
|
|
@@ -3341,6 +3464,7 @@ program
|
|
|
3341
3464
|
.action(async (options) => {
|
|
3342
3465
|
try {
|
|
3343
3466
|
printGcResult(await runGc(options.dryRun), options.dryRun);
|
|
3467
|
+
await detectAndReportCorruptedClusters(options.dryRun);
|
|
3344
3468
|
} catch (error) {
|
|
3345
3469
|
console.error('Error during garbage collection:', error.message);
|
|
3346
3470
|
process.exit(1);
|
|
@@ -3514,9 +3638,11 @@ function printNonDockerSettings(settings) {
|
|
|
3514
3638
|
if (dockerKeys.has(key)) {
|
|
3515
3639
|
continue;
|
|
3516
3640
|
}
|
|
3641
|
+
const displayValue =
|
|
3642
|
+
key === 'linearApiKey' && value ? `${String(value).slice(0, 7)}***` : value;
|
|
3517
3643
|
const isDefault = JSON.stringify(DEFAULT_SETTINGS[key]) === JSON.stringify(value);
|
|
3518
3644
|
const label = isDefault ? chalk.dim(key) : chalk.cyan(key);
|
|
3519
|
-
const val = isDefault ? chalk.dim(String(
|
|
3645
|
+
const val = isDefault ? chalk.dim(String(displayValue)) : chalk.white(String(displayValue));
|
|
3520
3646
|
console.log(` ${label.padEnd(30)} ${val}`);
|
|
3521
3647
|
}
|
|
3522
3648
|
}
|
|
@@ -4788,7 +4914,9 @@ function handleIssueOpenedRender({ msg, prefix, timestamp, lines }) {
|
|
|
4788
4914
|
}
|
|
4789
4915
|
|
|
4790
4916
|
function handleImplementationReadyRender({ prefix, timestamp, lines }) {
|
|
4791
|
-
lines.push(
|
|
4917
|
+
lines.push(
|
|
4918
|
+
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow(`✅ ${EVENT_COPY.IMPLEMENTATION_READY.toUpperCase()}`)}`
|
|
4919
|
+
);
|
|
4792
4920
|
}
|
|
4793
4921
|
|
|
4794
4922
|
function normalizeIssueList(rawIssues) {
|
|
@@ -4857,12 +4985,17 @@ function handleValidationResultRender({ msg, prefix, timestamp, lines }) {
|
|
|
4857
4985
|
function handlePrCreatedRender({ msg, prefix, timestamp, lines }) {
|
|
4858
4986
|
lines.push('');
|
|
4859
4987
|
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4860
|
-
lines.push(
|
|
4988
|
+
lines.push(
|
|
4989
|
+
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green(`🔗 ${EVENT_COPY.PR_CREATED.toUpperCase()}`)}`
|
|
4990
|
+
);
|
|
4861
4991
|
if (msg.content?.data?.pr_url) {
|
|
4862
4992
|
lines.push(`${prefix} ${chalk.cyan(msg.content.data.pr_url)}`);
|
|
4863
4993
|
}
|
|
4864
|
-
|
|
4865
|
-
|
|
4994
|
+
const mergeStatus = formatMergeStatus(msg.content?.data?.merged);
|
|
4995
|
+
if (mergeStatus) {
|
|
4996
|
+
lines.push(
|
|
4997
|
+
`${prefix} ${chalk.gray('Merge:')} ${mergeStatus === 'merged' ? chalk.bold.cyan(mergeStatus) : chalk.yellow(mergeStatus)}`
|
|
4998
|
+
);
|
|
4866
4999
|
}
|
|
4867
5000
|
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4868
5001
|
}
|
|
@@ -4945,7 +5078,15 @@ function handleAgentOutputRender({ msg, prefix, lines, buffers, toolCalls }) {
|
|
|
4945
5078
|
}
|
|
4946
5079
|
if (event.type === 'tool_result') {
|
|
4947
5080
|
appendAgentToolResultEvent(lines, msg.sender, prefix, toolCalls, event);
|
|
5081
|
+
continue;
|
|
4948
5082
|
}
|
|
5083
|
+
if (event.type === 'result') {
|
|
5084
|
+
appendAgentResultEvent(lines, prefix, event);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
if (events.length === 0) {
|
|
5089
|
+
appendRawAgentOutputLines(lines, prefix, content);
|
|
4949
5090
|
}
|
|
4950
5091
|
}
|
|
4951
5092
|
|
|
@@ -4960,6 +5101,63 @@ const RENDER_TOPIC_HANDLERS = {
|
|
|
4960
5101
|
AGENT_OUTPUT: handleAgentOutputRender,
|
|
4961
5102
|
};
|
|
4962
5103
|
|
|
5104
|
+
const HISTORY_NOISE_TOPICS = new Set(['STATE_SNAPSHOT', 'TOKEN_USAGE']);
|
|
5105
|
+
|
|
5106
|
+
function appendAgentResultEvent(lines, prefix, event) {
|
|
5107
|
+
if (!event.success) {
|
|
5108
|
+
lines.push(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
const usage = [];
|
|
5113
|
+
if (Number.isFinite(event.inputTokens)) usage.push(`${event.inputTokens} in`);
|
|
5114
|
+
if (Number.isFinite(event.outputTokens)) usage.push(`${event.outputTokens} out`);
|
|
5115
|
+
|
|
5116
|
+
const suffix = usage.length > 0 ? ` (${usage.join(', ')})` : '';
|
|
5117
|
+
lines.push(`${prefix} ${chalk.green('✓')} completed${suffix}`);
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
function appendRawAgentOutputLines(lines, prefix, content) {
|
|
5121
|
+
for (const line of String(content).split('\n')) {
|
|
5122
|
+
const trimmed = line.trim();
|
|
5123
|
+
if (shouldSkipAgentOutputLine(trimmed)) continue;
|
|
5124
|
+
lines.push(`${prefix} ${line}`);
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
function agentOutputHasPrintableHistory(msg) {
|
|
5129
|
+
const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
|
|
5130
|
+
if (!content || !content.trim()) return false;
|
|
5131
|
+
|
|
5132
|
+
const provider = normalizeProviderName(
|
|
5133
|
+
msg.content?.data?.provider || msg.sender_provider || 'claude'
|
|
5134
|
+
);
|
|
5135
|
+
const events = parseProviderChunk(provider, content);
|
|
5136
|
+
if (events.length === 0) {
|
|
5137
|
+
return String(content)
|
|
5138
|
+
.split('\n')
|
|
5139
|
+
.some((line) => !shouldSkipAgentOutputLine(line.trim()));
|
|
5140
|
+
}
|
|
5141
|
+
|
|
5142
|
+
return events.some((event) =>
|
|
5143
|
+
['text', 'thinking', 'thinking_start', 'tool_call', 'tool_result', 'result'].includes(
|
|
5144
|
+
event.type
|
|
5145
|
+
)
|
|
5146
|
+
);
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
function isPrintableHistoryMessage(msg) {
|
|
5150
|
+
if (!msg?.topic || HISTORY_NOISE_TOPICS.has(msg.topic)) return false;
|
|
5151
|
+
if (msg.topic === 'AGENT_OUTPUT') return agentOutputHasPrintableHistory(msg);
|
|
5152
|
+
return true;
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
function selectRecentPrintableMessages(messages, limit) {
|
|
5156
|
+
const printableMessages = messages.filter(isPrintableHistoryMessage);
|
|
5157
|
+
if (!Number.isFinite(limit) || limit <= 0) return printableMessages;
|
|
5158
|
+
return printableMessages.slice(-limit);
|
|
5159
|
+
}
|
|
5160
|
+
|
|
4963
5161
|
/**
|
|
4964
5162
|
* Render messages to terminal-style output with ANSI colors (same as zeroshot logs)
|
|
4965
5163
|
*/
|
|
@@ -4987,6 +5185,13 @@ function renderMessagesToTerminal(clusterId, messages) {
|
|
|
4987
5185
|
return lines.join('\n');
|
|
4988
5186
|
}
|
|
4989
5187
|
|
|
5188
|
+
function renderRecentMessagesToTerminal(messages, limit) {
|
|
5189
|
+
const selectedMessages = selectRecentPrintableMessages(messages, limit);
|
|
5190
|
+
if (selectedMessages.length === 0) return '';
|
|
5191
|
+
|
|
5192
|
+
return renderMessagesToTerminal(null, selectedMessages);
|
|
5193
|
+
}
|
|
5194
|
+
|
|
4990
5195
|
// Get terminal width for word wrapping
|
|
4991
5196
|
function getTerminalWidth() {
|
|
4992
5197
|
return process.stdout.columns || 100;
|
|
@@ -5412,6 +5617,15 @@ async function main() {
|
|
|
5412
5617
|
|
|
5413
5618
|
printLegacyDistroNotice();
|
|
5414
5619
|
|
|
5620
|
+
try {
|
|
5621
|
+
const { onPath, binDir } = checkBinDirOnPath();
|
|
5622
|
+
if (!onPath && binDir) {
|
|
5623
|
+
printPathWarning(binDir);
|
|
5624
|
+
}
|
|
5625
|
+
} catch {
|
|
5626
|
+
// Never block CLI startup on a PATH check failure
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5415
5629
|
// Check for updates (non-blocking if offline)
|
|
5416
5630
|
if (!isTest) {
|
|
5417
5631
|
await checkForUpdates({ quiet: isQuiet });
|
|
@@ -5447,7 +5661,11 @@ async function main() {
|
|
|
5447
5661
|
}
|
|
5448
5662
|
|
|
5449
5663
|
// Run main
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5664
|
+
if (require.main === module) {
|
|
5665
|
+
main().catch((err) => {
|
|
5666
|
+
console.error('Fatal error:', err.message);
|
|
5667
|
+
process.exit(1);
|
|
5668
|
+
});
|
|
5669
|
+
}
|
|
5670
|
+
|
|
5671
|
+
module.exports = { renderRecentMessagesToTerminal, resolveRunMode };
|