@lvnt/release-radar 1.7.15 → 1.8.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.
File without changes
File without changes
@@ -1,8 +1,21 @@
1
1
  {
2
+ "VSCode": {
3
+ "displayName": "VS Code",
4
+ "downloadUrl": "{{MIRROR_URL}}",
5
+ "filename": "VSCode-{{VERSION}}-win-x64.msi",
6
+ "mirror": {
7
+ "sourceUrl": "https://update.code.visualstudio.com/latest/win32-x64/stable"
8
+ }
9
+ },
2
10
  "Claude Code VSCode": {
3
11
  "displayName": "Claude Code Extension",
4
12
  "downloadUrl": "{{MIRROR_URL}}",
5
- "filename": "claude-code-{{VERSION}}-win32-x64.vsix"
13
+ "filename": "claude-code-{{VERSION}}-win32-x64.vsix",
14
+ "mirror": {
15
+ "sourceUrl": "marketplace-api",
16
+ "extensionId": "anthropic.claude-code",
17
+ "targetPlatform": "win32-x64"
18
+ }
6
19
  },
7
20
  "Claude Code CLI": {
8
21
  "displayName": "Claude Code CLI",
@@ -78,5 +91,104 @@
78
91
  "displayName": "PowerShell",
79
92
  "downloadUrl": "github.com/PowerShell/PowerShell/releases/download/v{{VERSION}}/PowerShell-{{VERSION}}-win-x64.msi",
80
93
  "filename": "PowerShell-{{VERSION}}-win-x64.msi"
94
+ },
95
+ "C/C++ Extension Pack": {
96
+ "displayName": "C/C++ Extension Pack",
97
+ "downloadUrl": "{{MIRROR_URL}}",
98
+ "filename": "cpptools-extension-pack-{{VERSION}}.vsix",
99
+ "mirror": {
100
+ "sourceUrl": "marketplace-api",
101
+ "extensionId": "ms-vscode.cpptools-extension-pack"
102
+ }
103
+ },
104
+ "C/C++ Themes": {
105
+ "displayName": "C/C++ Themes",
106
+ "downloadUrl": "{{MIRROR_URL}}",
107
+ "filename": "cpptools-themes-{{VERSION}}.vsix",
108
+ "mirror": {
109
+ "sourceUrl": "marketplace-api",
110
+ "extensionId": "ms-vscode.cpptools-themes"
111
+ }
112
+ },
113
+ "YAML": {
114
+ "displayName": "YAML Extension",
115
+ "downloadUrl": "{{MIRROR_URL}}",
116
+ "filename": "vscode-yaml-{{VERSION}}.vsix",
117
+ "mirror": {
118
+ "sourceUrl": "marketplace-api",
119
+ "extensionId": "redhat.vscode-yaml"
120
+ }
121
+ },
122
+ "Python": {
123
+ "displayName": "Python Extension",
124
+ "downloadUrl": "{{MIRROR_URL}}",
125
+ "filename": "python-{{VERSION}}.vsix",
126
+ "mirror": {
127
+ "sourceUrl": "marketplace-api",
128
+ "extensionId": "ms-python.python"
129
+ }
130
+ },
131
+ "Python Debugger": {
132
+ "displayName": "Python Debugger",
133
+ "downloadUrl": "{{MIRROR_URL}}",
134
+ "filename": "debugpy-{{VERSION}}.vsix",
135
+ "mirror": {
136
+ "sourceUrl": "marketplace-api",
137
+ "extensionId": "ms-python.debugpy"
138
+ }
139
+ },
140
+ "Python Environments": {
141
+ "displayName": "Python Environments",
142
+ "downloadUrl": "{{MIRROR_URL}}",
143
+ "filename": "vscode-python-envs-{{VERSION}}.vsix",
144
+ "mirror": {
145
+ "sourceUrl": "marketplace-api",
146
+ "extensionId": "ms-python.vscode-python-envs"
147
+ }
148
+ },
149
+ "Pylance": {
150
+ "displayName": "Pylance",
151
+ "downloadUrl": "{{MIRROR_URL}}",
152
+ "filename": "vscode-pylance-{{VERSION}}.vsix",
153
+ "mirror": {
154
+ "sourceUrl": "marketplace-api",
155
+ "extensionId": "ms-python.vscode-pylance"
156
+ }
157
+ },
158
+ "isort": {
159
+ "displayName": "isort Extension",
160
+ "downloadUrl": "{{MIRROR_URL}}",
161
+ "filename": "isort-{{VERSION}}.vsix",
162
+ "mirror": {
163
+ "sourceUrl": "marketplace-api",
164
+ "extensionId": "ms-python.isort"
165
+ }
166
+ },
167
+ "Go": {
168
+ "displayName": "Go Extension",
169
+ "downloadUrl": "{{MIRROR_URL}}",
170
+ "filename": "go-{{VERSION}}.vsix",
171
+ "mirror": {
172
+ "sourceUrl": "marketplace-api",
173
+ "extensionId": "golang.go"
174
+ }
175
+ },
176
+ "GitHub Theme": {
177
+ "displayName": "GitHub Theme",
178
+ "downloadUrl": "{{MIRROR_URL}}",
179
+ "filename": "github-vscode-theme-{{VERSION}}.vsix",
180
+ "mirror": {
181
+ "sourceUrl": "marketplace-api",
182
+ "extensionId": "github.github-vscode-theme"
183
+ }
184
+ },
185
+ "Material Icon Theme": {
186
+ "displayName": "Material Icon Theme",
187
+ "downloadUrl": "{{MIRROR_URL}}",
188
+ "filename": "material-icon-theme-{{VERSION}}.vsix",
189
+ "mirror": {
190
+ "sourceUrl": "marketplace-api",
191
+ "extensionId": "pkief.material-icon-theme"
192
+ }
81
193
  }
82
194
  }
package/config/tools.json CHANGED
@@ -85,6 +85,61 @@
85
85
  "name": "PowerShell",
86
86
  "type": "github",
87
87
  "repo": "PowerShell/PowerShell"
88
+ },
89
+ {
90
+ "name": "C/C++ Extension Pack",
91
+ "type": "vscode-marketplace",
92
+ "extensionId": "ms-vscode.cpptools-extension-pack"
93
+ },
94
+ {
95
+ "name": "C/C++ Themes",
96
+ "type": "vscode-marketplace",
97
+ "extensionId": "ms-vscode.cpptools-themes"
98
+ },
99
+ {
100
+ "name": "YAML",
101
+ "type": "vscode-marketplace",
102
+ "extensionId": "redhat.vscode-yaml"
103
+ },
104
+ {
105
+ "name": "Python",
106
+ "type": "vscode-marketplace",
107
+ "extensionId": "ms-python.python"
108
+ },
109
+ {
110
+ "name": "Python Debugger",
111
+ "type": "vscode-marketplace",
112
+ "extensionId": "ms-python.debugpy"
113
+ },
114
+ {
115
+ "name": "Python Environments",
116
+ "type": "vscode-marketplace",
117
+ "extensionId": "ms-python.vscode-python-envs"
118
+ },
119
+ {
120
+ "name": "Pylance",
121
+ "type": "vscode-marketplace",
122
+ "extensionId": "ms-python.vscode-pylance"
123
+ },
124
+ {
125
+ "name": "isort",
126
+ "type": "vscode-marketplace",
127
+ "extensionId": "ms-python.isort"
128
+ },
129
+ {
130
+ "name": "Go",
131
+ "type": "vscode-marketplace",
132
+ "extensionId": "golang.go"
133
+ },
134
+ {
135
+ "name": "GitHub Theme",
136
+ "type": "vscode-marketplace",
137
+ "extensionId": "github.github-vscode-theme"
138
+ },
139
+ {
140
+ "name": "Material Icon Theme",
141
+ "type": "vscode-marketplace",
142
+ "extensionId": "pkief.material-icon-theme"
88
143
  }
89
144
  ]
90
145
  }
@@ -0,0 +1,17 @@
1
+ import type { MirrorConfig } from './types.js';
2
+ export interface MirrorResult {
3
+ success: boolean;
4
+ downloadUrl?: string;
5
+ error?: string;
6
+ }
7
+ export declare class AssetMirror {
8
+ private repo;
9
+ mirror(toolName: string, version: string, config: MirrorConfig, filenameTemplate: string): Promise<MirrorResult>;
10
+ buildTag(toolName: string, version: string): string;
11
+ releaseExists(tag: string): Promise<boolean>;
12
+ private applyVersion;
13
+ private getSourceUrl;
14
+ private getMarketplaceVsixUrl;
15
+ private downloadFile;
16
+ private createRelease;
17
+ }
@@ -0,0 +1,121 @@
1
+ // src/asset-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 AssetMirror {
7
+ repo = 'lvntbkdmr/apps';
8
+ async mirror(toolName, version, config, filenameTemplate) {
9
+ const tag = this.buildTag(toolName, version);
10
+ const filename = this.applyVersion(filenameTemplate, version);
11
+ const downloadUrl = `github.com/${this.repo}/releases/download/${tag}/${filename}`;
12
+ try {
13
+ // Check if release already exists
14
+ if (await this.releaseExists(tag)) {
15
+ console.log(`[AssetMirror] Release ${tag} already exists, skipping`);
16
+ return { success: true, downloadUrl };
17
+ }
18
+ // Get actual source URL
19
+ console.log(`[AssetMirror] Getting source URL for ${toolName} v${version}...`);
20
+ const sourceUrl = await this.getSourceUrl(config, version);
21
+ // Download to temp file
22
+ const tempPath = join(tmpdir(), filename);
23
+ console.log(`[AssetMirror] Downloading to ${tempPath}...`);
24
+ await this.downloadFile(sourceUrl, tempPath);
25
+ // Create GitHub release with asset attached
26
+ console.log(`[AssetMirror] Creating release ${tag}...`);
27
+ await this.createRelease(tag, tempPath, filename, toolName, version);
28
+ // Cleanup temp file
29
+ if (existsSync(tempPath)) {
30
+ unlinkSync(tempPath);
31
+ }
32
+ console.log(`[AssetMirror] Successfully mirrored to ${downloadUrl}`);
33
+ return { success: true, downloadUrl };
34
+ }
35
+ catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ console.error(`[AssetMirror] Failed to mirror ${toolName}: ${message}`);
38
+ return { success: false, error: message };
39
+ }
40
+ }
41
+ buildTag(toolName, version) {
42
+ const kebab = toolName.toLowerCase().replace(/\s+/g, '-');
43
+ return `${kebab}-v${version}`;
44
+ }
45
+ async releaseExists(tag) {
46
+ try {
47
+ execSync(`gh release view ${tag} --repo ${this.repo}`, {
48
+ encoding: 'utf-8',
49
+ stdio: ['pipe', 'pipe', 'pipe'],
50
+ });
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ applyVersion(template, version) {
58
+ return template.replace(/\{\{VERSION\}\}/g, version);
59
+ }
60
+ async getSourceUrl(config, version) {
61
+ if (config.sourceUrl === 'marketplace-api') {
62
+ if (!config.extensionId) {
63
+ throw new Error('extensionId is required when sourceUrl is "marketplace-api"');
64
+ }
65
+ return this.getMarketplaceVsixUrl(config.extensionId, version, config.targetPlatform);
66
+ }
67
+ // For direct URLs, just return as-is (curl -L will follow redirects)
68
+ return config.sourceUrl;
69
+ }
70
+ async getMarketplaceVsixUrl(extensionId, version, targetPlatform) {
71
+ const query = JSON.stringify({
72
+ filters: [{
73
+ criteria: [{ filterType: 7, value: extensionId }],
74
+ pageNumber: 1,
75
+ pageSize: 1,
76
+ }],
77
+ flags: 3,
78
+ });
79
+ const cmd = `curl -sS 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' ` +
80
+ `-H 'Accept: application/json; api-version=7.2-preview.1' ` +
81
+ `-H 'Content-Type: application/json' ` +
82
+ `--data '${query}'`;
83
+ const response = execSync(cmd, { encoding: 'utf-8', timeout: 30000 });
84
+ const data = JSON.parse(response);
85
+ const versions = data.results?.[0]?.extensions?.[0]?.versions || [];
86
+ // Find matching version - with or without platform filter
87
+ const targetVersion = versions.find((v) => {
88
+ if (v.version !== version)
89
+ return false;
90
+ if (targetPlatform) {
91
+ return v.targetPlatform === targetPlatform;
92
+ }
93
+ // For universal extensions, targetPlatform is undefined/null
94
+ return !v.targetPlatform;
95
+ });
96
+ if (!targetVersion) {
97
+ const platformInfo = targetPlatform ? ` for ${targetPlatform}` : ' (universal)';
98
+ throw new Error(`Version ${version}${platformInfo} not found in marketplace for ${extensionId}`);
99
+ }
100
+ const vsixFile = targetVersion.files?.find((f) => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage');
101
+ if (!vsixFile?.source) {
102
+ throw new Error(`VSIX download URL not found in marketplace response for ${extensionId}`);
103
+ }
104
+ return vsixFile.source;
105
+ }
106
+ async downloadFile(url, destPath) {
107
+ execSync(`curl -sS -L -o "${destPath}" "${url}"`, {
108
+ encoding: 'utf-8',
109
+ timeout: 300000, // 5 minutes for large files
110
+ });
111
+ if (!existsSync(destPath)) {
112
+ throw new Error('Download failed - file not created');
113
+ }
114
+ }
115
+ async createRelease(tag, filePath, filename, toolName, version) {
116
+ const title = `${toolName} ${version}`;
117
+ const notes = `Mirrored for Nexus proxy access.`;
118
+ execSync(`gh release create "${tag}" "${filePath}#${filename}" ` +
119
+ `--repo ${this.repo} --title "${title}" --notes "${notes}"`, { encoding: 'utf-8', timeout: 300000 });
120
+ }
121
+ }
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { AssetMirror } from './asset-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('AssetMirror', () => {
13
+ let mirror;
14
+ beforeEach(() => {
15
+ vi.resetAllMocks();
16
+ mirror = new AssetMirror();
17
+ });
18
+ describe('buildTag', () => {
19
+ it('converts tool name to kebab-case tag', () => {
20
+ expect(mirror.buildTag('VSCode', '1.96.0')).toBe('vscode-v1.96.0');
21
+ expect(mirror.buildTag('Claude Code VSCode', '2.1.9')).toBe('claude-code-vscode-v2.1.9');
22
+ expect(mirror.buildTag('Ninja', '1.12.0')).toBe('ninja-v1.12.0');
23
+ });
24
+ });
25
+ describe('mirror', () => {
26
+ it('returns existing URL if release already exists', async () => {
27
+ vi.mocked(execSync).mockReturnValueOnce(Buffer.from(''));
28
+ const result = await mirror.mirror('VSCode', '1.96.0', {
29
+ sourceUrl: 'https://update.code.visualstudio.com/latest/win32-x64/stable'
30
+ }, 'VSCode-{{VERSION}}-win-x64.msi');
31
+ expect(result.success).toBe(true);
32
+ expect(result.downloadUrl).toBe('github.com/lvntbkdmr/apps/releases/download/vscode-v1.96.0/VSCode-1.96.0-win-x64.msi');
33
+ expect(execSync).toHaveBeenCalledTimes(1);
34
+ });
35
+ it('returns error when download fails', async () => {
36
+ // gh release view fails = release does not exist
37
+ vi.mocked(execSync).mockImplementationOnce(() => {
38
+ throw new Error('release not found');
39
+ });
40
+ // curl download fails
41
+ vi.mocked(execSync).mockImplementationOnce(() => {
42
+ throw new Error('network error');
43
+ });
44
+ const result = await mirror.mirror('VSCode', '1.96.0', {
45
+ sourceUrl: 'https://update.code.visualstudio.com/latest/win32-x64/stable'
46
+ }, 'VSCode-{{VERSION}}-win-x64.msi');
47
+ expect(result.success).toBe(false);
48
+ expect(result.error).toContain('network error');
49
+ });
50
+ it('handles marketplace-api source for Claude Code VSCode', async () => {
51
+ const marketplaceResponse = JSON.stringify({
52
+ results: [{
53
+ extensions: [{
54
+ versions: [{
55
+ version: '2.1.9',
56
+ targetPlatform: 'win32-x64',
57
+ files: [{
58
+ assetType: 'Microsoft.VisualStudio.Services.VSIXPackage',
59
+ source: 'https://marketplace.visualstudio.com/vsix/download'
60
+ }]
61
+ }]
62
+ }]
63
+ }]
64
+ });
65
+ // 1. gh release view fails
66
+ vi.mocked(execSync).mockImplementationOnce(() => {
67
+ throw new Error('release not found');
68
+ });
69
+ // 2. curl marketplace query succeeds
70
+ vi.mocked(execSync).mockReturnValueOnce(marketplaceResponse);
71
+ // 3. curl download succeeds
72
+ vi.mocked(execSync).mockReturnValueOnce('');
73
+ // 4. gh release create succeeds
74
+ vi.mocked(execSync).mockReturnValueOnce('');
75
+ vi.mocked(existsSync).mockReturnValue(true);
76
+ const result = await mirror.mirror('Claude Code VSCode', '2.1.9', {
77
+ sourceUrl: 'marketplace-api',
78
+ extensionId: 'anthropic.claude-code',
79
+ targetPlatform: 'win32-x64'
80
+ }, 'claude-code-{{VERSION}}-win32-x64.vsix');
81
+ expect(result.success).toBe(true);
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');
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
+ });
127
+ it('successfully mirrors direct URL through full flow', async () => {
128
+ // 1. gh release view fails
129
+ vi.mocked(execSync).mockImplementationOnce(() => {
130
+ throw new Error('release not found');
131
+ });
132
+ // 2. curl download succeeds
133
+ vi.mocked(execSync).mockReturnValueOnce('');
134
+ // 3. gh release create succeeds
135
+ vi.mocked(execSync).mockReturnValueOnce('');
136
+ vi.mocked(existsSync).mockReturnValue(true);
137
+ const result = await mirror.mirror('VSCode', '1.96.0', {
138
+ sourceUrl: 'https://update.code.visualstudio.com/latest/win32-x64/stable'
139
+ }, 'VSCode-{{VERSION}}-win-x64.msi');
140
+ expect(result.success).toBe(true);
141
+ expect(execSync).toHaveBeenCalledTimes(3);
142
+ expect(unlinkSync).toHaveBeenCalled();
143
+ });
144
+ });
145
+ describe('releaseExists', () => {
146
+ it('returns true when release exists', async () => {
147
+ vi.mocked(execSync).mockReturnValueOnce(Buffer.from(''));
148
+ const exists = await mirror.releaseExists('vscode-v1.96.0');
149
+ expect(exists).toBe(true);
150
+ });
151
+ it('returns false when release does not exist', async () => {
152
+ vi.mocked(execSync).mockImplementationOnce(() => {
153
+ throw new Error('release not found');
154
+ });
155
+ const exists = await mirror.releaseExists('vscode-v1.96.0');
156
+ expect(exists).toBe(false);
157
+ });
158
+ });
159
+ });
package/dist/checker.d.ts CHANGED
@@ -1,15 +1,17 @@
1
- import type { ToolConfig } from './types.js';
1
+ import type { ToolConfig, DownloadsConfig } 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
+ import type { AssetMirror } from './asset-mirror.js';
5
5
  export declare class Checker {
6
6
  private tools;
7
7
  private storage;
8
8
  private notifier;
9
- private vsixMirror?;
10
- constructor(tools: ToolConfig[], storage: Storage, notifier: Notifier, vsixMirror?: VsixMirror | undefined);
9
+ private assetMirror?;
10
+ private downloadsConfig?;
11
+ constructor(tools: ToolConfig[], storage: Storage, notifier: Notifier, assetMirror?: AssetMirror | undefined, downloadsConfig?: DownloadsConfig | undefined);
11
12
  checkAll(): Promise<{
12
13
  hasUpdates: boolean;
13
14
  updateCount: number;
14
15
  }>;
16
+ private mirrorIfConfigured;
15
17
  }
package/dist/checker.js CHANGED
@@ -3,12 +3,14 @@ export class Checker {
3
3
  tools;
4
4
  storage;
5
5
  notifier;
6
- vsixMirror;
7
- constructor(tools, storage, notifier, vsixMirror) {
6
+ assetMirror;
7
+ downloadsConfig;
8
+ constructor(tools, storage, notifier, assetMirror, downloadsConfig) {
8
9
  this.tools = tools;
9
10
  this.storage = storage;
10
11
  this.notifier = notifier;
11
- this.vsixMirror = vsixMirror;
12
+ this.assetMirror = assetMirror;
13
+ this.downloadsConfig = downloadsConfig;
12
14
  }
13
15
  async checkAll() {
14
16
  const updates = [];
@@ -24,13 +26,8 @@ export class Checker {
24
26
  else if (oldVersion !== newVersion) {
25
27
  updates.push({ name: tool.name, oldVersion, newVersion });
26
28
  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
- }
29
+ // Mirror asset if configured
30
+ await this.mirrorIfConfigured(tool.name, newVersion);
34
31
  }
35
32
  }
36
33
  catch (error) {
@@ -42,4 +39,18 @@ export class Checker {
42
39
  await this.notifier.sendBatchedFailures(failures);
43
40
  return { hasUpdates: updates.length > 0, updateCount: updates.length };
44
41
  }
42
+ async mirrorIfConfigured(toolName, version) {
43
+ if (!this.assetMirror || !this.downloadsConfig)
44
+ return;
45
+ const downloadConfig = this.downloadsConfig[toolName];
46
+ if (!downloadConfig || downloadConfig.type === 'npm')
47
+ return;
48
+ const urlConfig = downloadConfig;
49
+ 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
+ }
55
+ }
45
56
  }
@@ -78,52 +78,61 @@ describe('Checker', () => {
78
78
  expect(result.hasUpdates).toBe(false);
79
79
  expect(result.updateCount).toBe(0);
80
80
  });
81
- describe('VSIX mirroring', () => {
82
- let mockVsixMirror;
81
+ describe('Asset mirroring', () => {
82
+ let mockAssetMirror;
83
+ let downloadsConfig;
83
84
  beforeEach(() => {
84
- mockVsixMirror = {
85
- mirror: vi.fn().mockResolvedValue({ success: true, downloadUrl: 'github.com/test/url.vsix' })
85
+ mockAssetMirror = {
86
+ mirror: vi.fn().mockResolvedValue({ success: true, downloadUrl: 'github.com/test/url' })
86
87
  };
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'
88
+ downloadsConfig = {
89
+ 'VSCode': {
90
+ displayName: 'VS Code',
91
+ downloadUrl: '{{MIRROR_URL}}',
92
+ filename: 'VSCode-{{VERSION}}-win-x64.msi',
93
+ mirror: { sourceUrl: 'https://update.code.visualstudio.com/latest/win32-x64/stable' }
94
+ },
95
+ 'Ninja': {
96
+ displayName: 'Ninja',
97
+ downloadUrl: 'github.com/ninja/releases/{{VERSION}}/ninja.zip',
98
+ filename: 'ninja-{{VERSION}}.zip'
99
+ // No mirror config
100
+ }
93
101
  };
94
- const checkerWithMirror = new Checker([vsCodeTool], mockStorage, mockNotifier, mockVsixMirror);
95
- mockStorage.getVersion.mockReturnValue('2.1.8');
96
- vi.mocked(fetchVersion).mockResolvedValue('2.1.9');
102
+ });
103
+ it('mirrors asset when tool has mirror config and version updates', async () => {
104
+ const vscodeTool = { name: 'VSCode', type: 'custom', customFetcher: 'vscode' };
105
+ const checkerWithMirror = new Checker([vscodeTool], mockStorage, mockNotifier, mockAssetMirror, downloadsConfig);
106
+ mockStorage.getVersion.mockReturnValue('1.95.0');
107
+ vi.mocked(fetchVersion).mockResolvedValue('1.96.0');
108
+ 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');
110
+ expect(mockStorage.setMirrorUrl).toHaveBeenCalledWith('VSCode', 'github.com/test/url');
111
+ });
112
+ it('does not mirror when tool has no mirror config', async () => {
113
+ const ninjaTool = { name: 'Ninja', type: 'github', repo: 'ninja-build/ninja' };
114
+ const checkerWithMirror = new Checker([ninjaTool], mockStorage, mockNotifier, mockAssetMirror, downloadsConfig);
115
+ mockStorage.getVersion.mockReturnValue('1.11.0');
116
+ vi.mocked(fetchVersion).mockResolvedValue('1.12.0');
97
117
  await checkerWithMirror.checkAll();
98
- expect(mockVsixMirror.mirror).toHaveBeenCalledWith('2.1.9');
99
- expect(mockStorage.setMirrorUrl).toHaveBeenCalledWith('Claude Code VSCode', 'github.com/test/url.vsix');
118
+ expect(mockAssetMirror.mirror).not.toHaveBeenCalled();
100
119
  });
101
120
  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');
121
+ const vscodeTool = { name: 'VSCode', type: 'custom', customFetcher: 'vscode' };
122
+ const checkerWithMirror = new Checker([vscodeTool], mockStorage, mockNotifier, mockAssetMirror, downloadsConfig);
123
+ mockStorage.getVersion.mockReturnValue('1.96.0');
124
+ vi.mocked(fetchVersion).mockResolvedValue('1.96.0');
110
125
  await checkerWithMirror.checkAll();
111
- expect(mockVsixMirror.mirror).not.toHaveBeenCalled();
126
+ expect(mockAssetMirror.mirror).not.toHaveBeenCalled();
112
127
  });
113
128
  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');
129
+ const vscodeTool = { name: 'VSCode', type: 'custom', customFetcher: 'vscode' };
130
+ mockAssetMirror.mirror.mockResolvedValue({ success: false, error: 'network error' });
131
+ const checkerWithMirror = new Checker([vscodeTool], mockStorage, mockNotifier, mockAssetMirror, downloadsConfig);
132
+ mockStorage.getVersion.mockReturnValue('1.95.0');
133
+ vi.mocked(fetchVersion).mockResolvedValue('1.96.0');
123
134
  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
135
+ expect(mockStorage.setVersion).toHaveBeenCalledWith('VSCode', '1.96.0');
127
136
  expect(mockStorage.setMirrorUrl).not.toHaveBeenCalled();
128
137
  });
129
138
  });
package/dist/index.js CHANGED
@@ -11,7 +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
+ import { AssetMirror } from './asset-mirror.js';
15
15
  // Get package directory for resolving config paths
16
16
  const __filename = fileURLToPath(import.meta.url);
17
17
  const __dirname = dirname(__filename);
@@ -102,8 +102,8 @@ const bot = new TelegramBot(BOT_TOKEN, { polling: true });
102
102
  const storage = new Storage(join(DATA_DIR, 'versions.json'));
103
103
  const notifier = new Notifier(bot, validatedChatId);
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
+ const assetMirror = new AssetMirror();
106
+ const checker = new Checker(configData.tools, storage, notifier, assetMirror, downloadsConfig);
107
107
  // Track scheduled task for rescheduling
108
108
  let scheduledTask = null;
109
109
  let lastCheckTime = null;
@@ -275,19 +275,45 @@ bot.onText(/\/publishcli/, async (msg) => {
275
275
  await bot.sendMessage(validatedChatId, `❌ CLI publish failed: ${result.error}`);
276
276
  }
277
277
  });
278
- bot.onText(/\/mirrorvsix(?:\s+(.+))?/, async (msg, match) => {
278
+ bot.onText(/\/mirror(?:\s+(.+))?/, async (msg, match) => {
279
279
  if (msg.chat.id.toString() !== validatedChatId)
280
280
  return;
281
- const versionArg = match?.[1]?.trim();
282
- const version = versionArg || storage.getVersion('Claude Code VSCode');
281
+ const args = match?.[1]?.trim();
282
+ if (!args) {
283
+ await bot.sendMessage(validatedChatId, 'Usage: /mirror <toolname> [version]\nExample: /mirror VSCode\nExample: /mirror "Claude Code VSCode" 2.1.9');
284
+ return;
285
+ }
286
+ // Parse tool name and optional version
287
+ let toolName;
288
+ let version;
289
+ const quoteMatch = args.match(/^"([^"]+)"(?:\s+(.+))?$/);
290
+ if (quoteMatch) {
291
+ toolName = quoteMatch[1];
292
+ version = quoteMatch[2]?.trim() || null;
293
+ }
294
+ else {
295
+ const parts = args.split(/\s+/);
296
+ toolName = parts[0];
297
+ version = parts[1] || null;
298
+ }
299
+ // Get download config for this tool
300
+ const downloadConfig = downloadsConfig[toolName];
301
+ if (!downloadConfig || downloadConfig.type === 'npm' || !('mirror' in downloadConfig) || !downloadConfig.mirror) {
302
+ await bot.sendMessage(validatedChatId, `Tool "${toolName}" is not configured for mirroring.`);
303
+ return;
304
+ }
305
+ // Get version if not provided
306
+ if (!version) {
307
+ version = storage.getVersion(toolName);
308
+ }
283
309
  if (!version) {
284
- await bot.sendMessage(validatedChatId, 'No version specified and no tracked version found. Usage: /mirrorvsix [version]');
310
+ await bot.sendMessage(validatedChatId, `No version specified and no tracked version found for "${toolName}".`);
285
311
  return;
286
312
  }
287
- await bot.sendMessage(validatedChatId, `Mirroring Claude Code VSCode v${version}...`);
288
- const result = await vsixMirror.mirror(version);
313
+ await bot.sendMessage(validatedChatId, `Mirroring ${toolName} v${version}...`);
314
+ const result = await assetMirror.mirror(toolName, version, downloadConfig.mirror, downloadConfig.filename);
289
315
  if (result.success) {
290
- storage.setMirrorUrl('Claude Code VSCode', result.downloadUrl);
316
+ storage.setMirrorUrl(toolName, result.downloadUrl);
291
317
  await bot.sendMessage(validatedChatId, `✅ Mirrored successfully\nURL: ${result.downloadUrl}`);
292
318
  }
293
319
  else {
package/dist/types.d.ts CHANGED
@@ -12,11 +12,17 @@ export interface Config {
12
12
  checkIntervalHours: number;
13
13
  tools: ToolConfig[];
14
14
  }
15
+ export interface MirrorConfig {
16
+ sourceUrl: string;
17
+ extensionId?: string;
18
+ targetPlatform?: string;
19
+ }
15
20
  export interface DownloadConfigUrl {
16
21
  type?: 'download';
17
22
  displayName: string;
18
23
  downloadUrl: string;
19
24
  filename: string;
25
+ mirror?: MirrorConfig;
20
26
  }
21
27
  export interface DownloadConfigNpm {
22
28
  type: 'npm';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvnt/release-radar",
3
- "version": "1.7.15",
3
+ "version": "1.8.2",
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
- }
@@ -1,96 +0,0 @@
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
- });