@slats/claude-assets-sync 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ var cli = require('./core/cli.cjs');
4
+
5
+ cli.run().catch((error) => {
6
+ console.error('Fatal error:', error.message);
7
+ process.exit(1);
8
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './core/cli.mjs';
3
+
4
+ run().catch((error) => {
5
+ console.error('Fatal error:', error.message);
6
+ process.exit(1);
7
+ });
@@ -6,7 +6,7 @@ var readline = require('node:readline/promises');
6
6
  var pc = require('picocolors');
7
7
  var syncMeta = require('../core/syncMeta.cjs');
8
8
  var logger = require('../utils/logger.cjs');
9
- var nameTransform = require('../utils/nameTransform.cjs');
9
+ var packageName = require('../utils/packageName.cjs');
10
10
 
11
11
  function _interopNamespaceDefault(e) {
12
12
  var n = Object.create(null);
@@ -30,17 +30,17 @@ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
30
30
  var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
31
31
 
32
32
  const runRemoveCommand = async (options, cwd = process.cwd()) => {
33
- const { package: packageName, yes, dryRun } = options;
34
- const prefix = nameTransform.packageNameToPrefix(packageName);
33
+ const { package: packageName$1, yes, dryRun } = options;
34
+ const prefix = packageName.packageNameToPrefix(packageName$1);
35
35
  const meta = syncMeta.readUnifiedSyncMeta(cwd);
36
36
  if (!meta || !meta.packages || !meta.packages[prefix]) {
37
- logger.logger.error(`Package ${packageName} is not synced.`);
37
+ logger.logger.error(`Package ${packageName$1} is not synced.`);
38
38
  process.exit(1);
39
39
  return;
40
40
  }
41
41
  const packageInfo = meta.packages[prefix];
42
42
  if (!packageInfo || !packageInfo.files) {
43
- logger.logger.error(`Package ${packageName} has no files to remove.`);
43
+ logger.logger.error(`Package ${packageName$1} has no files to remove.`);
44
44
  process.exit(1);
45
45
  return;
46
46
  }
@@ -57,7 +57,7 @@ const runRemoveCommand = async (options, cwd = process.cwd()) => {
57
57
  }
58
58
  }
59
59
  else {
60
- const dirPath = path__namespace.join(cwd, '.claude', assetType, packageName);
60
+ const dirPath = path__namespace.join(cwd, '.claude', assetType, packageName$1);
61
61
  filesToRemove.push({ assetType, path: dirPath });
62
62
  }
63
63
  }
@@ -103,7 +103,7 @@ const runRemoveCommand = async (options, cwd = process.cwd()) => {
103
103
  delete meta.packages[prefix];
104
104
  meta.syncedAt = new Date().toISOString();
105
105
  syncMeta.writeUnifiedSyncMeta(cwd, meta);
106
- logger.logger.success(`\nRemoved package ${packageName}`);
106
+ logger.logger.success(`\nRemoved package ${packageName$1}`);
107
107
  };
108
108
 
109
109
  exports.runRemoveCommand = runRemoveCommand;
@@ -4,7 +4,7 @@ import * as readline from 'node:readline/promises';
4
4
  import pc from 'picocolors';
5
5
  import { readUnifiedSyncMeta, writeUnifiedSyncMeta } from '../core/syncMeta.mjs';
6
6
  import { logger } from '../utils/logger.mjs';
7
- import { packageNameToPrefix } from '../utils/nameTransform.mjs';
7
+ import { packageNameToPrefix } from '../utils/packageName.mjs';
8
8
 
9
9
  const runRemoveCommand = async (options, cwd = process.cwd()) => {
10
10
  const { package: packageName, yes, dryRun } = options;
@@ -1,4 +1,4 @@
1
- import type { AssetStructure, AssetsConfig } from '../utils/types';
1
+ import type { AssetStructure, AssetsConfig } from '../utils/types.js';
2
2
  import { DEFAULT_ASSET_TYPES } from './constants';
3
3
  export { DEFAULT_ASSET_TYPES };
4
4
  /**
@@ -22,7 +22,7 @@ export declare const META_FILES: {
22
22
  * Schema versions for metadata files
23
23
  */
24
24
  export declare const SCHEMA_VERSIONS: {
25
- readonly UNIFIED_SYNC_META: "0.0.2";
25
+ readonly UNIFIED_SYNC_META: "0.0.4";
26
26
  readonly LEGACY_SYNC_META: "1.0.0";
27
27
  };
28
28
  /**
@@ -58,24 +58,36 @@ const cleanFlatAssetFiles = (cwd, assetType, prefix, existingMeta) => {
58
58
  const packageInfo = existingMeta.packages[prefix];
59
59
  const filesToRemove = packageInfo.files[assetType];
60
60
  if (Array.isArray(filesToRemove)) {
61
+ const skillDirs = new Set();
61
62
  for (const fileMapping of filesToRemove) {
62
63
  const fileName = typeof fileMapping === 'string'
63
64
  ? fileMapping
64
65
  : fileMapping.transformed;
65
- const filePath = path.join(destDir, fileName);
66
- if (io.fileExists(filePath)) {
67
- fs.rmSync(filePath, { force: true });
66
+ if (fileName.includes('/')) {
67
+ skillDirs.add(fileName.split('/')[0]);
68
+ }
69
+ else {
70
+ const filePath = path.join(destDir, fileName);
71
+ if (io.fileExists(filePath)) {
72
+ fs.rmSync(filePath, { force: true });
73
+ }
74
+ }
75
+ }
76
+ for (const dir of skillDirs) {
77
+ const dirPath = path.join(destDir, dir);
78
+ if (io.fileExists(dirPath)) {
79
+ fs.rmSync(dirPath, { recursive: true, force: true });
68
80
  }
69
81
  }
70
82
  }
71
83
  }
72
84
  else {
73
85
  const pattern = `${prefix}_`;
74
- const files = io.listDirectory(destDir);
75
- for (const file of files) {
76
- if (file.startsWith(pattern) && file.endsWith('.md')) {
77
- const filePath = path.join(destDir, file);
78
- fs.rmSync(filePath, { force: true });
86
+ const entries = io.listDirectory(destDir);
87
+ for (const entry of entries) {
88
+ if (entry.startsWith(pattern)) {
89
+ const entryPath = path.join(destDir, entry);
90
+ fs.rmSync(entryPath, { recursive: true, force: true });
79
91
  }
80
92
  }
81
93
  }
@@ -1,4 +1,4 @@
1
- import type { AssetType, SyncMeta, UnifiedSyncMeta } from '../utils/types';
1
+ import type { AssetType, SyncMeta, UnifiedSyncMeta } from '../utils/types.js';
2
2
  import { ensureDirectory, writeTextFile } from './io';
3
3
  /**
4
4
  * Ensure directory exists (creates recursively if needed)
@@ -71,7 +71,8 @@ export declare const createSyncMeta: (version: string, files: string[]) => SyncM
71
71
  export declare const writeFlatAssetFile: (cwd: string, assetType: AssetType, flatFileName: string, content: string) => void;
72
72
  /**
73
73
  * Clean flat asset files with specific prefix
74
- * Removes only files belonging to the specified package, preserving others
74
+ * Removes only files belonging to the specified package, preserving others.
75
+ * Handles both single flat files (prefix_file.md) and directory-based skills (prefix_dir/).
75
76
  * @param cwd - Current working directory
76
77
  * @param assetType - Asset type (commands, skills, agents, or any custom string)
77
78
  * @param prefix - Package prefix (e.g., "canard-schemaForm")
@@ -56,24 +56,36 @@ const cleanFlatAssetFiles = (cwd, assetType, prefix, existingMeta) => {
56
56
  const packageInfo = existingMeta.packages[prefix];
57
57
  const filesToRemove = packageInfo.files[assetType];
58
58
  if (Array.isArray(filesToRemove)) {
59
+ const skillDirs = new Set();
59
60
  for (const fileMapping of filesToRemove) {
60
61
  const fileName = typeof fileMapping === 'string'
61
62
  ? fileMapping
62
63
  : fileMapping.transformed;
63
- const filePath = join(destDir, fileName);
64
- if (fileExists(filePath)) {
65
- rmSync(filePath, { force: true });
64
+ if (fileName.includes('/')) {
65
+ skillDirs.add(fileName.split('/')[0]);
66
+ }
67
+ else {
68
+ const filePath = join(destDir, fileName);
69
+ if (fileExists(filePath)) {
70
+ rmSync(filePath, { force: true });
71
+ }
72
+ }
73
+ }
74
+ for (const dir of skillDirs) {
75
+ const dirPath = join(destDir, dir);
76
+ if (fileExists(dirPath)) {
77
+ rmSync(dirPath, { recursive: true, force: true });
66
78
  }
67
79
  }
68
80
  }
69
81
  }
70
82
  else {
71
83
  const pattern = `${prefix}_`;
72
- const files = listDirectory(destDir);
73
- for (const file of files) {
74
- if (file.startsWith(pattern) && file.endsWith('.md')) {
75
- const filePath = join(destDir, file);
76
- rmSync(filePath, { force: true });
84
+ const entries = listDirectory(destDir);
85
+ for (const entry of entries) {
86
+ if (entry.startsWith(pattern)) {
87
+ const entryPath = join(destDir, entry);
88
+ rmSync(entryPath, { recursive: true, force: true });
77
89
  }
78
90
  }
79
91
  }
@@ -37,17 +37,44 @@ const fetchDirectoryContents = async (repoInfo, path, tag) => {
37
37
  if (!response.ok)
38
38
  throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
39
39
  const data = await response.json();
40
- return data.filter((entry) => entry.type === 'file' && entry.name.endsWith('.md'));
40
+ return data.filter((entry) => (entry.type === 'file' && entry.name.endsWith('.md')) ||
41
+ entry.type === 'dir');
42
+ };
43
+ const expandDirectoryEntries = async (repoInfo, parentPath, entries, tag, prefix = '') => {
44
+ const result = [];
45
+ for (const entry of entries) {
46
+ const entryPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
47
+ if (entry.type === 'file') {
48
+ result.push({
49
+ ...entry,
50
+ name: prefix ? entryPrefix : entry.name,
51
+ });
52
+ }
53
+ else if (entry.type === 'dir') {
54
+ const subEntries = await fetchDirectoryContents(repoInfo, `${parentPath}/${entry.name}`, tag);
55
+ if (subEntries) {
56
+ const expanded = await expandDirectoryEntries(repoInfo, `${parentPath}/${entry.name}`, subEntries, tag, entryPrefix);
57
+ result.push(...expanded);
58
+ }
59
+ }
60
+ }
61
+ return result;
41
62
  };
42
63
  const fetchAssetFiles = async (repoInfo, assetPath, tag, assetTypes) => {
43
64
  const basePath = repoInfo.directory
44
65
  ? `${repoInfo.directory}/${assetPath}`
45
66
  : assetPath;
46
67
  const fetchPromises = assetTypes.map((assetType) => fetchDirectoryContents(repoInfo, `${basePath}/${assetType}`, tag));
47
- const results = await Promise.all(fetchPromises);
68
+ const rawResults = await Promise.all(fetchPromises);
69
+ const expandedResults = await Promise.all(rawResults.map((entries, index) => {
70
+ if (!entries)
71
+ return Promise.resolve([]);
72
+ const assetDirPath = `${basePath}/${assetTypes[index]}`;
73
+ return expandDirectoryEntries(repoInfo, assetDirPath, entries, tag);
74
+ }));
48
75
  const assetFiles = {};
49
76
  assetTypes.forEach((assetType, index) => {
50
- assetFiles[assetType] = results[index] || [];
77
+ assetFiles[assetType] = expandedResults[index] || [];
51
78
  });
52
79
  return assetFiles;
53
80
  };
@@ -83,5 +110,6 @@ exports.NotFoundError = NotFoundError;
83
110
  exports.RateLimitError = RateLimitError;
84
111
  exports.downloadAssetFiles = downloadAssetFiles;
85
112
  exports.downloadFile = downloadFile;
113
+ exports.expandDirectoryEntries = expandDirectoryEntries;
86
114
  exports.fetchAssetFiles = fetchAssetFiles;
87
115
  exports.fetchDirectoryContents = fetchDirectoryContents;
@@ -1,4 +1,4 @@
1
- import type { AssetType, GitHubEntry, GitHubRepoInfo } from '../utils/types';
1
+ import type { AssetType, GitHubEntry, GitHubRepoInfo } from '../utils/types.js';
2
2
  /**
3
3
  * Error thrown when GitHub API rate limit is exceeded
4
4
  */
@@ -19,6 +19,19 @@ export declare class NotFoundError extends Error {
19
19
  * @returns Array of GitHubEntry or null if directory doesn't exist
20
20
  */
21
21
  export declare const fetchDirectoryContents: (repoInfo: GitHubRepoInfo, path: string, tag: string) => Promise<GitHubEntry[] | null>;
22
+ /**
23
+ * Expand directory entries into flat file entries with recursive traversal.
24
+ * Fetches contents of each directory and prefixes file names with the directory path.
25
+ * Recursively traverses subdirectories to collect all nested files.
26
+ *
27
+ * @param repoInfo - GitHub repository information
28
+ * @param parentPath - Parent directory path in the repository
29
+ * @param entries - Array of GitHubEntry (may contain both file and dir types)
30
+ * @param tag - Git tag or ref to fetch from
31
+ * @param prefix - Accumulated path prefix for nested entries
32
+ * @returns Flat array of file GitHubEntry with dir-prefixed names
33
+ */
34
+ export declare const expandDirectoryEntries: (repoInfo: GitHubRepoInfo, parentPath: string, entries: GitHubEntry[], tag: string, prefix?: string) => Promise<GitHubEntry[]>;
22
35
  /**
23
36
  * Fetch asset files dynamically from GitHub
24
37
  * @param repoInfo - GitHub repository information
@@ -35,17 +35,44 @@ const fetchDirectoryContents = async (repoInfo, path, tag) => {
35
35
  if (!response.ok)
36
36
  throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
37
37
  const data = await response.json();
38
- return data.filter((entry) => entry.type === 'file' && entry.name.endsWith('.md'));
38
+ return data.filter((entry) => (entry.type === 'file' && entry.name.endsWith('.md')) ||
39
+ entry.type === 'dir');
40
+ };
41
+ const expandDirectoryEntries = async (repoInfo, parentPath, entries, tag, prefix = '') => {
42
+ const result = [];
43
+ for (const entry of entries) {
44
+ const entryPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
45
+ if (entry.type === 'file') {
46
+ result.push({
47
+ ...entry,
48
+ name: prefix ? entryPrefix : entry.name,
49
+ });
50
+ }
51
+ else if (entry.type === 'dir') {
52
+ const subEntries = await fetchDirectoryContents(repoInfo, `${parentPath}/${entry.name}`, tag);
53
+ if (subEntries) {
54
+ const expanded = await expandDirectoryEntries(repoInfo, `${parentPath}/${entry.name}`, subEntries, tag, entryPrefix);
55
+ result.push(...expanded);
56
+ }
57
+ }
58
+ }
59
+ return result;
39
60
  };
40
61
  const fetchAssetFiles = async (repoInfo, assetPath, tag, assetTypes) => {
41
62
  const basePath = repoInfo.directory
42
63
  ? `${repoInfo.directory}/${assetPath}`
43
64
  : assetPath;
44
65
  const fetchPromises = assetTypes.map((assetType) => fetchDirectoryContents(repoInfo, `${basePath}/${assetType}`, tag));
45
- const results = await Promise.all(fetchPromises);
66
+ const rawResults = await Promise.all(fetchPromises);
67
+ const expandedResults = await Promise.all(rawResults.map((entries, index) => {
68
+ if (!entries)
69
+ return Promise.resolve([]);
70
+ const assetDirPath = `${basePath}/${assetTypes[index]}`;
71
+ return expandDirectoryEntries(repoInfo, assetDirPath, entries, tag);
72
+ }));
46
73
  const assetFiles = {};
47
74
  assetTypes.forEach((assetType, index) => {
48
- assetFiles[assetType] = results[index] || [];
75
+ assetFiles[assetType] = expandedResults[index] || [];
49
76
  });
50
77
  return assetFiles;
51
78
  };
@@ -77,4 +104,4 @@ const downloadAssetFiles = async (repoInfo, assetPath, assetType, entries, tag)
77
104
  return results;
78
105
  };
79
106
 
80
- export { NotFoundError, RateLimitError, downloadAssetFiles, downloadFile, fetchAssetFiles, fetchDirectoryContents };
107
+ export { NotFoundError, RateLimitError, downloadAssetFiles, downloadFile, expandDirectoryEntries, fetchAssetFiles, fetchDirectoryContents };
@@ -3,6 +3,7 @@
3
3
  var fs = require('node:fs');
4
4
  var path = require('node:path');
5
5
  var nameTransform = require('../utils/nameTransform.cjs');
6
+ var packageName = require('../utils/packageName.cjs');
6
7
  var filesystem = require('./filesystem.cjs');
7
8
  var syncMeta = require('./syncMeta.cjs');
8
9
 
@@ -62,8 +63,8 @@ async function migrateToFlat(cwd, options = {}) {
62
63
  });
63
64
  for (const packageDir of packageDirs) {
64
65
  const packagePath = path.join(scopePath, packageDir);
65
- const packageName = `${scopeDir}/${packageDir}`;
66
- console.log(` 📦 Processing ${packageName}...`);
66
+ const packageName$1 = `${scopeDir}/${packageDir}`;
67
+ console.log(` 📦 Processing ${packageName$1}...`);
67
68
  try {
68
69
  const metaPath = path.join(packagePath, '.sync-meta.json');
69
70
  let legacyMeta = null;
@@ -75,7 +76,7 @@ async function migrateToFlat(cwd, options = {}) {
75
76
  console.log(` ⚠️ No .sync-meta.json found, skipping`);
76
77
  continue;
77
78
  }
78
- const prefix = nameTransform.packageNameToPrefix(packageName);
79
+ const prefix = packageName.packageNameToPrefix(packageName$1);
79
80
  const commandFiles = [];
80
81
  const fileMappings = [];
81
82
  for (const fileName of legacyMeta.files) {
@@ -112,7 +113,7 @@ async function migrateToFlat(cwd, options = {}) {
112
113
  }
113
114
  }
114
115
  const packageInfo = {
115
- originalName: packageName,
116
+ originalName: packageName$1,
116
117
  version: legacyMeta.version,
117
118
  files: {
118
119
  commands: assetType === 'commands' ? commandFiles : [],
@@ -136,11 +137,11 @@ async function migrateToFlat(cwd, options = {}) {
136
137
  }
137
138
  }
138
139
  unifiedMeta = syncMeta.updatePackageInMeta(unifiedMeta, prefix, packageInfo);
139
- migratedPackages.push(packageName);
140
+ migratedPackages.push(packageName$1);
140
141
  legacyDirs.push(packagePath);
141
142
  }
142
143
  catch (error) {
143
- const errorMsg = `Failed to migrate ${packageName}: ${error}`;
144
+ const errorMsg = `Failed to migrate ${packageName$1}: ${error}`;
144
145
  console.error(` ❌ ${errorMsg}`);
145
146
  errors.push(errorMsg);
146
147
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, statSync, readFileSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { packageNameToPrefix, toFlatFileName } from '../utils/nameTransform.mjs';
3
+ import { toFlatFileName } from '../utils/nameTransform.mjs';
4
+ import { packageNameToPrefix } from '../utils/packageName.mjs';
4
5
  import { writeFlatAssetFile } from './filesystem.mjs';
5
6
  import { readUnifiedSyncMeta, createEmptyUnifiedMeta, updatePackageInMeta, writeUnifiedSyncMeta } from './syncMeta.mjs';
6
7
 
@@ -72,15 +72,25 @@ async function scanRemoteAssets(packageName, ref) {
72
72
  throw new Error(`Invalid GitHub repository URL in package ${packageName}`);
73
73
  }
74
74
  const assetBasePath = pkgInfo.claude.assetPath;
75
+ const tag = ref ?? 'HEAD';
75
76
  const trees = [];
76
77
  for (const assetType of constants.DEFAULT_ASSET_TYPES) {
77
78
  const assetPath = repoInfo.directory
78
79
  ? `${repoInfo.directory}/${assetBasePath}/${assetType}`
79
80
  : `${assetBasePath}/${assetType}`;
80
81
  try {
81
- const entries = await github.fetchDirectoryContents(repoInfo, assetPath, ref ?? 'HEAD');
82
+ const entries = await github.fetchDirectoryContents(repoInfo, assetPath, tag);
82
83
  if (entries && entries.length > 0) {
83
- const tree = buildTreeFromGitHubEntries(assetType, entries, assetType);
84
+ const dirContentsMap = new Map();
85
+ for (const entry of entries) {
86
+ if (entry.type === 'dir') {
87
+ const dirEntries = await github.fetchDirectoryContents(repoInfo, `${assetPath}/${entry.name}`, tag);
88
+ if (dirEntries) {
89
+ dirContentsMap.set(entry.name, dirEntries);
90
+ }
91
+ }
92
+ }
93
+ const tree = buildTreeFromGitHubEntries(assetType, entries, assetType, dirContentsMap);
84
94
  if (tree.children && tree.children.length > 0) {
85
95
  trees.push(tree);
86
96
  }
@@ -100,7 +110,8 @@ function buildTreeFromLocalDir(label, dirPath, basePath) {
100
110
  const stat = fs.statSync(fullPath);
101
111
  const relativePath = path.join(basePath, entry);
102
112
  if (stat.isDirectory()) {
103
- const isSkill = fs.existsSync(path.join(fullPath, 'Skill.md'));
113
+ const isSkill = fs.existsSync(path.join(fullPath, 'SKILL.md')) ||
114
+ fs.existsSync(path.join(fullPath, 'Skill.md'));
104
115
  if (isSkill) {
105
116
  children.push({
106
117
  id: relativePath,
@@ -139,36 +150,37 @@ function buildTreeFromLocalDir(label, dirPath, basePath) {
139
150
  expanded: true,
140
151
  };
141
152
  }
142
- function buildTreeFromGitHubEntries(label, entries, basePath) {
153
+ function buildTreeFromGitHubEntries(label, entries, basePath, dirContentsMap) {
143
154
  const children = [];
144
- const grouped = groupByTopLevel(entries);
145
- for (const [name, items] of Object.entries(grouped)) {
146
- if (items.length === 1 && items[0].type === 'file') {
147
- const filePath = items[0].path;
155
+ for (const entry of entries) {
156
+ if (entry.type === 'file') {
148
157
  children.push({
149
- id: filePath,
150
- label: name,
151
- path: filePath,
158
+ id: entry.path,
159
+ label: entry.name,
160
+ path: entry.path,
152
161
  type: 'file',
153
162
  selected: true,
154
163
  expanded: false,
155
164
  });
156
165
  }
157
- else {
158
- const isSkill = items.some((item) => item.type === 'file' && item.name === 'Skill.md');
159
- if (isSkill) {
160
- const skillPath = `${basePath}/${name}`;
166
+ else if (entry.type === 'dir') {
167
+ const dirEntries = dirContentsMap?.get(entry.name);
168
+ const hasSkillMd = dirEntries
169
+ ? isDirectorySkill(dirEntries)
170
+ : false;
171
+ if (hasSkillMd) {
172
+ const skillPath = `${basePath}/${entry.name}`;
161
173
  children.push({
162
174
  id: skillPath,
163
- label: name,
175
+ label: entry.name,
164
176
  path: skillPath,
165
177
  type: 'skill-directory',
166
178
  selected: true,
167
179
  expanded: false,
168
180
  });
169
181
  }
170
- else {
171
- const subTree = buildTreeFromGitHubEntries(name, items, `${basePath}/${name}`);
182
+ else if (dirEntries && dirEntries.length > 0) {
183
+ const subTree = buildTreeFromGitHubEntries(entry.name, dirEntries, `${basePath}/${entry.name}`);
172
184
  if (subTree.children && subTree.children.length > 0) {
173
185
  children.push(subTree);
174
186
  }
@@ -185,17 +197,9 @@ function buildTreeFromGitHubEntries(label, entries, basePath) {
185
197
  expanded: true,
186
198
  };
187
199
  }
188
- function groupByTopLevel(entries) {
189
- const groups = {};
190
- for (const entry of entries) {
191
- const parts = entry.path.split('/');
192
- const topLevel = parts[parts.length - (entry.type === 'file' ? 1 : 0)];
193
- if (!groups[topLevel]) {
194
- groups[topLevel] = [];
195
- }
196
- groups[topLevel].push(entry);
197
- }
198
- return groups;
200
+ function isDirectorySkill(entries) {
201
+ return entries.some((entry) => entry.type === 'file' &&
202
+ (entry.name === 'SKILL.md' || entry.name === 'Skill.md'));
199
203
  }
200
204
  function findLocalPackage(packageName, cwd) {
201
205
  const monorepoRoot = findMonorepoRoot(cwd);
@@ -256,4 +260,5 @@ function searchPackagesRecursively(dir, packageName) {
256
260
  return null;
257
261
  }
258
262
 
263
+ exports.isDirectorySkill = isDirectorySkill;
259
264
  exports.scanPackageAssets = scanPackageAssets;
@@ -70,15 +70,25 @@ async function scanRemoteAssets(packageName, ref) {
70
70
  throw new Error(`Invalid GitHub repository URL in package ${packageName}`);
71
71
  }
72
72
  const assetBasePath = pkgInfo.claude.assetPath;
73
+ const tag = ref ?? 'HEAD';
73
74
  const trees = [];
74
75
  for (const assetType of DEFAULT_ASSET_TYPES) {
75
76
  const assetPath = repoInfo.directory
76
77
  ? `${repoInfo.directory}/${assetBasePath}/${assetType}`
77
78
  : `${assetBasePath}/${assetType}`;
78
79
  try {
79
- const entries = await fetchDirectoryContents(repoInfo, assetPath, ref ?? 'HEAD');
80
+ const entries = await fetchDirectoryContents(repoInfo, assetPath, tag);
80
81
  if (entries && entries.length > 0) {
81
- const tree = buildTreeFromGitHubEntries(assetType, entries, assetType);
82
+ const dirContentsMap = new Map();
83
+ for (const entry of entries) {
84
+ if (entry.type === 'dir') {
85
+ const dirEntries = await fetchDirectoryContents(repoInfo, `${assetPath}/${entry.name}`, tag);
86
+ if (dirEntries) {
87
+ dirContentsMap.set(entry.name, dirEntries);
88
+ }
89
+ }
90
+ }
91
+ const tree = buildTreeFromGitHubEntries(assetType, entries, assetType, dirContentsMap);
82
92
  if (tree.children && tree.children.length > 0) {
83
93
  trees.push(tree);
84
94
  }
@@ -98,7 +108,8 @@ function buildTreeFromLocalDir(label, dirPath, basePath) {
98
108
  const stat = statSync(fullPath);
99
109
  const relativePath = join(basePath, entry);
100
110
  if (stat.isDirectory()) {
101
- const isSkill = existsSync(join(fullPath, 'Skill.md'));
111
+ const isSkill = existsSync(join(fullPath, 'SKILL.md')) ||
112
+ existsSync(join(fullPath, 'Skill.md'));
102
113
  if (isSkill) {
103
114
  children.push({
104
115
  id: relativePath,
@@ -137,36 +148,37 @@ function buildTreeFromLocalDir(label, dirPath, basePath) {
137
148
  expanded: true,
138
149
  };
139
150
  }
140
- function buildTreeFromGitHubEntries(label, entries, basePath) {
151
+ function buildTreeFromGitHubEntries(label, entries, basePath, dirContentsMap) {
141
152
  const children = [];
142
- const grouped = groupByTopLevel(entries);
143
- for (const [name, items] of Object.entries(grouped)) {
144
- if (items.length === 1 && items[0].type === 'file') {
145
- const filePath = items[0].path;
153
+ for (const entry of entries) {
154
+ if (entry.type === 'file') {
146
155
  children.push({
147
- id: filePath,
148
- label: name,
149
- path: filePath,
156
+ id: entry.path,
157
+ label: entry.name,
158
+ path: entry.path,
150
159
  type: 'file',
151
160
  selected: true,
152
161
  expanded: false,
153
162
  });
154
163
  }
155
- else {
156
- const isSkill = items.some((item) => item.type === 'file' && item.name === 'Skill.md');
157
- if (isSkill) {
158
- const skillPath = `${basePath}/${name}`;
164
+ else if (entry.type === 'dir') {
165
+ const dirEntries = dirContentsMap?.get(entry.name);
166
+ const hasSkillMd = dirEntries
167
+ ? isDirectorySkill(dirEntries)
168
+ : false;
169
+ if (hasSkillMd) {
170
+ const skillPath = `${basePath}/${entry.name}`;
159
171
  children.push({
160
172
  id: skillPath,
161
- label: name,
173
+ label: entry.name,
162
174
  path: skillPath,
163
175
  type: 'skill-directory',
164
176
  selected: true,
165
177
  expanded: false,
166
178
  });
167
179
  }
168
- else {
169
- const subTree = buildTreeFromGitHubEntries(name, items, `${basePath}/${name}`);
180
+ else if (dirEntries && dirEntries.length > 0) {
181
+ const subTree = buildTreeFromGitHubEntries(entry.name, dirEntries, `${basePath}/${entry.name}`);
170
182
  if (subTree.children && subTree.children.length > 0) {
171
183
  children.push(subTree);
172
184
  }
@@ -183,17 +195,9 @@ function buildTreeFromGitHubEntries(label, entries, basePath) {
183
195
  expanded: true,
184
196
  };
185
197
  }
186
- function groupByTopLevel(entries) {
187
- const groups = {};
188
- for (const entry of entries) {
189
- const parts = entry.path.split('/');
190
- const topLevel = parts[parts.length - (entry.type === 'file' ? 1 : 0)];
191
- if (!groups[topLevel]) {
192
- groups[topLevel] = [];
193
- }
194
- groups[topLevel].push(entry);
195
- }
196
- return groups;
198
+ function isDirectorySkill(entries) {
199
+ return entries.some((entry) => entry.type === 'file' &&
200
+ (entry.name === 'SKILL.md' || entry.name === 'Skill.md'));
197
201
  }
198
202
  function findLocalPackage(packageName, cwd) {
199
203
  const monorepoRoot = findMonorepoRoot(cwd);
@@ -254,4 +258,4 @@ function searchPackagesRecursively(dir, packageName) {
254
258
  return null;
255
259
  }
256
260
 
257
- export { scanPackageAssets };
261
+ export { isDirectorySkill, scanPackageAssets };