feat(cli): add feedback add and delete commands for quick CLI feedback - #5988
feat(cli): add feedback add and delete commands for quick CLI feedback#5988kanadgupta wants to merge 29 commits into
Conversation
The vendored effect clone in .repos/ drowns out workspace results in editor-wide search. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This reverts commit 8c53f2e.
LegacyCliConfig.projectId is a bare SUPABASE_PROJECT_ID env passthrough, so the feedback submission's project_ref was null in a linked project unless that env var happened to be set. Fall back to <workdir>/supabase/.temp/ project-ref, the file supabase link writes, mirroring the soft-load half of LegacyProjectRefResolver.resolveOptional. The file is read directly rather than through the resolver so the command keeps working unauthenticated; a broken ref file degrades to unlinked instead of failing the submission. The previous integration test injected projectId straight into the config mock, so it only proved the handler forwarded the field and never exercised resolution -- despite being named for the workdir-linked scenario that did not work. Replace it with coverage that seeds the real file, plus env precedence, unlinked, and unreadable-file cases. Also correct the AGENTS.md row claiming LegacyCliConfig reads project-id from config.toml, which is what made this field look project-aware. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 656f13a667
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Supabase CLI previewnpx --yes https://pkg.pr.new/supabase/cli/supabase@d8a427eb17f081911ac3246cae480174b44ba5abPreview package for commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 830e565f2a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae5d202bd6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…alias Restructures the TS-only feedback command from a single `supabase feedback` command (with a `btw` alias) into a `feedback` group with an `add` subcommand, following the nested-subcommand layout. Telemetry now records `command: "feedback add"`; behavior is otherwise unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…07-22/feedback-command # Conflicts: # apps/cli/src/legacy/commands/functions/delete/delete.integration.test.ts # apps/cli/src/legacy/commands/functions/download/download.integration.test.ts # apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ca265f84d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- `feedback add` now calls the SECURITY DEFINER `submit_interfaces_feedback` RPC (the upstream table no longer allows direct inserts) and surfaces the server-issued delete token exactly once in every output format - new `feedback delete <token>` previews the feedback text, confirms (`--yes`/`SUPABASE_YES` to skip; machine modes fail loudly without it), and hard-deletes via the token-gated RLS policy (`x-feedback-token` + optional `x-feedback-project-ref` context header) - reshape the shared `FeedbackSubmitter` service into `FeedbackClient` (submit/preview/delete) and hoist the shared feedback layers and project-ref resolver to the command family root CLI-1946, CLI-2188 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eedback-submission-e37650 # Conflicts: # apps/cli/docs/go-cli-porting-status.md
## Summary Attach the gotrue user UUID to `supabase feedback add` submissions via the `submit_interfaces_feedback` RPC's optional `user_id` parameter, mirroring the project-ref convention: best-effort and never failing the submission. The id is sourced from the persisted telemetry identity (distinct_id in ~/.supabase/telemetry.json, stamped at login) — a synchronous in-memory read, so the command keeps its zero-auth posture and works logged-out (user_id omitted). Submit-side attribution is gated on telemetry consent: opted-out users submit anonymously. No deviceId fallback — user_id is semantically a gotrue UUID, not an anonymous device id. Because the `interfaces_feedback` RLS policies require a matching `x-feedback-user-id` header to read or delete a row that was submitted with a `user_id`, `feedback delete` now presents the persisted id on both the preview and the delete requests. Unlike submission, the header is **not** consent-gated — it is functional auth context, and gating it would strand rows submitted before a consent opt-out. Logged-out runs omit the header, which still matches all anonymous rows. Stacked on #5988 (base: `kanad-claude-2026-07-22/feedback-command`); this is a clean reapplication of the change from #5998 onto the RPC-based `FeedbackClient`. ## Linked issue closes [CLI-2008](https://linear.app/supabase/issue/CLI-2008) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-conflicts-develop-2fe0dd # Conflicts: # apps/cli/AGENTS.md
Applies the accepted Codex review findings on the feedback family: - Abort in-flight feedback requests on fiber interruption: the client's run helper now threads Effect.tryPromise's interruption signal into every postgrest call via AbortSignal.any with the 10s timeout, so Ctrl-C can no longer let a submit commit after cancellation. - Gate the interactive prompt on stdin.isTTY in addition to output.interactive, so whitespace-only piped stdin with a TTY stdout fails with the documented empty-message error instead of opening a prompt against exhausted stdin. - Honor the --agent yes|no override in the submission payload via legacyResolveAgentMode (hoisted from db query to legacy/shared); --agent no also suppresses the detected agent_name. - Refresh ~/.supabase/telemetry.json on every feedback add/delete run via the standard Effect.ensuring(telemetryState.flush) finalizer. - Honor -o json on both commands (machine payload via encodeGoJson, stdout payload-only); values outside feedback's pretty|json enum are rejected pre-run like db query's restricted set. yaml/toml stay unsupported: the struct-spec encoders reproduce Go field names and no Go struct exists for this TS-only command. - Route feedback HTTP through the legacy transport: a composed fetch wires --debug request logging and --dns-resolver https DoH resolution into the supabase-js client. - Move the real-backend add→delete round trip to the gated live tier (add.live.test.ts) and keep a hermetic e2e for subcommand routing via the no-network empty-message path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const FEEDBACK_STAGING: FeedbackEnvironment = { | ||
| url: "https://imrwaufzgcaczqmpnxyr.supabase.co", | ||
| key: "sb_publishable_puOyAlqG5J_XfBMTDM2Ckw_L5mieFdb", | ||
| }; | ||
|
|
||
| // No dedicated production feedback project exists yet (CLI-1946): production | ||
| // intentionally reuses the staging values until one is provisioned. | ||
| const FEEDBACK_PRODUCTION: FeedbackEnvironment = { ...FEEDBACK_STAGING }; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8a427eb17
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; | ||
| // The logger's write is synchronous (a gated stderr write); running it | ||
| // at this plain-fetch boundary keeps the wrapper a `typeof fetch`. | ||
| Effect.runSync(logger.http(method, url)); |
There was a problem hiding this comment.
Redact deletion tokens from debug URLs
When feedback delete <token> --debug runs, both the preview and DELETE requests encode the capability in the URL as delete_token=eq.<token>, and this call writes that entire URL to stderr. This exposes the token—and therefore authority to read and delete potentially sensitive feedback—in terminal recordings, CI logs, or debug output shared with support; redact this query parameter only for logging while sending the original URL to the transport.
AGENTS.md reference: apps/cli/AGENTS.md:L318-L318
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Sounds like an actual risk.
Maybe strip or redact the query string in legacyFeedbackFetch before logger.http(...) the Management API debug logger never has this problem because its credentials ride in headers.
| const deleted = await runSupabaseLive( | ||
| [ |
There was a problem hiding this comment.
Clean up live feedback in a finally block
If the add succeeds but JSON parsing, an assertion, or the subsequent delete invocation fails, execution never completes this cleanup and the persistent staging row remains. Fresh evidence in the current revision is that deletion is still only a sequential test step after assertions rather than guaranteed teardown; retain the returned token and perform an exact best-effort delete in finally so failing live runs do not pollute staging.
AGENTS.md reference: AGENTS.md:L170-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid but minor, a leaked row only happens on assertion failure between the add and the delete, staging-only, and the message self-identifies as test data. Fine to wrap the delete in a finally for hygiene, but I wouldn't block on it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8a427eb17
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| */ | ||
| export function legacyFeedbackFetch(options: LegacyFeedbackFetchOptions): typeof globalThis.fetch { | ||
| const { dnsResolver, logger } = options; | ||
| const dohFetch = legacyDohFetch({ dnsResolver, innerFetch: options.innerFetch }); |
There was a problem hiding this comment.
Preserve headers in DoH feedback requests
When --dns-resolver https is used, this wrapper delegates supabase-js requests to legacyDohFetch, but supabase-js supplies init.headers as a Headers instance and legacy-http-dns.ts rebuilds it with { ...init?.headers, Host: ... }; spreading Headers yields no entries. The resulting submit, preview, and delete requests lose apikey, content type, and the x-feedback-* capability headers, so feedback operations fail in the exact environments that need the DoH fallback. Fresh evidence beyond the earlier transport-wiring comment is this incompatibility between the newly wired supabase-js transport and the existing DoH header reconstruction; clone with new Headers(init.headers) and set Host instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
the ...init?.headers spread in legacyDohFetch preserves apikey/x-feedback-token as-is. Not an issue for this client
avallete
left a comment
There was a problem hiding this comment.
One thing to fix before merge (the open Codex P1 on debug URLs, commented on that thread), one doc fix (PR description still says user_id is never sent), and a couple of non-blocking notes inline.
the PR description says "The access token is never sent; user_id is never sent", but the current code sends the consent-gated gotrue UUID as user_id (add.handler.ts:91), and SIDE_EFFECTS.md documents that correctly. Since the description is what privacy sign-off reads, could you update that bullet to match?
| const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; | ||
| // The logger's write is synchronous (a gated stderr write); running it | ||
| // at this plain-fetch boundary keeps the wrapper a `typeof fetch`. | ||
| Effect.runSync(logger.http(method, url)); |
There was a problem hiding this comment.
Sounds like an actual risk.
Maybe strip or redact the query string in legacyFeedbackFetch before logger.http(...) the Management API debug logger never has this problem because its credentials ride in headers.
| */ | ||
| export function legacyFeedbackFetch(options: LegacyFeedbackFetchOptions): typeof globalThis.fetch { | ||
| const { dnsResolver, logger } = options; | ||
| const dohFetch = legacyDohFetch({ dnsResolver, innerFetch: options.innerFetch }); |
There was a problem hiding this comment.
the ...init?.headers spread in legacyDohFetch preserves apikey/x-feedback-token as-is. Not an issue for this client
| const deleted = await runSupabaseLive( | ||
| [ |
There was a problem hiding this comment.
Valid but minor, a leaked row only happens on assertion failure between the add and the delete, staging-only, and the message self-identifies as test data. Fine to wrap the delete in a finally for hygiene, but I wouldn't block on it.
| const config = { | ||
| message: Argument.string("message").pipe( | ||
| Argument.withDescription( | ||
| "Freeform feedback. Bare words are joined with spaces. 1000 character limit.", |
There was a problem hiding this comment.
The char limit is documented here but only enforced server-side, so an over-limit message surfaces as a raw PostgREST error classified externalNetwork, so a user mistake gets counted as a backend failure in the actionability KPIs. A client-side length check that fails with an invalidInput classified error before any request would be nice. Non-blocking if you'd rather do it as a follow-up.
What
Adds a TS-only
supabase feedbackcommand family (from the original brainstorm) to the legacy shell so users — and agents — can send quick, low-friction feedback to the Supabase team without filing a GitHub issue, and revoke a submission later (e.g. an accidentally pasted secret):Part of CLI-1946; the delete command is CLI-2188. Scope evolved in this thread:
feedback add(nobtwalias) plus a token-based delete path, rather than full user-scoped CRUD.How
feedback addworks--sentinel.SECURITY DEFINERRPCsubmit_interfaces_feedback(feat: table for collecting interfaces feedback supabase#48420) viasupabase-js— the table has no insert grant, so the RPC is the only door and the delete token is always server-generated. The committed key is a publishable (anon) key, safe to ship in the binary. 10s timeout.delete_tokenexactly once. Text mode prints it with a "to delete this later" hint;json/stream-jsoncarry it asdelete_tokenin the result payload. The CLI never persists it.is_agent/agent_namevia@vercel/detect-agent, to support the activation analysis in AI-961), and the linked project ref.metadata.source: "cli"distinguishes CLI rows from the future MCP path. The access token is never sent;user_idis never sent.SUPABASE_PROJECT_ID→<workdir>/supabase/.temp/project-ref(the filesupabase linkwrites) → omitted. Reads the file directly (not viaLegacyProjectRefResolver, whose prompt path needs the platform API) so feedback works logged-out; a broken ref file degrades to "unlinked".How
feedback delete <token>worksPermanently delete this feedback? [y/N]);--yes/SUPABASE_YESskips it. Machine modes (json/stream-json) fail loudly without--yesrather than deleting silently — same contract aslogout.DELETEwithPrefer: count=exact; the CLI verifiesContent-Rangereports exactly one row. Authorization is thex-feedback-tokenrequest header matched by RLS — thedelete_token=eq.URL filter only satisfies PostgREST's filterless-delete rejection.x-feedback-project-refheader. The delete command resolves the ref as--project-ref→SUPABASE_PROJECT_ID→ linked-ref file and always sends whatever resolves (extra context against a context-free row is ignored server-side).{ "feedback": "...", "message": "Feedback deleted." }.Privacy note for reviewers
The feedback message, the delete token, and the
--project-refvalue go only to the feedback backend — never to PostHog. Message and token are positional arguments, whichextractChangedFlagNamesstructurally excludes from theflagstelemetry property;--project-refis recorded by name only with its value redacted. Regression tests assert none of them appear in captured analytics events.Reviewer-relevant context
FeedbackSubmitter(insert-only) intoFeedbackClient(submit/preview/delete) insrc/shared/feedback/feedback-client.{service,layer}.ts, and the profile→environment mapping and cli-config layer wiring were hoisted to the feedback family root (feedback.layers.ts,feedback-project-ref.ts) now that two commands share them.src/shared/feedback/database.types.tsis generated (supabase gen types) and excluded from formatting/knip.--profile supabase-staging), which also cleans up its own row each run.developpicked up the CLI-1970 docs restructure: the feedback commands are recorded indocs/go-cli-divergences.md(TS-only section) and registered inlegacy-docs-spec.tables.ts(other-commandstag) instead of the old porting-status tracker.LegacyCliConfig.projectId: it is a bareSUPABASE_PROJECT_IDenv passthrough — it does not readconfig.tomlor the linked-project file, so it isNonein a linked project unless that env var is set. An earlier revision of this branch used it directly as "the linked project ref", which meantproject_refwas alwaysnullin practice. TheAGENTS.mdrow that described it as resolving project-id fromconfig.tomlis corrected here, since that phrasing is what made the field look project-aware.services.integration.test.tsnow uses an isolated temp workdir instead ofprocess.cwd(), fixing machine-dependent behavior when the developer has localsupabase startstate.🤖 Generated with Claude Code