**Build a component library that looks like your product, with design rules your coding agents can use.**
-CodeRocket UI combines **48 components and 26 interface blocks for both React and Vue**, a typed design-system model, and CLI/MCP integrations. Start from accessible primitives, customize shared tokens, and keep editable source in your application. The core is **MIT licensed**, written in **TypeScript**, and works **with or without Tailwind**.
-
-[Try the visual builder](https://ui.coderocket.app) · [React documentation](https://ui.coderocket.app/docs/components) · [Vue documentation](https://ui.coderocket.app/docs/vue/components) · [Request Pro access](https://ui.coderocket.app/contact?intent=pro)
+CodeRocket UI combines React and Vue components, interface blocks, a typed design-system model, and CLI/MCP integrations. Start from accessible primitives, customize shared tokens, and keep editable source in your application. The open-source core is **MIT licensed**, written in **TypeScript**, and works **with or without Tailwind**.
## Two frameworks, one design system
@@ -12,28 +32,57 @@ React uses Base UI. Vue uses native Vue single-file components and Reka UI. Both
Use the components directly from this source workspace, or use the [hosted Studio](https://ui.coderocket.app/studio) to customize a library visually, review AI proposals and export its source. An account or AI provider is **not required** to use the open-source components locally. Svelte and SolidJS are planned, without announced release dates.
+## Start with your own library
+
+1. Open [Studio](https://ui.coderocket.app/studio) and create a React or Vue library.
+2. Customize its tokens, components and blocks. AI assistance is optional.
+3. Save, then **Export** a ZIP or use **Connect** to install the source through the CLI.
+
+Exported components run locally without a CodeRocket connection. Follow the [React installation guide](https://ui.coderocket.app/docs/export) or [Vue and Nuxt guide](https://ui.coderocket.app/docs/vue) for dependencies and styles.
+
+### Install and update source with the CLI
+
+The CLI is available on npm as [`@coderocketapp/cli`](https://www.npmjs.com/package/@coderocketapp/cli). In Studio, open your saved library's **Connect** dialog and create a scoped connection token. Set `CODEROCKET_LIBRARY` and `CODEROCKET_TOKEN` in your environment, then run these commands from your application directory:
+
+```sh
+npx @coderocketapp/cli@latest init
+npx @coderocketapp/cli@latest list
+npx @coderocketapp/cli@latest add button
+npx @coderocketapp/cli@latest sync
+```
+
+The library selects React or Vue source automatically. `sync` preserves local changes and puts conflicting updates aside for review. Keep connection tokens out of version control. See the [CLI guide](packages/cli/README.md) for blocks, complete-library installation and local import analysis.
+
+### Give coding agents the same design rules
+
+[`@coderocketapp/mcp`](https://www.npmjs.com/package/@coderocketapp/mcp) exposes a saved library's design rules, components and blocks through a read-only MCP server. Follow the [MCP setup guide](packages/mcp/README.md) to connect a compatible coding agent.
+
+The CLI and MCP need no AI provider key. Agents can also use the [public Markdown documentation](docs/content), typed specifications and exported `AGENTS.md`. Review generated source before applying it to your application.
+
## What is open source?
| Package | What it provides |
| --------------------------------------- | -------------------------------------------------------------------------------------------- |
-| [`@coderocket/react`](packages/react) | 48 React components, Base UI primitives, compiled CSS and a composition renderer. |
-| [`@coderocket/blocks`](packages/blocks) | 26 React interface blocks with typed application callbacks. |
-| [`@coderocket/vue`](packages/vue) | 48 Vue components and 26 Vue blocks, Reka UI primitives, CSS and a composition renderer. |
+| [`@coderocket/react`](packages/react) | React components, Base UI primitives, compiled CSS and a composition renderer. |
+| [`@coderocket/blocks`](packages/blocks) | React interface blocks with typed application callbacks. |
+| [`@coderocket/vue`](packages/vue) | Vue components and blocks, Reka UI primitives, CSS and a composition renderer. |
| [`@coderocket/engine`](packages/engine) | Versioned design-system model, token validation, theme generation and reviewed token import. |
| [`@coderocket/specs`](packages/specs) | Shared catalogue specifications and validated composition schemas. |
| [`@coderocket/shared`](packages/shared) | Types and a read-only registry client for the integration tools. |
-| [`@coderocket/cli`](packages/cli) | Local import analysis and installation/sync from a saved library, preserving local changes. |
-| [`@coderocket/mcp`](packages/mcp) | A read-only MCP server exposing a saved library's rules and component sources. |
+| [`@coderocketapp/cli`](packages/cli) | Local import analysis and installation/sync from a saved library, preserving local changes. |
+| [`@coderocketapp/mcp`](packages/mcp) | A read-only MCP server exposing a saved library's rules and component sources. |
+
+**CLI and MCP are distributed on npm.** The component and model packages remain source workspaces: build them locally, or use Studio exports and the CLI to install editable component source. The `@coderocket/react` and `@coderocket/vue` workspace names do not imply npm availability.
The [documentation source](docs/content) is public too. It is rendered at [ui.coderocket.app/docs](https://ui.coderocket.app/docs) by the separately maintained website.
The hosted Studio, accounts, database, AI-provider integration and commercial operations remain private. Authentication, payments, email delivery and uploads are not bundled component backends: blocks expose callbacks for your own application. The CLI's registry commands and MCP need a saved library and scoped connection token, or a compatible registry server.
-The catalogue is experimental. Review accessibility, behavior and framework integration in your application's context before release.
+The catalogue is experimental during early access. Review accessibility, behavior and framework integration in your application's context before release.
## Build from source
-Use Node.js 24+ and pnpm 12.4.2. **This is a source distribution**: workspace package names do not imply a public npm release.
+Use the Node.js and pnpm versions declared in [`package.json`](package.json):
```sh
git clone https://github.com/elreco/coderocket-ui.git
@@ -44,7 +93,7 @@ pnpm check
The checks validate and build the workspace and run its tests without contacting the Studio or an AI provider. The legacy datepicker is outside this pnpm workspace.
-### React
+### React workspace consumer
Add `@coderocket/react` as a `workspace:*` dependency in a pnpm workspace consumer. Import its compiled styles once:
@@ -61,7 +110,7 @@ export function App() {
}
```
-### Vue
+### Vue workspace consumer
Add `@coderocket/vue` as a `workspace:*` dependency in a Vue workspace consumer:
@@ -76,25 +125,19 @@ import "@coderocket/vue/styles.css";
```
-See [development and integration](docs/DEVELOPMENT.md) for consumer dependencies, styles and framework differences. React 19.3 / Base UI 1.8 and Vue 3.5 / Reka UI 2.10 are the versions used by this source release.
-
-### Coding agents, CLI and MCP
-
-After building, run `node packages/cli/dist/index.mjs --help`. Follow the [CLI guide](packages/cli/README.md) for installation and sync, or the [MCP guide](packages/mcp/README.md) to expose a saved library to a compatible coding agent. The library's framework determines the returned source. No Gemini or other AI provider key is required by these clients.
-
-[Markdown documentation](docs/content), typed specifications and a read-only MCP interface help agents use the same components and design rules as developers. Generated source still needs review before application.
+See [development and integration](docs/DEVELOPMENT.md) for consumer dependencies, styles and framework differences.
## From Vue Tailwind Datepicker
This is the same repository, with its stars, issues and Git history. CodeRocket UI is its active successor; it is **not a drop-in replacement** for the old datepicker API.
-**Vue Tailwind Datepicker is frozen and no longer maintained.** Its npm package, `@coderocketapp/vue-tailwind-datepicker`, remains available. Its source, original MIT attribution, changelog and documentation are preserved in [`legacy/vue-tailwind-datepicker`](legacy/vue-tailwind-datepicker). Existing applications can continue using their installed version; no automatic migration is required. No further legacy fixes or releases are planned.
+**Vue Tailwind Datepicker is frozen and no longer maintained.** Its npm package, [`@coderocketapp/vue-tailwind-datepicker`](https://www.npmjs.com/package/@coderocketapp/vue-tailwind-datepicker), remains available. Its source, original MIT attribution, changelog and documentation are preserved in [`legacy/vue-tailwind-datepicker`](legacy/vue-tailwind-datepicker). Existing applications can continue using their installed version; no automatic migration is required. No further legacy fixes or releases are planned.
For new work, explore the [Vue DatePicker](https://ui.coderocket.app/docs/vue/components/date-picker) or [React DatePicker](https://ui.coderocket.app/docs/components/date-picker). Read the [transition announcement](ANNOUNCEMENT.md) and [legacy documentation](https://vue-tailwind-datepicker.com) before migrating.
## Hosted access and Pro
-The hosted editor, component catalogue and source export are free. Signed-in accounts receive **5 trial AI generations in total**. Pro adds **100 AI generations per UTC calendar month** during an agreed paid period, subject to the shared service limit. Price, payment and activation are arranged personally; there is no automatic billing or renewal. Manual editing and export remain usable without AI allowance.
+The hosted editor, component catalogue and source export are free. AI assistance has usage limits; see [current access and allowances](https://ui.coderocket.app/docs/pilot). Pro price, payment and activation are arranged personally, with no automatic billing or renewal. Manual editing and export remain usable without AI allowance.
[Request Pro or integration help](https://ui.coderocket.app/contact?intent=pro), or read [how access works](https://ui.coderocket.app/docs/pilot). Submitting a request does not charge you or activate paid access. GitHub stars and historical datepicker use are not permission for sales outreach.
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index b667880..292fb61 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -4,6 +4,6 @@ The active CodeRocket UI packages and public documentation are licensed under th
The historical Vue Tailwind Datepicker source keeps its original [MIT license and Kenhyuwa attribution](legacy/vue-tailwind-datepicker/LICENSE). Moving that source does not remove or replace its license. Its original dependency manifests and lockfiles are preserved alongside it.
-Third-party dependencies retain their own licenses. The active source uses React, React DOM, Base UI, Vue, Reka UI, Lucide, Zod and the Model Context Protocol SDK, among others. Dependency implementations are obtained through the package manager and are not relicensed by this repository. The public library build keeps runtime dependencies external.
+Third-party dependencies retain their own licenses. The active source uses React, React DOM, Base UI, Vue, Reka UI, Lucide, Zod and the Model Context Protocol SDK, among others. Dependency implementations are obtained through the package manager and are not relicensed by this repository. The public library build keeps runtime dependencies external. The npm CLI and MCP clients bundle their runtime dependencies; each archive includes their full license texts in `THIRD_PARTY_NOTICES.md`.
Documentation examples and interface blocks do not provide hosted authentication, billing or storage services. The hosted CodeRocket UI application is maintained separately and is not licensed by this repository's MIT license.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 6c522f1..b11d1c8 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -2,7 +2,7 @@
## Build and test
-Use Node.js 24+ and pnpm 12.4.2:
+Use the Node.js and pnpm versions declared in [`package.json`](../package.json):
```sh
pnpm install --frozen-lockfile
@@ -13,7 +13,7 @@ pnpm check
The build emits ES modules, TypeScript declarations and CSS. React entries retain the client boundary for frameworks such as Next.js. Vue single-file components are compiled for consumers. Runtime dependencies remain external. Tests cover date values, model validation, composition, framework behavior and the CLI's filesystem protection.
-These are source workspaces, not an announcement of npm availability. Add a consumer inside this pnpm workspace with `workspace:*` dependencies, or build and package the required dependencies locally. The hosted Studio also exports standalone source you can copy into an existing application.
+The component and model packages are source workspaces. Add a consumer inside this pnpm workspace with `workspace:*` dependencies, or build and package the required dependencies locally. The hosted Studio also exports standalone source you can copy into an existing application. The integration clients are distributed separately on npm as `@coderocketapp/cli` and `@coderocketapp/mcp`; see their [CLI](../packages/cli/README.md) and [MCP](../packages/mcp/README.md) guides.
## A local React consumer
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
new file mode 100644
index 0000000..53c0acb
--- /dev/null
+++ b/docs/RELEASING.md
@@ -0,0 +1,46 @@
+# Releasing CLI and MCP clients
+
+The public npm packages are `@coderocketapp/cli` and `@coderocketapp/mcp`.
+The internal `@coderocket/*` manifests stay private to prevent accidentally
+publishing source workspace dependencies. `pnpm clients:pack` builds standalone
+public packages into `.release/`, with metadata, executable code and licenses only.
+
+## Validate a release
+
+1. Update `version` in both `packages/cli/package.json` and `packages/mcp/package.json`
+ to the same next version. Keep the Studio copies in sync.
+2. Update the client READMEs for changed commands. Installation examples use
+ `@latest`; badges read their versions from npm.
+3. Run `pnpm check`. This includes installation of both real archives in a clean
+ temporary application, CLI installation/sync conflict checks and an MCP session.
+4. Review and merge the release into `main`.
+5. Tag that commit `clients-vX.Y.Z`, using the version from the manifests, and push
+ that tag. The `Publish npm clients` workflow checks the tag, rebuilds, tests and
+ publishes both packages with provenance through npm trusted publishing.
+
+Publication is versioned independently from the legacy datepicker and does not
+publish the hosted Studio or its server code. A retry verifies the integrity of any
+already published version and skips it only when its bytes match exactly.
+
+## First publication and npm authentication
+
+Each package requires an npm trusted publisher for `elreco/coderocket-ui`, workflow
+`publish-clients.yml`, with direct publishing allowed. No long-lived npm token is
+stored in GitHub. Creating this relationship requires npm account authentication.
+New package names must be published once before configuring their trusted publisher.
+
+After `pnpm clients:pack && pnpm clients:check`, a maintainer with npm write access
+can publish the exact archives listed in `.release/manifest.json` using
+`npm publish .release/ --access public --ignore-scripts` and complete npm's
+interactive authentication. Then configure each package:
+
+```sh
+npm trust github @coderocketapp/cli --repo elreco/coderocket-ui --file publish-clients.yml --allow-publish
+npm trust github @coderocketapp/mcp --repo elreco/coderocket-ui --file publish-clients.yml --allow-publish
+```
+
+Verify package versions and install from npm in a fresh application before updating
+production installation instructions. Future releases use the tag workflow.
+Published versions are immutable: fix an issue in a new version instead of deleting
+an existing release. The CLI's old quality-gate release remains accessible by its
+original version; new releases provide CodeRocket UI library installation.
diff --git a/docs/assets/brand/coderocket-mark.svg b/docs/assets/brand/coderocket-mark.svg
new file mode 100644
index 0000000..5671d62
--- /dev/null
+++ b/docs/assets/brand/coderocket-mark.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/content/agents.mdx b/docs/content/agents.mdx
index bbff94a..7b264f5 100644
--- a/docs/content/agents.mdx
+++ b/docs/content/agents.mdx
@@ -5,30 +5,49 @@ description: Connect your project and give your coding agent the same components
You can work entirely from an exported ZIP. Use a connection when you want your terminal or coding agent to read the latest saved version of a library.
+The [MIT-licensed component source](https://github.com/elreco/coderocket-ui) is available without a CodeRocket account. The hosted registry used by the CLI and MCP requires an account, a saved library and a connection token. Installed source runs locally without a connection.
+
## Create a connection
1. Save the library in the editor, then open **Connect**.
2. Name the connection for the machine or agent that will use it.
-3. Choose **Create read-only connection** and copy the token shown once.
+3. Choose **Create connection** and copy the token shown once.
4. Copy the generated setup commands or MCP configuration into your development environment.
A connection can read only its library. It expires after 90 days and can be revoked from the same dialog. Put its token in your terminal or agent's protected environment; keep it out of source files, prompts and public URLs.
## Install with the CLI
-The **Connect** dialog provides the CLI archive URL and complete commands for your library. Its commands use `npm exec --package -- coderocket …`, so a global installation is unnecessary. The table below abbreviates that prefix; use the full command from **Connect** and replace only its final arguments.
+With **Node.js 24 or newer** installed, run the official npm package from your application folder. Copy your library ID from **Connect** and provide the token in your terminal environment:
+
+In bash or zsh, paste your connection token at the hidden prompt and press Enter:
+
+```bash
+printf "Connection token: "
+read -rs CODEROCKET_TOKEN
+export CODEROCKET_TOKEN
+printf "\n"
+npx @coderocketapp/cli@latest init YOUR_LIBRARY_ID
+npx @coderocketapp/cli@latest add button dialog
+```
+
+The CLI remembers the library in `.coderocket/manifest.json`; it never writes the token to project files. Keep `CODEROCKET_TOKEN` available for later commands, including in new terminals. You can instead set `CODEROCKET_LIBRARY` and run `npx @coderocketapp/cli@latest init` without a positional ID. No global installation is required.
-| Command | Result |
-| ------------------------------ | ---------------------------------------------------------- |
-| `coderocket init` | Connect the current project to a saved library |
-| `coderocket add button dialog` | Install the named components and required shared files |
-| `coderocket add block-login` | Install a complete block |
-| `coderocket add --all` | Install the full catalogue |
-| `coderocket sync` | Update unchanged local files from the latest saved version |
-| `coderocket import` | Write a local analysis report for importing project tokens |
+| Command | Result |
+| ------------------------------------------------- | ---------------------------------------------------------- |
+| `npx @coderocketapp/cli@latest list` | List available components and blocks |
+| `npx @coderocketapp/cli@latest add button dialog` | Install named components and their required shared files |
+| `npx @coderocketapp/cli@latest add block-login` | Install a complete block |
+| `npx @coderocketapp/cli@latest add --all` | Install the full catalogue |
+| `npx @coderocketapp/cli@latest sync` | Update installed items from the latest saved version |
+| `npx @coderocketapp/cli@latest import` | Write a local analysis report for importing project tokens |
Install the listed runtime dependencies and import the generated styles once. See [export and installation](/docs/export) for React/Next.js, or [Vue and Nuxt setup](/docs/vue). The connection reads the framework from your saved library and serves the matching source and dependencies.
+Commit `.coderocket/manifest.json` and `.coderocket/lock.json` with the installed source. The CLI writes integration instructions and agent rules to `.coderocket/README.md` and `.coderocket/AGENTS.md`. The `import` command analyzes local files without uploading them and does not require a token.
+
+`CODEROCKET_SERVER` defaults to `https://ui.coderocket.app`. If you use a different Studio deployment, set its origin in the environment before `init` and keep it set for subsequent commands. **Connect** includes that variable when needed.
+
### Your local edits stay yours
The CLI compares file hashes before applying an update. If it finds a local edit or deletion, it stops the installation and writes the proposed changes into a `.coderocket/review-*` folder. Review and merge those changes in your codebase; the CLI does not silently replace your work.
@@ -39,7 +58,7 @@ Save changes in the editor before syncing. A connection reads saved versions, no
Vue libraries use the CodeRocket CLI or MCP above. The instructions below target React projects.
-Open **Connect → Use the shadcn registry** and copy the registry entry into your existing `components.json`. The configuration uses an authorization header from your environment to keep the library private.
+Open **Connect → shadcn registry** and copy the registry entry into your existing `components.json`. The configuration uses an authorization header from your environment to keep the library private.
You can then install a component through your configured registry:
@@ -51,7 +70,24 @@ The local-change protection described above belongs to the CodeRocket CLI. When
## Connect an agent with MCP
-Open **Connect → Connect a coding agent with MCP**. Copy the configuration to a compatible MCP client, storing the token in its protected environment settings.
+Open **Connect → Coding agents · MCP** to copy the configuration for your saved library. Use Node.js 24 or newer and an MCP client that supports local stdio servers:
+
+```json
+{
+ "mcpServers": {
+ "coderocket": {
+ "command": "npx",
+ "args": ["-y", "@coderocketapp/mcp@latest"],
+ "env": {
+ "CODEROCKET_LIBRARY": "YOUR_LIBRARY_ID",
+ "CODEROCKET_TOKEN": "YOUR_CONNECTION_TOKEN"
+ }
+ }
+ }
+}
+```
+
+Replace the placeholders in your client's private configuration and keep it outside version control. If the client has protected secret settings, provide `CODEROCKET_TOKEN` there and remove its entry from the JSON. Restart or reconnect the MCP server after configuration. The token is passed to the server as an environment variable.
The connector gives your agent read access to:
@@ -62,6 +98,8 @@ The connector gives your agent read access to:
For example, you can ask an agent to “build a settings form using this library's input, select and button components.” The agent has the source and design context it needs to reuse your library. Design changes still happen in the editor, where you review and save them.
+The MCP server reads the library; your agent uses its own tools and permissions to edit your application. If the token expires or is revoked, create a new connection for the same library, update the environment and reconnect.
+
## Include your design rules
Every source export includes `AGENTS.md` with the library's conventions. It asks agents to reuse existing controls, consume semantic tokens, and preserve keyboard and focus behavior.
diff --git a/docs/content/ai.mdx b/docs/content/ai.mdx
index 6c70c76..16414b0 100644
--- a/docs/content/ai.mdx
+++ b/docs/content/ai.mdx
@@ -42,3 +42,11 @@ The schema, tree and labels are checked before the preview. The editor then runs
Accepted compositions include their structured specification, editable React or Vue source, an SSR smoke test and a Storybook story in the ZIP export. Connect the exported stories to your application's Storybook theme decorator. Run the tests and accessibility checks in that application before promoting an experimental component to production.
The current generator assembles validated primitives. It does not execute arbitrary generated JavaScript or invent application backends. Unsupported behavior requires clarification and implementation in your own action callbacks.
+
+## Use your library with a coding agent
+
+To give your coding agent the saved design rules and catalogue, open **Connect → Coding agents · MCP**. The official npm server runs with `npx -y @coderocketapp/mcp@latest` and requires Node.js 24 or newer. Set `CODEROCKET_LIBRARY` and `CODEROCKET_TOKEN` in the MCP client's environment, using the library-scoped connection created in the Studio. Keep tokens out of prompts and source files. [Copy the complete MCP configuration](/docs/agents#connect-an-agent-with-mcp).
+
+The MCP server provides read-only access to the saved library. Your agent can fetch source and use its own tools to integrate it into your app. Save Studio changes before requesting an updated design. To install and sync from a terminal, use `npx @coderocketapp/cli@latest init YOUR_LIBRARY_ID` with the token in your environment, then `add` or `sync` as shown in the [CLI guide](/docs/agents#install-with-the-cli).
+
+Hosted connections require an account and a saved library. The public documentation and [MIT-licensed component source](https://github.com/elreco/coderocket-ui) can be used with an assistant without an account or connection token.
diff --git a/docs/content/export.mdx b/docs/content/export.mdx
index 8f1b434..85acc4e 100644
--- a/docs/content/export.mdx
+++ b/docs/content/export.mdx
@@ -7,6 +7,25 @@ Your export contains the components, blocks and theme from your saved library. T
Vue libraries export native `.vue` components, Vue-specific CLI/MCP rules and the same theme files. Follow [Vue and Nuxt installation](/docs/vue). The React setup below applies to React libraries.
+## Install through npm
+
+For ongoing updates, use the official CLI with Node.js 24 or newer. Save your library and create a token in **Connect**, then run these commands from your application folder with the library ID shown there:
+
+In bash or zsh, paste your connection token at the hidden prompt and press Enter:
+
+```bash
+printf "Connection token: "
+read -rs CODEROCKET_TOKEN
+export CODEROCKET_TOKEN
+printf "\n"
+npx @coderocketapp/cli@latest init YOUR_LIBRARY_ID
+npx @coderocketapp/cli@latest add button dialog
+```
+
+The CLI writes source and styles into your application and prints the runtime dependencies to install. Use `npx @coderocketapp/cli@latest list` to browse items and `npx @coderocketapp/cli@latest sync` after saving design changes. Keep the token in your environment. See [CLI and coding agents](/docs/agents) for local-edit protection, blocks and MCP setup.
+
+Hosted registry access needs an account, a saved library and its token. The [MIT-licensed component source](https://github.com/elreco/coderocket-ui) can be used without an account. The ZIP workflow below is also available for a saved Studio library.
+
## Download
Save your library and select **Export**. The archive includes:
diff --git a/docs/content/getting-started.mdx b/docs/content/getting-started.mdx
index f1a151e..7729a4e 100644
--- a/docs/content/getting-started.mdx
+++ b/docs/content/getting-started.mdx
@@ -7,6 +7,8 @@ For Vue, follow the [Vue setup guide](/docs/vue); the Studio workflow and AI too
This walkthrough uses a React library called **Acme UI**. You can complete the design manually; AI is optional.
+You can use the [MIT-licensed component source](https://github.com/elreco/coderocket-ui) without an account. This walkthrough uses the hosted Studio to save a customized library and install it through its private registry, which requires an account and a library connection token.
+
## 1. Create your library
[Create an account](/signup), confirm your email address, then open the editor. Your libraries are private to your account.
@@ -42,15 +44,24 @@ Select **Save** to store the current design. **Version history** lets you restor
If another session has changed the library, saving reports a conflict. Reload and inspect the newer version before continuing.
-## 5. Export and render a component
+## 5. Install and render a component
+
+Save any pending changes, then open **Connect** and choose **Create connection**. Copy the token shown once. With Node.js 24 or newer installed, run the official CLI from your application's folder using the library ID shown in **Connect**:
-Save any pending changes, then choose **Export** and download the ZIP. Copy its `components/` and `styles/` folders into your React app, and install the runtime dependencies:
+In bash or zsh, paste your connection token at the hidden prompt and press Enter:
```bash
-npm install react@19.3.0 react-dom@19.3.0 @base-ui/react@1.8.0 lucide-react@1.47.0
+printf "Connection token: "
+read -rs CODEROCKET_TOKEN
+export CODEROCKET_TOKEN
+printf "\n"
+npx @coderocketapp/cli@latest init YOUR_LIBRARY_ID
+npx @coderocketapp/cli@latest add button
```
-React and React DOM must use matching versions. Calendar and DatePicker require React 19.2 or newer; the current library is tested with 19.3. Lucide is used by the calendar, datepicker and blocks.
+Keep your token in the terminal environment, outside source and version control. The CLI remembers the library ID in `.coderocket/manifest.json` and prints the required runtime dependencies; install those in your app. React and React DOM must use matching versions. See [CLI and coding agents](/docs/agents) for configuration and token renewal.
+
+You can also choose **Export** and copy the ZIP's `components/` and `styles/` folders into your app. Follow [export and installation](/docs/export) for that setup.
Import the compiled styles once, then use the source directly:
@@ -71,6 +82,16 @@ export default function App() {
Your button uses the saved Acme UI theme. It renders from local source and CSS. Connect its `onClick` callback to your application when you add behavior.
+Keep `CODEROCKET_TOKEN` set when using the CLI again. List available items, add more components, or save a new design in the Studio and sync it:
+
+```bash
+npx @coderocketapp/cli@latest list
+npx @coderocketapp/cli@latest add dialog
+npx @coderocketapp/cli@latest sync
+```
+
+The CLI preserves locally edited files and places conflicting updates in a review folder.
+
();
+
+beforeEach(async () => {
+ previousExitCode = process.exitCode;
+ root = await mkdtemp(join(tmpdir(), "coderocket-cli-"));
+ vi.stubGlobal("fetch", fetchMock);
+ fetchMock.mockReset();
+ fetchMock.mockImplementation(async (input) =>
+ Response.json(String(input).includes("snapshot.json") ? snapshot : bundle),
+ );
+ vi.spyOn(console, "log").mockImplementation(() => {});
+});
+afterEach(async () => {
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+ process.exitCode = previousExitCode;
+ await rm(root, { recursive: true, force: true });
+});
+function output() {
+ return vi.mocked(console.log).mock.calls.flat().join("\n");
+}
+async function connect() {
+ await mkdir(join(root, ".coderocket"));
+ await writeFile(
+ join(root, ".coderocket/manifest.json"),
+ JSON.stringify({
+ schemaVersion: 1,
+ server: origin,
+ libraryId,
+ components: [],
+ blocks: [],
+ revision: 1,
+ }),
+ );
+}
+
+describe("CLI setup", () => {
+ it.each(
+ [[], ["--help"], ["init", "--help"], ["--version"], ["-v"]].map((args) => ({
+ args,
+ })),
+ )(
+ "prints metadata without credentials, network requests or project writes: $args",
+ async ({ args }) => {
+ await main(args, root, { CODEROCKET_SERVER: "invalid" });
+ expect(output()).toContain(VERSION);
+ if (!args.includes("--version") && !args.includes("-v")) {
+ expect(output()).toContain("npx @coderocketapp/cli@latest");
+ expect(output()).toContain("read -rs CODEROCKET_TOKEN");
+ }
+ expect(fetchMock).not.toHaveBeenCalled();
+ expect(await readdir(root)).toEqual([]);
+ },
+ );
+
+ it.each(
+ [
+ ["init", libraryId, "--token", token],
+ ["init", libraryId, token],
+ ["add"],
+ ["add", "--all", "button"],
+ ["list", "unknown"],
+ ["sync", "unexpected"],
+ ].map((args) => ({ args })),
+ )(
+ "rejects incorrect arguments before any project writes: $args",
+ async ({ args }) => {
+ await expect(main(args, root, {})).rejects.toThrow();
+ expect(await readdir(root)).toEqual([]);
+ expect(fetchMock).not.toHaveBeenCalled();
+ },
+ );
+
+ it.each(["add", "sync"])(
+ "explains missing project setup for %s",
+ async (command) => {
+ await expect(
+ main(command === "add" ? [command, "button"] : [command], root, {
+ CODEROCKET_TOKEN: token,
+ }),
+ ).rejects.toThrow("Run coderocket init first");
+ expect(fetchMock).not.toHaveBeenCalled();
+ },
+ );
+
+ it("explains missing library and token configuration before network access", async () => {
+ await expect(main(["init"], root, {})).rejects.toThrow("valid library ID");
+ await expect(main(["init", libraryId], root, {})).rejects.toThrow(
+ "CODEROCKET_TOKEN",
+ );
+ await expect(main(["list"], root, {})).rejects.toThrow("valid library ID");
+ expect(fetchMock).not.toHaveBeenCalled();
+ });
+
+ it.each([true, false])(
+ "initializes using a positional ID or the existing environment variable (%s)",
+ async (positional) => {
+ await main(positional ? ["init", libraryId] : ["init"], root, {
+ CODEROCKET_LIBRARY: positional ? otherId : libraryId,
+ CODEROCKET_TOKEN: token,
+ });
+ const manifest = await readFile(
+ join(root, ".coderocket/manifest.json"),
+ "utf8",
+ );
+ expect(JSON.parse(manifest)).toMatchObject({ libraryId, revision: 1 });
+ expect(manifest).not.toContain(token);
+ expect(await readFile(join(root, "styles/theme.css"), "utf8")).toBe(
+ bundle.files["styles/theme.css"],
+ );
+ expect(fetchMock).toHaveBeenCalledTimes(2);
+ expect(String(fetchMock.mock.calls[0][0])).toBe(
+ `${origin}/r/${libraryId}/snapshot.json`,
+ );
+ expect(fetchMock.mock.calls[0][1]?.headers).toEqual({
+ Authorization: `Bearer ${token}`,
+ });
+ expect(output()).not.toContain(token);
+ },
+ );
+});
+
+describe("CLI catalogue", () => {
+ it("lists installable component and block slugs without initializing or downloading source", async () => {
+ await main(["list"], root, {
+ CODEROCKET_LIBRARY: libraryId,
+ CODEROCKET_TOKEN: token,
+ });
+ expect(output()).toContain("Components (1)\n button Button");
+ expect(output()).toContain("Blocks (1)\n block-login Login");
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(await readdir(root)).toEqual([]);
+ });
+
+ it.each(["components", "blocks"])(
+ "filters %s using the connected project's library",
+ async (kind) => {
+ await connect();
+ const before = await readFile(
+ join(root, ".coderocket/manifest.json"),
+ "utf8",
+ );
+ await main(["list", kind], root, {
+ CODEROCKET_LIBRARY: otherId,
+ CODEROCKET_TOKEN: token,
+ });
+ expect(output()).toContain(
+ kind === "components" ? "button Button" : "block-login Login",
+ );
+ expect(output()).not.toContain(
+ kind === "components" ? "block-login" : "button Button",
+ );
+ expect(String(fetchMock.mock.calls[0][0])).toBe(
+ `${origin}/r/${libraryId}/snapshot.json`,
+ );
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(await readdir(join(root, ".coderocket"))).toEqual([
+ "manifest.json",
+ ]);
+ expect(
+ await readFile(join(root, ".coderocket/manifest.json"), "utf8"),
+ ).toBe(before);
+ },
+ );
+
+ it.each(["list", "sync"])(
+ "does not send credentials to a different server for %s",
+ async (command) => {
+ await connect();
+ await expect(
+ main([command], root, {
+ CODEROCKET_SERVER: "https://other.example",
+ CODEROCKET_TOKEN: token,
+ }),
+ ).rejects.toThrow("different server");
+ expect(fetchMock).not.toHaveBeenCalled();
+ },
+ );
+});
+
+it("preserves edited files and the installed manifest when sync needs review", async () => {
+ await main(["init", libraryId], root, { CODEROCKET_TOKEN: token });
+ await writeFile(join(root, "styles/theme.css"), "local theme");
+ fetchMock.mockImplementation(async (input) =>
+ Response.json(
+ String(input).includes("snapshot.json")
+ ? { ...snapshot, revision: 2 }
+ : {
+ ...bundle,
+ revision: 2,
+ files: { "styles/theme.css": "upstream theme" },
+ },
+ ),
+ );
+ await main(["sync"], root, { CODEROCKET_TOKEN: token });
+ expect(await readFile(join(root, "styles/theme.css"), "utf8")).toBe(
+ "local theme",
+ );
+ expect(
+ JSON.parse(await readFile(join(root, ".coderocket/manifest.json"), "utf8"))
+ .revision,
+ ).toBe(1);
+ expect(process.exitCode).toBe(2);
+ expect(output()).toContain("Local edits preserved");
+});
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index fe9ae3f..a1645bc 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -25,6 +25,89 @@ import {
type Snapshot,
type Bundle,
} from "@coderocket/shared/registry-client";
+
+declare const __CODEROCKET_VERSION__: string;
+export const VERSION =
+ typeof __CODEROCKET_VERSION__ === "string" ? __CODEROCKET_VERSION__ : "0.3.0";
+const HELP = `CodeRocket CLI ${VERSION}
+
+Usage: coderocket
+ or: npx @coderocketapp/cli@latest
+
+Commands:
+ init Connect a saved library and install its theme
+ list [components|blocks] Browse the connected library's catalogue
+ add button dialog Install components by slug
+ add block-login Install a block by its prefixed slug
+ add --all Install the complete catalogue
+ sync Update untouched files; preserve local changes
+ import Analyse this project locally
+ --help, -h Show this help
+ --version, -v Show the installed version
+
+Quick start (Node.js 24 or newer):
+ 1. Open your saved library's Connect panel in https://ui.coderocket.app.
+ 2. Copy its library ID and create a connection token.
+ 3. Set CODEROCKET_TOKEN using a hidden shell prompt or secret manager.
+ In bash/zsh: read -rs CODEROCKET_TOKEN; export CODEROCKET_TOKEN
+ Paste the token at the hidden prompt and press Enter.
+ 4. Run npx @coderocketapp/cli@latest init in your project.
+ 5. Run npx @coderocketapp/cli@latest list, then add the slugs you need.
+
+CODEROCKET_LIBRARY can supply the library ID for init or list before init.
+CODEROCKET_SERVER optionally selects a different registry server.
+Tokens are read only from CODEROCKET_TOKEN and never saved in project files.
+Never include a token in command arguments or commit it to version control.`;
+
+function validateArguments(args: string[]) {
+ const [command, ...rest] = args;
+ if (!["init", "list", "add", "sync", "import"].includes(command))
+ throw new Error("Unknown command. Run coderocket --help.");
+ if (rest.some((value) => /^--?token(?:=|$)/i.test(value)))
+ throw new Error(
+ "Tokens must be supplied through CODEROCKET_TOKEN, never as command arguments. Run coderocket --help for secure setup.",
+ );
+ if (
+ command === "init" &&
+ (rest.length > 1 || rest.some((value) => value.startsWith("-")))
+ )
+ throw new Error(
+ "Usage: coderocket init . Supply the token through CODEROCKET_TOKEN. Run coderocket --help for setup.",
+ );
+ if (
+ command === "list" &&
+ (rest.length > 1 ||
+ (rest[0] && !["components", "blocks"].includes(rest[0])))
+ )
+ throw new Error("Usage: coderocket list [components|blocks].");
+ if (["sync", "import"].includes(command) && rest.length)
+ throw new Error(`Usage: coderocket ${command}.`);
+ if (command === "add") {
+ if (!rest.length)
+ throw new Error(
+ "Choose components or blocks with coderocket list, then run coderocket add or coderocket add --all.",
+ );
+ if (
+ (rest.includes("--all") && rest.length !== 1) ||
+ rest.some((value) => value.startsWith("-") && value !== "--all")
+ )
+ throw new Error(
+ "Usage: coderocket add ... or coderocket add --all.",
+ );
+ }
+}
+
+function requireConnection(libraryId: string, token: string) {
+ if (!/^[0-9a-f-]{36}$/i.test(libraryId))
+ throw new Error(
+ "A valid library ID is required. Copy it from your saved library's Connect panel, then run coderocket init or set CODEROCKET_LIBRARY.",
+ );
+ if (!/^cr_[A-Za-z0-9_-]{43}$/.test(token))
+ throw new Error(
+ "Set a valid CODEROCKET_TOKEN from your saved library's Connect panel. Use a hidden shell prompt or secret manager; run coderocket --help for setup.",
+ );
+}
+
export type Manifest = {
schemaVersion: 1;
server: string;
@@ -34,9 +117,14 @@ export type Manifest = {
revision: number;
};
export async function readManifest(root: string): Promise {
- const data = JSON.parse(
- await readFile(await safePath(root, ".coderocket/manifest.json"), "utf8"),
- ) as Manifest;
+ const source = await readOptional(
+ await safePath(root, ".coderocket/manifest.json"),
+ );
+ if (!source)
+ throw new Error(
+ "This project is not connected. Run coderocket init first. Run coderocket --help for setup.",
+ );
+ const data = JSON.parse(source) as Manifest;
if (
data.schemaVersion !== 1 ||
!Array.isArray(data.components) ||
@@ -122,12 +210,6 @@ async function run(
env = process.env,
) {
const [command, ...rest] = args;
- if (!command || ["help", "--help", "-h"].includes(command)) {
- console.log(
- "CodeRocket\n\ninit Connect a saved library\nadd button dialog Install components\nadd block-login Install a block\nadd --all Install the complete catalogue\nsync Update untouched files; preserve local changes\nimport Analyse this project locally\n\nSet CODEROCKET_LIBRARY, CODEROCKET_TOKEN, and optionally CODEROCKET_SERVER. Tokens are never written into project files.",
- );
- return;
- }
if (command === "import") {
const report = await inspectCodebase(root);
const path = ".coderocket/import-report.json";
@@ -139,18 +221,22 @@ async function run(
console.log(`Analysis saved to ${path}. No source changes or uploads.`);
return;
}
- if (!["init", "add", "sync"].includes(command))
- throw new Error("Unknown command. Run coderocket --help.");
const server = serverOrigin(env.CODEROCKET_SERVER || DEFAULT_SERVER),
token = env.CODEROCKET_TOKEN || "";
let manifest: Manifest;
- if (command === "init") {
- if (await readOptional(await safePath(root, ".coderocket/manifest.json")))
+ const manifestExists =
+ (await readOptional(await safePath(root, ".coderocket/manifest.json"))) !==
+ undefined;
+ if (command === "init" || (command === "list" && !manifestExists)) {
+ if (command === "init" && manifestExists)
throw new Error("This project is already connected. Use add or sync.");
manifest = {
schemaVersion: 1,
server,
- libraryId: env.CODEROCKET_LIBRARY || "",
+ libraryId:
+ (command === "init" ? rest[0] : undefined) ||
+ env.CODEROCKET_LIBRARY ||
+ "",
components: [],
blocks: [],
revision: 0,
@@ -162,12 +248,36 @@ async function run(
"This project uses a different server. Set CODEROCKET_SERVER explicitly before sending your token.",
);
}
+ requireConnection(manifest.libraryId, token);
const snapshot = await registryRequest(
server,
manifest.libraryId,
"snapshot.json",
token,
);
+ if (command === "list") {
+ const kinds = rest[0]
+ ? [rest[0] as "components" | "blocks"]
+ : (["components", "blocks"] as const);
+ const sections = kinds.map((kind) => {
+ const items = snapshot[kind];
+ return [
+ `${kind === "components" ? "Components" : "Blocks"} (${items.length})`,
+ ...items.map(
+ (item) =>
+ ` ${kind === "blocks" ? "block-" : ""}${item.slug} ${item.name}`,
+ ),
+ ...(items.length ? [] : [" No items available."]),
+ ].join("\n");
+ });
+ const setup = manifestExists
+ ? ""
+ : `Connect this project first: npx @coderocketapp/cli@latest init ${manifest.libraryId}\n`;
+ console.log(
+ `${snapshot.name} — revision ${snapshot.revision}\n\n${sections.join("\n\n")}\n\n${setup}Install with: npx @coderocketapp/cli@latest add `,
+ );
+ return;
+ }
const selectedComponents = new Set(manifest.components),
selectedBlocks = new Set(manifest.blocks);
if (command === "add") {
@@ -263,8 +373,21 @@ export async function main(
root = process.cwd(),
env = process.env,
) {
- if (!args[0] || ["help", "--help", "-h"].includes(args[0]))
- return run(args, root, env);
+ if (
+ !args[0] ||
+ args[0] === "help" ||
+ args.includes("--help") ||
+ args.includes("-h")
+ ) {
+ console.log(HELP);
+ return;
+ }
+ if (args.length === 1 && ["--version", "-v"].includes(args[0])) {
+ console.log(VERSION);
+ return;
+ }
+ validateArguments(args);
+ if (args[0] === "list") return run(args, root, env);
await mkdir(await safePath(root, ".coderocket"), { recursive: true });
const path = await safePath(root, ".coderocket/command.lock");
const handle = await open(path, "wx").catch(() => {
diff --git a/packages/mcp/README.md b/packages/mcp/README.md
index 36f2fc0..4f05295 100644
--- a/packages/mcp/README.md
+++ b/packages/mcp/README.md
@@ -1,48 +1,83 @@
# CodeRocket MCP
-A MIT-licensed, read-only MCP server giving an agent the design rules and source from one saved library. Requires Node.js 24+.
+A read-only MCP server that gives your coding agent access to a saved CodeRocket library: its design system, integration rules, components, blocks, source files and dependencies. React libraries return React source; Vue libraries return native Vue single-file components.
-## Build and connect
+Requires **Node.js 24 or newer**, npm, and an MCP client that supports local stdio servers. The official [`@coderocketapp/mcp`](https://www.npmjs.com/package/@coderocketapp/mcp) package runs through `npx`; no global installation is needed.
-From the repository root, run `pnpm install --frozen-lockfile` and `pnpm build`. Save a library in [the Studio](https://ui.coderocket.app/studio), open **Connect**, and create a scoped registry token.
+## Create a connection
-Configure your MCP client's stdio server entry:
+1. Open [CodeRocket Studio](https://ui.coderocket.app), create a library, and save it.
+2. Open **Connect**, create a connection for your coding agent, and copy the token shown once.
+3. Copy the library ID from the MCP configuration in **Connect**.
+
+A token grants read-only access to one library, expires after 90 days, and can be revoked from **Connect**. Hosted registry access requires an account, a saved library and its token. You can use the [MIT-licensed component source](https://github.com/elreco/coderocket-ui) without an account or this server.
+
+## Configure your MCP client
+
+Merge the following server into your client's MCP configuration. Replace the placeholders with your library ID and connection token:
```json
{
"mcpServers": {
"coderocket": {
- "command": "node",
- "args": ["/absolute/path/to/coderocket-ui/packages/mcp/dist/index.mjs"],
+ "command": "npx",
+ "args": ["-y", "@coderocketapp/mcp@latest"],
"env": {
- "CODEROCKET_LIBRARY": "YOUR_LIBRARY_UUID",
- "CODEROCKET_TOKEN": "YOUR_SCOPED_CONNECTION_TOKEN",
- "CODEROCKET_SERVER": "https://ui.coderocket.app"
+ "CODEROCKET_LIBRARY": "YOUR_LIBRARY_ID",
+ "CODEROCKET_TOKEN": "YOUR_CONNECTION_TOKEN"
}
}
}
}
```
-Client configuration formats vary; translate the command, arguments and environment into your client's settings. Prefer secret storage over a checked-in configuration. Values shown are placeholders. `CODEROCKET_SERVER` is optional and defaults to `https://ui.coderocket.app`. Remote requests require HTTPS. This repository includes the integration client, not the hosted account/registry backend; a compatible registry can be configured explicitly.
+Keep this configuration private and outside version control. The `env` fields become environment variables for the server process. If your client offers protected secret settings, provide `CODEROCKET_TOKEN` there and remove the token entry from this JSON. Never place tokens in prompts, source files, URLs or command arguments.
+
+MCP clients differ in where they store configuration; use your client's local stdio server settings. Restart or reconnect the server after updating its configuration. The client launches and manages the process. With valid connection environment variables, a server started manually waits for MCP messages on standard input; missing settings produce setup guidance.
+
+## Environment
+
+| Variable | Purpose |
+| -------------------- | --------------------------------------------------------------- |
+| `CODEROCKET_LIBRARY` | Required saved library ID from the Studio |
+| `CODEROCKET_TOKEN` | Required connection token for that library |
+| `CODEROCKET_SERVER` | Optional server origin; defaults to `https://ui.coderocket.app` |
+
+For a different Studio deployment, add `CODEROCKET_SERVER` to the server environment using its HTTPS origin, without a path or credentials. Local development also supports HTTP on localhost.
-## Tools
+## Available tools
-| Tool | Result |
-| ------------------- | ---------------------------------------------------------------- |
-| `get_design_system` | Latest saved model and revision. |
-| `get_design_rules` | Token rules, component conventions and integration instructions. |
-| `list_components` | Available component descriptions. |
-| `search_components` | Components matching all supplied search terms. |
-| `get_component` | Source files, dependencies and paths for one component. |
-| `list_blocks` | Available block descriptions. |
-| `search_blocks` | Blocks matching all supplied search terms. |
-| `get_block` | Source files, dependencies and paths for one block. |
+| Tool | Returns |
+| ------------------- | --------------------------------------------------------------- |
+| `get_design_system` | The latest saved design model and revision |
+| `get_design_rules` | Component conventions, token rules and integration instructions |
+| `list_components` | Available component names and descriptions |
+| `search_components` | Components matching a `query` |
+| `get_component` | Source files, dependencies and installation paths for a `slug` |
+| `list_blocks` | Available block names and descriptions |
+| `search_blocks` | Blocks matching a `query` |
+| `get_block` | Source files, dependencies and installation paths for a `slug` |
-The `coderocket://design-rules` resource exposes the same rules as Markdown. Tools read the latest saved library; unsaved editor changes are not included.
+The `coderocket://design-rules` resource exposes the same integration rules as Markdown.
-## Boundaries
+Try asking your agent:
+
+> Read my CodeRocket design rules and build a settings form with the library's input, select and button components. Install the required dependencies and import the theme styles.
+
+The server reads your saved library. It does not save Studio changes or write project files. Your coding agent applies source changes using its own tools and permissions. Review those changes in your application. Save changes in the Studio before asking the agent to fetch an updated design.
+
+## Troubleshooting
+
+Check package availability and setup instructions without credentials:
+
+```sh
+npx -y @coderocketapp/mcp@latest --help
+npx -y @coderocketapp/mcp@latest --version
+```
-This server does not edit project files, run shell commands, publish packages, apply AI proposals or modify saved libraries. Retrieved source is project data: the agent must review it and obey its host application's permissions before writing files.
+- If the client cannot find `npx`, make sure Node.js and npm are available to the application that launches the MCP server.
+- If a token is expired or revoked, create another connection for the same library, update the server environment and reconnect.
+- If the library cannot be read, confirm that its ID and token belong to the same saved library and that `CODEROCKET_SERVER` points to the Studio that created them.
+- If the catalogue appears unchanged, save the library in the Studio and request it again. Unsaved previews are not exposed.
-Tokens remain in the process environment and are sent to the explicitly configured registry. Redirects are rejected, responses bounded and requests timed out. Revoke tokens in the Studio when no longer needed. No AI provider key is required by this server; your coding agent manages its own model connection.
+Use [`@coderocketapp/cli`](https://www.npmjs.com/package/@coderocketapp/cli) to install and sync source from a terminal with local-edit protection. See the [coding-agent guide](https://ui.coderocket.app/docs/agents) for integration details.
diff --git a/packages/mcp/package.json b/packages/mcp/package.json
index 414808e..dda9b07 100644
--- a/packages/mcp/package.json
+++ b/packages/mcp/package.json
@@ -1,6 +1,6 @@
{
"name": "@coderocket/mcp",
- "version": "0.1.0",
+ "version": "0.3.1",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/mcp/src/index.test.ts b/packages/mcp/src/index.test.ts
new file mode 100644
index 0000000..d63a86d
--- /dev/null
+++ b/packages/mcp/src/index.test.ts
@@ -0,0 +1,115 @@
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { createServer, main, VERSION } from "./index";
+
+const env = {
+ CODEROCKET_LIBRARY: "11111111-1111-4111-8111-111111111111",
+ CODEROCKET_TOKEN: "cr_" + "x".repeat(43),
+};
+
+afterEach(() => {
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+});
+
+describe("MCP command entry point", () => {
+ it.each(["--help", "-h", "help", "--version", "-v"])(
+ "%s does not start stdio, require configuration or access the registry",
+ async (argument) => {
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
+ const connect = vi.spyOn(McpServer.prototype, "connect");
+ const start = vi.spyOn(StdioServerTransport.prototype, "start");
+ const fetchMock = vi.fn();
+ vi.stubGlobal("fetch", fetchMock);
+ await main([argument], { CODEROCKET_SERVER: "invalid" });
+ expect(log.mock.calls.flat().join("\n")).toContain(VERSION);
+ expect(connect).not.toHaveBeenCalled();
+ expect(start).not.toHaveBeenCalled();
+ expect(fetchMock).not.toHaveBeenCalled();
+ },
+ );
+
+ it("explains npm client configuration in help", async () => {
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
+ await main(["--help"], {});
+ const output = log.mock.calls.flat().join("\n");
+ expect(output).toContain("@coderocketapp/mcp@latest");
+ expect(output).toContain("CODEROCKET_LIBRARY and CODEROCKET_TOKEN");
+ expect(output).toContain("list_components, list_blocks");
+ });
+
+ it("rejects missing configuration and unexpected arguments before connecting", async () => {
+ const connect = vi.spyOn(McpServer.prototype, "connect");
+ await expect(main([], {})).rejects.toThrow("MCP client's environment");
+ await expect(main(["--token", env.CODEROCKET_TOKEN], env)).rejects.toThrow(
+ "never command arguments",
+ );
+ expect(connect).not.toHaveBeenCalled();
+ });
+
+ it.each([[], ["--stdio"]].map((args) => ({ args })))(
+ "starts stdio with the existing environment configuration: $args",
+ async ({ args }) => {
+ const connect = vi
+ .spyOn(McpServer.prototype, "connect")
+ .mockResolvedValue();
+ await main(args, env);
+ expect(connect).toHaveBeenCalledExactlyOnceWith(
+ expect.any(StdioServerTransport),
+ );
+ },
+ );
+});
+
+it("advertises the package version and keeps catalogue tools authenticated and read-only", async () => {
+ const components = [
+ {
+ slug: "button",
+ name: "Button",
+ category: "Inputs",
+ description: "A button",
+ },
+ ];
+ const fetchMock = vi
+ .fn()
+ .mockImplementation(async () => Response.json({ components }));
+ vi.stubGlobal("fetch", fetchMock);
+ const server = createServer(env);
+ const client = new Client({ name: "test-client", version: "1.0.0" });
+ const [clientTransport, serverTransport] =
+ InMemoryTransport.createLinkedPair();
+ try {
+ await server.connect(serverTransport);
+ await client.connect(clientTransport);
+ expect(client.getServerVersion()).toEqual({
+ name: "coderocket",
+ version: VERSION,
+ });
+ const { tools } = await client.listTools();
+ expect(tools).toHaveLength(8);
+ expect(tools.every((tool) => tool.annotations?.readOnlyHint === true)).toBe(
+ true,
+ );
+ const result = await client.callTool({
+ name: "list_components",
+ arguments: {},
+ });
+ expect(result.content).toEqual([
+ { type: "text", text: JSON.stringify(components, null, 2) },
+ ]);
+ expect(fetchMock).toHaveBeenCalledExactlyOnceWith(
+ new URL(
+ `https://ui.coderocket.app/r/${env.CODEROCKET_LIBRARY}/snapshot.json`,
+ ),
+ expect.objectContaining({
+ headers: { Authorization: `Bearer ${env.CODEROCKET_TOKEN}` },
+ }),
+ );
+ } finally {
+ await client.close();
+ await server.close();
+ }
+});
diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts
index 3658d9c..f67ecf1 100644
--- a/packages/mcp/src/index.ts
+++ b/packages/mcp/src/index.ts
@@ -12,10 +12,40 @@ import {
type Snapshot,
} from "@coderocket/shared/registry-client";
+declare const __CODEROCKET_VERSION__: string;
+export const VERSION =
+ typeof __CODEROCKET_VERSION__ === "string" ? __CODEROCKET_VERSION__ : "0.3.0";
+const HELP = `CodeRocket MCP ${VERSION}
+
+Usage: coderocket-mcp [--stdio]
+ or: npx -y @coderocketapp/mcp@latest
+
+A read-only MCP server for your saved CodeRocket library. Add it to your
+coding agent's MCP configuration with:
+ command: npx
+ args: ["-y", "@coderocketapp/mcp@latest"]
+ env: CODEROCKET_LIBRARY and CODEROCKET_TOKEN
+
+Copy your library ID and create a connection token in your saved library's
+Connect panel at https://ui.coderocket.app. Store these values in your MCP
+client's environment or secret settings. Never pass tokens as arguments or
+commit them to version control. CODEROCKET_SERVER is optional.
+
+Tools: get_design_system, get_design_rules, list_components, list_blocks,
+ search_components, search_blocks, get_component, get_block
+Resource: coderocket://design-rules
+
+Options:
+ --stdio Start the stdio server (also the default)
+ --help, -h Show this help without starting the server
+ --version, -v Show the installed version
+
+Requires Node.js 24 or newer. The stdio server is intended for an MCP client.`;
+
export function createServer(
env: Record = process.env,
) {
- const server = new McpServer({ name: "coderocket", version: "0.1.0" });
+ const server = new McpServer({ name: "coderocket", version: VERSION });
const origin = serverOrigin(env.CODEROCKET_SERVER || DEFAULT_SERVER),
id = env.CODEROCKET_LIBRARY || "",
token = env.CODEROCKET_TOKEN || "";
@@ -119,16 +149,38 @@ export function createServer(
);
return server;
}
-async function main() {
- await createServer().connect(new StdioServerTransport());
+export async function main(
+ args = process.argv.slice(2),
+ env: Record = process.env,
+) {
+ if (args.length === 1 && ["help", "--help", "-h"].includes(args[0])) {
+ console.log(HELP);
+ return;
+ }
+ if (args.length === 1 && ["--version", "-v"].includes(args[0])) {
+ console.log(VERSION);
+ return;
+ }
+ if (args.length && !(args.length === 1 && args[0] === "--stdio"))
+ throw new Error(
+ "Unknown arguments. Run coderocket-mcp --help. Supply credentials through environment variables, never command arguments.",
+ );
+ if (
+ !/^[0-9a-f-]{36}$/i.test(env.CODEROCKET_LIBRARY || "") ||
+ !/^cr_[A-Za-z0-9_-]{43}$/.test(env.CODEROCKET_TOKEN || "")
+ )
+ throw new Error(
+ "Set CODEROCKET_LIBRARY and CODEROCKET_TOKEN from your saved library's Connect panel in your MCP client's environment. Run coderocket-mcp --help for setup.",
+ );
+ await createServer(env).connect(new StdioServerTransport());
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href
)
- main().catch(() => {
+ main().catch((error) => {
console.error(
- "CodeRocket MCP failed. Check its connection environment variables.",
+ error instanceof Error ? error.message : "CodeRocket MCP failed.",
);
process.exitCode = 1;
});
diff --git a/tooling/build.mjs b/tooling/build.mjs
index 4f0ad11..1516499 100644
--- a/tooling/build.mjs
+++ b/tooling/build.mjs
@@ -51,6 +51,9 @@ for (const [name, names] of Object.entries(entries)) {
format: "esm",
target: executable ? "node24" : "es2022",
packages: "external",
+ define: executable
+ ? { __CODEROCKET_VERSION__: JSON.stringify(manifest.version) }
+ : undefined,
tsconfigRaw: { compilerOptions: { jsx: "react-jsx" } },
legalComments: "eof",
outExtension: executable ? { ".js": ".mjs" } : undefined,
diff --git a/tooling/check-client-packages.mjs b/tooling/check-client-packages.mjs
new file mode 100644
index 0000000..6138ef7
--- /dev/null
+++ b/tooling/check-client-packages.mjs
@@ -0,0 +1,248 @@
+import assert from "node:assert/strict";
+import { execFile, spawn } from "node:child_process";
+import { promisify } from "node:util";
+import { createServer } from "node:http";
+import { randomUUID } from "node:crypto";
+import {
+ mkdtemp,
+ mkdir,
+ readFile,
+ readdir,
+ rm,
+ writeFile,
+} from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const exec = promisify(execFile);
+const root = fileURLToPath(new URL("../", import.meta.url));
+const temp = await mkdtemp(resolve(tmpdir(), "coderocket-npm-smoke-"));
+const packages = JSON.parse(
+ await readFile(resolve(root, ".release/manifest.json"), "utf8"),
+);
+assert.equal(
+ packages.length,
+ 2,
+ "Build both clients before checking their packages.",
+);
+const library = randomUUID();
+const token = `cr_${"a".repeat(43)}`;
+let revision = 1;
+let calls = 0;
+const registry = createServer((request, response) => {
+ calls++;
+ if (request.headers.authorization !== `Bearer ${token}`) {
+ response.writeHead(401).end();
+ return;
+ }
+ const url = new URL(request.url, "http://localhost");
+ response.setHeader("Content-Type", "application/json");
+ if (url.pathname === `/r/${library}/snapshot.json`) {
+ response.end(
+ JSON.stringify({
+ id: library,
+ name: "Package smoke library",
+ revision,
+ model: {},
+ rules: "Use the saved design tokens.",
+ components: [
+ {
+ slug: "button",
+ name: "Button",
+ description: "Action",
+ category: "form",
+ },
+ ],
+ blocks: [],
+ custom_components: [],
+ }),
+ );
+ } else if (url.pathname === `/r/${library}/bundle.json`) {
+ response.end(
+ JSON.stringify({
+ schemaVersion: 1,
+ libraryId: library,
+ revision,
+ dependencies: {},
+ files: {
+ "styles/theme.css": `:root { --revision: ${revision}; }\n`,
+ ...(url.searchParams.get("components")?.includes("button")
+ ? {
+ "components/ui/button.tsx": `export const Button = () => ${JSON.stringify(`revision-${revision}`)};\n`,
+ }
+ : {}),
+ },
+ }),
+ );
+ } else response.writeHead(404).end();
+});
+await new Promise((done) => registry.listen(0, "127.0.0.1", done));
+const cleanEnv = Object.fromEntries(
+ Object.entries(process.env).filter(
+ ([key]) => !key.startsWith("CODEROCKET_") && key !== "NODE_PATH",
+ ),
+);
+const env = {
+ ...cleanEnv,
+ CODEROCKET_LIBRARY: library,
+ CODEROCKET_TOKEN: token,
+ CODEROCKET_SERVER: `http://127.0.0.1:${registry.address().port}`,
+};
+try {
+ await writeFile(
+ resolve(temp, "package.json"),
+ '{"name":"coderocket-package-consumer","private":true,"type":"module"}\n',
+ );
+ await exec(
+ "npm",
+ [
+ "install",
+ "--offline",
+ "--ignore-scripts",
+ "--no-audit",
+ "--no-fund",
+ "--cache",
+ resolve(temp, "npm-cache"),
+ ...packages.map((item) => resolve(root, ".release", item.filename)),
+ ],
+ { cwd: temp, env: cleanEnv },
+ );
+ for (const item of packages) {
+ const kind = item.name.split("/")[1];
+ const directory = resolve(temp, "node_modules", item.name);
+ const manifest = JSON.parse(
+ await readFile(resolve(directory, "package.json"), "utf8"),
+ );
+ assert.equal(manifest.private, undefined);
+ assert.equal(manifest.version, item.version);
+ assert.equal(
+ manifest.dependencies,
+ undefined,
+ "Clients must not depend on unpublished workspace packages.",
+ );
+ assert.deepEqual((await readdir(directory)).sort(), [
+ "LICENSE",
+ "README.md",
+ "THIRD_PARTY_NOTICES.md",
+ "dist",
+ "package.json",
+ ]);
+ const bin = resolve(
+ temp,
+ "node_modules/.bin",
+ kind === "cli" ? "coderocket" : "coderocket-mcp",
+ );
+ const help = await exec(bin, ["--help"], { cwd: temp, env: cleanEnv });
+ assert.match(help.stdout, /CodeRocket/);
+ const version = await exec(bin, ["--version"], {
+ cwd: temp,
+ env: cleanEnv,
+ });
+ assert.equal(version.stdout.trim(), item.version);
+ }
+ const cli = resolve(temp, "node_modules/.bin/coderocket");
+ const run = (...args) => exec(cli, args, { cwd: temp, env });
+ const before = calls;
+ await assert.rejects(
+ exec(cli, ["init"], { cwd: temp, env: cleanEnv }),
+ (error) => error.code === 1 && /CODEROCKET_|Connect/.test(error.stderr),
+ );
+ assert.equal(
+ calls,
+ before,
+ "Missing config must fail before contacting a registry.",
+ );
+ await run("init", library);
+ assert.equal(
+ JSON.parse(
+ await readFile(resolve(temp, ".coderocket/manifest.json"), "utf8"),
+ ).libraryId,
+ library,
+ );
+ assert.match((await run("list", "components")).stdout, /button/);
+ await run("add", "button");
+ const installed = resolve(temp, "components/ui/button.tsx");
+ assert.match(await readFile(installed, "utf8"), /revision-1/);
+ revision = 2;
+ await run("sync");
+ assert.match(await readFile(installed, "utf8"), /revision-2/);
+ await writeFile(installed, "// Local edits must survive\n");
+ revision = 3;
+ await assert.rejects(
+ run("sync"),
+ (error) => error.code === 2 && /Local edits preserved/.test(error.stdout),
+ );
+ assert.equal(
+ await readFile(installed, "utf8"),
+ "// Local edits must survive\n",
+ );
+ const mcp = spawn(resolve(temp, "node_modules/.bin/coderocket-mcp"), [], {
+ cwd: temp,
+ env,
+ stdio: ["pipe", "pipe", "pipe"],
+ });
+ let buffer = "";
+ const pending = new Map();
+ mcp.stdout.on("data", (chunk) => {
+ buffer += chunk;
+ while (buffer.includes("\n")) {
+ const end = buffer.indexOf("\n");
+ const line = buffer.slice(0, end);
+ buffer = buffer.slice(end + 1);
+ try {
+ const message = JSON.parse(line);
+ pending.get(message.id)?.(message);
+ } catch {
+ /* Ignore empty transport lines. */
+ }
+ }
+ });
+ const request = (id, method, params) =>
+ new Promise((done, reject) => {
+ const timeout = setTimeout(() => {
+ pending.delete(id);
+ reject(new Error(`MCP ${method} timed out.`));
+ }, 10000);
+ pending.set(id, (message) => {
+ clearTimeout(timeout);
+ pending.delete(id);
+ message.error
+ ? reject(new Error(JSON.stringify(message.error)))
+ : done(message.result);
+ });
+ mcp.stdin.write(
+ JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
+ );
+ });
+ try {
+ const init = await request(1, "initialize", {
+ protocolVersion: "2024-11-05",
+ capabilities: {},
+ clientInfo: { name: "package-smoke", version: "1.0.0" },
+ });
+ assert.equal(
+ init.serverInfo.version,
+ packages.find((item) => item.name.endsWith("/mcp")).version,
+ );
+ mcp.stdin.write(
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) +
+ "\n",
+ );
+ const tools = await request(2, "tools/list", {});
+ assert.ok(tools.tools.some((tool) => tool.name === "get_design_rules"));
+ const rules = await request(3, "tools/call", {
+ name: "get_design_rules",
+ arguments: {},
+ });
+ assert.match(rules.content[0].text, /saved design tokens/);
+ } finally {
+ mcp.kill();
+ }
+ console.log(
+ "Installed npm archives outside the workspace: CLI init/list/add/sync, local edit preservation and MCP handshake/tools passed.",
+ );
+} finally {
+ await new Promise((done) => registry.close(done));
+ await rm(temp, { recursive: true, force: true });
+}
diff --git a/tooling/client-release.mjs b/tooling/client-release.mjs
new file mode 100644
index 0000000..95488c3
--- /dev/null
+++ b/tooling/client-release.mjs
@@ -0,0 +1,197 @@
+import { build } from "esbuild";
+import {
+ chmod,
+ copyFile,
+ mkdir,
+ readFile,
+ readdir,
+ rm,
+ writeFile,
+} from "node:fs/promises";
+import { execFileSync } from "node:child_process";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+
+const root = fileURLToPath(new URL("../", import.meta.url));
+export const clients = ["cli", "mcp"];
+
+export async function buildClient(name) {
+ if (!clients.includes(name)) throw new Error("Choose cli or mcp.");
+ const directory = resolve(root, "packages", name);
+ const source = JSON.parse(
+ await readFile(resolve(directory, "package.json"), "utf8"),
+ );
+ if (!/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/.test(source.version))
+ throw new Error("Invalid release version.");
+ const output = resolve(directory, "dist/index.mjs");
+ await mkdir(dirname(output), { recursive: true });
+ const result = await build({
+ entryPoints: [resolve(directory, "src/index.ts")],
+ outfile: output,
+ absWorkingDir: root,
+ bundle: true,
+ platform: "node",
+ format: "esm",
+ target: "node24",
+ packages: "bundle",
+ metafile: true,
+ legalComments: "eof",
+ define: { __CODEROCKET_VERSION__: JSON.stringify(source.version) },
+ banner: {
+ js: "import {createRequire as __crCreateRequire} from 'node:module'; const require = __crCreateRequire(import.meta.url);",
+ },
+ });
+ await chmod(output, 0o755);
+ const notices = new Map();
+ for (const input of Object.keys(result.metafile.inputs)) {
+ if (!input.includes("node_modules/")) continue;
+ let current = dirname(resolve(root, input));
+ while (current !== dirname(current)) {
+ try {
+ const manifest = JSON.parse(
+ await readFile(resolve(current, "package.json"), "utf8"),
+ );
+ if (!manifest.name) {
+ current = dirname(current);
+ continue;
+ }
+ const key = `${manifest.name}@${manifest.version}`;
+ if (!notices.has(key)) {
+ const files = (await readdir(current))
+ .filter((file) =>
+ /^(?:licen[cs]e|copying|notice)(?:\.[a-z]+)?$/i.test(file),
+ )
+ .sort();
+ const texts = await Promise.all(
+ files.map((file) => readFile(resolve(current, file), "utf8")),
+ );
+ notices.set(
+ key,
+ `## ${key}\n\nLicense: ${manifest.license || "See package metadata"}\n\n${texts.join("\n\n")}`,
+ );
+ }
+ break;
+ } catch (error) {
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
+ }
+ current = dirname(current);
+ }
+ }
+ return {
+ directory,
+ version: source.version,
+ notices: [...notices]
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([, value]) => value)
+ .join("\n\n"),
+ };
+}
+
+export async function packClient(name, outputRoot = resolve(root, ".release")) {
+ const { directory, version, notices } = await buildClient(name);
+ const stage = resolve(outputRoot, name);
+ await rm(stage, { recursive: true, force: true });
+ await mkdir(resolve(stage, "dist"), { recursive: true });
+ for (const file of ["dist/index.mjs", "README.md", "LICENSE"])
+ await copyFile(resolve(directory, file), resolve(stage, file));
+ await writeFile(
+ resolve(stage, "THIRD_PARTY_NOTICES.md"),
+ `# Bundled dependencies\n\nThird-party code retains its original license.\n\n${notices || "This client bundles only CodeRocket source and uses Node.js built-ins."}\n`,
+ );
+ const manifest = {
+ name: `@coderocketapp/${name}`,
+ version,
+ description:
+ name === "cli"
+ ? "Install and sync editable React and Vue components from your CodeRocket UI library."
+ : "Give coding agents read-only access to your CodeRocket UI design system, components and blocks through MCP.",
+ type: "module",
+ license: "MIT",
+ engines: { node: ">=24" },
+ bin: {
+ [name === "cli" ? "coderocket" : "coderocket-mcp"]: "dist/index.mjs",
+ },
+ files: ["dist/index.mjs", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md"],
+ repository: {
+ type: "git",
+ url: "git+https://github.com/elreco/coderocket-ui.git",
+ directory: `packages/${name}`,
+ },
+ bugs: { url: "https://github.com/elreco/coderocket-ui/issues" },
+ homepage: "https://ui.coderocket.app/docs/agents",
+ keywords: [
+ "coderocket",
+ "react",
+ "vue",
+ "components",
+ "design-system",
+ name === "mcp" ? "model-context-protocol" : "cli",
+ ],
+ publishConfig: {
+ access: "public",
+ registry: "https://registry.npmjs.org/",
+ },
+ };
+ await writeFile(
+ resolve(stage, "package.json"),
+ JSON.stringify(manifest, null, 2) + "\n",
+ );
+ const [packed] = JSON.parse(
+ execFileSync(
+ "npm",
+ [
+ "pack",
+ "--json",
+ "--ignore-scripts",
+ "--cache",
+ resolve(outputRoot, ".npm-cache"),
+ "--pack-destination",
+ outputRoot,
+ ],
+ { cwd: stage, encoding: "utf8" },
+ ),
+ );
+ const allowed = new Set([
+ "package.json",
+ "dist/index.mjs",
+ "README.md",
+ "LICENSE",
+ "THIRD_PARTY_NOTICES.md",
+ ]);
+ if (
+ packed.files.some(({ path }) => !allowed.has(path)) ||
+ packed.files.length !== allowed.size
+ )
+ throw new Error("Unexpected files in client package.");
+ const metadata = {
+ name: manifest.name,
+ version,
+ filename: packed.filename,
+ integrity: packed.integrity,
+ size: packed.size,
+ };
+ console.log(
+ `${manifest.name}@${version}: ${packed.entryCount} files, ${packed.size} bytes`,
+ );
+ return metadata;
+}
+
+async function main() {
+ const selected = process.argv.slice(2);
+ const names = selected.length ? selected : clients;
+ if (names.some((name) => !clients.includes(name)))
+ throw new Error("Usage: node tooling/client-release.mjs [cli|mcp]");
+ const output = resolve(root, ".release");
+ await mkdir(output, { recursive: true });
+ const packages = [];
+ for (const name of names) packages.push(await packClient(name, output));
+ await writeFile(
+ resolve(output, "manifest.json"),
+ JSON.stringify(packages, null, 2) + "\n",
+ );
+}
+if (
+ process.argv[1] &&
+ import.meta.url === pathToFileURL(resolve(process.argv[1])).href
+)
+ await main();
diff --git a/tooling/publish-clients.mjs b/tooling/publish-clients.mjs
new file mode 100644
index 0000000..780375a
--- /dev/null
+++ b/tooling/publish-clients.mjs
@@ -0,0 +1,56 @@
+import { createHash } from "node:crypto";
+import { readFile } from "node:fs/promises";
+import { execFileSync } from "node:child_process";
+import { resolve } from "node:path";
+
+if (
+ process.env.GITHUB_REPOSITORY !== "elreco/coderocket-ui" ||
+ !process.env.GITHUB_REF_NAME?.startsWith("clients-v")
+)
+ throw new Error(
+ "Automatic publishing requires the public repository's client release workflow.",
+ );
+const packages = JSON.parse(await readFile(".release/manifest.json", "utf8"));
+if (packages.length !== 2)
+ throw new Error("Expected both tested client archives.");
+for (const item of packages) {
+ if (!["@coderocketapp/cli", "@coderocketapp/mcp"].includes(item.name))
+ throw new Error("Unexpected package.");
+ if (!/^coderocketapp-(cli|mcp)-[0-9a-z.-]+\.tgz$/.test(item.filename))
+ throw new Error("Invalid archive filename.");
+ const bytes = await readFile(resolve(".release", item.filename));
+ if (
+ `sha512-${createHash("sha512").update(bytes).digest("base64")}` !==
+ item.integrity
+ )
+ throw new Error("Package archive integrity mismatch.");
+ const response = await fetch(
+ `https://registry.npmjs.org/${encodeURIComponent(item.name)}/${item.version}`,
+ { signal: AbortSignal.timeout(30000) },
+ );
+ if (response.ok) {
+ const existing = await response.json();
+ if (existing.dist.integrity !== item.integrity)
+ throw new Error(
+ `${item.name}@${item.version} already exists with different contents. Bump the version.`,
+ );
+ console.log(
+ `${item.name}@${item.version} already published; exact archive verified.`,
+ );
+ continue;
+ }
+ if (response.status !== 404)
+ throw new Error(`Cannot verify registry state (${response.status}).`);
+ execFileSync(
+ "npm",
+ [
+ "publish",
+ resolve(".release", item.filename),
+ "--access",
+ "public",
+ "--provenance",
+ "--ignore-scripts",
+ ],
+ { stdio: "inherit" },
+ );
+}