Commit 229642c
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
- src
- commands
- lib
- test
- fixtures
- mini-commons-invalid
- packs/2026-01-bad/broken-skill
- mini-commons
- packs/2026-01-test
- alpha-skill
- beta-skill
- docs/plans
- site/src/content/docs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
29 | 32 | | |
30 | 33 | | |
31 | 34 | | |
| |||
118 | 121 | | |
119 | 122 | | |
120 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
121 | 152 | | |
122 | 153 | | |
123 | 154 | | |
| |||
0 commit comments