OptiCli 0.8.0
dotnet tool install --global OptiCli --version 0.8.0
dotnet new tool-manifest
dotnet tool install --local OptiCli --version 0.8.0
#tool dotnet:?package=OptiCli&version=0.8.0
nuke :add-package OptiCli --version 0.8.0
opticli
A command-line tool for reading and changing the content of an Optimizely CMS 12 site you develop on your own machine. It is built for AI coding agents (Claude Code and similar) as much as for people. It answers the questions you would otherwise answer with hand-written SQL or by clicking through the edit UI: what content exists, what a page contains, which C# class and view render it, where a block is used, what is unpublished. It can also make changes (set properties, add blocks to a ContentArea, create, translate, publish), as drafts by default.
opticli is an independent open-source project. It is not affiliated with or endorsed by Optimizely.
How it works
reads: opticli ──fixed read-only SQL──▶ the site's CMS database
writes: opticli ──HTTP on 127.0.0.1 + per-run token──▶ site agent inside your running site ──IContentRepository──▶ database
- Reads query the site's SQL Server database directly with fixed queries. Nothing needs to be running. Content comes back with every property decoded (ContentAreas, local blocks, links, rich text). Content types come with their C# class files and Razor views, found by scanning the site's source.
- Writes go through the CMS's own API, so versioning, validation, caches and events behave as they do in the
edit UI.
opticli servestarts the site's existing build output with the site agent injected at startup: a small assembly that adds a few HTTP endpoints under/_opticli/. The site's code and files are not changed. - Output is compact JSON when stdout is redirected (agents, pipes) and tables on a terminal. Every item has a
refyou can pass back to another command.
In this README, "site agent" means that injected assembly. "Coding agent" means an AI assistant that runs opticli.
Requirements
- The .NET 8 SDK or newer to install opticli. It runs on the newest .NET runtime installed.
- An Optimizely CMS 12 site (
EPiServer.CMS.AspNetCore12.x) whose repository you have checked out. - Its database on SQL Server: a local instance, LocalDB, a container, or a remote development database such as
Azure SQL. For Microsoft Entra ID authentication (
Authentication=Active Directory Default), sign in withaz loginfirst. - For writes only:
- The site must build and start locally in the
Developmentenvironment, with whatever it needs to start (its own secrets and services). - The site must run on .NET 8 or newer, since the site agent is built for .NET 8. Reads work for any CMS 12 site.
- The site must build and start locally in the
opticli is developed on Linux. Paths and process handling for Windows and macOS are covered, but get less testing.
Install
Install opticli from NuGet as a global .NET tool:
dotnet tool install -g OptiCli
opticli --version
To update, run dotnet tool update -g OptiCli. CHANGELOG.md lists what changed in each release.
To install from source instead, build the package and install it from the output folder:
git clone https://github.com/epinova/opticli.git && cd opticli
dotnet pack src/OptiCli -c Release -o artifacts
dotnet tool install -g OptiCli --add-source ./artifacts
To update, run dotnet tool update -g OptiCli --add-source ./artifacts after a new pack.
To run from source without installing: dotnet run --project src/OptiCli -- <command>.
Quick start
Run opticli from anywhere inside the site's repository. It walks up to the solution and picks the web project that
references EPiServer.CMS.AspNetCore. It then finds that project's development database, as described under
Which database.
opticli doctor # what was found and where; does the database answer?
opticli types --kind page # page types and how many pages use each
opticli type ArticlePage # properties, C# class file, views
opticli resolve https://www.example.com/en/news/ # which content a URL shows
opticli get 123 --fields Heading,MainArea # one item, decoded
opticli where-used 456 # every page and block that references content 456
A change, end to end:
opticli serve # start the site with the site agent (background, 30-60 s)
opticli set 123 Heading="New title" --dry-run # validate; show before and after
opticli set 123 Heading="New title" # save a new draft version
opticli get 123 --version latest --fields Heading # check the draft
opticli serve --stop
When stdout is redirected, every command prints a single JSON line:
{"ok":true,"data":{"ref":"123","type":"ArticlePage","name":"News","status":"published","url":"/en/news/",
"properties":{"Heading":{"type":"String","value":"Hello"}}},"meta":{"source":"db","version":"0.8.0"}}
Using opticli with coding agents
opticli ships a Claude Code skill that tells an agent when to use opticli and how to use it safely. skill/reference.md has the details it needs for writing content.
opticli skill install # into ~/.claude/skills/opticli/
opticli skill install --repo # into <repository>/.claude/skills/opticli/, to share it through git
For other agents, add the skill text to their instruction file, e.g.
opticli skill print >> AGENTS.md (and opticli skill print reference.md for the details). opticli doctor warns
when an installed skill is older than the opticli you run. Run skill install again after updating opticli. It
replaces a copy it installed itself, and asks for --force only when the files were edited since (or were installed
by an opticli older than 0.4, which kept no record of them).
The skill sets these rules for agents:
- Never publish or delete unless the user asked for it.
- Dry-run first.
- Start
serveonly when writing. - Never choose the development database for the user.
opticli enforces its own safety rules either way (see Safety model).
Which database
opticli works with the project's development database: the one the site uses when you run it locally in
Development.
opticli reads the connection string (
EPiServerDBunless--connection-namesays otherwise) from the site's Development configuration, in ASP.NET Core's order. The first source that has it wins:- launch profiles;
ConnectionStrings__EPiServerDB(orConnectionStrings:EPiServerDB) exported in the shell, shown as sourceenvironment. A launch profile that sets the same variable replaces it when the site runs with that profile, so profiles come first;- user secrets;
appsettings.Development.json;appsettings.json.
If that database is local, opticli uses it without asking.
User secrets are found by the project's
UserSecretsId. opticli reads it, likeTargetFrameworkandAssemblyName, from the project file and the nearestDirectory.Build.props(and the files that one imports). The project file wins.$(Name)references to properties set earlier are expanded. Conditions are evaluated when they are simple comparisons orExists(...), withConfigurationtaken asDebug. A value opticli can't expand is left as written (such aUserSecretsIdreads no user secrets), a condition it can't evaluate is skipped, anddoctorwarns about both.Otherwise opticli asks once. That happens when:
- the connection string points at a remote server such as Azure SQL;
- several launch profiles (or exported variables) disagree;
- only other environments'
appsettings.{Env}.jsonfiles have one.
On a terminal it shows a numbered list. Anywhere else, commands fail with
needs_selection(exit 6), anderror.details.choiceslists the options, so a coding agent can ask the user and runopticli db use <id>.The choice is saved in the user config file (see Files) and holds until that setting changes. If the server or database at that source changes, opticli asks again.
opticli db list # every connection string: id, server, database, local?, where it was found
opticli db use a71c3f # make it the development database (no id on a terminal: pick from a list)
opticli db forget # back to the default
opticli get 123 --db e02d9b # another database, for one run (a remote one is flagged in meta.warnings)
When the database in use is remote, every response carries meta.database (server, name, local,
development). A remote database that isn't the development one also adds a warning.
serve runs the site only against a local database or the chosen development database. It sets
ConnectionStrings__<Name> to that database and removes other spellings of it inherited from the shell, so an
exported one can't win over the pin. Against a remote one it turns off, for that run:
- the site's scheduler;
- automatic schema updates;
- content type sync;
- the remapping of Dynamic Data Store types whose properties changed.
So the CMS doesn't change a shared database just because a local build starts. serve also checks, before it
starts the site:
- EF Core migrations. A migration in the build that the database's
__EFMigrationsHistorylacks would be applied by a site that migrates at startup, soserverefuses (exit 3) unless--allow-pending-migrations. - The CMS schema version. When the database's doesn't fit the build's EPiServer packages, the CMS won't start, and
servesays which side to update. EPiServer.Framework 12.17 and later accept a schema one version newer; earlier ones (and a version opticli can't read) are taken not to.
Once the site answers, serve reports drift: what differs between the build and the database (see
Shared databases).
Safety model
| Rule | Enforced by |
|---|---|
A remote database is used only when the user chose it as the development database, or for one run with --db or --connection, with a warning on every response. Only literal loopback names (localhost, 127.0.0.1, ::1, ., (local)) and LocalDB count as local. A host name that resolves to 127.0.0.1 counts as remote. |
CLI, before connecting |
The site started by serve is pinned to the database the CLI reads. It refuses to start against any remote database except the approved development one. There it runs without scheduler, schema updates, content type sync or store remapping, and serve refuses a build with EF Core migrations the database lacks. If the site turns hosting startups off in its code, so the pin can't run, it refuses to start. |
CLI + site agent at startup |
The site agent answers only loopback callers that send the per-run token, and only in Development. The token is kept in a state file only your user can read. |
Site agent, per request |
Writes create drafts; publishing needs --publish (or publish). Saves are attributed to the user opticli. |
CLI + site agent |
A publish that would also put live changes someone else saved after the published version stops: on a terminal it shows who saved what and asks, elsewhere it fails with conflict (exit 5) listing them. --include-draft (a plan step's "includeDraft": true) confirms; publish --version <id> publishes that version as it is; --from published bases the change on the published version, leaving them out. |
Site agent |
Against a shared database, writes stop while the build and the database differ (drift): on a terminal it shows the differences and asks, elsewhere it fails with drift (exit 5). --accept-drift <fingerprint> confirms; the fingerprint stops counting when the differences change. |
Site agent, CLI first |
delete moves content to the recycle bin; nothing empties it. Site roots, start pages, asset roots and anything above them can't be moved or deleted. |
Site agent |
Reads use fixed queries. sql accepts a single SELECT, refuses anything that writes, runs code, reaches another database or reads server-wide views, logs and traces (in sys, only the views that describe the database's own schema), and always runs in a rolled-back transaction. Personal-data tables (form submissions, users) need --include-personal-data. |
CLI |
Passwords are never printed; doctor redacts connection strings. The exception is opticli env: it prints the per-run token, and with --include-connection the connection string too. |
CLI |
Things these rules can't see:
- A port-forward or tunnel on
localhost,<port>to a remote server looks local to opticli. - So does a hosts-file entry or SQL client alias that maps a loopback name elsewhere.
Don't point opticli at such a connection unless you mean to write to what is behind it.
Shared databases
A remote development database is often shared: with other developers, and with the environment it belongs to. The database doesn't say which build or commit is deployed against it. So opticli checks whether the database matches what the local build expects, which is what matters before a write:
- content types and properties in the code but not in the database, and the reverse (properties added in admin mode don't count);
- a property's type or culture-specific setting, and renames a migration step hasn't applied yet;
- EF Core migrations, Dynamic Data Store types and the CMS schema version.
Each difference says which side is ahead. local: your branch has changes that aren't deployed there (check out what
is deployed, or deploy first). database: the environment runs newer code than your checkout (pull and build).
unknown: they differ, and the database doesn't say which side changed. Required, display names, sort order, tabs
and [AllowedTypes] aren't stored for a model: the running code decides them, so they never show as drift.
opticli drift lists the differences and doctor shows them. Once serve has reported drift, a short warning comes
with every command that uses the database, and with serve --status. A site you start yourself with opticli env
reports drift only through opticli drift and the write errors. While there are any differences, every write stops, not only those to a type that differs: the risk is the build as a whole (its event
handlers and validators), not only its models. Reads keep working, since they don't run the site's code. Restart
serve to compare again after a deploy or a pull.
What opticli can't turn off is the site's own startup code. Against a shared database, a local build still:
- runs
Database.Migrate()if the site calls it at startup (the reason for the migration check); - registers its scheduled jobs (new jobs get rows, changed ones are updated), even with the scheduler off;
- creates the content root folders that add-ons register at startup (
IContentRootService), and Dynamic Data Store stores when they are first used, if they don't exist yet; - runs initialization modules and other startup code that writes, such as a site that creates its own content.
So use a SQL login without DDL rights (no db_ddladmin or db_owner) for a shared database where you can. Schema
changes then fail instead of reaching everyone. Some add-ons create or alter their own tables, views or stored
procedures every time the site starts, and a site with those may not start with such a login. Check what your
add-ons need first.
Commands
opticli <command> --help lists every option with an example, and a bare opticli shows an overview.
Reading
| Command | Answers |
|---|---|
doctor |
project, connection string candidates, database, schema version, site agent, drift (against a shared database), installed skill |
db list, db use, db forget |
the development database (see Which database) |
sites, languages |
site definitions and hosts; language branches |
types [--kind] [--unused] [--sort] |
content types with instance counts |
type <name> |
properties (type, culture-specific, required, tab, order, source line, [AllowedTypes]), C# class file, views |
allowed-in <type> |
which ContentArea/reference properties accept a type, from [AllowedTypes] in code |
get <ref> [--lang] [--version] [--fields] [--expand] |
one item, typed and decoded (ContentAreas, local blocks, rich-text links) |
tree, children, ancestors |
the content tree |
find --type T [--where Prop=value] [--under] [--status] |
items of a type, filtered |
search <text> [--in names\|strings\|all] |
names and text properties containing a string |
where-used <ref> [--pages], where-used --type T |
ContentAreas, references, links and rich text pointing at an item (--pages: through nested blocks up to pages); --type: for every instance of a type |
resolve <url>, url <ref> |
URL to content, and content to URL per language |
versions <ref>, drafts [--since] [--by] [--kind] [--type] |
version history; unpublished changes |
projects [<id>] |
projects, and the versions in one |
blob <ref> |
where a media file lives on disk |
drift |
what differs between the build and a shared database (needs serve; see Shared databases) |
access <ref> |
who may read and edit an item: its access rights, and the ancestor they are inherited from |
sql "<SELECT …>" |
anything else, read-only |
A <ref> is a content id (123), a version (123_456), a content GUID, or a URL or path (/en/about/,
https://host/en/about/). Content from a content provider, such as images from a DAM, shows up as 63__provider. You
can pass that form back in property values, ContentArea items and links. The id before __ is local to one database;
the GUID is the same in every environment. --lang <code> picks the language branch. The default is the master
language, or the language the URL selects.
Writing (needs opticli serve)
These commands write:
set,create,area(add, remove or move ContentArea items),block create,uploadandtranslatesave a draft unless--publishis given.upload <file>adds a PDF, image or other file (up to 50 MB) as media, as the type the site maps its extension to, and prints where the file was stored.createdoesn't take media types (that would be media without a file).publish,unpublish(takes a published branch offline, as the edit UI's expiry does),discard(deletes one unpublished version; it can't be undone),moveanddelete(to the recycle bin).deletestops when other content references what it deletes, unless--ignore-references.create,block create,uploadandmoveput content only where it can go: pages below pages, blocks, media and folders in asset folders, and only where the parent type allows the type ([AvailableContentTypes]and admin mode's settings, as the CMS answers it).access <ref>with--grant Role=Levels,--user Name=Levels,--revoke Name,--break-inheritanceor--inheritchanges one item's access rights. Children that inherit follow; nothing is applied to descendants. Access rights aren't versioned, so the output shows them before and after. The root, the recycle bin, start pages and asset roots are refused, and so is a change that leaves no role with Administer.apply plan.jsonruns several operations validated together. Later operations can refer to an item an earlier one created as$id. A property value"@texts/body.html"is that file's text, asProp=@fileis on the command line. Files in a plan are relative to the plan file and must stay inside its folder. With"guidNamespace", what a plan creates gets the same GUIDs in every database, andapply --update-existingruns it again: existing content is updated (and moved back out of the recycle bin), and steps that are already done change nothing. A plan that stops halfway (a failure, Ctrl+C) still reports what it saved, with undo hints anddetails.partial: true.
Every write command takes --dry-run. Structured values use --values, e.g.
--values '{"MainArea":[{"ref":"456"}]}'. set and area check that nobody saved a newer version in the meantime
(exit 5 on a conflict). --base-version <id> pins the version the change is based on, and --force skips the check.
--from published (or --from <version>) bases the change on that version instead of the latest, and the check
stays: newer drafts are left out and stay as they are, and the output lists them (leftOut).
A publish puts the whole version live. When someone else saved unpublished changes in it, opticli shows them and asks on
a terminal; elsewhere it fails with conflict and details.reason: "pendingDraft", and --include-draft confirms.
set ... --from published --publish publishes a change without them.
Output after a publish names previouslyPublished, the version to publish again to go back. Content with an approval
sequence isn't published directly (exit 3); --request-approval sends it for review instead, as the edit UI does.
Against a shared database that differs from the build, writes stop with drift (exit 5) until --accept-drift <fingerprint> (apply --accept-drift for a whole plan); dry runs don't stop.
skill/reference.md documents value syntax and the plan format.
serve and env
opticli serve runs the site's existing build output in Development on http://127.0.0.1:<port>, with the site
agent injected, and returns once the site agent answers.
| Option | Default |
|---|---|
--output <dll> |
the newest bin/{Debug,Release}/<tfm>/<AssemblyName>.dll (also below a <rid>/ folder, under the project's OutputPath/BaseOutputPath, or under artifacts/ with UseArtifactsOutput), or output from the user config |
--port <n> |
port from the user config, else 5199, else the first free port up to 5299 |
--build |
off: opticli warns when sources (the site's or a referenced project's) are newer than the build, and --build runs dotnet build first |
--timeout <s> |
180 seconds to wait for the site to answer |
--foreground |
off: the site runs in the background; with it, the site's output streams until Ctrl+C (to stderr when stdout is redirected, so stdout stays one JSON envelope) |
--https |
off (or "https": true in the user config, which --https false overrides): also listen on https://localhost:<next free port> with the development certificate, printed as browseUrl, for sites that redirect to HTTPS. serve warns when a site does |
--allow-pending-migrations |
off: against a shared database, serve refuses a build with EF Core migrations the database lacks |
serve --status, serve --logs [--tail N] and serve --stop manage the running site:
--stopasks the site to shut down through the site agent, on every OS, and falls back to SIGTERM on Linux and macOS. A site that hasn't exited after 20 s is killed. On Windows that kill is all there is when the agent doesn't answer.- Each start writes a new log.
--logsreads the latest from its end, anddata.previouslists the two before it. - A state file opticli can't read is treated as stale:
--statusand--stopremove it,doctorreports it. - Two
serveruns for the same project don't start two sites: the second waits for the first and reports its site. - Ctrl+C while
servewaits for the site stops the site again. - A site that configures
Kestrel:EndpointsignoresASPNETCORE_URLS.serveandenvthen add opticli's address as one more endpoint (Kestrel__Endpoints__OptiCli__Url) and warn. If the site sets its addresses in code, a timeout names the addresses it listens on instead.
To run the site yourself (IDE, dotnet run, hot reload), opticli env prints the variables serve would set:
--format shell|powershell|dotenv|json|launchSettingspicks the format.--jsonis the same as--format json.--portpicks the port.OPTICLI_*variables that this run leaves out but the shell still exports (from an earlieropticli env) are set to empty, so they can't pin or approve another database.
The connection string is left out unless --include-connection is given. Without it the site uses its own
configuration, which is still checked to be local (or the approved development database) and the same database
the CLI reads.
Set the variables for the site process only, e.g. in a subshell:
(eval "$(opticli env)" && dotnet run --no-launch-profile)
DOTNET_STARTUP_HOOKS makes every .NET process started from a shell that exports it load the hook. Launch profiles
are often committed, so keep the token out of git.
| Variable | Set by serve/env for the site |
Meaning |
|---|---|---|
DOTNET_STARTUP_HOOKS |
always | path to OptiCli.Agent.dll, which loads the site agent |
OPTICLI_TOKEN |
always | the per-run token the site agent requires |
OPTICLI_DB |
serve, env --include-connection |
the connection string to pin the site to (read by the CLI, it is the same as --connection) |
OPTICLI_CONNECTION_NAME |
when not EPiServerDB |
which connection string to pin |
ConnectionStrings__<Name> |
serve, env --include-connection |
the same connection string, for code that reads it before the agent's pin; serve also removes other spellings of it (ConnectionStrings:<Name>, another case) |
OPTICLI_REMOTE_DB |
against a remote development database | the one remote database the site agent accepts; turns on shared-database mode |
OPTICLI_DRIFT_FILE |
serve, against a remote development database |
what serve compared before the start (EF Core migrations, CMS schema version), for the site agent's drift report |
How the injection works
serve starts dotnet <Site>.dll with DOTNET_STARTUP_HOOKS pointing at OptiCli.Agent.dll, which ships in the
tool package under agent/. The startup hook makes the site agent's assembly resolvable and appends it to
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES. The hosting startup then:
- pins the connection string, added as the last configuration source and as a
PostConfigureof the CMS's data access options; - fails the start if the effective connection string is neither local nor the approved development database;
- maps
/_opticli/v1/*ahead of the site's own middleware.
The site agent is compiled against EPiServer.CMS.Core 12.0 and binds to the site's own, newer CMS assemblies at
runtime. It ships no copies of them.
Output and exit codes
- Redirected stdout gets compact JSON:
{"ok": true, "data": …, "meta": {"source", "version", "next", "warnings", "database"}}.sourceisdbfor reads,agentfor writes andclifor local commands.databaseis present only when the database is remote.
- A terminal gets tables.
--jsonand--textforce either. --jsonlon list commands prints one item per line. A final{"meta": {…}}line follows only when there is a next page or warnings.- Lists return 50 items by default (
sql: 100 rows). Passmeta.nextas--cursorfor the next page, or raise--limit. There is no total count. - Strings over 300 characters are cut (
truncated: true, length: N);--fullor--fieldsgives them whole. Dates are UTC (…Z). - Errors look like
{"ok": false, "error": {"code", "message", "hint", "details"}}. The hint says what to do next.
| Exit | Code | Meaning |
|---|---|---|
| 0 | ok | |
| 1 | usage / internal |
bad arguments (or a bug in opticli) |
| 2 | not_found |
project, connection string, content, type or version not found |
| 3 | refused |
a safety rule blocked it |
| 4 | unreachable |
database or site agent not reachable, or a query failed on the server (a write that timed out may still have been saved) |
| 5 | conflict / validation / drift |
a newer version exists, a publish would include someone else's unpublished changes (details.reason: "pendingDraft"), the CMS rejected the values, or a write against a shared database that differs from the build wasn't confirmed (details is the drift report) |
| 6 | needs_selection |
the user must choose the development database first (error.details.choices) |
| 130 | cancelled |
interrupted (Ctrl+C); an apply reports what it saved until then |
Files
| What | Linux / macOS | Windows |
|---|---|---|
| User config | $XDG_CONFIG_HOME/opticli/config.json (default ~/.config/opticli/config.json) |
%APPDATA%\opticli\config.json |
serve state, start lock and logs (the last 3 runs) |
$XDG_STATE_HOME/opticli/ (default ~/.local/state/opticli/) |
%LOCALAPPDATA%\opticli\ |
You can set per-project defaults in the user config. opticli db use adds the chosen database there. opticli
keeps the file's permissions when it rewrites it, and creates it readable by you only on Linux and macOS.
{"projects": {"/abs/path/to/Site": {"connection": "...", "output": "bin/Debug/net8.0/Site.dll", "port": 5199, "https": true}}}
Supported versions
- Optimizely CMS 12 (
EPiServer.CMS.AspNetCore12.x): reads on any runtime; writes on a site running .NET 8 or newer. - SQL Server on the local machine (including LocalDB and containers), or a remote SQL Server / Azure SQL development database.
- Not supported:
- CMS 11, Commerce, Forms data and search indexes;
- writes to environments other than development (test, staging, production).
Known limitations
- Rules that live only in code are read from the C# sources, not evaluated:
[AllowedTypes]is parsed. Editor descriptors and metadata extenders that change allowed types at runtime are not; auiHintflags the property.- Validation attributes are only pointed at (file and line).
- URLs a site rewrites in code (custom segments, partial routing) show as the plain content path.
typelists views, but not controllers.
- Content types are matched to C# classes by GUID or name with a lightweight source scan, not a compiler. Unusual declarations may be missed.
where-usedworks per item: there is no type-wide usage in one call.serveruns the existing build output; it doesn't build unless--buildis given.- Output field names may still change before 1.0.
Development
dotnet build opticli.slnx # warnings are errors
dotnet test opticli.slnx
The EPiServer packages come from Optimizely's public NuGet feed, which nuget.config already lists.
| Project | What it is |
|---|---|
src/OptiCli |
the opticli command line: commands, options, help text |
src/OptiCli.Core |
everything the CLI does: discovery, connection resolution and safety, SQL readers, decoders, output, serve |
src/OptiCli.Agent |
the site agent, loaded into the site process; referenced by nothing, copied into the tool package |
src/OptiCli.Protocol |
request and response types shared by the CLI and the site agent (source-linked into both) |
skill/ |
the coding-agent skill, embedded in opticli.dll |
tests/ |
unit tests for Core and the site agent; OptiCli.Integration, a test against a real site |
The unit tests use generic fixtures and need no database. The integration test is an oracle. It samples content across types, kinds and languages from a real site, reads each item both from the database and through the CMS (via the site agent), and compares them property by property. It is skipped unless a site is configured:
cd path/to/Site && opticli serve # the site agent must be running
OPTICLI_IT_PROJECT=path/to/Site \
OPTICLI_IT_SAMPLE=200 \
OPTICLI_IT_REPORT=/tmp/oracle-report.md \
dotnet test tests/OptiCli.Integration
| Variable | Meaning |
|---|---|
OPTICLI_IT_PROJECT |
site project directory (required; the test is skipped without it) |
OPTICLI_IT_SAMPLE |
content branches to compare (default 200) |
OPTICLI_IT_DRAFTS |
most recent drafts to compare as well (default 10) |
OPTICLI_IT_SEED |
sampling seed (fixed by default, so runs repeat) |
OPTICLI_IT_REPORT |
write the mismatch report here, with every mismatch as .jsonl next to it |
OPTICLI_IT_PLAN |
a plan whose content is always compared, on top of the sample (the edge-case plan below) |
Some differences the database can't reproduce by design, such as URL segments a site drops in code. Those are listed
with the reason in tests/OptiCli.Integration/Comparison/KnownDifferences.cs; any other mismatch fails the run.
The edge-case site
A sample site lacks much of what real sites have: fetch-data pages, a site whose start page is under another site's,
simple addresses on several sites, culture-specific properties in shared and local blocks, personalized ContentAreas,
an approval sequence, language fallback settings, and a media type for PDF files. tests/fixtures/edge-cases/ builds
them from an Alloy site (dotnet new epi-alloy-mvc) without changing it. setup.sh copies the site and its
database, adds EdgeCasesFixture.cs (the extra content types, plus a startup module for what a plan can't create),
and applies edge-cases.plan.json. Run it again to update the content: the plan is applied with
--update-existing, and the database copy is kept unless FRESH=1.
SQLCMDPASSWORD=... tests/fixtures/edge-cases/setup.sh path/to/Alloy path/to/AlloyEdge alloy alloy-edge
OPTICLI_IT_PROJECT=path/to/AlloyEdge \
OPTICLI_IT_PLAN=tests/fixtures/edge-cases/edge-cases.plan.json \
dotnet test tests/OptiCli.Integration
The integration tests run one at a time, since the write tests make and remove scratch content on the same site.
drift.sh in the same folder runs the shared-database scenario on copies of the edge-case site and its database. It
reaches the copy through a host name instead of a loopback name (<hostname>.localhost resolves to the loopback
address, but counts as remote), so serve runs in shared mode. It then changes the copy's code and checks each step:
nothing differs, local ahead, the database ahead, and an EF Core migration that serve refuses. Against that copy,
DriftTests checks that writes stop on drift until its fingerprint confirms it.
SQLCMDPASSWORD=... tests/fixtures/edge-cases/drift.sh path/to/AlloyEdge path/to/AlloyDrift
CI runs the unit tests on Linux, Windows and macOS for every push and pull request. Issues and pull requests are welcome. Please run the unit tests before sending a change. When a change touches reads, also run the integration test against a site you have.
Releasing
Set the new version as <Version> in Directory.Build.props and as opticli-version in
skill/SKILL.md, and add the release to CHANGELOG.md. Commit, then push a matching tag:
git tag v0.8.0 && git push origin v0.8.0
The release workflow checks that the tag matches both versions and is on main,
runs the unit tests, checks that the package holds the site agent and starts, and publishes it to nuget.org.
Licence
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.