vfox is a cross-platform SDK version manager with Lua plugins and Global, Project, and Session scopes. Use the Go version declared in go.mod.
This is the single project-wide guide. Keep behavior descriptions aligned with the implementation; use file paths and symbol names instead of fixed command counts, test counts, or source line numbers.
Choose verification for the affected behavior. Run from the repository root:
# Build
go build .
# Focused tests; choose the affected package
go test ./internal/sdk -v
# Full suite for changes that affect multiple packages
go test ./...
# Coverage when needed
go test ./... -coverprofile=coverage.out -covermode=atomic- For behavior changes, add or update tests that exercise the relevant behavior. Reproduce bugs with a failing regression test before fixing them when practical.
- Documentation and formatting changes do not require new tests. Check their diff and any affected examples or references.
- Do not remove or weaken assertions merely to make a failure pass. Update or remove obsolete tests when an intentional behavior change justifies it.
- Format changed Go files with
gofmt -w <files>. Usego fmt ./...when formatting the whole module is intended; preserve unrelated working-tree changes. - Use
go mod tidyfor dependency maintenance andgo get <module>@<version>for intentional dependency changes. These commands can modifygo.modandgo.sum. - Consult .github/workflows/ci.yml for CI checks and
go.modfor the required Go version.
- scripts/e2e-test.sh and scripts/e2e-test.ps1 install SDKs, change vfox state, and clean installation or user directories. Run them only in a disposable environment such as a CI runner or VM. Setting
VFOX_HOMEalone does not isolate the Unix script from the real user's vfox directory; Windows global use also writes user environment settings to the registry. - scripts/e2e-msix-test.ps1 also requires a disposable Windows runner or VM. It creates a temporary trusted certificate, installs and upgrades MSIX bundles, and verifies global SDK selections through the host registry. It restores the fixture's registry values and removes its packages and certificate on exit.
./scripts/bump.sh <version>changes the runtime version, stages it, commits, and creates a local tag. Use it for requested versioning work with the staging state reviewed; it does not push.goreleaser releaseruns the release workflow, including a GitHub draft release and configured distribution updates. Use it for requested release work. See .goreleaser.yaml and .github/workflows/go-releaser.yml.
- Commands obtain SDKs through
Manager.LookupSdkorLookupSdkWithInstall, then call SDK methods such asInstall,UseWithConfig, andUninstall. Managerlives in theinternalpackage. It owns SDK lookup and lifetime, plugin discovery/add/update/remove, and orchestration. Plugin loading and validation belong to that management work.- The SDK layer implements runtime installation, version resolution, scope links, and lifecycle hook calls. Keep these implementations out of Manager and commands.
- The plugin layer loads Lua code and implements hook invocation. Commands and Manager delegate SDK lifecycle hooks through SDK methods.
- Check
NewSdkManagererrors before deferringmanager.Close(). Manager owns cached SDK lifetimes, SDK owns its plugin, and callers creating temporary plugins own their cleanup.
Allowed project-internal dependencies are listed below. shared includes the internal/shared package and its subpackages.
| Package or subtree | Allowed internal dependencies |
|---|---|
cmd/ |
internal Manager, sdk, shell, env, pathmeta, config, shared |
internal Manager |
sdk, plugin, env, pathmeta, config, shared |
internal/sdk |
plugin, shell, env, pathmeta, shared |
internal/plugin/ |
Packages within plugin/, env, config, shared |
internal/shell |
env, shared |
internal/env |
pathmeta, config, shared |
internal/pathmeta, internal/config |
shared |
internal/shared/ |
Other packages within shared/ |
Keep imports acyclic. Shared utilities must not depend on SDK, plugin, environment, configuration, or orchestration packages. Resolve dependency questions within the task's scope; keep unrelated refactoring separate.
- Use the existing Apache 2.0 header style for new Go source files, without requiring a fixed line count. Preserve valid build-constraint placement.
- Group imports as standard library, third-party packages, then project packages in code being changed.
- Return errors from library code; do not introduce
log.Fatalthere. Preserve error chains withfmt.Errorf("context: %w", err). - Handle errors or explain intentional best-effort behavior, such as optional clipboard operations or cleanup.
- Keep
unsafelimited to necessary platform API interop, such as Windows elevation and environment-change broadcasts.
- Register command definitions in cmd/cmd.go. Helpers and platform-specific files may share a command implementation.
- Use
CategorySDKorCategoryPluginfor commands belonging to those groups. Utility commands may be uncategorized. - Keep
activatehidden unless CLI visibility changes are part of the task. Users still invokevfox activate <shell>during shell setup. - Activation and environment output is evaluated by the shell; keep unrelated output out of generated shell code.
- SDK argument parsing is command-specific. Check the relevant parser before changing version prefixes,
@latest, orexecarguments and its--separator.
- Installation calls
PreInstall, prepares the main runtime and additions, then calls optionalPostInstall. Preserve cleanup of partial directories created by a failed attempt. Uninstallation and its optionalPreUninstallhook belong to SDK. - Installation keeps the final payload path stable for plugin-generated paths.
Installuses a sibling.installingmarker untilPostInstallcompletes and a sibling.lockfile for cross-process exclusion. Preserve interrupted-install retry and legacy marker-free payloads; do not unlink the lock file after releasing it. - Version resolution calls optional
PreUsefirst. If no version is returned, use the existing exact-installed and prefix-matching logic.IsNoResultProvided(err)permits fallback; actual hook errors propagate.UseWithConfigchecks that the resolved version is installed before applying scope state. Currentchecks the highest-priority configured version. Activation/export uses the separate installed-version fallback in tool_resolution.go; preserve the caller-specific behavior.EnvKeysForScopepasses scope link paths to the plugin and does not create links. Use the platform helpers inenvto create/remove directory links for the main runtime and additions, preserving links that already point to the correct target.Available(args)caches by arguments in the plugin's.available.cache. PreserveAvailableHookDurationsemantics:0disables caching and-1means no expiry.- Required Lua hooks are
Available,PreInstall, andEnvKeys; optional hooks arePostInstall,PreUse,ParseLegacyFile, andPreUninstall. Hooks receive context tables, such asPLUGIN:PreInstall(ctx)usingctx.version. Use model.go for fields/results and the codec for structured conversion. - Plugin loading prefers
main.lua, with local modules in the plugin directory. Otherwise it loadsmetadata.luaand hook files, withhooks/?.luasearched beforelib/?.lua. Preserve package-path restrictions and plugin-local module support. - Preserve runtime-global initialization after plugin scripts load. Consult module registration for available built-ins, and both
HookFuncMapand invocation code for hook names, case, and filenames.
Tool selections support simple versions and attributes:
[tools]
nodejs = "21.5.1"
java = { version = "21", vendor = "openjdk" }- Within a directory, pathmeta.LoadConfig tries
.vfox.toml, thenvfox.toml, then.tool-versions. Reading.tool-versionsattempts to write a migrated.vfox.tomlwhile retaining the original. - Other legacy files are handled through enabled plugins and their declared
LegacyFilenamesorder. There is no core-wide ordering of.nvmrc,.node-version, and.sdkmanrc. Activation/export uses legacy results only for tools absent from the loaded project configuration. - vfox settings live in
config.yaml; their shared/user merge behavior is implemented by LoadConfigWithFallback and Merge. - Config chains append from lowest to highest priority. Use
GetToolConfig/GetToolVersionfor the winning configuration, orGetToolConfigsByPriorityfor fallback candidates; check the found flag before using a returned scope. - Within vfox-managed paths, priority is Project > Session > Global. Append paths in that order; merge ordinary variables in reverse order so Project wins.
- Final PATH assembly preserves user paths before the first existing vfox path, such as an activated virtualenv: preserved prefix > vfox-managed paths > remaining system paths. See SplitSystemPaths and environment export.
- Preserve state-cache invalidation on configuration, project, and PATH changes. Use named scope constants; their numeric values do not define priority.
- The user root uses an existing
~/.version-foxdirectory, otherwise~/.vfox. It holds user configuration, temporary session state, and global SDK links. - The shared root is
VFOX_HOME, defaulting to the user root. Its default directories areplugin/andcache/, with shared settings inconfig.yaml. storage.sdkPathoverrides the SDK installation root only. Preserve the legacy installation lookup insdk.NewSdkwhen changing storage behavior.- Global links use
<user-root>/sdks; project links use<project>/.vfox/sdks; session links usePathMeta.Working.SessionSdkDir. Use PathMeta and RuntimeEnvContext to resolve them. - SDK directory links use Unix symlinks and Windows junctions. The separate
shared/shimutility is not the SDK directory-link implementation. - Supported release OS/architecture combinations are defined in .goreleaser.yaml. Shell implementations live in internal/shell/.
- Shell integration variables are defined in env/flag.go and pathmeta/path_meta.go; preserve their contracts when changing hooks.
- Keep filesystem, network, clipboard, and process effects explicit in utility contracts; keep SDK lifecycle and scope policy in their owning packages.
- Review synchronization when changing shared state. A mutex in one cache instance does not establish safety between instances or processes using the same file. Logger level changes are not synchronized.
internal/shared/checksum.gobelongs to the existingsharedpackage. File moves and package redesign require a task that calls for that refactoring.
| Area | Entry points |
|---|---|
| CLI registration and commands | cmd/cmd.go, commands/ |
| SDK lifecycle and runtime types | sdk.go, runtime.go |
| Plugin loading and hooks | plugin.go, lua_plugin.go, model.go |
| Manager and registry | manager.go, manager_registry.go |
| Paths and tool configuration | pathmeta/ |
| Environment and scope handling | context.go, env.go, vfox_toml_chain.go |
| Shell initialization and exports | shell/ |
| vfox settings | config/ |
| Shared utilities | shared/ |
| Windows MSIX packaging | packaging/msix/ (AppxManifest.xml, make-msix.ps1, gen-assets.ps1; docs at msix.md; release via compile-msix.yml, e2e via e2e-msix-test.ps1) |