@tanstack/cli 0.0.8 → 0.48.3

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.js +7 -0
  3. package/dist/cli.js +481 -0
  4. package/dist/command-line.js +174 -0
  5. package/dist/dev-watch.js +290 -0
  6. package/dist/file-syncer.js +148 -0
  7. package/dist/index.js +1 -0
  8. package/dist/mcp/api.js +31 -0
  9. package/dist/mcp/tools.js +250 -0
  10. package/dist/mcp/types.js +37 -0
  11. package/dist/mcp.js +121 -0
  12. package/dist/options.js +162 -0
  13. package/dist/types/bin.d.ts +2 -0
  14. package/dist/types/cli.d.ts +16 -0
  15. package/dist/types/command-line.d.ts +10 -0
  16. package/dist/types/dev-watch.d.ts +27 -0
  17. package/dist/types/file-syncer.d.ts +18 -0
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/mcp/api.d.ts +4 -0
  20. package/dist/types/mcp/tools.d.ts +2 -0
  21. package/dist/types/mcp/types.d.ts +217 -0
  22. package/dist/types/mcp.d.ts +6 -0
  23. package/dist/types/options.d.ts +8 -0
  24. package/dist/types/types.d.ts +25 -0
  25. package/dist/types/ui-environment.d.ts +2 -0
  26. package/dist/types/ui-prompts.d.ts +12 -0
  27. package/dist/types/utils.d.ts +8 -0
  28. package/dist/types.js +1 -0
  29. package/dist/ui-environment.js +52 -0
  30. package/dist/ui-prompts.js +244 -0
  31. package/dist/utils.js +30 -0
  32. package/package.json +46 -47
  33. package/src/bin.ts +6 -93
  34. package/src/cli.ts +692 -0
  35. package/src/command-line.ts +236 -0
  36. package/src/dev-watch.ts +430 -0
  37. package/src/file-syncer.ts +205 -0
  38. package/src/index.ts +1 -85
  39. package/src/mcp.ts +190 -0
  40. package/src/options.ts +260 -0
  41. package/src/types.ts +27 -0
  42. package/src/ui-environment.ts +74 -0
  43. package/src/ui-prompts.ts +322 -0
  44. package/src/utils.ts +38 -0
  45. package/tests/command-line.test.ts +304 -0
  46. package/tests/index.test.ts +9 -0
  47. package/tests/mcp.test.ts +225 -0
  48. package/tests/options.test.ts +304 -0
  49. package/tests/setupVitest.ts +6 -0
  50. package/tests/ui-environment.test.ts +97 -0
  51. package/tests/ui-prompts.test.ts +238 -0
  52. package/tsconfig.json +17 -0
  53. package/vitest.config.js +7 -0
  54. package/dist/bin.cjs +0 -769
  55. package/dist/bin.d.cts +0 -1
  56. package/dist/bin.d.mts +0 -1
  57. package/dist/bin.mjs +0 -768
  58. package/dist/fetch-CbFFGJEw.cjs +0 -3
  59. package/dist/fetch-DG5dLrsb.cjs +0 -522
  60. package/dist/fetch-DhlVXS6S.mjs +0 -390
  61. package/dist/fetch-I_OVg8JX.mjs +0 -3
  62. package/dist/index.cjs +0 -37
  63. package/dist/index.d.cts +0 -1172
  64. package/dist/index.d.mts +0 -1172
  65. package/dist/index.mjs +0 -4
  66. package/dist/template-Szi7-AZJ.mjs +0 -2202
  67. package/dist/template-lWrIZhCQ.cjs +0 -2314
  68. package/src/api/fetch.test.ts +0 -114
  69. package/src/api/fetch.ts +0 -278
  70. package/src/cache/index.ts +0 -89
  71. package/src/commands/create.ts +0 -470
  72. package/src/commands/mcp.test.ts +0 -152
  73. package/src/commands/mcp.ts +0 -211
  74. package/src/engine/compile-with-addons.test.ts +0 -302
  75. package/src/engine/compile.test.ts +0 -404
  76. package/src/engine/compile.ts +0 -569
  77. package/src/engine/config-file.test.ts +0 -118
  78. package/src/engine/config-file.ts +0 -61
  79. package/src/engine/custom-addons/integration.ts +0 -323
  80. package/src/engine/custom-addons/shared.test.ts +0 -98
  81. package/src/engine/custom-addons/shared.ts +0 -281
  82. package/src/engine/custom-addons/template.test.ts +0 -288
  83. package/src/engine/custom-addons/template.ts +0 -124
  84. package/src/engine/template.test.ts +0 -256
  85. package/src/engine/template.ts +0 -269
  86. package/src/engine/types.ts +0 -336
  87. package/src/parse-gitignore.d.ts +0 -5
  88. package/src/templates/base.ts +0 -883
@@ -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
+ }