@hienlh/ppm 0.10.4 → 0.11.0
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 +39 -0
- package/dist/web/assets/ai-settings-section-D2vqiydT.js +1 -0
- package/dist/web/assets/{api-settings-CoKe_BdR.js → api-settings-2eTz4SgY.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-BRW4VwMk.js +1 -0
- package/dist/web/assets/chat-tab-CbguR_l0.js +10 -0
- package/dist/web/assets/code-editor-DbZP0Dnj.js +8 -0
- package/dist/web/assets/{conflict-editor-HvxI1A29.js → conflict-editor-BzrH1UpC.js} +3 -3
- package/dist/web/assets/{csv-preview-BizIVMyb.js → csv-preview-D37K2LRd.js} +1 -1
- package/dist/web/assets/{database-viewer-BgCXPc4e.js → database-viewer-CqMOv2Sg.js} +2 -2
- package/dist/web/assets/{diff-viewer-blzXAJHd.js → diff-viewer-B6a2oYYn.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-CZr_fvOm.js +3 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bt68dqWT.js +1 -0
- package/dist/web/assets/index-C68PuiOm.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/{keybindings-store-D2N-Tq4N.js → keybindings-store-CpP5_miA.js} +1 -1
- package/dist/web/assets/keybindings-store-qfYScgY0.js +1 -0
- package/dist/web/assets/{markdown-renderer-Hcj-59AX.js → markdown-renderer-BhNYbXCp.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-Dw9MUu5a.js +1 -0
- package/dist/web/assets/{postgres-viewer-BEUI1N1X.js → postgres-viewer-YKyNjTLp.js} +3 -3
- package/dist/web/assets/{project-store-Ciq-cK1O.js → project-store-CczGNZyf.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DxEpzVN_.js +1 -0
- package/dist/web/assets/settings-store-CuYjM0FF.js +2 -0
- package/dist/web/assets/settings-tab-2tdZuQIn.js +1 -0
- package/dist/web/assets/{sql-query-editor-DZ9xskL8.js → sql-query-editor-CVEi0jLM.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-sQs615K6.js → sqlite-viewer-Fx9qDD4-.js} +1 -1
- package/dist/web/assets/{tab-store-DZbiYk7y.js → tab-store-Jvy1eZGM.js} +1 -1
- package/dist/web/assets/terminal-tab-BxljmYb7.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-yelcZZqO.js +1 -0
- package/dist/web/assets/{use-monaco-theme-OY18iXNi.js → use-monaco-theme-kjiAwvOp.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-B2SLgECS.js → vendor-mermaid-CylkVm4U.js} +3 -3
- package/dist/web/index.html +13 -13
- 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/extension.ts +11 -4
- package/packages/ext-git-graph/src/webview-html.ts +25 -11
- 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/extension-host-worker.ts +3 -2
- package/src/services/extension.service.ts +4 -2
- 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/services/supervisor-state.ts +13 -1
- package/src/services/supervisor.ts +4 -3
- 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 +9 -0
- package/src/web/components/extensions/extension-webview.tsx +31 -13
- 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/report-bug.ts +3 -2
- package/src/web/lib/ws-client.ts +14 -6
- package/src/web/stores/jira-store.ts +198 -0
- package/src/web/stores/settings-store.ts +24 -5
- 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-LMO_cfIW.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-CUZIB1Vq.js +0 -1
- package/dist/web/assets/chat-tab-By7krQ3s.js +0 -10
- package/dist/web/assets/code-editor-BoKL57Co.js +0 -8
- package/dist/web/assets/extension-webview-Dvk_61ON.js +0 -3
- package/dist/web/assets/gitGraph-HDMCJU4V-CtOMUphQ.js +0 -1
- package/dist/web/assets/index-DPnjO2FY.css +0 -2
- package/dist/web/assets/index-EgCQVN13.js +0 -26
- package/dist/web/assets/info-3K5VOQVL-BCrPCWGY.js +0 -1
- package/dist/web/assets/keybindings-store-C7No6mtl.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-D_OqB-zi.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-WUHpLNJz.js +0 -1
- package/dist/web/assets/port-forwarding-tab-CUgwDn_5.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-HQIIecVM.js +0 -1
- package/dist/web/assets/settings-store-B470PCWf.js +0 -2
- package/dist/web/assets/settings-tab-BGvgK51L.js +0 -1
- package/dist/web/assets/square-nsMa3iMk.js +0 -1
- package/dist/web/assets/terminal-tab-CUyHmiHH.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-0wLgUUTz.js +0 -1
- /package/dist/web/assets/{api-client-o_6TmLGC.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/{vendor-xterm-ejLe7-tK.js → vendor-xterm-B9BUAFKA.js} +0 -0
|
@@ -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);
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -54,12 +54,33 @@ export function hasActiveClient(): boolean {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Broadcast event to ALL connected WebSocket clients across all sessions */
|
|
58
|
+
export function broadcastGlobalEvent(event: unknown): void {
|
|
59
|
+
const json = JSON.stringify(event);
|
|
60
|
+
for (const entry of activeSessions.values()) {
|
|
61
|
+
for (const ws of entry.clients) {
|
|
62
|
+
try { ws.send(json); } catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
/** Remove a client from the session, cleaning up its ping interval */
|
|
58
68
|
function evictClient(entry: SessionEntry, ws: ChatWsSocket): void {
|
|
59
69
|
clearClientPing(entry, ws);
|
|
60
70
|
entry.clients.delete(ws);
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Forward an event to connected WS clients for a session (if any).
|
|
75
|
+
* Used by background processes (e.g. Jira debug) that run sessions server-side
|
|
76
|
+
* but want to stream events to any frontend client viewing that session.
|
|
77
|
+
*/
|
|
78
|
+
export function forwardEventToSession(sessionId: string, event: unknown): void {
|
|
79
|
+
const entry = activeSessions.get(sessionId);
|
|
80
|
+
if (!entry || entry.clients.size === 0) return; // no connected clients, silently drop
|
|
81
|
+
bufferAndBroadcast(sessionId, event);
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
/** Broadcast event to all connected clients for a session */
|
|
64
85
|
function broadcast(sessionId: string, event: unknown): void {
|
|
65
86
|
const entry = activeSessions.get(sessionId);
|
|
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { mkdirSync, existsSync } from "node:fs";
|
|
4
4
|
import { encrypt, decrypt } from "../lib/account-crypto.ts";
|
|
5
5
|
import { getPpmDir } from "./ppm-dir.ts";
|
|
6
|
-
const CURRENT_SCHEMA_VERSION =
|
|
6
|
+
const CURRENT_SCHEMA_VERSION = 18;
|
|
7
7
|
|
|
8
8
|
let db: Database | null = null;
|
|
9
9
|
let dbProfile: string | null = null;
|
|
@@ -491,6 +491,70 @@ function runMigrations(database: Database): void {
|
|
|
491
491
|
PRAGMA user_version = 17;
|
|
492
492
|
`);
|
|
493
493
|
}
|
|
494
|
+
|
|
495
|
+
if (current < 18) {
|
|
496
|
+
database.exec(`
|
|
497
|
+
CREATE TABLE IF NOT EXISTS jira_config (
|
|
498
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
499
|
+
project_id INTEGER UNIQUE REFERENCES projects(id) ON DELETE CASCADE,
|
|
500
|
+
base_url TEXT NOT NULL,
|
|
501
|
+
email TEXT NOT NULL,
|
|
502
|
+
api_token_encrypted TEXT NOT NULL,
|
|
503
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
CREATE TABLE IF NOT EXISTS jira_watchers (
|
|
507
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
508
|
+
jira_config_id INTEGER NOT NULL REFERENCES jira_config(id) ON DELETE CASCADE,
|
|
509
|
+
name TEXT NOT NULL,
|
|
510
|
+
jql TEXT NOT NULL,
|
|
511
|
+
prompt_template TEXT,
|
|
512
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
513
|
+
mode TEXT NOT NULL DEFAULT 'debug',
|
|
514
|
+
interval_ms INTEGER NOT NULL DEFAULT 120000,
|
|
515
|
+
last_polled_at TEXT,
|
|
516
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
517
|
+
);
|
|
518
|
+
CREATE INDEX IF NOT EXISTS idx_jira_watchers_config
|
|
519
|
+
ON jira_watchers(jira_config_id);
|
|
520
|
+
CREATE INDEX IF NOT EXISTS idx_jira_watchers_enabled
|
|
521
|
+
ON jira_watchers(enabled);
|
|
522
|
+
|
|
523
|
+
CREATE TABLE IF NOT EXISTS jira_watch_results (
|
|
524
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
525
|
+
watcher_id INTEGER REFERENCES jira_watchers(id) ON DELETE CASCADE,
|
|
526
|
+
issue_key TEXT NOT NULL,
|
|
527
|
+
issue_summary TEXT,
|
|
528
|
+
issue_updated TEXT,
|
|
529
|
+
session_id TEXT,
|
|
530
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
531
|
+
ai_summary TEXT,
|
|
532
|
+
source TEXT NOT NULL DEFAULT 'watcher',
|
|
533
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
534
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
535
|
+
UNIQUE(watcher_id, issue_key, issue_updated)
|
|
536
|
+
);
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_jira_results_watcher
|
|
538
|
+
ON jira_watch_results(watcher_id, deleted);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_jira_results_status
|
|
540
|
+
ON jira_watch_results(status);
|
|
541
|
+
|
|
542
|
+
PRAGMA user_version = 18;
|
|
543
|
+
`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (current < 19) {
|
|
547
|
+
database.exec(`
|
|
548
|
+
ALTER TABLE jira_watch_results ADD COLUMN read_at TEXT;
|
|
549
|
+
ALTER TABLE jira_watch_results ADD COLUMN triggered_by TEXT DEFAULT 'auto';
|
|
550
|
+
-- Cleanup stale running results from bot_task era
|
|
551
|
+
UPDATE jira_watch_results SET status = 'failed', ai_summary = 'Stale: migrated from bot_task flow' WHERE status = 'running';
|
|
552
|
+
-- Index for unread count query (status='done' AND read_at IS NULL AND deleted=0)
|
|
553
|
+
CREATE INDEX IF NOT EXISTS idx_jira_results_unread
|
|
554
|
+
ON jira_watch_results(status, read_at) WHERE deleted = 0;
|
|
555
|
+
PRAGMA user_version = 19;
|
|
556
|
+
`);
|
|
557
|
+
}
|
|
494
558
|
}
|
|
495
559
|
|
|
496
560
|
// ---------------------------------------------------------------------------
|
|
@@ -28,12 +28,13 @@ self.addEventListener("message", (event: MessageEvent<RpcMessage>) => {
|
|
|
28
28
|
// --- RPC handlers ---
|
|
29
29
|
|
|
30
30
|
rpc.onRequest("ext:activate", async (params) => {
|
|
31
|
-
const [extId, entryPath, extensionPath, storedState, baseUrl] = params as [string, string, string, Record<string, Record<string, string | null>>?, string?];
|
|
31
|
+
const [extId, entryPath, extensionPath, storedState, baseUrl, authToken] = params as [string, string, string, Record<string, Record<string, string | null>>?, string?, string?];
|
|
32
32
|
console.log(`[ExtHost] activating ${extId} from ${entryPath}`);
|
|
33
33
|
if (activeExtensions.has(extId)) return { ok: true, already: true };
|
|
34
34
|
|
|
35
|
-
// Expose server base URL so extensions can use fetch() with absolute URLs
|
|
35
|
+
// Expose server base URL and auth token so extensions can use fetch() with absolute URLs
|
|
36
36
|
if (baseUrl) (globalThis as any).__PPM_BASE_URL__ = baseUrl;
|
|
37
|
+
if (authToken) (globalThis as any).__PPM_AUTH_TOKEN__ = authToken;
|
|
37
38
|
|
|
38
39
|
// Create RpcClient adapter for vscode-compat (Worker's RPC → vscode-compat interface)
|
|
39
40
|
const rpcClient = {
|
|
@@ -138,14 +138,16 @@ class ExtensionService {
|
|
|
138
138
|
workspace: Object.fromEntries(workspaceStorage.map((r) => [r.key, r.value])),
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
-
// Pass server base URL so extensions can make fetch() calls in the Worker
|
|
141
|
+
// Pass server base URL + auth token so extensions can make fetch() calls in the Worker
|
|
142
142
|
const { configService: cfg } = await import("./config.service.ts");
|
|
143
143
|
const port = cfg.get("port") ?? 8080;
|
|
144
144
|
const baseUrl = `http://localhost:${port}`;
|
|
145
|
+
const authConfig = cfg.get("auth");
|
|
146
|
+
const authToken = authConfig?.enabled ? authConfig.token : undefined;
|
|
145
147
|
|
|
146
148
|
console.log(`[ExtService] activating ${id} (entry: ${entryPath})`);
|
|
147
149
|
const result = await rpc.sendRequest<{ ok: boolean; error?: string }>(
|
|
148
|
-
"ext:activate", id, entryPath, extDir, storedState, baseUrl,
|
|
150
|
+
"ext:activate", id, entryPath, extDir, storedState, baseUrl, authToken,
|
|
149
151
|
);
|
|
150
152
|
if (!result.ok) {
|
|
151
153
|
this.activationErrors.set(id, result.error ?? "Unknown activation error");
|
|
@@ -127,6 +127,48 @@ class FileService {
|
|
|
127
127
|
return nodes;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Search project for files matching a given filename (exact, case-sensitive) */
|
|
131
|
+
resolveFilename(projectPath: string, filename: string, maxResults = 20): FileNode[] {
|
|
132
|
+
const ig = loadGitignore(projectPath);
|
|
133
|
+
const matches: FileNode[] = [];
|
|
134
|
+
this.walkForFilename(projectPath, projectPath, filename, ig, matches, maxResults);
|
|
135
|
+
return matches;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private walkForFilename(
|
|
139
|
+
rootPath: string,
|
|
140
|
+
dirPath: string,
|
|
141
|
+
targetName: string,
|
|
142
|
+
ig: Ignore,
|
|
143
|
+
results: FileNode[],
|
|
144
|
+
maxResults: number,
|
|
145
|
+
): void {
|
|
146
|
+
if (results.length >= maxResults) return;
|
|
147
|
+
if (!existsSync(dirPath)) return;
|
|
148
|
+
|
|
149
|
+
let entries;
|
|
150
|
+
try { entries = readdirSync(dirPath, { withFileTypes: true }); }
|
|
151
|
+
catch { return; }
|
|
152
|
+
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (results.length >= maxResults) return;
|
|
155
|
+
if (this.isExcluded(entry.name)) continue;
|
|
156
|
+
|
|
157
|
+
const fullPath = join(dirPath, entry.name);
|
|
158
|
+
const relPath = relative(rootPath, fullPath);
|
|
159
|
+
const relPosix = relPath.split("\\").join("/");
|
|
160
|
+
|
|
161
|
+
const checkPath = entry.isDirectory() ? `${relPosix}/` : relPosix;
|
|
162
|
+
if (ig.ignores(checkPath) || ig.ignores(relPosix)) continue;
|
|
163
|
+
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
this.walkForFilename(rootPath, fullPath, targetName, ig, results, maxResults);
|
|
166
|
+
} else if (entry.name === targetName) {
|
|
167
|
+
results.push({ name: entry.name, path: relPath, type: "file" });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
130
172
|
/** Read file content with encoding detection */
|
|
131
173
|
readFile(
|
|
132
174
|
projectPath: string,
|