Skip to content

Commit 30d18a5

Browse files
authored
Merge pull request #26 from bhouston/fast-version-check
Fast version check
2 parents af9f8af + a042adc commit 30d18a5

File tree

8 files changed

+163
-77
lines changed

8 files changed

+163
-77
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
### Patch Changes
66

7+
- A very fast and simple background version check
8+
9+
## 0.0.19
10+
11+
### Patch Changes
12+
713
- ensure the cli works on windows
814

915
## 0.0.18

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"build": "tsc --noEmit && tsc",
2424
"build:ci": "tsc",
2525
"clean": "rimraf dist",
26+
"clean:all": "rimraf dist node_modules",
2627
"lint": "eslint \"src/**/*.ts\" --fix",
2728
"format": "prettier --write \"src/**/*.*\"",
2829
"test": "vitest run",
@@ -51,6 +52,7 @@
5152
"@anthropic-ai/sdk": "^0.36",
5253
"chalk": "^5",
5354
"dotenv": "^16",
55+
"semver": "^7.7.1",
5456
"source-map-support": "^0.5",
5557
"uuid": "^11",
5658
"yargs": "^17",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/settings/settings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import * as path from "path";
4+
5+
const settingsDir = path.join(os.homedir(), ".mycoder");
6+
7+
export const getSettingsDir = (): string => {
8+
if (!fs.existsSync(settingsDir)) {
9+
fs.mkdirSync(settingsDir, { recursive: true });
10+
}
11+
return settingsDir;
12+
};

src/tools/system/shellStart.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe("shellStartTool", () => {
2323
description: "Test process",
2424
timeout: 500, // Generous timeout to ensure sync mode
2525
},
26-
{ logger },
26+
{ logger }
2727
);
2828

2929
expect(result.mode).toBe("sync");
@@ -41,7 +41,7 @@ describe("shellStartTool", () => {
4141
description: "Slow command test",
4242
timeout: 50, // Short timeout to force async mode
4343
},
44-
{ logger },
44+
{ logger }
4545
);
4646

4747
expect(result.mode).toBe("async");
@@ -57,7 +57,7 @@ describe("shellStartTool", () => {
5757
command: "nonexistentcommand",
5858
description: "Invalid command test",
5959
},
60-
{ logger },
60+
{ logger }
6161
);
6262

6363
expect(result.mode).toBe("sync");
@@ -75,7 +75,7 @@ describe("shellStartTool", () => {
7575
description: "Sync completion test",
7676
timeout: 500,
7777
},
78-
{ logger },
78+
{ logger }
7979
);
8080

8181
// Even sync results should be in processStates
@@ -88,7 +88,7 @@ describe("shellStartTool", () => {
8888
description: "Async completion test",
8989
timeout: 50,
9090
},
91-
{ logger },
91+
{ logger }
9292
);
9393

9494
if (asyncResult.mode === "async") {
@@ -103,7 +103,7 @@ describe("shellStartTool", () => {
103103
description: "Pipe test",
104104
timeout: 50, // Force async for interactive command
105105
},
106-
{ logger },
106+
{ logger }
107107
);
108108

109109
expect(result.mode).toBe("async");
@@ -130,15 +130,15 @@ describe("shellStartTool", () => {
130130
}
131131
});
132132

133-
it("should use default timeout of 100ms", async () => {
133+
it("should use default timeout of 10000ms", async () => {
134134
const result = await shellStartTool.execute(
135135
{
136136
command: "sleep 1",
137137
description: "Default timeout test",
138138
},
139-
{ logger },
139+
{ logger }
140140
);
141141

142-
expect(result.mode).toBe("async");
142+
expect(result.mode).toBe("sync");
143143
});
144144
});

src/tools/system/shellStart.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ const parameterSchema = z.object({
2929
timeout: z
3030
.number()
3131
.optional()
32-
.describe("Timeout in ms before switching to async mode (default: 100ms)"),
32+
.describe(
33+
"Timeout in ms before switching to async mode (default: 10s, which usually is sufficient)"
34+
),
3335
});
3436

3537
const returnSchema = z.union([
@@ -58,7 +60,7 @@ const returnSchema = z.union([
5860
type Parameters = z.infer<typeof parameterSchema>;
5961
type ReturnType = z.infer<typeof returnSchema>;
6062

61-
const DEFAULT_TIMEOUT = 100;
63+
const DEFAULT_TIMEOUT = 1000 * 10;
6264

6365
export const shellStartTool: Tool<Parameters, ReturnType> = {
6466
name: "shellStart",

src/utils/versionCheck.test.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1+
/* eslint-disable max-lines-per-function */
12
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
23
import {
34
generateUpgradeMessage,
45
fetchLatestVersion,
56
getPackageInfo,
67
checkForUpdates,
78
} from "./versionCheck.js";
9+
import * as fs from "fs";
10+
import * as fsPromises from "fs/promises";
11+
import * as path from "path";
12+
import { getSettingsDir } from "../settings/settings.js";
13+
14+
vi.mock("fs");
15+
vi.mock("fs/promises");
16+
vi.mock("../settings/settings.js");
817

918
describe("versionCheck", () => {
1019
describe("generateUpgradeMessage", () => {
1120
it("returns null when versions are the same", () => {
1221
expect(generateUpgradeMessage("1.0.0", "1.0.0", "test-package")).toBe(
13-
null,
22+
null
1423
);
1524
});
1625

@@ -19,6 +28,12 @@ describe("versionCheck", () => {
1928
expect(message).toContain("Update available: 1.0.0 → 1.1.0");
2029
expect(message).toContain("Run 'npm install -g test-package' to update");
2130
});
31+
32+
it("returns null when current version is higher", () => {
33+
expect(generateUpgradeMessage("2.0.0", "1.0.0", "test-package")).toBe(
34+
null
35+
);
36+
});
2237
});
2338

2439
describe("fetchLatestVersion", () => {
@@ -43,18 +58,30 @@ describe("versionCheck", () => {
4358
const version = await fetchLatestVersion("test-package");
4459
expect(version).toBe("1.1.0");
4560
expect(mockFetch).toHaveBeenCalledWith(
46-
"https://registry.npmjs.org/test-package/latest",
61+
"https://registry.npmjs.org/test-package/latest"
4762
);
4863
});
4964

50-
it("returns null when fetch fails", async () => {
65+
it("throws error when fetch fails", async () => {
5166
mockFetch.mockResolvedValueOnce({
5267
ok: false,
5368
statusText: "Not Found",
5469
});
5570

56-
const version = await fetchLatestVersion("test-package");
57-
expect(version).toBe(null);
71+
await expect(fetchLatestVersion("test-package")).rejects.toThrow(
72+
"Failed to fetch version info: Not Found"
73+
);
74+
});
75+
76+
it("throws error when version is missing from response", async () => {
77+
mockFetch.mockResolvedValueOnce({
78+
ok: true,
79+
json: () => Promise.resolve({}),
80+
});
81+
82+
await expect(fetchLatestVersion("test-package")).rejects.toThrow(
83+
"Version info not found in response"
84+
);
5885
});
5986
});
6087

@@ -71,28 +98,69 @@ describe("versionCheck", () => {
7198
describe("checkForUpdates", () => {
7299
const mockFetch = vi.fn();
73100
const originalFetch = global.fetch;
74-
const originalEnv = process.env;
101+
const mockSettingsDir = "/mock/settings/dir";
102+
const versionFilePath = path.join(mockSettingsDir, "lastVersionCheck");
75103

76104
beforeEach(() => {
77105
global.fetch = mockFetch;
78-
process.env = { ...originalEnv };
106+
vi.mocked(getSettingsDir).mockReturnValue(mockSettingsDir);
107+
vi.mocked(fs.existsSync).mockReturnValue(false);
79108
});
80109

81110
afterEach(() => {
82111
global.fetch = originalFetch;
83-
process.env = originalEnv;
84112
vi.clearAllMocks();
85113
});
86114

87-
it("returns upgrade message when update available", async () => {
88-
process.env.npm_config_global = "true";
115+
it("returns null and initiates background check when no cached version", async () => {
116+
vi.mocked(fs.existsSync).mockReturnValue(false);
89117
mockFetch.mockResolvedValueOnce({
90118
ok: true,
91-
json: () => Promise.resolve({ version: "999.0.0" }), // Much higher version
119+
json: () => Promise.resolve({ version: "2.0.0" }),
92120
});
93121

122+
const result = await checkForUpdates();
123+
expect(result).toBe(null);
124+
125+
// Wait for setImmediate to complete
126+
await new Promise((resolve) => setImmediate(resolve));
127+
128+
expect(mockFetch).toHaveBeenCalled();
129+
expect(fsPromises.writeFile).toHaveBeenCalledWith(
130+
versionFilePath,
131+
"2.0.0",
132+
"utf8"
133+
);
134+
});
135+
136+
it("returns upgrade message when cached version is newer", async () => {
137+
vi.mocked(fs.existsSync).mockReturnValue(true);
138+
vi.mocked(fsPromises.readFile).mockResolvedValue("2.0.0");
139+
94140
const result = await checkForUpdates();
95141
expect(result).toContain("Update available");
96142
});
143+
144+
it("handles errors gracefully during version check", async () => {
145+
vi.mocked(fs.existsSync).mockReturnValue(true);
146+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error("Test error"));
147+
148+
const result = await checkForUpdates();
149+
expect(result).toBe(null);
150+
});
151+
152+
it("handles errors gracefully during background update", async () => {
153+
vi.mocked(fs.existsSync).mockReturnValue(false);
154+
mockFetch.mockRejectedValue(new Error("Network error"));
155+
156+
const result = await checkForUpdates();
157+
expect(result).toBe(null);
158+
159+
// Wait for setImmediate to complete
160+
await new Promise((resolve) => setImmediate(resolve));
161+
162+
// Verify the error was handled
163+
expect(fsPromises.writeFile).not.toHaveBeenCalled();
164+
});
97165
});
98166
});

0 commit comments

Comments
 (0)