@muyichengshayu/promptx 0.1.0

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.
@@ -0,0 +1,121 @@
1
+ export const EXPIRY_PRESETS = {
2
+ none: {
3
+ label: '不过期',
4
+ hours: null,
5
+ },
6
+ }
7
+
8
+ export const EXPIRY_OPTIONS = Object.entries(EXPIRY_PRESETS).map(([value, preset]) => ({
9
+ value,
10
+ label: preset.label,
11
+ }))
12
+
13
+ export const VISIBILITY_OPTIONS = [
14
+ { value: 'private', label: '仅自己可见' },
15
+ ]
16
+
17
+ export const BLOCK_TYPES = {
18
+ TEXT: 'text',
19
+ IMAGE: 'image',
20
+ IMPORTED_TEXT: 'imported_text',
21
+ }
22
+
23
+ export const BLOCK_TYPE_LABELS = {
24
+ [BLOCK_TYPES.TEXT]: '文本',
25
+ [BLOCK_TYPES.IMAGE]: '图片',
26
+ [BLOCK_TYPES.IMPORTED_TEXT]: '导入文件',
27
+ }
28
+
29
+ export function normalizeVisibility(value) {
30
+ return 'private'
31
+ }
32
+
33
+ export function normalizeExpiry(value) {
34
+ return 'none'
35
+ }
36
+
37
+ export function getVisibilityLabel(value) {
38
+ return VISIBILITY_OPTIONS.find((item) => item.value === value)?.label || '仅自己可见'
39
+ }
40
+
41
+ export function getBlockTypeLabel(value) {
42
+ return BLOCK_TYPE_LABELS[value] || '内容'
43
+ }
44
+
45
+ export function resolveExpiresAt(expiry = 'none', now = new Date()) {
46
+ const preset = EXPIRY_PRESETS[normalizeExpiry(expiry)]
47
+ if (!preset || preset.hours === null) {
48
+ return null
49
+ }
50
+
51
+ return new Date(now.getTime() + preset.hours * 60 * 60 * 1000).toISOString()
52
+ }
53
+
54
+ export function getExpiryValue(expiresAt, now = new Date()) {
55
+ if (!expiresAt) {
56
+ return 'none'
57
+ }
58
+
59
+ const diffMs = new Date(expiresAt).getTime() - now.getTime()
60
+ if (diffMs <= 24 * 60 * 60 * 1000 + 60 * 1000) {
61
+ return 'none'
62
+ }
63
+ return 'none'
64
+ }
65
+
66
+ export function clampText(value = '', max = 20000) {
67
+ return String(value).slice(0, max)
68
+ }
69
+
70
+ export function slugifyTitle(title = '') {
71
+ const base = String(title)
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, '-')
74
+ .replace(/^-+|-+$/g, '')
75
+ .slice(0, 36)
76
+
77
+ return base || 'task'
78
+ }
79
+
80
+ export function deriveTitleFromBlocks(blocks = [], max = 10) {
81
+ const firstText = blocks.find(
82
+ (block) =>
83
+ (block.type === BLOCK_TYPES.TEXT || block.type === BLOCK_TYPES.IMPORTED_TEXT) &&
84
+ block.content?.trim()
85
+ )
86
+ if (!firstText) {
87
+ return ''
88
+ }
89
+
90
+ return firstText.content.replace(/\s+/g, ' ').trim().slice(0, max)
91
+ }
92
+
93
+ export function buildRawTaskText(task) {
94
+ const parts = []
95
+ if (task.title) {
96
+ parts.push(`标题:${task.title}`, '')
97
+ }
98
+
99
+ for (const [index, block] of (task.blocks || []).entries()) {
100
+ if (block.type === BLOCK_TYPES.TEXT || block.type === BLOCK_TYPES.IMPORTED_TEXT) {
101
+ parts.push(block.content?.trim() || '', '')
102
+ continue
103
+ }
104
+
105
+ if (block.type === BLOCK_TYPES.IMAGE) {
106
+ parts.push(`图片 ${index + 1}:${block.content || ''}`)
107
+ parts.push('')
108
+ }
109
+ }
110
+
111
+ return parts.join('\n').trim() + '\n'
112
+ }
113
+
114
+ export function summarizeTask(task) {
115
+ const textBlock = (task.blocks || []).find(
116
+ (block) =>
117
+ (block.type === BLOCK_TYPES.TEXT || block.type === BLOCK_TYPES.IMPORTED_TEXT) &&
118
+ block.content?.trim()
119
+ )
120
+ return (textBlock?.content || '').replace(/\s+/g, ' ').trim().slice(0, 180)
121
+ }
@@ -0,0 +1,251 @@
1
+ import fs from 'node:fs'
2
+ import net from 'node:net'
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+ import { execFileSync } from 'node:child_process'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import { resolvePromptxPaths } from '../apps/server/src/appPaths.js'
9
+
10
+ const DEFAULT_HOST = '127.0.0.1'
11
+ const DEFAULT_PORT = 3000
12
+ const SUPPORTED_NODE_RANGES = [
13
+ { min: [20, 19, 0], maxExclusiveMajor: 21, label: '20.19+' },
14
+ { min: [22, 13, 0], maxExclusiveMajor: 23, label: '22.13+' },
15
+ { min: [24, 0, 0], maxExclusiveMajor: 25, label: '24.x' },
16
+ ]
17
+ const RECOMMENDED_NODE_MAJOR = 22
18
+ const CODEX_BIN = process.env.CODEX_BIN || 'codex'
19
+
20
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
21
+ const webIndexPath = path.join(rootDir, 'apps', 'web', 'dist', 'index.html')
22
+
23
+ function compareVersions(left = [], right = []) {
24
+ const maxLength = Math.max(left.length, right.length)
25
+ for (let index = 0; index < maxLength; index += 1) {
26
+ const leftValue = Number(left[index] || 0)
27
+ const rightValue = Number(right[index] || 0)
28
+ if (leftValue > rightValue) {
29
+ return 1
30
+ }
31
+ if (leftValue < rightValue) {
32
+ return -1
33
+ }
34
+ }
35
+ return 0
36
+ }
37
+
38
+ function parseNodeVersion(value = '') {
39
+ return String(value || '')
40
+ .replace(/^v/, '')
41
+ .split('.')
42
+ .map((part) => Number(part) || 0)
43
+ }
44
+
45
+ function resolveCodexBinary() {
46
+ if (process.platform !== 'win32') {
47
+ return CODEX_BIN
48
+ }
49
+
50
+ if (path.extname(CODEX_BIN)) {
51
+ return CODEX_BIN
52
+ }
53
+
54
+ if (fs.existsSync(`${CODEX_BIN}.cmd`)) {
55
+ return `${CODEX_BIN}.cmd`
56
+ }
57
+
58
+ if (fs.existsSync(`${CODEX_BIN}.bat`)) {
59
+ return `${CODEX_BIN}.bat`
60
+ }
61
+
62
+ if (fs.existsSync(CODEX_BIN)) {
63
+ return CODEX_BIN
64
+ }
65
+
66
+ try {
67
+ const output = execFileSync('where.exe', [CODEX_BIN], {
68
+ encoding: 'utf8',
69
+ stdio: ['ignore', 'pipe', 'ignore'],
70
+ }).trim()
71
+
72
+ if (!output) {
73
+ return CODEX_BIN
74
+ }
75
+
76
+ const candidates = output
77
+ .split(/\r?\n/g)
78
+ .map((line) => line.trim())
79
+ .filter(Boolean)
80
+
81
+ return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
82
+ || candidates.find((item) => /\.(exe|com)$/i.test(item))
83
+ || candidates[0]
84
+ || CODEX_BIN
85
+ } catch {
86
+ return CODEX_BIN
87
+ }
88
+ }
89
+
90
+ function execCodex(args = []) {
91
+ const resolvedCodexBin = resolveCodexBinary()
92
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCodexBin)) {
93
+ return execFileSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', resolvedCodexBin, ...args], {
94
+ encoding: 'utf8',
95
+ stdio: ['ignore', 'pipe', 'pipe'],
96
+ })
97
+ }
98
+
99
+ return execFileSync(resolvedCodexBin, args, {
100
+ encoding: 'utf8',
101
+ stdio: ['ignore', 'pipe', 'pipe'],
102
+ })
103
+ }
104
+
105
+ function createCheck(name, status, detail) {
106
+ return { name, status, detail }
107
+ }
108
+
109
+ function printCheck(result) {
110
+ const symbol = result.status === 'pass' ? 'OK' : result.status === 'warn' ? 'WARN' : 'FAIL'
111
+ console.log(`[${symbol}] ${result.name}: ${result.detail}`)
112
+ }
113
+
114
+ function checkNodeVersion() {
115
+ const current = parseNodeVersion(process.version)
116
+ const matchedRange = SUPPORTED_NODE_RANGES.find((range) => (
117
+ current[0] < range.maxExclusiveMajor
118
+ && compareVersions(current, range.min) >= 0
119
+ ))
120
+
121
+ if (!matchedRange) {
122
+ return createCheck(
123
+ 'Node.js',
124
+ 'fail',
125
+ `当前 ${process.version},支持范围是 20.19+ / 22.13+ / 24.x,推荐 Node ${RECOMMENDED_NODE_MAJOR}`
126
+ )
127
+ }
128
+
129
+ const recommendation = current[0] === RECOMMENDED_NODE_MAJOR ? ',当前就是推荐版本线' : `,推荐 Node ${RECOMMENDED_NODE_MAJOR}`
130
+ return createCheck('Node.js', 'pass', `当前 ${process.version}(支持 ${matchedRange.label}${recommendation})`)
131
+ }
132
+
133
+ function checkStorageWritable() {
134
+ try {
135
+ const { promptxHomeDir, dataDir, uploadsDir, tmpDir } = resolvePromptxPaths()
136
+ ;[promptxHomeDir, dataDir, uploadsDir, tmpDir].forEach((targetPath) => {
137
+ fs.mkdirSync(targetPath, { recursive: true })
138
+ })
139
+ const probePath = path.join(tmpDir, `.doctor-${process.pid}-${Date.now()}.tmp`)
140
+ fs.writeFileSync(probePath, 'ok')
141
+ fs.rmSync(probePath, { force: true })
142
+ return createCheck('数据目录', 'pass', `${promptxHomeDir} 可读写`)
143
+ } catch (error) {
144
+ return createCheck('数据目录', 'fail', error.message || '无法写入 ~/.promptx')
145
+ }
146
+ }
147
+
148
+ function checkBuiltAssets() {
149
+ if (!fs.existsSync(webIndexPath)) {
150
+ return createCheck('前端产物', 'fail', '缺少 apps/web/dist/index.html,请先构建或确认发包内容完整')
151
+ }
152
+ return createCheck('前端产物', 'pass', '已包含前端构建产物')
153
+ }
154
+
155
+ function checkCodexVersion() {
156
+ try {
157
+ const output = execCodex(['--version']).trim()
158
+ const detail = output ? `${resolveCodexBinary()} -> ${output}` : `${resolveCodexBinary()} 可执行`
159
+ return createCheck('Codex CLI', 'pass', detail)
160
+ } catch (error) {
161
+ return createCheck(
162
+ 'Codex CLI',
163
+ 'fail',
164
+ `找不到可用 Codex。请先确认终端里可以运行 \`codex --version\`,或设置 \`CODEX_BIN\`。${error.message ? ` (${error.message})` : ''}`
165
+ )
166
+ }
167
+ }
168
+
169
+ function checkCodexFullModeFlags() {
170
+ try {
171
+ execCodex(['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--help'])
172
+ return createCheck('Codex 满血参数', 'pass', '支持 PromptX 默认的满血参数')
173
+ } catch (error) {
174
+ const detail = String(error?.stderr || error?.stdout || error?.message || '').trim()
175
+ return createCheck(
176
+ 'Codex 满血参数',
177
+ 'warn',
178
+ detail
179
+ ? `当前 Codex 可能不支持 PromptX 默认启动参数:${detail.split(/\r?\n/)[0]}`
180
+ : '当前 Codex 可能不支持 PromptX 默认启动参数'
181
+ )
182
+ }
183
+ }
184
+
185
+ async function isPortOccupied(host, port) {
186
+ return new Promise((resolve) => {
187
+ const server = net.createServer()
188
+ server.unref()
189
+ server.once('error', (error) => {
190
+ resolve(error?.code === 'EADDRINUSE')
191
+ })
192
+ server.once('listening', () => {
193
+ server.close(() => resolve(false))
194
+ })
195
+ server.listen(port, host)
196
+ })
197
+ }
198
+
199
+ async function checkServicePort() {
200
+ const host = String(process.env.HOST || DEFAULT_HOST).trim() || DEFAULT_HOST
201
+ const port = Math.max(1, Number(process.env.PORT || process.env.PROMPTX_SERVER_PORT) || DEFAULT_PORT)
202
+ const baseUrl = `http://${host}:${port}`
203
+
204
+ try {
205
+ const response = await fetch(`${baseUrl}/health`)
206
+ if (response.ok) {
207
+ return createCheck('服务端口', 'pass', `${baseUrl} 已有 PromptX 服务在运行`)
208
+ }
209
+ } catch {
210
+ // Ignore and continue.
211
+ }
212
+
213
+ if (await isPortOccupied(host, port)) {
214
+ return createCheck('服务端口', 'warn', `${baseUrl} 端口已被其他进程占用`)
215
+ }
216
+
217
+ return createCheck('服务端口', 'pass', `${baseUrl} 可用`)
218
+ }
219
+
220
+ async function main() {
221
+ const codexVersionCheck = checkCodexVersion()
222
+ const checks = [
223
+ checkNodeVersion(),
224
+ checkStorageWritable(),
225
+ checkBuiltAssets(),
226
+ codexVersionCheck,
227
+ codexVersionCheck.status === 'pass'
228
+ ? checkCodexFullModeFlags()
229
+ : createCheck('Codex 满血参数', 'warn', '已跳过,因为 Codex CLI 当前不可用'),
230
+ await checkServicePort(),
231
+ ]
232
+
233
+ console.log('PromptX Doctor')
234
+ console.log('')
235
+ checks.forEach(printCheck)
236
+
237
+ const failCount = checks.filter((item) => item.status === 'fail').length
238
+ const warnCount = checks.filter((item) => item.status === 'warn').length
239
+
240
+ console.log('')
241
+ console.log(`[promptx] 检查完成:${checks.length - failCount - warnCount} 通过,${warnCount} 警告,${failCount} 失败`)
242
+
243
+ if (failCount > 0) {
244
+ process.exitCode = 1
245
+ }
246
+ }
247
+
248
+ main().catch((error) => {
249
+ console.error(`[promptx] Doctor 执行失败:${error.message || error}`)
250
+ process.exitCode = 1
251
+ })
@@ -0,0 +1,308 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { spawn } from 'node:child_process'
5
+ import { setTimeout as delay } from 'node:timers/promises'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import { resolvePromptxPaths } from '../apps/server/src/appPaths.js'
9
+
10
+ const DEFAULT_PORT = 3000
11
+ const DEFAULT_HOST = '127.0.0.1'
12
+ const STARTUP_TIMEOUT_MS = 15_000
13
+ const STOP_TIMEOUT_MS = 8_000
14
+ const POLL_INTERVAL_MS = 250
15
+
16
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
17
+ const webDistDir = path.join(rootDir, 'apps', 'web', 'dist')
18
+ const webIndexPath = path.join(webDistDir, 'index.html')
19
+ const serverEntryPath = path.join(rootDir, 'apps', 'server', 'src', 'index.js')
20
+
21
+ function ensureRuntimeDir() {
22
+ const { promptxHomeDir } = resolvePromptxPaths()
23
+ const runtimeDir = path.join(promptxHomeDir, 'run')
24
+ fs.mkdirSync(runtimeDir, { recursive: true })
25
+ return runtimeDir
26
+ }
27
+
28
+ function getRuntimePaths() {
29
+ const runtimeDir = ensureRuntimeDir()
30
+ return {
31
+ runtimeDir,
32
+ pidFile: path.join(runtimeDir, 'service.pid'),
33
+ stateFile: path.join(runtimeDir, 'service.json'),
34
+ logFile: path.join(runtimeDir, 'service.log'),
35
+ }
36
+ }
37
+
38
+ function readJsonFile(filePath) {
39
+ try {
40
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
41
+ } catch {
42
+ return null
43
+ }
44
+ }
45
+
46
+ function readPid(filePath) {
47
+ try {
48
+ const value = fs.readFileSync(filePath, 'utf8').trim()
49
+ const pid = Number(value)
50
+ return Number.isInteger(pid) && pid > 0 ? pid : 0
51
+ } catch {
52
+ return 0
53
+ }
54
+ }
55
+
56
+ function isProcessAlive(pid) {
57
+ if (!Number.isInteger(pid) || pid <= 0) {
58
+ return false
59
+ }
60
+
61
+ try {
62
+ process.kill(pid, 0)
63
+ return true
64
+ } catch (error) {
65
+ return error?.code === 'EPERM'
66
+ }
67
+ }
68
+
69
+ function removeRuntimeFiles() {
70
+ const { pidFile, stateFile } = getRuntimePaths()
71
+ fs.rmSync(pidFile, { force: true })
72
+ fs.rmSync(stateFile, { force: true })
73
+ }
74
+
75
+ function getServiceState() {
76
+ const { pidFile, stateFile, logFile } = getRuntimePaths()
77
+ const pid = readPid(pidFile)
78
+ const state = readJsonFile(stateFile) || {}
79
+
80
+ if (!isProcessAlive(pid)) {
81
+ removeRuntimeFiles()
82
+ return {
83
+ running: false,
84
+ pid: 0,
85
+ state: null,
86
+ logFile,
87
+ }
88
+ }
89
+
90
+ return {
91
+ running: true,
92
+ pid,
93
+ state,
94
+ logFile,
95
+ }
96
+ }
97
+
98
+ function getBaseUrl(host, port) {
99
+ return `http://${host}:${port}`
100
+ }
101
+
102
+ async function waitForHealth(baseUrl, pid) {
103
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS
104
+ const healthUrl = `${baseUrl}/health`
105
+
106
+ while (Date.now() < deadline) {
107
+ if (!isProcessAlive(pid)) {
108
+ throw new Error('服务进程启动后很快退出。')
109
+ }
110
+
111
+ try {
112
+ const response = await fetch(healthUrl)
113
+ if (response.ok) {
114
+ return
115
+ }
116
+ } catch {
117
+ // Ignore until timeout.
118
+ }
119
+
120
+ await delay(POLL_INTERVAL_MS)
121
+ }
122
+
123
+ throw new Error('等待服务启动超时。')
124
+ }
125
+
126
+ async function checkHealth(baseUrl) {
127
+ try {
128
+ const response = await fetch(`${baseUrl}/health`)
129
+ return response.ok
130
+ } catch {
131
+ return false
132
+ }
133
+ }
134
+
135
+ function tailLog(filePath, maxLines = 30) {
136
+ try {
137
+ const lines = fs.readFileSync(filePath, 'utf8').trim().split(/\r?\n/)
138
+ return lines.slice(-maxLines).join('\n').trim()
139
+ } catch {
140
+ return ''
141
+ }
142
+ }
143
+
144
+ async function startService() {
145
+ if (!fs.existsSync(webIndexPath)) {
146
+ throw new Error('没有找到前端构建产物,请先运行 `pnpm build`。')
147
+ }
148
+
149
+ const existing = getServiceState()
150
+ if (existing.running) {
151
+ const host = String(existing.state?.host || DEFAULT_HOST)
152
+ const port = Number(existing.state?.port || DEFAULT_PORT)
153
+ console.log(`[promptx] 已在运行:${getBaseUrl(host, port)}(PID ${existing.pid})`)
154
+ return
155
+ }
156
+
157
+ const { pidFile, stateFile, logFile } = getRuntimePaths()
158
+ const host = String(process.env.HOST || DEFAULT_HOST).trim() || DEFAULT_HOST
159
+ const port = Math.max(1, Number(process.env.PORT || process.env.PROMPTX_SERVER_PORT) || DEFAULT_PORT)
160
+ const baseUrl = getBaseUrl(host, port)
161
+ const startedAt = new Date().toISOString()
162
+
163
+ if (await checkHealth(baseUrl)) {
164
+ throw new Error(`检测到 ${baseUrl} 已有服务在运行,请先释放端口或改用其他端口。`)
165
+ }
166
+
167
+ const logFd = fs.openSync(logFile, 'a')
168
+
169
+ const child = spawn(process.execPath, [serverEntryPath], {
170
+ cwd: rootDir,
171
+ detached: true,
172
+ stdio: ['ignore', logFd, logFd],
173
+ env: {
174
+ ...process.env,
175
+ HOST: host,
176
+ PORT: String(port),
177
+ },
178
+ })
179
+
180
+ fs.closeSync(logFd)
181
+ child.unref()
182
+
183
+ fs.writeFileSync(pidFile, `${child.pid}\n`, 'utf8')
184
+ fs.writeFileSync(stateFile, JSON.stringify({
185
+ pid: child.pid,
186
+ host,
187
+ port,
188
+ startedAt,
189
+ logFile,
190
+ }, null, 2))
191
+
192
+ try {
193
+ await waitForHealth(baseUrl, child.pid)
194
+ await delay(300)
195
+ if (!isProcessAlive(child.pid)) {
196
+ throw new Error(`服务进程已退出,通常是端口 ${port} 被占用或启动参数冲突。`)
197
+ }
198
+ } catch (error) {
199
+ if (isProcessAlive(child.pid)) {
200
+ try {
201
+ process.kill(child.pid, 'SIGTERM')
202
+ } catch {
203
+ // Ignore shutdown failure.
204
+ }
205
+ }
206
+
207
+ removeRuntimeFiles()
208
+ const recentLog = tailLog(logFile)
209
+ throw new Error([
210
+ error.message || '服务启动失败。',
211
+ recentLog ? `最近日志:\n${recentLog}` : '',
212
+ ].filter(Boolean).join('\n\n'))
213
+ }
214
+
215
+ console.log(`[promptx] 已后台启动:${baseUrl}`)
216
+ console.log(`[promptx] 日志文件:${logFile}`)
217
+ }
218
+
219
+ async function stopService() {
220
+ const current = getServiceState()
221
+ if (!current.running) {
222
+ console.log('[promptx] 当前没有运行中的服务。')
223
+ return
224
+ }
225
+
226
+ const pid = current.pid
227
+
228
+ try {
229
+ process.kill(pid, 'SIGTERM')
230
+ } catch (error) {
231
+ if (error?.code !== 'ESRCH') {
232
+ throw error
233
+ }
234
+ }
235
+
236
+ const deadline = Date.now() + STOP_TIMEOUT_MS
237
+ while (Date.now() < deadline) {
238
+ if (!isProcessAlive(pid)) {
239
+ removeRuntimeFiles()
240
+ console.log(`[promptx] 已停止服务(PID ${pid})。`)
241
+ return
242
+ }
243
+ await delay(POLL_INTERVAL_MS)
244
+ }
245
+
246
+ try {
247
+ process.kill(pid, 'SIGKILL')
248
+ } catch {
249
+ // Ignore when process already exited.
250
+ }
251
+
252
+ removeRuntimeFiles()
253
+ console.log(`[promptx] 已强制停止服务(PID ${pid})。`)
254
+ }
255
+
256
+ async function restartService() {
257
+ await stopService()
258
+ await startService()
259
+ }
260
+
261
+ function printStatus() {
262
+ const current = getServiceState()
263
+ if (!current.running) {
264
+ console.log('[promptx] 服务未运行。')
265
+ return
266
+ }
267
+
268
+ const host = String(current.state?.host || DEFAULT_HOST)
269
+ const port = Number(current.state?.port || DEFAULT_PORT)
270
+ const startedAt = current.state?.startedAt || ''
271
+ console.log(`[promptx] 运行中:${getBaseUrl(host, port)}`)
272
+ console.log(`[promptx] PID:${current.pid}`)
273
+ if (startedAt) {
274
+ console.log(`[promptx] 启动时间:${startedAt}`)
275
+ }
276
+ console.log(`[promptx] 日志文件:${current.logFile}`)
277
+ }
278
+
279
+ async function main() {
280
+ const command = String(process.argv[2] || 'status').trim()
281
+
282
+ if (command === 'start') {
283
+ await startService()
284
+ return
285
+ }
286
+
287
+ if (command === 'stop') {
288
+ await stopService()
289
+ return
290
+ }
291
+
292
+ if (command === 'status') {
293
+ printStatus()
294
+ return
295
+ }
296
+
297
+ if (command === 'restart') {
298
+ await restartService()
299
+ return
300
+ }
301
+
302
+ throw new Error(`不支持的命令:${command}`)
303
+ }
304
+
305
+ main().catch((error) => {
306
+ console.error(`[promptx] ${error.message || error}`)
307
+ process.exitCode = 1
308
+ })