@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.
- package/LICENSE +203 -0
- package/README.md +105 -0
- package/bin/kimchictl.mjs +17 -0
- package/dist/api/http.d.ts +7 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +60 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/keys.d.ts +7 -0
- package/dist/api/keys.d.ts.map +1 -0
- package/dist/api/keys.js +28 -0
- package/dist/api/keys.js.map +1 -0
- package/dist/api/me.d.ts +12 -0
- package/dist/api/me.d.ts.map +1 -0
- package/dist/api/me.js +28 -0
- package/dist/api/me.js.map +1 -0
- package/dist/api/resolver.d.ts +26 -0
- package/dist/api/resolver.d.ts.map +1 -0
- package/dist/api/resolver.js +62 -0
- package/dist/api/resolver.js.map +1 -0
- package/dist/api/resources.d.ts +16 -0
- package/dist/api/resources.d.ts.map +1 -0
- package/dist/api/resources.js +96 -0
- package/dist/api/resources.js.map +1 -0
- package/dist/api/templates.d.ts +19 -0
- package/dist/api/templates.d.ts.map +1 -0
- package/dist/api/templates.js +25 -0
- package/dist/api/templates.js.map +1 -0
- package/dist/api/tokens.d.ts +23 -0
- package/dist/api/tokens.d.ts.map +1 -0
- package/dist/api/tokens.js +51 -0
- package/dist/api/tokens.js.map +1 -0
- package/dist/api/types.d.ts +34 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +32 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/wait.d.ts +17 -0
- package/dist/api/wait.d.ts.map +1 -0
- package/dist/api/wait.js +37 -0
- package/dist/api/wait.js.map +1 -0
- package/dist/api/workspaces.d.ts +33 -0
- package/dist/api/workspaces.d.ts.map +1 -0
- package/dist/api/workspaces.js +159 -0
- package/dist/api/workspaces.js.map +1 -0
- package/dist/auth/auth-file.d.ts +37 -0
- package/dist/auth/auth-file.d.ts.map +1 -0
- package/dist/auth/auth-file.js +100 -0
- package/dist/auth/auth-file.js.map +1 -0
- package/dist/auth/browser-login.d.ts +45 -0
- package/dist/auth/browser-login.d.ts.map +1 -0
- package/dist/auth/browser-login.js +195 -0
- package/dist/auth/browser-login.js.map +1 -0
- package/dist/auth/config-file.d.ts +4 -0
- package/dist/auth/config-file.d.ts.map +1 -0
- package/dist/auth/config-file.js +52 -0
- package/dist/auth/config-file.js.map +1 -0
- package/dist/auth/models-providers.d.ts +17 -0
- package/dist/auth/models-providers.d.ts.map +1 -0
- package/dist/auth/models-providers.js +63 -0
- package/dist/auth/models-providers.js.map +1 -0
- package/dist/auth/paths.d.ts +15 -0
- package/dist/auth/paths.d.ts.map +1 -0
- package/dist/auth/paths.js +25 -0
- package/dist/auth/paths.js.map +1 -0
- package/dist/auth/provider-ids.d.ts +4 -0
- package/dist/auth/provider-ids.d.ts.map +1 -0
- package/dist/auth/provider-ids.js +12 -0
- package/dist/auth/provider-ids.js.map +1 -0
- package/dist/auth/resolve.d.ts +19 -0
- package/dist/auth/resolve.d.ts.map +1 -0
- package/dist/auth/resolve.js +37 -0
- package/dist/auth/resolve.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +71 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/flags.d.ts +16 -0
- package/dist/commands/flags.d.ts.map +1 -0
- package/dist/commands/flags.js +23 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/guard.d.ts +10 -0
- package/dist/commands/guard.d.ts.map +1 -0
- package/dist/commands/guard.js +41 -0
- package/dist/commands/guard.js.map +1 -0
- package/dist/commands/help.d.ts +4 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +19 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +80 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +14 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +41 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/prompt.d.ts +9 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js +21 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/registry.d.ts +16 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +35 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.d.ts.map +1 -0
- package/dist/commands/ssh.js +90 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/commands/table.d.ts +3 -0
- package/dist/commands/table.d.ts.map +1 -0
- package/dist/commands/table.js +10 -0
- package/dist/commands/table.js.map +1 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +57 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +6 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +30 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/commands/workspace.d.ts +15 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +206 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +30 -0
- package/dist/run.js.map +1 -0
- package/dist/ssh/bridge.d.ts +25 -0
- package/dist/ssh/bridge.d.ts.map +1 -0
- package/dist/ssh/bridge.js +95 -0
- package/dist/ssh/bridge.js.map +1 -0
- package/dist/ssh/config.d.ts +65 -0
- package/dist/ssh/config.d.ts.map +1 -0
- package/dist/ssh/config.js +198 -0
- package/dist/ssh/config.js.map +1 -0
- package/dist/ssh/connect.d.ts +27 -0
- package/dist/ssh/connect.d.ts.map +1 -0
- package/dist/ssh/connect.js +67 -0
- package/dist/ssh/connect.js.map +1 -0
- package/dist/ssh/tunnel.d.ts +29 -0
- package/dist/ssh/tunnel.d.ts.map +1 -0
- package/dist/ssh/tunnel.js +36 -0
- package/dist/ssh/tunnel.js.map +1 -0
- package/dist/test-support.d.ts +30 -0
- package/dist/test-support.d.ts.map +1 -0
- package/dist/test-support.js +67 -0
- package/dist/test-support.js.map +1 -0
- package/dist/updates/check.d.ts +18 -0
- package/dist/updates/check.d.ts.map +1 -0
- package/dist/updates/check.js +90 -0
- package/dist/updates/check.js.map +1 -0
- package/dist/updates/self.d.ts +36 -0
- package/dist/updates/self.d.ts.map +1 -0
- package/dist/updates/self.js +103 -0
- package/dist/updates/self.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +77 -0
- package/src/api/http.ts +70 -0
- package/src/api/keys.ts +40 -0
- package/src/api/me.ts +48 -0
- package/src/api/resolver.test.ts +109 -0
- package/src/api/resolver.ts +72 -0
- package/src/api/resources.test.ts +56 -0
- package/src/api/resources.ts +92 -0
- package/src/api/templates.ts +28 -0
- package/src/api/tokens.ts +80 -0
- package/src/api/types.ts +50 -0
- package/src/api/wait.ts +45 -0
- package/src/api/workspaces.ts +234 -0
- package/src/auth/auth-file.test.ts +176 -0
- package/src/auth/auth-file.ts +103 -0
- package/src/auth/browser-login.test.ts +93 -0
- package/src/auth/browser-login.ts +247 -0
- package/src/auth/config-file.test.ts +68 -0
- package/src/auth/config-file.ts +50 -0
- package/src/auth/models-providers.test.ts +63 -0
- package/src/auth/models-providers.ts +76 -0
- package/src/auth/paths.ts +28 -0
- package/src/auth/provider-ids.ts +15 -0
- package/src/auth/resolve.test.ts +72 -0
- package/src/auth/resolve.ts +46 -0
- package/src/cli.ts +16 -0
- package/src/commands/completion.test.ts +31 -0
- package/src/commands/completion.ts +77 -0
- package/src/commands/flags.ts +29 -0
- package/src/commands/guard.ts +44 -0
- package/src/commands/help.ts +20 -0
- package/src/commands/login.test.ts +159 -0
- package/src/commands/login.ts +94 -0
- package/src/commands/logout.test.ts +89 -0
- package/src/commands/logout.ts +53 -0
- package/src/commands/prompt.ts +21 -0
- package/src/commands/registry.ts +50 -0
- package/src/commands/ssh.test.ts +101 -0
- package/src/commands/ssh.ts +112 -0
- package/src/commands/table.test.ts +24 -0
- package/src/commands/table.ts +10 -0
- package/src/commands/update.test.ts +91 -0
- package/src/commands/update.ts +78 -0
- package/src/commands/version.ts +6 -0
- package/src/commands/whoami.test.ts +72 -0
- package/src/commands/whoami.ts +36 -0
- package/src/commands/workspace.test.ts +360 -0
- package/src/commands/workspace.ts +250 -0
- package/src/run.test.ts +53 -0
- package/src/run.ts +34 -0
- package/src/ssh/bridge.test.ts +70 -0
- package/src/ssh/bridge.ts +110 -0
- package/src/ssh/config.test.ts +213 -0
- package/src/ssh/config.ts +233 -0
- package/src/ssh/connect.test.ts +123 -0
- package/src/ssh/connect.ts +98 -0
- package/src/ssh/tunnel.test.ts +119 -0
- package/src/ssh/tunnel.ts +64 -0
- package/src/test-support.ts +77 -0
- package/src/updates/check.test.ts +99 -0
- package/src/updates/check.ts +117 -0
- package/src/updates/self.test.ts +117 -0
- package/src/updates/self.ts +131 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test helpers — co-located *.test.ts files import from here instead of
|
|
3
|
+
* re-rolling temp-dir / fetch-stub / console-capture boilerplate per file.
|
|
4
|
+
* Excluded from the published build (tsconfig.build.json).
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { vi } from "vitest";
|
|
10
|
+
const tempDirs = [];
|
|
11
|
+
export function makeTempDir(prefix = "kimchictl-test-") {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
13
|
+
tempDirs.push(dir);
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
/** Call from afterEach — removes every dir created via makeTempDir. */
|
|
17
|
+
export function cleanupTempDirs() {
|
|
18
|
+
for (const dir of tempDirs.splice(0))
|
|
19
|
+
rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build a fetch stub from a narrow implementation. The `as unknown` adapts the
|
|
23
|
+
* test's string-URL signature to fetch's wide `(string | URL | Request)` one —
|
|
24
|
+
* deliberate, one place, test-only.
|
|
25
|
+
*/
|
|
26
|
+
export function stubFetch(impl) {
|
|
27
|
+
return impl;
|
|
28
|
+
}
|
|
29
|
+
export function jsonResponse(body, status = 200) {
|
|
30
|
+
return new Response(JSON.stringify(body), {
|
|
31
|
+
status,
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Point the shared-credential paths at an isolated HOME + agent dir and neutralize ambient key env vars. Returns the stubbed HOME — the shared config.json lives at `<home>/.config/kimchi/config.json` (sibling of the harness agent dir, matching kimchi-dev:src/config.ts). */
|
|
36
|
+
export function stubAgentDirEnv(dir) {
|
|
37
|
+
const home = makeTempDir();
|
|
38
|
+
vi.stubEnv("HOME", home);
|
|
39
|
+
vi.stubEnv("KIMCHI_CODING_AGENT_DIR", dir);
|
|
40
|
+
// resolveApiKey treats an empty KIMCHI_API_KEY as absent.
|
|
41
|
+
vi.stubEnv("KIMCHI_API_KEY", "");
|
|
42
|
+
return { home };
|
|
43
|
+
}
|
|
44
|
+
/** Path of the harness's shared wizard config under a (stubbed) HOME. */
|
|
45
|
+
export function sharedConfigPath(home) {
|
|
46
|
+
return join(home, ".config", "kimchi", "config.json");
|
|
47
|
+
}
|
|
48
|
+
/** Seed the shared config.json under a stubbed HOME (creates parent dirs). */
|
|
49
|
+
export function writeSharedConfigJson(home, value) {
|
|
50
|
+
const configPath = sharedConfigPath(home);
|
|
51
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
52
|
+
writeFileSync(configPath, JSON.stringify(value, null, 2));
|
|
53
|
+
return configPath;
|
|
54
|
+
}
|
|
55
|
+
/** Capture console.log / console.error; restore() in afterEach (or rely on vi.restoreAllMocks). */
|
|
56
|
+
export function captureConsole() {
|
|
57
|
+
const lines = [];
|
|
58
|
+
const errors = [];
|
|
59
|
+
vi.spyOn(console, "log").mockImplementation((...args) => {
|
|
60
|
+
lines.push(args.map(String).join(" "));
|
|
61
|
+
});
|
|
62
|
+
vi.spyOn(console, "error").mockImplementation((...args) => {
|
|
63
|
+
errors.push(args.map(String).join(" "));
|
|
64
|
+
});
|
|
65
|
+
return { lines, errors };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=test-support.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-support.js","sourceRoot":"../src/","sources":["test-support.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Semantic-ish comparison: 0.10.0 > 0.9.9. Only digits matter; pre-releases compare numerically too. */
|
|
2
|
+
export declare function isNewer(latest: string, current: string): boolean;
|
|
3
|
+
export interface UpdateCheckResult {
|
|
4
|
+
latest: string;
|
|
5
|
+
current: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CheckForUpdateOptions {
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
fetch?: typeof globalThis.fetch;
|
|
10
|
+
now?: () => Date;
|
|
11
|
+
/** Version to compare against — tests inject; defaults to the baked-in VERSION. */
|
|
12
|
+
currentVersion?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Returns {latest, current} when a newer release exists, otherwise undefined. Never throws. */
|
|
15
|
+
export declare function checkForUpdate(options?: CheckForUpdateOptions): Promise<UpdateCheckResult | undefined>;
|
|
16
|
+
/** Post the update hint to stderr. Extracted so cli.ts stays a one-liner. */
|
|
17
|
+
export declare function maybeNotifyUpdate(options?: CheckForUpdateOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"../src/","sources":["updates/check.ts"],"names":[],"mappings":"AAuBA,yGAAyG;AACzG,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAahE;AAED,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CACf;AAqBD,MAAM,WAAW,qBAAqB;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,gGAAgG;AAChG,wBAAsB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAoChH;AAED,6EAA6E;AAC7E,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1F"}
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
* Daily background update check against the GitHub Releases API.
|
|
8
|
+
*
|
|
9
|
+
* The check must never block or fail a real command: every failure mode
|
|
10
|
+
* resolves to "no notification". Results cache in
|
|
11
|
+
* ~/.config/kimchi/kimchictl/update-check.json for 24h.
|
|
12
|
+
*/
|
|
13
|
+
const LATEST_RELEASE_URL = "https://api.github.com/repos/getkimchi/kimchictl/releases/latest";
|
|
14
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const FETCH_TIMEOUT_MS = 2500;
|
|
16
|
+
/** Semantic-ish comparison: 0.10.0 > 0.9.9. Only digits matter; pre-releases compare numerically too. */
|
|
17
|
+
export function isNewer(latest, current) {
|
|
18
|
+
const parse = (v) => v
|
|
19
|
+
.replace(/^v/, "")
|
|
20
|
+
.split(".")
|
|
21
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
22
|
+
const a = parse(latest);
|
|
23
|
+
const b = parse(current);
|
|
24
|
+
for (let i = 0; i < 3; i++) {
|
|
25
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
|
26
|
+
if (diff !== 0)
|
|
27
|
+
return diff > 0;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
async function readCache(path) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(await readFile(path, "utf-8"));
|
|
34
|
+
if (typeof raw.latestVersion === "string" && typeof raw.checkedAt === "string") {
|
|
35
|
+
return { latestVersion: raw.latestVersion, checkedAt: raw.checkedAt };
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err.code === "ENOENT")
|
|
41
|
+
return undefined;
|
|
42
|
+
return undefined; // corrupt cache → treat as stale (refetched below)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function writeCache(path, cache) {
|
|
46
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
47
|
+
await writeFile(path, `${JSON.stringify(cache, null, 2)}\n`, { mode: 0o600 });
|
|
48
|
+
await chmod(path, 0o600);
|
|
49
|
+
}
|
|
50
|
+
/** Returns {latest, current} when a newer release exists, otherwise undefined. Never throws. */
|
|
51
|
+
export async function checkForUpdate(options = {}) {
|
|
52
|
+
try {
|
|
53
|
+
const current = options.currentVersion ?? VERSION;
|
|
54
|
+
// Source checkouts and dev builds are not versioned against releases.
|
|
55
|
+
if (current.endsWith("-dev"))
|
|
56
|
+
return undefined;
|
|
57
|
+
const env = options.env ?? process.env;
|
|
58
|
+
if (env.KIMCHICTL_NO_UPDATE_CHECK)
|
|
59
|
+
return undefined;
|
|
60
|
+
const now = options.now ?? (() => new Date());
|
|
61
|
+
const cachePath = join(resolveSshPaths(env).home, "update-check.json");
|
|
62
|
+
const cached = await readCache(cachePath);
|
|
63
|
+
if (cached && now().getTime() - new Date(cached.checkedAt).getTime() < CHECK_INTERVAL_MS) {
|
|
64
|
+
return isNewer(cached.latestVersion, current) ? { latest: cached.latestVersion, current } : undefined;
|
|
65
|
+
}
|
|
66
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
67
|
+
const resp = await fetchWithTimeout(LATEST_RELEASE_URL, { headers: { "User-Agent": `kimchictl/${current}` } }, fetchImpl, FETCH_TIMEOUT_MS);
|
|
68
|
+
if (!resp.ok)
|
|
69
|
+
return undefined;
|
|
70
|
+
const data = (await resp.json());
|
|
71
|
+
if (typeof data.tag_name !== "string")
|
|
72
|
+
return undefined;
|
|
73
|
+
const latest = data.tag_name.replace(/^v/, "");
|
|
74
|
+
await writeCache(cachePath, { latestVersion: latest, checkedAt: now().toISOString() }).catch(() => {
|
|
75
|
+
// Cache writes are best-effort only.
|
|
76
|
+
});
|
|
77
|
+
return isNewer(latest, current) ? { latest, current } : undefined;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Post the update hint to stderr. Extracted so cli.ts stays a one-liner. */
|
|
84
|
+
export async function maybeNotifyUpdate(options = {}) {
|
|
85
|
+
const result = await checkForUpdate(options);
|
|
86
|
+
if (result) {
|
|
87
|
+
console.error(`⬆ kimchictl ${result.latest} available (current ${result.current}) — update: kimchictl update`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"../src/","sources":["updates/check.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update plumbing: detect how kimchictl was installed and, for binary
|
|
3
|
+
* installs, swap the executable in place from the latest GitHub release.
|
|
4
|
+
* Homebrew/npm/RTE (runtime) installs delegate to their own updaters.
|
|
5
|
+
*/
|
|
6
|
+
export type InstallMethod = "binary" | "homebrew" | "runtime";
|
|
7
|
+
/** realpath seam: tests feed synthetic paths. */
|
|
8
|
+
export declare function detectInstallMethod(realpath: () => string): InstallMethod;
|
|
9
|
+
/** realpath of the running executable, best-effort. */
|
|
10
|
+
export declare function defaultInstallMethod(execPath?: string): InstallMethod;
|
|
11
|
+
/** Release asset name for this platform, e.g. kimchictl-darwin-arm64. */
|
|
12
|
+
export declare function platformAssetName(platform?: NodeJS.Platform, arch?: string): string;
|
|
13
|
+
export declare class SelfUpdateError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
/** Distinct from a checksums-download failure: a verified mismatch always aborts. */
|
|
17
|
+
export declare class ChecksumMismatchError extends SelfUpdateError {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
export interface SelfUpdateOptions {
|
|
21
|
+
targetPath: string;
|
|
22
|
+
version: string;
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
download?: (url: string, destPath: string) => Promise<void>;
|
|
25
|
+
chmodFn?: (path: string, mode: number) => Promise<void>;
|
|
26
|
+
renameFn?: (from: string, to: string) => Promise<void>;
|
|
27
|
+
tmpRoot?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Download the release asset for the current platform, checksum-verify it
|
|
31
|
+
* against release checksums.txt, then atomically rename it over targetPath.
|
|
32
|
+
*/
|
|
33
|
+
export declare function selfUpdateToVersion(options: SelfUpdateOptions): Promise<void>;
|
|
34
|
+
/** Guard for the update command — plain error for guard.ts, no new error family. */
|
|
35
|
+
export declare function assertNotDevBuild(): void;
|
|
36
|
+
//# sourceMappingURL=self.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self.d.ts","sourceRoot":"../src/","sources":["updates/self.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AAEH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAA;AAE7D,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,MAAM,GAAG,aAAa,CAUzE;AAED,uDAAuD;AACvD,wBAAgB,oBAAoB,CAAC,QAAQ,SAAmB,GAAG,aAAa,CAM/E;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAAE,IAAI,GAAE,MAAqB,GAAG,MAAM,CAOnH;AAED,qBAAa,eAAgB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI3B;AAED,qFAAqF;AACrF,qBAAa,qBAAsB,SAAQ,eAAe;gBAC7C,OAAO,EAAE,MAAM;CAI3B;AAED,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB;AAUD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
/** realpath seam: tests feed synthetic paths. */
|
|
8
|
+
export function detectInstallMethod(realpath) {
|
|
9
|
+
const target = realpath();
|
|
10
|
+
if (/\/(Cellar|linuxbrew)\/[^/]*kimchictl?\//.test(target) || /homebrew.*kimchictl/i.test(target)) {
|
|
11
|
+
return "homebrew";
|
|
12
|
+
}
|
|
13
|
+
const base = basename(target);
|
|
14
|
+
if (base === "node" || base === "bun" || base === "node.exe") {
|
|
15
|
+
return "runtime";
|
|
16
|
+
}
|
|
17
|
+
return "binary";
|
|
18
|
+
}
|
|
19
|
+
/** realpath of the running executable, best-effort. */
|
|
20
|
+
export function defaultInstallMethod(execPath = process.execPath) {
|
|
21
|
+
// bun-compiled binaries ARE the install (execPath is the binary); node/bun
|
|
22
|
+
// runtime hosts are npm/dev installs.
|
|
23
|
+
const base = basename(execPath);
|
|
24
|
+
if (base === "node" || base === "bun" || base === "node.exe")
|
|
25
|
+
return "runtime";
|
|
26
|
+
return detectInstallMethod(() => execPath);
|
|
27
|
+
}
|
|
28
|
+
/** Release asset name for this platform, e.g. kimchictl-darwin-arm64. */
|
|
29
|
+
export function platformAssetName(platform = process.platform, arch = process.arch) {
|
|
30
|
+
const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : undefined;
|
|
31
|
+
const cpu = arch === "arm64" ? "arm64" : arch === "x64" ? "x64" : undefined;
|
|
32
|
+
if (!os || !cpu) {
|
|
33
|
+
throw new Error(`unsupported platform for self-update: ${platform}/${arch}`);
|
|
34
|
+
}
|
|
35
|
+
return `kimchictl-${os}-${cpu}`;
|
|
36
|
+
}
|
|
37
|
+
export class SelfUpdateError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "SelfUpdateError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Distinct from a checksums-download failure: a verified mismatch always aborts. */
|
|
44
|
+
export class ChecksumMismatchError extends SelfUpdateError {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "ChecksumMismatchError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function defaultDownload(url, destPath) {
|
|
51
|
+
const resp = await fetchWithTimeout(url, {}, globalThis.fetch, 60_000);
|
|
52
|
+
if (!resp.ok) {
|
|
53
|
+
throw new SelfUpdateError(`download failed: HTTP ${resp.status} for ${url}`);
|
|
54
|
+
}
|
|
55
|
+
await writeFile(destPath, new Uint8Array(await resp.arrayBuffer()));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Download the release asset for the current platform, checksum-verify it
|
|
59
|
+
* against release checksums.txt, then atomically rename it over targetPath.
|
|
60
|
+
*/
|
|
61
|
+
export async function selfUpdateToVersion(options) {
|
|
62
|
+
const base = `https://github.com/getkimchi/kimchictl/releases/download/${options.version}`;
|
|
63
|
+
const asset = platformAssetName();
|
|
64
|
+
const download = options.download ?? defaultDownload;
|
|
65
|
+
const chmodFn = options.chmodFn ?? chmod;
|
|
66
|
+
const renameFn = options.renameFn ?? rename;
|
|
67
|
+
const tmpDir = await mkdtemp(join(options.tmpRoot ?? tmpdir(), "kimchictl-update-"));
|
|
68
|
+
const staged = join(tmpDir, "kimchictl");
|
|
69
|
+
await download(`${base}/${asset}`, staged);
|
|
70
|
+
// Verify against the release checksums.txt when it exists.
|
|
71
|
+
const checksumsPath = join(tmpDir, "checksums.txt");
|
|
72
|
+
try {
|
|
73
|
+
await download(`${base}/checksums.txt`, checksumsPath);
|
|
74
|
+
const checksums = await readFile(checksumsPath, "utf-8");
|
|
75
|
+
const expected = checksums
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.find((line) => line.endsWith(` ${asset}`))
|
|
79
|
+
?.split(/\s+/)[0];
|
|
80
|
+
if (expected) {
|
|
81
|
+
const actual = createHash("sha256")
|
|
82
|
+
.update(await readFile(staged))
|
|
83
|
+
.digest("hex");
|
|
84
|
+
if (actual !== expected) {
|
|
85
|
+
throw new ChecksumMismatchError(`checksum mismatch for ${asset} — aborting update (expected ${expected}, got ${actual})`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof ChecksumMismatchError)
|
|
91
|
+
throw err;
|
|
92
|
+
// Fetching checksums.txt itself failed (older releases may lack it) — proceed without verification.
|
|
93
|
+
}
|
|
94
|
+
await chmodFn(staged, 0o755);
|
|
95
|
+
await renameFn(staged, options.targetPath);
|
|
96
|
+
}
|
|
97
|
+
/** Guard for the update command — plain error for guard.ts, no new error family. */
|
|
98
|
+
export function assertNotDevBuild() {
|
|
99
|
+
if (VERSION.endsWith("-dev")) {
|
|
100
|
+
throw new SelfUpdateError("self-update is not available from a dev build (VERSION is 0.0.0-dev)");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=self.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self.js","sourceRoot":"../src/","sources":["updates/self.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
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 declare const VERSION = "0.0.2";
|
|
7
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"../src/","sources":["version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,OAAO,UAAU,CAAA"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
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";
|
|
7
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"../src/","sources":["version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//name": "Scoped under the kimchi-dev npm org (the GitHub org is getkimchi)",
|
|
3
|
+
"name": "@kimchi-dev/kimchictl",
|
|
4
|
+
"//version": "Placeholder, never bumped: the git tag is the version, stamped by release.yml at publish time",
|
|
5
|
+
"version": "0.0.2",
|
|
6
|
+
"description": "Create and connect to kimchi remote workspaces from the terminal.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"kimchi",
|
|
9
|
+
"workspace",
|
|
10
|
+
"sandbox",
|
|
11
|
+
"remote-development",
|
|
12
|
+
"ssh",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"author": "CAST AI Group, Inc.",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/getkimchi/kimchictl.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/getkimchi/kimchictl#readme",
|
|
22
|
+
"bugs": "https://github.com/getkimchi/kimchictl/issues",
|
|
23
|
+
"packageManager": "pnpm@10.8.1",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.19"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"kimchictl": "./bin/kimchictl.mjs"
|
|
30
|
+
},
|
|
31
|
+
"//files": "dist is what consumers import; src ships so source maps resolve and git installs stay inspectable; bin is the CLI entry shim",
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"dist",
|
|
35
|
+
"src"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/run.d.ts",
|
|
43
|
+
"default": "./dist/run.js"
|
|
44
|
+
},
|
|
45
|
+
"./commands/workspace": {
|
|
46
|
+
"types": "./dist/commands/workspace.d.ts",
|
|
47
|
+
"default": "./dist/commands/workspace.js"
|
|
48
|
+
},
|
|
49
|
+
"./commands/guard": {
|
|
50
|
+
"types": "./dist/commands/guard.d.ts",
|
|
51
|
+
"default": "./dist/commands/guard.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"dev": "bun src/cli.ts",
|
|
56
|
+
"build": "tsc -p tsconfig.build.json",
|
|
57
|
+
"prepack": "pnpm run build",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"lint": "biome check .",
|
|
61
|
+
"lint:fix": "biome check --write .",
|
|
62
|
+
"check": "pnpm run lint && pnpm run typecheck && pnpm run test"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@biomejs/biome": "^2.5.6",
|
|
66
|
+
"@types/node": "^22.19.18",
|
|
67
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
68
|
+
"@types/ws": "^8.18.1",
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"vitest": "^3.2.4"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"open": "^11.0.4",
|
|
74
|
+
"proper-lockfile": "^4.1.2",
|
|
75
|
+
"ws": "^8.21.3"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/api/http.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ApiOptions } from "./types.js"
|
|
2
|
+
import { RemoteAuthError, RemoteNetworkError, RemoteQuotaError } from "./types.js"
|
|
3
|
+
|
|
4
|
+
/** Control-plane base URL: option → KIMCHI_REMOTE_ENDPOINT → https://app.kimchi.dev/api. */
|
|
5
|
+
export function resolveEndpoint(options?: ApiOptions): string {
|
|
6
|
+
return options?.endpoint ?? process.env.KIMCHI_REMOTE_ENDPOINT ?? "https://app.kimchi.dev/api"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function fetchWithTimeout(
|
|
10
|
+
url: string,
|
|
11
|
+
init: RequestInit,
|
|
12
|
+
fetchImpl: typeof globalThis.fetch,
|
|
13
|
+
ms = 30_000,
|
|
14
|
+
externalSignal?: AbortSignal,
|
|
15
|
+
): Promise<Response> {
|
|
16
|
+
const ctrl = new AbortController()
|
|
17
|
+
const timer = setTimeout(() => ctrl.abort(), ms)
|
|
18
|
+
const signal = externalSignal ? AbortSignal.any([ctrl.signal, externalSignal]) : ctrl.signal
|
|
19
|
+
try {
|
|
20
|
+
return await fetchImpl(url, { ...init, signal })
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout(timer)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Status-to-error mapping shared by all control-plane calls (mirrors the harness). */
|
|
27
|
+
export async function checkResponse(resp: Response, endpoint: string): Promise<void> {
|
|
28
|
+
if (resp.ok) return
|
|
29
|
+
|
|
30
|
+
const body = await resp.text().catch(() => "")
|
|
31
|
+
switch (resp.status) {
|
|
32
|
+
case 401:
|
|
33
|
+
throw new RemoteAuthError(`Invalid API key — run 'kimchictl login' to authenticate: ${endpoint}`, 401)
|
|
34
|
+
case 403:
|
|
35
|
+
throw new RemoteAuthError(
|
|
36
|
+
`Forbidden — your API key does not have permission to use remote workspaces. ${endpoint}`,
|
|
37
|
+
403,
|
|
38
|
+
)
|
|
39
|
+
case 404:
|
|
40
|
+
throw new RemoteAuthError(`Workspace not found or endpoint not available. ${endpoint}`, 404)
|
|
41
|
+
case 409:
|
|
42
|
+
throw new RemoteAuthError(`Workspace conflict — another client may already own this workspace. ${endpoint}`, 409)
|
|
43
|
+
case 429: {
|
|
44
|
+
// Quota-exceeded bodies look like
|
|
45
|
+
// {"message":"quota exceeded: user CPU limit exceeded","fieldViolations":[]}.
|
|
46
|
+
const detail = parseQuotaDetail(body)
|
|
47
|
+
if (detail !== undefined) {
|
|
48
|
+
throw new RemoteQuotaError(`Unable to provision workspace: ${detail}`, 429)
|
|
49
|
+
}
|
|
50
|
+
throw new RemoteNetworkError(`HTTP ${resp.status} from ${endpoint}${body ? `: ${body}` : ""}`)
|
|
51
|
+
}
|
|
52
|
+
default: {
|
|
53
|
+
throw new RemoteNetworkError(`HTTP ${resp.status} from ${endpoint}${body ? `: ${body}` : ""}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract the human-readable reason from a 429 body, or undefined when the
|
|
60
|
+
* body is not JSON with a "message" string (plain rate-limit response).
|
|
61
|
+
*/
|
|
62
|
+
function parseQuotaDetail(body: string): string | undefined {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(body) as { message?: unknown }
|
|
65
|
+
if (typeof parsed.message !== "string" || parsed.message.length === 0) return undefined
|
|
66
|
+
return parsed.message.replace(/^quota exceeded:\s*/i, "")
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/api/keys.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { checkResponse, fetchWithTimeout, resolveEndpoint } from "./http.js"
|
|
2
|
+
import type { ApiOptions } from "./types.js"
|
|
3
|
+
import { RemoteNetworkError } from "./types.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verify an API key against the control plane. Returns the organization id.
|
|
7
|
+
* (Mirror of kimchi-dev:src/sandbox/cloud/keys.ts.)
|
|
8
|
+
*/
|
|
9
|
+
export async function verifyApiKey(apiKey: string, options?: ApiOptions): Promise<string> {
|
|
10
|
+
const endpoint = resolveEndpoint(options)
|
|
11
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch
|
|
12
|
+
|
|
13
|
+
const url = `${endpoint}/ai-optimizer/v1beta/workspace-tokens:verifyKey`
|
|
14
|
+
const resp = await fetchWithTimeout(
|
|
15
|
+
url,
|
|
16
|
+
{
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${apiKey}`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
fetchImpl,
|
|
24
|
+
30_000,
|
|
25
|
+
options?.signal,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
await checkResponse(resp, url)
|
|
29
|
+
|
|
30
|
+
const data = await resp.json().catch(() => {
|
|
31
|
+
throw new RemoteNetworkError(`Unexpected non-JSON response from ${endpoint}`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const orgId = (data as { organizationId?: unknown }).organizationId
|
|
35
|
+
if (typeof orgId !== "string") {
|
|
36
|
+
throw new RemoteNetworkError(`Missing organizationId in verify response from ${endpoint}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return orgId
|
|
40
|
+
}
|
package/src/api/me.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fetchWithTimeout, resolveEndpoint } from "./http.js"
|
|
2
|
+
import type { ApiOptions } from "./types.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /v1/me — the authenticated user's profile.
|
|
6
|
+
* (Mirror of kimchi-dev:src/api/me.ts.)
|
|
7
|
+
*/
|
|
8
|
+
export async function getMe(
|
|
9
|
+
apiKey: string,
|
|
10
|
+
options?: ApiOptions,
|
|
11
|
+
): Promise<{
|
|
12
|
+
id: string
|
|
13
|
+
username?: string
|
|
14
|
+
name?: string
|
|
15
|
+
email?: string
|
|
16
|
+
}> {
|
|
17
|
+
const endpoint = resolveEndpoint(options)
|
|
18
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch
|
|
19
|
+
|
|
20
|
+
const url = `${endpoint}/v1/me`
|
|
21
|
+
const resp = await fetchWithTimeout(
|
|
22
|
+
url,
|
|
23
|
+
{
|
|
24
|
+
method: "GET",
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${apiKey}`,
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
fetchImpl,
|
|
31
|
+
30_000,
|
|
32
|
+
options?.signal,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
throw new Error(`GET ${url} failed with HTTP ${resp.status}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await resp.json().catch(() => {
|
|
40
|
+
throw new Error(`Unexpected non-JSON response from ${url}`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (typeof (data as { id?: unknown })?.id !== "string" || (data as { id: string }).id.length === 0) {
|
|
44
|
+
throw new Error(`Missing id in /v1/me response from ${url}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return data as { id: string; username?: string; name?: string; email?: string }
|
|
48
|
+
}
|