@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/web/assets/ai-settings-section-D2vqiydT.js +1 -0
  3. package/dist/web/assets/{api-settings-CoKe_BdR.js → api-settings-2eTz4SgY.js} +1 -1
  4. package/dist/web/assets/architecture-PBZL5I3N-BRW4VwMk.js +1 -0
  5. package/dist/web/assets/chat-tab-CbguR_l0.js +10 -0
  6. package/dist/web/assets/code-editor-DbZP0Dnj.js +8 -0
  7. package/dist/web/assets/{conflict-editor-HvxI1A29.js → conflict-editor-BzrH1UpC.js} +3 -3
  8. package/dist/web/assets/{csv-preview-BizIVMyb.js → csv-preview-D37K2LRd.js} +1 -1
  9. package/dist/web/assets/{database-viewer-BgCXPc4e.js → database-viewer-CqMOv2Sg.js} +2 -2
  10. package/dist/web/assets/{diff-viewer-blzXAJHd.js → diff-viewer-B6a2oYYn.js} +1 -1
  11. package/dist/web/assets/{esm-K1XIK4vc.js → esm-B99v94EE.js} +1 -1
  12. package/dist/web/assets/{extension-store-3yZYn07W.js → extension-store-CkyOvGbF.js} +1 -1
  13. package/dist/web/assets/extension-webview-CZr_fvOm.js +3 -0
  14. package/dist/web/assets/gitGraph-HDMCJU4V-Bt68dqWT.js +1 -0
  15. package/dist/web/assets/index-C68PuiOm.js +26 -0
  16. package/dist/web/assets/index-iZHWllzQ.css +2 -0
  17. package/dist/web/assets/info-3K5VOQVL-ySD5z855.js +1 -0
  18. package/dist/web/assets/{keybindings-store-D2N-Tq4N.js → keybindings-store-CpP5_miA.js} +1 -1
  19. package/dist/web/assets/keybindings-store-qfYScgY0.js +1 -0
  20. package/dist/web/assets/{markdown-renderer-Hcj-59AX.js → markdown-renderer-BhNYbXCp.js} +3 -3
  21. package/dist/web/assets/packet-RMMSAZCW-CLxaXgIf.js +1 -0
  22. package/dist/web/assets/pie-UPGHQEXC-C9wPZfkn.js +1 -0
  23. package/dist/web/assets/port-forwarding-tab-Dw9MUu5a.js +1 -0
  24. package/dist/web/assets/{postgres-viewer-BEUI1N1X.js → postgres-viewer-YKyNjTLp.js} +3 -3
  25. package/dist/web/assets/{project-store-Ciq-cK1O.js → project-store-CczGNZyf.js} +1 -1
  26. package/dist/web/assets/radar-KQ55EAFF-DxEpzVN_.js +1 -0
  27. package/dist/web/assets/settings-store-CuYjM0FF.js +2 -0
  28. package/dist/web/assets/settings-tab-2tdZuQIn.js +1 -0
  29. package/dist/web/assets/{sql-query-editor-DZ9xskL8.js → sql-query-editor-CVEi0jLM.js} +1 -1
  30. package/dist/web/assets/{sqlite-viewer-sQs615K6.js → sqlite-viewer-Fx9qDD4-.js} +1 -1
  31. package/dist/web/assets/{tab-store-DZbiYk7y.js → tab-store-Jvy1eZGM.js} +1 -1
  32. package/dist/web/assets/terminal-tab-BxljmYb7.js +1 -0
  33. package/dist/web/assets/treemap-KZPCXAKY-yelcZZqO.js +1 -0
  34. package/dist/web/assets/{use-monaco-theme-OY18iXNi.js → use-monaco-theme-kjiAwvOp.js} +1 -1
  35. package/dist/web/assets/{vendor-mermaid-B2SLgECS.js → vendor-mermaid-CylkVm4U.js} +3 -3
  36. package/dist/web/index.html +13 -13
  37. package/dist/web/sw.js +1 -1
  38. package/docs/codebase-summary.md +29 -5
  39. package/docs/project-changelog.md +31 -1
  40. package/docs/system-architecture.md +106 -1
  41. package/package.json +1 -1
  42. package/packages/ext-git-graph/src/extension.ts +11 -4
  43. package/packages/ext-git-graph/src/webview-html.ts +25 -11
  44. package/src/cli/commands/jira-cmd.ts +92 -0
  45. package/src/cli/commands/jira-watcher-cmd.ts +149 -0
  46. package/src/index.ts +3 -0
  47. package/src/server/index.ts +19 -0
  48. package/src/server/routes/files.ts +15 -0
  49. package/src/server/routes/fs-browse.ts +40 -1
  50. package/src/server/routes/jira-config-routes.ts +74 -0
  51. package/src/server/routes/jira-watcher-routes.ts +316 -0
  52. package/src/server/routes/jira.ts +7 -0
  53. package/src/server/ws/chat.ts +21 -0
  54. package/src/services/db.service.ts +65 -1
  55. package/src/services/extension-host-worker.ts +3 -2
  56. package/src/services/extension.service.ts +4 -2
  57. package/src/services/file.service.ts +42 -0
  58. package/src/services/jira-api-client.ts +216 -0
  59. package/src/services/jira-config.service.ts +83 -0
  60. package/src/services/jira-debug-session.service.ts +240 -0
  61. package/src/services/jira-watcher-db.service.ts +195 -0
  62. package/src/services/jira-watcher.service.ts +159 -0
  63. package/src/services/notification.service.ts +6 -0
  64. package/src/services/supervisor-state.ts +13 -1
  65. package/src/services/supervisor.ts +4 -3
  66. package/src/types/jira.ts +128 -0
  67. package/src/web/app.tsx +15 -12
  68. package/src/web/components/chat/chat-tab.tsx +32 -1
  69. package/src/web/components/chat/message-input.tsx +56 -5
  70. package/src/web/components/explorer/file-tree.tsx +9 -0
  71. package/src/web/components/extensions/extension-webview.tsx +31 -13
  72. package/src/web/components/jira/jira-config-form.tsx +109 -0
  73. package/src/web/components/jira/jira-debug-prompt-dialog.tsx +58 -0
  74. package/src/web/components/jira/jira-filter-builder.tsx +197 -0
  75. package/src/web/components/jira/jira-panel.tsx +201 -0
  76. package/src/web/components/jira/jira-results-panel.tsx +184 -0
  77. package/src/web/components/jira/jira-settings-section.tsx +58 -0
  78. package/src/web/components/jira/jira-status-badge.tsx +18 -0
  79. package/src/web/components/jira/jira-ticket-card.tsx +144 -0
  80. package/src/web/components/jira/jira-ticket-detail.tsx +153 -0
  81. package/src/web/components/jira/jira-watcher-form.tsx +154 -0
  82. package/src/web/components/jira/jira-watcher-list.tsx +98 -0
  83. package/src/web/components/layout/mobile-drawer.tsx +18 -5
  84. package/src/web/components/layout/sidebar.tsx +20 -3
  85. package/src/web/components/settings/settings-tab.tsx +20 -3
  86. package/src/web/components/shared/markdown-code-block.tsx +5 -3
  87. package/src/web/components/ui/file-browser-picker.tsx +88 -1
  88. package/src/web/hooks/use-chat.ts +6 -0
  89. package/src/web/lib/report-bug.ts +3 -2
  90. package/src/web/lib/ws-client.ts +14 -6
  91. package/src/web/stores/jira-store.ts +198 -0
  92. package/src/web/stores/settings-store.ts +24 -5
  93. package/src/web/styles/globals.css +7 -0
  94. package/vite.config.ts +5 -66
  95. package/bun.lock +0 -2062
  96. package/bunfig.toml +0 -2
  97. package/dist/web/assets/ai-settings-section-LMO_cfIW.js +0 -1
  98. package/dist/web/assets/architecture-PBZL5I3N-CUZIB1Vq.js +0 -1
  99. package/dist/web/assets/chat-tab-By7krQ3s.js +0 -10
  100. package/dist/web/assets/code-editor-BoKL57Co.js +0 -8
  101. package/dist/web/assets/extension-webview-Dvk_61ON.js +0 -3
  102. package/dist/web/assets/gitGraph-HDMCJU4V-CtOMUphQ.js +0 -1
  103. package/dist/web/assets/index-DPnjO2FY.css +0 -2
  104. package/dist/web/assets/index-EgCQVN13.js +0 -26
  105. package/dist/web/assets/info-3K5VOQVL-BCrPCWGY.js +0 -1
  106. package/dist/web/assets/keybindings-store-C7No6mtl.js +0 -1
  107. package/dist/web/assets/packet-RMMSAZCW-D_OqB-zi.js +0 -1
  108. package/dist/web/assets/pie-UPGHQEXC-WUHpLNJz.js +0 -1
  109. package/dist/web/assets/port-forwarding-tab-CUgwDn_5.js +0 -1
  110. package/dist/web/assets/radar-KQ55EAFF-HQIIecVM.js +0 -1
  111. package/dist/web/assets/settings-store-B470PCWf.js +0 -2
  112. package/dist/web/assets/settings-tab-BGvgK51L.js +0 -1
  113. package/dist/web/assets/square-nsMa3iMk.js +0 -1
  114. package/dist/web/assets/terminal-tab-CUyHmiHH.js +0 -1
  115. package/dist/web/assets/treemap-KZPCXAKY-0wLgUUTz.js +0 -1
  116. /package/dist/web/assets/{api-client-o_6TmLGC.js → api-client-C3tXCh0r.js} +0 -0
  117. /package/dist/web/assets/{csv-parser--2WJNgS7.js → csv-parser-BAa56Nnn.js} +0 -0
  118. /package/dist/web/assets/{dist-im4ynINo.js → dist-On3hz9_g.js} +0 -0
  119. /package/dist/web/assets/{katex-CKoArbIw.js → katex-Bbu770d9.js} +0 -0
  120. /package/dist/web/assets/{lib-D_kRA9p6.js → lib-BqkcKGFq.js} +0 -0
  121. /package/dist/web/assets/{react-GqWghJ-L.js → react-BkWDCPD7.js} +0 -0
  122. /package/dist/web/assets/{sql-completion-provider-C3cq9j99.js → sql-completion-provider-D3acAhav.js} +0 -0
  123. /package/dist/web/assets/{table-Dq575bPF.js → table-DbSviOmw.js} +0 -0
  124. /package/dist/web/assets/{text-wrap-Cn6BNQfq.js → text-wrap-DzvCTq_i.js} +0 -0
  125. /package/dist/web/assets/{trash-2-CJYoLw7Q.js → trash-2-BgDIBl6f.js} +0 -0
  126. /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();
@@ -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
+ });