chore: initial scaffold with plugin system and placeholders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
69cadb4ea6
23
.editorconfig
Normal file
23
.editorconfig
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{yaml,yml,json,toml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Compiled binaries
|
||||||
|
/ctx
|
||||||
|
/ctx.exe
|
||||||
|
cmd/ctx/ctx
|
||||||
|
cmd/ctx/ctx.exe
|
||||||
|
/bin/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Test and coverage artifacts
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# Profiling
|
||||||
|
*.prof
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# OS artifacts
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Ricardo Carneiro
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
90
README.md
Normal file
90
README.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# ctx — anti-tokens CLI for Claude Code
|
||||||
|
|
||||||
|
> Analyze your codebase locally. Feed Claude dense summaries, not raw files.
|
||||||
|
|
||||||
|
**Status:** alpha — under active development. Interfaces will change.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Every token Claude reads costs money and burns context window. When working on
|
||||||
|
a large C# or React codebase, Claude often spends thousands of tokens just
|
||||||
|
reading files to build a mental model before doing any actual work.
|
||||||
|
|
||||||
|
`ctx` runs locally, analyzes your project with language-aware tools (Roslyn for
|
||||||
|
C#, tree-sitter for TypeScript), and emits compact markdown summaries that give
|
||||||
|
Claude everything it needs in a fraction of the tokens.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install github.com/ricarneiro/ctx/cmd/ctx@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Go 1.22+. Binaries for common platforms will be published after the
|
||||||
|
MVP is validated.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Git context: recent commits, status, branch info
|
||||||
|
ctx git
|
||||||
|
|
||||||
|
# Auto-detect stack and emit project overview
|
||||||
|
ctx auto project
|
||||||
|
|
||||||
|
# C# project structure (requires .NET SDK)
|
||||||
|
ctx csharp project
|
||||||
|
|
||||||
|
# C# file outline: types, methods, signatures
|
||||||
|
ctx csharp outline src/MyService.cs
|
||||||
|
|
||||||
|
# List compilation errors
|
||||||
|
ctx csharp errors
|
||||||
|
```
|
||||||
|
|
||||||
|
All output is UTF-8 markdown on stdout. Pipe it where you need it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ctx csharp project | pbcopy # macOS
|
||||||
|
ctx csharp project | clip # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
Or reference it in a `CLAUDE.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Run `ctx csharp project` to get the project overview before making changes.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
`ctx` uses a plugin system. Each stack (`git`, `csharp`, `react`, `auto`) is a
|
||||||
|
plugin that implements the `core.Plugin` interface and registers itself via
|
||||||
|
`init()`.
|
||||||
|
|
||||||
|
```
|
||||||
|
core.Plugin interface
|
||||||
|
Name() string
|
||||||
|
Version() string
|
||||||
|
ShortDescription() string
|
||||||
|
Command(ctx *core.Context) *cobra.Command
|
||||||
|
```
|
||||||
|
|
||||||
|
In the MVP, plugins are compiled into the binary. Future plan: migrate to
|
||||||
|
subprocess dispatch (binaries named `ctx-csharp`, `ctx-react` in PATH), same
|
||||||
|
pattern as `kubectl` plugins.
|
||||||
|
|
||||||
|
C# analysis uses a separate helper process (`tools/roslyn-helper/`) written in
|
||||||
|
C# with Roslyn. The helper communicates with `ctx` via JSON-RPC over
|
||||||
|
stdin/stdout. This lets us use the best tool for the job without pulling a .NET
|
||||||
|
runtime into the Go binary.
|
||||||
|
|
||||||
|
See `docs/DECISIONS.md` for the full rationale behind each architectural choice.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions welcome. See `CONTRIBUTING.md` (coming soon) for guidelines.
|
||||||
|
Open an issue first if you plan a large change.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
10
cmd/ctx/main.go
Normal file
10
cmd/ctx/main.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ricarneiro/ctx/internal/cli"
|
||||||
|
_ "github.com/ricarneiro/ctx/internal/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cli.Execute()
|
||||||
|
}
|
||||||
158
docs/DECISIONS.md
Normal file
158
docs/DECISIONS.md
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# Architecture Decision Records — ctx
|
||||||
|
|
||||||
|
Decisions are recorded here as they are made. Each entry has context (why we
|
||||||
|
needed to decide), decision (what we chose), and consequences (what it implies).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Language: Go (not C# or TypeScript)
|
||||||
|
|
||||||
|
**Context:** ctx is a CLI tool that runs locally on developer machines. It
|
||||||
|
needs to start fast (< 100ms cold start), produce a single distributable
|
||||||
|
binary, and work on macOS, Linux, and Windows without asking the user to
|
||||||
|
install a runtime.
|
||||||
|
|
||||||
|
**Decision:** Go.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Single static binary, trivial cross-compilation (`GOOS=linux go build`).
|
||||||
|
- No runtime install required on target machines.
|
||||||
|
- Go's stdlib covers file I/O, JSON, HTTP, process spawning — no heavy deps.
|
||||||
|
- Cannot use Roslyn directly for C# analysis (Go has no decent C# parser).
|
||||||
|
Handled by a separate helper process (see decision 4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. CLI framework: Cobra
|
||||||
|
|
||||||
|
**Context:** Need subcommands (`ctx git`, `ctx csharp outline`), flags, help
|
||||||
|
text, and shell completion. Rolling our own is wasteful.
|
||||||
|
|
||||||
|
**Decision:** `github.com/spf13/cobra`. Optionally `viper` for config in the
|
||||||
|
future, but not added yet.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Cobra is the de facto standard (`kubectl`, `gh`, `hugo`, `helm` all use it).
|
||||||
|
- Well-understood by contributors. Good docs. Stable API.
|
||||||
|
- Shell completion (bash/zsh/fish/PowerShell) comes for free.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Plugin system: compile-time in MVP, subprocess later
|
||||||
|
|
||||||
|
**Context:** ctx needs to support multiple stacks. Plugins must be modular.
|
||||||
|
Two valid approaches: (a) compile all plugins into one binary, (b) dispatch
|
||||||
|
to external binaries (`ctx-csharp`, `ctx-react`) like kubectl.
|
||||||
|
|
||||||
|
**Decision:** MVP uses compile-time plugins registered via `init()`. The
|
||||||
|
`core.Plugin` interface is designed so subprocess plugins can implement it
|
||||||
|
later without changing callers.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Simpler to build and ship in early stages.
|
||||||
|
- All plugins must be written in Go (or wrap external tools).
|
||||||
|
- Subprocess migration is planned but deferred until we need third-party
|
||||||
|
plugins. At that point: scan PATH for `ctx-*` binaries, wrap them with a
|
||||||
|
shim that implements `core.Plugin`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. C# analysis: separate Roslyn helper process
|
||||||
|
|
||||||
|
**Context:** Semantic C# analysis (types, method signatures, usages, errors)
|
||||||
|
requires a compiler-level parser. Roslyn is the only production-quality option.
|
||||||
|
Go has no usable C# parser.
|
||||||
|
|
||||||
|
**Decision:** A separate C# program (`tools/roslyn-helper/`) that loads
|
||||||
|
projects with Roslyn and answers queries sent as JSON-RPC messages over
|
||||||
|
stdin/stdout. ctx spawns it as a subprocess and communicates via pipes.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Requires .NET SDK on the machine for C# commands (acceptable — users of
|
||||||
|
`ctx csharp` are already .NET developers).
|
||||||
|
- Clean separation: Go handles CLI, orchestration, output formatting; C#
|
||||||
|
handles semantic analysis.
|
||||||
|
- JSON-RPC over stdin/stdout is trivial to test in isolation.
|
||||||
|
- Performance: subprocess is spawned once per ctx invocation, amortized if we
|
||||||
|
batch queries (future optimization).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Output format: UTF-8 markdown without BOM on stdout
|
||||||
|
|
||||||
|
**Context:** ctx output is consumed by Claude Code (via pipe or CLAUDE.md
|
||||||
|
instructions). Claude understands markdown well. BOM causes issues in some
|
||||||
|
contexts.
|
||||||
|
|
||||||
|
**Decision:** All plugin output is plain UTF-8 markdown, no BOM, on stdout.
|
||||||
|
Errors go to stderr. No ANSI colors (markdown already has structure).
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Output is directly pasteable into Claude conversations.
|
||||||
|
- Easy to redirect to file: `ctx csharp project > context.md`.
|
||||||
|
- Plugins must use `ctx.Stdout` / `ctx.Stderr`, never `fmt.Println` directly,
|
||||||
|
so output destination can be overridden in tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Subcommand structure: ctx <stack> <command>
|
||||||
|
|
||||||
|
**Context:** Multiple stacks, each with multiple commands. Need a consistent,
|
||||||
|
scalable interface.
|
||||||
|
|
||||||
|
**Decision:** `ctx <stack> <command> [args] [flags]`. Examples:
|
||||||
|
- `ctx csharp project`
|
||||||
|
- `ctx csharp outline src/Foo.cs`
|
||||||
|
- `ctx git`
|
||||||
|
- `ctx auto project`
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Adding a new stack = adding a new top-level subcommand.
|
||||||
|
- Commands within a stack are subcommands of the stack command.
|
||||||
|
- Consistent with kubectl, gh, and other modern CLIs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Module path: github.com/ricarneiro/ctx
|
||||||
|
|
||||||
|
**Context:** Source is hosted on a private Gitea instance
|
||||||
|
(`git.carneiro.ddnsfree.com`). Go module paths do not need to match the actual
|
||||||
|
git host. If we want `go install` from the public GitHub mirror later, the
|
||||||
|
module path must match.
|
||||||
|
|
||||||
|
**Decision:** `github.com/ricarneiro/ctx` from day one.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Developers cloning from Gitea still use this module path — no friction.
|
||||||
|
- `go install github.com/ricarneiro/ctx/cmd/ctx@latest` works once we push
|
||||||
|
to GitHub. No rename needed.
|
||||||
|
- If we never publish to GitHub, the module path is just an identifier and
|
||||||
|
causes no problems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. License: MIT
|
||||||
|
|
||||||
|
**Context:** Want maximum adoption. No patent clause complexity.
|
||||||
|
|
||||||
|
**Decision:** MIT, copyright Ricardo Carneiro.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Anyone can use, fork, embed, sell without restrictions.
|
||||||
|
- No copyleft. If this matters later, we can dual-license, but that's a
|
||||||
|
future problem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Commit convention: Conventional Commits
|
||||||
|
|
||||||
|
**Context:** Want automated CHANGELOG generation in the future. Need a
|
||||||
|
consistent commit format from day one.
|
||||||
|
|
||||||
|
**Decision:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`,
|
||||||
|
`refactor:`, `test:`). Enforced by convention, not by hook (yet).
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- `git log --oneline` is readable.
|
||||||
|
- Can run `git-cliff` or `conventional-changelog` later to generate CHANGELOG.
|
||||||
|
- PRs should squash to a single conventional commit on merge.
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module github.com/ricarneiro/ctx
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require github.com/spf13/cobra v1.10.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
)
|
||||||
10
go.sum
Normal file
10
go.sum
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
15
internal/cli/plugins.go
Normal file
15
internal/cli/plugins.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerPluginCommands iterates the core registry and adds each plugin's
|
||||||
|
// root command as a subcommand of rootCmd. Called once from Execute().
|
||||||
|
func registerPluginCommands(rootCmd *cobra.Command) {
|
||||||
|
ctx := newContext()
|
||||||
|
for _, p := range core.All() {
|
||||||
|
rootCmd.AddCommand(p.Command(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
48
internal/cli/root.go
Normal file
48
internal/cli/root.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "ctx",
|
||||||
|
Short: "Anti-tokens CLI for Claude Code",
|
||||||
|
Long: `ctx reduces token consumption in Claude Code sessions by analyzing your
|
||||||
|
codebase locally and emitting dense markdown summaries that Claude consumes
|
||||||
|
instead of reading dozens of raw source files.
|
||||||
|
|
||||||
|
Each subcommand targets a specific stack:
|
||||||
|
ctx git — git log, status and diff summary
|
||||||
|
ctx auto — auto-detect project stack and emit context
|
||||||
|
ctx csharp — C# / .NET analysis via Roslyn
|
||||||
|
ctx react — React / TypeScript analysis
|
||||||
|
|
||||||
|
Output is always UTF-8 markdown on stdout, suitable for piping into Claude.`,
|
||||||
|
SilenceUsage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the root command. Called by cmd/ctx/main.go.
|
||||||
|
func Execute() {
|
||||||
|
registerPluginCommands(rootCmd)
|
||||||
|
// Set version after plugins are registered so versionString() sees them all.
|
||||||
|
rootCmd.Version = versionString()
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newContext builds the core.Context injected into plugins at runtime.
|
||||||
|
func newContext() *core.Context {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
return &core.Context{
|
||||||
|
Stdout: os.Stdout,
|
||||||
|
Stderr: os.Stderr,
|
||||||
|
WorkDir: wd,
|
||||||
|
Verbose: false,
|
||||||
|
Ctx: context.Background(),
|
||||||
|
}
|
||||||
|
}
|
||||||
51
internal/cli/version.go
Normal file
51
internal/cli/version.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version, Commit, and BuildDate are injected at build time via ldflags:
|
||||||
|
//
|
||||||
|
// go build -ldflags "-X github.com/ricarneiro/ctx/internal/cli.Version=1.0.0 \
|
||||||
|
// -X github.com/ricarneiro/ctx/internal/cli.Commit=abc1234 \
|
||||||
|
// -X github.com/ricarneiro/ctx/internal/cli.BuildDate=2025-01-01"
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
Commit = "unknown"
|
||||||
|
BuildDate = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
rootCmd.SetVersionTemplate("{{ .Version }}\n")
|
||||||
|
// rootCmd.Version is set lazily in Execute() after all plugins are
|
||||||
|
// registered, so that versionString() can read the full plugin list.
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print ctx version and compiled plugins",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Println(versionString())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionString builds the full version line including plugin list.
|
||||||
|
func versionString() string {
|
||||||
|
line1 := fmt.Sprintf("ctx %s (commit %s, built %s)", Version, Commit, BuildDate)
|
||||||
|
|
||||||
|
plugins := core.All()
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
return line1 + "\nplugins: none"
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]string, len(plugins))
|
||||||
|
for i, p := range plugins {
|
||||||
|
parts[i] = fmt.Sprintf("%s@%s", p.Name(), p.Version())
|
||||||
|
}
|
||||||
|
return line1 + "\nplugins: " + strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
26
internal/core/context.go
Normal file
26
internal/core/context.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is passed to plugins when their command is executed.
|
||||||
|
// Allows ctx to inject dependencies and configuration without coupling
|
||||||
|
// plugins to its internals. Mirrors the pattern used by kubectl plugins.
|
||||||
|
type Context struct {
|
||||||
|
// Stdout is where plugins write their primary markdown output.
|
||||||
|
Stdout io.Writer
|
||||||
|
|
||||||
|
// Stderr is where plugins write errors and diagnostic messages.
|
||||||
|
Stderr io.Writer
|
||||||
|
|
||||||
|
// WorkDir is the directory where ctx was invoked.
|
||||||
|
WorkDir string
|
||||||
|
|
||||||
|
// Verbose enables extra diagnostic output when true.
|
||||||
|
Verbose bool
|
||||||
|
|
||||||
|
// Ctx carries deadlines and cancellation signals.
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
24
internal/core/plugin.go
Normal file
24
internal/core/plugin.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
// Plugin represents a ctx module (a stack or utility).
|
||||||
|
// The interface is designed to support future migration from compile-time
|
||||||
|
// registration (via init()) to subprocess dispatch (ctx-<name> binaries in PATH,
|
||||||
|
// kubectl-plugin style). Adding subprocess support later should not require
|
||||||
|
// changing this interface.
|
||||||
|
type Plugin interface {
|
||||||
|
// Name returns the plugin identifier (e.g. "csharp", "react", "git").
|
||||||
|
// Used as the subcommand name: ctx <name> ...
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
// Version returns the semantic version of the plugin.
|
||||||
|
Version() string
|
||||||
|
|
||||||
|
// ShortDescription returns a one-line description shown in ctx --help.
|
||||||
|
ShortDescription() string
|
||||||
|
|
||||||
|
// Command returns the root cobra.Command for this plugin, with all
|
||||||
|
// subcommands already configured. ctx adds this as a subcommand of root.
|
||||||
|
Command(ctx *Context) *cobra.Command
|
||||||
|
}
|
||||||
44
internal/core/registry.go
Normal file
44
internal/core/registry.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
registry = make(map[string]Plugin)
|
||||||
|
registryMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register adds a plugin to the registry. Should be called from init()
|
||||||
|
// in plugin packages. Panics if a plugin with the same name is already registered.
|
||||||
|
func Register(p Plugin) {
|
||||||
|
registryMu.Lock()
|
||||||
|
defer registryMu.Unlock()
|
||||||
|
if _, exists := registry[p.Name()]; exists {
|
||||||
|
panic(fmt.Sprintf("ctx: plugin %q already registered", p.Name()))
|
||||||
|
}
|
||||||
|
registry[p.Name()] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// All returns all registered plugins, sorted by name.
|
||||||
|
func All() []Plugin {
|
||||||
|
registryMu.RLock()
|
||||||
|
defer registryMu.RUnlock()
|
||||||
|
plugins := make([]Plugin, 0, len(registry))
|
||||||
|
for _, p := range registry {
|
||||||
|
plugins = append(plugins, p)
|
||||||
|
}
|
||||||
|
sort.Slice(plugins, func(i, j int) bool {
|
||||||
|
return plugins[i].Name() < plugins[j].Name()
|
||||||
|
})
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a plugin by name, or nil if not found.
|
||||||
|
func Get(name string) Plugin {
|
||||||
|
registryMu.RLock()
|
||||||
|
defer registryMu.RUnlock()
|
||||||
|
return registry[name]
|
||||||
|
}
|
||||||
15
internal/output/encoding.go
Normal file
15
internal/output/encoding.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Package output provides helpers for writing consistent markdown to stdout.
|
||||||
|
package output
|
||||||
|
|
||||||
|
// UTF-8 and BOM notes:
|
||||||
|
//
|
||||||
|
// Go's string type is UTF-8 by default, and os.Stdout writes raw bytes.
|
||||||
|
// On Windows, some programs write a UTF-8 BOM (0xEF 0xBB 0xBF) to signal
|
||||||
|
// encoding, but Claude and most Unix tools do not expect or want a BOM.
|
||||||
|
//
|
||||||
|
// ctx never writes a BOM. All output is plain UTF-8. If a future caller
|
||||||
|
// needs a file with BOM (e.g. for Excel compatibility), that's a caller
|
||||||
|
// responsibility — not this package's job.
|
||||||
|
//
|
||||||
|
// Git Bash on Windows already uses UTF-8 for stdout when piped, so no
|
||||||
|
// runtime encoding conversion is needed.
|
||||||
50
internal/output/markdown.go
Normal file
50
internal/output/markdown.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// H1 writes a level-1 markdown heading followed by a blank line.
|
||||||
|
func H1(w io.Writer, text string) {
|
||||||
|
fmt.Fprintf(w, "# %s\n\n", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// H2 writes a level-2 markdown heading followed by a blank line.
|
||||||
|
func H2(w io.Writer, text string) {
|
||||||
|
fmt.Fprintf(w, "## %s\n\n", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// H3 writes a level-3 markdown heading followed by a blank line.
|
||||||
|
func H3(w io.Writer, text string) {
|
||||||
|
fmt.Fprintf(w, "### %s\n\n", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeBlock writes a fenced code block with an optional language identifier.
|
||||||
|
// If language is empty, the fence has no language tag.
|
||||||
|
func CodeBlock(w io.Writer, language, content string) {
|
||||||
|
fmt.Fprintf(w, "```%s\n%s\n```\n\n", language, strings.TrimRight(content, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyValue writes a single "**key:** value" line followed by a newline.
|
||||||
|
func KeyValue(w io.Writer, key, value string) {
|
||||||
|
fmt.Fprintf(w, "**%s:** %s\n", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulletList writes a markdown bullet list. Each item gets its own line.
|
||||||
|
// A blank line is written after the list.
|
||||||
|
func BulletList(w io.Writer, items []string) {
|
||||||
|
for _, item := range items {
|
||||||
|
fmt.Fprintf(w, "- %s\n", item)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section writes an H2 heading then calls body(w), then writes a blank line.
|
||||||
|
// Use this to group related output under a named section.
|
||||||
|
func Section(w io.Writer, title string, body func(w io.Writer)) {
|
||||||
|
H2(w, title)
|
||||||
|
body(w)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
31
internal/plugins/auto/auto.go
Normal file
31
internal/plugins/auto/auto.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Package auto implements the ctx auto plugin.
|
||||||
|
// Full implementation: prompt 2.
|
||||||
|
package auto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
core.Register(&autoPlugin{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type autoPlugin struct{}
|
||||||
|
|
||||||
|
func (a *autoPlugin) Name() string { return "auto" }
|
||||||
|
func (a *autoPlugin) Version() string { return "0.0.1" }
|
||||||
|
func (a *autoPlugin) ShortDescription() string { return "Auto-detect project stack and emit context" }
|
||||||
|
|
||||||
|
func (a *autoPlugin) Command(ctx *core.Context) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "auto",
|
||||||
|
Short: a.ShortDescription(),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Fprintln(ctx.Stderr, "Not implemented yet — coming in prompt 2")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/plugins/csharp/csharp.go
Normal file
31
internal/plugins/csharp/csharp.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Package csharp implements the ctx csharp plugin.
|
||||||
|
// Full implementation: prompts 4–6 (requires Roslyn helper from prompt 3).
|
||||||
|
package csharp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
core.Register(&csharpPlugin{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type csharpPlugin struct{}
|
||||||
|
|
||||||
|
func (c *csharpPlugin) Name() string { return "csharp" }
|
||||||
|
func (c *csharpPlugin) Version() string { return "0.0.1" }
|
||||||
|
func (c *csharpPlugin) ShortDescription() string { return "C# / .NET project analysis via Roslyn" }
|
||||||
|
|
||||||
|
func (c *csharpPlugin) Command(ctx *core.Context) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "csharp",
|
||||||
|
Short: c.ShortDescription(),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Fprintln(ctx.Stderr, "Not implemented yet — coming in prompt 4")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/plugins/git/git.go
Normal file
31
internal/plugins/git/git.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Package git implements the ctx git plugin.
|
||||||
|
// Full implementation: prompt 1.
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
core.Register(&gitPlugin{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitPlugin struct{}
|
||||||
|
|
||||||
|
func (g *gitPlugin) Name() string { return "git" }
|
||||||
|
func (g *gitPlugin) Version() string { return "0.0.1" }
|
||||||
|
func (g *gitPlugin) ShortDescription() string { return "Git repository summary for Claude" }
|
||||||
|
|
||||||
|
func (g *gitPlugin) Command(ctx *core.Context) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "git",
|
||||||
|
Short: g.ShortDescription(),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Fprintln(ctx.Stderr, "Not implemented yet — coming in prompt 1")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
11
internal/plugins/plugins.go
Normal file
11
internal/plugins/plugins.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Package plugins imports all built-in plugins to trigger their init()
|
||||||
|
// registration with the core registry. Import this package with a blank
|
||||||
|
// identifier in cmd/ctx/main.go.
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/ricarneiro/ctx/internal/plugins/auto"
|
||||||
|
_ "github.com/ricarneiro/ctx/internal/plugins/csharp"
|
||||||
|
_ "github.com/ricarneiro/ctx/internal/plugins/git"
|
||||||
|
_ "github.com/ricarneiro/ctx/internal/plugins/react"
|
||||||
|
)
|
||||||
31
internal/plugins/react/react.go
Normal file
31
internal/plugins/react/react.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Package react implements the ctx react plugin.
|
||||||
|
// Full implementation: phase 4 (future prompts).
|
||||||
|
package react
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ricarneiro/ctx/internal/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
core.Register(&reactPlugin{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type reactPlugin struct{}
|
||||||
|
|
||||||
|
func (r *reactPlugin) Name() string { return "react" }
|
||||||
|
func (r *reactPlugin) Version() string { return "0.0.1" }
|
||||||
|
func (r *reactPlugin) ShortDescription() string { return "React / TypeScript project analysis" }
|
||||||
|
|
||||||
|
func (r *reactPlugin) Command(ctx *core.Context) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "react",
|
||||||
|
Short: r.ShortDescription(),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Fprintln(ctx.Stderr, "Not implemented yet — coming in a future prompt")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
0
pkg/.gitkeep
Normal file
0
pkg/.gitkeep
Normal file
0
tools/.gitkeep
Normal file
0
tools/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user