Skip to content

feat(config): recognize devbox.jsonc as an alternate config filename - #2950

Merged
mikeland73 merged 5 commits into
mainfrom
claude/focused-goldberg-4lsa9z
Sep 16, 2026
Merged

mikeland73 merged 5 commits into
mainfrom
claude/focused-goldberg-4lsa9z

Conversation

@mikeland73

@mikeland73 mikeland73 commented Aug 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2602 (cc @gjoseph92).

devbox.json is parsed as JSONC and already supports comments, but editors and GitHub diffs treat comments in a .json file as syntax errors. This PR lets a project name its config devbox.jsonc so standard tooling highlights it correctly, with no per-repo files.associations or .gitattributes workarounds.

Changes (all in internal/devconfig):

  • Add configfile.AltName ("devbox.jsonc") and configfile.ValidNames, and have searchDir try both names during discovery. devbox.json is listed first so it still wins when a directory contains both files; existing projects are unaffected.
  • Add ConfigFile.FileName() and make SaveTo use it, so devbox add/rm/etc. write back to devbox.jsonc instead of creating a stray devbox.json. Configs with no on-disk path (e.g. loaded from a URL by devbox global pull) still fall back to devbox.json.

Scope / known follow-ups

This PR covers config discovery and save round-tripping. A few code paths still assume devbox.json and are left for a follow-up so this change stays small:

  • devbox run --all-projects / devbox update --all-projects (internal/boxcli/multi) only match devbox.json when walking subdirectories.
  • Generated Dockerfile / .envrc templates (devbox generate) hardcode devbox.json.
  • devbox global pull "is default config" checks (internal/pullbox) only consider devbox.json.
  • Docs don't yet mention devbox.jsonc.

How was it tested?

go build ./..., go vet ./internal/devconfig/..., and go test ./internal/devconfig/... pass. New unit tests:

  • TestJSONCConfig (internal/devconfig/config_test.go): Open and Find discover devbox.jsonc; devbox.json wins when both exist; SaveTo writes back to devbox.jsonc without creating a devbox.json.
  • TestFileName / TestSaveToPreservesFileName (internal/devconfig/configfile/file_test.go): FileName() maps each path to the right basename and falls back to devbox.json; SaveTo writes the expected filename.

Community Contribution License

All community contributions in this pull request are licensed to the project
maintainers under the terms of the
Apache 2 License.

By creating this pull request, I represent that I have the right to license the
contributions to the project maintainers under the Apache 2 License as stated in
the
Community Contribution License.

devbox.json is parsed as JSONC and supports comments, but editors and
GitHub diffs flag comments in a .json file as syntax errors. Allow users
to name their config devbox.jsonc so standard tooling highlights it
correctly, without any per-repo configuration.

- Add configfile.AltName ("devbox.jsonc") and configfile.ValidNames, and
  search both names during directory discovery (devbox.json wins when
  both are present, preserving existing behavior).
- Make SaveTo preserve the config's original filename via a new
  FileName() helper so `devbox add` and friends write back to
  devbox.jsonc instead of creating a stray devbox.json. Configs without
  an on-disk path (e.g. pulled from a URL) still default to devbox.json.

Closes #2602
Copilot AI lite review requested due to automatic review settings August 16, 2026 14:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@savil
savil marked this pull request as draft September 2, 2026 22:13
mikeland73 and others added 2 commits September 16, 2026 09:31
…vbox.jsonc-aware

Recognizing devbox.jsonc in searchDir alone left several code paths that
still hardcoded devbox.json:

- devconfig.Init created a devbox.json next to an existing devbox.jsonc,
  which would then silently take precedence. It now returns fs.ErrExist
  when any recognized config name is present.
- boxcli/multi and pullbox only matched devbox.json when walking
  directories; they now accept any ValidNames entry.
- The generated dev Dockerfile always did COPY devbox.json; it now copies
  the project's actual config filename.
- The generated .envrc now watch_file's both devbox.json and devbox.jsonc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikeland73
mikeland73 marked this pull request as ready for review September 16, 2026 16:36
@mikeland73 mikeland73 changed the title feat(config): recognize devbox.jsonc as a config filename (#2602) feat(config): recognize devbox.jsonc as an alternate config filename Sep 16, 2026
@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude please review this PR.

@claude

claude Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 3m 12s —— View job


Claude finished reviewing this PR

  • Gather context (read diff, changed files, CLAUDE.md)
  • Understand the request (code review of PR feat(config): recognize devbox.jsonc as an alternate config filename #2950)
  • Review internal/devconfig changes (config.go, configfile/file.go, init.go)
  • Review internal/boxcli changes (init.go, multi/multi.go)
  • Review internal/devbox/generate changes (Dockerfile/.envrc templates, devcontainer_util)
  • Review internal/pullbox/files.go changes
  • Review test coverage
  • Post final review feedback

Summary

This is a clean, well-scoped implementation. configfile.ValidNames/AltName plus searchDir trying both names, FileName()/SaveTo round-tripping to the original filename, and the Init guard against a shadowing devbox.json are all correct and match the documented "devbox.json wins when both exist" precedence. The latest commit (2201bf8) also closes out the "known follow-ups" from the PR description (multi, pullbox, generated Dockerfile/.envrc), so the description is slightly stale but the code itself is more complete than it states. Test coverage (TestJSONCConfig, TestFileName, TestSaveToPreservesFileName, TestInitRefusesWhenJSONCExists, TestCreateDockerfileDevCopiesConfigFileName) is thorough and exercises the right edge cases.

Finding

internal/boxcli/multi/multi.go (line 25) — --all-projects can open the same project twice if both devbox.json and devbox.jsonc exist in a directory.

if !dirEntry.IsDir() && slices.Contains(configfile.ValidNames, filepath.Base(path)) {
    ...
    box, err := devbox.Open(&optsCopy)

filepath.WalkDir visits every file, so a directory containing both config names matches this condition twice, and devbox.Open succeeds both times (each optsCopy.Dir points at a specific existing file). That means devbox run --all-projects / devbox update --all-projects would run the script twice for that one project. This contradicts the precedence documented in configfile.ValidNames ("devbox.json is listed first so it still wins when a directory contains both files") and enforced in config.searchDir/devconfig.Init — multi.Open just checks filename membership per-file instead of resolving one config per directory.

It's an edge case (requires both files to coexist, which Init now actively prevents for newly-created projects), but it's reachable for anyone who hand-creates a second config file, and there's no test covering multi.Open's behavior here. Consider tracking directories already added to boxes (e.g. by filepath.Dir(path)) and skipping duplicates, or reusing devconfig.searchDir's precedence logic instead of a raw slices.Contains check.

Fix this →

Minor / non-blocking

  • internal/devbox/generate/devcontainer_util.go:88 still hardcodes "devbox.json" in the prod-Dockerfile validation error message. Not incorrect, just slightly stale phrasing now that devbox.jsonc is supported — low priority, and docs are already called out as a known follow-up in the PR description.
  • Several CLI help strings elsewhere (run.go, services.go, shell.go, etc.) still say "devbox.json" only — out of scope per the PR's stated follow-ups, just flagging for completeness.

Note on verification

I wasn't able to run go build/go vet/go test in this sandboxed environment (shell commands beyond basic git required approval that wasn't available), so I reviewed the diff statically rather than confirming the build/test results described in the PR body. The logic and existing test additions read as correct.

Copy link
Copy Markdown
Collaborator Author

Review

Reviewed at 2201bf8 (my original change + the follow-up hardening commit). LGTM — the approach is sound and the follow-up closes the loose ends I'd originally left out of scope.

Verified locally (merged head, on top of current main):

  • go build ./..., go vet, and gofmt -l on all touched files are clean.
  • go test ./internal/devconfig/... ./internal/devbox/generate/... ./internal/boxcli/multi/... ./internal/pullbox/... passes. The only failures are TestFindError/Permissions and /ExactFilePermissions, which fail identically on main when the suite runs as root (uid 0 bypasses 0o000); GitHub's non-root runners are unaffected. Not introduced here.

Correctness of the core change:

  • Discovery is centralized in searchDir, now iterating configfile.ValidNames with devbox.json first, so a directory containing both files still resolves to devbox.json — existing projects are unaffected (DefaultNameWinsWhenBothExist covers this).
  • SaveTo now writes back via FileName() (basename of AbsRootPath, falling back to DefaultName when unset), so devbox add round-trips to devbox.jsonc instead of dropping a stray devbox.json. URL-loaded configs (AbsRootPath == "", e.g. pullbox) still default correctly.

Follow-up commit — completeness check. I grepped the tree for other spots that hardcoded the config name; the commit caught all the functional ones:

  • devconfig.Init now refuses when any recognized name exists, preventing a new devbox.json from silently shadowing an existing devbox.jsonc (TestInitRefusesWhenJSONCExists). The returned &fs.PathError{Err: fs.ErrExist} still satisfies the caller's errors.Is(err, os.ErrExist) check in boxcli/init.go. ✅
  • boxcli/multi and pullbox/files.go use slices.Contains(configfile.ValidNames, …) — faithful generalizations of the prior == DefaultName checks. ✅
  • Generated dev Dockerfile copies {{.ConfigFileName}} (defaulting to devbox.json via cmp.Or); the prod Dockerfile uses COPY . ., so it already includes devbox.jsonc — correctly left untouched. ✅
  • Generated .envrc now watch_files both names. ✅

Nits (non-blocking):

  • generate/devcontainer_util.go:88 still hardcodes "devbox.json", but it's only inside the "prod Dockerfile needs a 'start' script" error message — cosmetic, not a path.
  • Optional: a testscripts/ end-to-end case (rename to devbox.jsonc, then devbox add + devbox shell) would lock in the round-trip at the CLI layer, but the unit coverage across Open/Find/SaveTo/Init/Dockerfile is already solid.

Nothing here blocks merge.


Generated by Claude Code

Comment thread internal/boxcli/multi/multi.go Outdated
Comment on lines 23 to 27
}

if !dirEntry.IsDir() && filepath.Base(path) == configfile.DefaultName {
if !dirEntry.IsDir() && slices.Contains(configfile.ValidNames, filepath.Base(path)) {
optsCopy := *opts
optsCopy.Dir = path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: matching both config names in the multi-project walk can create duplicate project entries.

filepath.Base(path) == configfile.DefaultName was changed to slices.Contains(configfile.ValidNames, filepath.Base(path)). Since WalkDir visits every file independently, a directory containing both devbox.json and devbox.jsonc will now match twice, producing two *devbox.Devbox entries for the same project directory (there's no dedup by directory in this loop or in boxes = append(boxes, box) below).

This is inconsistent with the precedence this PR establishes elsewhere — devconfig.searchDir (and the new DefaultNameWinsWhenBothExist test) treat such a directory as a single project where devbox.json wins, and devconfig.Init refuses to create a second config file for exactly this reason.

Downstream consumers that call multi.Open (e.g. internal/boxcli/run.go's script loop, internal/boxcli/update.go) would then run the script/update twice for the same project directory.

Suggest deduping by directory in this walk (e.g. track directories already matched, or search each directory once using configfile.ValidNames order) so devbox.json wins here too, matching searchDir's behavior.

}
if !dirEntry.IsDir() && slices.Contains(configfile.ValidNames, filepath.Base(path)) {
optsCopy := *opts
optsCopy.Dir = path

…sonc exist

multi.Open walks the tree and opened a project for every file whose
basename is a recognized config name. A directory containing both
devbox.json and devbox.jsonc therefore matched twice, so
`devbox run/update --all-projects` would run against that project twice —
inconsistent with the "devbox.json wins" precedence enforced in
config.searchDir and devconfig.Init.

Deduplicate on the containing directory and open by directory (not by the
specific file) so devconfig applies its filename precedence. Adds a
regression test covering a directory that holds both config names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01554vf5pDDVmFCcAadatBea

Copy link
Copy Markdown
Collaborator Author

Good catch on the multi.Open finding — it's a real regression from the ValidNames change and I've fixed it in 4865407.

filepath.WalkDir visited both files, so a directory containing both devbox.json and devbox.jsonc was opened twice, and devbox run/update --all-projects would run against it twice — inconsistent with the "devbox.json wins" precedence used everywhere else.

Fix:

  • Deduplicate on the containing directory (a seenDirs set), so each project is opened once.
  • Open by directory (optsCopy.Dir = filepath.Dir(path)) instead of by the specific file, so devconfig.searchDir applies the documented filename precedence rather than multi picking whichever file WalkDir happened to hit first.
  • Added internal/boxcli/multi/multi_test.go::TestOpenDeduplicatesConfigsInSameDir, which sets up one project with both config names and one with only devbox.json and asserts exactly two projects are opened (fails at 3 without the fix).

Verified locally on the updated head: go build ./..., go vet, and gofmt -l are clean, and go test ./internal/boxcli/... ./internal/devconfig/... ./internal/devbox/generate/... passes (the only failures are the pre-existing TestFindError permission subtests that also fail on main under root).

On the nits: the devcontainer_util.go:88 string and the other CLI help strings are cosmetic wording, left as the PR's documented follow-up.


Generated by Claude Code

The devbox.jsonc-awareness commit changed two user-visible outputs but
left their testscript golden files stale, failing CI:

- The generated .envrc now watch_file's devbox.jsonc as well, so every
  direnv testscript that compares the rendered .envrc needed the extra
  path in its expected output.
- `devbox init` on an already-configured directory now warns "A devbox
  config already exists in ..." (it guards against any recognized config
  name, not just devbox.json), so init/empty.test's stderr assertion was
  updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01554vf5pDDVmFCcAadatBea
@mikeland73
mikeland73 merged commit 9704c95 into main Sep 16, 2026
28 checks passed
@mikeland73
mikeland73 deleted the claude/focused-goldberg-4lsa9z branch September 16, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Support devbox.jsonc for better editor & diff highlighting

3 participants