@remotedraw/cli 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/cli.d.ts +17 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +79 -23
- package/dist/generated/agent-skill.d.ts +1 -1
- package/dist/generated/agent-skill.d.ts.map +1 -1
- package/dist/generated/agent-skill.js +1 -1
- package/dist/index.js +1 -1
- package/dist/locales/en.d.ts +8 -8
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/en.js +8 -8
- package/dist/locales/index.d.ts +8 -8
- package/dist/locales/nl.d.ts.map +1 -1
- package/dist/locales/nl.js +7 -7
- package/dist/packageManagers.d.ts +75 -0
- package/dist/packageManagers.d.ts.map +1 -0
- package/dist/packageManagers.js +146 -0
- package/dist/scan.d.ts +19 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +22 -3
- package/dist/update.d.ts +11 -5
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +55 -28
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// GENERATED FILE — do not edit.
|
|
2
2
|
// Source: docs/agents/remotedraw/SKILL.md
|
|
3
3
|
// Regenerate: bun run --cwd packages/cli generate:skill
|
|
4
|
-
export const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Integrate RemoteDraw phone drawing into a customer's product — scan the codebase, ask the product questions, get approval before installing, then wire the receiver and the sender with the SDK's own components (React/Svelte/JS receiver, hosted web sender, RemoteDrawSenderKit full-screen iOS sender).\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks what RemoteDraw is, whether it fits their app,\nor to create, debug, or review a RemoteDraw integration.\n\n## What RemoteDraw is, in four sentences\n\nA phone becomes a pen for a screen. Your **backend** creates a **session** with\nan `rd_sk_` API key; your **receiver** (web, desktop, or any client) shows the\nsession's ink using a `rd_recv_` token; a **sender** (the hosted `/join` page\nthat any QR opens, the RemoteDraw iOS app, or your own iOS app through\n`RemoteDrawSenderKit`) draws with a `rd_send_` token. Strokes stream as drafts\nwhile the finger is down and commit on release; nothing runs on the customer's\ninfrastructure except the session-creation call.\n\n- API: `https://api.remotedraw.com` · Docs: `https://docs.remotedraw.com/docs`\n (agent summary: `https://docs.remotedraw.com/llms.txt`) · Keys:\n `https://dashboard.remotedraw.com/api/keys`\n- Packages: `@remotedraw/cli`, `@remotedraw/react`, `@remotedraw/svelte`,\n `@remotedraw/client`, `@remotedraw/protocol`, `@remotedraw/geometry`,\n and the SwiftPM package `https://github.com/AxioSOzo/remotedraw-swift.git`\n (product `RemoteDrawSenderKit`).\n\n## The flow — in this order\n\n1. **Explain before touching anything.** If the user is asking what RemoteDraw\n can do, answer from this file and the docs. Do not install, scaffold, or\n create sessions to answer a question.\n2. **Ask for approval before installing.** Name exactly what you want to add\n (`npm install -g @remotedraw/cli`, `@remotedraw/react`, a Swift package, a\n dashboard project + key) and why, then wait. This includes `remotedraw init`\n without `--offline`, which provisions a billable project and key.\n3. **Scan the codebase.** `remotedraw scan --format json` (or\n `npx @remotedraw/cli@latest scan --format json` before the CLI is installed)\n reports the web/server/iOS projects, where an `rd_sk_` key may live, any\n RemoteDraw wiring already present, the integration options that fit, and\n the product questions to ask. Read it; verify its `evidence` where it\n matters.\n4. **Ask the product questions.** They come back in the scan's `questions`\n array. The ones that decide the build: what is drawn on, which existing\n screen the receiver goes on, who holds the phone (signed-in user → direct\n sender, anyone → QR), what Submit does, and demo vs. real feature. Do not\n invent answers; a one-page \"drawing lab\" is only right when the user says a\n demo is what they want.\n5. **Propose one plan, then build all of it.** Receiver, session creation,\n sender, and the exit/submit path — an integration is not done when ink\n appears once on a test page. Build beside existing features; never delete\n or replace a host-app feature on your own initiative.\n6. **Verify by using it.** Run `remotedraw doctor --format json`, open a real\n session, draw from a phone (or `create-input --execute` + the hosted join\n URL), and confirm ink lands on the receiver. On iOS, run it on a device or\n simulator and look at the screen; a compiling canvas is not a working one.\n\n## Choosing the pieces\n\nReceiver (the screen that shows ink):\n\n| Host | Use | Not |\n| --- | --- | --- |\n| React / Next / Remix | `@remotedraw/react`: `RemoteDrawProvider`, `RemoteDrawReceiver`, `PairingCode`, `RemoteDrawSessionControls` | A hand-rolled SVG or polling loop |\n| Svelte / SvelteKit | `@remotedraw/svelte` receiver store | — |\n| Anything else with JS | `@remotedraw/client`: `createHttpReceiverClient`, `createRealtimeReceiverSource` | — |\n| No JS at all | Raw HTTP `POST /v1/receiver/*` with the receiver token | — |\n\nSender (the phone):\n\n| Situation | Use | Not |\n| --- | --- | --- |\n| No phone app in the product | **Hosted `/join` page.** Render `joinUrl` as a QR (`PairingCode`). Full-screen, polished, zero sender code; the RemoteDraw iOS app opens the same link. | An in-page web sender (`--sender embedded-web`) unless the user explicitly wants drawing inside their own web page |\n| The product has its own iOS app | **`RemoteDrawSenderKit`** full-screen surface (below). Backend mints `rd_send_` via `POST /v1/sessions/direct-sender` for a signed-in user; QR join stays as the fallback. | A custom canvas, a raw-HTTP Swift client, or `remotedraw init --sdk swift` when adding a SwiftPM dependency is possible |\n| Headless / tests | `POST /v1/join` → `/v1/sender/draft` → `/v1/sender/commit` | — |\n\nSession creation (`POST /v1/sessions`) needs the `rd_sk_` key and therefore\nruns only where the scan found server-side code: a Convex action, a Next route\nhandler, an Express/Hono route, a serverless function. If the scan found none,\nask where the backend is. Never scaffold `createRemoteDrawSession.ts` into a\nVite/Next client tree — `--target web` in `remotedraw init` still writes it\nunder `src/`; move it, or scaffold into a scratch directory and copy only what\nbelongs.\n\n## iOS: the bar\n\nThe reference is the RemoteDraw app's own drawing screen, and the SDK ships it.\n\n```swift\n// Package.swift / Xcode → Add Package\n.package(url: \"https://github.com/AxioSOzo/remotedraw-swift.git\", from: \"0.1.0\")\n.product(name: \"RemoteDrawSenderKit\", package: \"remotedraw-swift\")\n\nimport RemoteDrawSenderKit\n\n// Once, at app start.\nRemoteDraw.configure(.init(apiBaseURL: .production))\n\n// Wherever drawing starts. `token` is the rd_send_ string your backend\n// minted with POST /v1/sessions/direct-sender (a QR's rd_join_ works too).\nButton(\"Draw\") { drawing = true }\n .remoteDrawSurface(isPresented: $drawing, senderToken: token) { outcome in\n switch outcome {\n case .submitted(let receipt): record(receipt)\n case .left: break\n case .expired: refreshSession()\n case .failed(let error): report(error)\n }\n }\n```\n\nRules:\n\n- The drawing surface is **full screen**: `.remoteDrawSurface` (a\n `fullScreenCover`) or `RemoteDrawTakeover` inside your own cover or\n `UIHostingController`. A sheet is acceptable only if `RemoteDrawSurface`\n fills it edge to edge; the surface then owns the gestures and the sheet must\n not be draggable while drawing. There is no small-canvas option.\n- The user can always leave — the surface has its own exit; `RemoteDrawExit`\n only decides whether leaving with unsubmitted ink asks first — and the host\n gets an outcome. Submit, undo, clear, instruments, and paper are the SDK's.\n- Do not write a `UIViewRepresentable` canvas, a draft loop, or an HTTP client:\n cadence, point budgets, the packed-point codec, sequence healing, token\n refresh, and presence are protocol, and `RemoteDrawSenderSession` already\n implements them. If you must go headless, compose `RemoteDrawInkCanvas` +\n `RemoteDrawStrokeCapture` on that session — never raw `URLSession`.\n- `RemoteDraw.configure` is the only setup; the SDK needs no `Info.plist` keys.\n- Customers do not ship a separate RemoteDraw app; their app *is* the sender.\n\n## CLI\n\n```sh\nremotedraw options --format json # the option catalog\nremotedraw scan --format json # read the codebase first (step 3)\nremotedraw init --non-interactive --offline --dry-run --format json \\\n --path apps/web --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset sketch --json\nremotedraw create-input --execute # a real session + join URL, needs a key\n```\n\n`--sender own-ios` requires `--sdk swift`; other invalid combinations fail with\n`INVALID_COMBINATION`. By default `init`/`new` also create a dashboard project\nand a project-scoped development key in `.env.local` — that is the step that\nneeds approval (step 2); `--offline` writes files only. Never pass `--force`\nunless the user approved overwriting. Do not drive the interactive wizard or\nscrape human-formatted output; every command has `--format json`.\n\n## Security and tenancy\n\n- `rd_sk_…` keys: backend secrets only. Never in browser bundles, Swift, app\n bundles, screenshots, logs, or generated examples.\n- The account-level `rd_cli_…` credential stays in the user config directory;\n never copy it into a project. `REMOTEDRAW_CLI_TOKEN` is for CI secrets only.\n- Public clients receive only `joinUrl`, `joinToken`, `receiverToken`, or\n `senderToken`, each scoped to one session. Production QR codes use the HTTPS\n `joinUrl`, not the custom scheme.\n- One key serves every customer of the product, so a session id is not a\n capability. Create sessions with `externalId: \"<product>:<tenant>\"`, and\n check it (`POST /v1/sessions/get`) before attaching a sender or ending a\n session on a tenant's behalf.\n\n## Gotchas the SDKs hide and hand-written code hits\n\n- `POST /v1/sessions` is billable and not idempotent. React StrictMode runs\n mount effects twice in development: guard with a ref, or create the session\n in a server action / loader. Store the `receiverToken` if the receiver\n outlives a page load — it is the only credential that reads a session's ink.\n- Timestamps are integer milliseconds. `occurredAt` and point `t` values are\n accepted with a fraction (floored) but a hand-written client should send\n integers.\n- Committed points come back in board space, remapped through\n `device.aspectRatio`; send the aspect ratio of the pad the finger touches.\n- `RemoteDrawReceiver` defaults `phones` and `pointers` to on. A connected\n phone that enters viewport mode is drawn on the board as a phone outline; pass\n `phones={false}` for a plain surface where that is not wanted.\n- `PairingCode` hides the join URL text unless `showLink`; pass it when a human\n needs the link without a camera.\n- `joinTokenExpiresAt` is earlier than the session's `expiresAt`: the QR dies\n first, the board stays live.\n\n## API contract\n\n- Backend: `POST /v1/sessions` (create), `/v1/sessions/get`, `/v1/sessions/end`,\n `/v1/sessions/direct-sender` (mint `rd_send_` for your own app),\n `/v1/sessions/join-token` (a fresh QR).\n- Receiver: `POST /v1/receiver/session`, `/drawings`, `/drafts`, `/senders`\n with the receiver token, or the realtime source in the SDKs.\n- Sender: `POST /v1/join` (spends a join token; revokes other senders), then\n `/v1/sender/draft` (latest-only preview, throttle to ~32 ms),\n `/v1/sender/commit` (one durable stroke per pointer-up with a stable\n `clientStrokeId`), `/v1/sender/submit`.\n\n## AI actions\n\nReach for AI when the product needs something _from_ the finished drawing:\na generated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit,\nsubmit, or presence event — AI runs only on an explicit `POST /v1/ai-actions`\ncall the user asked for. Run it after the user is done; the route accepts\n`active` and `ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use `createAiAction` + `waitForAiAction` on\n`createHttpRemoteDrawApiClient` from `@remotedraw/client` (re-exported by\n`@remotedraw/react`) — backend only, it holds the key. There is no completion\nwebhook. See https://docs.remotedraw.com/docs/api#ai.\n\n## Verification\n\nAfter changes, verify against the customer's project — never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` —\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.";
|
|
4
|
+
export const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Integrate RemoteDraw phone drawing into a customer's product — a phone becomes the pen for a screen the customer already owns. Covers the two-device model, which flows are valid, the ready-made components (React/Svelte/JS receiver, hosted web sender, RemoteDrawSenderKit iOS sender), per-surface recipes, and how to judge whether a proposed use case fits at all.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks what RemoteDraw is, whether it fits their app,\nor to create, debug, or review a RemoteDraw integration.\n\nRead the whole file before proposing a design. The first two sections decide\nwhether the use case is possible; everything after decides how it is built.\n\n## The model: two devices, always\n\nRemoteDraw is not a drawing library. It is a wire between **two devices**.\n\n- **The receiver is the paper.** A screen someone is looking at — a laptop, a\n desktop, a large display, a kiosk, a tablet on a desk — showing *your*\n product. Your app renders whatever is being drawn on: the map, the photo, the\n PDF page, the form, the whiteboard. RemoteDraw renders none of that content.\n It paints ink on top of it.\n- **The sender is the pen.** A phone. It supplies a hand, pressure, tilt and a\n stroke. It does **not** supply the content: the iOS SDK opens no camera and no\n file picker (it needs no `Info.plist` entries at all). On a bounded surface\n the phone's pad *is* the surface; on a large one the phone is a viewport\n moving over it.\n- **RemoteDraw is the wire.** A hosted session carries strokes from the pen to\n the paper in real time and stores them. Nothing runs on the customer's\n infrastructure except the session-creation call.\n\nThe one question that decides whether RemoteDraw fits:\n\n> **What is being drawn on, and which screen is it already displayed on?**\n\nIf the answer is \"a screen the user is looking at, and they wish they could\ndraw on it with their hand\" — that is RemoteDraw. If the answer is \"the phone's\nown screen\", it is not: a phone drawing on its own content and uploading the\nresult is a camera-and-canvas feature you build with PencilKit or a `<canvas>`,\nand RemoteDraw would only add a round trip.\n\nThree consequences, because they are the mistakes integrators actually make:\n\n1. **The phone never supplies the picture.** \"The tenant photographs the leak\n and circles it\" is *not* a RemoteDraw flow — there is no second screen. The\n RemoteDraw version of that job: the photo is already open in your web app on\n the office desktop, and the person at that desk circles the leak with their\n phone instead of a mouse.\n2. **The receiver already exists.** RemoteDraw goes onto a screen your product\n already has. It does not get its own page unless the user asks for a demo.\n3. **Ink is coordinates, not pixels.** Strokes arrive in normalized board space\n (`0..1`, remapped through the sender's `device.aspectRatio`). Your app\n decides what board space *means* — a pixel, a page, a field, a coordinate on\n Earth. RemoteDraw never sees your content.\n\n## Which flows are valid\n\nRead the row for the **sender** (who holds the phone) and the column for the\n**receiver** (the screen showing the content).\n\n| Sender (the pen) | Receiver (the paper) | Valid? | Notes |\n| --- | --- | --- | --- |\n| iPhone — RemoteDraw app, your app via `RemoteDrawSenderKit`, or hosted `/join` in Safari | Desktop / laptop browser | **Yes — the canonical flow** | Everything below is written for it. |\n| iPhone (any of the three) | Large display, TV, projector, kiosk browser | **Yes** | Size `target.coordinateSpace` to the display. |\n| iPhone (any of the three) | Desktop app — Electron, macOS, Windows — via `@remotedraw/client` or raw HTTP | **Yes** | No React needed; poll `/v1/receiver/*` or use the realtime source. |\n| Android phone — hosted `/join` in Chrome | Any of the above | **Yes** | There is no native Android SDK. The hosted join page *is* the Android sender and it is full-featured. |\n| iPhone / Android | iPad or tablet browser, as a **second** device someone else is looking at | **Yes** | Two devices, two people. |\n| Headless script, test, or agent — raw `POST /v1/join` → `/v1/sender/draft` → `/v1/sender/commit` | Any receiver | **Yes, for verification only** | Never ship a hand-rolled sender to users. |\n| Any phone | **The same phone** — one device shows the content and draws on it | **No** | There is no second screen. Use PencilKit / `<canvas>`. RemoteDraw adds a network hop and nothing else. |\n| A phone that must first **capture** the content — photograph or scan it | *(anything)* | **No** | The iOS sender opens no camera and needs no `Info.plist` entries. The content must already be on the receiver. |\n| Desktop mouse or trackpad as the pen | *(anything)* | **No — does not exist** | There is no desktop sender. The macOS trackpad sender is unbuilt research. Do not promise it. |\n| Phone → phone, two different people, two different devices | Phone browser as receiver | *Technically yes, rarely right* | A phone browser is a browser. But if both people hold phones, ask why the drawing is not simply in one app. |\n\n**The three nevers of the model.**\n\n1. **Never same-device.** If sender and receiver would be one phone, stop and\n say so. Propose the non-RemoteDraw alternative.\n2. **Never make the phone the source of content.** The receiver supplies what\n is drawn on. (One honest exception: the hosted `/join` pad has a file-attach\n tool that can put a file on the board. It is a hosted-sender capability, not\n a way to make a same-device flow valid, and it cannot currently be disabled.)\n3. **Never promise a sender RemoteDraw does not ship.** iOS (native SDK) and\n any mobile browser (hosted `/join`) are the senders. Nothing else exists.\n\n## Never do these\n\n- **Never write a custom canvas.** Not a `UIViewRepresentable` drawing view, not\n a `<canvas>` sender pad, not a hand-rolled SVG receiver, not a raw\n `URLSession`/`fetch` draft loop. Cadence, point budgets, the packed-point\n codec, sequence healing, token refresh and presence are protocol, and the SDKs\n implement them. Go headless on the SDK's own primitives if you must.\n- **Never put the iOS surface in a small pad or a draggable sheet.** It is\n full screen (`.remoteDrawSurface` / `RemoteDrawTakeover`). A sheet is\n acceptable only if `RemoteDrawSurface` fills it edge to edge and the sheet\n cannot be dragged mid-stroke. There is no small-canvas option.\n- **Never create a session on mount or on page load.** `POST /v1/sessions` is\n billable and not idempotent, and React StrictMode fires mount effects twice.\n Create it on the server (route handler, loader, server action) or on explicit\n user intent, and guard with a ref if it must be a client effect.\n- **Never let an `rd_sk_…` key reach a client.** Not browser bundles, not Swift,\n not app bundles, screenshots, logs, or generated examples. The phone never\n calls `/v1/sessions/direct-sender`; your backend does.\n- **Never poll by hand when a component or store exists.** `RemoteDrawProvider`\n / `createReceiverStore` already do one-in-flight polling plus an optional\n realtime push source.\n- **Never promise Bluetooth or Wi-Fi pairing.** The API advertises\n `bluetooth` and `localNetwork` as pairing methods and the iOS/Android apps do\n *advertise* on those radios, but **nothing scans, browses, or connects\n anywhere in the product**. They are not implemented. Do not present them as\n options; do not build UI around them. QR and direct sender are the real ones.\n- **Never promise a native Android SDK, a desktop sender, per-tenant Universal\n Links, e-signature compliance, or streaming inside `RemoteDrawSenderKit`.**\n See \"What does not exist yet\".\n- **Never delete or replace a host-app feature on your own initiative.** Build\n beside it.\n\n## The decision tree\n\n**1. What is being drawn on? → `target.kind` + `inputMapping`.**\n\n`target.kind` accepts `whiteboard`, `paper`, `canvas`, `svg`, `map`, `tldraw`,\n`field`, `image`, `pdf`, `screen`, `custom`. It selects the background the\nreceiver defaults to, the tool policy, and deposit defaults.\n`target.inputMapping` is `surface` (the phone's whole pad *is* the target — use\nfor bounded targets like a signature field) or `viewport` (the phone is a\nmovable window over a larger board — the default when omitted).\n\n| Kind | Host renders | `inputMapping` | Sender | Status |\n| --- | --- | --- | --- | --- |\n| `whiteboard` / `paper` | nothing — RemoteDraw's own ground | `viewport` (or `surface`) | hosted `/join` or SenderKit | shipped |\n| `image` (photo, screenshot) | the `<img>`, as `background` | `surface` (whole photo) or `viewport` (zoomable) | either | shipped |\n| `pdf` | your PDF renderer, one page at a time, as `background` or behind a `transparent` receiver | `viewport` | either | shipped; RemoteDraw renders no PDFs |\n| `field` (signature, initials) | the form, with a baseline as SVG `children` | **`surface`** — mandatory | either | shipped; **not** an e-signature product |\n| `screen` | a screenshot, or a live stream you publish | `viewport` | either | shipped; live view is experimental |\n| `map` | your map (Mapbox / MapLibre / Leaflet / Google) | `viewport` | hosted `/join` or SenderKit | shipped — use `RemoteDrawMapReceiver` |\n| `custom` | anything else you own | `viewport` | hosted `/join` (streaming) or SenderKit (ink only) | shipped |\n\n**2. Which stack renders the receiver? → the composition.**\n\n| Host | Use | Not |\n| --- | --- | --- |\n| React / Next / Remix | `@remotedraw/react`: `RemoteDrawProvider` + `RemoteDrawReceiver` (or `RemoteDrawMapReceiver`) + `PairingCode`/`RemoteDrawConnect`/`RemoteDrawLaunchButton` + `RemoteDrawSessionControls` | A hand-rolled SVG or polling loop |\n| Svelte / SvelteKit | `@remotedraw/svelte`: `createRemoteDrawReceiver` store + `createDirectSender` store. **No components ship** — you write all the markup, including ink rendering | Assuming React's components exist here |\n| Anything else with JS (Electron, Vue, vanilla) | `@remotedraw/client`: `createHttpReceiverClient`, `createReceiverStore`, `createRealtimeReceiverSource` | — |\n| No JS at all | Raw HTTP `POST /v1/receiver/*` with the receiver token | — |\n\n**3. Who holds the phone? → the sender path.**\n\n| Situation | Path |\n| --- | --- |\n| Anyone who can scan; no phone app in the product | **Hosted `/join`.** Render `joinUrl` as a QR with `PairingCode`. Zero sender code. The RemoteDraw iOS app opens the same HTTPS link through Universal Links; every other phone gets the web pad. |\n| The product has a first-party iOS app the user already installed *and* it is the RemoteDraw app | Same QR. Universal Links open it. |\n| The product has **its own** iOS app and the user is signed in | **Direct sender.** Backend calls `POST /v1/sessions/direct-sender` with `launchUrlTemplate: \"yourapp://draw?senderToken={senderToken}\"`; the browser shows `RemoteDrawLaunchButton`; the app receives the URL in `onOpenURL` and presents `.remoteDrawSurface(isPresented:senderToken:)`. Keep the QR as the fallback — the button reveals one automatically. |\n| A QR that opens the **customer's own** app | **Not possible.** The Universal-Link association file has one hard-coded appID and the QR host is a RemoteDraw deployment setting. Use the direct sender instead. |\n| Headless / tests | `POST /v1/join` → `/v1/sender/draft` → `/v1/sender/commit` |\n\n## Component catalogue\n\nEverything below is a real export, read from source. Anything not listed does\nnot exist. Grouped by decision: the board → what is behind it → pairing →\nsession state → AI → clients → iOS.\n\n### `@remotedraw/react`\n\n**`RemoteDrawProvider`** — receiver session state, polling, and actions.\nRequires a `receiver` client to do anything.\nProps: `children`, `session`/`sessionId`/`joinUrl`/`joinUrls`/`joinTokenUse`/`pairing`/`receiverToken`,\n`receiver` (a `ReceiverClient` — **without it the store is inert and silent**),\n`createSession`, `createSessionRequest` (default `{ target: { kind: \"custom\" } }`),\n`autoCreate` (default: true when `createSession` and no `session`),\n`pollIntervalMs` (`1000`), `draftPollIntervalMs` (`250`), `source` (`null`),\n`fallbackToPolling` (`true`), `initialDrawings`/`initialDrafts`/`initialSenders` (`[]`),\n`onError` (`(error: Error) => void` — **wire it; it is the only signal for a dead credential**).\nContext: `session, sessionId, joinUrl, joinUrls, joinTokenUse, pairing, receiverToken, credentials, drawings, drafts, senders, loading, creating, error, transport, create, refetch, ingest, undo, clear`.\n*Limits:* `error` is a plain `Error`; narrow with `instanceof RemoteDrawHttpError`\nfor `.status`/`.code`/`.shouldReJoin`.\n\n**`RemoteDrawReceiver`** — the receiver foundation. Paints committed ink, live\ndrafts, sender pointers and connected phones over a configurable background, and\nprescribes nothing around it.\nProps: `drawings`/`drafts`/`senders` (fall back to the provider), `background`\n(default: the session target's surface inside a provider, else `\"whiteboard\"`;\naccepts a surface name, `\"transparent\"`, any CSS background string, or any\nReactNode), `pointers` (`true`), `phones` (`true`), `pointerColor` (`#1f7a8c`),\n`strokeColor` (`#151512`), `draftColor` (`#1f7a8c`), `strokeWidth` (`6`),\n`coordinateAspectRatio` (default: the session's `coordinateSpace`, else `1`;\nalso sets CSS `aspect-ratio`), `preserveAspectRatio` (`\"none\"`),\n**`projectPoint`** (`(point) => {x,y} | null` in **CSS pixels from the\nreceiver's top-left** — the seam for a camera; return `null` to drop a point),\n**`space`** (the same seam in the receiver's own viewBox coordinates; `projectPoint`\nwins when both are given), `animate` (`true`), `children` (SVG overlay in\n`0..surfaceWidth × 0..1000`), `className`/`style`/`svgProps`/`aria-label`.\n*Defaults that surprise:* it renders immediately with no empty state; `phones`\nand `pointers` are on.\n*Limits:* without `projectPoint`/`space` it stretches board space across its own\nelement — correct for a fixed board, silently wrong over a live map. Use\n`RemoteDrawMapReceiver` there.\n*Never:* stack it on a pannable map without a projection.\n\n**`RemoteDrawMapReceiver`** — `RemoteDrawReceiver` wired to a map the host owns.\nRenders a transparent ground (your map is the ground) plus the\nboard→geography→pixel projection. Takes every `RemoteDrawReceiver` prop except\n`projectPoint`/`space`, plus:\n`bounds` (the board's `target.coordinateSpace.bounds`; defaults to the session's\nown inside a provider — usually pass nothing), `mapBounds` (the map's current\nvisible bounds; **exact only for a north-up, unpitched camera**),\n`projectLngLat` (`(lng, lat) => {x,y} | null` — normally\n`map.project([lng, lat])`; exact under rotation and pitch, and wins over\n`mapBounds`).\n*Limits:* with neither `projectLngLat` nor `mapBounds` it renders unprojected\nand warns once in development. Leave `preserveAspectRatio` at `\"none\"`.\n*Never:* recompute the projection identity on every render — memoize it and bump\nit on the map's `move` event.\n\n**`RemoteDrawPhoneProjection`** — one connected phone drawn in place, as an SVG\n`<g>` for a host with its own `<svg>`. `layout` (required, from\n`phoneProjectionLayouts(senders)`), `space` (default 1000×1000), `color`,\n`model` (`\"auto\"`), `className`. `RemoteDrawReceiver` already renders these.\n\n**`PairingCode`** — the pairing component: the scannable code with live status,\nhover-to-copy, and subtle branding.\nProps: `joinUrl` (default: the provider's, falling back to `joinUrls.web`),\n`size` (`200`, or `\"fill\"`), `direction` (`\"paper\"`, one of 14 — TEMPORARY),\n`treatment` (`\"fluid\"`, one of 11 — TEMPORARY), `accentColor` (`#1f7a8c`),\n`inset`, `tile` (`true`), `logo` (the RemoteDraw mark; `false` for none, a\nstring for a URL), `logoPlacement` (`\"plate\"`), `title`/`description`,\n`showStatus` (`\"auto\"`), `joinMode` (the session's `joinTokenUse`),\n`showJoinMode`/`showLink` (`false`), `copyOnHover` (`true`), `onCopy`,\n`onConnected`/`onConnectedDismiss`, `labels`, `alt`, `placement` (`\"inline\"` +\nfour corners), `position`/`offset`/`zIndex` (`\"absolute\"`/`12`/`20`),\n`className`/`style`/`codeClassName`/`codeStyle`.\n*There is no `card`, `variant`, `showBrand`, `brand`, or `status` prop.* The\ncard is always drawn (`tile`, on by default) and extends to hold `title` /\n`description`; status is **inferred** (connected → error → expired → ready →\nidle) and cannot be passed.\n*Limits:* always encodes the HTTPS `joinUrl`. The animated optical field that\n`variant=\"aurora\"` once selected is now the separate EXPERIMENTAL\n`AuroraPairingField`, decodable only by the RemoteDraw app's own scanner.\n\n**`PairingDevices`** — the list UI for pairing methods that resolve to a device\n(`bluetooth | localNetwork | accountPresence | direct`). `method` (required),\n`devices` (`[]`), `onSelectDevice`, `joinUrl`, `showCodeFallback` (`true`),\n`status`, `size` (`168`), `emptyLabel`, `actions`, `accentColor`,\n`autoHideOnConnected` (`true`), `connectedHideDelayMs` (`1150`), `direction`,\n`logo`, `onCopy`, `className`/`style`.\n*Limits:* **purely presentational — it discovers nothing.** You supply `devices`\nand `onSelectDevice` from your own backend. There is no Bluetooth, no Bonjour,\nand no customer-reachable account-presence route.\n*Never:* present it as \"nearby device pairing\" to a customer. It is chrome.\n\n**`RemoteDrawConnect`** — a compact trigger button that opens the code or the\ndevice list in a popover, for pairing exactly at the field, margin, or toolbar\nthat needs it. Everything from `PairingCodeProps` except placement/position/\noffset/zIndex/className/style/size/tile, plus `size` (`168`), `open`,\n`defaultOpen` (`false`), `onOpenChange`, `placement` (`\"bottom\"`), `method`,\n`devices`, `onSelectDevice`, `trigger`, `triggerLabel` (`\"Pair phone\"`),\n`showTriggerLabel` (`false`), `triggerClassName`/`triggerStyle`/`triggerDisabled`,\n`openOnHover` (`true`), `panel*`/`popover*` class and style.\n*Use it instead of hand-rolling a \"connect phone\" button.*\n\n**`RemoteDrawLaunchButton`** — \"open on my phone\" for a user whose **own** app is\nthe sender. Mints a scoped sender through your backend, opens the deep link, and\nreveals a QR when the app never comes back.\nProps: `connect` (required — your own backend endpoint; may return the raw\n`connectSender` response, a create-session response carrying `senderConnection`,\nor just `{ launchUrl }`; resolving `null` means \"no direct sender for this user\"),\n`handoffTimeoutMs` (`DIRECT_SENDER_HANDOFF_TIMEOUT_MS` = `12_000`),\n`showQrFallback` (`true`), `fallback`, `pairingProps`, `labels`, `accentColor`\n(`#1f7a8c`), `disabled` (`false`), `autoLaunch` (`true`), `openUrl` (default\nassigns `window.location.href`), `onError`, `onStatusChange` (transitions only),\n`children` (node or `(state) => node`), `className`/`style`/`buttonClassName`/\n`buttonStyle`/`aria-label`.\n*Why it is a component and not an `onClick`:* a custom scheme nothing has\nregistered fails **silently** — no error, no navigation, no event. The timeout\nwith no sender on the board is the only detector.\n\n**`useDirectSender(options)`** — the hook the button is a thin default over.\nOptions: `connect` (required), `autoLaunch`, `openUrl`, `onError`,\n`onStatusChange`. Returns `{ status, launchUrl, senderToken, senderId,\nconnection, error, connect(), launch(), reset() }`.\n`status`: `idle | connecting | ready | drawing | submitted | expired | error`.\n`error` is a `RemoteDrawLaunchError` (an `Error`) with\n`kind: \"connect-failed\" | \"no-launch-url\" | \"launch-blocked\"` and the original\nrejection on `cause`.\n*Limits:* phases after `ready` are read from `RemoteDrawProvider`; outside one\nit can never advance past `ready`.\n\n**`RemoteDrawSessionControls`** — the one session-state surface: a collected\nstatus bar (or card, via `title`) telling the session's story — waiting for a\nphone, connected, drawing, submitted, with the sender's device name — plus\nundo/clear. `actions` (`[\"undo\",\"clear\"]`), `confirmClear` (`true`), `title`,\n`submission`, `labels`, `undoLabel`/`clearLabel`/`confirmClearLabel`,\n`metadataKeys`/`metadataLabels`/`formatMetadataValue` (keys render humanized,\nnever raw), `className`/`style`.\n*Limits:* every failure renders as one string, \"Connection problem\".\n\n**`AiImage` / `AiText`** — render a finished `AiAction`. `AiImage`: `action`,\n`direction` (`\"plate\"`, 6 options), `actions` (`\"hover\"`, 4),\n`standardActions` (`[\"download\",\"copy\"]`, plus `\"open\"`), `customActions`,\n`theme` (`\"auto\"`), `aspectRatio`, `radius` (`14`), `fit` (`\"cover\"`),\n`fileName`, `labels`, `onRetry`, `placeholder`, `imageAlt`, `className`/`style`.\n`AiText`: `action`, `direction` (`\"note\"`, 5), `actions` (`\"bar\"`),\n`standardActions`, `customActions`, `theme`, `radius`, `maxWidth` (`\"60ch\"`),\n`labels`, `onRetry`, `placeholder`, `className`/`style`.\n*Limits:* neither ever shows the model, tier, latency, credit cost, or the\nprovider's error string. A schema run renders nothing — `result.generatedData`\nis for your code.\n\n**`useVisualContextPublisher`** (experimental) — the **publish** half of live\nview: sends the receiver's pixels to the phone drawing on it. Has its own\n`onError`.\n\n**`RemoteDrawStreamView`** — the **consume** half, for a sender pad you host\nyourself: the receiver's stream as a ground, your pad as its `children`.\nProps: `senderToken`, `signaling` (a `VisualContextSignalingClient` — build it\nwith `createRealtimeVisualContextSignalingClient` for push, or\n`createHttpVisualContextSignalingClient` for the polled `/v1/.../visual-context/*`\nroutes), `enabled` (gate it on `viewReceiverContext` + `visualContext.enabled` +\na reported phone projection), `iceServers` (from the session's\n`visualContext.iceServers` — without them a phone on cellular connects to\nnothing), `pollIntervalMs`, `onStatus`, `onError`, plus `fit` (`\"contain\"`),\n`fadeMs`, `posterStyle`, `className`/`style`/`aria-label`, `children`.\n`useRemoteDrawStream(options)` is the same thing headless, returning\n`{ mediaStream, status, streamStatus, stream, error, markStreamLive }` with\n`status` one of `idle | connecting | streaming | unsupported | failed | closed`.\n*Limits:* `children` are deliberately **not** gated on the stream — a pad that\nonly appears once pixels arrive never appears on the networks where WebRTC\ncannot connect, and drawing must keep working there.\n\n**`VisualContextVideoLayer`** — the raw `<video>` for a `MediaStream` you\nproduce yourself. `mediaStream`, `status`, `fit` (`\"contain\"`), `fadeMs`\n(`VISUAL_CONTEXT_FADE_MS` = `220`), `posterStyle`, `onLiveChange`,\n`className`/`style`/`aria-label`. Hand over on `onLiveChange(true)`, not on\nhaving a stream. `RemoteDrawStreamView` wires this for you.\n\n**Hooks:** `useRemoteDraw` (throws outside the provider),\n`useRemoteDrawSession`, `useReceiverData`, `usePairingUrl`,\n`useRemoteDrawPointers`, `useReceiverStrokes`.\n\n**Low-level / rarely right:** `InkCanvas` (WebGL2 ink substrate — the receiver\ndrives it), `RemoteDrawMark`, `AuroraPairingField` (EXPERIMENTAL),\n`FreehandFilmGroup`/`freehandStrokePaths`, the element-selection helpers.\n\n**Deliberately not exported:** a web sender component. Hosted `/join` is the web\nsender. Custom in-page pads are built headless on `createHttpSenderClient`.\n`@remotedraw/react/next` is a separate, unfinished v2 entry point — do not mix\nit into a normal integration.\n\n### `@remotedraw/svelte`\n\n`createRemoteDrawReceiver(options)` → `{ subscribe, create, refetch, ingest,\nundo, clear, setSession, configure, start, stop }`.\n`createDirectSender({ connect, receiver, autoLaunch, openUrl, onError })` →\n`{ subscribe, connect, launch, reset, stop }`, the same state machine React's\n`useDirectSender` binds. Plus `export * from \"@remotedraw/client\"`.\n**No components ship.** A Svelte integrator writes the pairing UI, the ink\nrendering and the session UI themselves.\n\n### `@remotedraw/client` (framework-free)\n\n- `createHttpRemoteDrawApiClient(baseUrl, { apiKey })` — **server-only.**\n `createSession`, `getSession`, `listSessions`, `issueJoinToken`,\n `connectSender`, `endSession`, `createAiAction`, `getAiAction`,\n `cancelAiAction`, `waitForAiAction`.\n- `createSessionWithHttpApi(baseUrl, options)` — server-only convenience.\n- `createHttpReceiverClient(baseUrl)` — the seven `/v1/receiver/*` calls.\n Credentials go in the body, not a header.\n- `createHttpSenderClient(baseUrl, { packPoints })` — 14 sender calls.\n- `createReceiverStore(options)` — the headless receiver state machine\n `RemoteDrawProvider` and the Svelte store both bind.\n- `createRealtimeReceiverSource({ driver })` + `createConvexRealtimeDriver({ client })`\n — push transport over the hosted realtime endpoint. `convex` is never imported\n by the package; you hand it a two-method driver.\n- `RemoteDrawHttpError` — `status`, `code`, `upgradeUrl`, `body`, and the\n getters `isAuthenticationFailure`, `isPermissionFailure`, `isSessionOver`,\n `shouldReJoin`.\n- `joinTokenFromInput`, `joinUrlForOrigin`, `nativeJoinUrlFromSession`.\n- `createPacedDraftQueue` / `createLatestOnlyQueue` / `draftPointsForTransport`\n / `retryIdempotentRequest` — the 32 ms latest-only draft gate a custom sender\n must use instead of POSTing every pointer event.\n- `createDirectSenderController`, `resolveDirectSenderStatus`,\n `directSenderConnectionFromResult` — the shared direct-sender rules.\n- `createRealtimeVisualContextSignalingClient` /\n `createHttpVisualContextSignalingClient` — where live-view signals travel\n (realtime push, or the polled public `/v1/.../visual-context/*` routes).\n\n### `@remotedraw/geometry`\n\nStroke/shape helpers (`buildNormalizedStroke`, `recognizeStroke`,\n`simplifyNormalizedPoints`, hit-testing, transforms, `drawingsToSvg`), **and the\nmap board transform**, re-exported by `@remotedraw/react`:\n`boardPointFromLngLat`, `lngLatFromBoardPoint`, `longitudeFromBoardX`,\n`latitudeFromBoardY`, `mercatorYFromLatitude`, `latitudeFromMercatorY`,\n`boardViewportFromMapBounds`, `mapBoundsFromBoardViewport`,\n`mapBoardPointToScreen`, `mapBoardPointFromScreen`, `screenPointFromBoardPoint`,\n`surfacePointFromBoardPoint`, `padMapBounds`, `MAX_MERCATOR_LATITUDE`.\nUse these rather than reimplementing the projection — `x` is linear in\nlongitude, `y` is linear in **Web Mercator**, and a version that is linear in\nlatitude puts ink kilometres away.\n\n### `RemoteDrawSenderKit` (SwiftPM, iOS 17+)\n\n`https://github.com/AxioSOzo/remotedraw-swift.git`, product\n`RemoteDrawSenderKit`. Zero external dependencies. No `Info.plist` entries.\n\n- `RemoteDraw.shared` — **zero-config**: it installs production defaults the\n first time anything reads it. `RemoteDraw.configure(_:)` in `App.init` is\n *optional* and only overrides `apiBaseURL`, `device`, `tokenProvider`,\n `urlSession`, `onClientAdvisory`. `try RemoteDraw.requireConfigured()` throws\n `RemoteDrawError.notConfigured` if you want the strict behaviour back.\n (This used to `preconditionFailure` from inside the modifier's `.task` — a\n crash on the user's tap. It no longer does.)\n- `.remoteDrawSurface(isPresented:senderToken:appearance:strings:exit:onOutcome:)`\n — the whole integration, a full-screen cover with the board, an exit and an\n outcome. A second overload adds `background:` — a `@ViewBuilder` handed a\n `RemoteDrawGroundContext` (`session`, `mapBounds`, `phoneProjection`, `size`,\n `reportViewport`) for your own cartography or ground. A ground that **moves**\n must call `reportViewport(_:)`; a static one calls nothing.\n- `RemoteDrawTakeover` — the same board plus scene-phase wiring and exit, for\n your own cover / navigation push / `UIHostingController`.\n- `RemoteDrawSurface` — the board as a plain `View`, for a host that fills its\n presentation with it edge to edge.\n- `RemoteDrawSenderSession` — the headless core (`begin`/`append`/`end`,\n `undo`, `clear`, `submit(metadata:)`, `edit`, `updateProjection`, `leave`,\n published `phase`, `strokes`, `live`, `lastError`), with\n `RemoteDrawInkCanvas` + `RemoteDrawStrokeCapture` when you own the screen.\n- `RemoteDrawBoardCanvas`, `RemoteDrawMapBoardGround`, `RemoteDrawMapGeometry`,\n `RemoteDrawAppearance`, `RemoteDrawStrings`, `RemoteDrawExit`,\n `RemoteDrawError` (13 cases with `shouldReJoin` / `isRetriable`).\n- **Map boards are built in.** A `kind: \"map\"` session with\n `coordinateSpace.bounds` draws the geography through MapKit, using the same\n transform as `@remotedraw/geometry`. The built-in map is deliberately not\n pannable; supply your own through `background:` if it should be.\n- **Streaming boards are not.** A session with `senderIntegrationMode:\n \"streaming\"` or a receiver publishing `visualContext.enabled` cannot be drawn\n by this SDK — no WebRTC, no video, no `WKWebView`. It reports\n `.unsupportedSurface(_:)` carrying `hostedSenderURL`; present that in a\n `WKWebView` and streaming works today.\n\n`RemoteDrawOutcome` is a **closed** enum:\n\n| Case | Meaning | Do |\n| --- | --- | --- |\n| `.submitted(RemoteDrawReceipt)` | Drawing submitted. | Record it. Call `session.submit(metadata:)` yourself for the server's own ids — a submit from the SDK's controls reports a placeholder. |\n| `.left` | The person left; ink is on the board. | Nothing. |\n| `.expired` | The board finished or timed out. **Terminal.** | Create a new session. |\n| `.credentialLost(RemoteDrawError?)` | The token died; the board did not. **Recoverable.** | Mint a fresh `rd_send_` and present again. |\n| `.unsupportedSurface(RemoteDrawUnsupportedSurface)` | The board wants a renderer this SDK lacks. The cover **stays up** showing the reason. | Open `hostedSenderURL` in a `WKWebView`. |\n| `.failed(RemoteDrawError)` | Anything else. | Read `error.shouldReJoin` / `error.isRetriable`. |\n\n**There is no `.revoked`.** A revoked, unknown and malformed token all answer\n`invalid_sender_token` on purpose; only a genuine expiry is distinguishable, and\nthat travels in the error on `.credentialLost`.\n\nNote: `RemoteDrawKit` is a *different*, internal macOS-only package that some\nolder docs still name. Customers use `RemoteDrawSenderKit`.\n\n## Recipes, one per surface\n\nAll React snippets assume the session came from your backend and are wrapped in:\n\n```tsx\nconst receiver = createHttpReceiverClient(\"https://api.remotedraw.com\");\n\n<RemoteDrawProvider\n session={session.session}\n receiverToken={session.receiverToken}\n joinUrl={session.joinUrl}\n joinUrls={session.joinUrls}\n receiver={receiver}\n onError={(error) => reportToYourLogger(error)}\n>\n {/* the recipe */}\n</RemoteDrawProvider>\n```\n\n### Whiteboard / freeform sketch\n\n```ts\n// Backend\ntarget: { kind: \"whiteboard\", label: \"Session notes\" }\n```\n\n```tsx\n<PairingCode title=\"Scan to draw\" />\n<RemoteDrawReceiver />\n<RemoteDrawSessionControls title=\"Sender workflow\" />\n```\n\n### Photo / screenshot annotation (`image`)\n\nThe photo is **already on the receiver**. The phone never takes it.\n\n```ts\ntarget: {\n kind: \"image\",\n inputMapping: \"surface\", // the pad is the whole photo\n coordinateSpace: { width: 1600, height: 900 },\n metadata: { label: \"Inspection photo\", imageId: \"img_123\" },\n}\n```\n\n```tsx\n<div style={{ position: \"relative\" }}>\n <RemoteDrawReceiver\n coordinateAspectRatio={16 / 9}\n background={<img src={photoUrl} alt=\"\" style={{ width: \"100%\", height: \"100%\", objectFit: \"contain\" }} />}\n />\n <PairingCode placement=\"top-right\" size={112} />\n</div>\n```\n\n*Limit:* RemoteDraw stores no images. Your app owns the photo and the link\nbetween it and the drawings.\n\n### PDF page\n\n```ts\ntarget: { kind: \"pdf\", inputMapping: \"viewport\",\n metadata: { documentId: \"doc_9\", page: 3 } }\n```\n\n```tsx\n<div style={{ position: \"relative\" }}>\n <YourPdfPage page={3} />\n <RemoteDrawReceiver\n background=\"transparent\"\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n />\n</div>\n```\n\n*Limit:* RemoteDraw renders no PDFs and knows nothing about pages. One session\nper page, or carry the page number in `target.metadata` and re-create.\n\n### Signature / bounded field\n\n```ts\ntarget: {\n kind: \"field\",\n inputMapping: \"surface\", // mandatory — the pad IS the field\n coordinateSpace: { width: 1600, height: 500 },\n metadata: { label: \"Customer signature\", fieldId: \"sig_1\" },\n}\n```\n\n```tsx\n<RemoteDrawConnect method=\"qr\" triggerLabel=\"Sign with your phone\" showTriggerLabel />\n<RemoteDrawReceiver coordinateAspectRatio={16 / 5} strokeWidth={7}\n style={{ border: \"1px solid #ddd8cf\", borderRadius: 12 }}>\n <line x1=\"140\" y1=\"760\" x2=\"3060\" y2=\"760\" stroke=\"#d8d8d8\" strokeWidth=\"4\" />\n</RemoteDrawReceiver>\n```\n\n*Limit:* this is markup transport, **not** e-signature compliance. No identity\nproofing, no intent-to-sign ceremony, no tamper-evident audit package, no\ncertificates. Say so if the user asks for a legal signature.\n\n### Map\n\nThe board's `coordinateSpace.bounds` is a **hard geographic fence, fixed for the\nlife of the session** — no route changes it. Size it larger than the camera you\nopen on. The phone pans *inside* it.\n\n```ts\nimport { padMapBounds } from \"@remotedraw/geometry\";\n\ntarget: {\n kind: \"map\",\n inputMapping: \"viewport\",\n coordinateSpace: {\n width: 1600, height: 1310, // the fence's Mercator aspect\n ...padMapBounds(currentCameraBounds, 1), // 3x the camera\n },\n}\n```\n\n```tsx\nconst [camera, setCamera] = useState(0);\nuseEffect(() => {\n const onMove = () => setCamera((n) => n + 1);\n map.on(\"move\", onMove);\n return () => map.off(\"move\", onMove);\n}, [map]);\nconst projectLngLat = useCallback(\n (lng: number, lat: number) => map.project([lng, lat]), // CSS px in the container\n [map, camera],\n);\n\n<div style={{ position: \"relative\" }}>\n <div ref={mapContainer} style={{ position: \"absolute\", inset: 0 }} />\n <RemoteDrawMapReceiver\n projectLngLat={projectLngLat}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n />\n</div>\n```\n\n*Limits:* `mapBounds={map.getBounds()}` is the no-callback alternative but is\nexact only for a north-up, unpitched camera. Omitting `bounds` at session\ncreation does not mean \"the customer's map\" — it means RemoteDraw's own default\nregion. `senderIntegrationMode` stays `\"native\"` on a map board: streaming\nrequires a `phoneProjection` that a native map sender never sends.\n\n### Screen / live view\n\n```ts\ntarget: { kind: \"screen\", inputMapping: \"viewport\" }\ncapabilities: [..., \"viewReceiverContext\"]\nvisualContext: { enabled: true }\n```\n\nThe receiver publishes with `useVisualContextPublisher`. Three consumers, in\norder of how little you write:\n\n1. **The hosted `/join` pad** — consumes the stream automatically. Zero code.\n2. **Your own web pad** — `RemoteDrawStreamView` with your pad as its\n `children`, plus a signaling client.\n3. **`RemoteDrawSenderKit`** — *cannot* consume it. It returns\n `.unsupportedSurface(_:)` carrying a `hostedSenderURL`; present that in a\n `WKWebView` and streaming works today.\n\nLive view is experimental: build so that a session whose stream never starts is\nstill a working session — the phone keeps drawing, it simply does not see the\nreceiver's pixels.\n\n### iOS sender — the complete Swift\n\n```swift\n// Package.swift / Xcode → Add Package\n.package(url: \"https://github.com/AxioSOzo/remotedraw-swift.git\", from: \"0.1.0\")\n.product(name: \"RemoteDrawSenderKit\", package: \"remotedraw-swift\")\n\nimport RemoteDrawSenderKit\n\n// Optional. RemoteDraw.shared installs production defaults on first use, so\n// this line exists only to OVERRIDE something.\n@main struct MyApp: App {\n init() { RemoteDraw.configure(.init(tokenProvider: mintSenderToken)) }\n var body: some Scene { WindowGroup { RootView() } }\n}\n\n// Wherever drawing starts. `token` is the rd_send_ string your backend minted\n// with POST /v1/sessions/direct-sender. A QR's rd_join_ works too, but spending\n// one revokes every other sender on that session.\nButton(\"Draw\") { drawing = true }\n .remoteDrawSurface(isPresented: $drawing, senderToken: token) { outcome in\n switch outcome {\n case .submitted(let receipt): record(receipt)\n case .left: dismissBanner()\n case .expired: refreshSession() // terminal\n case .credentialLost: refreshToken() // recoverable\n case .unsupportedSurface(let it): openHostedPad(it.hostedSenderURL)\n case .failed(let error): report(error) // do not swallow\n }\n }\n```\n\nReceiving the launch URL (the other half of the direct sender — nothing else\ndocuments it):\n\n```swift\n// Info.plist: CFBundleURLTypes → your scheme, e.g. \"yourapp\"\n.onOpenURL { url in\n guard url.scheme == \"yourapp\",\n let token = URLComponents(url: url, resolvingAgainstBaseURL: false)?\n .queryItems?.first(where: { $0.name == \"senderToken\" })?.value\n else { return }\n senderToken = token\n drawing = true\n}\n```\n\nRules:\n\n- The surface is **full screen**. Not a small pad, not a draggable sheet.\n- The user can always leave; `RemoteDrawExit` only decides whether leaving with\n unsubmitted ink asks first. The host always gets an outcome.\n- Do not write a `UIViewRepresentable` canvas, a draft loop, or an HTTP client.\n Go headless on `RemoteDrawSenderSession` + `RemoteDrawInkCanvas` +\n `RemoteDrawStrokeCapture` if you own the screen — never raw `URLSession`.\n- Customers do not ship a separate RemoteDraw app; their app *is* the sender.\n\n## How to suggest use cases\n\n**What the product is good at**, as a sentence to pattern-match against:\n\n> A person is at a screen. The thing they need to mark is already on that\n> screen. A mouse is the wrong instrument for the mark — because it is\n> handwriting, a circle around a defect, a diagram, a signature, or a gesture\n> over a map — and their phone is in their pocket.\n\n**Ask these four before proposing anything:**\n\n1. Which screen in your product already shows the thing to be marked, and what\n device is that screen on?\n2. Is the person in front of it holding a phone at the same time?\n3. What does the mark mean afterwards — saved to which record, shown where?\n4. Anyone who scans, or a signed-in user of your own app? (QR vs direct sender.)\n\n**Good vs bad, for a property-management SaaS:**\n\n- ✅ Property manager reviews an inspection photo on the office desktop and\n circles the damage with their phone. *Two devices; content already on the\n receiver.*\n- ✅ Tenant signs the handover report on the manager's laptop screen using their\n own phone as the pen. *This is the flow that replaces a stylus.*\n- ✅ Planner marks a route on the dispatch map on the wall display.\n- ✅ Support agent circles the broken control on a customer's shared screen.\n- ❌ Tenant photographs a leak on their phone and circles it. *One device,\n phone-supplied content. **Not RemoteDraw.*** Say so and propose PencilKit.\n- ❌ An in-app sketch pad in the mobile app. *One device.*\n- ❌ Field engineer marks up a PDF on their iPad in the van. *One device —\n unless a second screen is genuinely present.*\n- ❌ \"Pair over Bluetooth when the phone is nearby.\" *Not implemented.*\n\n**The disqualifier:** if you cannot name two devices and say which one already\ndisplays the content, you do not have a use case yet — ask.\n\n## The flow — in this order\n\n1. **Explain before touching anything.** If the user is asking what RemoteDraw\n can do, answer from this file and the docs. Do not install, scaffold, or\n create sessions to answer a question.\n2. **Ask for approval before installing.** Name exactly what you want to add\n (`@remotedraw/cli`, `@remotedraw/react`, a Swift package, a dashboard\n project + key) and why, then wait. This includes `remotedraw init` without\n `--offline`, which provisions a billable project and key. Install with the\n project's own package manager — the scan below reports it, and a lockfile\n the project did not ask for is a mess a human has to clean up:\n `npm install -g @remotedraw/cli`, `pnpm add -g @remotedraw/cli`,\n `bun add -g @remotedraw/cli`, or — Yarn Berry has no global install —\n `yarn dlx @remotedraw/cli`. Same for the SDKs: `npm install`, `pnpm add`,\n `yarn add`, or `bun add`.\n3. **Scan the codebase.** `remotedraw scan --format json` (or, before the CLI\n is installed, `npx @remotedraw/cli@latest scan --format json` — `pnpm dlx`,\n `yarn dlx`, or `bunx @remotedraw/cli` for those managers) reports the\n web/server/iOS projects, the project's package manager and its install/add\n commands, where an `rd_sk_` key may live, any RemoteDraw wiring already\n present, the integration options that fit, and the product questions to ask.\n Read it; verify its `evidence` where it matters.\n4. **Ask the product questions** (the four above, plus the scan's own). Do not\n invent answers; a one-page \"drawing lab\" is only right when the user says a\n demo is what they want.\n5. **Propose one plan, then build all of it.** Receiver, session creation,\n sender, and the exit/submit path — an integration is not done when ink\n appears once on a test page. Build beside existing features.\n6. **Verify by using it.** Run `remotedraw doctor --format json`, open a real\n session, draw from a phone (or `create-input --execute` + the hosted join\n URL), and confirm ink lands on the receiver. On iOS, run it on a device or\n simulator and look at the screen; a compiling canvas is not a working one.\n\nSession creation (`POST /v1/sessions`) needs the `rd_sk_` key and therefore runs\nonly where the scan found server-side code: a Convex action, a Next route\nhandler, an Express/Hono route, a serverless function. If the scan found none,\nask where the backend is. Never scaffold `createRemoteDrawSession.ts` into a\nVite/Next client tree — `--target web` in `remotedraw init` still writes it\nunder `src/`; move it, or scaffold into a scratch directory and copy only what\nbelongs.\n\n## CLI\n\n```sh\nremotedraw options --format json # the option catalog\nremotedraw scan --format json # read the codebase first (step 3)\nremotedraw init --non-interactive --offline --dry-run --format json \\\n --path apps/web --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset sketch --json\nremotedraw create-input --execute # a real session + join URL, needs a key\nremotedraw agent --print-skill # this file, no install needed\n```\n\n`--sender own-ios` requires `--sdk swift`; other invalid combinations fail with\n`INVALID_COMBINATION`. By default `init`/`new` also create a dashboard project\nand a project-scoped development key in `.env.local` — that is the step that\nneeds approval (step 2); `--offline` writes files only. `--preset mapMarkup`\nemits an explicit `coordinateSpace.bounds`; replace the example region with the\ncustomer's. Never pass `--force` unless the user approved overwriting. Do not\ndrive the interactive wizard or scrape human-formatted output; every command has\n`--format json`.\n\n## Security and tenancy\n\n- `rd_sk_…` keys: backend secrets only. Never in browser bundles, Swift, app\n bundles, screenshots, logs, or generated examples.\n- The account-level `rd_cli_…` credential stays in the user config directory;\n never copy it into a project. `REMOTEDRAW_CLI_TOKEN` is for CI secrets only.\n- Public clients receive only `joinUrl`, `joinToken`, `receiverToken`, or\n `senderToken`, each scoped to one session. Production QR codes use the HTTPS\n `joinUrl`, not the custom scheme.\n- One key serves every customer of the product, so a session id is not a\n capability. Create sessions with `externalId: \"<product>:<tenant>\"`, and check\n it (`POST /v1/sessions/get`) before attaching a sender or ending a session on\n a tenant's behalf.\n\n## Gotchas the SDKs hide and hand-written code hits\n\n- `POST /v1/sessions` is billable and not idempotent. React StrictMode runs\n mount effects twice in development: guard with a ref, or create the session in\n a server action / loader. Store the `receiverToken` if the receiver outlives a\n page load — it is the only credential that reads a session's ink.\n- Timestamps are integer milliseconds. `occurredAt` and point `t` values are\n accepted with a fraction (floored) but a hand-written client should send\n integers.\n- Committed points come back in board space, remapped through\n `device.aspectRatio`; send the aspect ratio of the pad the finger touches.\n- `RemoteDrawReceiver` defaults `phones` and `pointers` to on, and renders\n immediately with no empty state. Pass `phones={false}` for a plain surface;\n drive your own empty state from `useReceiverData().senders`.\n- `PairingCode` hides the join URL text unless `showLink`.\n- `joinTokenExpiresAt` is earlier than the session's `expiresAt`: the QR dies\n first, the board stays live.\n- Wire `RemoteDrawProvider`'s `onError` (and `useVisualContextPublisher`'s).\n Without it a dead credential is silent and the board simply stops updating.\n- The hosted `/join` pad respects the joined capability list; a custom sender\n must too. It also has a **file-attach tool** available on any session granting\n `draw` or `point`, which cannot currently be turned off.\n\n## API contract\n\n- Backend: `POST /v1/sessions` (create), `/v1/sessions/get`, `/v1/sessions/end`,\n `/v1/sessions/direct-sender` (mint `rd_send_` for your own app),\n `/v1/sessions/join-token` (a fresh QR).\n- Receiver: `POST /v1/receiver/session`, `/drawings`, `/drafts`, `/senders`\n with the receiver token, or the realtime source in the SDKs.\n- Sender: `POST /v1/join` (spends a join token; revokes other senders), then\n `/v1/sender/draft` (latest-only preview, throttle to ~32 ms),\n `/v1/sender/commit` (one durable stroke per pointer-up with a stable\n `clientStrokeId`), `/v1/sender/submit`.\n- Capabilities: `draw`, `point`, `undo`, `clear`, `moveViewport`,\n `viewExisting`, `viewReceiverContext`. Omitting `capabilities` grants the\n first six. Reissued join tokens may narrow but never widen.\n\n## AI actions\n\nReach for AI when the product needs something _from_ the finished drawing: a\ngenerated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit, submit,\nor presence event — AI runs only on an explicit `POST /v1/ai-actions` call the\nuser asked for. Run it after the user is done; the route accepts `active` and\n`ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use `createAiAction` + `waitForAiAction` on\n`createHttpRemoteDrawApiClient` from `@remotedraw/client` (re-exported by\n`@remotedraw/react`) — backend only, it holds the key. There is no completion\nwebhook. Render results with `AiImage` / `AiText`. See\nhttps://docs.remotedraw.com/docs/api#ai.\n\n## What does not exist yet\n\nVerified against source, 2026-08-30. Read this *before* designing, so you never\npromise any of it.\n\n- **No Bluetooth or Wi-Fi pairing.** The radios advertise; nothing scans or\n browses, on any platform. `PairingDevices` is presentational chrome.\n- **No QR into a customer's own app.** The Universal-Link association file has\n one hard-coded appID and the QR host is a RemoteDraw deployment setting. Use\n the direct sender.\n- **No native Android SDK.** Hosted `/join` in a mobile browser is the Android\n sender, and it is full-featured.\n- **No desktop or trackpad sender.** Unbuilt research.\n- **No streaming consumer in `RemoteDrawSenderKit`.** It reports\n `.unsupportedSurface(_:)` with a `hostedSenderURL` to open in a `WKWebView`.\n (A customer's own *web* pad can consume a stream — `RemoteDrawStreamView`.\n It is the native SDK that cannot.)\n- **No Svelte components.** A receiver store and a direct-sender store only.\n- **No web sender component.** Deliberate: hosted `/join` is the web sender.\n- **No API-key route that reads a session's ink.** Lose the `receiverToken` and\n the session is unreadable while still billable.\n- **No `clientSessionId` idempotency on `POST /v1/sessions`.**\n- **No completion webhook for AI actions** — poll `POST /v1/ai-actions/get`.\n- **No e-signature compliance.** No identity proofing, intent-to-sign ceremony,\n tamper-evident audit package, or certificate handling.\n- **No content rendering of any kind.** No PDF renderer, no image storage, no\n document pipeline, no auth, no billing UI. The host renders; RemoteDraw inks.\n\n## Verification\n\nAfter changes, verify against the customer's project — never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` —\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.\n\n## Reference\n\n- API: `https://api.remotedraw.com` · Docs: `https://docs.remotedraw.com/docs`\n (agent summary: `https://docs.remotedraw.com/llms.txt`) · Keys:\n `https://dashboard.remotedraw.com/api/keys`\n- Packages: `@remotedraw/cli`, `@remotedraw/react`, `@remotedraw/svelte`,\n `@remotedraw/client`, `@remotedraw/protocol`, `@remotedraw/geometry`, and the\n SwiftPM package `https://github.com/AxioSOzo/remotedraw-swift.git` (product\n `RemoteDrawSenderKit`).";
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ if (process.stderr.isTTY && process.argv[2] !== "update") {
|
|
|
31
31
|
try {
|
|
32
32
|
const check = await checkForUpdate(runtime, CLI_VERSION);
|
|
33
33
|
if (check?.updateAvailable) {
|
|
34
|
-
const method = detectInstallMethod(runtime.binPath ?? "");
|
|
34
|
+
const method = detectInstallMethod(runtime.binPath ?? "", runtime.env);
|
|
35
35
|
process.stderr.write(`\n${updateNotice(check, method)}\n`);
|
|
36
36
|
}
|
|
37
37
|
}
|
package/dist/locales/en.d.ts
CHANGED
|
@@ -166,22 +166,22 @@ export declare const en: {
|
|
|
166
166
|
readonly "choice.preset.sketch.summary": "Normalized input for your own receiver surface.";
|
|
167
167
|
readonly "choice.preset.sketch.explanation": "Choose this when your product owns the photo, map, PDF, canvas, or other receiver UI. RemoteDraw supplies the input primitives without prescribing a component design.";
|
|
168
168
|
readonly "choice.preset.sketch.docs": "Open sketch docs";
|
|
169
|
-
readonly "choice.preset.photoMarkup.summary": "
|
|
170
|
-
readonly "choice.preset.photoMarkup.explanation": "
|
|
169
|
+
readonly "choice.preset.photoMarkup.summary": "Draw on a photo your app is already showing on a screen.";
|
|
170
|
+
readonly "choice.preset.photoMarkup.explanation": "Your receiver renders the photo — on the desktop, laptop, or display someone is looking at — and a phone marks it up from across the room. The phone never takes or uploads the picture; your app supplies it and keeps the link with the drawings. Use this for reviewing an inspection photo at a desk, giving feedback on imagery, or annotating a screenshot.";
|
|
171
171
|
readonly "choice.preset.photoMarkup.docs": "Open photo markup docs";
|
|
172
|
-
readonly "choice.preset.pdfMarkup.summary": "
|
|
173
|
-
readonly "choice.preset.pdfMarkup.explanation": "Use this for document review
|
|
172
|
+
readonly "choice.preset.pdfMarkup.summary": "Draw on a PDF page your app is already showing on a screen.";
|
|
173
|
+
readonly "choice.preset.pdfMarkup.explanation": "Use this for document review at a desk: your receiver renders the right page and a phone marks it. RemoteDraw renders no PDFs — your app owns the page and maps RemoteDraw coordinates onto that fixed view.";
|
|
174
174
|
readonly "choice.preset.pdfMarkup.docs": "Open PDF markup docs";
|
|
175
|
-
readonly "choice.preset.mapMarkup.summary": "
|
|
176
|
-
readonly "choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback. Your app owns the map and
|
|
175
|
+
readonly "choice.preset.mapMarkup.summary": "Draw routes and directions on the map your app is showing.";
|
|
176
|
+
readonly "choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback marked from a phone onto a map on a desktop or wall display. Your app owns the map and the camera; RemoteDraw carries the input in geographic board space. The board's coordinateSpace.bounds is a fixed fence — size it larger than the camera you open on.";
|
|
177
177
|
readonly "choice.preset.mapMarkup.docs": "Open map markup docs";
|
|
178
178
|
readonly "choice.preset.screenMarkup.label": "Screen annotation";
|
|
179
179
|
readonly "choice.preset.screenMarkup.hint": "Mark up a screen or window your receiver shares.";
|
|
180
180
|
readonly "choice.preset.screenMarkup.summary": "Annotations on a screen or application view.";
|
|
181
|
-
readonly "choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback. Your receiver supplies the screen image the phone marks are projected onto.";
|
|
181
|
+
readonly "choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback: the screen being marked is the receiver's, and the phone is the pen. Your receiver supplies the screen image the phone marks are projected onto.";
|
|
182
182
|
readonly "choice.preset.screenMarkup.docs": "Open screen markup docs";
|
|
183
183
|
readonly "choice.preset.designReview.summary": "Focused visual feedback on a design.";
|
|
184
|
-
readonly "choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines. Your product owns comments, versions, and decisions.";
|
|
184
|
+
readonly "choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines drawn from a phone onto the design open on a reviewer's screen. Your product owns comments, versions, and decisions.";
|
|
185
185
|
readonly "choice.preset.designReview.docs": "Open design review docs";
|
|
186
186
|
readonly "choice.preset.pointer.summary": "Live pointing without persistent ink.";
|
|
187
187
|
readonly "choice.preset.pointer.explanation": "Use this for presentations and guidance where the phone acts as a pointer and movement matters more than stored ink.";
|
package/dist/locales/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4lBL,CAAC"}
|
package/dist/locales/en.js
CHANGED
|
@@ -179,22 +179,22 @@ export const en = {
|
|
|
179
179
|
"choice.preset.sketch.summary": "Normalized input for your own receiver surface.",
|
|
180
180
|
"choice.preset.sketch.explanation": "Choose this when your product owns the photo, map, PDF, canvas, or other receiver UI. RemoteDraw supplies the input primitives without prescribing a component design.",
|
|
181
181
|
"choice.preset.sketch.docs": "Open sketch docs",
|
|
182
|
-
"choice.preset.photoMarkup.summary": "
|
|
183
|
-
"choice.preset.photoMarkup.explanation": "
|
|
182
|
+
"choice.preset.photoMarkup.summary": "Draw on a photo your app is already showing on a screen.",
|
|
183
|
+
"choice.preset.photoMarkup.explanation": "Your receiver renders the photo — on the desktop, laptop, or display someone is looking at — and a phone marks it up from across the room. The phone never takes or uploads the picture; your app supplies it and keeps the link with the drawings. Use this for reviewing an inspection photo at a desk, giving feedback on imagery, or annotating a screenshot.",
|
|
184
184
|
"choice.preset.photoMarkup.docs": "Open photo markup docs",
|
|
185
|
-
"choice.preset.pdfMarkup.summary": "
|
|
186
|
-
"choice.preset.pdfMarkup.explanation": "Use this for document review
|
|
185
|
+
"choice.preset.pdfMarkup.summary": "Draw on a PDF page your app is already showing on a screen.",
|
|
186
|
+
"choice.preset.pdfMarkup.explanation": "Use this for document review at a desk: your receiver renders the right page and a phone marks it. RemoteDraw renders no PDFs — your app owns the page and maps RemoteDraw coordinates onto that fixed view.",
|
|
187
187
|
"choice.preset.pdfMarkup.docs": "Open PDF markup docs",
|
|
188
|
-
"choice.preset.mapMarkup.summary": "
|
|
189
|
-
"choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback. Your app owns the map and
|
|
188
|
+
"choice.preset.mapMarkup.summary": "Draw routes and directions on the map your app is showing.",
|
|
189
|
+
"choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback marked from a phone onto a map on a desktop or wall display. Your app owns the map and the camera; RemoteDraw carries the input in geographic board space. The board's coordinateSpace.bounds is a fixed fence — size it larger than the camera you open on.",
|
|
190
190
|
"choice.preset.mapMarkup.docs": "Open map markup docs",
|
|
191
191
|
"choice.preset.screenMarkup.label": "Screen annotation",
|
|
192
192
|
"choice.preset.screenMarkup.hint": "Mark up a screen or window your receiver shares.",
|
|
193
193
|
"choice.preset.screenMarkup.summary": "Annotations on a screen or application view.",
|
|
194
|
-
"choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback. Your receiver supplies the screen image the phone marks are projected onto.",
|
|
194
|
+
"choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback: the screen being marked is the receiver's, and the phone is the pen. Your receiver supplies the screen image the phone marks are projected onto.",
|
|
195
195
|
"choice.preset.screenMarkup.docs": "Open screen markup docs",
|
|
196
196
|
"choice.preset.designReview.summary": "Focused visual feedback on a design.",
|
|
197
|
-
"choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines. Your product owns comments, versions, and decisions.",
|
|
197
|
+
"choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines drawn from a phone onto the design open on a reviewer's screen. Your product owns comments, versions, and decisions.",
|
|
198
198
|
"choice.preset.designReview.docs": "Open design review docs",
|
|
199
199
|
"choice.preset.pointer.summary": "Live pointing without persistent ink.",
|
|
200
200
|
"choice.preset.pointer.explanation": "Use this for presentations and guidance where the phone acts as a pointer and movement matters more than stored ink.",
|
package/dist/locales/index.d.ts
CHANGED
|
@@ -169,22 +169,22 @@ export declare const LOCALES: {
|
|
|
169
169
|
readonly "choice.preset.sketch.summary": "Normalized input for your own receiver surface.";
|
|
170
170
|
readonly "choice.preset.sketch.explanation": "Choose this when your product owns the photo, map, PDF, canvas, or other receiver UI. RemoteDraw supplies the input primitives without prescribing a component design.";
|
|
171
171
|
readonly "choice.preset.sketch.docs": "Open sketch docs";
|
|
172
|
-
readonly "choice.preset.photoMarkup.summary": "
|
|
173
|
-
readonly "choice.preset.photoMarkup.explanation": "
|
|
172
|
+
readonly "choice.preset.photoMarkup.summary": "Draw on a photo your app is already showing on a screen.";
|
|
173
|
+
readonly "choice.preset.photoMarkup.explanation": "Your receiver renders the photo — on the desktop, laptop, or display someone is looking at — and a phone marks it up from across the room. The phone never takes or uploads the picture; your app supplies it and keeps the link with the drawings. Use this for reviewing an inspection photo at a desk, giving feedback on imagery, or annotating a screenshot.";
|
|
174
174
|
readonly "choice.preset.photoMarkup.docs": "Open photo markup docs";
|
|
175
|
-
readonly "choice.preset.pdfMarkup.summary": "
|
|
176
|
-
readonly "choice.preset.pdfMarkup.explanation": "Use this for document review
|
|
175
|
+
readonly "choice.preset.pdfMarkup.summary": "Draw on a PDF page your app is already showing on a screen.";
|
|
176
|
+
readonly "choice.preset.pdfMarkup.explanation": "Use this for document review at a desk: your receiver renders the right page and a phone marks it. RemoteDraw renders no PDFs — your app owns the page and maps RemoteDraw coordinates onto that fixed view.";
|
|
177
177
|
readonly "choice.preset.pdfMarkup.docs": "Open PDF markup docs";
|
|
178
|
-
readonly "choice.preset.mapMarkup.summary": "
|
|
179
|
-
readonly "choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback. Your app owns the map and
|
|
178
|
+
readonly "choice.preset.mapMarkup.summary": "Draw routes and directions on the map your app is showing.";
|
|
179
|
+
readonly "choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback marked from a phone onto a map on a desktop or wall display. Your app owns the map and the camera; RemoteDraw carries the input in geographic board space. The board's coordinateSpace.bounds is a fixed fence — size it larger than the camera you open on.";
|
|
180
180
|
readonly "choice.preset.mapMarkup.docs": "Open map markup docs";
|
|
181
181
|
readonly "choice.preset.screenMarkup.label": "Screen annotation";
|
|
182
182
|
readonly "choice.preset.screenMarkup.hint": "Mark up a screen or window your receiver shares.";
|
|
183
183
|
readonly "choice.preset.screenMarkup.summary": "Annotations on a screen or application view.";
|
|
184
|
-
readonly "choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback. Your receiver supplies the screen image the phone marks are projected onto.";
|
|
184
|
+
readonly "choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback: the screen being marked is the receiver's, and the phone is the pen. Your receiver supplies the screen image the phone marks are projected onto.";
|
|
185
185
|
readonly "choice.preset.screenMarkup.docs": "Open screen markup docs";
|
|
186
186
|
readonly "choice.preset.designReview.summary": "Focused visual feedback on a design.";
|
|
187
|
-
readonly "choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines. Your product owns comments, versions, and decisions.";
|
|
187
|
+
readonly "choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines drawn from a phone onto the design open on a reviewer's screen. Your product owns comments, versions, and decisions.";
|
|
188
188
|
readonly "choice.preset.designReview.docs": "Open design review docs";
|
|
189
189
|
readonly "choice.preset.pointer.summary": "Live pointing without persistent ink.";
|
|
190
190
|
readonly "choice.preset.pointer.explanation": "Use this for presentations and guidance where the phone acts as a pointer and movement matters more than stored ink.";
|
package/dist/locales/nl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nl.d.ts","sourceRoot":"","sources":["../../src/locales/nl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"nl.d.ts","sourceRoot":"","sources":["../../src/locales/nl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,aA8mBhB,CAAC"}
|
package/dist/locales/nl.js
CHANGED
|
@@ -171,19 +171,19 @@ export const nl = {
|
|
|
171
171
|
"choice.preset.sketch.summary": "Genormaliseerde invoer voor een eigen ontvangeroppervlak.",
|
|
172
172
|
"choice.preset.sketch.explanation": "Kies dit wanneer jouw product de foto, kaart, PDF, canvas of andere receiver-UI beheert. RemoteDraw levert de invoerprimitieven zonder een voorgeschreven componentontwerp.",
|
|
173
173
|
"choice.preset.sketch.docs": "Schetsdocs openen",
|
|
174
|
-
"choice.preset.photoMarkup.summary": "
|
|
175
|
-
"choice.preset.photoMarkup.explanation": "
|
|
174
|
+
"choice.preset.photoMarkup.summary": "Teken op een foto die je app al op een scherm toont.",
|
|
175
|
+
"choice.preset.photoMarkup.explanation": "Je ontvanger toont de foto — op de desktop, laptop of display waar iemand naar kijkt — en een telefoon zet er markeringen op. De telefoon maakt of uploadt de foto nooit; je app levert die en bewaart de relatie met de tekeningen. Gebruik dit voor het beoordelen van een inspectiefoto aan een bureau, feedback op beeldmateriaal of het annoteren van een schermafbeelding.",
|
|
176
176
|
"choice.preset.photoMarkup.docs": "Fotoannotatiedocs openen",
|
|
177
|
-
"choice.preset.pdfMarkup.summary": "
|
|
178
|
-
"choice.preset.pdfMarkup.explanation": "Gebruik dit voor documentreview
|
|
177
|
+
"choice.preset.pdfMarkup.summary": "Teken op een PDF-pagina die je app al op een scherm toont.",
|
|
178
|
+
"choice.preset.pdfMarkup.explanation": "Gebruik dit voor documentreview aan een bureau: je ontvanger rendert de juiste pagina en een telefoon markeert die. RemoteDraw rendert geen PDF's — je app beheert de pagina en koppelt RemoteDraw-coördinaten aan die vaste weergave.",
|
|
179
179
|
"choice.preset.pdfMarkup.docs": "PDF-annotatiedocs openen",
|
|
180
|
-
"choice.preset.mapMarkup.summary": "
|
|
181
|
-
"choice.preset.mapMarkup.explanation": "Gebruik dit voor routes, locaties of ruimtelijke feedback. Je app beheert de kaart en
|
|
180
|
+
"choice.preset.mapMarkup.summary": "Teken routes en aanwijzingen op de kaart die je app toont.",
|
|
181
|
+
"choice.preset.mapMarkup.explanation": "Gebruik dit voor routes, locaties of ruimtelijke feedback die vanaf een telefoon op een kaart op een desktop of wanddisplay worden gezet. Je app beheert de kaart en de camera; RemoteDraw draagt de invoer in geografische board-ruimte. De coordinateSpace.bounds van het board is een vast hek — maak het groter dan de camera waarmee je opent.",
|
|
182
182
|
"choice.preset.mapMarkup.docs": "Kaartannotatiedocs openen",
|
|
183
183
|
"choice.preset.screenMarkup.label": "Schermannotatie",
|
|
184
184
|
"choice.preset.screenMarkup.hint": "Teken over een scherm of venster dat je ontvanger deelt.",
|
|
185
185
|
"choice.preset.screenMarkup.summary": "Aantekeningen op een scherm of applicatieweergave.",
|
|
186
|
-
"choice.preset.screenMarkup.explanation": "Gebruik dit voor support, demo's en UI-feedback. Je ontvanger levert het schermbeeld waarop de telefoonmarkeringen worden geprojecteerd.",
|
|
186
|
+
"choice.preset.screenMarkup.explanation": "Gebruik dit voor support, demo's en UI-feedback: het scherm dat gemarkeerd wordt is dat van de ontvanger en de telefoon is de pen. Je ontvanger levert het schermbeeld waarop de telefoonmarkeringen worden geprojecteerd.",
|
|
187
187
|
"choice.preset.screenMarkup.docs": "Schermaantekeningdocs openen",
|
|
188
188
|
"choice.preset.designReview.summary": "Gerichte visuele feedback op een ontwerp.",
|
|
189
189
|
"choice.preset.designReview.explanation": "Gebruik dit voor ontwerpbeoordelingen met pijlen, vormen en vrije lijnen. Je product beheert opmerkingen, versies en besluitvorming.",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One table for the four package managers RemoteDraw supports, and the
|
|
3
|
+
* detection that picks the right one for a project.
|
|
4
|
+
*
|
|
5
|
+
* Two things go wrong without this. First, every surface that prints a command
|
|
6
|
+
* invents its own spelling, and they drift: `pnpm install` is not `pnpm add`,
|
|
7
|
+
* `npx` is not `pnpm dlx`, and Yarn Berry has no global install at all. Second,
|
|
8
|
+
* the CLI defaults to npm and tells a pnpm user to run `npm install` in a
|
|
9
|
+
* pnpm project - a command that works, and quietly leaves a `package-lock.json`
|
|
10
|
+
* next to their `pnpm-lock.yaml`.
|
|
11
|
+
*
|
|
12
|
+
* The table below is the single source of truth. `tests/api/docs-route-coverage`
|
|
13
|
+
* style parity tests keep the docs snippets generated from it rather than typed
|
|
14
|
+
* out beside it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const packageManagerIds: readonly ["npm", "pnpm", "yarn", "bun"];
|
|
17
|
+
export type PackageManagerId = (typeof packageManagerIds)[number];
|
|
18
|
+
export type PackageManagerCommands = {
|
|
19
|
+
/** Install everything the manifest already declares. */
|
|
20
|
+
install: string;
|
|
21
|
+
/** Add a dependency to the project. */
|
|
22
|
+
add: string;
|
|
23
|
+
/**
|
|
24
|
+
* Install a CLI globally. `null` where the manager has no such concept:
|
|
25
|
+
* Yarn Berry removed `yarn global`, and points people at `yarn dlx` instead.
|
|
26
|
+
*/
|
|
27
|
+
addGlobal: string | null;
|
|
28
|
+
/** Run a published binary without installing it first. */
|
|
29
|
+
dlx: string;
|
|
30
|
+
/** Run a script from package.json. */
|
|
31
|
+
run: string;
|
|
32
|
+
/** The lockfiles this manager writes, most specific first. */
|
|
33
|
+
lockfiles: string[];
|
|
34
|
+
};
|
|
35
|
+
export declare const packageManagerCommands: Record<PackageManagerId, PackageManagerCommands>;
|
|
36
|
+
export declare function isPackageManagerId(value: string): value is PackageManagerId;
|
|
37
|
+
/**
|
|
38
|
+
* How a reader of the docs gets the CLI. A manager without a global install
|
|
39
|
+
* runs it straight from the registry instead, which is a real answer rather
|
|
40
|
+
* than a caveat.
|
|
41
|
+
*/
|
|
42
|
+
export declare function cliInstallCommand(manager: PackageManagerId, packageName: string): string;
|
|
43
|
+
/** `npm install @remotedraw/react`, `pnpm add @remotedraw/react`, ... */
|
|
44
|
+
export declare function addPackageCommand(manager: PackageManagerId, packageName: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Every manager sets `npm_config_user_agent` when it runs a script or a binary
|
|
47
|
+
* it installed, and the name comes first: "pnpm/10.4.1 npm/? node/v24.0.0".
|
|
48
|
+
* This is the only signal that reports the manager actually in use rather than
|
|
49
|
+
* one inferred from files on disk.
|
|
50
|
+
*/
|
|
51
|
+
export declare function packageManagerFromUserAgent(env: Record<string, string | undefined>): PackageManagerId | undefined;
|
|
52
|
+
type DetectRuntime = {
|
|
53
|
+
env: Record<string, string | undefined>;
|
|
54
|
+
exists: (filePath: string) => boolean | Promise<boolean>;
|
|
55
|
+
readFile: (filePath: string) => Promise<string>;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Walks up from `dir` looking for the nearest declaration, because the answer
|
|
59
|
+
* usually lives at the root of a monorepo rather than in the package being
|
|
60
|
+
* scaffolded into it.
|
|
61
|
+
*
|
|
62
|
+
* Order: the `packageManager` field (explicit), then a lockfile (evidence),
|
|
63
|
+
* then the running manager's user agent (ambient), then npm.
|
|
64
|
+
*/
|
|
65
|
+
export declare function detectPackageManager(dir: string, runtime: DetectRuntime, options?: {
|
|
66
|
+
pathModule?: {
|
|
67
|
+
join: (...parts: string[]) => string;
|
|
68
|
+
dirname: (p: string) => string;
|
|
69
|
+
};
|
|
70
|
+
}): Promise<{
|
|
71
|
+
manager: PackageManagerId;
|
|
72
|
+
reason: string;
|
|
73
|
+
}>;
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=packageManagers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packageManagers.d.ts","sourceRoot":"","sources":["../src/packageManagers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,yCAA0C,CAAC;AAEzE,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAAG;IACnC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CACzC,gBAAgB,EAChB,sBAAsB,CAqCvB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,MAAM,GAClB,MAAM,CAKR;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,gBAAgB,GAAG,SAAS,CAK9B;AAED,KAAK,aAAa,GAAG;IACnB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD,CAAC;AAmBF;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,CAAA;CAAO,GACtG,OAAO,CAAC;IAAE,OAAO,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAsCxD"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One table for the four package managers RemoteDraw supports, and the
|
|
3
|
+
* detection that picks the right one for a project.
|
|
4
|
+
*
|
|
5
|
+
* Two things go wrong without this. First, every surface that prints a command
|
|
6
|
+
* invents its own spelling, and they drift: `pnpm install` is not `pnpm add`,
|
|
7
|
+
* `npx` is not `pnpm dlx`, and Yarn Berry has no global install at all. Second,
|
|
8
|
+
* the CLI defaults to npm and tells a pnpm user to run `npm install` in a
|
|
9
|
+
* pnpm project - a command that works, and quietly leaves a `package-lock.json`
|
|
10
|
+
* next to their `pnpm-lock.yaml`.
|
|
11
|
+
*
|
|
12
|
+
* The table below is the single source of truth. `tests/api/docs-route-coverage`
|
|
13
|
+
* style parity tests keep the docs snippets generated from it rather than typed
|
|
14
|
+
* out beside it.
|
|
15
|
+
*/
|
|
16
|
+
export const packageManagerIds = ["npm", "pnpm", "yarn", "bun"];
|
|
17
|
+
export const packageManagerCommands = {
|
|
18
|
+
npm: {
|
|
19
|
+
install: "npm install",
|
|
20
|
+
add: "npm install",
|
|
21
|
+
addGlobal: "npm install -g",
|
|
22
|
+
dlx: "npx",
|
|
23
|
+
run: "npm run",
|
|
24
|
+
lockfiles: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
25
|
+
},
|
|
26
|
+
pnpm: {
|
|
27
|
+
install: "pnpm install",
|
|
28
|
+
add: "pnpm add",
|
|
29
|
+
addGlobal: "pnpm add -g",
|
|
30
|
+
dlx: "pnpm dlx",
|
|
31
|
+
run: "pnpm",
|
|
32
|
+
lockfiles: ["pnpm-lock.yaml"],
|
|
33
|
+
},
|
|
34
|
+
yarn: {
|
|
35
|
+
install: "yarn install",
|
|
36
|
+
add: "yarn add",
|
|
37
|
+
// Berry (Yarn 2+) dropped global installs entirely, and classic's
|
|
38
|
+
// `yarn global add` is deprecated. `yarn dlx` is the one answer that is
|
|
39
|
+
// correct on both.
|
|
40
|
+
addGlobal: null,
|
|
41
|
+
dlx: "yarn dlx",
|
|
42
|
+
run: "yarn",
|
|
43
|
+
lockfiles: ["yarn.lock"],
|
|
44
|
+
},
|
|
45
|
+
bun: {
|
|
46
|
+
install: "bun install",
|
|
47
|
+
add: "bun add",
|
|
48
|
+
addGlobal: "bun add -g",
|
|
49
|
+
dlx: "bunx",
|
|
50
|
+
run: "bun run",
|
|
51
|
+
lockfiles: ["bun.lock", "bun.lockb"],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
export function isPackageManagerId(value) {
|
|
55
|
+
return packageManagerIds.includes(value);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* How a reader of the docs gets the CLI. A manager without a global install
|
|
59
|
+
* runs it straight from the registry instead, which is a real answer rather
|
|
60
|
+
* than a caveat.
|
|
61
|
+
*/
|
|
62
|
+
export function cliInstallCommand(manager, packageName) {
|
|
63
|
+
const commands = packageManagerCommands[manager];
|
|
64
|
+
return commands.addGlobal
|
|
65
|
+
? `${commands.addGlobal} ${packageName}`
|
|
66
|
+
: `${commands.dlx} ${packageName}`;
|
|
67
|
+
}
|
|
68
|
+
/** `npm install @remotedraw/react`, `pnpm add @remotedraw/react`, ... */
|
|
69
|
+
export function addPackageCommand(manager, packageName) {
|
|
70
|
+
return `${packageManagerCommands[manager].add} ${packageName}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every manager sets `npm_config_user_agent` when it runs a script or a binary
|
|
74
|
+
* it installed, and the name comes first: "pnpm/10.4.1 npm/? node/v24.0.0".
|
|
75
|
+
* This is the only signal that reports the manager actually in use rather than
|
|
76
|
+
* one inferred from files on disk.
|
|
77
|
+
*/
|
|
78
|
+
export function packageManagerFromUserAgent(env) {
|
|
79
|
+
const agent = env.npm_config_user_agent?.trim();
|
|
80
|
+
if (!agent)
|
|
81
|
+
return undefined;
|
|
82
|
+
const name = agent.split("/")[0]?.trim().toLowerCase() ?? "";
|
|
83
|
+
return isPackageManagerId(name) ? name : undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Corepack's `packageManager` field, e.g. "pnpm@10.4.1+sha512...". It is the
|
|
87
|
+
* project's own declaration, so it outranks anything inferred.
|
|
88
|
+
*/
|
|
89
|
+
function managerFromManifest(contents) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(contents);
|
|
92
|
+
if (typeof parsed.packageManager !== "string")
|
|
93
|
+
return undefined;
|
|
94
|
+
const name = parsed.packageManager.split("@")[0]?.trim().toLowerCase() ?? "";
|
|
95
|
+
return isPackageManagerId(name) ? name : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A malformed package.json is the caller's problem, not a reason to fail
|
|
99
|
+
// a question that has a perfectly good fallback.
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Walks up from `dir` looking for the nearest declaration, because the answer
|
|
105
|
+
* usually lives at the root of a monorepo rather than in the package being
|
|
106
|
+
* scaffolded into it.
|
|
107
|
+
*
|
|
108
|
+
* Order: the `packageManager` field (explicit), then a lockfile (evidence),
|
|
109
|
+
* then the running manager's user agent (ambient), then npm.
|
|
110
|
+
*/
|
|
111
|
+
export async function detectPackageManager(dir, runtime, options = {}) {
|
|
112
|
+
const join = options.pathModule?.join ?? ((...parts) => parts.join("/"));
|
|
113
|
+
const dirname = options.pathModule?.dirname ??
|
|
114
|
+
((value) => value.slice(0, Math.max(value.lastIndexOf("/"), 0)));
|
|
115
|
+
let current = dir;
|
|
116
|
+
// A repo root is a handful of levels up at most; the bound is only here so a
|
|
117
|
+
// symlink loop cannot turn detection into a hang.
|
|
118
|
+
for (let depth = 0; depth < 32; depth += 1) {
|
|
119
|
+
const manifest = join(current, "package.json");
|
|
120
|
+
if (await runtime.exists(manifest)) {
|
|
121
|
+
const declared = managerFromManifest(await runtime.readFile(manifest));
|
|
122
|
+
if (declared) {
|
|
123
|
+
return {
|
|
124
|
+
manager: declared,
|
|
125
|
+
reason: `${manifest} declares packageManager: ${declared}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const manager of packageManagerIds) {
|
|
130
|
+
for (const lockfile of packageManagerCommands[manager].lockfiles) {
|
|
131
|
+
if (await runtime.exists(join(current, lockfile))) {
|
|
132
|
+
return { manager, reason: `found ${join(current, lockfile)}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const parent = dirname(current);
|
|
137
|
+
if (!parent || parent === current)
|
|
138
|
+
break;
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
const ambient = packageManagerFromUserAgent(runtime.env);
|
|
142
|
+
if (ambient) {
|
|
143
|
+
return { manager: ambient, reason: `running under ${ambient}` };
|
|
144
|
+
}
|
|
145
|
+
return { manager: "npm", reason: "no lockfile or declaration found" };
|
|
146
|
+
}
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type PackageManagerId } from "./packageManagers.js";
|
|
1
2
|
export type ScanRuntime = {
|
|
2
3
|
cwd: string;
|
|
3
4
|
exists: (filePath: string) => boolean | Promise<boolean>;
|
|
4
5
|
readFile: (filePath: string) => Promise<string>;
|
|
5
6
|
readdir?: (dirPath: string) => Promise<string[]>;
|
|
7
|
+
/** Only read for `npm_config_user_agent`, when nothing on disk answers. */
|
|
8
|
+
env?: Record<string, string | undefined>;
|
|
6
9
|
};
|
|
7
10
|
export type ScanProjectKind = "web" | "server" | "ios" | "library" | "unknown";
|
|
8
11
|
export type ScanProject = {
|
|
@@ -33,6 +36,22 @@ export type ScanReport = {
|
|
|
33
36
|
command: "scan";
|
|
34
37
|
schemaVersion: 1;
|
|
35
38
|
root: string;
|
|
39
|
+
/**
|
|
40
|
+
* The manager this project already uses, and the evidence for it. An agent
|
|
41
|
+
* that installs with the wrong one leaves a second lockfile behind, which is
|
|
42
|
+
* a mess a human then has to notice and clean up.
|
|
43
|
+
*/
|
|
44
|
+
packageManager: {
|
|
45
|
+
id: PackageManagerId;
|
|
46
|
+
reason: string;
|
|
47
|
+
/** Ready to run, so nothing downstream has to spell them itself. */
|
|
48
|
+
commands: {
|
|
49
|
+
install: string;
|
|
50
|
+
add: string;
|
|
51
|
+
dlx: string;
|
|
52
|
+
run: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
36
55
|
projects: ScanProject[];
|
|
37
56
|
remotedraw: {
|
|
38
57
|
configured: boolean;
|
package/dist/scan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAiBA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjD,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/E,MAAM,MAAM,WAAW,GAAG;IACxB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,eAAe,CAAC;IACtB,iEAAiE;IACjE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,cAAc,EAAE;QACd,EAAE,EAAE,gBAAgB,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,oEAAoE;QACpE,QAAQ,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;KACtE,CAAC;IACF,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,UAAU,EAAE;QACV,UAAU,EAAE,OAAO,CAAC;QACpB,WAAW,EAAE,MAAM,EAAE,CAAC;QACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,WAAW,EAAE;QACX,4EAA4E;QAC5E,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,eAAe,EAAE,kBAAkB,EAAE,CAAC;IACtC,iEAAiE;IACjE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,qDAAqD;IACrD,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAoPF,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CA6KrB;AA6HD,eAAO,MAAM,iBAAiB,UAa7B,CAAC;AAEF,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,UAiDhD"}
|