UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

259 lines 9.76 kB
/** * `aiwg run skill <name>` — execute a script-bearing skill (#1227). * * Resolves the skill via the artifact index (the same one `aiwg discover` * and `aiwg show` use), reads its `script:` declaration, and dispatches * the entrypoint through the runtime registry. * * Critical CWD invariant: by default, the script runs from the CALLING * project's root, not the skill's source directory. Skill scripts live * at `$AIWG_ROOT/agentic/code/...` but operate on the user's project, so * relative paths inside the script (`.aiwg/`, `src/`, `package.json`) * must resolve into the user's tree. The manifest can override via * `cwd: skill-dir` for the rare case of a script that bundles its own * relative assets, or `cwd: aiwg-root` as an escape hatch. * * Env vars exported to the script: * AIWG_SKILL_DIR absolute path to the skill's source directory * AIWG_PROJECT_ROOT absolute path to the calling project's root * AIWG_ROOT absolute path to the AIWG install root * * Stdin streams in, stdout/stderr stream out, the script's exit code is * the CLI's exit code. No magic wrapping. */ import { spawn } from 'node:child_process'; import { promises as fs } from 'node:fs'; import * as path from 'node:path'; import { resolveRuntime, supportedRuntimes } from './runtime.js'; /** * Resolve the AIWG installation root. Prefers `$AIWG_ROOT` env, falls * back to the channel manager's framework-root resolver. */ async function getAiwgRoot() { if (process.env.AIWG_ROOT) return process.env.AIWG_ROOT; try { const mod = await import('../channel/manager.mjs'); if (typeof mod.getFrameworkRoot === 'function') { const r = await mod.getFrameworkRoot(); return r || null; } } catch { // fall through } return null; } /** * Find a skill entry by name across the framework / project / codebase * graphs. Mirrors the lookup `aiwg show` uses — basename match against * the skill's directory name (skills/<name>/SKILL.md). */ async function findSkillEntry(cwd, name) { const reader = await import('../artifacts/index-reader.js'); const entries = []; for (const g of ['framework', 'project', 'codebase']) { const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g); if (idx) entries.push(...Object.values(idx.entries)); } if (entries.length === 0) { const legacy = reader.loadMetadataIndex(cwd); if (legacy) entries.push(...Object.values(legacy.entries)); } const skills = entries.filter(e => e.type === 'skill'); const needle = name.trim(); // Basename match — skills are conventionally `<dir>/SKILL.md` const matches = skills.filter(e => { const dir = path.basename(path.dirname(e.path)); const stem = path.basename(e.path).replace(/\.[^.]+$/, ''); return dir === needle || stem === needle || e.path === needle; }); if (matches.length === 0) return null; // Prefer one with a script declaration over one without const withScript = matches.find(m => m.script); return withScript ?? matches[0]; } /** * Resolve the skill's source directory to an absolute path. Framework * entries are stored relative to AIWG_ROOT; project entries relative to * the project cwd. */ function resolveSkillDir(entry, cwd, aiwgRoot) { const skillFile = path.isAbsolute(entry.path) ? entry.path : entry.path.startsWith('agentic/code/') && aiwgRoot ? path.join(aiwgRoot, entry.path) : path.join(cwd, entry.path); return path.dirname(skillFile); } /** * Run a skill's script entrypoint. Returns the exit code; the caller * decides whether to `process.exit()` with it. */ export async function runSkill(opts) { const aiwgRoot = await getAiwgRoot(); const entry = await findSkillEntry(opts.cwd, opts.name); if (!entry) { console.error(`Error: no skill matching "${opts.name}" found in the artifact index.`); console.error('Try `aiwg discover "<phrase>"` to find the right name, or run `aiwg index build --graph framework` if the index is stale.'); return 1; } if (!entry.script) { console.error(`Error: skill "${opts.name}" is instructional only — it has no script entrypoint to run.`); console.error(`Read its instructions with: aiwg show skill ${opts.name} --first`); console.error('If you need an executable skill, run `aiwg discover "<phrase>" --json` and choose an entry with "executable": true.'); return 1; } const skillDir = resolveSkillDir(entry, opts.cwd, aiwgRoot); const entrypointPath = path.resolve(skillDir, entry.script.entrypoint); try { await fs.access(entrypointPath); } catch { console.error(`Error: skill entrypoint not found: ${entrypointPath}`); console.error('The skill manifest declares this entrypoint but the file is missing. Check the skill source.'); return 1; } const invocation = await resolveRuntime(entry.script.runtime, entrypointPath); if (!invocation) { console.error(`Error: unknown runtime "${entry.script.runtime}" for skill "${opts.name}".`); console.error(`Supported runtimes: ${supportedRuntimes().join(', ')}`); return 1; } // Resolve CWD per the policy: project-root (default), skill-dir, aiwg-root, // or an explicit CLI override. let runCwd; if (opts.cwdOverride) { runCwd = path.resolve(opts.cwdOverride); } else { switch (entry.script.cwd ?? 'project-root') { case 'skill-dir': runCwd = skillDir; break; case 'aiwg-root': runCwd = aiwgRoot ?? opts.cwd; break; case 'project-root': default: runCwd = opts.cwd; break; } } const env = { ...process.env, AIWG_SKILL_DIR: skillDir, AIWG_PROJECT_ROOT: opts.cwd, ...(aiwgRoot ? { AIWG_ROOT: aiwgRoot } : {}), }; const fullArgs = [...invocation.prefixArgs, entrypointPath, ...opts.args]; return new Promise((resolve) => { const child = spawn(invocation.command, fullArgs, { cwd: runCwd, env, stdio: 'inherit', }); child.on('error', (err) => { console.error(`Error: failed to spawn ${invocation.command}: ${err.message}`); resolve(127); }); child.on('exit', (code, signal) => { if (signal) { // Match shell convention: signal-killed → 128 + signal number const signalNum = process.binding?.('constants')?.[signal] ?? 0; resolve(128 + signalNum); } else { resolve(code ?? 0); } }); }); } /** * CLI entrypoint for `aiwg run skill <name> [-- args...]`. * * Argument convention: * aiwg run skill <name> [--cwd <path>] [-- <args forwarded to script>] * * Anything after `--` is verbatim-forwarded. If no `--`, all args after * the skill name are forwarded. */ export async function main(args) { // First positional must be the kind ("skill" — reserved for future kinds). if (args.length === 0) { printUsage(); return 1; } const kind = args[0]; if (kind === '--help' || kind === '-h') { printUsage(); return 0; } if (kind !== 'skill') { console.error(`Error: unknown kind "${kind}". Only "skill" is supported.`); printUsage(); return 1; } if (args.length < 2) { console.error('Error: missing skill name.'); printUsage(); return 1; } const name = args[1]; // #1231 — `aiwg run skill --help` should print usage, not search the // index for a skill named "--help". if (name === '--help' || name === '-h') { printUsage(); return 0; } // Split remaining args at the first `--` separator if present. const rest = args.slice(2); let cwdOverride; let scriptArgs; const sepIndex = rest.indexOf('--'); let head; if (sepIndex >= 0) { head = rest.slice(0, sepIndex); scriptArgs = rest.slice(sepIndex + 1); } else { head = []; scriptArgs = rest; } for (let i = 0; i < head.length; i++) { if (head[i] === '--cwd' && i + 1 < head.length) { cwdOverride = head[i + 1]; i++; } else { // Unknown flag before `--` — treat as script arg only when no `--` // separator was provided, otherwise error out so typos surface. if (sepIndex >= 0) { console.error(`Error: unknown flag "${head[i]}" before \`--\`.`); printUsage(); return 1; } } } return runSkill({ cwd: process.cwd(), name, args: scriptArgs, cwdOverride, }); } function printUsage() { console.log('Usage: aiwg run skill <name> [--cwd <path>] [-- <args...>]'); console.log(''); console.log('Examples:'); console.log(' aiwg run skill voice-apply -- --voice technical-authority --input draft.md'); console.log(' aiwg run skill template-engine -- render adr-template.md --vars vars.yaml'); console.log(' aiwg run skill ai-pattern-detection -- --path docs/'); console.log(''); console.log('The script runs from the project root by default. Use `--cwd <path>` to override.'); console.log('Tip: `aiwg discover "<phrase>" --json` shows which skills have an "executable: true" flag.'); } //# sourceMappingURL=run.js.map