@proletariat/cli 0.3.19 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent/login.js +2 -2
- package/dist/commands/agent/remove.d.ts +1 -0
- package/dist/commands/agent/remove.js +36 -28
- package/dist/commands/agent/shell.js +2 -2
- package/dist/commands/agent/staff/remove.d.ts +1 -0
- package/dist/commands/agent/staff/remove.js +36 -28
- package/dist/commands/agent/status.js +2 -2
- package/dist/commands/agent/temp/cleanup.js +10 -17
- package/dist/commands/agent/themes/add-names.d.ts +1 -0
- package/dist/commands/agent/themes/add-names.js +5 -1
- package/dist/commands/agent/visit.js +2 -2
- package/dist/commands/board/view.d.ts +15 -0
- package/dist/commands/board/view.js +136 -0
- package/dist/commands/config/index.js +6 -3
- package/dist/commands/epic/link/index.js +17 -0
- package/dist/commands/execution/config.d.ts +34 -0
- package/dist/commands/execution/config.js +433 -0
- package/dist/commands/execution/index.js +6 -1
- package/dist/commands/execution/kill.d.ts +12 -0
- package/dist/commands/execution/kill.js +17 -0
- package/dist/commands/execution/list.js +5 -4
- package/dist/commands/execution/logs.js +1 -0
- package/dist/commands/execution/view.d.ts +17 -0
- package/dist/commands/execution/view.js +288 -0
- package/dist/commands/phase/move.js +8 -0
- package/dist/commands/phase/template/apply.js +2 -2
- package/dist/commands/phase/template/create.js +67 -20
- package/dist/commands/phase/template/list.js +1 -1
- package/dist/commands/pr/index.js +6 -2
- package/dist/commands/pr/list.d.ts +17 -0
- package/dist/commands/pr/list.js +163 -0
- package/dist/commands/project/update.d.ts +19 -0
- package/dist/commands/project/update.js +163 -0
- package/dist/commands/roadmap/create.js +5 -0
- package/dist/commands/spec/delete.d.ts +18 -0
- package/dist/commands/spec/delete.js +111 -0
- package/dist/commands/spec/edit.d.ts +23 -0
- package/dist/commands/spec/edit.js +232 -0
- package/dist/commands/spec/index.js +5 -0
- package/dist/commands/status/create.js +38 -34
- package/dist/commands/status/list.js +5 -3
- package/dist/commands/template/phase/create.d.ts +1 -0
- package/dist/commands/template/phase/create.js +10 -1
- package/dist/commands/template/phase/index.js +4 -4
- package/dist/commands/template/ticket/create.d.ts +20 -0
- package/dist/commands/template/ticket/create.js +87 -0
- package/dist/commands/template/ticket/delete.d.ts +1 -1
- package/dist/commands/template/ticket/delete.js +4 -2
- package/dist/commands/template/ticket/save.d.ts +2 -0
- package/dist/commands/template/ticket/save.js +11 -0
- package/dist/commands/ticket/create.js +8 -1
- package/dist/commands/ticket/edit.js +1 -1
- package/dist/commands/ticket/list.d.ts +2 -0
- package/dist/commands/ticket/list.js +39 -2
- package/dist/commands/ticket/template/create.d.ts +9 -1
- package/dist/commands/ticket/template/create.js +224 -52
- package/dist/commands/ticket/template/save.d.ts +2 -1
- package/dist/commands/ticket/template/save.js +58 -7
- package/dist/commands/ticket/update.js +2 -2
- package/dist/commands/work/ready.js +8 -8
- package/dist/commands/work/spawn.js +32 -8
- package/dist/commands/work/watch.js +2 -0
- package/dist/lib/agents/commands.d.ts +7 -0
- package/dist/lib/agents/commands.js +11 -0
- package/dist/lib/agents/index.js +14 -4
- package/dist/lib/branch/index.js +24 -0
- package/dist/lib/execution/config.d.ts +2 -0
- package/dist/lib/execution/config.js +12 -0
- package/dist/lib/execution/runners.js +1 -2
- package/dist/lib/pmo/storage/epics.js +20 -10
- package/dist/lib/pmo/storage/helpers.d.ts +10 -0
- package/dist/lib/pmo/storage/helpers.js +59 -1
- package/dist/lib/pmo/storage/projects.js +20 -8
- package/dist/lib/pmo/storage/specs.js +23 -13
- package/dist/lib/pmo/storage/statuses.js +39 -18
- package/dist/lib/pmo/storage/subtasks.js +19 -8
- package/dist/lib/pmo/storage/tickets.js +27 -15
- package/dist/lib/pmo/utils.d.ts +4 -2
- package/dist/lib/pmo/utils.js +4 -2
- package/oclif.manifest.json +4037 -3234
- package/package.json +2 -4
package/dist/lib/branch/index.js
CHANGED
|
@@ -129,6 +129,14 @@ export function validateBranchName(name) {
|
|
|
129
129
|
if (parts.length === 2) {
|
|
130
130
|
// {type}/{description}
|
|
131
131
|
const description = parts[1];
|
|
132
|
+
// Check if description looks like a ticket ID (user put ticket in wrong position)
|
|
133
|
+
if (isTicketId(description)) {
|
|
134
|
+
return {
|
|
135
|
+
valid: false,
|
|
136
|
+
error: `Segment "${description}" looks like a ticket ID, but ticket IDs must be the first segment. ` +
|
|
137
|
+
`Expected format: {ticketId}/{type}/{description}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
132
140
|
if (!isKebabCase(description)) {
|
|
133
141
|
return {
|
|
134
142
|
valid: false,
|
|
@@ -143,12 +151,28 @@ export function validateBranchName(name) {
|
|
|
143
151
|
// {type}/{owner}/{description}
|
|
144
152
|
const owner = parts[1];
|
|
145
153
|
const description = parts[2];
|
|
154
|
+
// Check if owner looks like a ticket ID (user put ticket in wrong position)
|
|
155
|
+
if (isTicketId(owner)) {
|
|
156
|
+
return {
|
|
157
|
+
valid: false,
|
|
158
|
+
error: `Segment "${owner}" looks like a ticket ID, but it's in the owner position (segment 2). ` +
|
|
159
|
+
`Ticket IDs must be the first segment. Expected format: {ticketId}/{type}/{owner}/{description}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
146
162
|
if (!isKebabCase(owner)) {
|
|
147
163
|
return {
|
|
148
164
|
valid: false,
|
|
149
165
|
error: `Owner name must be kebab-case: "${owner}"`,
|
|
150
166
|
};
|
|
151
167
|
}
|
|
168
|
+
// Check if description looks like a ticket ID (user put ticket in wrong position)
|
|
169
|
+
if (isTicketId(description)) {
|
|
170
|
+
return {
|
|
171
|
+
valid: false,
|
|
172
|
+
error: `Segment "${description}" looks like a ticket ID, but it's in the description position (segment 3). ` +
|
|
173
|
+
`Ticket IDs must be the first segment.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
152
176
|
if (!isKebabCase(description)) {
|
|
153
177
|
return {
|
|
154
178
|
valid: false,
|
|
@@ -18,6 +18,8 @@ const CONFIG_KEYS = {
|
|
|
18
18
|
defaultMode: 'execution.default_mode',
|
|
19
19
|
defaultExecutor: 'execution.default_executor',
|
|
20
20
|
autoExecute: 'execution.auto_execute',
|
|
21
|
+
outputMode: 'execution.output_mode',
|
|
22
|
+
sandboxed: 'execution.sandboxed',
|
|
21
23
|
tmuxSession: 'execution.tmux.session',
|
|
22
24
|
tmuxLayout: 'execution.tmux.layout',
|
|
23
25
|
tmuxControlMode: 'execution.tmux.control_mode',
|
|
@@ -85,6 +87,16 @@ export function loadExecutionConfig(db) {
|
|
|
85
87
|
if (autoExecute !== null) {
|
|
86
88
|
config.autoExecute = autoExecute === 'true';
|
|
87
89
|
}
|
|
90
|
+
// Load output mode
|
|
91
|
+
const outputMode = getSetting(db, CONFIG_KEYS.outputMode);
|
|
92
|
+
if (outputMode) {
|
|
93
|
+
config.outputMode = outputMode;
|
|
94
|
+
}
|
|
95
|
+
// Load sandboxed preference
|
|
96
|
+
const sandboxed = getSetting(db, CONFIG_KEYS.sandboxed);
|
|
97
|
+
if (sandboxed !== null) {
|
|
98
|
+
config.sandboxed = sandboxed === 'true';
|
|
99
|
+
}
|
|
88
100
|
// Load tmux settings
|
|
89
101
|
const tmuxSession = getSetting(db, CONFIG_KEYS.tmuxSession);
|
|
90
102
|
if (tmuxSession) {
|
|
@@ -612,8 +612,7 @@ export function isDockerRunning() {
|
|
|
612
612
|
execSync('docker info', { stdio: 'pipe', timeout });
|
|
613
613
|
return true;
|
|
614
614
|
}
|
|
615
|
-
catch
|
|
616
|
-
console.debug(`[runners:docker] Docker check attempt ${attempt}/${maxRetries} failed:`, err);
|
|
615
|
+
catch {
|
|
617
616
|
if (attempt === maxRetries) {
|
|
618
617
|
return false;
|
|
619
618
|
}
|
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
}
|
|
@@ -5,6 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { PMO_TABLES } from '../schema.js';
|
|
6
6
|
import { PMOError } from '../types.js';
|
|
7
7
|
import { slugify } from '../utils.js';
|
|
8
|
+
import { wrapSqliteError } from './helpers.js';
|
|
8
9
|
const T = PMO_TABLES;
|
|
9
10
|
export class SubtaskStorage {
|
|
10
11
|
ctx;
|
|
@@ -42,10 +43,15 @@ export class SubtaskStorage {
|
|
|
42
43
|
id = `${baseId}-${counter}`;
|
|
43
44
|
}
|
|
44
45
|
const position = maxPos.max_pos + 1;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
try {
|
|
47
|
+
this.ctx.db.prepare(`
|
|
48
|
+
INSERT INTO ${T.subtasks} (id, ticket_id, title, done, position)
|
|
49
|
+
VALUES (?, ?, ?, 0, ?)
|
|
50
|
+
`).run(id, ticketId, title, position);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
wrapSqliteError('Subtask', 'create', err);
|
|
54
|
+
}
|
|
49
55
|
// Update ticket timestamp
|
|
50
56
|
this.ctx.db.prepare(`
|
|
51
57
|
UPDATE ${T.tickets} SET updated_at = ? WHERE id = ?
|
|
@@ -139,10 +145,15 @@ export class AcceptanceCriteriaStorage {
|
|
|
139
145
|
// Use UUID to guarantee uniqueness even when multiple ACs are added in the same millisecond
|
|
140
146
|
const id = `ac-${randomUUID()}`;
|
|
141
147
|
const position = maxPos.max_pos + 1;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
try {
|
|
149
|
+
this.ctx.db.prepare(`
|
|
150
|
+
INSERT INTO ${T.ticket_acceptance_criteria} (id, ticket_id, criterion, verifiable, verified, position)
|
|
151
|
+
VALUES (?, ?, ?, 1, 0, ?)
|
|
152
|
+
`).run(id, ticketId, criterion, position);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
wrapSqliteError('Acceptance criterion', 'create', err);
|
|
156
|
+
}
|
|
146
157
|
this.ctx.db.prepare(`
|
|
147
158
|
UPDATE ${T.tickets} SET updated_at = ? WHERE id = ?
|
|
148
159
|
`).run(Date.now(), ticketId);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { PMO_TABLES } from '../schema.js';
|
|
7
7
|
import { PMOError } from '../types.js';
|
|
8
8
|
import { slugify, generateEntityId } from '../utils.js';
|
|
9
|
-
import { rowToTicket } from './helpers.js';
|
|
9
|
+
import { rowToTicket, wrapSqliteError } from './helpers.js';
|
|
10
10
|
const T = PMO_TABLES;
|
|
11
11
|
export class TicketStorage {
|
|
12
12
|
ctx;
|
|
@@ -126,14 +126,19 @@ export class TicketStorage {
|
|
|
126
126
|
}
|
|
127
127
|
// Insert ticket
|
|
128
128
|
const labels = ticket.labels || [];
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
try {
|
|
130
|
+
this.ctx.db.prepare(`
|
|
131
|
+
INSERT INTO ${T.tickets} (
|
|
132
|
+
id, project_id, title, description, priority, category,
|
|
133
|
+
status_id, owner, assignee, spec_id, epic_id, labels,
|
|
134
|
+
created_at, updated_at, last_synced_from_spec, last_synced_from_board
|
|
135
|
+
)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
137
|
+
`).run(id, projectId, title, ticket.description || null, ticket.priority || null, ticket.category || null, statusId, ticket.owner || null, ticket.assignee || null, specId, ticket.epicId || null, JSON.stringify(labels), now, now, ticket.lastSyncedFromSpec || null, ticket.lastSyncedFromBoard || null);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
wrapSqliteError('Ticket', 'create', err);
|
|
141
|
+
}
|
|
137
142
|
// Insert subtasks
|
|
138
143
|
if (ticket.subtasks && ticket.subtasks.length > 0) {
|
|
139
144
|
const insertSubtask = this.ctx.db.prepare(`
|
|
@@ -335,12 +340,19 @@ export class TicketStorage {
|
|
|
335
340
|
}
|
|
336
341
|
// Delete ticket (by ID only, since IDs are globally unique)
|
|
337
342
|
// Related data (subtasks, metadata) are deleted via CASCADE
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
try {
|
|
344
|
+
const result = this.ctx.db.prepare(`
|
|
345
|
+
DELETE FROM ${T.tickets}
|
|
346
|
+
WHERE id = ?
|
|
347
|
+
`).run(id);
|
|
348
|
+
if (result.changes === 0) {
|
|
349
|
+
throw new PMOError('NOT_FOUND', `Ticket not found: ${id}`, id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
if (err instanceof PMOError)
|
|
354
|
+
throw err;
|
|
355
|
+
wrapSqliteError('Ticket', 'delete', err);
|
|
344
356
|
}
|
|
345
357
|
// Update board timestamp for the ticket's project
|
|
346
358
|
this.updateProjectTimestamp(ticketProjectId);
|
package/dist/lib/pmo/utils.d.ts
CHANGED
|
@@ -75,14 +75,16 @@ export declare function deepClone<T>(obj: T): T;
|
|
|
75
75
|
/**
|
|
76
76
|
* Default column names for work commands (Linear-style workflow)
|
|
77
77
|
*
|
|
78
|
-
* Linear-style: Backlog → Planned → In Progress → Done
|
|
78
|
+
* Linear-style: Backlog → Planned → In Progress → Review → Done
|
|
79
79
|
* - planned: Move tickets here when scheduled/assigned
|
|
80
80
|
* - in_progress: Move tickets here when work starts
|
|
81
|
-
* -
|
|
81
|
+
* - review: Move tickets here when work is ready for review
|
|
82
|
+
* - done: Move tickets here when work is complete (reviewed/merged)
|
|
82
83
|
*/
|
|
83
84
|
export declare const DEFAULT_WORK_COLUMNS: {
|
|
84
85
|
readonly planned: "Planned";
|
|
85
86
|
readonly in_progress: "In Progress";
|
|
87
|
+
readonly review: "Review";
|
|
86
88
|
readonly done: "Done";
|
|
87
89
|
};
|
|
88
90
|
export type WorkColumnType = keyof typeof DEFAULT_WORK_COLUMNS;
|
package/dist/lib/pmo/utils.js
CHANGED
|
@@ -122,14 +122,16 @@ export function deepClone(obj) {
|
|
|
122
122
|
/**
|
|
123
123
|
* Default column names for work commands (Linear-style workflow)
|
|
124
124
|
*
|
|
125
|
-
* Linear-style: Backlog → Planned → In Progress → Done
|
|
125
|
+
* Linear-style: Backlog → Planned → In Progress → Review → Done
|
|
126
126
|
* - planned: Move tickets here when scheduled/assigned
|
|
127
127
|
* - in_progress: Move tickets here when work starts
|
|
128
|
-
* -
|
|
128
|
+
* - review: Move tickets here when work is ready for review
|
|
129
|
+
* - done: Move tickets here when work is complete (reviewed/merged)
|
|
129
130
|
*/
|
|
130
131
|
export const DEFAULT_WORK_COLUMNS = {
|
|
131
132
|
planned: 'Planned',
|
|
132
133
|
in_progress: 'In Progress',
|
|
134
|
+
review: 'Review',
|
|
133
135
|
done: 'Done',
|
|
134
136
|
};
|
|
135
137
|
/**
|