kanna-code 0.2.0 → 0.4.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.
- package/README.md +32 -10
- package/dist/client/assets/index-5ura1eo0.js +419 -0
- package/dist/client/assets/index-B0Cwdy1-.css +1 -0
- package/dist/client/index.html +2 -2
- package/package.json +3 -2
- package/src/server/agent.test.ts +297 -1
- package/src/server/agent.ts +56 -8
- package/src/server/cli-runtime.test.ts +180 -0
- package/src/server/cli-runtime.ts +274 -0
- package/src/server/cli.ts +20 -127
- package/src/server/codex-app-server.test.ts +236 -0
- package/src/server/codex-app-server.ts +68 -2
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +253 -17
- package/src/server/generate-title.ts +32 -39
- package/src/server/quick-response.test.ts +86 -0
- package/src/server/quick-response.ts +124 -0
- package/src/server/read-models.test.ts +43 -1
- package/src/server/server.ts +5 -3
- package/src/server/ws-router.test.ts +47 -0
- package/src/server/ws-router.ts +4 -0
- package/src/shared/protocol.ts +1 -0
- package/src/shared/tools.test.ts +12 -1
- package/src/shared/tools.ts +19 -1
- package/src/shared/types.ts +5 -1
- package/dist/client/assets/index-C-sGbl7X.js +0 -409
- package/dist/client/assets/index-gld9RxCU.css +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { compareVersions, parseArgs, runCli } from "./cli-runtime"
|
|
3
|
+
|
|
4
|
+
function createDeps(overrides: Partial<Parameters<typeof runCli>[1]> = {}) {
|
|
5
|
+
const calls = {
|
|
6
|
+
startServer: [] as Array<{ port: number; openBrowser: boolean; strictPort: boolean }>,
|
|
7
|
+
fetchLatestVersion: [] as string[],
|
|
8
|
+
installLatest: [] as string[],
|
|
9
|
+
relaunch: [] as Array<{ command: string; args: string[] }>,
|
|
10
|
+
openUrl: [] as string[],
|
|
11
|
+
log: [] as string[],
|
|
12
|
+
warn: [] as string[],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const deps: Parameters<typeof runCli>[1] = {
|
|
16
|
+
version: "0.3.0",
|
|
17
|
+
startServer: async (options) => {
|
|
18
|
+
calls.startServer.push(options)
|
|
19
|
+
return {
|
|
20
|
+
port: options.port,
|
|
21
|
+
stop: async () => {},
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
fetchLatestVersion: async (packageName) => {
|
|
25
|
+
calls.fetchLatestVersion.push(packageName)
|
|
26
|
+
return "0.3.0"
|
|
27
|
+
},
|
|
28
|
+
installLatest: (packageName) => {
|
|
29
|
+
calls.installLatest.push(packageName)
|
|
30
|
+
return true
|
|
31
|
+
},
|
|
32
|
+
relaunch: (command, args) => {
|
|
33
|
+
calls.relaunch.push({ command, args })
|
|
34
|
+
return 0
|
|
35
|
+
},
|
|
36
|
+
openUrl: (url) => {
|
|
37
|
+
calls.openUrl.push(url)
|
|
38
|
+
},
|
|
39
|
+
log: (message) => {
|
|
40
|
+
calls.log.push(message)
|
|
41
|
+
},
|
|
42
|
+
warn: (message) => {
|
|
43
|
+
calls.warn.push(message)
|
|
44
|
+
},
|
|
45
|
+
...overrides,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { calls, deps }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("parseArgs", () => {
|
|
52
|
+
test("parses runtime options", () => {
|
|
53
|
+
expect(parseArgs(["--port", "4000", "--no-open"])).toEqual({
|
|
54
|
+
kind: "run",
|
|
55
|
+
options: {
|
|
56
|
+
port: 4000,
|
|
57
|
+
openBrowser: false,
|
|
58
|
+
strictPort: false,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("parses strict port mode", () => {
|
|
64
|
+
expect(parseArgs(["--strict-port"])).toEqual({
|
|
65
|
+
kind: "run",
|
|
66
|
+
options: {
|
|
67
|
+
port: 3210,
|
|
68
|
+
openBrowser: true,
|
|
69
|
+
strictPort: true,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("returns version and help actions without running startup", () => {
|
|
75
|
+
expect(parseArgs(["--version"])).toEqual({ kind: "version" })
|
|
76
|
+
expect(parseArgs(["--help"])).toEqual({ kind: "help" })
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("compareVersions", () => {
|
|
81
|
+
test("orders semver-like versions", () => {
|
|
82
|
+
expect(compareVersions("0.3.0", "0.3.0")).toBe(0)
|
|
83
|
+
expect(compareVersions("0.3.0", "0.3.1")).toBe(-1)
|
|
84
|
+
expect(compareVersions("1.0.0", "0.9.9")).toBe(1)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe("runCli", () => {
|
|
89
|
+
test("skips update checks for --version", async () => {
|
|
90
|
+
const { calls, deps } = createDeps()
|
|
91
|
+
|
|
92
|
+
const result = await runCli(["--version"], deps)
|
|
93
|
+
|
|
94
|
+
expect(result).toEqual({ kind: "exited", code: 0 })
|
|
95
|
+
expect(calls.fetchLatestVersion).toEqual([])
|
|
96
|
+
expect(calls.startServer).toEqual([])
|
|
97
|
+
expect(calls.log).toEqual(["0.3.0"])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test("starts normally when no newer version exists", async () => {
|
|
101
|
+
const { calls, deps } = createDeps()
|
|
102
|
+
|
|
103
|
+
const result = await runCli(["--port", "4000", "--no-open"], deps)
|
|
104
|
+
|
|
105
|
+
expect(result.kind).toBe("started")
|
|
106
|
+
expect(calls.fetchLatestVersion).toEqual(["kanna-code"])
|
|
107
|
+
expect(calls.installLatest).toEqual([])
|
|
108
|
+
expect(calls.relaunch).toEqual([])
|
|
109
|
+
expect(calls.startServer).toEqual([{ port: 4000, openBrowser: false, strictPort: false }])
|
|
110
|
+
expect(calls.openUrl).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test("installs and relaunches when a newer version is available", async () => {
|
|
114
|
+
const { calls, deps } = createDeps({
|
|
115
|
+
fetchLatestVersion: async (packageName) => {
|
|
116
|
+
calls.fetchLatestVersion.push(packageName)
|
|
117
|
+
return "0.4.0"
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const result = await runCli(["--port", "4000", "--no-open"], deps)
|
|
122
|
+
|
|
123
|
+
expect(result).toEqual({ kind: "exited", code: 0 })
|
|
124
|
+
expect(calls.installLatest).toEqual(["kanna-code"])
|
|
125
|
+
expect(calls.relaunch).toEqual([{ command: "kanna", args: ["--port", "4000", "--no-open"] }])
|
|
126
|
+
expect(calls.startServer).toEqual([])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("falls back to current version when install fails", async () => {
|
|
130
|
+
const { calls, deps } = createDeps({
|
|
131
|
+
fetchLatestVersion: async (packageName) => {
|
|
132
|
+
calls.fetchLatestVersion.push(packageName)
|
|
133
|
+
return "0.4.0"
|
|
134
|
+
},
|
|
135
|
+
installLatest: (packageName) => {
|
|
136
|
+
calls.installLatest.push(packageName)
|
|
137
|
+
return false
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const result = await runCli(["--no-open"], deps)
|
|
142
|
+
|
|
143
|
+
expect(result.kind).toBe("started")
|
|
144
|
+
expect(calls.installLatest).toEqual(["kanna-code"])
|
|
145
|
+
expect(calls.relaunch).toEqual([])
|
|
146
|
+
expect(calls.warn).toContain("[kanna] update failed, continuing current version")
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test("falls back to current version when the registry check fails", async () => {
|
|
150
|
+
const { calls, deps } = createDeps({
|
|
151
|
+
fetchLatestVersion: async (packageName) => {
|
|
152
|
+
calls.fetchLatestVersion.push(packageName)
|
|
153
|
+
throw new Error("network unavailable")
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const result = await runCli(["--no-open"], deps)
|
|
158
|
+
|
|
159
|
+
expect(result.kind).toBe("started")
|
|
160
|
+
expect(calls.installLatest).toEqual([])
|
|
161
|
+
expect(calls.relaunch).toEqual([])
|
|
162
|
+
expect(calls.warn).toContain("[kanna] update check failed, continuing current version")
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test("preserves original argv when relaunching", async () => {
|
|
166
|
+
const { calls, deps } = createDeps({
|
|
167
|
+
fetchLatestVersion: async (packageName) => {
|
|
168
|
+
calls.fetchLatestVersion.push(packageName)
|
|
169
|
+
return "0.4.0"
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await runCli(["--port", "4567", "--no-open"], deps)
|
|
174
|
+
|
|
175
|
+
expect(calls.relaunch[0]).toEqual({
|
|
176
|
+
command: "kanna",
|
|
177
|
+
args: ["--port", "4567", "--no-open"],
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import process from "node:process"
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
3
|
+
import { APP_NAME, CLI_COMMAND, getDataDirDisplay, LOG_PREFIX, PACKAGE_NAME } from "../shared/branding"
|
|
4
|
+
import { PROD_SERVER_PORT } from "../shared/ports"
|
|
5
|
+
|
|
6
|
+
export interface CliOptions {
|
|
7
|
+
port: number
|
|
8
|
+
openBrowser: boolean
|
|
9
|
+
strictPort: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StartedCli {
|
|
13
|
+
kind: "started"
|
|
14
|
+
stop: () => Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExitedCli {
|
|
18
|
+
kind: "exited"
|
|
19
|
+
code: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CliRunResult = StartedCli | ExitedCli
|
|
23
|
+
|
|
24
|
+
export interface CliRuntimeDeps {
|
|
25
|
+
version: string
|
|
26
|
+
startServer: (options: CliOptions) => Promise<{ port: number; stop: () => Promise<void> }>
|
|
27
|
+
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
28
|
+
installLatest: (packageName: string) => boolean
|
|
29
|
+
relaunch: (command: string, args: string[]) => number | null
|
|
30
|
+
openUrl: (url: string) => void
|
|
31
|
+
log: (message: string) => void
|
|
32
|
+
warn: (message: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ParsedArgs =
|
|
36
|
+
| { kind: "run"; options: CliOptions }
|
|
37
|
+
| { kind: "help" }
|
|
38
|
+
| { kind: "version" }
|
|
39
|
+
|
|
40
|
+
function printHelp() {
|
|
41
|
+
console.log(`${APP_NAME} — local-only project chat UI
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
${CLI_COMMAND} [options]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--port <number> Port to listen on (default: ${PROD_SERVER_PORT})
|
|
48
|
+
--strict-port Fail instead of trying another port
|
|
49
|
+
--no-open Don't open browser automatically
|
|
50
|
+
--version Print version and exit
|
|
51
|
+
--help Show this help message`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
55
|
+
let port = PROD_SERVER_PORT
|
|
56
|
+
let openBrowser = true
|
|
57
|
+
let strictPort = false
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
60
|
+
const arg = argv[index]
|
|
61
|
+
if (arg === "--version" || arg === "-v") {
|
|
62
|
+
return { kind: "version" }
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--help" || arg === "-h") {
|
|
65
|
+
return { kind: "help" }
|
|
66
|
+
}
|
|
67
|
+
if (arg === "--port") {
|
|
68
|
+
const next = argv[index + 1]
|
|
69
|
+
if (!next) throw new Error("Missing value for --port")
|
|
70
|
+
port = Number(next)
|
|
71
|
+
index += 1
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (arg === "--no-open") {
|
|
75
|
+
openBrowser = false
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--strict-port") {
|
|
79
|
+
strictPort = true
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (!arg.startsWith("-")) throw new Error(`Unexpected positional argument: ${arg}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
kind: "run",
|
|
87
|
+
options: {
|
|
88
|
+
port,
|
|
89
|
+
openBrowser,
|
|
90
|
+
strictPort,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function compareVersions(currentVersion: string, latestVersion: string) {
|
|
96
|
+
const currentParts = normalizeVersion(currentVersion)
|
|
97
|
+
const latestParts = normalizeVersion(latestVersion)
|
|
98
|
+
const length = Math.max(currentParts.length, latestParts.length)
|
|
99
|
+
|
|
100
|
+
for (let index = 0; index < length; index += 1) {
|
|
101
|
+
const current = currentParts[index] ?? 0
|
|
102
|
+
const latest = latestParts[index] ?? 0
|
|
103
|
+
if (current === latest) continue
|
|
104
|
+
return current < latest ? -1 : 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeVersion(version: string) {
|
|
111
|
+
return version
|
|
112
|
+
.trim()
|
|
113
|
+
.replace(/^v/i, "")
|
|
114
|
+
.split("-")[0]
|
|
115
|
+
.split(".")
|
|
116
|
+
.map((part) => Number.parseInt(part, 10))
|
|
117
|
+
.filter((part) => Number.isFinite(part))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function maybeSelfUpdate(argv: string[], deps: CliRuntimeDeps) {
|
|
121
|
+
deps.log(`${LOG_PREFIX} checking for updates`)
|
|
122
|
+
|
|
123
|
+
let latestVersion: string
|
|
124
|
+
try {
|
|
125
|
+
latestVersion = await deps.fetchLatestVersion(PACKAGE_NAME)
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
deps.warn(`${LOG_PREFIX} update check failed, continuing current version`)
|
|
129
|
+
if (error instanceof Error && error.message) {
|
|
130
|
+
deps.warn(`${LOG_PREFIX} ${error.message}`)
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!latestVersion || compareVersions(deps.version, latestVersion) >= 0) {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
deps.log(`${LOG_PREFIX} updating to ${latestVersion}`)
|
|
140
|
+
if (!deps.installLatest(PACKAGE_NAME)) {
|
|
141
|
+
deps.warn(`${LOG_PREFIX} update failed, continuing current version`)
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
deps.log(`${LOG_PREFIX} restarting into updated version`)
|
|
146
|
+
const exitCode = deps.relaunch(CLI_COMMAND, argv)
|
|
147
|
+
if (exitCode === null) {
|
|
148
|
+
deps.warn(`${LOG_PREFIX} restart failed, continuing current version`)
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return exitCode
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliRunResult> {
|
|
156
|
+
const parsedArgs = parseArgs(argv)
|
|
157
|
+
if (parsedArgs.kind === "version") {
|
|
158
|
+
deps.log(deps.version)
|
|
159
|
+
return { kind: "exited", code: 0 }
|
|
160
|
+
}
|
|
161
|
+
if (parsedArgs.kind === "help") {
|
|
162
|
+
printHelp()
|
|
163
|
+
return { kind: "exited", code: 0 }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const relaunchExitCode = await maybeSelfUpdate(argv, deps)
|
|
167
|
+
if (relaunchExitCode !== null) {
|
|
168
|
+
return { kind: "exited", code: relaunchExitCode }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { port, stop } = await deps.startServer(parsedArgs.options)
|
|
172
|
+
const url = `http://localhost:${port}`
|
|
173
|
+
const launchUrl = `${url}/projects`
|
|
174
|
+
|
|
175
|
+
deps.log(`${LOG_PREFIX} listening on ${url}`)
|
|
176
|
+
deps.log(`${LOG_PREFIX} data dir: ${getDataDirDisplay()}`)
|
|
177
|
+
|
|
178
|
+
if (parsedArgs.options.openBrowser) {
|
|
179
|
+
deps.openUrl(launchUrl)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
kind: "started",
|
|
184
|
+
stop,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function spawnDetached(command: string, args: string[]) {
|
|
189
|
+
spawn(command, args, { stdio: "ignore", detached: true }).unref()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function hasCommand(command: string) {
|
|
193
|
+
const result = spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" })
|
|
194
|
+
return result.status === 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function canOpenMacApp(appName: string) {
|
|
198
|
+
const result = spawnSync("open", ["-Ra", appName], { stdio: "ignore" })
|
|
199
|
+
return result.status === 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function openUrl(url: string) {
|
|
203
|
+
const platform = process.platform
|
|
204
|
+
if (platform === "darwin") {
|
|
205
|
+
const appCandidates = [
|
|
206
|
+
"Google Chrome",
|
|
207
|
+
"Chromium",
|
|
208
|
+
"Brave Browser",
|
|
209
|
+
"Microsoft Edge",
|
|
210
|
+
"Arc",
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
for (const appName of appCandidates) {
|
|
214
|
+
if (!canOpenMacApp(appName)) continue
|
|
215
|
+
spawnDetached("open", ["-a", appName, "--args", `--app=${url}`])
|
|
216
|
+
console.log(`${LOG_PREFIX} opened in app window via ${appName}`)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
spawnDetached("open", [url])
|
|
221
|
+
console.log(`${LOG_PREFIX} opened in default browser`)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
if (platform === "win32") {
|
|
225
|
+
const browserCommands = ["chrome", "msedge", "brave", "chromium"]
|
|
226
|
+
for (const command of browserCommands) {
|
|
227
|
+
if (!hasCommand(command)) continue
|
|
228
|
+
spawnDetached(command, [`--app=${url}`])
|
|
229
|
+
console.log(`${LOG_PREFIX} opened in app window via ${command}`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
spawnDetached("cmd", ["/c", "start", "", url])
|
|
234
|
+
console.log(`${LOG_PREFIX} opened in default browser`)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const browserCommands = ["google-chrome", "chromium", "brave-browser", "microsoft-edge"]
|
|
239
|
+
for (const command of browserCommands) {
|
|
240
|
+
if (!hasCommand(command)) continue
|
|
241
|
+
spawnDetached(command, [`--app=${url}`])
|
|
242
|
+
console.log(`${LOG_PREFIX} opened in app window via ${command}`)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
spawnDetached("xdg-open", [url])
|
|
247
|
+
console.log(`${LOG_PREFIX} opened in default browser`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function fetchLatestPackageVersion(packageName: string) {
|
|
251
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`)
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new Error(`registry returned ${response.status}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const payload = await response.json() as { version?: unknown }
|
|
257
|
+
if (typeof payload.version !== "string" || !payload.version.trim()) {
|
|
258
|
+
throw new Error("registry response did not include a version")
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return payload.version
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function installLatestPackage(packageName: string) {
|
|
265
|
+
if (!hasCommand("bun")) return false
|
|
266
|
+
const result = spawnSync("bun", ["install", "-g", `${packageName}@latest`], { stdio: "inherit" })
|
|
267
|
+
return result.status === 0
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function relaunchCli(command: string, args: string[]) {
|
|
271
|
+
const result = spawnSync(command, args, { stdio: "inherit" })
|
|
272
|
+
if (result.error) return null
|
|
273
|
+
return result.status ?? 0
|
|
274
|
+
}
|
package/src/server/cli.ts
CHANGED
|
@@ -1,141 +1,34 @@
|
|
|
1
1
|
import process from "node:process"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
fetchLatestPackageVersion,
|
|
4
|
+
installLatestPackage,
|
|
5
|
+
openUrl,
|
|
6
|
+
relaunchCli,
|
|
7
|
+
runCli,
|
|
8
|
+
} from "./cli-runtime"
|
|
5
9
|
import { startKannaServer } from "./server"
|
|
6
10
|
|
|
7
11
|
// Read version from package.json at the package root
|
|
8
12
|
const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json()
|
|
9
13
|
const VERSION: string = pkg.version ?? "0.0.0"
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Options:
|
|
23
|
-
--port <number> Port to listen on (default: ${PROD_SERVER_PORT})
|
|
24
|
-
--no-open Don't open browser automatically
|
|
25
|
-
--version Print version and exit
|
|
26
|
-
--help Show this help message`)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseArgs(argv: string[]): CliOptions {
|
|
30
|
-
let port = PROD_SERVER_PORT
|
|
31
|
-
let openBrowser = true
|
|
32
|
-
|
|
33
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
-
const arg = argv[index]
|
|
35
|
-
if (arg === "--version" || arg === "-v") {
|
|
36
|
-
console.log(VERSION)
|
|
37
|
-
process.exit(0)
|
|
38
|
-
}
|
|
39
|
-
if (arg === "--help" || arg === "-h") {
|
|
40
|
-
printHelp()
|
|
41
|
-
process.exit(0)
|
|
42
|
-
}
|
|
43
|
-
if (arg === "--port") {
|
|
44
|
-
const next = argv[index + 1]
|
|
45
|
-
if (!next) throw new Error("Missing value for --port")
|
|
46
|
-
port = Number(next)
|
|
47
|
-
index += 1
|
|
48
|
-
continue
|
|
49
|
-
}
|
|
50
|
-
if (arg === "--no-open") {
|
|
51
|
-
openBrowser = false
|
|
52
|
-
continue
|
|
53
|
-
}
|
|
54
|
-
if (!arg.startsWith("-")) throw new Error(`Unexpected positional argument: ${arg}`)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
port,
|
|
59
|
-
openBrowser,
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function spawnDetached(command: string, args: string[]) {
|
|
64
|
-
spawn(command, args, { stdio: "ignore", detached: true }).unref()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function hasCommand(command: string) {
|
|
68
|
-
const result = spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" })
|
|
69
|
-
return result.status === 0
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function canOpenMacApp(appName: string) {
|
|
73
|
-
const result = spawnSync("open", ["-Ra", appName], { stdio: "ignore" })
|
|
74
|
-
return result.status === 0
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function openUrl(url: string) {
|
|
78
|
-
const platform = process.platform
|
|
79
|
-
if (platform === "darwin") {
|
|
80
|
-
const appCandidates = [
|
|
81
|
-
"Google Chrome",
|
|
82
|
-
"Chromium",
|
|
83
|
-
"Brave Browser",
|
|
84
|
-
"Microsoft Edge",
|
|
85
|
-
"Arc",
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
for (const appName of appCandidates) {
|
|
89
|
-
if (!canOpenMacApp(appName)) continue
|
|
90
|
-
spawnDetached("open", ["-a", appName, "--args", `--app=${url}`])
|
|
91
|
-
console.log(`${LOG_PREFIX} opened in app window via ${appName}`)
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
spawnDetached("open", [url])
|
|
96
|
-
console.log(`${LOG_PREFIX} opened in default browser`)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
if (platform === "win32") {
|
|
100
|
-
const browserCommands = ["chrome", "msedge", "brave", "chromium"]
|
|
101
|
-
for (const command of browserCommands) {
|
|
102
|
-
if (!hasCommand(command)) continue
|
|
103
|
-
spawnDetached(command, [`--app=${url}`])
|
|
104
|
-
console.log(`${LOG_PREFIX} opened in app window via ${command}`)
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
spawnDetached("cmd", ["/c", "start", "", url])
|
|
109
|
-
console.log(`${LOG_PREFIX} opened in default browser`)
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const browserCommands = ["google-chrome", "chromium", "brave-browser", "microsoft-edge"]
|
|
114
|
-
for (const command of browserCommands) {
|
|
115
|
-
if (!hasCommand(command)) continue
|
|
116
|
-
spawnDetached(command, [`--app=${url}`])
|
|
117
|
-
console.log(`${LOG_PREFIX} opened in app window via ${command}`)
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
spawnDetached("xdg-open", [url])
|
|
122
|
-
console.log(`${LOG_PREFIX} opened in default browser`)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const options = parseArgs(process.argv.slice(2))
|
|
126
|
-
const { port, stop } = await startKannaServer(options)
|
|
127
|
-
const url = `http://localhost:${port}`
|
|
128
|
-
const launchUrl = `${url}/projects`
|
|
129
|
-
|
|
130
|
-
console.log(`${LOG_PREFIX} listening on ${url}`)
|
|
131
|
-
console.log(`${LOG_PREFIX} data dir: ${getDataDirDisplay()}`)
|
|
15
|
+
const result = await runCli(process.argv.slice(2), {
|
|
16
|
+
version: VERSION,
|
|
17
|
+
startServer: startKannaServer,
|
|
18
|
+
fetchLatestVersion: fetchLatestPackageVersion,
|
|
19
|
+
installLatest: installLatestPackage,
|
|
20
|
+
relaunch: relaunchCli,
|
|
21
|
+
openUrl,
|
|
22
|
+
log: console.log,
|
|
23
|
+
warn: console.warn,
|
|
24
|
+
})
|
|
132
25
|
|
|
133
|
-
if (
|
|
134
|
-
|
|
26
|
+
if (result.kind === "exited") {
|
|
27
|
+
process.exit(result.code)
|
|
135
28
|
}
|
|
136
29
|
|
|
137
30
|
const shutdown = async () => {
|
|
138
|
-
await stop()
|
|
31
|
+
await result.stop()
|
|
139
32
|
process.exit(0)
|
|
140
33
|
}
|
|
141
34
|
|