code-foundry 1.47.5 → 1.48.1

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.
@@ -119,6 +119,9 @@ jobs:
119
119
  if-no-files-found: error
120
120
  retention-days: 14
121
121
 
122
+ - name: Save Vite+ task cache
123
+ uses: ./.github/actions/vite-plus-cache-save
124
+
122
125
  lint:
123
126
  name: Lint
124
127
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -180,6 +183,9 @@ jobs:
180
183
  if-no-files-found: error
181
184
  retention-days: 14
182
185
 
186
+ - name: Save Vite+ task cache
187
+ uses: ./.github/actions/vite-plus-cache-save
188
+
183
189
  type-check:
184
190
  name: Type-Check
185
191
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -246,6 +252,9 @@ jobs:
246
252
  if-no-files-found: error
247
253
  retention-days: 14
248
254
 
255
+ - name: Save Vite+ task cache
256
+ uses: ./.github/actions/vite-plus-cache-save
257
+
249
258
  build:
250
259
  name: Build
251
260
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -311,3 +320,6 @@ jobs:
311
320
  include-hidden-files: true
312
321
  if-no-files-found: error
313
322
  retention-days: 14
323
+
324
+ - name: Save Vite+ task cache
325
+ uses: ./.github/actions/vite-plus-cache-save
@@ -243,7 +243,14 @@ jobs:
243
243
  isAbsolute(workspaceRelative) ||
244
244
  workspaceRelative === '..' ||
245
245
  workspaceRelative.startsWith(`..${sep}`)
246
- if (!existsSync('turbo.json') && !existsSync('turbo.jsonc')) {
246
+ // Vite+ repositories are not served by this Turbo-only check.
247
+ const config = resolve(workspace, '.github/code-foundry.yml')
248
+ const optedIntoVitePlus =
249
+ existsSync(config) &&
250
+ /^monorepo_tool:\s*["']?vite-plus["']?\s*(?:#.*)?$/m.test(readFileSync(config, 'utf8'))
251
+ if (optedIntoVitePlus) {
252
+ conservative('monorepo_tool is vite-plus')
253
+ } else if (!existsSync('turbo.json') && !existsSync('turbo.jsonc')) {
247
254
  conservative('Turbo config not found')
248
255
  } else if (outsideWorkspace) {
249
256
  conservative('working-directory is outside the workspace')
@@ -171,7 +171,14 @@ jobs:
171
171
  isAbsolute(workspaceRelative) ||
172
172
  workspaceRelative === '..' ||
173
173
  workspaceRelative.startsWith(`..${sep}`)
174
- if (!existsSync('turbo.json') && !existsSync('turbo.jsonc')) {
174
+ // Vite+ repositories are not served by this Turbo-only check.
175
+ const config = resolve(workspace, '.github/code-foundry.yml')
176
+ const optedIntoVitePlus =
177
+ existsSync(config) &&
178
+ /^monorepo_tool:\s*["']?vite-plus["']?\s*(?:#.*)?$/m.test(readFileSync(config, 'utf8'))
179
+ if (optedIntoVitePlus) {
180
+ conservative('monorepo_tool is vite-plus')
181
+ } else if (!existsSync('turbo.json') && !existsSync('turbo.jsonc')) {
175
182
  conservative('Turbo config not found')
176
183
  } else if (outsideWorkspace) {
177
184
  conservative('working-directory is outside the workspace')
@@ -124,3 +124,6 @@ jobs:
124
124
  eval-results/summary.json
125
125
  if-no-files-found: error
126
126
  retention-days: 14
127
+
128
+ - name: Save Vite+ task cache
129
+ uses: ./.github/actions/vite-plus-cache-save
@@ -161,6 +161,9 @@ jobs:
161
161
  if-no-files-found: ignore
162
162
  retention-days: 14
163
163
 
164
+ - name: Save Vite+ task cache
165
+ uses: ./.github/actions/vite-plus-cache-save
166
+
164
167
  performance:
165
168
  name: Performance
166
169
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -247,6 +250,9 @@ jobs:
247
250
  if-no-files-found: ignore
248
251
  retention-days: 14
249
252
 
253
+ - name: Save Vite+ task cache
254
+ uses: ./.github/actions/vite-plus-cache-save
255
+
250
256
  integration:
251
257
  name: Integration
252
258
  if: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true
@@ -317,6 +323,9 @@ jobs:
317
323
  if-no-files-found: error
318
324
  retention-days: 14
319
325
 
326
+ - name: Save Vite+ task cache
327
+ uses: ./.github/actions/vite-plus-cache-save
328
+
320
329
  e2e:
321
330
  name: E2E
322
331
  if: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true
@@ -410,6 +419,9 @@ jobs:
410
419
  if-no-files-found: error
411
420
  retention-days: 14
412
421
 
422
+ - name: Save Vite+ task cache
423
+ uses: ./.github/actions/vite-plus-cache-save
424
+
413
425
  smoke:
414
426
  name: Smoke
415
427
  if: vars.CI_BILLING_PAUSED != 'true' && inputs.unit-only != true
@@ -479,3 +491,6 @@ jobs:
479
491
  include-hidden-files: true
480
492
  if-no-files-found: error
481
493
  retention-days: 14
494
+
495
+ - name: Save Vite+ task cache
496
+ uses: ./.github/actions/vite-plus-cache-save
@@ -469,6 +469,9 @@ jobs:
469
469
  if-no-files-found: error
470
470
  retention-days: 14
471
471
 
472
+ - name: Save Vite+ task cache
473
+ uses: ./.github/actions/vite-plus-cache-save
474
+
472
475
  lint:
473
476
  name: Lint
474
477
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -530,6 +533,9 @@ jobs:
530
533
  if-no-files-found: error
531
534
  retention-days: 14
532
535
 
536
+ - name: Save Vite+ task cache
537
+ uses: ./.github/actions/vite-plus-cache-save
538
+
533
539
  type-check:
534
540
  name: Type-Check
535
541
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -596,6 +602,9 @@ jobs:
596
602
  if-no-files-found: error
597
603
  retention-days: 14
598
604
 
605
+ - name: Save Vite+ task cache
606
+ uses: ./.github/actions/vite-plus-cache-save
607
+
599
608
  unit:
600
609
  name: Unit
601
610
  if: vars.CI_BILLING_PAUSED != 'true'
@@ -682,6 +691,9 @@ jobs:
682
691
  if-no-files-found: ignore
683
692
  retention-days: 14
684
693
 
694
+ - name: Save Vite+ task cache
695
+ uses: ./.github/actions/vite-plus-cache-save
696
+
685
697
  e2e:
686
698
  name: E2E
687
699
  if: vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit'
@@ -775,11 +787,15 @@ jobs:
775
787
  if-no-files-found: error
776
788
  retention-days: 14
777
789
 
790
+ - name: Save Vite+ task cache
791
+ uses: ./.github/actions/vite-plus-cache-save
792
+
778
793
  # Profile and dependency audit merged into one job. The split orchestrators
779
794
  # run a profile job that feeds an audit matrix; here the language blocks run
780
795
  # sequentially, driven directly by the profile step's outputs, so the second
781
796
  # runner spin-up disappears. Python requirements run in one loop inside the
782
797
  # audit step because a matrix cannot follow preceding steps in the same job.
798
+
783
799
  audit:
784
800
  name: Audit
785
801
  if: vars.CI_BILLING_PAUSED != 'true' && inputs.mode == 'audit'
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.48.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.48.0...v1.48.1) (2026-10-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * vite-plus-cache-post-job-save ([#765](https://github.com/0xPlayerOne/code-foundry/issues/765)) ([3f82e85](https://github.com/0xPlayerOne/code-foundry/commit/3f82e850f5c09af6cea4de6f1a791d098bbd2f6e))
9
+
10
+ ## [1.48.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.47.5...v1.48.0) (2026-10-16)
11
+
12
+
13
+ ### Features
14
+
15
+ * add opt-in Vite+ task runner support ([#763](https://github.com/0xPlayerOne/code-foundry/issues/763)) ([f7970cd](https://github.com/0xPlayerOne/code-foundry/commit/f7970cdf1c1880aaee159d2de79ba043f93711a7))
16
+
3
17
  ## [1.47.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.47.4...v1.47.5) (2026-10-15)
4
18
 
5
19
 
@@ -126,23 +140,9 @@
126
140
 
127
141
  * **release:** treat ref-creation errors in the REST fallback as non-fatal ([#727](https://github.com/0xPlayerOne/code-foundry/issues/727)) ([75ea614](https://github.com/0xPlayerOne/code-foundry/commit/75ea61465e7c4cacd39e3aa8c52df93c2ea9bf03))
128
142
 
129
- ## [1.44.9](https://github.com/0xPlayerOne/code-foundry/compare/v1.44.8...v1.44.9) (2026-10-06)
130
-
131
-
132
- ### Bug Fixes
133
-
134
- * **release:** make the REST fallback draft-aware and idempotent ([#725](https://github.com/0xPlayerOne/code-foundry/issues/725)) ([c29da6e](https://github.com/0xPlayerOne/code-foundry/commit/c29da6efb919d68a568c2328eea2c2f7654334fe))
135
-
136
- ## [1.44.8](https://github.com/0xPlayerOne/code-foundry/compare/v1.44.7...v1.44.8) (2026-10-06)
137
-
138
-
139
- ### Bug Fixes
140
-
141
- * **release:** query the version-PR branch directly in the REST fallback ([#722](https://github.com/0xPlayerOne/code-foundry/issues/722)) ([b38f394](https://github.com/0xPlayerOne/code-foundry/commit/b38f394c0ca6438117fdd2f45bd106b0b145a0f9))
142
-
143
143
  <!-- code-foundry: packaged changelog -->
144
144
 
145
145
  ---
146
146
 
147
- This packaged changelog lists the 20 most recent releases; 247 older releases are omitted to keep the package small.
147
+ This packaged changelog lists the 20 most recent releases; 249 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.
package/docs/CACHING.md CHANGED
@@ -22,6 +22,90 @@ Non-Turborepo repositories do not need either setting. Deployment to Vercel is
22
22
  not required. Custom workflows can pass these values to Turbo directly when
23
23
  the repository's task graph benefits from remote reuse.
24
24
 
25
+ ## Vite+ local task cache (opt-in)
26
+
27
+ Repositories that run their workspace tasks with Vite+ (`vp run`, `vpr`) can
28
+ opt in with `monorepo_tool: vite-plus` in `.github/code-foundry.yml`. Turbo
29
+ stays the default for every repository that does not set it.
30
+
31
+ When opted in, the setup action:
32
+
33
+ - stops treating a `turbo.json` as the active runner, so no Turborepo cache or
34
+ remote-cache step runs;
35
+ - restores `node_modules/.vite/task-cache` as its last step, after Install and
36
+ after every step that archives `node_modules` (Install can replace it);
37
+ - publishes a cache path, key, and save-intent contract for the explicit
38
+ `.github/actions/vite-plus-cache-save` action to use after repository tasks;
39
+ - saves it push-only, so pull requests and scheduled runs cannot poison it;
40
+ - keeps it out of the generic JavaScript build-state archive.
41
+
42
+ The generated CI, test, eval, and billing workflows call the save action at the
43
+ end of each task job. Custom workflows that use `.github/actions/setup` must
44
+ also call it after their Vite+ tasks complete; it reads the setup contract from
45
+ the job environment. A job can disable saving with `cache-save: false` on every
46
+ applicable setup call. With multiple setup calls in one job, save intent is
47
+ combined so an enabled Vite+ task setup is not overridden by a later disabled
48
+ setup. The setup action also exposes `vite-plus-cache-path`,
49
+ `vite-plus-cache-key`, and `vite-plus-cache-save` outputs for workflows that
50
+ prefer to pass these values explicitly to the save action.
51
+
52
+ Keys are `<namespace>-<workflow>-<job>-<lockfiles>-<vite.config>-<run id>-<attempt>`,
53
+ so every push saves a refreshed entry (more storage than a fixed key). Restore
54
+ keys fall back by lockfiles, job, then any job, but never leave the namespace
55
+ `vite-plus-v1-<OS>-<arch>-node<version>-bun<version>-vp<version>`, because Vite
56
+ Task does not fingerprint the Node or Bun binary. If the `vite-plus` version
57
+ cannot be resolved, nothing is restored or saved.
58
+
59
+ If `VITE_CACHE_PATH` (absolute, from any environment source) is set, Vite+
60
+ writes its cache there and the setup action caches that directory too.
61
+
62
+ Vite+ has no remote cache and this repository does not configure one.
63
+
64
+ Known limits (not parity with Turbo):
65
+
66
+ - Vite Task keeps one cache entry per task, so alternating between states
67
+ (A, B, A) re-runs the task instead of hitting the cache.
68
+ - `package.json` scripts are not cached unless the consumer sets
69
+ `run.cache.scripts: true` in its root `vite.config.ts` or passes `--cache` to
70
+ `vp run`. Code Foundry does not change consumer config. Commands that read no
71
+ files, such as a bare `echo`, are never cached.
72
+ - Tasks reading files that only Bun loads may need explicit input globs in the
73
+ consumer's Vite+ config (untested here).
74
+ - `cloudflare-deploy.yml` and `cloudflare-delivery.yml` keep a Turbo-only
75
+ affected check. With `monorepo_tool: vite-plus` they skip it and deploy
76
+ conservatively, even with a leftover `turbo.json`.
77
+
78
+ ### Affected packages
79
+
80
+ `npx code-foundry affected --base <ref> --head <ref>` prints the affected
81
+ packages as JSON (`tool`, `packages`, `removedPackages`, `all`, `conservative`,
82
+ `reason`, `filterArgs`). Both refs must be present in the clone (use a
83
+ non-shallow checkout or fetch the base commit).
84
+
85
+ - Turbo repositories use `turbo query affected --packages`; this module does
86
+ not parse their workspace.
87
+ - Vite+ repositories compute the set from `git diff base...head` and the
88
+ workspace graphs read from the committed trees at both refs, because Vite+
89
+ has no equivalent query. Each changed file marks the package that owns it;
90
+ dependents are added transitively over the union of both graphs' edges, so
91
+ deleted, renamed, moved, and re-wired packages still select their dependents.
92
+ `removedPackages` lists selected packages that no longer exist at the head.
93
+ - Any changed file that no workspace package owns selects every package
94
+ (`all: true`): lockfiles, root manifest, config, scripts, docs, templates. A
95
+ build may read any of them. Only `affected_ignore` in `.github/code-foundry.yml`
96
+ (comma-separated globs, for example `README.md, docs/**`) skips unowned files.
97
+ - Anything the module cannot evaluate faithfully makes the result
98
+ conservative (`all: true`, `conservative: true`): no runner, a git or Turbo
99
+ failure, a malformed or unnamed workspace manifest at either ref, duplicate
100
+ package names, or workspace syntax it does not support (brace, bracket, or
101
+ extglob patterns). Supported syntax is `package.json` `workspaces` (array or
102
+ `{ packages }`) and pnpm `packages:` as a block or flow list, with `*`, `**`,
103
+ `?`, and `!` exclusions.
104
+ - `--tool turbo|vite-plus` overrides `monorepo_tool` for one invocation.
105
+ - For Vite+, `filterArgs` is ready to append to `vp run`. When `all` is true
106
+ run with `-r` instead, and when `packages` is empty skip the run: an empty
107
+ filter list would otherwise select every package.
108
+
25
109
  ## Runner selection
26
110
 
27
111
  Lightweight detection, orchestration, audits, and release metadata jobs can use
@@ -289,6 +289,19 @@ behavior:
289
289
  `TURBO_TEAM`.
290
290
  - `TURBO_TOKEN` and `TURBO_TEAM` provide the Turborepo remote-cache
291
291
  credentials.
292
+ - `monorepo_tool: auto`, `turbo`, or `vite-plus` selects the workspace task
293
+ runner. `auto` (the default) is Turbo when a `turbo.json` or `turbo.jsonc`
294
+ exists and no runner otherwise, exactly as before Vite+ support. Vite+ is
295
+ opt-in: only `monorepo_tool: vite-plus` selects it, and it then takes
296
+ precedence over a turbo config. `doctor` warns when the selected runner has
297
+ no matching evidence in the repository.
298
+ - `cache-vite-plus: auto`, `true`, or `false` is an input of the setup action
299
+ (not a config key). It controls the local Vite+ task cache only when
300
+ `monorepo_tool: vite-plus` is set.
301
+ - Custom workflows using the setup action must add
302
+ `.github/actions/vite-plus-cache-save` after task execution. The generated
303
+ Code Foundry workflows already include this save boundary; see
304
+ [Caching and remote caching](CACHING.md).
292
305
 
293
306
  Use these controls only after measuring a repeatable benefit. See [Caching and
294
307
  remote caching](CACHING.md).
package/docs/README.md CHANGED
@@ -33,7 +33,7 @@ details before copying any guide elsewhere.
33
33
  - [Declarative fleet rollouts](fleet-rollouts.md) — inventory, canaries, staged upgrades, and recovery.
34
34
  - [Fleet release eligibility](fleet-release-eligibility.md) — require verified release evidence before upgrades.
35
35
  - [Verified Cloudflare delivery](cloudflare-delivery.md) — build, verify, canary, and promote Worker versions.
36
- - [Caching and remote caching](CACHING.md) — configure package, build, and Turborepo caching.
36
+ - [Caching and remote caching](CACHING.md) — configure package, build, Turborepo, and opt-in Vite+ caching.
37
37
 
38
38
  ## Repository-specific documentation
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.47.5",
3
+ "version": "1.48.1",
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": {
@@ -18,6 +18,7 @@
18
18
  "src",
19
19
  "docs",
20
20
  ".github",
21
+ "!.github/actions/**",
21
22
  ".githooks",
22
23
  ".editorconfig",
23
24
  ".gitattributes",
package/src/cli.mjs CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'
7
7
 
8
8
  const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
9
9
 
10
- /** @typedef {{ target: string, root: string, source: string, dryRun: boolean, force: boolean, github: boolean, createPr: boolean, exclude: string[], base: string, head: string, tag: string, workflow: string, mode: string, version: string, versionExplicit: boolean, repository: string, expectedSha: string, assets: string[], rootProvided: boolean, releaseSubcommand?: string, fleetSubcommand?: string, integritySubcommand?: 'settings'|'release'|'manifest', ciSubcommand?: 'pause'|'resume'|'status' }} Options */
10
+ /** @typedef {{ target: string, root: string, source: string, dryRun: boolean, force: boolean, github: boolean, createPr: boolean, exclude: string[], base: string, head: string, tag: string, workflow: string, mode: string, version: string, versionExplicit: boolean, repository: string, expectedSha: string, assets: string[], rootProvided: boolean, tool: string, headProvided: boolean, releaseSubcommand?: string, fleetSubcommand?: string, integritySubcommand?: 'settings'|'release'|'manifest', ciSubcommand?: 'pause'|'resume'|'status' }} Options */
11
11
  /** @typedef {{ command: string, options: Options }} ParsedArgs */
12
12
 
13
13
  const usage = `code-foundry — initialize and maintain agent-ready repositories
@@ -18,6 +18,7 @@ Usage:
18
18
  npx code-foundry doctor [--target PATH]
19
19
  npx code-foundry plan [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--json]
20
20
  npx code-foundry check [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--timeout SECONDS] [--json]
21
+ npx code-foundry affected [--target PATH] [--base REF] [--head REF] [--tool auto|turbo|vite-plus]
21
22
  npx code-foundry ci pause|resume|status [--target PATH]
22
23
  npx code-foundry release reconcile [--github] [--base BRANCH] [--head BRANCH]
23
24
  npx code-foundry release hook --tag TAG --workflow WORKFLOW
@@ -37,7 +38,8 @@ Options:
37
38
  --force Replace protected standard documents
38
39
  --github Apply a verified fast-forward through GitHub
39
40
  --base BRANCH Release source branch (default: main)
40
- --head BRANCH Branch to reconcile (default: staging)
41
+ --head BRANCH Branch to reconcile (default: staging; HEAD for affected)
42
+ --tool TOOL Task runner for affected: auto, turbo, or vite-plus (default: auto)
41
43
  --tag TAG Published release tag for a post-release hook
42
44
  --workflow FILE Workflow to dispatch for a post-release hook
43
45
  --mode MODE auto, workflow-dispatch, release-event, or disabled
@@ -78,6 +80,8 @@ function parseArgs(argv) {
78
80
  tag: '',
79
81
  workflow: '',
80
82
  mode: 'auto',
83
+ tool: 'auto',
84
+ headProvided: false,
81
85
  version: `v${readPackageVersion(packageRoot)}`,
82
86
  versionExplicit: false,
83
87
  repository: '',
@@ -128,8 +132,15 @@ function parseArgs(argv) {
128
132
  else if (arg === '--force') options.force = true
129
133
  else if (arg === '--github') options.github = true
130
134
  else if (arg === '--base') options.base = argv.shift() ?? fail('--base requires a branch')
131
- else if (arg === '--head') options.head = argv.shift() ?? fail('--head requires a branch')
132
- else if (arg === '--tag') options.tag = argv.shift() ?? fail('--tag requires a tag')
135
+ else if (arg === '--head') {
136
+ options.head = argv.shift() ?? fail('--head requires a branch')
137
+ options.headProvided = true
138
+ } else if (arg === '--tool') {
139
+ if (command !== 'affected') fail('--tool is only supported by the affected command')
140
+ options.tool = argv.shift() ?? fail('--tool requires auto, turbo, or vite-plus')
141
+ if (!['auto', 'turbo', 'vite-plus'].includes(options.tool))
142
+ fail(`unsupported --tool: ${options.tool}; use auto, turbo, or vite-plus`)
143
+ } else if (arg === '--tag') options.tag = argv.shift() ?? fail('--tag requires a tag')
133
144
  else if (arg === '--repo') {
134
145
  if (command !== 'release-integrity')
135
146
  fail('--repo is only supported by release-integrity commands')
@@ -204,6 +215,18 @@ async function main() {
204
215
  console.error(`code-foundry: ${error instanceof Error ? error.message : String(error)}`)
205
216
  process.exitCode = 1
206
217
  }
218
+ } else if (command === 'affected') {
219
+ try {
220
+ const { affectedPackages } = await import('./lib/task-filters.mjs')
221
+ const result = affectedPackages(target, {
222
+ base: options.base,
223
+ head: options.headProvided ? options.head : 'HEAD',
224
+ tool: options.tool,
225
+ })
226
+ console.log(JSON.stringify(result, null, 2))
227
+ } catch (error) {
228
+ fail(error instanceof Error ? error.message : String(error))
229
+ }
207
230
  } else if (command === 'release') {
208
231
  try {
209
232
  const {
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
4
4
  import { join, resolve, isAbsolute } from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
6
  import { includesValue, readConfig } from '../lib/config.mjs'
7
- import { recommendRunners, resolveProfile } from '../lib/profile.mjs'
7
+ import { recommendRunners, resolveMonorepoTool, resolveProfile } from '../lib/profile.mjs'
8
8
  import { packageManagerForLockfile } from '../lib/lockfiles.mjs'
9
9
  import { doctorGithub } from '../lib/github-doctor.mjs'
10
10
  import { isGeneratedEventCaller } from './sync.mjs'
@@ -38,6 +38,10 @@ export function doctor(root, options = {}) {
38
38
  : 'Toolchain: native (mise not configured).'
39
39
  )
40
40
  }
41
+ const monorepoTool = resolveMonorepoTool(target, config)
42
+ for (const message of monorepoTool.warnings) warn(message)
43
+ if (monorepoTool.tool === 'vite-plus')
44
+ console.log(`Task runner: vite-plus (${monorepoTool.reason}).`)
41
45
  const hooks = git(target, ['config', '--get', 'core.hooksPath'])
42
46
  if (hooks !== '.githooks') warn('Git hooks are not enabled; run `npx code-foundry init`')
43
47
  // A correctly enabled hook that enforces nothing is the failure this check
@@ -2168,6 +2168,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
2168
2168
  coverage_metrics: 'lines',
2169
2169
  coverage_report: '',
2170
2170
  turbo_remote: 'auto',
2171
+ monorepo_tool: 'auto',
2171
2172
  release_type: detectPackageManager(root) === 'none' ? 'auto' : 'node',
2172
2173
  npm_publish: 'false',
2173
2174
  post_release: 'false',
@@ -160,3 +160,123 @@ function detectReleaseType(root) {
160
160
  }
161
161
 
162
162
  export { includesValue }
163
+
164
+ /**
165
+ * Workspace task runner. `auto` (default) is Turbo when a turbo config exists,
166
+ * else none; Vite+ needs an explicit `monorepo_tool: vite-plus`.
167
+ */
168
+ export const MONOREPO_TOOLS = ['auto', 'turbo', 'vite-plus']
169
+
170
+ /** @typedef {'turbo'|'vite-plus'|'none'} ResolvedTool */
171
+ /** @typedef {{ configured: string, tool: ResolvedTool, reason: string, warnings: string[] }} ToolResolution */
172
+
173
+ /** @param {string} root @returns {Record<string, any> | null} */
174
+ function readPackageJson(root) {
175
+ try {
176
+ return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
177
+ } catch {
178
+ return null
179
+ }
180
+ }
181
+
182
+ /** @param {string} root */
183
+ export function hasTurboConfig(root) {
184
+ return existsSync(join(root, 'turbo.json')) || existsSync(join(root, 'turbo.jsonc'))
185
+ }
186
+
187
+ /**
188
+ * Whether a shell command invokes the Vite+ task runner. `vp run ...` and the
189
+ * `vpr` shorthand populate the task cache; plain `vp build` or `vp test` do
190
+ * not, so they are intentionally not matched.
191
+ * @param {string} command
192
+ */
193
+ export function commandUsesVitePlus(command) {
194
+ return /(?:^|[\s;&|(])(?:vp\s+(?:-C\s+\S+\s+)?run|vpr)(?:\s|$)/.test(command)
195
+ }
196
+
197
+ /** @param {string} command */
198
+ export function commandUsesTurbo(command) {
199
+ return /\bturbo(?:\s|$)/.test(command)
200
+ }
201
+
202
+ /**
203
+ * Evidence that a repository declares Vite+: the `vite-plus` package in any
204
+ * dependency table or a script that runs `vp run` / `vpr`.
205
+ * @param {string} root
206
+ * @returns {{ declared: boolean, via: string[] }}
207
+ */
208
+ export function detectVitePlus(root) {
209
+ const packageJson = readPackageJson(root)
210
+ /** @type {string[]} */
211
+ const via = []
212
+ if (!packageJson) return { declared: false, via }
213
+ for (const table of ['dependencies', 'devDependencies', 'optionalDependencies']) {
214
+ if (
215
+ packageJson[table] &&
216
+ typeof packageJson[table] === 'object' &&
217
+ packageJson[table]['vite-plus']
218
+ )
219
+ via.push(`${table}.vite-plus`)
220
+ }
221
+ const scripts =
222
+ packageJson.scripts && typeof packageJson.scripts === 'object' ? packageJson.scripts : {}
223
+ for (const [name, command] of Object.entries(scripts)) {
224
+ if (typeof command === 'string' && commandUsesVitePlus(command)) via.push(`scripts.${name}`)
225
+ }
226
+ return { declared: via.length > 0, via }
227
+ }
228
+
229
+ /**
230
+ * Resolve which task runner a repository uses. Never throws: invalid or
231
+ * unsatisfied configuration falls back to the legacy behavior and is reported
232
+ * in `warnings` so doctor can surface it.
233
+ * @param {string} root
234
+ * @param {Record<string, string>} [config] Defaults to .github/code-foundry.yml.
235
+ * @returns {ToolResolution}
236
+ */
237
+ export function resolveMonorepoTool(
238
+ root,
239
+ config = readConfig(join(root, '.github/code-foundry.yml'))
240
+ ) {
241
+ /** @type {string[]} */
242
+ const warnings = []
243
+ let selected = (config.monorepo_tool ?? '').trim() || 'auto'
244
+ if (!MONOREPO_TOOLS.includes(selected)) {
245
+ warnings.push(
246
+ `unsupported monorepo_tool: ${selected}; use auto, turbo, or vite-plus. Falling back to auto.`
247
+ )
248
+ selected = 'auto'
249
+ }
250
+ const turbo = hasTurboConfig(root)
251
+
252
+ if (selected === 'vite-plus') {
253
+ const evidence = detectVitePlus(root)
254
+ if (!evidence.declared)
255
+ warnings.push(
256
+ 'monorepo_tool: vite-plus but neither a vite-plus dependency nor a `vp run` script was found in package.json.'
257
+ )
258
+ return {
259
+ configured: selected,
260
+ tool: 'vite-plus',
261
+ reason: turbo
262
+ ? 'monorepo_tool: vite-plus (turbo config present but ignored)'
263
+ : 'monorepo_tool: vite-plus',
264
+ warnings,
265
+ }
266
+ }
267
+ if (selected === 'turbo') {
268
+ if (!turbo) warnings.push('monorepo_tool: turbo but no turbo.json or turbo.jsonc was found.')
269
+ return {
270
+ configured: selected,
271
+ tool: turbo ? 'turbo' : 'none',
272
+ reason: 'monorepo_tool: turbo',
273
+ warnings,
274
+ }
275
+ }
276
+ return {
277
+ configured: selected,
278
+ tool: turbo ? 'turbo' : 'none',
279
+ reason: turbo ? 'turbo config detected' : 'no task runner configured',
280
+ warnings,
281
+ }
282
+ }