Repository navigation
Conversation
alukach
added this pull request to stack #25
October 9, 2026 19:20
alukach
force-pushed
the
feat/product-commands
branch
from
October 9, 2026 19:38
0102eef to
bbe48fe
Compare
This was referenced Oct 9, 2026
alukach
added a commit
to source-cooperative/source.coop
that referenced
this pull request
Oct 13, 2026
Part of #588. The CLI half is source-cooperative/source-coop-cli#23. The API now takes an Ory access token as a bearer, alongside the assertions the data proxy signs, so the CLI can call `/api/v1` as the person who logged in. `getApiSession` routes a bearer by its unverified `iss`: Ory's tokens go to the new `authenticateWithOryAccessToken`, and the proxy's go to the existing verifier. Neither falls back to the other or to the session cookie. The Ory verifier checks the token against Ory's published JWKS, with the issuer (with or without a trailing slash), RS256, expiry, a 30-second tolerance, and the request's origin as audience. An ID token, which Ory issues to the client ID, never passes, and which clients may mint a token for the API is decided in Ory by the audiences each client is allowed. The subject is looked up only as a person's Ory identity. Both verifiers build the session in one place, `sessionForAccount`. The issuer is `NEXT_PUBLIC_ORY_SDK_URL`, which is each Ory project's issuer domain. **Deploying:** The CLI's Ory clients need the `jwt` access-token strategy and this site's origin as an allowed audience. Until a client has both, the API answers that environment's Ory tokens with 401. Nothing else breaks: only newer CLI builds send Ory tokens, so merging first is safe. | | Staging client (`a79c9537…`) | Production client (`197e20e7…`) | |---|---|---| | Allowed audience | `https://staging.source.coop` ✅ | `https://source.coop` ✅ | | `access_token_strategy: jwt` | ✅ set 2026-10-13 | ✅ set 2026-10-13 | **Changing a client** (done for both; kept for reference and rollback): `ory patch oauth2-client` doesn't exist, and `ory update oauth2-client` replaces the whole client. So export the client, change one field, and send the whole thing back: ```bash P=d567800d-b705-49d1-8a28-6f92f3a4c053 C=197e20e7-d52d-4d1d-9e54-4b73a342034b ory get oauth2-client $C --project $P --format json > prod-cli-client.before.json jq '.access_token_strategy = "jwt"' prod-cli-client.before.json > prod-cli-client.jwt.json ory update oauth2-client $C --project $P --file prod-cli-client.jwt.json ory get oauth2-client $C --project $P --format json | jq .access_token_strategy # "jwt" ``` Rollback: `ory update oauth2-client $C --project $P --file prod-cli-client.before.json`. - **No Vercel changes.** Each deployment's `NEXT_PUBLIC_ORY_SDK_URL` is already the issuer in its tokens: `https://auth.staging.source.coop` on staging and previews (checked against a real staging token), `https://auth.source.coop` on production. A local API can't accept CLI tokens at all, since no CLI client allows a localhost audience. - Merging deploys staging. Check with a staging CLI build: `source-coop login`, then `source-coop api whoami --api-url https://staging.source.coop` should return your account, and `creds` with `aws s3 ls` should still work. - The next release deploys production. Repeat the checks against `https://source.coop`. The release ships everything on `main`: if #651 is in it, data.source.coop#253 must be deployed first. - Rollback is a revert: only Ory-token calls are affected. **Docs:** - **ADR-005:** amended in source-cooperative/data.source.coop#254. It said the API authenticates proxy-signed tokens; it now records the second signer. - **OpenAPI:** the `bearerAuth` description names `source-coop login` and `source-coop auth token`. - **`public/llms.txt`:** checked. Its line ("a token from the CLI's login or a service account") still holds. - **docs.source.coop:** checked. No page describes calling the API with a token yet. ## Testing - `npm run type-check` passes. - `npx jest src/app/api src/lib`: 535 passed across 53 suites, including new suites for the Ory verifier and for `getApiSession`'s routing. - `next lint` on the changed files reports nothing. - Against real Ory (staging, 2026-10-13): after the CLI client was switched to `jwt`, a `source-coop login` token was an RS256 JWT with `iss` `https://auth.staging.source.coop` and `aud` `["https://staging.source.coop"]`, and empty `ext`. That matches what the verifier checks. The API call itself (`source-coop api whoami` against staging) waits for this to deploy, since a preview's origin isn't an allowed audience. ## Related - CLI login session, which sends these tokens: source-cooperative/source-coop-cli#23 - CLI product commands: source-cooperative/source-coop-cli#24 - ADR-005 amendment: source-cooperative/data.source.coop#254 - Deployment runbook: in source-cooperative/source-coop-cli#23 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adds `source-coop product`, the first gh-style command group from #21, over the `/api/v1/products` endpoints from source-cooperative/source.coop#651. A small API client sends the request and turns the API's `{"error": {code, message, field_errors}}` body into a message; the CLI does no validation of its own. Requests carry the login session's access token (or `SOURCE_TOKEN`, if set), and run signed out without one, which is enough to read public products. `create` and `edit` take fields from flags, from a JSON file (`--from-file`), or both. When stdin and stderr are terminals, whatever is still missing is prompted for with defaults: a title from the product ID, the data connections the account can use, and the visibilities the chosen one allows. `edit` with no flags prompts from the product's current values and sends only what changed. `delete` asks whether to keep the data and for the name to be typed back; without a terminal it needs `--yes`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF
When the API rejects a field and someone is at the terminal, the CLI shows why and asks for just that field again, keeping every other answer, then resends; the API stays the only judge. A rejection naming a field nobody can be asked for is an error, as before. Descriptions can be written in `$VISUAL`/`$EDITOR`, gh's way: Enter keeps, `e` opens the editor, anything else is the text. `edit` with no flags first asks which fields to change. `list` prints a header and aligned columns on a terminal, and tab-separated rows when piped. `create` and `edit` print the product URL on stdout and their message on stderr. A request that gets no response now says why (DNS, TLS, refused connection), not just which URL. `source-coop api PATH` sends any `/api/v1` request as the signed-in user, with gh-style `-X`, `-f`, `-F` and `--input`, prints the response, and exits non-zero on an error status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF
Rebased onto per-environment caching, which keys the session by proxy host; `product` and `api` now take --proxy-url / SOURCE_PROXY_URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alukach
force-pushed
the
feat/product-commands
branch
from
October 13, 2026 22:41
bbe48fe to
dfcfaae
Compare
|
Claude finished @alukach's task in 22s —— View job ✅ No blocking issues — safe to merge. I read
Minor, not blocking:
Simplify (ponytail)
💰 Estimated review cost: $0.16 · 0m22s · 6 turns |
product list takes --cursor and, on a terminal, prints the cursor for the next page. It no longer truncates to -L, since a search page can hold more than asked for and the cursor resumes after the whole page. product view shows one labelled row per field (visibility, status, data mode, featured, account, dates, URL) with colour on a terminal, honouring NO_COLOR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a terminal, without --json, `product list` streams each page into $SOURCE_PAGER, $PAGER or `less -FRX` as it is fetched. The pipe's backpressure holds off fetching until the user scrolls, so without an explicit -L the list goes on to the end; quitting the pager ends the command quietly. Column widths come from the first page. An empty pager or `cat`, --no-pager, or a pager that won't start prints directly as before. Piped and --json output are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
product list now shows each product's fields as view does, with a rule between products, streamed through the pager like the table. --format table keeps the aligned table (tab-separated when piped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tags live under metadata.tags; empty entries (some records hold [""]) are skipped. data_mode is the legacy field visibility replaced, so it no longer gets a row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… drafts create and edit ask GET /whoami before their first question, so a missing or refused token stops them before anyone types a form. If sending the answers still fails, they're saved as a draft, and the next run of the same command offers them back (flags still win) or takes them with --from-file. Drafts live where the OS keeps app state, not its cache: XDG_STATE_HOME on Linux, Application Support on macOS, LocalAppData on Windows. src/draft.rs is resource-agnostic: a command calls resume() before prompting and finish() with the send's result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alukach
added a commit
that referenced
this pull request
Oct 13, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: product browse, a picker over the product list browse lists products in a type-to-filter picker (dialoguer's FuzzySelect), with "Load more…" while the listing has pages left. A picked product is shown as view shows it, with Back, Open in browser, Edit and Delete, which run the same code as those commands; a failed action is reported and the menu offered again. list and browse share their filters (account, -q, --tags, --featured) through a flattened Filters struct, and one page fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: pick products with product list -i, not a browse command One way to the picker: -i on list takes the same filters and refuses the printing-only flags (--json, --format, -L, --cursor). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: start the list -i server through api_server, as #24's tests do Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
-i now means interactive wherever a product is involved: - list -i moves through products with a picker of our own (on console, which dialoguer already brings in), since dialoguer's take no shortcuts: arrows or j/k move, → or Enter opens, ← Esc or q quits, / filters, and b, e, d open in the browser, edit or delete the highlighted product. - view PRODUCT -i shows the product with the same b/e/d shortcuts and ← to leave; without a product it picks one from public products first. - edit -i and delete -i pick one of the signed-in account's products (from /whoami), then run as if it had been named. Edit and delete run the commands' own code. The fuzzy-select feature is no longer needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- api: refuse URLs outside the API's origin, so the bearer token can't be sent to another host - cache: create cache files 0600 before writing them - picker: read keys raw so Ctrl-C restores the cursor; fit every line to the terminal so redraws don't wrap - session: no access token from a session that asked for no audience - .env.staging: point SOURCE_API_URL at staging - product: report a failed re-fetch after an edit instead of ending -i; show the --cursor hint after a pager that stopped at -L; convert dates to UTC before labelling them so - draft: clear a draft sent back with --from-file - main: an empty SOURCE_TOKEN is unset - read_fields reuses api_cmd::read_json Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pick and choose were the same loop over a Listing; browse_products does both, with a choosing flag. The -i arms in run and view use let-else, and keypress no longer reports an Open nobody acted on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rror exit Esc (or q) on a select, confirm or multi-select returns prompt::CANCELLED from the Prompter itself, so every command and record type gets it. The type-the-name step before something irreversible is prompt::confirm_by_name, where Enter on nothing backs out; product delete is its first user and now checks sign-in before asking. main's fail() reports every command's error, printing a cancel without "Error:". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prompt::report holds the rule main's fail used; act in the -i picker uses it too, so Esc in an edit or delete prompt reads as a cancel there as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared src/style.rs paints a record's ID (account/product) bright black, lighter than the name beside it, in the details view, the table and the -i picker, and holds the one NO_COLOR/terminal rule every output uses. The table pads the ID before painting it, so columns still line up; the picker filters on and highlights an item's plain text, so styling neither matches a filter nor cuts the highlight short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As the highlight comes within three items of the end of what's loaded (or a filter matches nothing), the picker shows "Loading more…" and returns Action::More; the caller fetches the next page and browses again from a prompt::Place, which keeps the highlight, the filter and the lines on screen, so the list grows in place. A filter thus searches on through later pages. The caller keeps one Place for the session, so coming back from a product also keeps your position and filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-i now takes the terminal's alternate screen, as less and vim do: each list and product screen clears and redraws instead of piling up, and leaving restores the terminal as it was (also on Ctrl+C mid-request). What a shortcut did (Edited, Deleted, Cancelled, an error) shows once on the redrawn screen as a notice instead of scrolling away. In the list of all public products, Enter on a / filter searches the server (q, keeping --tags/--featured) and browses the results; Back returns to the whole list. Typing still narrows what's loaded at once, and account listings, which the API can't search, keep that filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
view (and list's details) now shows every field, saying (none) or no rather than leaving a row out: Disabled and Featured as yes/no, Tags, each mirror's connection and prefix with the primary first and marked, and Created/Updated with how long ago. The -i list's rows end with the product's visibility, colored as in view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list's shortcuts are now only b (browser): editing or deleting takes opening the product first, so a stray key on a long list changes nothing, and its hint says so. The scripted prompter refuses a key the list wasn't given. The README's key table drops e/d from lists and describes the server search, paging and full screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dialoguer's text prompts ignore Esc, so editing a title or description had no way out. picker::line replaces them: the default starts in the line to keep or edit, ←/→, Home/End and Ctrl+A move, Backspace/Delete erase whole characters (accents, emoji), a line wider than the terminal scrolls sideways, empty is refused unless allowed, and Esc returns CANCELLED, so every text question in every command can be abandoned. Long text opens $VISUAL/$EDITOR on Ctrl+E rather than on answering "e", which made "e" itself unsayable. console reads Ctrl+E as End, so at the end of the line it opens the editor and elsewhere moves there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create, asked interactively, offers only the accounts source.coop lets you create products under, read from /whoami as its createRepository rule decides: your own with create_repositories, and each organization you're an org-wide owner or maintainer of (admins may also name another). With one, it says so and goes straight to the product ID; with none, it explains why before asking anything. check_signed_in returns the /whoami session it fetches. Ctrl+C prints "Interrupted." once, without "Error:", and exits 130. dialoguer's choices have console raise it as SIGINT, which ended the process with their cursor hidden; one handler now restores the cursor (and leaves the full screen) for every interactive command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In create's prompts, ↑ (or ← at the start of a line) in a text question and ← in a list go back to the question before, which starts from the answer given there; later answers are kept. Questions answered by a flag, --from-file or a resumed draft are skipped both ways, and the draft is offered once per product named. - prompt: input_or_back, select_or_back and long_text_or_back, opt-in, defaulting to the plain prompts; scripted Answer::Back - picker: line takes back; a small select of our own, since dialoguer's can't tell ← from Esc (Esc still cancels) - product: ask_create runs create's questions as steps and replaces ask_new_product and prompt_new_product Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once create has asked anything, it shows the product, title, first line of the description, data connection and visibility, and asks "Create this product?" (Yes / No, cancel). ← goes back to the last question; declining cancels with nothing sent. A run given every field by flag or file goes straight through, as before. The summary is drawn as part of the question (picker::select takes details), so going back clears it with the question. Prompter::confirm_or_back carries it, and the scripted prompter records it. The README describes going back and the review, and Ctrl+E for the editor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le ones AGENTS.md collects how the CLI behaves (commands, input, prompts and going back, the -i picker, output, stored data, testing, git), each rule pointing at the code that already does it; CLAUDE.md imports it. It leads with keeping README.md in sync with every user-visible change. Two tests enforce what can be checked: every resource command but create takes -i and the reads take --json, and every command appears in README.md as `source-coop <command>`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`auth login` is now where you log in, beside `auth token`; the top-level `login` still runs it but is hidden from --help. Every "run 'source-coop login'" hint, the README and .env.staging say `auth login`. AGENTS.md records that signing-in commands live under `auth`, that `creds` stays top-level for AWS credential_process configs, and that renamed commands keep their old names as hidden aliases. The README check skips hidden commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alukach
marked this pull request as ready for review
October 14, 2026 18:27
This was referenced Oct 14, 2026
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #21, stacked on the login-session PR. Builds on the product endpoints in source-cooperative/source.coop#651.
source-coop product list | view | create | edit | deleteover/api/v1/products. As #21 asks, the CLI checks nothing itself and shows the API's errors field by field.SOURCE_TOKENif set. Without either they run signed out, which is enough to read public products, and a 401 says to runsource-coop login.listfollows paging up to-L(default 30). Reads take--json, andview --webopens the product page.createandedittake fields as flags, from a JSON object with-F/--from-file(-for stdin), or both, with flags winning.SOURCE_PROMPT_DISABLEDis unset), the CLI asks for whatever is still missing:create: a title defaulting to one made from the ID, a data connection from those the account can use, and only the visibilities that connection allows.$VISUAL/$EDITORby answeringe, as in gh.editwith no flags: asks which fields to change, and starts each from its current value.delete: asks whether to keep the data, then for the product name typed back. Without a terminal it needs--yes.listprints a header and aligned columns on a terminal, and tab-separated rows without a header when piped.createandeditprint the product URL on stdout and their message on stderr.source-coop api PATHsends any/api/v1request as the signed-in user, for whatever the other commands don't cover yet:-Xfor the method,-f key=valuefor a string,-F key=valuefor a JSON boolean, null or number, and--input FILE.Follow-ups, not in this PR:
--jq./data-connectionsroute itself, and doesn't know about connections that require an account flag.Deploying: needs the login-session PR, and source.coop#651 deployed. See the runbook in {{CLI_LOGIN_PR}}, steps 4 and 6.
Testing
cargo fmt --check,cargo clippy --all-targets -- -D warningsandcargo test(58 passed) all pass.apitests run against wiremock, using the request shapes from #651's routes. They include asking again for just the rejected field, a rejection of another field staying an error, the field picker, the piped table format, andapi's query and body handling.product listandapisent the session's refreshed access token.apiexited 1 on a 404.Related
🤖 Generated with Claude Code
https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF