@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.
- package/bin/release-radar-updater.js +0 -0
- package/bin/release-radar.js +0 -0
- package/config/downloads.json +113 -1
- package/config/tools.json +55 -0
- package/dist/asset-mirror.d.ts +17 -0
- package/dist/asset-mirror.js +121 -0
- package/dist/asset-mirror.test.js +159 -0
- package/dist/checker.d.ts +6 -4
- package/dist/checker.js +21 -10
- package/dist/checker.test.js +45 -36
- package/dist/index.js +36 -10
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
- package/dist/vsix-mirror.d.ts +0 -15
- package/dist/vsix-mirror.js +0 -96
- package/dist/vsix-mirror.test.js +0 -96
- /package/dist/{vsix-mirror.test.d.ts → asset-mirror.test.d.ts} +0 -0
|
File without changes
|
package/bin/release-radar.js
CHANGED
|
File without changes
|
package/config/downloads.json
CHANGED
|
@@ -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 {
|
|
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
|
|
10
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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.
|
|
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
|
|
28
|
-
|
|
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
|
}
|
package/dist/checker.test.js
CHANGED
|
@@ -78,52 +78,61 @@ describe('Checker', () => {
|
|
|
78
78
|
expect(result.hasUpdates).toBe(false);
|
|
79
79
|
expect(result.updateCount).toBe(0);
|
|
80
80
|
});
|
|
81
|
-
describe('
|
|
82
|
-
let
|
|
81
|
+
describe('Asset mirroring', () => {
|
|
82
|
+
let mockAssetMirror;
|
|
83
|
+
let downloadsConfig;
|
|
83
84
|
beforeEach(() => {
|
|
84
|
-
|
|
85
|
-
mirror: vi.fn().mockResolvedValue({ success: true, downloadUrl: 'github.com/test/url
|
|
85
|
+
mockAssetMirror = {
|
|
86
|
+
mirror: vi.fn().mockResolvedValue({ success: true, downloadUrl: 'github.com/test/url' })
|
|
86
87
|
};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
126
|
+
expect(mockAssetMirror.mirror).not.toHaveBeenCalled();
|
|
112
127
|
});
|
|
113
128
|
it('continues if mirror fails', async () => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
106
|
-
const checker = new Checker(configData.tools, storage, notifier,
|
|
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(/\/
|
|
278
|
+
bot.onText(/\/mirror(?:\s+(.+))?/, async (msg, match) => {
|
|
279
279
|
if (msg.chat.id.toString() !== validatedChatId)
|
|
280
280
|
return;
|
|
281
|
-
const
|
|
282
|
-
|
|
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,
|
|
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
|
|
288
|
-
const result = await
|
|
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(
|
|
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
package/dist/vsix-mirror.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/vsix-mirror.js
DELETED
|
@@ -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
|
-
}
|
package/dist/vsix-mirror.test.js
DELETED
|
@@ -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
|
-
});
|
|
File without changes
|