whitesmith 0.0.2 → 0.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/README.md +286 -88
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +90 -2
- package/dist/cli.js.map +1 -1
- package/dist/comment.d.ts.map +1 -1
- package/dist/comment.js +18 -11
- package/dist/comment.js.map +1 -1
- package/dist/git.d.ts +5 -3
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +20 -29
- package/dist/git.js.map +1 -1
- package/dist/harnesses/pi.d.ts.map +1 -1
- package/dist/harnesses/pi.js +22 -6
- package/dist/harnesses/pi.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts +31 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +214 -10
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompts.d.ts +52 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +197 -0
- package/dist/prompts.js.map +1 -1
- package/dist/providers/github-ci.d.ts +40 -0
- package/dist/providers/github-ci.d.ts.map +1 -1
- package/dist/providers/github-ci.js +463 -213
- package/dist/providers/github-ci.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/review.d.ts +48 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +221 -0
- package/dist/review.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +116 -3
- package/src/comment.ts +20 -14
- package/src/git.ts +23 -30
- package/src/harnesses/pi.ts +27 -6
- package/src/index.ts +9 -1
- package/src/orchestrator.ts +253 -14
- package/src/prompts.ts +239 -0
- package/src/providers/github-ci.ts +513 -217
- package/src/providers/index.ts +1 -1
- package/src/review.ts +290 -0
- package/src/types.ts +4 -0
|
@@ -127,19 +127,23 @@ async function promptDefaults(providers) {
|
|
|
127
127
|
return { provider, model };
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
130
|
+
* Set API key secrets on GitHub. If `knownSecrets` contains a value for an
|
|
131
|
+
* env var, it is used directly; otherwise the user is prompted interactively.
|
|
132
|
+
* Returns the list of secret names that were successfully set.
|
|
132
133
|
*/
|
|
133
|
-
async function
|
|
134
|
+
async function setOrPromptSecrets(ctx, providers, knownSecrets) {
|
|
134
135
|
const setSecrets = [];
|
|
135
136
|
const seen = new Set();
|
|
136
137
|
for (const p of providers) {
|
|
137
138
|
if (seen.has(p.apiKeyEnvVar))
|
|
138
139
|
continue;
|
|
139
140
|
seen.add(p.apiKeyEnvVar);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
let apiKey = knownSecrets?.[p.apiKeyEnvVar];
|
|
142
|
+
if (!apiKey) {
|
|
143
|
+
apiKey = await password({
|
|
144
|
+
message: `Enter API key for ${p.name} (secret: ${p.apiKeyEnvVar}):`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
143
147
|
if (!apiKey) {
|
|
144
148
|
console.log(` ⚠ Skipped ${p.apiKeyEnvVar} (empty)`);
|
|
145
149
|
continue;
|
|
@@ -189,86 +193,157 @@ function indent(text, spaces) {
|
|
|
189
193
|
.map((line) => (line.trim() === '' ? '' : pad + line))
|
|
190
194
|
.join('\n');
|
|
191
195
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
};
|
|
196
|
+
/**
|
|
197
|
+
* Top-level env block shared by whitesmith.yml and whitesmith-comment.yml.
|
|
198
|
+
* Includes defaults, GH_TOKEN, and API key secrets.
|
|
199
|
+
*/
|
|
200
|
+
function generateTopLevelEnv(config) {
|
|
201
|
+
const lines = [
|
|
202
|
+
` WHITESMITH_PROVIDER: ${config.defaultProvider}`,
|
|
203
|
+
` WHITESMITH_MODEL: ${config.defaultModel}`,
|
|
204
|
+
` GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}`,
|
|
205
|
+
];
|
|
234
206
|
if (config.authMode === 'models-json') {
|
|
207
|
+
const seen = new Set();
|
|
235
208
|
for (const p of config.providers) {
|
|
236
|
-
|
|
209
|
+
if (seen.has(p.apiKeyEnvVar))
|
|
210
|
+
continue;
|
|
211
|
+
seen.add(p.apiKeyEnvVar);
|
|
212
|
+
lines.push(` ${p.apiKeyEnvVar}: \${{ secrets.${p.apiKeyEnvVar} }}`);
|
|
237
213
|
}
|
|
238
214
|
}
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Composite action: node setup, git config, npm cache, install, auth config.
|
|
219
|
+
* This is written to .github/actions/setup-whitesmith/action.yml so workflows
|
|
220
|
+
* can just do `uses: ./.github/actions/setup-whitesmith`.
|
|
221
|
+
*/
|
|
222
|
+
function generateSetupAction(config) {
|
|
223
|
+
let authStep;
|
|
224
|
+
if (config.authMode === 'auth-json') {
|
|
225
|
+
authStep = `\
|
|
226
|
+
- name: Configure pi auth
|
|
227
|
+
shell: bash
|
|
228
|
+
run: |
|
|
229
|
+
if [ -z "$PI_AUTH_JSON" ]; then
|
|
230
|
+
echo "ERROR: PI_AUTH_JSON secret is not set" >&2; exit 1
|
|
231
|
+
fi
|
|
232
|
+
mkdir -p ~/.pi/agent
|
|
233
|
+
echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
|
|
234
|
+
chmod 600 ~/.pi/agent/auth.json
|
|
235
|
+
|
|
236
|
+
# Workaround for https://github.com/badlogic/pi-mono/issues/2743
|
|
237
|
+
- name: Refresh OAuth token
|
|
238
|
+
shell: bash
|
|
239
|
+
run: node .github/scripts/refresh-oauth-token.mjs`;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const modelsJson = buildModelsJson(config.providers);
|
|
243
|
+
const modelsJsonStr = JSON.stringify(modelsJson, null, 2);
|
|
244
|
+
authStep = `\
|
|
245
|
+
- name: Configure pi models
|
|
246
|
+
shell: bash
|
|
247
|
+
run: |
|
|
248
|
+
mkdir -p ~/.pi/agent
|
|
249
|
+
cat > ~/.pi/agent/models.json << 'MODELS_EOF'
|
|
250
|
+
${indent(modelsJsonStr, 8)}
|
|
251
|
+
MODELS_EOF`;
|
|
252
|
+
}
|
|
253
|
+
let installSteps;
|
|
254
|
+
if (config.dev) {
|
|
255
|
+
// Dev mode: build whitesmith from source using pnpm.
|
|
256
|
+
// We add pnpm's global bin to $GITHUB_PATH so that `whitesmith` and `pi`
|
|
257
|
+
// are available in all subsequent steps (persists across composite action
|
|
258
|
+
// steps and the calling workflow).
|
|
259
|
+
// We always rebuild (even on cache hit) because source changes per commit.
|
|
260
|
+
installSteps = `\
|
|
261
|
+
- name: Setup pnpm
|
|
262
|
+
uses: pnpm/action-setup@v4
|
|
263
|
+
|
|
264
|
+
- name: Add pnpm global bin to PATH
|
|
265
|
+
shell: bash
|
|
266
|
+
run: |
|
|
267
|
+
pnpm setup
|
|
268
|
+
echo "$HOME/.local/share/pnpm" >> "$GITHUB_PATH"
|
|
269
|
+
|
|
270
|
+
- name: Install dependencies and build whitesmith
|
|
271
|
+
shell: bash
|
|
272
|
+
run: |
|
|
273
|
+
pnpm install
|
|
274
|
+
pnpm run build
|
|
275
|
+
pnpm link --global
|
|
276
|
+
|
|
277
|
+
- name: Install pi
|
|
278
|
+
shell: bash
|
|
279
|
+
run: pnpm add -g @mariozechner/pi-coding-agent`;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
installSteps = `\
|
|
283
|
+
- name: Get npm global prefix
|
|
284
|
+
id: npm-prefix
|
|
285
|
+
shell: bash
|
|
286
|
+
run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
|
|
287
|
+
|
|
288
|
+
- name: Cache npm packages
|
|
289
|
+
id: npm-cache
|
|
290
|
+
uses: actions/cache@v4
|
|
291
|
+
with:
|
|
292
|
+
path: \${{ steps.npm-prefix.outputs.dir }}
|
|
293
|
+
key: whitesmith-\${{ runner.os }}-${config.version}
|
|
294
|
+
|
|
295
|
+
- name: Install whitesmith and pi
|
|
296
|
+
if: steps.npm-cache.outputs.cache-hit != 'true'
|
|
297
|
+
shell: bash
|
|
298
|
+
run: npm install -g whitesmith@${config.version} @mariozechner/pi-coding-agent`;
|
|
299
|
+
}
|
|
300
|
+
return `\
|
|
301
|
+
name: Setup whitesmith
|
|
302
|
+
description: Install Node.js, whitesmith, pi, and configure AI provider auth
|
|
303
|
+
|
|
304
|
+
runs:
|
|
305
|
+
using: composite
|
|
306
|
+
steps:
|
|
307
|
+
- name: Setup Node.js
|
|
308
|
+
uses: actions/setup-node@v4
|
|
309
|
+
with:
|
|
310
|
+
node-version: '22'
|
|
311
|
+
|
|
312
|
+
- name: Configure git
|
|
313
|
+
shell: bash
|
|
314
|
+
run: |
|
|
315
|
+
git config user.name "whitesmith[bot]"
|
|
316
|
+
git config user.email "whitesmith[bot]@users.noreply.github.com"
|
|
317
|
+
|
|
318
|
+
${installSteps}
|
|
319
|
+
|
|
320
|
+
${authStep}
|
|
321
|
+
`;
|
|
242
322
|
}
|
|
243
323
|
function generateMainWorkflow(config) {
|
|
244
|
-
const
|
|
245
|
-
const envBlock = generateRunEnvBlock(config);
|
|
324
|
+
const envBlock = generateTopLevelEnv(config);
|
|
246
325
|
return `\
|
|
247
|
-
#
|
|
248
|
-
# Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
|
|
249
|
-
# Without this, PR creation will fail with a permissions error.
|
|
326
|
+
# Requires: Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
|
|
250
327
|
name: whitesmith
|
|
251
328
|
|
|
252
329
|
on:
|
|
253
|
-
schedule:
|
|
254
|
-
- cron: '*/15 * * * *'
|
|
255
330
|
workflow_dispatch:
|
|
256
331
|
inputs:
|
|
332
|
+
issue:
|
|
333
|
+
description: 'Issue number to target (leave empty for global scan)'
|
|
257
334
|
max_iterations:
|
|
258
335
|
description: 'Maximum iterations'
|
|
259
336
|
default: '3'
|
|
260
|
-
type: string
|
|
261
337
|
provider:
|
|
262
|
-
description: 'AI provider (
|
|
263
|
-
required: false
|
|
264
|
-
type: string
|
|
338
|
+
description: 'AI provider (overrides WHITESMITH_PROVIDER)'
|
|
265
339
|
model:
|
|
266
|
-
description: 'AI model
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
description: 'AI model (overrides WHITESMITH_MODEL)'
|
|
341
|
+
|
|
342
|
+
env:
|
|
343
|
+
${envBlock}
|
|
269
344
|
|
|
270
345
|
concurrency:
|
|
271
|
-
group: whitesmith-
|
|
346
|
+
group: \${{ inputs.issue && format('whitesmith-issue-{0}', inputs.issue) || 'whitesmith-global' }}
|
|
272
347
|
cancel-in-progress: false
|
|
273
348
|
|
|
274
349
|
permissions:
|
|
@@ -283,62 +358,34 @@ jobs:
|
|
|
283
358
|
- uses: actions/checkout@v4
|
|
284
359
|
with:
|
|
285
360
|
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
361
|
|
|
309
|
-
-
|
|
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}
|
|
362
|
+
- uses: ./.github/actions/setup-whitesmith
|
|
316
363
|
|
|
317
|
-
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
MODEL="\${{ inputs.model || '${config.defaultModel}' }}"
|
|
364
|
+
- run: |
|
|
365
|
+
ISSUE_FLAG=""
|
|
366
|
+
if [ -n "\${{ inputs.issue }}" ]; then
|
|
367
|
+
ISSUE_FLAG="--issue \${{ inputs.issue }}"
|
|
368
|
+
fi
|
|
323
369
|
whitesmith run . \\
|
|
324
|
-
|
|
325
|
-
--provider "
|
|
326
|
-
--model "
|
|
370
|
+
\$ISSUE_FLAG \\
|
|
371
|
+
--provider "\${{ inputs.provider || env.WHITESMITH_PROVIDER }}" \\
|
|
372
|
+
--model "\${{ inputs.model || env.WHITESMITH_MODEL }}" \\
|
|
327
373
|
--max-iterations \${{ inputs.max_iterations || '3' }}
|
|
328
374
|
`;
|
|
329
375
|
}
|
|
330
376
|
function generateCommentWorkflow(config) {
|
|
331
|
-
const
|
|
332
|
-
const envBlock = generateRunEnvBlock(config);
|
|
377
|
+
const envBlock = generateTopLevelEnv(config);
|
|
333
378
|
return `\
|
|
334
|
-
#
|
|
335
|
-
# Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
|
|
379
|
+
# Requires: Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
|
|
336
380
|
name: whitesmith-comment
|
|
337
381
|
|
|
338
382
|
on:
|
|
339
383
|
issue_comment:
|
|
340
384
|
types: [created]
|
|
341
385
|
|
|
386
|
+
env:
|
|
387
|
+
${envBlock}
|
|
388
|
+
|
|
342
389
|
concurrency:
|
|
343
390
|
group: whitesmith-comment-\${{ github.event.issue.number }}
|
|
344
391
|
cancel-in-progress: false
|
|
@@ -354,119 +401,196 @@ jobs:
|
|
|
354
401
|
outputs:
|
|
355
402
|
should_run: \${{ steps.check.outputs.should_run }}
|
|
356
403
|
steps:
|
|
357
|
-
-
|
|
358
|
-
id: check
|
|
404
|
+
- id: check
|
|
359
405
|
env:
|
|
360
|
-
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
361
406
|
COMMENT_BODY: \${{ github.event.comment.body }}
|
|
362
407
|
run: |
|
|
363
|
-
# Always run if comment contains /whitesmith
|
|
364
408
|
if echo "$COMMENT_BODY" | grep -q '/whitesmith'; then
|
|
365
409
|
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
|
366
|
-
echo "Triggered by /whitesmith keyword"
|
|
367
410
|
exit 0
|
|
368
411
|
fi
|
|
369
|
-
|
|
370
|
-
# For PR comments, auto-trigger if the PR is on a whitesmith branch
|
|
371
412
|
if [ -n "\${{ github.event.issue.pull_request.url }}" ]; then
|
|
372
413
|
BRANCH=$(gh pr view \${{ github.event.issue.number }} \\
|
|
373
|
-
--repo \${{ github.repository }}
|
|
374
|
-
|
|
375
|
-
echo "PR branch: $BRANCH"
|
|
376
|
-
if echo "$BRANCH" | grep -qE '^(investigate|task)/'; then
|
|
414
|
+
--repo \${{ github.repository }} --json headRefName -q .headRefName)
|
|
415
|
+
if echo "$BRANCH" | grep -qE '^(investigate|issue)/'; then
|
|
377
416
|
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
|
378
|
-
echo "Triggered by comment on whitesmith PR branch"
|
|
379
417
|
exit 0
|
|
380
418
|
fi
|
|
381
419
|
fi
|
|
382
|
-
|
|
383
420
|
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
|
384
|
-
echo "Skipping: not a /whitesmith command and not a whitesmith PR"
|
|
385
421
|
|
|
386
422
|
run:
|
|
387
423
|
needs: check
|
|
388
|
-
runs-on: ubuntu-latest
|
|
389
424
|
if: needs.check.outputs.should_run == 'true'
|
|
425
|
+
runs-on: ubuntu-latest
|
|
390
426
|
steps:
|
|
391
|
-
-
|
|
392
|
-
env:
|
|
393
|
-
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
394
|
-
run: |
|
|
427
|
+
- run: |
|
|
395
428
|
gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
|
|
396
429
|
-f content=eyes
|
|
397
430
|
|
|
398
431
|
- uses: actions/checkout@v4
|
|
399
432
|
with:
|
|
400
433
|
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
434
|
|
|
413
|
-
-
|
|
414
|
-
id: npm-prefix
|
|
415
|
-
run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
|
|
435
|
+
- uses: ./.github/actions/setup-whitesmith
|
|
416
436
|
|
|
417
|
-
-
|
|
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:
|
|
437
|
+
- env:
|
|
434
438
|
COMMENT_BODY: \${{ github.event.comment.body }}
|
|
435
439
|
run: |
|
|
436
440
|
printf '%s' "$COMMENT_BODY" > .whitesmith-comment-body.txt
|
|
437
|
-
|
|
438
|
-
- name: Run whitesmith comment
|
|
439
|
-
env:
|
|
440
|
-
${envBlock}
|
|
441
|
-
run: |
|
|
442
441
|
whitesmith comment . \\
|
|
443
442
|
--number "\${{ github.event.issue.number }}" \\
|
|
444
443
|
--body-file .whitesmith-comment-body.txt \\
|
|
445
|
-
--provider "$
|
|
446
|
-
--model "$
|
|
444
|
+
--provider "$WHITESMITH_PROVIDER" \\
|
|
445
|
+
--model "$WHITESMITH_MODEL" \\
|
|
447
446
|
--post
|
|
448
447
|
|
|
449
|
-
-
|
|
450
|
-
if: success()
|
|
451
|
-
env:
|
|
452
|
-
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
448
|
+
- if: success()
|
|
453
449
|
run: |
|
|
454
450
|
gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
|
|
455
451
|
-f content="+1"
|
|
456
452
|
|
|
457
|
-
-
|
|
458
|
-
if: failure()
|
|
459
|
-
env:
|
|
460
|
-
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
453
|
+
- if: failure()
|
|
461
454
|
run: |
|
|
462
455
|
gh api repos/\${{ github.repository }}/issues/comments/\${{ github.event.comment.id }}/reactions \\
|
|
463
456
|
-f content="-1"
|
|
464
|
-
gh issue comment \${{ github.event.issue.number }} \\
|
|
465
|
-
--
|
|
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."
|
|
457
|
+
gh issue comment \${{ github.event.issue.number }} --repo \${{ github.repository }} \\
|
|
458
|
+
--body "❌ Agent run failed. See [workflow run](\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }})."
|
|
467
459
|
`;
|
|
468
460
|
}
|
|
469
|
-
function
|
|
461
|
+
function generateReviewWorkflow(config) {
|
|
462
|
+
const envBlock = generateTopLevelEnv(config);
|
|
463
|
+
// When the review step is enabled in the main loop, whitesmith PRs are
|
|
464
|
+
// already reviewed inline. The workflow should only review non-whitesmith PRs.
|
|
465
|
+
// When the review step is disabled, the workflow reviews ALL PRs.
|
|
466
|
+
const skipWhitesmithCheck = config.reviewStepEnabled
|
|
467
|
+
? `\
|
|
468
|
+
check:
|
|
469
|
+
if: github.event_name == 'pull_request'
|
|
470
|
+
runs-on: ubuntu-latest
|
|
471
|
+
outputs:
|
|
472
|
+
should_run: \${{ steps.check.outputs.should_run }}
|
|
473
|
+
steps:
|
|
474
|
+
- id: check
|
|
475
|
+
run: |
|
|
476
|
+
BRANCH="\${{ github.event.pull_request.head.ref }}"
|
|
477
|
+
if echo "$BRANCH" | grep -qE '^(investigate|issue)/'; then
|
|
478
|
+
echo "Skipping review for whitesmith-managed branch: $BRANCH"
|
|
479
|
+
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
|
480
|
+
else
|
|
481
|
+
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
review:
|
|
485
|
+
needs: check
|
|
486
|
+
if: >-
|
|
487
|
+
(github.event_name == 'workflow_dispatch') ||
|
|
488
|
+
(needs.check.outputs.should_run == 'true')`
|
|
489
|
+
: `\
|
|
490
|
+
review:`;
|
|
491
|
+
return `\
|
|
492
|
+
name: whitesmith-review
|
|
493
|
+
|
|
494
|
+
on:
|
|
495
|
+
pull_request:
|
|
496
|
+
types: [opened, synchronize]
|
|
497
|
+
workflow_dispatch:
|
|
498
|
+
inputs:
|
|
499
|
+
number:
|
|
500
|
+
description: 'PR or issue number to review'
|
|
501
|
+
required: true
|
|
502
|
+
type:
|
|
503
|
+
description: 'Review type (auto-detected if empty): pr, issue-tasks, issue-tasks-completed'
|
|
504
|
+
provider:
|
|
505
|
+
description: 'AI provider (overrides WHITESMITH_PROVIDER)'
|
|
506
|
+
model:
|
|
507
|
+
description: 'AI model (overrides WHITESMITH_MODEL)'
|
|
508
|
+
|
|
509
|
+
env:
|
|
510
|
+
${envBlock}
|
|
511
|
+
|
|
512
|
+
concurrency:
|
|
513
|
+
group: whitesmith-review-\${{ github.event.pull_request.number || inputs.number }}
|
|
514
|
+
cancel-in-progress: true
|
|
515
|
+
|
|
516
|
+
permissions:
|
|
517
|
+
contents: read
|
|
518
|
+
issues: write
|
|
519
|
+
pull-requests: write
|
|
520
|
+
|
|
521
|
+
jobs:
|
|
522
|
+
${skipWhitesmithCheck}
|
|
523
|
+
runs-on: ubuntu-latest
|
|
524
|
+
steps:
|
|
525
|
+
- uses: actions/checkout@v4
|
|
526
|
+
with:
|
|
527
|
+
fetch-depth: 0
|
|
528
|
+
|
|
529
|
+
- uses: ./.github/actions/setup-whitesmith
|
|
530
|
+
|
|
531
|
+
- if: github.event_name == 'pull_request'
|
|
532
|
+
run: |
|
|
533
|
+
whitesmith review . \\
|
|
534
|
+
--number "\${{ github.event.pull_request.number }}" \\
|
|
535
|
+
--provider "\${{ env.WHITESMITH_PROVIDER }}" \\
|
|
536
|
+
--model "\${{ env.WHITESMITH_MODEL }}" \\
|
|
537
|
+
--post
|
|
538
|
+
|
|
539
|
+
- if: github.event_name == 'workflow_dispatch'
|
|
540
|
+
run: |
|
|
541
|
+
TYPE_FLAG=""
|
|
542
|
+
if [ -n "\${{ inputs.type }}" ]; then
|
|
543
|
+
TYPE_FLAG="--type \${{ inputs.type }}"
|
|
544
|
+
fi
|
|
545
|
+
whitesmith review . \\
|
|
546
|
+
--number "\${{ inputs.number }}" \\
|
|
547
|
+
\$TYPE_FLAG \\
|
|
548
|
+
--provider "\${{ inputs.provider || env.WHITESMITH_PROVIDER }}" \\
|
|
549
|
+
--model "\${{ inputs.model || env.WHITESMITH_MODEL }}" \\
|
|
550
|
+
--post
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
function generateIssueWorkflow(config) {
|
|
554
|
+
const envBlock = generateTopLevelEnv(config);
|
|
555
|
+
return `\
|
|
556
|
+
name: whitesmith-issue
|
|
557
|
+
|
|
558
|
+
on:
|
|
559
|
+
issues:
|
|
560
|
+
types: [opened]
|
|
561
|
+
|
|
562
|
+
env:
|
|
563
|
+
${envBlock}
|
|
564
|
+
|
|
565
|
+
concurrency:
|
|
566
|
+
group: whitesmith-issue-\${{ github.event.issue.number }}
|
|
567
|
+
cancel-in-progress: false
|
|
568
|
+
|
|
569
|
+
permissions:
|
|
570
|
+
contents: write
|
|
571
|
+
issues: write
|
|
572
|
+
pull-requests: write
|
|
573
|
+
|
|
574
|
+
jobs:
|
|
575
|
+
run:
|
|
576
|
+
runs-on: ubuntu-latest
|
|
577
|
+
steps:
|
|
578
|
+
- uses: actions/checkout@v4
|
|
579
|
+
with:
|
|
580
|
+
fetch-depth: 0
|
|
581
|
+
|
|
582
|
+
- uses: ./.github/actions/setup-whitesmith
|
|
583
|
+
|
|
584
|
+
- run: |
|
|
585
|
+
whitesmith run . \\
|
|
586
|
+
--issue "\${{ github.event.issue.number }}" \\
|
|
587
|
+
--provider "$WHITESMITH_PROVIDER" \\
|
|
588
|
+
--model "$WHITESMITH_MODEL" \\
|
|
589
|
+
--max-iterations 10
|
|
590
|
+
`;
|
|
591
|
+
}
|
|
592
|
+
function generateReconcileWorkflow(config) {
|
|
593
|
+
const envBlock = generateTopLevelEnv(config);
|
|
470
594
|
return `\
|
|
471
595
|
name: whitesmith-reconcile
|
|
472
596
|
|
|
@@ -475,42 +599,71 @@ on:
|
|
|
475
599
|
types: [closed]
|
|
476
600
|
branches: [main]
|
|
477
601
|
|
|
602
|
+
env:
|
|
603
|
+
${envBlock}
|
|
604
|
+
|
|
478
605
|
permissions:
|
|
479
|
-
contents:
|
|
606
|
+
contents: write
|
|
480
607
|
issues: write
|
|
481
|
-
pull-requests:
|
|
608
|
+
pull-requests: write
|
|
482
609
|
|
|
483
610
|
jobs:
|
|
484
|
-
|
|
611
|
+
parse:
|
|
485
612
|
if: github.event.pull_request.merged == true
|
|
486
613
|
runs-on: ubuntu-latest
|
|
614
|
+
outputs:
|
|
615
|
+
issue_number: \${{ steps.parse.outputs.issue_number }}
|
|
616
|
+
branch_type: \${{ steps.parse.outputs.branch_type }}
|
|
487
617
|
steps:
|
|
488
|
-
-
|
|
618
|
+
- id: parse
|
|
619
|
+
run: |
|
|
620
|
+
BRANCH="\${{ github.event.pull_request.head.ref }}"
|
|
621
|
+
INVESTIGATE_NUM=$(echo "$BRANCH" | sed -n 's|^investigate/\\([0-9]*\\)$|\\1|p')
|
|
622
|
+
ISSUE_NUM=$(echo "$BRANCH" | sed -n 's|^issue/\\([0-9]*\\)$|\\1|p')
|
|
623
|
+
if [ -n "$INVESTIGATE_NUM" ]; then
|
|
624
|
+
echo "issue_number=$INVESTIGATE_NUM" >> "$GITHUB_OUTPUT"
|
|
625
|
+
echo "branch_type=investigate" >> "$GITHUB_OUTPUT"
|
|
626
|
+
elif [ -n "$ISSUE_NUM" ]; then
|
|
627
|
+
echo "issue_number=$ISSUE_NUM" >> "$GITHUB_OUTPUT"
|
|
628
|
+
echo "branch_type=issue" >> "$GITHUB_OUTPUT"
|
|
629
|
+
else
|
|
630
|
+
echo "branch_type=other" >> "$GITHUB_OUTPUT"
|
|
631
|
+
fi
|
|
489
632
|
|
|
490
|
-
|
|
491
|
-
|
|
633
|
+
implement:
|
|
634
|
+
needs: parse
|
|
635
|
+
if: needs.parse.outputs.branch_type == 'investigate'
|
|
636
|
+
runs-on: ubuntu-latest
|
|
637
|
+
concurrency:
|
|
638
|
+
group: whitesmith-issue-\${{ needs.parse.outputs.issue_number }}
|
|
639
|
+
cancel-in-progress: false
|
|
640
|
+
steps:
|
|
641
|
+
- uses: actions/checkout@v4
|
|
492
642
|
with:
|
|
493
|
-
|
|
643
|
+
fetch-depth: 0
|
|
494
644
|
|
|
495
|
-
-
|
|
496
|
-
id: npm-prefix
|
|
497
|
-
run: echo "dir=$(npm prefix -g)" >> "$GITHUB_OUTPUT"
|
|
645
|
+
- uses: ./.github/actions/setup-whitesmith
|
|
498
646
|
|
|
499
|
-
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
647
|
+
- run: |
|
|
648
|
+
whitesmith run . \\
|
|
649
|
+
--issue "\${{ needs.parse.outputs.issue_number }}" \\
|
|
650
|
+
--provider "$WHITESMITH_PROVIDER" \\
|
|
651
|
+
--model "$WHITESMITH_MODEL" \\
|
|
652
|
+
--max-iterations 10
|
|
653
|
+
|
|
654
|
+
reconcile:
|
|
655
|
+
needs: parse
|
|
656
|
+
if: needs.parse.outputs.branch_type != 'investigate'
|
|
657
|
+
runs-on: ubuntu-latest
|
|
658
|
+
concurrency:
|
|
659
|
+
group: \${{ (needs.parse.outputs.issue_number && format('whitesmith-issue-{0}', needs.parse.outputs.issue_number)) || 'whitesmith-reconcile-other' }}
|
|
660
|
+
cancel-in-progress: false
|
|
661
|
+
steps:
|
|
662
|
+
- uses: actions/checkout@v4
|
|
505
663
|
|
|
506
|
-
-
|
|
507
|
-
if: steps.npm-cache.outputs.cache-hit != 'true'
|
|
508
|
-
run: npm install -g whitesmith
|
|
664
|
+
- uses: ./.github/actions/setup-whitesmith
|
|
509
665
|
|
|
510
|
-
-
|
|
511
|
-
env:
|
|
512
|
-
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
513
|
-
run: whitesmith reconcile .
|
|
666
|
+
- run: whitesmith reconcile .
|
|
514
667
|
`;
|
|
515
668
|
}
|
|
516
669
|
// ─── Refresh OAuth Script (auth-json mode only) ─────────────────────────────
|
|
@@ -608,30 +761,58 @@ if (repo && token) {
|
|
|
608
761
|
console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
|
|
609
762
|
}
|
|
610
763
|
`;
|
|
764
|
+
/**
|
|
765
|
+
* Load config from a JSON file, skipping interactive prompts.
|
|
766
|
+
*/
|
|
767
|
+
function loadConfigFile(filePath) {
|
|
768
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
769
|
+
const data = JSON.parse(raw);
|
|
770
|
+
if (!data.providers || !Array.isArray(data.providers) || data.providers.length === 0) {
|
|
771
|
+
throw new Error(`Config file must contain a non-empty "providers" array`);
|
|
772
|
+
}
|
|
773
|
+
if (!data.defaultProvider) {
|
|
774
|
+
throw new Error(`Config file must contain "defaultProvider"`);
|
|
775
|
+
}
|
|
776
|
+
if (!data.defaultModel) {
|
|
777
|
+
throw new Error(`Config file must contain "defaultModel"`);
|
|
778
|
+
}
|
|
779
|
+
return data;
|
|
780
|
+
}
|
|
611
781
|
export async function installGitHubCI(ctx, options) {
|
|
612
782
|
const { authMode } = options;
|
|
783
|
+
const fake = options.fake ?? false;
|
|
784
|
+
const exportConfig = options.exportConfig ?? undefined;
|
|
613
785
|
console.log('=== whitesmith install-ci (GitHub) ===\n');
|
|
614
786
|
console.log(`Auth mode: ${authMode}\n`);
|
|
615
787
|
let repo = ctx.repo;
|
|
616
|
-
if (!repo && authMode === 'models-json' && ctx.ghAvailable) {
|
|
788
|
+
if (!exportConfig && !fake && !repo && authMode === 'models-json' && ctx.ghAvailable) {
|
|
617
789
|
repo = await input({
|
|
618
790
|
message: 'GitHub repository (owner/repo) — needed to set secrets:',
|
|
619
791
|
});
|
|
620
792
|
ctx.repo = repo;
|
|
621
793
|
}
|
|
622
|
-
let providers
|
|
794
|
+
let providers;
|
|
623
795
|
let defaultProvider;
|
|
624
796
|
let defaultModel;
|
|
625
|
-
|
|
626
|
-
|
|
797
|
+
let loadedSecrets;
|
|
798
|
+
if (options.configFile) {
|
|
799
|
+
// Load from file — no prompts
|
|
800
|
+
const loaded = loadConfigFile(options.configFile);
|
|
801
|
+
providers = loaded.providers;
|
|
802
|
+
defaultProvider = loaded.defaultProvider;
|
|
803
|
+
defaultModel = loaded.defaultModel;
|
|
804
|
+
loadedSecrets = loaded.secrets;
|
|
805
|
+
}
|
|
806
|
+
else if (authMode === 'models-json') {
|
|
807
|
+
// Interactive prompts
|
|
627
808
|
providers = await promptProviders();
|
|
628
|
-
// Pick defaults
|
|
629
809
|
const defaults = await promptDefaults(providers);
|
|
630
810
|
defaultProvider = defaults.provider;
|
|
631
811
|
defaultModel = defaults.model;
|
|
632
812
|
}
|
|
633
813
|
else {
|
|
634
814
|
// auth.json mode — still need provider/model for whitesmith commands
|
|
815
|
+
providers = [];
|
|
635
816
|
defaultProvider = await input({
|
|
636
817
|
message: 'Default AI provider:',
|
|
637
818
|
default: 'anthropic',
|
|
@@ -641,22 +822,75 @@ export async function installGitHubCI(ctx, options) {
|
|
|
641
822
|
default: 'claude-sonnet-4-20250514',
|
|
642
823
|
});
|
|
643
824
|
}
|
|
825
|
+
// ── Export config mode — write JSON to stdout and exit ───────────────
|
|
826
|
+
if (exportConfig) {
|
|
827
|
+
const configFile = { providers, defaultProvider, defaultModel };
|
|
828
|
+
if (options.includeSecrets && providers.length > 0) {
|
|
829
|
+
const secrets = {};
|
|
830
|
+
const seen = new Set();
|
|
831
|
+
for (const p of providers) {
|
|
832
|
+
if (seen.has(p.apiKeyEnvVar))
|
|
833
|
+
continue;
|
|
834
|
+
seen.add(p.apiKeyEnvVar);
|
|
835
|
+
const key = await password({
|
|
836
|
+
message: `Enter API key for ${p.name} (${p.apiKeyEnvVar}):`,
|
|
837
|
+
});
|
|
838
|
+
if (key)
|
|
839
|
+
secrets[p.apiKeyEnvVar] = key;
|
|
840
|
+
}
|
|
841
|
+
if (Object.keys(secrets).length > 0) {
|
|
842
|
+
configFile.secrets = secrets;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const json = JSON.stringify(configFile, null, 2) + '\n';
|
|
846
|
+
fs.writeFileSync(exportConfig, json, 'utf-8');
|
|
847
|
+
console.log(`\n✅ Config written to ${exportConfig}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
// Auto-detect dev mode: check if we're inside the whitesmith repo itself
|
|
851
|
+
let dev = options.dev ?? false;
|
|
852
|
+
if (!dev) {
|
|
853
|
+
try {
|
|
854
|
+
const pkgPath = path.join(ctx.workDir, 'package.json');
|
|
855
|
+
if (fs.existsSync(pkgPath)) {
|
|
856
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
857
|
+
if (pkg.name === 'whitesmith') {
|
|
858
|
+
dev = true;
|
|
859
|
+
console.log('📦 Detected whitesmith repo — using dev mode (build from source)\n');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
// Ignore — not in whitesmith repo
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const reviewWorkflow = options.reviewWorkflow ?? false;
|
|
868
|
+
const reviewStepEnabled = options.reviewStepEnabled ?? true;
|
|
644
869
|
const config = {
|
|
645
870
|
authMode,
|
|
646
871
|
providers,
|
|
647
872
|
defaultProvider,
|
|
648
873
|
defaultModel,
|
|
874
|
+
dev,
|
|
875
|
+
reviewWorkflow,
|
|
876
|
+
reviewStepEnabled,
|
|
877
|
+
version: options.version,
|
|
649
878
|
};
|
|
650
879
|
// ── Set GitHub secrets via gh CLI ─────────────────────────────────────
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
880
|
+
const skipSecrets = options.skipSecrets ?? false;
|
|
881
|
+
if (skipSecrets) {
|
|
882
|
+
console.log('\n🔑 Skipping secret setup (--no-secrets)');
|
|
883
|
+
}
|
|
884
|
+
else if (!fake && authMode === 'models-json' && repo) {
|
|
653
885
|
if (!ctx.ghAvailable) {
|
|
654
886
|
console.log('\n⚠ GitHub CLI (gh) is not available or not authenticated.');
|
|
655
887
|
console.log(' You will need to set the following secrets manually.\n');
|
|
656
888
|
}
|
|
657
889
|
else {
|
|
890
|
+
// If config file included secrets, set them directly without prompting
|
|
891
|
+
const configSecrets = options.configFile ? loadedSecrets : undefined;
|
|
658
892
|
console.log('\n🔑 Setting API key secrets on GitHub...\n');
|
|
659
|
-
const setSecrets = await
|
|
893
|
+
const setSecrets = await setOrPromptSecrets(ctx, providers, configSecrets);
|
|
660
894
|
const allEnvVars = [...new Set(providers.map((p) => p.apiKeyEnvVar))];
|
|
661
895
|
const missing = allEnvVars.filter((v) => !setSecrets.includes(v));
|
|
662
896
|
if (missing.length > 0) {
|
|
@@ -672,10 +906,16 @@ export async function installGitHubCI(ctx, options) {
|
|
|
672
906
|
}
|
|
673
907
|
// ── Generate and write workflow files ─────────────────────────────────
|
|
674
908
|
const outputBase = fake ? '.fake' : '.github';
|
|
675
|
-
const
|
|
676
|
-
const workflowsDir = path.join(
|
|
909
|
+
const baseDir = path.join(ctx.workDir, outputBase);
|
|
910
|
+
const workflowsDir = path.join(baseDir, 'workflows');
|
|
911
|
+
const actionsDir = path.join(baseDir, 'actions', 'setup-whitesmith');
|
|
677
912
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
913
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
678
914
|
const files = [
|
|
915
|
+
{
|
|
916
|
+
path: path.join(actionsDir, 'action.yml'),
|
|
917
|
+
content: generateSetupAction(config),
|
|
918
|
+
},
|
|
679
919
|
{
|
|
680
920
|
path: path.join(workflowsDir, 'whitesmith.yml'),
|
|
681
921
|
content: generateMainWorkflow(config),
|
|
@@ -684,13 +924,23 @@ export async function installGitHubCI(ctx, options) {
|
|
|
684
924
|
path: path.join(workflowsDir, 'whitesmith-comment.yml'),
|
|
685
925
|
content: generateCommentWorkflow(config),
|
|
686
926
|
},
|
|
927
|
+
{
|
|
928
|
+
path: path.join(workflowsDir, 'whitesmith-issue.yml'),
|
|
929
|
+
content: generateIssueWorkflow(config),
|
|
930
|
+
},
|
|
687
931
|
{
|
|
688
932
|
path: path.join(workflowsDir, 'whitesmith-reconcile.yml'),
|
|
689
|
-
content: generateReconcileWorkflow(),
|
|
933
|
+
content: generateReconcileWorkflow(config),
|
|
690
934
|
},
|
|
691
935
|
];
|
|
936
|
+
if (config.reviewWorkflow) {
|
|
937
|
+
files.push({
|
|
938
|
+
path: path.join(workflowsDir, 'whitesmith-review.yml'),
|
|
939
|
+
content: generateReviewWorkflow(config),
|
|
940
|
+
});
|
|
941
|
+
}
|
|
692
942
|
if (authMode === 'auth-json') {
|
|
693
|
-
const scriptsDir = path.join(
|
|
943
|
+
const scriptsDir = path.join(baseDir, 'scripts');
|
|
694
944
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
695
945
|
files.push({
|
|
696
946
|
path: path.join(scriptsDir, 'refresh-oauth-token.mjs'),
|