@hienlh/ppm 0.10.5 → 0.11.1
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/CHANGELOG.md +35 -0
- package/dist/web/assets/ai-settings-section-D2vqiydT.js +1 -0
- package/dist/web/assets/{api-settings-C__hxGX2.js → api-settings-2eTz4SgY.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-BRW4VwMk.js +1 -0
- package/dist/web/assets/chat-tab-DYf6U6UF.js +10 -0
- package/dist/web/assets/code-editor-BPxBeu0S.js +8 -0
- package/dist/web/assets/{conflict-editor-Bxq4QiW1.js → conflict-editor-BCkYHDUy.js} +1 -1
- package/dist/web/assets/{csv-preview-BizIVMyb.js → csv-preview-D37K2LRd.js} +1 -1
- package/dist/web/assets/{database-viewer-CvQc1PZH.js → database-viewer-CCe8qa1Q.js} +2 -2
- package/dist/web/assets/{diff-viewer-x7kjfVYW.js → diff-viewer-DIjzWvaG.js} +1 -1
- package/dist/web/assets/{esm-K1XIK4vc.js → esm-B99v94EE.js} +1 -1
- package/dist/web/assets/{extension-store-3yZYn07W.js → extension-store-CkyOvGbF.js} +1 -1
- package/dist/web/assets/extension-webview-HY8XueLo.js +3 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bt68dqWT.js +1 -0
- package/dist/web/assets/index-DpRxWGjM.js +26 -0
- package/dist/web/assets/index-iZHWllzQ.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-ySD5z855.js +1 -0
- package/dist/web/assets/{input-ClhO__YM.js → input-CHRMley8.js} +1 -1
- package/dist/web/assets/{keybindings-store-C9KsBH7z.js → keybindings-store-CpP5_miA.js} +1 -1
- package/dist/web/assets/keybindings-store-qfYScgY0.js +1 -0
- package/dist/web/assets/{markdown-renderer-CKmmrUuy.js → markdown-renderer-BQV0AIm5.js} +3 -3
- package/dist/web/assets/packet-RMMSAZCW-CLxaXgIf.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-C9wPZfkn.js +1 -0
- package/dist/web/assets/port-forwarding-tab-DPmTpfFX.js +1 -0
- package/dist/web/assets/{postgres-viewer-YkljtDWX.js → postgres-viewer-BUSNt_7x.js} +3 -3
- package/dist/web/assets/{project-store-BYmQ0fDC.js → project-store-CczGNZyf.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DxEpzVN_.js +1 -0
- package/dist/web/assets/{scroll-area-DW7L4Gnc.js → scroll-area-DwWF9FpN.js} +1 -1
- package/dist/web/assets/settings-store-CuYjM0FF.js +2 -0
- package/dist/web/assets/settings-tab-DHBG5O0C.js +1 -0
- package/dist/web/assets/{sql-query-editor-CM_qEhaX.js → sql-query-editor-CVEi0jLM.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-f6ZJHIzh.js → sqlite-viewer-B7WnFN29.js} +1 -1
- package/dist/web/assets/{tab-store-B3M9hjho.js → tab-store-Jvy1eZGM.js} +1 -1
- package/dist/web/assets/{terminal-tab-CVdfvDSK.js → terminal-tab-1K4ijyNe.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-yelcZZqO.js +1 -0
- package/dist/web/assets/{use-monaco-theme-CvV5vy_F.js → use-monaco-theme-kjiAwvOp.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-CwOSbfhN.js → vendor-mermaid-CylkVm4U.js} +3 -3
- package/dist/web/index.html +16 -16
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +29 -5
- package/docs/project-changelog.md +31 -1
- package/docs/system-architecture.md +106 -1
- package/package.json +1 -1
- package/packages/ext-git-graph/src/webview-html.ts +8 -7
- package/src/cli/commands/jira-cmd.ts +92 -0
- package/src/cli/commands/jira-watcher-cmd.ts +149 -0
- package/src/index.ts +3 -0
- package/src/server/index.ts +19 -0
- package/src/server/routes/files.ts +15 -0
- package/src/server/routes/fs-browse.ts +40 -1
- package/src/server/routes/jira-config-routes.ts +74 -0
- package/src/server/routes/jira-watcher-routes.ts +316 -0
- package/src/server/routes/jira.ts +7 -0
- package/src/server/ws/chat.ts +21 -0
- package/src/services/db.service.ts +65 -1
- package/src/services/file.service.ts +42 -0
- package/src/services/jira-api-client.ts +216 -0
- package/src/services/jira-config.service.ts +83 -0
- package/src/services/jira-debug-session.service.ts +240 -0
- package/src/services/jira-watcher-db.service.ts +195 -0
- package/src/services/jira-watcher.service.ts +159 -0
- package/src/services/notification.service.ts +6 -0
- package/src/types/jira.ts +128 -0
- package/src/web/app.tsx +15 -12
- package/src/web/components/chat/chat-tab.tsx +32 -1
- package/src/web/components/chat/message-input.tsx +56 -5
- package/src/web/components/explorer/file-tree.tsx +70 -19
- package/src/web/components/extensions/extension-webview.tsx +24 -10
- package/src/web/components/jira/jira-config-form.tsx +109 -0
- package/src/web/components/jira/jira-debug-prompt-dialog.tsx +58 -0
- package/src/web/components/jira/jira-filter-builder.tsx +197 -0
- package/src/web/components/jira/jira-panel.tsx +201 -0
- package/src/web/components/jira/jira-results-panel.tsx +184 -0
- package/src/web/components/jira/jira-settings-section.tsx +58 -0
- package/src/web/components/jira/jira-status-badge.tsx +18 -0
- package/src/web/components/jira/jira-ticket-card.tsx +144 -0
- package/src/web/components/jira/jira-ticket-detail.tsx +153 -0
- package/src/web/components/jira/jira-watcher-form.tsx +154 -0
- package/src/web/components/jira/jira-watcher-list.tsx +98 -0
- package/src/web/components/layout/mobile-drawer.tsx +18 -5
- package/src/web/components/layout/sidebar.tsx +20 -3
- package/src/web/components/settings/settings-tab.tsx +20 -3
- package/src/web/components/shared/markdown-code-block.tsx +5 -3
- package/src/web/components/ui/file-browser-picker.tsx +88 -1
- package/src/web/hooks/use-chat.ts +6 -0
- package/src/web/lib/ws-client.ts +10 -3
- package/src/web/stores/jira-store.ts +198 -0
- package/src/web/stores/settings-store.ts +17 -2
- package/src/web/styles/globals.css +7 -0
- package/vite.config.ts +5 -66
- package/bun.lock +0 -2062
- package/bunfig.toml +0 -2
- package/dist/web/assets/ai-settings-section-D2rONDPd.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DmL1WyG-.js +0 -1
- package/dist/web/assets/chat-tab-Dki1pz84.js +0 -10
- package/dist/web/assets/code-editor-D3AAT8nI.js +0 -8
- package/dist/web/assets/extension-webview-BFd0USXC.js +0 -3
- package/dist/web/assets/gitGraph-HDMCJU4V-D8vKfkjC.js +0 -1
- package/dist/web/assets/index-DPnjO2FY.css +0 -2
- package/dist/web/assets/index-DuEUN2Eg.js +0 -26
- package/dist/web/assets/info-3K5VOQVL-VG29MIoT.js +0 -1
- package/dist/web/assets/keybindings-store-BkZjvU9J.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-Bl_WpvPc.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BVpLpAIy.js +0 -1
- package/dist/web/assets/port-forwarding-tab-BUH9aImG.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CJGco43I.js +0 -1
- package/dist/web/assets/settings-store-D9CflsKU.js +0 -2
- package/dist/web/assets/settings-tab-DfPjX9uY.js +0 -1
- package/dist/web/assets/square-nsMa3iMk.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-BsOrObtE.js +0 -1
- /package/dist/web/assets/{api-client-Bn-Pi9k5.js → api-client-C3tXCh0r.js} +0 -0
- /package/dist/web/assets/{csv-parser--2WJNgS7.js → csv-parser-BAa56Nnn.js} +0 -0
- /package/dist/web/assets/{dist-im4ynINo.js → dist-On3hz9_g.js} +0 -0
- /package/dist/web/assets/{katex-CKoArbIw.js → katex-Bbu770d9.js} +0 -0
- /package/dist/web/assets/{lib-D_kRA9p6.js → lib-BqkcKGFq.js} +0 -0
- /package/dist/web/assets/{react-GqWghJ-L.js → react-BkWDCPD7.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-C3cq9j99.js → sql-completion-provider-D3acAhav.js} +0 -0
- /package/dist/web/assets/{table-Dq575bPF.js → table-DbSviOmw.js} +0 -0
- /package/dist/web/assets/{text-wrap-Cn6BNQfq.js → text-wrap-DzvCTq_i.js} +0 -0
- /package/dist/web/assets/{trash-2-CJYoLw7Q.js → trash-2-BgDIBl6f.js} +0 -0
- /package/dist/web/assets/{utils-CTg5uAYR.js → utils-ChWX7pZv.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-ejLe7-tK.js → vendor-xterm-B9BUAFKA.js} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
const C = {
|
|
4
|
+
reset: "\x1b[0m", green: "\x1b[32m", red: "\x1b[31m",
|
|
5
|
+
yellow: "\x1b[33m", cyan: "\x1b[36m", dim: "\x1b[2m",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function registerJiraWatcherCommands(jira: Command): void {
|
|
9
|
+
const watch = jira.command("watch").description("Manage Jira watchers");
|
|
10
|
+
|
|
11
|
+
watch.command("add")
|
|
12
|
+
.description("Create a new watcher")
|
|
13
|
+
.requiredOption("--config <id>", "Jira config ID")
|
|
14
|
+
.requiredOption("--name <name>", "Watcher name")
|
|
15
|
+
.requiredOption("--jql <jql>", "JQL filter query")
|
|
16
|
+
.option("--interval <ms>", "Poll interval in ms", "120000")
|
|
17
|
+
.option("--prompt <template>", "Custom prompt template")
|
|
18
|
+
.option("--mode <mode>", "debug or notify", "debug")
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
try {
|
|
21
|
+
const { createWatcher } = await import("../../services/jira-watcher-db.service.ts");
|
|
22
|
+
const w = createWatcher(Number(opts.config), opts.name, opts.jql, {
|
|
23
|
+
intervalMs: Number(opts.interval), promptTemplate: opts.prompt, mode: opts.mode,
|
|
24
|
+
});
|
|
25
|
+
console.log(`${C.green}✓${C.reset} Watcher created (id: ${w.id})`);
|
|
26
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
watch.command("list")
|
|
30
|
+
.description("List watchers")
|
|
31
|
+
.option("--config <id>", "Filter by config ID")
|
|
32
|
+
.action(async (opts) => {
|
|
33
|
+
try {
|
|
34
|
+
const { getWatchersByConfigId } = await import("../../services/jira-watcher-db.service.ts");
|
|
35
|
+
const { getAllEnabledWatchers } = await import("../../services/jira-watcher-db.service.ts");
|
|
36
|
+
const list = opts.config
|
|
37
|
+
? getWatchersByConfigId(Number(opts.config))
|
|
38
|
+
: getAllEnabledWatchers().map((w) => ({ id: w.id, name: w.name, jql: w.jql, mode: w.mode, intervalMs: w.interval_ms, enabled: w.enabled === 1 }));
|
|
39
|
+
if (!list.length) { console.log("No watchers found."); return; }
|
|
40
|
+
console.table(list);
|
|
41
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
watch.command("enable <id>").description("Enable watcher").action(async (id) => {
|
|
45
|
+
try {
|
|
46
|
+
const { updateWatcher } = await import("../../services/jira-watcher-db.service.ts");
|
|
47
|
+
updateWatcher(Number(id), { enabled: true });
|
|
48
|
+
console.log(`${C.green}✓${C.reset} Watcher ${id} enabled`);
|
|
49
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
watch.command("disable <id>").description("Disable watcher").action(async (id) => {
|
|
53
|
+
try {
|
|
54
|
+
const { updateWatcher } = await import("../../services/jira-watcher-db.service.ts");
|
|
55
|
+
updateWatcher(Number(id), { enabled: false });
|
|
56
|
+
console.log(`${C.green}✓${C.reset} Watcher ${id} disabled`);
|
|
57
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
watch.command("remove <id>").description("Delete watcher").action(async (id) => {
|
|
61
|
+
try {
|
|
62
|
+
const { deleteWatcher } = await import("../../services/jira-watcher-db.service.ts");
|
|
63
|
+
deleteWatcher(Number(id));
|
|
64
|
+
console.log(`${C.green}✓${C.reset} Watcher ${id} deleted`);
|
|
65
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
watch.command("test <id>").description("Dry-run poll (show matches without creating tasks)")
|
|
69
|
+
.action(async (id) => {
|
|
70
|
+
try {
|
|
71
|
+
const { getDb } = await import("../../services/db.service.ts");
|
|
72
|
+
const w = getDb().query("SELECT * FROM jira_watchers WHERE id = ?").get(Number(id)) as any;
|
|
73
|
+
if (!w) { console.error(`${C.red}✗${C.reset} Watcher not found`); process.exit(1); }
|
|
74
|
+
const { getDecryptedCredentials } = await import("../../services/jira-config.service.ts");
|
|
75
|
+
const creds = getDecryptedCredentials(w.jira_config_id);
|
|
76
|
+
if (!creds) { console.error(`${C.red}✗${C.reset} No credentials`); process.exit(1); }
|
|
77
|
+
const { searchIssues } = await import("../../services/jira-api-client.ts");
|
|
78
|
+
const res = await searchIssues(creds, w.jql);
|
|
79
|
+
console.log(`Found ${res.total} issues (showing ${res.issues.length}):`);
|
|
80
|
+
for (const i of res.issues) console.log(` ${C.cyan}${i.key}${C.reset} ${i.fields.summary}`);
|
|
81
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
watch.command("pull [id]").description("Manual pull (one watcher or all enabled)")
|
|
85
|
+
.action(async (id?: string) => {
|
|
86
|
+
try {
|
|
87
|
+
const { jiraWatcherService } = await import("../../services/jira-watcher.service.ts");
|
|
88
|
+
if (id) {
|
|
89
|
+
const count = await jiraWatcherService.pollWatcher(Number(id));
|
|
90
|
+
console.log(`${C.green}✓${C.reset} Pulled ${count} new issue(s)`);
|
|
91
|
+
} else {
|
|
92
|
+
const { getAllEnabledWatchers } = await import("../../services/jira-watcher-db.service.ts");
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const w of getAllEnabledWatchers()) {
|
|
95
|
+
try { total += await jiraWatcherService.pollWatcher(w.id); } catch {}
|
|
96
|
+
}
|
|
97
|
+
console.log(`${C.green}✓${C.reset} Pulled ${total} new issue(s) total`);
|
|
98
|
+
}
|
|
99
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── Results ───────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const results = jira.command("results").description("View Jira watch results");
|
|
105
|
+
results
|
|
106
|
+
.option("--watcher <id>", "Filter by watcher ID")
|
|
107
|
+
.option("--status <status>", "Filter by status")
|
|
108
|
+
.action(async (opts) => {
|
|
109
|
+
try {
|
|
110
|
+
const { getResultsByWatcherId } = await import("../../services/jira-watcher-db.service.ts");
|
|
111
|
+
const list = getResultsByWatcherId(
|
|
112
|
+
opts.watcher ? Number(opts.watcher) : undefined,
|
|
113
|
+
{ status: opts.status },
|
|
114
|
+
);
|
|
115
|
+
if (!list.length) { console.log("No results."); return; }
|
|
116
|
+
console.table(list.map((r) => ({
|
|
117
|
+
id: r.id, key: r.issueKey, status: r.status,
|
|
118
|
+
summary: (r.issueSummary ?? "").slice(0, 50), source: r.source,
|
|
119
|
+
})));
|
|
120
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
results.command("delete <id>").description("Soft-delete result").action(async (id) => {
|
|
124
|
+
try {
|
|
125
|
+
const { softDeleteResult } = await import("../../services/jira-watcher-db.service.ts");
|
|
126
|
+
softDeleteResult(Number(id));
|
|
127
|
+
console.log(`${C.green}✓${C.reset} Result ${id} deleted`);
|
|
128
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Ticket ────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
jira.command("track <issueKey>")
|
|
134
|
+
.description("Manually track a Jira issue")
|
|
135
|
+
.requiredOption("--config <id>", "Jira config ID")
|
|
136
|
+
.action(async (issueKey, opts) => {
|
|
137
|
+
try {
|
|
138
|
+
const { getDecryptedCredentials } = await import("../../services/jira-config.service.ts");
|
|
139
|
+
const creds = getDecryptedCredentials(Number(opts.config));
|
|
140
|
+
if (!creds) { console.error(`${C.red}✗${C.reset} Invalid config`); process.exit(1); }
|
|
141
|
+
const { getIssue } = await import("../../services/jira-api-client.ts");
|
|
142
|
+
const issue = await getIssue(creds, issueKey);
|
|
143
|
+
const { insertResult } = await import("../../services/jira-watcher-db.service.ts");
|
|
144
|
+
const { inserted } = insertResult(null, issue.key, issue.fields.summary, issue.fields.updated, "manual");
|
|
145
|
+
if (!inserted) { console.log(`${C.yellow}Already tracked${C.reset}`); return; }
|
|
146
|
+
console.log(`${C.green}✓${C.reset} Tracking ${issue.key}: ${issue.fields.summary}`);
|
|
147
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
148
|
+
});
|
|
149
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -157,4 +157,7 @@ registerDbCommands(program);
|
|
|
157
157
|
const { registerBotCommands } = await import("./cli/commands/bot-cmd.ts");
|
|
158
158
|
registerBotCommands(program);
|
|
159
159
|
|
|
160
|
+
const { registerJiraCommands } = await import("./cli/commands/jira-cmd.ts");
|
|
161
|
+
await registerJiraCommands(program);
|
|
162
|
+
|
|
160
163
|
program.parse();
|
package/src/server/index.ts
CHANGED
|
@@ -150,6 +150,10 @@ app.route("/api/postgres", postgresRoutes);
|
|
|
150
150
|
app.route("/api/db", databaseRoutes);
|
|
151
151
|
app.route("/api/accounts", accountsRoutes);
|
|
152
152
|
|
|
153
|
+
// Jira watcher
|
|
154
|
+
import { jiraRoutes } from "./routes/jira.ts";
|
|
155
|
+
app.route("/api/jira", jiraRoutes);
|
|
156
|
+
|
|
153
157
|
// Agent Teams management
|
|
154
158
|
import { teamRoutes } from "./routes/teams.ts";
|
|
155
159
|
app.route("/api/teams", teamRoutes);
|
|
@@ -650,5 +654,20 @@ if (process.argv.includes("__serve__")) {
|
|
|
650
654
|
console.error("[ppmbot] Startup error:", e);
|
|
651
655
|
});
|
|
652
656
|
|
|
657
|
+
// Start Jira watchers (non-blocking, cleanup on exit)
|
|
658
|
+
import("../services/jira-watcher.service.ts")
|
|
659
|
+
.then(({ jiraWatcherService }) => {
|
|
660
|
+
// Reset zombie debug sessions from previous server run
|
|
661
|
+
import("../services/jira-debug-session.service.ts").then(({ jiraDebugService }) => {
|
|
662
|
+
jiraDebugService.init();
|
|
663
|
+
}).catch(() => {});
|
|
664
|
+
jiraWatcherService.startAll().catch((e) => {
|
|
665
|
+
console.error("[jira] Failed to start watchers:", (e as Error).message);
|
|
666
|
+
});
|
|
667
|
+
process.on("SIGTERM", () => jiraWatcherService.stopAll());
|
|
668
|
+
process.on("SIGINT", () => jiraWatcherService.stopAll());
|
|
669
|
+
})
|
|
670
|
+
.catch(() => {});
|
|
671
|
+
|
|
653
672
|
console.log(`Server child ready on port ${port}`);
|
|
654
673
|
}
|
|
@@ -160,6 +160,21 @@ function globToPathRegex(glob: string): RegExp {
|
|
|
160
160
|
return new RegExp(re);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** GET /files/resolve?name=filename — resolve filename to project path(s) */
|
|
164
|
+
fileRoutes.get("/resolve", (c) => {
|
|
165
|
+
try {
|
|
166
|
+
const projectPath = c.get("projectPath");
|
|
167
|
+
const name = c.req.query("name");
|
|
168
|
+
if (!name || name.includes("/") || name.includes("\\")) {
|
|
169
|
+
return c.json(err("Invalid filename"), 400);
|
|
170
|
+
}
|
|
171
|
+
const matches = fileService.resolveFilename(projectPath, name);
|
|
172
|
+
return c.json(ok({ matches }));
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return c.json(err((e as Error).message), errorStatus(e));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
163
178
|
/** GET /files/search?q=...&caseSensitive=false — search file content with grep */
|
|
164
179
|
fileRoutes.get("/search", async (c) => {
|
|
165
180
|
const projectPath = c.get("projectPath");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { existsSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, statSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { $ } from "bun";
|
|
3
5
|
import {
|
|
4
6
|
browse,
|
|
5
7
|
list,
|
|
@@ -73,6 +75,43 @@ fsBrowseRoutes.get("/raw", (c) => {
|
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
|
|
78
|
+
/** DELETE /api/fs/rmdir — delete a directory { path } */
|
|
79
|
+
fsBrowseRoutes.delete("/rmdir", async (c) => {
|
|
80
|
+
try {
|
|
81
|
+
const body = await c.req.json<{ path: string }>();
|
|
82
|
+
if (!body.path) return c.json(err("path is required"), 400);
|
|
83
|
+
|
|
84
|
+
const abs = resolve(body.path);
|
|
85
|
+
if (!existsSync(abs)) return c.json(err("Directory not found"), 404);
|
|
86
|
+
if (!statSync(abs).isDirectory()) return c.json(err("Path is not a directory"), 400);
|
|
87
|
+
|
|
88
|
+
rmSync(abs, { recursive: true });
|
|
89
|
+
return c.json(ok({ removed: abs }));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return c.json(err((e as Error).message), errorStatus(e));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** POST /api/fs/mkdir — create directory and git init { path } */
|
|
96
|
+
fsBrowseRoutes.post("/mkdir", async (c) => {
|
|
97
|
+
try {
|
|
98
|
+
const body = await c.req.json<{ path: string }>();
|
|
99
|
+
if (!body.path) return c.json(err("path is required"), 400);
|
|
100
|
+
|
|
101
|
+
const abs = resolve(body.path);
|
|
102
|
+
if (existsSync(abs)) {
|
|
103
|
+
return c.json(err(`Directory already exists: ${abs}`), 400);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
mkdirSync(abs, { recursive: true });
|
|
107
|
+
// git init in the new directory
|
|
108
|
+
await $`git init ${abs}`.quiet();
|
|
109
|
+
return c.json(ok({ path: abs, gitInitialized: true }), 201);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
return c.json(err((e as Error).message), errorStatus(e));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
76
115
|
/** PUT /api/fs/write — write file outside project { path, content } */
|
|
77
116
|
fsBrowseRoutes.put("/write", async (c) => {
|
|
78
117
|
try {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
getAllConfigs, getConfigByProjectId,
|
|
4
|
+
upsertConfig, deleteConfig, getDecryptedCredentials,
|
|
5
|
+
} from "../../services/jira-config.service.ts";
|
|
6
|
+
import { getProjects } from "../../services/db.service.ts";
|
|
7
|
+
import { testConnection } from "../../services/jira-api-client.ts";
|
|
8
|
+
import { ok, err } from "../../types/api.ts";
|
|
9
|
+
|
|
10
|
+
export const jiraConfigRoutes = new Hono();
|
|
11
|
+
|
|
12
|
+
/** GET /projects — list projects with integer IDs for frontend selectors */
|
|
13
|
+
jiraConfigRoutes.get("/projects", (c) => {
|
|
14
|
+
return c.json(ok(getProjects()));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** GET / — list all configs */
|
|
18
|
+
jiraConfigRoutes.get("/", (c) => {
|
|
19
|
+
return c.json(ok(getAllConfigs()));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** GET /:projectId — get config for project */
|
|
23
|
+
jiraConfigRoutes.get("/:projectId", (c) => {
|
|
24
|
+
const projectId = parseInt(c.req.param("projectId"), 10);
|
|
25
|
+
if (isNaN(projectId)) return c.json(err("Invalid projectId"), 400);
|
|
26
|
+
const config = getConfigByProjectId(projectId);
|
|
27
|
+
if (!config) return c.json(err("No Jira config for this project"), 404);
|
|
28
|
+
return c.json(ok(config));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** PUT /:projectId — upsert config (token optional on update) */
|
|
32
|
+
jiraConfigRoutes.put("/:projectId", async (c) => {
|
|
33
|
+
const projectId = parseInt(c.req.param("projectId"), 10);
|
|
34
|
+
if (isNaN(projectId)) return c.json(err("Invalid projectId"), 400);
|
|
35
|
+
const body = await c.req.json<{ baseUrl?: string; email?: string; token?: string }>();
|
|
36
|
+
if (!body.baseUrl || !body.email) {
|
|
37
|
+
return c.json(err("baseUrl and email are required"), 400);
|
|
38
|
+
}
|
|
39
|
+
if (!body.baseUrl.startsWith("https://")) {
|
|
40
|
+
return c.json(err("baseUrl must start with https://"), 400);
|
|
41
|
+
}
|
|
42
|
+
// Token required for new configs, optional for updates
|
|
43
|
+
const existing = getConfigByProjectId(projectId);
|
|
44
|
+
if (!existing && !body.token) {
|
|
45
|
+
return c.json(err("token is required for new configs"), 400);
|
|
46
|
+
}
|
|
47
|
+
const config = upsertConfig(projectId, body.baseUrl, body.email, body.token);
|
|
48
|
+
return c.json(ok(config));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** DELETE /:projectId — delete config + cascade */
|
|
52
|
+
jiraConfigRoutes.delete("/:projectId", (c) => {
|
|
53
|
+
const projectId = parseInt(c.req.param("projectId"), 10);
|
|
54
|
+
if (isNaN(projectId)) return c.json(err("Invalid projectId"), 400);
|
|
55
|
+
const deleted = deleteConfig(projectId);
|
|
56
|
+
if (!deleted) return c.json(err("Config not found"), 404);
|
|
57
|
+
return c.json(ok({ deleted: true }));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** POST /:projectId/test — test Jira connection */
|
|
61
|
+
jiraConfigRoutes.post("/:projectId/test", async (c) => {
|
|
62
|
+
const projectId = parseInt(c.req.param("projectId"), 10);
|
|
63
|
+
if (isNaN(projectId)) return c.json(err("Invalid projectId"), 400);
|
|
64
|
+
const config = getConfigByProjectId(projectId);
|
|
65
|
+
if (!config) return c.json(err("No Jira config for this project"), 404);
|
|
66
|
+
const creds = getDecryptedCredentials(config.id);
|
|
67
|
+
if (!creds) return c.json(err("Failed to decrypt credentials"), 500);
|
|
68
|
+
try {
|
|
69
|
+
await testConnection(creds);
|
|
70
|
+
return c.json(ok({ connected: true }));
|
|
71
|
+
} catch (e: any) {
|
|
72
|
+
return c.json(err(`Connection failed: ${e.message}`), 502);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
createWatcher, updateWatcher, deleteWatcher,
|
|
4
|
+
getWatchersByConfigId, getWatcherById,
|
|
5
|
+
getResultsByWatcherId, getResultById, softDeleteResult,
|
|
6
|
+
getWatcherStats, insertResult, markResultRead, getUnreadCount,
|
|
7
|
+
} from "../../services/jira-watcher-db.service.ts";
|
|
8
|
+
import { jiraWatcherService, clampInterval } from "../../services/jira-watcher.service.ts";
|
|
9
|
+
import { jiraDebugService } from "../../services/jira-debug-session.service.ts";
|
|
10
|
+
import { getDecryptedCredentials } from "../../services/jira-config.service.ts";
|
|
11
|
+
import {
|
|
12
|
+
getIssue, updateIssue, getTransitions, transitionIssue,
|
|
13
|
+
searchText, searchIssues, getProjects, getFieldOptions, getAssignableUsers,
|
|
14
|
+
} from "../../services/jira-api-client.ts";
|
|
15
|
+
import { ok, err } from "../../types/api.ts";
|
|
16
|
+
import type { JiraWatcherMode } from "../../types/jira.ts";
|
|
17
|
+
|
|
18
|
+
/** Validate Jira issue key format (e.g. PROJ-123) */
|
|
19
|
+
const ISSUE_KEY_RE = /^[A-Z][A-Z0-9_]+-\d+$/i;
|
|
20
|
+
|
|
21
|
+
export const jiraWatcherRoutes = new Hono();
|
|
22
|
+
|
|
23
|
+
// ── Watcher CRUD ──────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
jiraWatcherRoutes.get("/watchers", (c) => {
|
|
26
|
+
const configId = c.req.query("configId");
|
|
27
|
+
if (!configId) return c.json(err("configId query param required"), 400);
|
|
28
|
+
return c.json(ok(getWatchersByConfigId(parseInt(configId, 10))));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
jiraWatcherRoutes.post("/watchers", async (c) => {
|
|
32
|
+
const body = await c.req.json<{
|
|
33
|
+
configId: number; name: string; jql: string;
|
|
34
|
+
promptTemplate?: string; intervalMs?: number; mode?: JiraWatcherMode;
|
|
35
|
+
}>();
|
|
36
|
+
if (!body.configId || !body.name || !body.jql) {
|
|
37
|
+
return c.json(err("configId, name, and jql are required"), 400);
|
|
38
|
+
}
|
|
39
|
+
if (body.mode && !["debug", "notify"].includes(body.mode)) {
|
|
40
|
+
return c.json(err("mode must be 'debug' or 'notify'"), 400);
|
|
41
|
+
}
|
|
42
|
+
const interval = body.intervalMs ? clampInterval(body.intervalMs) : 120000;
|
|
43
|
+
const watcher = createWatcher(body.configId, body.name, body.jql, {
|
|
44
|
+
promptTemplate: body.promptTemplate,
|
|
45
|
+
intervalMs: interval,
|
|
46
|
+
mode: body.mode,
|
|
47
|
+
});
|
|
48
|
+
// Auto-start if enabled
|
|
49
|
+
jiraWatcherService.startWatcher(watcher.id, watcher.intervalMs);
|
|
50
|
+
return c.json(ok(watcher), 201);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
jiraWatcherRoutes.put("/watchers/:id", async (c) => {
|
|
54
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
55
|
+
const body = await c.req.json<Partial<{
|
|
56
|
+
name: string; jql: string; promptTemplate: string | null;
|
|
57
|
+
intervalMs: number; enabled: boolean; mode: JiraWatcherMode;
|
|
58
|
+
}>>();
|
|
59
|
+
if (body.intervalMs !== undefined) body.intervalMs = clampInterval(body.intervalMs);
|
|
60
|
+
if (body.mode !== undefined && !["debug", "notify"].includes(body.mode)) {
|
|
61
|
+
return c.json(err("mode must be 'debug' or 'notify'"), 400);
|
|
62
|
+
}
|
|
63
|
+
const watcher = updateWatcher(id, body);
|
|
64
|
+
if (!watcher) return c.json(err("Watcher not found"), 404);
|
|
65
|
+
// Restart or stop based on enabled state
|
|
66
|
+
if (watcher.enabled) {
|
|
67
|
+
jiraWatcherService.startWatcher(watcher.id, watcher.intervalMs);
|
|
68
|
+
} else {
|
|
69
|
+
jiraWatcherService.stopWatcher(watcher.id);
|
|
70
|
+
}
|
|
71
|
+
return c.json(ok(watcher));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
jiraWatcherRoutes.delete("/watchers/:id", (c) => {
|
|
75
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
76
|
+
jiraWatcherService.stopWatcher(id);
|
|
77
|
+
if (!deleteWatcher(id)) return c.json(err("Watcher not found"), 404);
|
|
78
|
+
return c.json(ok({ deleted: true }));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
jiraWatcherRoutes.post("/watchers/test-jql", async (c) => {
|
|
82
|
+
const body = await c.req.json<{ configId: number; jql: string }>();
|
|
83
|
+
if (!body.configId || !body.jql) return c.json(err("configId and jql required"), 400);
|
|
84
|
+
const creds = getDecryptedCredentials(body.configId);
|
|
85
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
86
|
+
try {
|
|
87
|
+
const response = await searchIssues(creds, body.jql, undefined, 20);
|
|
88
|
+
return c.json(ok({ issues: response.issues, total: response.total }));
|
|
89
|
+
} catch (e: any) {
|
|
90
|
+
return c.json(err(`JQL search failed: ${e.message}`), 502);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
jiraWatcherRoutes.post("/watchers/:id/pull", async (c) => {
|
|
95
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
96
|
+
try {
|
|
97
|
+
const count = await jiraWatcherService.pollWatcher(id, "manual");
|
|
98
|
+
return c.json(ok({ polled: true, newIssues: count }));
|
|
99
|
+
} catch (e: any) {
|
|
100
|
+
return c.json(err(`Poll failed: ${e.message}`), 502);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
jiraWatcherRoutes.post("/watchers/pull-all", async (c) => {
|
|
105
|
+
try {
|
|
106
|
+
const all = (await import("../../services/jira-watcher-db.service.ts")).getAllEnabledWatchers();
|
|
107
|
+
let total = 0;
|
|
108
|
+
for (const w of all) {
|
|
109
|
+
try { total += await jiraWatcherService.pollWatcher(w.id, "manual"); } catch {}
|
|
110
|
+
}
|
|
111
|
+
return c.json(ok({ polled: true, watcherCount: all.length, newIssues: total }));
|
|
112
|
+
} catch (e: any) {
|
|
113
|
+
return c.json(err(`Pull failed: ${e.message}`), 502);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── Debug sessions ───────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
jiraWatcherRoutes.post("/results/:id/debug", async (c) => {
|
|
120
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
121
|
+
const body = await c.req.json<{ prompt?: string }>().catch(() => ({} as { prompt?: string }));
|
|
122
|
+
const result = getResultById(id);
|
|
123
|
+
if (!result) return c.json(err("Result not found"), 404);
|
|
124
|
+
if (result.status !== "pending" && result.status !== "failed") {
|
|
125
|
+
return c.json(err("Result must be pending or failed to debug"), 400);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
jiraDebugService.enqueue(id, body.prompt);
|
|
129
|
+
return c.json(ok({ queued: true }));
|
|
130
|
+
} catch (e: any) {
|
|
131
|
+
return c.json(err(e.message), 500);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
jiraWatcherRoutes.post("/results/:id/resume", async (c) => {
|
|
136
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
137
|
+
const body = await c.req.json<{ prompt?: string }>().catch(() => ({} as { prompt?: string }));
|
|
138
|
+
const result = getResultById(id);
|
|
139
|
+
if (!result) return c.json(err("Result not found"), 404);
|
|
140
|
+
if (result.status !== "failed") return c.json(err("Only failed results can be resumed"), 400);
|
|
141
|
+
if (!result.sessionId) return c.json(err("No session to resume"), 400);
|
|
142
|
+
try {
|
|
143
|
+
jiraDebugService.resumeDebug(id, body.prompt);
|
|
144
|
+
return c.json(ok({ resumed: true }));
|
|
145
|
+
} catch (e: any) {
|
|
146
|
+
return c.json(err(e.message), 500);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
jiraWatcherRoutes.post("/results/:id/cancel", (c) => {
|
|
151
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
152
|
+
const cancelled = jiraDebugService.cancelDebug(id);
|
|
153
|
+
if (!cancelled) return c.json(err("No active debug session for this result"), 404);
|
|
154
|
+
return c.json(ok({ cancelled: true }));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
jiraWatcherRoutes.patch("/results/:id/read", (c) => {
|
|
158
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
159
|
+
if (!markResultRead(id)) return c.json(err("Result not found or already read"), 404);
|
|
160
|
+
return c.json(ok({ read: true }));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
jiraWatcherRoutes.get("/results/unread-count", (c) => {
|
|
164
|
+
return c.json(ok({ count: getUnreadCount() }));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── Results ───────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
jiraWatcherRoutes.get("/results", (c) => {
|
|
170
|
+
const watcherId = c.req.query("watcherId");
|
|
171
|
+
const status = c.req.query("status");
|
|
172
|
+
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
173
|
+
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
174
|
+
const results = getResultsByWatcherId(
|
|
175
|
+
watcherId ? parseInt(watcherId, 10) : undefined,
|
|
176
|
+
{ status: status ?? undefined, limit, offset },
|
|
177
|
+
);
|
|
178
|
+
return c.json(ok(results));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
jiraWatcherRoutes.get("/results/stats", (c) => {
|
|
182
|
+
return c.json(ok(getWatcherStats()));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
jiraWatcherRoutes.get("/results/:id", (c) => {
|
|
186
|
+
const result = getResultById(parseInt(c.req.param("id"), 10));
|
|
187
|
+
if (!result) return c.json(err("Result not found"), 404);
|
|
188
|
+
return c.json(ok(result));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
jiraWatcherRoutes.delete("/results/:id", (c) => {
|
|
192
|
+
if (!softDeleteResult(parseInt(c.req.param("id"), 10))) {
|
|
193
|
+
return c.json(err("Result not found"), 404);
|
|
194
|
+
}
|
|
195
|
+
return c.json(ok({ deleted: true }));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── Manual ticket tracking ────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
jiraWatcherRoutes.post("/results/manual", async (c) => {
|
|
201
|
+
const body = await c.req.json<{ configId: number; issueKey: string }>();
|
|
202
|
+
if (!body.configId || !body.issueKey) return c.json(err("configId and issueKey required"), 400);
|
|
203
|
+
if (!ISSUE_KEY_RE.test(body.issueKey)) return c.json(err("Invalid issueKey format"), 400);
|
|
204
|
+
const creds = getDecryptedCredentials(body.configId);
|
|
205
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
206
|
+
try {
|
|
207
|
+
const issue = await getIssue(creds, body.issueKey);
|
|
208
|
+
const { inserted, resultId } = insertResult(
|
|
209
|
+
null, issue.key, issue.fields.summary, issue.fields.updated, "manual",
|
|
210
|
+
);
|
|
211
|
+
if (!inserted) return c.json(err("Issue already tracked"), 409);
|
|
212
|
+
return c.json(ok({ resultId, issueKey: issue.key }), 201);
|
|
213
|
+
} catch (e: any) {
|
|
214
|
+
return c.json(err(`Jira API error: ${e.message}`), 502);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Search ────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
jiraWatcherRoutes.get("/search/:configId", async (c) => {
|
|
221
|
+
const configId = parseInt(c.req.param("configId"), 10);
|
|
222
|
+
const q = c.req.query("q") ?? "";
|
|
223
|
+
if (!q) return c.json(err("q query param required"), 400);
|
|
224
|
+
const creds = getDecryptedCredentials(configId);
|
|
225
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
226
|
+
try {
|
|
227
|
+
const results = await searchText(creds, q);
|
|
228
|
+
return c.json(ok(results));
|
|
229
|
+
} catch (e: any) {
|
|
230
|
+
return c.json(err(`Search failed: ${e.message}`), 502);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── Ticket proxy ──────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
jiraWatcherRoutes.get("/ticket/:configId/:issueKey", async (c) => {
|
|
237
|
+
const issueKey = c.req.param("issueKey");
|
|
238
|
+
if (!ISSUE_KEY_RE.test(issueKey)) return c.json(err("Invalid issueKey format"), 400);
|
|
239
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
240
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
241
|
+
try {
|
|
242
|
+
const issue = await getIssue(creds, issueKey);
|
|
243
|
+
return c.json(ok(issue));
|
|
244
|
+
} catch (e: any) {
|
|
245
|
+
return c.json(err(`Jira error: ${e.message}`), 502);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
jiraWatcherRoutes.put("/ticket/:configId/:issueKey", async (c) => {
|
|
250
|
+
const issueKey = c.req.param("issueKey");
|
|
251
|
+
if (!ISSUE_KEY_RE.test(issueKey)) return c.json(err("Invalid issueKey format"), 400);
|
|
252
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
253
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
254
|
+
const body = await c.req.json<{ fields: Record<string, unknown> }>();
|
|
255
|
+
try {
|
|
256
|
+
await updateIssue(creds, issueKey, body.fields ?? body);
|
|
257
|
+
return c.json(ok({ updated: true }));
|
|
258
|
+
} catch (e: any) {
|
|
259
|
+
return c.json(err(`Jira error: ${e.message}`), 502);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
jiraWatcherRoutes.get("/ticket/:configId/:issueKey/transitions", async (c) => {
|
|
264
|
+
const issueKey = c.req.param("issueKey");
|
|
265
|
+
if (!ISSUE_KEY_RE.test(issueKey)) return c.json(err("Invalid issueKey format"), 400);
|
|
266
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
267
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
268
|
+
try {
|
|
269
|
+
const transitions = await getTransitions(creds, issueKey);
|
|
270
|
+
return c.json(ok(transitions));
|
|
271
|
+
} catch (e: any) {
|
|
272
|
+
return c.json(err(`Jira error: ${e.message}`), 502);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
jiraWatcherRoutes.post("/ticket/:configId/:issueKey/transition", async (c) => {
|
|
277
|
+
const issueKey = c.req.param("issueKey");
|
|
278
|
+
if (!ISSUE_KEY_RE.test(issueKey)) return c.json(err("Invalid issueKey format"), 400);
|
|
279
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
280
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
281
|
+
const body = await c.req.json<{ transitionId: string }>();
|
|
282
|
+
if (!body.transitionId) return c.json(err("transitionId required"), 400);
|
|
283
|
+
try {
|
|
284
|
+
await transitionIssue(creds, issueKey, body.transitionId);
|
|
285
|
+
return c.json(ok({ transitioned: true }));
|
|
286
|
+
} catch (e: any) {
|
|
287
|
+
return c.json(err(`Jira error: ${e.message}`), 502);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── Metadata for filter builder ───────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
jiraWatcherRoutes.get("/metadata/:configId/projects", async (c) => {
|
|
294
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
295
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
296
|
+
try { return c.json(ok(await getProjects(creds))); }
|
|
297
|
+
catch (e: any) { return c.json(err(e.message), 502); }
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
jiraWatcherRoutes.get("/metadata/:configId/assignees", async (c) => {
|
|
301
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
302
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
303
|
+
try { return c.json(ok(await getAssignableUsers(creds))); }
|
|
304
|
+
catch (e: any) { return c.json(err(e.message), 502); }
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
jiraWatcherRoutes.get("/metadata/:configId/:field", async (c) => {
|
|
308
|
+
const creds = getDecryptedCredentials(parseInt(c.req.param("configId"), 10));
|
|
309
|
+
if (!creds) return c.json(err("Invalid config"), 404);
|
|
310
|
+
const field = c.req.param("field") as "issuetype" | "priority" | "status";
|
|
311
|
+
if (!["issuetype", "priority", "status"].includes(field)) {
|
|
312
|
+
return c.json(err("Invalid field"), 400);
|
|
313
|
+
}
|
|
314
|
+
try { return c.json(ok(await getFieldOptions(creds, field))); }
|
|
315
|
+
catch (e: any) { return c.json(err(e.message), 502); }
|
|
316
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { jiraConfigRoutes } from "./jira-config-routes.ts";
|
|
3
|
+
import { jiraWatcherRoutes } from "./jira-watcher-routes.ts";
|
|
4
|
+
|
|
5
|
+
export const jiraRoutes = new Hono();
|
|
6
|
+
jiraRoutes.route("/config", jiraConfigRoutes);
|
|
7
|
+
jiraRoutes.route("/", jiraWatcherRoutes);
|