@slats/claude-assets-sync 0.0.3 → 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.
@@ -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.3";
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 };
@@ -3,23 +3,24 @@
3
3
  var logger = require('../utils/logger.cjs');
4
4
  var nameTransform = require('../utils/nameTransform.cjs');
5
5
  var _package = require('../utils/package.cjs');
6
+ var packageName = require('../utils/packageName.cjs');
6
7
  var paths = require('../utils/paths.cjs');
7
8
  var filesystem = require('./filesystem.cjs');
8
9
  var github = require('./github.cjs');
9
10
  var syncMeta = require('./syncMeta.cjs');
10
11
  var assetStructure = require('./assetStructure.cjs');
11
12
 
12
- const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions, outputDir) => {
13
- logger.logger.packageStart(packageName);
13
+ const syncPackage = async (packageName$1, options, cwd = process.cwd(), exclusions, outputDir) => {
14
+ logger.logger.packageStart(packageName$1);
14
15
  try {
15
16
  const destDir = outputDir ?? _package.findGitRoot(cwd) ?? cwd;
16
17
  const packageInfo = options.local
17
- ? _package.readLocalPackageJson(packageName, cwd)
18
- : _package.readPackageJson(packageName, cwd);
18
+ ? _package.readLocalPackageJson(packageName$1, cwd)
19
+ : _package.readPackageJson(packageName$1, cwd);
19
20
  if (!packageInfo) {
20
21
  const location = options.local ? 'workspace' : 'node_modules';
21
22
  return {
22
- packageName,
23
+ packageName: packageName$1,
23
24
  success: false,
24
25
  skipped: true,
25
26
  reason: `Package not found in ${location}`,
@@ -27,7 +28,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
27
28
  }
28
29
  if (!packageInfo.claude?.assetPath)
29
30
  return {
30
- packageName,
31
+ packageName: packageName$1,
31
32
  success: false,
32
33
  skipped: true,
33
34
  reason: 'Package does not have claude.assetPath in package.json',
@@ -35,7 +36,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
35
36
  const repoInfo = _package.parseGitHubRepo(packageInfo.repository);
36
37
  if (!repoInfo) {
37
38
  return {
38
- packageName,
39
+ packageName: packageName$1,
39
40
  success: false,
40
41
  skipped: true,
41
42
  reason: 'Unable to parse GitHub repository URL',
@@ -43,18 +44,18 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
43
44
  }
44
45
  const useFlat = options.flat !== false;
45
46
  if (useFlat) {
46
- const prefix = nameTransform.packageNameToPrefix(packageName);
47
+ const prefix = packageName.packageNameToPrefix(packageName$1);
47
48
  const unifiedMeta = syncMeta.readUnifiedSyncMeta(destDir) ?? syncMeta.createEmptyUnifiedMeta();
48
49
  if (!options.force &&
49
50
  !syncMeta.needsSyncUnified(unifiedMeta, prefix, packageInfo.version)) {
50
51
  return {
51
- packageName,
52
+ packageName: packageName$1,
52
53
  success: true,
53
54
  skipped: true,
54
55
  reason: `Already synced at version ${packageInfo.version}`,
55
56
  };
56
57
  }
57
- const tag = options.ref ?? _package.buildVersionTag(packageName, packageInfo.version);
58
+ const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
58
59
  const assetPath = _package.buildAssetPath(packageInfo.claude.assetPath);
59
60
  logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
60
61
  const assetTypes = _package.getAssetTypes(packageInfo.claude);
@@ -65,7 +66,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
65
66
  }
66
67
  if (totalFiles === 0)
67
68
  return {
68
- packageName,
69
+ packageName: packageName$1,
69
70
  success: false,
70
71
  skipped: true,
71
72
  reason: `No assets found in package (checked: ${assetTypes.join(', ')})`,
@@ -105,7 +106,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
105
106
  continue;
106
107
  const structure = assetStructure.getAssetStructure(assetType, packageInfo.claude);
107
108
  if (structure === 'nested') {
108
- logger.logger.step(`Would sync ${assetType} to`, paths.getDestinationDir(destDir, packageName, assetType));
109
+ logger.logger.step(`Would sync ${assetType} to`, paths.getDestinationDir(destDir, packageName$1, assetType));
109
110
  mappings.forEach((fileName) => {
110
111
  logger.logger.file('create', fileName);
111
112
  });
@@ -130,7 +131,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
130
131
  }
131
132
  }
132
133
  return {
133
- packageName,
134
+ packageName: packageName$1,
134
135
  success: true,
135
136
  skipped: false,
136
137
  syncedFiles,
@@ -139,7 +140,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
139
140
  for (const assetType of assetTypes) {
140
141
  const structure = assetStructure.getAssetStructure(assetType, packageInfo.claude);
141
142
  if (structure === 'nested') {
142
- filesystem.cleanAssetDir(destDir, packageName, assetType);
143
+ filesystem.cleanAssetDir(destDir, packageName$1, assetType);
143
144
  }
144
145
  else {
145
146
  filesystem.cleanFlatAssetFiles(destDir, assetType, prefix, unifiedMeta);
@@ -162,7 +163,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
162
163
  const downloadedFiles = await github.downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
163
164
  if (structure === 'nested') {
164
165
  for (const [fileName, content] of downloadedFiles) {
165
- filesystem.writeAssetFile(destDir, packageName, assetType, fileName, content);
166
+ filesystem.writeAssetFile(destDir, packageName$1, assetType, fileName, content);
166
167
  logger.logger.file('create', fileName);
167
168
  }
168
169
  }
@@ -177,7 +178,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
177
178
  }
178
179
  }
179
180
  const updatedMeta = syncMeta.updatePackageInMeta(unifiedMeta, prefix, {
180
- originalName: packageName,
181
+ originalName: packageName$1,
181
182
  version: packageInfo.version,
182
183
  local: options.local,
183
184
  files: fileMappings,
@@ -200,14 +201,14 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
200
201
  }
201
202
  }
202
203
  return {
203
- packageName,
204
+ packageName: packageName$1,
204
205
  success: true,
205
206
  skipped: false,
206
207
  syncedFiles,
207
208
  };
208
209
  }
209
210
  else {
210
- const tag = options.ref ?? _package.buildVersionTag(packageName, packageInfo.version);
211
+ const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
211
212
  const assetPath = _package.buildAssetPath(packageInfo.claude.assetPath);
212
213
  logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
213
214
  const assetTypes = _package.getAssetTypes(packageInfo.claude);
@@ -218,7 +219,7 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
218
219
  }
219
220
  if (totalFiles === 0)
220
221
  return {
221
- packageName,
222
+ packageName: packageName$1,
222
223
  success: false,
223
224
  skipped: true,
224
225
  reason: `No assets found in package (checked: ${assetTypes.join(', ')})`,
@@ -228,9 +229,9 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
228
229
  .join(', ');
229
230
  logger.logger.step('Found', foundSummary);
230
231
  if (!options.force &&
231
- !filesystem.needsSync(destDir, packageName, packageInfo.version, assetTypes)) {
232
+ !filesystem.needsSync(destDir, packageName$1, packageInfo.version, assetTypes)) {
232
233
  return {
233
- packageName,
234
+ packageName: packageName$1,
234
235
  success: true,
235
236
  skipped: true,
236
237
  reason: `Already synced at version ${packageInfo.version}`,
@@ -250,12 +251,12 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
250
251
  });
251
252
  if (filteredEntries.length === 0)
252
253
  continue;
253
- logger.logger.step(`Would sync ${assetType} to`, paths.getDestinationDir(destDir, packageName, assetType));
254
+ logger.logger.step(`Would sync ${assetType} to`, paths.getDestinationDir(destDir, packageName$1, assetType));
254
255
  filteredEntries.forEach((entry) => logger.logger.file('create', entry.name));
255
256
  syncedFiles[assetType] = filteredEntries.map((e) => e.name);
256
257
  }
257
258
  return {
258
- packageName,
259
+ packageName: packageName$1,
259
260
  success: true,
260
261
  skipped: false,
261
262
  syncedFiles,
@@ -276,17 +277,17 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
276
277
  continue;
277
278
  logger.logger.step('Downloading', assetType);
278
279
  const downloadedFiles = await github.downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
279
- filesystem.cleanAssetDir(destDir, packageName, assetType);
280
+ filesystem.cleanAssetDir(destDir, packageName$1, assetType);
280
281
  syncedFiles[assetType] = [];
281
282
  for (const [fileName, content] of downloadedFiles) {
282
- filesystem.writeAssetFile(destDir, packageName, assetType, fileName, content);
283
+ filesystem.writeAssetFile(destDir, packageName$1, assetType, fileName, content);
283
284
  logger.logger.file('create', fileName);
284
285
  syncedFiles[assetType].push(fileName);
285
286
  }
286
- filesystem.writeSyncMeta(destDir, packageName, assetType, filesystem.createSyncMeta(packageInfo.version, syncedFiles[assetType]));
287
+ filesystem.writeSyncMeta(destDir, packageName$1, assetType, filesystem.createSyncMeta(packageInfo.version, syncedFiles[assetType]));
287
288
  }
288
289
  return {
289
- packageName,
290
+ packageName: packageName$1,
290
291
  success: true,
291
292
  skipped: false,
292
293
  syncedFiles,
@@ -296,14 +297,14 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
296
297
  catch (error) {
297
298
  if (error instanceof github.RateLimitError)
298
299
  return {
299
- packageName,
300
+ packageName: packageName$1,
300
301
  success: false,
301
302
  skipped: false,
302
303
  reason: error.message,
303
304
  };
304
305
  const message = error instanceof Error ? error.message : 'Unknown error occurred';
305
306
  return {
306
- packageName,
307
+ packageName: packageName$1,
307
308
  success: false,
308
309
  skipped: false,
309
310
  reason: message,
@@ -1,6 +1,7 @@
1
1
  import { logger } from '../utils/logger.mjs';
2
- import { packageNameToPrefix, toFlatFileName } from '../utils/nameTransform.mjs';
2
+ import { toFlatFileName } from '../utils/nameTransform.mjs';
3
3
  import { findGitRoot, readLocalPackageJson, readPackageJson, parseGitHubRepo, buildVersionTag, buildAssetPath, getAssetTypes } from '../utils/package.mjs';
4
+ import { packageNameToPrefix } from '../utils/packageName.mjs';
4
5
  import { getDestinationDir, getFlatDestinationDir } from '../utils/paths.mjs';
5
6
  import { cleanAssetDir, cleanFlatAssetFiles, writeAssetFile, writeFlatAssetFile, needsSync, writeSyncMeta, createSyncMeta } from './filesystem.mjs';
6
7
  import { fetchAssetFiles, downloadAssetFiles, RateLimitError } from './github.mjs';
@@ -2,7 +2,7 @@ import type { PackageSyncInfo, UnifiedSyncMeta } from '../utils/types.js';
2
2
  /**
3
3
  * Schema version for the unified metadata format
4
4
  */
5
- export declare const SCHEMA_VERSION: "0.0.3";
5
+ export declare const SCHEMA_VERSION: "0.0.4";
6
6
  /**
7
7
  * Read unified sync metadata from .claude/.sync-meta.json
8
8
  *
@@ -1,12 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var packageName = require('./packageName.cjs');
4
-
5
- const packageNameToPrefix = packageName.packageNameToPrefix;
6
3
  function toFlatFileName(prefix, fileName) {
7
- const flatFileName = fileName.replace(/[/\\]/g, '_');
8
- return `${prefix}_${flatFileName}`;
4
+ const slashIndex = fileName.indexOf('/');
5
+ if (slashIndex === -1) {
6
+ return `${prefix}_${fileName}`;
7
+ }
8
+ const dirName = fileName.substring(0, slashIndex);
9
+ const rest = fileName.substring(slashIndex);
10
+ return `${prefix}_${dirName}${rest}`;
9
11
  }
10
12
 
11
- exports.packageNameToPrefix = packageNameToPrefix;
12
13
  exports.toFlatFileName = toFlatFileName;
@@ -1,8 +1,3 @@
1
- /**
2
- * Name transformation utilities for Claude assets synchronization
3
- * Handles conversion between package names and flat file naming conventions
4
- */
5
- import { packageNameToPrefix as packageNameToPrefixUtil } from './packageName';
6
1
  /**
7
2
  * Converts kebab-case string to camelCase
8
3
  * All hyphens are removed and the following character is capitalized
@@ -19,40 +14,34 @@ import { packageNameToPrefix as packageNameToPrefixUtil } from './packageName';
19
14
  */
20
15
  export declare function kebabToCamel(str: string): string;
21
16
  /**
22
- * Converts a scoped package name to a flat prefix
23
- * @deprecated Import from './packageName' instead
24
- * @param packageName - Scoped package name (e.g., '@canard/schema-form')
25
- * @returns Flat prefix (e.g., 'canard-schemaForm')
26
- *
27
- * @example
28
- * ```ts
29
- * packageNameToPrefix('@canard/schema-form') // 'canard-schemaForm'
30
- * packageNameToPrefix('@canard/schema-form-plugin') // 'canard-schemaFormPlugin'
31
- * packageNameToPrefix('@winglet/react-utils') // 'winglet-reactUtils'
32
- * packageNameToPrefix('@lerx/promise-modal') // 'lerx-promiseModal'
33
- * ```
34
- */
35
- export declare const packageNameToPrefix: typeof packageNameToPrefixUtil;
36
- /**
37
- * Creates a flat file name by combining prefix and original file name
17
+ * Creates a flat file name by combining prefix and original file name.
18
+ * For single files (no path separator), creates prefix_filename.
19
+ * For directory-based entries (with path separator), applies prefix only
20
+ * to the top-level directory name, preserving internal path structure.
38
21
  *
39
22
  * @param prefix - Package prefix (e.g., 'canard-schemaForm')
40
- * @param fileName - Original file name (e.g., 'guide.md')
41
- * @returns Flat file name (e.g., 'canard-schemaForm_guide.md')
23
+ * @param fileName - Original file name (e.g., 'guide.md' or 'expert/SKILL.md')
24
+ * @returns Flat file name with prefix
42
25
  *
43
26
  * @example
44
27
  * ```ts
45
- * toFlatFileName('canard-schemaForm', 'guide.md') // 'canard-schemaForm_guide.md'
46
- * toFlatFileName('winglet-reactUtils', 'README.md') // 'winglet-reactUtils_README.md'
47
- * toFlatFileName('lerx-promiseModal', 'api/types.md') // 'lerx-promiseModal_api_types.md'
28
+ * toFlatFileName('canard-schemaForm', 'guide.md')
29
+ * // 'canard-schemaForm_guide.md'
30
+ *
31
+ * toFlatFileName('canard-schemaForm', 'expert/SKILL.md')
32
+ * // 'canard-schemaForm_expert/SKILL.md'
33
+ *
34
+ * toFlatFileName('canard-schemaForm', 'expert/knowledge/api.md')
35
+ * // 'canard-schemaForm_expert/knowledge/api.md'
48
36
  * ```
49
37
  */
50
38
  export declare function toFlatFileName(prefix: string, fileName: string): string;
51
39
  /**
52
- * Parses a flat file name back into prefix and original file name
53
- * Returns null if the file name doesn't match the expected pattern
40
+ * Parses a flat file name back into prefix and original file name.
41
+ * Reverses the transformation done by toFlatFileName.
42
+ * Returns null if the file name doesn't match the expected pattern.
54
43
  *
55
- * @param flatName - Flat file name (e.g., 'canard-schemaForm_guide.md')
44
+ * @param flatName - Flat file name (e.g., 'canard-schemaForm_guide.md' or 'canard-schemaForm_expert/SKILL.md')
56
45
  * @returns Object with prefix and original name, or null if invalid
57
46
  *
58
47
  * @example
@@ -60,11 +49,11 @@ export declare function toFlatFileName(prefix: string, fileName: string): string
60
49
  * parseFlatFileName('canard-schemaForm_guide.md')
61
50
  * // { prefix: 'canard-schemaForm', original: 'guide.md' }
62
51
  *
63
- * parseFlatFileName('winglet-reactUtils_README.md')
64
- * // { prefix: 'winglet-reactUtils', original: 'README.md' }
52
+ * parseFlatFileName('canard-schemaForm_expert/SKILL.md')
53
+ * // { prefix: 'canard-schemaForm', original: 'expert/SKILL.md' }
65
54
  *
66
- * parseFlatFileName('lerx-promiseModal_api_types.md')
67
- * // { prefix: 'lerx-promiseModal', original: 'api/types.md' }
55
+ * parseFlatFileName('canard-schemaForm_expert/knowledge/api.md')
56
+ * // { prefix: 'canard-schemaForm', original: 'expert/knowledge/api.md' }
68
57
  *
69
58
  * parseFlatFileName('invalid-name.md')
70
59
  * // null (no underscore separator)
@@ -1,9 +1,11 @@
1
- import { packageNameToPrefix as packageNameToPrefix$1 } from './packageName.mjs';
2
-
3
- const packageNameToPrefix = packageNameToPrefix$1;
4
1
  function toFlatFileName(prefix, fileName) {
5
- const flatFileName = fileName.replace(/[/\\]/g, '_');
6
- return `${prefix}_${flatFileName}`;
2
+ const slashIndex = fileName.indexOf('/');
3
+ if (slashIndex === -1) {
4
+ return `${prefix}_${fileName}`;
5
+ }
6
+ const dirName = fileName.substring(0, slashIndex);
7
+ const rest = fileName.substring(slashIndex);
8
+ return `${prefix}_${dirName}${rest}`;
7
9
  }
8
10
 
9
- export { packageNameToPrefix, toFlatFileName };
11
+ export { toFlatFileName };
package/dist/version.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const VERSION = '0.0.3';
3
+ const VERSION = '0.0.4';
4
4
 
5
5
  exports.VERSION = VERSION;
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Current package version from package.json
3
3
  * Automatically synchronized during build process
4
4
  */
5
- export declare const VERSION = "0.0.3";
5
+ export declare const VERSION = "0.0.4";
package/dist/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
- const VERSION = '0.0.3';
1
+ const VERSION = '0.0.4';
2
2
 
3
3
  export { VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slats/claude-assets-sync",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "CLI tool to sync Claude commands and skills from npm packages to your project's .claude directory",
5
5
  "keywords": [
6
6
  "claude",
@@ -43,7 +43,7 @@
43
43
  "build": "node scripts/inject-version.js && rollup -c && yarn build:types",
44
44
  "build:publish:npm": "yarn build && yarn publish:npm",
45
45
  "build:types": "tsc -p ./tsconfig.declarations.json && tsc-alias -p ./tsconfig.declarations.json",
46
- "dev": "node scripts/inject-version.js && tsx src/index.ts",
46
+ "dev": "node scripts/inject-version.js && tsx src/cli.ts",
47
47
  "format": "prettier --write \"src/**/*.ts\"",
48
48
  "lint": "eslint \"src/**/*.ts\"",
49
49
  "publish:npm": "yarn npm publish --access public",
package/CHANGELOG.md DELETED
@@ -1,189 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to @slats/claude-assets-sync will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.0.1] - 2025-02-05
9
-
10
- ### Added
11
-
12
- #### Phase 1: Version Management Unification
13
- - Unified version management system across all packages
14
- - Single `.sync-meta.json` file replacing per-package metadata
15
- - Comprehensive package tracking with original names and file mappings
16
- - Timestamp tracking for all sync operations
17
- - Version comparison logic to skip unnecessary syncs
18
-
19
- #### Phase 2: Code Consolidation & Architecture
20
- - Modular command architecture with pluggable command system
21
- - New `src/commands/` directory for command implementations
22
- - Command registry system (`COMMANDS` export) with metadata
23
- - Centralized command types in `src/commands/types.ts`
24
- - Unified CLI structure using Commander.js
25
- - Better separation of concerns between CLI and command logic
26
-
27
- #### Phase 3: Flat Directory Structure Support
28
- - Modern flat file organization with prefixed filenames
29
- - Name transformation system for scoped packages
30
- - Support for both flat and nested directory structures
31
- - `--no-flat` flag to maintain backward compatibility with legacy structure
32
- - Smart detection of directory structure type during sync operations
33
-
34
- #### Phase 4: Package Management Features
35
- - **list command**: List all synced packages with asset details
36
- - Human-readable output with package names, versions, and asset counts
37
- - JSON output support for scripting and automation
38
- - Asset breakdown by type (commands, skills, etc.)
39
- - Sort by package name for consistency
40
-
41
- - **remove command**: Remove synced packages safely
42
- - Support for both flat and nested structures
43
- - Confirmation prompts (skip with `-y/--yes`)
44
- - Dry-run mode for preview
45
- - Automatic metadata cleanup after removal
46
- - Graceful error handling for missing files
47
-
48
- - **status command**: Monitor sync status and check for updates
49
- - Real-time remote version checking via npm registry
50
- - Version mismatch detection with visual indicators
51
- - Cached remote version checks (5-minute TTL)
52
- - `--no-remote` flag to skip remote checks
53
- - Summary statistics of sync status
54
-
55
- - **migrate command**: Migrate from legacy to flat structure
56
- - Automatic conversion of nested directories to flat naming
57
- - Comprehensive dry-run support
58
- - Metadata preservation during migration
59
- - Safe multi-run operation
60
-
61
- #### Phase 5: Interactive UI Infrastructure
62
- - Ink + React configuration for interactive CLI components
63
- - TypeScript JSX support (jsx: "react-jsx")
64
- - UI component infrastructure with fallback to plain text
65
- - ink (^4.4.1), ink-spinner (^5.0.0), react (^18.2.0) dependencies
66
- - Type definitions for React components (@types/react)
67
- - ESM compatibility maintained with interactive UI support
68
-
69
- #### Phase 6: Testing & Documentation
70
- - Comprehensive README documentation for all commands
71
- - Korean translation (README-ko_kr.md) with all features
72
- - Detailed command usage examples and workflows
73
- - Environment variable documentation
74
- - Troubleshooting guide with common issues and solutions
75
- - Architecture documentation explaining data flow
76
- - CI/CD integration examples
77
- - Rate limit documentation with mitigation strategies
78
-
79
- ### Enhanced
80
-
81
- - **Sync Logic**: Extended to support both flat and nested structures
82
- - **Error Handling**: Improved error messages with context-aware suggestions
83
- - **Logging**: Color-coded output with picocolors for better visibility
84
- - **GitHub Integration**: Support for custom git refs (branches, tags, commits)
85
- - **Local Workspace Support**: Option to read packages from local workspace
86
- - **File System Operations**: Safe handling of both file and directory removal
87
-
88
- ### Changed
89
-
90
- - CLI structure: Main sync is now default command with sub-commands (list, remove, status, migrate)
91
- - Version checking: Now compares against unified metadata instead of per-package files
92
- - Directory organization: Flat structure is now default (use `--no-flat` for legacy)
93
- - Metadata format: Unified schema with package prefixes as keys
94
- - File naming: Scoped packages now use hyphen-separated prefixes (e.g., @scope-package-file.md)
95
-
96
- ### Fixed
97
-
98
- - Version comparison for flat structure packages
99
- - Metadata update timing during removal operations
100
- - Directory creation for deeply nested legacy structures
101
- - File path handling for Windows compatibility
102
-
103
- ### Technical Details
104
-
105
- #### New File Mappings
106
- Each file is now tracked with original and transformed names:
107
- ```json
108
- {
109
- "original": "my-command.md",
110
- "transformed": "@scope-package-my-command.md"
111
- }
112
- ```
113
-
114
- #### Updated Metadata Structure
115
- ```json
116
- {
117
- "version": "0.0.1",
118
- "syncedAt": "2025-02-05T10:30:00.000Z",
119
- "packages": {
120
- "@scope-package": {
121
- "originalName": "@scope/package",
122
- "version": "1.0.0",
123
- "files": {
124
- "commands": [...],
125
- "skills": [...]
126
- }
127
- }
128
- }
129
- }
130
- ```
131
-
132
- #### Command Dependencies
133
- - **sync**: Core functionality, no command dependencies
134
- - **list**: Depends on unified metadata reading
135
- - **remove**: Depends on unified metadata, file system operations
136
- - **status**: Depends on npm registry API, version caching
137
- - **migrate**: Depends on legacy structure detection, transformation logic
138
-
139
- ### Dependencies Added
140
-
141
- - **ink** (^4.4.1): React renderer for terminal UIs
142
- - **ink-spinner** (^5.0.0): Loading spinner component for ink
143
- - **react** (^18.2.0): UI component framework
144
- - **@types/react** (^18.2.0): TypeScript types for React
145
-
146
- ### Dependencies Unchanged
147
-
148
- - **commander** (^12.1.0): CLI argument parsing
149
- - **picocolors** (^1.1.1): Terminal color output
150
-
151
- ### Breaking Changes
152
-
153
- None. The tool maintains backward compatibility:
154
- - `--no-flat` flag allows using legacy nested structure
155
- - Existing metadata files are automatically migrated
156
- - All previous commands and options continue to work
157
-
158
- ### Security
159
-
160
- - No security vulnerabilities introduced
161
- - Confirmation prompts for destructive operations
162
- - Dry-run mode for all write operations
163
- - Safe file system operations with error handling
164
-
165
- ### Performance
166
-
167
- - Efficient metadata reading and writing
168
- - Cached remote version checks reduce API calls
169
- - Parallel package processing capability ready (infrastructure in place)
170
- - Minimal memory overhead for large package lists
171
-
172
- ### Compatibility
173
-
174
- - Node.js: Compatible with modern Node versions supporting ES modules
175
- - Operating Systems: Windows, macOS, Linux
176
- - npm Packages: Works with all npm packages providing claude assets
177
-
178
- ## [Unreleased]
179
-
180
- ### Planned
181
-
182
- - Interactive UI components for progress visualization
183
- - Batch operations with progress indication
184
- - Configuration file support (.claude-sync.json)
185
- - Pre/post sync hooks
186
- - Asset validation and schema checking
187
- - Custom asset type support
188
- - Asset versioning and conflict resolution
189
- - Cloud storage integration (optional)