code-foundry 1.48.3 → 1.48.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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Code Foundry configuration",
4
+ "type": "object",
5
+ "properties": {
6
+ "formatter": {
7
+ "description": "Explicit JavaScript formatter. Omit to preserve repository scripts and the Oxfmt fallback.",
8
+ "type": "string",
9
+ "enum": ["oxfmt", "prettier", "bun"]
10
+ }
11
+ },
12
+ "additionalProperties": true
13
+ }
@@ -48,7 +48,7 @@ on:
48
48
  bun-version:
49
49
  required: false
50
50
  type: string
51
- default: '1.4.0'
51
+ default: '1.4.4'
52
52
  wrangler-version:
53
53
  description: 'local uses the locked workspace installation; otherwise specify an exact version.'
54
54
  required: false
@@ -38,7 +38,7 @@ on:
38
38
  bun-version:
39
39
  required: false
40
40
  type: string
41
- default: '1.4.0'
41
+ default: '1.4.4'
42
42
  runner:
43
43
  required: false
44
44
  type: string
@@ -802,7 +802,7 @@ jobs:
802
802
  # fail with `bun: command not found` before publish.
803
803
  uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
804
804
  with:
805
- bun-version: latest
805
+ bun-version: 1.4.4
806
806
  - name: Enable Corepack (pnpm/yarn managers)
807
807
  run: corepack enable
808
808
  - name: Install dependencies
@@ -311,7 +311,7 @@ jobs:
311
311
  steps.audit-toolchain.outputs.package_manager == 'bun'
312
312
  uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
313
313
  with:
314
- bun-version: 1.4.3
314
+ bun-version: 1.4.4
315
315
  - name: Enable Corepack
316
316
  if: >-
317
317
  matrix.entry.language == 'javascript' &&
@@ -352,122 +352,121 @@ jobs:
352
352
  id: integration_execute
353
353
  if: ${{ !cancelled() && inputs.mode == 'audit' && steps.integration_applicability.outputs.applicable == 'true' && steps.integration_applicability.outputs.affected != 'false' }}
354
354
  run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci integration
355
- # Receipts are independent artifact uploads; a second parallel
356
- # group joins them instead of serializing 8+ uploads after the
357
- # executes.
358
- - parallel:
359
- - name: Retain format task receipt
360
- if: >-
361
- always() &&
362
- (steps.format_execute.outcome == 'success' || steps.format_execute.outcome == 'failure' ||
363
- steps.format_applicability.outputs.applicable == 'false' || steps.format_applicability.outputs.affected == 'false') &&
364
- hashFiles('.code-foundry/results/format.json') != ''
365
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
366
- with:
367
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-format
368
- path: .code-foundry/results/format.json
369
- include-hidden-files: true
370
- if-no-files-found: error
371
- retention-days: 14
372
- - name: Retain build task receipt
373
- if: >-
374
- always() &&
375
- (steps.build_execute.outcome == 'success' || steps.build_execute.outcome == 'failure' ||
376
- steps.build_applicability.outputs.applicable == 'false' || steps.build_applicability.outputs.affected == 'false') &&
377
- hashFiles('.code-foundry/results/build.json') != ''
378
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
379
- with:
380
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-build
381
- path: .code-foundry/results/build.json
382
- include-hidden-files: true
383
- if-no-files-found: error
384
- retention-days: 14
385
- - name: Retain performance task receipt
386
- if: >-
387
- always() &&
388
- inputs.mode == 'audit' &&
389
- (steps.performance_execute.outcome == 'success' || steps.performance_execute.outcome == 'failure' ||
390
- steps.performance_applicability.outputs.applicable == 'false' || steps.performance_applicability.outputs.affected == 'false') &&
391
- hashFiles('.code-foundry/results/performance.json') != ''
392
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
393
- with:
394
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-performance
395
- path: .code-foundry/results/performance.json
396
- include-hidden-files: true
397
- if-no-files-found: error
398
- retention-days: 14
399
- - name: Upload performance results
400
- if: >-
401
- always() &&
402
- inputs.mode == 'audit' &&
403
- steps.performance_applicability.outputs.applicable == 'true' &&
404
- hashFiles('artifacts/performance/**', 'performance-results.json', 'performance-results/**') != ''
405
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
406
- with:
407
- name: performance-${{ github.run_id }}
408
- path: |
409
- artifacts/performance/**
410
- performance-results.json
411
- performance-results/**
412
- compression-level: 1
413
- if-no-files-found: ignore
414
- retention-days: 14
415
- - name: Retain smoke task receipt
416
- if: >-
417
- always() &&
418
- inputs.mode == 'audit' &&
419
- (steps.smoke_execute.outcome == 'success' || steps.smoke_execute.outcome == 'failure' ||
420
- steps.smoke_applicability.outputs.applicable == 'false' || steps.smoke_applicability.outputs.affected == 'false') &&
421
- hashFiles('.code-foundry/results/smoke.json') != ''
422
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
423
- with:
424
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-smoke
425
- path: .code-foundry/results/smoke.json
426
- include-hidden-files: true
427
- if-no-files-found: error
428
- retention-days: 14
429
- - name: Retain eval task receipt
430
- if: >-
431
- always() &&
432
- inputs.mode == 'audit' &&
433
- (steps.eval_execute.outcome == 'success' || steps.eval_execute.outcome == 'failure' ||
434
- steps.eval_applicability.outputs.applicable == 'false' || steps.eval_applicability.outputs.affected == 'false') &&
435
- hashFiles('.code-foundry/results/eval.json') != ''
436
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
437
- with:
438
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval
439
- path: .code-foundry/results/eval.json
440
- include-hidden-files: true
441
- if-no-files-found: error
442
- retention-days: 14
443
- - name: Retain eval report
444
- if: >-
445
- always() &&
446
- inputs.mode == 'audit' &&
447
- steps.eval_execute.outcome == 'failure' &&
448
- hashFiles('eval-results/result.json') != ''
449
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
450
- with:
451
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval-report
452
- path: |
453
- eval-results/result.json
454
- eval-results/summary.json
455
- if-no-files-found: error
456
- retention-days: 14
457
- - name: Retain integration task receipt
458
- if: >-
459
- always() &&
460
- inputs.mode == 'audit' &&
461
- (steps.integration_execute.outcome == 'success' || steps.integration_execute.outcome == 'failure' ||
462
- steps.integration_applicability.outputs.applicable == 'false' || steps.integration_applicability.outputs.affected == 'false') &&
463
- hashFiles('.code-foundry/results/integration.json') != ''
464
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
465
- with:
466
- name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-integration
467
- path: .code-foundry/results/integration.json
468
- include-hidden-files: true
469
- if-no-files-found: error
470
- retention-days: 14
355
+ # Upload only after execution joins, and keep uploads sequential: the
356
+ # runner rewrites GITHUB_EVENT_PATH for each action, racing the event
357
+ # JSON read in upload-artifact's context initialization (runner#4641).
358
+ - name: Retain format task receipt
359
+ if: >-
360
+ always() &&
361
+ (steps.format_execute.outcome == 'success' || steps.format_execute.outcome == 'failure' ||
362
+ steps.format_applicability.outputs.applicable == 'false' || steps.format_applicability.outputs.affected == 'false') &&
363
+ hashFiles('.code-foundry/results/format.json') != ''
364
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
365
+ with:
366
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-format
367
+ path: .code-foundry/results/format.json
368
+ include-hidden-files: true
369
+ if-no-files-found: error
370
+ retention-days: 14
371
+ - name: Retain build task receipt
372
+ if: >-
373
+ always() &&
374
+ (steps.build_execute.outcome == 'success' || steps.build_execute.outcome == 'failure' ||
375
+ steps.build_applicability.outputs.applicable == 'false' || steps.build_applicability.outputs.affected == 'false') &&
376
+ hashFiles('.code-foundry/results/build.json') != ''
377
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
378
+ with:
379
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-build
380
+ path: .code-foundry/results/build.json
381
+ include-hidden-files: true
382
+ if-no-files-found: error
383
+ retention-days: 14
384
+ - name: Retain performance task receipt
385
+ if: >-
386
+ always() &&
387
+ inputs.mode == 'audit' &&
388
+ (steps.performance_execute.outcome == 'success' || steps.performance_execute.outcome == 'failure' ||
389
+ steps.performance_applicability.outputs.applicable == 'false' || steps.performance_applicability.outputs.affected == 'false') &&
390
+ hashFiles('.code-foundry/results/performance.json') != ''
391
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
392
+ with:
393
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-performance
394
+ path: .code-foundry/results/performance.json
395
+ include-hidden-files: true
396
+ if-no-files-found: error
397
+ retention-days: 14
398
+ - name: Upload performance results
399
+ if: >-
400
+ always() &&
401
+ inputs.mode == 'audit' &&
402
+ steps.performance_applicability.outputs.applicable == 'true' &&
403
+ hashFiles('artifacts/performance/**', 'performance-results.json', 'performance-results/**') != ''
404
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
405
+ with:
406
+ name: performance-${{ github.run_id }}
407
+ path: |
408
+ artifacts/performance/**
409
+ performance-results.json
410
+ performance-results/**
411
+ compression-level: 1
412
+ if-no-files-found: ignore
413
+ retention-days: 14
414
+ - name: Retain smoke task receipt
415
+ if: >-
416
+ always() &&
417
+ inputs.mode == 'audit' &&
418
+ (steps.smoke_execute.outcome == 'success' || steps.smoke_execute.outcome == 'failure' ||
419
+ steps.smoke_applicability.outputs.applicable == 'false' || steps.smoke_applicability.outputs.affected == 'false') &&
420
+ hashFiles('.code-foundry/results/smoke.json') != ''
421
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
422
+ with:
423
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-smoke
424
+ path: .code-foundry/results/smoke.json
425
+ include-hidden-files: true
426
+ if-no-files-found: error
427
+ retention-days: 14
428
+ - name: Retain eval task receipt
429
+ if: >-
430
+ always() &&
431
+ inputs.mode == 'audit' &&
432
+ (steps.eval_execute.outcome == 'success' || steps.eval_execute.outcome == 'failure' ||
433
+ steps.eval_applicability.outputs.applicable == 'false' || steps.eval_applicability.outputs.affected == 'false') &&
434
+ hashFiles('.code-foundry/results/eval.json') != ''
435
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
436
+ with:
437
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval
438
+ path: .code-foundry/results/eval.json
439
+ include-hidden-files: true
440
+ if-no-files-found: error
441
+ retention-days: 14
442
+ - name: Retain eval report
443
+ if: >-
444
+ always() &&
445
+ inputs.mode == 'audit' &&
446
+ steps.eval_execute.outcome == 'failure' &&
447
+ hashFiles('eval-results/result.json') != ''
448
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
449
+ with:
450
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval-report
451
+ path: |
452
+ eval-results/result.json
453
+ eval-results/summary.json
454
+ if-no-files-found: error
455
+ retention-days: 14
456
+ - name: Retain integration task receipt
457
+ if: >-
458
+ always() &&
459
+ inputs.mode == 'audit' &&
460
+ (steps.integration_execute.outcome == 'success' || steps.integration_execute.outcome == 'failure' ||
461
+ steps.integration_applicability.outputs.applicable == 'false' || steps.integration_applicability.outputs.affected == 'false') &&
462
+ hashFiles('.code-foundry/results/integration.json') != ''
463
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
464
+ with:
465
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-integration
466
+ path: .code-foundry/results/integration.json
467
+ include-hidden-files: true
468
+ if-no-files-found: error
469
+ retention-days: 14
471
470
 
472
471
  - name: Save Vite+ task cache
473
472
  uses: ./.github/actions/vite-plus-cache-save
@@ -950,7 +949,7 @@ jobs:
950
949
  steps.audit-toolchain.outputs.package_manager == 'bun'
951
950
  uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
952
951
  with:
953
- bun-version: 1.4.3
952
+ bun-version: 1.4.4
954
953
  - name: Enable Corepack
955
954
  if: >-
956
955
  steps.profile.outputs.javascript == 'true' &&
package/.mise.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tools]
2
2
  node = "24.18.0"
3
- bun = "1.4.3"
3
+ bun = "1.4.4"
4
4
 
5
5
  [settings]
6
6
  experimental = true
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.48.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.4...v1.48.5) (2026-10-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** serialize billing receipt and report uploads ([#773](https://github.com/0xPlayerOne/code-foundry/issues/773)) ([6497225](https://github.com/0xPlayerOne/code-foundry/commit/6497225bf2d672380b6b5231378b424dffb8afbb))
9
+
10
+ ## [1.48.4](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.3...v1.48.4) (2026-10-22)
11
+
12
+
13
+ ### Maintenance
14
+
15
+ * bun-1.4.4-cloud ([#771](https://github.com/0xPlayerOne/code-foundry/issues/771)) ([c99f0ed](https://github.com/0xPlayerOne/code-foundry/commit/c99f0ed8b66129f3b599326bc67d900719b921f4))
16
+
3
17
  ## [1.48.3](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.2...v1.48.3) (2026-10-17)
4
18
 
5
19
 
@@ -126,23 +140,9 @@
126
140
 
127
141
  * **billing:** emit the codeql input as a YAML boolean ([#735](https://github.com/0xPlayerOne/code-foundry/issues/735)) ([e0b348c](https://github.com/0xPlayerOne/code-foundry/commit/e0b348c9a3c730e46210fe0b9dc46af49c9db605))
128
142
 
129
- ## [1.45.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.45.0...v1.45.1) (2026-10-06)
130
-
131
-
132
- ### Bug Fixes
133
-
134
- * **billing:** declare the runner inputs the callers always forward ([#733](https://github.com/0xPlayerOne/code-foundry/issues/733)) ([f7a29cb](https://github.com/0xPlayerOne/code-foundry/commit/f7a29cb6dbc9a8695a9bef9e85ccce82aa9c84db))
135
-
136
- ## [1.45.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.44.11...v1.45.0) (2026-10-06)
137
-
138
-
139
- ### Features
140
-
141
- * add a merged-lane billing orchestrator for minute-billed consumers ([#731](https://github.com/0xPlayerOne/code-foundry/issues/731)) ([a91402e](https://github.com/0xPlayerOne/code-foundry/commit/a91402e701f458ece0d2ea4715f9586a06b15108))
142
-
143
143
  <!-- code-foundry: packaged changelog -->
144
144
 
145
145
  ---
146
146
 
147
- This packaged changelog lists the 20 most recent releases; 251 older releases are omitted to keep the package small.
147
+ This packaged changelog lists the 20 most recent releases; 253 older releases are omitted to keep the package small.
148
148
  See the [full changelog](https://github.com/0xPlayerOne/code-foundry/blob/main/CHANGELOG.md) for the complete history.
@@ -1,5 +1,43 @@
1
1
  # Configuration reference
2
2
 
3
+ ## JavaScript formatter
4
+
5
+ Set `formatter: oxfmt`, `formatter: prettier`, or `formatter: bun` in
6
+ `.github/code-foundry.yml` to select the formatter explicitly. The shipped
7
+ `.github/code-foundry.schema.json` describes this enum; sync, doctor, task
8
+ discovery and execution reject unknown choices. With no key, existing package
9
+ format scripts retain precedence and the existing Oxfmt fallback is unchanged.
10
+ Current application repositories continue to use Oxfmt.
11
+
12
+ An explicit choice controls the format gate without changing lint, type-check,
13
+ test or build scripts. Sync retains repository-owned Prettier configuration
14
+ for Prettier/Bun choices, and only installs/migrates the Oxfmt baseline for
15
+ Oxfmt. It does not install packages or rewrite dependency lockfiles: install
16
+ the selected Prettier/Oxfmt dependency using the repository's package manager,
17
+ or install official Bun 1.4.4 for the Bun choice.
18
+
19
+ `code-foundry format --check` checks the selected formatter; omit `--check`
20
+ to write. Bun opt-in also supports `code-foundry format --staged` and
21
+ `code-foundry format --changed REF`, with optional `--check`. These invoke
22
+ `bun fmt --no-script --staged` and `bun fmt --no-script --changed=REF`.
23
+ The scopes are mutually exclusive and fail for other formatters. Changed mode
24
+ does not stage files. Staged write mode restages formatted files and refuses
25
+ partially staged files before running Bun, protecting intentionally unstaged
26
+ edits. The commit gate uses check-only staged formatting and continues its
27
+ independent staged lint, type-check, Python and Rust checks.
28
+
29
+ For Bun opt-in, a hook can replace its **formatter-only** lint-staged operation
30
+ with `code-foundry format --staged`. Retain every other lint-staged lint/test
31
+ command and any hook ordering. Remove the lint-staged dependency only when no
32
+ remaining command needs it, and regenerate the dependency lockfile normally.
33
+ Sync never removes lint-staged, its configuration or other hook operations
34
+ automatically. Oxfmt/Prettier consumers retain them unchanged.
35
+
36
+ Type-check-only native fallbacks use `bun check --no-script -p tsconfig.json`
37
+ on Bun 1.4.4. Repository scripts remain authoritative, including framework
38
+ checks and declaration-emitting builds. Bun's checker emits no files; it must
39
+ not replace a `tsc` build that emits declarations or other compiler output.
40
+
3
41
  Code Foundry has one repository-owned control plane:
4
42
  `.github/code-foundry.yml`. `init` creates it, `sync` renders the selected
5
43
  baseline, and `doctor` checks local and GitHub-facing prerequisites.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.48.3",
3
+ "version": "1.48.5",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
@@ -47,7 +47,7 @@
47
47
  "performance:check": "node scripts/performance-check.mjs",
48
48
  "test": "node --test",
49
49
  "test:unit": "node --test",
50
- "type-check": "tsc --project jsconfig.json",
50
+ "type-check": "bun check --no-script -p jsconfig.json",
51
51
  "pack:check": "npm pack --dry-run",
52
52
  "prepublishOnly": "npm test && npm run pack:check"
53
53
  },
@@ -60,5 +60,5 @@
60
60
  "engines": {
61
61
  "node": ">=24"
62
62
  },
63
- "packageManager": "bun@1.4.3"
63
+ "packageManager": "bun@1.4.4"
64
64
  }
package/src/cli.mjs CHANGED
@@ -16,6 +16,7 @@ Usage:
16
16
  npx code-foundry init [--target PATH]
17
17
  npx code-foundry sync [--target PATH]
18
18
  npx code-foundry doctor [--target PATH]
19
+ npx code-foundry format [--target PATH] [--check] [--staged | --changed REF]
19
20
  npx code-foundry plan [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--json]
20
21
  npx code-foundry check [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--timeout SECONDS] [--json]
21
22
  npx code-foundry affected [--target PATH] [--base REF] [--head REF] [--tool auto|turbo|vite-plus]
@@ -179,6 +180,19 @@ function parseArgs(argv) {
179
180
  }
180
181
 
181
182
  async function main() {
183
+ if (process.argv[2] === 'format') {
184
+ if (process.argv.slice(3).some((arg) => arg === '--help' || arg === '-h')) {
185
+ console.log('Usage: code-foundry format [--target PATH] [--check] [--staged | --changed REF]')
186
+ return
187
+ }
188
+ try {
189
+ const { formatCommand } = await import('./commands/format.mjs')
190
+ process.exitCode = formatCommand(process.argv.slice(3))
191
+ } catch (error) {
192
+ fail(error instanceof Error ? error.message : String(error))
193
+ }
194
+ return
195
+ }
182
196
  if (['plan', 'check'].includes(process.argv[2])) {
183
197
  const { agentCommand } = await import('./commands/agent-check.mjs')
184
198
  process.exitCode = agentCommand(process.argv.slice(2))
@@ -8,6 +8,7 @@ import { recommendRunners, resolveMonorepoTool, resolveProfile } from '../lib/pr
8
8
  import { packageManagerForLockfile } from '../lib/lockfiles.mjs'
9
9
  import { doctorGithub } from '../lib/github-doctor.mjs'
10
10
  import { isGeneratedEventCaller } from './sync.mjs'
11
+ import { formatterChoice } from '../lib/formatter.mjs'
11
12
 
12
13
  /** @param {string} message */
13
14
  function warn(message) {
@@ -28,6 +29,11 @@ export function doctor(root, options = {}) {
28
29
  }
29
30
 
30
31
  const toolchain = config.toolchain ?? 'auto'
32
+ try {
33
+ formatterChoice(config.formatter)
34
+ } catch (cause) {
35
+ error(cause instanceof Error ? cause.message : String(cause))
36
+ }
31
37
  if (!['auto', 'native', 'mise'].includes(toolchain)) error(`unsupported toolchain: ${toolchain}`)
32
38
  if (toolchain === 'mise' && !existsSync(join(target, '.mise.toml')))
33
39
  error('.mise.toml is required when toolchain: mise')
@@ -237,7 +243,7 @@ export function doctor(root, options = {}) {
237
243
  error('validation caller registers draft-time checks; run code-foundry sync.')
238
244
  }
239
245
  if (
240
- !/uses:\s+(?:\.\/\.github\/workflows\/validation(?:-no-codeql)?\.yml|\S+\/\.github\/workflows\/validation(?:-no-codeql)?\.yml@)/.test(
246
+ !/uses:\s+(?:\.\/\.github\/workflows\/validation(?:-no-codeql|-billing)?\.yml|\S+\/\.github\/workflows\/validation(?:-no-codeql|-billing)?\.yml@)/.test(
241
247
  caller
242
248
  )
243
249
  ) {
@@ -0,0 +1,44 @@
1
+ // @ts-check
2
+
3
+ import { resolve } from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
5
+ import { readConfig } from '../lib/config.mjs'
6
+ import { formatterCommand } from '../lib/formatter.mjs'
7
+
8
+ /** @param {string[]} argv */
9
+ export function formatCommand(argv) {
10
+ let target = process.cwd()
11
+ let check = false
12
+ let staged = false
13
+ /** @type {string|undefined} */
14
+ let changed
15
+ while (argv.length) {
16
+ const arg = argv.shift()
17
+ if (arg === '--target') target = argv.shift() || ''
18
+ else if (arg === '--check') check = true
19
+ else if (arg === '--staged') staged = true
20
+ else if (arg === '--changed') changed = argv.shift() || ''
21
+ else throw new Error(`Unknown format option: ${arg}`)
22
+ }
23
+ if (!target || target.startsWith('-')) throw new Error('--target requires a path')
24
+ target = resolve(target)
25
+ const config = readConfig(resolve(target, '.github/code-foundry.yml'))
26
+ const [tool, ...args] = formatterCommand(config.formatter, { check, staged, changed })
27
+ if (staged && !check) {
28
+ // Bun restages working-tree contents. Refuse partial staging rather than
29
+ // silently including edits that the caller deliberately left out.
30
+ /** @param {string[]} diffArgs */
31
+ const paths = (diffArgs) => {
32
+ const result = spawnSync('git', diffArgs, { cwd: target, encoding: 'utf8' })
33
+ if (result.status !== 0) throw new Error('Cannot inspect staged formatter changes')
34
+ return result.stdout.split('\0').filter(Boolean)
35
+ }
36
+ const index = new Set(paths(['diff', '--cached', '--name-only', '-z']))
37
+ if (paths(['diff', '--name-only', '-z']).some((file) => index.has(file)))
38
+ throw new Error('Partially staged files: format and stage those files explicitly first')
39
+ }
40
+ const command = tool === 'bun' ? [tool, ...args] : ['npx', '--no-install', tool, ...args]
41
+ const result = spawnSync(command[0], command.slice(1), { cwd: target, stdio: 'inherit' })
42
+ if (result.error) throw result.error
43
+ return result.status ?? 1
44
+ }
@@ -27,6 +27,7 @@ import {
27
27
  } from '../lib/config.mjs'
28
28
  import { buildReleaseConfig, buildReleaseManifest } from '../lib/release-manifest.mjs'
29
29
  import { customWorkflowFiles, overlayPolicy } from '../lib/overlay.mjs'
30
+ import { formatterChoice } from '../lib/formatter.mjs'
30
31
  import {
31
32
  inspectQueueCaller,
32
33
  mergeQueueEnabled,
@@ -54,6 +55,7 @@ const standardFiles = [
54
55
  '.github/PULL_REQUEST_TEMPLATE.md',
55
56
  '.github/SECURITY.md',
56
57
  '.github/dependabot.yml',
58
+ '.github/code-foundry.schema.json',
57
59
  '.github/ISSUE_TEMPLATE/bug_report.yml',
58
60
  '.github/ISSUE_TEMPLATE/config.yml',
59
61
  '.github/ISSUE_TEMPLATE/feature_request.yml',
@@ -153,6 +155,7 @@ function synchronize(options) {
153
155
  }
154
156
  const defaults = createDefaultConfig(target, source, existingConfig.git_workflow)
155
157
  let config = { ...defaults, ...existingConfig }
158
+ formatterChoice(config.formatter)
156
159
  const workflow = gitWorkflow(config.git_workflow)
157
160
  if (!['direct', 'staging-release'].includes(workflow)) {
158
161
  throw new Error(`Unsupported git_workflow: ${workflow}; use direct or staging-release.`)
@@ -514,7 +517,7 @@ function synchronize(options) {
514
517
  else rmSync(join(target, file), { force: true })
515
518
  }
516
519
  }
517
- if (includesValue(languages, 'typescript')) {
520
+ if (includesValue(languages, 'typescript') && formatterChoice(config.formatter) === 'oxfmt') {
518
521
  // The Oxfmt baseline supersedes .prettierrc/.prettierignore. Entries the
519
522
  // consumer added beyond the baseline survive the migration inside the
520
523
  // .oxfmtrc.json ignorePatterns list.
@@ -543,7 +546,7 @@ function synchronize(options) {
543
546
  changed,
544
547
  dryRun
545
548
  )
546
- } else {
549
+ } else if (!includesValue(languages, 'typescript')) {
547
550
  for (const file of ['.prettierrc', '.prettierignore']) {
548
551
  if (existsSync(join(target, file))) {
549
552
  changed.push(file)
@@ -690,9 +693,11 @@ function renderReleaseConfig(target, sourceFile, config) {
690
693
 
691
694
  /** @param {string} file @param {string} languages @param {string} features @param {Record<string, string>} config */
692
695
  function shouldInclude(file, languages, features, config) {
696
+ if (file === '.github/code-foundry.schema.json') return includesValue(languages, 'typescript')
693
697
  if (file === 'ruff.toml') return includesValue(languages, 'python')
694
- if (file === '.oxfmtrc.json' || file === '.oxlintrc.json')
695
- return includesValue(languages, 'typescript')
698
+ if (file === '.oxfmtrc.json')
699
+ return includesValue(languages, 'typescript') && formatterChoice(config.formatter) === 'oxfmt'
700
+ if (file === '.oxlintrc.json') return includesValue(languages, 'typescript')
696
701
  if (file === '.github/dependabot.yml')
697
702
  return (
698
703
  configured(config.dependency_updater, 'dependabot') === 'dependabot' &&
@@ -0,0 +1,32 @@
1
+ // @ts-check
2
+
3
+ /** Explicit formatter choices. An absent key preserves repository-script precedence. */
4
+ export const FORMATTERS = Object.freeze(['oxfmt', 'prettier', 'bun'])
5
+
6
+ /** @param {string|undefined} value */
7
+ export function formatterChoice(value) {
8
+ const choice = value || 'oxfmt'
9
+ if (!FORMATTERS.includes(choice))
10
+ throw new Error(`Unsupported formatter: ${choice}; use oxfmt, prettier, or bun.`)
11
+ return choice
12
+ }
13
+
14
+ /** @param {string|undefined} value @param {{check?: boolean, staged?: boolean, changed?: string, files?: string[]}} [options] */
15
+ export function formatterCommand(value, options = {}) {
16
+ const choice = formatterChoice(value)
17
+ const { check = true, staged = false, changed, files = ['.'] } = options
18
+ if (staged && changed !== undefined) throw new Error('--staged and --changed are exclusive')
19
+ if ((staged || changed !== undefined) && choice !== 'bun')
20
+ throw new Error('--staged and --changed require formatter: bun')
21
+ if (changed !== undefined && (!changed.trim() || changed.startsWith('-')))
22
+ throw new Error('--changed requires a Git branch or commit')
23
+ if (choice === 'bun')
24
+ return [
25
+ 'bun',
26
+ 'fmt',
27
+ '--no-script',
28
+ ...(check ? ['--check'] : []),
29
+ ...(staged ? ['--staged'] : changed !== undefined ? [`--changed=${changed}`] : files),
30
+ ]
31
+ return [choice, ...(check ? ['--check'] : ['--write']), ...files]
32
+ }
@@ -20,7 +20,8 @@ const LINTABLE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i
20
20
  const TYPED = /\.(?:ts|tsx|mts|cts)$|(?:^|\/)tsconfig[^/]*\.json$/i
21
21
  /** JavaScript sources, type-checked only when the repository opts in with a root jsconfig. */
22
22
  const JAVASCRIPT = /\.(?:js|jsx|mjs|cjs)$|(?:^|\/)jsconfig[^/]*\.json$/i
23
- const FORMAT_CONFIG = /^(?:\.oxfmtrc\.jsonc?|oxfmt\.config\.[^/]+|\.prettierignore)$/
23
+ const FORMAT_CONFIG =
24
+ /^(?:\.oxfmtrc\.jsonc?|oxfmt\.config\.[^/]+|\.prettierrc(?:\.[^/]+)?|prettier\.config\.[^/]+|\.prettierignore|bunfig\.toml|\.github\/code-foundry\.yml)$/
24
25
  const LINT_CONFIG = /^(?:\.oxlintrc\.json|oxlint\.config\.[^/]+)$/
25
26
  const PYTHON = /\.pyi?$/i
26
27
  const PYTHON_CONFIG =
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process'
5
5
  import { isAbsolute, relative, resolve, sep } from 'node:path'
6
6
  import { createHash } from 'node:crypto'
7
7
  import { listValue, readConfig } from './config.mjs'
8
+ import { formatterChoice } from './formatter.mjs'
8
9
 
9
10
  export const TASKS = Object.freeze([
10
11
  'format',
@@ -36,6 +37,7 @@ export const TASK_SCRIPTS = Object.freeze({
36
37
  /** @param {string} root */
37
38
  export function readTaskPolicy(root) {
38
39
  const config = readConfig(resolve(root, '.github/code-foundry.yml'))
40
+ formatterChoice(config.formatter)
39
41
  const required = listValue(config.required_capabilities ?? '')
40
42
  for (const capability of required) {
41
43
  if (![...TASKS, 'coverage'].includes(capability))
@@ -114,7 +116,9 @@ export function describeTask(root, task, profile) {
114
116
  rust ||
115
117
  (['format', 'lint'].includes(task) && python) ||
116
118
  (task === 'type_check' && existsSync(resolve(root, 'tsconfig.json'))) ||
117
- (js && task === 'format' && hasNativeToolSetup(root, pkg, 'oxfmt')) ||
119
+ (js &&
120
+ task === 'format' &&
121
+ (Boolean(policy.config.formatter) || hasNativeToolSetup(root, pkg, 'oxfmt'))) ||
118
122
  (js && task === 'lint' && hasNativeToolSetup(root, pkg, 'oxlint'))
119
123
  if (!native) {
120
124
  applicable = false
@@ -28,6 +28,7 @@ import {
28
28
  freshCloneBuildHint,
29
29
  } from './lib/pre-commit.mjs'
30
30
  import { runNodePackagePerformance } from './lib/node-package-performance.mjs'
31
+ import { formatterChoice, formatterCommand } from './lib/formatter.mjs'
31
32
  import {
32
33
  EVAL_BUDGET_FILE_DEFAULT,
33
34
  EVAL_REPORT_FILE,
@@ -38,6 +39,7 @@ import {
38
39
 
39
40
  const root = process.cwd()
40
41
  const config = readConfig(resolve(root, '.github/code-foundry.yml'))
42
+ formatterChoice(config.formatter)
41
43
  const repoProfile = resolveProfile(root)
42
44
  /** @type {string[]} */
43
45
  const languages = configured(config.languages, repoProfile.languages).split(',').filter(Boolean)
@@ -737,7 +739,8 @@ function ci(task) {
737
739
  return
738
740
  }
739
741
  if (task === 'format') {
740
- const scripted = runScript(['format:check', 'format', 'fmt'])
742
+ if (config.formatter && hasRootJavascriptProject()) runSelectedFormatter()
743
+ const scripted = config.formatter || runScript(['format:check', 'format', 'fmt'])
741
744
  if (
742
745
  !scripted &&
743
746
  (hasLanguage('typescript') || hasLanguage('javascript')) &&
@@ -774,7 +777,8 @@ function ci(task) {
774
777
  }
775
778
  if (task === 'type_check') {
776
779
  const scripted = runScript(['type-check', 'typecheck', 'type:check'])
777
- if (!scripted && existsSync(resolve(root, 'tsconfig.json'))) runTool('tsc', ['--noEmit'])
780
+ if (!scripted && existsSync(resolve(root, 'tsconfig.json')))
781
+ run('bun', ['check', '--no-script', '-p', 'tsconfig.json'])
778
782
  if (hasLanguage('rust') && hasRootRustProject()) run('cargo', ['check', '--all-targets'])
779
783
  return
780
784
  }
@@ -1189,6 +1193,13 @@ function runToolOnFiles(tool, args, files) {
1189
1193
  for (const chunk of chunkFiles(files)) runTool(tool, [...args, ...chunk])
1190
1194
  }
1191
1195
 
1196
+ /** @param {{staged?: boolean, files?: string[]}} [options] */
1197
+ function runSelectedFormatter(options = {}) {
1198
+ const [tool, ...args] = formatterCommand(config.formatter, options)
1199
+ if (tool === 'bun') run(tool, args)
1200
+ else runTool(tool, args)
1201
+ }
1202
+
1192
1203
  /**
1193
1204
  * The local commit gate.
1194
1205
  *
@@ -1221,7 +1232,15 @@ export function preCommit() {
1221
1232
  const js = (hasLanguage('typescript') || hasLanguage('javascript')) && hasRootJavascriptProject()
1222
1233
 
1223
1234
  if (js && (plan.formatAll || plan.formatFiles.length)) {
1224
- if (plan.formatAll) {
1235
+ if (config.formatter) {
1236
+ runSelectedFormatter(
1237
+ plan.formatAll
1238
+ ? {}
1239
+ : config.formatter === 'bun'
1240
+ ? { staged: true }
1241
+ : { files: plan.formatFiles }
1242
+ )
1243
+ } else if (plan.formatAll) {
1225
1244
  if (!runScript(['format:check', 'format', 'fmt']) && hasOxfmtSetup())
1226
1245
  runTool('oxfmt', ['--check', '.'])
1227
1246
  } else if (hasOxfmtSetup()) {
@@ -1265,7 +1284,7 @@ export function preCommit() {
1265
1284
  onFailure: buildHint,
1266
1285
  })
1267
1286
  if (!scripted && existsSync(resolve(root, 'tsconfig.json')))
1268
- runTool('tsc', ['--noEmit'], { onFailure: buildHint })
1287
+ run('bun', ['check', '--no-script', '-p', 'tsconfig.json'], { onFailure: buildHint })
1269
1288
  }
1270
1289
  if (plan.rust) {
1271
1290
  run('cargo', ['fmt', '--check'])