Skip to content

Commit 4fa9ff4

Browse files
feat: improve dev watch command output with tree-style formatting
- Replace emoji-based logging with cleaner tree-style output - Add visual hierarchy for build progress and file changes - Show inline diffs with proper indentation - Reduce visual noise while maintaining all important information - Create silent environment for initial app creation to avoid duplicate output
1 parent 12b82dc commit 4fa9ff4

2 files changed

Lines changed: 131 additions & 43 deletions

File tree

packages/cta-cli/src/cli.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,15 @@ Remove your node_modules directory and package lock file and re-install.`,
408408
normalizedOpts.targetDir =
409409
options.targetDir || resolve(process.cwd(), projectName)
410410

411-
// Create the initial app
412-
intro(`Creating initial ${appName} app in ${normalizedOpts.targetDir}...`)
413-
await createApp(environment, normalizedOpts)
411+
// Create the initial app with minimal output for dev watch mode
412+
console.log(chalk.bold('\ndev-watch'))
413+
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
414+
if (normalizedOpts.install !== false) {
415+
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
416+
}
417+
const silentEnvironment = createUIEnvironment(appName, true)
418+
await createApp(silentEnvironment, normalizedOpts)
419+
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
414420

415421
// Now start the dev watch mode
416422
const manager = new DevWatchManager({

packages/cta-cli/src/dev-watch.ts

Lines changed: 122 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
registerFramework,
1111
} from '@tanstack/cta-engine'
1212
import { FileSyncer } from './file-syncer.js'
13+
import { createUIEnvironment } from './ui-environment.js'
1314
import type {
1415
Environment,
1516
Framework,
@@ -61,6 +62,10 @@ class DebounceQueue {
6162
}, this.delay)
6263
}
6364

65+
size(): number {
66+
return this.changes.size
67+
}
68+
6469
clear(): void {
6570
if (this.timer) {
6671
clearTimeout(this.timer)
@@ -100,11 +105,12 @@ export class DevWatchManager {
100105
throw new Error('Cannot use the --no-install flag when using --dev-watch')
101106
}
102107

103-
// Log startup
104-
this.log.info(`Starting dev watch mode`)
105-
this.log.info(`Watching: ${chalk.cyan(this.options.watchPath)}`)
106-
this.log.info(`Target: ${chalk.cyan(this.options.targetDir)}`)
107-
this.log.info(`Waiting for file changes...`)
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)
108114

109115
// Setup signal handlers
110116
process.on('SIGINT', () => this.cleanup())
@@ -115,7 +121,8 @@ export class DevWatchManager {
115121
}
116122

117123
async stop(): Promise<void> {
118-
this.log.info('Stopping dev watch mode')
124+
console.log()
125+
this.log.info('Stopping dev watch mode...')
119126

120127
if (this.watcher) {
121128
await this.watcher.close()
@@ -158,12 +165,20 @@ export class DevWatchManager {
158165
this.log.error(`Watcher error: ${error.message}`),
159166
)
160167

161-
this.log.success('File watcher started')
168+
this.watcher.on('ready', () => {
169+
// Already shown in startup, no need to repeat
170+
})
162171
}
163172

164173
private handleChange(_type: ChangeEvent['type'], filePath: string): void {
165174
const relativePath = path.relative(this.options.watchPath, filePath)
166-
this.log.change(relativePath)
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+
}
167182
this.debounceQueue.add(filePath)
168183
}
169184

@@ -178,7 +193,7 @@ export class DevWatchManager {
178193
const buildId = this.buildCount
179194

180195
try {
181-
this.log.build(`Starting build #${buildId}`)
196+
this.log.section(`build #${buildId}`)
182197
const startTime = Date.now()
183198

184199
if (!this.options.frameworkDefinitionInitializers) {
@@ -241,8 +256,17 @@ export class DevWatchManager {
241256
install: packageJsonModified,
242257
}
243258

244-
// Create app in temp directory
245-
await createApp(this.options.environment, updatedOptions)
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)
246270

247271
// Sync files to target directory
248272
const syncResult = await this.syncer.sync(
@@ -260,18 +284,61 @@ export class DevWatchManager {
260284
}
261285

262286
const elapsed = Date.now() - startTime
263-
this.log.success(`Build #${buildId} completed in ${elapsed}ms`)
264287

265-
// Report sync results
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+
266301
if (syncResult.updated.length > 0) {
267-
this.log.sync(`Updated ${syncResult.updated.length} files`)
268-
syncResult.updated.forEach((update) => {
269-
this.log.sync(` ↳ ${update.path}`)
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+
}
270322

271-
// Show diff if available
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
272340
if (update.diff) {
273341
const diffLines = update.diff.split('\n')
274-
// Skip the header lines and show the actual diff
275342
const relevantLines = diffLines
276343
.slice(4)
277344
.filter(
@@ -282,49 +349,58 @@ export class DevWatchManager {
282349
)
283350

284351
if (relevantLines.length > 0) {
285-
this.log.sync(` ${chalk.dim('Changes:')}`)
352+
// Always use │ to continue the tree line through the diff
353+
const prefix = ' │ '
286354
relevantLines.forEach((line) => {
287355
if (line.startsWith('+') && !line.startsWith('+++')) {
288-
this.log.sync(` ${chalk.green(line)}`)
356+
console.log(chalk.gray(prefix) + ' ' + chalk.green(line))
289357
} else if (line.startsWith('-') && !line.startsWith('---')) {
290-
this.log.sync(` ${chalk.red(line)}`)
358+
console.log(chalk.gray(prefix) + ' ' + chalk.red(line))
291359
} else if (line.startsWith('@')) {
292-
this.log.sync(` ${chalk.cyan(line)}`)
360+
console.log(chalk.gray(prefix) + ' ' + chalk.cyan(line))
293361
}
294362
})
295363
}
296364
}
297365
})
298366
}
367+
368+
// Show created files
299369
if (syncResult.created.length > 0) {
300-
this.log.sync(`Created ${syncResult.created.length} files`)
301-
syncResult.created.forEach((file) => this.log.sync(` ↳ ${file}`))
302-
}
303-
if (syncResult.skipped.length > 0) {
304-
this.log.sync(`Skipped ${syncResult.skipped.length} unchanged files`)
370+
syncResult.created.forEach((file, index) => {
371+
const isLast = index === syncResult.created.length - 1
372+
this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast)
373+
})
305374
}
375+
376+
// Always show errors
306377
if (syncResult.errors.length > 0) {
307-
this.log.error(`Failed to sync ${syncResult.errors.length} files`)
308-
syncResult.errors.forEach((err) => this.log.error(` ↳ ${err}`))
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+
})
309386
}
310387
} catch (error) {
311388
this.log.error(
312389
`Build #${buildId} failed: ${error instanceof Error ? error.message : String(error)}`,
313390
)
314-
console.error(error)
315391
} finally {
316392
this.isBuilding = false
317393
}
318394
}
319395

320396
private cleanup(): void {
321-
this.log.info('Cleaning up...')
397+
console.log()
398+
console.log('Cleaning up...')
322399

323400
// Clean up temp directory
324401
if (this.tempDir && fs.existsSync(this.tempDir)) {
325402
try {
326403
fs.rmSync(this.tempDir, { recursive: true, force: true })
327-
this.log.success('Temp directory cleaned up')
328404
} catch (error) {
329405
this.log.error(
330406
`Failed to clean up temp directory: ${error instanceof Error ? error.message : String(error)}`,
@@ -336,13 +412,19 @@ export class DevWatchManager {
336412
}
337413

338414
private log = {
339-
info: (msg: string) => console.log(chalk.blue('ℹ️ ') + ` ${msg}`),
340-
change: (path: string) =>
341-
console.log(chalk.yellow('📝') + ` File changed: ${path}`),
342-
build: (msg: string) => console.log(chalk.cyan('🔨') + ` ${msg}`),
343-
sync: (msg: string) => console.log(chalk.magenta(' ') + ` ${msg}`),
344-
error: (msg: string) => console.error(chalk.red('❌') + ` ${msg}`),
345-
success: (msg: string) => console.log(chalk.green('✅') + ` ${msg}`),
346-
warning: (msg: string) => console.log(chalk.yellow('⚠️ ') + ` ${msg}`),
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),
347429
}
348430
}

0 commit comments

Comments
 (0)