@vlandoss/clibuddy 0.5.1-git-f368be9.0 → 0.6.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/dist/index.d.mts CHANGED
@@ -75,12 +75,16 @@ declare class ShellService {
75
75
  declare const cwd: string;
76
76
  declare function createShellService(options?: ShellOptions): ShellService;
77
77
  //#endregion
78
- //#region src/shell/resolve-bin.d.ts
79
- declare function resolveBinPath(pkg: string, options: {
78
+ //#region src/shell/resolve-package-bin.d.ts
79
+ type Options = {
80
80
  from: string;
81
- binPath?: string;
82
81
  binName?: string;
83
- }): string;
82
+ };
83
+ declare function _resolvePackageBin(pkg: string, {
84
+ from,
85
+ binName
86
+ }: Options): Promise<string>;
87
+ declare const resolvePackageBin: typeof _resolvePackageBin;
84
88
  //#endregion
85
89
  //#region src/shell/utils.d.ts
86
90
  declare function isNonZeroExitError(value: unknown): value is NonZeroExitError;
@@ -91,4 +95,4 @@ declare const text: {
91
95
  version: (version: string) => string;
92
96
  };
93
97
  //#endregion
94
- export { NonZeroExitError, Pkg, type Project, RunOptions, ShellOptions, ShellService, colorize, createPkg, createShellService, cwd, dirnameOf, filenameOf, hasTTY, isCI, isNonZeroExitError, palette, resolveBinPath, run, text };
98
+ export { NonZeroExitError, Pkg, type Project, RunOptions, ShellOptions, ShellService, colorize, createPkg, createShellService, cwd, dirnameOf, filenameOf, hasTTY, isCI, isNonZeroExitError, palette, resolvePackageBin, run, text };
package/dist/index.mjs CHANGED
@@ -9,6 +9,7 @@ import { findPackages } from "@pnpm/fs.find-packages";
9
9
  import { readPackageJSON, resolvePackageJSON, writePackageJSON } from "pkg-types";
10
10
  import { parse } from "yaml";
11
11
  import { NonZeroExitError, x } from "tinyexec";
12
+ import memoize from "memoize";
12
13
  //#region src/colors.ts
13
14
  const colorize = (hex) => ansis.hex(hex);
14
15
  const palette = {
@@ -203,34 +204,20 @@ function createShellService(options = {}) {
203
204
  });
204
205
  }
205
206
  //#endregion
206
- //#region src/shell/resolve-bin.ts
207
- function resolveBinPath(pkg, options) {
208
- const pkgRoot = findPackageRoot(createRequire(options.from), pkg);
209
- if (options.binPath) return path.join(pkgRoot, options.binPath);
210
- const bin = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8")).bin;
211
- if (!bin) throw new Error(`Package ${pkg} has no "bin" field`);
212
- if (typeof bin === "string") return path.join(pkgRoot, bin);
213
- const wantName = options.binName ?? pkg.replace(/^@[^/]+\//, "");
214
- const rel = bin[wantName] ?? Object.values(bin)[0];
215
- if (!rel) throw new Error(`No bin entry found for ${pkg} (asked for ${wantName})`);
216
- return path.join(pkgRoot, rel);
217
- }
218
- function findPackageRoot(require, pkg) {
207
+ //#region src/shell/resolve-package-bin.ts
208
+ async function _resolvePackageBin(pkg, { from, binName }) {
209
+ let pkgJsonPath;
219
210
  try {
220
- return path.dirname(require.resolve(`${pkg}/package.json`));
221
- } catch {}
222
- const mainPath = require.resolve(pkg);
223
- let dir = path.dirname(mainPath);
224
- const fsRoot = path.parse(dir).root;
225
- while (dir !== fsRoot) {
226
- const pkgJsonPath = path.join(dir, "package.json");
227
- if (fs.existsSync(pkgJsonPath)) try {
228
- if (JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).name === pkg) return dir;
229
- } catch {}
230
- dir = path.dirname(dir);
231
- }
232
- throw new Error(`Could not find package root for ${pkg} from ${mainPath}`);
211
+ pkgJsonPath = await resolvePackageJSON(pkg, { from });
212
+ } catch {
213
+ pkgJsonPath = createRequire(from).resolve(`${pkg}/package.json`);
214
+ }
215
+ const { bin } = await readPackageJSON(pkgJsonPath);
216
+ const rel = typeof bin === "string" ? bin : bin?.[binName ?? pkg.replace(/^@[^/]+\//, "")];
217
+ if (!rel) throw new Error(`No bin "${binName ?? pkg}" in ${pkg}`);
218
+ return path.join(path.dirname(pkgJsonPath), rel);
233
219
  }
220
+ const resolvePackageBin = memoize(_resolvePackageBin, { cacheKey: ([pkg, opts]) => `${pkg}|${opts.from}|${opts.binName ?? ""}` });
234
221
  //#endregion
235
222
  //#region src/text.ts
236
223
  const text = {
@@ -238,4 +225,4 @@ const text = {
238
225
  version: (version) => palette.muted(`v${version}`)
239
226
  };
240
227
  //#endregion
241
- export { NonZeroExitError, Pkg, ShellService, colorize, createPkg, createShellService, cwd, dirnameOf, filenameOf, hasTTY, isCI, isNonZeroExitError, palette, resolveBinPath, run, text };
228
+ export { NonZeroExitError, Pkg, ShellService, colorize, createPkg, createShellService, cwd, dirnameOf, filenameOf, hasTTY, isCI, isNonZeroExitError, palette, resolvePackageBin, run, text };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vlandoss/clibuddy",
3
- "version": "0.5.1-git-f368be9.0",
3
+ "version": "0.6.0",
4
4
  "description": "A helper library to create CLIs in Variable Land",
5
5
  "homepage": "https://github.com/variableland/dx/tree/main/packages/clibuddy#readme",
6
6
  "bugs": {
@@ -30,6 +30,7 @@
30
30
  "@pnpm/fs.find-packages": "1000.0.24",
31
31
  "@pnpm/types": "1001.3.0",
32
32
  "ansis": "4.2.0",
33
+ "memoize": "10.2.0",
33
34
  "pkg-types": "2.3.0",
34
35
  "std-env": "3.9.0",
35
36
  "tinyexec": "1.1.2",
@@ -1,5 +1,5 @@
1
1
  export * from "./create.ts";
2
- export * from "./resolve-bin.ts";
2
+ export * from "./resolve-package-bin.ts";
3
3
  export * from "./shell.ts";
4
4
  export * from "./types.ts";
5
5
  export * from "./utils.ts";
@@ -0,0 +1,26 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import memoize from "memoize";
4
+ import { readPackageJSON, resolvePackageJSON } from "pkg-types";
5
+
6
+ type Options = { from: string; binName?: string };
7
+
8
+ // pkg-types covers any package whose `.` entry is in `exports` (including
9
+ // restrictive ones like oxlint). The fallback handles packages without
10
+ // `main`/`exports` at all (e.g. @biomejs/biome).
11
+ async function _resolvePackageBin(pkg: string, { from, binName }: Options): Promise<string> {
12
+ let pkgJsonPath: string;
13
+ try {
14
+ pkgJsonPath = await resolvePackageJSON(pkg, { from });
15
+ } catch {
16
+ pkgJsonPath = createRequire(from).resolve(`${pkg}/package.json`);
17
+ }
18
+ const { bin } = await readPackageJSON(pkgJsonPath);
19
+ const rel = typeof bin === "string" ? bin : bin?.[binName ?? pkg.replace(/^@[^/]+\//, "")];
20
+ if (!rel) throw new Error(`No bin "${binName ?? pkg}" in ${pkg}`);
21
+ return path.join(path.dirname(pkgJsonPath), rel);
22
+ }
23
+
24
+ export const resolvePackageBin = memoize(_resolvePackageBin, {
25
+ cacheKey: ([pkg, opts]) => `${pkg}|${opts.from}|${opts.binName ?? ""}`,
26
+ });
@@ -1,53 +0,0 @@
1
- import fs from "node:fs";
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
-
5
- export function resolveBinPath(pkg: string, options: { from: string; binPath?: string; binName?: string }): string {
6
- const require = createRequire(options.from);
7
- const pkgRoot = findPackageRoot(require, pkg);
8
-
9
- if (options.binPath) {
10
- return path.join(pkgRoot, options.binPath);
11
- }
12
-
13
- const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8")) as {
14
- bin?: string | Record<string, string>;
15
- };
16
- const bin = pkgJson.bin;
17
- if (!bin) throw new Error(`Package ${pkg} has no "bin" field`);
18
-
19
- if (typeof bin === "string") return path.join(pkgRoot, bin);
20
-
21
- const wantName = options.binName ?? pkg.replace(/^@[^/]+\//, "");
22
- const rel = bin[wantName] ?? Object.values(bin)[0];
23
- if (!rel) throw new Error(`No bin entry found for ${pkg} (asked for ${wantName})`);
24
- return path.join(pkgRoot, rel);
25
- }
26
-
27
- // Two-step lookup tolerates packages that don't expose `./package.json` in
28
- // their `exports` map (e.g. oxlint) and packages with no `main`/`exports` at
29
- // all (e.g. @biomejs/biome) — `require.resolve(pkg)` fails for the latter.
30
- function findPackageRoot(require: NodeJS.Require, pkg: string): string {
31
- try {
32
- return path.dirname(require.resolve(`${pkg}/package.json`));
33
- } catch {
34
- // fall through to manual walk
35
- }
36
-
37
- const mainPath = require.resolve(pkg);
38
- let dir = path.dirname(mainPath);
39
- const fsRoot = path.parse(dir).root;
40
- while (dir !== fsRoot) {
41
- const pkgJsonPath = path.join(dir, "package.json");
42
- if (fs.existsSync(pkgJsonPath)) {
43
- try {
44
- const data = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")) as { name?: string };
45
- if (data.name === pkg) return dir;
46
- } catch {
47
- // not a valid package.json — keep walking
48
- }
49
- }
50
- dir = path.dirname(dir);
51
- }
52
- throw new Error(`Could not find package root for ${pkg} from ${mainPath}`);
53
- }