Skip to content

[Detail Bug] CLI: javachat update fails in npm workspaces when the CLI is a dependency of a workspace member #247

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_d126f742-3d90-4d2d-8c7a-459f1fb4d15e

Introduced in e6844cf by @WilliamAGH on Sep 1, 2026

Summary

  • Context: commandUpdate (cli/bin/javachat.js:338-352) calls resolveNpmInstallTarget (cli/bin/javachat.js:355-416); the latter is introduced, and is the only code path that reaches the guard, by commit e6844cfb4 ("feat(cli): add self-update and clearer source gaps", 2026-09-01). git blame -L 355,416 cli/bin/javachat.js attributes every line of resolveNpmInstallTarget to e6844cfb4 (sole attribution); it is the last commit touching cli/bin/javachat.js. resolveNpmInstallTarget is called from exactly one site (commandUpdate, line 339) — no other subcommand reaches the guard.
  • Bug: javachat update exits 1 in an npm workspaces monorepo when @wcallahan/javachat-cli is declared in a workspace member package. npm hoists node_modules/.bin/javachat to the workspace root, but resolveNpmInstallTarget treats the .bin owner (root) as projectRoot, reads only the root package.json, and throws because the root does not declare the dependency.
  • Actual vs. expected: Actual: javachat update prints The project at <root> does not declare @wcallahan/javachat-cli; npm will not be allowed to modify it. and never invokes npm. Expected (per README wording “runs … npm install @wcallahan/javachat-cli@latest in that project”): when a workspace member declares the dependency, the update should run the equivalent npm install @wcallahan/javachat-cli@latest with cwd set to that member (or otherwise update the declaring project without polluting the root manifest).
  • Impact: The update convenience command cannot be used in the default npm-workspaces hoisted layout unless the user manually runs the install in the member directory (or uses npm install -w <member> ... / install-strategy=nested). Other CLI commands are unaffected.

Code with Bug

async function resolveNpmInstallTarget() {
  const invokedEntrypoint = argv[1] ? resolve(argv[1]) : "";
  if (basename(invokedEntrypoint) !== "javachat") {
    throw new Error(/* ... */);
  }

  const invokedDirectory = dirname(invokedEntrypoint);          // .../node_modules/.bin
  if (basename(invokedDirectory) === ".bin" && basename(dirname(invokedDirectory)) === "node_modules") {
    const projectRoot = dirname(dirname(invokedDirectory));      // <-- BUG 🔴 treats workspace root (hoisted .bin owner) as the project to update
    if (projectRoot.split(sep).includes("_npx")) { throw new Error(/* ... */); }
    const projectManifest = await readProjectManifest(projectRoot); // <-- BUG 🔴 only reads root package.json, ignores workspace members
    const declaresJavaChat = [
      projectManifest.dependencies,
      projectManifest.devDependencies,
      projectManifest.optionalDependencies,
    ].some((g) => Object.hasOwn(g ?? {}, CLI_PACKAGE));
    if (!declaresJavaChat) {
      throw new Error(
        `The project at ${projectRoot} does not declare ${CLI_PACKAGE}; npm will not be allowed to modify it.`,
      );
    }
    return {
      npmArguments: ["install", `${CLI_PACKAGE}@latest`],
      workingDirectory: projectRoot,
    };
  }
}

Explanation

In npm workspaces with default hoisting, node_modules/.bin/javachat is created at the workspace root even when the dependency is declared only in a member. The current logic infers projectRoot from the .bin directory and then validates the dependency declaration only in <root>/package.json. Because the root typically does not declare @wcallahan/javachat-cli in this setup, the guard fails and exits before running npm.

This is reproducible on supported Node/npm (Node v24.18.0, npm 11.16.0): after installing the CLI as a member dependency and ensuring .bin/javachat is hoisted to the root, javachat update refuses with the “does not declare” error and exit code 1.

Recommended Fix

When projectRoot/package.json does not declare @wcallahan/javachat-cli, do not immediately refuse if the root is a workspaces root; instead, consult workspace members and, if a member declares the dependency, run the existing command (npm install @wcallahan/javachat-cli@latest) with workingDirectory set to that member directory. Preserve the existing refusal as the fallback when enumeration fails or no member declares the package.

History

This bug was introduced in commit e6844cf. The commit "feat(cli): add self-update and clearer source gaps" added the entire self-update feature — commandUpdate and resolveNpmInstallTarget — in a single pass; neither function existed in the prior commit (655f3d09), and git log -S "resolveNpmInstallTarget" returns this commit alone.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions