@slats/claude-assets-sync 0.0.5 → 0.1.0

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.
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  var pc = require('picocolors');
4
+ var constants = require('../core/constants.cjs');
4
5
  var packageScanner = require('../core/packageScanner.cjs');
5
6
  var sync = require('../core/sync.cjs');
6
7
  var syncMeta = require('../core/syncMeta.cjs');
7
- var constants = require('../core/constants.cjs');
8
8
  var logger = require('../utils/logger.cjs');
9
9
  var _package = require('../utils/package.cjs');
10
10
  var packageName = require('../utils/packageName.cjs');
@@ -113,8 +113,12 @@ const runUpdateCommand = async (options, cwd = process.cwd()) => {
113
113
  const refreshedUnits = currentUnits.map((current) => {
114
114
  const scanned = scannedUnits.find((s) => s.name === current.name);
115
115
  if (scanned && scanned.isDirectory && current.isDirectory) {
116
- const currentInternal = (current.internalFiles || []).sort().join(',');
117
- const scannedInternal = (scanned.internalFiles || []).sort().join(',');
116
+ const currentInternal = (current.internalFiles || [])
117
+ .sort()
118
+ .join(',');
119
+ const scannedInternal = (scanned.internalFiles || [])
120
+ .sort()
121
+ .join(',');
118
122
  if (currentInternal !== scannedInternal) {
119
123
  internalChanged = true;
120
124
  return { ...current, internalFiles: scanned.internalFiles };
@@ -1,8 +1,8 @@
1
1
  import pc from 'picocolors';
2
+ import { SCHEMA_VERSIONS } from '../core/constants.mjs';
2
3
  import { scanPackageAssets, buildSkillUnitsFromTree } from '../core/packageScanner.mjs';
3
4
  import { syncPackage } from '../core/sync.mjs';
4
5
  import { readUnifiedSyncMeta, updatePackageVersion, updatePackageFilesystemMeta, writeUnifiedSyncMeta } from '../core/syncMeta.mjs';
5
- import { SCHEMA_VERSIONS } from '../core/constants.mjs';
6
6
  import { logger } from '../utils/logger.mjs';
7
7
  import { findGitRoot, readLocalPackageJson, readPackageJson } from '../utils/package.mjs';
8
8
  import { packageNameToPrefix } from '../utils/packageName.mjs';
@@ -111,8 +111,12 @@ const runUpdateCommand = async (options, cwd = process.cwd()) => {
111
111
  const refreshedUnits = currentUnits.map((current) => {
112
112
  const scanned = scannedUnits.find((s) => s.name === current.name);
113
113
  if (scanned && scanned.isDirectory && current.isDirectory) {
114
- const currentInternal = (current.internalFiles || []).sort().join(',');
115
- const scannedInternal = (scanned.internalFiles || []).sort().join(',');
114
+ const currentInternal = (current.internalFiles || [])
115
+ .sort()
116
+ .join(',');
117
+ const scannedInternal = (scanned.internalFiles || [])
118
+ .sort()
119
+ .join(',');
116
120
  if (currentInternal !== scannedInternal) {
117
121
  internalChanged = true;
118
122
  return { ...current, internalFiles: scanned.internalFiles };
@@ -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.5";
25
+ readonly UNIFIED_SYNC_META: "0.1.0";
26
26
  readonly LEGACY_SYNC_META: "1.0.0";
27
27
  readonly SKILL_UNIT_FORMAT: "2";
28
28
  };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs');
4
+ var path = require('node:path');
5
+
6
+ const canUseLocalSource = (packageName, requestedVersion, assetPath, cwd) => {
7
+ const docsPath = path.join(cwd, 'node_modules', packageName, assetPath);
8
+ if (!fs.existsSync(docsPath)) {
9
+ return {
10
+ available: false,
11
+ reason: `Local docs path not found: ${docsPath}`,
12
+ };
13
+ }
14
+ try {
15
+ const pkgJsonPath = path.join(cwd, 'node_modules', packageName, 'package.json');
16
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
17
+ if (pkgJson.version !== requestedVersion) {
18
+ return {
19
+ available: false,
20
+ reason: `Version mismatch: installed=${pkgJson.version}, requested=${requestedVersion}`,
21
+ };
22
+ }
23
+ }
24
+ catch {
25
+ return {
26
+ available: false,
27
+ reason: 'Failed to read package.json from node_modules',
28
+ };
29
+ }
30
+ return { available: true, docsPath };
31
+ };
32
+ const fetchLocalDirectoryContents = (dirPath) => {
33
+ if (!fs.existsSync(dirPath))
34
+ return null;
35
+ try {
36
+ const entries = fs.readdirSync(dirPath);
37
+ const result = [];
38
+ for (const name of entries) {
39
+ const fullPath = path.join(dirPath, name);
40
+ const stat = fs.statSync(fullPath);
41
+ if (stat.isDirectory()) {
42
+ result.push({
43
+ name,
44
+ path: fullPath,
45
+ type: 'dir',
46
+ download_url: null,
47
+ sha: '',
48
+ });
49
+ }
50
+ else if (stat.isFile() && name.endsWith('.md')) {
51
+ result.push({
52
+ name,
53
+ path: fullPath,
54
+ type: 'file',
55
+ download_url: null,
56
+ sha: '',
57
+ });
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ };
66
+ const expandLocalDirectoryEntries = (dirPath, entries, prefix = '') => {
67
+ const result = [];
68
+ for (const entry of entries) {
69
+ const entryPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
70
+ if (entry.type === 'file') {
71
+ result.push({
72
+ ...entry,
73
+ name: prefix ? entryPrefix : entry.name,
74
+ });
75
+ }
76
+ else if (entry.type === 'dir') {
77
+ const subDirPath = path.join(dirPath, entry.name);
78
+ const subEntries = fetchLocalDirectoryContents(subDirPath);
79
+ if (subEntries) {
80
+ const expanded = expandLocalDirectoryEntries(subDirPath, subEntries, entryPrefix);
81
+ result.push(...expanded);
82
+ }
83
+ }
84
+ }
85
+ return result;
86
+ };
87
+ const fetchLocalAssetFiles = async (docsBasePath, assetTypes) => {
88
+ const assetFiles = {};
89
+ for (const assetType of assetTypes) {
90
+ const assetDirPath = path.join(docsBasePath, assetType);
91
+ const entries = fetchLocalDirectoryContents(assetDirPath);
92
+ if (!entries) {
93
+ assetFiles[assetType] = [];
94
+ continue;
95
+ }
96
+ assetFiles[assetType] = expandLocalDirectoryEntries(assetDirPath, entries);
97
+ }
98
+ return assetFiles;
99
+ };
100
+ const downloadLocalAssetFiles = async (docsBasePath, assetType, entries) => {
101
+ const results = new Map();
102
+ for (const entry of entries) {
103
+ const filePath = path.join(docsBasePath, assetType, entry.name);
104
+ try {
105
+ const content = fs.readFileSync(filePath, 'utf-8');
106
+ results.set(entry.name, content);
107
+ }
108
+ catch {
109
+ }
110
+ }
111
+ return results;
112
+ };
113
+
114
+ exports.canUseLocalSource = canUseLocalSource;
115
+ exports.downloadLocalAssetFiles = downloadLocalAssetFiles;
116
+ exports.expandLocalDirectoryEntries = expandLocalDirectoryEntries;
117
+ exports.fetchLocalAssetFiles = fetchLocalAssetFiles;
118
+ exports.fetchLocalDirectoryContents = fetchLocalDirectoryContents;
@@ -0,0 +1,33 @@
1
+ import type { AssetType, GitHubEntry } from '../utils/types.js';
2
+ export interface LocalSourceResult {
3
+ available: boolean;
4
+ docsPath?: string;
5
+ reason?: string;
6
+ }
7
+ /**
8
+ * Check if local docs source is available in node_modules.
9
+ * Returns available=true if:
10
+ * 1. node_modules/<packageName>/<assetPath> directory exists
11
+ * 2. Installed version matches requestedVersion
12
+ */
13
+ export declare const canUseLocalSource: (packageName: string, requestedVersion: string, assetPath: string, cwd: string) => LocalSourceResult;
14
+ /**
15
+ * Read directory contents and return .md files and subdirs in GitHubEntry format.
16
+ * Returns null if the directory doesn't exist.
17
+ */
18
+ export declare const fetchLocalDirectoryContents: (dirPath: string) => GitHubEntry[] | null;
19
+ /**
20
+ * Recursively expand directory entries into flat file entries with path prefixes.
21
+ * Mirrors github.ts expandDirectoryEntries behaviour but reads from local filesystem.
22
+ */
23
+ export declare const expandLocalDirectoryEntries: (dirPath: string, entries: GitHubEntry[], prefix?: string) => GitHubEntry[];
24
+ /**
25
+ * Fetch asset files from local filesystem.
26
+ * Mirrors github.ts fetchAssetFiles but reads from node_modules instead of GitHub API.
27
+ */
28
+ export declare const fetchLocalAssetFiles: (docsBasePath: string, assetTypes: string[]) => Promise<Record<string, GitHubEntry[]>>;
29
+ /**
30
+ * Read file contents from local filesystem for the given entries.
31
+ * Mirrors github.ts downloadAssetFiles but reads local files instead of HTTP requests.
32
+ */
33
+ export declare const downloadLocalAssetFiles: (docsBasePath: string, assetType: AssetType, entries: GitHubEntry[]) => Promise<Map<string, string>>;
@@ -0,0 +1,112 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const canUseLocalSource = (packageName, requestedVersion, assetPath, cwd) => {
5
+ const docsPath = join(cwd, 'node_modules', packageName, assetPath);
6
+ if (!existsSync(docsPath)) {
7
+ return {
8
+ available: false,
9
+ reason: `Local docs path not found: ${docsPath}`,
10
+ };
11
+ }
12
+ try {
13
+ const pkgJsonPath = join(cwd, 'node_modules', packageName, 'package.json');
14
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
15
+ if (pkgJson.version !== requestedVersion) {
16
+ return {
17
+ available: false,
18
+ reason: `Version mismatch: installed=${pkgJson.version}, requested=${requestedVersion}`,
19
+ };
20
+ }
21
+ }
22
+ catch {
23
+ return {
24
+ available: false,
25
+ reason: 'Failed to read package.json from node_modules',
26
+ };
27
+ }
28
+ return { available: true, docsPath };
29
+ };
30
+ const fetchLocalDirectoryContents = (dirPath) => {
31
+ if (!existsSync(dirPath))
32
+ return null;
33
+ try {
34
+ const entries = readdirSync(dirPath);
35
+ const result = [];
36
+ for (const name of entries) {
37
+ const fullPath = join(dirPath, name);
38
+ const stat = statSync(fullPath);
39
+ if (stat.isDirectory()) {
40
+ result.push({
41
+ name,
42
+ path: fullPath,
43
+ type: 'dir',
44
+ download_url: null,
45
+ sha: '',
46
+ });
47
+ }
48
+ else if (stat.isFile() && name.endsWith('.md')) {
49
+ result.push({
50
+ name,
51
+ path: fullPath,
52
+ type: 'file',
53
+ download_url: null,
54
+ sha: '',
55
+ });
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ };
64
+ const expandLocalDirectoryEntries = (dirPath, entries, prefix = '') => {
65
+ const result = [];
66
+ for (const entry of entries) {
67
+ const entryPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
68
+ if (entry.type === 'file') {
69
+ result.push({
70
+ ...entry,
71
+ name: prefix ? entryPrefix : entry.name,
72
+ });
73
+ }
74
+ else if (entry.type === 'dir') {
75
+ const subDirPath = join(dirPath, entry.name);
76
+ const subEntries = fetchLocalDirectoryContents(subDirPath);
77
+ if (subEntries) {
78
+ const expanded = expandLocalDirectoryEntries(subDirPath, subEntries, entryPrefix);
79
+ result.push(...expanded);
80
+ }
81
+ }
82
+ }
83
+ return result;
84
+ };
85
+ const fetchLocalAssetFiles = async (docsBasePath, assetTypes) => {
86
+ const assetFiles = {};
87
+ for (const assetType of assetTypes) {
88
+ const assetDirPath = join(docsBasePath, assetType);
89
+ const entries = fetchLocalDirectoryContents(assetDirPath);
90
+ if (!entries) {
91
+ assetFiles[assetType] = [];
92
+ continue;
93
+ }
94
+ assetFiles[assetType] = expandLocalDirectoryEntries(assetDirPath, entries);
95
+ }
96
+ return assetFiles;
97
+ };
98
+ const downloadLocalAssetFiles = async (docsBasePath, assetType, entries) => {
99
+ const results = new Map();
100
+ for (const entry of entries) {
101
+ const filePath = join(docsBasePath, assetType, entry.name);
102
+ try {
103
+ const content = readFileSync(filePath, 'utf-8');
104
+ results.set(entry.name, content);
105
+ }
106
+ catch {
107
+ }
108
+ }
109
+ return results;
110
+ };
111
+
112
+ export { canUseLocalSource, downloadLocalAssetFiles, expandLocalDirectoryEntries, fetchLocalAssetFiles, fetchLocalDirectoryContents };
@@ -2,8 +2,8 @@
2
2
 
3
3
  var fs = require('node:fs');
4
4
  var path = require('node:path');
5
- var _package = require('../utils/package.cjs');
6
5
  var nameTransform = require('../utils/nameTransform.cjs');
6
+ var _package = require('../utils/package.cjs');
7
7
  var constants = require('./constants.cjs');
8
8
  var github = require('./github.cjs');
9
9
 
@@ -65,6 +65,24 @@ async function scanRemoteAssets(packageName, ref) {
65
65
  if (!pkgInfo || !pkgInfo.claude?.assetPath) {
66
66
  throw new Error(`Package ${packageName} has no claude.assetPath configured`);
67
67
  }
68
+ const assetBasePath = pkgInfo.claude.assetPath;
69
+ if (!ref) {
70
+ const localDocsPath = path.join(nodeModulesPath, assetBasePath);
71
+ if (fs.existsSync(localDocsPath)) {
72
+ const trees = [];
73
+ for (const assetType of constants.DEFAULT_ASSET_TYPES) {
74
+ const assetDir = path.join(localDocsPath, assetType);
75
+ if (!fs.existsSync(assetDir))
76
+ continue;
77
+ const tree = buildTreeFromLocalDir(assetType, assetDir, assetType);
78
+ if (tree.children && tree.children.length > 0) {
79
+ trees.push(tree);
80
+ }
81
+ }
82
+ if (trees.length > 0)
83
+ return trees;
84
+ }
85
+ }
68
86
  if (!pkgInfo.repository) {
69
87
  throw new Error(`Package ${packageName} has no repository field`);
70
88
  }
@@ -72,7 +90,6 @@ async function scanRemoteAssets(packageName, ref) {
72
90
  if (!repoInfo) {
73
91
  throw new Error(`Invalid GitHub repository URL in package ${packageName}`);
74
92
  }
75
- const assetBasePath = pkgInfo.claude.assetPath;
76
93
  const tag = ref ?? 'HEAD';
77
94
  const trees = [];
78
95
  for (const assetType of constants.DEFAULT_ASSET_TYPES) {
@@ -198,9 +215,7 @@ function buildTreeFromGitHubEntries(label, entries, basePath, dirContentsMap) {
198
215
  }
199
216
  else if (entry.type === 'dir') {
200
217
  const dirEntries = dirContentsMap?.get(entry.name);
201
- const hasSkillMd = dirEntries
202
- ? isDirectorySkill(dirEntries)
203
- : false;
218
+ const hasSkillMd = dirEntries ? isDirectorySkill(dirEntries) : false;
204
219
  if (hasSkillMd) {
205
220
  const skillPath = `${basePath}/${entry.name}`;
206
221
  const internalChildren = dirEntries
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { parseGitHubRepo } from '../utils/package.mjs';
4
3
  import { toFlatFileName } from '../utils/nameTransform.mjs';
4
+ import { parseGitHubRepo } from '../utils/package.mjs';
5
5
  import { DEFAULT_ASSET_TYPES } from './constants.mjs';
6
6
  import { fetchDirectoryContents } from './github.mjs';
7
7
 
@@ -63,6 +63,24 @@ async function scanRemoteAssets(packageName, ref) {
63
63
  if (!pkgInfo || !pkgInfo.claude?.assetPath) {
64
64
  throw new Error(`Package ${packageName} has no claude.assetPath configured`);
65
65
  }
66
+ const assetBasePath = pkgInfo.claude.assetPath;
67
+ if (!ref) {
68
+ const localDocsPath = join(nodeModulesPath, assetBasePath);
69
+ if (existsSync(localDocsPath)) {
70
+ const trees = [];
71
+ for (const assetType of DEFAULT_ASSET_TYPES) {
72
+ const assetDir = join(localDocsPath, assetType);
73
+ if (!existsSync(assetDir))
74
+ continue;
75
+ const tree = buildTreeFromLocalDir(assetType, assetDir, assetType);
76
+ if (tree.children && tree.children.length > 0) {
77
+ trees.push(tree);
78
+ }
79
+ }
80
+ if (trees.length > 0)
81
+ return trees;
82
+ }
83
+ }
66
84
  if (!pkgInfo.repository) {
67
85
  throw new Error(`Package ${packageName} has no repository field`);
68
86
  }
@@ -70,7 +88,6 @@ async function scanRemoteAssets(packageName, ref) {
70
88
  if (!repoInfo) {
71
89
  throw new Error(`Invalid GitHub repository URL in package ${packageName}`);
72
90
  }
73
- const assetBasePath = pkgInfo.claude.assetPath;
74
91
  const tag = ref ?? 'HEAD';
75
92
  const trees = [];
76
93
  for (const assetType of DEFAULT_ASSET_TYPES) {
@@ -196,9 +213,7 @@ function buildTreeFromGitHubEntries(label, entries, basePath, dirContentsMap) {
196
213
  }
197
214
  else if (entry.type === 'dir') {
198
215
  const dirEntries = dirContentsMap?.get(entry.name);
199
- const hasSkillMd = dirEntries
200
- ? isDirectorySkill(dirEntries)
201
- : false;
216
+ const hasSkillMd = dirEntries ? isDirectorySkill(dirEntries) : false;
202
217
  if (hasSkillMd) {
203
218
  const skillPath = `${basePath}/${entry.name}`;
204
219
  const internalChildren = dirEntries
@@ -5,10 +5,11 @@ var nameTransform = require('../utils/nameTransform.cjs');
5
5
  var _package = require('../utils/package.cjs');
6
6
  var packageName = require('../utils/packageName.cjs');
7
7
  var paths = require('../utils/paths.cjs');
8
+ var constants = require('./constants.cjs');
8
9
  var filesystem = require('./filesystem.cjs');
9
10
  var github = require('./github.cjs');
11
+ var localSource = require('./localSource.cjs');
10
12
  var syncMeta = require('./syncMeta.cjs');
11
- var constants = require('./constants.cjs');
12
13
  var assetStructure = require('./assetStructure.cjs');
13
14
 
14
15
  function groupEntriesIntoSkillUnits(entries, prefix) {
@@ -67,14 +68,6 @@ const syncPackage = async (packageName$1, options, cwd = process.cwd(), exclusio
67
68
  reason: 'Package does not have claude.assetPath in package.json',
68
69
  };
69
70
  const repoInfo = _package.parseGitHubRepo(packageInfo.repository);
70
- if (!repoInfo) {
71
- return {
72
- packageName: packageName$1,
73
- success: false,
74
- skipped: true,
75
- reason: 'Unable to parse GitHub repository URL',
76
- };
77
- }
78
71
  const useFlat = options.flat !== false;
79
72
  if (useFlat) {
80
73
  const prefix = packageName.packageNameToPrefix(packageName$1);
@@ -88,11 +81,32 @@ const syncPackage = async (packageName$1, options, cwd = process.cwd(), exclusio
88
81
  reason: `Already synced at version ${packageInfo.version}`,
89
82
  };
90
83
  }
91
- const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
92
84
  const assetPath = _package.buildAssetPath(packageInfo.claude.assetPath);
93
- logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
85
+ const useLocalSource = options.ref
86
+ ? { available: false }
87
+ : localSource.canUseLocalSource(packageName$1, packageInfo.version, assetPath, cwd);
94
88
  const assetTypes = _package.getAssetTypes(packageInfo.claude);
95
- const assetFiles = await github.fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
89
+ let assetFiles;
90
+ let isLocalSource;
91
+ if (useLocalSource.available && useLocalSource.docsPath) {
92
+ isLocalSource = true;
93
+ logger.logger.step('Using', 'local docs from node_modules');
94
+ assetFiles = await localSource.fetchLocalAssetFiles(useLocalSource.docsPath, assetTypes);
95
+ }
96
+ else {
97
+ if (!repoInfo) {
98
+ return {
99
+ packageName: packageName$1,
100
+ success: false,
101
+ skipped: true,
102
+ reason: `Package ${packageName$1} has no valid GitHub repository URL for remote fetch`,
103
+ };
104
+ }
105
+ isLocalSource = false;
106
+ const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
107
+ logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
108
+ assetFiles = await github.fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
109
+ }
96
110
  let totalFiles = 0;
97
111
  for (const assetType of assetTypes) {
98
112
  totalFiles += (assetFiles[assetType] || []).length;
@@ -189,7 +203,14 @@ const syncPackage = async (packageName$1, options, cwd = process.cwd(), exclusio
189
203
  continue;
190
204
  const structure = assetStructure.getAssetStructure(assetType, packageInfo.claude);
191
205
  logger.logger.step('Downloading', assetType);
192
- const downloadedFiles = await github.downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
206
+ let downloadedFiles;
207
+ if (isLocalSource && useLocalSource.docsPath) {
208
+ downloadedFiles = await localSource.downloadLocalAssetFiles(useLocalSource.docsPath, assetType, filteredEntries);
209
+ }
210
+ else {
211
+ const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
212
+ downloadedFiles = await github.downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
213
+ }
193
214
  if (structure === 'nested') {
194
215
  for (const [fileName, content] of downloadedFiles) {
195
216
  filesystem.writeAssetFile(destDir, packageName$1, assetType, fileName, content);
@@ -231,11 +252,32 @@ const syncPackage = async (packageName$1, options, cwd = process.cwd(), exclusio
231
252
  };
232
253
  }
233
254
  else {
234
- const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
235
255
  const assetPath = _package.buildAssetPath(packageInfo.claude.assetPath);
236
- logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
256
+ const useLocalSource = options.ref
257
+ ? { available: false }
258
+ : localSource.canUseLocalSource(packageName$1, packageInfo.version, assetPath, cwd);
237
259
  const assetTypes = _package.getAssetTypes(packageInfo.claude);
238
- const assetFiles = await github.fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
260
+ let assetFiles;
261
+ let isLocalSource;
262
+ if (useLocalSource.available && useLocalSource.docsPath) {
263
+ isLocalSource = true;
264
+ logger.logger.step('Using', 'local docs from node_modules');
265
+ assetFiles = await localSource.fetchLocalAssetFiles(useLocalSource.docsPath, assetTypes);
266
+ }
267
+ else {
268
+ if (!repoInfo) {
269
+ return {
270
+ packageName: packageName$1,
271
+ success: false,
272
+ skipped: true,
273
+ reason: `Package ${packageName$1} has no valid GitHub repository URL for remote fetch`,
274
+ };
275
+ }
276
+ isLocalSource = false;
277
+ const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
278
+ logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
279
+ assetFiles = await github.fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
280
+ }
239
281
  let totalFiles = 0;
240
282
  for (const assetType of assetTypes) {
241
283
  totalFiles += (assetFiles[assetType] || []).length;
@@ -299,7 +341,14 @@ const syncPackage = async (packageName$1, options, cwd = process.cwd(), exclusio
299
341
  if (filteredEntries.length === 0)
300
342
  continue;
301
343
  logger.logger.step('Downloading', assetType);
302
- const downloadedFiles = await github.downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
344
+ let downloadedFiles;
345
+ if (isLocalSource && useLocalSource.docsPath) {
346
+ downloadedFiles = await localSource.downloadLocalAssetFiles(useLocalSource.docsPath, assetType, filteredEntries);
347
+ }
348
+ else {
349
+ const tag = options.ref ?? _package.buildVersionTag(packageName$1, packageInfo.version);
350
+ downloadedFiles = await github.downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
351
+ }
303
352
  filesystem.cleanAssetDir(destDir, packageName$1, assetType);
304
353
  syncedFiles[assetType] = [];
305
354
  for (const [fileName, content] of downloadedFiles) {
@@ -1,12 +1,13 @@
1
1
  import { logger } from '../utils/logger.mjs';
2
2
  import { toFlatFileName } from '../utils/nameTransform.mjs';
3
- import { findGitRoot, readLocalPackageJson, readPackageJson, parseGitHubRepo, buildVersionTag, buildAssetPath, getAssetTypes } from '../utils/package.mjs';
3
+ import { findGitRoot, readLocalPackageJson, readPackageJson, parseGitHubRepo, buildAssetPath, getAssetTypes, buildVersionTag } from '../utils/package.mjs';
4
4
  import { packageNameToPrefix } from '../utils/packageName.mjs';
5
5
  import { getDestinationDir, getFlatDestinationDir } from '../utils/paths.mjs';
6
+ import { SCHEMA_VERSIONS } from './constants.mjs';
6
7
  import { cleanAssetDir, cleanFlatAssetFiles, writeAssetFile, writeFlatAssetFile, needsSync, writeSyncMeta, createSyncMeta } from './filesystem.mjs';
7
8
  import { fetchAssetFiles, downloadAssetFiles, RateLimitError } from './github.mjs';
9
+ import { canUseLocalSource, fetchLocalAssetFiles, downloadLocalAssetFiles } from './localSource.mjs';
8
10
  import { readUnifiedSyncMeta, createEmptyUnifiedMeta, needsSyncUnified, updatePackageInMeta, writeUnifiedSyncMeta } from './syncMeta.mjs';
9
- import { SCHEMA_VERSIONS } from './constants.mjs';
10
11
  import { getAssetStructure } from './assetStructure.mjs';
11
12
 
12
13
  function groupEntriesIntoSkillUnits(entries, prefix) {
@@ -65,14 +66,6 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
65
66
  reason: 'Package does not have claude.assetPath in package.json',
66
67
  };
67
68
  const repoInfo = parseGitHubRepo(packageInfo.repository);
68
- if (!repoInfo) {
69
- return {
70
- packageName,
71
- success: false,
72
- skipped: true,
73
- reason: 'Unable to parse GitHub repository URL',
74
- };
75
- }
76
69
  const useFlat = options.flat !== false;
77
70
  if (useFlat) {
78
71
  const prefix = packageNameToPrefix(packageName);
@@ -86,11 +79,32 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
86
79
  reason: `Already synced at version ${packageInfo.version}`,
87
80
  };
88
81
  }
89
- const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
90
82
  const assetPath = buildAssetPath(packageInfo.claude.assetPath);
91
- logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
83
+ const useLocalSource = options.ref
84
+ ? { available: false }
85
+ : canUseLocalSource(packageName, packageInfo.version, assetPath, cwd);
92
86
  const assetTypes = getAssetTypes(packageInfo.claude);
93
- const assetFiles = await fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
87
+ let assetFiles;
88
+ let isLocalSource;
89
+ if (useLocalSource.available && useLocalSource.docsPath) {
90
+ isLocalSource = true;
91
+ logger.step('Using', 'local docs from node_modules');
92
+ assetFiles = await fetchLocalAssetFiles(useLocalSource.docsPath, assetTypes);
93
+ }
94
+ else {
95
+ if (!repoInfo) {
96
+ return {
97
+ packageName,
98
+ success: false,
99
+ skipped: true,
100
+ reason: `Package ${packageName} has no valid GitHub repository URL for remote fetch`,
101
+ };
102
+ }
103
+ isLocalSource = false;
104
+ const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
105
+ logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
106
+ assetFiles = await fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
107
+ }
94
108
  let totalFiles = 0;
95
109
  for (const assetType of assetTypes) {
96
110
  totalFiles += (assetFiles[assetType] || []).length;
@@ -187,7 +201,14 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
187
201
  continue;
188
202
  const structure = getAssetStructure(assetType, packageInfo.claude);
189
203
  logger.step('Downloading', assetType);
190
- const downloadedFiles = await downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
204
+ let downloadedFiles;
205
+ if (isLocalSource && useLocalSource.docsPath) {
206
+ downloadedFiles = await downloadLocalAssetFiles(useLocalSource.docsPath, assetType, filteredEntries);
207
+ }
208
+ else {
209
+ const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
210
+ downloadedFiles = await downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
211
+ }
191
212
  if (structure === 'nested') {
192
213
  for (const [fileName, content] of downloadedFiles) {
193
214
  writeAssetFile(destDir, packageName, assetType, fileName, content);
@@ -229,11 +250,32 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
229
250
  };
230
251
  }
231
252
  else {
232
- const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
233
253
  const assetPath = buildAssetPath(packageInfo.claude.assetPath);
234
- logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
254
+ const useLocalSource = options.ref
255
+ ? { available: false }
256
+ : canUseLocalSource(packageName, packageInfo.version, assetPath, cwd);
235
257
  const assetTypes = getAssetTypes(packageInfo.claude);
236
- const assetFiles = await fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
258
+ let assetFiles;
259
+ let isLocalSource;
260
+ if (useLocalSource.available && useLocalSource.docsPath) {
261
+ isLocalSource = true;
262
+ logger.step('Using', 'local docs from node_modules');
263
+ assetFiles = await fetchLocalAssetFiles(useLocalSource.docsPath, assetTypes);
264
+ }
265
+ else {
266
+ if (!repoInfo) {
267
+ return {
268
+ packageName,
269
+ success: false,
270
+ skipped: true,
271
+ reason: `Package ${packageName} has no valid GitHub repository URL for remote fetch`,
272
+ };
273
+ }
274
+ isLocalSource = false;
275
+ const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
276
+ logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
277
+ assetFiles = await fetchAssetFiles(repoInfo, assetPath, tag, assetTypes);
278
+ }
237
279
  let totalFiles = 0;
238
280
  for (const assetType of assetTypes) {
239
281
  totalFiles += (assetFiles[assetType] || []).length;
@@ -297,7 +339,14 @@ const syncPackage = async (packageName, options, cwd = process.cwd(), exclusions
297
339
  if (filteredEntries.length === 0)
298
340
  continue;
299
341
  logger.step('Downloading', assetType);
300
- const downloadedFiles = await downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
342
+ let downloadedFiles;
343
+ if (isLocalSource && useLocalSource.docsPath) {
344
+ downloadedFiles = await downloadLocalAssetFiles(useLocalSource.docsPath, assetType, filteredEntries);
345
+ }
346
+ else {
347
+ const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
348
+ downloadedFiles = await downloadAssetFiles(repoInfo, assetPath, assetType, filteredEntries, tag);
349
+ }
301
350
  cleanAssetDir(destDir, packageName, assetType);
302
351
  syncedFiles[assetType] = [];
303
352
  for (const [fileName, content] of downloadedFiles) {
@@ -143,7 +143,9 @@ function needsSkillUnitMigration(meta) {
143
143
  !('isDirectory' in first)) {
144
144
  return true;
145
145
  }
146
- if (typeof first === 'object' && first !== null && 'isDirectory' in first) {
146
+ if (typeof first === 'object' &&
147
+ first !== null &&
148
+ 'isDirectory' in first) {
147
149
  return false;
148
150
  }
149
151
  }
@@ -191,7 +193,10 @@ function migrateToSkillUnitSchema(meta) {
191
193
  ? mapping.transformed.substring(0, transformedSlashIndex)
192
194
  : mapping.transformed;
193
195
  if (!groupedByDir.has(dirName)) {
194
- groupedByDir.set(dirName, { transformed: transformedDir, internalFiles: [] });
196
+ groupedByDir.set(dirName, {
197
+ transformed: transformedDir,
198
+ internalFiles: [],
199
+ });
195
200
  }
196
201
  groupedByDir.get(dirName).internalFiles.push(internalFile);
197
202
  }
@@ -2,7 +2,7 @@ import type { PackageSyncInfo, SkillUnit, UnifiedSyncMeta } from '../utils/types
2
2
  /**
3
3
  * Schema version for the unified metadata format
4
4
  */
5
- export declare const SCHEMA_VERSION: "0.0.5";
5
+ export declare const SCHEMA_VERSION: "0.1.0";
6
6
  /**
7
7
  * Read unified sync metadata from .claude/.sync-meta.json
8
8
  *
@@ -141,7 +141,9 @@ function needsSkillUnitMigration(meta) {
141
141
  !('isDirectory' in first)) {
142
142
  return true;
143
143
  }
144
- if (typeof first === 'object' && first !== null && 'isDirectory' in first) {
144
+ if (typeof first === 'object' &&
145
+ first !== null &&
146
+ 'isDirectory' in first) {
145
147
  return false;
146
148
  }
147
149
  }
@@ -189,7 +191,10 @@ function migrateToSkillUnitSchema(meta) {
189
191
  ? mapping.transformed.substring(0, transformedSlashIndex)
190
192
  : mapping.transformed;
191
193
  if (!groupedByDir.has(dirName)) {
192
- groupedByDir.set(dirName, { transformed: transformedDir, internalFiles: [] });
194
+ groupedByDir.set(dirName, {
195
+ transformed: transformedDir,
196
+ internalFiles: [],
197
+ });
193
198
  }
194
199
  groupedByDir.get(dirName).internalFiles.push(internalFile);
195
200
  }
package/dist/version.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const VERSION = '0.0.5';
3
+ const VERSION = '0.1.0';
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.5";
5
+ export declare const VERSION = "0.1.0";
package/dist/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
- const VERSION = '0.0.5';
1
+ const VERSION = '0.1.0';
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.5",
3
+ "version": "0.1.0",
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",
@@ -53,7 +53,7 @@
53
53
  "version:patch": "yarn version patch"
54
54
  },
55
55
  "dependencies": {
56
- "@winglet/react-utils": "^0.10.0",
56
+ "@winglet/react-utils": "^0.11.0",
57
57
  "commander": "^12.1.0",
58
58
  "ink": "^6.6.0",
59
59
  "ink-spinner": "^5.0.0",