@proletariat/cli 0.3.21 → 0.3.22

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 (333) hide show
  1. package/dist/commands/action/create.d.ts +0 -1
  2. package/dist/commands/action/delete.d.ts +0 -1
  3. package/dist/commands/action/index.d.ts +0 -1
  4. package/dist/commands/action/list.d.ts +0 -1
  5. package/dist/commands/action/list.js +2 -0
  6. package/dist/commands/action/run.d.ts +0 -1
  7. package/dist/commands/action/show.d.ts +0 -1
  8. package/dist/commands/action/update.d.ts +0 -1
  9. package/dist/commands/agent/auth.d.ts +0 -1
  10. package/dist/commands/agent/auth.js +3 -7
  11. package/dist/commands/agent/discover.d.ts +0 -1
  12. package/dist/commands/agent/discover.js +3 -7
  13. package/dist/commands/agent/index.d.ts +0 -1
  14. package/dist/commands/agent/index.js +2 -0
  15. package/dist/commands/agent/list.d.ts +0 -1
  16. package/dist/commands/agent/list.js +30 -1
  17. package/dist/commands/agent/login.d.ts +0 -1
  18. package/dist/commands/agent/login.js +2 -0
  19. package/dist/commands/agent/rebuild.d.ts +0 -1
  20. package/dist/commands/agent/rebuild.js +2 -0
  21. package/dist/commands/agent/remove.d.ts +0 -1
  22. package/dist/commands/agent/remove.js +2 -0
  23. package/dist/commands/agent/restart.d.ts +0 -1
  24. package/dist/commands/agent/restart.js +2 -0
  25. package/dist/commands/agent/shell.d.ts +0 -1
  26. package/dist/commands/agent/shell.js +2 -0
  27. package/dist/commands/agent/staff/add.d.ts +0 -1
  28. package/dist/commands/agent/staff/add.js +3 -7
  29. package/dist/commands/agent/staff/index.d.ts +0 -1
  30. package/dist/commands/agent/staff/index.js +2 -0
  31. package/dist/commands/agent/staff/remove.d.ts +0 -1
  32. package/dist/commands/agent/staff/remove.js +2 -0
  33. package/dist/commands/agent/status.d.ts +0 -1
  34. package/dist/commands/agent/status.js +2 -0
  35. package/dist/commands/agent/temp/cleanup.d.ts +0 -1
  36. package/dist/commands/agent/temp/cleanup.js +2 -0
  37. package/dist/commands/agent/temp/index.d.ts +0 -1
  38. package/dist/commands/agent/temp/index.js +2 -0
  39. package/dist/commands/agent/themes/index.d.ts +0 -1
  40. package/dist/commands/agent/themes/index.js +3 -7
  41. package/dist/commands/agent/themes/set.d.ts +0 -1
  42. package/dist/commands/agent/themes/set.js +3 -7
  43. package/dist/commands/agent/visit.d.ts +0 -1
  44. package/dist/commands/agent/visit.js +2 -0
  45. package/dist/commands/autocomplete/setup.d.ts +0 -1
  46. package/dist/commands/board/index.d.ts +0 -1
  47. package/dist/commands/board/view.d.ts +0 -1
  48. package/dist/commands/board/watch.d.ts +0 -1
  49. package/dist/commands/branch/create.d.ts +0 -1
  50. package/dist/commands/branch/create.js +2 -0
  51. package/dist/commands/branch/index.d.ts +0 -1
  52. package/dist/commands/branch/index.js +2 -0
  53. package/dist/commands/branch/list.d.ts +0 -1
  54. package/dist/commands/branch/validate.d.ts +0 -1
  55. package/dist/commands/branch/where.d.ts +0 -1
  56. package/dist/commands/branch/where.js +2 -0
  57. package/dist/commands/category/create.d.ts +18 -0
  58. package/dist/commands/category/create.js +108 -0
  59. package/dist/commands/category/delete.d.ts +17 -0
  60. package/dist/commands/category/delete.js +103 -0
  61. package/dist/commands/category/index.d.ts +15 -0
  62. package/dist/commands/category/index.js +87 -0
  63. package/dist/commands/category/list.d.ts +17 -0
  64. package/dist/commands/category/list.js +85 -0
  65. package/dist/commands/category/rename.d.ts +18 -0
  66. package/dist/commands/category/rename.js +127 -0
  67. package/dist/commands/claude.js +2 -0
  68. package/dist/commands/commit.js +2 -0
  69. package/dist/commands/config/index.js +2 -0
  70. package/dist/commands/docker/clean.d.ts +0 -1
  71. package/dist/commands/docker/index.d.ts +0 -1
  72. package/dist/commands/docker/prune.d.ts +0 -1
  73. package/dist/commands/docker/restart.d.ts +0 -1
  74. package/dist/commands/docker/stop.d.ts +0 -1
  75. package/dist/commands/epic/activate.d.ts +0 -1
  76. package/dist/commands/epic/activate.js +2 -0
  77. package/dist/commands/epic/archive.d.ts +0 -1
  78. package/dist/commands/epic/archive.js +2 -0
  79. package/dist/commands/epic/create.d.ts +0 -1
  80. package/dist/commands/epic/create.js +3 -1
  81. package/dist/commands/epic/delete.d.ts +14 -0
  82. package/dist/commands/epic/delete.js +129 -0
  83. package/dist/commands/epic/index.d.ts +0 -1
  84. package/dist/commands/epic/index.js +6 -0
  85. package/dist/commands/epic/link/block.d.ts +0 -1
  86. package/dist/commands/epic/link/block.js +2 -0
  87. package/dist/commands/epic/link/duplicates.d.ts +0 -1
  88. package/dist/commands/epic/link/duplicates.js +2 -0
  89. package/dist/commands/epic/link/index.d.ts +0 -1
  90. package/dist/commands/epic/link/index.js +2 -0
  91. package/dist/commands/epic/link/relates.d.ts +0 -1
  92. package/dist/commands/epic/link/relates.js +2 -0
  93. package/dist/commands/epic/link/remove.d.ts +0 -1
  94. package/dist/commands/epic/link/remove.js +2 -0
  95. package/dist/commands/epic/list.d.ts +0 -1
  96. package/dist/commands/epic/move.d.ts +0 -1
  97. package/dist/commands/epic/move.js +2 -0
  98. package/dist/commands/epic/progress.d.ts +0 -1
  99. package/dist/commands/epic/progress.js +2 -0
  100. package/dist/commands/epic/project.d.ts +0 -1
  101. package/dist/commands/epic/project.js +2 -0
  102. package/dist/commands/epic/reorder.d.ts +0 -1
  103. package/dist/commands/epic/reorder.js +2 -0
  104. package/dist/commands/epic/spec.d.ts +0 -1
  105. package/dist/commands/epic/spec.js +2 -0
  106. package/dist/commands/epic/ticket.d.ts +0 -1
  107. package/dist/commands/epic/ticket.js +2 -0
  108. package/dist/commands/epic/view.d.ts +0 -1
  109. package/dist/commands/epic/view.js +2 -0
  110. package/dist/commands/execution/config.d.ts +0 -1
  111. package/dist/commands/execution/config.js +2 -0
  112. package/dist/commands/execution/index.d.ts +0 -1
  113. package/dist/commands/execution/index.js +3 -1
  114. package/dist/commands/execution/list.d.ts +0 -1
  115. package/dist/commands/execution/logs.d.ts +0 -1
  116. package/dist/commands/execution/logs.js +3 -1
  117. package/dist/commands/execution/stop.d.ts +0 -1
  118. package/dist/commands/execution/stop.js +3 -1
  119. package/dist/commands/execution/view.d.ts +0 -1
  120. package/dist/commands/execution/view.js +3 -1
  121. package/dist/commands/gh/index.d.ts +0 -1
  122. package/dist/commands/gh/login.d.ts +0 -1
  123. package/dist/commands/gh/status.d.ts +0 -1
  124. package/dist/commands/gh/token.d.ts +0 -1
  125. package/dist/commands/init.js +2 -0
  126. package/dist/commands/phase/create.d.ts +0 -1
  127. package/dist/commands/phase/create.js +1 -2
  128. package/dist/commands/phase/delete.d.ts +0 -1
  129. package/dist/commands/phase/delete.js +1 -1
  130. package/dist/commands/phase/list.d.ts +0 -1
  131. package/dist/commands/phase/move.d.ts +0 -1
  132. package/dist/commands/phase/move.js +2 -2
  133. package/dist/commands/phase/template/apply.d.ts +0 -1
  134. package/dist/commands/phase/template/apply.js +2 -0
  135. package/dist/commands/phase/template/create.d.ts +0 -1
  136. package/dist/commands/phase/template/create.js +0 -1
  137. package/dist/commands/phase/template/delete.d.ts +0 -1
  138. package/dist/commands/phase/template/delete.js +2 -0
  139. package/dist/commands/phase/template/index.d.ts +0 -1
  140. package/dist/commands/phase/template/index.js +2 -0
  141. package/dist/commands/phase/template/list.d.ts +0 -1
  142. package/dist/commands/phase/template/list.js +2 -0
  143. package/dist/commands/phase/template/update.d.ts +0 -1
  144. package/dist/commands/phase/update.d.ts +0 -1
  145. package/dist/commands/phase/update.js +2 -2
  146. package/dist/commands/pmo/init.js +2 -0
  147. package/dist/commands/pr/create.d.ts +0 -1
  148. package/dist/commands/pr/index.d.ts +0 -1
  149. package/dist/commands/pr/link.d.ts +0 -1
  150. package/dist/commands/pr/list.d.ts +0 -1
  151. package/dist/commands/pr/status.d.ts +0 -1
  152. package/dist/commands/project/archive.d.ts +0 -1
  153. package/dist/commands/project/create.d.ts +0 -1
  154. package/dist/commands/project/delete.d.ts +0 -1
  155. package/dist/commands/project/index.d.ts +0 -1
  156. package/dist/commands/project/list.d.ts +0 -1
  157. package/dist/commands/project/spec.d.ts +0 -1
  158. package/dist/commands/project/unarchive.d.ts +0 -1
  159. package/dist/commands/project/update.d.ts +0 -1
  160. package/dist/commands/project/view.d.ts +0 -1
  161. package/dist/commands/repo/add.d.ts +0 -1
  162. package/dist/commands/repo/add.js +2 -0
  163. package/dist/commands/repo/index.d.ts +0 -1
  164. package/dist/commands/repo/list.d.ts +0 -1
  165. package/dist/commands/repo/remove.d.ts +0 -1
  166. package/dist/commands/repo/view.d.ts +0 -1
  167. package/dist/commands/roadmap/add-project.d.ts +0 -1
  168. package/dist/commands/roadmap/add-project.js +2 -0
  169. package/dist/commands/roadmap/create.d.ts +0 -1
  170. package/dist/commands/roadmap/create.js +2 -0
  171. package/dist/commands/roadmap/delete.d.ts +0 -1
  172. package/dist/commands/roadmap/delete.js +12 -1
  173. package/dist/commands/roadmap/generate.d.ts +0 -1
  174. package/dist/commands/roadmap/generate.js +2 -0
  175. package/dist/commands/roadmap/index.d.ts +0 -1
  176. package/dist/commands/roadmap/index.js +2 -0
  177. package/dist/commands/roadmap/list.d.ts +0 -1
  178. package/dist/commands/roadmap/remove-project.d.ts +0 -1
  179. package/dist/commands/roadmap/remove-project.js +2 -0
  180. package/dist/commands/roadmap/reorder.d.ts +0 -1
  181. package/dist/commands/roadmap/reorder.js +2 -0
  182. package/dist/commands/roadmap/update.d.ts +0 -1
  183. package/dist/commands/roadmap/update.js +2 -0
  184. package/dist/commands/roadmap/view.d.ts +0 -1
  185. package/dist/commands/roadmap/view.js +2 -0
  186. package/dist/commands/session/attach.d.ts +0 -1
  187. package/dist/commands/session/attach.js +9 -0
  188. package/dist/commands/session/index.d.ts +0 -1
  189. package/dist/commands/session/index.js +2 -0
  190. package/dist/commands/session/list.d.ts +0 -1
  191. package/dist/commands/spec/create.d.ts +0 -1
  192. package/dist/commands/spec/create.js +1 -1
  193. package/dist/commands/spec/delete.d.ts +0 -1
  194. package/dist/commands/spec/edit.d.ts +0 -1
  195. package/dist/commands/spec/index.d.ts +0 -1
  196. package/dist/commands/spec/link/depends.d.ts +0 -1
  197. package/dist/commands/spec/link/duplicates.d.ts +0 -1
  198. package/dist/commands/spec/link/index.d.ts +0 -1
  199. package/dist/commands/spec/link/relates.d.ts +0 -1
  200. package/dist/commands/spec/link/remove.d.ts +0 -1
  201. package/dist/commands/spec/list.d.ts +0 -1
  202. package/dist/commands/spec/plan.d.ts +0 -1
  203. package/dist/commands/spec/ticket.d.ts +0 -3
  204. package/dist/commands/spec/ticket.js +7 -38
  205. package/dist/commands/spec/view.d.ts +0 -1
  206. package/dist/commands/status/category.d.ts +14 -0
  207. package/dist/commands/status/category.js +63 -0
  208. package/dist/commands/status/create.d.ts +0 -1
  209. package/dist/commands/status/create.js +1 -1
  210. package/dist/commands/status/delete.d.ts +0 -1
  211. package/dist/commands/status/index.d.ts +0 -1
  212. package/dist/commands/status/list.d.ts +0 -1
  213. package/dist/commands/status/move.d.ts +0 -1
  214. package/dist/commands/status/update.d.ts +0 -1
  215. package/dist/commands/template/delete.d.ts +0 -1
  216. package/dist/commands/template/delete.js +2 -0
  217. package/dist/commands/template/index.d.ts +0 -1
  218. package/dist/commands/template/list.d.ts +0 -1
  219. package/dist/commands/template/list.js +2 -0
  220. package/dist/commands/template/phase/apply.js +2 -0
  221. package/dist/commands/template/phase/create.d.ts +0 -1
  222. package/dist/commands/template/phase/create.js +3 -9
  223. package/dist/commands/template/phase/delete.js +2 -0
  224. package/dist/commands/template/phase/index.d.ts +0 -1
  225. package/dist/commands/template/phase/list.js +2 -0
  226. package/dist/commands/template/phase/update.js +2 -0
  227. package/dist/commands/template/ticket/apply.js +2 -0
  228. package/dist/commands/template/ticket/create.js +2 -0
  229. package/dist/commands/template/ticket/delete.js +2 -0
  230. package/dist/commands/template/ticket/index.d.ts +0 -1
  231. package/dist/commands/template/ticket/list.js +2 -0
  232. package/dist/commands/template/ticket/save.d.ts +0 -1
  233. package/dist/commands/template/ticket/save.js +0 -6
  234. package/dist/commands/terminal/title.d.ts +0 -1
  235. package/dist/commands/ticket/bulk.d.ts +0 -1
  236. package/dist/commands/ticket/bulk.js +2 -0
  237. package/dist/commands/ticket/category.d.ts +14 -0
  238. package/dist/commands/ticket/category.js +63 -0
  239. package/dist/commands/ticket/complete.d.ts +0 -1
  240. package/dist/commands/ticket/complete.js +2 -0
  241. package/dist/commands/ticket/create.d.ts +0 -1
  242. package/dist/commands/ticket/create.js +6 -4
  243. package/dist/commands/ticket/delete.d.ts +0 -1
  244. package/dist/commands/ticket/delete.js +2 -0
  245. package/dist/commands/ticket/edit.d.ts +0 -1
  246. package/dist/commands/ticket/edit.js +4 -2
  247. package/dist/commands/ticket/epic.d.ts +0 -1
  248. package/dist/commands/ticket/epic.js +2 -0
  249. package/dist/commands/ticket/index.d.ts +0 -1
  250. package/dist/commands/ticket/index.js +2 -0
  251. package/dist/commands/ticket/link/block.d.ts +0 -1
  252. package/dist/commands/ticket/link/block.js +2 -0
  253. package/dist/commands/ticket/link/duplicates.d.ts +0 -1
  254. package/dist/commands/ticket/link/duplicates.js +2 -0
  255. package/dist/commands/ticket/link/index.d.ts +0 -1
  256. package/dist/commands/ticket/link/index.js +2 -0
  257. package/dist/commands/ticket/link/relates.d.ts +0 -1
  258. package/dist/commands/ticket/link/relates.js +2 -0
  259. package/dist/commands/ticket/link/remove.d.ts +0 -1
  260. package/dist/commands/ticket/link/remove.js +2 -0
  261. package/dist/commands/ticket/list.d.ts +0 -1
  262. package/dist/commands/ticket/move.d.ts +0 -1
  263. package/dist/commands/ticket/move.js +2 -0
  264. package/dist/commands/ticket/project.d.ts +0 -1
  265. package/dist/commands/ticket/project.js +2 -0
  266. package/dist/commands/ticket/reassign.d.ts +0 -1
  267. package/dist/commands/ticket/reassign.js +29 -0
  268. package/dist/commands/ticket/spec.d.ts +0 -1
  269. package/dist/commands/ticket/spec.js +2 -0
  270. package/dist/commands/ticket/status.d.ts +0 -1
  271. package/dist/commands/ticket/status.js +2 -0
  272. package/dist/commands/ticket/template/apply.d.ts +0 -1
  273. package/dist/commands/ticket/template/apply.js +2 -0
  274. package/dist/commands/ticket/template/create.d.ts +0 -1
  275. package/dist/commands/ticket/template/create.js +4 -2
  276. package/dist/commands/ticket/template/delete.d.ts +0 -1
  277. package/dist/commands/ticket/template/delete.js +2 -0
  278. package/dist/commands/ticket/template/index.d.ts +0 -1
  279. package/dist/commands/ticket/template/index.js +2 -0
  280. package/dist/commands/ticket/template/list.d.ts +0 -1
  281. package/dist/commands/ticket/template/list.js +2 -0
  282. package/dist/commands/ticket/template/save.d.ts +0 -1
  283. package/dist/commands/ticket/template/save.js +2 -0
  284. package/dist/commands/ticket/update.d.ts +0 -1
  285. package/dist/commands/ticket/update.js +2 -0
  286. package/dist/commands/ticket/view.d.ts +0 -1
  287. package/dist/commands/ticket/view.js +2 -0
  288. package/dist/commands/work/complete.d.ts +0 -1
  289. package/dist/commands/work/complete.js +2 -0
  290. package/dist/commands/work/index.d.ts +0 -1
  291. package/dist/commands/work/index.js +2 -0
  292. package/dist/commands/work/ready.d.ts +1 -2
  293. package/dist/commands/work/ready.js +11 -5
  294. package/dist/commands/work/revise.d.ts +0 -1
  295. package/dist/commands/work/revise.js +3 -1
  296. package/dist/commands/work/spawn-all.d.ts +0 -1
  297. package/dist/commands/work/spawn-all.js +2 -0
  298. package/dist/commands/work/spawn.d.ts +0 -1
  299. package/dist/commands/work/spawn.js +2 -0
  300. package/dist/commands/work/start.d.ts +0 -1
  301. package/dist/commands/work/start.js +6 -0
  302. package/dist/commands/work/watch.d.ts +0 -1
  303. package/dist/commands/work/watch.js +3 -1
  304. package/dist/commands/workflow/create.d.ts +0 -1
  305. package/dist/commands/workflow/delete.d.ts +0 -1
  306. package/dist/commands/workflow/index.d.ts +0 -1
  307. package/dist/commands/workflow/index.js +2 -0
  308. package/dist/commands/workflow/list.d.ts +0 -1
  309. package/dist/commands/workflow/switch.d.ts +0 -1
  310. package/dist/commands/workflow/view.d.ts +0 -1
  311. package/dist/commands/workspace/list.js +2 -0
  312. package/dist/commands/workspace/prune.d.ts +13 -0
  313. package/dist/commands/workspace/prune.js +186 -0
  314. package/dist/commands/workspace/remove.js +2 -0
  315. package/dist/commands/workspace/use.js +2 -0
  316. package/dist/lib/pmo/base-command.d.ts +2 -4
  317. package/dist/lib/pmo/base-command.js +8 -10
  318. package/dist/lib/pmo/schema.d.ts +2 -0
  319. package/dist/lib/pmo/schema.js +17 -0
  320. package/dist/lib/pmo/storage/base.d.ts +4 -0
  321. package/dist/lib/pmo/storage/base.js +31 -0
  322. package/dist/lib/pmo/storage/categories.d.ts +50 -0
  323. package/dist/lib/pmo/storage/categories.js +205 -0
  324. package/dist/lib/pmo/storage/index.d.ts +14 -1
  325. package/dist/lib/pmo/storage/index.js +35 -1
  326. package/dist/lib/pmo/storage/tickets.d.ts +5 -0
  327. package/dist/lib/pmo/storage/tickets.js +31 -3
  328. package/dist/lib/pmo/storage/types.d.ts +10 -0
  329. package/dist/lib/pmo/types.d.ts +25 -0
  330. package/dist/lib/prompt-json.d.ts +10 -16
  331. package/dist/lib/prompt-json.js +8 -16
  332. package/oclif.manifest.json +4283 -4345
  333. package/package.json +1 -1
@@ -0,0 +1,186 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { styles } from '../../lib/styles.js';
5
+ import { getRegisteredHeadquarters, unregisterHeadquarters, } from '../../lib/machine-config.js';
6
+ import { getWorkspaceAgents, removeAgentsFromDatabase, getDatabasePath, } from '../../lib/database/index.js';
7
+ export default class WorkspacePrune extends Command {
8
+ static description = 'Remove stale workspace entries and agents with deleted worktrees';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %> --dry-run',
11
+ '<%= config.bin %> <%= command.id %>',
12
+ ];
13
+ static flags = {
14
+ 'dry-run': Flags.boolean({
15
+ char: 'd',
16
+ description: 'Show what would be removed without removing',
17
+ default: false,
18
+ }),
19
+ json: Flags.boolean({
20
+ description: 'Output as JSON',
21
+ default: false,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(WorkspacePrune);
26
+ // Find stale entries
27
+ const staleWorkspaces = this.findStaleWorkspaces();
28
+ const staleAgents = this.findStaleAgents();
29
+ const totalStale = staleWorkspaces.length + staleAgents.length;
30
+ // JSON output
31
+ if (flags.json) {
32
+ const output = {
33
+ dryRun: flags['dry-run'],
34
+ staleWorkspaces: staleWorkspaces.map(w => ({
35
+ name: w.name,
36
+ path: w.path,
37
+ })),
38
+ staleAgents: staleAgents.map(a => ({
39
+ workspaceName: a.workspaceName,
40
+ agentName: a.agentName,
41
+ expectedPath: a.expectedPath,
42
+ })),
43
+ totalRemoved: flags['dry-run'] ? 0 : totalStale,
44
+ totalFound: totalStale,
45
+ };
46
+ this.log(JSON.stringify(output, null, 2));
47
+ if (!flags['dry-run'] && totalStale > 0) {
48
+ this.performPrune(staleWorkspaces, staleAgents);
49
+ }
50
+ return;
51
+ }
52
+ // Human-readable output
53
+ this.log(`\n${styles.header('Workspace Prune')}`);
54
+ this.log('─'.repeat(50));
55
+ if (totalStale === 0) {
56
+ this.log(styles.success('\nNo stale entries found.'));
57
+ return;
58
+ }
59
+ // Show stale workspaces
60
+ if (staleWorkspaces.length > 0) {
61
+ this.log(styles.subheader('\nStale workspace registrations:'));
62
+ for (const workspace of staleWorkspaces) {
63
+ this.log(` ${styles.removed('×')} ${styles.emphasis(workspace.name)}`);
64
+ this.log(styles.muted(` Path no longer exists: ${workspace.path}`));
65
+ }
66
+ }
67
+ // Show stale agents
68
+ if (staleAgents.length > 0) {
69
+ this.log(styles.subheader('\nAgents with deleted worktrees:'));
70
+ // Group by workspace for cleaner output
71
+ const agentsByWorkspace = new Map();
72
+ for (const agent of staleAgents) {
73
+ const list = agentsByWorkspace.get(agent.workspaceName) || [];
74
+ list.push(agent);
75
+ agentsByWorkspace.set(agent.workspaceName, list);
76
+ }
77
+ for (const [workspaceName, agents] of agentsByWorkspace) {
78
+ this.log(` ${styles.muted(`In ${workspaceName}:`)}`);
79
+ for (const agent of agents) {
80
+ this.log(` ${styles.removed('×')} ${agent.agentName}`);
81
+ this.log(styles.muted(` Expected: ${agent.expectedPath}`));
82
+ }
83
+ }
84
+ }
85
+ // Summary
86
+ this.log('');
87
+ if (flags['dry-run']) {
88
+ this.log(styles.warning(`[DRY RUN] Would remove:`));
89
+ if (staleWorkspaces.length > 0) {
90
+ this.log(styles.muted(` • ${staleWorkspaces.length} workspace registration(s)`));
91
+ }
92
+ if (staleAgents.length > 0) {
93
+ this.log(styles.muted(` • ${staleAgents.length} agent record(s)`));
94
+ }
95
+ this.log('');
96
+ this.log(styles.muted('Run without --dry-run to remove these entries.'));
97
+ }
98
+ else {
99
+ // Perform the actual prune
100
+ this.performPrune(staleWorkspaces, staleAgents);
101
+ this.log(styles.success('Pruned:'));
102
+ if (staleWorkspaces.length > 0) {
103
+ this.log(styles.muted(` • ${staleWorkspaces.length} workspace registration(s)`));
104
+ }
105
+ if (staleAgents.length > 0) {
106
+ this.log(styles.muted(` • ${staleAgents.length} agent record(s)`));
107
+ }
108
+ }
109
+ this.log('');
110
+ }
111
+ findStaleWorkspaces() {
112
+ const workspaces = getRegisteredHeadquarters();
113
+ const stale = [];
114
+ for (const workspace of workspaces) {
115
+ if (!fs.existsSync(workspace.path)) {
116
+ stale.push({
117
+ name: workspace.name,
118
+ path: workspace.path,
119
+ });
120
+ }
121
+ }
122
+ return stale;
123
+ }
124
+ findStaleAgents() {
125
+ const workspaces = getRegisteredHeadquarters();
126
+ const stale = [];
127
+ for (const workspace of workspaces) {
128
+ // Skip workspaces that don't exist (handled separately)
129
+ if (!fs.existsSync(workspace.path)) {
130
+ continue;
131
+ }
132
+ // Skip workspaces without a database
133
+ const dbPath = getDatabasePath(workspace.path);
134
+ if (!fs.existsSync(dbPath)) {
135
+ continue;
136
+ }
137
+ try {
138
+ // Get active agents in this workspace
139
+ const agents = getWorkspaceAgents(workspace.path, false);
140
+ for (const agent of agents) {
141
+ // Determine expected directory path
142
+ let agentDir;
143
+ if (agent.worktree_path) {
144
+ agentDir = path.join(workspace.path, agent.worktree_path);
145
+ }
146
+ else if (agent.type === 'ephemeral') {
147
+ agentDir = path.join(workspace.path, 'agents', 'temp', agent.name);
148
+ }
149
+ else {
150
+ agentDir = path.join(workspace.path, 'agents', 'staff', agent.name);
151
+ }
152
+ // If directory doesn't exist, mark as stale
153
+ if (!fs.existsSync(agentDir)) {
154
+ stale.push({
155
+ workspacePath: workspace.path,
156
+ workspaceName: workspace.name,
157
+ agentName: agent.name,
158
+ expectedPath: agentDir,
159
+ });
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ // Skip workspaces where we can't read agents (e.g., database issues)
165
+ continue;
166
+ }
167
+ }
168
+ return stale;
169
+ }
170
+ performPrune(staleWorkspaces, staleAgents) {
171
+ // Remove stale workspace registrations
172
+ for (const workspace of staleWorkspaces) {
173
+ unregisterHeadquarters(workspace.path);
174
+ }
175
+ // Remove stale agents, grouped by workspace
176
+ const agentsByWorkspace = new Map();
177
+ for (const agent of staleAgents) {
178
+ const list = agentsByWorkspace.get(agent.workspacePath) || [];
179
+ list.push(agent.agentName);
180
+ agentsByWorkspace.set(agent.workspacePath, list);
181
+ }
182
+ for (const [workspacePath, agentNames] of agentsByWorkspace) {
183
+ removeAgentsFromDatabase(workspacePath, agentNames);
184
+ }
185
+ }
186
+ }
@@ -17,6 +17,8 @@ export default class WorkspaceRemove extends PromptCommand {
17
17
  };
18
18
  static flags = {
19
19
  json: Flags.boolean({
20
+ char: 'm',
21
+ aliases: ['machine'],
20
22
  description: 'Output prompt configuration as JSON (for AI agents/scripts)',
21
23
  default: false,
22
24
  }),
@@ -18,6 +18,8 @@ export default class WorkspaceUse extends PromptCommand {
18
18
  };
19
19
  static flags = {
20
20
  json: Flags.boolean({
21
+ char: 'm',
22
+ aliases: ['machine'],
21
23
  description: 'Output prompt configuration as JSON (for AI agents/scripts)',
22
24
  default: false,
23
25
  }),
@@ -2,7 +2,7 @@ import { type PMOContext } from './pmo-context.js';
2
2
  import { PromptCommand } from '../prompt-command.js';
3
3
  import { type JsonFlags } from '../prompt-json.js';
4
4
  /**
5
- * Base flags for JSON/agent mode support (legacy)
5
+ * Base flags for JSON/agent mode support
6
6
  * Include these in your command's flags by spreading: ...jsonModeFlags
7
7
  * @deprecated Use machineOutputFlags instead
8
8
  */
@@ -12,10 +12,9 @@ export declare const jsonModeFlags: {
12
12
  /**
13
13
  * Base flags for machine-readable output mode
14
14
  * Include these in your command's flags by spreading: ...machineOutputFlags
15
- * Supports both --machine (new) and --json (legacy, deprecated)
15
+ * --json is the primary flag, -m/--machine are aliases
16
16
  */
17
17
  export declare const machineOutputFlags: {
18
- machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
18
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
19
  };
21
20
  /**
@@ -23,7 +22,6 @@ export declare const machineOutputFlags: {
23
22
  * Include these in your command's flags by spreading: ...pmoBaseFlags
24
23
  */
25
24
  export declare const pmoBaseFlags: {
26
- machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
25
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
26
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
27
  };
@@ -5,31 +5,29 @@ import { styles } from '../styles.js';
5
5
  import { PromptCommand } from '../prompt-command.js';
6
6
  import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, } from '../prompt-json.js';
7
7
  /**
8
- * Base flags for JSON/agent mode support (legacy)
8
+ * Base flags for JSON/agent mode support
9
9
  * Include these in your command's flags by spreading: ...jsonModeFlags
10
10
  * @deprecated Use machineOutputFlags instead
11
11
  */
12
12
  export const jsonModeFlags = {
13
13
  json: Flags.boolean({
14
- description: 'Output prompts as JSON for AI agents/scripts',
14
+ char: 'm',
15
+ aliases: ['machine'],
16
+ description: 'Output as JSON for AI agents/scripts',
15
17
  default: false,
16
18
  }),
17
19
  };
18
20
  /**
19
21
  * Base flags for machine-readable output mode
20
22
  * Include these in your command's flags by spreading: ...machineOutputFlags
21
- * Supports both --machine (new) and --json (legacy, deprecated)
23
+ * --json is the primary flag, -m/--machine are aliases
22
24
  */
23
25
  export const machineOutputFlags = {
24
- machine: Flags.boolean({
25
- char: 'm',
26
- description: 'Output as JSON for AI agents/scripts (machine-readable mode)',
27
- default: false,
28
- }),
29
26
  json: Flags.boolean({
30
- description: 'Output as JSON (deprecated, use --machine)',
27
+ char: 'm',
28
+ aliases: ['machine'],
29
+ description: 'Output as JSON for AI agents/scripts',
31
30
  default: false,
32
- hidden: true, // Hide from help since it's deprecated
33
31
  }),
34
32
  };
35
33
  /**
@@ -8,6 +8,7 @@ export declare const PMO_TABLES: {
8
8
  readonly projects: "pmo_projects";
9
9
  readonly initiatives: "pmo_initiatives";
10
10
  readonly tickets: "pmo_tickets";
11
+ readonly categories: "pmo_categories";
11
12
  readonly board_views: "pmo_board_views";
12
13
  readonly subtasks: "pmo_subtasks";
13
14
  readonly ticket_metadata: "pmo_ticket_metadata";
@@ -41,6 +42,7 @@ export declare const PMO_TABLES: {
41
42
  export declare const PMO_TABLE_SCHEMAS: {
42
43
  readonly projects: "\n CREATE TABLE IF NOT EXISTS pmo_projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n template TEXT,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n phase_id TEXT,\n workflow_id TEXT,\n is_archived INTEGER NOT NULL DEFAULT 0,\n target_date TIMESTAMP,\n initiative_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (phase_id) REFERENCES pmo_phases(id) ON DELETE SET NULL,\n FOREIGN KEY (workflow_id) REFERENCES pmo_workflows(id) ON DELETE SET NULL\n )";
43
44
  readonly initiatives: "\n CREATE TABLE IF NOT EXISTS pmo_initiatives (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n objective TEXT,\n key_results TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
45
+ readonly categories: "\n CREATE TABLE IF NOT EXISTS pmo_categories (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n type TEXT NOT NULL CHECK (type IN ('ticket', 'status')),\n description TEXT,\n color TEXT,\n position INTEGER NOT NULL DEFAULT 0,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(name, type)\n )";
44
46
  readonly workflows: "\n CREATE TABLE IF NOT EXISTS pmo_workflows (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
45
47
  readonly workflow_statuses: "\n CREATE TABLE IF NOT EXISTS pmo_workflow_statuses (\n id TEXT PRIMARY KEY,\n workflow_id TEXT NOT NULL,\n name TEXT NOT NULL,\n category TEXT NOT NULL,\n position INTEGER NOT NULL DEFAULT 0,\n color TEXT,\n description TEXT,\n is_default INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (workflow_id) REFERENCES pmo_workflows(id) ON DELETE CASCADE,\n UNIQUE(workflow_id, name)\n )";
46
48
  readonly columns: "\n CREATE TABLE IF NOT EXISTS pmo_columns (\n id TEXT NOT NULL,\n project_id TEXT NOT NULL DEFAULT 'default',\n name TEXT NOT NULL,\n position INTEGER NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (project_id, id)\n )";
@@ -11,6 +11,7 @@ export const PMO_TABLES = {
11
11
  projects: 'pmo_projects',
12
12
  initiatives: 'pmo_initiatives',
13
13
  tickets: 'pmo_tickets',
14
+ categories: 'pmo_categories',
14
15
  board_views: 'pmo_board_views', // Saved board view configurations
15
16
  subtasks: 'pmo_subtasks',
16
17
  ticket_metadata: 'pmo_ticket_metadata',
@@ -75,6 +76,19 @@ export const PMO_TABLE_SCHEMAS = {
75
76
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
76
77
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
77
78
  )`,
79
+ // Categories for tickets and status types
80
+ categories: `
81
+ CREATE TABLE IF NOT EXISTS ${PMO_TABLES.categories} (
82
+ id TEXT PRIMARY KEY,
83
+ name TEXT NOT NULL,
84
+ type TEXT NOT NULL CHECK (type IN ('ticket', 'status')),
85
+ description TEXT,
86
+ color TEXT,
87
+ position INTEGER NOT NULL DEFAULT 0,
88
+ is_builtin INTEGER NOT NULL DEFAULT 0,
89
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
90
+ UNIQUE(name, type)
91
+ )`,
78
92
  // Shared workflow definitions - projects reference these via workflow_id
79
93
  workflows: `
80
94
  CREATE TABLE IF NOT EXISTS ${PMO_TABLES.workflows} (
@@ -484,6 +498,8 @@ export const PMO_INDEXES = `
484
498
  CREATE INDEX IF NOT EXISTS idx_pmo_roadmap_projects_roadmap ON ${PMO_TABLES.roadmap_projects}(roadmap_id);
485
499
  CREATE INDEX IF NOT EXISTS idx_pmo_roadmap_projects_project ON ${PMO_TABLES.roadmap_projects}(project_id);
486
500
  CREATE INDEX IF NOT EXISTS idx_pmo_roadmap_projects_position ON ${PMO_TABLES.roadmap_projects}(roadmap_id, position);
501
+ CREATE INDEX IF NOT EXISTS idx_pmo_categories_type ON ${PMO_TABLES.categories}(type);
502
+ CREATE INDEX IF NOT EXISTS idx_pmo_categories_position ON ${PMO_TABLES.categories}(type, position);
487
503
  `;
488
504
  // =============================================================================
489
505
  // Combined Schema
@@ -499,6 +515,7 @@ export const PMO_SCHEMA_SQL = [
499
515
  PMO_TABLE_SCHEMAS.workflow_statuses, // Workflow statuses (= board columns)
500
516
  PMO_TABLE_SCHEMAS.projects,
501
517
  PMO_TABLE_SCHEMAS.initiatives,
518
+ PMO_TABLE_SCHEMAS.categories, // Ticket and status categories
502
519
  // PMO_TABLE_SCHEMAS.templates, // REMOVED: workflows are now used directly
503
520
  PMO_TABLE_SCHEMAS.specs, // Must be before tickets (FK reference)
504
521
  PMO_TABLE_SCHEMAS.spec_dependencies, // Spec-to-spec dependencies
@@ -33,6 +33,10 @@ export declare function seedBuiltinActions(db: Database.Database): void;
33
33
  * Seed built-in ticket templates.
34
34
  */
35
35
  export declare function seedBuiltinTicketTemplates(db: Database.Database): void;
36
+ /**
37
+ * Seed built-in categories from TICKET_CATEGORIES and STATE_CATEGORY_ORDER.
38
+ */
39
+ export declare function seedBuiltinCategories(db: Database.Database): void;
36
40
  /**
37
41
  * Update board timestamp for a project.
38
42
  */
@@ -3,6 +3,7 @@
3
3
  * This module handles database setup and provides shared utilities.
4
4
  */
5
5
  import { PMO_TABLES, PMO_SCHEMA_SQL, validateTicketSchema } from '../schema.js';
6
+ import { TICKET_CATEGORIES, STATE_CATEGORY_ORDER } from '../types.js';
6
7
  import { BUILTIN_TEMPLATES } from '../templates-builtin.js';
7
8
  const T = PMO_TABLES;
8
9
  /**
@@ -802,6 +803,36 @@ Why is this refactor needed?
802
803
  insertTemplate.run(template.id, template.name, template.description || null, template.titlePattern || null, template.descriptionTemplate || null, template.defaultPriority || null, template.defaultCategory || null, null, null, null, '[]', JSON.stringify(template.suggestedSubtasks || []), now);
803
804
  }
804
805
  }
806
+ /**
807
+ * Seed built-in categories from TICKET_CATEGORIES and STATE_CATEGORY_ORDER.
808
+ */
809
+ export function seedBuiltinCategories(db) {
810
+ const insertCategory = db.prepare(`
811
+ INSERT OR IGNORE INTO ${T.categories} (id, name, type, description, position, is_builtin, created_at)
812
+ VALUES (?, ?, ?, ?, ?, 1, ?)
813
+ `);
814
+ const now = new Date().toISOString();
815
+ // Seed ticket categories from TICKET_CATEGORIES
816
+ for (let i = 0; i < TICKET_CATEGORIES.length; i++) {
817
+ const category = TICKET_CATEGORIES[i];
818
+ const id = `ticket-${category}`;
819
+ insertCategory.run(id, category, 'ticket', null, i, now);
820
+ }
821
+ // Seed status categories from STATE_CATEGORY_ORDER
822
+ const statusCategoryDescriptions = {
823
+ triage: 'Inbox - needs review before entering workflow',
824
+ backlog: 'Not yet scheduled for work',
825
+ unstarted: 'Scheduled but work has not begun',
826
+ started: 'Work is actively in progress',
827
+ completed: 'Work finished successfully',
828
+ canceled: 'Work will not be done',
829
+ };
830
+ for (let i = 0; i < STATE_CATEGORY_ORDER.length; i++) {
831
+ const category = STATE_CATEGORY_ORDER[i];
832
+ const id = `status-${category}`;
833
+ insertCategory.run(id, category, 'status', statusCategoryDescriptions[category] || null, i, now);
834
+ }
835
+ }
805
836
  /**
806
837
  * Update board timestamp for a project.
807
838
  */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Category operations.
3
+ * Manages ticket and status categories.
4
+ */
5
+ import { Category, CategoryFilter, CategoryType } from '../types.js';
6
+ import { StorageContext } from './types.js';
7
+ export declare class CategoryStorage {
8
+ private ctx;
9
+ constructor(ctx: StorageContext);
10
+ /**
11
+ * List categories.
12
+ */
13
+ listCategories(filter?: CategoryFilter): Promise<Category[]>;
14
+ /**
15
+ * Get a category by ID.
16
+ */
17
+ getCategory(id: string): Promise<Category | null>;
18
+ /**
19
+ * Get a category by name and type.
20
+ */
21
+ getCategoryByName(name: string, type: CategoryType): Promise<Category | null>;
22
+ /**
23
+ * Create a new category.
24
+ */
25
+ createCategory(category: Partial<Category> & {
26
+ name: string;
27
+ type: CategoryType;
28
+ }): Promise<Category>;
29
+ /**
30
+ * Update a category.
31
+ */
32
+ updateCategory(id: string, changes: Partial<Category>): Promise<Category>;
33
+ /**
34
+ * Rename a category.
35
+ */
36
+ renameCategory(id: string, newName: string): Promise<Category>;
37
+ /**
38
+ * Delete a category.
39
+ */
40
+ deleteCategory(id: string): Promise<void>;
41
+ /**
42
+ * Get category names for a type (for validation and autocomplete).
43
+ */
44
+ getCategoryNames(type: CategoryType): Promise<string[]>;
45
+ /**
46
+ * Check if a category name is valid for a type.
47
+ */
48
+ isValidCategory(name: string, type: CategoryType): Promise<boolean>;
49
+ private rowToCategory;
50
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Category operations.
3
+ * Manages ticket and status categories.
4
+ */
5
+ import { PMO_TABLES } from '../schema.js';
6
+ import { PMOError } from '../types.js';
7
+ import { slugify } from '../utils.js';
8
+ const T = PMO_TABLES;
9
+ export class CategoryStorage {
10
+ ctx;
11
+ constructor(ctx) {
12
+ this.ctx = ctx;
13
+ }
14
+ /**
15
+ * List categories.
16
+ */
17
+ async listCategories(filter) {
18
+ let sql = `SELECT * FROM ${T.categories}`;
19
+ const conditions = [];
20
+ const params = [];
21
+ if (filter?.type) {
22
+ conditions.push('type = ?');
23
+ params.push(filter.type);
24
+ }
25
+ if (filter?.isBuiltin !== undefined) {
26
+ conditions.push('is_builtin = ?');
27
+ params.push(filter.isBuiltin ? 1 : 0);
28
+ }
29
+ if (filter?.search) {
30
+ conditions.push('(name LIKE ? OR description LIKE ?)');
31
+ params.push(`%${filter.search}%`, `%${filter.search}%`);
32
+ }
33
+ if (conditions.length > 0) {
34
+ sql += ` WHERE ${conditions.join(' AND ')}`;
35
+ }
36
+ sql += ' ORDER BY type, position ASC, name ASC';
37
+ const rows = this.ctx.db.prepare(sql).all(...params);
38
+ return rows.map((row) => this.rowToCategory(row));
39
+ }
40
+ /**
41
+ * Get a category by ID.
42
+ */
43
+ async getCategory(id) {
44
+ const row = this.ctx.db.prepare(`SELECT * FROM ${T.categories} WHERE id = ?`).get(id);
45
+ if (!row)
46
+ return null;
47
+ return this.rowToCategory(row);
48
+ }
49
+ /**
50
+ * Get a category by name and type.
51
+ */
52
+ async getCategoryByName(name, type) {
53
+ const row = this.ctx.db.prepare(`
54
+ SELECT * FROM ${T.categories} WHERE LOWER(name) = LOWER(?) AND type = ?
55
+ `).get(name, type);
56
+ if (!row)
57
+ return null;
58
+ return this.rowToCategory(row);
59
+ }
60
+ /**
61
+ * Create a new category.
62
+ */
63
+ async createCategory(category) {
64
+ if (!category.name) {
65
+ throw new PMOError('INVALID', 'Category name is required');
66
+ }
67
+ if (!category.type) {
68
+ throw new PMOError('INVALID', 'Category type is required');
69
+ }
70
+ const id = category.id || slugify(category.name);
71
+ // Check for duplicate name within the same type
72
+ const existing = this.ctx.db.prepare(`
73
+ SELECT id FROM ${T.categories} WHERE LOWER(name) = LOWER(?) AND type = ?
74
+ `).get(category.name, category.type);
75
+ if (existing) {
76
+ throw new PMOError('CONFLICT', `Category "${category.name}" already exists for type "${category.type}"`);
77
+ }
78
+ // Get the next position
79
+ const maxPos = this.ctx.db.prepare(`
80
+ SELECT MAX(position) as max FROM ${T.categories} WHERE type = ?
81
+ `).get(category.type);
82
+ const position = category.position ?? (maxPos.max ?? -1) + 1;
83
+ const now = new Date().toISOString();
84
+ this.ctx.db.prepare(`
85
+ INSERT INTO ${T.categories} (id, name, type, description, color, position, is_builtin, created_at)
86
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
87
+ `).run(id, category.name, category.type, category.description || null, category.color || null, position, category.isBuiltin ? 1 : 0, now);
88
+ return {
89
+ id,
90
+ name: category.name,
91
+ type: category.type,
92
+ description: category.description,
93
+ color: category.color,
94
+ position,
95
+ isBuiltin: category.isBuiltin || false,
96
+ createdAt: new Date(now),
97
+ };
98
+ }
99
+ /**
100
+ * Update a category.
101
+ */
102
+ async updateCategory(id, changes) {
103
+ const existing = await this.getCategory(id);
104
+ if (!existing) {
105
+ throw new PMOError('NOT_FOUND', `Category not found: ${id}`);
106
+ }
107
+ if (existing.isBuiltin) {
108
+ throw new PMOError('INVALID', 'Cannot modify built-in categories');
109
+ }
110
+ // Check for duplicate name if name is changing
111
+ if (changes.name && changes.name.toLowerCase() !== existing.name.toLowerCase()) {
112
+ const dup = this.ctx.db.prepare(`
113
+ SELECT id FROM ${T.categories} WHERE LOWER(name) = LOWER(?) AND type = ? AND id != ?
114
+ `).get(changes.name, existing.type, id);
115
+ if (dup) {
116
+ throw new PMOError('CONFLICT', `Category "${changes.name}" already exists for type "${existing.type}"`);
117
+ }
118
+ }
119
+ const updates = [];
120
+ const params = [];
121
+ if (changes.name !== undefined) {
122
+ updates.push('name = ?');
123
+ params.push(changes.name);
124
+ }
125
+ if (changes.description !== undefined) {
126
+ updates.push('description = ?');
127
+ params.push(changes.description || null);
128
+ }
129
+ if (changes.color !== undefined) {
130
+ updates.push('color = ?');
131
+ params.push(changes.color || null);
132
+ }
133
+ if (changes.position !== undefined) {
134
+ updates.push('position = ?');
135
+ params.push(changes.position);
136
+ }
137
+ if (updates.length > 0) {
138
+ params.push(id);
139
+ this.ctx.db.prepare(`UPDATE ${T.categories} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
140
+ }
141
+ return (await this.getCategory(id));
142
+ }
143
+ /**
144
+ * Rename a category.
145
+ */
146
+ async renameCategory(id, newName) {
147
+ return this.updateCategory(id, { name: newName });
148
+ }
149
+ /**
150
+ * Delete a category.
151
+ */
152
+ async deleteCategory(id) {
153
+ const existing = await this.getCategory(id);
154
+ if (!existing) {
155
+ throw new PMOError('NOT_FOUND', `Category not found: ${id}`);
156
+ }
157
+ if (existing.isBuiltin) {
158
+ throw new PMOError('INVALID', 'Cannot delete built-in categories');
159
+ }
160
+ // Check if the category is in use
161
+ if (existing.type === 'ticket') {
162
+ const ticketsUsing = this.ctx.db.prepare(`
163
+ SELECT COUNT(*) as count FROM ${T.tickets} WHERE category = ?
164
+ `).get(existing.name);
165
+ if (ticketsUsing.count > 0) {
166
+ throw new PMOError('INVALID', `Cannot delete category "${existing.name}": ${ticketsUsing.count} ticket(s) are using it. Reassign tickets first.`);
167
+ }
168
+ }
169
+ else if (existing.type === 'status') {
170
+ const statusesUsing = this.ctx.db.prepare(`
171
+ SELECT COUNT(*) as count FROM ${T.workflow_statuses} WHERE category = ?
172
+ `).get(existing.name);
173
+ if (statusesUsing.count > 0) {
174
+ throw new PMOError('INVALID', `Cannot delete category "${existing.name}": ${statusesUsing.count} status(es) are using it. Reassign statuses first.`);
175
+ }
176
+ }
177
+ this.ctx.db.prepare(`DELETE FROM ${T.categories} WHERE id = ?`).run(id);
178
+ }
179
+ /**
180
+ * Get category names for a type (for validation and autocomplete).
181
+ */
182
+ async getCategoryNames(type) {
183
+ const categories = await this.listCategories({ type });
184
+ return categories.map(c => c.name);
185
+ }
186
+ /**
187
+ * Check if a category name is valid for a type.
188
+ */
189
+ async isValidCategory(name, type) {
190
+ const category = await this.getCategoryByName(name, type);
191
+ return category !== null;
192
+ }
193
+ rowToCategory(row) {
194
+ return {
195
+ id: row.id,
196
+ name: row.name,
197
+ type: row.type,
198
+ description: row.description || undefined,
199
+ color: row.color || undefined,
200
+ position: row.position,
201
+ isBuiltin: row.is_builtin === 1,
202
+ createdAt: new Date(row.created_at),
203
+ };
204
+ }
205
+ }