* docs(skills): route frontend work through an embedded shadcn/ui skill The application development Skill now carries the shadcn/ui skill from shadcn 4.21.0, unchanged, under references/frontend/shadcn/, and references/frontend/references/shadcn.md says what to read there and where an application departs from it: `pnpm exec shadcn`, no `apply` or `--preset`, overlays as child routes, labels above inputs, and the English built into a few primitives translated. - Primitives are added on demand with `yes n | pnpm exec shadcn add`, which keeps installed primitives from being overwritten and lets a non-interactive run finish, then formatted with Prettier. - styling.md, form.md and the other topic references link to the skill instead of restating it; the worked example names the primitives each file needs on an **Add first** line. - The frontend handbook rework: the projects worked example split into one document per file, shell.md, the capture tool guide and the reviewer prompts. - Both .prettierignore files skip the vendored skill so a shadcn upgrade can replace it wholesale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(app-template-default): ship only the primitives the shell uses Primitives are added with the shadcn CLI when a page first needs one, so every application, template and plugin starts from the same CLI output. The shadcn/ui skill embedded in the development Skill replaces the reference gallery. - Remove client/pages/reference/ with its tests, the 49 primitives that neither the shell nor the shipped compositions import, and use-mobile.ts. The template keeps button, calendar, dialog, dropdown-menu, input, label, popover, select, spinner, table, toast and tooltip. - Drop the devDependencies only those primitives used (recharts, cmdk, embla-carousel-react, input-otp, react-resizable-panels, @shadcn/react) and add cn, which registry primitives now import. - toast.tsx names its close button through actions.close, as dialog.tsx and spinner.tsx translate theirs; primitive-labels.test.tsx fails when an update brings the registry's English back. - DataTable takes showSelectedCount, so a table without row selection hides "0 of N row(s) selected". - Add the page test harness and application locale coverage tests; the client routes test pins page grants by their authz resource id. - AGENTS.md and CLAUDE.md send UI work to ui-workflow.md, list the shipped primitives, and describe adding the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(templates): ship only the primitives Hub and Examples use Hub and Examples follow Default: primitives are added with the shadcn CLI when a page first needs one, and the shadcn/ui skill embedded in the development Skill replaces the reference gallery. - Remove client/pages/reference/ with its tests and use-mobile.ts from both templates. Hub keeps the twelve primitives its shell and compositions use; Examples also keeps badge, card, field, separator, skeleton, textarea, toggle and toggle-group for its example pages. - Drop recharts, cmdk, embla-carousel-react, input-otp, react-resizable-panels and @shadcn/react, which no template imports now, and add cn, which registry primitives import. - Carry over Default's toast close label, DataTable showSelectedCount, the page harness, locale coverage and primitive label tests, and the AGENTS.md and CLAUDE.md guidance, keeping each template's own route grants and wording. - Add the changeset for all three templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): name the primitives each template ships The frontend handbook said the template ships twelve primitives, which holds for Default and Hub but not for Examples. shadcn.md now lists the eight Examples adds, and frontend-dev.md and example.md refer to the primitives the shell uses instead of a count. Add the changeset for the embedded shadcn/ui skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): open records in place, add the back button and the dashboard template A review of a customer-service application built from the handbook found records opening over the wrong page, a row's Edit opening the drawer as well, a back button among the page actions, an unaligned table in a card and unsortable date columns. The handbook now says a record opens over the page the user is on (I9), declares the drawer under every page that opens it, gives a row's Edit its own route, puts BackButton above the title (L6), makes date and number columns sortable (T1.8), and adds a dashboard template (T5) with a worked example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(templates): add BackButton and keep the page count on one line BackButton is the way back from a page below another one: a link above the heading that leads to the parent route with the query string and replaces the history entry, as closing a route overlay does. It stands on its own, without PageHeader. DataTablePagination gives the page count a minimum width, so the Chinese label no longer wraps. RouteChildPage and the UI Library describe leaving by the back link instead of breadcrumbs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): show the success path in the unsaved-changes example A generated application hit this exact bug: the edit dialog's save cleared neither the dirty flag that beforeClose reads nor the form's report, so closing after a successful save asked to discard the just saved changes. The overlay.md section 2.5 example now carries the whole flow: a stable onDirtyChange callback, an EditProjectBody whose onSubmitted clears the flag before onSaved and close, and the imports that go with it. The frontend handbook's Common mistakes list gains the matching entry. * fix(templates): keep the Switch and pie chart in proportion in the compact theme * docs(app-skills): compare the whole design at acceptance and require consistent navigation icons The acceptance review's D classification was scoped to the design's numbered criteria, so a design's structural declarations — routes, navigation entries and their icons, permissions, the component list and states — were only compared with the code when they happened to be criteria. A trial feature lost two menu icons this way and the acceptance pass did not catch it. D now compares the whole design file with the code; the design template requires every declaration in sections 2–4 to map to a criterion or be marked explicitly as not a criterion; and the review checklist plus page.md require navigation icons to be consistent within a group. * docs(app-skills): open a record's detail under the page that opens it I9 covered drawers and dialogs only, so a page that referenced a record whose detail is a page (a payment list's expense number) linked into the other module's route: the page the user was on disappeared, the menu highlight moved, and Back landed elsewhere. The page version of "the same drawer over another page" was undocumented, and page.md told every record page to hard-code `to`, which blocks reusing one page module under several parents. I9 now covers a covering child page as well, child-routes.md gains "The same detail page over another page", page.md limits `to` to a page with one parent, and the common-mistakes entry names the detail page next to the overlay URL. * fix(templates): make BackButton a muted text link * refactor(templates): group the DataTable components in one directory * docs(app-skills): show a cut-off table cell's full content on hover Guideline T1.11 requires a table cell that is too narrow for its content to show the full content on hover. table.md gives the pattern: the cut-off content in a Popover whose trigger opens on hover, since a Tooltip does not work well here. A cell does not wrap and the table widens to fit it, so the pattern applies only where a column limits its width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app-skills): treat the worked example as a reference, not code to copy Agents copied the projects example verbatim, carrying its names, fields and fixed values into features they did not fit. The frontend handbook now says the examples show the rules rather than the feature to build, and names the projects-domain values to decide again. example.md and styling.md no longer tell an agent to copy the files a task depends on; only session-expired-alert.tsx and use-url-search.ts, which are shared infrastructure, are still copied unchanged. The acceptance review also checks the other direction of design conformance: names and copy keys from the projects domain, fields, columns, filters or actions the design does not declare, and example values that do not fit the feature's data are reported as D issues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app-skills): end a cut-off cell in an ellipsis and reveal it in a tooltip A generated articles table clipped long titles mid-character and showed "release-notes" as "elease-note": the title used line-clamp in a cell that never wraps, and the tags put truncate on Badge, a flex element that shows no ellipsis and loses both ends of its centered text. Guideline T1.11 now requires the ellipsis and a reveal on hover only when the content is cut off. table.md shows text and links, the name column included, in a Tooltip whose onOpenChange cancels it when the value fits; the tooltip trigger leaves a Link a link, which a Popover trigger would turn into a button. Styled content such as a Badge stays in a Popover, since a badge is unreadable on the inverted tooltip. It also says where truncate works and that line-clamp does not belong in a cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(templates): line DataTable up with a card by itself A generated analytics page put a DataTable in a CardContent with px-0, as the handbook's "Table in a card" said, but not the cell padding that went with it. Its own border then sat on the card's edge, a strip was left below it, and the text started to the left of the card's title. The rule lived in four places and was still only half applied, because it asked the agent to pick a different component and hand-write selector classes. DataTable now does it: inside a CardContent it drops its frame, reaches the card's edges and pads its first and last cells with --card-spacing, and a CardContent still given px-0 keeps working. The handbook builds a card's short list from DataTable with plain headers and no pagination, the dashboard example uses it, and guideline T1 says it covers list pages while a short list in a card follows T5.3, which is where the design had picked up sorting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app-skills): record every review checklist item in the review record Both reviews of a generated content application were non-independent, since the environment could not start subagents, and neither checked T5.3: the design review raised only B1-B4 and S1-S4, and the acceptance review widened a clipped card without asking why the table sorted. The prompts already said to go through the checklist item by item, but the record kept only the items that failed, so a skipped item looked the same as a passed one. The review record now has a "Review checklist" section with one row per checklist item: pass, fail with the issue it raised, or not applicable with the reason. Both prompts and the checklist's own introduction say to fill it in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app-skills): validate forms on submit, not as a field loses focus A field error flashed for a frame whenever a dialog was closed without saving. Guideline T3.3 validated a field when it lost focus, so the handbook and its examples set useForm's mode to 'onTouched'; clicking Cancel, the close button or the backdrop takes the focus off the field first, and the error rendered while the dialog was fading out. T3.3 now validates on submit, and a form closed without submitting shows no error. The handbook and the four examples leave mode at the default 'onSubmit', and it explains why the blur-based modes do not fit. After a failed submission a field is still revalidated as it changes, so its error clears once it is corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(templates): let a child page inside another one cover it whole A covering page under a tab of a record's page can only render through the tab's outlet, inside the record page's RouteChildPage. That layer both positioned and scrolled, so the nested layer resolved inset-0 against it and scrolled out of sight with its content: in the recruitment application, after the candidate page had been scrolled 292px, the offer page opened at top -240 and showed only its footer over the candidate's details. RouteChildPage is now two elements, the way the layout's content area is: the outer one positions and never scrolls, the inner one scrolls, with overscroll-contain so scrolling stops at the layer. A layer rendered inside another one anchors to that layer's outer element and covers it whole. Inside another layer it also makes everything around it inert up to that layer's scrolling element — the record page's back link, header and tab bar — not only its own siblings. The layer carries data-slot='route-child-page'; the props are unchanged. The three templates and the UI Library registry item carry the same file. The templates' AGENTS.md add that an overlay a page's header opens on a page with tabs is declared under every tab and linked through the current tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(app-skills): open a page header's overlays over the tab being shown A review of the recruitment application found a record page whose header opened its edit dialog at /candidates/80/edit while the user was on /candidates/80/interviews. The handbook said a record opens over the page the user is on, but not that on a page with tabs that is the tab, and its two examples — the drawer with its stacked edit dialog, and the record page with tabs — never met. A link with a bare `edit` resolves against the route that renders it, not the URL on screen, and the header belongs to the page's route: the dialog replaced the tab in the page's outlet, and closing it went to the page's URL, which redirected to the default tab. - Guidelines T2.1, I1, I6 and I9 define the view the user is on, the tab included, and I9 gives the test: an overlay's URL minus its own segments is the URL it was opened from, and closing returns there with no redirect. - child-routes.md declares the header's overlays under every tab through customerHeaderRoutes(tab) inside customerDetailRoutes(owner), links the header through the tab read from the URL, and has each tab pass the page's context on to its outlet. page.md explains what a relative link resolves against, with a table of the cases. - Going back restores the page underneath exactly (L6): links carry its whole query string down, a view below another one names its own parameters apart (table.md), and the way back removes them with withoutParams (example/url-search.md). child-routes.md no longer advises dropping the current page's filters when linking to another page's record, which lost them on the way back. - example/detail-page-tabs.md is the complete page, a tab, a test that opens the header's "Edit" from every tab and goes back, and the route test helper; its code was compiled, linted and tested inside the default template. - The review checklist, overlay.md, testing.md, frontend-dev.md and the run and design templates check overlays opened from a view other than the first one a page shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(templates): add DataTable and DatePicker from the UI Library on demand The templates no longer ship DataTable or DatePicker, or the calendar, select and table primitives only they used. Both are UI Library items now, added with `shadcn add @nocobase/data-table` or `@nocobase/date-picker` when a page first needs one. Examples keeps select and table for its own pages, and Hub keeps react-day-picker, which app-plugin-hub requires as a peer. The dataTable and datePicker locale keys stay, so an item added later is translated at once. - ui-library publishes data-table: DataTable and its column header, pagination and view menu as one item of four files under client/components/data-table/, with a preview demo and the dropdown-menu, select and table primitives it renders with. - date-time-picker declares the calendar, popover and react-day-picker that the date-picker.tsx it installs imports; without calendar in the templates, installing it alone no longer compiled. - ui-library gets Vitest. The DataTable and DatePicker tests move there from the templates and render the registry sources against the preview's primitives; Quality runs them on every pull request and the UI Library workflow before it publishes. - The development Skill adds the items on the worked example's Add first lines and says which of the CLI's package.json changes to undo; the upgrade Skill covers an application that still has the old copies. - The pending changesets that described a data-table directory in the templates now describe what ships. * refactor(templates): drop the unused Typography components client/components/typography.tsx held the class strings from the shadcn Typography guide for hand-written long-form text. Nothing in the templates or the plugins imported it, and article content usually comes from Markdown or rich text, which these components cannot style. The templates' AGENTS.md and the development Skill no longer list it. * feat(ui-library): publish BackButton and keep it preinstalled in the templates Make the UI Library the source of truth for BackButton, as it already is for PageHeader and the route overlays. - ui-library: add the back-button component item (back-button.tsx, its registry entry, a Navigation preview demo, and a test that renders it in the strict runtime built from the components README), document it in the components README with its navigation.back key, and use it in the RouteChildPage demo instead of a hand-written back link. - templates: keep client/components/back-button.tsx as an exact copy, checked by tests/scripts/template-ui-library.test.mjs, and name BackButton among the UI Library components in AGENTS.md. The component itself is unchanged, and each template keeps its own back-button test, which is the one check that the template's own locales carry the key. - MAINTAINING.md and USAGE.md list back-button among the preinstalled components, and MAINTAINING.md records navigation.back as the one exception to item-prefixed keys: it predates the item and pairs with the templates' navigation.breadcrumb. - The development Skill's styling reference now says which composed components are preinstalled from the UI Library and which are the template's own, instead of saying the CLI cannot add any of them, and the plugin Skill lists back-button among the items a plugin copies. * feat(ui-library): add a ConfirmDialog item built on alert-dialog ConfirmDialog confirms one action in an alert dialog the page opens from its own state. It runs the action: while a returned promise is pending the confirm button shows a spinner and holds the focus, both buttons are disabled and Escape is ignored; resolving closes the dialog with onOpenChange(false, 'confirm'), and a rejection or a returned { error, retryable } keeps it open with the error inside it. Cancel holds the initial focus, and focusAfterConfirm names where focus goes after a success, when the opener has often gone with its record. A result is reported to the onOpenChange of the latest render, and one that arrives after the caller closed or unmounted the dialog is dropped, so a handler whose close meant cancel is not called with stale state. A ref, not render state, keeps two clicks from running the action twice. The preview gains the alert-dialog and spinner primitives, a demo with a slow, a failing and a refused delete, and the item's tests render it in the strict i18n runtime built from the README's Translations table. * docs(app-skills): fix the frontend guidance the PR review found wrong - The toast examples in complete-button.md and public-switch.md, and the 401 rule in api.md, used actionProps, left over from the Base UI toast API; the toaster's option is action: { label, onClick }. - ui-workflow.md's development step still said to copy the worked example, as did frontend-dev.md and i18n.md in passing; they now say to write the feature's files from its design and copy only what a document calls shared infrastructure. i18n.md no longer lists the extra shared keys twice with different wording. - The customer page test in detail-page-tabs.md assumed a mocked i18n client and queried keys; it renders in the strict test runtime with the application's locale files and queries the wording. The document also says detail/edit.tsx has to be written before the test imports it, with the customers.form keys copy.md does not list, and no longer claims the test checks what only the route test can. - project-form.md and list-page.md name the select primitive they import, and server-table.md says what it needs and names data-table/index.tsx. - The end-to-end guidance and the screenshot tool say what a Hub application sets up first, since the Hub template has no Playwright. - shadcn.md no longer claims the application installs CLI 4.21.0. - The docs site's child page example leaves by BackButton, says why a deeper outlet sits beside the layer as the component now behaves, and its shadcn add example no longer offers to overwrite the translated dialog primitive. - form.md's create page is the frame to follow, not to copy. * docs(app-skills): require RouteChildPage for a child route that is a page of its own A generated application declared its article editor as a child route of a dashboard and of a list and returned a bare PageContainer, so the editor rendered at the parent's Outlet below the dashboard instead of covering it. The handbook implied the rule without stating it. - child-routes.md, page.md, form.md and frontend-dev.md state that only tab content renders inline and a page of its own returns RouteChildPage, including a form page shared by several parents. - testing.md adds a covering child page test: the parent's heading sits in an inert element, which fails for a page rendered inline. - The template AGENTS.md files carry the rule, since they are always read. - The UI Library README and the docs site place the parent's Outlet at the end of its PageContainer, as the handbook does. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Skills
This directory holds every Skill this repository commits. pnpm install links each of them into .agents/skills/ and .claude/skills/, so agents working in the checkout see them; see "Repository Skills" in the root AGENTS.md.
| Skill | Who uses it | What it does |
|---|---|---|
nocobase-create-app |
Users starting a project to develop, installed globally | Creates an application with pnpm create @nocobase/app, configures it with nocobase config init, config set and config check, and starts it |
nocobase-app-installer |
Users installing built NocoBase where it runs — a Hub, or an application's archive — installed globally | Installs, upgrades, rolls back and checks a Hub from its template, or an application's deployment archive, with @nocobase/app-installer |
nocobase-plugin-development |
Contributors developing plugins in a NocoBase 3 source workspace, linked by this checkout or installed globally | Scaffolds, implements, registers and verifies a NocoBase 3 plugin |
The two global Skills split by what the user ends up with. A project — source code someone changes, developed locally — is nocobase-create-app. An installation — built NocoBase that only runs, usually in production — is nocobase-app-installer. A Hub is installed, not created: installing one goes to nocobase-app-installer unless the user will develop the Hub's own code, which is a nocobase-create-app project with --template=hub. An application goes from one to the other: it is created and developed as a project, its own nocobase-deployment Skill builds the archive, and nocobase-app-installer installs it.
The rest of this file is about the two Skills users install globally. nocobase-create-app is how an agent reaches NocoBase 3 before any application exists; everything it needs after that ships inside the application, under .agents/skills/, synchronized from the installed packages. nocobase-app-installer is for the server: it installs a deployment archive built in an application project, or a Hub whose source is not changed, and a server has no project and no application Skills, so this Skill stays in use for every later upgrade.
Each is used against the published packages, which is what users do, or against the unreleased checkout, published to a local npm registry, which is how a change is tested before it is released.
Where the packages come from
NocoBase 3 publishes its packages to https://npm.nocobase.ai, not to the public npm, where @nocobase/create-app and @nocobase/app-installer answer 404. Both Skills therefore name the registry on the command that fetches the first package, pnpm --registry=… create @nocobase/app and npx --registry=… @nocobase/app-installer, and nothing needs configuring beforehand. From there the tools carry the registry themselves: create-app installs from it and writes @nocobase:registry=https://npm.nocobase.ai/ into the new project's .npmrc, so a later pnpm add @nocobase/… inside the project resolves too, and app-installer does the same for every Hub release it builds. The user's own pnpm and npm configuration is not changed.
The Skills write the registry as ${NOCOBASE_REGISTRY:-https://npm.nocobase.ai}. NOCOBASE_REGISTRY is unset for users; pnpm unreleased:env sets it, which is what lets the same commands install the unreleased checkout.
nocobase-create-app
Requirements
- Node.js 24 or later and pnpm 11.
- An agent that loads Skills, such as Claude Code or Codex.
Install the Skill
npx skills add nocobase/nocobase3 --skill nocobase-create-app -g
--skill is required. The skills CLI reads this whole directory, and without it would offer every Skill here at once. The others install the same way with their own name, nocobase-plugin-development for an agent working in a fork or another checkout, which this checkout links already. Add -a claude-code, or another agent's name, to install for one agent only.
Agents load Skills when a session starts, so start a new session after installing.
Ask the agent
Open the agent in an empty directory and ask for an application, for example:
Create a NocoBase application in this directory with SQLite, start it, and tell me how to sign in.
The Skill then:
- Runs
pnpm --registry=… create @nocobase/app <name> --jsonfrom the parent directory, so the files land in the directory you opened. - Follows the
nextCommandsthat creation returns:pnpm nocobase config init, thenpnpm nocobase config setfor anyrequiredSettingsof a database other than SQLite, thenpnpm nocobase config check, and finallypnpm devin the background. - Reports the URL and the first sign-in account, and recommends starting a new session in the application directory, where the application's own Skills are loaded reliably. If you keep working in the same session, it reads the application's
AGENTS.mdand Skills directly instead.
It never asks for a database password in the conversation. For a database other than SQLite it asks you to put the password in an environment variable, then reads it with pnpm nocobase config set --from-env.
Without an agent
The Skill runs nothing you cannot run yourself:
PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 pnpm --registry=https://npm.nocobase.ai create @nocobase/app my-app
cd my-app
pnpm nocobase config init
pnpm nocobase config check
pnpm dev
PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 lets pnpm install versions published minutes ago. For another database, install its driver, name it, and fill in the connection before pnpm nocobase config check:
pnpm add @nocobase/db-postgres
pnpm nocobase config init --dialect postgres
pnpm nocobase config set database.connections.main.host=db.internal database.connections.main.username=crm
pnpm nocobase config set --from-env database.connections.main.password=CRM_DB_PASSWORD
nocobase-app-installer
Requirements
On the server that will run the application:
- Linux or macOS; on Windows, WSL.
- Node.js 24 or later, and pm2 4.3 or later installed globally with
npm install -g pm2. Building a Hub from its template also needs pnpm 11 or later. - An agent that loads Skills.
Install the Skill
npx skills add nocobase/nocobase3 --skill nocobase-app-installer -g
Ask the agent
On the server, with the archive copied over, ask for the application, for example:
Install the NocoBase application in /tmp/crm.tar.gz into /srv/nocobase/crm for https://apps.example.com.
Or ask for a Hub:
Install a NocoBase Hub in /srv/nocobase/hub for https://apps.example.com, with SQLite.
The Skill first settles whether app-installer is the right route: a Hub whose source will change is an application project for nocobase-create-app, and Docker is the documented alternative. It says how to build the archive for the server when there is none yet, checks Node.js and pm2 (and pnpm for a Hub), runs app-installer install --json, and reports the URL, the first sign-in account, the pm2 startup command for you to run with sudo, and what the reverse proxy needs.
Later, in the installation directory, ask it to check, upgrade or roll back. It relays what an upgrade or a rollback will stop and back up, and runs it only after you confirm. An upgrade that fails after switching rolls itself back, and the Skill reports what happened; an operation interrupted while the application is down is recovered with rollback.
Without an agent
npx --registry=https://npm.nocobase.ai @nocobase/app-installer install /srv/nocobase/crm --archive /tmp/crm.tar.gz --origin https://apps.example.com
npx --registry=https://npm.nocobase.ai @nocobase/app-installer install /srv/nocobase/hub --template hub --origin https://apps.example.com
upgrade, rollback and status take --dir with the installation directory; an archive installation upgrades with --archive and the new archive. The package README, packages/tools/app-installer/README.md, documents every flag, the directory layout and the exit codes.
Test against the unreleased checkout
Use this to test a change before it is released: a change to a Skill, or to a package it drives, such as create-app, app-installer, a template or app-cli. The checkout is published to a local npm registry on your machine, and a shell is pointed at it, so the Skills' unchanged commands install the unreleased code.
A global Skill is released by merging it into develop, while packages are released by release-beta. A change to a Skill that describes new package behavior therefore has to be tried here first, because the published packages cannot show whether it works.
Requirements
- Everything above, plus Docker, which runs the local npm registry.
- A checkout of this repository with dependencies installed.
1. Publish the checkout
From the repository root:
pnpm unreleased:prepare
This builds every publishable package, publishes it to a Verdaccio on http://127.0.0.1:4873/, and points each package's latest tag at the snapshot. Add --reset to replace a previous snapshot. The session lives under $TMPDIR/nocobase-unreleased-<id>/ and does not depend on which branch is checked out, so switching branches afterwards keeps it usable.
2. Link the Skill from the checkout
For Claude Code, link it, so that an edit to its SKILL.md reaches the next session without reinstalling:
ln -sfn "$PWD/skills/nocobase-create-app" ~/.claude/skills/nocobase-create-app
ln -sfn "$PWD/skills/nocobase-app-installer" ~/.claude/skills/nocobase-app-installer
The link follows the working tree, so it breaks while a branch without the Skill is checked out. For any agent, npx skills add ./skills/<name> -g installs a copy instead, which has to be repeated after every edit.
3. Point a shell at the snapshot
Open a new shell, and from the repository root:
eval "$(pnpm -s unreleased:env)"
pnpm config get @nocobase:registry
The second command must print http://127.0.0.1:4873/. -s keeps pnpm's own $ node … line out of what the shell evaluates.
unreleased:env prints the same variables unreleased:create and unreleased:smoke run with, including NOCOBASE_REGISTRY and a session-only store and cache. Setting a few of them by hand is not enough, and fails silently:
- A snapshot carries the same version numbers as the last release until one is cut. A package resolved from
https://npm.nocobase.ai/looks identical to pnpm, so a partly configured shell produces an application that mixes a new template with old packages, and nothing reports it. The symptoms are aconfig.ymlcreated beforeconfig initran, no.npmrc, and apnpm nocobasecommand reported as not found. pnpm config set @nocobase:registry …saves the scoped registry toauth.iniin pnpm's global configuration directory,~/Library/Preferences/pnpmon macOS.PNPM_CONFIG_USERCONFIGdoes not replace that file, and onlyXDG_CONFIG_HOMEmoves the directory, so the command sets it for the whole shell. Tools that keep their own settings there, such asgh, will not find them until you open a new shell.
If the shell uses an HTTP proxy, keep 127.0.0.1 in NO_PROXY so the local npm registry is reached directly.
4. Ask the agent
In the same shell, create an empty directory outside the repository and start the agent there:
mkdir -p ~/nb-skill-test/my-app && cd ~/nb-skill-test/my-app && claude
Ask exactly as in the published case. A snapshot install can be told apart from a published one:
- There is no
config.ymluntilpnpm nocobase config initruns. .npmrccontains@nocobase:registry=http://127.0.0.1:4873/.node_modules/@nocobase/app-cli/dist/commands/config/containsinit.js,check.jsandset.js.- An installation made by app-installer records
"registry": "http://127.0.0.1:4873"in itsinstaller.json. A Hub needs its port and the App Host port 13010 free; to keep an installation off your own pm2 processes, export a shortPM2_HOME, such as/tmp/nb-pm2, before starting the agent. An archive to install comes from an application created withpnpm unreleased:createand built there withpnpm build --tar.
To look at what the local npm registry serves, query it with curl. In a shell without the variables above, a scoped registry in your own configuration overrides npm view --registry, and the answer comes from https://npm.nocobase.ai/ instead:
curl -s http://127.0.0.1:4873/@nocobase%2fcreate-app
Worth covering when either global Skill changes, since each hands requests to the other:
- "Install NocoBase" or "try NocoBase" goes to
nocobase-create-app, which creates a project. - "Install a NocoBase Hub", on a laptop or a server, goes to
nocobase-app-installerwith--template hub. - "I want to change the Hub's code" goes to
nocobase-create-appwith--template=hub. - "Install NocoBase on this server" with no project or archive makes the agent ask what it is for before choosing a route.
Worth covering when nocobase-create-app changes:
- The default SQLite path, through to the page opening and the sign-in account being reported.
- A database other than SQLite, with the password supplied through
--from-env. - A directory that is not empty, which the Skill must refuse rather than overwrite.
- A directory that already holds an application, where the Skill must hand over to that application's
AGENTS.md. - With NocoBase 2 Skills also installed, the agent must choose this one and never run the
nbCLI.
Worth covering when nocobase-app-installer changes:
- A fresh install from an archive and from
--template hub, through to the URL, the sign-in account and thepm2 startupstep being reported. - An archive built for another machine, where the agent must relay the build command the error names rather than retry.
- An upgrade asked for without prior consent: the agent must relay the confirmation notes and wait before passing
--yes. - A request to customise the Hub's code, which the Skill must route to
nocobase-create-appwith--template=hub. - A directory holding
installer.json, where the agent must start withstatus.
Without an agent
pnpm unreleased:create my-app creates an application from the snapshot under ../nocobase-local-apps/, and pnpm unreleased:smoke runs the create-app smoke test against it. Both set up the environment themselves.
pnpm unreleased:installer-smoke runs the snapshot's @nocobase/app-installer against both sources through scripts/smoke-app-installer.mjs, the script the app-installer CI jobs run: it creates a default application, builds its archive, installs, upgrades and rolls it back, then does the same for a Hub from its template. --source archive or --source template runs one of them. It covers changes to the templates, app-cli, app-host and create-app that the CI jobs cannot see before a release. It needs pm2 on PATH and, for the Hub, the App Host port 13010 free, runs pm2 under its own PM2_HOME and stops it afterwards, and keeps the installations and their logs under the temporary directory it prints.
5. Clean up
rm ~/.claude/skills/nocobase-create-app ~/.claude/skills/nocobase-app-installer
pnpm unreleased:clean
unreleased:clean removes the local npm registry and its caches, not the applications or Hubs created from it.