Skip to content
Closed
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Host support is data-first.

- Put paths and aliases in `spec/hosts.json` whenever possible.
- Do not add host-specific branching unless the generic resolver cannot express the host.
- `projectSkillsDirs` and `userSkillsDirs` are ordered; the first path is the canonical install target.
- `projectSkillsDirs` and `userSkillsDirs` are ordered; reuse the first existing compatible path, or fall back to the first canonical path.
- Project paths must be relative. User paths must start with `~/`.
- A host may be project-only or user-only.
- Multiple selected hosts may resolve to the same target directory; copy once and report all matching hosts.
Expand Down
15 changes: 10 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────────────

Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mycli skill install
- validate bundled skills
- install from a local directory, embedded bundle tree, or public GitHub bundle directory
- copy, update, and uninstall kitup-owned installs
- inspect installed ownership, source, CLI version, and revision metadata
- refuse unsafe overwrite conflicts
- return structured install reports

Expand Down Expand Up @@ -143,11 +144,21 @@ import (
)

root.AddCommand(kitupcobra.NewSkillCommand(kitupcobra.Options{
AppID: "mycli",
Bundle: kitup.FSBundle(embeddedSkills, "skills/mycli"),
AppID: "mycli",
SkillName: "mycli",
Bundle: kitup.WithBundleMetadata(
kitup.FSBundle(embeddedSkills, "skills/mycli"),
kitup.BundledMetadata{
CLIVersion: "1.2.3",
Revision: "abc123",
SourceID: "mycli:embedded",
},
),
}))
```

The command includes `skill install`, `skill status`, and `skill uninstall`. Status and uninstall support `--json`; uninstall only removes directories with valid `.kitup.json` ownership matching `AppID`.

### Rust

Install:
Expand Down
107 changes: 92 additions & 15 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ The core flow is:
2. resolve safe target agent selection for CLI workflows
3. validate `SKILL.md`
4. copy, update, skip, or report conflicts
5. write `.kitup.json` ownership metadata
6. return a structured report
5. write or read `.kitup.json` ownership and provenance metadata
6. return a structured install, status, or uninstall report

## TypeScript

Package: `@kitup/sdk`

```ts
import {
type BundledMetadata,
detectHosts,
directoryBundle,
filesBundle,
Expand All @@ -27,6 +28,7 @@ import {
installUxText,
moduleDirBundle,
planBundledSkill,
readInstalledMetadata,
parseInstallFlags,
classifyInstallWorkflowExit,
resolveInstallSelection,
Expand All @@ -42,7 +44,12 @@ Primitive install call:
```ts
const report = await installBundledSkill({
appId: "mycli",
skillBundle: directoryBundle("./skills/mycli"),
skillBundle: directoryBundle("./skills/mycli", {
cliVersion: "1.2.3",
revision: "abc123",
sourceId: "mycli:embedded",
provenance: { build: "release" },
}),
scope: "user",
});
```
Expand Down Expand Up @@ -77,9 +84,9 @@ Implemented functions:
- `resolveInstallTargets({ home?, cwd?, hostsFile?, agents?, scope, skillName })`
- `validateSkillBundle(bundle, cwd?)`
- `computeBundleContentHash(bundle, cwd?)`
- `directoryBundle(path)`
- `filesBundle(files)`
- `moduleDirBundle(importMetaUrl, relativePath)`
- `directoryBundle(path, metadata?)`
- `filesBundle(files, metadata?)`
- `moduleDirBundle(importMetaUrl, relativePath, metadata?)`
- `githubBundle(options)`
- `parseInstallFlags(flags)`
- `agentSelectorFromFlags(values)`
Expand All @@ -92,6 +99,7 @@ Implemented functions:
- `installBundledSkill(options)`
- `updateBundledSkill(options)`
- `uninstallBundledSkill(options)`
- `readInstalledMetadata(targetDir)`
- `installUxText`

## Go
Expand All @@ -107,7 +115,15 @@ Primitive install call:
```go
report, err := kitup.InstallBundledSkill(kitup.InstallOptions{
AppID: "mycli",
SkillBundle: kitup.DirectoryBundle("./skills/mycli"),
SkillBundle: kitup.WithBundleMetadata(
kitup.DirectoryBundle("./skills/mycli"),
kitup.BundledMetadata{
CLIVersion: "1.2.3",
Revision: "abc123",
SourceID: "mycli:embedded",
Provenance: map[string]string{"build": "release"},
},
),
Scope: kitup.UserScope,
})
```
Expand Down Expand Up @@ -140,6 +156,7 @@ Implemented functions:
- `FSBundle(fsys, root)`
- `FilesBundle(files)`
- `GitHubBundle(opts)`
- `WithBundleMetadata(bundle, metadata)`
- `ParseInstallFlags(flags)`
- `AgentSelectorFromFlags(values)`
- `ParseScopeFlag(value)`
Expand All @@ -151,12 +168,16 @@ Implemented functions:
- `InstallBundledSkill(opts)`
- `UpdateBundledSkill(opts)`
- `UninstallBundledSkill(opts)`
- `StatusBundledSkill(opts)`
- `ReadInstalledMetadata(targetDir)`
- `InstallUX`

Optional Cobra adapter module: `github.com/lathe-cli/kitup/go-cobra`

- `NewSkillCommand(opts)`
- `NewInstallCommand(opts)`
- `NewStatusCommand(opts)`
- `NewUninstallCommand(opts)`

## Rust

Expand All @@ -168,7 +189,15 @@ Primitive install call:
let report = kitup::install_bundled_skill(&kitup::InstallOptions {
base: kitup::BaseOptions::default(),
app_id: "mycli".to_string(),
skill_bundle: kitup::directory_bundle("./skills/mycli"),
skill_bundle: kitup::with_bundle_metadata(
kitup::directory_bundle("./skills/mycli"),
kitup::BundledMetadata {
cli_version: Some("1.2.3".to_string()),
revision: Some("abc123".to_string()),
source_id: Some("mycli:embedded".to_string()),
provenance: [("build".to_string(), "release".to_string())].into(),
},
),
scope: kitup::Scope::User,
agents: kitup::AgentSelector::Auto,
force: false,
Expand Down Expand Up @@ -217,6 +246,7 @@ Implemented functions:
- `files_bundle(files)`
- `include_dir_bundle(dir)` with the `include-dir` feature
- `github_bundle(options)`
- `with_bundle_metadata(bundle, metadata)`
- `parse_install_flags(flags)`
- `agent_selector_from_flags(values, errors)`
- `parse_scope_flag(value, errors)`
Expand All @@ -229,6 +259,7 @@ Implemented functions:
- `install_bundled_skill(options)`
- `update_bundled_skill(options)`
- `uninstall_bundled_skill(options)`
- `read_installed_metadata(target_dir)`
- `INSTALL_UX`

## Python
Expand All @@ -238,6 +269,7 @@ Package: `kitup-sdk`
```python
from kitup import (
BaseOptions,
BundledMetadata,
InstallOptions,
InstallWorkflowOptions,
classify_install_workflow_exit,
Expand All @@ -253,6 +285,7 @@ from kitup import (
parse_install_flags,
parse_scope_flag,
plan_bundled_skill,
read_installed_metadata,
resolve_hosts,
resolve_install_selection,
resolve_install_targets,
Expand All @@ -273,7 +306,15 @@ report = install_bundled_skill(
InstallOptions(
base=BaseOptions(),
app_id="mycli",
skill_bundle=directory_bundle("./skills/mycli"),
skill_bundle=directory_bundle(
"./skills/mycli",
BundledMetadata(
cli_version="1.2.3",
revision="abc123",
source_id="mycli:embedded",
provenance={"build": "release"},
),
),
scope="user",
)
)
Expand Down Expand Up @@ -322,9 +363,9 @@ Implemented functions:
- `resolve_install_targets(options, agents, scope, skill_name)`
- `validate_skill_bundle(bundle, cwd=None)`
- `compute_bundle_content_hash(bundle, cwd=None)`
- `directory_bundle(path)`
- `files_bundle(files)`
- `resources_bundle(root)`
- `directory_bundle(path, metadata=None)`
- `files_bundle(files, metadata=None)`
- `resources_bundle(root, metadata=None)`
- `github_bundle(options)`
- `parse_install_flags(flags)`
- `agent_selector_from_flags(values, errors)`
Expand All @@ -338,6 +379,7 @@ Implemented functions:
- `install_bundled_skill(options)`
- `update_bundled_skill(options)`
- `uninstall_bundled_skill(options)`
- `read_installed_metadata(target_dir)`
- `INSTALL_UX`

## Options
Expand All @@ -361,6 +403,28 @@ The first non-local bundle constructor is GitHub only:

GitHub bundle resolution downloads only files under the configured directory path, requires `SKILL.md` at that bundle root, records the requested ref and resolved commit, and writes GitHub provenance into `.kitup.json`. It does not search GitHub, install dependencies, execute scripts, handle private auth, or install whole repositories by default.

Bundled and embedded bundles can provide optional `cliVersion`, `revision`, `sourceId`, and string-valued `provenance`. These values describe the embedding CLI build and source; they do not change the skill content hash or ownership rules.

Reinstalling an unchanged skill refreshes `.kitup.json` when these source or build fields changed, so status does not remain pinned to metadata from an older CLI release.

## Installed metadata

`InstalledMetadata` is public in all four SDKs. The reader APIs are `readInstalledMetadata`, `ReadInstalledMetadata`, and `read_installed_metadata`. A missing `.kitup.json` is reported as absent; malformed content, an unsupported schema, invalid ownership fields, or invalid optional field types is reported as an error.

For a missing file, TypeScript returns `undefined`, Python returns `None`, Rust returns `Ok(None)`, and Go returns `ErrInstalledMetadataNotFound`. Go uses `ErrInvalidInstalledMetadata` for invalid content; the other SDKs use their standard invalid-data error mechanism.

Optional string fields may be omitted, but when present they must be non-empty strings. `provenance` may be omitted, but when present it must be an object whose values are strings. Explicit `null` values fail closed in every SDK.

The `.kitup.json` schema remains at `schemaVersion: 1`. Existing required fields remain unchanged:

- `appId`, `skillName`: ownership identity
- `source`: `bundled` or `github`
- `hash`: installed bundle content hash

The existing optional `sourceId`, `version`, and `provenance` fields remain compatible. `cliVersion` and `revision` are new optional fields. Older version 1 metadata remains readable without them.

Install, update, status, and uninstall treat malformed metadata as unmanaged and fail closed. Uninstall moves a matching target to a same-parent quarantine path, revalidates its metadata after the move, and only then removes that exact tree. It restores a target that fails revalidation where safe. There is no implicit force behavior.

The embedding CLI owns command names and framework attachment. `kitup` owns standard install flag semantics, selector mapping, user-facing workflow text, summary rendering, confirmation, dry-run planning, workflow exit classification, and execution. For user-facing commands, call `runBundledSkillInstall` / `RunBundledSkillInstall` / `run_bundled_skill_install` with values from the shared flag parsing helpers.

Workflow-only options:
Expand Down Expand Up @@ -401,8 +465,14 @@ mycli skill install
mycli skill install --scope user --agent codex
mycli skill install --scope project --agent codex --agent claude-code
mycli skill install --scope user --agent codex --force
mycli skill status --scope user --agent codex --json
mycli skill uninstall --scope user --agent codex --json
```

The Go Cobra adapter provides `skill status` and `skill uninstall`. Both accept `--scope`, repeatable `--agent`, and optional `--json`, and reuse the same existing-compatible-directory-first target resolution as install. When `CurrentAgent` is set and `--agent` is omitted, both commands inspect the current agent and universal targets selected by install. Neither command exposes `--force`.

Set Cobra `Options.SkillName` for lifecycle commands that must remain available when the original local, embedded, or GitHub bundle cannot be read. When it is omitted, the adapter derives the name by validating `Options.Bundle` for backward compatibility.

The lower-level selection resolver remains available for custom shells. It returns one of:

- `install`: proceed to plan and confirmation with `selectedHostIds`
Expand All @@ -415,8 +485,8 @@ In TTY mode, zero detected hosts prompts from all supported hosts. One detected

Selector semantics:

- `scope: "user"` installs into the first `userSkillsDirs` path for each host.
- `scope: "project"` installs into the first `projectSkillsDirs` path for each host.
- `scope: "user"` reuses the first existing `userSkillsDirs` path, or creates the first canonical path when none exist.
- `scope: "project"` reuses the first existing `projectSkillsDirs` path, or creates the first canonical path when none exist.
- `agents: "auto"` uses host detection.
- `agents: "*"` selects every host adapter.
- explicit agents select canonical host ids or aliases.
Expand All @@ -439,7 +509,14 @@ Uninstall reports include:
- `conflicts`
- `errors`

TypeScript returns typed report objects. Go exposes `InstallReport`, `UninstallReport`, `TargetResult`, `TargetStatus`, and `ReportError`. Rust exposes `InstallReport`, `UninstallReport`, `TargetResult`, `TargetStatus`, and `ReportError`.
Go status reports include:

- `installed`, with an `InstalledMetadata` object for each target
- `missing`
- `conflicts`
- `errors`

TypeScript returns typed report objects. Go exposes `InstallReport`, `StatusReport`, `UninstallReport`, `InstalledMetadata`, `TargetResult`, `TargetStatus`, and `ReportError`. Rust exposes `InstallReport`, `UninstallReport`, `InstalledMetadata`, `TargetResult`, `TargetStatus`, and `ReportError`. Python exposes the same installed metadata fields as `InstalledMetadata`.

The serialized JSON report shape is the same across TypeScript, Go, and Rust. `installed`, `updated`, and `removed` contain target results. `skipped` and `conflicts` contain target results plus `reason`.

Expand Down
10 changes: 10 additions & 0 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions docs/architecture.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ flowchart TB
BUNDLE["Bundle Resolver\nlocal · embedded · GitHub"]:::execution
VALIDATE["Validator\nSKILL.md frontmatter"]:::execution
HOST["Host Resolver\nids · aliases · detection · targets"]:::execution
INSTALL["Installer\nplan · conflict policy · copy · update · uninstall"]:::execution
REPORT["Reports\nInstallReport · UninstallReport"]:::execution
INSTALL["Lifecycle\nplan · copy · update · status · uninstall"]:::execution
REPORT["Reports\nInstallReport · StatusReport · UninstallReport"]:::execution

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the canonical architecture map limited to implemented reports

The diagram places StatusReport inside the shared SDK (ts / go / rust / python) boundary, but a repo-wide search shows that only Go defines StatusReport or a status operation, and the shared case schema has no status operation. Since this file is the canonical architecture map, it currently documents a cross-language report boundary that is neither implemented nor fixture-covered; label status as Go-only or add the missing SDK behavior and golden cases.

AGENTS.md reference: AGENTS.md:L134-L134

Useful? React with 👍 / 👎.

end

HOSTSPEC["Host Spec\nspec/hosts.json"]:::contract
Expand All @@ -18,7 +18,7 @@ flowchart TB
VERIFY["Verification\ncheck.mjs · sync-hosts.mjs"]:::control
GITHUB["GitHub API"]:::external
TARGETS["Agent Host\nDirectory State"]:::state
METADATA[".kitup.json"]:::state
METADATA[".kitup.json\nownership · source · CLI build · provenance"]:::state

AUTHOR -->|"provides flags"| WORKFLOW
WORKFLOW --> BUNDLE
Expand All @@ -31,6 +31,7 @@ flowchart TB
HOST --> INSTALL
INSTALL -->|"copies, updates, removes"| TARGETS
INSTALL -->|"writes .kitup.json"| METADATA
METADATA -->|"reads fail closed"| INSTALL
INSTALL -->|"returns report"| REPORT

SCHEMAS -.-> HOSTSPEC
Expand Down
Loading
Loading