@lvnt/release-radar 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import { DownloadTracker } from './tracker.js';
7
7
  import { loadVersions } from './versions.js';
8
8
  import { checkAndUpdate } from './updater.js';
9
9
  import { downloadFile, updateNpmPackage } from './downloader.js';
10
- import { promptSetup, promptToolSelection, renderTable, type ToolChoice, type TableRow } from './ui.js';
10
+ import { promptSetup, promptToolSelection, renderTable, isVscodeExtension, type ToolChoice, type TableRow } from './ui.js';
11
11
  import { isNpmTool } from './types.js';
12
12
 
13
13
  function getVersion(): string {
@@ -89,6 +89,7 @@ async function showStatus(): Promise<void> {
89
89
  downloadedVersion: record?.version ?? '-',
90
90
  status,
91
91
  type: isNpmTool(tool) ? 'npm' : 'download',
92
+ category: isVscodeExtension(tool) ? 'vscode-extension' : 'tool',
92
93
  };
93
94
  });
94
95
 
package/cli/src/ui.ts CHANGED
@@ -226,7 +226,7 @@ export function renderTable(rows: TableRow[]): void {
226
226
  }
227
227
  }
228
228
 
229
- function isVscodeExtension(tool: VersionsJsonTool): boolean {
229
+ export function isVscodeExtension(tool: VersionsJsonTool): boolean {
230
230
  if (isNpmTool(tool)) return false;
231
231
  // Check if filename ends with .vsix
232
232
  return tool.filename.toLowerCase().endsWith('.vsix');
package/dist/index.js CHANGED
@@ -397,6 +397,58 @@ bot.onText(/\/publishcli/, async (msg) => {
397
397
  await bot.sendMessage(validatedChatId, `❌ CLI publish failed: ${result.error}`);
398
398
  }
399
399
  });
400
+ bot.onText(/\/mirrorall/, async (msg) => {
401
+ if (msg.chat.id.toString() !== validatedChatId)
402
+ return;
403
+ // Find all tools that need mirroring (have mirror config and use {{MIRROR_URL}})
404
+ const mirrorItems = [];
405
+ for (const [toolName, config] of Object.entries(downloadsConfig)) {
406
+ if (config.type === 'npm' || !('mirror' in config) || !config.mirror)
407
+ continue;
408
+ if (config.downloadUrl !== '{{MIRROR_URL}}')
409
+ continue;
410
+ const version = storage.getVersion(toolName);
411
+ if (!version) {
412
+ console.log(`[mirrorall] Skipping ${toolName}: no tracked version`);
413
+ continue;
414
+ }
415
+ mirrorItems.push({
416
+ toolName,
417
+ version,
418
+ config: config.mirror,
419
+ filenameTemplate: config.filename,
420
+ });
421
+ }
422
+ if (mirrorItems.length === 0) {
423
+ await bot.sendMessage(validatedChatId, 'No tools configured for mirroring (or no tracked versions).');
424
+ return;
425
+ }
426
+ const toolList = mirrorItems.map(i => `• ${i.toolName} v${i.version}`).join('\n');
427
+ await bot.sendMessage(validatedChatId, `🔄 Mirroring ${mirrorItems.length} tools...\n\n${toolList}`);
428
+ const result = await assetMirror.mirrorBatch(mirrorItems);
429
+ // Update storage with successful mirrors
430
+ let successCount = 0;
431
+ let failCount = 0;
432
+ const failures = [];
433
+ for (const [toolName, mirrorResult] of result.results) {
434
+ if (mirrorResult.success && mirrorResult.downloadUrl) {
435
+ storage.setMirrorUrl(toolName, mirrorResult.downloadUrl);
436
+ successCount++;
437
+ }
438
+ else {
439
+ failCount++;
440
+ failures.push(`• ${toolName}: ${mirrorResult.error || 'Unknown error'}`);
441
+ }
442
+ }
443
+ let message = `✅ Mirrored ${successCount}/${mirrorItems.length} tools`;
444
+ if (result.tag) {
445
+ message += `\nRelease: ${result.tag}`;
446
+ }
447
+ if (failCount > 0) {
448
+ message += `\n\n❌ ${failCount} failed:\n${failures.join('\n')}`;
449
+ }
450
+ await bot.sendMessage(validatedChatId, message);
451
+ });
400
452
  bot.onText(/\/mirror(?:\s+(.+))?/, async (msg, match) => {
401
453
  if (msg.chat.id.toString() !== validatedChatId)
402
454
  return;
@@ -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.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Monitor tool versions and notify via Telegram when updates are detected",
5
5
  "main": "dist/index.js",
6
6
  "bin": {