@tanstack/cli 0.60.1 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +266 -11
- package/dist/command-line.js +103 -8
- package/dist/discovery.js +144 -0
- package/dist/options.js +35 -2
- package/dist/types/command-line.d.ts +7 -0
- package/dist/types/{mcp/types.d.ts → discovery.d.ts} +23 -75
- package/dist/types/types.d.ts +1 -2
- package/dist/types/ui-prompts.d.ts +5 -0
- package/dist/ui-prompts.js +26 -0
- package/package.json +6 -5
- package/skills/CHANGELOG.md +18 -0
- package/skills/add-addons-existing-app/SKILL.md +113 -0
- package/skills/choose-ecosystem-integrations/SKILL.md +140 -0
- package/skills/choose-ecosystem-integrations/references/authentication-providers.md +19 -0
- package/skills/choose-ecosystem-integrations/references/data-layer-providers.md +20 -0
- package/skills/choose-ecosystem-integrations/references/deployment-targets.md +19 -0
- package/skills/create-app-scaffold/SKILL.md +132 -0
- package/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +34 -0
- package/skills/create-app-scaffold/references/deployment-providers.md +19 -0
- package/skills/create-app-scaffold/references/framework-adapters.md +17 -0
- package/skills/create-app-scaffold/references/toolchains.md +17 -0
- package/skills/maintain-custom-addons-dev-watch/SKILL.md +118 -0
- package/skills/query-docs-library-metadata/SKILL.md +85 -0
- package/skills/query-docs-library-metadata/references/discovery-command-output-schemas.md +70 -0
- package/CHANGELOG.md +0 -787
- package/dist/mcp/api.js +0 -31
- package/dist/mcp/tools.js +0 -250
- package/dist/mcp/types.js +0 -37
- package/dist/mcp.js +0 -181
- package/dist/types/mcp/api.d.ts +0 -4
- package/dist/types/mcp/tools.d.ts +0 -2
- package/dist/types/mcp.d.ts +0 -5
- package/playwright-report/index.html +0 -85
- package/playwright.config.ts +0 -21
- package/src/bin.ts +0 -15
- package/src/cli.ts +0 -767
- package/src/command-line.ts +0 -473
- package/src/dev-watch.ts +0 -564
- package/src/file-syncer.ts +0 -263
- package/src/index.ts +0 -21
- package/src/mcp/api.ts +0 -42
- package/src/mcp/tools.ts +0 -323
- package/src/mcp/types.ts +0 -46
- package/src/mcp.ts +0 -263
- package/src/options.ts +0 -234
- package/src/types.ts +0 -28
- package/src/ui-environment.ts +0 -74
- package/src/ui-prompts.ts +0 -355
- package/src/utils.ts +0 -30
- package/test-results/.last-run.json +0 -4
- package/tests/command-line.test.ts +0 -622
- package/tests/index.test.ts +0 -9
- package/tests/mcp.test.ts +0 -225
- package/tests/options.test.ts +0 -216
- package/tests/setupVitest.ts +0 -6
- package/tests/ui-environment.test.ts +0 -97
- package/tests/ui-prompts.test.ts +0 -205
- package/tests-e2e/addons-smoke.spec.ts +0 -31
- package/tests-e2e/create-smoke.spec.ts +0 -39
- package/tests-e2e/helpers.ts +0 -526
- package/tests-e2e/matrix-opportunistic.spec.ts +0 -142
- package/tests-e2e/router-only-smoke.spec.ts +0 -68
- package/tests-e2e/solid-smoke.spec.ts +0 -25
- package/tests-e2e/templates-smoke.spec.ts +0 -52
- package/tsconfig.json +0 -17
- package/vitest.config.js +0 -8
package/src/file-syncer.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import crypto from 'node:crypto'
|
|
4
|
-
import * as diff from 'diff'
|
|
5
|
-
|
|
6
|
-
export interface FileUpdate {
|
|
7
|
-
path: string
|
|
8
|
-
diff?: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface SyncResult {
|
|
12
|
-
updated: Array<FileUpdate>
|
|
13
|
-
skipped: Array<string>
|
|
14
|
-
created: Array<string>
|
|
15
|
-
deleted: Array<string>
|
|
16
|
-
sourceFiles: Array<string>
|
|
17
|
-
errors: Array<string>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface SyncOptions {
|
|
21
|
-
deleteRemoved?: boolean
|
|
22
|
-
previousSourceFiles?: Set<string>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class FileSyncer {
|
|
26
|
-
async sync(
|
|
27
|
-
sourceDir: string,
|
|
28
|
-
targetDir: string,
|
|
29
|
-
options?: SyncOptions,
|
|
30
|
-
): Promise<SyncResult> {
|
|
31
|
-
const result: SyncResult = {
|
|
32
|
-
updated: [],
|
|
33
|
-
skipped: [],
|
|
34
|
-
created: [],
|
|
35
|
-
deleted: [],
|
|
36
|
-
sourceFiles: [],
|
|
37
|
-
errors: [],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Ensure directories exist
|
|
41
|
-
if (!fs.existsSync(sourceDir)) {
|
|
42
|
-
throw new Error(`Source directory does not exist: ${sourceDir}`)
|
|
43
|
-
}
|
|
44
|
-
if (!fs.existsSync(targetDir)) {
|
|
45
|
-
throw new Error(`Target directory does not exist: ${targetDir}`)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Walk through source directory and sync files
|
|
49
|
-
await this.syncDirectory(sourceDir, targetDir, sourceDir, result)
|
|
50
|
-
|
|
51
|
-
if (options?.deleteRemoved && options.previousSourceFiles) {
|
|
52
|
-
const currentSourceFileSet = new Set(result.sourceFiles)
|
|
53
|
-
await this.deleteRemovedFiles(
|
|
54
|
-
targetDir,
|
|
55
|
-
options.previousSourceFiles,
|
|
56
|
-
currentSourceFileSet,
|
|
57
|
-
result,
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return result
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private async syncDirectory(
|
|
65
|
-
currentPath: string,
|
|
66
|
-
targetBase: string,
|
|
67
|
-
sourceBase: string,
|
|
68
|
-
result: SyncResult,
|
|
69
|
-
): Promise<void> {
|
|
70
|
-
const entries = await fs.promises.readdir(currentPath, {
|
|
71
|
-
withFileTypes: true,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
for (const entry of entries) {
|
|
75
|
-
const sourcePath = path.join(currentPath, entry.name)
|
|
76
|
-
const relativePath = path.relative(sourceBase, sourcePath)
|
|
77
|
-
const targetPath = path.join(targetBase, relativePath)
|
|
78
|
-
|
|
79
|
-
// Skip certain directories
|
|
80
|
-
if (entry.isDirectory()) {
|
|
81
|
-
if (this.shouldSkipDirectory(entry.name)) {
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Ensure target directory exists
|
|
86
|
-
if (!fs.existsSync(targetPath)) {
|
|
87
|
-
await fs.promises.mkdir(targetPath, { recursive: true })
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Recursively sync subdirectory
|
|
91
|
-
await this.syncDirectory(sourcePath, targetBase, sourceBase, result)
|
|
92
|
-
} else if (entry.isFile()) {
|
|
93
|
-
// Skip certain files
|
|
94
|
-
if (this.shouldSkipFile(entry.name)) {
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
result.sourceFiles.push(relativePath)
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const shouldUpdate = await this.shouldUpdateFile(
|
|
102
|
-
sourcePath,
|
|
103
|
-
targetPath,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if (shouldUpdate) {
|
|
107
|
-
// Check if file exists to generate diff
|
|
108
|
-
let fileDiff: string | undefined
|
|
109
|
-
const targetExists = fs.existsSync(targetPath)
|
|
110
|
-
|
|
111
|
-
if (targetExists) {
|
|
112
|
-
// Generate diff for existing files
|
|
113
|
-
const oldContent = await fs.promises.readFile(targetPath, 'utf-8')
|
|
114
|
-
const newContent = await fs.promises.readFile(sourcePath, 'utf-8')
|
|
115
|
-
|
|
116
|
-
const changes = diff.createPatch(
|
|
117
|
-
relativePath,
|
|
118
|
-
oldContent,
|
|
119
|
-
newContent,
|
|
120
|
-
'Previous',
|
|
121
|
-
'Current',
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
// Only include diff if there are actual changes
|
|
125
|
-
if (changes && changes.split('\n').length > 5) {
|
|
126
|
-
fileDiff = changes
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Copy file
|
|
131
|
-
await fs.promises.copyFile(sourcePath, targetPath)
|
|
132
|
-
|
|
133
|
-
// Touch file to trigger dev server reload
|
|
134
|
-
const now = new Date()
|
|
135
|
-
await fs.promises.utimes(targetPath, now, now)
|
|
136
|
-
|
|
137
|
-
if (!targetExists) {
|
|
138
|
-
result.created.push(relativePath)
|
|
139
|
-
} else {
|
|
140
|
-
result.updated.push({
|
|
141
|
-
path: relativePath,
|
|
142
|
-
diff: fileDiff,
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
result.skipped.push(relativePath)
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
result.errors.push(
|
|
150
|
-
`${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private async shouldUpdateFile(
|
|
158
|
-
sourcePath: string,
|
|
159
|
-
targetPath: string,
|
|
160
|
-
): Promise<boolean> {
|
|
161
|
-
// If target doesn't exist, definitely update
|
|
162
|
-
if (!fs.existsSync(targetPath)) {
|
|
163
|
-
return true
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Compare file sizes first (quick check)
|
|
167
|
-
const [sourceStats, targetStats] = await Promise.all([
|
|
168
|
-
fs.promises.stat(sourcePath),
|
|
169
|
-
fs.promises.stat(targetPath),
|
|
170
|
-
])
|
|
171
|
-
|
|
172
|
-
if (sourceStats.size !== targetStats.size) {
|
|
173
|
-
return true
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Compare MD5 hashes for content
|
|
177
|
-
const [sourceHash, targetHash] = await Promise.all([
|
|
178
|
-
this.calculateHash(sourcePath),
|
|
179
|
-
this.calculateHash(targetPath),
|
|
180
|
-
])
|
|
181
|
-
|
|
182
|
-
return sourceHash !== targetHash
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private async calculateHash(filePath: string): Promise<string> {
|
|
186
|
-
return new Promise((resolve, reject) => {
|
|
187
|
-
const hash = crypto.createHash('md5')
|
|
188
|
-
const stream = fs.createReadStream(filePath)
|
|
189
|
-
|
|
190
|
-
stream.on('data', (data) => hash.update(data))
|
|
191
|
-
stream.on('end', () => resolve(hash.digest('hex')))
|
|
192
|
-
stream.on('error', reject)
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private shouldSkipDirectory(name: string): boolean {
|
|
197
|
-
const skipDirs = [
|
|
198
|
-
'node_modules',
|
|
199
|
-
'.git',
|
|
200
|
-
'dist',
|
|
201
|
-
'build',
|
|
202
|
-
'.next',
|
|
203
|
-
'.nuxt',
|
|
204
|
-
'.cache',
|
|
205
|
-
'.tmp-dev',
|
|
206
|
-
'coverage',
|
|
207
|
-
'.turbo',
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
return skipDirs.includes(name) || name.startsWith('.')
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private shouldSkipFile(name: string): boolean {
|
|
214
|
-
const skipFiles = [
|
|
215
|
-
'.DS_Store',
|
|
216
|
-
'Thumbs.db',
|
|
217
|
-
'desktop.ini',
|
|
218
|
-
'.cta.json', // Skip .cta.json as it contains framework ID that changes each build
|
|
219
|
-
]
|
|
220
|
-
|
|
221
|
-
const skipExtensions = ['.log', '.lock', '.pid', '.seed', '.sqlite']
|
|
222
|
-
|
|
223
|
-
if (skipFiles.includes(name)) {
|
|
224
|
-
return true
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const ext = path.extname(name).toLowerCase()
|
|
228
|
-
return skipExtensions.includes(ext)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private async deleteRemovedFiles(
|
|
232
|
-
targetDir: string,
|
|
233
|
-
previousSourceFiles: Set<string>,
|
|
234
|
-
currentSourceFiles: Set<string>,
|
|
235
|
-
result: SyncResult,
|
|
236
|
-
): Promise<void> {
|
|
237
|
-
for (const relativePath of previousSourceFiles) {
|
|
238
|
-
if (currentSourceFiles.has(relativePath)) {
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const targetPath = path.join(targetDir, relativePath)
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
if (!fs.existsSync(targetPath)) {
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const stats = await fs.promises.stat(targetPath)
|
|
250
|
-
if (!stats.isFile()) {
|
|
251
|
-
continue
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
await fs.promises.unlink(targetPath)
|
|
255
|
-
result.deleted.push(relativePath)
|
|
256
|
-
} catch (error) {
|
|
257
|
-
result.errors.push(
|
|
258
|
-
`${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { pathToFileURL } from 'node:url'
|
|
2
|
-
import {
|
|
3
|
-
createReactFrameworkDefinition,
|
|
4
|
-
createSolidFrameworkDefinition,
|
|
5
|
-
} from '@tanstack/create'
|
|
6
|
-
|
|
7
|
-
import { cli } from './cli.js'
|
|
8
|
-
|
|
9
|
-
export { cli }
|
|
10
|
-
|
|
11
|
-
const entryPath = process.argv[1]
|
|
12
|
-
if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
13
|
-
cli({
|
|
14
|
-
name: 'tanstack',
|
|
15
|
-
appName: 'TanStack',
|
|
16
|
-
frameworkDefinitionInitializers: [
|
|
17
|
-
createReactFrameworkDefinition,
|
|
18
|
-
createSolidFrameworkDefinition,
|
|
19
|
-
],
|
|
20
|
-
})
|
|
21
|
-
}
|
package/src/mcp/api.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { LibrariesResponseSchema, PartnersResponseSchema } from './types.js'
|
|
2
|
-
import type { LibrariesResponse, PartnersResponse } from './types.js'
|
|
3
|
-
|
|
4
|
-
const TANSTACK_API_BASE = 'https://tanstack.com/api/data'
|
|
5
|
-
|
|
6
|
-
export async function fetchLibraries(): Promise<LibrariesResponse> {
|
|
7
|
-
const response = await fetch(`${TANSTACK_API_BASE}/libraries`)
|
|
8
|
-
if (!response.ok) {
|
|
9
|
-
throw new Error(`Failed to fetch libraries: ${response.statusText}`)
|
|
10
|
-
}
|
|
11
|
-
const data = await response.json()
|
|
12
|
-
return LibrariesResponseSchema.parse(data)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function fetchPartners(): Promise<PartnersResponse> {
|
|
16
|
-
const response = await fetch(`${TANSTACK_API_BASE}/partners`)
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
throw new Error(`Failed to fetch partners: ${response.statusText}`)
|
|
19
|
-
}
|
|
20
|
-
const data = await response.json()
|
|
21
|
-
return PartnersResponseSchema.parse(data)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function fetchDocContent(
|
|
25
|
-
repo: string,
|
|
26
|
-
branch: string,
|
|
27
|
-
filePath: string,
|
|
28
|
-
): Promise<string | null> {
|
|
29
|
-
const url = `https://raw.githubusercontent.com/${repo}/${branch}/${filePath}`
|
|
30
|
-
const response = await fetch(url, {
|
|
31
|
-
headers: { 'User-Agent': 'tanstack-cli' },
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
if (response.status === 404) {
|
|
36
|
-
return null
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`Failed to fetch doc: ${response.statusText}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return response.text()
|
|
42
|
-
}
|
package/src/mcp/tools.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { fetchDocContent, fetchLibraries, fetchPartners } from './api.js'
|
|
3
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
|
-
|
|
5
|
-
// Algolia config (public read-only keys)
|
|
6
|
-
const ALGOLIA_APP_ID = 'FQ0DQ6MA3C'
|
|
7
|
-
const ALGOLIA_API_KEY = '10c34d6a5c89f6048cf644d601e65172'
|
|
8
|
-
const ALGOLIA_INDEX = 'tanstack-test'
|
|
9
|
-
|
|
10
|
-
const GROUP_KEYS = ['state', 'headlessUI', 'performance', 'tooling'] as const
|
|
11
|
-
|
|
12
|
-
function jsonResult(data: unknown) {
|
|
13
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function errorResult(error: string) {
|
|
17
|
-
return { content: [{ type: 'text' as const, text: `Error: ${error}` }], isError: true }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function registerDocTools(server: McpServer) {
|
|
21
|
-
// Tool: tanstack_list_libraries
|
|
22
|
-
server.tool(
|
|
23
|
-
'tanstack_list_libraries',
|
|
24
|
-
'List TanStack libraries with metadata, frameworks, and docs URLs.',
|
|
25
|
-
{
|
|
26
|
-
group: z
|
|
27
|
-
.enum(GROUP_KEYS)
|
|
28
|
-
.optional()
|
|
29
|
-
.describe('Filter libraries by group. Options: state, headlessUI, performance, tooling'),
|
|
30
|
-
},
|
|
31
|
-
async ({ group }) => {
|
|
32
|
-
try {
|
|
33
|
-
const data = await fetchLibraries()
|
|
34
|
-
let libraries = data.libraries
|
|
35
|
-
|
|
36
|
-
if (group && data.groups[group]) {
|
|
37
|
-
const groupIds = data.groups[group]
|
|
38
|
-
libraries = libraries.filter((lib) => groupIds.includes(lib.id))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const groupName = group ? data.groupNames[group] || group : 'All Libraries'
|
|
42
|
-
|
|
43
|
-
return jsonResult({
|
|
44
|
-
group: groupName,
|
|
45
|
-
count: libraries.length,
|
|
46
|
-
libraries: libraries.map((lib) => ({
|
|
47
|
-
id: lib.id,
|
|
48
|
-
name: lib.name,
|
|
49
|
-
tagline: lib.tagline,
|
|
50
|
-
description: lib.description,
|
|
51
|
-
frameworks: lib.frameworks,
|
|
52
|
-
latestVersion: lib.latestVersion,
|
|
53
|
-
docsUrl: lib.docsUrl,
|
|
54
|
-
githubUrl: lib.githubUrl,
|
|
55
|
-
})),
|
|
56
|
-
})
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return errorResult(String(error))
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
// Tool: tanstack_doc
|
|
64
|
-
server.tool(
|
|
65
|
-
'tanstack_doc',
|
|
66
|
-
'Fetch a TanStack documentation page by library and path.',
|
|
67
|
-
{
|
|
68
|
-
library: z.string().describe('Library ID (e.g., query, router, table, form)'),
|
|
69
|
-
path: z.string().describe('Documentation path (e.g., framework/react/overview)'),
|
|
70
|
-
version: z.string().optional().describe('Version (e.g., v5, v1). Defaults to latest'),
|
|
71
|
-
},
|
|
72
|
-
async ({ library: libraryId, path, version = 'latest' }) => {
|
|
73
|
-
try {
|
|
74
|
-
const data = await fetchLibraries()
|
|
75
|
-
const library = data.libraries.find((l) => l.id === libraryId)
|
|
76
|
-
|
|
77
|
-
if (!library) {
|
|
78
|
-
return errorResult(
|
|
79
|
-
`Library "${libraryId}" not found. Use tanstack_list_libraries to see available libraries.`,
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (version !== 'latest' && !library.availableVersions.includes(version)) {
|
|
84
|
-
return errorResult(
|
|
85
|
-
`Version "${version}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`,
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Resolve branch
|
|
90
|
-
const branch =
|
|
91
|
-
version === 'latest' || version === library.latestVersion
|
|
92
|
-
? library.latestBranch || 'main'
|
|
93
|
-
: version
|
|
94
|
-
|
|
95
|
-
const docsRoot = library.docsRoot || 'docs'
|
|
96
|
-
const filePath = `${docsRoot}/${path}.md`
|
|
97
|
-
const content = await fetchDocContent(library.repo, branch, filePath)
|
|
98
|
-
|
|
99
|
-
if (!content) {
|
|
100
|
-
return errorResult(
|
|
101
|
-
`Document not found: ${library.name} / ${path} (version: ${version})`,
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Extract frontmatter title if present
|
|
106
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
107
|
-
let title = path.split('/').pop() || 'Untitled'
|
|
108
|
-
let docContent = content
|
|
109
|
-
|
|
110
|
-
if (frontmatterMatch && frontmatterMatch[1]) {
|
|
111
|
-
const frontmatter = frontmatterMatch[1]
|
|
112
|
-
const titleMatch = frontmatter.match(/title:\s*['"]?([^'"\n]+)['"]?/)
|
|
113
|
-
if (titleMatch && titleMatch[1]) {
|
|
114
|
-
title = titleMatch[1]
|
|
115
|
-
}
|
|
116
|
-
docContent = content.slice(frontmatterMatch[0].length).trim()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return jsonResult({
|
|
120
|
-
title,
|
|
121
|
-
content: docContent,
|
|
122
|
-
url: `https://tanstack.com/${libraryId}/${version}/docs/${path}`,
|
|
123
|
-
library: library.name,
|
|
124
|
-
version: version === 'latest' ? library.latestVersion : version,
|
|
125
|
-
})
|
|
126
|
-
} catch (error) {
|
|
127
|
-
return errorResult(String(error))
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
// Tool: tanstack_search_docs
|
|
133
|
-
server.tool(
|
|
134
|
-
'tanstack_search_docs',
|
|
135
|
-
'Search TanStack documentation. Returns matching pages with snippets.',
|
|
136
|
-
{
|
|
137
|
-
query: z.string().describe('Search query'),
|
|
138
|
-
library: z.string().optional().describe('Filter to specific library (e.g., query, router)'),
|
|
139
|
-
framework: z
|
|
140
|
-
.string()
|
|
141
|
-
.optional()
|
|
142
|
-
.describe('Filter to specific framework (e.g., react, vue, solid)'),
|
|
143
|
-
limit: z
|
|
144
|
-
.number()
|
|
145
|
-
.min(1)
|
|
146
|
-
.max(50)
|
|
147
|
-
.optional()
|
|
148
|
-
.describe('Maximum number of results (default: 10, max: 50)'),
|
|
149
|
-
},
|
|
150
|
-
async ({ query, library, framework, limit = 10 }) => {
|
|
151
|
-
try {
|
|
152
|
-
const ALL_LIBRARIES = [
|
|
153
|
-
'config', 'form', 'optimistic', 'pacer', 'query', 'ranger',
|
|
154
|
-
'react-charts', 'router', 'start', 'store', 'table', 'virtual', 'db', 'devtools',
|
|
155
|
-
]
|
|
156
|
-
const ALL_FRAMEWORKS = ['react', 'vue', 'solid', 'svelte', 'angular']
|
|
157
|
-
|
|
158
|
-
// Build filters
|
|
159
|
-
const filterParts: Array<string> = ['version:latest']
|
|
160
|
-
|
|
161
|
-
if (library) {
|
|
162
|
-
const otherLibraries = ALL_LIBRARIES.filter((l) => l !== library)
|
|
163
|
-
const exclusions = otherLibraries.map((l) => `NOT library:${l}`).join(' AND ')
|
|
164
|
-
if (exclusions) filterParts.push(`(${exclusions})`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (framework) {
|
|
168
|
-
const otherFrameworks = ALL_FRAMEWORKS.filter((f) => f !== framework)
|
|
169
|
-
const exclusions = otherFrameworks.map((f) => `NOT framework:${f}`).join(' AND ')
|
|
170
|
-
if (exclusions) filterParts.push(`(${exclusions})`)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Call Algolia REST API directly
|
|
174
|
-
const searchParams = {
|
|
175
|
-
requests: [
|
|
176
|
-
{
|
|
177
|
-
indexName: ALGOLIA_INDEX,
|
|
178
|
-
query,
|
|
179
|
-
hitsPerPage: Math.min(limit, 50),
|
|
180
|
-
filters: filterParts.join(' AND '),
|
|
181
|
-
attributesToRetrieve: ['hierarchy', 'url', 'content', 'library'],
|
|
182
|
-
attributesToSnippet: ['content:80'],
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const response = await fetch(
|
|
188
|
-
`https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/*/queries`,
|
|
189
|
-
{
|
|
190
|
-
method: 'POST',
|
|
191
|
-
headers: {
|
|
192
|
-
'Content-Type': 'application/json',
|
|
193
|
-
'X-Algolia-Application-Id': ALGOLIA_APP_ID,
|
|
194
|
-
'X-Algolia-API-Key': ALGOLIA_API_KEY,
|
|
195
|
-
},
|
|
196
|
-
body: JSON.stringify(searchParams),
|
|
197
|
-
},
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
if (!response.ok) {
|
|
201
|
-
return errorResult(`Algolia search failed: ${response.statusText}`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const searchResponse = await response.json() as {
|
|
205
|
-
results: Array<{
|
|
206
|
-
hits: Array<{
|
|
207
|
-
objectID: string
|
|
208
|
-
url: string
|
|
209
|
-
library?: string
|
|
210
|
-
hierarchy: Record<string, string | undefined>
|
|
211
|
-
content?: string
|
|
212
|
-
_snippetResult?: { content?: { value?: string } }
|
|
213
|
-
}>
|
|
214
|
-
nbHits?: number
|
|
215
|
-
}>
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const searchResult = searchResponse.results[0]
|
|
219
|
-
if (!searchResult) {
|
|
220
|
-
return jsonResult({ query, totalHits: 0, results: [] })
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const results = searchResult.hits.map((hit) => {
|
|
224
|
-
const breadcrumb = Object.values(hit.hierarchy).filter((v): v is string => Boolean(v))
|
|
225
|
-
return {
|
|
226
|
-
title: hit.hierarchy.lvl1 || hit.hierarchy.lvl0 || 'Untitled',
|
|
227
|
-
url: hit.url,
|
|
228
|
-
snippet: hit._snippetResult?.content?.value || hit.content || '',
|
|
229
|
-
library: hit.library || 'unknown',
|
|
230
|
-
breadcrumb,
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
return jsonResult({
|
|
235
|
-
query,
|
|
236
|
-
totalHits: searchResult.nbHits || results.length,
|
|
237
|
-
results,
|
|
238
|
-
})
|
|
239
|
-
} catch (error) {
|
|
240
|
-
return errorResult(String(error))
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
// Tool: tanstack_ecosystem
|
|
246
|
-
server.tool(
|
|
247
|
-
'tanstack_ecosystem',
|
|
248
|
-
'Ecosystem partner recommendations. Filter by category (database, auth, deployment, monitoring, cms, api, data-grid) or library.',
|
|
249
|
-
{
|
|
250
|
-
category: z
|
|
251
|
-
.string()
|
|
252
|
-
.optional()
|
|
253
|
-
.describe(
|
|
254
|
-
'Filter by category: database, auth, deployment, monitoring, cms, api, data-grid, code-review, learning',
|
|
255
|
-
),
|
|
256
|
-
library: z
|
|
257
|
-
.string()
|
|
258
|
-
.optional()
|
|
259
|
-
.describe('Filter by TanStack library (e.g., start, router, query, table)'),
|
|
260
|
-
},
|
|
261
|
-
async ({ category, library }) => {
|
|
262
|
-
try {
|
|
263
|
-
const data = await fetchPartners()
|
|
264
|
-
|
|
265
|
-
// Category aliases
|
|
266
|
-
const categoryAliases: Record<string, string> = {
|
|
267
|
-
db: 'database',
|
|
268
|
-
postgres: 'database',
|
|
269
|
-
sql: 'database',
|
|
270
|
-
login: 'auth',
|
|
271
|
-
authentication: 'auth',
|
|
272
|
-
hosting: 'deployment',
|
|
273
|
-
deploy: 'deployment',
|
|
274
|
-
serverless: 'deployment',
|
|
275
|
-
errors: 'monitoring',
|
|
276
|
-
logging: 'monitoring',
|
|
277
|
-
content: 'cms',
|
|
278
|
-
'api-keys': 'api',
|
|
279
|
-
grid: 'data-grid',
|
|
280
|
-
review: 'code-review',
|
|
281
|
-
courses: 'learning',
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
let resolvedCategory: string | undefined
|
|
285
|
-
if (category) {
|
|
286
|
-
const normalized = category.toLowerCase().trim()
|
|
287
|
-
resolvedCategory = categoryAliases[normalized] || normalized
|
|
288
|
-
if (!data.categories.includes(resolvedCategory)) {
|
|
289
|
-
resolvedCategory = undefined
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const lib = library?.toLowerCase().trim()
|
|
294
|
-
|
|
295
|
-
const partners = data.partners
|
|
296
|
-
.filter((p) => !resolvedCategory || p.category === resolvedCategory)
|
|
297
|
-
.filter((p) => !lib || p.libraries.some((l) => l === lib))
|
|
298
|
-
.map((p) => ({
|
|
299
|
-
id: p.id,
|
|
300
|
-
name: p.name,
|
|
301
|
-
tagline: p.tagline,
|
|
302
|
-
description: p.description,
|
|
303
|
-
category: p.category,
|
|
304
|
-
categoryLabel: p.categoryLabel,
|
|
305
|
-
url: p.url,
|
|
306
|
-
libraries: p.libraries,
|
|
307
|
-
}))
|
|
308
|
-
|
|
309
|
-
return jsonResult({
|
|
310
|
-
query: {
|
|
311
|
-
category,
|
|
312
|
-
categoryResolved: resolvedCategory,
|
|
313
|
-
library,
|
|
314
|
-
},
|
|
315
|
-
count: partners.length,
|
|
316
|
-
partners,
|
|
317
|
-
})
|
|
318
|
-
} catch (error) {
|
|
319
|
-
return errorResult(String(error))
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
)
|
|
323
|
-
}
|
package/src/mcp/types.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
// API response types from tanstack.com
|
|
4
|
-
export const LibrarySchema = z.object({
|
|
5
|
-
id: z.string(),
|
|
6
|
-
name: z.string(),
|
|
7
|
-
tagline: z.string(),
|
|
8
|
-
description: z.string().optional(),
|
|
9
|
-
frameworks: z.array(z.string()),
|
|
10
|
-
latestVersion: z.string(),
|
|
11
|
-
latestBranch: z.string().optional(),
|
|
12
|
-
availableVersions: z.array(z.string()),
|
|
13
|
-
repo: z.string(),
|
|
14
|
-
docsRoot: z.string().optional(),
|
|
15
|
-
defaultDocs: z.string().optional(),
|
|
16
|
-
docsUrl: z.string().optional(),
|
|
17
|
-
githubUrl: z.string().optional(),
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export const LibrariesResponseSchema = z.object({
|
|
21
|
-
libraries: z.array(LibrarySchema),
|
|
22
|
-
groups: z.record(z.array(z.string())),
|
|
23
|
-
groupNames: z.record(z.string()),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export const PartnerSchema = z.object({
|
|
27
|
-
id: z.string(),
|
|
28
|
-
name: z.string(),
|
|
29
|
-
tagline: z.string().optional(),
|
|
30
|
-
description: z.string(),
|
|
31
|
-
category: z.string(),
|
|
32
|
-
categoryLabel: z.string(),
|
|
33
|
-
libraries: z.array(z.string()),
|
|
34
|
-
url: z.string(),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
export const PartnersResponseSchema = z.object({
|
|
38
|
-
partners: z.array(PartnerSchema),
|
|
39
|
-
categories: z.array(z.string()),
|
|
40
|
-
categoryLabels: z.record(z.string()),
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
export type Library = z.infer<typeof LibrarySchema>
|
|
44
|
-
export type LibrariesResponse = z.infer<typeof LibrariesResponseSchema>
|
|
45
|
-
export type Partner = z.infer<typeof PartnerSchema>
|
|
46
|
-
export type PartnersResponse = z.infer<typeof PartnersResponseSchema>
|