@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,92 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
const C = {
|
|
4
|
+
reset: "\x1b[0m", bold: "\x1b[1m", green: "\x1b[32m",
|
|
5
|
+
red: "\x1b[31m", yellow: "\x1b[33m", cyan: "\x1b[36m", dim: "\x1b[2m",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function registerJiraCommands(program: Command): Promise<void> {
|
|
9
|
+
const jira = program.command("jira").description("Jira watcher utilities");
|
|
10
|
+
registerConfigCommands(jira);
|
|
11
|
+
await registerWatcherCommands(jira);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── Config commands ───────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function registerConfigCommands(jira: Command): void {
|
|
17
|
+
const config = jira.command("config").description("Manage Jira configs");
|
|
18
|
+
|
|
19
|
+
config
|
|
20
|
+
.command("set <projectName>")
|
|
21
|
+
.description("Set Jira config for a project")
|
|
22
|
+
.requiredOption("--url <url>", "Jira base URL (https://...)")
|
|
23
|
+
.requiredOption("--email <email>", "Jira account email")
|
|
24
|
+
.requiredOption("--token <token>", "API token (⚠ visible in shell history)")
|
|
25
|
+
.action(async (projectName: string, opts: { url: string; email: string; token: string }) => {
|
|
26
|
+
try {
|
|
27
|
+
const { getDb } = await import("../../services/db.service.ts");
|
|
28
|
+
const project = getDb().query("SELECT id FROM projects WHERE name = ?").get(projectName) as { id: number } | null;
|
|
29
|
+
if (!project) { console.error(`${C.red}✗${C.reset} Project "${projectName}" not found`); process.exit(1); }
|
|
30
|
+
const { upsertConfig } = await import("../../services/jira-config.service.ts");
|
|
31
|
+
const cfg = upsertConfig(project.id, opts.url, opts.email, opts.token);
|
|
32
|
+
console.log(`${C.green}✓${C.reset} Jira config saved for "${projectName}" (id: ${cfg.id})`);
|
|
33
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
config
|
|
37
|
+
.command("show <projectName>")
|
|
38
|
+
.description("Show Jira config (token masked)")
|
|
39
|
+
.action(async (projectName: string) => {
|
|
40
|
+
try {
|
|
41
|
+
const { getDb } = await import("../../services/db.service.ts");
|
|
42
|
+
const project = getDb().query("SELECT id FROM projects WHERE name = ?").get(projectName) as { id: number } | null;
|
|
43
|
+
if (!project) { console.error(`${C.red}✗${C.reset} Project not found`); process.exit(1); }
|
|
44
|
+
const { getConfigByProjectId } = await import("../../services/jira-config.service.ts");
|
|
45
|
+
const cfg = getConfigByProjectId(project.id);
|
|
46
|
+
if (!cfg) { console.log(`${C.yellow}No Jira config for "${projectName}"${C.reset}`); return; }
|
|
47
|
+
console.log(` URL: ${cfg.baseUrl}`);
|
|
48
|
+
console.log(` Email: ${cfg.email}`);
|
|
49
|
+
console.log(` Token: ${cfg.hasToken ? "****" : "(none)"}`);
|
|
50
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
config
|
|
54
|
+
.command("remove <projectName>")
|
|
55
|
+
.description("Remove Jira config (cascades watchers + results)")
|
|
56
|
+
.action(async (projectName: string) => {
|
|
57
|
+
try {
|
|
58
|
+
const { getDb } = await import("../../services/db.service.ts");
|
|
59
|
+
const project = getDb().query("SELECT id FROM projects WHERE name = ?").get(projectName) as { id: number } | null;
|
|
60
|
+
if (!project) { console.error(`${C.red}✗${C.reset} Project not found`); process.exit(1); }
|
|
61
|
+
const { deleteConfig } = await import("../../services/jira-config.service.ts");
|
|
62
|
+
deleteConfig(project.id);
|
|
63
|
+
console.log(`${C.green}✓${C.reset} Config removed for "${projectName}"`);
|
|
64
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
config
|
|
68
|
+
.command("test <projectName>")
|
|
69
|
+
.description("Test Jira connection")
|
|
70
|
+
.action(async (projectName: string) => {
|
|
71
|
+
try {
|
|
72
|
+
const { getDb } = await import("../../services/db.service.ts");
|
|
73
|
+
const project = getDb().query("SELECT id FROM projects WHERE name = ?").get(projectName) as { id: number } | null;
|
|
74
|
+
if (!project) { console.error(`${C.red}✗${C.reset} Project not found`); process.exit(1); }
|
|
75
|
+
const { getConfigByProjectId, getDecryptedCredentials } = await import("../../services/jira-config.service.ts");
|
|
76
|
+
const cfg = getConfigByProjectId(project.id);
|
|
77
|
+
if (!cfg) { console.error(`${C.red}✗${C.reset} No config found`); process.exit(1); }
|
|
78
|
+
const creds = getDecryptedCredentials(cfg.id);
|
|
79
|
+
if (!creds) { console.error(`${C.red}✗${C.reset} Failed to decrypt token`); process.exit(1); }
|
|
80
|
+
const { testConnection } = await import("../../services/jira-api-client.ts");
|
|
81
|
+
await testConnection(creds);
|
|
82
|
+
console.log(`${C.green}✓${C.reset} Connection successful`);
|
|
83
|
+
} catch (e: any) { console.error(`${C.red}✗${C.reset} ${e.message}`); process.exit(1); }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Watcher + result + ticket commands ────────────────────────────────
|
|
88
|
+
|
|
89
|
+
async function registerWatcherCommands(jira: Command): Promise<void> {
|
|
90
|
+
const { registerJiraWatcherCommands } = await import("./jira-watcher-cmd.ts");
|
|
91
|
+
registerJiraWatcherCommands(jira);
|
|
92
|
+
}
|
|
@@ -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
|
+
});
|