@tothalex/nulljs 0.0.48 → 0.0.53

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 (58) hide show
  1. package/package.json +22 -32
  2. package/src/cli.ts +24 -0
  3. package/src/commands/config.ts +130 -0
  4. package/src/commands/deploy.ts +182 -123
  5. package/src/commands/dev.ts +10 -0
  6. package/src/commands/host.ts +130 -139
  7. package/src/commands/index.ts +6 -8
  8. package/src/commands/secret.ts +364 -56
  9. package/src/commands/status.ts +41 -0
  10. package/src/components/DeployAnimation.tsx +92 -0
  11. package/src/components/DeploymentLogsPane.tsx +79 -0
  12. package/src/components/Header.tsx +57 -0
  13. package/src/components/HelpModal.tsx +64 -0
  14. package/src/components/SystemLogsPane.tsx +78 -0
  15. package/src/config/index.ts +181 -0
  16. package/src/lib/bundle/function.ts +125 -0
  17. package/src/lib/bundle/index.ts +3 -0
  18. package/src/lib/bundle/react.ts +149 -0
  19. package/src/lib/deploy.ts +103 -0
  20. package/src/lib/server.ts +160 -0
  21. package/src/lib/vite.ts +120 -0
  22. package/src/lib/watcher.ts +274 -0
  23. package/src/ui.tsx +363 -0
  24. package/tsconfig.json +30 -0
  25. package/scripts/install-server.js +0 -199
  26. package/src/commands/api.ts +0 -16
  27. package/src/commands/auth.ts +0 -54
  28. package/src/commands/create.ts +0 -43
  29. package/src/commands/dev/function/index.ts +0 -221
  30. package/src/commands/dev/function/utils.ts +0 -99
  31. package/src/commands/dev/index.tsx +0 -126
  32. package/src/commands/dev/logging-manager.ts +0 -87
  33. package/src/commands/dev/server/index.ts +0 -48
  34. package/src/commands/dev/server/utils.ts +0 -37
  35. package/src/commands/dev/ui/components/scroll-area.tsx +0 -141
  36. package/src/commands/dev/ui/components/tab-bar.tsx +0 -67
  37. package/src/commands/dev/ui/index.tsx +0 -71
  38. package/src/commands/dev/ui/logging-context.tsx +0 -76
  39. package/src/commands/dev/ui/tabs/functions-tab.tsx +0 -35
  40. package/src/commands/dev/ui/tabs/server-tab.tsx +0 -36
  41. package/src/commands/dev/ui/tabs/vite-tab.tsx +0 -35
  42. package/src/commands/dev/ui/use-logging.tsx +0 -34
  43. package/src/commands/dev/vite/index.ts +0 -54
  44. package/src/commands/dev/vite/utils.ts +0 -71
  45. package/src/commands/profile.ts +0 -189
  46. package/src/index.ts +0 -346
  47. package/src/lib/api.ts +0 -189
  48. package/src/lib/bundle/function/index.ts +0 -46
  49. package/src/lib/bundle/react/index.ts +0 -2
  50. package/src/lib/bundle/react/spa.ts +0 -77
  51. package/src/lib/bundle/react/ssr/client.ts +0 -93
  52. package/src/lib/bundle/react/ssr/config.ts +0 -77
  53. package/src/lib/bundle/react/ssr/index.ts +0 -4
  54. package/src/lib/bundle/react/ssr/props.ts +0 -71
  55. package/src/lib/bundle/react/ssr/server.ts +0 -83
  56. package/src/lib/config.ts +0 -347
  57. package/src/lib/deployment.ts +0 -244
  58. package/src/lib/update-server.ts +0 -262
package/package.json CHANGED
@@ -1,46 +1,36 @@
1
1
  {
2
2
  "name": "@tothalex/nulljs",
3
- "module": "index.ts",
4
- "version": "0.0.48",
3
+ "module": "src/index.tsx",
4
+ "version": "0.0.53",
5
5
  "type": "module",
6
6
  "bin": {
7
- "nulljs": "./src/index.ts"
7
+ "nulljs": "./src/cli.ts"
8
8
  },
9
9
  "scripts": {
10
- "postinstall": "bun scripts/install-server.js"
10
+ "dev": "bun run src/cli.ts dev"
11
11
  },
12
- "files": [
13
- "src",
14
- "scripts"
15
- ],
16
12
  "devDependencies": {
17
- "@eslint/js": "^9.20.0",
18
- "@types/bun": "1.2.16",
19
- "@types/degit": "^2.8.6",
20
- "@types/mime-types": "^2.1.4",
21
- "@types/yargs": "^17.0.33",
22
- "eslint": "^9.20.1",
23
- "rollup": "^4.30.1",
24
- "typescript-eslint": "^8.24.0"
13
+ "@types/bun": "latest",
14
+ "@types/react": "^19.2.3"
25
15
  },
26
16
  "peerDependencies": {
27
- "typescript": "~5.7.2"
17
+ "typescript": "^5"
18
+ },
19
+ "optionalDependencies": {
20
+ "@tothalex/nulljs-darwin-arm64": "^0.0.79",
21
+ "@tothalex/nulljs-linux-arm64": "^0.0.79",
22
+ "@tothalex/nulljs-linux-x64": "^0.0.79"
28
23
  },
29
24
  "dependencies": {
30
- "@tailwindcss/vite": "^4.1.11",
31
- "@types/react": "^19.1.11",
32
- "@vitejs/plugin-react": "^4.3.4",
33
- "@vitejs/plugin-react-swc": "^3.8.0",
34
- "chalk": "^5.4.1",
35
- "chokidar": "^4.0.3",
36
- "degit": "^2.8.4",
37
- "execa": "^9.6.0",
38
- "glob": "^11.0.3",
39
- "ink": "^6.5.1",
40
- "mime-types": "^2.1.35",
41
- "react": "^19.1.1",
42
- "tar": "^7.5.2",
43
- "vite": "^7.0.0",
44
- "yargs": "^17.7.2"
25
+ "@clack/prompts": "^0.11.0",
26
+ "@nulljs/api": "workspace:*",
27
+ "@opentui/core": "^0.1.62",
28
+ "@opentui/react": "^0.1.62",
29
+ "@tailwindcss/vite": "^4.1.18",
30
+ "@vitejs/plugin-react": "^5.1.2",
31
+ "chalk": "^5.6.2",
32
+ "commander": "^14.0.2",
33
+ "react": "^19.2.3",
34
+ "vite": "^7.3.0"
45
35
  }
46
36
  }
package/src/cli.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from 'commander'
3
+
4
+ import {
5
+ registerDevCommand,
6
+ registerDeployCommand,
7
+ registerConfigCommand,
8
+ registerStatusCommand,
9
+ registerSecretCommand,
10
+ registerHostCommand
11
+ } from './commands'
12
+
13
+ const program = new Command()
14
+
15
+ program.name('nulljs').description('NullJS CLI').version('0.0.49')
16
+
17
+ registerDevCommand(program)
18
+ registerDeployCommand(program)
19
+ registerConfigCommand(program)
20
+ registerStatusCommand(program)
21
+ registerSecretCommand(program)
22
+ registerHostCommand(program)
23
+
24
+ program.parse()
@@ -0,0 +1,130 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import * as p from '@clack/prompts'
4
+
5
+ import { createConfig, listConfigs, useConfig } from '../config'
6
+
7
+ export const registerConfigCommand = (program: Command) => {
8
+ program
9
+ .command('config')
10
+ .description('Configuration management')
11
+ .addCommand(
12
+ new Command('new')
13
+ .description('Create a new configuration')
14
+ .argument('[name]', 'Configuration name (e.g., prod, staging)')
15
+ .argument('[api]', 'API endpoint (e.g., https://api.example.com)')
16
+ .action(async (name?: string, api?: string) => {
17
+ let configName = name
18
+ let configApi = api
19
+
20
+ // Interactive mode if no arguments provided
21
+ if (!configName || !configApi) {
22
+ p.intro(chalk.cyan('Create new configuration'))
23
+
24
+ if (!configName) {
25
+ const nameInput = await p.text({
26
+ message: 'Configuration name',
27
+ placeholder: 'e.g., prod, staging',
28
+ validate: (value) => {
29
+ if (!value.trim()) return 'Name is required'
30
+ if (!/^[a-zA-Z0-9-_]+$/.test(value)) {
31
+ return 'Name can only contain letters, numbers, hyphens and underscores'
32
+ }
33
+ }
34
+ })
35
+
36
+ if (p.isCancel(nameInput)) {
37
+ p.cancel('Configuration cancelled')
38
+ process.exit(0)
39
+ }
40
+
41
+ configName = nameInput as string
42
+ }
43
+
44
+ if (!configApi) {
45
+ const apiInput = await p.text({
46
+ message: 'API endpoint',
47
+ placeholder: 'e.g., https://api.example.com',
48
+ validate: (value) => {
49
+ if (!value.trim()) return 'API endpoint is required'
50
+ try {
51
+ new URL(value)
52
+ } catch {
53
+ return 'Please enter a valid URL'
54
+ }
55
+ }
56
+ })
57
+
58
+ if (p.isCancel(apiInput)) {
59
+ p.cancel('Configuration cancelled')
60
+ process.exit(0)
61
+ }
62
+
63
+ configApi = apiInput as string
64
+ }
65
+ }
66
+
67
+ await createConfig(configName, configApi)
68
+ })
69
+ )
70
+ .addCommand(
71
+ new Command('list').description('List all configurations').action(() => {
72
+ const result = listConfigs()
73
+
74
+ if (!result || result.configs.length === 0) {
75
+ console.log(chalk.yellow('No configurations found.'))
76
+ console.log(chalk.gray(' Run "nulljs dev" to create a dev config, or'))
77
+ console.log(chalk.gray(' Run "nulljs config new <name> <api>" to create a new config'))
78
+ return
79
+ }
80
+
81
+ console.log(chalk.bold('Configurations:'))
82
+ result.configs.forEach((config) => {
83
+ const isActive = config.name === result.current
84
+ const marker = isActive ? chalk.green('●') : chalk.gray('○')
85
+ const name = isActive ? chalk.green(config.name) : config.name
86
+ const suffix = isActive ? chalk.green(' (active)') : ''
87
+ console.log(` ${marker} ${name}${suffix}`)
88
+ console.log(chalk.gray(` API: ${config.api}`))
89
+ })
90
+ })
91
+ )
92
+ .addCommand(
93
+ new Command('use')
94
+ .description('Switch to a configuration')
95
+ .argument('[name]', 'Configuration name')
96
+ .action(async (name?: string) => {
97
+ let configName = name
98
+
99
+ if (!configName) {
100
+ const configList = listConfigs()
101
+
102
+ if (!configList || configList.configs.length === 0) {
103
+ console.log(chalk.yellow('No configurations found.'))
104
+ console.log(chalk.gray(' Run "nulljs dev" to create a dev config, or'))
105
+ console.log(chalk.gray(' Run "nulljs config new" to create a new config'))
106
+ return
107
+ }
108
+
109
+ const selected = await p.select({
110
+ message: 'Select config to use',
111
+ options: configList.configs.map((c) => ({
112
+ value: c.name,
113
+ label: c.name,
114
+ hint: c.name === configList.current ? 'current' : c.api
115
+ })),
116
+ initialValue: configList.current
117
+ })
118
+
119
+ if (p.isCancel(selected)) {
120
+ p.cancel('Cancelled')
121
+ process.exit(0)
122
+ }
123
+
124
+ configName = selected as string
125
+ }
126
+
127
+ useConfig(configName)
128
+ })
129
+ )
130
+ }
@@ -1,160 +1,219 @@
1
- import { resolve } from 'path'
2
- import { readdir, exists, stat } from 'node:fs/promises'
1
+ import type { Command } from 'commander'
3
2
  import chalk from 'chalk'
4
- import chokidar from 'chokidar'
3
+ import { basename, resolve, join } from 'path'
4
+ import { existsSync } from 'fs'
5
+ import { readdir } from 'fs/promises'
6
+ import * as p from '@clack/prompts'
5
7
 
6
- import { deployFunction, deployPage, isReact, isTypescript } from '../lib/deployment'
7
- import { requireProfileConfiguration } from '../lib/config'
8
+ import { loadPrivateKey, readLocalConfig, getConfig, listConfigs, type Config } from '../config'
9
+ import { isReact } from '../lib/bundle'
10
+ import { deployFunction, deployReact, clearDeployCache } from '../lib/deploy'
8
11
 
9
- const deployFunctionsRecursively = async (functionsPath: string, force: boolean = true) => {
10
- if (!(await exists(functionsPath))) {
11
- return
12
+ const FUNCTION_DIRS = ['api', 'cron', 'event']
13
+
14
+ const selectConfig = async (): Promise<Config | null> => {
15
+ const configList = listConfigs()
16
+
17
+ if (!configList || configList.configs.length === 0) {
18
+ return null
12
19
  }
13
20
 
14
- const functionsStat = await stat(functionsPath)
15
- if (!functionsStat.isDirectory()) {
16
- return
21
+ // If only one config, use it directly
22
+ if (configList.configs.length === 1) {
23
+ return configList.configs[0] ?? null
17
24
  }
18
25
 
19
- const items = await readdir(functionsPath)
20
- for (const item of items) {
21
- const fullPath = `${functionsPath}/${item}`
22
- const itemStat = await stat(fullPath)
23
-
24
- if (itemStat.isDirectory()) {
25
- // Recursively process subdirectories
26
- await deployFunctionsRecursively(fullPath, force)
27
- } else if (isTypescript(item)) {
28
- const result = await deployFunction(fullPath, console, force)
29
-
30
- if (result.error) {
31
- console.log(chalk.red(`${item} deployment failed: ${result.error.message}`))
32
- } else if (result.cached) {
33
- console.log(chalk.gray(`${item} skipped (no changes)`))
34
- } else if (result.deployed) {
35
- console.log(chalk.green(`${item} deployed successfully`))
36
- }
37
- }
26
+ const selected = await p.select({
27
+ message: 'Select config to deploy to',
28
+ options: configList.configs.map((c) => ({
29
+ value: c.name,
30
+ label: c.name,
31
+ hint: c.name === configList.current ? 'current' : c.api
32
+ })),
33
+ initialValue: configList.current
34
+ })
35
+
36
+ if (p.isCancel(selected)) {
37
+ p.cancel('Deployment cancelled')
38
+ process.exit(0)
38
39
  }
40
+
41
+ return getConfig(selected as string)
39
42
  }
40
43
 
41
- const deploy = async (path?: string, force: boolean = true) => {
42
- // Ensure a profile is selected and properly configured for deployment
43
- const { profileName, config } = requireProfileConfiguration(['api', 'key'])
44
- console.log(chalk.gray(`Using profile: ${profileName}`))
45
- console.log(config.api ? chalk.gray(`API: ${config.api}`) : '')
46
- console.log(config.key?.public ? chalk.gray(`Key: ${config.key.public}`) : '')
47
-
48
- let deployPath = path
49
-
50
- // If no path provided, look for index.tsx
51
- if (!deployPath) {
52
- const indexPath = resolve('src/index.tsx')
53
- if (await exists(indexPath)) {
54
- deployPath = indexPath
55
- } else {
56
- deployPath = resolve('.')
44
+ const findAllDeployables = async (srcPath: string) => {
45
+ const functions: string[] = []
46
+
47
+ // Find functions in src/function/{api,cron,event}/
48
+ const functionsPath = join(srcPath, 'function')
49
+ for (const dir of FUNCTION_DIRS) {
50
+ const dirPath = join(functionsPath, dir)
51
+ try {
52
+ const items = await readdir(dirPath, { withFileTypes: true })
53
+ for (const item of items) {
54
+ if (item.isFile() && (item.name.endsWith('.ts') || item.name.endsWith('.tsx'))) {
55
+ functions.push(join(dirPath, item.name))
56
+ }
57
+ }
58
+ } catch {
59
+ // Directory doesn't exist
57
60
  }
58
61
  }
59
62
 
60
- if (!(await exists(deployPath))) {
61
- console.log(chalk.red("Path doesn't exists: ") + chalk.bgRed(deployPath))
62
- process.exit(0)
63
- }
63
+ // React entrypoint is always src/index.tsx
64
+ const reactEntry = join(srcPath, 'index.tsx')
65
+ const hasReact = existsSync(reactEntry)
64
66
 
65
- const pathStat = await stat(deployPath)
67
+ return { functions, reactEntry: hasReact ? reactEntry : null }
68
+ }
66
69
 
67
- if (pathStat.isFile()) {
68
- const file = resolve(deployPath)
70
+ const deployAll = async (config: Config, options: { force?: boolean }) => {
71
+ const srcPath = join(process.cwd(), 'src')
69
72
 
70
- if (isReact(file)) {
71
- console.log(`${chalk.gray('Deploying page: ')} ${chalk.green(file)}`)
72
- await deployPage(file)
73
- }
73
+ if (!existsSync(srcPath)) {
74
+ console.error(chalk.red(' No src directory found'))
75
+ process.exit(1)
76
+ }
74
77
 
75
- // Also deploy functions directory recursively
76
- const functionsPath = resolve('src/function')
77
- await deployFunctionsRecursively(functionsPath, force)
78
+ const { functions, reactEntry } = await findAllDeployables(srcPath)
79
+ const total = functions.length + (reactEntry ? 1 : 0)
78
80
 
81
+ if (total === 0) {
82
+ console.log(chalk.yellow('No functions or React app found to deploy'))
79
83
  return
80
84
  }
81
85
 
82
- const dir = await readdir(deployPath)
83
- const p = resolve(deployPath)
84
- for (const file of dir) {
85
- if (isTypescript(file)) {
86
- const result = await deployFunction(`${p}/${file}`, console, force)
87
-
88
- if (result.error) {
89
- console.log(chalk.red(`${file} deployment failed: ${result.error.message}`))
90
- } else if (result.cached) {
91
- console.log(chalk.gray(`${file} skipped (no changes)`))
92
- } else if (result.deployed) {
93
- console.log(chalk.green(`${file} deployed successfully`))
94
- }
95
- }
86
+ if (options.force) {
87
+ clearDeployCache()
88
+ }
96
89
 
97
- if (isReact(file)) {
98
- await deployPage(`${p}/${file}`)
90
+ const privateKey = await loadPrivateKey(config)
91
+ let successful = 0
92
+ let failed = 0
93
+ let skipped = 0
94
+
95
+ console.log(chalk.cyan(`Deploying ${total} file(s) to ${config.name}...\n`))
96
+
97
+ // Deploy functions
98
+ for (const filePath of functions) {
99
+ const fileName = basename(filePath)
100
+ try {
101
+ console.log(chalk.yellow('Bundling ') + chalk.bold(fileName))
102
+ const result = await deployFunction(filePath, privateKey, config.api)
103
+ if (result.skipped) {
104
+ console.log(chalk.gray('– Skipped ') + chalk.bold(fileName) + chalk.gray(' (unchanged)'))
105
+ skipped++
106
+ } else {
107
+ console.log(chalk.green('✓ Deployed ') + chalk.bold(fileName))
108
+ successful++
109
+ }
110
+ } catch (error) {
111
+ console.error(chalk.red('✗ Failed ') + chalk.bold(fileName) + chalk.red(`: ${error instanceof Error ? error.message : error}`))
112
+ failed++
99
113
  }
100
114
  }
101
115
 
102
- // Also deploy functions directory recursively
103
- const functionsPath = resolve('src/function')
104
- await deployFunctionsRecursively(functionsPath, force)
105
- }
106
-
107
- const onFileChange = async (file: string) => {
108
- const absolutePath = resolve(file)
109
- console.log(`File changed: ${absolutePath}`)
110
-
111
- try {
112
- const indexPage = resolve('src/index.tsx')
113
- if (await exists(indexPage)) {
114
- if (isReact(indexPage)) {
115
- await deployPage(indexPage)
116
+ // Deploy React app (src/index.tsx)
117
+ if (reactEntry) {
118
+ const fileName = basename(reactEntry)
119
+ try {
120
+ console.log(chalk.yellow('Bundling React SPA ') + chalk.bold(fileName))
121
+ const result = await deployReact(reactEntry, privateKey, config.api)
122
+ if (result.skipped) {
123
+ console.log(chalk.gray('– Skipped ') + chalk.bold(fileName) + chalk.gray(' (unchanged)'))
124
+ skipped++
125
+ } else {
126
+ console.log(chalk.green('✓ Deployed ') + chalk.bold(fileName))
127
+ successful++
116
128
  }
129
+ } catch (error) {
130
+ console.error(chalk.red('✗ Failed ') + chalk.bold(fileName) + chalk.red(`: ${error instanceof Error ? error.message : error}`))
131
+ failed++
117
132
  }
133
+ }
118
134
 
119
- // Deploy all files in src/functions/ directory recursively
120
- const functionsPath = resolve('src/function')
121
- await deployFunctionsRecursively(functionsPath)
122
- } catch (error) {
123
- console.error(chalk.red('Deployment failed:'), error)
135
+ console.log('')
136
+ if (failed > 0) {
137
+ console.log(chalk.red(`Deployment completed with errors: ${successful} deployed, ${skipped} skipped, ${failed} failed`))
138
+ process.exit(1)
139
+ } else {
140
+ console.log(chalk.green(`Deployment completed: ${successful} deployed, ${skipped} skipped`))
124
141
  }
125
142
  }
126
143
 
127
- const watch = async (path: string) => {
128
- // Ensure a profile is selected and properly configured for deployment
129
- const { profileName } = requireProfileConfiguration(['api', 'key'])
130
- console.log(chalk.gray(`Using profile: ${profileName}`))
144
+ export const registerDeployCommand = (program: Command) => {
145
+ program
146
+ .command('deploy')
147
+ .description('Bundle and deploy functions and React SPAs')
148
+ .argument('[file]', 'Path to a specific file (deploys all if omitted)')
149
+ .option('-e, --env <name>', 'Use a specific config environment')
150
+ .option('-f, --force', 'Force deploy even if unchanged')
151
+ .action(async (file: string | undefined, options: { env?: string; force?: boolean }) => {
152
+ let config: Config | null
153
+
154
+ if (options.env) {
155
+ // Use specified config
156
+ config = getConfig(options.env)
157
+ if (!config) {
158
+ console.error(chalk.red(`✗ Config "${options.env}" not found.`))
159
+ console.error(chalk.gray(' Run "nulljs config list" to see available configs'))
160
+ process.exit(1)
161
+ }
162
+ } else {
163
+ // Interactive picker
164
+ config = await selectConfig()
165
+ if (!config) {
166
+ console.error(chalk.red('✗ No configurations found.'))
167
+ console.error(chalk.gray(' Run "nulljs dev" first to initialize the project'))
168
+ process.exit(1)
169
+ }
170
+ }
131
171
 
132
- if (!exists(path)) {
133
- console.log(chalk.red("Path doesn't exists: ") + chalk.bgRed(path))
134
- process.exit(0)
135
- }
172
+ // If no file specified, deploy all
173
+ if (!file) {
174
+ await deployAll(config, options)
175
+ return
176
+ }
136
177
 
137
- const watcher = chokidar.watch(`${path}`, {
138
- ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)),
139
- persistent: true,
140
- ignoreInitial: true,
141
- cwd: process.cwd()
142
- })
178
+ // Single file deployment
179
+ const filePath = resolve(file)
143
180
 
144
- watcher
145
- .on('add', (filePath) => onFileChange(filePath))
146
- .on('change', (filePath) => onFileChange(filePath))
147
- .on('ready', () => {
148
- console.log('Initial scan complete. Ready for changes')
149
- })
181
+ if (!existsSync(filePath)) {
182
+ console.error(chalk.red(`✗ File not found: ${filePath}`))
183
+ process.exit(1)
184
+ }
150
185
 
151
- process.on('SIGINT', () => {
152
- console.log('\nShutting down file watcher...')
153
- watcher.close().then(() => {
154
- console.log('File watcher stopped')
155
- process.exit(0)
186
+ if (options.force) {
187
+ clearDeployCache()
188
+ }
189
+
190
+ const fileName = basename(filePath)
191
+ const privateKey = await loadPrivateKey(config)
192
+
193
+ try {
194
+ if (isReact(filePath)) {
195
+ console.log(chalk.yellow('Bundling React SPA ') + chalk.bold(fileName))
196
+ const result = await deployReact(filePath, privateKey, config.api)
197
+ if (result.skipped) {
198
+ console.log(chalk.gray('– Skipped ') + chalk.bold(fileName) + chalk.gray(' (unchanged)'))
199
+ } else {
200
+ console.log(chalk.green('✓ Deployed ') + chalk.bold(fileName) + chalk.gray(` to ${config.name}`))
201
+ }
202
+ } else {
203
+ console.log(chalk.yellow('Bundling ') + chalk.bold(fileName))
204
+ const result = await deployFunction(filePath, privateKey, config.api)
205
+ if (result.skipped) {
206
+ console.log(chalk.gray('– Skipped ') + chalk.bold(fileName) + chalk.gray(' (unchanged)'))
207
+ } else {
208
+ console.log(chalk.green('✓ Deployed ') + chalk.bold(fileName) + chalk.gray(` to ${config.name}`))
209
+ }
210
+ }
211
+ } catch (error) {
212
+ console.error(
213
+ chalk.red('✗ Deployment failed:'),
214
+ error instanceof Error ? error.message : error
215
+ )
216
+ process.exit(1)
217
+ }
156
218
  })
157
- })
158
219
  }
159
-
160
- export { deploy, watch }
@@ -0,0 +1,10 @@
1
+ import type { Command } from 'commander'
2
+
3
+ export const registerDevCommand = (program: Command) => {
4
+ program
5
+ .command('dev')
6
+ .description('Launch the development renderer')
7
+ .action(async () => {
8
+ await import('../ui.tsx')
9
+ })
10
+ }