cskit-cli 1.0.0 → 1.0.3
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/package.json +3 -3
- package/src/commands/init.js +217 -63
- package/src/commands/status.js +2 -1
- package/src/commands/update.js +3 -3
- package/src/index.js +1 -0
- package/src/lib/github.js +94 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cskit-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Content Suite Kit CLI - Download and manage CSK skills from private repository",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/tohaitrieu/
|
|
28
|
+
"url": "git+https://github.com/tohaitrieu/content-suite-kit.git"
|
|
29
29
|
},
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/tohaitrieu/
|
|
31
|
+
"url": "https://github.com/tohaitrieu/content-suite-kit/issues"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://cskit.net",
|
|
34
34
|
"dependencies": {
|
package/src/commands/init.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Init Command
|
|
5
5
|
*
|
|
6
|
-
* Downloads CSK from private GitHub repository
|
|
7
|
-
*
|
|
6
|
+
* Downloads CSK from private GitHub repository.
|
|
7
|
+
* Features: version selection, progress bar, Python setup.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
@@ -12,8 +12,16 @@ const path = require('path');
|
|
|
12
12
|
const chalk = require('chalk');
|
|
13
13
|
const ora = require('ora');
|
|
14
14
|
const crypto = require('crypto');
|
|
15
|
+
const inquirer = require('inquirer');
|
|
16
|
+
const { execSync, spawn } = require('child_process');
|
|
15
17
|
const { getToken } = require('../lib/keychain');
|
|
16
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
verifyAccess,
|
|
20
|
+
getRepoTreeFromRef,
|
|
21
|
+
downloadFileFromRef,
|
|
22
|
+
getAllReleases,
|
|
23
|
+
getLatestRelease
|
|
24
|
+
} = require('../lib/github');
|
|
17
25
|
const {
|
|
18
26
|
isProtected,
|
|
19
27
|
shouldExclude,
|
|
@@ -23,9 +31,131 @@ const {
|
|
|
23
31
|
ensureDir
|
|
24
32
|
} = require('../lib/merge');
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Format date for display
|
|
36
|
+
*/
|
|
37
|
+
function formatDate(isoDate) {
|
|
38
|
+
const d = new Date(isoDate);
|
|
39
|
+
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create progress bar string
|
|
44
|
+
*/
|
|
45
|
+
function progressBar(current, total, width = 30) {
|
|
46
|
+
const percent = Math.round((current / total) * 100);
|
|
47
|
+
const filled = Math.round((current / total) * width);
|
|
48
|
+
const empty = width - filled;
|
|
49
|
+
const bar = chalk.green('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
50
|
+
return `${bar} ${percent}%`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Setup Python virtual environment
|
|
55
|
+
*/
|
|
56
|
+
async function setupPythonEnv(projectDir, spinner) {
|
|
57
|
+
const libPythonDir = path.join(projectDir, 'lib', 'python');
|
|
58
|
+
const requirementsFile = path.join(libPythonDir, 'requirements.txt');
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(requirementsFile)) {
|
|
61
|
+
return { success: true, skipped: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
spinner.start('Setting up Python environment...');
|
|
65
|
+
|
|
66
|
+
const venvPath = path.join(libPythonDir, '.venv');
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Check if Python 3 is available
|
|
70
|
+
try {
|
|
71
|
+
execSync('python3 --version', { stdio: 'pipe' });
|
|
72
|
+
} catch {
|
|
73
|
+
spinner.warn('Python 3 not found, skipping venv setup');
|
|
74
|
+
return { success: true, skipped: true, reason: 'python3 not found' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create venv if not exists
|
|
78
|
+
if (!fs.existsSync(venvPath)) {
|
|
79
|
+
spinner.text = 'Creating virtual environment...';
|
|
80
|
+
execSync(`python3 -m venv "${venvPath}"`, { stdio: 'pipe', cwd: libPythonDir });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Install requirements
|
|
84
|
+
spinner.text = 'Installing Python packages...';
|
|
85
|
+
const pipPath = path.join(venvPath, 'bin', 'pip');
|
|
86
|
+
execSync(`"${pipPath}" install -r requirements.txt -q`, {
|
|
87
|
+
stdio: 'pipe',
|
|
88
|
+
cwd: libPythonDir
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
spinner.succeed('Python environment ready');
|
|
92
|
+
return { success: true };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
spinner.warn(`Python setup failed: ${error.message}`);
|
|
95
|
+
return { success: false, error: error.message };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Setup CSK in Claude Code directories
|
|
101
|
+
*/
|
|
102
|
+
async function setupCommands(projectDir, spinner) {
|
|
103
|
+
spinner.start('Setting up CSK for Claude Code...');
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
107
|
+
ensureDir(claudeDir);
|
|
108
|
+
|
|
109
|
+
// Map: source → destination
|
|
110
|
+
const mappings = [
|
|
111
|
+
{ src: 'src/commands', dest: '.claude/commands' },
|
|
112
|
+
{ src: 'core', dest: '.claude/skills/csk/core' },
|
|
113
|
+
{ src: 'domains', dest: '.claude/skills/csk/domains' },
|
|
114
|
+
{ src: 'industries', dest: '.claude/skills/csk/industries' },
|
|
115
|
+
{ src: 'lib', dest: '.claude/skills/csk/lib' }
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
let copied = 0;
|
|
119
|
+
for (const { src, dest } of mappings) {
|
|
120
|
+
const srcPath = path.join(projectDir, src);
|
|
121
|
+
const destPath = path.join(projectDir, dest);
|
|
122
|
+
|
|
123
|
+
if (fs.existsSync(srcPath)) {
|
|
124
|
+
ensureDir(destPath);
|
|
125
|
+
copyDirRecursive(srcPath, destPath);
|
|
126
|
+
copied++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
spinner.succeed(`CSK installed to .claude/ (${copied} modules)`);
|
|
131
|
+
return { success: true };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
spinner.warn(`CSK setup failed: ${error.message}`);
|
|
134
|
+
return { success: false, error: error.message };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Recursively copy directory
|
|
140
|
+
*/
|
|
141
|
+
function copyDirRecursive(src, dest) {
|
|
142
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
143
|
+
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const srcPath = path.join(src, entry.name);
|
|
146
|
+
const destPath = path.join(dest, entry.name);
|
|
147
|
+
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
ensureDir(destPath);
|
|
150
|
+
copyDirRecursive(srcPath, destPath);
|
|
151
|
+
} else {
|
|
152
|
+
fs.copyFileSync(srcPath, destPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
26
157
|
/**
|
|
27
158
|
* Main init command handler
|
|
28
|
-
* @param {Object} options - Command options
|
|
29
159
|
*/
|
|
30
160
|
async function initCommand(options) {
|
|
31
161
|
const projectDir = process.cwd();
|
|
@@ -38,11 +168,10 @@ async function initCommand(options) {
|
|
|
38
168
|
const token = await getToken();
|
|
39
169
|
if (!token) {
|
|
40
170
|
spinner.fail(chalk.red('Not authenticated'));
|
|
41
|
-
console.log(chalk.dim('\n Run `
|
|
171
|
+
console.log(chalk.dim('\n Run `cskit auth --login` first.\n'));
|
|
42
172
|
process.exit(1);
|
|
43
173
|
}
|
|
44
174
|
|
|
45
|
-
// Verify access
|
|
46
175
|
const { valid, error } = await verifyAccess(token);
|
|
47
176
|
if (!valid) {
|
|
48
177
|
spinner.fail(chalk.red('Access denied'));
|
|
@@ -52,51 +181,70 @@ async function initCommand(options) {
|
|
|
52
181
|
|
|
53
182
|
spinner.succeed('Authenticated');
|
|
54
183
|
|
|
55
|
-
// Check
|
|
184
|
+
// Check existing installation
|
|
56
185
|
const manifest = loadManifest(projectDir);
|
|
57
186
|
const isUpdate = manifest.version !== null;
|
|
58
187
|
|
|
59
188
|
if (isUpdate) {
|
|
60
|
-
|
|
61
|
-
console.log(chalk.dim(
|
|
189
|
+
const currentVer = manifest.version.replace(/^v/, '');
|
|
190
|
+
console.log(chalk.dim(`\n Current: v${currentVer} (${formatDate(manifest.installedAt)})\n`));
|
|
62
191
|
}
|
|
63
192
|
|
|
64
|
-
//
|
|
65
|
-
spinner.start('Fetching
|
|
193
|
+
// Fetch available versions
|
|
194
|
+
spinner.start('Fetching available versions...');
|
|
66
195
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
spinner.succeed(`Latest version: ${release.tag}`);
|
|
72
|
-
} else {
|
|
196
|
+
const releases = await getAllReleases(token);
|
|
197
|
+
const latest = await getLatestRelease(token);
|
|
198
|
+
|
|
199
|
+
if (releases.length === 0) {
|
|
73
200
|
spinner.info('No releases found, using main branch');
|
|
201
|
+
} else {
|
|
202
|
+
spinner.succeed(`Found ${releases.length} release(s)`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Version selection
|
|
206
|
+
let selectedVersion = 'main';
|
|
207
|
+
|
|
208
|
+
if (releases.length > 0 && !options.latest) {
|
|
209
|
+
const choices = releases.map((r, i) => ({
|
|
210
|
+
name: `${r.tag}${i === 0 ? chalk.green(' (latest)') : ''} - ${formatDate(r.published)}`,
|
|
211
|
+
value: r.tag
|
|
212
|
+
}));
|
|
213
|
+
|
|
214
|
+
choices.push({ name: chalk.dim('main branch (development)'), value: 'main' });
|
|
215
|
+
|
|
216
|
+
const answer = await inquirer.prompt([{
|
|
217
|
+
type: 'list',
|
|
218
|
+
name: 'version',
|
|
219
|
+
message: 'Select version to install:',
|
|
220
|
+
choices,
|
|
221
|
+
default: releases[0]?.tag || 'main'
|
|
222
|
+
}]);
|
|
223
|
+
|
|
224
|
+
selectedVersion = answer.version;
|
|
225
|
+
} else if (latest) {
|
|
226
|
+
selectedVersion = latest.tag;
|
|
74
227
|
}
|
|
75
228
|
|
|
76
|
-
|
|
229
|
+
console.log(chalk.dim(`\n Installing: ${selectedVersion}\n`));
|
|
230
|
+
|
|
231
|
+
// Fetch file tree
|
|
77
232
|
spinner.start('Fetching file list...');
|
|
78
233
|
|
|
79
|
-
const tree = await
|
|
234
|
+
const tree = await getRepoTreeFromRef(token, selectedVersion);
|
|
80
235
|
const files = tree.filter(item =>
|
|
81
236
|
item.type === 'blob' && !shouldExclude(item.path)
|
|
82
237
|
);
|
|
83
238
|
|
|
84
239
|
spinner.succeed(`Found ${files.length} files`);
|
|
85
240
|
|
|
86
|
-
// Download
|
|
87
|
-
console.log(
|
|
88
|
-
|
|
89
|
-
const stats = {
|
|
90
|
-
created: 0,
|
|
91
|
-
updated: 0,
|
|
92
|
-
skipped: 0,
|
|
93
|
-
protected: 0,
|
|
94
|
-
userModified: 0
|
|
95
|
-
};
|
|
241
|
+
// Download files with progress bar
|
|
242
|
+
console.log('');
|
|
96
243
|
|
|
244
|
+
const stats = { created: 0, updated: 0, skipped: 0, protected: 0, userModified: 0 };
|
|
97
245
|
const newManifest = {
|
|
98
246
|
files: {},
|
|
99
|
-
version:
|
|
247
|
+
version: selectedVersion,
|
|
100
248
|
installedAt: new Date().toISOString()
|
|
101
249
|
};
|
|
102
250
|
|
|
@@ -105,25 +253,21 @@ async function initCommand(options) {
|
|
|
105
253
|
const targetPath = path.join(projectDir, file.path);
|
|
106
254
|
const relativePath = file.path;
|
|
107
255
|
|
|
108
|
-
//
|
|
109
|
-
|
|
256
|
+
// Update progress bar
|
|
257
|
+
process.stdout.write(`\r ${progressBar(i + 1, files.length)} ${chalk.dim(`(${i + 1}/${files.length})`)}`);
|
|
110
258
|
|
|
111
259
|
try {
|
|
112
|
-
|
|
113
|
-
const content = await downloadFile(token, file.path);
|
|
260
|
+
const content = await downloadFileFromRef(token, file.path, selectedVersion);
|
|
114
261
|
const contentHash = crypto.createHash('md5').update(content).digest('hex');
|
|
115
262
|
|
|
116
|
-
// Determine action
|
|
117
263
|
const { action, reason } = options.force
|
|
118
264
|
? { action: isProtected(relativePath) ? 'skip' : 'update', reason: 'forced' }
|
|
119
265
|
: determineMergeAction(targetPath, relativePath, content, manifest.files);
|
|
120
266
|
|
|
121
|
-
// Execute action
|
|
122
267
|
switch (action) {
|
|
123
268
|
case 'create':
|
|
124
269
|
ensureDir(path.dirname(targetPath));
|
|
125
270
|
fs.writeFileSync(targetPath, content);
|
|
126
|
-
console.log(chalk.green(` ${progress} + ${relativePath}`));
|
|
127
271
|
stats.created++;
|
|
128
272
|
newManifest.files[relativePath] = contentHash;
|
|
129
273
|
break;
|
|
@@ -131,52 +275,62 @@ async function initCommand(options) {
|
|
|
131
275
|
case 'update':
|
|
132
276
|
ensureDir(path.dirname(targetPath));
|
|
133
277
|
fs.writeFileSync(targetPath, content);
|
|
134
|
-
console.log(chalk.blue(` ${progress} ~ ${relativePath}`));
|
|
135
278
|
stats.updated++;
|
|
136
279
|
newManifest.files[relativePath] = contentHash;
|
|
137
280
|
break;
|
|
138
281
|
|
|
139
282
|
case 'skip':
|
|
140
|
-
if (reason === 'protected')
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.log(chalk.yellow(` ${progress} ! ${relativePath} (modified)`));
|
|
145
|
-
stats.userModified++;
|
|
146
|
-
} else {
|
|
147
|
-
// Unchanged, don't log to reduce noise
|
|
148
|
-
stats.skipped++;
|
|
149
|
-
}
|
|
150
|
-
// Preserve existing hash in manifest
|
|
283
|
+
if (reason === 'protected') stats.protected++;
|
|
284
|
+
else if (reason === 'user-modified') stats.userModified++;
|
|
285
|
+
else stats.skipped++;
|
|
286
|
+
|
|
151
287
|
if (manifest.files[relativePath]) {
|
|
152
288
|
newManifest.files[relativePath] = manifest.files[relativePath];
|
|
153
289
|
}
|
|
154
290
|
break;
|
|
155
291
|
}
|
|
156
292
|
} catch (err) {
|
|
157
|
-
|
|
293
|
+
// Silent fail for individual files
|
|
158
294
|
}
|
|
159
295
|
}
|
|
160
296
|
|
|
297
|
+
// Clear progress line
|
|
298
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
299
|
+
|
|
161
300
|
// Save manifest
|
|
162
301
|
saveManifest(projectDir, newManifest);
|
|
163
302
|
|
|
164
303
|
// Summary
|
|
165
|
-
console.log(chalk.cyan('
|
|
304
|
+
console.log(chalk.cyan(' Summary\n'));
|
|
166
305
|
console.log(` ${chalk.green('+')} Created: ${stats.created}`);
|
|
167
306
|
console.log(` ${chalk.blue('~')} Updated: ${stats.updated}`);
|
|
168
|
-
console.log(` ${chalk.yellow('!')} Protected: ${stats.protected}`);
|
|
169
|
-
console.log(` ${chalk.yellow('!')} User modified: ${stats.userModified}`);
|
|
170
|
-
console.log(` ${chalk.dim('-')} Unchanged: ${stats.skipped}`);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
307
|
+
if (stats.protected > 0) console.log(` ${chalk.yellow('!')} Protected: ${stats.protected}`);
|
|
308
|
+
if (stats.userModified > 0) console.log(` ${chalk.yellow('!')} User modified: ${stats.userModified}`);
|
|
309
|
+
if (stats.skipped > 0) console.log(` ${chalk.dim('-')} Unchanged: ${stats.skipped}`);
|
|
310
|
+
|
|
311
|
+
// Setup Python environment
|
|
312
|
+
console.log('');
|
|
313
|
+
const pythonResult = await setupPythonEnv(projectDir, spinner);
|
|
314
|
+
|
|
315
|
+
// Copy commands to .claude/commands/
|
|
316
|
+
await setupCommands(projectDir, spinner);
|
|
317
|
+
|
|
318
|
+
// Success message
|
|
319
|
+
const ver = selectedVersion.replace(/^v/, '');
|
|
320
|
+
console.log(chalk.green(`\n ✓ CSK v${ver} ${isUpdate ? 'updated' : 'installed'} successfully!\n`));
|
|
321
|
+
|
|
322
|
+
// Quick start guide
|
|
323
|
+
console.log(chalk.cyan(' Quick Start\n'));
|
|
324
|
+
console.log(chalk.dim(' 1. Open Claude Code in this directory:'));
|
|
325
|
+
console.log(` ${chalk.white('claude')}\n`);
|
|
326
|
+
console.log(chalk.dim(' 2. Start with CSK command:'));
|
|
327
|
+
console.log(` ${chalk.white('/csk')}\n`);
|
|
328
|
+
console.log(chalk.dim(' 3. Or explore available skills:'));
|
|
329
|
+
console.log(` ${chalk.white('./cli/csk list')}\n`);
|
|
330
|
+
|
|
331
|
+
if (pythonResult.skipped && pythonResult.reason) {
|
|
332
|
+
console.log(chalk.dim(` Note: ${pythonResult.reason}`));
|
|
333
|
+
console.log(chalk.dim(' Run manually: cd lib/python && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt\n'));
|
|
180
334
|
}
|
|
181
335
|
}
|
|
182
336
|
|
package/src/commands/status.js
CHANGED
|
@@ -39,7 +39,8 @@ async function statusCommand() {
|
|
|
39
39
|
|
|
40
40
|
console.log(chalk.dim(' Installation:'));
|
|
41
41
|
if (manifest.version) {
|
|
42
|
-
|
|
42
|
+
const version = manifest.version.replace(/^v/, '');
|
|
43
|
+
console.log(chalk.green(` Installed: v${version}`));
|
|
43
44
|
console.log(chalk.dim(` Date: ${manifest.installedAt}`));
|
|
44
45
|
console.log(chalk.dim(` Files: ${Object.keys(manifest.files).length}`));
|
|
45
46
|
} else {
|
package/src/commands/update.js
CHANGED
|
@@ -23,7 +23,7 @@ async function updateCommand() {
|
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
// Get latest version from npm
|
|
26
|
-
const latestVersion = execSync('npm view
|
|
26
|
+
const latestVersion = execSync('npm view cskit-cli version', {
|
|
27
27
|
encoding: 'utf-8',
|
|
28
28
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
29
29
|
}).trim();
|
|
@@ -36,7 +36,7 @@ async function updateCommand() {
|
|
|
36
36
|
spinner.text = `Updating to v${latestVersion}...`;
|
|
37
37
|
|
|
38
38
|
// Run npm update
|
|
39
|
-
execSync('npm install -g
|
|
39
|
+
execSync('npm install -g cskit-cli@latest', {
|
|
40
40
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -50,7 +50,7 @@ async function updateCommand() {
|
|
|
50
50
|
} else {
|
|
51
51
|
spinner.fail(chalk.red('Update failed'));
|
|
52
52
|
console.log(chalk.dim(`\n ${error.message}`));
|
|
53
|
-
console.log(chalk.dim('\n Try manually: npm install -g
|
|
53
|
+
console.log(chalk.dim('\n Try manually: npm install -g cskit-cli@latest\n'));
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/index.js
CHANGED
|
@@ -42,6 +42,7 @@ program
|
|
|
42
42
|
.command('init')
|
|
43
43
|
.description('Initialize or update CSK in current project')
|
|
44
44
|
.option('-f, --force', 'Force overwrite existing files')
|
|
45
|
+
.option('-l, --latest', 'Skip version selection, use latest')
|
|
45
46
|
.option('--no-merge', 'Skip smart merge, overwrite all')
|
|
46
47
|
.action(initCommand);
|
|
47
48
|
|
package/src/lib/github.js
CHANGED
|
@@ -172,15 +172,107 @@ async function getLatestRelease(token) {
|
|
|
172
172
|
published: data.published_at
|
|
173
173
|
};
|
|
174
174
|
} catch (error) {
|
|
175
|
-
// No releases, use branch
|
|
176
175
|
return null;
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Get all releases
|
|
181
|
+
* @param {string} token - GitHub PAT
|
|
182
|
+
* @param {number} limit - Max releases to fetch
|
|
183
|
+
* @returns {Promise<Array<{tag: string, name: string, published: string}>>}
|
|
184
|
+
*/
|
|
185
|
+
async function getAllReleases(token, limit = 10) {
|
|
186
|
+
try {
|
|
187
|
+
const data = await apiRequest(
|
|
188
|
+
token,
|
|
189
|
+
`/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=${limit}`
|
|
190
|
+
);
|
|
191
|
+
return data.map(r => ({
|
|
192
|
+
tag: r.tag_name,
|
|
193
|
+
name: r.name || r.tag_name,
|
|
194
|
+
published: r.published_at
|
|
195
|
+
}));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Download file from specific ref (tag/branch)
|
|
203
|
+
* @param {string} token - GitHub PAT
|
|
204
|
+
* @param {string} filePath - Path to file
|
|
205
|
+
* @param {string} ref - Tag or branch name
|
|
206
|
+
* @returns {Promise<Buffer>}
|
|
207
|
+
*/
|
|
208
|
+
async function downloadFileFromRef(token, filePath, ref) {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const options = {
|
|
211
|
+
hostname: 'raw.githubusercontent.com',
|
|
212
|
+
path: `/${GITHUB_OWNER}/${GITHUB_REPO}/${ref}/${filePath}`,
|
|
213
|
+
method: 'GET',
|
|
214
|
+
headers: {
|
|
215
|
+
'Authorization': `Bearer ${token}`,
|
|
216
|
+
'User-Agent': 'csk-cli'
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const req = https.request(options, (res) => {
|
|
221
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
222
|
+
const redirectUrl = new URL(res.headers.location);
|
|
223
|
+
const redirectOptions = {
|
|
224
|
+
hostname: redirectUrl.hostname,
|
|
225
|
+
path: redirectUrl.pathname + redirectUrl.search,
|
|
226
|
+
method: 'GET',
|
|
227
|
+
headers: { 'User-Agent': 'csk-cli' }
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const redirectReq = https.request(redirectOptions, (redirectRes) => {
|
|
231
|
+
const chunks = [];
|
|
232
|
+
redirectRes.on('data', chunk => chunks.push(chunk));
|
|
233
|
+
redirectRes.on('end', () => resolve(Buffer.concat(chunks)));
|
|
234
|
+
});
|
|
235
|
+
redirectReq.on('error', reject);
|
|
236
|
+
redirectReq.end();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (res.statusCode !== 200) {
|
|
241
|
+
reject(new Error(`Failed to download: ${res.statusCode}`));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const chunks = [];
|
|
246
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
247
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
req.on('error', reject);
|
|
251
|
+
req.end();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get repo tree from specific ref
|
|
257
|
+
* @param {string} token - GitHub PAT
|
|
258
|
+
* @param {string} ref - Tag or branch
|
|
259
|
+
* @returns {Promise<Array>}
|
|
260
|
+
*/
|
|
261
|
+
async function getRepoTreeFromRef(token, ref) {
|
|
262
|
+
const data = await apiRequest(
|
|
263
|
+
token,
|
|
264
|
+
`/repos/${GITHUB_OWNER}/${GITHUB_REPO}/git/trees/${ref}?recursive=1`
|
|
265
|
+
);
|
|
266
|
+
return data.tree || [];
|
|
267
|
+
}
|
|
268
|
+
|
|
180
269
|
module.exports = {
|
|
181
270
|
verifyAccess,
|
|
182
271
|
getRepoTree,
|
|
272
|
+
getRepoTreeFromRef,
|
|
183
273
|
getFileContent,
|
|
184
274
|
downloadFile,
|
|
185
|
-
|
|
275
|
+
downloadFileFromRef,
|
|
276
|
+
getLatestRelease,
|
|
277
|
+
getAllReleases
|
|
186
278
|
};
|