Skip to content

feat: residency + components add (completes v0 feature parity) - #115

Merged
PaulAsjes merged 3 commits into
nextfrom
feat/residency-components
Jul 28, 2026
Merged

feat: residency + components add (completes v0 feature parity)#115
PaulAsjes merged 3 commits into
nextfrom
feat/residency-components

Conversation

@PaulAsjes

Copy link
Copy Markdown
Collaborator

What

The last two v0 features. With these, v1 has full v0 feature parity.

residency

elevenlabs residency                 # show current region + its base URL
elevenlabs residency eu-residency    # switch

The interesting part: v0 built its own HTTP client, so it routed straight from the stored setting. The framework resolves the base URL only from --base-url / ELEVENLABS_BASE_URL and exposes no config-file hook — so a stored region would normally affect nothing.

The bridge: custom::register runs before CliApp::run, which is where the framework reads the env. So register exports the region's base URL when nothing more explicit is set. A configured residency therefore routes every command, generated API commands included.

Verified with --dry-run on a generated command:

Setup Resulting URL
stored eu-residency https://api.eu.residency.elevenlabs.io/v1/models
ELEVENLABS_BASE_URL=https://example.test https://example.test/v1/models
--base-url https://flag.test https://flag.test/v1/models

So precedence is --base-url > ELEVENLABS_BASE_URL (env or .env) > stored residency > default. .env is handled explicitly: the framework loads it inside run() and dotenvy never overrides an existing var, so register loads it first to avoid clobbering a .env-provided base URL.

Placement note: registered top-level, not under auth as in v0. The framework grafts its own auth group after custom commands (app.rs:982) and dispatches auth before custom commands (1067 vs 1078), so auth residency would have produced a duplicate group whose subcommand never reached the handler.

components add

elevenlabs components add                    # all
elevenlabs components add conversation-bar

Delegates to npx shadcn@latest add <registry-url> with stdio inherited so prompts work.

Hardened vs v0: v0 built a shell string and ran it with shell: true, interpolating the component name — a command-injection vector. Here the argv is passed separately (never through a shell) and the name is restricted to registry-slug characters. Rejected: a;rm -rf /, $(whoami), a&&b, ../../etc/passwd, a/b, .hidden, ... Windows goes through cmd /C because npx is a .cmd shim std::process::Command won't resolve alone. Missing npx gives a friendly message rather than a raw OS error.

Verified

cargo build green; 22 workflow tests pass (3 new, covering slug validation, injection/traversal rejection, and that the argv never goes through a shell). Smoke-tested in an isolated HOME: residency get/set/persist/validate, the routing table above, component validation, and the missing-npx path.

Remaining for v1

Task #7 only: port the remaining high-value v0 unit tests, an E2E smoke test in CI, docs polish, then tag 1.0.0.

🤖 Generated with Claude Code

PaulAsjes and others added 3 commits July 28, 2026 12:51
residency: show or set the data-residency region, stored in
~/.elevenlabs/config.json. v0 built its own HTTP client so it could route
from the stored setting directly; the framework instead resolves the base URL
from --base-url / ELEVENLABS_BASE_URL with no config-file hook. So register()
exports the region's base URL before CliApp::run reads the env, which makes a
configured residency route every command, generated API commands included.
Precedence: --base-url > ELEVENLABS_BASE_URL (env or .env) > stored residency
> default. Registered top-level rather than under `auth` (where v0 had it)
because the framework grafts its own `auth` group after custom commands and
dispatches it first, so a nested subcommand would never reach the handler.

components add: install ElevenLabs UI components via shadcn. Unlike v0, which
interpolated the component name into a shell string (`shell: true`), the
argv is passed separately and never through a shell, and the name is
restricted to registry-slug characters — so it can't inject arguments or
traverse paths. Windows goes through `cmd /C` since npx is a .cmd shim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
residency.rs and settings.rs shipped untested while components.rs had all
three tests; the residency half carries the riskier logic (env precedence and
the region→URL mapping, which v0 covered in residency.test.ts).

Extracted two pure functions so the logic is testable without mutating process
env or touching a real home directory (env mutation is racy under Rust's
parallel test runner):

- residency::base_url_to_export(existing_env, region) — the precedence
  decision. Also fixes a latent bug found while writing the tests: an
  ELEVENLABS_BASE_URL set to an empty string used to suppress the residency
  and then break every request; it now counts as unset.
- settings::merge_residency(existing_config, region) — the config merge,
  covering that unrelated keys survive and that an api_key is never persisted.

Plus the full region→URL table and an unknown-region fallback. 10 new tests
(22 → 32).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.fernignore audit — verified with a real gitignore matcher, in both
directions (every hand-written file protected; every generated file still
regenerable). One genuine gap found: the generator emits
.github/workflows/ci.yml *with* actions-rust-lang/setup-rust-toolchain, which
this org's Actions allowlist rejects. The next regeneration would therefore
have silently reverted the CI fix and put every run back to startup_failure.
Added it, plus the hand-written elevenlabs-types/.gitignore. release.yml is
deliberately left regenerable so cargo-dist updates still land.

Test coverage — agents.rs, tools.rs and util.rs had none. Added tests for the
pure logic in each, and removed a triplication while doing it: the pull
create/update/skip decision was copy-pasted across agents/tools/tests, so it
now lives in util::plan_pull_action with the flag matrix covered once
(including v0's quirk that --update suppresses new items even alongside --all).

Also covered: build_pulled_config (server metadata must not leak into the
config file; workflow omitted when null), tool/test id spelling tolerance,
default tool shapes incl. verbatim header names, and dry_run_flag/opt_string.

47 workflow tests, every module covered except mod.rs (registration wiring).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PaulAsjes
PaulAsjes merged commit 361444c into next Jul 28, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant