@tothalex/nulljs 0.0.53 → 0.0.54
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 +8 -5
- package/src/cli.ts +0 -24
- package/src/commands/config.ts +0 -130
- package/src/commands/deploy.ts +0 -219
- package/src/commands/dev.ts +0 -10
- package/src/commands/host.ts +0 -330
- package/src/commands/index.ts +0 -6
- package/src/commands/secret.ts +0 -387
- package/src/commands/status.ts +0 -41
- package/src/components/DeployAnimation.tsx +0 -92
- package/src/components/DeploymentLogsPane.tsx +0 -79
- package/src/components/Header.tsx +0 -57
- package/src/components/HelpModal.tsx +0 -64
- package/src/components/SystemLogsPane.tsx +0 -78
- package/src/config/index.ts +0 -181
- package/src/lib/bundle/external.ts +0 -23
- package/src/lib/bundle/function.ts +0 -125
- package/src/lib/bundle/index.ts +0 -5
- package/src/lib/bundle/react.ts +0 -149
- package/src/lib/bundle/types.ts +0 -4
- package/src/lib/deploy.ts +0 -103
- package/src/lib/server.ts +0 -160
- package/src/lib/vite.ts +0 -120
- package/src/lib/watcher.ts +0 -274
- package/src/ui.tsx +0 -363
- package/tsconfig.json +0 -30
package/src/commands/secret.ts
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander'
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import { resolve } from 'path'
|
|
4
|
-
import { readFile, readdir } from 'fs/promises'
|
|
5
|
-
import { existsSync } from 'fs'
|
|
6
|
-
import * as p from '@clack/prompts'
|
|
7
|
-
|
|
8
|
-
import { listSecrets, createSecret } from '@nulljs/api'
|
|
9
|
-
import { loadPrivateKey, listConfigs, getConfig, type Config } from '../config'
|
|
10
|
-
|
|
11
|
-
const selectConfig = async (): Promise<Config | null> => {
|
|
12
|
-
const configList = listConfigs()
|
|
13
|
-
|
|
14
|
-
if (!configList || configList.configs.length === 0) {
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (configList.configs.length === 1) {
|
|
19
|
-
return configList.configs[0] ?? null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const selected = await p.select({
|
|
23
|
-
message: 'Select config',
|
|
24
|
-
options: configList.configs.map((c) => ({
|
|
25
|
-
value: c.name,
|
|
26
|
-
label: c.name,
|
|
27
|
-
hint: c.name === configList.current ? 'current' : c.api
|
|
28
|
-
})),
|
|
29
|
-
initialValue: configList.current
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
if (p.isCancel(selected)) {
|
|
33
|
-
p.cancel('Cancelled')
|
|
34
|
-
process.exit(0)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return getConfig(selected as string)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const selectFile = async (cwd: string): Promise<string | null> => {
|
|
41
|
-
const files: string[] = []
|
|
42
|
-
|
|
43
|
-
// Find .secret files in current directory
|
|
44
|
-
try {
|
|
45
|
-
const items = await readdir(cwd)
|
|
46
|
-
for (const item of items) {
|
|
47
|
-
if (item === '.secret' || item.startsWith('.secret.')) {
|
|
48
|
-
files.push(item)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
// Ignore errors
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (files.length === 0) {
|
|
56
|
-
console.log(chalk.yellow('No .secret files found in current directory'))
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const selected = await p.select({
|
|
61
|
-
message: 'Select file to import secrets from',
|
|
62
|
-
options: files.map((f) => ({
|
|
63
|
-
value: f,
|
|
64
|
-
label: f
|
|
65
|
-
}))
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
if (p.isCancel(selected)) {
|
|
69
|
-
p.cancel('Cancelled')
|
|
70
|
-
process.exit(0)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return resolve(cwd, selected as string)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const parseEnvFile = async (filePath: string): Promise<Array<{ key: string; value: string }>> => {
|
|
77
|
-
const content = await readFile(filePath, 'utf-8')
|
|
78
|
-
const lines = content.split('\n')
|
|
79
|
-
const secrets: Array<{ key: string; value: string }> = []
|
|
80
|
-
|
|
81
|
-
for (const line of lines) {
|
|
82
|
-
const trimmed = line.trim()
|
|
83
|
-
|
|
84
|
-
// Skip empty lines and comments
|
|
85
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const equalIndex = trimmed.indexOf('=')
|
|
90
|
-
if (equalIndex === -1) {
|
|
91
|
-
continue
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const key = trimmed.substring(0, equalIndex).trim()
|
|
95
|
-
let value = trimmed.substring(equalIndex + 1).trim()
|
|
96
|
-
|
|
97
|
-
// Remove quotes if present
|
|
98
|
-
if (
|
|
99
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
100
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
101
|
-
) {
|
|
102
|
-
value = value.slice(1, -1)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (key) {
|
|
106
|
-
secrets.push({ key, value })
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return secrets
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const registerSecretCommand = (program: Command) => {
|
|
114
|
-
program
|
|
115
|
-
.command('secret')
|
|
116
|
-
.description('Secret management')
|
|
117
|
-
.addCommand(
|
|
118
|
-
new Command('list')
|
|
119
|
-
.description('List all secret keys')
|
|
120
|
-
.option('-e, --env <name>', 'Use a specific config environment')
|
|
121
|
-
.action(async (options: { env?: string }) => {
|
|
122
|
-
let config: Awaited<ReturnType<typeof selectConfig>>
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
config = options.env ? getConfig(options.env) : await selectConfig()
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.error(chalk.red('✗ Failed to get config:'), err)
|
|
128
|
-
process.exit(1)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Allow event loop to clear after p.select
|
|
132
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
133
|
-
|
|
134
|
-
if (!config) {
|
|
135
|
-
console.error(chalk.red('✗ No configuration found.'))
|
|
136
|
-
console.error(chalk.gray(' Run "nulljs dev" first to initialize the project'))
|
|
137
|
-
process.exit(1)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
console.log(chalk.gray(`Fetching secrets from ${config.api}...`))
|
|
142
|
-
|
|
143
|
-
const privateKey = await loadPrivateKey(config)
|
|
144
|
-
const keys = await listSecrets({ privateKey, url: config.api })
|
|
145
|
-
|
|
146
|
-
if (keys.length === 0) {
|
|
147
|
-
console.log(chalk.yellow('No secrets found'))
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log(chalk.bold('\nSecret keys:'))
|
|
152
|
-
keys.forEach((key, index) => {
|
|
153
|
-
console.log(chalk.green(` ${index + 1}.`) + ` ${chalk.cyan(key)}`)
|
|
154
|
-
})
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error(chalk.red('✗ Failed to list secrets:'), error instanceof Error ? error.message : String(error))
|
|
157
|
-
process.exit(1)
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
)
|
|
161
|
-
.addCommand(
|
|
162
|
-
new Command('create')
|
|
163
|
-
.description('Create a new secret')
|
|
164
|
-
.argument('[key]', 'Secret key')
|
|
165
|
-
.argument('[value]', 'Secret value')
|
|
166
|
-
.option('-e, --env <name>', 'Use a specific config environment')
|
|
167
|
-
.action(async (key?: string, value?: string, options?: { env?: string }) => {
|
|
168
|
-
const config = options?.env ? getConfig(options.env) : await selectConfig()
|
|
169
|
-
|
|
170
|
-
if (!config) {
|
|
171
|
-
console.error(chalk.red('✗ No configuration found.'))
|
|
172
|
-
console.error(chalk.gray(' Run "nulljs dev" first to initialize the project'))
|
|
173
|
-
process.exit(1)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let secretKey = key
|
|
177
|
-
let secretValue = value
|
|
178
|
-
|
|
179
|
-
// Interactive mode
|
|
180
|
-
if (!secretKey || !secretValue) {
|
|
181
|
-
if (!secretKey) {
|
|
182
|
-
const keyInput = await p.text({
|
|
183
|
-
message: 'Secret key',
|
|
184
|
-
placeholder: 'e.g., API_KEY, DATABASE_URL',
|
|
185
|
-
validate: (v) => {
|
|
186
|
-
if (!v.trim()) return 'Key is required'
|
|
187
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(v)) {
|
|
188
|
-
return 'Key must start with a letter or underscore and contain only letters, numbers, and underscores'
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
if (p.isCancel(keyInput)) {
|
|
194
|
-
p.cancel('Cancelled')
|
|
195
|
-
process.exit(0)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
secretKey = keyInput as string
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!secretValue) {
|
|
202
|
-
const valueInput = await p.password({
|
|
203
|
-
message: 'Secret value',
|
|
204
|
-
validate: (v) => {
|
|
205
|
-
if (!v) return 'Value is required'
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
if (p.isCancel(valueInput)) {
|
|
210
|
-
p.cancel('Cancelled')
|
|
211
|
-
process.exit(0)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
secretValue = valueInput as string
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Allow event loop to clear after prompts
|
|
219
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const privateKey = await loadPrivateKey(config)
|
|
223
|
-
await createSecret(
|
|
224
|
-
{ key: secretKey, value: secretValue },
|
|
225
|
-
{ privateKey, url: config.api }
|
|
226
|
-
)
|
|
227
|
-
console.log(chalk.green('✓ Secret created:') + ` ${chalk.cyan(secretKey)}`)
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error(
|
|
230
|
-
chalk.red('✗ Failed to create secret:'),
|
|
231
|
-
error instanceof Error ? error.message : error
|
|
232
|
-
)
|
|
233
|
-
process.exit(1)
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
)
|
|
237
|
-
.addCommand(
|
|
238
|
-
new Command('deploy')
|
|
239
|
-
.description('Deploy secrets from a .secret file')
|
|
240
|
-
.argument('[file]', 'Path to .secret file')
|
|
241
|
-
.option('-e, --env <name>', 'Use a specific config environment')
|
|
242
|
-
.action(async (file?: string, options?: { env?: string }) => {
|
|
243
|
-
const config = options?.env ? getConfig(options.env) : await selectConfig()
|
|
244
|
-
|
|
245
|
-
if (!config) {
|
|
246
|
-
console.error(chalk.red('✗ No configuration found.'))
|
|
247
|
-
console.error(chalk.gray(' Run "nulljs dev" first to initialize the project'))
|
|
248
|
-
process.exit(1)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
let filePath = file ? resolve(file) : null
|
|
252
|
-
|
|
253
|
-
// Interactive file picker if no file provided
|
|
254
|
-
if (!filePath) {
|
|
255
|
-
filePath = await selectFile(process.cwd())
|
|
256
|
-
if (!filePath) {
|
|
257
|
-
process.exit(1)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (!existsSync(filePath)) {
|
|
262
|
-
console.error(chalk.red(`✗ File not found: ${filePath}`))
|
|
263
|
-
process.exit(1)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Allow event loop to clear after prompts
|
|
267
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const secrets = await parseEnvFile(filePath)
|
|
271
|
-
|
|
272
|
-
if (secrets.length === 0) {
|
|
273
|
-
console.log(chalk.yellow('No secrets found in file'))
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const privateKey = await loadPrivateKey(config)
|
|
278
|
-
let created = 0
|
|
279
|
-
let failed = 0
|
|
280
|
-
|
|
281
|
-
console.log(chalk.cyan(`Deploying ${secrets.length} secret(s) to ${config.name}...\n`))
|
|
282
|
-
|
|
283
|
-
for (const secret of secrets) {
|
|
284
|
-
try {
|
|
285
|
-
await createSecret(secret, { privateKey, url: config.api })
|
|
286
|
-
console.log(chalk.green('✓') + ` ${secret.key}`)
|
|
287
|
-
created++
|
|
288
|
-
} catch (error) {
|
|
289
|
-
console.log(
|
|
290
|
-
chalk.red('✗') +
|
|
291
|
-
` ${secret.key}: ${error instanceof Error ? error.message : error}`
|
|
292
|
-
)
|
|
293
|
-
failed++
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
console.log('')
|
|
298
|
-
if (failed > 0) {
|
|
299
|
-
console.log(chalk.yellow(`Deploy completed: ${created} created, ${failed} failed`))
|
|
300
|
-
} else {
|
|
301
|
-
console.log(chalk.green(`Deploy completed: ${created} secret(s) created`))
|
|
302
|
-
}
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.error(
|
|
305
|
-
chalk.red('✗ Failed to deploy secrets:'),
|
|
306
|
-
error instanceof Error ? error.message : error
|
|
307
|
-
)
|
|
308
|
-
process.exit(1)
|
|
309
|
-
}
|
|
310
|
-
})
|
|
311
|
-
)
|
|
312
|
-
.addCommand(
|
|
313
|
-
new Command('import')
|
|
314
|
-
.description('Import secrets from a file (alias for deploy)')
|
|
315
|
-
.argument('[file]', 'Path to .secret file')
|
|
316
|
-
.option('-e, --env <name>', 'Use a specific config environment')
|
|
317
|
-
.action(async (file?: string, options?: { env?: string }) => {
|
|
318
|
-
const config = options?.env ? getConfig(options.env) : await selectConfig()
|
|
319
|
-
|
|
320
|
-
if (!config) {
|
|
321
|
-
console.error(chalk.red('✗ No configuration found.'))
|
|
322
|
-
console.error(chalk.gray(' Run "nulljs dev" first to initialize the project'))
|
|
323
|
-
process.exit(1)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
let filePath = file ? resolve(file) : null
|
|
327
|
-
|
|
328
|
-
// Interactive file picker if no file provided
|
|
329
|
-
if (!filePath) {
|
|
330
|
-
filePath = await selectFile(process.cwd())
|
|
331
|
-
if (!filePath) {
|
|
332
|
-
process.exit(1)
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (!existsSync(filePath)) {
|
|
337
|
-
console.error(chalk.red(`✗ File not found: ${filePath}`))
|
|
338
|
-
process.exit(1)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Allow event loop to clear after prompts
|
|
342
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
const secrets = await parseEnvFile(filePath)
|
|
346
|
-
|
|
347
|
-
if (secrets.length === 0) {
|
|
348
|
-
console.log(chalk.yellow('No secrets found in file'))
|
|
349
|
-
return
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const privateKey = await loadPrivateKey(config)
|
|
353
|
-
let created = 0
|
|
354
|
-
let failed = 0
|
|
355
|
-
|
|
356
|
-
console.log(chalk.cyan(`Importing ${secrets.length} secret(s) to ${config.name}...\n`))
|
|
357
|
-
|
|
358
|
-
for (const secret of secrets) {
|
|
359
|
-
try {
|
|
360
|
-
await createSecret(secret, { privateKey, url: config.api })
|
|
361
|
-
console.log(chalk.green('✓') + ` ${secret.key}`)
|
|
362
|
-
created++
|
|
363
|
-
} catch (error) {
|
|
364
|
-
console.log(
|
|
365
|
-
chalk.red('✗') +
|
|
366
|
-
` ${secret.key}: ${error instanceof Error ? error.message : error}`
|
|
367
|
-
)
|
|
368
|
-
failed++
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
console.log('')
|
|
373
|
-
if (failed > 0) {
|
|
374
|
-
console.log(chalk.yellow(`Import completed: ${created} created, ${failed} failed`))
|
|
375
|
-
} else {
|
|
376
|
-
console.log(chalk.green(`Import completed: ${created} secret(s) created`))
|
|
377
|
-
}
|
|
378
|
-
} catch (error) {
|
|
379
|
-
console.error(
|
|
380
|
-
chalk.red('✗ Failed to import secrets:'),
|
|
381
|
-
error instanceof Error ? error.message : error
|
|
382
|
-
)
|
|
383
|
-
process.exit(1)
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
)
|
|
387
|
-
}
|
package/src/commands/status.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander'
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
|
|
4
|
-
import { listConfigs } from '../config'
|
|
5
|
-
|
|
6
|
-
export const registerStatusCommand = (program: Command) => {
|
|
7
|
-
program.action(() => {
|
|
8
|
-
const result = listConfigs()
|
|
9
|
-
|
|
10
|
-
if (result && result.configs.length > 0) {
|
|
11
|
-
const currentConfig = result.configs.find((c) => c.name === result.current)
|
|
12
|
-
if (currentConfig) {
|
|
13
|
-
console.log(chalk.green('●') + ' ' + chalk.bold(`Active: ${currentConfig.name}`))
|
|
14
|
-
console.log(chalk.blue(' API:') + ` ${currentConfig.api}`)
|
|
15
|
-
console.log(chalk.blue(' Public Key:') + ` ${currentConfig.key.public.substring(0, 20)}...`)
|
|
16
|
-
|
|
17
|
-
if (result.configs.length > 1) {
|
|
18
|
-
console.log(chalk.gray(`\n ${result.configs.length - 1} other config(s) available`))
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
} else {
|
|
22
|
-
console.log(chalk.yellow('○ No local configuration found.'))
|
|
23
|
-
console.log(chalk.gray(' Run "nulljs dev" to initialize the project'))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
printAvailableCommands()
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const printAvailableCommands = () => {
|
|
31
|
-
console.log('\n' + chalk.bold('Commands:'))
|
|
32
|
-
console.log(chalk.cyan(' nulljs dev') + chalk.gray(' - Launch development server'))
|
|
33
|
-
console.log(chalk.cyan(' nulljs deploy') + chalk.gray(' - Deploy all functions and React apps'))
|
|
34
|
-
console.log(chalk.cyan(' nulljs config list') + chalk.gray(' - List all configurations'))
|
|
35
|
-
console.log(chalk.cyan(' nulljs config new') + chalk.gray(' - Create new configuration'))
|
|
36
|
-
console.log(chalk.cyan(' nulljs config use') + chalk.gray(' - Switch active configuration'))
|
|
37
|
-
console.log(chalk.cyan(' nulljs secret list') + chalk.gray(' - List all secret keys'))
|
|
38
|
-
console.log(chalk.cyan(' nulljs secret create') + chalk.gray(' - Create a new secret'))
|
|
39
|
-
console.log(chalk.cyan(' nulljs secret deploy') + chalk.gray(' - Deploy secrets from .secret file'))
|
|
40
|
-
console.log(chalk.cyan(' nulljs host') + chalk.gray(' - Set up production hosting with systemd'))
|
|
41
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from 'react'
|
|
2
|
-
|
|
3
|
-
export type DeployedFunction = {
|
|
4
|
-
name: string
|
|
5
|
-
success: boolean
|
|
6
|
-
error?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const DeployAnimation = (props: {
|
|
10
|
-
deployed: DeployedFunction[]
|
|
11
|
-
onComplete: () => void
|
|
12
|
-
}) => {
|
|
13
|
-
const [currentIndex, setCurrentIndex] = useState(0)
|
|
14
|
-
const [progress, setProgress] = useState(0)
|
|
15
|
-
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
16
|
-
const completedRef = useRef(false)
|
|
17
|
-
|
|
18
|
-
const duration = 400 // ms per item
|
|
19
|
-
const frameRate = 16 // ~60fps
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (props.deployed.length === 0) {
|
|
23
|
-
props.onComplete()
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (completedRef.current) {
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Reset progress for new item
|
|
32
|
-
setProgress(0)
|
|
33
|
-
const startTime = Date.now()
|
|
34
|
-
|
|
35
|
-
intervalRef.current = setInterval(() => {
|
|
36
|
-
const elapsed = Date.now() - startTime
|
|
37
|
-
const newProgress = Math.min(100, (elapsed / duration) * 100)
|
|
38
|
-
setProgress(newProgress)
|
|
39
|
-
|
|
40
|
-
if (newProgress >= 100) {
|
|
41
|
-
if (intervalRef.current) {
|
|
42
|
-
clearInterval(intervalRef.current)
|
|
43
|
-
intervalRef.current = null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Move to next or complete
|
|
47
|
-
setTimeout(() => {
|
|
48
|
-
if (currentIndex >= props.deployed.length - 1) {
|
|
49
|
-
completedRef.current = true
|
|
50
|
-
props.onComplete()
|
|
51
|
-
} else {
|
|
52
|
-
setCurrentIndex((prev) => prev + 1)
|
|
53
|
-
}
|
|
54
|
-
}, 150)
|
|
55
|
-
}
|
|
56
|
-
}, frameRate)
|
|
57
|
-
|
|
58
|
-
return () => {
|
|
59
|
-
if (intervalRef.current) {
|
|
60
|
-
clearInterval(intervalRef.current)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}, [currentIndex, props.deployed.length, props.onComplete])
|
|
64
|
-
|
|
65
|
-
const current = props.deployed[currentIndex]
|
|
66
|
-
|
|
67
|
-
if (props.deployed.length === 0 || !current || completedRef.current) {
|
|
68
|
-
return null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const barWidth = 8
|
|
72
|
-
const filled = Math.round((progress / 100) * barWidth)
|
|
73
|
-
const empty = barWidth - filled
|
|
74
|
-
const bar = '━'.repeat(filled) + '─'.repeat(empty)
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<box backgroundColor="black" flexDirection="row" paddingLeft={1} paddingRight={1}>
|
|
78
|
-
<text>
|
|
79
|
-
<span fg={current.success ? '#22c55e' : '#ef4444'}>
|
|
80
|
-
{bar} {current.success ? '✓' : '✗'}
|
|
81
|
-
</span>
|
|
82
|
-
<span> {current.name}</span>
|
|
83
|
-
{props.deployed.length > 1 && (
|
|
84
|
-
<span fg="#666">
|
|
85
|
-
{' '}
|
|
86
|
-
({currentIndex + 1}/{props.deployed.length})
|
|
87
|
-
</span>
|
|
88
|
-
)}
|
|
89
|
-
</text>
|
|
90
|
-
</box>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import type { LogEntry } from '@nulljs/api'
|
|
2
|
-
|
|
3
|
-
interface DeploymentLogsPaneProps {
|
|
4
|
-
logs: LogEntry[]
|
|
5
|
-
loading: boolean
|
|
6
|
-
autoScroll: boolean
|
|
7
|
-
jumpTrigger: number
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const getLevelColor = (level: string): string => {
|
|
11
|
-
switch (level.toLowerCase()) {
|
|
12
|
-
case 'error':
|
|
13
|
-
return 'red'
|
|
14
|
-
case 'warn':
|
|
15
|
-
case 'warning':
|
|
16
|
-
return 'yellow'
|
|
17
|
-
case 'info':
|
|
18
|
-
return 'cyan'
|
|
19
|
-
case 'debug':
|
|
20
|
-
return 'gray'
|
|
21
|
-
default:
|
|
22
|
-
return 'white'
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const sanitizeMessage = (message: string): string => {
|
|
27
|
-
return message
|
|
28
|
-
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
29
|
-
.replace(/[\r\n]+/g, ' ')
|
|
30
|
-
.replace(/\s+/g, ' ')
|
|
31
|
-
.trim()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const DeploymentLogsPane = (props: DeploymentLogsPaneProps) => {
|
|
35
|
-
if (props.loading && props.logs.length === 0) {
|
|
36
|
-
return (
|
|
37
|
-
<box flexDirection="column" padding={1}>
|
|
38
|
-
<text>Loading deployment logs...</text>
|
|
39
|
-
</box>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!props.loading && props.logs.length === 0) {
|
|
44
|
-
return (
|
|
45
|
-
<box flexDirection="column" padding={1}>
|
|
46
|
-
<text>No deployment logs found</text>
|
|
47
|
-
</box>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Reverse logs so oldest are at top, newest at bottom
|
|
52
|
-
const logsInOrder = [...props.logs].reverse()
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<scrollbox
|
|
56
|
-
focused
|
|
57
|
-
stickyStart={props.autoScroll ? 'bottom' : 'top'}
|
|
58
|
-
stickyScroll={props.autoScroll}
|
|
59
|
-
key={`${props.autoScroll}-${props.jumpTrigger}`}>
|
|
60
|
-
{logsInOrder.map((log) => {
|
|
61
|
-
const time = new Date(log.time).toLocaleTimeString()
|
|
62
|
-
const timestamp = `[${time}]`
|
|
63
|
-
const level = log.level.toUpperCase()
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<box key={log.id} flexDirection="row">
|
|
67
|
-
<text>
|
|
68
|
-
<span fg="gray">{timestamp}</span>
|
|
69
|
-
<span fg={getLevelColor(log.level)}>{` ${level} `}</span>
|
|
70
|
-
<span fg="blue">{`(${log.deployment_name})`}</span>
|
|
71
|
-
<span fg="cyan">{` ${log.trigger} `}</span>
|
|
72
|
-
<span>{sanitizeMessage(log.message)}</span>
|
|
73
|
-
</text>
|
|
74
|
-
</box>
|
|
75
|
-
)
|
|
76
|
-
})}
|
|
77
|
-
</scrollbox>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { BinarySource } from '../lib/server'
|
|
2
|
-
|
|
3
|
-
type Tab = 'system' | 'deployment'
|
|
4
|
-
|
|
5
|
-
type HeaderProps = {
|
|
6
|
-
activeTab: Tab
|
|
7
|
-
functionCount: number
|
|
8
|
-
viteRunning: boolean
|
|
9
|
-
isDeploying: boolean
|
|
10
|
-
binarySource: BinarySource
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const getBinarySourceLabel = (source: BinarySource): string => {
|
|
14
|
-
switch (source) {
|
|
15
|
-
case 'local-debug':
|
|
16
|
-
return 'local (debug)'
|
|
17
|
-
case 'local-release':
|
|
18
|
-
return 'local (release)'
|
|
19
|
-
case 'local-env':
|
|
20
|
-
return 'local (env)'
|
|
21
|
-
case 'npm':
|
|
22
|
-
return 'npm'
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const Header = (props: HeaderProps) => {
|
|
27
|
-
const { activeTab, functionCount, viteRunning, isDeploying, binarySource } = props
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<box flexDirection="row" justifyContent="space-between">
|
|
31
|
-
<box flexDirection="row">
|
|
32
|
-
<text fg={activeTab === 'system' ? 'cyan' : undefined} content="[1] System Logs" />
|
|
33
|
-
<text content=" | " />
|
|
34
|
-
<text fg={activeTab === 'deployment' ? 'cyan' : undefined} content="[2] Deployment Logs" />
|
|
35
|
-
</box>
|
|
36
|
-
<box flexDirection="row">
|
|
37
|
-
{isDeploying ? (
|
|
38
|
-
<text fg="yellow" content="● deploying all..." />
|
|
39
|
-
) : (
|
|
40
|
-
<text fg={functionCount > 0 ? 'green' : 'yellow'} content="● watching" />
|
|
41
|
-
)}
|
|
42
|
-
<text fg="gray" content={` (${functionCount} functions)`} />
|
|
43
|
-
{viteRunning && (
|
|
44
|
-
<>
|
|
45
|
-
<text fg="gray" content=" | vite: " />
|
|
46
|
-
<text fg="magenta" content=":5173" />
|
|
47
|
-
</>
|
|
48
|
-
)}
|
|
49
|
-
<text fg="gray" content=" | server: " />
|
|
50
|
-
<text
|
|
51
|
-
fg={binarySource.startsWith('local') ? 'yellow' : 'green'}
|
|
52
|
-
content={getBinarySourceLabel(binarySource)}
|
|
53
|
-
/>
|
|
54
|
-
</box>
|
|
55
|
-
</box>
|
|
56
|
-
)
|
|
57
|
-
}
|