VisCommandsvis sync

vis sync

Synchronise derived workspace artefacts like CODEOWNERS

vis sync

Generates workspace-wide artefacts that are derived from per-project configuration.

Usage

vis sync <kind> [options]

Kinds

codeowners

Aggregates owners entries from every project's project.json into a single CODEOWNERS file.

vis sync codeowners                           # Write to <workspace>/CODEOWNERS
vis sync codeowners --out=.github/CODEOWNERS  # Custom output path
vis sync codeowners --check                   # Verify file is up to date (CI guard)
vis sync codeowners --write-guard             # Also emit Write Guard CI (GitHub + GitLab)
vis sync codeowners --write-guard --check     # CI: fail if Write Guard files drift

project.json owners

Each project declares its code owners:

{
    "$schema": "https://unpkg.com/@visulima/vis/schemas/project.schema.json",
    "owners": [
        { "path": "src/**", "owners": ["@myorg/core-team"] },
        { "path": "docs/**", "owners": ["@myorg/docs-team"], "channel": "#docs-reviews" }
    ]
}

vis.config.ts codeowners block

export default defineConfig({
    codeowners: {
        orderBy: "project-id", // or "file-source" (default)
        provider: "github", // "github" | "gitlab" | "bitbucket" | "other"
        globalPaths: {
            "/.github/**": ["@myorg/platform"],
            "/pnpm-workspace.yaml": ["@myorg/infra"],
        },
    },
});

Write Guard (--write-guard)

--write-guard is opt-in. On top of generating CODEOWNERS, it emits CI for projects flagged restricted: true in their project.json. Two artefacts are written so the guard works on both forges, scoped to the restricted project roots only — unrelated changes are never blocked.

The two forges enforce at different strengths — this is deliberate. The GitHub workflow is a hard gate: it fails the PR check when a restricted path changed without code-owner approval. The GitLab job is a soft guard: GitLab CI cannot portably gate a merge on code-owner approval from a job, so it only verifies CODEOWNERS freshness. Real enforcement on GitLab requires enabling the native protected-branch "Require approval from Code Owners" setting (GitLab Premium / Ultimate) — the generated .gitlab file says this loudly in both a header comment and its own job log so it can't be mistaken for a blocking check.

ForgeOutput pathStrengthMechanism
GitHub.github/workflows/write-guard.ymlHard gatepull_request workflow gated on restricted paths, delegates to the geritol/write-guard action (fails the check without approval)
GitLab.gitlab/write-guard.gitlab-ci.ymlSoft guardIncludable MR-gated job; verifies CODEOWNERS is in sync + loudly flags the change. Needs the native CODEOWNERS-approval setting for a real gate

Opt a project in via its project.json:

{
    "$schema": "https://unpkg.com/@visulima/vis/schemas/project.schema.json",
    "restricted": true,
    "owners": [{ "path": "src/**", "owners": ["@myorg/security"] }]
}

include the GitLab file from your root .gitlab-ci.yml and enable the native protected-branch "Require approval from Code Owners" setting — the CI job alone is advisory until you do. With --write-guard --check, CI exits non-zero if either generated file drifts from what the current restricted set would produce — a clean CODEOWNERS does not reset that failure. When no project is flagged restricted: true, the flag no-ops with an informational message.

Upstream references: the GitHub workflow delegates to the geritol/write-guard action; both forges build on native CODEOWNERS approval — GitHub CODEOWNERS and GitLab Code Owners.

flowchart TD
    A["vis sync codeowners --write-guard"] --> B["Scan every project.json"]
    B --> C{"restricted: true?"}
    C -- none --> Z["Info: nothing restricted, skip"]
    C -- "1+ projects" --> D["Resolve + dedupe project-root globs"]
    D --> E["Render GitHub workflow"]
    D --> F["Render GitLab CI job"]
    E --> G{"--check?"}
    F --> G
    G -- no --> H["Write .github + .gitlab artefacts"]
    G -- yes --> I{"On-disk content == rendered?"}
    I -- yes --> J["Info: Write Guard up to date"]
    I -- no --> K["Error + exitCode=1\n(not reset by clean CODEOWNERS)"]

package-json-fields

Mirrors a small set of metadata fields from the root package.json to every workspace package, so each package keeps a consistent license, author, bugs, homepage, engines, and repository. repository.directory is preserved per package — only type and url are copied from root.

vis sync package-json-fields                            # Mirror defaults from root → every package
vis sync package-json-fields --check                    # CI: exit 1 if any package is out of sync
vis sync package-json-fields --fields license,engines   # Override the field list for this run
vis sync package-json-fields --ignore-package-name '@scope/internal-*'
vis sync package-json-fields --format=json              # Machine-readable diff
vis sync package-json-fields --quiet                    # Only print the summary line

Default fields

author, bugs, homepage, license, repository, engines.

For repository, root's type and url overwrite the package's, but the package's directory (its subpath inside the monorepo) is kept. For every other field the root value is copied verbatim.

Fields missing from root are skipped — sync never deletes from a package. Fields already deep-equal to root are skipped — no mtime churn.

Options

OptionDefaultDescription
--out<workspace>/CODEOWNERSOutput file path (codeowners only)
--checkfalseExit non-zero if drift is found (no writes)
--fromproject-jsonInput sources for codeowners (comma-separated/repeatable): project-json, nested-codeowners, package-json-maintainers
--nested-includes**/CODEOWNERSGlob (repeatable) used to discover nested CODEOWNERS files (codeowners only)
--regeneration-commandHeader instruction shown to reviewers, replacing the default note (codeowners only)
--preserve-blockfalseSplice the generated block between markers in an existing file instead of overwriting (codeowners only)
--write-guardfalseAlso emit GitHub + GitLab Write Guard CI for restricted: true projects (codeowners only)
--fieldsComma-separated field list to mirror (package-json-fields only). Repeatable.
--ignore-package-nameGlob of package names to skip (package-json-fields only). Repeatable.
--formathumanOutput format for package-json-fields: human or json.
--quietfalseSuppress per-package log lines; print only the summary (package-json-fields only).
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues