@holic512/slothtool 1.0.1

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,81 @@
1
+ # SlothTool
2
+
3
+ 🐌 A plugin manager and dispatcher for CLI tools.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g slothtool
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Install a plugin
15
+ slothtool install @slothtool/plugin-loc
16
+
17
+ # List installed plugins
18
+ slothtool list
19
+
20
+ # Run a plugin
21
+ slothtool loc ./src
22
+
23
+ # Uninstall a plugin
24
+ slothtool uninstall loc
25
+
26
+ # Get help
27
+ slothtool --help
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ - `slothtool install <plugin>` - Install a plugin from npm
33
+ - `slothtool uninstall <plugin>` - Uninstall a plugin
34
+ - `slothtool list` - List all installed plugins
35
+ - `slothtool run <plugin> [args]` - Run a plugin with arguments
36
+ - `slothtool <plugin> [args]` - Shorthand for running a plugin
37
+
38
+ ## Features
39
+
40
+ - **Zero Global Pollution**: Plugins are installed in `~/.slothtool/plugins/`
41
+ - **Plugin Isolation**: Each plugin has its own dependencies
42
+ - **Simple CLI**: Easy-to-use commands
43
+ - **Shorthand Syntax**: Run plugins directly with `slothtool <plugin>`
44
+
45
+ ## How It Works
46
+
47
+ SlothTool manages CLI tools as plugins:
48
+
49
+ 1. Plugins are npm packages with a `bin` field
50
+ 2. When you install a plugin, it's downloaded to `~/.slothtool/plugins/`
51
+ 3. SlothTool maintains a registry at `~/.slothtool/registry.json`
52
+ 4. When you run a plugin, SlothTool spawns the plugin's executable
53
+
54
+ ## Official Plugins
55
+
56
+ - [@holic512/plugin-loc](../plugin-loc) - Count lines of code
57
+
58
+ ## Creating Plugins
59
+
60
+ Any npm package with a `bin` field can be a SlothTool plugin:
61
+
62
+ ```json
63
+ {
64
+ "name": "@yourscope/plugin-mytool",
65
+ "version": "1.0.0",
66
+ "bin": {
67
+ "mytool": "bin/my-tool.js"
68
+ }
69
+ }
70
+ ```
71
+
72
+ Publish to npm and users can install it:
73
+
74
+ ```bash
75
+ slothtool install @yourscope/plugin-mytool
76
+ slothtool mytool
77
+ ```
78
+
79
+ ## License
80
+
81
+ ISC
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ const commands = require('../lib/commands');
4
+
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+
8
+ // 如果没有参数,显示帮助信息
9
+ if (!command) {
10
+ console.log('🐌 SlothTool - A plugin manager for CLI tools\n');
11
+ console.log('Usage:');
12
+ console.log(' slothtool install <plugin> Install a plugin');
13
+ console.log(' slothtool uninstall <plugin> Uninstall a plugin');
14
+ console.log(' slothtool list List installed plugins');
15
+ console.log(' slothtool run <plugin> [args] Run a plugin');
16
+ console.log(' slothtool <plugin> [args] Run a plugin (shorthand)\n');
17
+ console.log('Examples:');
18
+ console.log(' slothtool install @holic512/plugin-loc');
19
+ console.log(' slothtool loc ./src');
20
+ console.log(' slothtool list');
21
+ process.exit(0);
22
+ }
23
+
24
+ // 内置命令
25
+ if (command === 'install') {
26
+ commands.install(args.slice(1));
27
+ } else if (command === 'uninstall') {
28
+ commands.uninstall(args.slice(1));
29
+ } else if (command === 'list') {
30
+ commands.list();
31
+ } else if (command === 'run') {
32
+ commands.run(args.slice(1));
33
+ } else if (command === '--help' || command === '-h') {
34
+ console.log('🐌 SlothTool - A plugin manager for CLI tools\n');
35
+ console.log('Usage:');
36
+ console.log(' slothtool install <plugin> Install a plugin');
37
+ console.log(' slothtool uninstall <plugin> Uninstall a plugin');
38
+ console.log(' slothtool list List installed plugins');
39
+ console.log(' slothtool run <plugin> [args] Run a plugin');
40
+ console.log(' slothtool <plugin> [args] Run a plugin (shorthand)\n');
41
+ console.log('Examples:');
42
+ console.log(' slothtool install @holic512/plugin-loc');
43
+ console.log(' slothtool loc ./src');
44
+ console.log(' slothtool list');
45
+ process.exit(0);
46
+ } else {
47
+ // 简写形式:slothtool <plugin> [...args]
48
+ // 直接将所有参数传递给 run 命令
49
+ commands.run(args);
50
+ }
@@ -0,0 +1,11 @@
1
+ const install = require('./install');
2
+ const uninstall = require('./uninstall');
3
+ const list = require('./list');
4
+ const run = require('./run');
5
+
6
+ module.exports = {
7
+ install,
8
+ uninstall,
9
+ list,
10
+ run
11
+ };
@@ -0,0 +1,15 @@
1
+ const { installPlugin } = require('../plugin-manager');
2
+
3
+ function install(args) {
4
+ if (args.length === 0) {
5
+ console.error('Error: Please specify a plugin to install.');
6
+ console.log('Usage: slothtool install <plugin-name>');
7
+ console.log('Example: slothtool install @slothtool/plugin-loc');
8
+ process.exit(1);
9
+ }
10
+
11
+ const packageName = args[0];
12
+ installPlugin(packageName);
13
+ }
14
+
15
+ module.exports = install;
@@ -0,0 +1,22 @@
1
+ const registry = require('../registry');
2
+
3
+ function list() {
4
+ const plugins = registry.getAllPlugins();
5
+
6
+ if (Object.keys(plugins).length === 0) {
7
+ console.log('No plugins installed.');
8
+ console.log('\nInstall a plugin with: slothtool install <plugin-name>');
9
+ return;
10
+ }
11
+
12
+ console.log('Installed plugins:\n');
13
+ for (const [alias, info] of Object.entries(plugins)) {
14
+ console.log(` ${alias}`);
15
+ console.log(` Package: ${info.name}`);
16
+ console.log(` Version: ${info.version}`);
17
+ console.log(` Installed: ${new Date(info.installedAt).toLocaleString()}`);
18
+ console.log('');
19
+ }
20
+ }
21
+
22
+ module.exports = list;
@@ -0,0 +1,41 @@
1
+ const { spawn } = require('child_process');
2
+ const registry = require('../registry');
3
+
4
+ function run(args) {
5
+ if (args.length === 0) {
6
+ console.error('Error: Please specify a plugin to run.');
7
+ console.log('Usage: slothtool run <plugin-alias> [args...]');
8
+ console.log(' or: slothtool <plugin-alias> [args...]');
9
+ console.log('\nExample: slothtool run loc ./src');
10
+ console.log(' or: slothtool loc ./src');
11
+ process.exit(1);
12
+ }
13
+
14
+ const pluginAlias = args[0];
15
+ const pluginArgs = args.slice(1);
16
+
17
+ const plugin = registry.getPlugin(pluginAlias);
18
+
19
+ if (!plugin) {
20
+ console.error(`Error: Plugin "${pluginAlias}" not found.`);
21
+ console.log(`\nRun "slothtool list" to see installed plugins.`);
22
+ console.log(`Or install it with: slothtool install <plugin-name>`);
23
+ process.exit(1);
24
+ }
25
+
26
+ // 调用插件
27
+ const child = spawn('node', [plugin.binPath, ...pluginArgs], {
28
+ stdio: 'inherit'
29
+ });
30
+
31
+ child.on('error', (error) => {
32
+ console.error(`Failed to run plugin "${pluginAlias}":`, error.message);
33
+ process.exit(1);
34
+ });
35
+
36
+ child.on('exit', (code) => {
37
+ process.exit(code || 0);
38
+ });
39
+ }
40
+
41
+ module.exports = run;
@@ -0,0 +1,15 @@
1
+ const { uninstallPlugin } = require('../plugin-manager');
2
+
3
+ function uninstall(args) {
4
+ if (args.length === 0) {
5
+ console.error('Error: Please specify a plugin to uninstall.');
6
+ console.log('Usage: slothtool uninstall <plugin-alias>');
7
+ console.log('Example: slothtool uninstall loc');
8
+ process.exit(1);
9
+ }
10
+
11
+ const alias = args[0];
12
+ uninstallPlugin(alias);
13
+ }
14
+
15
+ module.exports = uninstall;
@@ -0,0 +1,122 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const registry = require('./registry');
5
+ const { extractPluginAlias, getPluginDir, ensureDir, getPluginsDir } = require('./utils');
6
+
7
+ /**
8
+ * 安装插件
9
+ * @param {string} packageName - npm 包名 (例如: @slothtool/plugin-loc 或 plugin-loc)
10
+ */
11
+ function installPlugin(packageName) {
12
+ console.log(`Installing plugin: ${packageName}...`);
13
+
14
+ // 提取插件别名
15
+ const alias = extractPluginAlias(packageName);
16
+
17
+ // 检查是否已安装
18
+ if (registry.hasPlugin(alias)) {
19
+ console.log(`Plugin "${alias}" is already installed.`);
20
+ console.log(`Run "slothtool uninstall ${alias}" first if you want to reinstall.`);
21
+ return;
22
+ }
23
+
24
+ // 确保插件目录存在
25
+ ensureDir(getPluginsDir());
26
+
27
+ const pluginDir = getPluginDir(alias);
28
+ ensureDir(pluginDir);
29
+
30
+ try {
31
+ // 使用 npm install 安装插件到指定目录
32
+ console.log(`Installing to: ${pluginDir}`);
33
+ execSync(`npm install ${packageName} --prefix "${pluginDir}"`, {
34
+ stdio: 'inherit',
35
+ encoding: 'utf8'
36
+ });
37
+
38
+ // 读取插件的 package.json
39
+ const pkgPath = path.join(pluginDir, 'node_modules', packageName, 'package.json');
40
+
41
+ if (!fs.existsSync(pkgPath)) {
42
+ throw new Error(`Package.json not found at: ${pkgPath}`);
43
+ }
44
+
45
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
46
+
47
+ // 获取 bin 路径
48
+ let binPath;
49
+ if (typeof pkg.bin === 'string') {
50
+ binPath = path.join(pluginDir, 'node_modules', packageName, pkg.bin);
51
+ } else if (typeof pkg.bin === 'object') {
52
+ // 如果 bin 是对象,取第一个值
53
+ const binKey = Object.keys(pkg.bin)[0];
54
+ binPath = path.join(pluginDir, 'node_modules', packageName, pkg.bin[binKey]);
55
+ } else {
56
+ throw new Error(`No bin field found in package.json for ${packageName}`);
57
+ }
58
+
59
+ // 验证 bin 文件存在
60
+ if (!fs.existsSync(binPath)) {
61
+ throw new Error(`Bin file not found at: ${binPath}`);
62
+ }
63
+
64
+ // 添加到注册表
65
+ registry.addPlugin(alias, {
66
+ name: packageName,
67
+ version: pkg.version,
68
+ binPath: binPath,
69
+ installedAt: new Date().toISOString()
70
+ });
71
+
72
+ console.log(`\n✓ Plugin "${alias}" installed successfully!`);
73
+ console.log(` Run: slothtool ${alias} --help`);
74
+
75
+ } catch (error) {
76
+ console.error(`\n✗ Failed to install plugin "${packageName}":`, error.message);
77
+
78
+ // 清理失败的安装
79
+ if (fs.existsSync(pluginDir)) {
80
+ fs.rmSync(pluginDir, { recursive: true, force: true });
81
+ }
82
+
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 卸载插件
89
+ * @param {string} alias - 插件别名
90
+ */
91
+ function uninstallPlugin(alias) {
92
+ const plugin = registry.getPlugin(alias);
93
+
94
+ if (!plugin) {
95
+ console.error(`Plugin "${alias}" is not installed.`);
96
+ process.exit(1);
97
+ }
98
+
99
+ console.log(`Uninstalling plugin: ${alias}...`);
100
+
101
+ try {
102
+ // 删除插件目录
103
+ const pluginDir = getPluginDir(alias);
104
+ if (fs.existsSync(pluginDir)) {
105
+ fs.rmSync(pluginDir, { recursive: true, force: true });
106
+ }
107
+
108
+ // 从注册表移除
109
+ registry.removePlugin(alias);
110
+
111
+ console.log(`✓ Plugin "${alias}" uninstalled successfully!`);
112
+
113
+ } catch (error) {
114
+ console.error(`✗ Failed to uninstall plugin "${alias}":`, error.message);
115
+ process.exit(1);
116
+ }
117
+ }
118
+
119
+ module.exports = {
120
+ installPlugin,
121
+ uninstallPlugin
122
+ };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const { getRegistryPath, ensureDir, getSlothToolHome } = require('./utils');
3
+
4
+ /**
5
+ * 读取注册表
6
+ * @returns {Object} 注册表对象
7
+ */
8
+ function readRegistry() {
9
+ const registryPath = getRegistryPath();
10
+
11
+ // 确保 .slothtool 目录存在
12
+ ensureDir(getSlothToolHome());
13
+
14
+ // 如果注册表文件不存在,返回空对象
15
+ if (!fs.existsSync(registryPath)) {
16
+ return { plugins: {} };
17
+ }
18
+
19
+ try {
20
+ const content = fs.readFileSync(registryPath, 'utf8');
21
+ return JSON.parse(content);
22
+ } catch (error) {
23
+ console.error('Failed to read registry:', error.message);
24
+ return { plugins: {} };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 写入注册表
30
+ * @param {Object} registry - 注册表对象
31
+ */
32
+ function writeRegistry(registry) {
33
+ const registryPath = getRegistryPath();
34
+
35
+ // 确保 .slothtool 目录存在
36
+ ensureDir(getSlothToolHome());
37
+
38
+ try {
39
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
40
+ } catch (error) {
41
+ console.error('Failed to write registry:', error.message);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 添加插件到注册表
48
+ * @param {string} alias - 插件别名
49
+ * @param {Object} pluginInfo - 插件信息
50
+ */
51
+ function addPlugin(alias, pluginInfo) {
52
+ const registry = readRegistry();
53
+ registry.plugins[alias] = pluginInfo;
54
+ writeRegistry(registry);
55
+ }
56
+
57
+ /**
58
+ * 从注册表移除插件
59
+ * @param {string} alias - 插件别名
60
+ */
61
+ function removePlugin(alias) {
62
+ const registry = readRegistry();
63
+ delete registry.plugins[alias];
64
+ writeRegistry(registry);
65
+ }
66
+
67
+ /**
68
+ * 获取插件信息
69
+ * @param {string} alias - 插件别名
70
+ * @returns {Object|null} 插件信息,如果不存在返回 null
71
+ */
72
+ function getPlugin(alias) {
73
+ const registry = readRegistry();
74
+ return registry.plugins[alias] || null;
75
+ }
76
+
77
+ /**
78
+ * 获取所有插件
79
+ * @returns {Object} 所有插件的映射
80
+ */
81
+ function getAllPlugins() {
82
+ const registry = readRegistry();
83
+ return registry.plugins;
84
+ }
85
+
86
+ /**
87
+ * 检查插件是否已安装
88
+ * @param {string} alias - 插件别名
89
+ * @returns {boolean} 是否已安装
90
+ */
91
+ function hasPlugin(alias) {
92
+ const registry = readRegistry();
93
+ return alias in registry.plugins;
94
+ }
95
+
96
+ module.exports = {
97
+ readRegistry,
98
+ writeRegistry,
99
+ addPlugin,
100
+ removePlugin,
101
+ getPlugin,
102
+ getAllPlugins,
103
+ hasPlugin
104
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,72 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ /**
6
+ * 获取 slothtool 的主目录
7
+ * @returns {string} ~/.slothtool
8
+ */
9
+ function getSlothToolHome() {
10
+ return path.join(os.homedir(), '.slothtool');
11
+ }
12
+
13
+ /**
14
+ * 获取插件安装目录
15
+ * @returns {string} ~/.slothtool/plugins
16
+ */
17
+ function getPluginsDir() {
18
+ return path.join(getSlothToolHome(), 'plugins');
19
+ }
20
+
21
+ /**
22
+ * 获取注册表文件路径
23
+ * @returns {string} ~/.slothtool/registry.json
24
+ */
25
+ function getRegistryPath() {
26
+ return path.join(getSlothToolHome(), 'registry.json');
27
+ }
28
+
29
+ /**
30
+ * 确保目录存在,如果不存在则创建
31
+ * @param {string} dir - 目录路径
32
+ */
33
+ function ensureDir(dir) {
34
+ if (!fs.existsSync(dir)) {
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 从完整的包名中提取插件别名
41
+ * 例如: @slothtool/plugin-loc -> loc
42
+ * plugin-loc -> loc
43
+ * @param {string} packageName - npm 包名
44
+ * @returns {string} 插件别名
45
+ */
46
+ function extractPluginAlias(packageName) {
47
+ // 移除 scope (如果有)
48
+ const withoutScope = packageName.replace(/^@[^/]+\//, '');
49
+
50
+ // 移除 plugin- 前缀 (如果有)
51
+ const alias = withoutScope.replace(/^plugin-/, '');
52
+
53
+ return alias;
54
+ }
55
+
56
+ /**
57
+ * 获取指定插件的安装目录
58
+ * @param {string} pluginAlias - 插件别名
59
+ * @returns {string} 插件安装目录路径
60
+ */
61
+ function getPluginDir(pluginAlias) {
62
+ return path.join(getPluginsDir(), pluginAlias);
63
+ }
64
+
65
+ module.exports = {
66
+ getSlothToolHome,
67
+ getPluginsDir,
68
+ getRegistryPath,
69
+ ensureDir,
70
+ extractPluginAlias,
71
+ getPluginDir
72
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@holic512/slothtool",
3
+ "version": "1.0.1",
4
+ "description": "A plugin manager and dispatcher for CLI tools",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "slothtool": "bin/slothtool.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "cli",
14
+ "plugin",
15
+ "tool",
16
+ "manager"
17
+ ],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "files": [
21
+ "bin",
22
+ "lib"
23
+ ]
24
+ }