@elefunc/send 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -15,16 +15,21 @@ bun add -g @elefunc/send
15
15
  ## Usage
16
16
 
17
17
  ```bash
18
+ send
18
19
  send peers
19
20
  send offer ./file.txt
20
21
  send accept
21
22
  send tui --events
22
23
  ```
23
24
 
25
+ When no subcommand is provided, `send` launches the TUI by default.
26
+
24
27
  ## Rooms
25
28
 
26
29
  `--room` is optional on all commands. If you omit it, `send` creates a random room and prints or shows it.
27
30
 
31
+ In the TUI, the room row includes a `📋` invite link that opens the equivalent web app URL for the current committed room and toggle state. Set `SEND_WEB_URL` to change its base URL; it defaults to `https://send.rt.ht/`.
32
+
28
33
  ## Self Identity
29
34
 
30
35
  `--self` accepts three forms:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elefunc/send",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -197,8 +197,23 @@ const tuiCommand = async (options: Record<string, unknown>) => {
197
197
  await startTui(initialConfig, !!options.events)
198
198
  }
199
199
 
200
- export const createCli = () => {
200
+ type CliHandlers = {
201
+ peers: typeof peersCommand
202
+ offer: typeof offerCommand
203
+ accept: typeof acceptCommand
204
+ tui: typeof tuiCommand
205
+ }
206
+
207
+ const defaultCliHandlers: CliHandlers = {
208
+ peers: peersCommand,
209
+ offer: offerCommand,
210
+ accept: acceptCommand,
211
+ tui: tuiCommand,
212
+ }
213
+
214
+ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
201
215
  const cli = cac("send")
216
+ cli.usage("[command] [options]")
202
217
 
203
218
  cli
204
219
  .command("peers", "list discovered peers")
@@ -210,7 +225,7 @@ export const createCli = () => {
210
225
  .option("--turn-url <url>", "custom TURN url, repeat or comma-separate")
211
226
  .option("--turn-username <value>", "custom TURN username")
212
227
  .option("--turn-credential <value>", "custom TURN credential")
213
- .action(peersCommand)
228
+ .action(handlers.peers)
214
229
 
215
230
  cli
216
231
  .command("offer [...files]", "offer files to browser-compatible peers")
@@ -223,7 +238,7 @@ export const createCli = () => {
223
238
  .option("--turn-url <url>", "custom TURN url, repeat or comma-separate")
224
239
  .option("--turn-username <value>", "custom TURN username")
225
240
  .option("--turn-credential <value>", "custom TURN credential")
226
- .action(offerCommand)
241
+ .action(handlers.offer)
227
242
 
228
243
  cli
229
244
  .command("accept", "receive and save files")
@@ -235,7 +250,7 @@ export const createCli = () => {
235
250
  .option("--turn-url <url>", "custom TURN url, repeat or comma-separate")
236
251
  .option("--turn-username <value>", "custom TURN username")
237
252
  .option("--turn-credential <value>", "custom TURN credential")
238
- .action(acceptCommand)
253
+ .action(handlers.accept)
239
254
 
240
255
  cli
241
256
  .command("tui", "launch the interactive terminal UI")
@@ -246,24 +261,39 @@ export const createCli = () => {
246
261
  .option("--turn-url <url>", "custom TURN url, repeat or comma-separate")
247
262
  .option("--turn-username <value>", "custom TURN username")
248
263
  .option("--turn-credential <value>", "custom TURN credential")
249
- .action(tuiCommand)
264
+ .action(handlers.tui)
250
265
 
251
- cli.help()
266
+ cli.help(sections => {
267
+ const usage = sections.find(section => section.title === "Usage:")
268
+ if (usage) usage.body = " $ send [command] [options]"
269
+ const moreInfoIndex = sections.findIndex(section => section.title?.startsWith("For more info"))
270
+ const defaultSection = {
271
+ title: "Default",
272
+ body: " send with no command launches the terminal UI (same as `send tui`).",
273
+ }
274
+ if (moreInfoIndex < 0) sections.push(defaultSection)
275
+ else sections.splice(moreInfoIndex, 0, defaultSection)
276
+ })
252
277
 
253
278
  return cli
254
279
  }
255
280
 
256
- const ensureKnownCommand = (cli: CAC, argv: string[]) => {
281
+ const explicitCommand = (cli: CAC, argv: string[]) => {
257
282
  const command = argv[2]
258
- if (!command || command.startsWith("-")) return
259
- if (cli.commands.some(entry => entry.isMatched(command))) return
283
+ if (!command || command.startsWith("-")) return undefined
284
+ if (cli.commands.some(entry => entry.isMatched(command))) return command
260
285
  throw new ExitError(`Unknown command \`${command}\``, 1)
261
286
  }
262
287
 
263
- export const runCli = async (argv = process.argv) => {
264
- const cli = createCli()
265
- ensureKnownCommand(cli, argv)
266
- cli.parse(argv, { run: false })
288
+ export const runCli = async (argv = process.argv, handlers: CliHandlers = defaultCliHandlers) => {
289
+ const cli = createCli(handlers)
290
+ const command = explicitCommand(cli, argv)
291
+ const parsed = cli.parse(argv, { run: false }) as { options: Record<string, unknown> }
292
+ const helpRequested = !!parsed.options.help || !!parsed.options.h
293
+ if (!command && !helpRequested) {
294
+ await handlers.tui(parsed.options)
295
+ return
296
+ }
267
297
  await cli.runMatchedCommand()
268
298
  }
269
299
 
package/src/tui/app.ts CHANGED
@@ -97,6 +97,7 @@ const NAME_INPUT_ID = "name-input"
97
97
  const DRAFT_INPUT_ID = "draft-input"
98
98
  const TRANSPARENT_BORDER_STYLE = { fg: rgb(7, 10, 12) } as const
99
99
  const METRIC_BORDER_STYLE = { fg: rgb(20, 25, 32) } as const
100
+ const DEFAULT_WEB_URL = "https://send.rt.ht/"
100
101
 
101
102
  const countFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 })
102
103
  const percentFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 })
@@ -107,6 +108,32 @@ export const visiblePanes = (showEvents: boolean): VisiblePane[] => showEvents ?
107
108
 
108
109
  const noop = () => {}
109
110
 
111
+ const hashBool = (value: boolean) => value ? "1" : "0"
112
+
113
+ export const resolveWebUrlBase = (value = process.env.SEND_WEB_URL) => {
114
+ const candidate = `${value ?? ""}`.trim() || DEFAULT_WEB_URL
115
+ try {
116
+ return new URL(candidate).toString()
117
+ } catch {
118
+ return DEFAULT_WEB_URL
119
+ }
120
+ }
121
+
122
+ export const webInviteUrl = (
123
+ state: Pick<TuiState, "snapshot" | "hideTerminalPeers" | "autoAcceptIncoming" | "autoOfferOutgoing" | "autoSaveIncoming">,
124
+ baseUrl = resolveWebUrlBase(),
125
+ ) => {
126
+ const url = new URL(baseUrl)
127
+ url.hash = new URLSearchParams({
128
+ room: cleanRoom(state.snapshot.room),
129
+ clean: hashBool(state.hideTerminalPeers),
130
+ accept: hashBool(state.autoAcceptIncoming),
131
+ offer: hashBool(state.autoOfferOutgoing),
132
+ save: hashBool(state.autoSaveIncoming),
133
+ }).toString()
134
+ return url.toString()
135
+ }
136
+
110
137
  export const createNoopTuiActions = (): TuiActions => ({
111
138
  toggleEvents: noop,
112
139
  jumpToRandomRoom: noop,
@@ -505,6 +532,14 @@ const renderRoomCard = (state: TuiState, actions: TuiActions) => denseSection({
505
532
  onBlur: actions.commitRoom,
506
533
  }),
507
534
  ]),
535
+ ui.row({ id: "room-invite-slot", width: 6, justify: "center", items: "center" }, [
536
+ ui.link({
537
+ id: "room-invite-link",
538
+ label: "📋",
539
+ accessibleLabel: "Open invite link",
540
+ url: webInviteUrl(state),
541
+ }),
542
+ ]),
508
543
  ]),
509
544
  ])
510
545