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