@kimchi-dev/kimchictl 0.0.2

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.
Files changed (231) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +105 -0
  3. package/bin/kimchictl.mjs +17 -0
  4. package/dist/api/http.d.ts +7 -0
  5. package/dist/api/http.d.ts.map +1 -0
  6. package/dist/api/http.js +60 -0
  7. package/dist/api/http.js.map +1 -0
  8. package/dist/api/keys.d.ts +7 -0
  9. package/dist/api/keys.d.ts.map +1 -0
  10. package/dist/api/keys.js +28 -0
  11. package/dist/api/keys.js.map +1 -0
  12. package/dist/api/me.d.ts +12 -0
  13. package/dist/api/me.d.ts.map +1 -0
  14. package/dist/api/me.js +28 -0
  15. package/dist/api/me.js.map +1 -0
  16. package/dist/api/resolver.d.ts +26 -0
  17. package/dist/api/resolver.d.ts.map +1 -0
  18. package/dist/api/resolver.js +62 -0
  19. package/dist/api/resolver.js.map +1 -0
  20. package/dist/api/resources.d.ts +16 -0
  21. package/dist/api/resources.d.ts.map +1 -0
  22. package/dist/api/resources.js +96 -0
  23. package/dist/api/resources.js.map +1 -0
  24. package/dist/api/templates.d.ts +19 -0
  25. package/dist/api/templates.d.ts.map +1 -0
  26. package/dist/api/templates.js +25 -0
  27. package/dist/api/templates.js.map +1 -0
  28. package/dist/api/tokens.d.ts +23 -0
  29. package/dist/api/tokens.d.ts.map +1 -0
  30. package/dist/api/tokens.js +51 -0
  31. package/dist/api/tokens.js.map +1 -0
  32. package/dist/api/types.d.ts +34 -0
  33. package/dist/api/types.d.ts.map +1 -0
  34. package/dist/api/types.js +32 -0
  35. package/dist/api/types.js.map +1 -0
  36. package/dist/api/wait.d.ts +17 -0
  37. package/dist/api/wait.d.ts.map +1 -0
  38. package/dist/api/wait.js +37 -0
  39. package/dist/api/wait.js.map +1 -0
  40. package/dist/api/workspaces.d.ts +33 -0
  41. package/dist/api/workspaces.d.ts.map +1 -0
  42. package/dist/api/workspaces.js +159 -0
  43. package/dist/api/workspaces.js.map +1 -0
  44. package/dist/auth/auth-file.d.ts +37 -0
  45. package/dist/auth/auth-file.d.ts.map +1 -0
  46. package/dist/auth/auth-file.js +100 -0
  47. package/dist/auth/auth-file.js.map +1 -0
  48. package/dist/auth/browser-login.d.ts +45 -0
  49. package/dist/auth/browser-login.d.ts.map +1 -0
  50. package/dist/auth/browser-login.js +195 -0
  51. package/dist/auth/browser-login.js.map +1 -0
  52. package/dist/auth/config-file.d.ts +4 -0
  53. package/dist/auth/config-file.d.ts.map +1 -0
  54. package/dist/auth/config-file.js +52 -0
  55. package/dist/auth/config-file.js.map +1 -0
  56. package/dist/auth/models-providers.d.ts +17 -0
  57. package/dist/auth/models-providers.d.ts.map +1 -0
  58. package/dist/auth/models-providers.js +63 -0
  59. package/dist/auth/models-providers.js.map +1 -0
  60. package/dist/auth/paths.d.ts +15 -0
  61. package/dist/auth/paths.d.ts.map +1 -0
  62. package/dist/auth/paths.js +25 -0
  63. package/dist/auth/paths.js.map +1 -0
  64. package/dist/auth/provider-ids.d.ts +4 -0
  65. package/dist/auth/provider-ids.d.ts.map +1 -0
  66. package/dist/auth/provider-ids.js +12 -0
  67. package/dist/auth/provider-ids.js.map +1 -0
  68. package/dist/auth/resolve.d.ts +19 -0
  69. package/dist/auth/resolve.d.ts.map +1 -0
  70. package/dist/auth/resolve.js +37 -0
  71. package/dist/auth/resolve.js.map +1 -0
  72. package/dist/cli.d.ts +2 -0
  73. package/dist/cli.d.ts.map +1 -0
  74. package/dist/cli.js +15 -0
  75. package/dist/cli.js.map +1 -0
  76. package/dist/commands/completion.d.ts +2 -0
  77. package/dist/commands/completion.d.ts.map +1 -0
  78. package/dist/commands/completion.js +71 -0
  79. package/dist/commands/completion.js.map +1 -0
  80. package/dist/commands/flags.d.ts +16 -0
  81. package/dist/commands/flags.d.ts.map +1 -0
  82. package/dist/commands/flags.js +23 -0
  83. package/dist/commands/flags.js.map +1 -0
  84. package/dist/commands/guard.d.ts +10 -0
  85. package/dist/commands/guard.d.ts.map +1 -0
  86. package/dist/commands/guard.js +41 -0
  87. package/dist/commands/guard.js.map +1 -0
  88. package/dist/commands/help.d.ts +4 -0
  89. package/dist/commands/help.d.ts.map +1 -0
  90. package/dist/commands/help.js +19 -0
  91. package/dist/commands/help.js.map +1 -0
  92. package/dist/commands/login.d.ts +18 -0
  93. package/dist/commands/login.d.ts.map +1 -0
  94. package/dist/commands/login.js +80 -0
  95. package/dist/commands/login.js.map +1 -0
  96. package/dist/commands/logout.d.ts +14 -0
  97. package/dist/commands/logout.d.ts.map +1 -0
  98. package/dist/commands/logout.js +41 -0
  99. package/dist/commands/logout.js.map +1 -0
  100. package/dist/commands/prompt.d.ts +9 -0
  101. package/dist/commands/prompt.d.ts.map +1 -0
  102. package/dist/commands/prompt.js +21 -0
  103. package/dist/commands/prompt.js.map +1 -0
  104. package/dist/commands/registry.d.ts +16 -0
  105. package/dist/commands/registry.d.ts.map +1 -0
  106. package/dist/commands/registry.js +35 -0
  107. package/dist/commands/registry.js.map +1 -0
  108. package/dist/commands/ssh.d.ts +15 -0
  109. package/dist/commands/ssh.d.ts.map +1 -0
  110. package/dist/commands/ssh.js +90 -0
  111. package/dist/commands/ssh.js.map +1 -0
  112. package/dist/commands/table.d.ts +3 -0
  113. package/dist/commands/table.d.ts.map +1 -0
  114. package/dist/commands/table.js +10 -0
  115. package/dist/commands/table.js.map +1 -0
  116. package/dist/commands/update.d.ts +16 -0
  117. package/dist/commands/update.d.ts.map +1 -0
  118. package/dist/commands/update.js +57 -0
  119. package/dist/commands/update.js.map +1 -0
  120. package/dist/commands/version.d.ts +2 -0
  121. package/dist/commands/version.d.ts.map +1 -0
  122. package/dist/commands/version.js +6 -0
  123. package/dist/commands/version.js.map +1 -0
  124. package/dist/commands/whoami.d.ts +7 -0
  125. package/dist/commands/whoami.d.ts.map +1 -0
  126. package/dist/commands/whoami.js +30 -0
  127. package/dist/commands/whoami.js.map +1 -0
  128. package/dist/commands/workspace.d.ts +15 -0
  129. package/dist/commands/workspace.d.ts.map +1 -0
  130. package/dist/commands/workspace.js +206 -0
  131. package/dist/commands/workspace.js.map +1 -0
  132. package/dist/run.d.ts +10 -0
  133. package/dist/run.d.ts.map +1 -0
  134. package/dist/run.js +30 -0
  135. package/dist/run.js.map +1 -0
  136. package/dist/ssh/bridge.d.ts +25 -0
  137. package/dist/ssh/bridge.d.ts.map +1 -0
  138. package/dist/ssh/bridge.js +95 -0
  139. package/dist/ssh/bridge.js.map +1 -0
  140. package/dist/ssh/config.d.ts +65 -0
  141. package/dist/ssh/config.d.ts.map +1 -0
  142. package/dist/ssh/config.js +198 -0
  143. package/dist/ssh/config.js.map +1 -0
  144. package/dist/ssh/connect.d.ts +27 -0
  145. package/dist/ssh/connect.d.ts.map +1 -0
  146. package/dist/ssh/connect.js +67 -0
  147. package/dist/ssh/connect.js.map +1 -0
  148. package/dist/ssh/tunnel.d.ts +29 -0
  149. package/dist/ssh/tunnel.d.ts.map +1 -0
  150. package/dist/ssh/tunnel.js +36 -0
  151. package/dist/ssh/tunnel.js.map +1 -0
  152. package/dist/test-support.d.ts +30 -0
  153. package/dist/test-support.d.ts.map +1 -0
  154. package/dist/test-support.js +67 -0
  155. package/dist/test-support.js.map +1 -0
  156. package/dist/updates/check.d.ts +18 -0
  157. package/dist/updates/check.d.ts.map +1 -0
  158. package/dist/updates/check.js +90 -0
  159. package/dist/updates/check.js.map +1 -0
  160. package/dist/updates/self.d.ts +36 -0
  161. package/dist/updates/self.d.ts.map +1 -0
  162. package/dist/updates/self.js +103 -0
  163. package/dist/updates/self.js.map +1 -0
  164. package/dist/version.d.ts +7 -0
  165. package/dist/version.d.ts.map +1 -0
  166. package/dist/version.js +7 -0
  167. package/dist/version.js.map +1 -0
  168. package/package.json +77 -0
  169. package/src/api/http.ts +70 -0
  170. package/src/api/keys.ts +40 -0
  171. package/src/api/me.ts +48 -0
  172. package/src/api/resolver.test.ts +109 -0
  173. package/src/api/resolver.ts +72 -0
  174. package/src/api/resources.test.ts +56 -0
  175. package/src/api/resources.ts +92 -0
  176. package/src/api/templates.ts +28 -0
  177. package/src/api/tokens.ts +80 -0
  178. package/src/api/types.ts +50 -0
  179. package/src/api/wait.ts +45 -0
  180. package/src/api/workspaces.ts +234 -0
  181. package/src/auth/auth-file.test.ts +176 -0
  182. package/src/auth/auth-file.ts +103 -0
  183. package/src/auth/browser-login.test.ts +93 -0
  184. package/src/auth/browser-login.ts +247 -0
  185. package/src/auth/config-file.test.ts +68 -0
  186. package/src/auth/config-file.ts +50 -0
  187. package/src/auth/models-providers.test.ts +63 -0
  188. package/src/auth/models-providers.ts +76 -0
  189. package/src/auth/paths.ts +28 -0
  190. package/src/auth/provider-ids.ts +15 -0
  191. package/src/auth/resolve.test.ts +72 -0
  192. package/src/auth/resolve.ts +46 -0
  193. package/src/cli.ts +16 -0
  194. package/src/commands/completion.test.ts +31 -0
  195. package/src/commands/completion.ts +77 -0
  196. package/src/commands/flags.ts +29 -0
  197. package/src/commands/guard.ts +44 -0
  198. package/src/commands/help.ts +20 -0
  199. package/src/commands/login.test.ts +159 -0
  200. package/src/commands/login.ts +94 -0
  201. package/src/commands/logout.test.ts +89 -0
  202. package/src/commands/logout.ts +53 -0
  203. package/src/commands/prompt.ts +21 -0
  204. package/src/commands/registry.ts +50 -0
  205. package/src/commands/ssh.test.ts +101 -0
  206. package/src/commands/ssh.ts +112 -0
  207. package/src/commands/table.test.ts +24 -0
  208. package/src/commands/table.ts +10 -0
  209. package/src/commands/update.test.ts +91 -0
  210. package/src/commands/update.ts +78 -0
  211. package/src/commands/version.ts +6 -0
  212. package/src/commands/whoami.test.ts +72 -0
  213. package/src/commands/whoami.ts +36 -0
  214. package/src/commands/workspace.test.ts +360 -0
  215. package/src/commands/workspace.ts +250 -0
  216. package/src/run.test.ts +53 -0
  217. package/src/run.ts +34 -0
  218. package/src/ssh/bridge.test.ts +70 -0
  219. package/src/ssh/bridge.ts +110 -0
  220. package/src/ssh/config.test.ts +213 -0
  221. package/src/ssh/config.ts +233 -0
  222. package/src/ssh/connect.test.ts +123 -0
  223. package/src/ssh/connect.ts +98 -0
  224. package/src/ssh/tunnel.test.ts +119 -0
  225. package/src/ssh/tunnel.ts +64 -0
  226. package/src/test-support.ts +77 -0
  227. package/src/updates/check.test.ts +99 -0
  228. package/src/updates/check.ts +117 -0
  229. package/src/updates/self.test.ts +117 -0
  230. package/src/updates/self.ts +131 -0
  231. package/src/version.ts +6 -0
@@ -0,0 +1,117 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"
2
+ import { dirname, join } from "node:path"
3
+ import { fetchWithTimeout } from "../api/http.js"
4
+ import { resolveSshPaths } from "../ssh/config.js"
5
+ import { VERSION } from "../version.js"
6
+
7
+ /**
8
+ * Daily background update check against the GitHub Releases API.
9
+ *
10
+ * The check must never block or fail a real command: every failure mode
11
+ * resolves to "no notification". Results cache in
12
+ * ~/.config/kimchi/kimchictl/update-check.json for 24h.
13
+ */
14
+
15
+ const LATEST_RELEASE_URL = "https://api.github.com/repos/getkimchi/kimchictl/releases/latest"
16
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
17
+ const FETCH_TIMEOUT_MS = 2500
18
+
19
+ interface UpdateCache {
20
+ latestVersion: string
21
+ checkedAt: string
22
+ }
23
+
24
+ /** Semantic-ish comparison: 0.10.0 > 0.9.9. Only digits matter; pre-releases compare numerically too. */
25
+ export function isNewer(latest: string, current: string): boolean {
26
+ const parse = (v: string) =>
27
+ v
28
+ .replace(/^v/, "")
29
+ .split(".")
30
+ .map((part) => Number.parseInt(part, 10) || 0)
31
+ const a = parse(latest)
32
+ const b = parse(current)
33
+ for (let i = 0; i < 3; i++) {
34
+ const diff = (a[i] ?? 0) - (b[i] ?? 0)
35
+ if (diff !== 0) return diff > 0
36
+ }
37
+ return false
38
+ }
39
+
40
+ export interface UpdateCheckResult {
41
+ latest: string
42
+ current: string
43
+ }
44
+
45
+ async function readCache(path: string): Promise<UpdateCache | undefined> {
46
+ try {
47
+ const raw = JSON.parse(await readFile(path, "utf-8")) as Partial<UpdateCache>
48
+ if (typeof raw.latestVersion === "string" && typeof raw.checkedAt === "string") {
49
+ return { latestVersion: raw.latestVersion, checkedAt: raw.checkedAt }
50
+ }
51
+ return undefined
52
+ } catch (err) {
53
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return undefined
54
+ return undefined // corrupt cache → treat as stale (refetched below)
55
+ }
56
+ }
57
+
58
+ async function writeCache(path: string, cache: UpdateCache): Promise<void> {
59
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
60
+ await writeFile(path, `${JSON.stringify(cache, null, 2)}\n`, { mode: 0o600 })
61
+ await chmod(path, 0o600)
62
+ }
63
+
64
+ export interface CheckForUpdateOptions {
65
+ env?: NodeJS.ProcessEnv
66
+ fetch?: typeof globalThis.fetch
67
+ now?: () => Date
68
+ /** Version to compare against — tests inject; defaults to the baked-in VERSION. */
69
+ currentVersion?: string
70
+ }
71
+
72
+ /** Returns {latest, current} when a newer release exists, otherwise undefined. Never throws. */
73
+ export async function checkForUpdate(options: CheckForUpdateOptions = {}): Promise<UpdateCheckResult | undefined> {
74
+ try {
75
+ const current = options.currentVersion ?? VERSION
76
+ // Source checkouts and dev builds are not versioned against releases.
77
+ if (current.endsWith("-dev")) return undefined
78
+
79
+ const env = options.env ?? process.env
80
+ if (env.KIMCHICTL_NO_UPDATE_CHECK) return undefined
81
+
82
+ const now = options.now ?? (() => new Date())
83
+ const cachePath = join(resolveSshPaths(env).home, "update-check.json")
84
+
85
+ const cached = await readCache(cachePath)
86
+ if (cached && now().getTime() - new Date(cached.checkedAt).getTime() < CHECK_INTERVAL_MS) {
87
+ return isNewer(cached.latestVersion, current) ? { latest: cached.latestVersion, current } : undefined
88
+ }
89
+
90
+ const fetchImpl = options.fetch ?? globalThis.fetch
91
+ const resp = await fetchWithTimeout(
92
+ LATEST_RELEASE_URL,
93
+ { headers: { "User-Agent": `kimchictl/${current}` } },
94
+ fetchImpl,
95
+ FETCH_TIMEOUT_MS,
96
+ )
97
+ if (!resp.ok) return undefined
98
+ const data = (await resp.json()) as { tag_name?: string }
99
+ if (typeof data.tag_name !== "string") return undefined
100
+
101
+ const latest = data.tag_name.replace(/^v/, "")
102
+ await writeCache(cachePath, { latestVersion: latest, checkedAt: now().toISOString() }).catch(() => {
103
+ // Cache writes are best-effort only.
104
+ })
105
+ return isNewer(latest, current) ? { latest, current } : undefined
106
+ } catch {
107
+ return undefined
108
+ }
109
+ }
110
+
111
+ /** Post the update hint to stderr. Extracted so cli.ts stays a one-liner. */
112
+ export async function maybeNotifyUpdate(options: CheckForUpdateOptions = {}): Promise<void> {
113
+ const result = await checkForUpdate(options)
114
+ if (result) {
115
+ console.error(`⬆ kimchictl ${result.latest} available (current ${result.current}) — update: kimchictl update`)
116
+ }
117
+ }
@@ -0,0 +1,117 @@
1
+ import { createHash } from "node:crypto"
2
+ import { writeFile } from "node:fs/promises"
3
+ import { afterEach, describe, expect, it } from "vitest"
4
+ import { cleanupTempDirs, makeTempDir } from "../test-support.js"
5
+ import { detectInstallMethod, platformAssetName, selfUpdateToVersion } from "./self.js"
6
+
7
+ afterEach(() => cleanupTempDirs())
8
+
9
+ describe("detectInstallMethod", () => {
10
+ it("recognizes Homebrew, runtimes, and plain binaries", () => {
11
+ expect(detectInstallMethod(() => "/opt/homebrew/Cellar/kimchictl/0.2.0/bin/kimchictl")).toBe("homebrew")
12
+ expect(detectInstallMethod(() => "/usr/local/bin/node")).toBe("runtime")
13
+ expect(detectInstallMethod(() => "/opt/homebrew/bin/bun")).toBe("runtime")
14
+ expect(detectInstallMethod(() => "/usr/local/bin/kimchictl")).toBe("binary")
15
+ expect(detectInstallMethod(() => "/home/u/.local/bin/kimchictl")).toBe("binary")
16
+ })
17
+ })
18
+
19
+ describe("platformAssetName", () => {
20
+ it("maps node platform/arch to release asset names", () => {
21
+ expect(platformAssetName("darwin", "arm64")).toBe("kimchictl-darwin-arm64")
22
+ expect(platformAssetName("linux", "x64")).toBe("kimchictl-linux-x64")
23
+ expect(() => platformAssetName("win32" as NodeJS.Platform, "x64")).toThrow("unsupported platform")
24
+ })
25
+ })
26
+
27
+ describe("selfUpdateToVersion", () => {
28
+ function scenario(options?: { withChecksums?: boolean; tamper?: boolean }) {
29
+ const tmp = makeTempDir()
30
+ const binary = Buffer.from("#!/bin/fake kimchictl binary\n")
31
+ const downloads = new Map<string, Buffer>()
32
+ // Checksum of the PRISTINE bytes; tamper must corrupt the served binary only.
33
+ if (options?.withChecksums !== false) {
34
+ downloads.set(
35
+ "checksums.txt",
36
+ Buffer.from(
37
+ `${createHash("sha256").update(binary).digest("hex")} kimchictl-${process.platform === "darwin" ? "darwin" : "linux"}-${process.arch === "arm64" ? "arm64" : "x64"}\n`,
38
+ ),
39
+ )
40
+ }
41
+ if (options?.tamper) binary[0] = 0x21
42
+ downloads.set("kimchictl-darwin-arm64", binary)
43
+ downloads.set("kimchictl-linux-arm64", binary)
44
+ downloads.set("kimchictl-darwin-x64", binary)
45
+ downloads.set("kimchictl-linux-x64", binary)
46
+
47
+ const written = new Map<string, Buffer>()
48
+ const renamed: string[][] = []
49
+ return {
50
+ tmp,
51
+ binary,
52
+ // Real download writes to dest — selfUpdateToVersion re-reads the staged
53
+ // file from disk when checksum-verifying.
54
+ download: async (url: string, dest: string) => {
55
+ const name = url.split("/").pop() ?? ""
56
+ const bytes = downloads.get(name)
57
+ if (!bytes) throw new Error(`404 ${name}`)
58
+ written.set(dest, bytes)
59
+ await writeFile(dest, bytes)
60
+ },
61
+ renamed,
62
+ renameFn: async (from: string, to: string) => {
63
+ renamed.push([from, to])
64
+ },
65
+ chmodFn: async () => {},
66
+ }
67
+ }
68
+
69
+ it("downloads the platform asset, verifies the checksum, and renames over the target", async () => {
70
+ const s = scenario()
71
+
72
+ await selfUpdateToVersion({
73
+ targetPath: "/usr/local/bin/kimchictl",
74
+ version: "0.3.0",
75
+ download: s.download,
76
+ renameFn: s.renameFn,
77
+ chmodFn: s.chmodFn,
78
+ tmpRoot: s.tmp,
79
+ })
80
+
81
+ const stagedContent = [...s.renamed][0]
82
+ expect(stagedContent?.[1]).toBe("/usr/local/bin/kimchictl")
83
+ expect(stagedContent?.[0]).toContain("kimchictl-update-")
84
+ })
85
+
86
+ it("aborts on checksum mismatch", async () => {
87
+ const s = scenario({ tamper: true })
88
+
89
+ await expect(
90
+ selfUpdateToVersion({
91
+ targetPath: "/usr/local/bin/kimchictl",
92
+ version: "0.3.0",
93
+ download: s.download,
94
+ renameFn: s.renameFn,
95
+ chmodFn: s.chmodFn,
96
+ tmpRoot: s.tmp,
97
+ }),
98
+ ).rejects.toThrow(/checksum mismatch/)
99
+
100
+ expect(s.renamed.length).toBe(0)
101
+ })
102
+
103
+ it("proceeds without verification when checksums.txt is absent (older releases)", async () => {
104
+ const s = scenario({ withChecksums: false })
105
+
106
+ await selfUpdateToVersion({
107
+ targetPath: "/usr/local/bin/kimchictl",
108
+ version: "0.3.0",
109
+ download: s.download,
110
+ renameFn: s.renameFn,
111
+ chmodFn: s.chmodFn,
112
+ tmpRoot: s.tmp,
113
+ })
114
+
115
+ expect(s.renamed.length).toBe(1)
116
+ })
117
+ })
@@ -0,0 +1,131 @@
1
+ import { createHash } from "node:crypto"
2
+ import { chmod, mkdtemp, readFile, rename, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import { basename, join } from "node:path"
5
+ import { fetchWithTimeout } from "../api/http.js"
6
+ import { VERSION } from "../version.js"
7
+
8
+ /**
9
+ * Self-update plumbing: detect how kimchictl was installed and, for binary
10
+ * installs, swap the executable in place from the latest GitHub release.
11
+ * Homebrew/npm/RTE (runtime) installs delegate to their own updaters.
12
+ */
13
+
14
+ export type InstallMethod = "binary" | "homebrew" | "runtime"
15
+
16
+ /** realpath seam: tests feed synthetic paths. */
17
+ export function detectInstallMethod(realpath: () => string): InstallMethod {
18
+ const target = realpath()
19
+ if (/\/(Cellar|linuxbrew)\/[^/]*kimchictl?\//.test(target) || /homebrew.*kimchictl/i.test(target)) {
20
+ return "homebrew"
21
+ }
22
+ const base = basename(target)
23
+ if (base === "node" || base === "bun" || base === "node.exe") {
24
+ return "runtime"
25
+ }
26
+ return "binary"
27
+ }
28
+
29
+ /** realpath of the running executable, best-effort. */
30
+ export function defaultInstallMethod(execPath = process.execPath): InstallMethod {
31
+ // bun-compiled binaries ARE the install (execPath is the binary); node/bun
32
+ // runtime hosts are npm/dev installs.
33
+ const base = basename(execPath)
34
+ if (base === "node" || base === "bun" || base === "node.exe") return "runtime"
35
+ return detectInstallMethod(() => execPath)
36
+ }
37
+
38
+ /** Release asset name for this platform, e.g. kimchictl-darwin-arm64. */
39
+ export function platformAssetName(platform: NodeJS.Platform = process.platform, arch: string = process.arch): string {
40
+ const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : undefined
41
+ const cpu = arch === "arm64" ? "arm64" : arch === "x64" ? "x64" : undefined
42
+ if (!os || !cpu) {
43
+ throw new Error(`unsupported platform for self-update: ${platform}/${arch}`)
44
+ }
45
+ return `kimchictl-${os}-${cpu}`
46
+ }
47
+
48
+ export class SelfUpdateError extends Error {
49
+ constructor(message: string) {
50
+ super(message)
51
+ this.name = "SelfUpdateError"
52
+ }
53
+ }
54
+
55
+ /** Distinct from a checksums-download failure: a verified mismatch always aborts. */
56
+ export class ChecksumMismatchError extends SelfUpdateError {
57
+ constructor(message: string) {
58
+ super(message)
59
+ this.name = "ChecksumMismatchError"
60
+ }
61
+ }
62
+
63
+ export interface SelfUpdateOptions {
64
+ targetPath: string
65
+ version: string // e.g. "0.2.0" — bare tag (release.yml tags are bare)
66
+ fetch?: typeof globalThis.fetch
67
+ download?: (url: string, destPath: string) => Promise<void>
68
+ chmodFn?: (path: string, mode: number) => Promise<void>
69
+ renameFn?: (from: string, to: string) => Promise<void>
70
+ tmpRoot?: string
71
+ }
72
+
73
+ async function defaultDownload(url: string, destPath: string): Promise<void> {
74
+ const resp = await fetchWithTimeout(url, {}, globalThis.fetch, 60_000)
75
+ if (!resp.ok) {
76
+ throw new SelfUpdateError(`download failed: HTTP ${resp.status} for ${url}`)
77
+ }
78
+ await writeFile(destPath, new Uint8Array(await resp.arrayBuffer()))
79
+ }
80
+
81
+ /**
82
+ * Download the release asset for the current platform, checksum-verify it
83
+ * against release checksums.txt, then atomically rename it over targetPath.
84
+ */
85
+ export async function selfUpdateToVersion(options: SelfUpdateOptions): Promise<void> {
86
+ const base = `https://github.com/getkimchi/kimchictl/releases/download/${options.version}`
87
+ const asset = platformAssetName()
88
+ const download = options.download ?? defaultDownload
89
+ const chmodFn = options.chmodFn ?? chmod
90
+ const renameFn = options.renameFn ?? rename
91
+
92
+ const tmpDir = await mkdtemp(join(options.tmpRoot ?? tmpdir(), "kimchictl-update-"))
93
+ const staged = join(tmpDir, "kimchictl")
94
+
95
+ await download(`${base}/${asset}`, staged)
96
+
97
+ // Verify against the release checksums.txt when it exists.
98
+ const checksumsPath = join(tmpDir, "checksums.txt")
99
+ try {
100
+ await download(`${base}/checksums.txt`, checksumsPath)
101
+ const checksums = await readFile(checksumsPath, "utf-8")
102
+ const expected = checksums
103
+ .split("\n")
104
+ .map((line) => line.trim())
105
+ .find((line) => line.endsWith(` ${asset}`))
106
+ ?.split(/\s+/)[0]
107
+ if (expected) {
108
+ const actual = createHash("sha256")
109
+ .update(await readFile(staged))
110
+ .digest("hex")
111
+ if (actual !== expected) {
112
+ throw new ChecksumMismatchError(
113
+ `checksum mismatch for ${asset} — aborting update (expected ${expected}, got ${actual})`,
114
+ )
115
+ }
116
+ }
117
+ } catch (err) {
118
+ if (err instanceof ChecksumMismatchError) throw err
119
+ // Fetching checksums.txt itself failed (older releases may lack it) — proceed without verification.
120
+ }
121
+
122
+ await chmodFn(staged, 0o755)
123
+ await renameFn(staged, options.targetPath)
124
+ }
125
+
126
+ /** Guard for the update command — plain error for guard.ts, no new error family. */
127
+ export function assertNotDevBuild(): void {
128
+ if (VERSION.endsWith("-dev")) {
129
+ throw new SelfUpdateError("self-update is not available from a dev build (VERSION is 0.0.0-dev)")
130
+ }
131
+ }
package/src/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Package version. The release pipeline (release.yml) stamps the git-tag
3
+ * version over this constant at build time; the committed value stays a
4
+ * sentinel so source checkouts are recognizable.
5
+ */
6
+ export const VERSION = "0.0.2"