@tuanhv/op-cli 0.2.0 → 0.2.2

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.
@@ -6,6 +6,9 @@ import { cliVersion } from "./version.js";
6
6
  const PACKAGE_SPEC = "@tuanhv/op-cli@latest";
7
7
  // Scoped package names must escape the slash for a registry path segment.
8
8
  const REGISTRY_URL = "https://registry.npmjs.org/@tuanhv%2Fop-cli/latest";
9
+ // The bare name `npm ls -g` answers ownership with: exit 0 exactly when
10
+ // npm's global tree holds the package.
11
+ const PACKAGE_NAME = "@tuanhv/op-cli";
9
12
  // The notice is a convenience, never a delay: its lookup gets a budget far
10
13
  // below the API's own, and every failure path stays silent.
11
14
  const NOTICE_TIMEOUT_MS = 1_500;
@@ -20,9 +23,14 @@ function isHomebrewPath(path) {
20
23
  || path.includes("/usr/local/Homebrew");
21
24
  }
22
25
  // An install another manager owns would be overwritten by npm today and
23
- // reverted by that manager's next upgrade, so the exact command is printed
24
- // for the human to run instead. Detection is heuristic on purpose: npm's
25
- // global prefix and the resolved binary path name their manager plainly.
26
+ // reverted by that manager's next upgrade, so the exact command is
27
+ // printed for the human to run instead. Ownership is asked of npm
28
+ // itself: `npm ls -g` exits 0 exactly when npm's global tree holds the
29
+ // package, so a Homebrew-built Node with an npm -g install stays npm's.
30
+ // A Homebrew *prefix* only names Node's installer; treating it as the
31
+ // package's manager misroutes every npm -g install on such a Node.
32
+ // Homebrew is claimed only when npm does not own the package and the
33
+ // binary or prefix lands inside a Homebrew tree.
26
34
  async function detectManagingInstall(runtime) {
27
35
  const capture = runtime.captureExternal;
28
36
  if (capture === undefined) {
@@ -32,14 +40,16 @@ async function detectManagingInstall(runtime) {
32
40
  if (prefix.includes("volta")) {
33
41
  return { label: "Volta", command: `volta install ${PACKAGE_SPEC}` };
34
42
  }
35
- if (isHomebrewPath(prefix)) {
36
- return { label: "Homebrew", command: "brew upgrade op-cli" };
37
- }
38
43
  const bin = (await capture("which", ["op-cli"]))?.trim() ?? "";
39
44
  if (bin.includes("volta")) {
40
45
  return { label: "Volta", command: `volta install ${PACKAGE_SPEC}` };
41
46
  }
42
- if (isHomebrewPath(bin)) {
47
+ // The exit code is the answer; the listing text itself is not read.
48
+ const ownedByNpm = await capture("npm", ["ls", "-g", PACKAGE_NAME]);
49
+ if (ownedByNpm !== undefined) {
50
+ return undefined;
51
+ }
52
+ if (isHomebrewPath(bin) || isHomebrewPath(prefix)) {
43
53
  return { label: "Homebrew", command: "brew upgrade op-cli" };
44
54
  }
45
55
  return undefined;
@@ -44,6 +44,12 @@ export function createNodeIo(input = process.stdin, output = process.stdout, err
44
44
  const decoder = new StringDecoder("utf8");
45
45
  let answer = "";
46
46
  let terminated = false;
47
+ // A preceding askLine closed its readline interface, which pauses the
48
+ // input; on a socket-backed stdin that stops the read handle, and the
49
+ // async iterator below never restarts it, so the event loop would drain
50
+ // and abort the awaited run(). resume() restarts the handle; the
51
+ // iterator's readable listener takes over before any chunk can flow.
52
+ input.resume();
47
53
  try {
48
54
  for await (const chunk of input) {
49
55
  const text = typeof chunk === "string" ? chunk : decoder.write(chunk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuanhv/op-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A resolving command-line client for OpenProject",
5
5
  "license": "MIT",
6
6
  "author": "hoangvantuan",