@remotedraw/cli 0.2.1 → 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 +36 -6
- package/dist/cli.d.ts +17 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +261 -64
- package/dist/cloud.d.ts +1 -1
- package/dist/cloud.d.ts.map +1 -1
- 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 +27 -12
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/en.js +27 -12
- package/dist/locales/index.d.ts +28 -13
- package/dist/locales/index.d.ts.map +1 -1
- package/dist/locales/nl.d.ts.map +1 -1
- package/dist/locales/nl.js +26 -11
- 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 +77 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +563 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- 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
package/dist/scan.js
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remotedraw scan` — read a codebase before proposing how RemoteDraw fits it.
|
|
3
|
+
*
|
|
4
|
+
* A coding agent handed "add RemoteDraw" and nothing else has two failure
|
|
5
|
+
* modes: inventing a product (a drawing lab nobody asked for) and inventing an
|
|
6
|
+
* implementation (a hand-written HTTP sender in a sheet when the SDK ships a
|
|
7
|
+
* full-screen surface). This command removes the excuse for both. It walks the
|
|
8
|
+
* project, names what is there — web frameworks, server-side code, iOS apps,
|
|
9
|
+
* an existing RemoteDraw wiring — and turns that into integration options plus
|
|
10
|
+
* the product questions only the user can answer.
|
|
11
|
+
*
|
|
12
|
+
* It reads manifests and file names, never file contents beyond a few kilobytes
|
|
13
|
+
* of Swift (`import SwiftUI`), and reports env *keys*, never values.
|
|
14
|
+
*/
|
|
15
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { detectPackageManager, packageManagerCommands, } from "./packageManagers.js";
|
|
18
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
19
|
+
"node_modules",
|
|
20
|
+
".git",
|
|
21
|
+
"dist",
|
|
22
|
+
"build",
|
|
23
|
+
"out",
|
|
24
|
+
".next",
|
|
25
|
+
".nuxt",
|
|
26
|
+
".svelte-kit",
|
|
27
|
+
".output",
|
|
28
|
+
".turbo",
|
|
29
|
+
".cache",
|
|
30
|
+
"coverage",
|
|
31
|
+
"DerivedData",
|
|
32
|
+
"Pods",
|
|
33
|
+
"Carthage",
|
|
34
|
+
".build",
|
|
35
|
+
".swiftpm",
|
|
36
|
+
"vendor",
|
|
37
|
+
"target",
|
|
38
|
+
".venv",
|
|
39
|
+
"venv",
|
|
40
|
+
"__pycache__",
|
|
41
|
+
".agent-worktrees",
|
|
42
|
+
".claude",
|
|
43
|
+
".sandbox",
|
|
44
|
+
]);
|
|
45
|
+
const MAX_DEPTH = 4;
|
|
46
|
+
const MAX_DIRECTORIES = 4000;
|
|
47
|
+
const SWIFT_SAMPLE_LIMIT = 40;
|
|
48
|
+
async function listDirectory(dirPath, runtime) {
|
|
49
|
+
try {
|
|
50
|
+
if (runtime.readdir)
|
|
51
|
+
return await runtime.readdir(dirPath);
|
|
52
|
+
return await readdir(dirPath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function isDirectory(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
return (await stat(filePath)).isDirectory();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function readText(filePath, runtime) {
|
|
67
|
+
try {
|
|
68
|
+
return await runtime.readFile(filePath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
try {
|
|
72
|
+
return await readFile(filePath, "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readManifest(filePath, runtime) {
|
|
80
|
+
const text = await readText(filePath, runtime);
|
|
81
|
+
if (!text)
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Breadth-first walk, bounded, skipping build output and dependencies. */
|
|
91
|
+
async function walk(root, runtime) {
|
|
92
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
93
|
+
const visited = [];
|
|
94
|
+
while (queue.length > 0 && visited.length < MAX_DIRECTORIES) {
|
|
95
|
+
const next = queue.shift();
|
|
96
|
+
const entries = await listDirectory(next.dir, runtime);
|
|
97
|
+
const relative = path.relative(root, next.dir) || ".";
|
|
98
|
+
visited.push({ dir: next.dir, relative, entries, depth: next.depth });
|
|
99
|
+
if (next.depth >= MAX_DEPTH)
|
|
100
|
+
continue;
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.startsWith("._"))
|
|
103
|
+
continue;
|
|
104
|
+
if (SKIPPED_DIRECTORIES.has(entry))
|
|
105
|
+
continue;
|
|
106
|
+
// Xcode projects and bundles are directories a walker has no business in.
|
|
107
|
+
if (/\.(xcodeproj|xcworkspace|xcassets|app|framework|bundle|playground)$/.test(entry)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const child = path.join(next.dir, entry);
|
|
111
|
+
if (await isDirectory(child))
|
|
112
|
+
queue.push({ dir: child, depth: next.depth + 1 });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return visited;
|
|
116
|
+
}
|
|
117
|
+
function allDependencies(manifest) {
|
|
118
|
+
return {
|
|
119
|
+
...manifest.peerDependencies,
|
|
120
|
+
...manifest.devDependencies,
|
|
121
|
+
...manifest.dependencies,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const WEB_FRAMEWORKS = [
|
|
125
|
+
["next", "next"],
|
|
126
|
+
["react", "react"],
|
|
127
|
+
["react-dom", "react-dom"],
|
|
128
|
+
["@sveltejs/kit", "sveltekit"],
|
|
129
|
+
["svelte", "svelte"],
|
|
130
|
+
["vue", "vue"],
|
|
131
|
+
["nuxt", "nuxt"],
|
|
132
|
+
["@angular/core", "angular"],
|
|
133
|
+
["solid-js", "solid"],
|
|
134
|
+
["@remix-run/react", "remix"],
|
|
135
|
+
["react-router", "react-router"],
|
|
136
|
+
["react-router-dom", "react-router"],
|
|
137
|
+
["vite", "vite"],
|
|
138
|
+
["@tanstack/react-router", "tanstack-router"],
|
|
139
|
+
["expo", "expo"],
|
|
140
|
+
["react-native", "react-native"],
|
|
141
|
+
["electron", "electron"],
|
|
142
|
+
["@tauri-apps/api", "tauri"],
|
|
143
|
+
];
|
|
144
|
+
const SERVER_FRAMEWORKS = [
|
|
145
|
+
["convex", "convex"],
|
|
146
|
+
["express", "express"],
|
|
147
|
+
["fastify", "fastify"],
|
|
148
|
+
["hono", "hono"],
|
|
149
|
+
["koa", "koa"],
|
|
150
|
+
["@nestjs/core", "nestjs"],
|
|
151
|
+
["@trpc/server", "trpc"],
|
|
152
|
+
["@vercel/node", "vercel-functions"],
|
|
153
|
+
["firebase-functions", "firebase-functions"],
|
|
154
|
+
["@supabase/supabase-js", "supabase"],
|
|
155
|
+
["@aws-sdk/client-lambda", "aws-lambda"],
|
|
156
|
+
["aws-lambda", "aws-lambda"],
|
|
157
|
+
["next", "next-route-handlers"],
|
|
158
|
+
["@sveltejs/kit", "sveltekit-endpoints"],
|
|
159
|
+
["nuxt", "nuxt-server"],
|
|
160
|
+
["@remix-run/node", "remix-loaders"],
|
|
161
|
+
];
|
|
162
|
+
const SERVER_DIRECTORIES = ["server", "api", "backend", "functions", "convex", "worker", "workers", "lambda", "netlify/functions", "supabase/functions"];
|
|
163
|
+
function classifyManifest(manifest, entries, relative) {
|
|
164
|
+
const deps = allDependencies(manifest);
|
|
165
|
+
const frameworks = [];
|
|
166
|
+
const evidence = [];
|
|
167
|
+
const serverSide = [];
|
|
168
|
+
const add = (label, proof) => {
|
|
169
|
+
if (!frameworks.includes(label))
|
|
170
|
+
frameworks.push(label);
|
|
171
|
+
evidence.push(proof);
|
|
172
|
+
};
|
|
173
|
+
let web = false;
|
|
174
|
+
for (const [dependency, label] of WEB_FRAMEWORKS) {
|
|
175
|
+
if (deps[dependency]) {
|
|
176
|
+
add(label, `${relative}/package.json depends on ${dependency}`);
|
|
177
|
+
if (!["electron", "tauri", "expo", "react-native"].includes(label))
|
|
178
|
+
web = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let server = false;
|
|
182
|
+
for (const [dependency, label] of SERVER_FRAMEWORKS) {
|
|
183
|
+
if (deps[dependency]) {
|
|
184
|
+
add(label, `${relative}/package.json depends on ${dependency}`);
|
|
185
|
+
server = true;
|
|
186
|
+
serverSide.push(label);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const directory of SERVER_DIRECTORIES) {
|
|
190
|
+
const [head] = directory.split("/");
|
|
191
|
+
if (head && entries.includes(head)) {
|
|
192
|
+
const proof = `${relative}/${directory}/ exists`;
|
|
193
|
+
if (!evidence.includes(proof))
|
|
194
|
+
evidence.push(proof);
|
|
195
|
+
server = true;
|
|
196
|
+
if (!serverSide.includes(directory))
|
|
197
|
+
serverSide.push(directory);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (entries.includes("app") && deps.next) {
|
|
201
|
+
serverSide.push("app/**/route.ts (Next.js route handlers)");
|
|
202
|
+
}
|
|
203
|
+
if (entries.includes("pages") && deps.next) {
|
|
204
|
+
serverSide.push("pages/api (Next.js API routes)");
|
|
205
|
+
}
|
|
206
|
+
const kind = web
|
|
207
|
+
? "web"
|
|
208
|
+
: server
|
|
209
|
+
? "server"
|
|
210
|
+
: frameworks.length > 0
|
|
211
|
+
? "web"
|
|
212
|
+
: manifest.private === false || entries.includes("src")
|
|
213
|
+
? "library"
|
|
214
|
+
: "unknown";
|
|
215
|
+
return { kind, frameworks, evidence, serverSide };
|
|
216
|
+
}
|
|
217
|
+
async function detectSwiftUI(dir, entries, runtime) {
|
|
218
|
+
const swiftFiles = entries.filter((entry) => entry.endsWith(".swift")).slice(0, SWIFT_SAMPLE_LIMIT);
|
|
219
|
+
let swiftui = false;
|
|
220
|
+
let uikit = false;
|
|
221
|
+
for (const file of swiftFiles) {
|
|
222
|
+
const text = await readText(path.join(dir, file), runtime);
|
|
223
|
+
const head = text.slice(0, 4000);
|
|
224
|
+
if (/^\s*import SwiftUI/m.test(head))
|
|
225
|
+
swiftui = true;
|
|
226
|
+
if (/^\s*import UIKit/m.test(head))
|
|
227
|
+
uikit = true;
|
|
228
|
+
if (swiftui && uikit)
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return { swiftui, uikit, sampled: swiftFiles.length };
|
|
232
|
+
}
|
|
233
|
+
export async function scanProject(root, runtime) {
|
|
234
|
+
const visited = await walk(root, runtime);
|
|
235
|
+
const projects = [];
|
|
236
|
+
const configFiles = [];
|
|
237
|
+
const remotedrawPackages = new Set();
|
|
238
|
+
const envKeys = new Set();
|
|
239
|
+
let swiftPackage = false;
|
|
240
|
+
// iOS apps announce themselves by an .xcodeproj / .xcworkspace / project.yml
|
|
241
|
+
// beside their sources; Swift files under that directory tell us the UI
|
|
242
|
+
// framework. A directory holding both is one project, not two.
|
|
243
|
+
const iosRoots = new Map();
|
|
244
|
+
for (const entry of visited) {
|
|
245
|
+
const relative = entry.relative;
|
|
246
|
+
if (entry.entries.includes("remotedraw.config.json")) {
|
|
247
|
+
configFiles.push(relative === "." ? "remotedraw.config.json" : `${relative}/remotedraw.config.json`);
|
|
248
|
+
}
|
|
249
|
+
for (const envFile of entry.entries.filter((name) => /^\.env(\..+)?$/.test(name))) {
|
|
250
|
+
const text = await readText(path.join(entry.dir, envFile), runtime);
|
|
251
|
+
for (const match of text.matchAll(/^\s*(REMOTEDRAW_[A-Z0-9_]+)\s*=/gm)) {
|
|
252
|
+
envKeys.add(match[1]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (entry.entries.includes("package.json")) {
|
|
256
|
+
const manifest = await readManifest(path.join(entry.dir, "package.json"), runtime);
|
|
257
|
+
if (manifest) {
|
|
258
|
+
const deps = allDependencies(manifest);
|
|
259
|
+
for (const name of Object.keys(deps)) {
|
|
260
|
+
if (name.startsWith("@remotedraw/"))
|
|
261
|
+
remotedrawPackages.add(name);
|
|
262
|
+
}
|
|
263
|
+
const classified = classifyManifest(manifest, entry.entries, relative);
|
|
264
|
+
const isWorkspaceRoot = manifest.workspaces != null && classified.frameworks.length === 0;
|
|
265
|
+
if (!isWorkspaceRoot) {
|
|
266
|
+
projects.push({
|
|
267
|
+
path: relative,
|
|
268
|
+
...(manifest.name ? { name: manifest.name } : {}),
|
|
269
|
+
kind: classified.kind,
|
|
270
|
+
frameworks: classified.frameworks,
|
|
271
|
+
evidence: classified.evidence,
|
|
272
|
+
...(classified.serverSide.length > 0 ? { serverSide: classified.serverSide } : {}),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const xcode = entry.entries.filter((name) => !name.startsWith("._") && /\.(xcodeproj|xcworkspace)$/.test(name));
|
|
278
|
+
const hasProjectYml = entry.entries.includes("project.yml");
|
|
279
|
+
const hasPackageSwift = entry.entries.includes("Package.swift");
|
|
280
|
+
if (hasPackageSwift) {
|
|
281
|
+
const text = await readText(path.join(entry.dir, "Package.swift"), runtime);
|
|
282
|
+
if (text.includes("remotedraw-swift") || text.includes("RemoteDrawSenderKit"))
|
|
283
|
+
swiftPackage = true;
|
|
284
|
+
}
|
|
285
|
+
if (xcode.length > 0 || hasProjectYml) {
|
|
286
|
+
const project = {
|
|
287
|
+
path: relative,
|
|
288
|
+
kind: "ios",
|
|
289
|
+
frameworks: [],
|
|
290
|
+
evidence: xcode.map((name) => `${relative}/${name}`),
|
|
291
|
+
};
|
|
292
|
+
if (hasProjectYml)
|
|
293
|
+
project.evidence.push(`${relative}/project.yml (xcodegen)`);
|
|
294
|
+
for (const name of xcode) {
|
|
295
|
+
const pbxproj = path.join(entry.dir, name, "project.pbxproj");
|
|
296
|
+
const text = await readText(pbxproj, runtime);
|
|
297
|
+
if (text.includes("remotedraw-swift") || text.includes("RemoteDrawSenderKit"))
|
|
298
|
+
swiftPackage = true;
|
|
299
|
+
if (text.includes("SwiftUI"))
|
|
300
|
+
project.evidence.push(`${relative}/${name} references SwiftUI`);
|
|
301
|
+
}
|
|
302
|
+
const resolved = path.join(entry.dir, "Package.resolved");
|
|
303
|
+
if (entry.entries.includes("Package.resolved")) {
|
|
304
|
+
const text = await readText(resolved, runtime);
|
|
305
|
+
if (text.includes("remotedraw-swift"))
|
|
306
|
+
swiftPackage = true;
|
|
307
|
+
}
|
|
308
|
+
iosRoots.set(relative, project);
|
|
309
|
+
projects.push(project);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Second pass: UI framework per iOS root, from Swift files at or below it.
|
|
313
|
+
for (const [iosRelative, project] of iosRoots) {
|
|
314
|
+
let swiftui = false;
|
|
315
|
+
let uikit = false;
|
|
316
|
+
let sampled = 0;
|
|
317
|
+
for (const entry of visited) {
|
|
318
|
+
const inside = iosRelative === "." ||
|
|
319
|
+
entry.relative === iosRelative ||
|
|
320
|
+
entry.relative.startsWith(`${iosRelative}${path.sep}`) ||
|
|
321
|
+
entry.relative.startsWith(`${iosRelative}/`);
|
|
322
|
+
if (!inside || sampled >= SWIFT_SAMPLE_LIMIT)
|
|
323
|
+
continue;
|
|
324
|
+
const result = await detectSwiftUI(entry.dir, entry.entries, runtime);
|
|
325
|
+
sampled += result.sampled;
|
|
326
|
+
swiftui ||= result.swiftui;
|
|
327
|
+
uikit ||= result.uikit;
|
|
328
|
+
}
|
|
329
|
+
if (swiftui) {
|
|
330
|
+
project.frameworks.push("swiftui");
|
|
331
|
+
project.evidence.push("Swift sources import SwiftUI");
|
|
332
|
+
}
|
|
333
|
+
if (uikit) {
|
|
334
|
+
project.frameworks.push("uikit");
|
|
335
|
+
project.evidence.push("Swift sources import UIKit");
|
|
336
|
+
}
|
|
337
|
+
if (!swiftui && !uikit && sampled === 0) {
|
|
338
|
+
project.evidence.push("no Swift sources sampled under this directory");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const webProjects = projects.filter((project) => project.kind === "web");
|
|
342
|
+
const serverProjects = projects.filter((project) => project.kind === "server");
|
|
343
|
+
const iosProjects = projects.filter((project) => project.kind === "ios");
|
|
344
|
+
const serverSide = [
|
|
345
|
+
...serverProjects.map((project) => project.path),
|
|
346
|
+
...webProjects.flatMap((project) => (project.serverSide ?? []).map((location) => project.path === "." ? location : `${project.path} (${location})`)),
|
|
347
|
+
];
|
|
348
|
+
const recommendations = buildRecommendations({
|
|
349
|
+
webProjects,
|
|
350
|
+
iosProjects,
|
|
351
|
+
serverSide,
|
|
352
|
+
swiftPackage,
|
|
353
|
+
});
|
|
354
|
+
const keyBoundary = { serverSide };
|
|
355
|
+
if (serverSide.length === 0) {
|
|
356
|
+
keyBoundary.warning =
|
|
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.";
|
|
358
|
+
}
|
|
359
|
+
const detected = await detectPackageManager(root, { env: runtime.env ?? {}, exists: runtime.exists, readFile: runtime.readFile }, { pathModule: path });
|
|
360
|
+
const managerCommands = packageManagerCommands[detected.manager];
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
command: "scan",
|
|
364
|
+
schemaVersion: 1,
|
|
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
|
+
},
|
|
376
|
+
projects,
|
|
377
|
+
remotedraw: {
|
|
378
|
+
configured: configFiles.length > 0 || remotedrawPackages.size > 0 || swiftPackage,
|
|
379
|
+
configFiles,
|
|
380
|
+
packages: [...remotedrawPackages].sort(),
|
|
381
|
+
envKeys: [...envKeys].sort(),
|
|
382
|
+
swiftPackage,
|
|
383
|
+
},
|
|
384
|
+
keyBoundary,
|
|
385
|
+
recommendations,
|
|
386
|
+
questions: productQuestions({ webProjects, iosProjects }),
|
|
387
|
+
rules: INTEGRATION_RULES,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const SWIFT_PACKAGE_LINE = 'Add https://github.com/AxioSOzo/remotedraw-swift.git (from: "0.1.0"), product RemoteDrawSenderKit';
|
|
391
|
+
function buildRecommendations(input) {
|
|
392
|
+
const { webProjects, iosProjects, serverSide } = input;
|
|
393
|
+
const recommendations = [];
|
|
394
|
+
const keyLocation = serverSide.length > 0
|
|
395
|
+
? `Session creation (rd_sk_) belongs in: ${serverSide.join(", ")}`
|
|
396
|
+
: "No server-side location found — ask the user; do not scaffold createRemoteDrawSession into client source.";
|
|
397
|
+
const receiverFor = (project) => {
|
|
398
|
+
const frameworks = project.frameworks;
|
|
399
|
+
if (frameworks.includes("svelte") || frameworks.includes("sveltekit")) {
|
|
400
|
+
return {
|
|
401
|
+
receiver: `@remotedraw/svelte receiver store in ${project.path}`,
|
|
402
|
+
packages: ["@remotedraw/svelte"],
|
|
403
|
+
sdk: "svelte",
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (frameworks.includes("react") || frameworks.includes("next") || frameworks.includes("remix")) {
|
|
407
|
+
return {
|
|
408
|
+
receiver: `@remotedraw/react — RemoteDrawProvider + RemoteDrawReceiver + PairingCode in ${project.path}`,
|
|
409
|
+
packages: ["@remotedraw/react"],
|
|
410
|
+
sdk: "react",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
receiver: `@remotedraw/client — createHttpReceiverClient + createRealtimeReceiverSource in ${project.path} (${frameworks.join(", ") || "no framework detected"})`,
|
|
415
|
+
packages: ["@remotedraw/client"],
|
|
416
|
+
sdk: "js",
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
for (const project of webProjects) {
|
|
420
|
+
const { receiver, packages, sdk } = receiverFor(project);
|
|
421
|
+
const hasIos = iosProjects.length > 0;
|
|
422
|
+
recommendations.push({
|
|
423
|
+
id: `web-receiver:${project.path}`,
|
|
424
|
+
title: hasIos
|
|
425
|
+
? `Receiver in ${project.path}, sender in the existing iOS app`
|
|
426
|
+
: `Receiver in ${project.path}, hosted phone sender (no phone code)`,
|
|
427
|
+
fit: "strong",
|
|
428
|
+
receiver,
|
|
429
|
+
sender: hasIos
|
|
430
|
+
? `RemoteDrawSenderKit full-screen surface in ${iosProjects.map((p) => p.path).join(", ")}; the backend mints an rd_send_ token with POST /v1/sessions/direct-sender for a signed-in user, QR scan stays as the fallback`
|
|
431
|
+
: "The hosted /join page: render the session's joinUrl as a QR (PairingCode). The phone opens it in Safari or the RemoteDraw iOS app; you ship no sender code.",
|
|
432
|
+
keyLocation,
|
|
433
|
+
why: hasIos
|
|
434
|
+
? "The product already has a phone app; making it the pen removes the QR step for signed-in users and keeps the drawing experience native."
|
|
435
|
+
: "The sender is not where the product adds value; the hosted pad is polished, full screen, and costs no code.",
|
|
436
|
+
commands: [
|
|
437
|
+
`remotedraw init --non-interactive --dry-run --format json --path ${project.path} --target web --sender ${hasIos ? "own-ios --sdk " + sdk : "remotedraw-ios --sdk " + sdk} --preset sketch`,
|
|
438
|
+
],
|
|
439
|
+
packages,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
for (const project of iosProjects) {
|
|
443
|
+
const swiftui = project.frameworks.includes("swiftui");
|
|
444
|
+
recommendations.push({
|
|
445
|
+
id: `ios-sender:${project.path}`,
|
|
446
|
+
title: `Native full-screen sender in ${project.path} (RemoteDrawSenderKit)`,
|
|
447
|
+
fit: swiftui ? "strong" : "possible",
|
|
448
|
+
receiver: webProjects.length > 0
|
|
449
|
+
? `See web-receiver:${webProjects[0].path}`
|
|
450
|
+
: "Ask: which screen shows the drawing? A web app, a desktop app, or another device?",
|
|
451
|
+
sender: swiftui
|
|
452
|
+
? "`.remoteDrawSurface(isPresented:senderToken:)` — the first-party board presented full screen with an exit; or `RemoteDrawTakeover` inside your own fullScreenCover"
|
|
453
|
+
: "`RemoteDrawTakeover` hosted in a UIHostingController presented full screen (UIKit app); do not build a canvas view",
|
|
454
|
+
keyLocation,
|
|
455
|
+
why: "It is the same board the RemoteDraw app ships: paper, instruments, undo, submit, exit. A custom canvas is a smaller, worse copy that also has to reimplement the wire protocol.",
|
|
456
|
+
commands: [SWIFT_PACKAGE_LINE],
|
|
457
|
+
packages: ["RemoteDrawSenderKit (SwiftPM)"],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (webProjects.length === 0 && iosProjects.length === 0) {
|
|
461
|
+
recommendations.push({
|
|
462
|
+
id: "no-surface-found",
|
|
463
|
+
title: "No web or iOS surface detected",
|
|
464
|
+
fit: "possible",
|
|
465
|
+
receiver: "Ask the user which screen should show the drawing before choosing an SDK.",
|
|
466
|
+
sender: "Hosted /join page by default.",
|
|
467
|
+
keyLocation,
|
|
468
|
+
why: "Nothing in this tree identifies a receiver surface; scaffolding blind produces a demo, not an integration.",
|
|
469
|
+
commands: ["remotedraw options --format json"],
|
|
470
|
+
packages: [],
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return recommendations;
|
|
474
|
+
}
|
|
475
|
+
function productQuestions(input) {
|
|
476
|
+
const questions = [
|
|
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.",
|
|
480
|
+
"Who holds the phone — the same signed-in user (direct sender, no QR), or anyone who scans (QR join)?",
|
|
481
|
+
"What happens to the drawing when the phone taps Submit — saved to which record, shown where, exported how?",
|
|
482
|
+
"Is this a minimal demo to evaluate RemoteDraw, or the real feature? A demo is one page; the feature is the answers above.",
|
|
483
|
+
];
|
|
484
|
+
if (input.iosProjects.length > 0) {
|
|
485
|
+
questions.push("In the iOS app: should drawing take over the whole app (full-screen cover, default) or open as a sheet? Either way the surface fills the presentation; there is no small-canvas option.");
|
|
486
|
+
}
|
|
487
|
+
if (input.webProjects.length === 0 && input.iosProjects.length > 0) {
|
|
488
|
+
questions.push("Which screen shows the drawing? Nothing in this repository looks like a receiver surface.");
|
|
489
|
+
}
|
|
490
|
+
return questions;
|
|
491
|
+
}
|
|
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.",
|
|
495
|
+
"Ask before installing anything (packages, CLI, Swift package) — name what and why, then wait.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
501
|
+
"Timestamps (`occurredAt`, point `t`) are integer milliseconds; the SDKs handle this, hand-written clients often do not.",
|
|
502
|
+
"Session creation is billable and not idempotent: guard React StrictMode double effects, and persist the receiverToken if the receiver outlives a page load.",
|
|
503
|
+
"One API key serves every tenant: scope sessions with externalId and verify it before attaching a sender or ending a session.",
|
|
504
|
+
"Do not delete or replace existing features in the host app; integrate beside them and let the user decide what retires.",
|
|
505
|
+
];
|
|
506
|
+
export function renderScanText(report) {
|
|
507
|
+
const lines = [];
|
|
508
|
+
lines.push("RemoteDraw scan", "");
|
|
509
|
+
lines.push(`Root: ${report.root}`);
|
|
510
|
+
lines.push(`Package manager: ${report.packageManager.id} (${report.packageManager.reason})`, "");
|
|
511
|
+
lines.push("Projects:");
|
|
512
|
+
if (report.projects.length === 0)
|
|
513
|
+
lines.push(" (none detected)");
|
|
514
|
+
for (const project of report.projects) {
|
|
515
|
+
const frameworks = project.frameworks.length > 0 ? ` [${project.frameworks.join(", ")}]` : "";
|
|
516
|
+
lines.push(` ${project.kind.padEnd(8)} ${project.path}${project.name ? ` (${project.name})` : ""}${frameworks}`);
|
|
517
|
+
for (const proof of project.evidence)
|
|
518
|
+
lines.push(` - ${proof}`);
|
|
519
|
+
}
|
|
520
|
+
lines.push("");
|
|
521
|
+
lines.push("Existing RemoteDraw wiring:");
|
|
522
|
+
if (!report.remotedraw.configured) {
|
|
523
|
+
lines.push(" none — this is a fresh integration");
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
if (report.remotedraw.configFiles.length > 0)
|
|
527
|
+
lines.push(` config: ${report.remotedraw.configFiles.join(", ")}`);
|
|
528
|
+
if (report.remotedraw.packages.length > 0)
|
|
529
|
+
lines.push(` packages: ${report.remotedraw.packages.join(", ")}`);
|
|
530
|
+
if (report.remotedraw.swiftPackage)
|
|
531
|
+
lines.push(" swift: RemoteDrawSenderKit is already a dependency");
|
|
532
|
+
if (report.remotedraw.envKeys.length > 0)
|
|
533
|
+
lines.push(` env keys: ${report.remotedraw.envKeys.join(", ")}`);
|
|
534
|
+
}
|
|
535
|
+
lines.push("");
|
|
536
|
+
lines.push("Where the API key may live:");
|
|
537
|
+
if (report.keyBoundary.serverSide.length === 0) {
|
|
538
|
+
lines.push(` ${report.keyBoundary.warning}`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
for (const location of report.keyBoundary.serverSide)
|
|
542
|
+
lines.push(` ${location}`);
|
|
543
|
+
}
|
|
544
|
+
lines.push("");
|
|
545
|
+
lines.push("Integration options:");
|
|
546
|
+
for (const recommendation of report.recommendations) {
|
|
547
|
+
lines.push(` [${recommendation.fit}] ${recommendation.title}`);
|
|
548
|
+
lines.push(` receiver: ${recommendation.receiver}`);
|
|
549
|
+
lines.push(` sender: ${recommendation.sender}`);
|
|
550
|
+
lines.push(` key: ${recommendation.keyLocation}`);
|
|
551
|
+
lines.push(` why: ${recommendation.why}`);
|
|
552
|
+
for (const command of recommendation.commands)
|
|
553
|
+
lines.push(` next: ${command}`);
|
|
554
|
+
}
|
|
555
|
+
lines.push("");
|
|
556
|
+
lines.push("Ask the user before building:");
|
|
557
|
+
report.questions.forEach((question, index) => lines.push(` ${index + 1}. ${question}`));
|
|
558
|
+
lines.push("");
|
|
559
|
+
lines.push("Rules for the implementation:");
|
|
560
|
+
for (const rule of report.rules)
|
|
561
|
+
lines.push(` - ${rule}`);
|
|
562
|
+
return lines.join("\n");
|
|
563
|
+
}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type TelemetryEnvironment = {
|
|
|
16
16
|
platform: string;
|
|
17
17
|
nodeVersion: string;
|
|
18
18
|
/** Injected by the test suite so no suite run can reach the network. */
|
|
19
|
-
fetch?:
|
|
19
|
+
fetch?: ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) | undefined;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* Off whenever the user says so, and off in CI, where a crash report has no
|
package/dist/telemetry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,CAAC,EACF,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAC1E,SAAS,CAAC;CACf,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,WAOxE;AAMD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM,UAW7C;AAmBD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsClC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,oBAAoB,iBA+BlC;AAED,gEAAgE;AAChE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,GACd,oBAAoB,CAOtB"}
|
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:
|
|
16
|
-
/**
|
|
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
|
|
33
|
-
*
|
|
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>;
|
package/dist/update.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|