termhub-p2p 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TermHub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # TermHub šŸš€
2
+
3
+ **TermHub** is a decentralized terminal hub for P2P file sharing, often described as "GitHub for the terminal." It leverages WebTorrent to enable seamless publishing and downloading of files and directories directly from your command line.
4
+
5
+ ---
6
+
7
+ ## 🌟 Features
8
+
9
+ - **Decentralized**: No central server for file hosting; everything is shared via P2P.
10
+ - **Simple CLI**: Intuitive commands inspired by Git and GitHub.
11
+ - **Fast & Lightweight**: Built with Node.js and WebTorrent.
12
+ - **Global Registry**: Search and discover shared repositories.
13
+ - **Star System**: Keep track of your favorite P2P repositories.
14
+
15
+ ---
16
+
17
+ ## šŸ“¦ Installation
18
+
19
+ To install TermHub globally, use npm:
20
+
21
+ ```bash
22
+ npm install -g termhub
23
+ ```
24
+
25
+ ---
26
+
27
+ ## šŸš€ Usage
28
+
29
+ ### Publish a Repository
30
+ Share a file or directory with the world:
31
+ ```bash
32
+ termhub publish ./my-awesome-project
33
+ ```
34
+
35
+ ### Download a Repository
36
+ Download using a magnet link or info hash:
37
+ ```bash
38
+ termhub download <magnet-link-or-hash> -o ./destination
39
+ ```
40
+
41
+ ### Search
42
+ Find repositories in the global registry:
43
+ ```bash
44
+ termhub search "cool-project"
45
+ ```
46
+
47
+ ### List Published Items
48
+ See what you have shared:
49
+ ```bash
50
+ termhub list
51
+ ```
52
+
53
+ ### Star Repositories
54
+ Star a repository to save it for later:
55
+ ```bash
56
+ termhub star "project-name" <magnet-link>
57
+ ```
58
+ Or list your stars:
59
+ ```bash
60
+ termhub star
61
+ ```
62
+
63
+ ---
64
+
65
+ ## šŸ› ļø Tech Stack
66
+
67
+ - **Node.js**: Runtime environment.
68
+ - **WebTorrent**: P2P engine.
69
+ - **Commander.js**: CLI framework.
70
+ - **Chalk & Ora**: Beautiful terminal UI/UX.
71
+
72
+ ---
73
+
74
+ ## šŸ“„ License
75
+
76
+ This project is licensed under the **ISC License**. See the [LICENSE](LICENSE) file for details.
77
+
78
+ ---
79
+
80
+ <p align="center">
81
+ Made with ā¤ļø for the Terminal community.
82
+ </p>
@@ -0,0 +1 @@
1
+ export declare function daemonManager(action: string): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import os from 'os';
5
+ import chalk from 'chalk';
6
+ import { fileURLToPath } from 'url';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const PID_FILE = path.join(os.homedir(), '.termhub', 'daemon.pid');
9
+ const LOG_FILE = path.join(os.homedir(), '.termhub', 'daemon.log');
10
+ export async function daemonManager(action) {
11
+ if (action === 'start') {
12
+ if (await fs.pathExists(PID_FILE)) {
13
+ console.log(chalk.yellow('āš ļø Daemon is already running (or PID file exists).'));
14
+ return;
15
+ }
16
+ console.log(chalk.blue('šŸš€ Starting TermHub Daemon in background...'));
17
+ // Path to the worker entry point in dist
18
+ const workerPath = path.join(__dirname, '..', 'daemon', 'worker.js');
19
+ const out = await fs.open(LOG_FILE, 'a');
20
+ const err = await fs.open(LOG_FILE, 'a');
21
+ const subprocess = spawn('node', [workerPath], {
22
+ detached: true,
23
+ stdio: ['ignore', out, err]
24
+ });
25
+ await fs.writeFile(PID_FILE, subprocess.pid?.toString() || '');
26
+ subprocess.unref();
27
+ console.log(chalk.green('āœ… Daemon launched! All your published files are now being seeded in background.'));
28
+ console.log(chalk.gray(`Logs: ${LOG_FILE}`));
29
+ }
30
+ else if (action === 'stop') {
31
+ if (!(await fs.pathExists(PID_FILE))) {
32
+ console.log(chalk.red('āŒ Daemon is not running.'));
33
+ return;
34
+ }
35
+ const pid = parseInt(await fs.readFile(PID_FILE, 'utf8'));
36
+ try {
37
+ process.kill(pid);
38
+ console.log(chalk.green('šŸ›‘ Daemon stopped.'));
39
+ }
40
+ catch (e) {
41
+ console.log(chalk.yellow('āš ļø Could not kill process. It might have already exited.'));
42
+ }
43
+ await fs.remove(PID_FILE);
44
+ }
45
+ else if (action === 'status') {
46
+ const isRunning = await fs.pathExists(PID_FILE);
47
+ if (isRunning) {
48
+ const pid = await fs.readFile(PID_FILE, 'utf8');
49
+ console.log(chalk.green(`ā— TermHub Daemon is RUNNING (PID: ${pid})`));
50
+ console.log(chalk.gray(`Seeding your global repositories.`));
51
+ }
52
+ else {
53
+ console.log(chalk.red('ā—‹ TermHub Daemon is OFFLINE'));
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,5 @@
1
+ interface DownloadOptions {
2
+ output: string;
3
+ }
4
+ export declare function download(magnet: string, options: DownloadOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,64 @@
1
+ import WebTorrent from 'webtorrent';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import path from 'path';
5
+ import fs from 'fs-extra';
6
+ const GLOBAL_TRACKERS = [
7
+ 'udp://tracker.leechers-paradise.org:6969',
8
+ 'udp://tracker.coppersurfer.tk:6969',
9
+ 'udp://tracker.opentrackr.org:1337',
10
+ 'udp://explodie.org:6969',
11
+ 'wss://tracker.btorrent.xyz',
12
+ 'wss://tracker.openwebtorrent.com',
13
+ 'wss://tracker.webtorrent.dev'
14
+ ];
15
+ export async function download(magnet, options) {
16
+ const client = new WebTorrent();
17
+ const spinner = ora('Connecting to peers...').start();
18
+ const outputPath = path.resolve(options.output);
19
+ try {
20
+ await fs.ensureDir(outputPath);
21
+ // Add trackers to the magnet if they are missing
22
+ let finalMagnet = magnet;
23
+ if (!finalMagnet.includes('tr=')) {
24
+ GLOBAL_TRACKERS.forEach(tr => {
25
+ finalMagnet += `&tr=${encodeURIComponent(tr)}`;
26
+ });
27
+ }
28
+ client.add(finalMagnet, { path: outputPath, announce: GLOBAL_TRACKERS }, (torrent) => {
29
+ spinner.text = `Found metadata. Downloading ${chalk.cyan(torrent.name)}...`;
30
+ torrent.on('download', () => {
31
+ const progress = (torrent.progress * 100).toFixed(1);
32
+ const downloaded = (torrent.downloaded / 1024 / 1024).toFixed(2);
33
+ const speed = (torrent.downloadSpeed / 1024 / 1024).toFixed(2);
34
+ spinner.text = `Downloading ${chalk.cyan(torrent.name)}: ${chalk.green(progress + '%')} (${downloaded} MB) | Speed: ${speed} MB/s | Peers: ${torrent.numPeers}`;
35
+ });
36
+ torrent.on('done', () => {
37
+ spinner.succeed(chalk.green(`Download complete: ${chalk.bold(torrent.name)}`));
38
+ console.log(`${chalk.yellow('Location:')} ${path.join(outputPath, torrent.name)}`);
39
+ process.exit(0);
40
+ });
41
+ torrent.on('error', (err) => {
42
+ spinner.fail(chalk.red('Torrent error occurred.'));
43
+ console.error(err);
44
+ process.exit(1);
45
+ });
46
+ torrent.on('wire', (wire) => {
47
+ // New peer connected!
48
+ spinner.text = `New peer found (${wire.remoteAddress}). Starting transfer...`;
49
+ });
50
+ });
51
+ // Timeout if no peers found in 30 seconds
52
+ setTimeout(() => {
53
+ const torrent = client.torrents[0];
54
+ if (torrent && torrent.numPeers === 0) {
55
+ spinner.warn(chalk.yellow('No peers found for a while. Are you sure someone is seeding this file?'));
56
+ }
57
+ }, 30000);
58
+ }
59
+ catch (error) {
60
+ spinner.fail(chalk.red('Failed to start download.'));
61
+ console.error(error.message);
62
+ process.exit(1);
63
+ }
64
+ }
@@ -0,0 +1 @@
1
+ export declare function explore(): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import pkg from 'enquirer';
4
+ const { Select } = pkg;
5
+ import { getAllFromRegistry } from '../utils/p2p-registry.js';
6
+ import { download } from './download.js';
7
+ export async function explore() {
8
+ const spinner = ora('Exploring decentralized registry...').start();
9
+ const repos = await getAllFromRegistry();
10
+ if (repos.length === 0) {
11
+ spinner.fail(chalk.red('No repositories found in the network.'));
12
+ process.exit(0);
13
+ }
14
+ spinner.succeed(chalk.green(`Found ${repos.length} repositories.`));
15
+ const choices = repos.map(repo => ({
16
+ name: repo.infoHash,
17
+ message: `${chalk.bold.cyan(repo.name)} ${chalk.gray('- ' + repo.description)}`,
18
+ value: repo
19
+ }));
20
+ const prompt = new Select({
21
+ name: 'repository',
22
+ message: 'Select a repository to view details or download:',
23
+ choices: choices
24
+ });
25
+ try {
26
+ const selectedHash = await prompt.run();
27
+ const selectedRepo = repos.find(r => r.infoHash === selectedHash);
28
+ if (selectedRepo) {
29
+ console.log('\n' + chalk.bold.yellow('--- Repository Details ---'));
30
+ console.log(`${chalk.cyan('Name:')} ${selectedRepo.name}`);
31
+ console.log(`${chalk.cyan('Description:')} ${selectedRepo.description}`);
32
+ console.log(`${chalk.cyan('Author:')} ${selectedRepo.author}`);
33
+ console.log(`${chalk.cyan('Magnet:')} ${chalk.magenta(selectedRepo.magnetURI)}`);
34
+ console.log(chalk.bold.yellow('--------------------------\n'));
35
+ const actionPrompt = new Select({
36
+ name: 'action',
37
+ message: 'What would you like to do?',
38
+ choices: [
39
+ { name: 'download', message: 'šŸ“„ Download now' },
40
+ { name: 'copy', message: 'šŸ“‹ Copy Magnet Link' },
41
+ { name: 'cancel', message: 'āŒ Cancel' }
42
+ ]
43
+ });
44
+ const action = await actionPrompt.run();
45
+ if (action === 'download') {
46
+ await download(selectedRepo.magnetURI, { output: '.' });
47
+ }
48
+ else if (action === 'copy') {
49
+ console.log(chalk.green('\nMagnet link copied to console! (Copy-paste manually for now)'));
50
+ console.log(selectedRepo.magnetURI);
51
+ process.exit(0);
52
+ }
53
+ else {
54
+ process.exit(0);
55
+ }
56
+ }
57
+ }
58
+ catch (err) {
59
+ // User cancelled (Ctrl+C)
60
+ process.exit(0);
61
+ }
62
+ }
@@ -0,0 +1 @@
1
+ export declare function list(): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import chalk from 'chalk';
2
+ import { getRepositories, initStorage } from '../utils/storage.js';
3
+ export async function list() {
4
+ await initStorage();
5
+ const repos = await getRepositories();
6
+ if (repos.length === 0) {
7
+ console.log(chalk.yellow('You haven\'t published any repositories yet.'));
8
+ process.exit(0);
9
+ }
10
+ console.log(chalk.bold.cyan('\nYour Published Repositories:\n'));
11
+ repos.forEach((repo, index) => {
12
+ console.log(`${chalk.green(index + 1 + '.')} ${chalk.bold(repo.name)}`);
13
+ console.log(` ${chalk.gray('Hash:')} ${repo.infoHash}`);
14
+ console.log(` ${chalk.gray('Path:')} ${repo.path}`);
15
+ console.log(` ${chalk.gray('Magnet:')} ${chalk.magenta(repo.magnetURI.substring(0, 60) + '...')}`);
16
+ console.log('');
17
+ });
18
+ process.exit(0);
19
+ }
@@ -0,0 +1 @@
1
+ export declare function setUsername(name: string): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { setConfig, getConfig, initStorage } from '../utils/storage.js';
4
+ import { checkNameAvailability, claimUsername } from '../utils/p2p-registry.js';
5
+ export async function setUsername(name) {
6
+ await initStorage();
7
+ const spinner = ora('Checking name availability in the decentralized mesh...').start();
8
+ // Validation
9
+ const nameRegex = /^[a-zA-Z0-9_]{3,20}$/;
10
+ if (!nameRegex.test(name)) {
11
+ spinner.fail(chalk.red('āŒ Invalid username!'));
12
+ console.log(chalk.yellow('Rules: Between 3-20 chars, alphanumeric and underscores only.'));
13
+ process.exit(1);
14
+ }
15
+ const { publicKey } = await getConfig();
16
+ const { available, owner } = await checkNameAvailability(name, publicKey);
17
+ if (!available) {
18
+ spinner.fail(chalk.red(`āŒ The name "${chalk.bold(name)}" is already taken by another user.`));
19
+ process.exit(1);
20
+ }
21
+ if (!owner) {
22
+ spinner.text = 'Claiming name...';
23
+ await claimUsername(name, publicKey);
24
+ }
25
+ const config = await getConfig();
26
+ config.username = name;
27
+ await setConfig(config);
28
+ spinner.succeed(chalk.green(`āœ… Identity updated! Your handle is now: ${chalk.bold(name)}`));
29
+ process.exit(0);
30
+ }
@@ -0,0 +1,3 @@
1
+ export declare function publish(input: string | string[], options?: {
2
+ name?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import WebTorrent from 'webtorrent';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import path from 'path';
5
+ import { saveRepository, initStorage, getConfig } from '../utils/storage.js';
6
+ import { addToRegistry } from '../utils/p2p-registry.js';
7
+ const GLOBAL_TRACKERS = [
8
+ 'udp://tracker.leechers-paradise.org:6969',
9
+ 'udp://tracker.coppersurfer.tk:6969',
10
+ 'udp://tracker.opentrackr.org:1337',
11
+ 'udp://explodie.org:6969',
12
+ 'wss://tracker.btorrent.xyz',
13
+ 'wss://tracker.openwebtorrent.com',
14
+ 'wss://tracker.webtorrent.dev'
15
+ ];
16
+ export async function publish(input, options = {}) {
17
+ await initStorage();
18
+ const { username } = await getConfig();
19
+ const client = new WebTorrent();
20
+ const displayName = Array.isArray(input) ? `${input.length} files` : input;
21
+ const spinner = ora(`Preparing to publish ${chalk.cyan(displayName)}...`).start();
22
+ try {
23
+ const seedPath = Array.isArray(input) ? input : path.resolve(input);
24
+ const seedOptions = {
25
+ name: options.name,
26
+ announce: GLOBAL_TRACKERS
27
+ };
28
+ client.seed(seedPath, seedOptions, async (torrent) => {
29
+ spinner.succeed(chalk.green('Repository published successfully!'));
30
+ const repoData = {
31
+ name: torrent.name,
32
+ infoHash: torrent.infoHash,
33
+ magnetURI: torrent.magnetURI,
34
+ path: Array.isArray(input) ? process.cwd() : path.resolve(input),
35
+ author: username
36
+ };
37
+ await saveRepository(repoData);
38
+ // Add to decentralized P2P registry
39
+ spinner.start(chalk.blue('Announcing to decentralized registry...'));
40
+ await addToRegistry(repoData);
41
+ spinner.succeed(chalk.green('Announced to global registry!'));
42
+ console.log('\n' + chalk.bold('Repository Details:'));
43
+ console.log(`${chalk.yellow('Name:')} ${torrent.name}`);
44
+ console.log(`${chalk.yellow('Info Hash:')} ${torrent.infoHash}`);
45
+ console.log(`${chalk.yellow('Magnet Link:')} \n${chalk.magenta(torrent.magnetURI)}`);
46
+ console.log('\n' + chalk.blue('Keep this terminal open to continue seeding your repository.'));
47
+ console.log(chalk.gray('Press Ctrl+C to stop seeding.'));
48
+ torrent.on('upload', () => {
49
+ process.stdout.write(`\r${chalk.green('↑')} Uploaded: ${chalk.white((torrent.uploaded / 1024 / 1024).toFixed(2))} MB | Peers: ${torrent.numPeers}`);
50
+ });
51
+ torrent.on('wire', (wire) => {
52
+ // Track when a downloader connects
53
+ console.log(chalk.gray(`\n[Peer Connected: ${wire.remoteAddress}]`));
54
+ });
55
+ });
56
+ }
57
+ catch (error) {
58
+ spinner.fail(chalk.red('Failed to publish repository.'));
59
+ console.error(error.message);
60
+ process.exit(1);
61
+ }
62
+ }
@@ -0,0 +1 @@
1
+ export declare function push(): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { globby } from 'globby';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { publish } from './publish.js';
5
+ export async function push() {
6
+ const currentDir = process.cwd();
7
+ console.log(chalk.bold.blue(`\nšŸš€ TermHub Push: ${path.basename(currentDir)}`));
8
+ // Get all files recursively, respecting .gitignore
9
+ const allFiles = await globby('**/*', {
10
+ cwd: currentDir,
11
+ dot: true,
12
+ gitignore: true,
13
+ });
14
+ if (allFiles.length === 0) {
15
+ console.log(chalk.red('āŒ No files found to push!'));
16
+ process.exit(1);
17
+ }
18
+ console.log(chalk.gray(`šŸ“¦ Found ${allFiles.length} files (ignoring items in .gitignore).`));
19
+ // Convert relative globby paths to absolute paths for WebTorrent
20
+ const absoluteFiles = allFiles.map(file => path.join(currentDir, file));
21
+ // Name the repository after the current directory
22
+ const repoName = path.basename(currentDir);
23
+ await publish(absoluteFiles, { name: repoName });
24
+ }
@@ -0,0 +1 @@
1
+ export declare function search(query: string): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { searchInRegistry } from '../utils/p2p-registry.js';
4
+ export async function search(query) {
5
+ const spinner = ora(`Searching decentralized registry for ${chalk.cyan(query)}...`).start();
6
+ const results = await searchInRegistry(query);
7
+ if (results.length === 0) {
8
+ spinner.fail(chalk.red(`No repositories found matching "${query}"`));
9
+ console.log(chalk.gray('Tip: Make sure you are connected to the internet and give the network a few seconds to discover peers.'));
10
+ process.exit(0);
11
+ }
12
+ spinner.succeed(chalk.green(`Found ${results.length} repositories in the P2P network:`));
13
+ console.log('');
14
+ results.forEach(repo => {
15
+ console.log(`${chalk.bold.cyan(repo.name)}`);
16
+ console.log(`${chalk.white(repo.description)}`);
17
+ console.log(`${chalk.gray('Magnet:')} ${chalk.magenta(repo.magnetURI)}`);
18
+ if (repo.timestamp) {
19
+ console.log(`${chalk.gray('Timestamp:')} ${new Date(repo.timestamp).toLocaleString()}`);
20
+ }
21
+ console.log('');
22
+ });
23
+ process.exit(0);
24
+ }
@@ -0,0 +1 @@
1
+ export declare function star(name?: string, magnet?: string): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk';
2
+ import { starRepository, initStars, getStars } from '../utils/stars.js';
3
+ export async function star(name, magnet) {
4
+ await initStars();
5
+ if (!name || !magnet) {
6
+ // If no args, list stars
7
+ const stars = await getStars();
8
+ if (stars.length === 0) {
9
+ console.log(chalk.yellow('You haven\'t starred any repositories yet.'));
10
+ process.exit(0);
11
+ }
12
+ console.log(chalk.bold.yellow('\nYour Starred Repositories:\n'));
13
+ stars.forEach((s) => {
14
+ console.log(`${chalk.yellow('ā˜…')} ${chalk.bold(s.name)}`);
15
+ console.log(` ${chalk.gray('Magnet:')} ${chalk.magenta(s.magnet)}`);
16
+ console.log('');
17
+ });
18
+ process.exit(0);
19
+ }
20
+ const success = await starRepository(name, magnet);
21
+ if (success) {
22
+ console.log(chalk.green(`Successfully starred ${chalk.bold(name)}!`));
23
+ }
24
+ else {
25
+ console.log(chalk.yellow(`You already starred ${chalk.bold(name)}.`));
26
+ }
27
+ process.exit(0);
28
+ }
@@ -0,0 +1 @@
1
+ export declare function status(): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import chalk from 'chalk';
2
+ import { getRepositories, initStorage } from '../utils/storage.js';
3
+ import { initStars, getStars } from '../utils/stars.js';
4
+ import os from 'os';
5
+ export async function status() {
6
+ await initStorage();
7
+ await initStars();
8
+ const repos = await getRepositories();
9
+ const stars = await getStars();
10
+ console.log(chalk.bold.cyan('\nšŸ“Š TermHub Node Status\n'));
11
+ // General Stats
12
+ console.log(chalk.yellow('--- Global Metrics ---'));
13
+ console.log(`${chalk.white('Total Published:')} ${repos.length}`);
14
+ console.log(`${chalk.white('Total Stars:')} ${stars.length}`);
15
+ console.log('');
16
+ // Local Storage Metrics
17
+ console.log(chalk.yellow('--- Local Identity ---'));
18
+ console.log(`${chalk.white('Config Path:')} ${os.homedir()}/.termhub`);
19
+ if (repos.length > 0) {
20
+ console.log('\n' + chalk.yellow('--- Top Repositories ---'));
21
+ // List top 3 recent ones
22
+ repos.slice(-3).reverse().forEach(repo => {
23
+ console.log(`${chalk.green('ā—')} ${chalk.bold(repo.name)} ${chalk.gray('(' + repo.infoHash.substring(0, 8) + '...)')}`);
24
+ });
25
+ }
26
+ console.log('\n' + chalk.blue('P2P Health: ') + chalk.green('Active'));
27
+ console.log(chalk.gray('Discovery: ') + chalk.white('Nostr Relay Mesh'));
28
+ process.exit(0);
29
+ }
@@ -0,0 +1 @@
1
+ export declare function ui(): Promise<void>;
@@ -0,0 +1,71 @@
1
+ import express from 'express';
2
+ import open from 'open';
3
+ import chalk from 'chalk';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { getRepositories, getConfig, setConfig } from '../utils/storage.js';
7
+ import { getAllFromRegistry, checkNameAvailability } from '../utils/p2p-registry.js';
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ export async function ui() {
10
+ const app = express();
11
+ const PORT = 3000;
12
+ // Serve static files from the build directory
13
+ const publicPath = path.join(__dirname, '..', 'ui', 'public');
14
+ app.use(express.static(publicPath));
15
+ app.use(express.json());
16
+ // API Endpoints
17
+ app.get('/api/status', async (req, res) => {
18
+ const repos = await getRepositories();
19
+ const config = await getConfig();
20
+ res.json({
21
+ status: 'active',
22
+ publishedCount: repos.length,
23
+ nodeVersion: process.version,
24
+ username: config.username
25
+ });
26
+ });
27
+ app.get('/api/config', async (req, res) => {
28
+ const config = await getConfig();
29
+ res.json(config);
30
+ });
31
+ app.post('/api/config', async (req, res) => {
32
+ const { username } = req.body;
33
+ const { publicKey, privateKey } = await getConfig();
34
+ // Validation
35
+ const nameRegex = /^[a-zA-Z0-9_]{3,20}$/;
36
+ if (!nameRegex.test(username)) {
37
+ return res.status(400).json({ error: 'Invalid username' });
38
+ }
39
+ const { available, owner } = await checkNameAvailability(username, publicKey);
40
+ if (!available) {
41
+ return res.status(400).json({ error: 'Name already taken by another user' });
42
+ }
43
+ await setConfig({ username, publicKey, privateKey });
44
+ res.json({ success: true });
45
+ });
46
+ app.get('/api/repositories', async (req, res) => {
47
+ const repos = await getRepositories();
48
+ res.json(repos);
49
+ });
50
+ app.get('/api/explore', async (req, res) => {
51
+ try {
52
+ const repos = await getAllFromRegistry();
53
+ res.json(repos);
54
+ }
55
+ catch (err) {
56
+ res.status(500).json({ error: 'Failed to fetch registry' });
57
+ }
58
+ });
59
+ // Start Server
60
+ app.listen(PORT, async () => {
61
+ console.log(chalk.bold.cyan(`\nšŸ–„ļø TermHub Dashboard is running!`));
62
+ console.log(chalk.white(`Local URL: `) + chalk.underline.blue(`http://localhost:${PORT}`));
63
+ console.log(chalk.gray(`Press Ctrl+C to stop the server.\n`));
64
+ try {
65
+ await open(`http://localhost:${PORT}`);
66
+ }
67
+ catch (err) {
68
+ console.log(chalk.yellow('Could not open browser automatically. Please open the URL manually.'));
69
+ }
70
+ });
71
+ }
@@ -0,0 +1 @@
1
+ export {};