@remotedraw/cli 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,20 +7,38 @@ 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.1.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
43
  "signature",
26
44
  "initials",
@@ -40,7 +58,6 @@ const exampleChoices = [
40
58
  "ios-owned-sender",
41
59
  ];
42
60
  const packageManagerChoices = ["npm", "pnpm", "yarn", "bun"];
43
- const apiEndpointChoices = ["deployment", "local"];
44
61
  export function createNodeRuntime() {
45
62
  const cwd = process.cwd();
46
63
  const env = cliEnvironmentForWorkspace(cwd, process.env);
@@ -97,7 +114,7 @@ async function runChildCommand(command, args) {
97
114
  async function openExternalUrl(url) {
98
115
  const parsed = new URL(url);
99
116
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
100
- throw new Error("Only HTTP(S) URLs can be opened.");
117
+ throw new Error(t("error.httpUrlsOnly"));
101
118
  }
102
119
  const [command, args] = process.platform === "darwin"
103
120
  ? ["open", [url]]
@@ -148,10 +165,10 @@ function createTerminalPrompter(input, output) {
148
165
  }
149
166
  function terminalSelect(input, output, options) {
150
167
  if (options.choices.length === 0) {
151
- throw new Error(`No choices available for ${options.message}.`);
168
+ throw new Error(t("error.noChoices", { message: options.message }));
152
169
  }
153
170
  if (!input.isTTY || !output.isTTY || !input.setRawMode) {
154
- throw new Error("Interactive setup requires a TTY.");
171
+ throw new Error(t("error.interactiveNeedsTty"));
155
172
  }
156
173
  const defaultIndex = Math.max(0, options.choices.findIndex((choiceOption) => choiceOption.value === options.defaultValue));
157
174
  let selectedIndex = defaultIndex;
@@ -225,7 +242,13 @@ function selectPromptLines(options, selectedIndex) {
225
242
  const hint = choiceOption.hint ? ` ${style("dim", choiceOption.hint)}` : "";
226
243
  lines.push(`${marker} ${label}${hint}`);
227
244
  }
228
- lines.push(`${style("dim", "↑/↓")} move · ${style("dim", "Enter")} select · ${style("dim", "Ctrl+C")} quit`);
245
+ // Rendered in the active locale, which for the language question itself is
246
+ // English — nothing has been chosen yet at that point.
247
+ lines.push([
248
+ `${style("dim", "↑/↓")} ${t("prompt.control.move")}`,
249
+ `${style("dim", "Enter")} ${t("prompt.control.select")}`,
250
+ `${style("dim", "Ctrl+C")} ${t("prompt.control.quit")}`,
251
+ ].join(" · "));
229
252
  return lines;
230
253
  }
231
254
  function clearRendered(output, lineCount) {
@@ -241,13 +264,61 @@ function style(kind, value) {
241
264
  };
242
265
  return `${codes[kind]}${value}\x1b[0m`;
243
266
  }
267
+ /**
268
+ * How the locale for this run was decided. Only `language` reports it, but it
269
+ * has to be recorded where the decision is actually made.
270
+ */
271
+ let resolvedLocaleSource = "default";
272
+ /**
273
+ * The language the CLI's *chrome* resolved to. It can differ from the active
274
+ * locale on a machine-readable run, which is forced to English — `language`
275
+ * must still report what the reader actually chose.
276
+ */
277
+ let resolvedChromeLocale = SOURCE_LOCALE;
244
278
  export async function runCli(args, runtime = createNodeRuntime()) {
279
+ // The language flag is global, so it is taken out before per-command parsing
280
+ // — which rejects any option the command has not declared.
281
+ const { value: languageFlag, rest: cliArgs } = extractLanguageFlag(args);
245
282
  try {
246
- const command = args[0];
247
- const commandArgs = args.slice(1);
283
+ const stored = await readStoredLanguage(runtime);
284
+ const resolution = resolveLocale({
285
+ flag: languageFlag,
286
+ env: runtime.env,
287
+ stored,
288
+ });
289
+ if (!resolution.ok) {
290
+ // Report the bad value in whatever language the reader already chose,
291
+ // falling back to English when they have not chosen one.
292
+ setActiveLocale(normalizeLocale(stored) ?? SOURCE_LOCALE);
293
+ const message = t("language.unknown", {
294
+ value: resolution.value,
295
+ supported: supportedLocaleList(),
296
+ variable: LANGUAGE_ENV_VARS[0],
297
+ });
298
+ return requestedJsonOutput(args)
299
+ ? jsonFailure("language", new Error(message))
300
+ : fail(message);
301
+ }
302
+ // Machine-readable output is a data interchange format, not chrome: it
303
+ // stays English so an agent quoting a label back to its user does not
304
+ // suddenly speak the operator's language. `--language` on the same
305
+ // invocation is a deliberate request and does localize it; an ambient
306
+ // stored choice or environment variable does not.
307
+ const machineReadable = machineReadableOutput(cliArgs);
308
+ const effectiveLocale = machineReadable && resolution.source !== "flag"
309
+ ? SOURCE_LOCALE
310
+ : resolution.locale;
311
+ setActiveLocale(effectiveLocale);
312
+ resolvedChromeLocale = resolution.locale;
313
+ resolvedLocaleSource = resolution.source;
314
+ // Unresolved means "never asked". Interactive entry points ask; every
315
+ // other invocation — CI, a pipe, an agent shelling out — stays English.
316
+ const askLanguage = resolution.needsPrompt;
317
+ const command = cliArgs[0];
318
+ const commandArgs = cliArgs.slice(1);
248
319
  if (command == null) {
249
320
  if (runtime.wizardTerminal) {
250
- return await setupWizardCommand(runtime);
321
+ return await setupWizardCommand(runtime, askLanguage);
251
322
  }
252
323
  return ok(mainHelpText());
253
324
  }
@@ -262,6 +333,8 @@ export async function runCli(args, runtime = createNodeRuntime()) {
262
333
  return ok(guideText());
263
334
  case "options":
264
335
  return optionsCommand(commandArgs);
336
+ case "language":
337
+ return await languageCommand(commandArgs, runtime);
265
338
  case "login":
266
339
  return await loginCommand(commandArgs, runtime);
267
340
  case "logout":
@@ -269,7 +342,7 @@ export async function runCli(args, runtime = createNodeRuntime()) {
269
342
  case "whoami":
270
343
  return await whoamiCommand(commandArgs, runtime);
271
344
  case "new":
272
- return await newCommand(commandArgs, runtime);
345
+ return await newCommand(commandArgs, runtime, askLanguage);
273
346
  case "init":
274
347
  return await initCommand(commandArgs, runtime);
275
348
  case "update":
@@ -286,20 +359,36 @@ export async function runCli(args, runtime = createNodeRuntime()) {
286
359
  return await agentCommand(commandArgs, runtime);
287
360
  default:
288
361
  return requestedJsonOutput(args)
289
- ? jsonFailure(command, new Error(`Unknown command "${command}". Run remotedraw --help.`))
290
- : fail(`Unknown command "${command}". Run remotedraw --help.`);
362
+ ? jsonFailure(command, new Error(t("error.unknownCommand", { command })))
363
+ : fail(t("error.unknownCommand", { command }));
291
364
  }
292
365
  }
293
366
  catch (error) {
367
+ // Only genuine faults land here: a bad flag or an unknown command returns a
368
+ // `fail(...)` result instead of throwing, so this path is not user error.
369
+ const command = cliArgs[0] ?? "help";
370
+ await reportCliCrash(error, command, cliTelemetryEnvironment(runtime));
294
371
  return requestedJsonOutput(args)
295
- ? jsonFailure(args[0] ?? "help", error)
372
+ ? jsonFailure(command, error)
296
373
  : fail(errorMessage(error));
297
374
  }
298
375
  }
299
- async function setupWizardCommand(runtime) {
376
+ function cliTelemetryEnvironment(runtime) {
377
+ return {
378
+ env: runtime.env,
379
+ version: CLI_VERSION,
380
+ platform: process.platform,
381
+ nodeVersion: process.versions.node,
382
+ fetch: runtime.fetch,
383
+ };
384
+ }
385
+ async function setupWizardCommand(runtime, askLanguage) {
300
386
  const terminal = runtime.wizardTerminal;
301
387
  if (!terminal)
302
388
  return ok(mainHelpText());
389
+ // Before login, before the first field: nothing the reader has to understand
390
+ // is printed until the language is settled.
391
+ await ensureLanguageChosen(runtime, askLanguage);
303
392
  if (!cloudSetupDisabled(runtime)) {
304
393
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env);
305
394
  await loginToRemoteDraw(runtime, apiBaseUrl);
@@ -324,6 +413,121 @@ async function setupWizardCommand(runtime) {
324
413
  });
325
414
  return { exitCode: result.exitCode, stdout: "" };
326
415
  }
416
+ /**
417
+ * The language question, asked once and only where a human is already being
418
+ * prompted. The question itself is built from every locale's own word for
419
+ * "Language" and lists endonyms, so it reads before any language is chosen.
420
+ */
421
+ async function ensureLanguageChosen(runtime, askLanguage) {
422
+ if (!askLanguage)
423
+ return;
424
+ const prompts = runtime.prompts;
425
+ // No TTY, no question: piped and CI runs must never block on this.
426
+ if (!prompts || runtime.isInteractive !== true)
427
+ return;
428
+ let chosen;
429
+ try {
430
+ chosen = await prompts.select({
431
+ message: languagePromptMessage(),
432
+ defaultValue: systemLocaleHint(runtime.env) ?? SOURCE_LOCALE,
433
+ choices: languageChoices(),
434
+ });
435
+ }
436
+ catch {
437
+ // A prompter that cannot run (stdin is not a real terminal after all)
438
+ // means English, never a failed command over a preference question.
439
+ return;
440
+ }
441
+ const locale = normalizeLocale(chosen) ?? SOURCE_LOCALE;
442
+ await applyLanguage(runtime, locale);
443
+ }
444
+ /** Sets the locale for the rest of this run and remembers it for later ones. */
445
+ async function applyLanguage(runtime, locale) {
446
+ setActiveLocale(locale);
447
+ resolvedChromeLocale = locale;
448
+ resolvedLocaleSource = "prompt";
449
+ try {
450
+ await writeStoredLanguage(runtime, locale);
451
+ }
452
+ catch (error) {
453
+ // An unwritable config is worth saying out loud, but it must not abort the
454
+ // command the reader actually asked for.
455
+ runtime.notify?.(t("language.saveFailed", {
456
+ reason: errorMessage(error),
457
+ language: localeEndonym(locale),
458
+ }));
459
+ }
460
+ }
461
+ async function languageCommand(args, runtime) {
462
+ const parsed = parseArgs(args, { values: ["format"], booleans: ["help"] });
463
+ if (parsed.booleans.has("help"))
464
+ return ok(languageHelpText());
465
+ const format = outputFormat(parsed);
466
+ const requested = parsed.positionals[0];
467
+ if (requested != null) {
468
+ const locale = normalizeLocale(requested);
469
+ if (!locale) {
470
+ const message = t("language.unknown", {
471
+ value: requested,
472
+ supported: supportedLocaleList(),
473
+ variable: LANGUAGE_ENV_VARS[0],
474
+ });
475
+ return format === "json"
476
+ ? jsonFailure("language", new Error(message))
477
+ : fail(message);
478
+ }
479
+ await applyLanguage(runtime, locale);
480
+ return format === "json"
481
+ ? jsonResult({ ok: true, command: "language", language: locale })
482
+ : ok(t("language.saved", { language: localeEndonym(locale) }));
483
+ }
484
+ if (format !== "json" && runtime.prompts && runtime.isInteractive === true) {
485
+ const chosen = await runtime.prompts.select({
486
+ message: languagePromptMessage(),
487
+ defaultValue: resolvedChromeLocale,
488
+ choices: languageChoices(),
489
+ });
490
+ const locale = normalizeLocale(chosen) ?? SOURCE_LOCALE;
491
+ await applyLanguage(runtime, locale);
492
+ return ok(t("language.saved", { language: localeEndonym(locale) }));
493
+ }
494
+ const current = resolvedChromeLocale;
495
+ if (format === "json") {
496
+ return jsonResult({
497
+ ok: true,
498
+ command: "language",
499
+ language: current,
500
+ source: resolvedLocaleSource,
501
+ available: SUPPORTED_LOCALES.map((locale) => ({
502
+ code: locale,
503
+ endonym: localeEndonym(locale),
504
+ })),
505
+ });
506
+ }
507
+ return ok([
508
+ t("language.current", {
509
+ language: localeEndonym(current),
510
+ locale: current,
511
+ }),
512
+ languageSourceLine(runtime),
513
+ "",
514
+ t("language.available"),
515
+ ...SUPPORTED_LOCALES.map((locale) => ` ${locale.padEnd(6)} ${localeEndonym(locale)}`),
516
+ ].join("\n"));
517
+ }
518
+ function languageSourceLine(runtime) {
519
+ if (resolvedLocaleSource === "flag")
520
+ return t("language.source.flag");
521
+ if (resolvedLocaleSource === "env") {
522
+ const variable = LANGUAGE_ENV_VARS.find((name) => runtime.env[name]?.trim()) ??
523
+ LANGUAGE_ENV_VARS[0];
524
+ return t("language.source.env", { variable });
525
+ }
526
+ if (resolvedLocaleSource === "config" || resolvedLocaleSource === "prompt") {
527
+ return t("language.source.config", { path: globalConfigPath(runtime) });
528
+ }
529
+ return t("language.source.default");
530
+ }
327
531
  function ok(stdout) {
328
532
  return { exitCode: 0, stdout: ensureTrailingNewline(stdout) };
329
533
  }
@@ -335,6 +539,12 @@ function jsonResult(value, exitCode = 0) {
335
539
  }
336
540
  function jsonFailure(command, error) {
337
541
  const message = errorMessage(error);
542
+ // A declared code survives translation; the substring heuristic below is
543
+ // only a fallback for errors raised outside the catalog (a failed fetch, a
544
+ // filesystem fault) and is matched against English internals.
545
+ if (error instanceof CliError) {
546
+ return jsonResult({ ok: false, command, error: { code: error.code, message } }, 1);
547
+ }
338
548
  const code = message.includes("required")
339
549
  ? "MISSING_ARGUMENT"
340
550
  : message.includes(" requires ") || message.includes("only supported")
@@ -348,6 +558,14 @@ function jsonFailure(command, error) {
348
558
  : "COMMAND_FAILED";
349
559
  return jsonResult({ ok: false, command, error: { code, message } }, 1);
350
560
  }
561
+ /**
562
+ * True when this invocation asks for output meant to be parsed rather than
563
+ * read: `--format json` on any command, or `create-input --json`.
564
+ */
565
+ function machineReadableOutput(args) {
566
+ return (requestedJsonOutput(args) ||
567
+ args.some((argument) => argument === "--json"));
568
+ }
351
569
  function requestedJsonOutput(args) {
352
570
  return args.some((argument, index) => argument === "--format=json" ||
353
571
  (argument === "--format" && args[index + 1] === "json"));
@@ -365,32 +583,34 @@ function errorMessage(error) {
365
583
  return error instanceof Error ? error.message : String(error);
366
584
  }
367
585
  function mainHelpText() {
586
+ const command = (name, description) => ` ${name.padEnd(14)} ${description}`;
368
587
  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:",
588
+ t("help.title"),
589
+ "",
590
+ t("word.usage"),
591
+ t("help.usage.main"),
592
+ "",
593
+ t("word.commands"),
594
+ command("guide", t("help.command.guide")),
595
+ command("options", t("help.command.options")),
596
+ command("language", t("help.command.language")),
597
+ command("login", t("help.command.login")),
598
+ command("logout", t("help.command.logout")),
599
+ command("whoami", t("help.command.whoami")),
600
+ command("new", t("help.command.new")),
601
+ command("init", t("help.command.init")),
602
+ command("dev", t("help.command.dev")),
603
+ command("doctor", t("help.command.doctor")),
604
+ command("update", t("help.command.update")),
605
+ command("create-input", t("help.command.createInput")),
606
+ command("examples", t("help.command.examples")),
607
+ command("agent", t("help.command.agent")),
608
+ "",
609
+ t("help.startHere"),
390
610
  " remotedraw guide",
391
611
  " remotedraw login",
392
612
  " remotedraw new",
393
- " remotedraw new --app-name MijnApp --target web --sender remotedraw-ios --sdk react",
613
+ " remotedraw new --app-name MyApp --target web --sender remotedraw-ios --sdk react",
394
614
  " remotedraw init --target web --sender embedded-web --sdk react",
395
615
  ].join("\n");
396
616
  }
@@ -405,39 +625,39 @@ function optionsCommand(args) {
405
625
  if (outputFormat(parsed) === "json")
406
626
  return jsonResult(catalog);
407
627
  return ok([
408
- "RemoteDraw setup options",
628
+ t("options.title"),
409
629
  "",
410
630
  ...catalog.fields.flatMap((field) => [
411
631
  `${field.label} (${field.flag})`,
412
632
  ...field.options.map((option) => ` ${option.value}: ${option.details.summary}`),
413
633
  "",
414
634
  ]),
415
- "For machine-readable metadata: remotedraw options --format json",
635
+ t("options.machineReadable"),
416
636
  ].join("\n"));
417
637
  }
418
638
  function guideText() {
419
639
  return [
420
- "RemoteDraw integration guide",
640
+ t("guide.title"),
421
641
  "",
422
- "Choose the surface your customer already has:",
423
- " Web app receiver + RemoteDraw iOS app sender",
642
+ t("guide.intro"),
643
+ t("guide.path.webIos"),
424
644
  " remotedraw init --target web --sender remotedraw-ios --sdk react --preset signature",
425
645
  "",
426
- " Web app receiver + your own web sender UI",
646
+ t("guide.path.webOwned"),
427
647
  " remotedraw init --target web --sender embedded-web --sdk react --preset sketch",
428
648
  "",
429
- " Svelte app receiver + RemoteDraw iOS app sender",
649
+ t("guide.path.svelteIos"),
430
650
  " remotedraw init --target web --sender remotedraw-ios --sdk svelte --preset signature",
431
651
  "",
432
- " Desktop app receiver with raw HTTP",
652
+ t("guide.path.desktop"),
433
653
  " remotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch",
434
654
  "",
435
- " Your own iOS sender app",
655
+ t("guide.path.ownIos"),
436
656
  " remotedraw init --target ios --sender own-ios --sdk swift --preset sketch",
437
657
  "",
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.",
658
+ t("guide.note.keys"),
659
+ t("guide.note.provisioning"),
660
+ t("guide.note.offline"),
441
661
  ].join("\n");
442
662
  }
443
663
  async function loginCommand(args, runtime) {
@@ -455,11 +675,9 @@ async function loginCommand(args, runtime) {
455
675
  ...(deviceName == null ? {} : { deviceName }),
456
676
  });
457
677
  return ok([
458
- result.alreadyLoggedIn
459
- ? "Already logged in."
460
- : "RemoteDraw login complete.",
678
+ result.alreadyLoggedIn ? t("login.already") : t("login.complete"),
461
679
  accountSummary(result.account),
462
- `Credential: ${globalConfigPath(runtime)}`,
680
+ t("login.credential", { path: globalConfigPath(runtime) }),
463
681
  ].join("\n"));
464
682
  }
465
683
  async function logoutCommand(args, runtime) {
@@ -472,8 +690,8 @@ async function logoutCommand(args, runtime) {
472
690
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
473
691
  const result = await logoutFromRemoteDraw(runtime, apiBaseUrl);
474
692
  return ok(result.hadCredential
475
- ? `Logged out from ${apiBaseUrl}.`
476
- : `No stored credential for ${apiBaseUrl}.`);
693
+ ? t("logout.done", { url: apiBaseUrl })
694
+ : t("logout.none", { url: apiBaseUrl }));
477
695
  }
478
696
  async function whoamiCommand(args, runtime) {
479
697
  const parsed = parseArgs(args, {
@@ -485,15 +703,15 @@ async function whoamiCommand(args, runtime) {
485
703
  const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
486
704
  const account = await currentCliAccount(runtime, apiBaseUrl);
487
705
  if (!account) {
488
- return fail(`Not logged in to ${apiBaseUrl}. Run remotedraw login.`);
706
+ return fail(t("whoami.notLoggedIn", { url: apiBaseUrl }));
489
707
  }
490
- return ok([accountSummary(account), `API: ${apiBaseUrl}`].join("\n"));
708
+ return ok([accountSummary(account), t("whoami.api", { url: apiBaseUrl })].join("\n"));
491
709
  }
492
710
  function accountSummary(account) {
493
- const identity = account.email || account.name || "RemoteDraw user";
711
+ const identity = account.email || account.name || t("whoami.anonymous");
494
712
  return `${identity} (${account.tenantSlug})`;
495
713
  }
496
- async function newCommand(args, runtime) {
714
+ async function newCommand(args, runtime, askLanguage = false) {
497
715
  const parsed = parseArgs(args, {
498
716
  values: [
499
717
  "app-name",
@@ -511,8 +729,13 @@ async function newCommand(args, runtime) {
511
729
  if (parsed.booleans.has("help"))
512
730
  return ok(newHelpText());
513
731
  const format = outputFormat(parsed);
514
- const plan = !parsed.booleans.has("non-interactive") &&
515
- shouldPromptForNewProject(parsed, runtime)
732
+ const interactive = !parsed.booleans.has("non-interactive") &&
733
+ shouldPromptForNewProject(parsed, runtime);
734
+ // Only ask when this run was going to prompt anyway; a fully flagged
735
+ // `new` stays as non-interactive as the caller wrote it.
736
+ if (interactive)
737
+ await ensureLanguageChosen(runtime, askLanguage);
738
+ const plan = interactive
516
739
  ? await promptForNewProject(parsed, runtime)
517
740
  : planFromArgs(parsed, {
518
741
  appNameRequired: true,
@@ -537,7 +760,7 @@ async function newCommand(args, runtime) {
537
760
  }
538
761
  return format === "json"
539
762
  ? jsonResult(scaffoldJsonResult("new", plan, outputDir, written, parsed, provisioning, offline, dryRun))
540
- : ok(scaffoldSummary("Created", plan, outputDir, written, dryRun) +
763
+ : ok(scaffoldSummary(t("scaffold.verb.created"), plan, outputDir, written, dryRun) +
541
764
  cloudSetupSummary(provisioning, offline, dryRun));
542
765
  }
543
766
  export async function createRemoteDrawProject(plan, outputDir, runtime, options = {}) {
@@ -594,7 +817,7 @@ async function initCommand(args, runtime) {
594
817
  }
595
818
  return format === "json"
596
819
  ? jsonResult(scaffoldJsonResult("init", plan, outputDir, written, parsed, provisioning, offline, dryRun))
597
- : ok(scaffoldSummary("Initialized", plan, outputDir, written, dryRun) +
820
+ : ok(scaffoldSummary(t("scaffold.verb.initialized"), plan, outputDir, written, dryRun) +
598
821
  cloudSetupSummary(provisioning, offline, dryRun));
599
822
  }
600
823
  function cloudSetupDisabled(runtime) {
@@ -616,16 +839,16 @@ function cloudSetupSummary(provisioning, offline, dryRun) {
616
839
  if (dryRun)
617
840
  return "";
618
841
  if (offline) {
619
- return "\n\nCloud setup skipped (--offline). Run remotedraw init later to create a dashboard project.";
842
+ return `\n\n${t("cloud.skipped")}`;
620
843
  }
621
844
  if (!provisioning)
622
845
  return "";
623
846
  return [
624
847
  "",
625
848
  "",
626
- `Dashboard project: ${provisioning.project.dashboardUrl}`,
627
- "Development API key: written to .env.local (server-side only)",
628
- `Project ID: ${provisioning.project.id}`,
849
+ t("cloud.dashboardProject", { url: provisioning.project.dashboardUrl }),
850
+ t("cloud.devKey"),
851
+ t("cloud.projectId", { id: provisioning.project.id }),
629
852
  ].join("\n");
630
853
  }
631
854
  const DOCTOR_PROBE_TIMEOUT_MS = 8_000;
@@ -634,7 +857,7 @@ function probeFailureMessage(error) {
634
857
  }
635
858
  async function probeApiHealth(runtime, apiBaseUrl) {
636
859
  if (!runtime.fetch) {
637
- return { ok: false, message: "this runtime cannot make network requests" };
860
+ return { ok: false, message: t("doctor.probe.noNetwork") };
638
861
  }
639
862
  try {
640
863
  const response = await runtime.fetch(`${apiBaseUrl}/health`, {
@@ -657,7 +880,7 @@ async function probeApiHealth(runtime, apiBaseUrl) {
657
880
  */
658
881
  async function probeApiKey(runtime, apiBaseUrl, apiKey) {
659
882
  if (!runtime.fetch) {
660
- return { ok: false, message: "This runtime cannot make network requests." };
883
+ return { ok: false, message: t("doctor.probe.noNetworkKey") };
661
884
  }
662
885
  try {
663
886
  const response = await runtime.fetch(`${apiBaseUrl}/v1/sessions/list`, {
@@ -677,7 +900,7 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
677
900
  catch {
678
901
  return {
679
902
  ok: false,
680
- message: `Deployment returned a non-JSON response (HTTP ${response.status}).`,
903
+ message: t("doctor.probe.nonJson", { status: response.status }),
681
904
  };
682
905
  }
683
906
  if (!response.ok) {
@@ -687,7 +910,7 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
687
910
  typeof payload.error?.message ===
688
911
  "string"
689
912
  ? payload.error.message
690
- : `Deployment rejected the key (HTTP ${response.status}).`;
913
+ : t("doctor.probe.rejected", { status: response.status });
691
914
  return { ok: false, message };
692
915
  }
693
916
  const items = typeof payload === "object" && payload !== null && "items" in payload
@@ -718,13 +941,19 @@ async function updateCommand(args, runtime) {
718
941
  // ambient opt-outs that only silence the passive notice.
719
942
  const check = await checkForUpdate(runtime, CLI_VERSION, { force: true });
720
943
  if (!check) {
721
- const message = `Could not reach the npm registry to check for ${CLI_PACKAGE_NAME} updates. Run ${command} to update anyway.`;
944
+ const message = t("update.unreachable", {
945
+ package: CLI_PACKAGE_NAME,
946
+ command,
947
+ });
722
948
  return format === "json"
723
949
  ? jsonFailure("update", new Error(message))
724
950
  : fail(message);
725
951
  }
726
952
  if (!check.updateAvailable) {
727
- const message = `${CLI_PACKAGE_NAME} ${check.current} is already the latest version.`;
953
+ const message = t("update.alreadyLatest", {
954
+ package: CLI_PACKAGE_NAME,
955
+ version: check.current,
956
+ });
728
957
  return format === "json"
729
958
  ? jsonResult({ ok: true, command: "update", ...check, updated: false })
730
959
  : ok(message);
@@ -732,12 +961,18 @@ async function updateCommand(args, runtime) {
732
961
  if (checkOnly || method.ephemeral) {
733
962
  const message = method.ephemeral
734
963
  ? [
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.`,
964
+ t("update.available", {
965
+ current: check.current,
966
+ latest: check.latest,
967
+ }),
968
+ t("update.ephemeral", { package: CLI_PACKAGE_NAME }),
737
969
  ].join("\n")
738
970
  : [
739
- `Update available: ${check.current} -> ${check.latest}.`,
740
- `Run ${command} to install it.`,
971
+ t("update.available", {
972
+ current: check.current,
973
+ latest: check.latest,
974
+ }),
975
+ t("update.runToInstall", { command }),
741
976
  ].join("\n");
742
977
  return format === "json"
743
978
  ? jsonResult({
@@ -752,7 +987,11 @@ async function updateCommand(args, runtime) {
752
987
  }
753
988
  const install = await runInstall(runtime, method);
754
989
  if (!install.ran) {
755
- const message = `Update available: ${check.current} -> ${check.latest}. Run ${command} to install it.`;
990
+ const message = t("update.availableRun", {
991
+ current: check.current,
992
+ latest: check.latest,
993
+ command,
994
+ });
756
995
  return format === "json"
757
996
  ? jsonResult({
758
997
  ok: true,
@@ -764,7 +1003,10 @@ async function updateCommand(args, runtime) {
764
1003
  : ok(message);
765
1004
  }
766
1005
  if (install.exitCode !== 0) {
767
- const message = `${command} failed with exit code ${install.exitCode}.`;
1006
+ const message = t("update.failed", {
1007
+ command,
1008
+ code: install.exitCode,
1009
+ });
768
1010
  return format === "json"
769
1011
  ? jsonFailure("update", new Error(message))
770
1012
  : fail(message);
@@ -777,7 +1019,10 @@ async function updateCommand(args, runtime) {
777
1019
  updated: true,
778
1020
  installCommand: command,
779
1021
  })
780
- : ok(`Updated ${CLI_PACKAGE_NAME} to ${check.latest}.`);
1022
+ : ok(t("update.done", {
1023
+ package: CLI_PACKAGE_NAME,
1024
+ version: check.latest,
1025
+ }));
781
1026
  }
782
1027
  async function doctorCommand(args, runtime) {
783
1028
  const parsed = parseArgs(args, {
@@ -796,28 +1041,29 @@ async function doctorCommand(args, runtime) {
796
1041
  const apiBaseUrl = resolveDoctorApiBaseUrl(projectEnv, readString(parsed, "api-base-url"));
797
1042
  const normalizedEnvApiBaseUrl = apiBaseUrl.state === "ok" ? apiBaseUrl.url : undefined;
798
1043
  const envApiKey = projectEnv.REMOTEDRAW_API_KEY;
799
- const lines = ["RemoteDraw doctor", ""];
1044
+ const lines = [t("doctor.title"), ""];
800
1045
  const checks = [];
801
1046
  const addCheck = (state, label, message) => {
802
1047
  checks.push({ state, label, message });
803
1048
  lines.push(statusLine(state, label, message));
804
1049
  };
805
1050
  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.");
1051
+ addCheck(hasConfig ? "ok" : "warn", "remotedraw.config.json", hasConfig ? t("doctor.config.ok") : t("doctor.config.missing"));
809
1052
  const packageJson = (await runtime.exists(packagePath))
810
1053
  ? await readJsonFile(packagePath, runtime)
811
1054
  : null;
812
- addCheck(packageJson ? "ok" : "warn", "package.json", packageJson
813
- ? "Package metadata found."
814
- : "No package.json found in this directory.");
1055
+ addCheck(packageJson ? "ok" : "warn", "package.json", packageJson ? t("doctor.package.ok") : t("doctor.package.missing"));
815
1056
  const config = hasConfig ? await readJsonFile(configPath, runtime) : null;
816
1057
  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.");
1058
+ const sdkPackage = sdkPackageName(sdk);
1059
+ if (sdkPackage) {
1060
+ const installed = hasDependency(packageJson, sdkPackage);
1061
+ addCheck(installed ? "ok" : "warn", sdkPackage, installed
1062
+ ? t("doctor.sdk.ok", { package: sdkPackage })
1063
+ : t("doctor.sdk.missing", {
1064
+ package: sdkPackage,
1065
+ range: REMOTEDRAW_SDK_RANGE,
1066
+ }));
821
1067
  }
822
1068
  addCheck(apiBaseUrl.state === "ok"
823
1069
  ? "ok"
@@ -825,47 +1071,55 @@ async function doctorCommand(args, runtime) {
825
1071
  ? "warn"
826
1072
  : "error", "REMOTEDRAW_API_BASE_URL", doctorApiBaseUrlMessage(apiBaseUrl));
827
1073
  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.");
1074
+ ? t("doctor.apiKey.ok")
1075
+ : t("doctor.apiKey.missing"));
830
1076
  // The checks above only prove the strings look right. A revoked key, a URL
831
1077
  // pointing at the wrong deployment, and an exhausted credit balance all pass
832
1078
  // them, so doctor also asks the deployment itself.
833
1079
  if (offline) {
834
- addCheck("warn", "deployment", "Skipped network checks (--offline). Rerun without --offline to verify the key against the deployment.");
1080
+ addCheck("warn", t("doctor.label.deployment"), t("doctor.network.offline"));
835
1081
  }
836
1082
  else if (!normalizedEnvApiBaseUrl) {
837
- addCheck("warn", "deployment", "Skipped network checks because REMOTEDRAW_API_BASE_URL is not a usable HTTP origin.");
1083
+ addCheck("warn", t("doctor.label.deployment"), t("doctor.network.noOrigin"));
838
1084
  }
839
1085
  else {
840
1086
  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}`);
1087
+ addCheck(health.ok ? "ok" : "error", t("doctor.label.deploymentReachable"), health.ok
1088
+ ? t("doctor.health.ok", { url: normalizedEnvApiBaseUrl })
1089
+ : t("doctor.health.failed", {
1090
+ url: normalizedEnvApiBaseUrl,
1091
+ reason: health.message,
1092
+ }));
844
1093
  if (!envApiKey?.startsWith("rd_sk_")) {
845
- addCheck("warn", "API key accepted", "Skipped because no rd_sk_... key was found in this project's environment.");
1094
+ addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoKey"));
846
1095
  }
847
1096
  else if (!health.ok) {
848
- addCheck("warn", "API key accepted", "Skipped because the deployment did not answer.");
1097
+ addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoHealth"));
849
1098
  }
850
1099
  else {
851
1100
  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.`
1101
+ addCheck(probe.ok ? "ok" : "error", t("doctor.label.apiKeyAccepted"), probe.ok
1102
+ ? t(probe.sessionCount === 1
1103
+ ? "doctor.key.liveOne"
1104
+ : "doctor.key.liveMany", { count: probe.sessionCount })
854
1105
  : probe.message);
855
1106
  }
856
1107
  }
857
1108
  // A stale global install is the quietest failure mode there is: npm pins the
858
1109
  // binary at install time and never revisits it, so doctor has to say so.
859
1110
  if (offline) {
860
- addCheck("warn", "CLI version", `Running ${CLI_VERSION}. Skipped the update check (--offline).`);
1111
+ addCheck("warn", t("doctor.label.cliVersion"), t("doctor.version.offline", { version: CLI_VERSION }));
861
1112
  }
862
1113
  else {
863
1114
  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.`
1115
+ addCheck(!update || !update.updateAvailable ? "ok" : "warn", t("doctor.label.cliVersion"), !update
1116
+ ? t("doctor.version.unknown", { version: CLI_VERSION })
866
1117
  : update.updateAvailable
867
- ? `Running ${update.current}; ${update.latest} is published. Run remotedraw update.`
868
- : `Running ${update.current}, the latest published version.`);
1118
+ ? t("doctor.version.stale", {
1119
+ current: update.current,
1120
+ latest: update.latest,
1121
+ })
1122
+ : t("doctor.version.latest", { current: update.current }));
869
1123
  }
870
1124
  return format === "json"
871
1125
  ? jsonResult({
@@ -888,23 +1142,22 @@ async function devCommand(args, runtime) {
888
1142
  const projectEnv = await environmentForProject(dir, runtime);
889
1143
  const apiBaseUrl = readString(parsed, "api-base-url") ??
890
1144
  projectEnv.REMOTEDRAW_API_BASE_URL ??
891
- "http://localhost:3210";
1145
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
892
1146
  return ok([
893
- "RemoteDraw local development workflow",
1147
+ t("dev.title"),
894
1148
  "",
895
- `API base URL: ${apiBaseUrl}`,
1149
+ t("dev.apiBaseUrl", { url: apiBaseUrl }),
896
1150
  "",
897
- "1. Start the RemoteDraw API/web service for local testing.",
898
- " Inside this repository: bun run dev:codex",
1151
+ t("dev.hosted"),
899
1152
  "",
900
- "2. Start your app's dev server.",
901
- " Use the package manager and dev script generated by remotedraw new/init.",
1153
+ t("dev.step1"),
1154
+ t("dev.step1.detail"),
902
1155
  "",
903
- "3. Create a test input request from trusted backend code.",
1156
+ t("dev.step2"),
904
1157
  ` remotedraw create-input --preset signature --api-base-url ${apiBaseUrl}`,
905
1158
  "",
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.",
1159
+ t("dev.step3"),
1160
+ t("dev.step3.detail"),
908
1161
  ].join("\n"));
909
1162
  }
910
1163
  async function createInputCommand(args, runtime) {
@@ -925,7 +1178,7 @@ async function createInputCommand(args, runtime) {
925
1178
  const preset = choice(readString(parsed, "preset") ?? "signature", presetChoices, "preset");
926
1179
  const apiBaseUrl = readString(parsed, "api-base-url") ??
927
1180
  projectEnv.REMOTEDRAW_API_BASE_URL ??
928
- "https://<deployment>.convex.site";
1181
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
929
1182
  const payload = createSessionPayload({
930
1183
  preset,
931
1184
  label: readString(parsed, "label") ?? labelForPreset(preset),
@@ -935,9 +1188,9 @@ async function createInputCommand(args, runtime) {
935
1188
  if (parsed.booleans.has("execute")) {
936
1189
  const apiKey = readString(parsed, "api-key") ?? projectEnv.REMOTEDRAW_API_KEY;
937
1190
  if (!runtime.fetch)
938
- throw new Error("This runtime cannot execute HTTP requests.");
1191
+ throw new Error(t("createInput.error.noFetch"));
939
1192
  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.");
1193
+ throw new Error(t("createInput.error.noKey"));
941
1194
  }
942
1195
  const response = await runtime.fetch(`${normalizedOrigin(apiBaseUrl)}/v1/sessions`, {
943
1196
  method: "POST",
@@ -949,20 +1202,20 @@ async function createInputCommand(args, runtime) {
949
1202
  });
950
1203
  const text = await response.text();
951
1204
  if (!response.ok)
952
- throw new Error(text || `RemoteDraw API returned ${response.status}.`);
1205
+ throw new Error(text || t("createInput.error.apiStatus", { status: response.status }));
953
1206
  return ok(text);
954
1207
  }
955
1208
  if (parsed.booleans.has("json")) {
956
1209
  return ok(JSON.stringify(payload, null, 2));
957
1210
  }
958
1211
  return ok([
959
- "RemoteDraw test input request",
1212
+ t("createInput.title"),
960
1213
  "",
961
- "Create this from trusted backend code. Do not run API-key requests in a browser client.",
1214
+ t("createInput.warning"),
962
1215
  "",
963
1216
  curlForCreateInput(apiBaseUrl, payload),
964
1217
  "",
965
- "Use --json to print only the request body or --execute to call the API with REMOTEDRAW_API_KEY.",
1218
+ t("createInput.footer"),
966
1219
  ].join("\n"));
967
1220
  }
968
1221
  async function examplesCommand(args, runtime) {
@@ -975,14 +1228,14 @@ async function examplesCommand(args, runtime) {
975
1228
  const example = readString(parsed, "install");
976
1229
  if (example == null || parsed.booleans.has("list")) {
977
1230
  return ok([
978
- "RemoteDraw examples",
1231
+ t("examples.title"),
979
1232
  "",
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.",
1233
+ ` ${"react-ios".padEnd(19)} ${t("examples.reactIos")}`,
1234
+ ` ${"react-owned-sender".padEnd(19)} ${t("examples.reactOwnedSender")}`,
1235
+ ` ${"raw-http".padEnd(19)} ${t("examples.rawHttp")}`,
1236
+ ` ${"ios-owned-sender".padEnd(19)} ${t("examples.iosOwnedSender")}`,
984
1237
  "",
985
- "Install one:",
1238
+ t("examples.installOne"),
986
1239
  " remotedraw examples --install react-ios --path ./remotedraw-react-ios",
987
1240
  ].join("\n"));
988
1241
  }
@@ -1000,7 +1253,7 @@ async function examplesCommand(args, runtime) {
1000
1253
  dryRun,
1001
1254
  force: parsed.booleans.has("force"),
1002
1255
  });
1003
- return ok(scaffoldSummary("Installed example", plan, outputDir, written, dryRun));
1256
+ return ok(scaffoldSummary(t("scaffold.verb.installedExample"), plan, outputDir, written, dryRun));
1004
1257
  }
1005
1258
  async function agentCommand(args, runtime) {
1006
1259
  const parsed = parseArgs(args, {
@@ -1014,15 +1267,18 @@ async function agentCommand(args, runtime) {
1014
1267
  const targetDir = readString(parsed, "path");
1015
1268
  if (targetDir == null) {
1016
1269
  return ok([
1017
- "RemoteDraw agent setup",
1270
+ t("agent.title"),
1018
1271
  "",
1019
- "Use this when an AI coding agent needs to add RemoteDraw to a customer app.",
1272
+ t("agent.intro"),
1020
1273
  "",
1021
- "Print the skill:",
1274
+ t("agent.printSkill"),
1022
1275
  " remotedraw agent --print-skill",
1023
1276
  "",
1024
- "Install the skill file:",
1025
- " remotedraw agent --path ~/.codex/skills/remotedraw",
1277
+ t("agent.installSkill"),
1278
+ // Agent runtimes read skills from a per-project directory. `~/.codex`
1279
+ // is the retired location and installing there reaches nothing.
1280
+ " remotedraw agent --path .agents/skills/remotedraw",
1281
+ " remotedraw agent --path .claude/skills/remotedraw",
1026
1282
  ].join("\n"));
1027
1283
  }
1028
1284
  const outputDir = resolveAgentPath(targetDir, runtime);
@@ -1030,7 +1286,7 @@ async function agentCommand(args, runtime) {
1030
1286
  if (!parsed.booleans.has("dry-run") &&
1031
1287
  !parsed.booleans.has("force") &&
1032
1288
  (await runtime.exists(outputFile))) {
1033
- throw new Error("SKILL.md already exists. Pass --force to replace it.");
1289
+ throw new Error(t("agent.error.exists"));
1034
1290
  }
1035
1291
  if (!parsed.booleans.has("dry-run")) {
1036
1292
  await runtime.mkdir(outputDir);
@@ -1038,141 +1294,150 @@ async function agentCommand(args, runtime) {
1038
1294
  }
1039
1295
  return ok([
1040
1296
  parsed.booleans.has("dry-run")
1041
- ? `Would write ${outputFile}`
1042
- : `Installed RemoteDraw agent skill at ${outputFile}`,
1297
+ ? t("agent.wouldWrite", { path: outputFile })
1298
+ : t("agent.installed", { path: outputFile }),
1043
1299
  "",
1044
- "Next:",
1045
- " Ask your agent to use the RemoteDraw skill before editing API, CLI, sender, receiver, or iOS integration code.",
1300
+ t("word.next"),
1301
+ t("agent.next"),
1046
1302
  ].join("\n"));
1047
1303
  }
1048
1304
  function newHelpText() {
1305
+ const option = (flag, description) => ` ${flag.padEnd(25)} ${description}`;
1049
1306
  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.",
1307
+ t("word.usage"),
1308
+ t("new.help.usage.bare"),
1309
+ t("new.help.usage.named"),
1310
+ "",
1311
+ t("new.help.interactive"),
1312
+ "",
1313
+ t("word.options"),
1314
+ option("--path <dir>", t("new.help.option.path")),
1315
+ option("--target <kind>", t("new.help.option.target")),
1316
+ option("--sender <kind>", t("new.help.option.sender")),
1317
+ option("--sdk <kind>", t("new.help.option.sdk")),
1318
+ option("--preset <kind>", t("new.help.option.preset")),
1319
+ option("--package-manager <name>", t("new.help.option.packageManager")),
1320
+ option("--api-base-url <url>", t("new.help.option.apiBaseUrl")),
1321
+ option("--force", t("new.help.option.force")),
1322
+ option("--offline", t("new.help.option.offline")),
1323
+ option("--non-interactive", t("new.help.option.nonInteractive")),
1324
+ option("--format <text|json>", t("new.help.option.format")),
1325
+ option("--dry-run", t("new.help.option.dryRun")),
1326
+ option("--language <code>", t("new.help.option.language")),
1069
1327
  ].join("\n");
1070
1328
  }
1071
1329
  function initHelpText() {
1072
1330
  return [
1073
- "Usage:",
1074
- " remotedraw init [options]",
1331
+ t("word.usage"),
1332
+ t("init.help.usage"),
1075
1333
  "",
1076
- "Options match remotedraw new. init writes remotedraw.config.json, .env.example, and src/remotedraw starter files into an existing project.",
1334
+ t("init.help.body"),
1077
1335
  ].join("\n");
1078
1336
  }
1079
- function loginHelpText() {
1337
+ function languageHelpText() {
1080
1338
  return [
1081
- "Authorize the RemoteDraw CLI",
1339
+ t("language.help.title"),
1340
+ "",
1341
+ t("word.usage"),
1342
+ t("language.help.usage.list"),
1343
+ t("language.help.usage.set"),
1082
1344
  "",
1083
- "Usage:",
1084
- " remotedraw login [--no-open] [--force] [--api-base-url <origin>]",
1345
+ t("language.help.body"),
1346
+ t("language.help.override"),
1085
1347
  "",
1086
- "The browser confirms your account. A revocable CLI credential is stored in your user config directory, never in the project.",
1348
+ t("language.available"),
1349
+ ...SUPPORTED_LOCALES.map((locale) => ` ${locale.padEnd(6)} ${localeEndonym(locale)}`),
1087
1350
  ].join("\n");
1088
1351
  }
1089
- function logoutHelpText() {
1352
+ function loginHelpText() {
1090
1353
  return [
1091
- "Revoke the RemoteDraw CLI credential",
1354
+ t("login.help.title"),
1092
1355
  "",
1093
- "Usage:",
1094
- " remotedraw logout [--api-base-url <origin>]",
1356
+ t("word.usage"),
1357
+ t("login.help.usage"),
1358
+ "",
1359
+ t("login.help.body"),
1095
1360
  ].join("\n");
1096
1361
  }
1362
+ function logoutHelpText() {
1363
+ return [t("logout.help.title"), "", t("word.usage"), t("logout.help.usage")].join("\n");
1364
+ }
1097
1365
  function whoamiHelpText() {
1098
- return [
1099
- "Show the active RemoteDraw CLI account",
1100
- "",
1101
- "Usage:",
1102
- " remotedraw whoami [--api-base-url <origin>]",
1103
- ].join("\n");
1366
+ return [t("whoami.help.title"), "", t("word.usage"), t("whoami.help.usage")].join("\n");
1104
1367
  }
1105
1368
  function doctorHelpText() {
1106
1369
  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.",
1370
+ t("word.usage"),
1371
+ t("doctor.help.usage"),
1372
+ "",
1373
+ t("doctor.help.checks"),
1374
+ t("doctor.help.baseUrl", { url: DEFAULT_REMOTEDRAW_API_BASE_URL }),
1375
+ t("doctor.help.network"),
1376
+ t("doctor.help.version"),
1377
+ t("doctor.help.offline"),
1115
1378
  ].join("\n");
1116
1379
  }
1117
1380
  function updateHelpText() {
1118
1381
  return [
1119
- "Usage:",
1120
- " remotedraw update [--check] [--format text|json]",
1382
+ t("word.usage"),
1383
+ t("update.help.usage"),
1121
1384
  "",
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.",
1385
+ t("update.help.body", { package: CLI_PACKAGE_NAME }),
1386
+ t("update.help.global"),
1387
+ t("update.help.check"),
1125
1388
  "",
1126
- "The passive 'update available' notice on other commands can be silenced with REMOTEDRAW_CLI_NO_UPDATE_CHECK=1.",
1389
+ t("update.help.silence"),
1127
1390
  ].join("\n");
1128
1391
  }
1129
1392
  function optionsHelpText() {
1130
1393
  return [
1131
- "Usage:",
1394
+ t("word.usage"),
1132
1395
  " remotedraw options [--format text|json]",
1133
1396
  "",
1134
- "Prints every setup choice, compatibility rule, default, explanation, and documentation URL.",
1397
+ t("options.help.body"),
1135
1398
  ].join("\n");
1136
1399
  }
1137
1400
  function devHelpText() {
1138
1401
  return [
1139
- "Usage:",
1140
- " remotedraw dev [--api-base-url <url>]",
1402
+ t("word.usage"),
1403
+ t("dev.help.usage"),
1141
1404
  "",
1142
- "Prints the local test workflow for pairing, input request creation, and receiver rendering.",
1405
+ t("dev.help.body"),
1143
1406
  ].join("\n");
1144
1407
  }
1145
1408
  function createInputHelpText() {
1409
+ const option = (flag, description) => ` ${flag.padEnd(21)} ${description}`;
1146
1410
  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.",
1411
+ t("word.usage"),
1412
+ t("createInput.help.usage"),
1413
+ "",
1414
+ t("word.options"),
1415
+ option("--preset <kind>", t("createInput.help.option.preset")),
1416
+ option("--label <text>", t("createInput.help.option.label")),
1417
+ option("--external-id <id>", t("createInput.help.option.externalId")),
1418
+ option("--api-base-url <url>", t("createInput.help.option.apiBaseUrl")),
1419
+ option("--api-key <key>", t("createInput.help.option.apiKey")),
1420
+ option("--expires-in-ms <n>", t("createInput.help.option.expiresInMs")),
1157
1421
  ].join("\n");
1158
1422
  }
1159
1423
  function examplesHelpText() {
1160
1424
  return [
1161
- "Usage:",
1162
- " remotedraw examples [--list]",
1163
- " remotedraw examples --install <example> --path <dir>",
1425
+ t("word.usage"),
1426
+ t("examples.help.usage.list"),
1427
+ t("examples.help.usage.install"),
1164
1428
  "",
1165
- `Examples: ${exampleChoices.join(", ")}`,
1429
+ t("word.examples", { list: exampleChoices.join(", ") }),
1166
1430
  ].join("\n");
1167
1431
  }
1168
1432
  function agentHelpText() {
1169
1433
  return [
1170
- "Usage:",
1171
- " remotedraw agent",
1172
- " remotedraw agent --print-skill",
1173
- " remotedraw agent --path <dir> [--force] [--dry-run]",
1434
+ t("word.usage"),
1435
+ t("agent.help.usage.bare"),
1436
+ t("agent.help.usage.print"),
1437
+ t("agent.help.usage.install"),
1174
1438
  "",
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.",
1439
+ t("agent.help.body"),
1440
+ t("agent.help.paths"),
1176
1441
  ].join("\n");
1177
1442
  }
1178
1443
  function parseArgs(args, allowed) {
@@ -1195,17 +1460,17 @@ function parseArgs(args, allowed) {
1195
1460
  const inlineValue = equalsIndex === -1 ? undefined : withoutPrefix.slice(equalsIndex + 1);
1196
1461
  if (booleanFlags.has(name)) {
1197
1462
  if (inlineValue != null) {
1198
- throw new Error(`--${name} does not accept a value.`);
1463
+ throw cliError("INVALID_ARGUMENT", "error.optionNoValue", { name });
1199
1464
  }
1200
1465
  booleans.add(name);
1201
1466
  continue;
1202
1467
  }
1203
1468
  if (!valueFlags.has(name)) {
1204
- throw new Error(`Unsupported option "--${name}".`);
1469
+ throw cliError("INVALID_ARGUMENT", "error.unsupportedOption", { name });
1205
1470
  }
1206
1471
  const value = inlineValue ?? args[index + 1];
1207
1472
  if (value == null || value.startsWith("--")) {
1208
- throw new Error(`--${name} requires a value.`);
1473
+ throw cliError("MISSING_ARGUMENT", "error.optionNeedsValue", { name });
1209
1474
  }
1210
1475
  values.set(name, value);
1211
1476
  if (inlineValue == null)
@@ -1222,7 +1487,7 @@ function optionalInteger(value) {
1222
1487
  return undefined;
1223
1488
  const number = Number(value);
1224
1489
  if (!Number.isInteger(number) || number <= 0) {
1225
- throw new Error("--expires-in-ms requires a positive integer.");
1490
+ throw cliError("INVALID_ARGUMENT", "error.expiresInMs");
1226
1491
  }
1227
1492
  return number;
1228
1493
  }
@@ -1239,16 +1504,16 @@ async function promptForNewProject(parsed, runtime) {
1239
1504
  defaultAppName: "RemoteDraw app",
1240
1505
  });
1241
1506
  }
1242
- await prompts.intro?.("RemoteDraw project setup");
1507
+ await prompts.intro?.(t("prompt.intro.setup"));
1243
1508
  const appName = readString(parsed, "app-name") ??
1244
1509
  (await prompts.text({
1245
- message: "Project name",
1510
+ message: t("field.appName"),
1246
1511
  defaultValue: "RemoteDraw App",
1247
1512
  validate: validateProjectName,
1248
1513
  }));
1249
1514
  const target = readString(parsed, "target") == null
1250
1515
  ? await prompts.select({
1251
- message: "Project type",
1516
+ message: t("field.target"),
1252
1517
  defaultValue: "web",
1253
1518
  choices: targetPromptChoices(),
1254
1519
  })
@@ -1261,25 +1526,19 @@ async function promptForNewProject(parsed, runtime) {
1261
1526
  : choice(readString(parsed, "sdk"), sdkChoices, "sdk");
1262
1527
  const preset = readString(parsed, "preset") == null
1263
1528
  ? await prompts.select({
1264
- message: "Starter experience",
1529
+ message: t("field.preset"),
1265
1530
  defaultValue: "signature",
1266
1531
  choices: presetPromptChoices(),
1267
1532
  })
1268
1533
  : choice(readString(parsed, "preset"), presetChoices, "preset");
1269
1534
  const packageManager = readString(parsed, "package-manager") == null
1270
1535
  ? await prompts.select({
1271
- message: "Package manager",
1536
+ message: t("field.packageManager"),
1272
1537
  defaultValue: "npm",
1273
1538
  choices: packageManagerPromptChoices(),
1274
1539
  })
1275
1540
  : 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
- }));
1541
+ const apiBaseUrl = readString(parsed, "api-base-url") ?? DEFAULT_REMOTEDRAW_API_BASE_URL;
1283
1542
  validatePlan({ target, sender, sdk });
1284
1543
  return {
1285
1544
  appName,
@@ -1293,13 +1552,13 @@ async function promptForNewProject(parsed, runtime) {
1293
1552
  };
1294
1553
  }
1295
1554
  function validateProjectName(value) {
1296
- return value.trim() === "" ? "Enter a project name." : undefined;
1555
+ return value.trim() === "" ? t("field.appName.error") : undefined;
1297
1556
  }
1298
1557
  async function promptForSender(prompts, target) {
1299
1558
  const choices = senderPromptChoices(target);
1300
1559
  return await prompts.select({
1301
- message: "Phone sender",
1302
- helperText: "Choose where drawing input will run.",
1560
+ message: t("field.sender"),
1561
+ helperText: t("field.sender.helper"),
1303
1562
  defaultValue: defaultPromptValue(defaultSenderForTarget(target), choices),
1304
1563
  choices,
1305
1564
  });
@@ -1307,8 +1566,8 @@ async function promptForSender(prompts, target) {
1307
1566
  async function promptForSdk(prompts, target, sender) {
1308
1567
  const choices = sdkPromptChoices(target, sender);
1309
1568
  return await prompts.select({
1310
- message: "UI framework / SDK",
1311
- helperText: "Pick the SDK that matches your receiver UI.",
1569
+ message: t("field.sdk"),
1570
+ helperText: t("field.sdk.helper"),
1312
1571
  defaultValue: defaultPromptValue(defaultSdkForChoices(target, sender), choices),
1313
1572
  choices,
1314
1573
  });
@@ -1320,10 +1579,10 @@ function defaultPromptValue(preferred, choices) {
1320
1579
  }
1321
1580
  function targetPromptChoices() {
1322
1581
  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" },
1582
+ { value: "web", label: t("choice.target.web.label") },
1583
+ { value: "desktop", label: t("choice.target.desktop.label") },
1584
+ { value: "ios", label: t("choice.target.ios.label") },
1585
+ { value: "headless", label: t("choice.target.headless.label") },
1327
1586
  ];
1328
1587
  }
1329
1588
  const DOCS_INTEGRATION_PATHS = "https://docs.remotedraw.com/docs#paths";
@@ -1331,103 +1590,112 @@ const DOCS_SDKS = "https://docs.remotedraw.com/docs/sdks#components";
1331
1590
  const DOCS_PAYLOADS = "https://docs.remotedraw.com/docs/api#payloads";
1332
1591
  const DOCS_ENDPOINTS = "https://docs.remotedraw.com/docs/api#endpoints";
1333
1592
  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
- };
1593
+ // Detail bundles are built per call, not frozen at module load: the active
1594
+ // locale is only known once `runCli` has parsed the flag, the environment, and
1595
+ // the stored choice.
1596
+ function targetWizardDetails() {
1597
+ return {
1598
+ web: {
1599
+ summary: t("choice.target.web.summary"),
1600
+ explanation: t("choice.target.web.explanation"),
1601
+ docsUrl: DOCS_INTEGRATION_PATHS,
1602
+ docsLabel: t("choice.target.web.docs"),
1603
+ },
1604
+ desktop: {
1605
+ summary: t("choice.target.desktop.summary"),
1606
+ explanation: t("choice.target.desktop.explanation"),
1607
+ docsUrl: DOCS_INTEGRATION_PATHS,
1608
+ docsLabel: t("choice.target.desktop.docs"),
1609
+ },
1610
+ ios: {
1611
+ summary: t("choice.target.ios.summary"),
1612
+ explanation: t("choice.target.ios.explanation"),
1613
+ docsUrl: DOCS_INTEGRATION_PATHS,
1614
+ docsLabel: t("choice.target.ios.docs"),
1615
+ },
1616
+ headless: {
1617
+ summary: t("choice.target.headless.summary"),
1618
+ explanation: t("choice.target.headless.explanation"),
1619
+ docsUrl: DOCS_ENDPOINTS,
1620
+ docsLabel: t("choice.target.headless.docs"),
1621
+ },
1622
+ };
1623
+ }
1360
1624
  function wizardChoices(choices, details) {
1361
- return dutchWizardChoices(choices).map((choiceOption) => ({
1625
+ return choices.map((choiceOption) => ({
1362
1626
  ...choiceOption,
1363
1627
  details: details[choiceOption.value],
1364
1628
  }));
1365
1629
  }
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
- };
1630
+ function senderWizardDetails() {
1631
+ return {
1632
+ "remotedraw-ios": {
1633
+ summary: t("choice.sender.remotedrawIos.summary"),
1634
+ explanation: t("choice.sender.remotedrawIos.explanation"),
1635
+ docsUrl: DOCS_INTEGRATION_PATHS,
1636
+ docsLabel: t("choice.sender.remotedrawIos.docs"),
1637
+ },
1638
+ "embedded-web": {
1639
+ summary: t("choice.sender.embeddedWeb.summary"),
1640
+ explanation: t("choice.sender.embeddedWeb.explanation"),
1641
+ docsUrl: DOCS_INTEGRATION_PATHS,
1642
+ docsLabel: t("choice.sender.embeddedWeb.docs"),
1643
+ },
1644
+ "own-ios": {
1645
+ summary: t("choice.sender.ownIos.summary"),
1646
+ explanation: t("choice.sender.ownIos.explanation"),
1647
+ docsUrl: DOCS_INTEGRATION_PATHS,
1648
+ docsLabel: t("choice.sender.ownIos.docs"),
1649
+ },
1650
+ headless: {
1651
+ summary: t("choice.sender.headless.summary"),
1652
+ explanation: t("choice.sender.headless.explanation"),
1653
+ docsUrl: DOCS_ENDPOINTS,
1654
+ docsLabel: t("choice.sender.headless.docs"),
1655
+ },
1656
+ };
1657
+ }
1658
+ function sdkWizardDetails() {
1659
+ return {
1660
+ react: {
1661
+ summary: t("choice.sdk.react.summary"),
1662
+ explanation: t("choice.sdk.react.explanation"),
1663
+ docsUrl: DOCS_SDKS,
1664
+ docsLabel: t("choice.sdk.react.docs"),
1665
+ },
1666
+ svelte: {
1667
+ summary: t("choice.sdk.svelte.summary"),
1668
+ explanation: t("choice.sdk.svelte.explanation"),
1669
+ docsUrl: DOCS_SDKS,
1670
+ docsLabel: t("choice.sdk.svelte.docs"),
1671
+ },
1672
+ js: {
1673
+ summary: t("choice.sdk.js.summary"),
1674
+ explanation: t("choice.sdk.js.explanation"),
1675
+ docsUrl: DOCS_ENDPOINTS,
1676
+ docsLabel: t("choice.sdk.js.docs"),
1677
+ },
1678
+ swift: {
1679
+ summary: t("choice.sdk.swift.summary"),
1680
+ explanation: t("choice.sdk.swift.explanation"),
1681
+ docsUrl: DOCS_SDKS,
1682
+ docsLabel: t("choice.sdk.swift.docs"),
1683
+ },
1684
+ headless: {
1685
+ summary: t("choice.sdk.headless.summary"),
1686
+ explanation: t("choice.sdk.headless.explanation"),
1687
+ docsUrl: DOCS_ENDPOINTS,
1688
+ docsLabel: t("choice.sdk.headless.docs"),
1689
+ },
1690
+ };
1691
+ }
1424
1692
  function senderPromptChoices(target) {
1425
1693
  if (target === "ios") {
1426
1694
  return [
1427
1695
  {
1428
1696
  value: "own-ios",
1429
- label: "Own iOS sender",
1430
- hint: "Use Swift join-link and sender request helpers.",
1697
+ label: t("choice.sender.ownIos.label"),
1698
+ hint: t("choice.sender.ownIos.hint"),
1431
1699
  },
1432
1700
  ];
1433
1701
  }
@@ -1435,8 +1703,8 @@ function senderPromptChoices(target) {
1435
1703
  return [
1436
1704
  {
1437
1705
  value: "headless",
1438
- label: "Headless/custom sender",
1439
- hint: "No hosted phone UI.",
1706
+ label: t("choice.sender.headless.label"),
1707
+ hint: t("choice.sender.headless.hint.noUi"),
1440
1708
  },
1441
1709
  ];
1442
1710
  }
@@ -1444,26 +1712,26 @@ function senderPromptChoices(target) {
1444
1712
  return [
1445
1713
  {
1446
1714
  value: "remotedraw-ios",
1447
- label: "RemoteDraw iOS app",
1448
- hint: "Show a join URL or QR code from your receiver.",
1715
+ label: t("choice.sender.remotedrawIos.label"),
1716
+ hint: t("choice.sender.remotedrawIos.hint.joinUrl"),
1449
1717
  },
1450
1718
  {
1451
1719
  value: "headless",
1452
- label: "Headless/custom sender",
1453
- hint: "Build directly on the HTTP sender routes.",
1720
+ label: t("choice.sender.headless.label"),
1721
+ hint: t("choice.sender.headless.hint.httpRoutes"),
1454
1722
  },
1455
1723
  ];
1456
1724
  }
1457
1725
  return [
1458
1726
  {
1459
1727
  value: "remotedraw-ios",
1460
- label: "RemoteDraw iOS app",
1461
- hint: "Fastest path: scan the receiver QR code.",
1728
+ label: t("choice.sender.remotedrawIos.label"),
1729
+ hint: t("choice.sender.remotedrawIos.hint.scan"),
1462
1730
  },
1463
1731
  {
1464
1732
  value: "embedded-web",
1465
- label: "Own web sender",
1466
- hint: "Use a web sender component or raw sender helpers.",
1733
+ label: t("choice.sender.embeddedWeb.label"),
1734
+ hint: t("choice.sender.embeddedWeb.hint"),
1467
1735
  },
1468
1736
  ];
1469
1737
  }
@@ -1472,8 +1740,8 @@ function sdkPromptChoices(target, sender) {
1472
1740
  return [
1473
1741
  {
1474
1742
  value: "swift",
1475
- label: "Swift helpers",
1476
- hint: "For customer-owned iOS sender apps.",
1743
+ label: t("choice.sdk.swift.label"),
1744
+ hint: t("choice.sdk.swift.hint"),
1477
1745
  },
1478
1746
  ];
1479
1747
  }
@@ -1481,13 +1749,13 @@ function sdkPromptChoices(target, sender) {
1481
1749
  return [
1482
1750
  {
1483
1751
  value: "js",
1484
- label: "JavaScript client",
1485
- hint: "Framework-free HTTP clients and protocol types.",
1752
+ label: t("choice.sdk.js.label.client"),
1753
+ hint: t("choice.sdk.js.hint.client"),
1486
1754
  },
1487
1755
  {
1488
1756
  value: "headless",
1489
- label: "Config only",
1490
- hint: "No UI package dependency.",
1757
+ label: t("choice.sdk.headless.label"),
1758
+ hint: t("choice.sdk.headless.hint"),
1491
1759
  },
1492
1760
  ];
1493
1761
  }
@@ -1495,26 +1763,26 @@ function sdkPromptChoices(target, sender) {
1495
1763
  return [
1496
1764
  {
1497
1765
  value: "js",
1498
- label: "JavaScript client",
1499
- hint: "Framework-free HTTP clients and protocol types.",
1766
+ label: t("choice.sdk.js.label.client"),
1767
+ hint: t("choice.sdk.js.hint.client"),
1500
1768
  },
1501
1769
  ];
1502
1770
  }
1503
1771
  return [
1504
1772
  {
1505
1773
  value: "react",
1506
- label: "React SDK",
1507
- hint: "Receiver, pairing, and optional embedded sender components.",
1774
+ label: t("choice.sdk.react.label"),
1775
+ hint: t("choice.sdk.react.hint"),
1508
1776
  },
1509
1777
  {
1510
1778
  value: "svelte",
1511
- label: "Svelte SDK",
1512
- hint: "Svelte receiver store plus shared HTTP clients.",
1779
+ label: t("choice.sdk.svelte.label"),
1780
+ hint: t("choice.sdk.svelte.hint"),
1513
1781
  },
1514
1782
  {
1515
1783
  value: "js",
1516
- label: "No framework",
1517
- hint: "Framework-free clients and raw HTTP starter files.",
1784
+ label: t("choice.sdk.js.label.noFramework"),
1785
+ hint: t("choice.sdk.js.hint.noFramework"),
1518
1786
  },
1519
1787
  ];
1520
1788
  }
@@ -1522,13 +1790,13 @@ function presetPromptChoices() {
1522
1790
  return [
1523
1791
  {
1524
1792
  value: "signature",
1525
- label: "Signature strip",
1526
- hint: "Product-ready branded field with exact surface mapping.",
1793
+ label: t("choice.preset.signature.label"),
1794
+ hint: t("choice.preset.signature.hint"),
1527
1795
  },
1528
1796
  {
1529
1797
  value: "sketch",
1530
- label: "Custom drawing surface",
1531
- hint: "Flexible normalized input for your own receiver UI.",
1798
+ label: t("choice.preset.sketch.label"),
1799
+ hint: t("choice.preset.sketch.hint"),
1532
1800
  },
1533
1801
  ];
1534
1802
  }
@@ -1538,122 +1806,98 @@ function packageManagerPromptChoices() {
1538
1806
  label: packageManager,
1539
1807
  }));
1540
1808
  }
1541
- function apiEndpointPromptChoices() {
1542
- return [
1543
- {
1544
- value: "deployment",
1545
- label: "Deployment URL placeholder",
1546
- hint: "Fill in your Convex site origin later.",
1809
+ function presetWizardDetails() {
1810
+ return {
1811
+ signature: {
1812
+ summary: t("choice.preset.signature.summary"),
1813
+ explanation: t("choice.preset.signature.explanation"),
1814
+ docsUrl: DOCS_PAYLOADS,
1815
+ docsLabel: t("choice.preset.signature.docs"),
1547
1816
  },
1548
- {
1549
- value: "local",
1550
- label: "Local dev server",
1551
- hint: "Use http://localhost:3210.",
1817
+ initials: {
1818
+ summary: t("choice.preset.initials.summary"),
1819
+ explanation: t("choice.preset.initials.explanation"),
1820
+ docsUrl: DOCS_PAYLOADS,
1821
+ docsLabel: t("choice.preset.initials.docs"),
1552
1822
  },
1553
- ];
1823
+ approval: {
1824
+ summary: t("choice.preset.approval.summary"),
1825
+ explanation: t("choice.preset.approval.explanation"),
1826
+ docsUrl: DOCS_PAYLOADS,
1827
+ docsLabel: t("choice.preset.approval.docs"),
1828
+ },
1829
+ sketch: {
1830
+ summary: t("choice.preset.sketch.summary"),
1831
+ explanation: t("choice.preset.sketch.explanation"),
1832
+ docsUrl: DOCS_PAYLOADS,
1833
+ docsLabel: t("choice.preset.sketch.docs"),
1834
+ },
1835
+ photoMarkup: {
1836
+ summary: t("choice.preset.photoMarkup.summary"),
1837
+ explanation: t("choice.preset.photoMarkup.explanation"),
1838
+ docsUrl: DOCS_PAYLOADS,
1839
+ docsLabel: t("choice.preset.photoMarkup.docs"),
1840
+ },
1841
+ pdfMarkup: {
1842
+ summary: t("choice.preset.pdfMarkup.summary"),
1843
+ explanation: t("choice.preset.pdfMarkup.explanation"),
1844
+ docsUrl: DOCS_PAYLOADS,
1845
+ docsLabel: t("choice.preset.pdfMarkup.docs"),
1846
+ },
1847
+ mapMarkup: {
1848
+ summary: t("choice.preset.mapMarkup.summary"),
1849
+ explanation: t("choice.preset.mapMarkup.explanation"),
1850
+ docsUrl: DOCS_PAYLOADS,
1851
+ docsLabel: t("choice.preset.mapMarkup.docs"),
1852
+ },
1853
+ screenMarkup: {
1854
+ summary: t("choice.preset.screenMarkup.summary"),
1855
+ explanation: t("choice.preset.screenMarkup.explanation"),
1856
+ docsUrl: DOCS_PAYLOADS,
1857
+ docsLabel: t("choice.preset.screenMarkup.docs"),
1858
+ },
1859
+ designReview: {
1860
+ summary: t("choice.preset.designReview.summary"),
1861
+ explanation: t("choice.preset.designReview.explanation"),
1862
+ docsUrl: DOCS_PAYLOADS,
1863
+ docsLabel: t("choice.preset.designReview.docs"),
1864
+ },
1865
+ pointer: {
1866
+ summary: t("choice.preset.pointer.summary"),
1867
+ explanation: t("choice.preset.pointer.explanation"),
1868
+ docsUrl: DOCS_PAYLOADS,
1869
+ docsLabel: t("choice.preset.pointer.docs"),
1870
+ },
1871
+ };
1872
+ }
1873
+ function packageManagerWizardDetails() {
1874
+ return {
1875
+ npm: {
1876
+ summary: t("choice.packageManager.npm.summary"),
1877
+ explanation: t("choice.packageManager.npm.explanation"),
1878
+ docsUrl: "https://docs.npmjs.com/",
1879
+ docsLabel: t("choice.packageManager.npm.docs"),
1880
+ },
1881
+ pnpm: {
1882
+ summary: t("choice.packageManager.pnpm.summary"),
1883
+ explanation: t("choice.packageManager.pnpm.explanation"),
1884
+ docsUrl: "https://pnpm.io/",
1885
+ docsLabel: t("choice.packageManager.pnpm.docs"),
1886
+ },
1887
+ yarn: {
1888
+ summary: t("choice.packageManager.yarn.summary"),
1889
+ explanation: t("choice.packageManager.yarn.explanation"),
1890
+ docsUrl: "https://yarnpkg.com/getting-started/usage",
1891
+ docsLabel: t("choice.packageManager.yarn.docs"),
1892
+ },
1893
+ bun: {
1894
+ summary: t("choice.packageManager.bun.summary"),
1895
+ explanation: t("choice.packageManager.bun.explanation"),
1896
+ docsUrl: "https://bun.com/docs/pm/cli/install",
1897
+ docsLabel: t("choice.packageManager.bun.docs"),
1898
+ },
1899
+ };
1554
1900
  }
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
1901
  function agentOptionCatalog() {
1658
1902
  const catalogOption = (choiceOption, compatibility) => ({
1659
1903
  value: choiceOption.value,
@@ -1662,13 +1906,13 @@ function agentOptionCatalog() {
1662
1906
  details: choiceOption.details,
1663
1907
  ...(compatibility ?? {}),
1664
1908
  });
1665
- const targets = wizardChoices(targetPromptChoices(), targetWizardDetails);
1909
+ const targets = wizardChoices(targetPromptChoices(), targetWizardDetails());
1666
1910
  const senders = senderChoices.map((value) => {
1667
1911
  const compatibleTargets = targetChoices.filter((target) => senderPromptChoices(target).some((option) => option.value === value));
1668
1912
  const prompt = compatibleTargets
1669
1913
  .flatMap((target) => senderPromptChoices(target))
1670
1914
  .find((option) => option.value === value);
1671
- return catalogOption(wizardChoices([prompt], senderWizardDetails)[0], {
1915
+ return catalogOption(wizardChoices([prompt], senderWizardDetails())[0], {
1672
1916
  compatibleTargets,
1673
1917
  });
1674
1918
  });
@@ -1678,7 +1922,7 @@ function agentOptionCatalog() {
1678
1922
  : []));
1679
1923
  const firstPlan = compatiblePlans[0];
1680
1924
  const prompt = sdkPromptChoices(firstPlan.target, firstPlan.sender).find((option) => option.value === value);
1681
- return catalogOption(wizardChoices([prompt], sdkWizardDetails)[0], {
1925
+ return catalogOption(wizardChoices([prompt], sdkWizardDetails())[0], {
1682
1926
  compatiblePlans,
1683
1927
  });
1684
1928
  });
@@ -1690,7 +1934,7 @@ function agentOptionCatalog() {
1690
1934
  {
1691
1935
  key: "appName",
1692
1936
  flag: "--app-name",
1693
- label: "Projectnaam",
1937
+ label: t("field.appName"),
1694
1938
  kind: "text",
1695
1939
  requiredFor: ["new"],
1696
1940
  options: [],
@@ -1698,48 +1942,44 @@ function agentOptionCatalog() {
1698
1942
  {
1699
1943
  key: "target",
1700
1944
  flag: "--target",
1701
- label: "Projecttype",
1945
+ label: t("field.target"),
1702
1946
  kind: "select",
1703
1947
  options: targets.map((option) => catalogOption(option)),
1704
1948
  },
1705
1949
  {
1706
1950
  key: "sender",
1707
1951
  flag: "--sender",
1708
- label: "Telefoonzender",
1952
+ label: t("field.sender"),
1709
1953
  kind: "select",
1710
1954
  options: senders,
1711
1955
  },
1712
1956
  {
1713
1957
  key: "sdk",
1714
1958
  flag: "--sdk",
1715
- label: "UI-framework / SDK",
1959
+ label: t("field.sdk"),
1716
1960
  kind: "select",
1717
1961
  options: sdks,
1718
1962
  },
1719
1963
  {
1720
1964
  key: "preset",
1721
1965
  flag: "--preset",
1722
- label: "Startpunt",
1966
+ label: t("field.preset"),
1723
1967
  kind: "select",
1724
- options: wizardChoices(presetPromptChoices(), presetWizardDetails).map((option) => catalogOption(option)),
1968
+ options: wizardChoices(presetPromptChoices(), presetWizardDetails()).map((option) => catalogOption(option)),
1725
1969
  },
1726
1970
  {
1727
1971
  key: "packageManager",
1728
1972
  flag: "--package-manager",
1729
- label: "Pakketbeheerder",
1973
+ label: t("field.packageManager"),
1730
1974
  kind: "select",
1731
- options: wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails).map((option) => catalogOption(option)),
1975
+ options: wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails()).map((option) => catalogOption(option)),
1732
1976
  },
1733
1977
  {
1734
1978
  key: "apiBaseUrl",
1735
1979
  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
- })),
1980
+ label: t("field.apiBaseUrl"),
1981
+ kind: "text",
1982
+ options: [],
1743
1983
  },
1744
1984
  ],
1745
1985
  defaults: {
@@ -1751,7 +1991,7 @@ function agentOptionCatalog() {
1751
1991
  ]))),
1752
1992
  preset: "signature",
1753
1993
  packageManager: "npm",
1754
- apiBaseUrl: "https://<deployment>.convex.site",
1994
+ apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
1755
1995
  },
1756
1996
  safety: {
1757
1997
  dryRunFlag: "--dry-run",
@@ -1761,11 +2001,6 @@ function agentOptionCatalog() {
1761
2001
  },
1762
2002
  };
1763
2003
  }
1764
- function apiBaseUrlForEndpoint(endpoint) {
1765
- if (endpoint === "local")
1766
- return "http://localhost:3210";
1767
- return "https://<deployment>.convex.site";
1768
- }
1769
2004
  export function projectSetupDefinition() {
1770
2005
  return {
1771
2006
  defaults: {
@@ -1775,7 +2010,6 @@ export function projectSetupDefinition() {
1775
2010
  sdk: "react",
1776
2011
  preset: "signature",
1777
2012
  packageManager: "npm",
1778
- apiEndpoint: "deployment",
1779
2013
  },
1780
2014
  fields: projectSetupFields(),
1781
2015
  normalize(values, changed) {
@@ -1799,13 +2033,13 @@ export function projectSetupDefinition() {
1799
2033
  const plan = planFromWizardValues(values);
1800
2034
  const label = (key, fallback) => wizardChoiceLabel(key, values) ?? fallback;
1801
2035
  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],
2036
+ [t("field.project"), plan.appName],
2037
+ [t("field.folder"), `./${plan.slug}`],
2038
+ [t("field.target"), label("target", plan.target)],
2039
+ [t("field.sender"), label("sender", plan.sender)],
2040
+ [t("field.sdkShort"), label("sdk", plan.sdk)],
2041
+ [t("field.packageManager"), label("packageManager", plan.packageManager)],
2042
+ [t("field.apiBaseUrl"), plan.apiBaseUrl],
1809
2043
  ];
1810
2044
  },
1811
2045
  };
@@ -1814,42 +2048,36 @@ function projectSetupFields() {
1814
2048
  return [
1815
2049
  {
1816
2050
  key: "appName",
1817
- label: "Projectnaam",
2051
+ label: t("field.appName"),
1818
2052
  kind: "text",
1819
2053
  validate: validateWizardProjectName,
1820
2054
  preview: (values) => values.appName.trim() === ""
1821
2055
  ? undefined
1822
- : `map ./${slugify(values.appName)} · pakket ${slugify(values.appName)}`,
2056
+ : t("field.appName.preview", { slug: slugify(values.appName) }),
1823
2057
  },
1824
2058
  {
1825
2059
  key: "target",
1826
- label: "Projecttype",
2060
+ label: t("field.target"),
1827
2061
  kind: "select",
1828
- choices: () => wizardChoices(targetPromptChoices(), targetWizardDetails),
2062
+ choices: () => wizardChoices(targetPromptChoices(), targetWizardDetails()),
1829
2063
  },
1830
2064
  {
1831
2065
  key: "sender",
1832
- label: "Telefoonzender",
2066
+ label: t("field.sender"),
1833
2067
  kind: "select",
1834
- choices: (values) => wizardChoices(senderPromptChoices(choice(values.target, targetChoices, "target")), senderWizardDetails),
2068
+ choices: (values) => wizardChoices(senderPromptChoices(choice(values.target, targetChoices, "target")), senderWizardDetails()),
1835
2069
  },
1836
2070
  {
1837
2071
  key: "sdk",
1838
- label: "UI-framework / SDK",
2072
+ label: t("field.sdk"),
1839
2073
  kind: "select",
1840
- choices: (values) => wizardChoices(sdkPromptChoices(choice(values.target, targetChoices, "target"), choice(values.sender, senderChoices, "sender")), sdkWizardDetails),
2074
+ choices: (values) => wizardChoices(sdkPromptChoices(choice(values.target, targetChoices, "target"), choice(values.sender, senderChoices, "sender")), sdkWizardDetails()),
1841
2075
  },
1842
2076
  {
1843
2077
  key: "packageManager",
1844
- label: "Pakketbeheerder",
2078
+ label: t("field.packageManager"),
1845
2079
  kind: "select",
1846
- choices: () => wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails),
1847
- },
1848
- {
1849
- key: "apiEndpoint",
1850
- label: "API-eindpunt",
1851
- kind: "select",
1852
- choices: () => wizardChoices(apiEndpointPromptChoices(), apiEndpointWizardDetails),
2080
+ choices: () => wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails()),
1853
2081
  },
1854
2082
  ];
1855
2083
  }
@@ -1859,62 +2087,7 @@ function wizardChoiceLabel(key, values) {
1859
2087
  ?.label;
1860
2088
  }
1861
2089
  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
- }));
2090
+ return value.trim() === "" ? t("field.appName.error") : undefined;
1918
2091
  }
1919
2092
  function planFromWizardValues(values) {
1920
2093
  const appName = values.appName.trim();
@@ -1929,11 +2102,11 @@ function planFromWizardValues(values) {
1929
2102
  sdk,
1930
2103
  preset: choice(values.preset, presetChoices, "preset"),
1931
2104
  packageManager: choice(values.packageManager, packageManagerChoices, "package-manager"),
1932
- apiBaseUrl: apiBaseUrlForEndpoint(choice(values.apiEndpoint, apiEndpointChoices, "api endpoint")),
2105
+ apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
1933
2106
  };
1934
2107
  const nameError = validateProjectName(plan.appName);
1935
2108
  if (nameError)
1936
- throw new Error(nameError);
2109
+ throw cliError("MISSING_ARGUMENT", "field.appName.error");
1937
2110
  validatePlan(plan);
1938
2111
  return plan;
1939
2112
  }
@@ -1942,7 +2115,7 @@ function planFromArgs(parsed, options) {
1942
2115
  options.defaults?.appName ??
1943
2116
  options.defaultAppName;
1944
2117
  if (options.appNameRequired && !readString(parsed, "app-name")) {
1945
- throw new Error("--app-name is required.");
2118
+ throw cliError("MISSING_ARGUMENT", "error.appNameRequired");
1946
2119
  }
1947
2120
  const target = choice(readString(parsed, "target") ?? options.defaults?.target ?? "web", targetChoices, "target");
1948
2121
  const sender = choice(readString(parsed, "sender") ??
@@ -1957,7 +2130,7 @@ function planFromArgs(parsed, options) {
1957
2130
  "npm", packageManagerChoices, "package-manager");
1958
2131
  const apiBaseUrl = readString(parsed, "api-base-url") ??
1959
2132
  options.defaults?.apiBaseUrl ??
1960
- "https://<deployment>.convex.site";
2133
+ DEFAULT_REMOTEDRAW_API_BASE_URL;
1961
2134
  validatePlan({ target, sender, sdk });
1962
2135
  return {
1963
2136
  appName,
@@ -1986,22 +2159,26 @@ function defaultSdkForChoices(target, sender) {
1986
2159
  }
1987
2160
  function validatePlan(plan) {
1988
2161
  if (plan.sender === "own-ios" && plan.sdk !== "swift") {
1989
- throw new Error("--sender own-ios requires --sdk swift.");
2162
+ throw cliError("INVALID_COMBINATION", "error.plan.ownIosNeedsSwift");
1990
2163
  }
1991
2164
  if (plan.sdk === "swift" && plan.sender !== "own-ios") {
1992
- throw new Error("--sdk swift is only supported with --sender own-ios.");
2165
+ throw cliError("INVALID_COMBINATION", "error.plan.swiftNeedsOwnIos");
1993
2166
  }
1994
2167
  if (plan.target === "ios" && plan.sender !== "own-ios") {
1995
- throw new Error("--target ios requires --sender own-ios.");
2168
+ throw cliError("INVALID_COMBINATION", "error.plan.iosNeedsOwnIos");
1996
2169
  }
1997
2170
  if (plan.sender === "headless" && plan.sdk === "react") {
1998
- throw new Error("--sender headless requires --sdk js or --sdk headless.");
2171
+ throw cliError("INVALID_COMBINATION", "error.plan.headlessSenderSdk");
1999
2172
  }
2000
2173
  }
2001
2174
  function choice(value, choices, label) {
2002
2175
  if (choices.includes(value))
2003
2176
  return value;
2004
- throw new Error(`Unknown ${label} "${value}". Expected one of: ${choices.join(", ")}.`);
2177
+ throw cliError("INVALID_ARGUMENT", "error.unknownChoice", {
2178
+ label,
2179
+ value,
2180
+ expected: choices.join(", "),
2181
+ });
2005
2182
  }
2006
2183
  function resolveOutputDir(cwd, parsed, fallback) {
2007
2184
  const requestedPath = readString(parsed, "path") ?? parsed.positionals[0] ?? fallback ?? ".";
@@ -2023,6 +2200,11 @@ function newProjectFiles(plan) {
2023
2200
  files.set("tsconfig.json", reactTsconfig());
2024
2201
  files.set("vite.config.ts", reactViteConfig());
2025
2202
  }
2203
+ else if (plan.sdk !== "swift") {
2204
+ // Without this the starter shipped a TypeScript toolchain and nothing to
2205
+ // point it at, so `tsc` had no config and no way to resolve node globals.
2206
+ files.set("tsconfig.json", nodeTsconfig());
2207
+ }
2026
2208
  return files;
2027
2209
  }
2028
2210
  function initProjectFiles(plan, packageJsonAlreadyExists) {
@@ -2037,7 +2219,7 @@ function initProjectFiles(plan, packageJsonAlreadyExists) {
2037
2219
  files.set("src/remotedraw/createRemoteDrawSession.ts", reactBackendSessionTs(plan));
2038
2220
  files.set("src/remotedraw/RemoteDrawReceiver.tsx", reactReceiverTsx());
2039
2221
  if (plan.sender === "embedded-web") {
2040
- files.set("src/remotedraw/RemoteDrawEmbeddedSender.tsx", reactEmbeddedSenderTsx());
2222
+ files.set("src/remotedraw/useRemoteDrawSender.ts", reactSenderTsx());
2041
2223
  }
2042
2224
  }
2043
2225
  else if (plan.sdk === "svelte") {
@@ -2077,7 +2259,9 @@ async function assertProjectFilesWritable(files, outputDir, runtime, dryRun, for
2077
2259
  return;
2078
2260
  for (const relativePath of files.keys()) {
2079
2261
  if (await runtime.exists(path.join(outputDir, relativePath))) {
2080
- throw new Error(`Refusing to overwrite ${relativePath}. Pass --force to replace generated files.`);
2262
+ throw cliError("FILE_CONFLICT", "error.refuseOverwrite", {
2263
+ file: relativePath,
2264
+ });
2081
2265
  }
2082
2266
  }
2083
2267
  }
@@ -2146,25 +2330,30 @@ function hasDependency(packageJson, dependencyName) {
2146
2330
  return (isRecord(packageJson.dependencies) &&
2147
2331
  typeof packageJson.dependencies[dependencyName] === "string");
2148
2332
  }
2333
+ /**
2334
+ * The RemoteDraw package an sdk choice installs, if any. The js starter calls
2335
+ * the HTTP API with plain fetch and takes only the request/response contract
2336
+ * from `@remotedraw/protocol`; the headless and swift starters take nothing.
2337
+ */
2338
+ function sdkPackageName(sdk) {
2339
+ if (sdk === "react")
2340
+ return "@remotedraw/react";
2341
+ if (sdk === "svelte")
2342
+ return "@remotedraw/svelte";
2343
+ if (sdk === "js")
2344
+ return "@remotedraw/protocol";
2345
+ return undefined;
2346
+ }
2149
2347
  function dependenciesForPlan(plan) {
2348
+ const sdkPackage = sdkPackageName(plan.sdk);
2349
+ const dependencies = sdkPackage
2350
+ ? { [sdkPackage]: REMOTEDRAW_SDK_RANGE }
2351
+ : {};
2150
2352
  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
- };
2353
+ dependencies.react = "^19.0.0";
2354
+ dependencies["react-dom"] = "^19.0.0";
2161
2355
  }
2162
- if (plan.sdk === "js") {
2163
- return {
2164
- "@remotedraw/protocol": "latest",
2165
- };
2166
- }
2167
- return {};
2356
+ return dependencies;
2168
2357
  }
2169
2358
  function newPackageJson(plan) {
2170
2359
  const dependencies = dependenciesForPlan(plan);
@@ -2180,15 +2369,24 @@ function newPackageJson(plan) {
2180
2369
  },
2181
2370
  dependencies,
2182
2371
  devDependencies: {
2372
+ // src/remotedraw/createRemoteDrawSession.ts reads process.env, so the
2373
+ // starter cannot typecheck without node types.
2374
+ "@types/node": "^25.0.0",
2183
2375
  "@types/react": "^19.0.0",
2184
2376
  "@types/react-dom": "^19.0.0",
2185
- "@vitejs/plugin-react": "latest",
2377
+ "@vitejs/plugin-react": "^6.0.0",
2186
2378
  typescript: "^6.0.0",
2187
- vite: "latest",
2379
+ vite: "^8.0.0",
2188
2380
  },
2189
2381
  };
2190
2382
  }
2191
- return initPackageJson(plan);
2383
+ const manifest = initPackageJson(plan);
2384
+ if (plan.sdk === "swift")
2385
+ return manifest;
2386
+ return {
2387
+ ...manifest,
2388
+ scripts: { ...manifest.scripts, typecheck: "tsc --noEmit" },
2389
+ };
2192
2390
  }
2193
2391
  function initPackageJson(plan) {
2194
2392
  return {
@@ -2201,14 +2399,21 @@ function initPackageJson(plan) {
2201
2399
  "remotedraw:create-input": "remotedraw create-input --preset " + plan.preset,
2202
2400
  },
2203
2401
  dependencies: dependenciesForPlan(plan),
2204
- devDependencies: {
2205
- typescript: "^6.0.0",
2206
- },
2402
+ // The scripts above shell out to `remotedraw`, so the CLI has to be a real
2403
+ // dependency of the project instead of an assumed global install. The swift
2404
+ // starter writes no TypeScript, so it gets no TypeScript toolchain.
2405
+ devDependencies: plan.sdk === "swift"
2406
+ ? { "@remotedraw/cli": REMOTEDRAW_CLI_RANGE }
2407
+ : {
2408
+ "@remotedraw/cli": REMOTEDRAW_CLI_RANGE,
2409
+ "@types/node": "^25.0.0",
2410
+ typescript: "^6.0.0",
2411
+ },
2207
2412
  };
2208
2413
  }
2209
2414
  function configJson(plan) {
2210
2415
  return {
2211
- $schema: "https://remotedraw.app/schemas/remotedraw.config.json",
2416
+ $schema: "https://www.remotedraw.com/schemas/remotedraw.config.json",
2212
2417
  appName: plan.appName,
2213
2418
  environment: "dev",
2214
2419
  target: plan.target,
@@ -2220,7 +2425,7 @@ function configJson(plan) {
2220
2425
  }
2221
2426
  function envExample(plan) {
2222
2427
  return [
2223
- "# RemoteDraw API origin. Use your Convex site URL in production.",
2428
+ "# RemoteDraw API origin. The hosted RemoteDraw API; leave as-is unless RemoteDraw support tells you otherwise.",
2224
2429
  `REMOTEDRAW_API_BASE_URL=${plan.apiBaseUrl}`,
2225
2430
  "",
2226
2431
  "# Non-secret dashboard project identifier. remotedraw new/init fills this in.",
@@ -2308,7 +2513,7 @@ function reactReceiverTsx() {
2308
2513
  "import {",
2309
2514
  " RemoteDrawProvider,",
2310
2515
  " RemoteDrawReceiver,",
2311
- " SubmissionStatus,",
2516
+ " RemoteDrawSessionControls,",
2312
2517
  " createHttpReceiverClient,",
2313
2518
  '} from "@remotedraw/react";',
2314
2519
  'import type { CreateSessionResponse } from "@remotedraw/react";',
@@ -2333,36 +2538,130 @@ function reactReceiverTsx() {
2333
2538
  " receiver={receiver}",
2334
2539
  " >",
2335
2540
  ' <RemoteDrawReceiver aria-label="RemoteDraw receiver surface" />',
2336
- ' <SubmissionStatus metadataKeys={["externalId", "senderLabel"]} />',
2541
+ " <RemoteDrawSessionControls />",
2337
2542
  " </RemoteDrawProvider>",
2338
2543
  " );",
2339
2544
  "}",
2340
2545
  "",
2341
2546
  ].join("\n");
2342
2547
  }
2343
- function reactEmbeddedSenderTsx() {
2548
+ function reactSenderTsx() {
2549
+ // Headless on purpose: the ready-made web sender is the hosted /join page
2550
+ // (the QR / Universal Link target). This starter is for products that must
2551
+ // host the pad themselves — RemoteDraw transports normalized points while
2552
+ // the canvas, tools, and styling stay app-owned.
2344
2553
  return [
2345
- 'import { EmbeddedSender, createHttpSenderClient } from "@remotedraw/react";',
2346
- "",
2347
- "type RemoteDrawEmbeddedSenderPanelProps = {",
2348
- " apiBaseUrl: string;",
2349
- " joinTokenOrUrl: string;",
2350
- "};",
2554
+ 'import { useEffect, useMemo, useRef, useState } from "react";',
2555
+ "import {",
2556
+ " createHttpSenderClient,",
2557
+ " createPacedDraftQueue,",
2558
+ " draftPointsForTransport,",
2559
+ " joinTokenFromInput,",
2560
+ '} from "@remotedraw/react";',
2561
+ "import type {",
2562
+ " DraftInputUpdate,",
2563
+ " JoinSessionResponse,",
2564
+ " NormalizedPoint,",
2565
+ '} from "@remotedraw/react";',
2351
2566
  "",
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
- " />",
2567
+ "// Headless RemoteDraw sender. The hosted /join page is the ready-made",
2568
+ "// pad; use this hook when the drawing surface must live inside your own",
2569
+ "// page. Points are normalized 0..1; the canvas and tools stay yours.",
2570
+ "export function useRemoteDrawSender(",
2571
+ " apiBaseUrl: string,",
2572
+ " joinTokenOrUrl: string,",
2573
+ ") {",
2574
+ " const client = useMemo(",
2575
+ " () => createHttpSenderClient(apiBaseUrl),",
2576
+ " [apiBaseUrl],",
2577
+ " );",
2578
+ " const [joined, setJoined] = useState<JoinSessionResponse | null>(null);",
2579
+ " const [error, setError] = useState<string | null>(null);",
2580
+ " const joinedRef = useRef<JoinSessionResponse | null>(null);",
2581
+ " const sequenceRef = useRef(0);",
2582
+ " // Drafts are throttled and latest-only: never POST every pointer event.",
2583
+ " const draftQueue = useMemo(",
2584
+ " () =>",
2585
+ " createPacedDraftQueue<DraftInputUpdate>({",
2586
+ " intervalMs: 32,",
2587
+ " run: async (request) => {",
2588
+ " await client.updateDraft?.(request);",
2589
+ " },",
2590
+ " }),",
2591
+ " [client],",
2365
2592
  " );",
2593
+ "",
2594
+ " useEffect(() => {",
2595
+ " const joinToken = joinTokenFromInput(joinTokenOrUrl);",
2596
+ " if (!joinToken) {",
2597
+ ' setError("Join token is required.");',
2598
+ " return;",
2599
+ " }",
2600
+ " let cancelled = false;",
2601
+ " client",
2602
+ " .join?.({",
2603
+ " joinToken,",
2604
+ ' device: { platform: "web", displayName: "Embedded sender" },',
2605
+ " })",
2606
+ " .then((result) => {",
2607
+ " if (cancelled || !result) return;",
2608
+ " // Resuming a token must continue its sequence: the server drops",
2609
+ " // anything at or below the last sequence it has seen.",
2610
+ " sequenceRef.current = result.lastSequence ?? 0;",
2611
+ " joinedRef.current = result;",
2612
+ " setJoined(result);",
2613
+ " })",
2614
+ " .catch((caught: unknown) => {",
2615
+ " if (!cancelled) setError(String(caught));",
2616
+ " });",
2617
+ " return () => {",
2618
+ " cancelled = true;",
2619
+ " };",
2620
+ " }, [client, joinTokenOrUrl]);",
2621
+ "",
2622
+ " return {",
2623
+ " joined,",
2624
+ " error,",
2625
+ " /** Call on pointermove with the whole in-progress stroke. */",
2626
+ " previewDraft(points: NormalizedPoint[]) {",
2627
+ " const senderToken = joinedRef.current?.senderToken;",
2628
+ " if (!senderToken || points.length === 0) return;",
2629
+ " draftQueue.offer(() => ({",
2630
+ " senderToken,",
2631
+ " sequence: ++sequenceRef.current,",
2632
+ ' tool: "freehand" as const,',
2633
+ ' pointerType: "touch" as const,',
2634
+ " points: draftPointsForTransport(points),",
2635
+ " occurredAt: Date.now(),",
2636
+ " }));",
2637
+ " },",
2638
+ " /** Call once on pointer-up with a stable id per stroke. */",
2639
+ " async commitStroke(points: NormalizedPoint[], clientStrokeId: string) {",
2640
+ " const senderToken = joinedRef.current?.senderToken;",
2641
+ " if (!senderToken || points.length < 2) return;",
2642
+ " draftQueue.clear();",
2643
+ " await client.commitStroke?.({",
2644
+ " senderToken,",
2645
+ " clientStrokeId,",
2646
+ " sequence: ++sequenceRef.current,",
2647
+ ' tool: "freehand",',
2648
+ ' pointerType: "touch",',
2649
+ " points,",
2650
+ " occurredAt: Date.now(),",
2651
+ " });",
2652
+ " },",
2653
+ " /** Call when the sender-side workflow is complete. */",
2654
+ " async submit(metadata?: Record<string, string>) {",
2655
+ " const senderToken = joinedRef.current?.senderToken;",
2656
+ " if (!senderToken) return;",
2657
+ " await client.submit?.({",
2658
+ " senderToken,",
2659
+ " clientSubmissionId: crypto.randomUUID(),",
2660
+ " occurredAt: Date.now(),",
2661
+ " ...(metadata ? { metadata } : {}),",
2662
+ " });",
2663
+ " },",
2664
+ " };",
2366
2665
  "}",
2367
2666
  "",
2368
2667
  ].join("\n");
@@ -2440,11 +2739,24 @@ function svelteReceiverSvelte() {
2440
2739
  ].join("\n");
2441
2740
  }
2442
2741
  function rawHttpSessionTs(plan) {
2742
+ // The js starter declares @remotedraw/protocol, so it uses it: the contract
2743
+ // types are the whole reason that dependency is there. The headless starter
2744
+ // declares nothing and stays import-free.
2745
+ const typed = plan.sdk === "js";
2443
2746
  return [
2747
+ ...(typed
2748
+ ? [
2749
+ "import type {",
2750
+ " CreateSessionRequest,",
2751
+ " CreateSessionResponse,",
2752
+ '} from "@remotedraw/protocol";',
2753
+ "",
2754
+ ]
2755
+ : []),
2444
2756
  "const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
2445
2757
  "const apiKey = process.env.REMOTEDRAW_API_KEY;",
2446
2758
  "",
2447
- "export function createRemoteDrawSessionRequest() {",
2759
+ `export function createRemoteDrawSessionRequest()${typed ? ": CreateSessionRequest" : ""} {`,
2448
2760
  " return " +
2449
2761
  JSON.stringify(createSessionPayload({
2450
2762
  preset: plan.preset,
@@ -2454,7 +2766,7 @@ function rawHttpSessionTs(plan) {
2454
2766
  ";",
2455
2767
  "}",
2456
2768
  "",
2457
- "export async function createRemoteDrawSession() {",
2769
+ `export async function createRemoteDrawSession()${typed ? ": Promise<CreateSessionResponse>" : ""} {`,
2458
2770
  " if (!apiBaseUrl || !apiKey) {",
2459
2771
  ' throw new Error("Set REMOTEDRAW_API_BASE_URL and REMOTEDRAW_API_KEY in backend secrets.");',
2460
2772
  " }",
@@ -2468,7 +2780,9 @@ function rawHttpSessionTs(plan) {
2468
2780
  " body: JSON.stringify(createRemoteDrawSessionRequest()),",
2469
2781
  " });",
2470
2782
  " if (!response.ok) throw new Error(await response.text());",
2471
- " return await response.json();",
2783
+ typed
2784
+ ? " return (await response.json()) as CreateSessionResponse;"
2785
+ : " return await response.json();",
2472
2786
  "}",
2473
2787
  "",
2474
2788
  ].join("\n");
@@ -2678,6 +2992,9 @@ function reactTsconfig() {
2678
2992
  ' "target": "ES2022",',
2679
2993
  ' "useDefineForClassFields": true,',
2680
2994
  ' "lib": ["ES2022", "DOM", "DOM.Iterable"],',
2995
+ // vite/client declares the CSS side-effect import in src/main.tsx; node
2996
+ // covers the process.env reads in the backend session helper.
2997
+ ' "types": ["node", "vite/client"],',
2681
2998
  ' "allowJs": false,',
2682
2999
  ' "skipLibCheck": true,',
2683
3000
  ' "esModuleInterop": true,',
@@ -2695,6 +3012,26 @@ function reactTsconfig() {
2695
3012
  "",
2696
3013
  ].join("\n");
2697
3014
  }
3015
+ function nodeTsconfig() {
3016
+ return [
3017
+ "{",
3018
+ ' "compilerOptions": {',
3019
+ ' "target": "ES2022",',
3020
+ ' "lib": ["ES2022", "DOM"],',
3021
+ ' "types": ["node"],',
3022
+ ' "module": "ESNext",',
3023
+ ' "moduleResolution": "Bundler",',
3024
+ ' "strict": true,',
3025
+ ' "skipLibCheck": true,',
3026
+ ' "resolveJsonModule": true,',
3027
+ ' "isolatedModules": true,',
3028
+ ' "noEmit": true',
3029
+ " },",
3030
+ ' "include": ["src"]',
3031
+ "}",
3032
+ "",
3033
+ ].join("\n");
3034
+ }
2698
3035
  function reactViteConfig() {
2699
3036
  return [
2700
3037
  'import react from "@vitejs/plugin-react";',
@@ -2848,10 +3185,10 @@ function shellQuote(value) {
2848
3185
  function normalizedOrigin(value) {
2849
3186
  const parsed = new URL(value);
2850
3187
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
2851
- throw new Error("Expected an HTTP(S) API base URL.");
3188
+ throw cliError("INVALID_ARGUMENT", "error.expectedHttpOrigin");
2852
3189
  }
2853
3190
  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.");
3191
+ throw cliError("INVALID_ARGUMENT", "error.expectedBareOrigin");
2855
3192
  }
2856
3193
  return parsed.origin;
2857
3194
  }
@@ -2866,95 +3203,20 @@ function resolveAgentPath(value, runtime) {
2866
3203
  if (value.startsWith("~/")) {
2867
3204
  const home = runtime.env.HOME;
2868
3205
  if (home == null) {
2869
- throw new Error("Cannot expand ~ because HOME is not set.");
3206
+ throw cliError("INVALID_ARGUMENT", "error.noHome");
2870
3207
  }
2871
3208
  return path.join(home, value.slice(2));
2872
3209
  }
2873
3210
  return path.resolve(runtime.cwd, value);
2874
3211
  }
3212
+ /**
3213
+ * The skill a coding agent installs is the same file this repository
3214
+ * documents. `docs/agents/remotedraw/SKILL.md` is the single source; the
3215
+ * module below is generated from it by `bun run generate:skill`, and
3216
+ * tests/agent-skill.test.ts fails if the two drift apart again.
3217
+ */
2875
3218
  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");
3219
+ return AGENT_SKILL_MARKDOWN;
2958
3220
  }
2959
3221
  /**
2960
3222
  * Doctor used to read REMOTEDRAW_API_BASE_URL raw and call an unset value a
@@ -2991,20 +3253,30 @@ function resolveDoctorApiBaseUrl(projectEnv, requested) {
2991
3253
  }
2992
3254
  function doctorApiBaseUrlMessage(resolved) {
2993
3255
  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.`;
3256
+ return t("doctor.baseUrl.placeholder", {
3257
+ value: resolved.value,
3258
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
3259
+ });
2995
3260
  }
2996
3261
  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.`;
3262
+ return t("doctor.baseUrl.invalid", {
3263
+ value: resolved.value,
3264
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
3265
+ });
2998
3266
  }
2999
3267
  if (resolved.source === "default") {
3000
- return `Using ${resolved.url} (RemoteDraw production API; nothing overrides it here).`;
3268
+ return t("doctor.baseUrl.default", { url: resolved.url });
3001
3269
  }
3002
3270
  const origin = resolved.source === "flag"
3003
- ? "--api-base-url"
3004
- : "this project's environment";
3271
+ ? t("doctor.baseUrl.origin.flag")
3272
+ : t("doctor.baseUrl.origin.project");
3005
3273
  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).`;
3274
+ ? t("doctor.baseUrl.production", { url: resolved.url, origin })
3275
+ : t("doctor.baseUrl.override", {
3276
+ url: resolved.url,
3277
+ origin,
3278
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
3279
+ });
3008
3280
  }
3009
3281
  function isHttpOrigin(value) {
3010
3282
  if (value == null || value.includes("<"))
@@ -3045,24 +3317,27 @@ function planDefaultsForExample(example) {
3045
3317
  };
3046
3318
  }
3047
3319
  function scaffoldSummary(verb, plan, outputDir, written, dryRun) {
3048
- const nextCommandPrefix = dryRun ? "Would write" : "Wrote";
3049
3320
  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}:`,
3321
+ t("scaffold.headline", {
3322
+ verb,
3323
+ target: plan.target,
3324
+ dir: outputDir,
3325
+ }),
3326
+ "",
3327
+ t("scaffold.choices"),
3328
+ t("scaffold.choice.app", { value: plan.appName }),
3329
+ t("scaffold.choice.sdk", { value: plan.sdk }),
3330
+ t("scaffold.choice.sender", { value: plan.sender }),
3331
+ t("scaffold.choice.starter", { value: plan.preset }),
3332
+ "",
3333
+ dryRun ? t("scaffold.wouldWrite") : t("scaffold.wrote"),
3059
3334
  ...written.map((file) => ` ${file}`),
3060
3335
  "",
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.",
3336
+ t("word.next"),
3337
+ t("scaffold.next.install", { command: installCommand(plan.packageManager) }),
3338
+ t("scaffold.next.secrets"),
3339
+ t("scaffold.next.createInput"),
3340
+ t("scaffold.next.render"),
3066
3341
  ].join("\n");
3067
3342
  }
3068
3343
  function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisioning, offline, dryRun) {
@@ -3080,16 +3355,12 @@ function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisionin
3080
3355
  .map(([field, , value]) => ({ field, value }));
3081
3356
  const warnings = [
3082
3357
  ...(plan.apiBaseUrl.includes("<deployment>")
3083
- ? ["Replace the deployment API placeholder before making live requests."]
3358
+ ? [t("cloud.warning.placeholder")]
3084
3359
  : []),
3085
3360
  ...(offline
3086
- ? [
3087
- "Cloud provisioning was skipped; no dashboard project or API key was created.",
3088
- ]
3361
+ ? [t("cloud.warning.offline")]
3089
3362
  : dryRun
3090
- ? [
3091
- "Cloud authentication and provisioning were not executed during dry-run.",
3092
- ]
3363
+ ? [t("cloud.warning.dryRun")]
3093
3364
  : []),
3094
3365
  ];
3095
3366
  return {