Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions docs/content/sips/024-spin-deps-cli-dx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
title = "SIP 024 - spin deps cli dx"
template = "main"
date = "2026-04-09T00:00:00Z"

---

Summary: A CLI command (`spin deps add`) for adding component dependencies to a Spin application, with interactive prompts for selecting components, exports, and capability inheritance.

Owner(s): [brian.hardock@fermyon.com](mailto:brian.hardock@fermyon.com)

Created: April 9, 2026

# Background

[SIP 020](docs/content/sips/020-component-dependencies.md) introduced the concept of component dependencies in Spin, allowing developers to compose components together by declaring dependencies in `spin.toml`. [SIP 023](docs/content/sips/023-granular-capability-inheritance.md) extended this with per-dependency, granular capability inheritance — replacing the all-or-nothing `dependencies_inherit_configuration` boolean with a flexible `inherit_configuration` field that accepts `true`, `false`, or a list of specific capability keys.

However, authoring the dependency entries by hand requires understanding the TOML schema, knowing which exports a component offers, and correctly configuring capability inheritance — all of which are error-prone.

`spin deps add` provides a guided CLI experience for adding a component dependency. It resolves the source, inspects the Wasm component's exports, and writes the correct entry into `spin.toml`, along with regenerating the `spin-dependencies.wit` file.

# Proposal
## Command Syntax

```
spin deps add <source> [options]
```

### Source Formats

The `<source>` positional argument accepts three forms:

| Form | Example | Description |
|------|---------|-------------|
| Local path | `./my-component.wasm` | A path to a Wasm component on disk |
| HTTP URL | `https://example.com/component.wasm` | A remote Wasm component (requires `--digest`) |
| Registry reference | `aws:client@1.0.0` | A package from a component registry |
Comment on lines +33 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also support adding by component ID? I am imagining a case where we have a component that we already build as part of the manifest that we would like to add as dependency to another.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strong agree on this one. I don't want to have to know/copy-paste the build output path of my Rust component to add it as a dependency of my TS component in the same spin project. I want that to be auto-figured out from the component ID


### Options

| Flag | Description |
|------|-------------|
| `--to <component-id>` | Target component to add the dependency to. Prompted if omitted and the app has multiple components. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `--to <component-id>` | Target component to add the dependency to. Prompted if omitted and the app has multiple components. |
| `--to <component-id>` | Parent component to add the dependency to. Prompted if omitted and the app has multiple components. |

To be more consistent with later prose (i.e., inheriting "parent" component capabilities)

| `-f, --from <path>` | Path to the `spin.toml` manifest. Defaults to the current directory. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This feels a bit weird... I get that it's consistent with e.g., spin build, but there it makes more semantic sense (you build "from" a .toml).

In this setup, it's a bit weird given that you're adding dependencies TO something.

In spin add the -f is --file not --from, which would make more sense here as well imo.

| `--export <name>` | Export to use from the dependency. Prompted if omitted and the component has multiple exports. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `--export <name>` | Export to use from the dependency. Prompted if omitted and the component has multiple exports. |
| `--export <name>` | Export to use from the dependency. Prompted if omitted and the dependency has multiple exports. |

| `-d, --digest <sha256>` | SHA-256 digest for verifying HTTP downloads. Required for HTTP sources. |
| `-r, --registry <url>` | Override the default registry. Only applies to registry sources. |
| `--inherit <value>` | Capability inheritance: `true`/`all`, `false`/`none`, or comma-separated capabilities. Prompted if omitted and the dependency requires capabilities. |

## Interactive Prompts

When optional flags are omitted, `spin deps add` presents interactive prompts to guide the developer through each decision. The following sections illustrate the prompt flow.

### Step 1: Select the target component

If `--to` is omitted and the application has more than one component, the user is prompted:

```
$ spin deps add aws:client@1.0.0

? Which component should the dependency be added to?
> api-server
worker
dashboard
```

If the application has exactly one component, it is selected automatically.

### Step 2: Select the export

The command inspects the resolved Wasm component to enumerate its exports. If `--export` is omitted, the prompt flow depends on the number of packages and interfaces.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this document should also detail the failure path? Or if there's somewhere else that describes exactly how you "resolve" a Wasm component from an HTTP URL/registry and what happens if it can't be found / if it doesn't actually export anything that can be used?

What would be the error message shown if the dependency can't be found? If it's somehow incompatible (e.g., wasm-bindgen .wasm from HTTP URL)?

When does spin try the resolution? Before or after or parallel with showing the interactive prompt for --to? Is there a progress-bar or something for large/slow downloads?


#### Single export — auto-selected

If the component exports only one interface, it is selected automatically with no prompt.

#### Multiple packages — select a package first

If the component exports interfaces from multiple packages, the user first selects a package:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry with this that we may be asking a question the user doesn't know how to answer. I want the authentication interface, is that in client or util? If I guess wrong, is there a way back?

Possible alternative approaches:

  1. Show them a list of everything they could import - all the interfaces plus an "everything" for each package.
  2. When they come to interface selection, offer a "The interface I'm looking for isn't here. Take me back to package selection" option

Or maybe this isn't a concern and we have high confidence that users will know what they are looking for and just want to save on ye olde typing, I am not sure.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. Ill have a tinker and see if i could smooth this out. I think listing everything with all options for each package is the way to go.

Copy link
Copy Markdown

@rmarx rmarx May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking something similar, but you might run into an issue where there's just a TON of exports in a given dependency, and navigating them manually would get annoying.

Counterpoint is of course that that won't happen super-often and you only need to do this once/infrequently, so I'd also be in favor of having just 1 flat list of everything with the "All from DEP" in the list as just another option.


```
? Which package should be used?
> aws:client@1.0.0
aws:util@1.0.0
```

#### Within-package selection — all or a specific interface

After a package is selected (or if there is only one), the user chooses between all exports from that package or a single specific interface:

```
? Which export should be used?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I would avoid saying 'export' here (because I want to import something, where is the choice for that). Consider e.g. "Which interface do you want to import?" or some better wording that avoids the whole import/export debacle altogether (a la "which interface do you want to use" although that's hardly going to set the poetry world alight either)

Copy link
Copy Markdown

@rmarx rmarx May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a similar feeling when seeing the CLI argument is called --export instead of --import... it depends on how you want to look at it of course.

However, since I'm adding a dependency to a specific component, I probably want that component to import/use stuff from the dependency (I don't necessarily want to think about the stuff the dependency exports).

Kind of like a github PR should really be a Merge Request instead? :D (imo)

Either way, if we keep --export (instead of --import or --interfaces) I agree it would be more sensible to have some other language here like suggested by @itowlson

EDIT: just now reading the whole discussion on this below :)

> All from aws:client@1.0.0
aws:client/s3@1.0.0
aws:client/dynamodb@1.0.0
aws:client/sqs@1.0.0
```

Selecting **"All from aws:client@1.0.0"** records `aws:client@1.0.0` as the dependency name (a package-level selector). Selecting a specific interface records that interface (e.g. `aws:client/s3@1.0.0`).

#### Explicit `--export` flag
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not surfaced interactively, right? (I would like it not to be - I just want to check we are on the same page.)

oh wait, this is the interface on the left hand side, not the export = option? That coincidence of terminology is going to be a trap

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're equivalent when selecting interfaces. I couldnt think of a better name here that encapsulated packages and interfaces. How do you feel about --use?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait once again it seems I am utterly confused about a feature we have been shipping for a gazillion years - I thought export was about remapping interfaces... sigh

I was initially going to suggest --interface, but I now realise that if you want all a package's APIs, you have to tell it the package name if you don't want it to interact. --use seems a bit open-ended but could be an option. We could try --import perhaps? It's not great though. Well maybe it's okay, I originally thought "ugh, importing exports" but well, huh, that is what importers do, so maybe it makes sense?

Or send up the BIKESHED BAT-SIGNAL and let Lann come up with something.

Copy link
Copy Markdown
Collaborator Author

@fibonacci1729 fibonacci1729 Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think import works. We could also keep export for the remapping scenario you mention which is the point of it. All I meant was that if the LHS is an interface, it implies that an export with that name exists with that name so its technically equivalent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I.e. "a:b/c" = { export = none, ... } is equivalent to "a:b/c" = { export = "a:b/c", ... }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, I think I see what you mean now. Thanks!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could allow add a mutually exclusive --package/-p, --interface/-i.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, I forgot about plain names.


The `--export` flag accepts the same forms:

- **Specific interface:** `--export aws:client/s3@1.0.0`
Copy link
Copy Markdown

@rmarx rmarx May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: just read the bottom of the doc saying that multiple exports are not yet in-scope. Decided to leave the comment though, as I do need we can/should make it a bit clearer in this doc/eventual documentation that it's intended you can only add 1 interface at a time.

Just want to confirm if/how multiple exports work. I assume that

--export aws:client/s3@1.0.0,aws:client/sqs@1.0.0
and
--export aws:client/s3@1.0.0 --export aws:client/sqs@1.0.0

would both work? Or is it always just 1 export at a time?

If there are multiple possible at once, then some text above needs to be adjusted (e.g., "Which exportS should be used").

If however you can only do 1 interface at a time, that can probably also be made a bit clearer in the text/description that people would run multiple spin deps add commands for multiple exports, even if from the same package.

- **Package selector:** `--export aws:client@1.0.0` (selects all matching exports)
- **Plain name:** `--export my-export`

### Step 3: Select capability inheritance

The command inspects the dependency's imports and matches them against known capability sets (e.g. `allowed_outbound_hosts`, `ai_models`, `key_value_stores`) using semver-compatible matching. If the dependency requires any capabilities and `--inherit` is omitted, the user is prompted:

```
This dependency requires the following capabilities: allowed_outbound_hosts, ai_models

? Select capabilities to inherit from the parent component
> All
allowed_outbound_hosts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably these will be check boxes rather than a single-select?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed

ai_models
```

Selecting **"All"** sets `inherit_configuration = true` in the manifest. Selecting individual capabilities records them as a list (e.g. `inherit_configuration = ["allowed_outbound_hosts"]`). Selecting nothing results in no inheritance.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is that inherit_configuration = [] or simply no inherit_configuration?


#### Explicit `--inherit` flag

- `--inherit true` or `--inherit all` → inherits all capabilities
- `--inherit false` or `--inherit none` → inherits nothing
- `--inherit allowed_outbound_hosts,ai_models` → inherits only those capabilities
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or --inherit allowed_outbound_hosts --inherit ai_models, right? (that is, as well as not instead of)


### Step 4: Write to manifest and regenerate WIT

After all selections are made, the command:

1. Serializes the dependency into the `[component.<id>.dependencies]` table in `spin.toml`
2. Regenerates `spin-dependencies.wit` in the component's build directory
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this made me wonder what happens if a user wants to REMOVE a dependency instead of adding it (obviously not covered by spin deps ADD but still).

Is it just removing the line in the .toml and spin build picks that up and re-generates the spin-dependencies.wit to remove the dependency? Or do you need a separate command to regen spin-dependencies.wit?

In the first case: why would regeneration be part of spin deps add instead of just spin build?
In the second case: do we have a separate spin deps remove or something to help re-trigger?

3. Prints a confirmation message

```
Added aws:client@1.0.0 to component 'api-server'

NOTE: This dependency requires the following capabilities: allowed_outbound_hosts, ai_models
Copy link
Copy Markdown

@rmarx rmarx May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this printed only for capabilities that were not inherited?

Would be confusing if you explicitly chose --inherit all or --inherit ai_models and then still saw this kind of message at the end?

The example below does indicate this warning would be present if inheriting, so I think that's not ideal.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading it a bit more, it seems it's saying you might need to actually fill in ai_models on the PARENT component, even if you added it as --inherit here.

Conceptually that makes sense, but it's confusing imo.

Is there a way to check if the parent component has the capabilities already filled out and only print this type of warning if it doesn't?

You may need to add configuration for these capabilities to your component.
```

## End-to-End Examples

### Fully interactive

```
$ spin deps add aws:client@1.0.0

? Which component should the dependency be added to?
> api-server

? Which package should be used?
> aws:client@1.0.0

? Which export should be used?
> aws:client/s3@1.0.0

This dependency requires the following capabilities: allowed_outbound_hosts

? Select capabilities to inherit from the parent component
> allowed_outbound_hosts

Added aws:client/s3@1.0.0 to component 'api-server'

NOTE: This dependency requires the following capabilities: allowed_outbound_hosts
You may need to add configuration for these capabilities to your component.
```

### Fully non-interactive

```
$ spin deps add aws:client@1.0.0 \
--to api-server \
--export aws:client/s3@1.0.0 \
--inherit allowed_outbound_hosts

Added aws:client/s3@1.0.0 to component 'api-server'

NOTE: This dependency requires the following capabilities: allowed_outbound_hosts
You may need to add configuration for these capabilities to your component.
```

### Local component with all capabilities

```
$ spin deps add ./my-component.wasm --to worker --export my-export --inherit all

Added my-export to component 'worker'
```

### HTTP source

```
$ spin deps add https://example.com/component.wasm \
--digest abc123... \
--to dashboard \
--export foo:bar/baz@0.1.0 \
--inherit false

Added foo:bar/baz@0.1.0 to component 'dashboard'
```

## Resulting Manifest Entries

The command produces entries in `spin.toml` matching the schema defined in [SIP 020](docs/content/sips/020-component-dependencies.md) and the per-dependency `inherit_configuration` field introduced in [SIP 023](docs/content/sips/023-granular-capability-inheritance.md):

```toml
# Package-level selector with full inheritance
[component.api-server.dependencies]
"aws:client@1.0.0" = { version = "=1.0.0", package = "aws:client", inherit_configuration = true }

# Specific interface with selective inheritance
[component.api-server.dependencies]
"aws:client/s3@1.0.0" = { version = "=1.0.0", package = "aws:client", inherit_configuration = ["allowed_outbound_hosts"] }

# Local dependency with no inheritance
[component.worker.dependencies]
"my-export" = { path = "my-component.wasm" }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"my-export" = { path = "my-component.wasm" }
"my-export" = { path = "./my-component.wasm" }

Nit for consistency


# HTTP dependency
[component.dashboard.dependencies]
"foo:bar/baz@0.1.0" = { url = "https://example.com/component.wasm", digest = "sha256:abc123..." }
```

## Capability Detection

The command detects required capabilities by inspecting the dependency's component-level imports and matching them against the capability sets defined in [SIP 023](docs/content/sips/023-granular-capability-inheritance.md) using **semver-compatible** matching (via `wac_graph::types::are_semver_compatible`). This means a dependency importing `wasi:http/outgoing-handler@0.2.7` correctly matches the `allowed_outbound_hosts` capability set even though the set is defined with `@0.2.6`.

The recognized capability sets are:

| Capability | Example interfaces |
|---|---|
| `ai_models` | `fermyon:spin/llm` |
| `allowed_outbound_hosts` | `wasi:http/outgoing-handler`, `wasi:sockets/tcp`, `fermyon:spin/mqtt` |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasi:http/client, spin:postgres, fermyon:spin/mysql, spin:mqtt, Redis itfs

| `environment` | `wasi:cli/environment` |
| `files` | `wasi:filesystem/preopens` |
| `key_value_stores` | `fermyon:spin/key-value` |
| `sqlite_databases` | `fermyon:spin/sqlite` |
| `variables` | `fermyon:spin/variables` |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these have spin:* forms as of Spin 4 (actually SQLite as of Spin 3.2 I think)


## Potential Future Work

### Multiple selections within a single package

The current design allows selecting either **all** exports from a package or a **single** specific interface. A natural extension would be to support selecting **multiple** (but not all) interfaces from the same package in a single invocation. For example, a multi-select prompt could allow the user to pick both `aws:client/s3@1.0.0` and `aws:client/dynamodb@1.0.0` without selecting the entire `aws:client@1.0.0` package. This would generate one dependency entry per selected interface and avoid requiring the user to run `spin deps add` multiple times for the same package.
Loading