@lvnt/release-radar 1.7.13 → 1.7.14

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.
@@ -1,4 +1,9 @@
1
1
  {
2
+ "Claude Code VSCode": {
3
+ "displayName": "Claude Code Extension",
4
+ "downloadUrl": "{{MIRROR_URL}}",
5
+ "filename": "claude-code-{{VERSION}}-win32-x64.vsix"
6
+ },
2
7
  "Claude Code CLI": {
3
8
  "displayName": "Claude Code CLI",
4
9
  "downloadUrl": "storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/{{VERSION}}/win32-x64/claude.exe",
package/dist/checker.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { ToolConfig } from './types.js';
2
2
  import type { Storage } from './storage.js';
3
3
  import type { Notifier } from './notifier.js';
4
+ import type { VsixMirror } from './vsix-mirror.js';
4
5
  export declare class Checker {
5
6
  private tools;
6
7
  private storage;
7
8
  private notifier;
8
- constructor(tools: ToolConfig[], storage: Storage, notifier: Notifier);
9
+ private vsixMirror?;
10
+ constructor(tools: ToolConfig[], storage: Storage, notifier: Notifier, vsixMirror?: VsixMirror | undefined);
9
11
  checkAll(): Promise<{
10
12
  hasUpdates: boolean;
11
13
  updateCount: number;
package/dist/checker.js CHANGED
@@ -3,10 +3,12 @@ export class Checker {
3
3
  tools;
4
4
  storage;
5
5
  notifier;
6
- constructor(tools, storage, notifier) {
6
+ vsixMirror;
7
+ constructor(tools, storage, notifier, vsixMirror) {
7
8
  this.tools = tools;
8
9
  this.storage = storage;
9
10
  this.notifier = notifier;
11
+ this.vsixMirror = vsixMirror;
10
12
  }
11
13
  async checkAll() {
12
14
  const updates = [];
@@ -22,6 +24,13 @@ export class Checker {
22
24
  else if (oldVersion !== newVersion) {
23
25
  updates.push({ name: tool.name, oldVersion, newVersion });
24
26
  this.storage.setVersion(tool.name, newVersion);
27
+ // Mirror VSIX to GitHub if this is Claude Code VSCode
28
+ if (tool.name === 'Claude Code VSCode' && this.vsixMirror) {
29
+ const mirrorResult = await this.vsixMirror.mirror(newVersion);
30
+ if (mirrorResult.success && mirrorResult.downloadUrl) {
31
+ this.storage.setMirrorUrl(tool.name, mirrorResult.downloadUrl);
32
+ }
33
+ }
25
34
  }
26
35
  }
27
36
  catch (error) {
@@ -14,7 +14,8 @@ describe('Checker', () => {
14
14
  vi.clearAllMocks();
15
15
  mockStorage = {
16
16
  getVersion: vi.fn(),
17
- setVersion: vi.fn()
17
+ setVersion: vi.fn(),
18
+ setMirrorUrl: vi.fn()
18
19
  };
19
20
  mockNotifier = {
20
21
  sendBatchedUpdates: vi.fn().mockResolvedValue(undefined),
@@ -77,4 +78,53 @@ describe('Checker', () => {
77
78
  expect(result.hasUpdates).toBe(false);
78
79
  expect(result.updateCount).toBe(0);
79
80
  });
81
+ describe('VSIX mirroring', () => {
82
+ let mockVsixMirror;
83
+ beforeEach(() => {
84
+ mockVsixMirror = {
85
+ mirror: vi.fn().mockResolvedValue({ success: true, downloadUrl: 'github.com/test/url.vsix' })
86
+ };
87
+ });
88
+ it('mirrors VSIX when Claude Code VSCode updates', async () => {
89
+ const vsCodeTool = {
90
+ name: 'Claude Code VSCode',
91
+ type: 'vscode-marketplace',
92
+ extensionId: 'anthropic.claude-code'
93
+ };
94
+ const checkerWithMirror = new Checker([vsCodeTool], mockStorage, mockNotifier, mockVsixMirror);
95
+ mockStorage.getVersion.mockReturnValue('2.1.8');
96
+ vi.mocked(fetchVersion).mockResolvedValue('2.1.9');
97
+ await checkerWithMirror.checkAll();
98
+ expect(mockVsixMirror.mirror).toHaveBeenCalledWith('2.1.9');
99
+ expect(mockStorage.setMirrorUrl).toHaveBeenCalledWith('Claude Code VSCode', 'github.com/test/url.vsix');
100
+ });
101
+ it('does not mirror when version unchanged', async () => {
102
+ const vsCodeTool = {
103
+ name: 'Claude Code VSCode',
104
+ type: 'vscode-marketplace',
105
+ extensionId: 'anthropic.claude-code'
106
+ };
107
+ const checkerWithMirror = new Checker([vsCodeTool], mockStorage, mockNotifier, mockVsixMirror);
108
+ mockStorage.getVersion.mockReturnValue('2.1.9');
109
+ vi.mocked(fetchVersion).mockResolvedValue('2.1.9');
110
+ await checkerWithMirror.checkAll();
111
+ expect(mockVsixMirror.mirror).not.toHaveBeenCalled();
112
+ });
113
+ it('continues if mirror fails', async () => {
114
+ const vsCodeTool = {
115
+ name: 'Claude Code VSCode',
116
+ type: 'vscode-marketplace',
117
+ extensionId: 'anthropic.claude-code'
118
+ };
119
+ mockVsixMirror.mirror.mockResolvedValue({ success: false, error: 'network error' });
120
+ const checkerWithMirror = new Checker([vsCodeTool], mockStorage, mockNotifier, mockVsixMirror);
121
+ mockStorage.getVersion.mockReturnValue('2.1.8');
122
+ vi.mocked(fetchVersion).mockResolvedValue('2.1.9');
123
+ await checkerWithMirror.checkAll();
124
+ // Version should still be updated
125
+ expect(mockStorage.setVersion).toHaveBeenCalledWith('Claude Code VSCode', '2.1.9');
126
+ // But no mirror URL stored
127
+ expect(mockStorage.setMirrorUrl).not.toHaveBeenCalled();
128
+ });
129
+ });
80
130
  });
@@ -9,5 +9,5 @@ export declare class CliPublisher {
9
9
  private cliPath;
10
10
  constructor(downloadsConfig: DownloadsConfig, cliPath?: string);
11
11
  isConfigured(): boolean;
12
- publish(versions: Record<string, string>): Promise<PublishResult>;
12
+ publish(versions: Record<string, string>, mirrorUrls?: Record<string, string>): Promise<PublishResult>;
13
13
  }
@@ -30,13 +30,13 @@ export class CliPublisher {
30
30
  isConfigured() {
31
31
  return Object.keys(this.downloadsConfig).length > 0 && existsSync(this.cliPath);
32
32
  }
33
- async publish(versions) {
33
+ async publish(versions, mirrorUrls = {}) {
34
34
  if (!this.isConfigured()) {
35
35
  return { success: false, error: 'CLI publisher not configured' };
36
36
  }
37
37
  try {
38
- // Generate versions.json
39
- const versionsJson = generateVersionsJson(versions, this.downloadsConfig);
38
+ // Generate versions.json with mirror URLs
39
+ const versionsJson = generateVersionsJson(versions, this.downloadsConfig, mirrorUrls);
40
40
  // Write to CLI package
41
41
  const cliVersionsPath = `${this.cliPath}/versions.json`;
42
42
  writeFileSync(cliVersionsPath, JSON.stringify(versionsJson, null, 2));
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { Notifier } from './notifier.js';
11
11
  import { Checker } from './checker.js';
12
12
  import { generateVersionsJson } from './versions-generator.js';
13
13
  import { CliPublisher } from './cli-publisher.js';
14
+ import { VsixMirror } from './vsix-mirror.js';
14
15
  // Get package directory for resolving config paths
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = dirname(__filename);
@@ -100,8 +101,9 @@ else {
100
101
  const bot = new TelegramBot(BOT_TOKEN, { polling: true });
101
102
  const storage = new Storage(join(DATA_DIR, 'versions.json'));
102
103
  const notifier = new Notifier(bot, validatedChatId);
103
- const checker = new Checker(configData.tools, storage, notifier);
104
104
  const cliPublisher = new CliPublisher(downloadsConfig, USER_CLI_DIR);
105
+ const vsixMirror = new VsixMirror();
106
+ const checker = new Checker(configData.tools, storage, notifier, vsixMirror);
105
107
  // Track scheduled task for rescheduling
106
108
  let scheduledTask = null;
107
109
  let lastCheckTime = null;
@@ -130,7 +132,8 @@ function scheduleChecks(intervalHours) {
130
132
  // Auto-publish CLI if updates were detected
131
133
  if (result.hasUpdates && cliPublisher.isConfigured()) {
132
134
  const state = storage.load();
133
- const publishResult = await cliPublisher.publish(state.versions);
135
+ const mirrorUrls = storage.getAllMirrorUrls();
136
+ const publishResult = await cliPublisher.publish(state.versions, mirrorUrls);
134
137
  if (publishResult.success) {
135
138
  await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
136
139
  }
@@ -149,7 +152,8 @@ bot.onText(/\/check/, async (msg) => {
149
152
  // Auto-publish CLI if updates were detected
150
153
  if (result.hasUpdates && cliPublisher.isConfigured()) {
151
154
  const state = storage.load();
152
- const publishResult = await cliPublisher.publish(state.versions);
155
+ const mirrorUrls = storage.getAllMirrorUrls();
156
+ const publishResult = await cliPublisher.publish(state.versions, mirrorUrls);
153
157
  if (publishResult.success) {
154
158
  await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
155
159
  }
@@ -260,9 +264,10 @@ bot.onText(/\/publishcli/, async (msg) => {
260
264
  return;
261
265
  }
262
266
  const state = storage.load();
267
+ const mirrorUrls = storage.getAllMirrorUrls();
263
268
  const preview = formatCliPreview(state.versions);
264
269
  await bot.sendMessage(validatedChatId, `📦 Publishing CLI...\n\n${preview}`);
265
- const result = await cliPublisher.publish(state.versions);
270
+ const result = await cliPublisher.publish(state.versions, mirrorUrls);
266
271
  if (result.success) {
267
272
  await bot.sendMessage(validatedChatId, `✅ CLI published: v${result.version}`);
268
273
  }
@@ -270,6 +275,25 @@ bot.onText(/\/publishcli/, async (msg) => {
270
275
  await bot.sendMessage(validatedChatId, `❌ CLI publish failed: ${result.error}`);
271
276
  }
272
277
  });
278
+ bot.onText(/\/mirrorvsix(?:\s+(.+))?/, async (msg, match) => {
279
+ if (msg.chat.id.toString() !== validatedChatId)
280
+ return;
281
+ const versionArg = match?.[1]?.trim();
282
+ const version = versionArg || storage.getVersion('Claude Code VSCode');
283
+ if (!version) {
284
+ await bot.sendMessage(validatedChatId, 'No version specified and no tracked version found. Usage: /mirrorvsix [version]');
285
+ return;
286
+ }
287
+ await bot.sendMessage(validatedChatId, `Mirroring Claude Code VSCode v${version}...`);
288
+ const result = await vsixMirror.mirror(version);
289
+ if (result.success) {
290
+ storage.setMirrorUrl('Claude Code VSCode', result.downloadUrl);
291
+ await bot.sendMessage(validatedChatId, `✅ Mirrored successfully\nURL: ${result.downloadUrl}`);
292
+ }
293
+ else {
294
+ await bot.sendMessage(validatedChatId, `❌ Mirror failed: ${result.error}`);
295
+ }
296
+ });
273
297
  // Start scheduled checks
274
298
  scheduleChecks(configData.checkIntervalHours);
275
299
  console.log(`ReleaseRadar started. Checking every ${configData.checkIntervalHours} hours.`);
package/dist/storage.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface StorageState {
2
2
  lastCheck: string | null;
3
3
  versions: Record<string, string>;
4
+ mirrorUrls?: Record<string, string>;
4
5
  }
5
6
  export declare class Storage {
6
7
  private filePath;
@@ -11,4 +12,7 @@ export declare class Storage {
11
12
  save(state: StorageState): void;
12
13
  getVersion(toolName: string): string | null;
13
14
  setVersion(toolName: string, version: string): void;
15
+ getMirrorUrl(toolName: string): string | null;
16
+ setMirrorUrl(toolName: string, url: string): void;
17
+ getAllMirrorUrls(): Record<string, string>;
14
18
  }
package/dist/storage.js CHANGED
@@ -34,4 +34,20 @@ export class Storage {
34
34
  state.lastCheck = new Date().toISOString();
35
35
  this.save(state);
36
36
  }
37
+ getMirrorUrl(toolName) {
38
+ const state = this.ensureLoaded();
39
+ return state.mirrorUrls?.[toolName] ?? null;
40
+ }
41
+ setMirrorUrl(toolName, url) {
42
+ const state = this.ensureLoaded();
43
+ if (!state.mirrorUrls) {
44
+ state.mirrorUrls = {};
45
+ }
46
+ state.mirrorUrls[toolName] = url;
47
+ this.save(state);
48
+ }
49
+ getAllMirrorUrls() {
50
+ const state = this.ensureLoaded();
51
+ return state.mirrorUrls ?? {};
52
+ }
37
53
  }
@@ -50,4 +50,33 @@ describe('Storage', () => {
50
50
  const newStorage = new Storage(testPath);
51
51
  expect(newStorage.getVersion('Ninja')).toBe('1.12.0');
52
52
  });
53
+ it('getMirrorUrl returns null for unknown tool', () => {
54
+ expect(storage.getMirrorUrl('Unknown')).toBeNull();
55
+ });
56
+ it('getMirrorUrl returns stored mirror URL', () => {
57
+ const state = {
58
+ lastCheck: null,
59
+ versions: {},
60
+ mirrorUrls: { 'Claude Code VSCode': 'github.com/lvntbkdmr/apps/releases/download/v1/file.vsix' }
61
+ };
62
+ writeFileSync(testPath, JSON.stringify(state));
63
+ storage = new Storage(testPath);
64
+ expect(storage.getMirrorUrl('Claude Code VSCode')).toBe('github.com/lvntbkdmr/apps/releases/download/v1/file.vsix');
65
+ });
66
+ it('setMirrorUrl updates and persists', () => {
67
+ storage.setMirrorUrl('Claude Code VSCode', 'github.com/test/url.vsix');
68
+ const newStorage = new Storage(testPath);
69
+ expect(newStorage.getMirrorUrl('Claude Code VSCode')).toBe('github.com/test/url.vsix');
70
+ });
71
+ it('getAllMirrorUrls returns empty object when none set', () => {
72
+ expect(storage.getAllMirrorUrls()).toEqual({});
73
+ });
74
+ it('getAllMirrorUrls returns all stored mirror URLs', () => {
75
+ storage.setMirrorUrl('Tool1', 'url1');
76
+ storage.setMirrorUrl('Tool2', 'url2');
77
+ expect(storage.getAllMirrorUrls()).toEqual({
78
+ 'Tool1': 'url1',
79
+ 'Tool2': 'url2'
80
+ });
81
+ });
53
82
  });
@@ -1,2 +1,2 @@
1
1
  import type { DownloadsConfig, VersionsJson } from './types.js';
2
- export declare function generateVersionsJson(versions: Record<string, string>, downloads: DownloadsConfig): VersionsJson;
2
+ export declare function generateVersionsJson(versions: Record<string, string>, downloads: DownloadsConfig, mirrorUrls?: Record<string, string>): VersionsJson;
@@ -8,7 +8,7 @@ function applyVersionPlaceholders(template, version) {
8
8
  .replace(/\{\{VERSION\}\}/g, version)
9
9
  .replace(/\{\{VERSION_BASE\}\}/g, getVersionBase(version));
10
10
  }
11
- export function generateVersionsJson(versions, downloads) {
11
+ export function generateVersionsJson(versions, downloads, mirrorUrls = {}) {
12
12
  const tools = [];
13
13
  for (const [toolName, version] of Object.entries(versions)) {
14
14
  const downloadConfig = downloads[toolName];
@@ -26,18 +26,39 @@ export function generateVersionsJson(versions, downloads) {
26
26
  });
27
27
  }
28
28
  else {
29
- // download type (default)
30
- const downloadUrl = '{{NEXUS_URL}}/' +
31
- applyVersionPlaceholders(downloadConfig.downloadUrl, version);
32
- const filename = applyVersionPlaceholders(downloadConfig.filename, version);
33
- tools.push({
34
- name: toolName,
35
- displayName: downloadConfig.displayName,
36
- version,
37
- publishedAt: new Date().toISOString(),
38
- downloadUrl,
39
- filename,
40
- });
29
+ // Check if this uses MIRROR_URL placeholder
30
+ if (downloadConfig.downloadUrl === '{{MIRROR_URL}}') {
31
+ const mirrorUrl = mirrorUrls[toolName];
32
+ if (!mirrorUrl) {
33
+ // Skip this tool - no mirror URL available
34
+ console.log(`[versions-generator] Skipping ${toolName}: no mirror URL available`);
35
+ continue;
36
+ }
37
+ const downloadUrl = '{{NEXUS_URL}}/' + mirrorUrl;
38
+ const filename = applyVersionPlaceholders(downloadConfig.filename, version);
39
+ tools.push({
40
+ name: toolName,
41
+ displayName: downloadConfig.displayName,
42
+ version,
43
+ publishedAt: new Date().toISOString(),
44
+ downloadUrl,
45
+ filename,
46
+ });
47
+ }
48
+ else {
49
+ // download type (default) with version template
50
+ const downloadUrl = '{{NEXUS_URL}}/' +
51
+ applyVersionPlaceholders(downloadConfig.downloadUrl, version);
52
+ const filename = applyVersionPlaceholders(downloadConfig.filename, version);
53
+ tools.push({
54
+ name: toolName,
55
+ displayName: downloadConfig.displayName,
56
+ version,
57
+ publishedAt: new Date().toISOString(),
58
+ downloadUrl,
59
+ filename,
60
+ });
61
+ }
41
62
  }
42
63
  }
43
64
  return {
@@ -82,4 +82,46 @@ describe('generateVersionsJson', () => {
82
82
  expect(ralphy.package).toBe('ralphy-cli');
83
83
  expect(ralphy.downloadUrl).toBeUndefined();
84
84
  });
85
+ it('uses mirrorUrls for MIRROR_URL placeholder', () => {
86
+ const versions = {
87
+ 'Claude Code VSCode': '2.1.9',
88
+ };
89
+ const downloads = {
90
+ 'Claude Code VSCode': {
91
+ displayName: 'Claude Code Extension',
92
+ downloadUrl: '{{MIRROR_URL}}',
93
+ filename: 'claude-code-{{VERSION}}-win32-x64.vsix',
94
+ },
95
+ };
96
+ const mirrorUrls = {
97
+ 'Claude Code VSCode': 'github.com/lvntbkdmr/apps/releases/download/claude-code-vsix-v2.1.9/claude-code-2.1.9-win32-x64.vsix',
98
+ };
99
+ const result = generateVersionsJson(versions, downloads, mirrorUrls);
100
+ expect(result.tools).toHaveLength(1);
101
+ const tool = result.tools[0];
102
+ expect(tool.downloadUrl).toBe('{{NEXUS_URL}}/github.com/lvntbkdmr/apps/releases/download/claude-code-vsix-v2.1.9/claude-code-2.1.9-win32-x64.vsix');
103
+ expect(tool.filename).toBe('claude-code-2.1.9-win32-x64.vsix');
104
+ });
105
+ it('skips tool with MIRROR_URL placeholder when no mirrorUrl available', () => {
106
+ const versions = {
107
+ 'Claude Code VSCode': '2.1.9',
108
+ 'Ninja': '1.12.0',
109
+ };
110
+ const downloads = {
111
+ 'Claude Code VSCode': {
112
+ displayName: 'Claude Code Extension',
113
+ downloadUrl: '{{MIRROR_URL}}',
114
+ filename: 'claude-code-{{VERSION}}-win32-x64.vsix',
115
+ },
116
+ 'Ninja': {
117
+ displayName: 'Ninja',
118
+ downloadUrl: 'github.com/ninja/releases/{{VERSION}}/ninja.zip',
119
+ filename: 'ninja-{{VERSION}}.zip',
120
+ },
121
+ };
122
+ // No mirrorUrls provided for Claude Code VSCode
123
+ const result = generateVersionsJson(versions, downloads, {});
124
+ expect(result.tools).toHaveLength(1);
125
+ expect(result.tools[0].name).toBe('Ninja');
126
+ });
85
127
  });
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,96 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { VsixMirror } from './vsix-mirror.js';
3
+ import { execSync } from 'child_process';
4
+ import { existsSync, unlinkSync } from 'fs';
5
+ vi.mock('child_process', () => ({
6
+ execSync: vi.fn()
7
+ }));
8
+ vi.mock('fs', () => ({
9
+ existsSync: vi.fn(),
10
+ unlinkSync: vi.fn()
11
+ }));
12
+ describe('VsixMirror', () => {
13
+ let mirror;
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ mirror = new VsixMirror();
17
+ });
18
+ describe('mirror', () => {
19
+ it('returns existing URL if release already exists', async () => {
20
+ // gh release view succeeds = release exists
21
+ vi.mocked(execSync).mockReturnValueOnce(Buffer.from(''));
22
+ const result = await mirror.mirror('2.1.9');
23
+ expect(result.success).toBe(true);
24
+ expect(result.downloadUrl).toBe('github.com/lvntbkdmr/apps/releases/download/claude-code-vsix-v2.1.9/claude-code-2.1.9-win32-x64.vsix');
25
+ // Should not attempt to create release
26
+ expect(execSync).toHaveBeenCalledTimes(1);
27
+ });
28
+ it('returns error when marketplace query fails', async () => {
29
+ // gh release view fails = release does not exist
30
+ vi.mocked(execSync).mockImplementationOnce(() => {
31
+ throw new Error('release not found');
32
+ });
33
+ // curl for marketplace query fails
34
+ vi.mocked(execSync).mockImplementationOnce(() => {
35
+ throw new Error('network error');
36
+ });
37
+ const result = await mirror.mirror('2.1.9');
38
+ expect(result.success).toBe(false);
39
+ expect(result.error).toContain('network error');
40
+ });
41
+ it('successfully mirrors new version through full flow', async () => {
42
+ const marketplaceResponse = JSON.stringify({
43
+ results: [{
44
+ extensions: [{
45
+ versions: [{
46
+ version: '2.1.9',
47
+ targetPlatform: 'win32-x64',
48
+ files: [{
49
+ assetType: 'Microsoft.VisualStudio.Services.VSIXPackage',
50
+ source: 'https://marketplace.visualstudio.com/vsix/download'
51
+ }]
52
+ }]
53
+ }]
54
+ }]
55
+ });
56
+ // 1. gh release view fails = release does not exist
57
+ vi.mocked(execSync).mockImplementationOnce(() => {
58
+ throw new Error('release not found');
59
+ });
60
+ // 2. curl marketplace query succeeds
61
+ vi.mocked(execSync).mockReturnValueOnce(marketplaceResponse);
62
+ // 3. curl download succeeds (returns empty)
63
+ vi.mocked(execSync).mockReturnValueOnce('');
64
+ // 4. gh release create succeeds
65
+ vi.mocked(execSync).mockReturnValueOnce('');
66
+ // Mock existsSync to return true (file exists after download)
67
+ vi.mocked(existsSync).mockReturnValue(true);
68
+ const result = await mirror.mirror('2.1.9');
69
+ expect(result.success).toBe(true);
70
+ expect(result.downloadUrl).toBe('github.com/lvntbkdmr/apps/releases/download/claude-code-vsix-v2.1.9/claude-code-2.1.9-win32-x64.vsix');
71
+ expect(execSync).toHaveBeenCalledTimes(4);
72
+ // Verify the gh release create was called with correct arguments
73
+ const lastCall = vi.mocked(execSync).mock.calls[3][0];
74
+ expect(lastCall).toContain('gh release create');
75
+ expect(lastCall).toContain('claude-code-vsix-v2.1.9');
76
+ expect(lastCall).toContain('--repo lvntbkdmr/apps');
77
+ // Verify cleanup was called
78
+ expect(unlinkSync).toHaveBeenCalled();
79
+ });
80
+ });
81
+ describe('releaseExists', () => {
82
+ it('returns true when release exists', async () => {
83
+ vi.mocked(execSync).mockReturnValueOnce(Buffer.from(''));
84
+ const exists = await mirror.releaseExists('claude-code-vsix-v2.1.9');
85
+ expect(exists).toBe(true);
86
+ expect(execSync).toHaveBeenCalledWith('gh release view claude-code-vsix-v2.1.9 --repo lvntbkdmr/apps', expect.any(Object));
87
+ });
88
+ it('returns false when release does not exist', async () => {
89
+ vi.mocked(execSync).mockImplementationOnce(() => {
90
+ throw new Error('release not found');
91
+ });
92
+ const exists = await mirror.releaseExists('claude-code-vsix-v2.1.9');
93
+ expect(exists).toBe(false);
94
+ });
95
+ });
96
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvnt/release-radar",
3
- "version": "1.7.13",
3
+ "version": "1.7.14",
4
4
  "description": "Monitor tool versions and notify via Telegram when updates are detected",
5
5
  "main": "dist/index.js",
6
6
  "bin": {