start-command 0.7.4 → 0.7.6

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.
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Experiment: Test screen output capture behavior
4
+ *
5
+ * This experiment tests different approaches to capture output from GNU screen
6
+ * sessions, specifically addressing issue #25 where output is lost on macOS
7
+ * with screen 4.00.03.
8
+ */
9
+
10
+ const { execSync, spawn, spawnSync } = require('child_process');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+
15
+ // Check if screen is available
16
+ function isScreenAvailable() {
17
+ try {
18
+ execSync('which screen', { stdio: ['pipe', 'pipe', 'pipe'] });
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ // Get screen version
26
+ function getScreenVersion() {
27
+ try {
28
+ const output = execSync('screen --version', {
29
+ encoding: 'utf8',
30
+ stdio: ['pipe', 'pipe', 'pipe'],
31
+ });
32
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
33
+ if (match) {
34
+ return {
35
+ major: parseInt(match[1], 10),
36
+ minor: parseInt(match[2], 10),
37
+ patch: parseInt(match[3], 10),
38
+ raw: output.trim(),
39
+ };
40
+ }
41
+ } catch {
42
+ // Ignore
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Check if -Logfile is supported (screen >= 4.5.1)
48
+ function supportsLogfileOption(version) {
49
+ if (!version) {
50
+ return false;
51
+ }
52
+ if (version.major > 4) {
53
+ return true;
54
+ }
55
+ if (version.major < 4) {
56
+ return false;
57
+ }
58
+ if (version.minor > 5) {
59
+ return true;
60
+ }
61
+ if (version.minor < 5) {
62
+ return false;
63
+ }
64
+ return version.patch >= 1;
65
+ }
66
+
67
+ // Test 1: Direct screen invocation (current approach for TTY)
68
+ async function testDirectScreen(command) {
69
+ console.log('\n=== Test 1: Direct screen invocation (TTY mode) ===');
70
+ const sessionName = `test-direct-${Date.now()}`;
71
+ const shell = process.env.SHELL || '/bin/sh';
72
+
73
+ console.log(`Command: screen -S ${sessionName} ${shell} -c '${command}'`);
74
+ console.log('Note: This approach loses output for quick commands');
75
+
76
+ // This is what happens currently with TTY
77
+ return new Promise((resolve) => {
78
+ const child = spawn('screen', ['-S', sessionName, shell, '-c', command], {
79
+ stdio: 'inherit',
80
+ });
81
+
82
+ child.on('exit', (code) => {
83
+ console.log(`Exit code: ${code}`);
84
+ resolve({ success: code === 0, output: '(output not captured)' });
85
+ });
86
+
87
+ child.on('error', (err) => {
88
+ console.error(`Error: ${err.message}`);
89
+ resolve({ success: false, output: '' });
90
+ });
91
+ });
92
+ }
93
+
94
+ // Test 2: Detached screen with log file (current approach for no-TTY)
95
+ async function testDetachedWithLog(command) {
96
+ console.log('\n=== Test 2: Detached screen with log capture ===');
97
+ const sessionName = `test-detached-${Date.now()}`;
98
+ const shell = process.env.SHELL || '/bin/sh';
99
+ const logFile = path.join(os.tmpdir(), `screen-test-${sessionName}.log`);
100
+
101
+ const version = getScreenVersion();
102
+ const useNativeLogging = supportsLogfileOption(version);
103
+
104
+ console.log(`Screen version: ${version ? version.raw : 'unknown'}`);
105
+ console.log(`Supports -Logfile: ${useNativeLogging}`);
106
+
107
+ let screenArgs;
108
+ let effectiveCommand = command;
109
+
110
+ if (useNativeLogging) {
111
+ // Modern screen
112
+ screenArgs = [
113
+ '-dmS',
114
+ sessionName,
115
+ '-L',
116
+ '-Logfile',
117
+ logFile,
118
+ shell,
119
+ '-c',
120
+ command,
121
+ ];
122
+ } else {
123
+ // Older screen - use tee fallback
124
+ effectiveCommand = `(${command}) 2>&1 | tee "${logFile}"`;
125
+ screenArgs = ['-dmS', sessionName, shell, '-c', effectiveCommand];
126
+ }
127
+
128
+ console.log(`Command: screen ${screenArgs.join(' ')}`);
129
+
130
+ return new Promise((resolve) => {
131
+ try {
132
+ const result = spawnSync('screen', screenArgs, { stdio: 'inherit' });
133
+
134
+ if (result.error) {
135
+ console.error(`Error: ${result.error.message}`);
136
+ resolve({ success: false, output: '' });
137
+ return;
138
+ }
139
+
140
+ // Poll for session completion
141
+ const checkInterval = 100;
142
+ const maxWait = 10000;
143
+ let waited = 0;
144
+
145
+ const checkCompletion = () => {
146
+ try {
147
+ const sessions = execSync('screen -ls', {
148
+ encoding: 'utf8',
149
+ stdio: ['pipe', 'pipe', 'pipe'],
150
+ });
151
+
152
+ if (!sessions.includes(sessionName)) {
153
+ // Session ended
154
+ let output = '';
155
+ try {
156
+ output = fs.readFileSync(logFile, 'utf8');
157
+ console.log(`Captured output: "${output.trim()}"`);
158
+ fs.unlinkSync(logFile);
159
+ } catch {
160
+ console.log('Log file not found or empty');
161
+ }
162
+ resolve({ success: true, output });
163
+ return;
164
+ }
165
+
166
+ waited += checkInterval;
167
+ if (waited >= maxWait) {
168
+ resolve({ success: false, output: 'timeout' });
169
+ return;
170
+ }
171
+
172
+ setTimeout(checkCompletion, checkInterval);
173
+ } catch {
174
+ let output = '';
175
+ try {
176
+ output = fs.readFileSync(logFile, 'utf8');
177
+ console.log(`Captured output: "${output.trim()}"`);
178
+ fs.unlinkSync(logFile);
179
+ } catch {
180
+ // Ignore
181
+ }
182
+ resolve({ success: true, output });
183
+ }
184
+ };
185
+
186
+ setTimeout(checkCompletion, checkInterval);
187
+ } catch (err) {
188
+ console.error(`Error: ${err.message}`);
189
+ resolve({ success: false, output: '' });
190
+ }
191
+ });
192
+ }
193
+
194
+ // Test 3: Script command for output capture (alternative approach)
195
+ async function testScriptCapture(command) {
196
+ console.log('\n=== Test 3: Using script command for output capture ===');
197
+ const logFile = path.join(os.tmpdir(), `script-test-${Date.now()}.log`);
198
+ const shell = process.env.SHELL || '/bin/sh';
199
+
200
+ // Use 'script' command which is available on both macOS and Linux
201
+ // script -q logfile command (macOS/BSD)
202
+ // script -q -c command logfile (Linux)
203
+ const isMac = process.platform === 'darwin';
204
+
205
+ let scriptArgs;
206
+ if (isMac) {
207
+ scriptArgs = ['-q', logFile, shell, '-c', command];
208
+ } else {
209
+ scriptArgs = ['-q', '-c', `${shell} -c '${command}'`, logFile];
210
+ }
211
+
212
+ console.log(`Command: script ${scriptArgs.join(' ')}`);
213
+
214
+ return new Promise((resolve) => {
215
+ const child = spawn('script', scriptArgs, {
216
+ stdio: 'inherit',
217
+ });
218
+
219
+ child.on('exit', (code) => {
220
+ let output = '';
221
+ try {
222
+ output = fs.readFileSync(logFile, 'utf8');
223
+ console.log(`Captured output: "${output.trim()}"`);
224
+ fs.unlinkSync(logFile);
225
+ } catch {
226
+ console.log('Log file not found');
227
+ }
228
+ resolve({ success: code === 0, output });
229
+ });
230
+
231
+ child.on('error', (err) => {
232
+ console.error(`Error: ${err.message}`);
233
+ resolve({ success: false, output: '' });
234
+ });
235
+ });
236
+ }
237
+
238
+ // Main
239
+ async function main() {
240
+ console.log('Screen Output Capture Experiment');
241
+ console.log('=================================');
242
+ console.log(`Platform: ${process.platform}`);
243
+ console.log(
244
+ `TTY: stdin=${process.stdin.isTTY}, stdout=${process.stdout.isTTY}`
245
+ );
246
+
247
+ if (!isScreenAvailable()) {
248
+ console.log('Screen is not installed. Exiting.');
249
+ return;
250
+ }
251
+
252
+ const version = getScreenVersion();
253
+ console.log(`Screen version: ${version ? version.raw : 'unknown'}`);
254
+
255
+ const testCommand = 'echo "hello from screen"';
256
+
257
+ // Test 2 is the recommended approach
258
+ const result = await testDetachedWithLog(testCommand);
259
+ console.log('\n=== Summary ===');
260
+ console.log(
261
+ `Test 2 (detached with log): Success=${result.success}, Output captured=${result.output.includes('hello')}`
262
+ );
263
+ }
264
+
265
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/bin/cli.js CHANGED
@@ -237,6 +237,16 @@ function printUsage() {
237
237
  console.log(' $ -i screen -d bun start');
238
238
  console.log(' $ --isolated docker --image oven/bun:latest -- bun install');
239
239
  console.log('');
240
+ console.log('Piping with $:');
241
+ console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
242
+ console.log(
243
+ ' $ \'echo "hi" | agent\' # Alternative - quote entire pipeline'
244
+ );
245
+ console.log('');
246
+ console.log('Quoting for special characters:');
247
+ console.log(" $ 'npm test && npm build' # Wrap for logical operators");
248
+ console.log(" $ 'cat file > output.txt' # Wrap for redirections");
249
+ console.log('');
240
250
  console.log('Features:');
241
251
  console.log(' - Logs all output to temporary directory');
242
252
  console.log(' - Displays timestamps and exit codes');
@@ -342,50 +342,22 @@ function runInScreen(command, options = {}) {
342
342
  message: `Command started in detached screen session: ${sessionName}\nReattach with: screen -r ${sessionName}`,
343
343
  });
344
344
  } else {
345
- // Attached mode: need TTY for screen to work properly
346
-
347
- // Check if we have a TTY
348
- if (hasTTY()) {
349
- // We have a TTY, use direct screen invocation
350
- const screenArgs = ['-S', sessionName, shell, shellArg, command];
351
-
352
- if (DEBUG) {
353
- console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
354
- }
355
-
356
- return new Promise((resolve) => {
357
- const child = spawn('screen', screenArgs, {
358
- stdio: 'inherit',
359
- });
360
-
361
- child.on('exit', (code) => {
362
- resolve({
363
- success: code === 0,
364
- sessionName,
365
- message: `Screen session "${sessionName}" exited with code ${code}`,
366
- exitCode: code,
367
- });
368
- });
369
-
370
- child.on('error', (err) => {
371
- resolve({
372
- success: false,
373
- sessionName,
374
- message: `Failed to start screen: ${err.message}`,
375
- });
376
- });
377
- });
378
- } else {
379
- // No TTY available - use detached mode with log capture
380
- // This allows screen to run without a terminal while still capturing output
381
- if (DEBUG) {
382
- console.log(
383
- `[DEBUG] No TTY available, using detached mode with log capture`
384
- );
385
- }
386
-
387
- return runScreenWithLogCapture(command, sessionName, shellInfo);
345
+ // Attached mode: always use detached mode with log capture
346
+ // This ensures output is captured and displayed correctly, even for quick commands
347
+ // that would otherwise have their output lost in a rapidly-terminating screen session.
348
+ // Direct screen invocation (screen -S session shell -c command) loses output because:
349
+ // 1. Screen creates a virtual terminal for the session
350
+ // 2. Command output goes to that virtual terminal
351
+ // 3. When the command exits quickly, screen shows "[screen is terminating]"
352
+ // 4. The virtual terminal is destroyed and output is lost
353
+ // See issue #25 for details: https://github.com/link-foundation/start/issues/25
354
+ if (DEBUG) {
355
+ console.log(
356
+ `[DEBUG] Using detached mode with log capture for reliable output`
357
+ );
388
358
  }
359
+
360
+ return runScreenWithLogCapture(command, sessionName, shellInfo);
389
361
  }
390
362
  } catch (err) {
391
363
  return Promise.resolve({
@@ -380,6 +380,33 @@ describe('Isolation Runner with Available Backends', () => {
380
380
  );
381
381
  }
382
382
  });
383
+
384
+ it('should always return output property in attached mode (issue #25 fix verification)', async () => {
385
+ if (!isCommandAvailable('screen')) {
386
+ console.log(' Skipping: screen not installed');
387
+ return;
388
+ }
389
+
390
+ // This test verifies that attached mode always uses log capture,
391
+ // ensuring output is never lost even for quick commands.
392
+ // This is the core fix for issue #25 where output was lost on macOS
393
+ // because screen's virtual terminal was destroyed before output could be seen.
394
+ const result = await runInScreen('echo "quick command output"', {
395
+ session: `test-output-guaranteed-${Date.now()}`,
396
+ detached: false,
397
+ });
398
+
399
+ assert.strictEqual(result.success, true);
400
+ assert.ok(
401
+ result.output !== undefined,
402
+ 'Attached mode should always return output property'
403
+ );
404
+ assert.ok(
405
+ result.output.includes('quick command output'),
406
+ 'Output should be captured (issue #25 fix verification)'
407
+ );
408
+ console.log(` Verified output capture: "${result.output.trim()}"`);
409
+ });
383
410
  });
384
411
 
385
412
  describe('runInTmux (if available)', () => {