Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ducklocal propose dashboard.dash --from new.dash # suggest a change for
ducklocal session show # how an analysis got here, and what no longer holds
ducklocal open --run --sql "SELECT ..." # hand a result to the running window
ducklocal open --state # read back what the window shows
ducklocal project check # will this analysis folder work here, and if not, why
```

Database files open read-only unless you pass `--read-write`. See the [CLI guide](https://ducklocal.app/docs/cli) for every command and option, and the [handoff contract](docs/agent-handoff.md) for how an agent stays in step with the window.
Expand Down
6 changes: 6 additions & 0 deletions examples/portable_project/data/orders.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
date,channel,region,orders,revenue
2026-09-01,web,EU,12,340.50
2026-09-01,store,EU,7,210.00
2026-09-02,web,US,20,610.25
2026-09-02,store,US,4,95.00
2026-09-03,web,EU,15,402.75
7 changes: 7 additions & 0 deletions examples/portable_project/ducklocal-project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 1,
"name": "orders",
"dashboards": [
"reports/orders.dash"
]
}
26 changes: 26 additions & 0 deletions examples/portable_project/reports/orders.dash
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// A dashboard inside a portable project. Its data path is relative to this
// file, so the whole folder can move:
//
// ducklocal project check examples/portable_project
// ducklocal examples/portable_project/ducklocal-project.json

source "orders" {
path = "../data/orders.csv"
}

query "by_channel" {
sql = <<SQL
SELECT channel, sum(revenue) AS revenue
FROM orders
GROUP BY channel
ORDER BY revenue DESC
SQL
}

plot "revenue" {
type = "bar"
title = "Revenue by channel"
query = query.by_channel
x = "channel"
y = "revenue"
}
2 changes: 2 additions & 0 deletions skills/ducklocal/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ filter "station" {

Always validate before handing a spec over: `ducklocal check dashboard.dash` runs every query read-only — in memory, which is enough for a spec over its own `source` files; a query reading a table no source defines is listed under `unresolved` instead of failing — or `ducklocal check dashboard.dash --database warehouse.duckdb` to run them on that database. Either way it verifies every `x`/`y`/`series`/`lat`/`lng`/`color`/`size`/`tooltip`/`value` column against the columns the queries actually return (a non-numeric `y` is an error outside `table`, and so is a non-numeric `lat`, `lng` or `size`). Each query that ran reports its `rows`; `empty` lists queries that ran and returned nothing (a regex or join that matched nothing passes every other check), so run down each one before handing the spec over. Add `--summary` to get one line on success (`{file, ok, sources, queries, filters, plots, unresolved, empty}`, with `unresolved` the names of the queries not run and `empty` those that returned no rows) instead of the full report; failures print the same either way. One run reports every mistake it finds: a spec mistake is exit 2 with kind `spec`, one `file:line: message` per diagnostic — the queries that are sound still run, so their plots' columns are checked and any query failing on the database is listed in the same message; with no spec mistake, a query that fails on the database is exit 1 with kind `sql`, one line per failing query. Fix all of them, not just the first. Pass `--database` whenever the queries read a database's tables: anything under `unresolved` went unchecked. To see it rendered, open the file in the GUI (`ducklocal dashboard.dash` or drag it onto the window): it becomes a dashboard tab: rows of plots on the 12-column grid, each row's height resizable, with per-plot inline errors.

To keep an analysis movable, or to check one that has just arrived, use `ducklocal project init` and `ducklocal project check` in its folder. Read `ok`, `portable`, `problems` (each with a `hint`) and `limitations`; passing does not mean the data is the same. `project check` never reaches the network or installs anything. Don't install what it says is missing without asking. Ask before `ducklocal project relink`, because it changes the person's dashboards.

When the person should approve a dashboard change, or asked to review it, propose it instead of editing the file: `ducklocal propose dashboard.dash --from new.dash --note "what and why"`. The spec stays as it is, and the window offers the change with its diff and checks, for the person to apply or reject. Only they can apply it, so don't write the file yourself to get round a pending or rejected proposal. Read `check` in the output and fix what failed. `ducklocal propose dashboard.dash --status` reports `pending`, `applied` or `rejected`, and `stale: true` means the spec changed since, so propose again from what it says now. Before you start, note `current_hash` from `--status` and pass it as `--based-on` to catch edits made while you worked.

People review a dashboard by commenting on its plots in the window (hover a plot → **Comment**). When asked to review or address dashboard feedback, run `ducklocal comments dashboard.dash`: it lists the open threads as JSON, each with the plot it is on (`plot_title`, `plot_query`, and `plot_lines`, the line range of its `plot` block in the spec). Make the change in the spec, run `ducklocal check`, then answer each thread with `ducklocal comments dashboard.dash --reply c1 --text "what changed" --resolve` (`--reply` alone keeps it open, for a question back). When a request cannot be done in a `.dash` file — data-dependent styling, a plot type or option that does not exist — do not fake it or resolve the thread: reply saying what cannot be done and what can instead, and leave the thread open for the person to choose. The window follows both the spec and the threads live, so the person sees the redrawn plot and the reply together. `--all` includes resolved threads; `--reopen ID` and `--add PLOT --text …` exist too. The threads are kept in `dashboard.dash.comments.json` beside the spec. When the person has the spec open, the window shows you at work on its own: your `check` and `comments` runs keep your name up in the corner, and the plots your edits change, add or remove stay marked until the person has seen them. Before you edit a dashboard the person has open, say where the edit will land: `ducklocal presence dashboard.dash --doing editing --plot revenue,share` (a plot you are about to add may be named before it exists; the window holds its place). `--doing idle` when you are done; 30 seconds without a command ends it too.
Expand Down
15 changes: 15 additions & 0 deletions skills/ducklocal/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ Stages `--from` (the whole new spec, or `-` for stdin) beside the spec in `dashb

Output: `{id, status, stale, base_hash, current_hash, note, author, at, decided_at, backup, applied_despite_failed_checks}`, plus after proposing `check`: `{ok, failure, unresolved, empty, truncated}`. `status` is `pending`, `applied`, `rejected`, or `none` with only `current_hash`. A proposal is staged even when `check.ok` is false, and the person sees why. Exit 2 for arguments (including a proposal identical to the spec), 1 with kind `stale` when `--based-on` no longer matches, or `io`.

## Portable projects

```bash
ducklocal project init [DIR] [--name NAME] # write ducklocal-project.json, listing the dashboards found
ducklocal project check [PATH] # what a move would break
ducklocal project relink --source orders --to data/2026/orders.csv
ducklocal project relink --data archive --to /mnt/archive/old.csv
```

An optional `ducklocal-project.json` in a folder: `{"version": 1, "name", "dashboards": ["reports/a.dash"], "data": [{"name", "path", "sample"}], "requires": {"duckdb": "v2.0.0", "extensions": ["httpfs"]}}`, with relative paths and forward slashes. Data is referenced, never copied. Opening the manifest in the window opens its dashboards, only those inside its folder.

`check` runs the `check` pipeline over each dashboard and prints `{project, manifest, ok, portable, ducklocal, duckdb, dashboards, data, extensions, problems, limitations}`, exiting 0 whatever it found. `ok: false` means something is missing or failed; `portable: false` means a path won't survive a move (absolute, outside the folder, or backslashes). Each problem has `{kind, what, hint}`, with kind `missing_source`, `missing_data`, `missing_dashboard`, `missing_extension`, `failed_check`, `not_portable` or `no_dashboards`. Statuses are `ok`, `missing`, `absolute`, `outside` and `remote` for data, `ok`, `failed`, `remote`, `missing` and `outside` for dashboards (`checked` says whether its queries ran), and `loaded`, `installed` and `missing` for extensions. It never reaches the network: a dashboard reading remote data is left unrun and listed in `limitations`, which also says what passing does not cover, such as a source with the same name and different data.

`relink` points a source at a new path in every dashboard naming it, keeping each old one as `NAME.dash.bak`, or changes a manifest data entry. It works everything out before writing, and refuses a path that isn't there. Exit 2 for arguments, 1 with kind `project` (the manifest doesn't read), `exists` (`init` won't replace one) or `not_found`.

## Record an analysis session

```bash
Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Usage:
ducklocal check FILE Validate a .dash dashboard spec
ducklocal comments FILE Read and answer review comments on a dashboard
ducklocal propose FILE Suggest a dashboard change for the person to review
ducklocal project check [DIR] Check a portable analysis folder and what it needs
ducklocal session show [FILE] Read back how an analysis got where it did
ducklocal presence FILE Show the person which plots you are working on
ducklocal --version
Expand Down Expand Up @@ -937,6 +938,7 @@ pub fn dispatch(args: &[OsString]) -> Option<i32> {
| "check"
| "comments"
| "propose"
| "project"
| "session"
| "presence"
| "lsp"
Expand Down Expand Up @@ -982,6 +984,8 @@ pub fn dispatch(args: &[OsString]) -> Option<i32> {
Ok(crate::spec::proposal::PROPOSE_HELP.to_string())
}
("propose", _) => crate::spec::proposal::run(&args[1..]),
("project", 2) if args[1] == "--help" => Ok(crate::project::HELP.to_string()),
("project", _) => crate::project::run(&args[1..]),
("session", 2) if args[1] == "--help" => Ok(crate::session::HELP.to_string()),
("session", _) => crate::session::run(&args[1..]),
("presence", 2) if args[1] == "--help" => Ok(crate::spec::presence::HELP.to_string()),
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod overview;
#[cfg(test)]
mod perf_probe;
mod profile;
mod project;
mod query;
mod query_tabs;
mod recents;
Expand Down
Loading
Loading