@lvnt/release-radar-cli 0.1.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
@@ -0,0 +1,11 @@
1
+ export interface CliConfig {
2
+ nexusUrl: string;
3
+ downloadDir: string;
4
+ }
5
+ export declare class ConfigManager {
6
+ private configPath;
7
+ constructor(baseDir?: string);
8
+ isConfigured(): boolean;
9
+ load(): CliConfig;
10
+ save(config: CliConfig): void;
11
+ }
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ export class ConfigManager {
5
+ configPath;
6
+ constructor(baseDir) {
7
+ const dir = baseDir ?? join(homedir(), '.release-radar-cli');
8
+ if (!existsSync(dir)) {
9
+ mkdirSync(dir, { recursive: true });
10
+ }
11
+ this.configPath = join(dir, 'config.json');
12
+ }
13
+ isConfigured() {
14
+ return existsSync(this.configPath);
15
+ }
16
+ load() {
17
+ const content = readFileSync(this.configPath, 'utf-8');
18
+ return JSON.parse(content);
19
+ }
20
+ save(config) {
21
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { ConfigManager } from './config.js';
3
+ import { mkdtempSync, rmSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ describe('ConfigManager', () => {
7
+ let tempDir;
8
+ let configManager;
9
+ beforeEach(() => {
10
+ tempDir = mkdtempSync(join(tmpdir(), 'cli-test-'));
11
+ configManager = new ConfigManager(tempDir);
12
+ });
13
+ afterEach(() => {
14
+ rmSync(tempDir, { recursive: true });
15
+ });
16
+ it('returns false for isConfigured when no config exists', () => {
17
+ expect(configManager.isConfigured()).toBe(false);
18
+ });
19
+ it('saves and loads config correctly', () => {
20
+ configManager.save({
21
+ nexusUrl: 'http://nexus.local',
22
+ downloadDir: '/downloads',
23
+ });
24
+ expect(configManager.isConfigured()).toBe(true);
25
+ const loaded = configManager.load();
26
+ expect(loaded.nexusUrl).toBe('http://nexus.local');
27
+ expect(loaded.downloadDir).toBe('/downloads');
28
+ });
29
+ it('creates config directory if it does not exist', () => {
30
+ const configPath = join(tempDir, 'config.json');
31
+ expect(existsSync(configPath)).toBe(false);
32
+ configManager.save({
33
+ nexusUrl: 'http://nexus.local',
34
+ downloadDir: '/downloads',
35
+ });
36
+ expect(existsSync(configPath)).toBe(true);
37
+ });
38
+ });
@@ -0,0 +1,7 @@
1
+ export declare function replaceNexusUrl(url: string, nexusUrl: string): string;
2
+ export declare function buildWgetCommand(url: string, outputPath: string): string;
3
+ export interface DownloadResult {
4
+ success: boolean;
5
+ error?: string;
6
+ }
7
+ export declare function downloadFile(url: string, downloadDir: string, filename: string, nexusUrl: string): DownloadResult;
@@ -0,0 +1,25 @@
1
+ import { execSync } from 'child_process';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ export function replaceNexusUrl(url, nexusUrl) {
5
+ return url.replace('{{NEXUS_URL}}', nexusUrl);
6
+ }
7
+ export function buildWgetCommand(url, outputPath) {
8
+ return `wget -O "${outputPath}" "${url}"`;
9
+ }
10
+ export function downloadFile(url, downloadDir, filename, nexusUrl) {
11
+ const resolvedUrl = replaceNexusUrl(url, nexusUrl);
12
+ if (!existsSync(downloadDir)) {
13
+ mkdirSync(downloadDir, { recursive: true });
14
+ }
15
+ const outputPath = join(downloadDir, filename);
16
+ const command = buildWgetCommand(resolvedUrl, outputPath);
17
+ try {
18
+ execSync(command, { stdio: 'inherit' });
19
+ return { success: true };
20
+ }
21
+ catch (error) {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ return { success: false, error: message };
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildWgetCommand, replaceNexusUrl } from './downloader.js';
3
+ describe('downloader', () => {
4
+ describe('replaceNexusUrl', () => {
5
+ it('replaces {{NEXUS_URL}} placeholder', () => {
6
+ const url = '{{NEXUS_URL}}/github.com/ninja/v1.0/ninja.zip';
7
+ const result = replaceNexusUrl(url, 'http://nexus.local');
8
+ expect(result).toBe('http://nexus.local/github.com/ninja/v1.0/ninja.zip');
9
+ });
10
+ });
11
+ describe('buildWgetCommand', () => {
12
+ it('builds correct wget command', () => {
13
+ const cmd = buildWgetCommand('http://nexus.local/file.zip', '/downloads/file.zip');
14
+ expect(cmd).toBe('wget -O "/downloads/file.zip" "http://nexus.local/file.zip"');
15
+ });
16
+ });
17
+ });
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,89 @@
1
+ import chalk from 'chalk';
2
+ import { ConfigManager } from './config.js';
3
+ import { DownloadTracker } from './tracker.js';
4
+ import { loadVersions } from './versions.js';
5
+ import { checkAndUpdate } from './updater.js';
6
+ import { downloadFile } from './downloader.js';
7
+ import { promptSetup, promptToolSelection } from './ui.js';
8
+ async function showStatus() {
9
+ const tracker = new DownloadTracker();
10
+ const versions = loadVersions();
11
+ const downloaded = tracker.getAll();
12
+ console.log(chalk.bold('\nTool Status:\n'));
13
+ for (const tool of versions.tools) {
14
+ const record = downloaded[tool.name];
15
+ const downloadedVersion = record?.version ?? '-';
16
+ let status;
17
+ if (!record) {
18
+ status = chalk.blue('NEW');
19
+ }
20
+ else if (record.version !== tool.version) {
21
+ status = chalk.yellow('UPDATE');
22
+ }
23
+ else {
24
+ status = chalk.green('✓');
25
+ }
26
+ console.log(` ${tool.displayName.padEnd(20)} ${tool.version.padEnd(12)} ${downloadedVersion.padEnd(12)} ${status}`);
27
+ }
28
+ console.log('');
29
+ }
30
+ async function runConfig() {
31
+ const configManager = new ConfigManager();
32
+ const config = await promptSetup();
33
+ configManager.save(config);
34
+ console.log(chalk.green('\nConfiguration saved!'));
35
+ }
36
+ async function runInteractive() {
37
+ const configManager = new ConfigManager();
38
+ const tracker = new DownloadTracker();
39
+ // First run setup
40
+ if (!configManager.isConfigured()) {
41
+ const config = await promptSetup();
42
+ configManager.save(config);
43
+ console.log(chalk.green('\nConfiguration saved!\n'));
44
+ }
45
+ // Check for updates and restart if needed
46
+ await checkAndUpdate();
47
+ // Load data
48
+ const config = configManager.load();
49
+ const versions = loadVersions();
50
+ const downloaded = tracker.getAll();
51
+ // Show interactive menu
52
+ const selected = await promptToolSelection(versions.tools, downloaded, versions.generatedAt);
53
+ if (selected.length === 0) {
54
+ console.log(chalk.gray('\nNo tools selected. Exiting.'));
55
+ return;
56
+ }
57
+ // Download selected tools
58
+ console.log('');
59
+ for (const tool of selected) {
60
+ console.log(chalk.bold(`Downloading ${tool.displayName} ${tool.version}...`));
61
+ const result = downloadFile(tool.downloadUrl, config.downloadDir, tool.filename, config.nexusUrl);
62
+ if (result.success) {
63
+ tracker.recordDownload(tool.name, tool.version, tool.filename);
64
+ console.log(chalk.green(` Saved to ${config.downloadDir}/${tool.filename} ✓\n`));
65
+ }
66
+ else {
67
+ console.log(chalk.red(` Failed: ${result.error}\n`));
68
+ }
69
+ }
70
+ console.log(chalk.green('Done!'));
71
+ }
72
+ async function main() {
73
+ const command = process.argv[2];
74
+ switch (command) {
75
+ case 'status':
76
+ await showStatus();
77
+ break;
78
+ case 'config':
79
+ await runConfig();
80
+ break;
81
+ default:
82
+ await runInteractive();
83
+ break;
84
+ }
85
+ }
86
+ main().catch((error) => {
87
+ console.error(chalk.red('Error:'), error.message);
88
+ process.exit(1);
89
+ });
@@ -0,0 +1,15 @@
1
+ export interface DownloadRecord {
2
+ version: string;
3
+ downloadedAt: string;
4
+ filename: string;
5
+ }
6
+ export interface DownloadedState {
7
+ [toolName: string]: DownloadRecord;
8
+ }
9
+ export declare class DownloadTracker {
10
+ private filePath;
11
+ constructor(baseDir?: string);
12
+ getAll(): DownloadedState;
13
+ getDownloadedVersion(toolName: string): string | null;
14
+ recordDownload(toolName: string, version: string, filename: string): void;
15
+ }
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ export class DownloadTracker {
5
+ filePath;
6
+ constructor(baseDir) {
7
+ const dir = baseDir ?? join(homedir(), '.release-radar-cli');
8
+ if (!existsSync(dir)) {
9
+ mkdirSync(dir, { recursive: true });
10
+ }
11
+ this.filePath = join(dir, 'downloaded.json');
12
+ }
13
+ getAll() {
14
+ if (!existsSync(this.filePath)) {
15
+ return {};
16
+ }
17
+ return JSON.parse(readFileSync(this.filePath, 'utf-8'));
18
+ }
19
+ getDownloadedVersion(toolName) {
20
+ const all = this.getAll();
21
+ return all[toolName]?.version ?? null;
22
+ }
23
+ recordDownload(toolName, version, filename) {
24
+ const all = this.getAll();
25
+ all[toolName] = {
26
+ version,
27
+ downloadedAt: new Date().toISOString(),
28
+ filename,
29
+ };
30
+ writeFileSync(this.filePath, JSON.stringify(all, null, 2));
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { DownloadTracker } from './tracker.js';
3
+ import { mkdtempSync, rmSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ describe('DownloadTracker', () => {
7
+ let tempDir;
8
+ let tracker;
9
+ beforeEach(() => {
10
+ tempDir = mkdtempSync(join(tmpdir(), 'tracker-test-'));
11
+ tracker = new DownloadTracker(tempDir);
12
+ });
13
+ afterEach(() => {
14
+ rmSync(tempDir, { recursive: true });
15
+ });
16
+ it('returns null for untracked tool', () => {
17
+ expect(tracker.getDownloadedVersion('unknown')).toBeNull();
18
+ });
19
+ it('tracks downloaded version', () => {
20
+ tracker.recordDownload('Ninja', '1.12.0', 'ninja-1.12.0.zip');
21
+ const version = tracker.getDownloadedVersion('Ninja');
22
+ expect(version).toBe('1.12.0');
23
+ });
24
+ it('returns all downloaded tools', () => {
25
+ tracker.recordDownload('Ninja', '1.12.0', 'ninja.zip');
26
+ tracker.recordDownload('CMake', '3.28.0', 'cmake.tar.gz');
27
+ const all = tracker.getAll();
28
+ expect(Object.keys(all)).toHaveLength(2);
29
+ expect(all['Ninja'].version).toBe('1.12.0');
30
+ expect(all['CMake'].version).toBe('3.28.0');
31
+ });
32
+ });
@@ -0,0 +1,12 @@
1
+ export interface VersionsJsonTool {
2
+ name: string;
3
+ displayName: string;
4
+ version: string;
5
+ publishedAt: string;
6
+ downloadUrl: string;
7
+ filename: string;
8
+ }
9
+ export interface VersionsJson {
10
+ generatedAt: string;
11
+ tools: VersionsJsonTool[];
12
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/ui.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { VersionsJsonTool } from './types.js';
2
+ import type { DownloadedState } from './tracker.js';
3
+ import type { CliConfig } from './config.js';
4
+ export declare function promptSetup(): Promise<CliConfig>;
5
+ interface ToolChoice {
6
+ name: string;
7
+ displayName: string;
8
+ version: string;
9
+ downloadUrl: string;
10
+ filename: string;
11
+ status: 'new' | 'update' | 'current';
12
+ downloadedVersion: string | null;
13
+ }
14
+ export declare function promptToolSelection(tools: VersionsJsonTool[], downloaded: DownloadedState, generatedAt: string): Promise<ToolChoice[]>;
15
+ export {};
package/dist/ui.js ADDED
@@ -0,0 +1,86 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ export async function promptSetup() {
4
+ console.log(chalk.bold('\nWelcome to release-radar-cli!\n'));
5
+ console.log("Let's configure your settings.\n");
6
+ const answers = await inquirer.prompt([
7
+ {
8
+ type: 'input',
9
+ name: 'nexusUrl',
10
+ message: 'Enter your Nexus proxy base URL:',
11
+ validate: (input) => {
12
+ if (!input.trim())
13
+ return 'URL is required';
14
+ if (!input.startsWith('http'))
15
+ return 'URL must start with http:// or https://';
16
+ return true;
17
+ },
18
+ },
19
+ {
20
+ type: 'input',
21
+ name: 'downloadDir',
22
+ message: 'Enter download directory:',
23
+ default: '~/downloads/tools',
24
+ validate: (input) => input.trim() ? true : 'Directory is required',
25
+ },
26
+ ]);
27
+ return {
28
+ nexusUrl: answers.nexusUrl.replace(/\/$/, ''), // Remove trailing slash
29
+ downloadDir: answers.downloadDir.replace('~', process.env.HOME || ''),
30
+ };
31
+ }
32
+ function getStatus(tool, downloaded) {
33
+ const record = downloaded[tool.name];
34
+ if (!record) {
35
+ return { status: 'new', downloadedVersion: null };
36
+ }
37
+ if (record.version !== tool.version) {
38
+ return { status: 'update', downloadedVersion: record.version };
39
+ }
40
+ return { status: 'current', downloadedVersion: record.version };
41
+ }
42
+ export async function promptToolSelection(tools, downloaded, generatedAt) {
43
+ const choices = tools.map((tool) => {
44
+ const { status, downloadedVersion } = getStatus(tool, downloaded);
45
+ return {
46
+ ...tool,
47
+ status,
48
+ downloadedVersion,
49
+ };
50
+ });
51
+ console.log(chalk.bold(`\nrelease-radar-cli`));
52
+ console.log(chalk.gray(`Last updated: ${new Date(generatedAt).toLocaleString()}\n`));
53
+ // Display table
54
+ console.log(chalk.bold(' Tool Latest Downloaded Status'));
55
+ console.log(chalk.gray('─'.repeat(60)));
56
+ choices.forEach((choice) => {
57
+ const downloadedStr = choice.downloadedVersion ?? '-';
58
+ let statusStr;
59
+ switch (choice.status) {
60
+ case 'new':
61
+ statusStr = chalk.blue('NEW');
62
+ break;
63
+ case 'update':
64
+ statusStr = chalk.yellow('UPDATE');
65
+ break;
66
+ case 'current':
67
+ statusStr = chalk.green('✓');
68
+ break;
69
+ }
70
+ console.log(` ${choice.displayName.padEnd(18)} ${choice.version.padEnd(12)} ${downloadedStr.padEnd(12)} ${statusStr}`);
71
+ });
72
+ console.log('');
73
+ const { selected } = await inquirer.prompt([
74
+ {
75
+ type: 'checkbox',
76
+ name: 'selected',
77
+ message: 'Select tools to download:',
78
+ choices: choices.map((choice) => ({
79
+ name: `${choice.displayName} ${choice.version}`,
80
+ value: choice,
81
+ checked: choice.status !== 'current',
82
+ })),
83
+ },
84
+ ]);
85
+ return selected;
86
+ }
@@ -0,0 +1 @@
1
+ export declare function checkAndUpdate(): Promise<boolean>;
@@ -0,0 +1,51 @@
1
+ import { execSync, spawn } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ function getCurrentVersion() {
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const pkgPath = join(__dirname, '..', 'package.json');
8
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
9
+ return pkg.version;
10
+ }
11
+ function getLatestVersion() {
12
+ try {
13
+ const result = execSync('npm view @lvnt/release-radar-cli version', {
14
+ encoding: 'utf-8',
15
+ stdio: ['pipe', 'pipe', 'pipe'],
16
+ });
17
+ return result.trim();
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export async function checkAndUpdate() {
24
+ const current = getCurrentVersion();
25
+ const latest = getLatestVersion();
26
+ if (!latest) {
27
+ // Can't reach npm registry, continue with current
28
+ return false;
29
+ }
30
+ if (current === latest) {
31
+ return false;
32
+ }
33
+ console.log(`Updating from ${current} to ${latest}...`);
34
+ try {
35
+ execSync('npm update -g @lvnt/release-radar-cli', {
36
+ stdio: 'inherit',
37
+ });
38
+ console.log('Update complete. Restarting...\n');
39
+ // Restart self
40
+ const child = spawn(process.argv[0], process.argv.slice(1), {
41
+ detached: true,
42
+ stdio: 'inherit',
43
+ });
44
+ child.unref();
45
+ process.exit(0);
46
+ }
47
+ catch (error) {
48
+ console.error('Update failed, continuing with current version');
49
+ return false;
50
+ }
51
+ }
@@ -0,0 +1,2 @@
1
+ import type { VersionsJson } from './types.js';
2
+ export declare function loadVersions(): VersionsJson;
@@ -0,0 +1,10 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ export function loadVersions() {
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ // In dist/, versions.json is at package root
7
+ const versionsPath = join(__dirname, '..', 'versions.json');
8
+ const content = readFileSync(versionsPath, 'utf-8');
9
+ return JSON.parse(content);
10
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@lvnt/release-radar-cli",
3
+ "version": "0.1.0",
4
+ "description": "Interactive CLI for downloading tools through Nexus proxy",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "release-radar-cli": "bin/release-radar-cli.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "test": "vitest run",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "bin",
19
+ "versions.json"
20
+ ],
21
+ "keywords": [
22
+ "release",
23
+ "download",
24
+ "nexus",
25
+ "cli"
26
+ ],
27
+ "author": "lvnt",
28
+ "license": "ISC",
29
+ "dependencies": {
30
+ "chalk": "^5.3.0",
31
+ "inquirer": "^9.2.12"
32
+ },
33
+ "devDependencies": {
34
+ "@types/inquirer": "^9.0.7",
35
+ "@types/node": "^20.10.0",
36
+ "tsx": "^4.7.0",
37
+ "typescript": "^5.3.0",
38
+ "vitest": "^1.1.0"
39
+ }
40
+ }
package/versions.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "generatedAt": "2026-01-24T10:00:00Z",
3
+ "tools": [
4
+ {
5
+ "name": "Ninja",
6
+ "displayName": "Ninja",
7
+ "version": "1.12.0",
8
+ "publishedAt": "2026-01-20T00:00:00Z",
9
+ "downloadUrl": "{{NEXUS_URL}}/github.com/ninja-build/ninja/releases/download/v1.12.0/ninja-linux.zip",
10
+ "filename": "ninja-1.12.0-linux.zip"
11
+ },
12
+ {
13
+ "name": "CMake",
14
+ "displayName": "CMake",
15
+ "version": "3.28.1",
16
+ "publishedAt": "2026-01-18T00:00:00Z",
17
+ "downloadUrl": "{{NEXUS_URL}}/github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz",
18
+ "filename": "cmake-3.28.1-linux-x86_64.tar.gz"
19
+ }
20
+ ]
21
+ }