@lvnt/release-radar 1.7.16 → 1.9.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.
@@ -12,7 +12,7 @@ vi.mock('fs', () => ({
12
12
  describe('AssetMirror', () => {
13
13
  let mirror;
14
14
  beforeEach(() => {
15
- vi.clearAllMocks();
15
+ vi.resetAllMocks();
16
16
  mirror = new AssetMirror();
17
17
  });
18
18
  describe('buildTag', () => {
@@ -74,11 +74,56 @@ describe('AssetMirror', () => {
74
74
  vi.mocked(execSync).mockReturnValueOnce('');
75
75
  vi.mocked(existsSync).mockReturnValue(true);
76
76
  const result = await mirror.mirror('Claude Code VSCode', '2.1.9', {
77
- sourceUrl: 'marketplace-api'
77
+ sourceUrl: 'marketplace-api',
78
+ extensionId: 'anthropic.claude-code',
79
+ targetPlatform: 'win32-x64'
78
80
  }, 'claude-code-{{VERSION}}-win32-x64.vsix');
79
81
  expect(result.success).toBe(true);
80
82
  expect(result.downloadUrl).toBe('github.com/lvntbkdmr/apps/releases/download/claude-code-vscode-v2.1.9/claude-code-2.1.9-win32-x64.vsix');
81
83
  });
84
+ it('handles universal marketplace extensions without targetPlatform', async () => {
85
+ const marketplaceResponse = JSON.stringify({
86
+ results: [{
87
+ extensions: [{
88
+ versions: [{
89
+ version: '1.2.3',
90
+ files: [{
91
+ assetType: 'Microsoft.VisualStudio.Services.VSIXPackage',
92
+ source: 'https://marketplace.visualstudio.com/vsix/download'
93
+ }]
94
+ }]
95
+ }]
96
+ }]
97
+ });
98
+ // 1. gh release view fails
99
+ vi.mocked(execSync).mockImplementationOnce(() => {
100
+ throw new Error('release not found');
101
+ });
102
+ // 2. curl marketplace query succeeds
103
+ vi.mocked(execSync).mockReturnValueOnce(marketplaceResponse);
104
+ // 3. curl download succeeds
105
+ vi.mocked(execSync).mockReturnValueOnce('');
106
+ // 4. gh release create succeeds
107
+ vi.mocked(execSync).mockReturnValueOnce('');
108
+ vi.mocked(existsSync).mockReturnValue(true);
109
+ const result = await mirror.mirror('GitHub Theme', '1.2.3', {
110
+ sourceUrl: 'marketplace-api',
111
+ extensionId: 'github.github-vscode-theme'
112
+ }, 'github-vscode-theme-{{VERSION}}.vsix');
113
+ expect(result.success).toBe(true);
114
+ expect(result.downloadUrl).toBe('github.com/lvntbkdmr/apps/releases/download/github-theme-v1.2.3/github-vscode-theme-1.2.3.vsix');
115
+ });
116
+ it('fails when extensionId is missing for marketplace-api', async () => {
117
+ // gh release view fails
118
+ vi.mocked(execSync).mockImplementationOnce(() => {
119
+ throw new Error('release not found');
120
+ });
121
+ const result = await mirror.mirror('Test Extension', '1.0.0', {
122
+ sourceUrl: 'marketplace-api'
123
+ }, 'test-{{VERSION}}.vsix');
124
+ expect(result.success).toBe(false);
125
+ expect(result.error).toContain('extensionId is required');
126
+ });
82
127
  it('successfully mirrors direct URL through full flow', async () => {
83
128
  // 1. gh release view fails
84
129
  vi.mocked(execSync).mockImplementationOnce(() => {
package/dist/checker.d.ts CHANGED
@@ -13,5 +13,5 @@ export declare class Checker {
13
13
  hasUpdates: boolean;
14
14
  updateCount: number;
15
15
  }>;
16
- private mirrorIfConfigured;
16
+ private getMirrorItem;
17
17
  }
package/dist/checker.js CHANGED
@@ -15,6 +15,7 @@ export class Checker {
15
15
  async checkAll() {
16
16
  const updates = [];
17
17
  const failures = [];
18
+ const mirrorItems = [];
18
19
  for (const tool of this.tools) {
19
20
  try {
20
21
  const newVersion = await fetchVersion(tool);
@@ -26,8 +27,11 @@ export class Checker {
26
27
  else if (oldVersion !== newVersion) {
27
28
  updates.push({ name: tool.name, oldVersion, newVersion });
28
29
  this.storage.setVersion(tool.name, newVersion);
29
- // Mirror asset if configured
30
- await this.mirrorIfConfigured(tool.name, newVersion);
30
+ // Collect mirror items for batch processing
31
+ const mirrorItem = this.getMirrorItem(tool.name, newVersion);
32
+ if (mirrorItem) {
33
+ mirrorItems.push(mirrorItem);
34
+ }
31
35
  }
32
36
  }
33
37
  catch (error) {
@@ -35,22 +39,34 @@ export class Checker {
35
39
  failures.push({ name: tool.name, error: message });
36
40
  }
37
41
  }
42
+ // Batch mirror all updated assets
43
+ if (mirrorItems.length > 0 && this.assetMirror) {
44
+ const batchResult = await this.assetMirror.mirrorBatch(mirrorItems);
45
+ // Store mirror URLs for successful items
46
+ for (const [toolName, result] of batchResult.results) {
47
+ if (result.success && result.downloadUrl) {
48
+ this.storage.setMirrorUrl(toolName, result.downloadUrl);
49
+ }
50
+ }
51
+ }
38
52
  await this.notifier.sendBatchedUpdates(updates);
39
53
  await this.notifier.sendBatchedFailures(failures);
40
54
  return { hasUpdates: updates.length > 0, updateCount: updates.length };
41
55
  }
42
- async mirrorIfConfigured(toolName, version) {
43
- if (!this.assetMirror || !this.downloadsConfig)
44
- return;
56
+ getMirrorItem(toolName, version) {
57
+ if (!this.downloadsConfig)
58
+ return null;
45
59
  const downloadConfig = this.downloadsConfig[toolName];
46
60
  if (!downloadConfig || downloadConfig.type === 'npm')
47
- return;
61
+ return null;
48
62
  const urlConfig = downloadConfig;
49
63
  if (!urlConfig.mirror)
50
- return;
51
- const result = await this.assetMirror.mirror(toolName, version, urlConfig.mirror, urlConfig.filename);
52
- if (result.success && result.downloadUrl) {
53
- this.storage.setMirrorUrl(toolName, result.downloadUrl);
54
- }
64
+ return null;
65
+ return {
66
+ toolName,
67
+ version,
68
+ config: urlConfig.mirror,
69
+ filenameTemplate: urlConfig.filename,
70
+ };
55
71
  }
56
72
  }
@@ -83,7 +83,10 @@ describe('Checker', () => {
83
83
  let downloadsConfig;
84
84
  beforeEach(() => {
85
85
  mockAssetMirror = {
86
- mirror: vi.fn().mockResolvedValue({ success: true, downloadUrl: 'github.com/test/url' })
86
+ mirrorBatch: vi.fn().mockResolvedValue({
87
+ tag: 'batch-2026-01-29-120000',
88
+ results: new Map([['VSCode', { success: true, downloadUrl: 'github.com/test/url' }]])
89
+ })
87
90
  };
88
91
  downloadsConfig = {
89
92
  'VSCode': {
@@ -106,7 +109,14 @@ describe('Checker', () => {
106
109
  mockStorage.getVersion.mockReturnValue('1.95.0');
107
110
  vi.mocked(fetchVersion).mockResolvedValue('1.96.0');
108
111
  await checkerWithMirror.checkAll();
109
- expect(mockAssetMirror.mirror).toHaveBeenCalledWith('VSCode', '1.96.0', { sourceUrl: 'https://update.code.visualstudio.com/latest/win32-x64/stable' }, 'VSCode-{{VERSION}}-win-x64.msi');
112
+ expect(mockAssetMirror.mirrorBatch).toHaveBeenCalledWith([
113
+ {
114
+ toolName: 'VSCode',
115
+ version: '1.96.0',
116
+ config: { sourceUrl: 'https://update.code.visualstudio.com/latest/win32-x64/stable' },
117
+ filenameTemplate: 'VSCode-{{VERSION}}-win-x64.msi'
118
+ }
119
+ ]);
110
120
  expect(mockStorage.setMirrorUrl).toHaveBeenCalledWith('VSCode', 'github.com/test/url');
111
121
  });
112
122
  it('does not mirror when tool has no mirror config', async () => {
@@ -115,7 +125,8 @@ describe('Checker', () => {
115
125
  mockStorage.getVersion.mockReturnValue('1.11.0');
116
126
  vi.mocked(fetchVersion).mockResolvedValue('1.12.0');
117
127
  await checkerWithMirror.checkAll();
118
- expect(mockAssetMirror.mirror).not.toHaveBeenCalled();
128
+ // mirrorBatch should not be called when no items have mirror config
129
+ expect(mockAssetMirror.mirrorBatch).not.toHaveBeenCalled();
119
130
  });
120
131
  it('does not mirror when version unchanged', async () => {
121
132
  const vscodeTool = { name: 'VSCode', type: 'custom', customFetcher: 'vscode' };
@@ -123,11 +134,14 @@ describe('Checker', () => {
123
134
  mockStorage.getVersion.mockReturnValue('1.96.0');
124
135
  vi.mocked(fetchVersion).mockResolvedValue('1.96.0');
125
136
  await checkerWithMirror.checkAll();
126
- expect(mockAssetMirror.mirror).not.toHaveBeenCalled();
137
+ expect(mockAssetMirror.mirrorBatch).not.toHaveBeenCalled();
127
138
  });
128
139
  it('continues if mirror fails', async () => {
129
140
  const vscodeTool = { name: 'VSCode', type: 'custom', customFetcher: 'vscode' };
130
- mockAssetMirror.mirror.mockResolvedValue({ success: false, error: 'network error' });
141
+ mockAssetMirror.mirrorBatch.mockResolvedValue({
142
+ tag: 'batch-2026-01-29-120000',
143
+ results: new Map([['VSCode', { success: false, error: 'network error' }]])
144
+ });
131
145
  const checkerWithMirror = new Checker([vscodeTool], mockStorage, mockNotifier, mockAssetMirror, downloadsConfig);
132
146
  mockStorage.getVersion.mockReturnValue('1.95.0');
133
147
  vi.mocked(fetchVersion).mockResolvedValue('1.96.0');
package/dist/index.js CHANGED
@@ -108,7 +108,18 @@ const checker = new Checker(configData.tools, storage, notifier, assetMirror, do
108
108
  let scheduledTask = null;
109
109
  let lastCheckTime = null;
110
110
  let nextCheckTime = null;
111
- function calculateNextCheckTime(intervalHours) {
111
+ // Parse HH:MM time string
112
+ function parseTime(timeStr) {
113
+ const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
114
+ if (!match)
115
+ return null;
116
+ const hour = parseInt(match[1], 10);
117
+ const minute = parseInt(match[2], 10);
118
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
119
+ return null;
120
+ return { hour, minute };
121
+ }
122
+ function calculateNextCheckTimeInterval(intervalHours) {
112
123
  const now = new Date();
113
124
  const next = new Date(now);
114
125
  next.setMinutes(0, 0, 0);
@@ -118,28 +129,67 @@ function calculateNextCheckTime(intervalHours) {
118
129
  }
119
130
  return next;
120
131
  }
121
- function scheduleChecks(intervalHours) {
132
+ function calculateNextCheckTimeDaily(timeStr) {
133
+ const parsed = parseTime(timeStr);
134
+ if (!parsed) {
135
+ // Fallback to 6am if invalid
136
+ return calculateNextCheckTimeDaily('06:00');
137
+ }
138
+ const now = new Date();
139
+ const next = new Date(now);
140
+ next.setHours(parsed.hour, parsed.minute, 0, 0);
141
+ // If the time has passed today, schedule for tomorrow
142
+ if (next <= now) {
143
+ next.setDate(next.getDate() + 1);
144
+ }
145
+ return next;
146
+ }
147
+ function calculateNextCheckTime() {
148
+ const mode = configData.scheduleMode || 'interval';
149
+ if (mode === 'daily') {
150
+ return calculateNextCheckTimeDaily(configData.dailyCheckTime || '06:00');
151
+ }
152
+ return calculateNextCheckTimeInterval(configData.checkIntervalHours);
153
+ }
154
+ async function runScheduledCheck() {
155
+ console.log(`[${new Date().toISOString()}] Running scheduled check`);
156
+ lastCheckTime = new Date();
157
+ const result = await checker.checkAll();
158
+ nextCheckTime = calculateNextCheckTime();
159
+ // Auto-publish CLI if updates were detected
160
+ if (result.hasUpdates && cliPublisher.isConfigured()) {
161
+ const state = storage.load();
162
+ const mirrorUrls = storage.getAllMirrorUrls();
163
+ const publishResult = await cliPublisher.publish(state.versions, mirrorUrls);
164
+ if (publishResult.success) {
165
+ await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
166
+ }
167
+ }
168
+ }
169
+ function scheduleChecks() {
122
170
  if (scheduledTask) {
123
171
  scheduledTask.stop();
124
172
  }
125
- nextCheckTime = calculateNextCheckTime(intervalHours);
126
- const cronExpression = `0 */${intervalHours} * * *`;
127
- scheduledTask = cron.schedule(cronExpression, async () => {
128
- console.log(`[${new Date().toISOString()}] Running scheduled check`);
129
- lastCheckTime = new Date();
130
- const result = await checker.checkAll();
131
- nextCheckTime = calculateNextCheckTime(intervalHours);
132
- // Auto-publish CLI if updates were detected
133
- if (result.hasUpdates && cliPublisher.isConfigured()) {
134
- const state = storage.load();
135
- const mirrorUrls = storage.getAllMirrorUrls();
136
- const publishResult = await cliPublisher.publish(state.versions, mirrorUrls);
137
- if (publishResult.success) {
138
- await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
139
- }
140
- }
141
- });
142
- console.log(`Scheduled checks every ${intervalHours} hours`);
173
+ const mode = configData.scheduleMode || 'interval';
174
+ let cronExpression;
175
+ if (mode === 'daily') {
176
+ const timeStr = configData.dailyCheckTime || '06:00';
177
+ const parsed = parseTime(timeStr);
178
+ const hour = parsed?.hour ?? 6;
179
+ const minute = parsed?.minute ?? 0;
180
+ cronExpression = `${minute} ${hour} * * *`;
181
+ console.log(`Scheduled daily check at ${timeStr}`);
182
+ }
183
+ else {
184
+ const intervalHours = configData.checkIntervalHours;
185
+ cronExpression = `0 */${intervalHours} * * *`;
186
+ console.log(`Scheduled checks every ${intervalHours} hours`);
187
+ }
188
+ nextCheckTime = calculateNextCheckTime();
189
+ scheduledTask = cron.schedule(cronExpression, runScheduledCheck);
190
+ }
191
+ function saveConfig() {
192
+ writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
143
193
  }
144
194
  // Bot commands
145
195
  bot.onText(/\/check/, async (msg) => {
@@ -148,7 +198,7 @@ bot.onText(/\/check/, async (msg) => {
148
198
  await bot.sendMessage(validatedChatId, 'Checking for updates...');
149
199
  lastCheckTime = new Date();
150
200
  const result = await checker.checkAll();
151
- nextCheckTime = calculateNextCheckTime(configData.checkIntervalHours);
201
+ nextCheckTime = calculateNextCheckTime();
152
202
  // Auto-publish CLI if updates were detected
153
203
  if (result.hasUpdates && cliPublisher.isConfigured()) {
154
204
  const state = storage.load();
@@ -213,12 +263,84 @@ bot.onText(/\/setinterval(?:\s+(\d+))?/, async (msg, match) => {
213
263
  await bot.sendMessage(validatedChatId, 'Interval must be between 1 and 24 hours');
214
264
  return;
215
265
  }
216
- // Update config
266
+ // Update config (also switch to interval mode)
217
267
  configData.checkIntervalHours = hours;
218
- writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
268
+ configData.scheduleMode = 'interval';
269
+ saveConfig();
219
270
  // Reschedule
220
- scheduleChecks(hours);
221
- await bot.sendMessage(validatedChatId, `Check interval updated to every ${hours} hours`);
271
+ scheduleChecks();
272
+ await bot.sendMessage(validatedChatId, `Check interval updated to every ${hours} hours (interval mode)`);
273
+ });
274
+ // /schedule - show current schedule configuration
275
+ bot.onText(/\/schedule$/, async (msg) => {
276
+ if (msg.chat.id.toString() !== validatedChatId)
277
+ return;
278
+ const mode = configData.scheduleMode || 'interval';
279
+ let message;
280
+ if (mode === 'daily') {
281
+ const time = configData.dailyCheckTime || '06:00';
282
+ message = `📅 Schedule: Daily at ${time}`;
283
+ }
284
+ else {
285
+ message = `🔄 Schedule: Every ${configData.checkIntervalHours} hours`;
286
+ }
287
+ // Add next check info
288
+ if (nextCheckTime) {
289
+ const mins = Math.round((nextCheckTime.getTime() - Date.now()) / 60000);
290
+ if (mins > 0) {
291
+ const hours = Math.floor(mins / 60);
292
+ const remainingMins = mins % 60;
293
+ message += `\nNext check: in ${hours > 0 ? hours + 'h ' : ''}${remainingMins}m`;
294
+ }
295
+ else {
296
+ message += '\nNext check: soon';
297
+ }
298
+ }
299
+ await bot.sendMessage(validatedChatId, message);
300
+ });
301
+ // /settime <HH:MM> - set daily check time
302
+ bot.onText(/\/settime(?:\s+(.+))?/, async (msg, match) => {
303
+ if (msg.chat.id.toString() !== validatedChatId)
304
+ return;
305
+ const timeStr = match?.[1]?.trim();
306
+ if (!timeStr) {
307
+ await bot.sendMessage(validatedChatId, 'Usage: /settime <HH:MM>\nExample: /settime 06:00');
308
+ return;
309
+ }
310
+ const parsed = parseTime(timeStr);
311
+ if (!parsed) {
312
+ await bot.sendMessage(validatedChatId, 'Invalid time format. Use HH:MM (24-hour), e.g., 06:00 or 18:30');
313
+ return;
314
+ }
315
+ // Update config and switch to daily mode
316
+ configData.dailyCheckTime = timeStr;
317
+ configData.scheduleMode = 'daily';
318
+ saveConfig();
319
+ // Reschedule
320
+ scheduleChecks();
321
+ await bot.sendMessage(validatedChatId, `✅ Daily check scheduled at ${timeStr}`);
322
+ });
323
+ // /setmode <daily|interval> - switch schedule mode
324
+ bot.onText(/\/setmode(?:\s+(.+))?/, async (msg, match) => {
325
+ if (msg.chat.id.toString() !== validatedChatId)
326
+ return;
327
+ const modeArg = match?.[1]?.trim().toLowerCase();
328
+ if (!modeArg || !['daily', 'interval'].includes(modeArg)) {
329
+ await bot.sendMessage(validatedChatId, 'Usage: /setmode <daily|interval>\n\n• daily - check once per day at configured time\n• interval - check every N hours');
330
+ return;
331
+ }
332
+ const newMode = modeArg;
333
+ configData.scheduleMode = newMode;
334
+ saveConfig();
335
+ // Reschedule
336
+ scheduleChecks();
337
+ if (newMode === 'daily') {
338
+ const time = configData.dailyCheckTime || '06:00';
339
+ await bot.sendMessage(validatedChatId, `📅 Switched to daily mode. Checking at ${time}\n\nUse /settime to change the time.`);
340
+ }
341
+ else {
342
+ await bot.sendMessage(validatedChatId, `🔄 Switched to interval mode. Checking every ${configData.checkIntervalHours} hours\n\nUse /setinterval to change the interval.`);
343
+ }
222
344
  });
223
345
  bot.onText(/\/generate/, async (msg) => {
224
346
  if (msg.chat.id.toString() !== validatedChatId)
@@ -321,6 +443,12 @@ bot.onText(/\/mirror(?:\s+(.+))?/, async (msg, match) => {
321
443
  }
322
444
  });
323
445
  // Start scheduled checks
324
- scheduleChecks(configData.checkIntervalHours);
325
- console.log(`ReleaseRadar started. Checking every ${configData.checkIntervalHours} hours.`);
446
+ scheduleChecks();
447
+ const mode = configData.scheduleMode || 'interval';
448
+ if (mode === 'daily') {
449
+ console.log(`ReleaseRadar started. Daily check at ${configData.dailyCheckTime || '06:00'}.`);
450
+ }
451
+ else {
452
+ console.log(`ReleaseRadar started. Checking every ${configData.checkIntervalHours} hours.`);
453
+ }
326
454
  console.log(`Tracking ${configData.tools.length} tools.`);
@@ -0,0 +1,85 @@
1
+ // src/mirror-and-publish.ts
2
+ // Standalone script to mirror assets and publish CLI
3
+ import { readFileSync, existsSync, mkdirSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+ import { Storage } from './storage.js';
8
+ import { AssetMirror } from './asset-mirror.js';
9
+ import { CliPublisher } from './cli-publisher.js';
10
+ import { fetchVersion } from './fetchers/index.js';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const PKG_ROOT = join(__dirname, '..');
14
+ // Paths
15
+ const CONFIG_PATH = join(PKG_ROOT, 'config', 'tools.json');
16
+ const DOWNLOADS_PATH = join(PKG_ROOT, 'config', 'downloads.json');
17
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || '/tmp';
18
+ const DATA_DIR = process.env.RELEASE_RADAR_DATA_DIR || join(HOME_DIR, '.release-radar');
19
+ // Ensure data directory exists
20
+ if (!existsSync(DATA_DIR)) {
21
+ mkdirSync(DATA_DIR, { recursive: true });
22
+ }
23
+ // Load configs
24
+ const configData = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
25
+ const downloadsConfig = JSON.parse(readFileSync(DOWNLOADS_PATH, 'utf-8'));
26
+ // Initialize components
27
+ const storage = new Storage(join(DATA_DIR, 'versions.json'));
28
+ const assetMirror = new AssetMirror();
29
+ const USER_CLI_DIR = join(DATA_DIR, 'cli');
30
+ const cliPublisher = new CliPublisher(downloadsConfig, USER_CLI_DIR);
31
+ async function main() {
32
+ console.log('🔍 Checking versions and mirroring assets...\n');
33
+ const toolsToMirror = configData.tools.filter((tool) => {
34
+ const downloadConfig = downloadsConfig[tool.name];
35
+ return downloadConfig &&
36
+ downloadConfig.type !== 'npm' &&
37
+ 'mirror' in downloadConfig &&
38
+ downloadConfig.mirror;
39
+ });
40
+ console.log(`Found ${toolsToMirror.length} tools configured for mirroring`);
41
+ for (const tool of toolsToMirror) {
42
+ try {
43
+ console.log(`\n📦 ${tool.name}`);
44
+ // Fetch current version
45
+ const version = await fetchVersion(tool);
46
+ console.log(` Version: ${version}`);
47
+ // Store version
48
+ const oldVersion = storage.getVersion(tool.name);
49
+ if (oldVersion !== version) {
50
+ storage.setVersion(tool.name, version);
51
+ console.log(` Updated from ${oldVersion || 'none'} to ${version}`);
52
+ }
53
+ // Mirror asset
54
+ const downloadConfig = downloadsConfig[tool.name];
55
+ const result = await assetMirror.mirror(tool.name, version, downloadConfig.mirror, downloadConfig.filename);
56
+ if (result.success && result.downloadUrl) {
57
+ storage.setMirrorUrl(tool.name, result.downloadUrl);
58
+ console.log(` ✅ Mirrored: ${result.downloadUrl}`);
59
+ }
60
+ else {
61
+ console.log(` ⚠️ Mirror failed: ${result.error}`);
62
+ }
63
+ }
64
+ catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ console.error(` ❌ Error: ${message}`);
67
+ }
68
+ }
69
+ // Publish CLI
70
+ console.log('\n\n📦 Publishing CLI...');
71
+ const state = storage.load();
72
+ const mirrorUrls = storage.getAllMirrorUrls();
73
+ const result = await cliPublisher.publish(state.versions, mirrorUrls);
74
+ if (result.success) {
75
+ console.log(`✅ CLI published: v${result.version}`);
76
+ }
77
+ else {
78
+ console.error(`❌ CLI publish failed: ${result.error}`);
79
+ process.exit(1);
80
+ }
81
+ }
82
+ main().catch((error) => {
83
+ console.error('Fatal error:', error);
84
+ process.exit(1);
85
+ });
package/dist/types.d.ts CHANGED
@@ -8,12 +8,17 @@ export interface ToolConfig {
8
8
  fallbackUrl?: string;
9
9
  customFetcher?: string;
10
10
  }
11
+ export type ScheduleMode = 'interval' | 'daily';
11
12
  export interface Config {
12
13
  checkIntervalHours: number;
14
+ scheduleMode?: ScheduleMode;
15
+ dailyCheckTime?: string;
13
16
  tools: ToolConfig[];
14
17
  }
15
18
  export interface MirrorConfig {
16
19
  sourceUrl: string;
20
+ extensionId?: string;
21
+ targetPlatform?: string;
17
22
  }
18
23
  export interface DownloadConfigUrl {
19
24
  type?: 'download';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvnt/release-radar",
3
- "version": "1.7.16",
3
+ "version": "1.9.0",
4
4
  "description": "Monitor tool versions and notify via Telegram when updates are detected",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,15 +0,0 @@
1
- export interface MirrorResult {
2
- success: boolean;
3
- downloadUrl?: string;
4
- error?: string;
5
- }
6
- export declare class VsixMirror {
7
- private repo;
8
- private extensionId;
9
- private targetPlatform;
10
- mirror(version: string): Promise<MirrorResult>;
11
- releaseExists(tag: string): Promise<boolean>;
12
- private getMarketplaceVsixUrl;
13
- private downloadVsix;
14
- private createRelease;
15
- }
@@ -1,96 +0,0 @@
1
- // src/vsix-mirror.ts
2
- import { execSync } from 'child_process';
3
- import { unlinkSync, existsSync } from 'fs';
4
- import { tmpdir } from 'os';
5
- import { join } from 'path';
6
- export class VsixMirror {
7
- repo = 'lvntbkdmr/apps';
8
- extensionId = 'anthropic.claude-code';
9
- targetPlatform = 'win32-x64';
10
- async mirror(version) {
11
- const tag = `claude-code-vsix-v${version}`;
12
- const filename = `claude-code-${version}-win32-x64.vsix`;
13
- const downloadUrl = `github.com/${this.repo}/releases/download/${tag}/${filename}`;
14
- try {
15
- // Check if release already exists
16
- if (await this.releaseExists(tag)) {
17
- console.log(`[VsixMirror] Release ${tag} already exists, skipping`);
18
- return { success: true, downloadUrl };
19
- }
20
- // Get VSIX URL from marketplace
21
- console.log(`[VsixMirror] Querying marketplace for ${this.extensionId} v${version}...`);
22
- const vsixUrl = await this.getMarketplaceVsixUrl(version);
23
- // Download VSIX to temp file
24
- const tempPath = join(tmpdir(), filename);
25
- console.log(`[VsixMirror] Downloading VSIX to ${tempPath}...`);
26
- await this.downloadVsix(vsixUrl, tempPath);
27
- // Create GitHub release with VSIX attached
28
- console.log(`[VsixMirror] Creating release ${tag}...`);
29
- await this.createRelease(tag, tempPath, filename, version);
30
- // Cleanup temp file
31
- if (existsSync(tempPath)) {
32
- unlinkSync(tempPath);
33
- }
34
- console.log(`[VsixMirror] Successfully mirrored to ${downloadUrl}`);
35
- return { success: true, downloadUrl };
36
- }
37
- catch (error) {
38
- const message = error instanceof Error ? error.message : String(error);
39
- console.error(`[VsixMirror] Failed to mirror: ${message}`);
40
- return { success: false, error: message };
41
- }
42
- }
43
- async releaseExists(tag) {
44
- try {
45
- execSync(`gh release view ${tag} --repo ${this.repo}`, {
46
- encoding: 'utf-8',
47
- stdio: ['pipe', 'pipe', 'pipe'],
48
- });
49
- return true;
50
- }
51
- catch {
52
- return false;
53
- }
54
- }
55
- async getMarketplaceVsixUrl(version) {
56
- const query = JSON.stringify({
57
- filters: [{
58
- criteria: [{ filterType: 7, value: this.extensionId }],
59
- pageNumber: 1,
60
- pageSize: 1,
61
- }],
62
- flags: 3,
63
- });
64
- const cmd = `curl -sS 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' ` +
65
- `-H 'Accept: application/json; api-version=7.2-preview.1' ` +
66
- `-H 'Content-Type: application/json' ` +
67
- `--data '${query}'`;
68
- const response = execSync(cmd, { encoding: 'utf-8', timeout: 30000 });
69
- const data = JSON.parse(response);
70
- const versions = data.results?.[0]?.extensions?.[0]?.versions || [];
71
- const targetVersion = versions.find((v) => v.version === version && v.targetPlatform === this.targetPlatform);
72
- if (!targetVersion) {
73
- throw new Error(`Version ${version} for ${this.targetPlatform} not found in marketplace`);
74
- }
75
- const vsixFile = targetVersion.files?.find((f) => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage');
76
- if (!vsixFile?.source) {
77
- throw new Error('VSIX download URL not found in marketplace response');
78
- }
79
- return vsixFile.source;
80
- }
81
- async downloadVsix(url, destPath) {
82
- execSync(`curl -sS -L -o "${destPath}" "${url}"`, {
83
- encoding: 'utf-8',
84
- timeout: 300000, // 5 minutes for large files
85
- });
86
- if (!existsSync(destPath)) {
87
- throw new Error('Download failed - file not created');
88
- }
89
- }
90
- async createRelease(tag, vsixPath, filename, version) {
91
- const title = `Claude Code VSCode ${version}`;
92
- const notes = `Mirrored from VS Code Marketplace for Nexus proxy access.\n\nPlatform: win32-x64`;
93
- execSync(`gh release create "${tag}" "${vsixPath}#${filename}" ` +
94
- `--repo ${this.repo} --title "${title}" --notes "${notes}"`, { encoding: 'utf-8', timeout: 300000 });
95
- }
96
- }