retire Go sidecar; adopt Dart core (ADR 0005)
ADR 0005 supersedes ADR 0002. The "sidecar vs app" split was load-bearing on Go-vs-Dart being a meaningful boundary. Going all-Dart collapses that — the daemon becomes clide --daemon mode of the same AOT binary the CLI lives in, both sharing lib/ with the Flutter app. The one native gap (Dart's multi-threaded VM can't safely fork+exec) is filled by a small C supporter tool — ptyc, Project Terminal Controller, peer of pql — rather than introducing a second core language. ADR 0006 defines the CLI/event surface on top of that Dart core: subsystem list (pane/tab/editor/panel/tree/git/pql/canvas/graph/ theme/settings/project), command shape, versioned JSON event schema, pql-parity exit codes, and command-event duality as the operational form of user/Claude parity. Deleted: sidecar/cmd, sidecar/go.mod, every sidecar/internal package. Rewritten: Makefile (dart compile exe, flutter analyze/format/test, build-linux/build-macos, ptyc-build), ci/*.sh, .githooks/pre-push (no more GOBIN PATH dance), .gitignore (Flutter/Dart at repo root, ptyc section), project.yaml (drop module: and go_version:), CLAUDE.md (guardrails, dependencies, commands refreshed). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,5 @@ set -euo pipefail
|
|||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
# `make tools` installs Go-installed binaries into $HOME/go/bin (or
|
|
||||||
# $GOBIN if set). Users routinely forget to put that on PATH, so the
|
|
||||||
# hook prepends it here to avoid spurious "command not found" failures
|
|
||||||
# for govulncheck / goimports / golangci-lint.
|
|
||||||
export PATH="${GOBIN:-$HOME/go/bin}:$PATH"
|
|
||||||
|
|
||||||
echo "==> pre-push: make push-check"
|
echo "==> pre-push: make push-check"
|
||||||
make push-check
|
make push-check
|
||||||
|
|||||||
+19
-24
@@ -1,25 +1,24 @@
|
|||||||
# -- Flutter / Dart (app/) ----------------------------------------------
|
# -- Flutter / Dart (app + core at repo root) ---------------------------
|
||||||
/app/.dart_tool/
|
/.dart_tool/
|
||||||
/app/.packages
|
/.packages
|
||||||
/app/.flutter-plugins
|
/.flutter-plugins
|
||||||
/app/.flutter-plugins-dependencies
|
/.flutter-plugins-dependencies
|
||||||
/app/build/
|
/build/
|
||||||
/app/.idea/
|
/.idea/
|
||||||
/app/.vscode/
|
/.vscode/
|
||||||
/app/*.iml
|
/*.iml
|
||||||
/app/ios/Pods/
|
/ios/Pods/
|
||||||
/app/macos/Pods/
|
/macos/Pods/
|
||||||
/app/windows/flutter/ephemeral/
|
/windows/flutter/ephemeral/
|
||||||
/app/linux/flutter/ephemeral/
|
/linux/flutter/ephemeral/
|
||||||
/app/macos/Flutter/ephemeral/
|
/macos/Flutter/ephemeral/
|
||||||
|
/bin/clide
|
||||||
|
/bin/clide.exe
|
||||||
# pubspec.lock is committed (supply-chain policy).
|
# pubspec.lock is committed (supply-chain policy).
|
||||||
|
|
||||||
# -- Go sidecar ---------------------------------------------------------
|
# -- ptyc (C supporter tool) --------------------------------------------
|
||||||
/sidecar/bin/
|
/ptyc/bin/
|
||||||
/sidecar/dist/
|
/ptyc/*.o
|
||||||
/sidecar/vendor/
|
|
||||||
/clide
|
|
||||||
/clide.exe
|
|
||||||
|
|
||||||
# -- Test, coverage, profile output ------------------------------------
|
# -- Test, coverage, profile output ------------------------------------
|
||||||
*.test
|
*.test
|
||||||
@@ -29,10 +28,6 @@
|
|||||||
coverage.*
|
coverage.*
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
# -- Go module / workspace artefacts (we don't vendor or use workspaces)
|
|
||||||
/go.work
|
|
||||||
/go.work.sum
|
|
||||||
|
|
||||||
# -- pql per-repo state (clide dogfoods against itself; pql's index lands
|
# -- pql per-repo state (clide dogfoods against itself; pql's index lands
|
||||||
# here when running queries locally). Mirror what `pql init` writes.
|
# here when running queries locally). Mirror what `pql init` writes.
|
||||||
/.pql/
|
/.pql/
|
||||||
|
|||||||
+12
-1
@@ -16,12 +16,23 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Go sidecar skeleton under `sidecar/` — `cmd/clide/main.go`, `go.mod`, and the `internal/*` packages (`cli`, `daemon`, `diag`, `git`, `ipc`, `pql`, `proc`, `pty`, `version`). Deleted wholesale per [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md): the "sidecar language: Go" premise no longer holds once the core is Dart. All functionality listed for those packages will be reimplemented under `lib/` as part of Tier 0.
|
||||||
|
- Go-specific Makefile targets (`lint`, `vuln`, `test-race`, `fmt`, `tidy`, `snapshot`, `tools`, `install` via Go), the `govulncheck`/`goimports`/`golangci-lint` version pins, and the pre-push hook's `GOBIN` PATH injection. Replaced with Dart/Flutter equivalents (`analyze`, `format`, `test`, `test-integration`, `build` via `dart compile exe`).
|
||||||
|
- `module:` and `go_version:` from `project.yaml` — single-language core means no Go module path to track.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `.gitignore` rewritten for the new toolchain: Flutter (build output under `app/`), Dart (`.dart_tool/`), Go sidecar (`sidecar/bin/`, `sidecar/dist/`, the legacy `/clide` binary), plus the usual test/OS/editor/secret rules. Python-specific rules narrow to `legacy/**` where they still apply.
|
- [ADR 0002](docs/ADRs/0002-sidecar-language-go.md) marked **superseded** by [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md). The "sidecar language: Go" guardrail is retired. CLAUDE.md's guardrails, dependency notes, and command reference are updated to reflect the Dart-core direction.
|
||||||
|
- `.gitignore` retargeted: Flutter/Dart output at the repo root (`.dart_tool/`, `build/`, platform ephemeral dirs, `bin/clide`), plus a `ptyc/` section for the C helper's build artefacts. Go-specific rules removed.
|
||||||
|
- `ci/lint.sh`, `ci/test.sh`, `ci/security.sh`, and `.githooks/pre-push` rewritten for the Dart toolchain — no Go shell-outs, no `GOBIN` PATH dance.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md) — Dart core; sidecar directory dissolved; `ptyc` as pql-peer. Establishes one Dart AOT binary for both CLI and daemon, `lib/` as the shared core, and promotes the C PTY helper to a standalone supporter tool on the same footing as pql.
|
||||||
|
- [ADR 0006](docs/ADRs/0006-cli-and-event-surface.md) — CLI and event surface contract. Defines the subsystem list (`pane`, `tab`, `editor`, `panel`, `tree`, `git`, `pql`, `canvas`, `graph`, `theme`, `settings`, `project`), the command shape, the versioned JSON event schema, the pql-style exit-code contract, and the command↔event duality rule that operationalises user/Claude parity.
|
||||||
|
|
||||||
- Architectural decision records carried forward from the short-lived
|
- Architectural decision records carried forward from the short-lived
|
||||||
`claudian` plugin project (discarded in favour of this Flutter
|
`claudian` plugin project (discarded in favour of this Flutter
|
||||||
rebuild):
|
rebuild):
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## What clide is
|
## What clide is
|
||||||
|
|
||||||
A Flutter desktop IDE for Claude Code. Three surfaces, one coherent tool:
|
A Flutter desktop IDE for Claude Code. One language (Dart) across the stack, plus small native supporter tools where Dart can't reach.
|
||||||
|
|
||||||
- **`app/`** — Flutter desktop application (Linux / macOS primary; Windows stretch).
|
- **`app/`** — Flutter desktop application (Linux / macOS primary; Windows stretch).
|
||||||
- **`sidecar/`** — Go sidecar/CLI, single binary with two modes: `clide <subcommand>` (one-shot for Claude) and `clide --daemon` (long-running for the app). Owns PTYs, subprocesses, file watchers, git, pql invocations.
|
- **`lib/` + `bin/clide.dart`** — Dart core, shared by the app and by one AOT-compiled binary that has two modes: `clide <subcommand>` (one-shot for Claude) and `clide --daemon` (long-running for the app). Owns IPC, PTYs (via `ptyc`), subprocesses, file watchers, git shell-outs, pql invocations.
|
||||||
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||||
|
- **`ptyc`** — small C supporter tool, peer of pql. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Clide shells out to it for every pane (shell, tmux, claude, LSP, debug adapter).
|
||||||
|
|
||||||
App ↔ sidecar ↔ CLI speak a single JSON-lines unix socket protocol. The sidecar outlives app restarts so Claude sessions survive reopens.
|
App ↔ daemon ↔ CLI speak a single JSON-lines unix socket protocol. The daemon outlives app restarts so Claude sessions survive reopens. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
||||||
|
|
||||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`docs/ADRs/`](docs/ADRs/). Python Textual predecessor under [`legacy/`](legacy/).
|
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`docs/ADRs/`](docs/ADRs/). Python Textual predecessor under [`legacy/`](legacy/).
|
||||||
|
|
||||||
@@ -18,19 +19,20 @@ Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`docs/AD
|
|||||||
|
|
||||||
These are load-bearing. Violating any means the design is wrong, not the rule.
|
These are load-bearing. Violating any means the design is wrong, not the rule.
|
||||||
|
|
||||||
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. `xterm.dart` is the terminal renderer.
|
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer: it matches the desktop rendering pipeline, keeps our custom `CustomPaint` components pixel-identical, and avoids the DOM-renderer quirks around input handling and terminal-style content. `xterm.dart` is the terminal renderer (Tier 1); markdown, canvas, graph are custom `CustomPaint`/widget components (Tiers 2+, 5).
|
||||||
- **No heavy lifting in the UI layer.** Flutter app renders and handles input. PTYs/subprocesses/filesystem/git all live in the sidecar.
|
- **No heavy lifting in the UI layer.** The app renders and handles input; process/PTY/IO lifecycles live in the daemon. The daemon is Dart too — the split is *process boundary*, not language boundary.
|
||||||
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [ADR 0001](docs/ADRs/0001-cli-first-not-mcp.md).
|
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [ADR 0001](docs/ADRs/0001-cli-first-not-mcp.md).
|
||||||
- **Sidecar language: Go.** Module `git.schweitz.net/jpmschweitzer/clide/sidecar`. See [ADR 0002](docs/ADRs/0002-sidecar-language-go.md).
|
- **Dart is the core; native supporter tools fill specific gaps.** One Dart AOT binary for CLI + daemon. `ptyc` (C) for PTY spawning. `pql` (Go) for queries. No second "core language." See [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md) (supersedes [ADR 0002](docs/ADRs/0002-sidecar-language-go.md)).
|
||||||
- **User/Claude parity.** Every CLI subcommand has a UI affordance in the app, and every UI action has a CLI. If you add one side without the other, the feature is incomplete.
|
- **Own the rendering stack.** PTY (via `ptyc`), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages. Third-party rendering is where we'd hit ceilings first; we'd rather pay the cost up front.
|
||||||
- **pql: wrap, don't duplicate, and treat it as a clide subsystem when present.** Pql logic only lives in `sidecar/internal/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [ADR 0003](docs/ADRs/0003-pql-as-supporter-tool.md).
|
- **User/Claude parity.** Every CLI subcommand has a UI affordance in the app, and every UI action has a CLI. Events are symmetric: every UI state change is a subscribable event. See [ADR 0006](docs/ADRs/0006-cli-and-event-surface.md).
|
||||||
|
- **pql: wrap, don't duplicate, and treat it as a clide subsystem when present.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [ADR 0003](docs/ADRs/0003-pql-as-supporter-tool.md).
|
||||||
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept. Clide dogfoods against its own repo.
|
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept. Clide dogfoods against its own repo.
|
||||||
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. Default `[.gitignore]`; clide writes `[.gitignore, .clideignore]` when `.clideignore` exists. See [ADR 0004](docs/ADRs/0004-ignore-file-strategy.md).
|
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. Default `[.gitignore]`; clide writes `[.gitignore, .clideignore]` when `.clideignore` exists. See [ADR 0004](docs/ADRs/0004-ignore-file-strategy.md).
|
||||||
|
|
||||||
## Tier ordering (don't skip ahead)
|
## Tier ordering (don't skip ahead)
|
||||||
|
|
||||||
1. **Tier 0** — Flutter app + sidecar daemon handshake, empty IDE shell.
|
1. **Tier 0** — Flutter app + Dart daemon handshake, empty IDE shell.
|
||||||
2. **Tier 1** — Claude in an `xterm.dart` pane; session persists across app restarts.
|
2. **Tier 1** — Claude in an `xterm.dart` pane backed by `ptyc`-spawned PTYs; session persists across app restarts.
|
||||||
3. **Tier 2** — Pane model + active-file awareness + `clide open/active/insert/replace-selection/tail`.
|
3. **Tier 2** — Pane model + active-file awareness + `clide open/active/insert/replace-selection/tail`.
|
||||||
4. **Tier 3** — Git panel (staged/unstaged, hunk stage, conflict UI) + diff tab + `clide git …`.
|
4. **Tier 3** — Git panel (staged/unstaged, hunk stage, conflict UI) + diff tab + `clide git …`.
|
||||||
5. **Tier 4** — pql integration: Query panel, file tree, backlinks, problems — all drawing from pql.
|
5. **Tier 4** — pql integration: Query panel, file tree, backlinks, problems — all drawing from pql.
|
||||||
@@ -42,37 +44,36 @@ See `docs/initial-plan.md` for the full tier definitions and acceptance criteria
|
|||||||
## Parent projects
|
## Parent projects
|
||||||
|
|
||||||
- **`legacy/`** — Python Textual clide v1.2.0. Feature-frozen. Reference for the pane model, panel set, git skills (`/commit`, `/stash`, `/pull`, `/push` — rewire to `clide git …`), TODO.md parsing format.
|
- **`legacy/`** — Python Textual clide v1.2.0. Feature-frozen. Reference for the pane model, panel set, git skills (`/commit`, `/stash`, `/pull`, `/push` — rewire to `clide git …`), TODO.md parsing format.
|
||||||
- **`projects/claudian`** (April 2026, discarded) — 2-day experiment with an Obsidian-plugin approach. Its architectural patterns (Go sidecar, CLI-first, pql-as-subsystem, ignore-file strategy, supply-chain gate, changelog discipline, commit conventions) are the ADRs and skills you see here.
|
- **`projects/claudian`** (April 2026, discarded) — 2-day experiment with an Obsidian-plugin approach. Its architectural patterns (CLI-first, pql-as-subsystem, ignore-file strategy, supply-chain gate, changelog discipline, commit conventions) are the ADRs and skills you see here. It first proposed a Go sidecar; ADR 0005 reversed that in favour of a Dart core.
|
||||||
- **[`projects/pql`](https://github.com/postmeridiem/pql)** — active supporter tool. Clide depends on it; never duplicates it.
|
- **[`projects/pql`](https://github.com/postmeridiem/pql)** — active supporter tool. Clide depends on it; never duplicates it.
|
||||||
|
|
||||||
## Dependencies & supply chain
|
## Dependencies & supply chain
|
||||||
|
|
||||||
- **Go (sidecar):** exact-pin versions (never `@latest`). `make vuln` (`govulncheck ./...`) runs after every dep add/bump and gates CI.
|
- **Dart (core + app):** prefer-zero-deps. Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
||||||
- **Dart (app):** prefer-zero-deps. Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
- **`ptyc` and any future native supporter tool:** no dep graph by design (libc-only for `ptyc`). "Audit" is reading the source before each bump.
|
||||||
- `go.sum` and `pubspec.lock` are always committed.
|
- `pubspec.lock` is always committed.
|
||||||
- `make security` aggregates the Go and Dart CVE gates; `ci/security.sh` is the CI entry.
|
- `make security` runs the Dart advisory review; `ci/security.sh` is the CI entry.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
(Placeholder until Phase 3 commits land these targets.)
|
(Placeholder until Tier 0 commits land these targets in real form.)
|
||||||
|
|
||||||
```
|
```
|
||||||
make build # build sidecar/bin/clide
|
make build # dart compile exe bin/clide.dart -o bin/clide
|
||||||
make test # go test ./...
|
make test # flutter test
|
||||||
make test-race # with race detector
|
make test-integration# daemon + CLI + fixture-repo suite
|
||||||
make lint # golangci-lint
|
make analyze # flutter analyze
|
||||||
make vuln # govulncheck (sidecar CVE gate)
|
make format # dart format --set-exit-if-changed
|
||||||
make app-build # flutter build linux / macos
|
make build-linux # flutter build linux
|
||||||
make app-test # flutter test
|
make build-macos # flutter build macos
|
||||||
make app-analyze # dart analyze
|
make ptyc-build # build the ptyc PTY-spawn helper
|
||||||
make security # run all CVE gates (go + dart)
|
make security # Dart advisory review + ptyc source review
|
||||||
make push-check # full pre-push gate: lint + test + test-race + vuln + app-analyze + app-test
|
make push-check # pre-push gate: analyze + format + test
|
||||||
make hooks # install the repo's git hooks (one-time setup)
|
make hooks # install the repo's git hooks (one-time setup)
|
||||||
make tools # install Go tooling at pinned versions (govulncheck, goimports, golangci-lint)
|
|
||||||
make clean # remove build artefacts
|
make clean # remove build artefacts
|
||||||
```
|
```
|
||||||
|
|
||||||
One-time setup on a fresh clone: `make tools && make hooks`, plus `cd app && flutter pub get` once Flutter is installed.
|
One-time setup on a fresh clone: `make hooks && flutter pub get` once Flutter is installed.
|
||||||
|
|
||||||
## Changelog discipline
|
## Changelog discipline
|
||||||
|
|
||||||
|
|||||||
@@ -1,154 +1,139 @@
|
|||||||
# clide — local dev targets. CI scripts in ci/ shell out to these.
|
# clide — local dev targets. CI scripts in ci/ shell out to these.
|
||||||
#
|
#
|
||||||
# Two components under one Makefile:
|
# One toolchain: Flutter + Dart. Native supporter tools (`ptyc`, future
|
||||||
# - Go sidecar/CLI under sidecar/
|
# peers) live in their own directories with their own build targets and
|
||||||
# - Flutter desktop app under app/ (scaffolded once Flutter is installed
|
# compose in under `make build`.
|
||||||
# locally; targets gracefully noop when app/ isn't present yet).
|
|
||||||
|
|
||||||
GO ?= go
|
|
||||||
BIN_DIR ?= sidecar/bin
|
|
||||||
INSTALL_DIR ?= $(HOME)/.local/bin
|
INSTALL_DIR ?= $(HOME)/.local/bin
|
||||||
|
|
||||||
# Version stamping. Source of truth: project.yaml `version:` field. Local
|
# Version stamping. Source of truth: project.yaml `version:` field. Local
|
||||||
# builds augment with git short SHA + dirty marker (semver build metadata).
|
# builds augment with git short SHA + dirty marker (semver build metadata).
|
||||||
# Tagged releases are handled by goreleaser using the git tag instead.
|
|
||||||
VERSION_BASE ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' project.yaml)
|
VERSION_BASE ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' project.yaml)
|
||||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
DIRTY := $(shell git diff --quiet HEAD 2>/dev/null || echo .dirty)
|
DIRTY := $(shell git diff --quiet HEAD 2>/dev/null || echo .dirty)
|
||||||
VERSION ?= $(VERSION_BASE)+$(COMMIT)$(DIRTY)
|
VERSION ?= $(VERSION_BASE)+$(COMMIT)$(DIRTY)
|
||||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
LDFLAGS := -s -w \
|
|
||||||
-X 'git.schweitz.net/jpmschweitzer/clide/sidecar/internal/version.Version=$(VERSION)' \
|
|
||||||
-X 'git.schweitz.net/jpmschweitzer/clide/sidecar/internal/version.Commit=$(COMMIT)' \
|
|
||||||
-X 'git.schweitz.net/jpmschweitzer/clide/sidecar/internal/version.Date=$(DATE)'
|
|
||||||
|
|
||||||
GO_PACKAGES := ./...
|
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show this help.
|
help: ## Show this help.
|
||||||
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
# -- sidecar (Go) --------------------------------------------------------
|
# -- app + core (Flutter / Dart) ----------------------------------------
|
||||||
|
|
||||||
.PHONY: build
|
# App and core targets gracefully noop if the Dart package isn't
|
||||||
build: ## Build the clide sidecar/CLI binary into sidecar/bin/clide.
|
# scaffolded yet (pre-Tier-0 state) or if flutter isn't installed
|
||||||
cd sidecar && $(GO) build -ldflags="$(LDFLAGS)" -o bin/clide ./cmd/clide
|
# locally.
|
||||||
|
APP_PRESENT := $(shell test -f pubspec.yaml && echo yes || echo no)
|
||||||
.PHONY: install
|
|
||||||
install: build ## Install sidecar/bin/clide into $(INSTALL_DIR).
|
|
||||||
install -m 0755 $(BIN_DIR)/clide $(INSTALL_DIR)/clide
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test: ## Unit tests (Go), fast.
|
|
||||||
cd sidecar && $(GO) test $(GO_PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: test-race
|
|
||||||
test-race: ## Unit tests with the race detector.
|
|
||||||
cd sidecar && $(GO) test -race $(GO_PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: test-integration
|
|
||||||
test-integration: build ## Integration tests (binary + fixture repos). Tag: integration.
|
|
||||||
cd sidecar && $(GO) test -tags=integration ./...
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: ## golangci-lint run on the sidecar.
|
|
||||||
cd sidecar && golangci-lint run
|
|
||||||
|
|
||||||
.PHONY: vuln
|
|
||||||
vuln: ## govulncheck on all sidecar packages — Go CVE gate.
|
|
||||||
cd sidecar && govulncheck ./...
|
|
||||||
|
|
||||||
.PHONY: fmt
|
|
||||||
fmt: ## gofmt + goimports on the sidecar.
|
|
||||||
cd sidecar && gofmt -w .
|
|
||||||
@command -v goimports >/dev/null && cd sidecar && goimports -w . || echo "(goimports not installed; skipping)"
|
|
||||||
|
|
||||||
.PHONY: tidy
|
|
||||||
tidy: ## go mod tidy for the sidecar.
|
|
||||||
cd sidecar && $(GO) mod tidy
|
|
||||||
|
|
||||||
.PHONY: snapshot
|
|
||||||
snapshot: ## GoReleaser snapshot build (dry-run, no publish).
|
|
||||||
goreleaser release --snapshot --clean
|
|
||||||
|
|
||||||
# -- app (Flutter) -------------------------------------------------------
|
|
||||||
|
|
||||||
# App targets gracefully noop if app/ doesn't exist yet (pre-Tier-0
|
|
||||||
# scaffold) or if flutter isn't installed locally.
|
|
||||||
APP_PRESENT := $(shell test -f app/pubspec.yaml && echo yes || echo no)
|
|
||||||
HAS_FLUTTER := $(shell command -v flutter >/dev/null && echo yes || echo no)
|
HAS_FLUTTER := $(shell command -v flutter >/dev/null && echo yes || echo no)
|
||||||
|
|
||||||
.PHONY: app-pubget
|
.PHONY: build
|
||||||
app-pubget: ## flutter pub get (hydrate the app's pub cache).
|
build: ## Build the Dart AOT `clide` binary (CLI + --daemon modes in one binary).
|
||||||
ifeq ($(APP_PRESENT),yes)
|
ifeq ($(APP_PRESENT),yes)
|
||||||
cd app && flutter pub get
|
dart compile exe bin/clide.dart -o bin/clide \
|
||||||
|
--define=clideVersion=$(VERSION) \
|
||||||
|
--define=clideCommit=$(COMMIT) \
|
||||||
|
--define=clideDate=$(DATE)
|
||||||
else
|
else
|
||||||
@echo "(app/ not scaffolded yet; skipping)"
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: app-analyze
|
.PHONY: install
|
||||||
app-analyze: ## dart analyze on the app.
|
install: build ## Install bin/clide into $(INSTALL_DIR).
|
||||||
ifeq ($(APP_PRESENT),yes)
|
ifeq ($(APP_PRESENT),yes)
|
||||||
cd app && flutter analyze
|
install -m 0755 bin/clide $(INSTALL_DIR)/clide
|
||||||
else
|
else
|
||||||
@echo "(app/ not scaffolded yet; skipping)"
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: app-format
|
.PHONY: pubget
|
||||||
app-format: ## dart format on the app.
|
pubget: ## flutter pub get (hydrate the pub cache).
|
||||||
ifeq ($(APP_PRESENT),yes)
|
ifeq ($(APP_PRESENT),yes)
|
||||||
cd app && dart format --set-exit-if-changed .
|
flutter pub get
|
||||||
else
|
else
|
||||||
@echo "(app/ not scaffolded yet; skipping)"
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: app-test
|
.PHONY: analyze
|
||||||
app-test: ## flutter test on the app.
|
analyze: ## dart analyze / flutter analyze.
|
||||||
ifeq ($(APP_PRESENT),yes)
|
ifeq ($(APP_PRESENT),yes)
|
||||||
cd app && flutter test
|
flutter analyze
|
||||||
else
|
else
|
||||||
@echo "(app/ not scaffolded yet; skipping)"
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: app-build-linux
|
.PHONY: format
|
||||||
app-build-linux: ## flutter build linux.
|
format: ## dart format --set-exit-if-changed.
|
||||||
ifeq ($(APP_PRESENT),yes)
|
ifeq ($(APP_PRESENT),yes)
|
||||||
cd app && flutter build linux
|
dart format --set-exit-if-changed .
|
||||||
else
|
else
|
||||||
@echo "(app/ not scaffolded yet; skipping)"
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: app-build-macos
|
.PHONY: test
|
||||||
app-build-macos: ## flutter build macos.
|
test: ## flutter test (unit + widget).
|
||||||
ifeq ($(APP_PRESENT),yes)
|
ifeq ($(APP_PRESENT),yes)
|
||||||
cd app && flutter build macos
|
flutter test
|
||||||
else
|
else
|
||||||
@echo "(app/ not scaffolded yet; skipping)"
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: test-integration
|
||||||
|
test-integration: build ## Integration tests (daemon + CLI + fixture repos).
|
||||||
|
ifeq ($(APP_PRESENT),yes)
|
||||||
|
flutter test integration_test || echo "(no integration_test suite yet)"
|
||||||
|
else
|
||||||
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: build-linux
|
||||||
|
build-linux: ## flutter build linux (desktop bundle).
|
||||||
|
ifeq ($(APP_PRESENT),yes)
|
||||||
|
flutter build linux
|
||||||
|
else
|
||||||
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: build-macos
|
||||||
|
build-macos: ## flutter build macos (desktop bundle).
|
||||||
|
ifeq ($(APP_PRESENT),yes)
|
||||||
|
flutter build macos
|
||||||
|
else
|
||||||
|
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
# -- ptyc (C supporter tool) --------------------------------------------
|
||||||
|
|
||||||
|
# Compiled alongside the main binary. Tiny, no deps beyond libc.
|
||||||
|
PTYX_PRESENT := $(shell test -f ptyc/Makefile && echo yes || echo no)
|
||||||
|
|
||||||
|
.PHONY: ptyc-build
|
||||||
|
ptyc-build: ## Build the ptyc PTY-spawn helper.
|
||||||
|
ifeq ($(PTYX_PRESENT),yes)
|
||||||
|
$(MAKE) -C ptyc
|
||||||
|
else
|
||||||
|
@echo "(ptyc/ not scaffolded yet; skipping)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: ptyc-clean
|
||||||
|
ptyc-clean: ## Clean ptyc build artefacts.
|
||||||
|
ifeq ($(PTYX_PRESENT),yes)
|
||||||
|
$(MAKE) -C ptyc clean
|
||||||
|
else
|
||||||
|
@echo "(ptyc/ not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# -- security ------------------------------------------------------------
|
# -- security ------------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: security
|
.PHONY: security
|
||||||
security: vuln ## Run all CVE gates. For now: Go (govulncheck). Dart CVE gate lands when a reliable tooling exists.
|
security: ## Dart advisory review + ptyc source review (manual — no floating deps).
|
||||||
|
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps;"
|
||||||
# -- tooling -------------------------------------------------------------
|
@echo " ptyc is reviewed by reading it (tiny libc-only C)."
|
||||||
|
@echo " Automated Dart CVE tooling lands here when a reliable option exists."
|
||||||
# Pinned Go tool versions. Bump deliberately; never floating.
|
|
||||||
GOVULNCHECK_VERSION ?= v1.1.4
|
|
||||||
GOIMPORTS_VERSION ?= v0.29.0
|
|
||||||
GOLANGCI_LINT_VERSION ?= v2.11.4
|
|
||||||
|
|
||||||
.PHONY: tools
|
|
||||||
tools: ## Install Go tooling at pinned versions (govulncheck, goimports, golangci-lint).
|
|
||||||
$(GO) install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)
|
|
||||||
$(GO) install golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION)
|
|
||||||
$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
|
||||||
|
|
||||||
# -- pre-push gate -------------------------------------------------------
|
# -- pre-push gate -------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: push-check
|
.PHONY: push-check
|
||||||
push-check: lint test test-race test-integration vuln app-analyze app-test ## Full pre-push gate — everything that must pass before a push.
|
push-check: analyze format test ## Full pre-push gate — everything that must pass before a push.
|
||||||
|
|
||||||
.PHONY: hooks
|
.PHONY: hooks
|
||||||
hooks: ## Install the repo's git hooks (points core.hooksPath at .githooks/).
|
hooks: ## Install the repo's git hooks (points core.hooksPath at .githooks/).
|
||||||
@@ -159,9 +144,7 @@ hooks: ## Install the repo's git hooks (points core.hooksPath at .githooks/).
|
|||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Remove build artefacts.
|
clean: ## Remove build artefacts.
|
||||||
rm -rf sidecar/bin sidecar/dist
|
rm -rf bin build .dart_tool
|
||||||
ifeq ($(APP_PRESENT),yes)
|
$(MAKE) ptyc-clean
|
||||||
rm -rf app/build app/.dart_tool
|
|
||||||
endif
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|||||||
+2
-2
@@ -7,6 +7,6 @@ set -euo pipefail
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
make lint
|
make analyze
|
||||||
make app-analyze
|
make format
|
||||||
make security
|
make security
|
||||||
|
|||||||
+3
-2
@@ -3,8 +3,9 @@
|
|||||||
# local dev and CI run the same commands.
|
# local dev and CI run the same commands.
|
||||||
#
|
#
|
||||||
# See ~/.claude/projects/-var-mnt-data-projects-clide/memory/ for the
|
# See ~/.claude/projects/-var-mnt-data-projects-clide/memory/ for the
|
||||||
# standing requirements: Go deps must be version-locked and CVE-checked;
|
# standing requirement: Dart deps prefer zero; what remains is pinned
|
||||||
# Dart deps prefer zero, with what remains pinned and audited.
|
# and advisory-audited before bumping. Native supporter tools (ptyc, any
|
||||||
|
# future peer) are reviewed by reading them — they have no dep graph.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|||||||
+1
-2
@@ -5,5 +5,4 @@ set -euo pipefail
|
|||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
make test
|
make test
|
||||||
make test-race
|
make test-integration
|
||||||
make app-test
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# ADR 0002 — Sidecar language: Go
|
# ADR 0002 — Sidecar language: Go
|
||||||
|
|
||||||
**Status:** accepted
|
**Status:** superseded by [ADR 0005](0005-dart-core-ptyc-peer.md)
|
||||||
**Date:** 2026-04-20 (ported from the claudian lineage)
|
**Date:** 2026-04-20 (ported from the claudian lineage)
|
||||||
|
**Superseded:** 2026-04-20 — the "separate language" premise dissolved once we chose Dart for the core; see 0005 for the rationale.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# ADR 0005 — Dart core; sidecar directory dissolved; `ptyc` as pql-peer
|
||||||
|
|
||||||
|
**Status:** accepted
|
||||||
|
**Date:** 2026-04-20
|
||||||
|
**Supersedes:** [ADR 0002](0002-sidecar-language-go.md)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR 0002 picked Go for the sidecar/CLI on the reasoning that (a) the
|
||||||
|
heavy work (PTYs, subprocesses, file watchers, git, IPC) belongs in
|
||||||
|
a language separate from the UI layer, and (b) pql is Go so the
|
||||||
|
muscle memory transfers. The Flutter app would talk to a Go binary
|
||||||
|
over a unix socket.
|
||||||
|
|
||||||
|
On reassessment, two facts broke that reasoning:
|
||||||
|
|
||||||
|
1. **What the "heavy work" actually is.** Stripped of the PTY layer,
|
||||||
|
the sidecar is I/O-bound glue around shell-outs (`git`, `claude`,
|
||||||
|
`pql`), a unix-socket server, JSON-lines framing, and a process
|
||||||
|
table. `dart:io` covers all of this cleanly. The Go-versus-Rust
|
||||||
|
debate implicit in 0002 was the wrong axis — the real choice was
|
||||||
|
**separate process vs shared language**, and separate-process is
|
||||||
|
what matters (session persistence needs the daemon to outlive the
|
||||||
|
app), not language.
|
||||||
|
|
||||||
|
2. **PTY is the one place Dart is genuinely weak** — and not because
|
||||||
|
of ecosystem, but because Dart's multi-threaded VM can't safely
|
||||||
|
`fork()`. That single constraint forces a native helper regardless
|
||||||
|
of what language wraps it. Once you accept a small native helper,
|
||||||
|
the question is whether *everything else* needs to be in that same
|
||||||
|
native language. It doesn't.
|
||||||
|
|
||||||
|
So the "sidecar" directory stopped carrying weight. It existed to
|
||||||
|
justify the Go/Dart split. With the split gone, the directory is
|
||||||
|
ceremony.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Three moves.
|
||||||
|
|
||||||
|
### 1. Dart is the core language.
|
||||||
|
|
||||||
|
Everything that used to live under `sidecar/` — IPC server, CLI
|
||||||
|
dispatch, process management, file watching, git shell-outs, pql
|
||||||
|
wrapper — is written in Dart. Two execution modes of one Dart AOT
|
||||||
|
binary:
|
||||||
|
|
||||||
|
- `clide <subcommand>` — one-shot, pql-style. Parses args, connects
|
||||||
|
to the running daemon socket, sends a request, prints JSON on
|
||||||
|
stdout, exits with the pql exit-code contract.
|
||||||
|
- `clide --daemon` — long-running. Owns PTYs, subscriptions, file
|
||||||
|
watchers, subprocess lifecycles. Started by the app on load;
|
||||||
|
survives app restarts so Claude sessions persist.
|
||||||
|
|
||||||
|
The Flutter app imports the Dart core as a library for in-process
|
||||||
|
state (views, widgets, models) *and* connects to the daemon over the
|
||||||
|
same IPC the CLI uses. One protocol, two clients.
|
||||||
|
|
||||||
|
### 2. The sidecar directory dissolves.
|
||||||
|
|
||||||
|
```
|
||||||
|
app/ # Flutter UI (Linux / macOS primary)
|
||||||
|
lib/ # Dart core shared by app + CLI + daemon:
|
||||||
|
# ipc/, pty/, proc/, git/, pql/, events/, panes/
|
||||||
|
bin/clide.dart # Dart AOT entry: subcommand dispatch + --daemon
|
||||||
|
tool/ # Dart scripts used by the Makefile
|
||||||
|
tests/ # integration tests that span app + daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
No `sidecar/`. No Go module. `project.yaml` drops `module:`; the
|
||||||
|
Dart package name replaces it.
|
||||||
|
|
||||||
|
### 3. `ptyc` is a pql-peer supporter tool.
|
||||||
|
|
||||||
|
The PTY helper — a small C binary that does `posix_openpt` + `fork`
|
||||||
|
+ `exec` + fd-passing via `SCM_RIGHTS` — graduates to the same
|
||||||
|
status as pql: single-purpose, language-appropriate, standalone,
|
||||||
|
reusable outside Clide. It lives in its own directory (eventually
|
||||||
|
its own repo) and Clide wraps it the same way it wraps pql. Working
|
||||||
|
name: **`ptyc`**.
|
||||||
|
|
||||||
|
- Clide shells out to `ptyc` to spawn every PTY (terminal pane,
|
||||||
|
tmux session, claude, LSP server, debug adapter — all one code
|
||||||
|
path).
|
||||||
|
- `ptyc` writes only what it needs to write (a PTY + forked child)
|
||||||
|
and does nothing else. No IPC protocol of its own, no long-running
|
||||||
|
state. One-shot per pane.
|
||||||
|
- Consumers other than Clide (a Python script, another Dart app, a
|
||||||
|
Go tool) can use `ptyc` standalone with no Clide dependency.
|
||||||
|
|
||||||
|
This mirrors ADR 0003's pql contract: **wrap, don't duplicate**;
|
||||||
|
supporter tools stay independent and reusable.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **ADR 0002 is superseded.** Go sidecar removed. Existing
|
||||||
|
`sidecar/` contents (Go skeleton — `cmd/clide/`, `internal/*`,
|
||||||
|
`go.mod`) are deleted; the ideas it encoded (exit-code contract,
|
||||||
|
ldflag-stamped version, JSON diagnostics) are reimplemented in
|
||||||
|
Dart. The supersession note stays in 0002 so the history reads
|
||||||
|
correctly.
|
||||||
|
- **One toolchain for the IDE proper.** Flutter + Dart. The C
|
||||||
|
toolchain is needed only to build `ptyc` — a tiny, rarely-changing
|
||||||
|
artifact.
|
||||||
|
- **`project.yaml` simplifies.** `module:` and `go_version:` go
|
||||||
|
away. A `ptyc_version:` pin joins the existing `dart_sdk:` and
|
||||||
|
`flutter_channel:` keys.
|
||||||
|
- **Supply-chain gates stay, shape changes.** The Go gate
|
||||||
|
(`govulncheck`) is removed. The Dart gate stays (advisories review
|
||||||
|
+ exact-pin `pubspec.yaml`). `ptyc` gets its own tiny gate: it has
|
||||||
|
no deps beyond libc, so the review is "read the 150 lines before
|
||||||
|
every bump." `make security` becomes `make security` = Dart
|
||||||
|
advisories + `ptyc` review checklist.
|
||||||
|
- **IPC stays.** The daemon / app / CLI split is unchanged — unix
|
||||||
|
socket, token auth, JSON-lines. It was never about language.
|
||||||
|
- **Session persistence stays.** PTY master fds live in the Dart
|
||||||
|
daemon process, not the app process. App restart does not kill
|
||||||
|
Claude.
|
||||||
|
- **Pql continues as-is.** Wrapped via shell-out from
|
||||||
|
`lib/src/pql/` (the Dart equivalent of the deleted
|
||||||
|
`sidecar/internal/pql/`). No protocol change to pql.
|
||||||
|
- **CLAUDE.md and the Makefile need updates.** Commands, directory
|
||||||
|
references, and the "sidecar language: Go" guardrail all shift.
|
||||||
|
- **Rust remains an escape hatch, not a plan.** If a Dart limit
|
||||||
|
later forces a second native helper (file-watching at scale on
|
||||||
|
macOS, a tree-sitter host, etc.), the precedent set here is: new
|
||||||
|
native need → new supporter tool, peer of pql and `ptyc`. Never a
|
||||||
|
second "core language."
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Name: `ptyc`** (pronounced "p-tic"). Three honest readings, all
|
||||||
|
pointing at the same tool:
|
||||||
|
1. **Project Terminal Controller** — parallel to pql's **Project
|
||||||
|
Query Language**. Clide's supporter tools follow a `p*` pattern
|
||||||
|
where `p` = *project*: pql handles project queries, ptyc
|
||||||
|
handles project terminals. Future supporter tools that fit the
|
||||||
|
"small single-purpose peer of pql" slot should follow the same
|
||||||
|
pattern when the fit is natural.
|
||||||
|
2. **PTY + child** — domain vocabulary (PTY parent/child pair).
|
||||||
|
This is what a reader seeing the name on a command line will
|
||||||
|
decode it as, and it's exactly what the tool does: run a child
|
||||||
|
process under a PTY.
|
||||||
|
3. **PTY + C** — the implementation language. Accurate and
|
||||||
|
non-limiting; Unix has a long tradition of tools advertising
|
||||||
|
their implementation (`gcc`, `libc`, `musl`). If we ever
|
||||||
|
rewrote it in another language it would become a new tool with
|
||||||
|
a new name, same as pql would if rewritten.
|
||||||
|
|
||||||
|
Crucially, none of the readings tie the **caller** to any
|
||||||
|
ecosystem — `ptyc` is usable from Dart, Python, Go, shell,
|
||||||
|
anywhere a subprocess can be spawned and a fd can be received.
|
||||||
|
Alternatives considered and rejected: `clide-pty-spawn` (too
|
||||||
|
clide-specific for a peer tool), `dpty` (already taken on
|
||||||
|
crates.io), `ptyspawn` (verbose), `dartmx` (falsely signals
|
||||||
|
caller-ecosystem + implies multiplex), `ptyx` (arbitrary suffix,
|
||||||
|
no domain reading), `ptyc` as read-only "PTY C" (works but sells
|
||||||
|
the name short).
|
||||||
|
- The helper's wire contract (stdin JSON → stdout JSON + SCM_RIGHTS
|
||||||
|
fd transfer) is intentionally small so wrapping it is trivial from
|
||||||
|
any language.
|
||||||
|
- This ADR does not define the Clide CLI / event surface itself —
|
||||||
|
that is [ADR 0006](0006-cli-and-event-surface.md).
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# ADR 0006 — CLI and event surface contract
|
||||||
|
|
||||||
|
**Status:** accepted
|
||||||
|
**Date:** 2026-04-20
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0001](0001-cli-first-not-mcp.md) established that Claude drives
|
||||||
|
Clide via a Bash CLI, not MCP. That decided the *channel*. It did
|
||||||
|
not define the *surface* — which subsystems exist, how commands
|
||||||
|
relate to events, what the JSON looks like on the wire, how
|
||||||
|
subscribers discover state changes.
|
||||||
|
|
||||||
|
CLAUDE.md states the rule colloquially: "every CLI subcommand has a
|
||||||
|
UI affordance in the app, and every UI action has a CLI. If you add
|
||||||
|
one side without the other, the feature is incomplete." This ADR
|
||||||
|
restates that as an implementable contract.
|
||||||
|
|
||||||
|
The shape needs to satisfy three things at once:
|
||||||
|
|
||||||
|
1. **User/Claude parity.** Anything the user can do with a mouse,
|
||||||
|
Claude can do with `clide <...>`. Anything the user can observe
|
||||||
|
in the UI, Claude can observe via events.
|
||||||
|
2. **Daemon as authoritative state.** The app and the CLI are both
|
||||||
|
clients. State lives in the `clide --daemon` process; commands
|
||||||
|
mutate it; events broadcast changes to all subscribers.
|
||||||
|
3. **pql-style ergonomics.** One tool-use pattern for Claude across
|
||||||
|
pql and clide — same exit codes, same JSON-on-stdout habit, same
|
||||||
|
stderr-for-diagnostics rule.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The CLI is organised into **subsystems**. Each subsystem owns a
|
||||||
|
noun, a set of verbs, and a set of events. The set is closed at any
|
||||||
|
point in time (documented); growth is additive (new verbs, new
|
||||||
|
events — never renaming existing ones without a version bump).
|
||||||
|
|
||||||
|
### Subsystem list (initial, by tier)
|
||||||
|
|
||||||
|
| Subsystem | Tier | Nouns | Representative verbs | Representative events |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `pane` | 1 | terminal pane | spawn, list, focus, close, write, resize, tail | `pane.spawned`, `pane.output`, `pane.exit`, `pane.resized` |
|
||||||
|
| `tab` | 2 | workspace tab | new, switch, close, list | `tab.opened`, `tab.switched`, `tab.closed` |
|
||||||
|
| `open` | 2 | editor shortcut | *(verb-only: `clide open <path>`)* | — (emits `editor.opened`) |
|
||||||
|
| `editor` | 2 | active editor buffer | goto, highlight, insert, replace-selection, save | `editor.opened`, `editor.selection_changed`, `editor.saved` |
|
||||||
|
| `panel` | 2 | sidebar/context panels | show, hide, toggle, list | `panel.visibility_changed` |
|
||||||
|
| `tree` | 2 | file tree | reveal, refresh | `tree.node_expanded`, `file.changed` |
|
||||||
|
| `git` | 3 | working tree | status, stage, unstage, stage-hunk, commit, stash, pull, push | `git.status_changed`, `git.branch_changed` |
|
||||||
|
| `pql` | 4 | queries | run, tags, backlinks | `pql.result` |
|
||||||
|
| `canvas` | 5 | canvas surface | open, node add/move/connect, save | `canvas.node_added`, `canvas.node_moved`, `canvas.connection_added` |
|
||||||
|
| `graph` | 5 | graph view | open, focus, filter | `graph.focused`, `graph.filter_changed` |
|
||||||
|
| `theme` | 6 | theme/palette | set, list, get | `theme.changed` |
|
||||||
|
| `settings` | 6 | settings store | get, set, list | `settings.changed` |
|
||||||
|
| `project` | — | whole-workspace | status, reload, events | `project.ready`, `project.reloaded` |
|
||||||
|
|
||||||
|
Two umbrella entry points sit outside any subsystem:
|
||||||
|
|
||||||
|
- `clide tail --events [--filter <subsystem>[:<id>]]` — subscribe to
|
||||||
|
the event stream. Bare `tail --events` gets everything; filtered
|
||||||
|
forms narrow by subsystem or by subsystem+id (e.g.
|
||||||
|
`--filter pane:p_7`, `--filter git`).
|
||||||
|
- `clide status` — one-shot daemon snapshot: connected clients,
|
||||||
|
live panes, open tabs, workspace root, daemon version, uptime.
|
||||||
|
|
||||||
|
### Command shape
|
||||||
|
|
||||||
|
```
|
||||||
|
clide <subsystem> <verb> [<positional>...] [--flag ...] [-- argv...]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Positionals are nouns/ids; flags are modifiers.
|
||||||
|
- `--` separates Clide's args from an inner argv passed through
|
||||||
|
(e.g. `clide pane spawn --cwd X -- tmux new-session -A -s foo`).
|
||||||
|
- Verbs are imperative (`spawn`, not `create-pane`).
|
||||||
|
- Where it reads naturally, single-word shortcuts exist for the
|
||||||
|
hottest paths (`clide open <path>` → `clide editor open <path>`).
|
||||||
|
Shortcuts alias; they do not fork.
|
||||||
|
|
||||||
|
### Exit-code contract (parity with pql)
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `0` | Success |
|
||||||
|
| `1` | User error (bad args, unknown id, precondition failed) |
|
||||||
|
| `2` | Tool error (daemon unreachable, IPC failure, internal panic) |
|
||||||
|
| `3` | Not-found (id or path doesn't resolve) |
|
||||||
|
| `4` | Conflict (state busy, already-running, concurrent-modify) |
|
||||||
|
| `64`–`78` | Reserved, per `sysexits.h`, for future specific cases |
|
||||||
|
|
||||||
|
Diagnostic JSON on **stderr** (not stdout) on any non-zero exit:
|
||||||
|
```json
|
||||||
|
{"code":1,"kind":"user_error","subsystem":"pane","message":"pane id p_99 not found","hint":"clide pane list"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stdout stays machine-parseable on success. This matches pql.
|
||||||
|
|
||||||
|
### Event schema
|
||||||
|
|
||||||
|
Events are JSON objects, one per line, on the `--events` stream.
|
||||||
|
Every event:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"ts": "2026-04-20T21:00:00.123Z",
|
||||||
|
"type": "pane.output",
|
||||||
|
"subsystem": "pane",
|
||||||
|
"id": "p_7",
|
||||||
|
"payload": { "bytes_b64": "…", "seq": 412 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `v`: schema version. Bumped only for breaking changes. Old
|
||||||
|
subscribers pin `v`.
|
||||||
|
- `ts`: ISO-8601 UTC with millisecond precision.
|
||||||
|
- `type`: `<subsystem>.<verb_past|noun_changed>`. Past-tense for
|
||||||
|
things that happened; `_changed` suffix for state transitions.
|
||||||
|
- `subsystem` + `id`: redundant with `type`, but makes
|
||||||
|
filtering cheap and future-flexible.
|
||||||
|
- `payload`: subsystem-defined; documented per subsystem.
|
||||||
|
|
||||||
|
Binary payloads (PTY output, file contents) are base64. The ergonomic
|
||||||
|
cost is worth the "entire stream is line-delimited JSON" invariant.
|
||||||
|
|
||||||
|
### Command ↔ event duality
|
||||||
|
|
||||||
|
Every state-changing command emits at least one event. Subscribers
|
||||||
|
see the same mutation whether they triggered it or not, and the
|
||||||
|
issuing client gets the event back (so `clide pane spawn` followed
|
||||||
|
by a `tail --events` subscription sees `pane.spawned` regardless of
|
||||||
|
subscribe order, via a short replay buffer per subsystem).
|
||||||
|
|
||||||
|
Read-only commands (`list`, `get`, `status`) emit nothing.
|
||||||
|
|
||||||
|
### User/Claude parity as a check
|
||||||
|
|
||||||
|
Every merge to `main` that adds a UI affordance must either:
|
||||||
|
- add the matching CLI verb, or
|
||||||
|
- include a linked follow-up task naming the verb to add next.
|
||||||
|
|
||||||
|
Every merge that adds a CLI verb must either:
|
||||||
|
- surface it in the UI, or
|
||||||
|
- document why the verb is Claude-only (rare; mostly diagnostics
|
||||||
|
like `clide status`).
|
||||||
|
|
||||||
|
Events have the symmetric rule: any UI surface that reacts to state
|
||||||
|
must react to the corresponding event; any new event must be
|
||||||
|
consumable both by the UI and by `clide tail --events`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Surface is enumerable.** Adding a subsystem means adding a row
|
||||||
|
to the table above and specifying its verbs + events in a short
|
||||||
|
doc under `docs/cli/`. The daemon registers it; the CLI dispatcher
|
||||||
|
picks it up; `clide --help` and `clide <subsystem> --help` stay
|
||||||
|
accurate by construction.
|
||||||
|
- **Wire schema is versioned.** `v: 1` is the starting point.
|
||||||
|
Compatibility breaks bump the major and land alongside a
|
||||||
|
`project.yaml` `schema_version:` bump.
|
||||||
|
- **Replay buffer per subsystem.** Cheap (most subsystems emit
|
||||||
|
seldom); needed so a subscriber that connects after a command
|
||||||
|
still sees that command's effect. Buffer depth per subsystem is a
|
||||||
|
tuning parameter; defaults to 16 events.
|
||||||
|
- **Events are the only UI→app state channel.** The Flutter app
|
||||||
|
does not poll; it subscribes. Panels render from the last event
|
||||||
|
for their subsystem + current snapshot from `project status`.
|
||||||
|
- **pql events fit naturally.** Long-running `pql` queries stream
|
||||||
|
rows as `pql.result` events keyed by a query id, letting the
|
||||||
|
Query panel render incrementally.
|
||||||
|
- **Testability.** Every subsystem can be integration-tested by:
|
||||||
|
start a daemon → open a `tail --events` subscriber → issue
|
||||||
|
commands over the CLI → assert events. No UI needed for
|
||||||
|
protocol-level coverage.
|
||||||
|
- **Extension API (Tier 6) inherits this.** A Dart extension
|
||||||
|
publishes a subsystem; the same registration pipeline exposes
|
||||||
|
it to Claude via the CLI. Extensions don't get a second-class
|
||||||
|
channel.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- **Authorisation granularity.** The daemon's token auth is coarse
|
||||||
|
(allow all / deny all). Later, per-subsystem grants may matter
|
||||||
|
(e.g. restrict `git push`). Out of scope here.
|
||||||
|
- **Back-pressure on event streams.** A subscriber that falls behind
|
||||||
|
on `pane.output` (a firehose) needs a policy: drop oldest, block
|
||||||
|
producer, coalesce, or kill subscriber. Defer until Tier 1 is in
|
||||||
|
real use.
|
||||||
|
- **Event persistence.** Events are in-memory only in v1. If a
|
||||||
|
future need (audit log, undo) wants persistence, it becomes a
|
||||||
|
subsystem that subscribes and writes — not a property of the bus.
|
||||||
+4
-5
@@ -23,12 +23,11 @@ status: pre-v2.0, scaffolding (Tier 0 — Flutter app + sidecar handshake — is
|
|||||||
|
|
||||||
repository: ssh://git@git.schweitz.net:2222/jpmschweitzer/clide.git
|
repository: ssh://git@git.schweitz.net:2222/jpmschweitzer/clide.git
|
||||||
license: MIT
|
license: MIT
|
||||||
# Go sidecar module path. Gitea-hosted; Go module resolution works if
|
|
||||||
# the host serves the meta tags, otherwise a vanity path can be added.
|
|
||||||
module: git.schweitz.net/jpmschweitzer/clide/sidecar
|
|
||||||
|
|
||||||
# Toolchain pins. Mirror in sidecar/go.mod and app/pubspec.yaml when bumping.
|
# Toolchain pins. Mirror in pubspec.yaml when bumping.
|
||||||
go_version: ">=1.25"
|
# Single-language core — Dart + Flutter. Native supporter tools
|
||||||
|
# (ptyc today, peers tomorrow) pin their own toolchains in their own
|
||||||
|
# directories; not tracked here.
|
||||||
flutter_channel: stable
|
flutter_channel: stable
|
||||||
dart_sdk: ">=3.5.0 <4.0.0"
|
dart_sdk: ">=3.5.0 <4.0.0"
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
// Command clide is the CLI and sidecar-daemon entry point.
|
|
||||||
//
|
|
||||||
// One binary, two modes:
|
|
||||||
// - One-shot CLI (default): clide <subcommand> [args...]
|
|
||||||
// - Long-running sidecar: clide --daemon
|
|
||||||
//
|
|
||||||
// See docs/initial-plan.md and docs/ADRs/ for the architecture.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.schweitz.net/jpmschweitzer/clide/sidecar/internal/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
os.Exit(cli.Run(os.Args[1:]))
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.schweitz.net/jpmschweitzer/clide/sidecar
|
|
||||||
|
|
||||||
go 1.25.0
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Package cli is the CLI front-end. cmd/clide/main.go calls Run with
|
|
||||||
// os.Args[1:]. Subcommands land here one file per command once Tier 2
|
|
||||||
// begins (see docs/initial-plan.md).
|
|
||||||
//
|
|
||||||
// Stdlib-only for the pre-Tier-0 scaffold. Cobra arrives with the first
|
|
||||||
// real subcommand surface.
|
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.schweitz.net/jpmschweitzer/clide/sidecar/internal/diag"
|
|
||||||
"git.schweitz.net/jpmschweitzer/clide/sidecar/internal/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run dispatches CLI args. Returns the process exit code per the diag
|
|
||||||
// contract.
|
|
||||||
func Run(args []string) int {
|
|
||||||
fs := flag.NewFlagSet("clide", flag.ContinueOnError)
|
|
||||||
fs.SetOutput(os.Stderr)
|
|
||||||
|
|
||||||
daemon := fs.Bool("daemon", false, "run as the long-running sidecar daemon")
|
|
||||||
showVersion := fs.Bool("version", false, "print build info and exit")
|
|
||||||
|
|
||||||
if err := fs.Parse(args); err != nil {
|
|
||||||
return diag.Usage
|
|
||||||
}
|
|
||||||
|
|
||||||
if *showVersion {
|
|
||||||
b, _ := json.Marshal(version.Info())
|
|
||||||
fmt.Println(string(b))
|
|
||||||
return diag.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
if *daemon {
|
|
||||||
diag.Error("cli.not-implemented", "daemon mode not yet implemented", "tier 0 — sidecar scaffold lands next")
|
|
||||||
return diag.Software
|
|
||||||
}
|
|
||||||
|
|
||||||
// No subcommand yet. Surface the pql-style exit 64 so callers can tell
|
|
||||||
// the difference between "no instructions" and a successful run.
|
|
||||||
rest := fs.Args()
|
|
||||||
if len(rest) == 0 {
|
|
||||||
diag.Error("cli.usage", "no subcommand given", "see docs/initial-plan.md — Tier 2 defines the CLI surface")
|
|
||||||
return diag.Usage
|
|
||||||
}
|
|
||||||
|
|
||||||
diag.Error("cli.not-implemented", fmt.Sprintf("subcommand %q not yet implemented", rest[0]), "tier 2 lands the CLI surface")
|
|
||||||
return diag.Software
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// Package daemon hosts the --daemon-mode entry point: lifecycle
|
|
||||||
// management (socket binding, token-auth setup, single-instance lock,
|
|
||||||
// graceful shutdown), subsystem wiring (pty, proc, git, pql, ipc), and
|
|
||||||
// the long-lived event loop.
|
|
||||||
//
|
|
||||||
// Empty for now — lands in Tier 0.
|
|
||||||
package daemon
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// Package diag defines the exit-code contract and emits structured
|
|
||||||
// diagnostics to stderr as line-delimited JSON.
|
|
||||||
//
|
|
||||||
// Mirrors pql's diag contract so Claude (and any other tool that
|
|
||||||
// consumes both CLIs) sees a single shape.
|
|
||||||
package diag
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Exit codes. Same numeric space as pql's.
|
|
||||||
const (
|
|
||||||
OK = 0 // success
|
|
||||||
NoMatch = 2 // success with zero matches / nothing to do
|
|
||||||
Usage = 64 // EX_USAGE — bad CLI flag or missing subcommand
|
|
||||||
DataErr = 65 // EX_DATAERR — malformed request
|
|
||||||
NoInput = 66 // EX_NOINPUT — missing required resource (repo, socket, file)
|
|
||||||
Unavail = 69 // EX_UNAVAILABLE — sidecar unreachable / subsystem down
|
|
||||||
Software = 70 // EX_SOFTWARE — internal error
|
|
||||||
)
|
|
||||||
|
|
||||||
type Level string
|
|
||||||
|
|
||||||
const (
|
|
||||||
LevelWarn Level = "warn"
|
|
||||||
LevelError Level = "error"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Diagnostic is one entry in the stderr JSON-per-line stream.
|
|
||||||
type Diagnostic struct {
|
|
||||||
Level Level `json:"level"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Hint string `json:"hint,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit writes a diagnostic as one JSON line to w.
|
|
||||||
func Emit(w io.Writer, d Diagnostic) {
|
|
||||||
b, err := json.Marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
_, _ = io.WriteString(w, `{"level":"error","code":"diag.marshal","msg":"failed to marshal diagnostic"}`+"\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = w.Write(append(b, '\n'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn emits a warning diagnostic to stderr.
|
|
||||||
func Warn(code, msg string) {
|
|
||||||
Emit(os.Stderr, Diagnostic{Level: LevelWarn, Code: code, Msg: msg})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error emits an error diagnostic to stderr.
|
|
||||||
func Error(code, msg, hint string) {
|
|
||||||
Emit(os.Stderr, Diagnostic{Level: LevelError, Code: code, Msg: msg, Hint: hint})
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Package git shells out to git for the Git panel, diff rendering, and
|
|
||||||
// the `clide git <subcommand>` CLI surface.
|
|
||||||
//
|
|
||||||
// Empty for now — lands in Tier 3.
|
|
||||||
package git
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Package ipc hosts the unix-socket JSON-lines server the sidecar
|
|
||||||
// exposes to the Flutter app and CLI. Token-authenticated; dispatches
|
|
||||||
// to the pty, proc, git, pql, canvas, and pane subsystems.
|
|
||||||
//
|
|
||||||
// Empty for now — lands in Tier 0.
|
|
||||||
package ipc
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// Package pql is the *only* place Clide contains pql logic. Pure
|
|
||||||
// shell-outs to the `pql` binary — no re-implementation of pql's
|
|
||||||
// indexer, ranker, or frontmatter parsing.
|
|
||||||
//
|
|
||||||
// See docs/ADRs/0003-pql-as-supporter-tool.md for the wrap-don't-
|
|
||||||
// duplicate rule and the "pql is a Clide subsystem when present"
|
|
||||||
// invariant.
|
|
||||||
//
|
|
||||||
// Empty for now — lands in Tier 4.
|
|
||||||
package pql
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Package proc manages subprocess lifecycles spawned by the sidecar —
|
|
||||||
// git operations, pql invocations, and anything else the app or CLI
|
|
||||||
// requests.
|
|
||||||
//
|
|
||||||
// Empty for now — lands alongside Tier 1/3 subsystems.
|
|
||||||
package proc
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Package pty owns the PTY lifecycle for terminals hosted inside the
|
|
||||||
// sidecar daemon. Drives xterm.dart panes in the Flutter app and the
|
|
||||||
// `clide` CLI's direct-attach mode.
|
|
||||||
//
|
|
||||||
// Empty for now — lands in Tier 1.
|
|
||||||
package pty
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Package version exposes build-info stamped via -ldflags at build time.
|
|
||||||
// See the root Makefile's LDFLAGS for the -X targets.
|
|
||||||
package version
|
|
||||||
|
|
||||||
import "runtime"
|
|
||||||
|
|
||||||
var (
|
|
||||||
Version = "dev"
|
|
||||||
Commit = "unknown"
|
|
||||||
Date = "unknown"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SchemaVersion tracks the app↔sidecar IPC contract. Bump when the wire
|
|
||||||
// format changes in a way that requires coordinated app and sidecar
|
|
||||||
// updates. Mirror in project.yaml `schema_version:` when that field is
|
|
||||||
// added there.
|
|
||||||
const SchemaVersion = 0
|
|
||||||
|
|
||||||
type BuildInfo struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Commit string `json:"commit"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
GoVersion string `json:"go_version"`
|
|
||||||
SchemaVersion int `json:"schema_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Info() BuildInfo {
|
|
||||||
return BuildInfo{
|
|
||||||
Version: Version,
|
|
||||||
Commit: Commit,
|
|
||||||
Date: Date,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
SchemaVersion: SchemaVersion,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user