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