acdev 1.0.13 → 1.0.15

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/src/server.js CHANGED
@@ -3,13 +3,14 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { Store } from './store.js';
6
- import { parseIssueUrl, createPr, fetchIssueDetails } from './github.js';
6
+ import { parseIssueUrl, createPr, fetchGithubPrStatus, fetchIssueDetails, listGithubIssueStatuses } from './github.js';
7
7
  import {
8
8
  parseJiraIssueRef,
9
9
  resolveJiraCredentials,
10
10
  testJiraConnection,
11
11
  fetchJiraIssue,
12
12
  mapJiraIssueType,
13
+ listJiraBoardStatuses,
13
14
  } from './jira.js';
14
15
  import { applyAfterPrOpenedRules } from './afterPrRules.js';
15
16
  import {
@@ -43,7 +44,7 @@ import { upsertEnvVars } from './env.js';
43
44
  import { listModels } from './models.js';
44
45
  import { splitIssueUrls } from './urls.js';
45
46
  import { usageFromLogs, withJobUsage, snapshotJobLlm, tagUsageProvider } from './usage.js';
46
- import { checkGhAuth } from './gh-auth.js';
47
+ import { checkGhAuth, originRemoteInfo } from './gh-auth.js';
47
48
  import { checkClaudeAuth } from './claude-auth.js';
48
49
  import { checkOpenRouterAuth } from './openrouter-auth.js';
49
50
  import { isValidModelId, isNoModel, NO_MODEL, isModelIdForProvider } from './models.js';
@@ -171,6 +172,8 @@ export function normalizeReviewComments(body) {
171
172
  * deps?: {
172
173
  * pushBranch?: typeof pushBranch,
173
174
  * createPr?: typeof createPr,
175
+ * fetchGithubPrStatus?: typeof fetchGithubPrStatus,
176
+ * removeWorktree?: typeof removeWorktree,
174
177
  * getDiff?: typeof getDiff,
175
178
  * listChangedFiles?: typeof listChangedFiles,
176
179
  * applyFileExclusions?: typeof applyFileExclusions,
@@ -182,6 +185,8 @@ export function normalizeReviewComments(body) {
182
185
  * resolveJiraCredentials?: Function,
183
186
  * checkGhAuth?: typeof checkGhAuth,
184
187
  * checkClaudeAuth?: typeof checkClaudeAuth,
188
+ * listJiraBoardStatuses?: typeof listJiraBoardStatuses,
189
+ * listGithubIssueStatuses?: typeof listGithubIssueStatuses,
185
190
  * },
186
191
  * }} options
187
192
  */
@@ -189,14 +194,36 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
189
194
  const app = express();
190
195
  const publicDir = path.join(__dirname, '..', 'public');
191
196
  app.use(express.json());
192
-
193
197
  const doPushBranch = deps.pushBranch || pushBranch;
194
198
  const doCreatePr = deps.createPr || createPr;
199
+ const doRemoveWorktree = deps.removeWorktree || removeWorktree;
195
200
  const doGetDiff = deps.getDiff || getDiff;
196
201
  const doListChangedFiles = deps.listChangedFiles || listChangedFiles;
197
202
  const doApplyFileExclusions = deps.applyFileExclusions || applyFileExclusions;
203
+ const doFetchGithubPrStatus = deps.fetchGithubPrStatus || fetchGithubPrStatus;
198
204
  const doCheckGhAuth = deps.checkGhAuth || checkGhAuth;
199
205
  const doCheckClaudeAuth = deps.checkClaudeAuth || checkClaudeAuth;
206
+ const doListJiraBoardStatuses = deps.listJiraBoardStatuses || listJiraBoardStatuses;
207
+ const doListGithubIssueStatuses = deps.listGithubIssueStatuses || listGithubIssueStatuses;
208
+
209
+ /** @type {Map<string, { at: number, value: object }>} */
210
+ const prStatusCache = new Map();
211
+
212
+ async function getPrStatusForJob(job) {
213
+ const prUrl = String(job?.prUrl || '').trim();
214
+ if (!prUrl) return null;
215
+
216
+ const cached = prStatusCache.get(prUrl);
217
+ const now = Date.now();
218
+ if (cached && now - cached.at < 15_000) {
219
+ return cached.value;
220
+ }
221
+
222
+ const result = await doFetchGithubPrStatus({ prUrl, cwd: repoRoot });
223
+ const value = result.ok ? { ok: true, ...result } : result;
224
+ prStatusCache.set(prUrl, { at: now, value });
225
+ return value;
226
+ }
200
227
 
201
228
  function formatAgentJobError(err) {
202
229
  return err instanceof Error ? err.message : String(err);
@@ -608,10 +635,16 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
608
635
  }
609
636
  }
610
637
 
611
- app.get('/api/jobs', (_req, res) => {
638
+ app.get('/api/jobs', async (_req, res) => {
612
639
  try {
613
- // Backfill usage from logs for older jobs that never got a first-class field.
614
- res.json(store.getJobs().map(withJobUsage));
640
+ const jobs = await Promise.all(
641
+ store.getJobs().map(async (job) => {
642
+ const withUsage = withJobUsage(job);
643
+ const prStatus = await getPrStatusForJob(withUsage);
644
+ return prStatus ? { ...withUsage, prStatus } : withUsage;
645
+ })
646
+ );
647
+ res.json(jobs);
615
648
  } catch (err) {
616
649
  res.status(500).json({ error: err.message });
617
650
  }
@@ -731,6 +764,54 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
731
764
  }
732
765
  });
733
766
 
767
+ app.get('/api/jira/statuses', async (_req, res) => {
768
+ try {
769
+ const creds = resolveJiraCredentials({ configBaseUrl: config.jiraBaseUrl });
770
+ if ('error' in creds) {
771
+ return res.status(400).json({ ok: false, error: creds.error, statuses: [] });
772
+ }
773
+ const result = await doListJiraBoardStatuses(creds);
774
+ if (!result.ok) {
775
+ return res.status(400).json(result);
776
+ }
777
+ res.json(result);
778
+ } catch (err) {
779
+ res.status(500).json({
780
+ ok: false,
781
+ error: err instanceof Error ? err.message : String(err),
782
+ statuses: [],
783
+ });
784
+ }
785
+ });
786
+
787
+ app.get('/api/github/statuses', async (_req, res) => {
788
+ try {
789
+ const gh = doCheckGhAuth();
790
+ if (!gh.ok) {
791
+ const error =
792
+ gh.reason === 'not-found'
793
+ ? 'GitHub CLI (gh) is not installed.'
794
+ : 'GitHub is not authenticated. Configure a PAT in Settings → Authentication, or run gh auth login.';
795
+ return res.status(400).json({ ok: false, error, statuses: [] });
796
+ }
797
+ const origin = originRemoteInfo(repoRoot);
798
+ const result = await doListGithubIssueStatuses({
799
+ cwd: repoRoot,
800
+ originUrl: origin.url,
801
+ });
802
+ if (!result.ok) {
803
+ return res.status(400).json(result);
804
+ }
805
+ res.json(result);
806
+ } catch (err) {
807
+ res.status(500).json({
808
+ ok: false,
809
+ error: err instanceof Error ? err.message : String(err),
810
+ statuses: [],
811
+ });
812
+ }
813
+ });
814
+
734
815
  app.post('/api/issues', (req, res) => {
735
816
  try {
736
817
  const gate = authGate({ needGh: true, needLlm: true });
@@ -1076,7 +1157,23 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
1076
1157
  );
1077
1158
  }
1078
1159
 
1079
- res.json(store.getJob(updated.id) || updated);
1160
+ const refreshed = store.getJob(updated.id) || updated;
1161
+ const wtId = worktreeIdForJob(refreshed);
1162
+ if (wtId != null && refreshed.worktreePath) {
1163
+ try {
1164
+ await doRemoveWorktree(repoRoot, wtId, refreshed.branchName);
1165
+ } catch (cleanupErr) {
1166
+ console.warn(
1167
+ `[acdev] removeWorktree after PR open failed for job ${refreshed.id}:`,
1168
+ cleanupErr instanceof Error ? cleanupErr.message : cleanupErr
1169
+ );
1170
+ }
1171
+ store.updateJob(refreshed.id, { worktreePath: undefined });
1172
+ }
1173
+
1174
+ const finalJob = withJobUsage(store.getJob(updated.id) || updated);
1175
+ const finalPrStatus = await getPrStatusForJob(finalJob);
1176
+ res.json(finalPrStatus ? { ...finalJob, prStatus: finalPrStatus } : finalJob);
1080
1177
  } catch (err) {
1081
1178
  const message = err instanceof Error ? err.message : String(err);
1082
1179
  store.updateJob(job.id, { status: 'failed', error: message });