OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy commits / dbf6f8c

Refactor: rebuild on hyper-overlay + ESM + standalone examples (0.3.0)
Major in-progress refactor that's been on disk for weeks. Coherent slice — committing as a single landmark before any further work. Architectural shift: - Switched to ESM ("type": "module"), import-map keys re-keyed with #fs / #path / #os prefix (modern bare convention). - Added corestore + hyper-overlay file: dep — hyper-deploy is now a thin layer ON TOP of hyper-overlay rather than a parallel file-distribution implementation. Per the pear-git modular architecture plan (hyper-fleeky/pear-git-modular-architecture memory), hyper-deploy = hyper-overlay-with-release-semantics. File reorganization: - DELETE bin/hyper-deploy.js — CLI is now examples/cli.js per the standalone-CLI / standalone-web pattern (each module's examples use separate storage paths; orchestrator wires them together). - DELETE lib/scanner.js, lib/web.js — replaced by hyper-overlay primitives. - HEAVY rewrite of index.js (~724-line diff): old-style class replaced with a thin DeployServer + DeployClient pair that delegate file storage to hyper-overlay. - lib/protocol.js: trimmed message types now that overlay handles chunk transfer. ADD examples/cli.js + examples/web.js — standalone usage, separate storage env vars (DEPLOY_STORAGE_CLI / DEPLOY_STORAGE_WEB). Version bump 0.2.0 → 0.3.0 reflects the breaking interface change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dbf6f8c5561e3475baf0105c35a8f613df39dfe3
by plankatron <fleeky@gmail.com> · 2026-04-15T21:34:27.000Z

Changed files (9) +769 -1066

bin/hyper-deploy.js +0 -288
……@@ -1,288 +0,0 @@
1#!/usr/bin/env bare
2/**
3 * hyper-deploy CLI — P2P release distribution
4 *
5 * Commands:
6 * serve <dir> Serve a directory to peers
7 * pull <topic> [dest] Pull files from a server
8 * info <topic> Show remote manifest without pulling
9 * help Show usage
10 * version Show version
11 */
12
13await import('bare-node-runtime/global')
14
15import fs from 'bare-fs'
16import path from 'bare-path'
17import { DeployServer, DeployClient } from '../index.js'
18import { startWeb } from '../lib/web.js'
19import { diffManifest } from '../lib/scanner.js'
20
21const VERSION = '0.1.0'
22
23function fmt (b) {
24 if (b < 1024) return b + ' B'
25 if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
26 if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'
27 return (b / 1073741824).toFixed(2) + ' GB'
28}
29
30function parseFlags (args) {
31 const flags = {}
32 const positional = []
33 for (let i = 0; i < args.length; i++) {
34 if (args[i].startsWith('--')) {
35 const key = args[i].slice(2)
36 if (key === 'no-web' || key === 'no-verify' || key === 'dry-run' || key === 'verify') {
37 flags[key] = true
38 } else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
39 flags[key] = args[++i]
40 } else {
41 flags[key] = true
42 }
43 } else {
44 positional.push(args[i])
45 }
46 }
47 return { flags, positional }
48}
49
50const args = process.argv.slice(2)
51const command = args[0]
52
53if (!command || command === 'help' || command === '--help') {
54 console.log(`hyper-deploy v${VERSION} — P2P release distribution
55
56Commands:
57 serve <dir> Index and serve a directory to peers
58 pull <topic> [dest] Pull files from a server
59 info <topic> Show remote manifest without pulling
60 version Show version
61
62Serve options:
63 --topic <hex> Use a specific topic (default: random)
64 --web-port <port> Web UI port (default: 9090)
65 --no-web Disable web UI
66
67Pull options:
68 --timeout <ms> Connection timeout (default: 30000)
69 --no-verify Skip hash verification on received files
70 --dry-run Show what would transfer without doing it
71
72Examples:
73 hyper-deploy serve ./build/release
74 hyper-deploy pull abc123...def456 ./my-app
75 hyper-deploy pull abc123...def456 ./my-app --dry-run
76 hyper-deploy info abc123...def456
77`)
78 process.exit(0)
79}
80
81if (command === 'version' || command === '--version') {
82 console.log('hyper-deploy v' + VERSION)
83 process.exit(0)
84}
85
86// ─── SERVE ───────────────────────────────────────────────────
87
88if (command === 'serve') {
89 const { flags, positional } = parseFlags(args.slice(1))
90 const dir = positional[0]
91
92 if (!dir) {
93 console.error('Usage: hyper-deploy serve <dir>')
94 process.exit(1)
95 }
96
97 if (!fs.existsSync(dir)) {
98 console.error('Directory not found: ' + dir)
99 process.exit(1)
100 }
101
102 const topic = flags.topic ? Buffer.from(flags.topic, 'hex') : undefined
103
104 const server = new DeployServer(dir, { topic })
105
106 server.on('index-progress', (i, total) => {
107 if (i % 2000 === 0) console.log(` indexing ${i}/${total}...`)
108 })
109
110 server.on('indexed', (count, size) => {
111 console.log(`Indexed: ${count} files, ${fmt(size)}`)
112 })
113
114 server.on('ready', () => {
115 console.log(`\nServing on topic:`)
116 console.log(` ${server.topicHex}`)
117 console.log(`\nPull with:`)
118 console.log(` hyper-deploy pull ${server.topicHex} <dest-dir>`)
119 console.log(`\nWaiting for peers...`)
120 })
121
122 server.on('peer', ({ key, state }) => {
123 if (state === 'connected') console.log(`\nPeer ${key}: connected`)
124 else if (state === 'transferring') console.log(` Peer ${key}: transferring...`)
125 else if (state === 'done') console.log(` Peer ${key}: done`)
126 else if (state === 'disconnected') console.log(` Peer ${key}: disconnected`)
127 })
128
129 server.on('progress', (p) => {
130 if (p.filesServed && p.filesServed % 500 === 0) {
131 console.log(` ${p.filesServed} files, ${fmt(p.bytesServed)}`)
132 }
133 })
134
135 server.on('error', (err) => {
136 console.error('Error:', err.message)
137 })
138
139 await server.ready()
140
141 if (!flags['no-web']) {
142 const webPort = parseInt(flags['web-port']) || 9090
143 const { port } = startWeb(server, { port: webPort })
144 console.log(`\nWeb UI: http://localhost:${port}`)
145 }
146}
147
148// ─── PULL ────────────────────────────────────────────────────
149
150else if (command === 'pull') {
151 const { flags, positional } = parseFlags(args.slice(1))
152 const topicHex = positional[0]
153 const dest = positional[1] || './deploy-output'
154
155 if (!topicHex || topicHex.length !== 64) {
156 console.error('Usage: hyper-deploy pull <64-char-topic-hex> [dest-dir]')
157 process.exit(1)
158 }
159
160 const timeout = parseInt(flags.timeout) || 30000
161 const verify = !flags['no-verify']
162 const dryRun = !!flags['dry-run']
163
164 console.log(`Pulling to: ${path.resolve(dest)}`)
165 console.log(`Connecting to: ${topicHex.slice(0, 16)}...`)
166 if (dryRun) console.log('(dry run — no files will be transferred)')
167
168 const startTime = Date.now()
169 const client = new DeployClient(topicHex, dest, { timeout, verify })
170
171 client.on('connected', (key) => {
172 console.log(`Connected: ${key} (${Date.now() - startTime}ms)`)
173 })
174
175 client.on('manifest', (count) => {
176 console.log(`Manifest: ${count} files`)
177 })
178
179 client.on('diff', (needed, skipped) => {
180 console.log(`Need: ${needed}, already have: ${skipped}`)
181
182 if (dryRun) {
183 console.log('\n(dry run complete — would transfer ' + needed + ' files)')
184 client.close().then(() => process.exit(0))
185 }
186 })
187
188 client.on('up-to-date', () => {
189 console.log('Already up to date!')
190 })
191
192 client.on('progress', (p) => {
193 const total = p.filesTransferred + p.filesSkipped
194 if (p.filesTransferred % 500 === 0 || total === p.totalFiles) {
195 const elapsed = (Date.now() - startTime) / 1000
196 const pct = (total / p.totalFiles * 100).toFixed(0)
197 console.log(` ${pct}% — ${p.filesTransferred} transferred, ${fmt(p.bytesReceived)}, ${elapsed.toFixed(0)}s`)
198 }
199 })
200
201 client.on('error', (err) => {
202 console.error('Error:', err.message)
203 })
204
205 try {
206 await client.ready()
207
208 if (dryRun) {
209 // Wait for diff event to fire then exit (handled in diff listener above)
210 await new Promise(r => setTimeout(r, 60000))
211 } else {
212 const result = await client.pull()
213 const speed = result.bytesReceived > 0 ? (result.bytesReceived / result.elapsed * 1000 / 1024 / 1024).toFixed(1) : '0'
214 console.log(`\nComplete!`)
215 console.log(` Files: ${result.filesTransferred} transferred, ${result.filesSkipped} skipped`)
216 console.log(` Data: ${fmt(result.bytesReceived)}`)
217 console.log(` Time: ${(result.elapsed / 1000).toFixed(1)}s (${speed} MB/s)`)
218 if (result.errors.length > 0) {
219 console.log(` Errors: ${result.errors.length}`)
220 for (const e of result.errors) console.log(` ${e.path}: ${e.message}`)
221 }
222 }
223 } catch (err) {
224 console.error('\nFailed:', err.message)
225 process.exit(1)
226 }
227
228 // process.exit after close — libudx has a double-free bug in io_uring
229 // teardown that crashes when swarm.destroy() runs after real network transfers.
230 await client.close()
231 process.exit(0)
232}
233
234// ─── INFO ────────────────────────────────────────────────────
235
236else if (command === 'info') {
237 const topicHex = args[1]
238
239 if (!topicHex || topicHex.length !== 64) {
240 console.error('Usage: hyper-deploy info <64-char-topic-hex>')
241 process.exit(1)
242 }
243
244 console.log(`Connecting to: ${topicHex.slice(0, 16)}...`)
245
246 const client = new DeployClient(topicHex, '/tmp/hyper-deploy-info-dummy', { timeout: 15000 })
247
248 client.on('connected', (key) => {
249 console.log(`Connected: ${key}`)
250 })
251
252 client.on('manifest', (count) => {
253 // We have the manifest — print summary and exit
254 })
255
256 client.on('diff', (needed, skipped) => {
257 console.log(`\nRemote has ${needed + skipped} files`)
258 console.log(` Would transfer: ${needed}`)
259 console.log(` Already local: ${skipped}`)
260
261 client.close().then(() => process.exit(0))
262 })
263
264 client.on('up-to-date', () => {
265 console.log('Already up to date!')
266 client.close().then(() => process.exit(0))
267 })
268
269 try {
270 await client.ready()
271 await client.pull()
272 } catch (err) {
273 if (err.message.includes('closed during pull')) {
274 // Expected — we close after getting the manifest
275 } else {
276 console.error('Failed:', err.message)
277 process.exit(1)
278 }
279 }
280}
281
282// ─── UNKNOWN ─────────────────────────────────────────────────
283
284else {
285 console.error('Unknown command: ' + command)
286 console.error('Run: hyper-deploy help')
287 process.exit(1)
288}
examples/cli.js +114 -0
……@@ -0,0 +1,114 @@
1#!/usr/bin/env node
2/**
3 * Mini CLI example for hyper-deploy
4 *
5 * Usage:
6 * node examples/cli.js serve <dir> [--topic <hex>]
7 * node examples/cli.js pull <topic> [dest]
8 * node examples/cli.js info <topic>
9 */
10
11import { DeployServer, DeployClient } from '../index.js'
12
13function fmt (b) {
14 if (b < 1024) return b + ' B'
15 if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
16 if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'
17 return (b / 1073741824).toFixed(2) + ' GB'
18}
19
20const args = process.argv.slice(2)
21const cmd = args[0]
22
23if (cmd === 'serve') {
24 const dir = args[1]
25 if (!dir) { console.error('Usage: cli.js serve <dir>'); process.exit(1) }
26
27 let topic
28 const ti = args.indexOf('--topic')
29 if (ti !== -1 && args[ti + 1]) topic = Buffer.from(args[ti + 1], 'hex')
30
31 const server = new DeployServer(dir, { topic })
32
33 server.on('indexed', (count, size) => {
34 console.log('Indexed: %d files, %s', count, fmt(size))
35 })
36
37 server.on('ready', () => {
38 console.log('Topic: %s', server.topicHex)
39 console.log('Pull: node examples/cli.js pull %s ./dest', server.topicHex)
40 console.log('Waiting for peers...')
41 })
42
43 server.on('peer', ({ key, state }) => console.log('Peer %s: %s', key, state))
44
45 server.on('progress', (p) => {
46 if (p.file) console.log(' sent %s (%d files, %s)', p.file, p.filesServed, fmt(p.bytesServed))
47 })
48
49 server.ready().catch((err) => { console.error(err.message); process.exit(1) })
50} else if (cmd === 'pull') {
51 const topicHex = args[1]
52 const dest = args[2] || './deploy-output'
53 if (!topicHex || topicHex.length !== 64) {
54 console.error('Usage: cli.js pull <64-char-hex> [dest]')
55 process.exit(1)
56 }
57
58 const client = new DeployClient(topicHex, dest)
59 const t0 = Date.now()
60
61 client.on('connected', (key) => console.log('Connected: %s (%dms)', key, Date.now() - t0))
62 client.on('manifest', (n) => console.log('Manifest: %d files', n))
63 client.on('diff', (needed, skipped) => console.log('Need: %d, skip: %d', needed, skipped))
64 client.on('up-to-date', () => console.log('Up to date'))
65
66 client.on('progress', (p) => {
67 const total = p.filesTransferred + p.filesSkipped
68 if (p.filesTransferred % 100 === 0 || total === p.totalFiles) {
69 console.log(' %d/%d transferred, %s', p.filesTransferred, p.totalFiles, fmt(p.bytesReceived))
70 }
71 })
72
73 client.ready()
74 .then(() => client.pull())
75 .then((stats) => {
76 const speed = stats.bytesReceived > 0 ? (stats.bytesReceived / stats.elapsed * 1000 / 1024 / 1024).toFixed(1) : '0'
77 console.log('Done: %d files, %s, %ss (%s MB/s)',
78 stats.filesTransferred, fmt(stats.bytesReceived),
79 (stats.elapsed / 1000).toFixed(1), speed)
80 if (stats.errors.length) console.log('Errors:', stats.errors)
81 return client.close()
82 })
83 .then(() => process.exit(0))
84 .catch((err) => { console.error(err.message); process.exit(1) })
85} else if (cmd === 'info') {
86 const topicHex = args[1]
87 if (!topicHex || topicHex.length !== 64) {
88 console.error('Usage: cli.js info <64-char-hex>')
89 process.exit(1)
90 }
91
92 const client = new DeployClient(topicHex, '/tmp/hyper-deploy-info', { timeout: 15000 })
93
94 client.on('connected', (key) => console.log('Connected: %s', key))
95 client.on('manifest', (n) => console.log('Files: %d', n))
96 client.on('diff', (needed, skipped) => {
97 console.log('Would transfer: %d, already local: %d', needed, skipped)
98 client.close().then(() => process.exit(0))
99 })
100
101 client.ready()
102 .then(() => client.pull())
103 .catch((err) => {
104 if (!err.message.includes('closed during pull')) {
105 console.error(err.message)
106 process.exit(1)
107 }
108 })
109} else {
110 console.log('hyper-deploy example CLI')
111 console.log(' serve <dir> [--topic <hex>] Serve a directory')
112 console.log(' pull <topic> [dest] Pull files from server')
113 console.log(' info <topic> Show remote manifest')
114}
examples/web.js +132 -0
……@@ -0,0 +1,132 @@
1#!/usr/bin/env node
2/**
3 * Mini web UI example for hyper-deploy
4 *
5 * Usage:
6 * node examples/web.js <dir> [--port 9090] [--topic <hex>]
7 *
8 * Starts a deploy server and an HTTP dashboard showing live status.
9 */
10
11import http from 'http'
12import { DeployServer } from '../index.js'
13
14const args = process.argv.slice(2)
15const dir = args[0]
16if (!dir) { console.error('Usage: web.js <dir> [--port N]'); process.exit(1) }
17
18let port = 9090
19let topic
20for (let i = 1; i < args.length; i++) {
21 if (args[i] === '--port' && args[i + 1]) port = parseInt(args[++i])
22 if (args[i] === '--topic' && args[i + 1]) topic = Buffer.from(args[++i], 'hex')
23}
24
25function fmt (b) {
26 if (b < 1024) return b + ' B'
27 if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
28 if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'
29 return (b / 1073741824).toFixed(2) + ' GB'
30}
31
32const server = new DeployServer(dir, { topic })
33const sseClients = new Set()
34
35function broadcast (event, data) {
36 const msg = 'event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n'
37 for (const res of sseClients) {
38 try { res.write(msg) } catch { sseClients.delete(res) }
39 }
40}
41
42server.on('peer', (info) => broadcast('peer', info))
43server.on('progress', (info) => broadcast('progress', info))
44
45function getStatus () {
46 return {
47 dir: server.dir,
48 topic: server.topicHex,
49 files: server.manifest ? server.manifest.files.length : 0,
50 totalSize: server.manifest ? fmt(server.manifest.totalSize) : '0',
51 connections: server.connections,
52 filesServed: server.stats.filesServed,
53 bytesServed: fmt(server.stats.bytesServed)
54 }
55}
56
57const httpServer = http.createServer((req, res) => {
58 const url = req.url.split('?')[0]
59
60 if (url === '/api/status') {
61 res.writeHead(200, { 'Content-Type': 'application/json' })
62 res.end(JSON.stringify(getStatus()))
63 return
64 }
65
66 if (url === '/events') {
67 res.writeHead(200, {
68 'Content-Type': 'text/event-stream',
69 'Cache-Control': 'no-cache',
70 Connection: 'keep-alive'
71 })
72 res.write('data: ' + JSON.stringify(getStatus()) + '\n\n')
73 sseClients.add(res)
74 req.on('close', () => sseClients.delete(res))
75 return
76 }
77
78 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
79 res.end(`<!DOCTYPE html>
80<html><head><title>hyper-deploy</title>
81<style>
82 body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 640px; margin: 0 auto; }
83 h1 { color: #58a6ff; }
84 .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
85 .row { display: flex; justify-content: space-between; padding: 0.3rem 0; }
86 .label { color: #8b949e; }
87 .topic { font-size: 0.75rem; word-break: break-all; color: #79c0ff; }
88 #log { max-height: 300px; overflow-y: auto; font-size: 0.8rem; color: #8b949e; }
89 #log div { padding: 2px 0; border-bottom: 1px solid #21262d; }
90</style></head><body>
91<h1>hyper-deploy</h1>
92<div class="card">
93 <div class="topic" id="topic">...</div>
94</div>
95<div class="card">
96 <div class="row"><span class="label">Files</span><span id="files">-</span></div>
97 <div class="row"><span class="label">Size</span><span id="size">-</span></div>
98 <div class="row"><span class="label">Peers</span><span id="peers">0</span></div>
99 <div class="row"><span class="label">Served</span><span id="served">0 files</span></div>
100 <div class="row"><span class="label">Bytes</span><span id="bytes">0 B</span></div>
101</div>
102<div class="card"><h3 style="color:#8b949e;margin:0 0 0.5rem">Log</h3><div id="log"></div></div>
103<script>
104const $ = id => document.getElementById(id)
105function log(msg) {
106 const d = document.createElement('div'); d.textContent = msg
107 $('log').prepend(d)
108 while ($('log').children.length > 100) $('log').lastChild.remove()
109}
110function update(d) {
111 if (d.topic) $('topic').textContent = d.topic
112 if (d.files !== undefined) $('files').textContent = d.files
113 if (d.totalSize) $('size').textContent = d.totalSize
114 if (d.connections !== undefined) $('peers').textContent = d.connections
115 if (d.filesServed !== undefined) $('served').textContent = d.filesServed + ' files'
116 if (d.bytesServed) $('bytes').textContent = d.bytesServed
117}
118const es = new EventSource('/events')
119es.onmessage = e => update(JSON.parse(e.data))
120es.addEventListener('peer', e => { const d = JSON.parse(e.data); log('Peer ' + d.key + ': ' + d.state) })
121es.addEventListener('progress', e => { const d = JSON.parse(e.data); if (d.file) log('sent ' + d.file) })
122</script></body></html>`)
123})
124
125server.ready().then(() => {
126 httpServer.listen(port, () => {
127 console.log('Serving: %s', server.dir)
128 console.log('Topic: %s', server.topicHex)
129 console.log('Files: %d (%s)', server.manifest.files.length, fmt(server.manifest.totalSize))
130 console.log('Web UI: http://localhost:%d', port)
131 })
132}).catch((err) => { console.error(err.message); process.exit(1) })
index.js +410 -314
……@@ -1,12 +1,12 @@
11/**
2 * hyper-deploy — P2P release distribution
2 * hyper-deploy — P2P release distribution built on hyper-overlay
33 *
44 * Serves build artifacts from a directory to peers over Hyperswarm.
5 * File-level dedup: client compares content hashes, only pulls what changed.
6 * Backpressure via Protomux ondrain. Hash verification on received files.
5 * File-level dedup via content hashing. Files >= 1MB get FastCDC chunking
6 * for resumable transfers. Smaller files transfer as whole blobs.
77 *
88 * Usage:
9 * const { DeployServer, DeployClient } = require('hyper-deploy')
9 * import { DeployServer, DeployClient } from 'hyper-deploy'
1010 *
1111 * const server = new DeployServer(dir)
1212 * await server.ready()
……@@ -16,221 +16,292 @@
1616 * const result = await client.pull()
1717 */
1818
19const Hyperswarm = require('hyperswarm');
20const crypto = require('hypercore-crypto');
21const fs = require('fs');
22const path = require('path');
23const ReadyResource = require('ready-resource');
24const safetyCatch = require('safety-catch');
19import Hyperswarm from 'hyperswarm'
20import Corestore from 'corestore'
21import crypto from 'hypercore-crypto'
22import fs from '#fs'
23import path from '#path'
24import os from '#os'
25import ReadyResource from 'ready-resource'
26import safetyCatch from 'safety-catch'
27
28import { FileIndex } from 'hyper-overlay/file-index'
29import { TransferManager } from 'hyper-overlay/transfer'
30import { hashChunk } from 'hyper-overlay/chunker'
31import { attachProtocol } from './lib/protocol.js'
32
33const CHUNK_THRESHOLD = 1048576 // 1 MB — files below this transfer as whole blobs
34
35// ── Helpers ──────────────────────────────────────────────────
36
37function walkDir (dir, prefix) {
38 prefix = prefix || ''
39 const results = []
40 for (const entry of fs.readdirSync(dir)) {
41 const full = path.join(dir, entry)
42 const rel = prefix ? prefix + '/' + entry : entry
43 try {
44 const stat = fs.statSync(full)
45 if (stat.isDirectory()) results.push(...walkDir(full, rel))
46 else if (stat.isFile()) results.push({ path: rel, size: stat.size })
47 } catch {}
48 }
49 return results
50}
2551
26const { buildManifest, diffManifest, readChunk, hashFile } = require('./lib/scanner');
27const { attachProtocol, PROTOCOL_VERSION, CHUNK_SIZE } = require('./lib/protocol');
52function hashFile (filePath) {
53 return hashChunk(fs.readFileSync(filePath))
54}
2855
29// ─── DeployServer ────────────────────────────────────────────
56// ── DeployServer ─────────────────────────────────────────────
3057
31class DeployServer extends ReadyResource {
58export class DeployServer extends ReadyResource {
3259 constructor (dir, opts) {
33 super();
34 opts = opts || {};
35
36 this.dir = path.resolve(dir);
37 this.topic = opts.topic || crypto.randomBytes(32);
38 this.swarm = opts.swarm || null;
39 this._ownSwarm = !this.swarm;
40 this.manifest = null;
41 this._manifestHash = null;
42 this._discovery = null;
43 this._sessions = new Set();
44
45 this.stats = { totalConnections: 0, filesServed: 0, bytesServed: 0 };
46
47 this.ready().catch(safetyCatch);
60 super()
61 opts = opts || {}
62
63 this.dir = path.resolve(dir)
64 this.topic = opts.topic || crypto.randomBytes(32)
65 this.swarm = opts.swarm || null
66 this._ownSwarm = !this.swarm
67 this._corestore = opts.corestore || null
68 this._ownStore = !this._corestore
69 this._fileIndex = null
70 this._transferManager = null
71 this._discovery = null
72 this._sessions = new Set()
73
74 this.manifest = null
75 this.stats = { totalConnections: 0, filesServed: 0, bytesServed: 0 }
76
77 this.ready().catch(safetyCatch)
4878 }
4979
5080 async _open () {
5181 if (!fs.existsSync(this.dir)) {
52 throw new Error('Directory not found: ' + this.dir);
82 throw new Error('Directory not found: ' + this.dir)
83 }
84
85 // Setup corestore + overlay primitives
86 if (!this._corestore) {
87 const tmp = path.join(os.tmpdir(), 'hyper-deploy-srv-' + Date.now())
88 this._corestore = new Corestore(tmp)
5389 }
5490
55 await this._scan();
91 this._fileIndex = new FileIndex(this._corestore)
92 await this._fileIndex.ready()
93 this._transferManager = new TransferManager(this._fileIndex)
94
95 // Index directory
96 await this._scan()
5697
57 if (!this.swarm) this.swarm = new Hyperswarm();
58 this.swarm.on('connection', (conn) => this._onConnection(conn));
98 // Start swarm
99 if (!this.swarm) this.swarm = new Hyperswarm()
100 this.swarm.on('connection', (conn) => this._onConnection(conn))
59101
60 this._discovery = this.swarm.join(this.topic, { server: true, client: false });
61 await this._discovery.flushed();
102 this._discovery = this.swarm.join(this.topic, { server: true, client: false })
103 await this._discovery.flushed()
62104
63 this.emit('ready');
105 this.emit('ready')
64106 }
65107
66108 async _close () {
67109 for (const session of this._sessions) {
68 session.destroyed = true;
110 session.destroyed = true
69111 }
70 this._sessions.clear();
112 this._sessions.clear()
71113
72114 if (this._discovery) {
73 await this._discovery.destroy();
74 this._discovery = null;
115 await this._discovery.destroy()
116 this._discovery = null
75117 }
76118 if (this._ownSwarm && this.swarm) {
77 await this.swarm.destroy();
78 this.swarm = null;
119 await this.swarm.destroy()
120 this.swarm = null
79121 }
122 if (this._fileIndex) await this._fileIndex.close()
123 if (this._ownStore && this._corestore) await this._corestore.close()
80124 }
81125
82126 get topicHex () {
83 return this.topic.toString('hex');
127 return this.topic.toString('hex')
84128 }
85129
86130 get connections () {
87 return this._sessions.size;
131 return this._sessions.size
88132 }
89133
90134 async rescan () {
91 await this._scan();
92 return { files: this.manifest.files.length, totalSize: this.manifest.totalSize };
135 await this._scan()
136 return { files: this.manifest.files.length, totalSize: this.manifest.totalSize }
93137 }
94138
95139 async _scan () {
96 this.manifest = buildManifest(this.dir, {
97 onProgress: (i, total) => {
98 if (i % 1000 === 0) this.emit('index-progress', i, total);
140 const files = walkDir(this.dir)
141 let totalSize = 0
142
143 for (let i = 0; i < files.length; i++) {
144 const f = files[i]
145 const fullPath = path.join(this.dir, f.path)
146 const contentHash = hashFile(fullPath)
147
148 await this._fileIndex.putFile(f.path, {
149 contentHash,
150 size: f.size,
151 mtime: fs.statSync(fullPath).mtimeMs
152 })
153
154 // For large files, prepare chunk map via TransferManager
155 if (f.size >= CHUNK_THRESHOLD) {
156 await this._transferManager.prepareFile(fullPath, f.path)
99157 }
100 });
101158
102 const manifestJson = JSON.stringify(this.manifest.files.map(f => f.path + ':' + f.hash));
103 this._manifestHash = crypto.data(Buffer.from(manifestJson)).toString('hex');
159 totalSize += f.size
160 if ((i + 1) % 1000 === 0) this.emit('index-progress', i + 1, files.length)
161 }
162
163 this.manifest = {
164 files: files.map(f => ({
165 path: f.path,
166 size: f.size,
167 hash: null // populated lazily from fileIndex
168 })),
169 totalSize
170 }
171
172 // Populate hashes from the index
173 for (const f of this.manifest.files) {
174 const meta = await this._fileIndex.getFile(f.path)
175 if (meta) f.hash = meta.contentHash
176 }
104177
105 this.emit('indexed', this.manifest.files.length, this.manifest.totalSize);
178 this.emit('indexed', this.manifest.files.length, totalSize)
106179 }
107180
108181 _onConnection (conn) {
109182 const peerKey = conn.remotePublicKey
110183 ? conn.remotePublicKey.toString('hex').slice(0, 12)
111 : 'unknown';
184 : 'unknown'
112185
113 this.stats.totalConnections++;
186 this.stats.totalConnections++
114187
115 const proto = attachProtocol(conn);
188 const proto = attachProtocol(conn)
116189 if (!proto) {
117 this.emit('error', new Error('Failed to attach protocol for peer ' + peerKey));
118 return;
190 this.emit('error', new Error('Failed to attach protocol for peer ' + peerKey))
191 return
119192 }
120193
121 const session = { peerKey, destroyed: false, filesServed: 0, bytesServed: 0 };
122 this._sessions.add(session);
123 this.emit('peer', { key: peerKey, state: 'connected' });
194 const session = { peerKey, destroyed: false, filesServed: 0, bytesServed: 0 }
195 this._sessions.add(session)
196 this.emit('peer', { key: peerKey, state: 'connected' })
124197
125198 conn.on('close', () => {
126 this._sessions.delete(session);
127 this.emit('peer', { key: peerKey, state: 'disconnected' });
128 });
199 this._sessions.delete(session)
200 this.emit('peer', { key: peerKey, state: 'disconnected' })
201 })
129202
130203 conn.on('error', (err) => {
131 this._sessions.delete(session);
132 this.emit('error', err);
133 });
134
135 proto.sendMeta({
136 type: 'manifest',
137 version: PROTOCOL_VERSION,
138 files: this.manifest.files,
139 manifestHash: this._manifestHash
140 }).catch(safetyCatch);
141
142 proto.metaMsg.onmessage = async (raw) => {
143 if (session.destroyed) return;
144
145 let msg;
146 try { msg = proto.parseMeta(raw); } catch { return; }
147
148 if (msg.type === 'up-to-date') {
149 this.emit('peer', { key: peerKey, state: 'done' });
150 return;
204 this._sessions.delete(session)
205 this.emit('error', err)
206 })
207
208 // Send file list
209 const fileList = this.manifest.files.map(f => ({
210 path: f.path, contentHash: f.hash, size: f.size
211 }))
212 proto.sendFileList(fileList).catch(safetyCatch)
213
214 // Handle file requests
215 proto.onFileRequest(async (req) => {
216 if (session.destroyed) return
217
218 this.emit('peer', { key: peerKey, state: 'transferring' })
219
220 const fullPath = path.join(this.dir, req.path)
221 let stat
222 try { stat = fs.statSync(fullPath) } catch {
223 await proto.sendControl({ type: 'error', message: 'File not found', path: req.path })
224 return
151225 }
152226
153 if (msg.type === 'requests') {
154 this.emit('peer', { key: peerKey, state: 'transferring' });
155
156 const requested = new Set(msg.paths);
157 this.emit('progress', {
158 peer: peerKey,
159 requested: requested.size,
160 skipped: this.manifest.files.length - requested.size
161 });
162
163 for (const f of this.manifest.files) {
164 if (session.destroyed) break;
165 if (!requested.has(f.path)) continue;
166
167 const fullPath = path.join(this.dir, f.path);
168 let stat;
169 try { stat = fs.statSync(fullPath); } catch {
170 await proto.sendMeta({ type: 'error', message: 'File not found', path: f.path });
171 continue;
172 }
173
174 await proto.sendMeta({ type: 'file-start', path: f.path, size: stat.size, hash: f.hash });
175
176 let fd;
177 try {
178 fd = fs.openSync(fullPath, 'r');
179 for (let off = 0; off < stat.size; off += CHUNK_SIZE) {
180 if (session.destroyed) break;
181 const len = Math.min(CHUNK_SIZE, stat.size - off);
182 const buf = Buffer.alloc(len);
183 fs.readSync(fd, buf, 0, len, off);
184 await proto.sendData(buf);
185 session.bytesServed += len;
186 this.stats.bytesServed += len;
187 }
188 } finally {
189 if (fd !== undefined) try { fs.closeSync(fd); } catch {}
190 }
191
192 await proto.sendMeta({ type: 'file-end', path: f.path });
227 if (stat.size >= CHUNK_THRESHOLD) {
228 // Large file: use TransferManager chunking
229 const prepared = await this._transferManager.prepareFile(fullPath, req.path)
230 if (!prepared) {
231 await proto.sendControl({ type: 'error', message: 'Prepare failed', path: req.path })
232 return
233 }
193234
194 session.filesServed++;
195 this.stats.filesServed++;
196 this.emit('progress', {
197 peer: peerKey,
198 file: f.path,
199 filesServed: session.filesServed,
200 bytesServed: session.bytesServed
201 });
235 await proto.sendChunkInfo({
236 path: req.path,
237 size: prepared.size,
238 chunks: prepared.chunks
239 })
240
241 for (let i = 0; i < prepared.chunks.length; i++) {
242 if (session.destroyed) break
243 const bytes = this._transferManager.readChunk(fullPath, prepared.chunks[i].offset, prepared.chunks[i].length)
244 if (!bytes) continue
245 await proto.sendChunkData(i, bytes)
246 session.bytesServed += bytes.length
247 this.stats.bytesServed += bytes.length
202248 }
249 } else {
250 // Small file: send as single whole-file blob
251 await proto.sendChunkInfo({
252 path: req.path,
253 size: stat.size,
254 chunks: [{ hash: hashFile(fullPath), offset: 0, length: stat.size }]
255 })
256
257 const data = fs.readFileSync(fullPath)
258 await proto.sendChunkData(0, data)
259 session.bytesServed += data.length
260 this.stats.bytesServed += data.length
261 }
203262
204 await proto.sendMeta({
205 type: 'transfer-complete',
206 filesServed: session.filesServed,
207 bytesServed: session.bytesServed
208 });
263 await proto.sendControl({ type: 'file-end', path: req.path })
209264
210 this.emit('peer', { key: peerKey, state: 'done' });
211 }
265 session.filesServed++
266 this.stats.filesServed++
267 this.emit('progress', {
268 peer: peerKey,
269 file: req.path,
270 filesServed: session.filesServed,
271 bytesServed: session.bytesServed
272 })
273 })
212274
213 if (msg.type === 'done') {
214 this._sessions.delete(session);
275 // Handle control messages
276 proto.onControl((msg) => {
277 if (msg.type === 'up-to-date') {
278 this.emit('peer', { key: peerKey, state: 'done' })
279 } else if (msg.type === 'done') {
280 this.emit('peer', { key: peerKey, state: 'done' })
281 this._sessions.delete(session)
215282 }
216 };
283 })
217284 }
218285}
219286
220// ─── DeployClient ────────────────────────────────────────────
287// ── DeployClient ─────────────────────────────────────────────
221288
222class DeployClient extends ReadyResource {
289export class DeployClient extends ReadyResource {
223290 constructor (topic, destDir, opts) {
224 super();
225 opts = opts || {};
226
227 this.topic = typeof topic === 'string' ? Buffer.from(topic, 'hex') : topic;
228 this.destDir = path.resolve(destDir);
229 this.swarm = opts.swarm || null;
230 this._ownSwarm = !this.swarm;
231 this._timeout = opts.timeout !== undefined ? opts.timeout : 30000;
232 this._verify = opts.verify !== false;
233 this.connected = false;
291 super()
292 opts = opts || {}
293
294 this.topic = typeof topic === 'string' ? Buffer.from(topic, 'hex') : topic
295 this.destDir = path.resolve(destDir)
296 this.swarm = opts.swarm || null
297 this._ownSwarm = !this.swarm
298 this._corestore = opts.corestore || null
299 this._ownStore = !this._corestore
300 this._fileIndex = null
301 this._transferManager = null
302 this._timeout = opts.timeout !== undefined ? opts.timeout : 30000
303 this._verify = opts.verify !== false
304 this.connected = false
234305
235306 this.stats = {
236307 filesTransferred: 0,
……@@ -239,236 +310,261 @@ class DeployClient extends ReadyResource {
239310 totalFiles: 0,
240311 elapsed: 0,
241312 errors: []
242 };
243
244 this._pullResolve = null;
245 this._pullReject = null;
246 this._startTime = null;
247 this._currentFd = null;
248 this._currentPath = null;
249 this._currentHash = null;
250 this._currentWritten = 0;
251 this._connectTimer = null;
252
253 this.ready().catch(safetyCatch);
313 }
314
315 this._pullResolve = null
316 this._pullReject = null
317 this._startTime = null
318 this._connectTimer = null
319 this._completed = false
320 this._completedStats = null
321
322 this.ready().catch(safetyCatch)
254323 }
255324
256325 async _open () {
257 fs.mkdirSync(this.destDir, { recursive: true });
326 fs.mkdirSync(this.destDir, { recursive: true })
258327
259 if (!this.swarm) this.swarm = new Hyperswarm();
260 this.swarm.on('connection', (conn) => this._onConnection(conn));
328 // Setup corestore + overlay primitives
329 if (!this._corestore) {
330 const tmp = path.join(os.tmpdir(), 'hyper-deploy-cli-' + Date.now())
331 this._corestore = new Corestore(tmp)
332 }
333
334 this._fileIndex = new FileIndex(this._corestore)
335 await this._fileIndex.ready()
336 this._transferManager = new TransferManager(this._fileIndex)
337
338 // Start swarm
339 if (!this.swarm) this.swarm = new Hyperswarm()
340 this.swarm.on('connection', (conn) => this._onConnection(conn))
261341
262 this.swarm.join(this.topic, { server: false, client: true });
263 await this.swarm.flush();
342 this.swarm.join(this.topic, { server: false, client: true })
343 await this.swarm.flush()
264344 }
265345
266346 async _close () {
267 if (this._currentFd !== null) {
268 try { fs.closeSync(this._currentFd); } catch {}
269 this._currentFd = null;
270 }
271
272347 if (this._connectTimer) {
273 clearTimeout(this._connectTimer);
274 this._connectTimer = null;
348 clearTimeout(this._connectTimer)
349 this._connectTimer = null
275350 }
276351
277352 if (this._pullReject) {
278 this._pullReject(new Error('Client closed during pull'));
279 this._pullReject = null;
280 this._pullResolve = null;
353 this._pullReject(new Error('Client closed during pull'))
354 this._pullReject = null
355 this._pullResolve = null
281356 }
282357
358 if (this._fileIndex) await this._fileIndex.close()
359 if (this._ownStore && this._corestore) await this._corestore.close()
360
283361 if (this._ownSwarm && this.swarm) {
284 await this.swarm.destroy();
285 this.swarm = null;
362 await this.swarm.destroy()
363 this.swarm = null
286364 }
287365 }
288366
289367 pull () {
290 if (this._pullResolve) return Promise.reject(new Error('Pull already in progress'));
368 // Already completed (transfer happened during ready/connection)
369 if (this._completed) return Promise.resolve(this._completedStats)
370 if (this._pullResolve) return Promise.reject(new Error('Pull already in progress'))
291371
292 this._startTime = Date.now();
372 if (!this._startTime) this._startTime = Date.now()
293373
294374 return new Promise((resolve, reject) => {
295 this._pullResolve = resolve;
296 this._pullReject = reject;
375 this._pullResolve = resolve
376 this._pullReject = reject
297377
298378 if (this._timeout > 0 && !this.connected) {
299379 this._connectTimer = setTimeout(() => {
300380 if (!this.connected && this._pullReject) {
301 this._pullReject(new Error('Connection timeout after ' + this._timeout + 'ms'));
302 this._pullReject = null;
303 this._pullResolve = null;
381 this._pullReject(new Error('Connection timeout after ' + this._timeout + 'ms'))
382 this._pullReject = null
383 this._pullResolve = null
304384 }
305 }, this._timeout);
385 }, this._timeout)
306386 }
307 });
387 })
308388 }
309389
310390 _finish () {
311 this.stats.elapsed = Date.now() - this._startTime;
391 if (this._completed) return
392 this.stats.elapsed = Date.now() - (this._startTime || Date.now())
393 this._completed = true
394 this._completedStats = { ...this.stats }
395
312396 if (this._pullResolve) {
313 const resolve = this._pullResolve;
314 this._pullResolve = null;
315 this._pullReject = null;
316 resolve({ ...this.stats });
397 const resolve = this._pullResolve
398 this._pullResolve = null
399 this._pullReject = null
400 resolve(this._completedStats)
317401 }
318 this.emit('complete', { ...this.stats });
402 this.emit('complete', this._completedStats)
319403 }
320404
321405 _fail (err) {
322 this.stats.elapsed = Date.now() - (this._startTime || Date.now());
323 this.emit('error', err);
406 this.stats.elapsed = Date.now() - (this._startTime || Date.now())
407 this.emit('error', err)
324408 if (this._pullReject) {
325 const reject = this._pullReject;
326 this._pullReject = null;
327 this._pullResolve = null;
328 reject(err);
409 const reject = this._pullReject
410 this._pullReject = null
411 this._pullResolve = null
412 reject(err)
329413 }
330414 }
331415
332416 _onConnection (conn) {
333417 if (this._connectTimer) {
334 clearTimeout(this._connectTimer);
335 this._connectTimer = null;
418 clearTimeout(this._connectTimer)
419 this._connectTimer = null
336420 }
337421
338 this.connected = true;
422 this.connected = true
423 if (!this._startTime) this._startTime = Date.now()
339424 const peerKey = conn.remotePublicKey
340425 ? conn.remotePublicKey.toString('hex').slice(0, 12)
341 : 'unknown';
342 this.emit('connected', peerKey);
426 : 'unknown'
427 this.emit('connected', peerKey)
343428
344429 conn.on('close', () => {
345 this.connected = false;
430 this.connected = false
346431 if (this._pullReject && this.stats.filesTransferred + this.stats.filesSkipped < this.stats.totalFiles) {
347 this._fail(new Error('Connection lost during transfer'));
432 this._fail(new Error('Connection lost during transfer'))
348433 }
349 });
434 })
350435
351436 conn.on('error', (err) => {
352 this.connected = false;
353 this._fail(err);
354 });
437 this.connected = false
438 this._fail(err)
439 })
355440
356 const proto = attachProtocol(conn);
441 const proto = attachProtocol(conn)
357442 if (!proto) {
358 this._fail(new Error('Failed to attach protocol'));
359 return;
443 this._fail(new Error('Failed to attach protocol'))
444 return
360445 }
361446
362 proto.metaMsg.onmessage = (raw) => {
363 let msg;
364 try { msg = proto.parseMeta(raw); } catch {
365 this._fail(new Error('Invalid protocol message'));
366 return;
367 }
447 // State for current file transfer
448 let pendingFiles = []
449 let currentFileIdx = 0
450 let currentPath = null
368451
369 if (msg.type === 'manifest') this._handleManifest(msg, proto);
370 else if (msg.type === 'file-start') this._handleFileStart(msg);
371 else if (msg.type === 'file-end') this._handleFileEnd(msg);
372 else if (msg.type === 'transfer-complete') this._handleTransferComplete(msg, proto);
373 else if (msg.type === 'error') {
374 this.stats.errors.push({ path: msg.path, message: msg.message });
375 this.emit('error', new Error(msg.message));
376 }
377 };
452 // Receive file list from server, diff against local
453 proto.onFileList((files) => {
454 this.stats.totalFiles = files.length
455 this.emit('manifest', files.length)
378456
379 proto.dataMsg.onmessage = (chunk) => {
380 if (this._currentFd !== null) {
457 // Diff: which files do we need?
458 const needed = []
459 let skipped = 0
460
461 for (const f of files) {
462 const localPath = path.join(this.destDir, f.path)
381463 try {
382 fs.writeSync(this._currentFd, chunk, 0, chunk.length, this._currentWritten);
383 this._currentWritten += chunk.length;
384 this.stats.bytesReceived += chunk.length;
385 } catch (err) {
386 this._fail(new Error('Write failed: ' + err.message));
387 }
464 const stat = fs.statSync(localPath)
465 if (stat.size === f.size) {
466 const localHash = hashFile(localPath)
467 if (localHash === f.contentHash) {
468 skipped++
469 continue
470 }
471 }
472 } catch {}
473 needed.push(f)
388474 }
389 };
390 }
391475
392 _handleManifest (msg, proto) {
393 if (msg.version && msg.version !== PROTOCOL_VERSION) {
394 this._fail(new Error('Protocol version mismatch: expected ' + PROTOCOL_VERSION + ', got ' + msg.version));
395 return;
396 }
397
398 this.stats.totalFiles = msg.files.length;
399 this.emit('manifest', msg.files.length);
476 this.stats.filesSkipped = skipped
477 this.emit('diff', needed.length, skipped)
400478
401 const { needed, skipped } = diffManifest(msg.files, this.destDir);
402 this.stats.filesSkipped = skipped;
403 this.emit('diff', needed.length, skipped);
404
405 if (needed.length === 0) {
406 proto.sendMeta({ type: 'up-to-date' }).catch(safetyCatch);
407 this.emit('up-to-date');
408 this._finish();
409 return;
410 }
479 if (needed.length === 0) {
480 proto.sendControl({ type: 'up-to-date' }).catch(safetyCatch)
481 this.emit('up-to-date')
482 this._finish()
483 return
484 }
411485
412 proto.sendMeta({ type: 'requests', paths: needed }).catch(safetyCatch);
413 }
486 pendingFiles = needed
487 currentFileIdx = 0
488
489 // Request first file
490 proto.sendFileRequest(needed[0].path).catch(safetyCatch)
491 })
492
493 // Receive chunk info for a file — setup TransferManager receive
494 proto.onChunkInfo((info) => {
495 currentPath = info.path
496 const targetPath = path.join(this.destDir, info.path)
497 this._transferManager.startReceive(targetPath, {
498 size: info.size,
499 chunks: info.chunks
500 })
501 })
502
503 // Receive chunk data
504 proto.onChunkData((index, data) => {
505 if (!currentPath) return
506 const targetPath = path.join(this.destDir, currentPath)
507
508 const result = this._transferManager.writeChunk(targetPath, index, data)
509 if (!result.ok) {
510 this.stats.errors.push({ path: currentPath, message: result.error })
511 this.emit('error', new Error(result.error))
512 }
414513
415 _handleFileStart (msg) {
416 const targetPath = path.resolve(path.join(this.destDir, msg.path));
514 this.stats.bytesReceived += data.length
515 })
417516
418 if (!targetPath.startsWith(this.destDir + path.sep) && targetPath !== this.destDir) {
419 this.stats.errors.push({ path: msg.path, message: 'Path traversal rejected' });
420 this.emit('error', new Error('Path traversal rejected: ' + msg.path));
421 return;
422 }
517 // Receive control messages (file-end, error, etc.)
518 proto.onControl((msg) => {
519 if (msg.type === 'file-end') {
520 const targetPath = path.join(this.destDir, msg.path)
423521
424 fs.mkdirSync(path.dirname(targetPath), { recursive: true });
522 if (this._transferManager.isComplete(targetPath)) {
523 const fin = this._transferManager.finalize(targetPath)
524 if (!fin.ok) {
525 this.stats.errors.push({ path: msg.path, message: fin.error })
526 }
527 }
425528
426 this._currentPath = msg.path;
427 this._currentHash = msg.hash;
428 this._currentFd = fs.openSync(targetPath, 'w');
429 this._currentWritten = 0;
430 if (msg.size > 0) fs.ftruncateSync(this._currentFd, msg.size);
431 }
529 // Post-transfer verification (whole-file hash)
530 if (this._verify && pendingFiles[currentFileIdx]) {
531 try {
532 const actual = hashFile(targetPath)
533 const expected = pendingFiles[currentFileIdx].contentHash
534 if (actual !== expected) {
535 const err = 'Hash mismatch for ' + msg.path
536 this.stats.errors.push({ path: msg.path, message: err })
537 this.emit('error', new Error(err))
538 try { fs.unlinkSync(targetPath) } catch {}
539 }
540 } catch (err) {
541 this.stats.errors.push({ path: msg.path, message: err.message })
542 }
543 }
432544
433 _handleFileEnd (msg) {
434 if (this._currentFd !== null) {
435 fs.closeSync(this._currentFd);
436 this._currentFd = null;
437 }
545 this.stats.filesTransferred++
546 currentPath = null
438547
439 if (this._verify && this._currentHash) {
440 const targetPath = path.join(this.destDir, msg.path);
441 try {
442 const actual = hashFile(targetPath);
443 if (actual !== this._currentHash) {
444 const err = 'Hash mismatch for ' + msg.path;
445 this.stats.errors.push({ path: msg.path, message: err });
446 this.emit('error', new Error(err));
447 try { fs.unlinkSync(targetPath); } catch {}
448 return;
548 this.emit('progress', {
549 file: msg.path,
550 filesTransferred: this.stats.filesTransferred,
551 filesSkipped: this.stats.filesSkipped,
552 bytesReceived: this.stats.bytesReceived,
553 totalFiles: this.stats.totalFiles
554 })
555
556 // Request next file or finish
557 currentFileIdx++
558 if (currentFileIdx < pendingFiles.length) {
559 proto.sendFileRequest(pendingFiles[currentFileIdx].path).catch(safetyCatch)
560 } else {
561 proto.sendControl({ type: 'done' }).catch(safetyCatch)
562 this._finish()
449563 }
450 } catch (err) {
451 this.stats.errors.push({ path: msg.path, message: err.message });
564 } else if (msg.type === 'error') {
565 this.stats.errors.push({ path: msg.path, message: msg.message })
566 this.emit('error', new Error(msg.message))
452567 }
453 }
454
455 this.stats.filesTransferred++;
456 this._currentPath = null;
457 this._currentHash = null;
458
459 this.emit('progress', {
460 file: msg.path,
461 filesTransferred: this.stats.filesTransferred,
462 filesSkipped: this.stats.filesSkipped,
463 bytesReceived: this.stats.bytesReceived,
464 totalFiles: this.stats.totalFiles
465 });
466 }
467
468 _handleTransferComplete (msg, proto) {
469 proto.sendMeta({ type: 'done' }).catch(safetyCatch);
470 this._finish();
568 })
471569 }
472570}
473
474module.exports = { DeployServer, DeployClient };
lib/protocol.js +96 -35
……@@ -1,60 +1,121 @@
1const Protomux = require('protomux');
2const c = require('compact-encoding');
1/**
2 * Deploy Protocol — Protomux channel for release distribution
3 *
4 * Messages:
5 * 0: fileList (server→client) JSON array of { path, contentHash, size }
6 * 1: fileRequest (client→server) JSON { path }
7 * 2: chunkInfo (server→client) JSON { path, size, chunks: [{ hash, offset, length }] }
8 * 3: chunkData (server→client) 4-byte LE index + raw chunk bytes
9 * 4: control (bidirectional) JSON { type, ... }
10 *
11 * Backpressure: ondrain callback on Protomux channel.
12 */
313
4const PROTOCOL_NAME = 'hyper-deploy';
5const PROTOCOL_VERSION = 1;
6const CHUNK_SIZE = 65536;
14import Protomux from 'protomux'
15import c from 'compact-encoding'
716
8function attachProtocol (stream) {
9 const mux = Protomux.from(stream);
10 if (!mux) return null;
17const PROTOCOL_NAME = 'hyper-deploy'
18const PROTOCOL_VERSION = 2
1119
12 let drainResolve = null;
20/**
21 * Attach the deploy protocol to a stream.
22 *
23 * @param {Stream} stream — encrypted stream (SecretStream or Hyperswarm connection)
24 * @returns {{ channel, msgs, send, onMessage, destroy } | null}
25 */
26export function attachProtocol (stream) {
27 const mux = Protomux.from(stream)
28 if (!mux) return null
1329
14 const ch = mux.createChannel({
30 let drainResolve = null
31
32 const channel = mux.createChannel({
1533 protocol: PROTOCOL_NAME,
1634 ondrain () {
1735 if (drainResolve) {
18 const r = drainResolve;
19 drainResolve = null;
20 r();
36 const r = drainResolve
37 drainResolve = null
38 r()
2139 }
2240 }
23 });
41 })
2442
25 if (!ch) return null;
43 if (!channel) return null
2644
27 const metaMsg = ch.addMessage({ encoding: c.raw });
28 const dataMsg = ch.addMessage({ encoding: c.raw });
45 const fileListMsg = channel.addMessage({ encoding: c.raw })
46 const fileRequestMsg = channel.addMessage({ encoding: c.raw })
47 const chunkInfoMsg = channel.addMessage({ encoding: c.raw })
48 const chunkDataMsg = channel.addMessage({ encoding: c.raw })
49 const controlMsg = channel.addMessage({ encoding: c.raw })
2950
30 ch.open();
51 channel.open()
3152
3253 async function waitDrain () {
33 await new Promise(r => { drainResolve = r; });
54 await new Promise(r => { drainResolve = r })
55 }
56
57 async function sendRaw (msg, buf) {
58 if (!msg.send(buf)) await waitDrain()
3459 }
3560
3661 return {
37 ch,
38 metaMsg,
39 dataMsg,
40 mux,
41
42 async sendMeta (obj) {
43 if (!metaMsg.send(Buffer.from(JSON.stringify(obj)))) {
44 await waitDrain();
45 }
62 channel,
63 version: PROTOCOL_VERSION,
64
65 // ── Senders ──────────────────────────────────────────────
66
67 async sendFileList (files) {
68 await sendRaw(fileListMsg, Buffer.from(JSON.stringify(files)))
69 },
70
71 async sendFileRequest (path) {
72 await sendRaw(fileRequestMsg, Buffer.from(JSON.stringify({ path })))
73 },
74
75 async sendChunkInfo (info) {
76 await sendRaw(chunkInfoMsg, Buffer.from(JSON.stringify(info)))
77 },
78
79 async sendChunkData (index, data) {
80 const header = Buffer.alloc(4)
81 header.writeUInt32LE(index)
82 await sendRaw(chunkDataMsg, Buffer.concat([header, data]))
4683 },
4784
48 async sendData (buf) {
49 if (!dataMsg.send(buf)) {
50 await waitDrain();
85 async sendControl (obj) {
86 await sendRaw(controlMsg, Buffer.from(JSON.stringify(obj)))
87 },
88
89 // ── Receivers ─────────────────────────────────────────────
90
91 onFileList (fn) {
92 fileListMsg.onmessage = (raw) => fn(JSON.parse(raw.toString()))
93 },
94
95 onFileRequest (fn) {
96 fileRequestMsg.onmessage = (raw) => fn(JSON.parse(raw.toString()))
97 },
98
99 onChunkInfo (fn) {
100 chunkInfoMsg.onmessage = (raw) => fn(JSON.parse(raw.toString()))
101 },
102
103 onChunkData (fn) {
104 chunkDataMsg.onmessage = (raw) => {
105 const index = raw.readUInt32LE(0)
106 const data = raw.subarray(4)
107 fn(index, data)
51108 }
52109 },
53110
54 parseMeta (raw) {
55 return JSON.parse(raw.toString());
111 onControl (fn) {
112 controlMsg.onmessage = (raw) => fn(JSON.parse(raw.toString()))
113 },
114
115 destroy () {
116 channel.close()
56117 }
57 };
118 }
58119}
59120
60module.exports = { attachProtocol, PROTOCOL_NAME, PROTOCOL_VERSION, CHUNK_SIZE };
121export { PROTOCOL_NAME, PROTOCOL_VERSION }
lib/scanner.js +0 -71
……@@ -1,71 +0,0 @@
1const fs = require('fs');
2const path = require('path');
3const crypto = require('hypercore-crypto');
4
5function walkDir (dir, prefix) {
6 prefix = prefix || '';
7 const results = [];
8 try {
9 for (const entry of fs.readdirSync(dir)) {
10 const full = path.join(dir, entry);
11 const rel = prefix ? prefix + '/' + entry : entry;
12 try {
13 const stat = fs.statSync(full);
14 if (stat.isDirectory()) results.push(...walkDir(full, rel));
15 else if (stat.isFile()) results.push({ path: rel, size: stat.size });
16 } catch {}
17 }
18 } catch {}
19 return results;
20}
21
22function hashFile (filePath) {
23 return crypto.data(fs.readFileSync(filePath)).toString('hex');
24}
25
26function buildManifest (dir, opts) {
27 const onProgress = opts && opts.onProgress;
28 const files = walkDir(dir);
29 let totalSize = 0;
30
31 for (let i = 0; i < files.length; i++) {
32 const f = files[i];
33 f.hash = hashFile(path.join(dir, f.path));
34 totalSize += f.size;
35 if (onProgress) onProgress(i + 1, files.length);
36 }
37
38 return { files, totalSize };
39}
40
41function diffManifest (manifest, localDir) {
42 const needed = [];
43 let skipped = 0;
44
45 for (const f of manifest) {
46 const localPath = path.join(localDir, f.path);
47 try {
48 const stat = fs.statSync(localPath);
49 if (stat.size === f.size) {
50 const localHash = hashFile(localPath);
51 if (localHash === f.hash) {
52 skipped++;
53 continue;
54 }
55 }
56 } catch {}
57 needed.push(f.path);
58 }
59
60 return { needed, skipped };
61}
62
63function readChunk (filePath, offset, length) {
64 const fd = fs.openSync(filePath, 'r');
65 const buf = Buffer.alloc(length);
66 fs.readSync(fd, buf, 0, length, offset);
67 fs.closeSync(fd);
68 return buf;
69}
70
71module.exports = { walkDir, hashFile, buildManifest, diffManifest, readChunk };
lib/web.js +0 -318
……@@ -1,318 +0,0 @@
1/**
2 * Web UI for hyper-deploy server status
3 *
4 * Serves a single-page status dashboard:
5 * GET / — HTML page with live updates via SSE
6 * GET /api/status — JSON status (files, peers, transfers)
7 * GET /events — Server-Sent Events for live updates
8 */
9
10import http from 'bare-http1'
11
12function fmt (b) {
13 if (b < 1024) return b + ' B'
14 if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
15 if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'
16 return (b / 1073741824).toFixed(2) + ' GB'
17}
18
19/**
20 * Start the web UI server.
21 * @param {DeployServer} deployServer — the running deploy server instance
22 * @param {{ port?: number }} [opts]
23 * @returns {{ server, port }}
24 */
25export function startWeb (deployServer, opts) {
26 const port = (opts && opts.port) || 9090
27 const sseClients = new Set()
28
29 function broadcast (event, data) {
30 const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
31 for (const res of sseClients) {
32 try { res.write(msg) } catch { sseClients.delete(res) }
33 }
34 }
35
36 // Wire deploy server events to SSE broadcasts
37 deployServer.on('peer', (info) => broadcast('peer', info))
38 deployServer.on('progress', (info) => {
39 broadcast('progress', {
40 ...info,
41 totalFilesServed: deployServer.stats.filesServed,
42 totalBytesServed: deployServer.stats.bytesServed
43 })
44 })
45 deployServer.on('error', (err) => broadcast('error', { message: err.message }))
46
47 function getStatus () {
48 return {
49 dir: deployServer.dir,
50 topic: deployServer.topicHex,
51 files: deployServer.manifest ? deployServer.manifest.files.length : 0,
52 totalSize: deployServer.manifest ? deployServer.manifest.totalSize : 0,
53 totalSizeFmt: deployServer.manifest ? fmt(deployServer.manifest.totalSize) : '0',
54 connections: deployServer.connections,
55 stats: {
56 totalConnections: deployServer.stats.totalConnections,
57 filesServed: deployServer.stats.filesServed,
58 bytesServed: deployServer.stats.bytesServed,
59 bytesServedFmt: fmt(deployServer.stats.bytesServed)
60 }
61 }
62 }
63
64 const server = http.createServer(async (req, res) => {
65 const url = req.url.split('?')[0]
66
67 if (url === '/api/status') {
68 res.writeHead(200, { 'Content-Type': 'application/json' })
69 res.end(JSON.stringify(getStatus()))
70 return
71 }
72
73 if (url === '/api/rescan' && req.method === 'POST') {
74 try {
75 const result = await deployServer.rescan()
76 res.writeHead(200, { 'Content-Type': 'application/json' })
77 res.end(JSON.stringify({ ok: true, ...result, ...getStatus() }))
78 } catch (err) {
79 res.writeHead(500, { 'Content-Type': 'application/json' })
80 res.end(JSON.stringify({ ok: false, error: err.message }))
81 }
82 return
83 }
84
85 if (url === '/events') {
86 res.writeHead(200, {
87 'Content-Type': 'text/event-stream',
88 'Cache-Control': 'no-cache',
89 Connection: 'keep-alive'
90 })
91 res.write(`data: ${JSON.stringify(getStatus())}\n\n`)
92 sseClients.add(res)
93 req.on('close', () => sseClients.delete(res))
94 return
95 }
96
97 // Serve HTML dashboard
98 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
99 res.end(renderPage())
100 })
101
102 server.listen(port, () => {})
103
104 return { server, port }
105}
106
107function renderPage () {
108 return `<!DOCTYPE html>
109<html lang="en">
110<head>
111<meta charset="UTF-8">
112<meta name="viewport" content="width=device-width, initial-scale=1.0">
113<title>hyper-deploy</title>
114<style>
115 * { margin: 0; padding: 0; box-sizing: border-box; }
116 body {
117 font-family: -apple-system, system-ui, sans-serif;
118 background: #0d1117;
119 color: #c9d1d9;
120 padding: 2rem;
121 max-width: 720px;
122 margin: 0 auto;
123 }
124 h1 { color: #58a6ff; font-size: 1.4rem; margin-bottom: 1.5rem; }
125 .card {
126 background: #161b22;
127 border: 1px solid #30363d;
128 border-radius: 6px;
129 padding: 1rem 1.2rem;
130 margin-bottom: 1rem;
131 }
132 .card h2 {
133 font-size: 0.75rem;
134 text-transform: uppercase;
135 letter-spacing: 0.05em;
136 color: #8b949e;
137 margin-bottom: 0.6rem;
138 }
139 .topic {
140 font-family: monospace;
141 font-size: 0.8rem;
142 word-break: break-all;
143 color: #79c0ff;
144 cursor: pointer;
145 padding: 0.5rem;
146 background: #0d1117;
147 border-radius: 4px;
148 position: relative;
149 }
150 .topic:hover { background: #1a2332; }
151 .topic .copied {
152 position: absolute;
153 right: 0.5rem;
154 top: 0.5rem;
155 color: #3fb950;
156 font-size: 0.7rem;
157 display: none;
158 }
159 .stat-row {
160 display: flex;
161 justify-content: space-between;
162 padding: 0.3rem 0;
163 border-bottom: 1px solid #21262d;
164 }
165 .stat-row:last-child { border-bottom: none; }
166 .stat-label { color: #8b949e; }
167 .stat-value { color: #e6edf3; font-family: monospace; }
168 .peers {
169 min-height: 2rem;
170 }
171 .peer {
172 display: inline-block;
173 background: #1f6feb22;
174 border: 1px solid #1f6feb44;
175 color: #58a6ff;
176 padding: 0.2rem 0.5rem;
177 border-radius: 3px;
178 font-family: monospace;
179 font-size: 0.75rem;
180 margin: 0.2rem;
181 }
182 .peer.done { border-color: #3fb95044; color: #3fb950; }
183 .progress-bar {
184 height: 4px;
185 background: #21262d;
186 border-radius: 2px;
187 margin-top: 0.5rem;
188 overflow: hidden;
189 }
190 .progress-fill {
191 height: 100%;
192 background: #1f6feb;
193 border-radius: 2px;
194 transition: width 0.3s;
195 width: 0%;
196 }
197 .empty { color: #484f58; font-style: italic; font-size: 0.85rem; }
198 .cmd {
199 font-family: monospace;
200 font-size: 0.8rem;
201 color: #c9d1d9;
202 background: #0d1117;
203 padding: 0.5rem;
204 border-radius: 4px;
205 margin-top: 0.4rem;
206 }
207 .rescan-btn {
208 background: #21262d;
209 color: #c9d1d9;
210 border: 1px solid #30363d;
211 padding: 0.4rem 0.8rem;
212 border-radius: 4px;
213 cursor: pointer;
214 font-size: 0.8rem;
215 float: right;
216 margin-top: -0.2rem;
217 }
218 .rescan-btn:hover { background: #30363d; }
219</style>
220</head>
221<body>
222<h1>hyper-deploy</h1>
223
224<div class="card">
225 <h2>Topic <button class="rescan-btn" onclick="rescan()">Rescan</button></h2>
226 <div class="topic" onclick="copyTopic(this)" id="topic">loading...</div>
227 <div class="cmd" id="pull-cmd">loading...</div>
228</div>
229
230<div class="card">
231 <h2>Release</h2>
232 <div class="stat-row"><span class="stat-label">Directory</span><span class="stat-value" id="dir">-</span></div>
233 <div class="stat-row"><span class="stat-label">Files</span><span class="stat-value" id="files">-</span></div>
234 <div class="stat-row"><span class="stat-label">Total size</span><span class="stat-value" id="size">-</span></div>
235</div>
236
237<div class="card">
238 <h2>Transfer Stats</h2>
239 <div class="stat-row"><span class="stat-label">Connections</span><span class="stat-value" id="connections">0</span></div>
240 <div class="stat-row"><span class="stat-label">Files served</span><span class="stat-value" id="served">0</span></div>
241 <div class="stat-row"><span class="stat-label">Data served</span><span class="stat-value" id="bytes">0 B</span></div>
242 <div class="progress-bar"><div class="progress-fill" id="progress"></div></div>
243</div>
244
245<div class="card">
246 <h2>Peers</h2>
247 <div class="peers" id="peers"><span class="empty">No peers connected</span></div>
248</div>
249
250<script>
251const $ = id => document.getElementById(id)
252const peers = new Map()
253let totalFiles = 0
254
255function update (data) {
256 if (data.topic) {
257 $('topic').textContent = data.topic
258 $('pull-cmd').textContent = 'hyper-deploy pull ' + data.topic + ' <dest>'
259 }
260 if (data.dir) $('dir').textContent = data.dir
261 if (data.files) { $('files').textContent = data.files; totalFiles = data.files }
262 if (data.totalSizeFmt) $('size').textContent = data.totalSizeFmt
263 if (data.stats) {
264 $('connections').textContent = data.stats.connections
265 $('served').textContent = data.stats.filesServed
266 $('bytes').textContent = data.stats.bytesServedFmt
267 if (totalFiles > 0) {
268 $('progress').style.width = Math.min(100, data.stats.filesServed / totalFiles * 100) + '%'
269 }
270 }
271}
272
273function addPeer (key, type) {
274 peers.set(key, type)
275 renderPeers()
276}
277
278function renderPeers () {
279 if (peers.size === 0) {
280 $('peers').innerHTML = '<span class="empty">No peers connected</span>'
281 return
282 }
283 $('peers').innerHTML = Array.from(peers.entries())
284 .map(([k, t]) => '<span class="peer' + (t === 'done' ? ' done' : '') + '">' + k + '</span>')
285 .join('')
286}
287
288function copyTopic (el) {
289 navigator.clipboard.writeText(el.textContent).then(() => {
290 const copied = el.querySelector('.copied')
291 if (copied) { copied.style.display = 'inline'; setTimeout(() => copied.style.display = 'none', 1500) }
292 })
293}
294
295function rescan () {
296 fetch('/api/rescan', { method: 'POST' }).then(r => r.json()).then(update)
297}
298
299// SSE for live updates
300const es = new EventSource('/events')
301es.onmessage = (e) => update(JSON.parse(e.data))
302es.addEventListener('peer', (e) => {
303 const d = JSON.parse(e.data)
304 addPeer(d.key, d.type)
305})
306es.addEventListener('progress', (e) => {
307 const d = JSON.parse(e.data)
308 $('served').textContent = d.filesServed
309 if (totalFiles > 0) $('progress').style.width = Math.min(100, d.filesServed / totalFiles * 100) + '%'
310})
311es.addEventListener('complete', (e) => {
312 const d = JSON.parse(e.data)
313 $('progress').style.width = '100%'
314})
315</script>
316</body>
317</html>`
318}
package-lock.json +9 -34
……@@ -1,14 +1,16 @@
11{
22 "name": "hyper-deploy",
3 "version": "0.2.0",
3 "version": "0.3.0",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "hyper-deploy",
9 "version": "0.2.0",
9 "version": "0.3.0",
1010 "dependencies": {
1111 "compact-encoding": "^2.0.0",
12 "corestore": "^7.0.0",
13 "hyper-overlay": "file:../hyper-overlay",
1214 "hypercore-crypto": "^3.0.0",
1315 "hyperswarm": "^4.0.0",
1416 "protomux": "^3.0.0",
……@@ -17,13 +19,11 @@
1719 },
1820 "devDependencies": {
1921 "@hyperswarm/secret-stream": "^6.0.0",
20 "brittle": "^3.0.0",
21 "corestore": "^7.0.0"
22 "brittle": "^3.0.0"
2223 }
2324 },
2425 "../hyper-overlay": {
2526 "version": "0.1.0",
26 "extraneous": true,
2727 "license": "AGPL-3.0",
2828 "dependencies": {
2929 "@hyperswarm/testnet": "^3.1.4",
……@@ -319,7 +319,6 @@
319319 "version": "4.6.0",
320320 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.6.0.tgz",
321321 "integrity": "sha512-2YkS7NuiJceSEbyEOdSNLE9tsGd+f4+f7C+Nik/MCk27SYdwIMPT/yRKvg++FZhQXgk0KWJKJyXX9RhVV0RGqA==",
322 "dev": true,
323322 "license": "Apache-2.0",
324323 "dependencies": {
325324 "bare-events": "^2.5.4",
……@@ -531,7 +530,6 @@
531530 "version": "3.8.7",
532531 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
533532 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
534 "devOptional": true,
535533 "license": "Apache-2.0",
536534 "engines": {
537535 "bare": ">=1.14.0"
……@@ -541,7 +539,6 @@
541539 "version": "3.0.0",
542540 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
543541 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
544 "devOptional": true,
545542 "license": "Apache-2.0",
546543 "dependencies": {
547544 "bare-os": "^3.0.1"
……@@ -728,7 +725,6 @@
728725 "version": "2.4.0",
729726 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
730727 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
731 "devOptional": true,
732728 "license": "Apache-2.0",
733729 "peer": true,
734730 "dependencies": {
……@@ -823,7 +819,6 @@
823819 "version": "1.0.3",
824820 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
825821 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
826 "dev": true,
827822 "license": "MIT"
828823 },
829824 "node_modules/bits-to-bytes": {
……@@ -929,7 +924,6 @@
929924 "version": "7.9.2",
930925 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz",
931926 "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==",
932 "dev": true,
933927 "license": "MIT",
934928 "dependencies": {
935929 "b4a": "^1.6.7",
……@@ -946,7 +940,6 @@
946940 "version": "2.3.1",
947941 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
948942 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
949 "dev": true,
950943 "license": "Apache-2.0",
951944 "dependencies": {
952945 "b4a": "^1.6.7",
……@@ -1006,7 +999,6 @@
1006999 "version": "2.1.1",
10071000 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz",
10081001 "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==",
1009 "dev": true,
10101002 "license": "Apache-2.0",
10111003 "dependencies": {
10121004 "bare-fs": "^4.5.0",
……@@ -1019,14 +1011,12 @@
10191011 "version": "1.13.0",
10201012 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
10211013 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
1022 "dev": true,
10231014 "license": "MIT"
10241015 },
10251016 "node_modules/fs-native-extensions": {
10261017 "version": "1.4.5",
10271018 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
10281019 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
1029 "dev": true,
10301020 "license": "Apache-2.0",
10311021 "dependencies": {
10321022 "require-addon": "^1.1.0",
……@@ -1037,7 +1027,6 @@
10371027 "version": "2.0.0",
10381028 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
10391029 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
1040 "dev": true,
10411030 "license": "MIT",
10421031 "dependencies": {
10431032 "is-property": "^1.0.0"
……@@ -1047,7 +1036,6 @@
10471036 "version": "1.0.1",
10481037 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
10491038 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
1050 "dev": true,
10511039 "license": "MIT"
10521040 },
10531041 "node_modules/globbie": {
……@@ -1063,11 +1051,14 @@
10631051 "picomatch": "^4.0.2"
10641052 }
10651053 },
1054 "node_modules/hyper-overlay": {
1055 "resolved": "../hyper-overlay",
1056 "link": true
1057 },
10661058 "node_modules/hypercore": {
10671059 "version": "11.28.1",
10681060 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
10691061 "integrity": "sha512-d8jqUilG7Gri8wT+sRdkFTYRPMHGyvBz/NCYHf5rX7+LoivRNRxQm5nWChT7caiUKbcsDak6h7IiEQ5wbD1wCg==",
1070 "dev": true,
10711062 "license": "MIT",
10721063 "dependencies": {
10731064 "@hyperswarm/secret-stream": "^6.0.0",
……@@ -1108,7 +1099,6 @@
11081099 "version": "1.5.0",
11091100 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
11101101 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
1111 "dev": true,
11121102 "license": "Apache-2.0",
11131103 "dependencies": {
11141104 "hypercore-id-encoding": "^1.3.0"
……@@ -1128,7 +1118,6 @@
11281118 "version": "2.8.0",
11291119 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.8.0.tgz",
11301120 "integrity": "sha512-s35HllFMYvrkSHvkIIklVFmCI/bRVNO7H3pi4mKvWl0XLyhGGGM+NtJ7fpsh82oPRCmL9XtMRO+pT6LiMAO8MA==",
1131 "dev": true,
11321121 "license": "Apache-2.0",
11331122 "dependencies": {
11341123 "b4a": "^1.6.7",
……@@ -1179,7 +1168,6 @@
11791168 "version": "1.20.1",
11801169 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz",
11811170 "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==",
1182 "dev": true,
11831171 "license": "Apache-2.0",
11841172 "dependencies": {
11851173 "bare-fs": "^4.0.1",
……@@ -1207,7 +1195,6 @@
12071195 "version": "3.5.0",
12081196 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
12091197 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
1210 "dev": true,
12111198 "license": "Apache-2.0",
12121199 "dependencies": {
12131200 "b4a": "^1.6.4"
……@@ -1217,7 +1204,6 @@
12171204 "version": "1.0.2",
12181205 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
12191206 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1220 "dev": true,
12211207 "license": "MIT",
12221208 "dependencies": {
12231209 "b4a": "^1.1.1"
……@@ -1227,7 +1213,6 @@
12271213 "version": "1.0.2",
12281214 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
12291215 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
1230 "dev": true,
12311216 "license": "MIT"
12321217 },
12331218 "node_modules/kademlia-routing-table": {
……@@ -1316,7 +1301,6 @@
13161301 "version": "2.4.8",
13171302 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
13181303 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1319 "dev": true,
13201304 "license": "Apache-2.0",
13211305 "optional": true,
13221306 "dependencies": {
……@@ -1327,7 +1311,6 @@
13271311 "version": "2.2.0",
13281312 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
13291313 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1330 "dev": true,
13311314 "license": "ISC",
13321315 "dependencies": {
13331316 "b4a": "^1.6.0",
……@@ -1341,7 +1324,6 @@
13411324 "version": "1.0.0",
13421325 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
13431326 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1344 "dev": true,
13451327 "license": "MIT"
13461328 },
13471329 "node_modules/ready-resource": {
……@@ -1366,7 +1348,6 @@
13661348 "version": "1.0.0",
13671349 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
13681350 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
1369 "dev": true,
13701351 "license": "Apache-2.0"
13711352 },
13721353 "node_modules/require-addon": {
……@@ -1385,21 +1366,18 @@
13851366 "version": "1.1.0",
13861367 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
13871368 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1388 "dev": true,
13891369 "license": "MIT"
13901370 },
13911371 "node_modules/resource-on-exit": {
13921372 "version": "1.0.0",
13931373 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
13941374 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
1395 "dev": true,
13961375 "license": "Apache-2.0"
13971376 },
13981377 "node_modules/rocksdb-native": {
13991378 "version": "3.15.0",
14001379 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz",
14011380 "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==",
1402 "dev": true,
14031381 "license": "Apache-2.0",
14041382 "dependencies": {
14051383 "compact-encoding": "^2.15.0",
……@@ -1431,7 +1409,6 @@
14311409 "version": "1.2.4",
14321410 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
14331411 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
1434 "dev": true,
14351412 "license": "Apache-2.0"
14361413 },
14371414 "node_modules/shuffled-priority-queue": {
……@@ -1453,7 +1430,6 @@
14531430 "version": "1.3.9",
14541431 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
14551432 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1456 "dev": true,
14571433 "license": "Apache-2.0",
14581434 "optional": true,
14591435 "dependencies": {
……@@ -1465,7 +1441,6 @@
14651441 "version": "1.1.2",
14661442 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
14671443 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1468 "dev": true,
14691444 "license": "ISC",
14701445 "dependencies": {
14711446 "b4a": "^1.6.0"
package.json +8 -6
……@@ -1,19 +1,22 @@
11{
22 "name": "hyper-deploy",
3 "version": "0.2.0",
3 "version": "0.3.0",
44 "description": "P2P release distribution — serve build artifacts to peers over Hyperswarm",
5 "type": "module",
56 "main": "index.js",
67 "imports": {
7 "fs": { "bare": "bare-fs", "default": "fs" },
8 "path": { "bare": "bare-path", "default": "path" },
9 "os": { "bare": "bare-os", "default": "os" }
8 "#fs": { "bare": "bare-fs", "default": "fs" },
9 "#path": { "bare": "bare-path", "default": "path" },
10 "#os": { "bare": "bare-os", "default": "os" }
1011 },
1112 "scripts": {
1213 "test": "brittle test/*.test.js"
1314 },
1415 "dependencies": {
16 "corestore": "^7.0.0",
1517 "compact-encoding": "^2.0.0",
1618 "hypercore-crypto": "^3.0.0",
19 "hyper-overlay": "file:../hyper-overlay",
1720 "hyperswarm": "^4.0.0",
1821 "protomux": "^3.0.0",
1922 "ready-resource": "^1.0.0",
……@@ -21,7 +24,6 @@
2124 },
2225 "devDependencies": {
2326 "@hyperswarm/secret-stream": "^6.0.0",
24 "brittle": "^3.0.0",
25 "corestore": "^7.0.0"
27 "brittle": "^3.0.0"
2628 }
2729}