@renseiai/agentfactory-cli 0.8.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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/src/agent.d.ts +20 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +109 -0
- package/dist/src/analyze-logs.d.ts +26 -0
- package/dist/src/analyze-logs.d.ts.map +1 -0
- package/dist/src/analyze-logs.js +152 -0
- package/dist/src/cleanup.d.ts +17 -0
- package/dist/src/cleanup.d.ts.map +1 -0
- package/dist/src/cleanup.js +111 -0
- package/dist/src/governor.d.ts +26 -0
- package/dist/src/governor.d.ts.map +1 -0
- package/dist/src/governor.js +305 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +76 -0
- package/dist/src/lib/agent-runner.d.ts +28 -0
- package/dist/src/lib/agent-runner.d.ts.map +1 -0
- package/dist/src/lib/agent-runner.js +272 -0
- package/dist/src/lib/analyze-logs-runner.d.ts +47 -0
- package/dist/src/lib/analyze-logs-runner.d.ts.map +1 -0
- package/dist/src/lib/analyze-logs-runner.js +216 -0
- package/dist/src/lib/auto-updater.d.ts +40 -0
- package/dist/src/lib/auto-updater.d.ts.map +1 -0
- package/dist/src/lib/auto-updater.js +109 -0
- package/dist/src/lib/cleanup-runner.d.ts +29 -0
- package/dist/src/lib/cleanup-runner.d.ts.map +1 -0
- package/dist/src/lib/cleanup-runner.js +295 -0
- package/dist/src/lib/governor-dependencies.d.ts +23 -0
- package/dist/src/lib/governor-dependencies.d.ts.map +1 -0
- package/dist/src/lib/governor-dependencies.js +361 -0
- package/dist/src/lib/governor-logger.d.ts +30 -0
- package/dist/src/lib/governor-logger.d.ts.map +1 -0
- package/dist/src/lib/governor-logger.js +210 -0
- package/dist/src/lib/governor-runner.d.ts +103 -0
- package/dist/src/lib/governor-runner.d.ts.map +1 -0
- package/dist/src/lib/governor-runner.js +210 -0
- package/dist/src/lib/linear-runner.d.ts +8 -0
- package/dist/src/lib/linear-runner.d.ts.map +1 -0
- package/dist/src/lib/linear-runner.js +7 -0
- package/dist/src/lib/orchestrator-runner.d.ts +51 -0
- package/dist/src/lib/orchestrator-runner.d.ts.map +1 -0
- package/dist/src/lib/orchestrator-runner.js +151 -0
- package/dist/src/lib/queue-admin-runner.d.ts +30 -0
- package/dist/src/lib/queue-admin-runner.d.ts.map +1 -0
- package/dist/src/lib/queue-admin-runner.js +378 -0
- package/dist/src/lib/sync-routes-runner.d.ts +28 -0
- package/dist/src/lib/sync-routes-runner.d.ts.map +1 -0
- package/dist/src/lib/sync-routes-runner.js +110 -0
- package/dist/src/lib/version.d.ts +35 -0
- package/dist/src/lib/version.d.ts.map +1 -0
- package/dist/src/lib/version.js +168 -0
- package/dist/src/lib/worker-fleet-runner.d.ts +32 -0
- package/dist/src/lib/worker-fleet-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-fleet-runner.js +256 -0
- package/dist/src/lib/worker-runner.d.ts +33 -0
- package/dist/src/lib/worker-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-runner.js +781 -0
- package/dist/src/linear.d.ts +37 -0
- package/dist/src/linear.d.ts.map +1 -0
- package/dist/src/linear.js +118 -0
- package/dist/src/orchestrator.d.ts +21 -0
- package/dist/src/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator.js +190 -0
- package/dist/src/queue-admin.d.ts +25 -0
- package/dist/src/queue-admin.d.ts.map +1 -0
- package/dist/src/queue-admin.js +96 -0
- package/dist/src/sync-routes.d.ts +17 -0
- package/dist/src/sync-routes.d.ts.map +1 -0
- package/dist/src/sync-routes.js +100 -0
- package/dist/src/worker-fleet.d.ts +25 -0
- package/dist/src/worker-fleet.d.ts.map +1 -0
- package/dist/src/worker-fleet.js +140 -0
- package/dist/src/worker.d.ts +26 -0
- package/dist/src/worker.d.ts.map +1 -0
- package/dist/src/worker.js +135 -0
- package/package.json +175 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared version utilities for AgentFactory CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides current version detection and npm update checking with
|
|
5
|
+
* file-based caching to avoid excessive network requests.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Current version
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const PACKAGE_NAME = '@renseiai/agentfactory-cli';
|
|
15
|
+
/**
|
|
16
|
+
* Read the current package version from the CLI's package.json.
|
|
17
|
+
*/
|
|
18
|
+
export function getVersion() {
|
|
19
|
+
try {
|
|
20
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
// Walk up from the current file until we find the CLI package.json
|
|
22
|
+
for (let i = 0; i < 5; i++) {
|
|
23
|
+
const candidate = path.join(dir, 'package.json');
|
|
24
|
+
if (existsSync(candidate)) {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
26
|
+
if (pkg.name === PACKAGE_NAME) {
|
|
27
|
+
return pkg.version ?? 'unknown';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
dir = path.dirname(dir);
|
|
31
|
+
}
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Cache management
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
const CACHE_DIR = path.join(os.tmpdir(), 'agentfactory');
|
|
42
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
|
|
43
|
+
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
44
|
+
function readCache() {
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(CACHE_FILE))
|
|
47
|
+
return null;
|
|
48
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
49
|
+
if (typeof data.latestVersion !== 'string' || typeof data.checkedAt !== 'number')
|
|
50
|
+
return null;
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeCache(entry) {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(CACHE_DIR)) {
|
|
60
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(CACHE_FILE, JSON.stringify(entry), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Non-critical — silently ignore cache write failures
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// npm registry fetch
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
async function fetchLatestVersion() {
|
|
72
|
+
try {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
75
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal: controller.signal });
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
return null;
|
|
79
|
+
const data = (await response.json());
|
|
80
|
+
return data.version ?? null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Semver comparison (major.minor.patch only)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
function parseVersion(v) {
|
|
90
|
+
const match = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
91
|
+
if (!match)
|
|
92
|
+
return null;
|
|
93
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
94
|
+
}
|
|
95
|
+
function isNewer(latest, current) {
|
|
96
|
+
const l = parseVersion(latest);
|
|
97
|
+
const c = parseVersion(current);
|
|
98
|
+
if (!l || !c)
|
|
99
|
+
return false;
|
|
100
|
+
if (l[0] !== c[0])
|
|
101
|
+
return l[0] > c[0];
|
|
102
|
+
if (l[1] !== c[1])
|
|
103
|
+
return l[1] > c[1];
|
|
104
|
+
return l[2] > c[2];
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Public API
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Check whether a newer version is available on npm.
|
|
111
|
+
*
|
|
112
|
+
* Uses a file-based cache (4-hour TTL) to avoid hitting the registry
|
|
113
|
+
* on every CLI invocation. Returns null if the check is skipped or fails.
|
|
114
|
+
*
|
|
115
|
+
* Disabled when:
|
|
116
|
+
* - `AF_NO_UPDATE_CHECK=1` env var is set
|
|
117
|
+
* - `--no-update-check` was passed
|
|
118
|
+
* - Current version is 'unknown'
|
|
119
|
+
*/
|
|
120
|
+
export async function checkForUpdate(opts) {
|
|
121
|
+
if (opts?.noUpdateCheck)
|
|
122
|
+
return null;
|
|
123
|
+
if (process.env.AF_NO_UPDATE_CHECK === '1' || process.env.AF_NO_UPDATE_CHECK === 'true')
|
|
124
|
+
return null;
|
|
125
|
+
const currentVersion = getVersion();
|
|
126
|
+
if (currentVersion === 'unknown')
|
|
127
|
+
return null;
|
|
128
|
+
// Check cache first
|
|
129
|
+
const cached = readCache();
|
|
130
|
+
if (cached && Date.now() - cached.checkedAt < CHECK_INTERVAL_MS) {
|
|
131
|
+
return {
|
|
132
|
+
currentVersion,
|
|
133
|
+
latestVersion: cached.latestVersion,
|
|
134
|
+
updateAvailable: isNewer(cached.latestVersion, currentVersion),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Fetch from npm (non-blocking — don't slow down startup)
|
|
138
|
+
const latestVersion = await fetchLatestVersion();
|
|
139
|
+
if (!latestVersion)
|
|
140
|
+
return null;
|
|
141
|
+
writeCache({ latestVersion, checkedAt: Date.now() });
|
|
142
|
+
return {
|
|
143
|
+
currentVersion,
|
|
144
|
+
latestVersion,
|
|
145
|
+
updateAvailable: isNewer(latestVersion, currentVersion),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Display helpers
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
const c = {
|
|
152
|
+
reset: '\x1b[0m',
|
|
153
|
+
bold: '\x1b[1m',
|
|
154
|
+
dim: '\x1b[2m',
|
|
155
|
+
yellow: '\x1b[33m',
|
|
156
|
+
cyan: '\x1b[36m',
|
|
157
|
+
green: '\x1b[32m',
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Print an update notification to stderr if a newer version is available.
|
|
161
|
+
* Designed to be non-intrusive — just a single line after the startup banner.
|
|
162
|
+
*/
|
|
163
|
+
export function printUpdateNotification(result) {
|
|
164
|
+
if (!result?.updateAvailable)
|
|
165
|
+
return;
|
|
166
|
+
console.log(`\n${c.yellow}${c.bold}Update available:${c.reset} ${c.dim}v${result.currentVersion}${c.reset} → ${c.green}v${result.latestVersion}${c.reset}` +
|
|
167
|
+
` ${c.dim}Run${c.reset} ${c.cyan}npm i -g @renseiai/agentfactory-cli@latest${c.reset} ${c.dim}to update${c.reset}\n`);
|
|
168
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Fleet Runner — Programmatic API for the worker fleet manager CLI.
|
|
3
|
+
*
|
|
4
|
+
* Spawns and manages multiple worker processes for parallel agent execution.
|
|
5
|
+
* Each worker runs as a separate OS process with its own resources.
|
|
6
|
+
*/
|
|
7
|
+
export interface FleetRunnerConfig {
|
|
8
|
+
/** Number of worker processes (default: CPU cores / 2) */
|
|
9
|
+
workers?: number;
|
|
10
|
+
/** Agents per worker (default: 3) */
|
|
11
|
+
capacity?: number;
|
|
12
|
+
/** Show configuration without starting workers (default: false) */
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
/** Coordinator API URL (required) */
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
/** API key for authentication (required) */
|
|
17
|
+
apiKey: string;
|
|
18
|
+
/** Path to the worker script/binary (default: auto-detect from this package) */
|
|
19
|
+
workerScript?: string;
|
|
20
|
+
/** Linear project names for workers to accept (undefined = all) */
|
|
21
|
+
projects?: string[];
|
|
22
|
+
/** Enable auto-update (CLI flag override) */
|
|
23
|
+
autoUpdate?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run a fleet of worker processes.
|
|
27
|
+
*
|
|
28
|
+
* The caller can cancel via the optional {@link AbortSignal}. The function
|
|
29
|
+
* returns once all workers have been stopped.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runWorkerFleet(config: FleetRunnerConfig, signal?: AbortSignal): Promise<void>;
|
|
32
|
+
//# sourceMappingURL=worker-fleet-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-fleet-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/worker-fleet-runner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AA6UD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Fleet Runner — Programmatic API for the worker fleet manager CLI.
|
|
3
|
+
*
|
|
4
|
+
* Spawns and manages multiple worker processes for parallel agent execution.
|
|
5
|
+
* Each worker runs as a separate OS process with its own resources.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { getVersion, checkForUpdate, printUpdateNotification } from './version.js';
|
|
13
|
+
import { maybeAutoUpdate, isAutoUpdateEnabled } from './auto-updater.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// ANSI colors
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
const colors = {
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
green: '\x1b[32m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
blue: '\x1b[34m',
|
|
23
|
+
magenta: '\x1b[35m',
|
|
24
|
+
cyan: '\x1b[36m',
|
|
25
|
+
gray: '\x1b[90m',
|
|
26
|
+
};
|
|
27
|
+
const workerColors = [
|
|
28
|
+
colors.cyan,
|
|
29
|
+
colors.magenta,
|
|
30
|
+
colors.yellow,
|
|
31
|
+
colors.green,
|
|
32
|
+
colors.blue,
|
|
33
|
+
];
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
function timestamp() {
|
|
38
|
+
return new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
39
|
+
}
|
|
40
|
+
function fleetLog(workerId, color, level, message) {
|
|
41
|
+
const prefix = workerId !== null
|
|
42
|
+
? `[W${workerId.toString().padStart(2, '0')}]`
|
|
43
|
+
: '[FLEET]';
|
|
44
|
+
const levelColor = level === 'ERR' ? colors.red : level === 'WRN' ? colors.yellow : colors.gray;
|
|
45
|
+
console.log(`${colors.gray}${timestamp()}${colors.reset} ${color}${prefix}${colors.reset} ${levelColor}${level}${colors.reset} ${message}`);
|
|
46
|
+
}
|
|
47
|
+
function getDefaultWorkerScript() {
|
|
48
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
49
|
+
const __dirname = path.dirname(__filename);
|
|
50
|
+
// Runner lives in lib/, worker entry is one level up.
|
|
51
|
+
// When running from compiled dist/ the .js file exists; when running from
|
|
52
|
+
// source via tsx only the .ts file exists.
|
|
53
|
+
const jsPath = path.resolve(__dirname, '..', 'worker.js');
|
|
54
|
+
if (fs.existsSync(jsPath))
|
|
55
|
+
return jsPath;
|
|
56
|
+
return path.resolve(__dirname, '..', 'worker.ts');
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// WorkerFleet class (internal)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
class WorkerFleet {
|
|
62
|
+
workers = new Map();
|
|
63
|
+
fleetConfig;
|
|
64
|
+
shuttingDown = false;
|
|
65
|
+
workerScript;
|
|
66
|
+
autoUpdateFlag;
|
|
67
|
+
resolveRunning = null;
|
|
68
|
+
updateInterval = null;
|
|
69
|
+
constructor(fleetConfig, workerScript, autoUpdateFlag) {
|
|
70
|
+
this.fleetConfig = fleetConfig;
|
|
71
|
+
this.workerScript = workerScript;
|
|
72
|
+
this.autoUpdateFlag = autoUpdateFlag;
|
|
73
|
+
}
|
|
74
|
+
async start(signal) {
|
|
75
|
+
const { workers, capacity, dryRun } = this.fleetConfig;
|
|
76
|
+
const totalCapacity = workers * capacity;
|
|
77
|
+
const version = getVersion();
|
|
78
|
+
console.log(`
|
|
79
|
+
${colors.cyan}================================================================${colors.reset}
|
|
80
|
+
${colors.cyan} AgentFactory Worker Fleet Manager${colors.reset} ${colors.gray}v${version}${colors.reset}
|
|
81
|
+
${colors.cyan}================================================================${colors.reset}
|
|
82
|
+
Workers: ${colors.green}${workers}${colors.reset}
|
|
83
|
+
Capacity/Worker: ${colors.green}${capacity}${colors.reset}
|
|
84
|
+
Total Capacity: ${colors.green}${totalCapacity}${colors.reset} concurrent agents
|
|
85
|
+
Projects: ${colors.green}${this.fleetConfig.projects?.length ? this.fleetConfig.projects.join(', ') : 'all'}${colors.reset}
|
|
86
|
+
Auto-update: ${isAutoUpdateEnabled(this.autoUpdateFlag) ? `${colors.green}enabled${colors.reset}` : `${colors.gray}disabled${colors.reset}`}
|
|
87
|
+
|
|
88
|
+
System:
|
|
89
|
+
CPU Cores: ${os.cpus().length}
|
|
90
|
+
Total RAM: ${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB
|
|
91
|
+
Free RAM: ${Math.round(os.freemem() / 1024 / 1024 / 1024)} GB
|
|
92
|
+
${colors.cyan}================================================================${colors.reset}
|
|
93
|
+
`);
|
|
94
|
+
// Update check
|
|
95
|
+
const updateCheck = await checkForUpdate();
|
|
96
|
+
printUpdateNotification(updateCheck);
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
console.log(`${colors.yellow}Dry run mode - not starting workers${colors.reset}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Wire up AbortSignal for graceful shutdown
|
|
102
|
+
const onAbort = () => this.shutdown('AbortSignal');
|
|
103
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
104
|
+
try {
|
|
105
|
+
// Spawn workers with staggered start to avoid thundering herd
|
|
106
|
+
for (let i = 0; i < workers; i++) {
|
|
107
|
+
if (signal?.aborted)
|
|
108
|
+
break;
|
|
109
|
+
await this.spawnWorker(i);
|
|
110
|
+
if (i < workers - 1) {
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (signal?.aborted)
|
|
115
|
+
return;
|
|
116
|
+
fleetLog(null, colors.green, 'INF', `All ${workers} workers started`);
|
|
117
|
+
// Periodic auto-update check (every 4 hours)
|
|
118
|
+
if (isAutoUpdateEnabled(this.autoUpdateFlag)) {
|
|
119
|
+
this.updateInterval = setInterval(async () => {
|
|
120
|
+
const check = await checkForUpdate();
|
|
121
|
+
await maybeAutoUpdate(check, {
|
|
122
|
+
cliFlag: this.autoUpdateFlag,
|
|
123
|
+
hasActiveWorkers: async () => this.workers.size > 0 && !this.shuttingDown,
|
|
124
|
+
onBeforeRestart: async () => this.shutdown('auto-update'),
|
|
125
|
+
});
|
|
126
|
+
}, 4 * 60 * 60 * 1000); // 4 hours
|
|
127
|
+
}
|
|
128
|
+
// Keep the fleet manager running until shutdown
|
|
129
|
+
await new Promise((resolve) => {
|
|
130
|
+
this.resolveRunning = resolve;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (this.updateInterval)
|
|
135
|
+
clearInterval(this.updateInterval);
|
|
136
|
+
signal?.removeEventListener('abort', onAbort);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async spawnWorker(id) {
|
|
140
|
+
const color = workerColors[id % workerColors.length];
|
|
141
|
+
const existingWorker = this.workers.get(id);
|
|
142
|
+
const restartCount = existingWorker?.restartCount ?? 0;
|
|
143
|
+
fleetLog(id, color, 'INF', `Starting worker (capacity: ${this.fleetConfig.capacity})${restartCount > 0 ? ` [restart #${restartCount}]` : ''}`);
|
|
144
|
+
const nodeArgs = [];
|
|
145
|
+
// When running a .ts worker script, register tsx so Node can load it
|
|
146
|
+
if (this.workerScript.endsWith('.ts')) {
|
|
147
|
+
nodeArgs.push('--import', 'tsx');
|
|
148
|
+
}
|
|
149
|
+
nodeArgs.push(this.workerScript, '--capacity', String(this.fleetConfig.capacity), '--api-url', this.fleetConfig.apiUrl, '--api-key', this.fleetConfig.apiKey);
|
|
150
|
+
if (this.fleetConfig.projects?.length) {
|
|
151
|
+
nodeArgs.push('--projects', this.fleetConfig.projects.join(','));
|
|
152
|
+
}
|
|
153
|
+
const workerProcess = spawn('node', nodeArgs, {
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
env: {
|
|
156
|
+
...process.env,
|
|
157
|
+
WORKER_FLEET_ID: String(id),
|
|
158
|
+
},
|
|
159
|
+
cwd: process.cwd(),
|
|
160
|
+
});
|
|
161
|
+
const workerInfo = {
|
|
162
|
+
id,
|
|
163
|
+
process: workerProcess,
|
|
164
|
+
color,
|
|
165
|
+
startedAt: new Date(),
|
|
166
|
+
restartCount,
|
|
167
|
+
};
|
|
168
|
+
this.workers.set(id, workerInfo);
|
|
169
|
+
// Handle stdout — prefix with worker ID
|
|
170
|
+
workerProcess.stdout?.on('data', (data) => {
|
|
171
|
+
const lines = data.toString().trim().split('\n');
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (line.trim()) {
|
|
174
|
+
console.log(`${color}[W${id.toString().padStart(2, '0')}]${colors.reset} ${line}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// Handle stderr
|
|
179
|
+
workerProcess.stderr?.on('data', (data) => {
|
|
180
|
+
const lines = data.toString().trim().split('\n');
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (line.trim()) {
|
|
183
|
+
console.log(`${color}[W${id.toString().padStart(2, '0')}]${colors.reset} ${colors.red}${line}${colors.reset}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Handle worker exit
|
|
188
|
+
workerProcess.on('exit', (code, sig) => {
|
|
189
|
+
if (this.shuttingDown) {
|
|
190
|
+
fleetLog(id, color, 'INF', `Worker stopped (code: ${code}, signal: ${sig})`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
fleetLog(id, color, 'WRN', `Worker exited unexpectedly (code: ${code}, signal: ${sig}) - restarting in 5s`);
|
|
194
|
+
const worker = this.workers.get(id);
|
|
195
|
+
if (worker) {
|
|
196
|
+
worker.restartCount++;
|
|
197
|
+
}
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
if (!this.shuttingDown) {
|
|
200
|
+
this.spawnWorker(id);
|
|
201
|
+
}
|
|
202
|
+
}, 5000);
|
|
203
|
+
});
|
|
204
|
+
workerProcess.on('error', (err) => {
|
|
205
|
+
fleetLog(id, color, 'ERR', `Worker error: ${err.message}`);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async shutdown(reason) {
|
|
209
|
+
if (this.shuttingDown)
|
|
210
|
+
return;
|
|
211
|
+
this.shuttingDown = true;
|
|
212
|
+
console.log(`\n${colors.yellow}Received ${reason} - shutting down fleet...${colors.reset}`);
|
|
213
|
+
for (const [id, worker] of this.workers) {
|
|
214
|
+
fleetLog(id, worker.color, 'INF', 'Stopping worker...');
|
|
215
|
+
worker.process.kill('SIGTERM');
|
|
216
|
+
}
|
|
217
|
+
// Wait for workers to exit (max 30 seconds)
|
|
218
|
+
const forceKillTimeout = setTimeout(() => {
|
|
219
|
+
console.log(`${colors.red}Timeout waiting for workers - force killing${colors.reset}`);
|
|
220
|
+
for (const worker of this.workers.values()) {
|
|
221
|
+
worker.process.kill('SIGKILL');
|
|
222
|
+
}
|
|
223
|
+
}, 30000);
|
|
224
|
+
await Promise.all(Array.from(this.workers.values()).map((worker) => new Promise((resolve) => {
|
|
225
|
+
worker.process.on('exit', () => resolve());
|
|
226
|
+
})));
|
|
227
|
+
clearTimeout(forceKillTimeout);
|
|
228
|
+
console.log(`${colors.green}All workers stopped${colors.reset}`);
|
|
229
|
+
// Resolve the running promise so start() returns
|
|
230
|
+
this.resolveRunning?.();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Runner
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
/**
|
|
237
|
+
* Run a fleet of worker processes.
|
|
238
|
+
*
|
|
239
|
+
* The caller can cancel via the optional {@link AbortSignal}. The function
|
|
240
|
+
* returns once all workers have been stopped.
|
|
241
|
+
*/
|
|
242
|
+
export async function runWorkerFleet(config, signal) {
|
|
243
|
+
const workers = config.workers ?? Math.max(1, Math.floor(os.cpus().length / 2));
|
|
244
|
+
const capacity = config.capacity ?? 3;
|
|
245
|
+
const dryRun = config.dryRun ?? false;
|
|
246
|
+
const workerScript = config.workerScript ?? getDefaultWorkerScript();
|
|
247
|
+
const fleet = new WorkerFleet({
|
|
248
|
+
workers,
|
|
249
|
+
capacity,
|
|
250
|
+
dryRun,
|
|
251
|
+
apiUrl: config.apiUrl,
|
|
252
|
+
apiKey: config.apiKey,
|
|
253
|
+
projects: config.projects?.length ? config.projects : undefined,
|
|
254
|
+
}, workerScript, config.autoUpdate);
|
|
255
|
+
await fleet.start(signal);
|
|
256
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Runner — Programmatic API for the remote worker CLI.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates all global state into the runner function's closure so that
|
|
5
|
+
* multiple workers can be started from the same process (e.g. tests) without
|
|
6
|
+
* leaking state between invocations.
|
|
7
|
+
*/
|
|
8
|
+
export interface WorkerRunnerConfig {
|
|
9
|
+
/** Coordinator API URL */
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
/** API key for authentication */
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** Worker hostname (default: os.hostname()) */
|
|
14
|
+
hostname?: string;
|
|
15
|
+
/** Maximum concurrent agents (default: 3) */
|
|
16
|
+
capacity?: number;
|
|
17
|
+
/** Poll but don't execute work (default: false) */
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
/** Linear API key for agent operations (default: process.env.LINEAR_API_KEY) */
|
|
20
|
+
linearApiKey?: string;
|
|
21
|
+
/** Git repository root (default: auto-detect) */
|
|
22
|
+
gitRoot?: string;
|
|
23
|
+
/** Linear project names to accept (undefined = all) */
|
|
24
|
+
projects?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run a worker that polls the coordinator for work and executes agents.
|
|
28
|
+
*
|
|
29
|
+
* All state is encapsulated in the function closure. The caller can cancel
|
|
30
|
+
* via the optional {@link AbortSignal}.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runWorker(config: WorkerRunnerConfig, signal?: AbortSignal): Promise<void>;
|
|
33
|
+
//# sourceMappingURL=worker-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/worker-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAwED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAm7Bf"}
|