-
Notifications
You must be signed in to change notification settings - Fork 302
SIP-024: spin deps cli dx #3453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | | ||||||
|
|
||||||
| ### Options | ||||||
|
|
||||||
| | Flag | Description | | ||||||
| |------|-------------| | ||||||
| | `--to <component-id>` | Target component to add the dependency to. Prompted if omitted and the app has multiple components. | | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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. | | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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., In this setup, it's a bit weird given that you're adding dependencies TO something. In |
||||||
| | `--export <name>` | Export to use from the dependency. Prompted if omitted and the component has multiple exports. | | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| | `-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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
|
|
||||||
| #### 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: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Possible alternative approaches:
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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had a similar feeling when seeing the CLI argument is called However, since I'm Kind of like a github PR should really be a Merge Request instead? :D (imo) Either way, if we keep 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 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
oh wait, this is the interface on the left hand side, not the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I was initially going to suggest Or send up the BIKESHED BAT-SIGNAL and let Lann come up with something.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I.e.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha, I think I see what you mean now. Thanks!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could allow add a mutually exclusive --package/-p, --interface/-i.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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 |
||||||
| - **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 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Presumably these will be check boxes rather than a single-select?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is that |
||||||
|
|
||||||
| #### 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 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or |
||||||
|
|
||||||
| ### 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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Is it just removing the line in the .toml and In the first case: why would regeneration be part of |
||||||
| 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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The example below does indicate this warning would be present if inheriting, so I think that's not ideal. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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" } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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` | | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| | `environment` | `wasi:cli/environment` | | ||||||
| | `files` | `wasi:filesystem/preopens` | | ||||||
| | `key_value_stores` | `fermyon:spin/key-value` | | ||||||
| | `sqlite_databases` | `fermyon:spin/sqlite` | | ||||||
| | `variables` | `fermyon:spin/variables` | | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of these have |
||||||
|
|
||||||
| ## 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. | ||||||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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