Skip to content

feat(anchor-pnpm): new CLI for managing pnpm-workspace.yaml anchors#293

Open
stephansama wants to merge 3 commits into
mainfrom
stephansama/ste-187-create-anchor-pnpm-cli
Open

feat(anchor-pnpm): new CLI for managing pnpm-workspace.yaml anchors#293
stephansama wants to merge 3 commits into
mainfrom
stephansama/ste-187-create-anchor-pnpm-cli

Conversation

@stephansama

Copy link
Copy Markdown
Owner

Closes STE-187

New @stephansama/anchor-pnpm package at core/anchor-pnpm/. CLI for managing YAML anchor declarations in pnpm-workspace.yaml while preserving anchors / aliases / comments via the yaml package's Document API.

Commands shipped

  • anchor-pnpm list — print every &<name> entry with its resolved version
  • anchor-pnpm add <name> <version> — append to __versions
  • anchor-pnpm update <name> <version> — change a scalar value; aliases follow automatically
  • anchor-pnpm remove <name> — delete, with --force to inline any *<name> aliases first
  • anchor-pnpm sync — scan catalogs for repeated versions and print proposed anchor+alias pairs (--apply to commit them)

Global flags: --workspace/-w (override path), --dry-run/-d (print rewritten YAML, don't write), --verbose/-v.

Tests: test/anchors.test.ts — 9 cases covering list / add / update / remove (with dangling-alias detection) / sync / applySyncProposal. All passing.

Acceptance criteria

  • list prints all anchor name → version pairs
  • add inserts a new entry into __versions
  • update changes the scalar value of an existing anchor
  • remove errors on dangling aliases unless --force is passed
  • sync detects catalog groups with shared versions and writes anchors + aliases
  • All commands support --dry-run
  • Round-trip: existing anchors/aliases preserved after any write

@vercel

vercel Bot commented May 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
packages Ready Ready Preview, Comment May 18, 2026 3:32am

@changeset-bot

changeset-bot Bot commented May 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6484ff6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • New @stephansama/anchor-pnpm package added with CLI for managing YAML anchors in pnpm workspace files
    • Supported commands: list, add, update, remove, and sync
    • Global options: --workspace, --dry-run, --verbose
  • Documentation

    • Complete package documentation with CLI command reference
  • Tests

    • Comprehensive test suite included

Walkthrough

This PR introduces @stephansama/anchor-pnpm, a new npm package that provides a CLI and TypeScript API for managing YAML anchors in pnpm workspace catalogs. It includes workspace file discovery, anchor add/update/remove/list operations, sync proposal generation, and five CLI subcommands with comprehensive tests.

Changes

anchor-pnpm package

Layer / File(s) Summary
Package setup and build configuration
core/anchor-pnpm/package.json, core/anchor-pnpm/tsconfig.json, core/anchor-pnpm/tsdown.config.ts, core/anchor-pnpm/vitest.config.ts, core/anchor-pnpm/README.md, README.md
Package metadata, exports map (main module, CLI, types), build configuration (ESM, DTS, API snapshots), and documentation for the anchor-pnpm CLI workflow.
Workspace file I/O utilities
core/anchor-pnpm/src/workspace.ts
Async functions to discover pnpm workspace file path via monorepo root, load YAML documents as yaml.Document instances, and save modified documents back to disk.
Core anchor and alias operations
core/anchor-pnpm/src/anchors.ts
Add/update/remove anchored scalars in __versions sequence, list existing anchors, remove anchors with dangling-alias detection and optional force inlining, detect repeated catalog versions and emit sync proposals, and apply proposals by writing anchors and replacing scalar literals with aliases.
Public API re-exports
core/anchor-pnpm/src/index.ts
Top-level module exports forwarding anchor operations (add, list, remove, sync, update) and workspace utilities (loadWorkspace, resolveWorkspacePath, saveWorkspace).
CLI implementation and commands
core/anchor-pnpm/src/cli.ts
Five subcommands (list, add, update, remove, sync) using cleye CLI framework; shared global flags (--dry-run, --verbose, --workspace); commit helper that writes document to disk unless --dry-run is set.
Tests and API snapshots
core/anchor-pnpm/__snapshots__/tsnapi/*, core/anchor-pnpm/test/anchors.test.ts
Generated tsnapi snapshots documenting the public API surface; Vitest suite validating anchor listing, add/update/remove semantics, removal blockers, forced removal, sync proposal detection, and alias-preserving round-trips.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops with anchors tight,
Through YAML storms and alias plight,
With sync proposals dancing free,
The workspace anchors all agree! 🐰⚓

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is comprehensive and well-structured, covering objectives, commands, flags, tests, and acceptance criteria. However, it does not follow the repository's required checklist template. Add the repository's required checklist items (merge status, conflicts, branch, linting, tests) to the description for consistency with repository standards.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing a new CLI tool for managing YAML anchors in pnpm-workspace.yaml files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stephansama/ste-187-create-anchor-pnpm-cli

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

core/anchor-pnpm/__snapshots__/tsnapi/cli.snapshot.d.ts

Oops! Something went wrong! :(

ESLint: 10.2.1

Error: Cannot find module '/node_modules/@stephansama/eslint-config/dist/index.mjs'
at createEsmNotFoundErr (node:internal/modules/cjs/loader:1513:15)
at finalizeEsmResolution (node:internal/modules/cjs/loader:1502:9)
at resolveExports (node:internal/modules/cjs/loader:685:14)
at Module._findPath (node:internal/modules/cjs/loader:752:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1461:27)
at wrapResolveFilename (node:internal/modules/cjs/loader:1049:27)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1094:12)
at require.resolve (node:internal/modules/helpers:171:31)
at jitiResolve (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:148703)
at jitiRequire (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:150290)
at import (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:158307)
at /eslint.config.ts:1:223
at eval

... [truncated 540 characters] ...

265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:637:25)
at async ESLint.lintFiles (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint.js:1004:21)
at async Object.execute (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/bin/eslint.js:175:19)

core/anchor-pnpm/src/anchors.ts

Oops! Something went wrong! :(

ESLint: 10.2.1

Error: Cannot find module '/node_modules/@stephansama/eslint-config/dist/index.mjs'
at createEsmNotFoundErr (node:internal/modules/cjs/loader:1513:15)
at finalizeEsmResolution (node:internal/modules/cjs/loader:1502:9)
at resolveExports (node:internal/modules/cjs/loader:685:14)
at Module._findPath (node:internal/modules/cjs/loader:752:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1461:27)
at wrapResolveFilename (node:internal/modules/cjs/loader:1049:27)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1094:12)
at require.resolve (node:internal/modules/helpers:171:31)
at jitiResolve (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:148703)
at jitiRequire (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:150290)
at import (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:158307)
at /eslint.config.ts:1:223
at eval

... [truncated 540 characters] ...

265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:637:25)
at async ESLint.lintFiles (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint.js:1004:21)
at async Object.execute (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/bin/eslint.js:175:19)

core/anchor-pnpm/src/cli.ts

Oops! Something went wrong! :(

ESLint: 10.2.1

Error: Cannot find module '/node_modules/@stephansama/eslint-config/dist/index.mjs'
at createEsmNotFoundErr (node:internal/modules/cjs/loader:1513:15)
at finalizeEsmResolution (node:internal/modules/cjs/loader:1502:9)
at resolveExports (node:internal/modules/cjs/loader:685:14)
at Module._findPath (node:internal/modules/cjs/loader:752:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1461:27)
at wrapResolveFilename (node:internal/modules/cjs/loader:1049:27)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1094:12)
at require.resolve (node:internal/modules/helpers:171:31)
at jitiResolve (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:148703)
at jitiRequire (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:150290)
at import (/node_modules/.pnpm/jiti@2.6.1/node_modules/jiti/dist/jiti.cjs:1:158307)
at /eslint.config.ts:1:223
at eval

... [truncated 540 characters] ...

265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:637:25)
at async ESLint.lintFiles (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint.js:1004:21)
at async Object.execute (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/.pnpm/eslint@10.2.1_jiti@2.6.1/node_modules/eslint/bin/eslint.js:175:19)

  • 8 others

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.

@codecov

codecov Bot commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.85417% with 79 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
core/anchor-pnpm/src/cli.ts 0.00% 64 Missing ⚠️
core/anchor-pnpm/src/workspace.ts 0.00% 10 Missing ⚠️
core/anchor-pnpm/src/anchors.ts 95.76% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@pkg-pr-new

pkg-pr-new Bot commented May 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@stephansama/ai-commit-msg

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/ai-commit-msg@293

@stephansama/alfred-kaomoji

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/alfred-kaomoji@293

@stephansama/anchor-pnpm

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/anchor-pnpm@293

@stephansama/astro-iconify-svgmap

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/astro-iconify-svgmap@293

@stephansama/auto-readme

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/auto-readme@293

@stephansama/catppuccin-jsonresume-theme

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-jsonresume-theme@293

@stephansama/catppuccin-opml

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-opml@293

@stephansama/catppuccin-rss

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-rss@293

@stephansama/catppuccin-typedoc

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-typedoc@293

@stephansama/catppuccin-xsl

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-xsl@293

@stephansama/eslint-config

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/eslint-config@293

create-stephansama-example

pnpm add https://pkg.pr.new/stephansama/packages/create-stephansama-example@293

@stephansama/find-makefile-targets

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/find-makefile-targets@293

@stephansama/github-env

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/github-env@293

@stephansama/multipublish

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/multipublish@293

@stephansama/pnpm-hooks

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/pnpm-hooks@293

@stephansama/prettier-plugin-handlebars

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/prettier-plugin-handlebars@293

@stephansama/remark-asciinema

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/remark-asciinema@293

@stephansama/single-file

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/single-file@293

@stephansama/svelte-social-share-links

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/svelte-social-share-links@293

@stephansama/typed-env

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-env@293

@stephansama/typed-events

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-events@293

@stephansama/typed-nocodb-api

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-nocodb-api@293

@stephansama/typed-templates

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-templates@293

@stephansama/types-github-action-env

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/types-github-action-env@293

@stephansama/types-lhci

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/types-lhci@293

commit: 6484ff6

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces @stephansama/anchor-pnpm, a CLI tool for managing YAML anchor declarations in pnpm-workspace.yaml to streamline version management across catalogs. The implementation includes core logic for adding, listing, updating, removing, and syncing anchors, along with a command-line interface and unit tests. Feedback highlights several critical logic issues: applySyncProposal needs to verify anchor versions to prevent data corruption, the sync function requires logic to handle duplicate or existing anchor names, and the remove function should check for an anchor's existence before applying side effects. Additionally, the README should be updated to reflect that the sync command is already implemented.

Comment on lines +38 to +53
export function applySyncProposal(
document: Document,
proposal: SyncProposal,
): void {
add(document, proposal.anchorName, proposal.version);
const catalogs = document.get(CATALOGS_KEY);
if (!isMap(catalogs)) return;
const group = catalogs.get(proposal.catalog);
if (!isMap(group)) return;

for (const entry of group.items) {
if (!isPair(entry) || !isScalar(entry.key)) continue;
if (!proposal.keys.includes(String(entry.key.value))) continue;
entry.value = new Alias(proposal.anchorName);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In applySyncProposal, the result of add is ignored. If add fails because an anchor with the same name already exists but has a different version, the function will proceed to alias catalog entries to that incorrect version. This can cause silent data corruption where packages are pinned to the wrong version.

It's safer to verify that the anchor exists and has the expected version before applying aliases.

export function applySyncProposal(
	document: Document,
	proposal: SyncProposal,
): void {
	const { added } = add(document, proposal.anchorName, proposal.version);
	if (!added) {
		const existing = list(document).find((e) => e.name === proposal.anchorName);
		if (existing?.value !== proposal.version) return;
	}

	const catalogs = document.get(CATALOGS_KEY);
	if (!isMap(catalogs)) return;
	const group = catalogs.get(proposal.catalog);
	if (!isMap(group)) return;

	for (const entry of group.items) {
		if (!isPair(entry) || !isScalar(entry.key)) continue;
		if (!proposal.keys.includes(String(entry.key.value))) continue;
		entry.value = new Alias(proposal.anchorName);
	}
}

Comment on lines +92 to +128
export function sync(document: Document): SyncProposal[] {
const catalogs = document.get(CATALOGS_KEY);
if (!isMap(catalogs)) return [];

const proposals: SyncProposal[] = [];

for (const pair of catalogs.items) {
if (!isPair(pair) || !isScalar(pair.key) || !isMap(pair.value))
continue;
const catalogName = String(pair.key.value);
const versionToKeys = new Map<string, string[]>();

for (const entry of pair.value.items) {
if (!isPair(entry) || !isScalar(entry.key)) continue;
const value = entry.value;
if (!isScalar(value) || value.anchor) continue;
const versionString = String(value.value);
const keyString = String(entry.key.value);
versionToKeys.set(versionString, [
...(versionToKeys.get(versionString) ?? []),
keyString,
]);
}

for (const [version, keys] of versionToKeys) {
if (keys.length < 2) continue;
proposals.push({
anchorName: catalogName,
catalog: catalogName,
keys,
version,
});
}
}

return proposals;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The sync function has a bug where it generates duplicate anchorName values if a single catalog contains multiple distinct versions that are each repeated. It also fails to account for existing anchor names in the __versions section. This can lead to data corruption when applying proposals, as multiple versions might end up aliased to the same (first) anchor.

I suggest tracking existing anchor names and ensuring each proposal gets a unique name.

export function sync(document: Document): SyncProposal[] {
	const catalogs = document.get(CATALOGS_KEY);
	if (!isMap(catalogs)) return [];

	const proposals: SyncProposal[] = [];
	const existingAnchors = new Set(list(document).map((e) => e.name));

	for (const pair of catalogs.items) {
		if (!isPair(pair) || !isScalar(pair.key) || !isMap(pair.value))
			continue;
		const catalogName = String(pair.key.value);
		const versionToKeys = new Map<string, string[]>();

		for (const entry of pair.value.items) {
			if (!isPair(entry) || !isScalar(entry.key)) continue;
			const value = entry.value;
			if (!isScalar(value) || value.anchor) continue;
			const versionString = String(value.value);
			const keyString = String(entry.key.value);
			versionToKeys.set(versionString, [
				...(versionToKeys.get(versionString) ?? []),
				keyString,
			]);
		}

		for (const [version, keys] of versionToKeys) {
			if (keys.length < 2) continue;

			let anchorName = catalogName;
			let counter = 1;
			while (existingAnchors.has(anchorName)) {
				anchorName = `${catalogName}-${counter++}`;
			}
			existingAnchors.add(anchorName);

			proposals.push({
				anchorName,
				catalog: catalogName,
				keys,
				version,
			});
		}
	}

	return proposals;
}

- `anchor-pnpm add <name> <version>` — append a new `&<name> <version>` entry.
- `anchor-pnpm update <name> <version>` — set the scalar value of an existing anchor (all `*<name>` aliases follow automatically).
- `anchor-pnpm remove <name>` — remove the anchor; errors if aliases still reference it (pass `--force` to inline them first).
- `anchor-pnpm sync` — _scheduled, not yet implemented._ Auto-detect catalog groups with repeated versions and convert them to anchor+alias pairs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The README states that anchor-pnpm sync is "not yet implemented", but the implementation is included in this pull request. This should be updated to reflect the current status.

Suggested change
- `anchor-pnpm sync`_scheduled, not yet implemented._ Auto-detect catalog groups with repeated versions and convert them to anchor+alias pairs.
- `anchor-pnpm sync` — Auto-detect catalog groups with repeated versions and convert them to anchor+alias pairs (use `--apply` to write changes).

Comment on lines +66 to +90
export function remove(
document: Document,
name: string,
options: { force?: boolean } = {},
): { dangling: string[]; removed: boolean } {
const seq = getVersionsSeq(document);
if (!seq) return { dangling: [], removed: false };

const dangling = collectAliasPaths(document, name);
if (dangling.length > 0 && !options.force) {
return { dangling, removed: false };
}

if (dangling.length > 0 && options.force) {
const scalar = findAnchor(seq, name);
replaceAliasesWithLiteral(document, name, scalar?.value);
}

const index = seq.items.findIndex(
(item) => isScalar(item) && item.anchor === name,
);
if (index === -1) return { dangling: [], removed: false };
seq.items.splice(index, 1);
return { dangling: [], removed: true };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The remove function should verify the anchor's existence in the __versions section before performing any side effects like replacing aliases. Currently, if force is true but the anchor is missing from __versions, it replaces aliases with undefined (resulting in null in YAML) and then returns removed: false, leaving the document in an inconsistent state.

export function remove(
	document: Document,
	name: string,
	options: { force?: boolean } = {},
): { dangling: string[]; removed: boolean } {
	const seq = getVersionsSeq(document);
	if (!seq) return { dangling: [], removed: false };

	const index = seq.items.findIndex(
		(item) => isScalar(item) && item.anchor === name,
	);
	if (index === -1) return { dangling: [], removed: false };
	const scalar = seq.items[index] as Scalar;

	const dangling = collectAliasPaths(document, name);
	if (dangling.length > 0 && !options.force) {
		return { dangling, removed: false };
	}

	if (dangling.length > 0 && options.force) {
		replaceAliasesWithLiteral(document, name, scalar.value);
	}

	seq.items.splice(index, 1);
	return { dangling: [], removed: true };
}

@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.

Actionable comments posted: 4

🤖 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.

Inline comments:
In `@core/anchor-pnpm/README.md`:
- Line 34: Update the README entry for the anchor-pnpm CLI: change the
`anchor-pnpm sync` line to reflect that the command is implemented (remove
"scheduled, not yet implemented") and briefly describe its behavior (auto-detect
catalog groups with repeated versions and convert them to anchor+alias pairs),
so users are not misled; locate the `anchor-pnpm sync` text in
core/anchor-pnpm/README.md and replace the outdated status text with a short,
accurate description of the shipped functionality.
- Around line 38-42: The fenced code block showing pnpm flags is missing a
language specifier; update the opening fence from ``` to use a language label
(e.g., ```text or ```console) so the block is lint-safe (MD040) and preserves
plaintext formatting—edit the fenced block that contains the lines starting with
"--workspace, -w", "--dry-run, -d", and "--verbose, -v" in the README.

In `@core/anchor-pnpm/src/anchors.ts`:
- Around line 116-123: sync() currently assigns the same anchorName (anchorName:
catalogName) for every repeated-version group, causing collisions when multiple
duplicated versions exist; change sync() to generate deterministic,
non-conflicting anchor names for each proposal (e.g., include a stable suffix
derived from the version or an incremental index: use catalogName + ":" +
versionHash or catalogName + "#" + groupIndex) and populate proposals with that
unique anchorName (refer to versionToKeys, proposals, catalogName, version).
Additionally add a collision guard in applySyncProposal that, before rewriting
values, resolves any existing anchorName and aborts with an error if it points
to a different version than the proposal (i.e., check resolvedVersion vs
proposal.version), so applySyncProposal will not silently overwrite a mismatched
anchor.
- Around line 79-89: The current logic calls replaceAliasesWithLiteral(document,
name, scalar?.value) when options.force is true before verifying the anchor
actually exists, which can rewrite aliases even if no anchor is removed; change
the ordering so you first locate the anchor (use seq.items.findIndex / isScalar
with anchor === name) and only if index !== -1 proceed to call
replaceAliasesWithLiteral with the real scalar value and then splice seq.items
and return removed: true; if the anchor is not found, return removed: false
without touching aliases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7fdf174c-8962-4bd1-a6f4-a76cc51787f3

📥 Commits

Reviewing files that changed from the base of the PR and between 9a84b42 and 6484ff6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • README.md
  • core/anchor-pnpm/README.md
  • core/anchor-pnpm/__snapshots__/tsnapi/cli.snapshot.d.ts
  • core/anchor-pnpm/__snapshots__/tsnapi/cli.snapshot.js
  • core/anchor-pnpm/__snapshots__/tsnapi/index.snapshot.d.ts
  • core/anchor-pnpm/__snapshots__/tsnapi/index.snapshot.js
  • core/anchor-pnpm/package.json
  • core/anchor-pnpm/src/anchors.ts
  • core/anchor-pnpm/src/cli.ts
  • core/anchor-pnpm/src/index.ts
  • core/anchor-pnpm/src/workspace.ts
  • core/anchor-pnpm/test/anchors.test.ts
  • core/anchor-pnpm/tsconfig.json
  • core/anchor-pnpm/tsdown.config.ts
  • core/anchor-pnpm/vitest.config.ts

- `anchor-pnpm add <name> <version>` — append a new `&<name> <version>` entry.
- `anchor-pnpm update <name> <version>` — set the scalar value of an existing anchor (all `*<name>` aliases follow automatically).
- `anchor-pnpm remove <name>` — remove the anchor; errors if aliases still reference it (pass `--force` to inline them first).
- `anchor-pnpm sync` — _scheduled, not yet implemented._ Auto-detect catalog groups with repeated versions and convert them to anchor+alias pairs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix outdated sync command status in docs.

Line 34 says sync is not implemented, but this PR ships it. This will mislead users about available functionality.

Suggested doc correction
-- `anchor-pnpm sync` — _scheduled, not yet implemented._ Auto-detect catalog groups with repeated versions and convert them to anchor+alias pairs.
+- `anchor-pnpm sync` — detect catalog groups with repeated versions and convert them to anchor+alias pairs (`--apply` writes the proposal).
🤖 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 `@core/anchor-pnpm/README.md` at line 34, Update the README entry for the
anchor-pnpm CLI: change the `anchor-pnpm sync` line to reflect that the command
is implemented (remove "scheduled, not yet implemented") and briefly describe
its behavior (auto-detect catalog groups with repeated versions and convert them
to anchor+alias pairs), so users are not misled; locate the `anchor-pnpm sync`
text in core/anchor-pnpm/README.md and replace the outdated status text with a
short, accurate description of the shipped functionality.

Comment on lines +38 to +42
```
--workspace, -w Path to pnpm-workspace.yaml (default: auto-detect via @manypkg/find-root)
--dry-run, -d Print the rewritten YAML to stdout instead of writing
--verbose, -v Show detailed output
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block.

Line 38 opens a fence without a language (MD040), which will keep markdown lint warnings active.

Suggested lint-safe fence
-```
+```text
 --workspace, -w   Path to pnpm-workspace.yaml  (default: auto-detect via `@manypkg/find-root`)
 --dry-run,  -d    Print the rewritten YAML to stdout instead of writing
 --verbose,  -v    Show detailed output
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 38-38: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@core/anchor-pnpm/README.md` around lines 38 - 42, The fenced code block
showing pnpm flags is missing a language specifier; update the opening fence
from ``` to use a language label (e.g., ```text or ```console) so the block is
lint-safe (MD040) and preserves plaintext formatting—edit the fenced block that
contains the lines starting with "--workspace, -w", "--dry-run, -d", and
"--verbose, -v" in the README.

Comment on lines +79 to +89
if (dangling.length > 0 && options.force) {
const scalar = findAnchor(seq, name);
replaceAliasesWithLiteral(document, name, scalar?.value);
}

const index = seq.items.findIndex(
(item) => isScalar(item) && item.anchor === name,
);
if (index === -1) return { dangling: [], removed: false };
seq.items.splice(index, 1);
return { dangling: [], removed: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent forced removal from mutating aliases when the anchor is missing.

On Line 79–82, aliases are inlined before confirming the anchor exists. If the anchor is absent, Line 87 returns removed: false after already rewriting aliases (potentially to null), which is data corruption.

Safe ordering fix
 export function remove(
 	document: Document,
 	name: string,
 	options: { force?: boolean } = {},
 ): { dangling: string[]; removed: boolean } {
 	const seq = getVersionsSeq(document);
 	if (!seq) return { dangling: [], removed: false };

 	const dangling = collectAliasPaths(document, name);
 	if (dangling.length > 0 && !options.force) {
 		return { dangling, removed: false };
 	}

+	const scalar = findAnchor(seq, name);
+	if (!scalar) return { dangling, removed: false };
+
 	if (dangling.length > 0 && options.force) {
-		const scalar = findAnchor(seq, name);
-		replaceAliasesWithLiteral(document, name, scalar?.value);
+		replaceAliasesWithLiteral(document, name, scalar.value);
 	}

 	const index = seq.items.findIndex(
 		(item) => isScalar(item) && item.anchor === name,
 	);
 	if (index === -1) return { dangling: [], removed: false };
 	seq.items.splice(index, 1);
 	return { dangling: [], removed: true };
 }
🤖 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 `@core/anchor-pnpm/src/anchors.ts` around lines 79 - 89, The current logic
calls replaceAliasesWithLiteral(document, name, scalar?.value) when
options.force is true before verifying the anchor actually exists, which can
rewrite aliases even if no anchor is removed; change the ordering so you first
locate the anchor (use seq.items.findIndex / isScalar with anchor === name) and
only if index !== -1 proceed to call replaceAliasesWithLiteral with the real
scalar value and then splice seq.items and return removed: true; if the anchor
is not found, return removed: false without touching aliases.

Comment on lines +116 to +123
for (const [version, keys] of versionToKeys) {
if (keys.length < 2) continue;
proposals.push({
anchorName: catalogName,
catalog: catalogName,
keys,
version,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

sync() can generate conflicting anchor names for different versions.

Line 119 uses anchorName: catalogName for every repeated-version group in that catalog. If one catalog has multiple duplicated versions, proposals collide on the same anchor and --apply can rewrite entries to the wrong version.

Direction for deterministic non-conflicting names
 			proposals.push({
-				anchorName: catalogName,
+				anchorName: `${catalogName}_${version.replace(/[^A-Za-z0-9_-]/g, "_")}`,
 				catalog: catalogName,
 				keys,
 				version,
 			});
Also add a collision guard in `applySyncProposal` (abort when an existing anchor name resolves to a different version) before rewriting values.
🤖 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 `@core/anchor-pnpm/src/anchors.ts` around lines 116 - 123, sync() currently
assigns the same anchorName (anchorName: catalogName) for every repeated-version
group, causing collisions when multiple duplicated versions exist; change sync()
to generate deterministic, non-conflicting anchor names for each proposal (e.g.,
include a stable suffix derived from the version or an incremental index: use
catalogName + ":" + versionHash or catalogName + "#" + groupIndex) and populate
proposals with that unique anchorName (refer to versionToKeys, proposals,
catalogName, version). Additionally add a collision guard in applySyncProposal
that, before rewriting values, resolves any existing anchorName and aborts with
an error if it points to a different version than the proposal (i.e., check
resolvedVersion vs proposal.version), so applySyncProposal will not silently
overwrite a mismatched anchor.

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.

1 participant