@tothalex/nulljs 0.0.47 → 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/src/ui.tsx
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { createCliRenderer } from '@opentui/core'
|
|
3
|
+
import { createRoot } from '@opentui/react'
|
|
4
|
+
import {
|
|
5
|
+
getSystemLogs,
|
|
6
|
+
searchDeploymentLogs,
|
|
7
|
+
type LogEntry,
|
|
8
|
+
type SystemLogEntry
|
|
9
|
+
} from '@nulljs/api'
|
|
10
|
+
|
|
11
|
+
import { startServer, type BinarySource } from './lib/server'
|
|
12
|
+
import { startWatcher, forceDeployAll } from './lib/watcher'
|
|
13
|
+
import { startViteServer, stopViteServer } from './lib/vite'
|
|
14
|
+
import { getOrCreateLocalDevConfig, type Config } from './config'
|
|
15
|
+
import { DeploymentLogsPane } from './components/DeploymentLogsPane'
|
|
16
|
+
import { SystemLogsPane } from './components/SystemLogsPane'
|
|
17
|
+
import { DeployAnimation, type DeployedFunction } from './components/DeployAnimation'
|
|
18
|
+
import { Header } from './components/Header'
|
|
19
|
+
import { HelpModal } from './components/HelpModal'
|
|
20
|
+
|
|
21
|
+
type WatcherEvent = {
|
|
22
|
+
type: 'deploy' | 'change'
|
|
23
|
+
deployed?: DeployedFunction[]
|
|
24
|
+
filePath?: string
|
|
25
|
+
success?: boolean
|
|
26
|
+
skipped?: boolean
|
|
27
|
+
error?: string
|
|
28
|
+
affectedCount?: number
|
|
29
|
+
timestamp: Date
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const App = (props: {
|
|
33
|
+
config: Config
|
|
34
|
+
binarySource: BinarySource
|
|
35
|
+
functionCount: number
|
|
36
|
+
viteRunning: boolean
|
|
37
|
+
onWatcherEvent: (handler: (event: WatcherEvent) => void) => void
|
|
38
|
+
onForceDeploy: () => Promise<void>
|
|
39
|
+
}) => {
|
|
40
|
+
const [error, setError] = useState<string | null>(null)
|
|
41
|
+
const [loading, setLoading] = useState(true)
|
|
42
|
+
const [deploymentLogs, setDeploymentLogs] = useState<LogEntry[]>([])
|
|
43
|
+
const [systemLogs, setSystemLogs] = useState<SystemLogEntry[]>([])
|
|
44
|
+
const [activeTab, setActiveTab] = useState<'deployment' | 'system'>('system')
|
|
45
|
+
const [autoScroll, setAutoScroll] = useState(true)
|
|
46
|
+
const [jumpTrigger, setJumpTrigger] = useState(0)
|
|
47
|
+
const [watcherStatus, setWatcherStatus] = useState<WatcherEvent | null>(null)
|
|
48
|
+
const [showKeyBindings, setShowKeyBindings] = useState(false)
|
|
49
|
+
const [isDeploying, setIsDeploying] = useState(false)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
props.onWatcherEvent((event) => {
|
|
53
|
+
console.error('DEBUG: received watcher event:', event.type, event.deployed?.length)
|
|
54
|
+
setWatcherStatus(event)
|
|
55
|
+
})
|
|
56
|
+
}, [props.onWatcherEvent])
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const fn = async () => {
|
|
60
|
+
try {
|
|
61
|
+
setLoading(true)
|
|
62
|
+
setError(null)
|
|
63
|
+
|
|
64
|
+
const [deploymentLogsData, systemLogsData] = await Promise.all([
|
|
65
|
+
searchDeploymentLogs(props.config.key.public, { limit: 100 }, props.config.api),
|
|
66
|
+
getSystemLogs(props.config.key.public, { limit: 100 }, props.config.api)
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
setDeploymentLogs(deploymentLogsData)
|
|
70
|
+
setSystemLogs(systemLogsData)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
73
|
+
console.error('Failed to fetch logs:', err)
|
|
74
|
+
} finally {
|
|
75
|
+
setLoading(false)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn()
|
|
80
|
+
}, [props.config.key.public, props.config.api])
|
|
81
|
+
|
|
82
|
+
// Poll for new logs every 2 seconds
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const interval = setInterval(async () => {
|
|
85
|
+
if (loading) return
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const [newDeploymentLogs, newSystemLogs] = await Promise.all([
|
|
89
|
+
searchDeploymentLogs(props.config.key.public, { limit: 100 }, props.config.api),
|
|
90
|
+
getSystemLogs(props.config.key.public, { limit: 100 }, props.config.api)
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
// Only update if we have new logs
|
|
94
|
+
if (newDeploymentLogs.length > 0) {
|
|
95
|
+
setDeploymentLogs(newDeploymentLogs)
|
|
96
|
+
}
|
|
97
|
+
if (newSystemLogs.length > 0) {
|
|
98
|
+
setSystemLogs(newSystemLogs)
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Failed to poll for new logs:', err)
|
|
102
|
+
}
|
|
103
|
+
}, 2000)
|
|
104
|
+
|
|
105
|
+
return () => clearInterval(interval)
|
|
106
|
+
}, [loading, props.config.key.public, props.config.api])
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const handleKeyPress = (data: Buffer) => {
|
|
110
|
+
const key = data.toString()
|
|
111
|
+
|
|
112
|
+
// Number keys to jump to specific panel
|
|
113
|
+
if (key === '1') {
|
|
114
|
+
setActiveTab('system')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if (key === '2') {
|
|
118
|
+
setActiveTab('deployment')
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Tab key cycles between panels
|
|
123
|
+
if (key === '\t') {
|
|
124
|
+
setActiveTab((prev) => (prev === 'system' ? 'deployment' : 'system'))
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// h/l for vim-style navigation
|
|
129
|
+
if (key === 'h') {
|
|
130
|
+
setActiveTab('system')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (key === 'l') {
|
|
134
|
+
setActiveTab('deployment')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// g - jump to top and disable auto-scroll
|
|
139
|
+
if (key === 'g') {
|
|
140
|
+
setAutoScroll(false)
|
|
141
|
+
setJumpTrigger((prev) => prev + 1)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Shift+G - jump to bottom and enable auto-scroll
|
|
146
|
+
if (key === 'G') {
|
|
147
|
+
setAutoScroll(true)
|
|
148
|
+
setJumpTrigger((prev) => prev + 1)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ? - toggle key bindings modal
|
|
153
|
+
if (key === '?') {
|
|
154
|
+
setShowKeyBindings((prev) => !prev)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ESC - close key bindings modal
|
|
159
|
+
if (key === '\x1b') {
|
|
160
|
+
setShowKeyBindings(false)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// d - force deploy all
|
|
165
|
+
if (key === 'd' && !isDeploying) {
|
|
166
|
+
setIsDeploying(true)
|
|
167
|
+
props.onForceDeploy().finally(() => {
|
|
168
|
+
setIsDeploying(false)
|
|
169
|
+
})
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
process.stdin.setRawMode(true)
|
|
175
|
+
process.stdin.on('data', handleKeyPress)
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
process.stdin.setRawMode(false)
|
|
179
|
+
process.stdin.off('data', handleKeyPress)
|
|
180
|
+
}
|
|
181
|
+
}, [isDeploying, props.onForceDeploy])
|
|
182
|
+
|
|
183
|
+
if (error) {
|
|
184
|
+
return (
|
|
185
|
+
<box flexGrow={1} flexDirection="column" padding={1}>
|
|
186
|
+
<text>
|
|
187
|
+
<span fg="red">Error: {error}</span>
|
|
188
|
+
</text>
|
|
189
|
+
</box>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<box flexDirection="column">
|
|
195
|
+
<Header
|
|
196
|
+
activeTab={activeTab}
|
|
197
|
+
functionCount={props.functionCount}
|
|
198
|
+
viteRunning={props.viteRunning}
|
|
199
|
+
isDeploying={isDeploying}
|
|
200
|
+
binarySource={props.binarySource}
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
<box paddingTop={1} flexDirection="column">
|
|
204
|
+
{activeTab === 'system' ? (
|
|
205
|
+
<SystemLogsPane
|
|
206
|
+
logs={systemLogs}
|
|
207
|
+
loading={loading}
|
|
208
|
+
autoScroll={autoScroll}
|
|
209
|
+
jumpTrigger={jumpTrigger}
|
|
210
|
+
/>
|
|
211
|
+
) : (
|
|
212
|
+
<DeploymentLogsPane
|
|
213
|
+
logs={deploymentLogs}
|
|
214
|
+
loading={loading}
|
|
215
|
+
autoScroll={autoScroll}
|
|
216
|
+
jumpTrigger={jumpTrigger}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
{watcherStatus?.type === 'deploy' &&
|
|
220
|
+
watcherStatus.deployed &&
|
|
221
|
+
watcherStatus.deployed.length > 0 && (
|
|
222
|
+
<DeployAnimation
|
|
223
|
+
deployed={watcherStatus.deployed}
|
|
224
|
+
onComplete={() => {
|
|
225
|
+
console.error('DEBUG: animation complete')
|
|
226
|
+
setWatcherStatus(null)
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
</box>
|
|
231
|
+
|
|
232
|
+
<HelpModal visible={showKeyBindings} />
|
|
233
|
+
</box>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const main = async () => {
|
|
238
|
+
let serverInfo: Awaited<ReturnType<typeof startServer>> | null = null
|
|
239
|
+
let viteInfo: Awaited<ReturnType<typeof startViteServer>> | null = null
|
|
240
|
+
let stopWatcher: (() => void) | null = null
|
|
241
|
+
let isCleaningUp = false
|
|
242
|
+
|
|
243
|
+
const cleanup = async (exitAfter = true) => {
|
|
244
|
+
if (isCleaningUp) return
|
|
245
|
+
isCleaningUp = true
|
|
246
|
+
|
|
247
|
+
if (stopWatcher) {
|
|
248
|
+
stopWatcher()
|
|
249
|
+
stopWatcher = null
|
|
250
|
+
}
|
|
251
|
+
if (viteInfo) {
|
|
252
|
+
await stopViteServer(viteInfo)
|
|
253
|
+
viteInfo = null
|
|
254
|
+
}
|
|
255
|
+
if (serverInfo) {
|
|
256
|
+
const pid = serverInfo.process.pid
|
|
257
|
+
serverInfo = null
|
|
258
|
+
try {
|
|
259
|
+
// Kill the server process and its children
|
|
260
|
+
process.kill(pid, 'SIGTERM')
|
|
261
|
+
} catch {
|
|
262
|
+
// Process might already be dead
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (exitAfter) {
|
|
266
|
+
process.exit(0)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
process.on('SIGINT', () => cleanup(true))
|
|
271
|
+
process.on('SIGTERM', () => cleanup(true))
|
|
272
|
+
process.on('exit', () => cleanup(false))
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
serverInfo = await startServer()
|
|
276
|
+
|
|
277
|
+
const devConfig = await getOrCreateLocalDevConfig()
|
|
278
|
+
|
|
279
|
+
const srcPath = process.cwd() + '/src'
|
|
280
|
+
|
|
281
|
+
viteInfo = await startViteServer({
|
|
282
|
+
srcDir: srcPath,
|
|
283
|
+
onLog: (_msg, _level) => {}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
let watcherEventHandler: ((event: WatcherEvent) => void) | null = null
|
|
287
|
+
let pendingEvents: WatcherEvent[] = []
|
|
288
|
+
let functionCount = 0
|
|
289
|
+
|
|
290
|
+
const sendEvent = (event: WatcherEvent) => {
|
|
291
|
+
if (watcherEventHandler) {
|
|
292
|
+
watcherEventHandler(event)
|
|
293
|
+
} else {
|
|
294
|
+
pendingEvents.push(event)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const renderer = await createCliRenderer()
|
|
299
|
+
const root = createRoot(renderer)
|
|
300
|
+
|
|
301
|
+
const handleForceDeploy = async () => {
|
|
302
|
+
const deployResults: DeployedFunction[] = []
|
|
303
|
+
|
|
304
|
+
await forceDeployAll(srcPath, {
|
|
305
|
+
onDeploy: (filePath, success, error) => {
|
|
306
|
+
deployResults.push({ name: filePath, success, error })
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
if (deployResults.length > 0) {
|
|
311
|
+
sendEvent({
|
|
312
|
+
type: 'deploy',
|
|
313
|
+
deployed: deployResults,
|
|
314
|
+
timestamp: new Date()
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Start watcher after renderer is ready
|
|
320
|
+
stopWatcher = await startWatcher(srcPath, {
|
|
321
|
+
silent: true,
|
|
322
|
+
onReady: (count) => {
|
|
323
|
+
functionCount = count
|
|
324
|
+
},
|
|
325
|
+
onDeployBatch: (results) => {
|
|
326
|
+
sendEvent({
|
|
327
|
+
type: 'deploy',
|
|
328
|
+
deployed: results.map((r) => ({
|
|
329
|
+
name: r.name,
|
|
330
|
+
success: r.success,
|
|
331
|
+
error: r.error
|
|
332
|
+
})),
|
|
333
|
+
timestamp: new Date()
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
root.render(
|
|
339
|
+
<App
|
|
340
|
+
config={devConfig}
|
|
341
|
+
binarySource={serverInfo.binarySource}
|
|
342
|
+
functionCount={functionCount}
|
|
343
|
+
viteRunning={viteInfo !== null}
|
|
344
|
+
onWatcherEvent={(handler) => {
|
|
345
|
+
watcherEventHandler = handler
|
|
346
|
+
// Flush any pending events
|
|
347
|
+
if (pendingEvents.length > 0) {
|
|
348
|
+
for (const event of pendingEvents) {
|
|
349
|
+
handler(event)
|
|
350
|
+
}
|
|
351
|
+
pendingEvents = []
|
|
352
|
+
}
|
|
353
|
+
}}
|
|
354
|
+
onForceDeploy={handleForceDeploy}
|
|
355
|
+
/>
|
|
356
|
+
)
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Failed to start server:', error)
|
|
359
|
+
cleanup()
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
main()
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"jsxImportSource": "@opentui/react",
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
// Best practices
|
|
19
|
+
"strict": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
|
|
25
|
+
// Some stricter flags (disabled by default)
|
|
26
|
+
"noUnusedLocals": false,
|
|
27
|
+
"noUnusedParameters": false,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
const https = require('https')
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const { createWriteStream } = require('fs')
|
|
7
|
-
const tar = require('tar')
|
|
8
|
-
|
|
9
|
-
// --- Configuration ---
|
|
10
|
-
const S3_BASE_URL = 'https://nulljs.s3.eu-north-1.amazonaws.com'
|
|
11
|
-
const S3_PREFIX = 'releases/'
|
|
12
|
-
const DOWNLOAD_BASE_URL = `${S3_BASE_URL}/${S3_PREFIX}`
|
|
13
|
-
// ---------------------
|
|
14
|
-
|
|
15
|
-
function getPlatformInfo() {
|
|
16
|
-
const platform = process.platform
|
|
17
|
-
const arch = process.arch
|
|
18
|
-
|
|
19
|
-
const platformMap = {
|
|
20
|
-
linux: {
|
|
21
|
-
x64: 'x86_64-unknown-linux-gnu',
|
|
22
|
-
arm64: 'aarch64-unknown-linux-gnu'
|
|
23
|
-
},
|
|
24
|
-
darwin: {
|
|
25
|
-
x64: 'x86_64-apple-darwin',
|
|
26
|
-
arm64: 'aarch64-apple-darwin'
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const target = platformMap[platform]?.[arch]
|
|
31
|
-
if (!target) {
|
|
32
|
-
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const extension = '.tar.gz'
|
|
36
|
-
const binaryName = 'server'
|
|
37
|
-
|
|
38
|
-
return { target, extension, binaryName }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Helper to fetch XML and return response data
|
|
42
|
-
async function fetchXml(url) {
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
https
|
|
45
|
-
.get(url, (res) => {
|
|
46
|
-
if (res.statusCode !== 200) {
|
|
47
|
-
reject(
|
|
48
|
-
new Error(`S3 Request Failed: ${res.statusCode}. Check if public listing is enabled.`)
|
|
49
|
-
)
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
let data = ''
|
|
53
|
-
res.on('data', (chunk) => (data += chunk))
|
|
54
|
-
res.on('end', () => resolve(data))
|
|
55
|
-
})
|
|
56
|
-
.on('error', reject)
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Lists the S3 prefix, parses the XML, and determines the latest version tag.
|
|
62
|
-
*/
|
|
63
|
-
async function getLatestVersionFromS3() {
|
|
64
|
-
// Query S3 for folders using delimiter and prefix
|
|
65
|
-
const listUrl = `${S3_BASE_URL}/?delimiter=/&prefix=${S3_PREFIX}`
|
|
66
|
-
const xmlData = await fetchXml(listUrl)
|
|
67
|
-
|
|
68
|
-
// Regex to find all CommonPrefixes entries (the version folders)
|
|
69
|
-
const prefixRegex = /<Prefix>(releases\/v[^<]+)\/<\/Prefix>/g
|
|
70
|
-
const versionFolders = []
|
|
71
|
-
let match
|
|
72
|
-
|
|
73
|
-
while ((match = prefixRegex.exec(xmlData)) !== null) {
|
|
74
|
-
// Extract version part: "releases/v1.0.0/" -> "v1.0.0"
|
|
75
|
-
const fullPrefix = match[1]
|
|
76
|
-
const version = fullPrefix.substring(S3_PREFIX.length)
|
|
77
|
-
versionFolders.push(version)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (versionFolders.length === 0) {
|
|
81
|
-
throw new Error('No version folders found in S3 bucket.')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Sort versions to find the latest (using semantic versioning logic)
|
|
85
|
-
const sortedVersions = versionFolders.sort((a, b) => {
|
|
86
|
-
// Strips 'v', splits by '.', and converts to number arrays for comparison
|
|
87
|
-
const aParts = a.replace('v', '').split('.').map(Number)
|
|
88
|
-
const bParts = b.replace('v', '').split('.').map(Number)
|
|
89
|
-
|
|
90
|
-
// Compare major, minor, and patch numbers
|
|
91
|
-
for (let i = 0; i < 3; i++) {
|
|
92
|
-
if (aParts[i] > bParts[i]) return 1
|
|
93
|
-
if (aParts[i] < bParts[i]) return -1
|
|
94
|
-
}
|
|
95
|
-
return 0
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
// The latest version is the last one after sorting
|
|
99
|
-
return sortedVersions[sortedVersions.length - 1]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function downloadFile(url, destination) {
|
|
103
|
-
return new Promise((resolve, reject) => {
|
|
104
|
-
const file = createWriteStream(destination)
|
|
105
|
-
|
|
106
|
-
https
|
|
107
|
-
.get(url, (response) => {
|
|
108
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
109
|
-
// Handle redirect
|
|
110
|
-
return downloadFile(response.headers.location, destination).then(resolve).catch(reject)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (response.statusCode !== 200) {
|
|
114
|
-
reject(new Error(`Failed to download: ${response.statusCode}`))
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
response.pipe(file)
|
|
119
|
-
|
|
120
|
-
file.on('finish', () => {
|
|
121
|
-
file.close()
|
|
122
|
-
resolve()
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
file.on('error', reject)
|
|
126
|
-
})
|
|
127
|
-
.on('error', reject)
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function extractArchive(archivePath, extractPath, binaryName) {
|
|
132
|
-
// Extract tar.gz
|
|
133
|
-
await tar.x({
|
|
134
|
-
file: archivePath,
|
|
135
|
-
cwd: extractPath
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// Make binary executable
|
|
139
|
-
const binaryPath = path.join(extractPath, binaryName)
|
|
140
|
-
if (fs.existsSync(binaryPath)) {
|
|
141
|
-
fs.chmodSync(binaryPath, '755')
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function installServer() {
|
|
146
|
-
try {
|
|
147
|
-
console.log('📦 Installing nulljs server binary...')
|
|
148
|
-
|
|
149
|
-
const { target, extension, binaryName } = getPlatformInfo()
|
|
150
|
-
const binDir = path.join(__dirname, '..', 'bin')
|
|
151
|
-
const archivePath = path.join(binDir, `server${extension}`)
|
|
152
|
-
const binaryPath = path.join(binDir, 'server')
|
|
153
|
-
|
|
154
|
-
// Create bin directory
|
|
155
|
-
if (!fs.existsSync(binDir)) {
|
|
156
|
-
fs.mkdirSync(binDir, { recursive: true })
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Check if binary already exists (optional optimization)
|
|
160
|
-
if (fs.existsSync(binaryPath)) {
|
|
161
|
-
console.log('✅ Server binary already installed')
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 1. Determine Version from S3
|
|
166
|
-
console.log('🔍 Checking for latest version on S3...')
|
|
167
|
-
const version = await getLatestVersionFromS3()
|
|
168
|
-
console.log(` Found version: ${version}`)
|
|
169
|
-
|
|
170
|
-
// 2. Construct URL
|
|
171
|
-
// URL format: https://.../releases/VERSION/nulljs-server-TARGET.tar.gz
|
|
172
|
-
const downloadUrl = `${DOWNLOAD_BASE_URL}${version}/nulljs-server-${target}${extension}`
|
|
173
|
-
console.log(` Downloading from: ${downloadUrl}`)
|
|
174
|
-
|
|
175
|
-
// 3. Download
|
|
176
|
-
await downloadFile(downloadUrl, archivePath)
|
|
177
|
-
console.log('✅ Download completed')
|
|
178
|
-
|
|
179
|
-
// 4. Extract
|
|
180
|
-
console.log('📂 Extracting binary...')
|
|
181
|
-
await extractArchive(archivePath, binDir, binaryName)
|
|
182
|
-
|
|
183
|
-
// 5. Clean up archive
|
|
184
|
-
fs.unlinkSync(archivePath)
|
|
185
|
-
|
|
186
|
-
console.log('✅ nulljs server installed successfully')
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.error('❌ Failed to install server binary:', error.message)
|
|
189
|
-
console.error('⚠️ You may need to build the server manually')
|
|
190
|
-
process.exit(0)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Only run if called directly
|
|
195
|
-
if (require.main === module) {
|
|
196
|
-
installServer()
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
module.exports = { installServer }
|
package/src/commands/api.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
|
|
3
|
-
import { saveApiUrl } from '../lib/config'
|
|
4
|
-
|
|
5
|
-
const setApiUrl = (url: string, profileName?: string) => {
|
|
6
|
-
try {
|
|
7
|
-
saveApiUrl(url, profileName)
|
|
8
|
-
const profileText = profileName ? ` to profile '${profileName}'` : ''
|
|
9
|
-
console.log(chalk.green('✓'), `API URL saved${profileText}: ${chalk.blue(url)}`)
|
|
10
|
-
} catch (error) {
|
|
11
|
-
console.error(chalk.red('✗'), 'Failed to save API URL:', error)
|
|
12
|
-
process.exit(1)
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export { setApiUrl }
|
package/src/commands/auth.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import readline from 'node:readline'
|
|
3
|
-
|
|
4
|
-
import { loadConfigWithProfile, saveKeys } from '../lib/config'
|
|
5
|
-
|
|
6
|
-
const askQuestion = (query: string): Promise<string> => {
|
|
7
|
-
const rl = readline.createInterface({
|
|
8
|
-
input: process.stdin,
|
|
9
|
-
output: process.stdout
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
return new Promise((resolve) =>
|
|
13
|
-
rl.question(query, (answer) => {
|
|
14
|
-
rl.close()
|
|
15
|
-
resolve(answer.trim())
|
|
16
|
-
})
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const auth = async (profileName?: string) => {
|
|
21
|
-
const config = loadConfigWithProfile()
|
|
22
|
-
|
|
23
|
-
if (config.key) {
|
|
24
|
-
const answer = await askQuestion(
|
|
25
|
-
'Keys already exist. Are you sure you want to regenerate them? (Y/n): '
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
if (answer.toLowerCase() === 'n') {
|
|
29
|
-
console.log(chalk.green('Public Key:'), chalk.blue(config.key.public))
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const keyPair = await crypto.subtle.generateKey(
|
|
35
|
-
{
|
|
36
|
-
name: 'Ed25519',
|
|
37
|
-
namedCurve: 'Ed25519'
|
|
38
|
-
},
|
|
39
|
-
true,
|
|
40
|
-
['sign', 'verify']
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
|
44
|
-
const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey)
|
|
45
|
-
|
|
46
|
-
const privateKey = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)))
|
|
47
|
-
const publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)))
|
|
48
|
-
|
|
49
|
-
saveKeys(privateKey, publicKey, profileName)
|
|
50
|
-
|
|
51
|
-
console.log(chalk.green('Public Key:'), chalk.blue(publicKey))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export { auth }
|
package/src/commands/create.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import degit from 'degit'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
|
|
6
|
-
const repo = 'tothalex/nulljs-template'
|
|
7
|
-
|
|
8
|
-
const modifyName = async (newName: string, packagePath: string) => {
|
|
9
|
-
const file = Bun.file(packagePath)
|
|
10
|
-
const packageContent = await file.text()
|
|
11
|
-
const packageJson = JSON.parse(packageContent)
|
|
12
|
-
|
|
13
|
-
packageJson.name = newName
|
|
14
|
-
|
|
15
|
-
await Bun.write(packagePath, JSON.stringify(packageJson, null, 2) + '\n')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const create = async (name: string) => {
|
|
19
|
-
if (existsSync(name)) {
|
|
20
|
-
console.log(chalk.red('Folder already exists: ') + chalk.bgRed(name))
|
|
21
|
-
process.exit(0)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const targetDir = path.join(process.cwd(), name)
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const emitter = degit(repo, {
|
|
28
|
-
cache: false,
|
|
29
|
-
force: true,
|
|
30
|
-
verbose: true
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
await emitter.clone(targetDir)
|
|
34
|
-
|
|
35
|
-
await modifyName(name, targetDir + '/package.json')
|
|
36
|
-
console.log(chalk.green('Project setup completed successfully!'))
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error(chalk.red('An error occurred during project creation:'))
|
|
39
|
-
console.error(error)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export { create }
|