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.
Files changed (38) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.changeset/isolation-support.md +30 -0
  4. package/.github/workflows/release.yml +292 -0
  5. package/.husky/pre-commit +1 -0
  6. package/.prettierignore +6 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +24 -0
  9. package/LICENSE +24 -0
  10. package/README.md +249 -0
  11. package/REQUIREMENTS.md +229 -0
  12. package/bun.lock +453 -0
  13. package/bunfig.toml +3 -0
  14. package/eslint.config.mjs +122 -0
  15. package/experiments/debug-regex.js +49 -0
  16. package/experiments/isolation-design.md +142 -0
  17. package/experiments/test-cli.sh +42 -0
  18. package/experiments/test-substitution.js +143 -0
  19. package/package.json +63 -0
  20. package/scripts/changeset-version.mjs +38 -0
  21. package/scripts/check-file-size.mjs +103 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +89 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +219 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/publish-to-npm.mjs +129 -0
  28. package/scripts/setup-npm.mjs +37 -0
  29. package/scripts/validate-changeset.mjs +107 -0
  30. package/scripts/version-and-commit.mjs +237 -0
  31. package/src/bin/cli.js +670 -0
  32. package/src/lib/args-parser.js +259 -0
  33. package/src/lib/isolation.js +419 -0
  34. package/src/lib/substitution.js +323 -0
  35. package/src/lib/substitutions.lino +308 -0
  36. package/test/args-parser.test.js +389 -0
  37. package/test/isolation.test.js +248 -0
  38. 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
+ }