@remotedraw/cli 0.1.2 → 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 +79 -2
- package/dist/cli.d.ts +15 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1212 -759
- 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 +35 -2
- 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 +10 -2
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +593 -102
- 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 +53 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +240 -0
- package/package.json +11 -4
package/dist/cli.js
CHANGED
|
@@ -3,22 +3,42 @@ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { emitKeypressEvents } from "node:readline";
|
|
7
8
|
import { createInterface } from "node:readline/promises";
|
|
8
9
|
import { createWizardTerminal, runSetupWizard, } from "./setup-wizard.js";
|
|
9
|
-
import { assertEnvCanAcceptProvisioning, currentCliAccount, 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";
|
|
15
|
+
import { checkForUpdate, CLI_PACKAGE_NAME, detectInstallMethod, installMethodDisplay, runInstall, } from "./update.js";
|
|
10
16
|
const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
17
|
export const CLI_VERSION = typeof packageMetadata.version === "string"
|
|
12
18
|
? packageMetadata.version
|
|
13
19
|
: "unknown";
|
|
14
|
-
|
|
15
|
-
|
|
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 = [
|
|
16
30
|
"remotedraw-ios",
|
|
17
31
|
"embedded-web",
|
|
18
32
|
"own-ios",
|
|
19
33
|
"headless",
|
|
20
34
|
];
|
|
21
|
-
const sdkChoices = [
|
|
35
|
+
export const sdkChoices = [
|
|
36
|
+
"react",
|
|
37
|
+
"svelte",
|
|
38
|
+
"js",
|
|
39
|
+
"swift",
|
|
40
|
+
"headless",
|
|
41
|
+
];
|
|
22
42
|
const presetChoices = [
|
|
23
43
|
"signature",
|
|
24
44
|
"initials",
|
|
@@ -38,15 +58,16 @@ const exampleChoices = [
|
|
|
38
58
|
"ios-owned-sender",
|
|
39
59
|
];
|
|
40
60
|
const packageManagerChoices = ["npm", "pnpm", "yarn", "bun"];
|
|
41
|
-
const apiEndpointChoices = ["deployment", "local"];
|
|
42
61
|
export function createNodeRuntime() {
|
|
43
62
|
const cwd = process.cwd();
|
|
44
63
|
const env = cliEnvironmentForWorkspace(cwd, process.env);
|
|
45
64
|
return {
|
|
46
65
|
cwd,
|
|
47
66
|
env,
|
|
67
|
+
binPath: process.argv[1] ?? fileURLToPath(import.meta.url),
|
|
48
68
|
isInteractive: Boolean(process.stdin.isTTY && process.stderr.isTTY),
|
|
49
69
|
exists: existsSync,
|
|
70
|
+
spawnCommand: runChildCommand,
|
|
50
71
|
readFile: async (filePath) => {
|
|
51
72
|
return await readFile(filePath, "utf8");
|
|
52
73
|
},
|
|
@@ -78,10 +99,22 @@ export function cliEnvironmentForWorkspace(cwd, env, exists = existsSync) {
|
|
|
78
99
|
? { ...env, REMOTEDRAW_API_BASE_URL: env.CONVEX_SITE_URL }
|
|
79
100
|
: env;
|
|
80
101
|
}
|
|
102
|
+
async function runChildCommand(command, args) {
|
|
103
|
+
return await new Promise((resolve, reject) => {
|
|
104
|
+
const child = spawn(command, args, {
|
|
105
|
+
stdio: "inherit",
|
|
106
|
+
// Windows resolves npm/pnpm/yarn through .cmd shims, which need a shell.
|
|
107
|
+
shell: process.platform === "win32",
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
});
|
|
110
|
+
child.once("error", reject);
|
|
111
|
+
child.once("close", (code) => resolve({ exitCode: code ?? 1 }));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
81
114
|
async function openExternalUrl(url) {
|
|
82
115
|
const parsed = new URL(url);
|
|
83
116
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
84
|
-
throw new Error("
|
|
117
|
+
throw new Error(t("error.httpUrlsOnly"));
|
|
85
118
|
}
|
|
86
119
|
const [command, args] = process.platform === "darwin"
|
|
87
120
|
? ["open", [url]]
|
|
@@ -132,10 +165,10 @@ function createTerminalPrompter(input, output) {
|
|
|
132
165
|
}
|
|
133
166
|
function terminalSelect(input, output, options) {
|
|
134
167
|
if (options.choices.length === 0) {
|
|
135
|
-
throw new Error(
|
|
168
|
+
throw new Error(t("error.noChoices", { message: options.message }));
|
|
136
169
|
}
|
|
137
170
|
if (!input.isTTY || !output.isTTY || !input.setRawMode) {
|
|
138
|
-
throw new Error("
|
|
171
|
+
throw new Error(t("error.interactiveNeedsTty"));
|
|
139
172
|
}
|
|
140
173
|
const defaultIndex = Math.max(0, options.choices.findIndex((choiceOption) => choiceOption.value === options.defaultValue));
|
|
141
174
|
let selectedIndex = defaultIndex;
|
|
@@ -154,7 +187,7 @@ function terminalSelect(input, output, options) {
|
|
|
154
187
|
const selected = options.choices[selectedIndex];
|
|
155
188
|
cleanup();
|
|
156
189
|
clearRendered(output, renderedLines);
|
|
157
|
-
output.write(`${style("
|
|
190
|
+
output.write(`${style("green", "✓")} ${options.message} ${style("green", selected.label)}\n`);
|
|
158
191
|
resolve(selected.value);
|
|
159
192
|
};
|
|
160
193
|
const fail = () => {
|
|
@@ -197,18 +230,25 @@ function terminalSelect(input, output, options) {
|
|
|
197
230
|
});
|
|
198
231
|
}
|
|
199
232
|
function selectPromptLines(options, selectedIndex) {
|
|
200
|
-
const lines = [`${style("cyan", "?")} ${options.message}`];
|
|
233
|
+
const lines = [`${style("cyan", "?")} ${style("bold", options.message)}`];
|
|
201
234
|
if (options.helperText)
|
|
202
235
|
lines.push(style("dim", options.helperText));
|
|
203
236
|
for (const [index, choiceOption] of options.choices.entries()) {
|
|
204
237
|
const selected = index === selectedIndex;
|
|
238
|
+
const marker = selected ? style("cyan", "❯ ◉") : style("dim", " ○");
|
|
205
239
|
const label = selected
|
|
206
240
|
? style("bold", choiceOption.label)
|
|
207
241
|
: choiceOption.label;
|
|
208
242
|
const hint = choiceOption.hint ? ` ${style("dim", choiceOption.hint)}` : "";
|
|
209
|
-
lines.push(`${
|
|
210
|
-
}
|
|
211
|
-
|
|
243
|
+
lines.push(`${marker} ${label}${hint}`);
|
|
244
|
+
}
|
|
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(" · "));
|
|
212
252
|
return lines;
|
|
213
253
|
}
|
|
214
254
|
function clearRendered(output, lineCount) {
|
|
@@ -224,13 +264,61 @@ function style(kind, value) {
|
|
|
224
264
|
};
|
|
225
265
|
return `${codes[kind]}${value}\x1b[0m`;
|
|
226
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;
|
|
227
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);
|
|
228
282
|
try {
|
|
229
|
-
const
|
|
230
|
-
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);
|
|
231
319
|
if (command == null) {
|
|
232
320
|
if (runtime.wizardTerminal) {
|
|
233
|
-
return await setupWizardCommand(runtime);
|
|
321
|
+
return await setupWizardCommand(runtime, askLanguage);
|
|
234
322
|
}
|
|
235
323
|
return ok(mainHelpText());
|
|
236
324
|
}
|
|
@@ -245,6 +333,8 @@ export async function runCli(args, runtime = createNodeRuntime()) {
|
|
|
245
333
|
return ok(guideText());
|
|
246
334
|
case "options":
|
|
247
335
|
return optionsCommand(commandArgs);
|
|
336
|
+
case "language":
|
|
337
|
+
return await languageCommand(commandArgs, runtime);
|
|
248
338
|
case "login":
|
|
249
339
|
return await loginCommand(commandArgs, runtime);
|
|
250
340
|
case "logout":
|
|
@@ -252,9 +342,11 @@ export async function runCli(args, runtime = createNodeRuntime()) {
|
|
|
252
342
|
case "whoami":
|
|
253
343
|
return await whoamiCommand(commandArgs, runtime);
|
|
254
344
|
case "new":
|
|
255
|
-
return await newCommand(commandArgs, runtime);
|
|
345
|
+
return await newCommand(commandArgs, runtime, askLanguage);
|
|
256
346
|
case "init":
|
|
257
347
|
return await initCommand(commandArgs, runtime);
|
|
348
|
+
case "update":
|
|
349
|
+
return await updateCommand(commandArgs, runtime);
|
|
258
350
|
case "doctor":
|
|
259
351
|
return await doctorCommand(commandArgs, runtime);
|
|
260
352
|
case "dev":
|
|
@@ -267,20 +359,36 @@ export async function runCli(args, runtime = createNodeRuntime()) {
|
|
|
267
359
|
return await agentCommand(commandArgs, runtime);
|
|
268
360
|
default:
|
|
269
361
|
return requestedJsonOutput(args)
|
|
270
|
-
? jsonFailure(command, new Error(
|
|
271
|
-
: fail(
|
|
362
|
+
? jsonFailure(command, new Error(t("error.unknownCommand", { command })))
|
|
363
|
+
: fail(t("error.unknownCommand", { command }));
|
|
272
364
|
}
|
|
273
365
|
}
|
|
274
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));
|
|
275
371
|
return requestedJsonOutput(args)
|
|
276
|
-
? jsonFailure(
|
|
372
|
+
? jsonFailure(command, error)
|
|
277
373
|
: fail(errorMessage(error));
|
|
278
374
|
}
|
|
279
375
|
}
|
|
280
|
-
|
|
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) {
|
|
281
386
|
const terminal = runtime.wizardTerminal;
|
|
282
387
|
if (!terminal)
|
|
283
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);
|
|
284
392
|
if (!cloudSetupDisabled(runtime)) {
|
|
285
393
|
const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env);
|
|
286
394
|
await loginToRemoteDraw(runtime, apiBaseUrl);
|
|
@@ -305,6 +413,121 @@ async function setupWizardCommand(runtime) {
|
|
|
305
413
|
});
|
|
306
414
|
return { exitCode: result.exitCode, stdout: "" };
|
|
307
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
|
+
}
|
|
308
531
|
function ok(stdout) {
|
|
309
532
|
return { exitCode: 0, stdout: ensureTrailingNewline(stdout) };
|
|
310
533
|
}
|
|
@@ -316,6 +539,12 @@ function jsonResult(value, exitCode = 0) {
|
|
|
316
539
|
}
|
|
317
540
|
function jsonFailure(command, error) {
|
|
318
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
|
+
}
|
|
319
548
|
const code = message.includes("required")
|
|
320
549
|
? "MISSING_ARGUMENT"
|
|
321
550
|
: message.includes(" requires ") || message.includes("only supported")
|
|
@@ -329,6 +558,14 @@ function jsonFailure(command, error) {
|
|
|
329
558
|
: "COMMAND_FAILED";
|
|
330
559
|
return jsonResult({ ok: false, command, error: { code, message } }, 1);
|
|
331
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
|
+
}
|
|
332
569
|
function requestedJsonOutput(args) {
|
|
333
570
|
return args.some((argument, index) => argument === "--format=json" ||
|
|
334
571
|
(argument === "--format" && args[index + 1] === "json"));
|
|
@@ -346,31 +583,34 @@ function errorMessage(error) {
|
|
|
346
583
|
return error instanceof Error ? error.message : String(error);
|
|
347
584
|
}
|
|
348
585
|
function mainHelpText() {
|
|
586
|
+
const command = (name, description) => ` ${name.padEnd(14)} ${description}`;
|
|
349
587
|
return [
|
|
350
|
-
"
|
|
351
|
-
"",
|
|
352
|
-
"
|
|
353
|
-
"
|
|
354
|
-
"",
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"",
|
|
369
|
-
"
|
|
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"),
|
|
370
610
|
" remotedraw guide",
|
|
371
611
|
" remotedraw login",
|
|
372
612
|
" remotedraw new",
|
|
373
|
-
" remotedraw new --app-name
|
|
613
|
+
" remotedraw new --app-name MyApp --target web --sender remotedraw-ios --sdk react",
|
|
374
614
|
" remotedraw init --target web --sender embedded-web --sdk react",
|
|
375
615
|
].join("\n");
|
|
376
616
|
}
|
|
@@ -385,39 +625,39 @@ function optionsCommand(args) {
|
|
|
385
625
|
if (outputFormat(parsed) === "json")
|
|
386
626
|
return jsonResult(catalog);
|
|
387
627
|
return ok([
|
|
388
|
-
"
|
|
628
|
+
t("options.title"),
|
|
389
629
|
"",
|
|
390
630
|
...catalog.fields.flatMap((field) => [
|
|
391
631
|
`${field.label} (${field.flag})`,
|
|
392
632
|
...field.options.map((option) => ` ${option.value}: ${option.details.summary}`),
|
|
393
633
|
"",
|
|
394
634
|
]),
|
|
395
|
-
"
|
|
635
|
+
t("options.machineReadable"),
|
|
396
636
|
].join("\n"));
|
|
397
637
|
}
|
|
398
638
|
function guideText() {
|
|
399
639
|
return [
|
|
400
|
-
"
|
|
640
|
+
t("guide.title"),
|
|
401
641
|
"",
|
|
402
|
-
"
|
|
403
|
-
"
|
|
642
|
+
t("guide.intro"),
|
|
643
|
+
t("guide.path.webIos"),
|
|
404
644
|
" remotedraw init --target web --sender remotedraw-ios --sdk react --preset signature",
|
|
405
645
|
"",
|
|
406
|
-
"
|
|
646
|
+
t("guide.path.webOwned"),
|
|
407
647
|
" remotedraw init --target web --sender embedded-web --sdk react --preset sketch",
|
|
408
648
|
"",
|
|
409
|
-
"
|
|
649
|
+
t("guide.path.svelteIos"),
|
|
410
650
|
" remotedraw init --target web --sender remotedraw-ios --sdk svelte --preset signature",
|
|
411
651
|
"",
|
|
412
|
-
"
|
|
652
|
+
t("guide.path.desktop"),
|
|
413
653
|
" remotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch",
|
|
414
654
|
"",
|
|
415
|
-
"
|
|
655
|
+
t("guide.path.ownIos"),
|
|
416
656
|
" remotedraw init --target ios --sender own-ios --sdk swift --preset sketch",
|
|
417
657
|
"",
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
658
|
+
t("guide.note.keys"),
|
|
659
|
+
t("guide.note.provisioning"),
|
|
660
|
+
t("guide.note.offline"),
|
|
421
661
|
].join("\n");
|
|
422
662
|
}
|
|
423
663
|
async function loginCommand(args, runtime) {
|
|
@@ -435,11 +675,9 @@ async function loginCommand(args, runtime) {
|
|
|
435
675
|
...(deviceName == null ? {} : { deviceName }),
|
|
436
676
|
});
|
|
437
677
|
return ok([
|
|
438
|
-
result.alreadyLoggedIn
|
|
439
|
-
? "Already logged in."
|
|
440
|
-
: "RemoteDraw login complete.",
|
|
678
|
+
result.alreadyLoggedIn ? t("login.already") : t("login.complete"),
|
|
441
679
|
accountSummary(result.account),
|
|
442
|
-
|
|
680
|
+
t("login.credential", { path: globalConfigPath(runtime) }),
|
|
443
681
|
].join("\n"));
|
|
444
682
|
}
|
|
445
683
|
async function logoutCommand(args, runtime) {
|
|
@@ -452,8 +690,8 @@ async function logoutCommand(args, runtime) {
|
|
|
452
690
|
const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
|
|
453
691
|
const result = await logoutFromRemoteDraw(runtime, apiBaseUrl);
|
|
454
692
|
return ok(result.hadCredential
|
|
455
|
-
?
|
|
456
|
-
:
|
|
693
|
+
? t("logout.done", { url: apiBaseUrl })
|
|
694
|
+
: t("logout.none", { url: apiBaseUrl }));
|
|
457
695
|
}
|
|
458
696
|
async function whoamiCommand(args, runtime) {
|
|
459
697
|
const parsed = parseArgs(args, {
|
|
@@ -465,15 +703,15 @@ async function whoamiCommand(args, runtime) {
|
|
|
465
703
|
const apiBaseUrl = remoteDrawApiBaseUrl(runtime.env, readString(parsed, "api-base-url"));
|
|
466
704
|
const account = await currentCliAccount(runtime, apiBaseUrl);
|
|
467
705
|
if (!account) {
|
|
468
|
-
return fail(
|
|
706
|
+
return fail(t("whoami.notLoggedIn", { url: apiBaseUrl }));
|
|
469
707
|
}
|
|
470
|
-
return ok([accountSummary(account),
|
|
708
|
+
return ok([accountSummary(account), t("whoami.api", { url: apiBaseUrl })].join("\n"));
|
|
471
709
|
}
|
|
472
710
|
function accountSummary(account) {
|
|
473
|
-
const identity = account.email || account.name || "
|
|
711
|
+
const identity = account.email || account.name || t("whoami.anonymous");
|
|
474
712
|
return `${identity} (${account.tenantSlug})`;
|
|
475
713
|
}
|
|
476
|
-
async function newCommand(args, runtime) {
|
|
714
|
+
async function newCommand(args, runtime, askLanguage = false) {
|
|
477
715
|
const parsed = parseArgs(args, {
|
|
478
716
|
values: [
|
|
479
717
|
"app-name",
|
|
@@ -491,8 +729,13 @@ async function newCommand(args, runtime) {
|
|
|
491
729
|
if (parsed.booleans.has("help"))
|
|
492
730
|
return ok(newHelpText());
|
|
493
731
|
const format = outputFormat(parsed);
|
|
494
|
-
const
|
|
495
|
-
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
|
|
496
739
|
? await promptForNewProject(parsed, runtime)
|
|
497
740
|
: planFromArgs(parsed, {
|
|
498
741
|
appNameRequired: true,
|
|
@@ -517,7 +760,7 @@ async function newCommand(args, runtime) {
|
|
|
517
760
|
}
|
|
518
761
|
return format === "json"
|
|
519
762
|
? jsonResult(scaffoldJsonResult("new", plan, outputDir, written, parsed, provisioning, offline, dryRun))
|
|
520
|
-
: ok(scaffoldSummary("
|
|
763
|
+
: ok(scaffoldSummary(t("scaffold.verb.created"), plan, outputDir, written, dryRun) +
|
|
521
764
|
cloudSetupSummary(provisioning, offline, dryRun));
|
|
522
765
|
}
|
|
523
766
|
export async function createRemoteDrawProject(plan, outputDir, runtime, options = {}) {
|
|
@@ -574,7 +817,7 @@ async function initCommand(args, runtime) {
|
|
|
574
817
|
}
|
|
575
818
|
return format === "json"
|
|
576
819
|
? jsonResult(scaffoldJsonResult("init", plan, outputDir, written, parsed, provisioning, offline, dryRun))
|
|
577
|
-
: ok(scaffoldSummary("
|
|
820
|
+
: ok(scaffoldSummary(t("scaffold.verb.initialized"), plan, outputDir, written, dryRun) +
|
|
578
821
|
cloudSetupSummary(provisioning, offline, dryRun));
|
|
579
822
|
}
|
|
580
823
|
function cloudSetupDisabled(runtime) {
|
|
@@ -596,16 +839,16 @@ function cloudSetupSummary(provisioning, offline, dryRun) {
|
|
|
596
839
|
if (dryRun)
|
|
597
840
|
return "";
|
|
598
841
|
if (offline) {
|
|
599
|
-
return
|
|
842
|
+
return `\n\n${t("cloud.skipped")}`;
|
|
600
843
|
}
|
|
601
844
|
if (!provisioning)
|
|
602
845
|
return "";
|
|
603
846
|
return [
|
|
604
847
|
"",
|
|
605
848
|
"",
|
|
606
|
-
|
|
607
|
-
"
|
|
608
|
-
|
|
849
|
+
t("cloud.dashboardProject", { url: provisioning.project.dashboardUrl }),
|
|
850
|
+
t("cloud.devKey"),
|
|
851
|
+
t("cloud.projectId", { id: provisioning.project.id }),
|
|
609
852
|
].join("\n");
|
|
610
853
|
}
|
|
611
854
|
const DOCTOR_PROBE_TIMEOUT_MS = 8_000;
|
|
@@ -614,7 +857,7 @@ function probeFailureMessage(error) {
|
|
|
614
857
|
}
|
|
615
858
|
async function probeApiHealth(runtime, apiBaseUrl) {
|
|
616
859
|
if (!runtime.fetch) {
|
|
617
|
-
return { ok: false, message: "
|
|
860
|
+
return { ok: false, message: t("doctor.probe.noNetwork") };
|
|
618
861
|
}
|
|
619
862
|
try {
|
|
620
863
|
const response = await runtime.fetch(`${apiBaseUrl}/health`, {
|
|
@@ -637,7 +880,7 @@ async function probeApiHealth(runtime, apiBaseUrl) {
|
|
|
637
880
|
*/
|
|
638
881
|
async function probeApiKey(runtime, apiBaseUrl, apiKey) {
|
|
639
882
|
if (!runtime.fetch) {
|
|
640
|
-
return { ok: false, message: "
|
|
883
|
+
return { ok: false, message: t("doctor.probe.noNetworkKey") };
|
|
641
884
|
}
|
|
642
885
|
try {
|
|
643
886
|
const response = await runtime.fetch(`${apiBaseUrl}/v1/sessions/list`, {
|
|
@@ -657,7 +900,7 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
|
|
|
657
900
|
catch {
|
|
658
901
|
return {
|
|
659
902
|
ok: false,
|
|
660
|
-
message:
|
|
903
|
+
message: t("doctor.probe.nonJson", { status: response.status }),
|
|
661
904
|
};
|
|
662
905
|
}
|
|
663
906
|
if (!response.ok) {
|
|
@@ -667,7 +910,7 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
|
|
|
667
910
|
typeof payload.error?.message ===
|
|
668
911
|
"string"
|
|
669
912
|
? payload.error.message
|
|
670
|
-
:
|
|
913
|
+
: t("doctor.probe.rejected", { status: response.status });
|
|
671
914
|
return { ok: false, message };
|
|
672
915
|
}
|
|
673
916
|
const items = typeof payload === "object" && payload !== null && "items" in payload
|
|
@@ -679,9 +922,111 @@ async function probeApiKey(runtime, apiBaseUrl, apiKey) {
|
|
|
679
922
|
return { ok: false, message: probeFailureMessage(error) };
|
|
680
923
|
}
|
|
681
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* npm never refreshes a globally installed binary on its own, so staying
|
|
927
|
+
* current is an explicit step. This command makes that step one word.
|
|
928
|
+
*/
|
|
929
|
+
async function updateCommand(args, runtime) {
|
|
930
|
+
const parsed = parseArgs(args, {
|
|
931
|
+
values: ["format"],
|
|
932
|
+
booleans: ["check", "help"],
|
|
933
|
+
});
|
|
934
|
+
if (parsed.booleans.has("help"))
|
|
935
|
+
return ok(updateHelpText());
|
|
936
|
+
const format = outputFormat(parsed);
|
|
937
|
+
const checkOnly = parsed.booleans.has("check");
|
|
938
|
+
const method = detectInstallMethod(runtime.binPath ?? "");
|
|
939
|
+
const command = installMethodDisplay(method);
|
|
940
|
+
// `remotedraw update` is an explicit request, so it ignores the cache and the
|
|
941
|
+
// ambient opt-outs that only silence the passive notice.
|
|
942
|
+
const check = await checkForUpdate(runtime, CLI_VERSION, { force: true });
|
|
943
|
+
if (!check) {
|
|
944
|
+
const message = t("update.unreachable", {
|
|
945
|
+
package: CLI_PACKAGE_NAME,
|
|
946
|
+
command,
|
|
947
|
+
});
|
|
948
|
+
return format === "json"
|
|
949
|
+
? jsonFailure("update", new Error(message))
|
|
950
|
+
: fail(message);
|
|
951
|
+
}
|
|
952
|
+
if (!check.updateAvailable) {
|
|
953
|
+
const message = t("update.alreadyLatest", {
|
|
954
|
+
package: CLI_PACKAGE_NAME,
|
|
955
|
+
version: check.current,
|
|
956
|
+
});
|
|
957
|
+
return format === "json"
|
|
958
|
+
? jsonResult({ ok: true, command: "update", ...check, updated: false })
|
|
959
|
+
: ok(message);
|
|
960
|
+
}
|
|
961
|
+
if (checkOnly || method.ephemeral) {
|
|
962
|
+
const message = method.ephemeral
|
|
963
|
+
? [
|
|
964
|
+
t("update.available", {
|
|
965
|
+
current: check.current,
|
|
966
|
+
latest: check.latest,
|
|
967
|
+
}),
|
|
968
|
+
t("update.ephemeral", { package: CLI_PACKAGE_NAME }),
|
|
969
|
+
].join("\n")
|
|
970
|
+
: [
|
|
971
|
+
t("update.available", {
|
|
972
|
+
current: check.current,
|
|
973
|
+
latest: check.latest,
|
|
974
|
+
}),
|
|
975
|
+
t("update.runToInstall", { command }),
|
|
976
|
+
].join("\n");
|
|
977
|
+
return format === "json"
|
|
978
|
+
? jsonResult({
|
|
979
|
+
ok: true,
|
|
980
|
+
command: "update",
|
|
981
|
+
...check,
|
|
982
|
+
updated: false,
|
|
983
|
+
installCommand: command,
|
|
984
|
+
ephemeral: method.ephemeral,
|
|
985
|
+
})
|
|
986
|
+
: ok(message);
|
|
987
|
+
}
|
|
988
|
+
const install = await runInstall(runtime, method);
|
|
989
|
+
if (!install.ran) {
|
|
990
|
+
const message = t("update.availableRun", {
|
|
991
|
+
current: check.current,
|
|
992
|
+
latest: check.latest,
|
|
993
|
+
command,
|
|
994
|
+
});
|
|
995
|
+
return format === "json"
|
|
996
|
+
? jsonResult({
|
|
997
|
+
ok: true,
|
|
998
|
+
command: "update",
|
|
999
|
+
...check,
|
|
1000
|
+
updated: false,
|
|
1001
|
+
installCommand: command,
|
|
1002
|
+
})
|
|
1003
|
+
: ok(message);
|
|
1004
|
+
}
|
|
1005
|
+
if (install.exitCode !== 0) {
|
|
1006
|
+
const message = t("update.failed", {
|
|
1007
|
+
command,
|
|
1008
|
+
code: install.exitCode,
|
|
1009
|
+
});
|
|
1010
|
+
return format === "json"
|
|
1011
|
+
? jsonFailure("update", new Error(message))
|
|
1012
|
+
: fail(message);
|
|
1013
|
+
}
|
|
1014
|
+
return format === "json"
|
|
1015
|
+
? jsonResult({
|
|
1016
|
+
ok: true,
|
|
1017
|
+
command: "update",
|
|
1018
|
+
...check,
|
|
1019
|
+
updated: true,
|
|
1020
|
+
installCommand: command,
|
|
1021
|
+
})
|
|
1022
|
+
: ok(t("update.done", {
|
|
1023
|
+
package: CLI_PACKAGE_NAME,
|
|
1024
|
+
version: check.latest,
|
|
1025
|
+
}));
|
|
1026
|
+
}
|
|
682
1027
|
async function doctorCommand(args, runtime) {
|
|
683
1028
|
const parsed = parseArgs(args, {
|
|
684
|
-
values: ["path", "format"],
|
|
1029
|
+
values: ["path", "format", "api-base-url"],
|
|
685
1030
|
booleans: ["help", "offline"],
|
|
686
1031
|
});
|
|
687
1032
|
if (parsed.booleans.has("help"))
|
|
@@ -693,67 +1038,89 @@ async function doctorCommand(args, runtime) {
|
|
|
693
1038
|
const projectEnv = await environmentForProject(dir, runtime);
|
|
694
1039
|
const configPath = path.join(dir, "remotedraw.config.json");
|
|
695
1040
|
const packagePath = path.join(dir, "package.json");
|
|
696
|
-
const
|
|
697
|
-
const normalizedEnvApiBaseUrl =
|
|
698
|
-
? undefined
|
|
699
|
-
: normalizedOrigin(envApiBaseUrl);
|
|
1041
|
+
const apiBaseUrl = resolveDoctorApiBaseUrl(projectEnv, readString(parsed, "api-base-url"));
|
|
1042
|
+
const normalizedEnvApiBaseUrl = apiBaseUrl.state === "ok" ? apiBaseUrl.url : undefined;
|
|
700
1043
|
const envApiKey = projectEnv.REMOTEDRAW_API_KEY;
|
|
701
|
-
const lines = ["
|
|
1044
|
+
const lines = [t("doctor.title"), ""];
|
|
702
1045
|
const checks = [];
|
|
703
1046
|
const addCheck = (state, label, message) => {
|
|
704
1047
|
checks.push({ state, label, message });
|
|
705
1048
|
lines.push(statusLine(state, label, message));
|
|
706
1049
|
};
|
|
707
1050
|
const hasConfig = await runtime.exists(configPath);
|
|
708
|
-
addCheck(hasConfig ? "ok" : "warn", "remotedraw.config.json", hasConfig
|
|
709
|
-
? "Project is linked to a local integration profile."
|
|
710
|
-
: "Run remotedraw init to create one.");
|
|
1051
|
+
addCheck(hasConfig ? "ok" : "warn", "remotedraw.config.json", hasConfig ? t("doctor.config.ok") : t("doctor.config.missing"));
|
|
711
1052
|
const packageJson = (await runtime.exists(packagePath))
|
|
712
1053
|
? await readJsonFile(packagePath, runtime)
|
|
713
1054
|
: null;
|
|
714
|
-
addCheck(packageJson ? "ok" : "warn", "package.json", packageJson
|
|
715
|
-
? "Package metadata found."
|
|
716
|
-
: "No package.json found in this directory.");
|
|
1055
|
+
addCheck(packageJson ? "ok" : "warn", "package.json", packageJson ? t("doctor.package.ok") : t("doctor.package.missing"));
|
|
717
1056
|
const config = hasConfig ? await readJsonFile(configPath, runtime) : null;
|
|
718
1057
|
const sdk = stringFromRecord(config, "sdk");
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
addCheck(apiBaseUrl.state === "ok"
|
|
1069
|
+
? "ok"
|
|
1070
|
+
: apiBaseUrl.state === "placeholder"
|
|
1071
|
+
? "warn"
|
|
1072
|
+
: "error", "REMOTEDRAW_API_BASE_URL", doctorApiBaseUrlMessage(apiBaseUrl));
|
|
727
1073
|
addCheck(envApiKey?.startsWith("rd_sk_") ? "ok" : "warn", "REMOTEDRAW_API_KEY", envApiKey?.startsWith("rd_sk_")
|
|
728
|
-
? "
|
|
729
|
-
: "
|
|
1074
|
+
? t("doctor.apiKey.ok")
|
|
1075
|
+
: t("doctor.apiKey.missing"));
|
|
730
1076
|
// The checks above only prove the strings look right. A revoked key, a URL
|
|
731
1077
|
// pointing at the wrong deployment, and an exhausted credit balance all pass
|
|
732
1078
|
// them, so doctor also asks the deployment itself.
|
|
733
1079
|
if (offline) {
|
|
734
|
-
addCheck("warn", "deployment", "
|
|
1080
|
+
addCheck("warn", t("doctor.label.deployment"), t("doctor.network.offline"));
|
|
735
1081
|
}
|
|
736
1082
|
else if (!normalizedEnvApiBaseUrl) {
|
|
737
|
-
addCheck("warn", "deployment", "
|
|
1083
|
+
addCheck("warn", t("doctor.label.deployment"), t("doctor.network.noOrigin"));
|
|
738
1084
|
}
|
|
739
1085
|
else {
|
|
740
1086
|
const health = await probeApiHealth(runtime, normalizedEnvApiBaseUrl);
|
|
741
|
-
addCheck(health.ok ? "ok" : "error", "
|
|
742
|
-
?
|
|
743
|
-
:
|
|
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
|
+
}));
|
|
744
1093
|
if (!envApiKey?.startsWith("rd_sk_")) {
|
|
745
|
-
addCheck("warn", "
|
|
1094
|
+
addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoKey"));
|
|
746
1095
|
}
|
|
747
1096
|
else if (!health.ok) {
|
|
748
|
-
addCheck("warn", "
|
|
1097
|
+
addCheck("warn", t("doctor.label.apiKeyAccepted"), t("doctor.key.skippedNoHealth"));
|
|
749
1098
|
}
|
|
750
1099
|
else {
|
|
751
1100
|
const probe = await probeApiKey(runtime, normalizedEnvApiBaseUrl, envApiKey);
|
|
752
|
-
addCheck(probe.ok ? "ok" : "error", "
|
|
753
|
-
?
|
|
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 })
|
|
754
1105
|
: probe.message);
|
|
755
1106
|
}
|
|
756
1107
|
}
|
|
1108
|
+
// A stale global install is the quietest failure mode there is: npm pins the
|
|
1109
|
+
// binary at install time and never revisits it, so doctor has to say so.
|
|
1110
|
+
if (offline) {
|
|
1111
|
+
addCheck("warn", t("doctor.label.cliVersion"), t("doctor.version.offline", { version: CLI_VERSION }));
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
const update = await checkForUpdate(runtime, CLI_VERSION, { force: true });
|
|
1115
|
+
addCheck(!update || !update.updateAvailable ? "ok" : "warn", t("doctor.label.cliVersion"), !update
|
|
1116
|
+
? t("doctor.version.unknown", { version: CLI_VERSION })
|
|
1117
|
+
: update.updateAvailable
|
|
1118
|
+
? t("doctor.version.stale", {
|
|
1119
|
+
current: update.current,
|
|
1120
|
+
latest: update.latest,
|
|
1121
|
+
})
|
|
1122
|
+
: t("doctor.version.latest", { current: update.current }));
|
|
1123
|
+
}
|
|
757
1124
|
return format === "json"
|
|
758
1125
|
? jsonResult({
|
|
759
1126
|
ok: true,
|
|
@@ -775,23 +1142,22 @@ async function devCommand(args, runtime) {
|
|
|
775
1142
|
const projectEnv = await environmentForProject(dir, runtime);
|
|
776
1143
|
const apiBaseUrl = readString(parsed, "api-base-url") ??
|
|
777
1144
|
projectEnv.REMOTEDRAW_API_BASE_URL ??
|
|
778
|
-
|
|
1145
|
+
DEFAULT_REMOTEDRAW_API_BASE_URL;
|
|
779
1146
|
return ok([
|
|
780
|
-
"
|
|
1147
|
+
t("dev.title"),
|
|
781
1148
|
"",
|
|
782
|
-
|
|
1149
|
+
t("dev.apiBaseUrl", { url: apiBaseUrl }),
|
|
783
1150
|
"",
|
|
784
|
-
"
|
|
785
|
-
" Inside this repository: bun run dev:codex",
|
|
1151
|
+
t("dev.hosted"),
|
|
786
1152
|
"",
|
|
787
|
-
"
|
|
788
|
-
"
|
|
1153
|
+
t("dev.step1"),
|
|
1154
|
+
t("dev.step1.detail"),
|
|
789
1155
|
"",
|
|
790
|
-
"
|
|
1156
|
+
t("dev.step2"),
|
|
791
1157
|
` remotedraw create-input --preset signature --api-base-url ${apiBaseUrl}`,
|
|
792
1158
|
"",
|
|
793
|
-
"
|
|
794
|
-
"
|
|
1159
|
+
t("dev.step3"),
|
|
1160
|
+
t("dev.step3.detail"),
|
|
795
1161
|
].join("\n"));
|
|
796
1162
|
}
|
|
797
1163
|
async function createInputCommand(args, runtime) {
|
|
@@ -812,7 +1178,7 @@ async function createInputCommand(args, runtime) {
|
|
|
812
1178
|
const preset = choice(readString(parsed, "preset") ?? "signature", presetChoices, "preset");
|
|
813
1179
|
const apiBaseUrl = readString(parsed, "api-base-url") ??
|
|
814
1180
|
projectEnv.REMOTEDRAW_API_BASE_URL ??
|
|
815
|
-
|
|
1181
|
+
DEFAULT_REMOTEDRAW_API_BASE_URL;
|
|
816
1182
|
const payload = createSessionPayload({
|
|
817
1183
|
preset,
|
|
818
1184
|
label: readString(parsed, "label") ?? labelForPreset(preset),
|
|
@@ -822,9 +1188,9 @@ async function createInputCommand(args, runtime) {
|
|
|
822
1188
|
if (parsed.booleans.has("execute")) {
|
|
823
1189
|
const apiKey = readString(parsed, "api-key") ?? projectEnv.REMOTEDRAW_API_KEY;
|
|
824
1190
|
if (!runtime.fetch)
|
|
825
|
-
throw new Error("
|
|
1191
|
+
throw new Error(t("createInput.error.noFetch"));
|
|
826
1192
|
if (!apiKey?.startsWith("rd_sk_")) {
|
|
827
|
-
throw new Error("
|
|
1193
|
+
throw new Error(t("createInput.error.noKey"));
|
|
828
1194
|
}
|
|
829
1195
|
const response = await runtime.fetch(`${normalizedOrigin(apiBaseUrl)}/v1/sessions`, {
|
|
830
1196
|
method: "POST",
|
|
@@ -836,20 +1202,20 @@ async function createInputCommand(args, runtime) {
|
|
|
836
1202
|
});
|
|
837
1203
|
const text = await response.text();
|
|
838
1204
|
if (!response.ok)
|
|
839
|
-
throw new Error(text ||
|
|
1205
|
+
throw new Error(text || t("createInput.error.apiStatus", { status: response.status }));
|
|
840
1206
|
return ok(text);
|
|
841
1207
|
}
|
|
842
1208
|
if (parsed.booleans.has("json")) {
|
|
843
1209
|
return ok(JSON.stringify(payload, null, 2));
|
|
844
1210
|
}
|
|
845
1211
|
return ok([
|
|
846
|
-
"
|
|
1212
|
+
t("createInput.title"),
|
|
847
1213
|
"",
|
|
848
|
-
"
|
|
1214
|
+
t("createInput.warning"),
|
|
849
1215
|
"",
|
|
850
1216
|
curlForCreateInput(apiBaseUrl, payload),
|
|
851
1217
|
"",
|
|
852
|
-
"
|
|
1218
|
+
t("createInput.footer"),
|
|
853
1219
|
].join("\n"));
|
|
854
1220
|
}
|
|
855
1221
|
async function examplesCommand(args, runtime) {
|
|
@@ -862,14 +1228,14 @@ async function examplesCommand(args, runtime) {
|
|
|
862
1228
|
const example = readString(parsed, "install");
|
|
863
1229
|
if (example == null || parsed.booleans.has("list")) {
|
|
864
1230
|
return ok([
|
|
865
|
-
"
|
|
1231
|
+
t("examples.title"),
|
|
866
1232
|
"",
|
|
867
|
-
"
|
|
868
|
-
"
|
|
869
|
-
"
|
|
870
|
-
"
|
|
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")}`,
|
|
871
1237
|
"",
|
|
872
|
-
"
|
|
1238
|
+
t("examples.installOne"),
|
|
873
1239
|
" remotedraw examples --install react-ios --path ./remotedraw-react-ios",
|
|
874
1240
|
].join("\n"));
|
|
875
1241
|
}
|
|
@@ -887,7 +1253,7 @@ async function examplesCommand(args, runtime) {
|
|
|
887
1253
|
dryRun,
|
|
888
1254
|
force: parsed.booleans.has("force"),
|
|
889
1255
|
});
|
|
890
|
-
return ok(scaffoldSummary("
|
|
1256
|
+
return ok(scaffoldSummary(t("scaffold.verb.installedExample"), plan, outputDir, written, dryRun));
|
|
891
1257
|
}
|
|
892
1258
|
async function agentCommand(args, runtime) {
|
|
893
1259
|
const parsed = parseArgs(args, {
|
|
@@ -901,15 +1267,18 @@ async function agentCommand(args, runtime) {
|
|
|
901
1267
|
const targetDir = readString(parsed, "path");
|
|
902
1268
|
if (targetDir == null) {
|
|
903
1269
|
return ok([
|
|
904
|
-
"
|
|
1270
|
+
t("agent.title"),
|
|
905
1271
|
"",
|
|
906
|
-
"
|
|
1272
|
+
t("agent.intro"),
|
|
907
1273
|
"",
|
|
908
|
-
"
|
|
1274
|
+
t("agent.printSkill"),
|
|
909
1275
|
" remotedraw agent --print-skill",
|
|
910
1276
|
"",
|
|
911
|
-
"
|
|
912
|
-
|
|
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",
|
|
913
1282
|
].join("\n"));
|
|
914
1283
|
}
|
|
915
1284
|
const outputDir = resolveAgentPath(targetDir, runtime);
|
|
@@ -917,7 +1286,7 @@ async function agentCommand(args, runtime) {
|
|
|
917
1286
|
if (!parsed.booleans.has("dry-run") &&
|
|
918
1287
|
!parsed.booleans.has("force") &&
|
|
919
1288
|
(await runtime.exists(outputFile))) {
|
|
920
|
-
throw new Error("
|
|
1289
|
+
throw new Error(t("agent.error.exists"));
|
|
921
1290
|
}
|
|
922
1291
|
if (!parsed.booleans.has("dry-run")) {
|
|
923
1292
|
await runtime.mkdir(outputDir);
|
|
@@ -925,127 +1294,150 @@ async function agentCommand(args, runtime) {
|
|
|
925
1294
|
}
|
|
926
1295
|
return ok([
|
|
927
1296
|
parsed.booleans.has("dry-run")
|
|
928
|
-
?
|
|
929
|
-
:
|
|
1297
|
+
? t("agent.wouldWrite", { path: outputFile })
|
|
1298
|
+
: t("agent.installed", { path: outputFile }),
|
|
930
1299
|
"",
|
|
931
|
-
"
|
|
932
|
-
"
|
|
1300
|
+
t("word.next"),
|
|
1301
|
+
t("agent.next"),
|
|
933
1302
|
].join("\n"));
|
|
934
1303
|
}
|
|
935
1304
|
function newHelpText() {
|
|
1305
|
+
const option = (flag, description) => ` ${flag.padEnd(25)} ${description}`;
|
|
936
1306
|
return [
|
|
937
|
-
"
|
|
938
|
-
"
|
|
939
|
-
"
|
|
940
|
-
"",
|
|
941
|
-
"
|
|
942
|
-
"",
|
|
943
|
-
"
|
|
944
|
-
"
|
|
945
|
-
"
|
|
946
|
-
"
|
|
947
|
-
"
|
|
948
|
-
"
|
|
949
|
-
"
|
|
950
|
-
"
|
|
951
|
-
"
|
|
952
|
-
"
|
|
953
|
-
"
|
|
954
|
-
"
|
|
955
|
-
"
|
|
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")),
|
|
956
1327
|
].join("\n");
|
|
957
1328
|
}
|
|
958
1329
|
function initHelpText() {
|
|
959
1330
|
return [
|
|
960
|
-
"
|
|
961
|
-
"
|
|
1331
|
+
t("word.usage"),
|
|
1332
|
+
t("init.help.usage"),
|
|
962
1333
|
"",
|
|
963
|
-
"
|
|
1334
|
+
t("init.help.body"),
|
|
964
1335
|
].join("\n");
|
|
965
1336
|
}
|
|
966
|
-
function
|
|
1337
|
+
function languageHelpText() {
|
|
967
1338
|
return [
|
|
968
|
-
"
|
|
1339
|
+
t("language.help.title"),
|
|
1340
|
+
"",
|
|
1341
|
+
t("word.usage"),
|
|
1342
|
+
t("language.help.usage.list"),
|
|
1343
|
+
t("language.help.usage.set"),
|
|
969
1344
|
"",
|
|
970
|
-
"
|
|
971
|
-
"
|
|
1345
|
+
t("language.help.body"),
|
|
1346
|
+
t("language.help.override"),
|
|
972
1347
|
"",
|
|
973
|
-
"
|
|
1348
|
+
t("language.available"),
|
|
1349
|
+
...SUPPORTED_LOCALES.map((locale) => ` ${locale.padEnd(6)} ${localeEndonym(locale)}`),
|
|
974
1350
|
].join("\n");
|
|
975
1351
|
}
|
|
976
|
-
function
|
|
1352
|
+
function loginHelpText() {
|
|
977
1353
|
return [
|
|
978
|
-
"
|
|
1354
|
+
t("login.help.title"),
|
|
979
1355
|
"",
|
|
980
|
-
"
|
|
981
|
-
"
|
|
1356
|
+
t("word.usage"),
|
|
1357
|
+
t("login.help.usage"),
|
|
1358
|
+
"",
|
|
1359
|
+
t("login.help.body"),
|
|
982
1360
|
].join("\n");
|
|
983
1361
|
}
|
|
1362
|
+
function logoutHelpText() {
|
|
1363
|
+
return [t("logout.help.title"), "", t("word.usage"), t("logout.help.usage")].join("\n");
|
|
1364
|
+
}
|
|
984
1365
|
function whoamiHelpText() {
|
|
1366
|
+
return [t("whoami.help.title"), "", t("word.usage"), t("whoami.help.usage")].join("\n");
|
|
1367
|
+
}
|
|
1368
|
+
function doctorHelpText() {
|
|
985
1369
|
return [
|
|
986
|
-
"
|
|
987
|
-
"",
|
|
988
|
-
"
|
|
989
|
-
"
|
|
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"),
|
|
990
1378
|
].join("\n");
|
|
991
1379
|
}
|
|
992
|
-
function
|
|
1380
|
+
function updateHelpText() {
|
|
993
1381
|
return [
|
|
994
|
-
"
|
|
995
|
-
"
|
|
1382
|
+
t("word.usage"),
|
|
1383
|
+
t("update.help.usage"),
|
|
996
1384
|
"",
|
|
997
|
-
"
|
|
998
|
-
"
|
|
999
|
-
"
|
|
1385
|
+
t("update.help.body", { package: CLI_PACKAGE_NAME }),
|
|
1386
|
+
t("update.help.global"),
|
|
1387
|
+
t("update.help.check"),
|
|
1388
|
+
"",
|
|
1389
|
+
t("update.help.silence"),
|
|
1000
1390
|
].join("\n");
|
|
1001
1391
|
}
|
|
1002
1392
|
function optionsHelpText() {
|
|
1003
1393
|
return [
|
|
1004
|
-
"
|
|
1394
|
+
t("word.usage"),
|
|
1005
1395
|
" remotedraw options [--format text|json]",
|
|
1006
1396
|
"",
|
|
1007
|
-
"
|
|
1397
|
+
t("options.help.body"),
|
|
1008
1398
|
].join("\n");
|
|
1009
1399
|
}
|
|
1010
1400
|
function devHelpText() {
|
|
1011
1401
|
return [
|
|
1012
|
-
"
|
|
1013
|
-
"
|
|
1402
|
+
t("word.usage"),
|
|
1403
|
+
t("dev.help.usage"),
|
|
1014
1404
|
"",
|
|
1015
|
-
"
|
|
1405
|
+
t("dev.help.body"),
|
|
1016
1406
|
].join("\n");
|
|
1017
1407
|
}
|
|
1018
1408
|
function createInputHelpText() {
|
|
1409
|
+
const option = (flag, description) => ` ${flag.padEnd(21)} ${description}`;
|
|
1019
1410
|
return [
|
|
1020
|
-
"
|
|
1021
|
-
"
|
|
1022
|
-
"",
|
|
1023
|
-
"
|
|
1024
|
-
"
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
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")),
|
|
1030
1421
|
].join("\n");
|
|
1031
1422
|
}
|
|
1032
1423
|
function examplesHelpText() {
|
|
1033
1424
|
return [
|
|
1034
|
-
"
|
|
1035
|
-
"
|
|
1036
|
-
"
|
|
1425
|
+
t("word.usage"),
|
|
1426
|
+
t("examples.help.usage.list"),
|
|
1427
|
+
t("examples.help.usage.install"),
|
|
1037
1428
|
"",
|
|
1038
|
-
|
|
1429
|
+
t("word.examples", { list: exampleChoices.join(", ") }),
|
|
1039
1430
|
].join("\n");
|
|
1040
1431
|
}
|
|
1041
1432
|
function agentHelpText() {
|
|
1042
1433
|
return [
|
|
1043
|
-
"
|
|
1044
|
-
"
|
|
1045
|
-
"
|
|
1046
|
-
"
|
|
1434
|
+
t("word.usage"),
|
|
1435
|
+
t("agent.help.usage.bare"),
|
|
1436
|
+
t("agent.help.usage.print"),
|
|
1437
|
+
t("agent.help.usage.install"),
|
|
1047
1438
|
"",
|
|
1048
|
-
"
|
|
1439
|
+
t("agent.help.body"),
|
|
1440
|
+
t("agent.help.paths"),
|
|
1049
1441
|
].join("\n");
|
|
1050
1442
|
}
|
|
1051
1443
|
function parseArgs(args, allowed) {
|
|
@@ -1068,17 +1460,17 @@ function parseArgs(args, allowed) {
|
|
|
1068
1460
|
const inlineValue = equalsIndex === -1 ? undefined : withoutPrefix.slice(equalsIndex + 1);
|
|
1069
1461
|
if (booleanFlags.has(name)) {
|
|
1070
1462
|
if (inlineValue != null) {
|
|
1071
|
-
throw
|
|
1463
|
+
throw cliError("INVALID_ARGUMENT", "error.optionNoValue", { name });
|
|
1072
1464
|
}
|
|
1073
1465
|
booleans.add(name);
|
|
1074
1466
|
continue;
|
|
1075
1467
|
}
|
|
1076
1468
|
if (!valueFlags.has(name)) {
|
|
1077
|
-
throw
|
|
1469
|
+
throw cliError("INVALID_ARGUMENT", "error.unsupportedOption", { name });
|
|
1078
1470
|
}
|
|
1079
1471
|
const value = inlineValue ?? args[index + 1];
|
|
1080
1472
|
if (value == null || value.startsWith("--")) {
|
|
1081
|
-
throw
|
|
1473
|
+
throw cliError("MISSING_ARGUMENT", "error.optionNeedsValue", { name });
|
|
1082
1474
|
}
|
|
1083
1475
|
values.set(name, value);
|
|
1084
1476
|
if (inlineValue == null)
|
|
@@ -1095,7 +1487,7 @@ function optionalInteger(value) {
|
|
|
1095
1487
|
return undefined;
|
|
1096
1488
|
const number = Number(value);
|
|
1097
1489
|
if (!Number.isInteger(number) || number <= 0) {
|
|
1098
|
-
throw
|
|
1490
|
+
throw cliError("INVALID_ARGUMENT", "error.expiresInMs");
|
|
1099
1491
|
}
|
|
1100
1492
|
return number;
|
|
1101
1493
|
}
|
|
@@ -1112,16 +1504,16 @@ async function promptForNewProject(parsed, runtime) {
|
|
|
1112
1504
|
defaultAppName: "RemoteDraw app",
|
|
1113
1505
|
});
|
|
1114
1506
|
}
|
|
1115
|
-
await prompts.intro?.("
|
|
1507
|
+
await prompts.intro?.(t("prompt.intro.setup"));
|
|
1116
1508
|
const appName = readString(parsed, "app-name") ??
|
|
1117
1509
|
(await prompts.text({
|
|
1118
|
-
message: "
|
|
1510
|
+
message: t("field.appName"),
|
|
1119
1511
|
defaultValue: "RemoteDraw App",
|
|
1120
1512
|
validate: validateProjectName,
|
|
1121
1513
|
}));
|
|
1122
1514
|
const target = readString(parsed, "target") == null
|
|
1123
1515
|
? await prompts.select({
|
|
1124
|
-
message: "
|
|
1516
|
+
message: t("field.target"),
|
|
1125
1517
|
defaultValue: "web",
|
|
1126
1518
|
choices: targetPromptChoices(),
|
|
1127
1519
|
})
|
|
@@ -1134,25 +1526,19 @@ async function promptForNewProject(parsed, runtime) {
|
|
|
1134
1526
|
: choice(readString(parsed, "sdk"), sdkChoices, "sdk");
|
|
1135
1527
|
const preset = readString(parsed, "preset") == null
|
|
1136
1528
|
? await prompts.select({
|
|
1137
|
-
message: "
|
|
1529
|
+
message: t("field.preset"),
|
|
1138
1530
|
defaultValue: "signature",
|
|
1139
1531
|
choices: presetPromptChoices(),
|
|
1140
1532
|
})
|
|
1141
1533
|
: choice(readString(parsed, "preset"), presetChoices, "preset");
|
|
1142
1534
|
const packageManager = readString(parsed, "package-manager") == null
|
|
1143
1535
|
? await prompts.select({
|
|
1144
|
-
message: "
|
|
1536
|
+
message: t("field.packageManager"),
|
|
1145
1537
|
defaultValue: "npm",
|
|
1146
1538
|
choices: packageManagerPromptChoices(),
|
|
1147
1539
|
})
|
|
1148
1540
|
: choice(readString(parsed, "package-manager"), packageManagerChoices, "package-manager");
|
|
1149
|
-
const apiBaseUrl = readString(parsed, "api-base-url") ??
|
|
1150
|
-
apiBaseUrlForEndpoint(await prompts.select({
|
|
1151
|
-
message: "API endpoint",
|
|
1152
|
-
helperText: "Use the placeholder unless you are wiring local dev now.",
|
|
1153
|
-
defaultValue: "deployment",
|
|
1154
|
-
choices: apiEndpointPromptChoices(),
|
|
1155
|
-
}));
|
|
1541
|
+
const apiBaseUrl = readString(parsed, "api-base-url") ?? DEFAULT_REMOTEDRAW_API_BASE_URL;
|
|
1156
1542
|
validatePlan({ target, sender, sdk });
|
|
1157
1543
|
return {
|
|
1158
1544
|
appName,
|
|
@@ -1166,13 +1552,13 @@ async function promptForNewProject(parsed, runtime) {
|
|
|
1166
1552
|
};
|
|
1167
1553
|
}
|
|
1168
1554
|
function validateProjectName(value) {
|
|
1169
|
-
return value.trim() === "" ? "
|
|
1555
|
+
return value.trim() === "" ? t("field.appName.error") : undefined;
|
|
1170
1556
|
}
|
|
1171
1557
|
async function promptForSender(prompts, target) {
|
|
1172
1558
|
const choices = senderPromptChoices(target);
|
|
1173
1559
|
return await prompts.select({
|
|
1174
|
-
message: "
|
|
1175
|
-
helperText: "
|
|
1560
|
+
message: t("field.sender"),
|
|
1561
|
+
helperText: t("field.sender.helper"),
|
|
1176
1562
|
defaultValue: defaultPromptValue(defaultSenderForTarget(target), choices),
|
|
1177
1563
|
choices,
|
|
1178
1564
|
});
|
|
@@ -1180,8 +1566,8 @@ async function promptForSender(prompts, target) {
|
|
|
1180
1566
|
async function promptForSdk(prompts, target, sender) {
|
|
1181
1567
|
const choices = sdkPromptChoices(target, sender);
|
|
1182
1568
|
return await prompts.select({
|
|
1183
|
-
message: "
|
|
1184
|
-
helperText: "
|
|
1569
|
+
message: t("field.sdk"),
|
|
1570
|
+
helperText: t("field.sdk.helper"),
|
|
1185
1571
|
defaultValue: defaultPromptValue(defaultSdkForChoices(target, sender), choices),
|
|
1186
1572
|
choices,
|
|
1187
1573
|
});
|
|
@@ -1193,10 +1579,10 @@ function defaultPromptValue(preferred, choices) {
|
|
|
1193
1579
|
}
|
|
1194
1580
|
function targetPromptChoices() {
|
|
1195
1581
|
return [
|
|
1196
|
-
{ value: "web", label: "
|
|
1197
|
-
{ value: "desktop", label: "
|
|
1198
|
-
{ value: "ios", label: "
|
|
1199
|
-
{ 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") },
|
|
1200
1586
|
];
|
|
1201
1587
|
}
|
|
1202
1588
|
const DOCS_INTEGRATION_PATHS = "https://docs.remotedraw.com/docs#paths";
|
|
@@ -1204,103 +1590,112 @@ const DOCS_SDKS = "https://docs.remotedraw.com/docs/sdks#components";
|
|
|
1204
1590
|
const DOCS_PAYLOADS = "https://docs.remotedraw.com/docs/api#payloads";
|
|
1205
1591
|
const DOCS_ENDPOINTS = "https://docs.remotedraw.com/docs/api#endpoints";
|
|
1206
1592
|
const DOCS_AUTH = "https://docs.remotedraw.com/docs/api#auth";
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
+
}
|
|
1233
1624
|
function wizardChoices(choices, details) {
|
|
1234
|
-
return
|
|
1625
|
+
return choices.map((choiceOption) => ({
|
|
1235
1626
|
...choiceOption,
|
|
1236
1627
|
details: details[choiceOption.value],
|
|
1237
1628
|
}));
|
|
1238
1629
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
+
}
|
|
1297
1692
|
function senderPromptChoices(target) {
|
|
1298
1693
|
if (target === "ios") {
|
|
1299
1694
|
return [
|
|
1300
1695
|
{
|
|
1301
1696
|
value: "own-ios",
|
|
1302
|
-
label: "
|
|
1303
|
-
hint: "
|
|
1697
|
+
label: t("choice.sender.ownIos.label"),
|
|
1698
|
+
hint: t("choice.sender.ownIos.hint"),
|
|
1304
1699
|
},
|
|
1305
1700
|
];
|
|
1306
1701
|
}
|
|
@@ -1308,8 +1703,8 @@ function senderPromptChoices(target) {
|
|
|
1308
1703
|
return [
|
|
1309
1704
|
{
|
|
1310
1705
|
value: "headless",
|
|
1311
|
-
label: "
|
|
1312
|
-
hint: "
|
|
1706
|
+
label: t("choice.sender.headless.label"),
|
|
1707
|
+
hint: t("choice.sender.headless.hint.noUi"),
|
|
1313
1708
|
},
|
|
1314
1709
|
];
|
|
1315
1710
|
}
|
|
@@ -1317,26 +1712,26 @@ function senderPromptChoices(target) {
|
|
|
1317
1712
|
return [
|
|
1318
1713
|
{
|
|
1319
1714
|
value: "remotedraw-ios",
|
|
1320
|
-
label: "
|
|
1321
|
-
hint: "
|
|
1715
|
+
label: t("choice.sender.remotedrawIos.label"),
|
|
1716
|
+
hint: t("choice.sender.remotedrawIos.hint.joinUrl"),
|
|
1322
1717
|
},
|
|
1323
1718
|
{
|
|
1324
1719
|
value: "headless",
|
|
1325
|
-
label: "
|
|
1326
|
-
hint: "
|
|
1720
|
+
label: t("choice.sender.headless.label"),
|
|
1721
|
+
hint: t("choice.sender.headless.hint.httpRoutes"),
|
|
1327
1722
|
},
|
|
1328
1723
|
];
|
|
1329
1724
|
}
|
|
1330
1725
|
return [
|
|
1331
1726
|
{
|
|
1332
1727
|
value: "remotedraw-ios",
|
|
1333
|
-
label: "
|
|
1334
|
-
hint: "
|
|
1728
|
+
label: t("choice.sender.remotedrawIos.label"),
|
|
1729
|
+
hint: t("choice.sender.remotedrawIos.hint.scan"),
|
|
1335
1730
|
},
|
|
1336
1731
|
{
|
|
1337
1732
|
value: "embedded-web",
|
|
1338
|
-
label: "
|
|
1339
|
-
hint: "
|
|
1733
|
+
label: t("choice.sender.embeddedWeb.label"),
|
|
1734
|
+
hint: t("choice.sender.embeddedWeb.hint"),
|
|
1340
1735
|
},
|
|
1341
1736
|
];
|
|
1342
1737
|
}
|
|
@@ -1345,8 +1740,8 @@ function sdkPromptChoices(target, sender) {
|
|
|
1345
1740
|
return [
|
|
1346
1741
|
{
|
|
1347
1742
|
value: "swift",
|
|
1348
|
-
label: "
|
|
1349
|
-
hint: "
|
|
1743
|
+
label: t("choice.sdk.swift.label"),
|
|
1744
|
+
hint: t("choice.sdk.swift.hint"),
|
|
1350
1745
|
},
|
|
1351
1746
|
];
|
|
1352
1747
|
}
|
|
@@ -1354,13 +1749,13 @@ function sdkPromptChoices(target, sender) {
|
|
|
1354
1749
|
return [
|
|
1355
1750
|
{
|
|
1356
1751
|
value: "js",
|
|
1357
|
-
label: "
|
|
1358
|
-
hint: "
|
|
1752
|
+
label: t("choice.sdk.js.label.client"),
|
|
1753
|
+
hint: t("choice.sdk.js.hint.client"),
|
|
1359
1754
|
},
|
|
1360
1755
|
{
|
|
1361
1756
|
value: "headless",
|
|
1362
|
-
label: "
|
|
1363
|
-
hint: "
|
|
1757
|
+
label: t("choice.sdk.headless.label"),
|
|
1758
|
+
hint: t("choice.sdk.headless.hint"),
|
|
1364
1759
|
},
|
|
1365
1760
|
];
|
|
1366
1761
|
}
|
|
@@ -1368,26 +1763,26 @@ function sdkPromptChoices(target, sender) {
|
|
|
1368
1763
|
return [
|
|
1369
1764
|
{
|
|
1370
1765
|
value: "js",
|
|
1371
|
-
label: "
|
|
1372
|
-
hint: "
|
|
1766
|
+
label: t("choice.sdk.js.label.client"),
|
|
1767
|
+
hint: t("choice.sdk.js.hint.client"),
|
|
1373
1768
|
},
|
|
1374
1769
|
];
|
|
1375
1770
|
}
|
|
1376
1771
|
return [
|
|
1377
1772
|
{
|
|
1378
1773
|
value: "react",
|
|
1379
|
-
label: "
|
|
1380
|
-
hint: "
|
|
1774
|
+
label: t("choice.sdk.react.label"),
|
|
1775
|
+
hint: t("choice.sdk.react.hint"),
|
|
1381
1776
|
},
|
|
1382
1777
|
{
|
|
1383
1778
|
value: "svelte",
|
|
1384
|
-
label: "
|
|
1385
|
-
hint: "
|
|
1779
|
+
label: t("choice.sdk.svelte.label"),
|
|
1780
|
+
hint: t("choice.sdk.svelte.hint"),
|
|
1386
1781
|
},
|
|
1387
1782
|
{
|
|
1388
1783
|
value: "js",
|
|
1389
|
-
label: "
|
|
1390
|
-
hint: "
|
|
1784
|
+
label: t("choice.sdk.js.label.noFramework"),
|
|
1785
|
+
hint: t("choice.sdk.js.hint.noFramework"),
|
|
1391
1786
|
},
|
|
1392
1787
|
];
|
|
1393
1788
|
}
|
|
@@ -1395,13 +1790,13 @@ function presetPromptChoices() {
|
|
|
1395
1790
|
return [
|
|
1396
1791
|
{
|
|
1397
1792
|
value: "signature",
|
|
1398
|
-
label: "
|
|
1399
|
-
hint: "
|
|
1793
|
+
label: t("choice.preset.signature.label"),
|
|
1794
|
+
hint: t("choice.preset.signature.hint"),
|
|
1400
1795
|
},
|
|
1401
1796
|
{
|
|
1402
1797
|
value: "sketch",
|
|
1403
|
-
label: "
|
|
1404
|
-
hint: "
|
|
1798
|
+
label: t("choice.preset.sketch.label"),
|
|
1799
|
+
hint: t("choice.preset.sketch.hint"),
|
|
1405
1800
|
},
|
|
1406
1801
|
];
|
|
1407
1802
|
}
|
|
@@ -1411,122 +1806,98 @@ function packageManagerPromptChoices() {
|
|
|
1411
1806
|
label: packageManager,
|
|
1412
1807
|
}));
|
|
1413
1808
|
}
|
|
1414
|
-
function
|
|
1415
|
-
return
|
|
1416
|
-
{
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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"),
|
|
1420
1816
|
},
|
|
1421
|
-
{
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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"),
|
|
1425
1822
|
},
|
|
1426
|
-
|
|
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
|
+
};
|
|
1427
1900
|
}
|
|
1428
|
-
const presetWizardDetails = {
|
|
1429
|
-
signature: {
|
|
1430
|
-
summary: "De productklare RemoteDraw-handtekeningstrook.",
|
|
1431
|
-
explanation: "Kies dit voor de complete, merkbare SignatureStrip met een exact passend telefoonoppervlak. Je kunt logo, tekst, kleur en omliggende formulier-UI aanpassen.",
|
|
1432
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1433
|
-
docsLabel: "Handtekeningdocs openen",
|
|
1434
|
-
},
|
|
1435
|
-
initials: {
|
|
1436
|
-
summary: "Een klein tekenveld voor initialen.",
|
|
1437
|
-
explanation: "Gebruik dit wanneer iemand op één of meerdere plekken korte initialen moet plaatsen, bijvoorbeeld bij documentcontrole.",
|
|
1438
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1439
|
-
docsLabel: "Initialendocs openen",
|
|
1440
|
-
},
|
|
1441
|
-
approval: {
|
|
1442
|
-
summary: "Vrije tekeninvoer voor een visueel akkoord.",
|
|
1443
|
-
explanation: "Gebruik dit voor goedkeuringen waarbij een vink, paraaf of korte markering voldoende is en je workflow de status beheert.",
|
|
1444
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1445
|
-
docsLabel: "Goedkeuringsdocs openen",
|
|
1446
|
-
},
|
|
1447
|
-
sketch: {
|
|
1448
|
-
summary: "Genormaliseerde invoer voor een eigen ontvangeroppervlak.",
|
|
1449
|
-
explanation: "Kies dit wanneer jouw product de foto, kaart, PDF, canvas of andere receiver-UI beheert. RemoteDraw levert de invoerprimitieven zonder een voorgeschreven componentontwerp.",
|
|
1450
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1451
|
-
docsLabel: "Schetsdocs openen",
|
|
1452
|
-
},
|
|
1453
|
-
photoMarkup: {
|
|
1454
|
-
summary: "Aantekeningen bovenop een foto.",
|
|
1455
|
-
explanation: "Gebruik dit voor inspecties, feedback of aanwijzingen op beeldmateriaal. Je app levert de foto en bewaart de relatie met de tekeningen.",
|
|
1456
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1457
|
-
docsLabel: "Fotoannotatiedocs openen",
|
|
1458
|
-
},
|
|
1459
|
-
pdfMarkup: {
|
|
1460
|
-
summary: "Aantekeningen op een PDF-pagina.",
|
|
1461
|
-
explanation: "Gebruik dit voor documentreview. Je app rendert de juiste pagina en koppelt RemoteDraw-coördinaten aan die vaste weergave.",
|
|
1462
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1463
|
-
docsLabel: "PDF-annotatiedocs openen",
|
|
1464
|
-
},
|
|
1465
|
-
mapMarkup: {
|
|
1466
|
-
summary: "Tekeningen en aanwijzingen op een kaart.",
|
|
1467
|
-
explanation: "Gebruik dit voor routes, locaties of ruimtelijke feedback. Je app beheert de kaart en viewport; RemoteDraw synchroniseert de invoer.",
|
|
1468
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1469
|
-
docsLabel: "Kaartannotatiedocs openen",
|
|
1470
|
-
},
|
|
1471
|
-
screenMarkup: {
|
|
1472
|
-
summary: "Aantekeningen op een scherm of applicatieweergave.",
|
|
1473
|
-
explanation: "Gebruik dit voor support, demo's en UI-feedback. Je ontvanger levert het schermbeeld waarop de telefoonmarkeringen worden geprojecteerd.",
|
|
1474
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1475
|
-
docsLabel: "Schermaantekeningdocs openen",
|
|
1476
|
-
},
|
|
1477
|
-
designReview: {
|
|
1478
|
-
summary: "Gerichte visuele feedback op een ontwerp.",
|
|
1479
|
-
explanation: "Gebruik dit voor ontwerpbeoordelingen met pijlen, vormen en vrije lijnen. Je product beheert opmerkingen, versies en besluitvorming.",
|
|
1480
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1481
|
-
docsLabel: "Ontwerpbeoordelingsdocs openen",
|
|
1482
|
-
},
|
|
1483
|
-
pointer: {
|
|
1484
|
-
summary: "Live aanwijzen zonder blijvende tekening.",
|
|
1485
|
-
explanation: "Gebruik dit voor presentaties en begeleiding waarbij de telefoon als aanwijzer dient en beweging belangrijker is dan opgeslagen inkt.",
|
|
1486
|
-
docsUrl: DOCS_PAYLOADS,
|
|
1487
|
-
docsLabel: "Aanwijzerdocs openen",
|
|
1488
|
-
},
|
|
1489
|
-
};
|
|
1490
|
-
const packageManagerWizardDetails = {
|
|
1491
|
-
npm: {
|
|
1492
|
-
summary: "De standaard package manager die met Node.js wordt geleverd.",
|
|
1493
|
-
explanation: "Kies npm als je project package-lock.json gebruikt of geen voorkeur heeft. De wizard gebruikt npm voor installatie- en vervolgopdrachten.",
|
|
1494
|
-
docsUrl: "https://docs.npmjs.com/",
|
|
1495
|
-
docsLabel: "npm-docs openen",
|
|
1496
|
-
},
|
|
1497
|
-
pnpm: {
|
|
1498
|
-
summary: "Een snelle package manager met een gedeelde pakketopslag.",
|
|
1499
|
-
explanation: "Kies pnpm als je project pnpm-lock.yaml gebruikt, strikte dependency-isolatie wenst of al deel is van een pnpm-workspace.",
|
|
1500
|
-
docsUrl: "https://pnpm.io/",
|
|
1501
|
-
docsLabel: "pnpm-docs openen",
|
|
1502
|
-
},
|
|
1503
|
-
yarn: {
|
|
1504
|
-
summary: "Een package manager met workspace- en Plug'n'Play-ondersteuning.",
|
|
1505
|
-
explanation: "Kies Yarn als je project yarn.lock gebruikt. De wizard sluit aan op de bestaande Yarn-versie en projectconfiguratie.",
|
|
1506
|
-
docsUrl: "https://yarnpkg.com/getting-started/usage",
|
|
1507
|
-
docsLabel: "Yarn-docs openen",
|
|
1508
|
-
},
|
|
1509
|
-
bun: {
|
|
1510
|
-
summary: "De snelle package manager die onderdeel is van de Bun-runtime.",
|
|
1511
|
-
explanation: "Kies Bun als je project bun.lock gebruikt of Bun al inzet voor scripts, tests en installatie van dependencies.",
|
|
1512
|
-
docsUrl: "https://bun.com/docs/pm/cli/install",
|
|
1513
|
-
docsLabel: "Bun-docs openen",
|
|
1514
|
-
},
|
|
1515
|
-
};
|
|
1516
|
-
const apiEndpointWizardDetails = {
|
|
1517
|
-
deployment: {
|
|
1518
|
-
summary: "Een tijdelijke URL die je later door je echte API-origin vervangt.",
|
|
1519
|
-
explanation: "Kies dit voor een nieuw project dat nog niet aan een lokale server is gekoppeld. De gegenereerde configuratie bevat een duidelijke deployment-placeholder.",
|
|
1520
|
-
docsUrl: DOCS_AUTH,
|
|
1521
|
-
docsLabel: "Deploymentdocs openen",
|
|
1522
|
-
},
|
|
1523
|
-
local: {
|
|
1524
|
-
summary: "Verbind met de lokale API op http://localhost:3210.",
|
|
1525
|
-
explanation: "Kies dit wanneer je RemoteDraw lokaal draait en de ontvanger op dezelfde computer de ontwikkelserver kan bereiken.",
|
|
1526
|
-
docsUrl: DOCS_AUTH,
|
|
1527
|
-
docsLabel: "Lokale API-docs openen",
|
|
1528
|
-
},
|
|
1529
|
-
};
|
|
1530
1901
|
function agentOptionCatalog() {
|
|
1531
1902
|
const catalogOption = (choiceOption, compatibility) => ({
|
|
1532
1903
|
value: choiceOption.value,
|
|
@@ -1535,13 +1906,13 @@ function agentOptionCatalog() {
|
|
|
1535
1906
|
details: choiceOption.details,
|
|
1536
1907
|
...(compatibility ?? {}),
|
|
1537
1908
|
});
|
|
1538
|
-
const targets = wizardChoices(targetPromptChoices(), targetWizardDetails);
|
|
1909
|
+
const targets = wizardChoices(targetPromptChoices(), targetWizardDetails());
|
|
1539
1910
|
const senders = senderChoices.map((value) => {
|
|
1540
1911
|
const compatibleTargets = targetChoices.filter((target) => senderPromptChoices(target).some((option) => option.value === value));
|
|
1541
1912
|
const prompt = compatibleTargets
|
|
1542
1913
|
.flatMap((target) => senderPromptChoices(target))
|
|
1543
1914
|
.find((option) => option.value === value);
|
|
1544
|
-
return catalogOption(wizardChoices([prompt], senderWizardDetails)[0], {
|
|
1915
|
+
return catalogOption(wizardChoices([prompt], senderWizardDetails())[0], {
|
|
1545
1916
|
compatibleTargets,
|
|
1546
1917
|
});
|
|
1547
1918
|
});
|
|
@@ -1551,7 +1922,7 @@ function agentOptionCatalog() {
|
|
|
1551
1922
|
: []));
|
|
1552
1923
|
const firstPlan = compatiblePlans[0];
|
|
1553
1924
|
const prompt = sdkPromptChoices(firstPlan.target, firstPlan.sender).find((option) => option.value === value);
|
|
1554
|
-
return catalogOption(wizardChoices([prompt], sdkWizardDetails)[0], {
|
|
1925
|
+
return catalogOption(wizardChoices([prompt], sdkWizardDetails())[0], {
|
|
1555
1926
|
compatiblePlans,
|
|
1556
1927
|
});
|
|
1557
1928
|
});
|
|
@@ -1563,7 +1934,7 @@ function agentOptionCatalog() {
|
|
|
1563
1934
|
{
|
|
1564
1935
|
key: "appName",
|
|
1565
1936
|
flag: "--app-name",
|
|
1566
|
-
label: "
|
|
1937
|
+
label: t("field.appName"),
|
|
1567
1938
|
kind: "text",
|
|
1568
1939
|
requiredFor: ["new"],
|
|
1569
1940
|
options: [],
|
|
@@ -1571,48 +1942,44 @@ function agentOptionCatalog() {
|
|
|
1571
1942
|
{
|
|
1572
1943
|
key: "target",
|
|
1573
1944
|
flag: "--target",
|
|
1574
|
-
label: "
|
|
1945
|
+
label: t("field.target"),
|
|
1575
1946
|
kind: "select",
|
|
1576
1947
|
options: targets.map((option) => catalogOption(option)),
|
|
1577
1948
|
},
|
|
1578
1949
|
{
|
|
1579
1950
|
key: "sender",
|
|
1580
1951
|
flag: "--sender",
|
|
1581
|
-
label: "
|
|
1952
|
+
label: t("field.sender"),
|
|
1582
1953
|
kind: "select",
|
|
1583
1954
|
options: senders,
|
|
1584
1955
|
},
|
|
1585
1956
|
{
|
|
1586
1957
|
key: "sdk",
|
|
1587
1958
|
flag: "--sdk",
|
|
1588
|
-
label: "
|
|
1959
|
+
label: t("field.sdk"),
|
|
1589
1960
|
kind: "select",
|
|
1590
1961
|
options: sdks,
|
|
1591
1962
|
},
|
|
1592
1963
|
{
|
|
1593
1964
|
key: "preset",
|
|
1594
1965
|
flag: "--preset",
|
|
1595
|
-
label: "
|
|
1966
|
+
label: t("field.preset"),
|
|
1596
1967
|
kind: "select",
|
|
1597
|
-
options: wizardChoices(presetPromptChoices(), presetWizardDetails).map((option) => catalogOption(option)),
|
|
1968
|
+
options: wizardChoices(presetPromptChoices(), presetWizardDetails()).map((option) => catalogOption(option)),
|
|
1598
1969
|
},
|
|
1599
1970
|
{
|
|
1600
1971
|
key: "packageManager",
|
|
1601
1972
|
flag: "--package-manager",
|
|
1602
|
-
label: "
|
|
1973
|
+
label: t("field.packageManager"),
|
|
1603
1974
|
kind: "select",
|
|
1604
|
-
options: wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails).map((option) => catalogOption(option)),
|
|
1975
|
+
options: wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails()).map((option) => catalogOption(option)),
|
|
1605
1976
|
},
|
|
1606
1977
|
{
|
|
1607
1978
|
key: "apiBaseUrl",
|
|
1608
1979
|
flag: "--api-base-url",
|
|
1609
|
-
label: "
|
|
1610
|
-
kind: "
|
|
1611
|
-
options:
|
|
1612
|
-
...catalogOption(option),
|
|
1613
|
-
endpoint: option.value,
|
|
1614
|
-
value: apiBaseUrlForEndpoint(option.value),
|
|
1615
|
-
})),
|
|
1980
|
+
label: t("field.apiBaseUrl"),
|
|
1981
|
+
kind: "text",
|
|
1982
|
+
options: [],
|
|
1616
1983
|
},
|
|
1617
1984
|
],
|
|
1618
1985
|
defaults: {
|
|
@@ -1624,7 +1991,7 @@ function agentOptionCatalog() {
|
|
|
1624
1991
|
]))),
|
|
1625
1992
|
preset: "signature",
|
|
1626
1993
|
packageManager: "npm",
|
|
1627
|
-
apiBaseUrl:
|
|
1994
|
+
apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
1628
1995
|
},
|
|
1629
1996
|
safety: {
|
|
1630
1997
|
dryRunFlag: "--dry-run",
|
|
@@ -1634,11 +2001,6 @@ function agentOptionCatalog() {
|
|
|
1634
2001
|
},
|
|
1635
2002
|
};
|
|
1636
2003
|
}
|
|
1637
|
-
function apiBaseUrlForEndpoint(endpoint) {
|
|
1638
|
-
if (endpoint === "local")
|
|
1639
|
-
return "http://localhost:3210";
|
|
1640
|
-
return "https://<deployment>.convex.site";
|
|
1641
|
-
}
|
|
1642
2004
|
export function projectSetupDefinition() {
|
|
1643
2005
|
return {
|
|
1644
2006
|
defaults: {
|
|
@@ -1648,52 +2010,8 @@ export function projectSetupDefinition() {
|
|
|
1648
2010
|
sdk: "react",
|
|
1649
2011
|
preset: "signature",
|
|
1650
2012
|
packageManager: "npm",
|
|
1651
|
-
apiEndpoint: "deployment",
|
|
1652
2013
|
},
|
|
1653
|
-
fields:
|
|
1654
|
-
{
|
|
1655
|
-
key: "appName",
|
|
1656
|
-
label: "Projectnaam",
|
|
1657
|
-
kind: "text",
|
|
1658
|
-
validate: validateWizardProjectName,
|
|
1659
|
-
},
|
|
1660
|
-
{
|
|
1661
|
-
key: "target",
|
|
1662
|
-
label: "Projecttype",
|
|
1663
|
-
kind: "select",
|
|
1664
|
-
choices: () => wizardChoices(targetPromptChoices(), targetWizardDetails),
|
|
1665
|
-
},
|
|
1666
|
-
{
|
|
1667
|
-
key: "sender",
|
|
1668
|
-
label: "Telefoonzender",
|
|
1669
|
-
kind: "select",
|
|
1670
|
-
choices: (values) => wizardChoices(senderPromptChoices(choice(values.target, targetChoices, "target")), senderWizardDetails),
|
|
1671
|
-
},
|
|
1672
|
-
{
|
|
1673
|
-
key: "sdk",
|
|
1674
|
-
label: "UI-framework / SDK",
|
|
1675
|
-
kind: "select",
|
|
1676
|
-
choices: (values) => wizardChoices(sdkPromptChoices(choice(values.target, targetChoices, "target"), choice(values.sender, senderChoices, "sender")), sdkWizardDetails),
|
|
1677
|
-
},
|
|
1678
|
-
{
|
|
1679
|
-
key: "preset",
|
|
1680
|
-
label: "Startpunt",
|
|
1681
|
-
kind: "select",
|
|
1682
|
-
choices: () => wizardChoices(presetPromptChoices(), presetWizardDetails),
|
|
1683
|
-
},
|
|
1684
|
-
{
|
|
1685
|
-
key: "packageManager",
|
|
1686
|
-
label: "Pakketbeheerder",
|
|
1687
|
-
kind: "select",
|
|
1688
|
-
choices: () => wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails),
|
|
1689
|
-
},
|
|
1690
|
-
{
|
|
1691
|
-
key: "apiEndpoint",
|
|
1692
|
-
label: "API-eindpunt",
|
|
1693
|
-
kind: "select",
|
|
1694
|
-
choices: () => wizardChoices(apiEndpointPromptChoices(), apiEndpointWizardDetails),
|
|
1695
|
-
},
|
|
1696
|
-
],
|
|
2014
|
+
fields: projectSetupFields(),
|
|
1697
2015
|
normalize(values, changed) {
|
|
1698
2016
|
const next = { ...values };
|
|
1699
2017
|
const target = choice(next.target, targetChoices, "target");
|
|
@@ -1713,76 +2031,63 @@ export function projectSetupDefinition() {
|
|
|
1713
2031
|
},
|
|
1714
2032
|
review(values) {
|
|
1715
2033
|
const plan = planFromWizardValues(values);
|
|
2034
|
+
const label = (key, fallback) => wizardChoiceLabel(key, values) ?? fallback;
|
|
1716
2035
|
return [
|
|
1717
|
-
["
|
|
1718
|
-
["
|
|
1719
|
-
["
|
|
1720
|
-
["
|
|
1721
|
-
["
|
|
1722
|
-
["
|
|
1723
|
-
["
|
|
1724
|
-
["API-eindpunt", plan.apiBaseUrl],
|
|
2036
|
+
[t("field.project"), plan.appName],
|
|
2037
|
+
[t("field.folder"), `./${plan.slug}`],
|
|
2038
|
+
[t("field.target"), label("target", plan.target)],
|
|
2039
|
+
[t("field.sender"), label("sender", plan.sender)],
|
|
2040
|
+
[t("field.sdkShort"), label("sdk", plan.sdk)],
|
|
2041
|
+
[t("field.packageManager"), label("packageManager", plan.packageManager)],
|
|
2042
|
+
[t("field.apiBaseUrl"), plan.apiBaseUrl],
|
|
1725
2043
|
];
|
|
1726
2044
|
},
|
|
1727
2045
|
};
|
|
1728
2046
|
}
|
|
2047
|
+
function projectSetupFields() {
|
|
2048
|
+
return [
|
|
2049
|
+
{
|
|
2050
|
+
key: "appName",
|
|
2051
|
+
label: t("field.appName"),
|
|
2052
|
+
kind: "text",
|
|
2053
|
+
validate: validateWizardProjectName,
|
|
2054
|
+
preview: (values) => values.appName.trim() === ""
|
|
2055
|
+
? undefined
|
|
2056
|
+
: t("field.appName.preview", { slug: slugify(values.appName) }),
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
key: "target",
|
|
2060
|
+
label: t("field.target"),
|
|
2061
|
+
kind: "select",
|
|
2062
|
+
choices: () => wizardChoices(targetPromptChoices(), targetWizardDetails()),
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
key: "sender",
|
|
2066
|
+
label: t("field.sender"),
|
|
2067
|
+
kind: "select",
|
|
2068
|
+
choices: (values) => wizardChoices(senderPromptChoices(choice(values.target, targetChoices, "target")), senderWizardDetails()),
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
key: "sdk",
|
|
2072
|
+
label: t("field.sdk"),
|
|
2073
|
+
kind: "select",
|
|
2074
|
+
choices: (values) => wizardChoices(sdkPromptChoices(choice(values.target, targetChoices, "target"), choice(values.sender, senderChoices, "sender")), sdkWizardDetails()),
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
key: "packageManager",
|
|
2078
|
+
label: t("field.packageManager"),
|
|
2079
|
+
kind: "select",
|
|
2080
|
+
choices: () => wizardChoices(packageManagerPromptChoices(), packageManagerWizardDetails()),
|
|
2081
|
+
},
|
|
2082
|
+
];
|
|
2083
|
+
}
|
|
2084
|
+
function wizardChoiceLabel(key, values) {
|
|
2085
|
+
const field = projectSetupFields().find((entry) => entry.key === key);
|
|
2086
|
+
return field?.choices?.(values).find((option) => option.value === values[key])
|
|
2087
|
+
?.label;
|
|
2088
|
+
}
|
|
1729
2089
|
function validateWizardProjectName(value) {
|
|
1730
|
-
return value.trim() === "" ? "
|
|
1731
|
-
}
|
|
1732
|
-
const dutchWizardText = {
|
|
1733
|
-
"Web app": "Webapp",
|
|
1734
|
-
"Desktop or custom app": "Desktop- of maatwerkapp",
|
|
1735
|
-
"iOS sender app": "iOS-zenderapp",
|
|
1736
|
-
"Headless service": "Headless-service",
|
|
1737
|
-
"Own iOS sender": "Eigen iOS-zender",
|
|
1738
|
-
"Use Swift join-link and sender request helpers.": "Gebruik Swift-helpers voor deelnamelinks en zenderverzoeken.",
|
|
1739
|
-
"Headless/custom sender": "Headless- of maatwerkzender",
|
|
1740
|
-
"No hosted phone UI.": "Geen gehoste telefooninterface.",
|
|
1741
|
-
"RemoteDraw iOS app": "RemoteDraw-iOS-app",
|
|
1742
|
-
"Show a join URL or QR code from your receiver.": "Toon vanuit je ontvanger een deelname-URL of QR-code.",
|
|
1743
|
-
"Build directly on the HTTP sender routes.": "Bouw rechtstreeks op de HTTP-zenderroutes.",
|
|
1744
|
-
"Fastest path: scan the receiver QR code.": "Snelste route: scan de QR-code van de ontvanger.",
|
|
1745
|
-
"Own web sender": "Eigen webzender",
|
|
1746
|
-
"Use a web sender component or raw sender helpers.": "Gebruik een webzendercomponent of losse zenderhelpers.",
|
|
1747
|
-
"Swift helpers": "Swift-helpers",
|
|
1748
|
-
"For customer-owned iOS sender apps.": "Voor iOS-zenderapps in eigen beheer.",
|
|
1749
|
-
"JavaScript client": "JavaScript-client",
|
|
1750
|
-
"Framework-free HTTP clients and protocol types.": "Frameworkvrije HTTP-clients en protocoltypen.",
|
|
1751
|
-
"Config only": "Alleen configuratie",
|
|
1752
|
-
"No UI package dependency.": "Geen afhankelijkheid van een UI-pakket.",
|
|
1753
|
-
"Receiver, pairing, and optional embedded sender components.": "Componenten voor ontvanger, koppeling en optionele ingebouwde zender.",
|
|
1754
|
-
"Svelte receiver store plus shared HTTP clients.": "Svelte-store voor de ontvanger met gedeelde HTTP-clients.",
|
|
1755
|
-
"No framework": "Geen framework",
|
|
1756
|
-
"Framework-free clients and raw HTTP starter files.": "Frameworkvrije clients en kale HTTP-startbestanden.",
|
|
1757
|
-
"Signature strip": "Handtekeningstrook",
|
|
1758
|
-
"Product-ready branded field with exact surface mapping.": "Productklaar merkveld met exacte oppervlaktekoppeling.",
|
|
1759
|
-
"Custom drawing surface": "Eigen tekenoppervlak",
|
|
1760
|
-
"Flexible normalized input for your own receiver UI.": "Flexibele genormaliseerde invoer voor je eigen ontvanger-UI.",
|
|
1761
|
-
Signature: "Handtekening",
|
|
1762
|
-
Initials: "Initialen",
|
|
1763
|
-
Approval: "Goedkeuring",
|
|
1764
|
-
Sketch: "Schets",
|
|
1765
|
-
"Photo markup": "Fotoannotatie",
|
|
1766
|
-
"PDF markup": "PDF-annotatie",
|
|
1767
|
-
"Map markup": "Kaartannotatie",
|
|
1768
|
-
"Screen markup": "Schermaantekening",
|
|
1769
|
-
"Design review": "Ontwerpbeoordeling",
|
|
1770
|
-
Pointer: "Aanwijzer",
|
|
1771
|
-
"Deployment URL placeholder": "Tijdelijke deployment-URL",
|
|
1772
|
-
"Fill in your Convex site origin later.": "Vul later de oorsprong van je Convex-site in.",
|
|
1773
|
-
"Local dev server": "Lokale ontwikkelserver",
|
|
1774
|
-
"Use http://localhost:3210.": "Gebruik http://localhost:3210.",
|
|
1775
|
-
};
|
|
1776
|
-
function dutchWizardChoices(choices) {
|
|
1777
|
-
return choices.map((choiceOption) => ({
|
|
1778
|
-
...choiceOption,
|
|
1779
|
-
label: dutchWizardText[choiceOption.label] ?? choiceOption.label,
|
|
1780
|
-
...(choiceOption.hint == null
|
|
1781
|
-
? {}
|
|
1782
|
-
: {
|
|
1783
|
-
hint: dutchWizardText[choiceOption.hint] ?? choiceOption.hint,
|
|
1784
|
-
}),
|
|
1785
|
-
}));
|
|
2090
|
+
return value.trim() === "" ? t("field.appName.error") : undefined;
|
|
1786
2091
|
}
|
|
1787
2092
|
function planFromWizardValues(values) {
|
|
1788
2093
|
const appName = values.appName.trim();
|
|
@@ -1797,11 +2102,11 @@ function planFromWizardValues(values) {
|
|
|
1797
2102
|
sdk,
|
|
1798
2103
|
preset: choice(values.preset, presetChoices, "preset"),
|
|
1799
2104
|
packageManager: choice(values.packageManager, packageManagerChoices, "package-manager"),
|
|
1800
|
-
apiBaseUrl:
|
|
2105
|
+
apiBaseUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
1801
2106
|
};
|
|
1802
2107
|
const nameError = validateProjectName(plan.appName);
|
|
1803
2108
|
if (nameError)
|
|
1804
|
-
throw
|
|
2109
|
+
throw cliError("MISSING_ARGUMENT", "field.appName.error");
|
|
1805
2110
|
validatePlan(plan);
|
|
1806
2111
|
return plan;
|
|
1807
2112
|
}
|
|
@@ -1810,7 +2115,7 @@ function planFromArgs(parsed, options) {
|
|
|
1810
2115
|
options.defaults?.appName ??
|
|
1811
2116
|
options.defaultAppName;
|
|
1812
2117
|
if (options.appNameRequired && !readString(parsed, "app-name")) {
|
|
1813
|
-
throw
|
|
2118
|
+
throw cliError("MISSING_ARGUMENT", "error.appNameRequired");
|
|
1814
2119
|
}
|
|
1815
2120
|
const target = choice(readString(parsed, "target") ?? options.defaults?.target ?? "web", targetChoices, "target");
|
|
1816
2121
|
const sender = choice(readString(parsed, "sender") ??
|
|
@@ -1825,7 +2130,7 @@ function planFromArgs(parsed, options) {
|
|
|
1825
2130
|
"npm", packageManagerChoices, "package-manager");
|
|
1826
2131
|
const apiBaseUrl = readString(parsed, "api-base-url") ??
|
|
1827
2132
|
options.defaults?.apiBaseUrl ??
|
|
1828
|
-
|
|
2133
|
+
DEFAULT_REMOTEDRAW_API_BASE_URL;
|
|
1829
2134
|
validatePlan({ target, sender, sdk });
|
|
1830
2135
|
return {
|
|
1831
2136
|
appName,
|
|
@@ -1854,22 +2159,26 @@ function defaultSdkForChoices(target, sender) {
|
|
|
1854
2159
|
}
|
|
1855
2160
|
function validatePlan(plan) {
|
|
1856
2161
|
if (plan.sender === "own-ios" && plan.sdk !== "swift") {
|
|
1857
|
-
throw
|
|
2162
|
+
throw cliError("INVALID_COMBINATION", "error.plan.ownIosNeedsSwift");
|
|
1858
2163
|
}
|
|
1859
2164
|
if (plan.sdk === "swift" && plan.sender !== "own-ios") {
|
|
1860
|
-
throw
|
|
2165
|
+
throw cliError("INVALID_COMBINATION", "error.plan.swiftNeedsOwnIos");
|
|
1861
2166
|
}
|
|
1862
2167
|
if (plan.target === "ios" && plan.sender !== "own-ios") {
|
|
1863
|
-
throw
|
|
2168
|
+
throw cliError("INVALID_COMBINATION", "error.plan.iosNeedsOwnIos");
|
|
1864
2169
|
}
|
|
1865
2170
|
if (plan.sender === "headless" && plan.sdk === "react") {
|
|
1866
|
-
throw
|
|
2171
|
+
throw cliError("INVALID_COMBINATION", "error.plan.headlessSenderSdk");
|
|
1867
2172
|
}
|
|
1868
2173
|
}
|
|
1869
2174
|
function choice(value, choices, label) {
|
|
1870
2175
|
if (choices.includes(value))
|
|
1871
2176
|
return value;
|
|
1872
|
-
throw
|
|
2177
|
+
throw cliError("INVALID_ARGUMENT", "error.unknownChoice", {
|
|
2178
|
+
label,
|
|
2179
|
+
value,
|
|
2180
|
+
expected: choices.join(", "),
|
|
2181
|
+
});
|
|
1873
2182
|
}
|
|
1874
2183
|
function resolveOutputDir(cwd, parsed, fallback) {
|
|
1875
2184
|
const requestedPath = readString(parsed, "path") ?? parsed.positionals[0] ?? fallback ?? ".";
|
|
@@ -1891,6 +2200,11 @@ function newProjectFiles(plan) {
|
|
|
1891
2200
|
files.set("tsconfig.json", reactTsconfig());
|
|
1892
2201
|
files.set("vite.config.ts", reactViteConfig());
|
|
1893
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
|
+
}
|
|
1894
2208
|
return files;
|
|
1895
2209
|
}
|
|
1896
2210
|
function initProjectFiles(plan, packageJsonAlreadyExists) {
|
|
@@ -1905,7 +2219,7 @@ function initProjectFiles(plan, packageJsonAlreadyExists) {
|
|
|
1905
2219
|
files.set("src/remotedraw/createRemoteDrawSession.ts", reactBackendSessionTs(plan));
|
|
1906
2220
|
files.set("src/remotedraw/RemoteDrawReceiver.tsx", reactReceiverTsx());
|
|
1907
2221
|
if (plan.sender === "embedded-web") {
|
|
1908
|
-
files.set("src/remotedraw/
|
|
2222
|
+
files.set("src/remotedraw/useRemoteDrawSender.ts", reactSenderTsx());
|
|
1909
2223
|
}
|
|
1910
2224
|
}
|
|
1911
2225
|
else if (plan.sdk === "svelte") {
|
|
@@ -1945,7 +2259,9 @@ async function assertProjectFilesWritable(files, outputDir, runtime, dryRun, for
|
|
|
1945
2259
|
return;
|
|
1946
2260
|
for (const relativePath of files.keys()) {
|
|
1947
2261
|
if (await runtime.exists(path.join(outputDir, relativePath))) {
|
|
1948
|
-
throw
|
|
2262
|
+
throw cliError("FILE_CONFLICT", "error.refuseOverwrite", {
|
|
2263
|
+
file: relativePath,
|
|
2264
|
+
});
|
|
1949
2265
|
}
|
|
1950
2266
|
}
|
|
1951
2267
|
}
|
|
@@ -2014,25 +2330,30 @@ function hasDependency(packageJson, dependencyName) {
|
|
|
2014
2330
|
return (isRecord(packageJson.dependencies) &&
|
|
2015
2331
|
typeof packageJson.dependencies[dependencyName] === "string");
|
|
2016
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
|
+
}
|
|
2017
2347
|
function dependenciesForPlan(plan) {
|
|
2348
|
+
const sdkPackage = sdkPackageName(plan.sdk);
|
|
2349
|
+
const dependencies = sdkPackage
|
|
2350
|
+
? { [sdkPackage]: REMOTEDRAW_SDK_RANGE }
|
|
2351
|
+
: {};
|
|
2018
2352
|
if (plan.sdk === "react") {
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
react: "^19.0.0",
|
|
2022
|
-
"react-dom": "^19.0.0",
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
if (plan.sdk === "svelte") {
|
|
2026
|
-
return {
|
|
2027
|
-
"@remotedraw/svelte": "latest",
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
if (plan.sdk === "js") {
|
|
2031
|
-
return {
|
|
2032
|
-
"@remotedraw/protocol": "latest",
|
|
2033
|
-
};
|
|
2353
|
+
dependencies.react = "^19.0.0";
|
|
2354
|
+
dependencies["react-dom"] = "^19.0.0";
|
|
2034
2355
|
}
|
|
2035
|
-
return
|
|
2356
|
+
return dependencies;
|
|
2036
2357
|
}
|
|
2037
2358
|
function newPackageJson(plan) {
|
|
2038
2359
|
const dependencies = dependenciesForPlan(plan);
|
|
@@ -2048,15 +2369,24 @@ function newPackageJson(plan) {
|
|
|
2048
2369
|
},
|
|
2049
2370
|
dependencies,
|
|
2050
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",
|
|
2051
2375
|
"@types/react": "^19.0.0",
|
|
2052
2376
|
"@types/react-dom": "^19.0.0",
|
|
2053
|
-
"@vitejs/plugin-react": "
|
|
2377
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
2054
2378
|
typescript: "^6.0.0",
|
|
2055
|
-
vite: "
|
|
2379
|
+
vite: "^8.0.0",
|
|
2056
2380
|
},
|
|
2057
2381
|
};
|
|
2058
2382
|
}
|
|
2059
|
-
|
|
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
|
+
};
|
|
2060
2390
|
}
|
|
2061
2391
|
function initPackageJson(plan) {
|
|
2062
2392
|
return {
|
|
@@ -2069,14 +2399,21 @@ function initPackageJson(plan) {
|
|
|
2069
2399
|
"remotedraw:create-input": "remotedraw create-input --preset " + plan.preset,
|
|
2070
2400
|
},
|
|
2071
2401
|
dependencies: dependenciesForPlan(plan),
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
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
|
+
},
|
|
2075
2412
|
};
|
|
2076
2413
|
}
|
|
2077
2414
|
function configJson(plan) {
|
|
2078
2415
|
return {
|
|
2079
|
-
$schema: "https://remotedraw.
|
|
2416
|
+
$schema: "https://www.remotedraw.com/schemas/remotedraw.config.json",
|
|
2080
2417
|
appName: plan.appName,
|
|
2081
2418
|
environment: "dev",
|
|
2082
2419
|
target: plan.target,
|
|
@@ -2088,7 +2425,7 @@ function configJson(plan) {
|
|
|
2088
2425
|
}
|
|
2089
2426
|
function envExample(plan) {
|
|
2090
2427
|
return [
|
|
2091
|
-
"# RemoteDraw API origin.
|
|
2428
|
+
"# RemoteDraw API origin. The hosted RemoteDraw API; leave as-is unless RemoteDraw support tells you otherwise.",
|
|
2092
2429
|
`REMOTEDRAW_API_BASE_URL=${plan.apiBaseUrl}`,
|
|
2093
2430
|
"",
|
|
2094
2431
|
"# Non-secret dashboard project identifier. remotedraw new/init fills this in.",
|
|
@@ -2176,7 +2513,7 @@ function reactReceiverTsx() {
|
|
|
2176
2513
|
"import {",
|
|
2177
2514
|
" RemoteDrawProvider,",
|
|
2178
2515
|
" RemoteDrawReceiver,",
|
|
2179
|
-
"
|
|
2516
|
+
" RemoteDrawSessionControls,",
|
|
2180
2517
|
" createHttpReceiverClient,",
|
|
2181
2518
|
'} from "@remotedraw/react";',
|
|
2182
2519
|
'import type { CreateSessionResponse } from "@remotedraw/react";',
|
|
@@ -2201,36 +2538,130 @@ function reactReceiverTsx() {
|
|
|
2201
2538
|
" receiver={receiver}",
|
|
2202
2539
|
" >",
|
|
2203
2540
|
' <RemoteDrawReceiver aria-label="RemoteDraw receiver surface" />',
|
|
2204
|
-
|
|
2541
|
+
" <RemoteDrawSessionControls />",
|
|
2205
2542
|
" </RemoteDrawProvider>",
|
|
2206
2543
|
" );",
|
|
2207
2544
|
"}",
|
|
2208
2545
|
"",
|
|
2209
2546
|
].join("\n");
|
|
2210
2547
|
}
|
|
2211
|
-
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.
|
|
2212
2553
|
return [
|
|
2213
|
-
'import {
|
|
2214
|
-
"",
|
|
2215
|
-
"
|
|
2216
|
-
"
|
|
2217
|
-
"
|
|
2218
|
-
"
|
|
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";',
|
|
2219
2566
|
"",
|
|
2220
|
-
"
|
|
2221
|
-
"
|
|
2222
|
-
"
|
|
2223
|
-
"
|
|
2224
|
-
"
|
|
2225
|
-
"
|
|
2226
|
-
"
|
|
2227
|
-
"
|
|
2228
|
-
"
|
|
2229
|
-
|
|
2230
|
-
' submitMetadata={{ senderLabel: "Embedded sender" }}',
|
|
2231
|
-
' submitLabel="Done"',
|
|
2232
|
-
" />",
|
|
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],",
|
|
2233
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],",
|
|
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
|
+
" };",
|
|
2234
2665
|
"}",
|
|
2235
2666
|
"",
|
|
2236
2667
|
].join("\n");
|
|
@@ -2308,11 +2739,24 @@ function svelteReceiverSvelte() {
|
|
|
2308
2739
|
].join("\n");
|
|
2309
2740
|
}
|
|
2310
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";
|
|
2311
2746
|
return [
|
|
2747
|
+
...(typed
|
|
2748
|
+
? [
|
|
2749
|
+
"import type {",
|
|
2750
|
+
" CreateSessionRequest,",
|
|
2751
|
+
" CreateSessionResponse,",
|
|
2752
|
+
'} from "@remotedraw/protocol";',
|
|
2753
|
+
"",
|
|
2754
|
+
]
|
|
2755
|
+
: []),
|
|
2312
2756
|
"const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
|
|
2313
2757
|
"const apiKey = process.env.REMOTEDRAW_API_KEY;",
|
|
2314
2758
|
"",
|
|
2315
|
-
|
|
2759
|
+
`export function createRemoteDrawSessionRequest()${typed ? ": CreateSessionRequest" : ""} {`,
|
|
2316
2760
|
" return " +
|
|
2317
2761
|
JSON.stringify(createSessionPayload({
|
|
2318
2762
|
preset: plan.preset,
|
|
@@ -2322,7 +2766,7 @@ function rawHttpSessionTs(plan) {
|
|
|
2322
2766
|
";",
|
|
2323
2767
|
"}",
|
|
2324
2768
|
"",
|
|
2325
|
-
|
|
2769
|
+
`export async function createRemoteDrawSession()${typed ? ": Promise<CreateSessionResponse>" : ""} {`,
|
|
2326
2770
|
" if (!apiBaseUrl || !apiKey) {",
|
|
2327
2771
|
' throw new Error("Set REMOTEDRAW_API_BASE_URL and REMOTEDRAW_API_KEY in backend secrets.");',
|
|
2328
2772
|
" }",
|
|
@@ -2336,7 +2780,9 @@ function rawHttpSessionTs(plan) {
|
|
|
2336
2780
|
" body: JSON.stringify(createRemoteDrawSessionRequest()),",
|
|
2337
2781
|
" });",
|
|
2338
2782
|
" if (!response.ok) throw new Error(await response.text());",
|
|
2339
|
-
|
|
2783
|
+
typed
|
|
2784
|
+
? " return (await response.json()) as CreateSessionResponse;"
|
|
2785
|
+
: " return await response.json();",
|
|
2340
2786
|
"}",
|
|
2341
2787
|
"",
|
|
2342
2788
|
].join("\n");
|
|
@@ -2546,6 +2992,9 @@ function reactTsconfig() {
|
|
|
2546
2992
|
' "target": "ES2022",',
|
|
2547
2993
|
' "useDefineForClassFields": true,',
|
|
2548
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"],',
|
|
2549
2998
|
' "allowJs": false,',
|
|
2550
2999
|
' "skipLibCheck": true,',
|
|
2551
3000
|
' "esModuleInterop": true,',
|
|
@@ -2563,6 +3012,26 @@ function reactTsconfig() {
|
|
|
2563
3012
|
"",
|
|
2564
3013
|
].join("\n");
|
|
2565
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
|
+
}
|
|
2566
3035
|
function reactViteConfig() {
|
|
2567
3036
|
return [
|
|
2568
3037
|
'import react from "@vitejs/plugin-react";',
|
|
@@ -2716,10 +3185,10 @@ function shellQuote(value) {
|
|
|
2716
3185
|
function normalizedOrigin(value) {
|
|
2717
3186
|
const parsed = new URL(value);
|
|
2718
3187
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
2719
|
-
throw
|
|
3188
|
+
throw cliError("INVALID_ARGUMENT", "error.expectedHttpOrigin");
|
|
2720
3189
|
}
|
|
2721
3190
|
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
2722
|
-
throw
|
|
3191
|
+
throw cliError("INVALID_ARGUMENT", "error.expectedBareOrigin");
|
|
2723
3192
|
}
|
|
2724
3193
|
return parsed.origin;
|
|
2725
3194
|
}
|
|
@@ -2734,95 +3203,80 @@ function resolveAgentPath(value, runtime) {
|
|
|
2734
3203
|
if (value.startsWith("~/")) {
|
|
2735
3204
|
const home = runtime.env.HOME;
|
|
2736
3205
|
if (home == null) {
|
|
2737
|
-
throw
|
|
3206
|
+
throw cliError("INVALID_ARGUMENT", "error.noHome");
|
|
2738
3207
|
}
|
|
2739
3208
|
return path.join(home, value.slice(2));
|
|
2740
3209
|
}
|
|
2741
3210
|
return path.resolve(runtime.cwd, value);
|
|
2742
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
|
+
*/
|
|
2743
3218
|
export function agentSkillMarkdown() {
|
|
2744
|
-
return
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
"",
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
"",
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
"
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
"
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
"
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
""
|
|
2797
|
-
"
|
|
2798
|
-
|
|
2799
|
-
"
|
|
2800
|
-
"
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
"- Backend creates sessions with `POST /v1/sessions`.",
|
|
2806
|
-
"- Receiver clients read `POST /v1/receiver/session`, `/drawings`, `/drafts`, and `/senders` with a receiver token.",
|
|
2807
|
-
"- 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`.",
|
|
2808
|
-
"- Custom senders should throttle draft updates, coalesce to the latest pending preview, and commit one durable stroke on pointer-up with a stable `clientStrokeId`.",
|
|
2809
|
-
"",
|
|
2810
|
-
"## Verification",
|
|
2811
|
-
"",
|
|
2812
|
-
"After changes, run the narrowest relevant checks first:",
|
|
2813
|
-
"",
|
|
2814
|
-
"```sh",
|
|
2815
|
-
"remotedraw doctor",
|
|
2816
|
-
"bun run test:api",
|
|
2817
|
-
"bun run typecheck",
|
|
2818
|
-
"```",
|
|
2819
|
-
"",
|
|
2820
|
-
"For customer-owned iOS sender helpers in this repo, also run:",
|
|
2821
|
-
"",
|
|
2822
|
-
"```sh",
|
|
2823
|
-
"bun run ios:kit:test",
|
|
2824
|
-
"```",
|
|
2825
|
-
].join("\n");
|
|
3219
|
+
return AGENT_SKILL_MARKDOWN;
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Doctor used to read REMOTEDRAW_API_BASE_URL raw and call an unset value a
|
|
3223
|
+
* problem, which disagreed with login/whoami/new — those all fall back to the
|
|
3224
|
+
* production API. Resolving it the same way here keeps every command pointed at
|
|
3225
|
+
* one deployment, and reporting the source explains a non-production URL
|
|
3226
|
+
* instead of leaving the reader to guess where it came from.
|
|
3227
|
+
*/
|
|
3228
|
+
function resolveDoctorApiBaseUrl(projectEnv, requested) {
|
|
3229
|
+
const explicit = requested ?? projectEnv.REMOTEDRAW_API_BASE_URL;
|
|
3230
|
+
const source = requested == null ? "project" : "flag";
|
|
3231
|
+
if (explicit == null || explicit.trim() === "") {
|
|
3232
|
+
return {
|
|
3233
|
+
state: "ok",
|
|
3234
|
+
url: DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
3235
|
+
source: "default",
|
|
3236
|
+
isProductionDefault: true,
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
// `init --offline` writes the literal placeholder, so an unreplaced value is
|
|
3240
|
+
// an expected next step rather than a mistake.
|
|
3241
|
+
if (explicit.includes("<"))
|
|
3242
|
+
return { state: "placeholder", value: explicit };
|
|
3243
|
+
if (!isHttpOrigin(explicit)) {
|
|
3244
|
+
return { state: "invalid", value: explicit, source };
|
|
3245
|
+
}
|
|
3246
|
+
const url = normalizedOrigin(explicit);
|
|
3247
|
+
return {
|
|
3248
|
+
state: "ok",
|
|
3249
|
+
url,
|
|
3250
|
+
source,
|
|
3251
|
+
isProductionDefault: url === DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
function doctorApiBaseUrlMessage(resolved) {
|
|
3255
|
+
if (resolved.state === "placeholder") {
|
|
3256
|
+
return t("doctor.baseUrl.placeholder", {
|
|
3257
|
+
value: resolved.value,
|
|
3258
|
+
defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
if (resolved.state === "invalid") {
|
|
3262
|
+
return t("doctor.baseUrl.invalid", {
|
|
3263
|
+
value: resolved.value,
|
|
3264
|
+
defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
if (resolved.source === "default") {
|
|
3268
|
+
return t("doctor.baseUrl.default", { url: resolved.url });
|
|
3269
|
+
}
|
|
3270
|
+
const origin = resolved.source === "flag"
|
|
3271
|
+
? t("doctor.baseUrl.origin.flag")
|
|
3272
|
+
: t("doctor.baseUrl.origin.project");
|
|
3273
|
+
return resolved.isProductionDefault
|
|
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
|
+
});
|
|
2826
3280
|
}
|
|
2827
3281
|
function isHttpOrigin(value) {
|
|
2828
3282
|
if (value == null || value.includes("<"))
|
|
@@ -2863,24 +3317,27 @@ function planDefaultsForExample(example) {
|
|
|
2863
3317
|
};
|
|
2864
3318
|
}
|
|
2865
3319
|
function scaffoldSummary(verb, plan, outputDir, written, dryRun) {
|
|
2866
|
-
const nextCommandPrefix = dryRun ? "Would write" : "Wrote";
|
|
2867
3320
|
return [
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
"",
|
|
2876
|
-
|
|
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"),
|
|
2877
3334
|
...written.map((file) => ` ${file}`),
|
|
2878
3335
|
"",
|
|
2879
|
-
"
|
|
2880
|
-
|
|
2881
|
-
"
|
|
2882
|
-
"
|
|
2883
|
-
"
|
|
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"),
|
|
2884
3341
|
].join("\n");
|
|
2885
3342
|
}
|
|
2886
3343
|
function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisioning, offline, dryRun) {
|
|
@@ -2898,16 +3355,12 @@ function scaffoldJsonResult(command, plan, outputDir, files, parsed, provisionin
|
|
|
2898
3355
|
.map(([field, , value]) => ({ field, value }));
|
|
2899
3356
|
const warnings = [
|
|
2900
3357
|
...(plan.apiBaseUrl.includes("<deployment>")
|
|
2901
|
-
? ["
|
|
3358
|
+
? [t("cloud.warning.placeholder")]
|
|
2902
3359
|
: []),
|
|
2903
3360
|
...(offline
|
|
2904
|
-
? [
|
|
2905
|
-
"Cloud provisioning was skipped; no dashboard project or API key was created.",
|
|
2906
|
-
]
|
|
3361
|
+
? [t("cloud.warning.offline")]
|
|
2907
3362
|
: dryRun
|
|
2908
|
-
? [
|
|
2909
|
-
"Cloud authentication and provisioning were not executed during dry-run.",
|
|
2910
|
-
]
|
|
3363
|
+
? [t("cloud.warning.dryRun")]
|
|
2911
3364
|
: []),
|
|
2912
3365
|
];
|
|
2913
3366
|
return {
|