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

349 lines 17.3 kB
/** * Scope resolver — `--scope user|project` per ADR-4. * * The CLI flag `--scope user` redirects deploys to home-rooted paths * (`~/.<provider>/...`) instead of project-relative paths. This module * holds the per-provider user-scope path map and the helper to detect * the flag in command-line args. * * Per ADR-4 §2 path map. Per ADR-4 §1: `--scope user` and `--scope * project` are mutually exclusive; default is `project`. */ import { homedir } from 'node:os'; import * as path from 'node:path'; /** * User-scope deploy paths per provider per ADR-4 §2. Each path is absolute * (rooted in os.homedir()) so the orchestrator's existing path-join logic * (which calls `path.join(target, relativePath)`) treats them as authoritative * and bypasses the project-relative join. * * `.agents/skills/` and `~/.agents/skills/` deliberately appear for multiple * providers — that's the cross-provider canonical user-scope target. Per * ADR-4 §5 reference counting prevents one provider's removal from breaking * another's deploy at the shared path. */ export const USER_SCOPE_PATHS = { claude: { agents: path.join(homedir(), '.claude', 'agents'), skills: path.join(homedir(), '.claude', 'skills'), commands: path.join(homedir(), '.claude', 'commands'), rules: path.join(homedir(), '.claude', 'rules'), behaviors: path.join(homedir(), '.claude', 'hooks'), }, codex: { // #1158 — Verified against `codex-rs/core-skills/src/loader.rs` and // `docs/providers/skills-paths.md`. Skills primary path is // `~/.agents/skills/` (the cross-provider canonical). Commands deploy at // `~/.codex/prompts/` for operator visibility per ADR-1; codex-rs ships a // static built-in command enum so this directory is not auto-scanned, but // it is the documented location and is where Codex would look if it ever // scanned. Agents/rules at user scope route through AGENTS.md, not a // discrete directory — left empty here. agents: '', skills: path.join(homedir(), '.agents', 'skills'), commands: path.join(homedir(), '.codex', 'prompts'), rules: '', behaviors: '', }, copilot: { // #1160 — Non-applicable for filesystem user-scope discovery. // // VS Code GitHub Copilot's user-scope customization mechanism is the // VS Code settings UI (`settings.json` → `github.copilot.chat.*` keys // and `chat.modeFiles`/`chat.promptFiles` discovery), NOT a home-dir // filesystem scan. `~/.config/github-copilot/` exists on Linux but // stores auth state, not customization markdown files. Workspace // customization is `.github/{copilot-instructions.md,prompts/,agents/, // instructions/}` — see `PROVIDER_PATHS.copilot` in use.ts for the // project-scope deploy that IS verified. // // The paths below remain populated as a "harmless mirror" — deploying // there does not break Copilot, but the runtime won't pick them up. // Operators wanting cross-project Copilot customization should use // VS Code's Settings Sync (settings.json), not AIWG `--scope user`. // // Skills route through ~/.agents/skills/ which IS a documented // cross-provider canonical (Codex/OpenCode scan it; Copilot does // not auto-scan but doesn't refuse either). agents: path.join(homedir(), '.config', 'github-copilot', 'agents'), skills: path.join(homedir(), '.agents', 'skills'), commands: path.join(homedir(), '.config', 'github-copilot', 'prompts'), rules: path.join(homedir(), '.config', 'github-copilot', 'instructions'), behaviors: '', }, cursor: { // #1159 — Cursor is closed-source; user-scope behavior is unverified. // Cursor's documented "User Rules" feature lives in the app's settings // UI (preference data under platform-specific config dirs), not as // markdown files in `~/.cursor/`. Project Rules at `.cursor/rules/*.mdc` // are confirmed (per #1138 PUW-037), but user-scope filesystem // discovery is not documented anywhere AIWG can verify. // // The paths below are the natural home-dir mirrors of the project-scope // layout. Deploying there is harmless (Cursor will ignore the files if // it doesn't scan them) but may also be invisible to the runtime. Until // Cursor publishes user-scope discovery rules, treat this row as // "unverified" — see docs/customization/user-scope-deployment.md. agents: path.join(homedir(), '.cursor', 'agents'), skills: path.join(homedir(), '.cursor', 'skills'), commands: path.join(homedir(), '.cursor', 'commands'), rules: path.join(homedir(), '.cursor', 'rules'), behaviors: path.join(homedir(), '.cursor', 'rules'), }, opencode: { // #1161 — Verified against OpenCode docs (opencode.ai/docs/skills, // opencode.ai/docs/rules, deepwiki.com/sst/opencode/5.7-skills-system). // User-scope discovery roots at ~/.config/opencode/, NOT ~/.opencode/. // Subdirectories use the plural forms per the OpenCode docs convention // (agents/, commands/, skills/, etc.) — though the project-scope loader // accepts both singular and plural via globs (#773, #1107). At user // scope we use the plural form per the documented preference. // // Skills are also scanned by OpenCode at ~/.agents/skills/ (the // cross-provider canonical) and ~/.claude/skills/ — we deploy to // .agents/skills/ to keep one user-scope skills dir shared across // Codex/Copilot/Warp/Factory/OpenCode rather than duplicating into // ~/.config/opencode/skills/ as well. // // Rules at user scope route through ~/.config/opencode/AGENTS.md per // the docs — there is no discrete user-scope rules dir. agents: path.join(homedir(), '.config', 'opencode', 'agents'), skills: path.join(homedir(), '.agents', 'skills'), commands: path.join(homedir(), '.config', 'opencode', 'commands'), rules: '', behaviors: '', }, warp: { // #1162 — Non-applicable for filesystem user-scope discovery. // // Warp's user-scope mechanism is **Warp Drive** (cloud-synced agents, // workflows, notebooks, and rules tied to the operator's Warp account), // not a filesystem scan under `~/.warp/`. The project-scope AIWG // surface for Warp is `WARP.md` aggregation at the project root via // `aiwg-regenerate-warp` — that's the verified discovery path. // // The paths below remain populated as a "harmless mirror" — deploying // there does not break Warp, but the runtime won't pick them up. // Operators wanting cross-project Warp customization should publish // to Warp Drive via the Warp app, not AIWG `--scope user`. // // Skills route through ~/.agents/skills/ as a courtesy (Warp does not // auto-scan it but doesn't refuse either; aligns with the cross- // provider canonical used by Codex/OpenCode). agents: path.join(homedir(), '.warp', 'agents'), skills: path.join(homedir(), '.agents', 'skills'), commands: path.join(homedir(), '.warp', 'commands'), rules: path.join(homedir(), '.warp', 'rules'), behaviors: '', }, windsurf: { // #1163 — Non-applicable for filesystem user-scope discovery. // // Windsurf's user-scope mechanism is **Cascade Memories** (in-app, // managed by the Cascade agent itself, not file-discovered) and // global rules in the Windsurf settings UI. Project-scope discovery // is `.windsurf/{rules,workflows}/` and AGENTS.md aggregation. There // is no documented filesystem scan of `~/.windsurf/` for user-scope // content. // // The paths below remain populated as a "harmless mirror" — deploying // there does not break Windsurf, but the runtime won't pick them up. // Operators wanting cross-project Windsurf customization should use // Cascade Memories (commit a memory via the agent UI) or the // Windsurf settings UI, not AIWG `--scope user`. agents: path.join(homedir(), '.windsurf', 'agents'), skills: path.join(homedir(), '.windsurf', 'skills'), commands: path.join(homedir(), '.windsurf', 'workflows'), rules: path.join(homedir(), '.windsurf', 'rules'), behaviors: '', }, hermes: { agents: '', skills: path.join(homedir(), '.hermes', 'skills'), commands: '', rules: '', behaviors: '', }, openclaw: { agents: path.join(homedir(), '.openclaw', 'agents'), skills: path.join(homedir(), '.openclaw', 'skills'), commands: path.join(homedir(), '.openclaw', 'commands'), rules: path.join(homedir(), '.openclaw', 'rules'), behaviors: path.join(homedir(), '.openclaw', 'behaviors'), }, factory: { // #1164 — Verified against Factory docs (docs.factory.ai/cli/configuration/skills). // Skills primary user-scope path is ~/.factory/skills/, NOT the // cross-provider .agents/skills/ canonical. Factory's docs explicitly say // "for personal use across all projects, you can copy skills to // ~/.factory/skills/skill-name" — there's no public statement that // Factory scans ~/.agents/skills/, so we deploy to the documented path. // // Droids and commands at user scope mirror the project-scope layout // (.factory/droids/ and .factory/commands/). These paths aren't called // out explicitly in the Factory docs as user-scope discovery roots; they // follow the same convention as the skills path. If Factory adds dedicated // user-scope discovery for these, this entry should be revisited. // // Rules at user scope route through AGENTS.md per Factory's // multi-platform AGENTS.md convention — no discrete user-scope rules dir. agents: path.join(homedir(), '.factory', 'droids'), skills: path.join(homedir(), '.factory', 'skills'), commands: path.join(homedir(), '.factory', 'commands'), rules: '', behaviors: '', }, }; /** * Detect the `--scope` flag in a command-line arg list. Returns the resolved * scope; defaults to 'project'. Throws when both `--scope user` and `--scope * project` appear (mutually exclusive per ADR-4 §1). */ export function detectScope(args) { const idx = args.findIndex((a) => a === '--scope'); if (idx === -1) return 'project'; const value = args[idx + 1]; if (value !== 'user' && value !== 'project') { throw new Error(`--scope expected 'user' or 'project', got '${value ?? '(missing)'}'`); } // Check for duplicate --scope flags. const dupIdx = args.findIndex((a, i) => i > idx && a === '--scope'); if (dupIdx !== -1) { throw new Error('--scope appears more than once'); } return value; } /** * The path to the user-scope aiwg.config per ADR-4 §4. Each operator has * one of these per-machine; it tracks user-global deployments. */ export function userScopeConfigPath() { return path.join(homedir(), '.aiwg', 'aiwg.config'); } /** * Resolve the deploy paths for a (provider, scope) pair. For project scope, * returns the project-relative paths from PROVIDER_PATHS (the caller resolves * them against the project dir). For user scope, returns the absolute home- * rooted paths from USER_SCOPE_PATHS. */ export function resolveScopePaths(provider, scope, projectScopePaths) { if (scope === 'project') return projectScopePaths; const userPaths = USER_SCOPE_PATHS[provider]; if (!userPaths) { // Unknown provider — fall back to project paths so the caller doesn't crash. return projectScopePaths; } return userPaths; } /** * Mirror skills deployed under the project-scope skills directory to the * user-scope target. Per ADR-4 §2 the cross-agent canonical user-scope * skills target is `~/.agents/skills/` for codex/copilot/warp/opencode/ * factory; for other providers the user-scope skills dir is per-provider. * * This is an additive copy — the project-scope deploy stays in place; the * user-scope copy is created alongside. Operators get the skills available * across all their projects without re-running aiwg use per project. * * Returns the count of skills mirrored, or 0 when nothing was found. */ export async function mirrorSkillsToUserScope(provider, projectSkillsDir) { const userPaths = USER_SCOPE_PATHS[provider]; if (!userPaths || !userPaths.skills) { return { count: 0, targetDir: '' }; } return mirrorArtifactDir(projectSkillsDir, userPaths.skills); } /** * #1156 Phase 1 — Mirror the full per-provider artifact set (agents, commands, * skills, rules) from project scope to the user-scope target. Additive: the * project-scope deploy stays in place; user-scope copies are created alongside * so the framework is available across every project on the machine. * * `projectPaths` are the relative or absolute paths the caller already resolved * for project-scope deployment. Each one whose user-scope counterpart is * non-empty gets mirrored. Returns per-artifact-type counts, the resolved * user-scope target directories, and the list of entry names that were copied * (so callers can record them in a per-framework manifest for precise remove). */ export async function mirrorToUserScope(provider, projectPaths) { const userPaths = USER_SCOPE_PATHS[provider]; const empty = { count: 0, targetDir: '', entries: [] }; if (!userPaths) { return { agents: empty, skills: empty, commands: empty, rules: empty, behaviors: empty }; } const [agents, skills, commands, rules, behaviors] = await Promise.all([ userPaths.agents ? mirrorArtifactDir(projectPaths.agents, userPaths.agents) : Promise.resolve(empty), userPaths.skills ? mirrorArtifactDir(projectPaths.skills, userPaths.skills) : Promise.resolve(empty), userPaths.commands ? mirrorArtifactDir(projectPaths.commands, userPaths.commands) : Promise.resolve(empty), userPaths.rules ? mirrorArtifactDir(projectPaths.rules, userPaths.rules) : Promise.resolve(empty), userPaths.behaviors ? mirrorArtifactDir(projectPaths.behaviors, userPaths.behaviors) : Promise.resolve(empty), ]); return { agents, skills, commands, rules, behaviors }; } /** * Copy every directory or file under `src` into `dst` (creating `dst` if * needed). Returns the count of top-level entries successfully copied and * the entry names themselves — the names are needed by callers that record * a per-framework manifest at user scope so `aiwg remove --scope user` can * delete only what this deploy created. * * Used by the user-scope mirror to cover both directory-style artifacts * (skills, agents) and file-style artifacts (commands, rules in some * providers). Failures on individual entries are swallowed so a single bad * entry doesn't fail the whole mirror. */ async function mirrorArtifactDir(src, dst) { if (!src || !dst) return { count: 0, targetDir: dst, entries: [] }; const fs = await import('node:fs/promises'); let dirents; try { dirents = await fs.readdir(src, { withFileTypes: true }); } catch { return { count: 0, targetDir: dst, entries: [] }; } await fs.mkdir(dst, { recursive: true }); let count = 0; const entries = []; for (const entry of dirents) { const s = path.join(src, entry.name); const d = path.join(dst, entry.name); try { if (entry.isDirectory()) { await fs.cp(s, d, { recursive: true, force: true }); } else if (entry.isFile()) { await fs.copyFile(s, d); } else { continue; } count++; entries.push(entry.name); } catch { // ignore individual failures } } return { count, targetDir: dst, entries }; } /** * #1156 Phase 1 — OpenClaw is exclusively user-scope. `--scope project` against * OpenClaw is meaningless because all OpenClaw paths are already home-rooted; * silently accepting it would create the false impression that project-scope * deploys are tracked. This helper is called by the use/list/remove handlers * to fail fast with a clear message on `--scope project --provider openclaw`. * * `--scope user --provider openclaw` is a no-op: that's already what OpenClaw * does without the flag. */ export function rejectOpenClawProjectScope(provider, scope) { if (provider === 'openclaw' && scope === 'project') { throw new Error("OpenClaw is exclusively user-scope (~/.openclaw/). '--scope project' is not supported for this provider; omit the flag or pass '--scope user'."); } } //# sourceMappingURL=scope-resolver.js.map