11import { readFileSync , readdirSync , existsSync , mkdirSync , rmSync , cpSync , statSync } from 'fs' ;
2- import { join , basename , resolve , sep } from 'path' ;
2+ import { join , basename , resolve , sep , relative } from 'path' ;
33import { homedir } from 'os' ;
44import { execSync } from 'child_process' ;
55import chalk from 'chalk' ;
@@ -8,7 +8,16 @@ import { checkbox, confirm } from '@inquirer/prompts';
88import { ExitPromptError } from '@inquirer/core' ;
99import { hasValidFrontmatter , extractYamlField } from '../utils/yaml.js' ;
1010import { ANTHROPIC_MARKETPLACE_SKILLS } from '../utils/marketplace-skills.js' ;
11+ import { writeSkillMetadata } from '../utils/skill-metadata.js' ;
1112import type { InstallOptions } from '../types.js' ;
13+ import type { SkillSourceMetadata , SkillSourceType } from '../utils/skill-metadata.js' ;
14+
15+ interface InstallSourceInfo {
16+ source : string ;
17+ sourceType : SkillSourceType ;
18+ repoUrl ?: string ;
19+ localRoot ?: string ;
20+ }
1221
1322/**
1423 * Check if source is a local path
@@ -101,7 +110,12 @@ export async function installSkill(source: string, options: InstallOptions): Pro
101110 // Handle local path installation
102111 if ( isLocalPath ( source ) ) {
103112 const localPath = expandPath ( source ) ;
104- await installFromLocal ( localPath , targetDir , options ) ;
113+ const sourceInfo : InstallSourceInfo = {
114+ source,
115+ sourceType : 'local' ,
116+ localRoot : localPath ,
117+ } ;
118+ await installFromLocal ( localPath , targetDir , options , sourceInfo ) ;
105119 printPostInstallHints ( isProject ) ;
106120 return ;
107121 }
@@ -131,6 +145,11 @@ export async function installSkill(source: string, options: InstallOptions): Pro
131145 // Clone and install from git
132146 const tempDir = join ( homedir ( ) , `.openskills-temp-${ Date . now ( ) } ` ) ;
133147 mkdirSync ( tempDir , { recursive : true } ) ;
148+ const sourceInfo : InstallSourceInfo = {
149+ source,
150+ sourceType : 'git' ,
151+ repoUrl,
152+ } ;
134153
135154 try {
136155 const spinner = ora ( 'Cloning repository...' ) . start ( ) ;
@@ -152,10 +171,10 @@ export async function installSkill(source: string, options: InstallOptions): Pro
152171 const repoDir = join ( tempDir , 'repo' ) ;
153172
154173 if ( skillSubpath ) {
155- await installSpecificSkill ( repoDir , skillSubpath , targetDir , isProject , options ) ;
174+ await installSpecificSkill ( repoDir , skillSubpath , targetDir , isProject , options , sourceInfo ) ;
156175 } else {
157176 const repoName = getRepoName ( repoUrl ) ;
158- await installFromRepo ( repoDir , targetDir , options , repoName || undefined ) ;
177+ await installFromRepo ( repoDir , targetDir , options , repoName || undefined , sourceInfo ) ;
159178 }
160179 } finally {
161180 rmSync ( tempDir , { recursive : true , force : true } ) ;
@@ -177,7 +196,12 @@ function printPostInstallHints(isProject: boolean): void {
177196/**
178197 * Install from local path (directory containing skills or single skill)
179198 */
180- async function installFromLocal ( localPath : string , targetDir : string , options : InstallOptions ) : Promise < void > {
199+ async function installFromLocal (
200+ localPath : string ,
201+ targetDir : string ,
202+ options : InstallOptions ,
203+ sourceInfo : InstallSourceInfo
204+ ) : Promise < void > {
181205 if ( ! existsSync ( localPath ) ) {
182206 console . error ( chalk . red ( `Error: Path does not exist: ${ localPath } ` ) ) ;
183207 process . exit ( 1 ) ;
@@ -194,10 +218,10 @@ async function installFromLocal(localPath: string, targetDir: string, options: I
194218 if ( existsSync ( skillMdPath ) ) {
195219 // Single skill directory
196220 const isProject = targetDir . includes ( process . cwd ( ) ) ;
197- await installSingleLocalSkill ( localPath , targetDir , isProject , options ) ;
221+ await installSingleLocalSkill ( localPath , targetDir , isProject , options , sourceInfo ) ;
198222 } else {
199223 // Directory containing multiple skills
200- await installFromRepo ( localPath , targetDir , options ) ;
224+ await installFromRepo ( localPath , targetDir , options , undefined , sourceInfo ) ;
201225 }
202226}
203227
@@ -208,7 +232,8 @@ async function installSingleLocalSkill(
208232 skillDir : string ,
209233 targetDir : string ,
210234 isProject : boolean ,
211- options : InstallOptions
235+ options : InstallOptions ,
236+ sourceInfo : InstallSourceInfo
212237) : Promise < void > {
213238 const skillMdPath = join ( skillDir , 'SKILL.md' ) ;
214239 const content = readFileSync ( skillMdPath , 'utf-8' ) ;
@@ -235,6 +260,7 @@ async function installSingleLocalSkill(
235260 }
236261
237262 cpSync ( skillDir , targetPath , { recursive : true , dereference : true } ) ;
263+ writeSkillMetadata ( targetPath , buildLocalMetadata ( sourceInfo , skillDir ) ) ;
238264
239265 console . log ( chalk . green ( `✅ Installed: ${ skillName } ` ) ) ;
240266 console . log ( ` Location: ${ targetPath } ` ) ;
@@ -248,7 +274,8 @@ async function installSpecificSkill(
248274 skillSubpath : string ,
249275 targetDir : string ,
250276 isProject : boolean ,
251- options : InstallOptions
277+ options : InstallOptions ,
278+ sourceInfo : InstallSourceInfo
252279) : Promise < void > {
253280 const skillDir = join ( repoDir , skillSubpath ) ;
254281 const skillMdPath = join ( skillDir , 'SKILL.md' ) ;
@@ -282,6 +309,7 @@ async function installSpecificSkill(
282309 process . exit ( 1 ) ;
283310 }
284311 cpSync ( skillDir , targetPath , { recursive : true , dereference : true } ) ;
312+ writeSkillMetadata ( targetPath , buildGitMetadata ( sourceInfo , skillSubpath ) ) ;
285313
286314 console . log ( chalk . green ( `✅ Installed: ${ skillName } ` ) ) ;
287315 console . log ( ` Location: ${ targetPath } ` ) ;
@@ -294,7 +322,8 @@ async function installFromRepo(
294322 repoDir : string ,
295323 targetDir : string ,
296324 options : InstallOptions ,
297- repoName ?: string
325+ repoName : string | undefined ,
326+ sourceInfo : InstallSourceInfo
298327) : Promise < void > {
299328 const rootSkillPath = join ( repoDir , 'SKILL.md' ) ;
300329 let skillInfos : Array < {
@@ -444,6 +473,7 @@ async function installFromRepo(
444473 continue ;
445474 }
446475 cpSync ( info . skillDir , info . targetPath , { recursive : true , dereference : true } ) ;
476+ writeSkillMetadata ( info . targetPath , buildMetadataFromSource ( sourceInfo , info . skillDir , repoDir ) ) ;
447477
448478 console . log ( chalk . green ( `✅ Installed: ${ info . skillName } ` ) ) ;
449479 installedCount ++ ;
@@ -452,6 +482,38 @@ async function installFromRepo(
452482 console . log ( chalk . green ( `\n✅ Installation complete: ${ installedCount } skill(s) installed` ) ) ;
453483}
454484
485+ function buildMetadataFromSource (
486+ sourceInfo : InstallSourceInfo ,
487+ skillDir : string ,
488+ repoDir : string
489+ ) : SkillSourceMetadata {
490+ if ( sourceInfo . sourceType === 'local' ) {
491+ return buildLocalMetadata ( sourceInfo , skillDir ) ;
492+ }
493+ const subpath = relative ( repoDir , skillDir ) ;
494+ const normalizedSubpath = subpath === '' ? '' : subpath ;
495+ return buildGitMetadata ( sourceInfo , normalizedSubpath ) ;
496+ }
497+
498+ function buildGitMetadata ( sourceInfo : InstallSourceInfo , subpath : string ) : SkillSourceMetadata {
499+ return {
500+ source : sourceInfo . source ,
501+ sourceType : 'git' ,
502+ repoUrl : sourceInfo . repoUrl ,
503+ subpath,
504+ installedAt : new Date ( ) . toISOString ( ) ,
505+ } ;
506+ }
507+
508+ function buildLocalMetadata ( sourceInfo : InstallSourceInfo , skillDir : string ) : SkillSourceMetadata {
509+ return {
510+ source : sourceInfo . source ,
511+ sourceType : 'local' ,
512+ localPath : skillDir ,
513+ installedAt : new Date ( ) . toISOString ( ) ,
514+ } ;
515+ }
516+
455517/**
456518 * Warn if installing could conflict with Claude Code marketplace
457519 * Returns true if should proceed, false if should skip
0 commit comments