mirror of
https://github.com/nocobase/nocobase.git
synced 2026-10-13 07:47:55 +08:00
@nocobase/app-cli@1.0.0-beta.21
182
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a54505d89 |
fix(mail): improve workspace integration and public client APIs (#10658)
* fix(mail): clarify account authentication failure messages * fix(mail): clarify optional IMAP login names * fix(mail): expose workspace account navigation and actions * fix(mail): adapt workspace panes to available width * fix(mail): warn about development OAuth returns in production * fix(mail): compose section environment overrides * fix(mail): expose operation-specific composer completion * fix(mail): expose complete reply preparation * fix(mail): expose reusable unread count hook * fix(mail): align account credential fields and optional labels * fix(mail): reconcile upstream runtime and route changes * test(mail): move callback type checks out of Vitest |
||
|
|
251d19e46b |
fix(authentication): gate password reset and add member action (#10719)
* fix(authentication): gate password reset and add member action * fix(authentication): address password reset review feedback * fix(authentication): isolate the password-reset capability cache per host Key the capability cache and in-flight request by the host's own ApiClient instance instead of a module-level singleton, so one application's cached or in-flight capability is never handed to another. Add a real-application integration test for the member password reset flow covering authorized reset, revoked sessions, a caller without the permission, and an anonymous caller. |
||
|
|
8de421ab4b |
docs(workflow): add process diagrams and invoicing walkthroughs (#10727)
* docs(workflow): add process diagrams and invoicing walkthroughs * docs(approval): remove duplicate overview routes |
||
|
|
13620d9ffd |
feat(agents): install the runner and CLI from npm when the application names them (#10714)
Claude-Session: https://claude.ai/code/session_01XFYuunpkqzgMT8C24A4PvY |
||
|
|
42c5cbcb28 | feat(default): include the Mail plugin by default (#10704) | ||
|
|
425ed30dfb |
feat(studio-cli): publish nb-studio as its own package (#10703)
@nocobase/studio-cli is nb-studio, NocoBase Studio's command line: @nocobase/app-cli-client run with Studio's nocobase.cli brand, shipping the nb-studio-cli Skill under skills/ where the runner looks for a CLI's Skills. A machine with Node.js 24 can now install it from npm instead of from a Studio server's standalone tarball. Studio's nocobase.cli and ai/skills/nb-studio-cli remain what `nocobase cli build` packs; a script test keeps the package's copies equal to them. |
||
|
|
0cc0327806 |
feat(studio): import NocoBase Studio into packages/apps (#10666)
NocoBase Studio moves from the private nocobase/studio repository (archived at e4a18fd) into packages/apps/studio and is published as @nocobase/studio. The framework submodule, the mirrored workspace file, the lockfile and the submodule check go away; Studio now resolves the framework packages and plugins from this workspace. - packages/apps is a new package category for product applications. check-openapi checks Studio, and the database test portability check covers it; Studio's tests now take their databases from @nocobase/app-testing. - A new migration, 202610220010_studio_rename_package, moves the migration and seed history recorded under the old package name `studio` to `@nocobase/studio` and rewrites the i18n namespace of workflow definitions copied from the software template. Released migrations are unchanged. - Studio follows the framework changes since its last submodule pointer: the workflow plugin and the client's dev and settings route surfaces are gone. - Prepared for publication: internal infrastructure, ticket keys and references to the predecessor product are removed, the demo data is in English, and tests are no longer published. |
||
|
|
e99e1a68e8 | fix(app-template-default): depend on @nocobase/lifecycle (#10665) | ||
|
|
98e79a5a65 |
feat(lifecycle): durable examples (#10644)
* docs(lifecycle): add examples and docs * refactor(workflow): prune workflow * docs(workflow): prune workflow |
||
|
|
c796cb909b | feat(app-client)!: remove settings and dev route surfaces, Hub and AI employee (#10642) | ||
|
|
d89339eefa |
fix(agents): align model effort cells and report Claude models (#10640)
* fix(agent-runner): report Claude Code models Claude Code lists its models and efforts through the Agent SDK's supportedModels(), on a session that is sent no prompt, instead of reporting detection as unsupported. * fix(agents): match Codex reasoning efforts to current releases Codex takes low through xhigh, max and ultra, and no longer minimal; max is passed as itself. A saved effort its tool dropped stays until the entry changes. * fix(agents): keep model effort cells aligned and drop the built-in badge Reported efforts move to a line under the entry and name only selectable efforts, so the effort select no longer shifts its row. Model suggestions lose the Built-in badge. * fix(agents): wrap long descriptions in the agents list The description wraps inside a capped name column, two lines at most with the full text on hover, so other columns keep their widths. * fix(agents): group the agents list and sort it by shown name Everyone agents come first, the application's own in the order it added them, then the caller's own, then shared ones, each by translated name. List entries carry owned. * feat(agent-runner): give agents the runner's Node.js and pnpm The runner ships pnpm and puts node and pnpm launchers first on each run's PATH, pinned against packageManager switching; config set agent-tools system opts out. Store pruning uses the bundled pnpm. |
||
|
|
d86a77c746 |
chore(v3): deprecate AI employee plugin in application templates (#10599)
* chore: deprecate AI employee plugin in application templates * fix(v3): wait for enabled controls in workflow and mail flows |
||
|
|
dc91aab7fd |
PM-74: Use package-local subpath imports in registry sources (#576)
* fix(registry): use package-local subpath imports Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply> * docs(registry): preserve package imports in plugin guidance * fix(registry): validate installed components in packed plugins * test(create-plugin): isolate offline dependency metadata --------- Co-authored-by: 张中合 <zhonghe.zeke@gmail.com> Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply> |
||
|
|
71dbc34542 |
feat(lifecycle): add record lifecycles with ticket, expense and office flow examples (#519)
Add record lifecycle definitions, transactional transitions, durable effects, recovery, jobs integration, and React bindings. Include ticket, expense, and office flow examples in the Examples application, with concurrency and database compatibility fixes. |
||
|
|
860ef32830 | refactor: move Mail plugins into OSS examples | ||
|
|
6162033060 |
fix(tests): open popups after their triggers are wired (#561)
* fix(projects): keep an invitation's row menu open when the list renders again The columns depended on the whole useMutation result, a new object each render, so every cell remounted. * test: open popups the way a user does A Base UI trigger is wired to its popup only once React runs the passive effects of the render that mounted it, so a bare fireEvent.click right after the trigger appears is dropped under load. Open popups with userEvent, fix the timing races this exposed, and cancel pending replies when the UI Library chat demo resets. * chore: add a changeset for the new test dependency |
||
|
|
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 |
||
|
|
bc1e83ffac |
feat(plugins): update the plugins, templates and UI Library for the new runtime (#553)
* Update the plugins, templates and UI Library for the new runtime Device authorization, scoped API keys, service accounts, user invitations and preferences, secrets-backed credentials, the inbox and the cn package across the existing plugins, examples and the three templates, with the UI Library's auth, inbox, authorization and component items. * Declare the UI Library site's favicon * Use the NocoBase favicon on the UI Library site * Fix generated-app shell tests and the Hub's user deletion test Inline the authentication client in the templates' Vitest config so its guards share the test's react-router, and update the Hub test to the users plugin's new copy and spinner label. |
||
|
|
37c8d20aa6 |
feat(app): add the runtime, CLI client and agent runner for agent-run applications (#552)
Encrypted secrets, a command manifest derived from the API document, environment-variable manifests, sample data and on-demand Apps in the runtime, plus the new agent protocol, application CLI client, agent runner and Docker App Host packages. |
||
|
|
a5e19f7854 |
feat(examples): add an AI employee tasks example and a global AI employee entry (#550)
* fix(app-plugin-ai-employee): translate the nocobase-ai Registry item in the plugin namespace
The Registry item translated through its own locale files and a useAITranslate()
hook that picked a language by prefix and fell back to the English default, so a
key it never defined showed English in every language without anything noticing.
It now calls useTranslation('@nocobase/app-plugin-ai-employee') like the file
plugin's Registry item and the plugin's own pages, and its copy moves into the
plugin's client/locales, which the registered plugin loads.
Four keys the item used without defining them get English and Chinese text, and
a test fails when a key the Registry passes to t() is missing from either locale
file or a file translates outside the plugin namespace.
* feat(examples): add an AI employee tasks example and a global AI employee entry
@nocobase/app-plugin-ai-employee-example registers the AI employee iris and the
read-only tool example-ticket-history through AIResourceRegistrar, declares the
/ai-employee-example route with a fallback page, and publishes a tasks-page
Registry item that replaces it: support tickets whose AIEmployeeShortcut offers
an auto-sent analysis and a reply left in the composer, with the selected ticket
as work context, and a button that starts a queue triage through the global chat
controller.
The examples template installs the nocobase-ai item, wraps its signed-in layout
in a global AI employee entry (a floating trigger that opens the shared
conversation as a side panel and expands into a dialog), registers the example
plugin, installs its tasks page and links it from the homepage.
* test(templates): let the Examples layout carry its AI employee entry
The framework alignment check compares the Examples layout with Default byte for
byte, and the global AI employee entry wraps the signed-in shell. Like the
notification button in the header, it is an Examples-owned demonstration: the
check now removes exactly that wrapper, its import and its marker comment,
re-indents what it wraps, and still requires the shell itself to match Default.
|
||
|
|
1197085174 |
feat(app-plugin-ai-employee): release unused conversation checkpoints on a schedule (#549)
* feat(app-plugin-ai-employee): release unused conversation checkpoints on a schedule A recurring job on the application's jobs service releases the agent checkpoints of conversations nobody has used for a week, every day at 03:00 UTC by default. Releasing a conversation sets its thread to 0 and deletes the checkpoints of every thread it has had, in one transaction per batch, only while the conversation is still unused. A conversation whose latest message asks for a tool call is never released. The next run on a released conversation replays its latest stored messages onto a fresh thread. This applies only to conversations kept in the plugin's own tables, never to a resume or a tool decision, and never to a thread-0 conversation that still has a checkpoint there or has no messages, so existing runs behave as before. The schedule is configured under ai.checkpointCleanup and validated by config check. * feat(app-plugin-ai-employee): default a conversation's thread to 1 A conversation starts on thread 1 when AIConversationsManager creates it, but the aiConversations.thread column defaulted to 0, so a row inserted without a thread looked like a conversation whose checkpoints the cleanup had released. A new migration sets the default to 1. Thread 0 now always means a released conversation, so the agent decides whether to replay stored messages from the thread number alone, without reading the checkpoint store. * fix(app-plugin-ai-employee): read each cleanup batch's latest messages in two queries The checkpoint cleaner looked up the latest message of every candidate conversation with its own query. It now finds each conversation's latest message id with one groupBy over the batch and reads those messages with one more query. The aggregate goes through the Repository rather than the query builder, because it reads a bigint result back exactly on every dialect, and a message id is a snowflake beyond the integers a JavaScript number holds. |
||
|
|
3898a89017 |
feat(workflow): add durable wait nodes and quotation review (#531)
* feat(workflow): add durable wait instruction * feat(workflow): persist checkpoint recovery and quotation wait review * fix(examples): retain empty state primitive for workflow review * fix(examples): restore decision focus after submission settles |
||
|
|
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. |
||
|
|
299b35a255 |
feat(app-testing): render a page inside a started client application (#528)
Step 9 of the test infrastructure plan: a page test renders inside a real client application instead of mocking `@nocobase/app-client`.
## What changes
**`@nocobase/app-client` takes a `fetch` and mounts its providers under any router.** `new ClientApplication({ runtime, createRenderConfig, fetch })` sends the API client's requests through `fetch` instead of the global one; production leaves it out. `AppClientProviders` is the part of `AppClientRoot` inside its `BrowserRouter` — the application context, the React providers and Refine — around its children, so a test can render a page in a started application under a `MemoryRouter`. `AppClientRoot` renders the same tree as before.
**`@nocobase/app-testing/client`.** `renderWithApp(ui, { plugins, namespace, route, server | fetch })` starts a client application with the plugins' services and strict translations and renders the page in it, so `useApiClient()`, `useService()`, `useToaster()` and `useTranslation()` are the real ones:
- `server` is an application from `createTestApp()`, reached in process under its base path, with `cookie` from `signIn()` to act as a user; without it, `fetch` answers each request, and a request nothing answers fails the call that sent it.
- `answerApi(handler)` builds that `fetch` from a function of `{ method, path, query, json }` returning the body, so a `vi.fn()` handler replaces a mocked `request` and still records every call.
- `services` registers stand-ins before the plugins start; toasts render after the page as plain text and `toasts()` lists them; the application shuts down when the test finishes.
- The entry loads nothing from `@nocobase/db-testing`, so it runs under jsdom. The client packages are optional peers.
**`@nocobase/dev-config`'s React preset inlines `@refinedev/react-router`.** Loaded by Node, Refine's router bindings got a different copy of `react-router` than the test, so an application rendered under a test's `MemoryRouter` failed with "useLocation() may be used only in the context of a <Router> component". Nothing hit it before because no test rendered Refine's default router bindings under a router of its own.
**Pilot.** `@nocobase/app-plugin-hub`'s `api-keys-page.test.tsx` drops its `vi.mock('@nocobase/app-client')`, the stand-in toaster and the i18n wrapper, and answers the Hub API through `answerApi()`; its ten tests keep their assertions. The rest of the 19 plugin and example packages and 3 templates that mock `app-client` move in later changes.
## Verification
- `@nocobase/app-client`: lint, typecheck and its 161 tests, including the injected `fetch` and `AppClientProviders` under a `MemoryRouter`.
- `@nocobase/app-testing`: lint, typecheck, build, its node and client test projects, and `pnpm pack` with publint and attw; the built `./client` imports nothing from `@nocobase/db-testing`.
- `@nocobase/dev-config`: `pnpm --filter @nocobase/dev-config check`, and the tests of all 27 packages on the React preset.
- `@nocobase/app-plugin-hub`: lint, typecheck and its full suite (424 tests).
Changesets: `app-client-providers-and-fetch.md`, `dev-config-inline-refine-router.md` and `app-testing-client.md`.
|
||
|
|
7e5b7d4e72 |
refactor(templates): build the sidebars from shadcn Sidebar primitives (#534)
* refactor(templates): build the sidebars from shadcn Sidebar primitives Replace the hand-rolled LayoutSidebar and NavigationTree in the default, examples and hub templates with shadcn's Sidebar, keeping the collapsed menu behaviour from #317: zero-delay tooltips, hover and keyboard-focus popovers with permission-filtered children, nested expansion, parent navigation and the selected group's highlight. components/ui/sidebar.tsx stays as the shadcn CLI writes it. The layouts customize it from outside: widths through the provider style in spacing units, a phone Sheet with a translated title rendered by app-sidebar.tsx, and a document keydown listener that keeps Ctrl/Cmd+B from reaching the provider's window listener. Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2 * fix(templates): tidy the sidebar footer and add the edge rail Space the footer like the menu above it: the shield aligned to the first line, the slogan's two lines, then the name and version on their own truncated lines in the body font. In icon mode only the shield remains, with a tooltip carrying the same text. Add shadcn's SidebarRail with a translated label and title, passed as props that override the primitive's built-in "Toggle Sidebar". Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2 * refactor(ui-library): split auth-ui into presentational auth components Replace the auth-ui block with four presentational registry components built from shadcn primitives: auth-forms (Field, InputGroup, Alert), auth-methods (Tabs, FieldSeparator, Button), auth-centered-layout and auth-split-layout. They call no plugin and take every string as a prop with an English default. The templates' sign-in, sign-up and password pages become application code that wires the forms to the authentication plugin's headless actions, translates them from client/locales, and keeps the NocoBase brand panel as the split layout's aside. The ui-library preview loads shadcn/tailwind.css and the shadcn Label so the data-attribute variants render. Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2 * fix(ui-library): keep auth demo links inside their demo Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2 * refactor(templates): install the auth registry blocks under client/extensions Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2 |
||
|
|
21d274cc71 |
refactor: move every existing /api route onto the HTTP API specification (#532)
Moves every /api route in the repository onto the HTTP API specification: camelCase plugin namespaces, standard methods and slash-separated custom methods, { data } / { data, meta } responses, the standard error body with reason and domain, permission before validation, side-effect-free GETs, Repository errors that declare their own status, save-time validation of write grants, duplicate route detection at start, and optional global API limits in config.yml. All changed packages carry changesets.
|
||
|
|
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. |
||
|
|
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
|
||
|
|
e44f49cc5e |
feat(hub-cli)!: remotes, hub auth, build for the Hub, releases/status and resumable uploads (#498)
* feat(hub-cli)!: deploy to remotes, save keys with hub auth, build for the Hub hub-cli keeps the Apps an application deploys to as named remotes in the committed .nocobase/hub.json (`hub remote add/list/remove`), saves one API key per remote in the user's configuration directory (`hub auth login/logout/status`), and no longer reads HUB_URL, HUB_APP_ID, HUB_API_KEY, the App root .env or the --hub/--app-id/--api-key flags. Every command takes --remote. `hub deploy` and `hub upload` ask the Hub which platform its Host runs Apps on and run `nocobase build --target … --node-version … --tar` for it before uploading; --no-build and --file upload an existing archive after checking its recorded target. `hub deploy` uploads and then deploys through the deploy endpoint, so an archive the Hub already has is deployed as its existing Release. The Hub's upload route only uploads: the configuration-prefixed body and the deployment-intent and wait headers are gone. Uploads are checked against the Host's build target, the App detail reports it, and which routes a publishing key may call is declared with each route. The Host reports its runtime platform in its status. * feat(hub-cli): add hub releases and hub status `hub releases` lists an App's Releases newest first with the platform each archive targets, whether it runs now and whether it ever deployed, and `hub status` reports the platform the Hub builds for, what the App runs and its last deployment. Finding the Release to roll back to no longer needs the Hub's web page. The Hub opens its Release list, single Release and deployment list to publishing keys holding either scope. The Release list takes a `limit`, each Release reports `buildTarget`, `running` and `everDeployed`, computed from existing records, and deployment list items carry `finishedAt`. * feat(hub-cli): upload archives resumably hub-cli sends the archive through the Hub's resumable upload: a session for the archive's size and checksum, chunks in order at the offset the Hub has reached, then completion. A chunk whose answer is lost is resent from the offset the Hub reports, a session left unfinished by an earlier run is resumed, and an archive the Hub already has is not sent again. Archives may be up to 2 GiB, and a reverse proxy only needs to allow one 8 MiB chunk. The Hub stages sessions on local disk under a new `uploadsDir` option, which the Hub template sets to storage/hub/uploads, expires them 24 hours after their last chunk, and creates the Release on completion through the same checks as the single upload, which the management console keeps using. * fix(hub-cli): name the command to run when no remote is configured * feat(app-cli): let a CLI plugin describe its nested topics `defineCliPlugin` accepts `topics`, a one-line description for each topic nested under the plugin's own. Help showed one of a nested topic's commands' summaries in its place, so `hub remote` read "Add a Hub App to deploy this application to." A topic is described only where a command under it is registered, and a described topic with no command under it is rejected. hub-cli describes `hub remote` and `hub auth`. * fix(hub-cli): address review findings on remotes, retries and deadlines - Stop create-app from ignoring .nocobase/ in a generated application's .gitignore, so .nocobase/hub.json is committed as documented, and warn from hub remote add when an existing checkout still ignores it. - Count a failed offset read as one of the consecutive chunk failures instead of ending the upload, and go on from the offset an UPLOAD_OFFSET_MISMATCH reports without a second request. - Give each Hub request its own deadline, report one that outlasts it as TIMEOUT, and bound the wait for a deployment separately, so --timeout no longer caps a whole resumable upload. - Report a key as rejected in hub auth status only when the Hub rejects it; anything else leaves it unchecked. - Look remotes up as own properties, so a name like constructor is not a remote, and reject a default that is not a remote name. - Settle the hidden prompt when standard input ends or errors. - Stage upload sessions one directory per App, scan only the App's own sessions when an upload starts, and sweep every App's expired sessions at most once an hour. - Drop the unreachable not-found branch from getReleaseSummary. * fix(hub-cli): roll back under the default key, name a missing Hub, cancel login cleanly - `hub deploy --release-id` of a Release the App deployed before and has since moved on from was answered with that earlier deployment, so the documented rollback deployed nothing and exited 0. A reused deployment that is not the App's latest now leads to the next key, derived from it; a retry walks the same keys and still repeats nothing. A given --idempotency-key stays exact. - A 404 without a Hub error code, as at a mistyped mount path, fails with `HUB_NOT_FOUND` and suggests `hub remote list` instead of a bare `HUB_REQUEST_FAILED` or an "unknown result". - Ctrl-C at the `hub auth login` prompt reports `LOGIN_CANCELLED` (exit 130) rather than `LOGIN_FAILED`. |
||
|
|
e77641ba3c |
feat(queue): rebuild @nocobase/queue on BullMQ 6.3.6 (#516)
* feat(queue): rebuild @nocobase/queue on BullMQ 6.3.6 Replace the @boringnode/queue wrapper with a per-application QueueService whose producers, consumers and managers are bound to named queues. A queueBackend of redis (or a registered BullMQ backend factory) runs on BullMQ's public Queue and Worker APIs under a hash-tagged prefix; inMemory runs in the process and persists unfinished jobs to a state file at shutdown. Both implementations share one consumer layer and one contract test suite. - app-server: QueueServiceProvider, queueServiceToken and AppQueueConfig replace QueueProvider and its tokens; the queue migration source and planAppRuntimeDatabaseTasks are removed; queue.jobs is deprecated, ignored and reported once per plugin - templates: compose QueueServiceProvider, declare memory and redis keys, drop the @/jobs alias - create-plugin: remove the server.jobs capability - examples: rewrite the queue example on the new API - docs, Skills and AGENTS.md describe the new contract * docs(jobs): make JobExecutor the default for background work The previous commit rewrote every Queue Job reference onto the new queue API, but most of what those jobs did belongs to @nocobase/jobs now. - create-plugin: server.jobs generates a JobExecutor job and the provider that owns the plugin's executor instead of being removed - plugin and application development Skills: choose JobExecutor for one-off work, ScheduleExecutor for recurring work, Scheduler when administrators track it, and a queue only for delays, priorities, deduplicating IDs, batches, rate limits and fan-out; jobs receive services by closure - Scheduler Skill, evals and docs: hand a target's lengthy work to a JobExecutor with the occurrence's execution record as the reference, and let the job decide its terminal outcome; describe Scheduler's own jobs backend instead of the removed queue connection - deployment and Notification Skills: cover the jobs and queue backends instead of a queue manager * refactor(queue): select the implementation with adapter queueBackend used to pick both the implementation and a BullMQ backend factory. A configuration now selects the implementation with `adapter: 'inMemory' | 'redis'`, and the optional `queueBackend` only names a factory registered with `registerBackend()` for the redis adapter, defaulting to BullMQ's Redis backend. A registered factory interprets its own connection, so `connection` is required only on the Redis backend. `inMemory` is no longer a reserved factory name. |
||
|
|
7534fb6b99 |
refactor(templates): add shadcn/ui primitives on demand with an embedded shadcn/ui skill (#464)
* 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> |
||
|
|
a859ba1ede |
fix(jobs): stop the memory schedule adapter from crashing on a past-date timer (#511)
* fix(jobs): stop the memory schedule adapter from crashing on a past-date timer * test(jobs): assert a long-delay firing does not start a millisecond early --------- Co-authored-by: Drol <cgyrock@gmail.com> |
||
|
|
52f981169d |
refactor: run notification and workflow tasks on @nocobase/jobs (#502)
* refactor(notification): run deliveries on @nocobase/jobs Submit each Delivery to the plugin's own JobExecutor from @nocobase/app-server/jobs instead of the queue manager. The plugin sets the executor up when it starts, so deliveries are consumed on every jobs backend; nothing consumed the queue's default queue on a redis or database connection. A running delivery now finishes before its Provider is closed. notification.jobs selects the jobs configuration and rejects a name the jobs section does not define. The reconciler stays process-local: delivery leases make a scan from every instance harmless, and an `every` rule would fire before install mode has migrated the notification tables. * refactor(workflow): run tasks on @nocobase/jobs Publish and consume workflow tasks through the plugin's own JobExecutor from @nocobase/app-server/jobs instead of the queue manager. The job class closes over the engine's dispatch, which removes the module-level registry the boringnode locator forced on the adapter, and the jobs namespace replaces the workflow:<appName> queue name. Tasks are sent as strict JSON with undefined optional fields left out. workflow.jobs selects the jobs configuration and rejects a name the jobs section does not define. The unused delayed-publish path, the @boringnode/queue dev dependency and its lint exception are removed. * test(workflow): cover per-application task routing on one scope |
||
|
|
0459df1dda |
feat(ai-employee)!: rebuild AI settings on child routes and plugin-owned UI (#501)
* feat(ai-employee): add plugin-owned shadcn primitives and route overlays Generate the base-nova shadcn primitives the settings pages need into client/components/ui, adapted to relative .js imports and explicit declaration types, and copy the UI Library route dialog, drawer and overlay into the plugin. ConfirmDialog is rebuilt on those primitives. The plugin owns its components.json and generation stylesheet and keeps shadcn, tailwindcss and tw-animate-css as development dependencies; @base-ui/react stays a peer only. * refactor(ai-employee): route AI settings pages and guard unsaved edits Give each employee detail Tab, Skill and Tool detail, LLM service model editor and MCP server tool list its own child route, so every view is addressable, restorable on refresh, and follows back and forward. The parent pages render an Outlet and derive the active Tab from the URL. Unsaved employee drafts and model edits are protected without a data router, since the host renders a BrowserRouter where useBlocker throws: - explicit leaves (switching employees, closing the model editor) ask through one confirmation; - browser back and forward to another employee or out of the model editor are held by the page that stays mounted, which keeps the draft rendered and undoes the traversal when the user keeps editing; - reloading or closing the page warns through beforeunload. Settings UI uses the plugin's own Tabs, Button, Alert, Badge, Empty, Item and Field primitives instead of hand-built controls, associates every label with its control, and shares one Ask/Allow permission menu between employee tools and MCP tools. Remove the unused conversation-center settings page and route while keeping the public conversation client methods, the Registry recipes and the deprecated conversationCenterPath constant. * refactor(ai-employee)!: remove the deprecated AI settings tab registry registerAISettingsTabs, getAISettingsTabs and AISettingsTabDefinition fed a Tab bar the settings shell stopped rendering, and the activeTabKey and onTabChange props of AISettingsShellProps were accepted with no effect. Remove them together with the unused SettingsTabPage and the unreachable Tab navigation in SettingsShell. AI settings pages are contributed as Settings routes with parent: 'aiGroup'. The changeset moves to minor because the removed names were public exports of @nocobase/app-plugin-ai-employee/client. BREAKING CHANGE: registerAISettingsTabs, getAISettingsTabs, AISettingsTabDefinition and AISettingsShellProps.activeTabKey and onTabChange are removed. * test(ai-employee): let the permission menu close between choices Choosing a second MCP tool permission right after the first could click the trigger while the menu was still closing, which toggled it shut and left no item to pick. Wait for the menu to close after each choice. * feat(ai-employee): guide LLM service setup through a coding agent LLM services are declared in the application configuration, so an empty LLM services page cannot offer an add action. It now explains that, and walks the user through three steps beside a prompt they can copy: open a coding agent in the application directory, send it the prompt, then set the API key and restart. The prompt asks the agent to follow the application's AI employee Skill, to let the user choose the provider and models, to leave the API key to the user, and to summarize what remains to be done. Copying falls back to selecting the prompt when the clipboard is unavailable. * feat(ai-employee): guide MCP service setup through a coding agent MCP services are declared in the application configuration as well, so an empty MCP services page now offers the same steps and a copyable prompt as the LLM services page. The MCP prompt asks the agent which service and transport to use, keeps credentials in headers or env rather than in url or args, and ends with a configuration check. The first step, opening a coding agent in the application directory, is now one shared translation for both pages. * fix(ai-employee): stop the service tables flashing before the empty state While loading, the LLM and MCP services pages rendered their table with a loading row, then replaced the whole table with the empty state once an empty list arrived, which showed as a brief flash of the table. Loading now renders only a status with no shape of its own, and that status appears after a short delay, so a quick response goes straight to the table or the empty state. |
||
|
|
fac3d58f02 |
Add template printing example with PDF support (#491)
* feat(examples): add template printing example * docs(examples): align template print home label * fix(template-print-example): wait for empty-state hint * feat(examples): route notification card to task example |
||
|
|
3d44c4cd55 |
feat(jobs): add independent one-off job executor (#489)
* feat(jobs): add independent one-off job executor * feat(app-server): expose scoped job executors * docs(jobs): document one-off executor usage * fix(jobs): keep ordinary task identity independent of configuration key Renaming a jobs configuration key, re-pointing jobs.default, or replacing the built-in default with jobs.default: memory changed the ordinary Redis queue name and memory state file, silently stranding pending tasks. Identify ordinary tasks by connection or storage path, namespace and scope, as Schedule does. Memory keys naming the same file now share one executor, and conflicting concurrency or attempts for that file are rejected. * refactor(jobs): group job and schedule sources into their own directories Move ordinary job modules under src/job and schedule modules under src/schedule, dropping the job- file prefixes. Shared configuration, service, types and scope validation stay at the package root. Tests follow the same layout. The public exports are unchanged. * refactor(jobs): rename shared Schedule configuration types to Jobs The jobs configuration, adapter, connection, retention, logger and fallback types configure ordinary and recurring executors alike, so ScheduleConfig becomes JobsConfig and the related shared names follow. Shared error messages now say jobs instead of schedule. Types specific to recurring rules keep their Schedule names. * feat(jobs): let job handlers report attempt progress Add reportProgress(percent) to the job execution context. It validates a 0-100 value, stores it through the backend (BullMQ job progress on Redis, nothing on memory) and emits a local JobProgress event between JobStart and JobEnd. Reports after an attempt settles reject. Split JobStart and JobEnd into separate JobEvent members so that narrowing on name reaches JobError, and declare reportProgress as a property so handlers can destructure it. * feat(examples): replace the schedule example with a jobs example Rename @nocobase/app-plugin-schedule-example to @nocobase/app-plugin-jobs-example and give it a client with two pages under a Jobs example menu group. One-off jobs: a ProgressJob reports 10% each second through reportProgress; the provider keeps task records in memory and pushes every change on a user-audience realtime topic that the page subscribes to only while it is open. Schedules: four fixed rule slots whose handlers are registered before setup(), so a rule started from the page keeps running across restarts. Changes are announced on a public topic without data and the page reloads the rules through the authenticated route. The examples template registers the plugin's server and client in place of the schedule example. The consumed schedule example changeset drops the removed package so the next prerelease run can still read it. |
||
|
|
dfdd44902f |
refactor(workflow): refactor dsl (#478)
* feat(workflow): add typed DSL refactor demo * refactor(plugin-workflow): refactor dsl * fix(workflow): fix test case * refactor(workflow): update skills * fix(workflow): fix test case * refactor(workflow): change unpublished route to hash |
||
|
|
71361a843b | feat(ui-library): publish DatePicker and DateTimePicker as registry components (#488) | ||
|
|
414956d574 |
feat(ai-employee): discover and test models before startup (#483)
* feat(ai-employee): add model discovery and test commands * docs(ai-employee): consolidate LLM configuration guidance |
||
|
|
aeff80a1d2 |
feat(jobs)!: add @nocobase/jobs and move Scheduler onto it (#463)
* feat(schedule): scaffold the @nocobase/schedule package (T001) Adds packages/libs/schedule on the shared dev-config presets with BullMQ 6.3.6, cron and cron-parser as dependencies, lists it among the isolatedDeclarations libraries, and records msgpackr-extract, which arrives through BullMQ, as a deliberate build skip both here and in the allowBuilds that create-app writes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(schedule): add the service contract, configuration fallback and validation (T002) Exports the ScheduleExecuteService, ScheduleExecutor and job types from the plan and createScheduleExecuteService. The service resolves a named configuration, then schedule.default, then a built-in memory configuration it reports through onFallback; returns one executor per (scope, configuration); and rejects a repeated call whose overrides change the effective concurrency or attempts. Scopes, job names and job options are validated before anything reaches an adapter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(schedule): add the in-memory scheduler (T003) Adds the executor shared by both adapters: it registers handlers, holds jobs added before setup() and writes them there, skips a write whose rule, payload and execution settings are unchanged, passes immediately only for a new rule, keeps handlers after removeJob, fails firings without a handler as handler-not-registered, and delivers subscriber events in order. The memory adapter plans firings with cron-parser and fires them through one-shot cron jobs created from a Date. Every rule, its next firing and its firing count persist to a versioned state file per namespace and scope, written atomically; a missed firing runs once after a restart. An exclusive lock file refuses a live holder or another host and takes over a lock left by a crashed process. A cron rule without tz runs in UTC. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(schedule): add the redis adapter on BullMQ job schedulers (T004) The namespace is the queue prefix and the scope the queue name. Rules map to upsertJobScheduler with a template carrying the payload, attempts and retention, compared through getJobScheduler before any write. A worker runs firings with the configured concurrency and reports its local active, completed and failed events; a firing without a handler fails as an UnrecoverableError and is reported as handler-not-registered. Shutdown aborts running handlers and waits for them before closing. Only public Queue, Worker and Job API is used. The planned time is job.timestamp plus the delay in job.opts, because BullMQ zeroes the job's own delay once the job leaves the delayed set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(schedule): run the redis adapter against a real Redis (T005) Adds test:integration, which starts Redis in a disposable Compose project, runs tests/integration against it and removes the project. The suite covers one execution per firing across instances, continuing on another instance after a shutdown, planned and next firing times, skipping an unchanged write so a due firing survives, a changed rule replacing it, immediately firing once, re-adding a removed job, a missing handler failing without retry, retries, shutdown waiting for a running handler, limit, and listing. Measuring against BullMQ 6.3.6 changed two things in the adapters. An end date already in the past is rejected by upsertJobScheduler, so both adapters now remove such a rule instead of writing it. A scheduler whose limit is spent keeps reporting its last planned time as next, so a next firing that is not after the current one is reported as none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(app-server): compose the schedule service under @nocobase/app-server/schedule (T006) Adds scheduleExecuteServiceToken, AppScheduleConfig and ScheduleExecuteServiceProvider. The provider builds the service from the schedule section with the application name as the default namespace and storage/schedule as the built-in memory location, reports a fallback to that configuration outside develop and development, starts nothing, and shuts down whatever executors their owners left running. @nocobase/schedule is a peer, provided by the application. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(app-plugin-scheduler)!: schedule through @nocobase/schedule (T007) Scheduler now hands each definition's rule to its own executor from @nocobase/app-server/schedule, on the @nocobase/app-plugin-scheduler scope with concurrency 1 and a single attempt, instead of the queue's schedule projection and ScheduleDispatchJob. - A new migration adds nextRunAt, lastRunAt, runCount, appliedLimit and lastOccurrenceId to scheduleDefinitions and scheduledAt to scheduleOccurrences; the published migration is unchanged. - Startup registers every definition's handler, adding rules for the enabled ones, before setup(); sync-only mode sets the executor up without consuming and starts no reconciliation. - A rule is rewritten only when its definition changed, carrying the limit left after the firings already run; disabling removes the rule and keeps the handler. - The occurrence id is the firing's job id; starts count once per occurrence; a target that is not ready now records a skipped occurrence instead of failing with OCCURRENCE_NOT_FOUND. - Shutdown clears the reconciliation timer, unsubscribes, and waits for the executor. BREAKING CHANGE: ScheduleDispatchJob is no longer exported, the plugin contributes no queue jobs, and nothing is migrated from queue_schedules or queue_jobs. Applications must compose ScheduleExecuteServiceProvider and provide @nocobase/schedule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(templates): compose the schedule service in every template (T008) Each template adds ScheduleExecuteServiceProvider with the application's NODE_ENV, a server/config/schedule.ts offering memory and redis configurations without choosing a default, and @nocobase/schedule as a production dependency, and drops the queue that existed only for the Scheduler. config.example.yml documents the section, and the deployment Skill covers choosing the backend for one or several instances. Tests that start applications now keep schedule state in their own temporary directory: two suites starting apps on the template's storage/ at the same time are two processes on one memory state file, which the adapter's lock refuses. The deployable dist records msgpackr-extract as a deliberate build skip, as the repository and create-app already do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor!: replace @nocobase/cron with @nocobase/schedule (T009) Removes packages/libs/cron and the dependency the default and examples templates declared on it; nothing imported it. The application development Skill's section on work that runs on a schedule now uses an executor from scheduleExecuteServiceToken under the application's own scope, set up in start() and shut down in shutdown(), and explains that the redis adapter, not a hand-written lock, is what keeps several instances from firing the same job. The cron catalog entry stays for @nocobase/schedule. BREAKING CHANGE: @nocobase/cron is no longer developed here; its published 0.1.0 remains installable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(scheduler): describe scheduling on the schedule service (T007) The Scheduler capability page still described the queue's schedule projection: a schedule queue worker, queue_schedules, a paused queue schedule, and limit as queue claims. It now says where schedules run — the schedule configuration, memory on one host unless schedule.default names redis — in both languages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(changeset): describe the schedule service and the Scheduler move to it (T010) Adds changesets for @nocobase/schedule and @nocobase/app-server, the breaking Scheduler change with no migration from the queue tables, the templates and app-skills replacing @nocobase/cron with @nocobase/schedule, and the msgpackr-extract build skip in create-app and app-tools. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(schedule): share the memory adapter's state across the processes of one host (T011) The memory adapter held an exclusive lock for its whole lifetime, so a second process — nb3 schedule:sync while the application ran, or a PM2 cluster — was refused. It now reads its state file without a lock, since the file is replaced by rename and always whole, and takes a short exclusive lock only for each read-modify-write, reading the file again inside it. Each consuming process arms every planned firing and claims it under the lock by advancing the rule past it, so a firing runs once among them, and polls the file's signature to pick up rules the others changed. A lock left by a process that no longer runs, or held far longer than a write takes, is taken over. Every write records a revision and its writer; a file another host writes after this process started is refused, since a shared file system gives neither atomic rename nor exclusive create reliably. The executor passes its unchanged-rule check to the backend so memory decides it again under the lock. Documentation, Skills, the deployment guidance, the templates' comments and the changesets now describe memory as one host rather than one process. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(schedule): keep the memory adapter's state in memory and write it on shutdown (T012) The memory adapter now reads its state file once, in setup(), changes only its memory while it runs, and overwrites the whole file in shutdown(), after it stops firing and running handlers finish. This replaces the shared, lock-guarded file of T011: there is no lock, each process holds its own copy, and the one that shuts down last overwrites the file. A process that ends without shutting down loses what changed since it started, and a setup() that could not read the file never overwrites it. The lock module, the file's revision and writer, the signature poll, the executor's unchanged-rule argument, the multi-process fixture and its tsx dependency are removed. Documentation, Skills, deployment guidance, template comments and changesets describe memory as one process whose state reaches storage when the application stops. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(app-plugin-scheduler): remove stale rules again on every synchronization (T013) Synchronization only ever added rules; it removed one only when it deactivated a definition itself or an administrator disabled it. A removal that was lost stayed lost: on the memory adapter a running application overwrites the state file nb3 schedule:sync --finalize wrote, so the rule of a definition removed from code came back and failed as handler-not-registered at every firing. Every synchronization now removes again the rule of each disabled definition in the manifest and of each definition already inactive, once the executor is set up. It reads the definition again first, so a schedule another instance enabled in the meantime keeps its rule. An active definition a partial manifest leaves out is still left alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(schedule): explain how applications and plugins use the schedule service The README now covers when to use the package rather than the Scheduler plugin or a queue job, how an application composes it, the schedule configuration and how a consumer's configuration is chosen, the provider pattern for plugins and application code with its scope, ordering and shutdown rules, dependency declarations, job options and their edge cases, receipts, handlers and orphan rules, events, both adapters' guarantees and limits, and testing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(examples): add a plugin running a recurring job on the schedule service @nocobase/app-plugin-schedule-example shows the pattern the schedule README describes. Its provider takes an executor of its own from scheduleExecuteServiceToken under its package name, subscribes to its events, registers a heartbeat that runs every minute, sets the executor up, removes the rules of jobs it no longer defines, and shuts the executor down with the application. The handler records each run from its own context, since events arrive after the fact. An authenticated GET /api/schedule-example returns the next firing and the recent runs. The examples template registers the plugin and covers the route end to end on the application's schedule service. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: follow the consolidated app-cli after rebasing onto develop @nocobase/app-tools is now part of @nocobase/app-cli, so the msgpackr-extract changeset covers app-cli, whose dist workspace carries the entry. The Scheduler's command is nocobase scheduler sync in the changeset and the capability page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor!: rename @nocobase/schedule to @nocobase/jobs and let Scheduler choose its configuration The package moves to packages/libs/jobs as @nocobase/jobs, its configuration section from schedule to jobs, and the built-in memory configuration's state from storage/schedule to storage/jobs, so the section no longer reads like the Scheduler plugin's. Nothing was published under the old name. Scheduler gains a scheduler section: scheduler.jobs, or SCHEDULER_JOBS, names the jobs configuration its schedules run on, and without it they follow jobs.default. The jobs service would quietly fall back to the default for a name it does not know, so Scheduler refuses to start instead. The default and examples templates declare the section. @nocobase/app-server/schedule and the ScheduleExecute* identifiers keep their names for now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(jobs): report the planned time BullMQ stored in addJob receipts upsertJobScheduler returns a job whose timestamp and delay come from two clock readings, one in JavaScript and one in its script, so their sum could miss the planned time by a millisecond even for a cron rule. The receipt now reads the scheduler's own next, which is the planned time itself, and falls back to the job only when there is none. The Redis suite's check that a replaced firing runs at the receipt's time failed on that millisecond. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(jobs): rename JobExecutorService and use configured executor settings * fix(app-cli): resolve native platform packages from declared dependencies * fix(jobs): exclude optional native accelerator and revert retarget changes --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
9f75a27b15 |
refactor(cli): build the --json envelope and the Node.js guard from one @nocobase/cli-envelope package (#474)
* refactor(cli): build the --json envelope and the Node.js guard from one @nocobase/cli-envelope package
`@nocobase/app-cli`, `@nocobase/app-installer`, `@nocobase/create-app` and `@nocobase/create-plugin` each kept a copy of the `--json` envelope and of the Node.js version guard their `bin/run.js` runs before loading anything else. The copies had drifted once already, and a cross-package parity test was the only thing holding them together.
The new `packages/libs/cli-envelope` holds both: the envelope's types and builders in `src/index.ts`, and the guard in `node-guard.js`, plain JavaScript with no imports so that it runs on the Node.js it is there to refuse and on the TypeScript sources a development checkout runs from. `formatCommandLine` and `renderSuggestion`, which app-cli and app-installer also each had, move with them. The four packages declare it as an ordinary dependency: its envelope mark is a registered symbol, so two copies agree, and nothing else in it holds state.
The parity test stays, now comparing each tool's thin wrapper with the application CLI's document. `AGENTS.md` names the package as where the envelope is defined. app-cli's guard now answers `--json` with the `NODE_UNSUPPORTED` document like the other tools, and app-installer's guard takes the first non-flag argument as the command.
* refactor(cli-envelope): derive the guard's command, check node-guard.js against its declarations, vendor published files
Review follow-ups for the shared envelope package.
- `unsupportedNodeVersionOutput` names the command from the arguments before the first flag, through the new `commandFromArgv`, unless the tool names one. app-cli and app-installer drop their own derivations; the installer's took a leading flag's value for the command.
- `tsconfig.node-guard.json` checks `node-guard.js` under `checkJs` and the tests with it, and `node-guard.js` assigns its exports to `typeof import('./node-guard')`, so the hand-written declarations cannot drift from the JavaScript.
- `build-server-dist-package.mjs` vendors every path a workspace package's `files` names rather than `dist` alone, so the `./node-guard` export of the vendored `@nocobase/cli-envelope` no longer points at a missing file.
- The parity test keeps one comparison of the shared guard against the application CLI's failure document instead of three identical ones; `formatError` reads an InstallerError's details directly; AGENTS.md and the AppCommand header no longer describe the per-tool copies this branch removed.
---------
Co-authored-by: li'mac <limac@Lis-MacBook-Air.local>
|
||
|
|
84cc7d2c3b |
feat(hub-cli)!: move Hub publishing out of app-cli into @nocobase/hub-cli (#470)
* feat(hub-cli)!: move Hub publishing out of app-cli into @nocobase/hub-cli `release upload` and `release deploy` leave @nocobase/app-cli and become `hub upload` and `hub deploy` in the new @nocobase/hub-cli package. The `nocobase.cli.publishing` flag is removed: an application gets the commands by depending on the package, and the Default template declares it in devDependencies. `hub deploy` uploads the archive and deploys it; with `--release-id` it deploys a Release already on the Hub. `hub upload` only uploads. app-cli gains a general mechanism for this. A direct @nocobase/ dependency whose package.json names a CLI entry in `nocobase.cli.entry` contributes that entry's defineCliPlugin commands without an entry in cli/plugins.ts, and a package that is not an application plugin takes its topic from its name without the `-cli` suffix. Finding such packages reads JSON only; a run imports one only for a command under its topic, whole-tree help and `commands`, and a declared package nobody installed is reported as PACKAGE_NOT_INSTALLED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app-cli): accept colon-joined ids for CLI packages and check the entry's topic The package a run imports was chosen from the first space-separated word of the command line, so `hub:deploy`, which oclif accepts as `hub deploy`, matched no topic and the package was never loaded; the same split kept a colon-joined id off the PACKAGE_NOT_INSTALLED path. Both now split on a space or a colon. A loaded entry is also checked for the topic its package name gives, not only its package name, so an export that mounts under another topic is rejected rather than assembled under one the run did not select. The entries of several packages are imported concurrently, with the first failure in package order reported. The PACKAGE_NOT_INSTALLED message no longer asserts that an uninstalled package provides commands, since that cannot be known without its manifest, and `hub deploy --help` says the default key with --release-id includes the --config content, as the README and the code already did. --------- Co-authored-by: li'mac <limac@Lis-MacBook-Air.local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
fbbc5cd3ec | feat(templates): include template-print skill in all templates (#452) | ||
|
|
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. |
||
|
|
aafa3c39ef |
feat(hub-installer): start through a launcher, report endpoints, and make suggestions runnable (#448)
* 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. * feat(hub-installer): start through a launcher, report endpoints, and make suggestions runnable pm2 now runs launcher.mjs in the Hub root. On every start it reads hub.env and resolves `current`, then becomes the Hub through process.execve, keeping the pid pm2 watches and passing node flags on. A plain `pm2 restart` therefore applies an edited hub.env or a switched release; ecosystem.config.cjs no longer captures either. - status reports `endpoints`: the public URL and origin, and the host and port the Hub listens on. - install reports `initialAdmin` (username, email, and whether the password is still the template default, never the password), and takes --dir like the other commands. - Every suggested command and --help example runs as printed: npx with the registry named, pinned to this installer's version, paths quoted. Suggestions that were not commands, and the release CLI's own commands, become messages. - pm2 4.3 or later is required, the first release that reads ecosystem.config.cjs as a configuration. - Under --json, an unsupported Node.js prints the error envelope. - The README documents every error code, changing the origin or port, and the launcher. The smoke test switches releases with `pm2 restart` and checks that the pm2 process runs the expected release's server entry. * fix(hub-installer): address the launcher review - bin/run.js waits for stdout and stderr to flush before exiting, so a --json document on a slow pipe arrives whole; a plain process.exit cut a 2 MB write to 64 KB. - endpointsOf drops a trailing slash from a hand-edited origin and reports a port that is not one as null, with a warning from status; healthUrl reads host and port from it instead of repeating defaults. - install reports `endpoints` like status, and --version --json prints the same envelope as every other command. - pm2's version is the line of `pm2 --version` that is one, and a version that cannot be read is reported as such rather than as a pm2 older than 4.3. - A failed automatic rollback that did not restore the database says where the pre-upgrade database is and where it belongs again. - The launcher comment and the README state what pm2 actually does: it gives the launcher an IPC channel that execve drops, and ignores node_args for interpreter 'none', so node flags go in `args` or in NODE_OPTIONS. - A folded application CLI command no longer starts with a space when the suggestion had no message. |
||
|
|
7682daf6e5 |
feat(hub-installer): install a Hub from the published template (#444)
* 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. * 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. |
||
|
|
2f97f0016c |
feat(authorization): department subjects — inspector inheritance, localized subject titles, an organisation Skill, and a departments example (#435)
* feat(app-plugin-authorization): list a subject's members and link to its management page Subject types may implement members and manage; the assignments tab shows a members drawer and a manage link, the inspector explains inherited sets, and assignment changes on non-user subjects refresh every client. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(app-skills): build an organisation dimension and wire it into authorization Add organization.md to the application development Skill, covering the department model, service, routes, settings pages, subject type, refresh, seeds and tests, and link it from the reference table and authorization.md. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * feat(examples): add a departments example wired into authorization A department tree registered as the org.department subject type, with settings pages, a directory page, seeds and tests; registered in the examples template. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(app-skills): fill the gaps the departments example exposed Cover the plugin layout, the full service, 403 mapping, id-list record access, empty scopes, demo accounts and the test harness. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * refactor(authorization-example): replace team subjects with the departments example Drop the example's team subject, membership and team assignments; order relations now target carriers, and inherited jobs point to the departments example. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * fix(authorization): answer 403 for a denied require and an empty result for an empty scope AuthorizationDeniedError now answers 403 through Hono's default error handler, and a granted record access that selects nothing yields a scope matching no rows instead of a denial. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * feat(departments-example): seed demo accounts and show inherited, multi-department and direct access Demo accounts are written by the seed with hashPassword, and the workarounds for denied require and empty scopes are gone. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(app-skills): seed demo accounts directly and drop fixed workarounds Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * refactor(app-plugin-authorization): drop subject members and manage links, translate subject titles Removes members/manage for now and lets subject options carry { key, ns } titles rendered in the viewer's language. Inspector inheritance, the global refresh and the 403/empty-scope fixes stay. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * feat(departments-example): build departments on the authorization example's sales domain Seeds a trading company that reuses the sales permission sets and rules, syncs department regions into the sales data scope, and rebuilds Settings → Departments in the workspace style, localized in English and Chinese. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(app-skills): update organization.md for localized names and attribute sync Drops members/manage and describes one localized name, descriptor titles, attribute sync into business scopes, and department baselines with direct job roles. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(authorization): split the organisation Skill and make authorization Skills self-contained The organisation reference becomes an entry page with four sub-references, and no authorization Skill depends on the example plugins. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * feat(authorization-example): make the rule plugins optional The seed skips default-access, sharing and restriction rows when their Collection is absent. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * feat(departments-example): add department heads, department data scopes and cross-department sharing Heads hold a fixed subject; three owner-based scopes cover projects, quotes and orders; the seed works without the rule plugins. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(app-skills): add an organisation permission design guide and department scopes reference Covers job-role sets, department heads, scope choice and optional rule plugins with permission-set alternatives. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(authorization): link the authorization Skills to the organisation permission design guide Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * fix(departments-example): notify heads above a moved department Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * feat(departments-example): share specific sales projects with Delivery and seed the example's own sales data Drop the selected-department scope, give Delivery an own-projects viewer set, stop placing the authorization example's accounts in departments, and seed a few projects, quotes and an order owned by this example's staff. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * docs(app-skills): drop the selected-department scope and share cross-department records by id The department scopes are viewer-relative, so the guide now shares specific records with a department through a sharing rule, and says what remains with permission sets alone. Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * test(ai-employee): expect an empty result for an empty record scope Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE * chore: consolidate the changesets for department subjects Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE |
||
|
|
ec92b20eda |
feat(app-cli)!: AppCommand authoring API and one --json envelope for every command (#441)
* feat(app-cli)!: add the AppCommand authoring API on the root entry
AppCommand exposes rootDir, withApp(({ app, env }) => ...), setStatus and
appPath() flags; --json prints one envelope for success and failure, and
CommandError carries a code, suggestions and details. The authoring API moves
from ./plugins to the package root, a ./testing entry provides
bindAppCommand, the runner closes runtimes a command left open, and console
logs of an application a command creates go to stderr.
* feat(app-cli)!: move every command to the AppCommand output contract
Built-in, plugin and generated commands extend AppCommand, return their
result, and throw CommandError on failure, so --json prints one envelope for
every command. Plugin commands reach the application through withApp() and
take appPath() flags; the CLI example's artifact build is a devCommand.
The testing entry exports runAppCommand next to bindAppCommand, dev-config's
ESLint presets refuse the patterns the contract forbids in cli/, app-cli is
registered as identity-sensitive, and the application and plugin-development
Skills each carry the command-authoring guide, kept identical by a test.
* feat(app-cli): keep command diagnostics on stderr and behind NOCOBASE_CLI_DEBUG
An AppCommand's logToStderr keeps writing under --json: stderr never carries
the document, so progress and confirmations no longer disappear for the agent
that asked for JSON. A CommandError keeps its cause off the chain oclif prints,
and NOCOBASE_CLI_DEBUG prints the redacted error chain behind any command's
failure, from source or dist, replacing release's own handling. The Skills
describe using the application's logger inside withApp() for records that
belong in its log files.
* feat(app-cli): make commands discoverable, previewable and recoverable for agents
- `nocobase commands --json` lists every registered command with its source,
arguments, flags and whether it takes --json, --dry-run and --force; a
deployment lists no development command.
- Invalid usage suggests the flag or command that was probably meant and the
command's --help, drops oclif's trailing hint from the message, and always
exits 2.
- db apply, rollback, redo and reset take --dry-run and return the plan;
FORCE_REQUIRED carries that plan and the --force/--dry-run forms; a busy task
lock fails with DATABASE_LOCKED, backed by TaskLockBusyError in
@nocobase/db. Help examples lead with --dry-run.
- release reports upload and deployment progress on stderr while it waits.
- The runner no longer exits 13 with an unsettled top-level await when a
reader such as `| head` closes stdout early.
* fix(app-cli): address review findings on the command contract
- withApp() and the db commands report a failure to put the application away as a warning instead of failing work that already succeeded or replacing the failure it ended in; a rerun of `db rollback` no longer rolls back a second batch
- suggestions and config init's next steps name `node <dist>/cli/index.js` in a built dist/, which has no pnpm; AppCommand gains `this.cliCommand(args)` for the same in a command's own suggestions
- usage errors never repeat a typed value, list allowed values and the flags a conflict involves, and release upload/deploy report them as INVALID_USAGE like every other command; `this.error()` reports COMMAND_FAILED rather than invalid usage
- NOCOBASE_CLI_DEBUG prints a failure's diagnostics once, and oclif's raw stack no longer replaces the message and its suggestions
- appPath() defaults declared in baseFlags resolve against the application root
- every --dry-run answers success-noop, and so does a db command that found nothing to do
- a dry run creates no SQLite file that does not exist yet: drivers may report hasStorage(), and a dry run then answers for an empty database without connecting
- the runner's safety net destroys a leaked runtime's scope even when shutdown fails
- a command bound without an id is named after its class, and the Skills' examples name app commands as the runner does
* fix(app-cli): address the remaining review threads on the command contract
- a single misspelled topic, such as `plugn`, now suggests that topic's help instead of only the full command list
- `commands --json` marks an appPath() default with `defaultRelativeTo: "application-root"`, matching what --help says
- the reason oclif gives for a `combinable` conflict is repeated instead of collapsing to a generic message: the pattern expected "Only of the following" where oclif writes "Only the following"
- the runner's own EPIPE handler is removed: it never installed, because @oclif/core swallows EPIPE on stdout at import, and exiting there would have skipped withApp() and the runner's cleanup; a test now covers a command that keeps printing after its reader leaves
* test(ai-employee): give the Skills tests' first render room to load the page
The first render in the file waits for the employee heading with findBy's default 1 s, which the page's import graph can outlast when CI runs every package's tests at once. The Tools tests hit the same wait and got a 10 s timeout in #413; the Skills tests now match.
* test(app-cli): inspect a workspace of the test's own rather than the repository
The workspace-root inspection test from #439 scanned the real repository's packages/, where other app-cli tests create and delete temporary applications under tests/.tmp while they run. Run together, the scan read one mid-write and the inspection failed. It now builds the one-application workspace it needs.
|
||
|
|
dbf5631e1f |
fix: clear the leftovers of the CLI consolidation (#439)
* chore: replace the command ids left over from the CLI consolidation The scheduler and template-print documentation still named `schedule sync` and `skills:sync`, the template reference pages showed `pnpm migrate`, and the global create-app Skill taught `pnpm config:*` scripts that applications no longer carry. The create-app smoke test now records `<topic> <command>` ids directly instead of mapping them back to the old script names. * chore: drop references to the removed client:inspect and server:inspect Application inspection was removed in #329, but the plugin-development Skill, skills/README.md and a few comments still sent readers to `client:inspect` and `server:inspect`. `plugin inspect` covers registration state only, so the guidance now points there for registration and drops the composition diagnostics. The authentication test that asserts no command is prescribed now checks for `config init` rather than the old script name. * fix: clear the remaining leftovers of the CLI consolidation A second sweep after #431, #434, #437 and #438 found guidance and code that still described the old command line. - `plugin inspect` suggested the removed `pnpm plugin:register` and `pnpm skills:sync` scripts; it now suggests `pnpm nocobase plugin register` and `skills sync` with the flags that located the App, and the remaining colon-form hints name the real commands. - The Default template never registered the scheduler's CLI entry, so the documented `pnpm nocobase scheduler sync` did not exist there. - `@refinedev/cli` and `tsc-alias` are spawned by `nocobase build` and are now declared as optional peers of `@nocobase/app-cli`. - `peers:check` covers `@nocobase/app-cli` again after its move to `packages/app`, and `deps:check` scans plugin `cli/` sources and source directories named `dist`. - Skills, templates, plugin AGENTS.md copies, READMEs and the docs site now describe current commands, the `metadata/` directory from #437, the Hub archive path, `release` availability and the two `dist/package.json` scripts. Examples and Hub ignore `/.env`. * fix: address review threads on the CLI leftovers - the templates' Docker build contexts exclude build output where builds write it (each workspace package's dist/, the documentation sites') instead of every directory named dist/, so a source directory with that name, such as app-cli's dist commands, needs no exception - `plugin inspect` suggests `skills sync --package <full name>` rather than the compatibility `--plugin`, and a test covers suggestions for a `--workspace-root`/`--app` inspection - the CLI example Skill states when two plugin topics collide: once the scope and an `app-plugin-` prefix are removed - `peers:check` fails on a manifest that does not parse instead of leaving the package out - the smoke test's fake pnpm records a top-level command such as `nocobase build` by its own id, reading the top-level commands from app-cli - the plugins changeset no longer lists create-app, whose change another changeset already covers |