@remotedraw/cli 0.1.3 → 0.2.1

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
@@ -7,23 +7,39 @@ import { fileURLToPath } from "node:url";
7
7
  import { emitKeypressEvents } from "node:readline";
8
8
  import { createInterface } from "node:readline/promises";
9
9
  import { createWizardTerminal, runSetupWizard, } from "./setup-wizard.js";
10
- import { assertEnvCanAcceptProvisioning, currentCliAccount, DEFAULT_REMOTEDRAW_API_BASE_URL, globalConfigPath, loginToRemoteDraw, logoutFromRemoteDraw, provisionRemoteDrawProject, remoteDrawApiBaseUrl, writeProvisionedProjectSetup, } from "./cloud.js";
10
+ import { assertEnvCanAcceptProvisioning, currentCliAccount, DEFAULT_REMOTEDRAW_API_BASE_URL, globalConfigPath, loginToRemoteDraw, logoutFromRemoteDraw, provisionRemoteDrawProject, readStoredLanguage, remoteDrawApiBaseUrl, writeStoredLanguage, writeProvisionedProjectSetup, } from "./cloud.js";
11
+ import { LANGUAGE_ENV_VARS, SOURCE_LOCALE, extractLanguageFlag, languageChoices, languagePromptMessage, localeEndonym, normalizeLocale, resolveLocale, setActiveLocale, supportedLocaleList, SUPPORTED_LOCALES, systemLocaleHint, t, } from "./i18n.js";
12
+ import { CliError, cliError } from "./errors.js";
13
+ import { AGENT_SKILL_MARKDOWN } from "./generated/agent-skill.js";
14
+ import { reportCliCrash } from "./telemetry.js";
11
15
  import { checkForUpdate, CLI_PACKAGE_NAME, detectInstallMethod, installMethodDisplay, runInstall, } from "./update.js";
12
16
  const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
13
17
  export const CLI_VERSION = typeof packageMetadata.version === "string"
14
18
  ? packageMetadata.version
15
19
  : "unknown";
16
- const targetChoices = ["web", "desktop", "ios", "headless"];
17
- const senderChoices = [
20
+ // Generated manifests may only name packages this repository actually
21
+ // publishes, at a range those published versions satisfy. "latest" used to be
22
+ // written here, which resolved to nothing at all while the SDK packages were
23
+ // private, and would silently accept a future breaking major once they were
24
+ // not. tests/generated-manifest.test.ts cross-checks every range below against
25
+ // the workspace manifests.
26
+ export const REMOTEDRAW_SDK_RANGE = "^0.2.0";
27
+ export const REMOTEDRAW_CLI_RANGE = "^0.2.0";
28
+ export const targetChoices = ["web", "desktop", "ios", "headless"];
29
+ export const senderChoices = [
18
30
  "remotedraw-ios",
19
31
  "embedded-web",
20
32
  "own-ios",
21
33
  "headless",
22
34
  ];
23
- const sdkChoices = ["react", "svelte", "js", "swift", "headless"];
35
+ export const sdkChoices = [
36
+ "react",
37
+ "svelte",
38
+ "js",
39
+ "swift",
40
+ "headless",
41
+ ];
24
42
  const presetChoices = [
25
- "signature",
26
- "initials",
27
43
  "approval",
28
44
  "sketch",
29
45
  "photoMarkup",
@@ -40,7 +56,6 @@ const exampleChoices = [
40
56
  "ios-owned-sender",
41
57
  ];
42
58
  const packageManagerChoices = ["npm", "pnpm", "yarn", "bun"];
43
- const apiEndpointChoices = ["deployment", "local"];
44
59
  export function createNodeRuntime() {
45
60
  const cwd = process.cwd();
46
61
  const env = cliEnvironmentForWorkspace(cwd, process.env);
@@ -97,7 +112,7 @@ async function runChildCommand(command, args) {
97
112
  async function openExternalUrl(url) {
98
113
  const parsed = new URL(url);
99
114
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
100
- throw new Error("Only HTTP(S) URLs can be opened.");
115
+ throw new Error(t("error.httpUrlsOnly"));
101
116
  }
102
117
  const [command, args] = process.platform === "darwin"
103
118
  ? ["open", [url]]
@@ -148,10 +163,10 @@ function createTerminalPrompter(input, output) {
148
163
  }
149
164
  function terminalSelect(input, output, options) {
150
165
  if (options.choices.length === 0) {
151
- throw new Error(`No choices available for ${options.message}.`);
166
+ throw new Error(t("error.noChoices", { message: options.message }));
152
167
  }
153
168
  if (!input.isTTY || !output.isTTY || !input.setRawMode) {
154
- throw new Error("Interactive setup requires a TTY.");
169
+ throw new Error(t("error.interactiveNeedsTty"));
155
170
  }
156
171
  const defaultIndex = Math.max(0, options.choices.findIndex((choiceOption) => choiceOption.value === options.defaultValue));
157
172
  let selectedIndex = defaultIndex;
@@ -225,7 +240,13 @@ function selectPromptLines(options, selectedIndex) {
225
240
  const hint = choiceOption.hint ? ` ${style("dim", choiceOption.hint)}` : "";
226
241
  lines.push(`${marker} ${label}${hint}`);
227
242
  }
228
- lines.push(`${style("dim", "↑/↓")} move · ${style("dim", "Enter")} select · ${style("dim", "Ctrl+C")} quit`);
243
+ // Rendered in the active locale, which for the language question itself is
244
+ // English — nothing has been chosen yet at that point.
245
+ lines.push([
246
+ `${style("dim", "↑/↓")} ${t("prompt.control.move")}`,
247
+ `${style("dim", "Enter")} ${t("prompt.control.select")}`,
248
+ `${style("dim", "Ctrl+C")} ${t("prompt.control.quit")}`,
249
+ ].join(" · "));
229
250
  return lines;
230
251
  }
231
252
  function clearRendered(output, lineCount) {
@@ -241,13 +262,61 @@ function style(kind, value) {
241
262
  };
242
263
  return `${codes[kind]}${value}\x1b[0m`;
243
264
  }
265
+ /**
266
+ * How the locale for this run was decided. Only `language` reports it, but it
267
+ * has to be recorded where the decision is actually made.
268
+ */
269
+ let resolvedLocaleSource = "default";
270
+ /**
271
+ * The language the CLI's *chrome* resolved to. It can differ from the active
272
+ * locale on a machine-readable run, which is forced to English — `language`
273
+ * must still report what the reader actually chose.
274
+ */
275
+ let resolvedChromeLocale = SOURCE_LOCALE;
244
276
  export async function runCli(args, runtime = createNodeRuntime()) {
277
+ // The language flag is global, so it is taken out before per-command parsing
278
+ // — which rejects any option the command has not declared.
279
+ const { value: languageFlag, rest: cliArgs } = extractLanguageFlag(args);
245
280
  try {
246
- const command = args[0];
247
- const commandArgs = args.slice(1);
281
+ const stored = await readStoredLanguage(runtime);
282
+ const resolution = resolveLocale({
283
+ flag: languageFlag,
284
+ env: runtime.env,
285
+ stored,
286
+ });
287
+ if (!resolution.ok) {
288
+ // Report the bad value in whatever language the reader already chose,
289
+ // falling back to English when they have not chosen one.
290
+ setActiveLocale(normalizeLocale(stored) ?? SOURCE_LOCALE);
291
+ const message = t("language.unknown", {
292
+ value: resolution.value,
293
+ supported: supportedLocaleList(),
294
+ variable: LANGUAGE_ENV_VARS[0],
295
+ });
296
+ return requestedJsonOutput(args)
297
+ ? jsonFailure("language", new Error(message))
298
+ : fail(message);
299
+ }
300
+ // Machine-readable output is a data interchange format, not chrome: it
301
+ // stays English so an agent quoting a label back to its user does not
302
+ // suddenly speak the operator's language. `--language` on the same
303
+ // invocation is a deliberate request and does localize it; an ambient
304
+ // stored choice or environment variable does not.
305
+ const machineReadable = machineReadableOutput(cliArgs);
306
+ const effectiveLocale = machineReadable && resolution.source !== "flag"
307
+ ? SOURCE_LOCALE
308
+ : resolution.locale;
309
+ setActiveLocale(effectiveLocale);
310
+ resolvedChromeLocale = resolution.locale;
311
+ resolvedLocaleSource = resolution.source;
312
+ // Unresolved means "never asked". Interactive entry points ask; every
313
+ // other invocation — CI, a pipe, an agent shelling out — stays English.
314
+ const askLanguage = resolution.needsPrompt;
315
+ const command = cliArgs[0];
316
+ const commandArgs = cliArgs.slice(1);
248
317
  if (command == null) {
249
318
  if (runtime.wizardTerminal) {
250
- return await setupWizardCommand(runtime);
319
+ return await setupWizardCommand(runtime, askLanguage);
251
320
  }
252
321
  return ok(mainHelpText());
253
322
  }
@@ -262,6 +331,8 @@ export async function runCli(args, runtime = createNodeRuntime()) {
262
331
  return ok(guideText());
263
332
  case "options":
264
333
  return optionsCommand(commandArgs);
334
+ case "language":
335
+ return await languageCommand(commandArgs, runtime);
265
336
  case "login":
266
337
  return await loginCommand(commandArgs, runtime);
267
338
  case "logout":
@@ -269,7 +340,7 @@ export async function runCli(args, runtime = createNodeRuntime()) {
269
340
  case "whoami":
270
341
  return await whoamiCommand(commandArgs, runtime);
271
342
  case "new":
272
- return await newCommand(commandArgs, runtime);
343
+ return await newCommand(commandArgs, runtime, askLanguage);
273
344
  case "init":
274
345
  return await initCommand(commandArgs, runtime);
275
346
  case "update":
@@ -286,20 +357,36 @@ export async function runCli(args, runtime = createNodeRuntime()) {
286
357
  return await agentCommand(commandArgs, runtime);
287
358
  default:
288
359
  return requestedJsonOutput(args)
289
- ? jsonFailure(command, new Error(`Unknown command "${command}". Run remotedraw --help.`))
290
- : fail(`Unknown command "${command}". Run remotedraw --help.`);
360
+ ? jsonFailure(command, new Error(t("error.unknownCommand", { command })))
361
+ : fail(t("error.unknownCommand", { command }));
291
362
  }
292
363
  }
293
364
  catch (error) {
365
+ // Only genuine faults land here: a bad flag or an unknown command returns a
366
+ // `fail(...)` result instead of throwing, so this path is not user error.
367
+ const command = cliArgs[0] ?? "help";
368
+ await reportCliCrash(error, command, cliTelemetryEnvironment(runtime));
294
369
  return requestedJsonOutput(args)
295
- ? jsonFailure(args[0] ?? "help", error)
370
+ ? jsonFailure(command, error)
296
371
  : fail(errorMessage(error));
297
372
  }
298
373
  }
299
- async function setupWizardCommand(runtime) {
374
+ function cliTelemetryEnvironment(runtime) {
375
+ return {
376
+ env: runtime.env,
377
+ version: CLI_VERSION,
378
+ platform: process.platform,
379
+ nodeVersion: process.versions.node,
380
+ fetch: runtime.fetch,
381
+ };
382
+ }
383
+ async function setupWizardCommand(runtime, askLanguage) {
300
384
  const terminal = runtime.wizardTerminal;
301
385
  if (!terminal)
302
386
  return ok(mainHelpText());
387
+ // Before login, before the first field: nothing the reader has to understand
388
+ // is printed until the language is settled.
389
+ await ensureLanguageChosen(runtime, askLanguage);
303
390
  if (!cloudSetupDisabled(runtime)) {
304
391
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env);
305
392
  await loginToRemoteDraw(runtime, apiBaseUrl);
@@ -324,6 +411,121 @@ async function setupWizardCommand(runtime) {
324
411
  });
325
412
  return { exitCode: result.exitCode, stdout: "" };
326
413
  }
414
+ /**
415
+ * The language question, asked once and only where a human is already being
416
+ * prompted. The question itself is built from every locale's own word for
417
+ * "Language" and lists endonyms, so it reads before any language is chosen.
418
+ */
419
+ async function ensureLanguageChosen(runtime, askLanguage) {
420
+ if (!askLanguage)
421
+ return;
422
+ const prompts = runtime.prompts;
423
+ // No TTY, no question: piped and CI runs must never block on this.
424
+ if (!prompts || runtime.isInteractive !== true)
425
+ return;
426
+ let chosen;
427
+ try {
428
+ chosen = await prompts.select({
429
+ message: languagePromptMessage(),
430
+ defaultValue: systemLocaleHint(runtime.env) ?? SOURCE_LOCALE,
431
+ choices: languageChoices(),
432
+ });
433
+ }
434
+ catch {
435
+ // A prompter that cannot run (stdin is not a real terminal after all)
436
+ // means English, never a failed command over a preference question.
437
+ return;
438
+ }
439
+ const locale = normalizeLocale(chosen) ?? SOURCE_LOCALE;
440
+ await applyLanguage(runtime, locale);
441
+ }
442
+ /** Sets the locale for the rest of this run and remembers it for later ones. */
443
+ async function applyLanguage(runtime, locale) {
444
+ setActiveLocale(locale);
445
+ resolvedChromeLocale = locale;
446
+ resolvedLocaleSource = "prompt";
447
+ try {
448
+ await writeStoredLanguage(runtime, locale);
449
+ }
450
+ catch (error) {
451
+ // An unwritable config is worth saying out loud, but it must not abort the
452
+ // command the reader actually asked for.
453
+ runtime.notify?.(t("language.saveFailed", {
454
+ reason: errorMessage(error),
455
+ language: localeEndonym(locale),
456
+ }));
457
+ }
458
+ }
459
+ async function languageCommand(args, runtime) {
460
+ const parsed = parseArgs(args, { values: ["format"], booleans: ["help"] });
461
+ if (parsed.booleans.has("help"))
462
+ return ok(languageHelpText());
463
+ const format = outputFormat(parsed);
464
+ const requested = parsed.positionals[0];
465
+ if (requested != null) {
466
+ const locale = normalizeLocale(requested);
467
+ if (!locale) {
468
+ const message = t("language.unknown", {
469
+ value: requested,
470
+ supported: supportedLocaleList(),
471
+ variable: LANGUAGE_ENV_VARS[0],
472
+ });
473
+ return format === "json"
474
+ ? jsonFailure("language", new Error(message))
475
+ : fail(message);
476
+ }
477
+ await applyLanguage(runtime, locale);
478
+ return format === "json"
479
+ ? jsonResult({ ok: true, command: "language", language: locale })
480
+ : ok(t("language.saved", { language: localeEndonym(locale) }));
481
+ }
482
+ if (format !== "json" && runtime.prompts && runtime.isInteractive === true) {
483
+ const chosen = await runtime.prompts.select({
484
+ message: languagePromptMessage(),
485
+ defaultValue: resolvedChromeLocale,
486
+ choices: languageChoices(),
487
+ });
488
+ const locale = normalizeLocale(chosen) ?? SOURCE_LOCALE;
489
+ await applyLanguage(runtime, locale);
490
+ return ok(t("language.saved", { language: localeEndonym(locale) }));
491
+ }
492
+ const current = resolvedChromeLocale;
493
+ if (format === "json") {
494
+ return jsonResult({
495
+ ok: true,
496
+ command: "language",
497
+ language: current,
498
+ source: resolvedLocaleSource,
499
+ available: SUPPORTED_LOCALES.map((locale) => ({
500
+ code: locale,
501
+ endonym: localeEndonym(locale),
502
+ })),
503
+ });
504
+ }
505
+ return ok([
506
+ t("language.current", {
507
+ language: localeEndonym(current),
508
+ locale: current,
509
+ }),
510
+ languageSourceLine(runtime),
511
+ "",
512
+ t("language.available"),
513
+ ...SUPPORTED_LOCALES.map((locale) => ` ${locale.padEnd(6)} ${localeEndonym(locale)}`),
514
+ ].join("\n"));
515
+ }
516
+ function languageSourceLine(runtime) {
517
+ if (resolvedLocaleSource === "flag")
518
+ return t("language.source.flag");
519
+ if (resolvedLocaleSource === "env") {
520
+ const variable = LANGUAGE_ENV_VARS.find((name) => runtime.env[name]?.trim()) ??
521
+ LANGUAGE_ENV_VARS[0];
522
+ return t("language.source.env", { variable });
523
+ }
524
+ if (resolvedLocaleSource === "config" || resolvedLocaleSource === "prompt") {
525
+ return t("language.source.config", { path: globalConfigPath(runtime) });
526
+ }
527
+ return t("language.source.default");
528
+ }
327
529
  function ok(stdout) {
328
530
  return { exitCode: 0, stdout: ensureTrailingNewline(stdout) };
329
531
  }
@@ -335,6 +537,12 @@ function jsonResult(value, exitCode = 0) {
335
537
  }
336
538
  function jsonFailure(command, error) {
337
539
  const message = errorMessage(error);
540
+ // A declared code survives translation; the substring heuristic below is
541
+ // only a fallback for errors raised outside the catalog (a failed fetch, a
542
+ // filesystem fault) and is matched against English internals.
543
+ if (error instanceof CliError) {
544
+ return jsonResult({ ok: false, command, error: { code: error.code, message } }, 1);
545
+ }
338
546
  const code = message.includes("required")
339
547
  ? "MISSING_ARGUMENT"
340
548
  : message.includes(" requires ") || message.includes("only supported")
@@ -348,6 +556,14 @@ function jsonFailure(command, error) {
348
556
  : "COMMAND_FAILED";
349
557
  return jsonResult({ ok: false, command, error: { code, message } }, 1);
350
558
  }
559
+ /**
560
+ * True when this invocation asks for output meant to be parsed rather than
561
+ * read: `--format json` on any command, or `create-input --json`.
562
+ */
563
+ function machineReadableOutput(args) {
564
+ return (requestedJsonOutput(args) ||
565
+ args.some((argument) => argument === "--json"));
566
+ }
351
567
  function requestedJsonOutput(args) {
352
568
  return args.some((argument, index) => argument === "--format=json" ||
353
569
  (argument === "--format" && args[index + 1] === "json"));
@@ -365,32 +581,34 @@ function errorMessage(error) {
365
581
  return error instanceof Error ? error.message : String(error);
366
582
  }
367
583
  function mainHelpText() {
584
+ const command = (name, description) => ` ${name.padEnd(14)} ${description}`;
368
585
  return [
369
- "RemoteDraw CLI",
370
- "",
371
- "Usage:",
372
- " remotedraw <command> [options]",
373
- "",
374
- "Commands:",
375
- " guide Choose an integration path and SDK.",
376
- " options Print setup choices and compatibility metadata.",
377
- " login Authorize this computer through the dashboard.",
378
- " logout Revoke and remove this computer's CLI credential.",
379
- " whoami Show the signed-in RemoteDraw account.",
380
- " new Create a new app with RemoteDraw starter code.",
381
- " init Add RemoteDraw starter code to an existing project.",
382
- " dev Print the local sandbox workflow for an integration.",
383
- " doctor Check project config, packages, and environment variables.",
384
- " update Install the latest published RemoteDraw CLI.",
385
- " create-input Create or print a test input request payload.",
386
- " examples List or install example projects.",
387
- " agent Print or install the RemoteDraw agent skill.",
388
- "",
389
- "Start here:",
586
+ t("help.title"),
587
+ "",
588
+ t("word.usage"),
589
+ t("help.usage.main"),
590
+ "",
591
+ t("word.commands"),
592
+ command("guide", t("help.command.guide")),
593
+ command("options", t("help.command.options")),
594
+ command("language", t("help.command.language")),
595
+ command("login", t("help.command.login")),
596
+ command("logout", t("help.command.logout")),
597
+ command("whoami", t("help.command.whoami")),
598
+ command("new", t("help.command.new")),
599
+ command("init", t("help.command.init")),
600
+ command("dev", t("help.command.dev")),
601
+ command("doctor", t("help.command.doctor")),
602
+ command("update", t("help.command.update")),
603
+ command("create-input", t("help.command.createInput")),
604
+ command("examples", t("help.command.examples")),
605
+ command("agent", t("help.command.agent")),
606
+ "",
607
+ t("help.startHere"),
390
608
  " remotedraw guide",
391
609
  " remotedraw login",
392
610
  " remotedraw new",
393
- " remotedraw new --app-name MijnApp --target web --sender remotedraw-ios --sdk react",
611
+ " remotedraw new --app-name MyApp --target web --sender remotedraw-ios --sdk react",
394
612
  " remotedraw init --target web --sender embedded-web --sdk react",
395
613
  ].join("\n");
396
614
  }
@@ -405,39 +623,39 @@ function optionsCommand(args) {
405
623
  if (outputFormat(parsed) === "json")
406
624
  return jsonResult(catalog);
407
625
  return ok([
408
- "RemoteDraw setup options",
626
+ t("options.title"),
409
627
  "",
410
628
  ...catalog.fields.flatMap((field) => [
411
629
  `${field.label} (${field.flag})`,
412
630
  ...field.options.map((option) => ` ${option.value}: ${option.details.summary}`),
413
631
  "",
414
632
  ]),
415
- "For machine-readable metadata: remotedraw options --format json",
633
+ t("options.machineReadable"),
416
634
  ].join("\n"));
417
635
  }
418
636
  function guideText() {
419
637
  return [
420
- "RemoteDraw integration guide",
638
+ t("guide.title"),
421
639
  "",
422
- "Choose the surface your customer already has:",
423
- " Web app receiver + RemoteDraw iOS app sender",
424
- " remotedraw init --target web --sender remotedraw-ios --sdk react --preset signature",
640
+ t("guide.intro"),
641
+ t("guide.path.webIos"),
642
+ " remotedraw init --target web --sender remotedraw-ios --sdk react --preset sketch",
425
643
  "",
426
- " Web app receiver + your own web sender UI",
644
+ t("guide.path.webOwned"),
427
645
  " remotedraw init --target web --sender embedded-web --sdk react --preset sketch",
428
646
  "",
429
- " Svelte app receiver + RemoteDraw iOS app sender",
430
- " remotedraw init --target web --sender remotedraw-ios --sdk svelte --preset signature",
647
+ t("guide.path.svelteIos"),
648
+ " remotedraw init --target web --sender remotedraw-ios --sdk svelte --preset sketch",
431
649
  "",
432
- " Desktop app receiver with raw HTTP",
650
+ t("guide.path.desktop"),
433
651
  " remotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch",
434
652
  "",
435
- " Your own iOS sender app",
653
+ t("guide.path.ownIos"),
436
654
  " remotedraw init --target ios --sender own-ios --sdk swift --preset sketch",
437
655
  "",
438
- "API keys stay on trusted backend code. Receivers get receiver tokens, senders get join URLs or sender tokens.",
439
- "remotedraw new and init create a dashboard project, mint a project-scoped development API key, and write it to .env.local by default.",
440
- "Use --offline when you intentionally want local scaffolding only.",
656
+ t("guide.note.keys"),
657
+ t("guide.note.provisioning"),
658
+ t("guide.note.offline"),
441
659
  ].join("\n");
442
660
  }
443
661
  async function loginCommand(args, runtime) {
@@ -455,11 +673,9 @@ async function loginCommand(args, runtime) {
455
673
  ...(deviceName == null ? {} : { deviceName }),
456
674
  });
457
675
  return ok([
458
- result.alreadyLoggedIn
459
- ? "Already logged in."
460
- : "RemoteDraw login complete.",
676
+ result.alreadyLoggedIn ? t("login.already") : t("login.complete"),
461
677
  accountSummary(result.account),
462
- `Credential: ${globalConfigPath(runtime)}`,
678
+ t("login.credential", { path: globalConfigPath(runtime) }),
463
679
  ].join("\n"));
464
680
  }
465
681
  async function logoutCommand(args, runtime) {
@@ -472,8 +688,8 @@ async function logoutCommand(args, runtime) {
472
688
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
473
689
  const result = await logoutFromRemoteDraw(runtime, apiBaseUrl);
474
690
  return ok(result.hadCredential
475
- ? `Logged out from ${apiBaseUrl}.`
476
- : `No stored credential for ${apiBaseUrl}.`);
691
+ ? t("logout.done", { url: apiBaseUrl })
692
+ : t("logout.none", { url: apiBaseUrl }));
477
693
  }
478
694
  async function whoamiCommand(args, runtime) {
479
695
  const parsed = parseArgs(args, {
@@ -485,15 +701,15 @@ async function whoamiCommand(args, runtime) {
485
701
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
486
702
  const account = await currentCliAccount(runtime, apiBaseUrl);
487
703
  if (!account) {
488
- return fail(`Not logged in to ${apiBaseUrl}. Run remotedraw login.`);
704
+ return fail(t("whoami.notLoggedIn", { url: apiBaseUrl }));
489
705
  }
490
- return ok([accountSummary(account), `API: ${apiBaseUrl}`].join("\n"));
706
+ return ok([accountSummary(account), t("whoami.api", { url: apiBaseUrl })].join("\n"));
491
707
  }
492
708
  function accountSummary(account) {
493
- const identity = account.email || account.name || "RemoteDraw user";
709
+ const identity = account.email || account.name || t("whoami.anonymous");
494
710
  return `${identity} (${account.tenantSlug})`;
495
711
  }
496
- async function newCommand(args, runtime) {
712
+ async function newCommand(args, runtime, askLanguage = false) {
497
713
  const parsed = parseArgs(args, {
498
714
  values: [
499
715
  "app-name",
@@ -511,8 +727,13 @@ async function newCommand(args, runtime) {
511
727
  if (parsed.booleans.has("help"))
512
728
  return ok(newHelpText());
513
729
  const format = outputFormat(parsed);
514
- const plan = !parsed.booleans.has("non-interactive") &&
515
- shouldPromptForNewProject(parsed, runtime)
730
+ const interactive = !parsed.booleans.has("non-interactive") &&
731
+ shouldPromptForNewProject(parsed, runtime);
732
+ // Only ask when this run was going to prompt anyway; a fully flagged
733
+ // `new` stays as non-interactive as the caller wrote it.
734
+ if (interactive)
735
+ await ensureLanguageChosen(runtime, askLanguage);
736
+ const plan = interactive
516
737
  ? await promptForNewProject(parsed, runtime)
517
738
  : planFromArgs(parsed, {
518
739
  appNameRequired: true,
@@ -537,7 +758,7 @@ async function newCommand(args, runtime) {
537
758
  }
538
759
  return format === "json"
539
760
  ? jsonResult(scaffoldJsonResult("new", plan, outputDir, written, parsed, provisioning, offline, dryRun))
540
- : ok(scaffoldSummary("Created", plan, outputDir, written, dryRun) +
761
+ : ok(scaffoldSummary(t("scaffold.verb.created"), plan, outputDir, written, dryRun) +
541
762
  cloudSetupSummary(provisioning, offline, dryRun));
542
763
  }
543
764
  export async function createRemoteDrawProject(plan, outputDir, runtime, options = {}) {
@@ -594,7 +815,7 @@ async function initCommand(args, runtime) {
594
815
  }
595
816
  return format === "json"
596
817
  ? jsonResult(scaffoldJsonResult("init", plan, outputDir, written, parsed, provisioning, offline, dryRun))
597
- : ok(scaffoldSummary("Initialized", plan, outputDir, written, dryRun) +
818
+ : ok(scaffoldSummary(t("scaffold.verb.initialized"), plan, outputDir, written, dryRun) +
598
819
  cloudSetupSummary(provisioning, offline, dryRun));
599
820
  }
600
821
  function cloudSetupDisabled(runtime) {
@@ -616,16 +837,16 @@ function cloudSetupSummary(provisioning, offline, dryRun) {
616
837
  if (dryRun)
617
838
  return "";
618
839
  if (offline) {
619
- return "\n\nCloud setup skipped (--offline). Run remotedraw init later to create a dashboard project.";
840
+ return `\n\n${t("cloud.skipped")}`;
620
841
  }
621
842
  if (!provisioning)
622
843
  return "";
623
844
  return [
624
845
  "",
625
846
  "",
626
- `Dashboard project: ${provisioning.project.dashboardUrl}`,
627
- "Development API key: written to .env.local (server-side only)",
628
- `Project ID: ${provisioning.project.id}`,
847
+ t("cloud.dashboardProject", { url: provisioning.project.dashboardUrl }),
848
+ t("cloud.devKey"),
849
+ t("cloud.projectId", { id: provisioning.project.id }),
629
850
  ].join("\n");
630
851
  }
631
852
  const DOCTOR_PROBE_TIMEOUT_MS = 8_000;
@@ -634,7 +855,7 @@ function probeFailureMessage(error) {
634
855
  }
635
856
  async function probeApiHealth(runtime, apiBaseUrl) {
636
857
  if (!runtime.fetch) {
637
- return { ok: false, message: "this runtime cannot make network requests" };
858
+ return { ok: false, message: t("doctor.probe.noNetwork") };
638
859
  }
639
860
  try {
640
861
  const response = await runtime.fetch(`${apiBaseUrl}/health`, {
@@ -657,7 +878,7 @@ async function probeApiHealth(runtime, apiBaseUrl) {
657
878
  */
658
879
  async function probeApiKey(runtime, apiBaseUrl, apiKey) {
659
880
  if (!runtime.fetch) {
660
- return { ok: false, message: "This runtime cannot make network requests." };
881
+ return { ok: false, message: t("doctor.probe.noNetworkKey") };
661
882
  }
662
883
  try {
663
884
  const response = await runtime.fetch(`${apiBaseUrl}/v1/sessions/list`, {
@@ -677,7 +898,7 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
677
898
  catch {
678
899
  return {
679
900
  ok: false,
680
- message: `Deployment returned a non-JSON response (HTTP ${response.status}).`,
901
+ message: t("doctor.probe.nonJson", { status: response.status }),
681
902
  };
682
903
  }
683
904
  if (!response.ok) {
@@ -687,7 +908,7 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
687
908
  typeof payload.error?.message ===
688
909
  "string"
689
910
  ? payload.error.message
690
- : `Deployment rejected the key (HTTP ${response.status}).`;
911
+ : t("doctor.probe.rejected", { status: response.status });
691
912
  return { ok: false, message };
692
913
  }
693
914
  const items = typeof payload === "object" && payload !== null && "items" in payload
@@ -718,13 +939,19 @@ async function updateCommand(args, runtime) {
718
939
  // ambient opt-outs that only silence the passive notice.
719
940
  const check = await checkForUpdate(runtime, CLI_VERSION, { force: true });
720
941
  if (!check) {
721
- const message = `Could not reach the npm registry to check for ${CLI_PACKAGE_NAME} updates. Run ${command} to update anyway.`;
942
+ const message = t("update.unreachable", {
943
+ package: CLI_PACKAGE_NAME,
944
+ command,
945
+ });
722
946
  return format === "json"
723
947
  ? jsonFailure("update", new Error(message))
724
948
  : fail(message);
725
949
  }
726
950
  if (!check.updateAvailable) {
727
- const message = `${CLI_PACKAGE_NAME} ${check.current} is already the latest version.`;
951
+ const message = t("update.alreadyLatest", {
952
+ package: CLI_PACKAGE_NAME,
953
+ version: check.current,
954
+ });
728
955
  return format === "json"
729
956
  ? jsonResult({ ok: true, command: "update", ...check, updated: false })
730
957
  : ok(message);
@@ -732,12 +959,18 @@ async function updateCommand(args, runtime) {
732
959
  if (checkOnly || method.ephemeral) {
733
960
  const message = method.ephemeral
734
961
  ? [
735
- `Update available: ${check.current} -> ${check.latest}.`,
736
- `This CLI was run through npx/bunx, which resolves a version per run. Use ${CLI_PACKAGE_NAME}@latest instead of updating in place.`,
962
+ t("update.available", {
963
+ current: check.current,
964
+ latest: check.latest,
965
+ }),
966
+ t("update.ephemeral", { package: CLI_PACKAGE_NAME }),
737
967
  ].join("\n")
738
968
  : [
739
- `Update available: ${check.current} -> ${check.latest}.`,
740
- `Run ${command} to install it.`,
969
+ t("update.available", {
970
+ current: check.current,
971
+ latest: check.latest,
972
+ }),
973
+ t("update.runToInstall", { command }),
741
974
  ].join("\n");
742
975
  return format === "json"
743
976
  ? jsonResult({
@@ -752,7 +985,11 @@ async function updateCommand(args, runtime) {
752
985
  }
753
986
  const install = await runInstall(runtime, method);
754
987
  if (!install.ran) {
755
- const message = `Update available: ${check.current} -> ${check.latest}. Run ${command} to install it.`;
988
+ const message = t("update.availableRun", {
989
+ current: check.current,
990
+ latest: check.latest,
991
+ command,
992
+ });
756
993
  return format === "json"
757
994
  ? jsonResult({
758
995
  ok: true,
@@ -764,7 +1001,10 @@ async function updateCommand(args, runtime) {
764
1001
  : ok(message);
765
1002
  }
766
1003
  if (install.exitCode !== 0) {
767
- const message = `${command} failed with exit code ${install.exitCode}.`;
1004
+ const message = t("update.failed", {
1005
+ command,
1006
+ code: install.exitCode,
1007
+ });
768
1008
  return format === "json"
769
1009
  ? jsonFailure("update", new Error(message))
770
1010
  : fail(message);
@@ -777,7 +1017,10 @@ async function updateCommand(args, runtime) {
777
1017
  updated: true,
778
1018
  installCommand: command,
779
1019
  })
780
- : ok(`Updated ${CLI_PACKAGE_NAME} to ${check.latest}.`);
1020
+ : ok(t("update.done", {
1021
+ package: CLI_PACKAGE_NAME,
1022
+ version: check.latest,
1023
+ }));
781
1024
  }
782
1025
  async function doctorCommand(args, runtime) {
783
1026
  const parsed = parseArgs(args, {
@@ -796,28 +1039,29 @@ async function doctorCommand(args, runtime) {
796
1039
  const apiBaseUrl = resolveDoctorApiBaseUrl(projectEnv, readString(parsed, "api-base-url"));
797
1040
  const normalizedEnvApiBaseUrl = apiBaseUrl.state === "ok" ? apiBaseUrl.url : undefined;
798
1041
  const envApiKey = projectEnv.REMOTEDRAW_API_KEY;
799
- const lines = ["RemoteDraw doctor", ""];
1042
+ const lines = [t("doctor.title"), ""];
800
1043
  const checks = [];
801
1044
  const addCheck = (state, label, message) => {
802
1045
  checks.push({ state, label, message });
803
1046
  lines.push(statusLine(state, label, message));
804
1047
  };
805
1048
  const hasConfig = await runtime.exists(configPath);
806
- addCheck(hasConfig ? "ok" : "warn", "remotedraw.config.json", hasConfig
807
- ? "Project is linked to a local integration profile."
808
- : "Run remotedraw init to create one.");
1049
+ addCheck(hasConfig ? "ok" : "warn", "remotedraw.config.json", hasConfig ? t("doctor.config.ok") : t("doctor.config.missing"));
809
1050
  const packageJson = (await runtime.exists(packagePath))
810
1051
  ? await readJsonFile(packagePath, runtime)
811
1052
  : null;
812
- addCheck(packageJson ? "ok" : "warn", "package.json", packageJson
813
- ? "Package metadata found."
814
- : "No package.json found in this directory.");
1053
+ addCheck(packageJson ? "ok" : "warn", "package.json", packageJson ? t("doctor.package.ok") : t("doctor.package.missing"));
815
1054
  const config = hasConfig ? await readJsonFile(configPath, runtime) : null;
816
1055
  const sdk = stringFromRecord(config, "sdk");
817
- if (sdk === "react") {
818
- addCheck(hasDependency(packageJson, "@remotedraw/react") ? "ok" : "warn", "@remotedraw/react", hasDependency(packageJson, "@remotedraw/react")
819
- ? "React SDK dependency is installed in package.json."
820
- : "Install @remotedraw/react or rerun remotedraw init.");
1056
+ const sdkPackage = sdkPackageName(sdk);
1057
+ if (sdkPackage) {
1058
+ const installed = hasDependency(packageJson, sdkPackage);
1059
+ addCheck(installed ? "ok" : "warn", sdkPackage, installed
1060
+ ? t("doctor.sdk.ok", { package: sdkPackage })
1061
+ : t("doctor.sdk.missing", {
1062
+ package: sdkPackage,
1063
+ range: REMOTEDRAW_SDK_RANGE,
1064
+ }));
821
1065
  }
822
1066
  addCheck(apiBaseUrl.state === "ok"
823
1067
  ? "ok"
@@ -825,47 +1069,55 @@ async function doctorCommand(args, runtime) {
825
1069
  ? "warn"
826
1070
  : "error", "REMOTEDRAW_API_BASE_URL", doctorApiBaseUrlMessage(apiBaseUrl));
827
1071
  addCheck(envApiKey?.startsWith("rd_sk_") ? "ok" : "warn", "REMOTEDRAW_API_KEY", envApiKey?.startsWith("rd_sk_")
828
- ? "Server-side API key shape looks correct."
829
- : "Keep an rd_sk_... key in backend secrets only.");
1072
+ ? t("doctor.apiKey.ok")
1073
+ : t("doctor.apiKey.missing"));
830
1074
  // The checks above only prove the strings look right. A revoked key, a URL
831
1075
  // pointing at the wrong deployment, and an exhausted credit balance all pass
832
1076
  // them, so doctor also asks the deployment itself.
833
1077
  if (offline) {
834
- addCheck("warn", "deployment", "Skipped network checks (--offline). Rerun without --offline to verify the key against the deployment.");
1078
+ addCheck("warn", t("doctor.label.deployment"), t("doctor.network.offline"));
835
1079
  }
836
1080
  else if (!normalizedEnvApiBaseUrl) {
837
- addCheck("warn", "deployment", "Skipped network checks because REMOTEDRAW_API_BASE_URL is not a usable HTTP origin.");
1081
+ addCheck("warn", t("doctor.label.deployment"), t("doctor.network.noOrigin"));
838
1082
  }
839
1083
  else {
840
1084
  const health = await probeApiHealth(runtime, normalizedEnvApiBaseUrl);
841
- addCheck(health.ok ? "ok" : "error", "deployment reachable", health.ok
842
- ? `${normalizedEnvApiBaseUrl} answered /health.`
843
- : `${normalizedEnvApiBaseUrl} did not answer /health: ${health.message}`);
1085
+ addCheck(health.ok ? "ok" : "error", t("doctor.label.deploymentReachable"), health.ok
1086
+ ? t("doctor.health.ok", { url: normalizedEnvApiBaseUrl })
1087
+ : t("doctor.health.failed", {
1088
+ url: normalizedEnvApiBaseUrl,
1089
+ reason: health.message,
1090
+ }));
844
1091
  if (!envApiKey?.startsWith("rd_sk_")) {
845
- addCheck("warn", "API key accepted", "Skipped because no rd_sk_... key was found in this project's environment.");
1092
+ addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoKey"));
846
1093
  }
847
1094
  else if (!health.ok) {
848
- addCheck("warn", "API key accepted", "Skipped because the deployment did not answer.");
1095
+ addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoHealth"));
849
1096
  }
850
1097
  else {
851
1098
  const probe = await probeApiKey(runtime, normalizedEnvApiBaseUrl, envApiKey);
852
- addCheck(probe.ok ? "ok" : "error", "API key accepted", probe.ok
853
- ? `Key is live. ${probe.sessionCount} recent API ${probe.sessionCount === 1 ? "session" : "sessions"} visible.`
1099
+ addCheck(probe.ok ? "ok" : "error", t("doctor.label.apiKeyAccepted"), probe.ok
1100
+ ? t(probe.sessionCount === 1
1101
+ ? "doctor.key.liveOne"
1102
+ : "doctor.key.liveMany", { count: probe.sessionCount })
854
1103
  : probe.message);
855
1104
  }
856
1105
  }
857
1106
  // A stale global install is the quietest failure mode there is: npm pins the
858
1107
  // binary at install time and never revisits it, so doctor has to say so.
859
1108
  if (offline) {
860
- addCheck("warn", "CLI version", `Running ${CLI_VERSION}. Skipped the update check (--offline).`);
1109
+ addCheck("warn", t("doctor.label.cliVersion"), t("doctor.version.offline", { version: CLI_VERSION }));
861
1110
  }
862
1111
  else {
863
1112
  const update = await checkForUpdate(runtime, CLI_VERSION, { force: true });
864
- addCheck(!update || !update.updateAvailable ? "ok" : "warn", "CLI version", !update
865
- ? `Running ${CLI_VERSION}. Could not reach the npm registry to check for updates.`
1113
+ addCheck(!update || !update.updateAvailable ? "ok" : "warn", t("doctor.label.cliVersion"), !update
1114
+ ? t("doctor.version.unknown", { version: CLI_VERSION })
866
1115
  : update.updateAvailable
867
- ? `Running ${update.current}; ${update.latest} is published. Run remotedraw update.`
868
- : `Running ${update.current}, the latest published version.`);
1116
+ ? t("doctor.version.stale", {
1117
+ current: update.current,
1118
+ latest: update.latest,
1119
+ })
1120
+ : t("doctor.version.latest", { current: update.current }));
869
1121
  }
870
1122
  return format === "json"
871
1123
  ? jsonResult({
@@ -888,23 +1140,22 @@ async function devCommand(args, runtime) {
888
1140
  const projectEnv = await environmentForProject(dir, runtime);
889
1141
  const apiBaseUrl = readString(parsed, "api-base-url") ??
890
1142
  projectEnv.REMOTEDRAW_API_BASE_URL ??
891
- "http://localhost:3210";
1143
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
892
1144
  return ok([
893
- "RemoteDraw local development workflow",
1145
+ t("dev.title"),
894
1146
  "",
895
- `API base URL: ${apiBaseUrl}`,
1147
+ t("dev.apiBaseUrl", { url: apiBaseUrl }),
896
1148
  "",
897
- "1. Start the RemoteDraw API/web service for local testing.",
898
- " Inside this repository: bun run dev:codex",
1149
+ t("dev.hosted"),
899
1150
  "",
900
- "2. Start your app's dev server.",
901
- " Use the package manager and dev script generated by remotedraw new/init.",
1151
+ t("dev.step1"),
1152
+ t("dev.step1.detail"),
902
1153
  "",
903
- "3. Create a test input request from trusted backend code.",
904
- ` remotedraw create-input --preset signature --api-base-url ${apiBaseUrl}`,
1154
+ t("dev.step2"),
1155
+ ` remotedraw create-input --preset sketch --api-base-url ${apiBaseUrl}`,
905
1156
  "",
906
- "4. Show the returned HTTPS joinUrl in your receiver UI.",
907
- " Use the RemoteDraw iOS app for remotedraw-ios sender flows, or EmbeddedSender for owned web sender flows.",
1157
+ t("dev.step3"),
1158
+ t("dev.step3.detail"),
908
1159
  ].join("\n"));
909
1160
  }
910
1161
  async function createInputCommand(args, runtime) {
@@ -922,10 +1173,10 @@ async function createInputCommand(args, runtime) {
922
1173
  if (parsed.booleans.has("help"))
923
1174
  return ok(createInputHelpText());
924
1175
  const projectEnv = await environmentForProject(runtime.cwd, runtime);
925
- const preset = choice(readString(parsed, "preset") ?? "signature", presetChoices, "preset");
1176
+ const preset = choice(readString(parsed, "preset") ?? "sketch", presetChoices, "preset");
926
1177
  const apiBaseUrl = readString(parsed, "api-base-url") ??
927
1178
  projectEnv.REMOTEDRAW_API_BASE_URL ??
928
- "https://<deployment>.convex.site";
1179
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
929
1180
  const payload = createSessionPayload({
930
1181
  preset,
931
1182
  label: readString(parsed, "label") ?? labelForPreset(preset),
@@ -935,9 +1186,9 @@ async function createInputCommand(args, runtime) {
935
1186
  if (parsed.booleans.has("execute")) {
936
1187
  const apiKey = readString(parsed, "api-key") ?? projectEnv.REMOTEDRAW_API_KEY;
937
1188
  if (!runtime.fetch)
938
- throw new Error("This runtime cannot execute HTTP requests.");
1189
+ throw new Error(t("createInput.error.noFetch"));
939
1190
  if (!apiKey?.startsWith("rd_sk_")) {
940
- throw new Error("create-input --execute requires --api-key or REMOTEDRAW_API_KEY with an rd_sk_... server key.");
1191
+ throw new Error(t("createInput.error.noKey"));
941
1192
  }
942
1193
  const response = await runtime.fetch(`${normalizedOrigin(apiBaseUrl)}/v1/sessions`, {
943
1194
  method: "POST",
@@ -949,20 +1200,20 @@ async function createInputCommand(args, runtime) {
949
1200
  });
950
1201
  const text = await response.text();
951
1202
  if (!response.ok)
952
- throw new Error(text || `RemoteDraw API returned ${response.status}.`);
1203
+ throw new Error(text || t("createInput.error.apiStatus", { status: response.status }));
953
1204
  return ok(text);
954
1205
  }
955
1206
  if (parsed.booleans.has("json")) {
956
1207
  return ok(JSON.stringify(payload, null, 2));
957
1208
  }
958
1209
  return ok([
959
- "RemoteDraw test input request",
1210
+ t("createInput.title"),
960
1211
  "",
961
- "Create this from trusted backend code. Do not run API-key requests in a browser client.",
1212
+ t("createInput.warning"),
962
1213
  "",
963
1214
  curlForCreateInput(apiBaseUrl, payload),
964
1215
  "",
965
- "Use --json to print only the request body or --execute to call the API with REMOTEDRAW_API_KEY.",
1216
+ t("createInput.footer"),
966
1217
  ].join("\n"));
967
1218
  }
968
1219
  async function examplesCommand(args, runtime) {
@@ -975,14 +1226,14 @@ async function examplesCommand(args, runtime) {
975
1226
  const example = readString(parsed, "install");
976
1227
  if (example == null || parsed.booleans.has("list")) {
977
1228
  return ok([
978
- "RemoteDraw examples",
1229
+ t("examples.title"),
979
1230
  "",
980
- " react-ios Web receiver using the RemoteDraw iOS app as sender.",
981
- " react-owned-sender Web receiver plus EmbeddedSender for owned web input.",
982
- " raw-http Package-free backend/receiver/sender route fixtures.",
983
- " ios-owned-sender Swift join-link and sender request helper starter.",
1231
+ ` ${"react-ios".padEnd(19)} ${t("examples.reactIos")}`,
1232
+ ` ${"react-owned-sender".padEnd(19)} ${t("examples.reactOwnedSender")}`,
1233
+ ` ${"raw-http".padEnd(19)} ${t("examples.rawHttp")}`,
1234
+ ` ${"ios-owned-sender".padEnd(19)} ${t("examples.iosOwnedSender")}`,
984
1235
  "",
985
- "Install one:",
1236
+ t("examples.installOne"),
986
1237
  " remotedraw examples --install react-ios --path ./remotedraw-react-ios",
987
1238
  ].join("\n"));
988
1239
  }
@@ -1000,7 +1251,7 @@ async function examplesCommand(args, runtime) {
1000
1251
  dryRun,
1001
1252
  force: parsed.booleans.has("force"),
1002
1253
  });
1003
- return ok(scaffoldSummary("Installed example", plan, outputDir, written, dryRun));
1254
+ return ok(scaffoldSummary(t("scaffold.verb.installedExample"), plan, outputDir, written, dryRun));
1004
1255
  }
1005
1256
  async function agentCommand(args, runtime) {
1006
1257
  const parsed = parseArgs(args, {
@@ -1014,15 +1265,18 @@ async function agentCommand(args, runtime) {
1014
1265
  const targetDir = readString(parsed, "path");
1015
1266
  if (targetDir == null) {
1016
1267
  return ok([
1017
- "RemoteDraw agent setup",
1268
+ t("agent.title"),
1018
1269
  "",
1019
- "Use this when an AI coding agent needs to add RemoteDraw to a customer app.",
1270
+ t("agent.intro"),
1020
1271
  "",
1021
- "Print the skill:",
1272
+ t("agent.printSkill"),
1022
1273
  " remotedraw agent --print-skill",
1023
1274
  "",
1024
- "Install the skill file:",
1025
- " remotedraw agent --path ~/.codex/skills/remotedraw",
1275
+ t("agent.installSkill"),
1276
+ // Agent runtimes read skills from a per-project directory. `~/.codex`
1277
+ // is the retired location and installing there reaches nothing.
1278
+ " remotedraw agent --path .agents/skills/remotedraw",
1279
+ " remotedraw agent --path .claude/skills/remotedraw",
1026
1280
  ].join("\n"));
1027
1281
  }
1028
1282
  const outputDir = resolveAgentPath(targetDir, runtime);
@@ -1030,7 +1284,7 @@ async function agentCommand(args, runtime) {
1030
1284
  if (!parsed.booleans.has("dry-run") &&
1031
1285
  !parsed.booleans.has("force") &&
1032
1286
  (await runtime.exists(outputFile))) {
1033
- throw new Error("SKILL.md already exists. Pass --force to replace it.");
1287
+ throw new Error(t("agent.error.exists"));
1034
1288
  }
1035
1289
  if (!parsed.booleans.has("dry-run")) {
1036
1290
  await runtime.mkdir(outputDir);
@@ -1038,141 +1292,150 @@ async function agentCommand(args, runtime) {
1038
1292
  }
1039
1293
  return ok([
1040
1294
  parsed.booleans.has("dry-run")
1041
- ? `Would write ${outputFile}`
1042
- : `Installed RemoteDraw agent skill at ${outputFile}`,
1295
+ ? t("agent.wouldWrite", { path: outputFile })
1296
+ : t("agent.installed", { path: outputFile }),
1043
1297
  "",
1044
- "Next:",
1045
- " Ask your agent to use the RemoteDraw skill before editing API, CLI, sender, receiver, or iOS integration code.",
1298
+ t("word.next"),
1299
+ t("agent.next"),
1046
1300
  ].join("\n"));
1047
1301
  }
1048
1302
  function newHelpText() {
1303
+ const option = (flag, description) => ` ${flag.padEnd(25)} ${description}`;
1049
1304
  return [
1050
- "Usage:",
1051
- " remotedraw new",
1052
- " remotedraw new --app-name <name> [options]",
1053
- "",
1054
- "Run without options for the interactive setup flow.",
1055
- "",
1056
- "Options:",
1057
- " --path <dir> Output directory. Defaults to an app-name slug.",
1058
- " --target <kind> web, desktop, ios, or headless.",
1059
- " --sender <kind> remotedraw-ios, embedded-web, own-ios, or headless.",
1060
- " --sdk <kind> react, svelte, js, swift, or headless.",
1061
- " --preset <kind> signature or sketch. Legacy API presets remain accepted for existing integrations.",
1062
- " --package-manager <name> npm, pnpm, yarn, or bun.",
1063
- " --api-base-url <url> Default API origin for generated config.",
1064
- " --force Overwrite generated files and existing RemoteDraw .env.local values.",
1065
- " --offline Scaffold locally without login, dashboard project, or API key creation.",
1066
- " --non-interactive Never prompt; fail when required input is missing.",
1067
- " --format <text|json> Human-readable or machine-readable result.",
1068
- " --dry-run Print files that would be written.",
1305
+ t("word.usage"),
1306
+ t("new.help.usage.bare"),
1307
+ t("new.help.usage.named"),
1308
+ "",
1309
+ t("new.help.interactive"),
1310
+ "",
1311
+ t("word.options"),
1312
+ option("--path <dir>", t("new.help.option.path")),
1313
+ option("--target <kind>", t("new.help.option.target")),
1314
+ option("--sender <kind>", t("new.help.option.sender")),
1315
+ option("--sdk <kind>", t("new.help.option.sdk")),
1316
+ option("--preset <kind>", t("new.help.option.preset")),
1317
+ option("--package-manager <name>", t("new.help.option.packageManager")),
1318
+ option("--api-base-url <url>", t("new.help.option.apiBaseUrl")),
1319
+ option("--force", t("new.help.option.force")),
1320
+ option("--offline", t("new.help.option.offline")),
1321
+ option("--non-interactive", t("new.help.option.nonInteractive")),
1322
+ option("--format <text|json>", t("new.help.option.format")),
1323
+ option("--dry-run", t("new.help.option.dryRun")),
1324
+ option("--language <code>", t("new.help.option.language")),
1069
1325
  ].join("\n");
1070
1326
  }
1071
1327
  function initHelpText() {
1072
1328
  return [
1073
- "Usage:",
1074
- " remotedraw init [options]",
1329
+ t("word.usage"),
1330
+ t("init.help.usage"),
1075
1331
  "",
1076
- "Options match remotedraw new. init writes remotedraw.config.json, .env.example, and src/remotedraw starter files into an existing project.",
1332
+ t("init.help.body"),
1077
1333
  ].join("\n");
1078
1334
  }
1079
- function loginHelpText() {
1335
+ function languageHelpText() {
1080
1336
  return [
1081
- "Authorize the RemoteDraw CLI",
1337
+ t("language.help.title"),
1338
+ "",
1339
+ t("word.usage"),
1340
+ t("language.help.usage.list"),
1341
+ t("language.help.usage.set"),
1082
1342
  "",
1083
- "Usage:",
1084
- " remotedraw login [--no-open] [--force] [--api-base-url <origin>]",
1343
+ t("language.help.body"),
1344
+ t("language.help.override"),
1085
1345
  "",
1086
- "The browser confirms your account. A revocable CLI credential is stored in your user config directory, never in the project.",
1346
+ t("language.available"),
1347
+ ...SUPPORTED_LOCALES.map((locale) => ` ${locale.padEnd(6)} ${localeEndonym(locale)}`),
1087
1348
  ].join("\n");
1088
1349
  }
1089
- function logoutHelpText() {
1350
+ function loginHelpText() {
1090
1351
  return [
1091
- "Revoke the RemoteDraw CLI credential",
1352
+ t("login.help.title"),
1353
+ "",
1354
+ t("word.usage"),
1355
+ t("login.help.usage"),
1092
1356
  "",
1093
- "Usage:",
1094
- " remotedraw logout [--api-base-url <origin>]",
1357
+ t("login.help.body"),
1095
1358
  ].join("\n");
1096
1359
  }
1360
+ function logoutHelpText() {
1361
+ return [t("logout.help.title"), "", t("word.usage"), t("logout.help.usage")].join("\n");
1362
+ }
1097
1363
  function whoamiHelpText() {
1098
- return [
1099
- "Show the active RemoteDraw CLI account",
1100
- "",
1101
- "Usage:",
1102
- " remotedraw whoami [--api-base-url <origin>]",
1103
- ].join("\n");
1364
+ return [t("whoami.help.title"), "", t("word.usage"), t("whoami.help.usage")].join("\n");
1104
1365
  }
1105
1366
  function doctorHelpText() {
1106
1367
  return [
1107
- "Usage:",
1108
- " remotedraw doctor [--path <dir>] [--api-base-url <url>] [--format text|json] [--offline]",
1109
- "",
1110
- "Checks local config, package.json dependencies, and the REMOTEDRAW_API_BASE_URL / REMOTEDRAW_API_KEY environment variables.",
1111
- `Without an override the API base URL is ${DEFAULT_REMOTEDRAW_API_BASE_URL}; doctor names the source so a non-production URL is visible.`,
1112
- "Then reaches the deployment: GET /health, and a free POST /v1/sessions/list to confirm the API key is live.",
1113
- "It also compares this CLI against the latest published version.",
1114
- "Pass --offline to skip the network checks.",
1368
+ t("word.usage"),
1369
+ t("doctor.help.usage"),
1370
+ "",
1371
+ t("doctor.help.checks"),
1372
+ t("doctor.help.baseUrl", { url: DEFAULT_REMOTEDRAW_API_BASE_URL }),
1373
+ t("doctor.help.network"),
1374
+ t("doctor.help.version"),
1375
+ t("doctor.help.offline"),
1115
1376
  ].join("\n");
1116
1377
  }
1117
1378
  function updateHelpText() {
1118
1379
  return [
1119
- "Usage:",
1120
- " remotedraw update [--check] [--format text|json]",
1380
+ t("word.usage"),
1381
+ t("update.help.usage"),
1121
1382
  "",
1122
- `Compares this CLI against the latest ${CLI_PACKAGE_NAME} on the npm registry and installs it with the package manager that owns this binary.`,
1123
- "Global installs never refresh on their own, so this is the only in-place update path.",
1124
- "Pass --check to report the available version without installing.",
1383
+ t("update.help.body", { package: CLI_PACKAGE_NAME }),
1384
+ t("update.help.global"),
1385
+ t("update.help.check"),
1125
1386
  "",
1126
- "The passive 'update available' notice on other commands can be silenced with REMOTEDRAW_CLI_NO_UPDATE_CHECK=1.",
1387
+ t("update.help.silence"),
1127
1388
  ].join("\n");
1128
1389
  }
1129
1390
  function optionsHelpText() {
1130
1391
  return [
1131
- "Usage:",
1392
+ t("word.usage"),
1132
1393
  " remotedraw options [--format text|json]",
1133
1394
  "",
1134
- "Prints every setup choice, compatibility rule, default, explanation, and documentation URL.",
1395
+ t("options.help.body"),
1135
1396
  ].join("\n");
1136
1397
  }
1137
1398
  function devHelpText() {
1138
1399
  return [
1139
- "Usage:",
1140
- " remotedraw dev [--api-base-url <url>]",
1400
+ t("word.usage"),
1401
+ t("dev.help.usage"),
1141
1402
  "",
1142
- "Prints the local test workflow for pairing, input request creation, and receiver rendering.",
1403
+ t("dev.help.body"),
1143
1404
  ].join("\n");
1144
1405
  }
1145
1406
  function createInputHelpText() {
1407
+ const option = (flag, description) => ` ${flag.padEnd(21)} ${description}`;
1146
1408
  return [
1147
- "Usage:",
1148
- " remotedraw create-input [--preset signature] [--json|--curl|--execute]",
1149
- "",
1150
- "Options:",
1151
- " --preset <kind> Session/input preset.",
1152
- " --label <text> Receiver target label.",
1153
- " --external-id <id> App-owned correlation id.",
1154
- " --api-base-url <url> RemoteDraw API origin.",
1155
- " --api-key <key> Server API key for --execute.",
1156
- " --expires-in-ms <n> Optional session TTL.",
1409
+ t("word.usage"),
1410
+ t("createInput.help.usage"),
1411
+ "",
1412
+ t("word.options"),
1413
+ option("--preset <kind>", t("createInput.help.option.preset")),
1414
+ option("--label <text>", t("createInput.help.option.label")),
1415
+ option("--external-id <id>", t("createInput.help.option.externalId")),
1416
+ option("--api-base-url <url>", t("createInput.help.option.apiBaseUrl")),
1417
+ option("--api-key <key>", t("createInput.help.option.apiKey")),
1418
+ option("--expires-in-ms <n>", t("createInput.help.option.expiresInMs")),
1157
1419
  ].join("\n");
1158
1420
  }
1159
1421
  function examplesHelpText() {
1160
1422
  return [
1161
- "Usage:",
1162
- " remotedraw examples [--list]",
1163
- " remotedraw examples --install <example> --path <dir>",
1423
+ t("word.usage"),
1424
+ t("examples.help.usage.list"),
1425
+ t("examples.help.usage.install"),
1164
1426
  "",
1165
- `Examples: ${exampleChoices.join(", ")}`,
1427
+ t("word.examples", { list: exampleChoices.join(", ") }),
1166
1428
  ].join("\n");
1167
1429
  }
1168
1430
  function agentHelpText() {
1169
1431
  return [
1170
- "Usage:",
1171
- " remotedraw agent",
1172
- " remotedraw agent --print-skill",
1173
- " remotedraw agent --path <dir> [--force] [--dry-run]",
1432
+ t("word.usage"),
1433
+ t("agent.help.usage.bare"),
1434
+ t("agent.help.usage.print"),
1435
+ t("agent.help.usage.install"),
1174
1436
  "",
1175
- "Writes a SKILL.md file that tells coding agents how to choose an SDK, use the CLI, keep API keys server-side, and verify RemoteDraw integrations.",
1437
+ t("agent.help.body"),
1438
+ t("agent.help.paths"),
1176
1439
  ].join("\n");
1177
1440
  }
1178
1441
  function parseArgs(args, allowed) {
@@ -1195,17 +1458,17 @@ function parseArgs(args, allowed) {
1195
1458
  const inlineValue = equalsIndex === -1 ? undefined : withoutPrefix.slice(equalsIndex + 1);
1196
1459
  if (booleanFlags.has(name)) {
1197
1460
  if (inlineValue != null) {
1198
- throw new Error(`--${name} does not accept a value.`);
1461
+ throw cliError("INVALID_ARGUMENT", "error.optionNoValue", { name });
1199
1462
  }
1200
1463
  booleans.add(name);
1201
1464
  continue;
1202
1465
  }
1203
1466
  if (!valueFlags.has(name)) {
1204
- throw new Error(`Unsupported option "--${name}".`);
1467
+ throw cliError("INVALID_ARGUMENT", "error.unsupportedOption", { name });
1205
1468
  }
1206
1469
  const value = inlineValue ?? args[index + 1];
1207
1470
  if (value == null || value.startsWith("--")) {
1208
- throw new Error(`--${name} requires a value.`);
1471
+ throw cliError("MISSING_ARGUMENT", "error.optionNeedsValue", { name });
1209
1472
  }
1210
1473
  values.set(name, value);
1211
1474
  if (inlineValue == null)
@@ -1222,7 +1485,7 @@ function optionalInteger(value) {
1222
1485
  return undefined;
1223
1486
  const number = Number(value);
1224
1487
  if (!Number.isInteger(number) || number <= 0) {
1225
- throw new Error("--expires-in-ms requires a positive integer.");
1488
+ throw cliError("INVALID_ARGUMENT", "error.expiresInMs");
1226
1489
  }
1227
1490
  return number;
1228
1491
  }
@@ -1239,16 +1502,16 @@ async function promptForNewProject(parsed, runtime) {
1239
1502
  defaultAppName: "RemoteDraw app",
1240
1503
  });
1241
1504
  }
1242
- await prompts.intro?.("RemoteDraw project setup");
1505
+ await prompts.intro?.(t("prompt.intro.setup"));
1243
1506
  const appName = readString(parsed, "app-name") ??
1244
1507
  (await prompts.text({
1245
- message: "Project name",
1508
+ message: t("field.appName"),
1246
1509
  defaultValue: "RemoteDraw App",
1247
1510
  validate: validateProjectName,
1248
1511
  }));
1249
1512
  const target = readString(parsed, "target") == null
1250
1513
  ? await prompts.select({
1251
- message: "Project type",
1514
+ message: t("field.target"),
1252
1515
  defaultValue: "web",
1253
1516
  choices: targetPromptChoices(),
1254
1517
  })
@@ -1261,25 +1524,19 @@ async function promptForNewProject(parsed, runtime) {
1261
1524
  : choice(readString(parsed, "sdk"), sdkChoices, "sdk");
1262
1525
  const preset = readString(parsed, "preset") == null
1263
1526
  ? await prompts.select({
1264
- message: "Starter experience",
1265
- defaultValue: "signature",
1527
+ message: t("field.preset"),
1528
+ defaultValue: "sketch",
1266
1529
  choices: presetPromptChoices(),
1267
1530
  })
1268
1531
  : choice(readString(parsed, "preset"), presetChoices, "preset");
1269
1532
  const packageManager = readString(parsed, "package-manager") == null
1270
1533
  ? await prompts.select({
1271
- message: "Package manager",
1534
+ message: t("field.packageManager"),
1272
1535
  defaultValue: "npm",
1273
1536
  choices: packageManagerPromptChoices(),
1274
1537
  })
1275
1538
  : choice(readString(parsed, "package-manager"), packageManagerChoices, "package-manager");
1276
- const apiBaseUrl = readString(parsed, "api-base-url") ??
1277
- apiBaseUrlForEndpoint(await prompts.select({
1278
- message: "API endpoint",
1279
- helperText: "Use the placeholder unless you are wiring local dev now.",
1280
- defaultValue: "deployment",
1281
- choices: apiEndpointPromptChoices(),
1282
- }));
1539
+ const apiBaseUrl = readString(parsed, "api-base-url") ?? DEFAULT_REMOTEDRAW_API_BASE_URL;
1283
1540
  validatePlan({ target, sender, sdk });
1284
1541
  return {
1285
1542
  appName,
@@ -1293,13 +1550,13 @@ async function promptForNewProject(parsed, runtime) {
1293
1550
  };
1294
1551
  }
1295
1552
  function validateProjectName(value) {
1296
- return value.trim() === "" ? "Enter a project name." : undefined;
1553
+ return value.trim() === "" ? t("field.appName.error") : undefined;
1297
1554
  }
1298
1555
  async function promptForSender(prompts, target) {
1299
1556
  const choices = senderPromptChoices(target);
1300
1557
  return await prompts.select({
1301
- message: "Phone sender",
1302
- helperText: "Choose where drawing input will run.",
1558
+ message: t("field.sender"),
1559
+ helperText: t("field.sender.helper"),
1303
1560
  defaultValue: defaultPromptValue(defaultSenderForTarget(target), choices),
1304
1561
  choices,
1305
1562
  });
@@ -1307,8 +1564,8 @@ async function promptForSender(prompts, target) {
1307
1564
  async function promptForSdk(prompts, target, sender) {
1308
1565
  const choices = sdkPromptChoices(target, sender);
1309
1566
  return await prompts.select({
1310
- message: "UI framework / SDK",
1311
- helperText: "Pick the SDK that matches your receiver UI.",
1567
+ message: t("field.sdk"),
1568
+ helperText: t("field.sdk.helper"),
1312
1569
  defaultValue: defaultPromptValue(defaultSdkForChoices(target, sender), choices),
1313
1570
  choices,
1314
1571
  });
@@ -1320,10 +1577,10 @@ function defaultPromptValue(preferred, choices) {
1320
1577
  }
1321
1578
  function targetPromptChoices() {
1322
1579
  return [
1323
- { value: "web", label: "Web app" },
1324
- { value: "desktop", label: "Desktop or custom app" },
1325
- { value: "ios", label: "iOS sender app" },
1326
- { value: "headless", label: "Headless service" },
1580
+ { value: "web", label: t("choice.target.web.label") },
1581
+ { value: "desktop", label: t("choice.target.desktop.label") },
1582
+ { value: "ios", label: t("choice.target.ios.label") },
1583
+ { value: "headless", label: t("choice.target.headless.label") },
1327
1584
  ];
1328
1585
  }
1329
1586
  const DOCS_INTEGRATION_PATHS = "https://docs.remotedraw.com/docs#paths";
@@ -1331,103 +1588,112 @@ const DOCS_SDKS = "https://docs.remotedraw.com/docs/sdks#components";
1331
1588
  const DOCS_PAYLOADS = "https://docs.remotedraw.com/docs/api#payloads";
1332
1589
  const DOCS_ENDPOINTS = "https://docs.remotedraw.com/docs/api#endpoints";
1333
1590
  const DOCS_AUTH = "https://docs.remotedraw.com/docs/api#auth";
1334
- const targetWizardDetails = {
1335
- web: {
1336
- summary: "Voor webapps met een tekenoppervlak in de browser.",
1337
- explanation: "Kies dit als je website of webapp de tekeningen toont. De telefoon kan de RemoteDraw-iOS-app of een ingebouwde webzender gebruiken.",
1338
- docsUrl: DOCS_INTEGRATION_PATHS,
1339
- docsLabel: "Webapp-docs openen",
1340
- },
1341
- desktop: {
1342
- summary: "Voor desktopapps en andere maatwerkinterfaces.",
1343
- explanation: "Kies dit voor Electron, Tauri, een native desktopapp of een bestaande niet-webinterface. Je koppelt de ontvanger via de JavaScript-client of HTTP-API.",
1344
- docsUrl: DOCS_INTEGRATION_PATHS,
1345
- docsLabel: "Desktop-docs openen",
1346
- },
1347
- ios: {
1348
- summary: "Voor een eigen iOS-app die als telefoonzender werkt.",
1349
- explanation: "Kies dit als je zelf de native tekenervaring, navigatie en vormgeving op de iPhone beheert. De wizard maakt Swift-helpers aan.",
1350
- docsUrl: DOCS_INTEGRATION_PATHS,
1351
- docsLabel: "iOS-docs openen",
1352
- },
1353
- headless: {
1354
- summary: "Voor backends, automatisering en diensten zonder interface.",
1355
- explanation: "Kies dit als je sessies en tekengegevens rechtstreeks via de HTTP-API verwerkt en geen standaard ontvanger- of zenderinterface nodig hebt.",
1356
- docsUrl: DOCS_ENDPOINTS,
1357
- docsLabel: "Headless-docs openen",
1358
- },
1359
- };
1591
+ // Detail bundles are built per call, not frozen at module load: the active
1592
+ // locale is only known once `runCli` has parsed the flag, the environment, and
1593
+ // the stored choice.
1594
+ function targetWizardDetails() {
1595
+ return {
1596
+ web: {
1597
+ summary: t("choice.target.web.summary"),
1598
+ explanation: t("choice.target.web.explanation"),
1599
+ docsUrl: DOCS_INTEGRATION_PATHS,
1600
+ docsLabel: t("choice.target.web.docs"),
1601
+ },
1602
+ desktop: {
1603
+ summary: t("choice.target.desktop.summary"),
1604
+ explanation: t("choice.target.desktop.explanation"),
1605
+ docsUrl: DOCS_INTEGRATION_PATHS,
1606
+ docsLabel: t("choice.target.desktop.docs"),
1607
+ },
1608
+ ios: {
1609
+ summary: t("choice.target.ios.summary"),
1610
+ explanation: t("choice.target.ios.explanation"),
1611
+ docsUrl: DOCS_INTEGRATION_PATHS,
1612
+ docsLabel: t("choice.target.ios.docs"),
1613
+ },
1614
+ headless: {
1615
+ summary: t("choice.target.headless.summary"),
1616
+ explanation: t("choice.target.headless.explanation"),
1617
+ docsUrl: DOCS_ENDPOINTS,
1618
+ docsLabel: t("choice.target.headless.docs"),
1619
+ },
1620
+ };
1621
+ }
1360
1622
  function wizardChoices(choices, details) {
1361
- return dutchWizardChoices(choices).map((choiceOption) => ({
1623
+ return choices.map((choiceOption) => ({
1362
1624
  ...choiceOption,
1363
1625
  details: details[choiceOption.value],
1364
1626
  }));
1365
1627
  }
1366
- const senderWizardDetails = {
1367
- "remotedraw-ios": {
1368
- summary: "De officiële RemoteDraw-app is de tekenzender op de telefoon.",
1369
- explanation: "Dit is de snelste route. Je ontvanger toont een QR-code of deelnamelink; de RemoteDraw-iOS-app verzorgt de tekeninterface en synchronisatie.",
1370
- docsUrl: DOCS_INTEGRATION_PATHS,
1371
- docsLabel: "iOS-zenderdocs openen",
1372
- },
1373
- "embedded-web": {
1374
- summary: "Een webzender die onderdeel is van je eigen product.",
1375
- explanation: "Kies dit als je de telefooninterface zelf wilt vormgeven in React of een andere webstack. Je app verwerkt de deelnamelink en zenderstatus.",
1376
- docsUrl: DOCS_INTEGRATION_PATHS,
1377
- docsLabel: "Webzenderdocs openen",
1378
- },
1379
- "own-ios": {
1380
- summary: "Een native iOS-zender die je volledig zelf beheert.",
1381
- explanation: "Kies dit voor eigen navigatie, branding, pushberichten of app-routing. Je ontvangt Swift-helpers voor deelnamelinks en zenderverzoeken.",
1382
- docsUrl: DOCS_INTEGRATION_PATHS,
1383
- docsLabel: "Swift-zenderdocs openen",
1384
- },
1385
- headless: {
1386
- summary: "Een eigen zender zonder door RemoteDraw geleverde interface.",
1387
- explanation: "Kies dit als je rechtstreeks met de HTTP-zenderroutes werkt, bijvoorbeeld vanuit automatisering, hardware of een volledig eigen client.",
1388
- docsUrl: DOCS_ENDPOINTS,
1389
- docsLabel: "HTTP-zenderdocs openen",
1390
- },
1391
- };
1392
- const sdkWizardDetails = {
1393
- react: {
1394
- summary: "React-componenten voor koppeling, ontvanger en zender.",
1395
- explanation: "Kies dit voor React of Next.js. De starter gebruikt de componenten en hooks van @remotedraw/react voor sessiestatus en live tekeningen.",
1396
- docsUrl: DOCS_SDKS,
1397
- docsLabel: "React SDK-docs openen",
1398
- },
1399
- svelte: {
1400
- summary: "Een Svelte-store bovenop de frameworkvrije client.",
1401
- explanation: "Kies dit voor Svelte of SvelteKit. Je beheert zelf de markup en bindt sessie-, teken- en zenderstatus via de Svelte-store.",
1402
- docsUrl: DOCS_SDKS,
1403
- docsLabel: "Svelte SDK-docs openen",
1404
- },
1405
- js: {
1406
- summary: "Frameworkvrije JavaScript-clients en protocoltypen.",
1407
- explanation: "Kies dit voor desktopapps, andere webframeworks of maatwerkclients. De starter gebruikt fetch en de gedeelde RemoteDraw-contracten.",
1408
- docsUrl: DOCS_ENDPOINTS,
1409
- docsLabel: "JavaScript-docs openen",
1410
- },
1411
- swift: {
1412
- summary: "Native Swift-helpers voor een eigen iOS-zender.",
1413
- explanation: "Kies dit als je zender in Swift of SwiftUI bouwt. De starter bevat parsing van deelnamelinks en getypeerde zenderverzoeken.",
1414
- docsUrl: DOCS_SDKS,
1415
- docsLabel: "Swift SDK-docs openen",
1416
- },
1417
- headless: {
1418
- summary: "Alleen configuratie, zonder UI-pakket.",
1419
- explanation: "Kies dit als een andere taal of eigen client de HTTP-API aanroept. De wizard voegt geen frameworkafhankelijkheid toe.",
1420
- docsUrl: DOCS_ENDPOINTS,
1421
- docsLabel: "HTTP API-docs openen",
1422
- },
1423
- };
1628
+ function senderWizardDetails() {
1629
+ return {
1630
+ "remotedraw-ios": {
1631
+ summary: t("choice.sender.remotedrawIos.summary"),
1632
+ explanation: t("choice.sender.remotedrawIos.explanation"),
1633
+ docsUrl: DOCS_INTEGRATION_PATHS,
1634
+ docsLabel: t("choice.sender.remotedrawIos.docs"),
1635
+ },
1636
+ "embedded-web": {
1637
+ summary: t("choice.sender.embeddedWeb.summary"),
1638
+ explanation: t("choice.sender.embeddedWeb.explanation"),
1639
+ docsUrl: DOCS_INTEGRATION_PATHS,
1640
+ docsLabel: t("choice.sender.embeddedWeb.docs"),
1641
+ },
1642
+ "own-ios": {
1643
+ summary: t("choice.sender.ownIos.summary"),
1644
+ explanation: t("choice.sender.ownIos.explanation"),
1645
+ docsUrl: DOCS_INTEGRATION_PATHS,
1646
+ docsLabel: t("choice.sender.ownIos.docs"),
1647
+ },
1648
+ headless: {
1649
+ summary: t("choice.sender.headless.summary"),
1650
+ explanation: t("choice.sender.headless.explanation"),
1651
+ docsUrl: DOCS_ENDPOINTS,
1652
+ docsLabel: t("choice.sender.headless.docs"),
1653
+ },
1654
+ };
1655
+ }
1656
+ function sdkWizardDetails() {
1657
+ return {
1658
+ react: {
1659
+ summary: t("choice.sdk.react.summary"),
1660
+ explanation: t("choice.sdk.react.explanation"),
1661
+ docsUrl: DOCS_SDKS,
1662
+ docsLabel: t("choice.sdk.react.docs"),
1663
+ },
1664
+ svelte: {
1665
+ summary: t("choice.sdk.svelte.summary"),
1666
+ explanation: t("choice.sdk.svelte.explanation"),
1667
+ docsUrl: DOCS_SDKS,
1668
+ docsLabel: t("choice.sdk.svelte.docs"),
1669
+ },
1670
+ js: {
1671
+ summary: t("choice.sdk.js.summary"),
1672
+ explanation: t("choice.sdk.js.explanation"),
1673
+ docsUrl: DOCS_ENDPOINTS,
1674
+ docsLabel: t("choice.sdk.js.docs"),
1675
+ },
1676
+ swift: {
1677
+ summary: t("choice.sdk.swift.summary"),
1678
+ explanation: t("choice.sdk.swift.explanation"),
1679
+ docsUrl: DOCS_SDKS,
1680
+ docsLabel: t("choice.sdk.swift.docs"),
1681
+ },
1682
+ headless: {
1683
+ summary: t("choice.sdk.headless.summary"),
1684
+ explanation: t("choice.sdk.headless.explanation"),
1685
+ docsUrl: DOCS_ENDPOINTS,
1686
+ docsLabel: t("choice.sdk.headless.docs"),
1687
+ },
1688
+ };
1689
+ }
1424
1690
  function senderPromptChoices(target) {
1425
1691
  if (target === "ios") {
1426
1692
  return [
1427
1693
  {
1428
1694
  value: "own-ios",
1429
- label: "Own iOS sender",
1430
- hint: "Use Swift join-link and sender request helpers.",
1695
+ label: t("choice.sender.ownIos.label"),
1696
+ hint: t("choice.sender.ownIos.hint"),
1431
1697
  },
1432
1698
  ];
1433
1699
  }
@@ -1435,8 +1701,8 @@ function senderPromptChoices(target) {
1435
1701
  return [
1436
1702
  {
1437
1703
  value: "headless",
1438
- label: "Headless/custom sender",
1439
- hint: "No hosted phone UI.",
1704
+ label: t("choice.sender.headless.label"),
1705
+ hint: t("choice.sender.headless.hint.noUi"),
1440
1706
  },
1441
1707
  ];
1442
1708
  }
@@ -1444,26 +1710,26 @@ function senderPromptChoices(target) {
1444
1710
  return [
1445
1711
  {
1446
1712
  value: "remotedraw-ios",
1447
- label: "RemoteDraw iOS app",
1448
- hint: "Show a join URL or QR code from your receiver.",
1713
+ label: t("choice.sender.remotedrawIos.label"),
1714
+ hint: t("choice.sender.remotedrawIos.hint.joinUrl"),
1449
1715
  },
1450
1716
  {
1451
1717
  value: "headless",
1452
- label: "Headless/custom sender",
1453
- hint: "Build directly on the HTTP sender routes.",
1718
+ label: t("choice.sender.headless.label"),
1719
+ hint: t("choice.sender.headless.hint.httpRoutes"),
1454
1720
  },
1455
1721
  ];
1456
1722
  }
1457
1723
  return [
1458
1724
  {
1459
1725
  value: "remotedraw-ios",
1460
- label: "RemoteDraw iOS app",
1461
- hint: "Fastest path: scan the receiver QR code.",
1726
+ label: t("choice.sender.remotedrawIos.label"),
1727
+ hint: t("choice.sender.remotedrawIos.hint.scan"),
1462
1728
  },
1463
1729
  {
1464
1730
  value: "embedded-web",
1465
- label: "Own web sender",
1466
- hint: "Use a web sender component or raw sender helpers.",
1731
+ label: t("choice.sender.embeddedWeb.label"),
1732
+ hint: t("choice.sender.embeddedWeb.hint"),
1467
1733
  },
1468
1734
  ];
1469
1735
  }
@@ -1472,8 +1738,8 @@ function sdkPromptChoices(target, sender) {
1472
1738
  return [
1473
1739
  {
1474
1740
  value: "swift",
1475
- label: "Swift helpers",
1476
- hint: "For customer-owned iOS sender apps.",
1741
+ label: t("choice.sdk.swift.label"),
1742
+ hint: t("choice.sdk.swift.hint"),
1477
1743
  },
1478
1744
  ];
1479
1745
  }
@@ -1481,13 +1747,13 @@ function sdkPromptChoices(target, sender) {
1481
1747
  return [
1482
1748
  {
1483
1749
  value: "js",
1484
- label: "JavaScript client",
1485
- hint: "Framework-free HTTP clients and protocol types.",
1750
+ label: t("choice.sdk.js.label.client"),
1751
+ hint: t("choice.sdk.js.hint.client"),
1486
1752
  },
1487
1753
  {
1488
1754
  value: "headless",
1489
- label: "Config only",
1490
- hint: "No UI package dependency.",
1755
+ label: t("choice.sdk.headless.label"),
1756
+ hint: t("choice.sdk.headless.hint"),
1491
1757
  },
1492
1758
  ];
1493
1759
  }
@@ -1495,40 +1761,40 @@ function sdkPromptChoices(target, sender) {
1495
1761
  return [
1496
1762
  {
1497
1763
  value: "js",
1498
- label: "JavaScript client",
1499
- hint: "Framework-free HTTP clients and protocol types.",
1764
+ label: t("choice.sdk.js.label.client"),
1765
+ hint: t("choice.sdk.js.hint.client"),
1500
1766
  },
1501
1767
  ];
1502
1768
  }
1503
1769
  return [
1504
1770
  {
1505
1771
  value: "react",
1506
- label: "React SDK",
1507
- hint: "Receiver, pairing, and optional embedded sender components.",
1772
+ label: t("choice.sdk.react.label"),
1773
+ hint: t("choice.sdk.react.hint"),
1508
1774
  },
1509
1775
  {
1510
1776
  value: "svelte",
1511
- label: "Svelte SDK",
1512
- hint: "Svelte receiver store plus shared HTTP clients.",
1777
+ label: t("choice.sdk.svelte.label"),
1778
+ hint: t("choice.sdk.svelte.hint"),
1513
1779
  },
1514
1780
  {
1515
1781
  value: "js",
1516
- label: "No framework",
1517
- hint: "Framework-free clients and raw HTTP starter files.",
1782
+ label: t("choice.sdk.js.label.noFramework"),
1783
+ hint: t("choice.sdk.js.hint.noFramework"),
1518
1784
  },
1519
1785
  ];
1520
1786
  }
1521
1787
  function presetPromptChoices() {
1522
1788
  return [
1523
1789
  {
1524
- value: "signature",
1525
- label: "Signature strip",
1526
- hint: "Product-ready branded field with exact surface mapping.",
1790
+ value: "sketch",
1791
+ label: t("choice.preset.sketch.label"),
1792
+ hint: t("choice.preset.sketch.hint"),
1527
1793
  },
1528
1794
  {
1529
- value: "sketch",
1530
- label: "Custom drawing surface",
1531
- hint: "Flexible normalized input for your own receiver UI.",
1795
+ value: "screenMarkup",
1796
+ label: t("choice.preset.screenMarkup.label"),
1797
+ hint: t("choice.preset.screenMarkup.hint"),
1532
1798
  },
1533
1799
  ];
1534
1800
  }
@@ -1538,122 +1804,86 @@ function packageManagerPromptChoices() {
1538
1804
  label: packageManager,
1539
1805
  }));
1540
1806
  }
1541
- function apiEndpointPromptChoices() {
1542
- return [
1543
- {
1544
- value: "deployment",
1545
- label: "Deployment URL placeholder",
1546
- hint: "Fill in your Convex site origin later.",
1807
+ function presetWizardDetails() {
1808
+ return {
1809
+ approval: {
1810
+ summary: t("choice.preset.approval.summary"),
1811
+ explanation: t("choice.preset.approval.explanation"),
1812
+ docsUrl: DOCS_PAYLOADS,
1813
+ docsLabel: t("choice.preset.approval.docs"),
1547
1814
  },
1548
- {
1549
- value: "local",
1550
- label: "Local dev server",
1551
- hint: "Use http://localhost:3210.",
1815
+ sketch: {
1816
+ summary: t("choice.preset.sketch.summary"),
1817
+ explanation: t("choice.preset.sketch.explanation"),
1818
+ docsUrl: DOCS_PAYLOADS,
1819
+ docsLabel: t("choice.preset.sketch.docs"),
1552
1820
  },
1553
- ];
1821
+ photoMarkup: {
1822
+ summary: t("choice.preset.photoMarkup.summary"),
1823
+ explanation: t("choice.preset.photoMarkup.explanation"),
1824
+ docsUrl: DOCS_PAYLOADS,
1825
+ docsLabel: t("choice.preset.photoMarkup.docs"),
1826
+ },
1827
+ pdfMarkup: {
1828
+ summary: t("choice.preset.pdfMarkup.summary"),
1829
+ explanation: t("choice.preset.pdfMarkup.explanation"),
1830
+ docsUrl: DOCS_PAYLOADS,
1831
+ docsLabel: t("choice.preset.pdfMarkup.docs"),
1832
+ },
1833
+ mapMarkup: {
1834
+ summary: t("choice.preset.mapMarkup.summary"),
1835
+ explanation: t("choice.preset.mapMarkup.explanation"),
1836
+ docsUrl: DOCS_PAYLOADS,
1837
+ docsLabel: t("choice.preset.mapMarkup.docs"),
1838
+ },
1839
+ screenMarkup: {
1840
+ summary: t("choice.preset.screenMarkup.summary"),
1841
+ explanation: t("choice.preset.screenMarkup.explanation"),
1842
+ docsUrl: DOCS_PAYLOADS,
1843
+ docsLabel: t("choice.preset.screenMarkup.docs"),
1844
+ },
1845
+ designReview: {
1846
+ summary: t("choice.preset.designReview.summary"),
1847
+ explanation: t("choice.preset.designReview.explanation"),
1848
+ docsUrl: DOCS_PAYLOADS,
1849
+ docsLabel: t("choice.preset.designReview.docs"),
1850
+ },
1851
+ pointer: {
1852
+ summary: t("choice.preset.pointer.summary"),
1853
+ explanation: t("choice.preset.pointer.explanation"),
1854
+ docsUrl: DOCS_PAYLOADS,
1855
+ docsLabel: t("choice.preset.pointer.docs"),
1856
+ },
1857
+ };
1858
+ }
1859
+ function packageManagerWizardDetails() {
1860
+ return {
1861
+ npm: {
1862
+ summary: t("choice.packageManager.npm.summary"),
1863
+ explanation: t("choice.packageManager.npm.explanation"),
1864
+ docsUrl: "https://docs.npmjs.com/",
1865
+ docsLabel: t("choice.packageManager.npm.docs"),
1866
+ },
1867
+ pnpm: {
1868
+ summary: t("choice.packageManager.pnpm.summary"),
1869
+ explanation: t("choice.packageManager.pnpm.explanation"),
1870
+ docsUrl: "https://pnpm.io/",
1871
+ docsLabel: t("choice.packageManager.pnpm.docs"),
1872
+ },
1873
+ yarn: {
1874
+ summary: t("choice.packageManager.yarn.summary"),
1875
+ explanation: t("choice.packageManager.yarn.explanation"),
1876
+ docsUrl: "https://yarnpkg.com/getting-started/usage",
1877
+ docsLabel: t("choice.packageManager.yarn.docs"),
1878
+ },
1879
+ bun: {
1880
+ summary: t("choice.packageManager.bun.summary"),
1881
+ explanation: t("choice.packageManager.bun.explanation"),
1882
+ docsUrl: "https://bun.com/docs/pm/cli/install",
1883
+ docsLabel: t("choice.packageManager.bun.docs"),
1884
+ },
1885
+ };
1554
1886
  }
1555
- const presetWizardDetails = {
1556
- signature: {
1557
- summary: "De productklare RemoteDraw-handtekeningstrook.",
1558
- explanation: "Kies dit voor de complete, merkbare SignatureStrip met een exact passend telefoonoppervlak. Je kunt logo, tekst, kleur en omliggende formulier-UI aanpassen.",
1559
- docsUrl: DOCS_PAYLOADS,
1560
- docsLabel: "Handtekeningdocs openen",
1561
- },
1562
- initials: {
1563
- summary: "Een klein tekenveld voor initialen.",
1564
- explanation: "Gebruik dit wanneer iemand op één of meerdere plekken korte initialen moet plaatsen, bijvoorbeeld bij documentcontrole.",
1565
- docsUrl: DOCS_PAYLOADS,
1566
- docsLabel: "Initialendocs openen",
1567
- },
1568
- approval: {
1569
- summary: "Vrije tekeninvoer voor een visueel akkoord.",
1570
- explanation: "Gebruik dit voor goedkeuringen waarbij een vink, paraaf of korte markering voldoende is en je workflow de status beheert.",
1571
- docsUrl: DOCS_PAYLOADS,
1572
- docsLabel: "Goedkeuringsdocs openen",
1573
- },
1574
- sketch: {
1575
- summary: "Genormaliseerde invoer voor een eigen ontvangeroppervlak.",
1576
- explanation: "Kies dit wanneer jouw product de foto, kaart, PDF, canvas of andere receiver-UI beheert. RemoteDraw levert de invoerprimitieven zonder een voorgeschreven componentontwerp.",
1577
- docsUrl: DOCS_PAYLOADS,
1578
- docsLabel: "Schetsdocs openen",
1579
- },
1580
- photoMarkup: {
1581
- summary: "Aantekeningen bovenop een foto.",
1582
- explanation: "Gebruik dit voor inspecties, feedback of aanwijzingen op beeldmateriaal. Je app levert de foto en bewaart de relatie met de tekeningen.",
1583
- docsUrl: DOCS_PAYLOADS,
1584
- docsLabel: "Fotoannotatiedocs openen",
1585
- },
1586
- pdfMarkup: {
1587
- summary: "Aantekeningen op een PDF-pagina.",
1588
- explanation: "Gebruik dit voor documentreview. Je app rendert de juiste pagina en koppelt RemoteDraw-coördinaten aan die vaste weergave.",
1589
- docsUrl: DOCS_PAYLOADS,
1590
- docsLabel: "PDF-annotatiedocs openen",
1591
- },
1592
- mapMarkup: {
1593
- summary: "Tekeningen en aanwijzingen op een kaart.",
1594
- explanation: "Gebruik dit voor routes, locaties of ruimtelijke feedback. Je app beheert de kaart en viewport; RemoteDraw synchroniseert de invoer.",
1595
- docsUrl: DOCS_PAYLOADS,
1596
- docsLabel: "Kaartannotatiedocs openen",
1597
- },
1598
- screenMarkup: {
1599
- summary: "Aantekeningen op een scherm of applicatieweergave.",
1600
- explanation: "Gebruik dit voor support, demo's en UI-feedback. Je ontvanger levert het schermbeeld waarop de telefoonmarkeringen worden geprojecteerd.",
1601
- docsUrl: DOCS_PAYLOADS,
1602
- docsLabel: "Schermaantekeningdocs openen",
1603
- },
1604
- designReview: {
1605
- summary: "Gerichte visuele feedback op een ontwerp.",
1606
- explanation: "Gebruik dit voor ontwerpbeoordelingen met pijlen, vormen en vrije lijnen. Je product beheert opmerkingen, versies en besluitvorming.",
1607
- docsUrl: DOCS_PAYLOADS,
1608
- docsLabel: "Ontwerpbeoordelingsdocs openen",
1609
- },
1610
- pointer: {
1611
- summary: "Live aanwijzen zonder blijvende tekening.",
1612
- explanation: "Gebruik dit voor presentaties en begeleiding waarbij de telefoon als aanwijzer dient en beweging belangrijker is dan opgeslagen inkt.",
1613
- docsUrl: DOCS_PAYLOADS,
1614
- docsLabel: "Aanwijzerdocs openen",
1615
- },
1616
- };
1617
- const packageManagerWizardDetails = {
1618
- npm: {
1619
- summary: "De standaard package manager die met Node.js wordt geleverd.",
1620
- explanation: "Kies npm als je project package-lock.json gebruikt of geen voorkeur heeft. De wizard gebruikt npm voor installatie- en vervolgopdrachten.",
1621
- docsUrl: "https://docs.npmjs.com/",
1622
- docsLabel: "npm-docs openen",
1623
- },
1624
- pnpm: {
1625
- summary: "Een snelle package manager met een gedeelde pakketopslag.",
1626
- explanation: "Kies pnpm als je project pnpm-lock.yaml gebruikt, strikte dependency-isolatie wenst of al deel is van een pnpm-workspace.",
1627
- docsUrl: "https://pnpm.io/",
1628
- docsLabel: "pnpm-docs openen",
1629
- },
1630
- yarn: {
1631
- summary: "Een package manager met workspace- en Plug'n'Play-ondersteuning.",
1632
- explanation: "Kies Yarn als je project yarn.lock gebruikt. De wizard sluit aan op de bestaande Yarn-versie en projectconfiguratie.",
1633
- docsUrl: "https://yarnpkg.com/getting-started/usage",
1634
- docsLabel: "Yarn-docs openen",
1635
- },
1636
- bun: {
1637
- summary: "De snelle package manager die onderdeel is van de Bun-runtime.",
1638
- explanation: "Kies Bun als je project bun.lock gebruikt of Bun al inzet voor scripts, tests en installatie van dependencies.",
1639
- docsUrl: "https://bun.com/docs/pm/cli/install",
1640
- docsLabel: "Bun-docs openen",
1641
- },
1642
- };
1643
- const apiEndpointWizardDetails = {
1644
- deployment: {
1645
- summary: "Een tijdelijke URL die je later door je echte API-origin vervangt.",
1646
- explanation: "Kies dit voor een nieuw project dat nog niet aan een lokale server is gekoppeld. De gegenereerde configuratie bevat een duidelijke deployment-placeholder.",
1647
- docsUrl: DOCS_AUTH,
1648
- docsLabel: "Deploymentdocs openen",
1649
- },
1650
- local: {
1651
- summary: "Verbind met de lokale API op http://localhost:3210.",
1652
- explanation: "Kies dit wanneer je RemoteDraw lokaal draait en de ontvanger op dezelfde computer de ontwikkelserver kan bereiken.",
1653
- docsUrl: DOCS_AUTH,
1654
- docsLabel: "Lokale API-docs openen",
1655
- },
1656
- };
1657
1887
  function agentOptionCatalog() {
1658
1888
  const catalogOption = (choiceOption, compatibility) => ({
1659
1889
  value: choiceOption.value,
@@ -1662,13 +1892,13 @@ function agentOptionCatalog() {
1662
1892
  details: choiceOption.details,
1663
1893
  ...(compatibility ?? {}),
1664
1894
  });
1665
- const targets = wizardChoices(targetPromptChoices(), targetWizardDetails);
1895
+ const targets = wizardChoices(targetPromptChoices(), targetWizardDetails());
1666
1896
  const senders = senderChoices.map((value) => {
1667
1897
  const compatibleTargets = targetChoices.filter((target) => senderPromptChoices(target).some((option) => option.value === value));
1668
1898
  const prompt = compatibleTargets
1669
1899
  .flatMap((target) => senderPromptChoices(target))
1670
1900
  .find((option) => option.value === value);
1671
- return catalogOption(wizardChoices([prompt], senderWizardDetails)[0], {
1901
+ return catalogOption(wizardChoices([prompt], senderWizardDetails())[0], {
1672
1902
  compatibleTargets,
1673
1903
  });
1674
1904
  });
@@ -1678,7 +1908,7 @@ function agentOptionCatalog() {
1678
1908
  : []));
1679
1909
  const firstPlan = compatiblePlans[0];
1680
1910
  const prompt = sdkPromptChoices(firstPlan.target, firstPlan.sender).find((option) => option.value === value);
1681
- return catalogOption(wizardChoices([prompt], sdkWizardDetails)[0], {
1911
+ return catalogOption(wizardChoices([prompt], sdkWizardDetails())[0], {
1682
1912
  compatiblePlans,
1683
1913
  });
1684
1914
  });
@@ -1690,7 +1920,7 @@ function agentOptionCatalog() {
1690
1920
  {
1691
1921
  key: "appName",
1692
1922
  flag: "--app-name",
1693
- label: "Projectnaam",
1923
+ label: t("field.appName"),
1694
1924
  kind: "text",
1695
1925
  requiredFor: ["new"],
1696
1926
  options: [],
@@ -1698,48 +1928,44 @@ function agentOptionCatalog() {
1698
1928
  {
1699
1929
  key: "target",
1700
1930
  flag: "--target",
1701
- label: "Projecttype",
1931
+ label: t("field.target"),
1702
1932
  kind: "select",
1703
1933
  options: targets.map((option) => catalogOption(option)),
1704
1934
  },
1705
1935
  {
1706
1936
  key: "sender",
1707
1937
  flag: "--sender",
1708
- label: "Telefoonzender",
1938
+ label: t("field.sender"),
1709
1939
  kind: "select",
1710
1940
  options: senders,
1711
1941
  },
1712
1942
  {
1713
1943
  key: "sdk",
1714
1944
  flag: "--sdk",
1715
- label: "UI-framework / SDK",
1945
+ label: t("field.sdk"),
1716
1946
  kind: "select",
1717
1947
  options: sdks,
1718
1948
  },
1719
1949
  {
1720
1950
  key: "preset",
1721
1951
  flag: "--preset",
1722
- label: "Startpunt",
1952
+ label: t("field.preset"),
1723
1953
  kind: "select",
1724
- options: wizardChoices(presetPromptChoices(), presetWizardDetails).map((option) => catalogOption(option)),
1954
+ options: wizardChoices(presetPromptChoices(), presetWizardDetails()).map((option) => catalogOption(option)),
1725
1955
  },
1726
1956
  {
1727
1957
  key: "packageManager",
1728
1958
  flag: "--package-manager",
1729
- label: "Pakketbeheerder",
1959
+ label: t("field.packageManager"),
1730
1960
  kind: "select",
1731
- options: wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails).map((option) => catalogOption(option)),
1961
+ options: wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails()).map((option) => catalogOption(option)),
1732
1962
  },
1733
1963
  {
1734
1964
  key: "apiBaseUrl",
1735
1965
  flag: "--api-base-url",
1736
- label: "API-eindpunt",
1737
- kind: "select",
1738
- options: wizardChoices(apiEndpointPromptChoices(), apiEndpointWizardDetails).map((option) => ({
1739
- ...catalogOption(option),
1740
- endpoint: option.value,
1741
- value: apiBaseUrlForEndpoint(option.value),
1742
- })),
1966
+ label: t("field.apiBaseUrl"),
1967
+ kind: "text",
1968
+ options: [],
1743
1969
  },
1744
1970
  ],
1745
1971
  defaults: {
@@ -1749,9 +1975,9 @@ function agentOptionCatalog() {
1749
1975
  `${target}:${sender.value}`,
1750
1976
  defaultSdkForChoices(target, sender.value),
1751
1977
  ]))),
1752
- preset: "signature",
1978
+ preset: "sketch",
1753
1979
  packageManager: "npm",
1754
- apiBaseUrl: "https://<deployment>.convex.site",
1980
+ apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
1755
1981
  },
1756
1982
  safety: {
1757
1983
  dryRunFlag: "--dry-run",
@@ -1761,11 +1987,6 @@ function agentOptionCatalog() {
1761
1987
  },
1762
1988
  };
1763
1989
  }
1764
- function apiBaseUrlForEndpoint(endpoint) {
1765
- if (endpoint === "local")
1766
- return "http://localhost:3210";
1767
- return "https://<deployment>.convex.site";
1768
- }
1769
1990
  export function projectSetupDefinition() {
1770
1991
  return {
1771
1992
  defaults: {
@@ -1773,9 +1994,8 @@ export function projectSetupDefinition() {
1773
1994
  target: "web",
1774
1995
  sender: "remotedraw-ios",
1775
1996
  sdk: "react",
1776
- preset: "signature",
1997
+ preset: "sketch",
1777
1998
  packageManager: "npm",
1778
- apiEndpoint: "deployment",
1779
1999
  },
1780
2000
  fields: projectSetupFields(),
1781
2001
  normalize(values, changed) {
@@ -1799,13 +2019,13 @@ export function projectSetupDefinition() {
1799
2019
  const plan = planFromWizardValues(values);
1800
2020
  const label = (key, fallback) => wizardChoiceLabel(key, values) ?? fallback;
1801
2021
  return [
1802
- ["Project", plan.appName],
1803
- ["Map", `./${plan.slug}`],
1804
- ["Projecttype", label("target", plan.target)],
1805
- ["Telefoonzender", label("sender", plan.sender)],
1806
- ["SDK", label("sdk", plan.sdk)],
1807
- ["Pakketbeheerder", label("packageManager", plan.packageManager)],
1808
- ["API-eindpunt", plan.apiBaseUrl],
2022
+ [t("field.project"), plan.appName],
2023
+ [t("field.folder"), `./${plan.slug}`],
2024
+ [t("field.target"), label("target", plan.target)],
2025
+ [t("field.sender"), label("sender", plan.sender)],
2026
+ [t("field.sdkShort"), label("sdk", plan.sdk)],
2027
+ [t("field.packageManager"), label("packageManager", plan.packageManager)],
2028
+ [t("field.apiBaseUrl"), plan.apiBaseUrl],
1809
2029
  ];
1810
2030
  },
1811
2031
  };
@@ -1814,42 +2034,36 @@ function projectSetupFields() {
1814
2034
  return [
1815
2035
  {
1816
2036
  key: "appName",
1817
- label: "Projectnaam",
2037
+ label: t("field.appName"),
1818
2038
  kind: "text",
1819
2039
  validate: validateWizardProjectName,
1820
2040
  preview: (values) => values.appName.trim() === ""
1821
2041
  ? undefined
1822
- : `map ./${slugify(values.appName)} · pakket ${slugify(values.appName)}`,
2042
+ : t("field.appName.preview", { slug: slugify(values.appName) }),
1823
2043
  },
1824
2044
  {
1825
2045
  key: "target",
1826
- label: "Projecttype",
2046
+ label: t("field.target"),
1827
2047
  kind: "select",
1828
- choices: () => wizardChoices(targetPromptChoices(), targetWizardDetails),
2048
+ choices: () => wizardChoices(targetPromptChoices(), targetWizardDetails()),
1829
2049
  },
1830
2050
  {
1831
2051
  key: "sender",
1832
- label: "Telefoonzender",
2052
+ label: t("field.sender"),
1833
2053
  kind: "select",
1834
- choices: (values) => wizardChoices(senderPromptChoices(choice(values.target, targetChoices, "target")), senderWizardDetails),
2054
+ choices: (values) => wizardChoices(senderPromptChoices(choice(values.target, targetChoices, "target")), senderWizardDetails()),
1835
2055
  },
1836
2056
  {
1837
2057
  key: "sdk",
1838
- label: "UI-framework / SDK",
2058
+ label: t("field.sdk"),
1839
2059
  kind: "select",
1840
- choices: (values) => wizardChoices(sdkPromptChoices(choice(values.target, targetChoices, "target"), choice(values.sender, senderChoices, "sender")), sdkWizardDetails),
2060
+ choices: (values) => wizardChoices(sdkPromptChoices(choice(values.target, targetChoices, "target"), choice(values.sender, senderChoices, "sender")), sdkWizardDetails()),
1841
2061
  },
1842
2062
  {
1843
2063
  key: "packageManager",
1844
- label: "Pakketbeheerder",
1845
- kind: "select",
1846
- choices: () => wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails),
1847
- },
1848
- {
1849
- key: "apiEndpoint",
1850
- label: "API-eindpunt",
2064
+ label: t("field.packageManager"),
1851
2065
  kind: "select",
1852
- choices: () => wizardChoices(apiEndpointPromptChoices(), apiEndpointWizardDetails),
2066
+ choices: () => wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails()),
1853
2067
  },
1854
2068
  ];
1855
2069
  }
@@ -1859,62 +2073,7 @@ function wizardChoiceLabel(key, values) {
1859
2073
  ?.label;
1860
2074
  }
1861
2075
  function validateWizardProjectName(value) {
1862
- return value.trim() === "" ? "Voer een projectnaam in." : undefined;
1863
- }
1864
- const dutchWizardText = {
1865
- "Web app": "Webapp",
1866
- "Desktop or custom app": "Desktop- of maatwerkapp",
1867
- "iOS sender app": "iOS-zenderapp",
1868
- "Headless service": "Headless-service",
1869
- "Own iOS sender": "Eigen iOS-zender",
1870
- "Use Swift join-link and sender request helpers.": "Gebruik Swift-helpers voor deelnamelinks en zenderverzoeken.",
1871
- "Headless/custom sender": "Headless- of maatwerkzender",
1872
- "No hosted phone UI.": "Geen gehoste telefooninterface.",
1873
- "RemoteDraw iOS app": "RemoteDraw-iOS-app",
1874
- "Show a join URL or QR code from your receiver.": "Toon vanuit je ontvanger een deelname-URL of QR-code.",
1875
- "Build directly on the HTTP sender routes.": "Bouw rechtstreeks op de HTTP-zenderroutes.",
1876
- "Fastest path: scan the receiver QR code.": "Snelste route: scan de QR-code van de ontvanger.",
1877
- "Own web sender": "Eigen webzender",
1878
- "Use a web sender component or raw sender helpers.": "Gebruik een webzendercomponent of losse zenderhelpers.",
1879
- "Swift helpers": "Swift-helpers",
1880
- "For customer-owned iOS sender apps.": "Voor iOS-zenderapps in eigen beheer.",
1881
- "JavaScript client": "JavaScript-client",
1882
- "Framework-free HTTP clients and protocol types.": "Frameworkvrije HTTP-clients en protocoltypen.",
1883
- "Config only": "Alleen configuratie",
1884
- "No UI package dependency.": "Geen afhankelijkheid van een UI-pakket.",
1885
- "Receiver, pairing, and optional embedded sender components.": "Componenten voor ontvanger, koppeling en optionele ingebouwde zender.",
1886
- "Svelte receiver store plus shared HTTP clients.": "Svelte-store voor de ontvanger met gedeelde HTTP-clients.",
1887
- "No framework": "Geen framework",
1888
- "Framework-free clients and raw HTTP starter files.": "Frameworkvrije clients en kale HTTP-startbestanden.",
1889
- "Signature strip": "Handtekeningstrook",
1890
- "Product-ready branded field with exact surface mapping.": "Productklaar merkveld met exacte oppervlaktekoppeling.",
1891
- "Custom drawing surface": "Eigen tekenoppervlak",
1892
- "Flexible normalized input for your own receiver UI.": "Flexibele genormaliseerde invoer voor je eigen ontvanger-UI.",
1893
- Signature: "Handtekening",
1894
- Initials: "Initialen",
1895
- Approval: "Goedkeuring",
1896
- Sketch: "Schets",
1897
- "Photo markup": "Fotoannotatie",
1898
- "PDF markup": "PDF-annotatie",
1899
- "Map markup": "Kaartannotatie",
1900
- "Screen markup": "Schermaantekening",
1901
- "Design review": "Ontwerpbeoordeling",
1902
- Pointer: "Aanwijzer",
1903
- "Deployment URL placeholder": "Tijdelijke deployment-URL",
1904
- "Fill in your Convex site origin later.": "Vul later de oorsprong van je Convex-site in.",
1905
- "Local dev server": "Lokale ontwikkelserver",
1906
- "Use http://localhost:3210.": "Gebruik http://localhost:3210.",
1907
- };
1908
- function dutchWizardChoices(choices) {
1909
- return choices.map((choiceOption) => ({
1910
- ...choiceOption,
1911
- label: dutchWizardText[choiceOption.label] ?? choiceOption.label,
1912
- ...(choiceOption.hint == null
1913
- ? {}
1914
- : {
1915
- hint: dutchWizardText[choiceOption.hint] ?? choiceOption.hint,
1916
- }),
1917
- }));
2076
+ return value.trim() === "" ? t("field.appName.error") : undefined;
1918
2077
  }
1919
2078
  function planFromWizardValues(values) {
1920
2079
  const appName = values.appName.trim();
@@ -1929,11 +2088,11 @@ function planFromWizardValues(values) {
1929
2088
  sdk,
1930
2089
  preset: choice(values.preset, presetChoices, "preset"),
1931
2090
  packageManager: choice(values.packageManager, packageManagerChoices, "package-manager"),
1932
- apiBaseUrl: apiBaseUrlForEndpoint(choice(values.apiEndpoint, apiEndpointChoices, "api endpoint")),
2091
+ apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
1933
2092
  };
1934
2093
  const nameError = validateProjectName(plan.appName);
1935
2094
  if (nameError)
1936
- throw new Error(nameError);
2095
+ throw cliError("MISSING_ARGUMENT", "field.appName.error");
1937
2096
  validatePlan(plan);
1938
2097
  return plan;
1939
2098
  }
@@ -1942,7 +2101,7 @@ function planFromArgs(parsed, options) {
1942
2101
  options.defaults?.appName ??
1943
2102
  options.defaultAppName;
1944
2103
  if (options.appNameRequired && !readString(parsed, "app-name")) {
1945
- throw new Error("--app-name is required.");
2104
+ throw cliError("MISSING_ARGUMENT", "error.appNameRequired");
1946
2105
  }
1947
2106
  const target = choice(readString(parsed, "target") ?? options.defaults?.target ?? "web", targetChoices, "target");
1948
2107
  const sender = choice(readString(parsed, "sender") ??
@@ -1951,13 +2110,13 @@ function planFromArgs(parsed, options) {
1951
2110
  const sdk = choice(readString(parsed, "sdk") ??
1952
2111
  options.defaults?.sdk ??
1953
2112
  defaultSdkForChoices(target, sender), sdkChoices, "sdk");
1954
- const preset = choice(readString(parsed, "preset") ?? options.defaults?.preset ?? "signature", presetChoices, "preset");
2113
+ const preset = choice(readString(parsed, "preset") ?? options.defaults?.preset ?? "sketch", presetChoices, "preset");
1955
2114
  const packageManager = choice(readString(parsed, "package-manager") ??
1956
2115
  options.defaults?.packageManager ??
1957
2116
  "npm", packageManagerChoices, "package-manager");
1958
2117
  const apiBaseUrl = readString(parsed, "api-base-url") ??
1959
2118
  options.defaults?.apiBaseUrl ??
1960
- "https://<deployment>.convex.site";
2119
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
1961
2120
  validatePlan({ target, sender, sdk });
1962
2121
  return {
1963
2122
  appName,
@@ -1986,22 +2145,26 @@ function defaultSdkForChoices(target, sender) {
1986
2145
  }
1987
2146
  function validatePlan(plan) {
1988
2147
  if (plan.sender === "own-ios" && plan.sdk !== "swift") {
1989
- throw new Error("--sender own-ios requires --sdk swift.");
2148
+ throw cliError("INVALID_COMBINATION", "error.plan.ownIosNeedsSwift");
1990
2149
  }
1991
2150
  if (plan.sdk === "swift" && plan.sender !== "own-ios") {
1992
- throw new Error("--sdk swift is only supported with --sender own-ios.");
2151
+ throw cliError("INVALID_COMBINATION", "error.plan.swiftNeedsOwnIos");
1993
2152
  }
1994
2153
  if (plan.target === "ios" && plan.sender !== "own-ios") {
1995
- throw new Error("--target ios requires --sender own-ios.");
2154
+ throw cliError("INVALID_COMBINATION", "error.plan.iosNeedsOwnIos");
1996
2155
  }
1997
2156
  if (plan.sender === "headless" && plan.sdk === "react") {
1998
- throw new Error("--sender headless requires --sdk js or --sdk headless.");
2157
+ throw cliError("INVALID_COMBINATION", "error.plan.headlessSenderSdk");
1999
2158
  }
2000
2159
  }
2001
2160
  function choice(value, choices, label) {
2002
2161
  if (choices.includes(value))
2003
2162
  return value;
2004
- throw new Error(`Unknown ${label} "${value}". Expected one of: ${choices.join(", ")}.`);
2163
+ throw cliError("INVALID_ARGUMENT", "error.unknownChoice", {
2164
+ label,
2165
+ value,
2166
+ expected: choices.join(", "),
2167
+ });
2005
2168
  }
2006
2169
  function resolveOutputDir(cwd, parsed, fallback) {
2007
2170
  const requestedPath = readString(parsed, "path") ?? parsed.positionals[0] ?? fallback ?? ".";
@@ -2023,6 +2186,11 @@ function newProjectFiles(plan) {
2023
2186
  files.set("tsconfig.json", reactTsconfig());
2024
2187
  files.set("vite.config.ts", reactViteConfig());
2025
2188
  }
2189
+ else if (plan.sdk !== "swift") {
2190
+ // Without this the starter shipped a TypeScript toolchain and nothing to
2191
+ // point it at, so `tsc` had no config and no way to resolve node globals.
2192
+ files.set("tsconfig.json", nodeTsconfig());
2193
+ }
2026
2194
  return files;
2027
2195
  }
2028
2196
  function initProjectFiles(plan, packageJsonAlreadyExists) {
@@ -2037,7 +2205,7 @@ function initProjectFiles(plan, packageJsonAlreadyExists) {
2037
2205
  files.set("src/remotedraw/createRemoteDrawSession.ts", reactBackendSessionTs(plan));
2038
2206
  files.set("src/remotedraw/RemoteDrawReceiver.tsx", reactReceiverTsx());
2039
2207
  if (plan.sender === "embedded-web") {
2040
- files.set("src/remotedraw/RemoteDrawEmbeddedSender.tsx", reactEmbeddedSenderTsx());
2208
+ files.set("src/remotedraw/useRemoteDrawSender.ts", reactSenderTsx());
2041
2209
  }
2042
2210
  }
2043
2211
  else if (plan.sdk === "svelte") {
@@ -2077,7 +2245,9 @@ async function assertProjectFilesWritable(files, outputDir, runtime, dryRun, for
2077
2245
  return;
2078
2246
  for (const relativePath of files.keys()) {
2079
2247
  if (await runtime.exists(path.join(outputDir, relativePath))) {
2080
- throw new Error(`Refusing to overwrite ${relativePath}. Pass --force to replace generated files.`);
2248
+ throw cliError("FILE_CONFLICT", "error.refuseOverwrite", {
2249
+ file: relativePath,
2250
+ });
2081
2251
  }
2082
2252
  }
2083
2253
  }
@@ -2146,25 +2316,30 @@ function hasDependency(packageJson, dependencyName) {
2146
2316
  return (isRecord(packageJson.dependencies) &&
2147
2317
  typeof packageJson.dependencies[dependencyName] === "string");
2148
2318
  }
2319
+ /**
2320
+ * The RemoteDraw package an sdk choice installs, if any. The js starter calls
2321
+ * the HTTP API with plain fetch and takes only the request/response contract
2322
+ * from `@remotedraw/protocol`; the headless and swift starters take nothing.
2323
+ */
2324
+ function sdkPackageName(sdk) {
2325
+ if (sdk === "react")
2326
+ return "@remotedraw/react";
2327
+ if (sdk === "svelte")
2328
+ return "@remotedraw/svelte";
2329
+ if (sdk === "js")
2330
+ return "@remotedraw/protocol";
2331
+ return undefined;
2332
+ }
2149
2333
  function dependenciesForPlan(plan) {
2334
+ const sdkPackage = sdkPackageName(plan.sdk);
2335
+ const dependencies = sdkPackage
2336
+ ? { [sdkPackage]: REMOTEDRAW_SDK_RANGE }
2337
+ : {};
2150
2338
  if (plan.sdk === "react") {
2151
- return {
2152
- "@remotedraw/react": "latest",
2153
- react: "^19.0.0",
2154
- "react-dom": "^19.0.0",
2155
- };
2156
- }
2157
- if (plan.sdk === "svelte") {
2158
- return {
2159
- "@remotedraw/svelte": "latest",
2160
- };
2161
- }
2162
- if (plan.sdk === "js") {
2163
- return {
2164
- "@remotedraw/protocol": "latest",
2165
- };
2339
+ dependencies.react = "^19.0.0";
2340
+ dependencies["react-dom"] = "^19.0.0";
2166
2341
  }
2167
- return {};
2342
+ return dependencies;
2168
2343
  }
2169
2344
  function newPackageJson(plan) {
2170
2345
  const dependencies = dependenciesForPlan(plan);
@@ -2180,15 +2355,24 @@ function newPackageJson(plan) {
2180
2355
  },
2181
2356
  dependencies,
2182
2357
  devDependencies: {
2358
+ // src/remotedraw/createRemoteDrawSession.ts reads process.env, so the
2359
+ // starter cannot typecheck without node types.
2360
+ "@types/node": "^25.0.0",
2183
2361
  "@types/react": "^19.0.0",
2184
2362
  "@types/react-dom": "^19.0.0",
2185
- "@vitejs/plugin-react": "latest",
2363
+ "@vitejs/plugin-react": "^6.0.0",
2186
2364
  typescript: "^6.0.0",
2187
- vite: "latest",
2365
+ vite: "^8.0.0",
2188
2366
  },
2189
2367
  };
2190
2368
  }
2191
- return initPackageJson(plan);
2369
+ const manifest = initPackageJson(plan);
2370
+ if (plan.sdk === "swift")
2371
+ return manifest;
2372
+ return {
2373
+ ...manifest,
2374
+ scripts: { ...manifest.scripts, typecheck: "tsc --noEmit" },
2375
+ };
2192
2376
  }
2193
2377
  function initPackageJson(plan) {
2194
2378
  return {
@@ -2201,14 +2385,21 @@ function initPackageJson(plan) {
2201
2385
  "remotedraw:create-input": "remotedraw create-input --preset " + plan.preset,
2202
2386
  },
2203
2387
  dependencies: dependenciesForPlan(plan),
2204
- devDependencies: {
2205
- typescript: "^6.0.0",
2206
- },
2388
+ // The scripts above shell out to `remotedraw`, so the CLI has to be a real
2389
+ // dependency of the project instead of an assumed global install. The swift
2390
+ // starter writes no TypeScript, so it gets no TypeScript toolchain.
2391
+ devDependencies: plan.sdk === "swift"
2392
+ ? { "@remotedraw/cli": REMOTEDRAW_CLI_RANGE }
2393
+ : {
2394
+ "@remotedraw/cli": REMOTEDRAW_CLI_RANGE,
2395
+ "@types/node": "^25.0.0",
2396
+ typescript: "^6.0.0",
2397
+ },
2207
2398
  };
2208
2399
  }
2209
2400
  function configJson(plan) {
2210
2401
  return {
2211
- $schema: "https://remotedraw.app/schemas/remotedraw.config.json",
2402
+ $schema: "https://www.remotedraw.com/schemas/remotedraw.config.json",
2212
2403
  appName: plan.appName,
2213
2404
  environment: "dev",
2214
2405
  target: plan.target,
@@ -2220,7 +2411,7 @@ function configJson(plan) {
2220
2411
  }
2221
2412
  function envExample(plan) {
2222
2413
  return [
2223
- "# RemoteDraw API origin. Use your Convex site URL in production.",
2414
+ "# RemoteDraw API origin. The hosted RemoteDraw API; leave as-is unless RemoteDraw support tells you otherwise.",
2224
2415
  `REMOTEDRAW_API_BASE_URL=${plan.apiBaseUrl}`,
2225
2416
  "",
2226
2417
  "# Non-secret dashboard project identifier. remotedraw new/init fills this in.",
@@ -2308,7 +2499,7 @@ function reactReceiverTsx() {
2308
2499
  "import {",
2309
2500
  " RemoteDrawProvider,",
2310
2501
  " RemoteDrawReceiver,",
2311
- " SubmissionStatus,",
2502
+ " RemoteDrawSessionControls,",
2312
2503
  " createHttpReceiverClient,",
2313
2504
  '} from "@remotedraw/react";',
2314
2505
  'import type { CreateSessionResponse } from "@remotedraw/react";',
@@ -2333,36 +2524,130 @@ function reactReceiverTsx() {
2333
2524
  " receiver={receiver}",
2334
2525
  " >",
2335
2526
  ' <RemoteDrawReceiver aria-label="RemoteDraw receiver surface" />',
2336
- ' <SubmissionStatus metadataKeys={["externalId", "senderLabel"]} />',
2527
+ " <RemoteDrawSessionControls />",
2337
2528
  " </RemoteDrawProvider>",
2338
2529
  " );",
2339
2530
  "}",
2340
2531
  "",
2341
2532
  ].join("\n");
2342
2533
  }
2343
- function reactEmbeddedSenderTsx() {
2534
+ function reactSenderTsx() {
2535
+ // Headless on purpose: the ready-made web sender is the hosted /join page
2536
+ // (the QR / Universal Link target). This starter is for products that must
2537
+ // host the pad themselves — RemoteDraw transports normalized points while
2538
+ // the canvas, tools, and styling stay app-owned.
2344
2539
  return [
2345
- 'import { EmbeddedSender, createHttpSenderClient } from "@remotedraw/react";',
2346
- "",
2347
- "type RemoteDrawEmbeddedSenderPanelProps = {",
2348
- " apiBaseUrl: string;",
2349
- " joinTokenOrUrl: string;",
2350
- "};",
2540
+ 'import { useEffect, useMemo, useRef, useState } from "react";',
2541
+ "import {",
2542
+ " createHttpSenderClient,",
2543
+ " createPacedDraftQueue,",
2544
+ " draftPointsForTransport,",
2545
+ " joinTokenFromInput,",
2546
+ '} from "@remotedraw/react";',
2547
+ "import type {",
2548
+ " DraftInputUpdate,",
2549
+ " JoinSessionResponse,",
2550
+ " NormalizedPoint,",
2551
+ '} from "@remotedraw/react";',
2351
2552
  "",
2352
- "export function RemoteDrawEmbeddedSenderPanel({",
2353
- " apiBaseUrl,",
2354
- " joinTokenOrUrl,",
2355
- "}: RemoteDrawEmbeddedSenderPanelProps) {",
2356
- " return (",
2357
- " <EmbeddedSender",
2358
- " client={createHttpSenderClient(apiBaseUrl)}",
2359
- " initialJoinToken={joinTokenOrUrl}",
2360
- " autoJoin",
2361
- ' device={{ platform: "web", displayName: "Embedded sender" }}',
2362
- ' submitMetadata={{ senderLabel: "Embedded sender" }}',
2363
- ' submitLabel="Done"',
2364
- " />",
2553
+ "// Headless RemoteDraw sender. The hosted /join page is the ready-made",
2554
+ "// pad; use this hook when the drawing surface must live inside your own",
2555
+ "// page. Points are normalized 0..1; the canvas and tools stay yours.",
2556
+ "export function useRemoteDrawSender(",
2557
+ " apiBaseUrl: string,",
2558
+ " joinTokenOrUrl: string,",
2559
+ ") {",
2560
+ " const client = useMemo(",
2561
+ " () => createHttpSenderClient(apiBaseUrl),",
2562
+ " [apiBaseUrl],",
2365
2563
  " );",
2564
+ " const [joined, setJoined] = useState<JoinSessionResponse | null>(null);",
2565
+ " const [error, setError] = useState<string | null>(null);",
2566
+ " const joinedRef = useRef<JoinSessionResponse | null>(null);",
2567
+ " const sequenceRef = useRef(0);",
2568
+ " // Drafts are throttled and latest-only: never POST every pointer event.",
2569
+ " const draftQueue = useMemo(",
2570
+ " () =>",
2571
+ " createPacedDraftQueue<DraftInputUpdate>({",
2572
+ " intervalMs: 32,",
2573
+ " run: async (request) => {",
2574
+ " await client.updateDraft?.(request);",
2575
+ " },",
2576
+ " }),",
2577
+ " [client],",
2578
+ " );",
2579
+ "",
2580
+ " useEffect(() => {",
2581
+ " const joinToken = joinTokenFromInput(joinTokenOrUrl);",
2582
+ " if (!joinToken) {",
2583
+ ' setError("Join token is required.");',
2584
+ " return;",
2585
+ " }",
2586
+ " let cancelled = false;",
2587
+ " client",
2588
+ " .join?.({",
2589
+ " joinToken,",
2590
+ ' device: { platform: "web", displayName: "Embedded sender" },',
2591
+ " })",
2592
+ " .then((result) => {",
2593
+ " if (cancelled || !result) return;",
2594
+ " // Resuming a token must continue its sequence: the server drops",
2595
+ " // anything at or below the last sequence it has seen.",
2596
+ " sequenceRef.current = result.lastSequence ?? 0;",
2597
+ " joinedRef.current = result;",
2598
+ " setJoined(result);",
2599
+ " })",
2600
+ " .catch((caught: unknown) => {",
2601
+ " if (!cancelled) setError(String(caught));",
2602
+ " });",
2603
+ " return () => {",
2604
+ " cancelled = true;",
2605
+ " };",
2606
+ " }, [client, joinTokenOrUrl]);",
2607
+ "",
2608
+ " return {",
2609
+ " joined,",
2610
+ " error,",
2611
+ " /** Call on pointermove with the whole in-progress stroke. */",
2612
+ " previewDraft(points: NormalizedPoint[]) {",
2613
+ " const senderToken = joinedRef.current?.senderToken;",
2614
+ " if (!senderToken || points.length === 0) return;",
2615
+ " draftQueue.offer(() => ({",
2616
+ " senderToken,",
2617
+ " sequence: ++sequenceRef.current,",
2618
+ ' tool: "freehand" as const,',
2619
+ ' pointerType: "touch" as const,',
2620
+ " points: draftPointsForTransport(points),",
2621
+ " occurredAt: Date.now(),",
2622
+ " }));",
2623
+ " },",
2624
+ " /** Call once on pointer-up with a stable id per stroke. */",
2625
+ " async commitStroke(points: NormalizedPoint[], clientStrokeId: string) {",
2626
+ " const senderToken = joinedRef.current?.senderToken;",
2627
+ " if (!senderToken || points.length < 2) return;",
2628
+ " draftQueue.clear();",
2629
+ " await client.commitStroke?.({",
2630
+ " senderToken,",
2631
+ " clientStrokeId,",
2632
+ " sequence: ++sequenceRef.current,",
2633
+ ' tool: "freehand",',
2634
+ ' pointerType: "touch",',
2635
+ " points,",
2636
+ " occurredAt: Date.now(),",
2637
+ " });",
2638
+ " },",
2639
+ " /** Call when the sender-side workflow is complete. */",
2640
+ " async submit(metadata?: Record<string, string>) {",
2641
+ " const senderToken = joinedRef.current?.senderToken;",
2642
+ " if (!senderToken) return;",
2643
+ " await client.submit?.({",
2644
+ " senderToken,",
2645
+ " clientSubmissionId: crypto.randomUUID(),",
2646
+ " occurredAt: Date.now(),",
2647
+ " ...(metadata ? { metadata } : {}),",
2648
+ " });",
2649
+ " },",
2650
+ " };",
2366
2651
  "}",
2367
2652
  "",
2368
2653
  ].join("\n");
@@ -2440,11 +2725,24 @@ function svelteReceiverSvelte() {
2440
2725
  ].join("\n");
2441
2726
  }
2442
2727
  function rawHttpSessionTs(plan) {
2728
+ // The js starter declares @remotedraw/protocol, so it uses it: the contract
2729
+ // types are the whole reason that dependency is there. The headless starter
2730
+ // declares nothing and stays import-free.
2731
+ const typed = plan.sdk === "js";
2443
2732
  return [
2733
+ ...(typed
2734
+ ? [
2735
+ "import type {",
2736
+ " CreateSessionRequest,",
2737
+ " CreateSessionResponse,",
2738
+ '} from "@remotedraw/protocol";',
2739
+ "",
2740
+ ]
2741
+ : []),
2444
2742
  "const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
2445
2743
  "const apiKey = process.env.REMOTEDRAW_API_KEY;",
2446
2744
  "",
2447
- "export function createRemoteDrawSessionRequest() {",
2745
+ `export function createRemoteDrawSessionRequest()${typed ? ": CreateSessionRequest" : ""} {`,
2448
2746
  " return " +
2449
2747
  JSON.stringify(createSessionPayload({
2450
2748
  preset: plan.preset,
@@ -2454,7 +2752,7 @@ function rawHttpSessionTs(plan) {
2454
2752
  ";",
2455
2753
  "}",
2456
2754
  "",
2457
- "export async function createRemoteDrawSession() {",
2755
+ `export async function createRemoteDrawSession()${typed ? ": Promise<CreateSessionResponse>" : ""} {`,
2458
2756
  " if (!apiBaseUrl || !apiKey) {",
2459
2757
  ' throw new Error("Set REMOTEDRAW_API_BASE_URL and REMOTEDRAW_API_KEY in backend secrets.");',
2460
2758
  " }",
@@ -2468,7 +2766,9 @@ function rawHttpSessionTs(plan) {
2468
2766
  " body: JSON.stringify(createRemoteDrawSessionRequest()),",
2469
2767
  " });",
2470
2768
  " if (!response.ok) throw new Error(await response.text());",
2471
- " return await response.json();",
2769
+ typed
2770
+ ? " return (await response.json()) as CreateSessionResponse;"
2771
+ : " return await response.json();",
2472
2772
  "}",
2473
2773
  "",
2474
2774
  ].join("\n");
@@ -2678,6 +2978,9 @@ function reactTsconfig() {
2678
2978
  ' "target": "ES2022",',
2679
2979
  ' "useDefineForClassFields": true,',
2680
2980
  ' "lib": ["ES2022", "DOM", "DOM.Iterable"],',
2981
+ // vite/client declares the CSS side-effect import in src/main.tsx; node
2982
+ // covers the process.env reads in the backend session helper.
2983
+ ' "types": ["node", "vite/client"],',
2681
2984
  ' "allowJs": false,',
2682
2985
  ' "skipLibCheck": true,',
2683
2986
  ' "esModuleInterop": true,',
@@ -2695,6 +2998,26 @@ function reactTsconfig() {
2695
2998
  "",
2696
2999
  ].join("\n");
2697
3000
  }
3001
+ function nodeTsconfig() {
3002
+ return [
3003
+ "{",
3004
+ ' "compilerOptions": {',
3005
+ ' "target": "ES2022",',
3006
+ ' "lib": ["ES2022", "DOM"],',
3007
+ ' "types": ["node"],',
3008
+ ' "module": "ESNext",',
3009
+ ' "moduleResolution": "Bundler",',
3010
+ ' "strict": true,',
3011
+ ' "skipLibCheck": true,',
3012
+ ' "resolveJsonModule": true,',
3013
+ ' "isolatedModules": true,',
3014
+ ' "noEmit": true',
3015
+ " },",
3016
+ ' "include": ["src"]',
3017
+ "}",
3018
+ "",
3019
+ ].join("\n");
3020
+ }
2698
3021
  function reactViteConfig() {
2699
3022
  return [
2700
3023
  'import react from "@vitejs/plugin-react";',
@@ -2714,17 +3037,6 @@ export function createSessionPayload(options) {
2714
3037
  target: {
2715
3038
  kind: targetKindForPreset(options.preset),
2716
3039
  label: options.label,
2717
- ...(options.preset === "signature"
2718
- ? {
2719
- inputMapping: "surface",
2720
- coordinateSpace: { width: 1600, height: 500 },
2721
- }
2722
- : options.preset === "initials"
2723
- ? {
2724
- inputMapping: "surface",
2725
- coordinateSpace: { width: 800, height: 500 },
2726
- }
2727
- : {}),
2728
3040
  metadata: {
2729
3041
  descriptor,
2730
3042
  },
@@ -2735,31 +3047,6 @@ export function createSessionPayload(options) {
2735
3047
  };
2736
3048
  }
2737
3049
  function descriptorForPreset(preset, label) {
2738
- if (preset === "signature" || preset === "initials") {
2739
- return {
2740
- version: 1,
2741
- privacy: "metadata-only",
2742
- surfaceRole: "field",
2743
- inputIntent: preset,
2744
- label,
2745
- components: [
2746
- {
2747
- id: preset,
2748
- role: "field",
2749
- inputIntent: preset,
2750
- required: true,
2751
- bounds: { minX: 0.1, minY: 0.68, maxX: 0.9, maxY: 0.86 },
2752
- },
2753
- ],
2754
- phoneHints: {
2755
- preferredLayout: "signature-pad",
2756
- preferredTool: "freehand",
2757
- primaryActionLabel: "Submit",
2758
- showUndo: true,
2759
- showClear: true,
2760
- },
2761
- };
2762
- }
2763
3050
  if (preset === "approval") {
2764
3051
  return {
2765
3052
  version: 1,
@@ -2790,11 +3077,8 @@ function descriptorForPreset(preset, label) {
2790
3077
  };
2791
3078
  }
2792
3079
  function targetKindForPreset(preset) {
2793
- if (preset === "signature" ||
2794
- preset === "initials" ||
2795
- preset === "approval") {
3080
+ if (preset === "approval")
2796
3081
  return "field";
2797
- }
2798
3082
  if (preset === "photoMarkup" || preset === "designReview")
2799
3083
  return "image";
2800
3084
  if (preset === "pdfMarkup")
@@ -2818,8 +3102,6 @@ function surfaceRoleForPreset(preset) {
2818
3102
  }
2819
3103
  function labelForPreset(preset) {
2820
3104
  const labels = {
2821
- signature: "Customer signature",
2822
- initials: "Customer initials",
2823
3105
  approval: "Quick approval",
2824
3106
  sketch: "Sketch input",
2825
3107
  photoMarkup: "Photo annotation",
@@ -2848,10 +3130,10 @@ function shellQuote(value) {
2848
3130
  function normalizedOrigin(value) {
2849
3131
  const parsed = new URL(value);
2850
3132
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
2851
- throw new Error("Expected an HTTP(S) API base URL.");
3133
+ throw cliError("INVALID_ARGUMENT", "error.expectedHttpOrigin");
2852
3134
  }
2853
3135
  if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
2854
- throw new Error("Expected API base URL to be an origin without a path, query, or hash.");
3136
+ throw cliError("INVALID_ARGUMENT", "error.expectedBareOrigin");
2855
3137
  }
2856
3138
  return parsed.origin;
2857
3139
  }
@@ -2866,95 +3148,20 @@ function resolveAgentPath(value, runtime) {
2866
3148
  if (value.startsWith("~/")) {
2867
3149
  const home = runtime.env.HOME;
2868
3150
  if (home == null) {
2869
- throw new Error("Cannot expand ~ because HOME is not set.");
3151
+ throw cliError("INVALID_ARGUMENT", "error.noHome");
2870
3152
  }
2871
3153
  return path.join(home, value.slice(2));
2872
3154
  }
2873
3155
  return path.resolve(runtime.cwd, value);
2874
3156
  }
3157
+ /**
3158
+ * The skill a coding agent installs is the same file this repository
3159
+ * documents. `docs/agents/remotedraw/SKILL.md` is the single source; the
3160
+ * module below is generated from it by `bun run generate:skill`, and
3161
+ * tests/agent-skill.test.ts fails if the two drift apart again.
3162
+ */
2875
3163
  export function agentSkillMarkdown() {
2876
- return [
2877
- "---",
2878
- "name: remotedraw",
2879
- "description: Add RemoteDraw phone input to customer apps with the RemoteDraw CLI, public API, React SDK, raw HTTP, or customer-owned iOS sender flow.",
2880
- "---",
2881
- "",
2882
- "# RemoteDraw Agent Skill",
2883
- "",
2884
- "Use this skill when a user asks to create, initialize, debug, or review a RemoteDraw integration.",
2885
- "",
2886
- "## Decision Flow",
2887
- "",
2888
- "1. Identify the receiver surface: web app, desktop app, iOS app, or headless/backend workflow.",
2889
- "2. Identify the sender surface: RemoteDraw iOS app, embedded web sender, customer-owned iOS sender, or raw/headless sender.",
2890
- "3. Pick the SDK path:",
2891
- " - React SDK: web receiver and optional embedded web sender.",
2892
- " - Plain JavaScript/raw HTTP: non-React web, desktop, backend, or custom clients.",
2893
- " - Swift: customer-owned iOS sender apps.",
2894
- "4. Pick a supported starter: signature for the product-ready SignatureStrip, or sketch for a receiver UI owned by the integrating app. Configure target kind and descriptors directly for photos, PDFs, maps, screens, and other custom surfaces.",
2895
- "",
2896
- "## CLI First",
2897
- "",
2898
- "Read the machine-readable option catalog before choosing a plan:",
2899
- "",
2900
- "```sh",
2901
- "remotedraw options --format json",
2902
- "```",
2903
- "",
2904
- "Initialize a project with the closest supported path:",
2905
- "",
2906
- "```sh",
2907
- "remotedraw init --target web --sender remotedraw-ios --sdk react --preset signature",
2908
- "remotedraw init --target web --sender embedded-web --sdk react --preset sketch",
2909
- "remotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch",
2910
- "remotedraw init --target ios --sender own-ios --sdk swift --preset sketch",
2911
- "```",
2912
- "",
2913
- "By default, new/init create the dashboard project and a project-scoped development API key, then write REMOTEDRAW_API_BASE_URL, REMOTEDRAW_PROJECT_ID, and REMOTEDRAW_API_KEY to a gitignored .env.local. Use --offline only when cloud setup is intentionally out of scope.",
2914
- "",
2915
- "Agents must use explicit non-interactive dry runs before changing a project:",
2916
- "",
2917
- "```sh",
2918
- "remotedraw init --non-interactive --offline --dry-run --format json --target web --sender remotedraw-ios --sdk react --preset signature --package-manager npm",
2919
- "# Inspect plan, files, defaultsApplied, and warnings before applying.",
2920
- "remotedraw init --non-interactive --offline --format json --target web --sender remotedraw-ios --sdk react --preset signature --package-manager npm",
2921
- "remotedraw doctor --format json",
2922
- "remotedraw create-input --preset signature --json",
2923
- "```",
2924
- "",
2925
- "Do not use the interactive wizard, synthesize arrow-key input, or scrape human-formatted output. If receiver, sender, or preset intent is ambiguous, ask the user instead of guessing. Never pass --force unless overwrite scope was explicitly approved.",
2926
- "",
2927
- "## Security Rules",
2928
- "",
2929
- "- Keep `rd_sk_...` API keys in trusted backend secrets only.",
2930
- "- Keep the account-level `rd_cli_...` credential in the user config directory; never copy it into a project. Use REMOTEDRAW_CLI_TOKEN only as an explicitly managed CI secret.",
2931
- "- Never place API keys in browser bundles, mobile clients, screenshots, logs, or generated examples.",
2932
- "- Public clients should receive only `joinUrl`, `joinToken`, `receiverToken`, or `senderToken` values scoped to the session.",
2933
- "- Production QR codes should use HTTPS `joinUrl` values. Do not make the custom scheme the primary QR target.",
2934
- "",
2935
- "## API Contract",
2936
- "",
2937
- "- Backend creates sessions with `POST /v1/sessions`.",
2938
- "- Receiver clients read `POST /v1/receiver/session`, `/drawings`, `/drafts`, and `/senders` with a receiver token.",
2939
- "- Sender clients join with `POST /v1/join`, stream mutable drafts to `/v1/sender/draft`, commit durable strokes to `/v1/sender/commit`, and finish with `/v1/sender/submit`.",
2940
- "- Custom senders should throttle draft updates, coalesce to the latest pending preview, and commit one durable stroke on pointer-up with a stable `clientStrokeId`.",
2941
- "",
2942
- "## Verification",
2943
- "",
2944
- "After changes, run the narrowest relevant checks first:",
2945
- "",
2946
- "```sh",
2947
- "remotedraw doctor",
2948
- "bun run test:api",
2949
- "bun run typecheck",
2950
- "```",
2951
- "",
2952
- "For customer-owned iOS sender helpers in this repo, also run:",
2953
- "",
2954
- "```sh",
2955
- "bun run ios:kit:test",
2956
- "```",
2957
- ].join("\n");
3164
+ return AGENT_SKILL_MARKDOWN;
2958
3165
  }
2959
3166
  /**
2960
3167
  * Doctor used to read REMOTEDRAW_API_BASE_URL raw and call an unset value a
@@ -2991,20 +3198,30 @@ function resolveDoctorApiBaseUrl(projectEnv, requested) {
2991
3198
  }
2992
3199
  function doctorApiBaseUrlMessage(resolved) {
2993
3200
  if (resolved.state === "placeholder") {
2994
- return `Still the ${resolved.value} placeholder. Replace it with ${DEFAULT_REMOTEDRAW_API_BASE_URL}, or with your own deployment origin.`;
3201
+ return t("doctor.baseUrl.placeholder", {
3202
+ value: resolved.value,
3203
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
3204
+ });
2995
3205
  }
2996
3206
  if (resolved.state === "invalid") {
2997
- return `${resolved.value} is not an HTTP origin. Set it to ${DEFAULT_REMOTEDRAW_API_BASE_URL}, or to your own deployment origin.`;
3207
+ return t("doctor.baseUrl.invalid", {
3208
+ value: resolved.value,
3209
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
3210
+ });
2998
3211
  }
2999
3212
  if (resolved.source === "default") {
3000
- return `Using ${resolved.url} (RemoteDraw production API; nothing overrides it here).`;
3213
+ return t("doctor.baseUrl.default", { url: resolved.url });
3001
3214
  }
3002
3215
  const origin = resolved.source === "flag"
3003
- ? "--api-base-url"
3004
- : "this project's environment";
3216
+ ? t("doctor.baseUrl.origin.flag")
3217
+ : t("doctor.baseUrl.origin.project");
3005
3218
  return resolved.isProductionDefault
3006
- ? `Using ${resolved.url} (production API, set by ${origin}).`
3007
- : `Using ${resolved.url} (override from ${origin}, not the ${DEFAULT_REMOTEDRAW_API_BASE_URL} production API).`;
3219
+ ? t("doctor.baseUrl.production", { url: resolved.url, origin })
3220
+ : t("doctor.baseUrl.override", {
3221
+ url: resolved.url,
3222
+ origin,
3223
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
3224
+ });
3008
3225
  }
3009
3226
  function isHttpOrigin(value) {
3010
3227
  if (value == null || value.includes("<"))
@@ -3041,28 +3258,31 @@ function planDefaultsForExample(example) {
3041
3258
  target: "web",
3042
3259
  sender: "remotedraw-ios",
3043
3260
  sdk: "react",
3044
- preset: "signature",
3261
+ preset: "sketch",
3045
3262
  };
3046
3263
  }
3047
3264
  function scaffoldSummary(verb, plan, outputDir, written, dryRun) {
3048
- const nextCommandPrefix = dryRun ? "Would write" : "Wrote";
3049
3265
  return [
3050
- `${verb} RemoteDraw ${plan.target} integration in ${outputDir}`,
3051
- "",
3052
- "Choices:",
3053
- ` App: ${plan.appName}`,
3054
- ` SDK: ${plan.sdk}`,
3055
- ` Sender: ${plan.sender}`,
3056
- ` Starter: ${plan.preset}`,
3057
- "",
3058
- `${nextCommandPrefix}:`,
3266
+ t("scaffold.headline", {
3267
+ verb,
3268
+ target: plan.target,
3269
+ dir: outputDir,
3270
+ }),
3271
+ "",
3272
+ t("scaffold.choices"),
3273
+ t("scaffold.choice.app", { value: plan.appName }),
3274
+ t("scaffold.choice.sdk", { value: plan.sdk }),
3275
+ t("scaffold.choice.sender", { value: plan.sender }),
3276
+ t("scaffold.choice.starter", { value: plan.preset }),
3277
+ "",
3278
+ dryRun ? t("scaffold.wouldWrite") : t("scaffold.wrote"),
3059
3279
  ...written.map((file) => ` ${file}`),
3060
3280
  "",
3061
- "Next:",
3062
- ` 1. ${installCommand(plan.packageManager)}`,
3063
- " 2. Set REMOTEDRAW_API_BASE_URL and REMOTEDRAW_API_KEY in backend secrets.",
3064
- " 3. Create an input request from backend code or run remotedraw create-input --json.",
3065
- " 4. Render the returned HTTPS joinUrl in your receiver UI.",
3281
+ t("word.next"),
3282
+ t("scaffold.next.install", { command: installCommand(plan.packageManager) }),
3283
+ t("scaffold.next.secrets"),
3284
+ t("scaffold.next.createInput"),
3285
+ t("scaffold.next.render"),
3066
3286
  ].join("\n");
3067
3287
  }
3068
3288
  function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisioning, offline, dryRun) {
@@ -3080,16 +3300,12 @@ function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisionin
3080
3300
  .map(([field, , value]) => ({ field, value }));
3081
3301
  const warnings = [
3082
3302
  ...(plan.apiBaseUrl.includes("<deployment>")
3083
- ? ["Replace the deployment API placeholder before making live requests."]
3303
+ ? [t("cloud.warning.placeholder")]
3084
3304
  : []),
3085
3305
  ...(offline
3086
- ? [
3087
- "Cloud provisioning was skipped; no dashboard project or API key was created.",
3088
- ]
3306
+ ? [t("cloud.warning.offline")]
3089
3307
  : dryRun
3090
- ? [
3091
- "Cloud authentication and provisioning were not executed during dry-run.",
3092
- ]
3308
+ ? [t("cloud.warning.dryRun")]
3093
3309
  : []),
3094
3310
  ];
3095
3311
  return {