@proletariat/cli 0.3.34 → 0.3.36
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/auth.d.ts +15 -3
- package/dist/commands/agent/auth.js +136 -15
- package/dist/commands/agent/index.js +11 -2
- package/dist/commands/agent/list.js +16 -7
- package/dist/commands/agent/staff/add.d.ts +1 -0
- package/dist/commands/agent/staff/add.js +1 -0
- package/dist/commands/agent/staff/index.d.ts +15 -0
- package/dist/commands/agent/staff/index.js +83 -0
- package/dist/commands/agent/staff/list.d.ts +1 -0
- package/dist/commands/agent/staff/list.js +1 -0
- package/dist/commands/agent/staff/remove.d.ts +1 -0
- package/dist/commands/agent/staff/remove.js +1 -0
- package/dist/commands/agent/status.js +32 -4
- package/dist/commands/agent/themes/add-names.d.ts +1 -0
- package/dist/commands/agent/themes/add-names.js +1 -0
- package/dist/commands/agent/themes/create.d.ts +1 -0
- package/dist/commands/agent/themes/create.js +1 -0
- package/dist/commands/agent/themes/index.d.ts +10 -0
- package/dist/commands/agent/themes/index.js +144 -0
- package/dist/commands/agent/themes/list.d.ts +1 -0
- package/dist/commands/agent/themes/list.js +1 -0
- package/dist/commands/agent/themes/set.d.ts +1 -0
- package/dist/commands/agent/themes/set.js +1 -0
- package/dist/commands/agents/themes/add-names.d.ts +1 -0
- package/dist/commands/agents/themes/add-names.js +1 -0
- package/dist/commands/agents/themes/create.d.ts +1 -0
- package/dist/commands/agents/themes/create.js +1 -0
- package/dist/commands/agents/themes/list.d.ts +1 -0
- package/dist/commands/agents/themes/list.js +1 -0
- package/dist/commands/board/watch.js +6 -0
- package/dist/commands/branch/list.d.ts +1 -0
- package/dist/commands/branch/list.js +43 -12
- package/dist/commands/branch/where.js +3 -2
- package/dist/commands/category/list.d.ts +2 -1
- package/dist/commands/category/list.js +38 -13
- package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
- package/dist/commands/{claude.js → claude/index.js} +12 -12
- package/dist/commands/claude/open.d.ts +13 -0
- package/dist/commands/claude/open.js +175 -0
- package/dist/commands/diet.js +18 -2
- package/dist/commands/docker/logs.js +7 -3
- package/dist/commands/docker/shell.js +6 -0
- package/dist/commands/docker/start.js +20 -4
- package/dist/commands/docker/sync.d.ts +4 -0
- package/dist/commands/docker/sync.js +30 -2
- package/dist/commands/epic/show.d.ts +13 -0
- package/dist/commands/epic/show.js +16 -0
- package/dist/commands/epic/view.js +27 -0
- package/dist/commands/execution/config.d.ts +0 -4
- package/dist/commands/execution/config.js +10 -32
- package/dist/commands/execution/index.js +2 -1
- package/dist/commands/execution/logs.js +1 -1
- package/dist/commands/execution/stop.js +2 -1
- package/dist/commands/execution/view.js +22 -26
- package/dist/commands/init.js +2 -19
- package/dist/commands/label/create.d.ts +20 -0
- package/dist/commands/label/create.js +57 -0
- package/dist/commands/label/delete.d.ts +17 -0
- package/dist/commands/label/delete.js +32 -0
- package/dist/commands/label/group/create.d.ts +20 -0
- package/dist/commands/label/group/create.js +55 -0
- package/dist/commands/label/group/list.d.ts +14 -0
- package/dist/commands/label/group/list.js +52 -0
- package/dist/commands/label/index.d.ts +15 -0
- package/dist/commands/label/index.js +58 -0
- package/dist/commands/label/list.d.ts +16 -0
- package/dist/commands/label/list.js +83 -0
- package/dist/commands/link/list.js +3 -2
- package/dist/commands/mcp-server.js +27 -1
- package/dist/commands/phase/template/apply.d.ts +26 -0
- package/dist/commands/phase/template/apply.js +14 -0
- package/dist/commands/phase/template/create.d.ts +23 -0
- package/dist/commands/phase/template/create.js +14 -0
- package/dist/commands/phase/template/delete.d.ts +18 -0
- package/dist/commands/phase/template/delete.js +61 -0
- package/dist/commands/phase/template/list.d.ts +17 -0
- package/dist/commands/phase/template/list.js +89 -0
- package/dist/commands/phase/template/update.d.ts +1 -0
- package/dist/commands/phase/template/update.js +1 -0
- package/dist/commands/priority/add.js +1 -1
- package/dist/commands/project/create.js +3 -4
- package/dist/commands/project/update.js +5 -8
- package/dist/commands/pull.js +24 -0
- package/dist/commands/roadmap/generate.js +1 -2
- package/dist/commands/session/create.d.ts +19 -0
- package/dist/commands/session/create.js +102 -0
- package/dist/commands/session/health.js +2 -21
- package/dist/commands/session/index.js +14 -1
- package/dist/commands/session/list.js +26 -7
- package/dist/commands/session/peek.d.ts +38 -0
- package/dist/commands/session/peek.js +316 -0
- package/dist/commands/session/poke.d.ts +27 -0
- package/dist/commands/session/poke.js +219 -0
- package/dist/commands/spec/link/depends.d.ts +18 -0
- package/dist/commands/spec/link/depends.js +86 -0
- package/dist/commands/spec/link/index.d.ts +17 -0
- package/dist/commands/spec/link/index.js +92 -0
- package/dist/commands/spec/link/remove.d.ts +18 -0
- package/dist/commands/spec/link/remove.js +90 -0
- package/dist/commands/spec/view.js +29 -0
- package/dist/commands/support/logs.js +2 -2
- package/dist/commands/template/apply.js +5 -4
- package/dist/commands/template/create.js +1 -1
- package/dist/commands/template/list.js +2 -1
- package/dist/commands/theme/add-names.d.ts +4 -0
- package/dist/commands/theme/add-names.js +11 -1
- package/dist/commands/theme/create.d.ts +2 -0
- package/dist/commands/theme/create.js +8 -0
- package/dist/commands/ticket/bulk.js +2 -2
- package/dist/commands/ticket/complete.js +2 -2
- package/dist/commands/ticket/create.js +21 -0
- package/dist/commands/ticket/delete.js +8 -0
- package/dist/commands/ticket/edit.js +25 -0
- package/dist/commands/ticket/index.js +2 -2
- package/dist/commands/ticket/link/block.d.ts +15 -0
- package/dist/commands/ticket/link/block.js +95 -0
- package/dist/commands/ticket/link/index.d.ts +14 -0
- package/dist/commands/ticket/link/index.js +96 -0
- package/dist/commands/ticket/list.d.ts +1 -0
- package/dist/commands/ticket/list.js +6 -0
- package/dist/commands/ticket/move.js +25 -2
- package/dist/commands/ticket/resolve.js +4 -5
- package/dist/commands/ticket/show.d.ts +13 -0
- package/dist/commands/ticket/show.js +16 -0
- package/dist/commands/ticket/template/apply.d.ts +26 -0
- package/dist/commands/ticket/template/apply.js +14 -0
- package/dist/commands/ticket/template/delete.d.ts +18 -0
- package/dist/commands/ticket/template/delete.js +61 -0
- package/dist/commands/ticket/template/list.d.ts +17 -0
- package/dist/commands/ticket/template/list.js +78 -0
- package/dist/commands/ticket/template/save.d.ts +17 -0
- package/dist/commands/ticket/template/save.js +97 -0
- package/dist/commands/ticket/view.js +30 -0
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/ready.js +17 -0
- package/dist/commands/work/resolve.js +1 -1
- package/dist/commands/work/spawn.js +4 -4
- package/dist/commands/work/start.d.ts +1 -0
- package/dist/commands/work/start.js +203 -93
- package/dist/commands/work/status.d.ts +14 -0
- package/dist/commands/work/status.js +60 -0
- package/dist/commands/workflow/index.js +2 -1
- package/dist/commands/workflow/show.d.ts +13 -0
- package/dist/commands/workflow/show.js +16 -0
- package/dist/commands/workspace/add.js +15 -0
- package/dist/commands/workspace/list.js +2 -1
- package/dist/commands/workspace/prune.js +5 -5
- package/dist/lib/branch/index.d.ts +1 -0
- package/dist/lib/database/index.d.ts +1 -1
- package/dist/lib/database/index.js +20 -0
- package/dist/lib/execution/config.d.ts +15 -1
- package/dist/lib/execution/config.js +28 -0
- package/dist/lib/execution/devcontainer.js +3 -1
- package/dist/lib/execution/runners.d.ts +18 -2
- package/dist/lib/execution/runners.js +71 -29
- package/dist/lib/execution/session-utils.d.ts +11 -1
- package/dist/lib/execution/session-utils.js +26 -1
- package/dist/lib/execution/storage.d.ts +5 -0
- package/dist/lib/execution/storage.js +18 -3
- package/dist/lib/execution/types.d.ts +3 -0
- package/dist/lib/flags/resolver.js +1 -0
- package/dist/lib/mcp/helpers.d.ts +1 -2
- package/dist/lib/mcp/tools/board.js +4 -6
- package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
- package/dist/lib/mcp/tools/diet.js +1 -0
- package/dist/lib/mcp/tools/epic.js +8 -3
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/label.d.ts +6 -0
- package/dist/lib/mcp/tools/label.js +338 -0
- package/dist/lib/mcp/tools/spec.js +1 -1
- package/dist/lib/mcp/tools/ticket.js +57 -19
- package/dist/lib/mcp/tools/work.js +96 -6
- package/dist/lib/mcp/types.d.ts +10 -0
- package/dist/lib/multiline-input.js +8 -19
- package/dist/lib/pmo/base-command.d.ts +0 -1
- package/dist/lib/pmo/base-command.js +4 -5
- package/dist/lib/pmo/schema.d.ts +6 -0
- package/dist/lib/pmo/schema.js +44 -0
- package/dist/lib/pmo/storage/actions.js +1 -1
- package/dist/lib/pmo/storage/base.d.ts +6 -0
- package/dist/lib/pmo/storage/base.js +311 -52
- package/dist/lib/pmo/storage/index.d.ts +23 -1
- package/dist/lib/pmo/storage/index.js +59 -1
- package/dist/lib/pmo/storage/labels.d.ts +55 -0
- package/dist/lib/pmo/storage/labels.js +346 -0
- package/dist/lib/pmo/storage/tickets.js +17 -0
- package/dist/lib/pmo/storage/types.d.ts +25 -0
- package/dist/lib/pmo/types.d.ts +44 -0
- package/dist/lib/pmo/utils.js +1 -1
- package/dist/lib/prompt-command.d.ts +20 -0
- package/dist/lib/prompt-command.js +38 -2
- package/dist/lib/prompt-json.d.ts +36 -4
- package/dist/lib/prompt-json.js +129 -7
- package/dist/lib/styles.d.ts +37 -0
- package/dist/lib/styles.js +73 -0
- package/oclif.manifest.json +6399 -3799
- package/package.json +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label storage operations for PMO.
|
|
3
|
+
* Handles CRUD for labels, label groups, and ticket-label associations.
|
|
4
|
+
* Enforces group exclusivity constraints.
|
|
5
|
+
*/
|
|
6
|
+
import { PMO_TABLES } from '../schema.js';
|
|
7
|
+
import { PMOError } from '../types.js';
|
|
8
|
+
import { slugify } from '../utils.js';
|
|
9
|
+
const T = PMO_TABLES;
|
|
10
|
+
export class LabelStorage {
|
|
11
|
+
ctx;
|
|
12
|
+
constructor(ctx) {
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
}
|
|
15
|
+
// ===========================================================================
|
|
16
|
+
// Label Group Operations
|
|
17
|
+
// ===========================================================================
|
|
18
|
+
async listLabelGroups(filter) {
|
|
19
|
+
let query = `SELECT * FROM ${T.label_groups} WHERE 1=1`;
|
|
20
|
+
const params = [];
|
|
21
|
+
if (filter?.search) {
|
|
22
|
+
query += ' AND (name LIKE ? OR description LIKE ?)';
|
|
23
|
+
params.push(`%${filter.search}%`, `%${filter.search}%`);
|
|
24
|
+
}
|
|
25
|
+
query += ' ORDER BY position ASC, name ASC';
|
|
26
|
+
const rows = this.ctx.db.prepare(query).all(...params);
|
|
27
|
+
return rows.map(rowToLabelGroup);
|
|
28
|
+
}
|
|
29
|
+
async getLabelGroup(id) {
|
|
30
|
+
const row = this.ctx.db.prepare(`SELECT * FROM ${T.label_groups} WHERE id = ?`).get(id);
|
|
31
|
+
return row ? rowToLabelGroup(row) : null;
|
|
32
|
+
}
|
|
33
|
+
async getLabelGroupByName(name) {
|
|
34
|
+
const row = this.ctx.db.prepare(`SELECT * FROM ${T.label_groups} WHERE LOWER(name) = LOWER(?)`).get(name);
|
|
35
|
+
return row ? rowToLabelGroup(row) : null;
|
|
36
|
+
}
|
|
37
|
+
async createLabelGroup(group) {
|
|
38
|
+
const id = group.id || slugify(group.name);
|
|
39
|
+
// Check for duplicate name
|
|
40
|
+
const existing = await this.getLabelGroupByName(group.name);
|
|
41
|
+
if (existing) {
|
|
42
|
+
throw new PMOError('CONFLICT', `Label group "${group.name}" already exists`);
|
|
43
|
+
}
|
|
44
|
+
// Get next position
|
|
45
|
+
const maxPos = this.ctx.db.prepare(`SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.label_groups}`).get();
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
this.ctx.db.prepare(`
|
|
48
|
+
INSERT INTO ${T.label_groups} (id, name, description, is_exclusive, is_required, position, created_at)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
50
|
+
`).run(id, group.name, group.description || null, group.isExclusive !== undefined ? (group.isExclusive ? 1 : 0) : 1, group.isRequired !== undefined ? (group.isRequired ? 1 : 0) : 0, group.position ?? maxPos.max_pos + 1, now);
|
|
51
|
+
return (await this.getLabelGroup(id));
|
|
52
|
+
}
|
|
53
|
+
async updateLabelGroup(id, changes) {
|
|
54
|
+
const existing = await this.getLabelGroup(id);
|
|
55
|
+
if (!existing) {
|
|
56
|
+
throw new PMOError('NOT_FOUND', `Label group not found: ${id}`);
|
|
57
|
+
}
|
|
58
|
+
const updates = [];
|
|
59
|
+
const params = [];
|
|
60
|
+
if (changes.name !== undefined) {
|
|
61
|
+
updates.push('name = ?');
|
|
62
|
+
params.push(changes.name);
|
|
63
|
+
}
|
|
64
|
+
if (changes.description !== undefined) {
|
|
65
|
+
updates.push('description = ?');
|
|
66
|
+
params.push(changes.description);
|
|
67
|
+
}
|
|
68
|
+
if (changes.isExclusive !== undefined) {
|
|
69
|
+
updates.push('is_exclusive = ?');
|
|
70
|
+
params.push(changes.isExclusive ? 1 : 0);
|
|
71
|
+
}
|
|
72
|
+
if (changes.isRequired !== undefined) {
|
|
73
|
+
updates.push('is_required = ?');
|
|
74
|
+
params.push(changes.isRequired ? 1 : 0);
|
|
75
|
+
}
|
|
76
|
+
if (changes.position !== undefined) {
|
|
77
|
+
updates.push('position = ?');
|
|
78
|
+
params.push(changes.position);
|
|
79
|
+
}
|
|
80
|
+
if (updates.length > 0) {
|
|
81
|
+
params.push(id);
|
|
82
|
+
this.ctx.db.prepare(`UPDATE ${T.label_groups} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
83
|
+
}
|
|
84
|
+
return (await this.getLabelGroup(id));
|
|
85
|
+
}
|
|
86
|
+
async deleteLabelGroup(id) {
|
|
87
|
+
const existing = await this.getLabelGroup(id);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
throw new PMOError('NOT_FOUND', `Label group not found: ${id}`);
|
|
90
|
+
}
|
|
91
|
+
// Labels in this group will have group_id set to NULL (ON DELETE SET NULL)
|
|
92
|
+
this.ctx.db.prepare(`DELETE FROM ${T.label_groups} WHERE id = ?`).run(id);
|
|
93
|
+
}
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// Label Operations
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
async listLabels(filter) {
|
|
98
|
+
let query = `
|
|
99
|
+
SELECT l.*, lg.name as group_name
|
|
100
|
+
FROM ${T.labels} l
|
|
101
|
+
LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
|
|
102
|
+
WHERE 1=1
|
|
103
|
+
`;
|
|
104
|
+
const params = [];
|
|
105
|
+
if (filter?.groupId) {
|
|
106
|
+
query += ' AND l.group_id = ?';
|
|
107
|
+
params.push(filter.groupId);
|
|
108
|
+
}
|
|
109
|
+
if (filter?.search) {
|
|
110
|
+
query += ' AND (l.name LIKE ? OR l.description LIKE ?)';
|
|
111
|
+
params.push(`%${filter.search}%`, `%${filter.search}%`);
|
|
112
|
+
}
|
|
113
|
+
if (filter?.isBuiltin !== undefined) {
|
|
114
|
+
query += ' AND l.is_builtin = ?';
|
|
115
|
+
params.push(filter.isBuiltin ? 1 : 0);
|
|
116
|
+
}
|
|
117
|
+
query += ' ORDER BY lg.position ASC, l.position ASC, l.name ASC';
|
|
118
|
+
const rows = this.ctx.db.prepare(query).all(...params);
|
|
119
|
+
return rows.map(rowToLabel);
|
|
120
|
+
}
|
|
121
|
+
async getLabel(id) {
|
|
122
|
+
const row = this.ctx.db.prepare(`
|
|
123
|
+
SELECT l.*, lg.name as group_name
|
|
124
|
+
FROM ${T.labels} l
|
|
125
|
+
LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
|
|
126
|
+
WHERE l.id = ?
|
|
127
|
+
`).get(id);
|
|
128
|
+
return row ? rowToLabel(row) : null;
|
|
129
|
+
}
|
|
130
|
+
async getLabelByName(name, groupId) {
|
|
131
|
+
let query = `
|
|
132
|
+
SELECT l.*, lg.name as group_name
|
|
133
|
+
FROM ${T.labels} l
|
|
134
|
+
LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
|
|
135
|
+
WHERE LOWER(l.name) = LOWER(?)
|
|
136
|
+
`;
|
|
137
|
+
const params = [name];
|
|
138
|
+
if (groupId) {
|
|
139
|
+
query += ' AND l.group_id = ?';
|
|
140
|
+
params.push(groupId);
|
|
141
|
+
}
|
|
142
|
+
const row = this.ctx.db.prepare(query).get(...params);
|
|
143
|
+
return row ? rowToLabel(row) : null;
|
|
144
|
+
}
|
|
145
|
+
async createLabel(label) {
|
|
146
|
+
const id = label.id || slugify(label.name);
|
|
147
|
+
// Validate group if provided
|
|
148
|
+
if (label.groupId) {
|
|
149
|
+
const group = await this.getLabelGroup(label.groupId);
|
|
150
|
+
if (!group) {
|
|
151
|
+
throw new PMOError('NOT_FOUND', `Label group not found: ${label.groupId}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Get next position within group
|
|
155
|
+
let maxPos;
|
|
156
|
+
if (label.groupId) {
|
|
157
|
+
maxPos = this.ctx.db.prepare(`SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.labels} WHERE group_id = ?`).get(label.groupId);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
maxPos = this.ctx.db.prepare(`SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.labels} WHERE group_id IS NULL`).get();
|
|
161
|
+
}
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
try {
|
|
164
|
+
this.ctx.db.prepare(`
|
|
165
|
+
INSERT INTO ${T.labels} (id, name, color, description, group_id, position, is_builtin, created_at)
|
|
166
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
167
|
+
`).run(id, label.name, label.color || null, label.description || null, label.groupId || null, label.position ?? maxPos.max_pos + 1, label.isBuiltin ? 1 : 0, now);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (err instanceof Error && err.message.includes('UNIQUE constraint')) {
|
|
171
|
+
throw new PMOError('CONFLICT', `Label "${label.name}" already exists in this group`);
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
return (await this.getLabel(id));
|
|
176
|
+
}
|
|
177
|
+
async updateLabel(id, changes) {
|
|
178
|
+
const existing = await this.getLabel(id);
|
|
179
|
+
if (!existing) {
|
|
180
|
+
throw new PMOError('NOT_FOUND', `Label not found: ${id}`);
|
|
181
|
+
}
|
|
182
|
+
const updates = [];
|
|
183
|
+
const params = [];
|
|
184
|
+
if (changes.name !== undefined) {
|
|
185
|
+
updates.push('name = ?');
|
|
186
|
+
params.push(changes.name);
|
|
187
|
+
}
|
|
188
|
+
if (changes.color !== undefined) {
|
|
189
|
+
updates.push('color = ?');
|
|
190
|
+
params.push(changes.color);
|
|
191
|
+
}
|
|
192
|
+
if (changes.description !== undefined) {
|
|
193
|
+
updates.push('description = ?');
|
|
194
|
+
params.push(changes.description);
|
|
195
|
+
}
|
|
196
|
+
if (changes.groupId !== undefined) {
|
|
197
|
+
updates.push('group_id = ?');
|
|
198
|
+
params.push(changes.groupId);
|
|
199
|
+
}
|
|
200
|
+
if (changes.position !== undefined) {
|
|
201
|
+
updates.push('position = ?');
|
|
202
|
+
params.push(changes.position);
|
|
203
|
+
}
|
|
204
|
+
if (updates.length > 0) {
|
|
205
|
+
params.push(id);
|
|
206
|
+
this.ctx.db.prepare(`UPDATE ${T.labels} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
207
|
+
}
|
|
208
|
+
return (await this.getLabel(id));
|
|
209
|
+
}
|
|
210
|
+
async deleteLabel(id) {
|
|
211
|
+
const existing = await this.getLabel(id);
|
|
212
|
+
if (!existing) {
|
|
213
|
+
throw new PMOError('NOT_FOUND', `Label not found: ${id}`);
|
|
214
|
+
}
|
|
215
|
+
// ticket_labels rows cascade on delete
|
|
216
|
+
this.ctx.db.prepare(`DELETE FROM ${T.labels} WHERE id = ?`).run(id);
|
|
217
|
+
}
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
// Ticket-Label Association Operations
|
|
220
|
+
// ===========================================================================
|
|
221
|
+
/**
|
|
222
|
+
* Add a label to a ticket.
|
|
223
|
+
* Enforces group exclusivity: if the label belongs to an exclusive group,
|
|
224
|
+
* removes any existing label from the same group first.
|
|
225
|
+
*/
|
|
226
|
+
async addLabelToTicket(ticketId, labelId) {
|
|
227
|
+
// Validate ticket exists
|
|
228
|
+
const ticket = this.ctx.db.prepare(`SELECT id FROM ${T.tickets} WHERE id = ?`).get(ticketId);
|
|
229
|
+
if (!ticket) {
|
|
230
|
+
throw new PMOError('NOT_FOUND', `Ticket not found: ${ticketId}`);
|
|
231
|
+
}
|
|
232
|
+
// Get the label and its group info
|
|
233
|
+
const label = await this.getLabel(labelId);
|
|
234
|
+
if (!label) {
|
|
235
|
+
throw new PMOError('NOT_FOUND', `Label not found: ${labelId}`);
|
|
236
|
+
}
|
|
237
|
+
// If label belongs to an exclusive group, enforce exclusivity
|
|
238
|
+
if (label.groupId) {
|
|
239
|
+
const group = await this.getLabelGroup(label.groupId);
|
|
240
|
+
if (group?.isExclusive) {
|
|
241
|
+
// Remove any existing labels from this group on this ticket
|
|
242
|
+
this.ctx.db.prepare(`
|
|
243
|
+
DELETE FROM ${T.ticket_labels}
|
|
244
|
+
WHERE ticket_id = ? AND label_id IN (
|
|
245
|
+
SELECT id FROM ${T.labels} WHERE group_id = ?
|
|
246
|
+
)
|
|
247
|
+
`).run(ticketId, label.groupId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Add the label (ignore if already exists)
|
|
251
|
+
this.ctx.db.prepare(`
|
|
252
|
+
INSERT OR IGNORE INTO ${T.ticket_labels} (ticket_id, label_id)
|
|
253
|
+
VALUES (?, ?)
|
|
254
|
+
`).run(ticketId, labelId);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Remove a label from a ticket.
|
|
258
|
+
*/
|
|
259
|
+
async removeLabelFromTicket(ticketId, labelId) {
|
|
260
|
+
const result = this.ctx.db.prepare(`
|
|
261
|
+
DELETE FROM ${T.ticket_labels}
|
|
262
|
+
WHERE ticket_id = ? AND label_id = ?
|
|
263
|
+
`).run(ticketId, labelId);
|
|
264
|
+
if (result.changes === 0) {
|
|
265
|
+
throw new PMOError('NOT_FOUND', `Label ${labelId} is not applied to ticket ${ticketId}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get all labels for a ticket.
|
|
270
|
+
*/
|
|
271
|
+
async getLabelsForTicket(ticketId) {
|
|
272
|
+
const rows = this.ctx.db.prepare(`
|
|
273
|
+
SELECT l.*, lg.name as group_name
|
|
274
|
+
FROM ${T.ticket_labels} tl
|
|
275
|
+
JOIN ${T.labels} l ON tl.label_id = l.id
|
|
276
|
+
LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
|
|
277
|
+
WHERE tl.ticket_id = ?
|
|
278
|
+
ORDER BY lg.position ASC, l.position ASC
|
|
279
|
+
`).all(ticketId);
|
|
280
|
+
return rows.map(rowToLabel);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Add a label to a ticket by name.
|
|
284
|
+
* Resolves the label name to an ID first.
|
|
285
|
+
* If the label name matches a group:label pattern, resolves within that group.
|
|
286
|
+
*/
|
|
287
|
+
async addLabelToTicketByName(ticketId, labelName) {
|
|
288
|
+
const label = await this.resolveLabelByName(labelName);
|
|
289
|
+
if (!label) {
|
|
290
|
+
throw new PMOError('NOT_FOUND', `Label not found: "${labelName}"`);
|
|
291
|
+
}
|
|
292
|
+
return this.addLabelToTicket(ticketId, label.id);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Remove a label from a ticket by name.
|
|
296
|
+
*/
|
|
297
|
+
async removeLabelFromTicketByName(ticketId, labelName) {
|
|
298
|
+
const label = await this.resolveLabelByName(labelName);
|
|
299
|
+
if (!label) {
|
|
300
|
+
throw new PMOError('NOT_FOUND', `Label not found: "${labelName}"`);
|
|
301
|
+
}
|
|
302
|
+
return this.removeLabelFromTicket(ticketId, label.id);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Resolve a label by name or group:name pattern.
|
|
306
|
+
*/
|
|
307
|
+
async resolveLabelByName(nameOrPattern) {
|
|
308
|
+
// Check for group:label pattern
|
|
309
|
+
if (nameOrPattern.includes(':')) {
|
|
310
|
+
const [groupName, labelName] = nameOrPattern.split(':', 2);
|
|
311
|
+
const group = await this.getLabelGroupByName(groupName);
|
|
312
|
+
if (group) {
|
|
313
|
+
return this.getLabelByName(labelName, group.id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Try direct name lookup
|
|
317
|
+
return this.getLabelByName(nameOrPattern);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// ===========================================================================
|
|
321
|
+
// Row Converters
|
|
322
|
+
// ===========================================================================
|
|
323
|
+
function rowToLabelGroup(row) {
|
|
324
|
+
return {
|
|
325
|
+
id: row.id,
|
|
326
|
+
name: row.name,
|
|
327
|
+
description: row.description || undefined,
|
|
328
|
+
isExclusive: row.is_exclusive === 1,
|
|
329
|
+
isRequired: row.is_required === 1,
|
|
330
|
+
position: row.position,
|
|
331
|
+
createdAt: new Date(row.created_at),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function rowToLabel(row) {
|
|
335
|
+
return {
|
|
336
|
+
id: row.id,
|
|
337
|
+
name: row.name,
|
|
338
|
+
color: row.color || undefined,
|
|
339
|
+
description: row.description || undefined,
|
|
340
|
+
groupId: row.group_id || undefined,
|
|
341
|
+
groupName: row.group_name || undefined,
|
|
342
|
+
position: row.position,
|
|
343
|
+
isBuiltin: row.is_builtin === 1,
|
|
344
|
+
createdAt: new Date(row.created_at),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
@@ -576,6 +576,23 @@ export class TicketStorage {
|
|
|
576
576
|
query += ' AND ws.name = ?';
|
|
577
577
|
params.push(filter.column);
|
|
578
578
|
}
|
|
579
|
+
if (filter?.label) {
|
|
580
|
+
query += ` AND t.id IN (
|
|
581
|
+
SELECT tl.ticket_id FROM ${T.ticket_labels} tl
|
|
582
|
+
JOIN ${T.labels} l ON tl.label_id = l.id
|
|
583
|
+
WHERE LOWER(l.name) = LOWER(?)
|
|
584
|
+
)`;
|
|
585
|
+
params.push(filter.label);
|
|
586
|
+
}
|
|
587
|
+
if (filter?.labelGroup) {
|
|
588
|
+
query += ` AND t.id IN (
|
|
589
|
+
SELECT tl.ticket_id FROM ${T.ticket_labels} tl
|
|
590
|
+
JOIN ${T.labels} l ON tl.label_id = l.id
|
|
591
|
+
JOIN ${T.label_groups} lg ON l.group_id = lg.id
|
|
592
|
+
WHERE LOWER(lg.name) = LOWER(?)
|
|
593
|
+
)`;
|
|
594
|
+
params.push(filter.labelGroup);
|
|
595
|
+
}
|
|
579
596
|
// Order by status column position, then ticket position within status
|
|
580
597
|
if (projectIdOrName === undefined) {
|
|
581
598
|
query += ` ORDER BY p.name, ws.position, t.position ASC, t.created_at ASC`;
|
|
@@ -169,6 +169,7 @@ export interface WorkActionRow {
|
|
|
169
169
|
prompt: string;
|
|
170
170
|
end_prompt: string | null;
|
|
171
171
|
default_category: string | null;
|
|
172
|
+
modifies_code: number;
|
|
172
173
|
is_builtin: number;
|
|
173
174
|
position: number;
|
|
174
175
|
created_at: string;
|
|
@@ -226,3 +227,27 @@ export interface CategoryRow {
|
|
|
226
227
|
is_builtin: number;
|
|
227
228
|
created_at: string;
|
|
228
229
|
}
|
|
230
|
+
export interface LabelGroupRow {
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
description: string | null;
|
|
234
|
+
is_exclusive: number;
|
|
235
|
+
is_required: number;
|
|
236
|
+
position: number;
|
|
237
|
+
created_at: string;
|
|
238
|
+
}
|
|
239
|
+
export interface LabelRow {
|
|
240
|
+
id: string;
|
|
241
|
+
name: string;
|
|
242
|
+
color: string | null;
|
|
243
|
+
description: string | null;
|
|
244
|
+
group_id: string | null;
|
|
245
|
+
group_name?: string | null;
|
|
246
|
+
position: number;
|
|
247
|
+
is_builtin: number;
|
|
248
|
+
created_at: string;
|
|
249
|
+
}
|
|
250
|
+
export interface TicketLabelRow {
|
|
251
|
+
ticket_id: string;
|
|
252
|
+
label_id: string;
|
|
253
|
+
}
|
package/dist/lib/pmo/types.d.ts
CHANGED
|
@@ -248,6 +248,48 @@ export interface PhaseTemplatePhase {
|
|
|
248
248
|
description?: string;
|
|
249
249
|
isDefault?: boolean;
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Label group - groups related labels and optionally enforces mutual exclusivity.
|
|
253
|
+
* Example: "Function" group (ship/grow/support/bizops/strategy) is exclusive.
|
|
254
|
+
*/
|
|
255
|
+
export interface LabelGroup {
|
|
256
|
+
id: string;
|
|
257
|
+
name: string;
|
|
258
|
+
description?: string;
|
|
259
|
+
isExclusive: boolean;
|
|
260
|
+
isRequired: boolean;
|
|
261
|
+
position: number;
|
|
262
|
+
createdAt: Date;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Label - a tag that can be applied to tickets.
|
|
266
|
+
* Labels can optionally belong to a group.
|
|
267
|
+
*/
|
|
268
|
+
export interface Label {
|
|
269
|
+
id: string;
|
|
270
|
+
name: string;
|
|
271
|
+
color?: string;
|
|
272
|
+
description?: string;
|
|
273
|
+
groupId?: string;
|
|
274
|
+
groupName?: string;
|
|
275
|
+
position: number;
|
|
276
|
+
isBuiltin: boolean;
|
|
277
|
+
createdAt: Date;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Filter options for listing labels.
|
|
281
|
+
*/
|
|
282
|
+
export interface LabelFilter {
|
|
283
|
+
groupId?: string;
|
|
284
|
+
search?: string;
|
|
285
|
+
isBuiltin?: boolean;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Filter options for listing label groups.
|
|
289
|
+
*/
|
|
290
|
+
export interface LabelGroupFilter {
|
|
291
|
+
search?: string;
|
|
292
|
+
}
|
|
251
293
|
/**
|
|
252
294
|
* Roadmap - a curated collection of projects for documentation/visualization.
|
|
253
295
|
* Roadmaps group projects in a specific order for generating roadmap documents.
|
|
@@ -582,6 +624,8 @@ export interface TicketFilter {
|
|
|
582
624
|
column?: string;
|
|
583
625
|
projectId?: string;
|
|
584
626
|
allProjects?: boolean;
|
|
627
|
+
label?: string;
|
|
628
|
+
labelGroup?: string;
|
|
585
629
|
}
|
|
586
630
|
export interface SpecFilter {
|
|
587
631
|
status?: SpecStatus;
|
package/dist/lib/pmo/utils.js
CHANGED
|
@@ -30,6 +30,26 @@ import { type JsonFlags } from './prompt-json.js';
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export declare abstract class PromptCommand extends Command {
|
|
33
|
+
/**
|
|
34
|
+
* TTY-aware log method - strips ANSI codes and emoji in non-TTY mode.
|
|
35
|
+
*
|
|
36
|
+
* Use this instead of this.log() when outputting styled text (chalk colors, emoji prefixes).
|
|
37
|
+
* In TTY mode, outputs styled text as-is. In non-TTY mode, strips ANSI and emoji.
|
|
38
|
+
*
|
|
39
|
+
* @param message - The styled message (may contain ANSI codes and emoji)
|
|
40
|
+
* @param args - Additional arguments passed to this.log()
|
|
41
|
+
*/
|
|
42
|
+
protected logPlain(message?: string, ...args: string[]): void;
|
|
43
|
+
/**
|
|
44
|
+
* Check if plain output mode is active (non-TTY, PRLT_PLAIN, NO_COLOR).
|
|
45
|
+
* Convenience wrapper for use in commands.
|
|
46
|
+
*/
|
|
47
|
+
protected get isPlain(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Check if running in non-TTY environment.
|
|
50
|
+
* Convenience wrapper for use in commands.
|
|
51
|
+
*/
|
|
52
|
+
protected get isNonTTY(): boolean;
|
|
33
53
|
/**
|
|
34
54
|
* Prompt wrapper - drop-in replacement for inquirer.prompt
|
|
35
55
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import { isAgentMode, outputPromptAsJson, createMetadata, normalizeChoices, } from './prompt-json.js';
|
|
3
|
+
import { isAgentMode, isNonTTY, outputPromptAsJson, createMetadata, normalizeChoices, } from './prompt-json.js';
|
|
4
|
+
import { isPlainOutput, plainText } from './styles.js';
|
|
4
5
|
/**
|
|
5
6
|
* Lightweight base command with prompt() method for JSON mode support.
|
|
6
7
|
*
|
|
@@ -31,6 +32,41 @@ import { isAgentMode, outputPromptAsJson, createMetadata, normalizeChoices, } fr
|
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
34
|
export class PromptCommand extends Command {
|
|
35
|
+
/**
|
|
36
|
+
* TTY-aware log method - strips ANSI codes and emoji in non-TTY mode.
|
|
37
|
+
*
|
|
38
|
+
* Use this instead of this.log() when outputting styled text (chalk colors, emoji prefixes).
|
|
39
|
+
* In TTY mode, outputs styled text as-is. In non-TTY mode, strips ANSI and emoji.
|
|
40
|
+
*
|
|
41
|
+
* @param message - The styled message (may contain ANSI codes and emoji)
|
|
42
|
+
* @param args - Additional arguments passed to this.log()
|
|
43
|
+
*/
|
|
44
|
+
logPlain(message, ...args) {
|
|
45
|
+
if (message === undefined) {
|
|
46
|
+
this.log();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (isPlainOutput()) {
|
|
50
|
+
this.log(plainText(message), ...args);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.log(message, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if plain output mode is active (non-TTY, PRLT_PLAIN, NO_COLOR).
|
|
58
|
+
* Convenience wrapper for use in commands.
|
|
59
|
+
*/
|
|
60
|
+
get isPlain() {
|
|
61
|
+
return isPlainOutput();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if running in non-TTY environment.
|
|
65
|
+
* Convenience wrapper for use in commands.
|
|
66
|
+
*/
|
|
67
|
+
get isNonTTY() {
|
|
68
|
+
return isNonTTY();
|
|
69
|
+
}
|
|
34
70
|
/**
|
|
35
71
|
* Prompt wrapper - drop-in replacement for inquirer.prompt
|
|
36
72
|
*
|
|
@@ -73,7 +109,7 @@ export class PromptCommand extends Command {
|
|
|
73
109
|
*/
|
|
74
110
|
async prompt(questions, jsonModeConfig) {
|
|
75
111
|
// Auto-detect non-TTY: switch to JSON mode when no TTY present
|
|
76
|
-
if (!jsonModeConfig &&
|
|
112
|
+
if (!jsonModeConfig && isNonTTY()) {
|
|
77
113
|
jsonModeConfig = { flags: { json: true }, commandName: this.id ?? 'unknown' };
|
|
78
114
|
}
|
|
79
115
|
// Check for JSON/agent mode
|
|
@@ -193,6 +193,27 @@ export interface ExecutionResultJsonOutput {
|
|
|
193
193
|
* Union type for all JSON output types
|
|
194
194
|
*/
|
|
195
195
|
export type JsonOutput = PromptJsonOutput | SuccessJsonOutput | ErrorJsonOutput | DryRunJsonOutput | ConfirmationNeededJsonOutput | ExecutionResultJsonOutput;
|
|
196
|
+
/**
|
|
197
|
+
* All valid JSON envelope type discriminators.
|
|
198
|
+
* Used for contract tests and schema validation.
|
|
199
|
+
*/
|
|
200
|
+
export declare const JSON_ENVELOPE_TYPES: readonly ["prompt", "success", "error", "dry-run", "confirmation_needed", "execution_result"];
|
|
201
|
+
export type JsonEnvelopeType = typeof JSON_ENVELOPE_TYPES[number];
|
|
202
|
+
/**
|
|
203
|
+
* Required fields per envelope type for contract validation.
|
|
204
|
+
* Tests use this to verify no fields are accidentally removed.
|
|
205
|
+
*/
|
|
206
|
+
export declare const JSON_ENVELOPE_REQUIRED_FIELDS: Record<JsonEnvelopeType, string[]>;
|
|
207
|
+
/**
|
|
208
|
+
* Validate that a parsed JSON object conforms to the machine-mode envelope schema.
|
|
209
|
+
*
|
|
210
|
+
* Returns an array of validation errors (empty = valid).
|
|
211
|
+
* Useful for contract tests and runtime validation of JSON output.
|
|
212
|
+
*
|
|
213
|
+
* @param obj - Parsed JSON object to validate
|
|
214
|
+
* @returns Array of validation error strings (empty if valid)
|
|
215
|
+
*/
|
|
216
|
+
export declare function validateJsonEnvelope(obj: unknown): string[];
|
|
196
217
|
/**
|
|
197
218
|
* Flags interface for JSON mode detection
|
|
198
219
|
*/
|
|
@@ -211,9 +232,18 @@ export interface MachineOutputFlags {
|
|
|
211
232
|
machine?: boolean;
|
|
212
233
|
}
|
|
213
234
|
/**
|
|
214
|
-
* Check if the current environment is non-TTY (piped output)
|
|
235
|
+
* Check if the current environment is non-TTY (piped input or output)
|
|
236
|
+
*
|
|
237
|
+
* Uses the "either" strategy: returns true if EITHER stdin OR stdout is non-TTY.
|
|
238
|
+
* This covers the primary use case of scripts/agents calling prlt as a subprocess,
|
|
239
|
+
* where both stdin and stdout are typically non-TTY.
|
|
240
|
+
*
|
|
241
|
+
* Returns true if:
|
|
242
|
+
* - stdin is not a TTY (e.g., piped input)
|
|
243
|
+
* - stdout is not a TTY (e.g., piped output)
|
|
244
|
+
* - PRLT_JSON=1 environment variable is set (overrides TTY detection)
|
|
215
245
|
*
|
|
216
|
-
* @returns true if stdout is not a TTY
|
|
246
|
+
* @returns true if either stdin or stdout is not a TTY, or PRLT_JSON=1 is set
|
|
217
247
|
*/
|
|
218
248
|
export declare function isNonTTY(): boolean;
|
|
219
249
|
/**
|
|
@@ -221,7 +251,8 @@ export declare function isNonTTY(): boolean;
|
|
|
221
251
|
*
|
|
222
252
|
* Returns true if:
|
|
223
253
|
* - The --json flag is set (or -m/--machine aliases)
|
|
224
|
-
* - The environment is
|
|
254
|
+
* - The PRLT_JSON=1 environment variable is set
|
|
255
|
+
* - Either stdin or stdout is non-TTY (piped input/output)
|
|
225
256
|
*
|
|
226
257
|
* @param flags - Command flags object
|
|
227
258
|
* @returns true if JSON mode should be used
|
|
@@ -236,7 +267,8 @@ export declare const isAgentMode: typeof shouldOutputJson;
|
|
|
236
267
|
*
|
|
237
268
|
* Returns true if:
|
|
238
269
|
* - The --json flag is set (or -m/--machine aliases)
|
|
239
|
-
* - The environment is
|
|
270
|
+
* - The PRLT_JSON=1 environment variable is set
|
|
271
|
+
* - Either stdin or stdout is non-TTY (piped input/output)
|
|
240
272
|
*
|
|
241
273
|
* @param flags - Command flags object
|
|
242
274
|
* @returns true if machine-readable output mode should be used
|