@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.
- package/package.json +45 -0
- package/scripts/install-server.js +132 -0
- package/src/commands/api.ts +16 -0
- package/src/commands/auth.ts +54 -0
- package/src/commands/create.ts +43 -0
- package/src/commands/deploy.ts +160 -0
- package/src/commands/dev/function/index.ts +221 -0
- package/src/commands/dev/function/utils.ts +99 -0
- package/src/commands/dev/index.tsx +126 -0
- package/src/commands/dev/logging-manager.ts +87 -0
- package/src/commands/dev/server/index.ts +48 -0
- package/src/commands/dev/server/utils.ts +37 -0
- package/src/commands/dev/ui/components/scroll-area.tsx +141 -0
- package/src/commands/dev/ui/components/tab-bar.tsx +67 -0
- package/src/commands/dev/ui/index.tsx +71 -0
- package/src/commands/dev/ui/logging-context.tsx +76 -0
- package/src/commands/dev/ui/tabs/functions-tab.tsx +35 -0
- package/src/commands/dev/ui/tabs/server-tab.tsx +36 -0
- package/src/commands/dev/ui/tabs/vite-tab.tsx +35 -0
- package/src/commands/dev/ui/use-logging.tsx +34 -0
- package/src/commands/dev/vite/index.ts +54 -0
- package/src/commands/dev/vite/utils.ts +71 -0
- package/src/commands/host.ts +339 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/profile.ts +189 -0
- package/src/commands/secret.ts +79 -0
- package/src/index.ts +346 -0
- package/src/lib/api.ts +189 -0
- package/src/lib/bundle/external.ts +23 -0
- package/src/lib/bundle/function/index.ts +46 -0
- package/src/lib/bundle/index.ts +2 -0
- package/src/lib/bundle/react/index.ts +2 -0
- package/src/lib/bundle/react/spa.ts +77 -0
- package/src/lib/bundle/react/ssr/client.ts +93 -0
- package/src/lib/bundle/react/ssr/config.ts +77 -0
- package/src/lib/bundle/react/ssr/index.ts +4 -0
- package/src/lib/bundle/react/ssr/props.ts +71 -0
- package/src/lib/bundle/react/ssr/server.ts +83 -0
- package/src/lib/bundle/types.ts +4 -0
- package/src/lib/config.ts +347 -0
- package/src/lib/deployment.ts +244 -0
- package/src/lib/update-server.ts +180 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type LogEntry = {
|
|
4
|
+
id: string
|
|
5
|
+
timestamp: Date
|
|
6
|
+
service: 'server' | 'functions' | 'vite'
|
|
7
|
+
level: 'info' | 'warn' | 'error'
|
|
8
|
+
message: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type LoggingContextType = {
|
|
12
|
+
logs: Record<string, LogEntry[]>
|
|
13
|
+
addLog: (service: LogEntry['service'], level: LogEntry['level'], message: string) => void
|
|
14
|
+
clearLogs: (service?: LogEntry['service']) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const LoggingContext = createContext<LoggingContextType | undefined>(undefined)
|
|
18
|
+
|
|
19
|
+
export const useLogging = () => {
|
|
20
|
+
const context = useContext(LoggingContext)
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error('useLogging must be used within a LoggingProvider')
|
|
23
|
+
}
|
|
24
|
+
return context
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type LoggingProviderProps = {
|
|
28
|
+
children: ReactNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const LoggingProvider: React.FC<LoggingProviderProps> = ({ children }) => {
|
|
32
|
+
const [logs, setLogs] = useState<Record<string, LogEntry[]>>({
|
|
33
|
+
server: [],
|
|
34
|
+
functions: [],
|
|
35
|
+
vite: []
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const addLog = useCallback(
|
|
39
|
+
(service: LogEntry['service'], level: LogEntry['level'], message: string) => {
|
|
40
|
+
const newLog: LogEntry = {
|
|
41
|
+
id: `${service}-${Date.now()}-${Math.random()}`,
|
|
42
|
+
timestamp: new Date(),
|
|
43
|
+
service,
|
|
44
|
+
level,
|
|
45
|
+
message
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setLogs((prev) => ({
|
|
49
|
+
...prev,
|
|
50
|
+
[service]: [...prev[service], newLog]
|
|
51
|
+
}))
|
|
52
|
+
},
|
|
53
|
+
[]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const clearLogs = useCallback((service?: LogEntry['service']) => {
|
|
57
|
+
if (service) {
|
|
58
|
+
setLogs((prev) => ({
|
|
59
|
+
...prev,
|
|
60
|
+
[service]: []
|
|
61
|
+
}))
|
|
62
|
+
} else {
|
|
63
|
+
setLogs({
|
|
64
|
+
server: [],
|
|
65
|
+
functions: [],
|
|
66
|
+
vite: []
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<LoggingContext.Provider value={{ logs, addLog, clearLogs }}>
|
|
73
|
+
{children}
|
|
74
|
+
</LoggingContext.Provider>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text, useStdout } from 'ink'
|
|
3
|
+
import { ScrollArea } from '../components/scroll-area'
|
|
4
|
+
import { LoggingManager } from '../../logging-manager'
|
|
5
|
+
import { useLogging } from '../use-logging'
|
|
6
|
+
|
|
7
|
+
type FunctionsTabProps = {
|
|
8
|
+
loggingManager: LoggingManager
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FunctionsTab: React.FC<FunctionsTabProps> = ({ loggingManager }) => {
|
|
12
|
+
const { stdout } = useStdout()
|
|
13
|
+
const { logs } = useLogging(loggingManager, 'functions')
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<ScrollArea height={stdout.rows - 5} autoScroll={true}>
|
|
17
|
+
<Box flexDirection="column" padding={1}>
|
|
18
|
+
<Text color="green">Functions</Text>
|
|
19
|
+
{logs.length === 0 ? (
|
|
20
|
+
<Text dimColor>No function logs yet...</Text>
|
|
21
|
+
) : (
|
|
22
|
+
<Box flexDirection="column">
|
|
23
|
+
{logs.map((log) => (
|
|
24
|
+
<Text
|
|
25
|
+
key={`function-tab-${log.id}`}
|
|
26
|
+
color={log.level === 'error' ? 'red' : log.level === 'warn' ? 'yellow' : 'white'}>
|
|
27
|
+
[{log.timestamp.toLocaleTimeString()}] {log.message}
|
|
28
|
+
</Text>
|
|
29
|
+
))}
|
|
30
|
+
</Box>
|
|
31
|
+
)}
|
|
32
|
+
</Box>
|
|
33
|
+
</ScrollArea>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text, useStdout } from 'ink'
|
|
3
|
+
|
|
4
|
+
import { ScrollArea } from '../components/scroll-area'
|
|
5
|
+
import { LoggingManager } from '../../logging-manager'
|
|
6
|
+
import { useLogging } from '../use-logging'
|
|
7
|
+
|
|
8
|
+
type ServerTabProps = {
|
|
9
|
+
loggingManager: LoggingManager
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ServerTab: React.FC<ServerTabProps> = ({ loggingManager }) => {
|
|
13
|
+
const { stdout } = useStdout()
|
|
14
|
+
const { logs } = useLogging(loggingManager, 'server')
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<ScrollArea height={stdout.rows - 5} autoScroll={true}>
|
|
18
|
+
<Box flexDirection="column" padding={1}>
|
|
19
|
+
<Text color="cyan">Server</Text>
|
|
20
|
+
{logs.length === 0 ? (
|
|
21
|
+
<Text dimColor>No server logs yet...</Text>
|
|
22
|
+
) : (
|
|
23
|
+
<Box flexDirection="column">
|
|
24
|
+
{logs.map((log) => (
|
|
25
|
+
<Text
|
|
26
|
+
key={log.id}
|
|
27
|
+
color={log.level === 'error' ? 'red' : log.level === 'warn' ? 'yellow' : 'white'}>
|
|
28
|
+
[{log.timestamp.toLocaleTimeString()}] {log.message}
|
|
29
|
+
</Text>
|
|
30
|
+
))}
|
|
31
|
+
</Box>
|
|
32
|
+
)}
|
|
33
|
+
</Box>
|
|
34
|
+
</ScrollArea>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text, useStdout } from 'ink'
|
|
3
|
+
import { ScrollArea } from '../components/scroll-area'
|
|
4
|
+
import { LoggingManager } from '../../logging-manager'
|
|
5
|
+
import { useLogging } from '../use-logging'
|
|
6
|
+
|
|
7
|
+
type ViteTabProps = {
|
|
8
|
+
loggingManager: LoggingManager
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ViteTab: React.FC<ViteTabProps> = ({ loggingManager }) => {
|
|
12
|
+
const { stdout } = useStdout()
|
|
13
|
+
const { logs } = useLogging(loggingManager, 'vite')
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<ScrollArea height={stdout.rows - 5} autoScroll={true}>
|
|
17
|
+
<Box flexDirection="column" padding={1}>
|
|
18
|
+
<Text color="yellow">Vite</Text>
|
|
19
|
+
{logs.length === 0 ? (
|
|
20
|
+
<Text dimColor>No Vite logs yet...</Text>
|
|
21
|
+
) : (
|
|
22
|
+
<Box flexDirection="column">
|
|
23
|
+
{logs.map((log) => (
|
|
24
|
+
<Text
|
|
25
|
+
key={log.id}
|
|
26
|
+
color={log.level === 'error' ? 'red' : log.level === 'warn' ? 'yellow' : 'white'}>
|
|
27
|
+
[{log.timestamp.toLocaleTimeString()}] {log.message}
|
|
28
|
+
</Text>
|
|
29
|
+
))}
|
|
30
|
+
</Box>
|
|
31
|
+
)}
|
|
32
|
+
</Box>
|
|
33
|
+
</ScrollArea>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { LoggingManager, type LogEntry } from '../logging-manager'
|
|
3
|
+
|
|
4
|
+
export const useLogging = (loggingManager: LoggingManager, service?: LogEntry['service']) => {
|
|
5
|
+
const [logs, setLogs] = useState<LogEntry[]>(() => {
|
|
6
|
+
return service ? loggingManager.getLogs(service) : loggingManager.getLogs()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const [allLogs, setAllLogs] = useState<Record<string, LogEntry[]>>(() => {
|
|
10
|
+
return loggingManager.getAllLogs()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const unsubscribe = loggingManager.subscribe(() => {
|
|
15
|
+
if (service) {
|
|
16
|
+
setLogs(loggingManager.getLogs(service))
|
|
17
|
+
} else {
|
|
18
|
+
setLogs(loggingManager.getLogs())
|
|
19
|
+
}
|
|
20
|
+
setAllLogs(loggingManager.getAllLogs())
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
unsubscribe()
|
|
25
|
+
}
|
|
26
|
+
}, [loggingManager, service])
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
logs,
|
|
30
|
+
allLogs,
|
|
31
|
+
addLog: loggingManager.addLog.bind(loggingManager),
|
|
32
|
+
clearLogs: loggingManager.clearLogs.bind(loggingManager)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createServer, type LogOptions } from 'vite'
|
|
2
|
+
import { dirname, resolve } from 'path'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
5
|
+
import { writeFile } from 'node:fs/promises'
|
|
6
|
+
|
|
7
|
+
import { createTempIndexHtml, createViteLogger } from './utils'
|
|
8
|
+
import { getDevConfig } from '../../../lib/config'
|
|
9
|
+
|
|
10
|
+
const PORT = 5173
|
|
11
|
+
|
|
12
|
+
export const createVite = async (props: {
|
|
13
|
+
srcDir: string
|
|
14
|
+
indexTsxPath: string
|
|
15
|
+
log: (msg: string, level?: string, options?: LogOptions) => void
|
|
16
|
+
}) => {
|
|
17
|
+
const devConfig = getDevConfig()
|
|
18
|
+
const projectDir = dirname(props.srcDir)
|
|
19
|
+
const tempIndexPath = resolve(projectDir, 'index.html')
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const indexHtml = createTempIndexHtml(props.indexTsxPath, projectDir)
|
|
23
|
+
await writeFile(tempIndexPath, indexHtml)
|
|
24
|
+
|
|
25
|
+
const server = await createServer({
|
|
26
|
+
root: projectDir,
|
|
27
|
+
plugins: [react(), tailwindcss()],
|
|
28
|
+
customLogger: createViteLogger(props.log),
|
|
29
|
+
server: {
|
|
30
|
+
port: PORT,
|
|
31
|
+
host: true,
|
|
32
|
+
proxy: {
|
|
33
|
+
'/api': {
|
|
34
|
+
target: `http://localhost:${devConfig.gatewayPort}`,
|
|
35
|
+
changeOrigin: true,
|
|
36
|
+
secure: false
|
|
37
|
+
},
|
|
38
|
+
'/assets': {
|
|
39
|
+
target: `http://localhost:${devConfig.gatewayPort}`,
|
|
40
|
+
changeOrigin: true,
|
|
41
|
+
secure: false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
await server.listen()
|
|
48
|
+
props.log(`Vite server started on http://localhost:${PORT}`)
|
|
49
|
+
|
|
50
|
+
return server
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error setting up Vite:', error)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { resolve, basename } from 'path'
|
|
2
|
+
import type { Logger, LogOptions } from 'vite'
|
|
3
|
+
|
|
4
|
+
export const createViteLogger = (
|
|
5
|
+
log: (msg: string, level?: string, options?: LogOptions) => void
|
|
6
|
+
): Logger => {
|
|
7
|
+
return {
|
|
8
|
+
info: (msg: string, options) => {
|
|
9
|
+
log(msg, 'info', options)
|
|
10
|
+
},
|
|
11
|
+
warn: (msg: string, options) => {
|
|
12
|
+
log(msg, 'warn', options)
|
|
13
|
+
},
|
|
14
|
+
warnOnce: (msg: string, options) => {
|
|
15
|
+
log(msg, 'warn', options)
|
|
16
|
+
},
|
|
17
|
+
error: (msg: string, options) => {
|
|
18
|
+
log(msg, 'error', options)
|
|
19
|
+
},
|
|
20
|
+
clearScreen: () => {
|
|
21
|
+
// Not implemented - we don't want to clear screens in our UI
|
|
22
|
+
},
|
|
23
|
+
hasErrorLogged: () => {
|
|
24
|
+
return false // Simple implementation
|
|
25
|
+
},
|
|
26
|
+
hasWarned: false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const createTempIndexHtml = (componentPath: string, projectRoot: string): string => {
|
|
31
|
+
const relativePath = resolve(componentPath).replace(projectRoot + '/', '')
|
|
32
|
+
const componentName = basename(componentPath, '.tsx')
|
|
33
|
+
|
|
34
|
+
return `<!DOCTYPE html>
|
|
35
|
+
<html lang="en">
|
|
36
|
+
<head>
|
|
37
|
+
<meta charset="UTF-8" />
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
39
|
+
<title>${componentName} - Dev Mode</title>
|
|
40
|
+
<style>
|
|
41
|
+
* {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
body {
|
|
45
|
+
margin: 0;
|
|
46
|
+
padding: 0;
|
|
47
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
48
|
+
}
|
|
49
|
+
#root {
|
|
50
|
+
min-height: 100vh;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<div id="root"></div>
|
|
56
|
+
<script type="module">
|
|
57
|
+
import React from 'react'
|
|
58
|
+
import ReactDOM from 'react-dom/client'
|
|
59
|
+
import { Page } from '/${relativePath}'
|
|
60
|
+
|
|
61
|
+
const root = ReactDOM.createRoot(document.getElementById('root'))
|
|
62
|
+
|
|
63
|
+
root.render(
|
|
64
|
+
React.createElement(React.StrictMode, null,
|
|
65
|
+
React.createElement(Page)
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
</script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>`
|
|
71
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join, resolve, dirname } from 'node:path'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { updateServer } from '../lib/update-server'
|
|
8
|
+
|
|
9
|
+
const getServerBinPath = (): string => {
|
|
10
|
+
// Try to find the server binary relative to this module
|
|
11
|
+
// This works both in development and when installed globally
|
|
12
|
+
const currentFile = fileURLToPath(import.meta.url)
|
|
13
|
+
const moduleRoot = resolve(dirname(currentFile), '../..')
|
|
14
|
+
return join(moduleRoot, 'bin', 'server')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ProductionConfig = {
|
|
18
|
+
key: {
|
|
19
|
+
private: string
|
|
20
|
+
public: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const generateProductionKeys = async (): Promise<{ privateKey: string; publicKey: string }> => {
|
|
25
|
+
console.log(chalk.blue('đ Generating production keys...'))
|
|
26
|
+
|
|
27
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
28
|
+
{
|
|
29
|
+
name: 'Ed25519',
|
|
30
|
+
namedCurve: 'Ed25519'
|
|
31
|
+
},
|
|
32
|
+
true,
|
|
33
|
+
['sign', 'verify']
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
|
37
|
+
const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey)
|
|
38
|
+
|
|
39
|
+
const privateKey = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)))
|
|
40
|
+
const publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)))
|
|
41
|
+
|
|
42
|
+
return { privateKey, publicKey }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const saveProductionConfig = (cloudPath: string, privateKey: string, publicKey: string): void => {
|
|
46
|
+
const configPath = join(cloudPath, 'config.json')
|
|
47
|
+
const config: ProductionConfig = {
|
|
48
|
+
key: {
|
|
49
|
+
private: privateKey,
|
|
50
|
+
public: publicKey
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.blue('đž Saving production configuration...'))
|
|
55
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
56
|
+
|
|
57
|
+
// Set proper permissions on the config file (readable only by owner)
|
|
58
|
+
execSync(`chmod 600 ${configPath}`, { stdio: 'inherit' })
|
|
59
|
+
|
|
60
|
+
console.log(chalk.green('â
Production configuration saved'))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const createSystemdService = (cloudPath: string, publicKey: string, serverBinPath: string, userName: string) => {
|
|
64
|
+
const serviceContent = `
|
|
65
|
+
[Unit]
|
|
66
|
+
Description=NullJS Server
|
|
67
|
+
After=network.target
|
|
68
|
+
StartLimitIntervalSec=0
|
|
69
|
+
|
|
70
|
+
[Service]
|
|
71
|
+
Type=simple
|
|
72
|
+
Restart=always
|
|
73
|
+
RestartSec=1
|
|
74
|
+
User=${userName}
|
|
75
|
+
ExecStart=${serverBinPath}
|
|
76
|
+
Environment=CLOUD_PATH=${cloudPath}
|
|
77
|
+
Environment=PUBLIC_KEY=${publicKey}
|
|
78
|
+
WorkingDirectory=${cloudPath}
|
|
79
|
+
|
|
80
|
+
[Install]
|
|
81
|
+
WantedBy=multi-user.target
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
return serviceContent
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const ensureCloudDirectory = (cloudPath: string) => {
|
|
89
|
+
if (!existsSync(cloudPath)) {
|
|
90
|
+
console.log(chalk.blue('đ§ Creating cloud directory...'))
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(cloudPath, { recursive: true })
|
|
93
|
+
console.log(chalk.green(`â
Cloud directory created: ${cloudPath}`))
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.log(chalk.red('â Failed to create cloud directory:'), error.message)
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
console.log(chalk.gray(`âšī¸ Cloud directory already exists: ${cloudPath}`))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
const installSystemdService = (serviceContent: string) => {
|
|
105
|
+
const servicePath = '/etc/systemd/system/nulljs.service'
|
|
106
|
+
|
|
107
|
+
console.log(chalk.blue('đ§ Installing systemd service...'))
|
|
108
|
+
|
|
109
|
+
// Write service file
|
|
110
|
+
writeFileSync('/tmp/nulljs.service', serviceContent)
|
|
111
|
+
execSync(`sudo mv /tmp/nulljs.service ${servicePath}`, { stdio: 'inherit' })
|
|
112
|
+
|
|
113
|
+
// Reload systemd and enable service
|
|
114
|
+
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' })
|
|
115
|
+
execSync('sudo systemctl enable nulljs.service', { stdio: 'inherit' })
|
|
116
|
+
|
|
117
|
+
console.log(chalk.green('â
Systemd service installed and enabled'))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const validateLinux = () => {
|
|
121
|
+
if (process.platform !== 'linux') {
|
|
122
|
+
console.log(chalk.red('â This command only works on Linux'))
|
|
123
|
+
process.exit(1)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const validateServerBinary = (serverBinPath: string) => {
|
|
128
|
+
if (!existsSync(serverBinPath)) {
|
|
129
|
+
console.log(chalk.red('â Server binary not found at:'), serverBinPath)
|
|
130
|
+
console.log(chalk.yellow(' Make sure you have built the server first'))
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const checkExistingProductionConfig = (cloudPath: string): ProductionConfig | null => {
|
|
136
|
+
const configPath = join(cloudPath, 'config.json')
|
|
137
|
+
|
|
138
|
+
if (!existsSync(configPath)) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const configContent = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
144
|
+
if (configContent.key?.private && configContent.key?.public) {
|
|
145
|
+
return configContent as ProductionConfig
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
console.log(chalk.yellow('â ī¸ Existing production config is invalid, will regenerate'))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const unhost = async (options: { keepData?: boolean } = {}) => {
|
|
155
|
+
validateLinux()
|
|
156
|
+
|
|
157
|
+
console.log(chalk.blue('đ§š Removing NullJS production hosting...'))
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Stop and disable the systemd service
|
|
161
|
+
console.log(chalk.blue('đ Stopping NullJS service...'))
|
|
162
|
+
try {
|
|
163
|
+
execSync('sudo systemctl stop nulljs', { stdio: 'inherit' })
|
|
164
|
+
console.log(chalk.green('â
Service stopped'))
|
|
165
|
+
} catch {
|
|
166
|
+
console.log(chalk.yellow('â ī¸ Service was not running'))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
execSync('sudo systemctl disable nulljs', { stdio: 'inherit' })
|
|
171
|
+
console.log(chalk.green('â
Service disabled'))
|
|
172
|
+
} catch {
|
|
173
|
+
console.log(chalk.yellow('â ī¸ Service was not enabled'))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Remove the systemd service file
|
|
177
|
+
const servicePath = '/etc/systemd/system/nulljs.service'
|
|
178
|
+
if (existsSync(servicePath)) {
|
|
179
|
+
console.log(chalk.blue('đī¸ Removing systemd service file...'))
|
|
180
|
+
execSync(`sudo rm ${servicePath}`, { stdio: 'inherit' })
|
|
181
|
+
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' })
|
|
182
|
+
console.log(chalk.green('â
Systemd service file removed'))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Remove cloud directory (optional)
|
|
186
|
+
if (!options.keepData) {
|
|
187
|
+
const defaultCloudPath = join(homedir(), '.nulljs')
|
|
188
|
+
if (existsSync(defaultCloudPath)) {
|
|
189
|
+
console.log(chalk.blue('đī¸ Removing cloud directory...'))
|
|
190
|
+
execSync(`rm -rf ${defaultCloudPath}`, { stdio: 'inherit' })
|
|
191
|
+
console.log(chalk.green('â
Cloud directory removed'))
|
|
192
|
+
} else {
|
|
193
|
+
console.log(chalk.gray('âšī¸ Cloud directory does not exist'))
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.gray('âšī¸ Keeping cloud data (--keep-data specified)'))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(chalk.green('đ NullJS hosting cleanup complete!'))
|
|
200
|
+
console.log('')
|
|
201
|
+
console.log(chalk.blue('đ Summary:'))
|
|
202
|
+
console.log(chalk.gray(' âĸ Systemd service stopped and removed'))
|
|
203
|
+
if (!options.keepData) {
|
|
204
|
+
console.log(chalk.gray(' âĸ Cloud directory removed'))
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.log(chalk.red('â Failed to remove hosting setup:'), error.message)
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const hostUpdate = async () => {
|
|
213
|
+
validateLinux()
|
|
214
|
+
|
|
215
|
+
console.log(chalk.blue('đ Updating NullJS server...'))
|
|
216
|
+
|
|
217
|
+
// Check if service exists
|
|
218
|
+
const serviceExists = existsSync('/etc/systemd/system/nulljs.service')
|
|
219
|
+
if (!serviceExists) {
|
|
220
|
+
console.log(chalk.red('â NullJS service is not installed. Run `nulljs host` first.'))
|
|
221
|
+
process.exit(1)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let wasRunning = false
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Check if service is running
|
|
228
|
+
execSync('sudo systemctl is-active nulljs >/dev/null 2>&1')
|
|
229
|
+
wasRunning = true
|
|
230
|
+
console.log(chalk.blue('đ Stopping NullJS service...'))
|
|
231
|
+
execSync('sudo systemctl stop nulljs', { stdio: 'inherit' })
|
|
232
|
+
console.log(chalk.green('â
Service stopped'))
|
|
233
|
+
} catch {
|
|
234
|
+
// Service wasn't running, that's fine
|
|
235
|
+
console.log(chalk.gray('âšī¸ Service was not running'))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// Update the binary
|
|
240
|
+
await updateServer()
|
|
241
|
+
|
|
242
|
+
// Restart service if it was running before
|
|
243
|
+
if (wasRunning) {
|
|
244
|
+
console.log(chalk.blue('đ Starting NullJS service...'))
|
|
245
|
+
execSync('sudo systemctl start nulljs', { stdio: 'inherit' })
|
|
246
|
+
console.log(chalk.green('â
Service restarted'))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(chalk.green('đ NullJS server update complete!'))
|
|
250
|
+
console.log('')
|
|
251
|
+
console.log(chalk.blue('đ Service management commands:'))
|
|
252
|
+
console.log(chalk.gray(' Start: '), chalk.white('sudo systemctl start nulljs'))
|
|
253
|
+
console.log(chalk.gray(' Stop: '), chalk.white('sudo systemctl stop nulljs'))
|
|
254
|
+
console.log(chalk.gray(' Status: '), chalk.white('sudo systemctl status nulljs'))
|
|
255
|
+
console.log(chalk.gray(' Logs: '), chalk.white('sudo journalctl -u nulljs -f'))
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.log(chalk.red('â Failed to update server:'), error.message)
|
|
258
|
+
|
|
259
|
+
// Try to restart the service even if update failed
|
|
260
|
+
if (wasRunning) {
|
|
261
|
+
try {
|
|
262
|
+
console.log(chalk.yellow('â ī¸ Attempting to restart service with previous binary...'))
|
|
263
|
+
execSync('sudo systemctl start nulljs', { stdio: 'inherit' })
|
|
264
|
+
console.log(chalk.green('â
Service restarted with previous binary'))
|
|
265
|
+
} catch (restartError) {
|
|
266
|
+
console.log(chalk.red('â Failed to restart service:'), restartError.message)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const host = async (cloudPath?: string, options: { update?: boolean } = {}) => {
|
|
275
|
+
// Handle update flag
|
|
276
|
+
if (options.update) {
|
|
277
|
+
return hostUpdate()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
validateLinux()
|
|
281
|
+
|
|
282
|
+
// Use provided cloud path or default to ~/.nulljs
|
|
283
|
+
const resolvedCloudPath = cloudPath
|
|
284
|
+
? cloudPath.startsWith('/')
|
|
285
|
+
? cloudPath
|
|
286
|
+
: join(process.cwd(), cloudPath)
|
|
287
|
+
: join(homedir(), '.nulljs')
|
|
288
|
+
|
|
289
|
+
console.log(chalk.blue('đ Setting up NullJS production hosting...'))
|
|
290
|
+
console.log(chalk.gray(` Cloud path: ${resolvedCloudPath}`))
|
|
291
|
+
|
|
292
|
+
const serverBinPath = getServerBinPath()
|
|
293
|
+
validateServerBinary(serverBinPath)
|
|
294
|
+
|
|
295
|
+
// Get current user
|
|
296
|
+
const currentUser = process.env.USER || process.env.USERNAME || 'root'
|
|
297
|
+
console.log(chalk.blue(`đ§ Setting up service to run as user: ${currentUser}`))
|
|
298
|
+
|
|
299
|
+
// Ensure cloud directory exists
|
|
300
|
+
ensureCloudDirectory(resolvedCloudPath)
|
|
301
|
+
|
|
302
|
+
// Check for existing production config or generate new keys
|
|
303
|
+
let productionConfig = checkExistingProductionConfig(resolvedCloudPath)
|
|
304
|
+
|
|
305
|
+
if (!productionConfig) {
|
|
306
|
+
const { privateKey, publicKey } = await generateProductionKeys()
|
|
307
|
+
saveProductionConfig(resolvedCloudPath, privateKey, publicKey)
|
|
308
|
+
productionConfig = { key: { private: privateKey, public: publicKey } }
|
|
309
|
+
|
|
310
|
+
console.log(chalk.green('đ Production keys generated'))
|
|
311
|
+
console.log(chalk.blue('đ Production Public Key:'))
|
|
312
|
+
console.log(chalk.gray(publicKey))
|
|
313
|
+
} else {
|
|
314
|
+
console.log(chalk.green('đ Using existing production keys'))
|
|
315
|
+
console.log(chalk.blue('đ Production Public Key:'))
|
|
316
|
+
console.log(chalk.gray(productionConfig.key.public))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Create and install systemd service
|
|
320
|
+
const serviceContent = createSystemdService(
|
|
321
|
+
resolvedCloudPath,
|
|
322
|
+
productionConfig.key.public,
|
|
323
|
+
serverBinPath,
|
|
324
|
+
currentUser
|
|
325
|
+
)
|
|
326
|
+
installSystemdService(serviceContent)
|
|
327
|
+
|
|
328
|
+
console.log(chalk.green('đ NullJS hosting setup complete!'))
|
|
329
|
+
console.log('')
|
|
330
|
+
console.log(chalk.blue('đ Service management commands:'))
|
|
331
|
+
console.log(chalk.gray(' Start: '), chalk.white('sudo systemctl start nulljs'))
|
|
332
|
+
console.log(chalk.gray(' Stop: '), chalk.white('sudo systemctl stop nulljs'))
|
|
333
|
+
console.log(chalk.gray(' Status: '), chalk.white('sudo systemctl status nulljs'))
|
|
334
|
+
console.log(chalk.gray(' Logs: '), chalk.white('sudo journalctl -u nulljs -f'))
|
|
335
|
+
console.log('')
|
|
336
|
+
console.log(chalk.yellow('â ī¸ Remember to start the service: sudo systemctl start nulljs'))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export { host, unhost }
|