@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/dist/cloud.d.ts CHANGED
@@ -40,6 +40,19 @@ export type ProvisionedProject = {
40
40
  };
41
41
  export declare function remoteDrawApiBaseUrl(env: Record<string, string | undefined>, requested?: string): string;
42
42
  export declare function globalConfigPath(runtime: CloudRuntime): string;
43
+ /**
44
+ * The stored CLI language, or undefined when the reader has never chosen one.
45
+ * A damaged or unreadable config never blocks a command here — the caller
46
+ * simply falls back to English — because the language is a preference, not a
47
+ * credential.
48
+ */
49
+ export declare function readStoredLanguage(runtime: CloudRuntime): Promise<string | undefined>;
50
+ /**
51
+ * Persists the language into the same config.json (and same 0600 directory)
52
+ * that already holds the CLI credential, so there is one user-config file to
53
+ * find, back up, or delete.
54
+ */
55
+ export declare function writeStoredLanguage(runtime: CloudRuntime, language: string): Promise<void>;
43
56
  export declare function currentCliAccount(runtime: CloudRuntime, apiBaseUrl: string): Promise<CliAccount | undefined>;
44
57
  export declare function loginToRemoteDraw(runtime: CloudRuntime, apiBaseUrl: string, options?: {
45
58
  openBrowser?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"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"}
package/dist/cloud.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import path from "node:path";
3
+ import { cliError } from "./errors.js";
4
+ import { t } from "./i18n.js";
3
5
  export const DEFAULT_REMOTEDRAW_API_BASE_URL = "https://api.remotedraw.com";
4
6
  function normalizedOrigin(value) {
5
7
  const url = new URL(value.trim());
@@ -9,7 +11,7 @@ function normalizedOrigin(value) {
9
11
  url.pathname !== "/" ||
10
12
  url.search ||
11
13
  url.hash) {
12
- throw new Error(`Expected an HTTP API origin, received ${value}.`);
14
+ throw new Error(t("cloud.error.badOrigin", { value }));
13
15
  }
14
16
  return url.origin;
15
17
  }
@@ -59,12 +61,47 @@ async function readStoredConfig(runtime) {
59
61
  ? parsed.activeApiBaseUrl
60
62
  : "",
61
63
  profiles,
64
+ ...(typeof parsed.language === "string"
65
+ ? { language: parsed.language }
66
+ : {}),
62
67
  };
63
68
  }
64
69
  catch {
65
- throw new Error(`Could not read RemoteDraw CLI credentials from ${configPath}. Remove or repair that file.`);
70
+ throw new Error(t("cloud.error.configUnreadable", { path: configPath }));
66
71
  }
67
72
  }
73
+ /**
74
+ * The stored CLI language, or undefined when the reader has never chosen one.
75
+ * A damaged or unreadable config never blocks a command here — the caller
76
+ * simply falls back to English — because the language is a preference, not a
77
+ * credential.
78
+ */
79
+ export async function readStoredLanguage(runtime) {
80
+ try {
81
+ return (await readStoredConfig(runtime)).language;
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ /**
88
+ * Persists the language into the same config.json (and same 0600 directory)
89
+ * that already holds the CLI credential, so there is one user-config file to
90
+ * find, back up, or delete.
91
+ */
92
+ export async function writeStoredLanguage(runtime, language) {
93
+ let config;
94
+ try {
95
+ config = await readStoredConfig(runtime);
96
+ }
97
+ catch {
98
+ // A corrupt credential file must not make the language unsettable; the
99
+ // login flow reports that failure on its own terms.
100
+ config = { version: 1, activeApiBaseUrl: "", profiles: {} };
101
+ }
102
+ config.language = language;
103
+ await writeStoredConfig(runtime, config);
104
+ }
68
105
  async function writeStoredConfig(runtime, config) {
69
106
  const configPath = globalConfigPath(runtime);
70
107
  await runtime.mkdir(path.dirname(configPath));
@@ -89,7 +126,7 @@ async function storedToken(runtime, apiBaseUrl) {
89
126
  }
90
127
  async function fetchJson(runtime, apiBaseUrl, pathname, options = {}) {
91
128
  if (!runtime.fetch)
92
- throw new Error("This runtime cannot access RemoteDraw.");
129
+ throw new Error(t("cloud.error.noFetch"));
93
130
  let response;
94
131
  try {
95
132
  response = await runtime.fetch(`${apiBaseUrl}${pathname}`, {
@@ -102,7 +139,10 @@ async function fetchJson(runtime, apiBaseUrl, pathname, options = {}) {
102
139
  });
103
140
  }
104
141
  catch (error) {
105
- throw new Error(`Could not reach ${apiBaseUrl}: ${error instanceof Error ? error.message : String(error)}`);
142
+ throw new Error(t("cloud.error.unreachable", {
143
+ url: apiBaseUrl,
144
+ reason: error instanceof Error ? error.message : String(error),
145
+ }));
106
146
  }
107
147
  const raw = await response.text();
108
148
  let payload;
@@ -111,16 +151,20 @@ async function fetchJson(runtime, apiBaseUrl, pathname, options = {}) {
111
151
  }
112
152
  catch {
113
153
  if (response.status === 404) {
114
- throw new Error(`RemoteDraw CLI endpoint ${pathname} was not found at ${apiBaseUrl}. Set REMOTEDRAW_API_BASE_URL to a deployment with the latest CLI auth functions.`);
154
+ throw new Error(t("cloud.error.endpointMissing", {
155
+ path: pathname,
156
+ url: apiBaseUrl,
157
+ defaultUrl: DEFAULT_REMOTEDRAW_API_BASE_URL,
158
+ }));
115
159
  }
116
- throw new Error(`RemoteDraw returned an invalid response (${response.status}).`);
160
+ throw new Error(t("cloud.error.invalidResponse", { status: response.status }));
117
161
  }
118
162
  if (!response.ok) {
119
163
  const message = isRecord(payload) &&
120
164
  isRecord(payload.error) &&
121
165
  typeof payload.error.message === "string"
122
166
  ? payload.error.message
123
- : `RemoteDraw request failed (${response.status}).`;
167
+ : t("cloud.error.requestFailed", { status: response.status });
124
168
  throw new Error(message);
125
169
  }
126
170
  return payload;
@@ -146,14 +190,17 @@ export async function loginToRemoteDraw(runtime, apiBaseUrl, options = {}) {
146
190
  const request = await fetchJson(runtime, normalized, "/v1/cli/auth/start", {
147
191
  body: { deviceName: options.deviceName ?? "RemoteDraw CLI" },
148
192
  });
149
- runtime.notify?.(`Authorize this CLI at ${request.verificationUrl}\nConfirmation code: ${request.userCode}`);
193
+ runtime.notify?.(t("cloud.authorize", {
194
+ url: request.verificationUrl,
195
+ code: request.userCode,
196
+ }));
150
197
  if (options.openBrowser !== false && runtime.openUrl) {
151
198
  try {
152
199
  await runtime.openUrl(request.verificationUrl);
153
- runtime.notify?.("Opened the browser. Waiting for authorization...");
200
+ runtime.notify?.(t("cloud.browserOpened"));
154
201
  }
155
202
  catch {
156
- runtime.notify?.("Could not open the browser automatically; use the URL above.");
203
+ runtime.notify?.(t("cloud.browserFailed"));
157
204
  }
158
205
  }
159
206
  const sleep = runtime.sleep ??
@@ -166,7 +213,7 @@ export async function loginToRemoteDraw(runtime, apiBaseUrl, options = {}) {
166
213
  if (result.status === "pending")
167
214
  continue;
168
215
  if (result.status !== "authorized") {
169
- throw new Error(`CLI authorization ${result.status}. Run remotedraw login again.`);
216
+ throw new Error(t("cloud.error.authStatus", { status: result.status }));
170
217
  }
171
218
  const config = await readStoredConfig(runtime);
172
219
  config.activeApiBaseUrl = normalized;
@@ -179,7 +226,7 @@ export async function loginToRemoteDraw(runtime, apiBaseUrl, options = {}) {
179
226
  await writeStoredConfig(runtime, config);
180
227
  return { account: result.account, alreadyLoggedIn: false };
181
228
  }
182
- throw new Error("CLI authorization expired. Run remotedraw login again.");
229
+ throw new Error(t("cloud.error.authExpired"));
183
230
  }
184
231
  export async function logoutFromRemoteDraw(runtime, apiBaseUrl) {
185
232
  const normalized = normalizedOrigin(apiBaseUrl);
@@ -214,7 +261,7 @@ export async function provisionRemoteDrawProject(runtime, apiBaseUrl, input) {
214
261
  }
215
262
  }
216
263
  if (!token) {
217
- throw new Error("Not logged in. Run remotedraw login, or set REMOTEDRAW_CLI_TOKEN for non-interactive use.");
264
+ throw cliError("AUTH_REQUIRED", "cloud.error.notLoggedIn");
218
265
  }
219
266
  return await fetchJson(runtime, normalized, "/v1/cli/projects", { body: input, token });
220
267
  }
@@ -246,7 +293,7 @@ export function updateEnvContents(contents, values, force = false) {
246
293
  existing !== value &&
247
294
  !isReplaceableEnvValue(name, existing) &&
248
295
  !force) {
249
- throw new Error(`${name} is already set in .env.local. Pass --force to replace it or use --offline to keep setup local-only.`);
296
+ throw new Error(t("cloud.error.envConflict", { name }));
250
297
  }
251
298
  const pattern = new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=.*$`, "m");
252
299
  if (pattern.test(next)) {
@@ -0,0 +1,16 @@
1
+ import { type MessageId, type MessageParams } from "./i18n.js";
2
+ /**
3
+ * The machine-readable classification carried by `--format json` failures.
4
+ * Codes used to be inferred from English substrings in the message, which
5
+ * quietly stopped working the moment a message could be Dutch. They are now
6
+ * declared where the error is raised, so the JSON contract is independent of
7
+ * the reader's language.
8
+ */
9
+ export type CliErrorCode = "MISSING_ARGUMENT" | "INVALID_ARGUMENT" | "INVALID_COMBINATION" | "FILE_CONFLICT" | "AUTH_REQUIRED" | "COMMAND_FAILED";
10
+ export declare class CliError extends Error {
11
+ readonly code: CliErrorCode;
12
+ constructor(message: string, code: CliErrorCode);
13
+ }
14
+ /** Raises a localized message with a stable, language-independent code. */
15
+ export declare function cliError(code: CliErrorCode, id: MessageId, params?: MessageParams): CliError;
16
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAK,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB,kBAAkB,GAClB,kBAAkB,GAClB,qBAAqB,GACrB,eAAe,GACf,eAAe,GACf,gBAAgB,CAAC;AAErB,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY;CAKhD;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CACtB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,SAAS,EACb,MAAM,CAAC,EAAE,aAAa,YAGvB"}
package/dist/errors.js ADDED
@@ -0,0 +1,13 @@
1
+ import { t } from "./i18n.js";
2
+ export class CliError extends Error {
3
+ code;
4
+ constructor(message, code) {
5
+ super(message);
6
+ this.name = "CliError";
7
+ this.code = code;
8
+ }
9
+ }
10
+ /** Raises a localized message with a stable, language-independent code. */
11
+ export function cliError(code, id, params) {
12
+ return new CliError(t(id, params), code);
13
+ }
@@ -0,0 +1,2 @@
1
+ export declare const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Add RemoteDraw phone input to customer apps with the RemoteDraw CLI, public API, React SDK, raw HTTP, or customer-owned iOS sender flow.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks to create, initialize, debug, or review a RemoteDraw integration.\n\n## Decision Flow\n\n1. Identify the receiver surface: web app, desktop app, iOS app, or headless/backend workflow.\n2. Identify the sender surface: RemoteDraw iOS app, embedded web sender, customer-owned iOS sender, or raw/headless sender.\n3. Pick the SDK path:\n - React SDK: web receiver and optional embedded web sender.\n - Plain JavaScript/raw HTTP: non-React web, desktop, backend, or custom clients.\n - Swift: customer-owned iOS sender apps.\n4. Pick a supported starter: `signature` for a bounded signature-field target,\n or `sketch` for a free-form receiver surface \u2014 both render through the\n `RemoteDrawReceiver` foundation, with the surrounding UI owned by the\n integrating app. Configure the target kind and descriptor directly for\n photos, PDFs, maps, screens, and other custom surfaces.\n\n## Endpoint, Key, and Packages\n\nRemoteDraw is hosted. There is nothing for the customer to run or self-host.\n\n- API base URL: `https://api.remotedraw.com`\n- Public docs: `https://docs.remotedraw.com/docs`\n- API keys are created in the console at\n `https://dashboard.remotedraw.com/api/keys` and belong in `.env.local` as\n `REMOTEDRAW_API_KEY`, alongside\n `REMOTEDRAW_API_BASE_URL=https://api.remotedraw.com`.\n\nPublished npm packages \u2014 install only what the chosen path needs:\n\n| Package | Install | Use it for |\n| --- | --- | --- |\n| `@remotedraw/cli` | `npm install -g @remotedraw/cli` (or `npx @remotedraw/cli@latest`) | Scaffolding, doctor, and test sessions. |\n| `@remotedraw/react` | `npm install @remotedraw/react` | React receiver, pairing, and headless sender. Peers on `react`/`react-dom` >= 18. |\n| `@remotedraw/svelte` | `npm install @remotedraw/svelte` | Svelte receiver store over the framework-free client. |\n| `@remotedraw/client` | `npm install @remotedraw/client` | Framework-free receiver/sender/API clients for any JS runtime. |\n| `@remotedraw/protocol` | `npm install @remotedraw/protocol` | Shared schemas, types, and limits for raw-HTTP integrations. |\n| `@remotedraw/geometry` | `npm install @remotedraw/geometry` | Normalized stroke geometry, shape assist, hit testing, export. |\n\n`@remotedraw/react` already depends on `client`, `protocol`, and `geometry`, so\ndo not add those separately for a React app. There is no published Swift\npackage: `remotedraw init --sdk swift` writes one self-contained\n`RemoteDrawIntegration.swift` against the public HTTP routes.\n\n## CLI First\n\nRead the machine-readable option catalog before choosing a plan:\n\n```sh\nremotedraw options --format json\n```\n\nInitialize a project with the closest supported path:\n\n```sh\nremotedraw init --target web --sender remotedraw-ios --sdk react --preset signature\nremotedraw init --target web --sender embedded-web --sdk react --preset sketch\nremotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch\nremotedraw init --target ios --sender own-ios --sdk swift --preset sketch\n```\n\nBy default, `new` and `init` create the dashboard project and a project-scoped\ndevelopment API key, then write `REMOTEDRAW_API_BASE_URL`,\n`REMOTEDRAW_PROJECT_ID`, and `REMOTEDRAW_API_KEY` to a gitignored `.env.local`.\nUse `--offline` only when cloud setup is intentionally out of scope.\n\n`--sender own-ios` requires `--sdk swift`; every other combination of\n`--target`, `--sender`, and `--sdk` is accepted. Invalid combinations fail with\n`INVALID_COMBINATION` rather than guessing.\n\nAgents must use explicit non-interactive dry runs before changing a project:\n\n```sh\nremotedraw init --non-interactive --offline --dry-run --format json --target web --sender remotedraw-ios --sdk react --preset signature --package-manager npm\n# Inspect plan, files, defaultsApplied, and warnings before applying.\nremotedraw init --non-interactive --offline --format json --target web --sender remotedraw-ios --sdk react --preset signature --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset signature --json\n```\n\nDo not use the interactive wizard, synthesize arrow-key input, or scrape\nhuman-formatted output. If receiver, sender, or preset intent is ambiguous, ask\nthe user instead of guessing. Never pass `--force` unless overwrite scope was\nexplicitly approved.\n\n## Security Rules\n\n- Keep `rd_sk_...` API keys in trusted backend secrets only.\n- Keep the account-level `rd_cli_...` credential in the user config directory; never copy it into a project. Use `REMOTEDRAW_CLI_TOKEN` only as an explicitly managed CI secret.\n- Never place API keys in browser bundles, mobile clients, screenshots, logs, or generated examples.\n- Public clients should receive only `joinUrl`, `joinToken`, `receiverToken`, or `senderToken` values scoped to the session.\n- Production QR codes should use HTTPS `joinUrl` values. Do not make the custom scheme the primary QR target.\n\n## API Contract\n\n- Backend creates sessions with `POST /v1/sessions`.\n- Receiver clients read `POST /v1/receiver/session`, `/drawings`, `/drafts`, and `/senders` with a receiver token.\n- 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`.\n- Custom senders should throttle draft updates, coalesce to the latest pending preview, and commit one durable stroke on pointer-up with a stable `clientStrokeId`.\n\n## AI Actions\n\nReach for AI when the product needs something _from_ the finished drawing:\na generated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit,\nsubmit, or presence event \u2014 AI runs only on an explicit `POST /v1/ai-actions`\ncall the user asked for. Run it after the user is done; the route accepts\n`active` and `ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use the helper on the API client\n(`createHttpRemoteDrawApiClient` from `@remotedraw/client`, re-exported by\n`@remotedraw/react`). This client holds the `rd_sk_` key, so it only ever runs\non the backend:\n\n```ts\nconst action = await client.createAiAction({\n sessionId,\n request: \"image\",\n prompt,\n});\nconst finished = await client.waitForAiAction({ aiActionId: action.id });\n// finished.result: generatedImageUrl | generatedText | generatedData | boardDrawingIds\n```\n\nThere is no completion webhook. Results are RemoteDraw-hosted asset URLs, not\nprovider URLs. See https://docs.remotedraw.com/docs/api#ai for the full\nrequest, credit, and legacy-mapping tables.\n\n## Verification\n\nAfter changes, verify against the customer's project \u2014 never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` \u2014\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.";
2
+ //# sourceMappingURL=agent-skill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-skill.d.ts","sourceRoot":"","sources":["../../src/generated/agent-skill.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,6nQAAomQ,CAAC"}
@@ -0,0 +1,4 @@
1
+ // GENERATED FILE — do not edit.
2
+ // Source: docs/agents/remotedraw/SKILL.md
3
+ // Regenerate: bun run --cwd packages/cli generate:skill
4
+ export const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Add RemoteDraw phone input to customer apps with the RemoteDraw CLI, public API, React SDK, raw HTTP, or customer-owned iOS sender flow.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks to create, initialize, debug, or review a RemoteDraw integration.\n\n## Decision Flow\n\n1. Identify the receiver surface: web app, desktop app, iOS app, or headless/backend workflow.\n2. Identify the sender surface: RemoteDraw iOS app, embedded web sender, customer-owned iOS sender, or raw/headless sender.\n3. Pick the SDK path:\n - React SDK: web receiver and optional embedded web sender.\n - Plain JavaScript/raw HTTP: non-React web, desktop, backend, or custom clients.\n - Swift: customer-owned iOS sender apps.\n4. Pick a supported starter: `signature` for a bounded signature-field target,\n or `sketch` for a free-form receiver surface — both render through the\n `RemoteDrawReceiver` foundation, with the surrounding UI owned by the\n integrating app. Configure the target kind and descriptor directly for\n photos, PDFs, maps, screens, and other custom surfaces.\n\n## Endpoint, Key, and Packages\n\nRemoteDraw is hosted. There is nothing for the customer to run or self-host.\n\n- API base URL: `https://api.remotedraw.com`\n- Public docs: `https://docs.remotedraw.com/docs`\n- API keys are created in the console at\n `https://dashboard.remotedraw.com/api/keys` and belong in `.env.local` as\n `REMOTEDRAW_API_KEY`, alongside\n `REMOTEDRAW_API_BASE_URL=https://api.remotedraw.com`.\n\nPublished npm packages — install only what the chosen path needs:\n\n| Package | Install | Use it for |\n| --- | --- | --- |\n| `@remotedraw/cli` | `npm install -g @remotedraw/cli` (or `npx @remotedraw/cli@latest`) | Scaffolding, doctor, and test sessions. |\n| `@remotedraw/react` | `npm install @remotedraw/react` | React receiver, pairing, and headless sender. Peers on `react`/`react-dom` >= 18. |\n| `@remotedraw/svelte` | `npm install @remotedraw/svelte` | Svelte receiver store over the framework-free client. |\n| `@remotedraw/client` | `npm install @remotedraw/client` | Framework-free receiver/sender/API clients for any JS runtime. |\n| `@remotedraw/protocol` | `npm install @remotedraw/protocol` | Shared schemas, types, and limits for raw-HTTP integrations. |\n| `@remotedraw/geometry` | `npm install @remotedraw/geometry` | Normalized stroke geometry, shape assist, hit testing, export. |\n\n`@remotedraw/react` already depends on `client`, `protocol`, and `geometry`, so\ndo not add those separately for a React app. There is no published Swift\npackage: `remotedraw init --sdk swift` writes one self-contained\n`RemoteDrawIntegration.swift` against the public HTTP routes.\n\n## CLI First\n\nRead the machine-readable option catalog before choosing a plan:\n\n```sh\nremotedraw options --format json\n```\n\nInitialize a project with the closest supported path:\n\n```sh\nremotedraw init --target web --sender remotedraw-ios --sdk react --preset signature\nremotedraw init --target web --sender embedded-web --sdk react --preset sketch\nremotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch\nremotedraw init --target ios --sender own-ios --sdk swift --preset sketch\n```\n\nBy default, `new` and `init` create the dashboard project and a project-scoped\ndevelopment API key, then write `REMOTEDRAW_API_BASE_URL`,\n`REMOTEDRAW_PROJECT_ID`, and `REMOTEDRAW_API_KEY` to a gitignored `.env.local`.\nUse `--offline` only when cloud setup is intentionally out of scope.\n\n`--sender own-ios` requires `--sdk swift`; every other combination of\n`--target`, `--sender`, and `--sdk` is accepted. Invalid combinations fail with\n`INVALID_COMBINATION` rather than guessing.\n\nAgents must use explicit non-interactive dry runs before changing a project:\n\n```sh\nremotedraw init --non-interactive --offline --dry-run --format json --target web --sender remotedraw-ios --sdk react --preset signature --package-manager npm\n# Inspect plan, files, defaultsApplied, and warnings before applying.\nremotedraw init --non-interactive --offline --format json --target web --sender remotedraw-ios --sdk react --preset signature --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset signature --json\n```\n\nDo not use the interactive wizard, synthesize arrow-key input, or scrape\nhuman-formatted output. If receiver, sender, or preset intent is ambiguous, ask\nthe user instead of guessing. Never pass `--force` unless overwrite scope was\nexplicitly approved.\n\n## Security Rules\n\n- Keep `rd_sk_...` API keys in trusted backend secrets only.\n- Keep the account-level `rd_cli_...` credential in the user config directory; never copy it into a project. Use `REMOTEDRAW_CLI_TOKEN` only as an explicitly managed CI secret.\n- Never place API keys in browser bundles, mobile clients, screenshots, logs, or generated examples.\n- Public clients should receive only `joinUrl`, `joinToken`, `receiverToken`, or `senderToken` values scoped to the session.\n- Production QR codes should use HTTPS `joinUrl` values. Do not make the custom scheme the primary QR target.\n\n## API Contract\n\n- Backend creates sessions with `POST /v1/sessions`.\n- Receiver clients read `POST /v1/receiver/session`, `/drawings`, `/drafts`, and `/senders` with a receiver token.\n- 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`.\n- Custom senders should throttle draft updates, coalesce to the latest pending preview, and commit one durable stroke on pointer-up with a stable `clientStrokeId`.\n\n## AI Actions\n\nReach for AI when the product needs something _from_ the finished drawing:\na generated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit,\nsubmit, or presence event — AI runs only on an explicit `POST /v1/ai-actions`\ncall the user asked for. Run it after the user is done; the route accepts\n`active` and `ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use the helper on the API client\n(`createHttpRemoteDrawApiClient` from `@remotedraw/client`, re-exported by\n`@remotedraw/react`). This client holds the `rd_sk_` key, so it only ever runs\non the backend:\n\n```ts\nconst action = await client.createAiAction({\n sessionId,\n request: \"image\",\n prompt,\n});\nconst finished = await client.waitForAiAction({ aiActionId: action.id });\n// finished.result: generatedImageUrl | generatedText | generatedData | boardDrawingIds\n```\n\nThere is no completion webhook. Results are RemoteDraw-hosted asset URLs, not\nprovider URLs. See https://docs.remotedraw.com/docs/api#ai for the full\nrequest, credit, and legacy-mapping tables.\n\n## Verification\n\nAfter changes, verify against the customer's project — never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` —\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.";
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { LOCALES, SOURCE_LOCALE, SUPPORTED_LOCALES, type Locale, type MessageId } from "./locales/index.js";
2
+ export type { Locale, MessageId };
3
+ export { LOCALES, SOURCE_LOCALE, SUPPORTED_LOCALES };
4
+ export type MessageParams = Record<string, string | number>;
5
+ export type Translate = (id: MessageId, params?: MessageParams) => string;
6
+ /** Env vars that pick the CLI language without a flag or a prompt. */
7
+ export declare const LANGUAGE_ENV_VARS: readonly ["REMOTEDRAW_LANGUAGE", "REMOTEDRAW_LANG"];
8
+ /** Where the resolved locale came from — doctor and `language` report it. */
9
+ export type LocaleSource = "flag" | "env" | "config" | "prompt" | "default";
10
+ export declare function isLocale(value: string): value is Locale;
11
+ /**
12
+ * Accepts what a human or a CI job would actually type: a code (`nl`), a
13
+ * BCP-47 tag (`nl-BE`, `nl_BE.UTF-8`), an endonym (`Nederlands`), or an
14
+ * English name (`Dutch`). Returns undefined for anything unrecognized so the
15
+ * caller can fail loudly instead of silently falling back.
16
+ */
17
+ export declare function normalizeLocale(value: string | undefined): Locale | undefined;
18
+ export declare function localeEndonym(locale: Locale): string;
19
+ /** `en, nl` — for "supported languages" error text. */
20
+ export declare function supportedLocaleList(): string;
21
+ /**
22
+ * A missing key renders the English string; a key missing from English too
23
+ * renders the id, which only happens for a typo and is loud in review.
24
+ */
25
+ export declare function createTranslator(locale: Locale): Translate;
26
+ export declare function setActiveLocale(locale: Locale): void;
27
+ export declare function activeLocale(): Locale;
28
+ export declare const t: Translate;
29
+ /**
30
+ * The picker question is built from every installed locale's own word for
31
+ * "Language", so it reads for a Dutch speaker and an English speaker alike
32
+ * and grows automatically when a locale is added.
33
+ */
34
+ export declare function languagePromptMessage(): string;
35
+ export declare function languageChoices(): {
36
+ value: "en" | "nl";
37
+ label: string;
38
+ hint: "en" | "nl";
39
+ }[];
40
+ /**
41
+ * Only used to preselect a row in the picker. The user still confirms, so a
42
+ * wrong guess costs an arrow key rather than an unreadable CLI.
43
+ */
44
+ export declare function systemLocaleHint(env: Record<string, string | undefined>): Locale | undefined;
45
+ export type LocaleResolution = {
46
+ ok: true;
47
+ locale: Locale;
48
+ source: LocaleSource;
49
+ needsPrompt: boolean;
50
+ } | {
51
+ ok: false;
52
+ value: string;
53
+ variable: string;
54
+ };
55
+ /**
56
+ * Precedence: explicit flag, environment, stored choice, then unresolved.
57
+ * "Unresolved" is not an error — it means the CLI may ask, and must default
58
+ * to English when it cannot (no TTY, piped stdin, CI, an agent shelling out).
59
+ */
60
+ export declare function resolveLocale(inputs: {
61
+ flag?: string | undefined;
62
+ env: Record<string, string | undefined>;
63
+ stored?: string | undefined;
64
+ }): LocaleResolution;
65
+ /**
66
+ * Pulls `--language <code>` / `--lang=<code>` out of argv before per-command
67
+ * flag parsing, which rejects options it does not declare. Returns the value
68
+ * and the remaining arguments.
69
+ */
70
+ export declare function extractLanguageFlag(args: readonly string[]): {
71
+ value?: string;
72
+ rest: string[];
73
+ };
74
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,KAAK,MAAM,EACX,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAC5D,MAAM,MAAM,SAAS,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,MAAM,CAAC;AAE1E,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,qDAGpB,CAAC;AAEX,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAkB7E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,UAE3C;AAED,uDAAuD;AACvD,wBAAgB,mBAAmB,WAIlC;AAUD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAK1D;AAYD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,QAG7C;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,eAAO,MAAM,CAAC,EAAE,SAAwD,CAAC;AAEzE;;;;GAIG;AACH,wBAAgB,qBAAqB,WAOpC;AAED,wBAAgB,eAAe;;;;IAM9B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,MAAM,GAAG,SAAS,CAMpB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,GACxE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B,GAAG,gBAAgB,CA4BnB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CA0BA"}
package/dist/i18n.js ADDED
@@ -0,0 +1,178 @@
1
+ import { LOCALES, SOURCE_LOCALE, SUPPORTED_LOCALES, } from "./locales/index.js";
2
+ export { LOCALES, SOURCE_LOCALE, SUPPORTED_LOCALES };
3
+ /** Env vars that pick the CLI language without a flag or a prompt. */
4
+ export const LANGUAGE_ENV_VARS = [
5
+ "REMOTEDRAW_LANGUAGE",
6
+ "REMOTEDRAW_LANG",
7
+ ];
8
+ export function isLocale(value) {
9
+ return SUPPORTED_LOCALES.includes(value);
10
+ }
11
+ /**
12
+ * Accepts what a human or a CI job would actually type: a code (`nl`), a
13
+ * BCP-47 tag (`nl-BE`, `nl_BE.UTF-8`), an endonym (`Nederlands`), or an
14
+ * English name (`Dutch`). Returns undefined for anything unrecognized so the
15
+ * caller can fail loudly instead of silently falling back.
16
+ */
17
+ export function normalizeLocale(value) {
18
+ if (value == null)
19
+ return undefined;
20
+ const cleaned = value.trim().toLowerCase().split(".")[0]?.replace("_", "-");
21
+ if (!cleaned)
22
+ return undefined;
23
+ const base = cleaned.split("-")[0] ?? cleaned;
24
+ for (const locale of SUPPORTED_LOCALES) {
25
+ const definition = LOCALES[locale];
26
+ if (locale === cleaned ||
27
+ locale === base ||
28
+ definition.tags.includes(base) ||
29
+ definition.aliases.includes(cleaned) ||
30
+ definition.endonym.toLowerCase() === cleaned) {
31
+ return locale;
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ export function localeEndonym(locale) {
37
+ return LOCALES[locale].endonym;
38
+ }
39
+ /** `en, nl` — for "supported languages" error text. */
40
+ export function supportedLocaleList() {
41
+ return SUPPORTED_LOCALES.map((locale) => `${locale} (${localeEndonym(locale)})`).join(", ");
42
+ }
43
+ function interpolate(template, params) {
44
+ if (!params)
45
+ return template;
46
+ return template.replaceAll(/\{(\w+)\}/g, (match, key) => {
47
+ const value = params[key];
48
+ return value == null ? match : String(value);
49
+ });
50
+ }
51
+ /**
52
+ * A missing key renders the English string; a key missing from English too
53
+ * renders the id, which only happens for a typo and is loud in review.
54
+ */
55
+ export function createTranslator(locale) {
56
+ const catalog = LOCALES[locale].catalog;
57
+ const fallback = LOCALES[SOURCE_LOCALE].catalog;
58
+ return (id, params) => interpolate(catalog[id] ?? fallback[id] ?? id, params);
59
+ }
60
+ /**
61
+ * The CLI is one process running one command for one reader, so the active
62
+ * locale is process state rather than an argument threaded through ~120 text
63
+ * builders. `runCli` sets it deterministically on entry from flag/env/config,
64
+ * which is what keeps repeated in-process runs (the test suite) from leaking
65
+ * a locale into each other.
66
+ */
67
+ let activeLocaleValue = SOURCE_LOCALE;
68
+ let activeTranslator = createTranslator(SOURCE_LOCALE);
69
+ export function setActiveLocale(locale) {
70
+ activeLocaleValue = locale;
71
+ activeTranslator = createTranslator(locale);
72
+ }
73
+ export function activeLocale() {
74
+ return activeLocaleValue;
75
+ }
76
+ export const t = (id, params) => activeTranslator(id, params);
77
+ /**
78
+ * The picker question is built from every installed locale's own word for
79
+ * "Language", so it reads for a Dutch speaker and an English speaker alike
80
+ * and grows automatically when a locale is added.
81
+ */
82
+ export function languagePromptMessage() {
83
+ const words = [];
84
+ for (const locale of SUPPORTED_LOCALES) {
85
+ const word = createTranslator(locale)("language.word");
86
+ if (!words.includes(word))
87
+ words.push(word);
88
+ }
89
+ return words.join(" / ");
90
+ }
91
+ export function languageChoices() {
92
+ return SUPPORTED_LOCALES.map((locale) => ({
93
+ value: locale,
94
+ label: localeEndonym(locale),
95
+ hint: locale,
96
+ }));
97
+ }
98
+ /**
99
+ * Only used to preselect a row in the picker. The user still confirms, so a
100
+ * wrong guess costs an arrow key rather than an unreadable CLI.
101
+ */
102
+ export function systemLocaleHint(env) {
103
+ for (const name of ["LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"]) {
104
+ const guess = normalizeLocale(env[name]?.split(":")[0]);
105
+ if (guess)
106
+ return guess;
107
+ }
108
+ return undefined;
109
+ }
110
+ /**
111
+ * Precedence: explicit flag, environment, stored choice, then unresolved.
112
+ * "Unresolved" is not an error — it means the CLI may ask, and must default
113
+ * to English when it cannot (no TTY, piped stdin, CI, an agent shelling out).
114
+ */
115
+ export function resolveLocale(inputs) {
116
+ if (inputs.flag != null) {
117
+ // A present-but-empty flag (`--language` with nothing after it) is a
118
+ // mistake worth reporting, not a silent fall-through to the environment.
119
+ const locale = normalizeLocale(inputs.flag);
120
+ return locale
121
+ ? { ok: true, locale, source: "flag", needsPrompt: false }
122
+ : { ok: false, value: inputs.flag.trim(), variable: "--language" };
123
+ }
124
+ for (const name of LANGUAGE_ENV_VARS) {
125
+ const raw = inputs.env[name]?.trim();
126
+ if (!raw)
127
+ continue;
128
+ const locale = normalizeLocale(raw);
129
+ return locale
130
+ ? { ok: true, locale, source: "env", needsPrompt: false }
131
+ : { ok: false, value: raw, variable: name };
132
+ }
133
+ const stored = normalizeLocale(inputs.stored);
134
+ if (stored) {
135
+ return { ok: true, locale: stored, source: "config", needsPrompt: false };
136
+ }
137
+ // Nothing chosen yet: English until the reader says otherwise.
138
+ return {
139
+ ok: true,
140
+ locale: SOURCE_LOCALE,
141
+ source: "default",
142
+ needsPrompt: true,
143
+ };
144
+ }
145
+ /**
146
+ * Pulls `--language <code>` / `--lang=<code>` out of argv before per-command
147
+ * flag parsing, which rejects options it does not declare. Returns the value
148
+ * and the remaining arguments.
149
+ */
150
+ export function extractLanguageFlag(args) {
151
+ const rest = [];
152
+ let value;
153
+ for (let index = 0; index < args.length; index += 1) {
154
+ const arg = args[index];
155
+ if (arg == null)
156
+ continue;
157
+ const match = /^--(language|lang)(?:=(.*))?$/.exec(arg);
158
+ if (!match) {
159
+ rest.push(arg);
160
+ continue;
161
+ }
162
+ if (match[2] != null) {
163
+ value = match[2];
164
+ continue;
165
+ }
166
+ const next = args[index + 1];
167
+ if (next != null && !next.startsWith("--")) {
168
+ value = next;
169
+ index += 1;
170
+ }
171
+ else {
172
+ // `--language` with nothing after it: treat as an empty request so the
173
+ // caller reports an unknown language instead of eating the next flag.
174
+ value = "";
175
+ }
176
+ }
177
+ return value == null ? { rest } : { value, rest };
178
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,41 @@
1
1
  #!/usr/bin/env node
2
- import { createNodeRuntime, runCli } from "./cli.js";
3
- const result = await runCli(process.argv.slice(2), createNodeRuntime());
2
+ import process from "node:process";
3
+ import { CLI_VERSION, createNodeRuntime, runCli } from "./cli.js";
4
+ import { nodeTelemetryEnvironment, reportCliCrash } from "./telemetry.js";
5
+ import { checkForUpdate, detectInstallMethod, updateNotice } from "./update.js";
6
+ // `runCli` reports whatever it catches; the handlers below cover the thin shell
7
+ // around it — building the runtime, writing to a closed stdout, a rejection that
8
+ // escapes a detached promise — which would otherwise crash with a bare Node
9
+ // stack trace we never hear about.
10
+ //
11
+ // Installing them replaces Node's default crash behavior, so this has to restore
12
+ // it: print the error, then exit non-zero. The report is awaited first because
13
+ // `process.exit` would drop the in-flight request.
14
+ async function reportAndCrash(error) {
15
+ await reportCliCrash(error, process.argv[2] ?? "help", nodeTelemetryEnvironment(CLI_VERSION));
16
+ process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
17
+ process.exit(1);
18
+ }
19
+ process.on("uncaughtException", (error) => void reportAndCrash(error));
20
+ process.on("unhandledRejection", (reason) => void reportAndCrash(reason));
21
+ const runtime = createNodeRuntime();
22
+ const result = await runCli(process.argv.slice(2), runtime);
4
23
  if (result.stdout)
5
24
  process.stdout.write(result.stdout);
6
25
  if (result.stderr)
7
26
  process.stderr.write(result.stderr);
8
27
  process.exitCode = result.exitCode;
28
+ // Only a human at a terminal sees this. Piped output, CI, and machine-readable
29
+ // runs stay byte-for-byte what the command produced.
30
+ if (process.stderr.isTTY && process.argv[2] !== "update") {
31
+ try {
32
+ const check = await checkForUpdate(runtime, CLI_VERSION);
33
+ if (check?.updateAvailable) {
34
+ const method = detectInstallMethod(runtime.binPath ?? "");
35
+ process.stderr.write(`\n${updateNotice(check, method)}\n`);
36
+ }
37
+ }
38
+ catch {
39
+ // A failed update check never changes the outcome of the real command.
40
+ }
41
+ }
@@ -0,0 +1,21 @@
1
+ import type { en } from "./en.js";
2
+ /** Every translatable string in the CLI is addressed by one of these ids. */
3
+ export type MessageId = keyof typeof en;
4
+ /**
5
+ * A translation is a partial overlay on the English source catalog: anything
6
+ * a locale does not define renders in English instead of leaking a raw id.
7
+ */
8
+ export type LocaleCatalog = Partial<Record<MessageId, string>>;
9
+ export type LocaleDefinition = {
10
+ /**
11
+ * The language's own name for itself. The language picker lists endonyms so
12
+ * it is readable no matter which language the reader speaks.
13
+ */
14
+ endonym: string;
15
+ /** BCP-47 prefixes that map to this locale (`nl-BE` -> `nl`). */
16
+ tags: readonly string[];
17
+ /** Extra spellings accepted from --language / the environment. */
18
+ aliases: readonly string[];
19
+ catalog: LocaleCatalog;
20
+ };
21
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/locales/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAC;AAExC;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;AAE/D,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,kEAAkE;IAClE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC"}
@@ -0,0 +1 @@
1
+ export {};