@covibes/zeroshot 1.0.2 → 1.1.4
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/CHANGELOG.md +54 -0
- package/README.md +2 -0
- package/cli/index.js +152 -209
- package/cli/message-formatter-utils.js +75 -0
- package/cli/message-formatters-normal.js +214 -0
- package/cli/message-formatters-watch.js +181 -0
- package/cluster-templates/base-templates/debug-workflow.json +0 -1
- package/cluster-templates/base-templates/full-workflow.json +10 -6
- package/cluster-templates/base-templates/single-worker.json +0 -1
- package/cluster-templates/base-templates/worker-validator.json +0 -1
- package/docker/zeroshot-cluster/Dockerfile +6 -0
- package/lib/settings.js +1 -1
- package/package.json +4 -2
- package/src/agent/agent-task-executor.js +237 -112
- package/src/isolation-manager.js +94 -51
- package/src/orchestrator.js +45 -10
- package/src/preflight.js +383 -0
- package/src/process-metrics.js +554 -0
- package/src/status-footer.js +543 -0
- package/src/tui/formatters.js +6 -1
- package/cluster-templates/conductor-junior-bootstrap.json +0 -69
package/src/orchestrator.js
CHANGED
|
@@ -164,12 +164,19 @@ class Orchestrator {
|
|
|
164
164
|
isolationManager = new IsolationManager({ image: isolation.image });
|
|
165
165
|
// Restore the container mapping so cleanup works
|
|
166
166
|
isolationManager.containers.set(clusterId, isolation.containerId);
|
|
167
|
+
// Restore isolated dir mapping for workspace preservation during cleanup
|
|
168
|
+
if (isolation.workDir) {
|
|
169
|
+
isolationManager.isolatedDirs.set(clusterId, {
|
|
170
|
+
path: path.join(os.tmpdir(), 'zeroshot-isolated', clusterId),
|
|
171
|
+
originalDir: isolation.workDir,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
167
174
|
isolation = {
|
|
168
175
|
...isolation,
|
|
169
176
|
manager: isolationManager,
|
|
170
177
|
};
|
|
171
178
|
this._log(
|
|
172
|
-
`[Orchestrator] Restored isolation manager for ${clusterId} (container: ${isolation.containerId})`
|
|
179
|
+
`[Orchestrator] Restored isolation manager for ${clusterId} (container: ${isolation.containerId}, workDir: ${isolation.workDir || 'unknown'})`
|
|
173
180
|
);
|
|
174
181
|
}
|
|
175
182
|
|
|
@@ -289,6 +296,13 @@ class Orchestrator {
|
|
|
289
296
|
continue;
|
|
290
297
|
}
|
|
291
298
|
|
|
299
|
+
// CRITICAL: Killed clusters are DELETED from disk, not persisted
|
|
300
|
+
// This ensures they can't be accidentally resumed
|
|
301
|
+
if (cluster.state === 'killed') {
|
|
302
|
+
delete existingClusters[clusterId];
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
292
306
|
existingClusters[clusterId] = {
|
|
293
307
|
id: cluster.id,
|
|
294
308
|
config: cluster.config,
|
|
@@ -299,11 +313,13 @@ class Orchestrator {
|
|
|
299
313
|
// Persist failure info for resume capability
|
|
300
314
|
failureInfo: cluster.failureInfo || null,
|
|
301
315
|
// Persist isolation info (excluding manager instance which can't be serialized)
|
|
316
|
+
// CRITICAL: workDir is required for resume() to recreate container with same workspace
|
|
302
317
|
isolation: cluster.isolation
|
|
303
318
|
? {
|
|
304
319
|
enabled: cluster.isolation.enabled,
|
|
305
320
|
containerId: cluster.isolation.containerId,
|
|
306
321
|
image: cluster.isolation.image,
|
|
322
|
+
workDir: cluster.isolation.workDir, // Required for resume
|
|
307
323
|
}
|
|
308
324
|
: null,
|
|
309
325
|
};
|
|
@@ -489,12 +505,14 @@ class Orchestrator {
|
|
|
489
505
|
// Track PID for zombie detection (this process owns the cluster)
|
|
490
506
|
pid: process.pid,
|
|
491
507
|
// Isolation state (only if enabled)
|
|
508
|
+
// CRITICAL: Store workDir for resume capability - without this, resume() can't recreate container
|
|
492
509
|
isolation: options.isolation
|
|
493
510
|
? {
|
|
494
511
|
enabled: true,
|
|
495
512
|
containerId,
|
|
496
513
|
image: options.isolationImage || 'zeroshot-cluster-base',
|
|
497
514
|
manager: isolationManager,
|
|
515
|
+
workDir: options.cwd || process.cwd(), // Persisted for resume
|
|
498
516
|
}
|
|
499
517
|
: null,
|
|
500
518
|
};
|
|
@@ -823,10 +841,11 @@ class Orchestrator {
|
|
|
823
841
|
}
|
|
824
842
|
|
|
825
843
|
// Clean up isolation container if enabled
|
|
844
|
+
// CRITICAL: Preserve workspace for resume capability - only delete on kill()
|
|
826
845
|
if (cluster.isolation?.manager) {
|
|
827
|
-
this._log(`[Orchestrator]
|
|
828
|
-
await cluster.isolation.manager.cleanup(clusterId);
|
|
829
|
-
this._log(`[Orchestrator] Container
|
|
846
|
+
this._log(`[Orchestrator] Stopping isolation container for ${clusterId} (preserving workspace for resume)...`);
|
|
847
|
+
await cluster.isolation.manager.cleanup(clusterId, { preserveWorkspace: true });
|
|
848
|
+
this._log(`[Orchestrator] Container stopped, workspace preserved`);
|
|
830
849
|
}
|
|
831
850
|
|
|
832
851
|
cluster.state = 'stopped';
|
|
@@ -854,11 +873,11 @@ class Orchestrator {
|
|
|
854
873
|
await agent.stop();
|
|
855
874
|
}
|
|
856
875
|
|
|
857
|
-
// Force remove isolation container
|
|
876
|
+
// Force remove isolation container AND workspace (full cleanup, no resume)
|
|
858
877
|
if (cluster.isolation?.manager) {
|
|
859
|
-
this._log(`[Orchestrator] Force removing isolation container for ${clusterId}...`);
|
|
860
|
-
await cluster.isolation.manager.
|
|
861
|
-
this._log(`[Orchestrator] Container removed`);
|
|
878
|
+
this._log(`[Orchestrator] Force removing isolation container and workspace for ${clusterId}...`);
|
|
879
|
+
await cluster.isolation.manager.cleanup(clusterId, { preserveWorkspace: false });
|
|
880
|
+
this._log(`[Orchestrator] Container and workspace removed`);
|
|
862
881
|
}
|
|
863
882
|
|
|
864
883
|
// Close message bus and ledger
|
|
@@ -976,10 +995,26 @@ class Orchestrator {
|
|
|
976
995
|
if (!containerExists) {
|
|
977
996
|
this._log(`[Orchestrator] Container ${oldContainerId} not found, recreating...`);
|
|
978
997
|
|
|
979
|
-
// Create new container
|
|
998
|
+
// Create new container using saved workDir (CRITICAL for isolation mode resume)
|
|
999
|
+
// The isolated workspace at /tmp/zeroshot-isolated/{clusterId} was preserved by stop()
|
|
1000
|
+
const workDir = cluster.isolation.workDir;
|
|
1001
|
+
if (!workDir) {
|
|
1002
|
+
throw new Error(`Cannot resume cluster ${clusterId}: workDir not saved in isolation state`);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Check if isolated workspace still exists (it should, if stop() was used)
|
|
1006
|
+
const isolatedPath = path.join(os.tmpdir(), 'zeroshot-isolated', clusterId);
|
|
1007
|
+
if (!fs.existsSync(isolatedPath)) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`Cannot resume cluster ${clusterId}: isolated workspace deleted. ` +
|
|
1010
|
+
`Was the cluster killed (not stopped)? Use 'zeroshot run' to start fresh.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
980
1014
|
const newContainerId = await cluster.isolation.manager.createContainer(clusterId, {
|
|
981
|
-
workDir
|
|
1015
|
+
workDir, // Use saved workDir, NOT process.cwd()
|
|
982
1016
|
image: cluster.isolation.image,
|
|
1017
|
+
reuseExistingWorkspace: true, // CRITICAL: Don't wipe existing work
|
|
983
1018
|
});
|
|
984
1019
|
|
|
985
1020
|
this._log(`[Orchestrator] New container created: ${newContainerId}`);
|
package/src/preflight.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight Validation - Check all dependencies before starting
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - Claude CLI installed and authenticated
|
|
6
|
+
* - gh CLI installed and authenticated (if using issue numbers)
|
|
7
|
+
* - Docker available (if using --isolation)
|
|
8
|
+
*
|
|
9
|
+
* Provides CLEAR, ACTIONABLE error messages with recovery instructions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validation result
|
|
19
|
+
* @typedef {Object} ValidationResult
|
|
20
|
+
* @property {boolean} valid - Whether validation passed
|
|
21
|
+
* @property {string[]} errors - Fatal errors that block execution
|
|
22
|
+
* @property {string[]} warnings - Non-fatal warnings
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format error with recovery instructions
|
|
27
|
+
* @param {string} title - Error title
|
|
28
|
+
* @param {string} detail - Error details
|
|
29
|
+
* @param {string[]} recovery - Recovery steps
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function formatError(title, detail, recovery) {
|
|
33
|
+
let msg = `\n❌ ${title}\n`;
|
|
34
|
+
msg += ` ${detail}\n`;
|
|
35
|
+
if (recovery.length > 0) {
|
|
36
|
+
msg += `\n To fix:\n`;
|
|
37
|
+
recovery.forEach((step, i) => {
|
|
38
|
+
msg += ` ${i + 1}. ${step}\n`;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return msg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a command exists
|
|
46
|
+
* @param {string} cmd - Command to check
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function commandExists(cmd) {
|
|
50
|
+
try {
|
|
51
|
+
execSync(`which ${cmd}`, { encoding: 'utf8', stdio: 'pipe' });
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get Claude CLI version
|
|
60
|
+
* @returns {{ installed: boolean, version: string | null, error: string | null }}
|
|
61
|
+
*/
|
|
62
|
+
function getClaudeVersion() {
|
|
63
|
+
try {
|
|
64
|
+
const output = execSync('claude --version', { encoding: 'utf8', stdio: 'pipe' });
|
|
65
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
66
|
+
return {
|
|
67
|
+
installed: true,
|
|
68
|
+
version: match ? match[1] : 'unknown',
|
|
69
|
+
error: null,
|
|
70
|
+
};
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err.message.includes('command not found') || err.message.includes('not found')) {
|
|
73
|
+
return {
|
|
74
|
+
installed: false,
|
|
75
|
+
version: null,
|
|
76
|
+
error: 'Claude CLI not installed',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
installed: false,
|
|
81
|
+
version: null,
|
|
82
|
+
error: err.message,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check Claude CLI authentication status
|
|
89
|
+
* @returns {{ authenticated: boolean, error: string | null, configDir: string }}
|
|
90
|
+
*/
|
|
91
|
+
function checkClaudeAuth() {
|
|
92
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
93
|
+
const credentialsPath = path.join(configDir, '.credentials.json');
|
|
94
|
+
|
|
95
|
+
// Check if credentials file exists
|
|
96
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
97
|
+
return {
|
|
98
|
+
authenticated: false,
|
|
99
|
+
error: 'No credentials file found',
|
|
100
|
+
configDir,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if credentials file has content
|
|
105
|
+
try {
|
|
106
|
+
const content = fs.readFileSync(credentialsPath, 'utf8');
|
|
107
|
+
const creds = JSON.parse(content);
|
|
108
|
+
|
|
109
|
+
// Check for OAuth token (primary auth method)
|
|
110
|
+
if (creds.claudeAiOauth?.accessToken) {
|
|
111
|
+
// Check if token is expired
|
|
112
|
+
const expiresAt = creds.claudeAiOauth.expiresAt;
|
|
113
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
114
|
+
return {
|
|
115
|
+
authenticated: false,
|
|
116
|
+
error: 'OAuth token expired',
|
|
117
|
+
configDir,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
authenticated: true,
|
|
122
|
+
error: null,
|
|
123
|
+
configDir,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for API key auth
|
|
128
|
+
if (creds.apiKey) {
|
|
129
|
+
return {
|
|
130
|
+
authenticated: true,
|
|
131
|
+
error: null,
|
|
132
|
+
configDir,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
authenticated: false,
|
|
138
|
+
error: 'No valid authentication found in credentials',
|
|
139
|
+
configDir,
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return {
|
|
143
|
+
authenticated: false,
|
|
144
|
+
error: `Failed to parse credentials: ${err.message}`,
|
|
145
|
+
configDir,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check gh CLI authentication status
|
|
152
|
+
* @returns {{ installed: boolean, authenticated: boolean, error: string | null }}
|
|
153
|
+
*/
|
|
154
|
+
function checkGhAuth() {
|
|
155
|
+
// Check if gh is installed
|
|
156
|
+
if (!commandExists('gh')) {
|
|
157
|
+
return {
|
|
158
|
+
installed: false,
|
|
159
|
+
authenticated: false,
|
|
160
|
+
error: 'gh CLI not installed',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check auth status
|
|
165
|
+
try {
|
|
166
|
+
execSync('gh auth status', { encoding: 'utf8', stdio: 'pipe' });
|
|
167
|
+
return {
|
|
168
|
+
installed: true,
|
|
169
|
+
authenticated: true,
|
|
170
|
+
error: null,
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// gh auth status returns non-zero if not authenticated
|
|
174
|
+
const stderr = err.stderr || err.message || '';
|
|
175
|
+
|
|
176
|
+
if (stderr.includes('not logged in')) {
|
|
177
|
+
return {
|
|
178
|
+
installed: true,
|
|
179
|
+
authenticated: false,
|
|
180
|
+
error: 'gh CLI not authenticated',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
installed: true,
|
|
186
|
+
authenticated: false,
|
|
187
|
+
error: stderr.trim() || 'Unknown gh auth error',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check Docker availability
|
|
194
|
+
* @returns {{ available: boolean, error: string | null }}
|
|
195
|
+
*/
|
|
196
|
+
function checkDocker() {
|
|
197
|
+
try {
|
|
198
|
+
execSync('docker --version', { encoding: 'utf8', stdio: 'pipe' });
|
|
199
|
+
|
|
200
|
+
// Also check if Docker daemon is running
|
|
201
|
+
execSync('docker info', { encoding: 'utf8', stdio: 'pipe' });
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
available: true,
|
|
205
|
+
error: null,
|
|
206
|
+
};
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const stderr = err.stderr || err.message || '';
|
|
209
|
+
|
|
210
|
+
if (stderr.includes('command not found') || stderr.includes('not found')) {
|
|
211
|
+
return {
|
|
212
|
+
available: false,
|
|
213
|
+
error: 'Docker not installed',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (stderr.includes('Cannot connect') || stderr.includes('Is the docker daemon running')) {
|
|
218
|
+
return {
|
|
219
|
+
available: false,
|
|
220
|
+
error: 'Docker daemon not running',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
available: false,
|
|
226
|
+
error: stderr.trim() || 'Unknown Docker error',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Run all preflight checks
|
|
233
|
+
* @param {Object} options - Preflight options
|
|
234
|
+
* @param {boolean} options.requireGh - Whether gh CLI is required (true if using issue number)
|
|
235
|
+
* @param {boolean} options.requireDocker - Whether Docker is required (true if using --isolation)
|
|
236
|
+
* @param {boolean} options.quiet - Suppress success messages
|
|
237
|
+
* @returns {ValidationResult}
|
|
238
|
+
*/
|
|
239
|
+
function runPreflight(options = {}) {
|
|
240
|
+
const errors = [];
|
|
241
|
+
const warnings = [];
|
|
242
|
+
|
|
243
|
+
// 1. Check Claude CLI installation
|
|
244
|
+
const claude = getClaudeVersion();
|
|
245
|
+
if (!claude.installed) {
|
|
246
|
+
errors.push(
|
|
247
|
+
formatError(
|
|
248
|
+
'Claude CLI not installed',
|
|
249
|
+
claude.error,
|
|
250
|
+
[
|
|
251
|
+
'Install Claude CLI: npm install -g @anthropic-ai/claude-code',
|
|
252
|
+
'Or: brew install claude (macOS)',
|
|
253
|
+
'Then run: claude --version',
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
// 2. Check Claude CLI authentication
|
|
259
|
+
const auth = checkClaudeAuth();
|
|
260
|
+
if (!auth.authenticated) {
|
|
261
|
+
errors.push(
|
|
262
|
+
formatError(
|
|
263
|
+
'Claude CLI not authenticated',
|
|
264
|
+
auth.error,
|
|
265
|
+
[
|
|
266
|
+
'Run: claude login',
|
|
267
|
+
'Follow the browser prompts to authenticate',
|
|
268
|
+
`Config directory: ${auth.configDir}`,
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check version (warn if old)
|
|
275
|
+
if (claude.version) {
|
|
276
|
+
const [major, minor] = claude.version.split('.').map(Number);
|
|
277
|
+
if (major < 1 || (major === 1 && minor < 0)) {
|
|
278
|
+
warnings.push(
|
|
279
|
+
`⚠️ Claude CLI version ${claude.version} may be outdated. Consider upgrading.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 3. Check gh CLI (if required)
|
|
286
|
+
if (options.requireGh) {
|
|
287
|
+
const gh = checkGhAuth();
|
|
288
|
+
if (!gh.installed) {
|
|
289
|
+
errors.push(
|
|
290
|
+
formatError(
|
|
291
|
+
'GitHub CLI (gh) not installed',
|
|
292
|
+
'Required for fetching issues by number',
|
|
293
|
+
[
|
|
294
|
+
'Install: brew install gh (macOS) or apt install gh (Linux)',
|
|
295
|
+
'Or download from: https://cli.github.com/',
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
} else if (!gh.authenticated) {
|
|
300
|
+
errors.push(
|
|
301
|
+
formatError(
|
|
302
|
+
'GitHub CLI (gh) not authenticated',
|
|
303
|
+
gh.error,
|
|
304
|
+
[
|
|
305
|
+
'Run: gh auth login',
|
|
306
|
+
'Select GitHub.com, HTTPS, and authenticate via browser',
|
|
307
|
+
'Then verify: gh auth status',
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 4. Check Docker (if required)
|
|
315
|
+
if (options.requireDocker) {
|
|
316
|
+
const docker = checkDocker();
|
|
317
|
+
if (!docker.available) {
|
|
318
|
+
errors.push(
|
|
319
|
+
formatError(
|
|
320
|
+
'Docker not available',
|
|
321
|
+
docker.error,
|
|
322
|
+
docker.error.includes('daemon')
|
|
323
|
+
? ['Start Docker Desktop', 'Or run: sudo systemctl start docker (Linux)']
|
|
324
|
+
: [
|
|
325
|
+
'Install Docker Desktop from: https://docker.com/products/docker-desktop',
|
|
326
|
+
'Then start Docker and verify: docker info',
|
|
327
|
+
]
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
valid: errors.length === 0,
|
|
335
|
+
errors,
|
|
336
|
+
warnings,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Run preflight checks and exit if failed
|
|
342
|
+
* @param {Object} options - Preflight options
|
|
343
|
+
* @param {boolean} options.requireGh - Whether gh CLI is required
|
|
344
|
+
* @param {boolean} options.requireDocker - Whether Docker is required
|
|
345
|
+
* @param {boolean} options.quiet - Suppress success messages
|
|
346
|
+
*/
|
|
347
|
+
function requirePreflight(options = {}) {
|
|
348
|
+
const result = runPreflight(options);
|
|
349
|
+
|
|
350
|
+
// Print warnings regardless of success
|
|
351
|
+
if (result.warnings.length > 0) {
|
|
352
|
+
for (const warning of result.warnings) {
|
|
353
|
+
console.warn(warning);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!result.valid) {
|
|
358
|
+
console.error('\n' + '='.repeat(60));
|
|
359
|
+
console.error('PREFLIGHT CHECK FAILED');
|
|
360
|
+
console.error('='.repeat(60));
|
|
361
|
+
|
|
362
|
+
for (const error of result.errors) {
|
|
363
|
+
console.error(error);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.error('='.repeat(60) + '\n');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!options.quiet) {
|
|
371
|
+
console.log('✓ Preflight checks passed');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
runPreflight,
|
|
377
|
+
requirePreflight,
|
|
378
|
+
getClaudeVersion,
|
|
379
|
+
checkClaudeAuth,
|
|
380
|
+
checkGhAuth,
|
|
381
|
+
checkDocker,
|
|
382
|
+
formatError,
|
|
383
|
+
};
|