@tothalex/nulljs 0.0.40

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 (42) hide show
  1. package/package.json +45 -0
  2. package/scripts/install-server.js +132 -0
  3. package/src/commands/api.ts +16 -0
  4. package/src/commands/auth.ts +54 -0
  5. package/src/commands/create.ts +43 -0
  6. package/src/commands/deploy.ts +160 -0
  7. package/src/commands/dev/function/index.ts +221 -0
  8. package/src/commands/dev/function/utils.ts +99 -0
  9. package/src/commands/dev/index.tsx +126 -0
  10. package/src/commands/dev/logging-manager.ts +87 -0
  11. package/src/commands/dev/server/index.ts +48 -0
  12. package/src/commands/dev/server/utils.ts +37 -0
  13. package/src/commands/dev/ui/components/scroll-area.tsx +141 -0
  14. package/src/commands/dev/ui/components/tab-bar.tsx +67 -0
  15. package/src/commands/dev/ui/index.tsx +71 -0
  16. package/src/commands/dev/ui/logging-context.tsx +76 -0
  17. package/src/commands/dev/ui/tabs/functions-tab.tsx +35 -0
  18. package/src/commands/dev/ui/tabs/server-tab.tsx +36 -0
  19. package/src/commands/dev/ui/tabs/vite-tab.tsx +35 -0
  20. package/src/commands/dev/ui/use-logging.tsx +34 -0
  21. package/src/commands/dev/vite/index.ts +54 -0
  22. package/src/commands/dev/vite/utils.ts +71 -0
  23. package/src/commands/host.ts +339 -0
  24. package/src/commands/index.ts +8 -0
  25. package/src/commands/profile.ts +189 -0
  26. package/src/commands/secret.ts +79 -0
  27. package/src/index.ts +346 -0
  28. package/src/lib/api.ts +189 -0
  29. package/src/lib/bundle/external.ts +23 -0
  30. package/src/lib/bundle/function/index.ts +46 -0
  31. package/src/lib/bundle/index.ts +2 -0
  32. package/src/lib/bundle/react/index.ts +2 -0
  33. package/src/lib/bundle/react/spa.ts +77 -0
  34. package/src/lib/bundle/react/ssr/client.ts +93 -0
  35. package/src/lib/bundle/react/ssr/config.ts +77 -0
  36. package/src/lib/bundle/react/ssr/index.ts +4 -0
  37. package/src/lib/bundle/react/ssr/props.ts +71 -0
  38. package/src/lib/bundle/react/ssr/server.ts +83 -0
  39. package/src/lib/bundle/types.ts +4 -0
  40. package/src/lib/config.ts +347 -0
  41. package/src/lib/deployment.ts +244 -0
  42. package/src/lib/update-server.ts +180 -0
@@ -0,0 +1,99 @@
1
+ import { existsSync, readdirSync } from 'fs'
2
+ import { resolve, basename } from 'path'
3
+ import chalk from 'chalk'
4
+ import { deployFunction, isTypescript, type DeploymentResult } from '../../../lib/deployment'
5
+ import { createSecretsFromFile } from '../../secret'
6
+
7
+ export const deployFunctionAndLog = async (
8
+ filePath: string,
9
+ log: (data: string) => void,
10
+ force: boolean = false
11
+ ): Promise<void> => {
12
+ if (!isTypescript(filePath)) {
13
+ log(chalk.gray(`Skipping non-TypeScript file: ${basename(filePath)}`))
14
+ return
15
+ }
16
+
17
+ try {
18
+ // Silent logger for watch mode
19
+ const logger = {
20
+ log: () => {},
21
+ error: (error: any) => log(chalk.red(`Error: ${error}`))
22
+ }
23
+
24
+ const result: DeploymentResult = await deployFunction(filePath, logger, force)
25
+
26
+ // Show appropriate status based on actual result
27
+ if (result.error) {
28
+ log(chalk.red(` ${basename(filePath, '.ts')} deployment failed`))
29
+ if (result.error.message.includes('Unable to connect')) {
30
+ log(chalk.yellow(' Is your server running?'))
31
+ } else {
32
+ log(chalk.gray(` ${result.error.message}`))
33
+ }
34
+ } else if (result.cached) {
35
+ log(chalk.gray(` ${basename(filePath, '.ts')} skipped (no changes)`))
36
+ } else if (result.deployed) {
37
+ log(chalk.green(` ${basename(filePath, '.ts')} deployed successfully`))
38
+ }
39
+ } catch (error: any) {
40
+ log(chalk.red(` ✗ ${basename(filePath, '.ts')} failed`))
41
+ log(chalk.gray(` ${error.message || error}`))
42
+ }
43
+ }
44
+
45
+ export const deployAllFunctions = async (
46
+ srcDir: string,
47
+ log: (data: string) => void
48
+ ): Promise<void> => {
49
+ const functionDir = resolve(srcDir, 'function')
50
+
51
+ if (!existsSync(functionDir)) {
52
+ log(chalk.yellow('⚠️ No function directory found'))
53
+ return
54
+ }
55
+
56
+ try {
57
+ const files = readdirSync(functionDir, { recursive: true })
58
+ const tsFiles = files
59
+ .filter((file): file is string => typeof file === 'string')
60
+ .filter((file) => file.endsWith('.ts'))
61
+
62
+ if (tsFiles.length === 0) {
63
+ log(chalk.yellow('⚠️ No TypeScript functions found to deploy'))
64
+ return
65
+ }
66
+
67
+ log(chalk.blue(`🚀 Manually deploying ${tsFiles.length} functions...`))
68
+
69
+ for (const file of tsFiles) {
70
+ const absolutePath = resolve(functionDir, file)
71
+ await deployFunctionAndLog(absolutePath, log, true)
72
+ }
73
+
74
+ log(chalk.green('✅ Manual function deployment complete'))
75
+ } catch (error) {
76
+ log(chalk.red('❌ Error during manual function deployment: ' + error))
77
+ }
78
+ }
79
+
80
+ export const deploySecrets = async (srcDir: string, log: (data: string) => void): Promise<void> => {
81
+ const secretsPath = resolve(srcDir, '../.secrets')
82
+
83
+ log(chalk.cyan(`🔍 Checking for .secrets file at ${secretsPath}`))
84
+
85
+ if (!existsSync(secretsPath)) {
86
+ log(chalk.yellow('⚠️ No .secrets file found'))
87
+ return
88
+ }
89
+
90
+ try {
91
+ log(chalk.blue('🔐 Deploying secrets from .secrets file...' + ' ' + chalk.gray(secretsPath)))
92
+
93
+ await createSecretsFromFile({ filePath: secretsPath, log })
94
+
95
+ log(chalk.green('✅ Secret deployment complete'))
96
+ } catch (error) {
97
+ log(chalk.red('❌ Error during secret deployment: ' + error))
98
+ }
99
+ }
@@ -0,0 +1,126 @@
1
+ import { resolve } from 'path'
2
+ import { exists } from 'node:fs/promises'
3
+
4
+ import { createServer } from './server'
5
+ import { createVite } from './vite'
6
+ import { UI } from './ui/'
7
+ import { render } from 'ink'
8
+ import { LoggingManager } from './logging-manager'
9
+ import type { Tab } from './ui/components/tab-bar'
10
+ import { createFunctionWatcher } from './function'
11
+
12
+ const analyzeProject = async (srcDir: string) => {
13
+ const indexTsxPath = resolve(srcDir, 'index.tsx')
14
+ const functionDirPath = resolve(srcDir, 'function')
15
+
16
+ const hasReactApp = await exists(indexTsxPath)
17
+ const hasFunctions = await exists(functionDirPath)
18
+
19
+ return {
20
+ hasReactApp,
21
+ hasFunctions,
22
+ indexTsxPath: hasReactApp ? indexTsxPath : null
23
+ }
24
+ }
25
+
26
+ export const dev = async (srcPath: string) => {
27
+ const srcDir = resolve(srcPath)
28
+ const loggingManager = new LoggingManager()
29
+
30
+ const { hasFunctions, indexTsxPath } = await analyzeProject(srcDir)
31
+
32
+ const tabs: Tab[] = [
33
+ { name: 'Server', status: 'loading' },
34
+ { name: 'Functions', status: 'loading' },
35
+ { name: 'Vite', status: 'loading' }
36
+ ]
37
+
38
+ let viteServer: any = null
39
+ let serverProcess: any = null
40
+ let functionWatcher: any = null
41
+
42
+ if (indexTsxPath) {
43
+ viteServer = await createVite({
44
+ indexTsxPath,
45
+ srcDir,
46
+ log: loggingManager.getLogFunction('vite')
47
+ })
48
+
49
+ tabs.find((tab) => tab.name === 'Vite')!.status = 'running'
50
+ }
51
+
52
+ serverProcess = createServer({
53
+ log: loggingManager.getLogFunction('server')
54
+ })
55
+ tabs.find((tab) => tab.name === 'Server')!.status = 'running'
56
+
57
+ if (hasFunctions) {
58
+ functionWatcher = createFunctionWatcher({
59
+ srcDir,
60
+ log: loggingManager.getLogFunction('functions')
61
+ })
62
+
63
+ tabs.find((tab) => tab.name === 'Functions')!.status = 'running'
64
+ }
65
+
66
+ const shutdown = async () => {
67
+ loggingManager.addLog('server', 'info', 'Shutting down services...')
68
+
69
+ const shutdownTasks = []
70
+
71
+ if (viteServer) {
72
+ shutdownTasks.push(
73
+ viteServer
74
+ .close()
75
+ .then(() => {
76
+ loggingManager.addLog('vite', 'info', 'Vite server stopped')
77
+ tabs.find((tab) => tab.name === 'Vite')!.status = 'stopped'
78
+ })
79
+ .catch((error: any) =>
80
+ loggingManager.addLog('vite', 'error', `Error stopping Vite: ${error}`)
81
+ )
82
+ )
83
+ }
84
+
85
+ if (serverProcess) {
86
+ shutdownTasks.push(
87
+ Promise.resolve()
88
+ .then(() => {
89
+ serverProcess.kill()
90
+ loggingManager.addLog('server', 'info', 'Server process stopped')
91
+ tabs.find((tab) => tab.name === 'Server')!.status = 'stopped'
92
+ })
93
+ .catch((error: any) =>
94
+ loggingManager.addLog('server', 'error', `Error stopping server: ${error}`)
95
+ )
96
+ )
97
+ }
98
+
99
+ if (functionWatcher) {
100
+ shutdownTasks.push(
101
+ functionWatcher
102
+ .close()
103
+ .then(() => {
104
+ loggingManager.addLog('functions', 'info', 'Function watcher stopped')
105
+ tabs.find((tab) => tab.name === 'Functions')!.status = 'stopped'
106
+ })
107
+ .catch((error: any) =>
108
+ loggingManager.addLog('functions', 'error', `Error stopping function watcher: ${error}`)
109
+ )
110
+ )
111
+ }
112
+
113
+ try {
114
+ await Promise.race([
115
+ Promise.all(shutdownTasks),
116
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), 3000))
117
+ ])
118
+ } catch (error) {
119
+ loggingManager.addLog('server', 'warn', `Shutdown timeout or error: ${error}`)
120
+ }
121
+
122
+ process.exit(0)
123
+ }
124
+
125
+ render(<UI tabs={tabs} loggingManager={loggingManager} onShutdown={shutdown} srcDir={srcDir} />)
126
+ }
@@ -0,0 +1,87 @@
1
+ export type LogEntry = {
2
+ id: string
3
+ timestamp: Date
4
+ service: 'server' | 'functions' | 'vite'
5
+ level: 'info' | 'warn' | 'error'
6
+ message: string
7
+ }
8
+
9
+ export class LoggingManager {
10
+ private logs: Record<string, LogEntry[]> = {
11
+ server: [],
12
+ functions: [],
13
+ vite: []
14
+ }
15
+
16
+ private listeners: Set<() => void> = new Set()
17
+
18
+ addLog(service: LogEntry['service'], level: LogEntry['level'], message: string) {
19
+ const newLog: LogEntry = {
20
+ id: `${service}-${Date.now()}-${Math.random()}`,
21
+ timestamp: new Date(),
22
+ service,
23
+ level,
24
+ message
25
+ }
26
+
27
+ this.logs[service].push(newLog)
28
+ this.notifyListeners()
29
+ }
30
+
31
+ getLogs(service?: LogEntry['service']): LogEntry[] {
32
+ if (service) {
33
+ return [...this.logs[service]]
34
+ }
35
+ return Object.values(this.logs).flat()
36
+ }
37
+
38
+ getAllLogs(): Record<string, LogEntry[]> {
39
+ return {
40
+ server: [...this.logs.server],
41
+ functions: [...this.logs.functions],
42
+ vite: [...this.logs.vite]
43
+ }
44
+ }
45
+
46
+ clearLogs(service?: LogEntry['service']) {
47
+ if (service) {
48
+ this.logs[service] = []
49
+ } else {
50
+ this.logs = {
51
+ server: [],
52
+ functions: [],
53
+ vite: []
54
+ }
55
+ }
56
+ this.notifyListeners()
57
+ }
58
+
59
+ subscribe(listener: () => void) {
60
+ this.listeners.add(listener)
61
+
62
+ return () => this.listeners.delete(listener)
63
+ }
64
+
65
+ private notifyListeners() {
66
+ this.listeners.forEach((listener) => listener())
67
+ }
68
+
69
+ getLogFunction(service: LogEntry['service']) {
70
+ return (data: any) => {
71
+ let message: string
72
+ let level: LogEntry['level'] = 'info'
73
+
74
+ if (typeof data === 'string') {
75
+ message = data
76
+ } else if (data && typeof data === 'object') {
77
+ message = data.message || JSON.stringify(data)
78
+ level = data.level || level
79
+ } else {
80
+ message = String(data)
81
+ }
82
+
83
+ this.addLog(service, level, message)
84
+ }
85
+ }
86
+ }
87
+
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk'
2
+ import { spawn } from 'bun'
3
+ import { existsSync } from 'fs'
4
+
5
+ import { buildServerArgs, getServerBinPath } from './utils'
6
+ import { getDevConfig } from '../../../lib/config'
7
+
8
+ export const createServer = (props: { log?: (data: string) => void }) => {
9
+ const serverBinPath = getServerBinPath()
10
+
11
+ if (!existsSync(serverBinPath)) {
12
+ console.log(chalk.yellow('⚠️ Server binary not found ' + serverBinPath))
13
+ console.log(chalk.gray(' Run: bun run postinstall to install the server'))
14
+ return null
15
+ }
16
+
17
+ const devConfig = getDevConfig()
18
+
19
+ const args = buildServerArgs(devConfig)
20
+
21
+ const process = spawn([serverBinPath, ...args], {
22
+ stdout: props.log ? 'pipe' : 'inherit',
23
+ stderr: props.log ? 'pipe' : 'inherit'
24
+ })
25
+
26
+ if (props.log && process.stdout && process.stderr) {
27
+ const decoder = new TextDecoder()
28
+
29
+ const readStream = async (stream: ReadableStream<Uint8Array>) => {
30
+ const reader = stream.getReader()
31
+ while (true) {
32
+ const { done, value } = await reader.read()
33
+ if (done) {
34
+ break
35
+ }
36
+
37
+ if (props.log) {
38
+ props.log(decoder.decode(value))
39
+ }
40
+ }
41
+ }
42
+
43
+ readStream(process.stdout)
44
+ readStream(process.stderr)
45
+ }
46
+
47
+ return process
48
+ }
@@ -0,0 +1,37 @@
1
+ import { resolve, dirname, join } from 'path'
2
+ import { existsSync } from 'fs'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ import { getCloudPath, type DevConfig } from '../../../lib/config'
6
+
7
+ export const getServerBinPath = (): string => {
8
+ // Try to find the server binary relative to this module
9
+ // This works both in development and when installed globally
10
+ const currentFile = fileURLToPath(import.meta.url)
11
+ const moduleRoot = resolve(dirname(currentFile), '../../..')
12
+ const path = join(moduleRoot, 'bin', 'server')
13
+
14
+ if (!existsSync(path)) {
15
+ const currentFile = fileURLToPath(import.meta.url)
16
+ const moduleRoot = resolve(dirname(currentFile), '../../../..')
17
+ return join(moduleRoot, 'bin', 'server')
18
+ }
19
+
20
+ return path
21
+ }
22
+
23
+ export const buildServerArgs = (config: DevConfig): string[] => {
24
+ const args = ['--dev']
25
+
26
+ args.push('--api-port', config.apiPort.toString())
27
+ args.push('--gateway-port', config.gatewayPort.toString())
28
+
29
+ const cloudPath = getCloudPath()
30
+ args.push('--cloud-path', cloudPath)
31
+
32
+ if (config.publicKey) {
33
+ args.push('--public-key', config.publicKey)
34
+ }
35
+
36
+ return args
37
+ }
@@ -0,0 +1,141 @@
1
+ import { Box, type DOMElement, measureElement, useInput } from 'ink'
2
+ import { useEffect, useReducer, useRef } from 'react'
3
+
4
+ interface ScrollAreaState {
5
+ innerHeight: number
6
+ height: number
7
+ scrollTop: number
8
+ }
9
+
10
+ type ScrollAreaAction =
11
+ | { type: 'SET_INNER_HEIGHT'; innerHeight: number }
12
+ | { type: 'SET_HEIGHT'; height: number }
13
+ | { type: 'SCROLL_DOWN' }
14
+ | { type: 'SCROLL_UP' }
15
+ | { type: 'SCROLL_TO_BOTTOM' }
16
+ | { type: 'SCROLL_TO_TOP' }
17
+
18
+ const reducer = (state: ScrollAreaState, action: ScrollAreaAction) => {
19
+ switch (action.type) {
20
+ case 'SET_INNER_HEIGHT':
21
+ return {
22
+ ...state,
23
+ innerHeight: action.innerHeight
24
+ }
25
+ case 'SET_HEIGHT':
26
+ return {
27
+ ...state,
28
+ height: action.height
29
+ }
30
+
31
+ case 'SCROLL_DOWN':
32
+ return {
33
+ ...state,
34
+ scrollTop: Math.min(
35
+ state.innerHeight <= state.height ? 0 : state.innerHeight - state.height,
36
+ state.scrollTop + 1
37
+ )
38
+ }
39
+
40
+ case 'SCROLL_UP':
41
+ return {
42
+ ...state,
43
+ scrollTop: Math.max(0, state.scrollTop - 1)
44
+ }
45
+
46
+ case 'SCROLL_TO_BOTTOM':
47
+ return {
48
+ ...state,
49
+ scrollTop: Math.max(0, state.innerHeight - state.height)
50
+ }
51
+
52
+ case 'SCROLL_TO_TOP':
53
+ return {
54
+ ...state,
55
+ scrollTop: 0
56
+ }
57
+
58
+ default:
59
+ return state
60
+ }
61
+ }
62
+
63
+ export interface ScrollAreaProps extends React.PropsWithChildren {
64
+ height: number
65
+ autoScroll?: boolean
66
+ onRef?: (scrollToBottom: () => void) => void
67
+ onShutdown?: () => Promise<void>
68
+ }
69
+
70
+ export function ScrollArea({ height, children, autoScroll = false, onRef }: ScrollAreaProps) {
71
+ const [state, dispatch] = useReducer(reducer, {
72
+ height: height,
73
+ scrollTop: 0,
74
+ innerHeight: 0
75
+ })
76
+
77
+ const innerRef = useRef<DOMElement>(null)
78
+
79
+ const scrollToBottom = () => {
80
+ dispatch({ type: 'SCROLL_TO_BOTTOM' })
81
+ }
82
+
83
+ useEffect(() => {
84
+ if (onRef) {
85
+ onRef(scrollToBottom)
86
+ }
87
+ }, [onRef])
88
+
89
+ useEffect(() => {
90
+ dispatch({ type: 'SET_HEIGHT', height })
91
+ }, [height])
92
+
93
+ useEffect(() => {
94
+ if (!innerRef.current) return
95
+
96
+ const dimensions = measureElement(innerRef.current)
97
+
98
+ dispatch({
99
+ type: 'SET_INNER_HEIGHT',
100
+ innerHeight: dimensions.height
101
+ })
102
+
103
+ if (autoScroll) {
104
+ scrollToBottom()
105
+ }
106
+ }, [children, autoScroll])
107
+
108
+ useInput((input) => {
109
+ if (input === 'j') {
110
+ dispatch({
111
+ type: 'SCROLL_DOWN'
112
+ })
113
+ }
114
+
115
+ if (input === 'k') {
116
+ dispatch({
117
+ type: 'SCROLL_UP'
118
+ })
119
+ }
120
+
121
+ if (input === 'G') {
122
+ dispatch({
123
+ type: 'SCROLL_TO_BOTTOM'
124
+ })
125
+ }
126
+
127
+ if (input === 'g') {
128
+ dispatch({
129
+ type: 'SCROLL_TO_TOP'
130
+ })
131
+ }
132
+ })
133
+
134
+ return (
135
+ <Box height={height} flexDirection="column" flexGrow={1} overflow="hidden">
136
+ <Box ref={innerRef} flexShrink={0} flexDirection="column" marginTop={-state.scrollTop}>
137
+ {children}
138
+ </Box>
139
+ </Box>
140
+ )
141
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+
4
+ export type Tab = {
5
+ name: string
6
+ status?: 'running' | 'stopped' | 'error' | 'loading'
7
+ }
8
+
9
+ type TabBarProps = {
10
+ tabs: Tab[]
11
+ activeTabIndex: number
12
+ showKeyBindings?: boolean
13
+ isAutoScrolling?: boolean
14
+ }
15
+
16
+ const getStatusSymbol = (status?: Tab['status']): string => {
17
+ switch (status) {
18
+ case 'running':
19
+ return '●'
20
+ case 'stopped':
21
+ return '○'
22
+ case 'error':
23
+ return '✗'
24
+ case 'loading':
25
+ return '⋯'
26
+ default:
27
+ return '○'
28
+ }
29
+ }
30
+
31
+ export const TabBar: React.FC<TabBarProps> = ({ tabs, activeTabIndex, showKeyBindings = true }) => {
32
+ const tabsText =
33
+ tabs.length === 0
34
+ ? 'No services'
35
+ : tabs
36
+ .map((tab, index) => {
37
+ const statusSymbol = getStatusSymbol(tab.status)
38
+ const isActive = index === activeTabIndex
39
+ const activeIndicator = isActive ? '►' : ' '
40
+
41
+ return `${activeIndicator} ${index + 1}. ${statusSymbol} ${tab.name} `
42
+ })
43
+ .join('')
44
+
45
+ const currentTab = tabs[activeTabIndex]
46
+ const functionKeybinding =
47
+ currentTab?.name === 'Functions' ? ' | d: deploy all functions | s: deploy secrets' : ''
48
+ const keyBindings = showKeyBindings
49
+ ? `h/l: switch panels | j/k: scroll | g/G: top/bottom | q: quit${functionKeybinding}`
50
+ : ''
51
+
52
+ return (
53
+ <Box
54
+ borderStyle="single"
55
+ borderColor="white"
56
+ paddingX={1}
57
+ justifyContent="space-between"
58
+ flexDirection="row">
59
+ <Text>{tabsText}</Text>
60
+ {showKeyBindings && (
61
+ <Text color="gray" dimColor>
62
+ {keyBindings}
63
+ </Text>
64
+ )}
65
+ </Box>
66
+ )
67
+ }
@@ -0,0 +1,71 @@
1
+ import { useCallback, useState } from 'react'
2
+ import { Box, useStdout, useInput } from 'ink'
3
+
4
+ import { TabBar, type Tab } from './components/tab-bar'
5
+ import { ServerTab } from './tabs/server-tab'
6
+ import { FunctionsTab } from './tabs/functions-tab'
7
+ import { ViteTab } from './tabs/vite-tab'
8
+ import { LoggingManager } from '../logging-manager'
9
+ import { deployAllFunctions, deploySecrets } from '../function/utils'
10
+
11
+ type UIProps = {
12
+ loggingManager: LoggingManager
13
+ onShutdown: () => Promise<void>
14
+ tabs: Tab[]
15
+ srcDir: string
16
+ }
17
+
18
+ export const UI: React.FC<UIProps> = ({ loggingManager, onShutdown, tabs, srcDir }) => {
19
+ const { stdout } = useStdout()
20
+ const [activeTabIndex, setActiveTabIndex] = useState(0)
21
+
22
+ const switchToNextService = useCallback(() => {
23
+ setActiveTabIndex((prev) => (prev + 1) % tabs.length)
24
+ }, [tabs.length])
25
+
26
+ const switchToPrevService = useCallback(() => {
27
+ setActiveTabIndex((prev) => (prev - 1 + tabs.length) % tabs.length)
28
+ }, [tabs.length])
29
+
30
+ useInput(async (input, key) => {
31
+ if (input === 'l') {
32
+ switchToNextService()
33
+ }
34
+
35
+ if (input === 'h') {
36
+ switchToPrevService()
37
+ }
38
+
39
+ if (input === 'd') {
40
+ await deployAllFunctions(srcDir, loggingManager.getLogFunction('functions'))
41
+ }
42
+
43
+ if (input === 's') {
44
+ await deploySecrets(srcDir, loggingManager.getLogFunction('functions'))
45
+ }
46
+
47
+ if (input === 'q' || (key.ctrl && input === 'c')) {
48
+ await onShutdown()
49
+ }
50
+ })
51
+
52
+ const renderActiveTab = () => {
53
+ switch (activeTabIndex) {
54
+ case 0:
55
+ return <ServerTab loggingManager={loggingManager} />
56
+ case 1:
57
+ return <FunctionsTab loggingManager={loggingManager} />
58
+ case 2:
59
+ return <ViteTab loggingManager={loggingManager} />
60
+ default:
61
+ return <ServerTab loggingManager={loggingManager} />
62
+ }
63
+ }
64
+
65
+ return (
66
+ <Box flexDirection="column" width="100%" height={stdout?.rows - 1 || 24}>
67
+ <TabBar tabs={tabs} activeTabIndex={activeTabIndex} />
68
+ {renderActiveTab()}
69
+ </Box>
70
+ )
71
+ }