start-command 0.13.0 → 0.16.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 +34 -227
- package/bun.lock +5 -0
- package/eslint.config.mjs +1 -1
- package/package.json +11 -6
- package/src/bin/cli.js +332 -171
- package/src/lib/args-parser.js +118 -0
- package/src/lib/execution-store.js +722 -0
- package/src/lib/isolation.js +51 -0
- package/src/lib/output-blocks.js +357 -0
- package/src/lib/status-formatter.js +148 -0
- package/src/lib/version.js +143 -0
- package/test/args-parser.test.js +107 -0
- package/test/cli.test.js +11 -1
- package/test/docker-autoremove.test.js +11 -16
- package/test/execution-store.test.js +483 -0
- package/test/isolation-cleanup.test.js +11 -16
- package/test/isolation.test.js +11 -17
- package/test/output-blocks.test.js +197 -0
- package/test/public-exports.test.js +105 -0
- package/test/status-query.test.js +197 -0
- package/.github/workflows/release.yml +0 -352
- package/.husky/pre-commit +0 -1
- package/ARCHITECTURE.md +0 -297
- package/LICENSE +0 -24
- package/README.md +0 -339
- package/REQUIREMENTS.md +0 -299
- package/docs/PIPES.md +0 -243
- package/docs/USAGE.md +0 -194
- package/docs/case-studies/issue-15/README.md +0 -208
- package/docs/case-studies/issue-18/README.md +0 -343
- package/docs/case-studies/issue-18/issue-comments.json +0 -1
- package/docs/case-studies/issue-18/issue-data.json +0 -7
- package/docs/case-studies/issue-22/analysis.md +0 -547
- package/docs/case-studies/issue-22/issue-data.json +0 -12
- package/docs/case-studies/issue-25/README.md +0 -232
- package/docs/case-studies/issue-25/issue-data.json +0 -21
- package/docs/case-studies/issue-28/README.md +0 -405
- package/docs/case-studies/issue-28/issue-data.json +0 -105
- package/docs/case-studies/issue-28/raw-issue-data.md +0 -92
- package/experiments/debug-regex.js +0 -49
- package/experiments/isolation-design.md +0 -131
- package/experiments/screen-output-test.js +0 -265
- package/experiments/test-cli.sh +0 -42
- package/experiments/test-command-stream-cjs.cjs +0 -30
- package/experiments/test-command-stream-wrapper.js +0 -54
- package/experiments/test-command-stream.mjs +0 -56
- package/experiments/test-screen-attached.js +0 -126
- package/experiments/test-screen-logfile.js +0 -286
- package/experiments/test-screen-modes.js +0 -128
- package/experiments/test-screen-output.sh +0 -27
- package/experiments/test-screen-tee-debug.js +0 -237
- package/experiments/test-screen-tee-fallback.js +0 -230
- package/experiments/test-substitution.js +0 -143
- package/experiments/user-isolation-research.md +0 -83
- package/scripts/changeset-version.mjs +0 -38
- package/scripts/check-file-size.mjs +0 -103
- package/scripts/create-github-release.mjs +0 -93
- package/scripts/create-manual-changeset.mjs +0 -89
- package/scripts/format-github-release.mjs +0 -83
- package/scripts/format-release-notes.mjs +0 -219
- package/scripts/instant-version-bump.mjs +0 -121
- package/scripts/publish-to-npm.mjs +0 -129
- package/scripts/setup-npm.mjs +0 -37
- package/scripts/validate-changeset.mjs +0 -107
- package/scripts/version-and-commit.mjs +0 -237
|
@@ -238,22 +238,15 @@ describe('Isolation Resource Cleanup Verification', () => {
|
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
describe('docker resource cleanup', () => {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
try {
|
|
247
|
-
execSync('docker info', { stdio: 'ignore', timeout: 5000 });
|
|
248
|
-
return true;
|
|
249
|
-
} catch {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
241
|
+
// Use the canRunLinuxDockerImages function from isolation module
|
|
242
|
+
// to properly detect if Linux containers can run (handles Windows containers mode)
|
|
243
|
+
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
253
244
|
|
|
254
245
|
it('should show docker container as exited after command completes (auto-exit by default)', async () => {
|
|
255
|
-
if (!
|
|
256
|
-
console.log(
|
|
246
|
+
if (!canRunLinuxDockerImages()) {
|
|
247
|
+
console.log(
|
|
248
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
249
|
+
);
|
|
257
250
|
return;
|
|
258
251
|
}
|
|
259
252
|
|
|
@@ -325,8 +318,10 @@ describe('Isolation Resource Cleanup Verification', () => {
|
|
|
325
318
|
});
|
|
326
319
|
|
|
327
320
|
it('should keep docker container running when keepAlive is true', async () => {
|
|
328
|
-
if (!
|
|
329
|
-
console.log(
|
|
321
|
+
if (!canRunLinuxDockerImages()) {
|
|
322
|
+
console.log(
|
|
323
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
324
|
+
);
|
|
330
325
|
return;
|
|
331
326
|
}
|
|
332
327
|
|
package/test/isolation.test.js
CHANGED
|
@@ -444,23 +444,15 @@ describe('Isolation Keep-Alive Behavior', () => {
|
|
|
444
444
|
});
|
|
445
445
|
|
|
446
446
|
describe('runInDocker keep-alive messages', () => {
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
try {
|
|
453
|
-
// Try to ping the docker daemon
|
|
454
|
-
execSync('docker info', { stdio: 'ignore', timeout: 5000 });
|
|
455
|
-
return true;
|
|
456
|
-
} catch {
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
447
|
+
// Use the canRunLinuxDockerImages function from isolation module
|
|
448
|
+
// to properly detect if Linux containers can run (handles Windows containers mode)
|
|
449
|
+
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
460
450
|
|
|
461
451
|
it('should include auto-exit message by default in detached mode', async () => {
|
|
462
|
-
if (!
|
|
463
|
-
console.log(
|
|
452
|
+
if (!canRunLinuxDockerImages()) {
|
|
453
|
+
console.log(
|
|
454
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
455
|
+
);
|
|
464
456
|
return;
|
|
465
457
|
}
|
|
466
458
|
|
|
@@ -487,8 +479,10 @@ describe('Isolation Keep-Alive Behavior', () => {
|
|
|
487
479
|
});
|
|
488
480
|
|
|
489
481
|
it('should include keep-alive message when keepAlive is true', async () => {
|
|
490
|
-
if (!
|
|
491
|
-
console.log(
|
|
482
|
+
if (!canRunLinuxDockerImages()) {
|
|
483
|
+
console.log(
|
|
484
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
485
|
+
);
|
|
492
486
|
return;
|
|
493
487
|
}
|
|
494
488
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for output-blocks module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { describe, it, expect } = require('bun:test');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
BOX_STYLES,
|
|
9
|
+
DEFAULT_STYLE,
|
|
10
|
+
DEFAULT_WIDTH,
|
|
11
|
+
getBoxStyle,
|
|
12
|
+
createStartBlock,
|
|
13
|
+
createFinishBlock,
|
|
14
|
+
formatDuration,
|
|
15
|
+
escapeForLinksNotation,
|
|
16
|
+
formatAsNestedLinksNotation,
|
|
17
|
+
} = require('../src/lib/output-blocks');
|
|
18
|
+
|
|
19
|
+
describe('output-blocks module', () => {
|
|
20
|
+
describe('BOX_STYLES', () => {
|
|
21
|
+
it('should have all expected styles', () => {
|
|
22
|
+
expect(BOX_STYLES).toHaveProperty('rounded');
|
|
23
|
+
expect(BOX_STYLES).toHaveProperty('heavy');
|
|
24
|
+
expect(BOX_STYLES).toHaveProperty('double');
|
|
25
|
+
expect(BOX_STYLES).toHaveProperty('simple');
|
|
26
|
+
expect(BOX_STYLES).toHaveProperty('ascii');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have correct rounded style characters', () => {
|
|
30
|
+
expect(BOX_STYLES.rounded.topLeft).toBe('╭');
|
|
31
|
+
expect(BOX_STYLES.rounded.topRight).toBe('╮');
|
|
32
|
+
expect(BOX_STYLES.rounded.bottomLeft).toBe('╰');
|
|
33
|
+
expect(BOX_STYLES.rounded.bottomRight).toBe('╯');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getBoxStyle', () => {
|
|
38
|
+
it('should return rounded style by default', () => {
|
|
39
|
+
const style = getBoxStyle();
|
|
40
|
+
expect(style).toEqual(BOX_STYLES.rounded);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return requested style', () => {
|
|
44
|
+
expect(getBoxStyle('heavy')).toEqual(BOX_STYLES.heavy);
|
|
45
|
+
expect(getBoxStyle('double')).toEqual(BOX_STYLES.double);
|
|
46
|
+
expect(getBoxStyle('ascii')).toEqual(BOX_STYLES.ascii);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return rounded for unknown style', () => {
|
|
50
|
+
const style = getBoxStyle('unknown');
|
|
51
|
+
expect(style).toEqual(BOX_STYLES.rounded);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('createStartBlock', () => {
|
|
56
|
+
it('should create a start block with session ID', () => {
|
|
57
|
+
const block = createStartBlock({
|
|
58
|
+
sessionId: 'test-uuid-1234',
|
|
59
|
+
timestamp: '2025-01-01 00:00:00',
|
|
60
|
+
command: 'echo hello',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(block).toContain('╭');
|
|
64
|
+
expect(block).toContain('╰');
|
|
65
|
+
expect(block).toContain('Session ID: test-uuid-1234');
|
|
66
|
+
expect(block).toContain('Starting at 2025-01-01 00:00:00: echo hello');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should use specified style', () => {
|
|
70
|
+
const block = createStartBlock({
|
|
71
|
+
sessionId: 'test-uuid',
|
|
72
|
+
timestamp: '2025-01-01 00:00:00',
|
|
73
|
+
command: 'echo hello',
|
|
74
|
+
style: 'ascii',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(block).toContain('+');
|
|
78
|
+
expect(block).toContain('-');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('createFinishBlock', () => {
|
|
83
|
+
it('should create a finish block with session ID and exit code', () => {
|
|
84
|
+
const block = createFinishBlock({
|
|
85
|
+
sessionId: 'test-uuid-1234',
|
|
86
|
+
timestamp: '2025-01-01 00:00:01',
|
|
87
|
+
exitCode: 0,
|
|
88
|
+
logPath: '/tmp/test.log',
|
|
89
|
+
durationMs: 17,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(block).toContain('╭');
|
|
93
|
+
expect(block).toContain('╰');
|
|
94
|
+
expect(block).toContain('Session ID: test-uuid-1234');
|
|
95
|
+
expect(block).toContain(
|
|
96
|
+
'Finished at 2025-01-01 00:00:01 in 0.017 seconds'
|
|
97
|
+
);
|
|
98
|
+
expect(block).toContain('Exit code: 0');
|
|
99
|
+
expect(block).toContain('Log: /tmp/test.log');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should create a finish block without duration when not provided', () => {
|
|
103
|
+
const block = createFinishBlock({
|
|
104
|
+
sessionId: 'test-uuid-1234',
|
|
105
|
+
timestamp: '2025-01-01 00:00:01',
|
|
106
|
+
exitCode: 0,
|
|
107
|
+
logPath: '/tmp/test.log',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(block).toContain('Finished at 2025-01-01 00:00:01');
|
|
111
|
+
expect(block).not.toContain('seconds');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('formatDuration', () => {
|
|
116
|
+
it('should format very small durations', () => {
|
|
117
|
+
expect(formatDuration(0.5)).toBe('0.001');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should format millisecond durations', () => {
|
|
121
|
+
expect(formatDuration(17)).toBe('0.017');
|
|
122
|
+
expect(formatDuration(500)).toBe('0.500');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should format second durations', () => {
|
|
126
|
+
expect(formatDuration(1000)).toBe('1.000');
|
|
127
|
+
expect(formatDuration(5678)).toBe('5.678');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should format longer durations with less precision', () => {
|
|
131
|
+
expect(formatDuration(12345)).toBe('12.35');
|
|
132
|
+
expect(formatDuration(123456)).toBe('123.5');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('escapeForLinksNotation', () => {
|
|
137
|
+
it('should not quote simple values', () => {
|
|
138
|
+
expect(escapeForLinksNotation('simple')).toBe('simple');
|
|
139
|
+
expect(escapeForLinksNotation('123')).toBe('123');
|
|
140
|
+
expect(escapeForLinksNotation('true')).toBe('true');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should quote values with spaces', () => {
|
|
144
|
+
expect(escapeForLinksNotation('hello world')).toBe('"hello world"');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should quote values with colons', () => {
|
|
148
|
+
expect(escapeForLinksNotation('key:value')).toBe('"key:value"');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should use single quotes for values with double quotes', () => {
|
|
152
|
+
expect(escapeForLinksNotation('say "hello"')).toBe('\'say "hello"\'');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should use double quotes for values with single quotes', () => {
|
|
156
|
+
expect(escapeForLinksNotation("it's cool")).toBe('"it\'s cool"');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should escape quotes when both types are present', () => {
|
|
160
|
+
const result = escapeForLinksNotation('say "hello" it\'s');
|
|
161
|
+
// Should wrap in one quote type and escape the other
|
|
162
|
+
expect(result).toMatch(/^["'].*["']$/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle null values', () => {
|
|
166
|
+
expect(escapeForLinksNotation(null)).toBe('null');
|
|
167
|
+
expect(escapeForLinksNotation(undefined)).toBe('null');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('formatAsNestedLinksNotation', () => {
|
|
172
|
+
it('should format simple objects', () => {
|
|
173
|
+
const obj = { key: 'value', number: 123 };
|
|
174
|
+
const result = formatAsNestedLinksNotation(obj);
|
|
175
|
+
|
|
176
|
+
expect(result).toContain('key value');
|
|
177
|
+
expect(result).toContain('number 123');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should quote values with spaces', () => {
|
|
181
|
+
const obj = { message: 'hello world' };
|
|
182
|
+
const result = formatAsNestedLinksNotation(obj);
|
|
183
|
+
|
|
184
|
+
expect(result).toContain('message "hello world"');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle empty objects', () => {
|
|
188
|
+
expect(formatAsNestedLinksNotation({})).toBe('()');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle null', () => {
|
|
192
|
+
expect(formatAsNestedLinksNotation(null)).toBe('null');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log('=== Output Blocks Unit Tests ===');
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for public exports of start-command package
|
|
3
|
+
* Verifies that ExecutionStore can be imported via package exports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it, expect } = require('bun:test');
|
|
7
|
+
|
|
8
|
+
describe('Public Exports', () => {
|
|
9
|
+
describe('execution-store export', () => {
|
|
10
|
+
it('should export ExecutionStore class', () => {
|
|
11
|
+
const { ExecutionStore } = require('../src/lib/execution-store');
|
|
12
|
+
expect(ExecutionStore).toBeDefined();
|
|
13
|
+
expect(typeof ExecutionStore).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should export ExecutionRecord class', () => {
|
|
17
|
+
const { ExecutionRecord } = require('../src/lib/execution-store');
|
|
18
|
+
expect(ExecutionRecord).toBeDefined();
|
|
19
|
+
expect(typeof ExecutionRecord).toBe('function');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should export ExecutionStatus enum', () => {
|
|
23
|
+
const { ExecutionStatus } = require('../src/lib/execution-store');
|
|
24
|
+
expect(ExecutionStatus).toBeDefined();
|
|
25
|
+
expect(ExecutionStatus.EXECUTING).toBe('executing');
|
|
26
|
+
expect(ExecutionStatus.EXECUTED).toBe('executed');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should export LockManager class', () => {
|
|
30
|
+
const { LockManager } = require('../src/lib/execution-store');
|
|
31
|
+
expect(LockManager).toBeDefined();
|
|
32
|
+
expect(typeof LockManager).toBe('function');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should export isClinkInstalled function', () => {
|
|
36
|
+
const { isClinkInstalled } = require('../src/lib/execution-store');
|
|
37
|
+
expect(isClinkInstalled).toBeDefined();
|
|
38
|
+
expect(typeof isClinkInstalled).toBe('function');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should export configuration constants', () => {
|
|
42
|
+
const {
|
|
43
|
+
DEFAULT_APP_FOLDER,
|
|
44
|
+
LINO_DB_FILE,
|
|
45
|
+
LINKS_DB_FILE,
|
|
46
|
+
LOCK_FILE,
|
|
47
|
+
} = require('../src/lib/execution-store');
|
|
48
|
+
expect(DEFAULT_APP_FOLDER).toBeDefined();
|
|
49
|
+
expect(typeof DEFAULT_APP_FOLDER).toBe('string');
|
|
50
|
+
expect(LINO_DB_FILE).toBe('executions.lino');
|
|
51
|
+
expect(LINKS_DB_FILE).toBe('executions.links');
|
|
52
|
+
expect(LOCK_FILE).toBe('executions.lock');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should allow creating and using ExecutionStore instance', () => {
|
|
56
|
+
const os = require('os');
|
|
57
|
+
const path = require('path');
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
ExecutionStore,
|
|
62
|
+
ExecutionRecord,
|
|
63
|
+
ExecutionStatus,
|
|
64
|
+
} = require('../src/lib/execution-store');
|
|
65
|
+
|
|
66
|
+
// Create a temporary folder for testing
|
|
67
|
+
const testFolder = path.join(
|
|
68
|
+
os.tmpdir(),
|
|
69
|
+
`public-export-test-${Date.now()}`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const store = new ExecutionStore({ appFolder: testFolder });
|
|
74
|
+
expect(store).toBeDefined();
|
|
75
|
+
|
|
76
|
+
// Create and save a record
|
|
77
|
+
const record = new ExecutionRecord({
|
|
78
|
+
command: 'echo "test"',
|
|
79
|
+
logPath: '/tmp/test.log',
|
|
80
|
+
});
|
|
81
|
+
expect(record.status).toBe(ExecutionStatus.EXECUTING);
|
|
82
|
+
|
|
83
|
+
store.save(record);
|
|
84
|
+
|
|
85
|
+
// Retrieve the record
|
|
86
|
+
const retrieved = store.get(record.uuid);
|
|
87
|
+
expect(retrieved).toBeDefined();
|
|
88
|
+
expect(retrieved.command).toBe('echo "test"');
|
|
89
|
+
|
|
90
|
+
// Complete the record
|
|
91
|
+
record.complete(0);
|
|
92
|
+
store.save(record);
|
|
93
|
+
|
|
94
|
+
const completed = store.get(record.uuid);
|
|
95
|
+
expect(completed.status).toBe(ExecutionStatus.EXECUTED);
|
|
96
|
+
expect(completed.exitCode).toBe(0);
|
|
97
|
+
} finally {
|
|
98
|
+
// Cleanup
|
|
99
|
+
if (fs.existsSync(testFolder)) {
|
|
100
|
+
fs.rmSync(testFolder, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for --status query functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { describe, it, expect, beforeEach, afterEach } = require('bun:test');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
ExecutionStore,
|
|
13
|
+
ExecutionRecord,
|
|
14
|
+
ExecutionStatus,
|
|
15
|
+
} = require('../src/lib/execution-store');
|
|
16
|
+
|
|
17
|
+
// Use temp directory for tests
|
|
18
|
+
const TEST_APP_FOLDER = path.join(
|
|
19
|
+
os.tmpdir(),
|
|
20
|
+
`status-query-test-${Date.now()}`
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Path to CLI
|
|
24
|
+
const CLI_PATH = path.join(__dirname, '../src/bin/cli.js');
|
|
25
|
+
|
|
26
|
+
// Helper to clean up test directory
|
|
27
|
+
function cleanupTestDir() {
|
|
28
|
+
if (fs.existsSync(TEST_APP_FOLDER)) {
|
|
29
|
+
fs.rmSync(TEST_APP_FOLDER, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Helper to run CLI command
|
|
34
|
+
function runCli(args, env = {}) {
|
|
35
|
+
const result = spawnSync('bun', [CLI_PATH, ...args], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
START_APP_FOLDER: TEST_APP_FOLDER,
|
|
40
|
+
...env,
|
|
41
|
+
},
|
|
42
|
+
timeout: 10000,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
stdout: result.stdout || '',
|
|
46
|
+
stderr: result.stderr || '',
|
|
47
|
+
exitCode: result.status,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('--status query functionality', () => {
|
|
52
|
+
let store;
|
|
53
|
+
let testRecord;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
cleanupTestDir();
|
|
57
|
+
store = new ExecutionStore({
|
|
58
|
+
appFolder: TEST_APP_FOLDER,
|
|
59
|
+
useLinks: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Create a test execution record
|
|
63
|
+
testRecord = new ExecutionRecord({
|
|
64
|
+
command: 'echo hello world',
|
|
65
|
+
pid: 12345,
|
|
66
|
+
logPath: '/tmp/test.log',
|
|
67
|
+
workingDirectory: '/home/test',
|
|
68
|
+
shell: '/bin/bash',
|
|
69
|
+
});
|
|
70
|
+
testRecord.complete(0);
|
|
71
|
+
store.save(testRecord);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
cleanupTestDir();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('links-notation format (default)', () => {
|
|
79
|
+
it('should output status in links-notation indented format by default', () => {
|
|
80
|
+
const result = runCli(['--status', testRecord.uuid]);
|
|
81
|
+
|
|
82
|
+
expect(result.exitCode).toBe(0);
|
|
83
|
+
// Should start with UUID on its own line
|
|
84
|
+
expect(result.stdout).toMatch(new RegExp(`^${testRecord.uuid}\\n`));
|
|
85
|
+
// Should have indented properties (values without special chars are not quoted)
|
|
86
|
+
expect(result.stdout).toContain(` uuid ${testRecord.uuid}`);
|
|
87
|
+
expect(result.stdout).toContain(' status executed');
|
|
88
|
+
// Command with space should be quoted
|
|
89
|
+
expect(result.stdout).toContain(' command "echo hello world"');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should output status in links-notation format with explicit flag', () => {
|
|
93
|
+
const result = runCli([
|
|
94
|
+
'--status',
|
|
95
|
+
testRecord.uuid,
|
|
96
|
+
'--output-format',
|
|
97
|
+
'links-notation',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
expect(result.exitCode).toBe(0);
|
|
101
|
+
// Should start with UUID on its own line
|
|
102
|
+
expect(result.stdout).toMatch(new RegExp(`^${testRecord.uuid}\\n`));
|
|
103
|
+
// UUID without special chars is not quoted
|
|
104
|
+
expect(result.stdout).toContain(` uuid ${testRecord.uuid}`);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('json format', () => {
|
|
109
|
+
it('should output status in JSON format', () => {
|
|
110
|
+
const result = runCli([
|
|
111
|
+
'--status',
|
|
112
|
+
testRecord.uuid,
|
|
113
|
+
'--output-format',
|
|
114
|
+
'json',
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
expect(result.exitCode).toBe(0);
|
|
118
|
+
|
|
119
|
+
// Parse the JSON output
|
|
120
|
+
const parsed = JSON.parse(result.stdout);
|
|
121
|
+
expect(parsed.uuid).toBe(testRecord.uuid);
|
|
122
|
+
expect(parsed.command).toBe('echo hello world');
|
|
123
|
+
expect(parsed.status).toBe('executed');
|
|
124
|
+
expect(parsed.exitCode).toBe(0);
|
|
125
|
+
expect(parsed.pid).toBe(12345);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('text format', () => {
|
|
130
|
+
it('should output status in human-readable text format', () => {
|
|
131
|
+
const result = runCli([
|
|
132
|
+
'--status',
|
|
133
|
+
testRecord.uuid,
|
|
134
|
+
'--output-format',
|
|
135
|
+
'text',
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
expect(result.exitCode).toBe(0);
|
|
139
|
+
expect(result.stdout).toContain('Execution Status');
|
|
140
|
+
expect(result.stdout).toContain('UUID:');
|
|
141
|
+
expect(result.stdout).toContain(testRecord.uuid);
|
|
142
|
+
expect(result.stdout).toContain('Status:');
|
|
143
|
+
expect(result.stdout).toContain('executed');
|
|
144
|
+
expect(result.stdout).toContain('Command:');
|
|
145
|
+
expect(result.stdout).toContain('echo hello world');
|
|
146
|
+
expect(result.stdout).toContain('Exit Code:');
|
|
147
|
+
expect(result.stdout).toContain('0');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('error handling', () => {
|
|
152
|
+
it('should show error for non-existent UUID', () => {
|
|
153
|
+
const fakeUuid = '00000000-0000-0000-0000-000000000000';
|
|
154
|
+
const result = runCli(['--status', fakeUuid]);
|
|
155
|
+
|
|
156
|
+
expect(result.exitCode).toBe(1);
|
|
157
|
+
expect(result.stderr).toContain('No execution found with UUID');
|
|
158
|
+
expect(result.stderr).toContain(fakeUuid);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should show error when tracking is disabled', () => {
|
|
162
|
+
const result = runCli(['--status', testRecord.uuid], {
|
|
163
|
+
START_DISABLE_TRACKING: '1',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(result.exitCode).toBe(1);
|
|
167
|
+
expect(result.stderr).toContain('tracking is disabled');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('executing status', () => {
|
|
172
|
+
it('should show executing status for ongoing commands', () => {
|
|
173
|
+
// Create an executing (not completed) record
|
|
174
|
+
const executingRecord = new ExecutionRecord({
|
|
175
|
+
command: 'sleep 100',
|
|
176
|
+
pid: 99999,
|
|
177
|
+
logPath: '/tmp/executing.log',
|
|
178
|
+
});
|
|
179
|
+
store.save(executingRecord);
|
|
180
|
+
|
|
181
|
+
const result = runCli([
|
|
182
|
+
'--status',
|
|
183
|
+
executingRecord.uuid,
|
|
184
|
+
'--output-format',
|
|
185
|
+
'json',
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
expect(result.exitCode).toBe(0);
|
|
189
|
+
const parsed = JSON.parse(result.stdout);
|
|
190
|
+
expect(parsed.status).toBe('executing');
|
|
191
|
+
expect(parsed.exitCode).toBeNull();
|
|
192
|
+
expect(parsed.endTime).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log('=== Status Query Integration Tests ===');
|