cskit-cli 1.0.0

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/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # CSK CLI
2
+
3
+ > Content Suite Kit Command Line Interface
4
+
5
+ CLI tool để download và quản lý CSK từ private GitHub repository.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g csk-cli
11
+ ```
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js >= 16
16
+ - GitHub Personal Access Token (granted after purchase)
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ # 1. Authenticate with GitHub
22
+ csk auth --login
23
+
24
+ # 2. Initialize CSK in your project
25
+ cd your-project
26
+ csk init
27
+
28
+ # 3. Check status
29
+ csk status
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ### `csk auth`
35
+
36
+ Manage GitHub authentication.
37
+
38
+ ```bash
39
+ csk auth --login # Add/update GitHub token
40
+ csk auth --logout # Remove stored token
41
+ csk auth --status # Check auth status (default)
42
+ ```
43
+
44
+ **Token storage priority:**
45
+ 1. Environment variable (`GITHUB_TOKEN` or `GH_TOKEN`)
46
+ 2. GitHub CLI (`gh auth token`)
47
+ 3. OS Keychain (macOS Keychain, Windows Credential Manager, Linux libsecret)
48
+ 4. Config file (`~/.csk/config.json`)
49
+
50
+ ### `csk init`
51
+
52
+ Download/update CSK in current project.
53
+
54
+ ```bash
55
+ csk init # Smart merge (preserve user changes)
56
+ csk init --force # Force overwrite (except protected files)
57
+ csk init --no-merge # Skip merge, overwrite all
58
+ ```
59
+
60
+ **Protected files (never overwritten):**
61
+ - `.env`, `.env.local`, `.env.production`
62
+ - `*.key`, `*.pem`
63
+ - `config.local.json`
64
+
65
+ ### `csk status`
66
+
67
+ Check installation status.
68
+
69
+ ```bash
70
+ csk status
71
+ ```
72
+
73
+ Shows:
74
+ - Authentication status
75
+ - Installation version
76
+ - Directory status
77
+
78
+ ### `csk update`
79
+
80
+ Update CLI tool to latest version.
81
+
82
+ ```bash
83
+ csk update
84
+ ```
85
+
86
+ ## Creating a GitHub PAT
87
+
88
+ 1. Go to [GitHub Settings > Tokens](https://github.com/settings/tokens/new)
89
+ 2. Select scopes:
90
+ - `repo` (Full control of private repositories)
91
+ 3. Generate token
92
+ 4. Run `csk auth --login` and paste token
93
+
94
+ ## Support
95
+
96
+ - Website: https://cskit.net
97
+ - Email: support@cskit.net
98
+
99
+ ## License
100
+
101
+ MIT
package/bin/csk.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ require('../src/index.js');
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "cskit-cli",
3
+ "version": "1.0.0",
4
+ "description": "Content Suite Kit CLI - Download and manage CSK skills from private repository",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "cskit": "bin/csk.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "claude",
14
+ "claude-code",
15
+ "content",
16
+ "skills",
17
+ "cli",
18
+ "ai"
19
+ ],
20
+ "author": {
21
+ "name": "To Trieu",
22
+ "email": "contact@tohaitrieu.net",
23
+ "url": "https://cskit.net"
24
+ },
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/tohaitrieu/csk-cli.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/tohaitrieu/csk-cli/issues"
32
+ },
33
+ "homepage": "https://cskit.net",
34
+ "dependencies": {
35
+ "chalk": "^4.1.2",
36
+ "commander": "^11.1.0",
37
+ "inquirer": "^8.2.6",
38
+ "keytar": "^7.9.0",
39
+ "ora": "^5.4.1",
40
+ "simple-git": "^3.22.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=16.0.0"
44
+ },
45
+ "files": [
46
+ "bin",
47
+ "src",
48
+ "README.md"
49
+ ]
50
+ }
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Auth Command
5
+ *
6
+ * Manages GitHub authentication for accessing private CSK repository.
7
+ * Supports multiple token sources with fallback chain.
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const inquirer = require('inquirer');
12
+ const ora = require('ora');
13
+ const { storeToken, getToken, deleteToken, getTokenSource } = require('../lib/keychain');
14
+ const { verifyAccess } = require('../lib/github');
15
+
16
+ /**
17
+ * Main auth command handler
18
+ * @param {Object} options - Command options
19
+ */
20
+ async function authCommand(options) {
21
+ if (options.login) {
22
+ await login();
23
+ } else if (options.logout) {
24
+ await logout();
25
+ } else {
26
+ // Default: show status
27
+ await showStatus();
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Login - add or update GitHub token
33
+ */
34
+ async function login() {
35
+ console.log(chalk.cyan('\n GitHub Authentication\n'));
36
+
37
+ console.log(chalk.dim(' CSK requires a GitHub Personal Access Token (PAT) to download'));
38
+ console.log(chalk.dim(' from the private repository.\n'));
39
+
40
+ console.log(chalk.yellow(' Create a token at:'));
41
+ console.log(chalk.blue(' https://github.com/settings/tokens/new\n'));
42
+
43
+ console.log(chalk.dim(' Required scopes:'));
44
+ console.log(chalk.dim(' - repo (Full control of private repositories)\n'));
45
+
46
+ // Prompt for token
47
+ const { token } = await inquirer.prompt([
48
+ {
49
+ type: 'password',
50
+ name: 'token',
51
+ message: 'Enter your GitHub PAT:',
52
+ mask: '*',
53
+ validate: (input) => {
54
+ if (!input || input.trim().length === 0) {
55
+ return 'Token is required';
56
+ }
57
+ if (!input.startsWith('ghp_') && !input.startsWith('github_pat_')) {
58
+ return 'Invalid token format. Should start with ghp_ or github_pat_';
59
+ }
60
+ return true;
61
+ }
62
+ }
63
+ ]);
64
+
65
+ // Verify token
66
+ const spinner = ora('Verifying token...').start();
67
+
68
+ const { valid, error } = await verifyAccess(token.trim());
69
+
70
+ if (!valid) {
71
+ spinner.fail(chalk.red('Token verification failed'));
72
+ console.log(chalk.red(`\n ${error}`));
73
+ console.log(chalk.dim('\n Make sure you have been granted access to the CSK repository.'));
74
+ console.log(chalk.dim(' If you purchased CSK, please accept the GitHub invitation first.\n'));
75
+ process.exit(1);
76
+ }
77
+
78
+ spinner.text = 'Storing token...';
79
+
80
+ // Store token
81
+ const { success, method } = await storeToken(token.trim());
82
+
83
+ if (success) {
84
+ spinner.succeed(chalk.green('Authentication successful'));
85
+ console.log(chalk.dim(`\n Token stored in: ${method}`));
86
+ console.log(chalk.green('\n You can now use `csk init` to download CSK.\n'));
87
+ } else {
88
+ spinner.fail(chalk.red('Failed to store token'));
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Logout - remove stored token
95
+ */
96
+ async function logout() {
97
+ const spinner = ora('Removing stored token...').start();
98
+
99
+ const deleted = await deleteToken();
100
+
101
+ if (deleted) {
102
+ spinner.succeed(chalk.green('Token removed'));
103
+ console.log(chalk.dim('\n You will need to authenticate again to use csk init.\n'));
104
+ } else {
105
+ spinner.info(chalk.yellow('No stored token found'));
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Show authentication status
111
+ */
112
+ async function showStatus() {
113
+ console.log(chalk.cyan('\n Authentication Status\n'));
114
+
115
+ const spinner = ora('Checking authentication...').start();
116
+
117
+ const token = await getToken();
118
+ const source = await getTokenSource();
119
+
120
+ if (!token) {
121
+ spinner.fail(chalk.red('Not authenticated'));
122
+ console.log(chalk.dim('\n Run `csk auth --login` to add your GitHub token.\n'));
123
+ return;
124
+ }
125
+
126
+ // Verify token still works
127
+ const { valid, error } = await verifyAccess(token);
128
+
129
+ if (valid) {
130
+ spinner.succeed(chalk.green('Authenticated'));
131
+ console.log(chalk.dim(`\n Token source: ${formatSource(source)}`));
132
+ console.log(chalk.green(' Repository access: Verified\n'));
133
+ } else {
134
+ spinner.fail(chalk.red('Authentication invalid'));
135
+ console.log(chalk.dim(`\n Token source: ${formatSource(source)}`));
136
+ console.log(chalk.red(` Error: ${error}`));
137
+ console.log(chalk.dim('\n Run `csk auth --login` to update your token.\n'));
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Format token source for display
143
+ */
144
+ function formatSource(source) {
145
+ const sources = {
146
+ 'env': 'Environment variable (GITHUB_TOKEN or GH_TOKEN)',
147
+ 'gh-cli': 'GitHub CLI (gh auth)',
148
+ 'keychain': 'OS Keychain',
149
+ 'config': 'Config file (~/.csk/config.json)',
150
+ 'none': 'None'
151
+ };
152
+ return sources[source] || source;
153
+ }
154
+
155
+ module.exports = authCommand;
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Init Command
5
+ *
6
+ * Downloads CSK from private GitHub repository into current project.
7
+ * Handles smart merging to preserve user modifications.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const chalk = require('chalk');
13
+ const ora = require('ora');
14
+ const crypto = require('crypto');
15
+ const { getToken } = require('../lib/keychain');
16
+ const { verifyAccess, getRepoTree, downloadFile, getLatestRelease } = require('../lib/github');
17
+ const {
18
+ isProtected,
19
+ shouldExclude,
20
+ determineMergeAction,
21
+ loadManifest,
22
+ saveManifest,
23
+ ensureDir
24
+ } = require('../lib/merge');
25
+
26
+ /**
27
+ * Main init command handler
28
+ * @param {Object} options - Command options
29
+ */
30
+ async function initCommand(options) {
31
+ const projectDir = process.cwd();
32
+
33
+ console.log(chalk.cyan('\n Content Suite Kit - Init\n'));
34
+
35
+ // Check authentication
36
+ const spinner = ora('Checking authentication...').start();
37
+
38
+ const token = await getToken();
39
+ if (!token) {
40
+ spinner.fail(chalk.red('Not authenticated'));
41
+ console.log(chalk.dim('\n Run `csk auth --login` first.\n'));
42
+ process.exit(1);
43
+ }
44
+
45
+ // Verify access
46
+ const { valid, error } = await verifyAccess(token);
47
+ if (!valid) {
48
+ spinner.fail(chalk.red('Access denied'));
49
+ console.log(chalk.red(`\n ${error}`));
50
+ process.exit(1);
51
+ }
52
+
53
+ spinner.succeed('Authenticated');
54
+
55
+ // Check for existing installation
56
+ const manifest = loadManifest(projectDir);
57
+ const isUpdate = manifest.version !== null;
58
+
59
+ if (isUpdate) {
60
+ console.log(chalk.dim(`\n Existing installation found: v${manifest.version}`));
61
+ console.log(chalk.dim(` Installed: ${manifest.installedAt}\n`));
62
+ }
63
+
64
+ // Get latest version
65
+ spinner.start('Fetching latest version...');
66
+
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 {
73
+ spinner.info('No releases found, using main branch');
74
+ }
75
+
76
+ // Get file tree
77
+ spinner.start('Fetching file list...');
78
+
79
+ const tree = await getRepoTree(token);
80
+ const files = tree.filter(item =>
81
+ item.type === 'blob' && !shouldExclude(item.path)
82
+ );
83
+
84
+ spinner.succeed(`Found ${files.length} files`);
85
+
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
+ };
96
+
97
+ const newManifest = {
98
+ files: {},
99
+ version: version,
100
+ installedAt: new Date().toISOString()
101
+ };
102
+
103
+ for (let i = 0; i < files.length; i++) {
104
+ const file = files[i];
105
+ const targetPath = path.join(projectDir, file.path);
106
+ const relativePath = file.path;
107
+
108
+ // Progress indicator
109
+ const progress = `[${i + 1}/${files.length}]`;
110
+
111
+ try {
112
+ // Download file content
113
+ const content = await downloadFile(token, file.path);
114
+ const contentHash = crypto.createHash('md5').update(content).digest('hex');
115
+
116
+ // Determine action
117
+ const { action, reason } = options.force
118
+ ? { action: isProtected(relativePath) ? 'skip' : 'update', reason: 'forced' }
119
+ : determineMergeAction(targetPath, relativePath, content, manifest.files);
120
+
121
+ // Execute action
122
+ switch (action) {
123
+ case 'create':
124
+ ensureDir(path.dirname(targetPath));
125
+ fs.writeFileSync(targetPath, content);
126
+ console.log(chalk.green(` ${progress} + ${relativePath}`));
127
+ stats.created++;
128
+ newManifest.files[relativePath] = contentHash;
129
+ break;
130
+
131
+ case 'update':
132
+ ensureDir(path.dirname(targetPath));
133
+ fs.writeFileSync(targetPath, content);
134
+ console.log(chalk.blue(` ${progress} ~ ${relativePath}`));
135
+ stats.updated++;
136
+ newManifest.files[relativePath] = contentHash;
137
+ break;
138
+
139
+ 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
151
+ if (manifest.files[relativePath]) {
152
+ newManifest.files[relativePath] = manifest.files[relativePath];
153
+ }
154
+ break;
155
+ }
156
+ } catch (err) {
157
+ console.log(chalk.red(` ${progress} x ${relativePath} (${err.message})`));
158
+ }
159
+ }
160
+
161
+ // Save manifest
162
+ saveManifest(projectDir, newManifest);
163
+
164
+ // Summary
165
+ console.log(chalk.cyan('\n Summary\n'));
166
+ console.log(` ${chalk.green('+')} Created: ${stats.created}`);
167
+ 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'));
180
+ }
181
+ }
182
+
183
+ module.exports = initCommand;
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Status Command
5
+ *
6
+ * Shows CSK installation status in current project.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const chalk = require('chalk');
12
+ const { loadManifest } = require('../lib/merge');
13
+ const { getToken, getTokenSource } = require('../lib/keychain');
14
+
15
+ /**
16
+ * Main status command handler
17
+ */
18
+ async function statusCommand() {
19
+ const projectDir = process.cwd();
20
+
21
+ console.log(chalk.cyan('\n CSK Status\n'));
22
+
23
+ // Check authentication
24
+ const token = await getToken();
25
+ const source = await getTokenSource();
26
+
27
+ console.log(chalk.dim(' Authentication:'));
28
+ if (token) {
29
+ console.log(chalk.green(` Authenticated (${formatSource(source)})`));
30
+ } else {
31
+ console.log(chalk.red(' Not authenticated'));
32
+ console.log(chalk.dim(' Run `csk auth --login` to authenticate'));
33
+ }
34
+
35
+ console.log('');
36
+
37
+ // Check installation
38
+ const manifest = loadManifest(projectDir);
39
+
40
+ console.log(chalk.dim(' Installation:'));
41
+ if (manifest.version) {
42
+ console.log(chalk.green(` Installed: v${manifest.version}`));
43
+ console.log(chalk.dim(` Date: ${manifest.installedAt}`));
44
+ console.log(chalk.dim(` Files: ${Object.keys(manifest.files).length}`));
45
+ } else {
46
+ console.log(chalk.yellow(' Not installed'));
47
+ console.log(chalk.dim(' Run `csk init` to install'));
48
+ }
49
+
50
+ console.log('');
51
+
52
+ // Check key directories
53
+ console.log(chalk.dim(' Directories:'));
54
+
55
+ const dirs = [
56
+ { path: '.claude', name: 'Claude Config' },
57
+ { path: 'core', name: 'Core Rules' },
58
+ { path: 'domains', name: 'Domains' },
59
+ { path: 'industries', name: 'Industries' }
60
+ ];
61
+
62
+ for (const dir of dirs) {
63
+ const fullPath = path.join(projectDir, dir.path);
64
+ const exists = fs.existsSync(fullPath);
65
+ const icon = exists ? chalk.green('✓') : chalk.red('✗');
66
+ console.log(` ${icon} ${dir.name} (${dir.path}/)`);
67
+ }
68
+
69
+ console.log('');
70
+ }
71
+
72
+ /**
73
+ * Format token source for display
74
+ */
75
+ function formatSource(source) {
76
+ const sources = {
77
+ 'env': 'env',
78
+ 'gh-cli': 'gh',
79
+ 'keychain': 'keychain',
80
+ 'config': 'config',
81
+ 'none': 'none'
82
+ };
83
+ return sources[source] || source;
84
+ }
85
+
86
+ module.exports = statusCommand;
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Update Command
5
+ *
6
+ * Updates the CSK CLI tool itself to latest version.
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const ora = require('ora');
11
+ const { execSync } = require('child_process');
12
+ const pkg = require('../../package.json');
13
+
14
+ /**
15
+ * Main update command handler
16
+ */
17
+ async function updateCommand() {
18
+ console.log(chalk.cyan('\n CSK CLI - Update\n'));
19
+
20
+ console.log(chalk.dim(` Current version: v${pkg.version}\n`));
21
+
22
+ const spinner = ora('Checking for updates...').start();
23
+
24
+ try {
25
+ // Get latest version from npm
26
+ const latestVersion = execSync('npm view csk-cli version', {
27
+ encoding: 'utf-8',
28
+ stdio: ['pipe', 'pipe', 'pipe']
29
+ }).trim();
30
+
31
+ if (latestVersion === pkg.version) {
32
+ spinner.succeed(chalk.green('Already on latest version'));
33
+ return;
34
+ }
35
+
36
+ spinner.text = `Updating to v${latestVersion}...`;
37
+
38
+ // Run npm update
39
+ execSync('npm install -g csk-cli@latest', {
40
+ stdio: ['pipe', 'pipe', 'pipe']
41
+ });
42
+
43
+ spinner.succeed(chalk.green(`Updated to v${latestVersion}`));
44
+
45
+ console.log(chalk.dim('\n Run `csk init` to update project files.\n'));
46
+ } catch (error) {
47
+ if (error.message.includes('npm view')) {
48
+ spinner.fail(chalk.red('Could not check for updates'));
49
+ console.log(chalk.dim('\n Package may not be published yet.\n'));
50
+ } else {
51
+ spinner.fail(chalk.red('Update failed'));
52
+ console.log(chalk.dim(`\n ${error.message}`));
53
+ console.log(chalk.dim('\n Try manually: npm install -g csk-cli@latest\n'));
54
+ }
55
+ }
56
+ }
57
+
58
+ module.exports = updateCommand;
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CSK CLI Constants
5
+ * Central configuration for the CLI tool
6
+ */
7
+
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ // Keychain service name for storing GitHub token
12
+ const KEYCHAIN_SERVICE = 'csk-cli';
13
+ const KEYCHAIN_ACCOUNT = 'github-token';
14
+
15
+ // GitHub repository details
16
+ const GITHUB_OWNER = 'tohaitrieu';
17
+ const GITHUB_REPO = 'content-suite-kit';
18
+ const GITHUB_BRANCH = 'main';
19
+
20
+ // Local paths
21
+ const CSK_HOME = path.join(os.homedir(), '.csk');
22
+ const CSK_CONFIG_PATH = path.join(CSK_HOME, 'config.json');
23
+ const CSK_CACHE_DIR = path.join(CSK_HOME, 'cache');
24
+
25
+ // Protected files that should never be overwritten during update
26
+ const PROTECTED_FILES = [
27
+ '.env',
28
+ '.env.local',
29
+ '.env.production',
30
+ '*.key',
31
+ '*.pem',
32
+ 'config.local.json',
33
+ '.csk/config.json'
34
+ ];
35
+
36
+ // Files/directories to exclude from download
37
+ const EXCLUDE_PATTERNS = [
38
+ '.git',
39
+ 'node_modules',
40
+ '.DS_Store',
41
+ '*.log',
42
+ 'packages/csk-cli' // Don't download CLI itself
43
+ ];
44
+
45
+ // CLI colors
46
+ const COLORS = {
47
+ success: 'green',
48
+ error: 'red',
49
+ warning: 'yellow',
50
+ info: 'blue',
51
+ dim: 'gray'
52
+ };
53
+
54
+ module.exports = {
55
+ KEYCHAIN_SERVICE,
56
+ KEYCHAIN_ACCOUNT,
57
+ GITHUB_OWNER,
58
+ GITHUB_REPO,
59
+ GITHUB_BRANCH,
60
+ CSK_HOME,
61
+ CSK_CONFIG_PATH,
62
+ CSK_CACHE_DIR,
63
+ PROTECTED_FILES,
64
+ EXCLUDE_PATTERNS,
65
+ COLORS
66
+ };
package/src/index.js ADDED
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CSK CLI - Content Suite Kit Command Line Interface
5
+ *
6
+ * Downloads and manages CSK skills from private GitHub repository.
7
+ * Requires GitHub access (granted after purchase).
8
+ */
9
+
10
+ const { Command } = require('commander');
11
+ const chalk = require('chalk');
12
+ const pkg = require('../package.json');
13
+
14
+ // Import commands
15
+ const initCommand = require('./commands/init');
16
+ const authCommand = require('./commands/auth');
17
+ const statusCommand = require('./commands/status');
18
+ const updateCommand = require('./commands/update');
19
+
20
+ const program = new Command();
21
+
22
+ // ASCII art banner
23
+ const banner = `
24
+ _____ _____ _ __
25
+ / ____/ ____| |/ /
26
+ | | | (___ | ' /
27
+ | | \\___ \\| <
28
+ | |___ ____) | . \\
29
+ \\____|_____/|_|\\_\\
30
+
31
+ Content Suite Kit CLI v${pkg.version}
32
+ `;
33
+
34
+ program
35
+ .name('csk')
36
+ .description('Content Suite Kit CLI - Download and manage CSK skills')
37
+ .version(pkg.version)
38
+ .addHelpText('before', chalk.cyan(banner));
39
+
40
+ // Init command - download/update CSK in current project
41
+ program
42
+ .command('init')
43
+ .description('Initialize or update CSK in current project')
44
+ .option('-f, --force', 'Force overwrite existing files')
45
+ .option('--no-merge', 'Skip smart merge, overwrite all')
46
+ .action(initCommand);
47
+
48
+ // Auth command - manage GitHub authentication
49
+ program
50
+ .command('auth')
51
+ .description('Manage GitHub authentication')
52
+ .option('--login', 'Add or update GitHub token')
53
+ .option('--logout', 'Remove stored GitHub token')
54
+ .option('--status', 'Check authentication status')
55
+ .action(authCommand);
56
+
57
+ // Status command - check installation status
58
+ program
59
+ .command('status')
60
+ .description('Check CSK installation status')
61
+ .action(statusCommand);
62
+
63
+ // Update command - update CLI tool itself
64
+ program
65
+ .command('update')
66
+ .description('Update CSK CLI to latest version')
67
+ .action(updateCommand);
68
+
69
+ // Parse arguments
70
+ program.parse(process.argv);
71
+
72
+ // Show help if no command provided
73
+ if (!process.argv.slice(2).length) {
74
+ program.outputHelp();
75
+ }
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GitHub API Client
5
+ *
6
+ * Handles authentication and repository operations.
7
+ */
8
+
9
+ const https = require('https');
10
+ const { GITHUB_OWNER, GITHUB_REPO, GITHUB_BRANCH } = require('../constants');
11
+
12
+ /**
13
+ * Make authenticated GitHub API request
14
+ * @param {string} token - GitHub PAT
15
+ * @param {string} endpoint - API endpoint (without base URL)
16
+ * @returns {Promise<Object>}
17
+ */
18
+ async function apiRequest(token, endpoint) {
19
+ return new Promise((resolve, reject) => {
20
+ const options = {
21
+ hostname: 'api.github.com',
22
+ path: endpoint,
23
+ method: 'GET',
24
+ headers: {
25
+ 'Authorization': `Bearer ${token}`,
26
+ 'Accept': 'application/vnd.github+json',
27
+ 'User-Agent': 'csk-cli',
28
+ 'X-GitHub-Api-Version': '2022-11-28'
29
+ }
30
+ };
31
+
32
+ const req = https.request(options, (res) => {
33
+ let data = '';
34
+ res.on('data', chunk => data += chunk);
35
+ res.on('end', () => {
36
+ if (res.statusCode === 200) {
37
+ resolve(JSON.parse(data));
38
+ } else if (res.statusCode === 401) {
39
+ reject(new Error('Authentication failed. Please check your GitHub token.'));
40
+ } else if (res.statusCode === 403) {
41
+ reject(new Error('Access denied. You may not have access to this repository.'));
42
+ } else if (res.statusCode === 404) {
43
+ reject(new Error('Repository not found. Please verify your access.'));
44
+ } else {
45
+ reject(new Error(`GitHub API error: ${res.statusCode}`));
46
+ }
47
+ });
48
+ });
49
+
50
+ req.on('error', reject);
51
+ req.end();
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Verify token has access to the CSK repository
57
+ * @param {string} token - GitHub PAT
58
+ * @returns {Promise<{valid: boolean, error?: string}>}
59
+ */
60
+ async function verifyAccess(token) {
61
+ try {
62
+ await apiRequest(token, `/repos/${GITHUB_OWNER}/${GITHUB_REPO}`);
63
+ return { valid: true };
64
+ } catch (error) {
65
+ return { valid: false, error: error.message };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get repository tree (file listing)
71
+ * @param {string} token - GitHub PAT
72
+ * @param {string} branch - Branch name
73
+ * @returns {Promise<Array>}
74
+ */
75
+ async function getRepoTree(token, branch = GITHUB_BRANCH) {
76
+ const data = await apiRequest(
77
+ token,
78
+ `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/git/trees/${branch}?recursive=1`
79
+ );
80
+ return data.tree || [];
81
+ }
82
+
83
+ /**
84
+ * Get file content from repository
85
+ * @param {string} token - GitHub PAT
86
+ * @param {string} filePath - Path to file in repo
87
+ * @returns {Promise<string>}
88
+ */
89
+ async function getFileContent(token, filePath) {
90
+ const data = await apiRequest(
91
+ token,
92
+ `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents/${filePath}?ref=${GITHUB_BRANCH}`
93
+ );
94
+
95
+ if (data.encoding === 'base64') {
96
+ return Buffer.from(data.content, 'base64').toString('utf-8');
97
+ }
98
+
99
+ return data.content;
100
+ }
101
+
102
+ /**
103
+ * Download file as binary
104
+ * @param {string} token - GitHub PAT
105
+ * @param {string} filePath - Path to file in repo
106
+ * @returns {Promise<Buffer>}
107
+ */
108
+ async function downloadFile(token, filePath) {
109
+ return new Promise((resolve, reject) => {
110
+ const options = {
111
+ hostname: 'raw.githubusercontent.com',
112
+ path: `/${GITHUB_OWNER}/${GITHUB_REPO}/${GITHUB_BRANCH}/${filePath}`,
113
+ method: 'GET',
114
+ headers: {
115
+ 'Authorization': `Bearer ${token}`,
116
+ 'User-Agent': 'csk-cli'
117
+ }
118
+ };
119
+
120
+ const req = https.request(options, (res) => {
121
+ if (res.statusCode === 302 || res.statusCode === 301) {
122
+ // Follow redirect
123
+ const redirectUrl = new URL(res.headers.location);
124
+ const redirectOptions = {
125
+ hostname: redirectUrl.hostname,
126
+ path: redirectUrl.pathname + redirectUrl.search,
127
+ method: 'GET',
128
+ headers: {
129
+ 'User-Agent': 'csk-cli'
130
+ }
131
+ };
132
+
133
+ const redirectReq = https.request(redirectOptions, (redirectRes) => {
134
+ const chunks = [];
135
+ redirectRes.on('data', chunk => chunks.push(chunk));
136
+ redirectRes.on('end', () => resolve(Buffer.concat(chunks)));
137
+ });
138
+ redirectReq.on('error', reject);
139
+ redirectReq.end();
140
+ return;
141
+ }
142
+
143
+ if (res.statusCode !== 200) {
144
+ reject(new Error(`Failed to download file: ${res.statusCode}`));
145
+ return;
146
+ }
147
+
148
+ const chunks = [];
149
+ res.on('data', chunk => chunks.push(chunk));
150
+ res.on('end', () => resolve(Buffer.concat(chunks)));
151
+ });
152
+
153
+ req.on('error', reject);
154
+ req.end();
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Get latest release version
160
+ * @param {string} token - GitHub PAT
161
+ * @returns {Promise<{tag: string, name: string, published: string}>}
162
+ */
163
+ async function getLatestRelease(token) {
164
+ try {
165
+ const data = await apiRequest(
166
+ token,
167
+ `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`
168
+ );
169
+ return {
170
+ tag: data.tag_name,
171
+ name: data.name,
172
+ published: data.published_at
173
+ };
174
+ } catch (error) {
175
+ // No releases, use branch
176
+ return null;
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ verifyAccess,
182
+ getRepoTree,
183
+ getFileContent,
184
+ downloadFile,
185
+ getLatestRelease
186
+ };
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Keychain Manager
5
+ *
6
+ * Securely stores GitHub token in OS keychain:
7
+ * - macOS: Keychain Access
8
+ * - Windows: Credential Manager
9
+ * - Linux: libsecret (GNOME Keyring)
10
+ *
11
+ * Falls back to config file if keychain unavailable.
12
+ */
13
+
14
+ const keytar = require('keytar');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, CSK_HOME, CSK_CONFIG_PATH } = require('../constants');
18
+
19
+ /**
20
+ * Check if keytar (native keychain) is available
21
+ */
22
+ async function isKeychainAvailable() {
23
+ try {
24
+ // Try a simple operation to check if keytar works
25
+ await keytar.findCredentials(KEYCHAIN_SERVICE);
26
+ return true;
27
+ } catch (error) {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Store GitHub token securely
34
+ * @param {string} token - GitHub Personal Access Token
35
+ * @returns {Promise<{success: boolean, method: string}>}
36
+ */
37
+ async function storeToken(token) {
38
+ // Try keychain first
39
+ if (await isKeychainAvailable()) {
40
+ try {
41
+ await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
42
+ return { success: true, method: 'keychain' };
43
+ } catch (error) {
44
+ // Fall through to config file
45
+ }
46
+ }
47
+
48
+ // Fallback to config file (less secure)
49
+ try {
50
+ ensureConfigDir();
51
+ const config = loadConfig();
52
+ config.github_token = token;
53
+ saveConfig(config);
54
+ return { success: true, method: 'config' };
55
+ } catch (error) {
56
+ return { success: false, method: 'none', error: error.message };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Retrieve GitHub token
62
+ * @returns {Promise<string|null>}
63
+ */
64
+ async function getToken() {
65
+ // Priority 1: Environment variable
66
+ if (process.env.GITHUB_TOKEN) {
67
+ return process.env.GITHUB_TOKEN;
68
+ }
69
+ if (process.env.GH_TOKEN) {
70
+ return process.env.GH_TOKEN;
71
+ }
72
+
73
+ // Priority 2: GitHub CLI token
74
+ try {
75
+ const { execSync } = require('child_process');
76
+ const ghToken = execSync('gh auth token', { encoding: 'utf-8' }).trim();
77
+ if (ghToken) {
78
+ return ghToken;
79
+ }
80
+ } catch (error) {
81
+ // gh CLI not available or not logged in
82
+ }
83
+
84
+ // Priority 3: OS Keychain
85
+ if (await isKeychainAvailable()) {
86
+ try {
87
+ const token = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
88
+ if (token) {
89
+ return token;
90
+ }
91
+ } catch (error) {
92
+ // Keychain error, try config file
93
+ }
94
+ }
95
+
96
+ // Priority 4: Config file
97
+ try {
98
+ const config = loadConfig();
99
+ if (config.github_token) {
100
+ return config.github_token;
101
+ }
102
+ } catch (error) {
103
+ // No config file
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Delete stored GitHub token
111
+ * @returns {Promise<boolean>}
112
+ */
113
+ async function deleteToken() {
114
+ let deleted = false;
115
+
116
+ // Delete from keychain
117
+ if (await isKeychainAvailable()) {
118
+ try {
119
+ await keytar.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
120
+ deleted = true;
121
+ } catch (error) {
122
+ // Ignore
123
+ }
124
+ }
125
+
126
+ // Delete from config
127
+ try {
128
+ const config = loadConfig();
129
+ if (config.github_token) {
130
+ delete config.github_token;
131
+ saveConfig(config);
132
+ deleted = true;
133
+ }
134
+ } catch (error) {
135
+ // Ignore
136
+ }
137
+
138
+ return deleted;
139
+ }
140
+
141
+ /**
142
+ * Get token storage method currently in use
143
+ * @returns {Promise<string>} 'env', 'gh-cli', 'keychain', 'config', or 'none'
144
+ */
145
+ async function getTokenSource() {
146
+ if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) {
147
+ return 'env';
148
+ }
149
+
150
+ try {
151
+ const { execSync } = require('child_process');
152
+ const ghToken = execSync('gh auth token', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
153
+ if (ghToken) {
154
+ return 'gh-cli';
155
+ }
156
+ } catch (error) {
157
+ // gh CLI not available
158
+ }
159
+
160
+ if (await isKeychainAvailable()) {
161
+ try {
162
+ const token = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
163
+ if (token) {
164
+ return 'keychain';
165
+ }
166
+ } catch (error) {
167
+ // Keychain error
168
+ }
169
+ }
170
+
171
+ try {
172
+ const config = loadConfig();
173
+ if (config.github_token) {
174
+ return 'config';
175
+ }
176
+ } catch (error) {
177
+ // No config
178
+ }
179
+
180
+ return 'none';
181
+ }
182
+
183
+ // Helper functions for config file
184
+ function ensureConfigDir() {
185
+ if (!fs.existsSync(CSK_HOME)) {
186
+ fs.mkdirSync(CSK_HOME, { recursive: true });
187
+ }
188
+ }
189
+
190
+ function loadConfig() {
191
+ if (fs.existsSync(CSK_CONFIG_PATH)) {
192
+ return JSON.parse(fs.readFileSync(CSK_CONFIG_PATH, 'utf-8'));
193
+ }
194
+ return {};
195
+ }
196
+
197
+ function saveConfig(config) {
198
+ ensureConfigDir();
199
+ fs.writeFileSync(CSK_CONFIG_PATH, JSON.stringify(config, null, 2));
200
+ }
201
+
202
+ module.exports = {
203
+ storeToken,
204
+ getToken,
205
+ deleteToken,
206
+ getTokenSource,
207
+ isKeychainAvailable
208
+ };
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Smart Merge Logic
5
+ *
6
+ * Handles intelligent file merging during updates:
7
+ * - Protects user-modified files
8
+ * - Never overwrites sensitive files (.env, *.key)
9
+ * - Preserves custom configurations
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+ const { PROTECTED_FILES, EXCLUDE_PATTERNS } = require('../constants');
16
+
17
+ /**
18
+ * Check if file matches protected patterns
19
+ * @param {string} filePath - Relative file path
20
+ * @returns {boolean}
21
+ */
22
+ function isProtected(filePath) {
23
+ const fileName = path.basename(filePath);
24
+
25
+ for (const pattern of PROTECTED_FILES) {
26
+ if (pattern.includes('*')) {
27
+ // Wildcard pattern
28
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
29
+ if (regex.test(fileName)) {
30
+ return true;
31
+ }
32
+ } else {
33
+ // Exact match or path ends with pattern
34
+ if (fileName === pattern || filePath.endsWith(pattern)) {
35
+ return true;
36
+ }
37
+ }
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * Check if file/directory should be excluded
45
+ * @param {string} filePath - Relative file path
46
+ * @returns {boolean}
47
+ */
48
+ function shouldExclude(filePath) {
49
+ const parts = filePath.split('/');
50
+
51
+ for (const pattern of EXCLUDE_PATTERNS) {
52
+ if (pattern.includes('*')) {
53
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
54
+ if (parts.some(part => regex.test(part))) {
55
+ return true;
56
+ }
57
+ } else {
58
+ if (parts.includes(pattern)) {
59
+ return true;
60
+ }
61
+ }
62
+ }
63
+
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Calculate file hash for change detection
69
+ * @param {string} filePath - Absolute file path
70
+ * @returns {string|null}
71
+ */
72
+ function getFileHash(filePath) {
73
+ if (!fs.existsSync(filePath)) {
74
+ return null;
75
+ }
76
+
77
+ const content = fs.readFileSync(filePath);
78
+ return crypto.createHash('md5').update(content).digest('hex');
79
+ }
80
+
81
+ /**
82
+ * Determine merge action for a file
83
+ * @param {string} targetPath - Absolute path in target directory
84
+ * @param {string} relativePath - Relative path in repo
85
+ * @param {Buffer} newContent - New content from repo
86
+ * @param {Object} manifest - Previous installation manifest
87
+ * @returns {{action: string, reason: string}}
88
+ */
89
+ function determineMergeAction(targetPath, relativePath, newContent, manifest = {}) {
90
+ // Check if protected
91
+ if (isProtected(relativePath)) {
92
+ return { action: 'skip', reason: 'protected' };
93
+ }
94
+
95
+ // Check if file exists
96
+ if (!fs.existsSync(targetPath)) {
97
+ return { action: 'create', reason: 'new' };
98
+ }
99
+
100
+ // Calculate hashes
101
+ const currentHash = getFileHash(targetPath);
102
+ const newHash = crypto.createHash('md5').update(newContent).digest('hex');
103
+ const originalHash = manifest[relativePath];
104
+
105
+ // Same content, skip
106
+ if (currentHash === newHash) {
107
+ return { action: 'skip', reason: 'unchanged' };
108
+ }
109
+
110
+ // Check if user modified the file
111
+ if (originalHash && currentHash !== originalHash) {
112
+ // User modified, don't overwrite
113
+ return { action: 'skip', reason: 'user-modified' };
114
+ }
115
+
116
+ // File changed in repo but user didn't modify
117
+ return { action: 'update', reason: 'repo-updated' };
118
+ }
119
+
120
+ /**
121
+ * Load installation manifest
122
+ * @param {string} projectDir - Project directory
123
+ * @returns {Object}
124
+ */
125
+ function loadManifest(projectDir) {
126
+ const manifestPath = path.join(projectDir, '.csk', 'manifest.json');
127
+ if (fs.existsSync(manifestPath)) {
128
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
129
+ }
130
+ return { files: {}, version: null, installedAt: null };
131
+ }
132
+
133
+ /**
134
+ * Save installation manifest
135
+ * @param {string} projectDir - Project directory
136
+ * @param {Object} manifest - Manifest data
137
+ */
138
+ function saveManifest(projectDir, manifest) {
139
+ const cskDir = path.join(projectDir, '.csk');
140
+ if (!fs.existsSync(cskDir)) {
141
+ fs.mkdirSync(cskDir, { recursive: true });
142
+ }
143
+
144
+ const manifestPath = path.join(cskDir, 'manifest.json');
145
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
146
+ }
147
+
148
+ /**
149
+ * Create directory recursively
150
+ * @param {string} dirPath - Directory path
151
+ */
152
+ function ensureDir(dirPath) {
153
+ if (!fs.existsSync(dirPath)) {
154
+ fs.mkdirSync(dirPath, { recursive: true });
155
+ }
156
+ }
157
+
158
+ module.exports = {
159
+ isProtected,
160
+ shouldExclude,
161
+ getFileHash,
162
+ determineMergeAction,
163
+ loadManifest,
164
+ saveManifest,
165
+ ensureDir
166
+ };