Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,14 @@ export async function runAdd(args: string[], options: AddOptions = {}): Promise<
const token = getGitHubToken();
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, token);
if (hash) skillFolderHash = hash;
} else if (parsed.type === 'local' && parsed.localPath) {
// For local skills, compute a hash from the source directory
// so `skills check/update` can detect changes later
try {
skillFolderHash = await computeSkillFolderHash(parsed.localPath);
} catch {
// Non-fatal: skill works without hash, just can't detect updates
}
}

await addSkillToLock(skill.name, {
Expand Down
66 changes: 61 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { removeCommand, parseRemoveOptions } from './remove.ts';
import { runSync, parseSyncOptions } from './sync.ts';
import { track } from './telemetry.ts';
import { fetchSkillFolderHash, getGitHubToken } from './skill-lock.ts';
import { computeSkillFolderHash } from './local-lock.ts';

const __dirname = dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -367,8 +368,12 @@ interface SkippedSkill {
* Determine why a skill cannot be checked for updates automatically.
*/
function getSkipReason(entry: SkillLockEntry): string {
if (entry.sourceType === 'local' && entry.skillFolderHash) {
// Local skills with a stored hash can be checked via local hash comparison
return '';
}
if (entry.sourceType === 'local') {
return 'Local path';
return 'Local path (no hash recorded — reinstall to enable tracking)';
}
if (entry.sourceType === 'git') {
return 'Git URL (hash tracking not supported)';
Expand Down Expand Up @@ -414,15 +419,25 @@ async function runCheck(args: string[] = []): Promise<void> {

// Group skills by source (owner/repo) to batch GitHub API calls
const skillsBySource = new Map<string, Array<{ name: string; entry: SkillLockEntry }>>();
const localSkills: Array<{ name: string; entry: SkillLockEntry }> = [];
const skipped: SkippedSkill[] = [];

for (const skillName of skillNames) {
const entry = lock.skills[skillName];
if (!entry) continue;

// Only check skills with folder hash and skill path
// Local skills with a stored hash can be checked via local hash comparison
if (entry.sourceType === 'local' && entry.skillFolderHash) {
localSkills.push({ name: skillName, entry });
continue;
}

// Only check GitHub skills with folder hash and skill path
if (!entry.skillFolderHash || !entry.skillPath) {
skipped.push({ name: skillName, reason: getSkipReason(entry), sourceUrl: entry.sourceUrl });
const reason = getSkipReason(entry);
if (reason) {
skipped.push({ name: skillName, reason, sourceUrl: entry.sourceUrl });
}
continue;
}

Expand Down Expand Up @@ -467,6 +482,28 @@ async function runCheck(args: string[] = []): Promise<void> {
}
}

// Check local skills by computing fresh hash from source path
for (const { name, entry } of localSkills) {
try {
const sourcePath = entry.sourceUrl;
if (!existsSync(sourcePath)) {
errors.push({ name, source: sourcePath, error: 'Source path no longer exists' });
continue;
}

const currentHash = await computeSkillFolderHash(sourcePath);
if (currentHash !== entry.skillFolderHash) {
updates.push({ name, source: sourcePath });
}
} catch (err) {
errors.push({
name,
source: entry.sourceUrl,
error: err instanceof Error ? err.message : 'Unknown error',
});
}
}

console.log();

if (updates.length === 0) {
Expand Down Expand Up @@ -525,9 +562,28 @@ async function runUpdate(): Promise<void> {
const entry = lock.skills[skillName];
if (!entry) continue;

// Only check skills with folder hash and skill path
// Local skills with a stored hash: compare via local hash
if (entry.sourceType === 'local' && entry.skillFolderHash) {
try {
const sourcePath = entry.sourceUrl;
if (existsSync(sourcePath)) {
const currentHash = await computeSkillFolderHash(sourcePath);
if (currentHash !== entry.skillFolderHash) {
updates.push({ name: skillName, source: sourcePath, entry });
}
}
} catch {
// Skip local skills that fail to check
}
continue;
}

// Only check GitHub skills with folder hash and skill path
if (!entry.skillFolderHash || !entry.skillPath) {
skipped.push({ name: skillName, reason: getSkipReason(entry), sourceUrl: entry.sourceUrl });
const reason = getSkipReason(entry);
if (reason) {
skipped.push({ name: skillName, reason, sourceUrl: entry.sourceUrl });
}
continue;
}

Expand Down