cskit-cli 1.0.0 → 1.0.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cskit-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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/csk-cli.git"
28
+ "url": "git+https://github.com/tohaitrieu/content-suite-kit.git"
29
29
  },
30
30
  "bugs": {
31
- "url": "https://github.com/tohaitrieu/csk-cli/issues"
31
+ "url": "https://github.com/tohaitrieu/content-suite-kit/issues"
32
32
  },
33
33
  "homepage": "https://cskit.net",
34
34
  "dependencies": {
@@ -3,8 +3,8 @@
3
3
  /**
4
4
  * Init Command
5
5
  *
6
- * Downloads CSK from private GitHub repository into current project.
7
- * Handles smart merging to preserve user modifications.
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 { verifyAccess, getRepoTree, downloadFile, getLatestRelease } = require('../lib/github');
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,73 @@ 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
+
26
99
  /**
27
100
  * Main init command handler
28
- * @param {Object} options - Command options
29
101
  */
30
102
  async function initCommand(options) {
31
103
  const projectDir = process.cwd();
@@ -38,11 +110,10 @@ async function initCommand(options) {
38
110
  const token = await getToken();
39
111
  if (!token) {
40
112
  spinner.fail(chalk.red('Not authenticated'));
41
- console.log(chalk.dim('\n Run `csk auth --login` first.\n'));
113
+ console.log(chalk.dim('\n Run `cskit auth --login` first.\n'));
42
114
  process.exit(1);
43
115
  }
44
116
 
45
- // Verify access
46
117
  const { valid, error } = await verifyAccess(token);
47
118
  if (!valid) {
48
119
  spinner.fail(chalk.red('Access denied'));
@@ -52,51 +123,70 @@ async function initCommand(options) {
52
123
 
53
124
  spinner.succeed('Authenticated');
54
125
 
55
- // Check for existing installation
126
+ // Check existing installation
56
127
  const manifest = loadManifest(projectDir);
57
128
  const isUpdate = manifest.version !== null;
58
129
 
59
130
  if (isUpdate) {
60
- console.log(chalk.dim(`\n Existing installation found: v${manifest.version}`));
61
- console.log(chalk.dim(` Installed: ${manifest.installedAt}\n`));
131
+ const currentVer = manifest.version.replace(/^v/, '');
132
+ console.log(chalk.dim(`\n Current: v${currentVer} (${formatDate(manifest.installedAt)})\n`));
62
133
  }
63
134
 
64
- // Get latest version
65
- spinner.start('Fetching latest version...');
135
+ // Fetch available versions
136
+ spinner.start('Fetching available versions...');
66
137
 
67
- let version = 'main';
68
- const release = await getLatestRelease(token);
69
- if (release) {
70
- version = release.tag;
71
- spinner.succeed(`Latest version: ${release.tag}`);
72
- } else {
138
+ const releases = await getAllReleases(token);
139
+ const latest = await getLatestRelease(token);
140
+
141
+ if (releases.length === 0) {
73
142
  spinner.info('No releases found, using main branch');
143
+ } else {
144
+ spinner.succeed(`Found ${releases.length} release(s)`);
145
+ }
146
+
147
+ // Version selection
148
+ let selectedVersion = 'main';
149
+
150
+ if (releases.length > 0 && !options.latest) {
151
+ const choices = releases.map((r, i) => ({
152
+ name: `${r.tag}${i === 0 ? chalk.green(' (latest)') : ''} - ${formatDate(r.published)}`,
153
+ value: r.tag
154
+ }));
155
+
156
+ choices.push({ name: chalk.dim('main branch (development)'), value: 'main' });
157
+
158
+ const answer = await inquirer.prompt([{
159
+ type: 'list',
160
+ name: 'version',
161
+ message: 'Select version to install:',
162
+ choices,
163
+ default: releases[0]?.tag || 'main'
164
+ }]);
165
+
166
+ selectedVersion = answer.version;
167
+ } else if (latest) {
168
+ selectedVersion = latest.tag;
74
169
  }
75
170
 
76
- // Get file tree
171
+ console.log(chalk.dim(`\n Installing: ${selectedVersion}\n`));
172
+
173
+ // Fetch file tree
77
174
  spinner.start('Fetching file list...');
78
175
 
79
- const tree = await getRepoTree(token);
176
+ const tree = await getRepoTreeFromRef(token, selectedVersion);
80
177
  const files = tree.filter(item =>
81
178
  item.type === 'blob' && !shouldExclude(item.path)
82
179
  );
83
180
 
84
181
  spinner.succeed(`Found ${files.length} files`);
85
182
 
86
- // Download and merge files
87
- console.log(chalk.cyan('\n Downloading files...\n'));
88
-
89
- const stats = {
90
- created: 0,
91
- updated: 0,
92
- skipped: 0,
93
- protected: 0,
94
- userModified: 0
95
- };
183
+ // Download files with progress bar
184
+ console.log('');
96
185
 
186
+ const stats = { created: 0, updated: 0, skipped: 0, protected: 0, userModified: 0 };
97
187
  const newManifest = {
98
188
  files: {},
99
- version: version,
189
+ version: selectedVersion,
100
190
  installedAt: new Date().toISOString()
101
191
  };
102
192
 
@@ -105,25 +195,21 @@ async function initCommand(options) {
105
195
  const targetPath = path.join(projectDir, file.path);
106
196
  const relativePath = file.path;
107
197
 
108
- // Progress indicator
109
- const progress = `[${i + 1}/${files.length}]`;
198
+ // Update progress bar
199
+ process.stdout.write(`\r ${progressBar(i + 1, files.length)} ${chalk.dim(`(${i + 1}/${files.length})`)}`);
110
200
 
111
201
  try {
112
- // Download file content
113
- const content = await downloadFile(token, file.path);
202
+ const content = await downloadFileFromRef(token, file.path, selectedVersion);
114
203
  const contentHash = crypto.createHash('md5').update(content).digest('hex');
115
204
 
116
- // Determine action
117
205
  const { action, reason } = options.force
118
206
  ? { action: isProtected(relativePath) ? 'skip' : 'update', reason: 'forced' }
119
207
  : determineMergeAction(targetPath, relativePath, content, manifest.files);
120
208
 
121
- // Execute action
122
209
  switch (action) {
123
210
  case 'create':
124
211
  ensureDir(path.dirname(targetPath));
125
212
  fs.writeFileSync(targetPath, content);
126
- console.log(chalk.green(` ${progress} + ${relativePath}`));
127
213
  stats.created++;
128
214
  newManifest.files[relativePath] = contentHash;
129
215
  break;
@@ -131,52 +217,59 @@ async function initCommand(options) {
131
217
  case 'update':
132
218
  ensureDir(path.dirname(targetPath));
133
219
  fs.writeFileSync(targetPath, content);
134
- console.log(chalk.blue(` ${progress} ~ ${relativePath}`));
135
220
  stats.updated++;
136
221
  newManifest.files[relativePath] = contentHash;
137
222
  break;
138
223
 
139
224
  case 'skip':
140
- if (reason === 'protected') {
141
- console.log(chalk.yellow(` ${progress} ! ${relativePath} (protected)`));
142
- stats.protected++;
143
- } else if (reason === 'user-modified') {
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
225
+ if (reason === 'protected') stats.protected++;
226
+ else if (reason === 'user-modified') stats.userModified++;
227
+ else stats.skipped++;
228
+
151
229
  if (manifest.files[relativePath]) {
152
230
  newManifest.files[relativePath] = manifest.files[relativePath];
153
231
  }
154
232
  break;
155
233
  }
156
234
  } catch (err) {
157
- console.log(chalk.red(` ${progress} x ${relativePath} (${err.message})`));
235
+ // Silent fail for individual files
158
236
  }
159
237
  }
160
238
 
239
+ // Clear progress line
240
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
241
+
161
242
  // Save manifest
162
243
  saveManifest(projectDir, newManifest);
163
244
 
164
245
  // Summary
165
- console.log(chalk.cyan('\n Summary\n'));
246
+ console.log(chalk.cyan(' Summary\n'));
166
247
  console.log(` ${chalk.green('+')} Created: ${stats.created}`);
167
248
  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
- console.log(chalk.green(`\n CSK ${isUpdate ? 'updated' : 'installed'} successfully!\n`));
173
-
174
- // Next steps
175
- if (!isUpdate) {
176
- console.log(chalk.dim(' Next steps:'));
177
- console.log(chalk.dim(' 1. Review .claude/ directory'));
178
- console.log(chalk.dim(' 2. Copy .env.example to .env and configure'));
179
- console.log(chalk.dim(' 3. Start using CSK commands in Claude Code\n'));
249
+ if (stats.protected > 0) console.log(` ${chalk.yellow('!')} Protected: ${stats.protected}`);
250
+ if (stats.userModified > 0) console.log(` ${chalk.yellow('!')} User modified: ${stats.userModified}`);
251
+ if (stats.skipped > 0) console.log(` ${chalk.dim('-')} Unchanged: ${stats.skipped}`);
252
+
253
+ // Setup Python environment
254
+ console.log('');
255
+ const pythonResult = await setupPythonEnv(projectDir, spinner);
256
+
257
+ // Success message
258
+ const ver = selectedVersion.replace(/^v/, '');
259
+ console.log(chalk.green(`\n CSK v${ver} ${isUpdate ? 'updated' : 'installed'} successfully!\n`));
260
+
261
+ // Quick start guide
262
+ console.log(chalk.cyan(' Quick Start\n'));
263
+ console.log(chalk.dim(' 1. Open Claude Code in this directory:'));
264
+ console.log(` ${chalk.white('claude')}\n`);
265
+ console.log(chalk.dim(' 2. Start with CSK command:'));
266
+ console.log(` ${chalk.white('/csk')}\n`);
267
+ console.log(chalk.dim(' 3. Or explore available skills:'));
268
+ console.log(` ${chalk.white('./cli/csk list')}\n`);
269
+
270
+ if (pythonResult.skipped && pythonResult.reason) {
271
+ console.log(chalk.dim(` Note: ${pythonResult.reason}`));
272
+ console.log(chalk.dim(' Run manually: cd lib/python && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt\n'));
180
273
  }
181
274
  }
182
275
 
@@ -39,7 +39,8 @@ async function statusCommand() {
39
39
 
40
40
  console.log(chalk.dim(' Installation:'));
41
41
  if (manifest.version) {
42
- console.log(chalk.green(` Installed: v${manifest.version}`));
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 {
@@ -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 csk-cli version', {
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 csk-cli@latest', {
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 csk-cli@latest\n'));
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
- getLatestRelease
275
+ downloadFileFromRef,
276
+ getLatestRelease,
277
+ getAllReleases
186
278
  };