@slats/claude-assets-sync 0.0.4 → 0.0.5

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.
Files changed (43) hide show
  1. package/dist/commands/add.cjs +2 -7
  2. package/dist/commands/add.mjs +2 -7
  3. package/dist/commands/index.d.ts +22 -0
  4. package/dist/commands/remove.cjs +6 -6
  5. package/dist/commands/remove.mjs +6 -6
  6. package/dist/commands/types.d.ts +2 -4
  7. package/dist/commands/update.cjs +178 -0
  8. package/dist/commands/update.d.ts +13 -0
  9. package/dist/commands/update.mjs +176 -0
  10. package/dist/components/add/AddCommand.cjs +11 -0
  11. package/dist/components/add/AddCommand.mjs +11 -0
  12. package/dist/components/list/ListCommand.cjs +65 -47
  13. package/dist/components/list/ListCommand.mjs +66 -48
  14. package/dist/components/status/StatusDisplay.cjs +4 -3
  15. package/dist/components/status/StatusDisplay.d.ts +2 -4
  16. package/dist/components/status/StatusDisplay.mjs +4 -3
  17. package/dist/components/tree/AssetTreeNode.cjs +21 -4
  18. package/dist/components/tree/AssetTreeNode.d.ts +1 -1
  19. package/dist/components/tree/AssetTreeNode.mjs +21 -4
  20. package/dist/components/tree/TreeSelect.cjs +14 -6
  21. package/dist/components/tree/TreeSelect.mjs +14 -6
  22. package/dist/core/cli.cjs +18 -0
  23. package/dist/core/cli.mjs +18 -0
  24. package/dist/core/constants.cjs +4 -1
  25. package/dist/core/constants.d.ts +8 -1
  26. package/dist/core/constants.mjs +4 -1
  27. package/dist/core/filesystem.cjs +7 -13
  28. package/dist/core/filesystem.mjs +7 -13
  29. package/dist/core/migration.cjs +10 -9
  30. package/dist/core/migration.mjs +10 -9
  31. package/dist/core/packageScanner.cjs +76 -0
  32. package/dist/core/packageScanner.d.ts +6 -1
  33. package/dist/core/packageScanner.mjs +76 -1
  34. package/dist/core/sync.cjs +53 -30
  35. package/dist/core/sync.mjs +53 -30
  36. package/dist/core/syncMeta.cjs +153 -9
  37. package/dist/core/syncMeta.d.ts +22 -18
  38. package/dist/core/syncMeta.mjs +149 -9
  39. package/dist/utils/types.d.ts +24 -7
  40. package/dist/version.cjs +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.mjs +1 -1
  43. package/package.json +1 -1
@@ -10,12 +10,14 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
10
10
  const [treeData, setTreeData] = React.useState(trees);
11
11
  const flattenTree = (nodes, depth = 0) => {
12
12
  const result = [];
13
- const traverse = (items, currentDepth, parentPath = []) => {
13
+ const traverse = (items, currentDepth, parentPath = [], ancestorIsLast = []) => {
14
14
  items.forEach((item, index) => {
15
+ const isLast = index === items.length - 1;
16
+ const currentIsLast = [...ancestorIsLast, isLast];
15
17
  const currentPath = [...parentPath, index];
16
- result.push({ node: item, depth: currentDepth, path: currentPath });
18
+ result.push({ node: item, depth: currentDepth, path: currentPath, isLastAtDepth: currentIsLast });
17
19
  if (item.expanded && item.children) {
18
- traverse(item.children, currentDepth + 1, currentPath);
20
+ traverse(item.children, currentDepth + 1, currentPath, currentIsLast);
19
21
  }
20
22
  });
21
23
  };
@@ -32,7 +34,9 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
32
34
  }
33
35
  else if (key.rightArrow) {
34
36
  const current = flatItems[selectedIndex];
35
- if (current.node.type === 'directory' && !current.node.expanded) {
37
+ if ((current.node.type === 'directory' || current.node.type === 'skill-directory') &&
38
+ !current.node.expanded &&
39
+ current.node.children) {
36
40
  const newTrees = updateNodeAtPath(treeData, current.path, (node) => ({
37
41
  ...node,
38
42
  expanded: true,
@@ -42,7 +46,8 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
42
46
  }
43
47
  else if (key.leftArrow) {
44
48
  const current = flatItems[selectedIndex];
45
- if (current.node.type === 'directory' && current.node.expanded) {
49
+ if ((current.node.type === 'directory' || current.node.type === 'skill-directory') &&
50
+ current.node.expanded) {
46
51
  const newTrees = updateNodeAtPath(treeData, current.path, (node) => ({
47
52
  ...node,
48
53
  expanded: false,
@@ -52,6 +57,9 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
52
57
  }
53
58
  else if (input === ' ') {
54
59
  const current = flatItems[selectedIndex];
60
+ if (current.node.disabled) {
61
+ return;
62
+ }
55
63
  const newTrees = toggleNodeSelection(treeData, current.path);
56
64
  setTreeData(newTrees);
57
65
  }
@@ -65,7 +73,7 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
65
73
  onCancel();
66
74
  }
67
75
  });
68
- return (jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: flatItems.map((item, index) => (jsxRuntime.jsx(AssetTreeNode.AssetTreeNode, { node: item.node, depth: item.depth, isSelected: index === selectedIndex, onToggle: () => {
76
+ return (jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: flatItems.map((item, index) => (jsxRuntime.jsx(AssetTreeNode.AssetTreeNode, { node: item.node, isLastAtDepth: item.isLastAtDepth, isSelected: index === selectedIndex, onToggle: () => {
69
77
  const newTrees = toggleNodeSelection(treeData, item.path);
70
78
  setTreeData(newTrees);
71
79
  } }, item.path.join('-')))) }));
@@ -8,12 +8,14 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
8
8
  const [treeData, setTreeData] = useState(trees);
9
9
  const flattenTree = (nodes, depth = 0) => {
10
10
  const result = [];
11
- const traverse = (items, currentDepth, parentPath = []) => {
11
+ const traverse = (items, currentDepth, parentPath = [], ancestorIsLast = []) => {
12
12
  items.forEach((item, index) => {
13
+ const isLast = index === items.length - 1;
14
+ const currentIsLast = [...ancestorIsLast, isLast];
13
15
  const currentPath = [...parentPath, index];
14
- result.push({ node: item, depth: currentDepth, path: currentPath });
16
+ result.push({ node: item, depth: currentDepth, path: currentPath, isLastAtDepth: currentIsLast });
15
17
  if (item.expanded && item.children) {
16
- traverse(item.children, currentDepth + 1, currentPath);
18
+ traverse(item.children, currentDepth + 1, currentPath, currentIsLast);
17
19
  }
18
20
  });
19
21
  };
@@ -30,7 +32,9 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
30
32
  }
31
33
  else if (key.rightArrow) {
32
34
  const current = flatItems[selectedIndex];
33
- if (current.node.type === 'directory' && !current.node.expanded) {
35
+ if ((current.node.type === 'directory' || current.node.type === 'skill-directory') &&
36
+ !current.node.expanded &&
37
+ current.node.children) {
34
38
  const newTrees = updateNodeAtPath(treeData, current.path, (node) => ({
35
39
  ...node,
36
40
  expanded: true,
@@ -40,7 +44,8 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
40
44
  }
41
45
  else if (key.leftArrow) {
42
46
  const current = flatItems[selectedIndex];
43
- if (current.node.type === 'directory' && current.node.expanded) {
47
+ if ((current.node.type === 'directory' || current.node.type === 'skill-directory') &&
48
+ current.node.expanded) {
44
49
  const newTrees = updateNodeAtPath(treeData, current.path, (node) => ({
45
50
  ...node,
46
51
  expanded: false,
@@ -50,6 +55,9 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
50
55
  }
51
56
  else if (input === ' ') {
52
57
  const current = flatItems[selectedIndex];
58
+ if (current.node.disabled) {
59
+ return;
60
+ }
53
61
  const newTrees = toggleNodeSelection(treeData, current.path);
54
62
  setTreeData(newTrees);
55
63
  }
@@ -63,7 +71,7 @@ const TreeSelect = ({ trees, onSubmit, onCancel, onRefresh, }) => {
63
71
  onCancel();
64
72
  }
65
73
  });
66
- return (jsx(Box, { flexDirection: "column", children: flatItems.map((item, index) => (jsx(AssetTreeNode, { node: item.node, depth: item.depth, isSelected: index === selectedIndex, onToggle: () => {
74
+ return (jsx(Box, { flexDirection: "column", children: flatItems.map((item, index) => (jsx(AssetTreeNode, { node: item.node, isLastAtDepth: item.isLastAtDepth, isSelected: index === selectedIndex, onToggle: () => {
67
75
  const newTrees = toggleNodeSelection(treeData, item.path);
68
76
  setTreeData(newTrees);
69
77
  } }, item.path.join('-')))) }));
package/dist/core/cli.cjs CHANGED
@@ -7,6 +7,7 @@ var remove = require('../commands/remove.cjs');
7
7
  var status = require('../commands/status.cjs');
8
8
  var migrate = require('../commands/migrate.cjs');
9
9
  var add = require('../commands/add.cjs');
10
+ var update = require('../commands/update.cjs');
10
11
  var version = require('../version.cjs');
11
12
 
12
13
  const createProgram = () => {
@@ -81,6 +82,23 @@ const createProgram = () => {
81
82
  .action(async (opts) => {
82
83
  await migrate.runMigrateCommand({ dryRun: opts.dryRun });
83
84
  });
85
+ program
86
+ .command('update')
87
+ .description('Update package metadata in .sync-meta.json')
88
+ .option('-p, --package <name>', 'Package name to update (default: all)')
89
+ .option('-l, --local', 'Read packages from local workspace instead of node_modules', false)
90
+ .option('-r, --ref <ref>', 'Git ref (branch, tag, or commit) to fetch from')
91
+ .option('--dry-run', 'Preview changes without writing files', false)
92
+ .option('--sync', 'Re-sync files after updating metadata', false)
93
+ .action(async (opts) => {
94
+ await update.runUpdateCommand({
95
+ package: opts.package,
96
+ local: opts.local,
97
+ ref: opts.ref,
98
+ dryRun: opts.dryRun,
99
+ sync: opts.sync,
100
+ });
101
+ });
84
102
  return program;
85
103
  };
86
104
  const run = async () => {
package/dist/core/cli.mjs CHANGED
@@ -5,6 +5,7 @@ import { runRemoveCommand } from '../commands/remove.mjs';
5
5
  import { runStatusCommand } from '../commands/status.mjs';
6
6
  import { runMigrateCommand } from '../commands/migrate.mjs';
7
7
  import { runAddCommand } from '../commands/add.mjs';
8
+ import { runUpdateCommand } from '../commands/update.mjs';
8
9
  import { VERSION } from '../version.mjs';
9
10
 
10
11
  const createProgram = () => {
@@ -79,6 +80,23 @@ const createProgram = () => {
79
80
  .action(async (opts) => {
80
81
  await runMigrateCommand({ dryRun: opts.dryRun });
81
82
  });
83
+ program
84
+ .command('update')
85
+ .description('Update package metadata in .sync-meta.json')
86
+ .option('-p, --package <name>', 'Package name to update (default: all)')
87
+ .option('-l, --local', 'Read packages from local workspace instead of node_modules', false)
88
+ .option('-r, --ref <ref>', 'Git ref (branch, tag, or commit) to fetch from')
89
+ .option('--dry-run', 'Preview changes without writing files', false)
90
+ .option('--sync', 'Re-sync files after updating metadata', false)
91
+ .action(async (opts) => {
92
+ await runUpdateCommand({
93
+ package: opts.package,
94
+ local: opts.local,
95
+ ref: opts.ref,
96
+ dryRun: opts.dryRun,
97
+ sync: opts.sync,
98
+ });
99
+ });
82
100
  return program;
83
101
  };
84
102
  const run = async () => {
@@ -8,7 +8,10 @@ const META_FILES = {
8
8
  UNIFIED_SYNC_META: '.claude/.sync-meta.json',
9
9
  };
10
10
  const SCHEMA_VERSIONS = {
11
- UNIFIED_SYNC_META: version.VERSION};
11
+ UNIFIED_SYNC_META: version.VERSION,
12
+ LEGACY_SYNC_META: '1.0.0',
13
+ SKILL_UNIT_FORMAT: '2',
14
+ };
12
15
  const DEFAULT_ASSET_TYPES = ['commands', 'skills', 'agents'];
13
16
  const FS_PATTERNS = {
14
17
  GITHUB_HTTPS_URL: /https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
@@ -22,9 +22,16 @@ 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.4";
25
+ readonly UNIFIED_SYNC_META: "0.0.5";
26
26
  readonly LEGACY_SYNC_META: "1.0.0";
27
+ readonly SKILL_UNIT_FORMAT: "2";
27
28
  };
29
+ /**
30
+ * Schema version for SkillUnit-based metadata format.
31
+ * Separate from package VERSION to allow independent format evolution.
32
+ * Used to detect whether migration from old format is needed.
33
+ */
34
+ export declare const SKILL_UNIT_SCHEMA_VERSION: "2";
28
35
  /**
29
36
  * Default asset types (exported for backward compatibility)
30
37
  */
@@ -6,7 +6,10 @@ const META_FILES = {
6
6
  UNIFIED_SYNC_META: '.claude/.sync-meta.json',
7
7
  };
8
8
  const SCHEMA_VERSIONS = {
9
- UNIFIED_SYNC_META: VERSION};
9
+ UNIFIED_SYNC_META: VERSION,
10
+ LEGACY_SYNC_META: '1.0.0',
11
+ SKILL_UNIT_FORMAT: '2',
12
+ };
10
13
  const DEFAULT_ASSET_TYPES = ['commands', 'skills', 'agents'];
11
14
  const FS_PATTERNS = {
12
15
  GITHUB_HTTPS_URL: /https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
@@ -58,13 +58,13 @@ 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();
62
- for (const fileMapping of filesToRemove) {
63
- const fileName = typeof fileMapping === 'string'
64
- ? fileMapping
65
- : fileMapping.transformed;
66
- if (fileName.includes('/')) {
67
- skillDirs.add(fileName.split('/')[0]);
61
+ for (const unit of filesToRemove) {
62
+ const fileName = unit.transformed ?? unit.name;
63
+ if (unit.isDirectory) {
64
+ const dirPath = path.join(destDir, fileName);
65
+ if (io.fileExists(dirPath)) {
66
+ fs.rmSync(dirPath, { recursive: true, force: true });
67
+ }
68
68
  }
69
69
  else {
70
70
  const filePath = path.join(destDir, fileName);
@@ -73,12 +73,6 @@ const cleanFlatAssetFiles = (cwd, assetType, prefix, existingMeta) => {
73
73
  }
74
74
  }
75
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 });
80
- }
81
- }
82
76
  }
83
77
  }
84
78
  else {
@@ -56,13 +56,13 @@ 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();
60
- for (const fileMapping of filesToRemove) {
61
- const fileName = typeof fileMapping === 'string'
62
- ? fileMapping
63
- : fileMapping.transformed;
64
- if (fileName.includes('/')) {
65
- skillDirs.add(fileName.split('/')[0]);
59
+ for (const unit of filesToRemove) {
60
+ const fileName = unit.transformed ?? unit.name;
61
+ if (unit.isDirectory) {
62
+ const dirPath = join(destDir, fileName);
63
+ if (fileExists(dirPath)) {
64
+ rmSync(dirPath, { recursive: true, force: true });
65
+ }
66
66
  }
67
67
  else {
68
68
  const filePath = join(destDir, fileName);
@@ -71,12 +71,6 @@ const cleanFlatAssetFiles = (cwd, assetType, prefix, existingMeta) => {
71
71
  }
72
72
  }
73
73
  }
74
- for (const dir of skillDirs) {
75
- const dirPath = join(destDir, dir);
76
- if (fileExists(dirPath)) {
77
- rmSync(dirPath, { recursive: true, force: true });
78
- }
79
- }
80
74
  }
81
75
  }
82
76
  else {
@@ -77,8 +77,8 @@ async function migrateToFlat(cwd, options = {}) {
77
77
  continue;
78
78
  }
79
79
  const prefix = packageName.packageNameToPrefix(packageName$1);
80
- const commandFiles = [];
81
- const fileMappings = [];
80
+ const commandUnits = [];
81
+ const skillUnits = [];
82
82
  for (const fileName of legacyMeta.files) {
83
83
  const sourcePath = path.join(packagePath, fileName);
84
84
  if (!fs.existsSync(sourcePath)) {
@@ -94,14 +94,10 @@ async function migrateToFlat(cwd, options = {}) {
94
94
  else {
95
95
  console.log(` 🔍 Would migrate: ${fileName}`);
96
96
  }
97
- commandFiles.push(fileName);
97
+ commandUnits.push({ name: fileName, isDirectory: false });
98
98
  }
99
99
  else {
100
100
  const flatFileName = nameTransform.toFlatFileName(prefix, fileName);
101
- fileMappings.push({
102
- original: fileName,
103
- transformed: flatFileName,
104
- });
105
101
  if (!dryRun) {
106
102
  const content = fs.readFileSync(sourcePath, 'utf-8');
107
103
  filesystem.writeFlatAssetFile(cwd, assetType, flatFileName, content);
@@ -110,14 +106,19 @@ async function migrateToFlat(cwd, options = {}) {
110
106
  else {
111
107
  console.log(` 🔍 Would migrate: ${fileName} → ${flatFileName}`);
112
108
  }
109
+ skillUnits.push({
110
+ name: fileName,
111
+ isDirectory: false,
112
+ transformed: flatFileName,
113
+ });
113
114
  }
114
115
  }
115
116
  const packageInfo = {
116
117
  originalName: packageName$1,
117
118
  version: legacyMeta.version,
118
119
  files: {
119
- commands: assetType === 'commands' ? commandFiles : [],
120
- skills: assetType === 'skills' ? fileMappings : [],
120
+ commands: assetType === 'commands' ? commandUnits : [],
121
+ skills: assetType === 'skills' ? skillUnits : [],
121
122
  agents: [],
122
123
  },
123
124
  };
@@ -75,8 +75,8 @@ async function migrateToFlat(cwd, options = {}) {
75
75
  continue;
76
76
  }
77
77
  const prefix = packageNameToPrefix(packageName);
78
- const commandFiles = [];
79
- const fileMappings = [];
78
+ const commandUnits = [];
79
+ const skillUnits = [];
80
80
  for (const fileName of legacyMeta.files) {
81
81
  const sourcePath = join(packagePath, fileName);
82
82
  if (!existsSync(sourcePath)) {
@@ -92,14 +92,10 @@ async function migrateToFlat(cwd, options = {}) {
92
92
  else {
93
93
  console.log(` 🔍 Would migrate: ${fileName}`);
94
94
  }
95
- commandFiles.push(fileName);
95
+ commandUnits.push({ name: fileName, isDirectory: false });
96
96
  }
97
97
  else {
98
98
  const flatFileName = toFlatFileName(prefix, fileName);
99
- fileMappings.push({
100
- original: fileName,
101
- transformed: flatFileName,
102
- });
103
99
  if (!dryRun) {
104
100
  const content = readFileSync(sourcePath, 'utf-8');
105
101
  writeFlatAssetFile(cwd, assetType, flatFileName, content);
@@ -108,14 +104,19 @@ async function migrateToFlat(cwd, options = {}) {
108
104
  else {
109
105
  console.log(` 🔍 Would migrate: ${fileName} → ${flatFileName}`);
110
106
  }
107
+ skillUnits.push({
108
+ name: fileName,
109
+ isDirectory: false,
110
+ transformed: flatFileName,
111
+ });
111
112
  }
112
113
  }
113
114
  const packageInfo = {
114
115
  originalName: packageName,
115
116
  version: legacyMeta.version,
116
117
  files: {
117
- commands: assetType === 'commands' ? commandFiles : [],
118
- skills: assetType === 'skills' ? fileMappings : [],
118
+ commands: assetType === 'commands' ? commandUnits : [],
119
+ skills: assetType === 'skills' ? skillUnits : [],
119
120
  agents: [],
120
121
  },
121
122
  };
@@ -3,6 +3,7 @@
3
3
  var fs = require('node:fs');
4
4
  var path = require('node:path');
5
5
  var _package = require('../utils/package.cjs');
6
+ var nameTransform = require('../utils/nameTransform.cjs');
6
7
  var constants = require('./constants.cjs');
7
8
  var github = require('./github.cjs');
8
9
 
@@ -102,6 +103,26 @@ async function scanRemoteAssets(packageName, ref) {
102
103
  }
103
104
  return trees;
104
105
  }
106
+ function scanDirectoryRecursive(dirPath, prefix) {
107
+ const results = [];
108
+ try {
109
+ const entries = fs.readdirSync(dirPath);
110
+ for (const entry of entries) {
111
+ const fullPath = path.join(dirPath, entry);
112
+ const relativePath = prefix ? `${prefix}/${entry}` : entry;
113
+ const stat = fs.statSync(fullPath);
114
+ if (stat.isDirectory()) {
115
+ results.push(...scanDirectoryRecursive(fullPath, relativePath));
116
+ }
117
+ else {
118
+ results.push(relativePath);
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ }
124
+ return results;
125
+ }
105
126
  function buildTreeFromLocalDir(label, dirPath, basePath) {
106
127
  const entries = fs.readdirSync(dirPath);
107
128
  const children = [];
@@ -113,11 +134,23 @@ function buildTreeFromLocalDir(label, dirPath, basePath) {
113
134
  const isSkill = fs.existsSync(path.join(fullPath, 'SKILL.md')) ||
114
135
  fs.existsSync(path.join(fullPath, 'Skill.md'));
115
136
  if (isSkill) {
137
+ const internalFiles = scanDirectoryRecursive(fullPath, '');
138
+ const internalChildren = internalFiles.map((f) => ({
139
+ id: `${relativePath}/${f}`,
140
+ label: f,
141
+ path: `${relativePath}/${f}`,
142
+ type: 'file',
143
+ selected: true,
144
+ expanded: false,
145
+ disabled: true,
146
+ }));
116
147
  children.push({
117
148
  id: relativePath,
118
149
  label: entry,
119
150
  path: relativePath,
120
151
  type: 'skill-directory',
152
+ children: internalChildren,
153
+ viewOnly: true,
121
154
  selected: true,
122
155
  expanded: false,
123
156
  });
@@ -170,11 +203,24 @@ function buildTreeFromGitHubEntries(label, entries, basePath, dirContentsMap) {
170
203
  : false;
171
204
  if (hasSkillMd) {
172
205
  const skillPath = `${basePath}/${entry.name}`;
206
+ const internalChildren = dirEntries
207
+ ? dirEntries.map((de) => ({
208
+ id: `${skillPath}/${de.name}`,
209
+ label: de.name,
210
+ path: `${skillPath}/${de.name}`,
211
+ type: 'file',
212
+ selected: true,
213
+ expanded: false,
214
+ disabled: true,
215
+ }))
216
+ : [];
173
217
  children.push({
174
218
  id: skillPath,
175
219
  label: entry.name,
176
220
  path: skillPath,
177
221
  type: 'skill-directory',
222
+ children: internalChildren,
223
+ viewOnly: true,
178
224
  selected: true,
179
225
  expanded: false,
180
226
  });
@@ -259,6 +305,36 @@ function searchPackagesRecursively(dir, packageName) {
259
305
  }
260
306
  return null;
261
307
  }
308
+ function buildSkillUnitsFromTree(tree, prefix) {
309
+ if (!tree.children)
310
+ return [];
311
+ const units = [];
312
+ for (const child of tree.children) {
313
+ if (child.type === 'skill-directory') {
314
+ const internalFiles = child.children
315
+ ? child.children.map((c) => c.label)
316
+ : [];
317
+ units.push({
318
+ name: child.label,
319
+ isDirectory: true,
320
+ transformed: nameTransform.toFlatFileName(prefix, child.label),
321
+ internalFiles,
322
+ });
323
+ }
324
+ else if (child.type === 'file') {
325
+ units.push({
326
+ name: child.label,
327
+ isDirectory: false,
328
+ transformed: nameTransform.toFlatFileName(prefix, child.label),
329
+ });
330
+ }
331
+ else if (child.type === 'directory') {
332
+ units.push(...buildSkillUnitsFromTree(child, prefix));
333
+ }
334
+ }
335
+ return units;
336
+ }
262
337
 
338
+ exports.buildSkillUnitsFromTree = buildSkillUnitsFromTree;
263
339
  exports.isDirectorySkill = isDirectorySkill;
264
340
  exports.scanPackageAssets = scanPackageAssets;
@@ -1,4 +1,4 @@
1
- import type { GitHubEntry, TreeNode } from '../utils/types.js';
1
+ import type { GitHubEntry, SkillUnit, TreeNode } from '../utils/types.js';
2
2
  /**
3
3
  * Scan package assets from local workspace or GitHub
4
4
  *
@@ -15,3 +15,8 @@ export declare function scanPackageAssets(packageName: string, options: {
15
15
  * Check if entries represent a directory-based skill
16
16
  */
17
17
  export declare function isDirectorySkill(entries: GitHubEntry[]): boolean;
18
+ /**
19
+ * Convert a TreeNode asset-type subtree into SkillUnit[] for writing to meta.
20
+ * Used by both `add` and `list` commands when saving.
21
+ */
22
+ export declare function buildSkillUnitsFromTree(tree: TreeNode, prefix: string): SkillUnit[];
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { parseGitHubRepo } from '../utils/package.mjs';
4
+ import { toFlatFileName } from '../utils/nameTransform.mjs';
4
5
  import { DEFAULT_ASSET_TYPES } from './constants.mjs';
5
6
  import { fetchDirectoryContents } from './github.mjs';
6
7
 
@@ -100,6 +101,26 @@ async function scanRemoteAssets(packageName, ref) {
100
101
  }
101
102
  return trees;
102
103
  }
104
+ function scanDirectoryRecursive(dirPath, prefix) {
105
+ const results = [];
106
+ try {
107
+ const entries = readdirSync(dirPath);
108
+ for (const entry of entries) {
109
+ const fullPath = join(dirPath, entry);
110
+ const relativePath = prefix ? `${prefix}/${entry}` : entry;
111
+ const stat = statSync(fullPath);
112
+ if (stat.isDirectory()) {
113
+ results.push(...scanDirectoryRecursive(fullPath, relativePath));
114
+ }
115
+ else {
116
+ results.push(relativePath);
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ }
122
+ return results;
123
+ }
103
124
  function buildTreeFromLocalDir(label, dirPath, basePath) {
104
125
  const entries = readdirSync(dirPath);
105
126
  const children = [];
@@ -111,11 +132,23 @@ function buildTreeFromLocalDir(label, dirPath, basePath) {
111
132
  const isSkill = existsSync(join(fullPath, 'SKILL.md')) ||
112
133
  existsSync(join(fullPath, 'Skill.md'));
113
134
  if (isSkill) {
135
+ const internalFiles = scanDirectoryRecursive(fullPath, '');
136
+ const internalChildren = internalFiles.map((f) => ({
137
+ id: `${relativePath}/${f}`,
138
+ label: f,
139
+ path: `${relativePath}/${f}`,
140
+ type: 'file',
141
+ selected: true,
142
+ expanded: false,
143
+ disabled: true,
144
+ }));
114
145
  children.push({
115
146
  id: relativePath,
116
147
  label: entry,
117
148
  path: relativePath,
118
149
  type: 'skill-directory',
150
+ children: internalChildren,
151
+ viewOnly: true,
119
152
  selected: true,
120
153
  expanded: false,
121
154
  });
@@ -168,11 +201,24 @@ function buildTreeFromGitHubEntries(label, entries, basePath, dirContentsMap) {
168
201
  : false;
169
202
  if (hasSkillMd) {
170
203
  const skillPath = `${basePath}/${entry.name}`;
204
+ const internalChildren = dirEntries
205
+ ? dirEntries.map((de) => ({
206
+ id: `${skillPath}/${de.name}`,
207
+ label: de.name,
208
+ path: `${skillPath}/${de.name}`,
209
+ type: 'file',
210
+ selected: true,
211
+ expanded: false,
212
+ disabled: true,
213
+ }))
214
+ : [];
171
215
  children.push({
172
216
  id: skillPath,
173
217
  label: entry.name,
174
218
  path: skillPath,
175
219
  type: 'skill-directory',
220
+ children: internalChildren,
221
+ viewOnly: true,
176
222
  selected: true,
177
223
  expanded: false,
178
224
  });
@@ -257,5 +303,34 @@ function searchPackagesRecursively(dir, packageName) {
257
303
  }
258
304
  return null;
259
305
  }
306
+ function buildSkillUnitsFromTree(tree, prefix) {
307
+ if (!tree.children)
308
+ return [];
309
+ const units = [];
310
+ for (const child of tree.children) {
311
+ if (child.type === 'skill-directory') {
312
+ const internalFiles = child.children
313
+ ? child.children.map((c) => c.label)
314
+ : [];
315
+ units.push({
316
+ name: child.label,
317
+ isDirectory: true,
318
+ transformed: toFlatFileName(prefix, child.label),
319
+ internalFiles,
320
+ });
321
+ }
322
+ else if (child.type === 'file') {
323
+ units.push({
324
+ name: child.label,
325
+ isDirectory: false,
326
+ transformed: toFlatFileName(prefix, child.label),
327
+ });
328
+ }
329
+ else if (child.type === 'directory') {
330
+ units.push(...buildSkillUnitsFromTree(child, prefix));
331
+ }
332
+ }
333
+ return units;
334
+ }
260
335
 
261
- export { isDirectorySkill, scanPackageAssets };
336
+ export { buildSkillUnitsFromTree, isDirectorySkill, scanPackageAssets };