@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.
- package/package.json +22 -32
- package/src/cli.ts +24 -0
- package/src/commands/config.ts +130 -0
- package/src/commands/deploy.ts +182 -123
- package/src/commands/dev.ts +10 -0
- package/src/commands/host.ts +130 -139
- package/src/commands/index.ts +6 -8
- package/src/commands/secret.ts +364 -56
- package/src/commands/status.ts +41 -0
- package/src/components/DeployAnimation.tsx +92 -0
- package/src/components/DeploymentLogsPane.tsx +79 -0
- package/src/components/Header.tsx +57 -0
- package/src/components/HelpModal.tsx +64 -0
- package/src/components/SystemLogsPane.tsx +78 -0
- package/src/config/index.ts +181 -0
- package/src/lib/bundle/function.ts +125 -0
- package/src/lib/bundle/index.ts +3 -0
- package/src/lib/bundle/react.ts +149 -0
- package/src/lib/deploy.ts +103 -0
- package/src/lib/server.ts +160 -0
- package/src/lib/vite.ts +120 -0
- package/src/lib/watcher.ts +274 -0
- package/src/ui.tsx +363 -0
- package/tsconfig.json +30 -0
- package/scripts/install-server.js +0 -199
- package/src/commands/api.ts +0 -16
- package/src/commands/auth.ts +0 -54
- package/src/commands/create.ts +0 -43
- package/src/commands/dev/function/index.ts +0 -221
- package/src/commands/dev/function/utils.ts +0 -99
- package/src/commands/dev/index.tsx +0 -126
- package/src/commands/dev/logging-manager.ts +0 -87
- package/src/commands/dev/server/index.ts +0 -48
- package/src/commands/dev/server/utils.ts +0 -37
- package/src/commands/dev/ui/components/scroll-area.tsx +0 -141
- package/src/commands/dev/ui/components/tab-bar.tsx +0 -67
- package/src/commands/dev/ui/index.tsx +0 -71
- package/src/commands/dev/ui/logging-context.tsx +0 -76
- package/src/commands/dev/ui/tabs/functions-tab.tsx +0 -35
- package/src/commands/dev/ui/tabs/server-tab.tsx +0 -36
- package/src/commands/dev/ui/tabs/vite-tab.tsx +0 -35
- package/src/commands/dev/ui/use-logging.tsx +0 -34
- package/src/commands/dev/vite/index.ts +0 -54
- package/src/commands/dev/vite/utils.ts +0 -71
- package/src/commands/profile.ts +0 -189
- package/src/index.ts +0 -346
- package/src/lib/api.ts +0 -189
- package/src/lib/bundle/function/index.ts +0 -46
- package/src/lib/bundle/react/index.ts +0 -2
- package/src/lib/bundle/react/spa.ts +0 -77
- package/src/lib/bundle/react/ssr/client.ts +0 -93
- package/src/lib/bundle/react/ssr/config.ts +0 -77
- package/src/lib/bundle/react/ssr/index.ts +0 -4
- package/src/lib/bundle/react/ssr/props.ts +0 -71
- package/src/lib/bundle/react/ssr/server.ts +0 -83
- package/src/lib/config.ts +0 -347
- package/src/lib/deployment.ts +0 -244
- 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.
|
|
4
|
-
"version": "0.0.
|
|
3
|
+
"module": "src/index.tsx",
|
|
4
|
+
"version": "0.0.53",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"nulljs": "./src/
|
|
7
|
+
"nulljs": "./src/cli.ts"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
10
|
+
"dev": "bun run src/cli.ts dev"
|
|
11
11
|
},
|
|
12
|
-
"files": [
|
|
13
|
-
"src",
|
|
14
|
-
"scripts"
|
|
15
|
-
],
|
|
16
12
|
"devDependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@types/
|
|
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": "
|
|
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
|
-
"@
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
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
|
+
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,160 +1,219 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readdir, exists, stat } from 'node:fs/promises'
|
|
1
|
+
import type { Command } from 'commander'
|
|
3
2
|
import chalk from 'chalk'
|
|
4
|
-
import
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
if (
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
63
|
+
// React entrypoint is always src/index.tsx
|
|
64
|
+
const reactEntry = join(srcPath, 'index.tsx')
|
|
65
|
+
const hasReact = existsSync(reactEntry)
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
return { functions, reactEntry: hasReact ? reactEntry : null }
|
|
68
|
+
}
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const deployAll = async (config: Config, options: { force?: boolean }) => {
|
|
71
|
+
const srcPath = join(process.cwd(), 'src')
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
if (!existsSync(srcPath)) {
|
|
74
|
+
console.error(chalk.red('✗ No src directory found'))
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
172
|
+
// If no file specified, deploy all
|
|
173
|
+
if (!file) {
|
|
174
|
+
await deployAll(config, options)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
136
177
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
persistent: true,
|
|
140
|
-
ignoreInitial: true,
|
|
141
|
-
cwd: process.cwd()
|
|
142
|
-
})
|
|
178
|
+
// Single file deployment
|
|
179
|
+
const filePath = resolve(file)
|
|
143
180
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 }
|