secure-scuttlebot classic
1var fs = require('fs')
2var path = require('path')
3
4var manualEntries = {
5 start: {
6 description: 'Start the ssb-server daemon and generate ~/.ssb/manifest.json',
7 example: 'ssb-server start',
8 type: 'cli'
9 },
10 config: {
11 description: 'Print the effective configuration used by the server',
12 example: 'ssb-server config',
13 type: 'sync'
14 },
15 blobs: {
16 description: 'Manage blob storage, listing, adding, and pushing binary data.',
17 example: 'ssb-server blobs.ls'
18 },
19 auth: {
20 description: 'Create the RPC authentication handshake for connecting to a remote peer.',
21 example: 'ssb-server auth net:example.com:8008~shs:<key>'
22 },
23 address: {
24 description: 'Show the multi-server addresses clients can use to reach this server.',
25 example: 'ssb-server address'
26 },
27 manifest: {
28 description: 'Dump the RPC manifest so other programs know what to call.',
29 example: 'ssb-server manifest'
30 },
31 'multiserver.parse': {
32 description: 'Parse a multi-server address string into host/port/key components.',
33 example: 'ssb-server multiserver.parse net:example.com:8008~shs:<key>'
34 },
35 'multiserver.address': {
36 description: 'Build a canonical multi-server address from host, port, and key.',
37 example: 'ssb-server multiserver.address --host example.com --port 8008 --key @abc.ed25519'
38 },
39 multiserver: {
40 description: 'Helpers around multi-server address parsing and building.',
41 example: 'ssb-server multiserver.parse net:example.com:8008~shs:<key>'
42 },
43 multiserverNet: {
44 description: 'Inspect the legacy net transport configuration used by multi-server.',
45 example: 'ssb-server multiserverNet'
46 },
47 createFeedStream: {
48 description: 'Stream a feed by timestamp, optionally filtering with gt/gte/lt/lte and live=true.',
49 example: 'ssb-server createFeedStream --live --gt 0'
50 },
51 messagesByType: {
52 description: 'Stream messages filtered by the `type` field, ordered by receive time.',
53 example: 'ssb-server messagesByType --type about'
54 },
55 createUserStream: {
56 description: 'Stream a feed by sequence numbers with support for range filters.',
57 example: 'ssb-server createUserStream --id @alice --live'
58 },
59 createWriteStream: {
60 description: 'Send newline-delimited JSON messages on stdin straight into the database.',
61 example: 'cat message.json | ssb-server createWriteStream'
62 },
63 createSequenceStream: {
64 description: 'Stream the global sequence counter to track when the database advances.',
65 example: 'ssb-server createSequenceStream --live'
66 },
67 links: {
68 description: 'Query link edges (`source`, `dest`, `rel`) between messages, feeds, and blobs.',
69 example: 'ssb-server links --source @alice'
70 },
71 getLatest: {
72 description: 'Fetch a feed’s latest message so you can inspect the head of the log.',
73 example: 'ssb-server getLatest @alice'
74 },
75 latest: {
76 description: 'Stream the latest sequence seen for every feed that this server follows.',
77 example: 'ssb-server latest --limit 20'
78 },
79 latestSequence: {
80 description: 'Return the highest sequence number a feed has reached locally.',
81 example: 'ssb-server latestSequence @alice'
82 },
83 del: {
84 description: 'Drop a message from the local log (only works if the message is still cached).',
85 example: 'ssb-server del %messageid.sha256'
86 },
87 getVectorClock: {
88 description: 'Print the vector clock used when deciding how much of each feed we have.',
89 example: 'ssb-server getVectorClock'
90 },
91 help: {
92 description: 'Show the CLI help overview or focus on a single command.',
93 example: 'ssb-server help publish'
94 },
95 'plugins.help': {
96 description: 'List the plugin subcommands (install, uninstall, enable, disable).',
97 example: 'ssb-server plugins.help'
98 },
99 plugins: {
100 description: 'Manage ssb-server plugins (install, uninstall, enable, disable).',
101 example: 'ssb-server plugins.install plugin@version'
102 },
103 gossip: {
104 description: 'Manage gossip peers, connections, and their metadata.',
105 example: 'ssb-server gossip.peers'
106 },
107 'gossip.peers': {
108 description: 'List the gossip table peers and their current connection state.',
109 example: 'ssb-server gossip.peers'
110 },
111 'gossip.get': {
112 description: 'Inspect the gossip metadata for a peer by id or address.',
113 example: 'ssb-server gossip.get @peer'
114 },
115 'gossip.ping': {
116 description: 'Ping a peer to measure reachability, RTT, and grab a quick status.',
117 example: 'ssb-server gossip.ping @peer'
118 },
119 'gossip.help': {
120 description: 'Show gossip subcommands and their purpose.',
121 example: 'ssb-server gossip.help'
122 },
123 'blobs.meta': {
124 description: 'Read metadata (size, timestamps) for a blob without streaming the payload.',
125 example: 'ssb-server blobs.meta &blobid.sha256'
126 },
127 'blobs.changes': {
128 description: 'Stream notifications when blobs change locally (use --live to keep the stream open).',
129 example: 'ssb-server blobs.changes --live'
130 },
131 'blobs.createWants': {
132 description: 'Watch the current want list for peers so you can mirror their needs.',
133 example: 'ssb-server blobs.createWants --live'
134 },
135 'blobs.help': {
136 description: 'List available blob commands and their arguments.',
137 example: 'ssb-server blobs.help'
138 },
139 'invite.use': {
140 description: 'Call this on the server that owns an invite code; it validates one code and publishes a follow for the provided feed.',
141 example: 'ssb-server invite.use INVITE_CODE --feed @friend'
142 },
143 invite: {
144 description: 'Create, accept, or use invites for pubs.',
145 example: 'ssb-server invite.create 1'
146 },
147 'friends.help': {
148 description: 'List the available friends.* subcommands.',
149 example: 'ssb-server friends.help'
150 },
151 friends: {
152 description: 'Inspect the follow/block graph maintained by the friends plugin.',
153 example: 'ssb-server friends.hops'
154 },
155 'friends.onEdge': {
156 description: 'Inspect edges (follows/blocks) on the friends graph between feeds.',
157 example: 'ssb-server friends.onEdge --start @alice'
158 },
159 query: {
160 description: 'Run map/filter/reduce queries or explain how indexes are used.',
161 example: 'ssb-server query.read --query "{\\"type\\":\\"post\\"}"'
162 },
163 'query.help': {
164 description: 'Show the query.* commands for running or explaining map-filter-reduce queries.',
165 example: 'ssb-server query.help'
166 },
167 'links2.read': {
168 description: 'Run the newer links2 indexes to get fast link traversals.',
169 example: 'ssb-server links2.read --query "{\\"dest\\":\\"@id\\"}"'
170 },
171 'links2.help': {
172 description: 'Describe how to use the links2 query interface.',
173 example: 'ssb-server links2.help'
174 },
175 links2: {
176 description: 'Advanced multi-index link queries built on top of the links2 flumeview.',
177 example: 'ssb-server links2.read --query "{\\"dest\\":\\"@id\\"}"'
178 },
179 replicate: {
180 description: 'Legacy replication controls for requesting and blocking feeds.',
181 example: 'ssb-server replicate.request --id @alice'
182 },
183 ebt: {
184 description: 'EBT helpers for controlling replication with peers.',
185 example: 'ssb-server ebt.replicate --peer <multi-server-address>'
186 },
187 'ebt.replicate': {
188 description: 'Open a duplex replication stream that speaks the EBT protocol.',
189 example: 'ssb-server ebt.replicate --peer <multi-server-address>'
190 },
191 'ebt.request': {
192 description: 'Request that a peer replicate a specific feed.',
193 example: 'ssb-server ebt.request @alice'
194 },
195 'ebt.block': {
196 description: 'Block or unblock replication for another feed.',
197 example: 'ssb-server ebt.block --from @you --to @them --blocking true'
198 },
199 'ebt.peerStatus': {
200 description: 'Read the last-known metadata for an EBT peer connection.',
201 example: 'ssb-server ebt.peerStatus @peer'
202 },
203 ooo: {
204 description: 'Out-of-order (ooo) helpers for sharing messages without full replication.',
205 example: 'ssb-server ooo.stream @friend'
206 },
207 'ooo.stream': {
208 description: 'Stream messages handled out-of-order to avoid waiting for full replication.',
209 example: 'ssb-server ooo.stream @friend'
210 },
211 'ooo.help': {
212 description: 'Show the commands exposed by the ooo (out-of-order) plugin.',
213 example: 'ssb-server ooo.help'
214 },
215 ws: {
216 description: 'Inspect the websocket transport that Patchbay Lite and browsers rely on.',
217 example: 'ssb-server ws'
218 },
219 frontend: {
220 description: 'Serve the built-in Patchbay Lite UI over ssb-ws.',
221 example: 'ssb-server frontend'
222 },
223 private1: {
224 description: 'Expose the private1 transport helper for unix sockets or internal tooling.',
225 example: 'ssb-server private1'
226 },
227 'list-commands': {
228 description: 'Print every available RPC command in the manifest/catalog.',
229 example: 'ssb-server list-commands'
230 }
231}
232
233function cloneArgs (args) {
234 if (!args) return {}
235 var copy = {}
236 Object.keys(args).forEach(function (key) {
237 copy[key] = args[key]
238 })
239 return copy
240}
241
242function buildExample (name, args) {
243 var parts = ['ssb-server', name]
244 var argNames = args ? Object.keys(args).sort() : []
245 argNames.forEach(function (arg) {
246 var type = args[arg] && args[arg].type ? args[arg].type : 'value'
247 parts.push('--' + arg + ' <' + type + '>')
248 })
249 return parts.join(' ')
250}
251
252function addEntry (catalog, name, info) {
253 var args = cloneArgs(info.args)
254 catalog[name] = {
255 description: info.description || '',
256 args: args,
257 type: info.type || 'async',
258 example: info.example || buildExample(name, args)
259 }
260}
261
262function loadPluginEntries () {
263 var pluginsDir = path.join(__dirname, '..', 'plugins')
264 var catalog = {}
265 if (!fs.existsSync(pluginsDir)) return catalog
266
267 fs.readdirSync(pluginsDir).forEach(function (pluginName) {
268 var pluginPath = path.join(pluginsDir, pluginName)
269 var stats
270 try {
271 stats = fs.statSync(pluginPath)
272 } catch (_) {
273 return
274 }
275 if (!stats.isDirectory()) return
276
277 var helpFile = path.join(pluginPath, 'help.js')
278 if (!fs.existsSync(helpFile)) return
279
280 var helpModule
281 try {
282 helpModule = require(helpFile)
283 } catch (_) {
284 return
285 }
286 var commands = helpModule.commands || {}
287 var pluginDescription = helpModule.description
288 Object.keys(commands).forEach(function (commandName) {
289 var commandHelp = commands[commandName] || {}
290 var description = commandHelp.description || pluginDescription || ''
291 addEntry(catalog, pluginName + '.' + commandName, {
292 description: description,
293 args: commandHelp.args,
294 type: commandHelp.type
295 })
296 })
297 })
298
299 return catalog
300}
301
302var externalHelpModules = [
303 { name: 'ssb-db', prefix: '' },
304 { name: 'ssb-blobs', prefix: 'blobs' },
305 { name: 'ssb-gossip', prefix: 'gossip' },
306 { name: 'ssb-replicate', prefix: 'replicate' },
307 { name: 'ssb-query', prefix: 'query' },
308 { name: 'ssb-links', prefix: 'links' },
309 { name: 'ssb-ooo', prefix: 'ooo' },
310 { name: 'ssb-plugins', prefix: 'plugins' }
311]
312
313function loadExternalHelpEntries () {
314 var catalog = {}
315 externalHelpModules.forEach(function (moduleInfo) {
316 var helpModule
317 try {
318 helpModule = require(moduleInfo.name + '/help')
319 } catch (_) {
320 return
321 }
322 var moduleDescription = helpModule.description
323 var commands = helpModule.commands || {}
324 Object.keys(commands).forEach(function (commandName) {
325 var info = commands[commandName] || {}
326 var name = moduleInfo.prefix ? moduleInfo.prefix + '.' + commandName : commandName
327 addEntry(catalog, name, {
328 description: info.description || moduleDescription || '',
329 args: info.args,
330 type: info.type
331 })
332 })
333 })
334 return catalog
335}
336
337function createPlaceholder (name, manifestType) {
338 var entry = {
339 description: 'No detailed help is currently available for this command.',
340 args: {},
341 type: manifestType || 'async',
342 example: 'ssb-server ' + name
343 }
344 return entry
345}
346
347function createCatalog (manifest) {
348 manifest = manifest || {}
349 var catalog = {}
350
351 var pluginEntries = loadPluginEntries()
352 Object.keys(pluginEntries).forEach(function (name) {
353 catalog[name] = pluginEntries[name]
354 })
355
356 var externalEntries = loadExternalHelpEntries()
357 Object.keys(externalEntries).forEach(function (name) {
358 catalog[name] = externalEntries[name]
359 })
360
361 Object.keys(manualEntries).forEach(function (name) {
362 addEntry(catalog, name, manualEntries[name])
363 })
364
365 Object.keys(manifest).forEach(function (name) {
366 if (!catalog[name]) {
367 catalog[name] = createPlaceholder(name, manifest[name])
368 } else if (!catalog[name].type && manifest[name]) {
369 catalog[name].type = manifest[name]
370 }
371 })
372
373 var names = Object.keys(catalog).sort()
374
375 return {
376 entries: catalog,
377 names: names,
378 get: function (name) {
379 return catalog[name]
380 },
381 all: function () {
382 return names.map(function (name) {
383 return { name: name, entry: catalog[name] }
384 })
385 }
386 }
387}
388
389module.exports = {
390 createCatalog: createCatalog
391}