code-foundry 1.2.1 → 1.3.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.
@@ -3,21 +3,48 @@
3
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
4
  import { dirname, join, resolve } from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
- import { detectLanguages, detectPackageManager, detectProfile, recommendRunners } from '../lib/profile.mjs'
7
- import { configured, gitWorkflow, includesValue, isStagingRelease, readConfig } from '../lib/config.mjs'
6
+ import {
7
+ detectLanguages,
8
+ detectPackageManager,
9
+ detectProfile,
10
+ recommendRunners,
11
+ } from '../lib/profile.mjs'
12
+ import {
13
+ configured,
14
+ gitWorkflow,
15
+ includesValue,
16
+ isStagingRelease,
17
+ readConfig,
18
+ } from '../lib/config.mjs'
8
19
  import { buildReleaseConfig, buildReleaseManifest } from '../lib/release-manifest.mjs'
9
20
  import { customWorkflowFiles, overlayPolicy } from '../lib/overlay.mjs'
10
21
 
11
22
  const standardFiles = [
12
- '.editorconfig', '.gitattributes', '.gitignore', 'release-please-config.json',
23
+ '.editorconfig',
24
+ '.gitattributes',
25
+ '.gitignore',
26
+ 'release-please-config.json',
13
27
  'docs/EXTENSIONS.md',
14
- '.githooks/pre-commit', 'AGENTS.md', 'LICENSE', 'NOTICE', 'ruff.toml', '.prettierrc', '.prettierignore',
15
- '.github/CODEOWNERS', '.github/CODE_OF_CONDUCT.md', '.github/CONTRIBUTING.md',
16
- '.github/PULL_REQUEST_TEMPLATE.md', '.github/SECURITY.md', '.github/dependabot.yml',
17
- '.github/ISSUE_TEMPLATE/bug_report.yml', '.github/ISSUE_TEMPLATE/config.yml',
28
+ '.githooks/pre-commit',
29
+ 'AGENTS.md',
30
+ 'LICENSE',
31
+ 'NOTICE',
32
+ 'ruff.toml',
33
+ '.oxfmtrc.json',
34
+ '.oxlintrc.json',
35
+ '.github/CODEOWNERS',
36
+ '.github/CODE_OF_CONDUCT.md',
37
+ '.github/CONTRIBUTING.md',
38
+ '.github/PULL_REQUEST_TEMPLATE.md',
39
+ '.github/SECURITY.md',
40
+ '.github/dependabot.yml',
41
+ '.github/ISSUE_TEMPLATE/bug_report.yml',
42
+ '.github/ISSUE_TEMPLATE/config.yml',
18
43
  '.github/ISSUE_TEMPLATE/feature_request.yml',
19
- '.github/workflows/validation.yml', '.github/workflows/draft-pr.yml',
20
- '.github/workflows/release-pr.yml', '.github/workflows/release.yml',
44
+ '.github/workflows/validation.yml',
45
+ '.github/workflows/draft-pr.yml',
46
+ '.github/workflows/release-pr.yml',
47
+ '.github/workflows/release.yml',
21
48
  '.github/workflows/opencode-security.yml',
22
49
  ]
23
50
 
@@ -29,8 +56,12 @@ const standardFiles = [
29
56
  const LEGACY_GENERATED_CALLERS = ['ci', 'test', 'security', 'codeql']
30
57
 
31
58
  const protectedFiles = new Set([
32
- 'AGENTS.md', '.github/CODE_OF_CONDUCT.md', '.github/CONTRIBUTING.md',
33
- '.github/PULL_REQUEST_TEMPLATE.md', '.github/SECURITY.md', 'NOTICE',
59
+ 'AGENTS.md',
60
+ '.github/CODE_OF_CONDUCT.md',
61
+ '.github/CONTRIBUTING.md',
62
+ '.github/PULL_REQUEST_TEMPLATE.md',
63
+ '.github/SECURITY.md',
64
+ 'NOTICE',
34
65
  ])
35
66
 
36
67
  /** @type {Record<string, string>} */
@@ -38,18 +69,31 @@ const licenseFiles = {
38
69
  'gpl-3.0-or-later': 'GPL-3.0-or-later.txt',
39
70
  'agpl-3.0-or-later': 'AGPL-3.0-or-later.txt',
40
71
  'apache-2.0': 'APACHE-2.0.txt',
41
- 'mit': 'MIT.txt',
72
+ mit: 'MIT.txt',
42
73
  }
43
74
 
44
75
  const legacyFiles = [
45
- '.github/code-foundry.yml.example', '.github/template.yml', '.github/template.yml.example',
46
- '.github/scripts/bootstrap.sh', '.github/scripts/changed-files.sh', '.github/scripts/ci.sh',
47
- '.github/scripts/codeql-languages.sh', '.github/scripts/doctor.sh', '.github/scripts/format-fast-path.sh',
48
- '.github/scripts/init-repo.sh', '.github/scripts/pre-commit.sh', '.github/scripts/profile.sh',
49
- '.github/scripts/security.sh', '.github/scripts/sitecustomize.py', '.github/scripts/sync-codeowners.sh',
50
- '.github/scripts/sync-protection.sh', '.github/scripts/sync-template.sh', '.github/scripts/turbo-cache-probe.sh',
76
+ '.github/code-foundry.yml.example',
77
+ '.github/template.yml',
78
+ '.github/template.yml.example',
79
+ '.github/scripts/bootstrap.sh',
80
+ '.github/scripts/changed-files.sh',
81
+ '.github/scripts/ci.sh',
82
+ '.github/scripts/codeql-languages.sh',
83
+ '.github/scripts/doctor.sh',
84
+ '.github/scripts/format-fast-path.sh',
85
+ '.github/scripts/init-repo.sh',
86
+ '.github/scripts/pre-commit.sh',
87
+ '.github/scripts/profile.sh',
88
+ '.github/scripts/security.sh',
89
+ '.github/scripts/sitecustomize.py',
90
+ '.github/scripts/sync-codeowners.sh',
91
+ '.github/scripts/sync-protection.sh',
92
+ '.github/scripts/sync-template.sh',
93
+ '.github/scripts/turbo-cache-probe.sh',
51
94
  '.github/licenses/MIT.txt',
52
- '.github/licenses/GPL-3.0-or-later.txt', '.github/licenses/AGPL-3.0-or-later.txt',
95
+ '.github/licenses/GPL-3.0-or-later.txt',
96
+ '.github/licenses/AGPL-3.0-or-later.txt',
53
97
  ]
54
98
 
55
99
  /** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean }} SyncOptions */
@@ -62,13 +106,17 @@ export function syncRepository(options) {
62
106
  const force = options.force ?? false
63
107
  const configPath = join(target, '.github/code-foundry.yml')
64
108
  const existingConfig = readConfig(configPath)
65
- if (!Object.keys(existingConfig).length && !options.init) throw new Error('Missing .github/code-foundry.yml; run init first.')
109
+ if (!Object.keys(existingConfig).length && !options.init)
110
+ throw new Error('Missing .github/code-foundry.yml; run init first.')
66
111
  const defaults = createDefaultConfig(target, source)
67
112
  let config = { ...defaults, ...existingConfig }
68
113
  // Resolve and validate the license policy before any sync writes occur
69
114
  // (including config/default additions) so an unsupported policy fails
70
115
  // fast without leaving partially generated files behind.
71
- const license = configured(config.license, existsSync(join(target, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later')
116
+ const license = configured(
117
+ config.license,
118
+ existsSync(join(target, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later'
119
+ )
72
120
  const licenseFile = licenseFiles[license]
73
121
  if (license !== 'preserve' && license !== 'none' && !licenseFile) {
74
122
  const supported = [...Object.keys(licenseFiles), 'preserve', 'none'].join(', ')
@@ -108,17 +156,25 @@ export function syncRepository(options) {
108
156
  }
109
157
  const mergeStrategy = configured(config.merge_strategy, 'rebase')
110
158
  if (workflow === 'staging-release' && mergeStrategy !== 'rebase') {
111
- throw new Error(`Unsupported merge_strategy: ${mergeStrategy}; the staging-release topology requires rebase for staging to main promotions.`)
159
+ throw new Error(
160
+ `Unsupported merge_strategy: ${mergeStrategy}; the staging-release topology requires rebase for staging to main promotions.`
161
+ )
112
162
  }
113
163
  const releaseMergeStrategy = configured(config.release_merge_strategy, '')
114
164
  if (includesValue(features, 'release') && releaseMergeStrategy !== 'rebase') {
115
- throw new Error(`Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires rebase for Release Please version pull requests and never defaults to merge.`)
165
+ throw new Error(
166
+ `Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires rebase for Release Please version pull requests and never defaults to merge.`
167
+ )
116
168
  }
117
169
  const changed = []
118
170
 
119
171
  // Keep normal semver pins current during sync while preserving intentional
120
172
  // refs such as `main`, `staging`, or a custom immutable SHA.
121
- if (existingConfig.runtime_ref && /^v\d+\.\d+\.\d+$/.test(existingConfig.runtime_ref) && existingConfig.runtime_ref !== sourceRuntimeRef) {
173
+ if (
174
+ existingConfig.runtime_ref &&
175
+ /^v\d+\.\d+\.\d+$/.test(existingConfig.runtime_ref) &&
176
+ existingConfig.runtime_ref !== sourceRuntimeRef
177
+ ) {
122
178
  runtimeRef = sourceRuntimeRef
123
179
  const current = readFileSync(configPath, 'utf8')
124
180
  const updated = current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${sourceRuntimeRef}`)
@@ -137,7 +193,12 @@ export function syncRepository(options) {
137
193
  const existing = readFileSync(destination, 'utf8')
138
194
  if (!isLegacyManagedDoc(file, existing)) continue
139
195
  }
140
- if ((file === 'LICENSE' || file === 'NOTICE') && license === 'preserve' && existsSync(destination)) continue
196
+ if (
197
+ (file === 'LICENSE' || file === 'NOTICE') &&
198
+ license === 'preserve' &&
199
+ existsSync(destination)
200
+ )
201
+ continue
141
202
  if ((file === 'LICENSE' || file === 'NOTICE') && license === 'none') continue
142
203
  // An explicit license policy makes the license block below the single
143
204
  // owner of LICENSE; copying the runtime's own root LICENSE here would
@@ -149,7 +210,9 @@ export function syncRepository(options) {
149
210
  content = Buffer.from(renderReleaseConfig(target, sourceFile))
150
211
  }
151
212
  if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
152
- content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef, rustCodeql))
213
+ content = Buffer.from(
214
+ renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef, rustCodeql)
215
+ )
153
216
  }
154
217
  if (file === '.github/dependabot.yml') {
155
218
  content = Buffer.from(renderDependabot(content.toString('utf8'), config, languages))
@@ -158,10 +221,19 @@ export function syncRepository(options) {
158
221
  content = Buffer.from(renderContributionDocs(content.toString('utf8'), file, config))
159
222
  }
160
223
  if (file === '.gitignore' && existsSync(destination)) {
161
- content = Buffer.from(mergeGitignore(content.toString('utf8'), readFileSync(destination, 'utf8')))
224
+ content = Buffer.from(
225
+ mergeGitignore(content.toString('utf8'), readFileSync(destination, 'utf8'))
226
+ )
227
+ }
228
+ if (file === '.oxfmtrc.json' && existsSync(destination)) {
229
+ content = Buffer.from(
230
+ mergeIgnorePatternsConfig(content.toString('utf8'), readFileSync(destination, 'utf8'))
231
+ )
162
232
  }
163
- if (file === '.prettierignore' && existsSync(destination)) {
164
- content = Buffer.from(mergeIgnoreFile(content.toString('utf8'), readFileSync(destination, 'utf8')))
233
+ if (file === '.oxlintrc.json' && existsSync(destination)) {
234
+ content = Buffer.from(
235
+ mergeIgnorePatternsConfig(content.toString('utf8'), readFileSync(destination, 'utf8'))
236
+ )
165
237
  }
166
238
  if (!existsSync(destination) || !buffersEqual(content, readFileSync(destination))) {
167
239
  changed.push(file)
@@ -174,7 +246,8 @@ export function syncRepository(options) {
174
246
  if (!existsSync(destination)) continue
175
247
  if (isGeneratedEventCaller(readFileSync(destination, 'utf8'), stem, runtimeRepository)) {
176
248
  changed.push(`.github/workflows/${stem}.yml`)
177
- if (dryRun) console.log(`Would remove generated legacy caller ${stem}.yml; validation.yml replaces it.`)
249
+ if (dryRun)
250
+ console.log(`Would remove generated legacy caller ${stem}.yml; validation.yml replaces it.`)
178
251
  else rmSync(destination, { force: true })
179
252
  } else {
180
253
  console.log(`Preserved ${stem}.yml: not recognized as a Code Foundry-generated caller.`)
@@ -186,25 +259,39 @@ export function syncRepository(options) {
186
259
  // linger dormant (it triggers on pushes to a branch that does not exist).
187
260
  if (!isStagingRelease(config.git_workflow)) {
188
261
  const promotion = join(target, '.github/workflows/release-pr.yml')
189
- if (existsSync(promotion) && isGeneratedEventCaller(readFileSync(promotion, 'utf8'), 'release-pr', runtimeRepository)) {
262
+ if (
263
+ existsSync(promotion) &&
264
+ isGeneratedEventCaller(readFileSync(promotion, 'utf8'), 'release-pr', runtimeRepository)
265
+ ) {
190
266
  changed.push('.github/workflows/release-pr.yml')
191
- if (dryRun) console.log('Would remove generated release-pr caller; the direct topology targets pull requests at main.')
267
+ if (dryRun)
268
+ console.log(
269
+ 'Would remove generated release-pr caller; the direct topology targets pull requests at main.'
270
+ )
192
271
  else rmSync(promotion, { force: true })
193
272
  }
194
273
  }
195
274
 
196
- const releaseManifest = buildReleaseManifest(target, mergeReleaseConfig(target, sourcePath(source, 'release-please-config.json')))
275
+ const releaseManifest = buildReleaseManifest(
276
+ target,
277
+ mergeReleaseConfig(target, sourcePath(source, 'release-please-config.json'))
278
+ )
197
279
  if (releaseManifest) {
198
280
  const manifestPath = join(target, '.release-please-manifest.json')
199
281
  /** @type {Record<string, string>} */
200
282
  let existingManifest = {}
201
283
  if (existsSync(manifestPath)) {
202
- try { existingManifest = JSON.parse(readFileSync(manifestPath, 'utf8')) }
203
- catch { existingManifest = {} }
284
+ try {
285
+ existingManifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
286
+ } catch {
287
+ existingManifest = {}
288
+ }
204
289
  }
205
290
  /** @type {Record<string, string>} */
206
291
  const mergedManifest = {}
207
- for (const directory of [...new Set([...Object.keys(releaseManifest), ...Object.keys(existingManifest)])].sort()) {
292
+ for (const directory of [
293
+ ...new Set([...Object.keys(releaseManifest), ...Object.keys(existingManifest)]),
294
+ ].sort()) {
208
295
  mergedManifest[directory] = existingManifest[directory] ?? releaseManifest[directory]
209
296
  }
210
297
  const content = `${JSON.stringify(mergedManifest, null, 2)}\n`
@@ -218,11 +305,15 @@ export function syncRepository(options) {
218
305
  const sourceLicense = join(source, '.github/licenses', licenseFile)
219
306
  if (!existsSync(sourceLicense)) throw new Error(`License template missing: ${sourceLicense}`)
220
307
  const licenseContent = readFileSync(sourceLicense)
221
- if (!existsSync(join(target, 'LICENSE')) || !buffersEqual(licenseContent, readFileSync(join(target, 'LICENSE')))) {
308
+ if (
309
+ !existsSync(join(target, 'LICENSE')) ||
310
+ !buffersEqual(licenseContent, readFileSync(join(target, 'LICENSE')))
311
+ ) {
222
312
  changed.push('LICENSE')
223
313
  writeOrReport(join(target, 'LICENSE'), licenseContent, dryRun)
224
314
  }
225
- if (!existsSync(join(target, 'NOTICE'))) writeOrReport(join(target, 'NOTICE'), readFileSync(join(source, 'NOTICE')), dryRun)
315
+ if (!existsSync(join(target, 'NOTICE')))
316
+ writeOrReport(join(target, 'NOTICE'), readFileSync(join(source, 'NOTICE')), dryRun)
226
317
  }
227
318
 
228
319
  for (const file of legacyFiles) {
@@ -233,21 +324,53 @@ export function syncRepository(options) {
233
324
  else rmSync(destination, { force: true })
234
325
  }
235
326
  }
236
- for (const file of ['ruff.toml', '.prettierrc', '.prettierignore']) {
237
- const relevant = file === 'ruff.toml' ? includesValue(languages, 'python') : includesValue(languages, 'typescript')
327
+ for (const file of ['ruff.toml', '.oxfmtrc.json', '.oxlintrc.json']) {
328
+ const relevant =
329
+ file === 'ruff.toml'
330
+ ? includesValue(languages, 'python')
331
+ : includesValue(languages, 'typescript')
238
332
  if (!relevant && existsSync(join(target, file))) {
239
333
  changed.push(file)
240
334
  if (dryRun) console.log(`Would remove irrelevant language configuration ${file}`)
241
335
  else rmSync(join(target, file), { force: true })
242
336
  }
243
337
  }
244
- if (includesValue(languages, 'typescript') && existsSync(join(target, '.prettierignore'))) {
245
- const prettierIgnore = readFileSync(join(target, '.prettierignore'), 'utf8')
246
- const present = new Set(prettierIgnore.split(/\r?\n/).map((line) => line.trim()))
247
- const missing = ['.github/.code-foundry', '.github/actions/'].filter((entry) => !present.has(entry))
248
- if (missing.length) {
249
- writeOrReport(join(target, '.prettierignore'), `${prettierIgnore.trimEnd()}\n${missing.join('\n')}\n`, dryRun)
338
+ if (includesValue(languages, 'typescript')) {
339
+ // The Oxfmt baseline supersedes .prettierrc/.prettierignore. Entries the
340
+ // consumer added beyond the baseline survive the migration inside the
341
+ // .oxfmtrc.json ignorePatterns list.
342
+ const customPatterns = []
343
+ const legacyIgnore = join(target, '.prettierignore')
344
+ if (existsSync(legacyIgnore)) {
345
+ const baselineEntries = new Set(['CHANGELOG.md', '.github/.code-foundry', '.github/actions/'])
346
+ for (const line of readFileSync(legacyIgnore, 'utf8').split(/\r?\n/)) {
347
+ const entry = line.trim()
348
+ if (!entry || entry.startsWith('#') || baselineEntries.has(entry)) continue
349
+ customPatterns.push(entry)
350
+ }
250
351
  changed.push('.prettierignore')
352
+ if (dryRun) console.log('Would remove superseded .prettierignore')
353
+ else rmSync(legacyIgnore, { force: true })
354
+ }
355
+ const legacyConfig = join(target, '.prettierrc')
356
+ if (existsSync(legacyConfig)) {
357
+ changed.push('.prettierrc')
358
+ if (dryRun) console.log('Would remove superseded .prettierrc')
359
+ else rmSync(legacyConfig, { force: true })
360
+ }
361
+ ensureOxfmtIgnorePatterns(
362
+ target,
363
+ ['.github/.code-foundry', '.github/actions/', ...customPatterns],
364
+ changed,
365
+ dryRun
366
+ )
367
+ } else {
368
+ for (const file of ['.prettierrc', '.prettierignore']) {
369
+ if (existsSync(join(target, file))) {
370
+ changed.push(file)
371
+ if (dryRun) console.log(`Would remove irrelevant language configuration ${file}`)
372
+ else rmSync(join(target, file), { force: true })
373
+ }
251
374
  }
252
375
  }
253
376
  if (!dryRun && existsSync(join(target, '.githooks/pre-commit'))) {
@@ -267,20 +390,33 @@ export function syncRepository(options) {
267
390
  * Package and release-type policy is detected from the repository itself and
268
391
  * must never leak from the runtime template.
269
392
  */
270
- const RELEASE_BASELINE_KEYS = ['$schema', 'bump-minor-pre-major', 'changelog-sections', 'include-component-in-tag']
393
+ const RELEASE_BASELINE_KEYS = [
394
+ '$schema',
395
+ 'bump-minor-pre-major',
396
+ 'changelog-sections',
397
+ 'include-component-in-tag',
398
+ ]
271
399
 
272
400
  /** @param {string} target @param {string} sourceFile @returns {Record<string, any>} */
273
401
  function mergeReleaseConfig(target, sourceFile) {
274
402
  /** @type {Record<string, any>} */
275
403
  let baseline = {}
276
- try { baseline = JSON.parse(readFileSync(sourceFile, 'utf8')) }
277
- catch { baseline = {} }
278
- const safeBaseline = Object.fromEntries(RELEASE_BASELINE_KEYS.filter((key) => key in baseline).map((key) => [key, baseline[key]]))
404
+ try {
405
+ baseline = JSON.parse(readFileSync(sourceFile, 'utf8'))
406
+ } catch {
407
+ baseline = {}
408
+ }
409
+ const safeBaseline = Object.fromEntries(
410
+ RELEASE_BASELINE_KEYS.filter((key) => key in baseline).map((key) => [key, baseline[key]])
411
+ )
279
412
  const destination = join(target, 'release-please-config.json')
280
413
  let existing = baseline
281
414
  if (existsSync(destination)) {
282
- try { existing = JSON.parse(readFileSync(destination, 'utf8')) }
283
- catch { existing = baseline }
415
+ try {
416
+ existing = JSON.parse(readFileSync(destination, 'utf8'))
417
+ } catch {
418
+ existing = baseline
419
+ }
284
420
  }
285
421
  return buildReleaseConfig(target, { ...safeBaseline, ...existing })
286
422
  }
@@ -293,9 +429,11 @@ function renderReleaseConfig(target, sourceFile) {
293
429
  /** @param {string} file @param {string} languages @param {string} features @param {Record<string, string>} config */
294
430
  function shouldInclude(file, languages, features, config) {
295
431
  if (file === 'ruff.toml') return includesValue(languages, 'python')
296
- if (file === '.prettierrc' || file === '.prettierignore') return includesValue(languages, 'typescript')
432
+ if (file === '.oxfmtrc.json' || file === '.oxlintrc.json')
433
+ return includesValue(languages, 'typescript')
297
434
  if (file === '.github/dependabot.yml') return includesValue(features, 'dependabot')
298
- if (file === '.github/workflows/opencode-security.yml') return ['true', 'auto'].includes(config.opencode_security ?? 'false')
435
+ if (file === '.github/workflows/opencode-security.yml')
436
+ return ['true', 'auto'].includes(config.opencode_security ?? 'false')
299
437
  const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
300
438
  // The staging promotion caller only exists in the staging-release topology;
301
439
  // direct repositories open feature branches into main and need no promotion.
@@ -303,7 +441,10 @@ function shouldInclude(file, languages, features, config) {
303
441
  // The tiered validation caller supersedes the legacy ci/test/security/codeql
304
442
  // event callers, so legacy feature names keep selecting it.
305
443
  if (workflow === 'validation') {
306
- return includesValue(features, 'validation') || LEGACY_GENERATED_CALLERS.some((legacy) => includesValue(features, legacy))
444
+ return (
445
+ includesValue(features, 'validation') ||
446
+ LEGACY_GENERATED_CALLERS.some((legacy) => includesValue(features, legacy))
447
+ )
307
448
  }
308
449
  return !workflow || includesValue(features, workflow)
309
450
  }
@@ -332,14 +473,20 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
332
473
  const localPrefix = 'uses: ./.github/workflows/'
333
474
  const remotePrefix = `uses: ${repository}/.github/workflows/`
334
475
  let rendered = content.replaceAll(localPrefix, remotePrefix)
335
- rendered = rendered.replace(new RegExp(`${escapeRegExp(remotePrefix)}([^\\s@]+)`, 'g'), `$&@${ref}`)
476
+ rendered = rendered.replace(
477
+ new RegExp(`${escapeRegExp(remotePrefix)}([^\\s@]+)`, 'g'),
478
+ `$&@${ref}`
479
+ )
336
480
  // Pin every runtime reference in the rendered caller: the orchestrator input
337
481
  // in the `with:` block and the mode job's runtime checkout. Self templates
338
482
  // use ${{ github.sha }} and are only rewritten when rendered for consumers.
339
483
  rendered = rendered.replace(/^(\s+runtime-ref:)\s+.*$/gm, `$1 ${ref}`)
340
484
  rendered = rendered.replace(/^(\s+ref:)\s+\$\{\{\s*github\.sha\s*\}\}\s*$/gm, `$1 ${ref}`)
341
485
  rendered = rendered.replace(/^(\s+runtime-repository:)\s+.*$/gm, `$1 ${repository}`)
342
- rendered = rendered.replace(new RegExp(`^(\\s+repository:)\\s+0xPlayerOne\\/code-foundry\\s*$`, 'gm'), `$1 ${repository}`)
486
+ rendered = rendered.replace(
487
+ new RegExp(`^(\\s+repository:)\\s+0xPlayerOne\\/code-foundry\\s*$`, 'gm'),
488
+ `$1 ${repository}`
489
+ )
343
490
  /** @type {Record<string, string|undefined>} */
344
491
  const runners = {
345
492
  ci: config.ci_runner ?? config.runner,
@@ -361,7 +508,10 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
361
508
  // The draft PR caller states the PR base explicitly so the shared
362
509
  // reusable workflow creates pull requests against the repository's
363
510
  // configured integration branch (staging) or main (direct).
364
- rendered = rendered.replace(/^(\s+base:)\s+.*$/m, `$1 ${isStagingRelease(config.git_workflow) ? 'staging' : 'main'}`)
511
+ rendered = rendered.replace(
512
+ /^(\s+base:)\s+.*$/m,
513
+ `$1 ${isStagingRelease(config.git_workflow) ? 'staging' : 'main'}`
514
+ )
365
515
  }
366
516
  const runner = workflow ? runners[workflow] : undefined
367
517
  if (runner) rendered = rendered.replace(/^(\s+runner:)\s+.*$/m, `$1 ${runner}`)
@@ -514,10 +664,7 @@ const DIRECT_DOC_REPLACEMENTS = {
514
664
  'git switch staging\ngit pull --ff-only origin staging',
515
665
  'git switch main\ngit pull --ff-only origin main',
516
666
  ],
517
- [
518
- '1. Start from an up-to-date `staging` branch.',
519
- '1. Start from an up-to-date `main` branch.',
520
- ],
667
+ ['1. Start from an up-to-date `staging` branch.', '1. Start from an up-to-date `main` branch.'],
521
668
  [
522
669
  '8. Push the branch and open a pull request into `staging`.',
523
670
  '8. Push the branch and open a pull request into `main`.',
@@ -526,10 +673,7 @@ const DIRECT_DOC_REPLACEMENTS = {
526
673
  '10. Merge with a squash after required checks pass and the change is ready; feature PRs land on `staging` with squash merges.',
527
674
  '10. Merge with a squash after required checks pass and the change is ready; feature PRs land on `main` with squash merges.',
528
675
  ],
529
- [
530
- '3. Branch from the upstream `staging` branch.',
531
- '3. Branch from the upstream `main` branch.',
532
- ],
676
+ ['3. Branch from the upstream `staging` branch.', '3. Branch from the upstream `main` branch.'],
533
677
  [
534
678
  '7. Push to the fork and open a pull request targeting `staging`.',
535
679
  '7. Push to the fork and open a pull request targeting `main`.',
@@ -542,20 +686,14 @@ const DIRECT_DOC_REPLACEMENTS = {
542
686
  '| Change | Target | Merge method | Merge gate |\n| ---------------------------- | --------- | ----------------------------------------------- | --------------------------------------------------------- |\n| Working branch | `staging` | Squash | All applicable required checks pass |\n| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
543
687
  '| Change | Target | Merge method | Merge gate |\n|----------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |',
544
688
  ],
545
- [
546
- '1. Create a focused branch from `staging`.',
547
- '1. Create a focused branch from `main`.',
548
- ],
689
+ ['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
549
690
  ],
550
691
  '.github/SECURITY.md': [
551
692
  [
552
693
  'The latest commit on `staging` receives security patches. Patches are promoted to `main` through the next release cycle.',
553
694
  'The latest commit on `main` receives security patches.',
554
695
  ],
555
- [
556
- '| `staging` | ✅ |\n',
557
- '',
558
- ],
696
+ ['| `staging` | ✅ |\n', ''],
559
697
  ],
560
698
  }
561
699
 
@@ -606,7 +744,9 @@ function validateRustCodeqlConfig(config) {
606
744
  }
607
745
 
608
746
  /** @param {string} value */
609
- function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') }
747
+ function escapeRegExp(value) {
748
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
749
+ }
610
750
 
611
751
  /**
612
752
  * Recognize a Code Foundry-generated legacy event caller (the consumer copies
@@ -626,49 +766,113 @@ export function isGeneratedEventCaller(content, stem, runtimeRepository) {
626
766
  if (/^\s*(runs-on|steps):/m.test(text)) return false
627
767
  if (!text.includes('runtime-repository:')) return false
628
768
  if (!new RegExp(`^ ${stem}:`, 'm').test(text)) return false
629
- return new RegExp(`uses:\\s*${escapeRegExp(runtimeRepository)}/\\.github/workflows/${stem}\\.yml@`).test(text)
769
+ return new RegExp(
770
+ `uses:\\s*${escapeRegExp(runtimeRepository)}/\\.github/workflows/${stem}\\.yml@`
771
+ ).test(text)
630
772
  }
631
773
 
632
774
  /** @param {string} baseline @param {string} existing */
633
775
  function mergeGitignore(baseline, existing) {
634
776
  const marker = '# Repository-specific rules'
635
- const custom = existing.includes(marker) ? existing.slice(existing.indexOf(marker) + marker.length).trim() : ''
777
+ const custom = existing.includes(marker)
778
+ ? existing.slice(existing.indexOf(marker) + marker.length).trim()
779
+ : ''
636
780
  return custom ? `${baseline.trimEnd()}\n\n${marker}\n${custom}\n` : baseline
637
781
  }
638
782
 
639
- /** @param {string} baseline @param {string} existing */
640
- function mergeIgnoreFile(baseline, existing) {
641
- const marker = '# Repository-specific rules'
642
- const baselineLines = new Set(baseline.split(/\r?\n/).map((line) => line.trim()).filter(Boolean))
643
- const custom = existing.split(/\r?\n/)
644
- .map((line) => line.trimEnd())
645
- .filter((line) => line.trim() && line.trim() !== marker && !baselineLines.has(line.trim()))
646
- return custom.length ? `${baseline.trimEnd()}\n\n${marker}\n${custom.join('\n')}\n` : baseline
783
+ /**
784
+ * Merge a baseline Oxc config (.oxfmtrc.json / .oxlintrc.json) with the
785
+ * consumer's current config. The baseline owns every key except
786
+ * `ignorePatterns`, where consumer-specific patterns (added by sync
787
+ * migrations or by the repository) are preserved so repeated syncs never
788
+ * churn them.
789
+ * @param {string} baseline @param {string} existing @returns {string}
790
+ */
791
+ function mergeIgnorePatternsConfig(baseline, existing) {
792
+ /** @type {string[]} */
793
+ let extra = []
794
+ try {
795
+ const parsed = JSON.parse(existing)
796
+ if (Array.isArray(parsed.ignorePatterns)) extra = parsed.ignorePatterns
797
+ } catch {
798
+ return baseline
799
+ }
800
+ if (extra.length === 0) return baseline
801
+ let config
802
+ try {
803
+ config = JSON.parse(baseline)
804
+ } catch {
805
+ return baseline
806
+ }
807
+ const base = Array.isArray(config.ignorePatterns) ? config.ignorePatterns : []
808
+ const missing = extra.filter((pattern) => !base.includes(pattern))
809
+ if (missing.length === 0) return baseline
810
+ config.ignorePatterns = [...base, ...missing]
811
+ return `${JSON.stringify(config, null, 2)}\n`
812
+ }
813
+
814
+ /**
815
+ * Ensure the consumer's .oxfmtrc.json carries every baseline ignore pattern
816
+ * (plus any migrated custom patterns). Missing patterns are appended to the
817
+ * existing list; the file is left untouched when nothing is missing or the
818
+ * config is absent/unparseable.
819
+ * @param {string} target @param {string[]} patterns @param {string[]} changed @param {boolean} dryRun
820
+ */
821
+ function ensureOxfmtIgnorePatterns(target, patterns, changed, dryRun) {
822
+ const path = join(target, '.oxfmtrc.json')
823
+ if (patterns.length === 0 || !existsSync(path)) return
824
+ let config
825
+ try {
826
+ config = JSON.parse(readFileSync(path, 'utf8'))
827
+ } catch {
828
+ return
829
+ }
830
+ const existing = Array.isArray(config.ignorePatterns) ? config.ignorePatterns : []
831
+ const merged = [...existing]
832
+ for (const pattern of patterns) {
833
+ if (!merged.includes(pattern)) merged.push(pattern)
834
+ }
835
+ if (merged.length === existing.length) return
836
+ config.ignorePatterns = merged
837
+ changed.push('.oxfmtrc.json')
838
+ writeOrReport(path, `${JSON.stringify(config, null, 2)}\n`, dryRun)
647
839
  }
648
840
 
649
841
  /** @param {string} file @param {string} content */
650
842
  function isLegacyManagedDoc(file, content) {
651
843
  if (file === 'AGENTS.md') {
652
- return content.includes('.github/scripts/bootstrap.sh') && content.includes('bash .github/scripts/ci.sh')
844
+ return (
845
+ content.includes('.github/scripts/bootstrap.sh') &&
846
+ content.includes('bash .github/scripts/ci.sh')
847
+ )
653
848
  }
654
849
  if (file === '.github/CONTRIBUTING.md') {
655
- return content.includes('.github/scripts/bootstrap.sh') && content.includes('.github/template.yml')
850
+ return (
851
+ content.includes('.github/scripts/bootstrap.sh') && content.includes('.github/template.yml')
852
+ )
656
853
  }
657
854
  return false
658
855
  }
659
856
 
660
857
  /** @param {string} target @param {string[]} args */
661
- function git(target, args) { spawnSync('git', args, { cwd: target, stdio: 'ignore' }) }
858
+ function git(target, args) {
859
+ spawnSync('git', args, { cwd: target, stdio: 'ignore' })
860
+ }
662
861
 
663
862
  /** @param {string} file @param {Buffer|string} content @param {boolean} dryRun */
664
863
  function writeOrReport(file, content, dryRun) {
665
- if (dryRun) { console.log(`Would sync ${file}`); return }
864
+ if (dryRun) {
865
+ console.log(`Would sync ${file}`)
866
+ return
867
+ }
666
868
  mkdirSync(dirname(file), { recursive: true })
667
869
  writeFileSync(file, content)
668
870
  }
669
871
 
670
872
  /** @param {Buffer} a @param {Buffer} b */
671
- function buffersEqual(a, b) { return a.equals(b) }
873
+ function buffersEqual(a, b) {
874
+ return a.equals(b)
875
+ }
672
876
 
673
877
  /** @param {string} root @param {string} source @returns {Record<string,string>} */
674
878
  function createDefaultConfig(root, source) {
@@ -676,31 +880,61 @@ function createDefaultConfig(root, source) {
676
880
  const packageManager = detectPackageManager(root)
677
881
  const runners = recommendRunners(root)
678
882
  return {
679
- version: '1', profile: detectProfile(root), languages, features: 'all', codeql: 'auto', dependency_review: 'auto', package_manager: packageManager,
680
- codeql_rust_shards: '["all"]', codeql_rust_threads: '1', codeql_rust_max_parallel: '1',
681
- runtime_repository: '0xPlayerOne/code-foundry', runtime_ref: `v${readPackageVersion(source)}`,
883
+ version: '1',
884
+ profile: detectProfile(root),
885
+ languages,
886
+ features: 'all',
887
+ codeql: 'auto',
888
+ dependency_review: 'auto',
889
+ package_manager: packageManager,
890
+ codeql_rust_shards: '["all"]',
891
+ codeql_rust_threads: '1',
892
+ codeql_rust_max_parallel: '1',
893
+ runtime_repository: '0xPlayerOne/code-foundry',
894
+ runtime_ref: `v${readPackageVersion(source)}`,
682
895
  ...runners,
683
- toolchain: 'auto', staging_validation_mode: 'fast',
684
- prune_standard: 'false', cache_packages: 'auto', cache_build: 'auto', coverage_minimum: '80', turbo_remote: 'auto',
685
- release_type: detectPackageManager(root) === 'none' ? 'auto' : 'node', npm_publish: 'false',
686
- post_release: 'false', post_release_workflow: '', post_release_mode: 'auto',
687
- opencode_security: 'false', opencode_security_model: '',
688
- sync_mode: 'overlay', custom_workflows: 'preserve',
689
- license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'direct', merge_strategy: 'rebase', release_merge_strategy: 'rebase',
896
+ toolchain: 'auto',
897
+ staging_validation_mode: 'fast',
898
+ prune_standard: 'false',
899
+ cache_packages: 'auto',
900
+ cache_build: 'auto',
901
+ coverage_minimum: '80',
902
+ turbo_remote: 'auto',
903
+ release_type: detectPackageManager(root) === 'none' ? 'auto' : 'node',
904
+ npm_publish: 'false',
905
+ post_release: 'false',
906
+ post_release_workflow: '',
907
+ post_release_mode: 'auto',
908
+ opencode_security: 'false',
909
+ opencode_security_model: '',
910
+ sync_mode: 'overlay',
911
+ custom_workflows: 'preserve',
912
+ license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later',
913
+ git_workflow: 'direct',
914
+ merge_strategy: 'rebase',
915
+ release_merge_strategy: 'rebase',
690
916
  }
691
917
  }
692
918
 
693
919
  /** @param {Record<string,string>} config */
694
- function renderConfig(config) { return `${Object.entries(config).map(([key, value]) => renderConfigLine(key, value)).join('\n')}\n` }
920
+ function renderConfig(config) {
921
+ return `${Object.entries(config)
922
+ .map(([key, value]) => renderConfigLine(key, value))
923
+ .join('\n')}\n`
924
+ }
695
925
  /** @param {string} key @param {string} value */
696
926
  function renderConfigLine(key, value) {
697
927
  if (value === '') return `${key}:`
698
928
  // Quote values that YAML would parse as collections or that would trip
699
- // prettier's formatter (e.g. codeql_rust_shards: '["all"]').
700
- const needsQuotes = /^[\[\]{]|[:,#]\s|\s#/.test(value)
929
+ // the formatter (e.g. codeql_rust_shards: '["all"]').
930
+ const needsQuotes = /^[[\]{]|[:,#]\s|\s#/.test(value)
701
931
  return needsQuotes ? `${key}: '${value.replace(/'/g, "''")}'` : `${key}: ${value}`
702
932
  }
703
933
  /** @param {string} root */
704
934
  function readPackageVersion(root) {
705
- try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version ?? '0.0.0' } catch { return '0.0.0' }
935
+ try {
936
+ return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version ?? '0.0.0'
937
+ } catch {
938
+ return '0.0.0'
939
+ }
706
940
  }