@chvor/cli 0.1.2 → 0.1.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.
@@ -97,8 +97,9 @@ export async function spawnServer(opts = {}) {
97
97
  }
98
98
  if (opts.foreground) {
99
99
  console.log(`Starting Chvor on port ${port} (foreground)...`);
100
- const child = spawn("node", [serverEntry], {
100
+ const child = spawn("node", ["--import", "tsx", serverEntry], {
101
101
  env,
102
+ cwd: getAppDir(),
102
103
  stdio: "inherit",
103
104
  });
104
105
  if (child.pid === undefined) {
@@ -149,8 +150,9 @@ export async function spawnServer(opts = {}) {
149
150
  ensureDir(logsDir);
150
151
  const logPath = join(logsDir, "server.log");
151
152
  const logFd = openSync(logPath, "a");
152
- const child = spawn("node", [serverEntry], {
153
+ const child = spawn("node", ["--import", "tsx", serverEntry], {
153
154
  env,
155
+ cwd: getAppDir(),
154
156
  stdio: ["ignore", logFd, logFd],
155
157
  detached: true,
156
158
  });
@@ -232,7 +234,7 @@ export async function pollHealth(port, token, timeoutMs = 30000, intervalMs = 50
232
234
  function sleep(ms) {
233
235
  return new Promise((resolve) => setTimeout(resolve, ms));
234
236
  }
235
- function filterEnv(env) {
237
+ export function filterEnv(env) {
236
238
  const result = {};
237
239
  for (const [key, value] of Object.entries(env)) {
238
240
  if (value !== undefined) {
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { filterEnv, pollHealth } from "./process.js";
3
+ describe("filterEnv", () => {
4
+ it("removes entries with undefined values", () => {
5
+ const input = { A: "1", B: undefined, C: "3" };
6
+ const result = filterEnv(input);
7
+ expect(result).toEqual({ A: "1", C: "3" });
8
+ });
9
+ it("returns empty object for empty input", () => {
10
+ expect(filterEnv({})).toEqual({});
11
+ });
12
+ it("keeps all entries when none are undefined", () => {
13
+ const input = { X: "x", Y: "y" };
14
+ expect(filterEnv(input)).toEqual({ X: "x", Y: "y" });
15
+ });
16
+ });
17
+ describe("pollHealth", () => {
18
+ beforeEach(() => {
19
+ vi.useFakeTimers({ shouldAdvanceTime: true });
20
+ vi.stubGlobal("fetch", vi.fn());
21
+ });
22
+ afterEach(() => {
23
+ vi.useRealTimers();
24
+ vi.restoreAllMocks();
25
+ });
26
+ it("returns true when server responds with ok: true", async () => {
27
+ const mockFetch = vi.mocked(fetch);
28
+ mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
29
+ const result = await pollHealth("3001");
30
+ expect(result).toBe(true);
31
+ });
32
+ it("sends Authorization header when token is provided", async () => {
33
+ const mockFetch = vi.mocked(fetch);
34
+ mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
35
+ await pollHealth("3001", "my-secret-token");
36
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3001/api/health", { headers: { Authorization: "Bearer my-secret-token" } });
37
+ });
38
+ it("retries on fetch failure and succeeds eventually", async () => {
39
+ const mockFetch = vi.mocked(fetch);
40
+ mockFetch
41
+ .mockRejectedValueOnce(new Error("ECONNREFUSED"))
42
+ .mockRejectedValueOnce(new Error("ECONNREFUSED"))
43
+ .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
44
+ const result = await pollHealth("3001", undefined, 30000, 100);
45
+ expect(result).toBe(true);
46
+ expect(mockFetch).toHaveBeenCalledTimes(3);
47
+ });
48
+ it("returns false when timeout expires", async () => {
49
+ const mockFetch = vi.mocked(fetch);
50
+ mockFetch.mockRejectedValue(new Error("ECONNREFUSED"));
51
+ const result = await pollHealth("3001", undefined, 1000, 200);
52
+ expect(result).toBe(false);
53
+ });
54
+ it("retries when response is not ok", async () => {
55
+ const mockFetch = vi.mocked(fetch);
56
+ mockFetch
57
+ .mockResolvedValueOnce(new Response("Internal Server Error", { status: 500 }))
58
+ .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
59
+ const result = await pollHealth("3001", undefined, 30000, 100);
60
+ expect(result).toBe(true);
61
+ expect(mockFetch).toHaveBeenCalledTimes(2);
62
+ });
63
+ it("retries when body.ok is not true", async () => {
64
+ const mockFetch = vi.mocked(fetch);
65
+ mockFetch
66
+ .mockResolvedValueOnce(new Response(JSON.stringify({ ok: false }), { status: 200 }))
67
+ .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
68
+ const result = await pollHealth("3001", undefined, 30000, 100);
69
+ expect(result).toBe(true);
70
+ });
71
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chvor/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Your own AI — install and run chvor.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "module",
@@ -17,16 +17,21 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "prepublishOnly": "tsc",
20
- "typecheck": "tsc --noEmit"
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "vitest run"
21
22
  },
22
23
  "dependencies": {
23
24
  "commander": "^13",
24
25
  "@inquirer/prompts": "^7",
26
+ "tsx": "^4",
25
27
  "yaml": "^2"
26
28
  },
27
29
  "optionalDependencies": {
28
30
  "better-sqlite3": "^11"
29
31
  },
32
+ "devDependencies": {
33
+ "vitest": "^4.1.0"
34
+ },
30
35
  "repository": {
31
36
  "type": "git",
32
37
  "url": "https://github.com/luka-zivkovic/chvor.git",