xedoc-cli 0.1.18 → 0.1.20
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/README.md +42 -0
- package/bin/xedoc.mjs +578 -50
- package/build/server/index.js +1 -1
- package/package.json +1 -1
package/bin/xedoc.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process"
|
|
3
|
-
import { mkdir, readFile } from "node:fs/promises"
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
4
4
|
import { homedir } from "node:os"
|
|
5
5
|
import { dirname, join, resolve } from "node:path"
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
@@ -14,78 +14,54 @@ const packageJson = JSON.parse(
|
|
|
14
14
|
|
|
15
15
|
const options = parseArgs(process.argv.slice(2))
|
|
16
16
|
if (options.help) {
|
|
17
|
-
|
|
17
|
+
if (options.command === "service") {
|
|
18
|
+
printServiceHelp()
|
|
19
|
+
} else {
|
|
20
|
+
printHelp()
|
|
21
|
+
}
|
|
18
22
|
process.exit(0)
|
|
19
23
|
}
|
|
20
24
|
if (options.version) {
|
|
21
25
|
console.log(packageJson.version)
|
|
22
26
|
process.exit(0)
|
|
23
27
|
}
|
|
28
|
+
if (options.command === "service") {
|
|
29
|
+
await handleServiceCommand(options)
|
|
30
|
+
process.exit(0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const runtime = resolveRuntimeOptions(options)
|
|
34
|
+
await mkdir(runtime.appHome, { recursive: true, mode: 0o700 })
|
|
35
|
+
await mkdir(dirname(runtime.databasePath), { recursive: true, mode: 0o700 })
|
|
36
|
+
await mkdir(runtime.accountsHome, { recursive: true, mode: 0o700 })
|
|
24
37
|
|
|
25
|
-
const appHome = resolveHomePath(
|
|
26
|
-
options.home ?? process.env.XEDOC_HOME ?? "~/.xedoc",
|
|
27
|
-
)
|
|
28
|
-
await mkdir(appHome, { recursive: true, mode: 0o700 })
|
|
29
|
-
|
|
30
|
-
const port = options.port ?? process.env.PORT ?? "6354"
|
|
31
|
-
const host = options.host ?? process.env.HOST ?? "127.0.0.1"
|
|
32
|
-
const accountsHome = resolveHomePath(
|
|
33
|
-
options.accountsHome ??
|
|
34
|
-
process.env.CODEX_ACCOUNTS_HOME ??
|
|
35
|
-
join(appHome, "accounts"),
|
|
36
|
-
)
|
|
37
|
-
const sharedChatHome = resolveHomePath(
|
|
38
|
-
options.sharedChatHome ??
|
|
39
|
-
process.env.CODEX_SHARED_CHAT_HOME ??
|
|
40
|
-
process.env.CODEX_HOME ??
|
|
41
|
-
"~/.codex",
|
|
42
|
-
)
|
|
43
|
-
const workspaceRoot = resolveHomePath(
|
|
44
|
-
options.workspaceRoot ?? process.env.CODEX_WORKSPACE_ROOT ?? homedir(),
|
|
45
|
-
)
|
|
46
|
-
const databasePath = join(workspaceRoot, ".xedoc", "xedoc.db")
|
|
47
|
-
await mkdir(dirname(databasePath), { recursive: true, mode: 0o700 })
|
|
48
|
-
const databaseUrl = sqliteDatabaseUrl(databasePath)
|
|
49
|
-
|
|
50
|
-
const codexBin = require.resolve("@openai/codex/bin/codex.js")
|
|
51
|
-
const env = {
|
|
52
|
-
...process.env,
|
|
53
|
-
CODEX_ACCOUNTS_HOME: accountsHome,
|
|
54
|
-
CODEX_ARGS: options.codexArgs ?? process.env.CODEX_ARGS ?? `${codexBin} app-server`,
|
|
55
|
-
CODEX_COMMAND: options.codexCommand ?? process.env.CODEX_COMMAND ?? process.execPath,
|
|
56
|
-
CODEX_SHARED_CHAT_HOME: sharedChatHome,
|
|
57
|
-
CODEX_WORKSPACE_ROOT: workspaceRoot,
|
|
58
|
-
DATABASE_URL: databaseUrl,
|
|
59
|
-
HOST: host,
|
|
60
|
-
NODE_ENV: "production",
|
|
61
|
-
PORT: port,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await mkdir(accountsHome, { recursive: true, mode: 0o700 })
|
|
65
38
|
if (!options.skipPrismaGenerate) {
|
|
66
|
-
await runPrisma(["generate"], env)
|
|
39
|
+
await runPrisma(["generate"], runtime.env)
|
|
67
40
|
}
|
|
68
41
|
if (!options.skipSetup) {
|
|
69
|
-
await setupSqliteDatabase(env)
|
|
42
|
+
await setupSqliteDatabase(runtime.env)
|
|
70
43
|
}
|
|
71
44
|
|
|
72
|
-
const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`
|
|
45
|
+
const url = `http://${runtime.host === "0.0.0.0" ? "localhost" : runtime.host}:${runtime.port}`
|
|
73
46
|
console.log(`xedoc: ${url}`)
|
|
74
47
|
console.log("Set the server password in your browser on first visit.")
|
|
75
|
-
console.log(`Workspace root: ${workspaceRoot}`)
|
|
76
|
-
console.log(`Shared chat store: ${sharedChatHome}`)
|
|
48
|
+
console.log(`Workspace root: ${runtime.workspaceRoot}`)
|
|
49
|
+
console.log(`Shared chat store: ${runtime.sharedChatHome}`)
|
|
77
50
|
console.log("Press Ctrl+C to stop.")
|
|
78
51
|
|
|
79
|
-
await runServer(env)
|
|
52
|
+
await runServer(runtime.env)
|
|
80
53
|
|
|
81
54
|
function parseArgs(argv) {
|
|
82
55
|
const parsed = {}
|
|
56
|
+
const positional = []
|
|
83
57
|
for (let index = 0; index < argv.length; index += 1) {
|
|
84
58
|
const arg = argv[index]
|
|
85
59
|
if (arg === "--help" || arg === "-h") {
|
|
86
60
|
parsed.help = true
|
|
87
61
|
} else if (arg === "--version" || arg === "-v") {
|
|
88
62
|
parsed.version = true
|
|
63
|
+
} else if (arg === "--no-start") {
|
|
64
|
+
parsed.noStart = true
|
|
89
65
|
} else if (arg === "--skip-setup") {
|
|
90
66
|
parsed.skipSetup = true
|
|
91
67
|
} else if (arg === "--skip-prisma-generate") {
|
|
@@ -98,9 +74,19 @@ function parseArgs(argv) {
|
|
|
98
74
|
}
|
|
99
75
|
assignOption(parsed, name, value)
|
|
100
76
|
} else {
|
|
101
|
-
|
|
77
|
+
positional.push(arg)
|
|
102
78
|
}
|
|
103
79
|
}
|
|
80
|
+
if (positional.length) {
|
|
81
|
+
if (positional[0] !== "service") {
|
|
82
|
+
fail(`Unknown command: ${positional[0]}`)
|
|
83
|
+
}
|
|
84
|
+
if (positional.length > 2) {
|
|
85
|
+
fail(`Unknown service argument: ${positional[2]}`)
|
|
86
|
+
}
|
|
87
|
+
parsed.command = "service"
|
|
88
|
+
parsed.serviceAction = positional[1]
|
|
89
|
+
}
|
|
104
90
|
return parsed
|
|
105
91
|
}
|
|
106
92
|
|
|
@@ -115,6 +101,9 @@ function assignOption(parsed, name, value) {
|
|
|
115
101
|
case "--codex-command":
|
|
116
102
|
parsed.codexCommand = value
|
|
117
103
|
return
|
|
104
|
+
case "--forever-command":
|
|
105
|
+
parsed.foreverCommand = value
|
|
106
|
+
return
|
|
118
107
|
case "--home":
|
|
119
108
|
parsed.home = value
|
|
120
109
|
return
|
|
@@ -124,6 +113,12 @@ function assignOption(parsed, name, value) {
|
|
|
124
113
|
case "--port":
|
|
125
114
|
parsed.port = value
|
|
126
115
|
return
|
|
116
|
+
case "--service-name":
|
|
117
|
+
parsed.serviceName = value
|
|
118
|
+
return
|
|
119
|
+
case "--service-driver":
|
|
120
|
+
parsed.serviceDriver = value
|
|
121
|
+
return
|
|
127
122
|
case "--shared-chat-home":
|
|
128
123
|
parsed.sharedChatHome = value
|
|
129
124
|
return
|
|
@@ -135,6 +130,505 @@ function assignOption(parsed, name, value) {
|
|
|
135
130
|
}
|
|
136
131
|
}
|
|
137
132
|
|
|
133
|
+
function resolveRuntimeOptions(options) {
|
|
134
|
+
const appHome = resolveHomePath(
|
|
135
|
+
options.home ?? process.env.XEDOC_HOME ?? "~/.xedoc",
|
|
136
|
+
)
|
|
137
|
+
const port = String(options.port ?? process.env.PORT ?? "6354")
|
|
138
|
+
const host = String(options.host ?? process.env.HOST ?? "127.0.0.1")
|
|
139
|
+
const accountsHome = resolveHomePath(
|
|
140
|
+
options.accountsHome ??
|
|
141
|
+
process.env.CODEX_ACCOUNTS_HOME ??
|
|
142
|
+
join(appHome, "accounts"),
|
|
143
|
+
)
|
|
144
|
+
const sharedChatHome = resolveHomePath(
|
|
145
|
+
options.sharedChatHome ??
|
|
146
|
+
process.env.CODEX_SHARED_CHAT_HOME ??
|
|
147
|
+
process.env.CODEX_HOME ??
|
|
148
|
+
"~/.codex",
|
|
149
|
+
)
|
|
150
|
+
const workspaceRoot = resolveHomePath(
|
|
151
|
+
options.workspaceRoot ?? process.env.CODEX_WORKSPACE_ROOT ?? homedir(),
|
|
152
|
+
)
|
|
153
|
+
const databasePath = join(workspaceRoot, ".xedoc", "xedoc.db")
|
|
154
|
+
const databaseUrl = sqliteDatabaseUrl(databasePath)
|
|
155
|
+
const codexBin = require.resolve("@openai/codex/bin/codex.js")
|
|
156
|
+
const codexArgs =
|
|
157
|
+
options.codexArgs ?? process.env.CODEX_ARGS ?? `${codexBin} app-server`
|
|
158
|
+
const codexCommand =
|
|
159
|
+
options.codexCommand ?? process.env.CODEX_COMMAND ?? process.execPath
|
|
160
|
+
const env = {
|
|
161
|
+
...process.env,
|
|
162
|
+
CODEX_ACCOUNTS_HOME: accountsHome,
|
|
163
|
+
CODEX_ARGS: codexArgs,
|
|
164
|
+
CODEX_COMMAND: codexCommand,
|
|
165
|
+
CODEX_SHARED_CHAT_HOME: sharedChatHome,
|
|
166
|
+
CODEX_WORKSPACE_ROOT: workspaceRoot,
|
|
167
|
+
DATABASE_URL: databaseUrl,
|
|
168
|
+
HOST: host,
|
|
169
|
+
NODE_ENV: "production",
|
|
170
|
+
PORT: port,
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
accountsHome,
|
|
174
|
+
appHome,
|
|
175
|
+
codexArgs,
|
|
176
|
+
codexCommand,
|
|
177
|
+
databasePath,
|
|
178
|
+
databaseUrl,
|
|
179
|
+
env,
|
|
180
|
+
host,
|
|
181
|
+
port,
|
|
182
|
+
sharedChatHome,
|
|
183
|
+
skipPrismaGenerate: !!options.skipPrismaGenerate,
|
|
184
|
+
skipSetup: !!options.skipSetup,
|
|
185
|
+
workspaceRoot,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleServiceCommand(options) {
|
|
190
|
+
const driver = resolveServiceDriver(options.serviceDriver)
|
|
191
|
+
switch (options.serviceAction) {
|
|
192
|
+
case "install":
|
|
193
|
+
await installBackgroundService(options, driver)
|
|
194
|
+
return
|
|
195
|
+
case "uninstall":
|
|
196
|
+
await uninstallBackgroundService(options, driver)
|
|
197
|
+
return
|
|
198
|
+
case undefined:
|
|
199
|
+
printServiceHelp()
|
|
200
|
+
fail("Choose a service command: install or uninstall.")
|
|
201
|
+
return
|
|
202
|
+
default:
|
|
203
|
+
fail(`Unknown service command: ${options.serviceAction}`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function installBackgroundService(options, driver) {
|
|
208
|
+
switch (driver) {
|
|
209
|
+
case "systemd":
|
|
210
|
+
await installSystemdService(options)
|
|
211
|
+
return
|
|
212
|
+
case "launchd":
|
|
213
|
+
await installLaunchdService(options)
|
|
214
|
+
return
|
|
215
|
+
case "windows-task":
|
|
216
|
+
await installWindowsTaskService(options)
|
|
217
|
+
return
|
|
218
|
+
case "forever":
|
|
219
|
+
await installForeverService(options)
|
|
220
|
+
return
|
|
221
|
+
default:
|
|
222
|
+
fail(`Unsupported service driver: ${driver}`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function uninstallBackgroundService(options, driver) {
|
|
227
|
+
switch (driver) {
|
|
228
|
+
case "systemd":
|
|
229
|
+
await uninstallSystemdService(options)
|
|
230
|
+
return
|
|
231
|
+
case "launchd":
|
|
232
|
+
await uninstallLaunchdService(options)
|
|
233
|
+
return
|
|
234
|
+
case "windows-task":
|
|
235
|
+
await uninstallWindowsTaskService(options)
|
|
236
|
+
return
|
|
237
|
+
case "forever":
|
|
238
|
+
await uninstallForeverService(options)
|
|
239
|
+
return
|
|
240
|
+
default:
|
|
241
|
+
fail(`Unsupported service driver: ${driver}`)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveServiceDriver(value) {
|
|
246
|
+
const driver = value?.trim() || "auto"
|
|
247
|
+
if (driver === "auto") {
|
|
248
|
+
if (process.platform === "linux") {
|
|
249
|
+
return "systemd"
|
|
250
|
+
}
|
|
251
|
+
if (process.platform === "darwin") {
|
|
252
|
+
return "launchd"
|
|
253
|
+
}
|
|
254
|
+
if (process.platform === "win32") {
|
|
255
|
+
return "windows-task"
|
|
256
|
+
}
|
|
257
|
+
fail(
|
|
258
|
+
`No native service driver is available for ${process.platform}. Try --service-driver forever.`,
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
driver === "systemd" ||
|
|
263
|
+
driver === "launchd" ||
|
|
264
|
+
driver === "windows-task" ||
|
|
265
|
+
driver === "forever"
|
|
266
|
+
) {
|
|
267
|
+
return driver
|
|
268
|
+
}
|
|
269
|
+
fail(
|
|
270
|
+
`Unknown service driver: ${driver}. Use auto, systemd, launchd, windows-task, or forever.`,
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function installSystemdService(options) {
|
|
275
|
+
requireLinuxSystemd()
|
|
276
|
+
const runtime = resolveRuntimeOptions(options)
|
|
277
|
+
const serviceBaseName = normalizeServiceBaseName(options.serviceName)
|
|
278
|
+
const serviceName = `${serviceBaseName}.service`
|
|
279
|
+
const unitPath = userSystemdUnitPath(serviceName)
|
|
280
|
+
await mkdir(dirname(unitPath), { recursive: true, mode: 0o700 })
|
|
281
|
+
await writeFile(unitPath, systemdUnitFile(runtime), { mode: 0o644 })
|
|
282
|
+
await runSystemctl(["daemon-reload"])
|
|
283
|
+
await runSystemctl(["enable", ...(options.noStart ? [] : ["--now"]), serviceName])
|
|
284
|
+
|
|
285
|
+
const url = `http://${runtime.host === "0.0.0.0" ? "localhost" : runtime.host}:${runtime.port}`
|
|
286
|
+
console.log(`Installed ${serviceName}.`)
|
|
287
|
+
console.log(options.noStart ? "Service is enabled but not started." : `Service is running at ${url}.`)
|
|
288
|
+
console.log(`Logs: systemctl --user status ${serviceName}`)
|
|
289
|
+
console.log(`Uninstall: xedoc service uninstall --service-name ${serviceBaseName}`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function uninstallSystemdService(options) {
|
|
293
|
+
requireLinuxSystemd()
|
|
294
|
+
const serviceBaseName = normalizeServiceBaseName(options.serviceName)
|
|
295
|
+
const serviceName = `${serviceBaseName}.service`
|
|
296
|
+
const unitPath = userSystemdUnitPath(serviceName)
|
|
297
|
+
await runSystemctl(["disable", "--now", serviceName]).catch((error) => {
|
|
298
|
+
console.warn(
|
|
299
|
+
`Could not stop or disable ${serviceName}: ${error instanceof Error ? error.message : error}`,
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
await rm(unitPath, { force: true })
|
|
303
|
+
await runSystemctl(["daemon-reload"])
|
|
304
|
+
console.log(`Uninstalled ${serviceName}.`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function installLaunchdService(options) {
|
|
308
|
+
requireDarwinLaunchd()
|
|
309
|
+
const runtime = resolveRuntimeOptions(options)
|
|
310
|
+
const label = normalizeServiceBaseName(options.serviceName)
|
|
311
|
+
const plistPath = launchdPlistPath(label)
|
|
312
|
+
await mkdir(dirname(plistPath), { recursive: true, mode: 0o700 })
|
|
313
|
+
await mkdir(serviceLogDirectory(runtime), { recursive: true, mode: 0o700 })
|
|
314
|
+
await writeFile(plistPath, launchdPlistFile(runtime, label), { mode: 0o644 })
|
|
315
|
+
const domain = launchdDomain()
|
|
316
|
+
if (!options.noStart) {
|
|
317
|
+
await runLaunchctl(["bootout", domain, plistPath]).catch(() => undefined)
|
|
318
|
+
await runLaunchctl(["bootstrap", domain, plistPath])
|
|
319
|
+
await runLaunchctl(["enable", `${domain}/${label}`]).catch(() => undefined)
|
|
320
|
+
await runLaunchctl(["kickstart", "-k", `${domain}/${label}`])
|
|
321
|
+
}
|
|
322
|
+
const url = `http://${runtime.host === "0.0.0.0" ? "localhost" : runtime.host}:${runtime.port}`
|
|
323
|
+
console.log(`Installed ${label}.`)
|
|
324
|
+
console.log(options.noStart ? "LaunchAgent is installed and will start at next login." : `Service is running at ${url}.`)
|
|
325
|
+
console.log(`Logs: tail -f ${join(serviceLogDirectory(runtime), `${label}.out.log`)}`)
|
|
326
|
+
console.log(`Uninstall: xedoc service uninstall --service-name ${label}`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function uninstallLaunchdService(options) {
|
|
330
|
+
requireDarwinLaunchd()
|
|
331
|
+
const label = normalizeServiceBaseName(options.serviceName)
|
|
332
|
+
const plistPath = launchdPlistPath(label)
|
|
333
|
+
await runLaunchctl(["bootout", launchdDomain(), plistPath]).catch((error) => {
|
|
334
|
+
console.warn(
|
|
335
|
+
`Could not stop ${label}: ${error instanceof Error ? error.message : error}`,
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
await rm(plistPath, { force: true })
|
|
339
|
+
console.log(`Uninstalled ${label}.`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function installWindowsTaskService(options) {
|
|
343
|
+
requireWindowsTaskScheduler()
|
|
344
|
+
const runtime = resolveRuntimeOptions(options)
|
|
345
|
+
const taskName = normalizeServiceBaseName(options.serviceName)
|
|
346
|
+
const commandPath = await writeWindowsServiceCommand(runtime, taskName)
|
|
347
|
+
await runSchtasks([
|
|
348
|
+
"/Create",
|
|
349
|
+
"/TN",
|
|
350
|
+
taskName,
|
|
351
|
+
"/SC",
|
|
352
|
+
"ONLOGON",
|
|
353
|
+
"/TR",
|
|
354
|
+
`cmd.exe /d /c ${windowsBatchQuote(commandPath)}`,
|
|
355
|
+
"/F",
|
|
356
|
+
])
|
|
357
|
+
if (!options.noStart) {
|
|
358
|
+
await runSchtasks(["/Run", "/TN", taskName])
|
|
359
|
+
}
|
|
360
|
+
const url = `http://${runtime.host === "0.0.0.0" ? "localhost" : runtime.host}:${runtime.port}`
|
|
361
|
+
console.log(`Installed ${taskName}.`)
|
|
362
|
+
console.log(options.noStart ? "Task is installed but not started." : `Task is running at ${url}.`)
|
|
363
|
+
console.log(`Status: schtasks /Query /TN ${taskName}`)
|
|
364
|
+
console.log(`Uninstall: xedoc service uninstall --service-name ${taskName}`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function uninstallWindowsTaskService(options) {
|
|
368
|
+
requireWindowsTaskScheduler()
|
|
369
|
+
const runtime = resolveRuntimeOptions(options)
|
|
370
|
+
const taskName = normalizeServiceBaseName(options.serviceName)
|
|
371
|
+
await runSchtasks(["/End", "/TN", taskName]).catch(() => undefined)
|
|
372
|
+
await runSchtasks(["/Delete", "/TN", taskName, "/F"]).catch((error) => {
|
|
373
|
+
console.warn(
|
|
374
|
+
`Could not delete ${taskName}: ${error instanceof Error ? error.message : error}`,
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
await rm(windowsServiceCommandPath(runtime, taskName), { force: true })
|
|
378
|
+
console.log(`Uninstalled ${taskName}.`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function installForeverService(options) {
|
|
382
|
+
if (options.noStart) {
|
|
383
|
+
fail("--no-start is not supported with --service-driver forever.")
|
|
384
|
+
}
|
|
385
|
+
const runtime = resolveRuntimeOptions(options)
|
|
386
|
+
const serviceName = normalizeServiceBaseName(options.serviceName)
|
|
387
|
+
const logDirectory = serviceLogDirectory(runtime)
|
|
388
|
+
await mkdir(logDirectory, { recursive: true, mode: 0o700 })
|
|
389
|
+
await runForever(["stop", serviceName], options).catch(() => undefined)
|
|
390
|
+
await runForever([
|
|
391
|
+
"start",
|
|
392
|
+
"--uid",
|
|
393
|
+
serviceName,
|
|
394
|
+
"--append",
|
|
395
|
+
"--workingDir",
|
|
396
|
+
packageRoot,
|
|
397
|
+
"-l",
|
|
398
|
+
join(logDirectory, `${serviceName}.forever.log`),
|
|
399
|
+
"-o",
|
|
400
|
+
join(logDirectory, `${serviceName}.out.log`),
|
|
401
|
+
"-e",
|
|
402
|
+
join(logDirectory, `${serviceName}.err.log`),
|
|
403
|
+
"-c",
|
|
404
|
+
process.execPath,
|
|
405
|
+
join(packageRoot, "bin", "xedoc.mjs"),
|
|
406
|
+
...runtimeServiceArgs(runtime),
|
|
407
|
+
], options)
|
|
408
|
+
const url = `http://${runtime.host === "0.0.0.0" ? "localhost" : runtime.host}:${runtime.port}`
|
|
409
|
+
console.log(`Started ${serviceName} with forever at ${url}.`)
|
|
410
|
+
console.log("Note: forever keeps the process alive, but does not install OS boot integration.")
|
|
411
|
+
console.log(`Logs: forever logs ${serviceName}`)
|
|
412
|
+
console.log(`Uninstall: xedoc service uninstall --service-driver forever --service-name ${serviceName}`)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function uninstallForeverService(options) {
|
|
416
|
+
const serviceName = normalizeServiceBaseName(options.serviceName)
|
|
417
|
+
await runForever(["stop", serviceName], options).catch((error) => {
|
|
418
|
+
console.warn(
|
|
419
|
+
`Could not stop ${serviceName}: ${error instanceof Error ? error.message : error}`,
|
|
420
|
+
)
|
|
421
|
+
})
|
|
422
|
+
console.log(`Stopped ${serviceName} in forever.`)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function requireLinuxSystemd() {
|
|
426
|
+
if (process.platform !== "linux") {
|
|
427
|
+
fail("The systemd service driver only supports Linux.")
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function requireDarwinLaunchd() {
|
|
432
|
+
if (process.platform !== "darwin") {
|
|
433
|
+
fail("The launchd service driver only supports macOS.")
|
|
434
|
+
}
|
|
435
|
+
if (typeof process.getuid !== "function") {
|
|
436
|
+
fail("Could not determine the current macOS user id.")
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function requireWindowsTaskScheduler() {
|
|
441
|
+
if (process.platform !== "win32") {
|
|
442
|
+
fail("The windows-task service driver only supports Windows.")
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function normalizeServiceBaseName(value) {
|
|
447
|
+
const name = value?.trim() || "xedoc"
|
|
448
|
+
const normalized = name.endsWith(".service") ? name.slice(0, -".service".length) : name
|
|
449
|
+
if (!/^[A-Za-z0-9_.@-]+$/u.test(name)) {
|
|
450
|
+
fail("Service name may only contain letters, numbers, dots, underscores, @, and hyphens.")
|
|
451
|
+
}
|
|
452
|
+
if (!normalized) {
|
|
453
|
+
fail("Service name must not be empty.")
|
|
454
|
+
}
|
|
455
|
+
return normalized
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function userSystemdUnitPath(serviceName) {
|
|
459
|
+
return join(homedir(), ".config", "systemd", "user", serviceName)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function launchdPlistPath(label) {
|
|
463
|
+
return join(homedir(), "Library", "LaunchAgents", `${label}.plist`)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function launchdDomain() {
|
|
467
|
+
return `gui/${process.getuid()}`
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function serviceLogDirectory(runtime) {
|
|
471
|
+
return join(runtime.appHome, "logs")
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function launchdPlistFile(runtime, label) {
|
|
475
|
+
const execArgs = [
|
|
476
|
+
process.execPath,
|
|
477
|
+
join(packageRoot, "bin", "xedoc.mjs"),
|
|
478
|
+
...runtimeServiceArgs(runtime),
|
|
479
|
+
]
|
|
480
|
+
const logDirectory = serviceLogDirectory(runtime)
|
|
481
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
482
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
483
|
+
<plist version="1.0">
|
|
484
|
+
<dict>
|
|
485
|
+
<key>Label</key>
|
|
486
|
+
<string>${xmlEscape(label)}</string>
|
|
487
|
+
<key>ProgramArguments</key>
|
|
488
|
+
<array>
|
|
489
|
+
${execArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n")}
|
|
490
|
+
</array>
|
|
491
|
+
<key>WorkingDirectory</key>
|
|
492
|
+
<string>${xmlEscape(packageRoot)}</string>
|
|
493
|
+
<key>RunAtLoad</key>
|
|
494
|
+
<true/>
|
|
495
|
+
<key>KeepAlive</key>
|
|
496
|
+
<true/>
|
|
497
|
+
<key>StandardOutPath</key>
|
|
498
|
+
<string>${xmlEscape(join(logDirectory, `${label}.out.log`))}</string>
|
|
499
|
+
<key>StandardErrorPath</key>
|
|
500
|
+
<string>${xmlEscape(join(logDirectory, `${label}.err.log`))}</string>
|
|
501
|
+
</dict>
|
|
502
|
+
</plist>
|
|
503
|
+
`
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function systemdUnitFile(runtime) {
|
|
507
|
+
const execArgs = [
|
|
508
|
+
process.execPath,
|
|
509
|
+
join(packageRoot, "bin", "xedoc.mjs"),
|
|
510
|
+
...runtimeServiceArgs(runtime),
|
|
511
|
+
]
|
|
512
|
+
return `[Unit]
|
|
513
|
+
Description=xedoc local Codex web UI
|
|
514
|
+
After=network-online.target
|
|
515
|
+
|
|
516
|
+
[Service]
|
|
517
|
+
Type=simple
|
|
518
|
+
ExecStart=${execArgs.map(systemdQuoteExecArg).join(" ")}
|
|
519
|
+
Restart=on-failure
|
|
520
|
+
RestartSec=5
|
|
521
|
+
Environment=NODE_ENV=production
|
|
522
|
+
|
|
523
|
+
[Install]
|
|
524
|
+
WantedBy=default.target
|
|
525
|
+
`
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function runtimeServiceArgs(runtime) {
|
|
529
|
+
const args = [
|
|
530
|
+
"--home",
|
|
531
|
+
runtime.appHome,
|
|
532
|
+
"--host",
|
|
533
|
+
runtime.host,
|
|
534
|
+
"--port",
|
|
535
|
+
runtime.port,
|
|
536
|
+
"--workspace-root",
|
|
537
|
+
runtime.workspaceRoot,
|
|
538
|
+
"--accounts-home",
|
|
539
|
+
runtime.accountsHome,
|
|
540
|
+
"--shared-chat-home",
|
|
541
|
+
runtime.sharedChatHome,
|
|
542
|
+
"--codex-command",
|
|
543
|
+
runtime.codexCommand,
|
|
544
|
+
"--codex-args",
|
|
545
|
+
runtime.codexArgs,
|
|
546
|
+
]
|
|
547
|
+
if (runtime.skipSetup) {
|
|
548
|
+
args.push("--skip-setup")
|
|
549
|
+
}
|
|
550
|
+
if (runtime.skipPrismaGenerate) {
|
|
551
|
+
args.push("--skip-prisma-generate")
|
|
552
|
+
}
|
|
553
|
+
return args
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function systemdQuoteExecArg(value) {
|
|
557
|
+
return `"${String(value)
|
|
558
|
+
.replaceAll("\\", "\\\\")
|
|
559
|
+
.replaceAll('"', '\\"')
|
|
560
|
+
.replaceAll("%", "%%")}"`
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function xmlEscape(value) {
|
|
564
|
+
return String(value)
|
|
565
|
+
.replaceAll("&", "&")
|
|
566
|
+
.replaceAll("<", "<")
|
|
567
|
+
.replaceAll(">", ">")
|
|
568
|
+
.replaceAll('"', """)
|
|
569
|
+
.replaceAll("'", "'")
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function writeWindowsServiceCommand(runtime, taskName) {
|
|
573
|
+
const commandPath = windowsServiceCommandPath(runtime, taskName)
|
|
574
|
+
await mkdir(dirname(commandPath), { recursive: true, mode: 0o700 })
|
|
575
|
+
const command = [
|
|
576
|
+
"@echo off",
|
|
577
|
+
`cd /d ${windowsBatchQuote(packageRoot)}`,
|
|
578
|
+
[
|
|
579
|
+
windowsBatchQuote(process.execPath),
|
|
580
|
+
windowsBatchQuote(join(packageRoot, "bin", "xedoc.mjs")),
|
|
581
|
+
...runtimeServiceArgs(runtime).map(windowsBatchQuote),
|
|
582
|
+
].join(" "),
|
|
583
|
+
"",
|
|
584
|
+
].join("\r\n")
|
|
585
|
+
await writeFile(commandPath, command, { mode: 0o700 })
|
|
586
|
+
return commandPath
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function windowsServiceCommandPath(runtime, taskName) {
|
|
590
|
+
return join(runtime.appHome, "service", `${taskName}.cmd`)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function windowsBatchQuote(value) {
|
|
594
|
+
return `"${String(value)
|
|
595
|
+
.replaceAll("%", "%%")
|
|
596
|
+
.replaceAll("^", "^^")
|
|
597
|
+
.replaceAll('"', '""')}"`
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function runSystemctl(args) {
|
|
601
|
+
await run("systemctl", ["--user", ...args], {
|
|
602
|
+
cwd: packageRoot,
|
|
603
|
+
env: process.env,
|
|
604
|
+
stdio: "inherit",
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function runLaunchctl(args) {
|
|
609
|
+
await run("launchctl", args, {
|
|
610
|
+
cwd: packageRoot,
|
|
611
|
+
env: process.env,
|
|
612
|
+
stdio: "inherit",
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function runSchtasks(args) {
|
|
617
|
+
await run("schtasks.exe", args, {
|
|
618
|
+
cwd: packageRoot,
|
|
619
|
+
env: process.env,
|
|
620
|
+
stdio: "inherit",
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function runForever(args, options) {
|
|
625
|
+
await run(options.foreverCommand ?? process.env.FOREVER_COMMAND ?? "forever", args, {
|
|
626
|
+
cwd: packageRoot,
|
|
627
|
+
env: process.env,
|
|
628
|
+
stdio: "inherit",
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
|
|
138
632
|
async function runPrisma(args, env) {
|
|
139
633
|
await run(process.execPath, [
|
|
140
634
|
require.resolve("prisma/build/index.js"),
|
|
@@ -224,7 +718,9 @@ function printHelp() {
|
|
|
224
718
|
console.log(`xedoc ${packageJson.version}
|
|
225
719
|
|
|
226
720
|
Usage:
|
|
227
|
-
npx xedoc [options]
|
|
721
|
+
npx xedoc-cli [options]
|
|
722
|
+
xedoc service install [options]
|
|
723
|
+
xedoc service uninstall [options]
|
|
228
724
|
|
|
229
725
|
Options:
|
|
230
726
|
--port <port> Web server port. Defaults to 6354.
|
|
@@ -233,10 +729,42 @@ Options:
|
|
|
233
729
|
--accounts-home <path> Codex account state directory. Defaults to ~/.xedoc/accounts.
|
|
234
730
|
--shared-chat-home <path> Shared Codex chat store. Defaults to ~/.codex.
|
|
235
731
|
--skip-setup Do not create the SQLite database schema.
|
|
732
|
+
--skip-prisma-generate Do not regenerate Prisma Client.
|
|
236
733
|
--codex-command <command> Codex command used for new accounts.
|
|
237
734
|
--codex-args <args> Codex command arguments used for new accounts.
|
|
238
735
|
--home <path> App data directory. Defaults to ~/.xedoc.
|
|
736
|
+
--service-driver <driver> auto, systemd, launchd, windows-task, or forever. Defaults to auto.
|
|
737
|
+
--service-name <name> Service name. Defaults to xedoc.
|
|
738
|
+
--forever-command <command> forever executable for --service-driver forever. Defaults to forever.
|
|
739
|
+
--no-start Enable service without starting it during install.
|
|
239
740
|
--help Show this help.
|
|
240
741
|
--version Print the package version.
|
|
241
742
|
`)
|
|
242
743
|
}
|
|
744
|
+
|
|
745
|
+
function printServiceHelp() {
|
|
746
|
+
console.log(`xedoc ${packageJson.version}
|
|
747
|
+
|
|
748
|
+
Usage:
|
|
749
|
+
xedoc service install [options]
|
|
750
|
+
xedoc service uninstall [options]
|
|
751
|
+
|
|
752
|
+
Service commands:
|
|
753
|
+
install Install the background service and start it unless --no-start is set.
|
|
754
|
+
uninstall Stop and remove the background service.
|
|
755
|
+
|
|
756
|
+
Options:
|
|
757
|
+
--service-driver <driver> auto, systemd, launchd, windows-task, or forever. Defaults to auto.
|
|
758
|
+
--service-name <name> Service name. Defaults to xedoc.
|
|
759
|
+
--forever-command <command> forever executable for --service-driver forever. Defaults to forever.
|
|
760
|
+
--no-start Enable service without starting it during install.
|
|
761
|
+
--port <port> Web server port. Defaults to 6354.
|
|
762
|
+
--host <host> Web server host. Defaults to 127.0.0.1.
|
|
763
|
+
--workspace-root <path> Directory tree visible in the app. Defaults to your home directory.
|
|
764
|
+
--accounts-home <path> Codex account state directory. Defaults to ~/.xedoc/accounts.
|
|
765
|
+
--shared-chat-home <path> Shared Codex chat store. Defaults to ~/.codex.
|
|
766
|
+
--home <path> App data directory. Defaults to ~/.xedoc.
|
|
767
|
+
--skip-setup Do not create the SQLite database schema.
|
|
768
|
+
--skip-prisma-generate Do not regenerate Prisma Client.
|
|
769
|
+
`)
|
|
770
|
+
}
|