Skip to content

Commit 7ea5414

Browse files
committed
a very fast and simple version check
1 parent af9f8af commit 7ea5414

File tree

6 files changed

+150
-66
lines changed

6 files changed

+150
-66
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/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
});

src/utils/versionCheck.ts

Lines changed: 48 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,93 @@
11
import { Logger } from "./logger.js";
22
import chalk from "chalk";
33
import { createRequire } from "module";
4+
import * as path from "path";
45
import type { PackageJson } from "type-fest";
6+
import { getSettingsDir } from "../settings/settings.js";
7+
import * as fsPromises from "fs/promises";
8+
import * as fs from "fs";
9+
import semver from "semver";
510

611
const require = createRequire(import.meta.url);
712
const logger = new Logger({ name: "version-check" });
813

9-
/**
10-
* Gets the current package info from package.json
11-
*/
1214
export function getPackageInfo(): {
13-
name: string | undefined;
14-
version: string | undefined;
15+
name: string;
16+
version: string;
1517
} {
1618
const packageInfo = require("../../package.json") as PackageJson;
19+
if (!packageInfo.name || !packageInfo.version) {
20+
throw new Error("Unable to determine package info");
21+
}
22+
1723
return {
1824
name: packageInfo.name,
1925
version: packageInfo.version,
2026
};
2127
}
2228

23-
/**
24-
* Checks if the package is running as a global npm package
25-
*/
26-
export function isGlobalPackage(): boolean {
27-
return !!process.env.npm_config_global;
28-
}
29+
export async function fetchLatestVersion(packageName: string): Promise<string> {
30+
const registryUrl = `https://registry.npmjs.org/${packageName}/latest`;
31+
const response = await fetch(registryUrl);
2932

30-
/**
31-
* Fetches the latest version of a package from npm registry
32-
*/
33-
export async function fetchLatestVersion(
34-
packageName: string,
35-
): Promise<string | null> {
36-
try {
37-
const registryUrl = `https://registry.npmjs.org/${packageName}/latest`;
38-
const response = await fetch(registryUrl);
39-
40-
if (!response.ok) {
41-
throw new Error(`Failed to fetch version info: ${response.statusText}`);
42-
}
33+
if (!response.ok) {
34+
throw new Error(`Failed to fetch version info: ${response.statusText}`);
35+
}
4336

44-
const data = (await response.json()) as { version: string | undefined };
45-
return data.version ?? null;
46-
} catch (error) {
47-
logger.warn(
48-
"Error fetching latest version:",
49-
error instanceof Error ? error.message : String(error),
50-
);
51-
return null;
37+
const data = (await response.json()) as { version: string | undefined };
38+
if (!data.version) {
39+
throw new Error("Version info not found in response");
5240
}
41+
return data.version;
5342
}
5443

55-
/**
56-
* Generates an upgrade message if versions differ
57-
*/
5844
export function generateUpgradeMessage(
5945
currentVersion: string,
6046
latestVersion: string,
61-
packageName: string,
47+
packageName: string
6248
): string | null {
63-
return currentVersion !== latestVersion
49+
return semver.gt(latestVersion, currentVersion)
6450
? chalk.green(
65-
` Update available: ${currentVersion}${latestVersion}\n Run 'npm install -g ${packageName}' to update`,
51+
` Update available: ${currentVersion}${latestVersion}\n Run 'npm install -g ${packageName}' to update`
6652
)
6753
: null;
6854
}
6955

70-
/**
71-
* Checks if a newer version of the package is available on npm.
72-
* Only runs check when package is installed globally.
73-
*
74-
* @returns Upgrade message string if update available, null otherwise
75-
*/
7656
export async function checkForUpdates(): Promise<string | null> {
7757
try {
7858
const { name: packageName, version: currentVersion } = getPackageInfo();
7959

80-
if (!packageName || !currentVersion) {
81-
logger.warn("Unable to determine current package name or version");
82-
return null;
60+
const settingDir = getSettingsDir();
61+
const versionFilePath = path.join(settingDir, "lastVersionCheck");
62+
if (fs.existsSync(versionFilePath)) {
63+
const lastVersionCheck = await fsPromises.readFile(
64+
versionFilePath,
65+
"utf8"
66+
);
67+
return generateUpgradeMessage(
68+
currentVersion,
69+
lastVersionCheck,
70+
packageName
71+
);
8372
}
8473

85-
const latestVersion = await fetchLatestVersion(packageName);
74+
fetchLatestVersion(packageName)
75+
.then(async (latestVersion) => {
76+
return fsPromises.writeFile(versionFilePath, latestVersion, "utf8");
77+
})
78+
.catch((error) => {
79+
logger.warn(
80+
"Error fetching latest version:",
81+
error instanceof Error ? error.message : String(error)
82+
);
83+
});
8684

87-
if (!latestVersion) {
88-
logger.warn("Unable to determine latest published version");
89-
return null;
90-
}
91-
92-
return generateUpgradeMessage(currentVersion, latestVersion, packageName);
85+
return null;
9386
} catch (error) {
9487
// Log error but don't throw to handle gracefully
9588
logger.warn(
9689
"Error checking for updates:",
97-
error instanceof Error ? error.message : String(error),
90+
error instanceof Error ? error.message : String(error)
9891
);
9992
return null;
10093
}

0 commit comments

Comments
 (0)