Skip to content

feat(auth): implement pnpm login --scope#11727

Merged
zkochan merged 1 commit into
pnpm:mainfrom
shiminshen:fix/login-scope
May 19, 2026
Merged

feat(auth): implement pnpm login --scope#11727
zkochan merged 1 commit into
pnpm:mainfrom
shiminshen:fix/login-scope

Conversation

@shiminshen

@shiminshen shiminshen commented May 18, 2026

Copy link
Copy Markdown
Contributor

What

pnpm.io's cli/login page has long shown the usage as

pnpm login [--registry <url>] [--scope <scope>]

but in the CLI only --registry was actually wired up — pnpm login --scope foo errored with Unknown option: 'scope'. This PR implements the documented flag.

When --scope <scope> is passed, after the auth token is written, an @<scope>:registry=<registry> mapping is appended to the same pnpm auth file (~/.config/pnpm/auth.ini). config.reader already accepts @scope:registry= from that file (see Auth file read order), so subsequent installs of @<scope>/* packages route to the chosen registry without any further configuration.

Closes #11716.

Behavior summary

Invocation Result written to auth.ini
pnpm login --registry https://example.com/ //example.com/:_authToken=<token>
pnpm login --scope my-org --registry https://example.com/ //example.com/:_authToken=<token> + @my-org:registry=https://example.com/
pnpm login --scope @my-org --registry https://example.com/ same as above (no double @@)
pnpm login --scope " " --registry … / --scope @ blank scope is dropped — no broken @:registry= line

Files touched

  • auth/commands/src/login.ts — declare scope: allTypes.scope in rcOptionsTypes, document it in help(), normalize the value, write @<scope>:registry=<registry> alongside the token in login().
  • auth/commands/test/login.test.ts — three new tests:
    1. --scope my-org normalizes to @my-org and writes the mapping.
    2. --scope @my-org does not double-prefix.
    3. Omitting --scope writes no @…:registry key.

Notes

  • I went with auth.ini rather than ~/.npmrc because that's where pnpm 11 already writes auth-adjacent config (config.reader reads @scope:registry= from it) and it keeps everything pnpm login writes in one file. Happy to switch to ~/.npmrc if maintainers prefer the npm-compatible target — let me know and I'll push a follow-up commit.
  • A cafile repro and fix in config.reader is up as fix(config/reader): resolve relative cafile path against the .npmrc directory #11726 separately; the two PRs are independent.

Test plan

  • Three new login unit tests cover scope normalization and the no-scope case.
  • Standalone repro (script in PR thread for pnpm login --scope is documented but not implemented #11716 if helpful) confirms each edge case writes the expected auth.ini entries.
  • Existing pnpm login tests (web auth, classic fallback, OTP, etc.) are unchanged and continue to pass through the same code path.
  • Changeset added (minor bump for @pnpm/auth.commands and pnpm).

Notes

This contribution was prepared with the assistance of Claude Code.

Summary by CodeRabbit

  • New Features

    • Added --scope flag to pnpm login command, allowing users to authenticate with specific npm scopes and their dedicated registries. Scope values are automatically normalized with an @ prefix if needed.
  • Bug Fixes

    • Fixed pnpm login --scope command that previously resulted in an "Unknown option" error.

Review Change Stack

The online documentation has long listed `pnpm login [--registry <url>] [--scope <scope>]`,
but the CLI only ever accepted `--registry`. `pnpm login --scope foo` errored with
`Unknown option: 'scope'` and there was no way to associate a scope with a registry
through the login command — users had to edit `.npmrc` by hand.

This change wires `--scope` through:
  - `cliOptionsTypes` / `rcOptionsTypes` declare the option.
  - `help()` documents it.
  - After the token is written, `@<scope>:registry=<registry>` is written into the same
    auth file (`~/.config/pnpm/auth.ini`), which `config.reader` already accepts as a
    source of `@scope:registry=` mappings.
  - Scope normalization: `--scope foo` and `--scope @foo` both produce `@foo`. Blank
    values (`""`, `" "`, `"@"`) are treated as unset rather than writing a broken
    `@:registry=` entry.

Ref: pnpm#11716
@shiminshen shiminshen requested a review from zkochan as a code owner May 18, 2026 17:15
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR implements the --scope flag for pnpm login, which was documented but not implemented. The command now accepts --scope <scope>, normalizes it to a scoped package identifier, and persists a scope-to-registry mapping in the auth configuration file alongside the authentication token.

Changes

Login --scope Flag Implementation

Layer / File(s) Summary
Option type and CLI interface
auth/commands/src/login.ts
LoginCommandOptions gains optional scope?: string; rcOptionsTypes() adds scope definition; help text and usage strings document the --scope <scope> parameter.
Scope-to-registry mapping persistence
auth/commands/src/login.ts
After successful authentication, the code normalizes opts.scope (trimming whitespace, adding leading @ if missing, treating empty or bare @ as unset) and writes a ${scopeKey}:registry key-value pair to auth.ini alongside the token.
Test coverage for scope behavior
auth/commands/test/login.test.ts
Three test cases verify scope normalization (unprefixed my-org@my-org), no double-prefixing (@my-org@my-org:registry, not @@my-org:registry), and omission handling (no scope key written when --scope is absent).
Changeset documentation
.changeset/login-scope-flag.md
Minor version bump for @pnpm/auth.commands and pnpm; documents scope normalization, registry mapping, and the fix for the previously erroring Unknown option: 'scope' issue.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A scope now tags each registry call,
No more "Unknown option"—we fix it all!
Normalize and map with a gentle touch,
Tests confirm the behavior matches so much. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(auth): implement pnpm login --scope' directly and clearly summarizes the main change—adding scope support to the pnpm login command.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #11716: adds --scope flag to pnpm login with proper normalization, registers it in CLI/rc option types, persists scope→registry mappings to auth.ini, and includes comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the --scope flag for pnpm login as specified in issue #11716; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
auth/commands/test/login.test.ts (1)

211-239: ⚡ Quick win

Add explicit tests for invalid scope values handled by normalization.

Please add cases for scope: ' ' and scope: '@' to assert no @...:registry entry is written, matching the new guard paths in normalizeScope.

Proposed test addition
+  it('should not write a scope mapping for blank or invalid scope values', async () => {
+    for (const scope of ['   ', '@']) {
+      let savedSettings: Record<string, unknown> = {}
+      const context = createMockContext({
+        globalInfo: jest.fn(),
+        readIniFile: async () => ({}),
+        writeIniFile: async (_configPath, settings) => {
+          savedSettings = settings
+        },
+        fetch: async url => {
+          if (url === 'https://example.com/-/v1/login') {
+            return createMockResponse({
+              ok: true,
+              status: 200,
+              json: {
+                loginUrl: 'https://example.com/auth/login',
+                doneUrl: 'https://example.com/auth/done',
+              },
+            })
+          }
+          return createMockResponse({ ok: true, status: 200, json: { token: 'tok' } })
+        },
+      })
+      await login({ context, opts: { configDir: '/mock/config', dir: '/mock', authConfig: {}, registry: 'https://example.com', scope } })
+      for (const key of Object.keys(savedSettings)) {
+        expect(key.startsWith('@')).toBe(false)
+      }
+    }
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth/commands/test/login.test.ts` around lines 211 - 239, Add two additional
test cases in auth/commands/test/login.test.ts mirroring the existing "should
not write a scope mapping when --scope is omitted" test but passing opts.scope =
'   ' and opts.scope = '@' respectively; use the same createMockContext stub and
savedSettings capture, call login({ context, opts }), and assert that no
savedSettings key starts with '@' (i.e., for each Object.keys(savedSettings)
expect(key.startsWith('@')).toBe(false)). Ensure the new tests reference the
same login invocation and savedSettings assertion so they exercise the
normalizeScope guard paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@auth/commands/test/login.test.ts`:
- Around line 211-239: Add two additional test cases in
auth/commands/test/login.test.ts mirroring the existing "should not write a
scope mapping when --scope is omitted" test but passing opts.scope = '   ' and
opts.scope = '@' respectively; use the same createMockContext stub and
savedSettings capture, call login({ context, opts }), and assert that no
savedSettings key starts with '@' (i.e., for each Object.keys(savedSettings)
expect(key.startsWith('@')).toBe(false)). Ensure the new tests reference the
same login invocation and savedSettings assertion so they exercise the
normalizeScope guard paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c39ac002-1952-4cf0-b518-488b68a8a8d3

📥 Commits

Reviewing files that changed from the base of the PR and between e01d2bc and 73f9ccd.

📒 Files selected for processing (3)
  • .changeset/login-scope-flag.md
  • auth/commands/src/login.ts
  • auth/commands/test/login.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (rely on hoisting), and use a single options object for functions with more than two or three arguments
Sort imports in three groups: standard libraries, external dependencies (alphabetically), then relative imports
Write code that explains itself through clear naming and types — do not write comments that merely restate what the code already says; use comments only for non-obvious reasons, hidden invariants, or workarounds

Files:

  • auth/commands/src/login.ts
  • auth/commands/test/login.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use util.types.isNativeError() instead of instanceof Error when type-checking errors in Jest tests, as instanceof checks can fail across VM realms

Files:

  • auth/commands/test/login.test.ts
🧠 Learnings (1)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • auth/commands/src/login.ts
  • auth/commands/test/login.test.ts
🔇 Additional comments (2)
auth/commands/src/login.ts (1)

27-30: LGTM!

Also applies to: 52-55, 60-60, 75-75, 213-219, 225-233

.changeset/login-scope-flag.md (1)

1-7: LGTM!

@zkochan zkochan merged commit 56f3851 into pnpm:main May 19, 2026
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpm login --scope is documented but not implemented

2 participants