@remotedraw/cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/scan.js ADDED
@@ -0,0 +1,544 @@
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
+ const SKIPPED_DIRECTORIES = new Set([
18
+ "node_modules",
19
+ ".git",
20
+ "dist",
21
+ "build",
22
+ "out",
23
+ ".next",
24
+ ".nuxt",
25
+ ".svelte-kit",
26
+ ".output",
27
+ ".turbo",
28
+ ".cache",
29
+ "coverage",
30
+ "DerivedData",
31
+ "Pods",
32
+ "Carthage",
33
+ ".build",
34
+ ".swiftpm",
35
+ "vendor",
36
+ "target",
37
+ ".venv",
38
+ "venv",
39
+ "__pycache__",
40
+ ".agent-worktrees",
41
+ ".claude",
42
+ ".sandbox",
43
+ ]);
44
+ const MAX_DEPTH = 4;
45
+ const MAX_DIRECTORIES = 4000;
46
+ const SWIFT_SAMPLE_LIMIT = 40;
47
+ async function listDirectory(dirPath, runtime) {
48
+ try {
49
+ if (runtime.readdir)
50
+ return await runtime.readdir(dirPath);
51
+ return await readdir(dirPath);
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ }
57
+ async function isDirectory(filePath) {
58
+ try {
59
+ return (await stat(filePath)).isDirectory();
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ async function readText(filePath, runtime) {
66
+ try {
67
+ return await runtime.readFile(filePath);
68
+ }
69
+ catch {
70
+ try {
71
+ return await readFile(filePath, "utf8");
72
+ }
73
+ catch {
74
+ return "";
75
+ }
76
+ }
77
+ }
78
+ async function readManifest(filePath, runtime) {
79
+ const text = await readText(filePath, runtime);
80
+ if (!text)
81
+ return null;
82
+ try {
83
+ return JSON.parse(text);
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /** Breadth-first walk, bounded, skipping build output and dependencies. */
90
+ async function walk(root, runtime) {
91
+ const queue = [{ dir: root, depth: 0 }];
92
+ const visited = [];
93
+ while (queue.length > 0 && visited.length < MAX_DIRECTORIES) {
94
+ const next = queue.shift();
95
+ const entries = await listDirectory(next.dir, runtime);
96
+ const relative = path.relative(root, next.dir) || ".";
97
+ visited.push({ dir: next.dir, relative, entries, depth: next.depth });
98
+ if (next.depth >= MAX_DEPTH)
99
+ continue;
100
+ for (const entry of entries) {
101
+ if (entry.startsWith("._"))
102
+ continue;
103
+ if (SKIPPED_DIRECTORIES.has(entry))
104
+ continue;
105
+ // Xcode projects and bundles are directories a walker has no business in.
106
+ if (/\.(xcodeproj|xcworkspace|xcassets|app|framework|bundle|playground)$/.test(entry)) {
107
+ continue;
108
+ }
109
+ const child = path.join(next.dir, entry);
110
+ if (await isDirectory(child))
111
+ queue.push({ dir: child, depth: next.depth + 1 });
112
+ }
113
+ }
114
+ return visited;
115
+ }
116
+ function allDependencies(manifest) {
117
+ return {
118
+ ...manifest.peerDependencies,
119
+ ...manifest.devDependencies,
120
+ ...manifest.dependencies,
121
+ };
122
+ }
123
+ const WEB_FRAMEWORKS = [
124
+ ["next", "next"],
125
+ ["react", "react"],
126
+ ["react-dom", "react-dom"],
127
+ ["@sveltejs/kit", "sveltekit"],
128
+ ["svelte", "svelte"],
129
+ ["vue", "vue"],
130
+ ["nuxt", "nuxt"],
131
+ ["@angular/core", "angular"],
132
+ ["solid-js", "solid"],
133
+ ["@remix-run/react", "remix"],
134
+ ["react-router", "react-router"],
135
+ ["react-router-dom", "react-router"],
136
+ ["vite", "vite"],
137
+ ["@tanstack/react-router", "tanstack-router"],
138
+ ["expo", "expo"],
139
+ ["react-native", "react-native"],
140
+ ["electron", "electron"],
141
+ ["@tauri-apps/api", "tauri"],
142
+ ];
143
+ const SERVER_FRAMEWORKS = [
144
+ ["convex", "convex"],
145
+ ["express", "express"],
146
+ ["fastify", "fastify"],
147
+ ["hono", "hono"],
148
+ ["koa", "koa"],
149
+ ["@nestjs/core", "nestjs"],
150
+ ["@trpc/server", "trpc"],
151
+ ["@vercel/node", "vercel-functions"],
152
+ ["firebase-functions", "firebase-functions"],
153
+ ["@supabase/supabase-js", "supabase"],
154
+ ["@aws-sdk/client-lambda", "aws-lambda"],
155
+ ["aws-lambda", "aws-lambda"],
156
+ ["next", "next-route-handlers"],
157
+ ["@sveltejs/kit", "sveltekit-endpoints"],
158
+ ["nuxt", "nuxt-server"],
159
+ ["@remix-run/node", "remix-loaders"],
160
+ ];
161
+ const SERVER_DIRECTORIES = ["server", "api", "backend", "functions", "convex", "worker", "workers", "lambda", "netlify/functions", "supabase/functions"];
162
+ function classifyManifest(manifest, entries, relative) {
163
+ const deps = allDependencies(manifest);
164
+ const frameworks = [];
165
+ const evidence = [];
166
+ const serverSide = [];
167
+ const add = (label, proof) => {
168
+ if (!frameworks.includes(label))
169
+ frameworks.push(label);
170
+ evidence.push(proof);
171
+ };
172
+ let web = false;
173
+ for (const [dependency, label] of WEB_FRAMEWORKS) {
174
+ if (deps[dependency]) {
175
+ add(label, `${relative}/package.json depends on ${dependency}`);
176
+ if (!["electron", "tauri", "expo", "react-native"].includes(label))
177
+ web = true;
178
+ }
179
+ }
180
+ let server = false;
181
+ for (const [dependency, label] of SERVER_FRAMEWORKS) {
182
+ if (deps[dependency]) {
183
+ add(label, `${relative}/package.json depends on ${dependency}`);
184
+ server = true;
185
+ serverSide.push(label);
186
+ }
187
+ }
188
+ for (const directory of SERVER_DIRECTORIES) {
189
+ const [head] = directory.split("/");
190
+ if (head && entries.includes(head)) {
191
+ const proof = `${relative}/${directory}/ exists`;
192
+ if (!evidence.includes(proof))
193
+ evidence.push(proof);
194
+ server = true;
195
+ if (!serverSide.includes(directory))
196
+ serverSide.push(directory);
197
+ }
198
+ }
199
+ if (entries.includes("app") && deps.next) {
200
+ serverSide.push("app/**/route.ts (Next.js route handlers)");
201
+ }
202
+ if (entries.includes("pages") && deps.next) {
203
+ serverSide.push("pages/api (Next.js API routes)");
204
+ }
205
+ const kind = web
206
+ ? "web"
207
+ : server
208
+ ? "server"
209
+ : frameworks.length > 0
210
+ ? "web"
211
+ : manifest.private === false || entries.includes("src")
212
+ ? "library"
213
+ : "unknown";
214
+ return { kind, frameworks, evidence, serverSide };
215
+ }
216
+ async function detectSwiftUI(dir, entries, runtime) {
217
+ const swiftFiles = entries.filter((entry) => entry.endsWith(".swift")).slice(0, SWIFT_SAMPLE_LIMIT);
218
+ let swiftui = false;
219
+ let uikit = false;
220
+ for (const file of swiftFiles) {
221
+ const text = await readText(path.join(dir, file), runtime);
222
+ const head = text.slice(0, 4000);
223
+ if (/^\s*import SwiftUI/m.test(head))
224
+ swiftui = true;
225
+ if (/^\s*import UIKit/m.test(head))
226
+ uikit = true;
227
+ if (swiftui && uikit)
228
+ break;
229
+ }
230
+ return { swiftui, uikit, sampled: swiftFiles.length };
231
+ }
232
+ export async function scanProject(root, runtime) {
233
+ const visited = await walk(root, runtime);
234
+ const projects = [];
235
+ const configFiles = [];
236
+ const remotedrawPackages = new Set();
237
+ const envKeys = new Set();
238
+ let swiftPackage = false;
239
+ // iOS apps announce themselves by an .xcodeproj / .xcworkspace / project.yml
240
+ // beside their sources; Swift files under that directory tell us the UI
241
+ // framework. A directory holding both is one project, not two.
242
+ const iosRoots = new Map();
243
+ for (const entry of visited) {
244
+ const relative = entry.relative;
245
+ if (entry.entries.includes("remotedraw.config.json")) {
246
+ configFiles.push(relative === "." ? "remotedraw.config.json" : `${relative}/remotedraw.config.json`);
247
+ }
248
+ for (const envFile of entry.entries.filter((name) => /^\.env(\..+)?$/.test(name))) {
249
+ const text = await readText(path.join(entry.dir, envFile), runtime);
250
+ for (const match of text.matchAll(/^\s*(REMOTEDRAW_[A-Z0-9_]+)\s*=/gm)) {
251
+ envKeys.add(match[1]);
252
+ }
253
+ }
254
+ if (entry.entries.includes("package.json")) {
255
+ const manifest = await readManifest(path.join(entry.dir, "package.json"), runtime);
256
+ if (manifest) {
257
+ const deps = allDependencies(manifest);
258
+ for (const name of Object.keys(deps)) {
259
+ if (name.startsWith("@remotedraw/"))
260
+ remotedrawPackages.add(name);
261
+ }
262
+ const classified = classifyManifest(manifest, entry.entries, relative);
263
+ const isWorkspaceRoot = manifest.workspaces != null && classified.frameworks.length === 0;
264
+ if (!isWorkspaceRoot) {
265
+ projects.push({
266
+ path: relative,
267
+ ...(manifest.name ? { name: manifest.name } : {}),
268
+ kind: classified.kind,
269
+ frameworks: classified.frameworks,
270
+ evidence: classified.evidence,
271
+ ...(classified.serverSide.length > 0 ? { serverSide: classified.serverSide } : {}),
272
+ });
273
+ }
274
+ }
275
+ }
276
+ const xcode = entry.entries.filter((name) => !name.startsWith("._") && /\.(xcodeproj|xcworkspace)$/.test(name));
277
+ const hasProjectYml = entry.entries.includes("project.yml");
278
+ const hasPackageSwift = entry.entries.includes("Package.swift");
279
+ if (hasPackageSwift) {
280
+ const text = await readText(path.join(entry.dir, "Package.swift"), runtime);
281
+ if (text.includes("remotedraw-swift") || text.includes("RemoteDrawSenderKit"))
282
+ swiftPackage = true;
283
+ }
284
+ if (xcode.length > 0 || hasProjectYml) {
285
+ const project = {
286
+ path: relative,
287
+ kind: "ios",
288
+ frameworks: [],
289
+ evidence: xcode.map((name) => `${relative}/${name}`),
290
+ };
291
+ if (hasProjectYml)
292
+ project.evidence.push(`${relative}/project.yml (xcodegen)`);
293
+ for (const name of xcode) {
294
+ const pbxproj = path.join(entry.dir, name, "project.pbxproj");
295
+ const text = await readText(pbxproj, runtime);
296
+ if (text.includes("remotedraw-swift") || text.includes("RemoteDrawSenderKit"))
297
+ swiftPackage = true;
298
+ if (text.includes("SwiftUI"))
299
+ project.evidence.push(`${relative}/${name} references SwiftUI`);
300
+ }
301
+ const resolved = path.join(entry.dir, "Package.resolved");
302
+ if (entry.entries.includes("Package.resolved")) {
303
+ const text = await readText(resolved, runtime);
304
+ if (text.includes("remotedraw-swift"))
305
+ swiftPackage = true;
306
+ }
307
+ iosRoots.set(relative, project);
308
+ projects.push(project);
309
+ }
310
+ }
311
+ // Second pass: UI framework per iOS root, from Swift files at or below it.
312
+ for (const [iosRelative, project] of iosRoots) {
313
+ let swiftui = false;
314
+ let uikit = false;
315
+ let sampled = 0;
316
+ for (const entry of visited) {
317
+ const inside = iosRelative === "." ||
318
+ entry.relative === iosRelative ||
319
+ entry.relative.startsWith(`${iosRelative}${path.sep}`) ||
320
+ entry.relative.startsWith(`${iosRelative}/`);
321
+ if (!inside || sampled >= SWIFT_SAMPLE_LIMIT)
322
+ continue;
323
+ const result = await detectSwiftUI(entry.dir, entry.entries, runtime);
324
+ sampled += result.sampled;
325
+ swiftui ||= result.swiftui;
326
+ uikit ||= result.uikit;
327
+ }
328
+ if (swiftui) {
329
+ project.frameworks.push("swiftui");
330
+ project.evidence.push("Swift sources import SwiftUI");
331
+ }
332
+ if (uikit) {
333
+ project.frameworks.push("uikit");
334
+ project.evidence.push("Swift sources import UIKit");
335
+ }
336
+ if (!swiftui && !uikit && sampled === 0) {
337
+ project.evidence.push("no Swift sources sampled under this directory");
338
+ }
339
+ }
340
+ const webProjects = projects.filter((project) => project.kind === "web");
341
+ const serverProjects = projects.filter((project) => project.kind === "server");
342
+ const iosProjects = projects.filter((project) => project.kind === "ios");
343
+ const serverSide = [
344
+ ...serverProjects.map((project) => project.path),
345
+ ...webProjects.flatMap((project) => (project.serverSide ?? []).map((location) => project.path === "." ? location : `${project.path} (${location})`)),
346
+ ];
347
+ const recommendations = buildRecommendations({
348
+ webProjects,
349
+ iosProjects,
350
+ serverSide,
351
+ swiftPackage,
352
+ });
353
+ const keyBoundary = { serverSide };
354
+ if (serverSide.length === 0) {
355
+ keyBoundary.warning =
356
+ "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
+ return {
359
+ ok: true,
360
+ command: "scan",
361
+ schemaVersion: 1,
362
+ root,
363
+ projects,
364
+ remotedraw: {
365
+ configured: configFiles.length > 0 || remotedrawPackages.size > 0 || swiftPackage,
366
+ configFiles,
367
+ packages: [...remotedrawPackages].sort(),
368
+ envKeys: [...envKeys].sort(),
369
+ swiftPackage,
370
+ },
371
+ keyBoundary,
372
+ recommendations,
373
+ questions: productQuestions({ webProjects, iosProjects }),
374
+ rules: INTEGRATION_RULES,
375
+ };
376
+ }
377
+ const SWIFT_PACKAGE_LINE = 'Add https://github.com/AxioSOzo/remotedraw-swift.git (from: "0.1.0"), product RemoteDrawSenderKit';
378
+ function buildRecommendations(input) {
379
+ const { webProjects, iosProjects, serverSide } = input;
380
+ const recommendations = [];
381
+ const keyLocation = serverSide.length > 0
382
+ ? `Session creation (rd_sk_) belongs in: ${serverSide.join(", ")}`
383
+ : "No server-side location found — ask the user; do not scaffold createRemoteDrawSession into client source.";
384
+ const receiverFor = (project) => {
385
+ const frameworks = project.frameworks;
386
+ if (frameworks.includes("svelte") || frameworks.includes("sveltekit")) {
387
+ return {
388
+ receiver: `@remotedraw/svelte receiver store in ${project.path}`,
389
+ packages: ["@remotedraw/svelte"],
390
+ sdk: "svelte",
391
+ };
392
+ }
393
+ if (frameworks.includes("react") || frameworks.includes("next") || frameworks.includes("remix")) {
394
+ return {
395
+ receiver: `@remotedraw/react — RemoteDrawProvider + RemoteDrawReceiver + PairingCode in ${project.path}`,
396
+ packages: ["@remotedraw/react"],
397
+ sdk: "react",
398
+ };
399
+ }
400
+ return {
401
+ receiver: `@remotedraw/client — createHttpReceiverClient + createRealtimeReceiverSource in ${project.path} (${frameworks.join(", ") || "no framework detected"})`,
402
+ packages: ["@remotedraw/client"],
403
+ sdk: "js",
404
+ };
405
+ };
406
+ for (const project of webProjects) {
407
+ const { receiver, packages, sdk } = receiverFor(project);
408
+ const hasIos = iosProjects.length > 0;
409
+ recommendations.push({
410
+ id: `web-receiver:${project.path}`,
411
+ title: hasIos
412
+ ? `Receiver in ${project.path}, sender in the existing iOS app`
413
+ : `Receiver in ${project.path}, hosted phone sender (no phone code)`,
414
+ fit: "strong",
415
+ receiver,
416
+ sender: hasIos
417
+ ? `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`
418
+ : "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.",
419
+ keyLocation,
420
+ why: hasIos
421
+ ? "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."
422
+ : "The sender is not where the product adds value; the hosted pad is polished, full screen, and costs no code.",
423
+ commands: [
424
+ `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`,
425
+ ],
426
+ packages,
427
+ });
428
+ }
429
+ for (const project of iosProjects) {
430
+ const swiftui = project.frameworks.includes("swiftui");
431
+ recommendations.push({
432
+ id: `ios-sender:${project.path}`,
433
+ title: `Native full-screen sender in ${project.path} (RemoteDrawSenderKit)`,
434
+ fit: swiftui ? "strong" : "possible",
435
+ receiver: webProjects.length > 0
436
+ ? `See web-receiver:${webProjects[0].path}`
437
+ : "Ask: which screen shows the drawing? A web app, a desktop app, or another device?",
438
+ sender: swiftui
439
+ ? "`.remoteDrawSurface(isPresented:senderToken:)` — the first-party board presented full screen with an exit; or `RemoteDrawTakeover` inside your own fullScreenCover"
440
+ : "`RemoteDrawTakeover` hosted in a UIHostingController presented full screen (UIKit app); do not build a canvas view",
441
+ keyLocation,
442
+ 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.",
443
+ commands: [SWIFT_PACKAGE_LINE],
444
+ packages: ["RemoteDrawSenderKit (SwiftPM)"],
445
+ });
446
+ }
447
+ if (webProjects.length === 0 && iosProjects.length === 0) {
448
+ recommendations.push({
449
+ id: "no-surface-found",
450
+ title: "No web or iOS surface detected",
451
+ fit: "possible",
452
+ receiver: "Ask the user which screen should show the drawing before choosing an SDK.",
453
+ sender: "Hosted /join page by default.",
454
+ keyLocation,
455
+ why: "Nothing in this tree identifies a receiver surface; scaffolding blind produces a demo, not an integration.",
456
+ commands: ["remotedraw options --format json"],
457
+ packages: [],
458
+ });
459
+ }
460
+ return recommendations;
461
+ }
462
+ function productQuestions(input) {
463
+ 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.",
466
+ "Who holds the phone — the same signed-in user (direct sender, no QR), or anyone who scans (QR join)?",
467
+ "What happens to the drawing when the phone taps Submit — saved to which record, shown where, exported how?",
468
+ "Is this a minimal demo to evaluate RemoteDraw, or the real feature? A demo is one page; the feature is the answers above.",
469
+ ];
470
+ if (input.iosProjects.length > 0) {
471
+ 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.");
472
+ }
473
+ if (input.webProjects.length === 0 && input.iosProjects.length > 0) {
474
+ questions.push("Which screen shows the drawing? Nothing in this repository looks like a receiver surface.");
475
+ }
476
+ return questions;
477
+ }
478
+ export const INTEGRATION_RULES = [
479
+ "Ask before installing anything (packages, CLI, Swift package) — name what and why, then wait.",
480
+ "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
+ "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
+ "Web senders use the hosted /join page unless the user explicitly wants an in-page sender.",
483
+ "Timestamps (`occurredAt`, point `t`) are integer milliseconds; the SDKs handle this, hand-written clients often do not.",
484
+ "Session creation is billable and not idempotent: guard React StrictMode double effects, and persist the receiverToken if the receiver outlives a page load.",
485
+ "One API key serves every tenant: scope sessions with externalId and verify it before attaching a sender or ending a session.",
486
+ "Do not delete or replace existing features in the host app; integrate beside them and let the user decide what retires.",
487
+ ];
488
+ export function renderScanText(report) {
489
+ const lines = [];
490
+ lines.push("RemoteDraw scan", "");
491
+ lines.push(`Root: ${report.root}`, "");
492
+ lines.push("Projects:");
493
+ if (report.projects.length === 0)
494
+ lines.push(" (none detected)");
495
+ for (const project of report.projects) {
496
+ const frameworks = project.frameworks.length > 0 ? ` [${project.frameworks.join(", ")}]` : "";
497
+ lines.push(` ${project.kind.padEnd(8)} ${project.path}${project.name ? ` (${project.name})` : ""}${frameworks}`);
498
+ for (const proof of project.evidence)
499
+ lines.push(` - ${proof}`);
500
+ }
501
+ lines.push("");
502
+ lines.push("Existing RemoteDraw wiring:");
503
+ if (!report.remotedraw.configured) {
504
+ lines.push(" none — this is a fresh integration");
505
+ }
506
+ else {
507
+ if (report.remotedraw.configFiles.length > 0)
508
+ lines.push(` config: ${report.remotedraw.configFiles.join(", ")}`);
509
+ if (report.remotedraw.packages.length > 0)
510
+ lines.push(` packages: ${report.remotedraw.packages.join(", ")}`);
511
+ if (report.remotedraw.swiftPackage)
512
+ lines.push(" swift: RemoteDrawSenderKit is already a dependency");
513
+ if (report.remotedraw.envKeys.length > 0)
514
+ lines.push(` env keys: ${report.remotedraw.envKeys.join(", ")}`);
515
+ }
516
+ lines.push("");
517
+ lines.push("Where the API key may live:");
518
+ if (report.keyBoundary.serverSide.length === 0) {
519
+ lines.push(` ${report.keyBoundary.warning}`);
520
+ }
521
+ else {
522
+ for (const location of report.keyBoundary.serverSide)
523
+ lines.push(` ${location}`);
524
+ }
525
+ lines.push("");
526
+ lines.push("Integration options:");
527
+ for (const recommendation of report.recommendations) {
528
+ lines.push(` [${recommendation.fit}] ${recommendation.title}`);
529
+ lines.push(` receiver: ${recommendation.receiver}`);
530
+ lines.push(` sender: ${recommendation.sender}`);
531
+ lines.push(` key: ${recommendation.keyLocation}`);
532
+ lines.push(` why: ${recommendation.why}`);
533
+ for (const command of recommendation.commands)
534
+ lines.push(` next: ${command}`);
535
+ }
536
+ lines.push("");
537
+ lines.push("Ask the user before building:");
538
+ report.questions.forEach((question, index) => lines.push(` ${index + 1}. ${question}`));
539
+ lines.push("");
540
+ lines.push("Rules for the implementation:");
541
+ for (const rule of report.rules)
542
+ lines.push(` - ${rule}`);
543
+ return lines.join("\n");
544
+ }
@@ -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?: typeof fetch | undefined;
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
@@ -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,GAAG,SAAS,CAAC;CAClC,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"}
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotedraw/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Command-line tools for creating and inspecting RemoteDraw integrations.",
5
5
  "type": "module",
6
6
  "bin": {