@lvnt/release-radar 1.8.2 → 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/cli/src/ui.ts +91 -42
- package/config/tools.json +3 -1
- package/dist/asset-mirror.d.ts +20 -0
- package/dist/asset-mirror.js +98 -0
- 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.d.ts +1 -0
- package/dist/mirror-and-publish.js +85 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
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
|
-
| `/
|
|
173
|
-
| `/
|
|
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 |
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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[] =
|
|
204
|
-
displayName:
|
|
205
|
-
version:
|
|
206
|
-
downloadedVersion:
|
|
207
|
-
status:
|
|
208
|
-
type:
|
|
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:
|
|
268
|
+
choices: sortedChoices.map((choice) => ({
|
|
220
269
|
name: `${choice.displayName} ${choice.version}`,
|
|
221
270
|
value: choice,
|
|
222
271
|
checked: choice.status !== 'current',
|
package/config/tools.json
CHANGED
package/dist/asset-mirror.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/asset-mirror.js
CHANGED
|
@@ -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}`, {
|
|
@@ -118,4 +197,23 @@ export class AssetMirror {
|
|
|
118
197
|
execSync(`gh release create "${tag}" "${filePath}#${filename}" ` +
|
|
119
198
|
`--repo ${this.repo} --title "${title}" --notes "${notes}"`, { encoding: 'utf-8', timeout: 300000 });
|
|
120
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
|
+
}
|
|
121
219
|
}
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,8 +8,11 @@ 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 {
|