Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ scripts/sync-hosts.mjs generated host constants

TypeScript, Go, Rust, and Python SDKs live in `ts/`, `go/`, `rust/`, and `python/`.

Each SDK separates bundle loading, host resolution, installation, and CLI workflow. Public entry points stay at the package root. Start with the workflow for selection and confirmation, the installer for lifecycle decisions, and storage or metadata for ownership and filesystem writes. See [the architecture map](docs/architecture.mmd) for the data flow.

## Common Commands

```bash
Expand All @@ -61,6 +63,10 @@ make check

This validates the shared spec, fixtures, generated host constants, standalone Go modules, TypeScript, Go, Rust, Python, and examples.

`scripts/golden.mjs` owns golden fixture preparation, the local GitHub server, and result and filesystem assertions. Each language's golden test invokes its native SDK and returns structured results. These repository tests require Node.js 24; the published Go, Rust, and Python SDKs do not require Node.js.

The harness retains each SDK's existing hash order: TypeScript uses `localeCompare`, while Go, Rust, and Python use lexical order. It verifies each implementation against that order; this does not establish identical hashes across languages.

## Host Adapter Changes

Host support is data-first.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test-python: ## Run Python SDK tests
fmt: fmt-ts fmt-go fmt-rust fmt-python ## Format all SDK code

fmt-ts: ## Format TypeScript code
cd $(TS_DIR) && pnpm exec prettier --write src test ../examples/ts/cli.ts ../scripts/check.mjs ../scripts/check-go-modules.mjs ../scripts/prepare-release.mjs
cd $(TS_DIR) && pnpm exec prettier --write src test ../examples/ts/cli.ts ../scripts/check.mjs ../scripts/check-go-modules.mjs ../scripts/prepare-release.mjs ../scripts/golden.mjs ../scripts/golden.d.mts

fmt-go: ## Format Go code
gofmt -w $(GO_FILES)
Expand Down
10 changes: 7 additions & 3 deletions docs/architecture.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ flowchart TB
HOSTSPEC["Host Spec\nspec/hosts.json"]:::contract
SCHEMAS["Schemas\nspec/hosts.schema.json\ntestdata/cases.schema.json"]:::contract
CASES["Golden Cases\ntestdata/cases/*.json"]:::contract
GOGOLDEN["Go Golden Harness\ntests/go-golden"]:::artifact
GOLDEN["Shared Golden Harness\nscripts/golden.mjs\nfixtures · local GitHub server · assertions"]:::control
ADAPTERS["Native Test Adapters\nTypeScript · Go · Rust · Python"]:::artifact
GENHOSTS["Generated Host Constants\nts / go / rust / python"]:::artifact
VERIFY["Verification\ncheck.mjs · sync-hosts.mjs · standalone Go modules"]:::control
GITHUB["GitHub API"]:::external
Expand All @@ -39,8 +40,11 @@ flowchart TB
HOSTSPEC -.->|"generates"| GENHOSTS
VERIFY -.-> HOSTSPEC
VERIFY -.->|"checks parity"| CASES
VERIFY -.-> GOGOLDEN
CASES -.-> GOGOLDEN
VERIFY -.-> GOLDEN
CASES -.-> GOLDEN
GOLDEN -->|"prepared cases"| ADAPTERS
ADAPTERS -->|"native SDK calls"| SDK
ADAPTERS -->|"structured results"| GOLDEN
VERIFY -.-> GENHOSTS

classDef actor fill:#1f2937,stroke:#94a3b8,color:#f8fafc,stroke-width:1px;
Expand Down
77 changes: 28 additions & 49 deletions go-cobra/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ func NewUninstallCommand(opts Options) *cobra.Command {
Short: "Uninstall bundled Agent Skill",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
emit := func(report kitup.UninstallReport) error {
if jsonOutput {
return writeJSON(output(cmd, opts), report)
}
renderUninstallReport(output(cmd, opts), report)
return nil
}
parsed := kitup.ParseInstallFlags(kitup.InstallFlagValues{Scope: scope, ScopeSet: true, Agents: agents, Yes: yes})
if err := kitup.InstallFlagError(parsed.Errors); err != nil {
return err
Expand All @@ -115,22 +122,13 @@ func NewUninstallCommand(opts Options) *cobra.Command {
}
if len(status.Conflicts)+len(status.Errors) > 0 {
report := uninstallReportFromStatus(status)
if jsonOutput {
if err := writeJSON(output(cmd, opts), report); err != nil {
return err
}
} else {
renderUninstallReport(output(cmd, opts), report)
if err := emit(report); err != nil {
return err
}
return errors.New("kitup: uninstall has conflicts")
}
if len(status.Installed) == 0 {
report := uninstallReportFromStatus(status)
if jsonOutput {
return writeJSON(output(cmd, opts), report)
}
renderUninstallReport(output(cmd, opts), report)
return nil
return emit(uninstallReportFromStatus(status))
}
promptOut := output(cmd, opts)
if jsonOutput {
Expand Down Expand Up @@ -162,12 +160,8 @@ func NewUninstallCommand(opts Options) *cobra.Command {
if err != nil {
return err
}
if jsonOutput {
if err := writeJSON(output(cmd, opts), report); err != nil {
return err
}
} else {
renderUninstallReport(output(cmd, opts), report)
if err := emit(report); err != nil {
return err
}
if len(report.Conflicts)+len(report.Errors) > 0 {
return errors.New("kitup: uninstall failed")
Expand All @@ -183,35 +177,21 @@ func NewUninstallCommand(opts Options) *cobra.Command {
}

func NewInstallCommand(opts Options) *cobra.Command {
scope := ""
var agents []string
var yes bool
var dryRun bool
var force bool
flags := kitup.InstallFlagValues{}

cmd := &cobra.Command{
Use: kitup.InstallUX.InstallUse,
Short: kitup.InstallUX.InstallShort,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
parsed := kitup.ParseInstallFlags(kitup.InstallFlagValues{
Scope: scope,
ScopeSet: cmd.Flags().Changed("scope"),
Agents: agents,
Yes: yes,
DryRun: dryRun,
Force: force,
})
flags.ScopeSet = cmd.Flags().Changed("scope")
parsed := kitup.ParseInstallFlags(flags)
if err := kitup.InstallFlagError(parsed.Errors); err != nil {
return err
}
report, err := kitup.RunBundledSkillInstall(kitup.InstallWorkflowOptions{
InstallOptions: kitup.InstallOptions{
BaseOptions: kitup.BaseOptions{
Home: opts.Home,
CWD: opts.CWD,
HostsFile: opts.HostsFile,
},
BaseOptions: baseOptions(opts),
AppID: opts.AppID,
SkillBundle: opts.Bundle,
Scope: parsed.Scope,
Expand All @@ -235,11 +215,11 @@ func NewInstallCommand(opts Options) *cobra.Command {
return kitup.InstallWorkflowError(report)
},
}
cmd.Flags().StringVar(&scope, "scope", scope, kitup.InstallUX.ScopeFlag)
cmd.Flags().StringArrayVar(&agents, "agent", nil, kitup.InstallUX.AgentFlag)
cmd.Flags().BoolVar(&dryRun, "dry-run", false, kitup.InstallUX.DryRunFlag)
cmd.Flags().BoolVarP(&yes, "yes", "y", false, kitup.InstallUX.YesFlag)
cmd.Flags().BoolVar(&force, "force", false, kitup.InstallUX.ForceFlag)
cmd.Flags().StringVar(&flags.Scope, "scope", "", kitup.InstallUX.ScopeFlag)
cmd.Flags().StringArrayVar(&flags.Agents, "agent", nil, kitup.InstallUX.AgentFlag)
cmd.Flags().BoolVar(&flags.DryRun, "dry-run", false, kitup.InstallUX.DryRunFlag)
cmd.Flags().BoolVarP(&flags.Yes, "yes", "y", false, kitup.InstallUX.YesFlag)
cmd.Flags().BoolVar(&flags.Force, "force", false, kitup.InstallUX.ForceFlag)
return cmd
}

Expand Down Expand Up @@ -332,12 +312,7 @@ func renderStatusReport(out io.Writer, report kitup.StatusReport) {
for _, item := range report.Missing {
_, _ = fmt.Fprintf(out, "missing\t%s\t%s\n", targetHosts(item), item.TargetDir)
}
for _, item := range report.Conflicts {
_, _ = fmt.Fprintf(out, "conflict\t%s\t%s\t%s\n", targetHosts(item.TargetResult), item.TargetDir, item.Reason)
}
for _, item := range report.Errors {
_, _ = fmt.Fprintf(out, "error\t%s\n", item.Reason)
}
renderProblems(out, report.Conflicts, report.Errors)
}

func renderUninstallReport(out io.Writer, report kitup.UninstallReport) {
Expand All @@ -347,10 +322,14 @@ func renderUninstallReport(out io.Writer, report kitup.UninstallReport) {
for _, item := range report.Skipped {
_, _ = fmt.Fprintf(out, "skipped\t%s\t%s\t%s\n", targetHosts(item.TargetResult), item.TargetDir, item.Reason)
}
for _, item := range report.Conflicts {
renderProblems(out, report.Conflicts, report.Errors)
}

func renderProblems(out io.Writer, conflicts []kitup.TargetStatus, errors []kitup.ReportError) {
for _, item := range conflicts {
_, _ = fmt.Fprintf(out, "conflict\t%s\t%s\t%s\n", targetHosts(item.TargetResult), item.TargetDir, item.Reason)
}
for _, item := range report.Errors {
for _, item := range errors {
_, _ = fmt.Fprintf(out, "error\t%s\n", item.Reason)
}
}
Expand Down
Loading
Loading