@solarisdk/runloop-compat 0.0.0-stage → 0.1.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.
Files changed (58) hide show
  1. package/COMPAT.md +110 -0
  2. package/LICENSE +17 -0
  3. package/LICENSE-RUNLOOP +7 -0
  4. package/NOTICE +14 -0
  5. package/README.md +46 -2
  6. package/dist/blueprint-types.d.ts +522 -0
  7. package/dist/blueprint-types.d.ts.map +1 -0
  8. package/dist/blueprint-types.js +4 -0
  9. package/dist/blueprint-types.js.map +1 -0
  10. package/dist/blueprints.d.ts +71 -0
  11. package/dist/blueprints.d.ts.map +1 -0
  12. package/dist/blueprints.js +233 -0
  13. package/dist/blueprints.js.map +1 -0
  14. package/dist/client.d.ts +290 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +924 -0
  17. package/dist/client.js.map +1 -0
  18. package/dist/context.d.ts +42 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +198 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/dockerfile.d.ts +36 -0
  23. package/dist/dockerfile.d.ts.map +1 -0
  24. package/dist/dockerfile.js +338 -0
  25. package/dist/dockerfile.js.map +1 -0
  26. package/dist/errors.d.ts +67 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +171 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/image-ref.d.ts +35 -0
  31. package/dist/image-ref.d.ts.map +1 -0
  32. package/dist/image-ref.js +97 -0
  33. package/dist/image-ref.js.map +1 -0
  34. package/dist/index.d.ts +16 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +20 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/mapping.d.ts +49 -0
  39. package/dist/mapping.d.ts.map +1 -0
  40. package/dist/mapping.js +204 -0
  41. package/dist/mapping.js.map +1 -0
  42. package/dist/sdk.d.ts +182 -0
  43. package/dist/sdk.d.ts.map +1 -0
  44. package/dist/sdk.js +356 -0
  45. package/dist/sdk.js.map +1 -0
  46. package/dist/secrets.d.ts +41 -0
  47. package/dist/secrets.d.ts.map +1 -0
  48. package/dist/secrets.js +58 -0
  49. package/dist/secrets.js.map +1 -0
  50. package/dist/shared-types.d.ts +440 -0
  51. package/dist/shared-types.d.ts.map +1 -0
  52. package/dist/shared-types.js +4 -0
  53. package/dist/shared-types.js.map +1 -0
  54. package/dist/types.d.ts +920 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +4 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +48 -4
package/COMPAT.md ADDED
@@ -0,0 +1,110 @@
1
+ # COMPAT.md: Runloop SDK compatibility, TypeScript (target: @runloop/api-client 2.0.0)
2
+
3
+ Mirrors `sdk/python-runloop-compat/COMPAT.md` (same mapping rules, see `src/mapping.ts` vs `solari_runloop/_mapping.py`).
4
+ Legend: **OK** works and is tested live on staging; **DIFF** works with a behaviour difference; **GATEWAY** waits on a gateway
5
+ endpoint; **NO** throws `SolariCompatUnsupported`.
6
+
7
+ ## Setup
8
+ | Runloop | Here |
9
+ |---|---|
10
+ | `new Runloop({ bearerToken })`, `new RunloopSDK(...)`, env `RUNLOOP_API_KEY` | OK. Order: option (`bearerToken`/`apiKey`), `SOLARI_API_KEY`, `RUNLOOP_API_KEY`. **Only `slr_…` keys are used**: a non-`slr_` candidate (such as a real Runloop key in `RUNLOOP_API_KEY`) is skipped with a warning and never sent anywhere; if no Solari key is left the constructor throws. |
11
+ | `baseURL`, env `RUNLOOP_BASE_URL` | OK. Order: option, `SOLARI_BASE_URL`, `RUNLOOP_BASE_URL`, default `https://api.getsolari.com`. `RUNLOOP_BASE_URL` is only used when it is `https://getsolari.com` or a subdomain, or a loopback address (`localhost`, `127.x.x.x`, `[::1]`); otherwise it is ignored with a warning, so the `slr_` key is never sent to Runloop (`https://api.runloop.ai`) or a lookalike host. The option and `SOLARI_BASE_URL` are used as given. |
12
+ | `timeout`, `fetch` | OK (`timeout` is the control-channel call timeout). |
13
+ | `maxRetries` | Accepted; Solari's own retry policy applies. |
14
+ | `defaultHeaders`, `defaultQuery`, `httpAgent` | NO when non-empty. |
15
+
16
+ ## Object layer (`RunloopSDK`)
17
+ | Call | Status | Notes |
18
+ |---|---|---|
19
+ | `devbox.create(params)` | OK | Waits for `running`. Parameters: see below. |
20
+ | `devbox.createFromSnapshot(id)`, `snapshot.createDevbox()` | OK | `fromSnapshot`. |
21
+ | `devbox.createFromBlueprintName(name)`, `createFromBlueprintId(id)`, `blueprint.createDevbox()` | OK, live | A Solari template: a built-in name (`base`) or a blueprint built with `blueprint.create`. Custom names resolve to the newest matching `tpl_` id. |
22
+ | `devbox.fromId`, `devbox.list` | OK | `list` returns `Devbox[]` of the first page. |
23
+ | `getInfo`, `awaitRunning`, `awaitSuspended`, `shutdown`, `suspend`, `resume`, `resumeAsync`, `keepAlive` | OK | `suspend`/`resume` = Solari pause/resume (RAM+disk). `keepAlive` re-arms the idle window the devbox was created with (default 30 min). `resume` waits for a stable `running` (the gateway view can flap for ~1 s). |
24
+ | `cmd.exec(cmd, { stdout, stderr, output })` | OK | Runs `bash -lc "<cmd>"` from `$HOME` (created if missing). Callbacks get lines. Output is never truncated. |
25
+ | `cmd.execAsync(...)` → `Execution` | DIFF | `result()`, `getState()`, `kill()`, `sendStdIn()` work **inside the creating process** (in-memory registry). From another process: GATEWAY (durable executions). `closeStdIn` NO. |
26
+ | `shell(name)`, `shell_name` | GATEWAY | named persistent shells; throws until then. |
27
+ | `file.read / write / upload / download` | OK | Relative paths resolve against `$HOME`; parent dirs are created on write/upload. `upload({file})` takes a `Blob`/`File`, `Response`, bytes or string. `download` returns a `Response`. |
28
+ | `snapshotDisk(...)`, `snapshotDiskAsync(...)` | DIFF | Synchronous; returns a `Snapshot`. RAM+disk. `commit_message`/`metadata` are echoed on the return value, not stored (GATEWAY). |
29
+ | `snapshot.getInfo / delete / awaitCompleted / queryStatus`, `snapshot.list`, `snapshot.fromId` | OK | `delete` returns when the snapshot is really gone: large snapshots are erased incrementally (the gateway answers `202 {status:"deleting"}`), so it repeats the DELETE with backoff up to 5 minutes and then raises an error naming the snapshot instead of claiming success. `snapshot.update({name, commit_message, metadata})`: OK on a gateway with `PATCH /snapshots/{id}` (#566), else unsupported. Deleting a snapshot with live children is a 409 `ConflictError`. |
30
+ | `getTunnel()`, `getTunnelUrl(port)`, `net.enableTunnel()`, `net.removeTunnel()`, create `tunnel: {auth_mode, wake_on_http, http_keep_alive}` | DIFF, live | Solari's tunnel (`/sandboxes/:id/tunnel`, desktop#575): `auth_mode` `authenticated` (Runloop's default; returns `auth_token`, valid on every port for 1 h, sent as `x-solari-preview-token`) or `open`; `wake_on_http` (authenticated only; `open` + `wake_on_http` is refused) and `http_keep_alive` work. URLs are `https://<tunnel_key>-<port>.preview…` (the image of Runloop's `https://{port}-{key}.tunnel…`); `getTunnelUrl(port)` is the per-port preview URL, which carries a signed `slr_token`: treat it as a credential. `removeTunnel` revokes every token. `getTunnel()` is `null` when none is enabled. On a gateway without tunnel routes only `open` works (per-port preview). `net.createSSHKey` NO. |
31
+ | `devbox.logs({execution_id, text})` | DIFF | Uses the gateway's `GET /sandboxes/{id}/logs` when it exists; until then returns the output of commands run through **this client instance** (not durable across processes). |
32
+ | `devbox.update({name, metadata})` | OK on a gateway with `PATCH /sandboxes/{id}` (pinetree-desktop#566), else `SolariCompatUnsupported` | Detected per call: a 404 on the PATCH is re-checked against the devbox so a missing devbox is still `NotFoundError`. |
33
+ | `onEvict`, `cancelOnEvict` | NO | |
34
+ | `blueprint.create({name, dockerfile, system_setup_commands, file_mounts, base_blueprint_name/id, build_args, build_context, launch_parameters})` | DIFF, live | Builds a Solari template from a **Dockerfile subset** and waits for it (deleted again if the build fails). `FROM ubuntu/debian/base/workstation` = the native base; **any other public image** (docker.io, ghcr.io, public.ecr.aws, gcr.io, quay.io) becomes the gateway's native `fromImage` build (blueprint level 3b: desktop#572/#576, needs a golden with the new guest agent). A gateway without native image builds is detected from the 202 reply and the call raises `SolariCompatUnsupported` (the unwanted template is deleted when its build ends). `RUN ENV ARG WORKDIR ENTRYPOINT CMD`, `COPY`/`ADD` (below); not multi-stage, `USER`, `VOLUME`, `HEALTHCHECK`, `ONBUILD`. `code_mounts`, `services`, `secrets`, `network_policy_id` NO. |
35
+ | `blueprint.fromId / list`, `Blueprint.getInfo / logs / delete / createDevbox` | OK, live | `list()` returns your org's custom templates only. Build logs have no timestamps/levels (Solari returns plain lines; stderr is `ERROR`). Status: `building` → `building`, `ready` → `build_complete`, `failed` → `failed`. |
36
+ | `storageObject.uploadFromDir / uploadFromBytes`, `asBuildContext()`, `fromId`, `delete` | DIFF, live | Not real storage objects: a deterministic `.tar.gz` **held by this client** (honours `.dockerignore`, Docker semantics) and embedded in the build recipe in 60 KB chunks, capped at ~700 KB compressed (the template body limit is 1 MiB). `COPY`/`ADD` (incl. `--chown`, `--chmod`, globs, `ADD` of a local tar) copy from it; `COPY --from`, URLs and `../` or absolute sources are refused. **The context ends up inside the build recipe, so it is visible in the template's build record and logs: never put secrets in it.** A context from another process is `SolariCompatUnsupported`. The rest of `storageObject` (create, list, download, mounts) NO until the gateway has objects. |
37
+ | `client.secrets.{create, retrieve, update, list, delete}`; create `secrets: {ENV: name}` | DIFF, live | Solari account secrets (`/secrets`, desktop#583): named, org-scoped, **write-only** (no call returns a value; `retrieve`/`list` give name and times; `id` is the name). `create` on an existing name is `ConflictError`. Injected as env vars at create; deleting or changing a secret never affects a devbox that already received it. Needs a gateway with `ORG_SECRETS_KEY` (otherwise `501`). The `runloop.secret` convenience namespace is NO. |
38
+ | `agent`, `axon`, `networkPolicy`, `gatewayConfig`, `mcpConfig`, `secret` | NO | |
39
+
40
+ ## REST layer (`client.devboxes.*`)
41
+ `create`, `createAndAwaitRunning`, `retrieve`, `list` (auto-iterable page: `for await`), `shutdown`, `suspend`, `resume`,
42
+ `resumeAsync`, `keepAlive`, `awaitRunning`, `awaitSuspended`, `execute`, `executeSync`, `executeAsync`, `executeAndAwaitCompletion`,
43
+ `waitForCommand`, `readFileContents`, `writeFileContents`, `uploadFile`, `downloadFile`, `snapshotDisk`, `snapshotDiskAsync`,
44
+ `listDiskSnapshots`, `deleteDiskSnapshot`, `executions.{retrieve, awaitCompleted, executeAsync, executeSync, kill, sendStdIn}`,
45
+ `diskSnapshots.{list, retrieve, delete}`: OK (same caveats). `update` and `logs` as above; `enableTunnel`, `removeTunnel` OK. `createSSHKey`, `watchEvictions` NO. `client.blueprints` (`create`, `retrieve`, `list`, `delete`, `logs`, `awaitBuildComplete`, `createAndAwaitBuildComplete`): OK (Dockerfile subset). `client.secrets` OK (account secrets). `objects`, `agents`, `axons`, `networkPolicies`,
46
+ `gatewayConfigs`, `mcpConfigs`: every call throws `SolariCompatUnsupported`.
47
+
48
+ ## Create parameters
49
+ | Runloop param | Mapping |
50
+ |---|---|
51
+ | `name` | metadata key `runloop.name` (hidden from `metadata`) |
52
+ | `metadata` / `environment_variables` | `metadata` / `envs` |
53
+ | `blueprint_name` / `snapshot_id` | `template` (custom names resolved to `tpl_` ids) / `fromSnapshot` |
54
+ | `file_mounts` | files written after create |
55
+ | `entrypoint` | started in the background after create |
56
+ | `launch_parameters.launch_commands` | run in order after create; a non-zero exit fails the create (devbox is shut down) |
57
+ | `launch_parameters.keep_alive_time_seconds` | idle window; `lifecycle.onTimeout = "kill"` |
58
+ | `launch_parameters.after_idle` | idle window; `on_idle: "suspend"` → `onTimeout: "pause"`, else `"kill"` |
59
+ | `launch_parameters` sizing | see sizes |
60
+ | `blueprint_id` | a Solari template id |
61
+ | `tunnel: {auth_mode, wake_on_http, http_keep_alive}` | enables the tunnel right after the devbox is up (a failure kills the devbox); see tunnels |
62
+ | `secrets: {ENV: name}` | account secrets injected as env vars (see secrets); a name also in `environment_variables` is refused |
63
+ | `code_mounts`, `mounts`, `gateways`, `mcp`, `network_policy_id`, `user_parameters`, `required_services`, any unknown parameter | NO |
64
+ | `architecture`, `available_ports` | ignored |
65
+
66
+ ### Sizes (James's rule: cap silently, never error)
67
+ CPU rounds **up** to a whole vCPU; capped at Solari's limits (**8 vCPU / 16 GiB**) with a `console.warn`. Disk is sent only when
68
+ `custom_disk_size` is given. No size leaves Solari's defaults. Same table as the Python package:
69
+
70
+ | `resource_size_request` | Sent to Solari |
71
+ |---|---|
72
+ | `X_SMALL` | 1 vCPU, 1 GiB |
73
+ | `SMALL` | 1 vCPU, 2 GiB |
74
+ | `MEDIUM` | 2 vCPU, 4 GiB |
75
+ | `LARGE` | 2 vCPU, 8 GiB |
76
+ | `X_LARGE` | 4 vCPU, 16 GiB |
77
+ | `XX_LARGE` | 8 vCPU, 16 GiB (capped from 32) |
78
+ | `CUSTOM_SIZE` | ceil(`custom_cpu_cores`) capped at 8; `custom_gb_memory` × 1024 MiB capped at 16384 |
79
+
80
+ ## Status, errors, pagination
81
+ - Status: `starting`→`initializing`, `running`→`running`, `paused`→`suspended`, `archived`→`suspended`, `releasing`→`shutdown`,
82
+ `gone`→`shutdown`; anything else `failure`. `DevboxView` fields Solari lacks are synthesized (`create_time_ms`, one
83
+ `state_transitions` entry, `launch_parameters` from the real cpu/mem, `capabilities: []`).
84
+ - Errors: 400 `BadRequestError`, 401 `AuthenticationError`, 402/403 `PermissionDeniedError` (`status` keeps 402), 404 `NotFoundError`,
85
+ 409 `ConflictError`, 422 `UnprocessableEntityError`, 429 `RateLimitError`, 5xx `InternalServerError`; network `APIConnectionError`,
86
+ timeouts `APITimeoutError`. `.error` is Solari's `{code,error,message,retryable}` body.
87
+ - IDs are Solari's opaque signed strings (`:` and `.` inside), not `dbx_…`. Snapshot ids are `snap_…`.
88
+ - Pagination: `devboxes.list()` returns a page with `.devboxes`, `.has_more`, `.hasNextPage()`, `.getNextPage()`; it is async
89
+ iterable across pages. `starting_after` is sent as `startingAfter` to a gateway that supports it (#566, detected from `hasMore` in the list reply); on an older one the client scans from the start and skips past the id. `total_count` is the gateway's `totalCount` (with `include_total_count`) when it sends one, else the count so far.
90
+ - Staging quirks handled: GET right after create can 404 (retried); the first control-channel call after a resume can fail
91
+ (handle dropped, reconnected, retried); a snapshot right after a resume can be `NotRunning` or "Not snapshottable" for up to ~20 s (stale reads on another gateway replica; retried for up to a minute); the snapshot record can 404 briefly. A snapshot takes 25-30 s on staging.
92
+
93
+ ## Runloop's own examples (v2.0.0, import swapped; `test-live/run-runloop-examples.sh`)
94
+ | Example | Result |
95
+ |---|---|
96
+ | `devbox-snapshots` | PASS (suspend, resume, snapshot, two clones, isolation checks, snapshot deleted) |
97
+ | `devbox-from-blueprint-lifecycle` | PASS (Dockerfile blueprint, build logs, devbox logs from this client's record) |
98
+ | `blueprint-with-build-context` | PASS (`COPY . .` from a local directory context) |
99
+ | `devbox-tunnel` | PASS (open tunnel; the HTTP request through Solari's preview URL returns the directory listing) |
100
+ | `devbox-mounts`, `secrets-with-devbox`, `mcp-github-tools` | not run: object mounts / the secret store / MCP are not supported |
101
+ They clean up after themselves; the script lists what is left on the account afterwards.
102
+
103
+ ## Test status
104
+ - `test/blueprints.test.mjs`: offline: the Dockerfile compiler (including `FROM <image>` → native `fromImage`, chunked build contexts, COPY/ADD shapes), build contexts (deterministic tar.gz validated with `tar`, `.dockerignore`, long paths, size cap), blueprints against a fake gateway (create/await/delete-on-failure, name resolution, image-build detection), `update` / `logs` / `startingAfter` feature detection.
105
+ - `test/compat.test.mjs`: offline (fake gateway): exports, auth/base-URL order, status map, sizes, create translation, errors,
106
+ pagination, unsupported calls.
107
+ - `test-live/staging.test.mjs`: full lifecycle on staging (create, exec, files, async exec streaming, snapshot, create from snapshot,
108
+ keep-alive, list, suspend/resume, shutdown). `test-live/readme.test.mjs`: Runloop's TS README snippets with the import swapped
109
+ (quickstart, snapshot workflow, file write helpers, error handling, config); the README's `devbox.logs()`, blueprints and secrets
110
+ snippets assert the loud `SolariCompatUnsupported`.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Solari Desktop
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,7 @@
1
+ Copyright 2026 runloop
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,14 @@
1
+ @solarisdk/runloop-compat
2
+
3
+ Parts of this package are derived from @runloop/api-client 2.0.0
4
+ (https://github.com/runloopai/api-client-ts), which is distributed under the MIT
5
+ licence:
6
+
7
+ Copyright 2026 runloop
8
+
9
+ The licence text is reproduced in LICENSE-RUNLOOP. The derived files are
10
+ src/types.ts, src/shared-types.ts and src/blueprint-types.ts (type definitions). Their headers say so.
11
+
12
+ This package is not affiliated with, endorsed by, or sponsored by Runloop.
13
+ "Runloop" and "Devbox" are names of Runloop's products; they appear here only to
14
+ describe API compatibility.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @solarisdk/runloop-compat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Runloop-compatible API on top of [Solari Sandbox](https://getsolari.com). Change the import, keep your devbox code.
4
+
5
+ > **Not affiliated with, endorsed by, or sponsored by Runloop.** "Runloop" and "Devbox" are Runloop's product names; they
6
+ > appear here only to describe API compatibility. Type definitions are derived from `@runloop/api-client` 2.0.0 (MIT), see
7
+ > [NOTICE](./NOTICE). Do not look for this package under `@runloop/*`.
8
+
9
+ ```ts
10
+ // before: import { RunloopSDK } from '@runloop/api-client';
11
+ import { RunloopSDK } from '@solarisdk/runloop-compat';
12
+
13
+ const sdk = new RunloopSDK({ bearerToken: process.env.SOLARI_API_KEY });
14
+ const devbox = await sdk.devbox.create();
15
+ const result = await devbox.cmd.exec('echo "Hello, World!"');
16
+ console.log(await result.stdout(), result.exitCode);
17
+ await devbox.shutdown();
18
+ ```
19
+
20
+ Exports: `Runloop` (default and named; the REST layer, `client.devboxes.*`), `RunloopSDK` (the object layer), `Devbox`,
21
+ `Snapshot`, `Execution`, `ExecutionResult`, the Runloop error classes (`RunloopError`, `APIError`, `BadRequestError`,
22
+ `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `UnprocessableEntityError`,
23
+ `RateLimitError`, `InternalServerError`, `APIConnectionError`, `APITimeoutError`), `SolariCompatUnsupported`, and the types.
24
+
25
+ ## Configuration
26
+
27
+ | | Order |
28
+ |---|---|
29
+ | API key | `bearerToken` / `apiKey` option, `SOLARI_API_KEY`, `RUNLOOP_API_KEY`: only an `slr_…` Solari key is ever used. A non-`slr_` value (for example a real Runloop key) is skipped with a warning (the key is never printed or sent); with no Solari key the constructor throws |
30
+ | Base URL | `baseURL` option, `SOLARI_BASE_URL`, `RUNLOOP_BASE_URL` (only if it points at getsolari.com or loopback; otherwise ignored with a warning), default `https://api.getsolari.com` |
31
+
32
+ A Runloop key does not work here: use a Solari API key.
33
+
34
+ ## What works, what does not
35
+
36
+ [COMPAT.md](./COMPAT.md) is the support matrix. Anything Solari cannot do raises `SolariCompatUnsupported`
37
+ (an `APIError`) naming the feature; nothing is silently ignored. The sibling Python package is
38
+ `sdk/python-runloop-compat` and shares the same mapping rules.
39
+
40
+ ## Tests
41
+
42
+ ```sh
43
+ npm test # offline unit tests (a fake gateway)
44
+ SOLARI_API_KEY=… SOLARI_BASE_URL=https://api-sta.getsolari.com node --test test-live/*.test.mjs # staging; deletes what it creates
45
+ ```
46
+
47
+ `test-live/readme.test.mjs` runs Runloop's own TypeScript README snippets with only the import swapped.