diff --git a/.github/workflows/omarchy.yml b/.github/workflows/omarchy.yml new file mode 100644 index 00000000..2e485d22 --- /dev/null +++ b/.github/workflows/omarchy.yml @@ -0,0 +1,43 @@ +# The Omarchy plugin cannot be loaded on this runner (no omarchy-shell). +# What can be checked offline is the contract verify.sh owns: manifest +# shape, no SQLite, no display-name filters, and the real query against +# examples/demo.db. Registered the same way .github/workflows/scoop.yml +# and .github/workflows/aur.yml register their verify scripts: path-scoped, +# because nothing outside contrib/omarchy/ can change the answer. +# +# verify.sh builds gadak, so this job needs Go. It does not compare git +# tags, so checkout stays shallow (the AUR workflow sets fetch-depth: 0 +# only because check-pkgver.sh reads `git describe --tags`). +name: Omarchy + +on: + pull_request: + paths: ['contrib/omarchy/**', '.github/workflows/omarchy.yml'] + push: + branches: [main] + paths: ['contrib/omarchy/**', '.github/workflows/omarchy.yml'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: omarchy-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + plugin: + name: Omarchy plugin offline checks + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Offline plugin checks + run: ./contrib/omarchy/verify.sh diff --git a/contrib/omarchy/README.md b/contrib/omarchy/README.md new file mode 100644 index 00000000..303d4f39 --- /dev/null +++ b/contrib/omarchy/README.md @@ -0,0 +1,156 @@ +# Omarchy shell plugin + +A bar widget for [omarchy-shell](https://omarchy.org/manual/shell-plugins/) +that answers one question offline: how much of the local gadak mirror is +open, and how much of that has been sitting in the same status for more +than seven days. + +This is not a “my assigned tickets” widget. A standalone workspace has no +account (`gadak init --standalone` clears `AccountID`), and the competing +community plugins (`tmn73.jira`, Linear, Todoist, ClickUp, `37signals.basecamp`) +all need a cloud token and a live network. This one runs `gadak sql --json` +against the on-disk mirror and nothing else. + +**No Omarchy machine has run this plugin yet.** The gate below is offline +(manifest, grep contracts, the real query against `examples/demo.db`). Load, +placement, click-to-open, and `omarchy-plugin-validate` on a guest are +unproven. + +## What the widget shows + +Two numbers in a short bar label, `open·stuck`: + +| number | meaning | +| --- | --- | +| **open** | rows in `issues_full` whose `status_category` is not `done` | +| **stuck** | those same rows whose `status_changed_at` is older than 7 days | + +`status_category` is one of `new`, `inprogress`, `done`. Do not rewrite the +query to filter on a status display name — that is empty on a Korean-language +account (`AGENTS.md`, `CLAUDE.md`). `time-in-status` is not a stored column; +age is `julianday('now') - julianday(status_changed_at)`. NULL +`status_changed_at` is not counted as stuck. + +The query lives in one file: [`gadak/query.sql`](gadak/query.sql). +`BarWidget.qml` runs that file through `gadak sql --json`. `verify.sh` +runs the same file. There is no second copy of the SQL. + +`--json` is NDJSON (one object per line, `cmd/gadak/sql.go`). A stale-mirror +warning goes to stderr; the widget parses stdout only. + +### Degrade states (never an empty badge) + +| condition | badge | tooltip | +| --- | --- | --- | +| `gadak` not on `PATH` | `no gadak` | install the Linux tarball from GitHub Releases | +| `gadak` exits with `no mirror at … — run gadak sync` (`cmd/gadak/sql.go`) | `not synced` | run `gadak sync` | +| non-zero exit, or stdout is not NDJSON with `open`/`stuck` | `sql err` | the command failed | +| first poll still in flight | `…` | reading | +| success | `12·3` | `12 open · 3 stuck >7d` | + +Left click opens `http://127.0.0.1:7777`. That is the default `--addr` of +`gadak serve` (`cmd/gadak/serve.go:41`). Right click re-runs the query. + +`gadak://` is not the click target. `gadak views open` prints a +`gadak://view/…` deeplink (`cmd/gadak/views.go` `deepLinkURL`) that needs +the macOS app bundle to register the scheme. Nothing on this desktop does. +The bind is still `127.0.0.1:7777`; `gadak.localhost:7777` is only a +display URL when the resolver maps that name to loopback +(`cmd/gadak/main.go` `prettyOpenURL`). + +The plugin makes no outbound network call. Telemetry is forbidden +(`SECURITY.md`). + +## Data contract + +Read **only** with `gadak sql --json`. The mirror file is a disposable +cache whose schema is not a 0.x promise (`specs/000-product/data-model.md`). +Opening it from QML would break on a migration and can read a half-written +file mid-sync. The three 0.x promises are `issues_full` + the RECIPES +queries, the `gadak sql` stdout format, and `views open --keys -`. + +## Install + +On an Omarchy machine, from a clone of this repository: + +```bash +bash contrib/omarchy/install.sh +``` + +The script: + +- refuses with one line if `/etc/os-release` `ID` is not `omarchy` + (`docs/runbooks/omarchy-vm.md`) +- copies `gadak/` to `~/.config/omarchy/plugins/io.github.midagedev.gadak/` + (a copy, not a symlink — `omarchy-plugin-validate` rejects symlinks + inside a plugin folder) +- runs `omarchy-plugin-validate` when that CLI exists +- `omarchy-shell shell rescanPlugins` then `omarchy-plugin-enable` +- offers `omarchy-webapp-install gadak http://127.0.0.1:7777 web-browser` + (three arguments; fewer opens an interactive `gum` prompt — + `bin/omarchy-webapp-install`) +- prints `gadak install-service` as the way to keep `serve` up (systemd + **user** unit on Linux; `cmd/gadak/service.go`). It does not install + that unit itself. + +`omarchy-plugin-clone` clones a **built-in** plugin into +`~/.config/omarchy/plugins/./`. It is the wrong verb here. +`omarchy-plugin-add` clones a git URL whose root is a `manifest.json`; +this plugin is a subdirectory of the gadak repo, so add cannot see it +until it is published as its own repository. + +If `gadak` is missing, the script prints the install options that exist +today and still copies the plugin (the badge will say `no gadak`): + +- **not** an AUR package — new AUR registration is closed + (`docs/INSTALL.md`) +- Linux tarball from + : + `gadak__linux_amd64.tar.gz` / `linux_arm64` plus + `checksums.txt` (`README.md`) +- `brew install midagedev/tap/gadak-cli` +- in-repo `contrib/aur/gadak-bin` + `makepkg -si` (fetches that tarball) + +Idempotent: a second run overwrites the copy, skips enable if already +enabled, skips the desktop file if it exists. + +```bash +bash contrib/omarchy/uninstall.sh +``` + +Disables and removes the plugin (`omarchy-plugin-remove --yes`) and the +`gadak.desktop` web app. It does not remove the gadak binary or an +`install-service` unit. + +The hyphenated CLIs (`omarchy-plugin-enable`, `-validate`, `-remove`, +`-list`) live in `basecamp/omarchy` `bin/` on branch `quattro`. +`omarchy plugin …` is the documented dispatcher for the same scripts +(`omarchy:alias` headers). This recipe calls the hyphenated names. + +Plugins run **unsandboxed** inside `omarchy-shell`. Read the plugin +before you enable it +([Omarchy manual](https://omarchy.org/manual/shell-plugins/)). + +## Offline gate + +```bash +bash contrib/omarchy/verify.sh +``` + +Runs on macOS and Linux with no Omarchy present. Builds this repo's +`gadak`, points a temp `GADAK_HOME` at a copy of `examples/demo.db`, +and executes `gadak/query.sql`. CI: +[`.github/workflows/omarchy.yml`](../../.github/workflows/omarchy.yml) +(`actions/setup-go` + this script, path-scoped like the Scoop and AUR +workflows). `qmllint` is skipped with a printed reason when the tool +is absent. + +## Community directory (later, not this round) + +Listing at [omarchyplugins.com](https://omarchyplugins.com) / +[HANCORE-linux/omarchy-plugin-marketplace](https://github.com/HANCORE-linux/omarchy-plugin-marketplace) +is a lead step after this directory exists. Submission is an issue form +plus maintainer review. The marketplace states that it does **not** +security-audit plugins; they run as unsandboxed code. A listing also +wants a standalone public git repo with `manifest.json` at the root, +which this in-repo path is not. diff --git a/contrib/omarchy/gadak/BarWidget.qml b/contrib/omarchy/gadak/BarWidget.qml new file mode 100644 index 00000000..1e3931f2 --- /dev/null +++ b/contrib/omarchy/gadak/BarWidget.qml @@ -0,0 +1,147 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Ui + +// Bar label: "·" from `gadak sql --json` of query.sql. +// Data enters only through that command; this file never opens the mirror. +// Click opens the gadak serve default bind (cmd/gadak/serve.go:41), not +// gadak:// — that scheme is registered by the macOS app bundle only +// (cmd/gadak/views.go deepLinkURL). +BarWidget { + id: root + moduleName: "io.github.midagedev.gadak" + + // serve.go default --addr. Named here so the port is not a magic number. + readonly property string serveUrl: "http://127.0.0.1:7777" + readonly property string queryPath: String(Qt.resolvedUrl("query.sql")).replace(/^file:\/\//, "") + + // loading | ok | no-gadak | not-synced | sql-err. + // Not named `state`: QQuickItem already declares that property, and + // redeclaring it is a QML compile error we have no local qmllint to catch. + property string viewState: "loading" + property int openCount: 0 + property int stuckCount: 0 + + readonly property string displayText: { + if (viewState === "ok") return openCount + "·" + stuckCount + if (viewState === "no-gadak") return "no gadak" + if (viewState === "not-synced") return "not synced" + if (viewState === "sql-err") return "sql err" + return "…" + } + + readonly property string tooltipText: { + if (viewState === "ok") + return openCount + " open · " + stuckCount + " stuck >7d" + if (viewState === "no-gadak") + return "gadak is not on PATH. Install the Linux tarball from https://github.com/midagedev/gadak/releases/latest (gadak__linux_amd64.tar.gz or linux_arm64, plus checksums.txt)." + if (viewState === "not-synced") + return "no mirror — run gadak sync" + if (viewState === "sql-err") + return "gadak sql --json failed or was not NDJSON" + return "reading gadak sql --json" + } + + function refresh() { + if (queryProc.running) return + if (queryPath === "") { + viewState = "sql-err" + return + } + // One argv is the query file; stdout is NDJSON. stderr is ignored for parse + // (a stale-mirror warning is expected there). + queryProc.command = ["bash", "-c", "gadak sql --json \"$(cat \"$1\")\"", "gadak-omarchy", queryPath] + queryProc.running = true + } + + function openGadak() { + if (root.bar && typeof root.bar.run === "function") { + root.bar.run("omarchy-launch-webapp " + serveUrl) + return + } + Quickshell.execDetached(["omarchy-launch-webapp", serveUrl]) + } + + function applyResult(exitCode, stdout, stderr) { + var err = String(stderr || "") + var out = String(stdout || "").trim() + var code = Number(exitCode) + + // "no mirror" first: gadak ran, so it is not the missing-binary case. + if (/no mirror/i.test(err)) { + viewState = "not-synced" + return + } + // 127 is the shell's "command not found". The message test names gadak + // explicitly, because `cat` reports a missing query.sql with the same + // "No such file or directory" and that is a broken install, not a + // missing binary. + if (code === 127 || /gadak: command not found/i.test(err)) { + viewState = "no-gadak" + return + } + if (code !== 0) { + viewState = "sql-err" + return + } + + var line = out.split("\n")[0] || "" + var obj + try { + obj = JSON.parse(line) + } catch (e) { + viewState = "sql-err" + return + } + if (!obj || typeof obj !== "object" || obj.open === undefined || obj.stuck === undefined) { + viewState = "sql-err" + return + } + var o = Number(obj.open) + var s = Number(obj.stuck) + if (!isFinite(o) || !isFinite(s)) { + viewState = "sql-err" + return + } + openCount = o + stuckCount = s + viewState = "ok" + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + Timer { + interval: 60000 + repeat: true + running: true + triggeredOnStart: true + onTriggered: root.refresh() + } + + Process { + id: queryProc + running: false + command: [] + stdout: StdioCollector { id: queryOut; waitForEnd: true } + stderr: StdioCollector { id: queryErr; waitForEnd: true } + onExited: function (exitCode) { + var code = (exitCode === undefined || exitCode === null) ? queryProc.exitCode : exitCode + root.applyResult(code, String(queryOut.text || ""), String(queryErr.text || "")) + } + } + + WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + text: root.displayText + tooltipText: root.tooltipText + onPressed: function (b) { + if (b === Qt.RightButton) root.refresh() + else root.openGadak() + } + } +} diff --git a/contrib/omarchy/gadak/manifest.json b/contrib/omarchy/gadak/manifest.json new file mode 100644 index 00000000..c881813c --- /dev/null +++ b/contrib/omarchy/gadak/manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "io.github.midagedev.gadak", + "name": "gadak", + "version": "0.1.0", + "author": "midagedev", + "description": "Open and stuck issue counts from the local gadak mirror, offline", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "gadak", + "description": "Open and stuck counts from `gadak sql --json` (no network)", + "category": "Info", + "allowMultiple": false, + "defaultSection": "right" + } +} diff --git a/contrib/omarchy/gadak/query.sql b/contrib/omarchy/gadak/query.sql new file mode 100644 index 00000000..d85bee6e --- /dev/null +++ b/contrib/omarchy/gadak/query.sql @@ -0,0 +1,15 @@ +-- Single owner of the bar-widget query. BarWidget.qml and verify.sh both +-- run this file through `gadak sql --json`. Do not open the mirror file. +-- +-- open = issues whose status_category is not `done` +-- stuck = those same rows whose status_changed_at is older than 7 days +-- (time-in-status is not a stored column; compute it here) +-- NULL status_changed_at cannot be aged, so it is not stuck. +SELECT + COUNT(*) AS open, + COALESCE(SUM(CASE + WHEN status_changed_at IS NOT NULL + AND julianday('now') - julianday(status_changed_at) > 7 + THEN 1 ELSE 0 END), 0) AS stuck +FROM issues_full +WHERE status_category != 'done'; diff --git a/contrib/omarchy/install.sh b/contrib/omarchy/install.sh new file mode 100755 index 00000000..bfe617c7 --- /dev/null +++ b/contrib/omarchy/install.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Install the gadak omarchy-shell bar widget on this machine. +# +# Copies contrib/omarchy/gadak/ into ~/.config/omarchy/plugins//, +# validates if omarchy-plugin-validate exists, rescans, and enables. +# omarchy-plugin-clone is the built-in-clone verb (bin/omarchy-plugin-clone) +# and refuses anything that is not first-party — it cannot install this plugin. +# omarchy-plugin-add clones a git URL whose root is a manifest; this directory +# lives inside the gadak repo, so the install is a copy. +set -euo pipefail + +fail() { + echo "install.sh: $*" >&2 + exit 1 +} + +os_id="" +if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + os_id="$(. /etc/os-release && printf '%s' "${ID:-}")" +fi +if [[ "$os_id" != "omarchy" ]]; then + fail "this machine is not Omarchy (ID=${os_id:-unset}; want ID=omarchy). The widget is for omarchy-shell." +fi + +self="${BASH_SOURCE[0]}" +if [[ "$self" != /* ]]; then + self="$(pwd)/$self" +fi +here="${self%/*}" +src="${here}/gadak" +[[ -f "$src/manifest.json" ]] || fail "plugin missing at $src/manifest.json" + +id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$src/manifest.json")" +[[ -n "$id" ]] || fail "manifest id is empty" + +if ! command -v gadak >/dev/null 2>&1; then + cat >&2 <<'EOF' +install.sh: gadak is not on PATH. The widget will show "no gadak" until it is. + +The AUR package is not registered (docs/INSTALL.md). Honest Linux options today: + + 1. Release tarball from https://github.com/midagedev/gadak/releases/latest + gadak__linux_amd64.tar.gz + gadak__linux_arm64.tar.gz + checksums.txt + Then (README.md): + sha256sum --ignore-missing -c checksums.txt + tar -xzf gadak__linux_amd64.tar.gz + # put `gadak` on PATH + + 2. brew install midagedev/tap/gadak-cli + + 3. From a clone: contrib/aur/gadak-bin && makepkg -si + (fetches the same release tarball; not `pacman -S gadak`) + +Continuing with the plugin copy so the bar slot exists. +EOF +fi + +dest="${HOME}/.config/omarchy/plugins/${id}" +mkdir -p "$dest" +# Copy, do not symlink: omarchy-plugin-validate refuses any symlink inside +# the plugin folder (bin/omarchy-plugin-validate). +cp -a "$src/." "$dest/" +rm -rf "$dest/.git" +echo "copied plugin to $dest" + +if command -v omarchy-plugin-validate >/dev/null 2>&1; then + omarchy-plugin-validate "$dest" + echo "omarchy-plugin-validate: ok" +else + echo "install.sh: omarchy-plugin-validate not on PATH — skipped" +fi + +if command -v omarchy-shell >/dev/null 2>&1; then + omarchy-shell shell rescanPlugins >/dev/null || true +fi + +if command -v omarchy-plugin-enable >/dev/null 2>&1; then + already=0 + if command -v omarchy-plugin-list >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + if omarchy-plugin-list --json 2>/dev/null | jq -e --arg id "$id" 'any(.[]; .id == $id and .enabled == true)' >/dev/null; then + already=1 + fi + fi + if (( already )); then + echo "already enabled: $id" + else + omarchy-plugin-enable "$id" + fi +else + echo "install.sh: omarchy-plugin-enable not on PATH — enable later with: omarchy-plugin-enable $id" +fi + +# serve.go default --addr is 127.0.0.1:7777. Three arguments: fewer opens +# the interactive gum prompt (bin/omarchy-webapp-install). +serve_url="http://127.0.0.1:7777" +desktop="${HOME}/.local/share/applications/gadak.desktop" +if command -v omarchy-webapp-install >/dev/null 2>&1; then + if [[ -f "$desktop" ]]; then + echo "web app already present: $desktop" + else + omarchy-webapp-install gadak "$serve_url" web-browser + echo "web app: gadak -> $serve_url" + fi +else + echo "install.sh: omarchy-webapp-install not on PATH — skipped" +fi + +cat <&2 + exit 1 +} + +os_id="" +if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + os_id="$(. /etc/os-release && printf '%s' "${ID:-}")" +fi +if [[ "$os_id" != "omarchy" ]]; then + fail "this machine is not Omarchy (ID=${os_id:-unset}; want ID=omarchy)." +fi + +self="${BASH_SOURCE[0]}" +if [[ "$self" != /* ]]; then + self="$(pwd)/$self" +fi +here="${self%/*}" +src="${here}/gadak" +id="io.github.midagedev.gadak" +if [[ -f "$src/manifest.json" ]]; then + id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$src/manifest.json")" +fi +dest="${HOME}/.config/omarchy/plugins/${id}" + +if command -v omarchy-plugin-remove >/dev/null 2>&1 && [[ -e "$dest" || -L "$dest" ]]; then + omarchy-plugin-remove --yes "$id" +elif [[ -e "$dest" || -L "$dest" ]]; then + if command -v omarchy-plugin-disable >/dev/null 2>&1; then + omarchy-plugin-disable "$id" || true + fi + rm -rf "$dest" + echo "removed $dest" + if command -v omarchy-shell >/dev/null 2>&1; then + omarchy-shell shell rescanPlugins >/dev/null || true + fi +else + echo "plugin not installed at $dest" +fi + +desktop="${HOME}/.local/share/applications/gadak.desktop" +if command -v omarchy-webapp-remove >/dev/null 2>&1 && [[ -f "$desktop" ]]; then + OMARCHY_REMOVE_NOTIFY=false omarchy-webapp-remove gadak || omarchy-webapp-remove gadak +elif [[ -f "$desktop" ]]; then + rm -f "$desktop" + echo "removed $desktop" +else + echo "web app not installed" +fi diff --git a/contrib/omarchy/verify.sh b/contrib/omarchy/verify.sh new file mode 100755 index 00000000..a67c1d93 --- /dev/null +++ b/contrib/omarchy/verify.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# Offline checks for the gadak omarchy-shell plugin. No Omarchy required. +# +# Assertions: +# 1. manifest.json parses, schemaVersion is the number 1, every key the +# first-party clock manifest carries is present, entry-point files exist +# 2. plugin sources have no sqlite / gadak.db / .db path; data enters only +# via `gadak sql --json` +# 3. plugin sources do not filter on display names +# 4. the exact query.sql string runs against examples/demo.db and returns +# NDJSON with open/stuck and open > 0 +# 5. qmllint, if present; otherwise skip with a printed reason +# +# Exit 0 = one-line summary. Non-zero names the failing assertion. +set -euo pipefail + +fail() { + echo "verify.sh: FAIL: $*" >&2 + exit 1 +} + +ok() { + echo "ok: $*" +} + +self="${BASH_SOURCE[0]}" +if [[ "$self" != /* ]]; then + self="$(pwd)/$self" +fi +here="${self%/*}" +repo="$(cd "${here}/../.." && pwd)" +plugin="${here}/gadak" +manifest="${plugin}/manifest.json" +query_file="${plugin}/query.sql" + +[[ -f "$manifest" ]] || fail "manifest: missing $manifest" +[[ -f "$query_file" ]] || fail "query: missing $query_file" + +# ── 1. manifest ────────────────────────────────────────────────────────── +GADAK_OMARCHY_MANIFEST="$manifest" GADAK_OMARCHY_PLUGIN="$plugin" python3 - <<'PY' || fail "manifest: schema or entry points" +import json, os, sys +from pathlib import Path + +plugin = Path(os.environ["GADAK_OMARCHY_PLUGIN"]) +raw = Path(os.environ["GADAK_OMARCHY_MANIFEST"]).read_text() +try: + m = json.loads(raw) +except json.JSONDecodeError as e: + print(f"manifest is not JSON: {e}", file=sys.stderr) + raise SystemExit(1) + +# Clock manifest keys (basecamp/omarchy quattro shell/plugins/panels/clock/manifest.json). +top = ["schemaVersion", "id", "name", "version", "author", "description", "kinds", "entryPoints", "barWidget"] +missing = [k for k in top if k not in m] +if missing: + print(f"missing top-level keys (clock manifest): {missing}", file=sys.stderr) + raise SystemExit(1) +if m.get("schemaVersion") != 1 or isinstance(m.get("schemaVersion"), bool): + print(f"schemaVersion must be the JSON number 1, got {m.get('schemaVersion')!r}", file=sys.stderr) + raise SystemExit(1) +if not isinstance(m["id"], str) or not m["id"]: + print("id must be a non-empty string", file=sys.stderr) + raise SystemExit(1) +if m["id"].startswith("omarchy."): + print(f"id {m['id']!r} uses the reserved omarchy.* namespace", file=sys.stderr) + raise SystemExit(1) +if not isinstance(m["kinds"], list) or not m["kinds"]: + print("kinds must be a non-empty array", file=sys.stderr) + raise SystemExit(1) +if "bar-widget" not in m["kinds"]: + print("kinds must include bar-widget", file=sys.stderr) + raise SystemExit(1) +if not isinstance(m["entryPoints"], dict): + print("entryPoints must be an object", file=sys.stderr) + raise SystemExit(1) +if "barWidget" not in m["entryPoints"]: + print("kind bar-widget requires entryPoints.barWidget", file=sys.stderr) + raise SystemExit(1) +bw = m["barWidget"] +if not isinstance(bw, dict): + print("barWidget must be an object", file=sys.stderr) + raise SystemExit(1) +for k in ("displayName", "description", "category", "allowMultiple"): + if k not in bw: + print(f"barWidget missing clock key {k!r}", file=sys.stderr) + raise SystemExit(1) +for key, rel in m["entryPoints"].items(): + if not isinstance(rel, str) or not rel or rel.startswith("/") or ".." in rel: + print(f"entryPoints.{key} is not a safe relative path: {rel!r}", file=sys.stderr) + raise SystemExit(1) + path = plugin / rel + if not path.is_file(): + print(f"entry point file missing: {rel}", file=sys.stderr) + raise SystemExit(1) +print("ok: manifest schemaVersion 1, clock keys, entry points on disk") +PY + +# ── 2. no direct database access ───────────────────────────────────────── +# Plugin sources = everything under gadak/ (the files the shell loads). +db_hits="$(find "$plugin" -type f ! -name '.*' -print0 | xargs -0 grep -nE 'sqlite|gadak\.db|\.db\b' || true)" +if [[ -n "$db_hits" ]]; then + echo "$db_hits" >&2 + fail "no-sqlite: plugin sources mention sqlite / gadak.db / a .db path" +fi +if ! grep -q 'gadak sql --json' "$plugin/BarWidget.qml"; then + fail "no-sqlite: BarWidget.qml does not contain \`gadak sql --json\`" +fi +if ! grep -q 'query.sql' "$plugin/BarWidget.qml"; then + fail "no-sqlite: BarWidget.qml does not load query.sql (single-owner query)" +fi +ok "no-sqlite: data path is gadak sql --json of query.sql" + +# ── 3. no display-name filters ─────────────────────────────────────────── +name_hits="$(find "$plugin" -type f ! -name '.*' -print0 | xargs -0 grep -nE "'In Progress'|'Highest'|'Lowest'|'Sub-task'|\"In Progress\"|\"Highest\"|\"Lowest\"|\"Sub-task\"" || true)" +if [[ -n "$name_hits" ]]; then + echo "$name_hits" >&2 + fail "no-display-name: plugin sources filter on a localized display name" +fi +ok "no-display-name: no In Progress / Highest / Lowest / Sub-task literals" + +# ── 4. the real query against examples/demo.db ─────────────────────────── +command -v go >/dev/null 2>&1 || fail "query: go is not on PATH (needed to build gadak)" +command -v python3 >/dev/null 2>&1 || fail "query: python3 is not on PATH" +demo="${repo}/examples/demo.db" +[[ -f "$demo" ]] || fail "query: missing $demo" + +work="$(mktemp -d "${TMPDIR:-/tmp}/gadak-omarchy-verify.XXXXXX")" +cleanup() { rm -rf "$work"; } +trap cleanup EXIT INT HUP TERM + +if ! (cd "$repo" && go build -o "${work}/gadak" ./cmd/gadak); then + fail "query: go build ./cmd/gadak failed" +fi +mkdir -p "${work}/home" +cp "$demo" "${work}/home/gadak.db" +query="$(cat "$query_file")" +[[ -n "$query" ]] || fail "query: query.sql is empty" + +set +e +GADAK_HOME="${work}/home" "${work}/gadak" sql --json "$query" >"${work}/stdout" 2>"${work}/stderr" +sql_rc=$? +set -e +if [[ "$sql_rc" -ne 0 ]]; then + echo "stdout:" >&2 + cat "${work}/stdout" >&2 || true + echo "stderr:" >&2 + cat "${work}/stderr" >&2 || true + fail "query: gadak sql --json exited $sql_rc" +fi + +GADAK_OMARCHY_STDOUT="${work}/stdout" python3 - <<'PY' || fail "query: NDJSON / keys / open>0" +import json, os, sys +from pathlib import Path + +text = Path(os.environ["GADAK_OMARCHY_STDOUT"]).read_text() +lines = [ln for ln in text.splitlines() if ln.strip()] +if not lines: + print("stdout is empty (wanted one NDJSON object)", file=sys.stderr) + raise SystemExit(1) +try: + obj = json.loads(lines[0]) +except json.JSONDecodeError as e: + print(f"stdout is not NDJSON: {e}: {lines[0]!r}", file=sys.stderr) + raise SystemExit(1) +if not isinstance(obj, dict): + print(f"first row is not an object: {obj!r}", file=sys.stderr) + raise SystemExit(1) +for key in ("open", "stuck"): + if key not in obj: + print(f"missing key {key!r} in {obj!r}", file=sys.stderr) + raise SystemExit(1) + if isinstance(obj[key], bool) or not isinstance(obj[key], (int, float)): + print(f"{key} is not a number: {obj[key]!r}", file=sys.stderr) + raise SystemExit(1) +if int(obj["open"]) <= 0: + print(f"open={obj['open']} is not > 0 on the demo snapshot", file=sys.stderr) + raise SystemExit(1) +if int(obj["stuck"]) < 0: + print(f"stuck={obj['stuck']} is negative", file=sys.stderr) + raise SystemExit(1) +print(f"ok: query NDJSON open={int(obj['open'])} stuck={int(obj['stuck'])}") +PY + +# ── 5. qmllint if present ──────────────────────────────────────────────── +if command -v qmllint >/dev/null 2>&1; then + if ! qmllint "$plugin/BarWidget.qml"; then + fail "qmllint: BarWidget.qml" + fi + ok "qmllint BarWidget.qml" +else + echo "skip: qmllint not on PATH (tool absent; would syntax-check BarWidget.qml)" +fi + +echo "verify.sh: omarchy plugin checks passed"