@proletariat/cli 0.3.26 → 0.3.28

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.
Files changed (76) hide show
  1. package/bin/dev.js +7 -0
  2. package/bin/run.js +7 -0
  3. package/dist/commands/action/show.js +7 -1
  4. package/dist/commands/agent/shell.js +24 -10
  5. package/dist/commands/branch/list.js +14 -11
  6. package/dist/commands/branch/validate.js +10 -1
  7. package/dist/commands/claude.js +12 -40
  8. package/dist/commands/docker/clean.js +7 -9
  9. package/dist/commands/docker/index.js +5 -4
  10. package/dist/commands/docker/list.d.ts +1 -0
  11. package/dist/commands/docker/list.js +31 -17
  12. package/dist/commands/docker/status.d.ts +3 -1
  13. package/dist/commands/docker/status.js +28 -2
  14. package/dist/commands/docker/sync.js +7 -6
  15. package/dist/commands/epic/list.js +17 -2
  16. package/dist/commands/execution/list.js +25 -17
  17. package/dist/commands/pmo/init.js +22 -3
  18. package/dist/commands/repo/list.js +14 -8
  19. package/dist/commands/repo/view.js +2 -1
  20. package/dist/commands/roadmap/list.js +16 -1
  21. package/dist/commands/session/health.js +11 -10
  22. package/dist/commands/session/list.js +15 -8
  23. package/dist/commands/staff/list.d.ts +3 -1
  24. package/dist/commands/staff/list.js +15 -1
  25. package/dist/commands/theme/list.d.ts +3 -0
  26. package/dist/commands/theme/list.js +25 -0
  27. package/dist/commands/ticket/complete.js +4 -1
  28. package/dist/commands/ticket/create.d.ts +1 -0
  29. package/dist/commands/ticket/create.js +30 -0
  30. package/dist/commands/ticket/delete.js +3 -3
  31. package/dist/commands/ticket/edit.js +2 -2
  32. package/dist/commands/ticket/list.js +24 -5
  33. package/dist/commands/ticket/move.js +4 -1
  34. package/dist/commands/ticket/view.js +4 -2
  35. package/dist/commands/whoami.d.ts +3 -0
  36. package/dist/commands/whoami.js +22 -5
  37. package/dist/commands/work/complete.js +2 -2
  38. package/dist/commands/work/ready.js +2 -2
  39. package/dist/commands/work/revise.js +2 -2
  40. package/dist/commands/work/spawn.js +6 -21
  41. package/dist/commands/work/start.js +10 -25
  42. package/dist/commands/work/watch.js +57 -33
  43. package/dist/commands/workspace/prune.d.ts +3 -2
  44. package/dist/commands/workspace/prune.js +70 -10
  45. package/dist/lib/agents/commands.js +4 -0
  46. package/dist/lib/agents/index.js +12 -0
  47. package/dist/lib/execution/devcontainer.d.ts +4 -0
  48. package/dist/lib/execution/devcontainer.js +88 -3
  49. package/dist/lib/mcp/helpers.d.ts +15 -0
  50. package/dist/lib/mcp/helpers.js +15 -0
  51. package/dist/lib/mcp/tools/action.js +5 -5
  52. package/dist/lib/mcp/tools/board.js +7 -7
  53. package/dist/lib/mcp/tools/category.js +5 -5
  54. package/dist/lib/mcp/tools/cli-passthrough.js +30 -30
  55. package/dist/lib/mcp/tools/epic.js +8 -8
  56. package/dist/lib/mcp/tools/phase.js +7 -7
  57. package/dist/lib/mcp/tools/project.js +10 -10
  58. package/dist/lib/mcp/tools/roadmap.js +7 -7
  59. package/dist/lib/mcp/tools/spec.js +9 -9
  60. package/dist/lib/mcp/tools/status.js +6 -6
  61. package/dist/lib/mcp/tools/template.js +6 -6
  62. package/dist/lib/mcp/tools/ticket.js +19 -19
  63. package/dist/lib/mcp/tools/view.js +4 -4
  64. package/dist/lib/mcp/tools/work.js +6 -6
  65. package/dist/lib/mcp/tools/workflow.js +5 -5
  66. package/dist/lib/pmo/index.js +4 -0
  67. package/dist/lib/pmo/storage/base.js +49 -0
  68. package/dist/lib/pmo/types.d.ts +1 -1
  69. package/dist/lib/pmo/types.js +1 -0
  70. package/dist/lib/pr/index.d.ts +5 -0
  71. package/dist/lib/pr/index.js +69 -0
  72. package/dist/lib/repos/index.js +4 -0
  73. package/dist/lib/string-utils.d.ts +10 -0
  74. package/dist/lib/string-utils.js +16 -0
  75. package/oclif.manifest.json +3331 -3253
  76. package/package.json +3 -2
package/bin/dev.js CHANGED
@@ -2,4 +2,11 @@
2
2
 
3
3
  import {execute} from '@oclif/core'
4
4
 
5
+ // Support -v as shorthand for --version (only when it's the sole argument,
6
+ // to avoid conflicts with command-specific -v flags like repo create --visibility)
7
+ const args = process.argv.slice(2)
8
+ if (args.length === 1 && args[0] === '-v') {
9
+ process.argv[2] = '--version'
10
+ }
11
+
5
12
  await execute({development: true, dir: import.meta.url})
package/bin/run.js CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  import {execute} from '@oclif/core'
4
4
 
5
+ // Support -v as shorthand for --version (only when it's the sole argument,
6
+ // to avoid conflicts with command-specific -v flags like repo create --visibility)
7
+ const args = process.argv.slice(2)
8
+ if (args.length === 1 && args[0] === '-v') {
9
+ process.argv[2] = '--version'
10
+ }
11
+
5
12
  // Handle process termination gracefully
6
13
  process.on('SIGINT', () => {
7
14
  console.log('\n'); // Add newline for clean exit
@@ -1,6 +1,7 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { styles } from '../../lib/styles.js';
4
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
5
  export default class ActionShow extends PMOCommand {
5
6
  static description = 'Show details of a work action';
6
7
  static examples = [
@@ -20,11 +21,16 @@ export default class ActionShow extends PMOCommand {
20
21
  return { promptIfMultiple: false };
21
22
  }
22
23
  async execute() {
23
- const { args } = await this.parse(ActionShow);
24
+ const { args, flags } = await this.parse(ActionShow);
25
+ const jsonMode = shouldOutputJson(flags);
24
26
  const action = await this.storage.getAction(args.id);
25
27
  if (!action) {
26
28
  this.error(`Action not found: ${args.id}`);
27
29
  }
30
+ if (jsonMode) {
31
+ this.log(JSON.stringify(action, null, 2));
32
+ return;
33
+ }
28
34
  this.log(`\n${styles.emphasis(`Action: ${action.name}`)} ${styles.muted(`(${action.id})`)}`);
29
35
  this.log('');
30
36
  if (action.description) {
@@ -151,16 +151,30 @@ export default class Shell extends PMOCommand {
151
151
  // Interactive mode: Prompt for environment
152
152
  let environment = 'host';
153
153
  if (hasDevcontainer) {
154
- const { selectedEnvironment } = await this.prompt([{
155
- type: 'list',
156
- name: 'selectedEnvironment',
157
- message: 'Where should the shell run?',
158
- choices: [
159
- { name: '🐳 devcontainer (recommended)', value: 'devcontainer', command: '' },
160
- { name: '💻 host (agent worktree on your machine)', value: 'host', command: '' },
161
- ],
162
- }], null);
163
- environment = selectedEnvironment;
154
+ let environmentSelected = false;
155
+ while (!environmentSelected) {
156
+ // eslint-disable-next-line no-await-in-loop -- Interactive loop with retry on Docker check
157
+ const { selectedEnvironment } = await this.prompt([{
158
+ type: 'list',
159
+ name: 'selectedEnvironment',
160
+ message: 'Where should the shell run?',
161
+ choices: [
162
+ { name: '🐳 devcontainer (recommended)', value: 'devcontainer', command: '' },
163
+ { name: '💻 host (agent worktree on your machine)', value: 'host', command: '' },
164
+ ],
165
+ }], null);
166
+ if (selectedEnvironment === 'devcontainer') {
167
+ // Dynamically check Docker when selected (user may have started it)
168
+ if (!isDockerRunning()) {
169
+ this.log('');
170
+ this.warn('Docker is not running. Please start Docker and try again.');
171
+ this.log('');
172
+ continue;
173
+ }
174
+ }
175
+ environment = selectedEnvironment;
176
+ environmentSelected = true;
177
+ }
164
178
  }
165
179
  // Interactive mode: Prompt for display mode
166
180
  const { displayMode } = await this.prompt([{
@@ -2,6 +2,8 @@ import { Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { styles } from '../../lib/styles.js';
4
4
  import { BRANCH_TYPES, listBranches, isGitRepo, } from '../../lib/branch/index.js';
5
+ import { isNonTTY } from '../../lib/prompt-json.js';
6
+ import { visualPadEnd } from '../../lib/string-utils.js';
5
7
  export default class BranchList extends PMOCommand {
6
8
  static description = 'List branches with conventional naming information';
7
9
  static examples = [
@@ -34,6 +36,10 @@ export default class BranchList extends PMOCommand {
34
36
  }
35
37
  async execute() {
36
38
  const { flags } = await this.parse(BranchList);
39
+ // Default format to 'json' in non-TTY environments (piped output, CI, agents)
40
+ if (flags.format === 'table' && isNonTTY()) {
41
+ flags.format = 'json';
42
+ }
37
43
  // Check if in git repo
38
44
  if (!isGitRepo()) {
39
45
  this.error('Not in a git repository.');
@@ -72,10 +78,10 @@ export default class BranchList extends PMOCommand {
72
78
  this.log(styles.header(`🌿 Branches (${branches.length})`));
73
79
  this.log('');
74
80
  // Header
75
- this.log(styles.muted(padEnd('Name', 35) +
76
- padEnd('Type', 8) +
77
- padEnd('Owner', 12) +
78
- padEnd('Description', 25) +
81
+ this.log(styles.muted(visualPadEnd('Name', 35) +
82
+ visualPadEnd('Type', 8) +
83
+ visualPadEnd('Owner', 12) +
84
+ visualPadEnd('Description', 25) +
79
85
  'Status'));
80
86
  this.log('─'.repeat(90));
81
87
  // Rows
@@ -93,10 +99,10 @@ export default class BranchList extends PMOCommand {
93
99
  status = 'current';
94
100
  }
95
101
  this.log(marker +
96
- nameStyle(padEnd(branch.name.substring(0, 33), 33)) +
97
- padEnd(typeDisplay, 8) +
98
- padEnd(ownerDisplay.substring(0, 10), 12) +
99
- padEnd(descDisplay.substring(0, 23), 25) +
102
+ nameStyle(visualPadEnd(branch.name.substring(0, 33), 33)) +
103
+ visualPadEnd(typeDisplay, 8) +
104
+ visualPadEnd(ownerDisplay.substring(0, 10), 12) +
105
+ visualPadEnd(descDisplay.substring(0, 23), 25) +
100
106
  styles.muted(status));
101
107
  }
102
108
  this.log('');
@@ -115,6 +121,3 @@ export default class BranchList extends PMOCommand {
115
121
  this.log('');
116
122
  }
117
123
  }
118
- function padEnd(str, length) {
119
- return str.padEnd(length);
120
- }
@@ -2,6 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { styles } from '../../lib/styles.js';
4
4
  import { BRANCH_TYPES, validateBranchName, getCurrentBranch, isGitRepo, } from '../../lib/branch/index.js';
5
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
5
6
  export default class BranchValidate extends PMOCommand {
6
7
  static description = 'Validate branch name against conventional format';
7
8
  static examples = [
@@ -22,7 +23,8 @@ export default class BranchValidate extends PMOCommand {
22
23
  return { promptIfMultiple: false };
23
24
  }
24
25
  async execute() {
25
- const { args } = await this.parse(BranchValidate);
26
+ const { args, flags } = await this.parse(BranchValidate);
27
+ const jsonMode = shouldOutputJson(flags);
26
28
  let branchName = args.name || '';
27
29
  // Use current branch if not provided
28
30
  if (!branchName) {
@@ -36,6 +38,13 @@ export default class BranchValidate extends PMOCommand {
36
38
  branchName = currentBranch;
37
39
  }
38
40
  const result = validateBranchName(branchName);
41
+ if (jsonMode) {
42
+ this.log(JSON.stringify({ branch: branchName, ...result }, null, 2));
43
+ if (!result.valid) {
44
+ this.exit(1);
45
+ }
46
+ return;
47
+ }
39
48
  this.log('');
40
49
  if (result.valid && result.parts) {
41
50
  if (args.name) {
@@ -145,22 +145,10 @@ export default class Claude extends PromptCommand {
145
145
  environment = flags.environment;
146
146
  }
147
147
  else {
148
- // Check devcontainer prerequisites upfront
149
- const dockerRunning = isDockerRunning();
150
- const devcontainerCliInstalled = isDevcontainerCliInstalled();
151
- const devcontainerReady = dockerRunning && devcontainerCliInstalled;
152
- // Build devcontainer label with missing requirements
153
- let devcontainerLabel = hasProjectDevcontainer
148
+ // Build devcontainer label
149
+ const devcontainerLabel = hasProjectDevcontainer
154
150
  ? '🐳 devcontainer (uses project config, sandboxed)'
155
151
  : '🐳 devcontainer (uses catch-all container, sandboxed)';
156
- if (!devcontainerReady) {
157
- const missing = [];
158
- if (!dockerRunning)
159
- missing.push('Docker');
160
- if (!devcontainerCliInstalled)
161
- missing.push('devcontainer CLI');
162
- devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
163
- }
164
152
  // In JSON mode, output environment prompt and exit
165
153
  if (jsonMode) {
166
154
  await this.prompt([
@@ -172,7 +160,6 @@ export default class Claude extends PromptCommand {
172
160
  {
173
161
  name: devcontainerLabel,
174
162
  value: 'devcontainer',
175
- disabled: !devcontainerReady,
176
163
  command: `prlt claude --slug "${slug}" --environment devcontainer --json`,
177
164
  },
178
165
  {
@@ -181,7 +168,7 @@ export default class Claude extends PromptCommand {
181
168
  command: `prlt claude --slug "${slug}" --environment host --json`,
182
169
  },
183
170
  ],
184
- default: devcontainerReady ? 'devcontainer' : 'host',
171
+ default: 'devcontainer',
185
172
  },
186
173
  ], jsonModeConfig);
187
174
  return; // unreachable, but satisfies TypeScript
@@ -199,18 +186,17 @@ export default class Claude extends PromptCommand {
199
186
  {
200
187
  name: devcontainerLabel,
201
188
  value: 'devcontainer',
202
- disabled: !devcontainerReady,
203
189
  },
204
190
  { name: '💻 host (runs directly on your machine)', value: 'host' },
205
191
  ],
206
- default: devcontainerReady ? 'devcontainer' : 'host',
192
+ default: 'devcontainer',
207
193
  },
208
194
  ], null);
209
195
  if (selectedEnv === 'devcontainer') {
210
- // Double-check prerequisites (in case user retried after starting Docker)
196
+ // Dynamically check Docker when selected (user may have started it)
211
197
  if (!isDockerRunning()) {
212
198
  this.log('');
213
- this.warn('Docker is not running. Please start Docker Desktop or select "host".');
199
+ this.warn('Docker is not running. Please start Docker and try again.');
214
200
  this.log('');
215
201
  continue;
216
202
  }
@@ -553,22 +539,10 @@ export default class Claude extends PromptCommand {
553
539
  }
554
540
  // Prompt for environment first (before creating ticket) so user can cancel early
555
541
  const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
556
- // Check devcontainer prerequisites upfront
557
- const dockerRunning = isDockerRunning();
558
- const devcontainerCliInstalled = isDevcontainerCliInstalled();
559
- const devcontainerReady = dockerRunning && devcontainerCliInstalled;
560
- // Build devcontainer label with missing requirements
561
- let devcontainerLabel = hasProjectDevcontainer
542
+ // Build devcontainer label
543
+ const devcontainerLabel = hasProjectDevcontainer
562
544
  ? '🐳 devcontainer (uses project config, sandboxed)'
563
545
  : '🐳 devcontainer (uses catch-all container, sandboxed)';
564
- if (!devcontainerReady) {
565
- const missing = [];
566
- if (!dockerRunning)
567
- missing.push('Docker');
568
- if (!devcontainerCliInstalled)
569
- missing.push('devcontainer CLI');
570
- devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
571
- }
572
546
  let environment = 'host';
573
547
  if (flags.environment) {
574
548
  environment = flags.environment;
@@ -585,7 +559,6 @@ export default class Claude extends PromptCommand {
585
559
  {
586
560
  name: devcontainerLabel,
587
561
  value: 'devcontainer',
588
- disabled: !devcontainerReady,
589
562
  command: `prlt claude --project ${projectId} --title "${ticketTitle}" --environment devcontainer --json`,
590
563
  },
591
564
  {
@@ -594,7 +567,7 @@ export default class Claude extends PromptCommand {
594
567
  command: `prlt claude --project ${projectId} --title "${ticketTitle}" --environment host --json`,
595
568
  },
596
569
  ],
597
- default: devcontainerReady ? 'devcontainer' : 'host',
570
+ default: 'devcontainer',
598
571
  },
599
572
  ], jsonModeConfig);
600
573
  db.close();
@@ -613,18 +586,17 @@ export default class Claude extends PromptCommand {
613
586
  {
614
587
  name: devcontainerLabel,
615
588
  value: 'devcontainer',
616
- disabled: !devcontainerReady,
617
589
  },
618
590
  { name: '💻 host (runs directly on your machine)', value: 'host' },
619
591
  ],
620
- default: devcontainerReady ? 'devcontainer' : 'host',
592
+ default: 'devcontainer',
621
593
  },
622
594
  ], null);
623
595
  if (selectedEnv === 'devcontainer') {
624
- // Double-check prerequisites (in case user retried after starting Docker)
596
+ // Dynamically check Docker when selected (user may have started it)
625
597
  if (!isDockerRunning()) {
626
598
  this.log('');
627
- this.warn('Docker is not running. Please start Docker Desktop or select "host".');
599
+ this.warn('Docker is not running. Please start Docker and try again.');
628
600
  this.log('');
629
601
  continue;
630
602
  }
@@ -9,6 +9,7 @@ import { isDockerRunning } from '../../lib/execution/runners.js';
9
9
  import { sanitizeContainerId } from '../../lib/docker/resolve.js';
10
10
  import { FlagResolver, shouldOutputJson } from '../../lib/flags/index.js';
11
11
  import { machineOutputFlags } from '../../lib/pmo/base-command.js';
12
+ import { visualPadEnd } from '../../lib/string-utils.js';
12
13
  export default class DockerClean extends Command {
13
14
  static description = 'Remove orphaned containers (containers without running agents)';
14
15
  static examples = [
@@ -72,15 +73,15 @@ export default class DockerClean extends Command {
72
73
  // Display orphaned containers
73
74
  this.log(`\n${styles.header('Orphaned Containers')}`);
74
75
  this.log('='.repeat(80));
75
- this.log(styles.muted(padEnd('Container ID', 15) +
76
- padEnd('Name', 30) +
77
- padEnd('Status', 15) +
76
+ this.log(styles.muted(visualPadEnd('Container ID', 15) +
77
+ visualPadEnd('Name', 30) +
78
+ visualPadEnd('Status', 15) +
78
79
  'Reason'));
79
80
  this.log('-'.repeat(80));
80
81
  for (const container of orphanedContainers) {
81
- this.log(padEnd(container.id, 15) +
82
- padEnd(truncate(container.name, 28), 30) +
83
- padEnd(container.status, 15) +
82
+ this.log(visualPadEnd(container.id, 15) +
83
+ visualPadEnd(truncate(container.name, 28), 30) +
84
+ visualPadEnd(container.status, 15) +
84
85
  styles.muted(container.reason));
85
86
  }
86
87
  if (flags['dry-run']) {
@@ -205,9 +206,6 @@ export default class DockerClean extends Command {
205
206
  }
206
207
  }
207
208
  }
208
- function padEnd(str, length) {
209
- return str.padEnd(length);
210
- }
211
209
  function truncate(str, maxLength) {
212
210
  if (str.length <= maxLength)
213
211
  return str;
@@ -9,6 +9,7 @@ import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.
9
9
  import { isDockerRunning } from '../../lib/execution/runners.js';
10
10
  import { FlagResolver, shouldOutputJson } from '../../lib/flags/index.js';
11
11
  import { machineOutputFlags } from '../../lib/pmo/base-command.js';
12
+ import { visualPadEnd } from '../../lib/string-utils.js';
12
13
  export default class Docker extends PromptCommand {
13
14
  static description = 'Manage Docker containers used by agents';
14
15
  static examples = [
@@ -127,7 +128,7 @@ export default class Docker extends PromptCommand {
127
128
  // Build choices
128
129
  const choices = [];
129
130
  // Column header
130
- const header = styles.muted(` ${'ID'.padEnd(14)} ${'Agent'.padEnd(15)} Status`);
131
+ const header = styles.muted(` ${visualPadEnd('ID', 14)} ${visualPadEnd('Agent', 15)} Status`);
131
132
  // Add active executions first (with container info from DB)
132
133
  const activeExecutions = trackedExecutions.filter(e => e.status === 'running' || e.status === 'starting');
133
134
  if (activeExecutions.length > 0) {
@@ -143,7 +144,7 @@ export default class Docker extends PromptCommand {
143
144
  const statusIcon = isRunning ? styles.success('●') : styles.warning('◐');
144
145
  // Use agent name from execution record (DB source of truth)
145
146
  choices.push({
146
- name: `${statusIcon} ${exec.id.padEnd(14)} ${exec.agentName.padEnd(15)} ${styles.success(exec.status)}`,
147
+ name: `${statusIcon} ${visualPadEnd(exec.id, 14)} ${visualPadEnd(exec.agentName, 15)} ${styles.success(exec.status)}`,
147
148
  value: exec.id,
148
149
  });
149
150
  if (exec.containerId) {
@@ -167,7 +168,7 @@ export default class Docker extends PromptCommand {
167
168
  const statusText = isRunning ? styles.success('running') : styles.muted(container.status);
168
169
  // Agent name from DB (source of truth)
169
170
  choices.push({
170
- name: `${statusIcon} ${container.dockerId.substring(0, 12).padEnd(14)} ${container.agentName.padEnd(15)} ${statusText}`,
171
+ name: `${statusIcon} ${visualPadEnd(container.dockerId.substring(0, 12), 14)} ${visualPadEnd(container.agentName, 15)} ${statusText}`,
171
172
  value: container.dockerId,
172
173
  });
173
174
  addedDockerIds.add(container.dockerId.substring(0, 12));
@@ -185,7 +186,7 @@ export default class Docker extends PromptCommand {
185
186
  // Parse agent from image only for untracked containers
186
187
  const agentName = this.extractAgentFromImage(container.image);
187
188
  choices.push({
188
- name: `${statusIcon} ${container.id.substring(0, 12).padEnd(14)} ${agentName.padEnd(15)} ${statusText}`,
189
+ name: `${statusIcon} ${visualPadEnd(container.id.substring(0, 12), 14)} ${visualPadEnd(agentName, 15)} ${statusText}`,
189
190
  value: container.id,
190
191
  });
191
192
  }
@@ -5,6 +5,7 @@ export default class DockerList extends Command {
5
5
  static flags: {
6
6
  all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
7
  running: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  };
9
10
  run(): Promise<void>;
10
11
  private getTrackedContainers;
@@ -6,6 +6,9 @@ import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
7
7
  import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.js';
8
8
  import { isDockerRunning } from '../../lib/execution/runners.js';
9
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
10
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
11
+ import { visualPadEnd } from '../../lib/string-utils.js';
9
12
  export default class DockerList extends Command {
10
13
  static description = 'Show Docker containers from agent_work table with status';
11
14
  static examples = [
@@ -14,6 +17,7 @@ export default class DockerList extends Command {
14
17
  '<%= config.bin %> <%= command.id %> --running',
15
18
  ];
16
19
  static flags = {
20
+ ...machineOutputFlags,
17
21
  all: Flags.boolean({
18
22
  char: 'a',
19
23
  description: 'Show all containers (including non-devcontainer)',
@@ -27,8 +31,13 @@ export default class DockerList extends Command {
27
31
  };
28
32
  async run() {
29
33
  const { flags } = await this.parse(DockerList);
34
+ const jsonMode = shouldOutputJson(flags);
30
35
  // Check Docker status first
31
36
  if (!isDockerRunning()) {
37
+ if (jsonMode) {
38
+ this.log(JSON.stringify({ error: 'Docker is not running' }, null, 2));
39
+ return;
40
+ }
32
41
  this.log(`\n${styles.error('Docker is not running')}`);
33
42
  this.log(`${styles.muted('Start Docker Desktop or the Docker daemon first.')}\n`);
34
43
  return;
@@ -59,16 +68,24 @@ export default class DockerList extends Command {
59
68
  const dbContainers = containerStorage.listContainers({ limit: 50 });
60
69
  // Get running docker containers
61
70
  const runningContainers = this.getDockerContainers(workspaceInfo.agentsPath, flags.all);
71
+ if (jsonMode) {
72
+ this.log(JSON.stringify({
73
+ executions: trackedExecutions,
74
+ containers: runningContainers,
75
+ }, null, 2));
76
+ db.close();
77
+ return;
78
+ }
62
79
  this.log(`\n${styles.header('Docker Containers')}`);
63
80
  this.log('='.repeat(100));
64
81
  // Display active executions from database
65
82
  const activeExecutions = trackedExecutions.filter(e => e.status === 'running' || e.status === 'starting');
66
83
  if (activeExecutions.length > 0) {
67
84
  this.log(styles.subheader('\nActive Executions:'));
68
- this.log(styles.muted(padEnd('ID', 15) +
69
- padEnd('Agent', 15) +
70
- padEnd('Ticket', 12) +
71
- padEnd('Status', 12) +
85
+ this.log(styles.muted(visualPadEnd('ID', 15) +
86
+ visualPadEnd('Agent', 15) +
87
+ visualPadEnd('Ticket', 12) +
88
+ visualPadEnd('Status', 12) +
72
89
  'Container'));
73
90
  this.log('-'.repeat(80));
74
91
  for (const exec of activeExecutions) {
@@ -80,10 +97,10 @@ export default class DockerList extends Command {
80
97
  const isRunning = runningContainers.some(c => c.id.startsWith(exec.containerId.substring(0, 12)));
81
98
  containerStatus = isRunning ? styles.success(' (up)') : styles.warning(' (down)');
82
99
  }
83
- this.log(padEnd(exec.id, 15) +
84
- padEnd(exec.agentName, 15) +
85
- padEnd(exec.ticketId, 12) +
86
- statusColor(padEnd(exec.status, 12)) +
100
+ this.log(visualPadEnd(exec.id, 15) +
101
+ visualPadEnd(exec.agentName, 15) +
102
+ visualPadEnd(exec.ticketId, 12) +
103
+ statusColor(visualPadEnd(exec.status, 12)) +
87
104
  containerId + containerStatus);
88
105
  }
89
106
  }
@@ -93,9 +110,9 @@ export default class DockerList extends Command {
93
110
  : runningContainers;
94
111
  if (filteredContainers.length > 0) {
95
112
  this.log(styles.subheader('\nDocker Containers' + (flags.all ? ' (all)' : ' (devcontainers)') + ':'));
96
- this.log(styles.muted(padEnd('Container ID', 15) +
97
- padEnd('Agent', 15) +
98
- padEnd('Status', 15) +
113
+ this.log(styles.muted(visualPadEnd('Container ID', 15) +
114
+ visualPadEnd('Agent', 15) +
115
+ visualPadEnd('Status', 15) +
99
116
  'Uptime'));
100
117
  this.log('-'.repeat(70));
101
118
  for (const container of filteredContainers) {
@@ -103,9 +120,9 @@ export default class DockerList extends Command {
103
120
  // Look up agent name from DB (source of truth)
104
121
  const dbContainer = dbContainers.find(c => c.dockerId.startsWith(container.id.substring(0, 12)));
105
122
  const agentName = dbContainer?.agentName || this.extractAgentFromImage(container.image) || container.name;
106
- this.log(padEnd(container.id, 15) +
107
- padEnd(agentName, 15) +
108
- statusColor(padEnd(container.status, 15)) +
123
+ this.log(visualPadEnd(container.id, 15) +
124
+ visualPadEnd(agentName, 15) +
125
+ statusColor(visualPadEnd(container.status, 15)) +
109
126
  styles.muted(container.uptime));
110
127
  }
111
128
  }
@@ -174,9 +191,6 @@ export default class DockerList extends Command {
174
191
  return match ? match[1] : null;
175
192
  }
176
193
  }
177
- function padEnd(str, length) {
178
- return str.padEnd(length);
179
- }
180
194
  function getStatusColor(status) {
181
195
  switch (status) {
182
196
  case 'running':
@@ -2,6 +2,8 @@ import { Command } from '@oclif/core';
2
2
  export default class DockerStatus extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
- static flags: {};
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
6
8
  run(): Promise<void>;
7
9
  }
@@ -2,16 +2,42 @@ import { Command } from '@oclif/core';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { styles } from '../../lib/styles.js';
4
4
  import { isDockerRunning } from '../../lib/execution/runners.js';
5
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
6
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
5
7
  export default class DockerStatus extends Command {
6
8
  static description = 'Check if Docker daemon is running';
7
9
  static examples = [
8
10
  '<%= config.bin %> <%= command.id %>',
9
11
  ];
10
- static flags = {};
12
+ static flags = {
13
+ ...machineOutputFlags,
14
+ };
11
15
  async run() {
16
+ const { flags } = await this.parse(DockerStatus);
17
+ const jsonMode = shouldOutputJson(flags);
18
+ const running = isDockerRunning();
19
+ if (jsonMode) {
20
+ const result = { running };
21
+ if (running) {
22
+ try {
23
+ result.version = execSync('docker version --format "{{.Server.Version}}"', {
24
+ encoding: 'utf-8',
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ }).trim();
27
+ result.info = execSync('docker info --format "{{.Containers}} containers ({{.ContainersRunning}} running)"', {
28
+ encoding: 'utf-8',
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ }).trim();
31
+ }
32
+ catch {
33
+ // Ignore errors getting additional info
34
+ }
35
+ }
36
+ this.log(JSON.stringify(result, null, 2));
37
+ return;
38
+ }
12
39
  this.log(`\n${styles.header('Docker Status')}`);
13
40
  this.log('─'.repeat(50));
14
- const running = isDockerRunning();
15
41
  if (running) {
16
42
  this.log(`${styles.success('Running')} Docker daemon is available`);
17
43
  // Get more details
@@ -6,6 +6,7 @@ import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
7
7
  import { ContainerStorage } from '../../lib/execution/storage.js';
8
8
  import { isDockerRunning } from '../../lib/execution/runners.js';
9
+ import { visualPadEnd } from '../../lib/string-utils.js';
9
10
  export default class DockerSync extends Command {
10
11
  static description = 'Sync container status from Docker into the database';
11
12
  static examples = [
@@ -54,9 +55,9 @@ export default class DockerSync extends Command {
54
55
  if (containers.length > 0) {
55
56
  this.log(styles.subheader('Tracked Containers:'));
56
57
  this.log(styles.muted(' ' +
57
- 'ID'.padEnd(18) +
58
- 'Agent'.padEnd(15) +
59
- 'Status'.padEnd(12) +
58
+ visualPadEnd('ID', 18) +
59
+ visualPadEnd('Agent', 15) +
60
+ visualPadEnd('Status', 12) +
60
61
  'Docker ID'));
61
62
  this.log(styles.muted(' ' + '-'.repeat(65)));
62
63
  for (const container of containers) {
@@ -64,9 +65,9 @@ export default class DockerSync extends Command {
64
65
  container.status === 'exited' ? styles.muted :
65
66
  container.status === 'removed' ? styles.error : styles.warning;
66
67
  this.log(' ' +
67
- container.id.padEnd(18) +
68
- container.agentName.padEnd(15) +
69
- statusColor(container.status.padEnd(12)) +
68
+ visualPadEnd(container.id, 18) +
69
+ visualPadEnd(container.agentName, 15) +
70
+ statusColor(visualPadEnd(container.status, 12)) +
70
71
  styles.muted(container.dockerId.substring(0, 12)));
71
72
  }
72
73
  this.log('');