Skip to content

Latest commit

 

History

History
443 lines (364 loc) · 14.3 KB

File metadata and controls

443 lines (364 loc) · 14.3 KB

Wick — AI-Agent-First Go Scaffolding CLI

Goal

CLI buat scaffold Go project yang dirancang buat AI agent kerja di dalamnya. User go install, lalu wick init <name>, dapat project lengkap: cmd/ di root handle server/worker/module registration, Makefile install tailwind.exe + templ, agent.md kasih agent konteks project.

Target Audience

  • Primary: AI coding agent (Claude Code, Cursor, dll). Konvensi, naming, file layout optimized biar agent langsung paham tanpa explorasi panjang.
  • Secondary: human developer yang collaborate sama agent.

Core Decisions

  • Scaffolding only. Wick nggak di-import runtime. Sekali pakai pas init, user OWN semua file hasil scaffold.
  • Distribution: go install github.com/you/wick@latest.
  • Template source: folder example/ — nested module, runnable, dual-purpose (template + integration test fixture).
  • Bundling: //go:embed all:example.
  • Filter pas init: skip *_test.go, testdata/, bin/, .env.
  • Dev toolchain via Makefile: make install download tailwindcss.exe + templ.exe ke ./bin/ (version-pinned, reproducible, no global install).
  • agent.md wajib di scaffolded project — agent-facing docs.

Scaffolded Project Structure

myapp/
├── go.mod
├── Makefile                    # install tailwind+templ, dev/build/test commands
├── main.go                     # entry — panggil cmd.Execute()
├── cmd/                        # ROOT cmd: init, server, worker, modules
│   ├── root.go                 # cobra root
│   ├── init.go                 # bootstrap hook (load env, seed, dll)
│   ├── server.go               # `myapp server` — start HTTP
│   ├── worker.go               # `myapp worker` — start background jobs
│   └── modules.go              # register tools + jobs (SATU tempat)
├── internal/
│   ├── config/                 # config loader
│   ├── server/                 # chi + middleware stack
│   ├── worker/                 # job runner
│   ├── tools/
│   │   └── sample/             # NewTools() pattern
│   └── jobs/
│       └── sample/             # NewJob() pattern
├── web/
│   ├── input.css               # tailwind source
│   ├── layout.templ
│   └── static/
│       └── output.css          # tailwind output (generated)
├── agent.md                    # AI agent instructions — WAJIB
├── README.md                   # human-facing
├── .claude/
│   ├── settings.json
│   └── skills/
├── .vscode/
│   ├── settings.json
│   ├── launch.json
│   └── extensions.json
├── .gitignore                  # ignore bin/, output.css, dll
├── Dockerfile
├── tailwind.config.js
└── bin/                        # generated by `make install` — NOT scaffolded
    ├── tailwindcss.exe
    └── templ.exe

Wick Repo Structure

wick/
├── go.mod                      # module: github.com/you/wick
├── main.go                     # CLI entry
├── cmd/
│   ├── root.go                 # cobra root (`wick`)
│   ├── init.go                 # `wick init <name>`
│   ├── init_test.go            # scaffold integration test
│   └── version.go              # `wick version`
└── example/                    # template + test fixture (mirror scaffolded layout)
    ├── go.mod                  # module: example
    ├── Makefile
    ├── main.go
    ├── cmd/
    ├── internal/
    ├── web/
    ├── agent.md
    ├── README.md
    ├── .claude/
    ├── .vscode/
    ├── .gitignore
    ├── Dockerfile
    ├── tailwind.config.js
    └── *_test.go               # integration tests (skipped pas init)

Key File Responsibilities

main.go

package main

import "example/cmd"

func main() { cmd.Execute() }

cmd/root.go — cobra root

Declare root command. Semua subcommand di-register via init() di file lain di package cmd.

cmd/init.go — bootstrap

Subcommand myapp init (optional): seed DB, generate secret, first-run setup. BUKAN runtime init (itu di server.go/worker.go).

cmd/server.go — HTTP server

var serverCmd = &cobra.Command{
    Use: "server",
    RunE: func(c *cobra.Command, args []string) error {
        cfg, err := config.Load()
        if err != nil { return err }

        s := server.New(cfg)
        w := worker.New(cfg)
        registerModules(s, w)         // <- dari modules.go

        return s.Start(c.Context())
    },
}
func init() { rootCmd.AddCommand(serverCmd) }

cmd/worker.go — background worker

Sama pattern — panggil registerModules(nil, w) lalu w.Start(ctx).

cmd/modules.go — SATU tempat register

package cmd

import (
    tsample "example/internal/tools/sample"
    jsample "example/internal/jobs/sample"
    "example/internal/server"
    "example/internal/worker"
)

func registerModules(s *server.Server, w *worker.Worker) {
    if s != nil {
        s.RegisterTool(tsample.NewTools())
    }
    if w != nil {
        w.RegisterJob(jsample.NewJob())
    }
}

Kenapa dipisah: agent tau persis di mana nambah tool/job baru. Satu grep registerModules → ketahuan semua wiring.

agent.md — AI agent instructions

Template content:

# Agent Instructions — myapp

## Project Overview
[1-2 lines — apa yang app ini lakuin]

## Stack
- Go + cobra (cmd/)
- chi router + templ + tailwindcss (web/)
- Background worker untuk jobs

## Where to Add What
- **Tool (UI/handler):** `internal/tools/<name>/` dengan `NewTools()` constructor, register di `cmd/modules.go`
- **Job (background):** `internal/jobs/<name>/` dengan `NewJob()` constructor, register di `cmd/modules.go`
- **Route tambahan manual:** `internal/server/routes.go`
- **Config field baru:** `internal/config/config.go`

## Commands (via Makefile)
- `make install` — install tailwindcss.exe + templ.exe ke ./bin/ (sekali di awal)
- `make dev` — templ watch + tailwind watch + `go run . server`
- `make build` — generate templ, build tailwind, compile binary
- `make test` — `go test ./...`

## Dev Loop
1. Edit `.templ` file → `templ generate` (auto via `make dev`)
2. Edit `web/input.css` → tailwind regenerate `web/static/output.css` (auto)
3. Restart server untuk Go changes

## Testing Convention
- Unit test di `*_test.go` sebelahan file-nya
- Integration test pakai stub session (`internal/auth/testhelper.go`)
- SSO/external service → mock via interface

Makefile

TAILWIND_VERSION := 3.4.0
TEMPL_VERSION    := 0.2.747

BIN        := bin
TAILWIND   := $(BIN)/tailwindcss.exe
TEMPL      := $(BIN)/templ.exe

.PHONY: install dev build test clean

install: $(TAILWIND) $(TEMPL)

$(TAILWIND):
	@mkdir -p $(BIN)
	curl -L -o $@ https://github.com/tailwindlabs/tailwindcss/releases/download/v$(TAILWIND_VERSION)/tailwindcss-windows-x64.exe

$(TEMPL):
	@mkdir -p $(BIN)
	GOBIN=$(abspath $(BIN)) go install github.com/a-h/templ/cmd/templ@v$(TEMPL_VERSION)

dev: install
	$(TEMPL) generate --watch &
	$(TAILWIND) -i web/input.css -o web/static/output.css --watch &
	go run . server

build: install
	$(TEMPL) generate
	$(TAILWIND) -i web/input.css -o web/static/output.css --minify
	go build -o $(BIN)/myapp.exe .

test:
	go test ./...

clean:
	rm -rf $(BIN) web/static/output.css

Kenapa ./bin/ bukan global install:

  • Version-pinned per project
  • Agent nggak perlu check system PATH
  • Reproducible di CI
  • make clean benar-benar clean

wick init Flow

  1. wick init myapp
  2. Walk embed.FS rooted di example/
  3. Per file, filter:
    • Skip *_test.go (test-only, Wick-internal)
    • Skip testdata/ directory
    • Skip bin/ directory (generated)
    • Skip .env
    • Skip web/static/output.css (generated)
  4. Path rewrite: example/... → myapp/...
  5. Content rewrite:
    • module example → module myapp
    • "example/internal/... → "myapp/internal/...
    • Makefile: -o $(BIN)/example.exe → -o $(BIN)/myapp.exe
  6. Print next steps (agent-friendly):
    ✓ Scaffolded myapp/
    
    Next:
      cd myapp
      make install    # install tailwindcss.exe + templ.exe
      make dev        # start server + watchers
    
    Read agent.md untuk konvensi project.
    

Implementation Sketch

wick/cmd/init.go

//go:embed all:example
var tmpl embed.FS

func runInit(name string) error {
    if _, err := os.Stat(name); err == nil {
        return fmt.Errorf("directory %q already exists", name)
    }

    return fs.WalkDir(tmpl, "example", func(path string, d fs.DirEntry, err error) error {
        if err != nil { return err }
        base := filepath.Base(path)

        if strings.HasSuffix(base, "_test.go") { return nil }
        if d.IsDir() && (base == "testdata" || base == "bin") { return fs.SkipDir }
        if base == ".env" { return nil }
        if path == "example/web/static/output.css" { return nil }

        target := strings.Replace(path, "example", name, 1)
        if d.IsDir() {
            return os.MkdirAll(target, 0755)
        }

        data, err := tmpl.ReadFile(path)
        if err != nil { return err }
        data = bytes.ReplaceAll(data, []byte("module example"), []byte("module "+name))
        data = bytes.ReplaceAll(data, []byte(`"example/`), []byte(`"`+name+`/`))
        data = bytes.ReplaceAll(data, []byte("example.exe"), []byte(name+".exe"))
        return os.WriteFile(target, data, 0644)
    })
}

Development Loop

Edit example/ langsung (90% kerjaan)

cd example
make install
make dev

Rebuild Wick CLI

go install .

End-to-end smoke

rm -rf /tmp/foo
wick init /tmp/foo
cd /tmp/foo
make install && make build

Testing Strategy

Layer 1 — Scaffold integration test (wick/cmd/init_test.go)

Table-driven. Scaffold ke t.TempDir(), assert struktur + build sukses.

func TestInitScaffold(t *testing.T) {
    tests := []struct {
        name         string
        projectName  string
        mustExist    []string
        mustNotExist []string
        runCmds      [][]string
    }{
        {
            name:        "agent-ready scaffold",
            projectName: "myapp",
            mustExist: []string{
                "go.mod", "main.go", "Makefile",
                "agent.md",                                  // critical untuk AI
                "cmd/root.go", "cmd/modules.go", "cmd/server.go",
                "internal/tools/sample", "internal/jobs/sample",
                "web/input.css", "web/layout.templ",
                ".claude/settings.json",
                ".vscode/settings.json",
                ".gitignore", "Dockerfile", "tailwind.config.js",
            },
            mustNotExist: []string{
                "testdata", "bin",
                "cmd/init_test.go",
                "web/static/output.css",
            },
            runCmds: [][]string{
                {"go", "mod", "tidy"},
                {"go", "build", "./..."},
                {"go", "vet", "./..."},
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            dir := t.TempDir()
            target := filepath.Join(dir, tt.projectName)
            require.NoError(t, runInit(target))

            for _, p := range tt.mustExist {
                _, err := os.Stat(filepath.Join(target, p))
                assert.NoError(t, err, "missing %s", p)
            }
            for _, p := range tt.mustNotExist {
                _, err := os.Stat(filepath.Join(target, p))
                assert.Error(t, err, "should not exist: %s", p)
            }
            for _, args := range tt.runCmds {
                cmd := exec.Command(args[0], args[1:]...)
                cmd.Dir = target
                out, err := cmd.CombinedOutput()
                assert.NoError(t, err, "%v failed:\n%s", args, out)
            }
        })
    }
}

Layer 2 — Behavior tests di example/*_test.go

Di-skip pas init. Cover real routing/middleware/authz pakai stub session (guest/user/admin matrix, 401/403/404). SSO → mock only.

Layer 3 — Makefile smoke (optional, slow)

CI step terpisah: wick init /tmp/x && cd /tmp/x && make install && make build. Verify tailwind + templ beneran jalan. Lambat (network download) — jalanin nightly, bukan per-PR.

Embed Gotcha — Dotfiles

//go:embed default skip file/folder diawali . atau _. Fix: prefix all::

//go:embed all:example
var tmpl embed.FS

Scaffold integration test cover gotcha ini via mustExist: {".claude", ".vscode", ".gitignore"}.

Commands (MVP)

Command Fungsi
wick init <name> Scaffold project baru
wick version Print versi Wick

Scaffolded project punya command sendiri (via Makefile + cobra):

make install Download tailwindcss.exe + templ.exe ke ./bin/
make dev Watchers + server
make build Build production binary
./bin/myapp.exe server Run HTTP server
./bin/myapp.exe worker Run background worker

Out of Scope (MVP)

  • wick upgrade — template migration. User own hasil scaffold, upgrade manual.
  • Multi-template (--template=api-only) — start single template, split nanti.
  • Cross-platform Makefile — windows-focused dulu (.exe, curl). Nanti detect OS.
  • SSO integration testing — mock only, butuh secrets.
  • Wick as runtime lib — scaffolding only, user OWN code hasil scaffold.

Next Steps

  1. Bikin repo wick/ — main.go + cmd/root.go + cmd/init.go + cmd/version.go.
  2. Bikin example/ lengkap:
    • Makefile (install tailwind + templ)
    • main.go, cmd/{root,init,server,worker,modules}.go
    • internal/{config,server,worker,tools/sample,jobs/sample}/
    • web/{input.css,layout.templ} + sample page
    • agent.md (isi: overview, konvensi, commands, dev loop)
    • .claude/, .vscode/, .gitignore, Dockerfile, tailwind.config.js, README.md
  3. Pastiin cd example && make install && make dev jalan end-to-end.
  4. Implement cmd/init.go (embed + walk + rewrite + filter).
  5. Scaffold integration test (cmd/init_test.go) — table-driven, build output.
  6. Write behavior tests di example/ (guest/user/admin matrix).
  7. Polish: wick version, README di wick repo (pitch "agent-first scaffolding"), semver v0.1.0.