@tanstack/cli
Version:
TanStack CLI
1,133 lines (1,131 loc) • 50.5 kB
JavaScript
import fs from 'node:fs';
import { resolve } from 'node:path';
import { Command, InvalidArgumentError } from 'commander';
import { cancel, confirm, intro, isCancel, log } from '@clack/prompts';
import chalk from 'chalk';
import semver from 'semver';
import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, devAddOn, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
import { LIBRARY_GROUPS, fetchDocContent, fetchLibraries, fetchPartners, searchTanStackDocs, } from './discovery.js';
import { getTelemetryStatus, setTelemetryEnabled, } from './telemetry-config.js';
import { createTelemetryClient } from './telemetry.js';
import { promptForAddOns, promptForCreateOptions } from './options.js';
import { normalizeOptions, validateDevWatchOptions, validateLegacyCreateFlags, } from './command-line.js';
import { createUIEnvironment } from './ui-environment.js';
import { DevWatchManager } from './dev-watch.js';
// Read version from package.json
const packageJsonPath = new URL('../package.json', import.meta.url);
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const VERSION = packageJson.version;
function isLocalPath(value) {
return (value.startsWith('./') ||
value.startsWith('../') ||
value.startsWith('/') ||
/^[a-zA-Z]:[\\/]/.test(value));
}
function isRemoteUrl(value) {
return /^https?:\/\//i.test(value) || /^file:\/\//i.test(value);
}
function sanitizeId(value) {
const normalized = value.trim().toLowerCase();
if (!normalized) {
return undefined;
}
return normalized.replace(/[^a-z0-9._:/-]/g, '-');
}
function sanitizeIdList(values) {
return Array.from(new Set(values
.map((value) => sanitizeId(value))
.filter((value) => Boolean(value))));
}
function getStarterTelemetryProperties(value) {
if (!value) {
return {};
}
if (isRemoteUrl(value)) {
return {
starter_kind: 'remote_url',
};
}
if (isLocalPath(value)) {
return {
starter_kind: 'local_path',
};
}
return {
starter_id: sanitizeId(value),
starter_kind: 'built_in',
};
}
function getLengthBucket(value) {
const length = value.trim().length;
if (length === 0) {
return 'empty';
}
if (length <= 10) {
return '1_10';
}
if (length <= 25) {
return '11_25';
}
if (length <= 50) {
return '26_50';
}
return '51_plus';
}
function getCreateCommandVariant(options) {
if (options.listAddOns) {
return 'list_add_ons';
}
if (options.addonDetails) {
return 'addon_details';
}
if (options.devWatch) {
return 'dev_watch';
}
return 'scaffold';
}
function getCreateTelemetryProperties(projectName, options) {
const addOnIds = Array.isArray(options.addOns)
? sanitizeIdList(options.addOns)
: undefined;
return {
...getStarterTelemetryProperties(options.starter || options.templateId || options.template),
add_on_count: addOnIds?.length,
add_on_ids: addOnIds,
addon_details_id: options.addonDetails
? sanitizeId(options.addonDetails)
: undefined,
command_variant: getCreateCommandVariant(options),
deployment: options.deployment ? sanitizeId(options.deployment) : undefined,
examples: options.examples,
framework: options.framework ? sanitizeId(options.framework) : undefined,
git: options.git,
install: options.install !== false,
interactive: !!options.interactive,
json: !!options.json,
non_interactive: !!options.nonInteractive || !!options.yes,
package_manager: options.packageManager,
project_name_provided: Boolean(projectName),
router_only: !!options.routerOnly,
target_dir_flag: Boolean(options.targetDir),
toolchain: typeof options.toolchain === 'string' ? sanitizeId(options.toolchain) : undefined,
yes: !!options.yes,
};
}
function getResolvedCreateTelemetryProperties(finalOptions, cliOptions) {
const includeExamples = finalOptions.includeExamples !== false;
const addOnIds = sanitizeIdList(finalOptions.chosenAddOns.map((addOn) => addOn.id));
const deployment = finalOptions.chosenAddOns.find((addOn) => addOn.type === 'deployment');
const toolchain = finalOptions.chosenAddOns.find((addOn) => addOn.type === 'toolchain');
return {
...getStarterTelemetryProperties(finalOptions.starter?.id || cliOptions.starter || cliOptions.templateId || cliOptions.template),
add_on_count: addOnIds.length,
add_on_ids: addOnIds,
deployment: deployment ? sanitizeId(deployment.id) : undefined,
examples: includeExamples,
framework: sanitizeId(finalOptions.framework.id),
git: finalOptions.git,
install: finalOptions.install !== false,
package_manager: finalOptions.packageManager,
router_only: !!cliOptions.routerOnly,
toolchain: toolchain ? sanitizeId(toolchain.id) : undefined,
};
}
function formatErrorMessage(error) {
return error instanceof Error ? error.message : 'An unknown error occurred';
}
export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
let currentTelemetry;
const environment = createUIEnvironment(appName, false, () => currentTelemetry);
const program = new Command();
async function confirmTargetDirectorySafety(targetDir, forced) {
if (forced) {
return;
}
if (!fs.existsSync(targetDir)) {
return;
}
if (!fs.statSync(targetDir).isDirectory()) {
throw new Error(`Target path exists and is not a directory: ${targetDir}`);
}
if (fs.readdirSync(targetDir).length === 0) {
return;
}
const shouldContinue = await confirm({
message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
initialValue: false,
});
if (isCancel(shouldContinue) || !shouldContinue) {
cancel('Operation cancelled.');
process.exit(0);
}
}
const availableFrameworks = getFrameworks().map((f) => f.name);
function resolveBuiltInDevWatchPath(frameworkId) {
const candidates = [
resolve(process.cwd(), 'packages/create/src/frameworks', frameworkId),
resolve(process.cwd(), '../create/src/frameworks', frameworkId),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return candidates[0];
}
async function startDevWatchMode(projectName, options) {
// Validate dev watch options
const validation = validateDevWatchOptions({ ...options, projectName });
if (!validation.valid) {
throw new Error(validation.error);
}
// Enter dev watch mode
if (!projectName && !options.targetDir) {
throw new Error('Project name/target directory is required for dev watch mode');
}
if (!options.framework) {
throw new Error('Failed to detect framework');
}
const framework = getFrameworkByName(options.framework);
if (!framework) {
throw new Error('Failed to detect framework');
}
// First, create the app normally using the standard flow
const normalizedOpts = await normalizeOptions({
...options,
projectName,
framework: framework.id,
}, forcedAddOns);
if (!normalizedOpts) {
throw new Error('Failed to normalize options');
}
currentTelemetry?.mergeProperties(getResolvedCreateTelemetryProperties(normalizedOpts, options));
normalizedOpts.targetDir =
options.targetDir || resolve(process.cwd(), projectName);
// Create the initial app with minimal output for dev watch mode
console.log(chalk.bold('\ndev-watch'));
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`);
if (normalizedOpts.install !== false) {
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
}
const silentEnvironment = createUIEnvironment(appName, true, () => currentTelemetry);
await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
await createApp(silentEnvironment, normalizedOpts);
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
// Now start the dev watch mode
const manager = new DevWatchManager({
watchPath: options.devWatch,
targetDir: normalizedOpts.targetDir,
framework,
cliOptions: normalizedOpts,
packageManager: normalizedOpts.packageManager,
runDevCommand: options.runDev,
environment,
frameworkDefinitionInitializers,
});
await manager.start();
}
const toolchains = new Set();
for (const framework of getFrameworks()) {
for (const addOn of framework.getAddOns()) {
if (addOn.type === 'toolchain') {
toolchains.add(addOn.id);
}
}
}
const deployments = new Set();
for (const framework of getFrameworks()) {
for (const addOn of framework.getAddOns()) {
if (addOn.type === 'deployment') {
deployments.add(addOn.id);
}
}
}
// Mode is always file-router (TanStack Start)
const defaultMode = 'file-router';
const categoryAliases = {
db: 'database',
postgres: 'database',
sql: 'database',
login: 'auth',
authentication: 'auth',
hosting: 'deployment',
deploy: 'deployment',
serverless: 'deployment',
errors: 'monitoring',
logging: 'monitoring',
content: 'cms',
'api-keys': 'api',
grid: 'data-grid',
review: 'code-review',
courses: 'learning',
};
function printJson(data) {
console.log(JSON.stringify(data, null, 2));
}
function parsePositiveInteger(value) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new InvalidArgumentError('Value must be a positive integer');
}
return parsed;
}
async function runWithTelemetry(command, opts, action) {
const telemetry = await createTelemetryClient({ json: opts.json });
const startedAt = Date.now();
currentTelemetry = telemetry;
telemetry.captureCommandStarted(command, {
...opts.properties,
cli_version: VERSION,
});
try {
const result = await action(telemetry);
await telemetry.captureCommandCompleted(command, Date.now() - startedAt);
return result;
}
catch (error) {
await telemetry.captureCommandFailed(command, Date.now() - startedAt, error);
throw error;
}
finally {
currentTelemetry = undefined;
}
}
program
.name(name)
.description(`${appName} CLI`)
.version(VERSION, '-v, --version', 'output the current version');
// Helper to create the create command action handler
async function handleCreate(projectName, options) {
try {
await runWithTelemetry('create', {
json: options.json,
properties: getCreateTelemetryProperties(projectName, options),
}, async (telemetry) => {
const legacyCreateFlags = validateLegacyCreateFlags(options);
if (legacyCreateFlags.error) {
throw new Error(legacyCreateFlags.error);
}
for (const warning of legacyCreateFlags.warnings) {
log.warn(warning);
}
if (options.listAddOns) {
const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
const visibleAddOns = addOns.filter((a) => !forcedAddOns.includes(a.id));
telemetry.mergeProperties({
result_count: visibleAddOns.length,
});
if (options.json) {
printJson(visibleAddOns.map((addOn) => ({
id: addOn.id,
name: addOn.name,
description: addOn.description,
type: addOn.type,
category: addOn.category,
phase: addOn.phase,
modes: addOn.modes,
link: addOn.link,
warning: addOn.warning,
exclusive: addOn.exclusive,
dependsOn: addOn.dependsOn,
options: addOn.options,
})));
return;
}
let hasConfigurableAddOns = false;
for (const addOn of visibleAddOns) {
const hasOptions = addOn.options && Object.keys(addOn.options).length > 0;
const optionMarker = hasOptions ? '*' : ' ';
if (hasOptions)
hasConfigurableAddOns = true;
console.log(`${optionMarker} ${chalk.bold(addOn.id)}: ${addOn.description}`);
}
if (hasConfigurableAddOns) {
console.log('\n* = has configuration options');
}
return;
}
if (options.addonDetails) {
const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
const addOn = addOns.find((a) => a.id === options.addonDetails) ??
addOns.find((a) => a.id.toLowerCase() === options.addonDetails.toLowerCase());
if (!addOn) {
throw new Error(`Add-on '${options.addonDetails}' not found`);
}
telemetry.mergeProperties({
add_on_file_count: (await addOn.getFiles()).length,
});
if (options.json) {
const files = await addOn.getFiles();
printJson({
id: addOn.id,
name: addOn.name,
description: addOn.description,
type: addOn.type,
category: addOn.category,
phase: addOn.phase,
modes: addOn.modes,
link: addOn.link,
warning: addOn.warning,
exclusive: addOn.exclusive,
dependsOn: addOn.dependsOn,
options: addOn.options,
routes: addOn.routes,
packageAdditions: addOn.packageAdditions,
shadcnComponents: addOn.shadcnComponents,
integrations: addOn.integrations,
readme: addOn.readme,
files,
author: addOn.author,
version: addOn.version,
license: addOn.license,
});
return;
}
console.log(`${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`);
console.log(`${chalk.bold('ID:')} ${addOn.id}`);
console.log(`${chalk.bold('Description:')} ${addOn.description}`);
console.log(`${chalk.bold('Type:')} ${addOn.type}`);
console.log(`${chalk.bold('Phase:')} ${addOn.phase}`);
console.log(`${chalk.bold('Supported Modes:')} ${addOn.modes.join(', ')}`);
if (addOn.link) {
console.log(`${chalk.bold('Link:')} ${chalk.blue(addOn.link)}`);
}
if (addOn.dependsOn && addOn.dependsOn.length > 0) {
console.log(`${chalk.bold('Dependencies:')} ${addOn.dependsOn.join(', ')}`);
}
if (addOn.options && Object.keys(addOn.options).length > 0) {
console.log(`\n${chalk.bold.yellow('Configuration Options:')}`);
for (const [optionName, option] of Object.entries(addOn.options)) {
if ('type' in option) {
const opt = option;
console.log(` ${chalk.bold(optionName)}:`);
console.log(` Label: ${opt.label}`);
if (opt.description) {
console.log(` Description: ${opt.description}`);
}
console.log(` Type: ${opt.type}`);
console.log(` Default: ${opt.default}`);
if (opt.type === 'select' && opt.options) {
console.log(` Available values:`);
for (const choice of opt.options) {
console.log(` - ${choice.value}: ${choice.label}`);
}
}
}
}
}
else {
console.log(`\n${chalk.gray('No configuration options available')}`);
}
if (addOn.routes && addOn.routes.length > 0) {
console.log(`\n${chalk.bold.green('Routes:')}`);
for (const route of addOn.routes) {
console.log(` ${chalk.bold(route.url)} (${route.name})`);
console.log(` File: ${route.path}`);
}
}
return;
}
if (options.devWatch) {
await startDevWatchMode(projectName, options);
return;
}
const cliOptions = {
projectName,
...options,
};
if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
cliOptions.routerOnly = true;
}
if (cliOptions.routerOnly !== true &&
cliOptions.template &&
['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(cliOptions.template.toLowerCase()) &&
cliOptions.template.toLowerCase() !== 'file-router') {
cliOptions.routerOnly = true;
}
cliOptions.framework = getFrameworkByName(options.framework || defaultFramework || 'React').id;
const nonInteractive = !!cliOptions.nonInteractive || !!cliOptions.yes;
if (cliOptions.interactive && nonInteractive) {
throw new Error('Cannot combine --interactive with --non-interactive/--yes.');
}
const addOnsFlagPassed = process.argv.includes('--add-ons');
const wantsInteractiveMode = !nonInteractive &&
(cliOptions.interactive ||
(cliOptions.addOns === true && addOnsFlagPassed));
let finalOptions;
if (wantsInteractiveMode) {
cliOptions.addOns = true;
}
else {
finalOptions = await normalizeOptions(cliOptions, forcedAddOns, { forcedDeployment });
}
if (nonInteractive) {
if (cliOptions.addOns === true) {
throw new Error('When using --non-interactive/--yes, pass explicit add-ons via --add-ons <ids>.');
}
}
if (finalOptions) {
intro(`Creating a new ${appName} app in ${projectName}...`);
}
else {
if (nonInteractive) {
throw new Error('Project name is required in non-interactive mode. Pass [project-name] or --target-dir.');
}
intro(`Let's configure your ${appName} application`);
finalOptions = await promptForCreateOptions(cliOptions, {
forcedAddOns,
showDeploymentOptions,
});
}
if (!finalOptions) {
throw new Error('No options were provided');
}
telemetry.mergeProperties(getResolvedCreateTelemetryProperties(finalOptions, cliOptions));
finalOptions.routerOnly =
!!cliOptions.routerOnly;
if (options.targetDir) {
finalOptions.targetDir = options.targetDir;
}
else if (finalOptions.targetDir) {
// Keep the normalized target dir.
}
else if (projectName === '.') {
finalOptions.targetDir = resolve(process.cwd());
}
else {
finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName);
}
await confirmTargetDirectorySafety(finalOptions.targetDir, options.force);
await createApp(environment, finalOptions);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
}
// Helper to configure create command options
function configureCreateCommand(cmd) {
cmd.argument('[project-name]', 'name of the project');
if (!defaultFramework) {
cmd.option('--framework <type>', `project framework (${availableFrameworks.join(', ')})`, (value) => {
if (value.toLowerCase() === 'react-cra') {
return 'react';
}
if (!availableFrameworks.some((f) => f.toLowerCase() === value.toLowerCase())) {
throw new InvalidArgumentError(`Invalid framework: ${value}. Only the following are allowed: ${availableFrameworks.join(', ')}`);
}
return value;
}, defaultFramework || 'React');
}
cmd
.option('--starter [url-or-id]', 'DEPRECATED: use --template. Initializes from a template URL or built-in id', false)
.option('--template-id <id>', 'initialize using a built-in template id')
.option('--template [url-or-id]', 'initialize this project from a template URL or built-in template id')
.option('--no-install', 'skip installing dependencies')
.option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
}
return value;
})
.option('--dev-watch <path>', 'Watch a framework directory for changes and auto-rebuild')
.option('--run-dev', 'Run the app dev server alongside dev-watch', false)
.option('--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)')
.option('--tailwind', 'Deprecated: compatibility flag; Tailwind is always enabled')
.option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored')
.option('--examples', 'include demo/example pages')
.option('--no-examples', 'exclude demo/example pages');
if (deployments.size > 0) {
cmd.option(`--deployment <${Array.from(deployments).join('|')}>`, `Explicitly tell the CLI to use this deployment adapter`, (value) => {
if (!deployments.has(value)) {
throw new InvalidArgumentError(`Invalid adapter: ${value}. The following are allowed: ${Array.from(deployments).join(', ')}`);
}
return value;
});
}
if (toolchains.size > 0) {
cmd
.option(`--toolchain <${Array.from(toolchains).join('|')}>`, `Explicitly tell the CLI to use this toolchain`, (value) => {
if (!toolchains.has(value)) {
throw new InvalidArgumentError(`Invalid toolchain: ${value}. The following are allowed: ${Array.from(toolchains).join(', ')}`);
}
return value;
})
.option('--no-toolchain', 'skip toolchain selection');
}
cmd
.option('--interactive', 'interactive mode', false)
.option('--non-interactive', 'skip prompts and use defaults', false)
.option('-y, --yes', 'accept defaults and skip prompts', false)
.option('--add-ons [...add-ons]', 'pick from a list of available add-ons (comma separated list)', (value) => {
let addOns = !!value;
if (typeof value === 'string') {
addOns = value.split(',').map((addon) => addon.trim());
}
return addOns;
})
.option('--list-add-ons', 'list all available add-ons', false)
.option('--addon-details <addon-id>', 'show detailed information about a specific add-on')
.option('--json', 'output JSON for automation', false)
.option('--git', 'create a git repository')
.option('--no-git', 'do not create a git repository')
.option('--target-dir <path>', 'the target directory for the application root')
.option('--add-on-config <config>', 'JSON string with add-on configuration options')
.option('-f, --force', 'force project creation even if the target directory is not empty', false);
return cmd;
}
// === CREATE SUBCOMMAND ===
// Creates a TanStack Start app (file-router mode).
const createCommand = program
.command('create')
.description(`Create a new TanStack Start application`);
configureCreateCommand(createCommand);
createCommand.action(handleCreate);
// === DEV SUBCOMMAND ===
const devCommand = program
.command('dev')
.description('Create a sandbox app and watch built-in framework templates/add-ons');
configureCreateCommand(devCommand);
devCommand.action(async (projectName, options) => {
try {
await runWithTelemetry('dev', {
properties: {
framework: options.framework
? sanitizeId(options.framework)
: sanitizeId(defaultFramework || 'react'),
install: options.install !== false,
run_dev: true,
},
}, async () => {
const frameworkName = options.framework || defaultFramework || 'React';
const framework = getFrameworkByName(frameworkName);
if (!framework) {
throw new Error(`Unknown framework: ${frameworkName}`);
}
const watchPath = resolveBuiltInDevWatchPath(framework.id);
const devOptions = {
...options,
framework: framework.name,
devWatch: watchPath,
runDev: true,
install: options.install ?? true,
};
await startDevWatchMode(projectName, devOptions);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === LIBRARIES SUBCOMMAND ===
program
.command('libraries')
.description('List TanStack libraries')
.option('--group <group>', `filter by group (${LIBRARY_GROUPS.join(', ')})`)
.option('--json', 'output JSON for automation', false)
.action(async (options) => {
try {
await runWithTelemetry('libraries', {
json: options.json,
properties: {
group: options.group ? sanitizeId(options.group) : undefined,
json: options.json,
},
}, async (telemetry) => {
const data = await fetchLibraries();
let libraries = data.libraries;
if (options.group &&
Object.prototype.hasOwnProperty.call(data.groups, options.group)) {
const groupIds = data.groups[options.group];
libraries = libraries.filter((lib) => groupIds.includes(lib.id));
}
const groupName = options.group
? data.groupNames[options.group] || options.group
: 'All Libraries';
const payload = {
group: groupName,
count: libraries.length,
libraries: libraries.map((lib) => ({
id: lib.id,
name: lib.name,
tagline: lib.tagline,
description: lib.description,
frameworks: lib.frameworks,
latestVersion: lib.latestVersion,
docsUrl: lib.docsUrl,
githubUrl: lib.githubUrl,
})),
};
telemetry.mergeProperties({
result_count: payload.count,
});
if (options.json) {
printJson(payload);
return;
}
console.log(chalk.bold(groupName));
for (const lib of payload.libraries) {
console.log(`${chalk.bold(lib.id)} (${lib.latestVersion}) - ${lib.tagline}`);
}
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === DOC SUBCOMMAND ===
program
.command('doc')
.description('Fetch a TanStack documentation page')
.argument('<library>', 'library ID (eg. query, router, table)')
.argument('<path>', 'documentation path (eg. framework/react/overview)')
.option('--docs-version <version>', 'docs version (default: latest)', 'latest')
.option('--json', 'output JSON for automation', false)
.action(async (libraryId, path, options) => {
try {
await runWithTelemetry('doc', {
json: options.json,
properties: {
doc_path_depth: path.split('/').filter(Boolean).length,
docs_version: sanitizeId(options.docsVersion),
json: options.json,
library: sanitizeId(libraryId),
},
}, async (telemetry) => {
const data = await fetchLibraries();
const library = data.libraries.find((l) => l.id === libraryId);
if (!library) {
throw new Error(`Library "${libraryId}" not found. Use \`tanstack libraries\` to see available libraries.`);
}
if (options.docsVersion !== 'latest' &&
!library.availableVersions.includes(options.docsVersion)) {
throw new Error(`Version "${options.docsVersion}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`);
}
const branch = options.docsVersion === 'latest' ||
options.docsVersion === library.latestVersion
? library.latestBranch || 'main'
: options.docsVersion;
const docsRoot = library.docsRoot || 'docs';
const filePath = `${docsRoot}/${path}.md`;
const content = await fetchDocContent(library.repo, branch, filePath);
if (!content) {
throw new Error(`Document not found: ${library.name} / ${path} (version: ${options.docsVersion})`);
}
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
let title = path.split('/').pop() || 'Untitled';
let docContent = content;
if (frontmatterMatch && frontmatterMatch[1]) {
const frontmatter = frontmatterMatch[1];
const titleMatch = frontmatter.match(/title:\s*['"]?([^'"\n]+)['"]?/);
if (titleMatch && titleMatch[1]) {
title = titleMatch[1];
}
docContent = content.slice(frontmatterMatch[0].length).trim();
}
const payload = {
title,
content: docContent,
url: `https://tanstack.com/${libraryId}/${options.docsVersion}/docs/${path}`,
library: library.name,
version: options.docsVersion === 'latest'
? library.latestVersion
: options.docsVersion,
};
telemetry.mergeProperties({
content_length_bucket: getLengthBucket(docContent),
});
if (options.json) {
printJson(payload);
return;
}
console.log(chalk.bold(payload.title));
console.log(chalk.blue(payload.url));
console.log('');
console.log(payload.content);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === SEARCH-DOCS SUBCOMMAND ===
program
.command('search-docs')
.description('Search TanStack documentation')
.argument('<query>', 'search query')
.option('--library <id>', 'filter to specific library')
.option('--framework <name>', 'filter to specific framework')
.option('--limit <n>', 'max results (default: 10, max: 50)', parsePositiveInteger, 10)
.option('--json', 'output JSON for automation', false)
.action(async (query, options) => {
try {
await runWithTelemetry('search-docs', {
json: options.json,
properties: {
framework: options.framework
? sanitizeId(options.framework)
: undefined,
has_query: query.trim().length > 0,
json: options.json,
library: options.library ? sanitizeId(options.library) : undefined,
limit: options.limit,
query_length_bucket: getLengthBucket(query),
},
}, async (telemetry) => {
const payload = await searchTanStackDocs({
query,
library: options.library,
framework: options.framework,
limit: options.limit,
});
telemetry.mergeProperties({
result_count: payload.totalHits,
});
if (options.json) {
printJson(payload);
return;
}
for (const result of payload.results) {
console.log(`${chalk.bold(result.title)} [${result.library}]\n${chalk.blue(result.url)}\n${result.snippet}\n`);
}
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === ECOSYSTEM SUBCOMMAND ===
program
.command('ecosystem')
.description('List TanStack ecosystem partners')
.option('--category <category>', 'filter by category')
.option('--library <id>', 'filter by TanStack library')
.option('--json', 'output JSON for automation', false)
.action(async (options) => {
try {
await runWithTelemetry('ecosystem', {
json: options.json,
properties: {
category: options.category ? sanitizeId(options.category) : undefined,
json: options.json,
library: options.library ? sanitizeId(options.library) : undefined,
},
}, async (telemetry) => {
const data = await fetchPartners();
let resolvedCategory;
if (options.category) {
const normalized = options.category.toLowerCase().trim();
resolvedCategory = categoryAliases[normalized] || normalized;
if (!data.categories.includes(resolvedCategory)) {
resolvedCategory = undefined;
}
}
const library = options.library?.toLowerCase().trim();
const partners = data.partners
.filter((partner) => resolvedCategory ? partner.category === resolvedCategory : true)
.filter((partner) => library ? partner.libraries.some((l) => l === library) : true)
.map((partner) => ({
id: partner.id,
name: partner.name,
tagline: partner.tagline,
description: partner.description,
category: partner.category,
categoryLabel: partner.categoryLabel,
url: partner.url,
libraries: partner.libraries,
}));
const payload = {
query: {
category: options.category,
categoryResolved: resolvedCategory,
library: options.library,
},
count: partners.length,
partners,
};
telemetry.mergeProperties({
category_resolved: resolvedCategory
? sanitizeId(resolvedCategory)
: undefined,
result_count: payload.count,
});
if (options.json) {
printJson(payload);
return;
}
for (const partner of partners) {
console.log(`${chalk.bold(partner.name)} [${partner.category}] - ${partner.description}\n${chalk.blue(partner.url)}`);
}
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === PIN-VERSIONS SUBCOMMAND ===
program
.command('pin-versions')
.description('Pin versions of the TanStack libraries')
.action(async () => {
try {
await runWithTelemetry('pin-versions', {}, async (telemetry) => {
if (!fs.existsSync('package.json')) {
throw new Error('package.json not found');
}
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const packages = {
'@tanstack/react-router': '',
'@tanstack/router-generator': '',
'@tanstack/react-router-devtools': '',
'@tanstack/react-start': '',
'@tanstack/react-start-config': '',
'@tanstack/router-plugin': '',
'@tanstack/react-start-client': '',
'@tanstack/react-start-plugin': '1.115.0',
'@tanstack/react-start-server': '',
'@tanstack/start-server-core': '1.115.0',
};
function sortObject(obj) {
return Object.keys(obj)
.sort()
.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {});
}
if (!packageJson.dependencies['@tanstack/react-start']) {
throw new Error('@tanstack/react-start not found in dependencies');
}
let changed = 0;
const startVersion = packageJson.dependencies['@tanstack/react-start'].replace(/^\^/, '');
for (const pkg of Object.keys(packages)) {
if (!packageJson.dependencies[pkg]) {
packageJson.dependencies[pkg] = packages[pkg].length
? semver.maxSatisfying([startVersion, packages[pkg]], `^${packages[pkg]}`)
: startVersion;
changed++;
}
else {
if (packageJson.dependencies[pkg].startsWith('^')) {
packageJson.dependencies[pkg] = packageJson.dependencies[pkg].replace(/^\^/, '');
changed++;
}
}
}
telemetry.mergeProperties({
changed_count: changed,
});
packageJson.dependencies = sortObject(packageJson.dependencies);
if (changed > 0) {
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
console.log(`${changed} packages updated.
Remove your node_modules directory and package lock file and re-install.`);
}
else {
console.log('No changes needed. The relevant TanStack packages are already pinned.');
}
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
const telemetryCommand = program.command('telemetry');
telemetryCommand
.command('status')
.description('Show anonymous telemetry status')
.option('--json', 'output JSON for automation', false)
.action(async (options) => {
const status = await getTelemetryStatus({ createIfMissing: true });
const payload = {
configPath: status.configPath,
disabledBy: status.disabledBy,
distinctId: status.distinctId,
enabled: status.enabled,
noticeVersion: status.noticeVersion,
};
if (options.json) {
printJson(payload);
return;
}
console.log(`Telemetry ${status.enabled ? 'enabled' : 'disabled'}`);
console.log(`Config: ${status.configPath}`);
if (status.disabledBy) {
console.log(`Disabled by: ${status.disabledBy}`);
}
});
telemetryCommand
.command('enable')
.description('Enable anonymous telemetry')
.action(async () => {
await setTelemetryEnabled(true);
console.log('Anonymous telemetry enabled');
});
telemetryCommand
.command('disable')
.description('Disable anonymous telemetry')
.action(async () => {
await setTelemetryEnabled(false);
console.log('Anonymous telemetry disabled');
});
// === ADD SUBCOMMAND ===
program
.command('add')
.argument('[add-on...]', 'Name of the add-ons (or add-ons separated by spaces or commas)')
.option('--forced', 'Force the add-on to be added', false)
.action(async (addOns, options) => {
try {
await runWithTelemetry('add', {
properties: {
forced: options.forced,
},
}, async (telemetry) => {
const parsedAddOns = [];
for (const addOn of addOns) {
if (addOn.includes(',') || addOn.includes(' ')) {
parsedAddOns.push(...addOn.split(/[\s,]+/).map((addon) => addon.trim()));
}
else {
parsedAddOns.push(addOn.trim());
}
}
if (parsedAddOns.length < 1) {
const selectedAddOns = await promptForAddOns();
telemetry.mergeProperties({
add_on_count: selectedAddOns.length,
add_on_ids: sanitizeIdList(selectedAddOns),
prompted: true,
});
if (selectedAddOns.length) {
await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
forced: options.forced,
});
}
return;
}
telemetry.mergeProperties({
add_on_count: parsedAddOns.length,
add_on_ids: sanitizeIdList(parsedAddOns),
prompted: false,
});
await addToApp(environment, parsedAddOns, resolve(process.cwd()), {
forced: options.forced,
});
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === ADD-ON SUBCOMMAND ===
const addOnCommand = program.command('add-on');
addOnCommand
.command('init')
.description('Initialize an add-on from the current project')
.action(async () => {
try {
await runWithTelemetry('add-on:init', {}, async () => {
await initAddOn(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
addOnCommand
.command('compile')
.description('Update add-on from the current project')
.action(async () => {
try {
await runWithTelemetry('add-on:compile', {}, async () => {
await compileAddOn(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
addOnCommand
.command('dev')
.description('Watch project files and continuously refresh .add-on and add-on.json')
.action(async () => {
try {
await runWithTelemetry('add-on:dev', {}, async () => {
await devAddOn(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// === TEMPLATE SUBCOMMAND ===
const templateCommand = program.command('template');
templateCommand
.command('init')
.description('Initialize a project template from the current project')
.action(async () => {
try {
await runWithTelemetry('template:init', {}, async () => {
await initStarter(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
templateCommand
.command('compile')
.description('Compile the template JSON file for the current project')
.action(async () => {
try {
await runWithTelemetry('template:compile', {}, async () => {
await compileStarter(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
// Legacy alias for template command
const starterCommand = program.command('starter');
starterCommand
.command('init')
.description('Deprecated alias: initialize a project template')
.action(async () => {
try {
await runWithTelemetry('starter:init', {}, async () => {
await initStarter(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}
});
starterCommand
.command('compile')
.description('Deprecated alias: compile the template JSON file')
.action(async () => {
try {
await runWithTelemetry('starter:compile', {}, async () => {
await compileStarter(environment);
});
}
catch (error) {
log.error(formatErrorMessage(error));
process.exit(1);
}