@tanstack/cli 0.60.1 → 0.61.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @tanstack/cli
2
2
 
3
+ ## 0.61.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Auto-generated changeset from semantic commits on main.
8
+
9
+ - fix(ci): use direct changeset publish args (b6f5ff5)
10
+
11
+ - Updated dependencies []:
12
+ - @tanstack/create@0.62.3
13
+
14
+ ## 0.61.0
15
+
16
+ ### Minor Changes
17
+
18
+ - Remove the built-in MCP server from the CLI by dropping `tanstack mcp` and all MCP transport/tooling code. ([`78e3734`](https://github.com/TanStack/cli/commit/78e373444c5bcaf2ab59d2142e8b8b0cab415bbb))
19
+
20
+ Add CLI-native agent introspection commands (`libraries`, `doc`, `search-docs`, `ecosystem`) and JSON output for `create --list-add-ons` / `create --addon-details` so AI agents can rely on CLI commands directly.
21
+
22
+ ### Patch Changes
23
+
24
+ - Make the default base starter minimal (Home + About) for React and Solid, and add a new `blog` template option for both frameworks. ([`f33f8d4`](https://github.com/TanStack/cli/commit/f33f8d4954d9ad6771871257a4e1e58feee9b34d))
25
+
26
+ Interactive `create` now prompts for a template when one is not provided, and template id resolution prefers the selected framework when ids overlap.
27
+
28
+ - Updated dependencies [[`f33f8d4`](https://github.com/TanStack/cli/commit/f33f8d4954d9ad6771871257a4e1e58feee9b34d), [`16fcd67`](https://github.com/TanStack/cli/commit/16fcd674c0f74c1c62cf97b0042060d5a51981ef)]:
29
+ - @tanstack/create@0.62.2
30
+
3
31
  ## 0.60.1
4
32
 
5
33
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { cancel, confirm, intro, isCancel, log } from '@clack/prompts';
5
5
  import chalk from 'chalk';
6
6
  import semver from 'semver';
7
7
  import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, devAddOn, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
8
- import { runMCPServer } from './mcp.js';
8
+ import { LIBRARY_GROUPS, fetchDocContent, fetchLibraries, fetchPartners, searchTanStackDocs, } from './discovery.js';
9
9
  import { promptForAddOns, promptForCreateOptions } from './options.js';
10
10
  import { normalizeOptions, validateDevWatchOptions, validateLegacyCreateFlags, } from './command-line.js';
11
11
  import { createUIEnvironment } from './ui-environment.js';
@@ -125,6 +125,33 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
125
125
  }
126
126
  // Mode is always file-router (TanStack Start)
127
127
  const defaultMode = 'file-router';
128
+ const categoryAliases = {
129
+ db: 'database',
130
+ postgres: 'database',
131
+ sql: 'database',
132
+ login: 'auth',
133
+ authentication: 'auth',
134
+ hosting: 'deployment',
135
+ deploy: 'deployment',
136
+ serverless: 'deployment',
137
+ errors: 'monitoring',
138
+ logging: 'monitoring',
139
+ content: 'cms',
140
+ 'api-keys': 'api',
141
+ grid: 'data-grid',
142
+ review: 'code-review',
143
+ courses: 'learning',
144
+ };
145
+ function printJson(data) {
146
+ console.log(JSON.stringify(data, null, 2));
147
+ }
148
+ function parsePositiveInteger(value) {
149
+ const parsed = Number(value);
150
+ if (!Number.isInteger(parsed) || parsed < 1) {
151
+ throw new InvalidArgumentError('Value must be a positive integer');
152
+ }
153
+ return parsed;
154
+ }
128
155
  program
129
156
  .name(name)
130
157
  .description(`${appName} CLI`)
@@ -141,8 +168,26 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
141
168
  }
142
169
  if (options.listAddOns) {
143
170
  const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
171
+ const visibleAddOns = addOns.filter((a) => !forcedAddOns.includes(a.id));
172
+ if (options.json) {
173
+ printJson(visibleAddOns.map((addOn) => ({
174
+ id: addOn.id,
175
+ name: addOn.name,
176
+ description: addOn.description,
177
+ type: addOn.type,
178
+ category: addOn.category,
179
+ phase: addOn.phase,
180
+ modes: addOn.modes,
181
+ link: addOn.link,
182
+ warning: addOn.warning,
183
+ exclusive: addOn.exclusive,
184
+ dependsOn: addOn.dependsOn,
185
+ options: addOn.options,
186
+ })));
187
+ return;
188
+ }
144
189
  let hasConfigurableAddOns = false;
145
- for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
190
+ for (const addOn of visibleAddOns) {
146
191
  const hasOptions = addOn.options && Object.keys(addOn.options).length > 0;
147
192
  const optionMarker = hasOptions ? '*' : ' ';
148
193
  if (hasOptions)
@@ -162,6 +207,33 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
162
207
  console.error(`Add-on '${options.addonDetails}' not found`);
163
208
  process.exit(1);
164
209
  }
210
+ if (options.json) {
211
+ const files = await addOn.getFiles();
212
+ printJson({
213
+ id: addOn.id,
214
+ name: addOn.name,
215
+ description: addOn.description,
216
+ type: addOn.type,
217
+ category: addOn.category,
218
+ phase: addOn.phase,
219
+ modes: addOn.modes,
220
+ link: addOn.link,
221
+ warning: addOn.warning,
222
+ exclusive: addOn.exclusive,
223
+ dependsOn: addOn.dependsOn,
224
+ options: addOn.options,
225
+ routes: addOn.routes,
226
+ packageAdditions: addOn.packageAdditions,
227
+ shadcnComponents: addOn.shadcnComponents,
228
+ integrations: addOn.integrations,
229
+ readme: addOn.readme,
230
+ files,
231
+ author: addOn.author,
232
+ version: addOn.version,
233
+ license: addOn.license,
234
+ });
235
+ return;
236
+ }
165
237
  console.log(`${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`);
166
238
  console.log(`${chalk.bold('ID:')} ${addOn.id}`);
167
239
  console.log(`${chalk.bold('Description:')} ${addOn.description}`);
@@ -177,7 +249,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
177
249
  if (addOn.options && Object.keys(addOn.options).length > 0) {
178
250
  console.log(`\n${chalk.bold.yellow('Configuration Options:')}`);
179
251
  for (const [optionName, option] of Object.entries(addOn.options)) {
180
- if (option && typeof option === 'object' && 'type' in option) {
252
+ if ('type' in option) {
181
253
  const opt = option;
182
254
  console.log(` ${chalk.bold(optionName)}:`);
183
255
  console.log(` Label: ${opt.label}`);
@@ -335,6 +407,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
335
407
  })
336
408
  .option('--list-add-ons', 'list all available add-ons', false)
337
409
  .option('--addon-details <addon-id>', 'show detailed information about a specific add-on')
410
+ .option('--json', 'output JSON for automation', false)
338
411
  .option('--git', 'create a git repository')
339
412
  .option('--no-git', 'do not create a git repository')
340
413
  .option('--target-dir <path>', 'the target directory for the application root')
@@ -371,16 +444,198 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
371
444
  };
372
445
  await startDevWatchMode(projectName, devOptions);
373
446
  });
374
- // === MCP SUBCOMMAND ===
447
+ // === LIBRARIES SUBCOMMAND ===
375
448
  program
376
- .command('mcp')
377
- .description('Run the MCP (Model Context Protocol) server')
378
- .option('--sse', 'Run in SSE mode instead of stdio', false)
449
+ .command('libraries')
450
+ .description('List TanStack libraries')
451
+ .option('--group <group>', `filter by group (${LIBRARY_GROUPS.join(', ')})`)
452
+ .option('--json', 'output JSON for automation', false)
379
453
  .action(async (options) => {
380
- await runMCPServer(options.sse, {
381
- forcedAddOns,
382
- appName,
383
- });
454
+ try {
455
+ const data = await fetchLibraries();
456
+ let libraries = data.libraries;
457
+ if (options.group &&
458
+ Object.prototype.hasOwnProperty.call(data.groups, options.group)) {
459
+ const groupIds = data.groups[options.group];
460
+ libraries = libraries.filter((lib) => groupIds.includes(lib.id));
461
+ }
462
+ const groupName = options.group
463
+ ? data.groupNames[options.group] || options.group
464
+ : 'All Libraries';
465
+ const payload = {
466
+ group: groupName,
467
+ count: libraries.length,
468
+ libraries: libraries.map((lib) => ({
469
+ id: lib.id,
470
+ name: lib.name,
471
+ tagline: lib.tagline,
472
+ description: lib.description,
473
+ frameworks: lib.frameworks,
474
+ latestVersion: lib.latestVersion,
475
+ docsUrl: lib.docsUrl,
476
+ githubUrl: lib.githubUrl,
477
+ })),
478
+ };
479
+ if (options.json) {
480
+ printJson(payload);
481
+ return;
482
+ }
483
+ console.log(chalk.bold(groupName));
484
+ for (const lib of payload.libraries) {
485
+ console.log(`${chalk.bold(lib.id)} (${lib.latestVersion}) - ${lib.tagline}`);
486
+ }
487
+ }
488
+ catch (error) {
489
+ log.error(error instanceof Error ? error.message : String(error));
490
+ process.exit(1);
491
+ }
492
+ });
493
+ // === DOC SUBCOMMAND ===
494
+ program
495
+ .command('doc')
496
+ .description('Fetch a TanStack documentation page')
497
+ .argument('<library>', 'library ID (eg. query, router, table)')
498
+ .argument('<path>', 'documentation path (eg. framework/react/overview)')
499
+ .option('--docs-version <version>', 'docs version (default: latest)', 'latest')
500
+ .option('--json', 'output JSON for automation', false)
501
+ .action(async (libraryId, path, options) => {
502
+ try {
503
+ const data = await fetchLibraries();
504
+ const library = data.libraries.find((l) => l.id === libraryId);
505
+ if (!library) {
506
+ throw new Error(`Library "${libraryId}" not found. Use \`tanstack libraries\` to see available libraries.`);
507
+ }
508
+ if (options.docsVersion !== 'latest' &&
509
+ !library.availableVersions.includes(options.docsVersion)) {
510
+ throw new Error(`Version "${options.docsVersion}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`);
511
+ }
512
+ const branch = options.docsVersion === 'latest' ||
513
+ options.docsVersion === library.latestVersion
514
+ ? library.latestBranch || 'main'
515
+ : options.docsVersion;
516
+ const docsRoot = library.docsRoot || 'docs';
517
+ const filePath = `${docsRoot}/${path}.md`;
518
+ const content = await fetchDocContent(library.repo, branch, filePath);
519
+ if (!content) {
520
+ throw new Error(`Document not found: ${library.name} / ${path} (version: ${options.docsVersion})`);
521
+ }
522
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
523
+ let title = path.split('/').pop() || 'Untitled';
524
+ let docContent = content;
525
+ if (frontmatterMatch && frontmatterMatch[1]) {
526
+ const frontmatter = frontmatterMatch[1];
527
+ const titleMatch = frontmatter.match(/title:\s*['"]?([^'"\n]+)['"]?/);
528
+ if (titleMatch && titleMatch[1]) {
529
+ title = titleMatch[1];
530
+ }
531
+ docContent = content.slice(frontmatterMatch[0].length).trim();
532
+ }
533
+ const payload = {
534
+ title,
535
+ content: docContent,
536
+ url: `https://tanstack.com/${libraryId}/${options.docsVersion}/docs/${path}`,
537
+ library: library.name,
538
+ version: options.docsVersion === 'latest'
539
+ ? library.latestVersion
540
+ : options.docsVersion,
541
+ };
542
+ if (options.json) {
543
+ printJson(payload);
544
+ return;
545
+ }
546
+ console.log(chalk.bold(payload.title));
547
+ console.log(chalk.blue(payload.url));
548
+ console.log('');
549
+ console.log(payload.content);
550
+ }
551
+ catch (error) {
552
+ log.error(error instanceof Error ? error.message : String(error));
553
+ process.exit(1);
554
+ }
555
+ });
556
+ // === SEARCH-DOCS SUBCOMMAND ===
557
+ program
558
+ .command('search-docs')
559
+ .description('Search TanStack documentation')
560
+ .argument('<query>', 'search query')
561
+ .option('--library <id>', 'filter to specific library')
562
+ .option('--framework <name>', 'filter to specific framework')
563
+ .option('--limit <n>', 'max results (default: 10, max: 50)', parsePositiveInteger, 10)
564
+ .option('--json', 'output JSON for automation', false)
565
+ .action(async (query, options) => {
566
+ try {
567
+ const payload = await searchTanStackDocs({
568
+ query,
569
+ library: options.library,
570
+ framework: options.framework,
571
+ limit: options.limit,
572
+ });
573
+ if (options.json) {
574
+ printJson(payload);
575
+ return;
576
+ }
577
+ for (const result of payload.results) {
578
+ console.log(`${chalk.bold(result.title)} [${result.library}]\n${chalk.blue(result.url)}\n${result.snippet}\n`);
579
+ }
580
+ }
581
+ catch (error) {
582
+ log.error(error instanceof Error ? error.message : String(error));
583
+ process.exit(1);
584
+ }
585
+ });
586
+ // === ECOSYSTEM SUBCOMMAND ===
587
+ program
588
+ .command('ecosystem')
589
+ .description('List TanStack ecosystem partners')
590
+ .option('--category <category>', 'filter by category')
591
+ .option('--library <id>', 'filter by TanStack library')
592
+ .option('--json', 'output JSON for automation', false)
593
+ .action(async (options) => {
594
+ try {
595
+ const data = await fetchPartners();
596
+ let resolvedCategory;
597
+ if (options.category) {
598
+ const normalized = options.category.toLowerCase().trim();
599
+ resolvedCategory = categoryAliases[normalized] || normalized;
600
+ if (!data.categories.includes(resolvedCategory)) {
601
+ resolvedCategory = undefined;
602
+ }
603
+ }
604
+ const library = options.library?.toLowerCase().trim();
605
+ const partners = data.partners
606
+ .filter((partner) => resolvedCategory ? partner.category === resolvedCategory : true)
607
+ .filter((partner) => library ? partner.libraries.some((l) => l === library) : true)
608
+ .map((partner) => ({
609
+ id: partner.id,
610
+ name: partner.name,
611
+ tagline: partner.tagline,
612
+ description: partner.description,
613
+ category: partner.category,
614
+ categoryLabel: partner.categoryLabel,
615
+ url: partner.url,
616
+ libraries: partner.libraries,
617
+ }));
618
+ const payload = {
619
+ query: {
620
+ category: options.category,
621
+ categoryResolved: resolvedCategory,
622
+ library: options.library,
623
+ },
624
+ count: partners.length,
625
+ partners,
626
+ };
627
+ if (options.json) {
628
+ printJson(payload);
629
+ return;
630
+ }
631
+ for (const partner of partners) {
632
+ console.log(`${chalk.bold(partner.name)} [${partner.category}] - ${partner.description}\n${chalk.blue(partner.url)}`);
633
+ }
634
+ }
635
+ catch (error) {
636
+ log.error(error instanceof Error ? error.message : String(error));
637
+ process.exit(1);
638
+ }
384
639
  });
385
640
  // === PIN-VERSIONS SUBCOMMAND ===
386
641
  program
@@ -26,6 +26,13 @@ function slugifyStarterName(value) {
26
26
  .replace(/[^a-z0-9]+/g, '-')
27
27
  .replace(/^-+|-+$/g, '');
28
28
  }
29
+ function humanizeStarterId(value) {
30
+ return value
31
+ .split(/[-_]/g)
32
+ .filter(Boolean)
33
+ .map((part) => part[0].toUpperCase() + part.slice(1))
34
+ .join(' ');
35
+ }
29
36
  function isLikelyStarterUrlOrPath(value) {
30
37
  return (/^https?:\/\//i.test(value) ||
31
38
  /^file:\/\//i.test(value) ||
@@ -53,7 +60,7 @@ function getStarterIdsFromUrl(starterUrl) {
53
60
  }
54
61
  return ids;
55
62
  }
56
- function resolveMonorepoStarterById(starterId) {
63
+ function resolveMonorepoStarterById(starterId, preferredFramework) {
57
64
  const normalized = starterId.toLowerCase().trim();
58
65
  const idVariants = Array.from(new Set([
59
66
  normalized,
@@ -67,8 +74,11 @@ function resolveMonorepoStarterById(starterId) {
67
74
  resolve(cwd, '../..'),
68
75
  resolve(cwd, '../../..'),
69
76
  ];
77
+ const frameworkOrder = preferredFramework
78
+ ? [preferredFramework, ...['react', 'solid'].filter((f) => f !== preferredFramework)]
79
+ : ['react', 'solid'];
70
80
  for (const root of rootCandidates) {
71
- for (const framework of ['react', 'solid']) {
81
+ for (const framework of frameworkOrder) {
72
82
  for (const id of idVariants) {
73
83
  const templatePath = resolve(root, 'examples', framework, id, 'template.json');
74
84
  if (fs.existsSync(templatePath)) {
@@ -83,7 +93,7 @@ function resolveMonorepoStarterById(starterId) {
83
93
  }
84
94
  return undefined;
85
95
  }
86
- async function resolveStarterSpecifier(starterSpecifier) {
96
+ export async function resolveStarterSpecifier(starterSpecifier, preferredFramework) {
87
97
  const normalized = starterSpecifier.trim();
88
98
  if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
89
99
  return normalized;
@@ -91,7 +101,7 @@ async function resolveStarterSpecifier(starterSpecifier) {
91
101
  const registry = await getRawRegistry();
92
102
  if (registry && registry.starters?.length) {
93
103
  const lookup = normalized.toLowerCase();
94
- const match = registry.starters.find((starter) => {
104
+ const matches = registry.starters.filter((starter) => {
95
105
  const candidateIds = new Set();
96
106
  candidateIds.add(starter.name.toLowerCase());
97
107
  candidateIds.add(slugifyStarterName(starter.name));
@@ -100,11 +110,17 @@ async function resolveStarterSpecifier(starterSpecifier) {
100
110
  }
101
111
  return candidateIds.has(lookup);
102
112
  });
103
- if (match) {
104
- return match.url;
113
+ const frameworkMatch = preferredFramework
114
+ ? matches.find((starter) => starter.framework.toLowerCase() === preferredFramework)
115
+ : undefined;
116
+ if (frameworkMatch) {
117
+ return frameworkMatch.url;
118
+ }
119
+ if (matches.length > 0) {
120
+ return matches[0].url;
105
121
  }
106
122
  }
107
- const monorepoStarterPath = resolveMonorepoStarterById(normalized);
123
+ const monorepoStarterPath = resolveMonorepoStarterById(normalized, preferredFramework);
108
124
  if (monorepoStarterPath) {
109
125
  return monorepoStarterPath;
110
126
  }
@@ -120,6 +136,84 @@ async function resolveStarterSpecifier(starterSpecifier) {
120
136
  .sort();
121
137
  throw new Error(`Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`);
122
138
  }
139
+ export async function listTemplateChoices(preferredFramework) {
140
+ const frameworkFilter = preferredFramework?.toLowerCase();
141
+ const deduped = new Map();
142
+ const registry = await getRawRegistry();
143
+ for (const starter of registry?.starters || []) {
144
+ const framework = starter.framework.toLowerCase();
145
+ if (frameworkFilter && framework !== frameworkFilter) {
146
+ continue;
147
+ }
148
+ const ids = Array.from(getStarterIdsFromUrl(starter.url));
149
+ const id = ids[0] || slugifyStarterName(starter.name);
150
+ if (!id) {
151
+ continue;
152
+ }
153
+ const key = `${framework}:${id}`;
154
+ if (!deduped.has(key)) {
155
+ deduped.set(key, {
156
+ id,
157
+ name: starter.name,
158
+ description: starter.description,
159
+ framework,
160
+ });
161
+ }
162
+ }
163
+ const cwd = process.cwd();
164
+ const rootCandidates = [
165
+ cwd,
166
+ resolve(cwd, '..'),
167
+ resolve(cwd, '../..'),
168
+ resolve(cwd, '../../..'),
169
+ ];
170
+ const frameworks = frameworkFilter ? [frameworkFilter] : ['react', 'solid'];
171
+ for (const root of rootCandidates) {
172
+ for (const framework of frameworks) {
173
+ const frameworkDir = resolve(root, 'examples', framework);
174
+ if (!fs.existsSync(frameworkDir) || !fs.statSync(frameworkDir).isDirectory()) {
175
+ continue;
176
+ }
177
+ for (const entry of fs.readdirSync(frameworkDir, { withFileTypes: true })) {
178
+ if (!entry.isDirectory()) {
179
+ continue;
180
+ }
181
+ const id = entry.name;
182
+ const key = `${framework}:${id}`;
183
+ if (deduped.has(key)) {
184
+ continue;
185
+ }
186
+ const templatePath = resolve(frameworkDir, id, 'template.json');
187
+ const starterPath = resolve(frameworkDir, id, 'starter.json');
188
+ if (!fs.existsSync(templatePath) && !fs.existsSync(starterPath)) {
189
+ continue;
190
+ }
191
+ let name = humanizeStarterId(id);
192
+ let description;
193
+ const templateInfoPath = resolve(frameworkDir, id, 'template-info.json');
194
+ if (fs.existsSync(templateInfoPath)) {
195
+ try {
196
+ const info = JSON.parse(fs.readFileSync(templateInfoPath, 'utf8'));
197
+ if (info.name) {
198
+ name = info.name;
199
+ }
200
+ description = info.description;
201
+ }
202
+ catch {
203
+ // Ignore malformed template-info files and use fallback values.
204
+ }
205
+ }
206
+ deduped.set(key, {
207
+ id,
208
+ name,
209
+ description,
210
+ framework,
211
+ });
212
+ }
213
+ }
214
+ }
215
+ return Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name));
216
+ }
123
217
  export function validateLegacyCreateFlags(cliOptions) {
124
218
  const warnings = [];
125
219
  const legacyTemplate = getLegacyTemplateValue(cliOptions.template);
@@ -209,8 +303,9 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
209
303
  if (!cliOptions.starter && cliOptions.templateId) {
210
304
  cliOptions.starter = cliOptions.templateId;
211
305
  }
306
+ const preferredFramework = (cliOptions.framework || 'react').toLowerCase();
212
307
  const starter = !routerOnly && cliOptions.starter
213
- ? await loadStarter(await resolveStarterSpecifier(cliOptions.starter))
308
+ ? await loadStarter(await resolveStarterSpecifier(cliOptions.starter, preferredFramework))
214
309
  : undefined;
215
310
  // TypeScript and Tailwind are always enabled with TanStack Start
216
311
  const typescript = true;
@@ -0,0 +1,144 @@
1
+ import { z } from 'zod';
2
+ const TANSTACK_API_BASE = 'https://tanstack.com/api/data';
3
+ const LibrarySchema = z.object({
4
+ id: z.string(),
5
+ name: z.string(),
6
+ tagline: z.string(),
7
+ description: z.string().optional(),
8
+ frameworks: z.array(z.string()),
9
+ latestVersion: z.string(),
10
+ latestBranch: z.string().optional(),
11
+ availableVersions: z.array(z.string()),
12
+ repo: z.string(),
13
+ docsRoot: z.string().optional(),
14
+ defaultDocs: z.string().optional(),
15
+ docsUrl: z.string().optional(),
16
+ githubUrl: z.string().optional(),
17
+ });
18
+ const LibrariesResponseSchema = z.object({
19
+ libraries: z.array(LibrarySchema),
20
+ groups: z.record(z.array(z.string())),
21
+ groupNames: z.record(z.string()),
22
+ });
23
+ const PartnerSchema = z.object({
24
+ id: z.string(),
25
+ name: z.string(),
26
+ tagline: z.string().optional(),
27
+ description: z.string(),
28
+ category: z.string(),
29
+ categoryLabel: z.string(),
30
+ libraries: z.array(z.string()),
31
+ url: z.string(),
32
+ });
33
+ const PartnersResponseSchema = z.object({
34
+ partners: z.array(PartnerSchema),
35
+ categories: z.array(z.string()),
36
+ categoryLabels: z.record(z.string()),
37
+ });
38
+ export const LIBRARY_GROUPS = ['state', 'headlessUI', 'performance', 'tooling'];
39
+ // Algolia config (public read-only keys)
40
+ const ALGOLIA_APP_ID = 'FQ0DQ6MA3C';
41
+ const ALGOLIA_API_KEY = '10c34d6a5c89f6048cf644d601e65172';
42
+ const ALGOLIA_INDEX = 'tanstack-test';
43
+ export async function fetchLibraries() {
44
+ const response = await fetch(`${TANSTACK_API_BASE}/libraries`);
45
+ if (!response.ok) {
46
+ throw new Error(`Failed to fetch libraries: ${response.statusText}`);
47
+ }
48
+ const data = await response.json();
49
+ return LibrariesResponseSchema.parse(data);
50
+ }
51
+ export async function fetchPartners() {
52
+ const response = await fetch(`${TANSTACK_API_BASE}/partners`);
53
+ if (!response.ok) {
54
+ throw new Error(`Failed to fetch partners: ${response.statusText}`);
55
+ }
56
+ const data = await response.json();
57
+ return PartnersResponseSchema.parse(data);
58
+ }
59
+ export async function fetchDocContent(repo, branch, filePath) {
60
+ const url = `https://raw.githubusercontent.com/${repo}/${branch}/${filePath}`;
61
+ const response = await fetch(url, {
62
+ headers: { 'User-Agent': 'tanstack-cli' },
63
+ });
64
+ if (!response.ok) {
65
+ if (response.status === 404) {
66
+ return null;
67
+ }
68
+ throw new Error(`Failed to fetch doc: ${response.statusText}`);
69
+ }
70
+ return response.text();
71
+ }
72
+ export async function searchTanStackDocs({ query, library, framework, limit = 10, }) {
73
+ const ALL_LIBRARIES = [
74
+ 'config',
75
+ 'form',
76
+ 'optimistic',
77
+ 'pacer',
78
+ 'query',
79
+ 'ranger',
80
+ 'react-charts',
81
+ 'router',
82
+ 'start',
83
+ 'store',
84
+ 'table',
85
+ 'virtual',
86
+ 'db',
87
+ 'devtools',
88
+ ];
89
+ const ALL_FRAMEWORKS = ['react', 'vue', 'solid', 'svelte', 'angular'];
90
+ const filterParts = ['version:latest'];
91
+ if (library) {
92
+ const otherLibraries = ALL_LIBRARIES.filter((l) => l !== library);
93
+ const exclusions = otherLibraries.map((l) => `NOT library:${l}`).join(' AND ');
94
+ if (exclusions)
95
+ filterParts.push(`(${exclusions})`);
96
+ }
97
+ if (framework) {
98
+ const otherFrameworks = ALL_FRAMEWORKS.filter((f) => f !== framework);
99
+ const exclusions = otherFrameworks.map((f) => `NOT framework:${f}`).join(' AND ');
100
+ if (exclusions)
101
+ filterParts.push(`(${exclusions})`);
102
+ }
103
+ const searchParams = {
104
+ requests: [
105
+ {
106
+ indexName: ALGOLIA_INDEX,
107
+ query,
108
+ hitsPerPage: Math.min(limit, 50),
109
+ filters: filterParts.join(' AND '),
110
+ attributesToRetrieve: ['hierarchy', 'url', 'content', 'library'],
111
+ attributesToSnippet: ['content:80'],
112
+ },
113
+ ],
114
+ };
115
+ const response = await fetch(`https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/*/queries`, {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ 'X-Algolia-Application-Id': ALGOLIA_APP_ID,
120
+ 'X-Algolia-API-Key': ALGOLIA_API_KEY,
121
+ },
122
+ body: JSON.stringify(searchParams),
123
+ });
124
+ if (!response.ok) {
125
+ throw new Error(`Algolia search failed: ${response.statusText}`);
126
+ }
127
+ const searchResponse = (await response.json());
128
+ const searchResult = searchResponse.results[0];
129
+ const results = searchResult.hits.map((hit) => {
130
+ const breadcrumb = Object.values(hit.hierarchy).filter((v) => Boolean(v));
131
+ return {
132
+ title: hit.hierarchy.lvl1 || hit.hierarchy.lvl0 || 'Untitled',
133
+ url: hit.url,
134
+ snippet: hit._snippetResult?.content?.value || hit.content || '',
135
+ library: hit.library || 'unknown',
136
+ breadcrumb,
137
+ };
138
+ });
139
+ return {
140
+ query,
141
+ totalHits: searchResult.nbHits || results.length,
142
+ results,
143
+ };
144
+ }