@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/dist/scan.js CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { readdir, readFile, stat } from "node:fs/promises";
16
16
  import path from "node:path";
17
+ import { detectPackageManager, packageManagerCommands, } from "./packageManagers.js";
17
18
  const SKIPPED_DIRECTORIES = new Set([
18
19
  "node_modules",
19
20
  ".git",
@@ -355,11 +356,23 @@ export async function scanProject(root, runtime) {
355
356
  keyBoundary.warning =
356
357
  "No server-side code was found. POST /v1/sessions needs an rd_sk_ key, which must never ship in a browser or app bundle — ask the user where their backend (or a serverless function) lives before scaffolding anything.";
357
358
  }
359
+ const detected = await detectPackageManager(root, { env: runtime.env ?? {}, exists: runtime.exists, readFile: runtime.readFile }, { pathModule: path });
360
+ const managerCommands = packageManagerCommands[detected.manager];
358
361
  return {
359
362
  ok: true,
360
363
  command: "scan",
361
364
  schemaVersion: 1,
362
365
  root,
366
+ packageManager: {
367
+ id: detected.manager,
368
+ reason: detected.reason,
369
+ commands: {
370
+ install: managerCommands.install,
371
+ add: managerCommands.add,
372
+ dlx: managerCommands.dlx,
373
+ run: managerCommands.run,
374
+ },
375
+ },
363
376
  projects,
364
377
  remotedraw: {
365
378
  configured: configFiles.length > 0 || remotedrawPackages.size > 0 || swiftPackage,
@@ -461,8 +474,9 @@ function buildRecommendations(input) {
461
474
  }
462
475
  function productQuestions(input) {
463
476
  const questions = [
464
- "What should people draw on a blank sketch surface, a photo, a PDF, a map, a form field, or a shared screen? This picks target.kind and the preset.",
465
- "Where in the product does the drawing live? Name the existing screen or route the receiver goes on; RemoteDraw does not get its own page unless the user asks for a demo.",
477
+ "Which screen in the product already shows the thing people will draw on, and what device is that screen on? RemoteDraw needs two devices: that screen is the receiver, and a phone is the pen. If the only screen involved is the phone's own, this is not a RemoteDraw flow.",
478
+ "What is on that screen a blank sketch surface, a photo your app already shows, a PDF page it renders, a map it owns, a form field, or a shared screen? This picks target.kind and the preset. Your app renders it; the phone never supplies it.",
479
+ "Where does the receiver go? Name the existing screen or route; RemoteDraw does not get its own page unless the user asks for a demo.",
466
480
  "Who holds the phone — the same signed-in user (direct sender, no QR), or anyone who scans (QR join)?",
467
481
  "What happens to the drawing when the phone taps Submit — saved to which record, shown where, exported how?",
468
482
  "Is this a minimal demo to evaluate RemoteDraw, or the real feature? A demo is one page; the feature is the answers above.",
@@ -476,10 +490,14 @@ function productQuestions(input) {
476
490
  return questions;
477
491
  }
478
492
  export const INTEGRATION_RULES = [
493
+ "RemoteDraw needs two devices: the receiver is a screen the user is already looking at (desktop, laptop, large display) showing your content, and the sender is a phone. A phone that photographs something and annotates it on its own screen is not a RemoteDraw flow — say so and propose PencilKit or a canvas instead.",
494
+ "The phone supplies a hand, not content. Your app renders the photo, the PDF page, the map, the form; the iOS sender opens no camera and no file picker.",
479
495
  "Ask before installing anything (packages, CLI, Swift package) — name what and why, then wait.",
480
496
  "rd_sk_ keys live only in server-side code found by this scan; never in Vite/Next client source, Swift, or an app bundle.",
481
497
  "iOS senders use RemoteDrawSenderKit's full-screen surface (`.remoteDrawSurface` / `RemoteDrawTakeover`). Do not write a UIViewRepresentable canvas, and do not put a small canvas inside a sheet.",
482
498
  "Web senders use the hosted /join page unless the user explicitly wants an in-page sender.",
499
+ "Bluetooth and local-network pairing are not implemented anywhere in the product — the radios only advertise, nothing scans. Do not offer them; QR and direct sender are the real methods. There is no native Android SDK and no desktop sender either.",
500
+ "Use the ready-made components: PairingCode / RemoteDrawConnect / RemoteDrawLaunchButton for pairing, RemoteDrawReceiver (or RemoteDrawMapReceiver over a map) for ink, RemoteDrawSessionControls for state. Do not hand-roll a connect button, an SVG receiver, or a polling loop.",
483
501
  "Timestamps (`occurredAt`, point `t`) are integer milliseconds; the SDKs handle this, hand-written clients often do not.",
484
502
  "Session creation is billable and not idempotent: guard React StrictMode double effects, and persist the receiverToken if the receiver outlives a page load.",
485
503
  "One API key serves every tenant: scope sessions with externalId and verify it before attaching a sender or ending a session.",
@@ -488,7 +506,8 @@ export const INTEGRATION_RULES = [
488
506
  export function renderScanText(report) {
489
507
  const lines = [];
490
508
  lines.push("RemoteDraw scan", "");
491
- lines.push(`Root: ${report.root}`, "");
509
+ lines.push(`Root: ${report.root}`);
510
+ lines.push(`Package manager: ${report.packageManager.id} (${report.packageManager.reason})`, "");
492
511
  lines.push("Projects:");
493
512
  if (report.projects.length === 0)
494
513
  lines.push(" (none detected)");
package/dist/update.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type CloudRuntime } from "./cloud.js";
2
+ import { type PackageManagerId } from "./packageManagers.js";
2
3
  export declare const CLI_PACKAGE_NAME = "@remotedraw/cli";
3
4
  export type UpdateRuntime = CloudRuntime & {
4
5
  /**
@@ -12,8 +13,12 @@ export type UpdateRuntime = CloudRuntime & {
12
13
  };
13
14
  export type InstallMethod = {
14
15
  /** Package manager the binary appears to have been installed with. */
15
- manager: "npm" | "pnpm" | "yarn" | "bun";
16
- /** True when the binary was resolved by npx/bunx, which never needs updating. */
16
+ manager: PackageManagerId;
17
+ /**
18
+ * True when the binary was resolved by a throwaway runner (npx, pnpm dlx,
19
+ * yarn dlx, bunx). Nothing survives the command, so there is nothing to
20
+ * update: the next run fetches the latest by itself.
21
+ */
17
22
  ephemeral: boolean;
18
23
  command: string;
19
24
  args: string[];
@@ -29,10 +34,11 @@ export declare function parseVersion(value: string): {
29
34
  };
30
35
  export declare function compareVersions(a: string, b: string): 1 | 0 | -1;
31
36
  /**
32
- * Derives the update command from where the running binary lives. Guessing
33
- * wrong only prints the wrong suggestion, so the fallback is plain npm.
37
+ * Derives the update command from where the running binary lives, falling back
38
+ * to the manager that invoked us. Guessing wrong only prints the wrong
39
+ * suggestion, so the last fallback is plain npm.
34
40
  */
35
- export declare function detectInstallMethod(binPath: string): InstallMethod;
41
+ export declare function detectInstallMethod(binPath: string, env?: Record<string, string | undefined>): InstallMethod;
36
42
  export declare function installMethodDisplay(method: InstallMethod): string;
37
43
  export declare function updateCachePath(runtime: UpdateRuntime): string;
38
44
  export declare function latestPublishedVersion(runtime: UpdateRuntime, timeoutMs?: number): Promise<string | undefined>;
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../src/update.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAGjE,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAOlD,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IACzC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,sEAAsE;IACtE,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;IACzC,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAiBF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM;;;EAczC;AA2BD,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cASnD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAoClE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,UAEzD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,UAErD;AAgDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,aAAa,EACtB,SAAS,SAAmB,+BAoC7B;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,WAO3E;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,aAAa,EACtB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA2BlC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,UAWrE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa;;;;;;GAM7E"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../src/update.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAOlD,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IACzC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,sEAAsE;IACtE,OAAO,EAAE,gBAAgB,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAiBF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM;;;EAczC;AA2BD,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cASnD;AAuCD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM,GAC3C,aAAa,CAsCf;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,UAEzD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,UAErD;AAgDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,aAAa,EACtB,SAAS,SAAmB,+BAoC7B;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,WAO3E;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,aAAa,EACtB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA2BlC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,UAWrE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa;;;;;;GAM7E"}
package/dist/update.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { globalConfigPath } from "./cloud.js";
3
3
  import { t } from "./i18n.js";
4
+ import { packageManagerCommands, packageManagerFromUserAgent, } from "./packageManagers.js";
4
5
  export const CLI_PACKAGE_NAME = "@remotedraw/cli";
5
6
  const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
6
7
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -75,45 +76,71 @@ export function compareVersions(a, b) {
75
76
  }
76
77
  return comparePrerelease(left.prerelease, right.prerelease);
77
78
  }
79
+ /** Splits a table entry like "pnpm dlx" into something spawnable. */
80
+ function commandLine(text, ...extra) {
81
+ const [command, ...args] = text.split(" ");
82
+ return { command: command, args: [...args, ...extra] };
83
+ }
84
+ /**
85
+ * Where each manager parks a package it is running once and throwing away.
86
+ * Matching the wrong one is not cosmetic: telling someone who ran `pnpm dlx`
87
+ * to `pnpm add --global` installs a copy they never asked for.
88
+ */
89
+ const EPHEMERAL_PATHS = [
90
+ ["npm", /\/(_npx|\.npm\/_npx)\//],
91
+ ["bun", /\/\.bun\/install\/cache\//],
92
+ ["pnpm", /\/pnpm\/dlx\//],
93
+ // Berry's `yarn dlx` builds a throwaway project directory named dlx-<pid>.
94
+ ["yarn", /\/dlx-\d+\//],
95
+ ];
96
+ /** Where each manager parks a package it installed to stay. */
97
+ const INSTALLED_PATHS = [
98
+ ["bun", /\/\.bun\//],
99
+ ["pnpm", /\/(\.pnpm|pnpm)\//],
100
+ ["yarn", /\/(\.yarn|[Yy]arn)\//],
101
+ ];
78
102
  /**
79
- * Derives the update command from where the running binary lives. Guessing
80
- * wrong only prints the wrong suggestion, so the fallback is plain npm.
103
+ * Yarn Berry removed global installs, so `yarn global add` is not a command
104
+ * that exists for it; a Berry user has the CLI as a project dependency, and
105
+ * `yarn up` is what moves it. Classic (v1) still has the global store.
81
106
  */
82
- export function detectInstallMethod(binPath) {
107
+ function yarnIsBerry(normalizedBinPath) {
108
+ return /(\.yarn\/(berry|cache|unplugged|__virtual__)|\.pnp)/.test(normalizedBinPath);
109
+ }
110
+ /**
111
+ * Derives the update command from where the running binary lives, falling back
112
+ * to the manager that invoked us. Guessing wrong only prints the wrong
113
+ * suggestion, so the last fallback is plain npm.
114
+ */
115
+ export function detectInstallMethod(binPath, env = {}) {
83
116
  const normalized = binPath.replace(/\\/g, "/");
84
117
  const update = `${CLI_PACKAGE_NAME}@latest`;
85
- if (/\/(_npx|\.npm\/_npx|\.bun\/install\/cache)\//.test(normalized)) {
86
- return { manager: "npm", ephemeral: true, command: "npx", args: [update] };
87
- }
88
- if (/\/\.bun\//.test(normalized)) {
89
- return {
90
- manager: "bun",
91
- ephemeral: false,
92
- command: "bun",
93
- args: ["add", "--global", update],
94
- };
95
- }
96
- if (/\/(\.pnpm|pnpm)\//.test(normalized)) {
97
- return {
98
- manager: "pnpm",
99
- ephemeral: false,
100
- command: "pnpm",
101
- args: ["add", "--global", update],
102
- };
118
+ for (const [manager, pattern] of EPHEMERAL_PATHS) {
119
+ if (pattern.test(normalized)) {
120
+ return {
121
+ manager,
122
+ ephemeral: true,
123
+ ...commandLine(packageManagerCommands[manager].dlx, update),
124
+ };
125
+ }
103
126
  }
104
- if (/\/(\.yarn|Yarn)\//.test(normalized)) {
127
+ // The path says who owns the binary; the user agent only says who launched
128
+ // this run, which can be a project's npm script over a pnpm-installed CLI.
129
+ const manager = INSTALLED_PATHS.find(([, pattern]) => pattern.test(normalized))?.[0] ??
130
+ packageManagerFromUserAgent(env) ??
131
+ "npm";
132
+ if (manager === "yarn") {
105
133
  return {
106
- manager: "yarn",
134
+ manager,
107
135
  ephemeral: false,
108
- command: "yarn",
109
- args: ["global", "add", update],
136
+ ...commandLine(yarnIsBerry(normalized) ? "yarn up" : "yarn global add", update),
110
137
  };
111
138
  }
139
+ const addGlobal = packageManagerCommands[manager].addGlobal;
112
140
  return {
113
- manager: "npm",
141
+ manager,
114
142
  ephemeral: false,
115
- command: "npm",
116
- args: ["install", "--global", update],
143
+ ...commandLine(addGlobal ?? packageManagerCommands[manager].dlx, update),
117
144
  };
118
145
  }
119
146
  export function installMethodDisplay(method) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotedraw/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line tools for creating and inspecting RemoteDraw integrations.",
5
5
  "type": "module",
6
6
  "bin": {