auq-mcp-server 2.4.0 → 2.6.0
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/README.md +40 -0
- package/dist/bin/auq.js +40 -0
- package/dist/bin/tui-app.js +115 -1
- package/dist/package.json +1 -1
- package/dist/src/cli/commands/sessions.js +144 -2
- package/dist/src/cli/commands/update.js +124 -0
- package/dist/src/config/__tests__/updateCheck.test.js +34 -0
- package/dist/src/config/defaults.js +2 -0
- package/dist/src/config/types.js +2 -0
- package/dist/src/tui/components/Footer.js +4 -1
- package/dist/src/tui/components/Header.js +3 -1
- package/dist/src/tui/components/UpdateBadge.js +29 -0
- package/dist/src/tui/components/UpdateOverlay.js +199 -0
- package/dist/src/tui/constants/keybindings.js +3 -0
- package/dist/src/update/__tests__/cache.test.js +136 -0
- package/dist/src/update/__tests__/changelog.test.js +86 -0
- package/dist/src/update/__tests__/checker.test.js +148 -0
- package/dist/src/update/__tests__/index.test.js +37 -0
- package/dist/src/update/__tests__/installer.test.js +117 -0
- package/dist/src/update/__tests__/package-manager.test.js +73 -0
- package/dist/src/update/__tests__/version.test.js +74 -0
- package/dist/src/update/cache.js +74 -0
- package/dist/src/update/changelog.js +63 -0
- package/dist/src/update/checker.js +121 -0
- package/dist/src/update/index.js +15 -0
- package/dist/src/update/installer.js +51 -0
- package/dist/src/update/package-manager.js +49 -0
- package/dist/src/update/types.js +7 -0
- package/dist/src/update/version.js +114 -0
- package/package.json +1 -1
- package/dist/src/tui/components/Spinner.js +0 -19
- package/dist/src/tui/utils/__tests__/detectTheme.test.js +0 -78
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as updateModule from "../index.js";
|
|
3
|
+
describe("update module exports", () => {
|
|
4
|
+
it("exports UpdateChecker class", () => {
|
|
5
|
+
expect(typeof updateModule.UpdateChecker).toBe("function");
|
|
6
|
+
});
|
|
7
|
+
it("exports fetchChangelog function", () => {
|
|
8
|
+
expect(typeof updateModule.fetchChangelog).toBe("function");
|
|
9
|
+
});
|
|
10
|
+
it("exports cache functions", () => {
|
|
11
|
+
expect(typeof updateModule.readCache).toBe("function");
|
|
12
|
+
expect(typeof updateModule.writeCache).toBe("function");
|
|
13
|
+
expect(typeof updateModule.clearUpdateCache).toBe("function");
|
|
14
|
+
expect(typeof updateModule.isCacheFresh).toBe("function");
|
|
15
|
+
expect(typeof updateModule.shouldSkipVersion).toBe("function");
|
|
16
|
+
expect(typeof updateModule.getCachePath).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
it("exports CACHE_TTL as 3600000", () => {
|
|
19
|
+
expect(updateModule.CACHE_TTL).toBe(3600000);
|
|
20
|
+
});
|
|
21
|
+
it("exports installer functions", () => {
|
|
22
|
+
expect(typeof updateModule.installUpdate).toBe("function");
|
|
23
|
+
expect(typeof updateModule.getManualCommand).toBe("function");
|
|
24
|
+
});
|
|
25
|
+
it("exports package manager functions", () => {
|
|
26
|
+
expect(typeof updateModule.detectPackageManager).toBe("function");
|
|
27
|
+
});
|
|
28
|
+
it("exports PACKAGE_NAME as auq-mcp-server", () => {
|
|
29
|
+
expect(updateModule.PACKAGE_NAME).toBe("auq-mcp-server");
|
|
30
|
+
});
|
|
31
|
+
it("exports version utilities", () => {
|
|
32
|
+
expect(typeof updateModule.parseVersion).toBe("function");
|
|
33
|
+
expect(typeof updateModule.isNewer).toBe("function");
|
|
34
|
+
expect(typeof updateModule.getUpdateType).toBe("function");
|
|
35
|
+
expect(typeof updateModule.getCurrentVersion).toBe("function");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
// Mock node:child_process
|
|
4
|
+
vi.mock("node:child_process", () => ({
|
|
5
|
+
spawn: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { installUpdate, getManualCommand } from "../installer.js";
|
|
9
|
+
describe("installer", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
describe("installUpdate", () => {
|
|
17
|
+
function createMockChild() {
|
|
18
|
+
const child = new EventEmitter();
|
|
19
|
+
return child;
|
|
20
|
+
}
|
|
21
|
+
it("should return true when exit code is 0", async () => {
|
|
22
|
+
const child = createMockChild();
|
|
23
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
24
|
+
const promise = installUpdate({
|
|
25
|
+
name: "npm",
|
|
26
|
+
installCommand: "npm install -g",
|
|
27
|
+
});
|
|
28
|
+
child.emit("close", 0);
|
|
29
|
+
const result = await promise;
|
|
30
|
+
expect(result).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it("should return false when exit code is 1", async () => {
|
|
33
|
+
const child = createMockChild();
|
|
34
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
35
|
+
const promise = installUpdate({
|
|
36
|
+
name: "npm",
|
|
37
|
+
installCommand: "npm install -g",
|
|
38
|
+
});
|
|
39
|
+
child.emit("close", 1);
|
|
40
|
+
const result = await promise;
|
|
41
|
+
expect(result).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it("should return false on spawn error", async () => {
|
|
44
|
+
const child = createMockChild();
|
|
45
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
46
|
+
const promise = installUpdate({
|
|
47
|
+
name: "npm",
|
|
48
|
+
installCommand: "npm install -g",
|
|
49
|
+
});
|
|
50
|
+
child.emit("error", new Error("ENOENT"));
|
|
51
|
+
const result = await promise;
|
|
52
|
+
expect(result).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it("should use correct args for npm", async () => {
|
|
55
|
+
const child = createMockChild();
|
|
56
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
57
|
+
const pm = {
|
|
58
|
+
name: "npm",
|
|
59
|
+
installCommand: "npm install -g",
|
|
60
|
+
};
|
|
61
|
+
const promise = installUpdate(pm);
|
|
62
|
+
child.emit("close", 0);
|
|
63
|
+
await promise;
|
|
64
|
+
expect(spawn).toHaveBeenCalledWith("npm", ["install", "-g", "auq-mcp-server"], expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }));
|
|
65
|
+
});
|
|
66
|
+
it("should use correct args for bun", async () => {
|
|
67
|
+
const child = createMockChild();
|
|
68
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
69
|
+
const pm = {
|
|
70
|
+
name: "bun",
|
|
71
|
+
installCommand: "bun add -g",
|
|
72
|
+
};
|
|
73
|
+
const promise = installUpdate(pm);
|
|
74
|
+
child.emit("close", 0);
|
|
75
|
+
await promise;
|
|
76
|
+
expect(spawn).toHaveBeenCalledWith("bun", ["add", "-g", "auq-mcp-server"], expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }));
|
|
77
|
+
});
|
|
78
|
+
it("should use correct args for yarn", async () => {
|
|
79
|
+
const child = createMockChild();
|
|
80
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
81
|
+
const pm = {
|
|
82
|
+
name: "yarn",
|
|
83
|
+
installCommand: "yarn global add",
|
|
84
|
+
};
|
|
85
|
+
const promise = installUpdate(pm);
|
|
86
|
+
child.emit("close", 0);
|
|
87
|
+
await promise;
|
|
88
|
+
expect(spawn).toHaveBeenCalledWith("yarn", ["global", "add", "auq-mcp-server"], expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }));
|
|
89
|
+
});
|
|
90
|
+
it("should use correct args for pnpm", async () => {
|
|
91
|
+
const child = createMockChild();
|
|
92
|
+
vi.mocked(spawn).mockReturnValue(child);
|
|
93
|
+
const pm = {
|
|
94
|
+
name: "pnpm",
|
|
95
|
+
installCommand: "pnpm add -g",
|
|
96
|
+
};
|
|
97
|
+
const promise = installUpdate(pm);
|
|
98
|
+
child.emit("close", 0);
|
|
99
|
+
await promise;
|
|
100
|
+
expect(spawn).toHaveBeenCalledWith("pnpm", ["add", "-g", "auq-mcp-server"], expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("getManualCommand", () => {
|
|
104
|
+
it("should return correct command for npm", () => {
|
|
105
|
+
expect(getManualCommand({ name: "npm", installCommand: "npm install -g" })).toBe("npm install -g auq-mcp-server");
|
|
106
|
+
});
|
|
107
|
+
it("should return correct command for bun", () => {
|
|
108
|
+
expect(getManualCommand({ name: "bun", installCommand: "bun add -g" })).toBe("bun add -g auq-mcp-server");
|
|
109
|
+
});
|
|
110
|
+
it("should return correct command for yarn", () => {
|
|
111
|
+
expect(getManualCommand({ name: "yarn", installCommand: "yarn global add" })).toBe("yarn global add auq-mcp-server");
|
|
112
|
+
});
|
|
113
|
+
it("should return correct command for pnpm", () => {
|
|
114
|
+
expect(getManualCommand({ name: "pnpm", installCommand: "pnpm add -g" })).toBe("pnpm add -g auq-mcp-server");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { detectPackageManager, PACKAGE_NAME } from "../package-manager.js";
|
|
3
|
+
describe("package manager detection", () => {
|
|
4
|
+
const originalEnv = { ...process.env };
|
|
5
|
+
const originalExecPath = process.execPath;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Clear relevant env vars so each test starts clean
|
|
8
|
+
delete process.env.npm_config_user_agent;
|
|
9
|
+
delete process.env.npm_execpath;
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
process.env = { ...originalEnv };
|
|
13
|
+
Object.defineProperty(process, "execPath", { value: originalExecPath });
|
|
14
|
+
});
|
|
15
|
+
describe("PACKAGE_NAME", () => {
|
|
16
|
+
it("should equal auq-mcp-server", () => {
|
|
17
|
+
expect(PACKAGE_NAME).toBe("auq-mcp-server");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("detectPackageManager", () => {
|
|
21
|
+
it("should detect bun from npm_config_user_agent", () => {
|
|
22
|
+
process.env.npm_config_user_agent = "bun/1.0.0 linux x64";
|
|
23
|
+
const result = detectPackageManager();
|
|
24
|
+
expect(result.name).toBe("bun");
|
|
25
|
+
expect(result.installCommand).toBe("bun add -g");
|
|
26
|
+
});
|
|
27
|
+
it("should detect yarn from npm_config_user_agent", () => {
|
|
28
|
+
process.env.npm_config_user_agent = "yarn/4.0.0 node/v20.0.0";
|
|
29
|
+
const result = detectPackageManager();
|
|
30
|
+
expect(result.name).toBe("yarn");
|
|
31
|
+
expect(result.installCommand).toBe("yarn global add");
|
|
32
|
+
});
|
|
33
|
+
it("should detect pnpm from npm_config_user_agent", () => {
|
|
34
|
+
process.env.npm_config_user_agent = "pnpm/8.0.0 node/v20.0.0";
|
|
35
|
+
const result = detectPackageManager();
|
|
36
|
+
expect(result.name).toBe("pnpm");
|
|
37
|
+
expect(result.installCommand).toBe("pnpm add -g");
|
|
38
|
+
});
|
|
39
|
+
it("should detect npm from npm_config_user_agent", () => {
|
|
40
|
+
process.env.npm_config_user_agent = "npm/10.2.0 node/v20.9.0 darwin arm64";
|
|
41
|
+
const result = detectPackageManager();
|
|
42
|
+
expect(result.name).toBe("npm");
|
|
43
|
+
expect(result.installCommand).toBe("npm install -g");
|
|
44
|
+
});
|
|
45
|
+
it("should fallback to npm_execpath when user_agent is empty", () => {
|
|
46
|
+
process.env.npm_config_user_agent = "";
|
|
47
|
+
process.env.npm_execpath = "/usr/local/lib/yarn/bin/yarn.js";
|
|
48
|
+
const result = detectPackageManager();
|
|
49
|
+
expect(result.name).toBe("yarn");
|
|
50
|
+
});
|
|
51
|
+
it("should fallback to process.execPath for bun detection", () => {
|
|
52
|
+
process.env.npm_config_user_agent = "";
|
|
53
|
+
process.env.npm_execpath = "";
|
|
54
|
+
Object.defineProperty(process, "execPath", {
|
|
55
|
+
value: "/home/user/.bun/bin/bun",
|
|
56
|
+
configurable: true,
|
|
57
|
+
});
|
|
58
|
+
const result = detectPackageManager();
|
|
59
|
+
expect(result.name).toBe("bun");
|
|
60
|
+
});
|
|
61
|
+
it("should default to npm when no indicators found", () => {
|
|
62
|
+
process.env.npm_config_user_agent = "";
|
|
63
|
+
process.env.npm_execpath = "";
|
|
64
|
+
Object.defineProperty(process, "execPath", {
|
|
65
|
+
value: "/usr/local/bin/node",
|
|
66
|
+
configurable: true,
|
|
67
|
+
});
|
|
68
|
+
const result = detectPackageManager();
|
|
69
|
+
expect(result.name).toBe("npm");
|
|
70
|
+
expect(result.installCommand).toBe("npm install -g");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseVersion, isNewer, getUpdateType } from "../version.js";
|
|
3
|
+
describe("version utilities", () => {
|
|
4
|
+
describe("parseVersion", () => {
|
|
5
|
+
it("should parse a standard semver string", () => {
|
|
6
|
+
const result = parseVersion("2.5.0");
|
|
7
|
+
expect(result).toEqual({ major: 2, minor: 5, patch: 0 });
|
|
8
|
+
});
|
|
9
|
+
it("should strip a leading 'v' prefix", () => {
|
|
10
|
+
const result = parseVersion("v1.2.3");
|
|
11
|
+
expect(result).toEqual({ major: 1, minor: 2, patch: 3 });
|
|
12
|
+
});
|
|
13
|
+
it("should treat two-part versions as patch 0", () => {
|
|
14
|
+
const result = parseVersion("2.5");
|
|
15
|
+
expect(result).toEqual({ major: 2, minor: 5, patch: 0 });
|
|
16
|
+
});
|
|
17
|
+
it("should capture prerelease identifiers", () => {
|
|
18
|
+
const result = parseVersion("1.0.0-beta.1");
|
|
19
|
+
expect(result).toEqual({
|
|
20
|
+
major: 1,
|
|
21
|
+
minor: 0,
|
|
22
|
+
patch: 0,
|
|
23
|
+
prerelease: "beta.1",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it("should throw on completely invalid input 'abc'", () => {
|
|
27
|
+
expect(() => parseVersion("abc")).toThrow("Invalid version string");
|
|
28
|
+
});
|
|
29
|
+
it("should parse empty string as 0.0.0 (all parts default to 0)", () => {
|
|
30
|
+
const result = parseVersion("");
|
|
31
|
+
expect(result).toEqual({ major: 0, minor: 0, patch: 0 });
|
|
32
|
+
});
|
|
33
|
+
it("should throw on version with non-numeric parts", () => {
|
|
34
|
+
expect(() => parseVersion("1.x.3")).toThrow("Invalid version string");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("isNewer", () => {
|
|
38
|
+
it("should detect a newer patch version", () => {
|
|
39
|
+
expect(isNewer("2.4.0", "2.4.1")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it("should detect a newer minor version", () => {
|
|
42
|
+
expect(isNewer("2.4.0", "2.5.0")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it("should detect a newer major version", () => {
|
|
45
|
+
expect(isNewer("2.4.0", "3.0.0")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it("should return false when versions are the same", () => {
|
|
48
|
+
expect(isNewer("2.4.0", "2.4.0")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it("should return false when latest is older", () => {
|
|
51
|
+
expect(isNewer("2.5.0", "2.4.0")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it("should treat prerelease as older than same release version", () => {
|
|
54
|
+
expect(isNewer("2.5.0-beta.1", "2.5.0")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it("should treat release as newer than same prerelease version", () => {
|
|
57
|
+
expect(isNewer("2.5.0", "2.5.0-beta.1")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("getUpdateType", () => {
|
|
61
|
+
it("should return 'patch' for patch-level update", () => {
|
|
62
|
+
expect(getUpdateType("2.4.0", "2.4.1")).toBe("patch");
|
|
63
|
+
});
|
|
64
|
+
it("should return 'minor' for minor-level update", () => {
|
|
65
|
+
expect(getUpdateType("2.4.0", "2.5.0")).toBe("minor");
|
|
66
|
+
});
|
|
67
|
+
it("should return 'major' for major-level update", () => {
|
|
68
|
+
expect(getUpdateType("2.4.0", "3.0.0")).toBe("major");
|
|
69
|
+
});
|
|
70
|
+
it("should return 'major' when major differs even with other changes", () => {
|
|
71
|
+
expect(getUpdateType("1.0.0", "2.1.3")).toBe("major");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache management for update check results.
|
|
3
|
+
*
|
|
4
|
+
* Stores update check data in ~/.config/auq/update-check.json (XDG-compliant).
|
|
5
|
+
* Provides cache freshness checking and skip-version tracking.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
/** Cache time-to-live: 1 hour in milliseconds */
|
|
11
|
+
export const CACHE_TTL = 3600000;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the cache file path using XDG-compliant directory.
|
|
14
|
+
* Respects $XDG_CONFIG_HOME if set, otherwise uses ~/.config.
|
|
15
|
+
*/
|
|
16
|
+
export function getCachePath() {
|
|
17
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
18
|
+
return join(xdgConfigHome, "auq", "update-check.json");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read and parse the cached update check data.
|
|
22
|
+
* Returns null if the cache file is missing, unreadable, or contains invalid JSON.
|
|
23
|
+
*/
|
|
24
|
+
export async function readCache() {
|
|
25
|
+
try {
|
|
26
|
+
const cachePath = getCachePath();
|
|
27
|
+
const content = await readFile(cachePath, "utf-8");
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Write update check data to the cache file.
|
|
36
|
+
* Creates the parent directory if it doesn't exist.
|
|
37
|
+
* Silently handles write errors.
|
|
38
|
+
*/
|
|
39
|
+
export async function writeCache(cache) {
|
|
40
|
+
try {
|
|
41
|
+
const cachePath = getCachePath();
|
|
42
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
43
|
+
await writeFile(cachePath, JSON.stringify(cache, null, 2), "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Silently handle write errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if the cache is still fresh (within the TTL window).
|
|
51
|
+
* Returns true if lastCheck is within the last hour.
|
|
52
|
+
*/
|
|
53
|
+
export function isCacheFresh(cache) {
|
|
54
|
+
return Date.now() - cache.lastCheck < CACHE_TTL;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if the user has chosen to skip the given version.
|
|
58
|
+
*/
|
|
59
|
+
export function shouldSkipVersion(cache, version) {
|
|
60
|
+
return cache.skippedVersion === version;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Delete the cache file to force a fresh update check.
|
|
64
|
+
* Silently handles errors if the file doesn't exist or can't be deleted.
|
|
65
|
+
*/
|
|
66
|
+
export async function clearUpdateCache() {
|
|
67
|
+
try {
|
|
68
|
+
const cachePath = getCachePath();
|
|
69
|
+
await unlink(cachePath);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Silently handle deletion errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Releases API changelog fetcher.
|
|
3
|
+
*
|
|
4
|
+
* Fetches release notes for a given version from the GitHub Releases API.
|
|
5
|
+
* Handles rate limiting (403/429) gracefully and caches results to reduce API calls.
|
|
6
|
+
* Always returns a fallback URL even when content is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
import { readCache, writeCache } from "./cache.js";
|
|
9
|
+
const GITHUB_API_URL = "https://api.github.com/repos/AlpacaLOS/auq/releases/tags";
|
|
10
|
+
/**
|
|
11
|
+
* Fetch changelog content from GitHub Releases API for a specific version.
|
|
12
|
+
*
|
|
13
|
+
* Uses cached changelog if available for the requested version.
|
|
14
|
+
* Gracefully handles rate limits (HTTP 403/429) by returning null content
|
|
15
|
+
* with a fallback URL to the GitHub release page.
|
|
16
|
+
*
|
|
17
|
+
* @param version - The version to fetch changelog for (e.g., "2.5.0")
|
|
18
|
+
* @returns Changelog content and fallback URL
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchChangelog(version) {
|
|
21
|
+
const fallbackUrl = `https://github.com/AlpacaLOS/auq/releases/tag/v${version}`;
|
|
22
|
+
try {
|
|
23
|
+
// Check cache first to avoid unnecessary API calls
|
|
24
|
+
const cache = await readCache();
|
|
25
|
+
if (cache?.changelog &&
|
|
26
|
+
cache?.changelogFetchedAt &&
|
|
27
|
+
cache.latestVersion === version) {
|
|
28
|
+
return { content: cache.changelog, fallbackUrl };
|
|
29
|
+
}
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
32
|
+
const response = await fetch(`${GITHUB_API_URL}/v${version}`, {
|
|
33
|
+
headers: {
|
|
34
|
+
Accept: "application/vnd.github.v3+json",
|
|
35
|
+
"User-Agent": "auq-mcp-server",
|
|
36
|
+
},
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
});
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
// Handle rate limiting — return fallback without content
|
|
41
|
+
if (response.status === 403 || response.status === 429) {
|
|
42
|
+
return { content: null, fallbackUrl };
|
|
43
|
+
}
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
return { content: null, fallbackUrl };
|
|
46
|
+
}
|
|
47
|
+
const release = (await response.json());
|
|
48
|
+
const content = release.body || null;
|
|
49
|
+
// Cache the changelog alongside existing cache data
|
|
50
|
+
if (content && cache) {
|
|
51
|
+
await writeCache({
|
|
52
|
+
...cache,
|
|
53
|
+
changelog: content,
|
|
54
|
+
changelogFetchedAt: Date.now(),
|
|
55
|
+
}).catch(() => { });
|
|
56
|
+
}
|
|
57
|
+
return { content, fallbackUrl };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Network errors, timeouts, parse errors — return fallback
|
|
61
|
+
return { content: null, fallbackUrl };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core update checker orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Uses a memoized promise pattern to deduplicate concurrent update checks.
|
|
5
|
+
* Reads cached data first, falls back to npm registry fetch with a 5-second
|
|
6
|
+
* timeout. Silently returns null on any error — update checks must never
|
|
7
|
+
* break the application.
|
|
8
|
+
*/
|
|
9
|
+
import { isNewer, getUpdateType, getCurrentVersion } from "./version.js";
|
|
10
|
+
import { readCache, writeCache, isCacheFresh, shouldSkipVersion, clearUpdateCache, } from "./cache.js";
|
|
11
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/auq-mcp-server";
|
|
12
|
+
const GITHUB_RELEASES_URL = "https://github.com/AlpacaLOS/auq/releases/tag";
|
|
13
|
+
export class UpdateChecker {
|
|
14
|
+
checkPromise = null;
|
|
15
|
+
currentVersion;
|
|
16
|
+
constructor(currentVersion) {
|
|
17
|
+
this.currentVersion = currentVersion || getCurrentVersion();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check for available updates.
|
|
21
|
+
*
|
|
22
|
+
* Uses a memoized promise to prevent duplicate concurrent requests.
|
|
23
|
+
* Returns null if the check should be skipped (CI, env vars, etc.),
|
|
24
|
+
* if no update is available, or if any error occurs.
|
|
25
|
+
*/
|
|
26
|
+
async check() {
|
|
27
|
+
if (this.shouldSkipCheck())
|
|
28
|
+
return null;
|
|
29
|
+
if (this.checkPromise)
|
|
30
|
+
return this.checkPromise;
|
|
31
|
+
this.checkPromise = this.performCheck();
|
|
32
|
+
return this.checkPromise;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Determine if the update check should be skipped entirely.
|
|
36
|
+
*
|
|
37
|
+
* Skips in CI environments, when NO_UPDATE_NOTIFIER is set,
|
|
38
|
+
* or when NODE_ENV is "test".
|
|
39
|
+
*/
|
|
40
|
+
shouldSkipCheck() {
|
|
41
|
+
// CI environments
|
|
42
|
+
if (process.env.CI === "true" || process.env.CI === "1")
|
|
43
|
+
return true;
|
|
44
|
+
// Explicit opt-out via NO_UPDATE_NOTIFIER
|
|
45
|
+
if (process.env.NO_UPDATE_NOTIFIER === "1" ||
|
|
46
|
+
process.env.NO_UPDATE_NOTIFIER === "true")
|
|
47
|
+
return true;
|
|
48
|
+
// Test environment
|
|
49
|
+
if (process.env.NODE_ENV === "test")
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Perform the actual update check.
|
|
55
|
+
*
|
|
56
|
+
* 1. Read cache — if fresh, use cached version info.
|
|
57
|
+
* 2. Fetch npm registry with 5s timeout for latest dist-tag.
|
|
58
|
+
* 3. Write updated cache (preserving skippedVersion and changelog).
|
|
59
|
+
* 4. Compare versions and return UpdateInfo or null.
|
|
60
|
+
*/
|
|
61
|
+
async performCheck() {
|
|
62
|
+
try {
|
|
63
|
+
// 1. Check cache first
|
|
64
|
+
const cache = await readCache();
|
|
65
|
+
if (cache && isCacheFresh(cache)) {
|
|
66
|
+
if (!isNewer(this.currentVersion, cache.latestVersion))
|
|
67
|
+
return null;
|
|
68
|
+
if (shouldSkipVersion(cache, cache.latestVersion))
|
|
69
|
+
return null;
|
|
70
|
+
return {
|
|
71
|
+
currentVersion: this.currentVersion,
|
|
72
|
+
latestVersion: cache.latestVersion,
|
|
73
|
+
updateType: getUpdateType(this.currentVersion, cache.latestVersion),
|
|
74
|
+
changelogUrl: `${GITHUB_RELEASES_URL}/v${cache.latestVersion}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// 2. Fetch from npm registry
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
80
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
81
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
if (!response.ok)
|
|
86
|
+
return null;
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
const latestVersion = data["dist-tags"]?.latest;
|
|
89
|
+
if (!latestVersion)
|
|
90
|
+
return null;
|
|
91
|
+
// 3. Write cache (preserve skippedVersion and changelog from old cache)
|
|
92
|
+
const newCache = {
|
|
93
|
+
lastCheck: Date.now(),
|
|
94
|
+
latestVersion,
|
|
95
|
+
skippedVersion: cache?.skippedVersion,
|
|
96
|
+
changelog: cache?.changelog,
|
|
97
|
+
changelogFetchedAt: cache?.changelogFetchedAt,
|
|
98
|
+
};
|
|
99
|
+
await writeCache(newCache).catch(() => { });
|
|
100
|
+
// 4. Compare versions
|
|
101
|
+
if (!isNewer(this.currentVersion, latestVersion))
|
|
102
|
+
return null;
|
|
103
|
+
if (cache && shouldSkipVersion(cache, latestVersion))
|
|
104
|
+
return null;
|
|
105
|
+
return {
|
|
106
|
+
currentVersion: this.currentVersion,
|
|
107
|
+
latestVersion,
|
|
108
|
+
updateType: getUpdateType(this.currentVersion, latestVersion),
|
|
109
|
+
changelogUrl: `${GITHUB_RELEASES_URL}/v${latestVersion}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Reset the memoized promise and clear the on-disk cache. */
|
|
117
|
+
clearCache() {
|
|
118
|
+
this.checkPromise = null;
|
|
119
|
+
clearUpdateCache().catch(() => { });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update module for AUQ.
|
|
3
|
+
*
|
|
4
|
+
* Provides update checking against the npm registry, version comparison,
|
|
5
|
+
* XDG-compliant cache management, package manager detection, changelog
|
|
6
|
+
* fetching from GitHub Releases, and update installation.
|
|
7
|
+
*
|
|
8
|
+
* @module update
|
|
9
|
+
*/
|
|
10
|
+
export { UpdateChecker } from "./checker.js";
|
|
11
|
+
export { fetchChangelog } from "./changelog.js";
|
|
12
|
+
export { readCache, writeCache, clearUpdateCache, isCacheFresh, shouldSkipVersion, getCachePath, CACHE_TTL, } from "./cache.js";
|
|
13
|
+
export { installUpdate, getManualCommand } from "./installer.js";
|
|
14
|
+
export { detectPackageManager, PACKAGE_NAME } from "./package-manager.js";
|
|
15
|
+
export { parseVersion, isNewer, getUpdateType, getCurrentVersion, } from "./version.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update installation executor.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the detected package manager to install the latest version.
|
|
5
|
+
* Uses node:child_process for portability across Bun and Node.js runtimes.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
const PACKAGE_NAME = "auq-mcp-server";
|
|
9
|
+
/**
|
|
10
|
+
* Install the latest version of the package using the detected package manager.
|
|
11
|
+
*
|
|
12
|
+
* Spawns the package manager's global install command as a child process.
|
|
13
|
+
* Returns true on success (exit code 0), false on any failure.
|
|
14
|
+
* Never rejects — all errors are caught and resolved as false.
|
|
15
|
+
*
|
|
16
|
+
* @param packageManager - Detected package manager info with install command
|
|
17
|
+
* @returns Whether the installation succeeded
|
|
18
|
+
*/
|
|
19
|
+
export async function installUpdate(packageManager) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
try {
|
|
22
|
+
const [command, ...baseArgs] = packageManager.installCommand.split(" ");
|
|
23
|
+
const args = [...baseArgs, PACKAGE_NAME];
|
|
24
|
+
const child = spawn(command, args, {
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
shell: process.platform === "win32",
|
|
27
|
+
});
|
|
28
|
+
child.on("error", () => {
|
|
29
|
+
resolve(false);
|
|
30
|
+
});
|
|
31
|
+
child.on("close", (code) => {
|
|
32
|
+
resolve(code === 0);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
resolve(false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the manual install command string for display to the user.
|
|
42
|
+
*
|
|
43
|
+
* Used as a fallback when automatic installation fails, allowing
|
|
44
|
+
* the user to copy and run the command manually.
|
|
45
|
+
*
|
|
46
|
+
* @param packageManager - Detected package manager info
|
|
47
|
+
* @returns Full install command string (e.g., "npm install -g auq-mcp-server")
|
|
48
|
+
*/
|
|
49
|
+
export function getManualCommand(packageManager) {
|
|
50
|
+
return `${packageManager.installCommand} ${PACKAGE_NAME}`;
|
|
51
|
+
}
|