refactor(skills): collapse skill directory classification into one skill-directory-state module - #316
Conversation
…sync Promote auto-sync's private readManagedSkillTargetState into a public skill-directory-state module that owns the full classification: stat, metadata read/parse, and the symlink staleness check. Callers match on a discriminated union (missing | not-directory | unmanaged | managed) instead of composing directoryExists with kind-filtered metadata readers.
…ry-state Replace the nine inline directoryExists+reader classifications and the three divergent registry currency loops with readSkillDirectoryState and isCurrentRegistryPublication. Delete the now-unused read halves: readManagedSkillMetadata, readInstalledBundledSkillMetadata, isManagedBundledSkillInstallation, readSkillMetadataFileState, readLocalSkillMetadata, isForeignManagedMetadataState, the managed-skill-publication module (absorbed as publicationCurrent), and the listings/info-inventory/legacy-cleanup inline readers. Fold the deletion-test-failing helpers: inline the two bundled-skill conflict predicates at their call sites, delete bundled-skill-model.ts (bundledSkillDevelopmentVersion moves to embedded-assets.ts), delete the package-templates.ts re-export in favor of direct text imports, and drop the writeSkillOperationJson pass-through in favor of writeJsonOutput.
Add isSkillDirectoryAbsent for the recurring missing-or-not-directory grouping, adopt managedMetadataOfKind consistently at the remaining kind-check sites, drop the skillOperationOutputOptions constant alias in favor of jsonOutputOptions, reuse the shared temporary-directory test helper, and restore the registry metadata render assertion in skill-metadata.test.ts.
Summary by CodeRabbit
WalkthroughThe PR introduces Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/application/commands/skills/managed-skill-uninstall.ts (1)
469-503: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winParallelize independent directory-state reads.
Each
readSkillDirectoryStatecall here is independent per host installation, but the loop awaits them sequentially. As per coding guidelines, "UsePromise.all()for independent async operations instead of sequentialawait." This also matches howisEverythingCurrentincheck-update.tsand the sync loops inauto-sync.tsalready parallelize equivalent reads.♻️ Proposed refactor
async function resolveRegistrySkillUninstallTargets( hostInstallations: readonly ManagedSkillHostInstallation[], ): Promise<{ targets: RegistrySkillUninstallTarget[]; unmanagedInstallations: ManagedSkillHostInstallation[]; }> { + const targetStates = await Promise.all( + hostInstallations.map(installation => + readSkillDirectoryState(installation.installedSkillDirectoryPath), + ), + ); + const targets: RegistrySkillUninstallTarget[] = []; const unmanagedInstallations: ManagedSkillHostInstallation[] = []; - for (const installation of hostInstallations) { - const targetState = await readSkillDirectoryState( - installation.installedSkillDirectoryPath, - ); - + hostInstallations.forEach((installation, index) => { + const targetState = targetStates[index]!; + if (isSkillDirectoryAbsent(targetState)) { - continue; + return; } if ( targetState.kind === "managed" && targetState.metadata.kind === "registry" ) { targets.push({ ...installation, packageName: targetState.metadata.packageName, previousVersion: targetState.metadata.version, }); - continue; + return; } unmanagedInstallations.push(installation); - } + }); return { targets, unmanagedInstallations }; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/application/commands/skills/managed-skill-uninstall.ts` around lines 469 - 503, Update resolveRegistrySkillUninstallTargets to read all installation directory states concurrently with Promise.all, then process the resulting installation/state pairs to populate targets and unmanagedInstallations while preserving the existing absent, managed-registry, and unmanaged behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/application/commands/skills/managed-skill-uninstall.ts`:
- Around line 469-503: Update resolveRegistrySkillUninstallTargets to read all
installation directory states concurrently with Promise.all, then process the
resulting installation/state pairs to populate targets and
unmanagedInstallations while preserving the existing absent, managed-registry,
and unmanaged behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ce32368f-94d8-4d6a-9b3c-ad36b1aca8bd
📒 Files selected for processing (38)
src/application/commands/skills/adopt.tssrc/application/commands/skills/auto-sync.tssrc/application/commands/skills/bundled-skill-model.test.tssrc/application/commands/skills/bundled-skill-model.tssrc/application/commands/skills/bundled-skill-observation.test.tssrc/application/commands/skills/bundled-skill-observation.tssrc/application/commands/skills/check-update.tssrc/application/commands/skills/embedded-assets.tssrc/application/commands/skills/index.cli.test.tssrc/application/commands/skills/index.test.tssrc/application/commands/skills/info-inventory.tssrc/application/commands/skills/install.tssrc/application/commands/skills/legacy-codex-cleanup.tssrc/application/commands/skills/legacy-gpt-image-2-cleanup.tssrc/application/commands/skills/local-skill-ownership.tssrc/application/commands/skills/local-skill-source.tssrc/application/commands/skills/managed-skill-listings.tssrc/application/commands/skills/managed-skill-metadata.test.tssrc/application/commands/skills/managed-skill-metadata.tssrc/application/commands/skills/managed-skill-publication.test.tssrc/application/commands/skills/managed-skill-publication.tssrc/application/commands/skills/managed-skill-uninstall.tssrc/application/commands/skills/operation-result.tssrc/application/commands/skills/package-conversion.test.tssrc/application/commands/skills/package-conversion.tssrc/application/commands/skills/package-templates.tssrc/application/commands/skills/publish.tssrc/application/commands/skills/registry-skill-install.tssrc/application/commands/skills/registry-skill-publication.tssrc/application/commands/skills/repair.tssrc/application/commands/skills/share.tssrc/application/commands/skills/shared.tssrc/application/commands/skills/skill-directory-state.test.tssrc/application/commands/skills/skill-directory-state.tssrc/application/commands/skills/skill-metadata.test.tssrc/application/commands/skills/sync.tssrc/application/commands/skills/uninstall.tssrc/application/commands/skills/update.ts
💤 Files with no reviewable changes (9)
- src/application/commands/skills/managed-skill-metadata.test.ts
- src/application/commands/skills/managed-skill-publication.test.ts
- src/application/commands/skills/managed-skill-publication.ts
- src/application/commands/skills/package-templates.ts
- src/application/commands/skills/bundled-skill-model.ts
- src/application/commands/skills/bundled-skill-model.test.ts
- src/application/commands/skills/operation-result.ts
- src/application/commands/skills/local-skill-ownership.ts
- src/application/commands/skills/managed-skill-metadata.ts
Resolves the predicted overlap with #316 (skill-directory-state): per-host probes in check-update/update/uninstall/auto-sync keep main's readSkillDirectoryState / isCurrentRegistryPublication implementations, the data-source layer keeps this branch's readInstalledSkills projection, and the private directory-scan copies deleted on both sides stay deleted (auto-sync and legacy-gpt-image-2-cleanup now import the shared readSkillsDirectoryEntries). info-inventory keeps this branch's row projection; main's readHostScan semantics (not-directory and IO errors read as unparseable) are already covered by the inventory module's copy states.
Summary
Architecture deepening (candidate 06 of the codebase review, following the identity-resolver series #310–#312): collapse the question "what state is this skill directory in" into one deep module.
Before this change the classification — stat the directory, read
.oo-metadata.json, decide missing vs unmanaged vs managed — was re-derived inline at nine call sites through five divergent kind-filtered readers of the same file, while the correct implementation already existed privately asreadManagedSkillTargetStateinauto-sync.ts. One copy had drifted (update.tsrunGroupUpdateJsonskippeddirectoryExistsentirely), and the registry "is it current" rule existed in three slightly different loops.The new seam
src/application/commands/skills/skill-directory-state.ts:Internal (hidden from callers): the stat/readFile/parse sequence, the symlink staleness check (
publicationCurrent), the legacy untyped metadata fallback, and the error policy — IO errors other than not-found throw through, so commands keep their existing failure handling. Aggregation semantics (e.g. vacuous truth of "current in all hosts" over zero installations, and the JSON update loop'slength > 0guard) deliberately stay with each caller.What collapsed into it
uninstall(registry + bundled JSON loops),managed-skill-uninstall(bundled text path + a fused registry two-pass),update(text + JSON),check-update,repair,registry-skill-publication,install,share, plusregistry-skill-install,managed-skill-listings,info-inventory,local-skill-source,publish,adopt, and both legacy cleanups.isCurrentRegistryPublicationpredicate; the driftedrunGroupUpdateJsoncopy now stats first like everyone else.readManagedSkillMetadata,readInstalledBundledSkillMetadata/isManagedBundledSkillInstallation,readSkillMetadataFileState/readLocalSkillMetadata/isForeignManagedMetadataState, the listings/info-inventory/legacy-cleanup inline reads, andmanaged-skill-publication.ts(absorbed aspublicationCurrent). All write halves survive unchanged.resolveBundledSkillInstallConflictandcanUninstallManagedBundledSkillInstallationinlined at their call sites (each call hardcoded three of four inputs);bundled-skill-model.tsdeleted (bundledSkillDevelopmentVersionmoved toembedded-assets.ts);package-templates.tsdeleted in favor of directwith { type: "text" }imports;writeSkillOperationJsonand theskillOperationOutputOptionsalias deleted in favor ofwriteJsonOutput/jsonOutputOptions.The comment-enforced "remove local skill first" ordering in
uninstall.tsis intentionally preserved (the registry classifier still treats managed-local directories as unmanaged); making the registry pass local-aware is now a one-line follow-up if ever wanted.Behavior
No user-facing CLI contract change;
docs/commands*.mdand the telemetry manifest are untouched. One narrow edge class is deliberately unified: a regular file occupying a skill directory path (or a parent segment) is now classified (not-directory/missing) where a few old sites threw a rawENOTDIR/EISDIRerrno out of the command (e.g. it aborted the wholeoo skills list). Remaining differences are TOCTOU-window narrowings and internal booleans that were provably unobservable.This was verified adversarially: five independent reviewers derived old-vs-new outcomes per filesystem state (missing, broken symlink, file-at-path, no metadata, corrupt JSON, local/bundled/registry/legacy-untyped metadata, symlinked install, EACCES) for every migrated site — zero behavior-change bugs found; all 26 findings fall in the documented edge classes above.
Tests
skill-directory-state.test.ts(26 cases): all state classifications including legacy untyped metadata, broken/managed symlinks, parent-segment files, a deterministic non-ENOENT throw (EISDIR), and the currency predicate truth table.skill-metadata.test.ts; the conflict-predicate truth table replaced by theshared.tserror-path coverage that already exercises name/storage conflicts.bun run lint:fix/ts-check/knipclean;bun run test: 1674 pass, 0 fail.Net: −135 lines, 4 files deleted, one module added.