acdev 1.0.2 → 1.0.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/.acdev/.env.example +8 -2
- package/README.md +66 -24
- package/bin/acdev.js +13 -25
- package/package.json +1 -1
- package/public/app.js +469 -93
- package/public/index.html +201 -63
- package/public/styles.css +105 -21
- package/src/afterPrRules.js +63 -8
- package/src/claude-auth.js +30 -0
- package/src/config.js +130 -88
- package/src/env.js +1 -1
- package/src/gh-auth.js +123 -6
- package/src/jira.js +171 -0
- package/src/models.js +366 -0
- package/src/server.js +142 -13
package/src/server.js
CHANGED
|
@@ -33,8 +33,11 @@ import {
|
|
|
33
33
|
} from './agent.js';
|
|
34
34
|
import { publicConfig, updateConfig } from './config.js';
|
|
35
35
|
import { upsertEnvVars } from './env.js';
|
|
36
|
+
import { listModels } from './models.js';
|
|
36
37
|
import { splitIssueUrls } from './urls.js';
|
|
37
38
|
import { usageFromLogs, withJobUsage } from './usage.js';
|
|
39
|
+
import { checkGhAuth } from './gh-auth.js';
|
|
40
|
+
import { checkClaudeAuth } from './claude-auth.js';
|
|
38
41
|
|
|
39
42
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
40
43
|
|
|
@@ -76,6 +79,23 @@ function jobDedupeKey(job) {
|
|
|
76
79
|
}
|
|
77
80
|
return `github:${job.issueUrl}`;
|
|
78
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Empty string = leave the existing secret unchanged; null = clear.
|
|
85
|
+
* @param {Record<string, string | null>} envPatch
|
|
86
|
+
* @param {string} envKey
|
|
87
|
+
* @param {unknown} value
|
|
88
|
+
*/
|
|
89
|
+
function applySecretField(envPatch, envKey, value) {
|
|
90
|
+
if (value === undefined) return;
|
|
91
|
+
if (value === null) {
|
|
92
|
+
envPatch[envKey] = null;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === 'string' && value.trim()) {
|
|
96
|
+
envPatch[envKey] = value.trim();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
79
99
|
/**
|
|
80
100
|
* Normalize + validate POST /api/jobs/:id/review body.
|
|
81
101
|
* @param {unknown} body
|
|
@@ -146,9 +166,13 @@ export function normalizeReviewComments(body) {
|
|
|
146
166
|
* listChangedFiles?: typeof listChangedFiles,
|
|
147
167
|
* applyFileExclusions?: typeof applyFileExclusions,
|
|
148
168
|
* transitionJiraIssue?: Function,
|
|
169
|
+
* addJiraIssueLabel?: Function,
|
|
170
|
+
* closeJiraIssue?: Function,
|
|
149
171
|
* addIssueLabel?: Function,
|
|
150
172
|
* closeIssue?: Function,
|
|
151
173
|
* resolveJiraCredentials?: Function,
|
|
174
|
+
* checkGhAuth?: typeof checkGhAuth,
|
|
175
|
+
* checkClaudeAuth?: typeof checkClaudeAuth,
|
|
152
176
|
* },
|
|
153
177
|
* }} options
|
|
154
178
|
*/
|
|
@@ -162,6 +186,52 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
162
186
|
const doGetDiff = deps.getDiff || getDiff;
|
|
163
187
|
const doListChangedFiles = deps.listChangedFiles || listChangedFiles;
|
|
164
188
|
const doApplyFileExclusions = deps.applyFileExclusions || applyFileExclusions;
|
|
189
|
+
const doCheckGhAuth = deps.checkGhAuth || checkGhAuth;
|
|
190
|
+
const doCheckClaudeAuth = deps.checkClaudeAuth || checkClaudeAuth;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reject enqueue / agent / PR actions when required auth is missing.
|
|
194
|
+
* @param {{ needGh?: boolean, needClaude?: boolean }} [opts]
|
|
195
|
+
* @returns {{ status: number, error: string, code: string } | null}
|
|
196
|
+
*/
|
|
197
|
+
function authGate(opts = {}) {
|
|
198
|
+
const needGh = opts.needGh !== false;
|
|
199
|
+
const needClaude = opts.needClaude === true && !useStubAgent;
|
|
200
|
+
|
|
201
|
+
if (needGh) {
|
|
202
|
+
const gh = doCheckGhAuth();
|
|
203
|
+
if (!gh.ok) {
|
|
204
|
+
const error =
|
|
205
|
+
gh.reason === 'not-found'
|
|
206
|
+
? 'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/, then restart acdev.'
|
|
207
|
+
: 'GitHub is not authenticated. Configure a PAT in Settings → Authentication, or run gh auth login.';
|
|
208
|
+
return { status: 400, error, code: 'gh_auth_required' };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (needClaude) {
|
|
213
|
+
const claude = doCheckClaudeAuth();
|
|
214
|
+
if (!claude.ok) {
|
|
215
|
+
return {
|
|
216
|
+
status: 400,
|
|
217
|
+
error:
|
|
218
|
+
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
219
|
+
code: 'claude_auth_required',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function publicConfigPayload() {
|
|
228
|
+
return publicConfig(config, {
|
|
229
|
+
repoRoot,
|
|
230
|
+
stubAgent: useStubAgent,
|
|
231
|
+
ghAuth: doCheckGhAuth(),
|
|
232
|
+
claudeAuth: doCheckClaudeAuth(),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
165
235
|
|
|
166
236
|
/** @type {Map<string, Set<import('http').ServerResponse>>} */
|
|
167
237
|
const subscribers = new Map();
|
|
@@ -201,6 +271,17 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
201
271
|
let job = store.getJob(jobId);
|
|
202
272
|
if (!job) return;
|
|
203
273
|
|
|
274
|
+
const gate = authGate({ needGh: true, needClaude: true });
|
|
275
|
+
if (gate) {
|
|
276
|
+
appendLog(job, 'error', gate.error);
|
|
277
|
+
store.updateJob(jobId, { status: 'failed', error: gate.error });
|
|
278
|
+
const updated = store.getJob(jobId);
|
|
279
|
+
if (updated?.logs?.length) {
|
|
280
|
+
emitEvent(jobId, updated.logs[updated.logs.length - 1]);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
204
285
|
try {
|
|
205
286
|
job = setStatus(jobId, 'syncing');
|
|
206
287
|
if (!job) return;
|
|
@@ -329,6 +410,21 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
329
410
|
let job = store.getJob(jobId);
|
|
330
411
|
if (!job || job.status !== 'applying_feedback') return;
|
|
331
412
|
|
|
413
|
+
const gate = authGate({ needGh: false, needClaude: true });
|
|
414
|
+
if (gate) {
|
|
415
|
+
appendLog(job, 'error', gate.error);
|
|
416
|
+
store.updateJob(jobId, {
|
|
417
|
+
status: 'failed',
|
|
418
|
+
error: gate.error,
|
|
419
|
+
pendingReviewFeedback: undefined,
|
|
420
|
+
});
|
|
421
|
+
const updated = store.getJob(jobId);
|
|
422
|
+
if (updated?.logs?.length) {
|
|
423
|
+
emitEvent(jobId, updated.logs[updated.logs.length - 1]);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
332
428
|
const feedback = job.pendingReviewFeedback;
|
|
333
429
|
if (!feedback || (!feedback.generalComment && !(feedback.lineComments || []).length)) {
|
|
334
430
|
const message = 'Missing review feedback payload';
|
|
@@ -441,7 +537,20 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
441
537
|
|
|
442
538
|
app.get('/api/config', (_req, res) => {
|
|
443
539
|
try {
|
|
444
|
-
res.json(
|
|
540
|
+
res.json(publicConfigPayload());
|
|
541
|
+
} catch (err) {
|
|
542
|
+
res.status(500).json({ error: err.message });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
app.get('/api/models', async (req, res) => {
|
|
547
|
+
try {
|
|
548
|
+
const force =
|
|
549
|
+
req.query.refresh === '1' ||
|
|
550
|
+
req.query.refresh === 'true' ||
|
|
551
|
+
req.query.force === '1';
|
|
552
|
+
const result = await listModels({ selected: config.model, force });
|
|
553
|
+
res.json(result);
|
|
445
554
|
} catch (err) {
|
|
446
555
|
res.status(500).json({ error: err.message });
|
|
447
556
|
}
|
|
@@ -451,7 +560,8 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
451
560
|
try {
|
|
452
561
|
const patch = req.body || {};
|
|
453
562
|
|
|
454
|
-
//
|
|
563
|
+
// Secrets → .acdev/.env (never persisted in config.json).
|
|
564
|
+
// Empty string = leave unchanged; null = clear.
|
|
455
565
|
/** @type {Record<string, string | null>} */
|
|
456
566
|
const envPatch = {};
|
|
457
567
|
if (patch.jiraEmail !== undefined) {
|
|
@@ -459,16 +569,10 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
459
569
|
typeof patch.jiraEmail === 'string' ? patch.jiraEmail.trim() : '';
|
|
460
570
|
envPatch.JIRA_EMAIL = email || null;
|
|
461
571
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (patch.jiraApiToken === null) {
|
|
467
|
-
envPatch.JIRA_API_TOKEN = null;
|
|
468
|
-
} else if (token) {
|
|
469
|
-
envPatch.JIRA_API_TOKEN = token;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
572
|
+
applySecretField(envPatch, 'JIRA_API_TOKEN', patch.jiraApiToken);
|
|
573
|
+
applySecretField(envPatch, 'GH_TOKEN', patch.ghToken);
|
|
574
|
+
applySecretField(envPatch, 'ANTHROPIC_API_KEY', patch.anthropicApiKey);
|
|
575
|
+
applySecretField(envPatch, 'CLAUDE_CODE_OAUTH_TOKEN', patch.claudeOauthToken);
|
|
472
576
|
if (patch.jiraBaseUrl !== undefined && typeof patch.jiraBaseUrl === 'string') {
|
|
473
577
|
// Also mirror base URL into env for convenience when set via Settings
|
|
474
578
|
const trimmed = patch.jiraBaseUrl.trim();
|
|
@@ -483,10 +587,13 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
483
587
|
const {
|
|
484
588
|
jiraEmail: _e,
|
|
485
589
|
jiraApiToken: _t,
|
|
590
|
+
ghToken: _gh,
|
|
591
|
+
anthropicApiKey: _ak,
|
|
592
|
+
claudeOauthToken: _oa,
|
|
486
593
|
...configPatch
|
|
487
594
|
} = patch;
|
|
488
595
|
updateConfig(repoRoot, config, configPatch);
|
|
489
|
-
res.json(
|
|
596
|
+
res.json(publicConfigPayload());
|
|
490
597
|
} catch (err) {
|
|
491
598
|
res.status(400).json({ error: err.message });
|
|
492
599
|
}
|
|
@@ -516,6 +623,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
516
623
|
|
|
517
624
|
app.post('/api/issues', (req, res) => {
|
|
518
625
|
try {
|
|
626
|
+
const gate = authGate({ needGh: true, needClaude: true });
|
|
627
|
+
if (gate) {
|
|
628
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
629
|
+
}
|
|
630
|
+
|
|
519
631
|
const urls = splitIssueUrls(req.body?.urls);
|
|
520
632
|
if (urls.length === 0) {
|
|
521
633
|
return res.status(400).json({
|
|
@@ -691,6 +803,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
691
803
|
|
|
692
804
|
app.post('/api/jobs/:id/review', (req, res) => {
|
|
693
805
|
try {
|
|
806
|
+
const gate = authGate({ needGh: false, needClaude: true });
|
|
807
|
+
if (gate) {
|
|
808
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
809
|
+
}
|
|
810
|
+
|
|
694
811
|
const job = store.getJob(req.params.id);
|
|
695
812
|
if (!job) {
|
|
696
813
|
return res.status(404).json({ error: 'Job not found' });
|
|
@@ -736,6 +853,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
736
853
|
|
|
737
854
|
app.post('/api/jobs/:id/approve', async (req, res) => {
|
|
738
855
|
try {
|
|
856
|
+
const gate = authGate({ needGh: true, needClaude: false });
|
|
857
|
+
if (gate) {
|
|
858
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
859
|
+
}
|
|
860
|
+
|
|
739
861
|
const job = store.getJob(req.params.id);
|
|
740
862
|
if (!job) {
|
|
741
863
|
return res.status(404).json({ error: 'Job not found' });
|
|
@@ -814,6 +936,8 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
814
936
|
appendLog: (j, type, payload) => appendLog(j, type, payload),
|
|
815
937
|
deps: {
|
|
816
938
|
transitionJiraIssue: deps.transitionJiraIssue,
|
|
939
|
+
addJiraIssueLabel: deps.addJiraIssueLabel,
|
|
940
|
+
closeJiraIssue: deps.closeJiraIssue,
|
|
817
941
|
addIssueLabel: deps.addIssueLabel,
|
|
818
942
|
closeIssue: deps.closeIssue,
|
|
819
943
|
resolveJiraCredentials: deps.resolveJiraCredentials,
|
|
@@ -912,6 +1036,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
912
1036
|
|
|
913
1037
|
app.post('/api/jobs/:id/retry', (req, res) => {
|
|
914
1038
|
try {
|
|
1039
|
+
const gate = authGate({ needGh: true, needClaude: true });
|
|
1040
|
+
if (gate) {
|
|
1041
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
1042
|
+
}
|
|
1043
|
+
|
|
915
1044
|
const job = store.getJob(req.params.id);
|
|
916
1045
|
if (!job) {
|
|
917
1046
|
return res.status(404).json({ error: 'Job not found' });
|