feat(cli): add supabase notebooks pull - #6598
Conversation
425d460 to
dafd923
Compare
dafd923 to
8812be5
Compare
8812be5 to
7635c7e
Compare
7635c7e to
771cf16
Compare
There was a problem hiding this comment.
🤖 AI Review
After verifying all 14 submitted findings against the checked-out code and trusted conventions, I deduplicated them into 13 findings: 11 confirmed and 2 refuted. The confirmed issues include one critical symlink-containment vulnerability, one major Unicode filename-collision/data-loss risk, an output-precedence bug, a rename race, and several minor documentation, portability, sanitization, testing, and maintainability concerns. The claimed missing generated API client is refuted because the required contracts and operations already exist in the workspace package; the claimed duplicate-name bug for explicit ID pulls is also refuted because that path deliberately identifies the remote notebook by ID and is documented as an explicit overwrite operation.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🔴 CRITICAL | apps/cli/src/commands/notebooks/notebooks.shared.ts:226 |
security |
codex | A symlinked supabase/notebooks directory can redirect notebook reads and writes outside the project root. |
| 🟠 MAJOR | apps/cli/src/commands/notebooks/notebooks.shared.ts:157 |
data-loss |
codex | The portable filename collision key misses Unicode case-fold aliases, allowing explicit replacement to overwrite an aliased local notebook on some case-insensitive filesystems. |
| 🟡 MINOR | apps/cli/src/commands/notebooks/notebooks.shared.ts:113 |
security |
claude | Several errors interpolate API-provided notebook names without applying the module's terminal-safe name sanitizer. |
| 🟡 MINOR | apps/cli/src/commands/notebooks/pull/SIDE_EFFECTS.md:12 |
documentation |
claude | The side-effect contract omits credential/project-resolution reads and the linked-project cache write, and incorrectly says the notebooks directory is read only for broad pulls. |
| 🟡 MINOR | apps/cli/src/commands/notebooks/notebooks.shared.ts:148 |
correctness |
claude | Non-portable local .json filenames are silently omitted from reconciliation and output. |
| 🟡 MINOR | apps/cli/src/commands/notebooks/notebooks.shared.ts:237 |
portability |
claude | Broad pulls require hard-link support even though exclusive file creation could work on filesystems without hard links. |
| 🟡 MINOR | apps/cli/src/commands/notebooks/pull/pull.handler.ts:147 |
output-format |
codex | An explicit -o pretty does not override --output-format json as required by the CLI output precedence contract. |
| 🟡 MINOR | apps/cli/src/commands/notebooks/pull/pull.handler.ts:98 |
correctness |
codex | A notebook renamed between list and detail requests is written using the stale listed name. |
| ⚪ NIT | apps/cli/src/commands/notebooks/notebooks.output.ts:11 |
maintainability |
claude+codex | The notebooks output policy duplicates a cross-family responsibility and justifies it with prohibited provenance and future-work commentary. |
| ⚪ NIT | apps/cli/src/commands/notebooks/notebooks.shared.ts:83 |
test-coverage |
claude | The pure filename-safety predicate lacks focused unit coverage for several security and boundary cases. |
| ⚪ NIT | apps/cli/src/commands/notebooks/notebooks.integration.test.ts:32 |
testing |
claude | The shared test runner accepts an unused command argument and names its notebook-ID argument name, obscuring that it always invokes pull. |
Refuted findings (kept for transparency, not posted as review comments)
apps/cli/src/commands/notebooks/pull/pull.handler.ts:72(correctness): An explicit notebooks pull incorrectly bypasses duplicate-name protection and may replace the local file associated with another same-named notebook.
Refuted: The duplicate guard prevents guessing during broad name-based reconciliation. The by-ID path does not guess: the user identifies the remote notebook directly, and both handler documentation and the side-effect contract explicitly define this invocation as replacing the file selected by its returned name.apps/cli/src/commands/notebooks/notebooks.shared.ts:3(build): The PR cannot compile because the workspace API package lacks the notebook contracts and client methods used by the new command.
Refuted: The checked-out workspace already contains and exports every cited schema and API operation. Their absence from this PR's diff does not make the command uncompilable because they are present in the base workspace package.
Stats
Claude findings: 8 · Codex findings: 6 · Confirmed: 11 · Refuted: 2 · Uncertain: 0
Models: claude-opus-5 + gpt-5.6-sol · Trigger: auto · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
d8e9a44 to
00f1ae8
Compare
avallete
left a comment
There was a problem hiding this comment.
Approve. Staging pull passed: by id, unsafe names skipped on a broad pull and rejected on an explicit pull, Sales vs sales refused, duplicate names refused, and pagination past 100.
Nits, not blocking:
-o pretty --output-format jsonprints JSON.emitNotebooksMachineOutputignorespretty, then the handler followsoutput.format.compute.output.tstreats-o prettyas text. Worth aligning if a third caller shows up, not a problem for this command.notebooks.output.tscopiescompute.output.ts. The header says to wait for a third caller. Fine to keep here.tests/helpers/notebooks.tsrebuilds the platform mock thatmockCommandPlatformApialready provides.
00f1ae8 to
3606436
Compare
3606436 to
c2918b7
Compare
9646450 to
971c525
Compare
971c525 to
e1d2c1a
Compare
Adds the `supabase notebooks` command family and its first subcommand. `notebooks pull` writes the linked project's notebooks into `supabase/notebooks/<name>.json`, one committed JSON file per notebook. A notebook's identity across the two sides is its name, because a checkout is shared through git and the API's uuid is unreadable in a filename. Names the API allows but a filesystem cannot hold are reported rather than sanitised, and portable-filename aliases are refused before anything is written, so a pull never renames somebody's notebook by accident. Without an argument the checkout wins: only notebooks missing locally are written, and existing files are left alone. A notebook id is the opt-in overwrite path for a single notebook. Local files naming no project notebook are the divergence the command asks about — leave them, delete them, or create them in the project — and an unattended run reports the divergence and leaves both sides alone rather than resolving it in a direction nobody chose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e1d2c1a to
cb0a71b
Compare
Adds the
supabase notebookscommand family and its first subcommand,notebooks pull. Notebooks live insupabase/notebooks/<name>.json, one committed JSON file each, alongsidesupabase/functions/andsupabase/compute/.How to try it
The notebook id for the single-notebook form is the UUID in the notebook's dashboard URL.
Notebooks are behind a dashboard feature preview; switching it on lets you create and edit them in the dashboard, which is the quickest way to set up states worth pulling — several notebooks at once, charts and log cells, a notebook renamed or deleted after a pull, or two notebooks sharing a name.
Notes
Salesvssales, NFC vs NFD) are refused before anything is written.links.nextcursor and fails closed on a missing or repeated cursor.pull/SIDE_EFFECTS.mdrecords the full side-effect surface.