@tanstack/cli 0.0.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/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@tanstack/cli",
3
+ "version": "0.0.1",
4
+ "description": "TanStack CLI for scaffolding and tooling",
5
+ "author": "Tanner Linsley",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/cli.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "homepage": "https://tanstack.com/cli",
13
+ "keywords": [
14
+ "tanstack",
15
+ "cli",
16
+ "scaffold",
17
+ "mcp",
18
+ "create",
19
+ "start"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "import": {
25
+ "types": "./dist/index.d.mts",
26
+ "default": "./dist/index.mjs"
27
+ },
28
+ "require": {
29
+ "types": "./dist/index.d.cts",
30
+ "default": "./dist/index.cjs"
31
+ }
32
+ }
33
+ },
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.mjs",
36
+ "types": "./dist/index.d.mts",
37
+ "bin": {
38
+ "tanstack": "./dist/bin.mjs"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsdown --clean",
46
+ "test:types": "tsc --noEmit",
47
+ "test:eslint": "eslint ./src",
48
+ "test:lib": "vitest run --passWithNoTests"
49
+ },
50
+ "dependencies": {
51
+ "@clack/prompts": "^0.10.0",
52
+ "@modelcontextprotocol/sdk": "^1.6.0",
53
+ "chalk": "^5.4.1",
54
+ "commander": "^13.1.0",
55
+ "ejs": "^3.1.10",
56
+ "express": "^4.21.2",
57
+ "ignore": "^7.0.5",
58
+ "parse-gitignore": "^2.0.0",
59
+ "zod": "^3.24.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/ejs": "^3.1.5",
63
+ "@types/express": "^5.0.1",
64
+ "@types/node": "^25.0.7"
65
+ }
66
+ }
@@ -0,0 +1,114 @@
1
+ import { resolve } from 'node:path'
2
+ import { describe, expect, it } from 'vitest'
3
+ import {
4
+ fetchManifest,
5
+ fetchIntegration,
6
+ fetchIntegrations,
7
+ fetchIntegrationInfo,
8
+ fetchIntegrationFiles,
9
+ } from './fetch.js'
10
+
11
+ const INTEGRATIONS_PATH = resolve(__dirname, '../../../../integrations')
12
+
13
+ describe('fetch API', () => {
14
+ describe('fetchManifest', () => {
15
+ it('should fetch manifest from local path', async () => {
16
+ const manifest = await fetchManifest(INTEGRATIONS_PATH)
17
+
18
+ expect(manifest).toBeDefined()
19
+ expect(manifest.integrations).toBeInstanceOf(Array)
20
+ expect(manifest.integrations.length).toBeGreaterThan(0)
21
+ })
22
+
23
+ it('should have integrations with required fields', async () => {
24
+ const manifest = await fetchManifest(INTEGRATIONS_PATH)
25
+
26
+ for (const integration of manifest.integrations) {
27
+ expect(integration.id).toBeDefined()
28
+ expect(integration.name).toBeDefined()
29
+ expect(integration.modes).toBeInstanceOf(Array)
30
+ }
31
+ })
32
+
33
+ it('should throw for non-existent path', async () => {
34
+ await expect(fetchManifest('/non/existent/path')).rejects.toThrow()
35
+ })
36
+ })
37
+
38
+ describe('fetchIntegrationInfo', () => {
39
+ it('should fetch integration info from local path', async () => {
40
+ const info = await fetchIntegrationInfo('tanstack-query', INTEGRATIONS_PATH)
41
+
42
+ expect(info.name).toBe('TanStack Query')
43
+ expect(info.type).toBe('integration')
44
+ expect(info.modes).toContain('file-router')
45
+ })
46
+
47
+ it('should throw for non-existent integration', async () => {
48
+ await expect(
49
+ fetchIntegrationInfo('non-existent', INTEGRATIONS_PATH),
50
+ ).rejects.toThrow()
51
+ })
52
+ })
53
+
54
+ describe('fetchIntegrationFiles', () => {
55
+ it('should fetch integration files from local path', async () => {
56
+ const files = await fetchIntegrationFiles('tanstack-query', INTEGRATIONS_PATH)
57
+
58
+ expect(Object.keys(files).length).toBeGreaterThan(0)
59
+ // Files are in assets/src/integrations/query/
60
+ expect(files).toHaveProperty('src/integrations/query/provider.tsx')
61
+ })
62
+
63
+ it('should return empty object for integration without assets', async () => {
64
+ // This tests the case where assets dir doesn't exist
65
+ const files = await fetchIntegrationFiles('non-existent', INTEGRATIONS_PATH)
66
+ expect(files).toEqual({})
67
+ })
68
+ })
69
+
70
+ describe('fetchIntegration', () => {
71
+ it('should fetch complete integration', async () => {
72
+ const integration = await fetchIntegration('tanstack-query', INTEGRATIONS_PATH)
73
+
74
+ expect(integration.id).toBe('tanstack-query')
75
+ expect(integration.name).toBe('TanStack Query')
76
+ expect(integration.files).toBeDefined()
77
+ expect(Object.keys(integration.files).length).toBeGreaterThan(0)
78
+ })
79
+
80
+ it('should include hooks if defined', async () => {
81
+ const integration = await fetchIntegration('tanstack-query', INTEGRATIONS_PATH)
82
+
83
+ expect(integration.hooks).toBeDefined()
84
+ expect(integration.hooks!.length).toBeGreaterThan(0)
85
+ })
86
+
87
+ it('should merge package.json into packageAdditions', async () => {
88
+ const integration = await fetchIntegration('tanstack-query', INTEGRATIONS_PATH)
89
+
90
+ expect(integration.packageAdditions).toBeDefined()
91
+ expect(integration.packageAdditions?.dependencies).toHaveProperty(
92
+ '@tanstack/react-query',
93
+ )
94
+ })
95
+ })
96
+
97
+ describe('fetchIntegrations', () => {
98
+ it('should fetch multiple integrations in parallel', async () => {
99
+ const integrations = await fetchIntegrations(
100
+ ['tanstack-query', 'tanstack-form'],
101
+ INTEGRATIONS_PATH,
102
+ )
103
+
104
+ expect(integrations).toHaveLength(2)
105
+ expect(integrations[0]?.id).toBe('tanstack-query')
106
+ expect(integrations[1]?.id).toBe('tanstack-form')
107
+ })
108
+
109
+ it('should return empty array for empty input', async () => {
110
+ const integrations = await fetchIntegrations([], INTEGRATIONS_PATH)
111
+ expect(integrations).toEqual([])
112
+ })
113
+ })
114
+ })
@@ -0,0 +1,221 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import {
4
+ IntegrationCompiledSchema,
5
+ IntegrationInfoSchema,
6
+ ManifestSchema
7
+ } from '../engine/types.js'
8
+ import type { IntegrationCompiled, IntegrationInfo, Manifest } from '../engine/types.js'
9
+
10
+ const GITHUB_RAW_BASE =
11
+ 'https://raw.githubusercontent.com/TanStack/cli/main/integrations'
12
+
13
+ /**
14
+ * Check if a path is a local directory
15
+ */
16
+ function isLocalPath(path: string): boolean {
17
+ return path.startsWith('/') || path.startsWith('./') || path.startsWith('..')
18
+ }
19
+
20
+ /**
21
+ * Fetch the integration manifest from GitHub or local path
22
+ */
23
+ export async function fetchManifest(
24
+ baseUrl: string = GITHUB_RAW_BASE,
25
+ ): Promise<Manifest> {
26
+ if (isLocalPath(baseUrl)) {
27
+ const manifestPath = join(baseUrl, 'manifest.json')
28
+ if (!existsSync(manifestPath)) {
29
+ throw new Error(`Manifest not found at ${manifestPath}`)
30
+ }
31
+ const data = JSON.parse(readFileSync(manifestPath, 'utf-8'))
32
+ return ManifestSchema.parse(data)
33
+ }
34
+
35
+ const url = `${baseUrl}/manifest.json`
36
+ const response = await fetch(url)
37
+
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to fetch manifest: ${response.statusText}`)
40
+ }
41
+
42
+ const data = await response.json()
43
+ return ManifestSchema.parse(data)
44
+ }
45
+
46
+ /**
47
+ * Fetch integration info.json from GitHub or local path
48
+ */
49
+ export async function fetchIntegrationInfo(
50
+ integrationId: string,
51
+ baseUrl: string = GITHUB_RAW_BASE,
52
+ ): Promise<IntegrationInfo> {
53
+ if (isLocalPath(baseUrl)) {
54
+ const infoPath = join(baseUrl, integrationId, 'info.json')
55
+ if (!existsSync(infoPath)) {
56
+ throw new Error(`Integration info not found at ${infoPath}`)
57
+ }
58
+ const data = JSON.parse(readFileSync(infoPath, 'utf-8'))
59
+ return IntegrationInfoSchema.parse(data)
60
+ }
61
+
62
+ const url = `${baseUrl}/${integrationId}/info.json`
63
+ const response = await fetch(url)
64
+
65
+ if (!response.ok) {
66
+ throw new Error(`Failed to fetch integration ${integrationId}: ${response.statusText}`)
67
+ }
68
+
69
+ const data = await response.json()
70
+ return IntegrationInfoSchema.parse(data)
71
+ }
72
+
73
+ /**
74
+ * Recursively read all files from a directory
75
+ */
76
+ function readDirRecursive(
77
+ dir: string,
78
+ basePath: string = '',
79
+ ): Record<string, string> {
80
+ const files: Record<string, string> = {}
81
+
82
+ if (!existsSync(dir)) return files
83
+
84
+ for (const entry of readdirSync(dir)) {
85
+ const fullPath = join(dir, entry)
86
+ const relativePath = basePath ? `${basePath}/${entry}` : entry
87
+ const stat = statSync(fullPath)
88
+
89
+ if (stat.isDirectory()) {
90
+ Object.assign(files, readDirRecursive(fullPath, relativePath))
91
+ } else {
92
+ files[relativePath] = readFileSync(fullPath, 'utf-8')
93
+ }
94
+ }
95
+
96
+ return files
97
+ }
98
+
99
+ /**
100
+ * Fetch all files for an integration from GitHub or local path
101
+ */
102
+ export async function fetchIntegrationFiles(
103
+ integrationId: string,
104
+ baseUrl: string = GITHUB_RAW_BASE,
105
+ ): Promise<Record<string, string>> {
106
+ if (isLocalPath(baseUrl)) {
107
+ const assetsPath = join(baseUrl, integrationId, 'assets')
108
+ return readDirRecursive(assetsPath)
109
+ }
110
+
111
+ // First fetch the file list (we'll need a files.json or similar)
112
+ const filesUrl = `${baseUrl}/${integrationId}/files.json`
113
+ const response = await fetch(filesUrl)
114
+
115
+ if (!response.ok) {
116
+ // No files.json, return empty
117
+ return {}
118
+ }
119
+
120
+ const fileList: Array<string> = await response.json()
121
+ const files: Record<string, string> = {}
122
+
123
+ // Fetch each file
124
+ await Promise.all(
125
+ fileList.map(async (filePath) => {
126
+ const fileUrl = `${baseUrl}/${integrationId}/assets/${filePath}`
127
+ const fileResponse = await fetch(fileUrl)
128
+
129
+ if (fileResponse.ok) {
130
+ files[filePath] = await fileResponse.text()
131
+ }
132
+ }),
133
+ )
134
+
135
+ return files
136
+ }
137
+
138
+ /**
139
+ * Fetch integration package.json if it exists
140
+ */
141
+ async function fetchIntegrationPackageJson(
142
+ integrationId: string,
143
+ baseUrl: string,
144
+ ): Promise<{
145
+ dependencies?: Record<string, string>
146
+ devDependencies?: Record<string, string>
147
+ scripts?: Record<string, string>
148
+ } | null> {
149
+ if (isLocalPath(baseUrl)) {
150
+ const pkgPath = join(baseUrl, integrationId, 'package.json')
151
+ if (existsSync(pkgPath)) {
152
+ return JSON.parse(readFileSync(pkgPath, 'utf-8'))
153
+ }
154
+ return null
155
+ }
156
+
157
+ const url = `${baseUrl}/${integrationId}/package.json`
158
+ const response = await fetch(url)
159
+
160
+ if (!response.ok) {
161
+ return null
162
+ }
163
+
164
+ return response.json()
165
+ }
166
+
167
+ /**
168
+ * Fetch a complete compiled integration from GitHub
169
+ */
170
+ export async function fetchIntegration(
171
+ integrationId: string,
172
+ baseUrl: string = GITHUB_RAW_BASE,
173
+ ): Promise<IntegrationCompiled> {
174
+ const [info, files, pkgJson] = await Promise.all([
175
+ fetchIntegrationInfo(integrationId, baseUrl),
176
+ fetchIntegrationFiles(integrationId, baseUrl),
177
+ fetchIntegrationPackageJson(integrationId, baseUrl),
178
+ ])
179
+
180
+ // Merge package.json into packageAdditions if present
181
+ const packageAdditions = info.packageAdditions ?? {}
182
+ if (pkgJson) {
183
+ if (pkgJson.dependencies) {
184
+ packageAdditions.dependencies = {
185
+ ...packageAdditions.dependencies,
186
+ ...pkgJson.dependencies,
187
+ }
188
+ }
189
+ if (pkgJson.devDependencies) {
190
+ packageAdditions.devDependencies = {
191
+ ...packageAdditions.devDependencies,
192
+ ...pkgJson.devDependencies,
193
+ }
194
+ }
195
+ if (pkgJson.scripts) {
196
+ packageAdditions.scripts = {
197
+ ...packageAdditions.scripts,
198
+ ...pkgJson.scripts,
199
+ }
200
+ }
201
+ }
202
+
203
+ return IntegrationCompiledSchema.parse({
204
+ ...info,
205
+ id: integrationId,
206
+ files,
207
+ packageAdditions:
208
+ Object.keys(packageAdditions).length > 0 ? packageAdditions : undefined,
209
+ deletedFiles: [],
210
+ })
211
+ }
212
+
213
+ /**
214
+ * Fetch multiple integrations in parallel
215
+ */
216
+ export async function fetchIntegrations(
217
+ integrationIds: Array<string>,
218
+ baseUrl: string = GITHUB_RAW_BASE,
219
+ ): Promise<Array<IntegrationCompiled>> {
220
+ return Promise.all(integrationIds.map((id) => fetchIntegration(id, baseUrl)))
221
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from 'node:path'
4
+ import { Command } from 'commander'
5
+ import { runCreate } from './commands/create.js'
6
+ import { runMcp } from './commands/mcp.js'
7
+ import { compileIntegration, initIntegration } from './engine/custom-addons/integration.js'
8
+ import { compileTemplate, initTemplate } from './engine/custom-addons/template.js'
9
+
10
+ const program = new Command()
11
+
12
+ program
13
+ .name('tanstack')
14
+ .description('TanStack CLI for scaffolding and tooling')
15
+ .version('0.0.1')
16
+
17
+ program
18
+ .command('create')
19
+ .argument('[project-name]', 'name of the project')
20
+ .option('--template <template>', 'URL to a custom template JSON file')
21
+ .option('--package-manager <pm>', 'package manager (npm, pnpm, yarn, bun)')
22
+ .option('--integrations <integrations>', 'comma-separated list of integration IDs')
23
+ .option('--no-install', 'skip installing dependencies')
24
+ .option('--no-git', 'skip initializing git repository')
25
+ .option('--no-tailwind', 'skip tailwind CSS')
26
+ .option('-y, --yes', 'skip prompts and use defaults')
27
+ .option('--target-dir <path>', 'target directory for the project')
28
+ .option('--integrations-path <path>', 'local path to integrations directory (for development)')
29
+ .description('Create a new TanStack Start project')
30
+ .action(runCreate)
31
+
32
+ program
33
+ .command('mcp')
34
+ .option('--sse', 'run in SSE mode (for HTTP transport)')
35
+ .option('--port <port>', 'port for SSE server', '8080')
36
+ .description('Start the MCP server for AI agents')
37
+ .action(runMcp)
38
+
39
+ // Integration commands
40
+ const integrationCommand = program.command('integration')
41
+
42
+ integrationCommand
43
+ .command('init')
44
+ .option('--integrations-path <path>', 'local path to integrations directory (for development)')
45
+ .description('Initialize an integration from the current project')
46
+ .action(async (options: { integrationsPath?: string }) => {
47
+ try {
48
+ await initIntegration(resolve(process.cwd()), options.integrationsPath)
49
+ } catch (error) {
50
+ console.error(error instanceof Error ? error.message : 'An error occurred')
51
+ process.exit(1)
52
+ }
53
+ })
54
+
55
+ integrationCommand
56
+ .command('compile')
57
+ .option('--integrations-path <path>', 'local path to integrations directory (for development)')
58
+ .description('Compile/update the integration from the current project')
59
+ .action(async (options: { integrationsPath?: string }) => {
60
+ try {
61
+ await compileIntegration(resolve(process.cwd()), options.integrationsPath)
62
+ } catch (error) {
63
+ console.error(error instanceof Error ? error.message : 'An error occurred')
64
+ process.exit(1)
65
+ }
66
+ })
67
+
68
+ // Custom template commands
69
+ const templateCommand = program.command('template')
70
+
71
+ templateCommand
72
+ .command('init')
73
+ .description('Initialize a custom template from the current project')
74
+ .action(async () => {
75
+ try {
76
+ await initTemplate(resolve(process.cwd()))
77
+ } catch (error) {
78
+ console.error(error instanceof Error ? error.message : 'An error occurred')
79
+ process.exit(1)
80
+ }
81
+ })
82
+
83
+ templateCommand
84
+ .command('compile')
85
+ .description('Compile/update the custom template from the current project')
86
+ .action(async () => {
87
+ try {
88
+ await compileTemplate(resolve(process.cwd()))
89
+ } catch (error) {
90
+ console.error(error instanceof Error ? error.message : 'An error occurred')
91
+ process.exit(1)
92
+ }
93
+ })
94
+
95
+ program.parse()