Skip to content

Commit 229642c

Browse files
authored
feat(cli): skdd add/push/drops — Commons install + upstream evolution flow (#8)
* feat(cli): add skdd add/push/drops — the Commons verbs skdd add <source> [selector]: install a drop (or one skill) from a Commons repo — GitHub shorthand, git URL (#ref), or local path. Strict validation before install, collision check with --rename escape hatch, registry provenance owner/repo@shortsha (drop-id), full sha recorded in .skdd-lock.json, mirrors refreshed via the existing safe (never forced) link path. --dry-run/--json/--non-interactive/-g. skdd push <skill|pack>: ship a skill upstream as a PR via gh. Strips machine-local state (usage-count -> "0", last-used dropped), preserves forged-* provenance. Upstream-known skills branch as evolve/<name> with a diff summary; new skills branch as skill/<name> into incoming/ (or an existing drop via --drop, updating drops.json). Local-path targets are a --dry-run test seam. Default target from ~/.skdd/config.toml commons key (smol-toml, already a dependency). skdd drops [--from]: list a Commons' drops (table/json). No new runtime dependencies. 24 new tests incl. a regression test that add never force-replaces a populated mirror dir. * fix(cli): treat Commons manifests as hostile input; allowlist push payload Codex adversarial review findings: - [high] drops.json ids/names were used as filesystem path segments unchecked, so a malicious Commons could list a skill like '../escape-skill' and write outside the colony. Every manifest parse site now enforces the lowercase-kebab-case grammar (no slashes, dots, or absolute paths) and add asserts source/destination containment as defense in depth. - [medium] push copied the entire local skill directory into the PR clone, so dotfiles, logs, .env files, or symlinked content could leak to a public Commons PR. Only an allowlisted payload travels now (SKILL.md + regular files under scripts/, references/, assets/); dry-run enumerates exactly what travels and what stays home. +5 adversarial tests (hostile manifests, payload exclusion incl. symlinks and .env); 899 total green. * fix(cli): refuse symlinked skill dirs and SKILL.md in push readFileSync follows symlinks, so a symlinked SKILL.md (or a symlinked skill directory) could exfiltrate arbitrary file contents into a Commons PR even with the payload allowlist. push now lstats both and refuses; collectPublishablePayload throws defensively. +2 tests. * fix(cli): guard symlinked skills at push discovery, not just the items loop Pack discovery called parseSkill (readFileSync follows symlinks) before the items-loop symlink guard, so a pack push dereferenced a symlinked skill while scanning. Hoisted the check into a skillDirIsSymlinked helper applied at discovery: direct pushes refuse, pack scans skip+warn, and the items-loop check remains as defense in depth. +1 pack-scan test. * fix(cli): address Commons review findings in add/push/commons/config - add/push honor .colony.json canonicalSkillsDir (pass to runLink; resolve before scanning) — .colony.json users could not add/push before - reject symlinks anywhere in a fetched Commons skill tree (add), matching the push-side guard - registry cells escape pipes/newlines and the parser unescapes, so an untrusted Commons description can't inject fake rows - push: metadata stripping scoped to frontmatter only (body examples with usage-count/last-used lines are preserved) - push: validate each local skill --strict before opening a PR CI would reject; distinguish an empty diff from a real git commit failure and hint at missing git identity; validate pack ids as git-ref-safe branch slugs; clear the upstream dir before an evolve copy so deleted files don't linger - parseSource splits #ref before local-path detection (../commons#feature) and local #ref checks out from a clean clone (never mutates the user repo) - local dirty repos record a -dirty provenance marker + lock flag - malformed ~/.skdd/config.toml surfaces instead of silently defaulting - extract lib/colony.ts (canonicalDirName) shared by add/push +11 tests (hostile symlink/name-mismatch in add, frontmatter-only strip, registry injection round-trip, parseSource #ref, provenance dirty). 914 green. * fix(cli): address second-round PR review comments on add/push - local #ref add clones with --branch (branch/tag refs resolve; sha falls back to detach) instead of failing on origin-only branches - --dry-run (including -g) no longer creates ~/.skdd - reject symlinked pack ancestors via realpath containment (a Commons making packs/ or the drop dir a symlink can't serve outside bytes) - validate pack ids with a full git-ref rule (reject foo.lock, a..b, …) - push payload skips non-regular files (FIFO/socket/device would hang cp) - strip an emptied metadata: block so the pushed SKILL.md has no null key - push validates the stripped payload (what the Commons CI sees) - rewrite drops.json only when a new skill is actually added Deferred: renamed-then-pushed skills classify by local name (niche). +8 tests; 919 green.
1 parent 1b24fa2 commit 229642c

22 files changed

Lines changed: 2597 additions & 7 deletions

File tree

cli/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ skdd list [--format=table|json] [-g]
2626
skdd link [--mode=symlink|copy|auto] [--harness=<list>] [--force] [--quiet] [-g]
2727
skdd doctor [--json] [-g]
2828
skdd import [target] [--json] [--apply] [--canonical=<dir>] [--skip-link] [-g]
29+
skdd add <source> [selector] [--rename=<name>] [--dry-run] [--json] [--non-interactive] [-g]
30+
skdd push <skill|pack> [--to=<owner/repo>] [--drop=<id>] [--dry-run] [-g]
31+
skdd drops [--from=<source>] [--format=table|json]
2932
skdd hub
3033
skdd mcp <subcommand>
3134
```
@@ -118,6 +121,34 @@ skdd import --apply # migrate + link
118121
skdd import ../some-other-project --apply # operate on a different root
119122
```
120123

124+
### `skdd add`
125+
126+
Install skills from a **Commons repo** — a git repo with a `drops.json` manifest and `packs/<drop-id>/<skill>/` directories (see [SkDD Commons](https://github.com/zakelfassi/skdd-commons)). Sources: GitHub shorthand (`owner/repo`), a full git URL, or a local path, each with an optional `#ref`. Selector: a drop id, `drop/skill` for a single skill, or omitted for an interactive pick.
127+
128+
Every skill is validated with `--strict` before install (refused on failure), checked for name collisions against the target colony (`--rename` resolves single-skill collisions), registered with provenance (`owner/repo@shortsha (drop-id)` in the Source column, full sha in `.skdd-lock.json`), and mirrored via the same **safe, never-forced** link path as `skdd link`. The manifest is treated as hostile input: drop ids and skill names must match the lowercase-kebab-case grammar (no slashes, no `..`), so a malicious `drops.json` can never write outside your `skills/` directory.
129+
130+
```bash
131+
skdd add zakelfassi/skdd-commons 2026-07-frontier # whole drop
132+
skdd add zakelfassi/skdd-commons 2026-07-frontier/finish-the-loop -g # one skill, global colony
133+
skdd add ../my-commons 2026-01-test --dry-run # local source, plan only
134+
```
135+
136+
### `skdd push`
137+
138+
Ship a skill (or every local skill sharing a `metadata.pack` id) upstream to a Commons as a PR. Needs the [GitHub CLI](https://cli.github.com) authenticated. The default target repo comes from `~/.skdd/config.toml` (`commons = "owner/repo"`).
139+
140+
Machine-local state is stripped before travel (`usage-count` resets to `"0"`, `last-used` is dropped); `forged-*` provenance is preserved. **Only the skill payload travels**`SKILL.md` plus regular files under `scripts/`, `references/`, and `assets/`; dotfiles, symlinks, and anything else in the skill directory stay home, and `--dry-run` enumerates exactly which files travel and which don't. Skills that already exist upstream branch as `evolve/<name>` with a diff summary; new skills branch as `skill/<name>` and land in `incoming/` for maintainer triage, or in an existing drop with `--drop <id>`.
141+
142+
```bash
143+
skdd push what-would-you-cut --dry-run # inspect the PR before sending it
144+
skdd push what-would-you-cut # fork, branch, PR
145+
skdd push my-new-skill --drop 2026-07-frontier
146+
```
147+
148+
### `skdd drops`
149+
150+
List the drops a Commons offers (id, title, date, skill count, story link). `--from` accepts the same source forms as `add`; defaults to the configured commons.
151+
121152
## Development
122153

123154
```bash

0 commit comments

Comments
 (0)