@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,236 @@
1
+ import { resolve } from 'node:path'
2
+ import fs from 'node:fs'
3
+
4
+ import {
5
+ DEFAULT_PACKAGE_MANAGER,
6
+ finalizeAddOns,
7
+ getFrameworkById,
8
+ getPackageManager,
9
+ loadStarter,
10
+ populateAddOnOptionsDefaults,
11
+ } from '@tanstack/create'
12
+
13
+ import {
14
+ getCurrentDirectoryName,
15
+ sanitizePackageName,
16
+ validateProjectName,
17
+ } from './utils.js'
18
+ import type { Options } from '@tanstack/create'
19
+
20
+ import type { CliOptions } from './types.js'
21
+
22
+ export async function normalizeOptions(
23
+ cliOptions: CliOptions,
24
+ forcedMode?: string,
25
+ forcedAddOns?: Array<string>,
26
+ opts?: {
27
+ disableNameCheck?: boolean
28
+ forcedDeployment?: string
29
+ },
30
+ ): Promise<Options | undefined> {
31
+ let projectName = (cliOptions.projectName ?? '').trim()
32
+ let targetDir: string
33
+
34
+ // Handle "." as project name - use current directory
35
+ if (projectName === '.') {
36
+ projectName = sanitizePackageName(getCurrentDirectoryName())
37
+ targetDir = resolve(process.cwd())
38
+ } else {
39
+ targetDir = resolve(process.cwd(), projectName)
40
+ }
41
+
42
+ if (!projectName && !opts?.disableNameCheck) {
43
+ return undefined
44
+ }
45
+
46
+ if (projectName) {
47
+ const { valid, error } = validateProjectName(projectName)
48
+ if (!valid) {
49
+ console.error(error)
50
+ process.exit(1)
51
+ }
52
+ }
53
+
54
+ let tailwind = !!cliOptions.tailwind
55
+
56
+ let mode: string =
57
+ forcedMode ||
58
+ (cliOptions.template === 'file-router' ? 'file-router' : 'code-router')
59
+
60
+ const starter = cliOptions.starter
61
+ ? await loadStarter(cliOptions.starter)
62
+ : undefined
63
+
64
+ // TODO: Make this declarative
65
+ let typescript =
66
+ cliOptions.template === 'typescript' ||
67
+ cliOptions.template === 'file-router' ||
68
+ cliOptions.framework === 'solid'
69
+
70
+ if (starter) {
71
+ tailwind = starter.tailwind
72
+ typescript = starter.typescript
73
+ cliOptions.framework = starter.framework
74
+ mode = starter.mode
75
+ }
76
+
77
+ const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
78
+
79
+ if (
80
+ forcedMode &&
81
+ framework.supportedModes?.[forcedMode]?.forceTypescript !== undefined
82
+ ) {
83
+ typescript = true
84
+ }
85
+
86
+ if (cliOptions.framework === 'solid') {
87
+ tailwind = true
88
+ }
89
+
90
+ async function selectAddOns() {
91
+ // Edge case for Windows Powershell
92
+ if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
93
+ const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
94
+ if (parseSeparatedArgs.length > 1) {
95
+ cliOptions.addOns = parseSeparatedArgs
96
+ }
97
+ }
98
+
99
+ if (
100
+ Array.isArray(cliOptions.addOns) ||
101
+ starter?.dependsOn ||
102
+ forcedAddOns ||
103
+ cliOptions.toolchain ||
104
+ cliOptions.deployment
105
+ ) {
106
+ const selectedAddOns = new Set<string>([
107
+ ...(starter?.dependsOn || []),
108
+ ...(forcedAddOns || []),
109
+ ])
110
+ if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
111
+ for (const a of cliOptions.addOns) {
112
+ selectedAddOns.add(a)
113
+ }
114
+ }
115
+ if (cliOptions.toolchain) {
116
+ selectedAddOns.add(cliOptions.toolchain)
117
+ }
118
+ if (cliOptions.deployment) {
119
+ selectedAddOns.add(cliOptions.deployment)
120
+ }
121
+
122
+ if (!cliOptions.deployment && opts?.forcedDeployment) {
123
+ selectedAddOns.add(opts.forcedDeployment)
124
+ }
125
+
126
+ return await finalizeAddOns(framework, mode, Array.from(selectedAddOns))
127
+ }
128
+
129
+ return []
130
+ }
131
+
132
+ const chosenAddOns = await selectAddOns()
133
+
134
+ if (chosenAddOns.length) {
135
+ typescript = true
136
+
137
+ // Check if any add-on explicitly requires tailwind
138
+ const addOnsRequireTailwind = chosenAddOns.some(
139
+ (addOn) => addOn.tailwind === true,
140
+ )
141
+
142
+ // Only set tailwind to true if:
143
+ // 1. An add-on explicitly requires it, OR
144
+ // 2. User explicitly set it via CLI
145
+ if (addOnsRequireTailwind) {
146
+ tailwind = true
147
+ } else if (cliOptions.tailwind === true) {
148
+ tailwind = true
149
+ } else if (cliOptions.tailwind === false) {
150
+ tailwind = false
151
+ }
152
+ // If cliOptions.tailwind is undefined and no add-ons require it,
153
+ // leave tailwind as is (will be prompted in interactive mode)
154
+ }
155
+
156
+ // Handle add-on configuration option
157
+ let addOnOptionsFromCLI = {}
158
+ if (cliOptions.addOnConfig) {
159
+ try {
160
+ addOnOptionsFromCLI = JSON.parse(cliOptions.addOnConfig)
161
+ } catch (error) {
162
+ console.error('Error parsing add-on config:', error)
163
+ process.exit(1)
164
+ }
165
+ }
166
+
167
+ return {
168
+ projectName: projectName,
169
+ targetDir,
170
+ framework,
171
+ mode,
172
+ typescript,
173
+ tailwind,
174
+ packageManager:
175
+ cliOptions.packageManager ||
176
+ getPackageManager() ||
177
+ DEFAULT_PACKAGE_MANAGER,
178
+ git: !!cliOptions.git,
179
+ install: cliOptions.install,
180
+ chosenAddOns,
181
+ addOnOptions: {
182
+ ...populateAddOnOptionsDefaults(chosenAddOns),
183
+ ...addOnOptionsFromCLI,
184
+ },
185
+ starter: starter,
186
+ }
187
+ }
188
+
189
+ export function validateDevWatchOptions(cliOptions: CliOptions): {
190
+ valid: boolean
191
+ error?: string
192
+ } {
193
+ if (!cliOptions.devWatch) {
194
+ return { valid: true }
195
+ }
196
+
197
+ // Validate watch path exists
198
+ const watchPath = resolve(process.cwd(), cliOptions.devWatch)
199
+ if (!fs.existsSync(watchPath)) {
200
+ return {
201
+ valid: false,
202
+ error: `Watch path does not exist: ${watchPath}`,
203
+ }
204
+ }
205
+
206
+ // Validate it's a directory
207
+ const stats = fs.statSync(watchPath)
208
+ if (!stats.isDirectory()) {
209
+ return {
210
+ valid: false,
211
+ error: `Watch path is not a directory: ${watchPath}`,
212
+ }
213
+ }
214
+
215
+ // Ensure target directory is specified
216
+ if (!cliOptions.projectName && !cliOptions.targetDir) {
217
+ return {
218
+ valid: false,
219
+ error: 'Project name or target directory is required for dev watch mode',
220
+ }
221
+ }
222
+
223
+ // Check for framework structure
224
+ const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
225
+ const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
226
+ const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
227
+
228
+ if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
229
+ return {
230
+ valid: false,
231
+ error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
232
+ }
233
+ }
234
+
235
+ return { valid: true }
236
+ }
@@ -0,0 +1,430 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ import chokidar from 'chokidar'
5
+ import chalk from 'chalk'
6
+ import { temporaryDirectory } from 'tempy'
7
+ import {
8
+ createApp,
9
+ getFrameworkById,
10
+ registerFramework,
11
+ } from '@tanstack/create'
12
+ import { FileSyncer } from './file-syncer.js'
13
+ import { createUIEnvironment } from './ui-environment.js'
14
+ import type {
15
+ Environment,
16
+ Framework,
17
+ FrameworkDefinition,
18
+ Options,
19
+ } from '@tanstack/create'
20
+ import type { FSWatcher } from 'chokidar'
21
+
22
+ export interface DevWatchOptions {
23
+ watchPath: string
24
+ targetDir: string
25
+ framework: Framework
26
+ cliOptions: Options
27
+ packageManager: string
28
+ environment: Environment
29
+ frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
30
+ }
31
+
32
+ interface ChangeEvent {
33
+ type: 'add' | 'change' | 'unlink'
34
+ path: string
35
+ relativePath: string
36
+ timestamp: number
37
+ }
38
+
39
+ class DebounceQueue {
40
+ private timer: NodeJS.Timeout | null = null
41
+ private changes: Set<string> = new Set()
42
+ private callback: (changes: Set<string>) => void
43
+
44
+ constructor(
45
+ callback: (changes: Set<string>) => void,
46
+ private delay: number = 1000,
47
+ ) {
48
+ this.callback = callback
49
+ }
50
+
51
+ add(path: string): void {
52
+ this.changes.add(path)
53
+
54
+ if (this.timer) {
55
+ clearTimeout(this.timer)
56
+ }
57
+
58
+ this.timer = setTimeout(() => {
59
+ const currentChanges = new Set(this.changes)
60
+ this.callback(currentChanges)
61
+ this.changes.clear()
62
+ }, this.delay)
63
+ }
64
+
65
+ size(): number {
66
+ return this.changes.size
67
+ }
68
+
69
+ clear(): void {
70
+ if (this.timer) {
71
+ clearTimeout(this.timer)
72
+ this.timer = null
73
+ }
74
+ this.changes.clear()
75
+ }
76
+ }
77
+
78
+ export class DevWatchManager {
79
+ private watcher: FSWatcher | null = null
80
+ private debounceQueue: DebounceQueue
81
+ private syncer: FileSyncer
82
+ private tempDir: string | null = null
83
+ private isBuilding = false
84
+ private buildCount = 0
85
+
86
+ constructor(private options: DevWatchOptions) {
87
+ this.syncer = new FileSyncer()
88
+ this.debounceQueue = new DebounceQueue((changes) => this.rebuild(changes))
89
+ }
90
+
91
+ async start(): Promise<void> {
92
+ // Validate watch path
93
+ if (!fs.existsSync(this.options.watchPath)) {
94
+ throw new Error(`Watch path does not exist: ${this.options.watchPath}`)
95
+ }
96
+
97
+ // Validate target directory exists (should have been created by createApp)
98
+ if (!fs.existsSync(this.options.targetDir)) {
99
+ throw new Error(
100
+ `Target directory does not exist: ${this.options.targetDir}`,
101
+ )
102
+ }
103
+
104
+ if (this.options.cliOptions.install === false) {
105
+ throw new Error('Cannot use the --no-install flag when using --dev-watch')
106
+ }
107
+
108
+ // Log startup with tree style
109
+ console.log()
110
+ console.log(chalk.bold('dev-watch'))
111
+ this.log.tree('', `watching: ${chalk.cyan(this.options.watchPath)}`)
112
+ this.log.tree('', `target: ${chalk.cyan(this.options.targetDir)}`)
113
+ this.log.tree('', 'ready', true)
114
+
115
+ // Setup signal handlers
116
+ process.on('SIGINT', () => this.cleanup())
117
+ process.on('SIGTERM', () => this.cleanup())
118
+
119
+ // Start watching
120
+ this.startWatcher()
121
+ }
122
+
123
+ async stop(): Promise<void> {
124
+ console.log()
125
+ this.log.info('Stopping dev watch mode...')
126
+
127
+ if (this.watcher) {
128
+ await this.watcher.close()
129
+ this.watcher = null
130
+ }
131
+
132
+ this.debounceQueue.clear()
133
+ this.cleanup()
134
+ }
135
+
136
+ private startWatcher(): void {
137
+ const watcherConfig = {
138
+ ignored: [
139
+ '**/node_modules/**',
140
+ '**/.git/**',
141
+ '**/dist/**',
142
+ '**/build/**',
143
+ '**/.DS_Store',
144
+ '**/*.log',
145
+ this.tempDir!,
146
+ ],
147
+ persistent: true,
148
+ ignoreInitial: true,
149
+ awaitWriteFinish: {
150
+ stabilityThreshold: 100,
151
+ pollInterval: 100,
152
+ },
153
+ }
154
+
155
+ this.watcher = chokidar.watch(this.options.watchPath, watcherConfig)
156
+
157
+ this.watcher.on('add', (filePath) => this.handleChange('add', filePath))
158
+ this.watcher.on('change', (filePath) =>
159
+ this.handleChange('change', filePath),
160
+ )
161
+ this.watcher.on('unlink', (filePath) =>
162
+ this.handleChange('unlink', filePath),
163
+ )
164
+ this.watcher.on('error', (error) =>
165
+ this.log.error(`Watcher error: ${error.message}`),
166
+ )
167
+
168
+ this.watcher.on('ready', () => {
169
+ // Already shown in startup, no need to repeat
170
+ })
171
+ }
172
+
173
+ private handleChange(_type: ChangeEvent['type'], filePath: string): void {
174
+ const relativePath = path.relative(this.options.watchPath, filePath)
175
+ // Log change only once for the first file in debounce queue
176
+ if (this.debounceQueue.size() === 0) {
177
+ this.log.section('change detected')
178
+ this.log.subsection(`└─ ${relativePath}`)
179
+ } else {
180
+ this.log.subsection(`└─ ${relativePath}`)
181
+ }
182
+ this.debounceQueue.add(filePath)
183
+ }
184
+
185
+ private async rebuild(changes: Set<string>): Promise<void> {
186
+ if (this.isBuilding) {
187
+ this.log.warning('Build already in progress, skipping...')
188
+ return
189
+ }
190
+
191
+ this.isBuilding = true
192
+ this.buildCount++
193
+ const buildId = this.buildCount
194
+
195
+ try {
196
+ this.log.section(`build #${buildId}`)
197
+ const startTime = Date.now()
198
+
199
+ if (!this.options.frameworkDefinitionInitializers) {
200
+ throw new Error(
201
+ 'There must be framework initalizers passed to frameworkDefinitionInitializers to use --dev-watch',
202
+ )
203
+ }
204
+
205
+ const refreshedFrameworks =
206
+ this.options.frameworkDefinitionInitializers.map(
207
+ (frameworkInitalizer) => frameworkInitalizer(),
208
+ )
209
+
210
+ const refreshedFramework = refreshedFrameworks.find(
211
+ (f) => f.id === this.options.framework.id,
212
+ )
213
+
214
+ if (!refreshedFramework) {
215
+ throw new Error('Could not identify the framework')
216
+ }
217
+
218
+ // Update the chosen addons to use the latest code
219
+ const chosenAddonIds = this.options.cliOptions.chosenAddOns.map(
220
+ (m) => m.id,
221
+ )
222
+ const updatedChosenAddons = refreshedFramework.addOns.filter((f) =>
223
+ chosenAddonIds.includes(f.id),
224
+ )
225
+
226
+ // Create temp directory for this build using tempy
227
+ this.tempDir = temporaryDirectory()
228
+
229
+ // Register the scanned framework
230
+ registerFramework({
231
+ ...refreshedFramework,
232
+ id: `${refreshedFramework.id}-updated`,
233
+ })
234
+
235
+ // Get the registered framework
236
+ const registeredFramework = getFrameworkById(
237
+ `${refreshedFramework.id}-updated`,
238
+ )
239
+ if (!registeredFramework) {
240
+ throw new Error(
241
+ `Failed to register framework: ${this.options.framework.id}`,
242
+ )
243
+ }
244
+
245
+ // Check if package.json was modified
246
+ const packageJsonModified = Array.from(changes).some(
247
+ (filePath) => path.basename(filePath) === 'package.json',
248
+ )
249
+
250
+ const updatedOptions: Options = {
251
+ ...this.options.cliOptions,
252
+ chosenAddOns: updatedChosenAddons,
253
+ framework: registeredFramework,
254
+ targetDir: this.tempDir,
255
+ git: false,
256
+ install: packageJsonModified,
257
+ }
258
+
259
+ // Show package installation indicator if needed
260
+ if (packageJsonModified) {
261
+ this.log.tree(' ', `${chalk.yellow('⟳')} installing packages...`)
262
+ }
263
+
264
+ // Create app in temp directory with silent environment
265
+ const silentEnvironment = createUIEnvironment(
266
+ this.options.environment.appName,
267
+ true,
268
+ )
269
+ await createApp(silentEnvironment, updatedOptions)
270
+
271
+ // Sync files to target directory
272
+ const syncResult = await this.syncer.sync(
273
+ this.tempDir,
274
+ this.options.targetDir,
275
+ )
276
+
277
+ // Clean up temp directory after sync is complete
278
+ try {
279
+ await fs.promises.rm(this.tempDir, { recursive: true, force: true })
280
+ } catch (cleanupError) {
281
+ this.log.warning(
282
+ `Failed to clean up temp directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
283
+ )
284
+ }
285
+
286
+ const elapsed = Date.now() - startTime
287
+
288
+ // Build tree-style summary
289
+ this.log.tree(' ', `duration: ${chalk.cyan(elapsed + 'ms')}`)
290
+
291
+ if (packageJsonModified) {
292
+ this.log.tree(' ', `packages: ${chalk.green('✓ installed')}`)
293
+ }
294
+
295
+ // Always show the last item in tree without checking for files to show
296
+ const noMoreTreeItems =
297
+ syncResult.updated.length === 0 &&
298
+ syncResult.created.length === 0 &&
299
+ syncResult.errors.length === 0
300
+
301
+ if (syncResult.updated.length > 0) {
302
+ this.log.tree(
303
+ ' ',
304
+ `updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`,
305
+ syncResult.created.length === 0 && syncResult.errors.length === 0,
306
+ )
307
+ }
308
+ if (syncResult.created.length > 0) {
309
+ this.log.tree(
310
+ ' ',
311
+ `created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`,
312
+ syncResult.errors.length === 0,
313
+ )
314
+ }
315
+ if (syncResult.errors.length > 0) {
316
+ this.log.tree(
317
+ ' ',
318
+ `failed: ${chalk.red(syncResult.errors.length + ' file' + (syncResult.errors.length > 1 ? 's' : ''))}`,
319
+ true,
320
+ )
321
+ }
322
+
323
+ // If nothing changed, show that
324
+ if (noMoreTreeItems) {
325
+ this.log.tree(' ', `no changes`, true)
326
+ }
327
+
328
+ // Always show changed files with diffs
329
+ if (syncResult.updated.length > 0) {
330
+ syncResult.updated.forEach((update, index) => {
331
+ const isLastFile =
332
+ index === syncResult.updated.length - 1 &&
333
+ syncResult.created.length === 0
334
+
335
+ // For files with diffs, always use ├─
336
+ const fileIsLast = isLastFile && !update.diff
337
+ this.log.treeItem(' ', update.path, fileIsLast)
338
+
339
+ // Always show diff if available
340
+ if (update.diff) {
341
+ const diffLines = update.diff.split('\n')
342
+ const relevantLines = diffLines
343
+ .slice(4)
344
+ .filter(
345
+ (line) =>
346
+ line.startsWith('+') ||
347
+ line.startsWith('-') ||
348
+ line.startsWith('@'),
349
+ )
350
+
351
+ if (relevantLines.length > 0) {
352
+ // Always use │ to continue the tree line through the diff
353
+ const prefix = ' │ '
354
+ relevantLines.forEach((line) => {
355
+ if (line.startsWith('+') && !line.startsWith('+++')) {
356
+ console.log(chalk.gray(prefix) + ' ' + chalk.green(line))
357
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
358
+ console.log(chalk.gray(prefix) + ' ' + chalk.red(line))
359
+ } else if (line.startsWith('@')) {
360
+ console.log(chalk.gray(prefix) + ' ' + chalk.cyan(line))
361
+ }
362
+ })
363
+ }
364
+ }
365
+ })
366
+ }
367
+
368
+ // Show created files
369
+ if (syncResult.created.length > 0) {
370
+ syncResult.created.forEach((file, index) => {
371
+ const isLast = index === syncResult.created.length - 1
372
+ this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast)
373
+ })
374
+ }
375
+
376
+ // Always show errors
377
+ if (syncResult.errors.length > 0) {
378
+ console.log() // Add spacing
379
+ syncResult.errors.forEach((err, index) => {
380
+ this.log.tree(
381
+ ' ',
382
+ `${chalk.red('error:')} ${err}`,
383
+ index === syncResult.errors.length - 1,
384
+ )
385
+ })
386
+ }
387
+ } catch (error) {
388
+ this.log.error(
389
+ `Build #${buildId} failed: ${error instanceof Error ? error.message : String(error)}`,
390
+ )
391
+ } finally {
392
+ this.isBuilding = false
393
+ }
394
+ }
395
+
396
+ private cleanup(): void {
397
+ console.log()
398
+ console.log('Cleaning up...')
399
+
400
+ // Clean up temp directory
401
+ if (this.tempDir && fs.existsSync(this.tempDir)) {
402
+ try {
403
+ fs.rmSync(this.tempDir, { recursive: true, force: true })
404
+ } catch (error) {
405
+ this.log.error(
406
+ `Failed to clean up temp directory: ${error instanceof Error ? error.message : String(error)}`,
407
+ )
408
+ }
409
+ }
410
+
411
+ process.exit(0)
412
+ }
413
+
414
+ private log = {
415
+ tree: (prefix: string, msg: string, isLast = false) => {
416
+ const connector = isLast ? '└─' : '├─'
417
+ console.log(chalk.gray(prefix + connector) + ' ' + msg)
418
+ },
419
+ treeItem: (prefix: string, msg: string, isLast = false) => {
420
+ const connector = isLast ? '└─' : '├─'
421
+ console.log(chalk.gray(prefix + ' ' + connector) + ' ' + msg)
422
+ },
423
+ info: (msg: string) => console.log(msg),
424
+ error: (msg: string) => console.error(chalk.red('✗') + ' ' + msg),
425
+ success: (msg: string) => console.log(chalk.green('✓') + ' ' + msg),
426
+ warning: (msg: string) => console.log(chalk.yellow('⚠') + ' ' + msg),
427
+ section: (title: string) => console.log('\n' + chalk.bold('▸ ' + title)),
428
+ subsection: (msg: string) => console.log(' ' + msg),
429
+ }
430
+ }