@slats/claude-assets-sync 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/@aileron/declare/function.d.ts +16 -0
- package/dist/@aileron/declare/index.d.ts +4 -0
- package/dist/@aileron/declare/object.d.ts +11 -0
- package/dist/@aileron/declare/unit.d.ts +19 -0
- package/dist/@aileron/declare/utility.d.ts +45 -0
- package/dist/cli/index.cjs +56 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.mjs +53 -0
- package/dist/core/filesystem.cjs +81 -0
- package/dist/core/filesystem.d.ts +73 -0
- package/dist/core/filesystem.mjs +70 -0
- package/dist/core/github.cjs +90 -0
- package/dist/core/github.d.ts +50 -0
- package/dist/core/github.mjs +83 -0
- package/dist/core/sync.cjs +150 -0
- package/dist/core/sync.d.ts +17 -0
- package/dist/core/sync.mjs +147 -0
- package/dist/index.cjs +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +11 -0
- package/dist/utils/logger.cjs +67 -0
- package/dist/utils/logger.d.ts +57 -0
- package/dist/utils/logger.mjs +65 -0
- package/dist/utils/package.cjs +143 -0
- package/dist/utils/package.d.ts +66 -0
- package/dist/utils/package.mjs +133 -0
- package/dist/utils/types.d.ts +88 -0
- package/package.json +59 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class RateLimitError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable to increase the limit.');
|
|
6
|
+
this.name = 'RateLimitError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
class NotFoundError extends Error {
|
|
10
|
+
constructor(resource) {
|
|
11
|
+
super(`GitHub resource not found: ${resource}`);
|
|
12
|
+
this.name = 'NotFoundError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const getHeaders = () => {
|
|
16
|
+
const headers = {
|
|
17
|
+
Accept: 'application/vnd.github.v3+json',
|
|
18
|
+
'User-Agent': 'claude-assets-sync',
|
|
19
|
+
};
|
|
20
|
+
const token = process.env.GITHUB_TOKEN;
|
|
21
|
+
if (token)
|
|
22
|
+
headers.Authorization = `Bearer ${token}`;
|
|
23
|
+
return headers;
|
|
24
|
+
};
|
|
25
|
+
const fetchDirectoryContents = async (repoInfo, path, tag) => {
|
|
26
|
+
const url = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/contents/${path}?ref=${encodeURIComponent(tag)}`;
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
headers: getHeaders(),
|
|
29
|
+
});
|
|
30
|
+
if (response.status === 403) {
|
|
31
|
+
const remaining = response.headers.get('x-ratelimit-remaining');
|
|
32
|
+
if (remaining === '0')
|
|
33
|
+
throw new RateLimitError();
|
|
34
|
+
}
|
|
35
|
+
if (response.status === 404)
|
|
36
|
+
return null;
|
|
37
|
+
if (!response.ok)
|
|
38
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
return data.filter((entry) => entry.type === 'file' && entry.name.endsWith('.md'));
|
|
41
|
+
};
|
|
42
|
+
const fetchAssetFiles = async (repoInfo, assetPath, tag) => {
|
|
43
|
+
const basePath = repoInfo.directory
|
|
44
|
+
? `${repoInfo.directory}/${assetPath}`
|
|
45
|
+
: assetPath;
|
|
46
|
+
const commandsPath = `${basePath}/commands`;
|
|
47
|
+
const skillsPath = `${basePath}/skills`;
|
|
48
|
+
const [commandsEntries, skillsEntries] = await Promise.all([
|
|
49
|
+
fetchDirectoryContents(repoInfo, commandsPath, tag),
|
|
50
|
+
fetchDirectoryContents(repoInfo, skillsPath, tag),
|
|
51
|
+
]);
|
|
52
|
+
return {
|
|
53
|
+
commands: commandsEntries || [],
|
|
54
|
+
skills: skillsEntries || [],
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const downloadFile = async (repoInfo, filePath, tag) => {
|
|
58
|
+
const url = `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/${encodeURIComponent(tag)}/${filePath}`;
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
headers: {
|
|
61
|
+
'User-Agent': 'claude-assets-sync',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (response.status === 404) {
|
|
65
|
+
throw new NotFoundError(filePath);
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
|
|
69
|
+
}
|
|
70
|
+
return response.text();
|
|
71
|
+
};
|
|
72
|
+
const downloadAssetFiles = async (repoInfo, assetPath, assetType, entries, tag) => {
|
|
73
|
+
const basePath = repoInfo.directory
|
|
74
|
+
? `${repoInfo.directory}/${assetPath}/${assetType}`
|
|
75
|
+
: `${assetPath}/${assetType}`;
|
|
76
|
+
const results = new Map();
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const filePath = `${basePath}/${entry.name}`;
|
|
79
|
+
const content = await downloadFile(repoInfo, filePath, tag);
|
|
80
|
+
results.set(entry.name, content);
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
exports.NotFoundError = NotFoundError;
|
|
86
|
+
exports.RateLimitError = RateLimitError;
|
|
87
|
+
exports.downloadAssetFiles = downloadAssetFiles;
|
|
88
|
+
exports.downloadFile = downloadFile;
|
|
89
|
+
exports.fetchAssetFiles = fetchAssetFiles;
|
|
90
|
+
exports.fetchDirectoryContents = fetchDirectoryContents;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AssetType, GitHubEntry, GitHubRepoInfo } from '../utils/types';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when GitHub API rate limit is exceeded
|
|
4
|
+
*/
|
|
5
|
+
export declare class RateLimitError extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when GitHub resource is not found
|
|
10
|
+
*/
|
|
11
|
+
export declare class NotFoundError extends Error {
|
|
12
|
+
constructor(resource: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fetch directory contents from GitHub API
|
|
16
|
+
* @param repoInfo - GitHub repository information
|
|
17
|
+
* @param path - Path to the directory
|
|
18
|
+
* @param tag - Git tag or ref to fetch from
|
|
19
|
+
* @returns Array of GitHubEntry or null if directory doesn't exist
|
|
20
|
+
*/
|
|
21
|
+
export declare const fetchDirectoryContents: (repoInfo: GitHubRepoInfo, path: string, tag: string) => Promise<GitHubEntry[] | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Fetch asset files (commands and skills) from GitHub
|
|
24
|
+
* @param repoInfo - GitHub repository information
|
|
25
|
+
* @param assetPath - Base path to Claude assets (e.g., "docs/claude")
|
|
26
|
+
* @param tag - Git tag or ref to fetch from
|
|
27
|
+
* @returns Object with commands and skills file lists
|
|
28
|
+
*/
|
|
29
|
+
export declare const fetchAssetFiles: (repoInfo: GitHubRepoInfo, assetPath: string, tag: string) => Promise<{
|
|
30
|
+
commands: GitHubEntry[];
|
|
31
|
+
skills: GitHubEntry[];
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Download file content from raw.githubusercontent.com
|
|
35
|
+
* @param repoInfo - GitHub repository information
|
|
36
|
+
* @param filePath - Full path to the file
|
|
37
|
+
* @param tag - Git tag or ref
|
|
38
|
+
* @returns File content as string
|
|
39
|
+
*/
|
|
40
|
+
export declare const downloadFile: (repoInfo: GitHubRepoInfo, filePath: string, tag: string) => Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Download multiple files from a specific asset type
|
|
43
|
+
* @param repoInfo - GitHub repository information
|
|
44
|
+
* @param assetPath - Base path to Claude assets
|
|
45
|
+
* @param assetType - Type of asset (commands or skills)
|
|
46
|
+
* @param entries - Array of GitHubEntry to download
|
|
47
|
+
* @param tag - Git tag or ref
|
|
48
|
+
* @returns Map of filename to content
|
|
49
|
+
*/
|
|
50
|
+
export declare const downloadAssetFiles: (repoInfo: GitHubRepoInfo, assetPath: string, assetType: AssetType, entries: GitHubEntry[], tag: string) => Promise<Map<string, string>>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
class RateLimitError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super('GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable to increase the limit.');
|
|
4
|
+
this.name = 'RateLimitError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
class NotFoundError extends Error {
|
|
8
|
+
constructor(resource) {
|
|
9
|
+
super(`GitHub resource not found: ${resource}`);
|
|
10
|
+
this.name = 'NotFoundError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const getHeaders = () => {
|
|
14
|
+
const headers = {
|
|
15
|
+
Accept: 'application/vnd.github.v3+json',
|
|
16
|
+
'User-Agent': 'claude-assets-sync',
|
|
17
|
+
};
|
|
18
|
+
const token = process.env.GITHUB_TOKEN;
|
|
19
|
+
if (token)
|
|
20
|
+
headers.Authorization = `Bearer ${token}`;
|
|
21
|
+
return headers;
|
|
22
|
+
};
|
|
23
|
+
const fetchDirectoryContents = async (repoInfo, path, tag) => {
|
|
24
|
+
const url = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/contents/${path}?ref=${encodeURIComponent(tag)}`;
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
headers: getHeaders(),
|
|
27
|
+
});
|
|
28
|
+
if (response.status === 403) {
|
|
29
|
+
const remaining = response.headers.get('x-ratelimit-remaining');
|
|
30
|
+
if (remaining === '0')
|
|
31
|
+
throw new RateLimitError();
|
|
32
|
+
}
|
|
33
|
+
if (response.status === 404)
|
|
34
|
+
return null;
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return data.filter((entry) => entry.type === 'file' && entry.name.endsWith('.md'));
|
|
39
|
+
};
|
|
40
|
+
const fetchAssetFiles = async (repoInfo, assetPath, tag) => {
|
|
41
|
+
const basePath = repoInfo.directory
|
|
42
|
+
? `${repoInfo.directory}/${assetPath}`
|
|
43
|
+
: assetPath;
|
|
44
|
+
const commandsPath = `${basePath}/commands`;
|
|
45
|
+
const skillsPath = `${basePath}/skills`;
|
|
46
|
+
const [commandsEntries, skillsEntries] = await Promise.all([
|
|
47
|
+
fetchDirectoryContents(repoInfo, commandsPath, tag),
|
|
48
|
+
fetchDirectoryContents(repoInfo, skillsPath, tag),
|
|
49
|
+
]);
|
|
50
|
+
return {
|
|
51
|
+
commands: commandsEntries || [],
|
|
52
|
+
skills: skillsEntries || [],
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const downloadFile = async (repoInfo, filePath, tag) => {
|
|
56
|
+
const url = `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/${encodeURIComponent(tag)}/${filePath}`;
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
headers: {
|
|
59
|
+
'User-Agent': 'claude-assets-sync',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (response.status === 404) {
|
|
63
|
+
throw new NotFoundError(filePath);
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
return response.text();
|
|
69
|
+
};
|
|
70
|
+
const downloadAssetFiles = async (repoInfo, assetPath, assetType, entries, tag) => {
|
|
71
|
+
const basePath = repoInfo.directory
|
|
72
|
+
? `${repoInfo.directory}/${assetPath}/${assetType}`
|
|
73
|
+
: `${assetPath}/${assetType}`;
|
|
74
|
+
const results = new Map();
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const filePath = `${basePath}/${entry.name}`;
|
|
77
|
+
const content = await downloadFile(repoInfo, filePath, tag);
|
|
78
|
+
results.set(entry.name, content);
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { NotFoundError, RateLimitError, downloadAssetFiles, downloadFile, fetchAssetFiles, fetchDirectoryContents };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var logger = require('../utils/logger.cjs');
|
|
4
|
+
var _package = require('../utils/package.cjs');
|
|
5
|
+
var filesystem = require('./filesystem.cjs');
|
|
6
|
+
var github = require('./github.cjs');
|
|
7
|
+
|
|
8
|
+
const syncPackage = async (packageName, options, cwd = process.cwd(), outputDir) => {
|
|
9
|
+
logger.logger.packageStart(packageName);
|
|
10
|
+
try {
|
|
11
|
+
const destDir = outputDir ?? _package.findGitRoot(cwd) ?? cwd;
|
|
12
|
+
const packageInfo = options.local
|
|
13
|
+
? _package.readLocalPackageJson(packageName, cwd)
|
|
14
|
+
: _package.readPackageJson(packageName, cwd);
|
|
15
|
+
if (!packageInfo) {
|
|
16
|
+
const location = options.local ? 'workspace' : 'node_modules';
|
|
17
|
+
return {
|
|
18
|
+
packageName,
|
|
19
|
+
success: false,
|
|
20
|
+
skipped: true,
|
|
21
|
+
reason: `Package not found in ${location}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (!packageInfo.claude?.assetPath)
|
|
25
|
+
return {
|
|
26
|
+
packageName,
|
|
27
|
+
success: false,
|
|
28
|
+
skipped: true,
|
|
29
|
+
reason: 'Package does not have claude.assetPath in package.json',
|
|
30
|
+
};
|
|
31
|
+
const repoInfo = _package.parseGitHubRepo(packageInfo.repository);
|
|
32
|
+
if (!repoInfo) {
|
|
33
|
+
return {
|
|
34
|
+
packageName,
|
|
35
|
+
success: false,
|
|
36
|
+
skipped: true,
|
|
37
|
+
reason: 'Unable to parse GitHub repository URL',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!options.force &&
|
|
41
|
+
!filesystem.needsSync(destDir, packageName, packageInfo.version)) {
|
|
42
|
+
return {
|
|
43
|
+
packageName,
|
|
44
|
+
success: true,
|
|
45
|
+
skipped: true,
|
|
46
|
+
reason: `Already synced at version ${packageInfo.version}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const tag = options.ref ?? _package.buildVersionTag(packageName, packageInfo.version);
|
|
50
|
+
const assetPath = _package.buildAssetPath(packageInfo.claude.assetPath);
|
|
51
|
+
logger.logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
|
|
52
|
+
const { commands, skills } = await github.fetchAssetFiles(repoInfo, assetPath, tag);
|
|
53
|
+
if (commands.length === 0 && skills.length === 0)
|
|
54
|
+
return {
|
|
55
|
+
packageName,
|
|
56
|
+
success: false,
|
|
57
|
+
skipped: true,
|
|
58
|
+
reason: 'No commands or skills found in package',
|
|
59
|
+
};
|
|
60
|
+
logger.logger.step('Found', `${commands.length} commands, ${skills.length} skills`);
|
|
61
|
+
if (options.dryRun) {
|
|
62
|
+
if (commands.length > 0) {
|
|
63
|
+
logger.logger.step('Would sync commands to', filesystem.getDestinationDir(destDir, packageName, 'commands'));
|
|
64
|
+
commands.forEach((entry) => logger.logger.file('create', entry.name));
|
|
65
|
+
}
|
|
66
|
+
if (skills.length > 0) {
|
|
67
|
+
logger.logger.step('Would sync skills to', filesystem.getDestinationDir(destDir, packageName, 'skills'));
|
|
68
|
+
skills.forEach((entry) => logger.logger.file('create', entry.name));
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
packageName,
|
|
72
|
+
success: true,
|
|
73
|
+
skipped: false,
|
|
74
|
+
syncedFiles: {
|
|
75
|
+
commands: commands.map((e) => e.name),
|
|
76
|
+
skills: skills.map((e) => e.name),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const syncedFiles = {
|
|
81
|
+
commands: [],
|
|
82
|
+
skills: [],
|
|
83
|
+
};
|
|
84
|
+
if (commands.length > 0) {
|
|
85
|
+
logger.logger.step('Downloading', 'commands');
|
|
86
|
+
const commandFiles = await github.downloadAssetFiles(repoInfo, assetPath, 'commands', commands, tag);
|
|
87
|
+
filesystem.cleanAssetDir(destDir, packageName, 'commands');
|
|
88
|
+
for (const [fileName, content] of commandFiles) {
|
|
89
|
+
filesystem.writeAssetFile(destDir, packageName, 'commands', fileName, content);
|
|
90
|
+
logger.logger.file('create', fileName);
|
|
91
|
+
syncedFiles.commands.push(fileName);
|
|
92
|
+
}
|
|
93
|
+
filesystem.writeSyncMeta(destDir, packageName, 'commands', filesystem.createSyncMeta(packageInfo.version, syncedFiles.commands));
|
|
94
|
+
}
|
|
95
|
+
if (skills.length > 0) {
|
|
96
|
+
logger.logger.step('Downloading', 'skills');
|
|
97
|
+
const skillFiles = await github.downloadAssetFiles(repoInfo, assetPath, 'skills', skills, tag);
|
|
98
|
+
filesystem.cleanAssetDir(destDir, packageName, 'skills');
|
|
99
|
+
for (const [fileName, content] of skillFiles) {
|
|
100
|
+
filesystem.writeAssetFile(destDir, packageName, 'skills', fileName, content);
|
|
101
|
+
logger.logger.file('create', fileName);
|
|
102
|
+
syncedFiles.skills.push(fileName);
|
|
103
|
+
}
|
|
104
|
+
filesystem.writeSyncMeta(destDir, packageName, 'skills', filesystem.createSyncMeta(packageInfo.version, syncedFiles.skills));
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
packageName,
|
|
108
|
+
success: true,
|
|
109
|
+
skipped: false,
|
|
110
|
+
syncedFiles,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof github.RateLimitError)
|
|
115
|
+
return {
|
|
116
|
+
packageName,
|
|
117
|
+
success: false,
|
|
118
|
+
skipped: false,
|
|
119
|
+
reason: error.message,
|
|
120
|
+
};
|
|
121
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
122
|
+
return {
|
|
123
|
+
packageName,
|
|
124
|
+
success: false,
|
|
125
|
+
skipped: false,
|
|
126
|
+
reason: message,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const syncPackages = async (packages, options, cwd = process.cwd()) => {
|
|
131
|
+
const results = [];
|
|
132
|
+
const gitRoot = _package.findGitRoot(cwd);
|
|
133
|
+
if (gitRoot)
|
|
134
|
+
logger.logger.info(`[Output] ${gitRoot}/.claude\n`);
|
|
135
|
+
for (const packageName of packages) {
|
|
136
|
+
const result = await syncPackage(packageName, options, cwd, gitRoot ?? undefined);
|
|
137
|
+
logger.logger.packageEnd(packageName, result);
|
|
138
|
+
results.push(result);
|
|
139
|
+
}
|
|
140
|
+
const summary = {
|
|
141
|
+
success: results.filter((r) => r.success && !r.skipped).length,
|
|
142
|
+
skipped: results.filter((r) => r.skipped).length,
|
|
143
|
+
failed: results.filter((r) => !r.success && !r.skipped).length,
|
|
144
|
+
};
|
|
145
|
+
logger.logger.summary(summary);
|
|
146
|
+
return results;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
exports.syncPackage = syncPackage;
|
|
150
|
+
exports.syncPackages = syncPackages;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CliOptions, SyncResult } from '../utils/types';
|
|
2
|
+
/**
|
|
3
|
+
* Sync Claude assets for a single package
|
|
4
|
+
* @param packageName - Package name to sync
|
|
5
|
+
* @param options - CLI options
|
|
6
|
+
* @param cwd - Current working directory
|
|
7
|
+
* @returns Sync result
|
|
8
|
+
*/
|
|
9
|
+
export declare const syncPackage: (packageName: string, options: Pick<CliOptions, "force" | "dryRun" | "local" | "ref">, cwd?: string, outputDir?: string) => Promise<SyncResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Sync Claude assets for multiple packages
|
|
12
|
+
* @param packages - List of package names to sync
|
|
13
|
+
* @param options - CLI options
|
|
14
|
+
* @param cwd - Current working directory
|
|
15
|
+
* @returns Array of sync results
|
|
16
|
+
*/
|
|
17
|
+
export declare const syncPackages: (packages: string[], options: Pick<CliOptions, "force" | "dryRun" | "local" | "ref">, cwd?: string) => Promise<SyncResult[]>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.mjs';
|
|
2
|
+
import { findGitRoot, readLocalPackageJson, readPackageJson, parseGitHubRepo, buildVersionTag, buildAssetPath } from '../utils/package.mjs';
|
|
3
|
+
import { needsSync, getDestinationDir, cleanAssetDir, writeAssetFile, writeSyncMeta, createSyncMeta } from './filesystem.mjs';
|
|
4
|
+
import { fetchAssetFiles, downloadAssetFiles, RateLimitError } from './github.mjs';
|
|
5
|
+
|
|
6
|
+
const syncPackage = async (packageName, options, cwd = process.cwd(), outputDir) => {
|
|
7
|
+
logger.packageStart(packageName);
|
|
8
|
+
try {
|
|
9
|
+
const destDir = outputDir ?? findGitRoot(cwd) ?? cwd;
|
|
10
|
+
const packageInfo = options.local
|
|
11
|
+
? readLocalPackageJson(packageName, cwd)
|
|
12
|
+
: readPackageJson(packageName, cwd);
|
|
13
|
+
if (!packageInfo) {
|
|
14
|
+
const location = options.local ? 'workspace' : 'node_modules';
|
|
15
|
+
return {
|
|
16
|
+
packageName,
|
|
17
|
+
success: false,
|
|
18
|
+
skipped: true,
|
|
19
|
+
reason: `Package not found in ${location}`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!packageInfo.claude?.assetPath)
|
|
23
|
+
return {
|
|
24
|
+
packageName,
|
|
25
|
+
success: false,
|
|
26
|
+
skipped: true,
|
|
27
|
+
reason: 'Package does not have claude.assetPath in package.json',
|
|
28
|
+
};
|
|
29
|
+
const repoInfo = parseGitHubRepo(packageInfo.repository);
|
|
30
|
+
if (!repoInfo) {
|
|
31
|
+
return {
|
|
32
|
+
packageName,
|
|
33
|
+
success: false,
|
|
34
|
+
skipped: true,
|
|
35
|
+
reason: 'Unable to parse GitHub repository URL',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (!options.force &&
|
|
39
|
+
!needsSync(destDir, packageName, packageInfo.version)) {
|
|
40
|
+
return {
|
|
41
|
+
packageName,
|
|
42
|
+
success: true,
|
|
43
|
+
skipped: true,
|
|
44
|
+
reason: `Already synced at version ${packageInfo.version}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const tag = options.ref ?? buildVersionTag(packageName, packageInfo.version);
|
|
48
|
+
const assetPath = buildAssetPath(packageInfo.claude.assetPath);
|
|
49
|
+
logger.step('Fetching', `asset list from GitHub (ref: ${tag})`);
|
|
50
|
+
const { commands, skills } = await fetchAssetFiles(repoInfo, assetPath, tag);
|
|
51
|
+
if (commands.length === 0 && skills.length === 0)
|
|
52
|
+
return {
|
|
53
|
+
packageName,
|
|
54
|
+
success: false,
|
|
55
|
+
skipped: true,
|
|
56
|
+
reason: 'No commands or skills found in package',
|
|
57
|
+
};
|
|
58
|
+
logger.step('Found', `${commands.length} commands, ${skills.length} skills`);
|
|
59
|
+
if (options.dryRun) {
|
|
60
|
+
if (commands.length > 0) {
|
|
61
|
+
logger.step('Would sync commands to', getDestinationDir(destDir, packageName, 'commands'));
|
|
62
|
+
commands.forEach((entry) => logger.file('create', entry.name));
|
|
63
|
+
}
|
|
64
|
+
if (skills.length > 0) {
|
|
65
|
+
logger.step('Would sync skills to', getDestinationDir(destDir, packageName, 'skills'));
|
|
66
|
+
skills.forEach((entry) => logger.file('create', entry.name));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
packageName,
|
|
70
|
+
success: true,
|
|
71
|
+
skipped: false,
|
|
72
|
+
syncedFiles: {
|
|
73
|
+
commands: commands.map((e) => e.name),
|
|
74
|
+
skills: skills.map((e) => e.name),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const syncedFiles = {
|
|
79
|
+
commands: [],
|
|
80
|
+
skills: [],
|
|
81
|
+
};
|
|
82
|
+
if (commands.length > 0) {
|
|
83
|
+
logger.step('Downloading', 'commands');
|
|
84
|
+
const commandFiles = await downloadAssetFiles(repoInfo, assetPath, 'commands', commands, tag);
|
|
85
|
+
cleanAssetDir(destDir, packageName, 'commands');
|
|
86
|
+
for (const [fileName, content] of commandFiles) {
|
|
87
|
+
writeAssetFile(destDir, packageName, 'commands', fileName, content);
|
|
88
|
+
logger.file('create', fileName);
|
|
89
|
+
syncedFiles.commands.push(fileName);
|
|
90
|
+
}
|
|
91
|
+
writeSyncMeta(destDir, packageName, 'commands', createSyncMeta(packageInfo.version, syncedFiles.commands));
|
|
92
|
+
}
|
|
93
|
+
if (skills.length > 0) {
|
|
94
|
+
logger.step('Downloading', 'skills');
|
|
95
|
+
const skillFiles = await downloadAssetFiles(repoInfo, assetPath, 'skills', skills, tag);
|
|
96
|
+
cleanAssetDir(destDir, packageName, 'skills');
|
|
97
|
+
for (const [fileName, content] of skillFiles) {
|
|
98
|
+
writeAssetFile(destDir, packageName, 'skills', fileName, content);
|
|
99
|
+
logger.file('create', fileName);
|
|
100
|
+
syncedFiles.skills.push(fileName);
|
|
101
|
+
}
|
|
102
|
+
writeSyncMeta(destDir, packageName, 'skills', createSyncMeta(packageInfo.version, syncedFiles.skills));
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
packageName,
|
|
106
|
+
success: true,
|
|
107
|
+
skipped: false,
|
|
108
|
+
syncedFiles,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof RateLimitError)
|
|
113
|
+
return {
|
|
114
|
+
packageName,
|
|
115
|
+
success: false,
|
|
116
|
+
skipped: false,
|
|
117
|
+
reason: error.message,
|
|
118
|
+
};
|
|
119
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
120
|
+
return {
|
|
121
|
+
packageName,
|
|
122
|
+
success: false,
|
|
123
|
+
skipped: false,
|
|
124
|
+
reason: message,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const syncPackages = async (packages, options, cwd = process.cwd()) => {
|
|
129
|
+
const results = [];
|
|
130
|
+
const gitRoot = findGitRoot(cwd);
|
|
131
|
+
if (gitRoot)
|
|
132
|
+
logger.info(`[Output] ${gitRoot}/.claude\n`);
|
|
133
|
+
for (const packageName of packages) {
|
|
134
|
+
const result = await syncPackage(packageName, options, cwd, gitRoot ?? undefined);
|
|
135
|
+
logger.packageEnd(packageName, result);
|
|
136
|
+
results.push(result);
|
|
137
|
+
}
|
|
138
|
+
const summary = {
|
|
139
|
+
success: results.filter((r) => r.success && !r.skipped).length,
|
|
140
|
+
skipped: results.filter((r) => r.skipped).length,
|
|
141
|
+
failed: results.filter((r) => !r.success && !r.skipped).length,
|
|
142
|
+
};
|
|
143
|
+
logger.summary(summary);
|
|
144
|
+
return results;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export { syncPackage, syncPackages };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var index = require('./cli/index.cjs');
|
|
5
|
+
var sync = require('./core/sync.cjs');
|
|
6
|
+
|
|
7
|
+
index.run().catch((error) => {
|
|
8
|
+
console.error('Fatal error:', error.message);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
exports.createProgram = index.createProgram;
|
|
13
|
+
exports.run = index.run;
|
|
14
|
+
exports.syncPackage = sync.syncPackage;
|
|
15
|
+
exports.syncPackages = sync.syncPackages;
|
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from './cli/index.mjs';
|
|
3
|
+
export { createProgram } from './cli/index.mjs';
|
|
4
|
+
export { syncPackage, syncPackages } from './core/sync.mjs';
|
|
5
|
+
|
|
6
|
+
run().catch((error) => {
|
|
7
|
+
console.error('Fatal error:', error.message);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export { run };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pc = require('picocolors');
|
|
4
|
+
|
|
5
|
+
const logger = {
|
|
6
|
+
info(message) {
|
|
7
|
+
console.log(pc.blue('info'), message);
|
|
8
|
+
},
|
|
9
|
+
success(message) {
|
|
10
|
+
console.log(pc.green('success'), message);
|
|
11
|
+
},
|
|
12
|
+
warn(message) {
|
|
13
|
+
console.log(pc.yellow('warn'), message);
|
|
14
|
+
},
|
|
15
|
+
error(message) {
|
|
16
|
+
console.log(pc.red('error'), message);
|
|
17
|
+
},
|
|
18
|
+
debug(message) {
|
|
19
|
+
if (process.env.VERBOSE) {
|
|
20
|
+
console.log(pc.gray('debug'), message);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
step(step, detail) {
|
|
24
|
+
const stepText = pc.cyan(`[${step}]`);
|
|
25
|
+
console.log(stepText, detail || '');
|
|
26
|
+
},
|
|
27
|
+
file(operation, path) {
|
|
28
|
+
const colors = {
|
|
29
|
+
create: pc.green,
|
|
30
|
+
update: pc.yellow,
|
|
31
|
+
skip: pc.gray,
|
|
32
|
+
};
|
|
33
|
+
const symbols = {
|
|
34
|
+
create: '+',
|
|
35
|
+
update: '~',
|
|
36
|
+
skip: '-',
|
|
37
|
+
};
|
|
38
|
+
console.log(` ${colors[operation](symbols[operation])} ${path}`);
|
|
39
|
+
},
|
|
40
|
+
packageStart(packageName) {
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(pc.bold(pc.cyan(`Syncing ${packageName}...`)));
|
|
43
|
+
},
|
|
44
|
+
packageEnd(_packageName, result) {
|
|
45
|
+
if (result.skipped)
|
|
46
|
+
console.log(pc.gray(` Skipped: ${result.reason || 'Unknown reason'}`));
|
|
47
|
+
else if (result.success)
|
|
48
|
+
console.log(pc.green(` Completed successfully`));
|
|
49
|
+
else
|
|
50
|
+
console.log(pc.red(` Failed: ${result.reason || 'Unknown error'}`));
|
|
51
|
+
},
|
|
52
|
+
summary(results) {
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(pc.bold('Summary:'));
|
|
55
|
+
console.log(` ${pc.green('Success:')} ${results.success}`);
|
|
56
|
+
console.log(` ${pc.gray('Skipped:')} ${results.skipped}`);
|
|
57
|
+
if (results.failed > 0)
|
|
58
|
+
console.log(` ${pc.red('Failed:')} ${results.failed}`);
|
|
59
|
+
},
|
|
60
|
+
dryRunNotice() {
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(pc.yellow(pc.bold('[DRY RUN] No files will be created or modified.')));
|
|
63
|
+
console.log();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
exports.logger = logger;
|