@jiraacp/cli 2026.405.4
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/README.md +283 -0
- package/dist/abort-GQE4OI5S.js +103 -0
- package/dist/abort-GQE4OI5S.js.map +1 -0
- package/dist/abort-VMRQOADY.js +96 -0
- package/dist/abort-VMRQOADY.js.map +1 -0
- package/dist/bot-WOTETAJY.js +13 -0
- package/dist/bot-WOTETAJY.js.map +1 -0
- package/dist/cancel-clarification-4G5S2HJZ.js +64 -0
- package/dist/cancel-clarification-4G5S2HJZ.js.map +1 -0
- package/dist/chunk-3U373M37.js +67 -0
- package/dist/chunk-3U373M37.js.map +1 -0
- package/dist/chunk-3YHD4SIN.js +97 -0
- package/dist/chunk-3YHD4SIN.js.map +1 -0
- package/dist/chunk-6IY6CRUJ.js +690 -0
- package/dist/chunk-6IY6CRUJ.js.map +1 -0
- package/dist/chunk-B6OA3XJK.js +1167 -0
- package/dist/chunk-B6OA3XJK.js.map +1 -0
- package/dist/chunk-BM4R6NST.js +191 -0
- package/dist/chunk-BM4R6NST.js.map +1 -0
- package/dist/chunk-FLPIU2QO.js +77 -0
- package/dist/chunk-FLPIU2QO.js.map +1 -0
- package/dist/chunk-H7YXX4UA.js +86 -0
- package/dist/chunk-H7YXX4UA.js.map +1 -0
- package/dist/chunk-IT74N3UH.js +19 -0
- package/dist/chunk-IT74N3UH.js.map +1 -0
- package/dist/chunk-JOT4UVSO.js +186 -0
- package/dist/chunk-JOT4UVSO.js.map +1 -0
- package/dist/chunk-KSJKCLEJ.js +222 -0
- package/dist/chunk-KSJKCLEJ.js.map +1 -0
- package/dist/chunk-LIEW4ULF.js +139 -0
- package/dist/chunk-LIEW4ULF.js.map +1 -0
- package/dist/chunk-M4V3YOCY.js +82 -0
- package/dist/chunk-M4V3YOCY.js.map +1 -0
- package/dist/chunk-MMWQHH25.js +207 -0
- package/dist/chunk-MMWQHH25.js.map +1 -0
- package/dist/chunk-OJ4CNF73.js +78 -0
- package/dist/chunk-OJ4CNF73.js.map +1 -0
- package/dist/chunk-PFJAC3RO.js +137 -0
- package/dist/chunk-PFJAC3RO.js.map +1 -0
- package/dist/chunk-PVKVCUNR.js +159 -0
- package/dist/chunk-PVKVCUNR.js.map +1 -0
- package/dist/chunk-RXT4WSIY.js +35 -0
- package/dist/chunk-RXT4WSIY.js.map +1 -0
- package/dist/chunk-RZK74PDF.js +34 -0
- package/dist/chunk-RZK74PDF.js.map +1 -0
- package/dist/chunk-UDTWVKRX.js +68 -0
- package/dist/chunk-UDTWVKRX.js.map +1 -0
- package/dist/chunk-VCEONSWJ.js +307 -0
- package/dist/chunk-VCEONSWJ.js.map +1 -0
- package/dist/chunk-VWBCDZWQ.js +119 -0
- package/dist/chunk-VWBCDZWQ.js.map +1 -0
- package/dist/chunk-WEJCTFQB.js +228 -0
- package/dist/chunk-WEJCTFQB.js.map +1 -0
- package/dist/chunk-YJK7IRPI.js +223 -0
- package/dist/chunk-YJK7IRPI.js.map +1 -0
- package/dist/claude-md-HQ6L4CRP.js +8 -0
- package/dist/claude-md-HQ6L4CRP.js.map +1 -0
- package/dist/cli.js +276 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-RG45VBTZ.js +407 -0
- package/dist/commands-RG45VBTZ.js.map +1 -0
- package/dist/commands-WYVRVE5Z.js +400 -0
- package/dist/commands-WYVRVE5Z.js.map +1 -0
- package/dist/config-edit-G7O56HXO.js +50 -0
- package/dist/config-edit-G7O56HXO.js.map +1 -0
- package/dist/config-set-QN3JRNZL.js +63 -0
- package/dist/config-set-QN3JRNZL.js.map +1 -0
- package/dist/daemon-CGBV55JK.js +104 -0
- package/dist/daemon-CGBV55JK.js.map +1 -0
- package/dist/dashboard-YVFJ5DXR.js +143 -0
- package/dist/dashboard-YVFJ5DXR.js.map +1 -0
- package/dist/doctor-BPTLVLTD.js +98 -0
- package/dist/doctor-BPTLVLTD.js.map +1 -0
- package/dist/human-loop-RBTA2TYK.js +16 -0
- package/dist/human-loop-RBTA2TYK.js.map +1 -0
- package/dist/human-loop-XGWXUNCS.js +18 -0
- package/dist/human-loop-XGWXUNCS.js.map +1 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-DGW7HCJ5.js +21 -0
- package/dist/loader-DGW7HCJ5.js.map +1 -0
- package/dist/logs-JUVQWN6C.js +93 -0
- package/dist/logs-JUVQWN6C.js.map +1 -0
- package/dist/mcp.js +132 -0
- package/dist/mcp.js.map +1 -0
- package/dist/orchestrator-3MGXX3QW.js +22 -0
- package/dist/orchestrator-3MGXX3QW.js.map +1 -0
- package/dist/orchestrator-BVUKN5N3.js +13 -0
- package/dist/orchestrator-BVUKN5N3.js.map +1 -0
- package/dist/pause-FLDZ3OD6.js +62 -0
- package/dist/pause-FLDZ3OD6.js.map +1 -0
- package/dist/projects-QMIGNW7U.js +129 -0
- package/dist/projects-QMIGNW7U.js.map +1 -0
- package/dist/replay-M4JEG4Z4.js +151 -0
- package/dist/replay-M4JEG4Z4.js.map +1 -0
- package/dist/schedule-CDHD77VZ.js +17 -0
- package/dist/schedule-CDHD77VZ.js.map +1 -0
- package/dist/serve-XI7JTIPZ.js +231 -0
- package/dist/serve-XI7JTIPZ.js.map +1 -0
- package/dist/sprint-KZZWVNK6.js +200 -0
- package/dist/sprint-KZZWVNK6.js.map +1 -0
- package/dist/status-I6GU2LWE.js +48 -0
- package/dist/status-I6GU2LWE.js.map +1 -0
- package/dist/topic-manager-4AMEPMFI.js +12 -0
- package/dist/topic-manager-4AMEPMFI.js.map +1 -0
- package/dist/triage-WNHGPVZQ.js +251 -0
- package/dist/triage-WNHGPVZQ.js.map +1 -0
- package/dist/usage-AWWBI37F.js +155 -0
- package/dist/usage-AWWBI37F.js.map +1 -0
- package/dist/wizard-CYEJJLNF.js +190 -0
- package/dist/wizard-CYEJJLNF.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# jira-acp
|
|
2
|
+
|
|
3
|
+
> AI-powered Jira pipeline CLI: Ticket → Code → GitHub → Deploy → Notify
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/jira-acp)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
`jiraACP` automates the full ticket lifecycle for small dev teams. Pick up a Jira ticket in the morning — by the time you check Telegram, it's been analyzed, coded, reviewed, and deployed. Ambiguous tickets send a clarification message instead of silently failing.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @jiraacp/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Requirements:** Node.js >= 20, [Claude Code](https://claude.ai/code) installed globally.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. Configure a project (interactive wizard)
|
|
27
|
+
jiraACP init
|
|
28
|
+
|
|
29
|
+
# 2. Start the background server
|
|
30
|
+
jiraACP start
|
|
31
|
+
|
|
32
|
+
# 3. Run the pipeline for a ticket
|
|
33
|
+
jiraACP run PROJ-123
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Two Binaries
|
|
39
|
+
|
|
40
|
+
| Binary | Purpose |
|
|
41
|
+
|--------|---------|
|
|
42
|
+
| `jiraACP` | Pipeline CLI — all commands |
|
|
43
|
+
| `jiraACP-mcp` | Jira MCP server — auto-configured for Claude Code agents |
|
|
44
|
+
|
|
45
|
+
`jiraACP init` writes `.mcp.json` into your workspace so Claude Code agents have Jira tools available automatically.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## How It Works
|
|
50
|
+
|
|
51
|
+
Each **project** binds one Jira instance + one GitHub repo + one Claude Code workspace + one Telegram chat into a single orchestrated unit.
|
|
52
|
+
|
|
53
|
+
### 9-Stage Pipeline
|
|
54
|
+
|
|
55
|
+
| Stage | What happens | Model |
|
|
56
|
+
|-------|-------------|-------|
|
|
57
|
+
| 1. Fetch | Pull assigned Jira tickets | Haiku |
|
|
58
|
+
| 2. Analyze | Clarity scoring — criteria, design, dependencies | Haiku→Sonnet |
|
|
59
|
+
| 3. Clarify | Telegram prompt if ambiguous, await human reply | Haiku |
|
|
60
|
+
| 4. Code | Claude Code implements the ticket in your workspace | Sonnet/Opus |
|
|
61
|
+
| 5. Git | Create branch, commit, push, open PR | Haiku |
|
|
62
|
+
| 6. Review | Two-agent PR review, auto-merge if clean | Sonnet×2 |
|
|
63
|
+
| 7. Deploy | Run your deploy script | Haiku |
|
|
64
|
+
| 8. Test | Playwright agent tests on dev server | Sonnet |
|
|
65
|
+
| 9. Notify | Jira: transition + comment. Telegram: done | Haiku |
|
|
66
|
+
|
|
67
|
+
Stage 4 auto-upgrades to Opus for auth, payments, DB schema changes, or cross-module refactoring.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## CLI Reference
|
|
72
|
+
|
|
73
|
+
### Server (Daemon)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
jiraACP start [--port 3100] # Start background server
|
|
77
|
+
jiraACP stop # Stop background server
|
|
78
|
+
jiraACP restart # Restart background server
|
|
79
|
+
jiraACP status # Check if server is running
|
|
80
|
+
jiraACP logs [-f] [-n 100] # Tail server logs
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Setup
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
jiraACP init [--dir <path>] # Interactive setup wizard
|
|
87
|
+
jiraACP doctor [--fix] # Health-check all integrations
|
|
88
|
+
jiraACP update-context # Regenerate CLAUDE.md from codebase scan
|
|
89
|
+
jiraACP projects list|add|remove # Manage configured projects
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Pipeline
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
jiraACP run <ticketKey> [options]
|
|
96
|
+
--project <name> Target project (default: auto-detect from git)
|
|
97
|
+
--from <stage> Start from this stage
|
|
98
|
+
--to <stage> End at this stage
|
|
99
|
+
--dry-run Simulate without side effects
|
|
100
|
+
--no-confirm Non-interactive mode
|
|
101
|
+
|
|
102
|
+
jiraACP sprint [--project] [--sprint] [--parallel 2] [--filter <jql>] [--dry-run]
|
|
103
|
+
jiraACP triage [--project] [--sprint]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Monitoring
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
jiraACP status [ticketKey] # Daemon status, or pipeline state for a ticket
|
|
110
|
+
jiraACP logs [ticketKey] [-f] # Server logs, or ticket pipeline logs
|
|
111
|
+
jiraACP dashboard [--watch] # Terminal UI of active runs
|
|
112
|
+
jiraACP replay <ticketKey> # Replay completed run event log
|
|
113
|
+
jiraACP usage [--month YYYY-MM] # Token cost report per project
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Control
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
jiraACP pause <ticketKey>
|
|
120
|
+
jiraACP resume <ticketKey>
|
|
121
|
+
jiraACP abort <ticketKey> [--reason]
|
|
122
|
+
jiraACP cancel-clarification <ticketKey>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Config
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
jiraACP config get [<key>]
|
|
129
|
+
jiraACP config set <key> <value>
|
|
130
|
+
jiraACP config edit
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Schedule
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
jiraACP schedule add --cron "0 9 * * 1-5" --project <name>
|
|
137
|
+
jiraACP schedule list
|
|
138
|
+
jiraACP schedule remove <id>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Utilities
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
jiraACP serve [--port 3100] # Foreground webhook server (for dev/Docker)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
Configs live at `~/.jira-acp/`:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
~/.jira-acp/
|
|
155
|
+
├── config.json # Global shared settings (tokens, pipeline defaults)
|
|
156
|
+
└── projects/
|
|
157
|
+
└── my-project.json # Per-project overrides
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Global config (`~/.jira-acp/config.json`)
|
|
161
|
+
|
|
162
|
+
Shared settings across all projects:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"telegram": { "botToken": "env:TELEGRAM_BOT_TOKEN" },
|
|
167
|
+
"github": { "token": "env:GITHUB_TOKEN" },
|
|
168
|
+
"pipeline": {
|
|
169
|
+
"maxCostUsdPerRun": 2.0,
|
|
170
|
+
"skipClarificationIfClear": true,
|
|
171
|
+
"failOnDeployFailure": true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Project config (`~/.jira-acp/projects/<name>.json`)
|
|
177
|
+
|
|
178
|
+
Project-specific settings (override global):
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"name": "my-saas",
|
|
183
|
+
"jira": {
|
|
184
|
+
"url": "https://myteam.atlassian.net",
|
|
185
|
+
"email": "dev@myteam.com",
|
|
186
|
+
"token": "env:JIRA_TOKEN",
|
|
187
|
+
"projectKey": "PROJ",
|
|
188
|
+
"clarityScoreThreshold": 0.7
|
|
189
|
+
},
|
|
190
|
+
"github": {
|
|
191
|
+
"owner": "myorg",
|
|
192
|
+
"repo": "my-saas",
|
|
193
|
+
"defaultBranch": "main",
|
|
194
|
+
"autoMergeStrategy": "squash"
|
|
195
|
+
},
|
|
196
|
+
"workspace": {
|
|
197
|
+
"rootDir": "/path/to/codebase"
|
|
198
|
+
},
|
|
199
|
+
"telegram": {
|
|
200
|
+
"chatId": "-1001234567890"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Use `"env:VAR_NAME"` for any sensitive value — safe to commit the config file.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Human-in-the-Loop
|
|
210
|
+
|
|
211
|
+
jiraACP contacts you on Telegram when a decision is needed:
|
|
212
|
+
|
|
213
|
+
| Trigger | What you get |
|
|
214
|
+
|---------|-------------|
|
|
215
|
+
| Clarity score below threshold | Questions + `/answer PROJ-123` template |
|
|
216
|
+
| PR review: major issues | PR diff + Approve / Reject buttons |
|
|
217
|
+
| Merge conflict | Conflicting files + "I'll resolve" button |
|
|
218
|
+
| Tests fail after 3 retries | Playwright screenshot + error + Re-run button |
|
|
219
|
+
| Cost about to exceed limit | "Continue / Abort?" prompt |
|
|
220
|
+
|
|
221
|
+
**Clarification timeout flow** (default: 1 hour):
|
|
222
|
+
- T+30m — reminder
|
|
223
|
+
- T+45m — "Pipeline skips in 15 min"
|
|
224
|
+
- T+60m — execute `clarificationTimeoutAction` (`skip` / `abort` / `proceed-with-warning`)
|
|
225
|
+
|
|
226
|
+
### Telegram Commands
|
|
227
|
+
|
|
228
|
+
| Command | Action |
|
|
229
|
+
|---------|--------|
|
|
230
|
+
| `/run <ticketKey>` | Start pipeline for a ticket |
|
|
231
|
+
| `/abort <ticketKey>` | Abort running pipeline |
|
|
232
|
+
| `/resume <ticketKey>` | Resume paused pipeline |
|
|
233
|
+
| `/status` | Active pipeline states |
|
|
234
|
+
| `/logs` | Recent server log |
|
|
235
|
+
| `/tickets` | Open tickets grouped by release version |
|
|
236
|
+
| `/ticket <key>` | Ticket detail |
|
|
237
|
+
| `/projects` | Configured projects |
|
|
238
|
+
| `/archive <key>` | Close ticket topic |
|
|
239
|
+
| `/verbosity low\|medium\|high` | Notification verbosity |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Crash Recovery
|
|
244
|
+
|
|
245
|
+
Pipelines survive crashes. State is an append-only event log:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Process killed mid-way through Stage 4 (Code)
|
|
249
|
+
jiraACP resume PROJ-123
|
|
250
|
+
# Detects dead lock, offers: "Resume from 'code'? [Y/n]"
|
|
251
|
+
# Replays events, restores context, restarts from Stage 4
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
State stored per-ticket at `~/.jira-acp/runs/<project>/<ticketKey>/`.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Security
|
|
259
|
+
|
|
260
|
+
- **No plaintext secrets** — use `"env:VAR_NAME"` in config. `jiraACP doctor` warns on plaintext tokens.
|
|
261
|
+
- **Minimal env forwarding** — agents receive only `PATH`, `HOME`, `ANTHROPIC_API_KEY`, project vars. Never full `process.env`.
|
|
262
|
+
- **No shell injection** — all subprocess calls use `spawn(cmd, argsArray)`, never `exec(templateString)`.
|
|
263
|
+
- **Agent path scoping** — writes restricted to `workspace.allowedPaths`.
|
|
264
|
+
- **Telegram filtering** — only messages from configured `chatId` accepted.
|
|
265
|
+
- **Atomic locks** — `O_EXCL` open flag, crash-safe, no zombie pipelines.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Requirements
|
|
270
|
+
|
|
271
|
+
| Tool | Version | Purpose |
|
|
272
|
+
|------|---------|---------|
|
|
273
|
+
| Node.js | >= 20 | Runtime |
|
|
274
|
+
| [Claude Code](https://claude.ai/code) | latest | AI coding agent |
|
|
275
|
+
| Jira Cloud | — | Ticket source |
|
|
276
|
+
| GitHub | — | PR target |
|
|
277
|
+
| Telegram Bot | — | Notifications + human-in-the-loop |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readLockData
|
|
4
|
+
} from "./chunk-FLPIU2QO.js";
|
|
5
|
+
import {
|
|
6
|
+
createTelegramNotifier
|
|
7
|
+
} from "./chunk-PVKVCUNR.js";
|
|
8
|
+
import "./chunk-RZK74PDF.js";
|
|
9
|
+
import {
|
|
10
|
+
StateManager,
|
|
11
|
+
getLockPath,
|
|
12
|
+
getRunDir
|
|
13
|
+
} from "./chunk-VWBCDZWQ.js";
|
|
14
|
+
import {
|
|
15
|
+
loadConfig
|
|
16
|
+
} from "./chunk-3YHD4SIN.js";
|
|
17
|
+
import "./chunk-LIEW4ULF.js";
|
|
18
|
+
import "./chunk-3U373M37.js";
|
|
19
|
+
import "./chunk-OJ4CNF73.js";
|
|
20
|
+
import {
|
|
21
|
+
createLogger
|
|
22
|
+
} from "./chunk-IT74N3UH.js";
|
|
23
|
+
|
|
24
|
+
// src/commands/abort.ts
|
|
25
|
+
import fs from "fs";
|
|
26
|
+
async function abortPipeline(ticketKey, projectName, reason) {
|
|
27
|
+
const logger = createLogger("abort");
|
|
28
|
+
const config = loadConfig(projectName);
|
|
29
|
+
const runDir = getRunDir(projectName, ticketKey);
|
|
30
|
+
const lockPath = getLockPath(projectName, ticketKey);
|
|
31
|
+
const stateFile = `${runDir}/state.json`;
|
|
32
|
+
if (!fs.existsSync(stateFile)) {
|
|
33
|
+
logger.error({ ticketKey }, "No pipeline run found for ticket");
|
|
34
|
+
process.stderr.write(`No pipeline run found for ${ticketKey}
|
|
35
|
+
`);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const state = new StateManager(runDir).current;
|
|
40
|
+
if (state.isCompleted || state.isAborted) {
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
`Pipeline for ${ticketKey} is already ${state.isCompleted ? "completed" : "aborted"}
|
|
43
|
+
`
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const lockData = readLockData(lockPath);
|
|
48
|
+
const abortReason = reason ?? "Manually aborted by user";
|
|
49
|
+
if (lockData) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(lockData.pid, "SIGTERM");
|
|
52
|
+
logger.info(
|
|
53
|
+
{ ticketKey, pid: lockData.pid },
|
|
54
|
+
"SIGTERM sent to pipeline process"
|
|
55
|
+
);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const isESRCH = err instanceof Error && "code" in err && err.code === "ESRCH";
|
|
58
|
+
if (!isESRCH) throw err;
|
|
59
|
+
logger.warn(
|
|
60
|
+
{ ticketKey, pid: lockData.pid },
|
|
61
|
+
"Process already dead \u2014 recording abort"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
logger.warn(
|
|
66
|
+
{ ticketKey },
|
|
67
|
+
"No lock file found \u2014 recording abort event only"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const stateManager = new StateManager(runDir);
|
|
71
|
+
const currentState = stateManager.current;
|
|
72
|
+
if (!currentState.isAborted) {
|
|
73
|
+
stateManager.emit({ type: "PIPELINE_ABORTED", reason: abortReason });
|
|
74
|
+
logger.info(
|
|
75
|
+
{ ticketKey, reason: abortReason },
|
|
76
|
+
"PIPELINE_ABORTED event recorded"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const notifier = createTelegramNotifier(
|
|
81
|
+
config.telegram.botToken,
|
|
82
|
+
config.telegram.chatId,
|
|
83
|
+
ticketKey,
|
|
84
|
+
projectName,
|
|
85
|
+
config.telegram.topicId
|
|
86
|
+
);
|
|
87
|
+
await notifier.sendError(
|
|
88
|
+
ticketKey,
|
|
89
|
+
new Error(`Pipeline aborted: ${abortReason}`)
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
logger.warn(
|
|
93
|
+
{ ticketKey },
|
|
94
|
+
"Telegram notification failed during abort \u2014 continuing"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
process.stdout.write(`\u2713 Pipeline for ${ticketKey} aborted: ${abortReason}
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
abortPipeline
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=abort-GQE4OI5S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/abort.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { loadConfig } from \"../config/loader.js\";\nimport { getRunDir, getLockPath, StateManager } from \"../pipeline/state.js\";\nimport { readLockData } from \"../utils/lock.js\";\nimport { createLogger } from \"../utils/logger.js\";\nimport { createTelegramNotifier } from \"../integrations/telegram/notifier.js\";\n\nexport async function abortPipeline(\n ticketKey: string,\n projectName: string,\n reason?: string,\n): Promise<void> {\n const logger = createLogger(\"abort\");\n const config = loadConfig(projectName);\n\n const runDir = getRunDir(projectName, ticketKey);\n const lockPath = getLockPath(projectName, ticketKey);\n\n // Check state first\n const stateFile = `${runDir}/state.json`;\n if (!fs.existsSync(stateFile)) {\n logger.error({ ticketKey }, \"No pipeline run found for ticket\");\n process.stderr.write(`No pipeline run found for ${ticketKey}\\n`);\n process.exitCode = 1;\n return;\n }\n\n const state = new StateManager(runDir).current;\n if (state.isCompleted || state.isAborted) {\n process.stdout.write(\n `Pipeline for ${ticketKey} is already ${state.isCompleted ? \"completed\" : \"aborted\"}\\n`,\n );\n return;\n }\n\n // Read lock to get PID\n const lockData = readLockData(lockPath);\n const abortReason = reason ?? \"Manually aborted by user\";\n\n if (lockData) {\n try {\n process.kill(lockData.pid, \"SIGTERM\");\n logger.info(\n { ticketKey, pid: lockData.pid },\n \"SIGTERM sent to pipeline process\",\n );\n } catch (err: unknown) {\n // ESRCH = process not found — already dead\n const isESRCH =\n err instanceof Error &&\n \"code\" in err &&\n (err as NodeJS.ErrnoException).code === \"ESRCH\";\n if (!isESRCH) throw err;\n logger.warn(\n { ticketKey, pid: lockData.pid },\n \"Process already dead — recording abort\",\n );\n }\n } else {\n logger.warn(\n { ticketKey },\n \"No lock file found — recording abort event only\",\n );\n }\n\n // Emit abort event directly to state file (the killed process may not have time to)\n const stateManager = new StateManager(runDir);\n const currentState = stateManager.current;\n if (!currentState.isAborted) {\n stateManager.emit({ type: \"PIPELINE_ABORTED\", reason: abortReason });\n logger.info(\n { ticketKey, reason: abortReason },\n \"PIPELINE_ABORTED event recorded\",\n );\n }\n\n // Notify Telegram\n try {\n const notifier = createTelegramNotifier(\n config.telegram.botToken,\n config.telegram.chatId,\n ticketKey,\n projectName,\n config.telegram.topicId,\n );\n await notifier.sendError(\n ticketKey,\n new Error(`Pipeline aborted: ${abortReason}`),\n );\n } catch {\n logger.warn(\n { ticketKey },\n \"Telegram notification failed during abort — continuing\",\n );\n }\n\n process.stdout.write(`✓ Pipeline for ${ticketKey} aborted: ${abortReason}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AAOf,eAAsB,cACpB,WACA,aACA,QACe;AACf,QAAM,SAAS,aAAa,OAAO;AACnC,QAAM,SAAS,WAAW,WAAW;AAErC,QAAM,SAAS,UAAU,aAAa,SAAS;AAC/C,QAAM,WAAW,YAAY,aAAa,SAAS;AAGnD,QAAM,YAAY,GAAG,MAAM;AAC3B,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,WAAO,MAAM,EAAE,UAAU,GAAG,kCAAkC;AAC9D,YAAQ,OAAO,MAAM,6BAA6B,SAAS;AAAA,CAAI;AAC/D,YAAQ,WAAW;AACnB;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,aAAa,MAAM,EAAE;AACvC,MAAI,MAAM,eAAe,MAAM,WAAW;AACxC,YAAQ,OAAO;AAAA,MACb,gBAAgB,SAAS,eAAe,MAAM,cAAc,cAAc,SAAS;AAAA;AAAA,IACrF;AACA;AAAA,EACF;AAGA,QAAM,WAAW,aAAa,QAAQ;AACtC,QAAM,cAAc,UAAU;AAE9B,MAAI,UAAU;AACZ,QAAI;AACF,cAAQ,KAAK,SAAS,KAAK,SAAS;AACpC,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,SAAS,KAAc;AAErB,YAAM,UACJ,eAAe,SACf,UAAU,OACT,IAA8B,SAAS;AAC1C,UAAI,CAAC,QAAS,OAAM;AACpB,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AACL,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAGA,QAAM,eAAe,IAAI,aAAa,MAAM;AAC5C,QAAM,eAAe,aAAa;AAClC,MAAI,CAAC,aAAa,WAAW;AAC3B,iBAAa,KAAK,EAAE,MAAM,oBAAoB,QAAQ,YAAY,CAAC;AACnE,WAAO;AAAA,MACL,EAAE,WAAW,QAAQ,YAAY;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACF,UAAM,WAAW;AAAA,MACf,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB;AAAA,MACA;AAAA,MACA,OAAO,SAAS;AAAA,IAClB;AACA,UAAM,SAAS;AAAA,MACb;AAAA,MACA,IAAI,MAAM,qBAAqB,WAAW,EAAE;AAAA,IAC9C;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,uBAAkB,SAAS,aAAa,WAAW;AAAA,CAAI;AAC9E;","names":[]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig
|
|
3
|
+
} from "./chunk-MMWQHH25.js";
|
|
4
|
+
import {
|
|
5
|
+
createTelegramNotifier,
|
|
6
|
+
readLockData
|
|
7
|
+
} from "./chunk-WEJCTFQB.js";
|
|
8
|
+
import {
|
|
9
|
+
StateManager,
|
|
10
|
+
getLockPath,
|
|
11
|
+
getRunDir
|
|
12
|
+
} from "./chunk-BM4R6NST.js";
|
|
13
|
+
import {
|
|
14
|
+
createLogger
|
|
15
|
+
} from "./chunk-M4V3YOCY.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/abort.ts
|
|
18
|
+
import fs from "fs";
|
|
19
|
+
async function abortPipeline(ticketKey, projectName, reason) {
|
|
20
|
+
const logger = createLogger("abort");
|
|
21
|
+
const config = loadConfig(projectName);
|
|
22
|
+
const runDir = getRunDir(projectName, ticketKey);
|
|
23
|
+
const lockPath = getLockPath(projectName, ticketKey);
|
|
24
|
+
const stateFile = `${runDir}/state.json`;
|
|
25
|
+
if (!fs.existsSync(stateFile)) {
|
|
26
|
+
logger.error({ ticketKey }, "No pipeline run found for ticket");
|
|
27
|
+
process.stderr.write(`No pipeline run found for ${ticketKey}
|
|
28
|
+
`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const state = new StateManager(runDir).current;
|
|
33
|
+
if (state.isCompleted || state.isAborted) {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
`Pipeline for ${ticketKey} is already ${state.isCompleted ? "completed" : "aborted"}
|
|
36
|
+
`
|
|
37
|
+
);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const lockData = readLockData(lockPath);
|
|
41
|
+
const abortReason = reason ?? "Manually aborted by user";
|
|
42
|
+
if (lockData) {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(lockData.pid, "SIGTERM");
|
|
45
|
+
logger.info(
|
|
46
|
+
{ ticketKey, pid: lockData.pid },
|
|
47
|
+
"SIGTERM sent to pipeline process"
|
|
48
|
+
);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const isESRCH = err instanceof Error && "code" in err && err.code === "ESRCH";
|
|
51
|
+
if (!isESRCH) throw err;
|
|
52
|
+
logger.warn(
|
|
53
|
+
{ ticketKey, pid: lockData.pid },
|
|
54
|
+
"Process already dead \u2014 recording abort"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
logger.warn(
|
|
59
|
+
{ ticketKey },
|
|
60
|
+
"No lock file found \u2014 recording abort event only"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const stateManager = new StateManager(runDir);
|
|
64
|
+
const currentState = stateManager.current;
|
|
65
|
+
if (!currentState.isAborted) {
|
|
66
|
+
stateManager.emit({ type: "PIPELINE_ABORTED", reason: abortReason });
|
|
67
|
+
logger.info(
|
|
68
|
+
{ ticketKey, reason: abortReason },
|
|
69
|
+
"PIPELINE_ABORTED event recorded"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const notifier = createTelegramNotifier(
|
|
74
|
+
config.telegram.botToken,
|
|
75
|
+
config.telegram.chatId,
|
|
76
|
+
ticketKey,
|
|
77
|
+
projectName,
|
|
78
|
+
config.telegram.topicId
|
|
79
|
+
);
|
|
80
|
+
await notifier.sendError(
|
|
81
|
+
ticketKey,
|
|
82
|
+
new Error(`Pipeline aborted: ${abortReason}`)
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
logger.warn(
|
|
86
|
+
{ ticketKey },
|
|
87
|
+
"Telegram notification failed during abort \u2014 continuing"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write(`\u2713 Pipeline for ${ticketKey} aborted: ${abortReason}
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
abortPipeline
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=abort-VMRQOADY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/abort.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { loadConfig } from \"../config/loader.js\";\nimport { getRunDir, getLockPath, StateManager } from \"../pipeline/state.js\";\nimport { readLockData } from \"../utils/lock.js\";\nimport { createLogger } from \"../utils/logger.js\";\nimport { createTelegramNotifier } from \"../integrations/telegram/notifier.js\";\n\nexport async function abortPipeline(\n ticketKey: string,\n projectName: string,\n reason?: string,\n): Promise<void> {\n const logger = createLogger(\"abort\");\n const config = loadConfig(projectName);\n\n const runDir = getRunDir(projectName, ticketKey);\n const lockPath = getLockPath(projectName, ticketKey);\n\n // Check state first\n const stateFile = `${runDir}/state.json`;\n if (!fs.existsSync(stateFile)) {\n logger.error({ ticketKey }, \"No pipeline run found for ticket\");\n process.stderr.write(`No pipeline run found for ${ticketKey}\\n`);\n process.exitCode = 1;\n return;\n }\n\n const state = new StateManager(runDir).current;\n if (state.isCompleted || state.isAborted) {\n process.stdout.write(\n `Pipeline for ${ticketKey} is already ${state.isCompleted ? \"completed\" : \"aborted\"}\\n`,\n );\n return;\n }\n\n // Read lock to get PID\n const lockData = readLockData(lockPath);\n const abortReason = reason ?? \"Manually aborted by user\";\n\n if (lockData) {\n try {\n process.kill(lockData.pid, \"SIGTERM\");\n logger.info(\n { ticketKey, pid: lockData.pid },\n \"SIGTERM sent to pipeline process\",\n );\n } catch (err: unknown) {\n // ESRCH = process not found — already dead\n const isESRCH =\n err instanceof Error &&\n \"code\" in err &&\n (err as NodeJS.ErrnoException).code === \"ESRCH\";\n if (!isESRCH) throw err;\n logger.warn(\n { ticketKey, pid: lockData.pid },\n \"Process already dead — recording abort\",\n );\n }\n } else {\n logger.warn(\n { ticketKey },\n \"No lock file found — recording abort event only\",\n );\n }\n\n // Emit abort event directly to state file (the killed process may not have time to)\n const stateManager = new StateManager(runDir);\n const currentState = stateManager.current;\n if (!currentState.isAborted) {\n stateManager.emit({ type: \"PIPELINE_ABORTED\", reason: abortReason });\n logger.info(\n { ticketKey, reason: abortReason },\n \"PIPELINE_ABORTED event recorded\",\n );\n }\n\n // Notify Telegram\n try {\n const notifier = createTelegramNotifier(\n config.telegram.botToken,\n config.telegram.chatId,\n ticketKey,\n projectName,\n config.telegram.topicId,\n );\n await notifier.sendError(\n ticketKey,\n new Error(`Pipeline aborted: ${abortReason}`),\n );\n } catch {\n logger.warn(\n { ticketKey },\n \"Telegram notification failed during abort — continuing\",\n );\n }\n\n process.stdout.write(`✓ Pipeline for ${ticketKey} aborted: ${abortReason}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AAOf,eAAsB,cACpB,WACA,aACA,QACe;AACf,QAAM,SAAS,aAAa,OAAO;AACnC,QAAM,SAAS,WAAW,WAAW;AAErC,QAAM,SAAS,UAAU,aAAa,SAAS;AAC/C,QAAM,WAAW,YAAY,aAAa,SAAS;AAGnD,QAAM,YAAY,GAAG,MAAM;AAC3B,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,WAAO,MAAM,EAAE,UAAU,GAAG,kCAAkC;AAC9D,YAAQ,OAAO,MAAM,6BAA6B,SAAS;AAAA,CAAI;AAC/D,YAAQ,WAAW;AACnB;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,aAAa,MAAM,EAAE;AACvC,MAAI,MAAM,eAAe,MAAM,WAAW;AACxC,YAAQ,OAAO;AAAA,MACb,gBAAgB,SAAS,eAAe,MAAM,cAAc,cAAc,SAAS;AAAA;AAAA,IACrF;AACA;AAAA,EACF;AAGA,QAAM,WAAW,aAAa,QAAQ;AACtC,QAAM,cAAc,UAAU;AAE9B,MAAI,UAAU;AACZ,QAAI;AACF,cAAQ,KAAK,SAAS,KAAK,SAAS;AACpC,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,SAAS,KAAc;AAErB,YAAM,UACJ,eAAe,SACf,UAAU,OACT,IAA8B,SAAS;AAC1C,UAAI,CAAC,QAAS,OAAM;AACpB,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AACL,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAGA,QAAM,eAAe,IAAI,aAAa,MAAM;AAC5C,QAAM,eAAe,aAAa;AAClC,MAAI,CAAC,aAAa,WAAW;AAC3B,iBAAa,KAAK,EAAE,MAAM,oBAAoB,QAAQ,YAAY,CAAC;AACnE,WAAO;AAAA,MACL,EAAE,WAAW,QAAQ,YAAY;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACF,UAAM,WAAW;AAAA,MACf,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB;AAAA,MACA;AAAA,MACA,OAAO,SAAS;AAAA,IAClB;AACA,UAAM,SAAS;AAAA,MACb;AAAA,MACA,IAAI,MAAM,qBAAqB,WAAW,EAAE;AAAA,IAC9C;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,uBAAkB,SAAS,aAAa,WAAW;AAAA,CAAI;AAC9E;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
StateManager,
|
|
4
|
+
getRunDir
|
|
5
|
+
} from "./chunk-VWBCDZWQ.js";
|
|
6
|
+
import {
|
|
7
|
+
loadConfig
|
|
8
|
+
} from "./chunk-3YHD4SIN.js";
|
|
9
|
+
import "./chunk-LIEW4ULF.js";
|
|
10
|
+
import {
|
|
11
|
+
createLogger
|
|
12
|
+
} from "./chunk-IT74N3UH.js";
|
|
13
|
+
|
|
14
|
+
// src/commands/cancel-clarification.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import os from "os";
|
|
18
|
+
var PENDING_FILE = path.join(
|
|
19
|
+
os.homedir(),
|
|
20
|
+
".jira-acp",
|
|
21
|
+
"pending-clarifications.json"
|
|
22
|
+
);
|
|
23
|
+
async function cancelClarification(ticketKey, projectName) {
|
|
24
|
+
const logger = createLogger("cancel-clarification");
|
|
25
|
+
loadConfig(projectName);
|
|
26
|
+
let removed = false;
|
|
27
|
+
if (fs.existsSync(PENDING_FILE)) {
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(PENDING_FILE, "utf8"));
|
|
29
|
+
const filtered = raw.filter((e) => e.ticketKey !== ticketKey);
|
|
30
|
+
if (filtered.length < raw.length) {
|
|
31
|
+
const tmpPath = PENDING_FILE + ".tmp";
|
|
32
|
+
fs.writeFileSync(tmpPath, JSON.stringify(filtered, null, 2));
|
|
33
|
+
fs.renameSync(tmpPath, PENDING_FILE);
|
|
34
|
+
removed = true;
|
|
35
|
+
logger.info({ ticketKey }, "Removed from pending-clarifications");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!removed) {
|
|
39
|
+
process.stdout.write(`No pending clarification found for ${ticketKey}
|
|
40
|
+
`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const runDir = getRunDir(projectName, ticketKey);
|
|
44
|
+
if (fs.existsSync(runDir)) {
|
|
45
|
+
try {
|
|
46
|
+
new StateManager(runDir).emit({
|
|
47
|
+
type: "CLARIFICATION_RECEIVED",
|
|
48
|
+
answers: "CANCELLED"
|
|
49
|
+
});
|
|
50
|
+
logger.info({ ticketKey }, "CLARIFICATION_RECEIVED(CANCELLED) emitted");
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.warn(
|
|
53
|
+
{ ticketKey, err },
|
|
54
|
+
"Failed to emit CLARIFICATION_RECEIVED \u2014 non-fatal"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
process.stdout.write(`\u2713 Clarification for ${ticketKey} cancelled
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
cancelClarification
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=cancel-clarification-4G5S2HJZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/cancel-clarification.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { loadConfig } from \"../config/loader.js\";\nimport { getRunDir, StateManager } from \"../pipeline/state.js\";\nimport { createLogger } from \"../utils/logger.js\";\n\nconst PENDING_FILE = path.join(\n os.homedir(),\n \".jira-acp\",\n \"pending-clarifications.json\",\n);\n\nexport async function cancelClarification(\n ticketKey: string,\n projectName: string,\n): Promise<void> {\n const logger = createLogger(\"cancel-clarification\");\n loadConfig(projectName);\n\n let removed = false;\n if (fs.existsSync(PENDING_FILE)) {\n const raw = JSON.parse(fs.readFileSync(PENDING_FILE, \"utf8\")) as Array<{\n ticketKey: string;\n }>;\n const filtered = raw.filter((e) => e.ticketKey !== ticketKey);\n if (filtered.length < raw.length) {\n // Atomic write: write to temp file then rename to avoid race conditions\n const tmpPath = PENDING_FILE + \".tmp\";\n fs.writeFileSync(tmpPath, JSON.stringify(filtered, null, 2));\n fs.renameSync(tmpPath, PENDING_FILE);\n removed = true;\n logger.info({ ticketKey }, \"Removed from pending-clarifications\");\n }\n }\n\n if (!removed) {\n process.stdout.write(`No pending clarification found for ${ticketKey}\\n`);\n return;\n }\n\n // Emit CLARIFICATION_RECEIVED into state if run dir exists\n const runDir = getRunDir(projectName, ticketKey);\n if (fs.existsSync(runDir)) {\n try {\n new StateManager(runDir).emit({\n type: \"CLARIFICATION_RECEIVED\",\n answers: \"CANCELLED\",\n });\n logger.info({ ticketKey }, \"CLARIFICATION_RECEIVED(CANCELLED) emitted\");\n } catch (err) {\n logger.warn(\n { ticketKey, err },\n \"Failed to emit CLARIFICATION_RECEIVED — non-fatal\",\n );\n }\n }\n\n process.stdout.write(`✓ Clarification for ${ticketKey} cancelled\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAKf,IAAM,eAAe,KAAK;AAAA,EACxB,GAAG,QAAQ;AAAA,EACX;AAAA,EACA;AACF;AAEA,eAAsB,oBACpB,WACA,aACe;AACf,QAAM,SAAS,aAAa,sBAAsB;AAClD,aAAW,WAAW;AAEtB,MAAI,UAAU;AACd,MAAI,GAAG,WAAW,YAAY,GAAG;AAC/B,UAAM,MAAM,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAG5D,UAAM,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,cAAc,SAAS;AAC5D,QAAI,SAAS,SAAS,IAAI,QAAQ;AAEhC,YAAM,UAAU,eAAe;AAC/B,SAAG,cAAc,SAAS,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAC3D,SAAG,WAAW,SAAS,YAAY;AACnC,gBAAU;AACV,aAAO,KAAK,EAAE,UAAU,GAAG,qCAAqC;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AACZ,YAAQ,OAAO,MAAM,sCAAsC,SAAS;AAAA,CAAI;AACxE;AAAA,EACF;AAGA,QAAM,SAAS,UAAU,aAAa,SAAS;AAC/C,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,QAAI;AACF,UAAI,aAAa,MAAM,EAAE,KAAK;AAAA,QAC5B,MAAM;AAAA,QACN,SAAS;AAAA,MACX,CAAC;AACD,aAAO,KAAK,EAAE,UAAU,GAAG,2CAA2C;AAAA,IACxE,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,EAAE,WAAW,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,4BAAuB,SAAS;AAAA,CAAc;AACrE;","names":[]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/integrations/telegram/topic-manager.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
var STORE_PATH = path.join(os.homedir(), ".jira-acp", "telegram-topics.json");
|
|
8
|
+
function loadStore() {
|
|
9
|
+
if (!fs.existsSync(STORE_PATH)) return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function saveStore(store) {
|
|
17
|
+
fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
|
|
18
|
+
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
|
|
19
|
+
}
|
|
20
|
+
async function getOrCreateTopic(bot, chatId, ticketKey, projectName) {
|
|
21
|
+
const store = loadStore();
|
|
22
|
+
const key = `${projectName}:${ticketKey}`;
|
|
23
|
+
if (store[key] !== void 0) return store[key];
|
|
24
|
+
try {
|
|
25
|
+
const topic = await bot.api.createForumTopic(chatId, ticketKey);
|
|
26
|
+
store[key] = topic.message_thread_id;
|
|
27
|
+
saveStore(store);
|
|
28
|
+
return topic.message_thread_id;
|
|
29
|
+
} catch {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function getOrCreateSystemTopic(bot, chatId, projectName) {
|
|
34
|
+
const store = loadStore();
|
|
35
|
+
const key = `${projectName}:__notifications__`;
|
|
36
|
+
if (store[key] !== void 0) return store[key];
|
|
37
|
+
try {
|
|
38
|
+
const topic = await bot.api.createForumTopic(chatId, "\u{1F514} Notifications", {
|
|
39
|
+
icon_color: 7322096
|
|
40
|
+
// light blue
|
|
41
|
+
});
|
|
42
|
+
store[key] = topic.message_thread_id;
|
|
43
|
+
saveStore(store);
|
|
44
|
+
return topic.message_thread_id;
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function archiveTopic(bot, chatId, ticketKey, projectName) {
|
|
50
|
+
const store = loadStore();
|
|
51
|
+
const key = `${projectName}:${ticketKey}`;
|
|
52
|
+
const threadId = store[key];
|
|
53
|
+
if (threadId === void 0) return;
|
|
54
|
+
try {
|
|
55
|
+
await bot.api.closeForumTopic(chatId, threadId);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
delete store[key];
|
|
59
|
+
saveStore(store);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
getOrCreateTopic,
|
|
64
|
+
getOrCreateSystemTopic,
|
|
65
|
+
archiveTopic
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=chunk-3U373M37.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/integrations/telegram/topic-manager.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport type { Bot } from \"grammy\";\n\nconst STORE_PATH = path.join(os.homedir(), \".jira-acp\", \"telegram-topics.json\");\n\ntype TopicStore = Record<string, number>; // \"projectName:ticketKey\" => message_thread_id\n\nfunction loadStore(): TopicStore {\n if (!fs.existsSync(STORE_PATH)) return {};\n try {\n return JSON.parse(fs.readFileSync(STORE_PATH, \"utf8\")) as TopicStore;\n } catch {\n return {};\n }\n}\n\nfunction saveStore(store: TopicStore): void {\n fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });\n fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));\n}\n\nexport async function getOrCreateTopic(\n bot: Bot,\n chatId: number | string,\n ticketKey: string,\n projectName: string,\n): Promise<number | undefined> {\n const store = loadStore();\n const key = `${projectName}:${ticketKey}`;\n\n if (store[key] !== undefined) return store[key];\n\n try {\n const topic = await bot.api.createForumTopic(chatId, ticketKey);\n store[key] = topic.message_thread_id;\n saveStore(store);\n return topic.message_thread_id;\n } catch {\n // Chat is not a forum supergroup — topic creation not supported\n return undefined;\n }\n}\n\n/** Get or create a persistent \"🔔 Notifications\" topic for system-level messages. */\nexport async function getOrCreateSystemTopic(\n bot: Bot,\n chatId: number | string,\n projectName: string,\n): Promise<number | undefined> {\n const store = loadStore();\n const key = `${projectName}:__notifications__`;\n\n if (store[key] !== undefined) return store[key];\n\n try {\n const topic = await bot.api.createForumTopic(chatId, \"🔔 Notifications\", {\n icon_color: 0x6fb9f0, // light blue\n });\n store[key] = topic.message_thread_id;\n saveStore(store);\n return topic.message_thread_id;\n } catch {\n return undefined;\n }\n}\n\nexport async function archiveTopic(\n bot: Bot,\n chatId: number | string,\n ticketKey: string,\n projectName: string,\n): Promise<void> {\n const store = loadStore();\n const key = `${projectName}:${ticketKey}`;\n const threadId = store[key];\n if (threadId === undefined) return;\n\n try {\n await bot.api.closeForumTopic(chatId, threadId);\n } catch {\n // Topic may already be closed or deleted\n }\n\n delete store[key];\n saveStore(store);\n}\n"],"mappings":";;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAGf,IAAM,aAAa,KAAK,KAAK,GAAG,QAAQ,GAAG,aAAa,sBAAsB;AAI9E,SAAS,YAAwB;AAC/B,MAAI,CAAC,GAAG,WAAW,UAAU,EAAG,QAAO,CAAC;AACxC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG,aAAa,YAAY,MAAM,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,UAAU,OAAyB;AAC1C,KAAG,UAAU,KAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,KAAG,cAAc,YAAY,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAC7D;AAEA,eAAsB,iBACpB,KACA,QACA,WACA,aAC6B;AAC7B,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,GAAG,WAAW,IAAI,SAAS;AAEvC,MAAI,MAAM,GAAG,MAAM,OAAW,QAAO,MAAM,GAAG;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,IAAI,iBAAiB,QAAQ,SAAS;AAC9D,UAAM,GAAG,IAAI,MAAM;AACnB,cAAU,KAAK;AACf,WAAO,MAAM;AAAA,EACf,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,uBACpB,KACA,QACA,aAC6B;AAC7B,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,GAAG,WAAW;AAE1B,MAAI,MAAM,GAAG,MAAM,OAAW,QAAO,MAAM,GAAG;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,IAAI,iBAAiB,QAAQ,2BAAoB;AAAA,MACvE,YAAY;AAAA;AAAA,IACd,CAAC;AACD,UAAM,GAAG,IAAI,MAAM;AACnB,cAAU,KAAK;AACf,WAAO,MAAM;AAAA,EACf,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,aACpB,KACA,QACA,WACA,aACe;AACf,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,GAAG,WAAW,IAAI,SAAS;AACvC,QAAM,WAAW,MAAM,GAAG;AAC1B,MAAI,aAAa,OAAW;AAE5B,MAAI;AACF,UAAM,IAAI,IAAI,gBAAgB,QAAQ,QAAQ;AAAA,EAChD,QAAQ;AAAA,EAER;AAEA,SAAO,MAAM,GAAG;AAChB,YAAU,KAAK;AACjB;","names":[]}
|