start-command 0.10.0 → 0.13.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/.github/workflows/release.yml +52 -0
- package/CHANGELOG.md +24 -0
- package/README.md +12 -7
- package/bun.lock +5 -0
- package/experiments/test-command-stream-cjs.cjs +30 -0
- package/experiments/test-command-stream-wrapper.js +54 -0
- package/experiments/test-command-stream.mjs +56 -0
- package/package.json +4 -1
- package/src/bin/cli.js +175 -392
- package/src/lib/args-parser.js +41 -3
- package/src/lib/command-stream.js +258 -0
- package/src/lib/failure-handler.js +397 -0
- package/src/lib/isolation.js +97 -1
- package/test/args-parser.test.js +91 -1
- package/test/isolation.test.js +40 -0
- package/test/ssh-integration.test.js +328 -0
package/src/bin/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
hasSudoAccess,
|
|
29
29
|
getCurrentUserGroups,
|
|
30
30
|
} = require('../lib/user-manager');
|
|
31
|
+
const { handleFailure } = require('../lib/failure-handler');
|
|
31
32
|
|
|
32
33
|
// Configuration from environment variables
|
|
33
34
|
const config = {
|
|
@@ -50,6 +51,10 @@ const config = {
|
|
|
50
51
|
process.env.START_DISABLE_SUBSTITUTIONS === 'true',
|
|
51
52
|
// Custom substitutions file path
|
|
52
53
|
substitutionsPath: process.env.START_SUBSTITUTIONS_PATH || null,
|
|
54
|
+
// Use command-stream library for command execution (experimental)
|
|
55
|
+
useCommandStream:
|
|
56
|
+
process.env.START_USE_COMMAND_STREAM === '1' ||
|
|
57
|
+
process.env.START_USE_COMMAND_STREAM === 'true',
|
|
53
58
|
};
|
|
54
59
|
|
|
55
60
|
// Get all arguments passed after the command
|
|
@@ -86,6 +91,66 @@ if (args.length === 0) {
|
|
|
86
91
|
process.exit(0);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
// Parse wrapper options and command
|
|
95
|
+
let parsedArgs;
|
|
96
|
+
try {
|
|
97
|
+
parsedArgs = parseArgs(args);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`Error: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { wrapperOptions, command: parsedCommand } = parsedArgs;
|
|
104
|
+
|
|
105
|
+
// Check if no command was provided
|
|
106
|
+
if (!parsedCommand || parsedCommand.trim() === '') {
|
|
107
|
+
console.error('Error: No command provided');
|
|
108
|
+
printUsage();
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Process through substitution engine (unless disabled)
|
|
113
|
+
let command = parsedCommand;
|
|
114
|
+
let substitutionResult = null;
|
|
115
|
+
|
|
116
|
+
if (!config.disableSubstitutions) {
|
|
117
|
+
substitutionResult = processCommand(parsedCommand, {
|
|
118
|
+
customLinoPath: config.substitutionsPath,
|
|
119
|
+
verbose: config.verbose,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (substitutionResult.matched) {
|
|
123
|
+
command = substitutionResult.command;
|
|
124
|
+
if (config.verbose) {
|
|
125
|
+
console.log(`[Substitution] "${parsedCommand}" -> "${command}"`);
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Determine if we should use command-stream
|
|
132
|
+
// Can be enabled via --use-command-stream flag or START_USE_COMMAND_STREAM env var
|
|
133
|
+
const useCommandStream =
|
|
134
|
+
wrapperOptions.useCommandStream || config.useCommandStream;
|
|
135
|
+
|
|
136
|
+
// Main execution
|
|
137
|
+
(async () => {
|
|
138
|
+
// Check if running in isolation mode or with user isolation
|
|
139
|
+
if (hasIsolation(wrapperOptions) || wrapperOptions.user) {
|
|
140
|
+
await runWithIsolation(wrapperOptions, command, useCommandStream);
|
|
141
|
+
} else {
|
|
142
|
+
if (useCommandStream) {
|
|
143
|
+
await runDirectWithCommandStream(
|
|
144
|
+
command,
|
|
145
|
+
parsedCommand,
|
|
146
|
+
substitutionResult
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
await runDirect(command);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
|
|
89
154
|
/**
|
|
90
155
|
* Print version information
|
|
91
156
|
* @param {boolean} verbose - Whether to show verbose debugging info
|
|
@@ -224,15 +289,17 @@ function printUsage() {
|
|
|
224
289
|
$ <command> [args...]
|
|
225
290
|
|
|
226
291
|
Options:
|
|
227
|
-
--isolated, -i <env> Run in isolated environment (screen, tmux, docker)
|
|
292
|
+
--isolated, -i <env> Run in isolated environment (screen, tmux, docker, ssh)
|
|
228
293
|
--attached, -a Run in attached mode (foreground)
|
|
229
294
|
--detached, -d Run in detached mode (background)
|
|
230
295
|
--session, -s <name> Session name for isolation
|
|
231
296
|
--image <image> Docker image (required for docker isolation)
|
|
297
|
+
--endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
|
|
232
298
|
--isolated-user, -u [name] Create isolated user with same permissions
|
|
233
299
|
--keep-user Keep isolated user after command completes
|
|
234
300
|
--keep-alive, -k Keep isolation environment alive after command exits
|
|
235
301
|
--auto-remove-docker-container Auto-remove docker container after exit
|
|
302
|
+
--use-command-stream Use command-stream library for execution (experimental)
|
|
236
303
|
--version, -v Show version information
|
|
237
304
|
|
|
238
305
|
Examples:
|
|
@@ -241,10 +308,12 @@ Examples:
|
|
|
241
308
|
$ --isolated tmux -- bun start
|
|
242
309
|
$ -i screen -d bun start
|
|
243
310
|
$ --isolated docker --image oven/bun:latest -- bun install
|
|
311
|
+
$ --isolated ssh --endpoint user@remote.server -- ls -la
|
|
244
312
|
$ --isolated-user -- npm test # Create isolated user
|
|
245
313
|
$ -u myuser -- npm start # Custom username
|
|
246
314
|
$ -i screen --isolated-user -- npm test # Combine with process isolation
|
|
247
|
-
$ --isolated-user --keep-user -- npm start
|
|
315
|
+
$ --isolated-user --keep-user -- npm start
|
|
316
|
+
$ --use-command-stream echo "Hello" # Use command-stream library`);
|
|
248
317
|
console.log('');
|
|
249
318
|
console.log('Piping with $:');
|
|
250
319
|
console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
|
|
@@ -275,59 +344,13 @@ Examples:
|
|
|
275
344
|
);
|
|
276
345
|
}
|
|
277
346
|
|
|
278
|
-
// Parse wrapper options and command
|
|
279
|
-
let parsedArgs;
|
|
280
|
-
try {
|
|
281
|
-
parsedArgs = parseArgs(args);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
console.error(`Error: ${err.message}`);
|
|
284
|
-
process.exit(1);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const { wrapperOptions, command: parsedCommand } = parsedArgs;
|
|
288
|
-
|
|
289
|
-
// Check if no command was provided
|
|
290
|
-
if (!parsedCommand || parsedCommand.trim() === '') {
|
|
291
|
-
console.error('Error: No command provided');
|
|
292
|
-
printUsage();
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Process through substitution engine (unless disabled)
|
|
297
|
-
let command = parsedCommand;
|
|
298
|
-
let substitutionResult = null;
|
|
299
|
-
|
|
300
|
-
if (!config.disableSubstitutions) {
|
|
301
|
-
substitutionResult = processCommand(parsedCommand, {
|
|
302
|
-
customLinoPath: config.substitutionsPath,
|
|
303
|
-
verbose: config.verbose,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
if (substitutionResult.matched) {
|
|
307
|
-
command = substitutionResult.command;
|
|
308
|
-
if (config.verbose) {
|
|
309
|
-
console.log(`[Substitution] "${parsedCommand}" -> "${command}"`);
|
|
310
|
-
console.log('');
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Main execution
|
|
316
|
-
(async () => {
|
|
317
|
-
// Check if running in isolation mode or with user isolation
|
|
318
|
-
if (hasIsolation(wrapperOptions) || wrapperOptions.user) {
|
|
319
|
-
await runWithIsolation(wrapperOptions, command);
|
|
320
|
-
} else {
|
|
321
|
-
await runDirect(command);
|
|
322
|
-
}
|
|
323
|
-
})();
|
|
324
|
-
|
|
325
347
|
/**
|
|
326
348
|
* Run command in isolation mode
|
|
327
349
|
* @param {object} options - Wrapper options
|
|
328
350
|
* @param {string} cmd - Command to execute
|
|
351
|
+
* @param {boolean} useCommandStream - Whether to use command-stream for isolation
|
|
329
352
|
*/
|
|
330
|
-
async function runWithIsolation(options, cmd) {
|
|
353
|
+
async function runWithIsolation(options, cmd, useCommandStream = false) {
|
|
331
354
|
const environment = options.isolated;
|
|
332
355
|
const mode = getEffectiveMode(options);
|
|
333
356
|
const startTime = getTimestamp();
|
|
@@ -404,9 +427,15 @@ async function runWithIsolation(options, cmd) {
|
|
|
404
427
|
if (options.image) {
|
|
405
428
|
console.log(`[Isolation] Image: ${options.image}`);
|
|
406
429
|
}
|
|
430
|
+
if (options.endpoint) {
|
|
431
|
+
console.log(`[Isolation] Endpoint: ${options.endpoint}`);
|
|
432
|
+
}
|
|
407
433
|
if (createdUser) {
|
|
408
434
|
console.log(`[Isolation] User: ${createdUser} (isolated)`);
|
|
409
435
|
}
|
|
436
|
+
if (useCommandStream) {
|
|
437
|
+
console.log(`[Isolation] Using command-stream library`);
|
|
438
|
+
}
|
|
410
439
|
console.log('');
|
|
411
440
|
|
|
412
441
|
// Create log content
|
|
@@ -423,10 +452,13 @@ async function runWithIsolation(options, cmd) {
|
|
|
423
452
|
let result;
|
|
424
453
|
|
|
425
454
|
if (environment) {
|
|
426
|
-
// Run in isolation backend (screen, tmux, docker)
|
|
455
|
+
// Run in isolation backend (screen, tmux, docker, ssh)
|
|
456
|
+
// Note: Isolation backends currently use native spawn/execSync
|
|
457
|
+
// Future: Add command-stream support with raw() function for multiplexers
|
|
427
458
|
result = await runIsolated(environment, cmd, {
|
|
428
459
|
session: options.session,
|
|
429
460
|
image: options.image,
|
|
461
|
+
endpoint: options.endpoint,
|
|
430
462
|
detached: mode === 'detached',
|
|
431
463
|
user: createdUser,
|
|
432
464
|
keepAlive: options.keepAlive,
|
|
@@ -481,7 +513,7 @@ async function runWithIsolation(options, cmd) {
|
|
|
481
513
|
}
|
|
482
514
|
|
|
483
515
|
/**
|
|
484
|
-
* Run command directly (without isolation)
|
|
516
|
+
* Run command directly (without isolation) - original synchronous version
|
|
485
517
|
* @param {string} cmd - Command to execute
|
|
486
518
|
*/
|
|
487
519
|
function runDirect(cmd) {
|
|
@@ -576,7 +608,7 @@ function runDirect(cmd) {
|
|
|
576
608
|
|
|
577
609
|
// If command failed, try to auto-report
|
|
578
610
|
if (exitCode !== 0) {
|
|
579
|
-
handleFailure(commandName, cmd, exitCode, logFilePath
|
|
611
|
+
handleFailure(config, commandName, cmd, exitCode, logFilePath);
|
|
580
612
|
}
|
|
581
613
|
|
|
582
614
|
process.exit(exitCode);
|
|
@@ -605,379 +637,130 @@ function runDirect(cmd) {
|
|
|
605
637
|
console.log(`Exit code: 1`);
|
|
606
638
|
console.log(`Log saved: ${logFilePath}`);
|
|
607
639
|
|
|
608
|
-
handleFailure(commandName, cmd, 1, logFilePath
|
|
640
|
+
handleFailure(config, commandName, cmd, 1, logFilePath);
|
|
609
641
|
|
|
610
642
|
process.exit(1);
|
|
611
643
|
});
|
|
612
644
|
}
|
|
613
645
|
|
|
614
646
|
/**
|
|
615
|
-
*
|
|
616
|
-
* @
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const timestamp = Date.now();
|
|
620
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
621
|
-
return `start-command-${timestamp}-${random}.log`;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Handle command failure - detect repository, upload log, create issue
|
|
647
|
+
* Run command directly using command-stream library (experimental)
|
|
648
|
+
* @param {string} cmd - Command to execute
|
|
649
|
+
* @param {string} parsedCmd - Original parsed command
|
|
650
|
+
* @param {object} subResult - Result from substitution engine
|
|
626
651
|
*/
|
|
627
|
-
function
|
|
628
|
-
|
|
652
|
+
async function runDirectWithCommandStream(cmd, parsedCmd, subResult) {
|
|
653
|
+
// Lazy load command-stream
|
|
654
|
+
const { getCommandStream } = require('../lib/command-stream');
|
|
655
|
+
const { $, raw } = await getCommandStream();
|
|
629
656
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
if (config.verbose) {
|
|
633
|
-
console.log('Auto-issue creation disabled via START_DISABLE_AUTO_ISSUE');
|
|
634
|
-
}
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
657
|
+
// Get the command name (first word of the actual command to execute)
|
|
658
|
+
const commandName = cmd.split(' ')[0];
|
|
637
659
|
|
|
638
|
-
//
|
|
639
|
-
const
|
|
660
|
+
// Determine the shell based on the platform
|
|
661
|
+
const isWindows = process.platform === 'win32';
|
|
662
|
+
const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
640
663
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
664
|
+
// Setup logging
|
|
665
|
+
const logDir = config.logDir || os.tmpdir();
|
|
666
|
+
const logFilename = generateLogFilename();
|
|
667
|
+
const logFilePath = path.join(logDir, logFilename);
|
|
645
668
|
|
|
646
|
-
|
|
669
|
+
let logContent = '';
|
|
670
|
+
const startTime = getTimestamp();
|
|
647
671
|
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
);
|
|
653
|
-
console.log('Run "gh auth login" to enable automatic issue creation');
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
672
|
+
// Get runtime information
|
|
673
|
+
const runtime = typeof Bun !== 'undefined' ? 'Bun' : 'Node.js';
|
|
674
|
+
const runtimeVersion =
|
|
675
|
+
typeof Bun !== 'undefined' ? Bun.version : process.version;
|
|
656
676
|
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (logUrl) {
|
|
666
|
-
console.log(`Log uploaded: ${logUrl}`);
|
|
667
|
-
}
|
|
677
|
+
// Log header
|
|
678
|
+
logContent += `=== Start Command Log ===\n`;
|
|
679
|
+
logContent += `Timestamp: ${startTime}\n`;
|
|
680
|
+
logContent += `Execution Mode: command-stream\n`;
|
|
681
|
+
if (subResult && subResult.matched) {
|
|
682
|
+
logContent += `Original Input: ${parsedCmd}\n`;
|
|
683
|
+
logContent += `Substituted Command: ${cmd}\n`;
|
|
684
|
+
logContent += `Pattern Matched: ${subResult.rule.pattern}\n`;
|
|
668
685
|
} else {
|
|
669
|
-
|
|
670
|
-
console.log('Install with: bun install -g gh-upload-log');
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Check if we can create issues in this repository
|
|
674
|
-
if (!canCreateIssue(repoInfo.owner, repoInfo.repo)) {
|
|
675
|
-
console.log('Cannot create issue in repository - skipping issue creation');
|
|
676
|
-
return;
|
|
686
|
+
logContent += `Command: ${cmd}\n`;
|
|
677
687
|
}
|
|
688
|
+
logContent += `Shell: ${shell}\n`;
|
|
689
|
+
logContent += `Platform: ${process.platform}\n`;
|
|
690
|
+
logContent += `${runtime} Version: ${runtimeVersion}\n`;
|
|
691
|
+
logContent += `Working Directory: ${process.cwd()}\n`;
|
|
692
|
+
logContent += `${'='.repeat(50)}\n\n`;
|
|
678
693
|
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
logPath
|
|
686
|
-
);
|
|
687
|
-
if (issueUrl) {
|
|
688
|
-
console.log(`Issue created: ${issueUrl}`);
|
|
694
|
+
// Print start message to console
|
|
695
|
+
if (subResult && subResult.matched) {
|
|
696
|
+
console.log(`[${startTime}] Input: ${parsedCmd}`);
|
|
697
|
+
console.log(`[${startTime}] Executing: ${cmd}`);
|
|
698
|
+
} else {
|
|
699
|
+
console.log(`[${startTime}] Starting: ${cmd}`);
|
|
689
700
|
}
|
|
690
|
-
|
|
701
|
+
console.log('[command-stream] Using command-stream library');
|
|
702
|
+
console.log('');
|
|
691
703
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const isWindows = process.platform === 'win32';
|
|
704
|
+
// Execute the command using command-stream with real-time output
|
|
705
|
+
// Using mirror: true to show output in real-time, capture: true to collect it
|
|
706
|
+
// Using raw() to avoid auto-escaping that might interfere with complex shell commands
|
|
707
|
+
const $cmd = $({ mirror: true, capture: true });
|
|
697
708
|
|
|
709
|
+
let exitCode = 0;
|
|
698
710
|
try {
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}).trim();
|
|
708
|
-
} catch {
|
|
709
|
-
return null;
|
|
711
|
+
// Use raw() to pass the command without auto-escaping
|
|
712
|
+
// This is important for complex commands with pipes, redirects, etc.
|
|
713
|
+
const result = await $cmd`${raw(cmd)}`;
|
|
714
|
+
exitCode = result.code || 0;
|
|
715
|
+
|
|
716
|
+
// Collect output for log
|
|
717
|
+
if (result.stdout) {
|
|
718
|
+
logContent += result.stdout;
|
|
710
719
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Handle Windows where command that returns multiple lines
|
|
717
|
-
if (isWindows && cmdPath.includes('\n')) {
|
|
718
|
-
cmdPath = cmdPath.split('\n')[0].trim();
|
|
720
|
+
if (result.stderr) {
|
|
721
|
+
logContent += result.stderr;
|
|
719
722
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
encoding: 'utf8',
|
|
726
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
727
|
-
}).trim();
|
|
728
|
-
} catch {
|
|
729
|
-
return null;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Get the npm bin directory (parent of node_modules)
|
|
733
|
-
const npmBinPath = `${path.dirname(npmGlobalPath)}/bin`;
|
|
734
|
-
|
|
735
|
-
// Check if the command is located in the npm bin directory or node_modules
|
|
736
|
-
let packageName = null;
|
|
737
|
-
let isNpmPackage = false;
|
|
738
|
-
|
|
739
|
-
try {
|
|
740
|
-
// Try to resolve the symlink to find the actual path
|
|
741
|
-
const realPath = fs.realpathSync(cmdPath);
|
|
742
|
-
|
|
743
|
-
// Check if the real path is within node_modules
|
|
744
|
-
if (realPath.includes('node_modules')) {
|
|
745
|
-
isNpmPackage = true;
|
|
746
|
-
const npmPathMatch = realPath.match(/node_modules\/([^/]+)/);
|
|
747
|
-
if (npmPathMatch) {
|
|
748
|
-
packageName = npmPathMatch[1];
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Also check if the command path itself is in npm's bin directory
|
|
753
|
-
if (!isNpmPackage && cmdPath.includes(npmBinPath)) {
|
|
754
|
-
isNpmPackage = true;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Try to read the bin script to extract package info
|
|
758
|
-
if (!packageName) {
|
|
759
|
-
const binContent = fs.readFileSync(cmdPath, 'utf8');
|
|
760
|
-
|
|
761
|
-
// Check if this is a Node.js script
|
|
762
|
-
if (
|
|
763
|
-
binContent.startsWith('#!/usr/bin/env node') ||
|
|
764
|
-
binContent.includes('node_modules')
|
|
765
|
-
) {
|
|
766
|
-
isNpmPackage = true;
|
|
767
|
-
|
|
768
|
-
// Look for package path in the script
|
|
769
|
-
const packagePathMatch = binContent.match(/node_modules\/([^/'"]+)/);
|
|
770
|
-
if (packagePathMatch) {
|
|
771
|
-
packageName = packagePathMatch[1];
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
} catch {
|
|
776
|
-
// Could not read/resolve command - not an npm package
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// If we couldn't confirm this is an npm package, don't proceed
|
|
781
|
-
if (!isNpmPackage) {
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// If we couldn't find the package name from the path, use the command name
|
|
786
|
-
if (!packageName) {
|
|
787
|
-
packageName = cmdName;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Try to get repository URL from npm
|
|
791
|
-
try {
|
|
792
|
-
const npmInfo = execSync(
|
|
793
|
-
`npm view ${packageName} repository.url 2>/dev/null`,
|
|
794
|
-
{
|
|
795
|
-
encoding: 'utf8',
|
|
796
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
797
|
-
}
|
|
798
|
-
).trim();
|
|
799
|
-
|
|
800
|
-
if (npmInfo) {
|
|
801
|
-
// Parse git URL to extract owner and repo
|
|
802
|
-
const parsed = parseGitUrl(npmInfo);
|
|
803
|
-
if (parsed) {
|
|
804
|
-
return parsed;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
} catch {
|
|
808
|
-
// npm view failed, package might not exist or have no repository
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Try to get homepage or bugs URL as fallback
|
|
812
|
-
try {
|
|
813
|
-
const bugsUrl = execSync(`npm view ${packageName} bugs.url 2>/dev/null`, {
|
|
814
|
-
encoding: 'utf8',
|
|
815
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
816
|
-
}).trim();
|
|
817
|
-
|
|
818
|
-
if (bugsUrl && bugsUrl.includes('github.com')) {
|
|
819
|
-
const parsed = parseGitUrl(bugsUrl);
|
|
820
|
-
if (parsed) {
|
|
821
|
-
return parsed;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
} catch {
|
|
825
|
-
// Fallback also failed
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
return null;
|
|
829
|
-
} catch {
|
|
830
|
-
return null;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* Parse a git URL to extract owner, repo, and normalized URL
|
|
836
|
-
*/
|
|
837
|
-
function parseGitUrl(url) {
|
|
838
|
-
if (!url) {
|
|
839
|
-
return null;
|
|
723
|
+
} catch (err) {
|
|
724
|
+
exitCode = err.code || 1;
|
|
725
|
+
const errorMessage = `Error executing command: ${err.message}`;
|
|
726
|
+
logContent += `\n${errorMessage}\n`;
|
|
727
|
+
console.error(`\n${errorMessage}`);
|
|
840
728
|
}
|
|
841
729
|
|
|
842
|
-
|
|
843
|
-
// git+https://github.com/owner/repo.git
|
|
844
|
-
// git://github.com/owner/repo.git
|
|
845
|
-
// https://github.com/owner/repo.git
|
|
846
|
-
// https://github.com/owner/repo
|
|
847
|
-
// git@github.com:owner/repo.git
|
|
848
|
-
// https://github.com/owner/repo/issues
|
|
849
|
-
|
|
850
|
-
const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
851
|
-
if (match) {
|
|
852
|
-
const owner = match[1];
|
|
853
|
-
const repo = match[2].replace(/\.git$/, '');
|
|
854
|
-
return {
|
|
855
|
-
owner,
|
|
856
|
-
repo,
|
|
857
|
-
url: `https://github.com/${owner}/${repo}`,
|
|
858
|
-
};
|
|
859
|
-
}
|
|
730
|
+
const endTime = getTimestamp();
|
|
860
731
|
|
|
861
|
-
|
|
862
|
-
}
|
|
732
|
+
// Log footer
|
|
733
|
+
logContent += `\n${'='.repeat(50)}\n`;
|
|
734
|
+
logContent += `Finished: ${endTime}\n`;
|
|
735
|
+
logContent += `Exit Code: ${exitCode}\n`;
|
|
863
736
|
|
|
864
|
-
|
|
865
|
-
* Check if GitHub CLI is authenticated
|
|
866
|
-
*/
|
|
867
|
-
function isGhAuthenticated() {
|
|
868
|
-
try {
|
|
869
|
-
execSync('gh auth status', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
870
|
-
return true;
|
|
871
|
-
} catch {
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Check if gh-upload-log is available
|
|
878
|
-
*/
|
|
879
|
-
function isGhUploadLogAvailable() {
|
|
880
|
-
const isWindows = process.platform === 'win32';
|
|
737
|
+
// Write log file
|
|
881
738
|
try {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
} catch {
|
|
886
|
-
return false;
|
|
739
|
+
fs.writeFileSync(logFilePath, logContent, 'utf8');
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
887
742
|
}
|
|
888
|
-
}
|
|
889
743
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const result = execSync(`gh-upload-log "${logPath}" --public`, {
|
|
896
|
-
encoding: 'utf8',
|
|
897
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
// Extract URL from output
|
|
901
|
-
const urlMatch = result.match(/https:\/\/gist\.github\.com\/[^\s]+/);
|
|
902
|
-
if (urlMatch) {
|
|
903
|
-
return urlMatch[0];
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Try other URL patterns
|
|
907
|
-
const repoUrlMatch = result.match(/https:\/\/github\.com\/[^\s]+/);
|
|
908
|
-
if (repoUrlMatch) {
|
|
909
|
-
return repoUrlMatch[0];
|
|
910
|
-
}
|
|
744
|
+
// Print footer to console
|
|
745
|
+
console.log('');
|
|
746
|
+
console.log(`[${endTime}] Finished`);
|
|
747
|
+
console.log(`Exit code: ${exitCode}`);
|
|
748
|
+
console.log(`Log saved: ${logFilePath}`);
|
|
911
749
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
return null;
|
|
750
|
+
// If command failed, try to auto-report
|
|
751
|
+
if (exitCode !== 0) {
|
|
752
|
+
handleFailure(config, commandName, cmd, exitCode, logFilePath);
|
|
916
753
|
}
|
|
917
|
-
}
|
|
918
754
|
|
|
919
|
-
|
|
920
|
-
* Check if we can create an issue in a repository
|
|
921
|
-
*/
|
|
922
|
-
function canCreateIssue(owner, repo) {
|
|
923
|
-
try {
|
|
924
|
-
// Check if the repository exists and we have access
|
|
925
|
-
execSync(`gh repo view ${owner}/${repo} --json name`, {
|
|
926
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
927
|
-
});
|
|
928
|
-
return true;
|
|
929
|
-
} catch {
|
|
930
|
-
return false;
|
|
931
|
-
}
|
|
755
|
+
process.exit(exitCode);
|
|
932
756
|
}
|
|
933
757
|
|
|
934
758
|
/**
|
|
935
|
-
*
|
|
759
|
+
* Generate unique log filename for direct execution
|
|
760
|
+
* @returns {string} Log filename
|
|
936
761
|
*/
|
|
937
|
-
function
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
// Get runtime information
|
|
942
|
-
const runtime = typeof Bun !== 'undefined' ? 'Bun' : 'Node.js';
|
|
943
|
-
const runtimeVersion =
|
|
944
|
-
typeof Bun !== 'undefined' ? Bun.version : process.version;
|
|
945
|
-
|
|
946
|
-
let body = `## Command Execution Failure Report\n\n`;
|
|
947
|
-
body += `**Command:** \`${fullCommand}\`\n\n`;
|
|
948
|
-
body += `**Exit Code:** ${exitCode}\n\n`;
|
|
949
|
-
body += `**Timestamp:** ${getTimestamp()}\n\n`;
|
|
950
|
-
body += `### System Information\n\n`;
|
|
951
|
-
body += `- **Platform:** ${process.platform}\n`;
|
|
952
|
-
body += `- **OS Release:** ${os.release()}\n`;
|
|
953
|
-
body += `- **${runtime} Version:** ${runtimeVersion}\n`;
|
|
954
|
-
body += `- **Architecture:** ${process.arch}\n\n`;
|
|
955
|
-
|
|
956
|
-
if (logUrl) {
|
|
957
|
-
body += `### Log File\n\n`;
|
|
958
|
-
body += `Full log available at: ${logUrl}\n\n`;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
body += `---\n`;
|
|
962
|
-
body += `*This issue was automatically created by [start-command](https://github.com/link-foundation/start)*\n`;
|
|
963
|
-
|
|
964
|
-
const result = execSync(
|
|
965
|
-
`gh issue create --repo ${repoInfo.owner}/${repoInfo.repo} --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`,
|
|
966
|
-
{
|
|
967
|
-
encoding: 'utf8',
|
|
968
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
969
|
-
}
|
|
970
|
-
);
|
|
971
|
-
|
|
972
|
-
// Extract issue URL from output
|
|
973
|
-
const urlMatch = result.match(/https:\/\/github\.com\/[^\s]+/);
|
|
974
|
-
if (urlMatch) {
|
|
975
|
-
return urlMatch[0];
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
return null;
|
|
979
|
-
} catch (err) {
|
|
980
|
-
console.log(`Warning: Issue creation failed - ${err.message}`);
|
|
981
|
-
return null;
|
|
982
|
-
}
|
|
762
|
+
function generateLogFilename() {
|
|
763
|
+
const timestamp = Date.now();
|
|
764
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
765
|
+
return `start-command-${timestamp}-${random}.log`;
|
|
983
766
|
}
|