Skip to content

Repository files navigation

devtools

Shared development tools and repository standards for Ankhorage TypeScript projects.

What it owns

@ankhorage/devtools is the single source of truth for these separate concerns:

src/
├── cli/
├── policy/
└── tools/
    ├── agents/
    ├── skills/
    ├── eslint/
    ├── prettier/
    ├── knip/
    ├── package/
    ├── workflows/
    └── vscode/
  • policy: shared repository runtime policy, including the canonical Bun version
  • changesets: package-resolved Changesets execution and release command policy
  • agents: canonical repository AGENTS.md rendered from stable package identity
  • skills: immutable Ankhorage-owned repository skills under .agents/skills/
  • eslint: shared flat ESLint configuration, automatic project profiles, and the bundled ESLint runner
  • prettier: shared Prettier configuration and the bundled Prettier runner
  • knip: shared Knip configuration helpers and the bundled Knip runner
  • package: merge-aware synchronization of the shared package.json tooling and Bun runtime contract
  • workflows: canonical .github/workflows/ci.yml and release.yml
  • vscode: canonical .vscode/settings.json and extensions.json

The package owns the supported Changesets CLI, ESLint, TypeScript ESLint, Prettier, Knip, security, React, React Hooks, React Native, import/sort, unused-import, and formatting-plugin versions used by consuming repositories. It also owns the Bun runtime version used by Ankhorage repository metadata and managed workflows.

Bootstrap

For a repository that does not yet depend on the shared toolchain:

bun add -D @ankhorage/devtools
bunx @ankhorage/ankh devtools sync .

After the first install, the normal workflow is:

ankh devtools sync

The target path is optional and defaults to the current working directory.

Synchronization ensures @ankhorage/devtools is declared using the version of the provider performing the sync, installs the standard package scripts, applies the managed Bun runtime policy, and removes direct devDependencies for tools/plugins owned by devtools. When package metadata changes, sync runs bun install so installed dependencies and bun.lock match the synchronized manifest. Unrelated package metadata, dependencies, and scripts are preserved.

devtools sync does not upgrade the globally installed Bun executable. The managed version applies to repository metadata, Bun types, and GitHub workflows.

Ankh provider

The package is discovered under the devtools category and exposes these capabilities:

  • devtools.lint
  • devtools.changeset
  • devtools.format
  • devtools.knip
  • devtools.sync
  • devtools.status
  • devtools.agents.sync
  • devtools.agents.status
  • devtools.skills.sync
  • devtools.skills.status
  • devtools.eslint.sync
  • devtools.eslint.status
  • devtools.prettier.sync
  • devtools.prettier.status
  • devtools.knip.sync
  • devtools.knip.status
  • devtools.package.sync
  • devtools.package.status
  • devtools.workflows.sync
  • devtools.workflows.status
  • devtools.vscode.sync
  • devtools.vscode.status

The canonical command prefix is always:

ankh devtools ...

Tool commands

ankh devtools changeset -- status --since=origin/main
ankh devtools lint -- --max-warnings=0 .
ankh devtools format -- --check .
ankh devtools knip -- --production

These delegate to the same bundled tools as the package binaries:

  • ankh devtools changesetankhorage-changeset
  • ankh devtools lintankhorage-eslint
  • ankh devtools formatankhorage-prettier
  • ankh devtools knipankhorage-knip

The synchronized package scripts are:

{
  "scripts": {
    "changeset": "ankhorage-changeset",
    "changeset:status": "ankhorage-changeset status --since=origin/main",
    "lint": "ankhorage-eslint . --max-warnings=0",
    "lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
    "format": "ankhorage-prettier --write .",
    "format:check": "ankhorage-prettier --check .",
    "knip:check": "ankhorage-knip",
    "version-packages": "ankhorage-changeset version"
  }
}

Repository synchronization

Synchronize or inspect every managed concern:

ankh devtools sync .
ankh devtools status .

Synchronize one concern:

ankh devtools agents sync .
ankh devtools skills sync .
ankh devtools eslint sync .
ankh devtools prettier sync .
ankh devtools knip sync .
ankh devtools package sync .
ankh devtools workflows sync .
ankh devtools vscode sync .

Report one concern:

ankh devtools agents status .
ankh devtools skills status .
ankh devtools eslint status .
ankh devtools prettier status .
ankh devtools knip status .
ankh devtools package status .
ankh devtools workflows status .
ankh devtools vscode status .

Preview synchronization without writing:

ankh devtools sync . --dry-run
ankh devtools agents sync . --dry-run
ankh devtools skills sync . --dry-run
ankh devtools eslint sync . --dry-run
ankh devtools package sync . --dry-run

A dry run reports would create, would update, and would remove actions without mutating files. status exits with code 1 when managed state has drifted and 0 when it is current.

Synchronization guarantees

Synchronization is deterministic and idempotent:

  • missing managed artifacts are created
  • outdated centrally owned artifacts are updated
  • stale files in Devtools-owned skill trees are removed
  • the managed Bun runtime version is applied consistently to package metadata and workflows
  • Changesets-enabled repositories use the Devtools-owned runner without a direct @changesets/cli declaration
  • package changes are followed by bun install after all managed files have been written, keeping installed dependencies and bun.lock synchronized without invalidating the running sync
  • current artifacts are left untouched
  • unrelated files and package fields are preserved
  • repeated sync produces only unchanged results
  • invalid target paths and write failures return a non-zero exit code
  • create-only repository extension files are never overwritten after creation

The canonical workflow, VS Code, and skill files are packaged with @ankhorage/devtools; synchronization does not fetch mutable files from GitHub at runtime.

Managed agent instructions

ankh devtools agents sync owns the repository-root AGENTS.md. The shared instructions are intentionally small and stable. The target repository's package name and description are rendered from package.json; the remaining content defines the unconditional current-architecture policy, directs structural work to the managed project-structure skill, and requires the canonical build, type, lint, Knip, Changeset, and formatting commands before pull request creation.

Only the current Ankhorage architecture is supported. Managed instructions reject deprecated APIs, compatibility aliases, shims, dual old/new paths, historical-state fallbacks, and migrations whose sole purpose is obsolete state. A canonical cross-package change requires affected repositories to update to the latest released public API.

Managed repository skills

ankh devtools skills sync owns the complete .agents/skills/ankhorage-coding-rules/, .agents/skills/hexagonal-architecture/, and .agents/skills/ankhorage-project-structure/ trees from the immutable copies shipped in the Devtools release. It creates .agents/ when missing, replaces stale files in those managed skills, and preserves every unrelated skill directory. The project-structure skill requires both the coding-rules and hexagonal-architecture skills before structural work can continue.

.agents/.devtools-manifest.json records the source Devtools version and SHA-256 hashes for every managed skill file. That ownership record allows status and dry-run to report drift and lets later releases remove stale owned files without deleting repository-owned skills.

Agent Skill scripts are always TypeScript files with a .ts extension and run with Bun. JavaScript skill scripts using .js, .mjs, or .cjs are not supported.

ESLint profiles

createConfig() defaults to profile: 'auto'.

Automatic detection reads the consuming repository's package.json and delegates project trait detection to @ankhorage/utility/project. Dependency signals are considered across dependencies, devDependencies, and peerDependencies.

Profile precedence is:

React Native / Expo
        ↓
react-native
        ↓ includes
react
        ↓ includes
base

A React or Next.js project selects react. A React Native or Expo project selects react-native. Everything else selects base.

An unusual repository can opt out of automatic selection:

import { createConfig } from '@ankhorage/devtools/eslint';

export default createConfig({
  files: ['src/**/*.ts'],
  profile: 'base',
  project: ['./tsconfig.json'],
  tsconfigRootDir: import.meta.dirname,
});

The base profile enforces the shared TypeScript policy plus:

  • maximum 50 effective lines per function
  • maximum 300 effective lines per file
  • modified cyclomatic complexity maximum 15
  • security review for dynamic object access
  • rejection of non-literal require() calls

The React profile adds React and React Hooks correctness rules. The React Native profile composes the React profile and adds the selected React Native style rules.

Managed ESLint setup and local overrides

ankh devtools eslint sync centrally owns eslint.config.mjs and creates eslint.local.config.mjs once. When the repository has a root examples/ directory, synchronization also owns eslint.examples.config.mjs; repositories without public examples do not receive that file, and synchronization removes the managed wrapper when the directory is removed.

The canonical wrapper uses automatic profile detection and appends repository-owned flat-config entries:

import { createConfig } from '@ankhorage/devtools/eslint';
import localConfig from './eslint.local.config.mjs';

const localEntries = Array.isArray(localConfig) ? localConfig : [localConfig];

export default [
  ...createConfig({
    files: ['src/**/*.{ts,tsx}'],
    project: ['./tsconfig.json'],
    tsconfigRootDir: import.meta.dirname,
  }),
  ...localEntries,
];

Use eslint.local.config.mjs for narrow repository-specific flat-config overrides, including temporary file-specific migration overrides. On first synchronization, an existing non-canonical eslint.config.mjs is preserved as the initial local config before the canonical wrapper is installed. Synchronization never overwrites that local file afterward.

Each public example lives in a named directory, such as examples/basic-usage/*.ts; example source files do not live directly under examples/. The examples wrapper uses root tsconfig.eslint.json and tsconfig.json when present and discovers TypeScript projects below examples/. This covers example directories included only by the root ESLint project as well as standalone applications with their own tsconfig. It applies the same shared policy and appends the same repository-owned local entries.

An existing consumer-owned eslint.examples.config.mjs requires explicit adoption before synchronization can replace it. Status, dry-run, and sync report an actionable error instead of overwriting or deleting it. Move its repository-specific overrides into eslint.local.config.mjs, preserving existing entries and the examples-only file scope; retain custom parser options there when needed. Remove the old examples config only after that transfer, then rerun sync and the examples lint. The generated wrapper carries a Devtools ownership marker and subsequent synchronization updates it normally. Do not mark an old consumer config as managed to bypass this transfer.

Repositories can lint their independently runnable examples explicitly:

ankhorage-eslint examples --config eslint.examples.config.mjs --max-warnings=0

Prettier

ankh devtools prettier sync owns .prettierrc.js, emits the correct ESM or CommonJS wrapper based on the repository's package.json module type, and creates prettier.local.config.js once for narrow repository-specific options.

The consumer delegates formatting policy to:

@ankhorage/devtools/prettier

The wrapper merges shared and local overrides in that order. On first synchronization, an existing non-canonical .prettierrc.js is preserved as prettier.local.config.js; former shared-only Devtools delegates become an empty local config. Later synchronization never overwrites the local file.

Knip

ankh devtools knip sync bootstraps knip.config.ts with:

import { createKnipConfig } from '@ankhorage/devtools/knip';

export default createKnipConfig();

knip.config.ts is create-only after bootstrap so repositories can retain narrow local entries, projects, ignores, binaries, dependencies, or switch to createKnipMonorepoConfig() without synchronization overwriting those extensions.

Managed Bun runtime policy

The canonical Bun policy is defined once in devtools and consumed by both package and workflow synchronization. The current policy is:

Bun runtime       1.4.2
packageManager    bun@1.4.2
@types/bun        ^1.4.1

Renovate owns the single BUN_VERSION literal in src/policy/bunRuntimePolicy.ts. Its trusted base-branch workflow invokes bun scripts/sync-renovate-owner.ts sync repository to regenerate packageManager, @types/bun, the Bun workflow setup versions, this documentation block, and bun.lock, then runs bun scripts/sync-renovate-owner.ts status repository to reject stale generated artifacts. Do not synchronize those values manually in a Renovate branch.

Managed package contract

ankh devtools package sync merge-updates package.json rather than replacing it.

It owns:

  • the @ankhorage/devtools dependency version range
  • removal of direct consumer @changesets/cli dependencies
  • packageManager according to the managed Bun runtime policy
  • the @types/bun development dependency according to the managed Bun runtime policy
  • lint
  • lint:fix
  • format
  • format:check
  • knip:check
  • changeset, changeset:status, and version-packages for Changesets-enabled repositories

For normal consumers, @ankhorage/devtools is a devDependency. @ankhorage/ankh keeps devtools as a runtime dependency because it loads the provider. Devtools itself participates in the Bun runtime policy without attempting to install itself as a consumer dependency.

When this managed package contract changes, synchronization runs bun install. This updates installed dependencies and bun.lock before sync completes. It also removes direct dependencies for Changesets and direct devDependencies for tools and ESLint plugins already provided by @ankhorage/devtools. Unrelated scripts, dependencies, metadata, and repository-specific configuration remain unchanged.

A repository participates in Changesets synchronization when .changeset/config.json exists or any of the canonical changeset, changeset:status, or version-packages script keys is present. This explicit rule migrates partially configured repositories while ensuring repositories without Changesets do not acquire release tooling. The repository continues to own .changeset/config.json, pending .changeset/*.md files, and its package release semantics. Direct @changesets/cli declarations, ambient changeset scripts, and bunx changeset workflow commands are not supported consumer forms.

Managed GitHub Actions workflows

workflows owns exactly:

.github/workflows/ci.yml
.github/workflows/renovate.yml
.github/workflows/release.yml

CI and Release render their bun-version from the same managed Bun runtime policy used for package.json. They also render Changesets status, version, and publish commands from the same policy that owns the synchronized package scripts. The CI workflow installs that Bun version with the frozen lockfile, builds before repository-provider validation, runs bunx @ankhorage/ankh doctor validate ., and conditionally runs lint, formatting, Knip, tests, typecheck, and the strict changeset:status --since=origin/main guard for pull requests. After a green change reaches main, Release uses the scoped Ankhorage Renovate Sync App token to apply Changesets versioning directly to main in a chore(release) [skip ci] commit and publishes without creating a second Version Packages pull request. Release publishing calls the Devtools-owned runner through the synchronized package script; it never resolves an ambient or mutable Changesets executable. Changesets v3 creates the local release tags; the managed workflow pushes those exact tags and creates any missing GitHub Releases directly, without parsing Changesets’ human-readable publish output.

The Renovate workflow accepts only same-repository branches created by renovate[bot]. It calls the SHA-pinned ankhorage/renovate workflow to add a release Changeset for runtime Ankhorage dependency updates or an empty Changeset for dev-only updates. The trusted workflow scopes the Ankhorage Renovate Sync GitHub App token to the current repository and uses it only for the validated commit, allowing every normal pull-request CI workflow to start without manual approval. It never checks out or executes pull-request code in the privileged pull_request_target context.

Managed VS Code configuration

vscode owns exactly:

.vscode/settings.json
.vscode/extensions.json

Unknown workflow and VS Code files are never deleted.

Adding another managed concern

A new concern should:

  1. live in its own sibling directory under src/tools
  2. define only the files and behavior it owns
  3. expose deterministic status and synchronization
  4. add provider commands under ankh devtools
  5. include dry-run, status, and idempotence coverage
  6. document its central ownership and repository-owned extension points

Releases

Packages

Contributors

Languages