cskit-cli 1.0.20 → 1.0.22

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,299 +1,418 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Init Command
4
+ * Init Command - Improved Version
5
5
  *
6
- * Downloads CSK from private GitHub repository.
7
- * Features: version selection, progress bar, Python setup.
6
+ * Features:
7
+ * - Download zip instead of individual files
8
+ * - Timeline progress display (◇ pending → ◆ done)
9
+ * - Scan and show files to be overwritten
10
+ * - User confirmation before changes
11
+ * - Optional advanced packages installation
12
+ * - Platform-specific handling
8
13
  */
9
14
 
10
15
  const fs = require('fs');
11
16
  const path = require('path');
17
+ const os = require('os');
12
18
  const chalk = require('chalk');
13
- const ora = require('ora');
14
- const crypto = require('crypto');
15
19
  const inquirer = require('inquirer');
16
- const { execSync, spawn } = require('child_process');
20
+ const crypto = require('crypto');
21
+ const { execSync, spawnSync } = require('child_process');
17
22
  const { getToken } = require('../lib/keychain');
18
23
  const {
19
24
  verifyAccess,
20
- getRepoTreeFromRef,
21
- downloadFileFromRef,
22
25
  getAllReleases,
23
- getLatestRelease
26
+ getLatestRelease,
27
+ downloadZipFromRef
24
28
  } = require('../lib/github');
25
29
  const {
26
30
  isProtected,
27
31
  shouldExclude,
28
- determineMergeAction,
29
32
  loadManifest,
30
33
  saveManifest,
31
34
  ensureDir
32
35
  } = require('../lib/merge');
36
+ const {
37
+ detectPython,
38
+ checkAndReport: checkPythonEnv,
39
+ getInstallInstructions,
40
+ canAutoInstall,
41
+ installPython
42
+ } = require('../lib/python-check');
43
+
44
+ // =============================================================================
45
+ // Timeline Display
46
+ // =============================================================================
47
+
48
+ const SYMBOLS = {
49
+ pending: chalk.dim('◇'),
50
+ active: chalk.cyan('◈'),
51
+ done: chalk.green('◆'),
52
+ error: chalk.red('◆'),
53
+ skip: chalk.yellow('◇'),
54
+ file: chalk.dim('│ '),
55
+ fileNew: chalk.green('│ + '),
56
+ fileUpdate: chalk.blue('│ ~ '),
57
+ fileSkip: chalk.yellow('│ ! '),
58
+ fileDel: chalk.red('│ - '),
59
+ branch: chalk.dim('├──'),
60
+ branchLast: chalk.dim('└──'),
61
+ indent: ' ',
62
+ };
63
+
64
+ class Timeline {
65
+ constructor() {
66
+ this.steps = [];
67
+ this.currentStep = -1;
68
+ }
69
+
70
+ addStep(name) {
71
+ this.steps.push({ name, status: 'pending', children: [] });
72
+ }
73
+
74
+ start(stepIndex) {
75
+ this.currentStep = stepIndex;
76
+ this.steps[stepIndex].status = 'active';
77
+ this.render();
78
+ }
79
+
80
+ complete(stepIndex, message = null) {
81
+ this.steps[stepIndex].status = 'done';
82
+ if (message) this.steps[stepIndex].message = message;
83
+ this.render();
84
+ }
85
+
86
+ error(stepIndex, message) {
87
+ this.steps[stepIndex].status = 'error';
88
+ this.steps[stepIndex].message = message;
89
+ this.render();
90
+ }
91
+
92
+ skip(stepIndex, message) {
93
+ this.steps[stepIndex].status = 'skip';
94
+ this.steps[stepIndex].message = message;
95
+ this.render();
96
+ }
97
+
98
+ addChild(stepIndex, text, type = 'info') {
99
+ this.steps[stepIndex].children.push({ text, type });
100
+ }
101
+
102
+ render() {
103
+ // Clear and redraw (simple version)
104
+ console.log('');
105
+ for (let i = 0; i < this.steps.length; i++) {
106
+ const step = this.steps[i];
107
+ const symbol = SYMBOLS[step.status];
108
+ const num = chalk.dim(`${i + 1}.`);
109
+ const name = step.status === 'active' ? chalk.cyan(step.name) : step.name;
110
+ const msg = step.message ? chalk.dim(` (${step.message})`) : '';
111
+
112
+ console.log(` ${symbol} ${num} ${name}${msg}`);
113
+
114
+ // Show children (files, etc.)
115
+ for (let j = 0; j < step.children.length; j++) {
116
+ const child = step.children[j];
117
+ const isLast = j === step.children.length - 1;
118
+ const branch = isLast ? SYMBOLS.branchLast : SYMBOLS.branch;
119
+ const prefix = child.type === 'new' ? chalk.green('+') :
120
+ child.type === 'update' ? chalk.blue('~') :
121
+ child.type === 'skip' ? chalk.yellow('!') :
122
+ child.type === 'delete' ? chalk.red('-') :
123
+ chalk.dim('•');
124
+ console.log(` ${SYMBOLS.indent}${branch} ${prefix} ${child.text}`);
125
+ }
126
+ }
127
+ console.log('');
128
+ }
129
+ }
130
+
131
+ // =============================================================================
132
+ // Utility Functions
133
+ // =============================================================================
33
134
 
34
- /**
35
- * Format date for display
36
- */
37
135
  function formatDate(isoDate) {
38
136
  const d = new Date(isoDate);
39
137
  return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
40
138
  }
41
139
 
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}%`;
140
+ function getPlatform() {
141
+ const platform = os.platform();
142
+ if (platform === 'win32') return 'windows';
143
+ if (platform === 'darwin') return 'macos';
144
+ return 'linux';
51
145
  }
52
146
 
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');
147
+ function remapPath(filePath) {
148
+ const mappings = [
149
+ { from: 'src/commands/', to: '.claude/commands/' },
150
+ { from: 'src/skills/', to: '.claude/skills/' },
151
+ { from: 'core/', to: '.claude/skills/csk/core/' },
152
+ { from: 'domains/', to: '.claude/skills/csk/domains/' },
153
+ { from: 'industries/', to: '.claude/skills/csk/industries/' }
154
+ ];
59
155
 
60
- if (!fs.existsSync(requirementsFile)) {
61
- return { success: true, skipped: true };
156
+ for (const { from, to } of mappings) {
157
+ if (filePath.startsWith(from)) {
158
+ return filePath.replace(from, to);
159
+ }
62
160
  }
161
+ return filePath;
162
+ }
63
163
 
64
- spinner.start('Setting up Python environment...');
65
-
66
- const venvPath = path.join(libPythonDir, '.venv');
164
+ async function extractZip(zipPath, destDir) {
165
+ const platform = getPlatform();
67
166
 
68
167
  try {
69
- // Check if Python 3 is available and get version
70
- let pythonVersion;
71
- try {
72
- pythonVersion = execSync('python3 --version', { stdio: 'pipe' }).toString().trim();
73
- spinner.text = `Found ${pythonVersion}`;
74
- } catch {
75
- spinner.warn('Python 3 not found, skipping venv setup');
76
- return { success: true, skipped: true, reason: 'python3 not found' };
168
+ if (platform === 'windows') {
169
+ // Use PowerShell on Windows
170
+ execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'pipe' });
171
+ } else {
172
+ // Use unzip on Unix/macOS
173
+ execSync(`unzip -o -q "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
77
174
  }
175
+ return { success: true };
176
+ } catch (error) {
177
+ return { success: false, error: error.message };
178
+ }
179
+ }
78
180
 
79
- // Check minimum version (3.9+)
80
- const versionMatch = pythonVersion.match(/Python (\d+)\.(\d+)/);
81
- if (versionMatch) {
82
- const [, major, minor] = versionMatch.map(Number);
83
- if (major < 3 || (major === 3 && minor < 9)) {
84
- spinner.warn(`Python ${major}.${minor} found, requires 3.9+`);
85
- return { success: true, skipped: true, reason: `Python ${major}.${minor} requires 3.9+` };
86
- }
87
- }
181
+ function scanDirectory(dir, basePath = '') {
182
+ const files = [];
183
+ if (!fs.existsSync(dir)) return files;
184
+
185
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
186
+ for (const entry of entries) {
187
+ const fullPath = path.join(dir, entry.name);
188
+ const relativePath = path.join(basePath, entry.name);
88
189
 
89
- // Create venv if not exists
90
- if (!fs.existsSync(venvPath)) {
91
- spinner.text = 'Creating virtual environment...';
92
- execSync(`python3 -m venv "${venvPath}"`, { stdio: 'pipe', cwd: libPythonDir });
190
+ if (entry.isDirectory()) {
191
+ files.push(...scanDirectory(fullPath, relativePath));
192
+ } else {
193
+ files.push(relativePath);
93
194
  }
195
+ }
196
+ return files;
197
+ }
94
198
 
95
- // Parse requirements.txt (strip inline comments)
96
- const pipPath = path.join(venvPath, 'bin', 'pip');
97
- const requirements = fs.readFileSync(requirementsFile, 'utf8')
98
- .split('\n')
99
- .map(line => line.split('#')[0].trim()) // Remove inline comments
100
- .filter(line => line); // Remove empty lines
199
+ function compareFiles(sourceDir, targetDir, manifest) {
200
+ const sourceFiles = scanDirectory(sourceDir);
201
+ const changes = { new: [], update: [], skip: [], protected: [] };
101
202
 
102
- if (requirements.length === 0) {
103
- spinner.succeed('No packages to install');
104
- return { success: true, version: pythonVersion };
105
- }
203
+ for (const file of sourceFiles) {
204
+ if (shouldExclude(file)) continue;
106
205
 
107
- // Upgrade pip first (silent)
108
- spinner.text = 'Upgrading pip...';
109
- try {
110
- execSync(`"${pipPath}" install --upgrade pip -q`, { stdio: 'pipe', cwd: libPythonDir });
111
- } catch { /* ignore pip upgrade errors */ }
206
+ const targetPath = remapPath(file);
207
+ const fullTargetPath = path.join(targetDir, targetPath);
112
208
 
113
- // Check which packages need installation
114
- spinner.text = 'Checking installed packages...';
115
- let installedPkgs = [];
116
- try {
117
- const freezeOutput = execSync(`"${pipPath}" freeze`, { stdio: 'pipe', cwd: libPythonDir });
118
- installedPkgs = freezeOutput.toString().split('\n')
119
- .map(line => line.split('==')[0].toLowerCase().trim())
120
- .filter(Boolean);
121
- } catch { /* ignore */ }
122
-
123
- // Filter out already installed packages
124
- const toInstall = requirements.filter(pkg => {
125
- const pkgName = pkg.split(/[>=<\[]/)[0].toLowerCase();
126
- return !installedPkgs.includes(pkgName);
127
- });
128
-
129
- if (toInstall.length === 0) {
130
- spinner.succeed(`Python ready: ${requirements.length} packages already installed`);
131
- return { success: true, version: pythonVersion };
209
+ if (isProtected(targetPath)) {
210
+ changes.protected.push({ source: file, target: targetPath });
211
+ } else if (!fs.existsSync(fullTargetPath)) {
212
+ changes.new.push({ source: file, target: targetPath });
213
+ } else {
214
+ // Check if file changed
215
+ const sourceContent = fs.readFileSync(path.join(sourceDir, file));
216
+ const sourceHash = crypto.createHash('md5').update(sourceContent).digest('hex');
217
+ const targetHash = manifest.files?.[targetPath];
218
+
219
+ if (sourceHash !== targetHash) {
220
+ changes.update.push({ source: file, target: targetPath, hash: sourceHash });
221
+ } else {
222
+ changes.skip.push({ source: file, target: targetPath });
223
+ }
132
224
  }
225
+ }
133
226
 
134
- // Install packages one by one with progress
135
- spinner.stop();
136
- const skipped = requirements.length - toInstall.length;
137
- console.log(chalk.dim(`\n Installing ${toInstall.length} packages (${skipped} already installed):\n`));
227
+ return changes;
228
+ }
138
229
 
139
- const installed = [];
140
- const failed = [];
230
+ function formatFileList(files, maxShow = 10) {
231
+ const lines = [];
232
+ const showFiles = files.slice(0, maxShow);
141
233
 
142
- for (let i = 0; i < toInstall.length; i++) {
143
- const pkg = toInstall[i];
144
- const pkgName = pkg.split(/[>=<\[]/)[0]; // Extract package name
145
- const progress = `[${i + 1}/${toInstall.length}]`;
234
+ for (const file of showFiles) {
235
+ lines.push(file.target || file);
236
+ }
146
237
 
147
- process.stdout.write(` ${chalk.dim(progress)} ${pkgName}... `);
238
+ if (files.length > maxShow) {
239
+ lines.push(chalk.dim(`... and ${files.length - maxShow} more`));
240
+ }
148
241
 
149
- try {
150
- execSync(`"${pipPath}" install "${pkg}" -q`, {
151
- stdio: 'pipe',
152
- cwd: libPythonDir,
153
- timeout: 120000 // 2 min timeout per package
154
- });
155
- console.log(chalk.green('✓'));
156
- installed.push(pkgName);
157
- } catch (err) {
158
- console.log(chalk.red('✗'));
159
- const errMsg = err.stderr ? err.stderr.toString().split('\n')[0] : err.message;
160
- console.log(chalk.dim(` ${errMsg.slice(0, 60)}`));
161
- failed.push(pkgName);
162
- }
163
- }
242
+ return lines;
243
+ }
164
244
 
165
- console.log('');
245
+ // =============================================================================
246
+ // Package Installation
247
+ // =============================================================================
166
248
 
167
- // Summary
168
- const totalOk = installed.length + skipped;
169
- if (failed.length === 0) {
170
- spinner.succeed(`Python ready: ${totalOk} packages (${pythonVersion})`);
171
- return { success: true, version: pythonVersion };
172
- } else if (installed.length > 0 || skipped > 0) {
173
- spinner.warn(`Partial: ${totalOk} ok, ${failed.length} failed`);
174
- console.log(chalk.yellow(` Failed: ${failed.join(', ')}`));
175
- console.log(chalk.dim(` Run manually: ${pipPath} install <package>\n`));
176
- return { success: true, partial: true, version: pythonVersion, failed };
177
- } else {
178
- spinner.fail('All packages failed');
179
- return { success: false, error: 'All packages failed to install' };
180
- }
181
- } catch (error) {
182
- spinner.warn(`Python setup failed: ${error.message}`);
183
- return { success: false, error: error.message };
249
+ function checkPythonPackages(libPythonDir) {
250
+ const requirementsFile = path.join(libPythonDir, 'requirements.txt');
251
+ if (!fs.existsSync(requirementsFile)) {
252
+ return { required: [], installed: [], toInstall: [], toUpdate: [] };
184
253
  }
185
- }
186
254
 
187
- /**
188
- * Remap source paths to .claude/ destinations
189
- * Downloads directly to Claude Code directories
190
- */
191
- function remapPath(filePath) {
192
- const mappings = [
193
- { from: 'src/commands/', to: '.claude/commands/' },
194
- { from: 'core/', to: '.claude/skills/csk/core/' },
195
- { from: 'domains/', to: '.claude/skills/csk/domains/' },
196
- { from: 'industries/', to: '.claude/skills/csk/industries/' }
197
- ];
255
+ const platform = getPlatform();
256
+ const venvPath = path.join(libPythonDir, '.venv');
257
+ const pipPath = platform === 'windows'
258
+ ? path.join(venvPath, 'Scripts', 'pip.exe')
259
+ : path.join(venvPath, 'bin', 'pip');
198
260
 
199
- for (const { from, to } of mappings) {
200
- if (filePath.startsWith(from)) {
201
- return filePath.replace(from, to);
261
+ // Parse requirements
262
+ const required = fs.readFileSync(requirementsFile, 'utf8')
263
+ .split('\n')
264
+ .map(line => line.split('#')[0].trim())
265
+ .filter(line => line && !line.startsWith('#'));
266
+
267
+ // Get installed packages
268
+ let installed = [];
269
+ let installedVersions = {};
270
+
271
+ if (fs.existsSync(venvPath)) {
272
+ try {
273
+ const freeze = execSync(`"${pipPath}" freeze`, { stdio: 'pipe' }).toString();
274
+ freeze.split('\n').forEach(line => {
275
+ const match = line.match(/^([^=]+)==(.+)$/);
276
+ if (match) {
277
+ installed.push(match[1].toLowerCase());
278
+ installedVersions[match[1].toLowerCase()] = match[2];
279
+ }
280
+ });
281
+ } catch {}
282
+ }
283
+
284
+ // Compare
285
+ const toInstall = [];
286
+ const toUpdate = [];
287
+ const alreadyOk = [];
288
+
289
+ for (const req of required) {
290
+ const pkgName = req.split(/[>=<\[]/)[0].toLowerCase();
291
+ const reqVersion = req.match(/[>=<]+(.+)/)?.[1];
292
+
293
+ if (!installed.includes(pkgName)) {
294
+ toInstall.push({ name: pkgName, required: req });
295
+ } else if (reqVersion && installedVersions[pkgName] !== reqVersion) {
296
+ toUpdate.push({
297
+ name: pkgName,
298
+ current: installedVersions[pkgName],
299
+ required: reqVersion
300
+ });
301
+ } else {
302
+ alreadyOk.push({ name: pkgName, version: installedVersions[pkgName] });
202
303
  }
203
304
  }
204
305
 
205
- // Keep other files as-is (lib/, cli/, config/, etc.)
206
- return filePath;
306
+ return { required, installed: alreadyOk, toInstall, toUpdate };
207
307
  }
208
308
 
209
- /**
210
- * Recursively copy directory
211
- */
212
- function copyDirRecursive(src, dest) {
213
- const entries = fs.readdirSync(src, { withFileTypes: true });
309
+ async function installPackages(libPythonDir, packages, timeline, stepIndex, pythonCmd = 'python3') {
310
+ const platform = getPlatform();
311
+ const venvPath = path.join(libPythonDir, '.venv');
312
+ const pipPath = platform === 'windows'
313
+ ? path.join(venvPath, 'Scripts', 'pip.exe')
314
+ : path.join(venvPath, 'bin', 'pip');
214
315
 
215
- for (const entry of entries) {
216
- const srcPath = path.join(src, entry.name);
217
- const destPath = path.join(dest, entry.name);
316
+ // Create venv if not exists
317
+ if (!fs.existsSync(venvPath)) {
318
+ try {
319
+ execSync(`${pythonCmd} -m venv "${venvPath}"`, { stdio: 'pipe', cwd: libPythonDir });
320
+ timeline.addChild(stepIndex, 'Created virtual environment', 'new');
321
+ } catch (error) {
322
+ timeline.addChild(stepIndex, `Failed to create venv: ${error.message}`, 'skip');
323
+ return false;
324
+ }
325
+ }
218
326
 
219
- if (entry.isDirectory()) {
220
- ensureDir(destPath);
221
- copyDirRecursive(srcPath, destPath);
222
- } else {
223
- fs.copyFileSync(srcPath, destPath);
327
+ // Install packages
328
+ let success = true;
329
+ for (const pkg of packages) {
330
+ const pkgSpec = pkg.required || pkg.name;
331
+ try {
332
+ execSync(`"${pipPath}" install "${pkgSpec}" -q`, {
333
+ stdio: 'pipe',
334
+ cwd: libPythonDir,
335
+ timeout: 120000
336
+ });
337
+ timeline.addChild(stepIndex, `${pkg.name}`, 'new');
338
+ } catch (error) {
339
+ timeline.addChild(stepIndex, `${pkg.name} (failed)`, 'skip');
340
+ success = false;
224
341
  }
225
342
  }
343
+
344
+ return success;
226
345
  }
227
346
 
228
- /**
229
- * Main init command handler
230
- */
347
+ // =============================================================================
348
+ // Main Init Command
349
+ // =============================================================================
350
+
231
351
  async function initCommand(options) {
232
352
  const projectDir = process.cwd();
353
+ const platform = getPlatform();
233
354
 
234
355
  console.log(chalk.cyan('\n Content Suite Kit - Init\n'));
356
+ console.log(chalk.dim(` Platform: ${platform}\n`));
235
357
 
236
- // Check authentication
237
- const spinner = ora('Checking authentication...').start();
358
+ // Initialize timeline
359
+ const timeline = new Timeline();
360
+ timeline.addStep('Authenticate');
361
+ timeline.addStep('Download package');
362
+ timeline.addStep('Extract files');
363
+ timeline.addStep('Scan changes');
364
+ timeline.addStep('Confirm changes');
365
+ timeline.addStep('Apply changes');
366
+ timeline.addStep('Setup dependencies');
367
+
368
+ // Step 1: Authenticate
369
+ timeline.start(0);
238
370
 
239
371
  const token = await getToken();
240
372
  if (!token) {
241
- spinner.fail(chalk.red('Not authenticated'));
373
+ timeline.error(0, 'Not authenticated');
242
374
  console.log(chalk.dim('\n Run `cskit auth --login` first.\n'));
243
375
  process.exit(1);
244
376
  }
245
377
 
246
378
  const { valid, error } = await verifyAccess(token);
247
379
  if (!valid) {
248
- spinner.fail(chalk.red('Access denied'));
249
- console.log(chalk.red(`\n ${error}`));
380
+ timeline.error(0, error);
250
381
  process.exit(1);
251
382
  }
252
383
 
253
- spinner.succeed('Authenticated');
384
+ timeline.complete(0, 'OK');
254
385
 
255
386
  // Check existing installation
256
387
  const manifest = loadManifest(projectDir);
257
388
  const isUpdate = manifest.version !== null;
258
389
 
259
390
  if (isUpdate) {
260
- const currentVer = manifest.version.replace(/^v/, '');
261
- console.log(chalk.dim(`\n Current: v${currentVer} (${formatDate(manifest.installedAt)})\n`));
391
+ console.log(chalk.dim(` Current: ${manifest.version} (${formatDate(manifest.installedAt)})\n`));
262
392
  }
263
393
 
264
- // Fetch available versions
265
- spinner.start('Fetching available versions...');
266
-
394
+ // Get available versions
267
395
  const releases = await getAllReleases(token);
268
396
  const latest = await getLatestRelease(token);
269
397
 
270
- if (releases.length === 0) {
271
- spinner.info('No releases found, using main branch');
272
- } else {
273
- spinner.succeed(`Found ${releases.length} release(s)`);
274
- }
275
-
276
- // Version selection (show max 5 recent releases)
398
+ // Version selection
277
399
  let selectedVersion = 'main';
278
- const maxVersions = 5;
279
400
 
280
401
  if (releases.length > 0 && !options.latest) {
281
- const recentReleases = releases.slice(0, maxVersions);
402
+ const recentReleases = releases.slice(0, 5);
282
403
  const choices = recentReleases.map((r, i) => ({
283
404
  name: `${r.tag}${i === 0 ? chalk.green(' (latest)') : ''} - ${formatDate(r.published)}`,
284
405
  value: r.tag
285
406
  }));
286
-
287
- choices.push({ name: chalk.dim('main branch (development)'), value: 'main' });
407
+ choices.push({ name: chalk.dim('main branch (dev)'), value: 'main' });
288
408
 
289
409
  const answer = await inquirer.prompt([{
290
410
  type: 'list',
291
411
  name: 'version',
292
- message: 'Select version to install:',
412
+ message: 'Select version:',
293
413
  choices,
294
414
  default: releases[0]?.tag || 'main'
295
415
  }]);
296
-
297
416
  selectedVersion = answer.version;
298
417
  } else if (latest) {
299
418
  selectedVersion = latest.tag;
@@ -301,110 +420,320 @@ async function initCommand(options) {
301
420
 
302
421
  console.log(chalk.dim(`\n Installing: ${selectedVersion}\n`));
303
422
 
304
- // Fetch file tree
305
- spinner.start('Fetching file list...');
423
+ // Step 2: Download zip
424
+ timeline.start(1);
425
+
426
+ const tempDir = path.join(os.tmpdir(), `csk-${Date.now()}`);
427
+ const zipPath = path.join(tempDir, 'csk.zip');
428
+
429
+ ensureDir(tempDir);
430
+
431
+ try {
432
+ const zipBuffer = await downloadZipFromRef(token, selectedVersion);
433
+ fs.writeFileSync(zipPath, zipBuffer);
434
+ const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(2);
435
+ timeline.complete(1, `${sizeMB} MB`);
436
+ } catch (error) {
437
+ timeline.error(1, error.message);
438
+ process.exit(1);
439
+ }
440
+
441
+ // Step 3: Extract
442
+ timeline.start(2);
443
+
444
+ const extractDir = path.join(tempDir, 'extracted');
445
+ const result = await extractZip(zipPath, extractDir);
446
+
447
+ if (!result.success) {
448
+ timeline.error(2, result.error);
449
+ process.exit(1);
450
+ }
451
+
452
+ // Find the extracted folder (GitHub adds repo-branch prefix)
453
+ const extractedFolders = fs.readdirSync(extractDir);
454
+ const sourceDir = path.join(extractDir, extractedFolders[0]);
455
+
456
+ timeline.complete(2, `${extractedFolders[0]}`);
306
457
 
307
- const tree = await getRepoTreeFromRef(token, selectedVersion);
308
- const files = tree.filter(item =>
309
- item.type === 'blob' && !shouldExclude(item.path)
310
- );
458
+ // Step 4: Scan changes
459
+ timeline.start(3);
311
460
 
312
- spinner.succeed(`Found ${files.length} files`);
461
+ const changes = compareFiles(sourceDir, projectDir, manifest);
313
462
 
314
- // Download files with progress bar
315
- console.log('');
463
+ timeline.addChild(3, `${changes.new.length} new files`, 'new');
464
+ timeline.addChild(3, `${changes.update.length} updates`, 'update');
465
+ timeline.addChild(3, `${changes.skip.length} unchanged`, 'info');
466
+
467
+ if (changes.protected.length > 0) {
468
+ timeline.addChild(3, `${changes.protected.length} protected (skip)`, 'skip');
469
+ }
470
+
471
+ timeline.complete(3, `${changes.new.length + changes.update.length} changes`);
472
+
473
+ // Step 5: Confirm changes
474
+ timeline.start(4);
475
+
476
+ // Show files that will be changed
477
+ if (changes.update.length > 0) {
478
+ console.log(chalk.yellow('\n Files to be updated (will overwrite):\n'));
479
+ for (const file of changes.update.slice(0, 15)) {
480
+ console.log(chalk.yellow(` ! ${file.target}`));
481
+ }
482
+ if (changes.update.length > 15) {
483
+ console.log(chalk.dim(` ... and ${changes.update.length - 15} more\n`));
484
+ }
485
+ console.log('');
486
+ }
487
+
488
+ if (changes.new.length > 0) {
489
+ console.log(chalk.green(' New files to be created:\n'));
490
+ for (const file of changes.new.slice(0, 10)) {
491
+ console.log(chalk.green(` + ${file.target}`));
492
+ }
493
+ if (changes.new.length > 10) {
494
+ console.log(chalk.dim(` ... and ${changes.new.length - 10} more\n`));
495
+ }
496
+ console.log('');
497
+ }
498
+
499
+ // Confirm
500
+ if (changes.update.length > 0 && !options.force) {
501
+ const { confirm } = await inquirer.prompt([{
502
+ type: 'confirm',
503
+ name: 'confirm',
504
+ message: `Overwrite ${changes.update.length} files?`,
505
+ default: true
506
+ }]);
507
+
508
+ if (!confirm) {
509
+ timeline.skip(4, 'User cancelled');
510
+ timeline.skip(5, 'Skipped');
511
+ timeline.skip(6, 'Skipped');
512
+
513
+ // Cleanup
514
+ fs.rmSync(tempDir, { recursive: true, force: true });
515
+
516
+ console.log(chalk.yellow('\n Installation cancelled.\n'));
517
+ process.exit(0);
518
+ }
519
+ }
520
+
521
+ timeline.complete(4, 'Confirmed');
522
+
523
+ // Step 6: Apply changes
524
+ timeline.start(5);
316
525
 
317
- const stats = { created: 0, updated: 0, skipped: 0, protected: 0, userModified: 0 };
318
526
  const newManifest = {
319
527
  files: {},
320
528
  version: selectedVersion,
321
529
  installedAt: new Date().toISOString()
322
530
  };
323
531
 
324
- for (let i = 0; i < files.length; i++) {
325
- const file = files[i];
326
- const relativePath = remapPath(file.path);
327
- const targetPath = path.join(projectDir, relativePath);
532
+ let applied = 0;
328
533
 
329
- // Update progress bar
330
- process.stdout.write(`\r ${progressBar(i + 1, files.length)} ${chalk.dim(`(${i + 1}/${files.length})`)}`);
534
+ // Apply new files
535
+ for (const file of changes.new) {
536
+ const sourcePath = path.join(sourceDir, file.source);
537
+ const targetPath = path.join(projectDir, file.target);
331
538
 
332
- try {
333
- const content = await downloadFileFromRef(token, file.path, selectedVersion);
334
- const contentHash = crypto.createHash('md5').update(content).digest('hex');
335
-
336
- const { action, reason } = options.force
337
- ? { action: isProtected(relativePath) ? 'skip' : 'update', reason: 'forced' }
338
- : determineMergeAction(targetPath, relativePath, content, manifest.files);
339
-
340
- switch (action) {
341
- case 'create':
342
- ensureDir(path.dirname(targetPath));
343
- fs.writeFileSync(targetPath, content);
344
- stats.created++;
345
- newManifest.files[relativePath] = contentHash;
346
- break;
347
-
348
- case 'update':
349
- ensureDir(path.dirname(targetPath));
350
- fs.writeFileSync(targetPath, content);
351
- stats.updated++;
352
- newManifest.files[relativePath] = contentHash;
353
- break;
354
-
355
- case 'skip':
356
- if (reason === 'protected') stats.protected++;
357
- else if (reason === 'user-modified') stats.userModified++;
358
- else stats.skipped++;
359
-
360
- if (manifest.files[relativePath]) {
361
- newManifest.files[relativePath] = manifest.files[relativePath];
362
- }
363
- break;
364
- }
365
- } catch (err) {
366
- // Silent fail for individual files
367
- }
539
+ ensureDir(path.dirname(targetPath));
540
+ fs.copyFileSync(sourcePath, targetPath);
541
+
542
+ const content = fs.readFileSync(sourcePath);
543
+ const hash = crypto.createHash('md5').update(content).digest('hex');
544
+ newManifest.files[file.target] = hash;
545
+ applied++;
368
546
  }
369
547
 
370
- // Clear progress line
371
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
548
+ // Apply updates
549
+ for (const file of changes.update) {
550
+ const sourcePath = path.join(sourceDir, file.source);
551
+ const targetPath = path.join(projectDir, file.target);
552
+
553
+ ensureDir(path.dirname(targetPath));
554
+ fs.copyFileSync(sourcePath, targetPath);
555
+
556
+ newManifest.files[file.target] = file.hash;
557
+ applied++;
558
+ }
559
+
560
+ // Keep unchanged files in manifest
561
+ for (const file of changes.skip) {
562
+ if (manifest.files?.[file.target]) {
563
+ newManifest.files[file.target] = manifest.files[file.target];
564
+ }
565
+ }
372
566
 
373
567
  // Save manifest
374
568
  saveManifest(projectDir, newManifest);
375
569
 
376
- // Summary
377
- console.log(chalk.cyan(' Summary\n'));
378
- console.log(` ${chalk.green('+')} Created: ${stats.created}`);
379
- console.log(` ${chalk.blue('~')} Updated: ${stats.updated}`);
380
- if (stats.protected > 0) console.log(` ${chalk.yellow('!')} Protected: ${stats.protected}`);
381
- if (stats.userModified > 0) console.log(` ${chalk.yellow('!')} User modified: ${stats.userModified}`);
382
- if (stats.skipped > 0) console.log(` ${chalk.dim('-')} Unchanged: ${stats.skipped}`);
570
+ timeline.complete(5, `${applied} files`);
571
+
572
+ // Step 7: Setup dependencies
573
+ timeline.start(6);
574
+
575
+ const libPythonDir = path.join(projectDir, 'lib', 'python');
576
+
577
+ // Helper function to install Python packages
578
+ async function handlePythonPackages(pythonCmd) {
579
+ if (!fs.existsSync(libPythonDir)) {
580
+ timeline.skip(6, 'No lib/python folder');
581
+ return;
582
+ }
583
+
584
+ const { installAdvanced } = await inquirer.prompt([{
585
+ type: 'confirm',
586
+ name: 'installAdvanced',
587
+ message: 'Install Python dependencies?',
588
+ default: true
589
+ }]);
590
+
591
+ if (!installAdvanced) {
592
+ timeline.skip(6, 'Skipped by user');
593
+ return;
594
+ }
595
+
596
+ const pkgStatus = checkPythonPackages(libPythonDir);
597
+
598
+ if (pkgStatus.installed.length > 0) {
599
+ console.log(chalk.dim('\n Already installed:'));
600
+ for (const pkg of pkgStatus.installed.slice(0, 5)) {
601
+ console.log(chalk.dim(` ✓ ${pkg.name} (${pkg.version})`));
602
+ }
603
+ if (pkgStatus.installed.length > 5) {
604
+ console.log(chalk.dim(` ... and ${pkgStatus.installed.length - 5} more\n`));
605
+ }
606
+ }
607
+
608
+ const allToInstall = [...pkgStatus.toInstall, ...pkgStatus.toUpdate];
609
+
610
+ if (allToInstall.length === 0) {
611
+ timeline.complete(6, 'All packages ready');
612
+ return;
613
+ }
614
+
615
+ console.log(chalk.cyan('\n Packages to install/update:\n'));
616
+ for (const pkg of allToInstall) {
617
+ if (pkg.current) {
618
+ console.log(` ${pkg.name}: ${pkg.current} → ${pkg.required}`);
619
+ } else {
620
+ console.log(` ${pkg.name}: ${pkg.required || 'latest'}`);
621
+ }
622
+ }
623
+ console.log('');
624
+
625
+ const { confirmPkgs } = await inquirer.prompt([{
626
+ type: 'confirm',
627
+ name: 'confirmPkgs',
628
+ message: `Install ${allToInstall.length} packages?`,
629
+ default: true
630
+ }]);
631
+
632
+ if (confirmPkgs) {
633
+ await installPackages(libPythonDir, allToInstall, timeline, 6, pythonCmd);
634
+ timeline.complete(6, `${allToInstall.length} packages`);
635
+ } else {
636
+ timeline.skip(6, 'User skipped');
637
+ }
638
+ }
639
+
640
+ // Check Python availability first
641
+ let pythonInfo = detectPython();
642
+ let pythonReady = false;
383
643
 
384
- // Setup Python environment
385
- console.log('');
386
- const pythonResult = await setupPythonEnv(projectDir, spinner);
644
+ if (!pythonInfo.found) {
645
+ console.log('');
646
+ console.log(chalk.yellow(' Python not found on this system.'));
647
+ console.log(chalk.dim(' Python is required for:'));
648
+ console.log(chalk.dim(' - Data fetching and analysis'));
649
+ console.log(chalk.dim(' - Chart generation'));
650
+ console.log(chalk.dim(' - MCP server tools'));
651
+ console.log('');
387
652
 
388
- // CSK files already downloaded to .claude/ (no copy needed)
389
- spinner.succeed('CSK installed to .claude/');
653
+ // Check if we can auto-install
654
+ const autoInstall = canAutoInstall();
390
655
 
391
- // Success message
656
+ if (autoInstall.canInstall) {
657
+ console.log(chalk.cyan(` Auto-install available via ${autoInstall.manager}`));
658
+ if (autoInstall.needsSudo) {
659
+ console.log(chalk.dim(' (requires sudo password)'));
660
+ }
661
+ console.log(chalk.dim(` Command: ${autoInstall.command}`));
662
+ console.log('');
663
+
664
+ const { doInstall } = await inquirer.prompt([{
665
+ type: 'confirm',
666
+ name: 'doInstall',
667
+ message: `Install Python now via ${autoInstall.manager}?`,
668
+ default: true
669
+ }]);
670
+
671
+ if (doInstall) {
672
+ console.log('');
673
+ timeline.addChild(6, `Installing via ${autoInstall.manager}...`, 'update');
674
+
675
+ const result = await installPython((msg) => {
676
+ console.log(chalk.dim(` ${msg}`));
677
+ });
678
+
679
+ if (result.success) {
680
+ pythonInfo = detectPython();
681
+ timeline.addChild(6, `Python ${result.version} installed`, 'new');
682
+ pythonReady = true;
683
+ } else {
684
+ console.log('');
685
+ console.log(chalk.red(` ${result.error}`));
686
+ console.log(chalk.dim(' You may need to restart your terminal after installation.'));
687
+ console.log('');
688
+ timeline.skip(6, 'Installation failed');
689
+ }
690
+ } else {
691
+ timeline.skip(6, 'User declined install');
692
+ }
693
+ } else {
694
+ // No package manager - show manual instructions
695
+ const instructions = getInstallInstructions();
696
+ console.log(chalk.bold(` Install via ${instructions.method}:\n`));
697
+ for (const cmd of instructions.commands.slice(0, 4)) {
698
+ if (cmd.startsWith('#')) {
699
+ console.log(chalk.dim(` ${cmd}`));
700
+ } else if (cmd !== '') {
701
+ console.log(chalk.cyan(` ${cmd}`));
702
+ }
703
+ }
704
+ console.log('');
705
+ timeline.skip(6, 'Python not installed');
706
+ }
707
+ } else if (!pythonInfo.meetsMinimum) {
708
+ console.log('');
709
+ console.log(chalk.yellow(` Python ${pythonInfo.version} found, but 3.8+ required.`));
710
+ console.log(chalk.dim(' Please upgrade Python to use advanced features.'));
711
+ console.log('');
712
+ timeline.skip(6, `Python ${pythonInfo.version} too old`);
713
+ } else {
714
+ pythonReady = true;
715
+ }
716
+
717
+ // Install packages if Python is ready
718
+ if (pythonReady && pythonInfo.meetsMinimum) {
719
+ timeline.addChild(6, `Python ${pythonInfo.version} (${pythonInfo.command})`, 'update');
720
+ await handlePythonPackages(pythonInfo.command);
721
+ }
722
+
723
+ // Cleanup temp files
724
+ try {
725
+ fs.rmSync(tempDir, { recursive: true, force: true });
726
+ } catch {}
727
+
728
+ // Final message
392
729
  const ver = selectedVersion.replace(/^v/, '');
393
730
  console.log(chalk.green(`\n ✓ CSK v${ver} ${isUpdate ? 'updated' : 'installed'} successfully!\n`));
394
731
 
395
- // Quick start guide
396
732
  console.log(chalk.cyan(' Quick Start\n'));
397
- console.log(chalk.dim(' 1. Open Claude Code in this directory:'));
733
+ console.log(chalk.dim(' 1. Open Claude Code:'));
398
734
  console.log(` ${chalk.white('claude')}\n`);
399
- console.log(chalk.dim(' 2. Start with CSK command:'));
735
+ console.log(chalk.dim(' 2. Start with CSK:'));
400
736
  console.log(` ${chalk.white('/csk')}\n`);
401
- console.log(chalk.dim(' 3. Or explore available skills:'));
402
- console.log(` ${chalk.white('./cli/csk list')}\n`);
403
-
404
- if (pythonResult.skipped && pythonResult.reason) {
405
- console.log(chalk.dim(` Note: ${pythonResult.reason}`));
406
- console.log(chalk.dim(' Run manually: cd lib/python && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt\n'));
407
- }
408
737
  }
409
738
 
410
739
  module.exports = initCommand;