@ankhorage/devtools 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,7 @@ Shared development tools and repository standards for Ankhorage TypeScript proje
9
9
  ```text
10
10
  src/
11
11
  ├── cli/
12
+ ├── policy/
12
13
  └── tools/
13
14
  ├── eslint/
14
15
  ├── prettier/
@@ -18,14 +19,15 @@ src/
18
19
  └── vscode/
19
20
  ```
20
21
 
22
+ - `policy`: shared repository runtime policy, including the canonical Bun version
21
23
  - `eslint`: shared flat ESLint configuration, automatic project profiles, and the bundled ESLint runner
22
24
  - `prettier`: shared Prettier configuration and the bundled Prettier runner
23
25
  - `knip`: shared Knip configuration helpers and the bundled Knip runner
24
- - `package`: merge-aware synchronization of the shared `package.json` tooling contract
26
+ - `package`: merge-aware synchronization of the shared `package.json` tooling and Bun runtime contract
25
27
  - `workflows`: canonical `.github/workflows/ci.yml` and `release.yml`
26
28
  - `vscode`: canonical `.vscode/settings.json` and `extensions.json`
27
29
 
28
- The package owns the supported ESLint, TypeScript ESLint, Prettier, Knip, security, React, React Hooks, React Native, import/sort, unused-import, and formatting-plugin versions used by consuming repositories.
30
+ The package owns the supported ESLint, TypeScript ESLint, Prettier, Knip, security, React, React Hooks, React Native, import/sort, unused-import, and formatting-plugin versions used by consuming repositories. It also owns the Bun runtime version used by Ankhorage repository metadata and managed workflows.
29
31
 
30
32
  ## Bootstrap
31
33
 
@@ -34,7 +36,6 @@ For a repository that does not yet depend on the shared toolchain:
34
36
  ```bash
35
37
  bun add -D @ankhorage/devtools
36
38
  bunx @ankhorage/ankh devtools sync .
37
- bun install
38
39
  ```
39
40
 
40
41
  After the first install, the normal workflow is:
@@ -45,7 +46,9 @@ ankh devtools sync
45
46
 
46
47
  The target path is optional and defaults to the current working directory.
47
48
 
48
- Synchronization ensures `@ankhorage/devtools` is declared using the version of the provider performing the sync, installs the standard package scripts, and removes direct devDependencies for tools/plugins owned by devtools. Unrelated package metadata, dependencies, and scripts are preserved.
49
+ Synchronization ensures `@ankhorage/devtools` is declared using the version of the provider performing the sync, installs the standard package scripts, applies the managed Bun runtime policy, and removes direct devDependencies for tools/plugins owned by devtools. When package metadata changes, sync runs `bun install` so installed dependencies and `bun.lock` match the synchronized manifest. Unrelated package metadata, dependencies, and scripts are preserved.
50
+
51
+ `devtools sync` does not upgrade the globally installed Bun executable. The managed version applies to repository metadata, Bun types, and GitHub workflows.
49
52
 
50
53
  ## Ankh provider
51
54
 
@@ -150,6 +153,8 @@ Synchronization is deterministic and idempotent:
150
153
 
151
154
  - missing managed artifacts are created
152
155
  - outdated centrally owned artifacts are updated
156
+ - the managed Bun runtime version is applied consistently to package metadata and workflows
157
+ - package changes are followed by `bun install`, keeping installed dependencies and `bun.lock` synchronized
153
158
  - current artifacts are left untouched
154
159
  - unrelated files and package fields are preserved
155
160
  - repeated sync produces only `unchanged` results
@@ -247,20 +252,36 @@ export default createKnipConfig();
247
252
 
248
253
  `knip.config.ts` is create-only after bootstrap so repositories can retain narrow local entries, projects, ignores, binaries, dependencies, or switch to `createKnipMonorepoConfig()` without synchronization overwriting those extensions.
249
254
 
255
+ ## Managed Bun runtime policy
256
+
257
+ The canonical Bun policy is defined once in devtools and consumed by both package and workflow synchronization. The current policy is:
258
+
259
+ ```text
260
+ Bun runtime 1.3.14
261
+ packageManager bun@1.3.14
262
+ @types/bun ^1.3.14
263
+ ```
264
+
265
+ Changing the policy in devtools therefore updates the repository-facing Bun version consistently instead of maintaining independent version literals in multiple templates.
266
+
250
267
  ## Managed package contract
251
268
 
252
269
  `ankh devtools package sync` merge-updates `package.json` rather than replacing it.
253
270
 
254
271
  It owns:
255
272
 
256
- - the `@ankhorage/devtools` devDependency version range
273
+ - the `@ankhorage/devtools` dependency version range
274
+ - `packageManager` according to the managed Bun runtime policy
275
+ - the `@types/bun` development dependency according to the managed Bun runtime policy
257
276
  - `lint`
258
277
  - `lint:fix`
259
278
  - `format`
260
279
  - `format:check`
261
280
  - `knip`
262
281
 
263
- It also removes direct devDependencies for tools and ESLint plugins already provided by `@ankhorage/devtools`. Unrelated scripts, dependencies, metadata, and repository-specific configuration remain unchanged.
282
+ For normal consumers, `@ankhorage/devtools` is a devDependency. `@ankhorage/ankh` keeps devtools as a runtime dependency because it loads the provider. Devtools itself participates in the Bun runtime policy without attempting to install itself as a consumer dependency.
283
+
284
+ When this managed package contract changes, synchronization runs `bun install`. This updates installed dependencies and `bun.lock` before sync completes. It also removes direct devDependencies for tools and ESLint plugins already provided by `@ankhorage/devtools`. Unrelated scripts, dependencies, metadata, and repository-specific configuration remain unchanged.
264
285
 
265
286
  ## Managed GitHub Actions workflows
266
287
 
@@ -271,7 +292,7 @@ It also removes direct devDependencies for tools and ESLint plugins already prov
271
292
  .github/workflows/release.yml
272
293
  ```
273
294
 
274
- The CI workflow installs the pinned Bun version with the frozen lockfile, builds before repository-provider validation, runs `bunx @ankhorage/ankh doctor validate .`, and conditionally runs lint, formatting, Knip, tests, typecheck, and Changesets checks.
295
+ Both workflows render their `bun-version` from the same managed Bun runtime policy used for `package.json`. The CI workflow installs that Bun version with the frozen lockfile, builds before repository-provider validation, runs `bunx @ankhorage/ankh doctor validate .`, and conditionally runs lint, formatting, Knip, tests, typecheck, and Changesets checks.
275
296
 
276
297
  ## Managed VS Code configuration
277
298
 
@@ -1,6 +1,8 @@
1
+ import { type ManagedFileSyncResult } from '../tools/shared/managedFiles.js';
1
2
  import type { DevtoolsRepositoryCommandDefinition } from './commands.js';
2
3
  export interface DevtoolsRepositoryCommandContext {
3
4
  readonly cwd: string;
5
+ readonly syncDependencies?: (targetDirectory: string) => Promise<ManagedFileSyncResult>;
4
6
  writeStdout(text: string): void;
5
7
  writeStderr(text: string): void;
6
8
  }
@@ -1,6 +1,7 @@
1
1
  import { eslintManagedFiles } from '../tools/eslint/managed.js';
2
2
  import { knipManagedFiles } from '../tools/knip/managed.js';
3
3
  import { inspectPackageManifest, readCurrentDevtoolsVersion, syncPackageManifest, } from '../tools/package/index.js';
4
+ import { planBunDependencySync, syncBunDependencies, } from '../tools/package/syncBunDependencies.js';
4
5
  import { prettierManagedFiles } from '../tools/prettier/managed.js';
5
6
  import { inspectManagedFiles, resolveManagedTargetDirectory, syncManagedFiles, } from '../tools/shared/managedFiles.js';
6
7
  import { vscodeManagedFiles } from '../tools/vscode/index.js';
@@ -52,7 +53,17 @@ async function runStatus(scope, targetDirectory, devtoolsVersion, context) {
52
53
  async function runSync(scope, targetDirectory, devtoolsVersion, dryRun, context) {
53
54
  const results = [];
54
55
  if (scope === 'all' || scope === 'package') {
55
- results.push(await syncPackageManifest(targetDirectory, devtoolsVersion, { dryRun }));
56
+ const packageResult = await syncPackageManifest(targetDirectory, devtoolsVersion, { dryRun });
57
+ results.push(packageResult);
58
+ if (packageResult.action !== 'unchanged') {
59
+ if (dryRun) {
60
+ results.push(planBunDependencySync(targetDirectory));
61
+ }
62
+ else {
63
+ const syncDependencies = context.syncDependencies ?? syncBunDependencies;
64
+ results.push(await syncDependencies(targetDirectory));
65
+ }
66
+ }
56
67
  }
57
68
  results.push(...(await syncManagedFiles(targetDirectory, getManagedFiles(scope), { dryRun })));
58
69
  writeSyncOutput(results, context);
@@ -0,0 +1,5 @@
1
+ export declare const bunRuntimePolicy: {
2
+ readonly packageManager: "bun@1.3.14";
3
+ readonly typesRange: "^1.3.14";
4
+ readonly version: "1.3.14";
5
+ };
@@ -0,0 +1,5 @@
1
+ export const bunRuntimePolicy = {
2
+ packageManager: 'bun@1.3.14',
3
+ typesRange: '^1.3.14',
4
+ version: '1.3.14',
5
+ };
@@ -53,7 +53,7 @@ export const defaultRestrictedImports = [
53
53
  export function createConfig(options) {
54
54
  const normalized = normalizeOptions(options);
55
55
  const profile = resolveEslintProfile(options);
56
- return tseslint.config({ ignores: [...defaultIgnores, ...normalized.additionalIgnores] }, js.configs.recommended, ...createTypeCheckedConfigs(normalized), createBaseConfig(normalized), ...createProfileConfigs(profile, normalized.files), ...normalized.overrides, ...(normalized.includePrettier ? [prettierConfig] : []));
56
+ return tseslint.config({ ignores: [...defaultIgnores, ...normalized.additionalIgnores] }, { ...js.configs.recommended, files: normalized.files }, ...createTypeCheckedConfigs(normalized), createBaseConfig(normalized), ...createProfileConfigs(profile, normalized.files), ...normalized.overrides, ...(normalized.includePrettier ? [prettierConfig] : []));
57
57
  }
58
58
  function normalizeOptions(options) {
59
59
  return {
@@ -8,21 +8,23 @@
8
8
  * `format`, `format:check`, and `knip` scripts are written.
9
9
  * Unrelated manifest fields, scripts, dependencies, and metadata are preserved.
10
10
  *
11
+ * The Bun runtime policy is shared by every repository, including devtools itself. Devtools skips
12
+ * only its consumer dependency/script normalization so it never attempts to install itself.
13
+ *
11
14
  * Status compares only the fields owned by this contract, so unrelated repository customization
12
15
  * does not count as drift. `--dry-run` reports whether `package.json` would be created or updated
13
16
  * without writing it, and repeated synchronization is idempotent.
14
17
  *
15
- * The devtools package itself is excluded from the consumer contract to avoid rewriting its own
16
- * manifest.
17
- *
18
18
  * @readme
19
19
  */
20
20
  import { readFileSync } from 'node:fs';
21
21
  import { readFile, writeFile } from 'node:fs/promises';
22
22
  import { resolve } from 'node:path';
23
+ import { bunRuntimePolicy } from '../../policy/bunRuntimePolicy.js';
23
24
  const PACKAGE_PATH = 'package.json';
24
25
  const DEVTOOLS_PACKAGE_NAME = '@ankhorage/devtools';
25
26
  const ANKH_PACKAGE_NAME = '@ankhorage/ankh';
27
+ const BUN_TYPES_PACKAGE_NAME = '@types/bun';
26
28
  const STANDARD_SCRIPTS = {
27
29
  lint: 'ankhorage-eslint . --max-warnings=0',
28
30
  'lint:fix': 'ankhorage-eslint . --fix --max-warnings=0',
@@ -78,20 +80,23 @@ export async function syncPackageManifest(targetDirectory, devtoolsVersion, opti
78
80
  }
79
81
  export function applyManagedPackageContract(manifest, devtoolsVersion) {
80
82
  if (manifest.name === DEVTOOLS_PACKAGE_NAME) {
81
- return manifest;
83
+ return applyBunRuntimePolicy(manifest);
82
84
  }
83
85
  const scripts = { ...toRecord(manifest.scripts), ...STANDARD_SCRIPTS };
84
86
  const devDependencies = removeOwnedDependencies(toRecord(manifest.devDependencies));
85
87
  const dependencies = toRecord(manifest.dependencies);
86
88
  applyDevtoolsDependencyPlacement(manifest, dependencies, devDependencies, devtoolsVersion);
87
- return {
89
+ return applyBunRuntimePolicy({
88
90
  ...manifest,
89
91
  ...normalizedDependencies(manifest, dependencies),
90
92
  scripts,
91
93
  devDependencies,
92
- };
94
+ });
93
95
  }
94
96
  export function isManagedPackageContractCurrent(manifest, devtoolsVersion) {
97
+ if (!hasCurrentBunRuntimePolicy(manifest)) {
98
+ return false;
99
+ }
95
100
  if (manifest.name === DEVTOOLS_PACKAGE_NAME) {
96
101
  return true;
97
102
  }
@@ -125,6 +130,20 @@ async function readPackageManifest(targetDirectory) {
125
130
  throw error;
126
131
  }
127
132
  }
133
+ function applyBunRuntimePolicy(manifest) {
134
+ const devDependencies = toRecord(manifest.devDependencies);
135
+ devDependencies[BUN_TYPES_PACKAGE_NAME] = bunRuntimePolicy.typesRange;
136
+ return {
137
+ ...manifest,
138
+ packageManager: bunRuntimePolicy.packageManager,
139
+ devDependencies,
140
+ };
141
+ }
142
+ function hasCurrentBunRuntimePolicy(manifest) {
143
+ const devDependencies = toRecord(manifest.devDependencies);
144
+ return (manifest.packageManager === bunRuntimePolicy.packageManager &&
145
+ devDependencies[BUN_TYPES_PACKAGE_NAME] === bunRuntimePolicy.typesRange);
146
+ }
128
147
  function applyDevtoolsDependencyPlacement(manifest, dependencies, devDependencies, devtoolsVersion) {
129
148
  const versionRange = `^${devtoolsVersion}`;
130
149
  if (manifest.name === ANKH_PACKAGE_NAME) {
@@ -0,0 +1,3 @@
1
+ import type { ManagedFileSyncResult } from '../shared/managedFiles.js';
2
+ export declare function planBunDependencySync(targetDirectory: string): ManagedFileSyncResult;
3
+ export declare function syncBunDependencies(targetDirectory: string): Promise<ManagedFileSyncResult>;
@@ -0,0 +1,30 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ export function planBunDependencySync(targetDirectory) {
5
+ return {
6
+ relativePath: 'bun.lock',
7
+ action: existsSync(resolve(targetDirectory, 'bun.lock')) ? 'would-update' : 'would-create',
8
+ };
9
+ }
10
+ export async function syncBunDependencies(targetDirectory) {
11
+ const existed = existsSync(resolve(targetDirectory, 'bun.lock'));
12
+ await runBunInstall(targetDirectory);
13
+ return {
14
+ relativePath: 'bun.lock',
15
+ action: existed ? 'updated' : 'created',
16
+ };
17
+ }
18
+ async function runBunInstall(targetDirectory) {
19
+ await new Promise((resolvePromise, rejectPromise) => {
20
+ const child = spawn('bun', ['install'], { cwd: targetDirectory, stdio: 'inherit' });
21
+ child.once('error', rejectPromise);
22
+ child.once('exit', (code) => {
23
+ if (code === 0) {
24
+ resolvePromise();
25
+ return;
26
+ }
27
+ rejectPromise(new Error(`bun install exited with code ${code ?? 'unknown'}.`));
28
+ });
29
+ });
30
+ }
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Setup Bun
23
23
  uses: oven-sh/setup-bun@v2
24
24
  with:
25
- bun-version: '1.3.13'
25
+ bun-version: '__ANKH_BUN_VERSION__'
26
26
 
27
27
  - name: Install dependencies
28
28
  run: bun install --frozen-lockfile
@@ -27,7 +27,7 @@ jobs:
27
27
  - name: Setup Bun
28
28
  uses: oven-sh/setup-bun@v2
29
29
  with:
30
- bun-version: '1.3.13'
30
+ bun-version: '__ANKH_BUN_VERSION__'
31
31
 
32
32
  - name: Setup Node for npm publishing
33
33
  uses: actions/setup-node@v4
@@ -1,7 +1,2 @@
1
- export declare const workflowManagedFiles: readonly [{
2
- readonly relativePath: ".github/workflows/ci.yml";
3
- readonly sourceUrl: URL;
4
- }, {
5
- readonly relativePath: ".github/workflows/release.yml";
6
- readonly sourceUrl: URL;
7
- }];
1
+ import type { ManagedFileDefinition } from '../shared/managedFiles.js';
2
+ export declare const workflowManagedFiles: readonly [ManagedFileDefinition, ManagedFileDefinition];
@@ -1,10 +1,17 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { bunRuntimePolicy } from '../../policy/bunRuntimePolicy.js';
3
+ const BUN_VERSION_TOKEN = '__ANKH_BUN_VERSION__';
1
4
  export const workflowManagedFiles = [
2
- {
3
- relativePath: '.github/workflows/ci.yml',
4
- sourceUrl: new URL('./files/ci.yml', import.meta.url),
5
- },
6
- {
7
- relativePath: '.github/workflows/release.yml',
8
- sourceUrl: new URL('./files/release.yml', import.meta.url),
9
- },
5
+ createWorkflowDefinition('.github/workflows/ci.yml', './files/ci.yml'),
6
+ createWorkflowDefinition('.github/workflows/release.yml', './files/release.yml'),
10
7
  ];
8
+ function createWorkflowDefinition(relativePath, sourcePath) {
9
+ const sourceUrl = new URL(sourcePath, import.meta.url);
10
+ return {
11
+ relativePath,
12
+ render: async () => {
13
+ const template = await readFile(sourceUrl, 'utf8');
14
+ return template.replaceAll(BUN_VERSION_TOKEN, bunRuntimePolicy.version);
15
+ },
16
+ };
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",
@@ -116,9 +116,9 @@
116
116
  "@ankhorage/ankh": "^0.4.0",
117
117
  "@ankhorage/doctor": "0.3.0",
118
118
  "@changesets/cli": "^2.30.0",
119
- "@types/bun": "^1.3.13",
119
+ "@types/bun": "^1.3.14",
120
120
  "@types/node": "^25.2.3",
121
121
  "typescript": "^5.9.3"
122
122
  },
123
- "packageManager": "bun@1.3.13"
123
+ "packageManager": "bun@1.3.14"
124
124
  }