All files / src/skill-builder authoring-provider.ts

82.5% Statements 33/40
76.19% Branches 16/21
87.5% Functions 7/8
84.61% Lines 33/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84            4x 4x     23x     30x 30x 49x 75x     75x 75x 75x 7x 7x   68x     68x 68x 68x 68x       23x 23x       2x 2x 2x     4x       23x 23x 23x           23x       23x 23x 23x     23x 23x     23x                          
import { createHash } from 'node:crypto';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { SkillBuildAuthoringProvider } from './skill-contract.js';
 
const DEFAULT_AUTHORING_SKILL_NAME = 'skill-writer';
const AUTHORING_PROVIDER_ENV = 'WARDEN_SKILL_AUTHORING_ROOT';
 
function hashDirectory(rootDir: string): string {
  const hash = createHash('sha256');
 
  function visit(relativeDir: string): void {
    const absoluteDir = join(rootDir, relativeDir);
    for (const entry of readdirSync(absoluteDir, { withFileTypes: true })
      .sort((a, b) => a.name.localeCompare(b.name))) {
      Iif (entry.name === '.git' || entry.name === 'node_modules') {
        continue;
      }
      const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
      const absolutePath = join(rootDir, relativePath);
      if (entry.isDirectory()) {
        visit(relativePath);
        continue;
      }
      Iif (!entry.isFile()) {
        continue;
      }
      hash.update(relativePath);
      hash.update('\0');
      hash.update(readFileSync(absolutePath));
      hash.update('\0');
    }
  }
 
  visit('');
  return hash.digest('hex');
}
 
function candidateAuthoringSkillRoots(): string[] {
  const fromEnv = process.env[AUTHORING_PROVIDER_ENV];
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
  return [
    fromEnv ? resolve(fromEnv) : undefined,
    join(packageRoot, 'src', 'internal-skills', DEFAULT_AUTHORING_SKILL_NAME),
  ].filter((path): path is string => Boolean(path));
}
 
function authoringSkillName(rootDir: string): string {
  const content = readFileSync(join(rootDir, 'SKILL.md'), 'utf-8');
  const match = /^name:\s*["']?([^"'\n]+)["']?$/m.exec(content);
  return match?.[1]?.trim() || basename(rootDir);
}
 
export function resolveAuthoringProvider(args: {
  authoringSkillRoot?: string;
} = {}): SkillBuildAuthoringProvider {
  const roots = args.authoringSkillRoot
    ? [resolve(args.authoringSkillRoot)]
    : candidateAuthoringSkillRoots();
 
  for (const rootDir of roots) {
    const skillPath = join(rootDir, 'SKILL.md');
    Iif (!existsSync(skillPath)) {
      continue;
    }
    const stat = statSync(skillPath);
    Iif (!stat.isFile()) {
      continue;
    }
    return {
      name: authoringSkillName(rootDir),
      rootDir,
      contentHash: hashDirectory(rootDir),
    };
  }
 
  const searched = roots.map((root) => `- ${root}`).join('\n');
  throw new Error(
    `Unable to find generated-skill authoring provider "${DEFAULT_AUTHORING_SKILL_NAME}". ` +
    `Set ${AUTHORING_PROVIDER_ENV} or ensure the internal provider is packaged in one of:\n${searched}`,
  );
}