@proletariat/cli 0.3.9 → 0.3.11

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 (152) hide show
  1. package/README.md +25 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/action/index.js +1 -1
  4. package/dist/commands/action/run.js +8 -12
  5. package/dist/commands/agent/auth.d.ts +30 -0
  6. package/dist/commands/agent/auth.js +172 -0
  7. package/dist/commands/agent/discover.d.ts +9 -0
  8. package/dist/commands/agent/discover.js +67 -0
  9. package/dist/commands/agent/index.js +47 -12
  10. package/dist/commands/agent/list.d.ts +4 -1
  11. package/dist/commands/agent/list.js +78 -16
  12. package/dist/commands/agent/login.js +35 -31
  13. package/dist/commands/agent/restart.js +2 -0
  14. package/dist/commands/agent/shell.js +78 -19
  15. package/dist/commands/agent/staff/add.js +1 -12
  16. package/dist/commands/agent/staff/remove.js +9 -7
  17. package/dist/commands/agent/status.js +17 -4
  18. package/dist/commands/agent/temp/cleanup.js +7 -3
  19. package/dist/commands/agent/themes/index.js +4 -5
  20. package/dist/commands/agent/themes/list.js +5 -5
  21. package/dist/commands/agent/visit.js +17 -4
  22. package/dist/commands/branch/create.d.ts +4 -0
  23. package/dist/commands/branch/create.js +16 -8
  24. package/dist/commands/branch/index.js +1 -1
  25. package/dist/commands/branch/where.js +1 -0
  26. package/dist/commands/claude.d.ts +38 -0
  27. package/dist/commands/claude.js +899 -0
  28. package/dist/commands/commit.js +1 -1
  29. package/dist/commands/config/index.d.ts +12 -0
  30. package/dist/commands/config/index.js +271 -0
  31. package/dist/commands/docker/clean.js +2 -2
  32. package/dist/commands/docker/index.js +2 -2
  33. package/dist/commands/docker/list.js +3 -8
  34. package/dist/commands/docker/logs.js +2 -2
  35. package/dist/commands/docker/prune.js +1 -1
  36. package/dist/commands/docker/restart.js +2 -2
  37. package/dist/commands/docker/shell.js +2 -2
  38. package/dist/commands/docker/start.js +2 -2
  39. package/dist/commands/docker/status.js +1 -1
  40. package/dist/commands/docker/stop.js +2 -2
  41. package/dist/commands/docker/sync.js +2 -2
  42. package/dist/commands/epic/index.js +1 -1
  43. package/dist/commands/epic/link/index.js +25 -14
  44. package/dist/commands/epic/link/remove.js +2 -0
  45. package/dist/commands/epic/list.js +5 -5
  46. package/dist/commands/epic/progress.js +10 -4
  47. package/dist/commands/epic/spec.js +2 -0
  48. package/dist/commands/epic/ticket.js +3 -0
  49. package/dist/commands/execution/stop.js +1 -0
  50. package/dist/commands/init.js +4 -4
  51. package/dist/commands/project/index.js +1 -1
  52. package/dist/commands/project/spec.js +7 -0
  53. package/dist/commands/repo/add.js +1 -0
  54. package/dist/commands/repo/remove.js +1 -0
  55. package/dist/commands/roadmap/add-project.d.ts +18 -0
  56. package/dist/commands/roadmap/add-project.js +135 -0
  57. package/dist/commands/roadmap/create.d.ts +22 -0
  58. package/dist/commands/roadmap/create.js +156 -0
  59. package/dist/commands/roadmap/delete.d.ts +17 -0
  60. package/dist/commands/roadmap/delete.js +104 -0
  61. package/dist/commands/roadmap/generate.d.ts +22 -0
  62. package/dist/commands/roadmap/generate.js +201 -0
  63. package/dist/commands/roadmap/index.d.ts +13 -0
  64. package/dist/commands/roadmap/index.js +61 -0
  65. package/dist/commands/roadmap/list.d.ts +12 -0
  66. package/dist/commands/roadmap/list.js +42 -0
  67. package/dist/commands/roadmap/remove-project.d.ts +18 -0
  68. package/dist/commands/roadmap/remove-project.js +147 -0
  69. package/dist/commands/roadmap/reorder.d.ts +17 -0
  70. package/dist/commands/roadmap/reorder.js +157 -0
  71. package/dist/commands/roadmap/update.d.ts +19 -0
  72. package/dist/commands/roadmap/update.js +136 -0
  73. package/dist/commands/roadmap/view.d.ts +16 -0
  74. package/dist/commands/roadmap/view.js +103 -0
  75. package/dist/commands/spec/index.js +1 -1
  76. package/dist/commands/spec/link/index.js +24 -13
  77. package/dist/commands/spec/link/remove.js +2 -0
  78. package/dist/commands/status/index.js +1 -1
  79. package/dist/commands/status/list.js +0 -8
  80. package/dist/commands/template/delete.js +2 -0
  81. package/dist/commands/terminal/title.d.ts +12 -0
  82. package/dist/commands/terminal/title.js +48 -0
  83. package/dist/commands/ticket/complete.js +2 -0
  84. package/dist/commands/ticket/create.js +4 -2
  85. package/dist/commands/ticket/delete.js +2 -0
  86. package/dist/commands/ticket/edit.js +8 -2
  87. package/dist/commands/ticket/link/index.js +17 -3
  88. package/dist/commands/ticket/link/remove.js +2 -0
  89. package/dist/commands/ticket/list.js +1 -2
  90. package/dist/commands/ticket/move.js +2 -0
  91. package/dist/commands/ticket/project.js +3 -1
  92. package/dist/commands/ticket/reassign.js +2 -0
  93. package/dist/commands/ticket/spec.js +4 -2
  94. package/dist/commands/ticket/template/apply.js +4 -3
  95. package/dist/commands/ticket/template/create.js +2 -0
  96. package/dist/commands/ticket/template/index.js +1 -1
  97. package/dist/commands/ticket/update.js +2 -0
  98. package/dist/commands/work/index.js +1 -1
  99. package/dist/commands/work/revise.js +7 -1
  100. package/dist/commands/work/spawn.d.ts +2 -1
  101. package/dist/commands/work/spawn.js +131 -36
  102. package/dist/commands/work/start.d.ts +2 -1
  103. package/dist/commands/work/start.js +349 -69
  104. package/dist/commands/work/watch.js +10 -2
  105. package/dist/commands/workflow/create.js +3 -3
  106. package/dist/commands/workflow/switch.js +2 -1
  107. package/dist/commands/workspace/remove.js +0 -8
  108. package/dist/commands/workspace/use.js +1 -9
  109. package/dist/lib/agents/commands.js +18 -13
  110. package/dist/lib/database/index.d.ts +19 -12
  111. package/dist/lib/database/index.js +158 -42
  112. package/dist/lib/docker/resolve.js +1 -1
  113. package/dist/lib/execution/config.d.ts +6 -0
  114. package/dist/lib/execution/config.js +15 -2
  115. package/dist/lib/execution/devcontainer.d.ts +2 -0
  116. package/dist/lib/execution/devcontainer.js +41 -9
  117. package/dist/lib/execution/runners.d.ts +85 -3
  118. package/dist/lib/execution/runners.js +925 -228
  119. package/dist/lib/execution/spawner.d.ts +2 -2
  120. package/dist/lib/execution/spawner.js +4 -3
  121. package/dist/lib/execution/storage.d.ts +2 -1
  122. package/dist/lib/execution/storage.js +9 -13
  123. package/dist/lib/execution/types.d.ts +10 -1
  124. package/dist/lib/execution/types.js +3 -1
  125. package/dist/lib/init/index.js +1 -0
  126. package/dist/lib/machine-config.js +1 -1
  127. package/dist/lib/pmo/base-command.js +5 -9
  128. package/dist/lib/pmo/index.js +2 -0
  129. package/dist/lib/pmo/schema.d.ts +6 -0
  130. package/dist/lib/pmo/schema.js +36 -0
  131. package/dist/lib/pmo/storage/base.js +3 -3
  132. package/dist/lib/pmo/storage/index.d.ts +16 -1
  133. package/dist/lib/pmo/storage/index.js +45 -0
  134. package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
  135. package/dist/lib/pmo/storage/roadmaps.js +301 -0
  136. package/dist/lib/pmo/storage/specs.js +2 -0
  137. package/dist/lib/pmo/storage/types.d.ts +14 -0
  138. package/dist/lib/pmo/sync-manager.d.ts +1 -1
  139. package/dist/lib/pmo/sync-manager.js +1 -1
  140. package/dist/lib/pmo/types.d.ts +41 -0
  141. package/dist/lib/pmo/utils.d.ts +2 -0
  142. package/dist/lib/pmo/utils.js +22 -1
  143. package/dist/lib/repos/index.js +7 -1
  144. package/dist/lib/terminal.d.ts +31 -0
  145. package/dist/lib/terminal.js +48 -0
  146. package/dist/lib/themes.d.ts +21 -3
  147. package/dist/lib/themes.js +80 -23
  148. package/dist/lib/workspace-config.d.ts +80 -0
  149. package/dist/lib/workspace-config.js +100 -0
  150. package/oclif.manifest.json +4065 -3225
  151. package/package.json +10 -6
  152. package/LICENSE +0 -21
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
2
+ import { execSync } from 'node:child_process';
3
3
  import inquirer from 'inquirer';
4
4
  import { validateBranchName } from '../lib/branch/index.js';
5
5
  import { styles } from '../lib/styles.js';
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Config extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ list: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ private setConfigValue;
12
+ }
@@ -0,0 +1,271 @@
1
+ import { Flags } from '@oclif/core';
2
+ import * as path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import inquirer from 'inquirer';
5
+ import { Command } from '@oclif/core';
6
+ import { styles } from '../../lib/styles.js';
7
+ import { getWorkspaceInfo } from '../../lib/agents/commands.js';
8
+ import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, } from '../../lib/execution/config.js';
9
+ import { shouldOutputJson, outputPromptAsJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
10
+ export default class Config extends Command {
11
+ static description = 'View and update workspace configuration';
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %> # Interactive menu',
14
+ '<%= config.bin %> <%= command.id %> --json # Output current config as JSON',
15
+ '<%= config.bin %> <%= command.id %> --set terminal.app iTerm',
16
+ '<%= config.bin %> <%= command.id %> --set terminal.openInBackground true',
17
+ ];
18
+ static flags = {
19
+ json: Flags.boolean({
20
+ description: 'Output configuration as JSON (for AI agents/scripts)',
21
+ default: false,
22
+ }),
23
+ set: Flags.string({
24
+ char: 's',
25
+ description: 'Set a config value (format: key value)',
26
+ multiple: true,
27
+ }),
28
+ list: Flags.boolean({
29
+ char: 'l',
30
+ description: 'List all configuration values',
31
+ default: false,
32
+ }),
33
+ };
34
+ async run() {
35
+ const { flags } = await this.parse(Config);
36
+ const jsonMode = shouldOutputJson(flags);
37
+ // Get workspace info
38
+ let workspaceInfo;
39
+ try {
40
+ workspaceInfo = getWorkspaceInfo();
41
+ }
42
+ catch {
43
+ if (jsonMode) {
44
+ outputErrorAsJson('NOT_IN_WORKSPACE', 'Not in a workspace. Run "prlt init" first.', createMetadata('config', flags));
45
+ this.exit(1);
46
+ }
47
+ this.error('Not in a workspace. Run "prlt init" first.');
48
+ }
49
+ // Open database
50
+ const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
51
+ const db = new Database(dbPath);
52
+ try {
53
+ // Load current config
54
+ const config = loadExecutionConfig(db);
55
+ // Handle --set flag
56
+ if (flags.set && flags.set.length > 0) {
57
+ for (const setValue of flags.set) {
58
+ const [key, ...valueParts] = setValue.split(' ');
59
+ const value = valueParts.join(' ');
60
+ if (!key || !value) {
61
+ if (jsonMode) {
62
+ outputErrorAsJson('INVALID_SET_FORMAT', `Invalid format: "${setValue}". Use: --set "key value"`, createMetadata('config', flags));
63
+ }
64
+ else {
65
+ this.error(`Invalid format: "${setValue}". Use: --set "key value"`);
66
+ }
67
+ continue;
68
+ }
69
+ this.setConfigValue(db, key, value, jsonMode);
70
+ }
71
+ db.close();
72
+ return;
73
+ }
74
+ // Handle --list or --json flag (just show config)
75
+ if (flags.list || flags.json) {
76
+ if (jsonMode) {
77
+ outputSuccessAsJson({
78
+ terminal: {
79
+ app: config.terminal.app,
80
+ openInBackground: config.terminal.openInBackground,
81
+ },
82
+ shell: config.shell,
83
+ tmux: {
84
+ controlMode: config.tmux.controlMode,
85
+ },
86
+ defaultExecutor: config.defaultExecutor,
87
+ defaultEnvironment: config.defaultEnvironment,
88
+ outputMode: config.outputMode,
89
+ sandboxed: config.sandboxed,
90
+ }, createMetadata('config', flags));
91
+ }
92
+ else {
93
+ this.log('');
94
+ this.log(styles.header('Workspace Configuration'));
95
+ this.log('═'.repeat(50));
96
+ this.log('');
97
+ this.log(styles.emphasis('Terminal'));
98
+ this.log(` app: ${config.terminal.app}`);
99
+ this.log(` openInBackground: ${config.terminal.openInBackground}`);
100
+ this.log('');
101
+ this.log(styles.emphasis('Shell'));
102
+ this.log(` shell: ${config.shell}`);
103
+ this.log('');
104
+ this.log(styles.emphasis('Tmux'));
105
+ this.log(` controlMode: ${config.tmux.controlMode}`);
106
+ this.log('');
107
+ this.log(styles.emphasis('Execution'));
108
+ this.log(` defaultExecutor: ${config.defaultExecutor}`);
109
+ this.log(` defaultEnvironment: ${config.defaultEnvironment}`);
110
+ this.log(` outputMode: ${config.outputMode}`);
111
+ this.log(` sandboxed: ${config.sandboxed}`);
112
+ this.log('');
113
+ }
114
+ db.close();
115
+ return;
116
+ }
117
+ // Interactive menu
118
+ const settingChoices = [
119
+ { name: `Terminal App: ${config.terminal.app}`, value: 'terminal.app' },
120
+ { name: `Open Tabs in Background: ${config.terminal.openInBackground}`, value: 'terminal.openInBackground' },
121
+ { name: `Shell: ${config.shell}`, value: 'shell' },
122
+ { name: `Tmux Control Mode (iTerm -CC): ${config.tmux.controlMode}`, value: 'tmux.controlMode' },
123
+ ];
124
+ const settingMessage = 'Select setting to configure:';
125
+ if (jsonMode) {
126
+ outputPromptAsJson(buildPromptConfig('list', 'setting', settingMessage, settingChoices), createMetadata('config', flags));
127
+ db.close();
128
+ return;
129
+ }
130
+ const { setting } = await inquirer.prompt([
131
+ {
132
+ type: 'list',
133
+ name: 'setting',
134
+ message: settingMessage,
135
+ choices: [
136
+ new inquirer.Separator('── Terminal ──'),
137
+ ...settingChoices.slice(0, 2),
138
+ new inquirer.Separator('── Shell ──'),
139
+ settingChoices[2],
140
+ new inquirer.Separator('── Tmux ──'),
141
+ settingChoices[3],
142
+ new inquirer.Separator(),
143
+ { name: 'Exit', value: '__exit__' },
144
+ ],
145
+ },
146
+ ]);
147
+ if (setting === '__exit__') {
148
+ db.close();
149
+ return;
150
+ }
151
+ // Handle each setting
152
+ switch (setting) {
153
+ case 'terminal.app': {
154
+ const appChoices = [
155
+ { name: 'iTerm', value: 'iTerm' },
156
+ { name: 'Terminal.app (macOS default)', value: 'Terminal' },
157
+ { name: 'Ghostty', value: 'Ghostty' },
158
+ { name: 'Alacritty', value: 'Alacritty' },
159
+ { name: 'Kitty', value: 'Kitty' },
160
+ { name: 'WezTerm', value: 'WezTerm' },
161
+ { name: 'Warp', value: 'Warp' },
162
+ { name: 'tmux', value: 'tmux' },
163
+ ];
164
+ const { newApp } = await inquirer.prompt([
165
+ {
166
+ type: 'list',
167
+ name: 'newApp',
168
+ message: 'Select terminal app:',
169
+ choices: appChoices,
170
+ default: config.terminal.app,
171
+ },
172
+ ]);
173
+ saveTerminalApp(db, newApp);
174
+ this.log(styles.success(`Terminal app set to: ${newApp}`));
175
+ break;
176
+ }
177
+ case 'terminal.openInBackground': {
178
+ const bgChoices = [
179
+ { name: 'Yes - Open tabs in background (don\'t steal focus)', value: true },
180
+ { name: 'No - Bring terminal to foreground when opening tabs', value: false },
181
+ ];
182
+ const { openInBg } = await inquirer.prompt([
183
+ {
184
+ type: 'list',
185
+ name: 'openInBg',
186
+ message: 'Open terminal tabs in background?',
187
+ choices: bgChoices,
188
+ default: config.terminal.openInBackground,
189
+ },
190
+ ]);
191
+ saveTerminalOpenInBackground(db, openInBg);
192
+ this.log(styles.success(`Open in background set to: ${openInBg}`));
193
+ break;
194
+ }
195
+ case 'shell': {
196
+ const shellChoices = [
197
+ { name: 'zsh (macOS default)', value: 'zsh' },
198
+ { name: 'bash', value: 'bash' },
199
+ { name: 'fish', value: 'fish' },
200
+ ];
201
+ const { newShell } = await inquirer.prompt([
202
+ {
203
+ type: 'list',
204
+ name: 'newShell',
205
+ message: 'Select shell:',
206
+ choices: shellChoices,
207
+ default: config.shell,
208
+ },
209
+ ]);
210
+ saveShell(db, newShell);
211
+ this.log(styles.success(`Shell set to: ${newShell}`));
212
+ break;
213
+ }
214
+ case 'tmux.controlMode': {
215
+ const ccChoices = [
216
+ { name: 'Yes - Use tmux -CC for native iTerm integration', value: true },
217
+ { name: 'No - Standard tmux interface', value: false },
218
+ ];
219
+ const { controlMode } = await inquirer.prompt([
220
+ {
221
+ type: 'list',
222
+ name: 'controlMode',
223
+ message: 'Enable tmux control mode (-CC)?',
224
+ choices: ccChoices,
225
+ default: config.tmux.controlMode,
226
+ },
227
+ ]);
228
+ saveTmuxControlMode(db, controlMode);
229
+ this.log(styles.success(`Tmux control mode set to: ${controlMode}`));
230
+ break;
231
+ }
232
+ }
233
+ db.close();
234
+ }
235
+ catch (error) {
236
+ db.close();
237
+ throw error;
238
+ }
239
+ }
240
+ setConfigValue(db, key, value, jsonMode) {
241
+ const normalizedKey = key.toLowerCase();
242
+ switch (normalizedKey) {
243
+ case 'terminal.app':
244
+ saveTerminalApp(db, value);
245
+ break;
246
+ case 'terminal.openinbackground':
247
+ saveTerminalOpenInBackground(db, value.toLowerCase() === 'true');
248
+ break;
249
+ case 'shell':
250
+ saveShell(db, value);
251
+ break;
252
+ case 'tmux.controlmode':
253
+ saveTmuxControlMode(db, value.toLowerCase() === 'true');
254
+ break;
255
+ default:
256
+ if (jsonMode) {
257
+ outputErrorAsJson('UNKNOWN_KEY', `Unknown config key: ${key}`, createMetadata('config', {}));
258
+ }
259
+ else {
260
+ this.warn(`Unknown config key: ${key}`);
261
+ }
262
+ return;
263
+ }
264
+ if (jsonMode) {
265
+ outputSuccessAsJson({ key, value }, createMetadata('config', {}));
266
+ }
267
+ else {
268
+ this.log(styles.success(`Set ${key} = ${value}`));
269
+ }
270
+ }
271
+ }
@@ -1,6 +1,6 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import inquirer from 'inquirer';
6
6
  import { styles } from '../../lib/styles.js';
@@ -1,7 +1,7 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
- import { execSync } from 'child_process';
4
- import * as path from 'path';
3
+ import { execSync } from 'node:child_process';
4
+ import * as path from 'node:path';
5
5
  import Database from 'better-sqlite3';
6
6
  import { styles } from '../../lib/styles.js';
7
7
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
@@ -1,6 +1,6 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
@@ -134,7 +134,7 @@ export default class DockerList extends Command {
134
134
  getDockerContainers(agentsPath, showAll) {
135
135
  try {
136
136
  // Get containers - filter by devcontainer label unless --all
137
- let cmd = 'docker ps -a --format "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Label \\"devcontainer.local_folder\\"}}"';
137
+ const cmd = 'docker ps -a --format "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Label \\"devcontainer.local_folder\\"}}"';
138
138
  const output = execSync(cmd, {
139
139
  encoding: 'utf-8',
140
140
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -177,11 +177,6 @@ export default class DockerList extends Command {
177
177
  function padEnd(str, length) {
178
178
  return str.padEnd(length);
179
179
  }
180
- function truncate(str, maxLength) {
181
- if (str.length <= maxLength)
182
- return str;
183
- return str.substring(0, maxLength - 2) + '..';
184
- }
185
180
  function getStatusColor(status) {
186
181
  switch (status) {
187
182
  case 'running':
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { execSync, spawn } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync, spawn } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
@@ -1,5 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
2
+ import { execSync } from 'node:child_process';
3
3
  import inquirer from 'inquirer';
4
4
  import { styles } from '../../lib/styles.js';
5
5
  import { isDockerRunning } from '../../lib/execution/runners.js';
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import inquirer from 'inquirer';
6
6
  import { styles } from '../../lib/styles.js';
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { spawn } from 'child_process';
3
- import * as path from 'path';
2
+ import { spawn } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { execSync } from 'child_process';
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
5
  export default class DockerStatus extends Command {
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import inquirer from 'inquirer';
6
6
  import { styles } from '../../lib/styles.js';
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
2
+ import { execSync } from 'node:child_process';
3
+ import * as path from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
@@ -1,6 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
- import { shouldOutputJson, } from '../../lib/prompt-json.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
4
  export default class Epic extends PMOCommand {
5
5
  static description = 'Interactive menu for epic operations';
6
6
  static examples = [
@@ -99,8 +99,10 @@ export default class EpicLink extends PMOCommand {
99
99
  // Interactive mode: show menu in a loop
100
100
  let continueLoop = true;
101
101
  while (continueLoop) {
102
+ // eslint-disable-next-line no-await-in-loop -- Interactive user loop
102
103
  const allEpics = await this.storage.listEpics(projectId);
103
104
  const otherEpics = allEpics.filter(e => e.id !== epicId);
105
+ // eslint-disable-next-line no-await-in-loop -- Interactive user prompt
104
106
  const { action } = await inquirer.prompt([{
105
107
  type: 'list',
106
108
  name: 'action',
@@ -120,15 +122,18 @@ export default class EpicLink extends PMOCommand {
120
122
  continue;
121
123
  }
122
124
  if (action === 'view') {
125
+ // eslint-disable-next-line no-await-in-loop -- User action handling
123
126
  await this.viewDependencies(epicId, epic, flags.all);
124
127
  continue;
125
128
  }
126
129
  if (action === 'remove') {
130
+ // eslint-disable-next-line no-await-in-loop -- User action handling
127
131
  const dependencies = await this.storage.listEpicDependencies(epicId);
128
132
  if (dependencies.length === 0) {
129
133
  this.log(styles.muted('\nNo dependencies to remove.'));
130
134
  continue;
131
135
  }
136
+ // eslint-disable-next-line no-await-in-loop -- Building choices for current interaction
132
137
  const choices = await Promise.all(dependencies.map(async (dep) => {
133
138
  const depEpic = await this.storage.getEpic(dep.dependsOnEpicId);
134
139
  return {
@@ -136,13 +141,16 @@ export default class EpicLink extends PMOCommand {
136
141
  value: { targetId: dep.dependsOnEpicId, type: dep.dependencyType }
137
142
  };
138
143
  }));
144
+ // eslint-disable-next-line no-await-in-loop -- User selection prompt
139
145
  const { selected } = await inquirer.prompt([{
140
146
  type: 'list',
141
147
  name: 'selected',
142
148
  message: 'Select dependency to remove:',
143
149
  choices,
144
150
  }]);
151
+ // eslint-disable-next-line no-await-in-loop -- Action after user selection
145
152
  await this.storage.deleteEpicDependency(epicId, selected.targetId, selected.type);
153
+ // eslint-disable-next-line no-await-in-loop
146
154
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
147
155
  this.log(styles.success(`\nāœ… Removed dependency: ${epicId} → ${selected.targetId}`));
148
156
  continue;
@@ -152,12 +160,14 @@ export default class EpicLink extends PMOCommand {
152
160
  this.log(styles.muted('\nNo other epics to link to.'));
153
161
  continue;
154
162
  }
163
+ // eslint-disable-next-line no-await-in-loop -- User selection prompt
155
164
  const { targetId } = await inquirer.prompt([{
156
165
  type: 'list',
157
166
  name: 'targetId',
158
167
  message: `Select epic that ${epicId} ${action === 'blocks' ? 'is blocked by' : action === 'relates_to' ? 'relates to' : 'duplicates'}:`,
159
168
  choices: otherEpics.map(e => ({ name: `${e.id} - ${e.title}`, value: e.id })),
160
169
  }]);
170
+ // eslint-disable-next-line no-await-in-loop -- Action after user selection
161
171
  await this.addDependency(epicId, targetId, action, epic.title);
162
172
  }
163
173
  }
@@ -197,8 +207,9 @@ export default class EpicLink extends PMOCommand {
197
207
  const blockers = dependencies.filter(d => d.dependencyType === 'blocks');
198
208
  if (blockers.length > 0) {
199
209
  this.log(styles.muted('\n Blocked by:'));
200
- for (const dep of blockers) {
201
- const blockerEpic = await this.storage.getEpic(dep.dependsOnEpicId);
210
+ // Fetch all blocker epics in parallel
211
+ const blockerEpics = await Promise.all(blockers.map(dep => this.storage.getEpic(dep.dependsOnEpicId)));
212
+ for (const blockerEpic of blockerEpics) {
202
213
  if (blockerEpic) {
203
214
  const status = blockerEpic.status === 'complete' ? styles.success('complete') : styles.warning(blockerEpic.status);
204
215
  this.log(` - ${blockerEpic.id}: ${blockerEpic.title} (${status})`);
@@ -208,8 +219,9 @@ export default class EpicLink extends PMOCommand {
208
219
  const otherDeps = dependencies.filter(d => d.dependencyType !== 'blocks');
209
220
  if (otherDeps.length > 0) {
210
221
  this.log(styles.muted('\n Related:'));
211
- for (const dep of otherDeps) {
212
- const relatedEpic = await this.storage.getEpic(dep.dependsOnEpicId);
222
+ // Fetch all related epics in parallel
223
+ const relatedEpics = await Promise.all(otherDeps.map(async (dep) => ({ dep, epic: await this.storage.getEpic(dep.dependsOnEpicId) })));
224
+ for (const { dep, epic: relatedEpic } of relatedEpics) {
213
225
  if (relatedEpic) {
214
226
  this.log(` - ${dep.dependencyType}: ${relatedEpic.id} - ${relatedEpic.title}`);
215
227
  }
@@ -217,20 +229,19 @@ export default class EpicLink extends PMOCommand {
217
229
  }
218
230
  if (showAll) {
219
231
  const allEpics = await this.storage.listEpics(epic.projectId);
220
- const blocking = [];
221
- for (const otherEpic of allEpics) {
222
- if (otherEpic.id === epicId)
223
- continue;
232
+ // Find all epics that depend on this epic in parallel
233
+ const blockingResults = await Promise.all(allEpics
234
+ .filter(otherEpic => otherEpic.id !== epicId)
235
+ .map(async (otherEpic) => {
224
236
  const otherDeps = await this.storage.listEpicDependencies(otherEpic.id);
225
237
  const blockingDep = otherDeps.find(d => d.dependsOnEpicId === epicId);
226
- if (blockingDep) {
227
- blocking.push({ epic: otherEpic, type: blockingDep.dependencyType });
228
- }
229
- }
238
+ return blockingDep ? { epic: otherEpic, type: blockingDep.dependencyType } : null;
239
+ }));
240
+ const blocking = blockingResults.filter((b) => b !== null);
230
241
  if (blocking.length > 0) {
231
242
  this.log(styles.muted('\n Blocking:'));
232
- for (const { epic: blockedEpic, type } of blocking) {
233
- this.log(` - ${blockedEpic.id}: ${blockedEpic.title} (${type})`);
243
+ for (const item of blocking) {
244
+ this.log(` - ${item.epic.id}: ${item.epic.title} (${item.type})`);
234
245
  }
235
246
  }
236
247
  }
@@ -62,6 +62,8 @@ export default class EpicLinkRemove extends PMOCommand {
62
62
  this.log(styles.muted('\nCancelled.'));
63
63
  return;
64
64
  }
65
+ // Delete sequentially to maintain data integrity
66
+ // eslint-disable-next-line no-await-in-loop
65
67
  for (const dep of dependencies)
66
68
  await this.storage.deleteEpicDependency(args.id, dep.dependsOnEpicId, dep.dependencyType);
67
69
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
@@ -32,13 +32,13 @@ export default class EpicList extends PMOCommand {
32
32
  }
33
33
  // Group epics by status
34
34
  const grouped = this.groupByStatus(epics);
35
- // Get ticket counts for each epic
36
- const epicProgress = new Map();
37
- for (const epic of epics) {
35
+ // Get ticket counts for each epic in parallel
36
+ const ticketCounts = await Promise.all(epics.map(async (epic) => {
38
37
  const tickets = await this.storage.getTicketsForEpic(projectId, epic.id);
39
38
  const done = tickets.filter((t) => t.status === 'done').length;
40
- epicProgress.set(epic.id, { done, total: tickets.length });
41
- }
39
+ return { epicId: epic.id, done, total: tickets.length };
40
+ }));
41
+ const epicProgress = new Map(ticketCounts.map(({ epicId, done, total }) => [epicId, { done, total }]));
42
42
  const projectName = await this.getProjectName(projectId);
43
43
  this.log(`\nšŸŽÆ ${styles.emphasis('Epics')} - ${projectName}`);
44
44
  this.log('═'.repeat(55));
@@ -145,18 +145,24 @@ export default class EpicProgress extends PMOCommand {
145
145
  dropped: 'āŒ',
146
146
  future: 'šŸ”®',
147
147
  };
148
+ // Fetch all epic ticket counts in parallel
149
+ const epicProgress = new Map();
150
+ await Promise.all(epics.map(async (epic) => {
151
+ const tickets = await this.storage.getTicketsForEpic(projectId, epic.id);
152
+ const doneTickets = tickets.filter((t) => t.status === 'done').length;
153
+ epicProgress.set(epic.id, { done: doneTickets, total: tickets.length });
154
+ }));
148
155
  for (const status of statusOrder) {
149
156
  const statusEpics = grouped.get(status);
150
157
  if (!statusEpics || statusEpics.length === 0)
151
158
  continue;
152
159
  this.log(`\n${statusEmoji[status]} ${status.toUpperCase()} (${statusEpics.length})`);
153
160
  for (const epic of statusEpics) {
154
- const tickets = await this.storage.getTicketsForEpic(projectId, epic.id);
155
- const doneTickets = tickets.filter((t) => t.status === 'done').length;
156
- const percent = tickets.length > 0 ? Math.round((doneTickets / tickets.length) * 100) : 0;
161
+ const progress = epicProgress.get(epic.id) || { done: 0, total: 0 };
162
+ const percent = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
157
163
  const bar = progressBar(percent);
158
164
  const readyToArchive = percent === 100 && status === 'active' ? ' ← ready to archive' : '';
159
- this.log(` ${epic.id.padEnd(10)} ${epic.title.substring(0, 30).padEnd(30)} ${bar} ${String(percent).padStart(3)}% (${doneTickets}/${tickets.length})${readyToArchive}`);
165
+ this.log(` ${epic.id.padEnd(10)} ${epic.title.substring(0, 30).padEnd(30)} ${bar} ${String(percent).padStart(3)}% (${progress.done}/${progress.total})${readyToArchive}`);
160
166
  }
161
167
  }
162
168
  this.log(styles.muted('\nCommands:'));
@@ -176,7 +176,9 @@ export default class EpicSpec extends PMOCommand {
176
176
  return;
177
177
  }
178
178
  if (action === 'align_all') {
179
+ // Update sequentially for clear logging
179
180
  for (const t of ticketsWithDifferentSpec) {
181
+ // eslint-disable-next-line no-await-in-loop
180
182
  await this.storage.updateTicket(t.id, { specId });
181
183
  this.log(styles.muted(` Updated ${t.id} to spec "${specId}"`));
182
184
  }
@@ -194,6 +194,7 @@ export default class EpicTicket extends PMOCommand {
194
194
  // Process each ticket
195
195
  let successCount = 0;
196
196
  const linkedTickets = [];
197
+ // Process tickets - may prompt user for spec reconciliation
197
198
  for (const ticketId of ticketIds) {
198
199
  const ticket = allTickets.find((t) => t.id === ticketId);
199
200
  const currentEpicId = getTicketEpicId(ticketId);
@@ -226,6 +227,7 @@ export default class EpicTicket extends PMOCommand {
226
227
  if (ticketSpecId && epicSpecId && ticketSpecId !== epicSpecId) {
227
228
  // Both have specs but they differ - warn user
228
229
  this.log(styles.warning(` āš ļø Spec mismatch: ticket has "${ticketSpecId}", epic has "${epicSpecId}"`));
230
+ // eslint-disable-next-line no-await-in-loop
229
231
  const { action } = await inquirer.prompt([{
230
232
  type: 'list',
231
233
  name: 'action',
@@ -252,6 +254,7 @@ export default class EpicTicket extends PMOCommand {
252
254
  }
253
255
  else if (!ticketSpecId && epicSpecId) {
254
256
  // Ticket has no spec but epic does - offer to inherit
257
+ // eslint-disable-next-line no-await-in-loop
255
258
  const { inherit } = await inquirer.prompt([{
256
259
  type: 'confirm',
257
260
  name: 'inherit',