@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 CHANGED
@@ -113,10 +113,12 @@ TELEGRAM_CHAT_ID=your_chat_id_here
113
113
 
114
114
  ### Tools Configuration
115
115
 
116
- Edit `config/tools.json` to add/remove tools:
116
+ Edit `config/tools.json` to add/remove tools and configure scheduling:
117
117
 
118
118
  ```json
119
119
  {
120
+ "scheduleMode": "daily",
121
+ "dailyCheckTime": "06:00",
120
122
  "checkIntervalHours": 6,
121
123
  "tools": [
122
124
  {
@@ -128,6 +130,14 @@ Edit `config/tools.json` to add/remove tools:
128
130
  }
129
131
  ```
130
132
 
133
+ #### Schedule Options
134
+
135
+ | Field | Values | Description |
136
+ |-------|--------|-------------|
137
+ | `scheduleMode` | `"daily"` or `"interval"` | Check once per day or every N hours |
138
+ | `dailyCheckTime` | `"HH:MM"` | Time for daily check (24-hour format, e.g., `"06:00"`) |
139
+ | `checkIntervalHours` | `1-24` | Hours between checks (interval mode) |
140
+
131
141
  #### Tool Types
132
142
 
133
143
  | Type | Required Fields | Description |
@@ -169,8 +179,11 @@ Placeholders:
169
179
  |---------|-------------|
170
180
  | `/check` | Manually trigger version check (auto-publishes CLI if updates found) |
171
181
  | `/status` | Show all tracked versions + last/next check times |
172
- | `/interval` | Show current check interval |
173
- | `/setinterval <hours>` | Set check interval (1-24 hours) |
182
+ | `/schedule` | Show current schedule mode and next check time |
183
+ | `/settime <HH:MM>` | Set daily check time (e.g., `/settime 06:00`) and switch to daily mode |
184
+ | `/setmode <daily\|interval>` | Switch between daily and interval modes |
185
+ | `/interval` | Show current check interval (interval mode) |
186
+ | `/setinterval <hours>` | Set check interval (1-24 hours) and switch to interval mode |
174
187
  | `/generate` | Generate versions.json file locally |
175
188
  | `/clipreview` | Preview tools/versions that will be included in CLI |
176
189
  | `/publishcli` | Manually publish CLI with current tracked versions |
File without changes
File without changes
package/cli/src/ui.ts CHANGED
@@ -132,21 +132,28 @@ export interface TableRow {
132
132
  downloadedVersion: string;
133
133
  status: 'new' | 'update' | 'current';
134
134
  type: 'npm' | 'download';
135
+ category: 'tool' | 'vscode-extension';
135
136
  }
136
137
 
137
- export function renderTable(rows: TableRow[]): void {
138
- // Calculate dynamic column widths
139
- const colWidths = {
138
+ interface ColWidths {
139
+ tool: number;
140
+ latest: number;
141
+ downloaded: number;
142
+ status: number;
143
+ type: number;
144
+ }
145
+
146
+ function calculateColWidths(rows: TableRow[]): ColWidths {
147
+ return {
140
148
  tool: Math.max(4, ...rows.map(r => r.displayName.length)) + 2,
141
149
  latest: Math.max(6, ...rows.map(r => r.version.length)) + 2,
142
150
  downloaded: Math.max(10, ...rows.map(r => r.downloadedVersion.length)) + 2,
143
151
  status: 8,
144
152
  type: 4,
145
153
  };
154
+ }
146
155
 
147
- const totalWidth = colWidths.tool + colWidths.latest + colWidths.downloaded + colWidths.status + colWidths.type + 2;
148
-
149
- // Header
156
+ function renderTableHeader(colWidths: ColWidths): void {
150
157
  console.log(chalk.bold(
151
158
  ' ' +
152
159
  'Tool'.padEnd(colWidths.tool) +
@@ -155,40 +162,76 @@ export function renderTable(rows: TableRow[]): void {
155
162
  'Status'.padEnd(colWidths.status) +
156
163
  'Type'
157
164
  ));
165
+ const totalWidth = colWidths.tool + colWidths.latest + colWidths.downloaded + colWidths.status + colWidths.type + 2;
166
+ console.log(chalk.gray('─'.repeat(totalWidth)));
167
+ }
168
+
169
+ function renderTableRow(row: TableRow, colWidths: ColWidths): void {
170
+ let statusText: string;
171
+ let statusColored: string;
172
+ switch (row.status) {
173
+ case 'new':
174
+ statusText = 'NEW'.padEnd(colWidths.status);
175
+ statusColored = chalk.blue(statusText);
176
+ break;
177
+ case 'update':
178
+ statusText = 'UPDATE'.padEnd(colWidths.status);
179
+ statusColored = chalk.yellow(statusText);
180
+ break;
181
+ case 'current':
182
+ statusText = '✓'.padEnd(colWidths.status);
183
+ statusColored = chalk.green(statusText);
184
+ break;
185
+ }
186
+ const typeStr = row.type === 'npm' ? chalk.magenta('npm') : chalk.cyan('wget');
187
+
188
+ console.log(
189
+ ' ' +
190
+ row.displayName.padEnd(colWidths.tool) +
191
+ row.version.padEnd(colWidths.latest) +
192
+ row.downloadedVersion.padEnd(colWidths.downloaded) +
193
+ statusColored +
194
+ typeStr
195
+ );
196
+ }
197
+
198
+ function renderGroupHeader(title: string, colWidths: ColWidths): void {
199
+ const totalWidth = colWidths.tool + colWidths.latest + colWidths.downloaded + colWidths.status + colWidths.type + 2;
200
+ console.log('');
201
+ console.log(chalk.bold.underline(` ${title}`));
158
202
  console.log(chalk.gray('─'.repeat(totalWidth)));
203
+ }
204
+
205
+ export function renderTable(rows: TableRow[]): void {
206
+ const colWidths = calculateColWidths(rows);
159
207
 
160
- // Rows
161
- for (const row of rows) {
162
- // Pad plain text BEFORE applying colors (chalk adds invisible ANSI codes)
163
- let statusText: string;
164
- let statusColored: string;
165
- switch (row.status) {
166
- case 'new':
167
- statusText = 'NEW'.padEnd(colWidths.status);
168
- statusColored = chalk.blue(statusText);
169
- break;
170
- case 'update':
171
- statusText = 'UPDATE'.padEnd(colWidths.status);
172
- statusColored = chalk.yellow(statusText);
173
- break;
174
- case 'current':
175
- statusText = '✓'.padEnd(colWidths.status);
176
- statusColored = chalk.green(statusText);
177
- break;
208
+ // Group rows by category
209
+ const tools = rows.filter(r => r.category === 'tool');
210
+ const extensions = rows.filter(r => r.category === 'vscode-extension');
211
+
212
+ // Render tools first
213
+ if (tools.length > 0) {
214
+ renderTableHeader(colWidths);
215
+ for (const row of tools) {
216
+ renderTableRow(row, colWidths);
217
+ }
218
+ }
219
+
220
+ // Render VSCode extensions as a separate group
221
+ if (extensions.length > 0) {
222
+ renderGroupHeader('VSCode Extensions', colWidths);
223
+ for (const row of extensions) {
224
+ renderTableRow(row, colWidths);
178
225
  }
179
- const typeStr = row.type === 'npm' ? chalk.magenta('npm') : chalk.cyan('wget');
180
-
181
- console.log(
182
- ' ' +
183
- row.displayName.padEnd(colWidths.tool) +
184
- row.version.padEnd(colWidths.latest) +
185
- row.downloadedVersion.padEnd(colWidths.downloaded) +
186
- statusColored +
187
- typeStr
188
- );
189
226
  }
190
227
  }
191
228
 
229
+ function isVscodeExtension(tool: VersionsJsonTool): boolean {
230
+ if (isNpmTool(tool)) return false;
231
+ // Check if filename ends with .vsix
232
+ return tool.filename.toLowerCase().endsWith('.vsix');
233
+ }
234
+
192
235
  export async function promptToolSelection(
193
236
  tools: VersionsJsonTool[],
194
237
  downloaded: DownloadedState,
@@ -199,24 +242,30 @@ export async function promptToolSelection(
199
242
  console.log(chalk.bold(`\nrelease-radar-cli`));
200
243
  console.log(chalk.gray(`Last updated: ${new Date(generatedAt).toLocaleString()}\n`));
201
244
 
202
- // Convert to table rows and display
203
- const rows: TableRow[] = choices.map(choice => ({
204
- displayName: choice.displayName,
205
- version: choice.version,
206
- downloadedVersion: choice.downloadedVersion ?? '-',
207
- status: choice.status,
208
- type: choice.type === 'npm' ? 'npm' : 'download',
245
+ // Convert to table rows with category and display
246
+ const rows: TableRow[] = tools.map((tool, i) => ({
247
+ displayName: choices[i].displayName,
248
+ version: choices[i].version,
249
+ downloadedVersion: choices[i].downloadedVersion ?? '-',
250
+ status: choices[i].status,
251
+ type: choices[i].type === 'npm' ? 'npm' : 'download',
252
+ category: isVscodeExtension(tool) ? 'vscode-extension' : 'tool',
209
253
  }));
210
254
 
211
255
  renderTable(rows);
212
256
  console.log('');
213
257
 
258
+ // Sort choices to match displayed order (tools first, then extensions)
259
+ const toolChoices = choices.filter((_, i) => !isVscodeExtension(tools[i]));
260
+ const extensionChoices = choices.filter((_, i) => isVscodeExtension(tools[i]));
261
+ const sortedChoices = [...toolChoices, ...extensionChoices];
262
+
214
263
  const { selected } = await inquirer.prompt([
215
264
  {
216
265
  type: 'checkbox',
217
266
  name: 'selected',
218
267
  message: 'Select tools to download:',
219
- choices: choices.map((choice) => ({
268
+ choices: sortedChoices.map((choice) => ({
220
269
  name: `${choice.displayName} ${choice.version}`,
221
270
  value: choice,
222
271
  checked: choice.status !== 'current',
@@ -12,7 +12,9 @@
12
12
  "downloadUrl": "{{MIRROR_URL}}",
13
13
  "filename": "claude-code-{{VERSION}}-win32-x64.vsix",
14
14
  "mirror": {
15
- "sourceUrl": "marketplace-api"
15
+ "sourceUrl": "marketplace-api",
16
+ "extensionId": "anthropic.claude-code",
17
+ "targetPlatform": "win32-x64"
16
18
  }
17
19
  },
18
20
  "Claude Code CLI": {
@@ -89,5 +91,104 @@
89
91
  "displayName": "PowerShell",
90
92
  "downloadUrl": "github.com/PowerShell/PowerShell/releases/download/v{{VERSION}}/PowerShell-{{VERSION}}-win-x64.msi",
91
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
+ }
92
193
  }
93
194
  }
package/config/tools.json CHANGED
@@ -1,5 +1,7 @@
1
1
  {
2
- "checkIntervalHours": 1,
2
+ "checkIntervalHours": 6,
3
+ "scheduleMode": "daily",
4
+ "dailyCheckTime": "06:00",
3
5
  "tools": [
4
6
  {
5
7
  "name": "VSCode",
@@ -85,6 +87,61 @@
85
87
  "name": "PowerShell",
86
88
  "type": "github",
87
89
  "repo": "PowerShell/PowerShell"
90
+ },
91
+ {
92
+ "name": "C/C++ Extension Pack",
93
+ "type": "vscode-marketplace",
94
+ "extensionId": "ms-vscode.cpptools-extension-pack"
95
+ },
96
+ {
97
+ "name": "C/C++ Themes",
98
+ "type": "vscode-marketplace",
99
+ "extensionId": "ms-vscode.cpptools-themes"
100
+ },
101
+ {
102
+ "name": "YAML",
103
+ "type": "vscode-marketplace",
104
+ "extensionId": "redhat.vscode-yaml"
105
+ },
106
+ {
107
+ "name": "Python",
108
+ "type": "vscode-marketplace",
109
+ "extensionId": "ms-python.python"
110
+ },
111
+ {
112
+ "name": "Python Debugger",
113
+ "type": "vscode-marketplace",
114
+ "extensionId": "ms-python.debugpy"
115
+ },
116
+ {
117
+ "name": "Python Environments",
118
+ "type": "vscode-marketplace",
119
+ "extensionId": "ms-python.vscode-python-envs"
120
+ },
121
+ {
122
+ "name": "Pylance",
123
+ "type": "vscode-marketplace",
124
+ "extensionId": "ms-python.vscode-pylance"
125
+ },
126
+ {
127
+ "name": "isort",
128
+ "type": "vscode-marketplace",
129
+ "extensionId": "ms-python.isort"
130
+ },
131
+ {
132
+ "name": "Go",
133
+ "type": "vscode-marketplace",
134
+ "extensionId": "golang.go"
135
+ },
136
+ {
137
+ "name": "GitHub Theme",
138
+ "type": "vscode-marketplace",
139
+ "extensionId": "github.github-vscode-theme"
140
+ },
141
+ {
142
+ "name": "Material Icon Theme",
143
+ "type": "vscode-marketplace",
144
+ "extensionId": "pkief.material-icon-theme"
88
145
  }
89
146
  ]
90
147
  }
@@ -4,14 +4,34 @@ export interface MirrorResult {
4
4
  downloadUrl?: string;
5
5
  error?: string;
6
6
  }
7
+ export interface MirrorItem {
8
+ toolName: string;
9
+ version: string;
10
+ config: MirrorConfig;
11
+ filenameTemplate: string;
12
+ }
13
+ export interface BatchMirrorResult {
14
+ tag: string;
15
+ results: Map<string, MirrorResult>;
16
+ }
7
17
  export declare class AssetMirror {
8
18
  private repo;
19
+ /**
20
+ * Mirror a single tool (legacy method, still used for /mirror command)
21
+ */
9
22
  mirror(toolName: string, version: string, config: MirrorConfig, filenameTemplate: string): Promise<MirrorResult>;
23
+ /**
24
+ * Mirror multiple tools into a single batch release
25
+ */
26
+ mirrorBatch(items: MirrorItem[]): Promise<BatchMirrorResult>;
10
27
  buildTag(toolName: string, version: string): string;
28
+ buildBatchTag(): string;
29
+ private findExistingAsset;
11
30
  releaseExists(tag: string): Promise<boolean>;
12
31
  private applyVersion;
13
32
  private getSourceUrl;
14
33
  private getMarketplaceVsixUrl;
15
34
  private downloadFile;
16
35
  private createRelease;
36
+ private createBatchRelease;
17
37
  }
@@ -5,6 +5,9 @@ import { tmpdir } from 'os';
5
5
  import { join } from 'path';
6
6
  export class AssetMirror {
7
7
  repo = 'lvntbkdmr/apps';
8
+ /**
9
+ * Mirror a single tool (legacy method, still used for /mirror command)
10
+ */
8
11
  async mirror(toolName, version, config, filenameTemplate) {
9
12
  const tag = this.buildTag(toolName, version);
10
13
  const filename = this.applyVersion(filenameTemplate, version);
@@ -38,10 +41,86 @@ export class AssetMirror {
38
41
  return { success: false, error: message };
39
42
  }
40
43
  }
44
+ /**
45
+ * Mirror multiple tools into a single batch release
46
+ */
47
+ async mirrorBatch(items) {
48
+ const tag = this.buildBatchTag();
49
+ const results = new Map();
50
+ const downloadedFiles = [];
51
+ console.log(`[AssetMirror] Starting batch mirror for ${items.length} items, tag: ${tag}`);
52
+ // Download all files first
53
+ for (const item of items) {
54
+ const filename = this.applyVersion(item.filenameTemplate, item.version);
55
+ const downloadUrl = `github.com/${this.repo}/releases/download/${tag}/${filename}`;
56
+ try {
57
+ // Check if this specific file already exists in any release
58
+ const existingUrl = await this.findExistingAsset(item.toolName, item.version, filename);
59
+ if (existingUrl) {
60
+ console.log(`[AssetMirror] ${item.toolName} v${item.version} already mirrored, skipping`);
61
+ results.set(item.toolName, { success: true, downloadUrl: existingUrl });
62
+ continue;
63
+ }
64
+ console.log(`[AssetMirror] Getting source URL for ${item.toolName} v${item.version}...`);
65
+ const sourceUrl = await this.getSourceUrl(item.config, item.version);
66
+ const tempPath = join(tmpdir(), filename);
67
+ console.log(`[AssetMirror] Downloading to ${tempPath}...`);
68
+ await this.downloadFile(sourceUrl, tempPath);
69
+ downloadedFiles.push({ path: tempPath, filename });
70
+ results.set(item.toolName, { success: true, downloadUrl });
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ console.error(`[AssetMirror] Failed to download ${item.toolName}: ${message}`);
75
+ results.set(item.toolName, { success: false, error: message });
76
+ }
77
+ }
78
+ // Create single release with all downloaded files
79
+ if (downloadedFiles.length > 0) {
80
+ try {
81
+ console.log(`[AssetMirror] Creating batch release ${tag} with ${downloadedFiles.length} assets...`);
82
+ await this.createBatchRelease(tag, downloadedFiles, items);
83
+ console.log(`[AssetMirror] Batch release created successfully`);
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ console.error(`[AssetMirror] Failed to create batch release: ${message}`);
88
+ // Mark all as failed if release creation fails
89
+ for (const file of downloadedFiles) {
90
+ const toolName = items.find(i => this.applyVersion(i.filenameTemplate, i.version) === file.filename)?.toolName;
91
+ if (toolName) {
92
+ results.set(toolName, { success: false, error: message });
93
+ }
94
+ }
95
+ }
96
+ // Cleanup temp files
97
+ for (const file of downloadedFiles) {
98
+ if (existsSync(file.path)) {
99
+ unlinkSync(file.path);
100
+ }
101
+ }
102
+ }
103
+ return { tag, results };
104
+ }
41
105
  buildTag(toolName, version) {
42
106
  const kebab = toolName.toLowerCase().replace(/\s+/g, '-');
43
107
  return `${kebab}-v${version}`;
44
108
  }
109
+ buildBatchTag() {
110
+ const now = new Date();
111
+ const date = now.toISOString().split('T')[0]; // YYYY-MM-DD
112
+ const time = now.toTimeString().split(' ')[0].replace(/:/g, ''); // HHMMSS
113
+ return `batch-${date}-${time}`;
114
+ }
115
+ async findExistingAsset(toolName, version, filename) {
116
+ // Check legacy per-tool release first
117
+ const legacyTag = this.buildTag(toolName, version);
118
+ if (await this.releaseExists(legacyTag)) {
119
+ return `github.com/${this.repo}/releases/download/${legacyTag}/${filename}`;
120
+ }
121
+ // Could also search batch releases, but for simplicity we skip if not in legacy
122
+ return null;
123
+ }
45
124
  async releaseExists(tag) {
46
125
  try {
47
126
  execSync(`gh release view ${tag} --repo ${this.repo}`, {
@@ -59,14 +138,15 @@ export class AssetMirror {
59
138
  }
60
139
  async getSourceUrl(config, version) {
61
140
  if (config.sourceUrl === 'marketplace-api') {
62
- return this.getMarketplaceVsixUrl(version);
141
+ if (!config.extensionId) {
142
+ throw new Error('extensionId is required when sourceUrl is "marketplace-api"');
143
+ }
144
+ return this.getMarketplaceVsixUrl(config.extensionId, version, config.targetPlatform);
63
145
  }
64
146
  // For direct URLs, just return as-is (curl -L will follow redirects)
65
147
  return config.sourceUrl;
66
148
  }
67
- async getMarketplaceVsixUrl(version) {
68
- const extensionId = 'anthropic.claude-code';
69
- const targetPlatform = 'win32-x64';
149
+ async getMarketplaceVsixUrl(extensionId, version, targetPlatform) {
70
150
  const query = JSON.stringify({
71
151
  filters: [{
72
152
  criteria: [{ filterType: 7, value: extensionId }],
@@ -82,13 +162,23 @@ export class AssetMirror {
82
162
  const response = execSync(cmd, { encoding: 'utf-8', timeout: 30000 });
83
163
  const data = JSON.parse(response);
84
164
  const versions = data.results?.[0]?.extensions?.[0]?.versions || [];
85
- const targetVersion = versions.find((v) => v.version === version && v.targetPlatform === targetPlatform);
165
+ // Find matching version - with or without platform filter
166
+ const targetVersion = versions.find((v) => {
167
+ if (v.version !== version)
168
+ return false;
169
+ if (targetPlatform) {
170
+ return v.targetPlatform === targetPlatform;
171
+ }
172
+ // For universal extensions, targetPlatform is undefined/null
173
+ return !v.targetPlatform;
174
+ });
86
175
  if (!targetVersion) {
87
- throw new Error(`Version ${version} for ${targetPlatform} not found in marketplace`);
176
+ const platformInfo = targetPlatform ? ` for ${targetPlatform}` : ' (universal)';
177
+ throw new Error(`Version ${version}${platformInfo} not found in marketplace for ${extensionId}`);
88
178
  }
89
179
  const vsixFile = targetVersion.files?.find((f) => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage');
90
180
  if (!vsixFile?.source) {
91
- throw new Error('VSIX download URL not found in marketplace response');
181
+ throw new Error(`VSIX download URL not found in marketplace response for ${extensionId}`);
92
182
  }
93
183
  return vsixFile.source;
94
184
  }
@@ -107,4 +197,23 @@ export class AssetMirror {
107
197
  execSync(`gh release create "${tag}" "${filePath}#${filename}" ` +
108
198
  `--repo ${this.repo} --title "${title}" --notes "${notes}"`, { encoding: 'utf-8', timeout: 300000 });
109
199
  }
200
+ async createBatchRelease(tag, files, items) {
201
+ const date = new Date().toLocaleDateString('en-US', {
202
+ year: 'numeric',
203
+ month: 'long',
204
+ day: 'numeric',
205
+ });
206
+ const title = `Updates ${date}`;
207
+ // Build release notes listing all tools
208
+ const toolsList = items
209
+ .filter(item => files.some(f => f.filename === this.applyVersion(item.filenameTemplate, item.version)))
210
+ .map(item => `- ${item.toolName} ${item.version}`)
211
+ .join('\n');
212
+ const notes = `Mirrored for Nexus proxy access.\n\n**Included:**\n${toolsList}`;
213
+ // Build file arguments for gh release create
214
+ const fileArgs = files.map(f => `"${f.path}#${f.filename}"`).join(' ');
215
+ execSync(`gh release create "${tag}" ${fileArgs} ` +
216
+ `--repo ${this.repo} --title "${title}" --notes "${notes}"`, { encoding: 'utf-8', timeout: 600000 } // 10 minutes for multiple uploads
217
+ );
218
+ }
110
219
  }