@ankhorage/devtools 1.10.0 → 1.10.2

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.
@@ -3,6 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { applyBunRuntimePolicy } from '../policy/applyBunRuntimePolicy.js';
5
5
  import { nodeRuntimePolicy } from '../policy/bunRuntimePolicy.js';
6
+ import { renderRenovateWorkflowAsync } from '../tools/workflows/renderRenovateWorkflowAsync.js';
6
7
  import { renderWorkflowAsync } from '../tools/workflows/renderWorkflowAsync.js';
7
8
  export async function synchronizeRenovateOwnerAsync(operation, targetDirectory, options = {}) {
8
9
  const target = resolve(targetDirectory);
@@ -52,7 +53,7 @@ async function createManagedDefinitionsAsync(targetDirectory, policy) {
52
53
  },
53
54
  {
54
55
  relativePath: '.github/workflows/renovate.yml',
55
- contents: await renderWorkflowAsync(new URL('../tools/workflows/files/renovate.yml', import.meta.url), workflowPolicy),
56
+ contents: await renderRenovateWorkflowAsync(new URL('../tools/workflows/files/renovate.yml', import.meta.url), targetDirectory, workflowPolicy),
56
57
  },
57
58
  {
58
59
  relativePath: 'README.md',
@@ -1,8 +1,8 @@
1
1
  /***
2
2
  * Build shared Knip configuration while preserving repository-specific discovery.
3
3
  *
4
- * `createKnipConfig` keeps Knip zero-config behavior by default and only emits settings that the
5
- * consumer explicitly provides. Repositories can add narrow entries, project globs, ignored files,
4
+ * `createKnipConfig` keeps Knip zero-config discovery while excluding executable files in
5
+ * Devtools-managed skills. Repositories can add narrow entries, project globs, ignored files,
6
6
  * binaries, dependencies, and workspace overrides without duplicating the shared Knip version.
7
7
  *
8
8
  * `createKnipMonorepoConfig` adds a root workspace plus conventional `packages/*` and `apps/*`
@@ -29,5 +29,7 @@ export interface DevtoolsKnipMonorepoConfigOptions {
29
29
  readonly workspaceGlobs?: string[];
30
30
  readonly workspaces?: Record<string, DevtoolsKnipWorkspaceConfigOptions>;
31
31
  }
32
+ /*** Build shared Knip configuration with the Devtools-managed skill boundary excluded. */
32
33
  export declare function createKnipConfig(options?: DevtoolsKnipConfigOptions): KnipConfig;
34
+ /*** Build shared monorepo Knip configuration with the same managed-skill boundary. */
33
35
  export declare function createKnipMonorepoConfig(options?: DevtoolsKnipMonorepoConfigOptions): KnipConfig;
@@ -1,5 +1,8 @@
1
+ import { MANAGED_SKILL_EXECUTABLE_GLOB } from '../skills/manifest.js';
1
2
  const DEFAULT_MONOREPO_WORKSPACE_GLOBS = ['packages/*', 'apps/*'];
3
+ /*** Build shared Knip configuration with the Devtools-managed skill boundary excluded. */
2
4
  export function createKnipConfig(options = {}) {
5
+ const ignoreFiles = [...new Set([MANAGED_SKILL_EXECUTABLE_GLOB, ...(options.ignoreFiles ?? [])])];
3
6
  return {
4
7
  ...(options.entry === undefined ? {} : { entry: options.entry }),
5
8
  ...(options.project === undefined ? {} : { project: options.project }),
@@ -8,10 +11,11 @@ export function createKnipConfig(options = {}) {
8
11
  ...(options.ignoreDependencies === undefined
9
12
  ? {}
10
13
  : { ignoreDependencies: options.ignoreDependencies }),
11
- ...(options.ignoreFiles === undefined ? {} : { ignoreFiles: options.ignoreFiles }),
14
+ ignoreFiles,
12
15
  ...(options.workspaces === undefined ? {} : { workspaces: options.workspaces }),
13
16
  };
14
17
  }
18
+ /*** Build shared monorepo Knip configuration with the same managed-skill boundary. */
15
19
  export function createKnipMonorepoConfig(options = {}) {
16
20
  const explicitWorkspaces = options.workspaces ?? {};
17
21
  const workspaceGlobs = options.workspaceGlobs ?? [...DEFAULT_MONOREPO_WORKSPACE_GLOBS];
@@ -1,5 +1,6 @@
1
1
  export declare const MANIFEST_PATH = ".agents/.devtools-manifest.json";
2
2
  export declare const SKILLS_ROOT = ".agents/skills";
3
+ export declare const MANAGED_SKILL_EXECUTABLE_GLOB = ".agents/skills/**/scripts/**";
3
4
  interface ManagedSkillManifestEntry {
4
5
  readonly files: Readonly<Record<string, string>>;
5
6
  }
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
3
3
  import { posix, relative, resolve } from 'node:path';
4
4
  export const MANIFEST_PATH = '.agents/.devtools-manifest.json';
5
5
  export const SKILLS_ROOT = '.agents/skills';
6
+ export const MANAGED_SKILL_EXECUTABLE_GLOB = `${SKILLS_ROOT}/**/scripts/**`;
6
7
  const MANIFEST_SCHEMA_VERSION = 1;
7
8
  export function createManifestContents(contentsByPath, devtoolsVersion, skillNames) {
8
9
  const skills = Object.fromEntries(skillNames.map((skillName) => {
@@ -1,9 +1,10 @@
1
1
  import { bunRuntimePolicy, nodeRuntimePolicy } from '../../policy/bunRuntimePolicy.js';
2
+ import { renderRenovateWorkflowAsync } from './renderRenovateWorkflowAsync.js';
2
3
  import { renderWorkflowAsync } from './renderWorkflowAsync.js';
3
4
  export const workflowManagedFiles = [
4
5
  createWorkflowDefinition('.github/workflows/ci.yml', './files/ci.yml'),
5
6
  createWorkflowDefinition('.github/workflows/release.yml', './files/release.yml'),
6
- createWorkflowDefinition('.github/workflows/renovate.yml', './files/renovate.yml'),
7
+ createRenovateWorkflowDefinition(),
7
8
  ];
8
9
  function createWorkflowDefinition(relativePath, sourcePath) {
9
10
  const sourceUrl = new URL(sourcePath, import.meta.url);
@@ -15,3 +16,14 @@ function createWorkflowDefinition(relativePath, sourcePath) {
15
16
  }),
16
17
  };
17
18
  }
19
+ /*** Creates the managed workflow whose immutable digest remains Renovate-owned. */
20
+ function createRenovateWorkflowDefinition() {
21
+ const sourceUrl = new URL('./files/renovate.yml', import.meta.url);
22
+ return {
23
+ relativePath: '.github/workflows/renovate.yml',
24
+ render: async (targetDirectory) => await renderRenovateWorkflowAsync(sourceUrl, targetDirectory, {
25
+ bunVersion: bunRuntimePolicy.version,
26
+ nodeVersion: nodeRuntimePolicy.setupVersion,
27
+ }),
28
+ };
29
+ }
@@ -0,0 +1,3 @@
1
+ import { type WorkflowPolicy } from './renderWorkflowAsync.js';
2
+ /*** Renders the canonical workflow while retaining Renovate's exact trusted digest. */
3
+ export declare function renderRenovateWorkflowAsync(sourceUrl: URL, targetDirectory: string, policy: WorkflowPolicy): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { renderWorkflowAsync } from './renderWorkflowAsync.js';
4
+ /*** Renders the canonical workflow while retaining Renovate's exact trusted digest. */
5
+ export async function renderRenovateWorkflowAsync(sourceUrl, targetDirectory, policy) {
6
+ const template = await renderWorkflowAsync(sourceUrl, policy);
7
+ readSingleRenovateWorkflowDigest(template, 'Devtools workflow template');
8
+ const target = await readOptionalWorkflowAsync(resolve(targetDirectory, RENOVATE_WORKFLOW_RELATIVE_PATH));
9
+ if (target === null)
10
+ return template;
11
+ const digest = readSingleRenovateWorkflowDigest(target, 'target workflow');
12
+ return template.replace(RENOVATE_WORKFLOW_REFERENCE_PATTERN, (_reference, prefix, _templateDigest, suffix) => `${prefix}${digest}${suffix}`);
13
+ }
14
+ /*** Reads one exact immutable digest from the canonical Renovate workflow reference. */
15
+ function readSingleRenovateWorkflowDigest(contents, source) {
16
+ const matches = [...contents.matchAll(RENOVATE_WORKFLOW_REFERENCE_PATTERN)];
17
+ const digest = matches.length === 1 ? matches[0]?.[2] : undefined;
18
+ if (digest === undefined) {
19
+ throw new Error(`Expected exactly one immutable Renovate digest in the ${source}.`);
20
+ }
21
+ return digest;
22
+ }
23
+ /*** Reads an existing managed workflow without treating first-time creation as an error. */
24
+ async function readOptionalWorkflowAsync(path) {
25
+ try {
26
+ return await readFile(path, 'utf8');
27
+ }
28
+ catch (error) {
29
+ if (isNodeError(error) && error.code === 'ENOENT')
30
+ return null;
31
+ throw error;
32
+ }
33
+ }
34
+ /*** Narrows filesystem failures to Node errors with stable codes. */
35
+ function isNodeError(error) {
36
+ return error instanceof Error && 'code' in error;
37
+ }
38
+ const RENOVATE_WORKFLOW_REFERENCE_PATTERN = /^([ \t]*uses:[ \t]+ankhorage\/renovate\/\.github\/workflows\/changeset\.yml@)([0-9a-f]{40})([ \t]*)$/gmu;
39
+ const RENOVATE_WORKFLOW_RELATIVE_PATH = '.github/workflows/renovate.yml';
@@ -1,4 +1,6 @@
1
- export declare function renderWorkflowAsync(sourceUrl: URL, policy: {
1
+ export interface WorkflowPolicy {
2
2
  readonly bunVersion: string;
3
3
  readonly nodeVersion: string;
4
- }): Promise<string>;
4
+ }
5
+ /*** Renders one workflow template with the canonical runtime and Changesets policy. */
6
+ export declare function renderWorkflowAsync(sourceUrl: URL, policy: WorkflowPolicy): Promise<string>;
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { changesetsPolicy } from '../../policy/changesetsPolicy.js';
3
+ /*** Renders one workflow template with the canonical runtime and Changesets policy. */
3
4
  export async function renderWorkflowAsync(sourceUrl, policy) {
4
5
  const template = await readFile(sourceUrl, 'utf8');
5
6
  return template
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",