Skip to content

Commit eeef1bf

Browse files
committed
implement registry manifest generation and CLI integration
1 parent 445a673 commit eeef1bf

9 files changed

Lines changed: 371 additions & 0 deletions

File tree

packages/installer/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@tanstack/installer",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"bin": {
7+
"tanstack-agents": "dist/cli.js"
8+
},
9+
"scripts": {
10+
"build": "tsc -p tsconfig.json",
11+
"dev": "tsx src/cli.ts"
12+
},
13+
"devDependencies": {
14+
"typescript": "^5.5.0",
15+
"tsx": "^4.16.0",
16+
"yaml": "^2.5.0"
17+
}
18+
}

packages/installer/src/cli.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import yaml from "yaml";
4+
5+
export function readCatalog(repoRoot: string) {
6+
const p = path.join(repoRoot, "catalog.yaml");
7+
return yaml.parse(fs.readFileSync(p, "utf8"));
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function resolveLibrariesFromDeps(deps: Record<string, string>, catalog: any): Set<string> {
2+
const libs = new Set<string>();
3+
for (const [libId, lib] of Object.entries<any>(catalog.libraries)) {
4+
for (const pkg of lib.packages ?? []) {
5+
if (deps[pkg]) libs.add(libId);
6+
}
7+
}
8+
return libs;
9+
}

packages/installer/tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"moduleResolution": "Bundler",
6+
"outDir": "dist",
7+
"rootDir": "src",
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true
11+
},
12+
"include": ["src"]
13+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
type RegistryManifest = {
5+
schema: 1;
6+
generated_at: string;
7+
compiler: { name: string; version: string };
8+
skills: Array<{
9+
lib: string;
10+
version: string;
11+
skill_dir: string;
12+
skill_md: string;
13+
index_json: string;
14+
agent_md: string;
15+
}>;
16+
agents: Array<{ id: string; path: string }>;
17+
};
18+
19+
function listDirs(p: string) {
20+
if (!fs.existsSync(p)) return [];
21+
return fs
22+
.readdirSync(p, { withFileTypes: true })
23+
.filter((d) => d.isDirectory())
24+
.map((d) => d.name);
25+
}
26+
27+
export function buildRegistryManifest(args: { repoRoot: string; compilerVersion: string }) {
28+
const registryDir = path.join(args.repoRoot, "registry");
29+
const skillsRoot = path.join(registryDir, "skills");
30+
const agentsRoot = path.join(registryDir, "agents");
31+
const manifestPath = path.join(registryDir, "manifest.json");
32+
33+
const skills: RegistryManifest["skills"] = [];
34+
const agents: RegistryManifest["agents"] = [];
35+
36+
// agents inventory
37+
if (fs.existsSync(agentsRoot)) {
38+
for (const f of fs.readdirSync(agentsRoot)) {
39+
if (!f.endsWith(".md")) continue;
40+
const id = f.replace(/\.md$/, "");
41+
agents.push({ id, path: path.posix.join("registry", "agents", f) });
42+
}
43+
}
44+
45+
// skills inventory
46+
for (const lib of listDirs(skillsRoot)) {
47+
const libRoot = path.join(skillsRoot, lib);
48+
for (const version of listDirs(libRoot)) {
49+
const skillDir = path.join(libRoot, version);
50+
const skillMd = path.join(skillDir, "SKILL.md");
51+
const indexJson = path.join(skillDir, "index.json");
52+
53+
if (!fs.existsSync(skillMd) || !fs.existsSync(indexJson)) continue;
54+
55+
const agentMd = path.posix.join("registry", "agents", `tanstack-${lib}.md`);
56+
57+
skills.push({
58+
lib,
59+
version,
60+
skill_dir: path.posix.join("registry", "skills", lib, version),
61+
skill_md: path.posix.join("registry", "skills", lib, version, "SKILL.md"),
62+
index_json: path.posix.join("registry", "skills", lib, version, "index.json"),
63+
agent_md: agentMd,
64+
});
65+
}
66+
}
67+
68+
skills.sort((a, b) => (a.lib + "@" + a.version).localeCompare(b.lib + "@" + b.version));
69+
agents.sort((a, b) => a.id.localeCompare(b.id));
70+
71+
const out: RegistryManifest = {
72+
schema: 1,
73+
generated_at: new Date().toISOString(),
74+
compiler: { name: "@tanstack/skillc", version: args.compilerVersion },
75+
skills,
76+
agents,
77+
};
78+
79+
fs.mkdirSync(registryDir, { recursive: true });
80+
fs.writeFileSync(manifestPath, JSON.stringify(out, null, 2) + "\n");
81+
}

packages/skillc/src/cli.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { buildTopicsIndex } from "./buildTopics.js";
77
import { renderSkillMd } from "./renderSkill.js";
88
import { renderLibraryAgent } from "./renderLibraryAgent.js";
99
import { renderTanstackAgent } from "./renderTanstackAgent.js";
10+
import { buildRegistryManifest } from "./buildManifest.js";
1011

1112
function arg(name: string) {
1213
const idx = process.argv.indexOf(`--${name}`);
@@ -94,6 +95,12 @@ async function main() {
9495
return;
9596
}
9697

98+
if (cmd === "build-manifest") {
99+
buildRegistryManifest({ repoRoot: process.cwd(), compilerVersion: "0.0.0" });
100+
return;
101+
}
102+
103+
97104
throw new Error(`Unknown command: ${cmd}`);
98105
}
99106

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

registry/manifest.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"schema": 1,
3+
"generated_at": "2026-02-16T19:10:48.191Z",
4+
"compiler": {
5+
"name": "@tanstack/skillc",
6+
"version": "0.0.0"
7+
},
8+
"skills": [
9+
{
10+
"lib": "query",
11+
"version": "main",
12+
"skill_dir": "registry/skills/query/main",
13+
"skill_md": "registry/skills/query/main/SKILL.md",
14+
"index_json": "registry/skills/query/main/index.json",
15+
"agent_md": "registry/agents/tanstack-query.md"
16+
},
17+
{
18+
"lib": "router",
19+
"version": "main",
20+
"skill_dir": "registry/skills/router/main",
21+
"skill_md": "registry/skills/router/main/SKILL.md",
22+
"index_json": "registry/skills/router/main/index.json",
23+
"agent_md": "registry/agents/tanstack-router.md"
24+
}
25+
],
26+
"agents": [
27+
{
28+
"id": "tanstack",
29+
"path": "registry/agents/tanstack.md"
30+
},
31+
{
32+
"id": "tanstack-query",
33+
"path": "registry/agents/tanstack-query.md"
34+
},
35+
{
36+
"id": "tanstack-router",
37+
"path": "registry/agents/tanstack-router.md"
38+
}
39+
]
40+
}

0 commit comments

Comments
 (0)