acdev 1.0.3 → 1.0.5

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 CHANGED
@@ -5,11 +5,13 @@ Local CLI + web UI for running AI coding agents on GitHub Issues or Jira tickets
5
5
  ## Prerequisites
6
6
 
7
7
  - Node.js 20+
8
- - [GitHub CLI](https://cli.github.com/) (`gh`) — required for PRs (and for GitHub Issues when that is the ticket source). Authenticate with `gh auth login` **or** a PAT in Settings / `GH_TOKEN` (see [Authentication](#authentication))
9
- - Claude authentication (see [Authentication](#authentication) below)
8
+ - [GitHub CLI](https://cli.github.com/) (`gh`) on your `PATH` — required to fetch GitHub issues and open PRs. You can install `gh` first and authenticate later (see below).
10
9
  - A git repository with a `develop` branch on `origin` (or configure another base branch)
10
+ - Optional: Claude authentication for real agent runs (or use `--stub-agent` for UI-only testing)
11
11
  - Optional: Jira Cloud credentials when using Jira as the ticket source (see [Jira](#jira-ticket-source))
12
12
 
13
+ You do **not** need GitHub or Claude tokens before starting `acdev`. The server always starts (as long as you are inside a git repo); configure auth in **Settings → Authentication**, then enqueue jobs.
14
+
13
15
  ## Install
14
16
 
15
17
  ```bash
@@ -27,7 +29,9 @@ npm link # optional — makes `acdev` available globally
27
29
 
28
30
  ## Authentication
29
31
 
30
- Configure GitHub and Claude from **Settings** in the web UI (tokens are written to `.acdev/.env`, gitignored) or by editing that file directly. Leave a Settings field blank to keep the current secret. Shell environment variables override `.env` on startup; saving in Settings updates the running process immediately.
32
+ `acdev` starts even when auth is missing and prints a warning. Configure GitHub and Claude from **Settings → Authentication** in the web UI (tokens are written to `.acdev/.env`, gitignored) or by editing that file directly. Leave a Settings field blank to keep the current secret. Shell environment variables override `.env` on startup; saving in Settings updates the running process immediately so status refreshes without a restart.
33
+
34
+ Enqueueing issues, retrying jobs, and applying review feedback require Claude (unless `--stub-agent`). Opening a PR always requires GitHub auth. The Overview banner prompts you when auth is incomplete.
31
35
 
32
36
  `.env` is optional when you already use `gh auth login` and `claude auth login` on the machine.
33
37
 
@@ -39,7 +43,7 @@ Configure GitHub and Claude from **Settings** in the web UI (tokens are written
39
43
  2. **Personal access token** — paste a PAT in **Settings → GitHub / PR auth**, or set `GH_TOKEN` (or `GITHUB_TOKEN`) in `.acdev/.env`. `gh` reads those env vars, so no interactive login is required. Non-interactive CLI equivalent: `echo YOUR_PAT | gh auth login --with-token -h github.com`.
40
44
  3. **SSH remotes are not a substitute.** An SSH `origin` can push/fetch git, but it does **not** authenticate `gh` for issues or PRs. Settings may show the origin URL as read-only info.
41
45
 
42
- On a machine with no browser, copy `.acdev/.env.example` to `.acdev/.env`, set `GH_TOKEN`, then start `acdev`. After the UI is running you can update the token in Settings.
46
+ On a fresh machine: install `gh`, start `acdev`, then paste a PAT in Settings (or set `GH_TOKEN` in `.acdev/.env`). You can also copy `.acdev/.env.example` to `.acdev/.env` before starting.
43
47
 
44
48
  ### Claude
45
49
 
@@ -121,13 +125,13 @@ Optional **Rules** in Settings can move the Jira ticket to a target status (e.g.
121
125
 
122
126
  ## Usage
123
127
 
124
- From inside a cloned git repository (after authenticating as above):
128
+ From inside a cloned git repository:
125
129
 
126
130
  ```bash
127
131
  acdev
128
132
  ```
129
133
 
130
- The tool starts a local server (default port `4848`), opens the browser, and shows the web UI.
134
+ The tool starts a local server (default port `4848`), opens the browser, and shows the web UI. If GitHub or Claude auth is missing, the CLI warns and the UI shows a banner — open **Settings → Authentication** to add tokens, then enqueue jobs.
131
135
 
132
136
  ### CLI flags
133
137
 
@@ -228,7 +232,7 @@ npm test
228
232
 
229
233
  ## Architecture
230
234
 
231
- - `bin/acdev.js` — CLI entry, startup checks (`gh` always; Jira not required at startup). Tokens in `.acdev/.env` are loaded before auth checks
235
+ - `bin/acdev.js` — CLI entry; soft-checks `gh` / Claude auth (warns, does not exit). Tokens in `.acdev/.env` are loaded before auth checks. Job APIs reject until auth is OK.
232
236
  - `src/claude-auth.js` — Anthropic API key / Claude Code subscription auth detection
233
237
  - `src/gh-auth.js` — GitHub CLI auth (`gh auth login` or `GH_TOKEN`)
234
238
  - `src/server.js` — Express API, SSE logs, job queue, config + Jira test endpoints
package/bin/acdev.js CHANGED
@@ -58,18 +58,21 @@ async function main() {
58
58
  migrateLegacyWorktreesDir(repoRoot);
59
59
  loadEnv(repoRoot);
60
60
 
61
+ // Soft auth: never exit for missing GitHub/Claude credentials.
62
+ // Always start the server so Settings can add tokens; job endpoints
63
+ // reject with a clear 4xx until auth is configured.
61
64
  const ghAuth = checkGhAuth();
62
65
  if (!ghAuth.ok) {
63
- console.error(formatGhAuthError(ghAuth));
64
- process.exit(1);
66
+ console.warn(formatGhAuthError(ghAuth));
65
67
  }
66
68
 
67
69
  if (!opts.stubAgent) {
68
70
  const claudeAuth = checkClaudeAuth();
69
71
  if (!claudeAuth.ok) {
70
- console.error(formatClaudeAuthError(claudeAuth));
71
- process.exit(1);
72
+ console.warn(formatClaudeAuthError(claudeAuth));
72
73
  }
74
+ } else {
75
+ console.log('✓ Stub agent enabled (Claude auth not required)');
73
76
  }
74
77
 
75
78
  const config = loadConfig(repoRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acdev",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Local CLI + web UI for running AI agents on GitHub issues via git worktrees",
5
5
  "type": "module",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -163,6 +163,7 @@ let ticketSource = 'github';
163
163
  * anthropicApiKeyMasked?: string | null,
164
164
  * claudeOauthTokenSet?: boolean,
165
165
  * claudeOauthTokenMasked?: string | null,
166
+ * stubAgent?: boolean,
166
167
  * }} */
167
168
  let appConfig = {};
168
169
 
@@ -178,6 +179,9 @@ const els = {
178
179
  navBadgeRuns: document.getElementById('nav-badge-runs'),
179
180
  navBadgeReview: document.getElementById('nav-badge-review'),
180
181
  navBadgeAlerts: document.getElementById('nav-badge-alerts'),
182
+ authBanner: document.getElementById('auth-banner'),
183
+ authBannerText: document.getElementById('auth-banner-text'),
184
+ authBannerBtn: document.getElementById('auth-banner-btn'),
181
185
  issueUrls: document.getElementById('issue-urls'),
182
186
  issueUrlsLabel: document.getElementById('issue-urls-label'),
183
187
  enqueueFeedback: document.getElementById('enqueue-feedback'),
@@ -3427,11 +3431,33 @@ function applyConfigSnapshot(data) {
3427
3431
  if (els.repoBranch) {
3428
3432
  els.repoBranch.textContent = data?.baseBranch || '—';
3429
3433
  }
3434
+ updateAuthBanner(appConfig);
3430
3435
  if (currentView === 'settings') {
3431
3436
  fillSettingsForm(appConfig);
3432
3437
  }
3433
3438
  }
3434
3439
 
3440
+ /**
3441
+ * Surface missing GitHub / Claude auth on Overview so users can open Settings.
3442
+ * @param {typeof appConfig} cfg
3443
+ */
3444
+ function updateAuthBanner(cfg) {
3445
+ if (!els.authBanner || !els.authBannerText) return;
3446
+ const missing = [];
3447
+ if (!cfg.ghAuthOk) missing.push('GitHub');
3448
+ if (!cfg.claudeAuthOk && !cfg.stubAgent) missing.push('Claude');
3449
+ if (missing.length === 0) {
3450
+ els.authBanner.classList.add('hidden');
3451
+ els.authBanner.hidden = true;
3452
+ els.authBannerText.textContent = '';
3453
+ return;
3454
+ }
3455
+ const label = missing.join(' and ');
3456
+ els.authBannerText.textContent = `Configure ${label} in Settings → Authentication before starting jobs.`;
3457
+ els.authBanner.classList.remove('hidden');
3458
+ els.authBanner.hidden = false;
3459
+ }
3460
+
3435
3461
  async function fetchConfig() {
3436
3462
  try {
3437
3463
  const res = await fetch('/api/config');
@@ -3561,6 +3587,11 @@ document.querySelectorAll('.nav-item').forEach((btn) => {
3561
3587
  btn.addEventListener('click', () => setView(/** @type {ViewId} */ (btn.dataset.view)));
3562
3588
  });
3563
3589
 
3590
+ els.authBannerBtn?.addEventListener('click', () => {
3591
+ setView('settings');
3592
+ setSettingsTab('auth');
3593
+ });
3594
+
3564
3595
  els.themeToggle.addEventListener('click', () => {
3565
3596
  const next = els.app.dataset.theme === 'dark' ? 'light' : 'dark';
3566
3597
  applyTheme(next);
package/public/index.html CHANGED
@@ -100,6 +100,12 @@
100
100
  <!-- OVERVIEW -->
101
101
  <section class="view" id="view-overview" data-view-panel="overview">
102
102
  <div class="stack stack-lg max-w-overview">
103
+ <div id="auth-banner" class="auth-banner hidden" role="status" hidden>
104
+ <p id="auth-banner-text" class="auth-banner-text"></p>
105
+ <button type="button" class="btn btn-secondary btn-sm" id="auth-banner-btn">
106
+ Open Settings
107
+ </button>
108
+ </div>
103
109
  <section>
104
110
  <div class="section-label">Add issues</div>
105
111
  <div class="card enqueue-card">
package/public/styles.css CHANGED
@@ -1907,6 +1907,30 @@ body.diff-fs-open {
1907
1907
  color: var(--danger, #b42318);
1908
1908
  }
1909
1909
 
1910
+ .auth-banner {
1911
+ display: flex;
1912
+ align-items: center;
1913
+ justify-content: space-between;
1914
+ gap: 12px;
1915
+ flex-wrap: wrap;
1916
+ padding: 12px 14px;
1917
+ background: var(--amber-100);
1918
+ color: var(--amber-800);
1919
+ border-radius: 8px;
1920
+ font-size: 13px;
1921
+ }
1922
+
1923
+ .auth-banner.hidden,
1924
+ .auth-banner[hidden] {
1925
+ display: none;
1926
+ }
1927
+
1928
+ .auth-banner-text {
1929
+ margin: 0;
1930
+ flex: 1;
1931
+ min-width: 12rem;
1932
+ }
1933
+
1910
1934
  .auth-clear {
1911
1935
  display: inline-block;
1912
1936
  margin-top: 6px;
@@ -90,22 +90,16 @@ export function publicClaudeAuthMethod(result) {
90
90
  }
91
91
 
92
92
  /**
93
- * Human-readable startup error for a failed {@link checkClaudeAuth}.
93
+ * Human-readable startup warning for a failed {@link checkClaudeAuth}.
94
+ * Soft-auth: this is not fatal — the server still starts so Settings can configure auth.
94
95
  * @param {ClaudeAuthResult} [_result]
95
96
  */
96
97
  export function formatClaudeAuthError(_result) {
97
98
  return [
98
- ' No Anthropic / Claude Code authentication found.',
99
- '',
100
- 'Authenticate with one of:',
101
- ' 1. Claude Pro/Max subscription (browser): install Claude Code, then run',
102
- ' `claude auth login`. Settings cannot complete browser OAuth — that still',
103
- ' needs the CLI on this machine.',
104
- ' 2. Subscription token: `claude setup-token`, then set CLAUDE_CODE_OAUTH_TOKEN',
105
- ' in Settings → Claude authentication or `.acdev/.env`',
106
- ' 3. Anthropic API key (API billing): set ANTHROPIC_API_KEY in Settings or `.acdev/.env`',
107
- '',
108
- 'API keys take precedence over subscription login. For UI-only testing without',
109
- 'auth: pass `--stub-agent`.',
99
+ ' Claude is not authenticated server will still start.',
100
+ ' Open Settings → Authentication to add an API key or OAuth token',
101
+ ' (or set ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN in `.acdev/.env`).',
102
+ ' Browser login still needs `claude auth login` on this machine.',
103
+ ' For UI-only testing without auth: pass `--stub-agent`.',
110
104
  ].join('\n');
111
105
  }
package/src/config.js CHANGED
@@ -391,6 +391,7 @@ export function updateConfig(repoRoot, config, patch) {
391
391
  * @param {object} config
392
392
  * @param {{
393
393
  * repoRoot?: string,
394
+ * stubAgent?: boolean,
394
395
  * ghAuth?: import('./gh-auth.js').GhAuthResult,
395
396
  * claudeAuth?: import('./claude-auth.js').ClaudeAuthResult,
396
397
  * }} [opts]
@@ -461,6 +462,7 @@ export function publicConfig(config, opts = {}) {
461
462
  anthropicApiKeyMasked: anthropicMask.masked,
462
463
  claudeOauthTokenSet: claudeOauthMask.set,
463
464
  claudeOauthTokenMasked: claudeOauthMask.masked,
465
+ stubAgent: opts.stubAgent === true,
464
466
  ...(repoName ? { repoName } : {}),
465
467
  };
466
468
  }
package/src/gh-auth.js CHANGED
@@ -138,21 +138,22 @@ export function checkGhAuth() {
138
138
  }
139
139
 
140
140
  /**
141
- * Human-readable startup error for a failed {@link checkGhAuth}.
141
+ * Human-readable startup warning for a failed {@link checkGhAuth}.
142
+ * Soft-auth: this is not fatal — the server still starts so Settings can configure auth.
142
143
  * @param {GhAuthResult} result
143
144
  */
144
145
  export function formatGhAuthError(result) {
145
146
  if (result.reason === 'not-found') {
146
- return '✖ GitHub CLI (gh) not found in PATH. Install it from https://cli.github.com/';
147
+ return [
148
+ '⚠ GitHub CLI (gh) not found in PATH — server will still start.',
149
+ ' Install it from https://cli.github.com/, then open Settings → Authentication',
150
+ ' (or set GH_TOKEN in `.acdev/.env`) before enqueueing jobs.',
151
+ ].join('\n');
147
152
  }
148
153
  return [
149
- ' GitHub CLI is not authenticated for github.com.',
150
- '',
151
- 'Authenticate with one of:',
152
- ' 1. Interactive (browser/device): gh auth login -h github.com',
153
- ' 2. Personal access token: Settings → GitHub / PR auth, or set GH_TOKEN in `.acdev/.env`',
154
- ' 3. Non-interactive CLI: echo YOUR_PAT | gh auth login --with-token -h github.com',
155
- '',
156
- 'SSH remotes can push/fetch git, but do not authenticate gh for issues or PRs.',
154
+ ' GitHub is not authenticated server will still start.',
155
+ ' Open Settings → Authentication to paste a PAT, or set GH_TOKEN in `.acdev/.env`.',
156
+ ' Or run: gh auth login -h github.com',
157
+ ' (SSH remotes push/fetch git but do not authenticate gh for issues/PRs.)',
157
158
  ].join('\n');
158
159
  }
package/src/server.js CHANGED
@@ -36,6 +36,8 @@ import { upsertEnvVars } from './env.js';
36
36
  import { listModels } from './models.js';
37
37
  import { splitIssueUrls } from './urls.js';
38
38
  import { usageFromLogs, withJobUsage } from './usage.js';
39
+ import { checkGhAuth } from './gh-auth.js';
40
+ import { checkClaudeAuth } from './claude-auth.js';
39
41
 
40
42
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
43
 
@@ -169,6 +171,8 @@ export function normalizeReviewComments(body) {
169
171
  * addIssueLabel?: Function,
170
172
  * closeIssue?: Function,
171
173
  * resolveJiraCredentials?: Function,
174
+ * checkGhAuth?: typeof checkGhAuth,
175
+ * checkClaudeAuth?: typeof checkClaudeAuth,
172
176
  * },
173
177
  * }} options
174
178
  */
@@ -182,6 +186,52 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
182
186
  const doGetDiff = deps.getDiff || getDiff;
183
187
  const doListChangedFiles = deps.listChangedFiles || listChangedFiles;
184
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
+ }
185
235
 
186
236
  /** @type {Map<string, Set<import('http').ServerResponse>>} */
187
237
  const subscribers = new Map();
@@ -221,6 +271,17 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
221
271
  let job = store.getJob(jobId);
222
272
  if (!job) return;
223
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
+
224
285
  try {
225
286
  job = setStatus(jobId, 'syncing');
226
287
  if (!job) return;
@@ -349,6 +410,21 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
349
410
  let job = store.getJob(jobId);
350
411
  if (!job || job.status !== 'applying_feedback') return;
351
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
+
352
428
  const feedback = job.pendingReviewFeedback;
353
429
  if (!feedback || (!feedback.generalComment && !(feedback.lineComments || []).length)) {
354
430
  const message = 'Missing review feedback payload';
@@ -461,7 +537,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
461
537
 
462
538
  app.get('/api/config', (_req, res) => {
463
539
  try {
464
- res.json(publicConfig(config, { repoRoot }));
540
+ res.json(publicConfigPayload());
465
541
  } catch (err) {
466
542
  res.status(500).json({ error: err.message });
467
543
  }
@@ -517,7 +593,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
517
593
  ...configPatch
518
594
  } = patch;
519
595
  updateConfig(repoRoot, config, configPatch);
520
- res.json(publicConfig(config, { repoRoot }));
596
+ res.json(publicConfigPayload());
521
597
  } catch (err) {
522
598
  res.status(400).json({ error: err.message });
523
599
  }
@@ -547,6 +623,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
547
623
 
548
624
  app.post('/api/issues', (req, res) => {
549
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
+
550
631
  const urls = splitIssueUrls(req.body?.urls);
551
632
  if (urls.length === 0) {
552
633
  return res.status(400).json({
@@ -722,6 +803,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
722
803
 
723
804
  app.post('/api/jobs/:id/review', (req, res) => {
724
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
+
725
811
  const job = store.getJob(req.params.id);
726
812
  if (!job) {
727
813
  return res.status(404).json({ error: 'Job not found' });
@@ -767,6 +853,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
767
853
 
768
854
  app.post('/api/jobs/:id/approve', async (req, res) => {
769
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
+
770
861
  const job = store.getJob(req.params.id);
771
862
  if (!job) {
772
863
  return res.status(404).json({ error: 'Job not found' });
@@ -945,6 +1036,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
945
1036
 
946
1037
  app.post('/api/jobs/:id/retry', (req, res) => {
947
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
+
948
1044
  const job = store.getJob(req.params.id);
949
1045
  if (!job) {
950
1046
  return res.status(404).json({ error: 'Job not found' });