Commit Graph
41 Commits
Author SHA1 Message Date
PiEgg c796cb909b feat(app-client)!: remove settings and dev route surfaces, Hub and AI employee (#10642) 2026-10-10 02:02:13 -04:00
YANG QIA 68d4febbcc feat(plugins): add the agents, projects, knowledge and releases plugins (#554)
* Add the agents, projects, knowledge and releases plugins

Four plugins for agent-run applications, the UI Library's agents and projects items, and the generated UI Library catalog of the application Skill with its check.

* Wait on the sent attachment's link as it renders last in the chat test

* Keep the demo chat's reply timers off window, so they outlive a test's environment quietly
2026-10-08 07:10:52 +08:00
PiEgg 0b933b39c3 feat: generate the OpenAPI document and serve Swagger UI (#533)
Generates an OpenAPI 3.1 document for every application at GET /api/swagger and serves Swagger UI at GET /api/swagger/docs to signed-in users and API keys. Every hand-written /api route is declared or explicitly hidden, data endpoints are documented per field, Better Auth's endpoints are merged, routes behind runtime dispatchers are registered generically, and pnpm openapi:check enforces the declarations in CI. All changed packages carry changesets.
2026-10-05 09:23:03 +08:00
chenos 463a7a8f38 feat(app-testing): test applications on any dialect, with a portability check and every server dialect in CI (#525)
* chore(db-testing): fail tests that choose a dialect outside the database packages

pnpm db-tests:check reads every test and test helper outside the database
packages and fails on a dialect package import, ':memory:', dialect: 'sqlite',
PRAGMA, sqlite_master or a trigger. A test whose subject is a dialect says so
with a db-test-portability marker and a reason; template tests, which ship to
users, are exempted in the script instead. The app-server, app-cli and
template tests still to be ported with @nocobase/app-testing are listed as
pending, and the check fails once an entry is no longer needed. It runs in
pnpm check and in the Quality workflow next to the dependency checks.

* feat(db-testing): select every package that tests with db-testing through test:db --all

* feat(db-testing): run db-testing tests on every server dialect

* feat(create-plugin): generate database tests on db-testing

* test(create-plugin): keep the generated-test assertion clear of the portability check

* fix(db-testing): clean up a partial create, drop Oracle test users before 23ai, and run one provisioner contract everywhere

`createSqlTestDatabaseProvisioner()` drops what earlier create statements made when a later one fails, since the caller never receives the drop() that would; a failing first statement is left alone, because the name may belong to something that predates the run.

The Oracle provisioner drops a test user through a PL/SQL block that ignores only ORA-01918, instead of `drop user if exists`, which only 23ai accepts, and grants the materialized view and synonym privileges a migration may need.

`describeTestDatabaseProvisioner()` treats `listProvisioned` as optional throughout and drops the stale-run database in a finally, so a failed assertion leaves nothing behind. PostgreSQL and MySQL run it in place of their own provisioner tests.

* chore(db-testing): report every stale exemption and catch the SQLite driver in the portability check

A listed file or a marker that no longer covers a violation is reported, and so is a file both marked and listed, so every kind of exemption only shrinks. New rules catch a `better-sqlite3` or `sqlite3` import, a knex `client` naming either, and a dialect package imported by a name built at run time; the PRAGMA rule matches SQL rather than prose that mentions it. The `config init` test's marker reads as a sentence.

* test: take the pending database tests' databases from @nocobase/db-testing

Fifteen of the eighteen test files the portability check still listed as pending no longer choose a dialect. app-server's collections artifact, doctor, provider and task-plan tests, its repository routes and app-cli's db command tests run on databases provisioned by @nocobase/db-testing, one per connection they name, instead of SQLite files and in-memory databases. The examples template's article, analytics, numeric, permission-seed and workflow tests do the same; its articles migration test is a describeMigration. The cases whose subject is SQLite itself move to files marked sqlite-only or dialect-specific: a dry run not creating a missing SQLite file, and the driver loaded for a configured dialect. The examples template's external CRM test stays on its SQLite stand-in and is exempted in the check, since a template's tests ship to users and carry no marker.

PENDING keeps only the three templates' app-server tests, which move with @nocobase/app-testing.

* fix(db): only read in a dry run of rollback and repair

A migrator's rollback and repair, and a seeder's repair, took the task lock, ensured the history table and, for rollback, upgraded legacy checksums before looking at dryRun. On a database no task had run on, `nocobase db rollback --dry-run` (and redo, which goes through it) and `db repair --dry-run` therefore created the history and lock tables while reporting that nothing changed. SQLite hid it: a dry run against a database file that does not exist yet takes a separate read-only path, and every server database exists.

A dry run now reads history the way history() does, treating a missing table as empty history, and takes no lock. The migrator and seeder integration suites assert that these dry runs create nothing on every dialect, and the app-cli dry-run test no longer tolerates bookkeeping tables.

* feat(app-cli): let a bound test command load a configuration file of its own

bindAppCommand() takes configPath and hands it to the application's runtime the way APP_CONFIG_FILE is, so a command's test can run against a configuration it wrote, such as one naming a test database.

* feat(app-plugin-authentication): sign in from other packages' tests through a ./testing entry

signIn() signs in through the application's own sign-in route and returns a session whose fetch sends requests as that user, so a test can exercise the real authentication and authorization chain instead of an x-test-user header or a hand-written authenticationToken. DEFAULT_ADMIN_CREDENTIALS names the administrator the default seed creates.

* fix(db): keep in-process task lock claims per connection, not per connection name

Two Database Managers in one process each have a connection called main on databases of their own; one migrating while the other did failed with TaskLockBusyError although they never shared a database. The claims are now kept by the Database Connection a migration connection was made from, so the same connection still fails fast while another one under the same name proceeds; managers on the same database still take turns through the lock row. db-testing drops the queue that ran its test databases' migrations and seeds one after another to avoid the error.

* feat(app-testing): start an application on test databases of its own

@nocobase/app-testing/server's createTestApp() starts an application through its own standalone server on databases provisioned by @nocobase/db-testing, through the configuration file a deployment's own config.yml goes through, and installs it on start; createAppTest() is the Vitest fixture around it. ./cli carries @nocobase/app-cli/testing and bindTestAppCommand(), which points the application a command opens at a test configuration. Both re-export the layer below, so an application's or a plugin's tests depend on this package alone.

* test(templates): run the application tests on app-testing and check authorization through a real session

The three templates' application server tests start the template on test databases written by createTestAppConfig() instead of SQLite files, and the examples template's database tests take their API from @nocobase/app-testing/server, which every template now depends on for development. The portability check's pending list is empty and goes away; a template's SQLite-only test stays in EXEMPT.

The examples template signs in through its own sign-in route as the administrator and as a seeded proposal engineer, and checks that the sales confidentiality restriction leaves confidential quotes out of the engineer's list and in the administrator's, and that they appear once the restriction is no longer assigned. The nocobase-app-development Skill and AGENTS.md describe testing through the whole application.

* test: take plugin and example test fixtures from @nocobase/app-testing alone

Plugins, examples and the create-plugin generator import database fixtures
from @nocobase/app-testing/server and the command runner from
@nocobase/app-testing/cli instead of @nocobase/db-testing and
@nocobase/app-cli/testing, and depend on @nocobase/app-testing alone.
The database test portability check now fails when a test in a plugin, an
example or a template imports either lower layer directly; libraries and
tools keep using them. AGENTS.md, the db-testing README and the
app-development commands reference describe the single entry.

* fix: select app-testing users in test:db --all and keep each Skill's test entry its own

`pnpm test:db <dialect> --all` selected packages by a declared
@nocobase/db-testing, so once plugins, examples and templates moved to
@nocobase/app-testing the nightly Database tests workflow would have
stopped running them on PostgreSQL and MySQL. It now selects packages that
are or declare either one.

The command-authoring block shared by the application and plugin Skills
ended with the Test section, which now names a different runner in each:
@nocobase/app-testing/cli for an application, and the released
@nocobase/app-cli/testing in the global plugin Skill. The section moves
after the shared block in both.
2026-10-03 10:38:59 -04:00
chenos be0fbbd9ca feat(db-testing): portable database tests for packages outside db, with pnpm test:db (#523)
* feat(db-testing): portable database fixtures for tests outside the db packages

Add @nocobase/db-testing: tests run on in-memory SQLite by default and on
the dialect NOCOBASE_TEST_DB_DIALECT names otherwise, each file isolated in
databases of its own. createDatabaseTest() gives a Vitest test with migrated
databases, describeMigration() applies, rolls back and reapplies a migration
with metadata and table checks, and expectCollection() asserts on the schema
by Field and Collection name.

Dialect packages own provisioning: @nocobase/db/testing defines
TestDatabaseProvisioner, and db-sqlite, db-postgres and db-mysql export one
from a ./testing entry, with integration tests for the server dialects.

The scheduler plugin's migration tests move to describeMigration and no
longer use PRAGMA.

* chore(changeset): cover the scheduler's new development dependency

* chore(db-testing): keep the build output out of the format check

* fix(db-testing): leave the primary key's own index out of a Collection snapshot

PostgreSQL reports the index behind a primary key as a unique index of its
own, SQLite does not, so the same migration produced different snapshots.
The package's own tests also asserted the SQLite dialect by name; they now
compare against the selected one and pass on PostgreSQL and MySQL.

* fix(db-mysql): default test connections to the examples template's MySQL service

The examples application's docker-compose.yml publishes MySQL on 3306 and
reads MYSQL_PORT, as the provisioner does; defaulting to 13306, which only
the integration adapter used, meant starting that service was not enough to
test on MySQL. The db-testing README points at the compose file.

* feat(db-testing): remove the isolated databases an interrupted run left behind

Isolated databases are now named nbt_<host>_<pid>_..., and the first
provisioning in a process drops every nbt_ database carrying this machine's
host tag whose process no longer exists. Other machines' databases on a
shared server and those of runs still in progress are left alone, and a
failure to clean up only warns.

TestDatabaseProvisioner gains optional listProvisioned and dropProvisioned,
implemented by PostgreSQL and MySQL and covered by their integration tests.
provisionTestDatabases() accepts a provisioner directly.

* test(notification, workflow): run the existing dialect tests through db-testing

The notification migration-runner test read NOTIFICATION_MIGRATION_DIALECT,
which no script set, so it never ran; the workflow integration tests read
INTEGRATION_DB_CONNECTIONS and built their own connections. Both now take
their database from @nocobase/db-testing and run on SQLite by default and on
whatever NOCOBASE_TEST_DB_DIALECT selects.

The workflow tests pass on PostgreSQL and MySQL, including the MySQL-only
connection time zone test. The notification test passes on SQLite and MySQL;
on PostgreSQL its rollback fails because the idempotency migration's down
drops a partial unique index as a constraint, a released defect left for a
follow-up in @nocobase/db.

expectCollection().toHaveField() also compares a string Field's length.

* feat(db-testing): read the selected dialect's capabilities before any database exists

testDatabaseCapabilities() resolves them from the capabilities the dialect's
driver declares, which TestDatabaseProvisioner now carries, merged with the
defaults @nocobase/db applies. A test file awaits it at the top level and
passes it to test.skipIf(), so skipping is decided while tests are collected;
a test checks the result against what a live connection reports.

This replaces the planned Vitest plugin, whose only remaining purpose was to
hand capabilities to workers through provide()/inject().

The workflow plugin's Vitest alias for @nocobase/db matches the bare
specifier only, so it no longer rewrites @nocobase/db/testing.

* feat(db-testing): run packages' tests on a dialect with pnpm test:db

pnpm test:db <dialect> --filter <package> [-- <test arguments>] starts the
dialect from its package's Compose file in a disposable project with a random
port, runs the filtered packages' test scripts one after another with
NOCOBASE_TEST_DB_DIALECT and the service address set, and removes the project.
SQLite starts nothing.

@nocobase/db-testkit's runner splits the service lifecycle from the command
into runWithDatabaseService(), which runDatabaseIntegration() now uses. Each
server dialect declares its Compose service once, in
scripts/integration-service.ts, read by both its integration suite and
pnpm test:db.

* chore(changeset): cover the dialect packages' new integration service scripts

* feat(db-sqlite): provision SQLite test databases as files

An in-memory database is private to one Database Manager, so a test that
restarts an application on the database the first start seeded, or opens a
second connection to it, found it empty on SQLite while it worked on every
server. Each isolated database is now a file under
NOCOBASE_TEST_DB_SQLITE_DIRECTORY or the system's temporary directory, and
SQLite implements listProvisioned/dropProvisioned so an interrupted run's
files are removed too. The workflow and ai-employee suites run as fast as
on :memory:.

* fix(test-db): run every filtered package even after one fails

pnpm stops a recursive run at the first failing package, which hid the
results of every package after it on the dialect under test.

* test(plugins, examples): take test databases from db-testing

The database tests of 21 plugin and example packages no longer configure an
in-memory SQLite database. Helpers and test files get their databases from
createTestDatabase(), provisionTestDatabases() or createDatabaseTest() and
destroy them afterwards; PRAGMA and sqlite_master assertions are written
with expectCollection() against Field and Collection names; migration up and
down tests use describeMigration(); the SQLite triggers that made a write
fail in hub and authentication are replaced by spies on the write. Each
package drops its @nocobase/db-sqlite development dependency.

The departments example's seed replay test reopens the database the first
start seeded, which the file-backed SQLite provisioner now allows. All 21
packages pass on SQLite; running them on PostgreSQL and MySQL exposes
failures that are investigated separately.

* fix(departments-example): fit restriction rule assignment ids in their column

The organization seed built assignment ids up to 69 characters long for a
64-character column, so seeding failed on PostgreSQL and MySQL and the
application did not start. The shared example-public- prefix of the rule key
is left out of the id; the seed matches existing assignments by rule and
subject, so an installation seeded on SQLite is unaffected.

* fix(authentication, workflow): search regardless of case on every database

User administration's search and the workflow list query used the default
string mode, which follows the database's comparison: case-insensitive on
SQLite and MySQL, case-sensitive on PostgreSQL. Both pass
{ mode: 'insensitive' } now, as their in-memory matching already did.

* test(repository-example, notification): state the portable contract on every dialect

AVG results keep each database's decimal places, as @nocobase/db documents,
so the repository example compares them as exact decimals rather than as
SQLite's strings. A unique constraint that SQLite builds as an index is
reported under constraints only on PostgreSQL and MySQL, so the notification
and in-app notification migration tests look for it there.

* test(hub, ai-employee): give tests the schema an application has on every dialect

PostgreSQL and MySQL check what SQLite never did, and the tests relied on it:

- hub's authorization and role-permission tests now apply the api-keys
  migrations an application runs before hub's, whose foreign key needs
  the apikey table; ai-employee's tests apply authentication's, whose
  user table its conversations and usage events reference.
- hub's service tests destroyed their database only when every teardown
  step succeeded, leaking one per test until MySQL refused connections.
- the concurrent administrator deletion test boots UsersProvider, whose
  user subject is what makes a deleted user stop counting as active.
- ai-employee fixtures use a UUID where the column is a uuid, a numeric
  id generator for bigint keys, and compare decimals by their digits
  rather than SQLite's representation.

* fix(db): bind compared temporal and boolean values in update and delete predicates

A select's where clause encoded a value compared with a boolean Field by the
Field's type, but temporal Fields were not encoded anywhere, and an update's
or delete's where clause had no Collection in scope, so neither was encoded
there. MySQL rejected an ISO instant compared with a DATETIME column, which
broke notification delivery leasing, although the same string was accepted
as a written value.

Comparisons and between bounds on temporal and boolean Fields are now bound
like writes, and update and delete queries resolve their Collection to do it.
A shared contract test covers it on every dialect.

* fix(ai-employee): let the repository generate tool message ids

The message store gave tool messages and interrupted tool replies random
UUIDs for their bigint id columns. SQLite stored them; PostgreSQL and MySQL
rejected the insert, so every agent tool call failed to save there. The
repository already generates these ids from the application's id generator
when none is given.

* fix(hub): fit the release artifact key's unique index within MySQL's limit

artifactKey was varchar(1024) with a unique index, beyond MySQL's 3072-byte
key limit under utf8mb4, so the plugin's tables could not be created on
MySQL. It is now varchar(512); keys are at most 172 characters.

The released migration is edited deliberately: the failing statement is in
it, so no later migration could get a MySQL installation past it, and it
never succeeded there. Installations that already ran it keep the wider
column and see a checksum warning that db repair clears.

* fix(db): drop a partial unique constraint as the index it is

A unique constraint with a predicate exists only as a partial unique index,
but dropConstraint compiled it to ALTER TABLE ... DROP CONSTRAINT, which
PostgreSQL refuses, so notification's idempotency migration could not be
rolled back there. The compiler now drops it as an index, recognising it by
its predicate or, in a definition resolved from the database, by an index of
the same name. Covered by a unit test and by the shared partial-unique
contract on every dialect that supports partial indexes.

* fix(db-mysql): run transactions at READ COMMITTED

Under MySQL's REPEATABLE READ default, a transaction's snapshot is taken at
its first read, and every transaction starts by reading Collection metadata,
so a check made after taking a lock still counted rows a concurrent
transaction had removed: two Hub administrators could delete each other.
Each pooled connection now sets its session to READ COMMITTED, the level
PostgreSQL, SQL Server, Oracle and OceanBase default to.

* test(hub): register the user subject in the role scope tests

Without UsersProvider's user subject every assignment counts as active, so
whether a disabled administrator still protected the last one depended on
which of the two concurrent transactions took the lock first; MySQL ordered
them the other way and both succeeded.

* fix(db): encode compared temporal values only where a stored value is meant, and share the SQL test provisioner

Comparisons with a temporal Field ran the write-side validator on every operator, so a `like` pattern, a date without a time or an ISO string without seconds threw a RepositoryError instead of reaching the database as before. Only `= != <> < <= > >= in not in` encode the value now, and a string that is not a complete V1 literal is bound as given. The shared mutation contract test covers a date-only comparison and a `like` pattern.

Update, delete and insert resolve the Collection once instead of looking it up for the write fields and again for the comparison scope.

`@nocobase/db/testing` exports `createSqlTestDatabaseProvisioner()`; the PostgreSQL and MySQL provisioners declare only their connection options and three statements, and every administrative statement runs on a connection opened for it and closed afterwards.

* fix(db-testing): isolate concurrent tests, empty supplied metadata stores, and check rolled-back schemas field by field

A test running concurrently with its siblings gets databases of its own instead of resetting the file's shared database under them; migrations and seeds of every test database in one process run one after another, because the runners' in-process lock is keyed by connection name rather than by database. `reset()` empties a metadata store the caller supplied, whose Collection documents otherwise outlived their tables. `verifyMigration` compares every table's Fields, indexes and foreign keys after rollback, not only the list of tables.

Vitest becomes an optional peer that only the `./vitest` entry needs, exported constants carry explicit types, and a dialect package that is installed but predates its `./testing` entry is told to upgrade rather than to be added. The scheduler's foreign key test asserts `onDelete: 'restrict'` again, and AGENTS.md records the one exception to never editing a released migration.

* test(db-testkit): assert pass-through comparisons only where the database understands them

The date-only comparison and the `like` pattern on a temporal Field describe what a database does with a value the Field could not store, which is the database's own business: Oracle's session timestamp format refuses a date without a time, and the PostgreSQL family has no `like` operator for a timestamp. The contract is that the value reaches the database as given, so each assertion runs on the dialects whose behaviour it describes.

* test(workflow): give the tests that compile workflows room on a loaded runner
2026-10-03 10:17:12 -04:00
chenos 46ce11fabb refactor!: read client runtime values from the server-rendered config and build path-free clients (#455)
* refactor(app-client): read the mount path from the server-rendered config

resolveAppBase and resolveAppUrl now read app.basePath from the client
configuration the server renders into the page, and throw when the page
carries none, instead of reading window.APP_BASE_PATH or falling back to
the bundler's base. The router basename comes from the same value, so
AppRuntimeDefinition.basename is removed and the templates no longer take
it from the deprecated portal SDK.

The AI employee plugin no longer builds an API client when its modules
load. Its service functions take the ApiClient as their first, required
argument, and pages use the new useAIEmployeeClient() hook, which binds
them to the application's client. The registry's NocoBaseAIService now
requires its client, and the nocobaseAIService singleton is gone.

The templates render a config block in their test setup, as the server
would, and drop the unused assetUrl helper.

* refactor(app-server): stop putting runtime values on window

The SPA routes render only the lang attribute and the client
configuration block. window.APP_BASE_PATH, window.NOCOBASE_API_URL and
the window.__nocobase_api_client_* storage settings are no longer
injected: the client reads the mount path and API URL from the
configuration, and nothing read the storage settings except the portal
SDK's unused AuthSession. SpaConfig.runtime, createNocoBaseSpaRuntimeGlobals,
injectSpaRuntimeGlobals and the SpaRuntimeGlobals types are removed, and
runtime-globals.ts becomes runtime-html.ts.

The templates drop the matching spa.runtime defaults and the Vite
defines that baked NOCOBASE_API_URL, NOCOBASE_AUTHENTICATOR and the
NOCOBASE_WS_* values into the client; pnpm dev no longer sets
NOCOBASE_API_URL for its child processes.

* refactor!: remove the portal SDK and rename the portal presets

@nocobase/app-portal-sdk is deleted. Its runtime helpers were already
deprecated, and after the client stopped reading window globals the
templates used nothing from it; its client and routing entry points had
no users in this repository. The peer-dependency list, the registry
dependency check, the root lint configuration and AGENTS.md drop it.

The dev-config presets that still carried the Portal name are renamed:
vite/portal with createPortalViteConfig becomes vite/app with
createAppViteConfig, and the ESLint createPortalConfig becomes
createApplicationConfig. Browser code is now linted against reading
import.meta.env beyond PROD, DEV and MODE.

The templates no longer define __PORTAL_TEMPLATE_NAME__,
__PORTAL_TEMPLATE_VERSION__ or the unread __PORTAL_DEV_SOURCE_ROOT__ at
build time. The server publishes the application's displayName and
version from its package.json as app.displayName and app.version in the
public client configuration, and the sidebar footer reads them there.
The bootstrap loading classes become app-bootstrap-*.

* feat!: build path-free clients and choose the mount path at run time

The shared Vite preset builds with a relative base, so every chunk,
preload dependency and asset resolves against the module that references
it and one build can be mounted at any path. renderBuiltUrl and its
window.APP_BASE_PATH expression are gone. The development server still
needs an absolute base and takes it from APP_BASE_PATH, which pnpm dev
now resolves once and passes to both children; it refuses to start
without one. The templates no longer set a base.

The server points the remaining ./assets/ URLs in index.html, meta
content included, at the path it is mounted at when it renders the page.

pnpm build records nocobase.relocatable instead of nocobase.basePath and
no longer copies APP_BASE_PATH into dist/.env, so the deployment chooses
the mount path. DEFAULT_APP_BASE_PATH in @nocobase/app-server/support is
the only place /main is spelled.

In PROXY_TARGET_URL mode no local server renders the page, so a
development plugin from @nocobase/app-cli/dev/proxy takes the client
configuration from the remote page and substitutes the local mount path
and API URL.

* feat!: choose the mount path when installing or running a build

app-installer takes --base-path and keeps the mount path in app.env, so one
relocatable archive installs anywhere and moves by editing APP_BASE_PATH.
An archive from before relocatable builds records the path its client was
compiled for and is only installed, upgraded to or rolled back to there.

The Hub refuses such an archive unless it was built for /<appId>. The
template Dockerfiles no longer take APP_BASE_PATH as a build argument; the
image defaults to /main (/hub for the Hub) and -e APP_BASE_PATH moves it.
The deployment Skill and documentation drop the build-time mount path.

* chore: add changesets and upgrade steps for the client runtime configuration

The removed @nocobase/app-portal-sdk is dropped from the consumed changesets
in .changeset/pre so the next prerelease run can still read them, and the
one that named only it is deleted. The nocobase-app-upgrade Skill's edge
cases gain the steps an existing application takes to move to the
server-rendered client configuration and relocatable builds.

* ci(smoke): give the deployed archive its mount path at run time

A build no longer copies APP_BASE_PATH into dist/.env, so the extracted
archive started at the server's /main default. The Hub serves at /hub, and
the smoke waited there until it timed out. Pass the path the checkout
served, as a deployment does.

* fix: resolve review findings on client runtime configuration

- app-server rewrites every `./` URL in `index.html` to the mount path,
  not only `./assets/`: a relative base writes every `public/` file the
  page names as `./<file>`, and a deep route resolved those elsewhere.
- app-installer keeps `/hub` for a Hub archive installed without
  `--base-path`; a build records no path and ships no `.env`, so the
  server's `/main` default moved it. Adds a test for `--base-path /`.
- The proxy-mode client configuration fetch reports the status it met
  and a redirect to another origin instead of blaming the URL.
- `createDevProxy` and `createDevClientConfigPlugin` share the local
  prefix computation; the JSON escaping stays local, with the reason.
- The Vite preset README says why serve-mode resolution outside
  `pnpm dev` needs `APP_BASE_PATH` and how a build is previewed.
2026-09-27 06:52:43 -07:00
chenos a4ee8aa542 feat(app-installer): generalise hub-installer into app-installer for deployment archives (#451)
* feat(app-server): keep standalone storage under APP_STORAGE_DIR and record build metadata

A standalone application reads APP_STORAGE_DIR for its storage directory, so a
deployment whose code root is replaced per release keeps one storage directory.
The Hub template still honours HUB_STORAGE_DIR when APP_STORAGE_DIR is unset.

pnpm build records nocobase.basePath and nocobase.builtAt in dist/package.json,
and the templates' ecosystem.config.js derives the pm2 process name from the
application's package name, overridable with APP_PM2_NAME.

* refactor(app-installer): rename @nocobase/hub-installer to @nocobase/app-installer

A pure rename ahead of generalising the installer beyond the Hub: the package
directory, name and bin, its unreleased changesets, the smoke script and its
workflow, and pnpm unreleased:hub-smoke, now unreleased:installer-smoke. The
package was never published, so nothing outside this repository refers to the
old name. Behaviour is unchanged.

* refactor(app-installer): generalise the installer's layout, state and backups

Releases are named by version and build time (releases/<version>_<time>/app),
so two builds of one version are two releases; upgrade --rebuild becomes an
ordinary upgrade to a fresh build, which removes the staging and swap logic.
installer.json records the application name, base path and source. The runtime
variables move to app.env with APP_STORAGE_DIR (and HUB_STORAGE_DIR for Hub
releases published before it), health and URLs follow the release's base path,
and backups cover every SQLite database config.yml declares. Messages name the
application rather than the Hub, and the pm2 name defaults to the directory.

* feat(app-installer): install and upgrade from a deployment archive

install takes --archive <dist.tar.gz> or --template hub[@version], one of them
and only one. An archive is unpacked with the tar library pnpm build packs it
with, so a server needs no tar and no pnpm; its manifest names the release, and
an archive without a base path and build time, built for another machine, or
lacking the dialect's driver is refused with the build command that fixes it.
upgrade --archive requires the same application name and base path and refuses
an older version; a release writing its data inside itself is refused as well.
An upgrade takes the kind of source the install did.

* feat(app-installer): source-aware status and rollback advice, free-port suggestion

status and rollback tell an archive installation to build its archive again
for this machine's Node and upgrade to it, where a template installation gets
upgrade --rebuild; status reports the source and skips the registry for an
archive installation. install names a free port when the chosen one is taken.
The README documents both sources, release ids, app.env and every error code.

* test(app-installer): smoke both sources in CI and unreleased:installer-smoke

The smoke script takes --source template or --source archive. The archive run
installs a deployment archive, upgrades to a later build of the same version,
rolls back, restores the database after a migrating upgrade, and rolls back
automatically from a build that fails to start. CI gains a job that builds the
default template's archive from the checkout and runs it; unreleased:installer-
smoke creates and builds an application from the snapshot and runs both.

install reports the release's build time. The template alignment test no
longer expects each template to name its own pm2 process.

* docs: document app-installer and rename its global Skill

The global nocobase-hub-installer Skill becomes nocobase-app-installer and
covers both sources: building and installing a deployment archive, and the Hub
template. The nocobase-deployment Skill offers app-installer for a standalone
server, and the documentation site gains a page for it, with the Hub page,
the deployment overview, operations and standalone pages updated. Examples that
run published Hub images keep HUB_STORAGE_DIR, which those images still read.

The unreleased installer changesets, written for the Hub-only package, are
replaced by one describing the package as it now ships.

* docs: name only APP_STORAGE_DIR in the deployment pages

The Hub pages, operations page and the app-development Skill no longer
document HUB_STORAGE_DIR: nothing has been released under the old name that
the documentation needs to keep serving.

* refactor: drop HUB_STORAGE_DIR in favour of APP_STORAGE_DIR

The Hub template no longer reads HUB_STORAGE_DIR, which removes its own path
policy: the runtime applies APP_STORAGE_DIR for every standalone application.
app-installer stops writing the old name for a Hub installation. Nothing has
been released under the old name that has to keep working; a deployment that
sets only HUB_STORAGE_DIR renames it before upgrading.

* fix(app-installer): write HUB_STORAGE_DIR for a Hub until one reads APP_STORAGE_DIR

Every Hub release published so far reads only HUB_STORAGE_DIR, so a template
install of one kept its data inside the release and was refused with
STORAGE_IN_RELEASE. The installer writes the old name beside APP_STORAGE_DIR
for a Hub installation again; a Hub that reads APP_STORAGE_DIR ignores it.
Remove it once such a release is published.

* fix(app-installer): address review findings on PR 451

Recognise a Hub by its manifest's nocobase.templateKind rather than by the
install source, so a Hub project deployed from an archive keeps its App Host
child (treekill off) and its confirmations name the hosted applications;
installer.json records the kind. Suggestions no longer carry a placeholder in
a command meant to run as printed, and every rebuild suggestion made during an
upgrade builds for the installed base path. An unreadable config.yml stops an
upgrade with CONFIG_UNREADABLE instead of passing as "no databases".
rollback --to <version> resolves to the newest build of that version other
than the running one. The upgrade prechecks are hoisted out of both source
branches, and tests cover the storage check rolling an upgrade back and an
unrecorded release directory being adopted.

* docs(skills): route Hub installs and app-installer deployments to nocobase-app-installer

nocobase-create-app is for installing, creating or trying NocoBase, and hands a
request to install a Hub, or to deploy an application with app-installer, to
nocobase-app-installer. Only a Hub whose own code will change is created as a
project. Both Skills now carry the same routing table, and their descriptions
name what each is for and what it hands over.
2026-09-26 20:42:41 -07:00
chenos 3c56455152 feat(scripts): add unreleased:hub-smoke for the Hub installer (#446)
* feat(hub-installer): install a Hub from the published template

Add @nocobase/hub-installer for servers that run an unmodified Hub.

`install <dir>` resolves the template version, generates and builds the
Hub in `.build/<version>/` with create-app, unpacks only the deployment
archive into `releases/<version>/hub`, writes `hub.env` and `config.yml`
outside the release, applies the migrations with the release's own CLI,
switches `current`, and starts the Hub with pm2 until `/hub/api/healthz`
answers. A failure before the switch removes everything it wrote.

`status` reports the current version, health, pm2 process, releases,
Node compatibility and whether the registry has a newer version.

The pm2 configuration runs `node` itself with the entry file as its
argument, because pm2's fork mode loads a script through a wrapper where
`import.meta.main` is false and the Hub never starts.

A smoke test installs the published template on Debian bookworm with
Node 24 on pull requests that touch the package, and daily, since the
template ships no lockfile and a new dependency release can break it.

* feat(hub-installer): upgrade and roll back a Hub

`upgrade` builds the new release beside the running one, or reuses a
release already on disk, and checks it with its own `config check` and
`db apply --dry-run` before any downtime. It then stops the Hub, copies
the SQLite database, config.yml and hub.env to backups/, switches
`current`, migrates and starts the new release behind a health check.
A failure after the switch rolls back automatically, restoring the
database when the new release may have migrated it: exit 3 when the
previous release is healthy again, 4 when it is not. Releases beyond
`--keep` are pruned.

`rollback` returns to the release the last upgrade came from and
restores the pre-upgrade database only when that upgrade migrated.

An operation interrupted during the downtime is recorded in
installer.json: status reports it, upgrade refuses to stack on it, and
rollback recovers by undoing an interrupted upgrade or finishing an
interrupted rollback. Both commands confirm first or take `--yes`, and
a non-SQLite database requires `--backup-done`.

The smoke test now upgrades from a copy of the installed release and
rolls back, exercising the downtime path without a second build.

* feat(scripts): add unreleased:hub-smoke for the Hub installer

`pnpm unreleased:hub-smoke` installs a Hub with the snapshot's
hub-installer from the local registry, checks it, upgrades from a copy
registered as an older release, rolls back and checks again. It runs
pm2 under a short temporary PM2_HOME, stops it afterwards, and keeps
the Hub and its logs outside the repository.

The CI smoke test can only see changes to the installer itself: it
installs the published template. This covers changes to the Hub
template, app-cli, app-host and create-app before they are released.

Both run scripts/smoke-hub-installer.mjs, which replaces the inline
checks the workflow carried, so the local and CI checks cannot drift.

* fix(hub-installer): address the install review

- Refuse a pm2 name that is already registered: `pm2 start` on a taken
  name restarts that process with this install's environment instead of
  starting a new one, and START_FAILED then deleted it.
- Check before the first write that the port binds, that every
  `--set-from-env` variable is set, and bound `--port` and
  `--health-timeout`; a regular file at the target is a precheck error.
- SIGINT/SIGTERM stop the running step and let the failure handling
  clean up; a second signal exits. Children run in their own process
  group, so a timeout or interrupt stops what pnpm started, and the
  promise settles only once the group has exited.
- Write the pm2 config and installer.json before switching `current`,
  so the switch is the last write before starting; remove parents the
  install created; keep `.build/` on failure under `--keep-source`.
- Fail fast when pm2 reports the new process as crashed, instead of
  waiting out the health timeout.
- `status` no longer starts the pm2 daemon; the lock is linked into
  place with its pid already written; dist-tags use `hasOwn`;
  `install --help` prints help; `config set` values are masked in
  output; an unsupported Node exits 2.
- The smoke test builds the package and runs dist/, and also runs on
  lockfile and workspace changes.
- Tests drive `install()` through a fake of every child process it
  starts, and runCommand through real process groups.

* fix(hub-installer): address the upgrade and rollback review

rollback:
- Restore only from a backup that holds a database. An external
  database is never in one, so a Hub on PostgreSQL or MySQL used to
  stop, fail to restore, and stay down.
- Keep installer.json in step with the `current` link: `current` is
  written only after the link moves. A failed stop changes nothing and
  exits 1; a target that does not start stays pending, and running
  rollback again retries it, restore included.
- Recover an interrupted upgrade by what it actually did: before the
  switch nothing was migrated, so the old release is just started; an
  interrupted rollback keeps its backup and restores it again.

upgrade:
- Record the backup in `pending` only once it is complete, and mark the
  switch before the link moves; remove a half-written backup on abort.
- Move the switch inside the rollback's error handling.
- Write the successful result before pruning, then prune.
- Never prune the release upgraded from; `--keep` is at least 2.
- Keep the new release, and the pending state, when the automatic
  rollback fails too, so `rollback` can finish or return to it.
- Refuse a downgrade; reuse a recorded release only when it was built
  for this machine, otherwise rebuild it and replace its record.
- Check that the pm2 process belongs to this root and that the port is
  free once the Hub is stopped; wait for health failing fast on a
  crashed process; read the state under the lock; UTC backup names.

Flow tests drive both commands through the fake world: exit 3 and 4,
recovery after each, external databases, stop failures, interrupted
upgrades, rebuilds, ownership and pruning. The smoke test pins the
upgrade target to the installed version.

* test(scripts): cover the restore and automatic rollback paths in the Hub smoke test

The smoke test upgraded to a copy of the same release, which applies no
migrations, so the rollback never restored the database and no upgrade
ever failed. It now also rolls back an upgrade recorded as migrating and
checks that a table written after it is gone, and upgrades to a release
whose server entry throws, expecting exit 3 with UPGRADE_ROLLED_BACK and
the previous release healthy again.

Both scripts refuse port 13010, where the Hub's App Host listens; a Hub
on it failed its health check with no clear reason.
2026-09-26 07:37:46 -07:00
chenos 02d54029a8 refactor(app-cli)!: merge nb3-cli and app-tools into one application command line (#431)
* refactor(templates): drop the forwarding files in cli/

The templates re-exported @nocobase/app-cli through cli/database-command.ts,
cli/hub-publishing.ts and cli/commands/i18n-check.ts, and passed
createAppCommands through cli/standard-commands.ts. cli/commands/index.ts now
calls createAppCommands itself, the shared i18n check test lives in app-cli,
and app-plugin-hub imports publishToHub from the package rather than from a
template. app-cli drops the ./database-command and ./commands/i18n-check
subpaths that only those files used.

* refactor(app-cli)!: absorb nb3-cli and app-tools into one command line

@nocobase/app-cli now provides the nocobase bin and carries the command
assembly, the plugin contract and plugin/Skill management from nb3-cli, and
the dev, build, start and server-deps tooling from app-tools. Both packages
are removed.

Commands are found by path under src/commands and an application's
cli/commands, and the runner locates the application from the nearest
package.json (nocobase.templateKind or nocobase.buildTarget) or
NOCOBASE_APP_ROOT. A built-in command is dispatched without importing the
application's plugins. Development commands are not registered in a built
dist/, and the tooling they need is declared as optional peers. build and
dev hand the plugins' hooks to the scripts in NOCOBASE_CLI_HOOKS instead of
asking a second process.

Consumed changesets in .changeset/pre name the removed packages and are
renamed to @nocobase/app-cli, as the app-client consolidation did, so the
next prerelease run can still read them.

* refactor(plugins)!: take the CLI topic from the package name

A plugin's CLI topic is now its package name without the scope and
app-plugin- prefix, so the scheduler's commands move from schedule to
scheduler and the CLI example's from demo to cli-example; the built-in
topics avoid every plugin name. The workflow plugin's check and build are
devCommands, which a built dist/ leaves out.

Plugins and the create-plugin template peer on @nocobase/app-cli instead of
@nocobase/nb3-cli, and the workflow tests and skill-eval harness run the
Default template's CLI through the bin with NOCOBASE_APP_ROOT.

* refactor(templates)!: run every command through the nocobase bin

The templates drop scripts/*.mjs, cli/index.ts and cli/commands/index.ts.
Their scripts are postinstall, dev, build, start and the quality checks;
every other command is pnpm nocobase <topic> <command>, which pnpm resolves
to the bin without a script. The Default template enables the Hub
commands with nocobase.cli.publishing, and all three declare tar for the
build's --tar.

create-app runs pnpm nocobase skills sync and prints the new next steps.
The smoke script and the repository's template tests follow the new layout.

* docs: describe the merged CLI and use the new command ids

AGENTS.md, the application and plugin-development Skills, the docs site,
package READMEs and runtime messages now name nocobase db repair,
config init, locales check, release upload and the other new ids instead
of the removed app-topic ids and script aliases. The upgrade Skill gains the
step-by-step move to the new layout, which the changeset points to.

The global nocobase-create-app Skill and skills/README.md are left for a
follow-up after the release, because they reach users on merge.

* chore: cover the packages whose Skills name the new command ids

* refactor(app-cli)!: name the Collection and build commands by what they act on

db doctor compares Collection metadata with the schema, not migrations, so
it becomes collections doctor beside collections generate, and db holds only
migrations, seeds and locks. server-deps retarget and verify are two steps of
the build, run again on an existing dist/, so they become build retarget and
build verify. A dist topic was considered and rejected: a commands/dist
directory is ignored by the repository's .gitignore, Prettier and ESLint.

Topic and command descriptions now say application rather than app and Skill
rather than skill, package describes the one thing it does, and skills sync
mentions the .claude/skills links it writes.

* refactor(app-cli)!: move retarget and verify to a dist topic as dist retarget and dist check

Both act on an existing dist/ and never build, so they belong to a dist
topic rather than under build, which returns to being a plain command.
verify becomes check, the verb every other checking command uses
(config check, locales check, workflow check).

A dist directory is ignored by the repository's .gitignore, both Prettier
ignore files and the shared ESLint preset, and commands are found by their
directory, so src/commands/dist/ gets an exception in each of the four with a
comment saying why. tests/command-files.test.ts fails if any command file is
ignored by git, Prettier or ESLint, so losing an exception cannot go
unnoticed. The templates' AGENTS.md warns applications away from those
directory names in cli/commands/.

* docs(app-skills): add a command-line reference for the application CLI

nocobase-app-development gains references/cli.md: a task-to-command map, the
agent conventions --help does not state (--json, exit codes, --dry-run, and
that db reset, rollback, redo and repair refuse without --force outside a
terminal, which is not to be worked around), how to read the data-model
snapshot and keep it fresh, how to write an application command, and what a
built dist/ registers. The Skill links it before any pnpm nocobase command;
the deployment Skill notes dist retarget, dist check and running commands in
dist/; the three templates' AGENTS.md keep only the essentials and point to
the reference instead of repeating it.
2026-09-25 01:28:45 -07:00
chenos 57c0f6cf95 chore(scripts): rename local-registry commands to unreleased (#406)
The local-registry:* commands sat next to pnpm registry, which builds shadcn
Registry items, and read as a local variant of it. What they do is test the
unreleased checkout as published packages, so they are now unreleased:*,
with verify renamed to smoke and stop to clean. Clean also removes a session
prepared under the old names, which would otherwise keep holding the port.
2026-09-23 23:50:14 +08:00
chenos cde9a8ea11 chore(skills): commit repository Skills under skills/ and link them on install (#404)
* chore(skills): stop linking the deployment Skill into the repository

nocobase-deployment is written for generated applications and reaches
them through @nocobase/app-skills. Linking it into .agents/skills offered
it to agents working on the framework itself, where no application is
deployed.

* docs(db-testkit): move integration test guidance out of a repository Skill

The nocobase-db-integration-testing Skill was reference material for
this repository only, and AGENTS.md already pointed at it from a section
every agent loads. Fold it into db-testkit's TESTING.md next to the code
it covers, drop the parts AGENTS.md already states, and point AGENTS.md
and the plugin development reference there instead.

* docs(db-testkit): keep the integration testing guide under docs/

A guide at the package root counts as published output to
require-changesets, although the package ships only dist and README.
Under docs/ it is documentation, which is what it is.

* chore(skills): commit repository Skills under skills/ and link them on install

skills/ now holds every Skill this repository commits, including
nocobase-plugin-development. scripts/sync-skills.mjs replaces
link-claude-skills.mjs and, on postinstall, links each of them into both
.agents/skills/ and .claude/skills/, which become generated and ignored.

The links sit one directory deeper than skills/, so relative Markdown
links from the Skill into the repository resolved differently depending
on the path an agent opened. They are now repository-root paths.
2026-09-23 22:20:37 +08:00
chenos 58a851cc45 feat(scripts): add local-registry:env to point a shell at the snapshot (#401)
create and verify run pnpm with an isolated environment, but a shell
that should resolve the snapshot too — to try an agent Skill that runs
plain `pnpm create @nocobase/app`, for one — had to reassemble that
environment by hand, and leaving out any variable fails silently.

The one easiest to miss is XDG_CONFIG_HOME. `pnpm config set` saves a
scoped registry to the global auth.ini in pnpm's configuration
directory, which PNPM_CONFIG_USERCONFIG does not replace, so anyone who
followed the documented `pnpm config set @nocobase:registry …` still
resolved every @nocobase package against the published registry. A
snapshot carries the same version numbers as the last release, so that
produced an application mixing a new template with old packages and
reported nothing.

`eval "$(pnpm -s local-registry:env)"` prints exactly what registryEnv
sets, and unsets the inherited configuration it drops. A test
reproduces the auth.ini leak before asserting the exports override it.
2026-09-23 20:08:51 +08:00
PiEgg fc34a66814 chore: link .agents/skills into .claude/skills on install (#349) 2026-09-21 16:19:25 +08:00
chenos fe564d9f93 feat(create-app): add dialect selection and local application verification (#307)
* feat(create-app): support dialect selection and JSON output

* feat: add local registry workflow and fix generated app startup

* feat: add strict startup mode for application verification

* fix(create-app): address review on failure stages and verification

* test: isolate strict startup supervision from dev environment setup

* fix(test): transform dynamically discovered TypeScript queue jobs
2026-09-19 16:29:55 +08:00
chenos 24e771f4c3 fix(db): remove circular development dependencies (#267) 2026-09-17 16:30:57 +08:00
chenosandClaude Opus 5 ceb356b921 refactor(db): split dialects into packages and add Dameng, KingbaseES and OceanBase (#167)
* feat(examples): add analytics database and development guides

* fix(db): unify numeric results with native postgres and mysql behavior

* feat(db): add postgres dialect driver contract

* feat(db): add remaining dialect driver packages

* fix(db): honor external dialect knex clients

* chore(db): add dialect package changeset

* fix(db-postgres): include knex query stream dependency

* refactor(db): expose dialect knex hooks

* refactor(db): allow dialect schema inspectors

* test(db-sqlite): verify factory connection paths

* feat(db): wire dialect schema inspectors

* chore(db): update public api baseline

* fix(db): resolve registered dialect factories

* feat(db): move sqlite and oracle hooks to drivers

* fix(db): prefer driver connection resolution

* feat(app-server): register split database drivers

* chore(app-server): add split driver changeset

* chore(db): declare optional knex driver peers

* fix(db): let dialect drivers own pool hooks

* fix(db): protect dialect factory metadata

* feat(db): formalize dialect driver registrations

* feat(db): move dialect connection resolvers to drivers

* feat(db): move schema inspectors into dialect packages

* fix(db-postgres): own knex query stream loading

* fix(db): let dialect packages load native knex drivers

* fix(db): pin native driver names in factories

* docs(db): document split dialect usage

* fix(db): preserve mysql2 knex client class

* chore(db): add dialect package changelogs

* refactor(db): remove dialect implementations from core

* test(db): register sqlite dialect in internal consumers

* refactor(db): move capabilities into dialect drivers

* docs(db): record dialect split roadmap

* feat(db): add exact numeric and workflow examples

* refactor(db): inject dialect runtime strategies

* refactor(app): inject database dialect drivers

* refactor(db): move numeric query rules into dialect runtimes

* refactor(db): inject repository dialect strategies

* refactor(db): move temporal and json dialect strategies

* refactor(db): move schema dialect strategies

* refactor(db): make inspector strategies dialect owned

* refactor(db): finalize extensible dialect metadata

* refactor(db): move application dialect hooks into drivers

* test(db): cover extensible dialect configuration

* refactor(app): allow registered dialect identifiers

* docs(db): update dialect split roadmap

* docs(db): document open dialect extension contract

* test(db): cover sqlite and postgres runtime strategies

* test(db): cover mysql oracle and mssql runtime strategies

* test(db): cover dialect schema inspectors

* test(db): exercise sqlite schema inspection

* test(db): cover dialect connection hooks

* test(db): add shared dialect testkit

* test(db): run shared contract for sqlite

* test(db): move sqlite external metadata integration

* test(db): define dialect adapter boundary

* test(db): add sqlite integration entrypoint

* test(db): add dialect integration entrypoints

* test(db-testkit): add dialect integration adapter contracts

* test(db): add dialect-owned integration contract entrypoints

* test(db): stabilize dialect integration adapters

* test(db-testkit): own shared integration suite

* test(db-testkit): move dialect unit coverage out of core

* test(db-*): move inspector strategy coverage to dialect packages

* test(db-testkit): isolate inspector contracts by dialect

* test(db-*): move dialect unit suites to owning packages

* test(db-testkit): expose stable integration suite entrypoint

* test(db-sqlite): own sqlite-only integration capability case

* docs(db): add dialect extension guide

* docs(db): rewrite dialect extension guide

* fix(db): apply dialect strategies across repository bindings

* fix(db-oracle): normalize text and default insert bindings

* feat(db-dameng): add Dameng dialect foundation

* fix(db-dameng): refine identity and catalog inspection

* fix(db-dameng): align numeric and index integration behavior

* fix(db-dameng): normalize boolean schema defaults

* fix(db-dameng): complete integration compatibility

* feat(db): add fresh managed migration reset

* fix: add package changeset and sqlite test driver

* fix: format string-backed file sizes

* fix: declare runtime database dependencies

* fix: update sqlite metadata store test

* fix: register sqlite driver in example tests

* fix: register sqlite driver in workflow tests

* fix: register sqlite driver in users tests

* fix: register sqlite driver in hub tests

* fix(db-oracle): use identity columns for auto increment

* test(db-mysql): serialize integration files

* refactor(db): isolate dialect integration environments

* docs(db): update dialect integration workflows

* ci(db): run sqlite integration tests

* fix: pnpm-lock.yaml

* chore(users): add sqlite test changeset

* feat(db): add OceanBase MySQL dialect

* test(db-testkit): support Kingbase PostgreSQL compatibility

* feat(db-kingbase-postgres): add KingbaseES dialect package

* refactor(db-testkit): describe integration behavior with profiles

* refactor(db-testkit): describe scalar inspection capabilities

* refactor(db-kingbase): use Kingbase dialect identity

* ci: run Kingbase integration tests

* Simplify plugin development configuration

* fix(db): stabilize dialect integration behavior

* fix(db-oceanbase): account for JSON default limitations

* test(db): align postgres decimal integration expectation

* fix(db-testkit): model insert returning support

* feat(db-testkit): support selecting integration test files

* fix(db-dameng): decode returning LOB values

* fix(db-testkit): export integration argument parser

* fix: restore examples route order

* fix: close logging transports reliably

* test(db): add JSON repository and query coverage

* fix(db-oracle): update Oracle schema mappings

* test(db): normalize boolean query values

* fix(db): normalize temporal values across query and repository

* fix(examples): update database drivers and JSON contracts

* fix(db): use valid datetime values in playground

* fix(db): support local Date temporal mutations

* fix(db): preserve null temporal query values

* test(ai): register sqlite driver in database tests

* fix: test

* fix: test

* refactor(app-server): declare database drivers on the application config

An application lists the dialect packages it installs under `database.drivers`,
next to the connections that use them, so the runtime, the CLI commands and the
tests all resolve a dialect from one place and app-server stays independent of
every concrete driver.

This replaces the process-wide `registerAppDatabaseDrivers` registry and the
`databaseDrivers` option on `Application`, and removes each template's
`server/database-drivers.ts`, which existed only for its registration side
effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(app-server): supply plugin task sources outside the database config

`AppDatabaseConfig.taskSources` held the application's package name and the
migration and seed directories contributed by registered server plugins —
values the runtime derives from resolved plugins rather than values anyone
configures. Carrying them in the `database` namespace put them where a
config.yml deep-merges: `database.taskSources.migrations: []` silently dropped
every plugin's migrations, and the application still started.

They now travel as an `AppDatabaseTaskContributions` value alongside the
configuration. `planAppDatabaseTasks`, `runAppDatabaseTasks`, `runAppMigrations`
and `runAppSeeds` take an options object carrying it, with `paths`, `drivers`
and the task selection, in place of their positional parameters.
`createAppPluginDatabaseConfig` and `resolveAppPluginDatabaseConfig` are
replaced by `createAppDatabaseTaskContributions`. `contributions` is required,
so a call site that has not been updated fails to compile rather than quietly
planning without its plugins.

A template's server/config/database.ts keeps only what it configures, and its
cli/database-command.ts builds the contributions from the runtime it already
resolves; cli/commands/migrate.ts and cli/commands/seed.ts are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:00:25 +08:00
PiEgg a009e2d6b0 feat(i18n): derive offered languages from the application's own locale files (#190) 2026-09-11 15:50:32 +08:00
Zeke Zhang 7ec6b6bf99 chore(scripts): add examples app commands (#165)
* chore(scripts): add examples dev command

* chore(scripts): complete examples commands
2026-09-09 16:12:28 +08:00
PiEgg ec576ba059 feat(cli): let plugins contribute commands to an app's CLI (#150) 2026-09-08 17:27:15 +08:00
PiEgg 813da59768 fix(create-app): repair the commands a generated application ships broken (#115)
* fix(create-app): repair the commands a generated application ships broken

A freshly created application failed four of its own scripts before a line of
code was written, and its deployment artifact could not start.

`client:inspect` refused to run because `client/runtime.ts` kept the template's
package name while `package.json` carried the application's. That mismatch also
split the i18n namespace: the client derives it from the runtime declaration and
the server from the manifest, so `APP_NS` resolved differently in each half.
Scaffolding now rewrites the name in the three sources that embed it, and sets
`displayName` instead of dropping it, which had left the sidebar footer showing
"Default Template" in every generated application.

`lint` failed on an untouched shadcn/ui file. eslint-plugin-react-refresh 0.5.5
dropped the member-expression check, so `const Select = SelectPrimitive.Root`
read as a non-component export; 0.5.6 restores it.

`test` failed because `vitest.config.ts` listed test files by hand. The list had
drifted — it named a file that no longer existed while seven real test files were
absent from it and had never run — and a generated application ships no `tests/`
at all. It is a glob now, with `passWithNoTests`.

`format:check` failed on the lockfile, because `.prettierignore` was not among
the published files.

`build` produced a `dist/` that could not run. The generated `dist/package.json`
listed no database driver: knex loads one by name rather than importing it, and
`@nocobase/db` arrives as a transitive dependency, so neither path reached the
scan. The driver the application declares is now added explicitly, the install
runs under pnpm with the `allowBuilds` its native addon needs, and the stale
refine-era Dockerfile is gone.

* fix(repo): stop format:check from reading build output

Nine packages ran `prettier . --check` with no `.prettierignore`, so the check
descended into their own `dist` and reported the compiler's output as badly
formatted. `pnpm --filter @nocobase/i18n check` failed on 28 such files, none of
which a person wrote or could usefully fix.

The failure only appears once a package has been built, which is why it survived:
a clean checkout has no `dist` to walk, and CI builds and format-checks in
separate jobs.

Every other package in the repository already ships this file with the same
`dist/` line. It is development-only and stays out of `files`, so no published
tarball changes.

* feat(create-plugin): tell a generated plugin where its dependencies go

A new plugin had no agent-facing documentation at all, so nothing told the next
developer that `dependencies` and `devDependencies` mean different things here.
Eleven plugins in this repository had already put browser packages in
`dependencies` — harmless-looking, since the application bundles them anyway,
but the same walk that generates `dist/package.json` then installed all of them
into a server deployment that never required one. Two of them were 44 MB.

The generated `AGENTS.md` states the rule as a table, explains why the server
and client halves differ — one is deployed unbundled and resolves its imports at
runtime, the other is bundled by the application at build time — and points at
`app-template-default` for the version of a client package to reuse. `CLAUDE.md`
defers to it, matching both application templates.

The generator already emitted the right shape for the capabilities it scaffolds:
`hono` is a dependency, every runtime package is a peer paired with a
devDependency. What was missing was saying so, for the dependencies a developer
adds afterwards.
2026-09-03 13:23:12 +08:00
PiEgg 15274267fe fix(deps): declare identity-sensitive packages as peer dependencies (#95) 2026-09-01 11:42:25 +08:00
PiEgg 1b5f10fa44 refactor(packages): group packages into category directories (#86) 2026-08-31 19:07:58 +08:00
PiEgg a7d445376e feat(create-app): synchronize plugin skills after the install (#84) 2026-08-31 15:23:27 +08:00
chenos 78cf0a2092 feat: add agent-ready v3 plugin development (#80)
* feat: add agent-ready v3 plugin development

* docs: align plugin guides with capability-driven creation

* test: cover plugin capability generation matrix

* test: verify plugin capability publishing boundaries

* test: cover plugin registration lifecycle

* feat: add stable JSON errors for plugin tooling

* feat: complete agent-ready plugin lifecycle tooling

* docs: audit server plugin agent observability

* feat: add agent-friendly server inspection

* refactor: narrow server plugin inspection

* fix: align agent plugin development loop

* feat: add app-facing plugin skill example

* fix: keep synchronized agent skills local

* feat: complete agent-ready route development

* refactor plugin server route ownership

* docs: complete agent-ready route guidance

* docs: add agent-ready plugin module guides

* refactor client runtime composition

* docs: add plugin development table of contents

* feat: make plugin locale capabilities explicit

* feat: refine plugin inspection diagnostics

* docs: consolidate plugin development guides

* docs: streamline plugin development guidance for agents

* fix: provide dev config to plugin loop fixtures

* fix: address plugin lifecycle review findings

* fix(template): initialize installed app test databases
2026-08-31 07:23:16 +08:00
PiEgg b0492663af feat(i18n): internationalize applications and plugins, on the client and the server (#69)
Adds internationalization for both the application template and the plugins it installs, on the client and on the server.

@nocobase/app-i18n provides the mechanism across three entry points — core, /client, /server — and registers no routes. @nocobase/app-plugin-i18n adds what a user operates: the endpoints that record which language to answer in, and the hook a picker is built on. An application that does not install the plugin still translates; it only loses the ability to change language.

A namespace is a package name, so nothing has to be declared for namespaces not to collide, and APP_NS stands in for the application's own name, which a plugin cannot know at compile time. A key resolves through its own namespace, then the application's, then the base package's, which is what lets a plugin reuse application wording without naming a namespace at all. The default namespace is injected at the render layer from the packageName each contribution already carries.

Resources load one language at a time: the browser fetches only what it is showing and loads another on switch, while the server lazy-loads a language the first time it serves one. Code running outside a request must await ensureLocaleLoaded itself, and outbound content takes an explicit locale, because the language of a mail follows its recipient rather than whoever triggered it.

The chosen language lives in the browser and is mirrored to the session. Tabs sharing an account overwrite each other there, so error payloads carry ns, key, and params alongside the translated message, letting a frontend render errors in its own interface language regardless.

app-plugin-workflow is converted as the reference, including the page shell the application renders around its own pages and which therefore names its namespace explicitly. The Portal SDK i18n runtime is deprecated rather than rewritten: it keeps its own i18next instance, so the two must not be mixed inside one application.
2026-08-30 11:42:03 +08:00
chenos 12dfb6828d feat: add template-based plugin scaffolding (#68)
* feat: add template-based plugin scaffolding

* refactor server plugin contributions

* fix(create-plugin): replace template placeholders
2026-08-29 18:37:16 +08:00
PiEgg c8f38c8934 refactor(app-client): register plugin client extensions explicitly (#50) 2026-08-28 11:12:40 +08:00
chenos 04653230d4 chore: add package changelogs and publication checks 2026-08-26 17:22:01 +08:00
PiEgg b3286fcb84 feat: add create-app for scaffolding applications (#28) 2026-08-26 13:36:47 +08:00
PiEgg da1b1b07a7 docs: nocobase 3 version && branch 2026-08-25 16:46:15 +08:00
chenos dbbeaccc3f feat: publish authentication registry from plugin 2026-08-25 12:58:16 +08:00
PiEgg 1f13ea0fb8 feat(cli): add nb3 command line tool (#11) 2026-08-25 11:26:21 +08:00
chenos a83061dcd9 feat: support client plugin routes (#15)
* feat: support client plugin routes

* feat: support declarative client plugin contributions

* feat: support Refine client plugin configuration

* feat: expand app client plugin runtime

* feat: add customizable app client shell

* feat: organize application client contributions
2026-08-24 08:51:12 +08:00
chenos 554ad8dea5 feat: add plugin registration scripts 2026-08-22 23:46:49 +08:00
chenos 303c9f3e9a feat: add app plugin scaffold scripts 2026-08-22 23:01:44 +08:00
PiEgg ae3cbb0c5c refactor(dev-config): migrate shared configs to TypeScript (#9) 2026-08-20 16:44:29 +08:00
PiEgg 379de3c968 feat: unify monorepo development configuration (#7) 2026-08-20 15:58:22 +08:00
YANG QIA a9fd3f2dac feat: add authentication foundation packages (#1)
* feat: add authentication foundation packages

* feat(authorization): add policy and record access foundation

* chore: add eslint formatting rules

* refactor(logging): rename logger package

* refactor(logging): unify logger infrastructure

* refactor(logging): add logging factory

* refactor(caching): unify cache infrastructure

* feat(logging): add request logging middleware

* feat(authentication): add app authentication foundation

* fix(authentication): derive cookie scope from app runtime

* fix: stabilize app runtime integrations

* fix(authentication): persist cached sessions

* feat(authentication): add username authentication

* refactor(authentication): rename password registry
2026-08-20 14:52:02 +08:00
chenos b5f2e30c48 feat: app runtime adapters (#4)
* feat: add shared app runtime and migrations

* feat: support idempotent collection operations

* feat: add app runtime adapters

* chore(app-template): tidy server structure and dev workflow
2026-08-18 22:47:57 +08:00
chenos 1a5de4dbe0 chore: update app template config 2026-08-17 21:11:38 +08:00
chenos 7396098a2d Initial develop commit 2026-08-17 09:01:50 +08:00