@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/LICENSE +21 -0
- package/README.md +56 -1
- package/dist/cli.d.ts +11 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1020 -749
- package/dist/cloud.d.ts +13 -0
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +61 -14
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/generated/agent-skill.d.ts +2 -0
- package/dist/generated/agent-skill.d.ts.map +1 -0
- package/dist/generated/agent-skill.js +4 -0
- package/dist/i18n.d.ts +74 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +178 -0
- package/dist/index.js +17 -1
- package/dist/locales/catalog.d.ts +21 -0
- package/dist/locales/catalog.d.ts.map +1 -0
- package/dist/locales/catalog.js +1 -0
- package/dist/locales/en.d.ts +420 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +446 -0
- package/dist/locales/index.d.ts +436 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/index.js +27 -0
- package/dist/locales/nl.d.ts +10 -0
- package/dist/locales/nl.d.ts.map +1 -0
- package/dist/locales/nl.js +438 -0
- package/dist/setup-wizard.d.ts +1 -1
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +50 -36
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +145 -0
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +8 -3
- package/package.json +11 -4
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
|
-
|
|
17
|
-
|
|
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 = [
|
|
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("
|
|
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(
|
|
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("
|
|
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
|
-
|
|
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
|
|
247
|
-
const
|
|
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(
|
|
290
|
-
: fail(
|
|
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(
|
|
372
|
+
? jsonFailure(command, error)
|
|
296
373
|
: fail(errorMessage(error));
|
|
297
374
|
}
|
|
298
375
|
}
|
|
299
|
-
|
|
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
|
-
"
|
|
370
|
-
"",
|
|
371
|
-
"
|
|
372
|
-
"
|
|
373
|
-
"",
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
388
|
-
"",
|
|
389
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
635
|
+
t("options.machineReadable"),
|
|
416
636
|
].join("\n"));
|
|
417
637
|
}
|
|
418
638
|
function guideText() {
|
|
419
639
|
return [
|
|
420
|
-
"
|
|
640
|
+
t("guide.title"),
|
|
421
641
|
"",
|
|
422
|
-
"
|
|
423
|
-
"
|
|
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
|
-
"
|
|
646
|
+
t("guide.path.webOwned"),
|
|
427
647
|
" remotedraw init --target web --sender embedded-web --sdk react --preset sketch",
|
|
428
648
|
"",
|
|
429
|
-
"
|
|
649
|
+
t("guide.path.svelteIos"),
|
|
430
650
|
" remotedraw init --target web --sender remotedraw-ios --sdk svelte --preset signature",
|
|
431
651
|
"",
|
|
432
|
-
"
|
|
652
|
+
t("guide.path.desktop"),
|
|
433
653
|
" remotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch",
|
|
434
654
|
"",
|
|
435
|
-
"
|
|
655
|
+
t("guide.path.ownIos"),
|
|
436
656
|
" remotedraw init --target ios --sender own-ios --sdk swift --preset sketch",
|
|
437
657
|
"",
|
|
438
|
-
"
|
|
439
|
-
"
|
|
440
|
-
"
|
|
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
|
-
|
|
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
|
-
?
|
|
476
|
-
:
|
|
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(
|
|
706
|
+
return fail(t("whoami.notLoggedIn", { url: apiBaseUrl }));
|
|
489
707
|
}
|
|
490
|
-
return ok([accountSummary(account),
|
|
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 || "
|
|
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
|
|
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("
|
|
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("
|
|
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
|
|
842
|
+
return `\n\n${t("cloud.skipped")}`;
|
|
620
843
|
}
|
|
621
844
|
if (!provisioning)
|
|
622
845
|
return "";
|
|
623
846
|
return [
|
|
624
847
|
"",
|
|
625
848
|
"",
|
|
626
|
-
|
|
627
|
-
"
|
|
628
|
-
|
|
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: "
|
|
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: "
|
|
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:
|
|
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
|
-
:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
740
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 = ["
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
? "
|
|
829
|
-
: "
|
|
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", "
|
|
1080
|
+
addCheck("warn", t("doctor.label.deployment"), t("doctor.network.offline"));
|
|
835
1081
|
}
|
|
836
1082
|
else if (!normalizedEnvApiBaseUrl) {
|
|
837
|
-
addCheck("warn", "deployment", "
|
|
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", "
|
|
842
|
-
?
|
|
843
|
-
:
|
|
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", "
|
|
1094
|
+
addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoKey"));
|
|
846
1095
|
}
|
|
847
1096
|
else if (!health.ok) {
|
|
848
|
-
addCheck("warn", "
|
|
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", "
|
|
853
|
-
?
|
|
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", "
|
|
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", "
|
|
865
|
-
?
|
|
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
|
-
?
|
|
868
|
-
|
|
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
|
-
|
|
1145
|
+
DEFAULT_REMOTEDRAW_API_BASE_URL;
|
|
892
1146
|
return ok([
|
|
893
|
-
"
|
|
1147
|
+
t("dev.title"),
|
|
894
1148
|
"",
|
|
895
|
-
|
|
1149
|
+
t("dev.apiBaseUrl", { url: apiBaseUrl }),
|
|
896
1150
|
"",
|
|
897
|
-
"
|
|
898
|
-
" Inside this repository: bun run dev:codex",
|
|
1151
|
+
t("dev.hosted"),
|
|
899
1152
|
"",
|
|
900
|
-
"
|
|
901
|
-
"
|
|
1153
|
+
t("dev.step1"),
|
|
1154
|
+
t("dev.step1.detail"),
|
|
902
1155
|
"",
|
|
903
|
-
"
|
|
1156
|
+
t("dev.step2"),
|
|
904
1157
|
` remotedraw create-input --preset signature --api-base-url ${apiBaseUrl}`,
|
|
905
1158
|
"",
|
|
906
|
-
"
|
|
907
|
-
"
|
|
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
|
-
|
|
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("
|
|
1191
|
+
throw new Error(t("createInput.error.noFetch"));
|
|
939
1192
|
if (!apiKey?.startsWith("rd_sk_")) {
|
|
940
|
-
throw new Error("
|
|
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 ||
|
|
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
|
-
"
|
|
1212
|
+
t("createInput.title"),
|
|
960
1213
|
"",
|
|
961
|
-
"
|
|
1214
|
+
t("createInput.warning"),
|
|
962
1215
|
"",
|
|
963
1216
|
curlForCreateInput(apiBaseUrl, payload),
|
|
964
1217
|
"",
|
|
965
|
-
"
|
|
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
|
-
"
|
|
1231
|
+
t("examples.title"),
|
|
979
1232
|
"",
|
|
980
|
-
"
|
|
981
|
-
"
|
|
982
|
-
"
|
|
983
|
-
"
|
|
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
|
-
"
|
|
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("
|
|
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
|
-
"
|
|
1270
|
+
t("agent.title"),
|
|
1018
1271
|
"",
|
|
1019
|
-
"
|
|
1272
|
+
t("agent.intro"),
|
|
1020
1273
|
"",
|
|
1021
|
-
"
|
|
1274
|
+
t("agent.printSkill"),
|
|
1022
1275
|
" remotedraw agent --print-skill",
|
|
1023
1276
|
"",
|
|
1024
|
-
"
|
|
1025
|
-
|
|
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("
|
|
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
|
-
?
|
|
1042
|
-
:
|
|
1297
|
+
? t("agent.wouldWrite", { path: outputFile })
|
|
1298
|
+
: t("agent.installed", { path: outputFile }),
|
|
1043
1299
|
"",
|
|
1044
|
-
"
|
|
1045
|
-
"
|
|
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
|
-
"
|
|
1051
|
-
"
|
|
1052
|
-
"
|
|
1053
|
-
"",
|
|
1054
|
-
"
|
|
1055
|
-
"",
|
|
1056
|
-
"
|
|
1057
|
-
"
|
|
1058
|
-
"
|
|
1059
|
-
"
|
|
1060
|
-
"
|
|
1061
|
-
"
|
|
1062
|
-
"
|
|
1063
|
-
"
|
|
1064
|
-
"
|
|
1065
|
-
"
|
|
1066
|
-
"
|
|
1067
|
-
"
|
|
1068
|
-
"
|
|
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
|
-
"
|
|
1074
|
-
"
|
|
1331
|
+
t("word.usage"),
|
|
1332
|
+
t("init.help.usage"),
|
|
1075
1333
|
"",
|
|
1076
|
-
"
|
|
1334
|
+
t("init.help.body"),
|
|
1077
1335
|
].join("\n");
|
|
1078
1336
|
}
|
|
1079
|
-
function
|
|
1337
|
+
function languageHelpText() {
|
|
1080
1338
|
return [
|
|
1081
|
-
"
|
|
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
|
-
"
|
|
1084
|
-
"
|
|
1345
|
+
t("language.help.body"),
|
|
1346
|
+
t("language.help.override"),
|
|
1085
1347
|
"",
|
|
1086
|
-
"
|
|
1348
|
+
t("language.available"),
|
|
1349
|
+
...SUPPORTED_LOCALES.map((locale) => ` ${locale.padEnd(6)} ${localeEndonym(locale)}`),
|
|
1087
1350
|
].join("\n");
|
|
1088
1351
|
}
|
|
1089
|
-
function
|
|
1352
|
+
function loginHelpText() {
|
|
1090
1353
|
return [
|
|
1091
|
-
"
|
|
1354
|
+
t("login.help.title"),
|
|
1092
1355
|
"",
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
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
|
-
"
|
|
1108
|
-
"
|
|
1109
|
-
"",
|
|
1110
|
-
"
|
|
1111
|
-
|
|
1112
|
-
"
|
|
1113
|
-
"
|
|
1114
|
-
"
|
|
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
|
-
"
|
|
1120
|
-
"
|
|
1382
|
+
t("word.usage"),
|
|
1383
|
+
t("update.help.usage"),
|
|
1121
1384
|
"",
|
|
1122
|
-
|
|
1123
|
-
"
|
|
1124
|
-
"
|
|
1385
|
+
t("update.help.body", { package: CLI_PACKAGE_NAME }),
|
|
1386
|
+
t("update.help.global"),
|
|
1387
|
+
t("update.help.check"),
|
|
1125
1388
|
"",
|
|
1126
|
-
"
|
|
1389
|
+
t("update.help.silence"),
|
|
1127
1390
|
].join("\n");
|
|
1128
1391
|
}
|
|
1129
1392
|
function optionsHelpText() {
|
|
1130
1393
|
return [
|
|
1131
|
-
"
|
|
1394
|
+
t("word.usage"),
|
|
1132
1395
|
" remotedraw options [--format text|json]",
|
|
1133
1396
|
"",
|
|
1134
|
-
"
|
|
1397
|
+
t("options.help.body"),
|
|
1135
1398
|
].join("\n");
|
|
1136
1399
|
}
|
|
1137
1400
|
function devHelpText() {
|
|
1138
1401
|
return [
|
|
1139
|
-
"
|
|
1140
|
-
"
|
|
1402
|
+
t("word.usage"),
|
|
1403
|
+
t("dev.help.usage"),
|
|
1141
1404
|
"",
|
|
1142
|
-
"
|
|
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
|
-
"
|
|
1148
|
-
"
|
|
1149
|
-
"",
|
|
1150
|
-
"
|
|
1151
|
-
"
|
|
1152
|
-
"
|
|
1153
|
-
"
|
|
1154
|
-
"
|
|
1155
|
-
"
|
|
1156
|
-
"
|
|
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
|
-
"
|
|
1162
|
-
"
|
|
1163
|
-
"
|
|
1425
|
+
t("word.usage"),
|
|
1426
|
+
t("examples.help.usage.list"),
|
|
1427
|
+
t("examples.help.usage.install"),
|
|
1164
1428
|
"",
|
|
1165
|
-
|
|
1429
|
+
t("word.examples", { list: exampleChoices.join(", ") }),
|
|
1166
1430
|
].join("\n");
|
|
1167
1431
|
}
|
|
1168
1432
|
function agentHelpText() {
|
|
1169
1433
|
return [
|
|
1170
|
-
"
|
|
1171
|
-
"
|
|
1172
|
-
"
|
|
1173
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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?.("
|
|
1507
|
+
await prompts.intro?.(t("prompt.intro.setup"));
|
|
1243
1508
|
const appName = readString(parsed, "app-name") ??
|
|
1244
1509
|
(await prompts.text({
|
|
1245
|
-
message: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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() === "" ? "
|
|
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: "
|
|
1302
|
-
helperText: "
|
|
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: "
|
|
1311
|
-
helperText: "
|
|
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: "
|
|
1324
|
-
{ value: "desktop", label: "
|
|
1325
|
-
{ value: "ios", label: "
|
|
1326
|
-
{ value: "headless", label: "
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
|
1625
|
+
return choices.map((choiceOption) => ({
|
|
1362
1626
|
...choiceOption,
|
|
1363
1627
|
details: details[choiceOption.value],
|
|
1364
1628
|
}));
|
|
1365
1629
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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: "
|
|
1430
|
-
hint: "
|
|
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: "
|
|
1439
|
-
hint: "
|
|
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: "
|
|
1448
|
-
hint: "
|
|
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: "
|
|
1453
|
-
hint: "
|
|
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: "
|
|
1461
|
-
hint: "
|
|
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: "
|
|
1466
|
-
hint: "
|
|
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: "
|
|
1476
|
-
hint: "
|
|
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: "
|
|
1485
|
-
hint: "
|
|
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: "
|
|
1490
|
-
hint: "
|
|
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: "
|
|
1499
|
-
hint: "
|
|
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: "
|
|
1507
|
-
hint: "
|
|
1774
|
+
label: t("choice.sdk.react.label"),
|
|
1775
|
+
hint: t("choice.sdk.react.hint"),
|
|
1508
1776
|
},
|
|
1509
1777
|
{
|
|
1510
1778
|
value: "svelte",
|
|
1511
|
-
label: "
|
|
1512
|
-
hint: "
|
|
1779
|
+
label: t("choice.sdk.svelte.label"),
|
|
1780
|
+
hint: t("choice.sdk.svelte.hint"),
|
|
1513
1781
|
},
|
|
1514
1782
|
{
|
|
1515
1783
|
value: "js",
|
|
1516
|
-
label: "
|
|
1517
|
-
hint: "
|
|
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: "
|
|
1526
|
-
hint: "
|
|
1793
|
+
label: t("choice.preset.signature.label"),
|
|
1794
|
+
hint: t("choice.preset.signature.hint"),
|
|
1527
1795
|
},
|
|
1528
1796
|
{
|
|
1529
1797
|
value: "sketch",
|
|
1530
|
-
label: "
|
|
1531
|
-
hint: "
|
|
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
|
|
1542
|
-
return
|
|
1543
|
-
{
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1737
|
-
kind: "
|
|
1738
|
-
options:
|
|
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:
|
|
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
|
-
["
|
|
1803
|
-
["
|
|
1804
|
-
["
|
|
1805
|
-
["
|
|
1806
|
-
["
|
|
1807
|
-
["
|
|
1808
|
-
["
|
|
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: "
|
|
2051
|
+
label: t("field.appName"),
|
|
1818
2052
|
kind: "text",
|
|
1819
2053
|
validate: validateWizardProjectName,
|
|
1820
2054
|
preview: (values) => values.appName.trim() === ""
|
|
1821
2055
|
? undefined
|
|
1822
|
-
:
|
|
2056
|
+
: t("field.appName.preview", { slug: slugify(values.appName) }),
|
|
1823
2057
|
},
|
|
1824
2058
|
{
|
|
1825
2059
|
key: "target",
|
|
1826
|
-
label: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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() === "" ? "
|
|
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:
|
|
2105
|
+
apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
1933
2106
|
};
|
|
1934
2107
|
const nameError = validateProjectName(plan.appName);
|
|
1935
2108
|
if (nameError)
|
|
1936
|
-
throw
|
|
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
|
|
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
|
-
|
|
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
|
|
2162
|
+
throw cliError("INVALID_COMBINATION", "error.plan.ownIosNeedsSwift");
|
|
1990
2163
|
}
|
|
1991
2164
|
if (plan.sdk === "swift" && plan.sender !== "own-ios") {
|
|
1992
|
-
throw
|
|
2165
|
+
throw cliError("INVALID_COMBINATION", "error.plan.swiftNeedsOwnIos");
|
|
1993
2166
|
}
|
|
1994
2167
|
if (plan.target === "ios" && plan.sender !== "own-ios") {
|
|
1995
|
-
throw
|
|
2168
|
+
throw cliError("INVALID_COMBINATION", "error.plan.iosNeedsOwnIos");
|
|
1996
2169
|
}
|
|
1997
2170
|
if (plan.sender === "headless" && plan.sdk === "react") {
|
|
1998
|
-
throw
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
2152
|
-
|
|
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
|
-
|
|
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": "
|
|
2377
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
2186
2378
|
typescript: "^6.0.0",
|
|
2187
|
-
vite: "
|
|
2379
|
+
vite: "^8.0.0",
|
|
2188
2380
|
},
|
|
2189
2381
|
};
|
|
2190
2382
|
}
|
|
2191
|
-
|
|
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
|
-
|
|
2205
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
2541
|
+
" <RemoteDrawSessionControls />",
|
|
2337
2542
|
" </RemoteDrawProvider>",
|
|
2338
2543
|
" );",
|
|
2339
2544
|
"}",
|
|
2340
2545
|
"",
|
|
2341
2546
|
].join("\n");
|
|
2342
2547
|
}
|
|
2343
|
-
function
|
|
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 {
|
|
2346
|
-
"",
|
|
2347
|
-
"
|
|
2348
|
-
"
|
|
2349
|
-
"
|
|
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
|
-
"
|
|
2353
|
-
"
|
|
2354
|
-
"
|
|
2355
|
-
"
|
|
2356
|
-
"
|
|
2357
|
-
"
|
|
2358
|
-
"
|
|
2359
|
-
"
|
|
2360
|
-
"
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3188
|
+
throw cliError("INVALID_ARGUMENT", "error.expectedHttpOrigin");
|
|
2852
3189
|
}
|
|
2853
3190
|
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
2854
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3268
|
+
return t("doctor.baseUrl.default", { url: resolved.url });
|
|
3001
3269
|
}
|
|
3002
3270
|
const origin = resolved.source === "flag"
|
|
3003
|
-
? "
|
|
3004
|
-
: "
|
|
3271
|
+
? t("doctor.baseUrl.origin.flag")
|
|
3272
|
+
: t("doctor.baseUrl.origin.project");
|
|
3005
3273
|
return resolved.isProductionDefault
|
|
3006
|
-
?
|
|
3007
|
-
:
|
|
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
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
"",
|
|
3058
|
-
|
|
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
|
-
"
|
|
3062
|
-
|
|
3063
|
-
"
|
|
3064
|
-
"
|
|
3065
|
-
"
|
|
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
|
-
? ["
|
|
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 {
|