chore: initial scaffold with plugin system and placeholders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo Carneiro 2026-04-27 13:43:10 -03:00
commit 69cadb4ea6
23 changed files with 763 additions and 0 deletions

23
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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]
}

View 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.

View 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)
}

View 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
},
}
}

View File

@ -0,0 +1,31 @@
// Package csharp implements the ctx csharp plugin.
// Full implementation: prompts 46 (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
},
}
}

View 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
},
}
}

View 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"
)

View 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
View File

0
tools/.gitkeep Normal file
View File