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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import WebTorrent from 'webtorrent';
|
|
2
|
+
import { getRepositories, initStorage } from '../utils/storage.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
const PID_FILE = path.join(os.homedir(), '.termhub', 'daemon.pid');
|
|
7
|
+
async function runDaemon() {
|
|
8
|
+
await initStorage();
|
|
9
|
+
const client = new WebTorrent();
|
|
10
|
+
const activeHashes = new Set();
|
|
11
|
+
console.log('TermHub Daemon started...');
|
|
12
|
+
// Function to seed all repositories from storage
|
|
13
|
+
async function syncRepositories() {
|
|
14
|
+
const repos = await getRepositories();
|
|
15
|
+
for (const repo of repos) {
|
|
16
|
+
if (!activeHashes.has(repo.infoHash)) {
|
|
17
|
+
if (repo.path && await fs.pathExists(repo.path)) {
|
|
18
|
+
console.log(`Daemon: Seeding ${repo.name}...`);
|
|
19
|
+
client.seed(repo.path, { name: repo.name }, (torrent) => {
|
|
20
|
+
activeHashes.add(torrent.infoHash);
|
|
21
|
+
console.log(`Daemon: ${repo.name} is now online.`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Initial sync
|
|
28
|
+
await syncRepositories();
|
|
29
|
+
// Watch for changes in the repositories file
|
|
30
|
+
const reposFilePath = path.join(os.homedir(), '.termhub', 'repositories.json');
|
|
31
|
+
fs.watchFile(reposFilePath, async () => {
|
|
32
|
+
console.log('Daemon: Config changed, re-syncing...');
|
|
33
|
+
await syncRepositories();
|
|
34
|
+
});
|
|
35
|
+
// Keep process alive
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
const totalUpload = client.ratio;
|
|
38
|
+
console.log(`Daemon Heartbeat: Seeding ${client.torrents.length} repos. Ratio: ${totalUpload}`);
|
|
39
|
+
}, 60000);
|
|
40
|
+
}
|
|
41
|
+
runDaemon().catch(err => {
|
|
42
|
+
console.error('Daemon Error:', err);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { publish } from './commands/publish.js';
|
|
5
|
+
import { push } from './commands/push.js';
|
|
6
|
+
import { setUsername } from './commands/name.js';
|
|
7
|
+
import { download } from './commands/download.js';
|
|
8
|
+
import { list } from './commands/list.js';
|
|
9
|
+
import { search } from './commands/search.js';
|
|
10
|
+
import { explore } from './commands/explore.js';
|
|
11
|
+
import { ui } from './commands/ui.js';
|
|
12
|
+
import { status } from './commands/status.js';
|
|
13
|
+
import { daemonManager } from './commands/daemon.js';
|
|
14
|
+
import { star } from './commands/star.js';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
console.log(chalk.bold.cyan(`
|
|
17
|
+
_____ _ _ _
|
|
18
|
+
|_ _|__ _ __ _ __ ___ | | | |_ _| |__
|
|
19
|
+
| |/ _ \\ '__| '_ \` _ \\| |_| | | | | '_ \\
|
|
20
|
+
| | __/ | | | | | | | _ | |_| | |_) |
|
|
21
|
+
|_|\\___|_| |_| |_| |_|_| |_|\\__,_|_.__/
|
|
22
|
+
|
|
23
|
+
${chalk.italic('The Decentralized Terminal Hub')}
|
|
24
|
+
`));
|
|
25
|
+
program
|
|
26
|
+
.name('termhub')
|
|
27
|
+
.description('GitHub for the terminal - P2P File Sharing')
|
|
28
|
+
.version('1.0.0');
|
|
29
|
+
program
|
|
30
|
+
.command('publish')
|
|
31
|
+
.description('Publish a file or directory to TermHub')
|
|
32
|
+
.argument('<path>', 'path to the file or directory')
|
|
33
|
+
.action(publish);
|
|
34
|
+
program
|
|
35
|
+
.command('push')
|
|
36
|
+
.description('Publish the current directory respecting .gitignore')
|
|
37
|
+
.action(push);
|
|
38
|
+
program
|
|
39
|
+
.command('name')
|
|
40
|
+
.description('Set your decentralized handle')
|
|
41
|
+
.argument('<username>', 'desired username')
|
|
42
|
+
.action(setUsername);
|
|
43
|
+
program
|
|
44
|
+
.command('download')
|
|
45
|
+
.description('Download a repository from TermHub')
|
|
46
|
+
.argument('<magnet>', 'magnet link or info hash')
|
|
47
|
+
.option('-o, --output <path>', 'output directory', '.')
|
|
48
|
+
.action(download);
|
|
49
|
+
program
|
|
50
|
+
.command('list')
|
|
51
|
+
.description('List your published repositories')
|
|
52
|
+
.action(list);
|
|
53
|
+
program
|
|
54
|
+
.command('search')
|
|
55
|
+
.description('Search for repositories in the global registry')
|
|
56
|
+
.argument('<query>', 'search query')
|
|
57
|
+
.action(search);
|
|
58
|
+
program
|
|
59
|
+
.command('explore')
|
|
60
|
+
.description('Explore the global registry interactively')
|
|
61
|
+
.action(explore);
|
|
62
|
+
program
|
|
63
|
+
.command('ui')
|
|
64
|
+
.description('Open the TermHub Web Dashboard')
|
|
65
|
+
.action(ui);
|
|
66
|
+
program
|
|
67
|
+
.command('daemon')
|
|
68
|
+
.description('Manage the background seeding daemon')
|
|
69
|
+
.argument('<action>', 'start, stop, or status')
|
|
70
|
+
.action(daemonManager);
|
|
71
|
+
program
|
|
72
|
+
.command('status')
|
|
73
|
+
.description('Show local node status and metrics')
|
|
74
|
+
.action(status);
|
|
75
|
+
program
|
|
76
|
+
.command('star')
|
|
77
|
+
.description('Star a repository or list your stars')
|
|
78
|
+
.argument('[name]', 'repository name')
|
|
79
|
+
.argument('[magnet]', 'magnet link')
|
|
80
|
+
.action(star);
|
|
81
|
+
program.parse();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Repository {
|
|
2
|
+
name: string;
|
|
3
|
+
infoHash: string;
|
|
4
|
+
magnetURI: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
timestamp?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Star {
|
|
11
|
+
name: string;
|
|
12
|
+
magnet: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>/th/ - TermHub Registry</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
background-color: #ffffee;
|
|
9
|
+
color: #800000;
|
|
10
|
+
font-family: arial,helvetica,sans-serif;
|
|
11
|
+
font-size: 10pt;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 10px;
|
|
14
|
+
}
|
|
15
|
+
header {
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
align-items: flex-end;
|
|
19
|
+
padding: 10px 20px;
|
|
20
|
+
border-bottom: 2px solid #d9bfb7;
|
|
21
|
+
margin-bottom: 20px;
|
|
22
|
+
}
|
|
23
|
+
.header-left {
|
|
24
|
+
text-align: left;
|
|
25
|
+
}
|
|
26
|
+
h1 {
|
|
27
|
+
font-family: "Trebuchet MS",tahoma,arial,helvetica,sans-serif;
|
|
28
|
+
font-size: 24pt;
|
|
29
|
+
letter-spacing: -2px;
|
|
30
|
+
margin: 0;
|
|
31
|
+
line-height: 1;
|
|
32
|
+
}
|
|
33
|
+
.banner {
|
|
34
|
+
color: #af0a0f;
|
|
35
|
+
font-weight: bold;
|
|
36
|
+
font-size: 10pt;
|
|
37
|
+
margin-top: 5px;
|
|
38
|
+
}
|
|
39
|
+
.post {
|
|
40
|
+
background-color: #f0e0d6;
|
|
41
|
+
border: 1px solid #d9bfb7;
|
|
42
|
+
display: table;
|
|
43
|
+
padding: 5px;
|
|
44
|
+
margin-bottom: 10px;
|
|
45
|
+
width: 100%;
|
|
46
|
+
max-width: 800px;
|
|
47
|
+
margin-left: auto;
|
|
48
|
+
margin-right: auto;
|
|
49
|
+
}
|
|
50
|
+
.post-header {
|
|
51
|
+
font-weight: bold;
|
|
52
|
+
margin-bottom: 5px;
|
|
53
|
+
border-bottom: 1px solid #d9bfb7;
|
|
54
|
+
}
|
|
55
|
+
.post-id {
|
|
56
|
+
color: #117743;
|
|
57
|
+
font-weight: normal;
|
|
58
|
+
}
|
|
59
|
+
.name {
|
|
60
|
+
color: #117743;
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
}
|
|
63
|
+
.subject {
|
|
64
|
+
color: #0f0c5d;
|
|
65
|
+
font-weight: bold;
|
|
66
|
+
}
|
|
67
|
+
.magnet-btn {
|
|
68
|
+
color: #0000ee;
|
|
69
|
+
text-decoration: underline;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
background: none;
|
|
72
|
+
border: none;
|
|
73
|
+
padding: 0;
|
|
74
|
+
font-size: 9pt;
|
|
75
|
+
}
|
|
76
|
+
.magnet-btn:hover { color: #ff0000; }
|
|
77
|
+
|
|
78
|
+
.section-title {
|
|
79
|
+
background-color: #e04000;
|
|
80
|
+
color: white;
|
|
81
|
+
padding: 2px 10px;
|
|
82
|
+
font-weight: bold;
|
|
83
|
+
text-align: center;
|
|
84
|
+
margin: 20px auto;
|
|
85
|
+
width: fit-content;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.nav {
|
|
89
|
+
font-size: 9pt;
|
|
90
|
+
text-align: center;
|
|
91
|
+
margin-bottom: 10px;
|
|
92
|
+
}
|
|
93
|
+
.nav a { color: #34345c; text-decoration: none; }
|
|
94
|
+
.nav a:hover { color: #ff0000; }
|
|
95
|
+
|
|
96
|
+
.footer {
|
|
97
|
+
font-size: 8pt;
|
|
98
|
+
color: #706b5e;
|
|
99
|
+
text-align: center;
|
|
100
|
+
margin-top: 30px;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<header>
|
|
106
|
+
<div class="header-left">
|
|
107
|
+
<h1>TermHub</h1>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="nav">
|
|
110
|
+
[<a href="#" onclick="switchView('my')">My Files</a>]
|
|
111
|
+
[<a href="#" onclick="switchView('explore')">Explore</a>]
|
|
112
|
+
[<a href="#" onclick="switchView('status')">Status</a>]
|
|
113
|
+
</div>
|
|
114
|
+
</header>
|
|
115
|
+
|
|
116
|
+
<div class="section-title">Public Repositories</div>
|
|
117
|
+
<div id="content-area">
|
|
118
|
+
<!-- Posts will appear here -->
|
|
119
|
+
<div class="post">
|
|
120
|
+
<div class="post-header">System Notification</div>
|
|
121
|
+
Connecting to Gun.js nodes...
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="footer">
|
|
126
|
+
- TermHub v1.0.0 -
|
|
127
|
+
<br>
|
|
128
|
+
All files are P2P. No centralized hosting.
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<script>
|
|
132
|
+
async function loadStatus() {
|
|
133
|
+
const res = await fetch('/api/status');
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
const area = document.getElementById('content-area');
|
|
136
|
+
|
|
137
|
+
// Stats summary post
|
|
138
|
+
const statsPost = `
|
|
139
|
+
<div class="post">
|
|
140
|
+
<div class="post-header">Node Information</div>
|
|
141
|
+
<span class="subject">Current Handle:</span> <span class="name" style="font-size: 1.2rem;">${data.username}</span><br>
|
|
142
|
+
<span class="subject">Status:</span> Connected to Mesh<br>
|
|
143
|
+
<span class="subject">Local Repos:</span> ${data.publishedCount}<br>
|
|
144
|
+
<hr style="border: 0; border-top: 1px solid #d9bfb7; margin: 10px 0;">
|
|
145
|
+
<span class="subject">Change Handle:</span>
|
|
146
|
+
<input type="text" id="new-name" placeholder="new_handle" style="font-size: 8pt;">
|
|
147
|
+
[<a href="#" onclick="updateName()">Update</a>]
|
|
148
|
+
</div>
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
// Only update if we are in "Status" view or it's the first load
|
|
152
|
+
if (activeView === 'status') {
|
|
153
|
+
area.innerHTML = statsPost;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let activeView = 'explore';
|
|
158
|
+
|
|
159
|
+
async function updateName() {
|
|
160
|
+
const name = document.getElementById('new-name').value;
|
|
161
|
+
if (!name) return;
|
|
162
|
+
|
|
163
|
+
const res = await fetch('/api/config', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'Content-Type': 'application/json' },
|
|
166
|
+
body: JSON.stringify({ username: name })
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (res.ok) {
|
|
170
|
+
alert('Identity updated to ' + name);
|
|
171
|
+
loadStatus();
|
|
172
|
+
} else {
|
|
173
|
+
const err = await res.json();
|
|
174
|
+
alert('Error: ' + err.error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function loadMyRepos() {
|
|
179
|
+
activeView = 'my';
|
|
180
|
+
const res = await fetch('/api/repositories');
|
|
181
|
+
const data = await res.json();
|
|
182
|
+
const area = document.getElementById('content-area');
|
|
183
|
+
area.innerHTML = '';
|
|
184
|
+
|
|
185
|
+
if (data.length === 0) area.innerHTML = '<div class="post">No files published yet.</div>';
|
|
186
|
+
|
|
187
|
+
data.reverse().forEach(repo => {
|
|
188
|
+
const post = document.createElement('div');
|
|
189
|
+
post.className = 'post';
|
|
190
|
+
post.innerHTML = `
|
|
191
|
+
<div class="post-header">
|
|
192
|
+
<span class="subject">${repo.name}</span>
|
|
193
|
+
<span class="name">${repo.author || 'Anonymous'}</span>
|
|
194
|
+
<span class="post-id">No.${repo.infoHash.substring(0,8)}</span>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="body">
|
|
197
|
+
File Hash: ${repo.infoHash}<br>
|
|
198
|
+
Location: ${repo.path}<br>
|
|
199
|
+
<button class="magnet-btn" onclick="copy('${repo.magnetURI}')">Copy Magnet</button>
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
area.appendChild(post);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function loadExplore() {
|
|
207
|
+
activeView = 'explore';
|
|
208
|
+
const area = document.getElementById('content-area');
|
|
209
|
+
area.innerHTML = '<div class="post">Searching P2P network...</div>';
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch('/api/explore');
|
|
213
|
+
const data = await res.json();
|
|
214
|
+
area.innerHTML = '';
|
|
215
|
+
|
|
216
|
+
if (data.length === 0) area.innerHTML = '<div class="post">No files found on this relay. Try refreshing in a few seconds.</div>';
|
|
217
|
+
|
|
218
|
+
data.forEach(repo => {
|
|
219
|
+
const post = document.createElement('div');
|
|
220
|
+
post.className = 'post';
|
|
221
|
+
post.innerHTML = `
|
|
222
|
+
<div class="post-header">
|
|
223
|
+
<span class="subject">${repo.name}</span>
|
|
224
|
+
<span class="name">${repo.author || 'Anonymous'}</span>
|
|
225
|
+
<span class="post-id">No.${repo.infoHash.substring(0,8)}</span>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="body">
|
|
228
|
+
Published: ${new Date(repo.timestamp).toLocaleString()}<br><br>
|
|
229
|
+
<button class="magnet-btn" onclick="copy('${repo.magnetURI}')">Copy Magnet</button>
|
|
230
|
+
</div>
|
|
231
|
+
`;
|
|
232
|
+
area.appendChild(post);
|
|
233
|
+
});
|
|
234
|
+
} catch (err) {
|
|
235
|
+
area.innerHTML = '<div class="post">Error connecting to network.</div>';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function switchView(view) {
|
|
240
|
+
activeView = view;
|
|
241
|
+
if (view === 'status') loadStatus();
|
|
242
|
+
if (view === 'my') loadMyRepos();
|
|
243
|
+
if (view === 'explore') loadExplore();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function copy(m) {
|
|
247
|
+
navigator.clipboard.writeText(m);
|
|
248
|
+
alert('Magnet copied to clipboard!');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Default view
|
|
252
|
+
loadExplore();
|
|
253
|
+
</script>
|
|
254
|
+
</body>
|
|
255
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Repository } from '../types/index.js';
|
|
2
|
+
export declare function addToRegistry(repoData: Repository): Promise<void>;
|
|
3
|
+
export declare function searchInRegistry(query: string): Promise<Repository[]>;
|
|
4
|
+
export declare function getAllFromRegistry(): Promise<Repository[]>;
|
|
5
|
+
export declare function checkNameAvailability(name: string, myPublicKey: string): Promise<{
|
|
6
|
+
available: boolean;
|
|
7
|
+
owner: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function claimUsername(name: string, token: string): Promise<void>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { finalizeEvent, Relay } from 'nostr-tools';
|
|
2
|
+
import { getConfig } from './storage.js';
|
|
3
|
+
import WebSocket from 'ws';
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
global.WebSocket = WebSocket;
|
|
6
|
+
const RELAYS = [
|
|
7
|
+
'wss://relay.damus.io',
|
|
8
|
+
'wss://nos.lol',
|
|
9
|
+
'wss://relay.nostr.band',
|
|
10
|
+
'wss://offchain.pub'
|
|
11
|
+
];
|
|
12
|
+
function hexToBytes(hex) {
|
|
13
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
14
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
15
|
+
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
16
|
+
}
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
export async function addToRegistry(repoData) {
|
|
20
|
+
const { privateKey, username } = await getConfig();
|
|
21
|
+
const event = finalizeEvent({
|
|
22
|
+
kind: 1,
|
|
23
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
24
|
+
tags: [
|
|
25
|
+
['t', 'termhub-repo'],
|
|
26
|
+
['h', repoData.infoHash],
|
|
27
|
+
['m', repoData.magnetURI],
|
|
28
|
+
['n', repoData.name],
|
|
29
|
+
['a', username]
|
|
30
|
+
],
|
|
31
|
+
content: `Published TermHub Repository: ${repoData.name}\nMagnet: ${repoData.magnetURI}`,
|
|
32
|
+
}, hexToBytes(privateKey));
|
|
33
|
+
await Promise.allSettled(RELAYS.map(async (url) => {
|
|
34
|
+
try {
|
|
35
|
+
const relay = await Relay.connect(url);
|
|
36
|
+
await relay.publish(event);
|
|
37
|
+
relay.close();
|
|
38
|
+
}
|
|
39
|
+
catch (e) { }
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
export async function searchInRegistry(query) {
|
|
43
|
+
const results = [];
|
|
44
|
+
const seenHashes = new Set();
|
|
45
|
+
const filters = [{
|
|
46
|
+
kinds: [1],
|
|
47
|
+
'#t': ['termhub-repo'],
|
|
48
|
+
limit: 50
|
|
49
|
+
}];
|
|
50
|
+
for (const url of RELAYS) {
|
|
51
|
+
try {
|
|
52
|
+
const relay = await Relay.connect(url);
|
|
53
|
+
await new Promise((resolve) => {
|
|
54
|
+
const sub = relay.subscribe(filters, {
|
|
55
|
+
onevent(event) {
|
|
56
|
+
const infoHash = event.tags.find((t) => t[0] === 'h')?.[1];
|
|
57
|
+
const magnetURI = event.tags.find((t) => t[0] === 'm')?.[1];
|
|
58
|
+
const name = event.tags.find((t) => t[0] === 'n')?.[1];
|
|
59
|
+
const author = event.tags.find((t) => t[0] === 'a')?.[1];
|
|
60
|
+
if (infoHash && magnetURI && name && !seenHashes.has(infoHash)) {
|
|
61
|
+
if (name.toLowerCase().includes(query.toLowerCase())) {
|
|
62
|
+
seenHashes.add(infoHash);
|
|
63
|
+
results.push({
|
|
64
|
+
name,
|
|
65
|
+
infoHash,
|
|
66
|
+
magnetURI,
|
|
67
|
+
author: author || 'Anonymous',
|
|
68
|
+
timestamp: (event.created_at * 1000).toString()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
oneose() {
|
|
74
|
+
sub.close();
|
|
75
|
+
resolve();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Safety timeout
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
sub.close();
|
|
81
|
+
resolve();
|
|
82
|
+
}, 3000);
|
|
83
|
+
});
|
|
84
|
+
relay.close();
|
|
85
|
+
}
|
|
86
|
+
catch (err) { }
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
export async function getAllFromRegistry() {
|
|
91
|
+
return searchInRegistry('');
|
|
92
|
+
}
|
|
93
|
+
export async function checkNameAvailability(name, myPublicKey) {
|
|
94
|
+
const filters = [{
|
|
95
|
+
kinds: [1],
|
|
96
|
+
'#t': ['termhub-repo'],
|
|
97
|
+
'#a': [name],
|
|
98
|
+
limit: 10
|
|
99
|
+
}];
|
|
100
|
+
for (const url of RELAYS) {
|
|
101
|
+
try {
|
|
102
|
+
const relay = await Relay.connect(url);
|
|
103
|
+
const isAvailable = await new Promise((resolve) => {
|
|
104
|
+
let found = false;
|
|
105
|
+
let owner = false;
|
|
106
|
+
const sub = relay.subscribe(filters, {
|
|
107
|
+
onevent(event) {
|
|
108
|
+
found = true;
|
|
109
|
+
if (event.pubkey === myPublicKey)
|
|
110
|
+
owner = true;
|
|
111
|
+
},
|
|
112
|
+
oneose() {
|
|
113
|
+
sub.close();
|
|
114
|
+
resolve(!found || owner);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
sub.close();
|
|
119
|
+
resolve(true);
|
|
120
|
+
}, 2000);
|
|
121
|
+
});
|
|
122
|
+
relay.close();
|
|
123
|
+
if (!isAvailable)
|
|
124
|
+
return { available: false, owner: false };
|
|
125
|
+
}
|
|
126
|
+
catch (err) { }
|
|
127
|
+
}
|
|
128
|
+
return { available: true, owner: false };
|
|
129
|
+
}
|
|
130
|
+
export async function claimUsername(name, token) { }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.termhub');
|
|
5
|
+
const STARS_FILE = path.join(CONFIG_DIR, 'stars.json');
|
|
6
|
+
export async function initStars() {
|
|
7
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
8
|
+
if (!(await fs.pathExists(STARS_FILE))) {
|
|
9
|
+
await fs.writeJson(STARS_FILE, []);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function starRepository(name, magnet) {
|
|
13
|
+
const stars = await fs.readJson(STARS_FILE);
|
|
14
|
+
if (!stars.find(s => s.magnet === magnet)) {
|
|
15
|
+
stars.push({ name, magnet, timestamp: new Date().toISOString() });
|
|
16
|
+
await fs.writeJson(STARS_FILE, stars);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export async function getStars() {
|
|
22
|
+
return await fs.readJson(STARS_FILE);
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Repository } from '../types/index.js';
|
|
2
|
+
export declare function initStorage(): Promise<void>;
|
|
3
|
+
export declare function getConfig(): Promise<{
|
|
4
|
+
username: string;
|
|
5
|
+
privateKey: string;
|
|
6
|
+
publicKey: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function setConfig(config: {
|
|
9
|
+
username: string;
|
|
10
|
+
privateKey: string;
|
|
11
|
+
publicKey: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
export declare function saveRepository(repo: Repository): Promise<void>;
|
|
14
|
+
export declare function getRepositories(): Promise<Repository[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.termhub');
|
|
6
|
+
const REPOS_FILE = path.join(CONFIG_DIR, 'repositories.json');
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
+
// Helper to convert Uint8Array to Hex string
|
|
9
|
+
function bytesToHex(bytes) {
|
|
10
|
+
return Array.from(bytes)
|
|
11
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
12
|
+
.join('');
|
|
13
|
+
}
|
|
14
|
+
export async function initStorage() {
|
|
15
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
16
|
+
if (!(await fs.pathExists(REPOS_FILE))) {
|
|
17
|
+
await fs.writeJson(REPOS_FILE, []);
|
|
18
|
+
}
|
|
19
|
+
if (!(await fs.pathExists(CONFIG_FILE))) {
|
|
20
|
+
const sk = generateSecretKey();
|
|
21
|
+
const pk = getPublicKey(sk);
|
|
22
|
+
await fs.writeJson(CONFIG_FILE, {
|
|
23
|
+
username: 'Anonymous',
|
|
24
|
+
privateKey: bytesToHex(sk),
|
|
25
|
+
publicKey: pk
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function getConfig() {
|
|
30
|
+
return await fs.readJson(CONFIG_FILE);
|
|
31
|
+
}
|
|
32
|
+
export async function setConfig(config) {
|
|
33
|
+
await fs.writeJson(CONFIG_FILE, config);
|
|
34
|
+
}
|
|
35
|
+
export async function saveRepository(repo) {
|
|
36
|
+
const repos = await getRepositories();
|
|
37
|
+
repos.push(repo);
|
|
38
|
+
await fs.writeJson(REPOS_FILE, repos);
|
|
39
|
+
}
|
|
40
|
+
export async function getRepositories() {
|
|
41
|
+
return await fs.readJson(REPOS_FILE);
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "termhub-p2p",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Decentralized Terminal Hub - P2P file sharing and registry.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"termhub": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && node -e \"require('fs-extra').copySync('src/ui/public', 'dist/ui/public')\"",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"p2p",
|
|
21
|
+
"nostr",
|
|
22
|
+
"webtorrent",
|
|
23
|
+
"cli",
|
|
24
|
+
"share"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "^5.6.2",
|
|
30
|
+
"commander": "^14.0.3",
|
|
31
|
+
"enquirer": "^2.4.1",
|
|
32
|
+
"express": "^5.2.1",
|
|
33
|
+
"fs-extra": "^11.3.4",
|
|
34
|
+
"globby": "^16.2.0",
|
|
35
|
+
"ignore": "^7.0.5",
|
|
36
|
+
"nostr-tools": "^2.23.3",
|
|
37
|
+
"open": "^11.0.0",
|
|
38
|
+
"ora": "^9.3.0",
|
|
39
|
+
"webtorrent": "^2.8.5",
|
|
40
|
+
"ws": "^8.20.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@noble/hashes": "^2.2.0",
|
|
44
|
+
"@types/express": "^5.0.6",
|
|
45
|
+
"@types/fs-extra": "^11.0.4",
|
|
46
|
+
"@types/node": "^25.6.0",
|
|
47
|
+
"@types/webtorrent": "^0.110.1",
|
|
48
|
+
"@types/ws": "^8.18.1",
|
|
49
|
+
"tsx": "^4.21.0",
|
|
50
|
+
"typescript": "^6.0.2"
|
|
51
|
+
}
|
|
52
|
+
}
|