start-command 0.27.2 → 0.28.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/bin/cli.js +17 -0
- package/src/lib/args-parser.js +26 -1
- package/src/lib/log-uploader.js +192 -0
- package/src/lib/usage.js +3 -1
- package/test/args-parser-control.js +7 -1
- package/test/args-parser.js +30 -0
- package/test/status-query.js +103 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ const { handleFailure } = require('../lib/failure-handler');
|
|
|
33
33
|
const { ExecutionStore, ExecutionRecord } = require('../lib/execution-store');
|
|
34
34
|
const { queryStatus, listExecutions } = require('../lib/status-formatter');
|
|
35
35
|
const { ControlAction, controlExecution } = require('../lib/execution-control');
|
|
36
|
+
const { uploadExecutionLog } = require('../lib/log-uploader');
|
|
36
37
|
const { printVersion } = require('../lib/version');
|
|
37
38
|
const { createStartBlock, createFinishBlock } = require('../lib/output-blocks');
|
|
38
39
|
const { runWithBunSpawn, runWithNodeSpawn } = require('../lib/spawn-helpers');
|
|
@@ -201,6 +202,12 @@ if (wrapperOptions.list) {
|
|
|
201
202
|
process.exit(0);
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
// Handle --upload-log flag
|
|
206
|
+
if (wrapperOptions.uploadLog) {
|
|
207
|
+
const exitCode = handleUploadLogQuery(wrapperOptions.uploadLog);
|
|
208
|
+
process.exit(exitCode);
|
|
209
|
+
}
|
|
210
|
+
|
|
204
211
|
// Handle --stop flag
|
|
205
212
|
if (wrapperOptions.stop !== null && wrapperOptions.stop !== undefined) {
|
|
206
213
|
handleControlQuery(wrapperOptions.stop, ControlAction.STOP);
|
|
@@ -300,6 +307,16 @@ function handleListQuery(outputFormat) {
|
|
|
300
307
|
}
|
|
301
308
|
}
|
|
302
309
|
|
|
310
|
+
function handleUploadLogQuery(identifier) {
|
|
311
|
+
const result = uploadExecutionLog(getExecutionStore(), identifier);
|
|
312
|
+
if (result.success) {
|
|
313
|
+
return result.exitCode || 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.error(`Error: ${result.error}`);
|
|
317
|
+
return result.exitCode || 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
303
320
|
function handleControlQuery(identifier, action) {
|
|
304
321
|
const result = controlExecution(getExecutionStore(), identifier, action);
|
|
305
322
|
if (result.success) {
|
package/src/lib/args-parser.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* --verbose Enable verbose/debug output (sets START_VERBOSE=1)
|
|
22
22
|
* --status <uuid> Show status of a previous command execution by UUID
|
|
23
23
|
* --list List all tracked command executions
|
|
24
|
+
* --upload-log <uuid-or-session> Upload the stored log for a tracked execution
|
|
24
25
|
* --output-format <format> Output format for status/list (links-notation, json, text)
|
|
25
26
|
* --stop <uuid-or-session-name> Send CTRL+C/SIGINT to a detached execution
|
|
26
27
|
* --terminate <uuid-or-session-name> Terminate a detached execution immediately
|
|
@@ -177,6 +178,7 @@ function parseArgs(args) {
|
|
|
177
178
|
useCommandStream: false, // Use command-stream library for command execution
|
|
178
179
|
status: null, // UUID to show status for
|
|
179
180
|
list: false, // List all tracked execution records
|
|
181
|
+
uploadLog: null, // UUID/session name whose stored log should be uploaded
|
|
180
182
|
outputFormat: null, // Output format for status/list (links-notation, json, text)
|
|
181
183
|
stop: null, // UUID/session name to stop gracefully
|
|
182
184
|
terminate: null, // UUID/session name to terminate immediately
|
|
@@ -450,6 +452,28 @@ function parseOption(args, index, options) {
|
|
|
450
452
|
return 1;
|
|
451
453
|
}
|
|
452
454
|
|
|
455
|
+
// --upload-log <uuid-or-session-name>
|
|
456
|
+
if (arg === '--upload-log') {
|
|
457
|
+
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
458
|
+
options.uploadLog = args[index + 1];
|
|
459
|
+
return 2;
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error(`Option ${arg} requires a UUID or session name argument`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// --upload-log=<value>
|
|
466
|
+
if (arg.startsWith('--upload-log=')) {
|
|
467
|
+
const value = arg.slice('--upload-log='.length);
|
|
468
|
+
if (!value) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`Option --upload-log requires a UUID or session name argument`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
options.uploadLog = value;
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
476
|
+
|
|
453
477
|
// --stop <uuid-or-session-name>
|
|
454
478
|
if (arg === '--stop') {
|
|
455
479
|
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
@@ -724,6 +748,7 @@ function validateOptions(options) {
|
|
|
724
748
|
const queryModes = [
|
|
725
749
|
hasValue(options.status),
|
|
726
750
|
options.list,
|
|
751
|
+
hasValue(options.uploadLog),
|
|
727
752
|
hasValue(options.stop),
|
|
728
753
|
hasValue(options.terminate),
|
|
729
754
|
options.cleanup,
|
|
@@ -731,7 +756,7 @@ function validateOptions(options) {
|
|
|
731
756
|
|
|
732
757
|
if (queryModes > 1) {
|
|
733
758
|
throw new Error(
|
|
734
|
-
'Cannot combine --status, --list, --stop, --terminate, or --cleanup in the same invocation'
|
|
759
|
+
'Cannot combine --status, --list, --upload-log, --stop, --terminate, or --cleanup in the same invocation'
|
|
735
760
|
);
|
|
736
761
|
}
|
|
737
762
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for uploading stored execution logs with gh-upload-log.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
function isExecutable(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isCommandFile(filePath) {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
return fs.existsSync(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return isExecutable(filePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getPathCommandNames(commandName) {
|
|
28
|
+
if (process.platform !== 'win32' || path.extname(commandName)) {
|
|
29
|
+
return [commandName];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
33
|
+
.split(';')
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
|
|
36
|
+
return [commandName, ...extensions.map((ext) => `${commandName}${ext}`)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveCommandFromPath(commandName) {
|
|
40
|
+
const pathValue = process.env.PATH || '';
|
|
41
|
+
for (const pathEntry of pathValue.split(path.delimiter)) {
|
|
42
|
+
if (!pathEntry) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const directory = pathEntry.replace(/^"|"$/g, '');
|
|
47
|
+
for (const candidateName of getPathCommandNames(commandName)) {
|
|
48
|
+
const candidate = path.join(directory, candidateName);
|
|
49
|
+
if (isCommandFile(candidate)) {
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCommand(commandName) {
|
|
59
|
+
const isWindows = process.platform === 'win32';
|
|
60
|
+
const lookupCommand = isWindows ? 'where' : 'which';
|
|
61
|
+
const pathMatch = resolveCommandFromPath(commandName);
|
|
62
|
+
if (pathMatch) {
|
|
63
|
+
return pathMatch;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = spawnSync(lookupCommand, [commandName], {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
70
|
+
});
|
|
71
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
72
|
+
return result.stdout.trim().split(/\r?\n/)[0];
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Fall through to common locations.
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isWindows && commandName === 'gh-upload-log') {
|
|
79
|
+
const bunGlobalPath = path.join(os.homedir(), '.bun', 'bin', commandName);
|
|
80
|
+
if (isExecutable(bunGlobalPath)) {
|
|
81
|
+
return bunGlobalPath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shouldRunThroughShell(command) {
|
|
89
|
+
return process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runCommand(command, args, options = {}) {
|
|
93
|
+
return spawnSync(command, args, {
|
|
94
|
+
...options,
|
|
95
|
+
shell: shouldRunThroughShell(command),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function runInstall(command, displayName, args) {
|
|
100
|
+
console.log(
|
|
101
|
+
`gh-upload-log not found; installing with: ${displayName} ${args.join(' ')}`
|
|
102
|
+
);
|
|
103
|
+
const result = runCommand(command, args, { stdio: 'inherit' });
|
|
104
|
+
return result.status === 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensureGhUploadLogAvailable() {
|
|
108
|
+
const existing = resolveCommand('gh-upload-log');
|
|
109
|
+
if (existing) {
|
|
110
|
+
return { success: true, command: existing };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const installers = [
|
|
114
|
+
['bun', ['install', '-g', 'gh-upload-log']],
|
|
115
|
+
['npm', ['install', '-g', 'gh-upload-log']],
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const [command, args] of installers) {
|
|
119
|
+
const installer = resolveCommand(command);
|
|
120
|
+
if (!installer) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (runInstall(installer, command, args)) {
|
|
124
|
+
const installed = resolveCommand('gh-upload-log');
|
|
125
|
+
if (installed) {
|
|
126
|
+
return { success: true, command: installed };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error:
|
|
134
|
+
'gh-upload-log is not installed and automatic installation did not make it available on PATH.',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function uploadLogPath(logPath) {
|
|
139
|
+
if (!logPath) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
error: 'Execution record does not have a log path.',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (!fs.existsSync(logPath)) {
|
|
146
|
+
return { success: false, error: `Log file not found: ${logPath}` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const availability = ensureGhUploadLogAvailable();
|
|
150
|
+
if (!availability.success) {
|
|
151
|
+
return availability;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = runCommand(availability.command, [logPath], {
|
|
155
|
+
stdio: 'inherit',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const exitCode =
|
|
159
|
+
result.status !== null && result.status !== undefined ? result.status : 1;
|
|
160
|
+
if (exitCode !== 0) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
exitCode,
|
|
164
|
+
error: `gh-upload-log exited with code ${exitCode}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { success: true, exitCode: 0 };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function uploadExecutionLog(store, identifier) {
|
|
172
|
+
if (!store) {
|
|
173
|
+
return { success: false, error: 'Execution tracking is disabled.' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const record = store.get(identifier);
|
|
177
|
+
if (!record) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
error: `No execution found with UUID or session name: ${identifier}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return uploadLogPath(record.logPath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
ensureGhUploadLogAvailable,
|
|
189
|
+
resolveCommand,
|
|
190
|
+
uploadExecutionLog,
|
|
191
|
+
uploadLogPath,
|
|
192
|
+
};
|
package/src/lib/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Print usage information */
|
|
2
2
|
function printUsage() {
|
|
3
|
-
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--output-format <fmt>] | $ --stop <id> | $ --terminate <id>
|
|
3
|
+
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--output-format <fmt>] | $ --upload-log <id> | $ --stop <id> | $ --terminate <id>
|
|
4
4
|
|
|
5
5
|
Options:
|
|
6
6
|
--isolated, -i <env> Run in isolated environment (screen, tmux, docker, ssh)
|
|
@@ -19,6 +19,7 @@ Options:
|
|
|
19
19
|
--use-command-stream Use command-stream library for execution (experimental)
|
|
20
20
|
--status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
|
|
21
21
|
--list List all tracked executions (--output-format: links-notation|json|text)
|
|
22
|
+
--upload-log <id> Upload the stored log for an execution UUID or session name
|
|
22
23
|
--stop <id> Send CTRL+C/SIGINT to a detached isolated execution
|
|
23
24
|
--terminate <id> Terminate a detached isolated execution immediately
|
|
24
25
|
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
@@ -39,6 +40,7 @@ Examples:
|
|
|
39
40
|
$ --isolated-user --keep-user -- npm start
|
|
40
41
|
$ --list # List stored execution records
|
|
41
42
|
$ --list --output-format json # List stored records as JSON
|
|
43
|
+
$ --upload-log my-screen-session # Upload stored execution log
|
|
42
44
|
$ --stop my-screen-session # Ask detached execution to stop gracefully
|
|
43
45
|
$ --terminate my-screen-session # Terminate detached execution immediately
|
|
44
46
|
$ --use-command-stream echo "Hello" # Use command-stream library`);
|
|
@@ -54,7 +54,13 @@ describe('control options', () => {
|
|
|
54
54
|
it('should reject combining query and control modes', () => {
|
|
55
55
|
assert.throws(() => {
|
|
56
56
|
parseArgs(['--status', 'uuid-here', '--stop', 'my-session']);
|
|
57
|
-
}, /Cannot combine --status, --list, --stop, --terminate, or --cleanup/);
|
|
57
|
+
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, or --cleanup/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should reject combining upload-log with control modes', () => {
|
|
61
|
+
assert.throws(() => {
|
|
62
|
+
parseArgs(['--upload-log', 'uuid-here', '--terminate', 'my-session']);
|
|
63
|
+
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, or --cleanup/);
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
it('should reject output-format with control modes', () => {
|
package/test/args-parser.js
CHANGED
|
@@ -832,6 +832,36 @@ describe('status option', () => {
|
|
|
832
832
|
});
|
|
833
833
|
});
|
|
834
834
|
|
|
835
|
+
describe('upload-log option', () => {
|
|
836
|
+
it('should parse --upload-log with UUID or session name', () => {
|
|
837
|
+
const result = parseArgs(['--upload-log', 'my-session']);
|
|
838
|
+
assert.strictEqual(result.wrapperOptions.uploadLog, 'my-session');
|
|
839
|
+
assert.strictEqual(result.command, '');
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should parse --upload-log=value format', () => {
|
|
843
|
+
const result = parseArgs(['--upload-log=my-session']);
|
|
844
|
+
assert.strictEqual(result.wrapperOptions.uploadLog, 'my-session');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('should throw error for missing --upload-log argument', () => {
|
|
848
|
+
assert.throws(() => {
|
|
849
|
+
parseArgs(['--upload-log']);
|
|
850
|
+
}, /requires a UUID or session name argument/);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should throw error for empty --upload-log=value argument', () => {
|
|
854
|
+
assert.throws(() => {
|
|
855
|
+
parseArgs(['--upload-log=']);
|
|
856
|
+
}, /requires a UUID or session name argument/);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('should default uploadLog to null', () => {
|
|
860
|
+
const result = parseArgs(['echo', 'hello']);
|
|
861
|
+
assert.strictEqual(result.wrapperOptions.uploadLog, null);
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
835
865
|
describe('list option', () => {
|
|
836
866
|
it('should parse --list flag', () => {
|
|
837
867
|
const result = parseArgs(['--list']);
|
package/test/status-query.js
CHANGED
|
@@ -32,7 +32,7 @@ function cleanupTestDir() {
|
|
|
32
32
|
|
|
33
33
|
// Helper to run CLI command
|
|
34
34
|
function runCli(args, env = {}) {
|
|
35
|
-
const result = spawnSync(
|
|
35
|
+
const result = spawnSync(process.execPath, [CLI_PATH, ...args], {
|
|
36
36
|
encoding: 'utf8',
|
|
37
37
|
env: {
|
|
38
38
|
...process.env,
|
|
@@ -48,6 +48,26 @@ function runCli(args, env = {}) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function createExecutable(filePath, content) {
|
|
52
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
53
|
+
fs.chmodSync(filePath, 0o755);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createFakeUploader(fakeBin, outputPrefix) {
|
|
57
|
+
if (process.platform === 'win32') {
|
|
58
|
+
createExecutable(
|
|
59
|
+
path.join(fakeBin, 'gh-upload-log.cmd'),
|
|
60
|
+
`@echo off\r\necho ${outputPrefix}: %1\r\n`
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
createExecutable(
|
|
66
|
+
path.join(fakeBin, 'gh-upload-log'),
|
|
67
|
+
`#!/bin/sh\necho "${outputPrefix}: $1"\n`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
describe('--status query functionality', () => {
|
|
52
72
|
let store;
|
|
53
73
|
let testRecord;
|
|
@@ -241,6 +261,88 @@ describe('--status query functionality', () => {
|
|
|
241
261
|
});
|
|
242
262
|
});
|
|
243
263
|
|
|
264
|
+
describe('--upload-log functionality', () => {
|
|
265
|
+
it('should run gh-upload-log with the stored execution log path', () => {
|
|
266
|
+
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), 'upload-log-bin-'));
|
|
267
|
+
const logPath = path.join(TEST_APP_FOLDER, 'command.log');
|
|
268
|
+
fs.writeFileSync(logPath, 'captured command output\n', 'utf8');
|
|
269
|
+
createFakeUploader(fakeBin, 'fake uploader received');
|
|
270
|
+
|
|
271
|
+
testRecord.logPath = logPath;
|
|
272
|
+
store.save(testRecord);
|
|
273
|
+
|
|
274
|
+
const result = runCli(['--upload-log', testRecord.uuid], {
|
|
275
|
+
PATH: fakeBin,
|
|
276
|
+
HOME: fakeBin,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.exitCode).toBe(0);
|
|
280
|
+
expect(result.stdout).toContain(`fake uploader received: ${logPath}`);
|
|
281
|
+
expect(result.stderr).toBe('');
|
|
282
|
+
|
|
283
|
+
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should install gh-upload-log when it is missing before uploading', () => {
|
|
287
|
+
if (process.platform === 'win32') {
|
|
288
|
+
console.log(' Skipping: shell fixture uses POSIX scripts');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const fakeBin = fs.mkdtempSync(
|
|
293
|
+
path.join(os.tmpdir(), 'upload-log-install-bin-')
|
|
294
|
+
);
|
|
295
|
+
const installMarker = path.join(fakeBin, 'install.log');
|
|
296
|
+
const logPath = path.join(TEST_APP_FOLDER, 'install-command.log');
|
|
297
|
+
fs.writeFileSync(logPath, 'captured command output\n', 'utf8');
|
|
298
|
+
|
|
299
|
+
createExecutable(
|
|
300
|
+
path.join(fakeBin, 'bun'),
|
|
301
|
+
[
|
|
302
|
+
'#!/bin/sh',
|
|
303
|
+
`echo "$@" > "${installMarker}"`,
|
|
304
|
+
`cat > "${path.join(fakeBin, 'gh-upload-log')}" <<'SCRIPT'`,
|
|
305
|
+
'#!/bin/sh',
|
|
306
|
+
'echo "installed uploader received: $1"',
|
|
307
|
+
'SCRIPT',
|
|
308
|
+
`chmod +x "${path.join(fakeBin, 'gh-upload-log')}"`,
|
|
309
|
+
'exit 0',
|
|
310
|
+
'',
|
|
311
|
+
].join('\n')
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
testRecord.logPath = logPath;
|
|
315
|
+
store.save(testRecord);
|
|
316
|
+
|
|
317
|
+
const result = runCli(['--upload-log', testRecord.uuid], {
|
|
318
|
+
PATH: `${fakeBin}${path.delimiter}/usr/bin${path.delimiter}/bin`,
|
|
319
|
+
HOME: fakeBin,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.exitCode).toBe(0);
|
|
323
|
+
expect(fs.readFileSync(installMarker, 'utf8').trim()).toBe(
|
|
324
|
+
'install -g gh-upload-log'
|
|
325
|
+
);
|
|
326
|
+
expect(result.stdout).toContain('gh-upload-log not found');
|
|
327
|
+
expect(result.stdout).toContain(
|
|
328
|
+
`installed uploader received: ${logPath}`
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should show an error when the stored log file is missing', () => {
|
|
335
|
+
testRecord.logPath = path.join(TEST_APP_FOLDER, 'missing.log');
|
|
336
|
+
store.save(testRecord);
|
|
337
|
+
|
|
338
|
+
const result = runCli(['--upload-log', testRecord.uuid]);
|
|
339
|
+
|
|
340
|
+
expect(result.exitCode).toBe(1);
|
|
341
|
+
expect(result.stderr).toContain('Log file not found');
|
|
342
|
+
expect(result.stderr).toContain(testRecord.logPath);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
244
346
|
describe('executing status', () => {
|
|
245
347
|
it('should show executing status for ongoing commands', () => {
|
|
246
348
|
// Create an executing (not completed) record
|