Shell facet

Table of Contents

An entity's shell commands are codegen output. The ores.cpp.shell-command facet emits one command unit per entity: a submenu that carries the get, add, delete and history verbs. The entity opts in from its own model, and the unit lands in the adapter part named for the entity's component.

Return to Knowledge.

1. The facet

Field Value
Facet ores.cpp.shell-command
Model type domain_entity
Level cross
Default disabled
Archetypes command_header, command_implementation, command_tests

The facet is disabled by default, so an entity receives a unit only when its model asks for one. The three archetypes are all per-entity, and each entity owns its own three files.

2. Opting an entity in

Add one property to the top-level properties drawer of the entity model, beside :ID::

:PROPERTIES:
:ID: <uuid>
:ores.cpp.shell-command.enabled: true
:END:

Then generate the unit:

./compass.sh codegen generate --model projects/ores.<component>/modeling/ores.<component>.<entity>.org --address ores.cpp.shell-command

3. File locations

The unit belongs to the adapter part named for its component, not to ores.shell.application.

File Path
Header projects/ores.shell/{component}/include/ores.shell/app/commands/{component}/{entity}_commands.hpp
Implementation projects/ores.shell/{component}/src/app/commands/{component}/{entity}_commands.cpp
Tests projects/ores.shell/{component}/tests/{entity}_commands_tests.cpp

Every consumer includes the header as ores.shell/app/commands/{component}/{entity}_commands.hpp, which is its path under the part's own include/ directory. The part declares #+component_kind: adapter, so codegen writes only its two file lists beside the hand-authored CMakeLists.

4. Command class layout

The commands class is all-static. It carries an inline static std::string_view logger_name with the fully-qualified logger path and a static lg() accessor. register_commands(cli::Menu& root_menu, nats_client& session, pagination_context& pagination) inserts one submenu named for the entity's plural, registers one lambda per verb, and calls pagination.register_list_callback for the list verb. Each verb is a separate static process_* method.

5. The two shapes

The template branches on has_audit_group, which is true when the entity model declares #+domain_audit_group:. Both shapes carry the same four verbs under the same submenu name, so the two shapes never change the command a user types. They differ in the history verb.

Shape History verb
With an audit group {entities} history <key>
Without an audit group {entities} history <key> [--diff] [--version <n>]

A no-audit entity holds change_reason_code, change_commentary and recorded_at as flat members in place of v.audit. The add verb fills them from the last two positionals and from the clock, so its arity stays the same in both shapes.

The --diff branch renders a unified diff through render_history_diff and returns early. A --version without --diff is rejected.

The add verb takes free-form tokens rather than one typed parameter per column. The cli library expands a typed handler into one template instantiation per argument, and the Windows clang toolchain abandons the mangler on the deeper chains. A token vector holds the argument list at a fixed shape.

6. Who supplies a column's value

The add verb fills one domain member per positional argument. The :supplied_by: annotation on a column says who supplies the value.

Value Meaning
user The default. The verb reads the value from the next positional.
minted The verb mints a fresh uuid. The default for a uuid primary key.
session_party The verb reads the acting party from the session, and fails when the account has no default party.
unset The verb leaves the member at its default and asks for nothing.

The tenant_id member takes no annotation. It is not a declared column, so the template names it directly and fills it from the session.

A malformed token names the field it belongs to, and the block that builds the entity sits inside one try. The handler reports the failure through fail() and returns.

7. Registration

One aggregator per component wires the units to the shell host. It is hand-written and lives in the application part at projects/ores.shell/application/src/app/commands/{component}/{component}_commands.cpp. It includes each unit's header and calls register_commands once per unit. repl::setup_menus calls the aggregator once at startup.

The facet has no component-scoped archetype for that aggregator today, so adding an entity means one include and one call in the aggregator, plus a refresh of the part's file lists. A generated aggregator needs a render-time list of the component's opted-in entities, the same pattern the generator already uses for _COMPONENT_FILES_TEMPLATES.

The units replace, they do not join. Delete the hand-written unit for an entity in the same change that generates its replacement, so the shell never carries two units for one submenu.

8. See also

Emacs 29.3 (Org mode 9.6.15)