diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b7fbaf..513131cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/`. diff --git a/sidecar/cmd/clide/main.go b/sidecar/cmd/clide/main.go new file mode 100644 index 00000000..129802f4 --- /dev/null +++ b/sidecar/cmd/clide/main.go @@ -0,0 +1,18 @@ +// Command clide is the CLI and sidecar-daemon entry point. +// +// One binary, two modes: +// - One-shot CLI (default): clide [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:])) +} diff --git a/sidecar/go.mod b/sidecar/go.mod new file mode 100644 index 00000000..0a8f2368 --- /dev/null +++ b/sidecar/go.mod @@ -0,0 +1,3 @@ +module git.schweitz.net/jpmschweitzer/clide/sidecar + +go 1.25.0 diff --git a/sidecar/internal/cli/cli.go b/sidecar/internal/cli/cli.go new file mode 100644 index 00000000..f84ff652 --- /dev/null +++ b/sidecar/internal/cli/cli.go @@ -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 +} diff --git a/sidecar/internal/daemon/doc.go b/sidecar/internal/daemon/doc.go new file mode 100644 index 00000000..154958c7 --- /dev/null +++ b/sidecar/internal/daemon/doc.go @@ -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 diff --git a/sidecar/internal/diag/diag.go b/sidecar/internal/diag/diag.go new file mode 100644 index 00000000..85544777 --- /dev/null +++ b/sidecar/internal/diag/diag.go @@ -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}) +} diff --git a/sidecar/internal/git/doc.go b/sidecar/internal/git/doc.go new file mode 100644 index 00000000..fe58bacd --- /dev/null +++ b/sidecar/internal/git/doc.go @@ -0,0 +1,5 @@ +// Package git shells out to git for the Git panel, diff rendering, and +// the `clide git ` CLI surface. +// +// Empty for now — lands in Tier 3. +package git diff --git a/sidecar/internal/ipc/doc.go b/sidecar/internal/ipc/doc.go new file mode 100644 index 00000000..4760aa80 --- /dev/null +++ b/sidecar/internal/ipc/doc.go @@ -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 diff --git a/sidecar/internal/pql/doc.go b/sidecar/internal/pql/doc.go new file mode 100644 index 00000000..f4ab5eab --- /dev/null +++ b/sidecar/internal/pql/doc.go @@ -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 diff --git a/sidecar/internal/proc/doc.go b/sidecar/internal/proc/doc.go new file mode 100644 index 00000000..c0b2092c --- /dev/null +++ b/sidecar/internal/proc/doc.go @@ -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 diff --git a/sidecar/internal/pty/doc.go b/sidecar/internal/pty/doc.go new file mode 100644 index 00000000..557f55ec --- /dev/null +++ b/sidecar/internal/pty/doc.go @@ -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 diff --git a/sidecar/internal/version/version.go b/sidecar/internal/version/version.go new file mode 100644 index 00000000..605b1661 --- /dev/null +++ b/sidecar/internal/version/version.go @@ -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, + } +}