@lvnt/release-radar 1.2.0 → 1.4.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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "Ninja": {
3
+ "displayName": "Ninja",
4
+ "downloadUrl": "github.com/ninja-build/ninja/releases/download/v{{VERSION}}/ninja-linux.zip",
5
+ "filename": "ninja-{{VERSION}}-linux.zip"
6
+ },
7
+ "CMake": {
8
+ "displayName": "CMake",
9
+ "downloadUrl": "github.com/Kitware/CMake/releases/download/v{{VERSION}}/cmake-{{VERSION}}-linux-x86_64.tar.gz",
10
+ "filename": "cmake-{{VERSION}}-linux-x86_64.tar.gz"
11
+ },
12
+ "Git": {
13
+ "displayName": "Git for Windows",
14
+ "downloadUrl": "github.com/git-for-windows/git/releases/download/v{{VERSION}}.windows.1/Git-{{VERSION}}-64-bit.exe",
15
+ "filename": "Git-{{VERSION}}-64-bit.exe"
16
+ }
17
+ }
package/dist/checker.d.ts CHANGED
@@ -6,5 +6,8 @@ export declare class Checker {
6
6
  private storage;
7
7
  private notifier;
8
8
  constructor(tools: ToolConfig[], storage: Storage, notifier: Notifier);
9
- checkAll(): Promise<void>;
9
+ checkAll(): Promise<{
10
+ hasUpdates: boolean;
11
+ updateCount: number;
12
+ }>;
10
13
  }
package/dist/checker.js CHANGED
@@ -31,5 +31,6 @@ export class Checker {
31
31
  }
32
32
  await this.notifier.sendBatchedUpdates(updates);
33
33
  await this.notifier.sendBatchedFailures(failures);
34
+ return { hasUpdates: updates.length > 0, updateCount: updates.length };
34
35
  }
35
36
  }
@@ -61,4 +61,20 @@ describe('Checker', () => {
61
61
  expect(mockNotifier.sendBatchedUpdates).toHaveBeenCalledWith([]);
62
62
  expect(mockStorage.setVersion).not.toHaveBeenCalled();
63
63
  });
64
+ it('returns hasUpdates true when updates found', async () => {
65
+ mockStorage.getVersion.mockReturnValueOnce('1.11.1').mockReturnValueOnce('2.43.0');
66
+ vi.mocked(fetchVersion)
67
+ .mockResolvedValueOnce('1.12.0')
68
+ .mockResolvedValueOnce('2.43.0');
69
+ const result = await checker.checkAll();
70
+ expect(result.hasUpdates).toBe(true);
71
+ expect(result.updateCount).toBe(1);
72
+ });
73
+ it('returns hasUpdates false when no updates', async () => {
74
+ mockStorage.getVersion.mockReturnValue('1.12.0');
75
+ vi.mocked(fetchVersion).mockResolvedValue('1.12.0');
76
+ const result = await checker.checkAll();
77
+ expect(result.hasUpdates).toBe(false);
78
+ expect(result.updateCount).toBe(0);
79
+ });
64
80
  });
@@ -0,0 +1,13 @@
1
+ import type { DownloadsConfig } from './types.js';
2
+ export interface PublishResult {
3
+ success: boolean;
4
+ version?: string;
5
+ error?: string;
6
+ }
7
+ export declare class CliPublisher {
8
+ private downloadsConfig;
9
+ private cliPath;
10
+ constructor(downloadsConfig: DownloadsConfig, cliPath?: string);
11
+ isConfigured(): boolean;
12
+ publish(versions: Record<string, string>): Promise<PublishResult>;
13
+ }
@@ -0,0 +1,50 @@
1
+ // src/cli-publisher.ts
2
+ import { execSync } from 'child_process';
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { generateVersionsJson } from './versions-generator.js';
5
+ export class CliPublisher {
6
+ downloadsConfig;
7
+ cliPath;
8
+ constructor(downloadsConfig, cliPath = './cli') {
9
+ this.downloadsConfig = downloadsConfig;
10
+ this.cliPath = cliPath;
11
+ }
12
+ isConfigured() {
13
+ return Object.keys(this.downloadsConfig).length > 0 && existsSync(this.cliPath);
14
+ }
15
+ async publish(versions) {
16
+ if (!this.isConfigured()) {
17
+ return { success: false, error: 'CLI publisher not configured' };
18
+ }
19
+ try {
20
+ // Generate versions.json
21
+ const versionsJson = generateVersionsJson(versions, this.downloadsConfig);
22
+ // Write to data/ for reference
23
+ writeFileSync('./data/cli-versions.json', JSON.stringify(versionsJson, null, 2));
24
+ // Copy to CLI package
25
+ const cliVersionsPath = `${this.cliPath}/versions.json`;
26
+ writeFileSync(cliVersionsPath, JSON.stringify(versionsJson, null, 2));
27
+ // Read current CLI version
28
+ const pkgPath = `${this.cliPath}/package.json`;
29
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
30
+ const currentVersion = pkg.version;
31
+ // Bump patch version
32
+ const versionParts = currentVersion.split('.').map(Number);
33
+ versionParts[2]++;
34
+ const newVersion = versionParts.join('.');
35
+ pkg.version = newVersion;
36
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
37
+ // Build CLI
38
+ execSync('npm run build', { cwd: this.cliPath, stdio: 'pipe' });
39
+ // Publish to npm
40
+ execSync('npm publish --access public', { cwd: this.cliPath, stdio: 'pipe' });
41
+ console.log(`[CliPublisher] Published @lvnt/release-radar-cli v${newVersion}`);
42
+ return { success: true, version: newVersion };
43
+ }
44
+ catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ console.error(`[CliPublisher] Failed to publish: ${message}`);
47
+ return { success: false, error: message };
48
+ }
49
+ }
50
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import { readFileSync, writeFileSync } from 'fs';
7
7
  import { Storage } from './storage.js';
8
8
  import { Notifier } from './notifier.js';
9
9
  import { Checker } from './checker.js';
10
+ import { generateVersionsJson } from './versions-generator.js';
11
+ import { CliPublisher } from './cli-publisher.js';
10
12
  const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
11
13
  const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
12
14
  const CONFIG_PATH = './config/tools.json';
@@ -14,13 +16,24 @@ if (!BOT_TOKEN || !CHAT_ID) {
14
16
  console.error('Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID environment variables');
15
17
  process.exit(1);
16
18
  }
19
+ // Type-safe constants after validation
20
+ const validatedChatId = CHAT_ID;
17
21
  // Load config
18
22
  let configData = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
23
+ const DOWNLOADS_PATH = './config/downloads.json';
24
+ let downloadsConfig = {};
25
+ try {
26
+ downloadsConfig = JSON.parse(readFileSync(DOWNLOADS_PATH, 'utf-8'));
27
+ }
28
+ catch {
29
+ console.log('No downloads.json found, CLI generation disabled');
30
+ }
19
31
  // Initialize components
20
32
  const bot = new TelegramBot(BOT_TOKEN, { polling: true });
21
33
  const storage = new Storage('./data/versions.json');
22
- const notifier = new Notifier(bot, CHAT_ID);
34
+ const notifier = new Notifier(bot, validatedChatId);
23
35
  const checker = new Checker(configData.tools, storage, notifier);
36
+ const cliPublisher = new CliPublisher(downloadsConfig);
24
37
  // Track scheduled task for rescheduling
25
38
  let scheduledTask = null;
26
39
  let lastCheckTime = null;
@@ -44,23 +57,42 @@ function scheduleChecks(intervalHours) {
44
57
  scheduledTask = cron.schedule(cronExpression, async () => {
45
58
  console.log(`[${new Date().toISOString()}] Running scheduled check`);
46
59
  lastCheckTime = new Date();
47
- await checker.checkAll();
60
+ const result = await checker.checkAll();
48
61
  nextCheckTime = calculateNextCheckTime(intervalHours);
62
+ // Auto-publish CLI if updates were detected
63
+ if (result.hasUpdates && cliPublisher.isConfigured()) {
64
+ const state = storage.load();
65
+ const publishResult = await cliPublisher.publish(state.versions);
66
+ if (publishResult.success) {
67
+ await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
68
+ }
69
+ }
49
70
  });
50
71
  console.log(`Scheduled checks every ${intervalHours} hours`);
51
72
  }
52
73
  // Bot commands
53
74
  bot.onText(/\/check/, async (msg) => {
54
- if (msg.chat.id.toString() !== CHAT_ID)
75
+ if (msg.chat.id.toString() !== validatedChatId)
55
76
  return;
56
- await bot.sendMessage(CHAT_ID, 'Checking for updates...');
77
+ await bot.sendMessage(validatedChatId, 'Checking for updates...');
57
78
  lastCheckTime = new Date();
58
- await checker.checkAll();
79
+ const result = await checker.checkAll();
59
80
  nextCheckTime = calculateNextCheckTime(configData.checkIntervalHours);
60
- await bot.sendMessage(CHAT_ID, 'Check complete.');
81
+ // Auto-publish CLI if updates were detected
82
+ if (result.hasUpdates && cliPublisher.isConfigured()) {
83
+ const state = storage.load();
84
+ const publishResult = await cliPublisher.publish(state.versions);
85
+ if (publishResult.success) {
86
+ await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
87
+ }
88
+ else {
89
+ await bot.sendMessage(validatedChatId, `⚠️ CLI publish failed: ${publishResult.error}`);
90
+ }
91
+ }
92
+ await bot.sendMessage(validatedChatId, 'Check complete.');
61
93
  });
62
94
  bot.onText(/\/status/, async (msg) => {
63
- if (msg.chat.id.toString() !== CHAT_ID)
95
+ if (msg.chat.id.toString() !== validatedChatId)
64
96
  return;
65
97
  const state = storage.load();
66
98
  const lines = Object.entries(state.versions)
@@ -89,24 +121,24 @@ bot.onText(/\/status/, async (msg) => {
89
121
  message += '\nNext check: soon';
90
122
  }
91
123
  }
92
- await bot.sendMessage(CHAT_ID, message);
124
+ await bot.sendMessage(validatedChatId, message);
93
125
  });
94
126
  bot.onText(/\/interval$/, async (msg) => {
95
- if (msg.chat.id.toString() !== CHAT_ID)
127
+ if (msg.chat.id.toString() !== validatedChatId)
96
128
  return;
97
- await bot.sendMessage(CHAT_ID, `Check interval: every ${configData.checkIntervalHours} hours`);
129
+ await bot.sendMessage(validatedChatId, `Check interval: every ${configData.checkIntervalHours} hours`);
98
130
  });
99
131
  bot.onText(/\/setinterval(?:\s+(\d+))?/, async (msg, match) => {
100
- if (msg.chat.id.toString() !== CHAT_ID)
132
+ if (msg.chat.id.toString() !== validatedChatId)
101
133
  return;
102
134
  const hoursStr = match?.[1];
103
135
  if (!hoursStr) {
104
- await bot.sendMessage(CHAT_ID, 'Usage: /setinterval <hours>\nExample: /setinterval 12');
136
+ await bot.sendMessage(validatedChatId, 'Usage: /setinterval <hours>\nExample: /setinterval 12');
105
137
  return;
106
138
  }
107
139
  const hours = parseInt(hoursStr, 10);
108
140
  if (hours < 1 || hours > 24) {
109
- await bot.sendMessage(CHAT_ID, 'Interval must be between 1 and 24 hours');
141
+ await bot.sendMessage(validatedChatId, 'Interval must be between 1 and 24 hours');
110
142
  return;
111
143
  }
112
144
  // Update config
@@ -114,7 +146,37 @@ bot.onText(/\/setinterval(?:\s+(\d+))?/, async (msg, match) => {
114
146
  writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
115
147
  // Reschedule
116
148
  scheduleChecks(hours);
117
- await bot.sendMessage(CHAT_ID, `Check interval updated to every ${hours} hours`);
149
+ await bot.sendMessage(validatedChatId, `Check interval updated to every ${hours} hours`);
150
+ });
151
+ bot.onText(/\/generate/, async (msg) => {
152
+ if (msg.chat.id.toString() !== validatedChatId)
153
+ return;
154
+ if (Object.keys(downloadsConfig).length === 0) {
155
+ await bot.sendMessage(validatedChatId, 'No downloads.json configured.');
156
+ return;
157
+ }
158
+ const state = storage.load();
159
+ const versionsJson = generateVersionsJson(state.versions, downloadsConfig);
160
+ const outputPath = './data/cli-versions.json';
161
+ writeFileSync(outputPath, JSON.stringify(versionsJson, null, 2));
162
+ await bot.sendMessage(CHAT_ID, `Generated versions.json with ${versionsJson.tools.length} tools.\nPath: ${outputPath}`);
163
+ });
164
+ bot.onText(/\/publishcli/, async (msg) => {
165
+ if (msg.chat.id.toString() !== validatedChatId)
166
+ return;
167
+ if (!cliPublisher.isConfigured()) {
168
+ await bot.sendMessage(validatedChatId, 'CLI publisher not configured. Check downloads.json and cli/ directory.');
169
+ return;
170
+ }
171
+ await bot.sendMessage(validatedChatId, 'Publishing CLI...');
172
+ const state = storage.load();
173
+ const result = await cliPublisher.publish(state.versions);
174
+ if (result.success) {
175
+ await bot.sendMessage(validatedChatId, `✅ CLI published: v${result.version}`);
176
+ }
177
+ else {
178
+ await bot.sendMessage(validatedChatId, `❌ CLI publish failed: ${result.error}`);
179
+ }
118
180
  });
119
181
  // Start scheduled checks
120
182
  scheduleChecks(configData.checkIntervalHours);
package/dist/types.d.ts CHANGED
@@ -12,3 +12,23 @@ export interface Config {
12
12
  checkIntervalHours: number;
13
13
  tools: ToolConfig[];
14
14
  }
15
+ export interface DownloadConfig {
16
+ displayName: string;
17
+ downloadUrl: string;
18
+ filename: string;
19
+ }
20
+ export interface DownloadsConfig {
21
+ [toolName: string]: DownloadConfig;
22
+ }
23
+ export interface VersionsJsonTool {
24
+ name: string;
25
+ displayName: string;
26
+ version: string;
27
+ publishedAt: string;
28
+ downloadUrl: string;
29
+ filename: string;
30
+ }
31
+ export interface VersionsJson {
32
+ generatedAt: string;
33
+ tools: VersionsJsonTool[];
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { DownloadsConfig, VersionsJson } from './types.js';
2
+ export declare function generateVersionsJson(versions: Record<string, string>, downloads: DownloadsConfig): VersionsJson;
@@ -0,0 +1,23 @@
1
+ export function generateVersionsJson(versions, downloads) {
2
+ const tools = [];
3
+ for (const [toolName, version] of Object.entries(versions)) {
4
+ const downloadConfig = downloads[toolName];
5
+ if (!downloadConfig)
6
+ continue;
7
+ const downloadUrl = '{{NEXUS_URL}}/' +
8
+ downloadConfig.downloadUrl.replace(/\{\{VERSION\}\}/g, version);
9
+ const filename = downloadConfig.filename.replace(/\{\{VERSION\}\}/g, version);
10
+ tools.push({
11
+ name: toolName,
12
+ displayName: downloadConfig.displayName,
13
+ version,
14
+ publishedAt: new Date().toISOString(),
15
+ downloadUrl,
16
+ filename,
17
+ });
18
+ }
19
+ return {
20
+ generatedAt: new Date().toISOString(),
21
+ tools,
22
+ };
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateVersionsJson } from './versions-generator.js';
3
+ describe('generateVersionsJson', () => {
4
+ it('merges version data with download config', () => {
5
+ const versions = {
6
+ 'Ninja': '1.12.0',
7
+ 'CMake': '3.28.0',
8
+ };
9
+ const downloads = {
10
+ 'Ninja': {
11
+ displayName: 'Ninja Build',
12
+ downloadUrl: 'github.com/ninja-build/ninja/releases/download/v{{VERSION}}/ninja-linux.zip',
13
+ filename: 'ninja-{{VERSION}}-linux.zip',
14
+ },
15
+ 'CMake': {
16
+ displayName: 'CMake',
17
+ downloadUrl: 'github.com/Kitware/CMake/releases/download/v{{VERSION}}/cmake-{{VERSION}}.tar.gz',
18
+ filename: 'cmake-{{VERSION}}.tar.gz',
19
+ },
20
+ };
21
+ const result = generateVersionsJson(versions, downloads);
22
+ expect(result.tools).toHaveLength(2);
23
+ expect(result.generatedAt).toBeDefined();
24
+ const ninja = result.tools.find(t => t.name === 'Ninja');
25
+ expect(ninja).toBeDefined();
26
+ expect(ninja.displayName).toBe('Ninja Build');
27
+ expect(ninja.version).toBe('1.12.0');
28
+ expect(ninja.downloadUrl).toBe('{{NEXUS_URL}}/github.com/ninja-build/ninja/releases/download/v1.12.0/ninja-linux.zip');
29
+ expect(ninja.filename).toBe('ninja-1.12.0-linux.zip');
30
+ });
31
+ it('only includes tools that have download config', () => {
32
+ const versions = {
33
+ 'Ninja': '1.12.0',
34
+ 'UnknownTool': '1.0.0',
35
+ };
36
+ const downloads = {
37
+ 'Ninja': {
38
+ displayName: 'Ninja',
39
+ downloadUrl: 'github.com/ninja/releases/{{VERSION}}/ninja.zip',
40
+ filename: 'ninja-{{VERSION}}.zip',
41
+ },
42
+ };
43
+ const result = generateVersionsJson(versions, downloads);
44
+ expect(result.tools).toHaveLength(1);
45
+ expect(result.tools[0].name).toBe('Ninja');
46
+ });
47
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvnt/release-radar",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Monitor tool versions and notify via Telegram when updates are detected",
5
5
  "main": "dist/index.js",
6
6
  "bin": {