runkids/skillshare/proposals
- community
Checking that the SKILL.md file is still in the repository.
Install
npx vskill@latest install runkids/skillshare/proposalsSKILL.md
Feature Proposal: Move an installed skill into another folder
Issue: #510. Related: #247 (second half asks for the same), #108 (folder view; move was a non-goal there). Depends on: PR #513 (fix/install-missing-moved-skill), assumed merged first.
Problem
Install records live in one .metadata.json keyed by source-relative path, so moving an installed skill with mv strands its record at the old key (doctor: "N entries ... missing on disk"; dashboard: "Install missing"). website/docs/how-to/daily-tasks/organizing-skills.md now tells users to uninstall + install --into, which re-downloads, drops audit acceptances, and in project mode rewrites the lockfile pin. Dashboard "into folder" in InstallDialog is free text, so users mistype or cannot see existing folders.
PR #513 makes reconcile follow a plain mv by hash matching (MetadataStore.MovedEntryKey, MoveEntry). Its own "Not covered" list is this feature's reason to exist: records without file_hashes, skills edited after the move, and no way to do it from the dashboard. Hash inference is a recovery path; an explicit move should not depend on it.
Proposed Solution
One core operation, exposed as skillshare move and POST /api/resources/batch/move, plus two dashboard entry points. A move takes skills or whole folders; the verb is move because every existing command is a full word (mv, rename would be the odd ones out).
Core: internal/skillmove (new, mirrors internal/uninstall)
Plan(discovered, names, dest, Options) []Planned resolves and refuses without touching disk. Run([]Planned, Options) Outcome applies, with per-item results like uninstall.Run.
A name that resolves to a skill moves that skill, plus any skills nested below its directory (discovery keeps walking after a SKILL.md, so foo/SKILL.md and foo/sub/SKILL.md are both discovered; the rename carries both, so both need their records moved). A name that is a folder under the source moves the folder with every skill under it, keeping its base name: move frontend archive gives archive/frontend/.... Plan expands either into one Planned per discovered skill below it and refuses every one of them before any rename; a single refusal aborts the whole folder, so a folder never ends half-moved. Because discovery only sees skills, Plan also walks the whole directory tree of a skill or folder for _-prefixed tracked checkouts (even one with no discoverable skill) and followed links, and refuses on the first. The folder is then renamed once (Root.Rename), so empty subfolders and non-skill files move along; the per-skill steps below (store, lock, .skillignore, .gitignore) run for each skill, and the rename happens once per requested name.
Per skill, in this order:
sourcefs.MkdirAllIn(source, dest, follow)(ashandler_install.godoes for--into), thenRoot.Rename(from, to)(internal/sourcefs/sourcefs.go), withfromthe skill or the folder. Rename already refuses a link component and a rename across a followed link.- If the skill has a record:
store.MoveEntry(oldKey, to)from PR #513.MoveEntryre-keys the entry and itsAuditAcceptedlist but leavesGroupto the caller. Do not copy reconcile's group snippet: save the store, then run the existing reconcile (config.ReconcileGlobalSkills/ReconcileProjectSkills), which setsGroup, prunes, and in project mode rewritesconfig.yamlskills and callsWriteProjectLock. - Project mode, before that reconcile: carry the lock pin.
writeProjectLocknever moves a pin, so without this the pin would be re-created from the installed commit, losing a deliberately older pin. NewLock.MovePin(old, new)ininternal/install/lock.go, saved withinstall.LoadLock(projectdir.Resolve(root));pruneProjectLockincmd/skillshare/uninstall_project.gois the precedent. .skillignore: if a literal line equals the old relPath,skillignore.RemovePattern+AddPattern(internal/skillignore/write.go) with the new one. Glob rules are left alone; re-runsync.DiscoverSourceSkillsAlland warn whenDisabledflipped (astoggleOverrideErrordoes).- Project
.gitignore: swap the old entry for the new one viaconfig.ProjectGitignoreTarget+install.RemoveFromGitIgnoreBatch(reconcile'sonFoundonly adds). Whether project uninstall removes skill lines today is unverified; check before coding.
The reconcile walk stops at an installed parent (reconcile_core.go: existing.Source != "" returns SkipDir) and pruneStaleEntries then drops every record absent from that walk, so a re-keyed nested record under an installed parent would be pruned with its audit acceptances. Step 2 therefore also changes reconcile to keep records whose key lies below a live installed record (own test in internal/config), rather than bypassing the prune.
Order is rename first, store second: a failed rename changes nothing; a failed store save after a rename is healed by #513's reconcile when hashes exist, and reported otherwise.
MetadataStore has no exported "key for relPath" (GetByPath returns the entry only), so add KeyForPath(relPath) string matching KeyToRelPath(key, entry); needed for legacy basename keys that carry a Group.
Refusals (stable Code, shared by CLI and API):
| Code | Case | |
|---|---|---|
skill_not_found, ambiguous_name | resolved like resolveUninstallTarget (CLI) / resolveUninstallSkill (server): flat name, relPath, or unique basename; a name matching no skill falls back to a folder relPath, and a folder with no skill below it is skill_not_found | |
dest_exists | <dest>/<base> exists (skill or folder). Never overwritten or merged, --force does not change it | |
inside_tracked_repo | skill is under a _-prefixed git checkout (install.IsTrackedCheckout); update pulls into it. Same rule as uninstall.checkMoveOut (ErrInsideRepo). A folder that is or contains a tracked checkout is refused as a whole | |
dest_inside_tracked_repo | any ancestor of dest is a tracked checkout | |
linked_folder | skill is, or sits below, a followed source link (sourcewalk.Follow.Resolve), or dest is below one. The folder is the user's checkout. A folder that contains a link is refused as a whole | |
dest_is_skill | dest or an ancestor holds SKILL.md (guard; discovery behaviour for nested skills unverified) | |
invalid_dest | not validate.IntoPath; . means the source root. Segments starting with _ fail validate.SkillName, so tracked-repo names cannot be typed | |
dest_inside_source_folder | moving a folder into itself or one of its descendants | |
duplicate_dest | two names in one batch resolve to the same destination path (frontend/foo and backend/foo into archive). Both are refused before any rename; --force does not change it | |
overlapping_sources | one requested root equals or sits below another (move foo foo/sub archive). Refused before any rename; the parent already carries the child | |
same_folder | already there; no-op, exit 0 | |
name_collision | forceable, below |
Empty folders and non-skill files inside a moved folder are not refusal reasons; they move along with it.
Extract the tracked-repo ancestor loop from uninstall.checkMoveOut into an exported helper in internal/install (next to IsTrackedCheckout) so both packages share it.
Name collisions: target names are FlatName (grp__demo) or SKILL.md names, depending on target_naming. For each target run sync.ResolveTargetSkillsForTarget on the discovered list with the moved skills rewritten, and report Collisions that include a moved path. Identical destination paths are duplicate_dest, checked first, and never reach this step. The flat branch of that function returns before collision detection (verified, internal/sync/target_naming.go), so also compare FlatName duplicates directly. A collision does not lose data (sync skips both entries), so --force accepts it. That is the only thing --force does.
Target filters are not rewritten. include/exclude match flat names (internal/sync/filter.go), so a rule naming demo stops matching grp__demo. Preflight evaluates each target's effective filter (ShouldSyncFlatName) for the old and the new flat name and warns whenever the result differs in either direction (a rule naming demo stops matching; an exclude: archive__* newly drops it; an include newly adds it); sync already warns on unmatched includes (UnmatchedInclude.Warning).
CLI: skillshare move <skill|folder>... <dest-folder>
rg '"mv"|"move"|"rename"' cmd/ finds only TUI key hints ("↑↓ move"), no command. move is free in the commands map in cmd/skillshare/main.go. The existing verb pair link/unlink, enable/disable has no conflict.
- Last positional is the destination, relative to the skills source;
.moves to the root. At least two positionals. A folder argument moves under the destination (frontend archive→archive/frontend); a trailing/is accepted. - Flags:
--dry-run/-n,--force/-f(collision only),--json,-p/-gviaparseModeArgs,--help. No--kind: agents are out of scope (see below). - Handler split:
move.go(parse, mode),move_handlers.go(resolve, render), project differences only whereconfig.LoadProjectpaths differ, asuninstall_project.go. - Output with
ui.Row(ui.MarkOK, name, "→ grp/demo"),ui.Done, thenui.Next("skillshare sync", "rename the links in your targets").--dry-runusesui.DryRun()and prints the same rows. --json:{"moved":[{"name","from","to","record":bool,"skills":n}],"failed":[{"name","code","error"}],"skipped":0,"warnings":[],"dry_run":false,"duration":"..."}, shaped likeuninstallJSONOutput;skillsis 1 for a skill and the count below a folder. Non-zero exit whenfailedis non-empty.- oplog:
oplog.NewEntry("move", status, d), argsnames,dest,force,mode; written withoplog.Write(cfgPath, oplog.OpsFile, e)asenable.godoes. No command allow-list was found incmd/skillshare/log*.go(unverified for the UI log filters). - Completions: all five
completion_{bash,zsh,fish,powershell,nushell}.goplus a literal intests/integration/completion_test.go.
Sync is not automatic. install, uninstall, enable, disable and link all print ui.Next("skillshare sync") (cmd/skillshare/install_handlers.go, enable.go, link.go); sync touches every target and can refuse (symlink conflicts need --force, a different decision from move's). Until sync runs, the old target link is dangling, as after uninstall; the dashboard result view offers Sync now.
Web API
POST /api/resources/batch/move, registered next to batch/targets and batch/toggle in internal/server/server.go; handler in new internal/server/handler_move.go (split follows handler_toggle.go, handler_skills_batch.go).
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingproposals/510-move-installed-skill.md
Popularity
- Repository stars
- 2,768
- Repository forks
- 175
- 7-day trend score
- 0
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/runkids/skillshare/proposals)