@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/dist/cli.js CHANGED
@@ -5,11 +5,13 @@ import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { emitKeypressEvents } from "node:readline";
8
+ import { renderScanText, scanProject } from "./scan.js";
8
9
  import { createInterface } from "node:readline/promises";
9
10
  import { createWizardTerminal, runSetupWizard, } from "./setup-wizard.js";
10
- import { assertEnvCanAcceptProvisioning, currentCliAccount, DEFAULT_REMOTEDRAW_API_BASE_URL, globalConfigPath, loginToRemoteDraw, logoutFromRemoteDraw, provisionRemoteDrawProject, readStoredLanguage, remoteDrawApiBaseUrl, writeStoredLanguage, writeProvisionedProjectSetup, } from "./cloud.js";
11
+ import { assertEnvCanAcceptProvisioning, currentCliAccount, DEFAULT_REMOTEDRAW_API_BASE_URL, globalConfigPath, loginToRemoteDraw, logoutFromRemoteDraw, provisionRemoteDrawProject, updateEnvContents, readStoredLanguage, remoteDrawApiBaseUrl, writeStoredLanguage, writeProvisionedProjectSetup, } from "./cloud.js";
11
12
  import { LANGUAGE_ENV_VARS, SOURCE_LOCALE, extractLanguageFlag, languageChoices, languagePromptMessage, localeEndonym, normalizeLocale, resolveLocale, setActiveLocale, supportedLocaleList, SUPPORTED_LOCALES, systemLocaleHint, t, } from "./i18n.js";
12
13
  import { CliError, cliError } from "./errors.js";
14
+ import { detectPackageManager, packageManagerCommands, packageManagerIds, } from "./packageManagers.js";
13
15
  import { AGENT_SKILL_MARKDOWN } from "./generated/agent-skill.js";
14
16
  import { reportCliCrash } from "./telemetry.js";
15
17
  import { checkForUpdate, CLI_PACKAGE_NAME, detectInstallMethod, installMethodDisplay, runInstall, } from "./update.js";
@@ -23,8 +25,8 @@ export const CLI_VERSION = typeof packageMetadata.version === "string"
23
25
  // private, and would silently accept a future breaking major once they were
24
26
  // not. tests/generated-manifest.test.ts cross-checks every range below against
25
27
  // the workspace manifests.
26
- export const REMOTEDRAW_SDK_RANGE = "^0.2.0";
27
- export const REMOTEDRAW_CLI_RANGE = "^0.2.0";
28
+ export const REMOTEDRAW_SDK_RANGE = "^0.3.0";
29
+ export const REMOTEDRAW_CLI_RANGE = "^0.3.0";
28
30
  export const targetChoices = ["web", "desktop", "ios", "headless"];
29
31
  export const senderChoices = [
30
32
  "remotedraw-ios",
@@ -55,7 +57,7 @@ const exampleChoices = [
55
57
  "raw-http",
56
58
  "ios-owned-sender",
57
59
  ];
58
- const packageManagerChoices = ["npm", "pnpm", "yarn", "bun"];
60
+ const packageManagerChoices = packageManagerIds;
59
61
  export function createNodeRuntime() {
60
62
  const cwd = process.cwd();
61
63
  const env = cliEnvironmentForWorkspace(cwd, process.env);
@@ -355,6 +357,10 @@ export async function runCli(args, runtime = createNodeRuntime()) {
355
357
  return await examplesCommand(commandArgs, runtime);
356
358
  case "agent":
357
359
  return await agentCommand(commandArgs, runtime);
360
+ case "scan":
361
+ return await scanCommand(commandArgs, runtime);
362
+ case "project":
363
+ return await projectCommand(commandArgs, runtime);
358
364
  default:
359
365
  return requestedJsonOutput(args)
360
366
  ? jsonFailure(command, new Error(t("error.unknownCommand", { command })))
@@ -392,7 +398,9 @@ async function setupWizardCommand(runtime, askLanguage) {
392
398
  await loginToRemoteDraw(runtime, apiBaseUrl);
393
399
  }
394
400
  const result = await runSetupWizard({
395
- definition: projectSetupDefinition(),
401
+ definition: projectSetupDefinition({
402
+ packageManager: await projectPackageManager(runtime.cwd, runtime),
403
+ }),
396
404
  terminal,
397
405
  execute: async (values) => {
398
406
  const plan = planFromWizardValues(values);
@@ -603,6 +611,8 @@ function mainHelpText() {
603
611
  command("create-input", t("help.command.createInput")),
604
612
  command("examples", t("help.command.examples")),
605
613
  command("agent", t("help.command.agent")),
614
+ command("scan", t("help.command.scan")),
615
+ command("project", t("help.command.project")),
606
616
  "",
607
617
  t("help.startHere"),
608
618
  " remotedraw guide",
@@ -693,15 +703,33 @@ async function logoutCommand(args, runtime) {
693
703
  }
694
704
  async function whoamiCommand(args, runtime) {
695
705
  const parsed = parseArgs(args, {
696
- values: ["api-base-url"],
706
+ values: ["api-base-url", "format"],
697
707
  booleans: ["help"],
698
708
  });
699
709
  if (parsed.booleans.has("help"))
700
710
  return ok(whoamiHelpText());
711
+ const format = outputFormat(parsed);
701
712
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
702
713
  const account = await currentCliAccount(runtime, apiBaseUrl);
703
714
  if (!account) {
704
- return fail(t("whoami.notLoggedIn", { url: apiBaseUrl }));
715
+ const message = t("whoami.notLoggedIn", { url: apiBaseUrl });
716
+ return format === "json"
717
+ ? jsonFailure("whoami", cliError("AUTH_REQUIRED", "whoami.notLoggedIn", { url: apiBaseUrl }))
718
+ : fail(message);
719
+ }
720
+ if (format === "json") {
721
+ return jsonResult({
722
+ ok: true,
723
+ command: "whoami",
724
+ apiBaseUrl,
725
+ account: {
726
+ userId: account.userId,
727
+ tenantId: account.tenantId,
728
+ tenantSlug: account.tenantSlug,
729
+ ...(account.email ? { email: account.email } : {}),
730
+ ...(account.name ? { name: account.name } : {}),
731
+ },
732
+ });
705
733
  }
706
734
  return ok([accountSummary(account), t("whoami.api", { url: apiBaseUrl })].join("\n"));
707
735
  }
@@ -738,6 +766,9 @@ async function newCommand(args, runtime, askLanguage = false) {
738
766
  : planFromArgs(parsed, {
739
767
  appNameRequired: true,
740
768
  defaultAppName: "RemoteDraw app",
769
+ defaults: {
770
+ packageManager: await projectPackageManager(runtime.cwd, runtime),
771
+ },
741
772
  });
742
773
  const outputDir = resolveOutputDir(runtime.cwd, parsed, plan.slug);
743
774
  const dryRun = parsed.booleans.has("dry-run");
@@ -791,6 +822,9 @@ async function initCommand(args, runtime) {
791
822
  const plan = planFromArgs(parsed, {
792
823
  appNameRequired: false,
793
824
  defaultAppName,
825
+ defaults: {
826
+ packageManager: await projectPackageManager(outputDir, runtime),
827
+ },
794
828
  });
795
829
  const dryRun = parsed.booleans.has("dry-run");
796
830
  const force = parsed.booleans.has("force");
@@ -933,7 +967,7 @@ async function updateCommand(args, runtime) {
933
967
  return ok(updateHelpText());
934
968
  const format = outputFormat(parsed);
935
969
  const checkOnly = parsed.booleans.has("check");
936
- const method = detectInstallMethod(runtime.binPath ?? "");
970
+ const method = detectInstallMethod(runtime.binPath ?? "", runtime.env);
937
971
  const command = installMethodDisplay(method);
938
972
  // `remotedraw update` is an explicit request, so it ignores the cache and the
939
973
  // ambient opt-outs that only silence the passive notice.
@@ -1243,7 +1277,10 @@ async function examplesCommand(args, runtime) {
1243
1277
  const plan = planFromArgs(parsed, {
1244
1278
  appNameRequired: false,
1245
1279
  defaultAppName: readString(parsed, "app-name") ?? titleCase(installedExample),
1246
- defaults,
1280
+ defaults: {
1281
+ packageManager: await projectPackageManager(runtime.cwd, runtime),
1282
+ ...defaults,
1283
+ },
1247
1284
  });
1248
1285
  const files = newProjectFiles(plan);
1249
1286
  const dryRun = parsed.booleans.has("dry-run");
@@ -1253,6 +1290,125 @@ async function examplesCommand(args, runtime) {
1253
1290
  });
1254
1291
  return ok(scaffoldSummary(t("scaffold.verb.installedExample"), plan, outputDir, written, dryRun));
1255
1292
  }
1293
+ async function projectCommand(args, runtime) {
1294
+ const parsed = parseArgs(args, {
1295
+ values: ["name", "key-name", "api-base-url", "env", "format"],
1296
+ booleans: ["help", "force"],
1297
+ });
1298
+ const subcommand = parsed.positionals[0];
1299
+ if (parsed.booleans.has("help") || subcommand == null) {
1300
+ return ok(projectHelpText());
1301
+ }
1302
+ if (subcommand !== "create") {
1303
+ throw cliError("INVALID_ARGUMENT", "project.error.unknownSubcommand", {
1304
+ subcommand,
1305
+ });
1306
+ }
1307
+ const format = outputFormat(parsed);
1308
+ const name = readString(parsed, "name");
1309
+ if (name == null) {
1310
+ throw cliError("MISSING_ARGUMENT", "project.error.nameRequired");
1311
+ }
1312
+ const apiBaseUrl = readString(parsed, "api-base-url") ??
1313
+ runtime.env.REMOTEDRAW_API_BASE_URL ??
1314
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
1315
+ const keyName = readString(parsed, "key-name");
1316
+ // Provisioning is separate from scaffolding on purpose: an integration that
1317
+ // already has its files must not have to --force-overwrite them to get a key.
1318
+ const provisioning = await provisionRemoteDrawProject(runtime, apiBaseUrl, {
1319
+ name,
1320
+ ...(keyName ? { keyName } : {}),
1321
+ });
1322
+ const envDir = readString(parsed, "env");
1323
+ let envPath;
1324
+ if (envDir != null) {
1325
+ const outputDir = path.resolve(runtime.cwd, envDir);
1326
+ const force = parsed.booleans.has("force");
1327
+ await assertEnvCanAcceptProvisioning(runtime, outputDir, apiBaseUrl, force);
1328
+ await runtime.mkdir(outputDir);
1329
+ envPath = path.join(outputDir, ".env.local");
1330
+ const existing = (await runtime.exists(envPath))
1331
+ ? await runtime.readFile(envPath)
1332
+ : "# Generated by RemoteDraw CLI. Server-side secrets only.\n";
1333
+ const next = updateEnvContents(existing, {
1334
+ REMOTEDRAW_API_BASE_URL: provisioning.apiBaseUrl,
1335
+ REMOTEDRAW_PROJECT_ID: provisioning.project.id,
1336
+ REMOTEDRAW_API_KEY: provisioning.apiKey.value,
1337
+ }, force);
1338
+ if (runtime.writePrivateFile) {
1339
+ await runtime.writePrivateFile(envPath, next);
1340
+ }
1341
+ else {
1342
+ await runtime.writeFile(envPath, next);
1343
+ }
1344
+ }
1345
+ if (format === "json") {
1346
+ return jsonResult({
1347
+ ok: true,
1348
+ command: "project",
1349
+ subcommand: "create",
1350
+ project: provisioning.project,
1351
+ apiKey: {
1352
+ id: provisioning.apiKey.id,
1353
+ name: provisioning.apiKey.name,
1354
+ // Shown exactly once, here. Written to .env.local only with --env.
1355
+ ...(envPath ? {} : { value: provisioning.apiKey.value }),
1356
+ },
1357
+ apiBaseUrl: provisioning.apiBaseUrl,
1358
+ ...(envPath ? { envPath } : {}),
1359
+ });
1360
+ }
1361
+ return ok([
1362
+ t("project.created", {
1363
+ name: provisioning.project.name,
1364
+ id: provisioning.project.id,
1365
+ }),
1366
+ ` ${provisioning.project.dashboardUrl}`,
1367
+ "",
1368
+ envPath
1369
+ ? t("project.keyWritten", { path: envPath })
1370
+ : t("project.keyOnce", { key: provisioning.apiKey.value }),
1371
+ "",
1372
+ t("project.keyRule"),
1373
+ ].join("\n"));
1374
+ }
1375
+ function projectHelpText() {
1376
+ return [
1377
+ t("word.usage"),
1378
+ t("project.help.usage"),
1379
+ "",
1380
+ t("project.help.body"),
1381
+ t("project.help.env"),
1382
+ t("project.help.auth"),
1383
+ ].join("\n");
1384
+ }
1385
+ async function scanCommand(args, runtime) {
1386
+ const parsed = parseArgs(args, {
1387
+ values: ["path", "format"],
1388
+ booleans: ["help"],
1389
+ });
1390
+ if (parsed.booleans.has("help"))
1391
+ return ok(scanHelpText());
1392
+ const format = outputFormat(parsed);
1393
+ const root = resolveOutputDir(runtime.cwd, parsed);
1394
+ if (!(await runtime.exists(root))) {
1395
+ throw cliError("INVALID_ARGUMENT", "scan.error.missingPath", { path: root });
1396
+ }
1397
+ const report = await scanProject(root, runtime);
1398
+ if (format === "json")
1399
+ return jsonResult(report);
1400
+ return ok(renderScanText(report));
1401
+ }
1402
+ function scanHelpText() {
1403
+ return [
1404
+ t("word.usage"),
1405
+ t("scan.help.usage"),
1406
+ "",
1407
+ t("scan.help.body"),
1408
+ t("scan.help.privacy"),
1409
+ t("scan.help.json"),
1410
+ ].join("\n");
1411
+ }
1256
1412
  async function agentCommand(args, runtime) {
1257
1413
  const parsed = parseArgs(args, {
1258
1414
  values: ["path"],
@@ -1496,10 +1652,12 @@ function shouldPromptForNewProject(parsed, runtime) {
1496
1652
  }
1497
1653
  async function promptForNewProject(parsed, runtime) {
1498
1654
  const prompts = runtime.prompts;
1655
+ const detectedPackageManager = await projectPackageManager(runtime.cwd, runtime);
1499
1656
  if (!prompts) {
1500
1657
  return planFromArgs(parsed, {
1501
1658
  appNameRequired: true,
1502
1659
  defaultAppName: "RemoteDraw app",
1660
+ defaults: { packageManager: detectedPackageManager },
1503
1661
  });
1504
1662
  }
1505
1663
  await prompts.intro?.(t("prompt.intro.setup"));
@@ -1532,7 +1690,7 @@ async function promptForNewProject(parsed, runtime) {
1532
1690
  const packageManager = readString(parsed, "package-manager") == null
1533
1691
  ? await prompts.select({
1534
1692
  message: t("field.packageManager"),
1535
- defaultValue: "npm",
1693
+ defaultValue: detectedPackageManager,
1536
1694
  choices: packageManagerPromptChoices(),
1537
1695
  })
1538
1696
  : choice(readString(parsed, "package-manager"), packageManagerChoices, "package-manager");
@@ -1987,7 +2145,7 @@ function agentOptionCatalog() {
1987
2145
  },
1988
2146
  };
1989
2147
  }
1990
- export function projectSetupDefinition() {
2148
+ export function projectSetupDefinition(detected = {}) {
1991
2149
  return {
1992
2150
  defaults: {
1993
2151
  appName: "RemoteDraw App",
@@ -1995,7 +2153,8 @@ export function projectSetupDefinition() {
1995
2153
  sender: "remotedraw-ios",
1996
2154
  sdk: "react",
1997
2155
  preset: "sketch",
1998
- packageManager: "npm",
2156
+ // npm only when the project has not already answered this itself.
2157
+ packageManager: detected.packageManager ?? "npm",
1999
2158
  },
2000
2159
  fields: projectSetupFields(),
2001
2160
  normalize(values, changed) {
@@ -2399,7 +2558,7 @@ function initPackageJson(plan) {
2399
2558
  }
2400
2559
  function configJson(plan) {
2401
2560
  return {
2402
- $schema: "https://www.remotedraw.com/schemas/remotedraw.config.json",
2561
+ $schema: "https://remotedraw.com/schemas/remotedraw.config.json",
2403
2562
  appName: plan.appName,
2404
2563
  environment: "dev",
2405
2564
  target: plan.target,
@@ -2467,12 +2626,18 @@ function integrationReadme(plan) {
2467
2626
  }
2468
2627
  function reactBackendSessionTs(plan) {
2469
2628
  return [
2629
+ "// SERVER-SIDE ONLY. This file reads REMOTEDRAW_API_KEY (an rd_sk_ secret).",
2630
+ "// It must run where secrets live — an API route, a server action, a Convex",
2631
+ "// action, a serverless function — never in browser or app code. If this",
2632
+ "// landed under a Vite/Next client tree, move it to your server before use.",
2633
+ "",
2470
2634
  'import { createHttpRemoteDrawApiClient } from "@remotedraw/react";',
2471
2635
  'import type { CreateSessionRequest } from "@remotedraw/react";',
2472
2636
  "",
2473
2637
  "const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
2474
2638
  "const apiKey = process.env.REMOTEDRAW_API_KEY;",
2475
2639
  "",
2640
+ ...mapBoundsNote(plan.preset),
2476
2641
  "export function createRemoteDrawSessionRequest(): CreateSessionRequest {",
2477
2642
  " return " +
2478
2643
  JSON.stringify(createSessionPayload({
@@ -2654,12 +2819,18 @@ function reactSenderTsx() {
2654
2819
  }
2655
2820
  function svelteBackendSessionTs(plan) {
2656
2821
  return [
2822
+ "// SERVER-SIDE ONLY. This file reads REMOTEDRAW_API_KEY (an rd_sk_ secret).",
2823
+ "// It must run where secrets live — an API route, a server action, a Convex",
2824
+ "// action, a serverless function — never in browser or app code. If this",
2825
+ "// landed under a Vite/Next client tree, move it to your server before use.",
2826
+ "",
2657
2827
  'import { createHttpRemoteDrawApiClient } from "@remotedraw/svelte";',
2658
2828
  'import type { CreateSessionRequest } from "@remotedraw/svelte";',
2659
2829
  "",
2660
2830
  "const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
2661
2831
  "const apiKey = process.env.REMOTEDRAW_API_KEY;",
2662
2832
  "",
2833
+ ...mapBoundsNote(plan.preset),
2663
2834
  "export function createRemoteDrawSessionRequest(): CreateSessionRequest {",
2664
2835
  " return " +
2665
2836
  JSON.stringify(createSessionPayload({
@@ -2730,6 +2901,11 @@ function rawHttpSessionTs(plan) {
2730
2901
  // declares nothing and stays import-free.
2731
2902
  const typed = plan.sdk === "js";
2732
2903
  return [
2904
+ "// SERVER-SIDE ONLY. This file reads REMOTEDRAW_API_KEY (an rd_sk_ secret).",
2905
+ "// It must run where secrets live — an API route, a server action, a Convex",
2906
+ "// action, a serverless function — never in browser or app code. If this",
2907
+ "// landed under a Vite/Next client tree, move it to your server before use.",
2908
+ "",
2733
2909
  ...(typed
2734
2910
  ? [
2735
2911
  "import type {",
@@ -2742,6 +2918,7 @@ function rawHttpSessionTs(plan) {
2742
2918
  "const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
2743
2919
  "const apiKey = process.env.REMOTEDRAW_API_KEY;",
2744
2920
  "",
2921
+ ...mapBoundsNote(plan.preset),
2745
2922
  `export function createRemoteDrawSessionRequest()${typed ? ": CreateSessionRequest" : ""} {`,
2746
2923
  " return " +
2747
2924
  JSON.stringify(createSessionPayload({
@@ -3029,14 +3206,53 @@ function reactViteConfig() {
3029
3206
  "",
3030
3207
  ].join("\n");
3031
3208
  }
3209
+ /**
3210
+ * The example fence a map preset ships with.
3211
+ *
3212
+ * A map session's `coordinateSpace.bounds` is what every point on the wire is
3213
+ * normalized against — `x` linear in longitude, `y` linear in Web Mercator — and
3214
+ * it cannot be changed once the session exists. Omitting it does not mean "the
3215
+ * customer's map": it means the server's own default region, which is how
3216
+ * `remotedraw init --preset mapMarkup` used to produce a phone showing New York
3217
+ * whatever the customer's map was pointed at.
3218
+ *
3219
+ * So the preset states one, explicitly and visibly, with the note above it in
3220
+ * the generated file. `width`/`height` are that region's Mercator aspect.
3221
+ */
3222
+ const MAP_PRESET_COORDINATE_SPACE = {
3223
+ width: 1600,
3224
+ height: 1310,
3225
+ bounds: { minX: 4.85, minY: 52.35, maxX: 4.95, maxY: 52.4 },
3226
+ };
3227
+ /**
3228
+ * The comment that has to travel with the bounds. A map board's fence is the
3229
+ * one decision this scaffold cannot walk back for the customer.
3230
+ */
3231
+ function mapBoundsNote(preset) {
3232
+ if (PRESETS[preset].kind !== "map")
3233
+ return [];
3234
+ return [
3235
+ "// `target.coordinateSpace.bounds` is this board's geographic fence: every",
3236
+ "// point on the wire is normalized against it (x linear in longitude, y",
3237
+ "// linear in Web Mercator). It is FIXED for the life of the session — no",
3238
+ "// route changes it — so size it LARGER than the camera you open on, or a",
3239
+ "// sender that pans is drawing at the edge of the world. Replace the example",
3240
+ "// region below with yours; padMapBounds(camera, 1) from @remotedraw/geometry",
3241
+ "// grows a camera into a fence. width/height are that fence's Mercator aspect.",
3242
+ "",
3243
+ ];
3244
+ }
3032
3245
  export function createSessionPayload(options) {
3033
3246
  const descriptor = descriptorForPreset(options.preset, options.label);
3034
3247
  return {
3035
3248
  externalId: options.externalId,
3036
3249
  markupPreset: options.preset,
3037
3250
  target: {
3038
- kind: targetKindForPreset(options.preset),
3251
+ kind: PRESETS[options.preset].kind,
3039
3252
  label: options.label,
3253
+ ...(PRESETS[options.preset].kind === "map"
3254
+ ? { coordinateSpace: MAP_PRESET_COORDINATE_SPACE }
3255
+ : {}),
3040
3256
  metadata: {
3041
3257
  descriptor,
3042
3258
  },
@@ -3064,7 +3280,7 @@ function descriptorForPreset(preset, label) {
3064
3280
  return {
3065
3281
  version: 1,
3066
3282
  privacy: "metadata-only",
3067
- surfaceRole: surfaceRoleForPreset(preset),
3283
+ surfaceRole: PRESETS[preset].role,
3068
3284
  inputIntent: preset === "pointer" ? "point" : "annotate",
3069
3285
  label,
3070
3286
  phoneHints: {
@@ -3076,42 +3292,18 @@ function descriptorForPreset(preset, label) {
3076
3292
  },
3077
3293
  };
3078
3294
  }
3079
- function targetKindForPreset(preset) {
3080
- if (preset === "approval")
3081
- return "field";
3082
- if (preset === "photoMarkup" || preset === "designReview")
3083
- return "image";
3084
- if (preset === "pdfMarkup")
3085
- return "pdf";
3086
- if (preset === "mapMarkup")
3087
- return "map";
3088
- if (preset === "screenMarkup" || preset === "pointer")
3089
- return "screen";
3090
- return "svg";
3091
- }
3092
- function surfaceRoleForPreset(preset) {
3093
- if (preset === "photoMarkup" || preset === "designReview")
3094
- return "image";
3095
- if (preset === "mapMarkup")
3096
- return "map";
3097
- if (preset === "screenMarkup" || preset === "pointer")
3098
- return "screen";
3099
- if (preset === "sketch")
3100
- return "canvas";
3101
- return "document";
3102
- }
3295
+ const PRESETS = {
3296
+ approval: { kind: "field", role: "document", label: "Quick approval" },
3297
+ sketch: { kind: "svg", role: "canvas", label: "Sketch input" },
3298
+ photoMarkup: { kind: "image", role: "image", label: "Photo annotation" },
3299
+ pdfMarkup: { kind: "pdf", role: "document", label: "PDF annotation" },
3300
+ mapMarkup: { kind: "map", role: "map", label: "Map annotation" },
3301
+ screenMarkup: { kind: "screen", role: "screen", label: "Screen annotation" },
3302
+ designReview: { kind: "image", role: "image", label: "Design review" },
3303
+ pointer: { kind: "screen", role: "screen", label: "Pointer input" },
3304
+ };
3103
3305
  function labelForPreset(preset) {
3104
- const labels = {
3105
- approval: "Quick approval",
3106
- sketch: "Sketch input",
3107
- photoMarkup: "Photo annotation",
3108
- pdfMarkup: "PDF annotation",
3109
- mapMarkup: "Map annotation",
3110
- screenMarkup: "Screen annotation",
3111
- designReview: "Design review",
3112
- pointer: "Pointer input",
3113
- };
3114
- return labels[preset];
3306
+ return PRESETS[preset].label;
3115
3307
  }
3116
3308
  function curlForCreateInput(apiBaseUrl, payload) {
3117
3309
  return [
@@ -3302,6 +3494,12 @@ function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisionin
3302
3494
  ...(plan.apiBaseUrl.includes("<deployment>")
3303
3495
  ? [t("cloud.warning.placeholder")]
3304
3496
  : []),
3497
+ // The backend helper is written under src/ because the scaffold has no
3498
+ // idea where this project's server lives. Say so, every time, or a Vite
3499
+ // app ships its key.
3500
+ ...(plan.target === "web" && plan.sdk !== "swift"
3501
+ ? [t("cloud.warning.clientTree")]
3502
+ : []),
3305
3503
  ...(offline
3306
3504
  ? [t("cloud.warning.offline")]
3307
3505
  : dryRun
@@ -3339,22 +3537,21 @@ function statusLine(state, label, message) {
3339
3537
  return `[${state}] ${label}: ${message}`;
3340
3538
  }
3341
3539
  function installCommand(packageManager) {
3342
- if (packageManager === "npm")
3343
- return "npm install";
3344
- if (packageManager === "pnpm")
3345
- return "pnpm install";
3346
- if (packageManager === "yarn")
3347
- return "yarn install";
3348
- return "bun install";
3540
+ return packageManagerCommands[packageManager].install;
3349
3541
  }
3350
3542
  function devCommandForPackageManager(packageManager) {
3351
- if (packageManager === "npm")
3352
- return "npm run dev";
3353
- if (packageManager === "pnpm")
3354
- return "pnpm dev";
3355
- if (packageManager === "yarn")
3356
- return "yarn dev";
3357
- return "bun run dev";
3543
+ return `${packageManagerCommands[packageManager].run} dev`;
3544
+ }
3545
+ /**
3546
+ * The manager the project already uses, not the one we happen to prefer.
3547
+ * Scaffolding into a pnpm repo and then printing `npm install` is how a second
3548
+ * lockfile gets born.
3549
+ */
3550
+ async function projectPackageManager(dir, runtime) {
3551
+ const detected = await detectPackageManager(dir, runtime, {
3552
+ pathModule: path,
3553
+ });
3554
+ return detected.manager;
3358
3555
  }
3359
3556
  function titleCase(value) {
3360
3557
  return value
package/dist/cloud.d.ts CHANGED
@@ -2,7 +2,7 @@ export declare const DEFAULT_REMOTEDRAW_API_BASE_URL = "https://api.remotedraw.c
2
2
  export type CloudRuntime = {
3
3
  env: Record<string, string | undefined>;
4
4
  isInteractive?: boolean;
5
- fetch?: typeof fetch;
5
+ fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
6
6
  exists: (filePath: string) => boolean | Promise<boolean>;
7
7
  readFile: (filePath: string) => Promise<string>;
8
8
  writeFile: (filePath: string, contents: string) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"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"}