diff --git a/AGENTS.md b/AGENTS.md index c0f986c..b331e8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -240,6 +240,20 @@ spark - **全局 innate-fe-cli**:`bun build --compile` 打出单二进制后以 NTFS 硬链接装入 `~/.local/bin`(`innate-fe` / `innate-fe-cli` 各含 `.exe`,4 名共享 1 份磁盘数据)。 +### 9. 从零构建教程与 SKILL 文档(2026-10-21,issue #24) +- **新增手把手教程**(与 golang-cli-101 的概念课互补,代码可照抄执行): + - `docs/{zh,en}/quick-start/golang-project-from-scratch.md` — Go CLI 项目从零创建: + `go mod init` 生成 go.mod 的过程逐行解释、CLI 三件套(Cobra/Viper/PTerm)依赖清单与安装、 + cmd/internal/pkg 标准分层职责、第一个 Cobra 命令、Taskfile 逐段解读 + 最小模板、 + 附 SKILL `golang-project-from-scratch`。 + - `docs/{zh,en}/quick-start/docs-site-from-scratch.md` — docmd 文档站从零创建: + `npm init` 与 package.json 字段逐个解释(含 Go↔Node 对照表)、docmd 安装、 + docmd.config.js 配置讲解、navigation.json 与双语 i18n、GitHub Actions 发布 + GitHub Pages(`.github/workflows/docs.yml` 全文 + Settings → Pages → Source: GitHub Actions)、 + 附 SKILL `docs-site-from-scratch`。 +- **导航**:`docs/{zh,en}/navigation.json` 快速开始组新增两页;SKILL 以附录代码块形式内嵌 + (沿用 golang-cli-101 附录 B 的安装约定,存入 `.claude/skills//SKILL.md` 生效)。 + ## CLI 命令完整参考 ### 全局选项 diff --git a/docs/en/navigation.json b/docs/en/navigation.json index e347f1a..ab3d1fa 100644 --- a/docs/en/navigation.json +++ b/docs/en/navigation.json @@ -16,6 +16,14 @@ { "title": "Golang CLI Essentials · Lesson 1", "path": "/quick-start/golang-cli-101" + }, + { + "title": "Go CLI Project from Scratch", + "path": "/quick-start/golang-project-from-scratch" + }, + { + "title": "Docs Site from Scratch", + "path": "/quick-start/docs-site-from-scratch" } ] }, diff --git a/docs/en/quick-start/docs-site-from-scratch.md b/docs/en/quick-start/docs-site-from-scratch.md new file mode 100644 index 0000000..28cb988 --- /dev/null +++ b/docs/en/quick-start/docs-site-from-scratch.md @@ -0,0 +1,328 @@ +# Docs Site from Scratch and GitHub Pages Publishing + +The other half of the beginner track: [Go CLI Project from +Scratch](golang-project-from-scratch.md) covers the binary; this tutorial +covers the **Node world** — starting with `npm init`, generating a docs site +with docmd, and auto-publishing to GitHub Pages via GitHub Actions. This very +site (the spark-cli docs) was built with exactly this flow. + +You will walk away with: + +1. A standard Node project with `package.json` +2. A docmd-powered Markdown docs site (previewable locally) +3. A GitHub Pages site that auto-deploys on every push to main +4. An installable SKILL document for AI assistants + +--- + +## 0. Prerequisites + +Install Node.js 20+ (); npm ships with it, but +[pnpm](https://pnpm.io/) works too — both spellings are given below. Verify: + +```bash +node -v # v20.x or newer +npm -v +``` + +## 1. Step 1: `npm init` — where package.json comes from + +A Node project likewise starts by **declaring itself**: + +```bash +mkdir my-docs && cd my-docs +npm init -y +``` + +`-y` accepts all defaults. The generated `package.json`: + +```json +{ + "name": "my-docs", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "docmd dev", + "build": "docmd build", + "preview": "npx serve site" + }, + "dependencies": { + "@docmd/core": "^0.9.8", + "@docmd/plugin-search": "0.9.8", + "@docmd/plugin-seo": "0.9.8", + "@docmd/plugin-sitemap": "0.9.8" + } +} +``` + +(That's the **final** state after installing dependencies; fresh +`npm init -y` only writes the first few fields.) Field by field: + +| Field | Purpose | Notes | +|-------|---------|-------| +| `name` / `version` | Project name and version | Only matters when publishing to npm; private projects can be casual | +| `private: true` | Prevents accidental npm publishing | Always set this on a docs project | +| `type: "module"` | Use ES Module syntax project-wide | docmd's config uses `export default`, so this is required | +| `scripts` | Command aliases | `npm run build` = `docmd build`; turns long commands short | +| `dependencies` | Production dependency list | Written automatically by `npm install ` | + +It maps one-to-one onto Go: + +| Go | Node (this project) | Purpose | +|----|---------------------|---------| +| `go.mod` | `package.json` | Project + dependency declaration | +| `go.sum` | `package-lock.json` / `pnpm-lock.yaml` | Lock dependency versions | +| `go get ` | `npm install ` | Add a dependency | +| `go mod tidy` | `npm install` | Install what's declared | +| `go build` | `npm run build` | Produce artifacts | +| GOMODCACHE | `node_modules` (pnpm: hard links from a store) | Where dependencies live on disk | + +## 2. Step 2: install docmd + +[docmd](https://docmd.dev) is the generator behind this site: it reads +Markdown and emits static HTML with search, theme switching, i18n, mermaid, +and sitemap built in. + +```bash +npm install @docmd/core @docmd/plugin-search @docmd/plugin-seo @docmd/plugin-sitemap +``` + +Three things happen: `node_modules/` appears, `package.json`'s `dependencies` +is updated, and the lockfile is written — **all three are results, don't edit +them by hand**. + +Two more plugins spark uses (optional): + +| Plugin | Purpose | +|--------|---------| +| `@docmd/plugin-llms` | Generates `llms.txt` so AI assistants can ingest the whole site | +| `@docmd/plugin-mermaid` | Renders Mermaid diagrams | + +> pnpm users: just swap `npm install` for `pnpm add` / `pnpm install` — +> dependencies arrive as hard links from the global store (on Windows the store +> can live on D:, see [How to Build](../usage/how-to-build.md)). + +## 3. Step 3: `docmd.config.js` — the site's master switch + +Create `docmd.config.js` at the project root (trimmed from this repo's real +config): + +```js +// docmd.config.js +export default defineConfig({ + title: 'My Docs', + url: 'https://.github.io/my-docs', // Deploy URL; drives sitemap and SEO + + src: 'docs', // Markdown source directory + out: 'site', // Build output directory (gitignore it) + + layout: { + sidebar: { collapsible: true }, // Collapsible sidebar + }, + theme: { name: 'sky' }, // Built-in theme + + i18n: { + default: 'en', // Default locale renders at the root URL + locales: [ + { id: 'zh', label: '简体中文' }, // Other locale at /zh/ + { id: 'en', label: 'English' }, + ], + }, + + plugins: { + search: {}, // Matches the installed @docmd/plugin-search + sitemap: { defaultChangefreq: 'weekly' }, + }, +}); +``` + +The key sections: + +- **`src` / `out`**: input and output. `site/` is pure static files — safe to + delete and rebuild at any time +- **`title` / `url`**: site metadata; `url` must match the final GitHub Pages + address +- **`i18n`**: the `default` locale renders at the root URL, others mount under + `//`; one directory per locale (`docs/zh/`, `docs/en/`), each with + its own `navigation.json` for the sidebar +- **`plugins`**: keys map to `@docmd/plugin-*` packages; `{}` enables defaults + +## 4. Step 4: write your first page + +``` +docs/ +├── zh/ +│ ├── index.md # Site home (the h1 becomes the page title) +│ ├── navigation.json # Chinese sidebar +│ └── quick-start/ +│ └── hello.md # Your first page +└── en/ # English mirror (structure must mirror zh/ exactly) +``` + +`docs/en/navigation.json`: + +```json +[ + { "title": "Home", "path": "/" }, + { + "title": "Quick Start", + "icon": "rocket", + "collapsible": true, + "children": [ + { "title": "Hello", "path": "/quick-start/hello" } + ] + } +] +``` + +The body is plain Markdown; the page title is taken from the first `#`, no +front matter needed. The bilingual rule: **every new page exists in both +`zh/` and `en/`, and both `navigation.json` files register it** (if a +translation lags behind, docmd falls back to the default locale instead of +404ing). + +## 5. Step 5: local development and build + +```bash +npm run dev # docmd dev: local dev server, Markdown edits hot-reload +npm run build # docmd build: output into site/ +npm run preview # npx serve site: preview the static output like production +``` + +`site/` contains self-contained static HTML + search index + sitemap — +**add it to `.gitignore`** (build artifacts stay out of the repo; CI builds +them fresh). + +## 6. Step 6: publish to GitHub Pages + +The recommended way is "GitHub Actions + Pages artifact" — push to main +triggers a build and deploy. This repo's `.github/workflows/docs.yml` in full +(copy it as-is): + +```yaml +name: Deploy Docs to GitHub Pages + +on: + push: + branches: [main] + paths: # Only trigger when docs change, saves runner minutes + - 'docs/**' + - 'docmd.config.js' + workflow_dispatch: # Keep a manual trigger + +permissions: + contents: read + pages: write # Allow writing to Pages + id-token: write # OIDC deploy credential + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 20 } + - name: Install dependencies + run: npm install + - name: Build docs + run: npx docmd build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: { path: site } # Package site/ as a Pages artifact + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # Publish the artifact +``` + +Three steps to enable it: + +1. Commit the workflow as `.github/workflows/docs.yml` +2. In the repo, **Settings → Pages → Build and deployment → Source**, choose + **GitHub Actions** (not a branch — branch mode disables the Actions flow) +3. Push to main. Once Actions finishes, visit `https://.github.io/my-docs/` + +From then on, every change to `docs/**` or `docmd.config.js` merged into main +updates the site automatically — that's how this site ships. + +> Manual publishing works too (not recommended): build locally with +> `npm run build`, push `site/` to a `gh-pages` branch, and point the Pages +> source at it. Downsides: easy to forget, easy to skip the lockfile. + +## 7. Let spark do it for you + +spark turned this tutorial's scaffolding into commands: + +```bash +spark docs init # Create the standard docs/ directory structure (usage/ spec/ features/ ...; skips existing) +spark docs site # Detect title and Pages URL from the git remote, generate docmd.config.js, + # run npm init -y if package.json is missing, and install docmd deps +``` + +See the [Docs Management commands](../usage/docs-cmd.md). + +--- + +## Appendix: SKILL — docs-site-from-scratch + +Save as `SKILL.md` (e.g. `.claude/skills/docs-site-from-scratch/SKILL.md`) +and an AI assistant can build a docmd docs site and deploy it to GitHub Pages +for you: + +```markdown +--- +name: docs-site-from-scratch +description: Create a docmd docs site from scratch and publish it to GitHub + Pages (npm init, package.json, docmd.config.js, navigation.json, bilingual + i18n, Actions auto-deploy). Use when the user asks to "set up a docs site / + create a docs project / publish docs to GitHub Pages". +--- + +# Docs Site from Scratch + +Execute in this order, confirming with the user after each step: + +1. **Project init** — npm init -y; package.json must contain "private": true + and "type": "module" (docmd's config is ESM); explain scripts and + dependencies. +2. **Install docmd** — npm install @docmd/core plus plugins as needed + (search/seo/sitemap/llms/mermaid); remind that node_modules and the lockfile + are install results, never hand-edited. +3. **Write docmd.config.js** — the src/out/title/url essentials; multiple + languages via i18n.locales (default renders at the root URL, others at + //); plugin keys map to plugin packages. +4. **Organize docs** — docs// directories plus one navigation.json per + locale; bodies are plain Markdown, titles come from the h1; in bilingual + sites every new page is registered in both locales. +5. **Local verification** — npm run dev preview → npm run build into site/ → + add site/ to .gitignore. +6. **Publish** — write .github/workflows/docs.yml (push to main + paths filter + + upload-pages-artifact + deploy-pages, two jobs); set repo Settings → + Pages → Source to GitHub Actions; push and verify. + If the target repo has spark-cli installed, steps 3-4 can be replaced by + spark docs init / spark docs site. + +Reference implementation: github.com/variableway/spark-cli's docs/ +(en mirror of a zh-default site); its docmd.config.js and +.github/workflows/docs.yml can be copied directly. +``` + +--- + +## Further reading + +- [Go CLI Project from Scratch](golang-project-from-scratch.md) — the other half of this project: the Go world +- [How to Build](../usage/how-to-build.md) — Go vs Node build comparison, pnpm global config +- [Docs Management commands](../usage/docs-cmd.md) — `spark docs init` / `spark docs site` diff --git a/docs/en/quick-start/golang-project-from-scratch.md b/docs/en/quick-start/golang-project-from-scratch.md new file mode 100644 index 0000000..5405331 --- /dev/null +++ b/docs/en/quick-start/golang-project-from-scratch.md @@ -0,0 +1,377 @@ +# Build a Standard Go CLI Project from Scratch + +A hands-on tutorial for absolute beginners: start from zero code, end with a +standard Go CLI project that builds, tests, and ships with a Taskfile. +[Golang CLI Essentials · Lesson 1](golang-cli-101.md) explains the *what*; +this tutorial is the *how* — every command works as-is on Windows, Linux, +and macOS. + +Using the spark project as the template, you will walk away with: + +1. A proper module with `go.mod` / `go.sum` +2. A CLI powered by the Cobra + Viper + PTerm trio +3. The standard `cmd/` + `internal/` layering +4. A cross-platform `Taskfile.yml` +5. An installable SKILL document for AI assistants + +--- + +## 0. Prerequisites + +Install Go (1.22+ is fine; this project uses 1.27) from , +then verify: + +```bash +go version +# go version go1.27.0 windows/amd64 +``` + +## 1. Step 1: `go mod init` — where go.mod comes from + +A Go project starts not with code but by **declaring a module** — the import +path prefix for everything in the project: + +```bash +mkdir mycli && cd mycli +go mod init github.com/yourname/mycli +``` + +The generated `go.mod` is just two lines: + +``` +module github.com/yourname/mycli + +go 1.27.0 +``` + +| Line | Meaning | +|------|---------| +| `module` | The module path. Every package's import path is prefixed with it; convention is the repository URL (spark just uses `spark` — short local names work too) | +| `go` | The minimum Go language version required | + +There is **no** `go.sum` and no `require` block yet — both grow automatically +when you add dependencies: + +```bash +go get github.com/spf13/cobra@latest +``` + +Afterwards `go.mod` gains a dependency entry and `go.sum` appears (per-dependency +hash checksums, like a Node lockfile): + +``` +module github.com/yourname/mycli + +go 1.27.0 + +require github.com/spf13/cobra v1.10.2 +``` + +> Memory hook: **`go mod init` creates the skeleton, `go get` adds dependencies, +> `go.sum` is the tamper-proof receipt.** Both `go.mod` and `go.sum` must be +> committed to Git, otherwise a fresh clone cannot reproduce the build. + +## 2. Step 2: install the CLI trio + +A decent CLI needs a few wheels. Here are spark's direct dependencies +(the `require` block of its `go.mod`): + +| Dependency | Purpose | Install | +|------------|---------|---------| +| `github.com/spf13/cobra` | Commands / subcommands / flags | `go get github.com/spf13/cobra@latest` | +| `github.com/spf13/viper` | Config loading (yaml + env + flags) | `go get github.com/spf13/viper@latest` | +| `github.com/pterm/pterm` | Pretty terminal output (colors, tables, spinners) | `go get github.com/pterm/pterm@latest` | +| `github.com/charmbracelet/bubbletea` (+ `bubbles` / `lipgloss`) | Interactive TUI | `go get github.com/charmbracelet/bubbletea@latest` | +| `github.com/onsi/ginkgo/v2` + `gomega` | BDD test framework | `go get github.com/onsi/ginkgo/v2@latest` | +| `modernc.org/sqlite` | Pure-Go SQLite driver (used by `spark git scan`) | `go get modernc.org/sqlite@latest` | +| `go.yaml.in/yaml/v3` | YAML parsing (registry files) | `go get go.yaml.in/yaml/v3@latest` | + +Once everything is in, tidy up — drop unused deps, fill in missing checksums: + +```bash +go mod tidy +``` + +What installation actually does: sources are downloaded to `GOMODCACHE` +(on C: by default; `go env -w` can move it) and **compiled into your binary** +at `go build` time — no `node_modules`, still a single output file. + +## 3. Step 3: the standard directory layout + +Compare with spark's real layout (full version in [AGENTS.md](../Agents.md)): + +``` +mycli/ +├── main.go # Entry point: just calls cmd.Execute(), no business logic +├── cmd/ # "Command layer": one package per command group +│ ├── root.go # Root command, global flags, config bootstrap +│ └── magic/ # Subcommand group (spark has git/ repo/ magic/ script/ docs/) +│ └── flush_dns.go +├── internal/ # "Business layer": the real work; compiler forbids external imports +│ ├── config/ # Config loading +│ ├── git/ # Git operation wrappers +│ └── ... +├── pkg/ # "Shared layer": public libraries other projects may import +│ └── witr/model/ +├── tools/ # Auxiliary CLIs (spark's test-report renderer) +├── scripts/ # Install/verify scripts (bash + powershell pairs) +├── docs/ # Docs site (see the docs-site tutorial) +├── go.mod / go.sum # Dependency manifest + checksums (must be committed) +├── Makefile # Unix build entry +└── Taskfile.yml # Cross-platform build entry (preferred on Windows) +``` + +| Directory | Responsibility | In one sentence | +|-----------|----------------|-----------------| +| `main.go` | Program entry | Does exactly one thing: call `cmd.Execute()` | +| `cmd/` | Command definitions | Cobra commands and flag bindings; thin, no business logic | +| `internal/` | Business logic | Go-enforced: other modules cannot import from here | +| `pkg/` | Public libraries | Only put things here if you want them reused | +| `scripts/` | Build helpers | Binary installation, sha256 verification | + +The complete `main.go` (spark is exactly this): + +```go +package main + +import "spark/cmd" + +func main() { + cmd.Execute() +} +``` + +> Memory hook: **commands in `cmd/`, logic in `internal/`, the entry point is +> just a messenger.** + +## 4. Step 4: your first command + +`cmd/root.go` defines the root command and global flags (excerpted from spark): + +```go +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var rootCmd = &cobra.Command{ + Use: "mycli", + Short: "A CLI tool that does useful things", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + return + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringSliceP("path", "p", []string{"."}, "Directory to scan") + viper.BindPFlag("repo-path", rootCmd.PersistentFlags().Lookup("path")) +} +``` + +One subcommand per file (modeled on `cmd/magic/flush_dns.go`): + +```go +package magic + +import ( + "github.com/spf13/cobra" + "github.com/pterm/pterm" +) + +var flushDNSCmd = &cobra.Command{ + Use: "flush-dns", + Short: "Flush DNS cache on the current system", + RunE: func(cmd *cobra.Command, args []string) error { + pterm.Success.Println("DNS cache flushed!") + return nil + }, +} + +func init() { + MagicCmd.AddCommand(flushDNSCmd) +} +``` + +Three key points: + +1. `Use` is the command name; `RunE` is the body (return an error instead of + calling `os.Exit` yourself) +2. `func init()` calls `AddCommand` to attach the subcommand — every file + self-registers, no central registry needed +3. Output through `pterm` gives users colored success/warning messages + +Verify: + +```bash +go run . flush-dns +go build -o bin/mycli . && ./bin/mycli --help +``` + +## 5. Step 5: writing a Taskfile build script + +**Why a Taskfile**: real build commands get long (ldflags version stamping, +installing to `~/.local/bin`, sha256 verification…); retyping them is +impractical, and Windows often lacks `make`. [Task](https://taskfile.dev) is a +Go-written task runner — a single binary, YAML-declared, cross-platform — +that fills the gap. + +First, the skeleton of spark's `Taskfile.yml`: + +```yaml +version: '3' + +vars: # Global variables + BINARY_NAME: spark + BIN_DIR: bin + VERSION: # Dynamic variable: sh: runs a shell command, uses its output + sh: cat internal/witr/version/VERSION 2>/dev/null || echo dev + +env: + CGO_ENABLED: '0' # Pure-Go build, produces a portable static binary + +tasks: + build: + desc: Build for current OS into bin/, stamp version, install to ~/.local/bin + cmds: + - task: prepare-bin # Reference another task (creates bin/ first) + - cmd: '{{.GO}} build -ldflags="-s -w -X spark/internal/witr/version.Version=v{{.VERSION}}" -o {{.BIN_DIR}}/{{.BINARY_NAME}}{{exeExt}} main.go' + - cmd: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install-binary.ps1 ... + platforms: [windows] # Platform dispatch: Windows/Linux each run their own + - cmd: bash scripts/install-binary.sh ... + platforms: [linux, darwin] + + cross-build: # Internal task: hidden from task --list, reused by cross-compiles + internal: true + requires: + vars: [GOOS, GOARCH, OUTPUT] # Callers must provide these + env: + GOOS: '{{.GOOS}}' # Cross-compiling only needs these env vars + GOARCH: '{{.GOARCH}}' + cmds: + - '{{.GO}} build -o {{.OUTPUT}} main.go' + + build:linux: # Public target: fill in vars, then call cross-build + cmds: + - task: cross-build + vars: { GOOS: linux, GOARCH: amd64, OUTPUT: 'bin/spark_linux' } +``` + +The essentials, one by one: + +- **`vars`**: literal values, or `sh:` to run a shell command and capture output +- **`cmds`**: executed in order; `task:` references another task (a function call) +- **`platforms`**: per-OS dispatch within one task — the core of cross-platform support +- **`internal: true`**: hidden helper task, not shown by `task --list` +- **`requires.vars`**: forces callers to pass arguments; missing ones fail fast +- **`{{exeExt}}`**: built-in Task variable — `.exe` on Windows, empty elsewhere +- **`ignore_error: true`**: tolerate a failure and keep going (e.g. run all tests, + then render the report) + +A minimal template for a new project: + +```yaml +version: '3' + +vars: + BINARY_NAME: mycli + BIN_DIR: bin + +tasks: + build: + desc: Build for the current OS + cmds: + - go build -o {{.BIN_DIR}}/{{.BINARY_NAME}}{{exeExt}} . + + test: + desc: Run all tests + cmds: + - go test ./... + + lint: + desc: Run go vet + cmds: + - go vet ./... + + clean: + desc: Remove build artifacts + cmds: + - cmd: powershell -NoProfile -Command "Remove-Item -Recurse -Force {{.BIN_DIR}}" + platforms: [windows] + - cmd: rm -rf {{.BIN_DIR}} + platforms: [linux, darwin] +``` + +Install Task itself: `go install github.com/go-task/task/v3/cmd/task@latest`, +then run `task --list` to see all targets and `task build` to start using them. +The Makefile and Taskfile in this project expose identical targets, so +Linux/macOS users can equally use `make`. + +## 6. Wrap-up: the pre-commit checklist + +```text +✅ go.mod / go.sum committed (never gitignore them) +✅ bin/ added to .gitignore (build artifacts stay out of the repo) +✅ go mod tidy run, no stray dependencies +✅ go vet ./... passes +✅ go test ./... passes +``` + +When installing to `~/.local/bin` on Windows, keep both `mycli.exe` and the +extension-less `mycli` (bash prefers the latter) — that's what spark's +`install-binary.ps1` does. + +--- + +## Appendix: SKILL — golang-project-from-scratch + +Save as `SKILL.md` (e.g. `.claude/skills/golang-project-from-scratch/SKILL.md`) +and an AI assistant can scaffold a standard Go CLI project for you the spark way: + +```markdown +--- +name: golang-project-from-scratch +description: Create a Go CLI project from scratch following the standard flow + (go mod init, the Cobra/Viper/PTerm trio, cmd+internal layering, Taskfile + builds). Use when the user asks to "create a Go CLI project / scaffold a Go + project / standardize a Go project layout". +--- + +# Build a Standard Go CLI Project from Scratch + +Execute in this order, confirming with the user after each step: + +1. **Module init** — `go mod init ` (use the repo URL); explain the + module/go lines; state that go.mod and go.sum must be committed. +2. **Dependencies** — `go get` as needed: cobra (commands), viper (config), + pterm (output), bubbletea (TUI), ginkgo+gomega (BDD tests); finish with + `go mod tidy`. +3. **Layering** — main.go only calls cmd.Execute(); cmd/ defines commands (one + file per subcommand, self-registered via AddCommand in init()); all business + logic in internal/; only genuinely reusable code in pkg/; build helpers in + scripts/. +4. **First command** — root command plus a demo subcommand (Use/Short/RunE + trio, RunE returns an error); verify with `go run .`, then + `go build -o bin/ .`. +5. **Taskfile** — write Taskfile.yml (vars/cmds/platforms essentials; cross + compile via GOOS/GOARCH plus an internal cross-build task with + requires.vars; {{exeExt}} for the suffix); provide build/test/lint/clean. +6. **Wrap-up** — gitignore bin/; run go vet ./... and go test ./.... + +Reference implementation: github.com/variableway/spark-cli (Cobra+Viper+PTerm, +Makefile and Taskfile as dual entry points). Build details in its +docs/en/usage/how-to-build.md. +``` + +--- + +## Further reading + +- [Golang CLI Essentials · Lesson 1](golang-cli-101.md) — the concepts; do that first, then this +- [How to Build](../usage/how-to-build.md) — building an existing project, cross-compiling, pnpm setup +- [Docs Site from Scratch](docs-site-from-scratch.md) — the other half of this project: the Node world diff --git a/docs/zh/navigation.json b/docs/zh/navigation.json index f39e42f..ef8565b 100644 --- a/docs/zh/navigation.json +++ b/docs/zh/navigation.json @@ -16,6 +16,14 @@ { "title": "Golang CLI 入门 · 第一课", "path": "/quick-start/golang-cli-101" + }, + { + "title": "从零创建 Go CLI 项目", + "path": "/quick-start/golang-project-from-scratch" + }, + { + "title": "从零创建文档站", + "path": "/quick-start/docs-site-from-scratch" } ] }, diff --git a/docs/zh/quick-start/docs-site-from-scratch.md b/docs/zh/quick-start/docs-site-from-scratch.md new file mode 100644 index 0000000..04225b6 --- /dev/null +++ b/docs/zh/quick-start/docs-site-from-scratch.md @@ -0,0 +1,310 @@ +# 从零创建文档站并发布到 GitHub Pages + +面向零基础的另一半教程:[从零创建 Go CLI 项目](golang-project-from-scratch.md) +讲二进制,本篇讲 **Node 体系**——用 `npm init` 起步、用 docmd 生成文档站、 +用 GitHub Actions 自动发布到 GitHub Pages。本站(spark-cli 文档站)就是 +照这个流程搭起来的。 + +学完你将拥有: + +1. 一个带 `package.json` 的标准 Node 项目 +2. docmd 驱动的 Markdown 文档站(本地可预览) +3. 推送到 main 即自动发布的 GitHub Pages 站点 +4. 一个可以安装给 AI 助手的 SKILL 文档 + +--- + +## 0. 准备工作 + +安装 Node.js 20+(),包管理器用 npm(自带)或 +[pnpm](https://pnpm.io/) 均可,本篇两种写法都给出。验证: + +```bash +node -v # v20.x 或更高 +npm -v +``` + +## 1. 第一步:`npm init` —— package.json 是怎么来的 + +Node 项目的起点同样是**声明一个项目**: + +```bash +mkdir my-docs && cd my-docs +npm init -y +``` + +`-y` 表示所有问题都用默认值。生成的 `package.json`: + +```json +{ + "name": "my-docs", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "docmd dev", + "build": "docmd build", + "preview": "npx serve site" + }, + "dependencies": { + "@docmd/core": "^0.9.8", + "@docmd/plugin-search": "0.9.8", + "@docmd/plugin-seo": "0.9.8", + "@docmd/plugin-sitemap": "0.9.8" + } +} +``` + +(上面是**装完依赖后**的最终形态;`npm init -y` 初始只有前几个字段。) +逐个字段解释: + +| 字段 | 作用 | 备注 | +|------|------|------| +| `name` / `version` | 项目名与版本 | 发布到 npm 才有意义,私有项目随便填 | +| `private: true` | 禁止意外发布到 npm | 文档站项目务必加上 | +| `type: "module"` | 整个项目用 ES Module 语法 | docmd 的配置文件用 `export default`,必须加 | +| `scripts` | 命令别名 | `npm run build` = `docmd build`,把长命令变短 | +| `dependencies` | 生产依赖清单 | 由 `npm install ` 自动写入 | + +和 Go 完全对得上的对照表: + +| Go | Node(本项目) | 作用 | +|----|----------------|------| +| `go.mod` | `package.json` | 声明项目与依赖 | +| `go.sum` | `package-lock.json` / `pnpm-lock.yaml` | 锁定依赖版本 | +| `go get ` | `npm install ` | 添加依赖 | +| `go mod tidy` | `npm install` | 按声明安装依赖 | +| `go build` | `npm run build` | 生成产物 | +| GOMODCACHE | `node_modules`(pnpm 则是硬链接自 store) | 依赖落盘位置 | + +## 2. 第二步:安装 docmd + +[docmd](https://docmd.dev) 是本站使用的文档站生成器:读 Markdown,产静态 HTML, +自带搜索 / 主题切换 / 中英双语 / mermaid / sitemap。 + +```bash +npm install @docmd/core @docmd/plugin-search @docmd/plugin-seo @docmd/plugin-sitemap +``` + +装完会发生三件事:`node_modules/` 出现依赖、`package.json` 的 +`dependencies` 被更新、lockfile 被写入——**这三个都是结果,不要手改**。 + +spark 用到的另外两个插件(按需选装): + +| 插件 | 作用 | +|------|------| +| `@docmd/plugin-llms` | 生成 `llms.txt`,方便 AI 助手读取全站内容 | +| `@docmd/plugin-mermaid` | 渲染 Mermaid 图表 | + +> pnpm 用户把 `npm install` 换成 `pnpm add` / `pnpm install` 即可, +> 依赖以硬链接方式来自全局 store(Windows 上可把 store 指到 D 盘省 C 盘, +> 见 [How to Build](../usage/how-to-build.md))。 + +## 3. 第三步:`docmd.config.js` —— 站点的总开关 + +项目根目录新建 `docmd.config.js`(精简自本仓库真实配置): + +```js +// docmd.config.js +export default defineConfig({ + title: 'My Docs', + url: 'https://.github.io/my-docs', // 部署地址,影响 sitemap 与 SEO + + src: 'docs', // Markdown 源文件目录 + out: 'site', // 构建产物目录(gitignore 掉) + + layout: { + sidebar: { collapsible: true }, // 侧边栏可折叠 + }, + theme: { name: 'sky' }, // 内置主题 + + i18n: { + default: 'zh', // 默认语言渲染在根 URL + locales: [ + { id: 'zh', label: '简体中文' }, + { id: 'en', label: 'English' }, // 英文渲染在 /en/ + ], + }, + + plugins: { + search: {}, // 对应已安装的 @docmd/plugin-search + sitemap: { defaultChangefreq: 'weekly' }, + }, +}); +``` + +关键段解释: + +- **`src` / `out`**:输入输出。`site/` 是纯静态文件,随时可删掉重建 +- **`title` / `url`**:站点元信息,`url` 要与 GitHub Pages 最终地址一致 +- **`i18n`**:`default` 语言渲染在根 URL,其他语言自动挂在 `//` 下; + 每种语言一个目录(`docs/zh/`、`docs/en/`),各自的 + `navigation.json` 定义侧边栏 +- **`plugins`**:键名对应 `@docmd/plugin-*` 包,`{}` 即启用默认配置 + +## 4. 第四步:写第一篇文档 + +``` +docs/ +├── zh/ +│ ├── index.md # 站点首页(h1 会成为页面标题) +│ ├── navigation.json # 中文侧边栏 +│ └── quick-start/ +│ └── hello.md # 你的第一篇文档 +└── en/ # 英文镜像(结构必须与 zh/ 平行) +``` + +`docs/zh/navigation.json`: + +```json +[ + { "title": "首页", "path": "/" }, + { + "title": "快速开始", + "icon": "rocket", + "collapsible": true, + "children": [ + { "title": "Hello", "path": "/quick-start/hello" } + ] + } +] +``` + +正文就是普通 Markdown;页面标题自动取第一个 `#`,无需 front matter。 +双语站点的规则:**每新增一页,`zh/` 与 `en/` 各写一份,两个 +`navigation.json` 同步登记**(翻译没跟上时 docmd 会自动回退到默认语言, +不会 404)。 + +## 5. 第五步:本地开发与构建 + +```bash +npm run dev # docmd dev:本地开发服务器,改 Markdown 即时刷新 +npm run build # docmd build:产出到 site/ +npm run preview # npx serve site:像线上一样预览静态产物 +``` + +`site/` 里是自包含的静态 HTML + 搜索索引 + sitemap,**加入 `.gitignore`** +(构建产物不入库,CI 里现构建)。 + +## 6. 第六步:发布到 GitHub Pages + +推荐「GitHub Actions + Pages artifact」方式——推送到 main 自动构建发布, +本仓库的 `.github/workflows/docs.yml` 全文如下(可直接复制): + +```yaml +name: Deploy Docs to GitHub Pages + +on: + push: + branches: [main] + paths: # 只有文档变了才触发,省 runner 时间 + - 'docs/**' + - 'docmd.config.js' + workflow_dispatch: # 保留手动触发入口 + +permissions: + contents: read + pages: write # 允许写入 Pages + id-token: write # OIDC 部署凭证 + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 20 } + - name: Install dependencies + run: npm install + - name: Build docs + run: npx docmd build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: { path: site } # 把 site/ 打包成 Pages artifact + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # 把 artifact 发布上线 +``` + +三步开通: + +1. 把上面的 workflow 存为 `.github/workflows/docs.yml` 并提交 +2. 仓库 **Settings → Pages → Build and deployment → Source** 选 + **GitHub Actions**(不要选分支,选了分支模式 Actions 方式不生效) +3. 推送到 main。Actions 跑完后访问 `https://.github.io/my-docs/` + +之后每次改动 `docs/**` 或 `docmd.config.js` 并合入 main,站点自动更新—— +这就是本站的发布方式。 + +> 手动发布也行(不推荐):本地 `npm run build` 后把 `site/` 推到 +> `gh-pages` 分支,Pages Source 选该分支。缺点是容易忘、容易漏 lockfile。 + +## 7. 用 spark 加速以上全部 + +spark 把本教程的脚手架做成了命令: + +```bash +spark docs init # 创建 docs/ 标准目录结构(usage/ spec/ features/ ...,已存在则跳过) +spark docs site # 从 git remote 自动探测标题与 Pages 地址,生成 docmd.config.js, + # 缺 package.json 时自动 npm init -y,并装好 docmd 依赖 +``` + +详见 [文档管理命令](../usage/docs-cmd.md)。 + +--- + +## 附录:SKILL —— docs-site-from-scratch + +保存为 `SKILL.md`(如 `.claude/skills/docs-site-from-scratch/SKILL.md`), +AI 助手即可帮你从零搭建 docmd 文档站并发布到 GitHub Pages: + +```markdown +--- +name: docs-site-from-scratch +description: 从零创建 docmd 文档站并发布到 GitHub Pages(npm init、package.json、 + docmd.config.js、navigation.json、双语 i18n、Actions 自动部署)。当用户要求 + "搭文档站 / 建文档项目 / 发布文档到 GitHub Pages"时使用。 +--- + +# 从零创建 docmd 文档站 + +按以下顺序执行,每步完成后向用户确认再继续: + +1. **项目初始化** — npm init -y;package.json 必须含 "private": true 与 + "type": "module"(docmd 配置用 ESM);解释 scripts 与 dependencies 的作用。 +2. **安装 docmd** — npm install @docmd/core + 按需插件(search/seo/sitemap/llms/mermaid); + 提醒 node_modules 与 lockfile 是安装结果,不要手改。 +3. **写 docmd.config.js** — src/out/title/url 四要素;多语言用 i18n.locales + (default 渲染在根 URL,其他在 //);plugins 键名对应插件包。 +4. **组织文档** — docs// 目录 + 每语言一份 navigation.json;正文纯 + Markdown,标题取 h1;双语站点每新增一页两个 locale 同步登记。 +5. **本地验证** — npm run dev 预览 → npm run build 产出到 site/ → + site/ 加入 .gitignore。 +6. **发布** — 写 .github/workflows/docs.yml(push main + paths 过滤 + + upload-pages-artifact + deploy-pages 两段式);仓库 Settings → Pages → + Source 选 GitHub Actions;推送验证。 + 若目标仓库装有 spark-cli,可用 spark docs init / spark docs site 代替 3-4 步。 + +参考实现:github.com/variableway/spark-cli 的 docs/(zh 默认 + en 镜像), +docmd.config.js 与 .github/workflows/docs.yml 可直接对照。 +``` + +--- + +## 延伸阅读 + +- [从零创建 Go CLI 项目](golang-project-from-scratch.md) —— 本项目的另一半:Go 体系 +- [How to Build — 构建指南](../usage/how-to-build.md) —— Go 与 Node 构建对照、pnpm 全局地址 +- [文档管理命令](../usage/docs-cmd.md) —— `spark docs init` / `spark docs site` diff --git a/docs/zh/quick-start/golang-project-from-scratch.md b/docs/zh/quick-start/golang-project-from-scratch.md new file mode 100644 index 0000000..1d4a7eb --- /dev/null +++ b/docs/zh/quick-start/golang-project-from-scratch.md @@ -0,0 +1,362 @@ +# 从零创建一个标准 Go CLI 项目 + +面向零基础的手把手教程:从一行代码都没有,到一个能构建、能测试、带 Taskfile 的 +标准 Go CLI 项目。[Golang CLI 入门 · 第一课](golang-cli-101.md) 讲「是什么」, +本篇讲「怎么做」——所有命令在 Windows / Linux / macOS 上都能照抄执行。 + +以 spark 项目为模板,学完你将拥有: + +1. 一个带 `go.mod` / `go.sum` 的模块化项目 +2. Cobra + Viper + PTerm 三件套驱动的命令行工具 +3. `cmd/` + `internal/` 的标准分层结构 +4. 一份可跨平台运行的 `Taskfile.yml` +5. 一个可以安装给 AI 助手的 SKILL 文档 + +--- + +## 0. 准备工作 + +安装 Go(1.22 以上即可,本项目用 1.27),从 下载安装包, +装完验证: + +```bash +go version +# go version go1.27.0 windows/amd64 +``` + +## 1. 第一步:`go mod init` —— go.mod 是怎么来的 + +Go 项目的起点不是写代码,而是**声明一个模块**(module)。模块就是「这个项目 +的 import 路径前缀」: + +```bash +mkdir mycli && cd mycli +go mod init github.com/yourname/mycli +``` + +生成的 `go.mod` 只有两行: + +``` +module github.com/yourname/mycli + +go 1.27.0 +``` + +| 行 | 含义 | +|----|------| +| `module` | 模块路径。本项目内所有包的 import 路径都以它为前缀,习惯上填仓库地址(spark 填的是 `spark`,本地开发也可以这样简写) | +| `go` | 本项目要求的最低 Go 语言版本 | + +此刻还**没有** `go.sum`、没有 `require` 段——它们都是装依赖时自动长出来的: + +```bash +go get github.com/spf13/cobra@latest +``` + +执行后 `go.mod` 多出依赖声明,同时生成 `go.sum`(每个依赖的哈希校验,防篡改, +相当于 Node 的 lockfile): + +``` +module github.com/yourname/mycli + +go 1.27.0 + +require github.com/spf13/cobra v1.10.2 +``` + +> 记忆点:**`go mod init` 造骨架,`go get` 添依赖,`go.sum` 是防篡改的小票。** +> `go.mod` 和 `go.sum` 都必须提交进 Git,否则别人 clone 后无法复现构建。 + +## 2. 第二步:装上 CLI 三件套 + +一个像样的 CLI 工具离不开这几类轮子。下表是 spark 实际依赖的直接依赖 +(见 `go.mod` 的 `require` 段): + +| 依赖 | 作用 | 安装 | +|------|------|------| +| `github.com/spf13/cobra` | 命令 / 子命令 / flag 框架 | `go get github.com/spf13/cobra@latest` | +| `github.com/spf13/viper` | 配置读取(yaml + 环境变量 + flag) | `go get github.com/spf13/viper@latest` | +| `github.com/pterm/pterm` | 终端美化输出(颜色、表格、进度条) | `go get github.com/pterm/pterm@latest` | +| `github.com/charmbracelet/bubbletea`(+ `bubbles` / `lipgloss`) | 交互式 TUI | `go get github.com/charmbracelet/bubbletea@latest` | +| `github.com/onsi/ginkgo/v2` + `gomega` | BDD 测试框架 | `go get github.com/onsi/ginkgo/v2@latest` | +| `modernc.org/sqlite` | 纯 Go 的 SQLite 驱动(`spark git scan` 用) | `go get modernc.org/sqlite@latest` | +| `go.yaml.in/yaml/v3` | YAML 解析(registry 文件) | `go get go.yaml.in/yaml/v3@latest` | + +全部装完跑一次整理,把没用的删掉、把缺的校验补齐: + +```bash +go mod tidy +``` + +安装行为发生了什么:源码下载到 `GOMODCACHE`(默认在 C 盘,可 `go env -w` +指到其他盘),`go build` 时**编译进你的二进制**——没有 `node_modules`, +产物仍是一个文件。 + +## 3. 第三步:标准目录结构 + +对照 spark 的真实结构(完整版见 [AGENTS.md](../Agents.md)): + +``` +mycli/ +├── main.go # 入口:只调 cmd.Execute(),不写业务 +├── cmd/ # 「命令层」:每个命令组一个包 +│ ├── root.go # 根命令、全局 flag、配置初始化 +│ └── magic/ # 子命令组(spark 有 git/ repo/ magic/ script/ docs/) +│ └── flush_dns.go +├── internal/ # 「业务层」:真正干活的代码,编译器禁止外部 import +│ ├── config/ # 配置加载 +│ ├── git/ # Git 操作封装 +│ └── ... +├── pkg/ # 「共享层」:可以被外部项目 import 的公共库 +│ └── witr/model/ +├── tools/ # 辅助 CLI(spark 的 test-report 渲染器) +├── scripts/ # 安装/校验脚本(bash + powershell 双份) +├── docs/ # 文档站(见「从零创建文档站」教程) +├── go.mod / go.sum # 依赖清单 + 校验(必须提交) +├── Makefile # Unix 构建入口 +└── Taskfile.yml # 跨平台构建入口(Windows 首选) +``` + +| 目录 | 职责 | 一句话 | +|------|------|--------| +| `main.go` | 程序入口 | 只做一件事:调用 `cmd.Execute()` | +| `cmd/` | 命令定义 | Cobra 命令、flag 绑定;薄,不含业务 | +| `internal/` | 业务逻辑 | Go 的硬约束:别的模块 import 不到这里 | +| `pkg/` | 公共库 | 想被别人复用才放这里 | +| `scripts/` | 构建辅助 | 安装二进制、校验 sha256 | + +`main.go` 全文(spark 就是这样): + +```go +package main + +import "spark/cmd" + +func main() { + cmd.Execute() +} +``` + +> 记忆点:**命令在 `cmd/`,逻辑在 `internal/`,入口只当传话筒。** + +## 4. 第四步:第一个命令 + +`cmd/root.go` 定义根命令与全局 flag(节选自 spark): + +```go +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var rootCmd = &cobra.Command{ + Use: "mycli", + Short: "A CLI tool that does useful things", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + return + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringSliceP("path", "p", []string{"."}, "Directory to scan") + viper.BindPFlag("repo-path", rootCmd.PersistentFlags().Lookup("path")) +} +``` + +子命令一个文件一个(模仿 `cmd/magic/flush_dns.go` 的骨架): + +```go +package magic + +import ( + "github.com/spf13/cobra" + "github.com/pterm/pterm" +) + +var flushDNSCmd = &cobra.Command{ + Use: "flush-dns", + Short: "Flush DNS cache on the current system", + RunE: func(cmd *cobra.Command, args []string) error { + pterm.Success.Println("DNS cache flushed!") + return nil + }, +} + +func init() { + MagicCmd.AddCommand(flushDNSCmd) +} +``` + +三个关键点: + +1. `Use` 是命令名,`RunE` 是执行体(返回 error 而不是自己 `os.Exit`) +2. `func init()` 里 `AddCommand` 把子命令挂到父命令——每个文件自注册, + 不需要中心化的注册表 +3. 输出用 `pterm`,用户看到的就是带颜色的成功/警告提示 + +验证: + +```bash +go run . flush-dns +go build -o bin/mycli . && ./bin/mycli --help +``` + +## 5. 第五步:写 Taskfile 构建脚本 + +**为什么需要 Taskfile**:构建命令很长(ldflags 注入版本号、装到 `~/.local/bin`、 +校验 sha256……),每次手敲不现实;Windows 上又常常没有 `make`。 +[Task](https://taskfile.dev) 是 Go 写的任务运行器,单个二进制、YAML 声明、 +跨平台,正好补位。 + +先解读 spark 的 `Taskfile.yml` 骨架: + +```yaml +version: '3' + +vars: # 全局变量 + BINARY_NAME: spark + BIN_DIR: bin + VERSION: # 动态变量:冒号后是 shell 命令,取其输出 + sh: cat internal/witr/version/VERSION 2>/dev/null || echo dev + +env: + CGO_ENABLED: '0' # 纯 Go 构建,产出可移植的静态二进制 + +tasks: + build: + desc: Build for current OS into bin/, stamp version, install to ~/.local/bin + cmds: + - task: prepare-bin # 引用另一个 task(先建 bin/ 目录) + - cmd: '{{.GO}} build -ldflags="-s -w -X spark/internal/witr/version.Version=v{{.VERSION}}" -o {{.BIN_DIR}}/{{.BINARY_NAME}}{{exeExt}} main.go' + - cmd: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install-binary.ps1 ... + platforms: [windows] # 平台分派:同一 task 里 Windows/Linux 各跑各的 + - cmd: bash scripts/install-binary.sh ... + platforms: [linux, darwin] + + cross-build: # 内部 task:不显示在 task --list,供交叉编译复用 + internal: true + requires: + vars: [GOOS, GOARCH, OUTPUT] # 调用方必须提供这三个变量 + env: + GOOS: '{{.GOOS}}' # 跨平台编译只需这两个环境变量 + GOARCH: '{{.GOARCH}}' + cmds: + - '{{.GO}} build -o {{.OUTPUT}} main.go' + + build:linux: # 对外目标:填好变量再调 cross-build + cmds: + - task: cross-build + vars: { GOOS: linux, GOARCH: amd64, OUTPUT: 'bin/spark_linux' } +``` + +要点逐条: + +- **`vars`**:静态值直接写;`sh:` 开头表示运行 shell 命令取结果 +- **`cmds`**:按顺序执行;`task:` 引用别的任务(相当于函数调用) +- **`platforms`**:同一条目按操作系统分派,这是跨平台的核心 +- **`internal: true`**:内部任务,`task --list` 不显示 +- **`requires.vars`**:强制调用方传参,漏了会直接报错 +- **`{{exeExt}}`**:Task 内置变量,Windows 上是 `.exe`,其他平台空串 +- **`ignore_error: true`**:容忍失败继续(如 `test-report` 里先跑完所有测试再出报告) + +最小可用模板(新项目直接抄): + +```yaml +version: '3' + +vars: + BINARY_NAME: mycli + BIN_DIR: bin + +tasks: + build: + desc: Build for the current OS + cmds: + - go build -o {{.BIN_DIR}}/{{.BINARY_NAME}}{{exeExt}} . + + test: + desc: Run all tests + cmds: + - go test ./... + + lint: + desc: Run go vet + cmds: + - go vet ./... + + clean: + desc: Remove build artifacts + cmds: + - cmd: powershell -NoProfile -Command "Remove-Item -Recurse -Force {{.BIN_DIR}}" + platforms: [windows] + - cmd: rm -rf {{.BIN_DIR}} + platforms: [linux, darwin] +``` + +安装 Task 本体:`go install github.com/go-task/task/v3/cmd/task@latest`, +然后 `task --list` 查看全部目标,`task build` 开始使用。 +Makefile 与 Taskfile 在本项目中目标一一对应,Linux/macOS 用户用 `make` 也行。 + +## 6. 收尾:提交前的清单 + +```text +✅ go.mod / go.sum 已提交(绝不能进 .gitignore) +✅ bin/ 已加入 .gitignore(构建产物不入库) +✅ go mod tidy 跑过,没有多余依赖 +✅ go vet ./... 通过 +✅ go test ./... 通过 +``` + +Windows 安装到 `~/.local/bin` 时,记得同时放 `mycli.exe` 与无后缀的 +`mycli` 两份(bash 优先命中后者),这是 spark 的 `install-binary.ps1` 的做法。 + +--- + +## 附录:SKILL —— golang-project-from-scratch + +保存为 `SKILL.md`(如 `.claude/skills/golang-project-from-scratch/SKILL.md`), +AI 助手即可按 spark 的标准流程帮你从零创建 Go CLI 项目: + +```markdown +--- +name: golang-project-from-scratch +description: 按标准流程从零创建一个 Go CLI 项目(go mod init、Cobra/Viper/PTerm + 三件套、cmd+internal 分层、Taskfile 构建)。当用户要求"新建 Go CLI 项目 / + 搭 Go 项目骨架 / 标准化 Go 项目结构"时使用。 +--- + +# 从零创建标准 Go CLI 项目 + +按以下顺序执行,每步完成后向用户确认再继续: + +1. **模块初始化** — `go mod init `(模块路径用仓库地址); + 解释 go.mod 的 module/go 行,声明 go.mod 与 go.sum 必须提交。 +2. **依赖安装** — 按需 `go get`:cobra(命令)、viper(配置)、pterm(输出)、 + bubbletea(TUI)、ginkgo+gomega(BDD 测试);收尾 `go mod tidy`。 +3. **目录分层** — main.go 只调 cmd.Execute();cmd/ 定义命令(每个子命令一个 + 文件,init() 里 AddCommand 自注册);业务逻辑全部进 internal/;需要被外部 + 复用的才进 pkg/;构建辅助脚本进 scripts/。 +4. **第一个命令** — 根命令 + 一个演示子命令(Use/Short/RunE 三件套,RunE 返回 + error);`go run .` 验证后 `go build -o bin/ .`。 +5. **Taskfile** — 写 Taskfile.yml(vars/cmds/platforms 三要素;交叉编译用 + GOOS/GOARCH + requires.vars 的内部 cross-build task;{{exeExt}} 处理后缀); + 提供 build/test/lint/clean 四个基础目标。 +6. **收尾** — .gitignore 掉 bin/;跑 go vet ./... 与 go test ./...。 + +参考实现:github.com/variableway/spark-cli(Cobra+Viper+PTerm,Makefile 与 +Taskfile 双入口)。构建细节参考其 docs/zh/usage/how-to-build.md。 +``` + +--- + +## 延伸阅读 + +- [Golang CLI 入门 · 第一课](golang-cli-101.md) —— 概念课,先学这个再看本篇 +- [How to Build — 构建指南](../usage/how-to-build.md) —— 已有项目的构建、交叉编译与 pnpm 环境 +- [从零创建文档站](docs-site-from-scratch.md) —— 本项目的另一半:Node 体系