whitesmith 0.0.1 → 0.0.2

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 (64) hide show
  1. package/dist/auto-work.d.ts +11 -0
  2. package/dist/auto-work.d.ts.map +1 -0
  3. package/dist/auto-work.js +22 -0
  4. package/dist/auto-work.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +99 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/comment.d.ts +29 -0
  9. package/dist/comment.d.ts.map +1 -0
  10. package/dist/comment.js +390 -0
  11. package/dist/comment.js.map +1 -0
  12. package/dist/git.d.ts +12 -0
  13. package/dist/git.d.ts.map +1 -1
  14. package/dist/git.js +57 -14
  15. package/dist/git.js.map +1 -1
  16. package/dist/harnesses/pi.d.ts +2 -0
  17. package/dist/harnesses/pi.d.ts.map +1 -1
  18. package/dist/harnesses/pi.js +92 -6
  19. package/dist/harnesses/pi.js.map +1 -1
  20. package/dist/install-ci.d.ts +7 -0
  21. package/dist/install-ci.d.ts.map +1 -0
  22. package/dist/install-ci.js +760 -0
  23. package/dist/install-ci.js.map +1 -0
  24. package/dist/orchestrator.d.ts +24 -4
  25. package/dist/orchestrator.d.ts.map +1 -1
  26. package/dist/orchestrator.js +252 -67
  27. package/dist/orchestrator.js.map +1 -1
  28. package/dist/prompts.d.ts.map +1 -1
  29. package/dist/prompts.js +1 -0
  30. package/dist/prompts.js.map +1 -1
  31. package/dist/providers/github-ci.d.ts +16 -0
  32. package/dist/providers/github-ci.d.ts.map +1 -0
  33. package/dist/providers/github-ci.js +733 -0
  34. package/dist/providers/github-ci.js.map +1 -0
  35. package/dist/providers/github.d.ts +21 -0
  36. package/dist/providers/github.d.ts.map +1 -1
  37. package/dist/providers/github.js +88 -3
  38. package/dist/providers/github.js.map +1 -1
  39. package/dist/providers/index.d.ts +1 -0
  40. package/dist/providers/index.d.ts.map +1 -1
  41. package/dist/providers/issue-provider.d.ts +26 -0
  42. package/dist/providers/issue-provider.d.ts.map +1 -1
  43. package/dist/task-manager.d.ts +4 -0
  44. package/dist/task-manager.d.ts.map +1 -1
  45. package/dist/task-manager.js +6 -0
  46. package/dist/task-manager.js.map +1 -1
  47. package/dist/types.d.ts +9 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -1
  51. package/package.json +2 -1
  52. package/src/auto-work.ts +26 -0
  53. package/src/cli.ts +114 -0
  54. package/src/comment.ts +531 -0
  55. package/src/git.ts +58 -12
  56. package/src/harnesses/pi.ts +108 -6
  57. package/src/orchestrator.ts +287 -76
  58. package/src/prompts.ts +1 -0
  59. package/src/providers/github-ci.ts +840 -0
  60. package/src/providers/github.ts +118 -5
  61. package/src/providers/index.ts +1 -0
  62. package/src/providers/issue-provider.ts +25 -1
  63. package/src/task-manager.ts +7 -0
  64. package/src/types.ts +7 -0
@@ -0,0 +1,733 @@
1
+ import { select, input, confirm, password } from '@inquirer/prompts';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ // ─── Interactive Setup ───────────────────────────────────────────────────────
5
+ async function promptProviders() {
6
+ const providers = [];
7
+ let addMore = true;
8
+ while (addMore) {
9
+ const providerType = await select({
10
+ message: providers.length === 0 ? 'Add a provider:' : 'Add another provider:',
11
+ choices: [
12
+ { name: 'Anthropic (built-in provider, needs API key)', value: 'anthropic' },
13
+ { name: 'OpenAI (built-in provider, needs API key)', value: 'openai' },
14
+ { name: 'Custom provider', value: 'custom' },
15
+ ],
16
+ });
17
+ if (providerType === 'anthropic' || providerType === 'openai') {
18
+ const provider = await promptBuiltinProvider(providerType);
19
+ providers.push(provider);
20
+ }
21
+ else {
22
+ const provider = await promptCustomProvider();
23
+ providers.push(provider);
24
+ }
25
+ addMore = await confirm({ message: 'Add another provider?', default: false });
26
+ }
27
+ return providers;
28
+ }
29
+ async function promptBuiltinProvider(type) {
30
+ const defaultEnvVar = type === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
31
+ const defaultModel = type === 'anthropic' ? 'claude-sonnet-4-20250514' : 'gpt-4o';
32
+ const apiKeyEnvVar = await input({
33
+ message: 'GitHub secret name for the API key:',
34
+ default: defaultEnvVar,
35
+ });
36
+ const customUrl = await input({
37
+ message: 'Custom base URL (leave empty for default):',
38
+ });
39
+ // Collect models
40
+ const models = [];
41
+ let addModel = true;
42
+ while (addModel) {
43
+ const modelId = await input({
44
+ message: models.length === 0 ? 'Model ID:' : 'Another model ID:',
45
+ default: models.length === 0 ? defaultModel : undefined,
46
+ });
47
+ models.push({ id: modelId });
48
+ addModel = await confirm({ message: 'Add another model?', default: false });
49
+ }
50
+ return {
51
+ name: type,
52
+ baseUrl: customUrl || undefined,
53
+ apiKeyEnvVar,
54
+ models,
55
+ builtin: true,
56
+ };
57
+ }
58
+ async function promptCustomProvider() {
59
+ const name = await input({ message: 'Provider name:' });
60
+ const baseUrl = await input({ message: 'Base URL:' });
61
+ const api = await select({
62
+ message: 'API type:',
63
+ choices: [
64
+ { name: 'Anthropic Messages API', value: 'anthropic-messages' },
65
+ { name: 'OpenAI Completions API', value: 'openai-completions' },
66
+ ],
67
+ });
68
+ const apiKeyEnvVar = await input({
69
+ message: 'GitHub secret name for the API key:',
70
+ });
71
+ const needsCompat = api === 'openai-completions';
72
+ let compat;
73
+ if (needsCompat) {
74
+ const supportsDeveloperRole = await confirm({
75
+ message: 'Does this provider support the developer role?',
76
+ default: true,
77
+ });
78
+ const supportsReasoningEffort = await confirm({
79
+ message: 'Does this provider support reasoning effort?',
80
+ default: true,
81
+ });
82
+ if (!supportsDeveloperRole || !supportsReasoningEffort) {
83
+ compat = { supportsDeveloperRole, supportsReasoningEffort };
84
+ }
85
+ }
86
+ const models = [];
87
+ let addModel = true;
88
+ while (addModel) {
89
+ const modelId = await input({
90
+ message: models.length === 0 ? 'Model ID:' : 'Another model ID:',
91
+ });
92
+ models.push({ id: modelId });
93
+ addModel = await confirm({ message: 'Add another model?', default: false });
94
+ }
95
+ return {
96
+ name,
97
+ baseUrl,
98
+ api,
99
+ apiKeyEnvVar,
100
+ models,
101
+ builtin: false,
102
+ compat,
103
+ };
104
+ }
105
+ async function promptDefaults(providers) {
106
+ let provider;
107
+ let model;
108
+ if (providers.length === 1) {
109
+ provider = providers[0].name;
110
+ }
111
+ else {
112
+ provider = await select({
113
+ message: 'Default provider:',
114
+ choices: providers.map((p) => ({ name: p.name, value: p.name })),
115
+ });
116
+ }
117
+ const selected = providers.find((p) => p.name === provider);
118
+ if (selected.models.length === 1) {
119
+ model = selected.models[0].id;
120
+ }
121
+ else {
122
+ model = await select({
123
+ message: 'Default model:',
124
+ choices: selected.models.map((m) => ({ name: m.id, value: m.id })),
125
+ });
126
+ }
127
+ return { provider, model };
128
+ }
129
+ /**
130
+ * Prompt for API key values and set them as GitHub secrets.
131
+ * Returns the list of secrets that were set.
132
+ */
133
+ async function promptAndSetSecrets(ctx, providers) {
134
+ const setSecrets = [];
135
+ const seen = new Set();
136
+ for (const p of providers) {
137
+ if (seen.has(p.apiKeyEnvVar))
138
+ continue;
139
+ seen.add(p.apiKeyEnvVar);
140
+ const apiKey = await password({
141
+ message: `Enter API key for ${p.name} (secret: ${p.apiKeyEnvVar}):`,
142
+ });
143
+ if (!apiKey) {
144
+ console.log(` ⚠ Skipped ${p.apiKeyEnvVar} (empty)`);
145
+ continue;
146
+ }
147
+ try {
148
+ await ctx.setSecret(p.apiKeyEnvVar, apiKey);
149
+ console.log(` ✅ Secret ${p.apiKeyEnvVar} set on ${ctx.repo}`);
150
+ setSecrets.push(p.apiKeyEnvVar);
151
+ }
152
+ catch (error) {
153
+ const msg = error.stderr?.toString() || error.message || 'unknown error';
154
+ console.error(` ❌ Failed to set ${p.apiKeyEnvVar}: ${msg}`);
155
+ }
156
+ }
157
+ return setSecrets;
158
+ }
159
+ // ─── models.json generation ──────────────────────────────────────────────────
160
+ function buildModelsJson(providers) {
161
+ const providersObj = {};
162
+ for (const p of providers) {
163
+ if (p.builtin) {
164
+ // Built-in providers: apiKey references the env var name (pi resolves it at runtime)
165
+ const entry = { apiKey: p.apiKeyEnvVar };
166
+ if (p.baseUrl)
167
+ entry.baseUrl = p.baseUrl;
168
+ providersObj[p.name] = entry;
169
+ }
170
+ else {
171
+ const entry = {
172
+ baseUrl: p.baseUrl,
173
+ api: p.api,
174
+ apiKey: p.apiKeyEnvVar,
175
+ models: p.models,
176
+ };
177
+ if (p.compat)
178
+ entry.compat = p.compat;
179
+ providersObj[p.name] = entry;
180
+ }
181
+ }
182
+ return { providers: providersObj };
183
+ }
184
+ // ─── Workflow Templates ──────────────────────────────────────────────────────
185
+ function indent(text, spaces) {
186
+ const pad = ' '.repeat(spaces);
187
+ return text
188
+ .split('\n')
189
+ .map((line) => (line.trim() === '' ? '' : pad + line))
190
+ .join('\n');
191
+ }
192
+ function generateModelsJsonStep(config) {
193
+ const modelsJson = buildModelsJson(config.providers);
194
+ const modelsJsonStr = JSON.stringify(modelsJson, null, 2);
195
+ return `\
196
+ - name: Configure pi models
197
+ run: |
198
+ mkdir -p ~/.pi/agent
199
+ cat > ~/.pi/agent/models.json << 'MODELS_EOF'
200
+ ${indent(modelsJsonStr, 10)}
201
+ MODELS_EOF`;
202
+ }
203
+ function generateAuthJsonSteps() {
204
+ return `\
205
+ - name: Configure pi auth
206
+ env:
207
+ PI_AUTH_JSON: \${{ secrets.PI_AUTH_JSON }}
208
+ run: |
209
+ if [ -z "$PI_AUTH_JSON" ]; then
210
+ echo "ERROR: PI_AUTH_JSON secret is not configured."
211
+ echo "Set it to the contents of ~/.pi/agent/auth.json"
212
+ exit 1
213
+ fi
214
+ mkdir -p ~/.pi/agent
215
+ echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
216
+ chmod 600 ~/.pi/agent/auth.json
217
+
218
+ # Workaround for https://github.com/badlogic/pi-mono/issues/2743
219
+ - name: Refresh OAuth token
220
+ env:
221
+ GH_PAT: \${{ secrets.GH_PAT }}
222
+ run: node .github/scripts/refresh-oauth-token.mjs`;
223
+ }
224
+ function generateAuthSteps(config) {
225
+ if (config.authMode === 'auth-json') {
226
+ return generateAuthJsonSteps();
227
+ }
228
+ return generateModelsJsonStep(config);
229
+ }
230
+ function generateRunEnvBlock(config) {
231
+ const envs = {
232
+ GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
233
+ };
234
+ if (config.authMode === 'models-json') {
235
+ for (const p of config.providers) {
236
+ envs[p.apiKeyEnvVar] = `\${{ secrets.${p.apiKeyEnvVar} }}`;
237
+ }
238
+ }
239
+ return Object.entries(envs)
240
+ .map(([k, v]) => ` ${k}: ${v}`)
241
+ .join('\n');
242
+ }
243
+ function generateMainWorkflow(config) {
244
+ const authSteps = generateAuthSteps(config);
245
+ const envBlock = generateRunEnvBlock(config);
246
+ return `\
247
+ # NOTE: This workflow requires the repo setting:
248
+ # Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
249
+ # Without this, PR creation will fail with a permissions error.
250
+ name: whitesmith
251
+
252
+ on:
253
+ schedule:
254
+ - cron: '*/15 * * * *'
255
+ workflow_dispatch:
256
+ inputs:
257
+ max_iterations:
258
+ description: 'Maximum iterations'
259
+ default: '3'
260
+ type: string
261
+ provider:
262
+ description: 'AI provider (e.g. ${config.defaultProvider})'
263
+ required: false
264
+ type: string
265
+ model:
266
+ description: 'AI model ID (e.g. ${config.defaultModel})'
267
+ required: false
268
+ type: string
269
+
270
+ concurrency:
271
+ group: whitesmith-loop
272
+ cancel-in-progress: false
273
+
274
+ permissions:
275
+ contents: write
276
+ issues: write
277
+ pull-requests: write
278
+
279
+ jobs:
280
+ run:
281
+ runs-on: ubuntu-latest
282
+ steps:
283
+ - uses: actions/checkout@v4
284
+ with:
285
+ fetch-depth: 0
286
+ token: \${{ secrets.GITHUB_TOKEN }}
287
+
288
+ - name: Setup Node.js
289
+ uses: actions/setup-node@v4
290
+ with:
291
+ node-version: '22'
292
+
293
+ - name: Configure git
294
+ run: |
295
+ git config user.name "whitesmith[bot]"
296
+ git config user.email "whitesmith[bot]@users.noreply.github.com"
297
+
298
+ - name: Get npm global prefix
299
+ id: npm-prefix
300
+ run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
301
+
302
+ - name: Cache global npm packages
303
+ id: npm-cache
304
+ uses: actions/cache@v4
305
+ with:
306
+ path: \${{ steps.npm-prefix.outputs.dir }}
307
+ key: npm-global-\${{ runner.os }}-pi-v1
308
+
309
+ - name: Install whitesmith and pi
310
+ if: steps.npm-cache.outputs.cache-hit != 'true'
311
+ run: |
312
+ npm install -g whitesmith
313
+ npm install -g @mariozechner/pi-coding-agent
314
+
315
+ ${authSteps}
316
+
317
+ - name: Run whitesmith
318
+ env:
319
+ ${envBlock}
320
+ run: |
321
+ PROVIDER="\${{ inputs.provider || '${config.defaultProvider}' }}"
322
+ MODEL="\${{ inputs.model || '${config.defaultModel}' }}"
323
+ whitesmith run . \\
324
+ --agent-cmd "pi" \\
325
+ --provider "$PROVIDER" \\
326
+ --model "$MODEL" \\
327
+ --max-iterations \${{ inputs.max_iterations || '3' }}
328
+ `;
329
+ }
330
+ function generateCommentWorkflow(config) {
331
+ const authSteps = generateAuthSteps(config);
332
+ const envBlock = generateRunEnvBlock(config);
333
+ return `\
334
+ # NOTE: This workflow requires the repo setting:
335
+ # Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
336
+ name: whitesmith-comment
337
+
338
+ on:
339
+ issue_comment:
340
+ types: [created]
341
+
342
+ concurrency:
343
+ group: whitesmith-comment-\${{ github.event.issue.number }}
344
+ cancel-in-progress: false
345
+
346
+ permissions:
347
+ contents: write
348
+ issues: write
349
+ pull-requests: write
350
+
351
+ jobs:
352
+ check:
353
+ runs-on: ubuntu-latest
354
+ outputs:
355
+ should_run: \${{ steps.check.outputs.should_run }}
356
+ steps:
357
+ - name: Check if should run
358
+ id: check
359
+ env:
360
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
361
+ COMMENT_BODY: \${{ github.event.comment.body }}
362
+ run: |
363
+ # Always run if comment contains /whitesmith
364
+ if echo "$COMMENT_BODY" | grep -q '/whitesmith'; then
365
+ echo "should_run=true" >> "$GITHUB_OUTPUT"
366
+ echo "Triggered by /whitesmith keyword"
367
+ exit 0
368
+ fi
369
+
370
+ # For PR comments, auto-trigger if the PR is on a whitesmith branch
371
+ if [ -n "\${{ github.event.issue.pull_request.url }}" ]; then
372
+ BRANCH=$(gh pr view \${{ github.event.issue.number }} \\
373
+ --repo \${{ github.repository }} \\
374
+ --json headRefName -q .headRefName)
375
+ echo "PR branch: $BRANCH"
376
+ if echo "$BRANCH" | grep -qE '^(investigate|task)/'; then
377
+ echo "should_run=true" >> "$GITHUB_OUTPUT"
378
+ echo "Triggered by comment on whitesmith PR branch"
379
+ exit 0
380
+ fi
381
+ fi
382
+
383
+ echo "should_run=false" >> "$GITHUB_OUTPUT"
384
+ echo "Skipping: not a /whitesmith command and not a whitesmith PR"
385
+
386
+ run:
387
+ needs: check
388
+ runs-on: ubuntu-latest
389
+ if: needs.check.outputs.should_run == 'true'
390
+ steps:
391
+ - name: React with eyes to acknowledge
392
+ env:
393
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
394
+ run: |
395
+ gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
396
+ -f content=eyes
397
+
398
+ - uses: actions/checkout@v4
399
+ with:
400
+ fetch-depth: 0
401
+ token: \${{ secrets.GITHUB_TOKEN }}
402
+
403
+ - name: Setup Node.js
404
+ uses: actions/setup-node@v4
405
+ with:
406
+ node-version: '22'
407
+
408
+ - name: Configure git
409
+ run: |
410
+ git config user.name "whitesmith[bot]"
411
+ git config user.email "whitesmith[bot]@users.noreply.github.com"
412
+
413
+ - name: Get npm global prefix
414
+ id: npm-prefix
415
+ run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
416
+
417
+ - name: Cache global npm packages
418
+ id: npm-cache
419
+ uses: actions/cache@v4
420
+ with:
421
+ path: \${{ steps.npm-prefix.outputs.dir }}
422
+ key: npm-global-\${{ runner.os }}-pi-v1
423
+
424
+ - name: Install whitesmith and pi
425
+ if: steps.npm-cache.outputs.cache-hit != 'true'
426
+ run: |
427
+ npm install -g whitesmith
428
+ npm install -g @mariozechner/pi-coding-agent
429
+
430
+ ${authSteps}
431
+
432
+ - name: Save comment body to file
433
+ env:
434
+ COMMENT_BODY: \${{ github.event.comment.body }}
435
+ run: |
436
+ printf '%s' "$COMMENT_BODY" > .whitesmith-comment-body.txt
437
+
438
+ - name: Run whitesmith comment
439
+ env:
440
+ ${envBlock}
441
+ run: |
442
+ whitesmith comment . \\
443
+ --number "\${{ github.event.issue.number }}" \\
444
+ --body-file .whitesmith-comment-body.txt \\
445
+ --provider "${config.defaultProvider}" \\
446
+ --model "${config.defaultModel}" \\
447
+ --post
448
+
449
+ - name: React with checkmark on success
450
+ if: success()
451
+ env:
452
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
453
+ run: |
454
+ gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
455
+ -f content="+1"
456
+
457
+ - name: React with X and comment on failure
458
+ if: failure()
459
+ env:
460
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
461
+ run: |
462
+ gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
463
+ -f content="-1"
464
+ gh issue comment \${{ github.event.issue.number }} \\
465
+ --repo \${{ github.repository }} \\
466
+ --body "❌ Agent run failed for [this comment](\${{ github.event.comment.html_url }}). Check the [workflow run](\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }}) for details."
467
+ `;
468
+ }
469
+ function generateReconcileWorkflow() {
470
+ return `\
471
+ name: whitesmith-reconcile
472
+
473
+ on:
474
+ pull_request:
475
+ types: [closed]
476
+ branches: [main]
477
+
478
+ permissions:
479
+ contents: read
480
+ issues: write
481
+ pull-requests: read
482
+
483
+ jobs:
484
+ reconcile:
485
+ if: github.event.pull_request.merged == true
486
+ runs-on: ubuntu-latest
487
+ steps:
488
+ - uses: actions/checkout@v4
489
+
490
+ - name: Setup Node.js
491
+ uses: actions/setup-node@v4
492
+ with:
493
+ node-version: '22'
494
+
495
+ - name: Get npm global prefix
496
+ id: npm-prefix
497
+ run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
498
+
499
+ - name: Cache global npm packages
500
+ id: npm-cache
501
+ uses: actions/cache@v4
502
+ with:
503
+ path: \${{ steps.npm-prefix.outputs.dir }}
504
+ key: npm-global-\${{ runner.os }}-whitesmith-v1
505
+
506
+ - name: Install whitesmith
507
+ if: steps.npm-cache.outputs.cache-hit != 'true'
508
+ run: npm install -g whitesmith
509
+
510
+ - name: Reconcile
511
+ env:
512
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
513
+ run: whitesmith reconcile .
514
+ `;
515
+ }
516
+ // ─── Refresh OAuth Script (auth-json mode only) ─────────────────────────────
517
+ const REFRESH_OAUTH_SCRIPT = `\
518
+ #!/usr/bin/env node
519
+ /**
520
+ * Refresh OAuth tokens in pi's auth.json before pi runs.
521
+ *
522
+ * Workaround for https://github.com/badlogic/pi-mono/issues/2743
523
+ * pi-ai sends JSON to Anthropic's OAuth token endpoint, which now requires
524
+ * application/x-www-form-urlencoded. We refresh the token ourselves.
525
+ *
526
+ * After refreshing, updates the PI_AUTH_JSON GitHub secret so the next run
527
+ * has the latest rotated refresh token (requires GH_PAT with repo scope).
528
+ *
529
+ * Remove this script once the upstream fix is released.
530
+ */
531
+ import { readFileSync, writeFileSync, chmodSync } from "fs";
532
+ import { join } from "path";
533
+ import { execSync } from "child_process";
534
+
535
+ const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
536
+ const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
537
+
538
+ const authPath = join(process.env.HOME, ".pi", "agent", "auth.json");
539
+ const auth = JSON.parse(readFileSync(authPath, "utf-8"));
540
+ const cred = auth.anthropic;
541
+
542
+ if (!cred || cred.type !== "oauth") {
543
+ console.log("No OAuth credentials for anthropic, skipping refresh");
544
+ process.exit(0);
545
+ }
546
+
547
+ if (Date.now() < cred.expires) {
548
+ console.log("Token still valid until", new Date(cred.expires).toISOString());
549
+ process.exit(0);
550
+ }
551
+
552
+ console.log(
553
+ "Token expired at",
554
+ new Date(cred.expires).toISOString(),
555
+ "- refreshing..."
556
+ );
557
+
558
+ const response = await fetch(ANTHROPIC_TOKEN_URL, {
559
+ method: "POST",
560
+ headers: {
561
+ "Content-Type": "application/x-www-form-urlencoded",
562
+ Accept: "application/json",
563
+ },
564
+ body: new URLSearchParams({
565
+ grant_type: "refresh_token",
566
+ client_id: ANTHROPIC_CLIENT_ID,
567
+ refresh_token: cred.refresh,
568
+ }).toString(),
569
+ signal: AbortSignal.timeout(30_000),
570
+ });
571
+
572
+ const data = await response.json();
573
+
574
+ if (!response.ok) {
575
+ console.error("Refresh failed:", response.status, JSON.stringify(data));
576
+ process.exit(1);
577
+ }
578
+
579
+ auth.anthropic = {
580
+ type: "oauth",
581
+ refresh: data.refresh_token,
582
+ access: data.access_token,
583
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
584
+ };
585
+
586
+ writeFileSync(authPath, JSON.stringify(auth, null, 2));
587
+ chmodSync(authPath, 0o600);
588
+ console.log(
589
+ "Token refreshed, new expiry:",
590
+ new Date(auth.anthropic.expires).toISOString()
591
+ );
592
+
593
+ // Update the GitHub secret so the next run has the latest refresh token
594
+ const repo = process.env.GITHUB_REPOSITORY;
595
+ const token = process.env.GH_PAT;
596
+ if (repo && token) {
597
+ try {
598
+ execSync(\`gh secret set PI_AUTH_JSON --repo "\${repo}"\`, {
599
+ input: JSON.stringify(auth),
600
+ env: { ...process.env, GH_TOKEN: token },
601
+ stdio: ["pipe", "pipe", "pipe"],
602
+ });
603
+ console.log("PI_AUTH_JSON secret updated");
604
+ } catch (err) {
605
+ console.warn("Failed to update secret (non-fatal):", err.stderr?.toString() || err.message);
606
+ }
607
+ } else {
608
+ console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
609
+ }
610
+ `;
611
+ export async function installGitHubCI(ctx, options) {
612
+ const { authMode } = options;
613
+ console.log('=== whitesmith install-ci (GitHub) ===\n');
614
+ console.log(`Auth mode: ${authMode}\n`);
615
+ let repo = ctx.repo;
616
+ if (!repo && authMode === 'models-json' && ctx.ghAvailable) {
617
+ repo = await input({
618
+ message: 'GitHub repository (owner/repo) — needed to set secrets:',
619
+ });
620
+ ctx.repo = repo;
621
+ }
622
+ let providers = [];
623
+ let defaultProvider;
624
+ let defaultModel;
625
+ if (authMode === 'models-json') {
626
+ // Configure providers interactively
627
+ providers = await promptProviders();
628
+ // Pick defaults
629
+ const defaults = await promptDefaults(providers);
630
+ defaultProvider = defaults.provider;
631
+ defaultModel = defaults.model;
632
+ }
633
+ else {
634
+ // auth.json mode — still need provider/model for whitesmith commands
635
+ defaultProvider = await input({
636
+ message: 'Default AI provider:',
637
+ default: 'anthropic',
638
+ });
639
+ defaultModel = await input({
640
+ message: 'Default AI model:',
641
+ default: 'claude-sonnet-4-20250514',
642
+ });
643
+ }
644
+ const config = {
645
+ authMode,
646
+ providers,
647
+ defaultProvider,
648
+ defaultModel,
649
+ };
650
+ // ── Set GitHub secrets via gh CLI ─────────────────────────────────────
651
+ const fake = options.fake ?? false;
652
+ if (!fake && authMode === 'models-json' && repo) {
653
+ if (!ctx.ghAvailable) {
654
+ console.log('\n⚠ GitHub CLI (gh) is not available or not authenticated.');
655
+ console.log(' You will need to set the following secrets manually.\n');
656
+ }
657
+ else {
658
+ console.log('\n🔑 Setting API key secrets on GitHub...\n');
659
+ const setSecrets = await promptAndSetSecrets(ctx, providers);
660
+ const allEnvVars = [...new Set(providers.map((p) => p.apiKeyEnvVar))];
661
+ const missing = allEnvVars.filter((v) => !setSecrets.includes(v));
662
+ if (missing.length > 0) {
663
+ console.log(`\n⚠ The following secrets were not set and must be added manually:`);
664
+ for (const m of missing) {
665
+ console.log(` - ${m}`);
666
+ }
667
+ }
668
+ }
669
+ }
670
+ else if (fake) {
671
+ console.log('\n🔑 Skipping secret setup (--fake mode)');
672
+ }
673
+ // ── Generate and write workflow files ─────────────────────────────────
674
+ const outputBase = fake ? '.fake' : '.github';
675
+ const githubDir = path.join(ctx.workDir, outputBase);
676
+ const workflowsDir = path.join(githubDir, 'workflows');
677
+ fs.mkdirSync(workflowsDir, { recursive: true });
678
+ const files = [
679
+ {
680
+ path: path.join(workflowsDir, 'whitesmith.yml'),
681
+ content: generateMainWorkflow(config),
682
+ },
683
+ {
684
+ path: path.join(workflowsDir, 'whitesmith-comment.yml'),
685
+ content: generateCommentWorkflow(config),
686
+ },
687
+ {
688
+ path: path.join(workflowsDir, 'whitesmith-reconcile.yml'),
689
+ content: generateReconcileWorkflow(),
690
+ },
691
+ ];
692
+ if (authMode === 'auth-json') {
693
+ const scriptsDir = path.join(githubDir, 'scripts');
694
+ fs.mkdirSync(scriptsDir, { recursive: true });
695
+ files.push({
696
+ path: path.join(scriptsDir, 'refresh-oauth-token.mjs'),
697
+ content: REFRESH_OAUTH_SCRIPT,
698
+ });
699
+ }
700
+ for (const file of files) {
701
+ fs.writeFileSync(file.path, file.content, 'utf-8');
702
+ }
703
+ // ── Summary ──────────────────────────────────────────────────────────
704
+ console.log('\n✅ GitHub Actions workflows installed!\n');
705
+ console.log('Files created:');
706
+ for (const file of files) {
707
+ console.log(` ${path.relative(ctx.workDir, file.path)}`);
708
+ }
709
+ console.log('\n📋 Required setup:\n');
710
+ console.log('1. Enable in repo settings:');
711
+ console.log(' Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"\n');
712
+ if (authMode === 'auth-json') {
713
+ console.log('2. Add GitHub secrets:');
714
+ console.log(' - PI_AUTH_JSON: contents of ~/.pi/agent/auth.json');
715
+ console.log(' - GH_PAT: GitHub personal access token with repo scope (for OAuth token refresh)\n');
716
+ }
717
+ else if (!ctx.ghAvailable || !repo) {
718
+ console.log('2. Add GitHub secrets for your API keys:');
719
+ const seen = new Set();
720
+ for (const p of providers) {
721
+ if (!seen.has(p.apiKeyEnvVar)) {
722
+ console.log(` - ${p.apiKeyEnvVar}: API key for ${p.name}`);
723
+ seen.add(p.apiKeyEnvVar);
724
+ }
725
+ }
726
+ console.log('');
727
+ }
728
+ console.log(`Default provider: ${defaultProvider}`);
729
+ console.log(`Default model: ${defaultModel}`);
730
+ console.log('');
731
+ console.log('You can customize these by editing the generated workflow files.');
732
+ }
733
+ //# sourceMappingURL=github-ci.js.map