From 9bfd3b24f9378141836eb901a88ea725dc29afc6 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:19:20 +0200 Subject: [PATCH 01/68] Add devtools repository sync capabilities --- package.json | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e2dbb2d..a0d964b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ankhorage/devtools", "version": "1.1.2", - "description": "Shared Lint and Format Configuration for Ankhorage", + "description": "Shared development tools and repository standards for Ankhorage", "license": "MIT", "homepage": "https://github.com/ankhorage/devtools#readme", "bugs": { @@ -21,7 +21,13 @@ "capabilities": [ "devtools.lint", "devtools.format", - "devtools.knip" + "devtools.knip", + "devtools.sync", + "devtools.status", + "devtools.workflows.sync", + "devtools.workflows.status", + "devtools.vscode.sync", + "devtools.vscode.status" ] }, "keywords": [ @@ -29,15 +35,15 @@ "eslint", "prettier", "knip", - "linting", - "formatting", - "static-analysis", + "github-actions", + "vscode", + "repository-sync", "developer-tools" ], "bin": { - "ankhorage-eslint": "./dist/eslint-cli.js", - "ankhorage-knip": "./dist/knip-cli.js", - "ankhorage-prettier": "./dist/prettier-cli.js" + "ankhorage-eslint": "./dist/cli/bin/eslint.js", + "ankhorage-knip": "./dist/cli/bin/knip.js", + "ankhorage-prettier": "./dist/cli/bin/prettier.js" }, "exports": { "./cli": { @@ -45,15 +51,15 @@ "import": "./dist/cli/index.js" }, "./eslint": { - "types": "./dist/eslint.d.ts", - "import": "./dist/eslint.js" + "types": "./dist/tools/eslint/index.d.ts", + "import": "./dist/tools/eslint/index.js" }, "./knip": { - "types": "./dist/knip.d.ts", - "import": "./dist/knip.js" + "types": "./dist/tools/knip/index.d.ts", + "import": "./dist/tools/knip/index.js" }, "./prettier": { - "require": "./dist/prettier.cjs" + "require": "./dist/tools/prettier/index.cjs" } }, "files": [ @@ -63,7 +69,7 @@ "examples" ], "scripts": { - "build": "rm -rf dist tsconfig.tsbuildinfo && tsc && cp src/prettier.cjs dist/prettier.cjs", + "build": "rm -rf dist tsconfig.tsbuildinfo && tsc && mkdir -p dist/tools/prettier dist/tools/workflows dist/tools/vscode && cp src/tools/prettier/index.cjs dist/tools/prettier/index.cjs && cp -R src/tools/workflows/files dist/tools/workflows/files && cp -R src/tools/vscode/files dist/tools/vscode/files", "typecheck": "bun x tsc --noEmit -p tsconfig.test.json", "doctor": "ankhorage-doctor validate .", "knip": "knip", From b2e5ef3191e763ca2d99004e6f243717f1ff2fad Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:19:43 +0200 Subject: [PATCH 02/68] Document devtools synchronization commands --- README.md | 303 +++++++++++++++++++++++++++++------------------------- 1 file changed, 165 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 2a8d4b6..2fee4a9 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,29 @@ # devtools -Shared ESLint, Prettier, and Knip configuration for modern TypeScript projects. +Shared development tools and repository standards for Ankhorage TypeScript projects. -## What you get +## What it owns -- Consistent linting across repos -- Zero-config Prettier setup -- Shared Knip static-analysis defaults -- Bundled tool binaries for ESLint, Prettier, and Knip -- Ankh provider commands for lint, format, and Knip -- Strict TypeScript rules without compromise -- One source of truth for tooling +`@ankhorage/devtools` is the single source of truth for these equal, separate concerns: -## Features +```text +src/ +├── cli/ +└── tools/ + ├── eslint/ + ├── prettier/ + ├── knip/ + ├── workflows/ + └── vscode/ +``` + +- `eslint`: shared flat ESLint configuration and the bundled ESLint runner +- `prettier`: shared Prettier configuration and the bundled Prettier runner +- `knip`: shared Knip configuration and the bundled Knip runner +- `workflows`: canonical `.github/workflows/ci.yml` and `release.yml` +- `vscode`: canonical `.vscode/settings.json` and `extensions.json` -- Flat ESLint config (latest standard) -- Preconfigured plugin ecosystem -- Prettier integration -- Shared Knip config factories -- `ankhorage-eslint`, `ankhorage-prettier`, and `ankhorage-knip` binaries -- `ankh devtools lint`, `ankh devtools format`, and `ankh devtools knip` -- Monorepo-friendly +There is no generic template layer around these concerns. Each tool owns its canonical files and behavior. ## Installation @@ -28,23 +31,43 @@ Shared ESLint, Prettier, and Knip configuration for modern TypeScript projects. bun add -D @ankhorage/devtools ``` -The package owns the ESLint, Prettier, and Knip toolchain. Consuming repos should not install `eslint`, `prettier`, or `knip` directly unless they intentionally need a different version from the shared Ankhorage toolchain. +The package owns the ESLint, Prettier, and Knip versions used by consuming repositories. Do not install those tools directly unless a repository intentionally opts out of the shared Ankhorage toolchain. + +## Ankh provider -It also participates in Ankh package discovery with: +The package is discovered under the `devtools` category and exposes these capabilities: -- category: `devtools` -- capabilities: - - `devtools.lint` - - `devtools.format` - - `devtools.knip` +- `devtools.lint` +- `devtools.format` +- `devtools.knip` +- `devtools.sync` +- `devtools.status` +- `devtools.workflows.sync` +- `devtools.workflows.status` +- `devtools.vscode.sync` +- `devtools.vscode.status` -`@ankhorage/devtools` owns primitive lint/format/knip tooling. Local emulator, app, and workstation workflows belong in `@ankhorage/dev`. +The canonical command prefix is always: + +```bash +ankh devtools ... +``` + +## Tool commands + +```bash +ankh devtools lint -- --max-warnings=0 . +ankh devtools format -- --check . +ankh devtools knip -- --production +``` -## Usage +These commands delegate to the same bundled tools as the package binaries: -### Scripts +- `ankh devtools lint` → `ankhorage-eslint` +- `ankh devtools format` → `ankhorage-prettier` +- `ankh devtools knip` → `ankhorage-knip` -Use the devtools-owned binaries in package scripts: +Recommended package scripts: ```json { @@ -58,165 +81,169 @@ Use the devtools-owned binaries in package scripts: } ``` -### Ankh commands +## Repository synchronization -When discovered by `@ankhorage/ankh`, the package exposes: +### Synchronize all managed files ```bash -ankh devtools lint -- --max-warnings=0 . -ankh devtools format -- --check . -ankh devtools knip -- --production +ankh devtools sync . ``` -These provider-backed commands delegate to the same underlying tools as the standalone binaries: +The target path is optional and defaults to the current working directory: + +```bash +ankh devtools sync +``` -- `ankh devtools lint` -> `ankhorage-eslint` -- `ankh devtools format` -> `ankhorage-prettier` -- `ankh devtools knip` -> `ankhorage-knip` +### Report drift without changing files -### ESLint +```bash +ankh devtools status . +``` -Create an `eslint.config.js` file: +`status` exits with code `1` when any managed file is missing or outdated. It exits with code `0` when all managed files are current. -```js -import { createConfig } from '@ankhorage/devtools/eslint'; +Example output: -export default createConfig({ - files: ['src/**/*.{ts,tsx}'], - project: ['./tsconfig.json'], - tsconfigRootDir: import.meta.dirname, -}); +```text +✓ .github/workflows/ci.yml +✗ .github/workflows/release.yml outdated ++ .vscode/settings.json missing +✓ .vscode/extensions.json ``` -### Prettier +### Synchronize one concern -Create a Prettier config file: +```bash +ankh devtools workflows sync . +ankh devtools vscode sync . +``` -```js -export { default } from '@ankhorage/devtools/prettier'; +### Report one concern + +```bash +ankh devtools workflows status . +ankh devtools vscode status . ``` -CommonJS repos can use: +### Preview synchronization -```js -module.exports = require('@ankhorage/devtools/prettier'); +```bash +ankh devtools sync . --dry-run +ankh devtools workflows sync . --dry-run +ankh devtools vscode sync . --dry-run ``` -### Knip +A dry run reports `would create` and `would update` actions without writing any files. -Create a repo-local `knip.config.ts` file: +## Synchronization guarantees -```ts -import { createKnipConfig } from '@ankhorage/devtools/knip'; +Synchronization is deterministic and idempotent: -export default createKnipConfig(); -``` +- missing managed files are created +- outdated managed files are replaced with the canonical package version +- current managed files are left untouched +- unrelated files are never modified +- unknown files in `.github/workflows` and `.vscode` are never deleted +- repeating `sync` after a successful run produces only `unchanged` results +- invalid target paths and write failures return a non-zero exit code -Repos can add narrow repo-specific patterns when needed: +The canonical files are packaged with `@ankhorage/devtools`; synchronization does not fetch mutable files from GitHub at runtime. -```ts -import { createKnipConfig } from '@ankhorage/devtools/knip'; +## Managed GitHub Actions workflows -export default createKnipConfig({ - entry: ['scripts/release.ts'], - project: ['scripts/**/*.ts'], - ignore: ['fixtures/**'], - ignoreBinaries: ['custom-tool'], - ignoreFiles: ['examples/fixture.ts'], -}); +`workflows` owns exactly: + +```text +.github/workflows/ci.yml +.github/workflows/release.yml ``` -For workspaces-based monorepos, use the monorepo preset: +The CI workflow: -```ts -import { createKnipMonorepoConfig } from '@ankhorage/devtools/knip'; +- checks out full history +- installs the pinned Bun version +- installs dependencies with `bun install --frozen-lockfile` +- runs `bunx @ankhorage/ankh doctor validate .` +- conditionally runs build, lint, format check, Knip, tests, and typecheck when scripts exist +- conditionally runs `changeset:status` for pull requests when the script exists -export default createKnipMonorepoConfig({ - root: { - ignoreFiles: ['.prettierrc.js', 'eslint.config.js'], - }, -}); -``` +The release workflow: -By default, the monorepo preset configures Knip workspaces for the root package, `packages/*`, and `apps/*`. Repos can override those defaults or add extra workspace globs: +- checks out full history +- configures Bun and Node for npm publishing +- installs dependencies with the frozen lockfile +- conditionally builds +- runs the Changesets release PR/publish flow +- protects release execution with a concurrency group +- skips cleanly when no Changesets configuration exists -```ts -import { createKnipMonorepoConfig } from '@ankhorage/devtools/knip'; - -export default createKnipMonorepoConfig({ - workspaceGlobs: ['packages/*', 'apps/*', 'examples/*'], - workspaceDefaults: { - ignoreFiles: ['fixtures/**'], - }, - workspaces: { - '.': { - ignoreFiles: ['.prettierrc.js', 'eslint.config.js'], - }, - 'apps/editor': { - ignoreFiles: ['babel.config.js'], - ignoreDependencies: ['babel-preset-expo'], - }, - }, -}); +## Managed VS Code configuration + +`vscode` owns exactly: + +```text +.vscode/settings.json +.vscode/extensions.json ``` -The shared config intentionally keeps defaults narrow so Knip can still report real unused files, exports, dependencies, and binaries. Prefer explicit `entry`, `project`, `ignoreBinaries`, `ignoreDependencies`, or `ignoreFiles` over broad ignores. +The shared settings use the workspace TypeScript SDK, enable the workspace-SDK prompt, configure intentional ESLint save actions, and enforce basic whitespace/newline consistency. -### CI +The extension recommendations are limited to the standard Ankhorage workflow: -Run the scripts in CI after dependencies are installed: +- Bun +- ESLint +- Prettier +- YAML +- GitHub Actions -```yaml -- name: Run lint - run: bun run lint +`launch.json` is intentionally not managed globally. Libraries, CLIs, Expo packages, services, and integration repositories require different debug configurations. -- name: Run format check - run: bun run format:check +## ESLint -- name: Run Knip - run: bun run knip -``` +Create `eslint.config.mjs`: -For workflows that support optional scripts, use the same guard style as the other devtools checks: +```js +import { createConfig } from '@ankhorage/devtools/eslint'; -```yaml -- name: Run Knip - run: | - if node -e "const p=require('./package.json'); process.exit(p.scripts?.knip ? 0 : 1)"; then - bun run knip - else - echo "No knip script found; skipping." - fi +export default createConfig({ + files: ['src/**/*.{ts,tsx}'], + project: ['./tsconfig.json'], + tsconfigRootDir: import.meta.dirname, +}); ``` -## Use Cases +## Prettier -- Monorepos with shared standards -- Teams that want strict, predictable linting -- Projects avoiding duplicated config -- Repos that need consistent unused-file, unused-export, and dependency checks +ES modules: -## Why this exists +```js +export { default } from '@ankhorage/devtools/prettier'; +``` -Maintaining ESLint, Prettier, and Knip configs across multiple repositories leads to: +CommonJS: -- duplication -- inconsistency -- drift over time +```js +module.exports = require('@ankhorage/devtools/prettier'); +``` + +## Knip -This package centralizes tooling so all projects stay aligned. +```ts +import { createKnipConfig } from '@ankhorage/devtools/knip'; -## Scope +export default createKnipConfig(); +``` -Includes: +Monorepos can use `createKnipMonorepoConfig` and add narrow repository-specific entries, projects, ignores, binaries, dependencies, or workspace overrides. -- ESLint configuration and binary wrapper -- Prettier configuration and binary wrapper -- Knip configuration and binary wrapper -- Ankh provider descriptors and handlers for lint, format, and Knip +## Adding another managed concern -Excludes: +A new concern should: -- runtime code -- build tooling -- local dev workflows +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 through the shared managed-file engine +4. add provider commands under `ankh devtools` +5. include source-tree, built-package, dry-run, status, and idempotence coverage +6. document overwrite and exit-code behavior From 78269ec69cce8890960f93add3a91295b12edafc Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:19:58 +0200 Subject: [PATCH 03/68] Canonicalize CI workflow --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 756a8ac..05eb663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,8 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Run doctor validation - run: bun run doctor + - name: Validate Ankhorage repository + run: bunx @ankhorage/ankh doctor validate . - name: Run build run: | @@ -81,8 +81,8 @@ jobs: - name: Check changesets if: github.event_name == 'pull_request' run: | - if [ -f ./scripts/check-changeset-status.sh ]; then - bash ./scripts/check-changeset-status.sh + if node -e "const p=require('./package.json'); process.exit(p.scripts?.['changeset:status'] ? 0 : 1)"; then + bun run changeset:status else - echo "No changeset status script found; skipping." + echo "No changeset:status script found; skipping." fi From ea207aab5b731248ffb875d7dc62aa1715a74c26 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:07 +0200 Subject: [PATCH 04/68] Canonicalize release workflow From 404b03563b64182893c898314d9c30dc5b8af5d9 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:18 +0200 Subject: [PATCH 05/68] Add canonical workflow assets --- src/tools/workflows/files/ci.yml | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/tools/workflows/files/ci.yml diff --git a/src/tools/workflows/files/ci.yml b/src/tools/workflows/files/ci.yml new file mode 100644 index 0000000..05eb663 --- /dev/null +++ b/src/tools/workflows/files/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.13' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Validate Ankhorage repository + run: bunx @ankhorage/ankh doctor validate . + + - name: Run build + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.build ? 0 : 1)"; then + bun run build + else + echo "No build script found; skipping." + fi + + - name: Run lint + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.lint ? 0 : 1)"; then + bun run lint + else + echo "No lint script found; skipping." + fi + + - name: Run format check + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.['format:check'] ? 0 : 1)"; then + bun run format:check + else + echo "No format:check script found; skipping." + fi + + - name: Run Knip + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.knip ? 0 : 1)"; then + bun run knip + else + echo "No knip script found; skipping." + fi + + - name: Run tests + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.test ? 0 : 1)"; then + bun run test + else + echo "No test script found; skipping." + fi + + - name: Run typecheck + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.typecheck ? 0 : 1)"; then + bun run typecheck + else + echo "No typecheck script found; skipping." + fi + + - name: Check changesets + if: github.event_name == 'pull_request' + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.['changeset:status'] ? 0 : 1)"; then + bun run changeset:status + else + echo "No changeset:status script found; skipping." + fi From 6ea590618c19dc0bc1ea33ba6c9b9432acb049b0 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:26 +0200 Subject: [PATCH 06/68] Add canonical workflow assets --- src/tools/workflows/files/release.yml | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/tools/workflows/files/release.yml diff --git a/src/tools/workflows/files/release.yml b/src/tools/workflows/files/release.yml new file mode 100644 index 0000000..71e5870 --- /dev/null +++ b/src/tools/workflows/files/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + id-token: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.13' + + - name: Setup Node for npm publishing + uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + - name: Update npm + run: npm install -g npm@latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build package + run: | + if node -e "const p=require('./package.json'); process.exit(p.scripts?.build ? 0 : 1)"; then + bun run build + else + echo "No build script found; skipping." + fi + + - name: Create release pull request or publish to npm + if: hashFiles('.changeset/config.json') != '' + uses: changesets/action@v1 + with: + version: bun run version-packages + publish: bunx changeset publish + commit: Version Packages + title: Version Packages + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Skip release + if: hashFiles('.changeset/config.json') == '' + run: echo "No Changesets config found; skipping release." From 353e3a26fdb6ab64685c44ff52a887d510a63374 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:33 +0200 Subject: [PATCH 07/68] Add workflow synchronization concern --- src/tools/workflows/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/tools/workflows/index.ts diff --git a/src/tools/workflows/index.ts b/src/tools/workflows/index.ts new file mode 100644 index 0000000..34b5529 --- /dev/null +++ b/src/tools/workflows/index.ts @@ -0,0 +1,12 @@ +import type { ManagedFileDefinition } from '../shared/managedFiles.js'; + +export const workflowManagedFiles = [ + { + relativePath: '.github/workflows/ci.yml', + sourceUrl: new URL('./files/ci.yml', import.meta.url), + }, + { + relativePath: '.github/workflows/release.yml', + sourceUrl: new URL('./files/release.yml', import.meta.url), + }, +] as const satisfies readonly ManagedFileDefinition[]; From 1d4cdc38261a9d206ad70e86fc2bdfa640aa7d30 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:38 +0200 Subject: [PATCH 08/68] Add canonical VS Code assets --- src/tools/vscode/files/settings.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/tools/vscode/files/settings.json diff --git a/src/tools/vscode/files/settings.json b/src/tools/vscode/files/settings.json new file mode 100644 index 0000000..8ee7303 --- /dev/null +++ b/src/tools/vscode/files/settings.json @@ -0,0 +1,15 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "editor.tabSize": 2, + "editor.detectIndentation": false +} From 3c82707d3227de6005ce7b55e2785b8abc438467 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:44 +0200 Subject: [PATCH 09/68] Add canonical VS Code assets --- src/tools/vscode/files/extensions.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/tools/vscode/files/extensions.json diff --git a/src/tools/vscode/files/extensions.json b/src/tools/vscode/files/extensions.json new file mode 100644 index 0000000..4a81931 --- /dev/null +++ b/src/tools/vscode/files/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "oven.bun-vscode", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "redhat.vscode-yaml", + "github.vscode-github-actions" + ] +} From 7a9438796460b3e905c0321422c7af9350e62a5e Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:51 +0200 Subject: [PATCH 10/68] Add VS Code synchronization concern --- src/tools/vscode/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/tools/vscode/index.ts diff --git a/src/tools/vscode/index.ts b/src/tools/vscode/index.ts new file mode 100644 index 0000000..bd96bef --- /dev/null +++ b/src/tools/vscode/index.ts @@ -0,0 +1,12 @@ +import type { ManagedFileDefinition } from '../shared/managedFiles.js'; + +export const vscodeManagedFiles = [ + { + relativePath: '.vscode/settings.json', + sourceUrl: new URL('./files/settings.json', import.meta.url), + }, + { + relativePath: '.vscode/extensions.json', + sourceUrl: new URL('./files/extensions.json', import.meta.url), + }, +] as const satisfies readonly ManagedFileDefinition[]; From 691a2444c501597f309962641271c0d6e5200fad Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:20:58 +0200 Subject: [PATCH 11/68] Dogfood canonical VS Code settings --- .vscode/settings.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8ee7303 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "editor.tabSize": 2, + "editor.detectIndentation": false +} From edd839b967ee84dc118e71d170b69dcf7f3159ef Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:21:05 +0200 Subject: [PATCH 12/68] Dogfood canonical VS Code extensions --- .vscode/extensions.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4a81931 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "oven.bun-vscode", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "redhat.vscode-yaml", + "github.vscode-github-actions" + ] +} From 48e17e5c3fbdec3c503aaa3f1992dea2fc749a78 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:21:21 +0200 Subject: [PATCH 13/68] Add deterministic managed file engine --- src/tools/shared/managedFiles.ts | 134 +++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/tools/shared/managedFiles.ts diff --git a/src/tools/shared/managedFiles.ts b/src/tools/shared/managedFiles.ts new file mode 100644 index 0000000..de7e3ea --- /dev/null +++ b/src/tools/shared/managedFiles.ts @@ -0,0 +1,134 @@ +import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; + +export interface ManagedFileDefinition { + readonly relativePath: string; + readonly sourceUrl: URL; +} + +export type ManagedFileState = 'current' | 'missing' | 'outdated'; +export type ManagedFileSyncAction = + | 'unchanged' + | 'created' + | 'updated' + | 'would-create' + | 'would-update'; + +export interface ManagedFileStatus { + readonly relativePath: string; + readonly state: ManagedFileState; +} + +export interface ManagedFileSyncResult { + readonly relativePath: string; + readonly action: ManagedFileSyncAction; +} + +export async function resolveManagedTargetDirectory( + cwd: string, + requestedPath: string | undefined, +): Promise { + const targetDirectory = resolve(cwd, requestedPath ?? '.'); + + let targetStats; + try { + targetStats = await stat(targetDirectory); + } catch (error) { + throw new Error(`Target directory does not exist: ${targetDirectory}`, { cause: error }); + } + + if (!targetStats.isDirectory()) { + throw new Error(`Target path is not a directory: ${targetDirectory}`); + } + + return targetDirectory; +} + +export async function inspectManagedFiles( + targetDirectory: string, + definitions: readonly ManagedFileDefinition[], +): Promise { + return await Promise.all( + definitions.map(async (definition): Promise => { + const canonicalContents = await readCanonicalContents(definition); + const targetPath = resolve(targetDirectory, definition.relativePath); + + try { + const targetContents = await readFile(targetPath, 'utf8'); + return { + relativePath: definition.relativePath, + state: targetContents === canonicalContents ? 'current' : 'outdated', + }; + } catch (error) { + if (isMissingFileError(error)) { + return { + relativePath: definition.relativePath, + state: 'missing', + }; + } + + throw new Error(`Failed to inspect managed file: ${targetPath}`, { cause: error }); + } + }), + ); +} + +export async function syncManagedFiles( + targetDirectory: string, + definitions: readonly ManagedFileDefinition[], + options: { readonly dryRun: boolean }, +): Promise { + const statuses = await inspectManagedFiles(targetDirectory, definitions); + const definitionsByPath = new Map( + definitions.map((definition) => [definition.relativePath, definition] as const), + ); + const results: ManagedFileSyncResult[] = []; + + for (const status of statuses) { + if (status.state === 'current') { + results.push({ relativePath: status.relativePath, action: 'unchanged' }); + continue; + } + + const definition = definitionsByPath.get(status.relativePath); + if (definition === undefined) { + throw new Error(`Missing managed file definition for ${status.relativePath}.`); + } + + if (options.dryRun) { + results.push({ + relativePath: status.relativePath, + action: status.state === 'missing' ? 'would-create' : 'would-update', + }); + continue; + } + + const targetPath = resolve(targetDirectory, definition.relativePath); + await mkdir(dirname(targetPath), { recursive: true }); + await writeFile(targetPath, await readCanonicalContents(definition), 'utf8'); + results.push({ + relativePath: status.relativePath, + action: status.state === 'missing' ? 'created' : 'updated', + }); + } + + return results; +} + +async function readCanonicalContents(definition: ManagedFileDefinition): Promise { + try { + return await readFile(definition.sourceUrl, 'utf8'); + } catch (error) { + throw new Error(`Failed to read canonical managed file: ${definition.relativePath}`, { + cause: error, + }); + } +} + +function isMissingFileError(error: unknown): boolean { + return isNodeError(error) && error.code === 'ENOENT'; +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error; +} From c932cb7c42972fce0b900feb6db0d13146d3375f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:21:38 +0200 Subject: [PATCH 14/68] Define devtools command surface --- src/cli/commands.ts | 143 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/cli/commands.ts diff --git a/src/cli/commands.ts b/src/cli/commands.ts new file mode 100644 index 0000000..5ca3aae --- /dev/null +++ b/src/cli/commands.ts @@ -0,0 +1,143 @@ +export type DevtoolsToolName = 'format' | 'knip' | 'lint'; +export type DevtoolsManagedScope = 'all' | 'vscode' | 'workflows'; +export type DevtoolsManagedOperation = 'status' | 'sync'; + +export type DevtoolsCapability = + | 'devtools.format' + | 'devtools.knip' + | 'devtools.lint' + | 'devtools.status' + | 'devtools.sync' + | 'devtools.vscode.status' + | 'devtools.vscode.sync' + | 'devtools.workflows.status' + | 'devtools.workflows.sync'; + +interface DevtoolsCommandBase { + readonly path: readonly [string, ...string[]]; + readonly capability: DevtoolsCapability; + readonly summary: string; +} + +export interface DevtoolsExternalCommandDefinition extends DevtoolsCommandBase { + readonly kind: 'external'; + readonly toolName: DevtoolsToolName; + readonly packageName: 'eslint' | 'knip' | 'prettier'; + readonly binName: 'eslint' | 'knip' | 'prettier'; +} + +export interface DevtoolsRepositoryCommandDefinition extends DevtoolsCommandBase { + readonly kind: 'repository'; + readonly scope: DevtoolsManagedScope; + readonly operation: DevtoolsManagedOperation; +} + +export type DevtoolsCommandDefinition = + | DevtoolsExternalCommandDefinition + | DevtoolsRepositoryCommandDefinition; + +const DEVTOOLS_COMMANDS = [ + { + kind: 'external', + toolName: 'lint', + path: ['lint'], + capability: 'devtools.lint', + summary: 'Run the shared ESLint toolchain.', + packageName: 'eslint', + binName: 'eslint', + }, + { + kind: 'external', + toolName: 'format', + path: ['format'], + capability: 'devtools.format', + summary: 'Run the shared Prettier toolchain.', + packageName: 'prettier', + binName: 'prettier', + }, + { + kind: 'external', + toolName: 'knip', + path: ['knip'], + capability: 'devtools.knip', + summary: 'Run the shared Knip toolchain.', + packageName: 'knip', + binName: 'knip', + }, + { + kind: 'repository', + path: ['sync'], + capability: 'devtools.sync', + summary: 'Synchronize all centrally managed repository files.', + scope: 'all', + operation: 'sync', + }, + { + kind: 'repository', + path: ['status'], + capability: 'devtools.status', + summary: 'Report drift for all centrally managed repository files.', + scope: 'all', + operation: 'status', + }, + { + kind: 'repository', + path: ['workflows', 'sync'], + capability: 'devtools.workflows.sync', + summary: 'Synchronize the canonical GitHub Actions workflows.', + scope: 'workflows', + operation: 'sync', + }, + { + kind: 'repository', + path: ['workflows', 'status'], + capability: 'devtools.workflows.status', + summary: 'Report drift for the canonical GitHub Actions workflows.', + scope: 'workflows', + operation: 'status', + }, + { + kind: 'repository', + path: ['vscode', 'sync'], + capability: 'devtools.vscode.sync', + summary: 'Synchronize the canonical VS Code workspace configuration.', + scope: 'vscode', + operation: 'sync', + }, + { + kind: 'repository', + path: ['vscode', 'status'], + capability: 'devtools.vscode.status', + summary: 'Report drift for the canonical VS Code workspace configuration.', + scope: 'vscode', + operation: 'status', + }, +] as const satisfies readonly DevtoolsCommandDefinition[]; + +export function getDevtoolsCommands(): readonly DevtoolsCommandDefinition[] { + return DEVTOOLS_COMMANDS; +} + +export function findDevtoolsCommandByPath( + path: readonly string[], +): DevtoolsCommandDefinition | null { + return ( + DEVTOOLS_COMMANDS.find( + (command) => + command.path.length === path.length && + command.path.every((segment, index) => segment === path[index]), + ) ?? null + ); +} + +export function getDevtoolsToolCommand( + toolName: DevtoolsToolName, +): DevtoolsExternalCommandDefinition { + for (const command of DEVTOOLS_COMMANDS) { + if (command.kind === 'external' && command.toolName === toolName) { + return command; + } + } + + throw new Error(`Unknown devtools tool command: ${toolName}`); +} From b49dc025ccd5cf4c497f162e65603342adb8809d Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:21:52 +0200 Subject: [PATCH 15/68] Implement devtools sync and status commands --- src/cli/runRepositoryCommand.ts | 166 ++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/cli/runRepositoryCommand.ts diff --git a/src/cli/runRepositoryCommand.ts b/src/cli/runRepositoryCommand.ts new file mode 100644 index 0000000..849c95a --- /dev/null +++ b/src/cli/runRepositoryCommand.ts @@ -0,0 +1,166 @@ +import { + inspectManagedFiles, + resolveManagedTargetDirectory, + syncManagedFiles, + type ManagedFileDefinition, + type ManagedFileStatus, + type ManagedFileSyncResult, +} from '../tools/shared/managedFiles.js'; +import { vscodeManagedFiles } from '../tools/vscode/index.js'; +import { workflowManagedFiles } from '../tools/workflows/index.js'; +import type { DevtoolsRepositoryCommandDefinition } from './commands.js'; + +export interface DevtoolsRepositoryCommandContext { + readonly cwd: string; + writeStdout(text: string): void; + writeStderr(text: string): void; +} + +export interface DevtoolsRepositoryCommandResult { + readonly exitCode: number; +} + +interface ParsedRepositoryArguments { + readonly dryRun: boolean; + readonly targetPath: string | undefined; +} + +export async function runRepositoryCommand( + command: DevtoolsRepositoryCommandDefinition, + argv: readonly string[], + context: DevtoolsRepositoryCommandContext, +): Promise { + let parsedArguments: ParsedRepositoryArguments; + + try { + parsedArguments = parseRepositoryArguments(argv, command.operation === 'sync'); + } catch (error) { + context.writeStderr(`${getErrorMessage(error)}\n`); + return { exitCode: 1 }; + } + + try { + const targetDirectory = await resolveManagedTargetDirectory( + context.cwd, + parsedArguments.targetPath, + ); + const definitions = getManagedFiles(command.scope); + + if (command.operation === 'status') { + const statuses = await inspectManagedFiles(targetDirectory, definitions); + writeStatusOutput(statuses, context); + return { + exitCode: statuses.some((status) => status.state !== 'current') ? 1 : 0, + }; + } + + const results = await syncManagedFiles(targetDirectory, definitions, { + dryRun: parsedArguments.dryRun, + }); + writeSyncOutput(results, context); + return { exitCode: 0 }; + } catch (error) { + context.writeStderr(`${getErrorMessage(error)}\n`); + return { exitCode: 1 }; + } +} + +export function parseRepositoryArguments( + argv: readonly string[], + allowDryRun: boolean, +): ParsedRepositoryArguments { + let dryRun = false; + let targetPath: string | undefined; + + for (const argument of argv) { + if (argument === '--dry-run') { + if (!allowDryRun) { + throw new Error('--dry-run is only valid for sync commands.'); + } + dryRun = true; + continue; + } + + if (argument.startsWith('-')) { + throw new Error(`Unknown option: ${argument}`); + } + + if (targetPath !== undefined) { + throw new Error('Only one target path may be provided.'); + } + + targetPath = argument; + } + + return { dryRun, targetPath }; +} + +function getManagedFiles( + scope: DevtoolsRepositoryCommandDefinition['scope'], +): readonly ManagedFileDefinition[] { + if (scope === 'workflows') { + return workflowManagedFiles; + } + + if (scope === 'vscode') { + return vscodeManagedFiles; + } + + return [...workflowManagedFiles, ...vscodeManagedFiles]; +} + +function writeStatusOutput( + statuses: readonly ManagedFileStatus[], + context: DevtoolsRepositoryCommandContext, +): void { + for (const status of statuses) { + if (status.state === 'current') { + context.writeStdout(`✓ ${status.relativePath}\n`); + } else if (status.state === 'missing') { + context.writeStdout(`+ ${status.relativePath} missing\n`); + } else { + context.writeStdout(`✗ ${status.relativePath} outdated\n`); + } + } +} + +function writeSyncOutput( + results: readonly ManagedFileSyncResult[], + context: DevtoolsRepositoryCommandContext, +): void { + for (const result of results) { + const prefix = getActionPrefix(result.action); + context.writeStdout(`${prefix} ${result.relativePath} ${formatAction(result.action)}\n`); + } +} + +function getActionPrefix(action: ManagedFileSyncResult['action']): string { + if (action === 'unchanged') { + return '✓'; + } + + if (action === 'created' || action === 'would-create') { + return '+'; + } + + return '↻'; +} + +function formatAction(action: ManagedFileSyncResult['action']): string { + switch (action) { + case 'created': + return 'created'; + case 'updated': + return 'updated'; + case 'unchanged': + return 'unchanged'; + case 'would-create': + return 'would create'; + case 'would-update': + return 'would update'; + } +} + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} From cd50fb3c673e43ad5e4a615dfb849ff4aa4e366c Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:15 +0200 Subject: [PATCH 16/68] Move external tool runner into CLI concern --- src/cli/runExternalTool.ts | 205 +++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/cli/runExternalTool.ts diff --git a/src/cli/runExternalTool.ts b/src/cli/runExternalTool.ts new file mode 100644 index 0000000..2378702 --- /dev/null +++ b/src/cli/runExternalTool.ts @@ -0,0 +1,205 @@ +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, extname, join, resolve } from 'node:path'; + +import type { DevtoolsExternalCommandDefinition } from './commands.js'; + +const require = createRequire(import.meta.url); + +export interface DevtoolsRunResult { + readonly exitCode: number; +} + +interface DevtoolsRunnerOptions { + readonly cwd?: string; + readonly env?: NodeJS.ProcessEnv; +} + +interface ResolvedExecutionTarget { + readonly command: string; + readonly args: readonly string[]; + readonly shell: boolean; +} + +interface SpawnedProcess { + on(event: 'error', listener: (error: Error) => void): SpawnedProcess; + on( + event: 'exit', + listener: (code: number | null, signal: NodeJS.Signals | null) => void, + ): SpawnedProcess; +} + +type SpawnProcess = ( + command: string, + args: readonly string[], + options: { + readonly cwd: string; + readonly env: NodeJS.ProcessEnv; + readonly shell: boolean; + readonly stdio: 'inherit'; + }, +) => SpawnedProcess; + +interface DevtoolsRunnerDependencies { + readonly logError: (message: string) => void; + readonly resolveExecutionTarget: ( + command: DevtoolsExternalCommandDefinition, + ) => Promise; + readonly spawnProcess: SpawnProcess; +} + +const defaultRunnerDependencies: DevtoolsRunnerDependencies = { + logError: (message) => { + console.error(message); + }, + resolveExecutionTarget, + spawnProcess: (command, args, options) => spawn(command, args, options), +}; + +export async function runExternalTool( + command: DevtoolsExternalCommandDefinition, + argv: readonly string[], + options?: DevtoolsRunnerOptions, +): Promise { + return await runExternalToolWithDependencies(command, argv, options, defaultRunnerDependencies); +} + +export async function runExternalToolWithDependencies( + command: DevtoolsExternalCommandDefinition, + argv: readonly string[], + options: DevtoolsRunnerOptions | undefined, + dependencies: DevtoolsRunnerDependencies, +): Promise { + let executionTarget: ResolvedExecutionTarget; + + try { + executionTarget = await dependencies.resolveExecutionTarget(command); + } catch (error) { + dependencies.logError(`Failed to resolve ${command.binName}: ${getErrorMessage(error)}`); + return { exitCode: 1 }; + } + + const cwd = options?.cwd ?? process.cwd(); + const env = options?.env ?? process.env; + + return await new Promise((resolveResult) => { + let settled = false; + + const settle = (result: DevtoolsRunResult) => { + if (settled) { + return; + } + + settled = true; + resolveResult(result); + }; + + const child = dependencies.spawnProcess( + executionTarget.command, + [...executionTarget.args, ...argv], + { + cwd, + env, + shell: executionTarget.shell, + stdio: 'inherit', + }, + ); + + child.on('error', (error) => { + dependencies.logError(`Failed to start ${command.binName}: ${getErrorMessage(error)}`); + settle({ exitCode: 1 }); + }); + + child.on('exit', (code, signal) => { + if (signal !== null) { + dependencies.logError(`${command.binName} exited with signal ${signal}.`); + settle({ exitCode: 1 }); + return; + } + + settle({ exitCode: code ?? 1 }); + }); + }); +} + +async function resolveExecutionTarget( + command: DevtoolsExternalCommandDefinition, +): Promise { + const binPath = await readPackageBinPath(command.packageName, command.binName); + if (await shouldExecuteWithNode(binPath)) { + return { + command: process.execPath, + args: [binPath], + shell: false, + }; + } + + return { + command: binPath, + args: [], + shell: process.platform === 'win32', + }; +} + +function findPackageJsonPath(packageName: string): string { + let currentDirectory = dirname(require.resolve(packageName)); + + while (currentDirectory !== dirname(currentDirectory)) { + const packageJsonPath = join(currentDirectory, 'package.json'); + + if (existsSync(packageJsonPath)) { + return packageJsonPath; + } + + currentDirectory = dirname(currentDirectory); + } + + throw new Error(`Could not find package metadata for ${packageName}.`); +} + +async function readPackageBinPath(packageName: string, binName: string): Promise { + const packageJsonPath = findPackageJsonPath(packageName); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as unknown; + + if (!isRecord(packageJson)) { + throw new Error(`Package metadata for ${packageName} is not an object.`); + } + + const { bin } = packageJson; + let relativeBinPath: string | undefined; + + if (typeof bin === 'string') { + relativeBinPath = bin; + } else if (isRecord(bin)) { + const namedBin = bin[binName]; + if (typeof namedBin === 'string') { + relativeBinPath = namedBin; + } + } + + if (relativeBinPath === undefined) { + throw new Error(`Package ${packageName} does not expose a ${binName} binary.`); + } + + return resolve(dirname(packageJsonPath), relativeBinPath); +} + +async function shouldExecuteWithNode(binPath: string): Promise { + const extension = extname(binPath).toLowerCase(); + if (extension === '.cjs' || extension === '.js' || extension === '.mjs') { + return true; + } + + const firstLine = (await readFile(binPath, 'utf8')).split('\n', 1)[0] ?? ''; + return firstLine.startsWith('#!') && firstLine.includes('node'); +} + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} From 76725dd57fd2a34b40650aad4e4af6690b972ac5 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:21 +0200 Subject: [PATCH 17/68] Dispatch devtools provider commands --- src/cli/runProviderCommand.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/cli/runProviderCommand.ts diff --git a/src/cli/runProviderCommand.ts b/src/cli/runProviderCommand.ts new file mode 100644 index 0000000..6dbe1d4 --- /dev/null +++ b/src/cli/runProviderCommand.ts @@ -0,0 +1,25 @@ +import type { DevtoolsCommandDefinition } from './commands.js'; +import { runExternalTool } from './runExternalTool.js'; +import { + runRepositoryCommand, + type DevtoolsRepositoryCommandContext, +} from './runRepositoryCommand.js'; + +export interface DevtoolsProviderCommandContext extends DevtoolsRepositoryCommandContext { + readonly env: Readonly>; +} + +export async function runProviderCommand( + command: DevtoolsCommandDefinition, + argv: readonly string[], + context: DevtoolsProviderCommandContext, +): Promise<{ readonly exitCode: number }> { + if (command.kind === 'external') { + return await runExternalTool(command, argv, { + cwd: context.cwd, + env: { ...context.env }, + }); + } + + return await runRepositoryCommand(command, argv, context); +} From 9c2001cf9b5b1e1bb67d2203b6e6b4a92bc93c32 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:28 +0200 Subject: [PATCH 18/68] Move standalone tool dispatch into CLI concern --- src/cli/runStandaloneTool.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/cli/runStandaloneTool.ts diff --git a/src/cli/runStandaloneTool.ts b/src/cli/runStandaloneTool.ts new file mode 100644 index 0000000..190dfde --- /dev/null +++ b/src/cli/runStandaloneTool.ts @@ -0,0 +1,9 @@ +import { getDevtoolsToolCommand, type DevtoolsToolName } from './commands.js'; +import { runExternalTool, type DevtoolsRunResult } from './runExternalTool.js'; + +export async function runStandaloneTool( + toolName: DevtoolsToolName, + argv: readonly string[], +): Promise { + return await runExternalTool(getDevtoolsToolCommand(toolName), argv); +} From 369e805f45e2e84c716b9b9197238c7153e07305 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:34 +0200 Subject: [PATCH 19/68] Move devtools binaries under CLI --- src/cli/bin/eslint.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/cli/bin/eslint.ts diff --git a/src/cli/bin/eslint.ts b/src/cli/bin/eslint.ts new file mode 100644 index 0000000..8107b81 --- /dev/null +++ b/src/cli/bin/eslint.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +import { runStandaloneTool } from '../runStandaloneTool.js'; + +const result = await runStandaloneTool('lint', process.argv.slice(2)); +process.exit(result.exitCode); From c57646cc8c410516d501d59dc5bc52a440adffa1 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:40 +0200 Subject: [PATCH 20/68] Move devtools binaries under CLI --- src/cli/bin/knip.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/cli/bin/knip.ts diff --git a/src/cli/bin/knip.ts b/src/cli/bin/knip.ts new file mode 100644 index 0000000..2d6fd1e --- /dev/null +++ b/src/cli/bin/knip.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +import { runStandaloneTool } from '../runStandaloneTool.js'; + +const result = await runStandaloneTool('knip', process.argv.slice(2)); +process.exit(result.exitCode); From 09bd6c3e270b6f2515771b0e6b95a5c6763ae0bf Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:47 +0200 Subject: [PATCH 21/68] Move devtools binaries under CLI --- src/cli/bin/prettier.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/cli/bin/prettier.ts diff --git a/src/cli/bin/prettier.ts b/src/cli/bin/prettier.ts new file mode 100644 index 0000000..f8003a8 --- /dev/null +++ b/src/cli/bin/prettier.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +import { runStandaloneTool } from '../runStandaloneTool.js'; + +const result = await runStandaloneTool('format', process.argv.slice(2)); +process.exit(result.exitCode); From f4c1b29d1c3301708a616987c743e542f1d3d2e5 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:00 +0200 Subject: [PATCH 22/68] Wire devtools repository command handlers --- src/cli/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index b61d7bf..1846245 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,8 +2,8 @@ import { readFileSync } from 'node:fs'; import type { AnkhRuntimeCommandProvider } from '@ankhorage/ankh'; -import { getDevtoolsCommands } from '../internal/devtoolsCommands.js'; -import { runDevtoolsCommand } from '../internal/runDevtoolsCommand.js'; +import { getDevtoolsCommands } from './commands.js'; +import { runProviderCommand } from './runProviderCommand.js'; const packageVersion = readPackageVersion(); const commands = getDevtoolsCommands(); @@ -21,8 +21,7 @@ const provider = { handlers: commands.map((command) => ({ path: [...command.path], handler: async (request) => { - const result = await runDevtoolsCommand(command, request.argv); - return { exitCode: result.exitCode }; + return await runProviderCommand(command, request.argv, request.context); }, })), } satisfies AnkhRuntimeCommandProvider; From e750a686295d96d8897aa5150f8e51057846264a Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:08 +0200 Subject: [PATCH 23/68] Move ESLint concern under src tools --- src/tools/eslint/types.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/tools/eslint/types.ts diff --git a/src/tools/eslint/types.ts b/src/tools/eslint/types.ts new file mode 100644 index 0000000..84b9bae --- /dev/null +++ b/src/tools/eslint/types.ts @@ -0,0 +1,20 @@ +import type tseslint from 'typescript-eslint'; + +type FlatConfig = ReturnType; +export type FlatConfigItem = FlatConfig[number]; + +interface RestrictedImport { + readonly name: string; + readonly message: string; +} + +export interface DevtoolsConfigOptions { + readonly tsconfigRootDir: string; + readonly project: string[]; + readonly files: string[]; + readonly allowDefaultProject?: string[]; + readonly additionalIgnores?: string[]; + readonly restrictedImports?: RestrictedImport[]; + readonly overrides?: FlatConfigItem[]; + readonly includePrettier?: boolean; +} From 163567fda06a57f13714ac6abc52ae05df2e3ed4 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:22 +0200 Subject: [PATCH 24/68] Move ESLint concern under src tools --- src/tools/eslint/index.ts | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/tools/eslint/index.ts diff --git a/src/tools/eslint/index.ts b/src/tools/eslint/index.ts new file mode 100644 index 0000000..7929f1e --- /dev/null +++ b/src/tools/eslint/index.ts @@ -0,0 +1,121 @@ +import js from '@eslint/js'; +import prettierConfig from 'eslint-config-prettier'; +import importPlugin from 'eslint-plugin-import'; +import prettierPlugin from 'eslint-plugin-prettier'; +import simpleImportSort from 'eslint-plugin-simple-import-sort'; +import unusedImports from 'eslint-plugin-unused-imports'; +import tseslint from 'typescript-eslint'; + +import type { DevtoolsConfigOptions, FlatConfigItem } from './types.js'; + +export const defaultIgnores = [ + '**/ios/**', + '**/android/**', + '**/dist/**', + '**/build/**', + '**/.expo/**', + '**/.next/**', + '**/node_modules/**', + '**/*.d.ts', + '**/templates/**', + '**/files/**', +] as const; + +export const defaultRestrictedImports = [ + { + name: 'react-native-reanimated-dnd', + message: + "Forbidden in Ankhorage packages. Use '@ankhorage/react-native-reanimated-dnd-web' directly.", + }, +] as const; + +export function createConfig(options: DevtoolsConfigOptions): ReturnType { + const normalizedOptions = { + allowDefaultProject: [], + additionalIgnores: [], + restrictedImports: [], + overrides: [], + includePrettier: true, + ...options, + }; + + const combinedRestrictedImports = [ + ...defaultRestrictedImports, + ...normalizedOptions.restrictedImports, + ]; + + const plugins = { + import: importPlugin, + prettier: prettierPlugin, + 'simple-import-sort': simpleImportSort, + 'unused-imports': unusedImports, + }; + + return tseslint.config( + { + ignores: [...defaultIgnores, ...normalizedOptions.additionalIgnores], + }, + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked.map((config) => ({ + ...config, + files: normalizedOptions.files, + })), + ...tseslint.configs.stylisticTypeChecked.map((config) => ({ + ...config, + files: normalizedOptions.files, + })), + { + files: normalizedOptions.files, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: normalizedOptions.project, + tsconfigRootDir: normalizedOptions.tsconfigRootDir, + allowDefaultProject: normalizedOptions.allowDefaultProject, + }, + }, + plugins, + rules: { + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/prefer-readonly': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/no-unnecessary-type-arguments': 'error', + '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + '@typescript-eslint/prefer-nullish-coalescing': 'error', + 'no-restricted-imports': [ + 'error', + { + paths: combinedRestrictedImports, + }, + ], + 'prefer-destructuring': 'off', + '@typescript-eslint/prefer-destructuring': 'error', + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'error', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + 'import/order': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'error', + 'prettier/prettier': 'error', + 'no-console': 'off', + }, + }, + ...normalizedOptions.overrides, + ...(normalizedOptions.includePrettier ? [prettierConfig as FlatConfigItem] : []), + ); +} + +export type { DevtoolsConfigOptions, FlatConfigItem } from './types.js'; From 7a75f790e5ac2c4805816663fdfd014dd68dc1b0 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:35 +0200 Subject: [PATCH 25/68] Move Knip concern under src tools --- src/tools/knip/index.ts | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/tools/knip/index.ts diff --git a/src/tools/knip/index.ts b/src/tools/knip/index.ts new file mode 100644 index 0000000..4baac31 --- /dev/null +++ b/src/tools/knip/index.ts @@ -0,0 +1,60 @@ +import type { KnipConfig } from 'knip'; + +export interface DevtoolsKnipWorkspaceConfigOptions { + readonly entry?: string[]; + readonly project?: string[]; + readonly ignore?: string[]; + readonly ignoreBinaries?: string[]; + readonly ignoreDependencies?: (string | RegExp)[]; + readonly ignoreFiles?: string[]; +} + +export interface DevtoolsKnipConfigOptions extends DevtoolsKnipWorkspaceConfigOptions { + readonly workspaces?: Record; +} + +export interface DevtoolsKnipMonorepoConfigOptions { + readonly root?: DevtoolsKnipWorkspaceConfigOptions; + readonly workspaceDefaults?: DevtoolsKnipWorkspaceConfigOptions; + readonly workspaceGlobs?: string[]; + readonly workspaces?: Record; +} + +const DEFAULT_MONOREPO_WORKSPACE_GLOBS = ['packages/*', 'apps/*'] as const; + +export function createKnipConfig(options: DevtoolsKnipConfigOptions = {}): KnipConfig { + return { + ...(options.entry === undefined ? {} : { entry: options.entry }), + ...(options.project === undefined ? {} : { project: options.project }), + ...(options.ignore === undefined ? {} : { ignore: options.ignore }), + ...(options.ignoreBinaries === undefined ? {} : { ignoreBinaries: options.ignoreBinaries }), + ...(options.ignoreDependencies === undefined + ? {} + : { ignoreDependencies: options.ignoreDependencies }), + ...(options.ignoreFiles === undefined ? {} : { ignoreFiles: options.ignoreFiles }), + ...(options.workspaces === undefined ? {} : { workspaces: options.workspaces }), + } satisfies KnipConfig; +} + +export function createKnipMonorepoConfig( + options: DevtoolsKnipMonorepoConfigOptions = {}, +): KnipConfig { + const explicitWorkspaces = options.workspaces ?? {}; + const workspaceGlobs = options.workspaceGlobs ?? [...DEFAULT_MONOREPO_WORKSPACE_GLOBS]; + const workspaces: Record = { + '.': options.root ?? {}, + }; + + for (const workspaceGlob of workspaceGlobs) { + workspaces[workspaceGlob] = { + ...(options.workspaceDefaults ?? {}), + ...(explicitWorkspaces[workspaceGlob] ?? {}), + }; + } + + for (const [workspaceGlob, workspaceConfig] of Object.entries(explicitWorkspaces)) { + workspaces[workspaceGlob] = workspaceConfig; + } + + return createKnipConfig({ workspaces }); +} From 12f43634ba5269554285262d23412407d72f03dd Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:41 +0200 Subject: [PATCH 26/68] Move Prettier concern under src tools --- src/tools/prettier/index.cjs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/tools/prettier/index.cjs diff --git a/src/tools/prettier/index.cjs b/src/tools/prettier/index.cjs new file mode 100644 index 0000000..558a1c3 --- /dev/null +++ b/src/tools/prettier/index.cjs @@ -0,0 +1,9 @@ +/** @type {import('prettier').Config} */ +module.exports = { + semi: true, + singleQuote: true, + trailingComma: 'all', + printWidth: 100, + tabWidth: 2, + arrowParens: 'always', +}; From 61f4ed7bd3ee53790d8724f0161cb54d52e21ae6 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:47 +0200 Subject: [PATCH 27/68] Point configs at src tools concerns --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 76c967a..44d8317 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,4 +1,4 @@ -import { createConfig } from './dist/eslint.js'; +import { createConfig } from './dist/tools/eslint/index.js'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; From 4794e389ff9178e6fac3903fd0953653cc60ef04 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:23:53 +0200 Subject: [PATCH 28/68] Point configs at src tools concerns --- knip.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knip.config.ts b/knip.config.ts index 4d4df9f..a821621 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,4 +1,4 @@ -import { createKnipConfig } from './src/knip.js'; +import { createKnipConfig } from './src/tools/knip/index.js'; export default createKnipConfig({ ignoreFiles: [ From c2cefd1bfcd1816ca1ad3f5507080addb3c0bdb5 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:24:47 +0200 Subject: [PATCH 29/68] Test canonical devtools command surface --- src/cli/commands.test.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/cli/commands.test.ts diff --git a/src/cli/commands.test.ts b/src/cli/commands.test.ts new file mode 100644 index 0000000..e6427e3 --- /dev/null +++ b/src/cli/commands.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'bun:test'; + +import { + findDevtoolsCommandByPath, + getDevtoolsCommands, + getDevtoolsToolCommand, +} from './commands.js'; + +describe('devtools command table', () => { + it('defines the exact provider-backed command surface', () => { + expect(getDevtoolsCommands().map((command) => command.path.join(' '))).toEqual([ + 'lint', + 'format', + 'knip', + 'sync', + 'status', + 'workflows sync', + 'workflows status', + 'vscode sync', + 'vscode status', + ]); + }); + + it('resolves single- and multi-segment command paths', () => { + expect(getDevtoolsToolCommand('lint').capability).toBe('devtools.lint'); + expect(findDevtoolsCommandByPath(['workflows', 'sync'])?.capability).toBe( + 'devtools.workflows.sync', + ); + expect(findDevtoolsCommandByPath(['vscode', 'status'])?.capability).toBe( + 'devtools.vscode.status', + ); + expect(findDevtoolsCommandByPath(['dev', 'sync'])).toBeNull(); + expect(findDevtoolsCommandByPath(['workflows'])).toBeNull(); + }); +}); From d53a849e1b3d5fa752d992138a317c13d5cce85d Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:24:56 +0200 Subject: [PATCH 30/68] Test external devtools command runner --- src/cli/runExternalTool.test.ts | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/cli/runExternalTool.test.ts diff --git a/src/cli/runExternalTool.test.ts b/src/cli/runExternalTool.test.ts new file mode 100644 index 0000000..4534c6b --- /dev/null +++ b/src/cli/runExternalTool.test.ts @@ -0,0 +1,69 @@ +import { EventEmitter } from 'node:events'; + +import { describe, expect, it } from 'bun:test'; + +import { getDevtoolsToolCommand } from './commands.js'; +import { runExternalToolWithDependencies } from './runExternalTool.js'; + +type FakeChildProcess = EventEmitter & { + on(event: 'error', listener: (error: Error) => void): FakeChildProcess; + on( + event: 'exit', + listener: (code: number | null, signal: NodeJS.Signals | null) => void, + ): FakeChildProcess; +}; + +describe('runExternalTool', () => { + it('preserves argv, cwd, env, and returns the child exit code', async () => { + const spawnCalls: unknown[] = []; + const result = await runExternalToolWithDependencies( + getDevtoolsToolCommand('lint'), + ['--max-warnings=0', 'src'], + { cwd: '/tmp/devtools-fixture', env: { FOO: 'bar' } }, + { + logError: () => undefined, + resolveExecutionTarget: () => + Promise.resolve({ command: process.execPath, args: ['/tmp/eslint.js'], shell: false }), + spawnProcess: (command, args, options) => { + spawnCalls.push({ command, args, options }); + const child = new EventEmitter() as FakeChildProcess; + queueMicrotask(() => child.emit('exit', 7, null)); + return child; + }, + }, + ); + + expect(result).toEqual({ exitCode: 7 }); + expect(spawnCalls).toEqual([ + { + command: process.execPath, + args: ['/tmp/eslint.js', '--max-warnings=0', 'src'], + options: { + cwd: '/tmp/devtools-fixture', + env: { FOO: 'bar' }, + shell: false, + stdio: 'inherit', + }, + }, + ]); + }); + + it('fails cleanly when command resolution fails', async () => { + const messages: string[] = []; + const result = await runExternalToolWithDependencies( + getDevtoolsToolCommand('lint'), + ['.'], + undefined, + { + logError: (message) => messages.push(message), + resolveExecutionTarget: () => Promise.reject(new Error('missing eslint binary')), + spawnProcess: () => { + throw new Error('spawn should not run'); + }, + }, + ); + + expect(result).toEqual({ exitCode: 1 }); + expect(messages).toEqual(['Failed to resolve eslint: missing eslint binary']); + }); +}); From a1e757d485ff3b73d2e2dd25488ac19e89e32109 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:07 +0200 Subject: [PATCH 31/68] Test repository synchronization commands --- src/cli/runRepositoryCommand.test.ts | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/cli/runRepositoryCommand.test.ts diff --git a/src/cli/runRepositoryCommand.test.ts b/src/cli/runRepositoryCommand.test.ts new file mode 100644 index 0000000..66f856d --- /dev/null +++ b/src/cli/runRepositoryCommand.test.ts @@ -0,0 +1,73 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'bun:test'; + +import { findDevtoolsCommandByPath } from './commands.js'; +import { parseRepositoryArguments, runRepositoryCommand } from './runRepositoryCommand.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { force: true, recursive: true })), + ); +}); + +describe('repository commands', () => { + it('syncs only the selected concern and status reports drift without writing', async () => { + const target = await mkdtemp('/tmp/devtools-repository-command-'); + temporaryDirectories.push(target); + const stdout: string[] = []; + const stderr: string[] = []; + const context = { + cwd: target, + writeStdout: (text: string) => stdout.push(text), + writeStderr: (text: string) => stderr.push(text), + }; + const workflowsSync = findDevtoolsCommandByPath(['workflows', 'sync']); + const allStatus = findDevtoolsCommandByPath(['status']); + if (workflowsSync?.kind !== 'repository' || allStatus?.kind !== 'repository') { + throw new Error('Expected repository commands.'); + } + + expect((await runRepositoryCommand(workflowsSync, [], context)).exitCode).toBe(0); + expect(await readFile(join(target, '.github/workflows/ci.yml'), 'utf8')).toContain( + 'bunx @ankhorage/ankh doctor validate .', + ); + expect(await Bun.file(join(target, '.vscode/settings.json')).exists()).toBe(false); + + stdout.length = 0; + expect((await runRepositoryCommand(allStatus, [], context)).exitCode).toBe(1); + expect(stdout.join('')).toContain('.vscode/settings.json missing'); + expect(stderr).toEqual([]); + }); + + it('supports dry-run and validates arguments', async () => { + const target = await mkdtemp('/tmp/devtools-repository-dry-run-'); + temporaryDirectories.push(target); + const stdout: string[] = []; + const sync = findDevtoolsCommandByPath(['sync']); + if (sync?.kind !== 'repository') { + throw new Error('Expected sync command.'); + } + + expect( + ( + await runRepositoryCommand(sync, ['--dry-run'], { + cwd: target, + writeStdout: (text) => stdout.push(text), + writeStderr: () => undefined, + }) + ).exitCode, + ).toBe(0); + expect(stdout.join('')).toContain('would create'); + expect(await Bun.file(join(target, '.github/workflows/ci.yml')).exists()).toBe(false); + expect(() => parseRepositoryArguments(['--dry-run'], false)).toThrow( + '--dry-run is only valid for sync commands.', + ); + expect(() => parseRepositoryArguments(['one', 'two'], true)).toThrow( + 'Only one target path may be provided.', + ); + }); +}); From c24ff665cbf1afd386928e0f1820aed847bef253 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:21 +0200 Subject: [PATCH 32/68] Test deterministic managed file engine --- src/tools/shared/managedFiles.test.ts | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/tools/shared/managedFiles.test.ts diff --git a/src/tools/shared/managedFiles.test.ts b/src/tools/shared/managedFiles.test.ts new file mode 100644 index 0000000..255a64d --- /dev/null +++ b/src/tools/shared/managedFiles.test.ts @@ -0,0 +1,89 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { afterEach, describe, expect, it } from 'bun:test'; + +import { + inspectManagedFiles, + resolveManagedTargetDirectory, + syncManagedFiles, + type ManagedFileDefinition, +} from './managedFiles.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { force: true, recursive: true })), + ); +}); + +describe('managed file synchronization', () => { + it('creates, updates, preserves unrelated files, and becomes idempotent', async () => { + const fixture = await createFixture(); + await writeFile(join(fixture.target, 'unrelated.txt'), 'keep me\n'); + + expect(await inspectManagedFiles(fixture.target, fixture.definitions)).toEqual([ + { relativePath: '.managed/example.txt', state: 'missing' }, + ]); + + expect( + await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false }), + ).toEqual([{ relativePath: '.managed/example.txt', action: 'created' }]); + expect(await readFile(join(fixture.target, '.managed/example.txt'), 'utf8')).toBe( + 'canonical\n', + ); + expect(await readFile(join(fixture.target, 'unrelated.txt'), 'utf8')).toBe('keep me\n'); + + await writeFile(join(fixture.target, '.managed/example.txt'), 'outdated\n'); + expect( + await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false }), + ).toEqual([{ relativePath: '.managed/example.txt', action: 'updated' }]); + expect( + await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false }), + ).toEqual([{ relativePath: '.managed/example.txt', action: 'unchanged' }]); + }); + + it('reports dry-run actions without writing files', async () => { + const fixture = await createFixture(); + + expect(await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: true })).toEqual([ + { relativePath: '.managed/example.txt', action: 'would-create' }, + ]); + expect(await inspectManagedFiles(fixture.target, fixture.definitions)).toEqual([ + { relativePath: '.managed/example.txt', state: 'missing' }, + ]); + }); + + it('defaults targets to cwd and rejects invalid targets', async () => { + const fixture = await createFixture(); + expect(await resolveManagedTargetDirectory(fixture.target, undefined)).toBe(fixture.target); + await expect( + resolveManagedTargetDirectory(fixture.target, 'does-not-exist'), + ).rejects.toThrow('Target directory does not exist'); + }); +}); + +async function createFixture(): Promise<{ + readonly target: string; + readonly definitions: readonly ManagedFileDefinition[]; +}> { + const root = await mkdtemp('/tmp/devtools-managed-files-'); + temporaryDirectories.push(root); + const canonical = join(root, 'canonical.txt'); + const target = join(root, 'target'); + await Bun.write(canonical, 'canonical\n'); + await mkdir(target); + await writeFile(join(target, '.keep'), ''); + + return { + target, + definitions: [ + { + relativePath: '.managed/example.txt', + sourceUrl: pathToFileURL(canonical), + }, + ], + }; +} From da5e6e1ebfd2fc23c3387361a767b48fd1c036f8 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:29 +0200 Subject: [PATCH 33/68] Test complete devtools provider --- src/ankh.provider.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ankh.provider.test.ts b/src/ankh.provider.test.ts index 191288c..12c6974 100644 --- a/src/ankh.provider.test.ts +++ b/src/ankh.provider.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'bun:test'; import provider from './cli/index.js'; -import { getDevtoolsCommands } from './internal/devtoolsCommands.js'; +import { getDevtoolsCommands } from './cli/commands.js'; describe('devtools package provider', () => { - it('exposes only the shipped devtools commands and capabilities', () => { + it('exposes the complete canonical devtools command surface', () => { const commands = getDevtoolsCommands(); expect(provider.id).toBe('@ankhorage/devtools'); @@ -21,12 +21,19 @@ describe('devtools package provider', () => { 'lint', 'format', 'knip', + 'sync', + 'status', + 'workflows sync', + 'workflows status', + 'vscode sync', + 'vscode status', ]); }); it('binds exactly one handler for every command descriptor', () => { - const { handlers } = provider; - const handlerPaths = handlers.map((handler) => handler.path.join(' ')).sort(); + const handlerPaths = (provider.handlers ?? []) + .map((handler) => handler.path.join(' ')) + .sort(); const commandPaths = provider.commands.map((command) => command.path.join(' ')).sort(); expect(handlerPaths).toEqual(commandPaths); From 04c3ef77f7fb5eab81622414eeb2f7037102c5d4 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:37 +0200 Subject: [PATCH 34/68] Point ESLint tests at tool concern --- src/eslint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eslint.test.ts b/src/eslint.test.ts index 6a07e3c..e6771e8 100644 --- a/src/eslint.test.ts +++ b/src/eslint.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'bun:test'; -import { createConfig, defaultRestrictedImports } from './eslint.js'; +import { createConfig, defaultRestrictedImports } from './tools/eslint/index.js'; describe('createConfig sanity check', () => { const baseOptions = { From 8d094b43eb5372f7bd48073fb7baca4706a2f7c4 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:45 +0200 Subject: [PATCH 35/68] Point Knip tests at tool concern --- src/knip.test.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/knip.test.ts b/src/knip.test.ts index ecea2b1..fb80adf 100644 --- a/src/knip.test.ts +++ b/src/knip.test.ts @@ -1,25 +1,23 @@ import { describe, expect, it } from 'bun:test'; -import { createKnipConfig } from './knip.js'; +import { createKnipConfig } from './tools/knip/index.js'; describe('createKnipConfig', () => { it('returns an empty config by default so Knip can use zero-config discovery', () => { - const config = createKnipConfig(); - - expect(config).toEqual({}); + expect(createKnipConfig()).toEqual({}); }); it('uses explicit repo-specific config when provided', () => { - const config = createKnipConfig({ - entry: ['scripts/release.ts'], - project: ['scripts/**/*.ts'], - ignore: ['fixtures/**'], - ignoreBinaries: ['eslint', 'prettier'], - ignoreDependencies: ['optional-package'], - ignoreFiles: ['examples/package/prettier.config.cjs'], - }); - - expect(config).toEqual({ + expect( + createKnipConfig({ + entry: ['scripts/release.ts'], + project: ['scripts/**/*.ts'], + ignore: ['fixtures/**'], + ignoreBinaries: ['eslint', 'prettier'], + ignoreDependencies: ['optional-package'], + ignoreFiles: ['examples/package/prettier.config.cjs'], + }), + ).toEqual({ entry: ['scripts/release.ts'], project: ['scripts/**/*.ts'], ignore: ['fixtures/**'], From 1cb72b6269df0a9272d7469d8825c4915087baa5 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:59 +0200 Subject: [PATCH 36/68] Test packaged devtools assets and metadata --- src/package.test.ts | 87 +++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/package.test.ts b/src/package.test.ts index c3aab25..c82816f 100644 --- a/src/package.test.ts +++ b/src/package.test.ts @@ -1,9 +1,21 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { describe, expect, it } from 'bun:test'; +const capabilities = [ + 'devtools.lint', + 'devtools.format', + 'devtools.knip', + 'devtools.sync', + 'devtools.status', + 'devtools.workflows.sync', + 'devtools.workflows.status', + 'devtools.vscode.sync', + 'devtools.vscode.status', +]; + describe('package metadata', () => { - it('publishes exact Ankh metadata and required scripts without expanding exports', () => { + it('publishes the canonical provider, binaries, exports, and managed assets', () => { const packageJson = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), ) as Record; @@ -13,62 +25,59 @@ describe('package metadata', () => { expect(packageJson.ankh).toEqual({ category: 'devtools', provider: './dist/cli/index.js', - capabilities: ['devtools.lint', 'devtools.format', 'devtools.knip'], + capabilities, }); - expect(packageJson.bin).toEqual({ - 'ankhorage-eslint': './dist/eslint-cli.js', - 'ankhorage-knip': './dist/knip-cli.js', - 'ankhorage-prettier': './dist/prettier-cli.js', + 'ankhorage-eslint': './dist/cli/bin/eslint.js', + 'ankhorage-knip': './dist/cli/bin/knip.js', + 'ankhorage-prettier': './dist/cli/bin/prettier.js', }); - expect(packageJson.exports).toEqual({ './cli': { types: './dist/cli/index.d.ts', import: './dist/cli/index.js', }, './eslint': { - types: './dist/eslint.d.ts', - import: './dist/eslint.js', + types: './dist/tools/eslint/index.d.ts', + import: './dist/tools/eslint/index.js', }, './knip': { - types: './dist/knip.d.ts', - import: './dist/knip.js', + types: './dist/tools/knip/index.d.ts', + import: './dist/tools/knip/index.js', }, './prettier': { - require: './dist/prettier.cjs', + require: './dist/tools/prettier/index.cjs', }, }); - const scripts = packageJson.scripts as Record; - expect(Object.keys(scripts).sort()).toEqual( - [ - 'build', - 'changeset', - 'changeset:status', - 'docs', - 'docs:check', - 'doctor', - 'format', - 'format:check', - 'knip', - 'lint', - 'lint:fix', - 'test', - 'typecheck', - 'version-packages', - ].sort(), + const build = (packageJson.scripts as Record).build; + expect(build).toContain('src/tools/workflows/files'); + expect(build).toContain('src/tools/vscode/files'); + expect(build).toContain('src/tools/prettier/index.cjs'); + }); + + it('resolves managed assets from the built package after build', () => { + expect(existsSync(new URL('../dist/tools/workflows/files/ci.yml', import.meta.url))).toBe(true); + expect(existsSync(new URL('../dist/tools/workflows/files/release.yml', import.meta.url))).toBe( + true, + ); + expect(existsSync(new URL('../dist/tools/vscode/files/settings.json', import.meta.url))).toBe( + true, + ); + expect(existsSync(new URL('../dist/tools/vscode/files/extensions.json', import.meta.url))).toBe( + true, ); }); - it('documents the provider-backed command surface in the README', () => { + it('documents only the canonical devtools command surface', () => { const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8'); - - expect(readme).toContain('ankh devtools lint'); - expect(readme).toContain('ankh devtools format'); - expect(readme).toContain('ankh devtools knip'); - expect(readme).toContain('devtools.lint'); - expect(readme).toContain('devtools.format'); - expect(readme).toContain('devtools.knip'); + for (const capability of capabilities) { + expect(readme).toContain(capability); + } + expect(readme).toContain('ankh devtools sync'); + expect(readme).toContain('ankh devtools workflows sync'); + expect(readme).toContain('ankh devtools vscode sync'); + expect(readme).not.toContain('ankh dev '); + expect(readme).not.toContain('@ankhorage/dev'); }); }); From eb9adf429169ef3fd986d450f8cbb4b5c4e8128c Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:05 +0200 Subject: [PATCH 37/68] Require repository sync documentation --- src/internal/readmeDocs.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/internal/readmeDocs.ts b/src/internal/readmeDocs.ts index e7c3e86..2d420b9 100644 --- a/src/internal/readmeDocs.ts +++ b/src/internal/readmeDocs.ts @@ -2,13 +2,13 @@ const REQUIRED_README_SNIPPETS = [ 'ankh devtools lint', 'ankh devtools format', 'ankh devtools knip', - 'ankhorage-eslint', - 'ankhorage-prettier', - 'ankhorage-knip', - 'devtools', - 'devtools.lint', - 'devtools.format', - 'devtools.knip', + 'ankh devtools sync', + 'ankh devtools status', + 'ankh devtools workflows sync', + 'ankh devtools vscode sync', + 'devtools.workflows.sync', + 'devtools.vscode.sync', + '--dry-run', ] as const; export function getReadmeDocumentationErrors(readmeContents: string): string[] { From 24a62cfa37091baa735a19c453017c461caa7e63 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:11 +0200 Subject: [PATCH 38/68] Test repository sync documentation --- src/internal/readmeDocs.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/internal/readmeDocs.test.ts b/src/internal/readmeDocs.test.ts index 0ba0992..da21b0d 100644 --- a/src/internal/readmeDocs.test.ts +++ b/src/internal/readmeDocs.test.ts @@ -3,26 +3,26 @@ import { describe, expect, it } from 'bun:test'; import { getReadmeDocumentationErrors } from './readmeDocs.js'; describe('README documentation validation', () => { - it('accepts a README that documents the shipped command surface', () => { + it('accepts a README that documents the complete command surface', () => { const readme = [ 'ankh devtools lint', 'ankh devtools format', 'ankh devtools knip', - 'ankhorage-eslint', - 'ankhorage-prettier', - 'ankhorage-knip', - 'devtools', - 'devtools.lint', - 'devtools.format', - 'devtools.knip', + 'ankh devtools sync', + 'ankh devtools status', + 'ankh devtools workflows sync', + 'ankh devtools vscode sync', + 'devtools.workflows.sync', + 'devtools.vscode.sync', + '--dry-run', ].join('\n'); expect(getReadmeDocumentationErrors(readme)).toEqual([]); }); - it('reports missing command or capability references', () => { + it('reports missing command references', () => { expect(getReadmeDocumentationErrors('ankh devtools lint')).toContain( - 'README.md is missing required documentation snippet: devtools.knip', + 'README.md is missing required documentation snippet: ankh devtools sync', ); }); }); From 4d844adf8a1c06f38be788750eefa342fc2bb089 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:17 +0200 Subject: [PATCH 39/68] Add devtools synchronization changeset --- .changeset/centralize-repository-devtools.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/centralize-repository-devtools.md diff --git a/.changeset/centralize-repository-devtools.md b/.changeset/centralize-repository-devtools.md new file mode 100644 index 0000000..fd687bd --- /dev/null +++ b/.changeset/centralize-repository-devtools.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/devtools': minor +--- + +Add canonical GitHub Actions and VS Code synchronization commands, move tool implementations under `src/tools`, and expose the complete `ankh devtools` provider surface. From 96efa8c24339ae08d888f86d77cff454b4bc8abf Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:24 +0200 Subject: [PATCH 40/68] Remove superseded root tool implementations --- src/eslint.ts | 131 -------------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 src/eslint.ts diff --git a/src/eslint.ts b/src/eslint.ts deleted file mode 100644 index f8c98be..0000000 --- a/src/eslint.ts +++ /dev/null @@ -1,131 +0,0 @@ -import js from '@eslint/js'; -import prettierConfig from 'eslint-config-prettier'; -import importPlugin from 'eslint-plugin-import'; -import prettierPlugin from 'eslint-plugin-prettier'; -import simpleImportSort from 'eslint-plugin-simple-import-sort'; -import unusedImports from 'eslint-plugin-unused-imports'; -import tseslint from 'typescript-eslint'; - -import type { DevtoolsConfigOptions, FlatConfigItem } from './types.js'; - -export const defaultIgnores = [ - '**/ios/**', - '**/android/**', - '**/dist/**', - '**/build/**', - '**/.expo/**', - '**/.next/**', - '**/node_modules/**', - '**/*.d.ts', - '**/templates/**', - '**/files/**', -] as const; - -export const defaultRestrictedImports = [ - { - name: 'react-native-reanimated-dnd', - message: - "Forbidden in Ankhorage packages. Use '@ankhorage/react-native-reanimated-dnd-web' directly.", - }, -] as const; - -export function createConfig(options: DevtoolsConfigOptions): ReturnType { - const normalizedOptions = { - allowDefaultProject: [], - additionalIgnores: [], - restrictedImports: [], - overrides: [], - includePrettier: true, - ...options, - }; - - const combinedRestrictedImports = [ - ...defaultRestrictedImports, - ...normalizedOptions.restrictedImports, - ]; - - const plugins = { - import: importPlugin, - prettier: prettierPlugin, - 'simple-import-sort': simpleImportSort, - 'unused-imports': unusedImports, - }; - - return tseslint.config( - { - ignores: [...defaultIgnores, ...normalizedOptions.additionalIgnores], - }, - - js.configs.recommended, - - ...tseslint.configs.recommendedTypeChecked.map((config) => ({ - ...config, - files: normalizedOptions.files, - })), - ...tseslint.configs.stylisticTypeChecked.map((config) => ({ - ...config, - files: normalizedOptions.files, - })), - - { - files: normalizedOptions.files, - languageOptions: { - parser: tseslint.parser, - parserOptions: { - project: normalizedOptions.project, - tsconfigRootDir: normalizedOptions.tsconfigRootDir, - allowDefaultProject: normalizedOptions.allowDefaultProject, - }, - }, - plugins, - rules: { - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/prefer-readonly': 'error', - '@typescript-eslint/prefer-optional-chain': 'error', - '@typescript-eslint/prefer-as-const': 'error', - '@typescript-eslint/no-unnecessary-type-arguments': 'error', - '@typescript-eslint/no-unnecessary-condition': 'error', - '@typescript-eslint/no-unnecessary-type-constraint': 'error', - '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], - '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - '@typescript-eslint/prefer-nullish-coalescing': 'error', - - 'no-restricted-imports': [ - 'error', - { - paths: combinedRestrictedImports, - }, - ], - - 'prefer-destructuring': 'off', - '@typescript-eslint/prefer-destructuring': 'error', - - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - - 'unused-imports/no-unused-imports': 'error', - 'unused-imports/no-unused-vars': [ - 'error', - { - vars: 'all', - varsIgnorePattern: '^_', - args: 'after-used', - argsIgnorePattern: '^_', - }, - ], - - 'import/order': 'off', - '@typescript-eslint/no-unused-vars': 'off', - - '@typescript-eslint/no-explicit-any': 'error', - - 'prettier/prettier': 'error', - - 'no-console': 'off', - }, - }, - - ...normalizedOptions.overrides, - ...(normalizedOptions.includePrettier ? [prettierConfig as FlatConfigItem] : []), - ); -} From befce2b3ad76d2e98a1388c17e41dfb0f6091e45 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:29 +0200 Subject: [PATCH 41/68] Remove superseded root tool implementations --- src/knip.ts | 60 ----------------------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 src/knip.ts diff --git a/src/knip.ts b/src/knip.ts deleted file mode 100644 index b1ee540..0000000 --- a/src/knip.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { KnipConfig } from 'knip'; - -export interface DevtoolsKnipWorkspaceConfigOptions { - entry?: string[]; - project?: string[]; - ignore?: string[]; - ignoreBinaries?: string[]; - ignoreDependencies?: (string | RegExp)[]; - ignoreFiles?: string[]; -} - -export interface DevtoolsKnipConfigOptions extends DevtoolsKnipWorkspaceConfigOptions { - workspaces?: Record; -} - -export interface DevtoolsKnipMonorepoConfigOptions { - root?: DevtoolsKnipWorkspaceConfigOptions; - workspaceDefaults?: DevtoolsKnipWorkspaceConfigOptions; - workspaceGlobs?: string[]; - workspaces?: Record; -} - -const DEFAULT_MONOREPO_WORKSPACE_GLOBS = ['packages/*', 'apps/*'] as const; - -export function createKnipConfig(options: DevtoolsKnipConfigOptions = {}): KnipConfig { - return { - ...(options.entry === undefined ? {} : { entry: options.entry }), - ...(options.project === undefined ? {} : { project: options.project }), - ...(options.ignore === undefined ? {} : { ignore: options.ignore }), - ...(options.ignoreBinaries === undefined ? {} : { ignoreBinaries: options.ignoreBinaries }), - ...(options.ignoreDependencies === undefined - ? {} - : { ignoreDependencies: options.ignoreDependencies }), - ...(options.ignoreFiles === undefined ? {} : { ignoreFiles: options.ignoreFiles }), - ...(options.workspaces === undefined ? {} : { workspaces: options.workspaces }), - } satisfies KnipConfig; -} - -export function createKnipMonorepoConfig( - options: DevtoolsKnipMonorepoConfigOptions = {}, -): KnipConfig { - const explicitWorkspaces = options.workspaces ?? {}; - const workspaceGlobs = options.workspaceGlobs ?? [...DEFAULT_MONOREPO_WORKSPACE_GLOBS]; - const workspaces: Record = { - '.': options.root ?? {}, - }; - - for (const workspaceGlob of workspaceGlobs) { - workspaces[workspaceGlob] = { - ...(options.workspaceDefaults ?? {}), - ...(explicitWorkspaces[workspaceGlob] ?? {}), - }; - } - - for (const [workspaceGlob, workspaceConfig] of Object.entries(explicitWorkspaces)) { - workspaces[workspaceGlob] = workspaceConfig; - } - - return createKnipConfig({ workspaces }); -} From 7b23d9d83786d9222f89fcd036e1ae6b5c2666d3 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:35 +0200 Subject: [PATCH 42/68] Remove superseded root tool implementations --- src/prettier.cjs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/prettier.cjs diff --git a/src/prettier.cjs b/src/prettier.cjs deleted file mode 100644 index 558a1c3..0000000 --- a/src/prettier.cjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('prettier').Config} */ -module.exports = { - semi: true, - singleQuote: true, - trailingComma: 'all', - printWidth: 100, - tabWidth: 2, - arrowParens: 'always', -}; From 47a7fccd159da58b5757721e2f7b0f93bb23a166 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:40 +0200 Subject: [PATCH 43/68] Remove superseded root tool implementations --- src/types.ts | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/types.ts diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index bb95186..0000000 --- a/src/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type tseslint from 'typescript-eslint'; - -type FlatConfig = ReturnType; -export type FlatConfigItem = FlatConfig[number]; - -interface RestrictedImport { - name: string; - message: string; -} - -export interface DevtoolsConfigOptions { - tsconfigRootDir: string; - project: string[]; - files: string[]; - allowDefaultProject?: string[]; - additionalIgnores?: string[]; - restrictedImports?: RestrictedImport[]; - overrides?: FlatConfigItem[]; - includePrettier?: boolean; -} From 403d7158927c54e5cfaf4904f6375c03c3e91ea8 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:47 +0200 Subject: [PATCH 44/68] Remove superseded root CLI entrypoints --- src/eslint-cli.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/eslint-cli.ts diff --git a/src/eslint-cli.ts b/src/eslint-cli.ts deleted file mode 100644 index ef77662..0000000 --- a/src/eslint-cli.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node - -import { runStandaloneDevtoolsCommand } from './internal/runStandaloneDevtoolsCommand.js'; - -const result = await runStandaloneDevtoolsCommand('lint', process.argv.slice(2)); -process.exit(result.exitCode); From ee355c4fda2f7ec3032f55597f3d186af98dcbc6 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:26:54 +0200 Subject: [PATCH 45/68] Remove superseded root CLI entrypoints --- src/knip-cli.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/knip-cli.ts diff --git a/src/knip-cli.ts b/src/knip-cli.ts deleted file mode 100644 index d713c38..0000000 --- a/src/knip-cli.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node - -import { runStandaloneDevtoolsCommand } from './internal/runStandaloneDevtoolsCommand.js'; - -const result = await runStandaloneDevtoolsCommand('knip', process.argv.slice(2)); -process.exit(result.exitCode); From 16e00f641020e1441c60c8bd7b1d3ab31852a75f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:27:01 +0200 Subject: [PATCH 46/68] Remove superseded root CLI entrypoints --- src/prettier-cli.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/prettier-cli.ts diff --git a/src/prettier-cli.ts b/src/prettier-cli.ts deleted file mode 100644 index 094ffd7..0000000 --- a/src/prettier-cli.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node - -import { runStandaloneDevtoolsCommand } from './internal/runStandaloneDevtoolsCommand.js'; - -const result = await runStandaloneDevtoolsCommand('format', process.argv.slice(2)); -process.exit(result.exitCode); From 6cbc8d8d6739cc83f6c36d92f359c0feeefb2581 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:27:07 +0200 Subject: [PATCH 47/68] Remove superseded internal command implementation --- src/internal/devtoolsCommands.ts | 57 -------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 src/internal/devtoolsCommands.ts diff --git a/src/internal/devtoolsCommands.ts b/src/internal/devtoolsCommands.ts deleted file mode 100644 index 5308716..0000000 --- a/src/internal/devtoolsCommands.ts +++ /dev/null @@ -1,57 +0,0 @@ -export type DevtoolsToolName = 'format' | 'knip' | 'lint'; - -export interface DevtoolsCommandDefinition { - path: readonly [DevtoolsToolName]; - capability: 'devtools.format' | 'devtools.knip' | 'devtools.lint'; - summary: string; - packageName: 'eslint' | 'knip' | 'prettier'; - binName: 'eslint' | 'knip' | 'prettier'; -} - -const DEVTOOLS_COMMANDS = [ - { - path: ['lint'], - capability: 'devtools.lint', - summary: 'Run the shared ESLint toolchain.', - packageName: 'eslint', - binName: 'eslint', - }, - { - path: ['format'], - capability: 'devtools.format', - summary: 'Run the shared Prettier toolchain.', - packageName: 'prettier', - binName: 'prettier', - }, - { - path: ['knip'], - capability: 'devtools.knip', - summary: 'Run the shared Knip toolchain.', - packageName: 'knip', - binName: 'knip', - }, -] as const satisfies readonly DevtoolsCommandDefinition[]; - -export function getDevtoolsCommands(): readonly DevtoolsCommandDefinition[] { - return DEVTOOLS_COMMANDS; -} - -export function findDevtoolsCommandByPath( - path: readonly string[], -): DevtoolsCommandDefinition | null { - if (path.length !== 1) { - return null; - } - - return DEVTOOLS_COMMANDS.find((command) => command.path[0] === path[0]) ?? null; -} - -export function getDevtoolsCommand(toolName: DevtoolsToolName): DevtoolsCommandDefinition { - const command = DEVTOOLS_COMMANDS.find((candidate) => candidate.path[0] === toolName); - - if (command === undefined) { - throw new Error(`Unknown devtools command: ${toolName}`); - } - - return command; -} From 292739e6f8c11b7baaca48d630e7faee34e94cc9 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:27:11 +0200 Subject: [PATCH 48/68] Remove superseded internal command tests --- src/internal/devtoolsCommands.test.ts | 44 --------------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/internal/devtoolsCommands.test.ts diff --git a/src/internal/devtoolsCommands.test.ts b/src/internal/devtoolsCommands.test.ts deleted file mode 100644 index d72515e..0000000 --- a/src/internal/devtoolsCommands.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'bun:test'; - -import { - findDevtoolsCommandByPath, - getDevtoolsCommand, - getDevtoolsCommands, -} from './devtoolsCommands.js'; - -describe('devtools command table', () => { - it('defines the exact provider-backed command surface', () => { - const commands = getDevtoolsCommands(); - - expect(commands).toEqual([ - { - path: ['lint'], - capability: 'devtools.lint', - summary: 'Run the shared ESLint toolchain.', - packageName: 'eslint', - binName: 'eslint', - }, - { - path: ['format'], - capability: 'devtools.format', - summary: 'Run the shared Prettier toolchain.', - packageName: 'prettier', - binName: 'prettier', - }, - { - path: ['knip'], - capability: 'devtools.knip', - summary: 'Run the shared Knip toolchain.', - packageName: 'knip', - binName: 'knip', - }, - ]); - }); - - it('returns commands by path and rejects unknown commands', () => { - expect(getDevtoolsCommand('lint').path).toEqual(['lint']); - expect(findDevtoolsCommandByPath(['format'])?.capability).toBe('devtools.format'); - expect(findDevtoolsCommandByPath(['unknown'])).toBeNull(); - expect(findDevtoolsCommandByPath(['lint', 'extra'])).toBeNull(); - }); -}); From 2245febce2faa5f96cf41b79f172423ff46615bc Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:27:16 +0200 Subject: [PATCH 49/68] Remove superseded internal command implementation --- src/internal/runDevtoolsCommand.ts | 208 ----------------------------- 1 file changed, 208 deletions(-) delete mode 100644 src/internal/runDevtoolsCommand.ts diff --git a/src/internal/runDevtoolsCommand.ts b/src/internal/runDevtoolsCommand.ts deleted file mode 100644 index 99328c2..0000000 --- a/src/internal/runDevtoolsCommand.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { spawn } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { readFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; -import { dirname, extname, join, resolve } from 'node:path'; - -import type { DevtoolsCommandDefinition, DevtoolsToolName } from './devtoolsCommands.js'; - -const require = createRequire(import.meta.url); - -export type { DevtoolsCommandDefinition, DevtoolsToolName }; - -export interface DevtoolsRunResult { - exitCode: number; -} - -interface DevtoolsRunnerOptions { - cwd?: string; - env?: NodeJS.ProcessEnv; -} - -interface ResolvedExecutionTarget { - command: string; - args: readonly string[]; - shell: boolean; -} - -interface SpawnedProcess { - on(event: 'error', listener: (error: Error) => void): SpawnedProcess; - on( - event: 'exit', - listener: (code: number | null, signal: NodeJS.Signals | null) => void, - ): SpawnedProcess; -} - -type SpawnProcess = ( - command: string, - args: readonly string[], - options: { - cwd: string; - env: NodeJS.ProcessEnv; - shell: boolean; - stdio: 'inherit'; - }, -) => SpawnedProcess; - -interface DevtoolsRunnerDependencies { - readonly logError: (message: string) => void; - readonly resolveExecutionTarget: ( - command: DevtoolsCommandDefinition, - ) => Promise; - readonly spawnProcess: SpawnProcess; -} - -const defaultRunnerDependencies: DevtoolsRunnerDependencies = { - logError: (message) => { - console.error(message); - }, - resolveExecutionTarget, - spawnProcess: (command, args, options) => spawn(command, args, options), -}; - -export async function runDevtoolsCommand( - command: DevtoolsCommandDefinition, - argv: readonly string[], - options?: DevtoolsRunnerOptions, -): Promise { - return runDevtoolsCommandWithDependencies(command, argv, options, defaultRunnerDependencies); -} - -export async function runDevtoolsCommandWithDependencies( - command: DevtoolsCommandDefinition, - argv: readonly string[], - options: DevtoolsRunnerOptions | undefined, - dependencies: DevtoolsRunnerDependencies, -): Promise { - let executionTarget: ResolvedExecutionTarget; - - try { - executionTarget = await dependencies.resolveExecutionTarget(command); - } catch (error) { - dependencies.logError(`Failed to resolve ${command.binName}: ${getErrorMessage(error)}`); - - return { exitCode: 1 }; - } - - const cwd = options?.cwd ?? process.cwd(); - const env = options?.env ?? process.env; - - return await new Promise((resolveResult) => { - let settled = false; - - const settle = (result: DevtoolsRunResult) => { - if (settled) { - return; - } - - settled = true; - resolveResult(result); - }; - - const child = dependencies.spawnProcess( - executionTarget.command, - [...executionTarget.args, ...argv], - { - cwd, - env, - shell: executionTarget.shell, - stdio: 'inherit', - }, - ); - - child.on('error', (error) => { - dependencies.logError(`Failed to start ${command.binName}: ${getErrorMessage(error)}`); - settle({ exitCode: 1 }); - }); - - child.on('exit', (code, signal) => { - if (signal !== null) { - dependencies.logError(`${command.binName} exited with signal ${signal}.`); - settle({ exitCode: 1 }); - return; - } - - settle({ exitCode: code ?? 1 }); - }); - }); -} - -async function resolveExecutionTarget( - command: DevtoolsCommandDefinition, -): Promise { - const binPath = await readPackageBinPath(command.packageName, command.binName); - if (await shouldExecuteWithNode(binPath)) { - return { - command: process.execPath, - args: [binPath], - shell: false, - }; - } - - return { - command: binPath, - args: [], - shell: process.platform === 'win32', - }; -} - -function findPackageJsonPath(packageName: string): string { - let currentDirectory = dirname(require.resolve(packageName)); - - while (currentDirectory !== dirname(currentDirectory)) { - const packageJsonPath = join(currentDirectory, 'package.json'); - - if (existsSync(packageJsonPath)) { - return packageJsonPath; - } - - currentDirectory = dirname(currentDirectory); - } - - throw new Error(`Could not find package metadata for ${packageName}.`); -} - -async function readPackageBinPath(packageName: string, binName: string): Promise { - const packageJsonPath = findPackageJsonPath(packageName); - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as unknown; - - if (!isRecord(packageJson)) { - throw new Error(`Package metadata for ${packageName} is not an object.`); - } - - const { bin } = packageJson; - let relativeBinPath: string | undefined; - - if (typeof bin === 'string') { - relativeBinPath = bin; - } else if (isRecord(bin)) { - const namedBin = bin[binName]; - if (typeof namedBin === 'string') { - relativeBinPath = namedBin; - } - } - - if (relativeBinPath === undefined) { - throw new Error(`Package ${packageName} does not expose a ${binName} binary.`); - } - - return resolve(dirname(packageJsonPath), relativeBinPath); -} - -async function shouldExecuteWithNode(binPath: string): Promise { - const extension = extname(binPath).toLowerCase(); - if (extension === '.cjs' || extension === '.js' || extension === '.mjs') { - return true; - } - - const firstLine = (await readFile(binPath, 'utf8')).split('\n', 1)[0] ?? ''; - return firstLine.startsWith('#!') && firstLine.includes('node'); -} - -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} From a4ca8ead02eb7632ee5e5864a64cdab3d89be053 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:27:21 +0200 Subject: [PATCH 50/68] Remove superseded internal command tests --- src/internal/runDevtoolsCommand.test.ts | 174 ------------------------ 1 file changed, 174 deletions(-) delete mode 100644 src/internal/runDevtoolsCommand.test.ts diff --git a/src/internal/runDevtoolsCommand.test.ts b/src/internal/runDevtoolsCommand.test.ts deleted file mode 100644 index 325b732..0000000 --- a/src/internal/runDevtoolsCommand.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { EventEmitter } from 'node:events'; - -import { describe, expect, it } from 'bun:test'; - -import { getDevtoolsCommand } from './devtoolsCommands.js'; -import { runDevtoolsCommandWithDependencies } from './runDevtoolsCommand.js'; - -type FakeChildProcess = EventEmitter & { - on(event: 'error', listener: (error: Error) => void): FakeChildProcess; - on( - event: 'exit', - listener: (code: number | null, signal: NodeJS.Signals | null) => void, - ): FakeChildProcess; -}; - -describe('runDevtoolsCommand', () => { - it('preserves argv, cwd, env, and returns the child exit code', async () => { - const command = getDevtoolsCommand('lint'); - const messages: string[] = []; - const spawnCalls: { - command: string; - args: readonly string[]; - options: { - cwd: string; - env: NodeJS.ProcessEnv; - shell: boolean; - stdio: 'inherit'; - }; - }[] = []; - - const result = await runDevtoolsCommandWithDependencies( - command, - ['--max-warnings=0', 'src'], - { - cwd: '/tmp/devtools-fixture', - env: { ...process.env, FOO: 'bar' }, - }, - { - logError: (message) => { - messages.push(message); - }, - resolveExecutionTarget: () => - Promise.resolve({ - command: process.execPath, - args: ['/tmp/eslint.js'], - shell: false, - }), - spawnProcess: (spawnCommand, args, options) => { - spawnCalls.push({ - command: spawnCommand, - args, - options: { - cwd: options.cwd, - env: options.env, - shell: options.shell, - stdio: options.stdio, - }, - }); - - const child = new EventEmitter() as FakeChildProcess; - - queueMicrotask(() => { - child.emit('exit', 7, null); - }); - - return child; - }, - }, - ); - - expect(result).toEqual({ exitCode: 7 }); - expect(messages).toEqual([]); - expect(spawnCalls).toEqual([ - { - command: process.execPath, - args: ['/tmp/eslint.js', '--max-warnings=0', 'src'], - options: { - cwd: '/tmp/devtools-fixture', - env: { ...process.env, FOO: 'bar' }, - shell: false, - stdio: 'inherit', - }, - }, - ]); - }); - - it('treats spawn errors as exit code 1', async () => { - const messages: string[] = []; - - const result = await runDevtoolsCommandWithDependencies( - getDevtoolsCommand('format'), - ['--check', '.'], - undefined, - { - logError: (message) => { - messages.push(message); - }, - resolveExecutionTarget: () => - Promise.resolve({ - command: '/tmp/prettier', - args: [], - shell: false, - }), - spawnProcess: () => { - const child = new EventEmitter() as FakeChildProcess; - - queueMicrotask(() => { - child.emit('error', new Error('spawn failed')); - }); - - return child; - }, - }, - ); - - expect(result).toEqual({ exitCode: 1 }); - expect(messages).toEqual(['Failed to start prettier: spawn failed']); - }); - - it('treats process signals as non-zero failures with a clear message', async () => { - const messages: string[] = []; - - const result = await runDevtoolsCommandWithDependencies( - getDevtoolsCommand('knip'), - [], - undefined, - { - logError: (message) => { - messages.push(message); - }, - resolveExecutionTarget: () => - Promise.resolve({ - command: '/tmp/knip', - args: [], - shell: false, - }), - spawnProcess: () => { - const child = new EventEmitter() as FakeChildProcess; - - queueMicrotask(() => { - child.emit('exit', null, 'SIGTERM'); - }); - - return child; - }, - }, - ); - - expect(result).toEqual({ exitCode: 1 }); - expect(messages).toEqual(['knip exited with signal SIGTERM.']); - }); - - it('fails cleanly when command resolution fails', async () => { - const messages: string[] = []; - - const result = await runDevtoolsCommandWithDependencies( - getDevtoolsCommand('lint'), - ['.'], - undefined, - { - logError: (message) => { - messages.push(message); - }, - resolveExecutionTarget: () => Promise.reject(new Error('missing eslint binary')), - spawnProcess: () => { - throw new Error('spawn should not run'); - }, - }, - ); - - expect(result).toEqual({ exitCode: 1 }); - expect(messages).toEqual(['Failed to resolve eslint: missing eslint binary']); - }); -}); From 9829c43a9156bce5bb7c8ef27edf955be3bd1c2e Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:27:25 +0200 Subject: [PATCH 51/68] Remove superseded internal standalone runner --- src/internal/runStandaloneDevtoolsCommand.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/internal/runStandaloneDevtoolsCommand.ts diff --git a/src/internal/runStandaloneDevtoolsCommand.ts b/src/internal/runStandaloneDevtoolsCommand.ts deleted file mode 100644 index f955ef3..0000000 --- a/src/internal/runStandaloneDevtoolsCommand.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getDevtoolsCommand } from './devtoolsCommands.js'; -import type { DevtoolsToolName } from './runDevtoolsCommand.js'; -import { type DevtoolsRunResult, runDevtoolsCommand } from './runDevtoolsCommand.js'; - -export async function runStandaloneDevtoolsCommand( - toolName: DevtoolsToolName, - argv: readonly string[], -): Promise { - return runDevtoolsCommand(getDevtoolsCommand(toolName), argv); -} From 949b49ce8d7058dc6a00bc33b0298ca63fe81a02 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:28:13 +0200 Subject: [PATCH 52/68] Assert no legacy dev package references --- src/package.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.test.ts b/src/package.test.ts index c82816f..9f076a1 100644 --- a/src/package.test.ts +++ b/src/package.test.ts @@ -78,6 +78,6 @@ describe('package metadata', () => { expect(readme).toContain('ankh devtools workflows sync'); expect(readme).toContain('ankh devtools vscode sync'); expect(readme).not.toContain('ankh dev '); - expect(readme).not.toContain('@ankhorage/dev'); + expect(readme).not.toContain('`@ankhorage/dev`'); }); }); From 8631ceecd1a9aa6f08e21a6c1bb02c615abf9985 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:29:22 +0200 Subject: [PATCH 53/68] noop --- DO_NOT_CREATE.tmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 DO_NOT_CREATE.tmp diff --git a/DO_NOT_CREATE.tmp b/DO_NOT_CREATE.tmp new file mode 100644 index 0000000..e69de29 From 1ac24ac22691bfbc82efccb73fc2d48443b05fb2 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:29:32 +0200 Subject: [PATCH 54/68] Remove temporary empty file --- DO_NOT_CREATE.tmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 DO_NOT_CREATE.tmp diff --git a/DO_NOT_CREATE.tmp b/DO_NOT_CREATE.tmp deleted file mode 100644 index e69de29..0000000 From 2d77e3a064fb908c5b3f9a7bdd4a28227973f07e Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:33:00 +0200 Subject: [PATCH 55/68] Temporarily capture lint diagnostics --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05eb663..0948ea1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,17 @@ jobs: echo "No lint script found; skipping." fi + - name: Capture lint diagnostics + if: failure() + run: bun run lint -- --format json > lint-results.json || true + + - name: Upload lint diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: lint-results + path: lint-results.json + - name: Run format check run: | if node -e "const p=require('./package.json'); process.exit(p.scripts?.['format:check'] ? 0 : 1)"; then From e9eb133888fd48c33357609760e43784da535f04 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:33:50 +0200 Subject: [PATCH 56/68] Capture direct ESLint diagnostics --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0948ea1..832db46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Capture lint diagnostics if: failure() - run: bun run lint -- --format json > lint-results.json || true + run: bunx eslint . --format json > lint-results.json 2>&1 || true - name: Upload lint diagnostics if: failure() From f5dd95fd1b05311ac3fa4bb3e9b628bb58e13122 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Sun, 12 Jul 2026 03:34:00 +0200 Subject: [PATCH 57/68] chore(prettier): config --- ankhorage-devtools-1.0.5.tgz | Bin 0 -> 8455 bytes ankhorage-devtools-1.1.0.tgz | Bin 0 -> 8931 bytes prettier.config.cjs | 2 +- src/ankh.provider.test.ts | 6 ++---- src/cli/runProviderCommand.ts | 2 +- src/cli/runRepositoryCommand.ts | 4 ++-- src/cli/runStandaloneTool.ts | 4 ++-- src/package.test.ts | 2 +- src/tools/shared/managedFiles.test.ts | 26 +++++++++++++------------- 9 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 ankhorage-devtools-1.0.5.tgz create mode 100644 ankhorage-devtools-1.1.0.tgz diff --git a/ankhorage-devtools-1.0.5.tgz b/ankhorage-devtools-1.0.5.tgz new file mode 100644 index 0000000000000000000000000000000000000000..8f378abe5b6469696fd34b47f852872257be94fc GIT binary patch literal 8455 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 { it('exposes the complete canonical devtools command surface', () => { @@ -31,9 +31,7 @@ describe('devtools package provider', () => { }); it('binds exactly one handler for every command descriptor', () => { - const handlerPaths = (provider.handlers ?? []) - .map((handler) => handler.path.join(' ')) - .sort(); + const handlerPaths = (provider.handlers ?? []).map((handler) => handler.path.join(' ')).sort(); const commandPaths = provider.commands.map((command) => command.path.join(' ')).sort(); expect(handlerPaths).toEqual(commandPaths); diff --git a/src/cli/runProviderCommand.ts b/src/cli/runProviderCommand.ts index 6dbe1d4..13c03a2 100644 --- a/src/cli/runProviderCommand.ts +++ b/src/cli/runProviderCommand.ts @@ -1,8 +1,8 @@ import type { DevtoolsCommandDefinition } from './commands.js'; import { runExternalTool } from './runExternalTool.js'; import { - runRepositoryCommand, type DevtoolsRepositoryCommandContext, + runRepositoryCommand, } from './runRepositoryCommand.js'; export interface DevtoolsProviderCommandContext extends DevtoolsRepositoryCommandContext { diff --git a/src/cli/runRepositoryCommand.ts b/src/cli/runRepositoryCommand.ts index 849c95a..8fdf3b8 100644 --- a/src/cli/runRepositoryCommand.ts +++ b/src/cli/runRepositoryCommand.ts @@ -1,10 +1,10 @@ import { inspectManagedFiles, - resolveManagedTargetDirectory, - syncManagedFiles, type ManagedFileDefinition, type ManagedFileStatus, type ManagedFileSyncResult, + resolveManagedTargetDirectory, + syncManagedFiles, } from '../tools/shared/managedFiles.js'; import { vscodeManagedFiles } from '../tools/vscode/index.js'; import { workflowManagedFiles } from '../tools/workflows/index.js'; diff --git a/src/cli/runStandaloneTool.ts b/src/cli/runStandaloneTool.ts index 190dfde..6f87b93 100644 --- a/src/cli/runStandaloneTool.ts +++ b/src/cli/runStandaloneTool.ts @@ -1,5 +1,5 @@ -import { getDevtoolsToolCommand, type DevtoolsToolName } from './commands.js'; -import { runExternalTool, type DevtoolsRunResult } from './runExternalTool.js'; +import { type DevtoolsToolName, getDevtoolsToolCommand } from './commands.js'; +import { type DevtoolsRunResult, runExternalTool } from './runExternalTool.js'; export async function runStandaloneTool( toolName: DevtoolsToolName, diff --git a/src/package.test.ts b/src/package.test.ts index 9f076a1..8933b55 100644 --- a/src/package.test.ts +++ b/src/package.test.ts @@ -50,7 +50,7 @@ describe('package metadata', () => { }, }); - const build = (packageJson.scripts as Record).build; + const { build } = packageJson.scripts as Record; expect(build).toContain('src/tools/workflows/files'); expect(build).toContain('src/tools/vscode/files'); expect(build).toContain('src/tools/prettier/index.cjs'); diff --git a/src/tools/shared/managedFiles.test.ts b/src/tools/shared/managedFiles.test.ts index 255a64d..a2f8fe4 100644 --- a/src/tools/shared/managedFiles.test.ts +++ b/src/tools/shared/managedFiles.test.ts @@ -6,9 +6,9 @@ import { afterEach, describe, expect, it } from 'bun:test'; import { inspectManagedFiles, + type ManagedFileDefinition, resolveManagedTargetDirectory, syncManagedFiles, - type ManagedFileDefinition, } from './managedFiles.js'; const temporaryDirectories: string[] = []; @@ -28,21 +28,21 @@ describe('managed file synchronization', () => { { relativePath: '.managed/example.txt', state: 'missing' }, ]); - expect( - await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false }), - ).toEqual([{ relativePath: '.managed/example.txt', action: 'created' }]); + expect(await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false })).toEqual([ + { relativePath: '.managed/example.txt', action: 'created' }, + ]); expect(await readFile(join(fixture.target, '.managed/example.txt'), 'utf8')).toBe( 'canonical\n', ); expect(await readFile(join(fixture.target, 'unrelated.txt'), 'utf8')).toBe('keep me\n'); await writeFile(join(fixture.target, '.managed/example.txt'), 'outdated\n'); - expect( - await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false }), - ).toEqual([{ relativePath: '.managed/example.txt', action: 'updated' }]); - expect( - await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false }), - ).toEqual([{ relativePath: '.managed/example.txt', action: 'unchanged' }]); + expect(await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false })).toEqual([ + { relativePath: '.managed/example.txt', action: 'updated' }, + ]); + expect(await syncManagedFiles(fixture.target, fixture.definitions, { dryRun: false })).toEqual([ + { relativePath: '.managed/example.txt', action: 'unchanged' }, + ]); }); it('reports dry-run actions without writing files', async () => { @@ -59,9 +59,9 @@ describe('managed file synchronization', () => { it('defaults targets to cwd and rejects invalid targets', async () => { const fixture = await createFixture(); expect(await resolveManagedTargetDirectory(fixture.target, undefined)).toBe(fixture.target); - await expect( - resolveManagedTargetDirectory(fixture.target, 'does-not-exist'), - ).rejects.toThrow('Target directory does not exist'); + await expect(resolveManagedTargetDirectory(fixture.target, 'does-not-exist')).rejects.toThrow( + 'Target directory does not exist', + ); }); }); From 714e6e7bf4df3d7df775e16594f5ab5f75eddd2b Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:38:42 +0200 Subject: [PATCH 58/68] Fix provider handler lint error --- src/ankh.provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ankh.provider.test.ts b/src/ankh.provider.test.ts index fda1716..3a5eb14 100644 --- a/src/ankh.provider.test.ts +++ b/src/ankh.provider.test.ts @@ -31,7 +31,7 @@ describe('devtools package provider', () => { }); it('binds exactly one handler for every command descriptor', () => { - const handlerPaths = (provider.handlers ?? []).map((handler) => handler.path.join(' ')).sort(); + const handlerPaths = provider.handlers.map((handler) => handler.path.join(' ')).sort(); const commandPaths = provider.commands.map((command) => command.path.join(' ')).sort(); expect(handlerPaths).toEqual(commandPaths); From 033e64360fa71529d8ce3dd8ba35486abb012df3 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:39:13 +0200 Subject: [PATCH 59/68] Fix managed file rejection test lint error --- src/tools/shared/managedFiles.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/tools/shared/managedFiles.test.ts b/src/tools/shared/managedFiles.test.ts index a2f8fe4..42c47de 100644 --- a/src/tools/shared/managedFiles.test.ts +++ b/src/tools/shared/managedFiles.test.ts @@ -59,9 +59,16 @@ describe('managed file synchronization', () => { it('defaults targets to cwd and rejects invalid targets', async () => { const fixture = await createFixture(); expect(await resolveManagedTargetDirectory(fixture.target, undefined)).toBe(fixture.target); - await expect(resolveManagedTargetDirectory(fixture.target, 'does-not-exist')).rejects.toThrow( - 'Target directory does not exist', - ); + + let thrownError: unknown; + try { + await resolveManagedTargetDirectory(fixture.target, 'does-not-exist'); + } catch (error) { + thrownError = error; + } + + expect(thrownError).toBeInstanceOf(Error); + expect((thrownError as Error).message).toContain('Target directory does not exist'); }); }); From 6ffb0773213933b76c59de85d1d623096de6aee3 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:41:01 +0200 Subject: [PATCH 60/68] Keep internal command types private --- src/cli/commands.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 5ca3aae..7740473 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -1,8 +1,8 @@ export type DevtoolsToolName = 'format' | 'knip' | 'lint'; -export type DevtoolsManagedScope = 'all' | 'vscode' | 'workflows'; -export type DevtoolsManagedOperation = 'status' | 'sync'; +type DevtoolsManagedScope = 'all' | 'vscode' | 'workflows'; +type DevtoolsManagedOperation = 'status' | 'sync'; -export type DevtoolsCapability = +type DevtoolsCapability = | 'devtools.format' | 'devtools.knip' | 'devtools.lint' From de5cf71e412a62d94031775326bacb1cdef5ba53 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:41:17 +0200 Subject: [PATCH 61/68] Keep internal managed file types private --- src/tools/shared/managedFiles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/shared/managedFiles.ts b/src/tools/shared/managedFiles.ts index de7e3ea..caf384f 100644 --- a/src/tools/shared/managedFiles.ts +++ b/src/tools/shared/managedFiles.ts @@ -6,8 +6,8 @@ export interface ManagedFileDefinition { readonly sourceUrl: URL; } -export type ManagedFileState = 'current' | 'missing' | 'outdated'; -export type ManagedFileSyncAction = +type ManagedFileState = 'current' | 'missing' | 'outdated'; +type ManagedFileSyncAction = | 'unchanged' | 'created' | 'updated' From 4447d17185d3b0d736dfe7d8b14e55090d6a82d0 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:41:29 +0200 Subject: [PATCH 62/68] Remove temporary lint diagnostics --- .github/workflows/ci.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 832db46..05eb663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,17 +46,6 @@ jobs: echo "No lint script found; skipping." fi - - name: Capture lint diagnostics - if: failure() - run: bunx eslint . --format json > lint-results.json 2>&1 || true - - - name: Upload lint diagnostics - if: failure() - uses: actions/upload-artifact@v4 - with: - name: lint-results - path: lint-results.json - - name: Run format check run: | if node -e "const p=require('./package.json'); process.exit(p.scripts?.['format:check'] ? 0 : 1)"; then From dc761d3315a42f9b0d178ef916707d8cfb5fb211 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:41:36 +0200 Subject: [PATCH 63/68] Remove accidental package archives --- ankhorage-devtools-1.0.5.tgz | Bin 8455 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ankhorage-devtools-1.0.5.tgz diff --git a/ankhorage-devtools-1.0.5.tgz b/ankhorage-devtools-1.0.5.tgz deleted file mode 100644 index 8f378abe5b6469696fd34b47f852872257be94fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8455 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 Date: Sun, 12 Jul 2026 03:41:43 +0200 Subject: [PATCH 64/68] Remove accidental package archives --- ankhorage-devtools-1.1.0.tgz | Bin 8931 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ankhorage-devtools-1.1.0.tgz diff --git a/ankhorage-devtools-1.1.0.tgz b/ankhorage-devtools-1.1.0.tgz deleted file mode 100644 index cb531774bb3a6c10a4284a12dc4580e8292579e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8931 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 Date: Sun, 12 Jul 2026 03:42:34 +0200 Subject: [PATCH 65/68] Temporarily capture remaining lint diagnostics --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05eb663..832db46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,17 @@ jobs: echo "No lint script found; skipping." fi + - name: Capture lint diagnostics + if: failure() + run: bunx eslint . --format json > lint-results.json 2>&1 || true + + - name: Upload lint diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: lint-results + path: lint-results.json + - name: Run format check run: | if node -e "const p=require('./package.json'); process.exit(p.scripts?.['format:check'] ? 0 : 1)"; then From 6b5ec97f2eec4f6eb37f17767e5296484c50e52f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:43:30 +0200 Subject: [PATCH 66/68] Format managed file action type --- src/tools/shared/managedFiles.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/tools/shared/managedFiles.ts b/src/tools/shared/managedFiles.ts index caf384f..299a5a8 100644 --- a/src/tools/shared/managedFiles.ts +++ b/src/tools/shared/managedFiles.ts @@ -7,12 +7,7 @@ export interface ManagedFileDefinition { } type ManagedFileState = 'current' | 'missing' | 'outdated'; -type ManagedFileSyncAction = - | 'unchanged' - | 'created' - | 'updated' - | 'would-create' - | 'would-update'; +type ManagedFileSyncAction = 'unchanged' | 'created' | 'updated' | 'would-create' | 'would-update'; export interface ManagedFileStatus { readonly relativePath: string; From 5f62f045c23a56f08a02e36599bf7f131a7cebe3 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:43:44 +0200 Subject: [PATCH 67/68] Restore canonical CI workflow --- .github/workflows/ci.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 832db46..05eb663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,17 +46,6 @@ jobs: echo "No lint script found; skipping." fi - - name: Capture lint diagnostics - if: failure() - run: bunx eslint . --format json > lint-results.json 2>&1 || true - - - name: Upload lint diagnostics - if: failure() - uses: actions/upload-artifact@v4 - with: - name: lint-results - path: lint-results.json - - name: Run format check run: | if node -e "const p=require('./package.json'); process.exit(p.scripts?.['format:check'] ? 0 : 1)"; then From 0860ea78f1b97262fb5e42066320432502b4fdc1 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:44:28 +0200 Subject: [PATCH 68/68] Make package tests independent of prior build --- src/package.test.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/package.test.ts b/src/package.test.ts index 8933b55..f1196da 100644 --- a/src/package.test.ts +++ b/src/package.test.ts @@ -52,21 +52,18 @@ describe('package metadata', () => { const { build } = packageJson.scripts as Record; expect(build).toContain('src/tools/workflows/files'); + expect(build).toContain('dist/tools/workflows/files'); expect(build).toContain('src/tools/vscode/files'); + expect(build).toContain('dist/tools/vscode/files'); expect(build).toContain('src/tools/prettier/index.cjs'); + expect(build).toContain('dist/tools/prettier/index.cjs'); }); - it('resolves managed assets from the built package after build', () => { - expect(existsSync(new URL('../dist/tools/workflows/files/ci.yml', import.meta.url))).toBe(true); - expect(existsSync(new URL('../dist/tools/workflows/files/release.yml', import.meta.url))).toBe( - true, - ); - expect(existsSync(new URL('../dist/tools/vscode/files/settings.json', import.meta.url))).toBe( - true, - ); - expect(existsSync(new URL('../dist/tools/vscode/files/extensions.json', import.meta.url))).toBe( - true, - ); + it('ships every canonical managed asset in the source tree', () => { + expect(existsSync(new URL('./tools/workflows/files/ci.yml', import.meta.url))).toBe(true); + expect(existsSync(new URL('./tools/workflows/files/release.yml', import.meta.url))).toBe(true); + expect(existsSync(new URL('./tools/vscode/files/settings.json', import.meta.url))).toBe(true); + expect(existsSync(new URL('./tools/vscode/files/extensions.json', import.meta.url))).toBe(true); }); it('documents only the canonical devtools command surface', () => {