@lvnt/release-radar 1.0.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.
Files changed (41) hide show
  1. package/.env.example +2 -0
  2. package/README.md +188 -0
  3. package/bin/release-radar.js +43 -0
  4. package/config/tools.json +80 -0
  5. package/dist/checker.d.ts +10 -0
  6. package/dist/checker.js +35 -0
  7. package/dist/checker.test.d.ts +1 -0
  8. package/dist/checker.test.js +64 -0
  9. package/dist/fetchers/custom.d.ts +3 -0
  10. package/dist/fetchers/custom.js +62 -0
  11. package/dist/fetchers/custom.test.d.ts +1 -0
  12. package/dist/fetchers/custom.test.js +74 -0
  13. package/dist/fetchers/github-release.d.ts +5 -0
  14. package/dist/fetchers/github-release.js +21 -0
  15. package/dist/fetchers/github-release.test.d.ts +1 -0
  16. package/dist/fetchers/github-release.test.js +35 -0
  17. package/dist/fetchers/index.d.ts +2 -0
  18. package/dist/fetchers/index.js +36 -0
  19. package/dist/fetchers/index.test.d.ts +1 -0
  20. package/dist/fetchers/index.test.js +43 -0
  21. package/dist/fetchers/npm.d.ts +1 -0
  22. package/dist/fetchers/npm.js +10 -0
  23. package/dist/fetchers/npm.test.d.ts +1 -0
  24. package/dist/fetchers/npm.test.js +26 -0
  25. package/dist/fetchers/vscode-marketplace.d.ts +1 -0
  26. package/dist/fetchers/vscode-marketplace.js +25 -0
  27. package/dist/fetchers/vscode-marketplace.test.d.ts +1 -0
  28. package/dist/fetchers/vscode-marketplace.test.js +39 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +85 -0
  31. package/dist/notifier.d.ts +19 -0
  32. package/dist/notifier.js +30 -0
  33. package/dist/notifier.test.d.ts +1 -0
  34. package/dist/notifier.test.js +37 -0
  35. package/dist/storage.d.ts +14 -0
  36. package/dist/storage.js +37 -0
  37. package/dist/storage.test.d.ts +1 -0
  38. package/dist/storage.test.js +53 -0
  39. package/dist/types.d.ts +14 -0
  40. package/dist/types.js +1 -0
  41. package/package.json +57 -0
package/.env.example ADDED
@@ -0,0 +1,2 @@
1
+ TELEGRAM_BOT_TOKEN=your_bot_token_here
2
+ TELEGRAM_CHAT_ID=your_chat_id_here
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # ReleaseRadar
2
+
3
+ A Node.js service that monitors tool/software versions and sends Telegram notifications when updates are detected.
4
+
5
+ Built for environments with limited internet access (e.g., intranet) where manual version checking is tedious.
6
+
7
+ ## Features
8
+
9
+ - Monitors 15+ tools from various sources:
10
+ - GitHub Releases
11
+ - npm Registry
12
+ - VS Code Marketplace
13
+ - Custom APIs (VSCode, Claude Code CLI, CMake)
14
+ - Sends Telegram notifications on version changes
15
+ - Batched notifications (multiple updates in one message)
16
+ - Periodic checks via cron (configurable interval)
17
+ - Manual check via Telegram `/check` command
18
+ - Persistent version storage (survives restarts)
19
+
20
+ ## Tracked Tools
21
+
22
+ | Tool | Source |
23
+ |------|--------|
24
+ | VSCode | VS Code Update API |
25
+ | Claude Code CLI | Google Storage / GitHub |
26
+ | Ninja | GitHub |
27
+ | CMake | cmake.org |
28
+ | Git | GitHub (git-for-windows) |
29
+ | Clangd | GitHub |
30
+ | Wezterm | GitHub |
31
+ | Ralphy | npm |
32
+ | vscode-cpptools | GitHub |
33
+ | vscode-clangd | GitHub |
34
+ | Claude Code VSCode | VS Code Marketplace |
35
+ | CMake Tools | GitHub |
36
+ | Roo Code | GitHub |
37
+ | Atlascode | GitHub |
38
+ | Zed | GitHub |
39
+
40
+ ## Prerequisites
41
+
42
+ - Node.js 18+
43
+ - Telegram Bot Token (from [@BotFather](https://t.me/botfather))
44
+ - Telegram Chat ID (your user ID or group ID)
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ # Clone the repository
50
+ git clone https://github.com/lvntbkdmr/release-radar.git
51
+ cd release-radar
52
+
53
+ # Install dependencies
54
+ npm install
55
+
56
+ # Configure environment
57
+ cp .env.example .env
58
+ # Edit .env with your Telegram credentials
59
+
60
+ # Build
61
+ npm run build
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ ### Environment Variables
67
+
68
+ Create a `.env` file:
69
+
70
+ ```env
71
+ TELEGRAM_BOT_TOKEN=your_bot_token_here
72
+ TELEGRAM_CHAT_ID=your_chat_id_here
73
+ ```
74
+
75
+ ### Tools Configuration
76
+
77
+ Edit `config/tools.json` to add/remove tools:
78
+
79
+ ```json
80
+ {
81
+ "checkIntervalHours": 6,
82
+ "tools": [
83
+ {
84
+ "name": "MyTool",
85
+ "type": "github",
86
+ "repo": "owner/repo"
87
+ }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ #### Tool Types
93
+
94
+ | Type | Required Fields | Description |
95
+ |------|-----------------|-------------|
96
+ | `github` | `repo` | GitHub releases (e.g., `"owner/repo"`) |
97
+ | `npm` | `package` | npm registry package |
98
+ | `vscode-marketplace` | `extensionId` | VS Code extension (e.g., `"publisher.extension"`) |
99
+ | `custom` | `customFetcher` | Built-in fetchers: `vscode`, `claude-cli`, `cmake` |
100
+
101
+ ## Usage
102
+
103
+ ### Development
104
+ ```bash
105
+ npm run dev
106
+ ```
107
+
108
+ ### Production
109
+ ```bash
110
+ npm run build
111
+ npm start
112
+ ```
113
+
114
+ ### With pm2 (recommended)
115
+ ```bash
116
+ npm install -g pm2
117
+ pm2 start npm --name "release-radar" -- start
118
+ pm2 save
119
+ ```
120
+
121
+ ### Telegram Commands
122
+
123
+ | Command | Description |
124
+ |---------|-------------|
125
+ | `/check` | Manually trigger version check |
126
+ | `/status` | Show all tracked versions |
127
+ | `/interval` | Show current check interval |
128
+ | `/setinterval <hours>` | Set check interval (1-24 hours) |
129
+
130
+ ## Project Structure
131
+
132
+ ```
133
+ release-radar/
134
+ ├── src/
135
+ │ ├── index.ts # Main entry point
136
+ │ ├── checker.ts # Version check orchestration
137
+ │ ├── storage.ts # JSON persistence
138
+ │ ├── notifier.ts # Telegram notifications
139
+ │ ├── types.ts # TypeScript interfaces
140
+ │ └── fetchers/
141
+ │ ├── index.ts # Fetcher registry
142
+ │ ├── github-release.ts
143
+ │ ├── npm.ts
144
+ │ ├── vscode-marketplace.ts
145
+ │ └── custom.ts
146
+ ├── config/
147
+ │ └── tools.json # Tool configuration
148
+ ├── data/
149
+ │ └── versions.json # Persisted version state
150
+ ├── docs/
151
+ │ └── OPERATIONS.md # Operations guide
152
+ └── dist/ # Compiled JavaScript
153
+ ```
154
+
155
+ ## Testing
156
+
157
+ ```bash
158
+ # Run all tests
159
+ npm test
160
+
161
+ # Watch mode
162
+ npm run test:watch
163
+ ```
164
+
165
+ ## Notifications
166
+
167
+ ### Version Update
168
+ ```
169
+ 🔄 Ninja: 1.11.1 → 1.12.0
170
+ 🔄 Git: 2.43.0 → 2.44.0
171
+ ```
172
+
173
+ ### Fetch Failure
174
+ ```
175
+ ⚠️ Failed to check CMake: Request timeout
176
+ ```
177
+
178
+ ## Operations
179
+
180
+ See [docs/OPERATIONS.md](docs/OPERATIONS.md) for detailed instructions on:
181
+ - Starting/stopping the service
182
+ - Viewing logs
183
+ - Auto-start configuration
184
+ - Troubleshooting
185
+
186
+ ## License
187
+
188
+ ISC
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { existsSync, mkdirSync, copyFileSync } from 'fs';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const packageRoot = join(__dirname, '..');
10
+
11
+ // Ensure config directory exists in current working directory
12
+ const configDir = join(process.cwd(), 'config');
13
+ const dataDir = join(process.cwd(), 'data');
14
+
15
+ if (!existsSync(configDir)) {
16
+ mkdirSync(configDir, { recursive: true });
17
+ // Copy default config
18
+ const defaultConfig = join(packageRoot, 'config', 'tools.json');
19
+ if (existsSync(defaultConfig)) {
20
+ copyFileSync(defaultConfig, join(configDir, 'tools.json'));
21
+ console.log('Created config/tools.json with default configuration');
22
+ }
23
+ }
24
+
25
+ if (!existsSync(dataDir)) {
26
+ mkdirSync(dataDir, { recursive: true });
27
+ }
28
+
29
+ // Check for .env file
30
+ const envFile = join(process.cwd(), '.env');
31
+ if (!existsSync(envFile)) {
32
+ const envExample = join(packageRoot, '.env.example');
33
+ if (existsSync(envExample)) {
34
+ copyFileSync(envExample, envFile);
35
+ console.log('Created .env file - please edit with your Telegram credentials');
36
+ console.log(' TELEGRAM_BOT_TOKEN=your_bot_token_here');
37
+ console.log(' TELEGRAM_CHAT_ID=your_chat_id_here');
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ // Run the main application
43
+ import('../dist/index.js');
@@ -0,0 +1,80 @@
1
+ {
2
+ "checkIntervalHours": 1,
3
+ "tools": [
4
+ {
5
+ "name": "VSCode",
6
+ "type": "custom",
7
+ "customFetcher": "vscode"
8
+ },
9
+ {
10
+ "name": "Claude Code CLI",
11
+ "type": "custom",
12
+ "customFetcher": "claude-cli"
13
+ },
14
+ {
15
+ "name": "Ninja",
16
+ "type": "github",
17
+ "repo": "ninja-build/ninja"
18
+ },
19
+ {
20
+ "name": "CMake",
21
+ "type": "custom",
22
+ "customFetcher": "cmake"
23
+ },
24
+ {
25
+ "name": "Git",
26
+ "type": "github",
27
+ "repo": "git-for-windows/git"
28
+ },
29
+ {
30
+ "name": "Clangd",
31
+ "type": "github",
32
+ "repo": "clangd/clangd"
33
+ },
34
+ {
35
+ "name": "Wezterm",
36
+ "type": "github",
37
+ "repo": "wezterm/wezterm"
38
+ },
39
+ {
40
+ "name": "Ralphy",
41
+ "type": "npm",
42
+ "package": "ralphy-cli"
43
+ },
44
+ {
45
+ "name": "vscode-cpptools",
46
+ "type": "github",
47
+ "repo": "microsoft/vscode-cpptools"
48
+ },
49
+ {
50
+ "name": "vscode-clangd",
51
+ "type": "github",
52
+ "repo": "clangd/vscode-clangd"
53
+ },
54
+ {
55
+ "name": "Claude Code VSCode",
56
+ "type": "vscode-marketplace",
57
+ "extensionId": "anthropic.claude-code"
58
+ },
59
+ {
60
+ "name": "CMake Tools",
61
+ "type": "github",
62
+ "repo": "microsoft/vscode-cmake-tools"
63
+ },
64
+ {
65
+ "name": "Roo Code",
66
+ "type": "github",
67
+ "repo": "RooCodeInc/Roo-Code"
68
+ },
69
+ {
70
+ "name": "Atlascode",
71
+ "type": "github",
72
+ "repo": "atlassian/atlascode"
73
+ },
74
+ {
75
+ "name": "Zed",
76
+ "type": "github",
77
+ "repo": "zed-industries/zed"
78
+ }
79
+ ]
80
+ }
@@ -0,0 +1,10 @@
1
+ import type { ToolConfig } from './types.js';
2
+ import type { Storage } from './storage.js';
3
+ import type { Notifier } from './notifier.js';
4
+ export declare class Checker {
5
+ private tools;
6
+ private storage;
7
+ private notifier;
8
+ constructor(tools: ToolConfig[], storage: Storage, notifier: Notifier);
9
+ checkAll(): Promise<void>;
10
+ }
@@ -0,0 +1,35 @@
1
+ import { fetchVersion } from './fetchers/index.js';
2
+ export class Checker {
3
+ tools;
4
+ storage;
5
+ notifier;
6
+ constructor(tools, storage, notifier) {
7
+ this.tools = tools;
8
+ this.storage = storage;
9
+ this.notifier = notifier;
10
+ }
11
+ async checkAll() {
12
+ const updates = [];
13
+ const failures = [];
14
+ for (const tool of this.tools) {
15
+ try {
16
+ const newVersion = await fetchVersion(tool);
17
+ const oldVersion = this.storage.getVersion(tool.name);
18
+ if (oldVersion === null) {
19
+ // First run - store without notifying
20
+ this.storage.setVersion(tool.name, newVersion);
21
+ }
22
+ else if (oldVersion !== newVersion) {
23
+ updates.push({ name: tool.name, oldVersion, newVersion });
24
+ this.storage.setVersion(tool.name, newVersion);
25
+ }
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ failures.push({ name: tool.name, error: message });
30
+ }
31
+ }
32
+ await this.notifier.sendBatchedUpdates(updates);
33
+ await this.notifier.sendBatchedFailures(failures);
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ // src/checker.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { Checker } from './checker.js';
4
+ vi.mock('./fetchers/index.js', () => ({
5
+ fetchVersion: vi.fn()
6
+ }));
7
+ import { fetchVersion } from './fetchers/index.js';
8
+ describe('Checker', () => {
9
+ let mockStorage;
10
+ let mockNotifier;
11
+ let checker;
12
+ let tools;
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ mockStorage = {
16
+ getVersion: vi.fn(),
17
+ setVersion: vi.fn()
18
+ };
19
+ mockNotifier = {
20
+ sendBatchedUpdates: vi.fn().mockResolvedValue(undefined),
21
+ sendBatchedFailures: vi.fn().mockResolvedValue(undefined)
22
+ };
23
+ tools = [
24
+ { name: 'Ninja', type: 'github', repo: 'ninja-build/ninja' },
25
+ { name: 'Git', type: 'github', repo: 'git-for-windows/git' }
26
+ ];
27
+ checker = new Checker(tools, mockStorage, mockNotifier);
28
+ });
29
+ it('notifies on version change', async () => {
30
+ mockStorage.getVersion.mockReturnValueOnce('1.11.1').mockReturnValueOnce('2.43.0');
31
+ vi.mocked(fetchVersion)
32
+ .mockResolvedValueOnce('1.12.0')
33
+ .mockResolvedValueOnce('2.43.0');
34
+ await checker.checkAll();
35
+ expect(mockNotifier.sendBatchedUpdates).toHaveBeenCalledWith([
36
+ { name: 'Ninja', oldVersion: '1.11.1', newVersion: '1.12.0' }
37
+ ]);
38
+ expect(mockStorage.setVersion).toHaveBeenCalledWith('Ninja', '1.12.0');
39
+ });
40
+ it('skips notification on first run (no stored version)', async () => {
41
+ mockStorage.getVersion.mockReturnValue(null);
42
+ vi.mocked(fetchVersion).mockResolvedValue('1.12.0');
43
+ await checker.checkAll();
44
+ expect(mockNotifier.sendBatchedUpdates).toHaveBeenCalledWith([]);
45
+ expect(mockStorage.setVersion).toHaveBeenCalledTimes(2);
46
+ });
47
+ it('notifies on fetch failure', async () => {
48
+ mockStorage.getVersion.mockReturnValue('1.11.1');
49
+ vi.mocked(fetchVersion)
50
+ .mockRejectedValueOnce(new Error('Timeout'))
51
+ .mockResolvedValueOnce('2.43.0');
52
+ await checker.checkAll();
53
+ expect(mockNotifier.sendBatchedFailures).toHaveBeenCalledWith([
54
+ { name: 'Ninja', error: 'Timeout' }
55
+ ]);
56
+ });
57
+ it('does not notify when version unchanged', async () => {
58
+ mockStorage.getVersion.mockReturnValue('1.12.0');
59
+ vi.mocked(fetchVersion).mockResolvedValue('1.12.0');
60
+ await checker.checkAll();
61
+ expect(mockNotifier.sendBatchedUpdates).toHaveBeenCalledWith([]);
62
+ expect(mockStorage.setVersion).not.toHaveBeenCalled();
63
+ });
64
+ });
@@ -0,0 +1,3 @@
1
+ export declare function fetchVSCodeVersion(): Promise<string>;
2
+ export declare function fetchClaudeCodeCLI(): Promise<string>;
3
+ export declare function fetchCMakeVersion(): Promise<string>;
@@ -0,0 +1,62 @@
1
+ // src/fetchers/custom.ts
2
+ export async function fetchVSCodeVersion() {
3
+ const response = await fetch('https://update.code.visualstudio.com/api/releases/stable');
4
+ if (!response.ok) {
5
+ throw new Error(`VSCode API error: ${response.status} ${response.statusText}`);
6
+ }
7
+ const releases = await response.json();
8
+ return releases[0];
9
+ }
10
+ export async function fetchClaudeCodeCLI() {
11
+ const primaryUrl = 'https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest';
12
+ const fallbackUrl = 'https://api.github.com/repos/anthropics/claude-code/releases/latest';
13
+ try {
14
+ const response = await fetch(primaryUrl);
15
+ if (response.ok) {
16
+ const version = await response.text();
17
+ return version.trim();
18
+ }
19
+ }
20
+ catch {
21
+ // Fall through to fallback
22
+ }
23
+ const response = await fetch(fallbackUrl, {
24
+ headers: {
25
+ 'Accept': 'application/vnd.github.v3+json',
26
+ 'User-Agent': 'ReleaseRadar/1.0'
27
+ }
28
+ });
29
+ if (!response.ok) {
30
+ throw new Error(`Claude Code CLI fetch failed: ${response.status}`);
31
+ }
32
+ const data = await response.json();
33
+ return data.tag_name.replace(/^v/, '');
34
+ }
35
+ export async function fetchCMakeVersion() {
36
+ const jsonUrl = 'https://cmake.org/files/LatestRelease/cmake-latest-files-v1.json';
37
+ const fallbackUrl = 'https://cmake.org/files/LatestRelease/';
38
+ // Try JSON endpoint first
39
+ try {
40
+ const response = await fetch(jsonUrl);
41
+ if (response.ok) {
42
+ const data = await response.json();
43
+ return data.version.string;
44
+ }
45
+ }
46
+ catch {
47
+ // Fall through to HTML parsing
48
+ }
49
+ // Fallback: parse HTML directory listing (items are oldest to newest)
50
+ const response = await fetch(fallbackUrl);
51
+ if (!response.ok) {
52
+ throw new Error(`CMake fetch error: ${response.status} ${response.statusText}`);
53
+ }
54
+ const html = await response.text();
55
+ const matches = html.matchAll(/cmake-(\d+\.\d+\.\d+)/g);
56
+ const versions = [...matches].map(m => m[1]);
57
+ if (versions.length === 0) {
58
+ throw new Error('Could not parse CMake version from directory listing');
59
+ }
60
+ // Return last match (newest version)
61
+ return versions[versions.length - 1];
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ // src/fetchers/custom.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { fetchVSCodeVersion, fetchClaudeCodeCLI, fetchCMakeVersion } from './custom.js';
4
+ describe('custom fetchers', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ describe('fetchVSCodeVersion', () => {
9
+ it('extracts first version from releases array', async () => {
10
+ global.fetch = vi.fn().mockResolvedValue({
11
+ ok: true,
12
+ json: () => Promise.resolve(['1.96.0', '1.95.3', '1.95.2'])
13
+ });
14
+ const version = await fetchVSCodeVersion();
15
+ expect(version).toBe('1.96.0');
16
+ expect(fetch).toHaveBeenCalledWith('https://update.code.visualstudio.com/api/releases/stable');
17
+ });
18
+ });
19
+ describe('fetchClaudeCodeCLI', () => {
20
+ it('extracts version from text response', async () => {
21
+ global.fetch = vi.fn().mockResolvedValue({
22
+ ok: true,
23
+ text: () => Promise.resolve('1.0.5')
24
+ });
25
+ const version = await fetchClaudeCodeCLI();
26
+ expect(version).toBe('1.0.5');
27
+ });
28
+ it('falls back to GitHub on primary failure', async () => {
29
+ global.fetch = vi.fn()
30
+ .mockResolvedValueOnce({ ok: false, status: 503 })
31
+ .mockResolvedValueOnce({
32
+ ok: true,
33
+ json: () => Promise.resolve({ tag_name: 'v1.0.6' })
34
+ });
35
+ const version = await fetchClaudeCodeCLI();
36
+ expect(version).toBe('1.0.6');
37
+ });
38
+ });
39
+ describe('fetchCMakeVersion', () => {
40
+ it('extracts version from JSON endpoint', async () => {
41
+ global.fetch = vi.fn().mockResolvedValue({
42
+ ok: true,
43
+ json: () => Promise.resolve({ version: { string: '3.31.0' } })
44
+ });
45
+ const version = await fetchCMakeVersion();
46
+ expect(version).toBe('3.31.0');
47
+ expect(fetch).toHaveBeenCalledWith('https://cmake.org/files/LatestRelease/cmake-latest-files-v1.json');
48
+ });
49
+ it('falls back to HTML and returns newest (last) version', async () => {
50
+ const html = `
51
+ <a href="cmake-3.28.0-linux-x86_64.tar.gz">cmake-3.28.0-linux-x86_64.tar.gz</a>
52
+ <a href="cmake-3.31.0-linux-x86_64.tar.gz">cmake-3.31.0-linux-x86_64.tar.gz</a>
53
+ `;
54
+ global.fetch = vi.fn()
55
+ .mockResolvedValueOnce({ ok: false, status: 404 })
56
+ .mockResolvedValueOnce({
57
+ ok: true,
58
+ text: () => Promise.resolve(html)
59
+ });
60
+ const version = await fetchCMakeVersion();
61
+ expect(version).toBe('3.31.0');
62
+ });
63
+ it('throws when no version found in listing', async () => {
64
+ global.fetch = vi.fn()
65
+ .mockResolvedValueOnce({ ok: false, status: 404 })
66
+ .mockResolvedValueOnce({
67
+ ok: true,
68
+ text: () => Promise.resolve('<html>empty</html>')
69
+ });
70
+ await expect(fetchCMakeVersion())
71
+ .rejects.toThrow('Could not parse CMake version from directory listing');
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Fetches the latest stable release version from a GitHub repository.
3
+ * Uses /releases/latest endpoint which excludes pre-releases and drafts by design.
4
+ */
5
+ export declare function fetchGitHubRelease(repo: string): Promise<string>;
@@ -0,0 +1,21 @@
1
+ // src/fetchers/github-release.ts
2
+ /**
3
+ * Fetches the latest stable release version from a GitHub repository.
4
+ * Uses /releases/latest endpoint which excludes pre-releases and drafts by design.
5
+ */
6
+ export async function fetchGitHubRelease(repo) {
7
+ // Note: /releases/latest only returns stable releases (no pre-releases or drafts)
8
+ const url = `https://api.github.com/repos/${repo}/releases/latest`;
9
+ const response = await fetch(url, {
10
+ headers: {
11
+ 'Accept': 'application/vnd.github.v3+json',
12
+ 'User-Agent': 'ReleaseRadar/1.0'
13
+ }
14
+ });
15
+ if (!response.ok) {
16
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
17
+ }
18
+ const data = await response.json();
19
+ const version = data.tag_name.replace(/^v/, '');
20
+ return version;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ // src/fetchers/github-release.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { fetchGitHubRelease } from './github-release.js';
4
+ // Note: Uses /releases/latest which only returns stable releases (excludes pre-releases and drafts)
5
+ describe('fetchGitHubRelease', () => {
6
+ beforeEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+ it('fetches latest stable release (not pre-release)', async () => {
10
+ global.fetch = vi.fn().mockResolvedValue({
11
+ ok: true,
12
+ json: () => Promise.resolve({ tag_name: 'v1.12.0' })
13
+ });
14
+ const version = await fetchGitHubRelease('ninja-build/ninja');
15
+ expect(version).toBe('1.12.0');
16
+ expect(fetch).toHaveBeenCalledWith('https://api.github.com/repos/ninja-build/ninja/releases/latest', expect.any(Object));
17
+ });
18
+ it('handles tag without v prefix', async () => {
19
+ global.fetch = vi.fn().mockResolvedValue({
20
+ ok: true,
21
+ json: () => Promise.resolve({ tag_name: '2.44.0' })
22
+ });
23
+ const version = await fetchGitHubRelease('git-for-windows/git');
24
+ expect(version).toBe('2.44.0');
25
+ });
26
+ it('throws on API error', async () => {
27
+ global.fetch = vi.fn().mockResolvedValue({
28
+ ok: false,
29
+ status: 404,
30
+ statusText: 'Not Found'
31
+ });
32
+ await expect(fetchGitHubRelease('invalid/repo'))
33
+ .rejects.toThrow('GitHub API error: 404 Not Found');
34
+ });
35
+ });
@@ -0,0 +1,2 @@
1
+ import type { ToolConfig } from '../types.js';
2
+ export declare function fetchVersion(tool: ToolConfig): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { fetchGitHubRelease } from './github-release.js';
2
+ import { fetchNpmVersion } from './npm.js';
3
+ import { fetchVSCodeMarketplace } from './vscode-marketplace.js';
4
+ import { fetchVSCodeVersion, fetchClaudeCodeCLI, fetchCMakeVersion } from './custom.js';
5
+ export async function fetchVersion(tool) {
6
+ switch (tool.type) {
7
+ case 'github':
8
+ if (!tool.repo)
9
+ throw new Error(`Missing repo for ${tool.name}`);
10
+ return fetchGitHubRelease(tool.repo);
11
+ case 'npm':
12
+ if (!tool.package)
13
+ throw new Error(`Missing package for ${tool.name}`);
14
+ return fetchNpmVersion(tool.package);
15
+ case 'vscode-marketplace':
16
+ if (!tool.extensionId)
17
+ throw new Error(`Missing extensionId for ${tool.name}`);
18
+ return fetchVSCodeMarketplace(tool.extensionId);
19
+ case 'custom':
20
+ return fetchCustom(tool);
21
+ default:
22
+ throw new Error(`Unknown tool type: ${tool.type}`);
23
+ }
24
+ }
25
+ async function fetchCustom(tool) {
26
+ switch (tool.customFetcher) {
27
+ case 'vscode':
28
+ return fetchVSCodeVersion();
29
+ case 'claude-cli':
30
+ return fetchClaudeCodeCLI();
31
+ case 'cmake':
32
+ return fetchCMakeVersion();
33
+ default:
34
+ throw new Error(`Unknown custom fetcher: ${tool.customFetcher}`);
35
+ }
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ // src/fetchers/index.test.ts
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { fetchVersion } from './index.js';
4
+ vi.mock('./github-release.js', () => ({
5
+ fetchGitHubRelease: vi.fn().mockResolvedValue('1.12.0')
6
+ }));
7
+ vi.mock('./npm.js', () => ({
8
+ fetchNpmVersion: vi.fn().mockResolvedValue('2.1.0')
9
+ }));
10
+ vi.mock('./vscode-marketplace.js', () => ({
11
+ fetchVSCodeMarketplace: vi.fn().mockResolvedValue('1.2.3')
12
+ }));
13
+ vi.mock('./custom.js', () => ({
14
+ fetchVSCodeVersion: vi.fn().mockResolvedValue('1.96.0'),
15
+ fetchClaudeCodeCLI: vi.fn().mockResolvedValue('1.0.5'),
16
+ fetchCMakeVersion: vi.fn().mockResolvedValue('3.28.0')
17
+ }));
18
+ describe('fetchVersion', () => {
19
+ it('routes github type to GitHub fetcher', async () => {
20
+ const tool = { name: 'Ninja', type: 'github', repo: 'ninja-build/ninja' };
21
+ const version = await fetchVersion(tool);
22
+ expect(version).toBe('1.12.0');
23
+ });
24
+ it('routes npm type to npm fetcher', async () => {
25
+ const tool = { name: 'Ralphy', type: 'npm', package: 'ralphy-cli' };
26
+ const version = await fetchVersion(tool);
27
+ expect(version).toBe('2.1.0');
28
+ });
29
+ it('routes vscode-marketplace type', async () => {
30
+ const tool = { name: 'Claude Code', type: 'vscode-marketplace', extensionId: 'anthropic.claude-code' };
31
+ const version = await fetchVersion(tool);
32
+ expect(version).toBe('1.2.3');
33
+ });
34
+ it('routes custom type with customFetcher', async () => {
35
+ const tool = { name: 'VSCode', type: 'custom', customFetcher: 'vscode' };
36
+ const version = await fetchVersion(tool);
37
+ expect(version).toBe('1.96.0');
38
+ });
39
+ it('throws for unknown type', async () => {
40
+ const tool = { name: 'Unknown', type: 'invalid' };
41
+ await expect(fetchVersion(tool)).rejects.toThrow('Unknown tool type: invalid');
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export declare function fetchNpmVersion(packageName: string): Promise<string>;
@@ -0,0 +1,10 @@
1
+ // src/fetchers/npm.ts
2
+ export async function fetchNpmVersion(packageName) {
3
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
4
+ const response = await fetch(url);
5
+ if (!response.ok) {
6
+ throw new Error(`npm registry error: ${response.status} ${response.statusText}`);
7
+ }
8
+ const data = await response.json();
9
+ return data.version;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ // src/fetchers/npm.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { fetchNpmVersion } from './npm.js';
4
+ describe('fetchNpmVersion', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('extracts version from npm registry', async () => {
9
+ global.fetch = vi.fn().mockResolvedValue({
10
+ ok: true,
11
+ json: () => Promise.resolve({ version: '2.1.0' })
12
+ });
13
+ const version = await fetchNpmVersion('ralphy-cli');
14
+ expect(version).toBe('2.1.0');
15
+ expect(fetch).toHaveBeenCalledWith('https://registry.npmjs.org/ralphy-cli/latest');
16
+ });
17
+ it('throws on registry error', async () => {
18
+ global.fetch = vi.fn().mockResolvedValue({
19
+ ok: false,
20
+ status: 404,
21
+ statusText: 'Not Found'
22
+ });
23
+ await expect(fetchNpmVersion('nonexistent-package'))
24
+ .rejects.toThrow('npm registry error: 404 Not Found');
25
+ });
26
+ });
@@ -0,0 +1 @@
1
+ export declare function fetchVSCodeMarketplace(extensionId: string): Promise<string>;
@@ -0,0 +1,25 @@
1
+ export async function fetchVSCodeMarketplace(extensionId) {
2
+ const url = 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery';
3
+ const response = await fetch(url, {
4
+ method: 'POST',
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ 'Accept': 'application/json;api-version=7.1-preview.1'
8
+ },
9
+ body: JSON.stringify({
10
+ filters: [{
11
+ criteria: [{ filterType: 7, value: extensionId }]
12
+ }],
13
+ flags: 0x200 // Include versions
14
+ })
15
+ });
16
+ if (!response.ok) {
17
+ throw new Error(`VS Code Marketplace error: ${response.status} ${response.statusText}`);
18
+ }
19
+ const data = await response.json();
20
+ const extension = data.results[0]?.extensions[0];
21
+ if (!extension) {
22
+ throw new Error(`Extension not found: ${extensionId}`);
23
+ }
24
+ return extension.versions[0].version;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ // src/fetchers/vscode-marketplace.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { fetchVSCodeMarketplace } from './vscode-marketplace.js';
4
+ describe('fetchVSCodeMarketplace', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('extracts version from marketplace API', async () => {
9
+ global.fetch = vi.fn().mockResolvedValue({
10
+ ok: true,
11
+ json: () => Promise.resolve({
12
+ results: [{
13
+ extensions: [{
14
+ versions: [{ version: '1.2.3' }]
15
+ }]
16
+ }]
17
+ })
18
+ });
19
+ const version = await fetchVSCodeMarketplace('anthropic.claude-code');
20
+ expect(version).toBe('1.2.3');
21
+ });
22
+ it('throws on API error', async () => {
23
+ global.fetch = vi.fn().mockResolvedValue({
24
+ ok: false,
25
+ status: 500,
26
+ statusText: 'Internal Server Error'
27
+ });
28
+ await expect(fetchVSCodeMarketplace('some.extension'))
29
+ .rejects.toThrow('VS Code Marketplace error: 500 Internal Server Error');
30
+ });
31
+ it('throws when extension not found', async () => {
32
+ global.fetch = vi.fn().mockResolvedValue({
33
+ ok: true,
34
+ json: () => Promise.resolve({ results: [{ extensions: [] }] })
35
+ });
36
+ await expect(fetchVSCodeMarketplace('nonexistent.extension'))
37
+ .rejects.toThrow('Extension not found: nonexistent.extension');
38
+ });
39
+ });
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ // src/index.ts
2
+ import { config } from 'dotenv';
3
+ config();
4
+ import TelegramBot from 'node-telegram-bot-api';
5
+ import cron from 'node-cron';
6
+ import { readFileSync, writeFileSync } from 'fs';
7
+ import { Storage } from './storage.js';
8
+ import { Notifier } from './notifier.js';
9
+ import { Checker } from './checker.js';
10
+ const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
11
+ const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
12
+ const CONFIG_PATH = './config/tools.json';
13
+ if (!BOT_TOKEN || !CHAT_ID) {
14
+ console.error('Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID environment variables');
15
+ process.exit(1);
16
+ }
17
+ // Load config
18
+ let configData = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
19
+ // Initialize components
20
+ const bot = new TelegramBot(BOT_TOKEN, { polling: true });
21
+ const storage = new Storage('./data/versions.json');
22
+ const notifier = new Notifier(bot, CHAT_ID);
23
+ const checker = new Checker(configData.tools, storage, notifier);
24
+ // Track scheduled task for rescheduling
25
+ let scheduledTask = null;
26
+ function scheduleChecks(intervalHours) {
27
+ if (scheduledTask) {
28
+ scheduledTask.stop();
29
+ }
30
+ const cronExpression = `0 */${intervalHours} * * *`;
31
+ scheduledTask = cron.schedule(cronExpression, async () => {
32
+ console.log(`[${new Date().toISOString()}] Running scheduled check`);
33
+ await checker.checkAll();
34
+ });
35
+ console.log(`Scheduled checks every ${intervalHours} hours`);
36
+ }
37
+ // Bot commands
38
+ bot.onText(/\/check/, async (msg) => {
39
+ if (msg.chat.id.toString() !== CHAT_ID)
40
+ return;
41
+ await bot.sendMessage(CHAT_ID, 'Checking for updates...');
42
+ await checker.checkAll();
43
+ await bot.sendMessage(CHAT_ID, 'Check complete.');
44
+ });
45
+ bot.onText(/\/status/, async (msg) => {
46
+ if (msg.chat.id.toString() !== CHAT_ID)
47
+ return;
48
+ const state = storage.load();
49
+ const lines = Object.entries(state.versions)
50
+ .map(([name, version]) => `${name}: ${version}`)
51
+ .sort();
52
+ const message = lines.length > 0
53
+ ? lines.join('\n')
54
+ : 'No versions tracked yet. Run /check first.';
55
+ await bot.sendMessage(CHAT_ID, message);
56
+ });
57
+ bot.onText(/\/interval$/, async (msg) => {
58
+ if (msg.chat.id.toString() !== CHAT_ID)
59
+ return;
60
+ await bot.sendMessage(CHAT_ID, `Check interval: every ${configData.checkIntervalHours} hours`);
61
+ });
62
+ bot.onText(/\/setinterval(?:\s+(\d+))?/, async (msg, match) => {
63
+ if (msg.chat.id.toString() !== CHAT_ID)
64
+ return;
65
+ const hoursStr = match?.[1];
66
+ if (!hoursStr) {
67
+ await bot.sendMessage(CHAT_ID, 'Usage: /setinterval <hours>\nExample: /setinterval 12');
68
+ return;
69
+ }
70
+ const hours = parseInt(hoursStr, 10);
71
+ if (hours < 1 || hours > 24) {
72
+ await bot.sendMessage(CHAT_ID, 'Interval must be between 1 and 24 hours');
73
+ return;
74
+ }
75
+ // Update config
76
+ configData.checkIntervalHours = hours;
77
+ writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
78
+ // Reschedule
79
+ scheduleChecks(hours);
80
+ await bot.sendMessage(CHAT_ID, `Check interval updated to every ${hours} hours`);
81
+ });
82
+ // Start scheduled checks
83
+ scheduleChecks(configData.checkIntervalHours);
84
+ console.log(`ReleaseRadar started. Checking every ${configData.checkIntervalHours} hours.`);
85
+ console.log(`Tracking ${configData.tools.length} tools.`);
@@ -0,0 +1,19 @@
1
+ import TelegramBot from 'node-telegram-bot-api';
2
+ export interface UpdateInfo {
3
+ name: string;
4
+ oldVersion: string;
5
+ newVersion: string;
6
+ }
7
+ export interface FailureInfo {
8
+ name: string;
9
+ error: string;
10
+ }
11
+ export declare class Notifier {
12
+ private bot;
13
+ private chatId;
14
+ constructor(bot: TelegramBot, chatId: string);
15
+ sendUpdate(name: string, oldVersion: string, newVersion: string): Promise<void>;
16
+ sendBatchedUpdates(updates: UpdateInfo[]): Promise<void>;
17
+ sendFailure(name: string, error: string): Promise<void>;
18
+ sendBatchedFailures(failures: FailureInfo[]): Promise<void>;
19
+ }
@@ -0,0 +1,30 @@
1
+ export class Notifier {
2
+ bot;
3
+ chatId;
4
+ constructor(bot, chatId) {
5
+ this.bot = bot;
6
+ this.chatId = chatId;
7
+ }
8
+ async sendUpdate(name, oldVersion, newVersion) {
9
+ await this.bot.sendMessage(this.chatId, `🔄 ${name}: ${oldVersion} → ${newVersion}`);
10
+ }
11
+ async sendBatchedUpdates(updates) {
12
+ if (updates.length === 0)
13
+ return;
14
+ const message = updates
15
+ .map(u => `🔄 ${u.name}: ${u.oldVersion} → ${u.newVersion}`)
16
+ .join('\n');
17
+ await this.bot.sendMessage(this.chatId, message);
18
+ }
19
+ async sendFailure(name, error) {
20
+ await this.bot.sendMessage(this.chatId, `⚠️ Failed to check ${name}: ${error}`);
21
+ }
22
+ async sendBatchedFailures(failures) {
23
+ if (failures.length === 0)
24
+ return;
25
+ const message = failures
26
+ .map(f => `⚠️ Failed to check ${f.name}: ${f.error}`)
27
+ .join('\n');
28
+ await this.bot.sendMessage(this.chatId, message);
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ // src/notifier.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { Notifier } from './notifier.js';
4
+ describe('Notifier', () => {
5
+ let mockBot;
6
+ let notifier;
7
+ beforeEach(() => {
8
+ mockBot = { sendMessage: vi.fn().mockResolvedValue({}) };
9
+ notifier = new Notifier(mockBot, '123456789');
10
+ });
11
+ it('sends update notification with correct format', async () => {
12
+ await notifier.sendUpdate('Ninja', '1.11.1', '1.12.0');
13
+ expect(mockBot.sendMessage).toHaveBeenCalledWith('123456789', '🔄 Ninja: 1.11.1 → 1.12.0');
14
+ });
15
+ it('sends batched updates as single message', async () => {
16
+ const updates = [
17
+ { name: 'Ninja', oldVersion: '1.11.1', newVersion: '1.12.0' },
18
+ { name: 'Git', oldVersion: '2.43.0', newVersion: '2.44.0' }
19
+ ];
20
+ await notifier.sendBatchedUpdates(updates);
21
+ expect(mockBot.sendMessage).toHaveBeenCalledTimes(1);
22
+ expect(mockBot.sendMessage).toHaveBeenCalledWith('123456789', '🔄 Ninja: 1.11.1 → 1.12.0\n🔄 Git: 2.43.0 → 2.44.0');
23
+ });
24
+ it('sends failure notification', async () => {
25
+ await notifier.sendFailure('CMake', 'Request timeout');
26
+ expect(mockBot.sendMessage).toHaveBeenCalledWith('123456789', '⚠️ Failed to check CMake: Request timeout');
27
+ });
28
+ it('sends batched failures as single message', async () => {
29
+ const failures = [
30
+ { name: 'CMake', error: 'Timeout' },
31
+ { name: 'VSCode', error: 'Connection refused' }
32
+ ];
33
+ await notifier.sendBatchedFailures(failures);
34
+ expect(mockBot.sendMessage).toHaveBeenCalledTimes(1);
35
+ expect(mockBot.sendMessage).toHaveBeenCalledWith('123456789', '⚠️ Failed to check CMake: Timeout\n⚠️ Failed to check VSCode: Connection refused');
36
+ });
37
+ });
@@ -0,0 +1,14 @@
1
+ export interface StorageState {
2
+ lastCheck: string | null;
3
+ versions: Record<string, string>;
4
+ }
5
+ export declare class Storage {
6
+ private filePath;
7
+ private state;
8
+ constructor(filePath: string);
9
+ private ensureLoaded;
10
+ load(): StorageState;
11
+ save(state: StorageState): void;
12
+ getVersion(toolName: string): string | null;
13
+ setVersion(toolName: string, version: string): void;
14
+ }
@@ -0,0 +1,37 @@
1
+ // src/storage.ts
2
+ import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
3
+ export class Storage {
4
+ filePath;
5
+ state = null;
6
+ constructor(filePath) {
7
+ this.filePath = filePath;
8
+ }
9
+ ensureLoaded() {
10
+ if (!this.state) {
11
+ this.state = this.load();
12
+ }
13
+ return this.state;
14
+ }
15
+ load() {
16
+ if (!existsSync(this.filePath)) {
17
+ return { lastCheck: null, versions: {} };
18
+ }
19
+ const content = readFileSync(this.filePath, 'utf-8');
20
+ return JSON.parse(content);
21
+ }
22
+ save(state) {
23
+ const tempPath = `${this.filePath}.tmp`;
24
+ writeFileSync(tempPath, JSON.stringify(state, null, 2));
25
+ renameSync(tempPath, this.filePath);
26
+ }
27
+ getVersion(toolName) {
28
+ const state = this.ensureLoaded();
29
+ return state.versions[toolName] ?? null;
30
+ }
31
+ setVersion(toolName, version) {
32
+ const state = this.ensureLoaded();
33
+ state.versions[toolName] = version;
34
+ state.lastCheck = new Date().toISOString();
35
+ this.save(state);
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ // src/storage.test.ts
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import { Storage } from './storage.js';
4
+ import { writeFileSync, mkdirSync, rmSync } from 'fs';
5
+ describe('Storage', () => {
6
+ const testDir = './test-data';
7
+ const testPath = `${testDir}/versions.json`;
8
+ let storage;
9
+ beforeEach(() => {
10
+ mkdirSync(testDir, { recursive: true });
11
+ storage = new Storage(testPath);
12
+ });
13
+ afterEach(() => {
14
+ rmSync(testDir, { recursive: true, force: true });
15
+ });
16
+ it('returns empty state when file does not exist', () => {
17
+ const state = storage.load();
18
+ expect(state).toEqual({ lastCheck: null, versions: {} });
19
+ });
20
+ it('loads existing state from file', () => {
21
+ const existingState = {
22
+ lastCheck: '2026-01-23T10:00:00Z',
23
+ versions: { 'VSCode': '1.96.0' }
24
+ };
25
+ writeFileSync(testPath, JSON.stringify(existingState));
26
+ const state = storage.load();
27
+ expect(state).toEqual(existingState);
28
+ });
29
+ it('saves state to file', () => {
30
+ const state = {
31
+ lastCheck: '2026-01-23T10:00:00Z',
32
+ versions: { 'Ninja': '1.12.0' }
33
+ };
34
+ storage.save(state);
35
+ const loaded = storage.load();
36
+ expect(loaded).toEqual(state);
37
+ });
38
+ it('getVersion returns null for unknown tool', () => {
39
+ expect(storage.getVersion('Unknown')).toBeNull();
40
+ });
41
+ it('getVersion returns stored version', () => {
42
+ const state = { lastCheck: null, versions: { 'Git': '2.44.0' } };
43
+ writeFileSync(testPath, JSON.stringify(state));
44
+ storage = new Storage(testPath); // reload
45
+ expect(storage.getVersion('Git')).toBe('2.44.0');
46
+ });
47
+ it('setVersion updates and persists', () => {
48
+ storage.setVersion('Ninja', '1.12.0');
49
+ // Reload and verify
50
+ const newStorage = new Storage(testPath);
51
+ expect(newStorage.getVersion('Ninja')).toBe('1.12.0');
52
+ });
53
+ });
@@ -0,0 +1,14 @@
1
+ export interface ToolConfig {
2
+ name: string;
3
+ type: 'github' | 'npm' | 'vscode-marketplace' | 'custom';
4
+ repo?: string;
5
+ package?: string;
6
+ extensionId?: string;
7
+ url?: string;
8
+ fallbackUrl?: string;
9
+ customFetcher?: string;
10
+ }
11
+ export interface Config {
12
+ checkIntervalHours: number;
13
+ tools: ToolConfig[];
14
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@lvnt/release-radar",
3
+ "version": "1.0.0",
4
+ "description": "Monitor tool versions and notify via Telegram when updates are detected",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "release-radar": "./bin/release-radar.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx src/index.ts",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "release",
20
+ "monitor",
21
+ "version",
22
+ "telegram",
23
+ "notification",
24
+ "github",
25
+ "npm",
26
+ "vscode"
27
+ ],
28
+ "author": "lvnt",
29
+ "license": "ISC",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/lvntbkdmr/release-radar.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/lvntbkdmr/release-radar/issues"
36
+ },
37
+ "homepage": "https://github.com/lvntbkdmr/release-radar#readme",
38
+ "files": [
39
+ "dist",
40
+ "bin",
41
+ "config",
42
+ ".env.example"
43
+ ],
44
+ "dependencies": {
45
+ "dotenv": "^17.2.3",
46
+ "node-cron": "^4.2.1",
47
+ "node-telegram-bot-api": "^0.67.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^25.0.10",
51
+ "@types/node-cron": "^3.0.11",
52
+ "@types/node-telegram-bot-api": "^0.64.13",
53
+ "tsx": "^4.21.0",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.0.18"
56
+ }
57
+ }