start-command 0.25.4 → 0.26.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # start-command
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 01fbd90: Add `--list` to show all tracked command executions in Links Notation, JSON, or text format.
8
+
9
+ ## 0.25.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 02527f9: Teach `scripts/create-github-release.mjs` and `scripts/format-github-release.mjs`
14
+ to honour the `--prefix` argument that both workflows already pass. JavaScript
15
+ releases are now tagged `js-v<version>` with title `[JavaScript] <version>`,
16
+ matching the `rust-v<version>` / `[Rust] <version>` convention needed for the
17
+ mono-repo. Extracted the tag/title construction into `scripts/release-name.mjs`
18
+ with unit tests in `test/release-name.test.mjs`. See `docs/case-studies/issue-108/`
19
+ for the full analysis.
20
+
3
21
  ## 0.25.4
4
22
 
5
23
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # start-command — JavaScript package
2
+
3
+ [![npm version](https://img.shields.io/npm/v/start-command?style=flat)](https://www.npmjs.com/package/start-command)
4
+ [![npm downloads](https://img.shields.io/npm/dm/start-command?style=flat)](https://www.npmjs.com/package/start-command)
5
+ [![JavaScript CI/CD](https://github.com/link-foundation/start/actions/workflows/js.yml/badge.svg)](https://github.com/link-foundation/start/actions/workflows/js.yml)
6
+ [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](../LICENSE)
7
+
8
+ JavaScript/Bun implementation of the [`start-command`](../README.md) CLI (`$`).
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ bun install -g start-command
14
+ # or, via npm:
15
+ npm install -g start-command
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ $ ls -la
22
+ $ npm test
23
+ $ git status
24
+ $ --list
25
+ ```
26
+
27
+ See the project-wide [README](../README.md), [docs/USAGE.md](../docs/USAGE.md),
28
+ and [docs/PIPES.md](../docs/PIPES.md) for the full user-facing guide.
29
+
30
+ ## Development
31
+
32
+ ```bash
33
+ cd js
34
+ bun install
35
+ bun test
36
+ bun run lint
37
+ ```
38
+
39
+ ## Releases
40
+
41
+ JavaScript releases are tagged `js-v<version>` and published to both npm and
42
+ GitHub Releases. The release title carries the `[JavaScript]` prefix, e.g.
43
+ `[JavaScript] 0.25.4`, so JS and Rust releases can be told apart at a glance.
44
+
45
+ - **Release history**: https://github.com/link-foundation/start/releases?q=%5BJavaScript%5D
46
+ - **CHANGELOG**: [`CHANGELOG.md`](CHANGELOG.md) (per-package changelog generated
47
+ by [Changesets](https://github.com/changesets/changesets))
48
+
49
+ ## License
50
+
51
+ Released into the public domain under the [Unlicense](../LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.25.4",
3
+ "version": "0.26.0",
4
4
  "description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
5
5
  "main": "src/bin/cli.js",
6
6
  "exports": {
package/src/bin/cli.js CHANGED
@@ -31,10 +31,11 @@ const {
31
31
  } = require('../lib/user-manager');
32
32
  const { handleFailure } = require('../lib/failure-handler');
33
33
  const { ExecutionStore, ExecutionRecord } = require('../lib/execution-store');
34
- const { queryStatus } = require('../lib/status-formatter');
34
+ const { queryStatus, listExecutions } = require('../lib/status-formatter');
35
35
  const { printVersion } = require('../lib/version');
36
36
  const { createStartBlock, createFinishBlock } = require('../lib/output-blocks');
37
37
  const { runWithBunSpawn, runWithNodeSpawn } = require('../lib/spawn-helpers');
38
+ const { printUsage } = require('../lib/usage');
38
39
 
39
40
  // Configuration from environment variables
40
41
  const config = {
@@ -193,6 +194,12 @@ if (wrapperOptions.status) {
193
194
  process.exit(0);
194
195
  }
195
196
 
197
+ // Handle --list flag
198
+ if (wrapperOptions.list) {
199
+ handleListQuery(wrapperOptions.outputFormat);
200
+ process.exit(0);
201
+ }
202
+
196
203
  // Handle --cleanup flag
197
204
  if (wrapperOptions.cleanup) {
198
205
  handleCleanup(wrapperOptions.cleanupDryRun);
@@ -267,6 +274,16 @@ function handleStatusQuery(uuid, outputFormat) {
267
274
  }
268
275
  }
269
276
 
277
+ function handleListQuery(outputFormat) {
278
+ const result = listExecutions(getExecutionStore(), outputFormat);
279
+ if (result.success) {
280
+ console.log(result.output);
281
+ } else {
282
+ console.error(`Error: ${result.error}`);
283
+ process.exit(1);
284
+ }
285
+ }
286
+
270
287
  /**
271
288
  * Handle --cleanup flag
272
289
  * Cleans up stale "executing" records (processes that crashed or were killed)
@@ -314,72 +331,6 @@ function handleCleanup(dryRun) {
314
331
  }
315
332
  }
316
333
 
317
- /** Print usage information */
318
- function printUsage() {
319
- console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>]
320
-
321
- Options:
322
- --isolated, -i <env> Run in isolated environment (screen, tmux, docker, ssh)
323
- --attached, -a Run in attached mode (foreground)
324
- --detached, -d Run in detached mode (background)
325
- --session, -s <name> Session name for isolation
326
- --session-id <uuid> Session UUID for tracking (auto-generated if not provided)
327
- --session-name <uuid> Alias for --session-id
328
- --image <image> Docker image (required for docker isolation)
329
- --endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
330
- --isolated-user, -u [name] Create isolated user with same permissions
331
- --keep-user Keep isolated user after command completes
332
- --keep-alive, -k Keep isolation environment alive after command exits
333
- --auto-remove-docker-container Auto-remove docker container after exit
334
- --shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
335
- --use-command-stream Use command-stream library for execution (experimental)
336
- --status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
337
- --cleanup Clean up stale "executing" records (crashed/killed processes)
338
- --cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
339
- --version, -v Show version information
340
-
341
- Examples:
342
- $ echo "Hello World"
343
- $ bun test
344
- $ --isolated tmux -- bun start
345
- $ -i screen -d bun start
346
- $ --isolated docker --image oven/bun:latest -- bun install
347
- $ --isolated ssh --endpoint user@remote.server -- ls -la
348
- $ --isolated-user -- npm test # Create isolated user
349
- $ -u myuser -- npm start # Custom username
350
- $ -i screen --isolated-user -- npm test # Combine with process isolation
351
- $ --isolated-user --keep-user -- npm start
352
- $ --use-command-stream echo "Hello" # Use command-stream library`);
353
- console.log('');
354
- console.log('Piping with $:');
355
- console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
356
- console.log(
357
- ' $ \'echo "hi" | agent\' # Alternative - quote entire pipeline'
358
- );
359
- console.log('');
360
- console.log('Quoting for special characters:');
361
- console.log(" $ 'npm test && npm build' # Wrap for logical operators");
362
- console.log(" $ 'cat file > output.txt' # Wrap for redirections");
363
- console.log('');
364
- console.log('Features:');
365
- console.log(' - Logs all output to temporary directory');
366
- console.log(' - Displays timestamps and exit codes');
367
- console.log(
368
- ' - Auto-reports failures for NPM packages (when gh is available)'
369
- );
370
- console.log(' - Natural language command aliases (via substitutions.lino)');
371
- console.log(' - Process isolation via screen, tmux, or docker');
372
- console.log('');
373
- console.log('Alias examples:');
374
- console.log(' $ install lodash npm package -> npm install lodash');
375
- console.log(
376
- ' $ install 4.17.21 version of lodash npm package -> npm install lodash@4.17.21'
377
- );
378
- console.log(
379
- ' $ clone https://github.com/user/repo repository -> git clone https://github.com/user/repo'
380
- );
381
- }
382
-
383
334
  /**
384
335
  * Run command in isolation mode
385
336
  * @param {object} options - Wrapper options
@@ -20,7 +20,8 @@
20
20
  * --use-command-stream Use command-stream library for command execution (experimental)
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
- * --output-format <format> Output format for status (links-notation, json, text)
23
+ * --list List all tracked command executions
24
+ * --output-format <format> Output format for status/list (links-notation, json, text)
24
25
  * --cleanup Clean up stale "executing" records (processes that crashed or were killed)
25
26
  * --cleanup-dry-run Show stale records that would be cleaned up (without actually cleaning)
26
27
  */
@@ -169,7 +170,8 @@ function parseArgs(args) {
169
170
  shell: 'auto', // Shell to use in isolation environments: auto, bash, zsh, sh
170
171
  useCommandStream: false, // Use command-stream library for command execution
171
172
  status: null, // UUID to show status for
172
- outputFormat: null, // Output format for status (links-notation, json, text)
173
+ list: false, // List all tracked execution records
174
+ outputFormat: null, // Output format for status/list (links-notation, json, text)
173
175
  cleanup: false, // Clean up stale "executing" records
174
176
  cleanupDryRun: false, // Show what would be cleaned without actually cleaning
175
177
  };
@@ -434,6 +436,12 @@ function parseOption(args, index, options) {
434
436
  return 1;
435
437
  }
436
438
 
439
+ // --list
440
+ if (arg === '--list') {
441
+ options.list = true;
442
+ return 1;
443
+ }
444
+
437
445
  // --output-format <format>
438
446
  if (arg === '--output-format') {
439
447
  if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
@@ -656,9 +664,16 @@ function validateOptions(options) {
656
664
  }
657
665
  }
658
666
 
659
- // Output format is only valid with --status
660
- if (options.outputFormat && !options.status) {
661
- throw new Error('--output-format option is only valid with --status');
667
+ // Single-record and list query modes are mutually exclusive
668
+ if (options.status && options.list) {
669
+ throw new Error('Cannot use both --status and --list at the same time');
670
+ }
671
+
672
+ // Output format is only valid with query modes
673
+ if (options.outputFormat && !options.status && !options.list) {
674
+ throw new Error(
675
+ '--output-format option is only valid with --status or --list'
676
+ );
662
677
  }
663
678
 
664
679
  // Validate shell option
@@ -273,6 +273,86 @@ function formatRecord(record, format) {
273
273
  }
274
274
  }
275
275
 
276
+ function sortRecordsByStartTimeDesc(records) {
277
+ return [...records].sort(
278
+ (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
279
+ );
280
+ }
281
+
282
+ function indentBlock(block, spaces) {
283
+ const prefix = ' '.repeat(spaces);
284
+ return block
285
+ .split('\n')
286
+ .map((line) => `${prefix}${line}`)
287
+ .join('\n');
288
+ }
289
+
290
+ function prepareRecordForList(record) {
291
+ return attachCurrentTime(enrichDetachedStatus(record));
292
+ }
293
+
294
+ /**
295
+ * Format execution records as a Links Notation list.
296
+ * @param {Object[]} records - Execution records
297
+ * @returns {string} Links Notation formatted list
298
+ */
299
+ function formatRecordListAsLinksNotation(records) {
300
+ const lines = ['executions', ` count ${records.length}`];
301
+
302
+ if (records.length === 0) {
303
+ lines.push(' records ()');
304
+ return lines.join('\n');
305
+ }
306
+
307
+ lines.push(' records');
308
+ for (const record of records) {
309
+ lines.push(indentBlock(formatRecordAsLinksNotation(record), 4));
310
+ }
311
+
312
+ return lines.join('\n');
313
+ }
314
+
315
+ /**
316
+ * Format execution records as human-readable text.
317
+ * @param {Object[]} records - Execution records
318
+ * @returns {string} Human-readable list
319
+ */
320
+ function formatRecordListAsText(records) {
321
+ const lines = ['Executions', `${'='.repeat(50)}`, `Count: ${records.length}`];
322
+
323
+ for (const record of records) {
324
+ lines.push('', formatRecordAsText(record));
325
+ }
326
+
327
+ return lines.join('\n');
328
+ }
329
+
330
+ /**
331
+ * Format execution records based on format type.
332
+ * @param {Object[]} records - Execution records
333
+ * @param {string} format - Output format (links-notation, json, text)
334
+ * @returns {string} Formatted output string
335
+ */
336
+ function formatRecordList(records, format) {
337
+ switch (format) {
338
+ case 'links-notation':
339
+ return formatRecordListAsLinksNotation(records);
340
+ case 'json':
341
+ return JSON.stringify(
342
+ {
343
+ count: records.length,
344
+ executions: records.map((record) => record.toObject()),
345
+ },
346
+ null,
347
+ 2
348
+ );
349
+ case 'text':
350
+ return formatRecordListAsText(records);
351
+ default:
352
+ throw new Error(`Unknown output format: ${format}`);
353
+ }
354
+ }
355
+
276
356
  /**
277
357
  * Handle status query and output the result
278
358
  * @param {Object} store - ExecutionStore instance
@@ -305,11 +385,39 @@ function queryStatus(store, identifier, outputFormat) {
305
385
  }
306
386
  }
307
387
 
388
+ /**
389
+ * Handle execution list query and output the result
390
+ * @param {Object} store - ExecutionStore instance
391
+ * @param {string|null} outputFormat - Output format (links-notation, json, text)
392
+ * @returns {{success: boolean, output?: string, error?: string}}
393
+ */
394
+ function listExecutions(store, outputFormat) {
395
+ if (!store) {
396
+ return { success: false, error: 'Execution tracking is disabled.' };
397
+ }
398
+
399
+ try {
400
+ const records = sortRecordsByStartTimeDesc(
401
+ store.getAll().map(prepareRecordForList)
402
+ );
403
+ return {
404
+ success: true,
405
+ output: formatRecordList(records, outputFormat || 'links-notation'),
406
+ };
407
+ } catch (err) {
408
+ return { success: false, error: err.message };
409
+ }
410
+ }
411
+
308
412
  module.exports = {
309
413
  formatRecordAsLinksNotation,
310
414
  formatRecordAsText,
311
415
  formatRecord,
416
+ formatRecordListAsLinksNotation,
417
+ formatRecordListAsText,
418
+ formatRecordList,
312
419
  queryStatus,
420
+ listExecutions,
313
421
  isDetachedSessionAlive,
314
422
  enrichDetachedStatus,
315
423
  attachCurrentTime,
@@ -0,0 +1,70 @@
1
+ /** Print usage information */
2
+ function printUsage() {
3
+ console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--output-format <fmt>]
4
+
5
+ Options:
6
+ --isolated, -i <env> Run in isolated environment (screen, tmux, docker, ssh)
7
+ --attached, -a Run in attached mode (foreground)
8
+ --detached, -d Run in detached mode (background)
9
+ --session, -s <name> Session name for isolation
10
+ --session-id <uuid> Session UUID for tracking (auto-generated if not provided)
11
+ --session-name <uuid> Alias for --session-id
12
+ --image <image> Docker image (required for docker isolation)
13
+ --endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
14
+ --isolated-user, -u [name] Create isolated user with same permissions
15
+ --keep-user Keep isolated user after command completes
16
+ --keep-alive, -k Keep isolation environment alive after command exits
17
+ --auto-remove-docker-container Auto-remove docker container after exit
18
+ --shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
19
+ --use-command-stream Use command-stream library for execution (experimental)
20
+ --status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
21
+ --list List all tracked executions (--output-format: links-notation|json|text)
22
+ --cleanup Clean up stale "executing" records (crashed/killed processes)
23
+ --cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
24
+ --version, -v Show version information
25
+
26
+ Examples:
27
+ $ echo "Hello World"
28
+ $ bun test
29
+ $ --isolated tmux -- bun start
30
+ $ -i screen -d bun start
31
+ $ --isolated docker --image oven/bun:latest -- bun install
32
+ $ --isolated ssh --endpoint user@remote.server -- ls -la
33
+ $ --isolated-user -- npm test # Create isolated user
34
+ $ -u myuser -- npm start # Custom username
35
+ $ -i screen --isolated-user -- npm test # Combine with process isolation
36
+ $ --isolated-user --keep-user -- npm start
37
+ $ --list # List stored execution records
38
+ $ --list --output-format json # List stored records as JSON
39
+ $ --use-command-stream echo "Hello" # Use command-stream library`);
40
+ console.log('');
41
+ console.log('Piping with $:');
42
+ console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
43
+ console.log(
44
+ ' $ \'echo "hi" | agent\' # Alternative - quote entire pipeline'
45
+ );
46
+ console.log('');
47
+ console.log('Quoting for special characters:');
48
+ console.log(" $ 'npm test && npm build' # Wrap for logical operators");
49
+ console.log(" $ 'cat file > output.txt' # Wrap for redirections");
50
+ console.log('');
51
+ console.log('Features:');
52
+ console.log(' - Logs all output to temporary directory');
53
+ console.log(' - Displays timestamps and exit codes');
54
+ console.log(
55
+ ' - Auto-reports failures for NPM packages (when gh is available)'
56
+ );
57
+ console.log(' - Natural language command aliases (via substitutions.lino)');
58
+ console.log(' - Process isolation via screen, tmux, or docker');
59
+ console.log('');
60
+ console.log('Alias examples:');
61
+ console.log(' $ install lodash npm package -> npm install lodash');
62
+ console.log(
63
+ ' $ install 4.17.21 version of lodash npm package -> npm install lodash@4.17.21'
64
+ );
65
+ console.log(
66
+ ' $ clone https://github.com/user/repo repository -> git clone https://github.com/user/repo'
67
+ );
68
+ }
69
+
70
+ module.exports = { printUsage };
@@ -817,13 +817,13 @@ describe('status option', () => {
817
817
  it('should throw error for missing UUID argument', () => {
818
818
  assert.throws(() => {
819
819
  parseArgs(['--status']);
820
- }, /requires a UUID argument/);
820
+ }, /requires a UUID or session name argument/);
821
821
  });
822
822
 
823
823
  it('should throw error for --status with -flag as next argument', () => {
824
824
  assert.throws(() => {
825
825
  parseArgs(['--status', '--output-format']);
826
- }, /requires a UUID argument/);
826
+ }, /requires a UUID or session name argument/);
827
827
  });
828
828
 
829
829
  it('should default status to null', () => {
@@ -832,6 +832,30 @@ describe('status option', () => {
832
832
  });
833
833
  });
834
834
 
835
+ describe('list option', () => {
836
+ it('should parse --list flag', () => {
837
+ const result = parseArgs(['--list']);
838
+ assert.strictEqual(result.wrapperOptions.list, true);
839
+ assert.strictEqual(result.command, '');
840
+ });
841
+
842
+ it('should parse --list with JSON output format', () => {
843
+ const result = parseArgs(['--list', '--output-format', 'json']);
844
+ assert.strictEqual(result.wrapperOptions.list, true);
845
+ assert.strictEqual(result.wrapperOptions.outputFormat, 'json');
846
+ });
847
+
848
+ it('should normalize output format with --list', () => {
849
+ const result = parseArgs(['--list', '--output-format', 'TEXT']);
850
+ assert.strictEqual(result.wrapperOptions.outputFormat, 'text');
851
+ });
852
+
853
+ it('should default list to false', () => {
854
+ const result = parseArgs(['echo', 'hello']);
855
+ assert.strictEqual(result.wrapperOptions.list, false);
856
+ });
857
+ });
858
+
835
859
  describe('output-format option', () => {
836
860
  it('should parse --output-format with --status', () => {
837
861
  const uuid = '12345678-1234-1234-1234-123456789abc';
@@ -874,7 +898,7 @@ describe('output-format option', () => {
874
898
  it('should throw error for output-format without status', () => {
875
899
  assert.throws(() => {
876
900
  parseArgs(['--output-format', 'json', '--', 'npm', 'test']);
877
- }, /--output-format option is only valid with --status/);
901
+ }, /--output-format option is only valid with --status or --list/);
878
902
  });
879
903
 
880
904
  it('should accept all valid output formats', () => {
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { releaseName, releaseTag } from '../../scripts/release-name.mjs';
3
+
4
+ describe('releaseTag', () => {
5
+ it('uses plain "v${version}" when no prefix is given', () => {
6
+ expect(releaseTag('0.25.4')).toBe('v0.25.4');
7
+ expect(releaseTag('0.25.4', '')).toBe('v0.25.4');
8
+ });
9
+
10
+ it('prepends known language prefixes', () => {
11
+ expect(releaseTag('0.25.4', 'js-')).toBe('js-v0.25.4');
12
+ expect(releaseTag('0.14.0', 'rust-')).toBe('rust-v0.14.0');
13
+ });
14
+
15
+ it('passes arbitrary prefixes through', () => {
16
+ expect(releaseTag('1.0.0', 'api-')).toBe('api-v1.0.0');
17
+ });
18
+ });
19
+
20
+ describe('releaseName', () => {
21
+ it('returns bare version when prefix is empty (preserves pre-issue-108 behaviour)', () => {
22
+ expect(releaseName('0.25.4')).toBe('0.25.4');
23
+ expect(releaseName('0.25.4', '')).toBe('0.25.4');
24
+ });
25
+
26
+ it('decorates known language prefixes with human titles', () => {
27
+ expect(releaseName('0.25.4', 'js-')).toBe('[JavaScript] 0.25.4');
28
+ expect(releaseName('0.14.0', 'rust-')).toBe('[Rust] 0.14.0');
29
+ });
30
+
31
+ it('falls back to "${prefix}${version}" for unknown prefixes', () => {
32
+ expect(releaseName('1.0.0', 'api-')).toBe('api-1.0.0');
33
+ });
34
+ });
@@ -168,6 +168,77 @@ describe('--status query functionality', () => {
168
168
  });
169
169
  });
170
170
 
171
+ describe('--list query functionality', () => {
172
+ it('should output all tracked executions in links-notation format by default', () => {
173
+ const executingRecord = new ExecutionRecord({
174
+ command: 'sleep 100',
175
+ pid: 99999,
176
+ logPath: '/tmp/executing.log',
177
+ startTime: '2026-04-24T10:00:00.000Z',
178
+ });
179
+ store.save(executingRecord);
180
+
181
+ const result = runCli(['--list']);
182
+
183
+ expect(result.exitCode).toBe(0);
184
+ expect(result.stdout).toMatch(/^executions\n/);
185
+ expect(result.stdout).toContain(' count 2');
186
+ expect(result.stdout).toContain(` ${testRecord.uuid}`);
187
+ expect(result.stdout).toContain(` ${executingRecord.uuid}`);
188
+ expect(result.stdout).toContain(' command "echo hello world"');
189
+ expect(result.stdout).toContain(' command "sleep 100"');
190
+ expect(result.stdout).toContain(' status executed');
191
+ expect(result.stdout).toContain(' status executing');
192
+ });
193
+
194
+ it('should output all tracked executions in JSON format', () => {
195
+ const executingRecord = new ExecutionRecord({
196
+ command: 'sleep 100',
197
+ pid: 99999,
198
+ logPath: '/tmp/executing.log',
199
+ });
200
+ store.save(executingRecord);
201
+
202
+ const result = runCli(['--list', '--output-format', 'json']);
203
+
204
+ expect(result.exitCode).toBe(0);
205
+ const parsed = JSON.parse(result.stdout);
206
+ expect(parsed.count).toBe(2);
207
+ expect(parsed.executions.length).toBe(2);
208
+ expect(parsed.executions.map((record) => record.uuid)).toContain(
209
+ testRecord.uuid
210
+ );
211
+ expect(parsed.executions.map((record) => record.uuid)).toContain(
212
+ executingRecord.uuid
213
+ );
214
+ const executing = parsed.executions.find(
215
+ (record) => record.uuid === executingRecord.uuid
216
+ );
217
+ expect(executing.status).toBe('executing');
218
+ expect(executing.currentTime).toBeDefined();
219
+ });
220
+
221
+ it('should show count zero when no records are stored', () => {
222
+ cleanupTestDir();
223
+
224
+ const result = runCli(['--list']);
225
+
226
+ expect(result.exitCode).toBe(0);
227
+ expect(result.stdout).toMatch(/^executions\n/);
228
+ expect(result.stdout).toContain(' count 0');
229
+ expect(result.stdout).toContain(' records ()');
230
+ });
231
+
232
+ it('should show error when tracking is disabled', () => {
233
+ const result = runCli(['--list'], {
234
+ START_DISABLE_TRACKING: '1',
235
+ });
236
+
237
+ expect(result.exitCode).toBe(1);
238
+ expect(result.stderr).toContain('tracking is disabled');
239
+ });
240
+ });
241
+
171
242
  describe('executing status', () => {
172
243
  it('should show executing status for ongoing commands', () => {
173
244
  // Create an executing (not completed) record