|
| 1 | +import fs from "node:fs"; |
| 2 | +import path from "node:path"; |
| 3 | +import os from "node:os"; |
| 4 | +import { fileURLToPath } from "node:url"; |
| 5 | + |
| 6 | +type Manifest = { |
| 7 | + schema: number; |
| 8 | + skills: Array<{ |
| 9 | + lib: string; |
| 10 | + version: string; |
| 11 | + skill_dir: string; |
| 12 | + agent_md: string; |
| 13 | + }>; |
| 14 | + agents: Array<{ id: string; path: string }>; |
| 15 | +}; |
| 16 | + |
| 17 | +function arg(name: string) { |
| 18 | + const idx = process.argv.indexOf(`--${name}`); |
| 19 | + return idx >= 0 ? process.argv[idx + 1] : undefined; |
| 20 | +} |
| 21 | + |
| 22 | +function readJson<T>(p: string): T { |
| 23 | + return JSON.parse(fs.readFileSync(p, "utf8")); |
| 24 | +} |
| 25 | + |
| 26 | +function ensureDir(p: string) { |
| 27 | + fs.mkdirSync(p, { recursive: true }); |
| 28 | +} |
| 29 | + |
| 30 | +function copyDir(src: string, dst: string) { |
| 31 | + ensureDir(dst); |
| 32 | + fs.cpSync(src, dst, { recursive: true }); |
| 33 | +} |
| 34 | + |
| 35 | +function resolveProjectDir(input: string): string { |
| 36 | + // Node does not expand "~" automatically |
| 37 | + const expanded = |
| 38 | + input === "~" |
| 39 | + ? os.homedir() |
| 40 | + : input.startsWith("~/") |
| 41 | + ? path.join(os.homedir(), input.slice(2)) |
| 42 | + : input; |
| 43 | + |
| 44 | + return path.resolve(expanded); |
| 45 | +} |
| 46 | + |
| 47 | +function getRepoRootFromThisFile(): string { |
| 48 | + // dist/cli.js -> dist -> packages/installer -> packages -> repo root |
| 49 | + const __filename = fileURLToPath(import.meta.url); |
| 50 | + const __dirname = path.dirname(__filename); |
| 51 | + return path.resolve(__dirname, "..", "..", ".."); |
| 52 | +} |
| 53 | + |
| 54 | +function getAllDeps(pkg: any): Record<string, string> { |
| 55 | + return { |
| 56 | + ...(pkg.dependencies ?? {}), |
| 57 | + ...(pkg.devDependencies ?? {}), |
| 58 | + ...(pkg.peerDependencies ?? {}), |
| 59 | + }; |
| 60 | +} |
| 61 | + |
| 62 | +// MVP mapping (Step 5). Replace with catalog.yaml mapping later. |
| 63 | +function resolveLibsFromDeps(deps: Record<string, string>): Set<"query" | "router"> { |
| 64 | + const libs = new Set<"query" | "router">(); |
| 65 | + |
| 66 | + if (deps["@tanstack/react-query"] || deps["@tanstack/query-core"]) libs.add("query"); |
| 67 | + |
| 68 | + if ( |
| 69 | + deps["@tanstack/router"] || |
| 70 | + deps["@tanstack/react-router"] || |
| 71 | + deps["@tanstack/router-core"] || |
| 72 | + deps["@tanstack/start"] || |
| 73 | + deps["@tanstack/react-start"] |
| 74 | + ) libs.add("router"); |
| 75 | + |
| 76 | + return libs; |
| 77 | +} |
| 78 | + |
| 79 | +function main() { |
| 80 | + const cmd = process.argv[2]; |
| 81 | + if (cmd !== "install") throw new Error(`Unknown command: ${cmd}`); |
| 82 | + |
| 83 | + const projectArg = arg("project"); |
| 84 | + if (!projectArg) throw new Error("--project is required"); |
| 85 | + |
| 86 | + const projectDir = resolveProjectDir(projectArg); |
| 87 | + |
| 88 | + const pkgPath = path.join(projectDir, "package.json"); |
| 89 | + if (!fs.existsSync(pkgPath)) { |
| 90 | + throw new Error( |
| 91 | + `No package.json found at:\n${pkgPath}\n\n` + |
| 92 | + `Create a project there (pnpm init -y) or pass the correct --project path.` |
| 93 | + ); |
| 94 | + } |
| 95 | + |
| 96 | + // Find TanStack/agents repo root regardless of CWD |
| 97 | + const repoRoot = getRepoRootFromThisFile(); |
| 98 | + |
| 99 | + const manifestPath = path.join(repoRoot, "registry", "manifest.json"); |
| 100 | + if (!fs.existsSync(manifestPath)) { |
| 101 | + throw new Error( |
| 102 | + `Missing registry/manifest.json at:\n${manifestPath}\n\n` + |
| 103 | + `Generate it:\n` + |
| 104 | + ` node ${path.join(repoRoot, "packages/skillc/dist/cli.js")} build-manifest` |
| 105 | + ); |
| 106 | + } |
| 107 | + |
| 108 | + const manifest = readJson<Manifest>(manifestPath); |
| 109 | + |
| 110 | + // Determine which libs to install from consumer package.json |
| 111 | + const pkg = readJson<any>(pkgPath); |
| 112 | + const deps = getAllDeps(pkg); |
| 113 | + const libs = resolveLibsFromDeps(deps); |
| 114 | + |
| 115 | + // Step 5 policy: install "main" builds |
| 116 | + const version = "main"; |
| 117 | + |
| 118 | + const destAgentsDir = path.join(projectDir, ".agents", "agents"); |
| 119 | + const destSkillsDir = path.join(projectDir, ".agents", "skills"); |
| 120 | + ensureDir(destAgentsDir); |
| 121 | + ensureDir(destSkillsDir); |
| 122 | + |
| 123 | + // Always install the top-level tanstack agent if present |
| 124 | + const tanstackAgent = manifest.agents.find((a) => a.id === "tanstack"); |
| 125 | + if (tanstackAgent) { |
| 126 | + fs.copyFileSync( |
| 127 | + path.join(repoRoot, tanstackAgent.path), |
| 128 | + path.join(destAgentsDir, "tanstack.md") |
| 129 | + ); |
| 130 | + } |
| 131 | + |
| 132 | + const installed: Record<string, any> = {}; |
| 133 | + |
| 134 | + for (const lib of libs) { |
| 135 | + const skill = manifest.skills.find((s) => s.lib === lib && s.version === version); |
| 136 | + if (!skill) { |
| 137 | + throw new Error( |
| 138 | + `Registry missing ${lib}@${version}.\n` + |
| 139 | + `Expected:\n` + |
| 140 | + ` registry/skills/${lib}/${version}/SKILL.md\n\n` + |
| 141 | + `Generate it (build-topics for ${lib} @ ${version}) and rebuild manifest.` |
| 142 | + ); |
| 143 | + } |
| 144 | + |
| 145 | + // copy library agent |
| 146 | + const agentFileName = `tanstack-${lib}.md`; |
| 147 | + fs.copyFileSync( |
| 148 | + path.join(repoRoot, skill.agent_md), |
| 149 | + path.join(destAgentsDir, agentFileName) |
| 150 | + ); |
| 151 | + |
| 152 | + // copy skill directory |
| 153 | + const srcSkillDir = path.join(repoRoot, skill.skill_dir); |
| 154 | + const dstSkillDir = path.join(destSkillsDir, lib, version); |
| 155 | + copyDir(srcSkillDir, dstSkillDir); |
| 156 | + |
| 157 | + installed[lib] = { |
| 158 | + lib, |
| 159 | + version, |
| 160 | + agent_path: path.posix.join(".agents", "agents", agentFileName), |
| 161 | + skill_dir: path.posix.join(".agents", "skills", lib, version), |
| 162 | + }; |
| 163 | + } |
| 164 | + |
| 165 | + // write lockfile |
| 166 | + const lockPath = path.join(projectDir, "skills.lock.json"); |
| 167 | + fs.writeFileSync( |
| 168 | + lockPath, |
| 169 | + JSON.stringify( |
| 170 | + { |
| 171 | + schema: 1, |
| 172 | + registry: { type: "local", repoRoot }, |
| 173 | + installed, |
| 174 | + }, |
| 175 | + null, |
| 176 | + 2 |
| 177 | + ) + "\n" |
| 178 | + ); |
| 179 | + |
| 180 | + console.log(`Installed: ${Array.from(libs).join(", ") || "(none)"}`); |
| 181 | +} |
| 182 | + |
| 183 | +main(); |
0 commit comments