kaddidlehopper 0.1.0

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 (183) hide show
  1. package/CONTEXT.md +139 -0
  2. package/README.md +47 -0
  3. package/add-ons/ai/README.md +34 -0
  4. package/add-ons/ai/assets/_dot_env.local.append +13 -0
  5. package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
  6. package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
  7. package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
  8. package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
  9. package/add-ons/ai/assets/src/routes/chat.css +175 -0
  10. package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
  11. package/add-ons/ai/info.json +27 -0
  12. package/add-ons/ai/package.json +17 -0
  13. package/add-ons/ai/small-logo.svg +8 -0
  14. package/dist/cli.js +251 -0
  15. package/dist/index.js +33 -0
  16. package/dist/types/cli.d.ts +8 -0
  17. package/dist/types/index.d.ts +2 -0
  18. package/dist/types/types.d.ts +14 -0
  19. package/dist/types.js +1 -0
  20. package/examples/blog/README.md +60 -0
  21. package/examples/blog/assets/content/posts/beach.md +12 -0
  22. package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
  23. package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
  24. package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
  25. package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
  26. package/examples/blog/assets/content-collections.ts +30 -0
  27. package/examples/blog/assets/public/beach.jpg +0 -0
  28. package/examples/blog/assets/public/jungle.jpg +0 -0
  29. package/examples/blog/assets/public/mountains.jpg +0 -0
  30. package/examples/blog/assets/public/snorkeling.jpg +0 -0
  31. package/examples/blog/assets/public/waterfall.jpg +0 -0
  32. package/examples/blog/assets/src/components/Header.tsx +52 -0
  33. package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
  34. package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
  35. package/examples/blog/assets/src/components/ui/card.tsx +92 -0
  36. package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
  37. package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
  38. package/examples/blog/assets/src/lib/utils.ts +6 -0
  39. package/examples/blog/assets/src/routes/__root.tsx +57 -0
  40. package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
  41. package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
  42. package/examples/blog/assets/src/routes/index.tsx +19 -0
  43. package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
  44. package/examples/blog/assets/src/styles.css +138 -0
  45. package/examples/blog/info.json +43 -0
  46. package/examples/blog/package.json +23 -0
  47. package/examples/events/README.md +110 -0
  48. package/examples/events/assets/content/speakers/andre-costa.md +22 -0
  49. package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
  50. package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
  51. package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
  52. package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
  53. package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
  54. package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
  55. package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
  56. package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
  57. package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
  58. package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
  59. package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
  60. package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
  61. package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
  62. package/examples/events/assets/content-collections.ts +56 -0
  63. package/examples/events/assets/public/background-1.jpg +0 -0
  64. package/examples/events/assets/public/background-2.jpg +0 -0
  65. package/examples/events/assets/public/background-3.jpg +0 -0
  66. package/examples/events/assets/public/background-4.jpg +0 -0
  67. package/examples/events/assets/public/conference-logo.png +0 -0
  68. package/examples/events/assets/public/favicon.ico +0 -0
  69. package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
  70. package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
  71. package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
  72. package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
  73. package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
  74. package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
  75. package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
  76. package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
  77. package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
  78. package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
  79. package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
  80. package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
  81. package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
  82. package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
  83. package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
  84. package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
  85. package/examples/events/assets/src/components/Header.tsx +59 -0
  86. package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
  87. package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
  88. package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
  89. package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
  90. package/examples/events/assets/src/components/TalkCard.tsx +77 -0
  91. package/examples/events/assets/src/components/ui/card.tsx +92 -0
  92. package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
  93. package/examples/events/assets/src/lib/conference-tools.ts +210 -0
  94. package/examples/events/assets/src/lib/model-selection.ts +1 -0
  95. package/examples/events/assets/src/lib/utils.ts +6 -0
  96. package/examples/events/assets/src/routes/__root.tsx +70 -0
  97. package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
  98. package/examples/events/assets/src/routes/index.tsx +192 -0
  99. package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
  100. package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
  101. package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
  102. package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
  103. package/examples/events/assets/src/routes/talks.index.tsx +40 -0
  104. package/examples/events/assets/src/styles.css +182 -0
  105. package/examples/events/info.json +74 -0
  106. package/examples/events/package.json +23 -0
  107. package/examples/marketing/README.md +60 -0
  108. package/examples/marketing/assets/public/logo.png +0 -0
  109. package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
  110. package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
  111. package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
  112. package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
  113. package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
  114. package/examples/marketing/assets/src/components/Header.tsx +36 -0
  115. package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
  116. package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
  117. package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
  118. package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
  119. package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
  120. package/examples/marketing/assets/src/routes/__root.tsx +57 -0
  121. package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
  122. package/examples/marketing/assets/src/routes/index.tsx +72 -0
  123. package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
  124. package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
  125. package/examples/marketing/assets/src/styles.css +212 -0
  126. package/examples/marketing/info.json +38 -0
  127. package/examples/marketing/package.json +14 -0
  128. package/examples/resume/README.md +82 -0
  129. package/examples/resume/assets/content/education/code-school.md +17 -0
  130. package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
  131. package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
  132. package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
  133. package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
  134. package/examples/resume/assets/content-collections.ts +36 -0
  135. package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
  136. package/examples/resume/assets/src/components/Header.tsx +33 -0
  137. package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
  138. package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
  139. package/examples/resume/assets/src/components/ui/card.tsx +92 -0
  140. package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
  141. package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
  142. package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
  143. package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
  144. package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
  145. package/examples/resume/assets/src/lib/utils.ts +6 -0
  146. package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
  147. package/examples/resume/assets/src/routes/index.tsx +220 -0
  148. package/examples/resume/assets/src/styles.css +138 -0
  149. package/examples/resume/info.json +25 -0
  150. package/examples/resume/package.json +26 -0
  151. package/package.json +39 -0
  152. package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
  153. package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
  154. package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
  155. package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
  156. package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
  157. package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
  158. package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
  159. package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
  160. package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
  161. package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
  162. package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
  163. package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
  164. package/project/base/_dot_gitignore +8 -0
  165. package/project/base/netlify.toml +7 -0
  166. package/project/base/package.json +33 -0
  167. package/project/base/public/favicon.ico +0 -0
  168. package/project/base/public/tanstack-circle-logo.png +0 -0
  169. package/project/base/public/tanstack-word-logo-white.svg +1 -0
  170. package/project/base/src/components/Header.tsx +17 -0
  171. package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
  172. package/project/base/src/router.tsx +15 -0
  173. package/project/base/src/routes/__root.tsx +57 -0
  174. package/project/base/src/routes/index.tsx +48 -0
  175. package/project/base/src/styles.css +15 -0
  176. package/project/base/tsconfig.json +28 -0
  177. package/project/base/vite.config.ts.ejs +25 -0
  178. package/project/packages.json +22 -0
  179. package/scripts/check-outdated-packages.js +421 -0
  180. package/src/cli.ts +343 -0
  181. package/src/index.ts +49 -0
  182. package/src/types.ts +15 -0
  183. package/tsconfig.json +17 -0
package/src/cli.ts ADDED
@@ -0,0 +1,343 @@
1
+ import { resolve, basename, join } from 'node:path'
2
+ import { unlink } from 'node:fs/promises'
3
+ import { Command, InvalidArgumentError } from 'commander'
4
+ import chalk from 'chalk'
5
+ import validatePackageName from 'validate-npm-package-name'
6
+
7
+ import {
8
+ SUPPORTED_PACKAGE_MANAGERS,
9
+ createApp,
10
+ createDefaultEnvironment,
11
+ finalizeAddOns,
12
+ getAllAddOns,
13
+ getFrameworkByName,
14
+ getPackageManager,
15
+ DEFAULT_PACKAGE_MANAGER,
16
+ populateAddOnOptionsDefaults,
17
+ } from '@tanstack/cta-engine'
18
+
19
+ import type { CliOptions } from './types.js'
20
+ import type {
21
+ Options,
22
+ PackageManager,
23
+ } from '@tanstack/cta-engine'
24
+
25
+ // Utility functions
26
+ function sanitizePackageName(name: string): string {
27
+ return name
28
+ .toLowerCase()
29
+ .replace(/\s+/g, '-')
30
+ .replace(/_/g, '-')
31
+ .replace(/[^a-z0-9-]/g, '')
32
+ .replace(/^[^a-z]+/, '')
33
+ .replace(/-+/g, '-')
34
+ .replace(/-$/, '')
35
+ }
36
+
37
+ function getCurrentDirectoryName(): string {
38
+ return basename(process.cwd())
39
+ }
40
+
41
+ function validateProjectName(name: string) {
42
+ const { validForNewPackages, validForOldPackages, errors, warnings } =
43
+ validatePackageName(name)
44
+ const error = errors?.[0] || warnings?.[0]
45
+
46
+ return {
47
+ valid: validForNewPackages && validForOldPackages,
48
+ error:
49
+ error?.replace(/name/g, 'Project name') ||
50
+ 'Project name does not meet npm package naming requirements',
51
+ }
52
+ }
53
+
54
+ // Create environment with UI functions for console output
55
+ function createEnvironment(appName: string) {
56
+ const env = createDefaultEnvironment()
57
+ env.appName = appName
58
+ env.intro = (message: string) => console.log(chalk.bold.cyan(message))
59
+ env.outro = (message: string) => console.log(chalk.bold.green(message))
60
+ env.info = (title?: string, message?: string) => {
61
+ if (title && message) {
62
+ console.log(chalk.blue(`${title}: ${message}`))
63
+ } else if (title) {
64
+ console.log(chalk.blue(title))
65
+ }
66
+ }
67
+ env.error = (title?: string, message?: string) => {
68
+ if (title && message) {
69
+ console.error(chalk.red(`${title}: ${message}`))
70
+ } else if (title) {
71
+ console.error(chalk.red(title))
72
+ }
73
+ }
74
+ env.warn = (title?: string, message?: string) => {
75
+ if (title && message) {
76
+ console.warn(chalk.yellow(`${title}: ${message}`))
77
+ } else if (title) {
78
+ console.warn(chalk.yellow(title))
79
+ }
80
+ }
81
+ env.spinner = () => ({
82
+ start: (message: string) => console.log(chalk.gray(`⟳ ${message}`)),
83
+ stop: (message: string) => console.log(chalk.green(`✓ ${message}`)),
84
+ })
85
+ return env
86
+ }
87
+
88
+ export interface CliConfig {
89
+ name: string
90
+ appName: string
91
+ defaultFramework: string
92
+ forcedMode?: string
93
+ forcedAddOns?: Array<string>
94
+ }
95
+
96
+ export function cli({
97
+ name,
98
+ appName,
99
+ defaultFramework,
100
+ forcedMode,
101
+ forcedAddOns = [],
102
+ }: CliConfig) {
103
+ const environment = createEnvironment(appName)
104
+ const program = new Command()
105
+
106
+ const defaultMode = forcedMode || 'file-router'
107
+
108
+ program.name(name).description(`CLI to create a new ${appName} application`)
109
+
110
+ program.argument('[project-name]', 'name of the project')
111
+
112
+ program
113
+ .option('--no-install', 'skip installing dependencies')
114
+ .option<PackageManager>(
115
+ `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
116
+ `Explicitly tell the CLI to use this package manager`,
117
+ (value) => {
118
+ if (!SUPPORTED_PACKAGE_MANAGERS.includes(value as PackageManager)) {
119
+ throw new InvalidArgumentError(
120
+ `Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`,
121
+ )
122
+ }
123
+ return value as PackageManager
124
+ },
125
+ )
126
+ .option<Array<string> | boolean>(
127
+ '--add-ons [...add-ons]',
128
+ 'pick from a list of available add-ons (comma separated list)',
129
+ (value: string) => {
130
+ let addOns: Array<string> | boolean = !!value
131
+ if (typeof value === 'string') {
132
+ addOns = value.split(',').map((addon) => addon.trim())
133
+ }
134
+ return addOns
135
+ },
136
+ )
137
+ .option('--list-add-ons', 'list all available add-ons', false)
138
+ .option('--list-addons-json', 'list all available add-ons as JSON', false)
139
+ .option(
140
+ '--addon-details <addon-id>',
141
+ 'show detailed information about a specific add-on',
142
+ )
143
+ .option('--no-git', 'do not create a git repository')
144
+ .option(
145
+ '--target-dir <path>',
146
+ 'the target directory for the application root',
147
+ )
148
+ .option(
149
+ '-f, --force',
150
+ 'force project creation even if the target directory is not empty',
151
+ false,
152
+ )
153
+ .option(
154
+ '--bare-bones',
155
+ 'create minimal scaffolding for LLM modification',
156
+ false,
157
+ )
158
+
159
+ program.action(async (projectName: string, options: CliOptions) => {
160
+ const framework = getFrameworkByName(defaultFramework)
161
+ if (!framework) {
162
+ console.error(`Framework '${defaultFramework}' not found`)
163
+ process.exit(1)
164
+ }
165
+
166
+ // Handle --list-addons-json
167
+ if (options.listAddonsJson) {
168
+ const addOns = await getAllAddOns(framework, defaultMode)
169
+ const serialized = addOns
170
+ .filter((a) => !forcedAddOns.includes(a.id))
171
+ .map((addon) => ({
172
+ id: addon.id,
173
+ name: addon.name,
174
+ description: addon.description,
175
+ type: addon.type,
176
+ options: addon.options,
177
+ }))
178
+ console.log(JSON.stringify(serialized, null, 2))
179
+ return
180
+ }
181
+
182
+ // Handle --list-add-ons (text format)
183
+ if (options.listAddOns) {
184
+ const addOns = await getAllAddOns(framework, defaultMode)
185
+ let hasConfigurableAddOns = false
186
+ for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
187
+ const hasOptions =
188
+ addOn.options && Object.keys(addOn.options).length > 0
189
+ const optionMarker = hasOptions ? '*' : ' '
190
+ if (hasOptions) hasConfigurableAddOns = true
191
+ console.log(
192
+ `${optionMarker} ${chalk.bold(addOn.id)}: ${addOn.description}`,
193
+ )
194
+ }
195
+ if (hasConfigurableAddOns) {
196
+ console.log('\n* = has configuration options')
197
+ }
198
+ return
199
+ }
200
+
201
+ // Handle --addon-details
202
+ if (options.addonDetails) {
203
+ const addOns = await getAllAddOns(framework, defaultMode)
204
+ const addOn = addOns.find((a) => a.id === options.addonDetails)
205
+ if (!addOn) {
206
+ console.error(`Add-on '${options.addonDetails}' not found`)
207
+ process.exit(1)
208
+ }
209
+
210
+ console.log(
211
+ `${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`,
212
+ )
213
+ console.log(`${chalk.bold('ID:')} ${addOn.id}`)
214
+ console.log(`${chalk.bold('Description:')} ${addOn.description}`)
215
+ console.log(`${chalk.bold('Type:')} ${addOn.type}`)
216
+ console.log(`${chalk.bold('Phase:')} ${addOn.phase}`)
217
+ console.log(`${chalk.bold('Supported Modes:')} ${addOn.modes.join(', ')}`)
218
+
219
+ if (addOn.dependsOn && addOn.dependsOn.length > 0) {
220
+ console.log(
221
+ `${chalk.bold('Dependencies:')} ${addOn.dependsOn.join(', ')}`,
222
+ )
223
+ }
224
+
225
+ if (addOn.options && Object.keys(addOn.options).length > 0) {
226
+ console.log(`\n${chalk.bold.yellow('Configuration Options:')}`)
227
+ for (const [optionName, option] of Object.entries(addOn.options)) {
228
+ if (option && typeof option === 'object' && 'type' in option) {
229
+ const opt = option as { label: string; description?: string; type: string; default: unknown; options?: Array<{ value: string; label: string }> }
230
+ console.log(` ${chalk.bold(optionName)}:`)
231
+ console.log(` Label: ${opt.label}`)
232
+ if (opt.description) {
233
+ console.log(` Description: ${opt.description}`)
234
+ }
235
+ console.log(` Type: ${opt.type}`)
236
+ console.log(` Default: ${opt.default}`)
237
+ if (opt.type === 'select' && opt.options) {
238
+ console.log(` Available values:`)
239
+ for (const choice of opt.options) {
240
+ console.log(` - ${choice.value}: ${choice.label}`)
241
+ }
242
+ }
243
+ }
244
+ }
245
+ } else {
246
+ console.log(`\n${chalk.gray('No configuration options available')}`)
247
+ }
248
+
249
+ if (addOn.routes && addOn.routes.length > 0) {
250
+ console.log(`\n${chalk.bold.green('Routes:')}`)
251
+ for (const route of addOn.routes) {
252
+ console.log(` ${chalk.bold(route.url)} (${route.name})`)
253
+ console.log(` File: ${route.path}`)
254
+ }
255
+ }
256
+ return
257
+ }
258
+
259
+ // Handle project creation
260
+ if (!projectName) {
261
+ console.error('Project name is required')
262
+ program.help()
263
+ return
264
+ }
265
+
266
+ let resolvedProjectName = projectName
267
+ let targetDir: string
268
+
269
+ // Handle "." as project name - use current directory
270
+ if (projectName === '.') {
271
+ resolvedProjectName = sanitizePackageName(getCurrentDirectoryName())
272
+ targetDir = resolve(process.cwd())
273
+ } else {
274
+ targetDir = options.targetDir
275
+ ? resolve(options.targetDir)
276
+ : resolve(process.cwd(), projectName)
277
+ }
278
+
279
+ const { valid, error } = validateProjectName(resolvedProjectName)
280
+ if (!valid) {
281
+ console.error(error)
282
+ process.exit(1)
283
+ }
284
+
285
+ // Determine selected add-ons
286
+ const selectedAddOns = new Set<string>([...forcedAddOns])
287
+ if (options.addOns && Array.isArray(options.addOns)) {
288
+ for (const a of options.addOns) {
289
+ selectedAddOns.add(a)
290
+ }
291
+ }
292
+
293
+ const chosenAddOns = await finalizeAddOns(
294
+ framework,
295
+ defaultMode,
296
+ Array.from(selectedAddOns),
297
+ )
298
+
299
+ const finalOptions: Options = {
300
+ projectName: resolvedProjectName,
301
+ targetDir,
302
+ framework,
303
+ mode: defaultMode,
304
+ typescript: true,
305
+ tailwind: true,
306
+ packageManager:
307
+ options.packageManager ||
308
+ getPackageManager() ||
309
+ DEFAULT_PACKAGE_MANAGER,
310
+ git: options.git !== false,
311
+ install: options.install !== false,
312
+ chosenAddOns,
313
+ addOnOptions: {
314
+ ...populateAddOnOptionsDefaults(chosenAddOns),
315
+ project: { bareBones: options.bareBones ?? false },
316
+ },
317
+ }
318
+
319
+ environment.intro(`Creating a new ${appName} app in ${resolvedProjectName}...`)
320
+ await createApp(environment, finalOptions)
321
+
322
+ // Delete files specified in bareBones.deleteFiles for each add-on when in bare-bones mode
323
+ if (options.bareBones) {
324
+ for (const addOn of chosenAddOns) {
325
+ const addOnWithBareBones = addOn as typeof addOn & {
326
+ bareBones?: { deleteFiles?: Array<string> }
327
+ }
328
+ if (addOnWithBareBones.bareBones?.deleteFiles) {
329
+ for (const file of addOnWithBareBones.bareBones.deleteFiles) {
330
+ const filePath = join(targetDir, file)
331
+ try {
332
+ await unlink(filePath)
333
+ } catch {
334
+ // File may not exist, ignore errors
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ })
341
+
342
+ program.parse()
343
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import {
6
+ registerFramework,
7
+ scanAddOnDirectories,
8
+ scanProjectDirectory,
9
+ } from '@tanstack/cta-engine'
10
+ import { cli } from './cli.js'
11
+
12
+ const projectDirectory = join(
13
+ dirname(dirname(fileURLToPath(import.meta.url))),
14
+ 'project',
15
+ )
16
+
17
+ const addOns = scanAddOnDirectories([
18
+ join(dirname(dirname(fileURLToPath(import.meta.url))), 'add-ons'),
19
+ join(dirname(dirname(fileURLToPath(import.meta.url))), 'examples'),
20
+ ])
21
+
22
+ const { files, basePackageJSON, optionalPackages } = scanProjectDirectory(
23
+ projectDirectory,
24
+ join(dirname(dirname(fileURLToPath(import.meta.url))), 'project', 'base'),
25
+ )
26
+
27
+ registerFramework({
28
+ id: 'netlify-start',
29
+ name: 'Netlify TanStack Start',
30
+ description: 'TanStack Start applications for Netlify deployment',
31
+ version: '0.1.0',
32
+ base: files,
33
+ addOns,
34
+ basePackageJSON,
35
+ optionalPackages,
36
+ supportedModes: {
37
+ 'file-router': {
38
+ displayName: 'File Router',
39
+ description: 'File-based routing with TanStack Start',
40
+ forceTypescript: true,
41
+ },
42
+ },
43
+ })
44
+
45
+ cli({
46
+ name: 'netlify-cta',
47
+ appName: 'Netlify TanStack Start',
48
+ defaultFramework: 'Netlify TanStack Start',
49
+ })
package/src/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { PackageManager } from '@tanstack/cta-engine'
2
+
3
+ export interface CliOptions {
4
+ projectName?: string
5
+ packageManager?: PackageManager
6
+ git?: boolean
7
+ addOns?: Array<string> | boolean
8
+ listAddOns?: boolean
9
+ listAddonsJson?: boolean
10
+ addonDetails?: string
11
+ targetDir?: string
12
+ install?: boolean
13
+ force?: boolean
14
+ bareBones?: boolean
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "moduleResolution": "bundler",
12
+ "declaration": true,
13
+ "declarationDir": "./dist/types"
14
+ },
15
+ "include": ["./src/**/*.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }