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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cskit-cli",
3
- "version": "1.0.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/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,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 `csk auth --login` first.\n'));
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 for existing installation
184
+ // Check existing installation
56
185
  const manifest = loadManifest(projectDir);
57
186
  const isUpdate = manifest.version !== null;
58
187
 
59
188
  if (isUpdate) {
60
- console.log(chalk.dim(`\n Existing installation found: v${manifest.version}`));
61
- console.log(chalk.dim(` Installed: ${manifest.installedAt}\n`));
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
- // Get latest version
65
- spinner.start('Fetching latest version...');
193
+ // Fetch available versions
194
+ spinner.start('Fetching available versions...');
66
195
 
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 {
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
- // Get file tree
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 getRepoTree(token);
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 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
- };
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: 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
- // Progress indicator
109
- const progress = `[${i + 1}/${files.length}]`;
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
- // Download file content
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
- 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
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
- console.log(chalk.red(` ${progress} x ${relativePath} (${err.message})`));
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('\n Summary\n'));
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
- 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'));
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
 
@@ -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
  };