uxc: your Uxopian projects, as code

Uxopian Editorial
Engineering  ·  Uxopian Software

One open-source client for every Uxopian product. It treats a project like software: files in git, deployed by command, checked against the server, shipped as a package.


In July we gave you a quick tour of the Uxopian lab, five things we were building beyond the product news. One of the stops was uxc, a small open-source command-line tool. It has grown a lot since, and it deserves the long version.

A FlowerDocs project is never just the product. Take our Case Management engine. Version 0.4 is 413 objects: 207 tag classes, 52 document classes, 18 virtual folder classes, 12 screens, 30 server-side scripts and handlers, 40 reference datasets, and an AI layer of 22 prompts, an agent and a plan. Add the fast2 maps that bring existing documents in, and you get a typical project: a lot of configuration, all of it living on a server.

For a long time, that is where it stayed. Moving a project to another environment meant redoing the configuration by hand. Nobody could say for sure what had changed on a server since last week, and two people editing the same handler found out the hard way.

uxc (for uxopian-client) is how we fixed that. It turns a project into a package: a folder of plain files that you version in git, deploy to any environment, compare with what is really on the server, and publish to a marketplace. Those 413 objects install on a blank scope with one command, along with 369 tests that check the engine works once it is there.

uxc is meant to be the one client for every Uxopian product. FlowerDocs and Uxopian AI support is mature and used every day. fast2 support is arriving now: a package can already carry fast2 maps and run campaigns.

What follows is the same 360° tour, told three times. Each pass goes one level deeper. Stop after any part and you still have the whole picture, just at a coarser grain.


Part 1 · The 360° view

A project, treated like software

uxc is a command-line tool and a library. You need Node on your machine and nothing else. It covers every piece of a Uxopian project:

  • the content model: document, folder and task classes, tag classes, ACLs, workflows, virtual folders;
  • the behavior: the server-side handlers and scripts that react when a document arrives or changes;
  • the screens: GUI configurations, and how the scope shows each class;
  • the AI: prompts, agents, multi-step plans, applications, LLM and MCP connections;
  • the data: reference datasets, such as a clause library or a list of contract types;
  • the migration: fast2 maps that feed documents into FlowerDocs;
  • the tenant itself: FlowerDocs scopes, created and deleted remotely.

The lifecycle, end to end

You start a package, or adopt what already exists on a server. You push it to an environment, and uxc deploys it in the right order and checks it before writing. Later, uxc tells you what changed on the server and what changed on your side, and you decide which one wins.

Packages carry their own tests, so you can check that the pipeline actually works where you just installed it. When it is ready, you export a single archive or publish a versioned addon to the Uxopian marketplace, and anyone can install it elsewhere with one command. A product can be extended by a partner package without being forked, and upgraded later without breaking that extension.

Built in layers

Packages stack the way software libraries do. The Case Management engine is a product package that sits on FlowerDocs and Uxopian AI. Purchase Order Management is an extension that sits on the engine: it reuses the cases, tasks and screens, and adds what is specific to orders.

Each layer has its own prefix, its own version and its own tests. Each one declares which version of the layer below it accepts. The engine can ship a new release without breaking purchase orders, and a third team can build the next vertical on the same engine.

Why it matters

The first gain is time. Setting up a project on a new environment used to mean redoing every class, handler and screen by hand. Now it is an install command.

The second is safety, and it matters more. Before an import writes anything, uxc lists every collision with what is already there. Marketplace archives are checked against their published hash, so a corrupted package never reaches a server. The traps that break FlowerDocs deployments are handled by the tool instead of by someone's memory.

Then trust. Two-way drift detection answers "what is actually on this server?" in seconds, and the tests that travel with a package answer "does it work here?". Product teams, projects, presales and partners share one catalog of packages instead of copying each other's servers.

Last, uxc was built for AI agents as much as for people. It answers agents in JSON and ships a Claude Code skill. Our agentic-plan work was built and deployed with it, end to end.

Open source, and moving fast

uxc lives in the open-source spirit: take it as it is, use it, fork it, share it, and send us pull requests. The code is on GitHub under the MPL-2.0 licence.

It is also very much alive. We have used it intensively on real projects over the last couple of months, and improvements land every week. One of the next steps is to build its API layer on the newer, officially generated FlowerDocs and Uxopian AI API clients. The direction is clear: one wrapper over every Uxopian product API, plus the development flow and the packaging that go with it.


Part 2 · How it works

A folder, kept honest with every server

Same tour, one level down: what is in the folder, what each command does, and the safeguards that run without you asking.

The package

my-package/
  uxopian-project.json   name, code (the id prefix), version, dependencies
  registry.json          every resource: kind, id, path, policy
  fd/                    classes, tag classes, handlers, scripts, GUI configs
  ai/                    prompts, agents, plans, applications, MCP connections
  f2/                    fast2 migration maps
  data/                  reference datasets, one JSON line per row
  tests/                 functional tests that ship with the package
  .uxc/state.json        sync state per target (never exported)

Everything is a text file, so git sees every change and a reviewer can read the diff. The package code (cm for Case Management) prefixes every id the package owns. That prefix is how uxc knows what is yours. It matters again later, when several packages share one server.

Build, or adopt what exists

uxc init starts a package and uxc add <kind> <Name> scaffolds a resource. The template is not a blank file. It already carries the API mechanics we verified on real servers. If the build already exists, uxc adopt --scan finds everything with the project's prefix on the server and pulls it into files.

Smaller helpers cover daily work. uxc schema shows a class and its tags in one table. uxc watch waits until a document reaches a state. uxc run calls a prompt or a plan. And uxc explain F00903 tells you what that server error means and how to fix it.

Deploy

uxc push sends resources in dependency order: tag classes before the classes that use them, scripts before the handlers that call them. Lints run first, so a tag value the server would reject fails on your laptop and not halfway through a deploy. Each resource is recorded the moment it succeeds. If item 7 of 20 fails, items 1 to 6 stay deployed, and uxc push --changed picks up where it stopped.

Some FlowerDocs rules are enforced rather than written in a wiki. Task classes are create-once. A handler redeploy registers a new version (_v13, then _v14) and cleans up the old ones. uxc also waits out the 45 seconds or so during which a fresh handler is not listening yet.

Drift, in both directions

uxc status --remote compares three things for each resource: the local file, the last synced version and the server. That single comparison separates a local edit (push it) from a server edit (pull it), a conflict (read uxc diff, then choose) and a resource someone deleted on the server. uxc never overwrites anything silently. A conflict needs an explicit --force.

Proof that it works here

A package can ship tests that run against a live instance. uxc test creates throwaway documents, waits for the handlers and AI pipelines to react, checks the results and deletes what it created. When a precondition is missing (no LLM configured, a resource not deployed) the test is skipped with the reason instead of failing. A green run is stamped on the installation receipt, so uxc installed shows something like "tests: 5/5 pass" with the date.

Archives and the marketplace

uxc export writes one .uxpkg archive, without credentials or sync state. uxc import checks every resource against the target and prints the full collision report before it deploys anything.

The marketplace adds versions and discovery. uxc mp publish uploads a versioned addon, with screenshots and docs. uxc mp install <slug> downloads it, checks its SHA-256 against the published hash and installs it. Before the first write, a package can refuse a client that is too old (minClientVersion) or a server it was not built for (supportedVersions).

Products and extensions

This is how Purchase Order Management was built on top of the Case Management engine. It did not fork it. uxc init --extension po --depends-on case-management@">=0.4 <0.5" created a package with its own prefix (po) and a declared dependency, and lints check that it only writes what it owns. The extension is 68 objects and 536 tests of its own.

The extension can still add values to the product's tag classes (a tag-class delta) and rows to its shared datasets. Ownership follows the installation receipts, so pruning a dataset never deletes rows that belong to another package. Before a product upgrade, uxc import --report tells you, extension by extension, whether the new version still fits.

fast2, the third product

fast2 moves documents from legacy systems into FlowerDocs. In uxc, a fast2 map is one more resource in the package (f2.map), next to the classes it feeds and the AI that will read the documents. uxc f2 ls lists the maps and campaigns on a broker, and uxc f2 run <Map> --wait starts a campaign and reports each step. fast2 has its own login and its own version detection, and uxc handles both. This part is the newest and is moving fastest.

Agents welcome

When an agent drives it (Claude Code is detected automatically), uxc answers in compact JSON. uxc context sums up a whole package in about 600 tokens, and uxc help --search finds the right command. uxc install-claude adds a Claude Code skill and /ux-* slash commands.

A package can also set house rules for agents: which target to use, which resources never to touch, which commands are off limits. Several agents can work on one instance at once. Writes take a lock, and reads never wait.

Before touching a new server

Three checks come first: uxc doctor --ready (can we reach the server and log in?), --sandbox (can we write and clean up after ourselves?) and --ai-smoke (does the AI gateway answer?). Each failure points to an entry in a runbook that lists the symptom, the check, the fix, and what only the server team can fix.

The whole toolbox

For the technical reader who wants the breadth, here is what ships today, grouped by area:

AreaWhat you get
Authorinit (incl. --extension), add with verified templates for every kind, adopt --scan, shared code between scripts and handlers with @include, package variables resolved per target (--var), JSON Schemas for every package file, refs, context, size (push body against the ~1 MB server limit)
Sync and deploystatus --remote, diff, pull, push (--changed, --settle, --recreate, --revive), verify (post-deploy assertions, cross-references, offline lints), rm --local|--server|--both with tombstones, destroy --dry-run, cache-clear, disable/enable a handler as a kill switch
Datadata pull / data push with row-level three-way sync; --prune keeps the rows of other installed packages
Ship and upgradeexport, import (--code-remap to install under another prefix, --expect-sha256, --report), dependencies, minClientVersion and supportedVersions gates, upgrade pruning (removals ship with the version), compat.json, installation receipts with uxc installed
Marketplacemp login, init, publish --dry-run, ls with filters by product, version and audience, show, versions, pull, install with hash check, deprecate / --yank, categories, rm
Content at runtimecategory-aware search, get (--raw-tag), ls, schema, doc create / doc rm, task ls / task answer, watch --until, recent
Uxopian AIrun for prompts, goals and applications (--expect, --prompt-version), run --plan with a per-node report, versions (prompt history, draft vs served), LLM and MCP connections, agents, plans, the gateway's native tools
fast2f2.map kind, f2 ls --campaigns, f2 run --wait --expect-ok, doctor --f2
Scopesscope get, scope create (blank or cloned from a JSON definition), scope delete
Teststest with a harness (t.doc.create, t.waitFor, t.runPrompt, t.answerTask), throwaway fixtures cleaned up, skips with a reason, results stamped on the receipt
Diagnosticsdoctor (--ready, --sandbox, --ai-smoke, --roundtrip), explain <code>, help --search, api <METHOD> <path> for raw calls with auth, pacing and the lock
Agents and teamsJSON output for agents, per-package agent rules, per-target write lock, install-claude (skill + /ux-* commands), shell completion, and a library API (connect, openPackage, marketplace and scope clients)

Part 3 · Under the hood

Four ideas doing most of the work

uxc is about 21,000 lines of plain Node.js with zero runtime dependencies. Most of its value comes from four ideas: one adapter per kind, a canonical form, a three-way hash engine, and server dialects. Here is the tour one last time, at code level.

Zero dependencies, on purpose

uxc needs Node 18.17 or later and that's it. HTTP is the built-in fetch, hashing is node:crypto, and archives go through a ZIP reader and writer of about 160 lines on top of node:zlib.

That ZIP writer is deterministic: sorted entries, a fixed timestamp, a fixed compression level. The same package always gives the same bytes, which is what lets the marketplace check integrity by hash. No npm install also means no supply-chain surface, and nothing to break on a locked-down customer laptop.

One adapter per kind

Each of the 23 resource kinds, from fd.tagclass to ai.plan and f2.map, is one adapter in lib/kinds/. They all share one contract: list, get, create, update, delete, and how to read and write the local files.

The adapters are where the hard-won mechanics live. FlowerDocs Core REST takes arrays even for a single object. An update puts the id in the path and replaces the whole object. "Not found" comes back as an HTTP 500 carrying a code such as F00206. A create that collides with an existing object is healed into an update instead of failing. The sync engine knows none of this; it only talks to adapters.

The canonical form

A server never hands back exactly what you sent. It adds defaults, timestamps, owners and Java type names, and it drops empty arrays or not depending on its version. Compare raw JSON and you see drift everywhere.

So lib/canonical.mjs runs the same normalization on both sides, the local file and the server's echo. It strips volatile fields, folds known coercions and sorts keys, then hashes the result with SHA-256.

The base hash always comes from the server's echo, never from the local file. After every push, uxc reads the object back, canonicalizes it, and saves that as both the file and the base. Whatever the server injects disappears from the comparison instead of showing up as a phantom change.

We don't guess the rules. uxc doctor --roundtrip creates a throwaway Zz* object of each kind, reads it back, and every difference it finds becomes an explicit rule. That file is load-bearing (changing it re-hashes every resource), so a rule only goes in for a difference we saw on a live server.

The three-way engine

For each resource and each target, lib/sync.mjs holds three hashes: the file, the base and the canonical server copy. The state falls out of comparing them:

File vs baseServer vs baseStateWhat uxc does
samesamein syncnothing
changedsamelocal editpush
samechangedserver editpull
changedchanged, but file = serverrebasedrecords the new base
changedchangedconflictdiff, then an explicit --force
anymissingdeleted remotelyreports it; recreates only with --recreate
no basepresent, differentcollisionrefuses; you diff, then pull, push or adopt

Right before each write, push re-reads the server hash, in case someone else wrote in between. It saves state after each resource, so a run can always resume. Datasets go one level finer, with one hash per row: two people can edit different rows of the same clause library without a conflict.

Dialects: one package, several server versions

Uxopian AI ships monthly, FlowerDocs yearly, and their APIs move. uxc detects each server's version (from /actuator/info for FlowerDocs and fast2, from capability probes for the AI gateway) and maps it to a dialect, a set of capability flags in lib/dialects.mjs.

Adapters check flags such as promptVersioning or agenticPlans, never raw version strings. Supporting a new release means adding one range entry. When a kind's write API changes, a new write strategy is picked by a flag and the adapter body stays as it is.

A kind the server doesn't have (goals on Uxopian AI 2026.0.0-ft5, plans before it) is marked unsupported and skipped with the reason. One package can carry both generations and deploy what each server understands.

Every mechanic is proven first

Nothing in uxc relies on a guessed API shape. Each mechanic was first proven on a live server, then written down in a numbered learnings file per product: about 65 sections for FlowerDocs, 19 for Uxopian AI and 21 for fast2. The code cites those sections in its comments (LEARNINGS §25), so when a behavior looks odd, the reason is one lookup away.

Server errors go through the same knowledge base. Every HTTP failure carries an explanation line, and uxc explain <code> prints the cause and the fix.

Plumbing for agents

All output goes through one layer that knows whether a person or an agent is reading. Agents get compact JSON, capped and projected. Eight JSON Schemas describe the package files, and editors validate against them through $schema. uxc api exposes the raw REST surface with authentication handled. A per-target lock file serializes writes between agents, and the handler warm-up window is recorded so the next push waits it out.

Tests and release rhythm

npm test runs about 730 offline tests across 55 files with Node's built-in runner. None of them needs a server. Live mechanics get verified on a real test instance before they reach the code.

Releases are small and frequent. Fixes get a patch version, and a minor version only ships a capability a package can pin with minClientVersion. Between late September and early October 2026 that meant going from 0.18 to 0.25, one feature per pull request.


What's next

Maybe, Go

We are thinking about porting uxc to Go. Nothing is decided yet, but the reasons are concrete.

A Go build is one static binary for macOS, Linux and Windows, with no Node to install. On customer and partner laptops, installing a runtime is often the first thing that blocks. A native binary also starts in milliseconds, which adds up when an agent calls uxc hundreds of times in a session. And the adapter contract, the dialect flags and the package schemas would become interfaces the compiler checks, instead of conventions we keep by discipline.

The current design makes a port realistic. There are no dependencies to replace, the adapters share one small contract, the verified mechanics are written down, and the 730 offline tests describe what the tool must do. The package format, the .uxpkg archive and the marketplace contract would stay the same, so existing packages would keep working.

Until then, uxc stays a Node tool, and its test suite is the bar any port will have to clear.

Written by
Uxopian Editorial
Engineering
@Uxopian Software

Ship your next project as a package

If you build on FlowerDocs, Uxopian AI or fast2, start your next project with uxc. Take it, fork it, and send us your pull requests.