@proletariat/cli 0.3.20 → 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 (352) 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 +4 -2
  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 +1 -1
  22. package/dist/commands/agent/remove.js +38 -28
  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 +4 -2
  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 +4 -2
  33. package/dist/commands/agent/status.d.ts +0 -1
  34. package/dist/commands/agent/status.js +4 -2
  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/add-names.d.ts +1 -0
  40. package/dist/commands/agent/themes/add-names.js +5 -1
  41. package/dist/commands/agent/themes/index.d.ts +0 -1
  42. package/dist/commands/agent/themes/index.js +3 -7
  43. package/dist/commands/agent/themes/set.d.ts +0 -1
  44. package/dist/commands/agent/themes/set.js +3 -7
  45. package/dist/commands/agent/visit.d.ts +0 -1
  46. package/dist/commands/agent/visit.js +4 -2
  47. package/dist/commands/autocomplete/setup.d.ts +0 -1
  48. package/dist/commands/board/index.d.ts +0 -1
  49. package/dist/commands/board/view.d.ts +0 -1
  50. package/dist/commands/board/watch.d.ts +0 -1
  51. package/dist/commands/branch/create.d.ts +0 -1
  52. package/dist/commands/branch/create.js +2 -0
  53. package/dist/commands/branch/index.d.ts +0 -1
  54. package/dist/commands/branch/index.js +2 -0
  55. package/dist/commands/branch/list.d.ts +0 -1
  56. package/dist/commands/branch/validate.d.ts +0 -1
  57. package/dist/commands/branch/where.d.ts +0 -1
  58. package/dist/commands/branch/where.js +2 -0
  59. package/dist/commands/category/create.d.ts +18 -0
  60. package/dist/commands/category/create.js +108 -0
  61. package/dist/commands/category/delete.d.ts +17 -0
  62. package/dist/commands/category/delete.js +103 -0
  63. package/dist/commands/category/index.d.ts +15 -0
  64. package/dist/commands/category/index.js +87 -0
  65. package/dist/commands/category/list.d.ts +17 -0
  66. package/dist/commands/category/list.js +85 -0
  67. package/dist/commands/category/rename.d.ts +18 -0
  68. package/dist/commands/category/rename.js +127 -0
  69. package/dist/commands/claude.js +2 -0
  70. package/dist/commands/commit.js +2 -0
  71. package/dist/commands/config/index.js +2 -0
  72. package/dist/commands/docker/clean.d.ts +0 -1
  73. package/dist/commands/docker/index.d.ts +0 -1
  74. package/dist/commands/docker/prune.d.ts +0 -1
  75. package/dist/commands/docker/restart.d.ts +0 -1
  76. package/dist/commands/docker/stop.d.ts +0 -1
  77. package/dist/commands/epic/activate.d.ts +0 -1
  78. package/dist/commands/epic/activate.js +2 -0
  79. package/dist/commands/epic/archive.d.ts +0 -1
  80. package/dist/commands/epic/archive.js +2 -0
  81. package/dist/commands/epic/create.d.ts +0 -1
  82. package/dist/commands/epic/create.js +3 -1
  83. package/dist/commands/epic/delete.d.ts +14 -0
  84. package/dist/commands/epic/delete.js +129 -0
  85. package/dist/commands/epic/index.d.ts +0 -1
  86. package/dist/commands/epic/index.js +6 -0
  87. package/dist/commands/epic/link/block.d.ts +0 -1
  88. package/dist/commands/epic/link/block.js +2 -0
  89. package/dist/commands/epic/link/duplicates.d.ts +0 -1
  90. package/dist/commands/epic/link/duplicates.js +2 -0
  91. package/dist/commands/epic/link/index.d.ts +0 -1
  92. package/dist/commands/epic/link/index.js +19 -0
  93. package/dist/commands/epic/link/relates.d.ts +0 -1
  94. package/dist/commands/epic/link/relates.js +2 -0
  95. package/dist/commands/epic/link/remove.d.ts +0 -1
  96. package/dist/commands/epic/link/remove.js +2 -0
  97. package/dist/commands/epic/list.d.ts +0 -1
  98. package/dist/commands/epic/move.d.ts +0 -1
  99. package/dist/commands/epic/move.js +2 -0
  100. package/dist/commands/epic/progress.d.ts +0 -1
  101. package/dist/commands/epic/progress.js +2 -0
  102. package/dist/commands/epic/project.d.ts +0 -1
  103. package/dist/commands/epic/project.js +2 -0
  104. package/dist/commands/epic/reorder.d.ts +0 -1
  105. package/dist/commands/epic/reorder.js +2 -0
  106. package/dist/commands/epic/spec.d.ts +0 -1
  107. package/dist/commands/epic/spec.js +2 -0
  108. package/dist/commands/epic/ticket.d.ts +0 -1
  109. package/dist/commands/epic/ticket.js +2 -0
  110. package/dist/commands/epic/view.d.ts +0 -1
  111. package/dist/commands/epic/view.js +2 -0
  112. package/dist/commands/execution/config.d.ts +0 -1
  113. package/dist/commands/execution/config.js +24 -0
  114. package/dist/commands/execution/index.d.ts +0 -1
  115. package/dist/commands/execution/index.js +3 -1
  116. package/dist/commands/execution/kill.d.ts +3 -0
  117. package/dist/commands/execution/kill.js +1 -0
  118. package/dist/commands/execution/list.d.ts +0 -1
  119. package/dist/commands/execution/list.js +5 -4
  120. package/dist/commands/execution/logs.d.ts +0 -1
  121. package/dist/commands/execution/logs.js +4 -1
  122. package/dist/commands/execution/stop.d.ts +0 -1
  123. package/dist/commands/execution/stop.js +3 -1
  124. package/dist/commands/execution/view.d.ts +0 -1
  125. package/dist/commands/execution/view.js +3 -1
  126. package/dist/commands/gh/index.d.ts +0 -1
  127. package/dist/commands/gh/login.d.ts +0 -1
  128. package/dist/commands/gh/status.d.ts +0 -1
  129. package/dist/commands/gh/token.d.ts +0 -1
  130. package/dist/commands/init.js +2 -0
  131. package/dist/commands/phase/create.d.ts +0 -1
  132. package/dist/commands/phase/create.js +1 -2
  133. package/dist/commands/phase/delete.d.ts +0 -1
  134. package/dist/commands/phase/delete.js +1 -1
  135. package/dist/commands/phase/list.d.ts +0 -1
  136. package/dist/commands/phase/move.d.ts +0 -1
  137. package/dist/commands/phase/move.js +10 -2
  138. package/dist/commands/phase/template/apply.d.ts +0 -1
  139. package/dist/commands/phase/template/apply.js +4 -2
  140. package/dist/commands/phase/template/create.d.ts +0 -1
  141. package/dist/commands/phase/template/create.js +6 -7
  142. package/dist/commands/phase/template/delete.d.ts +0 -1
  143. package/dist/commands/phase/template/delete.js +2 -0
  144. package/dist/commands/phase/template/index.d.ts +0 -1
  145. package/dist/commands/phase/template/index.js +2 -0
  146. package/dist/commands/phase/template/list.d.ts +0 -1
  147. package/dist/commands/phase/template/list.js +3 -1
  148. package/dist/commands/phase/template/update.d.ts +0 -1
  149. package/dist/commands/phase/update.d.ts +0 -1
  150. package/dist/commands/phase/update.js +2 -2
  151. package/dist/commands/pmo/init.js +2 -0
  152. package/dist/commands/pr/create.d.ts +0 -1
  153. package/dist/commands/pr/index.d.ts +0 -1
  154. package/dist/commands/pr/link.d.ts +0 -1
  155. package/dist/commands/pr/list.d.ts +0 -1
  156. package/dist/commands/pr/status.d.ts +0 -1
  157. package/dist/commands/project/archive.d.ts +0 -1
  158. package/dist/commands/project/create.d.ts +0 -1
  159. package/dist/commands/project/delete.d.ts +0 -1
  160. package/dist/commands/project/index.d.ts +0 -1
  161. package/dist/commands/project/list.d.ts +0 -1
  162. package/dist/commands/project/spec.d.ts +0 -1
  163. package/dist/commands/project/unarchive.d.ts +0 -1
  164. package/dist/commands/project/update.d.ts +0 -1
  165. package/dist/commands/project/view.d.ts +0 -1
  166. package/dist/commands/repo/add.d.ts +0 -1
  167. package/dist/commands/repo/add.js +2 -0
  168. package/dist/commands/repo/index.d.ts +0 -1
  169. package/dist/commands/repo/list.d.ts +0 -1
  170. package/dist/commands/repo/remove.d.ts +0 -1
  171. package/dist/commands/repo/view.d.ts +0 -1
  172. package/dist/commands/roadmap/add-project.d.ts +0 -1
  173. package/dist/commands/roadmap/add-project.js +2 -0
  174. package/dist/commands/roadmap/create.d.ts +0 -1
  175. package/dist/commands/roadmap/create.js +2 -0
  176. package/dist/commands/roadmap/delete.d.ts +0 -1
  177. package/dist/commands/roadmap/delete.js +12 -1
  178. package/dist/commands/roadmap/generate.d.ts +0 -1
  179. package/dist/commands/roadmap/generate.js +2 -0
  180. package/dist/commands/roadmap/index.d.ts +0 -1
  181. package/dist/commands/roadmap/index.js +2 -0
  182. package/dist/commands/roadmap/list.d.ts +0 -1
  183. package/dist/commands/roadmap/remove-project.d.ts +0 -1
  184. package/dist/commands/roadmap/remove-project.js +2 -0
  185. package/dist/commands/roadmap/reorder.d.ts +0 -1
  186. package/dist/commands/roadmap/reorder.js +2 -0
  187. package/dist/commands/roadmap/update.d.ts +0 -1
  188. package/dist/commands/roadmap/update.js +2 -0
  189. package/dist/commands/roadmap/view.d.ts +0 -1
  190. package/dist/commands/roadmap/view.js +2 -0
  191. package/dist/commands/session/attach.d.ts +0 -1
  192. package/dist/commands/session/attach.js +9 -0
  193. package/dist/commands/session/index.d.ts +0 -1
  194. package/dist/commands/session/index.js +2 -0
  195. package/dist/commands/session/list.d.ts +0 -1
  196. package/dist/commands/spec/create.d.ts +0 -1
  197. package/dist/commands/spec/create.js +1 -1
  198. package/dist/commands/spec/delete.d.ts +0 -1
  199. package/dist/commands/spec/edit.d.ts +0 -1
  200. package/dist/commands/spec/index.d.ts +0 -1
  201. package/dist/commands/spec/link/depends.d.ts +0 -1
  202. package/dist/commands/spec/link/duplicates.d.ts +0 -1
  203. package/dist/commands/spec/link/index.d.ts +0 -1
  204. package/dist/commands/spec/link/relates.d.ts +0 -1
  205. package/dist/commands/spec/link/remove.d.ts +0 -1
  206. package/dist/commands/spec/list.d.ts +0 -1
  207. package/dist/commands/spec/plan.d.ts +0 -1
  208. package/dist/commands/spec/ticket.d.ts +0 -3
  209. package/dist/commands/spec/ticket.js +7 -38
  210. package/dist/commands/spec/view.d.ts +0 -1
  211. package/dist/commands/status/category.d.ts +14 -0
  212. package/dist/commands/status/category.js +63 -0
  213. package/dist/commands/status/create.d.ts +0 -1
  214. package/dist/commands/status/create.js +1 -1
  215. package/dist/commands/status/delete.d.ts +0 -1
  216. package/dist/commands/status/index.d.ts +0 -1
  217. package/dist/commands/status/list.d.ts +0 -1
  218. package/dist/commands/status/list.js +5 -3
  219. package/dist/commands/status/move.d.ts +0 -1
  220. package/dist/commands/status/update.d.ts +0 -1
  221. package/dist/commands/template/delete.d.ts +0 -1
  222. package/dist/commands/template/delete.js +2 -0
  223. package/dist/commands/template/index.d.ts +0 -1
  224. package/dist/commands/template/list.d.ts +0 -1
  225. package/dist/commands/template/list.js +2 -0
  226. package/dist/commands/template/phase/apply.js +2 -0
  227. package/dist/commands/template/phase/create.d.ts +0 -1
  228. package/dist/commands/template/phase/create.js +3 -9
  229. package/dist/commands/template/phase/delete.js +2 -0
  230. package/dist/commands/template/phase/index.d.ts +0 -1
  231. package/dist/commands/template/phase/index.js +4 -4
  232. package/dist/commands/template/phase/list.js +2 -0
  233. package/dist/commands/template/phase/update.js +2 -0
  234. package/dist/commands/template/ticket/apply.js +2 -0
  235. package/dist/commands/template/ticket/create.js +2 -0
  236. package/dist/commands/template/ticket/delete.d.ts +1 -1
  237. package/dist/commands/template/ticket/delete.js +6 -2
  238. package/dist/commands/template/ticket/index.d.ts +0 -1
  239. package/dist/commands/template/ticket/list.js +2 -0
  240. package/dist/commands/template/ticket/save.d.ts +0 -1
  241. package/dist/commands/template/ticket/save.js +0 -6
  242. package/dist/commands/terminal/title.d.ts +0 -1
  243. package/dist/commands/ticket/bulk.d.ts +0 -1
  244. package/dist/commands/ticket/bulk.js +2 -0
  245. package/dist/commands/ticket/category.d.ts +14 -0
  246. package/dist/commands/ticket/category.js +63 -0
  247. package/dist/commands/ticket/complete.d.ts +0 -1
  248. package/dist/commands/ticket/complete.js +2 -0
  249. package/dist/commands/ticket/create.d.ts +0 -1
  250. package/dist/commands/ticket/create.js +7 -5
  251. package/dist/commands/ticket/delete.d.ts +0 -1
  252. package/dist/commands/ticket/delete.js +2 -0
  253. package/dist/commands/ticket/edit.d.ts +0 -1
  254. package/dist/commands/ticket/edit.js +5 -3
  255. package/dist/commands/ticket/epic.d.ts +0 -1
  256. package/dist/commands/ticket/epic.js +2 -0
  257. package/dist/commands/ticket/index.d.ts +0 -1
  258. package/dist/commands/ticket/index.js +2 -0
  259. package/dist/commands/ticket/link/block.d.ts +0 -1
  260. package/dist/commands/ticket/link/block.js +2 -0
  261. package/dist/commands/ticket/link/duplicates.d.ts +0 -1
  262. package/dist/commands/ticket/link/duplicates.js +2 -0
  263. package/dist/commands/ticket/link/index.d.ts +0 -1
  264. package/dist/commands/ticket/link/index.js +2 -0
  265. package/dist/commands/ticket/link/relates.d.ts +0 -1
  266. package/dist/commands/ticket/link/relates.js +2 -0
  267. package/dist/commands/ticket/link/remove.d.ts +0 -1
  268. package/dist/commands/ticket/link/remove.js +2 -0
  269. package/dist/commands/ticket/list.d.ts +2 -1
  270. package/dist/commands/ticket/list.js +39 -2
  271. package/dist/commands/ticket/move.d.ts +0 -1
  272. package/dist/commands/ticket/move.js +2 -0
  273. package/dist/commands/ticket/project.d.ts +0 -1
  274. package/dist/commands/ticket/project.js +2 -0
  275. package/dist/commands/ticket/reassign.d.ts +0 -1
  276. package/dist/commands/ticket/reassign.js +29 -0
  277. package/dist/commands/ticket/spec.d.ts +0 -1
  278. package/dist/commands/ticket/spec.js +2 -0
  279. package/dist/commands/ticket/status.d.ts +0 -1
  280. package/dist/commands/ticket/status.js +2 -0
  281. package/dist/commands/ticket/template/apply.d.ts +0 -1
  282. package/dist/commands/ticket/template/apply.js +2 -0
  283. package/dist/commands/ticket/template/create.d.ts +0 -1
  284. package/dist/commands/ticket/template/create.js +4 -2
  285. package/dist/commands/ticket/template/delete.d.ts +0 -1
  286. package/dist/commands/ticket/template/delete.js +2 -0
  287. package/dist/commands/ticket/template/index.d.ts +0 -1
  288. package/dist/commands/ticket/template/index.js +2 -0
  289. package/dist/commands/ticket/template/list.d.ts +0 -1
  290. package/dist/commands/ticket/template/list.js +2 -0
  291. package/dist/commands/ticket/template/save.d.ts +0 -1
  292. package/dist/commands/ticket/template/save.js +2 -0
  293. package/dist/commands/ticket/update.d.ts +0 -1
  294. package/dist/commands/ticket/update.js +4 -2
  295. package/dist/commands/ticket/view.d.ts +0 -1
  296. package/dist/commands/ticket/view.js +2 -0
  297. package/dist/commands/work/complete.d.ts +0 -1
  298. package/dist/commands/work/complete.js +2 -0
  299. package/dist/commands/work/index.d.ts +0 -1
  300. package/dist/commands/work/index.js +2 -0
  301. package/dist/commands/work/ready.d.ts +1 -2
  302. package/dist/commands/work/ready.js +11 -5
  303. package/dist/commands/work/revise.d.ts +0 -1
  304. package/dist/commands/work/revise.js +3 -1
  305. package/dist/commands/work/spawn-all.d.ts +0 -1
  306. package/dist/commands/work/spawn-all.js +2 -0
  307. package/dist/commands/work/spawn.d.ts +0 -1
  308. package/dist/commands/work/spawn.js +34 -8
  309. package/dist/commands/work/start.d.ts +0 -1
  310. package/dist/commands/work/start.js +6 -0
  311. package/dist/commands/work/watch.d.ts +0 -1
  312. package/dist/commands/work/watch.js +5 -1
  313. package/dist/commands/workflow/create.d.ts +0 -1
  314. package/dist/commands/workflow/delete.d.ts +0 -1
  315. package/dist/commands/workflow/index.d.ts +0 -1
  316. package/dist/commands/workflow/index.js +2 -0
  317. package/dist/commands/workflow/list.d.ts +0 -1
  318. package/dist/commands/workflow/switch.d.ts +0 -1
  319. package/dist/commands/workflow/view.d.ts +0 -1
  320. package/dist/commands/workspace/list.js +2 -0
  321. package/dist/commands/workspace/prune.d.ts +13 -0
  322. package/dist/commands/workspace/prune.js +186 -0
  323. package/dist/commands/workspace/remove.js +2 -0
  324. package/dist/commands/workspace/use.js +2 -0
  325. package/dist/lib/agents/commands.d.ts +7 -0
  326. package/dist/lib/agents/commands.js +11 -0
  327. package/dist/lib/execution/runners.js +1 -2
  328. package/dist/lib/pmo/base-command.d.ts +2 -4
  329. package/dist/lib/pmo/base-command.js +8 -10
  330. package/dist/lib/pmo/schema.d.ts +2 -0
  331. package/dist/lib/pmo/schema.js +17 -0
  332. package/dist/lib/pmo/storage/base.d.ts +4 -0
  333. package/dist/lib/pmo/storage/base.js +31 -0
  334. package/dist/lib/pmo/storage/categories.d.ts +50 -0
  335. package/dist/lib/pmo/storage/categories.js +205 -0
  336. package/dist/lib/pmo/storage/epics.js +20 -10
  337. package/dist/lib/pmo/storage/helpers.d.ts +10 -0
  338. package/dist/lib/pmo/storage/helpers.js +59 -1
  339. package/dist/lib/pmo/storage/index.d.ts +14 -1
  340. package/dist/lib/pmo/storage/index.js +35 -1
  341. package/dist/lib/pmo/storage/projects.js +20 -8
  342. package/dist/lib/pmo/storage/specs.js +23 -13
  343. package/dist/lib/pmo/storage/statuses.js +39 -18
  344. package/dist/lib/pmo/storage/subtasks.js +19 -8
  345. package/dist/lib/pmo/storage/tickets.d.ts +5 -0
  346. package/dist/lib/pmo/storage/tickets.js +57 -17
  347. package/dist/lib/pmo/storage/types.d.ts +10 -0
  348. package/dist/lib/pmo/types.d.ts +25 -0
  349. package/dist/lib/prompt-json.d.ts +10 -16
  350. package/dist/lib/prompt-json.js +8 -16
  351. package/oclif.manifest.json +3831 -3864
  352. package/package.json +1 -1
@@ -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
+ }
@@ -4,7 +4,7 @@
4
4
  import { PMO_TABLES } from '../schema.js';
5
5
  import { PMOError } from '../types.js';
6
6
  import { generateEntityId } from '../utils.js';
7
- import { rowToTicket } from './helpers.js';
7
+ import { rowToTicket, wrapSqliteError } from './helpers.js';
8
8
  const T = PMO_TABLES;
9
9
  export class EpicStorage {
10
10
  ctx;
@@ -27,10 +27,15 @@ export class EpicStorage {
27
27
  `).get(projectId);
28
28
  position = maxPos.max_pos + 1;
29
29
  }
30
- this.ctx.db.prepare(`
31
- INSERT INTO ${T.epics} (id, project_id, title, description, status, position, file_path, spec_id, created_at, updated_at)
32
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33
- `).run(id, projectId, title, epic.description || null, status, position, epic.filePath || null, epic.specId || null, now, now);
30
+ try {
31
+ this.ctx.db.prepare(`
32
+ INSERT INTO ${T.epics} (id, project_id, title, description, status, position, file_path, spec_id, created_at, updated_at)
33
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
34
+ `).run(id, projectId, title, epic.description || null, status, position, epic.filePath || null, epic.specId || null, now, now);
35
+ }
36
+ catch (err) {
37
+ wrapSqliteError('Epic', 'create', err);
38
+ }
34
39
  this.ctx.updateBoardTimestamp(projectId);
35
40
  return {
36
41
  id,
@@ -155,11 +160,16 @@ export class EpicStorage {
155
160
  if (!epic) {
156
161
  throw new PMOError('NOT_FOUND', `Epic not found: ${id}`);
157
162
  }
158
- // Unlink tickets from this epic
159
- this.ctx.db.prepare(`
160
- UPDATE ${T.tickets} SET epic_id = NULL WHERE epic_id = ?
161
- `).run(id);
162
- this.ctx.db.prepare(`DELETE FROM ${T.epics} WHERE id = ?`).run(id);
163
+ try {
164
+ // Unlink tickets from this epic
165
+ this.ctx.db.prepare(`
166
+ UPDATE ${T.tickets} SET epic_id = NULL WHERE epic_id = ?
167
+ `).run(id);
168
+ this.ctx.db.prepare(`DELETE FROM ${T.epics} WHERE id = ?`).run(id);
169
+ }
170
+ catch (err) {
171
+ wrapSqliteError('Epic', 'delete', err);
172
+ }
163
173
  this.ctx.updateBoardTimestamp(epic.projectId);
164
174
  }
165
175
  /**
@@ -4,6 +4,16 @@
4
4
  import Database from 'better-sqlite3';
5
5
  import { AcceptanceCriterion, Spec, StateCategory, Ticket } from '../types.js';
6
6
  import { SpecRow, TicketRow, WorkflowStatusRow } from './types.js';
7
+ /**
8
+ * Wrap SQLite constraint errors with user-friendly messages.
9
+ * This function always throws - it never returns.
10
+ *
11
+ * @param entityType - The type of entity being operated on (e.g., 'Ticket', 'Spec', 'Project')
12
+ * @param operation - The operation being performed ('create', 'update', 'delete')
13
+ * @param err - The error thrown by SQLite
14
+ * @throws {PMOError} Always throws a user-friendly PMOError
15
+ */
16
+ export declare function wrapSqliteError(entityType: string, operation: 'create' | 'update' | 'delete', err: unknown): never;
7
17
  /**
8
18
  * Convert a database row to a Ticket object.
9
19
  * Fetches related data (subtasks, metadata, status info).
@@ -2,7 +2,65 @@
2
2
  * Helper functions for converting database rows to domain types.
3
3
  */
4
4
  import { PMO_TABLES } from '../schema.js';
5
- import { normalizePriority, } from '../types.js';
5
+ import { PMOError, normalizePriority, } from '../types.js';
6
+ /**
7
+ * Check if an error is a SQLite UNIQUE constraint violation.
8
+ */
9
+ function isUniqueConstraintError(err) {
10
+ if (!(err instanceof Error))
11
+ return false;
12
+ const sqliteErr = err;
13
+ return (sqliteErr.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
14
+ sqliteErr.message.includes('UNIQUE constraint failed'));
15
+ }
16
+ /**
17
+ * Check if an error is a SQLite FOREIGN KEY constraint violation.
18
+ */
19
+ function isForeignKeyConstraintError(err) {
20
+ if (!(err instanceof Error))
21
+ return false;
22
+ const sqliteErr = err;
23
+ return (sqliteErr.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' ||
24
+ sqliteErr.message.includes('FOREIGN KEY constraint failed'));
25
+ }
26
+ /**
27
+ * Check if an error is a SQLite CHECK constraint violation.
28
+ */
29
+ function isCheckConstraintError(err) {
30
+ if (!(err instanceof Error))
31
+ return false;
32
+ const sqliteErr = err;
33
+ return (sqliteErr.code === 'SQLITE_CONSTRAINT_CHECK' ||
34
+ sqliteErr.message.includes('CHECK constraint failed'));
35
+ }
36
+ /**
37
+ * Wrap SQLite constraint errors with user-friendly messages.
38
+ * This function always throws - it never returns.
39
+ *
40
+ * @param entityType - The type of entity being operated on (e.g., 'Ticket', 'Spec', 'Project')
41
+ * @param operation - The operation being performed ('create', 'update', 'delete')
42
+ * @param err - The error thrown by SQLite
43
+ * @throws {PMOError} Always throws a user-friendly PMOError
44
+ */
45
+ export function wrapSqliteError(entityType, operation, err) {
46
+ if (isUniqueConstraintError(err)) {
47
+ if (operation === 'create') {
48
+ throw new PMOError('CONFLICT', `${entityType} with this ID already exists`);
49
+ }
50
+ throw new PMOError('CONFLICT', `${entityType} already exists with that value`);
51
+ }
52
+ if (isForeignKeyConstraintError(err)) {
53
+ if (operation === 'delete') {
54
+ throw new PMOError('CONFLICT', `Cannot delete ${entityType.toLowerCase()}: it has dependencies. Remove them first.`);
55
+ }
56
+ throw new PMOError('INVALID', `Cannot ${operation} ${entityType.toLowerCase()}: referenced entity does not exist`);
57
+ }
58
+ if (isCheckConstraintError(err)) {
59
+ throw new PMOError('INVALID', `Invalid ${entityType.toLowerCase()} data: constraint check failed`);
60
+ }
61
+ // Re-throw unknown errors
62
+ throw err;
63
+ }
6
64
  const T = PMO_TABLES;
7
65
  /**
8
66
  * Convert a database row to a Ticket object.
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import Database from 'better-sqlite3';
11
11
  import { DrizzleDB } from '../../database/drizzle.js';
12
- import { AcceptanceCriterion, Board, BoardConfig, BoardView, BoardViewFilter, BoardViewFilters, Column, CreateTicketInput, Epic, EpicDependency, EpicDependencyType, EpicFilter, PhaseFilter, PhaseTemplate, PhaseTemplateFilter, PMOStorage, Project, ProjectFilter, ProjectPhase, Roadmap, RoadmapFilter, RoadmapProject, Spec, SpecDependency, SpecDependencyType, SpecFilter, StateCategory, Subtask, SyncResult, SyncStatus, Ticket, TicketDependency, TicketDependencyType, TicketFilter, TicketTemplate, TicketTemplateFilter, WorkAction, WorkActionFilter, Workflow, WorkflowFilter, WorkflowStatus } from '../types.js';
12
+ import { AcceptanceCriterion, Board, BoardConfig, BoardView, BoardViewFilter, BoardViewFilters, Category, CategoryFilter, CategoryType, Column, CreateTicketInput, Epic, EpicDependency, EpicDependencyType, EpicFilter, PhaseFilter, PhaseTemplate, PhaseTemplateFilter, PMOStorage, Project, ProjectFilter, ProjectPhase, Roadmap, RoadmapFilter, RoadmapProject, Spec, SpecDependency, SpecDependencyType, SpecFilter, StateCategory, Subtask, SyncResult, SyncStatus, Ticket, TicketDependency, TicketDependencyType, TicketFilter, TicketTemplate, TicketTemplateFilter, WorkAction, WorkActionFilter, Workflow, WorkflowFilter, WorkflowStatus } from '../types.js';
13
13
  export declare class SQLiteStorage implements PMOStorage {
14
14
  readonly type: "sqlite";
15
15
  private db;
@@ -28,6 +28,7 @@ export declare class SQLiteStorage implements PMOStorage {
28
28
  private actionStorage;
29
29
  private viewStorage;
30
30
  private roadmapStorage;
31
+ private categoryStorage;
31
32
  constructor(dbPath: string);
32
33
  /**
33
34
  * Get the underlying database connection.
@@ -186,6 +187,18 @@ export declare class SQLiteStorage implements PMOStorage {
186
187
  removeProjectFromRoadmap(roadmapId: string, projectId: string): Promise<void>;
187
188
  reorderRoadmapProject(roadmapId: string, projectId: string, newPosition: number): Promise<RoadmapProject>;
188
189
  getRoadmapsForProject(projectId: string): Promise<Roadmap[]>;
190
+ listCategories(filter?: CategoryFilter): Promise<Category[]>;
191
+ getCategory(id: string): Promise<Category | null>;
192
+ getCategoryByName(name: string, type: CategoryType): Promise<Category | null>;
193
+ createCategory(category: Partial<Category> & {
194
+ name: string;
195
+ type: CategoryType;
196
+ }): Promise<Category>;
197
+ updateCategory(id: string, changes: Partial<Category>): Promise<Category>;
198
+ renameCategory(id: string, newName: string): Promise<Category>;
199
+ deleteCategory(id: string): Promise<void>;
200
+ getCategoryNames(type: CategoryType): Promise<string[]>;
201
+ isValidCategory(name: string, type: CategoryType): Promise<boolean>;
189
202
  pull(): Promise<SyncResult>;
190
203
  push(): Promise<SyncResult>;
191
204
  status(): Promise<SyncStatus>;
@@ -10,7 +10,7 @@
10
10
  import Database from 'better-sqlite3';
11
11
  import { createDrizzleConnection } from '../../database/drizzle.js';
12
12
  import { PMO_TABLES, PMO_SCHEMA_SQL, validateTicketSchema } from '../schema.js';
13
- import { runMigrations, seedBuiltinWorkflows, seedBuiltinPhases, seedBuiltinPhaseTemplates, seedBuiltinActions, seedBuiltinTicketTemplates, updateBoardTimestamp, } from './base.js';
13
+ import { runMigrations, seedBuiltinWorkflows, seedBuiltinPhases, seedBuiltinPhaseTemplates, seedBuiltinActions, seedBuiltinTicketTemplates, seedBuiltinCategories, updateBoardTimestamp, } from './base.js';
14
14
  import { ProjectStorage } from './projects.js';
15
15
  import { TicketStorage } from './tickets.js';
16
16
  import { SubtaskStorage, AcceptanceCriteriaStorage } from './subtasks.js';
@@ -23,6 +23,7 @@ import { PhaseStorage } from './phases.js';
23
23
  import { ActionStorage } from './actions.js';
24
24
  import { ViewStorage } from './views.js';
25
25
  import { RoadmapStorage } from './roadmaps.js';
26
+ import { CategoryStorage } from './categories.js';
26
27
  const T = PMO_TABLES;
27
28
  export class SQLiteStorage {
28
29
  type = 'sqlite';
@@ -43,6 +44,7 @@ export class SQLiteStorage {
43
44
  actionStorage;
44
45
  viewStorage;
45
46
  roadmapStorage;
47
+ categoryStorage;
46
48
  constructor(dbPath) {
47
49
  this.dbPath = dbPath;
48
50
  // Open database (creates if doesn't exist)
@@ -71,6 +73,7 @@ export class SQLiteStorage {
71
73
  this.actionStorage = new ActionStorage(ctx);
72
74
  this.viewStorage = new ViewStorage(ctx);
73
75
  this.roadmapStorage = new RoadmapStorage(ctx);
76
+ this.categoryStorage = new CategoryStorage(ctx);
74
77
  // Ensure PMO tables exist
75
78
  this.ensurePMOTables();
76
79
  }
@@ -100,6 +103,7 @@ export class SQLiteStorage {
100
103
  seedBuiltinPhaseTemplates(this.db);
101
104
  seedBuiltinActions(this.db);
102
105
  seedBuiltinTicketTemplates(this.db);
106
+ seedBuiltinCategories(this.db);
103
107
  // Validate schema
104
108
  validateTicketSchema(this.db);
105
109
  }
@@ -579,6 +583,36 @@ export class SQLiteStorage {
579
583
  return this.roadmapStorage.getRoadmapsForProject(projectId);
580
584
  }
581
585
  // ===========================================================================
586
+ // Category Operations
587
+ // ===========================================================================
588
+ async listCategories(filter) {
589
+ return this.categoryStorage.listCategories(filter);
590
+ }
591
+ async getCategory(id) {
592
+ return this.categoryStorage.getCategory(id);
593
+ }
594
+ async getCategoryByName(name, type) {
595
+ return this.categoryStorage.getCategoryByName(name, type);
596
+ }
597
+ async createCategory(category) {
598
+ return this.categoryStorage.createCategory(category);
599
+ }
600
+ async updateCategory(id, changes) {
601
+ return this.categoryStorage.updateCategory(id, changes);
602
+ }
603
+ async renameCategory(id, newName) {
604
+ return this.categoryStorage.renameCategory(id, newName);
605
+ }
606
+ async deleteCategory(id) {
607
+ return this.categoryStorage.deleteCategory(id);
608
+ }
609
+ async getCategoryNames(type) {
610
+ return this.categoryStorage.getCategoryNames(type);
611
+ }
612
+ async isValidCategory(name, type) {
613
+ return this.categoryStorage.isValidCategory(name, type);
614
+ }
615
+ // ===========================================================================
582
616
  // Sync Operations (no-op for pure SQLite)
583
617
  // ===========================================================================
584
618
  async pull() {
@@ -6,7 +6,7 @@ import { PMO_TABLES } from '../schema.js';
6
6
  import { PMOError, } from '../types.js';
7
7
  import { generateEntityId, slugify } from '../utils.js';
8
8
  import { generateBoardMarkdown } from '../markdown.js';
9
- import { rowToTicket } from './helpers.js';
9
+ import { rowToTicket, wrapSqliteError } from './helpers.js';
10
10
  const T = PMO_TABLES;
11
11
  export class ProjectStorage {
12
12
  ctx;
@@ -144,10 +144,15 @@ export class ProjectStorage {
144
144
  // Use the requested workflow if it exists, otherwise fall back to default
145
145
  const finalWorkflowId = workflow ? workflowId : 'default';
146
146
  // Insert project with workflow
147
- this.ctx.db.prepare(`
148
- INSERT OR REPLACE INTO ${T.projects} (id, name, template, description, workflow_id, created_at, updated_at)
149
- VALUES (?, ?, ?, ?, ?, ?, ?)
150
- `).run(id, project.name, workflowId, project.description || null, finalWorkflowId, now, now);
147
+ try {
148
+ this.ctx.db.prepare(`
149
+ INSERT OR REPLACE INTO ${T.projects} (id, name, template, description, workflow_id, created_at, updated_at)
150
+ VALUES (?, ?, ?, ?, ?, ?, ?)
151
+ `).run(id, project.name, workflowId, project.description || null, finalWorkflowId, now, now);
152
+ }
153
+ catch (err) {
154
+ wrapSqliteError('Project', 'create', err);
155
+ }
151
156
  return this.getBoard(id);
152
157
  }
153
158
  /**
@@ -241,9 +246,16 @@ export class ProjectStorage {
241
246
  if (resolvedId === 'default') {
242
247
  throw new PMOError('INVALID', 'Cannot delete the default project');
243
248
  }
244
- const result = this.ctx.db.prepare(`DELETE FROM ${T.projects} WHERE id = ?`).run(resolvedId);
245
- if (result.changes === 0) {
246
- throw new PMOError('NOT_FOUND', `Project not found: ${projectIdOrName}`);
249
+ try {
250
+ const result = this.ctx.db.prepare(`DELETE FROM ${T.projects} WHERE id = ?`).run(resolvedId);
251
+ if (result.changes === 0) {
252
+ throw new PMOError('NOT_FOUND', `Project not found: ${projectIdOrName}`);
253
+ }
254
+ }
255
+ catch (err) {
256
+ if (err instanceof PMOError)
257
+ throw err;
258
+ wrapSqliteError('Project', 'delete', err);
247
259
  }
248
260
  // Tickets are deleted via CASCADE
249
261
  }
@@ -4,7 +4,7 @@
4
4
  import { PMO_TABLES } from '../schema.js';
5
5
  import { PMOError } from '../types.js';
6
6
  import { generateEntityId } from '../utils.js';
7
- import { rowToSpec, rowToTicket } from './helpers.js';
7
+ import { rowToSpec, rowToTicket, wrapSqliteError } from './helpers.js';
8
8
  const T = PMO_TABLES;
9
9
  export class SpecStorage {
10
10
  ctx;
@@ -17,16 +17,21 @@ export class SpecStorage {
17
17
  async createSpec(spec) {
18
18
  const id = spec.id || generateEntityId(this.ctx.db, 'spec');
19
19
  const now = Date.now();
20
- this.ctx.db.prepare(`
21
- INSERT INTO ${T.specs} (
22
- id, title, status, type, tags,
23
- problem, solution, decisions, not_now, ui_ux,
24
- acceptance_criteria, open_questions,
25
- requirements_functional, requirements_technical,
26
- context, created_at, updated_at
27
- )
28
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
29
- `).run(id, spec.title || 'Untitled Spec', spec.status || 'draft', spec.type || null, spec.tags ? JSON.stringify(spec.tags) : null, spec.problem || null, spec.solution || null, spec.decisions || null, spec.notNow || null, spec.uiUx || null, spec.acceptanceCriteria || null, spec.openQuestions || null, spec.requirementsFunctional || null, spec.requirementsTechnical || null, spec.context || null, now, now);
20
+ try {
21
+ this.ctx.db.prepare(`
22
+ INSERT INTO ${T.specs} (
23
+ id, title, status, type, tags,
24
+ problem, solution, decisions, not_now, ui_ux,
25
+ acceptance_criteria, open_questions,
26
+ requirements_functional, requirements_technical,
27
+ context, created_at, updated_at
28
+ )
29
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
30
+ `).run(id, spec.title || 'Untitled Spec', spec.status || 'draft', spec.type || null, spec.tags ? JSON.stringify(spec.tags) : null, spec.problem || null, spec.solution || null, spec.decisions || null, spec.notNow || null, spec.uiUx || null, spec.acceptanceCriteria || null, spec.openQuestions || null, spec.requirementsFunctional || null, spec.requirementsTechnical || null, spec.context || null, now, now);
31
+ }
32
+ catch (err) {
33
+ wrapSqliteError('Spec', 'create', err);
34
+ }
30
35
  return {
31
36
  id,
32
37
  title: spec.title || 'Untitled Spec',
@@ -176,8 +181,13 @@ export class SpecStorage {
176
181
  if (!existing) {
177
182
  throw new PMOError('NOT_FOUND', `Spec not found: ${id}`);
178
183
  }
179
- this.ctx.db.prepare(`DELETE FROM ${T.specs} WHERE id = ?`).run(id);
180
- this.ctx.db.prepare(`UPDATE ${T.tickets} SET spec_id = NULL WHERE spec_id = ?`).run(id);
184
+ try {
185
+ this.ctx.db.prepare(`DELETE FROM ${T.specs} WHERE id = ?`).run(id);
186
+ this.ctx.db.prepare(`UPDATE ${T.tickets} SET spec_id = NULL WHERE spec_id = ?`).run(id);
187
+ }
188
+ catch (err) {
189
+ wrapSqliteError('Spec', 'delete', err);
190
+ }
181
191
  }
182
192
  /**
183
193
  * Link a ticket to a spec.
@@ -6,6 +6,7 @@
6
6
  import { PMO_TABLES } from '../schema.js';
7
7
  import { PMOError, STATE_CATEGORY_ORDER } from '../types.js';
8
8
  import { slugify } from '../utils.js';
9
+ import { wrapSqliteError } from './helpers.js';
9
10
  const T = PMO_TABLES;
10
11
  /**
11
12
  * Convert database row to Workflow object.
@@ -87,10 +88,15 @@ export class StatusStorage {
87
88
  if (existing) {
88
89
  throw new PMOError('CONFLICT', `Workflow with name "${workflow.name}" already exists`);
89
90
  }
90
- this.ctx.db.prepare(`
91
- INSERT INTO ${T.workflows} (id, name, description, is_builtin, created_at, updated_at)
92
- VALUES (?, ?, ?, ?, ?, ?)
93
- `).run(id, workflow.name || 'New Workflow', workflow.description || null, workflow.isBuiltin ? 1 : 0, now, now);
91
+ try {
92
+ this.ctx.db.prepare(`
93
+ INSERT INTO ${T.workflows} (id, name, description, is_builtin, created_at, updated_at)
94
+ VALUES (?, ?, ?, ?, ?, ?)
95
+ `).run(id, workflow.name || 'New Workflow', workflow.description || null, workflow.isBuiltin ? 1 : 0, now, now);
96
+ }
97
+ catch (err) {
98
+ wrapSqliteError('Workflow', 'create', err);
99
+ }
94
100
  return {
95
101
  id,
96
102
  name: workflow.name || 'New Workflow',
@@ -153,9 +159,14 @@ export class StatusStorage {
153
159
  if (projectCount.count > 0) {
154
160
  throw new PMOError('CONFLICT', `Cannot delete workflow: ${projectCount.count} project(s) are using it`);
155
161
  }
156
- // Delete associated statuses first (cascaded by FK, but explicit for safety)
157
- this.ctx.db.prepare(`DELETE FROM ${T.workflow_statuses} WHERE workflow_id = ?`).run(id);
158
- this.ctx.db.prepare(`DELETE FROM ${T.workflows} WHERE id = ?`).run(id);
162
+ try {
163
+ // Delete associated statuses first (cascaded by FK, but explicit for safety)
164
+ this.ctx.db.prepare(`DELETE FROM ${T.workflow_statuses} WHERE workflow_id = ?`).run(id);
165
+ this.ctx.db.prepare(`DELETE FROM ${T.workflows} WHERE id = ?`).run(id);
166
+ }
167
+ catch (err) {
168
+ wrapSqliteError('Workflow', 'delete', err);
169
+ }
159
170
  }
160
171
  /**
161
172
  * Get the workflow for a project.
@@ -237,10 +248,15 @@ export class StatusStorage {
237
248
  WHERE workflow_id = ?
238
249
  `).run(workflowId);
239
250
  }
240
- this.ctx.db.prepare(`
241
- INSERT INTO ${T.workflow_statuses} (id, workflow_id, name, category, position, color, description, is_default, created_at)
242
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
243
- `).run(id, workflowId, status.name || 'New Status', category, position, status.color || null, status.description || null, status.isDefault ? 1 : 0, now);
251
+ try {
252
+ this.ctx.db.prepare(`
253
+ INSERT INTO ${T.workflow_statuses} (id, workflow_id, name, category, position, color, description, is_default, created_at)
254
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
255
+ `).run(id, workflowId, status.name || 'New Status', category, position, status.color || null, status.description || null, status.isDefault ? 1 : 0, now);
256
+ }
257
+ catch (err) {
258
+ wrapSqliteError('Status', 'create', err);
259
+ }
244
260
  // Update workflow's updated_at timestamp
245
261
  this.ctx.db.prepare(`UPDATE ${T.workflows} SET updated_at = ? WHERE id = ?`).run(now, workflowId);
246
262
  return {
@@ -332,13 +348,18 @@ export class StatusStorage {
332
348
  if (ticketCount.count > 0) {
333
349
  throw new PMOError('CONFLICT', `Cannot delete status: ${ticketCount.count} ticket(s) are using it`);
334
350
  }
335
- this.ctx.db.prepare(`DELETE FROM ${T.workflow_statuses} WHERE id = ?`).run(id);
336
- // Reorder remaining statuses
337
- this.ctx.db.prepare(`
338
- UPDATE ${T.workflow_statuses}
339
- SET position = position - 1
340
- WHERE workflow_id = ? AND position > ?
341
- `).run(existing.workflowId, existing.position);
351
+ try {
352
+ this.ctx.db.prepare(`DELETE FROM ${T.workflow_statuses} WHERE id = ?`).run(id);
353
+ // Reorder remaining statuses
354
+ this.ctx.db.prepare(`
355
+ UPDATE ${T.workflow_statuses}
356
+ SET position = position - 1
357
+ WHERE workflow_id = ? AND position > ?
358
+ `).run(existing.workflowId, existing.position);
359
+ }
360
+ catch (err) {
361
+ wrapSqliteError('Status', 'delete', err);
362
+ }
342
363
  // Update workflow's updated_at timestamp
343
364
  this.ctx.db.prepare(`UPDATE ${T.workflows} SET updated_at = ? WHERE id = ?`).run(new Date().toISOString(), existing.workflowId);
344
365
  }