add Go sidecar and CLI skeleton

cmd/clide/main.go is the single entry; the same binary will later
switch into sidecar-daemon mode via --daemon. internal/cli holds the
dispatch (stdlib flag for now, Cobra arrives with the first real
subcommand in Tier 2). internal/diag mirrors pql's exit-code contract
so the two tools share one mental model for callers. internal/version
exposes the build-info struct that the Makefile's -ldflags -X targets
stamp; Info() is wired to --version so the binary reports both the
declared project.yaml version and the commit it was built from.

The daemon, pty, proc, git, ipc, and pql sub-packages ship as doc.go
stubs naming the subsystem they will own. They keep the layout legible
before the code lands so tier work has a home on arrival.

Module path targets Gitea (git.schweitz.net/jpmschweitzer/clide/sidecar),
matching where the repo lives. Go module resolution works if the host
serves the meta tags; a vanity import path can be added later if
needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 22:11:54 +02:00
co-authored by Claude Opus 4.7
parent 4ae53ba16a
commit c39c2df6f5
12 changed files with 208 additions and 0 deletions
+1
View File
@@ -29,6 +29,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
[ADR 0002](docs/ADRs/0002-sidecar-language-go.md) — Sidecar language: Go.
[ADR 0003](docs/ADRs/0003-pql-as-supporter-tool.md) — pql as supporter tool; wrap, don't duplicate; pql is a Clide subsystem when present.
[ADR 0004](docs/ADRs/0004-ignore-file-strategy.md) — Ignore file strategy (`ignore_files:` in `.pql/config.yaml`, layered).
- Go sidecar/CLI skeleton under `sidecar/` (module `git.schweitz.net/jpmschweitzer/clide/sidecar`): `cmd/clide/main.go`, `internal/cli` with a stdlib-flag dispatch, `internal/diag` mirroring pql's exit-code + stderr-JSON contract, `internal/version` with ldflag-stamped build info, and placeholder packages for `daemon`, `pty`, `proc`, `git`, `ipc`, `pql` awaiting their tier. `clide --version` emits JSON build-info today.
- Root `Makefile` drives both the Go sidecar and the Flutter app under one toolchain. Version is read from `project.yaml` via awk and stamped into the sidecar via `-ldflags -X`. Flutter targets gracefully noop before the app is scaffolded so the Makefile is usable from day one. Pinned Go tooling (govulncheck, goimports, golangci-lint) installs via `make tools`.
- `ci/` entry scripts: `test.sh`, `lint.sh` (includes the supply-chain gate — no green lint without a green CVE scan), `security.sh`, `release.sh` (stub).
- Project identity files for the Flutter rebuild at the repo root: `project.yaml` (single source of truth for version + module path, version 2.0.0-dev), a fresh `README.md`, MIT `LICENSE`, and `.editorconfig`. The Python clide's manifest and README are preserved under `legacy/`.
+18
View File
@@ -0,0 +1,18 @@
// 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:]))
}
+3
View File
@@ -0,0 +1,3 @@
module git.schweitz.net/jpmschweitzer/clide/sidecar
go 1.25.0
+53
View File
@@ -0,0 +1,53 @@
// 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
}
+7
View File
@@ -0,0 +1,7 @@
// 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
+58
View File
@@ -0,0 +1,58 @@
// 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})
}
+5
View File
@@ -0,0 +1,5 @@
// 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
+6
View File
@@ -0,0 +1,6 @@
// 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
+10
View File
@@ -0,0 +1,10 @@
// 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
+6
View File
@@ -0,0 +1,6 @@
// 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
+6
View File
@@ -0,0 +1,6 @@
// 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
+35
View File
@@ -0,0 +1,35 @@
// 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,
}
}