@covibes/zeroshot 5.3.0 → 5.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.
Files changed (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Issue Provider Registry
3
+ *
4
+ * Manages issue provider registration and auto-detection
5
+ */
6
+
7
+ const GitHubProvider = require('./github-provider');
8
+ const GitLabProvider = require('./gitlab-provider');
9
+ const JiraProvider = require('./jira-provider');
10
+ const AzureDevOpsProvider = require('./azure-devops-provider');
11
+ const { detectGitContext } = require('../../lib/git-remote-utils');
12
+
13
+ /** @type {Map<string, typeof IssueProvider>} */
14
+ const providers = new Map();
15
+
16
+ /**
17
+ * Register a provider class
18
+ * @param {typeof IssueProvider} ProviderClass
19
+ */
20
+ function registerProvider(ProviderClass) {
21
+ if (!ProviderClass.id) {
22
+ throw new Error(`Provider ${ProviderClass.name} must define static id property`);
23
+ }
24
+ providers.set(ProviderClass.id, ProviderClass);
25
+ }
26
+
27
+ /**
28
+ * Detect which provider matches the input
29
+ * Automatically detects provider from git remote when in a git repository
30
+ *
31
+ * @param {string} input - User input (URL, issue key, number)
32
+ * @param {Object} settings - User settings
33
+ * @param {string|null} forceProvider - Force specific provider (from CLI flag)
34
+ * @param {Object|null|undefined} [gitContextOverride] - Override git context (for testing)
35
+ * - undefined: auto-detect from git remote (default)
36
+ * - null: explicitly no git context
37
+ * - object: use provided git context
38
+ * @returns {typeof IssueProvider|null}
39
+ */
40
+ function detectProvider(input, settings, forceProvider = null, gitContextOverride = undefined) {
41
+ // Force flag takes precedence
42
+ if (forceProvider) {
43
+ return providers.get(forceProvider) || null;
44
+ }
45
+
46
+ // Use provided gitContext or auto-detect
47
+ const gitContext = gitContextOverride === undefined ? detectGitContext() : gitContextOverride;
48
+
49
+ // Auto-detect by checking each provider's detectIdentifier
50
+ for (const ProviderClass of providers.values()) {
51
+ if (ProviderClass.detectIdentifier(input, settings, gitContext)) {
52
+ return ProviderClass;
53
+ }
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Get provider class by ID
61
+ * @param {string} providerId - Provider identifier
62
+ * @returns {typeof IssueProvider|null}
63
+ */
64
+ function getProvider(providerId) {
65
+ return providers.get(providerId) || null;
66
+ }
67
+
68
+ /**
69
+ * List all registered provider IDs
70
+ * @returns {string[]}
71
+ */
72
+ function listProviders() {
73
+ return Array.from(providers.keys());
74
+ }
75
+
76
+ /**
77
+ * List providers that support PR/MR creation
78
+ * @returns {string[]}
79
+ */
80
+ function listPRProviders() {
81
+ return Array.from(providers.values())
82
+ .filter((p) => p.supportsPR())
83
+ .map((p) => p.id);
84
+ }
85
+
86
+ /**
87
+ * Determine which git platform to create PR/MR on.
88
+ * ALWAYS uses git remote context, NEVER the issue provider.
89
+ *
90
+ * This is the unified replacement for platform-detector.js
91
+ *
92
+ * @param {string} [cwd=process.cwd()] - Working directory
93
+ * @returns {string} Platform ID ('github', 'gitlab', 'azure-devops')
94
+ * @throws {Error} If platform cannot be determined or doesn't support PRs
95
+ *
96
+ * @example
97
+ * // In a GitHub repo
98
+ * getPlatformForPR() // → 'github'
99
+ *
100
+ * @example
101
+ * // In a GitLab repo with Jira issue
102
+ * getPlatformForPR() // → 'gitlab' (creates GitLab MR, not related to Jira)
103
+ */
104
+ function getPlatformForPR(cwd = process.cwd()) {
105
+ const gitContext = detectGitContext(cwd);
106
+
107
+ if (!gitContext?.provider) {
108
+ throw new Error(
109
+ 'Cannot determine git platform for --pr mode. ' +
110
+ 'Ensure you are in a git repository with a remote URL from GitHub, GitLab, or Azure DevOps.'
111
+ );
112
+ }
113
+
114
+ const platform = gitContext.provider;
115
+ const ProviderClass = providers.get(platform);
116
+
117
+ if (!ProviderClass || !ProviderClass.supportsPR()) {
118
+ const supported = listPRProviders().join(', ');
119
+ throw new Error(
120
+ `Platform '${platform}' does not support --pr mode. Supported platforms: ${supported}`
121
+ );
122
+ }
123
+
124
+ return platform;
125
+ }
126
+
127
+ /**
128
+ * Get PR tool info for a platform
129
+ * @param {string} platform - Platform ID
130
+ * @returns {{ name: string, checkCmd: string, installHint: string, displayName: string }|null}
131
+ */
132
+ function getPRToolForPlatform(platform) {
133
+ const ProviderClass = providers.get(platform);
134
+ return ProviderClass?.getPRTool() || null;
135
+ }
136
+
137
+ /**
138
+ * Get aggregated settings schema from all issue providers
139
+ * @returns {Object.<string, Object>} Combined settings schema
140
+ */
141
+ function getIssueProviderSettingsSchema() {
142
+ const schema = {};
143
+ for (const ProviderClass of providers.values()) {
144
+ Object.assign(schema, ProviderClass.getSettingsSchema());
145
+ }
146
+ return schema;
147
+ }
148
+
149
+ /**
150
+ * Get default values for all issue provider settings
151
+ * @returns {Object.<string, any>} Default values keyed by setting name
152
+ */
153
+ function getIssueProviderSettingsDefaults() {
154
+ const defaults = {};
155
+ const schema = getIssueProviderSettingsSchema();
156
+ for (const [key, config] of Object.entries(schema)) {
157
+ defaults[key] = config.default;
158
+ }
159
+ return defaults;
160
+ }
161
+
162
+ /**
163
+ * Validate an issue provider setting
164
+ * Delegates to the appropriate provider's validateSetting method
165
+ * @param {string} key - Setting key
166
+ * @param {any} value - Setting value
167
+ * @returns {string|null|undefined} Error message if invalid, null if valid, undefined if not an issue provider setting
168
+ */
169
+ function validateIssueProviderSetting(key, value) {
170
+ for (const ProviderClass of providers.values()) {
171
+ const result = ProviderClass.validateSetting(key, value);
172
+ if (result !== undefined) {
173
+ return result;
174
+ }
175
+ }
176
+ return undefined; // Not an issue provider setting
177
+ }
178
+
179
+ // Auto-register providers on module load
180
+ registerProvider(GitHubProvider);
181
+ registerProvider(GitLabProvider);
182
+ registerProvider(JiraProvider);
183
+ registerProvider(AzureDevOpsProvider);
184
+
185
+ module.exports = {
186
+ registerProvider,
187
+ detectProvider,
188
+ getProvider,
189
+ listProviders,
190
+ listPRProviders,
191
+ getPlatformForPR,
192
+ getPRToolForPlatform,
193
+ getIssueProviderSettingsSchema,
194
+ getIssueProviderSettingsDefaults,
195
+ validateIssueProviderSetting,
196
+ };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Jira Provider - Fetch issues from Jira via jira CLI (go-jira)
3
+ * Supports both Jira Cloud (*.atlassian.net) and self-hosted (Server/Data Center)
4
+ */
5
+
6
+ const IssueProvider = require('./base-provider');
7
+ const { execSync } = require('../lib/safe-exec');
8
+
9
+ class JiraProvider extends IssueProvider {
10
+ static id = 'jira';
11
+ static displayName = 'Jira';
12
+
13
+ /**
14
+ * Detect Jira issue keys and URLs
15
+ * Matches:
16
+ * - Jira Cloud URLs (*.atlassian.net)
17
+ * - Self-hosted Jira URLs (via jiraInstance setting)
18
+ * - Jira issue keys (KEY-123 format)
19
+ * - Bare numbers when defaultIssueSource=jira (requires jiraProject setting)
20
+ *
21
+ * Note: Jira doesn't use git context since it's not a git hosting platform.
22
+ * Git context will never match 'jira', so this provider only activates via
23
+ * explicit Jira URLs, Jira issue keys, or settings.
24
+ *
25
+ * @param {string} input - Issue identifier (URL, key, or number)
26
+ * @param {Object} settings - User settings
27
+ * @param {Object|null} gitContext - Git context (unused for Jira, but kept for API consistency)
28
+ * @returns {boolean} True if this provider should handle the input
29
+ */
30
+ static detectIdentifier(input, settings, gitContext = null) {
31
+ // Jira Cloud URLs
32
+ if (/atlassian\.net\/browse\/[A-Z][A-Z0-9]+-\d+/.test(input)) {
33
+ return true;
34
+ }
35
+
36
+ // Self-hosted Jira URLs
37
+ if (
38
+ settings.jiraInstance &&
39
+ input.includes(settings.jiraInstance) &&
40
+ /\/browse\/[A-Z][A-Z0-9]+-\d+/.test(input)
41
+ ) {
42
+ return true;
43
+ }
44
+
45
+ // Jira issue key pattern (KEY-123)
46
+ if (/^[A-Z][A-Z0-9]+-\d+$/.test(input)) {
47
+ return true;
48
+ }
49
+
50
+ // Bare numbers - use shared priority cascade logic
51
+ // Jira requires jiraProject setting to convert bare numbers to issue keys
52
+ // Note: gitContext check will never match 'jira' since Jira isn't a git host
53
+ return IssueProvider.detectBareNumber(input, settings, gitContext, 'jira', {
54
+ requiredSettings: ['jiraProject'],
55
+ });
56
+ }
57
+
58
+ static getRequiredTool() {
59
+ return {
60
+ name: 'jira',
61
+ checkCmd: 'jira version',
62
+ installHint: 'Install jira-cli: brew install go-jira (or https://github.com/go-jira/jira)',
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Check jira CLI authentication
68
+ * go-jira uses config files (~/.jira.d/) rather than explicit auth commands
69
+ */
70
+ static checkAuth() {
71
+ try {
72
+ // go-jira uses 'jira session' to verify authentication
73
+ // If not configured, it fails with endpoint/login errors
74
+ execSync('jira session', { encoding: 'utf8', stdio: 'pipe', timeout: 5000 });
75
+ return { authenticated: true, error: null, recovery: [] };
76
+ } catch (err) {
77
+ const stderr = err.stderr || err.message || '';
78
+
79
+ // go-jira has various error patterns for auth issues
80
+ if (
81
+ stderr.includes('endpoint') ||
82
+ stderr.includes('login') ||
83
+ stderr.includes('authentication') ||
84
+ stderr.includes('401')
85
+ ) {
86
+ return {
87
+ authenticated: false,
88
+ error: 'Jira CLI not configured',
89
+ recovery: [
90
+ 'Create ~/.jira.d/config.yml with your Jira endpoint',
91
+ 'See: https://github.com/go-jira/jira#configuration',
92
+ 'Example config:\n endpoint: https://yourcompany.atlassian.net\n login: your-email@company.com',
93
+ 'Then verify: jira session',
94
+ ],
95
+ };
96
+ }
97
+
98
+ // If command just doesn't exist or times out, it's likely not configured
99
+ if (stderr.includes('command not found') || stderr.includes('ETIMEDOUT')) {
100
+ return {
101
+ authenticated: false,
102
+ error: 'Jira CLI not configured',
103
+ recovery: [
104
+ 'Configure ~/.jira.d/config.yml',
105
+ 'See: https://github.com/go-jira/jira#configuration',
106
+ ],
107
+ };
108
+ }
109
+
110
+ // Default: assume not authenticated
111
+ return {
112
+ authenticated: false,
113
+ error: stderr.trim() || 'Jira CLI not configured or authentication failed',
114
+ recovery: [
115
+ 'Create ~/.jira.d/config.yml with your Jira endpoint',
116
+ 'See: https://github.com/go-jira/jira#configuration',
117
+ ],
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Jira-specific settings schema
124
+ */
125
+ static getSettingsSchema() {
126
+ return {
127
+ jiraInstance: {
128
+ type: 'string',
129
+ nullable: true,
130
+ default: null,
131
+ description: "Self-hosted Jira URL (e.g., 'jira.company.com')",
132
+ },
133
+ jiraProject: {
134
+ type: 'string',
135
+ nullable: true,
136
+ default: null,
137
+ description: "Default Jira project key for bare numbers (e.g., 'PROJ')",
138
+ pattern: /^[A-Z][A-Z0-9]*$/,
139
+ patternMsg: 'jiraProject must be a valid Jira project key (e.g., PROJ, ABC)',
140
+ },
141
+ };
142
+ }
143
+
144
+ fetchIssue(identifier, settings) {
145
+ try {
146
+ const issueKey = this._extractIssueKey(identifier, settings);
147
+
148
+ // Fetch issue using jira CLI
149
+ const cmd = `jira issue view ${issueKey} --template json`;
150
+ const output = execSync(cmd, { encoding: 'utf8' });
151
+ const issue = JSON.parse(output);
152
+
153
+ return this._parseIssue(issue);
154
+ } catch (error) {
155
+ throw new Error(`Failed to fetch Jira issue: ${error.message}`);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Extract Jira issue key from URL or return as-is
161
+ * @private
162
+ */
163
+ _extractIssueKey(identifier, settings) {
164
+ // URL format: https://company.atlassian.net/browse/KEY-123
165
+ const urlMatch = identifier.match(/\/browse\/([A-Z][A-Z0-9]+-\d+)/);
166
+ if (urlMatch) {
167
+ return urlMatch[1];
168
+ }
169
+
170
+ // Bare number: construct from jiraProject setting
171
+ if (/^\d+$/.test(identifier) && settings.jiraProject) {
172
+ return `${settings.jiraProject}-${identifier}`;
173
+ }
174
+
175
+ // Assume it's already a key
176
+ return identifier;
177
+ }
178
+
179
+ /**
180
+ * Parse Jira issue into standardized InputData format
181
+ * @private
182
+ */
183
+ _parseIssue(issue) {
184
+ const fields = issue.fields || {};
185
+ const key = issue.key;
186
+ const summary = fields.summary || '';
187
+ const description = fields.description || '';
188
+ const labels = fields.labels || [];
189
+ const comments = fields.comment?.comments || [];
190
+
191
+ let context = `# Jira Issue ${key}\n\n`;
192
+ context += `## Title\n${summary}\n\n`;
193
+
194
+ if (description) {
195
+ context += `## Description\n${description}\n\n`;
196
+ }
197
+
198
+ if (labels.length > 0) {
199
+ context += `## Labels\n`;
200
+ context += labels.map((l) => `- ${l}`).join('\n');
201
+ context += '\n\n';
202
+ }
203
+
204
+ if (comments.length > 0) {
205
+ context += `## Comments\n\n`;
206
+ for (const comment of comments) {
207
+ const author = comment.author?.displayName || comment.author?.name || 'unknown';
208
+ context += `### ${author} (${comment.created})\n`;
209
+ context += `${comment.body}\n\n`;
210
+ }
211
+ }
212
+
213
+ // Map Jira labels to GitHub format
214
+ const mappedLabels = labels.map((name) => ({ name }));
215
+
216
+ // Map Jira comments to GitHub format
217
+ const mappedComments = comments.map((comment) => ({
218
+ author: { login: comment.author?.displayName || comment.author?.name || 'unknown' },
219
+ createdAt: comment.created,
220
+ body: comment.body,
221
+ }));
222
+
223
+ // Extract issue number from key (KEY-123 → 123)
224
+ const numberMatch = key.match(/-(\d+)$/);
225
+ const number = numberMatch ? parseInt(numberMatch[1], 10) : null;
226
+
227
+ return {
228
+ number,
229
+ title: summary,
230
+ body: description,
231
+ labels: mappedLabels,
232
+ comments: mappedComments,
233
+ url: fields.self || null,
234
+ context,
235
+ };
236
+ }
237
+ }
238
+
239
+ module.exports = JiraProvider;
package/src/ledger.js CHANGED
@@ -15,7 +15,15 @@ const crypto = require('crypto');
15
15
  class Ledger extends EventEmitter {
16
16
  constructor(dbPath = ':memory:') {
17
17
  super();
18
- this.db = new Database(dbPath);
18
+ this.dbPath = dbPath;
19
+ const busyTimeoutMs = (() => {
20
+ const raw = process.env.ZEROSHOT_SQLITE_BUSY_TIMEOUT_MS;
21
+ if (!raw) return 5000;
22
+ const value = Number(raw);
23
+ return Number.isFinite(value) && value >= 0 ? value : 5000;
24
+ })();
25
+
26
+ this.db = new Database(dbPath, { timeout: busyTimeoutMs });
19
27
  this.cache = new Map(); // LRU cache for queries
20
28
  this.cacheLimit = 1000;
21
29
  this._closed = false; // Track closed state to prevent write-after-close
@@ -24,12 +32,21 @@ class Ledger extends EventEmitter {
24
32
  }
25
33
 
26
34
  _initSchema() {
27
- // Enable WAL mode for concurrent reads
28
- this.db.pragma('journal_mode = WAL');
35
+ const journalMode = (process.env.ZEROSHOT_SQLITE_JOURNAL_MODE || 'WAL').trim().toUpperCase();
36
+ // Enable WAL mode for concurrent reads (default), but allow overrides for network filesystems.
37
+ this.db.pragma(`journal_mode = ${journalMode}`);
29
38
  // Force synchronous writes so other processes see changes immediately
30
39
  this.db.pragma('synchronous = NORMAL');
31
- // Checkpoint WAL frequently for cross-process visibility
32
- this.db.pragma('wal_autocheckpoint = 1');
40
+ // Autocheckpoint trades latency for WAL growth; 1-page checkpoints are extremely slow on
41
+ // higher-latency disks (common in Kubernetes PVs). Default to SQLite-ish behavior (1000 pages),
42
+ // but allow override for niche correctness/debugging needs.
43
+ const walAutocheckpointPages = (() => {
44
+ const raw = process.env.ZEROSHOT_SQLITE_WAL_AUTOCHECKPOINT_PAGES;
45
+ if (!raw) return 1000;
46
+ const value = Number(raw);
47
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 1000;
48
+ })();
49
+ this.db.pragma(`wal_autocheckpoint = ${walAutocheckpointPages}`);
33
50
 
34
51
  // Create messages table
35
52
  this.db.exec(`
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Safe subprocess execution with mandatory timeouts.
3
+ *
4
+ * NEVER use child_process.exec() or execSync() directly.
5
+ * These wrappers enforce timeouts to prevent infinite hangs.
6
+ */
7
+
8
+ const { exec: nodeExec, execSync: nodeExecSync } = require('child_process');
9
+
10
+ /** Default timeout: 30 seconds */
11
+ const DEFAULT_TIMEOUT_MS = 30000;
12
+
13
+ /**
14
+ * Execute command with mandatory timeout.
15
+ * Supports both Promise and callback styles for gradual migration.
16
+ *
17
+ * @param {string} command - Command to execute
18
+ * @param {object} [options] - Options (timeout uses default if not specified)
19
+ * @param {function} [callback] - Optional callback(error, stdout, stderr)
20
+ * @returns {Promise<{stdout: string, stderr: string}>|void} Promise if no callback, void if callback
21
+ *
22
+ * @example
23
+ * // Promise style (preferred)
24
+ * const { stdout } = await exec('ls -la', { timeout: 5000 });
25
+ *
26
+ * @example
27
+ * // Callback style (for legacy code migration)
28
+ * exec('ls -la', { timeout: 5000 }, (error, stdout) => { ... });
29
+ */
30
+ function exec(command, optionsOrCallback = {}, callbackArg = null) {
31
+ // Handle overloaded signature: exec(cmd, callback)
32
+ const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callbackArg;
33
+ const options = typeof optionsOrCallback === 'function' ? {} : optionsOrCallback;
34
+
35
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
36
+
37
+ if (timeout <= 0) {
38
+ const err = new Error('exec() timeout must be > 0. Infinite waits are forbidden.');
39
+ if (callback) {
40
+ callback(err);
41
+ return;
42
+ }
43
+ return Promise.reject(err);
44
+ }
45
+
46
+ // Callback style
47
+ if (callback) {
48
+ nodeExec(command, { ...options, timeout }, (error, stdout, stderr) => {
49
+ if (error && error.killed && error.signal === 'SIGTERM') {
50
+ error.message = `Command timed out after ${timeout}ms: ${command}`;
51
+ }
52
+ callback(error, stdout, stderr);
53
+ });
54
+ return;
55
+ }
56
+
57
+ // Promise style
58
+ return new Promise((resolve, reject) => {
59
+ nodeExec(command, { ...options, timeout }, (error, stdout, stderr) => {
60
+ if (error) {
61
+ if (error.killed && error.signal === 'SIGTERM') {
62
+ error.message = `Command timed out after ${timeout}ms: ${command}`;
63
+ }
64
+ reject(error);
65
+ } else {
66
+ resolve({ stdout, stderr });
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Execute command with mandatory timeout (sync)
74
+ * @param {string} command - Command to execute
75
+ * @param {object} [options] - Options (timeout required or uses default)
76
+ * @returns {string} stdout
77
+ */
78
+ function execSync(command, options = {}) {
79
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
80
+
81
+ if (timeout <= 0) {
82
+ throw new Error('execSync() timeout must be > 0. Infinite waits are forbidden.');
83
+ }
84
+
85
+ return nodeExecSync(command, { ...options, timeout });
86
+ }
87
+
88
+ module.exports = { exec, execSync, DEFAULT_TIMEOUT_MS };