start-command 0.27.0 → 0.27.2
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 +15 -0
- package/README.md +7 -3
- package/bun.lock +70 -94
- package/package.json +12 -8
- package/src/bin/cli.js +3 -12
- package/src/lib/docker-utils.js +1 -1
- package/src/lib/output-blocks.js +6 -4
- package/src/lib/usage.js +4 -3
- package/test/create-github-release.mjs +118 -0
- package/test/fixtures/issue-126-bin/pgrep +4 -0
- package/test/fixtures/issue-126-bin/screen +6 -0
- package/test/isolation-cleanup.js +120 -109
- package/test/output-blocks.js +25 -0
- package/test/publish-to-crates.mjs +194 -0
- package/test/status-query.js +40 -0
package/src/bin/cli.js
CHANGED
|
@@ -890,7 +890,7 @@ async function runDirectWithCommandStream(
|
|
|
890
890
|
// Using raw() to avoid auto-escaping that might interfere with complex shell commands
|
|
891
891
|
const $cmd = $({ mirror: true, capture: true });
|
|
892
892
|
|
|
893
|
-
let exitCode
|
|
893
|
+
let exitCode;
|
|
894
894
|
try {
|
|
895
895
|
writeLogFile(logFilePath, logContent);
|
|
896
896
|
// Use raw() to pass the command without auto-escaping
|
|
@@ -905,34 +905,25 @@ async function runDirectWithCommandStream(
|
|
|
905
905
|
|
|
906
906
|
// Collect output for log
|
|
907
907
|
if (result.stdout) {
|
|
908
|
-
logContent += result.stdout;
|
|
909
908
|
appendLogFile(logFilePath, result.stdout);
|
|
910
909
|
}
|
|
911
910
|
if (result.stderr) {
|
|
912
|
-
logContent += result.stderr;
|
|
913
911
|
appendLogFile(logFilePath, result.stderr);
|
|
914
912
|
}
|
|
915
913
|
} catch (err) {
|
|
916
914
|
exitCode = err.code || 1;
|
|
917
915
|
const errorMessage = `Error executing command: ${err.message}`;
|
|
918
|
-
logContent += `\n${errorMessage}\n`;
|
|
919
916
|
appendLogFile(logFilePath, `\n${errorMessage}\n`);
|
|
920
917
|
console.error(`\n${errorMessage}`);
|
|
921
918
|
}
|
|
922
919
|
|
|
923
920
|
const endTime = getTimestamp();
|
|
924
921
|
|
|
925
|
-
|
|
926
|
-
logContent += `\n${'='.repeat(50)}\n`;
|
|
927
|
-
logContent += `Finished: ${endTime}\n`;
|
|
928
|
-
logContent += `Exit Code: ${exitCode}\n`;
|
|
922
|
+
const logFooter = `\n${'='.repeat(50)}\nFinished: ${endTime}\nExit Code: ${exitCode}\n`;
|
|
929
923
|
|
|
930
924
|
// Write log file
|
|
931
925
|
try {
|
|
932
|
-
appendLogFile(
|
|
933
|
-
logFilePath,
|
|
934
|
-
`\n${'='.repeat(50)}\nFinished: ${endTime}\nExit Code: ${exitCode}\n`
|
|
935
|
-
);
|
|
926
|
+
appendLogFile(logFilePath, logFooter);
|
|
936
927
|
} catch (err) {
|
|
937
928
|
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
938
929
|
}
|
package/src/lib/docker-utils.js
CHANGED
package/src/lib/output-blocks.js
CHANGED
|
@@ -425,12 +425,13 @@ function formatAsNestedLinksNotation(obj, indent = 2, depth = 0) {
|
|
|
425
425
|
if (obj.length === 0) {
|
|
426
426
|
return '()';
|
|
427
427
|
}
|
|
428
|
-
const
|
|
428
|
+
const blockIndent = ' '.repeat(indent * depth);
|
|
429
|
+
const itemIndent = ' '.repeat(indent * (depth + 1));
|
|
429
430
|
const items = obj.map((item) => {
|
|
430
431
|
const formatted = formatAsNestedLinksNotation(item, indent, depth + 1);
|
|
431
|
-
return `${
|
|
432
|
+
return `${itemIndent}${formatted}`;
|
|
432
433
|
});
|
|
433
|
-
return
|
|
434
|
+
return `${blockIndent}(\n${items.join('\n')}\n${blockIndent})`;
|
|
434
435
|
}
|
|
435
436
|
|
|
436
437
|
// Format objects
|
|
@@ -444,7 +445,8 @@ function formatAsNestedLinksNotation(obj, indent = 2, depth = 0) {
|
|
|
444
445
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
445
446
|
.map(([key, value]) => {
|
|
446
447
|
if (typeof value === 'object') {
|
|
447
|
-
const
|
|
448
|
+
const nestedDepth = Array.isArray(value) ? depth + 2 : depth + 1;
|
|
449
|
+
const nested = formatAsNestedLinksNotation(value, indent, nestedDepth);
|
|
448
450
|
return `${indentStr}${key}\n${nested}`;
|
|
449
451
|
}
|
|
450
452
|
const formattedValue = escapeForLinksNotation(value);
|
package/src/lib/usage.js
CHANGED
|
@@ -9,7 +9,7 @@ Options:
|
|
|
9
9
|
--session, -s <name> Session name for isolation
|
|
10
10
|
--session-id <uuid> Session UUID for tracking (auto-generated if not provided)
|
|
11
11
|
--session-name <uuid> Alias for --session-id
|
|
12
|
-
--image <image> Docker image (
|
|
12
|
+
--image <image> Docker image (optional, defaults to OS-matched image)
|
|
13
13
|
--endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
|
|
14
14
|
--isolated-user, -u [name] Create isolated user with same permissions
|
|
15
15
|
--keep-user Keep isolated user after command completes
|
|
@@ -30,7 +30,8 @@ Examples:
|
|
|
30
30
|
$ bun test
|
|
31
31
|
$ --isolated tmux -- bun start
|
|
32
32
|
$ -i screen -d bun start
|
|
33
|
-
$ --isolated docker --
|
|
33
|
+
$ --isolated docker -- echo "hi" # uses OS-matched default image
|
|
34
|
+
$ --isolated docker --image ghcr.io/link-foundation/box-js:latest -- bun --version
|
|
34
35
|
$ --isolated ssh --endpoint user@remote.server -- ls -la
|
|
35
36
|
$ --isolated-user -- npm test # Create isolated user
|
|
36
37
|
$ -u myuser -- npm start # Custom username
|
|
@@ -59,7 +60,7 @@ Examples:
|
|
|
59
60
|
' - Auto-reports failures for NPM packages (when gh is available)'
|
|
60
61
|
);
|
|
61
62
|
console.log(' - Natural language command aliases (via substitutions.lino)');
|
|
62
|
-
console.log(' - Process isolation via screen, tmux, or
|
|
63
|
+
console.log(' - Process isolation via screen, tmux, docker, or ssh');
|
|
63
64
|
console.log('');
|
|
64
65
|
console.log('Alias examples:');
|
|
65
66
|
console.log(' $ install lodash npm package -> npm install lodash');
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const repoRoot = resolve(testDir, '../..');
|
|
10
|
+
const scriptPath = resolve(repoRoot, 'scripts/create-github-release.mjs');
|
|
11
|
+
|
|
12
|
+
function createFakeGhBin(tempDir) {
|
|
13
|
+
const fakeGhJs = join(tempDir, 'fake-gh.cjs');
|
|
14
|
+
writeFileSync(
|
|
15
|
+
fakeGhJs,
|
|
16
|
+
`
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
|
|
19
|
+
let input = '';
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('data', (chunk) => {
|
|
22
|
+
input += chunk;
|
|
23
|
+
});
|
|
24
|
+
process.stdin.on('end', () => {
|
|
25
|
+
fs.writeFileSync(process.env.FAKE_GH_PAYLOAD_PATH, input);
|
|
26
|
+
|
|
27
|
+
if (process.env.FAKE_GH_MODE === 'already_exists') {
|
|
28
|
+
console.error('gh: Validation Failed (HTTP 422)');
|
|
29
|
+
console.error(JSON.stringify({
|
|
30
|
+
message: 'Validation Failed',
|
|
31
|
+
errors: [{ resource: 'Release', code: 'already_exists', field: 'tag_name' }],
|
|
32
|
+
}));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.FAKE_GH_MODE === 'failure') {
|
|
37
|
+
console.error('gh: server exploded');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(JSON.stringify({ html_url: 'https://github.test/release' }));
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return fakeGhJs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runScript(mode) {
|
|
51
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'create-release-test-'));
|
|
52
|
+
const payloadPath = join(tempDir, 'payload.json');
|
|
53
|
+
const changelogPath = join(tempDir, 'CHANGELOG.md');
|
|
54
|
+
writeFileSync(
|
|
55
|
+
changelogPath,
|
|
56
|
+
'# Changelog\n\n## [1.2.3] - 2026-05-03\n\n- Release automation fix.\n'
|
|
57
|
+
);
|
|
58
|
+
const fakeGhJs = createFakeGhBin(tempDir);
|
|
59
|
+
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
'node',
|
|
62
|
+
[
|
|
63
|
+
scriptPath,
|
|
64
|
+
'--release-version',
|
|
65
|
+
'1.2.3',
|
|
66
|
+
'--repository',
|
|
67
|
+
'owner/repo',
|
|
68
|
+
'--prefix',
|
|
69
|
+
'rust-',
|
|
70
|
+
'--changelog-file',
|
|
71
|
+
changelogPath,
|
|
72
|
+
'--badge-type',
|
|
73
|
+
'crates',
|
|
74
|
+
'--package-name',
|
|
75
|
+
'start-command',
|
|
76
|
+
],
|
|
77
|
+
{
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
FAKE_GH_MODE: mode,
|
|
82
|
+
FAKE_GH_PAYLOAD_PATH: payloadPath,
|
|
83
|
+
START_GH_COMMAND: process.execPath,
|
|
84
|
+
START_GH_COMMAND_ARGS: JSON.stringify([fakeGhJs]),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const payload = JSON.parse(readFileSync(payloadPath, 'utf8'));
|
|
90
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
91
|
+
return { result, payload };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('create-github-release script', () => {
|
|
95
|
+
it('treats an existing GitHub release as an idempotent skip', () => {
|
|
96
|
+
const { result, payload } = runScript('already_exists');
|
|
97
|
+
|
|
98
|
+
expect(result.status).toBe(0);
|
|
99
|
+
expect(result.stdout).toContain(
|
|
100
|
+
'GitHub release already exists: rust-v1.2.3'
|
|
101
|
+
);
|
|
102
|
+
expect(result.stdout).not.toContain('Created GitHub release');
|
|
103
|
+
expect(payload).toMatchObject({
|
|
104
|
+
tag_name: 'rust-v1.2.3',
|
|
105
|
+
name: '[Rust] 1.2.3',
|
|
106
|
+
});
|
|
107
|
+
expect(payload.body).toContain('Release automation fix.');
|
|
108
|
+
expect(payload.body).toContain('crates.io');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('fails when gh api returns an unexpected error', () => {
|
|
112
|
+
const { result } = runScript('failure');
|
|
113
|
+
|
|
114
|
+
expect(result.status).not.toBe(0);
|
|
115
|
+
expect(result.stderr).toContain('server exploded');
|
|
116
|
+
expect(result.stderr).toContain('GitHub release creation failed');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -241,29 +241,116 @@ describe('Isolation Resource Cleanup Verification', () => {
|
|
|
241
241
|
// Use the canRunLinuxDockerImages function from isolation module
|
|
242
242
|
// to properly detect if Linux containers can run (handles Windows containers mode)
|
|
243
243
|
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
244
|
+
const DOCKER_TEST_TIMEOUT = process.platform === 'win32' ? 30000 : 20000;
|
|
245
|
+
const DOCKER_STATE_WAIT_TIMEOUT =
|
|
246
|
+
process.platform === 'win32' ? 20000 : 10000;
|
|
247
|
+
|
|
248
|
+
it(
|
|
249
|
+
'should show docker container as exited after command completes (auto-exit by default)',
|
|
250
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
251
|
+
async () => {
|
|
252
|
+
if (!canRunLinuxDockerImages()) {
|
|
253
|
+
console.log(
|
|
254
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
244
258
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
const containerName = `test-cleanup-docker-${Date.now()}`;
|
|
260
|
+
|
|
261
|
+
// Run a quick command in detached mode
|
|
262
|
+
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
263
|
+
image: 'alpine:latest',
|
|
264
|
+
session: containerName,
|
|
265
|
+
detached: true,
|
|
266
|
+
keepAlive: false,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.strictEqual(result.success, true);
|
|
270
|
+
|
|
271
|
+
// Wait for the container to exit
|
|
272
|
+
const containerExited = await waitFor(() => {
|
|
273
|
+
try {
|
|
274
|
+
const status = execSync(
|
|
275
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
276
|
+
{
|
|
277
|
+
encoding: 'utf8',
|
|
278
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
279
|
+
}
|
|
280
|
+
).trim();
|
|
281
|
+
return status === 'exited';
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}, DOCKER_STATE_WAIT_TIMEOUT);
|
|
286
|
+
|
|
287
|
+
assert.ok(
|
|
288
|
+
containerExited,
|
|
289
|
+
'Docker container should be in exited state after command completes (auto-exit by default)'
|
|
249
290
|
);
|
|
250
|
-
|
|
291
|
+
|
|
292
|
+
// Verify with docker ps -a that container is exited (not running)
|
|
293
|
+
try {
|
|
294
|
+
const allContainers = execSync('docker ps -a', {
|
|
295
|
+
encoding: 'utf8',
|
|
296
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
297
|
+
});
|
|
298
|
+
assert.ok(
|
|
299
|
+
allContainers.includes(containerName),
|
|
300
|
+
'Container should appear in docker ps -a'
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const runningContainers = execSync('docker ps', {
|
|
304
|
+
encoding: 'utf8',
|
|
305
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
306
|
+
});
|
|
307
|
+
assert.ok(
|
|
308
|
+
!runningContainers.includes(containerName),
|
|
309
|
+
'Container should NOT appear in docker ps (not running)'
|
|
310
|
+
);
|
|
311
|
+
console.log(
|
|
312
|
+
' ✓ Docker container auto-exited and stopped (resources released, filesystem preserved)'
|
|
313
|
+
);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
assert.fail(`Failed to verify container status: ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Clean up
|
|
319
|
+
try {
|
|
320
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
321
|
+
} catch {
|
|
322
|
+
// Ignore cleanup errors
|
|
323
|
+
}
|
|
251
324
|
}
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
it(
|
|
328
|
+
'should keep docker container running when keepAlive is true',
|
|
329
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
330
|
+
async () => {
|
|
331
|
+
if (!canRunLinuxDockerImages()) {
|
|
332
|
+
console.log(
|
|
333
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
252
337
|
|
|
253
|
-
|
|
338
|
+
const containerName = `test-keepalive-docker-${Date.now()}`;
|
|
254
339
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
340
|
+
// Run command with keepAlive enabled
|
|
341
|
+
const result = await runInDocker('echo "test"', {
|
|
342
|
+
image: 'alpine:latest',
|
|
343
|
+
session: containerName,
|
|
344
|
+
detached: true,
|
|
345
|
+
keepAlive: true,
|
|
346
|
+
});
|
|
262
347
|
|
|
263
|
-
|
|
348
|
+
assert.strictEqual(result.success, true);
|
|
349
|
+
|
|
350
|
+
// Wait a bit for the command to complete
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
264
352
|
|
|
265
|
-
|
|
266
|
-
const containerExited = await waitFor(() => {
|
|
353
|
+
// Container should still be running
|
|
267
354
|
try {
|
|
268
355
|
const status = execSync(
|
|
269
356
|
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
@@ -272,101 +359,25 @@ describe('Isolation Resource Cleanup Verification', () => {
|
|
|
272
359
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
273
360
|
}
|
|
274
361
|
).trim();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
362
|
+
assert.strictEqual(
|
|
363
|
+
status,
|
|
364
|
+
'running',
|
|
365
|
+
'Container should still be running with keepAlive=true'
|
|
366
|
+
);
|
|
367
|
+
console.log(
|
|
368
|
+
' ✓ Docker container kept running as expected with --keep-alive'
|
|
369
|
+
);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
assert.fail(`Failed to verify container is running: ${err.message}`);
|
|
278
372
|
}
|
|
279
|
-
}, 10000);
|
|
280
|
-
|
|
281
|
-
assert.ok(
|
|
282
|
-
containerExited,
|
|
283
|
-
'Docker container should be in exited state after command completes (auto-exit by default)'
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
// Verify with docker ps -a that container is exited (not running)
|
|
287
|
-
try {
|
|
288
|
-
const allContainers = execSync('docker ps -a', {
|
|
289
|
-
encoding: 'utf8',
|
|
290
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
291
|
-
});
|
|
292
|
-
assert.ok(
|
|
293
|
-
allContainers.includes(containerName),
|
|
294
|
-
'Container should appear in docker ps -a'
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const runningContainers = execSync('docker ps', {
|
|
298
|
-
encoding: 'utf8',
|
|
299
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
300
|
-
});
|
|
301
|
-
assert.ok(
|
|
302
|
-
!runningContainers.includes(containerName),
|
|
303
|
-
'Container should NOT appear in docker ps (not running)'
|
|
304
|
-
);
|
|
305
|
-
console.log(
|
|
306
|
-
' ✓ Docker container auto-exited and stopped (resources released, filesystem preserved)'
|
|
307
|
-
);
|
|
308
|
-
} catch (err) {
|
|
309
|
-
assert.fail(`Failed to verify container status: ${err.message}`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Clean up
|
|
313
|
-
try {
|
|
314
|
-
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
315
|
-
} catch {
|
|
316
|
-
// Ignore cleanup errors
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should keep docker container running when keepAlive is true', async () => {
|
|
321
|
-
if (!canRunLinuxDockerImages()) {
|
|
322
|
-
console.log(
|
|
323
|
-
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
324
|
-
);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const containerName = `test-keepalive-docker-${Date.now()}`;
|
|
329
|
-
|
|
330
|
-
// Run command with keepAlive enabled
|
|
331
|
-
const result = await runInDocker('echo "test"', {
|
|
332
|
-
image: 'alpine:latest',
|
|
333
|
-
session: containerName,
|
|
334
|
-
detached: true,
|
|
335
|
-
keepAlive: true,
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
assert.strictEqual(result.success, true);
|
|
339
|
-
|
|
340
|
-
// Wait a bit for the command to complete
|
|
341
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
342
373
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
350
|
-
}
|
|
351
|
-
).trim();
|
|
352
|
-
assert.strictEqual(
|
|
353
|
-
status,
|
|
354
|
-
'running',
|
|
355
|
-
'Container should still be running with keepAlive=true'
|
|
356
|
-
);
|
|
357
|
-
console.log(
|
|
358
|
-
' ✓ Docker container kept running as expected with --keep-alive'
|
|
359
|
-
);
|
|
360
|
-
} catch (err) {
|
|
361
|
-
assert.fail(`Failed to verify container is running: ${err.message}`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Clean up
|
|
365
|
-
try {
|
|
366
|
-
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
367
|
-
} catch {
|
|
368
|
-
// Ignore cleanup errors
|
|
374
|
+
// Clean up
|
|
375
|
+
try {
|
|
376
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
377
|
+
} catch {
|
|
378
|
+
// Ignore cleanup errors
|
|
379
|
+
}
|
|
369
380
|
}
|
|
370
|
-
|
|
381
|
+
);
|
|
371
382
|
});
|
|
372
383
|
});
|
package/test/output-blocks.js
CHANGED
|
@@ -402,6 +402,31 @@ describe('output-blocks module', () => {
|
|
|
402
402
|
it('should handle null', () => {
|
|
403
403
|
expect(formatAsNestedLinksNotation(null)).toBe('null');
|
|
404
404
|
});
|
|
405
|
+
|
|
406
|
+
it('should indent nested arrays under their parent key', () => {
|
|
407
|
+
const obj = {
|
|
408
|
+
processIds: {
|
|
409
|
+
wrapperPid: 667105,
|
|
410
|
+
screenPid: 667120,
|
|
411
|
+
commandPids: [667121, 667122],
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const result = formatAsNestedLinksNotation(obj);
|
|
416
|
+
|
|
417
|
+
expect(result).toContain(
|
|
418
|
+
[
|
|
419
|
+
' processIds',
|
|
420
|
+
' wrapperPid 667105',
|
|
421
|
+
' screenPid 667120',
|
|
422
|
+
' commandPids',
|
|
423
|
+
' (',
|
|
424
|
+
' 667121',
|
|
425
|
+
' 667122',
|
|
426
|
+
' )',
|
|
427
|
+
].join('\n')
|
|
428
|
+
);
|
|
429
|
+
});
|
|
405
430
|
});
|
|
406
431
|
|
|
407
432
|
describe('Virtual Command API', () => {
|