@tanstack/cli 0.0.7 → 0.48.2

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.
Files changed (83) hide show
  1. package/dist/bin.js +7 -0
  2. package/dist/cli.js +481 -0
  3. package/dist/command-line.js +174 -0
  4. package/dist/dev-watch.js +290 -0
  5. package/dist/file-syncer.js +148 -0
  6. package/dist/index.js +1 -0
  7. package/dist/mcp/api.js +31 -0
  8. package/dist/mcp/tools.js +250 -0
  9. package/dist/mcp/types.js +37 -0
  10. package/dist/mcp.js +121 -0
  11. package/dist/options.js +162 -0
  12. package/dist/types/bin.d.ts +2 -0
  13. package/dist/types/cli.d.ts +16 -0
  14. package/dist/types/command-line.d.ts +10 -0
  15. package/dist/types/dev-watch.d.ts +27 -0
  16. package/dist/types/file-syncer.d.ts +18 -0
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/mcp/api.d.ts +4 -0
  19. package/dist/types/mcp/tools.d.ts +2 -0
  20. package/dist/types/mcp/types.d.ts +217 -0
  21. package/dist/types/mcp.d.ts +6 -0
  22. package/dist/types/options.d.ts +8 -0
  23. package/dist/types/types.d.ts +25 -0
  24. package/dist/types/ui-environment.d.ts +2 -0
  25. package/dist/types/ui-prompts.d.ts +12 -0
  26. package/dist/types/utils.d.ts +8 -0
  27. package/dist/types.js +1 -0
  28. package/dist/ui-environment.js +52 -0
  29. package/dist/ui-prompts.js +244 -0
  30. package/dist/utils.js +30 -0
  31. package/package.json +46 -46
  32. package/src/bin.ts +6 -93
  33. package/src/cli.ts +692 -0
  34. package/src/command-line.ts +236 -0
  35. package/src/dev-watch.ts +430 -0
  36. package/src/file-syncer.ts +205 -0
  37. package/src/index.ts +1 -85
  38. package/src/mcp.ts +190 -0
  39. package/src/options.ts +260 -0
  40. package/src/types.ts +27 -0
  41. package/src/ui-environment.ts +74 -0
  42. package/src/ui-prompts.ts +322 -0
  43. package/src/utils.ts +38 -0
  44. package/tests/command-line.test.ts +304 -0
  45. package/tests/index.test.ts +9 -0
  46. package/tests/mcp.test.ts +225 -0
  47. package/tests/options.test.ts +304 -0
  48. package/tests/setupVitest.ts +6 -0
  49. package/tests/ui-environment.test.ts +97 -0
  50. package/tests/ui-prompts.test.ts +238 -0
  51. package/tsconfig.json +17 -0
  52. package/vitest.config.js +7 -0
  53. package/dist/bin.cjs +0 -761
  54. package/dist/bin.d.cts +0 -1
  55. package/dist/bin.d.mts +0 -1
  56. package/dist/bin.mjs +0 -760
  57. package/dist/index.cjs +0 -36
  58. package/dist/index.d.cts +0 -1172
  59. package/dist/index.d.mts +0 -1172
  60. package/dist/index.mjs +0 -3
  61. package/dist/template-CkAkdP8n.mjs +0 -2545
  62. package/dist/template-Cup47s9h.cjs +0 -2783
  63. package/src/api/fetch.test.ts +0 -114
  64. package/src/api/fetch.ts +0 -249
  65. package/src/cache/index.ts +0 -89
  66. package/src/commands/create.ts +0 -463
  67. package/src/commands/mcp.test.ts +0 -152
  68. package/src/commands/mcp.ts +0 -203
  69. package/src/engine/compile-with-addons.test.ts +0 -302
  70. package/src/engine/compile.test.ts +0 -404
  71. package/src/engine/compile.ts +0 -551
  72. package/src/engine/config-file.test.ts +0 -118
  73. package/src/engine/config-file.ts +0 -61
  74. package/src/engine/custom-addons/integration.ts +0 -323
  75. package/src/engine/custom-addons/shared.test.ts +0 -98
  76. package/src/engine/custom-addons/shared.ts +0 -281
  77. package/src/engine/custom-addons/template.test.ts +0 -288
  78. package/src/engine/custom-addons/template.ts +0 -124
  79. package/src/engine/template.test.ts +0 -256
  80. package/src/engine/template.ts +0 -269
  81. package/src/engine/types.ts +0 -336
  82. package/src/parse-gitignore.d.ts +0 -5
  83. package/src/templates/base.ts +0 -891
@@ -0,0 +1,205 @@
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
+ errors: Array<string>
16
+ }
17
+
18
+ export class FileSyncer {
19
+ async sync(sourceDir: string, targetDir: string): Promise<SyncResult> {
20
+ const result: SyncResult = {
21
+ updated: [],
22
+ skipped: [],
23
+ created: [],
24
+ errors: [],
25
+ }
26
+
27
+ // Ensure directories exist
28
+ if (!fs.existsSync(sourceDir)) {
29
+ throw new Error(`Source directory does not exist: ${sourceDir}`)
30
+ }
31
+ if (!fs.existsSync(targetDir)) {
32
+ throw new Error(`Target directory does not exist: ${targetDir}`)
33
+ }
34
+
35
+ // Walk through source directory and sync files
36
+ await this.syncDirectory(sourceDir, targetDir, sourceDir, result)
37
+
38
+ return result
39
+ }
40
+
41
+ private async syncDirectory(
42
+ currentPath: string,
43
+ targetBase: string,
44
+ sourceBase: string,
45
+ result: SyncResult,
46
+ ): Promise<void> {
47
+ const entries = await fs.promises.readdir(currentPath, {
48
+ withFileTypes: true,
49
+ })
50
+
51
+ for (const entry of entries) {
52
+ const sourcePath = path.join(currentPath, entry.name)
53
+ const relativePath = path.relative(sourceBase, sourcePath)
54
+ const targetPath = path.join(targetBase, relativePath)
55
+
56
+ // Skip certain directories
57
+ if (entry.isDirectory()) {
58
+ if (this.shouldSkipDirectory(entry.name)) {
59
+ continue
60
+ }
61
+
62
+ // Ensure target directory exists
63
+ if (!fs.existsSync(targetPath)) {
64
+ await fs.promises.mkdir(targetPath, { recursive: true })
65
+ }
66
+
67
+ // Recursively sync subdirectory
68
+ await this.syncDirectory(sourcePath, targetBase, sourceBase, result)
69
+ } else if (entry.isFile()) {
70
+ // Skip certain files
71
+ if (this.shouldSkipFile(entry.name)) {
72
+ continue
73
+ }
74
+
75
+ try {
76
+ const shouldUpdate = await this.shouldUpdateFile(
77
+ sourcePath,
78
+ targetPath,
79
+ )
80
+
81
+ if (shouldUpdate) {
82
+ // Check if file exists to generate diff
83
+ let fileDiff: string | undefined
84
+ const targetExists = fs.existsSync(targetPath)
85
+
86
+ if (targetExists) {
87
+ // Generate diff for existing files
88
+ const oldContent = await fs.promises.readFile(targetPath, 'utf-8')
89
+ const newContent = await fs.promises.readFile(sourcePath, 'utf-8')
90
+
91
+ const changes = diff.createPatch(
92
+ relativePath,
93
+ oldContent,
94
+ newContent,
95
+ 'Previous',
96
+ 'Current',
97
+ )
98
+
99
+ // Only include diff if there are actual changes
100
+ if (changes && changes.split('\n').length > 5) {
101
+ fileDiff = changes
102
+ }
103
+ }
104
+
105
+ // Copy file
106
+ await fs.promises.copyFile(sourcePath, targetPath)
107
+
108
+ // Touch file to trigger dev server reload
109
+ const now = new Date()
110
+ await fs.promises.utimes(targetPath, now, now)
111
+
112
+ if (!targetExists) {
113
+ result.created.push(relativePath)
114
+ } else {
115
+ result.updated.push({
116
+ path: relativePath,
117
+ diff: fileDiff,
118
+ })
119
+ }
120
+ } else {
121
+ result.skipped.push(relativePath)
122
+ }
123
+ } catch (error) {
124
+ result.errors.push(
125
+ `${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
126
+ )
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ private async shouldUpdateFile(
133
+ sourcePath: string,
134
+ targetPath: string,
135
+ ): Promise<boolean> {
136
+ // If target doesn't exist, definitely update
137
+ if (!fs.existsSync(targetPath)) {
138
+ return true
139
+ }
140
+
141
+ // Compare file sizes first (quick check)
142
+ const [sourceStats, targetStats] = await Promise.all([
143
+ fs.promises.stat(sourcePath),
144
+ fs.promises.stat(targetPath),
145
+ ])
146
+
147
+ if (sourceStats.size !== targetStats.size) {
148
+ return true
149
+ }
150
+
151
+ // Compare MD5 hashes for content
152
+ const [sourceHash, targetHash] = await Promise.all([
153
+ this.calculateHash(sourcePath),
154
+ this.calculateHash(targetPath),
155
+ ])
156
+
157
+ return sourceHash !== targetHash
158
+ }
159
+
160
+ private async calculateHash(filePath: string): Promise<string> {
161
+ return new Promise((resolve, reject) => {
162
+ const hash = crypto.createHash('md5')
163
+ const stream = fs.createReadStream(filePath)
164
+
165
+ stream.on('data', (data) => hash.update(data))
166
+ stream.on('end', () => resolve(hash.digest('hex')))
167
+ stream.on('error', reject)
168
+ })
169
+ }
170
+
171
+ private shouldSkipDirectory(name: string): boolean {
172
+ const skipDirs = [
173
+ 'node_modules',
174
+ '.git',
175
+ 'dist',
176
+ 'build',
177
+ '.next',
178
+ '.nuxt',
179
+ '.cache',
180
+ '.tmp-dev',
181
+ 'coverage',
182
+ '.turbo',
183
+ ]
184
+
185
+ return skipDirs.includes(name) || name.startsWith('.')
186
+ }
187
+
188
+ private shouldSkipFile(name: string): boolean {
189
+ const skipFiles = [
190
+ '.DS_Store',
191
+ 'Thumbs.db',
192
+ 'desktop.ini',
193
+ '.cta.json', // Skip .cta.json as it contains framework ID that changes each build
194
+ ]
195
+
196
+ const skipExtensions = ['.log', '.lock', '.pid', '.seed', '.sqlite']
197
+
198
+ if (skipFiles.includes(name)) {
199
+ return true
200
+ }
201
+
202
+ const ext = path.extname(name).toLowerCase()
203
+ return skipExtensions.includes(ext)
204
+ }
205
+ }
package/src/index.ts CHANGED
@@ -1,85 +1 @@
1
- // Engine exports
2
- export { compile, compileWithAttribution } from './engine/compile.js'
3
- export { processTemplateFile, relativePath } from './engine/template.js'
4
-
5
- // Config file exports
6
- export {
7
- CONFIG_FILE,
8
- writeConfigFile,
9
- readConfigFile,
10
- } from './engine/config-file.js'
11
- export type { PersistedOptions } from './engine/config-file.js'
12
-
13
- // Custom integration/template exports
14
- export { initIntegration, compileIntegration, loadRemoteIntegration } from './engine/custom-addons/integration.js'
15
- export { initTemplate, compileTemplate, loadTemplate } from './engine/custom-addons/template.js'
16
-
17
- // API exports
18
- export {
19
- fetchManifest,
20
- fetchIntegration,
21
- fetchIntegrations,
22
- fetchIntegrationInfo,
23
- fetchIntegrationFiles,
24
- } from './api/fetch.js'
25
-
26
- // Type exports
27
- export type {
28
- // Core types
29
- PackageManager,
30
- Category,
31
- IntegrationType,
32
- IntegrationPhase,
33
- RouterMode,
34
-
35
- // Option types
36
- IntegrationOption,
37
- IntegrationOptions,
38
-
39
- // Hook types (code injection points)
40
- HookType,
41
- Hook,
42
- Route,
43
- EnvVar,
44
- Command,
45
-
46
- // Integration types
47
- IntegrationInfo,
48
- IntegrationCompiled,
49
- CustomTemplateInfo,
50
- CustomTemplateCompiled,
51
-
52
- // Manifest types
53
- ManifestIntegration,
54
- Manifest,
55
-
56
- // Project types
57
- ProjectDefinition,
58
- CompileOptions,
59
- CompileOutput,
60
-
61
- // Attribution types
62
- LineAttribution,
63
- AttributedFile,
64
- AttributedCompileOutput,
65
- } from './engine/types.js'
66
-
67
- // Schema exports (for validation)
68
- export {
69
- CategorySchema,
70
- IntegrationTypeSchema,
71
- IntegrationPhaseSchema,
72
- RouterModeSchema,
73
- IntegrationOptionSchema,
74
- IntegrationOptionsSchema,
75
- HookSchema,
76
- RouteSchema,
77
- EnvVarSchema,
78
- CommandSchema,
79
- IntegrationInfoSchema,
80
- IntegrationCompiledSchema,
81
- CustomTemplateInfoSchema,
82
- CustomTemplateCompiledSchema,
83
- ManifestIntegrationSchema,
84
- ManifestSchema,
85
- } from './engine/types.js'
1
+ export { cli } from './cli.js'
package/src/mcp.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
+ import express from 'express'
5
+ import { z } from 'zod'
6
+
7
+ import {
8
+ createApp,
9
+ createDefaultEnvironment,
10
+ finalizeAddOns,
11
+ getFrameworkByName,
12
+ getFrameworks,
13
+ populateAddOnOptionsDefaults,
14
+ } from '@tanstack/create'
15
+
16
+ import { registerDocTools } from './mcp/tools.js'
17
+
18
+ function createServer({
19
+ appName,
20
+ forcedAddOns = [],
21
+ }: {
22
+ appName?: string
23
+ forcedAddOns?: Array<string>
24
+ name?: string
25
+ }) {
26
+ const server = new McpServer({
27
+ name: `${appName} Application Builder`,
28
+ version: '1.0.0',
29
+ })
30
+
31
+ const frameworks = getFrameworks()
32
+ const frameworkNames = frameworks.map((framework) => framework.name)
33
+
34
+ server.tool(
35
+ 'listTanStackAddOns',
36
+ 'List the available add-ons for creating TanStack applications',
37
+ {
38
+ framework: z
39
+ .string()
40
+ .describe(
41
+ `The framework to use. Available frameworks: ${frameworkNames.join(', ')}`,
42
+ ),
43
+ },
44
+ ({ framework: frameworkName }) => {
45
+ const framework = getFrameworkByName(frameworkName)!
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: JSON.stringify(
51
+ framework
52
+ .getAddOns()
53
+ .filter((addOn) => addOn.modes.includes('file-router'))
54
+ .map((addOn) => ({
55
+ id: addOn.id,
56
+ name: addOn.name,
57
+ description: addOn.description,
58
+ options: addOn.options,
59
+ dependsOn: addOn.dependsOn,
60
+ })),
61
+ ),
62
+ },
63
+ ],
64
+ }
65
+ },
66
+ )
67
+
68
+ server.tool(
69
+ 'createTanStackApplication',
70
+ 'Create a new TanStack application',
71
+ {
72
+ framework: z
73
+ .string()
74
+ .describe(
75
+ `The framework to use. Available frameworks: ${frameworkNames.join(', ')}`,
76
+ ),
77
+ projectName: z
78
+ .string()
79
+ .describe(
80
+ 'The package.json module name of the application (will also be the directory name)',
81
+ ),
82
+ cwd: z.string().describe('The directory to create the application in'),
83
+ addOns: z.array(z.string()).describe('Array of add-on IDs to install. Use listTanStackAddOns tool to see available add-ons and their configuration options. Example: ["prisma", "shadcn", "tanstack-query"]'),
84
+ addOnOptions: z.record(z.record(z.any())).optional().describe('Configuration options for add-ons. Format: {"addOnId": {"optionName": "value"}}. Use listTanStackAddOns to see available options for each add-on.'),
85
+ targetDir: z
86
+ .string()
87
+ .describe(
88
+ 'The directory to create the application in. Use the absolute path of the directory you want the application to be created in',
89
+ ),
90
+ },
91
+ async ({
92
+ framework: frameworkName,
93
+ projectName,
94
+ addOns,
95
+ addOnOptions,
96
+ cwd,
97
+ targetDir,
98
+ }) => {
99
+ const framework = getFrameworkByName(frameworkName)!
100
+ try {
101
+ process.chdir(cwd)
102
+ try {
103
+ const chosenAddOns = await finalizeAddOns(
104
+ framework,
105
+ 'file-router',
106
+ Array.from(
107
+ new Set([
108
+ ...(addOns as unknown as Array<string>),
109
+ ...forcedAddOns,
110
+ ]),
111
+ ),
112
+ )
113
+ await createApp(createDefaultEnvironment(), {
114
+ projectName: projectName.replace(/^\//, './'),
115
+ targetDir,
116
+ framework,
117
+ typescript: true,
118
+ tailwind: true,
119
+ packageManager: 'pnpm',
120
+ mode: 'file-router',
121
+ chosenAddOns,
122
+ addOnOptions: addOnOptions || populateAddOnOptionsDefaults(chosenAddOns),
123
+ git: true,
124
+ })
125
+ } catch (error) {
126
+ console.error(error)
127
+ return {
128
+ content: [
129
+ { type: 'text', text: `Error creating application: ${error}` },
130
+ ],
131
+ }
132
+ }
133
+ return {
134
+ content: [{ type: 'text', text: 'Application created successfully' }],
135
+ }
136
+ } catch (error) {
137
+ return {
138
+ content: [
139
+ { type: 'text', text: `Error creating application: ${error}` },
140
+ ],
141
+ }
142
+ }
143
+ },
144
+ )
145
+
146
+ // Register doc/ecosystem tools from TanStack API
147
+ registerDocTools(server)
148
+
149
+ return server
150
+ }
151
+
152
+ export async function runMCPServer(
153
+ sse: boolean,
154
+ {
155
+ forcedAddOns,
156
+ appName,
157
+ name,
158
+ }: {
159
+ forcedMode?: string
160
+ forcedAddOns?: Array<string>
161
+ appName?: string
162
+ name?: string
163
+ },
164
+ ) {
165
+ let transport: SSEServerTransport | null = null
166
+
167
+ const server = createServer({ appName, forcedAddOns, name })
168
+ if (sse) {
169
+ const app = express()
170
+
171
+ app.get('/sse', (req, res) => {
172
+ transport = new SSEServerTransport('/messages', res)
173
+ server.connect(transport)
174
+ })
175
+
176
+ app.post('/messages', (req, res) => {
177
+ if (transport) {
178
+ transport.handlePostMessage(req, res)
179
+ }
180
+ })
181
+
182
+ const port = process.env.PORT || 8080
183
+ app.listen(port, () => {
184
+ console.log(`Server is running on port http://localhost:${port}/sse`)
185
+ })
186
+ } else {
187
+ const transport = new StdioServerTransport()
188
+ await server.connect(transport)
189
+ }
190
+ }