Skip to content

cli: add command-line interface - #29

Open
nak2002k wants to merge 34 commits into
denislupookov:mainfrom
nak2002k:enhancement/cli-support
Open

cli: add command-line interface#29
nak2002k wants to merge 34 commits into
denislupookov:mainfrom
nak2002k:enhancement/cli-support

Conversation

@nak2002k

@nak2002k nak2002k commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Adds a standalone CLI tool (altersend) for P2P file transfer without a GUI. Targets users who prefer terminal workflows or need automation scripts.

Changes

  • New apps/cli/ package with TypeScript strict mode
  • send — share files, display join code or QR code, optional temp deletion
  • receive — download files from join code, auto-exits after transfer
  • peek — preview file offers without downloading
  • check-update / update — pear-based auto-update support
  • Progress bar with terminal width detection (supports small/split terminals)
  • Auto-isolated storage to prevent lock conflicts when running multiple instances
  • Proper disconnect messaging for connection failures vs transfer cancellations
  • --no-updates flag on all commands to skip OTA checks
  • --storage, --output, --qr, --temp flags for relevant commands

Testing

  • npm run lint (ESLint, --max-warnings=0)
  • npm run build -w packages/core
  • npm run build -w apps/cli
  • Manually verified send + receive end-to-end on Windows
  • Manually verified wrong join code shows "Connection failed or no sender"
  • Manually verified progress bar overwrites cleanly in full-size terminal
  • Manually verified Ctrl+C cancels cleanly without spurious messages

Screenshots / recordings

N/A (terminal-only tool)

Related issues

Closes #24

Summary by CodeRabbit

  • New Features

    • Full CLI for AlterSend with send/receive/peek/check-update/update commands, join-code validation, runtime management, in-terminal QR display, and update/apply behavior
    • Single-line progress UI with human-readable sizes, filename truncation, and interruption handling
  • Documentation

    • New CLI README covering install/build, commands/examples/flags, exit codes, storage defaults, architecture, and maintainer steps
  • Chores

    • Added CLI package manifest, npm scripts (cli:build, cli:dev, cli:test), TypeScript declarations, tsconfig, and test config

nak2002k added 28 commits June 12, 2026 09:54
- src/index.ts: paparam CLI entry point
- src/runtime.ts: worklet spawning via pear-runtime
- src/qr.ts: QR code terminal display
- src/commands/: send, receive, status, cancel, disconnect, peek
@nak2002k
nak2002k requested a review from denislupookov as a code owner June 12, 2026 07:44
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new AlterSend CLI app: runtime/type bindings and config, a paparam-based executable, join-code/QR/progress helpers, five commands (send, receive, peek, check-update, update), TypeScript/Vitest config, package manifest, README, and root npm scripts for building/testing the CLI.

Changes

AlterSend CLI Application

Layer / File(s) Summary
Runtime foundation & Configuration
apps/cli/src/types.d.ts, apps/cli/src/runtime.ts, apps/cli/package.json, apps/cli/tsconfig.json, apps/cli/vitest.config.ts
Ambient typings for pear-runtime, which-runtime, and paparam; createCliRuntime that instantiates PearRuntime and worker client with platform storage selection and lock-retry; CLI package manifest and TypeScript/test config.
CLI entrypoint & Utilities
apps/cli/src/index.ts, apps/cli/src/joinCode.ts, apps/cli/src/progress.ts, apps/cli/src/qr.ts
Node executable using paparam that parses/dispatches subcommands; join-code extraction/validation (64-hex chars); throttled single-line progress rendering; TTY-aware QR display.
Send command
apps/cli/src/commands/send.ts
Validates file paths and safety, hosts a transfer (client.host), optionally renders QR, waits for peer, shares files, reports progress, and handles SIGINT and error teardown.
Receive command
apps/cli/src/commands/receive.ts
Validates/extracts join code, ensures output dir, connects via worker client, converts offers to download requests, updates progress UI, handles interruption, and manages lifecycle.
Peek, Check-Update & Update commands
apps/cli/src/commands/peek.ts, apps/cli/src/commands/check-update.ts, apps/cli/src/commands/update.ts
Peek lists offered files without downloading; check-update waits up to 5s for updater notification; update applies staged updates immediately or after waiting up to 10s, all with event/timeouts and teardown.
Documentation & root scripts
apps/cli/README.md, CLAUDE.md, package.json
Adds comprehensive CLI README (usage, architecture, storage, maintainer steps), CLI subsection in CLAUDE.md listing cli:build, cli:dev, cli:test, and three root npm scripts to build/run/test the CLI.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as index.ts
  participant Command as Command Module
  participant Runtime as createCliRuntime
  participant Pear as PearRuntime
  participant Worker as WorkerClient
  participant Peer

  User->>CLI: npm run cli:dev send file.txt
  CLI->>Command: dispatch send(files, options)
  Command->>Runtime: createCliRuntime(storage, onEvent)
  Runtime->>Pear: new PearRuntime(config)
  Runtime->>Pear: run(workerEntry, args --storage=...)
  Runtime->>Worker: require and create worker client with onEvent
  Runtime->>Worker: await client.ready
  Runtime-->>Command: return {client, destroy, pear}
  Command->>Worker: client.host()
  Command->>User: display join code / QR
  Peer->>Worker: join with code
  Worker->>Command: onEvent('status','peer-connected')
  Command->>Worker: client.shareFiles(...)
  Worker->>Command: onEvent('status','download-progress')
  Command->>Command: writeProgress()
  Worker->>Peer: transfer chunks
  Worker->>Command: onEvent('status','disconnected')
  Command->>Runtime: destroy()
  Command->>User: exit 0
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • denislupookov

Poem

🐰 I hopped along the terminal light,
Shared a code, then queued the flight.
Bars and QR in a joyful spin,
Pear hummed softly, transfers begin.
A rabbit clap — the files hop in!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'cli: add command-line interface' accurately summarizes the main change: adding a CLI tool for the altersend project with multiple commands (send, receive, peek, check-update, update).
Linked Issues check ✅ Passed The PR implements a complete CLI tool addressing issue #24's requirement for command-line usage by providing send, receive, peek, check-update, and update commands with proper runtime management, storage handling, and progress tracking.
Out of Scope Changes check ✅ Passed All changes are in-scope: CLI implementation in apps/cli/ with required files, package configuration, TypeScript setup, and minimal root-level changes (CLAUDE.md update and npm scripts). No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@nak2002k nak2002k changed the title feat(cli): add command-line interface for P2P file transfer cli: add command-line interface Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/README.md`:
- Line 158: Add a regression test that verifies the CLI falls back to a temp
directory when the default storage is locked: in the CLI test suite create a
test that, without passing --storage, sets up the same lock file path checked by
createCliRuntime() (the .../app-storage/core/db/LOCK used in
apps/cli/src/runtime.ts), then invokes the CLI runtime creation path and asserts
that the runtime used makeTempStorage() (or that the resolved storage dir is
inside a temp directory) and that providing --storage <path> bypasses the
fallback; reference createCliRuntime(), makeTempStorage(), and the storagePath
handling when locating the LOCK file to locate and implement the test.

In `@apps/cli/src/commands/check-update.ts`:
- Around line 13-18: When createCliRuntime(...) succeeds in check-update and
update commands, ensure the Pear runtime is always torn down on error: in both
check-update.ts and update.ts add a guarded cleanup before exiting by calling
runtime?.destroy() inside each catch/failure path (i.e., before any
process.exit(1) or early return), so that the runtime variable created by
createCliRuntime is destroyed on failure as well as success.

In `@apps/cli/src/commands/peek.ts`:
- Around line 12-25: The transfer-ready handler (onEvent) currently prints
offers then returns without stopping the command; change it to initiate shutdown
by calling client.disconnect() after printing the offers, and wire the existing
donePromise resolution to the status events 'disconnected' and
'peer-disconnected' (same pattern used in receive.ts) so the command resolves
only after the client emits a disconnect; ensure you do not resolve the
donePromise immediately in onEvent but wait for the
disconnected/peer-disconnected event to resolve it.

In `@apps/cli/src/commands/receive.ts`:
- Around line 45-58: When downloadFiles() rejects or clientRef.disconnect()
throws, the code only logs the error and never settles donePromise, leaving the
command running; update the promise handling around
clientRef.downloadFiles(downloads) so that any rejection from downloadFiles()
immediately settles donePromise (reject or resolve as your donePromise API
expects) and, after a successful download when calling clientRef.disconnect(),
chain a .catch on disconnect() to also settle donePromise on failure; ensure you
guard with the interrupted flag to avoid double-settling and reference the
existing clientRef.downloadFiles, clientRef.disconnect, interrupted and
donePromise symbols when making these changes.

In `@apps/cli/src/commands/send.ts`:
- Around line 77-82: The SIGINT handler currently awaits client.disconnect()
then calls exitDeferred(), which can stall if disconnect() rejects; modify the
process.on('SIGINT') handler (the interrupted guard block) to call
client.disconnect() inside a try/finally so exitDeferred() is always invoked
even on rejection — i.e., wrap await client.disconnect() in try { await
client.disconnect() } finally { exitDeferred() } and optionally log the
disconnect error inside a catch block before rethrowing or swallowing; apply the
same pattern in the corresponding SIGINT handler in receive.ts.

In `@apps/cli/src/commands/update.ts`:
- Around line 23-39: The promise created around the updater never rejects so
failures in the async onUpdated handler (which awaits
runtime!.pear.updater.applyUpdate()) are swallowed; modify onUpdated (the
handler registered with runtime!.pear.updater.on('updated', onUpdated)) to catch
any errors from applyUpdate() and call the Promise's reject (or propagate the
error to the outer try/catch) after removing the listener and clearing the
timeout, or alternatively remove async/await from onUpdated and forward the
applyUpdate() promise to the outer Promise so the outer Promise rejects on
failure; ensure you still call runtime?.destroy() on success and always clean up
the listener/timeout on both success and failure.

In `@apps/cli/src/index.ts`:
- Around line 75-97: The extractFilesFromRest function incorrectly assumes any
unknown flag without '=' has a value and does i += 2, which can skip real file
args; change the logic in extractFilesFromRest so that when encountering a flag
(item.startsWith('--')) and it is not one of the known boolean flags and has no
'=', you only skip the next token if it exists AND does not start with '--'
(i.e., it's a value), otherwise advance by 1; update the branch that sets i += 2
to perform a bounds-and-prefix check on items[i+1] before skipping it, using the
existing variables flagName and items to locate the code.

In `@apps/cli/src/progress.ts`:
- Around line 15-23: The writeProgress function marks completion and computes
the displayed percent/bar from a rounded value which can prematurely set
finalised and cause negative repeat() lengths when current > total; change
finalised to be set when state.current >= state.total, compute pct from a
clamped ratio (clamp Math.round((current/total)*100) into 0..100) or clamp the
intermediate ratio to 0..1 before multiplying, and ensure any derived filled
value uses a clamped range (0..PROGRESS_CHARS) or compute filled from the
clamped pct so PROGRESS_CHARS - filled cannot go negative; update the same logic
used by formatLine/other occurrences (see writeProgress, formatLine,
ProgressState, finalised, lastPct, lastWrite, THROTTLE_MS, PROGRESS_CHARS) to
keep behavior consistent.

In `@apps/cli/src/qr.ts`:
- Around line 7-10: The else branch in displayQR (apps/cli/src/qr.ts) prints the
join code again in non-TTY mode; remove the duplicate console.log for `Join
code: ${topic}` and only emit the suppression note `(QR code suppressed —
non-TTY environment)` so callers (e.g., send command) remain the single source
of the join code output. Ensure the function still returns/behaves the same
otherwise.

In `@apps/cli/src/runtime.ts`:
- Around line 48-75: The current preflight probe in isStorageLocked +
createCliRuntime is racy; instead keep using getDefaultStorage() but attempt to
initialize the runtime normally and only fall back to a temp dir when the real
startup fails due to the corestore LOCK. Update createCliRuntime to try using
the chosen dir (defaultPath) first, catch the specific lock error thrown during
runtime/worklet startup (detect via error message/errno from the corestore/open
routine), then call makeTempStorage() and retry initialization with that temp
dir (ensure you only fallback on lock-related errors and propagate other
errors). Keep isStorageLocked as optional utility or remove its use in this
flow; reference functions: isStorageLocked, createCliRuntime, makeTempStorage,
and the runtime initialization code path that opens corestore/worklet.
- Around line 31-36: The two functions getWorkerEntryPath() and
getWorkerClientPath() currently build checkout-relative node_modules paths which
can resolve incorrectly; change them to resolve the package using Node's module
resolution (e.g. require.resolve('`@altersend/core/dist/worklet/index.js`') and
require.resolve('`@altersend/core/dist/client/worker-client.js`')) instead of
path.join with __dirname, and throw or log a clear error if require.resolve
fails so worker startup fails fast with a helpful message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 896485eb-a822-45fe-bf31-a97b151252c1

📥 Commits

Reviewing files that changed from the base of the PR and between 273d4c9 and 1ef8455.

📒 Files selected for processing (17)
  • CLAUDE.md
  • apps/cli/README.md
  • apps/cli/package.json
  • apps/cli/src/commands/check-update.ts
  • apps/cli/src/commands/peek.ts
  • apps/cli/src/commands/receive.ts
  • apps/cli/src/commands/send.ts
  • apps/cli/src/commands/update.ts
  • apps/cli/src/index.ts
  • apps/cli/src/joinCode.ts
  • apps/cli/src/progress.ts
  • apps/cli/src/qr.ts
  • apps/cli/src/runtime.ts
  • apps/cli/src/types.d.ts
  • apps/cli/tsconfig.json
  • apps/cli/vitest.config.ts
  • package.json

Comment thread apps/cli/README.md
Comment thread apps/cli/src/commands/check-update.ts
Comment thread apps/cli/src/commands/peek.ts
Comment thread apps/cli/src/commands/receive.ts
Comment thread apps/cli/src/commands/send.ts
Comment thread apps/cli/src/index.ts
Comment thread apps/cli/src/progress.ts
Comment thread apps/cli/src/qr.ts
Comment thread apps/cli/src/runtime.ts Outdated
Comment thread apps/cli/src/runtime.ts Outdated
nak2002k added 3 commits June 12, 2026 17:27
- worker paths: use require.resolve() instead of hardcoded relative path
- race condition: catch lock error at runtime, fallback to temp storage
- SIGINT teardown: add try/finally in send.ts and receive.ts
- peek disconnect: proper done/resolve flow, calls disconnect after offers
- progress overshoot: clamp values, use current>=total for finalised
- QR duplicate: remove duplicate join code log in non-TTY path
- error cleanup: add runtime?.destroy() in catch blocks of check-update and update
- extractFilesFromRest: fix unknown flag handling, skip single token not pair
- lock file: sync package-lock.json
@nak2002k
nak2002k force-pushed the enhancement/cli-support branch from 32c01cc to 4201986 Compare June 12, 2026 08:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/runtime.ts`:
- Around line 86-99: The fallback path creates a temporary PearRuntime named
fallbackPear and starts the worker/client from it, but the function still
returns the original pear instance; fix this by returning or assigning the
fallback instance instead of the locked original: when you construct
fallbackPear (PearRuntime) and call fallbackPear.run(...) and createClient(...),
set pear = fallbackPear (or directly return fallbackPear) so runtime.pear, its
.storage and .updater/event listeners point to the actual runtime in use (ensure
code paths that expect pear afterwards use the updated reference).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 022ca422-48da-46c7-957b-42f73fa8c9d9

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef8455 and 32c01cc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • apps/cli/src/commands/check-update.ts
  • apps/cli/src/commands/peek.ts
  • apps/cli/src/commands/receive.ts
  • apps/cli/src/commands/send.ts
  • apps/cli/src/commands/update.ts
  • apps/cli/src/index.ts
  • apps/cli/src/progress.ts
  • apps/cli/src/qr.ts
  • apps/cli/src/runtime.ts
💤 Files with no reviewable changes (1)
  • apps/cli/src/qr.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/cli/src/commands/update.ts
  • apps/cli/src/index.ts
  • apps/cli/src/commands/receive.ts
  • apps/cli/src/commands/check-update.ts
  • apps/cli/src/commands/send.ts
  • apps/cli/src/commands/peek.ts

Comment thread apps/cli/src/runtime.ts

@denislupookov denislupookov left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @nak2002k , thanks for the adding CLI support !

I tried running it locally and it crashes on running the commands, so that's the main thing to fix before this can go in. A couple of the send options don't seem to work either, and the test script runs nothing.

Can you take another look? Thanks!

Comment thread apps/cli/README.md

## Releasing (for maintainers)

The CLI ships via [pear](https://pear.software) — same infrastructure as desktop app.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should link to https://pears.com, but let's just drop that link and the releasing section for now. I'll add proper release support later.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @denislupookov,
mb I will take a look and fix the things this week.
ping me if you need any specific changes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Command line version

2 participants