diff --git a/Makefile b/Makefile index 59a40c6..d74871b 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ GO_FILES := $(shell find $(GO_DIR) $(GO_COBRA_DIR) $(EXAMPLE_GO_DIR) -name '*.go # ── Quality ────────────────────────────────────────────────────────────────── -.PHONY: check test test-ts test-go test-go-cobra test-rust test-python fmt fmt-ts fmt-go fmt-rust fmt-python +.PHONY: check test test-ts test-go test-go-cobra test-go-release test-rust test-python fmt fmt-ts fmt-go fmt-rust fmt-python check: ## Full parity gate node scripts/check.mjs @@ -31,10 +31,13 @@ test-ts: ## Run TypeScript tests pnpm --dir $(TS_DIR) test test-go: ## Run Go SDK tests - cd $(GO_DIR) && go test ./... + cd $(GO_DIR) && GOWORK=off go test ./... test-go-cobra: ## Run Go Cobra adapter tests - cd $(GO_COBRA_DIR) && go test ./... + sh scripts/check-go-cobra.sh + +test-go-release: ## Verify packaged Go modules from an external consumer + sh scripts/check-go-release.sh test-rust: ## Run Rust SDK tests cargo test --manifest-path $(RUST_DIR)/Cargo.toml @@ -62,11 +65,13 @@ fmt-python: ## Lint and format Python code .PHONY: generate generate-check -generate: ## Refresh generated host constants +generate: ## Refresh generated host constants and Go test fixtures node scripts/sync-hosts.mjs + node scripts/sync-go-testdata.mjs -generate-check: ## Verify generated host constants +generate-check: ## Verify generated host constants and Go test fixtures node scripts/sync-hosts.mjs --check + node scripts/sync-go-testdata.mjs --check # ── Examples ───────────────────────────────────────────────────────────────── diff --git a/docs/RELEASE.md b/docs/RELEASE.md index a67d090..6489819 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -55,6 +55,16 @@ Do not tag the release branch. Do not publish packages by hand during the normal The release workflow publishes npm, PyPI, and crates.io packages, creates the `go/vX.Y.Z` and `go-cobra/vX.Y.Z` tags, creates the GitHub Release, and runs the public install smoke check. +The Go modules share the same release version. No workspace or local replacement +is committed. Source checks copy the modules to a temporary directory and add a +one-off replacement there so the Cobra adapter can test unreleased core APIs. +Release checks disable workspace resolution and verify both module archives from +a temporary consumer: + +```bash +make test-go-release +``` + ## First npm Release npm trusted publishing is configured in the npm package settings. For the first package version, the package settings may not exist yet. diff --git a/go-cobra/go.mod b/go-cobra/go.mod index e5e8257..32c7a5c 100644 --- a/go-cobra/go.mod +++ b/go-cobra/go.mod @@ -11,5 +11,3 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.6 // indirect ) - -replace github.com/lathe-cli/kitup/go => ../go diff --git a/go-cobra/go.sum b/go-cobra/go.sum index ffae55e..abfc725 100644 --- a/go-cobra/go.sum +++ b/go-cobra/go.sum @@ -1,6 +1,8 @@ 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/lathe-cli/kitup/go v0.1.3 h1:7eEW8mDr5MbXFaTwr2dlnH8ebg+3Kmhj2/j4d9YfQNQ= +github.com/lathe-cli/kitup/go v0.1.3/go.mod h1:dZgJDmFRKjaFBZyaP1qlzOB9IEafnf1/ai4KX4hMA4c= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= diff --git a/go-cobra/skill_test.go b/go-cobra/skill_test.go index 15a1639..4096048 100644 --- a/go-cobra/skill_test.go +++ b/go-cobra/skill_test.go @@ -6,16 +6,25 @@ import ( "path/filepath" "strings" "testing" + "testing/fstest" kitup "github.com/lathe-cli/kitup/go" ) +func testBundle() kitup.SkillBundle { + return kitup.FSBundle(fstest.MapFS{ + "basic/SKILL.md": { + Data: []byte("---\nname: basic\ndescription: Basic skill.\n---\n"), + }, + }, "basic") +} + func TestSkillCommandInstallsWithCoreFlags(t *testing.T) { home := t.TempDir() var out bytes.Buffer cmd := NewSkillCommand(Options{ AppID: "example-cli", - Bundle: kitup.DirectoryBundle(filepath.Join("..", "testdata", "skills", "basic")), + Bundle: testBundle(), Home: home, Out: &out, }) @@ -36,7 +45,7 @@ func TestInstallCommandPromptsForScopeBeforeInstall(t *testing.T) { var out bytes.Buffer cmd := NewSkillCommand(Options{ AppID: "example-cli", - Bundle: kitup.DirectoryBundle(filepath.Join("..", "testdata", "skills", "basic")), + Bundle: testBundle(), Home: home, CWD: workspace, StdinTTY: true, @@ -71,7 +80,7 @@ func TestInstallCommandForceOverwritesUnmanaged(t *testing.T) { var out bytes.Buffer cmd := NewSkillCommand(Options{ AppID: "example-cli", - Bundle: kitup.DirectoryBundle(filepath.Join("..", "testdata", "skills", "basic")), + Bundle: testBundle(), Home: home, Out: &out, }) @@ -89,7 +98,7 @@ func TestInstallCommandForceOverwritesUnmanaged(t *testing.T) { func TestInstallCommandReturnsCoreFlagError(t *testing.T) { cmd := NewInstallCommand(Options{ AppID: "example-cli", - Bundle: kitup.DirectoryBundle(filepath.Join("..", "testdata", "skills", "basic")), + Bundle: testBundle(), Home: t.TempDir(), }) cmd.SetArgs([]string{"--scope", "bad"}) diff --git a/go/kitup_test.go b/go/kitup_test.go index 23bb6c3..4e213d4 100644 --- a/go/kitup_test.go +++ b/go/kitup_test.go @@ -28,7 +28,7 @@ type goldenCase struct { func TestGoldenCases(t *testing.T) { var file goldenFile - readJSON(t, "../testdata/cases/bundled-skill-install.json", &file) + readJSON(t, "testdata/cases/bundled-skill-install.json", &file) for _, tc := range file.Cases { t.Run(tc.ID, func(t *testing.T) { root := t.TempDir() @@ -530,7 +530,7 @@ func repoPathFromCase(path string) string { if filepath.IsAbs(path) { return path } - return filepath.Join("..", path) + return path } func hostIDs(hosts []Host) []string { diff --git a/go/spec/hosts.json b/go/spec/hosts.json new file mode 100644 index 0000000..d1af3c3 --- /dev/null +++ b/go/spec/hosts.json @@ -0,0 +1,1107 @@ +{ + "$schema": "./hosts.schema.json", + "schemaVersion": 1, + "hosts": [ + { + "id": "adal", + "displayName": "AdaL", + "projectSkillsDirs": [ + ".adal/skills" + ], + "userSkillsDirs": [ + "~/.adal/skills" + ], + "detect": [ + "~/.adal" + ], + "status": "community" + }, + { + "id": "aider-desk", + "displayName": "AiderDesk", + "projectSkillsDirs": [ + ".aider-desk/skills" + ], + "userSkillsDirs": [ + "~/.aider-desk/skills" + ], + "detect": [ + "~/.aider-desk" + ], + "status": "community" + }, + { + "id": "amp", + "displayName": "Amp", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.config/agents/skills" + ], + "detect": [ + "~/.config/amp", + "~/.config/agents" + ], + "status": "community" + }, + { + "id": "antigravity", + "displayName": "Antigravity", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.gemini/antigravity/skills" + ], + "detect": [ + "~/.gemini/antigravity" + ], + "status": "community" + }, + { + "id": "antigravity-cli", + "displayName": "Antigravity CLI", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.gemini/antigravity-cli/skills" + ], + "detect": [ + "~/.gemini/antigravity-cli" + ], + "status": "community" + }, + { + "id": "astrbot", + "displayName": "AstrBot", + "projectSkillsDirs": [ + "data/skills" + ], + "userSkillsDirs": [ + "~/.astrbot/data/skills" + ], + "detect": [ + "~/.astrbot", + "data/skills", + "~/.astrbot/data" + ], + "status": "community" + }, + { + "id": "augment", + "displayName": "Augment", + "projectSkillsDirs": [ + ".augment/skills" + ], + "userSkillsDirs": [ + "~/.augment/skills" + ], + "detect": [ + "~/.augment" + ], + "status": "community" + }, + { + "id": "autohand-code", + "displayName": "Autohand Code CLI", + "projectSkillsDirs": [ + ".autohand/skills" + ], + "userSkillsDirs": [ + "~/.autohand/skills" + ], + "detect": [ + "~/.autohand" + ], + "status": "community" + }, + { + "id": "bob", + "displayName": "IBM Bob", + "projectSkillsDirs": [ + ".bob/skills" + ], + "userSkillsDirs": [ + "~/.bob/skills" + ], + "detect": [ + "~/.bob" + ], + "status": "community" + }, + { + "id": "claude-code", + "displayName": "Claude Code", + "projectSkillsDirs": [ + ".claude/skills" + ], + "userSkillsDirs": [ + "~/.claude/skills" + ], + "detect": [ + "~/.claude" + ], + "status": "verified" + }, + { + "id": "cline", + "displayName": "Cline", + "projectSkillsDirs": [ + ".agents/skills", + ".cline/skills", + ".clinerules/skills", + ".claude/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills", + "~/.cline/skills" + ], + "detect": [ + "~/.cline", + "~/.agents" + ], + "status": "documented" + }, + { + "id": "codearts-agent", + "displayName": "CodeArts Agent", + "projectSkillsDirs": [ + ".codeartsdoer/skills" + ], + "userSkillsDirs": [ + "~/.codeartsdoer/skills" + ], + "detect": [ + "~/.codeartsdoer" + ], + "status": "community" + }, + { + "id": "codebuddy", + "displayName": "CodeBuddy", + "projectSkillsDirs": [ + ".codebuddy/skills" + ], + "userSkillsDirs": [ + "~/.codebuddy/skills" + ], + "detect": [ + "~/.codebuddy", + ".codebuddy" + ], + "status": "community" + }, + { + "id": "codemaker", + "displayName": "Codemaker", + "projectSkillsDirs": [ + ".codemaker/skills" + ], + "userSkillsDirs": [ + "~/.codemaker/skills" + ], + "detect": [ + "~/.codemaker" + ], + "status": "community" + }, + { + "id": "codestudio", + "displayName": "Code Studio", + "projectSkillsDirs": [ + ".codestudio/skills" + ], + "userSkillsDirs": [ + "~/.codestudio/skills" + ], + "detect": [ + "~/.codestudio" + ], + "status": "community" + }, + { + "id": "codex", + "displayName": "Codex", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills", + "~/.codex/skills" + ], + "detect": [ + "~/.codex", + "~/.agents/skills", + "~/.agents" + ], + "status": "verified", + "notes": [ + "Keep both ~/.agents/skills and ~/.codex/skills for compatibility." + ] + }, + { + "id": "command-code", + "displayName": "Command Code", + "projectSkillsDirs": [ + ".commandcode/skills" + ], + "userSkillsDirs": [ + "~/.commandcode/skills" + ], + "detect": [ + "~/.commandcode" + ], + "status": "community" + }, + { + "id": "continue", + "displayName": "Continue", + "projectSkillsDirs": [ + ".continue/skills" + ], + "userSkillsDirs": [ + "~/.continue/skills" + ], + "detect": [ + "~/.continue", + ".continue" + ], + "status": "community" + }, + { + "id": "cortex", + "displayName": "Cortex Code", + "projectSkillsDirs": [ + ".cortex/skills" + ], + "userSkillsDirs": [ + "~/.snowflake/cortex/skills" + ], + "detect": [ + "~/.snowflake/cortex" + ], + "status": "community" + }, + { + "id": "crush", + "displayName": "Crush", + "projectSkillsDirs": [ + ".crush/skills" + ], + "userSkillsDirs": [ + "~/.config/crush/skills" + ], + "detect": [ + "~/.config/crush" + ], + "status": "community" + }, + { + "id": "cursor", + "displayName": "Cursor", + "projectSkillsDirs": [ + ".agents/skills", + ".cursor/skills" + ], + "userSkillsDirs": [ + "~/.cursor/skills", + "~/.agents/skills" + ], + "detect": [ + "~/.cursor", + "~/.agents" + ], + "status": "documented" + }, + { + "id": "deepagents", + "displayName": "Deep Agents", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.deepagents/agent/skills" + ], + "detect": [ + "~/.deepagents", + "~/.deepagents/agent" + ], + "status": "community" + }, + { + "id": "devin", + "displayName": "Devin for Terminal", + "projectSkillsDirs": [ + ".devin/skills" + ], + "userSkillsDirs": [ + "~/.config/devin/skills" + ], + "detect": [ + "~/.config/devin" + ], + "status": "community" + }, + { + "id": "dexto", + "displayName": "Dexto", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills" + ], + "detect": [ + "~/.dexto", + "~/.agents" + ], + "status": "community" + }, + { + "id": "droid", + "displayName": "Droid", + "projectSkillsDirs": [ + ".factory/skills" + ], + "userSkillsDirs": [ + "~/.factory/skills" + ], + "detect": [ + "~/.factory" + ], + "status": "community" + }, + { + "id": "eve", + "displayName": "Eve", + "projectSkillsDirs": [ + "agent/skills" + ], + "userSkillsDirs": [], + "detect": [ + "agent", + "package.json" + ], + "status": "community", + "notes": [ + "Project-only host; userSkillsDirs is intentionally empty.", + "Detect from Eve project shape; no global skill directory." + ] + }, + { + "id": "firebender", + "displayName": "Firebender", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.firebender/skills" + ], + "detect": [ + "~/.firebender" + ], + "status": "community" + }, + { + "id": "forgecode", + "displayName": "ForgeCode", + "projectSkillsDirs": [ + ".forge/skills" + ], + "userSkillsDirs": [ + "~/.forge/skills" + ], + "detect": [ + "~/.forge" + ], + "status": "community" + }, + { + "id": "gemini-cli", + "displayName": "Gemini CLI", + "projectSkillsDirs": [ + ".agents/skills", + ".gemini/skills" + ], + "userSkillsDirs": [ + "~/.gemini/skills", + "~/.agents/skills" + ], + "detect": [ + "~/.gemini", + "~/.agents" + ], + "status": "documented" + }, + { + "id": "github-copilot", + "displayName": "GitHub Copilot", + "projectSkillsDirs": [ + ".agents/skills", + ".github/skills", + ".claude/skills" + ], + "userSkillsDirs": [ + "~/.copilot/skills", + "~/.agents/skills", + "~/.claude/skills" + ], + "detect": [ + "~/.copilot", + "~/.agents", + "~/.claude" + ], + "status": "documented" + }, + { + "id": "goose", + "displayName": "Goose", + "projectSkillsDirs": [ + ".goose/skills" + ], + "userSkillsDirs": [ + "~/.config/goose/skills" + ], + "detect": [ + "~/.config/goose" + ], + "status": "community" + }, + { + "id": "hermes-agent", + "displayName": "Hermes Agent", + "projectSkillsDirs": [ + ".hermes/skills" + ], + "userSkillsDirs": [ + "~/.hermes/skills" + ], + "detect": [ + "~/.hermes" + ], + "status": "community" + }, + { + "id": "iflow-cli", + "displayName": "iFlow CLI", + "projectSkillsDirs": [ + ".iflow/skills" + ], + "userSkillsDirs": [ + "~/.iflow/skills" + ], + "detect": [ + "~/.iflow" + ], + "status": "community" + }, + { + "id": "inference-sh", + "displayName": "inference.sh", + "projectSkillsDirs": [ + ".inferencesh/skills" + ], + "userSkillsDirs": [ + "~/.inferencesh/skills" + ], + "detect": [ + "~/.inferencesh" + ], + "status": "community" + }, + { + "id": "jazz", + "displayName": "Jazz", + "projectSkillsDirs": [ + ".jazz/skills" + ], + "userSkillsDirs": [ + "~/.jazz/skills" + ], + "detect": [ + "~/.jazz", + ".jazz" + ], + "status": "community" + }, + { + "id": "junie", + "displayName": "Junie", + "projectSkillsDirs": [ + ".junie/skills" + ], + "userSkillsDirs": [ + "~/.junie/skills" + ], + "detect": [ + "~/.junie" + ], + "status": "community" + }, + { + "id": "kilo", + "displayName": "Kilo Code", + "projectSkillsDirs": [ + ".kilocode/skills" + ], + "userSkillsDirs": [ + "~/.kilocode/skills" + ], + "detect": [ + "~/.kilocode" + ], + "status": "community" + }, + { + "id": "kimi-cli", + "displayName": "Kimi Code CLI", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.config/agents/skills", + "~/.agents/skills" + ], + "detect": [ + "~/.config/agents", + "~/.kimi-code", + "~/.kimi", + "~/.agents" + ], + "status": "community", + "notes": [ + "kimi-code-cli is an alias for the same Kimi Code CLI path family." + ], + "aliases": [ + "kimi-code-cli" + ] + }, + { + "id": "kiro-cli", + "displayName": "Kiro CLI", + "projectSkillsDirs": [ + ".kiro/skills" + ], + "userSkillsDirs": [ + "~/.kiro/skills" + ], + "detect": [ + "~/.kiro" + ], + "status": "community" + }, + { + "id": "kode", + "displayName": "Kode", + "projectSkillsDirs": [ + ".kode/skills" + ], + "userSkillsDirs": [ + "~/.kode/skills" + ], + "detect": [ + "~/.kode" + ], + "status": "community" + }, + { + "id": "lingma", + "displayName": "Lingma", + "projectSkillsDirs": [ + ".lingma/skills" + ], + "userSkillsDirs": [ + "~/.lingma/skills" + ], + "detect": [ + "~/.lingma" + ], + "status": "community" + }, + { + "id": "loaf", + "displayName": "Loaf", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills" + ], + "detect": [ + "~/.loaf", + "~/.agents" + ], + "status": "community" + }, + { + "id": "mcpjam", + "displayName": "MCPJam", + "projectSkillsDirs": [ + ".mcpjam/skills" + ], + "userSkillsDirs": [ + "~/.mcpjam/skills" + ], + "detect": [ + "~/.mcpjam" + ], + "status": "community" + }, + { + "id": "mistral-vibe", + "displayName": "Mistral Vibe", + "projectSkillsDirs": [ + ".vibe/skills" + ], + "userSkillsDirs": [ + "~/.vibe/skills" + ], + "detect": [ + "~/.vibe" + ], + "status": "community" + }, + { + "id": "moxby", + "displayName": "Moxby", + "projectSkillsDirs": [ + ".moxby/skills" + ], + "userSkillsDirs": [ + "~/.moxby/skills" + ], + "detect": [ + "~/.moxby" + ], + "status": "community" + }, + { + "id": "mux", + "displayName": "Mux", + "projectSkillsDirs": [ + ".mux/skills" + ], + "userSkillsDirs": [ + "~/.mux/skills" + ], + "detect": [ + "~/.mux" + ], + "status": "community" + }, + { + "id": "neovate", + "displayName": "Neovate", + "projectSkillsDirs": [ + ".neovate/skills" + ], + "userSkillsDirs": [ + "~/.neovate/skills" + ], + "detect": [ + "~/.neovate" + ], + "status": "community" + }, + { + "id": "ona", + "displayName": "Ona", + "projectSkillsDirs": [ + ".ona/skills" + ], + "userSkillsDirs": [ + "~/.ona/skills" + ], + "detect": [ + "~/.ona" + ], + "status": "community" + }, + { + "id": "openclaw", + "displayName": "OpenClaw", + "projectSkillsDirs": [ + "skills" + ], + "userSkillsDirs": [ + "~/.openclaw/skills" + ], + "detect": [ + "~/.openclaw", + "~/.clawdbot", + "~/.moltbot" + ], + "status": "community" + }, + { + "id": "opencode", + "displayName": "OpenCode", + "projectSkillsDirs": [ + ".agents/skills", + ".opencode/skills", + ".claude/skills" + ], + "userSkillsDirs": [ + "~/.config/opencode/skills", + "~/.agents/skills", + "~/.claude/skills" + ], + "detect": [ + "~/.config/opencode", + "~/.agents", + "~/.claude" + ], + "status": "verified" + }, + { + "id": "openhands", + "displayName": "OpenHands", + "projectSkillsDirs": [ + ".openhands/skills" + ], + "userSkillsDirs": [ + "~/.openhands/skills" + ], + "detect": [ + "~/.openhands" + ], + "status": "community" + }, + { + "id": "pi", + "displayName": "Pi", + "projectSkillsDirs": [ + ".pi/skills" + ], + "userSkillsDirs": [ + "~/.pi/agent/skills" + ], + "detect": [ + "~/.pi/agent" + ], + "status": "community" + }, + { + "id": "pochi", + "displayName": "Pochi", + "projectSkillsDirs": [ + ".pochi/skills" + ], + "userSkillsDirs": [ + "~/.pochi/skills" + ], + "detect": [ + "~/.pochi" + ], + "status": "community" + }, + { + "id": "promptscript", + "displayName": "PromptScript", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [], + "detect": [ + ".promptscript", + "promptscript.yaml" + ], + "status": "community", + "notes": [ + "Project-only host; userSkillsDirs is intentionally empty." + ] + }, + { + "id": "qoder", + "displayName": "Qoder", + "projectSkillsDirs": [ + ".qoder/skills" + ], + "userSkillsDirs": [ + "~/.qoder/skills" + ], + "detect": [ + "~/.qoder" + ], + "status": "community" + }, + { + "id": "qoder-cn", + "displayName": "Qoder CN", + "projectSkillsDirs": [ + ".qoder/skills" + ], + "userSkillsDirs": [ + "~/.qoder-cn/skills" + ], + "detect": [ + "~/.qoder-cn" + ], + "status": "community" + }, + { + "id": "qwen-code", + "displayName": "Qwen Code", + "projectSkillsDirs": [ + ".qwen/skills" + ], + "userSkillsDirs": [ + "~/.qwen/skills" + ], + "detect": [ + "~/.qwen" + ], + "status": "community" + }, + { + "id": "reasonix", + "displayName": "Reasonix", + "projectSkillsDirs": [ + ".reasonix/skills" + ], + "userSkillsDirs": [ + "~/.reasonix/skills" + ], + "detect": [ + "~/.reasonix" + ], + "status": "community" + }, + { + "id": "replit", + "displayName": "Replit", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.config/agents/skills" + ], + "detect": [ + ".replit", + "~/.config/agents" + ], + "status": "community" + }, + { + "id": "roo", + "displayName": "Roo Code", + "aliases": [ + "roo-code" + ], + "projectSkillsDirs": [ + ".roo/skills", + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.roo/skills", + "~/.agents/skills" + ], + "detect": [ + "~/.roo", + "~/.agents" + ], + "status": "documented" + }, + { + "id": "rovodev", + "displayName": "Rovo Dev", + "projectSkillsDirs": [ + ".rovodev/skills" + ], + "userSkillsDirs": [ + "~/.rovodev/skills" + ], + "detect": [ + "~/.rovodev" + ], + "status": "community" + }, + { + "id": "tabnine-cli", + "displayName": "Tabnine CLI", + "projectSkillsDirs": [ + ".tabnine/agent/skills" + ], + "userSkillsDirs": [ + "~/.tabnine/agent/skills" + ], + "detect": [ + "~/.tabnine", + "~/.tabnine/agent" + ], + "status": "community" + }, + { + "id": "terramind", + "displayName": "Terramind", + "projectSkillsDirs": [ + ".terramind/skills" + ], + "userSkillsDirs": [ + "~/.terramind/skills" + ], + "detect": [ + "~/.terramind" + ], + "status": "community" + }, + { + "id": "tinycloud", + "displayName": "Tinycloud", + "projectSkillsDirs": [ + ".tinycloud/skills" + ], + "userSkillsDirs": [ + "~/.tinycloud/skills" + ], + "detect": [ + "~/.tinycloud" + ], + "status": "community" + }, + { + "id": "trae", + "displayName": "Trae", + "projectSkillsDirs": [ + ".trae/skills" + ], + "userSkillsDirs": [ + "~/.trae/skills" + ], + "detect": [ + "~/.trae" + ], + "status": "community" + }, + { + "id": "trae-cn", + "displayName": "Trae CN", + "projectSkillsDirs": [ + ".trae/skills" + ], + "userSkillsDirs": [ + "~/.trae-cn/skills" + ], + "detect": [ + "~/.trae-cn" + ], + "status": "community" + }, + { + "id": "universal", + "displayName": "Universal", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills", + "~/.config/agents/skills" + ], + "detect": [ + "~/.agents", + "~/.config/agents" + ], + "status": "community" + }, + { + "id": "warp", + "displayName": "Warp", + "projectSkillsDirs": [ + ".agents/skills", + ".warp/skills", + ".claude/skills", + ".codex/skills", + ".cursor/skills", + ".gemini/skills", + ".copilot/skills", + ".factory/skills", + ".github/skills", + ".opencode/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills", + "~/.warp/skills", + "~/.claude/skills", + "~/.codex/skills", + "~/.cursor/skills", + "~/.gemini/skills", + "~/.copilot/skills", + "~/.factory/skills", + "~/.github/skills", + "~/.opencode/skills" + ], + "detect": [ + "~/.warp", + "~/.agents", + "~/.claude", + "~/.codex", + "~/.cursor", + "~/.gemini", + "~/.copilot", + "~/.factory", + "~/.github", + "~/.opencode" + ], + "status": "documented" + }, + { + "id": "windsurf", + "displayName": "Windsurf", + "projectSkillsDirs": [ + ".windsurf/skills" + ], + "userSkillsDirs": [ + "~/.codeium/windsurf/skills" + ], + "detect": [ + "~/.codeium/windsurf" + ], + "status": "community" + }, + { + "id": "zed", + "displayName": "Zed", + "projectSkillsDirs": [ + ".agents/skills" + ], + "userSkillsDirs": [ + "~/.agents/skills" + ], + "detect": [ + "~/.config/zed", + "~/.agents" + ], + "status": "community" + }, + { + "id": "zencoder", + "displayName": "Zencoder", + "projectSkillsDirs": [ + ".zencoder/skills" + ], + "userSkillsDirs": [ + "~/.zencoder/skills" + ], + "detect": [ + "~/.zencoder" + ], + "status": "community" + }, + { + "id": "zenflow", + "displayName": "Zenflow", + "projectSkillsDirs": [ + ".zencoder/skills" + ], + "userSkillsDirs": [ + "~/.zencoder/skills" + ], + "detect": [ + "~/.zencoder" + ], + "status": "community" + } + ] +} diff --git a/go/testdata/cases/bundled-skill-install.json b/go/testdata/cases/bundled-skill-install.json new file mode 100644 index 0000000..fcc5407 --- /dev/null +++ b/go/testdata/cases/bundled-skill-install.json @@ -0,0 +1,3073 @@ +{ + "$schema": "../cases.schema.json", + "schemaVersion": 1, + "cases": [ + { + "id": "all-supported-hosts-load", + "operation": "resolve-hosts", + "description": "Loads the full host adapter baseline from spec/hosts.json.", + "options": { + "agents": "*" + }, + "given": { + "hostsFile": "spec/hosts.json" + }, + "expected": { + "count": 72, + "hostIds": [ + "adal", + "aider-desk", + "amp", + "antigravity", + "antigravity-cli", + "astrbot", + "augment", + "autohand-code", + "bob", + "claude-code", + "cline", + "codearts-agent", + "codebuddy", + "codemaker", + "codestudio", + "codex", + "command-code", + "continue", + "cortex", + "crush", + "cursor", + "deepagents", + "devin", + "dexto", + "droid", + "eve", + "firebender", + "forgecode", + "gemini-cli", + "github-copilot", + "goose", + "hermes-agent", + "iflow-cli", + "inference-sh", + "jazz", + "junie", + "kilo", + "kimi-cli", + "kiro-cli", + "kode", + "lingma", + "loaf", + "mcpjam", + "mistral-vibe", + "moxby", + "mux", + "neovate", + "ona", + "openclaw", + "opencode", + "openhands", + "pi", + "pochi", + "promptscript", + "qoder", + "qoder-cn", + "qwen-code", + "reasonix", + "replit", + "roo", + "rovodev", + "tabnine-cli", + "terramind", + "tinycloud", + "trae", + "trae-cn", + "universal", + "warp", + "windsurf", + "zed", + "zencoder", + "zenflow" + ] + } + }, + { + "id": "alias-resolution", + "operation": "resolve-hosts", + "description": "Resolves legacy or ecosystem-compatible host aliases to canonical adapter ids.", + "options": { + "agents": [ + "roo-code" + ] + }, + "given": { + "hostsFile": "spec/hosts.json" + }, + "expected": { + "resolvedHostIds": [ + "roo" + ] + } + }, + { + "id": "kimi-alias-resolution", + "operation": "resolve-hosts", + "description": "Resolves the kimi-code-cli alias to the canonical kimi-cli adapter id.", + "options": { + "agents": [ + "kimi-code-cli" + ] + }, + "given": { + "hostsFile": "spec/hosts.json" + }, + "expected": { + "resolvedHostIds": [ + "kimi-cli" + ] + } + }, + { + "id": "unknown-host-id", + "operation": "resolve-hosts", + "description": "Reports a structured error when an explicit host id or alias is unknown.", + "options": { + "agents": [ + "missing-agent" + ] + }, + "given": { + "hostsFile": "spec/hosts.json" + }, + "expected": { + "resolvedHostIds": [], + "errors": [ + { + "agent": "missing-agent", + "reason": "unknown-host" + } + ] + } + }, + { + "id": "parse-install-flags-defaults", + "operation": "parse-install-flags", + "description": "Maps missing install flags to the user default while recording that scope was not explicit.", + "options": {}, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "parsed": { + "scope": "user", + "scopeSet": false, + "agentKind": "auto", + "agentIds": [], + "yes": false, + "dryRun": false, + "force": false, + "errors": [] + } + } + }, + { + "id": "parse-install-flags-explicit", + "operation": "parse-install-flags", + "description": "Maps repeated and comma-separated agent flags to a deduplicated explicit selector.", + "options": { + "scope": "project", + "agents": [ + "codex,claude-code", + "codex" + ], + "yes": true, + "dryRun": true, + "force": true + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "parsed": { + "scope": "project", + "scopeSet": true, + "agentKind": "explicit", + "agentIds": [ + "codex", + "claude-code" + ], + "yes": true, + "dryRun": true, + "force": true, + "errors": [] + } + } + }, + { + "id": "parse-install-flags-star", + "operation": "parse-install-flags", + "description": "Maps --agent '*' to the all-host selector.", + "options": { + "agents": [ + "*" + ] + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "parsed": { + "scope": "user", + "scopeSet": false, + "agentKind": "*", + "agentIds": [], + "yes": false, + "dryRun": false, + "force": false, + "errors": [] + } + } + }, + { + "id": "parse-install-flags-errors", + "operation": "parse-install-flags", + "description": "Reports invalid scope and mixed star agent selector flags.", + "options": { + "scope": "global", + "agents": [ + "*", + "codex" + ] + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "parsed": { + "scope": "user", + "scopeSet": true, + "agentKind": "*", + "agentIds": [], + "yes": false, + "dryRun": false, + "force": false, + "errors": [ + { + "flag": "scope", + "reason": "invalid-scope", + "value": "global" + }, + { + "flag": "agent", + "reason": "agent-star-must-be-alone", + "value": "*,codex" + } + ] + } + } + }, + { + "id": "shared-target-deduplication-many-hosts", + "operation": "install", + "description": "Copies once when many supported agents share the .agents/skills project target.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "amp", + "antigravity", + "antigravity-cli", + "cline", + "codex", + "cursor", + "deepagents", + "dexto", + "firebender", + "gemini-cli", + "github-copilot", + "kimi-cli", + "loaf", + "opencode", + "promptscript", + "replit", + "universal", + "warp", + "zed" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostIds": [ + "amp", + "antigravity", + "antigravity-cli", + "cline", + "codex", + "cursor", + "deepagents", + "dexto", + "firebender", + "gemini-cli", + "github-copilot", + "kimi-cli", + "loaf", + "opencode", + "promptscript", + "replit", + "universal", + "warp", + "zed" + ], + "skillName": "basic", + "targetDir": "$WORKSPACE/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "writeCountByTargetDir": { + "$WORKSPACE/.agents/skills/basic": 1 + } + } + }, + { + "id": "user-scope-install", + "operation": "install", + "description": "Installs a bundled skill into the canonical user scope for an explicit host.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.agents/skills/basic/references/guide.md", + "$HOME/.agents/skills/basic/scripts/helper.sh", + "$HOME/.agents/skills/basic/assets/template.json", + "$HOME/.agents/skills/basic/.kitup.json" + ], + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + }, + "hash": "from-skill-bundle-dir" + } + } + }, + { + "id": "codex-user-scope-prefers-first-user-dir", + "operation": "install", + "description": "Installs Codex user-scope skills into the first canonical user path when multiple user paths are valid.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills", + "$HOME/.codex/skills" + ], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.agents/skills/basic/.kitup.json" + ], + "filesAbsent": [ + "$HOME/.codex/skills/basic" + ] + } + }, + { + "id": "project-scope-install", + "operation": "install", + "description": "Installs a bundled skill into the canonical project scope for an explicit host.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$WORKSPACE/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$WORKSPACE/.agents/skills/basic/SKILL.md", + "$WORKSPACE/.agents/skills/basic/.kitup.json" + ], + "fileModes": { + "$WORKSPACE/.agents/skills/basic": "755" + } + } + }, + { + "id": "project-scope-plan", + "operation": "plan", + "description": "Reports the project-scope install target without writing skill files.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$WORKSPACE/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesAbsent": [ + "$WORKSPACE/.agents/skills/basic" + ] + } + }, + { + "id": "project-only-host-project-scope-install", + "operation": "install", + "description": "Installs into a project-only host when project scope is selected explicitly.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "eve" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "eve", + "skillName": "basic", + "targetDir": "$WORKSPACE/agent/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$WORKSPACE/agent/skills/basic/SKILL.md", + "$WORKSPACE/agent/skills/basic/.kitup.json" + ] + } + }, + { + "id": "project-only-host-user-scope-error", + "operation": "install", + "description": "Reports an unsupported-scope error when a project-only host is selected for user scope.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "eve" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [ + { + "hostId": "eve", + "skillName": "basic", + "scope": "user", + "reason": "unsupported-scope" + } + ] + } + } + }, + { + "id": "explicit-host-selection", + "operation": "install", + "description": "Installs only into the explicitly selected host targets.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex", + "claude-code" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills", + "$HOME/.claude/skills" + ], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + }, + { + "hostId": "claude-code", + "skillName": "basic", + "targetDir": "$HOME/.claude/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + } + } + }, + { + "id": "auto-host-detection", + "operation": "install", + "description": "Uses detection paths to select installed hosts for automatic installation.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": "auto", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex", + "$HOME/.claude", + "$HOME/.agents/skills", + "$HOME/.claude/skills" + ], + "files": {} + }, + "expected": { + "detectedHosts": [ + "codex", + "claude-code" + ], + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + }, + { + "hostId": "claude-code", + "skillName": "basic", + "targetDir": "$HOME/.claude/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + } + } + }, + { + "id": "auto-host-detection-empty", + "operation": "install", + "description": "Returns an empty report when automatic host detection finds no supported hosts.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": "auto", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "detectedHosts": [], + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + } + } + }, + { + "id": "shared-target-deduplication", + "operation": "install", + "description": "Copies once when multiple selected hosts resolve to the same canonical target directory.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex", + "warp", + "gemini-cli" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostIds": [ + "codex", + "warp", + "gemini-cli" + ], + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "writeCountByTargetDir": { + "$HOME/.agents/skills/basic": 1 + } + } + }, + { + "id": "unchanged-noop", + "operation": "install", + "description": "Skips a kitup-owned target when its metadata hash matches the bundled skill.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "copySkillBundleTo": "$HOME/.agents/skills/basic", + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + }, + "hash": "from-skill-bundle-dir" + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unchanged" + } + ], + "conflicts": [], + "errors": [] + } + } + }, + { + "id": "workflow-unchanged-silent", + "operation": "run-install-workflow", + "description": "Does not render a user-facing summary or confirmation when every selected target is unchanged.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "stdinTTY": true, + "yes": false, + "input": "y\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "copySkillBundleTo": "$HOME/.agents/skills/basic", + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + }, + "hash": "from-skill-bundle-dir" + } + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": true, + "code": "ok", + "message": "" + }, + "output": "", + "report": { + "installed": [], + "updated": [], + "skipped": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unchanged" + } + ], + "conflicts": [], + "errors": [] + } + } + }, + { + "id": "unchanged-repairs-script-mode", + "operation": "install", + "description": "Repairs script executable mode drift even when the stored content hash is unchanged.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "copySkillBundleTo": "$HOME/.agents/skills/basic", + "fileModes": { + "$HOME/.agents/skills/basic/scripts/helper.sh": "644" + }, + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + }, + "hash": "from-skill-bundle-dir" + } + }, + "expected": { + "report": { + "installed": [], + "updated": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "fileModes": { + "$HOME/.agents/skills/basic/scripts/helper.sh": "755" + } + } + }, + { + "id": "workflow-conflict-exit", + "operation": "run-install-workflow", + "description": "Returns a conflict exit classification when the plan has conflicts but no writes.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "stdinTTY": true, + "yes": false, + "input": "y\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/SKILL.md": "---\nname: basic\ndescription: Unmanaged fixture.\n---\n\n# Basic\n" + } + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": false, + "code": "conflict", + "message": "Installation has conflicts." + }, + "output": "", + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + } + } + }, + { + "id": "workflow-conflict-blocks-writes", + "operation": "run-install-workflow", + "description": "Does not write any target when the plan mixes writable targets with conflicts.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex", + "claude-code" + ], + "stdinTTY": true, + "yes": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/SKILL.md": "---\nname: basic\ndescription: Unmanaged fixture.\n---\n\n# Basic\n" + } + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": false, + "code": "conflict", + "message": "Installation has conflicts." + }, + "output": "", + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + }, + "filesAbsent": [ + "$HOME/.claude/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-dry-run-conflict-renders-plan", + "operation": "run-install-workflow", + "description": "Dry-run renders writable plan items even when another target conflicts.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex", + "claude-code" + ], + "stdinTTY": true, + "yes": true, + "dryRun": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/SKILL.md": "---\nname: basic\ndescription: Unmanaged fixture.\n---\n\n# Basic\n" + } + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": true + }, + "exit": { + "ok": false, + "code": "conflict", + "message": "Installation has conflicts." + }, + "outputContains": [ + " - basic -> ", + "(claude-code)" + ], + "report": { + "installed": [ + { + "hostId": "claude-code", + "skillName": "basic", + "targetDir": "$HOME/.claude/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + }, + "filesAbsent": [ + "$HOME/.claude/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-force-overwrites-conflicts", + "operation": "run-install-workflow", + "description": "Force turns unmanaged and different-owner targets into explicit updates.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex", + "claude-code" + ], + "stdinTTY": true, + "yes": true, + "force": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic", + "$HOME/.claude/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/SKILL.md": "---\nname: basic\ndescription: Unmanaged fixture.\n---\n\n# Basic\n", + "$HOME/.claude/skills/basic/SKILL.md": "---\nname: basic\ndescription: Other owner fixture.\n---\n\n# Basic\n", + "$HOME/.claude/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "other-cli", + "skillName": "basic", + "source": "bundled", + "hash": "sha256:old" + } + } + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": true, + "code": "ok", + "message": "" + }, + "outputContains": [ + "(codex)", + "(claude-code)" + ], + "report": { + "installed": [], + "updated": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + }, + { + "hostId": "claude-code", + "skillName": "basic", + "targetDir": "$HOME/.claude/skills/basic" + } + ], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.claude/skills/basic/SKILL.md" + ], + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "hash": "from-skill-bundle-dir", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + } + } + } + }, + { + "id": "changed-update", + "operation": "update", + "description": "Replaces a kitup-owned target when its metadata hash differs from the bundled skill.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/SKILL.md": "---\nname: basic\ndescription: Old fixture.\n---\n\n# Old\n", + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled", + "hash": "sha256:old" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/references/guide.md", + "$HOME/.agents/skills/basic/assets/template.json" + ] + } + }, + { + "id": "unmanaged-conflict", + "operation": "install", + "description": "Refuses to overwrite a target directory that has no kitup metadata.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/SKILL.md": "---\nname: basic\ndescription: Existing unmanaged skill.\n---\n" + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + } + } + }, + { + "id": "install-incomplete-metadata-conflict", + "operation": "install", + "description": "Treats incomplete .kitup.json as unmanaged and refuses overwrite.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "appId": "example-cli" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/.kitup.json" + ] + } + }, + { + "id": "install-skill-name-mismatch-conflict", + "operation": "install", + "description": "Treats metadata for a different skill name as unmanaged and refuses overwrite.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "other", + "source": "bundled", + "hash": "sha256:old" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/.kitup.json" + ] + } + }, + { + "id": "update-skill-name-mismatch-with-force", + "operation": "update", + "description": "Force replaces metadata for a different skill name and records the requested owner.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "force": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "other", + "source": "bundled", + "hash": "sha256:old" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md" + ], + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "hash": "from-skill-bundle-dir", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + } + } + } + }, + { + "id": "different-owner-conflict", + "operation": "install", + "description": "Refuses to overwrite a kitup-managed target owned by another app.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "other-cli", + "skillName": "basic", + "source": "bundled", + "hash": "sha256:old" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "owner-mismatch" + } + ], + "errors": [] + } + } + }, + { + "id": "uninstall-owned-skill", + "operation": "uninstall", + "description": "Removes only a kitup-owned skill target for the same app id.", + "options": { + "appId": "example-cli", + "skillName": "basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "copySkillBundleTo": "$HOME/.agents/skills/basic", + "metadata": { + "path": "$HOME/.agents/skills/basic/.kitup.json", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled" + }, + "hash": "from-skill-bundle-dir" + } + }, + "expected": { + "report": { + "removed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesAbsent": [ + "$HOME/.agents/skills/basic" + ] + } + }, + { + "id": "uninstall-owner-mismatch", + "operation": "uninstall", + "description": "Refuses to remove a skill target owned by another app id.", + "options": { + "appId": "example-cli", + "skillName": "basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "other-cli", + "skillName": "basic", + "source": "bundled", + "hash": "sha256:old" + } + } + }, + "expected": { + "report": { + "removed": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "owner-mismatch" + } + ], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/.kitup.json" + ] + } + }, + { + "id": "missing-skill-md", + "operation": "validate", + "description": "Rejects a skill directory without SKILL.md.", + "options": { + "skillBundleDir": "testdata/skills/missing-skill-md", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "valid": false, + "errorCode": "missing-skill-md" + } + }, + { + "id": "invalid-frontmatter", + "operation": "validate", + "description": "Rejects SKILL.md frontmatter that violates the Agent Skills naming and description rules.", + "options": { + "skillBundleDir": "testdata/skills/invalid-frontmatter", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "valid": false, + "errorCode": "invalid-frontmatter" + } + }, + { + "id": "nested-resources-copied", + "operation": "install", + "description": "Copies nested references, scripts, and assets with the bundled skill.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "files": {} + }, + "expected": { + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.agents/skills/basic/references/guide.md", + "$HOME/.agents/skills/basic/scripts/helper.sh", + "$HOME/.agents/skills/basic/assets/template.json" + ] + } + }, + { + "id": "embedded-skill-source", + "operation": "install", + "description": "Installs a bundled skill from an embedded directory tree source without materializing a directory source first.", + "options": { + "appId": "example-cli", + "skillFiles": [ + { + "path": "SKILL.md", + "contents": "---\nname: embedded\ndescription: Embedded whole skill tree fixture.\n---\n\n# Embedded\n" + }, + { + "path": "references/guide.md", + "contents": "# Guide\n" + }, + { + "path": "assets/template.json", + "contents": "{\"ok\":true}\n" + }, + { + "path": "scripts/helper.sh", + "contents": "#!/usr/bin/env sh\necho embedded\n" + }, + { + "path": "scripts/disabled.sh", + "contents": "#!/usr/bin/env sh\necho disabled\n", + "mode": 420 + }, + { + "path": ".kitup.json", + "contents": "{\"ignored\":true}\n" + }, + { + "path": ".DS_Store", + "contents": "ignored" + } + ], + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "files": {} + }, + "expected": { + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "embedded", + "targetDir": "$HOME/.agents/skills/embedded" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/embedded/SKILL.md", + "$HOME/.agents/skills/embedded/references/guide.md", + "$HOME/.agents/skills/embedded/assets/template.json", + "$HOME/.agents/skills/embedded/scripts/helper.sh", + "$HOME/.agents/skills/embedded/.kitup.json" + ], + "filesAbsent": [ + "$HOME/.agents/skills/embedded/.DS_Store" + ], + "fileModes": { + "$HOME/.agents/skills/embedded/scripts/helper.sh": "755", + "$HOME/.agents/skills/embedded/scripts/disabled.sh": "644" + }, + "metadata": { + "path": "$HOME/.agents/skills/embedded/.kitup.json", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "embedded", + "source": "bundled" + }, + "hash": "from-skill-files" + } + } + }, + { + "id": "workflow-explicit-agent", + "operation": "resolve-install-selection", + "description": "Explicit agents select canonical hosts without detection.", + "options": { + "scope": "user", + "agents": [ + "codex" + ], + "stdinTTY": false, + "yes": false, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "selection": { + "action": "install", + "selectedHostIds": [ + "codex" + ], + "candidateHostIds": [], + "detectedHostIds": [], + "needsConfirmation": false, + "errors": [] + } + } + }, + { + "id": "workflow-agent-star", + "operation": "resolve-install-selection", + "description": "Agent star explicitly selects every supported host.", + "options": { + "scope": "user", + "agents": "*", + "stdinTTY": false, + "yes": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "selection": { + "action": "install", + "selectedCount": 72, + "candidateHostIds": [], + "detectedHostIds": [], + "needsConfirmation": false, + "errors": [] + } + } + }, + { + "id": "workflow-scope-prompt-before-agent", + "operation": "run-install-workflow", + "description": "TTY workflow prompts for scope before planning an explicit agent install when scope was not provided.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "agents": [ + "codex" + ], + "stdinTTY": true, + "promptScope": true, + "input": "project\ny\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "workflow": { + "scope": "project", + "canceled": false, + "dryRun": false + }, + "outputContains": [ + "Select install scope:", + "Scope (user/project) [user]: ", + " - basic -> ", + "(codex)" + ], + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$WORKSPACE/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$WORKSPACE/.agents/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-scope-non-tty-error", + "operation": "run-install-workflow", + "description": "Non-TTY workflow refuses to choose scope implicitly when scope was not provided.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "agents": [ + "codex" + ], + "stdinTTY": false, + "promptScope": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "workflow": { + "scope": "", + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": false, + "code": "selection-error", + "message": "Agent selection failed." + }, + "output": "kitup: scope-selection-required\n", + "filesAbsent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$WORKSPACE/.agents/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-scope-yes-default", + "operation": "run-install-workflow", + "description": "Yes mode uses the configured default scope when scope was not provided.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "agents": [ + "codex" + ], + "stdinTTY": false, + "promptScope": true, + "defaultScope": "project", + "yes": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "workflow": { + "scope": "project", + "canceled": false, + "dryRun": false + }, + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$WORKSPACE/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$WORKSPACE/.agents/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-shared-target-renders-host-rows", + "operation": "run-install-workflow", + "description": "Shared target install still renders one user-facing row per selected host.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "codex", + "cursor", + "github-copilot", + "opencode" + ], + "stdinTTY": true, + "input": "y\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "workflow": { + "scope": "project", + "canceled": false, + "dryRun": false + }, + "outputContains": [ + "(codex)", + "(cursor)", + "(github-copilot)", + "(opencode)", + "Proceed? [y/N]" + ], + "report": { + "installed": [ + { + "hostIds": [ + "codex", + "cursor", + "github-copilot", + "opencode" + ], + "skillName": "basic", + "targetDir": "$WORKSPACE/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$WORKSPACE/.agents/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-zero-detected-tty-prompts", + "operation": "resolve-install-selection", + "description": "TTY with no detected hosts asks the user to choose from supported hosts.", + "options": { + "scope": "user", + "stdinTTY": true, + "yes": false, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "selection": { + "action": "select-agents", + "selectedHostIds": [], + "candidateCount": 72, + "detectedHostIds": [], + "needsConfirmation": true, + "errors": [] + } + } + }, + { + "id": "workflow-one-detected-auto-selects", + "operation": "resolve-install-selection", + "description": "TTY with one detected host can select it before summary confirmation.", + "options": { + "scope": "user", + "stdinTTY": true, + "yes": false, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {} + }, + "expected": { + "selection": { + "action": "install", + "selectedHostIds": [ + "codex" + ], + "candidateHostIds": [], + "detectedHostIds": [ + "codex" + ], + "needsConfirmation": true, + "errors": [] + } + } + }, + { + "id": "workflow-many-detected-tty-prompts", + "operation": "resolve-install-selection", + "description": "TTY with multiple detected hosts enters agent multi-selection instead of auto-installing all.", + "options": { + "scope": "user", + "stdinTTY": true, + "yes": false, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex", + "$HOME/.claude" + ], + "files": {} + }, + "expected": { + "selection": { + "action": "select-agents", + "selectedHostIds": [], + "candidateHostIds": [ + "codex", + "claude-code" + ], + "detectedHostIds": [ + "codex", + "claude-code" + ], + "needsConfirmation": true, + "errors": [] + } + } + }, + { + "id": "workflow-many-detected-enter-cancels", + "operation": "run-install-workflow", + "description": "TTY multi-select with an empty selection cancels instead of installing every detected host.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "stdinTTY": true, + "yes": false, + "input": "\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex", + "$HOME/.claude" + ], + "files": {} + }, + "expected": { + "workflow": { + "canceled": true, + "dryRun": false + }, + "exit": { + "ok": false, + "code": "canceled", + "message": "Installation canceled." + }, + "outputContains": [ + "Select agents:" + ], + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesAbsent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.claude/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-many-detected-select-one-confirms", + "operation": "run-install-workflow", + "description": "TTY multi-select installs only the selected host after summary confirmation.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "stdinTTY": true, + "yes": false, + "input": "1\ny\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex", + "$HOME/.claude" + ], + "files": {} + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": true, + "code": "ok", + "message": "" + }, + "outputContains": [ + "Select agents:", + "(codex)", + "Proceed? [y/N]" + ], + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.agents/skills/basic/assets/template.json" + ], + "filesAbsent": [ + "$HOME/.claude/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-one-detected-confirms-installs", + "operation": "run-install-workflow", + "description": "TTY with one detected host still renders a summary and waits for confirmation before writing.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "stdinTTY": true, + "yes": false, + "input": "y\n", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {} + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": true, + "code": "ok", + "message": "" + }, + "outputContains": [ + "(codex)", + "Proceed? [y/N]" + ], + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-yes-batch-installs-detected", + "operation": "run-install-workflow", + "description": "Yes mode skips prompts and installs every detected host explicitly selected by policy.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "stdinTTY": false, + "yes": true, + "input": "", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex", + "$HOME/.claude" + ], + "files": {} + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": false + }, + "exit": { + "ok": true, + "code": "ok", + "message": "" + }, + "outputContains": [ + "(codex)" + ], + "report": { + "installed": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic" + }, + { + "hostId": "claude-code", + "skillName": "basic", + "targetDir": "$HOME/.claude/skills/basic" + } + ], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/SKILL.md", + "$HOME/.claude/skills/basic/SKILL.md" + ] + } + }, + { + "id": "workflow-many-detected-yes-installs", + "operation": "resolve-install-selection", + "description": "Yes mode can accept detected hosts non-interactively.", + "options": { + "scope": "user", + "stdinTTY": false, + "yes": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex", + "$HOME/.claude" + ], + "files": {} + }, + "expected": { + "selection": { + "action": "install", + "selectedHostIds": [ + "codex", + "claude-code" + ], + "candidateHostIds": [], + "detectedHostIds": [ + "codex", + "claude-code" + ], + "needsConfirmation": false, + "errors": [] + } + } + }, + { + "id": "workflow-non-tty-no-agent-error", + "operation": "resolve-install-selection", + "description": "Non-TTY without explicit agent or yes cannot enter interactive selection.", + "options": { + "scope": "user", + "stdinTTY": false, + "yes": false, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {} + }, + "expected": { + "selection": { + "action": "error", + "selectedHostIds": [], + "candidateHostIds": [], + "detectedHostIds": [ + "codex" + ], + "needsConfirmation": false, + "errors": [ + { + "reason": "agent-selection-required" + } + ] + } + } + }, + { + "id": "workflow-zero-detected-yes-error", + "operation": "resolve-install-selection", + "description": "Yes mode does not turn zero detection into all-host installation.", + "options": { + "scope": "user", + "stdinTTY": false, + "yes": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": {} + }, + "expected": { + "selection": { + "action": "error", + "selectedHostIds": [], + "candidateHostIds": [], + "detectedHostIds": [], + "needsConfirmation": false, + "errors": [ + { + "reason": "no-detected-hosts" + } + ] + } + } + }, + { + "id": "github-bundle-install", + "operation": "install", + "description": "Installs a public GitHub bundle with provenance metadata while keeping the user-facing install flow unchanged.", + "options": { + "appId": "example-cli", + "githubBundle": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-basic", + "ref": "main" + }, + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {}, + "github": { + "owner": "acme", + "repo": "mycli-skills", + "ref": "main", + "commit": "abc123", + "treeSha": "tree123", + "files": { + "skills/github-basic/SKILL.md": "---\nname: github-basic\ndescription: GitHub sourced skill.\n---\n", + "skills/github-basic/references/guide.md": "GitHub guide.\n", + "other/SKILL.md": "---\nname: ignored\ndescription: Ignored skill.\n---\n" + } + } + }, + "expected": { + "filesPresent": [ + "$HOME/.agents/skills/github-basic/SKILL.md", + "$HOME/.agents/skills/github-basic/references/guide.md" + ], + "filesAbsent": [ + "$HOME/.agents/skills/github-basic/other/SKILL.md" + ], + "metadata": { + "path": "$HOME/.agents/skills/github-basic/.kitup.json", + "hash": "from-github-bundle", + "fields": { + "appId": "example-cli", + "skillName": "github-basic", + "source": "github", + "sourceId": "github:acme/mycli-skills/skills/github-basic", + "version": "main", + "provenance": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-basic", + "ref": "main", + "resolvedCommit": "abc123" + } + } + } + } + }, + { + "id": "github-bundle-mode-only-update-refreshes-metadata", + "operation": "install", + "description": "Refreshes GitHub provenance metadata when an unchanged content hash only needs mode repair.", + "options": { + "appId": "example-cli", + "githubBundle": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-mode", + "ref": "main" + }, + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": { + "$HOME/.agents/skills/github-mode/SKILL.md": "---\nname: github-mode\ndescription: GitHub mode skill.\n---\n", + "$HOME/.agents/skills/github-mode/scripts/helper.sh": "#!/bin/sh\n" + }, + "fileModes": { + "$HOME/.agents/skills/github-mode/scripts/helper.sh": "644" + }, + "metadata": { + "path": "$HOME/.agents/skills/github-mode/.kitup.json", + "hash": "from-github-bundle", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "github-mode", + "source": "github", + "sourceId": "github:acme/mycli-skills/skills/github-mode", + "version": "old", + "provenance": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-mode", + "ref": "old", + "resolvedCommit": "old123" + } + } + }, + "github": { + "owner": "acme", + "repo": "mycli-skills", + "ref": "main", + "commit": "new123", + "treeSha": "tree-mode", + "files": { + "skills/github-mode/SKILL.md": "---\nname: github-mode\ndescription: GitHub mode skill.\n---\n", + "skills/github-mode/scripts/helper.sh": "#!/bin/sh\n" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [ + { + "hostId": "codex", + "skillName": "github-mode", + "targetDir": "$HOME/.agents/skills/github-mode" + } + ], + "skipped": [], + "conflicts": [], + "errors": [] + }, + "fileModes": { + "$HOME/.agents/skills/github-mode/scripts/helper.sh": "755" + }, + "metadata": { + "path": "$HOME/.agents/skills/github-mode/.kitup.json", + "hash": "from-github-bundle", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "github-mode", + "source": "github", + "sourceId": "github:acme/mycli-skills/skills/github-mode", + "version": "main", + "provenance": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-mode", + "ref": "main", + "resolvedCommit": "new123" + } + } + } + } + }, + { + "id": "github-bundle-dry-run", + "operation": "run-install-workflow", + "description": "Plans a GitHub bundle install without writing files in dry-run mode.", + "options": { + "appId": "example-cli", + "githubBundle": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-basic", + "ref": "main" + }, + "scope": "user", + "agents": [ + "codex" + ], + "yes": true, + "dryRun": true, + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {}, + "github": { + "owner": "acme", + "repo": "mycli-skills", + "ref": "main", + "commit": "abc123", + "treeSha": "tree123", + "files": { + "skills/github-basic/SKILL.md": "---\nname: github-basic\ndescription: GitHub sourced skill.\n---\n" + } + } + }, + "expected": { + "workflow": { + "canceled": false, + "dryRun": true + }, + "filesAbsent": [ + "$HOME/.agents/skills/github-basic/SKILL.md", + "$HOME/.agents/skills/github-basic/.kitup.json" + ] + } + }, + { + "id": "github-bundle-resolve-failure", + "operation": "install", + "description": "Reports a structured error when the configured GitHub bundle path has no files.", + "options": { + "appId": "example-cli", + "githubBundle": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/missing", + "ref": "main" + }, + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {}, + "github": { + "owner": "acme", + "repo": "mycli-skills", + "ref": "main", + "commit": "abc123", + "treeSha": "tree123", + "files": { + "skills/github-basic/SKILL.md": "---\nname: github-basic\ndescription: GitHub sourced skill.\n---\n" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [ + { + "reason": "bundle-resolve-failed" + } + ] + }, + "filesAbsent": [ + "$HOME/.agents/skills/github-basic/SKILL.md" + ] + } + }, + { + "id": "github-bundle-unchanged", + "operation": "install", + "description": "Skips a GitHub bundle install when kitup ownership and content hash are unchanged.", + "options": { + "appId": "example-cli", + "githubBundle": { + "owner": "acme", + "repo": "mycli-skills", + "path": "skills/github-basic", + "ref": "main" + }, + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.codex" + ], + "files": {}, + "metadata": { + "path": "$HOME/.agents/skills/github-basic/.kitup.json", + "hash": "from-github-bundle", + "fields": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "github-basic", + "source": "github" + } + }, + "github": { + "owner": "acme", + "repo": "mycli-skills", + "ref": "main", + "commit": "abc123", + "treeSha": "tree123", + "files": { + "skills/github-basic/SKILL.md": "---\nname: github-basic\ndescription: GitHub sourced skill.\n---\n" + } + } + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [ + { + "hostId": "codex", + "skillName": "github-basic", + "targetDir": "$HOME/.agents/skills/github-basic", + "reason": "unchanged" + } + ], + "conflicts": [], + "errors": [] + } + } + }, + { + "id": "uninstall-rejects-path-traversal-skill-name", + "operation": "uninstall", + "description": "Refuses skill names that escape the install root and never deletes a sibling owned install.", + "options": { + "appId": "example-cli", + "skillName": "../victim", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/victim" + ], + "files": { + "$HOME/.agents/victim/.kitup.json": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "victim", + "source": "bundled", + "hash": "sha256:abc" + } + } + }, + "expected": { + "report": { + "removed": [], + "skipped": [], + "conflicts": [], + "errors": [ + { + "skillName": "../victim", + "reason": "invalid-skill-name" + } + ] + }, + "filesPresent": [ + "$HOME/.agents/victim/.kitup.json" + ] + } + }, + { + "id": "uninstall-incomplete-metadata-conflict", + "operation": "uninstall", + "description": "Treats incomplete .kitup.json as unmanaged and refuses deletion.", + "options": { + "appId": "example-cli", + "skillName": "basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "appId": "example-cli" + } + } + }, + "expected": { + "report": { + "removed": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/.kitup.json" + ] + } + }, + { + "id": "uninstall-skill-name-mismatch-conflict", + "operation": "uninstall", + "description": "Refuses deletion when metadata skillName does not exactly match the requested skill.", + "options": { + "appId": "example-cli", + "skillName": "basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "other", + "source": "bundled", + "hash": "sha256:abc" + } + } + }, + "expected": { + "report": { + "removed": [], + "skipped": [], + "conflicts": [ + { + "hostId": "codex", + "skillName": "basic", + "targetDir": "$HOME/.agents/skills/basic", + "reason": "unmanaged" + } + ], + "errors": [] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/.kitup.json" + ] + } + }, + { + "id": "hosts-file-rejects-escaped-project-path", + "operation": "install", + "description": "Rejects a custom hosts file whose project path escapes the workspace root.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "evil" + ], + "hostsFile": "$WORKSPACE/hosts.json", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": { + "$WORKSPACE/hosts.json": { + "schemaVersion": 1, + "hosts": [ + { + "id": "evil", + "displayName": "Evil", + "projectSkillsDirs": [ + "../outside" + ], + "userSkillsDirs": [], + "detect": [ + ".agents" + ], + "status": "community" + } + ] + } + } + }, + "expected": { + "throws": true, + "filesAbsent": [ + "$WORKSPACE/../outside/basic", + "$HOME/../outside/basic" + ] + } + }, + { + "id": "hosts-file-rejects-windows-project-path", + "operation": "install", + "description": "Rejects a custom hosts file whose Windows-style project path escapes the workspace root.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "project", + "agents": [ + "evil" + ], + "hostsFile": "$WORKSPACE/hosts.json", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": { + "$WORKSPACE/hosts.json": { + "schemaVersion": 1, + "hosts": [ + { + "id": "evil", + "displayName": "Evil", + "projectSkillsDirs": [ + "..\\outside" + ], + "userSkillsDirs": [], + "detect": [ + ".agents" + ], + "status": "community" + } + ] + } + } + }, + "expected": { + "throws": true, + "filesAbsent": [ + "$WORKSPACE/../outside/basic" + ] + } + }, + { + "id": "hosts-file-rejects-windows-home-path", + "operation": "install", + "description": "Rejects a custom hosts file whose home path contains a Windows volume path.", + "options": { + "appId": "example-cli", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "evil" + ], + "hostsFile": "$WORKSPACE/hosts.json", + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [], + "files": { + "$WORKSPACE/hosts.json": { + "schemaVersion": 1, + "hosts": [ + { + "id": "evil", + "displayName": "Evil", + "projectSkillsDirs": [], + "userSkillsDirs": [ + "~/C:/outside" + ], + "detect": [ + "~/.agents" + ], + "status": "community" + } + ] + } + } + }, + "expected": { + "throws": true, + "filesAbsent": [ + "$HOME/C:/outside/basic" + ] + } + }, + { + "id": "install-rejects-empty-app-id", + "operation": "install", + "description": "Rejects an empty owner id before writing install metadata.", + "options": { + "appId": "", + "skillBundleDir": "testdata/skills/basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "files": {} + }, + "expected": { + "report": { + "installed": [], + "updated": [], + "skipped": [], + "conflicts": [], + "errors": [ + { + "reason": "invalid-app-id" + } + ] + }, + "filesAbsent": [ + "$HOME/.agents/skills/basic" + ] + } + }, + { + "id": "uninstall-rejects-empty-app-id", + "operation": "uninstall", + "description": "Rejects an empty owner id before considering a managed install for deletion.", + "options": { + "appId": "", + "skillName": "basic", + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills/basic" + ], + "files": { + "$HOME/.agents/skills/basic/.kitup.json": { + "schemaVersion": 1, + "appId": "example-cli", + "skillName": "basic", + "source": "bundled", + "hash": "sha256:abc" + } + } + }, + "expected": { + "report": { + "removed": [], + "skipped": [], + "conflicts": [], + "errors": [ + { + "reason": "invalid-app-id" + } + ] + }, + "filesPresent": [ + "$HOME/.agents/skills/basic/.kitup.json" + ] + } + }, + { + "id": "initial-install-copy-failure-is-atomic", + "operation": "install", + "description": "Leaves no target behind when a new install fails while copying its bundle.", + "options": { + "appId": "example-cli", + "skillFiles": [ + { + "path": "SKILL.md", + "contents": "---\nname: atomic-failure\ndescription: Atomic initial install fixture.\n---\n" + }, + { + "path": "a", + "contents": "file\n" + }, + { + "path": "a/b", + "contents": "nested\n" + } + ], + "scope": "user", + "agents": [ + "codex" + ], + "home": "$HOME", + "cwd": "$WORKSPACE" + }, + "given": { + "dirs": [ + "$HOME/.agents/skills" + ], + "files": {} + }, + "expected": { + "throws": true, + "filesAbsent": [ + "$HOME/.agents/skills/atomic-failure" + ] + } + } + ] +} diff --git a/go/testdata/skills/basic/SKILL.md b/go/testdata/skills/basic/SKILL.md new file mode 100644 index 0000000..0a45681 --- /dev/null +++ b/go/testdata/skills/basic/SKILL.md @@ -0,0 +1,12 @@ +--- +name: basic +description: Validate kitup installation behavior. Use when testing bundled skill copy, metadata, and nested resource handling. +--- + +# Basic Skill + +Use `references/guide.md` for the expected behavior. + +Use `assets/template.json` as fixture data when a test needs a bundled resource. + +Run `scripts/helper.sh` only when an embedding test explicitly asks for script behavior. diff --git a/go/testdata/skills/basic/assets/template.json b/go/testdata/skills/basic/assets/template.json new file mode 100644 index 0000000..8961d7f --- /dev/null +++ b/go/testdata/skills/basic/assets/template.json @@ -0,0 +1,4 @@ +{ + "name": "basic", + "purpose": "fixture" +} diff --git a/go/testdata/skills/basic/references/guide.md b/go/testdata/skills/basic/references/guide.md new file mode 100644 index 0000000..2949a95 --- /dev/null +++ b/go/testdata/skills/basic/references/guide.md @@ -0,0 +1,5 @@ +# Basic Fixture Guide + +This fixture represents a small valid Agent Skill with nested resources. + +Installers must copy this file with the rest of the skill directory. diff --git a/go/testdata/skills/basic/scripts/helper.sh b/go/testdata/skills/basic/scripts/helper.sh new file mode 100755 index 0000000..352e354 --- /dev/null +++ b/go/testdata/skills/basic/scripts/helper.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +set -eu +printf '%s\n' "basic helper" diff --git a/go/testdata/skills/invalid-frontmatter/SKILL.md b/go/testdata/skills/invalid-frontmatter/SKILL.md new file mode 100644 index 0000000..da2e3d5 --- /dev/null +++ b/go/testdata/skills/invalid-frontmatter/SKILL.md @@ -0,0 +1,8 @@ +--- +name: Invalid Name +description: +--- + +# Invalid Frontmatter + +This fixture intentionally violates the Agent Skills frontmatter rules. diff --git a/go/testdata/skills/missing-skill-md/README.md b/go/testdata/skills/missing-skill-md/README.md new file mode 100644 index 0000000..f717a71 --- /dev/null +++ b/go/testdata/skills/missing-skill-md/README.md @@ -0,0 +1,3 @@ +# Missing SKILL Fixture + +This directory intentionally has no `SKILL.md`. diff --git a/scripts/check-go-cobra.sh b/scripts/check-go-cobra.sh new file mode 100644 index 0000000..4a1f0a5 --- /dev/null +++ b/scripts/check-go-cobra.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT INT TERM + +mkdir -p "$tmp/modules" +tar -C "$root" -cf "$tmp/go.tar" go +tar -C "$root" -cf "$tmp/go-cobra.tar" go-cobra +tar -C "$tmp/modules" -xf "$tmp/go.tar" +tar -C "$tmp/modules" -xf "$tmp/go-cobra.tar" + +cd "$tmp/modules/go-cobra" +go mod edit "-replace=github.com/lathe-cli/kitup/go=$tmp/modules/go" +GOWORK=off go test ./... + +echo "ok: Go Cobra adapter passes against the local core module" diff --git a/scripts/check-go-release.sh b/scripts/check-go-release.sh new file mode 100644 index 0000000..1240269 --- /dev/null +++ b/scripts/check-go-release.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +version="$(node -p 'require(process.argv[1]).version' "$root/ts/package.json")" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT INT TERM + +mkdir -p "$tmp/modules" +tar -C "$root" -cf "$tmp/go.tar" go +tar -C "$root" -cf "$tmp/go-cobra.tar" go-cobra +tar -C "$tmp/modules" -xf "$tmp/go.tar" +tar -C "$tmp/modules" -xf "$tmp/go-cobra.tar" + +( + cd "$tmp/modules/go" + GOWORK=off go test ./... +) +( + cd "$tmp/modules/go-cobra" + GOWORK=off go test ./... +) + +consumer="$tmp/consumer" +mkdir -p "$consumer" +cd "$consumer" +go mod init kitup-release-consumer >/dev/null +go mod edit \ + "-require=github.com/lathe-cli/kitup/go@v$version" \ + "-require=github.com/lathe-cli/kitup/go-cobra@v$version" \ + "-replace=github.com/lathe-cli/kitup/go=$tmp/modules/go" \ + "-replace=github.com/lathe-cli/kitup/go-cobra=$tmp/modules/go-cobra" + +cat > main.go <<'GO' +package main + +import ( + "fmt" + + kitup "github.com/lathe-cli/kitup/go" + kitupcobra "github.com/lathe-cli/kitup/go-cobra" +) + +func main() { + cmd := kitupcobra.NewSkillCommand(kitupcobra.Options{}) + if cmd.Use != kitup.InstallUX.SkillUse { + panic(fmt.Sprintf("expected %s, got %s", kitup.InstallUX.SkillUse, cmd.Use)) + } +} +GO + +GOWORK=off go mod tidy +GOWORK=off go list -m all >/dev/null +GOWORK=off go test ./... +GOWORK=off go build . + +test "$(GOWORK=off go list -m -f '{{.Version}}' github.com/lathe-cli/kitup/go)" = "v$version" +test "$(GOWORK=off go list -m -f '{{.Version}}' github.com/lathe-cli/kitup/go-cobra)" = "v$version" + +echo "ok: packaged Go modules are self-contained at v$version" diff --git a/scripts/check.mjs b/scripts/check.mjs index c67bbff..634f515 100755 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -331,6 +331,13 @@ for (const [group, name, command, args, cwd, env] of [ ["scripts/sync-hosts.mjs", "--check"], rootPath, ], + [ + "source", + "go-test-fixtures", + "node", + ["scripts/sync-go-testdata.mjs", "--check"], + rootPath, + ], [ "typescript", "typescript-format", @@ -356,14 +363,10 @@ for (const [group, name, command, args, cwd, env] of [ "go", ["test", "-count=1", "./..."], new URL("../go/", import.meta.url), + { GOWORK: "off" }, ], - [ - "go", - "go-cobra", - "go", - ["test", "./..."], - new URL("../go-cobra/", import.meta.url), - ], + ["go", "go-cobra", "sh", ["scripts/check-go-cobra.sh"], rootPath], + ["go", "go-release", "sh", ["scripts/check-go-release.sh"], rootPath], ["rust", "rust", "cargo", ["test"], new URL("../rust/", import.meta.url)], [ "rust", diff --git a/scripts/sync-go-testdata.mjs b/scripts/sync-go-testdata.mjs new file mode 100644 index 0000000..c5879e0 --- /dev/null +++ b/scripts/sync-go-testdata.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +import { cpSync, mkdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const root = new URL("../", import.meta.url); +const check = process.argv.slice(2).includes("--check"); +const files = [ + "spec/hosts.json", + "testdata/cases/bundled-skill-install.json", + "testdata/skills/basic/SKILL.md", + "testdata/skills/basic/assets/template.json", + "testdata/skills/basic/references/guide.md", + "testdata/skills/basic/scripts/helper.sh", + "testdata/skills/invalid-frontmatter/SKILL.md", + "testdata/skills/missing-skill-md/README.md", +]; + +for (const sourcePath of files) { + const targetPath = `go/${sourcePath}`; + const source = new URL(sourcePath, root); + const target = new URL(targetPath, root); + if (check) { + let targetContents; + try { + targetContents = readFileSync(target); + } catch { + fail(`missing generated Go test fixture: ${targetPath}`); + } + if (!readFileSync(source).equals(targetContents)) { + fail(`stale generated Go test fixture: ${targetPath}`); + } + continue; + } + mkdirSync(new URL("./", target), { recursive: true }); + cpSync(source, target); +} + +console.log( + check + ? `ok: ${files.length} Go test fixtures are current` + : `updated ${files.length} Go test fixtures`, +); + +function fail(message) { + console.error(message); + process.exit(1); +}