start-command 0.3.1
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.changeset/isolation-support.md +30 -0
- package/.github/workflows/release.yml +292 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +6 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +24 -0
- package/README.md +249 -0
- package/REQUIREMENTS.md +229 -0
- package/bun.lock +453 -0
- package/bunfig.toml +3 -0
- package/eslint.config.mjs +122 -0
- package/experiments/debug-regex.js +49 -0
- package/experiments/isolation-design.md +142 -0
- package/experiments/test-cli.sh +42 -0
- package/experiments/test-substitution.js +143 -0
- package/package.json +63 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/check-file-size.mjs +103 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +89 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +219 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/publish-to-npm.mjs +129 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +107 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/bin/cli.js +670 -0
- package/src/lib/args-parser.js +259 -0
- package/src/lib/isolation.js +419 -0
- package/src/lib/substitution.js +323 -0
- package/src/lib/substitutions.lino +308 -0
- package/test/args-parser.test.js +389 -0
- package/test/isolation.test.js +248 -0
- package/test/substitution.test.js +236 -0
package/src/bin/cli.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
|
+
const process = require('process');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Import modules
|
|
10
|
+
const { processCommand } = require('../lib/substitution');
|
|
11
|
+
const {
|
|
12
|
+
parseArgs,
|
|
13
|
+
hasIsolation,
|
|
14
|
+
getEffectiveMode,
|
|
15
|
+
} = require('../lib/args-parser');
|
|
16
|
+
const { runIsolated } = require('../lib/isolation');
|
|
17
|
+
|
|
18
|
+
// Configuration from environment variables
|
|
19
|
+
const config = {
|
|
20
|
+
// Disable automatic issue creation (useful for testing)
|
|
21
|
+
disableAutoIssue:
|
|
22
|
+
process.env.START_DISABLE_AUTO_ISSUE === '1' ||
|
|
23
|
+
process.env.START_DISABLE_AUTO_ISSUE === 'true',
|
|
24
|
+
// Disable log upload
|
|
25
|
+
disableLogUpload:
|
|
26
|
+
process.env.START_DISABLE_LOG_UPLOAD === '1' ||
|
|
27
|
+
process.env.START_DISABLE_LOG_UPLOAD === 'true',
|
|
28
|
+
// Custom log directory (defaults to OS temp)
|
|
29
|
+
logDir: process.env.START_LOG_DIR || null,
|
|
30
|
+
// Verbose mode
|
|
31
|
+
verbose:
|
|
32
|
+
process.env.START_VERBOSE === '1' || process.env.START_VERBOSE === 'true',
|
|
33
|
+
// Disable substitutions/aliases
|
|
34
|
+
disableSubstitutions:
|
|
35
|
+
process.env.START_DISABLE_SUBSTITUTIONS === '1' ||
|
|
36
|
+
process.env.START_DISABLE_SUBSTITUTIONS === 'true',
|
|
37
|
+
// Custom substitutions file path
|
|
38
|
+
substitutionsPath: process.env.START_SUBSTITUTIONS_PATH || null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Get all arguments passed after the command
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
|
|
44
|
+
if (args.length === 0) {
|
|
45
|
+
printUsage();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Print usage information
|
|
51
|
+
*/
|
|
52
|
+
function printUsage() {
|
|
53
|
+
console.log('Usage: $ [options] [--] <command> [args...]');
|
|
54
|
+
console.log(' $ <command> [args...]');
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log('Options:');
|
|
57
|
+
console.log(
|
|
58
|
+
' --isolated, -i <backend> Run in isolated environment (screen, tmux, docker, zellij)'
|
|
59
|
+
);
|
|
60
|
+
console.log(' --attached, -a Run in attached mode (foreground)');
|
|
61
|
+
console.log(' --detached, -d Run in detached mode (background)');
|
|
62
|
+
console.log(' --session, -s <name> Session name for isolation');
|
|
63
|
+
console.log(
|
|
64
|
+
' --image <image> Docker image (required for docker isolation)'
|
|
65
|
+
);
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log('Examples:');
|
|
68
|
+
console.log(' $ echo "Hello World"');
|
|
69
|
+
console.log(' $ npm test');
|
|
70
|
+
console.log(' $ --isolated tmux -- npm start');
|
|
71
|
+
console.log(' $ -i screen -d npm start');
|
|
72
|
+
console.log(' $ --isolated docker --image node:20 -- npm install');
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log('Features:');
|
|
75
|
+
console.log(' - Logs all output to temporary directory');
|
|
76
|
+
console.log(' - Displays timestamps and exit codes');
|
|
77
|
+
console.log(
|
|
78
|
+
' - Auto-reports failures for NPM packages (when gh is available)'
|
|
79
|
+
);
|
|
80
|
+
console.log(' - Natural language command aliases (via substitutions.lino)');
|
|
81
|
+
console.log(' - Process isolation via screen, tmux, zellij, or docker');
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log('Alias examples:');
|
|
84
|
+
console.log(' $ install lodash npm package -> npm install lodash');
|
|
85
|
+
console.log(
|
|
86
|
+
' $ install 4.17.21 version of lodash npm package -> npm install lodash@4.17.21'
|
|
87
|
+
);
|
|
88
|
+
console.log(
|
|
89
|
+
' $ clone https://github.com/user/repo repository -> git clone https://github.com/user/repo'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse wrapper options and command
|
|
94
|
+
let parsedArgs;
|
|
95
|
+
try {
|
|
96
|
+
parsedArgs = parseArgs(args);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Error: ${err.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { wrapperOptions, command: parsedCommand } = parsedArgs;
|
|
103
|
+
|
|
104
|
+
// Check if no command was provided
|
|
105
|
+
if (!parsedCommand || parsedCommand.trim() === '') {
|
|
106
|
+
console.error('Error: No command provided');
|
|
107
|
+
printUsage();
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Process through substitution engine (unless disabled)
|
|
112
|
+
let command = parsedCommand;
|
|
113
|
+
let substitutionResult = null;
|
|
114
|
+
|
|
115
|
+
if (!config.disableSubstitutions) {
|
|
116
|
+
substitutionResult = processCommand(parsedCommand, {
|
|
117
|
+
customLinoPath: config.substitutionsPath,
|
|
118
|
+
verbose: config.verbose,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (substitutionResult.matched) {
|
|
122
|
+
command = substitutionResult.command;
|
|
123
|
+
if (config.verbose) {
|
|
124
|
+
console.log(`[Substitution] "${parsedCommand}" -> "${command}"`);
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Main execution
|
|
131
|
+
(async () => {
|
|
132
|
+
// Check if running in isolation mode
|
|
133
|
+
if (hasIsolation(wrapperOptions)) {
|
|
134
|
+
await runWithIsolation(wrapperOptions, command);
|
|
135
|
+
} else {
|
|
136
|
+
await runDirect(command);
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run command in isolation mode
|
|
142
|
+
* @param {object} options - Wrapper options
|
|
143
|
+
* @param {string} cmd - Command to execute
|
|
144
|
+
*/
|
|
145
|
+
async function runWithIsolation(options, cmd) {
|
|
146
|
+
const backend = options.isolated;
|
|
147
|
+
const mode = getEffectiveMode(options);
|
|
148
|
+
|
|
149
|
+
// Log isolation info
|
|
150
|
+
console.log(`[Isolation] Backend: ${backend}, Mode: ${mode}`);
|
|
151
|
+
if (options.session) {
|
|
152
|
+
console.log(`[Isolation] Session: ${options.session}`);
|
|
153
|
+
}
|
|
154
|
+
if (options.image) {
|
|
155
|
+
console.log(`[Isolation] Image: ${options.image}`);
|
|
156
|
+
}
|
|
157
|
+
console.log(`[Isolation] Command: ${cmd}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
|
|
160
|
+
// Run in isolation
|
|
161
|
+
const result = await runIsolated(backend, cmd, {
|
|
162
|
+
session: options.session,
|
|
163
|
+
image: options.image,
|
|
164
|
+
detached: mode === 'detached',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Print result
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(result.message);
|
|
170
|
+
|
|
171
|
+
if (result.success) {
|
|
172
|
+
process.exit(result.exitCode || 0);
|
|
173
|
+
} else {
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run command directly (without isolation)
|
|
180
|
+
* @param {string} cmd - Command to execute
|
|
181
|
+
*/
|
|
182
|
+
function runDirect(cmd) {
|
|
183
|
+
// Get the command name (first word of the actual command to execute)
|
|
184
|
+
const commandName = cmd.split(' ')[0];
|
|
185
|
+
|
|
186
|
+
// Determine the shell based on the platform
|
|
187
|
+
const isWindows = process.platform === 'win32';
|
|
188
|
+
const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
189
|
+
const shellArgs = isWindows ? ['-Command', cmd] : ['-c', cmd];
|
|
190
|
+
|
|
191
|
+
// Setup logging
|
|
192
|
+
const logDir = config.logDir || os.tmpdir();
|
|
193
|
+
const logFilename = generateLogFilename();
|
|
194
|
+
const logFilePath = path.join(logDir, logFilename);
|
|
195
|
+
|
|
196
|
+
let logContent = '';
|
|
197
|
+
const startTime = getTimestamp();
|
|
198
|
+
|
|
199
|
+
// Log header
|
|
200
|
+
logContent += `=== Start Command Log ===\n`;
|
|
201
|
+
logContent += `Timestamp: ${startTime}\n`;
|
|
202
|
+
if (substitutionResult && substitutionResult.matched) {
|
|
203
|
+
logContent += `Original Input: ${parsedCommand}\n`;
|
|
204
|
+
logContent += `Substituted Command: ${cmd}\n`;
|
|
205
|
+
logContent += `Pattern Matched: ${substitutionResult.rule.pattern}\n`;
|
|
206
|
+
} else {
|
|
207
|
+
logContent += `Command: ${cmd}\n`;
|
|
208
|
+
}
|
|
209
|
+
logContent += `Shell: ${shell}\n`;
|
|
210
|
+
logContent += `Platform: ${process.platform}\n`;
|
|
211
|
+
logContent += `Node Version: ${process.version}\n`;
|
|
212
|
+
logContent += `Working Directory: ${process.cwd()}\n`;
|
|
213
|
+
logContent += `${'='.repeat(50)}\n\n`;
|
|
214
|
+
|
|
215
|
+
// Print start message to console
|
|
216
|
+
if (substitutionResult && substitutionResult.matched) {
|
|
217
|
+
console.log(`[${startTime}] Input: ${parsedCommand}`);
|
|
218
|
+
console.log(`[${startTime}] Executing: ${cmd}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`[${startTime}] Starting: ${cmd}`);
|
|
221
|
+
}
|
|
222
|
+
console.log('');
|
|
223
|
+
|
|
224
|
+
// Execute the command with captured output
|
|
225
|
+
const child = spawn(shell, shellArgs, {
|
|
226
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
227
|
+
shell: false,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Capture stdout
|
|
231
|
+
child.stdout.on('data', (data) => {
|
|
232
|
+
const text = data.toString();
|
|
233
|
+
process.stdout.write(text);
|
|
234
|
+
logContent += text;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Capture stderr
|
|
238
|
+
child.stderr.on('data', (data) => {
|
|
239
|
+
const text = data.toString();
|
|
240
|
+
process.stderr.write(text);
|
|
241
|
+
logContent += text;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Handle process exit
|
|
245
|
+
child.on('exit', (code) => {
|
|
246
|
+
const exitCode = code || 0;
|
|
247
|
+
const endTime = getTimestamp();
|
|
248
|
+
|
|
249
|
+
// Log footer
|
|
250
|
+
logContent += `\n${'='.repeat(50)}\n`;
|
|
251
|
+
logContent += `Finished: ${endTime}\n`;
|
|
252
|
+
logContent += `Exit Code: ${exitCode}\n`;
|
|
253
|
+
|
|
254
|
+
// Write log file
|
|
255
|
+
try {
|
|
256
|
+
fs.writeFileSync(logFilePath, logContent, 'utf8');
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Print footer to console
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(`[${endTime}] Finished`);
|
|
264
|
+
console.log(`Exit code: ${exitCode}`);
|
|
265
|
+
console.log(`Log saved: ${logFilePath}`);
|
|
266
|
+
|
|
267
|
+
// If command failed, try to auto-report
|
|
268
|
+
if (exitCode !== 0) {
|
|
269
|
+
handleFailure(commandName, cmd, exitCode, logFilePath, logContent);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
process.exit(exitCode);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Handle spawn errors
|
|
276
|
+
child.on('error', (err) => {
|
|
277
|
+
const endTime = getTimestamp();
|
|
278
|
+
const errorMessage = `Error executing command: ${err.message}`;
|
|
279
|
+
|
|
280
|
+
logContent += `\n${errorMessage}\n`;
|
|
281
|
+
logContent += `\n${'='.repeat(50)}\n`;
|
|
282
|
+
logContent += `Finished: ${endTime}\n`;
|
|
283
|
+
logContent += `Exit Code: 1\n`;
|
|
284
|
+
|
|
285
|
+
// Write log file
|
|
286
|
+
try {
|
|
287
|
+
fs.writeFileSync(logFilePath, logContent, 'utf8');
|
|
288
|
+
} catch (writeErr) {
|
|
289
|
+
console.error(`\nWarning: Could not save log file: ${writeErr.message}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.error(`\n${errorMessage}`);
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(`[${endTime}] Finished`);
|
|
295
|
+
console.log(`Exit code: 1`);
|
|
296
|
+
console.log(`Log saved: ${logFilePath}`);
|
|
297
|
+
|
|
298
|
+
handleFailure(commandName, cmd, 1, logFilePath, logContent);
|
|
299
|
+
|
|
300
|
+
process.exit(1);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Generate timestamp for logging
|
|
305
|
+
function getTimestamp() {
|
|
306
|
+
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Generate unique log filename
|
|
310
|
+
function generateLogFilename() {
|
|
311
|
+
const timestamp = Date.now();
|
|
312
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
313
|
+
return `start-command-${timestamp}-${random}.log`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Handle command failure - detect repository, upload log, create issue
|
|
318
|
+
*/
|
|
319
|
+
function handleFailure(cmdName, fullCommand, exitCode, logPath) {
|
|
320
|
+
console.log('');
|
|
321
|
+
|
|
322
|
+
// Check if auto-issue is disabled
|
|
323
|
+
if (config.disableAutoIssue) {
|
|
324
|
+
if (config.verbose) {
|
|
325
|
+
console.log('Auto-issue creation disabled via START_DISABLE_AUTO_ISSUE');
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Try to detect repository for the command
|
|
331
|
+
const repoInfo = detectRepository(cmdName);
|
|
332
|
+
|
|
333
|
+
if (!repoInfo) {
|
|
334
|
+
console.log('Repository not detected - automatic issue creation skipped');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`Detected repository: ${repoInfo.url}`);
|
|
339
|
+
|
|
340
|
+
// Check if gh CLI is available and authenticated
|
|
341
|
+
if (!isGhAuthenticated()) {
|
|
342
|
+
console.log(
|
|
343
|
+
'GitHub CLI not authenticated - automatic issue creation skipped'
|
|
344
|
+
);
|
|
345
|
+
console.log('Run "gh auth login" to enable automatic issue creation');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Try to upload log
|
|
350
|
+
let logUrl = null;
|
|
351
|
+
if (config.disableLogUpload) {
|
|
352
|
+
if (config.verbose) {
|
|
353
|
+
console.log('Log upload disabled via START_DISABLE_LOG_UPLOAD');
|
|
354
|
+
}
|
|
355
|
+
} else if (isGhUploadLogAvailable()) {
|
|
356
|
+
logUrl = uploadLog(logPath);
|
|
357
|
+
if (logUrl) {
|
|
358
|
+
console.log(`Log uploaded: ${logUrl}`);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
console.log('gh-upload-log not installed - log upload skipped');
|
|
362
|
+
console.log('Install with: npm install -g gh-upload-log');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if we can create issues in this repository
|
|
366
|
+
if (!canCreateIssue(repoInfo.owner, repoInfo.repo)) {
|
|
367
|
+
console.log('Cannot create issue in repository - skipping issue creation');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Create issue
|
|
372
|
+
const issueUrl = createIssue(
|
|
373
|
+
repoInfo,
|
|
374
|
+
fullCommand,
|
|
375
|
+
exitCode,
|
|
376
|
+
logUrl,
|
|
377
|
+
logPath
|
|
378
|
+
);
|
|
379
|
+
if (issueUrl) {
|
|
380
|
+
console.log(`Issue created: ${issueUrl}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Detect repository URL for a command (currently supports NPM global packages)
|
|
386
|
+
*/
|
|
387
|
+
function detectRepository(cmdName) {
|
|
388
|
+
const isWindows = process.platform === 'win32';
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
// Find command location
|
|
392
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
393
|
+
let cmdPath;
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
cmdPath = execSync(`${whichCmd} ${cmdName}`, {
|
|
397
|
+
encoding: 'utf8',
|
|
398
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
399
|
+
}).trim();
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!cmdPath) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Handle Windows where command that returns multiple lines
|
|
409
|
+
if (isWindows && cmdPath.includes('\n')) {
|
|
410
|
+
cmdPath = cmdPath.split('\n')[0].trim();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Check if it's in npm global modules
|
|
414
|
+
let npmGlobalPath;
|
|
415
|
+
try {
|
|
416
|
+
npmGlobalPath = execSync('npm root -g', {
|
|
417
|
+
encoding: 'utf8',
|
|
418
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
419
|
+
}).trim();
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Get the npm bin directory (parent of node_modules)
|
|
425
|
+
const npmBinPath = `${path.dirname(npmGlobalPath)}/bin`;
|
|
426
|
+
|
|
427
|
+
// Check if the command is located in the npm bin directory or node_modules
|
|
428
|
+
let packageName = null;
|
|
429
|
+
let isNpmPackage = false;
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Try to resolve the symlink to find the actual path
|
|
433
|
+
const realPath = fs.realpathSync(cmdPath);
|
|
434
|
+
|
|
435
|
+
// Check if the real path is within node_modules
|
|
436
|
+
if (realPath.includes('node_modules')) {
|
|
437
|
+
isNpmPackage = true;
|
|
438
|
+
const npmPathMatch = realPath.match(/node_modules\/([^/]+)/);
|
|
439
|
+
if (npmPathMatch) {
|
|
440
|
+
packageName = npmPathMatch[1];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Also check if the command path itself is in npm's bin directory
|
|
445
|
+
if (!isNpmPackage && cmdPath.includes(npmBinPath)) {
|
|
446
|
+
isNpmPackage = true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Try to read the bin script to extract package info
|
|
450
|
+
if (!packageName) {
|
|
451
|
+
const binContent = fs.readFileSync(cmdPath, 'utf8');
|
|
452
|
+
|
|
453
|
+
// Check if this is a Node.js script
|
|
454
|
+
if (
|
|
455
|
+
binContent.startsWith('#!/usr/bin/env node') ||
|
|
456
|
+
binContent.includes('node_modules')
|
|
457
|
+
) {
|
|
458
|
+
isNpmPackage = true;
|
|
459
|
+
|
|
460
|
+
// Look for package path in the script
|
|
461
|
+
const packagePathMatch = binContent.match(/node_modules\/([^/'"]+)/);
|
|
462
|
+
if (packagePathMatch) {
|
|
463
|
+
packageName = packagePathMatch[1];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
// Could not read/resolve command - not an npm package
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// If we couldn't confirm this is an npm package, don't proceed
|
|
473
|
+
if (!isNpmPackage) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// If we couldn't find the package name from the path, use the command name
|
|
478
|
+
if (!packageName) {
|
|
479
|
+
packageName = cmdName;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Try to get repository URL from npm
|
|
483
|
+
try {
|
|
484
|
+
const npmInfo = execSync(
|
|
485
|
+
`npm view ${packageName} repository.url 2>/dev/null`,
|
|
486
|
+
{
|
|
487
|
+
encoding: 'utf8',
|
|
488
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
489
|
+
}
|
|
490
|
+
).trim();
|
|
491
|
+
|
|
492
|
+
if (npmInfo) {
|
|
493
|
+
// Parse git URL to extract owner and repo
|
|
494
|
+
const parsed = parseGitUrl(npmInfo);
|
|
495
|
+
if (parsed) {
|
|
496
|
+
return parsed;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
// npm view failed, package might not exist or have no repository
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Try to get homepage or bugs URL as fallback
|
|
504
|
+
try {
|
|
505
|
+
const bugsUrl = execSync(`npm view ${packageName} bugs.url 2>/dev/null`, {
|
|
506
|
+
encoding: 'utf8',
|
|
507
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
508
|
+
}).trim();
|
|
509
|
+
|
|
510
|
+
if (bugsUrl && bugsUrl.includes('github.com')) {
|
|
511
|
+
const parsed = parseGitUrl(bugsUrl);
|
|
512
|
+
if (parsed) {
|
|
513
|
+
return parsed;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
// Fallback also failed
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return null;
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Parse a git URL to extract owner, repo, and normalized URL
|
|
528
|
+
*/
|
|
529
|
+
function parseGitUrl(url) {
|
|
530
|
+
if (!url) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Handle various git URL formats
|
|
535
|
+
// git+https://github.com/owner/repo.git
|
|
536
|
+
// git://github.com/owner/repo.git
|
|
537
|
+
// https://github.com/owner/repo.git
|
|
538
|
+
// https://github.com/owner/repo
|
|
539
|
+
// git@github.com:owner/repo.git
|
|
540
|
+
// https://github.com/owner/repo/issues
|
|
541
|
+
|
|
542
|
+
const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
543
|
+
if (match) {
|
|
544
|
+
const owner = match[1];
|
|
545
|
+
const repo = match[2].replace(/\.git$/, '');
|
|
546
|
+
return {
|
|
547
|
+
owner,
|
|
548
|
+
repo,
|
|
549
|
+
url: `https://github.com/${owner}/${repo}`,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check if GitHub CLI is authenticated
|
|
558
|
+
*/
|
|
559
|
+
function isGhAuthenticated() {
|
|
560
|
+
try {
|
|
561
|
+
execSync('gh auth status', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
562
|
+
return true;
|
|
563
|
+
} catch {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Check if gh-upload-log is available
|
|
570
|
+
*/
|
|
571
|
+
function isGhUploadLogAvailable() {
|
|
572
|
+
const isWindows = process.platform === 'win32';
|
|
573
|
+
try {
|
|
574
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
575
|
+
execSync(`${whichCmd} gh-upload-log`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
576
|
+
return true;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Upload log file using gh-upload-log
|
|
584
|
+
*/
|
|
585
|
+
function uploadLog(logPath) {
|
|
586
|
+
try {
|
|
587
|
+
const result = execSync(`gh-upload-log "${logPath}" --public`, {
|
|
588
|
+
encoding: 'utf8',
|
|
589
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Extract URL from output
|
|
593
|
+
const urlMatch = result.match(/https:\/\/gist\.github\.com\/[^\s]+/);
|
|
594
|
+
if (urlMatch) {
|
|
595
|
+
return urlMatch[0];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Try other URL patterns
|
|
599
|
+
const repoUrlMatch = result.match(/https:\/\/github\.com\/[^\s]+/);
|
|
600
|
+
if (repoUrlMatch) {
|
|
601
|
+
return repoUrlMatch[0];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return null;
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.log(`Warning: Log upload failed - ${err.message}`);
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Check if we can create an issue in a repository
|
|
613
|
+
*/
|
|
614
|
+
function canCreateIssue(owner, repo) {
|
|
615
|
+
try {
|
|
616
|
+
// Check if the repository exists and we have access
|
|
617
|
+
execSync(`gh repo view ${owner}/${repo} --json name`, {
|
|
618
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
619
|
+
});
|
|
620
|
+
return true;
|
|
621
|
+
} catch {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Create an issue in the repository
|
|
628
|
+
*/
|
|
629
|
+
function createIssue(repoInfo, fullCommand, exitCode, logUrl) {
|
|
630
|
+
try {
|
|
631
|
+
const title = `Command failed with exit code ${exitCode}: ${fullCommand.substring(0, 50)}${fullCommand.length > 50 ? '...' : ''}`;
|
|
632
|
+
|
|
633
|
+
let body = `## Command Execution Failure Report\n\n`;
|
|
634
|
+
body += `**Command:** \`${fullCommand}\`\n\n`;
|
|
635
|
+
body += `**Exit Code:** ${exitCode}\n\n`;
|
|
636
|
+
body += `**Timestamp:** ${getTimestamp()}\n\n`;
|
|
637
|
+
body += `### System Information\n\n`;
|
|
638
|
+
body += `- **Platform:** ${process.platform}\n`;
|
|
639
|
+
body += `- **OS Release:** ${os.release()}\n`;
|
|
640
|
+
body += `- **Node Version:** ${process.version}\n`;
|
|
641
|
+
body += `- **Architecture:** ${process.arch}\n\n`;
|
|
642
|
+
|
|
643
|
+
if (logUrl) {
|
|
644
|
+
body += `### Log File\n\n`;
|
|
645
|
+
body += `Full log available at: ${logUrl}\n\n`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
body += `---\n`;
|
|
649
|
+
body += `*This issue was automatically created by [start-command](https://github.com/link-foundation/start)*\n`;
|
|
650
|
+
|
|
651
|
+
const result = execSync(
|
|
652
|
+
`gh issue create --repo ${repoInfo.owner}/${repoInfo.repo} --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`,
|
|
653
|
+
{
|
|
654
|
+
encoding: 'utf8',
|
|
655
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
// Extract issue URL from output
|
|
660
|
+
const urlMatch = result.match(/https:\/\/github\.com\/[^\s]+/);
|
|
661
|
+
if (urlMatch) {
|
|
662
|
+
return urlMatch[0];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return null;
|
|
666
|
+
} catch (err) {
|
|
667
|
+
console.log(`Warning: Issue creation failed - ${err.message}`);
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
}
|