code-foundry 1.5.0 → 1.5.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.
- package/.github/actions/codeql/action.yml +4 -0
- package/.github/workflows/codeql.yml +4 -0
- package/CHANGELOG.md +7 -0
- package/README.md +10 -9
- package/docs/CONFIGURATION.md +3 -2
- package/docs/RELEASES.md +15 -14
- package/docs/WORKFLOWS.md +23 -17
- package/package.json +1 -1
- package/src/commands/doctor.mjs +7 -2
- package/src/commands/fleet.mjs +15 -3
- package/src/commands/sync.mjs +124 -14
|
@@ -25,5 +25,9 @@ runs:
|
|
|
25
25
|
uses: github/codeql-action/analyze@v4
|
|
26
26
|
with:
|
|
27
27
|
category: ${{ inputs.category }}
|
|
28
|
+
# Pin pull-request uploads to the contributor's head SHA/ref so a
|
|
29
|
+
# moving merge ref cannot race the required code-scanning context.
|
|
30
|
+
ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
|
31
|
+
sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
|
28
32
|
upload-database: false
|
|
29
33
|
wait-for-processing: false
|
|
@@ -285,6 +285,10 @@ jobs:
|
|
|
285
285
|
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
|
|
286
286
|
with:
|
|
287
287
|
category: /language:rust/${{ steps.scope.outputs.scope_id }}
|
|
288
|
+
# Keep uploads attached to the PR head rather than the mutable
|
|
289
|
+
# merge ref used by GitHub's pull_request event.
|
|
290
|
+
ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
|
291
|
+
sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
|
288
292
|
upload-database: false
|
|
289
293
|
wait-for-processing: false
|
|
290
294
|
- name: Not applicable
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.5.0...v1.5.1) (2026-09-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* sync-caller-runtime-pin ([#484](https://github.com/0xPlayerOne/code-foundry/issues/484)) ([e1a216a](https://github.com/0xPlayerOne/code-foundry/commit/e1a216a9dcebd040ef0eacdb20791f360a921908))
|
|
9
|
+
|
|
3
10
|
## [1.5.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.4.1...v1.5.0) (2026-09-07)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -91,15 +91,16 @@ See [Workflow and CI conventions](docs/WORKFLOWS.md) for triggers, required
|
|
|
91
91
|
checks, runners, coverage, caching, and custom workflow extensions.
|
|
92
92
|
|
|
93
93
|
The contribution policy defaults to the `direct` workflow: feature PRs squash
|
|
94
|
-
into `main`, and Release Please version PRs
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
into `main`, and Release Please version PRs use the configured
|
|
95
|
+
`release_merge_strategy` (rebase by default, or squash when opted in).
|
|
96
|
+
Repositories with a preview/staging environment opt into
|
|
97
|
+
`git_workflow: staging-release`, where feature PRs squash into `staging`, the
|
|
98
|
+
promotion PR rebases into `main` (`merge_strategy: rebase`), and Release Please
|
|
99
|
+
version PRs rebase into `main`. Release automation never defaults to a merge
|
|
100
|
+
method and never merges with `--admin`; `code-foundry doctor` and
|
|
101
|
+
`code-foundry sync` fail closed on any strategy outside the selected topology.
|
|
102
|
+
GitHub Stacks is not part of this topology and does not reduce the required
|
|
103
|
+
workflow runs.
|
|
103
104
|
|
|
104
105
|
## Releases and publishing
|
|
105
106
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -77,8 +77,9 @@ present.
|
|
|
77
77
|
- `direct` (default): feature branches open pull requests directly into
|
|
78
78
|
`main`. Validation and security scans run on every PR. No `staging` branch
|
|
79
79
|
exists, no promotion caller is generated, and `merge_strategy` is not
|
|
80
|
-
enforced.
|
|
81
|
-
|
|
80
|
+
enforced. Release Please version PRs use `release_merge_strategy` (rebase by
|
|
81
|
+
default; squash is also allowed). Dependabot updates target `main`. This is
|
|
82
|
+
the right choice when a repository has no preview or staging environment.
|
|
82
83
|
- `staging-release` (opt-in): feature branches squash into `staging`, a
|
|
83
84
|
promotion PR rebases validated changes into `main` (`merge_strategy:
|
|
84
85
|
rebase`), and Release Please version PRs rebase into `main`
|
package/docs/RELEASES.md
CHANGED
|
@@ -24,10 +24,11 @@ topology, feature and fix branches land on `staging` with **squash** merges,
|
|
|
24
24
|
the `staging` → `main` promotion PR merges with **rebase** (`merge_strategy:
|
|
25
25
|
rebase`), and Release Please version PRs merge with **rebase**
|
|
26
26
|
(`release_merge_strategy: rebase`). In the `direct` topology, feature and fix
|
|
27
|
-
branches squash straight into `main` and
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
branches squash straight into `main` and Release Please version PRs use the
|
|
28
|
+
configured `release_merge_strategy` (**rebase** by default, or **squash** when
|
|
29
|
+
opted in). `merge_strategy` is not enforced. Release automation never defaults
|
|
30
|
+
to a merge method and never merges with `--admin`: `staging-release` accepts
|
|
31
|
+
only rebase for release PRs, while `direct` accepts rebase or squash.
|
|
31
32
|
|
|
32
33
|
The release workflow opens or updates a versioned PR after changes reach
|
|
33
34
|
`main`. Merging that PR updates the changelog, creates the Git tag and GitHub
|
|
@@ -45,21 +46,21 @@ release_type: auto # auto, node, python, rust, simple, or none
|
|
|
45
46
|
npm_publish: false # true only for an npm package
|
|
46
47
|
git_workflow: direct # direct (default) or staging-release
|
|
47
48
|
merge_strategy: rebase # staging-release only: staging -> main promotion PRs rebase
|
|
48
|
-
release_merge_strategy: rebase # required
|
|
49
|
+
release_merge_strategy: rebase # required; direct also allows squash
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
`git_workflow: staging-release` is opt-in; without it, repositories use the
|
|
52
53
|
`direct` flow and no promotion PR exists. When the staging-release topology is
|
|
53
54
|
selected, `merge_strategy` applies to promotion PRs (`staging` into `main`)
|
|
54
55
|
and `release_merge_strategy` to Release Please version PRs; feature PRs into
|
|
55
|
-
`staging` use squash merges. `code-foundry doctor
|
|
56
|
-
reject any non-`rebase` `merge_strategy`
|
|
57
|
-
|
|
58
|
-
release
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
`staging` use squash merges. `code-foundry doctor`, `code-foundry sync`, and
|
|
57
|
+
the release workflow reject any non-`rebase` `merge_strategy` or release
|
|
58
|
+
strategy in `staging-release`; `direct` accepts `rebase` or `squash` for
|
|
59
|
+
release PRs and never falls back to `merge`. Both keep `main` fully linear,
|
|
60
|
+
which is what makes the post-release reconciliation possible. The final
|
|
61
|
+
branch trees are inspected before mutation; validated main-only changes are
|
|
62
|
+
inherited by `staging`, while unpromoted staging work is replayed on top of
|
|
63
|
+
`main`.
|
|
63
64
|
|
|
64
65
|
Promotion never opens a pull request directly from a divergent `staging`
|
|
65
66
|
history. The workflow creates or refreshes a deterministic promotion branch
|
|
@@ -135,6 +136,6 @@ already passed).
|
|
|
135
136
|
|
|
136
137
|
1. Merge tested changes into `main` (direct: feature PRs; staging-release: promote `staging` into `main`).
|
|
137
138
|
2. Review the generated Release Please PR and changelog.
|
|
138
|
-
3. Merge the release PR with the repository's configured `release_merge_strategy` (**rebase
|
|
139
|
+
3. Merge the release PR with the repository's configured `release_merge_strategy` (**rebase** by default; **squash** is also valid for direct topology).
|
|
139
140
|
4. Confirm the GitHub Release and any package publication.
|
|
140
141
|
5. staging-release only: synchronize `staging` with the new `main` release commit.
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -100,15 +100,17 @@ succeeds.
|
|
|
100
100
|
## Merge methods
|
|
101
101
|
|
|
102
102
|
The merge audit pins one merge method per transition. `code-foundry doctor`
|
|
103
|
-
and `code-foundry sync`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
103
|
+
and `code-foundry sync` validate release strategy against the repository
|
|
104
|
+
topology. Staging-release release PRs require `rebase`; direct release PRs
|
|
105
|
+
allow `rebase` or `squash`. The release workflow fails closed on any other
|
|
106
|
+
strategy and never falls back to `merge`.
|
|
107
|
+
|
|
108
|
+
| Transition | Merge method | Enforcement |
|
|
109
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| Feature/fix PR into `main` (direct topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
|
|
111
|
+
| Feature/fix PR into `staging` (staging-release topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
|
|
112
|
+
| `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | Code Foundry creates a one-commit head with `main` as its parent and the exact validated `staging` tree; `merge_strategy` must be `rebase`, and merge commits are rejected |
|
|
113
|
+
| Release Please version PR into `main` | Configured (`release_merge_strategy`: rebase default; squash direct only) | Release automation fails closed on unsupported topology/strategy; never defaults to `merge`, never uses `--admin` |
|
|
112
114
|
|
|
113
115
|
The promotion rows above apply only to `staging-release`; `direct`
|
|
114
116
|
repositories never generate a promotion caller and `merge_strategy` is not
|
|
@@ -123,14 +125,18 @@ additional required checks after earlier ones have passed (for example
|
|
|
123
125
|
merge policy-blocked. The mergeability poll is bounded and fails closed on
|
|
124
126
|
conflicts or timeout; releases without an automation token remain manual.
|
|
125
127
|
|
|
126
|
-
Keeping `main` linear — rebase promotions and
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
Keeping `main` linear — rebase promotions and release PRs in the
|
|
129
|
+
`staging-release` topology, or a single-commit rebase/squash release PR in the
|
|
130
|
+
`direct` topology — is what lets the post-release reconciliation fast-forward
|
|
131
|
+
or replay `staging` safely. `direct` repositories have no reconciliation step:
|
|
132
|
+
releases merge straight into `main` with the configured
|
|
133
|
+
`release_merge_strategy`.
|
|
134
|
+
|
|
135
|
+
Protect `main` with the aggregate `Validation / Gate`. Require squash for
|
|
136
|
+
feature/fix pull requests and permit the configured Release Please method:
|
|
137
|
+
rebase in `staging-release`, or rebase/squash in `direct`. In the
|
|
138
|
+
`staging-release` topology, protect `staging` the same way with a single GitHub
|
|
139
|
+
Actions integration path. That path uses the
|
|
134
140
|
GitHub Actions integration token by default, and optionally an SSH deploy key
|
|
135
141
|
when `STAGING_DEPLOY_KEY` is configured. The deploy key is required only when
|
|
136
142
|
a personal-repository ruleset for `staging` enforces a Deploy Key bypass for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.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": {
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -122,9 +122,14 @@ export function doctor(root, options = {}) {
|
|
|
122
122
|
)
|
|
123
123
|
}
|
|
124
124
|
const releaseMergeStrategy = config.release_merge_strategy ?? ''
|
|
125
|
-
|
|
125
|
+
const allowedReleaseStrategies =
|
|
126
|
+
workflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
|
|
127
|
+
if (
|
|
128
|
+
includesValue(features, 'release') &&
|
|
129
|
+
!allowedReleaseStrategies.includes(releaseMergeStrategy)
|
|
130
|
+
) {
|
|
126
131
|
error(
|
|
127
|
-
`release_merge_strategy must be "rebase" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`
|
|
132
|
+
`release_merge_strategy must be "rebase"${workflow === 'staging-release' ? '' : ' or "squash" (direct topology)'} for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`
|
|
128
133
|
)
|
|
129
134
|
}
|
|
130
135
|
for (const name of ['validation', 'draft-pr', 'release-pr', 'release']) {
|
package/src/commands/fleet.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } fro
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { spawnSync } from 'node:child_process'
|
|
7
|
-
import { syncRepository } from './sync.mjs'
|
|
7
|
+
import { readPackageVersion, syncRepository } from './sync.mjs'
|
|
8
8
|
|
|
9
9
|
/** @typedef {{ path: string, repository: string, runtimeRef: string, dirty: boolean, configured: boolean, gitWorkflow: string }} FleetRepository */
|
|
10
10
|
|
|
@@ -32,6 +32,13 @@ export function discoverRepositories(root) {
|
|
|
32
32
|
|
|
33
33
|
/** @param {string} root @param {string} source @param {{ createPr?: boolean, dryRun?: boolean, force?: boolean, version: string, exclude?: string[] }} options */
|
|
34
34
|
export function upgradeFleet(root, source, options) {
|
|
35
|
+
const sourceVersion = `v${readPackageVersion(source)}`
|
|
36
|
+
const version = options.version ?? sourceVersion
|
|
37
|
+
if (options.version && options.version !== sourceVersion) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`fleet upgrade target ${options.version} does not match the runtime source checkout (${sourceVersion}); refresh the code-foundry checkout to ${options.version} before upgrading so the rendered callers and the declared runtime agree.`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
35
42
|
const repositories = discoverRepositories(root)
|
|
36
43
|
const report = []
|
|
37
44
|
for (const repository of repositories) {
|
|
@@ -80,7 +87,7 @@ export function upgradeFleet(root, source, options) {
|
|
|
80
87
|
})
|
|
81
88
|
continue
|
|
82
89
|
}
|
|
83
|
-
report.push(upgradeRepository(repository, source,
|
|
90
|
+
report.push(upgradeRepository(repository, source, version))
|
|
84
91
|
}
|
|
85
92
|
console.log(JSON.stringify(report, null, 2))
|
|
86
93
|
return report
|
|
@@ -121,7 +128,12 @@ function upgradeRepository(repository, source, version) {
|
|
|
121
128
|
status: 'skipped',
|
|
122
129
|
reason: add.stderr.trim() || 'unable to create isolated worktree',
|
|
123
130
|
}
|
|
124
|
-
const result = syncRepository({
|
|
131
|
+
const result = syncRepository({
|
|
132
|
+
target: temporary,
|
|
133
|
+
source,
|
|
134
|
+
force: false,
|
|
135
|
+
runtimeRef: version,
|
|
136
|
+
})
|
|
125
137
|
const configFile = join(temporary, '.github/code-foundry.yml')
|
|
126
138
|
if (existsSync(configFile)) {
|
|
127
139
|
const current = readFileSync(configFile, 'utf8')
|
package/src/commands/sync.mjs
CHANGED
|
@@ -96,7 +96,7 @@ const legacyFiles = [
|
|
|
96
96
|
'.github/licenses/AGPL-3.0-or-later.txt',
|
|
97
97
|
]
|
|
98
98
|
|
|
99
|
-
/** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean }} SyncOptions */
|
|
99
|
+
/** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean, runtimeRef?: string }} SyncOptions */
|
|
100
100
|
|
|
101
101
|
/** @param {SyncOptions} options */
|
|
102
102
|
export function syncRepository(options) {
|
|
@@ -137,7 +137,11 @@ export function syncRepository(options) {
|
|
|
137
137
|
const features = configured(config.features, 'all')
|
|
138
138
|
const runtimeRepository = configured(config.runtime_repository, '0xPlayerOne/code-foundry')
|
|
139
139
|
const sourceRuntimeRef = `v${readPackageVersion(source)}`
|
|
140
|
-
|
|
140
|
+
// An explicit runtime ref (fleet upgrade) is authoritative: the rendered
|
|
141
|
+
// callers and the config pin must agree with it, otherwise an upgrade would
|
|
142
|
+
// declare one runtime while shipping another.
|
|
143
|
+
const targetRuntimeRef = options.runtimeRef ?? sourceRuntimeRef
|
|
144
|
+
let runtimeRef = options.runtimeRef ?? configured(config.runtime_ref, sourceRuntimeRef)
|
|
141
145
|
const toolchain = configured(config.toolchain, 'auto')
|
|
142
146
|
const overlays = overlayPolicy(target, config)
|
|
143
147
|
const rustCodeql = validateRustCodeqlConfig(config)
|
|
@@ -177,15 +181,17 @@ export function syncRepository(options) {
|
|
|
177
181
|
const changed = []
|
|
178
182
|
|
|
179
183
|
// Keep normal semver pins current during sync while preserving intentional
|
|
180
|
-
// refs such as `main`, `staging`, or a custom immutable SHA.
|
|
184
|
+
// refs such as `main`, `staging`, or a custom immutable SHA. An explicit
|
|
185
|
+
// runtime ref (fleet upgrade) is authoritative and overrides even those so
|
|
186
|
+
// the rendered callers and the config pin land on the same runtime.
|
|
181
187
|
if (
|
|
182
188
|
existingConfig.runtime_ref &&
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
existingConfig.runtime_ref !== targetRuntimeRef &&
|
|
190
|
+
(options.runtimeRef !== undefined || /^v\d+\.\d+\.\d+$/.test(existingConfig.runtime_ref))
|
|
185
191
|
) {
|
|
186
|
-
runtimeRef =
|
|
192
|
+
runtimeRef = targetRuntimeRef
|
|
187
193
|
const current = readFileSync(configPath, 'utf8')
|
|
188
|
-
const updated = current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${
|
|
194
|
+
const updated = current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${targetRuntimeRef}`)
|
|
189
195
|
if (updated !== current) {
|
|
190
196
|
changed.push('.github/code-foundry.yml')
|
|
191
197
|
writeOrReport(configPath, updated, dryRun)
|
|
@@ -829,6 +835,103 @@ function mergeGitignore(baseline, existing) {
|
|
|
829
835
|
: baseline
|
|
830
836
|
}
|
|
831
837
|
|
|
838
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
839
|
+
function isJsonObject(value) {
|
|
840
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** @param {string} source @returns {string} */
|
|
844
|
+
function stripJsonComments(source) {
|
|
845
|
+
const output = []
|
|
846
|
+
let inString = false
|
|
847
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
848
|
+
const char = source[index]
|
|
849
|
+
const next = source[index + 1]
|
|
850
|
+
if (inString) {
|
|
851
|
+
output.push(char)
|
|
852
|
+
if (char === '\\' && next !== undefined) {
|
|
853
|
+
output.push(next)
|
|
854
|
+
index += 1
|
|
855
|
+
} else if (char === '"') {
|
|
856
|
+
inString = false
|
|
857
|
+
}
|
|
858
|
+
continue
|
|
859
|
+
}
|
|
860
|
+
if (char === '"') {
|
|
861
|
+
inString = true
|
|
862
|
+
output.push(char)
|
|
863
|
+
continue
|
|
864
|
+
}
|
|
865
|
+
if (char === '/' && next === '/') {
|
|
866
|
+
output.push(' ', ' ')
|
|
867
|
+
index += 2
|
|
868
|
+
while (index < source.length && source[index] !== '\n' && source[index] !== '\r') {
|
|
869
|
+
output.push(' ')
|
|
870
|
+
index += 1
|
|
871
|
+
}
|
|
872
|
+
index -= 1
|
|
873
|
+
continue
|
|
874
|
+
}
|
|
875
|
+
if (char === '/' && next === '*') {
|
|
876
|
+
output.push(' ', ' ')
|
|
877
|
+
index += 2
|
|
878
|
+
while (index < source.length) {
|
|
879
|
+
const commentChar = source[index]
|
|
880
|
+
const commentNext = source[index + 1]
|
|
881
|
+
if (commentChar === '*' && commentNext === '/') {
|
|
882
|
+
output.push(' ', ' ')
|
|
883
|
+
index += 1
|
|
884
|
+
break
|
|
885
|
+
}
|
|
886
|
+
output.push(commentChar === '\n' || commentChar === '\r' ? commentChar : ' ')
|
|
887
|
+
index += 1
|
|
888
|
+
}
|
|
889
|
+
continue
|
|
890
|
+
}
|
|
891
|
+
output.push(char)
|
|
892
|
+
}
|
|
893
|
+
return output.join('')
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** @param {string} source @returns {string} */
|
|
897
|
+
function stripJsonTrailingCommas(source) {
|
|
898
|
+
const output = []
|
|
899
|
+
let inString = false
|
|
900
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
901
|
+
const char = source[index]
|
|
902
|
+
if (inString) {
|
|
903
|
+
output.push(char)
|
|
904
|
+
if (char === '\\') {
|
|
905
|
+
const escaped = source[index + 1]
|
|
906
|
+
if (escaped !== undefined) {
|
|
907
|
+
output.push(escaped)
|
|
908
|
+
index += 1
|
|
909
|
+
}
|
|
910
|
+
} else if (char === '"') {
|
|
911
|
+
inString = false
|
|
912
|
+
}
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
if (char === '"') {
|
|
916
|
+
inString = true
|
|
917
|
+
output.push(char)
|
|
918
|
+
continue
|
|
919
|
+
}
|
|
920
|
+
if (char === ',') {
|
|
921
|
+
let nextIndex = index + 1
|
|
922
|
+
while (/\s/.test(source[nextIndex] ?? '')) nextIndex += 1
|
|
923
|
+
if (source[nextIndex] === '}' || source[nextIndex] === ']') continue
|
|
924
|
+
}
|
|
925
|
+
output.push(char)
|
|
926
|
+
}
|
|
927
|
+
return output.join('')
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** @param {string} source @returns {unknown} */
|
|
931
|
+
function parseJsonc(source) {
|
|
932
|
+
return JSON.parse(stripJsonTrailingCommas(stripJsonComments(source)))
|
|
933
|
+
}
|
|
934
|
+
|
|
832
935
|
/**
|
|
833
936
|
* Merge a baseline Oxc config (.oxfmtrc.json / .oxlintrc.json) with the
|
|
834
937
|
* consumer's current config. The baseline owns every key it defines, except
|
|
@@ -836,7 +939,9 @@ function mergeGitignore(baseline, existing) {
|
|
|
836
939
|
* migrations or by the repository) are preserved so repeated syncs never
|
|
837
940
|
* churn them. Keys the baseline does not define (e.g. a repository-owned
|
|
838
941
|
* `overrides` block) belong to the consumer and are preserved as well, so
|
|
839
|
-
* a sync never silently drops repository-owned configuration.
|
|
942
|
+
* a sync never silently drops repository-owned configuration. Consumer
|
|
943
|
+
* category values are merged last so explicit category overrides survive the
|
|
944
|
+
* baseline's default category levels.
|
|
840
945
|
*
|
|
841
946
|
* When the merged semantics already match the consumer file, the exact
|
|
842
947
|
* existing bytes are returned untouched: rewriting canonical JSON here
|
|
@@ -848,23 +953,28 @@ function mergeIgnorePatternsConfig(baseline, existing) {
|
|
|
848
953
|
/** @type {Record<string, any>} */
|
|
849
954
|
let consumer = {}
|
|
850
955
|
try {
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
|
|
956
|
+
const parsed = parseJsonc(existing)
|
|
957
|
+
if (!isJsonObject(parsed)) return baseline
|
|
958
|
+
consumer = parsed
|
|
854
959
|
} catch {
|
|
855
960
|
return baseline
|
|
856
961
|
}
|
|
857
962
|
/** @type {Record<string, any>} */
|
|
858
963
|
let config = {}
|
|
859
964
|
try {
|
|
860
|
-
|
|
861
|
-
if (
|
|
965
|
+
const parsed = parseJsonc(baseline)
|
|
966
|
+
if (!isJsonObject(parsed)) return baseline
|
|
967
|
+
config = parsed
|
|
862
968
|
} catch {
|
|
863
969
|
return baseline
|
|
864
970
|
}
|
|
865
971
|
/** @type {Record<string, any>} */
|
|
866
972
|
const merged = { ...config }
|
|
867
973
|
for (const [key, value] of Object.entries(consumer)) {
|
|
974
|
+
if (key === 'categories' && isJsonObject(merged.categories) && isJsonObject(value)) {
|
|
975
|
+
merged.categories = { ...merged.categories, ...value }
|
|
976
|
+
continue
|
|
977
|
+
}
|
|
868
978
|
if (!(key in merged)) merged[key] = value
|
|
869
979
|
}
|
|
870
980
|
const extra = Array.isArray(consumer.ignorePatterns) ? consumer.ignorePatterns : []
|
|
@@ -1014,7 +1124,7 @@ function renderConfigLine(key, value) {
|
|
|
1014
1124
|
return needsQuotes ? `${key}: '${value.replace(/'/g, "''")}'` : `${key}: ${value}`
|
|
1015
1125
|
}
|
|
1016
1126
|
/** @param {string} root */
|
|
1017
|
-
function readPackageVersion(root) {
|
|
1127
|
+
export function readPackageVersion(root) {
|
|
1018
1128
|
try {
|
|
1019
1129
|
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version ?? '0.0.0'
|
|
1020
1130
|
} catch {
|