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 +21 -0
- package/README.md +82 -0
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +56 -0
- package/dist/commands/download.d.ts +5 -0
- package/dist/commands/download.js +64 -0
- package/dist/commands/explore.d.ts +1 -0
- package/dist/commands/explore.js +62 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +19 -0
- package/dist/commands/name.d.ts +1 -0
- package/dist/commands/name.js +30 -0
- package/dist/commands/publish.d.ts +3 -0
- package/dist/commands/publish.js +62 -0
- package/dist/commands/push.d.ts +1 -0
- package/dist/commands/push.js +24 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +24 -0
- package/dist/commands/star.d.ts +1 -0
- package/dist/commands/star.js +28 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +29 -0
- package/dist/commands/ui.d.ts +1 -0
- package/dist/commands/ui.js +71 -0
- package/dist/daemon/worker.d.ts +1 -0
- package/dist/daemon/worker.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +81 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.js +1 -0
- package/dist/ui/public/index.html +255 -0
- package/dist/utils/p2p-registry.d.ts +9 -0
- package/dist/utils/p2p-registry.js +130 -0
- package/dist/utils/stars.d.ts +4 -0
- package/dist/utils/stars.js +23 -0
- package/dist/utils/storage.d.ts +14 -0
- package/dist/utils/storage.js +42 -0
- package/package.json +52 -0
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,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,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 {};
|