@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
package/src/options.ts ADDED
@@ -0,0 +1,260 @@
1
+ import fs from 'node:fs'
2
+ import { cancel, confirm, intro, isCancel } from '@clack/prompts'
3
+
4
+ import {
5
+ finalizeAddOns,
6
+ getFrameworkById,
7
+ getPackageManager,
8
+ populateAddOnOptionsDefaults,
9
+ readConfigFile,
10
+ } from '@tanstack/create'
11
+
12
+ import {
13
+ getProjectName,
14
+ promptForAddOnOptions,
15
+ selectAddOns,
16
+ selectDeployment,
17
+ selectGit,
18
+ selectPackageManager,
19
+ selectRouterType,
20
+ selectTailwind,
21
+ selectToolchain,
22
+ selectTypescript,
23
+ } from './ui-prompts.js'
24
+
25
+ import {
26
+ getCurrentDirectoryName,
27
+ sanitizePackageName,
28
+ validateProjectName,
29
+ } from './utils.js'
30
+ import type { Options } from '@tanstack/create'
31
+
32
+ import type { CliOptions } from './types.js'
33
+
34
+ export async function promptForCreateOptions(
35
+ cliOptions: CliOptions,
36
+ {
37
+ forcedAddOns = [],
38
+ forcedMode,
39
+ showDeploymentOptions = false,
40
+ }: {
41
+ forcedAddOns?: Array<string>
42
+ forcedMode?: string
43
+ showDeploymentOptions?: boolean
44
+ },
45
+ ): Promise<Required<Options> | undefined> {
46
+ const options = {} as Required<Options>
47
+
48
+ options.framework = getFrameworkById(cliOptions.framework || 'react-cra')!
49
+
50
+ // Validate project name
51
+ if (cliOptions.projectName) {
52
+ // Handle "." as project name - use sanitized current directory name
53
+ if (cliOptions.projectName === '.') {
54
+ options.projectName = sanitizePackageName(getCurrentDirectoryName())
55
+ } else {
56
+ options.projectName = cliOptions.projectName
57
+ }
58
+ const { valid, error } = validateProjectName(options.projectName)
59
+ if (!valid) {
60
+ console.error(error)
61
+ process.exit(1)
62
+ }
63
+ } else {
64
+ options.projectName = await getProjectName()
65
+ }
66
+
67
+ // Check if target directory is empty
68
+ if (
69
+ !cliOptions.force &&
70
+ fs.existsSync(options.projectName) &&
71
+ fs.readdirSync(options.projectName).length > 0
72
+ ) {
73
+ const shouldContinue = await confirm({
74
+ message: `Target directory ${options.projectName} is not empty. Do you want to continue?`,
75
+ initialValue: true,
76
+ })
77
+
78
+ if (isCancel(shouldContinue) || !shouldContinue) {
79
+ cancel('Operation cancelled.')
80
+ process.exit(0)
81
+ }
82
+ }
83
+
84
+ // Router type selection
85
+ if (forcedMode) {
86
+ options.mode = forcedMode
87
+ } else if (cliOptions.template) {
88
+ options.mode =
89
+ cliOptions.template === 'file-router' ? 'file-router' : 'code-router'
90
+ } else {
91
+ options.mode = await selectRouterType()
92
+ }
93
+
94
+ // TypeScript selection (if using Code Router)
95
+ // TODO: Make this declarative
96
+ options.typescript =
97
+ options.mode === 'file-router' || options.framework.id === 'solid'
98
+ if (
99
+ forcedMode &&
100
+ options.framework.supportedModes[forcedMode].forceTypescript
101
+ ) {
102
+ options.typescript = true
103
+ }
104
+ if (!options.typescript && options.mode === 'code-router') {
105
+ options.typescript = await selectTypescript()
106
+ }
107
+
108
+ // Package manager selection
109
+ if (cliOptions.packageManager) {
110
+ options.packageManager = cliOptions.packageManager
111
+ } else {
112
+ const detectedPackageManager = await getPackageManager()
113
+ options.packageManager =
114
+ detectedPackageManager || (await selectPackageManager())
115
+ }
116
+
117
+ // Toolchain selection
118
+ const toolchain = await selectToolchain(
119
+ options.framework,
120
+ cliOptions.toolchain,
121
+ )
122
+
123
+ // Deployment selection
124
+ const deployment = showDeploymentOptions
125
+ ? await selectDeployment(options.framework, cliOptions.deployment)
126
+ : undefined
127
+
128
+ // Add-ons selection
129
+ const addOns: Set<string> = new Set()
130
+
131
+ if (toolchain) {
132
+ addOns.add(toolchain)
133
+ }
134
+ if (deployment) {
135
+ addOns.add(deployment)
136
+ }
137
+
138
+ for (const addOn of forcedAddOns) {
139
+ addOns.add(addOn)
140
+ }
141
+
142
+ if (Array.isArray(cliOptions.addOns)) {
143
+ for (const addOn of cliOptions.addOns) {
144
+ addOns.add(addOn)
145
+ }
146
+ } else {
147
+ for (const addOn of await selectAddOns(
148
+ options.framework,
149
+ options.mode,
150
+ 'add-on',
151
+ 'What add-ons would you like for your project?',
152
+ forcedAddOns,
153
+ )) {
154
+ addOns.add(addOn)
155
+ }
156
+
157
+ for (const addOn of await selectAddOns(
158
+ options.framework,
159
+ options.mode,
160
+ 'example',
161
+ 'Would you like an example?',
162
+ forcedAddOns,
163
+ false,
164
+ )) {
165
+ addOns.add(addOn)
166
+ }
167
+ }
168
+
169
+ options.chosenAddOns = Array.from(
170
+ await finalizeAddOns(options.framework, options.mode, Array.from(addOns)),
171
+ )
172
+
173
+ if (options.chosenAddOns.length) {
174
+ options.typescript = true
175
+ }
176
+
177
+ // Tailwind selection
178
+ // Only treat add-ons as requiring tailwind if they explicitly have "tailwind": true
179
+ const addOnsRequireTailwind = options.chosenAddOns.some(
180
+ (addOn) => addOn.tailwind === true,
181
+ )
182
+
183
+ if (addOnsRequireTailwind) {
184
+ // If any add-on explicitly requires tailwind, enable it automatically
185
+ options.tailwind = true
186
+ } else if (cliOptions.tailwind !== undefined) {
187
+ // User explicitly provided a CLI flag, respect it
188
+ options.tailwind = !!cliOptions.tailwind
189
+ } else if (options.framework.id === 'react-cra') {
190
+ // Only show prompt for react-cra when no CLI flag and no add-ons require it
191
+ options.tailwind = await selectTailwind()
192
+ } else {
193
+ // For other frameworks (like solid), default to true
194
+ options.tailwind = true
195
+ }
196
+
197
+ // Prompt for add-on options in interactive mode
198
+ if (Array.isArray(cliOptions.addOns)) {
199
+ // Non-interactive mode: use defaults
200
+ options.addOnOptions = populateAddOnOptionsDefaults(options.chosenAddOns)
201
+ } else {
202
+ // Interactive mode: prompt for options
203
+ const userOptions = await promptForAddOnOptions(
204
+ options.chosenAddOns.map((a) => a.id),
205
+ options.framework,
206
+ )
207
+ const defaultOptions = populateAddOnOptionsDefaults(options.chosenAddOns)
208
+ // Merge user options with defaults
209
+ options.addOnOptions = { ...defaultOptions, ...userOptions }
210
+ }
211
+
212
+ options.git = cliOptions.git || (await selectGit())
213
+ if (cliOptions.install === false) {
214
+ options.install = false
215
+ }
216
+
217
+ return options
218
+ }
219
+
220
+ export async function promptForAddOns(): Promise<Array<string>> {
221
+ const config = await readConfigFile(process.cwd())
222
+
223
+ if (!config) {
224
+ console.error('No config file found')
225
+ process.exit(1)
226
+ }
227
+
228
+ const framework = getFrameworkById(config.framework)
229
+
230
+ if (!framework) {
231
+ console.error(`Unknown framework: ${config.framework}`)
232
+ process.exit(1)
233
+ }
234
+
235
+ intro(`Adding new add-ons to '${config.projectName}'`)
236
+
237
+ const addOns: Set<string> = new Set()
238
+
239
+ for (const addOn of await selectAddOns(
240
+ framework,
241
+ config.mode!,
242
+ 'add-on',
243
+ 'What add-ons would you like for your project?',
244
+ config.chosenAddOns,
245
+ )) {
246
+ addOns.add(addOn)
247
+ }
248
+
249
+ for (const addOn of await selectAddOns(
250
+ framework,
251
+ config.mode!,
252
+ 'example',
253
+ 'Would you like any examples?',
254
+ config.chosenAddOns,
255
+ )) {
256
+ addOns.add(addOn)
257
+ }
258
+
259
+ return Array.from(addOns)
260
+ }
package/src/types.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { PackageManager } from '@tanstack/create'
2
+
3
+ export type TemplateOptions = 'typescript' | 'javascript' | 'file-router'
4
+
5
+ export interface CliOptions {
6
+ template?: TemplateOptions
7
+ framework?: string
8
+ tailwind?: boolean
9
+ packageManager?: PackageManager
10
+ toolchain?: string | false
11
+ deployment?: string
12
+ projectName?: string
13
+ git?: boolean
14
+ addOns?: Array<string> | boolean
15
+ listAddOns?: boolean
16
+ addonDetails?: string
17
+ mcp?: boolean
18
+ mcpSse?: boolean
19
+ starter?: string
20
+ targetDir?: string
21
+ interactive?: boolean
22
+ ui?: boolean
23
+ devWatch?: string
24
+ install?: boolean
25
+ addOnConfig?: string
26
+ force?: boolean
27
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ cancel,
3
+ confirm,
4
+ intro,
5
+ isCancel,
6
+ log,
7
+ outro,
8
+ spinner,
9
+ } from '@clack/prompts'
10
+ import chalk from 'chalk'
11
+
12
+ import { createDefaultEnvironment } from '@tanstack/create'
13
+
14
+ import type { Environment } from '@tanstack/create'
15
+
16
+ export function createUIEnvironment(
17
+ appName: string,
18
+ silent: boolean,
19
+ ): Environment {
20
+ const defaultEnvironment = createDefaultEnvironment()
21
+
22
+ let newEnvironment = {
23
+ ...defaultEnvironment,
24
+ appName,
25
+ }
26
+
27
+ if (!silent) {
28
+ newEnvironment = {
29
+ ...newEnvironment,
30
+ intro: (message: string) => {
31
+ intro(message)
32
+ },
33
+ outro: (message: string) => {
34
+ outro(message)
35
+ },
36
+ info: (title?: string, message?: string) => {
37
+ log.info(
38
+ `${title ? chalk.red(title) : ''}${message ? '\n' + chalk.green(message) : ''}`,
39
+ )
40
+ },
41
+ error: (title?: string, message?: string) => {
42
+ log.error(
43
+ `${title ? `${title}: ` : ''}${message ? '\n' + message : ''}`,
44
+ )
45
+ },
46
+ warn: (title?: string, message?: string) => {
47
+ log.warn(`${title ? `${title}: ` : ''}${message ? '\n' + message : ''}`)
48
+ },
49
+ confirm: async (message: string) => {
50
+ const shouldContinue = await confirm({
51
+ message,
52
+ })
53
+ if (isCancel(shouldContinue)) {
54
+ cancel('Operation cancelled.')
55
+ process.exit(0)
56
+ }
57
+ return shouldContinue
58
+ },
59
+ spinner: () => {
60
+ const s = spinner()
61
+ return {
62
+ start: (message: string) => {
63
+ s.start(message)
64
+ },
65
+ stop: (message: string) => {
66
+ s.stop(message)
67
+ },
68
+ }
69
+ },
70
+ }
71
+ }
72
+
73
+ return newEnvironment
74
+ }
@@ -0,0 +1,322 @@
1
+ import {
2
+ cancel,
3
+ confirm,
4
+ isCancel,
5
+ multiselect,
6
+ note,
7
+ select,
8
+ text,
9
+ } from '@clack/prompts'
10
+
11
+ import {
12
+ DEFAULT_PACKAGE_MANAGER,
13
+ SUPPORTED_PACKAGE_MANAGERS,
14
+ getAllAddOns,
15
+ } from '@tanstack/create'
16
+
17
+ import { validateProjectName } from './utils.js'
18
+ import type { AddOn, PackageManager } from '@tanstack/create'
19
+
20
+ import type { Framework } from '@tanstack/create/dist/types/types.js'
21
+
22
+ export async function getProjectName(): Promise<string> {
23
+ const value = await text({
24
+ message: 'What would you like to name your project?',
25
+ defaultValue: 'my-app',
26
+ validate(value) {
27
+ if (!value) {
28
+ return 'Please enter a name'
29
+ }
30
+
31
+ const { valid, error } = validateProjectName(value)
32
+ if (!valid) {
33
+ return error
34
+ }
35
+ },
36
+ })
37
+
38
+ if (isCancel(value)) {
39
+ cancel('Operation cancelled.')
40
+ process.exit(0)
41
+ }
42
+
43
+ return value
44
+ }
45
+
46
+ export async function selectRouterType(): Promise<string> {
47
+ const routerType = await select({
48
+ message: 'Select the router type:',
49
+ options: [
50
+ {
51
+ value: 'file-router',
52
+ label: 'File Router - File-based routing structure',
53
+ },
54
+ {
55
+ value: 'code-router',
56
+ label: 'Code Router - Traditional code-based routing',
57
+ },
58
+ ],
59
+ initialValue: 'file-router',
60
+ })
61
+
62
+ if (isCancel(routerType)) {
63
+ cancel('Operation cancelled.')
64
+ process.exit(0)
65
+ }
66
+
67
+ return routerType
68
+ }
69
+
70
+ export async function selectTypescript(): Promise<boolean> {
71
+ const typescriptEnable = await confirm({
72
+ message: 'Would you like to use TypeScript?',
73
+ initialValue: true,
74
+ })
75
+ if (isCancel(typescriptEnable)) {
76
+ cancel('Operation cancelled.')
77
+ process.exit(0)
78
+ }
79
+ return typescriptEnable
80
+ }
81
+
82
+ export async function selectTailwind(): Promise<boolean> {
83
+ const tailwind = await confirm({
84
+ message: 'Would you like to use Tailwind CSS?',
85
+ initialValue: true,
86
+ })
87
+
88
+ if (isCancel(tailwind)) {
89
+ cancel('Operation cancelled.')
90
+ process.exit(0)
91
+ }
92
+
93
+ return tailwind
94
+ }
95
+
96
+ export async function selectPackageManager(): Promise<PackageManager> {
97
+ const packageManager = await select({
98
+ message: 'Select package manager:',
99
+ options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
100
+ value: pm,
101
+ label: pm,
102
+ })),
103
+ initialValue: DEFAULT_PACKAGE_MANAGER,
104
+ })
105
+ if (isCancel(packageManager)) {
106
+ cancel('Operation cancelled.')
107
+ process.exit(0)
108
+ }
109
+ return packageManager
110
+ }
111
+
112
+ // Track if we've shown the multiselect help text
113
+ let hasShownMultiselectHelp = false
114
+
115
+ export async function selectAddOns(
116
+ framework: Framework,
117
+ mode: string,
118
+ type: string,
119
+ message: string,
120
+ forcedAddOns: Array<string> = [],
121
+ allowMultiple: boolean = true,
122
+ ): Promise<Array<string>> {
123
+ const allAddOns = await getAllAddOns(framework, mode)
124
+ const addOns = allAddOns.filter((addOn) => addOn.type === type)
125
+ if (addOns.length === 0) {
126
+ return []
127
+ }
128
+
129
+ // Show help text only once
130
+ if (!hasShownMultiselectHelp) {
131
+ note(
132
+ 'Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm',
133
+ 'Keyboard Shortcuts',
134
+ )
135
+ hasShownMultiselectHelp = true
136
+ }
137
+
138
+ if (allowMultiple) {
139
+ const value = await multiselect({
140
+ message,
141
+ options: addOns
142
+ .filter((addOn) => !forcedAddOns.includes(addOn.id))
143
+ .map((addOn) => ({
144
+ value: addOn.id,
145
+ label: addOn.name,
146
+ hint: addOn.description,
147
+ })),
148
+ required: false,
149
+ })
150
+
151
+ if (isCancel(value)) {
152
+ cancel('Operation cancelled.')
153
+ process.exit(0)
154
+ }
155
+
156
+ return value
157
+ } else {
158
+ const value = await select({
159
+ message,
160
+ options: [
161
+ {
162
+ value: 'none',
163
+ label: 'None',
164
+ },
165
+ ...addOns
166
+ .filter((addOn) => !forcedAddOns.includes(addOn.id))
167
+ .map((addOn) => ({
168
+ value: addOn.id,
169
+ label: addOn.name,
170
+ hint: addOn.description,
171
+ })),
172
+ ],
173
+ initialValue: 'none',
174
+ })
175
+
176
+ if (isCancel(value)) {
177
+ cancel('Operation cancelled.')
178
+ process.exit(0)
179
+ }
180
+
181
+ return value === 'none' ? [] : [value]
182
+ }
183
+ }
184
+
185
+ export async function selectGit(): Promise<boolean> {
186
+ const git = await confirm({
187
+ message: 'Would you like to initialize a new git repository?',
188
+ initialValue: true,
189
+ })
190
+ if (isCancel(git)) {
191
+ cancel('Operation cancelled.')
192
+ process.exit(0)
193
+ }
194
+ return git
195
+ }
196
+
197
+ export async function selectToolchain(
198
+ framework: Framework,
199
+ toolchain?: string | false,
200
+ ): Promise<string | undefined> {
201
+ if (toolchain === false) {
202
+ return undefined
203
+ }
204
+
205
+ const toolchains = new Set<AddOn>()
206
+ for (const addOn of framework.getAddOns()) {
207
+ if (addOn.type === 'toolchain') {
208
+ toolchains.add(addOn)
209
+ if (toolchain && addOn.id === toolchain) {
210
+ return toolchain
211
+ }
212
+ }
213
+ }
214
+
215
+ const tc = await select({
216
+ message: 'Select toolchain',
217
+ options: [
218
+ {
219
+ value: undefined,
220
+ label: 'None',
221
+ },
222
+ ...Array.from(toolchains).map((tc) => ({
223
+ value: tc.id,
224
+ label: tc.name,
225
+ })),
226
+ ],
227
+ initialValue: undefined,
228
+ })
229
+
230
+ if (isCancel(tc)) {
231
+ cancel('Operation cancelled.')
232
+ process.exit(0)
233
+ }
234
+
235
+ return tc
236
+ }
237
+
238
+ export async function promptForAddOnOptions(
239
+ addOnIds: Array<string>,
240
+ framework: Framework,
241
+ ): Promise<Record<string, Record<string, any>>> {
242
+ const addOnOptions: Record<string, Record<string, any>> = {}
243
+
244
+ for (const addOnId of addOnIds) {
245
+ const addOn = framework.getAddOns().find((a) => a.id === addOnId)
246
+ if (!addOn || !addOn.options) continue
247
+
248
+ addOnOptions[addOnId] = {}
249
+
250
+ for (const [optionName, option] of Object.entries(addOn.options)) {
251
+ if (option && typeof option === 'object' && 'type' in option) {
252
+ if (option.type === 'select') {
253
+ const selectOption = option as {
254
+ type: 'select'
255
+ label: string
256
+ description?: string
257
+ default: string
258
+ options: Array<{ value: string; label: string }>
259
+ }
260
+
261
+ const value = await select({
262
+ message: `${addOn.name}: ${selectOption.label}`,
263
+ options: selectOption.options.map((opt) => ({
264
+ value: opt.value,
265
+ label: opt.label,
266
+ })),
267
+ initialValue: selectOption.default,
268
+ })
269
+
270
+ if (isCancel(value)) {
271
+ cancel('Operation cancelled.')
272
+ process.exit(0)
273
+ }
274
+
275
+ addOnOptions[addOnId][optionName] = value
276
+ }
277
+ // Future option types can be added here
278
+ }
279
+ }
280
+ }
281
+
282
+ return addOnOptions
283
+ }
284
+
285
+ export async function selectDeployment(
286
+ framework: Framework,
287
+ deployment?: string,
288
+ ): Promise<string | undefined> {
289
+ const deployments = new Set<AddOn>()
290
+ let initialValue: string | undefined = undefined
291
+ for (const addOn of framework
292
+ .getAddOns()
293
+ .sort((a, b) => a.name.localeCompare(b.name))) {
294
+ if (addOn.type === 'deployment') {
295
+ deployments.add(addOn)
296
+ if (deployment && addOn.id === deployment) {
297
+ return deployment
298
+ }
299
+ if (addOn.default) {
300
+ initialValue = addOn.id
301
+ }
302
+ }
303
+ }
304
+
305
+ const dp = await select({
306
+ message: 'Select deployment adapter',
307
+ options: [
308
+ ...Array.from(deployments).map((d) => ({
309
+ value: d.id,
310
+ label: d.name,
311
+ })),
312
+ ],
313
+ initialValue: initialValue,
314
+ })
315
+
316
+ if (isCancel(dp)) {
317
+ cancel('Operation cancelled.')
318
+ process.exit(0)
319
+ }
320
+
321
+ return dp
322
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { basename } from 'node:path'
2
+ import validatePackageName from 'validate-npm-package-name'
3
+ import type { TemplateOptions } from './types.js'
4
+
5
+ export function convertTemplateToMode(template: TemplateOptions): string {
6
+ if (template === 'typescript' || template === 'javascript') {
7
+ return 'code-router'
8
+ }
9
+ return 'file-router'
10
+ }
11
+
12
+ export function sanitizePackageName(name: string): string {
13
+ return name
14
+ .toLowerCase()
15
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
16
+ .replace(/_/g, '-') // Replace underscores with hyphens
17
+ .replace(/[^a-z0-9-]/g, '') // Remove invalid characters
18
+ .replace(/^[^a-z]+/, '') // Ensure it starts with a letter
19
+ .replace(/-+/g, '-') // Collapse multiple hyphens
20
+ .replace(/-$/, '') // Remove trailing hyphen
21
+ }
22
+
23
+ export function getCurrentDirectoryName(): string {
24
+ return basename(process.cwd())
25
+ }
26
+
27
+ export function validateProjectName(name: string) {
28
+ const { validForNewPackages, validForOldPackages, errors, warnings } =
29
+ validatePackageName(name)
30
+ const error = errors?.[0] || warnings?.[0]
31
+
32
+ return {
33
+ valid: validForNewPackages && validForOldPackages,
34
+ error:
35
+ error?.replace(/name/g, 'Project name') ||
36
+ 'Project name does not meet npm package naming requirements',
37
+ }
38
+ }