Repository: withstudiocms/cli-utilities
Pull request #19: Add new upgrade package
Author: login: Adammatthiesen
Created: 2025-04-12T08:52:40Z | Merged: 2025-04-13T05:52:12Z

<!-- Thank you for opening a PR! We really appreciate you taking the time to help out 🙌 -->

#### Description

- Closes # <!-- If applicable add an issue number to this PR so it can be closed otherwise feel free to remove this. -->
- What does this PR change? Give us a brief description.

<!--
Here’s what will happen next:

One of our maintainers will review your pull request as soon as possible. We strive to provide feedback within a day, but please understand that responses may occasionally take longer depending on the circumstance and our availability. If we request any changes, please feel free to ask for clarification or provide additional context. We appreciate your patience and contribution to the project.
-->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Launched a CLI upgrade tool for StudioCMS that streamlines dependency updates and project verification, featuring enhanced JSON handling and dynamic registry detection.
  - Introduced new utility functions for improved JSON reading and registry management.
  - Added a command-line interface for executing upgrade processes and verifying project integrity.
  - Implemented a verification process to ensure project integrity and compatibility.
- **Bug Fixes**
  - Resolved export inconsistencies for improved reliability.
- **Documentation**
  - Provided clear usage instructions for the upgrade tool and added a license file.
- **Tests**
  - Expanded automated test coverage to ensure robust upgrade and verification processes.
- **Chores**
  - Updated build configurations, project metadata, and licensing details, including new dependencies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.changeset/lazy-radios-decide.md b/.changeset/lazy-radios-decide.md
new file mode 100644
index 0000000..19a2d37
--- /dev/null
+++ b/.changeset/lazy-radios-decide.md
@@ -0,0 +1,5 @@
+---
+"@withstudiocms/cli-kit": patch
+---
+
+Added two new utils, readJson and getRegistry
diff --git a/.changeset/many-queens-shake.md b/.changeset/many-queens-shake.md
new file mode 100644
index 0000000..ac1c3c0
--- /dev/null
+++ b/.changeset/many-queens-shake.md
@@ -0,0 +1,5 @@
+---
+"create-studiocms": patch
+---
+
+fix import, pathUtil was previously exported as default by mistake, fixed as named export
diff --git a/.changeset/nice-webs-joke.md b/.changeset/nice-webs-joke.md
new file mode 100644
index 0000000..7d79bf0
--- /dev/null
+++ b/.changeset/nice-webs-joke.md
@@ -0,0 +1,5 @@
+---
+"@studiocms/upgrade": minor
+---
+
+Initial release
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0db9b66..53b4415 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -23,6 +23,7 @@
     "npminstall",
     "ntqry",
     "pegasi",
+    "premajor",
     "SCMS",
     "spooktacular",
     "stefanzweifel",
diff --git a/knip.config.ts b/knip.config.ts
index 1a6945b..26229fe 100644
--- a/knip.config.ts
+++ b/knip.config.ts
@@ -19,7 +19,7 @@ const config: KnipConfig = {
 			],
 		},
 		'packages/*': {
-			ignoreDependencies: ['@clack/core', 'commander'],
+			ignoreDependencies: ['@clack/core', 'commander', '@commander-js/extra-typings'],
 			entry: ['src/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
 			project: ['**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
 		},
diff --git a/package.json b/package.json
index d3b5631..952d82f 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
   },
   "scripts": {
     "knip": "knip",
-    "build": "pnpm --filter create-studiocms --filter '@withstudiocms/*' run build",
+    "build": "pnpm --filter create-studiocms --filter '@withstudiocms/*' --filter '@studiocms/*' run build",
     "test:create-studiocms": "node ./packages/create-studiocms/create-studiocms.mjs",
     "test": "vitest run --reporter=default --reporter=html",
     "lint": "biome check .",
diff --git a/packages/create-studiocms/src/index.ts b/packages/create-studiocms/src/index.ts
index 7fd9e77..e14a181 100644
--- a/packages/create-studiocms/src/index.ts
+++ b/packages/create-studiocms/src/index.ts
@@ -1,7 +1,7 @@
 import { StudioCMSColorwayError } from '@withstudiocms/cli-kit/colors';
 import { Command, Option } from '@withstudiocms/cli-kit/commander';
 import { CLITitle, termPrefix } from '@withstudiocms/cli-kit/messages';
-import pathUtil from '@withstudiocms/cli-kit/utils';
+import { pathUtil } from '@withstudiocms/cli-kit/utils';
 import chalk from 'chalk';
 import readJson from './utils/readJson.js';
 
diff --git a/packages/create-studiocms/tests/fixtures/not-empty/git.json b/packages/create-studiocms/tests/fixtures/not-empty/git.json
index 0967ef4..9e26dfe 100644
--- a/packages/create-studiocms/tests/fixtures/not-empty/git.json
+++ b/packages/create-studiocms/tests/fixtures/not-empty/git.json
@@ -1 +1 @@
-{}
+{}
\ No newline at end of file
diff --git a/packages/studiocms_upgrade/LICENSE b/packages/studiocms_upgrade/LICENSE
new file mode 100644
index 0000000..61297c2
--- /dev/null
+++ b/packages/studiocms_upgrade/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 StudioCMS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/studiocms_upgrade/README.md b/packages/studiocms_upgrade/README.md
new file mode 100644
index 0000000..171cee2
--- /dev/null
+++ b/packages/studiocms_upgrade/README.md
@@ -0,0 +1,51 @@
+# @studiocms/upgrade
+
+A command-line tool for upgrading your StudioCMS installation and dependencies.
+
+You can run this command in your terminal to upgrade your official Astro Project, integrations at the same time as you upgrade StudiOCMS.
+
+## Usage
+
+`@studiocms/upgrade` should not be added as a dependency to your project, but run as a temporary executable whenever you want to upgrade using [`npx`](https://docs.npmjs.com/cli/v10/commands/npx) or [`dlx`](https://pnpm.io/cli/dlx).
+
+**With NPM:**
+
+```bash
+npx @studiocms/upgrade
+```
+
+**With Yarn:**
+
+```bash
+yarn dlx @studiocms/upgrade
+```
+
+**With PNPM:**
+
+```bash
+pnpm dlx @studiocms/upgrade
+```
+
+## Options
+
+### tag (optional)
+
+It is possible to pass a specific `tag` to resolve packages against. If not included, `@studiocms/upgrade` looks for the `latest` tag.
+
+**With NPM:**
+
+```bash
+npx @studiocms/upgrade beta
+```
+
+**With Yarn:**
+
+```bash
+yarn dlx @studiocms/upgrade beta
+```
+
+**With PNPM:**
+
+```bash
+pnpm dlx @studiocms/upgrade beta
+```
\ No newline at end of file
diff --git a/packages/studiocms_upgrade/package.json b/packages/studiocms_upgrade/package.json
new file mode 100644
index 0000000..f2d9044
--- /dev/null
+++ b/packages/studiocms_upgrade/package.json
@@ -0,0 +1,43 @@
+{
+	"name": "@studiocms/upgrade",
+	"version": "0.0.1",
+	"type": "module",
+	"author": "withstudiocms",
+	"license": "MIT",
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/withstudiocms/cli-utilities.git",
+		"directory": "packages/studiocms_upgrade"
+	},
+	"bugs": "https://github.com/withstudiocms/cli-utilities/issues",
+	"homepage": "https://studiocms.dev",
+	"scripts": {
+		"build": "buildkit build \"src/index.ts\" --bundle"
+	},
+	"exports": {
+	  ".": "./upgrade.mjs"
+	},
+	"main": "./upgrade.mjs",
+	"bin": "./upgrade.mjs",
+	"files": ["dist"],
+	"dependencies": {
+		"@clack/core": "0.4.2",
+		"@clack/prompts": "0.10.1",
+		"@commander-js/extra-typings": "^13.1.0",
+		"@withstudiocms/cli-kit": "workspace:*",
+		"commander": "^13.1.0",
+		"chalk": "^5.4.1",
+		"package-manager-detector": "^1.1.0",
+		"semver": "^7.7.1",
+		"terminal-link": "^4.0.0"
+	},
+	"devDependencies": {
+	  "@types/semver": "^7.7.0"
+	},
+	"engines": {
+		"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+	},
+	"publishConfig": {
+		"provenance": true
+	}
+}
diff --git a/packages/studiocms_upgrade/src/actions/install.ts b/packages/studiocms_upgrade/src/actions/install.ts
new file mode 100644
index 0000000..645bc81
--- /dev/null
+++ b/packages/studiocms_upgrade/src/actions/install.ts
@@ -0,0 +1,226 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { label, random, sleep } from '@withstudiocms/cli-kit/messages';
+import { shell } from '@withstudiocms/cli-kit/utils';
+import chalk from 'chalk';
+import { resolveCommand } from 'package-manager-detector';
+import terminalLink from 'terminal-link';
+import type { Context, PackageInfo } from '../utils/context';
+import { celebrations, done } from '../utils/messages';
+
+export async function install(
+	ctx: Pick<Context, 'prompt' | 'packageManager' | 'cwd' | 'exit' | 'tasks' | 'packages' | 'dryRun'>
+) {
+	ctx.prompt.note(
+		`\n${label('StudioCMS', chalk.bgGreen, chalk.black)}  ${chalk.bold(
+			'Package upgrade in progress.'
+		)}`
+	);
+
+	const { current, dependencies, devDependencies } = filterPackages(ctx);
+	const toInstall = [...dependencies, ...devDependencies].sort(sortPackages);
+	for (const packageInfo of current.sort(sortPackages)) {
+		const tag = /^\d/.test(packageInfo.targetVersion)
+			? packageInfo.targetVersion
+			: packageInfo.targetVersion.slice(1);
+		ctx.prompt.log.info(`${packageInfo.name} is up to date on v${tag}`);
+		await sleep(random(50, 150));
+	}
+
+	if (toInstall.length === 0 && !ctx.dryRun) {
+		await success(random(celebrations), random(done), ctx);
+		return;
+	}
+	const majors: PackageInfo[] = [];
+	for (const packageInfo of toInstall) {
+		const word = ctx.dryRun ? 'can' : 'will';
+		await upgrade(packageInfo, `${word} be updated`, ctx);
+		if (packageInfo.isMajor) {
+			majors.push(packageInfo);
+		}
+	}
+
+	if (majors.length > 0) {
+		const shouldProceed = await ctx.prompt.confirm({
+			message: `${pluralize(
+				['One package has', 'Some packages have'],
+				majors.length
+			)} breaking changes. Continue?`,
+			initialValue: true,
+		});
+
+		if (ctx.prompt.isCancel(shouldProceed)) {
+			return ctx.exit(0);
+		}
+
+		if (!shouldProceed) {
+			return ctx.exit(0);
+		}
+
+		ctx.prompt.log.warn(`Be sure to follow the ${pluralize('CHANGELOG', majors.length)}.`);
+		for (const pkg of majors.sort(sortPackages)) {
+			// biome-ignore lint/style/noNonNullAssertion: <explanation>
+			await changelog(pkg.name, pkg.changelogTitle!, pkg.changelogURL!, ctx);
+		}
+	}
+
+	if (ctx.dryRun) {
+		ctx.prompt.note('Skipping dependency installation', '--dry-run');
+	} else {
+		await runInstallCommand(ctx, dependencies, devDependencies);
+	}
+}
+
+function filterPackages(ctx: Pick<Context, 'packages'>) {
+	const current: PackageInfo[] = [];
+	const dependencies: PackageInfo[] = [];
+	const devDependencies: PackageInfo[] = [];
+	for (const packageInfo of ctx.packages) {
+		const { currentVersion, targetVersion, isDevDependency } = packageInfo;
+		// Remove prefix from version before comparing
+		if (currentVersion.replace(/^\D+/, '') === targetVersion.replace(/^\D+/, '')) {
+			current.push(packageInfo);
+		} else {
+			const arr = isDevDependency ? devDependencies : dependencies;
+			arr.push(packageInfo);
+		}
+	}
+	return { current, dependencies, devDependencies };
+}
+
+/**
+ * An `Array#sort` comparator function to normalize how packages are displayed.
+ * This only changes how the packages are displayed in the CLI, it is not persisted to `package.json`.
+ */
+function sortPackages(a: PackageInfo, b: PackageInfo): number {
+	if (a.isMajor && !b.isMajor) return 1;
+	if (b.isMajor && !a.isMajor) return -1;
+	return a.name.localeCompare(b.name);
+}
+
+const success = async (prefix: string, text: string, ctx: Pick<Context, 'prompt'>) => {
+	const length = 10 + prefix.length + text.length;
+	if (length > process.stdout.columns) {
+		ctx.prompt.log.success(`${chalk.green('✔')}  ${prefix}`);
+		ctx.prompt.log.success(`${' '.repeat(4)}${chalk.dim(text)}`);
+	} else {
+		ctx.prompt.log.success(`${chalk.green('✔')}  ${prefix} ${chalk.dim(text)}`);
+	}
+};
+
+const upgrade = async (packageInfo: PackageInfo, text: string, ctx: Pick<Context, 'prompt'>) => {
+	const { name, isMajor = false, targetVersion, currentVersion } = packageInfo;
+
+	const bg = isMajor ? (v: string) => chalk.bgYellow(chalk.black(` ${v} `)) : chalk.green;
+	const style = isMajor ? chalk.yellow : chalk.green;
+	const symbol = isMajor ? '▲' : '●';
+
+	const fromVersion = currentVersion.replace(/^\D+/, '');
+	const toVersion = targetVersion.replace(/^\D+/, '');
+	const version = `from v${fromVersion} to v${toVersion}`;
+
+	const length = 12 + name.length + text.length + version.length;
+	if (length > process.stdout.columns) {
+		ctx.prompt.log.info(`${style(symbol)}  ${name}`);
+		ctx.prompt.log.info(`${' '.repeat(4)}${chalk.dim(text)} ${bg(version)}`);
+	} else {
+		ctx.prompt.log.info(`${style(symbol)}  ${name} ${chalk.dim(text)} ${bg(version)}`);
+	}
+};
+
+function pluralize(word: string | [string, string], n: number) {
+	const [singular, plural] = Array.isArray(word) ? word : [word, `${word}s`];
+	if (n === 1) return singular;
+	return plural;
+}
+
+const changelog = async (name: string, text: string, url: string, ctx: Pick<Context, 'prompt'>) => {
+	const link = terminalLink(text, url, { fallback: () => url });
+	const linkLength = terminalLink.isSupported ? text.length : url.length;
+	const symbol = ' ';
+
+	const length = 12 + name.length + linkLength;
+	if (length > process.stdout.columns) {
+		ctx.prompt.log.info(`${symbol}  ${name}`);
+		ctx.prompt.log.info(`${' '.repeat(4)}${chalk.cyan(chalk.underline(link))}`);
+	} else {
+		ctx.prompt.log.info(`${symbol}  ${name} ${chalk.cyan(chalk.underline(link))}`);
+	}
+};
+
+async function runInstallCommand(
+	ctx: Pick<Context, 'prompt' | 'packageManager' | 'cwd' | 'exit' | 'tasks'>,
+	dependencies: PackageInfo[],
+	devDependencies: PackageInfo[]
+) {
+	const cwd = fileURLToPath(ctx.cwd);
+	if (ctx.packageManager.name === 'yarn') await ensureYarnLock({ cwd });
+
+	const installCommand = resolveCommand(ctx.packageManager.agent, 'add', []);
+	if (!installCommand) {
+		// NOTE: Usually it's impossible to reach here as `package-manager-detector` should
+		// already match a supported agent
+		ctx.prompt.log.error(`Unable to find install command for ${ctx.packageManager.name}.`);
+		return ctx.exit(1);
+	}
+
+	ctx.tasks.push({
+		title: 'Install Dependencies',
+		task: async (message) => {
+			message(`Installing dependencies with ${ctx.packageManager.name}...`);
+			try {
+				if (dependencies.length > 0) {
+					await shell(
+						installCommand.command,
+						[
+							...installCommand.args,
+							...dependencies.map(
+								({ name, targetVersion }) => `${name}@${targetVersion.replace(/^\^/, '')}`
+							),
+						],
+						{ cwd, timeout: 90_000, stdio: 'ignore' }
+					);
+				}
+				if (devDependencies.length > 0) {
+					await shell(
+						installCommand.command,
+						[
+							...installCommand.args,
+							...devDependencies.map(
+								({ name, targetVersion }) => `${name}@${targetVersion.replace(/^\^/, '')}`
+							),
+						],
+						{ cwd, timeout: 90_000, stdio: 'ignore' }
+					);
+				}
+			} catch {
+				const manualInstallCommand = [
+					installCommand.command,
+					...installCommand.args,
+					...[...dependencies, ...devDependencies].map(
+						({ name, targetVersion }) => `${name}@${targetVersion}`
+					),
+				].join(' ');
+				ctx.prompt.log.error(
+					`Dependencies failed to install, please run the following command manually:\n${chalk.bold(manualInstallCommand)}`
+				);
+				return ctx.exit(1);
+			}
+		},
+	});
+}
+
+/**
+ * Yarn Berry (PnP) versions will throw an error if there isn't an existing `yarn.lock` file
+ * If a `yarn.lock` file doesn't exist, this function writes an empty `yarn.lock` one.
+ * Unfortunately this hack is required to run `yarn install`.
+ *
+ * The empty `yarn.lock` file is immediately overwritten by the installation process.
+ * See https://github.com/withastro/astro/pull/8028
+ */
+async function ensureYarnLock({ cwd }: { cwd: string }) {
+	const yarnLock = path.join(cwd, 'yarn.lock');
+	if (fs.existsSync(yarnLock)) return;
+	return fs.promises.writeFile(yarnLock, '', { encoding: 'utf-8' });
+}
diff --git a/packages/studiocms_upgrade/src/actions/verify.ts b/packages/studiocms_upgrade/src/actions/verify.ts
new file mode 100644
index 0000000..6d33d69
--- /dev/null
+++ b/packages/studiocms_upgrade/src/actions/verify.ts
@@ -0,0 +1,199 @@
+import dns from 'node:dns/promises';
+import { existsSync } from 'node:fs';
+import { readFile } from 'node:fs/promises';
+import { getRegistry } from '@withstudiocms/cli-kit/utils';
+import chalk from 'chalk';
+import semverCoerce from 'semver/functions/coerce.js';
+import semverDiff from 'semver/functions/diff.js';
+import semverParse from 'semver/functions/parse.js';
+import type { Context, PackageInfo } from '../utils/context';
+
+export async function verify(
+	ctx: Pick<Context, 'version' | 'packages' | 'cwd' | 'dryRun' | 'exit' | 'prompt'>
+) {
+	const registry = await getRegistry();
+
+	if (!ctx.dryRun) {
+		const online = await isOnline(registry);
+		if (!online) {
+			ctx.prompt.log.error('Unable to connect to the internet.');
+			ctx.exit(1);
+		}
+	}
+
+	const isStudioCMSProject = await verifyStudioCMSProject(ctx);
+	if (!isStudioCMSProject) {
+		ctx.prompt.log.error('StudioCMS installation not found in the current directory.');
+		ctx.exit(1);
+	}
+
+	const ok = await verifyVersions(ctx, registry);
+	if (!ok) {
+		ctx.prompt.log.error(`Version ${chalk.reset(ctx.version)} ${chalk.dim('could not be found!')}`);
+		ctx.prompt.note('https://github.com/withstudiocms/studiocms/releases', 'check');
+		ctx.exit(1);
+	}
+}
+
+function isOnline(registry: string): Promise<boolean> {
+	const { hostname } = new URL(registry);
+	return dns.lookup(hostname).then(
+		() => true,
+		() => false
+	);
+}
+
+function safeJSONParse(value: string) {
+	try {
+		return JSON.parse(value);
+	} catch {}
+	return {};
+}
+
+async function verifyStudioCMSProject(ctx: Pick<Context, 'cwd' | 'version' | 'packages'>) {
+	const packageJson = new URL('./package.json', ctx.cwd);
+	if (!existsSync(packageJson)) return false;
+	const contents = await readFile(packageJson, { encoding: 'utf-8' });
+	const { dependencies = {}, devDependencies = {} } = safeJSONParse(contents);
+	if (!contents.includes('studiocms')) return false;
+
+	if (dependencies.studiocms === undefined && devDependencies.studiocms === undefined) return false;
+
+	// Side-effect! Persist dependency info to the shared context
+	collectPackageInfo(ctx, dependencies, devDependencies);
+
+	return true;
+}
+
+function isStudioCMSPackage(name: string, _version: string) {
+	return (
+		name === 'studiocms' ||
+		name.startsWith('@studiocms/') ||
+		name === 'astro' ||
+		name.startsWith('@astrojs/')
+	);
+}
+
+function isAllowedPackage(name: string, _version: string) {
+	return name !== '@studiocms/upgrade';
+}
+
+function isValidVersion(_name: string, version: string) {
+	return semverCoerce(version, { loose: true }) !== null;
+}
+
+function isSupportedPackage(name: string, version: string): boolean {
+	for (const validator of [isStudioCMSPackage, isAllowedPackage, isValidVersion]) {
+		if (!validator(name, version)) return false;
+	}
+	return true;
+}
+
+export function collectPackageInfo(
+	ctx: Pick<Context, 'version' | 'packages'>,
+	dependencies: Record<string, string> = {},
+	devDependencies: Record<string, string> = {}
+) {
+	for (const [name, currentVersion] of Object.entries(dependencies)) {
+		if (!isSupportedPackage(name, currentVersion)) continue;
+		ctx.packages.push({
+			name,
+			currentVersion,
+			targetVersion: ctx.version,
+		});
+	}
+	for (const [name, currentVersion] of Object.entries(devDependencies)) {
+		if (!isSupportedPackage(name, currentVersion)) continue;
+		ctx.packages.push({
+			name,
+			currentVersion,
+			targetVersion: ctx.version,
+			isDevDependency: true,
+		});
+	}
+}
+
+async function verifyVersions(
+	ctx: Pick<Context, 'version' | 'packages' | 'exit'>,
+	registry: string
+) {
+	const tasks: Promise<void>[] = [];
+	for (const packageInfo of ctx.packages) {
+		tasks.push(resolveTargetVersion(packageInfo, registry));
+	}
+	try {
+		await Promise.all(tasks);
+	} catch {
+		return false;
+	}
+	for (const packageInfo of ctx.packages) {
+		if (!packageInfo.targetVersion) {
+			return false;
+		}
+	}
+	return true;
+}
+
+async function resolveTargetVersion(packageInfo: PackageInfo, registry: string): Promise<void> {
+	const packageMetadata = await fetch(`${registry}/${packageInfo.name}`, {
+		headers: { accept: 'application/vnd.npm.install-v1+json' },
+	});
+	if (packageMetadata.status >= 400) {
+		throw new Error(`Unable to resolve "${packageInfo.name}"`);
+	}
+	const { 'dist-tags': distTags } = await packageMetadata.json();
+	let version = distTags[packageInfo.targetVersion];
+	if (version) {
+		packageInfo.tag = packageInfo.targetVersion;
+		packageInfo.targetVersion = version;
+	} else {
+		packageInfo.targetVersion = 'latest';
+		version = distTags.latest;
+	}
+	if (packageInfo.currentVersion === version) {
+		return;
+	}
+	const prefix = packageInfo.targetVersion === 'latest' ? '^' : '';
+	packageInfo.targetVersion = `${prefix}${version}`;
+	// biome-ignore lint/style/noNonNullAssertion: <explanation>
+	const fromVersion = semverCoerce(packageInfo.currentVersion)!;
+	// biome-ignore lint/style/noNonNullAssertion: <explanation>
+	const toVersion = semverParse(version)!;
+	const bump = semverDiff(fromVersion, toVersion);
+	if ((bump === 'major' && toVersion.prerelease.length === 0) || bump === 'premajor') {
+		packageInfo.isMajor = true;
+		if (packageInfo.name === 'astro') {
+			// TODO: Make an upgrade guide on the docs
+			// const upgradeGuide = `https://docs.studiocms.dev/en/guides/upgrade-to/v${toVersion.major}/`;
+			const upgradeGuide = 'https://docs.studiocms.dev';
+			const docsRes = await fetch(upgradeGuide);
+			// OK if this request fails, it's probably a prerelease without a public migration guide.
+			// In that case, we should fallback to the CHANGELOG check below.
+			if (docsRes.status === 200) {
+				packageInfo.changelogURL = upgradeGuide;
+				packageInfo.changelogTitle = `Upgrade to StudioCMS v${toVersion.major}`;
+				return;
+			}
+		}
+		const latestMetadata = await fetch(`${registry}/${packageInfo.name}/latest`);
+		if (latestMetadata.status >= 400) {
+			throw new Error(`Unable to resolve "${packageInfo.name}"`);
+		}
+		const { repository } = await latestMetadata.json();
+		const branch = bump === 'premajor' ? 'next' : 'main';
+		packageInfo.changelogURL = extractChangelogURLFromRepository(repository, version, branch);
+		packageInfo.changelogTitle = 'CHANGELOG';
+	} else {
+		// Dependency updates should not include the specific dist-tag
+		// since they are just for compatibility
+		packageInfo.tag = undefined;
+	}
+}
+
+function extractChangelogURLFromRepository(
+	repository: Record<string, string>,
+	version: string,
+	branch = 'main'
+) {
+	return `${repository.url.replace('git+', '').replace('.git', '')}/blob/${branch}/${repository.directory}/CHANGELOG.md#${version.replace(/\./g, '')}`;
+}
diff --git a/packages/studiocms_upgrade/src/index.ts b/packages/studiocms_upgrade/src/index.ts
new file mode 100644
index 0000000..c43c7c2
--- /dev/null
+++ b/packages/studiocms_upgrade/src/index.ts
@@ -0,0 +1,55 @@
+import { tasks } from '@clack/prompts';
+import { StudioCMSColorwayBg, StudioCMSColorwayError } from '@withstudiocms/cli-kit/colors';
+import { Command, Option } from '@withstudiocms/cli-kit/commander';
+import { CLITitle, label, random, say, termPrefix } from '@withstudiocms/cli-kit/messages';
+import { readJson } from '@withstudiocms/cli-kit/utils';
+import chalk from 'chalk';
+import { install } from './actions/install';
+import { verify } from './actions/verify';
+import { getContext } from './utils/context';
+import { bye, celebrations, done } from './utils/messages';
+
+export { collectPackageInfo } from './actions/verify';
+export { install, verify, getContext };
+
+const pkgJson = readJson<{ version: string }>(new URL('../package.json', import.meta.url));
+
+export const main = async () =>
+	await new Command('upgrade')
+		.description('Upgrade StudioCMS packages')
+		.version(pkgJson.version, '-v, --version', 'Output the current version of the upgrade CLI')
+		.addHelpText('beforeAll', CLITitle)
+		.addHelpText(
+			'afterAll',
+			`\n${termPrefix}${chalk.dim.italic(`${chalk.reset(StudioCMSColorwayError('*'))} Indicates the default command that is run when calling this CLI.`)}`
+		)
+		.showHelpAfterError('(add --help for additional information)')
+		.argument(
+			'[version]',
+			'Specific tag to resolve packages against. If not included, will use the latest tag.'
+		)
+		.addOption(new Option('--dry-run'))
+		// Global Options
+		.addOption(new Option('--color', 'Force color output') /* implemented by chalk */)
+		.addOption(new Option('--no-color', 'Disable color output') /* implemented by chalk */)
+		.enablePositionalOptions(true)
+		.action(async (version, flags) => {
+			const context = await getContext(version || 'latest', flags);
+
+			context.prompt.intro(`${label('StudioCMS', StudioCMSColorwayBg, chalk.black)} Upgrade CLI`);
+
+			const steps = [verify, install];
+
+			for (const step of steps) {
+				await step(context);
+			}
+
+			if (context.tasks) {
+				await tasks(context.tasks);
+			}
+
+			await say([`${random(celebrations)} ${random(done)}`, random(bye)], { clear: false });
+
+			process.exit(0);
+		})
+		.parseAsync();
diff --git a/packages/studiocms_upgrade/src/utils/context.ts b/packages/studiocms_upgrade/src/utils/context.ts
new file mode 100644
index 0000000..7cd1dc3
--- /dev/null
+++ b/packages/studiocms_upgrade/src/utils/context.ts
@@ -0,0 +1,61 @@
+import { pathToFileURL } from 'node:url';
+import * as prompt from '@clack/prompts';
+import { type DetectResult, detect } from 'package-manager-detector';
+
+export interface Context {
+	prompt: typeof prompt;
+	version: string;
+	dryRun?: boolean;
+	cwd: URL;
+	stdin?: typeof process.stdin;
+	stdout?: typeof process.stdout;
+	packageManager: DetectResult;
+	packages: PackageInfo[];
+	exit(code: number): never;
+	tasks: prompt.Task[];
+}
+
+export interface PackageInfo {
+	name: string;
+	currentVersion: string;
+	targetVersion: string;
+	tag?: string;
+	isDevDependency?: boolean;
+	isMajor?: boolean;
+	changelogURL?: string;
+	changelogTitle?: string;
+}
+
+export async function getContext(
+	version = 'latest',
+	flags?: {
+		dryRun?: true | undefined;
+	}
+): Promise<Context> {
+	let packageManager: DetectResult | null = null;
+	try {
+		packageManager = await detect({
+			// Include the `install-metadata` strategy to have the package manager that's
+			// used for installation take precedence
+			strategies: ['install-metadata', 'lockfile', 'packageManager-field'],
+		});
+	} catch (error) {
+		console.warn(
+			'Failed to detect package manager:',
+			error instanceof Error ? error.message : String(error)
+		);
+	}
+	packageManager = packageManager ?? { agent: 'npm', name: 'npm' };
+	return {
+		prompt,
+		packageManager,
+		packages: [],
+		cwd: new URL(`${pathToFileURL(process.cwd())}/`),
+		dryRun: flags?.dryRun || false,
+		version,
+		exit(code) {
+			process.exit(code);
+		},
+		tasks: [],
+	};
+}
diff --git a/packages/studiocms_upgrade/src/utils/messages.ts b/packages/studiocms_upgrade/src/utils/messages.ts
new file mode 100644
index 0000000..d9ae19e
--- /dev/null
+++ b/packages/studiocms_upgrade/src/utils/messages.ts
@@ -0,0 +1,30 @@
+export const celebrations = [
+	'Beautiful.',
+	'Excellent!',
+	'Sweet!',
+	'Nice!',
+	'Huzzah!',
+	'Success.',
+	'Nice.',
+	'Wonderful.',
+	'Lovely!',
+	"Lookin' good.",
+	'Awesome.',
+];
+
+export const done = [
+	"You're on the latest and greatest.",
+	'Your integrations are up-to-date.',
+	'Everything is current.',
+	'Everything is up to date.',
+	'Integrations are all up to date.',
+	'Everything is on the latest and greatest.',
+	'Integrations are up to date.',
+];
+
+export const bye = [
+	'Thanks for using StudioCMS!',
+	"Can't wait to see what you build.",
+	'Good luck out there.',
+	'See you around!',
+];
diff --git a/packages/studiocms_upgrade/tests/context.test.ts b/packages/studiocms_upgrade/tests/context.test.ts
new file mode 100644
index 0000000..db404f5
--- /dev/null
+++ b/packages/studiocms_upgrade/tests/context.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'vitest';
+import { getContext } from '../src/utils/context';
+
+describe('context', () => {
+	it('no arguments', async () => {
+		const ctx = await getContext();
+		expect(ctx.version).toBe('latest');
+		expect(ctx.dryRun).toBeFalsy();
+	});
+
+	it('tag', async () => {
+		const ctx = await getContext('beta');
+		expect(ctx.version).toBe('beta');
+		expect(ctx.dryRun).toBeFalsy();
+	});
+
+	it('dry run', async () => {
+		const ctx = await getContext(undefined, { dryRun: true });
+		expect(ctx.dryRun).toBe(true);
+	});
+});
diff --git a/packages/studiocms_upgrade/tests/install.test.ts b/packages/studiocms_upgrade/tests/install.test.ts
new file mode 100644
index 0000000..7ff0f0c
--- /dev/null
+++ b/packages/studiocms_upgrade/tests/install.test.ts
@@ -0,0 +1,193 @@
+import type { DetectResult } from 'package-manager-detector';
+import { describe, expect, it } from 'vitest';
+import { install } from '../src/actions/install';
+import type { Context } from '../src/utils/context';
+
+describe('install', () => {
+	// const fixture = setup();
+	const ctx: Pick<
+		Context,
+		'prompt' | 'packageManager' | 'cwd' | 'exit' | 'tasks' | 'packages' | 'dryRun' | 'version'
+	> & { messages: string[] } = {
+		cwd: '' as unknown as URL,
+		version: 'latest',
+		packageManager: 'npm' as unknown as DetectResult,
+		dryRun: true,
+		packages: [],
+		prompt: {
+			// @ts-ignore
+			log: {
+				info: (str: string) => ctx.messages.push(str),
+				warn: (str: string) => ctx.messages.push(str),
+				error: (str: string) => ctx.messages.push(str),
+			},
+			// @ts-ignore
+			note: (str: string, title?: string) => ctx.messages.push(str),
+			// @ts-ignore
+			isCancel: () => false,
+			cancel: () => {},
+			confirm: async ({ message }) => {
+				ctx.messages.push(message);
+				return true;
+			},
+		},
+		exit(code) {
+			process.exit(code);
+		},
+		tasks: [],
+		messages: [],
+	};
+
+	it('up to date', async () => {
+		const context = {
+			...ctx,
+			packages: [
+				{
+					name: 'studiocms',
+					currentVersion: '1.0.0',
+					targetVersion: '1.0.0',
+				},
+			],
+		};
+		await install(context);
+		expect(ctx.messages.includes('studiocms is up to date on v1.0.0')).toBe(true);
+	});
+
+	it('patch', async () => {
+		const context = {
+			...ctx,
+			packages: [
+				{
+					name: 'studiocms',
+					currentVersion: '1.0.0',
+					targetVersion: '1.0.1',
+				},
+			],
+		};
+		await install(context);
+		expect(ctx.messages.includes('●  studiocms can be updated from v1.0.0 to v1.0.1')).toBe(true);
+	});
+
+	it('minor', async () => {
+		const context = {
+			...ctx,
+			packages: [
+				{
+					name: 'studiocms',
+					currentVersion: '1.0.0',
+					targetVersion: '1.2.0',
+				},
+			],
+		};
+		await install(context);
+		expect(ctx.messages.includes('●  studiocms can be updated from v1.0.0 to v1.2.0')).toBe(true);
+	});
+
+	it('major (accept)', async () => {
+		// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
+		let exitCode;
+		const context = {
+			...ctx,
+			exit: (code) => {
+				exitCode = code;
+			},
+			packages: [
+				{
+					name: 'studiocms',
+					currentVersion: '1.0.0',
+					targetVersion: '2.0.0',
+					isMajor: true,
+					changelogTitle: 'CHANGELOG',
+					changelogURL: 'https://example.com',
+				},
+			],
+		};
+		// @ts-ignore
+		await install(context);
+		expect(ctx.messages.includes('▲  studiocms can be updated  from v1.0.0 to v2.0.0 ')).toBe(true);
+		expect(exitCode).toBeUndefined();
+		expect(ctx.messages.includes('Be sure to follow the CHANGELOG.')).toBe(true);
+	});
+
+	it('multiple major', async () => {
+		// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
+		let exitCode;
+		const context = {
+			...ctx,
+			exit: (code) => {
+				exitCode = code;
+			},
+			packages: [
+				{
+					name: 'a',
+					currentVersion: '1.0.0',
+					targetVersion: '2.0.0',
+					isMajor: true,
+					changelogTitle: 'CHANGELOG',
+					changelogURL: 'https://example.com',
+				},
+				{
+					name: 'b',
+					currentVersion: '6.0.0',
+					targetVersion: '7.0.0',
+					isMajor: true,
+					changelogTitle: 'CHANGELOG',
+					changelogURL: 'https://example.com',
+				},
+			],
+		};
+		// @ts-ignore
+		await install(context);
+		expect(ctx.messages.includes('▲  a can be updated  from v1.0.0 to v2.0.0 ')).toBe(true);
+		expect(ctx.messages.includes('▲  b can be updated  from v6.0.0 to v7.0.0 ')).toBe(true);
+		expect(exitCode).toBeUndefined();
+		const [changelog, a, b] = ctx.messages.slice(-4);
+		expect(changelog).toEqual('Be sure to follow the CHANGELOGs.');
+		expect(a).toMatch('   a https://example.com');
+		expect(b).toMatch('   b https://example.com');
+	});
+
+	it('current patch minor major', async () => {
+		// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
+		let exitCode;
+		const context = {
+			...ctx,
+			exit: (code) => {
+				exitCode = code;
+			},
+			packages: [
+				{
+					name: 'current',
+					currentVersion: '1.0.0',
+					targetVersion: '1.0.0',
+				},
+				{
+					name: 'patch',
+					currentVersion: '1.0.0',
+					targetVersion: '1.0.1',
+				},
+				{
+					name: 'minor',
+					currentVersion: '1.0.0',
+					targetVersion: '1.2.0',
+				},
+				{
+					name: 'major',
+					currentVersion: '1.0.0',
+					targetVersion: '3.0.0',
+					isMajor: true,
+					changelogTitle: 'CHANGELOG',
+					changelogURL: 'https://example.com',
+				},
+			],
+		};
+		// @ts-ignore
+		await install(context);
+		expect(ctx.messages.includes('current is up to date on v1.0.0')).toBe(true);
+		expect(ctx.messages.includes('●  patch can be updated from v1.0.0 to v1.0.1')).toBe(true);
+		expect(ctx.messages.includes('●  minor can be updated from v1.0.0 to v1.2.0')).toBe(true);
+		expect(ctx.messages.includes('▲  major can be updated  from v1.0.0 to v3.0.0 ')).toBe(true);
+		expect(exitCode).toBeUndefined();
+		expect(ctx.messages.includes('Be sure to follow the CHANGELOG.')).toBe(true);
+	});
+});
diff --git a/packages/studiocms_upgrade/tests/verify.test.ts b/packages/studiocms_upgrade/tests/verify.test.ts
new file mode 100644
index 0000000..22a83da
--- /dev/null
+++ b/packages/studiocms_upgrade/tests/verify.test.ts
@@ -0,0 +1,73 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { collectPackageInfo } from '../src/actions/verify';
+
+describe('collectPackageInfo', () => {
+	const context = {
+		version: 'latest',
+		packages: [],
+	};
+
+	beforeEach(() => {
+		context.packages = [];
+	});
+
+	it('detects studiocms', async () => {
+		collectPackageInfo(context, { studiocms: '1.0.0' }, {});
+		expect(context.packages).toEqual([
+			{ name: 'studiocms', currentVersion: '1.0.0', targetVersion: 'latest' },
+		]);
+	});
+
+	it('detects @studiocms', async () => {
+		collectPackageInfo(context, { '@studiocms/blog': '1.0.0' }, {});
+		expect(context.packages).toEqual([
+			{ name: '@studiocms/blog', currentVersion: '1.0.0', targetVersion: 'latest' },
+		]);
+	});
+
+	it('supports ^ prefixes', async () => {
+		collectPackageInfo(context, { studiocms: '^1.0.0' }, {});
+		expect(context.packages).toEqual([
+			{ name: 'studiocms', currentVersion: '^1.0.0', targetVersion: 'latest' },
+		]);
+	});
+
+	it('supports ~ prefixes', async () => {
+		collectPackageInfo(context, { studiocms: '~1.0.0' }, {});
+		expect(context.packages).toEqual([
+			{ name: 'studiocms', currentVersion: '~1.0.0', targetVersion: 'latest' },
+		]);
+	});
+
+	it('supports prereleases', async () => {
+		collectPackageInfo(context, { studiocms: '1.0.0-beta.0' }, {});
+		expect(context.packages).toEqual([
+			{ name: 'studiocms', currentVersion: '1.0.0-beta.0', targetVersion: 'latest' },
+		]);
+	});
+
+	it('ignores self', async () => {
+		collectPackageInfo(context, { '@studiocms/upgrade': '0.0.1' }, {});
+		expect(context.packages).toEqual([]);
+	});
+
+	it('ignores linked packages', async () => {
+		collectPackageInfo(context, { '@studiocms/test': 'link:../packages/test' }, {});
+		expect(context.packages).toEqual([]);
+	});
+
+	it('ignores workspace packages', async () => {
+		collectPackageInfo(context, { '@studiocms/test': 'workspace:*' }, {});
+		expect(context.packages).toEqual([]);
+	});
+
+	it('ignores github packages', async () => {
+		collectPackageInfo(context, { '@studiocms/test': 'github:withstudiocms/studiocms-test' }, {});
+		expect(context.packages).toEqual([]);
+	});
+
+	it('ignores tag', async () => {
+		collectPackageInfo(context, { '@studiocms/test': 'beta' }, {});
+		expect(context.packages).toEqual([]);
+	});
+});
diff --git a/packages/studiocms_upgrade/tsconfig.json b/packages/studiocms_upgrade/tsconfig.json
new file mode 100644
index 0000000..3276a01
--- /dev/null
+++ b/packages/studiocms_upgrade/tsconfig.json
@@ -0,0 +1,13 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "include": ["src"],
+  "compilerOptions": {
+    "allowJs": true,
+    "emitDeclarationOnly": true,
+    "target": "ES2022",
+    "module": "ES2022",
+    "moduleResolution": "Bundler",
+    "outDir": "./dist",
+    "declarationDir": "./dist/types"
+  }
+}
diff --git a/packages/studiocms_upgrade/upgrade.mjs b/packages/studiocms_upgrade/upgrade.mjs
new file mode 100644
index 0000000..03f4f61
--- /dev/null
+++ b/packages/studiocms_upgrade/upgrade.mjs
@@ -0,0 +1,13 @@
+#!/usr/bin/env node
+
+const currentVersion = process.versions.node;
+const requiredMajorVersion = Number.parseInt(currentVersion.split('.')[0], 10);
+const minimumMajorVersion = 18;
+
+if (requiredMajorVersion < minimumMajorVersion) {
+	console.error(`Node.js v${currentVersion} is out of date and unsupported!`);
+	console.error(`Please use Node.js v${minimumMajorVersion} or higher.`);
+	process.exit(1);
+}
+
+import('./dist/index.js').then(({ main }) => main());
diff --git a/packages/studiocms_upgrade/vitest.config.mts b/packages/studiocms_upgrade/vitest.config.mts
new file mode 100644
index 0000000..6802d0d
--- /dev/null
+++ b/packages/studiocms_upgrade/vitest.config.mts
@@ -0,0 +1,11 @@
+import { defineProject } from 'vitest/config';
+
+process.env.NODE_OPTIONS ??= '--enable-source-maps';
+process.setSourceMapsEnabled(true);
+
+export default defineProject({
+	test: {
+		maxConcurrency: 1,
+		name: '@studiocms/upgrade',
+	},
+});
diff --git a/packages/withstudiocms_cli-kit/package.json b/packages/withstudiocms_cli-kit/package.json
index 46ac4f0..c8b3d4f 100644
--- a/packages/withstudiocms_cli-kit/package.json
+++ b/packages/withstudiocms_cli-kit/package.json
@@ -59,7 +59,8 @@
 		"cli-cursor": "^5.0.0",
 		"is-unicode-supported": "^2.1.0",
 		"slice-ansi": "^7.1.0",
-		"tinyexec": "^1.0.1"
+		"tinyexec": "^1.0.1",
+		"package-manager-detector": "^1.1.0"
 	},
 	"devDependencies": {
 		"@types/figlet": "^1.7.0"
diff --git a/packages/withstudiocms_cli-kit/src/utils.ts b/packages/withstudiocms_cli-kit/src/utils.ts
index 2ade821..32f75a5 100644
--- a/packages/withstudiocms_cli-kit/src/utils.ts
+++ b/packages/withstudiocms_cli-kit/src/utils.ts
@@ -14,6 +14,7 @@ import type { Readable } from 'node:stream';
 import { text as textFromStream } from 'node:stream/consumers';
 import { fileURLToPath } from 'node:url';
 import type * as p from '@clack/prompts';
+import { detect } from 'package-manager-detector/detect';
 import { NonZeroExitError, type Options, x } from 'tinyexec';
 
 interface ExecError extends Error {
@@ -270,3 +271,69 @@ export function exitIfEmptyTasks(items: any[], label: string, prompts: typeof p)
 	prompts.log.warn(`No ${label} selected, exiting...`);
 	process.exit(0);
 }
+
+export type ReadFileSyncPathParam = Parameters<typeof fs.readFileSync>[0];
+/**
+ * Read and parse a JSON file.
+ *
+ * @param path - Path to the JSON file
+ * @returns Parsed JSON content with type T
+ * @throws Error if the file doesn't exist or contains invalid JSON
+ */
+export function readJson<T>(path: ReadFileSyncPathParam): T {
+	try {
+		return JSON.parse(fs.readFileSync(path, 'utf-8'));
+	} catch (error) {
+		if (error instanceof Error) {
+			// Better error message including the path
+			throw new Error(`Failed to read or parse JSON file at ${path}: ${error.message}`);
+		}
+		throw new Error(`Failed to read or parse JSON file at ${path}: Unknown Error`);
+	}
+}
+
+// Users might lack access to the global npm registry, this function
+// checks the user's project type and will return the proper npm registry
+//
+// This function is adapted from similar utilities in other projects
+let _registry: string;
+
+/**
+ * Get the npm registry URL based on the user's package manager configuration.
+ *
+ * @returns A Promise that resolves to the registry URL
+ */
+export async function getRegistry(): Promise<string> {
+	if (_registry) return _registry;
+
+	const fallback = 'https://registry.npmjs.org';
+
+	try {
+		const packageManager = (await detect())?.name || 'npm';
+		const { stdout } = await exec(packageManager, ['config', 'get', 'registry']);
+		const output = stdout.trim()?.replace(/\/$/, '');
+
+		if (output && isValidUrl(output)) {
+			_registry = output;
+			return _registry;
+		}
+	} catch (error) {
+		// Log the error or provide debug info but continue with the fallback
+		console.debug(
+			`Failed to get registry from package manager, using fallback: ${error instanceof Error ? error.message : 'Unknown error'}`
+		);
+	}
+
+	_registry = fallback;
+	return fallback;
+}
+
+function isValidUrl(url: string): boolean {
+	try {
+		const parsedUrl = new URL(url);
+		return !!parsedUrl.host && ['http:', 'https:'].includes(parsedUrl.protocol);
+	} catch (e) {
+		if (e instanceof Error) throw new Error('Unable to parse or verify URL', e);
+		throw new Error('Unable to parse or verify URL: Unknown Error');
+	}
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2ce8ceb..37dddd8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -66,6 +66,40 @@ importers:
         specifier: ^10.0.1
         version: 10.0.1
 
+  packages/studiocms_upgrade:
+    dependencies:
+      '@clack/core':
+        specifier: 0.4.2
+        version: 0.4.2
+      '@clack/prompts':
+        specifier: 0.10.1
+        version: 0.10.1
+      '@commander-js/extra-typings':
+        specifier: ^13.1.0
+        version: 13.1.0(commander@13.1.0)
+      '@withstudiocms/cli-kit':
+        specifier: workspace:*
+        version: link:../withstudiocms_cli-kit
+      chalk:
+        specifier: ^5.4.1
+        version: 5.4.1
+      commander:
+        specifier: ^13.1.0
+        version: 13.1.0
+      package-manager-detector:
+        specifier: ^1.1.0
+        version: 1.1.0
+      semver:
+        specifier: ^7.7.1
+        version: 7.7.1
+      terminal-link:
+        specifier: ^4.0.0
+        version: 4.0.0
+    devDependencies:
+      '@types/semver':
+        specifier: ^7.7.0
+        version: 7.7.0
+
   packages/withstudiocms_cli-kit:
     dependencies:
       '@clack/core':
@@ -98,6 +132,9 @@ importers:
       is-unicode-supported:
         specifier: ^2.1.0
         version: 2.1.0
+      package-manager-detector:
+        specifier: ^1.1.0
+        version: 1.1.0
       slice-ansi:
         specifier: ^7.1.0
         version: 7.1.0
@@ -242,6 +279,12 @@ packages:
   '@clack/core@0.4.1':
     resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==}
 
+  '@clack/core@0.4.2':
+    resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==}
+
+  '@clack/prompts@0.10.1':
+    resolution: {integrity: sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==}
+
   '@clack/prompts@0.9.1':
     resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==}
 
@@ -250,6 +293,11 @@ packages:
     peerDependencies:
       commander: ~13.0.0
 
+  '@commander-js/extra-typings@13.1.0':
+    resolution: {integrity: sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==}
+    peerDependencies:
+      commander: ~13.1.0
+
   '@esbuild/aix-ppc64@0.21.5':
     resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
     engines: {node: '>=12'}
@@ -758,6 +806,9 @@ packages:
   '@types/node@22.10.5':
     resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==}
 
+  '@types/semver@7.7.0':
+    resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
+
   '@vitest/expect@3.0.1':
     resolution: {integrity: sha512-oPrXe8dwvQdzUxQFWwibY97/smQ6k8iPVeSf09KEvU1yWzu40G6naHExY0lUgjnTPWMRGQOJnhMBb8lBu48feg==}
 
@@ -911,6 +962,10 @@ packages:
     resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==}
     engines: {node: '>=18'}
 
+  commander@13.1.0:
+    resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
+    engines: {node: '>=18'}
+
   commander@4.1.1:
     resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
     engines: {node: '>= 6'}
@@ -1093,6 +1148,10 @@ packages:
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
 
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
   human-id@4.1.1:
     resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==}
     hasBin: true
@@ -1300,6 +1359,9 @@ packages:
   package-manager-detector@0.2.7:
     resolution: {integrity: sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==}
 
+  package-manager-detector@1.1.0:
+    resolution: {integrity: sha512-Y8f9qUlBzW8qauJjd/eu6jlpJZsuPJm2ZAV0cDVd420o4EdpH5RPdoCv+60/TdJflGatr4sDfpAL6ArWZbM5tA==}
+
   parse-ms@4.0.0:
     resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
     engines: {node: '>=18'}
@@ -1426,6 +1488,11 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
+  semver@7.7.1:
+    resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+    engines: {node: '>=10'}
+    hasBin: true
+
   shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -1515,6 +1582,14 @@ packages:
   summary@2.1.0:
     resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==}
 
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  supports-hyperlinks@3.2.0:
+    resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
+    engines: {node: '>=14.18'}
+
   tapable@2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}
@@ -1527,6 +1602,10 @@ packages:
     resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
     engines: {node: '>=8'}
 
+  terminal-link@4.0.0:
+    resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==}
+    engines: {node: '>=18'}
+
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
 
@@ -1771,7 +1850,7 @@ snapshots:
       outdent: 0.5.0
       prettier: 2.8.8
       resolve-from: 5.0.0
-      semver: 7.6.3
+      semver: 7.7.1
 
   '@changesets/assemble-release-plan@6.0.6':
     dependencies:
@@ -1780,7 +1859,7 @@ snapshots:
       '@changesets/should-skip-package': 0.1.2
       '@changesets/types': 6.1.0
       '@manypkg/get-packages': 1.1.3
-      semver: 7.6.3
+      semver: 7.7.1
 
   '@changesets/changelog-git@0.2.1':
     dependencies:
@@ -1844,7 +1923,7 @@ snapshots:
       '@changesets/types': 6.1.0
       '@manypkg/get-packages': 1.1.3
       picocolors: 1.1.1
-      semver: 7.6.3
+      semver: 7.7.1
 
   '@changesets/get-github-info@0.6.0':
     dependencies:
@@ -1919,6 +1998,17 @@ snapshots:
       picocolors: 1.1.1
       sisteransi: 1.0.5
 
+  '@clack/core@0.4.2':
+    dependencies:
+      picocolors: 1.1.1
+      sisteransi: 1.0.5
+
+  '@clack/prompts@0.10.1':
+    dependencies:
+      '@clack/core': 0.4.2
+      picocolors: 1.1.1
+      sisteransi: 1.0.5
+
   '@clack/prompts@0.9.1':
     dependencies:
       '@clack/core': 0.4.1
@@ -1929,6 +2019,10 @@ snapshots:
     dependencies:
       commander: 13.0.0
 
+  '@commander-js/extra-typings@13.1.0(commander@13.1.0)':
+    dependencies:
+      commander: 13.1.0
+
   '@esbuild/aix-ppc64@0.21.5':
     optional: true
 
@@ -2281,6 +2375,8 @@ snapshots:
     dependencies:
       undici-types: 6.20.0
 
+  '@types/semver@7.7.0': {}
+
   '@vitest/expect@3.0.1':
     dependencies:
       '@vitest/spy': 3.0.1
@@ -2429,6 +2525,8 @@ snapshots:
 
   commander@13.0.0: {}
 
+  commander@13.1.0: {}
+
   commander@4.1.1: {}
 
   confbox@0.1.8: {}
@@ -2636,6 +2734,8 @@ snapshots:
 
   graceful-fs@4.2.11: {}
 
+  has-flag@4.0.0: {}
+
   human-id@4.1.1: {}
 
   iconv-lite@0.4.24:
@@ -2810,6 +2910,8 @@ snapshots:
 
   package-manager-detector@0.2.7: {}
 
+  package-manager-detector@1.1.0: {}
+
   parse-ms@4.0.0: {}
 
   path-exists@4.0.0: {}
@@ -2947,6 +3049,8 @@ snapshots:
 
   semver@7.6.3: {}
 
+  semver@7.7.1: {}
+
   shebang-command@2.0.0:
     dependencies:
       shebang-regex: 3.0.0
@@ -3019,6 +3123,15 @@ snapshots:
 
   summary@2.1.0: {}
 
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  supports-hyperlinks@3.2.0:
+    dependencies:
+      has-flag: 4.0.0
+      supports-color: 7.2.0
+
   tapable@2.2.1: {}
 
   tar@6.2.1:
@@ -3032,6 +3145,11 @@ snapshots:
 
   term-size@2.2.1: {}
 
+  terminal-link@4.0.0:
+    dependencies:
+      ansi-escapes: 7.0.0
+      supports-hyperlinks: 3.2.0
+
   tinybench@2.9.0: {}
 
   tinyexec@0.3.2: {}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (16)</summary><blockquote>

<details>
<summary>.changeset/many-queens-shake.md (1)</summary><blockquote>

`5-5`: **Consider simplifying the wording for clarity.**  
For example, revising to "fix import: corrected `pathUtil` export from default to named export" would be more concise.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~5-~5: ‘by mistake’ might be wordy. Consider a shorter alternative.
Context: ...Util was previously exported as default by mistake, fixed as named export 

(EN_WORDINESS_PREMIUM_BY_MISTAKE)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/upgrade.mjs (1)</summary><blockquote>

`1-13`: **Rename the misleading variable name.**

The variable `requiredMajorVersion` is storing the current Node.js major version, not the required version as the name suggests. This makes the code less readable and could lead to confusion during maintenance.


```diff
-const requiredMajorVersion = Number.parseInt(currentVersion.split('.')[0], 10);
+const currentMajorVersion = Number.parseInt(currentVersion.split('.')[0], 10);

-if (requiredMajorVersion < minimumMajorVersion) {
+if (currentMajorVersion < minimumMajorVersion) {
```

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/README.md (2)</summary><blockquote>

`5-5`: **Fix brand name capitalization**

There's an inconsistency in how the brand name is capitalized. In this line it's written as "StudiOCMS" but elsewhere in the document it's "StudioCMS".

```diff
-You can run this command in your terminal to upgrade your official Astro Project, integrations at the same time as you upgrade StudiOCMS.
+You can run this command in your terminal to upgrade your official Astro Project, integrations at the same time as you upgrade StudioCMS.
```

---

`9-9`: **Improve sentence structure**

The sentence structure could be improved by adding a clear subject.

```diff
-`@studiocms/upgrade` should not be added as a dependency to your project, but run as a temporary executable whenever you want to upgrade using [`npx`](https://docs.npmjs.com/cli/v10/commands/npx) or [`dlx`](https://pnpm.io/cli/dlx).
+`@studiocms/upgrade` should not be added as a dependency to your project, but instead it should be run as a temporary executable whenever you want to upgrade using [`npx`](https://docs.npmjs.com/cli/v10/commands/npx) or [`dlx`](https://pnpm.io/cli/dlx).
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~9-~9: To form a complete sentence, be sure to include a subject.
Context: ...diOCMS.  ## Usage  `@studiocms/upgrade` should not be added as a dependency to your pr...

(MISSING_IT_THERE)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/tests/context.test.ts (1)</summary><blockquote>

`5-20`: **Consider extending test coverage**

While the current tests verify the key parameters, you might consider adding assertions for other important properties returned by getContext (like packageManager or cwd) to ensure they're properly initialized.

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/utils/messages.ts (1)</summary><blockquote>

`15-23`: **Consider reducing slight redundancy in the done messages.**

There's minor duplication in the messaging, with lines 19 and 21 being very similar:
- "Everything is current." 
- "Everything is on the latest and greatest."

Consider consolidating or creating more distinct variations to improve message diversity.

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/package.json (1)</summary><blockquote>

`33-35`: **Consider adding more comprehensive dev dependencies.**

The package has minimal dev dependencies. Consider adding testing libraries, linting tools, and type checking to ensure code quality.

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (1)</summary><blockquote>

`29-53`: **Consider adding error handling for package manager detection.**

The getContext function uses package-manager-detector with the appropriate strategies, but it could benefit from more explicit error handling if detection fails. Currently, it defaults to npm without logging warnings.


```diff
export async function getContext(
	version = 'latest',
	flags?: {
		dryRun?: true | undefined;
		color?: boolean | undefined;
	}
): Promise<Context> {
-	const packageManager = (await detect({
-		// Include the `install-metadata` strategy to have the package manager that's
-		// used for installation take precedence
-		strategies: ['install-metadata', 'lockfile', 'packageManager-field'],
-	})) ?? { agent: 'npm', name: 'npm' };
+	let packageManager;
+	try {
+		packageManager = await detect({
+			// Include the `install-metadata` strategy to have the package manager that's
+			// used for installation take precedence
+			strategies: ['install-metadata', 'lockfile', 'packageManager-field'],
+		});
+	} catch (error) {
+		console.warn('Failed to detect package manager:', error instanceof Error ? error.message : String(error));
+	}
+	packageManager = packageManager ?? { agent: 'npm', name: 'npm' };
	return {
		prompt,
		packageManager,
		packages: [],
		cwd: new URL(`${pathToFileURL(process.cwd())}/`),
		dryRun: flags?.dryRun || false,
		version,
		exit(code) {
			process.exit(code);
		},
		tasks: [],
	};
}
```

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/tests/install.test.ts (1)</summary><blockquote>

`6-39`: **Consider re-initializing `ctx.messages` per test.**  
Currently, all tests share the same `ctx.messages` array. This could introduce cross-test contamination if any test relies on a clean state for message assertions. Adding a `beforeEach` block or otherwise resetting the array in each test can prevent side effects.

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/index.ts (1)</summary><blockquote>

`15-16`: **Validate the `package.json` reference.**  
Using a relative path to `../package.json` might break if the folder structure changes. As an optional safeguard, verify that the file is readable or consider a more robust resolution.

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/actions/install.ts (2)</summary><blockquote>

`12-73`: **Upgrade workflow is logically sound.**  
- Separates already up-to-date packages and logs them, which is user-friendly.  
- Prompts for major updates, allowing users to cancel before making breaking changes.  
- As an optional enhancement, consider whether partial acceptance of major upgrades (per package) is desired.

---

`152-212`: **Dependable installation process with a sensible fallback.**  
- Leveraging tasks for installation blends well with CLI workflows.  
- Consider capturing the install output if debugging is needed for large or complex installs.

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/actions/verify.ts (4)</summary><blockquote>

`11-36`: **Consider more robust connectivity checks.**

The `verify` function currently checks connectivity by relying on a DNS lookup. In some restricted network environments or when DNS might still resolve but actual HTTP requests could fail, this check can be insufficient. You might consider performing a minimal fetch against the registry endpoint to conclusively verify internet connectivity for your upgrade logic.

---

`38-44`: **Broaden connectivity checks for more reliability.**

`isOnline` solely checks DNS resolution, which may pass even if outbound requests to the registry are blocked. Consider refining this function to attempt a quick TCP or HTTP connection check against the registry.

---

`53-66`: **Optionally validate dependency versions.**

`verifyStudioCMSProject` only checks for the presence of `studiocms` in dependencies. If you need stricter validation (e.g., ensuring a minimum version), consider verifying that `dependencies.studiocms` is a valid semver within an acceptable range.

---

`137-191`: **Add defensive JSON parsing for registry responses.**

`resolveTargetVersion` throws if `packageMetadata.status >= 400`, but does not catch potential JSON parse errors if the registry response is malformed. Consider try/catch around `await packageMetadata.json()` for more resilience.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 98bf55e9a25bff175159dca54da6e185b404083d and 1bdf1c701ec4858559d740c29c6da40d1431c724.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (22)</summary>

* `.changeset/lazy-radios-decide.md` (1 hunks)
* `.changeset/many-queens-shake.md` (1 hunks)
* `.changeset/nice-webs-joke.md` (1 hunks)
* `package.json` (1 hunks)
* `packages/create-studiocms/src/index.ts` (1 hunks)
* `packages/create-studiocms/tests/fixtures/not-empty/git.json` (1 hunks)
* `packages/studiocms_upgrade/LICENSE` (1 hunks)
* `packages/studiocms_upgrade/README.md` (1 hunks)
* `packages/studiocms_upgrade/package.json` (1 hunks)
* `packages/studiocms_upgrade/src/actions/install.ts` (1 hunks)
* `packages/studiocms_upgrade/src/actions/verify.ts` (1 hunks)
* `packages/studiocms_upgrade/src/index.ts` (1 hunks)
* `packages/studiocms_upgrade/src/utils/context.ts` (1 hunks)
* `packages/studiocms_upgrade/src/utils/messages.ts` (1 hunks)
* `packages/studiocms_upgrade/tests/context.test.ts` (1 hunks)
* `packages/studiocms_upgrade/tests/install.test.ts` (1 hunks)
* `packages/studiocms_upgrade/tests/verify.test.ts` (1 hunks)
* `packages/studiocms_upgrade/tsconfig.json` (1 hunks)
* `packages/studiocms_upgrade/upgrade.mjs` (1 hunks)
* `packages/studiocms_upgrade/vitest.config.mts` (1 hunks)
* `packages/withstudiocms_cli-kit/package.json` (1 hunks)
* `packages/withstudiocms_cli-kit/src/utils.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>packages/studiocms_upgrade/tests/context.test.ts (1)</summary><blockquote>

<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (1)</summary>

* `getContext` (29-53)

</details>

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/actions/verify.ts (2)</summary><blockquote>

<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (2)</summary>

* `Context` (5-16)
* `PackageInfo` (18-27)

</details>
<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary>

* `getRegistry` (284-302)

</details>

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/index.ts (7)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary>

* `readJson` (275-277)

</details>
<details>
<summary>packages/withstudiocms_cli-kit/src/messages.ts (5)</summary>

* `CLITitle` (281-281)
* `termPrefix` (270-270)
* `label` (248-249)
* `say` (177-237)
* `random` (243-246)

</details>
<details>
<summary>packages/withstudiocms_cli-kit/src/colors.ts (2)</summary>

* `StudioCMSColorwayError` (11-11)
* `StudioCMSColorwayBg` (6-6)

</details>
<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (1)</summary>

* `getContext` (29-53)

</details>
<details>
<summary>packages/studiocms_upgrade/src/actions/verify.ts (1)</summary>

* `verify` (11-36)

</details>
<details>
<summary>packages/studiocms_upgrade/src/actions/install.ts (1)</summary>

* `install` (12-73)

</details>
<details>
<summary>packages/studiocms_upgrade/src/utils/messages.ts (3)</summary>

* `celebrations` (1-13)
* `done` (15-23)
* `bye` (25-30)

</details>

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/tests/verify.test.ts (1)</summary><blockquote>

<details>
<summary>packages/studiocms_upgrade/src/actions/verify.ts (1)</summary>

* `collectPackageInfo` (92-114)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>.changeset/many-queens-shake.md</summary>

[style] ~5-~5: ‘by mistake’ might be wordy. Consider a shorter alternative.
Context: ...Util was previously exported as default by mistake, fixed as named export 

(EN_WORDINESS_PREMIUM_BY_MISTAKE)

</details>
<details>
<summary>packages/studiocms_upgrade/README.md</summary>

[style] ~9-~9: To form a complete sentence, be sure to include a subject.
Context: ...diOCMS.  ## Usage  `@studiocms/upgrade` should not be added as a dependency to your pr...

(MISSING_IT_THERE)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (47)</summary><blockquote>

<details>
<summary>packages/create-studiocms/tests/fixtures/not-empty/git.json (1)</summary>

`1-1`: **Formatting adjustment noted.**  
This file now correctly contains an empty JSON object with an added newline for improved formatting consistency.

</details>
<details>
<summary>.changeset/nice-webs-joke.md (1)</summary>

`1-6`: **Initial changeset file looks clear.**  
The file appropriately documents the minor version update for "@studiocms/upgrade" with a succinct release note.

</details>
<details>
<summary>packages/studiocms_upgrade/vitest.config.mts (2)</summary>

`3-5`: **Environment configuration is well-structured.**  
The use of the nullish coalescing operator (`??=`) for setting `NODE_OPTIONS` and enabling source maps is a clear and effective approach.

---

`6-11`: **Project configuration block is clear.**  
The configuration object defined via `defineProject` correctly sets parameters like `maxConcurrency` and the project `name`, ensuring the tests run as intended.

</details>
<details>
<summary>packages/studiocms_upgrade/LICENSE (1)</summary>

`1-22`: **MIT License is correctly added.**  
The license text is complete and adheres to the standard MIT License terms, ensuring proper legal coverage for the package.

</details>
<details>
<summary>.changeset/lazy-radios-decide.md (1)</summary>

`1-6`: **LGTM! The changeset file correctly documents the changes.**

This changeset file properly documents the patch update to `@withstudiocms/cli-kit` and describes the two new utility functions being added (`readJson` and `getRegistry`).

</details>
<details>
<summary>package.json (1)</summary>

`10-10`: **LGTM! Build script correctly updated to include new package namespace.**

The build script is appropriately modified to include packages with the `@studiocms/*` namespace, which is necessary for building the newly added upgrade package.

</details>
<details>
<summary>packages/studiocms_upgrade/upgrade.mjs (1)</summary>

`13-13`: **LGTM! Dynamic import is correctly implemented.**

The dynamic import of the main function from the compiled JavaScript is well-implemented and follows best practices for ES modules.

</details>
<details>
<summary>packages/studiocms_upgrade/tsconfig.json (1)</summary>

`9-13`: **LGTM! Appropriate module configuration for modern Node.js.**

The TypeScript configuration uses appropriate settings for modern Node.js applications with ES2022 target and module resolution.

</details>
<details>
<summary>packages/studiocms_upgrade/README.md (1)</summary>

`1-51`: **Well-structured README with clear instructions**

The README provides comprehensive and clear documentation for the new upgrade tool. The instructions for different package managers and explanation of the optional tag parameter are helpful for users.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~9-~9: To form a complete sentence, be sure to include a subject.
Context: ...diOCMS.  ## Usage  `@studiocms/upgrade` should not be added as a dependency to your pr...

(MISSING_IT_THERE)

</details>

</details>

</details>
<details>
<summary>packages/withstudiocms_cli-kit/package.json (1)</summary>

`62-63`: <details>
<summary>❓ Verification inconclusive</summary>

**New dependency looks appropriate**

The addition of `package-manager-detector` as a dependency is appropriate for a CLI toolkit that needs to detect which package manager a user is using.


---

🌐 Web query:

```web
What is the current version, popularity, and maintenance status of the package-manager-detector npm package?
```

💡 Result:

The `package-manager-detector` npm package is a utility for detecting a project's package manager (e.g., npm, pnpm, yarn, bun) based on lock files or the `packageManager` field in `package.json`. Here's its current status:

---

### **Current Version**
- **Latest version**: `0.2.9` (published 18 days ago as of April 12, 2025) [1].

---

### **Popularity**
- The package is not among the most downloaded npm packages (e.g., React, TypeScript, or utility libraries like `lodash` [10]).
- It serves a niche use case for developers working with multiple package managers, so its weekly download count is likely moderate. Specific download statistics aren't provided in the search results, but tools like `pkg-stats` (mentioned in [8]) could be used to retrieve this data.

---

### **Maintenance Status**
- **Active development**: The package receives updates, with the latest release in March 2025 [1].
- **Features**: Includes detection logic for lock files, CLI command resolution for package managers, and compatibility with npm, yarn, pnpm, and bun [1].
- **License**: MIT, maintained by Anthony Fu [1].
- **No deprecation warnings**: The package isn’t flagged as deprecated in the npm registry or linked to archived repositories [7].

---

For real-time download stats or deeper maintenance insights, use tools like `pkg-stats` [8] or monitor the npm registry directly [3].

Citations:

- 1: https://www.npmjs.com/package/package-manager-detector/v/0.2.1
- 2: https://www.usenix.org/conference/usenixsecurity24/presentation/huang-cheng
- 3: https://statusgator.com/services/npm
- 4: https://npm-compare.com/depcheck,npm-check
- 5: https://github.com/raineorshine/npm-check-updates
- 6: http://nodesource.com/blog/nodejs-package-manager-comparative-guide-2024/
- 7: https://www.aquasec.com/blog/deceptive-deprecation-the-truth-about-npm-deprecated-packages/
- 8: https://dev.to/ajmal_hasan/quickly-check-npm-package-popularity-with-ease-13j8
- 9: https://www.warp.dev/terminus/npm-check-package-version
- 10: https://socket.dev/blog/2023-npm-retrospective

---



</details>

**Potential Version Discrepancy in the New Dependency**

The addition of `package-manager-detector` is well-motivated for detecting the user's package manager in a CLI toolkit. However, please verify the version specified in `package.json` (`^1.1.0`) because the current npm details indicate the latest published version is `0.2.9`. Confirm whether this is an intentional choice (e.g., a fork or custom branch) or if the dependency version needs to align with the official release.

- If the higher version was intended, please document the rationale.
- Otherwise, adjust to reflect the maintained version (`0.2.9`) to prevent potential issues with unexpected behavior.

</details>
<details>
<summary>packages/studiocms_upgrade/tests/context.test.ts (1)</summary>

`1-21`: **Good test coverage for the getContext function**

The tests cover the essential functionality of the getContext function, testing the default behavior and both optional parameters (version tag and dryRun flag).

</details>
<details>
<summary>packages/create-studiocms/src/index.ts (1)</summary>

`4-4`: **Correctly updated import statement to match export changes**

The change from default import to named import for `pathUtil` aligns with the updated export style in the `@withstudiocms/cli-kit` package. This ensures compatibility and consistency across the codebase.

</details>
<details>
<summary>packages/studiocms_upgrade/src/utils/messages.ts (2)</summary>

`1-13`: **Clean and well-structured message array.**

The celebrations array provides a good variety of positive affirmation messages that will enhance the user experience in the CLI tool.

---

`25-30`: **Good closing messages that reinforce the brand.**

The farewell messages effectively reinforce the StudioCMS brand while providing a friendly closing to the CLI interaction.

</details>
<details>
<summary>packages/studiocms_upgrade/package.json (4)</summary>

`1-13`: **LGTM: Package metadata is properly configured.**

The package metadata contains all necessary information including name, version, license, and repository details.

---

`14-16`: **Build configuration uses buildkit appropriately.**

The build script correctly uses buildkit to bundle the source file.

---

`17-22`: **Package exports are properly configured.**

The exports field correctly specifies the main entry point, which matches the main and bin fields.

---

`36-41`: **Node.js engine requirements are properly specified.**

The Node.js version requirements are well-defined and the publishConfig enables provenance, which is a good security practice.

</details>
<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary>

`279-302`: **Well-implemented registry detection with proper fallbacks.**

The `getRegistry` function is well-written with:
- Good memoization pattern to avoid redundant lookups
- Proper fallback to the default registry when needed
- Validation of the registry URL to ensure it's a valid HTTP(S) URL
- Clear comments explaining the purpose and implementation details

This implementation follows best practices for resilient network-dependent utilities.

</details>
<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (2)</summary>

`5-16`: **Well-defined Context interface with comprehensive properties.**

The Context interface clearly defines all the necessary properties for context management throughout the application, providing good type safety.

---

`18-27`: **Comprehensive PackageInfo interface.**

The PackageInfo interface includes all necessary fields for package management, with appropriate optional properties for flexibility.

</details>
<details>
<summary>packages/studiocms_upgrade/tests/install.test.ts (7)</summary>

`1-5`: **Imports look good.**  
No issues found with the libraries or references used.

---

`41-55`: **"Up to date" scenario is well-structured.**  
Verifies that the correct message is logged when the package is already at the target version.

---

`56-70`: **"Patch" scenario test is straightforward.**  
Ensures the correct message is logged for a minor patch upgrade.

---

`71-85`: **"Minor" scenario test is clear.**  
Checks that the expected log message appears for a minor version update.

---

`86-111`: **"Major (accept)" scenario test handles version bumps well.**  
Covers messaging, user acceptance, and ensures the exit code remains undefined if the user proceeds.

---

`112-149`: **"Multiple major" scenario is thorough.**  
Correctly validates messages for each major upgrade in a multi-package environment.

---

`150-193`: **Combined scenario test is comprehensive.**  
Covers current, patch, minor, and major updates in one go, checking the resulting logs and exit behavior.

</details>
<details>
<summary>packages/studiocms_upgrade/src/index.ts (3)</summary>

`1-5`: **Imports are consistent with usage.**  
All imported modules are relevant and properly utilized.

---

`6-14`: **Export statements are well-organized.**  
Consolidating exports in the index promotes clarity and discoverability.

---

`17-56`: **CLI command definition is clear and comprehensive.**  
- The `.description()`, `.version()`, and `.addHelpText()` usage is well structured.  
- The `.action()` method seamlessly coordinates the `verify` and `install` steps, concluding with a concluding message and immediate process exit.

</details>
<details>
<summary>packages/studiocms_upgrade/src/actions/install.ts (5)</summary>

`1-11`: **Import statements are relevant and straightforward.**  
Core Node modules, third-party packages, and local utilities are neatly organized.

---

`75-90`: **Package filtering is concise.**  
Cleanly categorizes packages into up-to-date, regular dependencies, and dev dependencies based on target vs. current version checks.

---

`96-136`: **Helper functions are simple and effective.**  
- `sortPackages` ensures major updates are highlighted.  
- `success` and `upgrade` produce clear, nicely formatted messages.  
- `pluralize` is a neat solution for grammar handling.

---

`138-150`: **`changelog` function provides a helpful link output.**  
Underlined link generation or fallback to plain text is user-friendly.

---

`222-226`: **Empty `yarn.lock` creation ensures Yarn Berry compatibility.**  
A practical solution to avoid install failures in Yarn PnP environments when no lockfile exists.

</details>
<details>
<summary>packages/studiocms_upgrade/src/actions/verify.ts (9)</summary>

`1-10`: **Imports look correct.**

All dependencies and type imports appear valid and necessary.

---

`46-51`: **Safe JSON parsing logic is straightforward.**

The fallback to an empty object is reasonable and helps avoid runtime errors.

---

`68-75`: **Package detection logic for 'studiocms' and 'astro' looks acceptable.**

These checks handle multiple naming patterns.

---

`77-79`: **Ignoring the upgrade package itself helps prevent circular updates.**

The function ensures that `@studiocms/upgrade` is not processed, preventing self-inclusion.

---

`81-83`: **Valid version checks are concise.**

Coercing versions to semver is a suitable approach for varied version strings.

---

`85-90`: **Supported package checks look correct.**

Combining multiple validator functions is a clean approach.

---

`92-114`: **Dependency collection logic is clear.**

`collectPackageInfo` properly filters out unsupported packages and populates `ctx.packages`.

---

`116-135`: **Version verification approach is sound.**

Spawning parallel checks (“tasks”) and gathering them with `Promise.all()` is efficient.

---

`193-199`: **Changelog URL extraction is well-structured.**

The function neatly composes a GitHub URL for the prospective changelog location.

</details>
<details>
<summary>packages/studiocms_upgrade/tests/verify.test.ts (1)</summary>

`4-73`: **Comprehensive tests with varied scenarios.**

The test suite thoroughly covers normal usage with different version prefixes and prereleases, as well as ignoring self/package references, linked packages, workspace references, GitHub references, and direct tag references. Overall, these tests ensure robust verification of `collectPackageInfo`. Consider adding a negative test case for invalid version strings or incomplete package data to further solidify coverage.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T12:16:23Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T12:31:00Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (2)</summary><blockquote>

`275-287`: **Great implementation of the readJson function with error handling.**

The function implementation looks good with proper error handling that includes clear error messages with the file path. This addresses the previous review comment perfectly.



Consider adding a JSDoc comment block to maintain consistency with other utility functions in this file:

```diff
+/**
+ * Read and parse a JSON file.
+ * 
+ * @param path - Path to the JSON file
+ * @returns Parsed JSON content with type T
+ * @throws Error if the file doesn't exist or contains invalid JSON
+ */
export function readJson<T>(path: ReadFileSyncPathParam): T {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci - Knip Lint</summary>

[warning]  Unused item in ignoreDependencies: 'commander'.

</details>

</details>

---

`289-312`: **Well-implemented registry detection with fallbacks.**

The `getRegistry` function has good error handling, caching, and fallback mechanisms. The URL validation helps prevent invalid registry URLs from being returned.



Consider these minor improvements:

1. Add a JSDoc comment block for consistency with other functions:

```diff
+/**
+ * Get the npm registry URL based on the user's package manager configuration.
+ * 
+ * @returns A Promise that resolves to the registry URL
+ */
export async function getRegistry(): Promise<string> {
```

2. Make the URL validation more concise using a single try/catch:

```diff
-		_registry = stdout.trim()?.replace(/\/$/, '') || fallback;
-		// Detect cases where the shell command returned a non-URL (e.g. a warning)
-		try {
-			const url = new URL(_registry);
-			if (!url.host || !['http:', 'https:'].includes(url.protocol)) _registry = fallback;
-		} catch {
-			_registry = fallback;
-		}
+		const registry = stdout.trim()?.replace(/\/$/, '') || fallback;
+		try {
+			const url = new URL(registry);
+			_registry = url.host && ['http:', 'https:'].includes(url.protocol) ? registry : fallback;
+		} catch {
+			_registry = fallback;
+		}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci - Knip Lint</summary>

[warning]  Unused item in ignoreDependencies: 'commander'.

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1bdf1c701ec4858559d740c29c6da40d1431c724 and fa3401985568363618b1424f6bc516ddc34f7cc4.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `packages/studiocms_upgrade/package.json` (1 hunks)
* `packages/studiocms_upgrade/tsconfig.json` (1 hunks)
* `packages/withstudiocms_cli-kit/src/utils.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* packages/studiocms_upgrade/tsconfig.json
* packages/studiocms_upgrade/package.json

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Actions: ci - Knip Lint</summary>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts</summary>

[warning]  Unused item in ignoreDependencies: 'commander'.

</details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T12:32:54Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary><blockquote>

`275-293`: **Good implementation of the readJson function with proper error handling.**

The function now includes robust error handling that provides informative error messages, including the path to the file that failed to parse. This addresses the feedback from the previous review.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary><blockquote>

`295-324`: **Consider adding logging in the empty catch block.**

While the function correctly falls back to the default registry when an error occurs, it might be helpful to log a warning or debug message about the failure to get the registry from the package manager.


```diff
-	} catch {}
+	} catch (error) {
+		// Log the error or provide debug info but continue with the fallback
+		console.debug(`Failed to get registry from package manager, using fallback: ${error instanceof Error ? error.message : 'Unknown error'}`);
+	}
```

</blockquote></details>
<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (4)</summary><blockquote>

`5-16`: **Consider providing TSDoc comments for clarity.**  
Adding TSDoc annotations for the `Context` interface could help document what each property represents and how it should be used, enhancing readability and maintainability.

---

`18-27`: **Consider stricter typing for version strings.**  
If you plan to rely on semantic versioning, consider using a specialized parser (e.g., `semver`) to ensure valid version strings and to facilitate version comparisons.

---

`29-35`: **Consider extracting a dedicated type for flags.**  
If these flags are reused across the codebase, creating a named type or interface might improve reusability and provide a single source of truth for all CLI-related flags.

---

`49-61`: **Remove the unused “color” flag or implement support for it.**  
As written, the `color` flag isn’t utilized in the returned context. If color handling isn’t planned, remove the flag to keep the code clean. Also note that calling `process.exit()` in `exit(code)` forcibly stops the process, which may skip pending asynchronous tasks. Confirm if that is intended behavior.


Here’s a small diff to illustrate removing the unused `color` flag:

```diff
29 export async function getContext(
30   version = 'latest',
31   flags?: {
32-    dryRun?: true | undefined;
33-    color?: boolean | undefined;
34+    dryRun?: true | undefined;
35   }
):
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fa3401985568363618b1424f6bc516ddc34f7cc4 and a1f78ae5b69a44471e44ca1b9476e33b735ecad0.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `knip.config.ts` (1 hunks)
* `packages/studiocms_upgrade/src/utils/context.ts` (1 hunks)
* `packages/withstudiocms_cli-kit/src/utils.ts` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (2)</summary>

`17-17`: **Good addition of the package-manager-detector dependency.**

This import supports the new `getRegistry` function, allowing for automatic detection of the user's package manager.

---

`326-333`: **Good URL validation implementation.**

The `isValidUrl` function provides a proper validation check for the registry URL, ensuring it has a host and uses either HTTP or HTTPS protocol, which is a good security practice.

</details>
<details>
<summary>knip.config.ts (1)</summary>

`22-22`: **Appropriate addition to ignoreDependencies.**

Adding `@commander-js/extra-typings` to the ignored dependencies is appropriate if this package provides TypeScript type definitions for Commander.js that are used in the project but not directly imported in the code files.

</details>
<details>
<summary>packages/studiocms_upgrade/src/utils/context.ts (2)</summary>

`1-4`: **Imports look good.**  
No concerns at this point; these imports are well-structured and serve the file’s purposes.

---

`36-48`: **Request confirmation about package manager detection fallback.**  
Currently, an error in detection simply logs a warning and defaults to npm. Verify whether this behavior is desired or if a more explicit error flow or rethrow should be considered for transparency.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T12:44:25Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary><blockquote>

`276-293`: **Error handling implemented correctly.**

The readJson function now properly handles errors when reading or parsing JSON files, with informative error messages that include the file path. This implementation addresses the previous review comment.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (1)</summary><blockquote>

`295-329`: **Registry resolution logic is robust.**

The getRegistry function implements a good caching strategy and properly handles fallback to the default registry. The error handling allows the function to continue even if the package manager detection fails.


Consider replacing the console.debug with a more structured logging approach consistent with the rest of the codebase.

```diff
-		console.debug(
-			`Failed to get registry from package manager, using fallback: ${error instanceof Error ? error.message : 'Unknown error'}`
-		);
+		// Use a structured logger if available, or define a default logging mechanism
+		// For example:
+		// logger.debug(`Failed to get registry from package manager, using fallback: ${error instanceof Error ? error.message : 'Unknown error'}`);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a1f78ae5b69a44471e44ca1b9476e33b735ecad0 and 5c5c07e3a1e13fb3ee0a71e6a89975043c9fc120.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/studiocms_upgrade/src/utils/context.ts` (1 hunks)
* `packages/withstudiocms_cli-kit/src/utils.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/studiocms_upgrade/src/utils/context.ts

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (3)</summary>

`17-17`: **Import statement looks good.**

The import for package-manager-detector's detect function is properly placed and will be used by the new getRegistry function.

---

`275-275`: **Well-defined TypeScript type.**

Good use of the `Parameters<typeof ...>[0]` pattern to extract the exact parameter type from `fs.readFileSync`. This makes the code more maintainable when Node.js types change.

---

`331-338`: **URL validation is correct.**

The isValidUrl helper function properly validates URLs by checking for both a valid host and protocol.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T12:47:25Z
user: login: dreyfus92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T14:11:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (2)</summary><blockquote>

`306-329`: **The getRegistry implementation looks good but could use caching improvement.**

The function correctly detects the package manager and retrieves the registry URL with a proper fallback. The caching mechanism using `_registry` is a good performance optimization.

However, there's a small improvement opportunity in how caching is handled:


```diff
export async function getRegistry(): Promise<string> {
-	if (_registry) return _registry;
+	// Checking for either truthy value or explicit string to avoid returning 
+	// when _registry is accidentally set to empty string
+	if (typeof _registry === 'string') return _registry;

	const fallback = 'https://registry.npmjs.org';
```

---

`283-293`: **Consider handling empty or whitespace-only JSON files.**

The current implementation will throw a `SyntaxError` with a generic message when parsing an empty file. You could enhance the error handling to provide a more specific message for this common edge case.


```diff
export function readJson<T>(path: ReadFileSyncPathParam): T {
	try {
-		return JSON.parse(fs.readFileSync(path, 'utf-8'));
+		const content = fs.readFileSync(path, 'utf-8');
+		if (!content.trim()) {
+			throw new Error(`Empty JSON file at ${path}`);
+		}
+		return JSON.parse(content);
	} catch (error) {
		if (error instanceof Error) {
			// Better error message including the path
			throw new Error(`Failed to read or parse JSON file at ${path}: ${error.message}`);
		}
		throw new Error(`Failed to read or parse JSON file at ${path}: Unknown Error`);
	}
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 030c73d71cd97552b97eb4732a4718db8a7477f7 and ed724ec479a4d17baf54f4d6bdfce9455e1c3768.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/withstudiocms_cli-kit/src/utils.ts` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/withstudiocms_cli-kit/src/utils.ts (2)</summary>

`17-17`: **Import of the package-manager-detector looks good.**

The addition of this dependency will support the new `getRegistry` function for dynamically determining the appropriate npm registry.

---

`275-293`: **Well-implemented readJson function with proper error handling.**

The function is nicely typed, well-documented, and handles errors appropriately as suggested in the previous review. The error messages are informative and include the file path for better debugging.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T20:39:13Z
user: login: dreyfus92
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-13T00:46:35Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T00:49:54Z

Review comments:
diff_hunk: @@ -0,0 +1,14 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "include": ["src"],
+  "compilerOptions": {
+    "allowJs": true,
+    "emitDeclarationOnly": false,
+    "noEmit": true,
+    "target": "ES2022",
path: packages/studiocms_upgrade/tsconfig.json
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Resolve conflicting TypeScript compiler options.**

There's a conflict in the compiler options: `noEmit: true` will prevent the compiler from emitting any files, which contradicts the `emitDeclarationOnly: false` setting and would prevent the creation of JavaScript files needed by `upgrade.mjs`.


Choose one of these approaches:

```diff
-    "emitDeclarationOnly": false,
-    "noEmit": true,
+    "noEmit": false,
```

or if you're using another build tool like esbuild or rollup to emit JavaScript:

```diff
-    "emitDeclarationOnly": false,
-    "noEmit": true,
+    "emitDeclarationOnly": true,
```


<!-- 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
    "noEmit": false,
    "target": "ES2022",
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-12T12:16:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+{
+	"name": "@studiocms/upgrade",
+	"version": "0.0.1",
+	"type": "module",
+	"author": "withstudiocms",
+	"license": "MIT",
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/withstudiocms/cli-utilities.git",
+		"directory": "packages/studiocms_upgrade"
+	},
+	"bugs": "https://github.com/withstudiocms/cli-utilities/issues",
+	"homepage": "https://studiocms.dev",
+	"scripts": {
+		"build": "buildkit build \"src/index.ts\" --bundle"
+	},
+	"exports": {
+	  ".": "./upgrade.mjs"
+	},
+	"main": "./upgrade.mjs",
+	"bin": "./upgrade.mjs",
+	"files": ["dist"],
+	"dependencies": {
+		"@clack/core": "0.4.1",
+		"@clack/prompts": "0.9.1",
+		"@withstudiocms/cli-kit": "workspace:*",
+		"commander": "^13.0.0",
+		"chalk": "^5.4.1",
+		"package-manager-detector": "^1.1.0",
+		"semver": "^7.7.1",
+		"terminal-link": "^4.0.0"
+	},
path: packages/studiocms_upgrade/package.json
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the latest versions of critical dependencies.**

While the dependencies are properly specified, it would be good to verify that the latest stable versions are being used, especially for security-critical packages.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for the latest versions of critical dependencies
echo "Checking latest versions of dependencies..."
npm show @clack/core version
npm show @clack/prompts version
npm show commander version
npm show chalk version
npm show semver version
```

Length of output: 273

---



</details>

**Action Required: Update dependency versions for security-critical packages.**

After running the version check, please note the following updates needed in **packages/studiocms_upgrade/package.json** (lines 23–32):

- **@clack/core**: Currently at "0.4.1" but the latest version is "0.4.2".
- **@clack/prompts**: Currently at "0.9.1" but the latest version is "0.10.1".

The remaining dependencies are already at their latest stable versions. Please update the versions for **@clack/core** and **@clack/prompts** to ensure optimal security and stability.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-12T12:16:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,3 +271,32 @@ export function exitIfEmptyTasks(items: any[], label: string, prompts: typeof p)
 	prompts.log.warn(`No ${label} selected, exiting...`);
 	process.exit(0);
 }
+
+export function readJson<T>(path: string | URL): T {
+	return JSON.parse(fs.readFileSync(path, 'utf-8'));
+}
path: packages/withstudiocms_cli-kit/src/utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to the readJson function.**

The current implementation will throw uncaught exceptions if the file doesn't exist or contains invalid JSON. Consider adding proper error handling with informative error messages.


```diff
-export function readJson<T>(path: string | URL): T {
-	return JSON.parse(fs.readFileSync(path, 'utf-8'));
+export function readJson<T>(path: string | URL): T {
+	try {
+		return JSON.parse(fs.readFileSync(path, 'utf-8'));
+	} catch (error) {
+		if (error instanceof Error) {
+			// Better error message including the path
+			throw new Error(`Failed to read or parse JSON file at ${path}: ${error.message}`);
+		}
+		throw error;
+	}
}
```


<!-- 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
export function readJson<T>(path: string | URL): T {
	try {
		return JSON.parse(fs.readFileSync(path, 'utf-8'));
	} catch (error) {
		if (error instanceof Error) {
			// Better error message including the path
			throw new Error(`Failed to read or parse JSON file at ${path}: ${error.message}`);
		}
		throw error;
	}
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-12T12:16:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,3 +271,68 @@ export function exitIfEmptyTasks(items: any[], label: string, prompts: typeof p)
 	prompts.log.warn(`No ${label} selected, exiting...`);
 	process.exit(0);
 }
+
+export type ReadFileSyncPathParam = Parameters<typeof fs.readFileSync>[0];
+/**
+ * Read and parse a JSON file.
+ *
+ * @param path - Path to the JSON file
+ * @returns Parsed JSON content with type T
+ * @throws Error if the file doesn't exist or contains invalid JSON
+ */
+export function readJson<T>(path: ReadFileSyncPathParam): T {
+	try {
+		return JSON.parse(fs.readFileSync(path, 'utf-8'));
+	} catch (error) {
+		if (error instanceof Error) {
+			// Better error message including the path
+			throw new Error(`Failed to read or parse JSON file at ${path}: ${error.message}`);
+		}
+		throw new Error(`Failed to read or parse JSON file at ${path}: Unknown Error`);
+	}
+}
+
+// Users might lack access to the global npm registry, this function
+// checks the user's project type and will return the proper npm registry
+//
+// This function is adapted from similar utilities in other projects
+let _registry: string;
+
+/**
+ * Get the npm registry URL based on the user's package manager configuration.
+ *
+ * @returns A Promise that resolves to the registry URL
+ */
+export async function getRegistry(): Promise<string> {
+	if (_registry) return _registry;
+
+	const fallback = 'https://registry.npmjs.org';
+
+	try {
+		const packageManager = (await detect())?.name || 'npm';
+		const { stdout } = await exec(packageManager, ['config', 'get', 'registry']);
+		const output = stdout.trim()?.replace(/\/$/, '');
+
+		if (output && isValidUrl(output)) {
+			_registry = output;
+			return _registry;
+		}
+	} catch (error) {
+		// Log the error or provide debug info but continue with the fallback
+		console.debug(
+			`Failed to get registry from package manager, using fallback: ${error instanceof Error ? error.message : 'Unknown error'}`
+		);
+	}
+
+	_registry = fallback;
+	return fallback;
+}
+
+function isValidUrl(url: string): boolean {
+	try {
+		const parsedUrl = new URL(url);
+		return !!parsedUrl.host && ['http:', 'https:'].includes(parsedUrl.protocol);
+	} catch {
+		return false;
+	}
+}
path: packages/withstudiocms_cli-kit/src/utils.ts
user: login: dreyfus92
body: if you're using a `try/catch` block i'd suggest throwing errors for better error handling.
created_at: 2025-04-12T14:11:41Z
author_association: MEMBER
diff_hunk: @@ -270,3 +271,69 @@ export function exitIfEmptyTasks(items: any[], label: string, prompts: typeof p)
 	prompts.log.warn(`No ${label} selected, exiting...`);
 	process.exit(0);
 }
+
+export type ReadFileSyncPathParam = Parameters<typeof fs.readFileSync>[0];
+/**
+ * Read and parse a JSON file.
+ *
+ * @param path - Path to the JSON file
+ * @returns Parsed JSON content with type T
+ * @throws Error if the file doesn't exist or contains invalid JSON
+ */
+export function readJson<T>(path: ReadFileSyncPathParam): T {
+	try {
+		return JSON.parse(fs.readFileSync(path, 'utf-8'));
+	} catch (error) {
+		if (error instanceof Error) {
+			// Better error message including the path
+			throw new Error(`Failed to read or parse JSON file at ${path}: ${error.message}`);
+		}
+		throw new Error(`Failed to read or parse JSON file at ${path}: Unknown Error`);
+	}
+}
+
+// Users might lack access to the global npm registry, this function
+// checks the user's project type and will return the proper npm registry
+//
+// This function is adapted from similar utilities in other projects
+let _registry: string;
+
+/**
+ * Get the npm registry URL based on the user's package manager configuration.
+ *
+ * @returns A Promise that resolves to the registry URL
+ */
+export async function getRegistry(): Promise<string> {
+	if (_registry) return _registry;
+
+	const fallback = 'https://registry.npmjs.org';
+
+	try {
+		const packageManager = (await detect())?.name || 'npm';
+		const { stdout } = await exec(packageManager, ['config', 'get', 'registry']);
+		const output = stdout.trim()?.replace(/\/$/, '');
+
+		if (output && isValidUrl(output)) {
+			_registry = output;
+			return _registry;
+		}
+	} catch (error) {
+		// Log the error or provide debug info but continue with the fallback
+		console.debug(
+			`Failed to get registry from package manager, using fallback: ${error instanceof Error ? error.message : 'Unknown error'}`
+		);
+	}
+
+	_registry = fallback;
+	return fallback;
+}
+
+function isValidUrl(url: string): boolean {
+	try {
+		const parsedUrl = new URL(url);
+		return !!parsedUrl.host && ['http:', 'https:'].includes(parsedUrl.protocol);
+	} catch (e) {
+		if (e instanceof Error) throw new Error('Unable to parse or verify URL', e);
+		throw new Error('Unable to parse or verify URL: Unknown Error');
+	}
+}
path: packages/withstudiocms_cli-kit/src/utils.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix the isValidUrl function to match its return type.**

The function declares a boolean return type but can throw errors instead of returning false for invalid URLs. This contradicts the function's contract.


```diff
function isValidUrl(url: string): boolean {
	try {
		const parsedUrl = new URL(url);
		return !!parsedUrl.host && ['http:', 'https:'].includes(parsedUrl.protocol);
	} catch (e) {
-		if (e instanceof Error) throw new Error('Unable to parse or verify URL', e);
-		throw new Error('Unable to parse or verify URL: Unknown Error');
+		return false;
	}
}
```


<!-- 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
function isValidUrl(url: string): boolean {
	try {
		const parsedUrl = new URL(url);
		return !!parsedUrl.host && ['http:', 'https:'].includes(parsedUrl.protocol);
	} catch (e) {
		return false;
	}
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-12T20:39:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-12T08:52:44Z
body: ###  🦋  Changeset detected

Latest commit: ed724ec479a4d17baf54f4d6bdfce9455e1c3768

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 3 packages</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| @withstudiocms/cli-kit | Patch |
| create-studiocms       | Patch |
| @studiocms/upgrade     | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/withstudiocms/cli-utilities/new/upgrade-util?filename=.changeset/nine-turkeys-tease.md&value=---%0A%22create-studiocms%22%3A%20patch%0A%22%40studiocms%2Fupgrade%22%3A%20patch%0A%22%40withstudiocms%2Fcli-kit%22%3A%20patch%0A---%0A%0AAdd%20new%20upgrade%20package%0A)


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-12T08:52:47Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This pull request introduces new utility functions (`readJson` and `getRegistry`) and corrects the export of the `pathUtil` module. It adds a new CLI upgrade tool package (`@studiocms/upgrade`) with its own license, documentation, configuration, actions (install and verify), and tests. The build process is updated in the main `package.json`, and additional dependencies are added to the CLI kit. Several new test suites and configuration files are also introduced, ensuring the new functionalities are properly validated and integrated into the codebase.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `.changeset/lazy-radios-decide.md`, `.changeset/many-queens-shake.md`, `.changeset/nice-webs-joke.md` | Document new utilities, fix default export of `pathUtil`, and note initial release of `@studiocms/upgrade`. |
| `package.json`, `packages/create-studiocms/src/index.ts` | Update build script to include filter `@studiocms/*` and correct import of `pathUtil` from default to named export. |
| `packages/create-studiocms/tests/fixtures/not-empty/git.json` | Add newline at the end of file. |
| `packages/studiocms_upgrade/LICENSE`, `packages/studiocms_upgrade/README.md`, `packages/studiocms_upgrade/package.json`, `packages/studiocms_upgrade/src/actions/install.ts`, `packages/studiocms_upgrade/src/actions/verify.ts`, `packages/studiocms_upgrade/src/index.ts`, `packages/studiocms_upgrade/src/utils/context.ts`, `packages/studiocms_upgrade/src/utils/messages.ts`, `packages/studiocms_upgrade/tsconfig.json`, `packages/studiocms_upgrade/upgrade.mjs`, `packages/studiocms_upgrade/vitest.config.mts` | Introduce a new CLI upgrade tool package with its core actions (install and verify), contextual utilities, messaging, TypeScript and Vitest configurations, license, and documentation. |
| `packages/studiocms_upgrade/tests/context.test.ts`, `packages/studiocms_upgrade/tests/install.test.ts`, `packages/studiocms_upgrade/tests/verify.test.ts` | Add comprehensive test suites for context initialization, installation process, and package verification routines. |
| `packages/withstudiocms_cli-kit/package.json`, `packages/withstudiocms_cli-kit/src/utils.ts` | Add dependency `"package-manager-detector": "^1.1.0"` and introduce the new utility functions `readJson` and `getRegistry` in the CLI kit. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as User
    participant NS as Node Script (upgrade.mjs)
    participant CLI as CLI (main in index.ts)
    participant CTX as Context (getContext)
    participant V as Verify (verify function)
    participant I as Install (install function)
    participant TR as Task Runner

    U ->> NS: Execute upgrade.mjs
    NS ->> NS: Check Node version (>=18)
    NS -->> CLI: Import main function from dist/index.js
    CLI ->> CTX: Call getContext()
    CTX -->> CLI: Return context
    CLI ->> U: Display welcome message
    CLI ->> V: Call verify(context)
    V -->> CLI: Verification results
    CLI ->> I: Call install(context)
    I -->> CLI: Installation process complete
    CLI ->> TR: Execute additional tasks (if any)
    TR -->> CLI: Task results
    CLI ->> U: Display completion message and exit
```

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=withstudiocms/cli-utilities&utm_content=19):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: pkg-pr-new[bot]
created_at: 2025-04-12T08:53:12Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/c2221dab-a32b-43cd-89f8-2e8a3fd1510b)


```
pnpm dlx https://pkg.pr.new/withstudiocms/cli-utilities/create-studiocms@19
```


```
pnpm dlx https://pkg.pr.new/withstudiocms/cli-utilities/@studiocms/upgrade@19
```


```
pnpm dlx https://pkg.pr.new/withstudiocms/cli-utilities/@withstudiocms/cli-kit@19
```


_commit: <a href="https://github.com/withstudiocms/cli-utilities/runs/40448341699"><code>ed724ec</code></a>_

author_association: NONE
user: login: studiocms-no-reply
created_at: 2025-04-12T08:53:30Z
body: # PR Test Results
Vitest Test Results: https://withstudiocms.github.io/cli-utilities/pr-preview/vitest/pr-19/
<!-- Sticky Pull Request Commentpr-preview -->
author_association: MEMBER