@allanpk716/work-skills-setup 0.1.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/bin/setup.js +10 -0
- package/package.json +38 -0
- package/src/cli.js +44 -0
- package/src/configurators/git-ssh.js +84 -0
- package/src/configurators/git-user.js +78 -0
- package/src/configurators/index.js +78 -0
- package/src/configurators/pushover.js +195 -0
- package/src/detectors/git.js +37 -0
- package/src/detectors/index.js +56 -0
- package/src/detectors/pip-package.js +43 -0
- package/src/detectors/python.js +48 -0
- package/src/detectors/ssh-tools.js +94 -0
- package/src/i18n/en.json +112 -0
- package/src/i18n/index.js +70 -0
- package/src/i18n/zh.json +112 -0
- package/src/index.js +61 -0
- package/src/installers/index.js +92 -0
- package/src/installers/pip-installer.js +63 -0
- package/src/marketplace/config-manager.js +87 -0
- package/src/marketplace/index.js +157 -0
- package/src/marketplace/plugin-discovery.js +68 -0
- package/src/marketplace/plugin-installer.js +144 -0
- package/src/platform.js +34 -0
- package/src/verification/formatter.js +64 -0
- package/src/verification/index.js +79 -0
- package/src/verification/parser.js +53 -0
- package/src/verification/runner.js +72 -0
- package/src/welcome.js +50 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get Claude Code config.json path
|
|
9
|
+
* @returns {string} Full path to config.json
|
|
10
|
+
*/
|
|
11
|
+
function getConfigPath() {
|
|
12
|
+
return path.join(os.homedir(), '.claude', 'config.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read Claude Code configuration
|
|
17
|
+
* @returns {Object} Parsed config object (empty object if file missing or invalid)
|
|
18
|
+
*/
|
|
19
|
+
function readClaudeConfig() {
|
|
20
|
+
const configPath = getConfigPath();
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(configPath)) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// Parse error or read error - return empty config
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write Claude Code configuration
|
|
37
|
+
* @param {Object} config - Configuration object to write
|
|
38
|
+
*/
|
|
39
|
+
function writeClaudeConfig(config) {
|
|
40
|
+
const configPath = getConfigPath();
|
|
41
|
+
|
|
42
|
+
// Ensure directory exists
|
|
43
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Write formatted JSON
|
|
46
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register work-skills as a marketplace source in Claude Code config
|
|
51
|
+
* @returns {{success: boolean, path: string, error?: string}}
|
|
52
|
+
*/
|
|
53
|
+
function registerMarketplaceSource() {
|
|
54
|
+
try {
|
|
55
|
+
const config = readClaudeConfig();
|
|
56
|
+
|
|
57
|
+
// Ensure marketplaceSources object exists
|
|
58
|
+
config.marketplaceSources = config.marketplaceSources || {};
|
|
59
|
+
|
|
60
|
+
// Add work-skills entry
|
|
61
|
+
config.marketplaceSources['work-skills'] = {
|
|
62
|
+
type: 'github',
|
|
63
|
+
url: 'https://github.com/allanpk716/work-skills',
|
|
64
|
+
branch: 'main'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
writeClaudeConfig(config);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
path: getConfigPath()
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
path: getConfigPath(),
|
|
77
|
+
error: error.message
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
getConfigPath,
|
|
84
|
+
readClaudeConfig,
|
|
85
|
+
writeClaudeConfig,
|
|
86
|
+
registerMarketplaceSource
|
|
87
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { MultiSelect } = require('enquirer');
|
|
5
|
+
const { t } = require('../i18n/index.js');
|
|
6
|
+
const { registerMarketplaceSource } = require('./config-manager.js');
|
|
7
|
+
const { fetchMarketplaceJson, parsePluginList } = require('./plugin-discovery.js');
|
|
8
|
+
const { isPluginInstalled, installPlugins } = require('./plugin-installer.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Display plugin list as a table
|
|
12
|
+
* @param {Array} plugins - List of plugins to display
|
|
13
|
+
*/
|
|
14
|
+
function displayPluginTable(plugins) {
|
|
15
|
+
console.log('\n' + chalk.bold(t('marketplace.available')));
|
|
16
|
+
console.log();
|
|
17
|
+
|
|
18
|
+
// Simple table format (no external table library)
|
|
19
|
+
const separator = chalk.gray('|----------------------|---------|------------------------------------------|');
|
|
20
|
+
const header = chalk.gray('| Name | Version | Description |');
|
|
21
|
+
console.log(separator);
|
|
22
|
+
console.log(header);
|
|
23
|
+
console.log(separator);
|
|
24
|
+
|
|
25
|
+
plugins.forEach(plugin => {
|
|
26
|
+
const name = plugin.name.padEnd(20).substring(0, 20);
|
|
27
|
+
const version = plugin.version.padEnd(7).substring(0, 7);
|
|
28
|
+
const desc = (plugin.description || '').padEnd(40).substring(0, 40);
|
|
29
|
+
const installed = isPluginInstalled(plugin.name);
|
|
30
|
+
const status = installed ? chalk.green(' [installed]') : '';
|
|
31
|
+
console.log(`| ${chalk.cyan(name)} | ${chalk.yellow(version)} | ${desc}|${status}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(separator);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Display installation summary
|
|
39
|
+
* @param {Object} result - {installed, skipped, failed}
|
|
40
|
+
*/
|
|
41
|
+
function displayInstallSummary(result) {
|
|
42
|
+
console.log('\n' + chalk.bold(t('marketplace.summary')));
|
|
43
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
44
|
+
|
|
45
|
+
if (result.installed.length > 0) {
|
|
46
|
+
console.log(chalk.green('✓ ' + t('marketplace.summary_installed', { count: result.installed.length })));
|
|
47
|
+
result.installed.forEach(p => console.log(chalk.gray(` - ${p.name} v${p.version}`)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (result.skipped.length > 0) {
|
|
51
|
+
console.log(chalk.yellow('⊘ ' + t('marketplace.summary_skipped', { count: result.skipped.length })));
|
|
52
|
+
result.skipped.forEach(p => console.log(chalk.gray(` - ${p.name}`)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (result.failed.length > 0) {
|
|
56
|
+
console.log(chalk.red('✗ ' + t('marketplace.summary_failed', { count: result.failed.length })));
|
|
57
|
+
result.failed.forEach(p => console.log(chalk.gray(` - ${p.name}: ${p.error}`)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
61
|
+
|
|
62
|
+
if (result.installed.length > 0) {
|
|
63
|
+
console.log(chalk.green('\n' + t('marketplace.complete')));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Main marketplace integration function
|
|
69
|
+
* @returns {Promise<{success: boolean, installed: number, skipped: number, failed: number}>}
|
|
70
|
+
*/
|
|
71
|
+
async function runMarketplaceIntegration() {
|
|
72
|
+
console.log(chalk.bold.blue('\n=== ' + t('marketplace.title') + ' ===\n'));
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Step 1: Register marketplace source
|
|
76
|
+
console.log(chalk.gray(t('marketplace.registering')));
|
|
77
|
+
const registerResult = registerMarketplaceSource();
|
|
78
|
+
|
|
79
|
+
if (!registerResult.success) {
|
|
80
|
+
console.log(chalk.red('✗ Failed to register marketplace source'));
|
|
81
|
+
return { success: false, installed: 0, skipped: 0, failed: 0, error: registerResult.error };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(chalk.green('✓ ' + t('marketplace.registered')));
|
|
85
|
+
|
|
86
|
+
// Step 2: Fetch plugin list
|
|
87
|
+
console.log(chalk.gray('\n' + t('marketplace.fetching')));
|
|
88
|
+
const marketplaceData = await fetchMarketplaceJson();
|
|
89
|
+
const plugins = parsePluginList(marketplaceData);
|
|
90
|
+
|
|
91
|
+
if (plugins.length === 0) {
|
|
92
|
+
console.log(chalk.yellow('No plugins available.'));
|
|
93
|
+
return { success: true, installed: 0, skipped: 0, failed: 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Step 3: Display plugins and prompt for selection
|
|
97
|
+
displayPluginTable(plugins);
|
|
98
|
+
|
|
99
|
+
const multiselect = new MultiSelect({
|
|
100
|
+
name: 'plugins',
|
|
101
|
+
message: t('marketplace.select_plugins'),
|
|
102
|
+
choices: plugins.map(p => ({
|
|
103
|
+
name: p.name,
|
|
104
|
+
message: `${p.name} (${p.version}) - ${(p.description || '').substring(0, 40)}...`,
|
|
105
|
+
disabled: isPluginInstalled(p.name)
|
|
106
|
+
})),
|
|
107
|
+
initial: []
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const selectedNames = await multiselect.run();
|
|
111
|
+
|
|
112
|
+
if (selectedNames.length === 0) {
|
|
113
|
+
console.log(chalk.yellow('\n' + t('marketplace.none_selected')));
|
|
114
|
+
const alreadyInstalledCount = plugins.filter(p => isPluginInstalled(p.name)).length;
|
|
115
|
+
return { success: true, installed: 0, skipped: alreadyInstalledCount, failed: 0 };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 4: Install selected plugins
|
|
119
|
+
const selectedPlugins = plugins.filter(p => selectedNames.includes(p.name));
|
|
120
|
+
|
|
121
|
+
const result = await installPlugins(selectedPlugins, {
|
|
122
|
+
onProgress: (plugin, status) => {
|
|
123
|
+
if (status === 'installing') {
|
|
124
|
+
console.log(chalk.gray(` ${t('marketplace.installing', { name: plugin.name })}`));
|
|
125
|
+
} else if (status === 'installed') {
|
|
126
|
+
console.log(chalk.green(` ✓ ${t('marketplace.install_success', { name: plugin.name })}`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Step 5: Display summary
|
|
132
|
+
displayInstallSummary(result);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
success: result.failed.length === 0,
|
|
136
|
+
installed: result.installed.length,
|
|
137
|
+
skipped: result.skipped.length,
|
|
138
|
+
failed: result.failed.length
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Handle specific errors
|
|
143
|
+
if (error.message.includes('Network') || error.message.includes('timeout')) {
|
|
144
|
+
console.log(chalk.red('\n✗ ' + t('marketplace.error.network')));
|
|
145
|
+
} else {
|
|
146
|
+
console.log(chalk.red('\n✗ ' + t('marketplace.error.unknown')));
|
|
147
|
+
console.log(chalk.gray(' ' + error.message));
|
|
148
|
+
}
|
|
149
|
+
return { success: false, installed: 0, skipped: 0, failed: 0, error: error.message };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
runMarketplaceIntegration,
|
|
155
|
+
displayPluginTable,
|
|
156
|
+
displayInstallSummary
|
|
157
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GitHub raw content URL for marketplace.json
|
|
7
|
+
*/
|
|
8
|
+
const MARKETPLACE_URL = 'https://raw.githubusercontent.com/allanpk716/work-skills/main/.claude-plugin/marketplace.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch marketplace.json from GitHub
|
|
12
|
+
* @param {number} timeout - Request timeout in milliseconds (default: 10000)
|
|
13
|
+
* @returns {Promise<Object>} Parsed marketplace.json object
|
|
14
|
+
*/
|
|
15
|
+
function fetchMarketplaceJson(timeout = 10000) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const req = https.get(MARKETPLACE_URL, { timeout }, (res) => {
|
|
18
|
+
let data = '';
|
|
19
|
+
|
|
20
|
+
res.on('data', (chunk) => {
|
|
21
|
+
data += chunk;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(data);
|
|
27
|
+
resolve(parsed);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
reject(new Error('Failed to parse marketplace.json: ' + err.message));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
req.on('error', (err) => {
|
|
35
|
+
reject(new Error('Network error: ' + err.message));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
req.on('timeout', () => {
|
|
39
|
+
req.destroy();
|
|
40
|
+
reject(new Error('Request timeout'));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse plugin list from marketplace data
|
|
47
|
+
* @param {Object} marketplaceData - Raw marketplace.json data
|
|
48
|
+
* @returns {Array<{name, description, version, source, category}>}
|
|
49
|
+
*/
|
|
50
|
+
function parsePluginList(marketplaceData) {
|
|
51
|
+
if (!marketplaceData.plugins || !Array.isArray(marketplaceData.plugins)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return marketplaceData.plugins.map(p => ({
|
|
56
|
+
name: p.name,
|
|
57
|
+
description: p.description || '',
|
|
58
|
+
version: p.version || '0.0.0',
|
|
59
|
+
source: p.source || '',
|
|
60
|
+
category: p.category || 'general'
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
MARKETPLACE_URL,
|
|
66
|
+
fetchMarketplaceJson,
|
|
67
|
+
parsePluginList
|
|
68
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const execa = require('execa');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GitHub repository URL for work-skills
|
|
10
|
+
*/
|
|
11
|
+
const REPO_URL = 'https://github.com/allanpk716/work-skills.git';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get Claude Code skills directory path
|
|
15
|
+
* @returns {string} Path to ~/.claude/skills
|
|
16
|
+
*/
|
|
17
|
+
function getSkillsDir() {
|
|
18
|
+
return path.join(os.homedir(), '.claude', 'skills');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a plugin is already installed
|
|
23
|
+
* @param {string} pluginName - Name of the plugin
|
|
24
|
+
* @returns {boolean} True if SKILL.md exists in plugin directory
|
|
25
|
+
*/
|
|
26
|
+
function isPluginInstalled(pluginName) {
|
|
27
|
+
const skillPath = path.join(getSkillsDir(), pluginName, 'SKILL.md');
|
|
28
|
+
return fs.existsSync(skillPath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Install a single plugin
|
|
33
|
+
* @param {Object} plugin - Plugin object {name, version, source}
|
|
34
|
+
* @param {Object} options - Options {tempDir, onProgress}
|
|
35
|
+
* @returns {Promise<{success: boolean, path?: string, error?: string}>}
|
|
36
|
+
*/
|
|
37
|
+
async function installPlugin(plugin, options = {}) {
|
|
38
|
+
let tempDir = null;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Create temporary directory for cloning
|
|
42
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'work-skills-'));
|
|
43
|
+
|
|
44
|
+
// Notify progress
|
|
45
|
+
if (options.onProgress) {
|
|
46
|
+
options.onProgress(plugin, 'cloning');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Clone repository with depth 1 (shallow clone)
|
|
50
|
+
await execa('git', ['clone', '--depth', '1', REPO_URL, tempDir]);
|
|
51
|
+
|
|
52
|
+
if (options.onProgress) {
|
|
53
|
+
options.onProgress(plugin, 'copying');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Source and target paths
|
|
57
|
+
const sourcePath = path.join(tempDir, plugin.source.replace('./', ''));
|
|
58
|
+
const targetPath = path.join(getSkillsDir(), plugin.name);
|
|
59
|
+
|
|
60
|
+
// Ensure target directory exists
|
|
61
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
62
|
+
|
|
63
|
+
// Copy plugin directory to skills directory
|
|
64
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
65
|
+
|
|
66
|
+
// Clean up temp directory
|
|
67
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
68
|
+
|
|
69
|
+
if (options.onProgress) {
|
|
70
|
+
options.onProgress(plugin, 'installed');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
path: targetPath
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Clean up on error
|
|
79
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
80
|
+
try {
|
|
81
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
82
|
+
} catch (cleanupError) {
|
|
83
|
+
// Ignore cleanup errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: error.message || 'Unknown error'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Install multiple plugins
|
|
96
|
+
* @param {Array} plugins - Array of plugin objects {name, version, source}
|
|
97
|
+
* @param {Object} options - Options {onProgress}
|
|
98
|
+
* @returns {Promise<{installed: Array, skipped: Array, failed: Array}>}
|
|
99
|
+
*/
|
|
100
|
+
async function installPlugins(plugins, options = {}) {
|
|
101
|
+
const result = {
|
|
102
|
+
installed: [],
|
|
103
|
+
skipped: [],
|
|
104
|
+
failed: []
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (const plugin of plugins) {
|
|
108
|
+
// Check if already installed
|
|
109
|
+
if (isPluginInstalled(plugin.name)) {
|
|
110
|
+
result.skipped.push({
|
|
111
|
+
name: plugin.name,
|
|
112
|
+
version: plugin.version
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Install plugin
|
|
118
|
+
const installResult = await installPlugin(plugin, options);
|
|
119
|
+
|
|
120
|
+
if (installResult.success) {
|
|
121
|
+
result.installed.push({
|
|
122
|
+
name: plugin.name,
|
|
123
|
+
version: plugin.version,
|
|
124
|
+
path: installResult.path
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
result.failed.push({
|
|
128
|
+
name: plugin.name,
|
|
129
|
+
version: plugin.version,
|
|
130
|
+
error: installResult.error
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
REPO_URL,
|
|
140
|
+
getSkillsDir,
|
|
141
|
+
isPluginInstalled,
|
|
142
|
+
installPlugin,
|
|
143
|
+
installPlugins
|
|
144
|
+
};
|
package/src/platform.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { t } = require('./i18n/index.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if current platform is Windows
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
function isWindows() {
|
|
10
|
+
return process.platform === 'win32';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check platform and exit with error if not Windows
|
|
15
|
+
* This function checks if the current platform is Windows.
|
|
16
|
+
* If not Windows, it prints an error message and exits with code 1.
|
|
17
|
+
* If Windows, it returns true.
|
|
18
|
+
* @returns {boolean} - true if Windows, never returns false (exits instead)
|
|
19
|
+
*/
|
|
20
|
+
function checkPlatform() {
|
|
21
|
+
if (!isWindows()) {
|
|
22
|
+
console.error('');
|
|
23
|
+
console.error(t('error.windowsOnly'));
|
|
24
|
+
console.error(t('error.currentPlatform') + ': ' + process.platform);
|
|
25
|
+
console.error('');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
isWindows,
|
|
33
|
+
checkPlatform
|
|
34
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format verification results as a simple ASCII table
|
|
7
|
+
* @param {Array<{name: string, status: string, symbol: string, details: string}>} results
|
|
8
|
+
* @returns {string} Formatted table string
|
|
9
|
+
*/
|
|
10
|
+
function formatVerificationTable(results) {
|
|
11
|
+
const separator = chalk.gray('|---------------------------|------------|--------------------------------------------------|');
|
|
12
|
+
const header = chalk.gray('| Check | Status | Details |');
|
|
13
|
+
|
|
14
|
+
const lines = [separator, header, separator];
|
|
15
|
+
|
|
16
|
+
results.forEach(result => {
|
|
17
|
+
let statusIcon;
|
|
18
|
+
if (result.status === 'PASS') {
|
|
19
|
+
statusIcon = chalk.green('✓ PASS');
|
|
20
|
+
} else if (result.status === 'FAIL') {
|
|
21
|
+
statusIcon = chalk.red('✗ FAIL');
|
|
22
|
+
} else {
|
|
23
|
+
statusIcon = chalk.gray('⊘ SKIP');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const name = result.name.padEnd(25).substring(0, 25);
|
|
27
|
+
const status = statusIcon.padEnd(10);
|
|
28
|
+
const details = (result.details || '').padEnd(48).substring(0, 48);
|
|
29
|
+
|
|
30
|
+
lines.push(`| ${name} | ${status} | ${details}|`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
lines.push(separator);
|
|
34
|
+
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calculate verification summary statistics
|
|
40
|
+
* @param {Array<{status: string}>} results
|
|
41
|
+
* @returns {{passed: number, total: number}}
|
|
42
|
+
*/
|
|
43
|
+
function calculateSummary(results) {
|
|
44
|
+
const passed = results.filter(r => r.status === 'PASS').length;
|
|
45
|
+
const total = results.length;
|
|
46
|
+
|
|
47
|
+
return { passed, total };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Display common solutions for failed checks
|
|
52
|
+
*/
|
|
53
|
+
function displayCommonSolutions() {
|
|
54
|
+
console.log(chalk.yellow('\nCommon solutions:'));
|
|
55
|
+
console.log(' - Install missing Python libraries: pip install requests');
|
|
56
|
+
console.log(' - Set environment variables: PUSHOVER_TOKEN, PUSHOVER_USER');
|
|
57
|
+
console.log(' - Check PowerShell execution policy');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
formatVerificationTable,
|
|
62
|
+
calculateSummary,
|
|
63
|
+
displayCommonSolutions
|
|
64
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { t } = require('../i18n/index.js');
|
|
5
|
+
const { runPythonVerification } = require('./runner.js');
|
|
6
|
+
const { parseVerificationOutput } = require('./parser.js');
|
|
7
|
+
const { formatVerificationTable, calculateSummary, displayCommonSolutions } = require('./formatter.js');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run installation verification and display results
|
|
11
|
+
* @returns {Promise<{success: boolean, passed: number, total: number}>}
|
|
12
|
+
*/
|
|
13
|
+
async function runVerification() {
|
|
14
|
+
// Display verification title
|
|
15
|
+
console.log(chalk.bold.blue('\n=== ' + t('verification.title') + ' ===\n'));
|
|
16
|
+
|
|
17
|
+
// Step 1: Execute Python verification script
|
|
18
|
+
const execResult = await runPythonVerification();
|
|
19
|
+
|
|
20
|
+
// Step 2: Handle execution errors
|
|
21
|
+
if (!execResult.success) {
|
|
22
|
+
if (execResult.error === 'python_not_found') {
|
|
23
|
+
console.error(chalk.red(t('verification.error.pythonNotFound')));
|
|
24
|
+
console.error(chalk.gray('Install Python 3.8+ from https://www.python.org/'));
|
|
25
|
+
return { success: false, passed: 0, total: 0 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (execResult.error === 'script_not_found') {
|
|
29
|
+
console.error(chalk.red(t('verification.error.scriptNotFound')));
|
|
30
|
+
return { success: false, passed: 0, total: 0 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (execResult.error === 'timeout') {
|
|
34
|
+
console.error(chalk.red(t('verification.error.timeout')));
|
|
35
|
+
return { success: false, passed: 0, total: 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generic execution failure
|
|
39
|
+
console.error(chalk.red(t('verification.error.executionFailed')));
|
|
40
|
+
if (execResult.stderr) {
|
|
41
|
+
console.error(chalk.gray(execResult.stderr));
|
|
42
|
+
}
|
|
43
|
+
return { success: false, passed: 0, total: 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Step 3: Parse output
|
|
47
|
+
const results = parseVerificationOutput(execResult.stdout);
|
|
48
|
+
|
|
49
|
+
// Step 4: Format and display table
|
|
50
|
+
const table = formatVerificationTable(results);
|
|
51
|
+
console.log(table);
|
|
52
|
+
|
|
53
|
+
// Step 5: Calculate and display summary
|
|
54
|
+
const { passed, total } = calculateSummary(results);
|
|
55
|
+
console.log('\n' + t('verification.summary', { passed, total }));
|
|
56
|
+
|
|
57
|
+
// Step 6: Display common solutions if any checks failed
|
|
58
|
+
if (passed < total) {
|
|
59
|
+
displayCommonSolutions();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Step 7: Display rerun command
|
|
63
|
+
console.log(chalk.gray('\n' + t('verification.rerunCommand')));
|
|
64
|
+
|
|
65
|
+
// Step 8: Display next steps guidance
|
|
66
|
+
console.log(chalk.cyan('\n=== ' + t('verification.nextSteps') + ' ==='));
|
|
67
|
+
console.log(chalk.white(t('verification.nextStep1')));
|
|
68
|
+
console.log(chalk.white(t('verification.nextStep2')));
|
|
69
|
+
console.log(chalk.white(t('verification.nextStep3')));
|
|
70
|
+
console.log(chalk.gray(t('verification.nextStep4')));
|
|
71
|
+
|
|
72
|
+
// Step 9: Return result (success requires at least 5/7 checks to pass)
|
|
73
|
+
const success = passed >= 5;
|
|
74
|
+
return { success, passed, total };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
runVerification
|
|
79
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse Python verification script output into structured results
|
|
5
|
+
* @param {string} stdout - Raw output from Python script
|
|
6
|
+
* @returns {Array<{name: string, status: string, symbol: string, details: string}>}
|
|
7
|
+
*/
|
|
8
|
+
function parseVerificationOutput(stdout) {
|
|
9
|
+
if (!stdout || stdout.trim() === '') {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const lines = stdout.split('\n');
|
|
14
|
+
const results = [];
|
|
15
|
+
|
|
16
|
+
// Pattern: " [OK] Python version: PASS"
|
|
17
|
+
// Pattern: " [X] Environment Variables: FAIL"
|
|
18
|
+
// Pattern: " [OK] Pushover API: SKIP"
|
|
19
|
+
const pattern = /^\s*\[(OK|X)\]\s*(.+?):\s*(PASS|FAIL|SKIP)/;
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i];
|
|
23
|
+
const match = line.match(pattern);
|
|
24
|
+
|
|
25
|
+
if (match) {
|
|
26
|
+
const [, symbol, name, status] = match;
|
|
27
|
+
|
|
28
|
+
// Check if next line is a detail line (indented, not a result line)
|
|
29
|
+
let details = '';
|
|
30
|
+
if (i + 1 < lines.length) {
|
|
31
|
+
const nextLine = lines[i + 1].trim();
|
|
32
|
+
// Detail lines start with spaces but don't match the result pattern
|
|
33
|
+
if (nextLine && !nextLine.match(/^\s*\[(OK|X)\]/)) {
|
|
34
|
+
details = nextLine;
|
|
35
|
+
i++; // Skip the detail line in next iteration
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
results.push({
|
|
40
|
+
name: name.trim(),
|
|
41
|
+
status, // 'PASS' | 'FAIL' | 'SKIP'
|
|
42
|
+
symbol, // 'OK' | 'X'
|
|
43
|
+
details
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
parseVerificationOutput
|
|
53
|
+
};
|