bin-home 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/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # bin-home
2
+
3
+ bin-home is a CLI tool that finds which global npm package provides a given
4
+ command. It helps when you know the command name (for example, `codex`) but not
5
+ the package you need to install or manage (for example, `@openai/codex`).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g bin-home
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ bin-home <command> [--open]
17
+ ```
18
+
19
+ ### Examples
20
+
21
+ ```bash
22
+ bin-home codex
23
+ ```
24
+
25
+ Expected output:
26
+
27
+ ```text
28
+ npm: @openai/codex
29
+ npm url: https://www.npmjs.com/package/@openai/codex
30
+ github: https://github.com/openai/codex
31
+ ```
32
+
33
+ Open npm package page automatically:
34
+
35
+ ```bash
36
+ bin-home codex --open
37
+ ```
38
+
39
+ ### Options
40
+
41
+ - `--help`, `-h`: Show help
42
+ - `--version`, `-v`: Show version
43
+ - `--open`, `-o`: Open npm package page in browser
44
+
45
+ ## Publishing
46
+
47
+ To publish to npm and GitHub:
48
+
49
+ 1. Update `package.json` metadata (name, version, repository, author).
50
+ 2. Create a GitHub repository and push the code.
51
+ 3. Run `npm publish`.
52
+
53
+ ---
54
+
55
+ # bin-home(中文)
56
+
57
+ bin-home 是一个 CLI 工具,用于查找某个命令来自哪个全局 npm 包。它解决了
58
+ “知道命令名但不知道包名”的痛点,比如命令是 `codex`,但包名是
59
+ `@openai/codex`。
60
+
61
+ ## 安装
62
+
63
+ ```bash
64
+ npm install -g bin-home
65
+ ```
66
+
67
+ ## 使用方法
68
+
69
+ ```bash
70
+ bin-home <命令名> [--open]
71
+ ```
72
+
73
+ ### 示例
74
+
75
+ ```bash
76
+ bin-home codex
77
+ ```
78
+
79
+ 预期输出:
80
+
81
+ ```text
82
+ npm: @openai/codex
83
+ npm url: https://www.npmjs.com/package/@openai/codex
84
+ github: https://github.com/openai/codex
85
+ ```
86
+
87
+ 自动打开 npm 包页面:
88
+
89
+ ```bash
90
+ bin-home codex --open
91
+ ```
92
+
93
+ ### 选项
94
+
95
+ - `--help`, `-h`: 显示帮助
96
+ - `--version`, `-v`: 显示版本号
97
+ - `--open`, `-o`: 打开 npm 包页面
98
+
99
+ ## 发布说明
100
+
101
+ 发布到 npm 和 GitHub 的步骤:
102
+
103
+ 1. 更新 `package.json` 元数据(name、version、repository、author)。
104
+ 2. 创建 GitHub 仓库并推送代码。
105
+ 3. 运行 `npm publish`。
package/bin/cli.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ const path = require("path");
3
+ const validateCommand = require("../lib/commandValidator");
4
+ const findPackageForCommand = require("../lib/packageFinder");
5
+ const displayPackageInfo = require("../lib/packageInfoFormatter");
6
+
7
+ const COMMAND_NOT_FOUND_EXIT_CODE = 1;
8
+ const PACKAGE_NOT_FOUND_EXIT_CODE = 2;
9
+
10
+ function printHelp() {
11
+ console.log("Usage: bin-home <command> [--open]");
12
+ console.log("");
13
+ console.log("Options:");
14
+ console.log(" --help, -h Show help");
15
+ console.log(" --version, -v Show version");
16
+ console.log(" --open, -o Open npm package page in browser");
17
+ }
18
+
19
+ function printVersion() {
20
+ const packageJson = require(path.join(__dirname, "..", "package.json"));
21
+ console.log(packageJson.version);
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ const flags = new Set();
26
+ let commandName = null;
27
+
28
+ for (const arg of argv) {
29
+ if (arg.startsWith("-")) {
30
+ flags.add(arg);
31
+ } else if (!commandName) {
32
+ commandName = arg;
33
+ }
34
+ }
35
+
36
+ return {
37
+ commandName,
38
+ help: flags.has("--help") || flags.has("-h"),
39
+ version: flags.has("--version") || flags.has("-v"),
40
+ open: flags.has("--open") || flags.has("-o")
41
+ };
42
+ }
43
+
44
+ async function run() {
45
+ const { commandName, help, version, open } = parseArgs(process.argv.slice(2));
46
+
47
+ if (version) {
48
+ printVersion();
49
+ return;
50
+ }
51
+
52
+ if (help || !commandName) {
53
+ printHelp();
54
+ return;
55
+ }
56
+
57
+ const commandExists = await validateCommand(commandName);
58
+ if (!commandExists) {
59
+ console.error(`Command '${commandName}' not found in system PATH`);
60
+ process.exit(COMMAND_NOT_FOUND_EXIT_CODE);
61
+ }
62
+
63
+ const packageInfo = await findPackageForCommand(commandName);
64
+ if (!packageInfo) {
65
+ console.error(
66
+ `No npm package found for command '${commandName}'. It may not be installed via npm.`
67
+ );
68
+ process.exit(PACKAGE_NOT_FOUND_EXIT_CODE);
69
+ }
70
+
71
+ await displayPackageInfo(commandName, packageInfo, open);
72
+ }
73
+
74
+ run().catch((error) => {
75
+ if (typeof error.exitCode === "number") {
76
+ console.error(error.message);
77
+ process.exit(error.exitCode);
78
+ }
79
+
80
+ console.error(`Unexpected error: ${error.message}`);
81
+ process.exit(1);
82
+ });
@@ -0,0 +1,16 @@
1
+ const which = require("which");
2
+
3
+ async function validateCommand(commandName) {
4
+ if (!commandName || typeof commandName !== "string") {
5
+ return false;
6
+ }
7
+
8
+ try {
9
+ await which(commandName);
10
+ return true;
11
+ } catch (error) {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ module.exports = validateCommand;
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ validateCommand: require("./commandValidator"),
3
+ findPackageForCommand: require("./packageFinder"),
4
+ parseRepository: require("./repositoryParser"),
5
+ displayPackageInfo: require("./packageInfoFormatter")
6
+ };
@@ -0,0 +1,236 @@
1
+ const { exec } = require("child_process");
2
+ const which = require("which");
3
+ const fs = require("fs/promises");
4
+ const path = require("path");
5
+
6
+ const NPM_LIST_EXIT_CODE = 3;
7
+ const PACKAGE_JSON_EXIT_CODE = 4;
8
+
9
+ function execNpmList() {
10
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
11
+ const command = `${npmCommand} ls -g --depth=0 --json`;
12
+ return new Promise((resolve, reject) => {
13
+ exec(
14
+ command,
15
+ { maxBuffer: 10 * 1024 * 1024, windowsHide: true },
16
+ (error, stdout) => {
17
+ if (error) {
18
+ const err = new Error(
19
+ `Failed to list global npm packages: ${error.message}`
20
+ );
21
+ err.exitCode = NPM_LIST_EXIT_CODE;
22
+ return reject(err);
23
+ }
24
+ resolve(stdout);
25
+ }
26
+ );
27
+ });
28
+ }
29
+
30
+ function execNpmRoot() {
31
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
32
+ const command = `${npmCommand} root -g`;
33
+ return new Promise((resolve, reject) => {
34
+ exec(command, { windowsHide: true }, (error, stdout) => {
35
+ if (error) {
36
+ const err = new Error(
37
+ `Failed to list global npm packages: ${error.message}`
38
+ );
39
+ err.exitCode = NPM_LIST_EXIT_CODE;
40
+ return reject(err);
41
+ }
42
+ resolve(stdout.trim());
43
+ });
44
+ });
45
+ }
46
+
47
+ function getCommandNamesForStringBin(packageName) {
48
+ const names = new Set();
49
+ if (packageName) {
50
+ names.add(packageName);
51
+ if (packageName.startsWith("@")) {
52
+ const parts = packageName.split("/");
53
+ if (parts[1]) {
54
+ names.add(parts[1]);
55
+ }
56
+ }
57
+ }
58
+ return names;
59
+ }
60
+
61
+ async function readPackageJson(packagePath, packageName) {
62
+ const packageJsonPath = path.join(packagePath, "package.json");
63
+ try {
64
+ const contents = await fs.readFile(packageJsonPath, "utf8");
65
+ return JSON.parse(contents);
66
+ } catch (error) {
67
+ const err = new Error(
68
+ `Failed to read package.json for ${packageName}: ${error.message}`
69
+ );
70
+ err.exitCode = PACKAGE_JSON_EXIT_CODE;
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ async function listGlobalPackages(nodeModulesRoot) {
76
+ try {
77
+ const entries = await fs.readdir(nodeModulesRoot, { withFileTypes: true });
78
+ const packages = [];
79
+
80
+ for (const entry of entries) {
81
+ if (!entry.isDirectory()) {
82
+ continue;
83
+ }
84
+
85
+ if (entry.name.startsWith("@")) {
86
+ const scopePath = path.join(nodeModulesRoot, entry.name);
87
+ const scopedEntries = await fs.readdir(scopePath, {
88
+ withFileTypes: true
89
+ });
90
+ for (const scopedEntry of scopedEntries) {
91
+ if (!scopedEntry.isDirectory()) {
92
+ continue;
93
+ }
94
+ packages.push({
95
+ name: `${entry.name}/${scopedEntry.name}`,
96
+ path: path.join(scopePath, scopedEntry.name)
97
+ });
98
+ }
99
+ } else {
100
+ packages.push({
101
+ name: entry.name,
102
+ path: path.join(nodeModulesRoot, entry.name)
103
+ });
104
+ }
105
+ }
106
+
107
+ return packages;
108
+ } catch (error) {
109
+ const err = new Error(
110
+ `Failed to read package.json for global packages: ${error.message}`
111
+ );
112
+ err.exitCode = PACKAGE_JSON_EXIT_CODE;
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ async function resolveVoltaPackage(commandName) {
118
+ try {
119
+ await which("volta");
120
+ } catch (error) {
121
+ return null;
122
+ }
123
+
124
+ const command = `volta which ${commandName}`;
125
+ const voltaPath = await new Promise((resolve, reject) => {
126
+ exec(command, { windowsHide: true }, (error, stdout) => {
127
+ if (error) {
128
+ return reject(error);
129
+ }
130
+ resolve(stdout.trim());
131
+ });
132
+ }).catch(() => null);
133
+
134
+ if (!voltaPath) {
135
+ return null;
136
+ }
137
+
138
+ const parts = path.normalize(voltaPath).split(path.sep);
139
+ const packagesIndex = parts.lastIndexOf("packages");
140
+ if (packagesIndex === -1 || !parts[packagesIndex + 1]) {
141
+ return null;
142
+ }
143
+
144
+ const packageName = parts[packagesIndex + 1];
145
+ const packagePath = path.join(
146
+ ...parts.slice(0, packagesIndex + 2),
147
+ "node_modules",
148
+ packageName
149
+ );
150
+
151
+ const packageJson = await readPackageJson(packagePath, packageName);
152
+ return { packageName, packagePath, packageJson };
153
+ }
154
+
155
+ async function findPackageForCommand(commandName) {
156
+ const [output, npmRoot] = await Promise.all([
157
+ execNpmList(),
158
+ execNpmRoot()
159
+ ]);
160
+
161
+ let parsed;
162
+ try {
163
+ parsed = JSON.parse(output);
164
+ } catch (error) {
165
+ const err = new Error(
166
+ `Failed to list global npm packages: ${error.message}`
167
+ );
168
+ err.exitCode = NPM_LIST_EXIT_CODE;
169
+ throw err;
170
+ }
171
+
172
+ const dependencies = parsed.dependencies || {};
173
+ const nodeModulesRoot = npmRoot || "";
174
+ const checked = new Set();
175
+
176
+ for (const packageName of Object.keys(dependencies)) {
177
+ const packagePath = path.join(nodeModulesRoot, packageName);
178
+ checked.add(packageName);
179
+ const packageJson = await readPackageJson(packagePath, packageName);
180
+ const bin = packageJson.bin;
181
+
182
+ if (typeof bin === "string") {
183
+ const candidateNames = getCommandNamesForStringBin(
184
+ packageJson.name || packageName
185
+ );
186
+ if (candidateNames.has(commandName)) {
187
+ return { packageName, packagePath, packageJson };
188
+ }
189
+ } else if (bin && typeof bin === "object") {
190
+ if (Object.prototype.hasOwnProperty.call(bin, commandName)) {
191
+ return { packageName, packagePath, packageJson };
192
+ }
193
+ }
194
+ }
195
+
196
+ if (nodeModulesRoot) {
197
+ const packages = await listGlobalPackages(nodeModulesRoot);
198
+ for (const pkg of packages) {
199
+ if (checked.has(pkg.name)) {
200
+ continue;
201
+ }
202
+ const packageJson = await readPackageJson(pkg.path, pkg.name);
203
+ const bin = packageJson.bin;
204
+
205
+ if (typeof bin === "string") {
206
+ const candidateNames = getCommandNamesForStringBin(
207
+ packageJson.name || pkg.name
208
+ );
209
+ if (candidateNames.has(commandName)) {
210
+ return {
211
+ packageName: pkg.name,
212
+ packagePath: pkg.path,
213
+ packageJson
214
+ };
215
+ }
216
+ } else if (bin && typeof bin === "object") {
217
+ if (Object.prototype.hasOwnProperty.call(bin, commandName)) {
218
+ return {
219
+ packageName: pkg.name,
220
+ packagePath: pkg.path,
221
+ packageJson
222
+ };
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ const voltaPackage = await resolveVoltaPackage(commandName);
229
+ if (voltaPackage) {
230
+ return voltaPackage;
231
+ }
232
+
233
+ return null;
234
+ }
235
+
236
+ module.exports = findPackageForCommand;
@@ -0,0 +1,35 @@
1
+ const parseRepository = require("./repositoryParser");
2
+
3
+ async function resolveOpen() {
4
+ const mod = await import("open");
5
+ return mod.default || mod;
6
+ }
7
+
8
+ async function displayPackageInfo(
9
+ commandName,
10
+ packageInfo,
11
+ shouldOpen = false,
12
+ openImpl
13
+ ) {
14
+ const packageName = packageInfo.packageName;
15
+ const npmUrl = `https://www.npmjs.com/package/${packageName}`;
16
+ const repoUrl = parseRepository(packageInfo.packageJson.repository);
17
+
18
+ console.log(`npm: ${packageName}`);
19
+ console.log(`npm url: ${npmUrl}`);
20
+ console.log(`github: ${repoUrl || "unavailable"}`);
21
+
22
+ if (shouldOpen) {
23
+ try {
24
+ const openFn = openImpl || (await resolveOpen());
25
+ if (typeof openFn !== "function") {
26
+ throw new Error("open is not a function");
27
+ }
28
+ await openFn(npmUrl);
29
+ } catch (error) {
30
+ console.error(`Failed to open URL in browser: ${error.message}`);
31
+ }
32
+ }
33
+ }
34
+
35
+ module.exports = displayPackageInfo;
@@ -0,0 +1,56 @@
1
+ function normalizeGitHubUrl(repo) {
2
+ if (!repo || typeof repo !== "string") {
3
+ return null;
4
+ }
5
+
6
+ const trimmed = repo.trim();
7
+ if (!trimmed) {
8
+ return null;
9
+ }
10
+
11
+ let candidate = trimmed;
12
+
13
+ if (candidate.startsWith("git+")) {
14
+ candidate = candidate.slice(4);
15
+ }
16
+
17
+ if (candidate.startsWith("github:")) {
18
+ candidate = candidate.slice("github:".length);
19
+ } else if (
20
+ !candidate.startsWith("http://") &&
21
+ !candidate.startsWith("https://") &&
22
+ /^[a-z]+:/i.test(candidate)
23
+ ) {
24
+ return null;
25
+ }
26
+
27
+ const githubMatch =
28
+ candidate.match(/github\.com[:/]+([^/]+)\/([^/]+?)(?:\.git)?$/i) ||
29
+ candidate.match(/^([^/]+)\/([^/]+)$/);
30
+
31
+ if (!githubMatch) {
32
+ return null;
33
+ }
34
+
35
+ const owner = githubMatch[1];
36
+ const repoName = githubMatch[2].replace(/\.git$/, "");
37
+ return `https://github.com/${owner}/${repoName}`;
38
+ }
39
+
40
+ function parseRepository(repository) {
41
+ if (!repository) {
42
+ return null;
43
+ }
44
+
45
+ if (typeof repository === "string") {
46
+ return normalizeGitHubUrl(repository);
47
+ }
48
+
49
+ if (typeof repository === "object" && typeof repository.url === "string") {
50
+ return normalizeGitHubUrl(repository.url);
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ module.exports = parseRepository;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "bin-home",
3
+ "version": "0.1.0",
4
+ "description": "Find which global npm package provides a CLI command",
5
+ "author": "bin-home",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "bin-home": "bin/cli.js"
9
+ },
10
+ "main": "lib/index.js",
11
+ "scripts": {
12
+ "test": "node --test"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/yourname/bin-home.git"
17
+ },
18
+ "keywords": [
19
+ "cli",
20
+ "npm",
21
+ "bin",
22
+ "package",
23
+ "finder"
24
+ ],
25
+ "engines": {
26
+ "node": ">=14.0.0"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "lib",
31
+ "README.md"
32
+ ],
33
+ "dependencies": {
34
+ "open": "^9.1.0",
35
+ "which": "^4.0.0"
36
+ }
37
+ }