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 +101 -0
- package/bin/csk.js +4 -0
- package/package.json +50 -0
- package/src/commands/auth.js +155 -0
- package/src/commands/init.js +183 -0
- package/src/commands/status.js +86 -0
- package/src/commands/update.js +58 -0
- package/src/constants.js +66 -0
- package/src/index.js +75 -0
- package/src/lib/github.js +186 -0
- package/src/lib/keychain.js +208 -0
- package/src/lib/merge.js +166 -0
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
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;
|
package/src/constants.js
ADDED
|
@@ -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
|
+
};
|
package/src/lib/merge.js
ADDED
|
@@ -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
|
+
};
|