kanna-code 0.13.2 → 0.13.4
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/dist/client/assets/index-CA-kJR8F.css +32 -0
- package/dist/client/assets/index-DlH0WHut.js +523 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/cli-runtime.test.ts +41 -4
- package/src/server/cli-runtime.ts +67 -9
- package/src/server/cli-supervisor.ts +7 -0
- package/src/server/cli.ts +7 -7
- package/src/server/restart.test.ts +13 -3
- package/src/server/restart.ts +8 -2
- package/src/server/server.ts +2 -1
- package/src/server/update-manager.test.ts +38 -6
- package/src/server/update-manager.ts +23 -10
- package/src/server/ws-router.test.ts +33 -1
- package/src/shared/types.ts +13 -0
- package/dist/client/assets/index-DTEcy0lx.css +0 -32
- package/dist/client/assets/index-rfFzSjyG.js +0 -518
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Kanna</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DlH0WHut.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CA-kJR8F.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { compareVersions, parseArgs, runCli } from "./cli-runtime"
|
|
2
|
+
import { compareVersions, classifyInstallVersionFailure, parseArgs, runCli } from "./cli-runtime"
|
|
3
|
+
import { CLI_SUPPRESS_OPEN_ONCE_ENV_VAR } from "./restart"
|
|
3
4
|
|
|
4
5
|
const originalRuntimeProfile = process.env.KANNA_RUNTIME_PROFILE
|
|
6
|
+
const originalSuppressOpen = process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]
|
|
5
7
|
|
|
6
8
|
afterEach(() => {
|
|
7
9
|
if (originalRuntimeProfile === undefined) {
|
|
@@ -9,6 +11,11 @@ afterEach(() => {
|
|
|
9
11
|
} else {
|
|
10
12
|
process.env.KANNA_RUNTIME_PROFILE = originalRuntimeProfile
|
|
11
13
|
}
|
|
14
|
+
if (originalSuppressOpen === undefined) {
|
|
15
|
+
delete process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]
|
|
16
|
+
} else {
|
|
17
|
+
process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] = originalSuppressOpen
|
|
18
|
+
}
|
|
12
19
|
})
|
|
13
20
|
|
|
14
21
|
function createDeps(overrides: Partial<Parameters<typeof runCli>[1]> = {}) {
|
|
@@ -46,7 +53,12 @@ function createDeps(overrides: Partial<Parameters<typeof runCli>[1]> = {}) {
|
|
|
46
53
|
},
|
|
47
54
|
installVersion: (packageName, version) => {
|
|
48
55
|
calls.installVersion.push({ packageName, version })
|
|
49
|
-
return
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
errorCode: null,
|
|
59
|
+
userTitle: null,
|
|
60
|
+
userMessage: null,
|
|
61
|
+
}
|
|
50
62
|
},
|
|
51
63
|
openUrl: (url) => {
|
|
52
64
|
calls.openUrl.push(url)
|
|
@@ -100,6 +112,17 @@ describe("compareVersions", () => {
|
|
|
100
112
|
})
|
|
101
113
|
})
|
|
102
114
|
|
|
115
|
+
describe("classifyInstallVersionFailure", () => {
|
|
116
|
+
test("maps version propagation failures to a user-facing retry message", () => {
|
|
117
|
+
expect(classifyInstallVersionFailure('error: No version matching "0.13.3" found for specifier "kanna-code"')).toEqual({
|
|
118
|
+
ok: false,
|
|
119
|
+
errorCode: "version_not_live_yet",
|
|
120
|
+
userTitle: "Update not live yet",
|
|
121
|
+
userMessage: "This update is still propagating. Try again in a few minutes.",
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
103
126
|
describe("runCli", () => {
|
|
104
127
|
test("skips update checks for --version", async () => {
|
|
105
128
|
const { calls, deps } = createDeps()
|
|
@@ -164,6 +187,15 @@ describe("runCli", () => {
|
|
|
164
187
|
expect(calls.openUrl).toEqual(["http://localhost:4000"])
|
|
165
188
|
})
|
|
166
189
|
|
|
190
|
+
test("suppresses browser open for a ui-triggered restarted child", async () => {
|
|
191
|
+
process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] = "1"
|
|
192
|
+
const { calls, deps } = createDeps()
|
|
193
|
+
|
|
194
|
+
await runCli(["--port", "4000"], deps)
|
|
195
|
+
|
|
196
|
+
expect(calls.openUrl).toEqual([])
|
|
197
|
+
})
|
|
198
|
+
|
|
167
199
|
test("returns restarting when a newer version is available", async () => {
|
|
168
200
|
const { calls, deps } = createDeps({
|
|
169
201
|
fetchLatestVersion: async (packageName) => {
|
|
@@ -174,7 +206,7 @@ describe("runCli", () => {
|
|
|
174
206
|
|
|
175
207
|
const result = await runCli(["--port", "4000", "--no-open"], deps)
|
|
176
208
|
|
|
177
|
-
expect(result).toEqual({ kind: "restarting" })
|
|
209
|
+
expect(result).toEqual({ kind: "restarting", reason: "startup_update" })
|
|
178
210
|
expect(calls.installVersion).toEqual([{ packageName: "kanna-code", version: "0.4.0" }])
|
|
179
211
|
expect(calls.startServer).toEqual([])
|
|
180
212
|
})
|
|
@@ -187,7 +219,12 @@ describe("runCli", () => {
|
|
|
187
219
|
},
|
|
188
220
|
installVersion: (packageName, version) => {
|
|
189
221
|
calls.installVersion.push({ packageName, version })
|
|
190
|
-
return
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
errorCode: "install_failed",
|
|
225
|
+
userTitle: "Update failed",
|
|
226
|
+
userMessage: "Kanna could not install the update. Try again later.",
|
|
227
|
+
}
|
|
191
228
|
},
|
|
192
229
|
})
|
|
193
230
|
|
|
@@ -2,7 +2,9 @@ import process from "node:process"
|
|
|
2
2
|
import { spawnSync } from "node:child_process"
|
|
3
3
|
import { hasCommand, spawnDetached } from "./process-utils"
|
|
4
4
|
import { APP_NAME, CLI_COMMAND, getDataDirDisplay, LOG_PREFIX, PACKAGE_NAME } from "../shared/branding"
|
|
5
|
+
import type { UpdateInstallErrorCode } from "../shared/types"
|
|
5
6
|
import { PROD_SERVER_PORT } from "../shared/ports"
|
|
7
|
+
import { CLI_SUPPRESS_OPEN_ONCE_ENV_VAR } from "./restart"
|
|
6
8
|
|
|
7
9
|
export interface CliOptions {
|
|
8
10
|
port: number
|
|
@@ -13,7 +15,7 @@ export interface CliOptions {
|
|
|
13
15
|
export interface CliUpdateOptions {
|
|
14
16
|
version: string
|
|
15
17
|
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
16
|
-
installVersion: (packageName: string, version: string) =>
|
|
18
|
+
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
17
19
|
argv: string[]
|
|
18
20
|
command: string
|
|
19
21
|
}
|
|
@@ -25,6 +27,7 @@ export interface StartedCli {
|
|
|
25
27
|
|
|
26
28
|
export interface RestartingCli {
|
|
27
29
|
kind: "restarting"
|
|
30
|
+
reason: "startup_update" | "ui_update"
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface ExitedCli {
|
|
@@ -39,12 +42,19 @@ export interface CliRuntimeDeps {
|
|
|
39
42
|
bunVersion: string
|
|
40
43
|
startServer: (options: CliOptions & { update: CliUpdateOptions }) => Promise<{ port: number; stop: () => Promise<void> }>
|
|
41
44
|
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
42
|
-
installVersion: (packageName: string, version: string) =>
|
|
45
|
+
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
43
46
|
openUrl: (url: string) => void
|
|
44
47
|
log: (message: string) => void
|
|
45
48
|
warn: (message: string) => void
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
export interface UpdateInstallAttemptResult {
|
|
52
|
+
ok: boolean
|
|
53
|
+
errorCode: UpdateInstallErrorCode | null
|
|
54
|
+
userTitle: string | null
|
|
55
|
+
userMessage: string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
type ParsedArgs =
|
|
49
59
|
| { kind: "run"; options: CliOptions }
|
|
50
60
|
| { kind: "help" }
|
|
@@ -156,13 +166,17 @@ async function maybeSelfUpdate(_argv: string[], deps: CliRuntimeDeps) {
|
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
deps.log(`${LOG_PREFIX} installing ${PACKAGE_NAME}@${latestVersion}`)
|
|
159
|
-
|
|
169
|
+
const installResult = deps.installVersion(PACKAGE_NAME, latestVersion)
|
|
170
|
+
if (!installResult.ok) {
|
|
160
171
|
deps.warn(`${LOG_PREFIX} update failed, continuing current version`)
|
|
172
|
+
if (installResult.userMessage) {
|
|
173
|
+
deps.warn(`${LOG_PREFIX} ${installResult.userMessage}`)
|
|
174
|
+
}
|
|
161
175
|
return null
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
deps.log(`${LOG_PREFIX} restarting into updated version`)
|
|
165
|
-
return "
|
|
179
|
+
return "startup_update"
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliRunResult> {
|
|
@@ -183,7 +197,7 @@ export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliR
|
|
|
183
197
|
|
|
184
198
|
const shouldRestart = await maybeSelfUpdate(argv, deps)
|
|
185
199
|
if (shouldRestart !== null) {
|
|
186
|
-
return { kind: "restarting" }
|
|
200
|
+
return { kind: "restarting", reason: shouldRestart }
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
const { port, stop } = await deps.startServer({
|
|
@@ -202,7 +216,8 @@ export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliR
|
|
|
202
216
|
deps.log(`${LOG_PREFIX} listening on ${url}`)
|
|
203
217
|
deps.log(`${LOG_PREFIX} data dir: ${getDataDirDisplay()}`)
|
|
204
218
|
|
|
205
|
-
|
|
219
|
+
const suppressOpenBrowser = process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] === "1"
|
|
220
|
+
if (parsedArgs.options.openBrowser && !suppressOpenBrowser) {
|
|
206
221
|
deps.openUrl(launchUrl)
|
|
207
222
|
}
|
|
208
223
|
|
|
@@ -238,8 +253,51 @@ export async function fetchLatestPackageVersion(packageName: string) {
|
|
|
238
253
|
return payload.version
|
|
239
254
|
}
|
|
240
255
|
|
|
256
|
+
export function classifyInstallVersionFailure(output: string): UpdateInstallAttemptResult {
|
|
257
|
+
const normalizedOutput = output.trim()
|
|
258
|
+
if (/No version matching .* found|failed to resolve/i.test(normalizedOutput)) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
errorCode: "version_not_live_yet",
|
|
262
|
+
userTitle: "Update not live yet",
|
|
263
|
+
userMessage: "This update is still propagating. Try again in a few minutes.",
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
errorCode: "install_failed",
|
|
270
|
+
userTitle: "Update failed",
|
|
271
|
+
userMessage: "Kanna could not install the update. Try again later.",
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
241
275
|
export function installPackageVersion(packageName: string, version: string) {
|
|
242
|
-
if (!hasCommand("bun"))
|
|
243
|
-
|
|
244
|
-
|
|
276
|
+
if (!hasCommand("bun")) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
errorCode: "command_missing",
|
|
280
|
+
userTitle: "Bun not found",
|
|
281
|
+
userMessage: "Kanna could not find Bun to install the update.",
|
|
282
|
+
} satisfies UpdateInstallAttemptResult
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const result = spawnSync("bun", ["install", "-g", `${packageName}@${version}`], {
|
|
286
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
287
|
+
encoding: "utf8",
|
|
288
|
+
})
|
|
289
|
+
const stdout = result.stdout ?? ""
|
|
290
|
+
const stderr = result.stderr ?? ""
|
|
291
|
+
if (stdout) process.stdout.write(stdout)
|
|
292
|
+
if (stderr) process.stderr.write(stderr)
|
|
293
|
+
if (result.status === 0) {
|
|
294
|
+
return {
|
|
295
|
+
ok: true,
|
|
296
|
+
errorCode: null,
|
|
297
|
+
userTitle: null,
|
|
298
|
+
userMessage: null,
|
|
299
|
+
} satisfies UpdateInstallAttemptResult
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return classifyInstallVersionFailure(`${stdout}\n${stderr}`)
|
|
245
303
|
}
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
CLI_CHILD_COMMAND_ENV_VAR,
|
|
7
7
|
CLI_CHILD_MODE,
|
|
8
8
|
CLI_CHILD_MODE_ENV_VAR,
|
|
9
|
+
CLI_SUPPRESS_OPEN_ONCE_ENV_VAR,
|
|
10
|
+
isUiUpdateRestart,
|
|
9
11
|
parseChildArgsEnv,
|
|
10
12
|
shouldRestartCliProcess,
|
|
11
13
|
} from "./restart"
|
|
@@ -23,12 +25,15 @@ function getChildProcessSpec() {
|
|
|
23
25
|
|
|
24
26
|
function spawnChild(argv: string[]) {
|
|
25
27
|
const childProcess = getChildProcessSpec()
|
|
28
|
+
const suppressOpenThisChild = suppressOpenOnNextChild
|
|
29
|
+
suppressOpenOnNextChild = false
|
|
26
30
|
return new Promise<ChildExit>((resolve, reject) => {
|
|
27
31
|
const child = spawn(childProcess.command, [...childProcess.args, ...argv], {
|
|
28
32
|
stdio: "inherit",
|
|
29
33
|
env: {
|
|
30
34
|
...process.env,
|
|
31
35
|
[CLI_CHILD_MODE_ENV_VAR]: CLI_CHILD_MODE,
|
|
36
|
+
...(suppressOpenThisChild ? { [CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]: "1" } : {}),
|
|
32
37
|
},
|
|
33
38
|
})
|
|
34
39
|
|
|
@@ -62,10 +67,12 @@ function spawnChild(argv: string[]) {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
const argv = process.argv.slice(2)
|
|
70
|
+
let suppressOpenOnNextChild = false
|
|
65
71
|
|
|
66
72
|
while (true) {
|
|
67
73
|
const result = await spawnChild(argv)
|
|
68
74
|
if (shouldRestartCliProcess(result.code, result.signal)) {
|
|
75
|
+
suppressOpenOnNextChild = isUiUpdateRestart(result.code, result.signal)
|
|
69
76
|
console.log(`${LOG_PREFIX} supervisor restarting ${CLI_COMMAND} in the same terminal session`)
|
|
70
77
|
continue
|
|
71
78
|
}
|
package/src/server/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
openUrl,
|
|
7
7
|
runCli,
|
|
8
8
|
} from "./cli-runtime"
|
|
9
|
-
import {
|
|
9
|
+
import { CLI_STARTUP_UPDATE_RESTART_EXIT_CODE, CLI_UI_UPDATE_RESTART_EXIT_CODE } from "./restart"
|
|
10
10
|
import { startKannaServer } from "./server"
|
|
11
11
|
|
|
12
12
|
// Read version from package.json at the package root
|
|
@@ -14,7 +14,7 @@ const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json(
|
|
|
14
14
|
const VERSION: string = pkg.version ?? "0.0.0"
|
|
15
15
|
|
|
16
16
|
const argv = process.argv.slice(2)
|
|
17
|
-
let resolveExitAction: ((action: "
|
|
17
|
+
let resolveExitAction: ((action: "ui_restart" | "exit") => void) | null = null
|
|
18
18
|
|
|
19
19
|
const result = await runCli(argv, {
|
|
20
20
|
version: VERSION,
|
|
@@ -25,7 +25,7 @@ const result = await runCli(argv, {
|
|
|
25
25
|
started.updateManager.onChange((snapshot) => {
|
|
26
26
|
if (snapshot.status !== "restart_pending") return
|
|
27
27
|
console.log(`${LOG_PREFIX} update installed, shutting down current process for restart`)
|
|
28
|
-
resolveExitAction?.("
|
|
28
|
+
resolveExitAction?.("ui_restart")
|
|
29
29
|
})
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -43,10 +43,10 @@ if (result.kind === "exited") {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (result.kind === "restarting") {
|
|
46
|
-
process.exit(
|
|
46
|
+
process.exit(result.reason === "startup_update" ? CLI_STARTUP_UPDATE_RESTART_EXIT_CODE : CLI_UI_UPDATE_RESTART_EXIT_CODE)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const exitAction = await new Promise<"
|
|
49
|
+
const exitAction = await new Promise<"ui_restart" | "exit">((resolve) => {
|
|
50
50
|
resolveExitAction = resolve
|
|
51
51
|
|
|
52
52
|
const shutdown = () => {
|
|
@@ -58,7 +58,7 @@ const exitAction = await new Promise<"restart" | "exit">((resolve) => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
await result.stop()
|
|
61
|
-
if (exitAction === "
|
|
61
|
+
if (exitAction === "ui_restart") {
|
|
62
62
|
console.log(`${LOG_PREFIX} current process stopped, handing restart back to supervisor`)
|
|
63
63
|
}
|
|
64
|
-
process.exit(exitAction === "
|
|
64
|
+
process.exit(exitAction === "ui_restart" ? CLI_UI_UPDATE_RESTART_EXIT_CODE : 0)
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CLI_CHILD_ARGS_ENV_VAR,
|
|
4
|
+
CLI_STARTUP_UPDATE_RESTART_EXIT_CODE,
|
|
5
|
+
CLI_UI_UPDATE_RESTART_EXIT_CODE,
|
|
6
|
+
isUiUpdateRestart,
|
|
7
|
+
parseChildArgsEnv,
|
|
8
|
+
shouldRestartCliProcess,
|
|
9
|
+
} from "./restart"
|
|
3
10
|
|
|
4
11
|
describe("shouldRestartCliProcess", () => {
|
|
5
12
|
test("restarts only for the sentinel exit code without a signal", () => {
|
|
6
|
-
expect(shouldRestartCliProcess(
|
|
13
|
+
expect(shouldRestartCliProcess(CLI_STARTUP_UPDATE_RESTART_EXIT_CODE, null)).toBe(true)
|
|
14
|
+
expect(shouldRestartCliProcess(CLI_UI_UPDATE_RESTART_EXIT_CODE, null)).toBe(true)
|
|
7
15
|
expect(shouldRestartCliProcess(0, null)).toBe(false)
|
|
8
16
|
expect(shouldRestartCliProcess(1, null)).toBe(false)
|
|
9
|
-
expect(shouldRestartCliProcess(
|
|
17
|
+
expect(shouldRestartCliProcess(CLI_STARTUP_UPDATE_RESTART_EXIT_CODE, "SIGTERM")).toBe(false)
|
|
18
|
+
expect(isUiUpdateRestart(CLI_UI_UPDATE_RESTART_EXIT_CODE, null)).toBe(true)
|
|
19
|
+
expect(isUiUpdateRestart(CLI_STARTUP_UPDATE_RESTART_EXIT_CODE, null)).toBe(false)
|
|
10
20
|
})
|
|
11
21
|
|
|
12
22
|
test("parses configured child args from the environment", () => {
|
package/src/server/restart.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
export const CLI_CHILD_MODE_ENV_VAR = "KANNA_CLI_MODE"
|
|
2
2
|
export const CLI_CHILD_MODE = "child"
|
|
3
|
-
export const
|
|
3
|
+
export const CLI_STARTUP_UPDATE_RESTART_EXIT_CODE = 75
|
|
4
|
+
export const CLI_UI_UPDATE_RESTART_EXIT_CODE = 76
|
|
4
5
|
export const CLI_CHILD_COMMAND_ENV_VAR = "KANNA_CLI_CHILD_COMMAND"
|
|
5
6
|
export const CLI_CHILD_ARGS_ENV_VAR = "KANNA_CLI_CHILD_ARGS"
|
|
7
|
+
export const CLI_SUPPRESS_OPEN_ONCE_ENV_VAR = "KANNA_SUPPRESS_OPEN_ONCE"
|
|
6
8
|
|
|
7
9
|
export function shouldRestartCliProcess(code: number | null, signal: NodeJS.Signals | null) {
|
|
8
|
-
return signal === null && code ===
|
|
10
|
+
return signal === null && (code === CLI_STARTUP_UPDATE_RESTART_EXIT_CODE || code === CLI_UI_UPDATE_RESTART_EXIT_CODE)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isUiUpdateRestart(code: number | null, signal: NodeJS.Signals | null) {
|
|
14
|
+
return signal === null && code === CLI_UI_UPDATE_RESTART_EXIT_CODE
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export function parseChildArgsEnv(value: string | undefined) {
|
package/src/server/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { KeybindingsManager } from "./keybindings"
|
|
|
7
7
|
import { getMachineDisplayName } from "./machine-name"
|
|
8
8
|
import { TerminalManager } from "./terminal-manager"
|
|
9
9
|
import { UpdateManager } from "./update-manager"
|
|
10
|
+
import type { UpdateInstallAttemptResult } from "./cli-runtime"
|
|
10
11
|
import { createWsRouter, type ClientState } from "./ws-router"
|
|
11
12
|
|
|
12
13
|
export interface StartKannaServerOptions {
|
|
@@ -15,7 +16,7 @@ export interface StartKannaServerOptions {
|
|
|
15
16
|
update?: {
|
|
16
17
|
version: string
|
|
17
18
|
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
18
|
-
installVersion: (packageName: string, version: string) =>
|
|
19
|
+
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -6,7 +6,12 @@ describe("UpdateManager", () => {
|
|
|
6
6
|
const manager = new UpdateManager({
|
|
7
7
|
currentVersion: "0.12.0",
|
|
8
8
|
fetchLatestVersion: async () => "0.13.0",
|
|
9
|
-
installVersion: () =>
|
|
9
|
+
installVersion: () => ({
|
|
10
|
+
ok: true,
|
|
11
|
+
errorCode: null,
|
|
12
|
+
userTitle: null,
|
|
13
|
+
userMessage: null,
|
|
14
|
+
}),
|
|
10
15
|
})
|
|
11
16
|
|
|
12
17
|
const snapshot = await manager.checkForUpdates({ force: true })
|
|
@@ -25,7 +30,12 @@ describe("UpdateManager", () => {
|
|
|
25
30
|
calls += 1
|
|
26
31
|
return calls === 1 ? "0.12.1" : "0.13.0"
|
|
27
32
|
},
|
|
28
|
-
installVersion: () =>
|
|
33
|
+
installVersion: () => ({
|
|
34
|
+
ok: true,
|
|
35
|
+
errorCode: null,
|
|
36
|
+
userTitle: null,
|
|
37
|
+
userMessage: null,
|
|
38
|
+
}),
|
|
29
39
|
})
|
|
30
40
|
|
|
31
41
|
await manager.checkForUpdates()
|
|
@@ -42,13 +52,24 @@ describe("UpdateManager", () => {
|
|
|
42
52
|
fetchLatestVersion: async () => "0.13.0",
|
|
43
53
|
installVersion: (_packageName, version) => {
|
|
44
54
|
installedVersion = version
|
|
45
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
errorCode: "version_not_live_yet",
|
|
58
|
+
userTitle: "Update not live yet",
|
|
59
|
+
userMessage: "This update is still propagating. Try again in a few minutes.",
|
|
60
|
+
}
|
|
46
61
|
},
|
|
47
62
|
})
|
|
48
63
|
|
|
49
64
|
const result = await manager.installUpdate()
|
|
50
65
|
|
|
51
|
-
expect(result).toEqual({
|
|
66
|
+
expect(result).toEqual({
|
|
67
|
+
ok: false,
|
|
68
|
+
action: "restart",
|
|
69
|
+
errorCode: "version_not_live_yet",
|
|
70
|
+
userTitle: "Update not live yet",
|
|
71
|
+
userMessage: "This update is still propagating. Try again in a few minutes.",
|
|
72
|
+
})
|
|
52
73
|
expect(installedVersion === "0.13.0").toBe(true)
|
|
53
74
|
expect(manager.getSnapshot().status).toBe("error")
|
|
54
75
|
expect(manager.getSnapshot().currentVersion).toBe("0.12.0")
|
|
@@ -58,7 +79,12 @@ describe("UpdateManager", () => {
|
|
|
58
79
|
const manager = new UpdateManager({
|
|
59
80
|
currentVersion: "0.12.0",
|
|
60
81
|
fetchLatestVersion: async () => "9.9.9",
|
|
61
|
-
installVersion: () =>
|
|
82
|
+
installVersion: () => ({
|
|
83
|
+
ok: true,
|
|
84
|
+
errorCode: null,
|
|
85
|
+
userTitle: null,
|
|
86
|
+
userMessage: null,
|
|
87
|
+
}),
|
|
62
88
|
devMode: true,
|
|
63
89
|
})
|
|
64
90
|
|
|
@@ -69,7 +95,13 @@ describe("UpdateManager", () => {
|
|
|
69
95
|
})
|
|
70
96
|
|
|
71
97
|
const result = await manager.installUpdate()
|
|
72
|
-
expect(result).toEqual({
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
ok: true,
|
|
100
|
+
action: "restart",
|
|
101
|
+
errorCode: null,
|
|
102
|
+
userTitle: null,
|
|
103
|
+
userMessage: null,
|
|
104
|
+
})
|
|
73
105
|
expect(manager.getSnapshot().status).toBe("restart_pending")
|
|
74
106
|
})
|
|
75
107
|
})
|
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import type { UpdateSnapshot } from "../shared/types"
|
|
1
|
+
import type { UpdateInstallResult, UpdateSnapshot } from "../shared/types"
|
|
2
2
|
import { PACKAGE_NAME } from "../shared/branding"
|
|
3
|
-
import { compareVersions } from "./cli-runtime"
|
|
3
|
+
import { compareVersions, type UpdateInstallAttemptResult } from "./cli-runtime"
|
|
4
4
|
|
|
5
5
|
const UPDATE_CACHE_TTL_MS = 5 * 60 * 1000
|
|
6
6
|
|
|
7
7
|
export interface UpdateManagerDeps {
|
|
8
8
|
currentVersion: string
|
|
9
9
|
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
10
|
-
installVersion: (packageName: string, version: string) =>
|
|
10
|
+
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
11
11
|
devMode?: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface UpdateInstallResult {
|
|
15
|
-
ok: boolean
|
|
16
|
-
action: "restart" | "reload"
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
export class UpdateManager {
|
|
20
15
|
private readonly deps: UpdateManagerDeps
|
|
21
16
|
private readonly listeners = new Set<(snapshot: UpdateSnapshot) => void>()
|
|
@@ -100,6 +95,9 @@ export class UpdateManager {
|
|
|
100
95
|
return {
|
|
101
96
|
ok: true,
|
|
102
97
|
action: "restart",
|
|
98
|
+
errorCode: null,
|
|
99
|
+
userTitle: null,
|
|
100
|
+
userMessage: null,
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
|
|
@@ -107,6 +105,9 @@ export class UpdateManager {
|
|
|
107
105
|
return {
|
|
108
106
|
ok: this.snapshot.updateAvailable,
|
|
109
107
|
action: "restart",
|
|
108
|
+
errorCode: null,
|
|
109
|
+
userTitle: null,
|
|
110
|
+
userMessage: null,
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -159,6 +160,9 @@ export class UpdateManager {
|
|
|
159
160
|
return {
|
|
160
161
|
ok: false,
|
|
161
162
|
action: "restart",
|
|
163
|
+
errorCode: null,
|
|
164
|
+
userTitle: null,
|
|
165
|
+
userMessage: null,
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
}
|
|
@@ -179,19 +183,25 @@ export class UpdateManager {
|
|
|
179
183
|
return {
|
|
180
184
|
ok: false,
|
|
181
185
|
action: "restart",
|
|
186
|
+
errorCode: "install_failed",
|
|
187
|
+
userTitle: "Update failed",
|
|
188
|
+
userMessage: "Kanna could not determine which version to install.",
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
const installed = this.deps.installVersion(PACKAGE_NAME, targetVersion)
|
|
186
|
-
if (!installed) {
|
|
193
|
+
if (!installed.ok) {
|
|
187
194
|
this.setSnapshot({
|
|
188
195
|
...this.snapshot,
|
|
189
196
|
status: "error",
|
|
190
|
-
error: "Unable to install the latest version.",
|
|
197
|
+
error: installed.userMessage ?? "Unable to install the latest version.",
|
|
191
198
|
})
|
|
192
199
|
return {
|
|
193
200
|
ok: false,
|
|
194
201
|
action: "restart",
|
|
202
|
+
errorCode: installed.errorCode,
|
|
203
|
+
userTitle: installed.userTitle,
|
|
204
|
+
userMessage: installed.userMessage,
|
|
195
205
|
}
|
|
196
206
|
}
|
|
197
207
|
|
|
@@ -205,6 +215,9 @@ export class UpdateManager {
|
|
|
205
215
|
return {
|
|
206
216
|
ok: true,
|
|
207
217
|
action: "restart",
|
|
218
|
+
errorCode: null,
|
|
219
|
+
userTitle: null,
|
|
220
|
+
userMessage: null,
|
|
208
221
|
}
|
|
209
222
|
}
|
|
210
223
|
|
|
@@ -281,7 +281,13 @@ describe("ws-router", () => {
|
|
|
281
281
|
return this.snapshot
|
|
282
282
|
},
|
|
283
283
|
async installUpdate() {
|
|
284
|
-
return
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
action: "restart",
|
|
287
|
+
errorCode: "version_not_live_yet",
|
|
288
|
+
userTitle: "Update not live yet",
|
|
289
|
+
userMessage: "This update is still propagating. Try again in a few minutes.",
|
|
290
|
+
}
|
|
285
291
|
},
|
|
286
292
|
}
|
|
287
293
|
|
|
@@ -351,5 +357,31 @@ describe("ws-router", () => {
|
|
|
351
357
|
installAction: "restart",
|
|
352
358
|
},
|
|
353
359
|
})
|
|
360
|
+
|
|
361
|
+
router.handleMessage(
|
|
362
|
+
ws as never,
|
|
363
|
+
JSON.stringify({
|
|
364
|
+
v: 1,
|
|
365
|
+
type: "command",
|
|
366
|
+
id: "update-install-1",
|
|
367
|
+
command: {
|
|
368
|
+
type: "update.install",
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
await Promise.resolve()
|
|
374
|
+
expect(ws.sent[2]).toEqual({
|
|
375
|
+
v: PROTOCOL_VERSION,
|
|
376
|
+
type: "ack",
|
|
377
|
+
id: "update-install-1",
|
|
378
|
+
result: {
|
|
379
|
+
ok: false,
|
|
380
|
+
action: "restart",
|
|
381
|
+
errorCode: "version_not_live_yet",
|
|
382
|
+
userTitle: "Update not live yet",
|
|
383
|
+
userMessage: "This update is still propagating. Try again in a few minutes.",
|
|
384
|
+
},
|
|
385
|
+
})
|
|
354
386
|
})
|
|
355
387
|
})
|
package/src/shared/types.ts
CHANGED
|
@@ -186,6 +186,19 @@ export interface UpdateSnapshot {
|
|
|
186
186
|
installAction: "restart" | "reload"
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
export type UpdateInstallErrorCode =
|
|
190
|
+
| "version_not_live_yet"
|
|
191
|
+
| "install_failed"
|
|
192
|
+
| "command_missing"
|
|
193
|
+
|
|
194
|
+
export interface UpdateInstallResult {
|
|
195
|
+
ok: boolean
|
|
196
|
+
action: "restart" | "reload"
|
|
197
|
+
errorCode: UpdateInstallErrorCode | null
|
|
198
|
+
userTitle: string | null
|
|
199
|
+
userMessage: string | null
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
export type KeybindingAction =
|
|
190
203
|
| "toggleEmbeddedTerminal"
|
|
191
204
|
| "toggleRightSidebar"
|