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/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, logContent);
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, logContent);
640
+ handleFailure(config, commandName, cmd, 1, logFilePath);
609
641
 
610
642
  process.exit(1);
611
643
  });
612
644
  }
613
645
 
614
646
  /**
615
- * Generate unique log filename for direct execution
616
- * @returns {string} Log filename
617
- */
618
- function generateLogFilename() {
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 handleFailure(cmdName, fullCommand, exitCode, logPath) {
628
- console.log('');
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
- // Check if auto-issue is disabled
631
- if (config.disableAutoIssue) {
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
- // Try to detect repository for the command
639
- const repoInfo = detectRepository(cmdName);
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
- if (!repoInfo) {
642
- console.log('Repository not detected - automatic issue creation skipped');
643
- return;
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
- console.log(`Detected repository: ${repoInfo.url}`);
669
+ let logContent = '';
670
+ const startTime = getTimestamp();
647
671
 
648
- // Check if gh CLI is available and authenticated
649
- if (!isGhAuthenticated()) {
650
- console.log(
651
- 'GitHub CLI not authenticated - automatic issue creation skipped'
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
- // Try to upload log
658
- let logUrl = null;
659
- if (config.disableLogUpload) {
660
- if (config.verbose) {
661
- console.log('Log upload disabled via START_DISABLE_LOG_UPLOAD');
662
- }
663
- } else if (isGhUploadLogAvailable()) {
664
- logUrl = uploadLog(logPath);
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
- console.log('gh-upload-log not installed - log upload skipped');
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
- // Create issue
680
- const issueUrl = createIssue(
681
- repoInfo,
682
- fullCommand,
683
- exitCode,
684
- logUrl,
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
- * Detect repository URL for a command (currently supports NPM global packages)
694
- */
695
- function detectRepository(cmdName) {
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
- // Find command location
700
- const whichCmd = isWindows ? 'where' : 'which';
701
- let cmdPath;
702
-
703
- try {
704
- cmdPath = execSync(`${whichCmd} ${cmdName}`, {
705
- encoding: 'utf8',
706
- stdio: ['pipe', 'pipe', 'pipe'],
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
- if (!cmdPath) {
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
- // Check if it's in npm global modules
722
- let npmGlobalPath;
723
- try {
724
- npmGlobalPath = execSync('npm root -g', {
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
- // Handle various git URL formats
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
- return null;
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
- const whichCmd = isWindows ? 'where' : 'which';
883
- execSync(`${whichCmd} gh-upload-log`, { stdio: ['pipe', 'pipe', 'pipe'] });
884
- return true;
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
- * Upload log file using gh-upload-log
892
- */
893
- function uploadLog(logPath) {
894
- try {
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
- return null;
913
- } catch (err) {
914
- console.log(`Warning: Log upload failed - ${err.message}`);
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
- * Create an issue in the repository
759
+ * Generate unique log filename for direct execution
760
+ * @returns {string} Log filename
936
761
  */
937
- function createIssue(repoInfo, fullCommand, exitCode, logUrl) {
938
- try {
939
- const title = `Command failed with exit code ${exitCode}: ${fullCommand.substring(0, 50)}${fullCommand.length > 50 ? '...' : ''}`;
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
  }