Metadata-Version: 2.4
Name: vortex_cli
Version: 8.3.0
Summary: Vortex CLI
Author-email: Jordan Amos <jordan.amos@gmail.com>
License: MIT License
        
        Copyright (c) 2023 jordanamos
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Keywords: vortex,cli,puakma,tornado
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Requires-Dist: tabulate>=0.9
Requires-Dist: watchfiles>=0.19
Provides-Extra: keyring
Requires-Dist: keyring; extra == "keyring"
Dynamic: license-file

# Vortex CLI

[![Build Status](https://dev.azure.com/amostj/vortex-cli/_apis/build/status%2Fjordanamos.vortex-cli?branchName=main)](https://dev.azure.com/amostj/vortex-cli/_build/latest?definitionId=11&branchName=main)  [![PyPI version](https://badge.fury.io/py/vortex-cli.svg)](https://badge.fury.io/py/vortex-cli)

Vortex CLI is a command line alternative to the [Puakma Vortex IDE](https://github.com/brendonupson/PuakmaVortex) that simplifies the process of developing Puakma Applications on a [Puakma Tornado Server](https://github.com/brendonupson/Puakma) using Visual Studio Code. It allows you to clone applications from the server to a local workspace, edit the files using Visual Studio Code, and automatically upload changes to the server as you work.

Vortex CLI also comes pre-packaged with the necessary Puakma .jar files for development.

#### Visual Studio Code and Extensions

While it is possible to use without it, this software has been purposefully designed for use with [Visual Studio Code](https://github.com/microsoft/vscode) and the [Project Manager For Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-dependency) or the [Extension Pack For Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) extension. This software leverages [Workspaces](https://code.visualstudio.com/docs/editor/workspaces) in Visual Studio Code and manages a `vortex.code-workspace` file within the workspace.

## Installation

1. Install the tool using pip.

   ```
   pip install vortex-cli
   ```

2. It is recommended to set the workspace you would like to work out of via the `VORTEX_HOME` environment variable.

   On Unix:

   ```
   export VORTEX_HOME=/path/to/workspace
   ```

   Otherwise, Vortex CLI will use a default **'vortex-cli-workspace'** directory inside your home directory.

3. Run vortex with the `--init` flag to create your workspace (If it doesn't already exist) and the necessary config files:
   ```
   vortex --init
   ```

4. Define the servers you will be working with in the `servers.ini` file inside the `.config` directory within your workspace. You can quickly access this using the `code` command to view your workspace in VSCode.

   ```
   vortex code
   ```

   In the `servers.ini` file, you can define as many servers as you need, each with their own unique name. For example:

   ```
   [DEFAULT] ; This section is optional and only useful if you have multiple definitions
   port = 80 ; Options provided under DEFAULT will be applied to all definitions if not provided
   soap_path = system/SOAPDesigner.pma
   default = server1 ; Useful when you have multiple definitions


   [server1] ; This can be called whatever you want and can be referenced using the '--server' flag
   host = example.com
   port = 8080 ; we can overwrite the DEFAULT value
   username = myuser ; Optional - Prompted at runtime if not provided
   password = mypassword ; Optional - Prompted at runtime if not provided
   ; Optional
   gateway_path = vortex/gateway.pma ; the default: through the vortex gateway. Blank = webdesign's vortex API directly - see Backend: gateway or webdesign
   clone_with_resources = html,css,js ; resources with these extensions are always cloned - 'clone --get-resources' still clones ALL resources
   lib_path = ; optional extra jars to add to the classpath (the server's own jars are downloaded automatically - see 'vortex server libs')
   workspace_folders = ~/dev/shared,notes ; extra folders to mount in the generated .code-workspace files. Relative paths resolve against the workspace root. Under [DEFAULT] they are added to every workspace; here they apply to this server's workspace (and the global one)
   java_home = /usr/lib/jvm/java-17-openjdk-amd64/ ; The local path to the JRE to use. Should be the same version running on your server
   java_environment_name = JavaSE-17 ; Java Execution Environment name https://docs.osgi.org/reference/eenames.html
   ```

## Upgrading to 8.3

- **`vortex weblog`** reads the server's HTTP access log (`HTTPTextLog`) through the gateway:
  its last lines, an older day's file, the whole file, or new lines as they are written (see
  [The web log](#the-web-log)). It needs `GatewaySystem` and a gateway with `GET logs?log=web`.
- **`vortex stats`** shows each loaded addin's statistics - CPU, memory, threads, errors,
  logins, HTTP traffic - from the gateway's `GET stats`, once or re-polled with `-k` (see
  [Server statistics](#server-statistics)). It needs `GatewaySystem`; a gateway without the
  route is refused with `NOT_EXPOSED` - update `vortex/gateway`.
- **`vortex log` reads a time window.** `--since 2h`, `--from 02:00 --to 03:00` (or full dates),
  `--search TEXT`, `-n` up to 500, and `--page N` / `--all-pages` through the gateway's pager
  (see [The server log](#the-server-log)). Without the new flags it prints and sends exactly
  what it did before.
- **`vortex file`** lists, reads, downloads, uploads and deletes the server's files below the
  Puakma root through the gateway's `files` route (see [Server files](#server-files)). It
  needs `Admin` and a gateway with the `files` route; an older one refuses it with
  `NOT_EXPOSED`.

## Upgrading to 8.2

- **`object run` runs ACTIONs, and waits.** It calls the gateway's new `POST run` (gateway
  `AppVersion` 0.1.4 or later) instead of sending `tell agenda run` to the console, and
  reports how long the action took, its reply and any exception (see
  [Running an action](#running-an-action)). An older gateway refuses it with `NOT_EXPOSED`.
  It no longer accepts a SCHEDULED_ACTION.
- **`vortex agenda`** lists the scheduled actions AGENDA has loaded and queues one with
  `agenda run`, refusing the cases where AGENDA would silently do nothing (see
  [The agenda](#the-agenda)). It replaces both `object run` on a scheduled action and
  `vortex execute tell agenda run ...`.

## Upgrading to 8.1

8.1 does two things. It replaces `vortex config` with plain top-level commands that read
`servers.ini`, which you edit by hand. And it stops a stale clone from silently overwriting
someone else's upload - another agent, in another workspace or on another machine - all
client side, with no gateway or webdesign change (see
[Working alongside other agents](#working-alongside-other-agents)). `vortex config` and the `execute` flags are **removed**: `config` is a stub
that runs nothing, prints the replacements and exits 1; the old `execute` flags are usage
errors (exit 2).

| 8.0 | 8.1 |
|---|---|
| `config --list-servers` | `server list` |
| `config --output-server-config` | `server get [NAME]` |
| `config --output-config-path` | the last line of `server list` |
| `config --output-workspace-path` | `vortex --path` |
| `config --update-vscode-settings` / `--reset-vscode-settings` | automatic on a `servers.ini` save, or `code --refresh [--reset]` |
| `config --set S O V` | edit `servers.ini` |
| `config --set-password` | `keyring set vortex-cli:<server> <username>` |
| `config --sample` | `server get` (every option it reads) |
| `execute --show-schedule` / `--refresh-agenda` / `--flush-cache` | `execute schedule` / `refresh-agenda` / `flush-cache` |
| `execute --refresh-design N` | `execute refresh-design N` or `app refresh N` |
| `execute --run PATH` | `object run ID --app-id N` |
| `status [--show-permissions] [--json]` | `vortex [-s NAME] [--show-permissions] [--json]` (no command) - plus the CLI version and the workspace |
| `vortex` with no command (the workspace path) | the live status of the default server: `vortex-cli` and `workspace` first, then the server (`vortex --path` prints just the path) |
| `use NAME` | `server use NAME` (`use NAME` still works) |
| `libs [--refresh] [-s NAME]` | `server libs [NAME] [--refresh]` |
| - | `fetch` - which cloned elements are out of step with the server, content and metadata (renames too); `fetch --pull` brings them in without losing local edits (3-way merges what it can) |
| - | `object diff` - server vs local (`--mine`: your edits, `--theirs`: the server's) |
| `compile --upload` sends every class | only those whose `.java` was edited since the clone synced it (`--all`: every one) |
| `find QUERY [--app-id ID]` (clones only) | `object list [QUERY] [--app-id ID [--local]] [--json]` - with `--app-id` it asks the server, so no clone is needed; QUERY is optional (every object) |
| `grep PATTERN` | `object grep PATTERN [--json]` - still the clones only |
| `db list --app-id ID` (required) | `db list [--app-id ID]` - without it, every application's connections; each row names its application (with `--app-id`, from its clone - the name is blank when it isn't cloned) |

- **Server commands are one noun, `vortex server`.** **`vortex server list`** lists every
  server definition: host, backend, protected, the number of cloned apps, and `*` on the
  default. **`vortex server get [NAME]`** (the default server without one) lists every option vortex reads with its value and where it comes from - the
  server's own section, `[DEFAULT]`, the built-in default, or unset - plus where the
  credentials come from and any option vortex ignores (typos, renamed and removed options).
  Neither sends a request or prints a password. Both say when `default` names a server that
  isn't defined.
- **`execute` joins its words**, so `vortex ex tell agenda schedule -s dev` needs no quotes.
- **A protected server must be named.** A command that writes to a server or sends a console
  command (`execute`, `compile --upload`, every `app`/`object`/`keyword`/`db` write, and a
  `db query` that is not a `SELECT`) refuses a protected server that was only picked up as
  the `vortex use` default (`PROTECTED_DEFAULT` in `--json`, nothing sent): pass `-s NAME`.
  Reads are unaffected.
- **Saving `servers.ini` updates the VS Code workspace files.** A running `vortex watch`
  rebuilds them on save (and says when a watched server's connection settings changed, which
  needs a restart); otherwise the next `vortex` command does. Unchanged files are no longer
  rewritten, so VS Code doesn't reload for nothing.
- **Uploads are refused on a conflict.** `watch`, `compile --upload` and `object update
  --source/--data` (on a cloned app) compare the server's current row with the copy the clone
  last synced - before anything is sent. If someone else wrote it since, the upload is refused
  (`CONFLICT`, exit 1) and names who and when: `vortex fetch ID --app-id N --pull`, merge,
  upload again.
  `--force` (`compile --upload`, `object update`) overrides it, and needs a terminal
  (`FORCE_NEEDS_TERMINAL` without one) so an agent can't.
- **`vortex find` and `vortex grep` are stubs** naming `vortex object list` and `vortex object
  grep`, and exit 1.
- **`vortex status` is a stub** that names `vortex` (the server's status, as `status` was
  in 8.0) and `vortex fetch` (the clone against the server), and exits 1. **`vortex libs`**
  is a stub naming `vortex server libs`.
- **Manifests from before 8.1 still work.** They lack the server's `updated` stamp, so the
  check compares content until the next `clone` or `fetch --pull` stores it. An older
  `watch` kept a failed upload's content as the clone's copy of the server, so an edit that
  never reached the server can look unedited: `fetch --pull` keeps any file it replaces or
  removes under such a copy in `<app>/.pull-backup/<time>/`, and `compile --upload` says
  when it skipped one. Run `vortex fetch --app-id ID --pull` once per clone after upgrading
  (it also brings in metadata an older clone stored differently).
- **The upgrade is one-way.** An 8.1 manifest stores a field 8.0 doesn't know, so an 8.0 CLI
  treats an 8.1 clone as unreadable - keep every machine and tool sharing a workspace on
  8.1 (or reclone after going back).
- **`fetch --pull --force` needs a terminal**, like the upload `--force`, and keeps every file
  it replaces or removes under `<app>/.pull-backup/<time>/`.

## Upgrading to 8.0

8.0 reorganises the server commands into `vortex <noun> <verb>` over four entities - `app`,
`object`, `keyword` and `db` - so people and AI agents can drive a server predictably.
Everything else keeps its 7.x shape. It is a breaking release: each removed 7.x command still
exists for one release as a stub that **runs nothing**, prints its 8.0 replacement and exits 1.

| 7.x | 8.0 |
|---|---|
| `list` | `app list` (`ls` stays, = `app list --local`) |
| `new app` | `app create` |
| `new object` / `new object --update` | `object create` / `object update` |
| `new keyword`, `keyword APP --name --values` | `keyword set NAME VALUE... --app-id APP` |
| `keyword APP` | `keyword list --app-id APP` |
| `copy IDS --app-id DEST` | `object copy IDS --app-id SRC --to-app-id DEST` (`--app-id` now means the SOURCE) |
| `delete` | `object delete IDS --app-id APP` |
| `import` / `export` | `app import` (`--group` now required) / `app export` |
| `db ID --sql` | `db query DB --app-id APP` (no `--update` flag) |
| `db NAME --list` / `--schema T` | `db list-tables` / `db get-table` |
| `schema --add-table` ... `--delete-column` | `db create-table` ... `db delete-column` |
| `schema --ddl` | removed |
| `config --check-gateway` | `status` (`vortex` with no command since 8.1) |

- **Numeric IDs, no guessing.** `--app-id` and `APP_ID` are numeric everywhere (only
  `clone APP...` still takes a TemplateName, group or `group/name`), and `--app-id` is
  required on every `object`, `keyword` and `db` command - it is never inferred from a clone.
  The server is `-s/--server`, else the `vortex use` default; `SERVER:ID` qualifiers and
  inferring the server from local clones are gone.
- **No clone needed for server work.** When the app *is* cloned, every change is written
  into the clone too (see [Local clones stay in sync](#local-clones-stay-in-sync)).
- **Output for scripts and agents.** Readable by default; `--json` on every entity command
  and `fetch` prints one envelope with a machine-readable error code. Exit codes are 0/1
  (2 for bad arguments). See [Output, errors and exit codes](#output-errors-and-exit-codes).
- **`gateway_path` alone picks the route.** Set (the default `vortex/gateway.pma`) means the
  gateway, required - no probe, no fallback; blank means webdesign's `vortex` API directly.
  The 7.x server options for choosing SOAP and for pinning the log's database connection
  are gone and silently ignored if still present. **Blank `gateway_path` on any server that
  has no gateway** (the old SOAP servers, and any server still running the old gateway) or
  8.0 cannot reach it. SOAP is used only for console commands (`status`, `execute`) on
  servers without the gateway.
- **`protected = true` only means `watch` skips the server** unless `--include-protected`.
  The typed server-name confirmation is gone from every command; the gateway's roles are the
  protection.
- **`clone` no longer downloads the server libraries.** It prints a one-line
  `vortex libs --refresh` hint when they are not cached; `compile` fetches them on first use.
- **`watch` is no longer the only deploy path:** `object create`/`update --source/--data`
  and `compile --upload` upload content too. A running watch blocks them (and any compile).
- **`clean`, `grep`, `find`** take one `--app-id`. `list --show-inactive` and
  `list --connections` are gone (the inventory has no `DisableApp` flag; use
  `db list --local`).

## Upgrading from 6.x or 7.x

6.0 added the gateway as an opt-in alternative to the SOAP designer, and 7.0 replaced the old
gateway application with today's pass-through to webdesign's `vortex` API (removing `push`,
`pull`, `undo`, `render`, `agenda`, `compile` and the undo journal). 8.0 supersedes both:
follow [Upgrading to 8.0](#upgrading-to-80) - in particular, set `gateway_path` per server.

## Upgrading to 5.0

5.0 changes some defaults you may rely on:

- **`resource_ext_only` is replaced by `clone_with_resources` - and the meaning flipped.**
  Previously the extensions *restricted* what `--get-resources` cloned. Now resources with the
  listed extensions are **always** cloned, and `--get-resources` clones every resource
  unfiltered. Rename the key in `servers.ini` (vortex warns while the old key is present).
- **`vortex watch` now watches every cloned app across all servers** and uploads each change to
  the server it was cloned from. Use `--server` for the old single-server behaviour, and mark
  production definitions `protected = true` so they are never watched by accident.
- `find`, `grep` and `vortex list --local` now search all cloned apps unless `--server` is
  given, and ID-taking commands infer their server from local clones (see below).

## Usage

For a full list of commands see `--help` (every command and verb has its own:
`vortex db update-column --help`).

### Command Overview

```
── app ──────────────────────────────────────────────────────────────────────
vortex app list     [--group --name --template --strict --local --show-inherited
                     --all --ids-only --open-urls --open-dev-urls] [--json]
vortex app get      APP_ID [--show-params] [--json]
vortex app create   --name N --group G [--template --inherit-from --description]
vortex app update   APP_ID [--name --group --description --inherit-from --template]
                    [--param NAME=VALUE ...] [--remove-param NAME ...]
vortex app export   APP_ID... [--out-dir --exclude-source --timeout]
vortex app import   FILE.pmx --name N --group G
vortex app refresh  APP_ID  (alias: refresh-design)

── object ───────────────────────────────────────────────────────────────────
vortex object get    ID --app-id ID [--show-source --show-data] [--json]
vortex object create --app-id ID --type T --name N [--source FILE --data FILE
                     --content-type --comment --inherit-from --open-action
                     --save-action --parent-page] [schedule options]
vortex object update ID... --app-id ID [--source FILE --data FILE] [metadata options]
                     [schedule options]
  schedule options (SCHEDULED_ACTION only): --schedule N|S|I|H|D|W|M|Y --interval N
                     --days SMTWHFA --start-time HH:mm --finish-time HH:mm --date N --month N
vortex object diff   [ID...] --app-id ID [--mine|--theirs] [--stat --word-diff -U N] [--json]
                     [-- GIT-DIFF-OPTIONS... | --write DIR]
vortex object copy   ID... --app-id SRC --to-app-id TGT [--copy-params]
vortex object list   [QUERY] [--app-id ID [--local]] [--strict --inherits-from|--parent-page
                     --ids-only --show-params --type T...] [--json]
vortex object grep   PATTERN [--app-id ID] [--output-paths|--output-apps]
                     [--include-resources|--type T...] [--json]
vortex object run    ID --app-id ID [--output] [--json]
vortex object delete ID... --app-id ID [--yes]

── keyword ──────────────────────────────────────────────────────────────────
vortex keyword list   --app-id ID [--local] [--reveal] [--json]
vortex keyword get    NAME --app-id ID [--local] [--reveal] [--json]
vortex keyword set    NAME [VALUE...] --app-id ID
vortex keyword delete NAME --app-id ID [--yes]

── db ───────────────────────────────────────────────────────────────────────
vortex db list          [--app-id ID] [--local] [--json]
vortex db get           DB --app-id ID [--json]
vortex db query         DB --app-id ID [SQL | --file F | -] [--limit N] [--all-cols] [--json]
vortex db list-tables   DB --app-id ID [--json]
vortex db get-table     DB TABLE --app-id ID [--json]
vortex db create-table  DB TABLE --app-id ID [--description]
vortex db update-table  DB TABLE --app-id ID [--name --description]
vortex db delete-table  DB TABLE --app-id ID [--yes]
vortex db create-column DB TABLE COLUMN --app-id ID --type T [--size N --pk --not-null
                        --unique --auto-increment --ref TABLE --cascade-delete --description]
vortex db update-column DB TABLE COLUMN --app-id ID [--name --type --size --description
                        --pk|--no-pk --null|--not-null --unique|--no-unique
                        --auto-increment|--no-auto-increment --ref TABLE|--no-ref
                        --cascade-delete|--no-cascade-delete]
vortex db delete-column DB TABLE COLUMN --app-id ID [--yes]

── agenda ───────────────────────────────────────────────────────────────────
vortex agenda [list] [--app-id ID] [--json]
vortex agenda run    ID --app-id ID [--param KEY=VALUE ...] [--refresh --force] [--json]

── file (Admin) ─────────────────────────────────────────────────────────────
vortex file list     [PATH] [--json]
vortex file get      PATH [--hash --meta] [--json]
vortex file download PATH [-o DEST] [--overwrite] [--json]
vortex file upload   LOCAL... --to FOLDER [--overwrite] [--json]
vortex file delete   PATH [--yes] [--json]

── Workspace ────────────────────────────────────────────────────────────────
vortex ls        [app list filters] [--json]      = app list --local
vortex clone     APP... [--group --reclone --all --get-resources --open-urls --timeout]
vortex compile   --app-id ID [--object ID...] [--upload [--include-source --all --force]]
                 [--show-warnings]
vortex fetch     [ID...] [--app-id ID] [--all] [--pull [--force --no-merge]] [--json]
vortex watch     [--include-protected]
vortex clean     [--app-id ID] [--all --include-libs]
vortex code      [--refresh [--reset]] [-s]

── Server and setup ─────────────────────────────────────────────────────────
vortex           [-s NAME] [--show-permissions] [--json]   (the server's live status)
vortex --path    (the workspace path)
vortex log       [-n --source -m --search --errors-only|--debug-only|--info-only]
                 [--since D | --from WHEN] [--to WHEN] [--page N | --all-pages] [-k -d] [--json]
vortex weblog    [-n -m [--regex] --file NAME] [--list | --all | -o PATH | -k [-d]] [--json]
vortex stats     [--addin NAME] [--key KEY] [-k [-d]] [--json]
vortex execute   CMD... | flush-cache | refresh-agenda | refresh-design APP_ID | schedule
                 [--json]  (alias: ex)
vortex server list  [--json]
vortex server get   [NAME] [--json]
vortex server use   NAME                                  (or: vortex use NAME)
vortex server libs  [NAME] [--refresh] [--json]
vortex docs      [--serve --port]
```

Every server command takes `-s/--server NAME`, after the command or before it (default: the
server set with `vortex use`;
a protected default must be named with `-s` to write or run a console command).
`--show-X` adds X to what is **printed**; `--include-X` adds X to what is **sent**.

Each entity command makes at most three small requests (three per ID for multi-ID
commands); only `clone` downloads a whole application.

### Output, errors and exit codes

Output is readable by default - tables for `list`, key/value for `get`. `--json` (every
`app`, `object`, `keyword` and `db` command, `fetch`, `server list|get|libs`, `execute`, `log`,
`ls` and a bare `vortex`) prints
exactly one JSON envelope
on stdout; logs, progress and prompts always go to stderr. `log --keep-alive --json` never
ends, so it prints one envelope per line instead - one per log entry, as it arrives:

```json
{"ok": true, "server": "dev", "data": {"appid": 9, "appname": "app", "appgroup": "bettrackr"}}
{"ok": false, "server": "dev", "error": {"code": "FORBIDDEN", "role": "GatewayDBWrite", "message": "..."}}
```

`data` keeps the server's own lowercase keys (`designbucketid`, `appname`). `error.code` is
the gateway's refusal code as sent (`FORBIDDEN` with the missing `role`, `NOT_FOUND`,
`SYSTEM_DB`, `SQL_NOT_ALLOWED`, `WEBDESIGN_UNAVAILABLE`, ...), or one the CLI sets:
`NOT_FOUND` (404), `FORBIDDEN` (a login page, 401/403), `UNAVAILABLE` (network, timeout, a
missing route), `CONFIRMATION_REQUIRED`, `CONFLICT` (an upload over someone else's newer
copy, or a pull that left something to merge), `OUT_OF_SYNC` (`fetch`: something needs a
pull), `NOT_CLONED`, `FORCE_NEEDS_TERMINAL` and `ERROR`. A `hint` says what to do next when
there is something to do.

Exit codes: `0` success, `1` failure (including a partly failed multi-ID command, whose
`data` then lists each ID's result), `2` bad arguments.

Nothing ever prompts without a terminal. The only wizards are `app create` and
`object create` run with no arguments in a terminal. Deletes look the item up, show it and
ask; `--yes` confirms, and without a terminal and without `--yes` they fail with
`CONFIRMATION_REQUIRED` and send nothing.

`vortex --show-permissions` lists every server command, whether this identity may run
it and the gateway role it is missing - read from the gateway's own route table (`whoami`),
so it is always the server's current rules.

### Updates are partial

Pass only what changes. The CLI starts from the server's current values, because
webdesign's writes replace whole rows (an omitted field is written blank):

- `app update` always reads the application row first (`GET ""`), overlays the passed
  fields and writes it back. `--param NAME=VALUE` replaces every param of that name,
  `--remove-param NAME` removes them; the whole param list is read, merged and written back.
- `object update` always reads the object from the **server** - never the clone, which lacks
  `Options` and unfetched resources and may hold undeployed edits - and writes it back with
  only the passed fields changed. `--source FILE` / `--data FILE` upload content (one ID).
- `keyword set` replaces a keyword's **whole** value list - there is no per-value edit - and
  folds duplicate KEYWORD rows of the same name into the oldest.
- `db update-table` / `update-column` start from the current dictionary row; the `--no-*`
  forms and `--null` turn things off.

Keyword and param writes are read-then-write: a concurrent edit between the two is lost.

No cache flush is needed after a write: webdesign's `vortex` API flushes the application's
cache itself (`flushHttpServerCache`) after every design, design-param, keyword, app-param
and application write, so a manual cache flush is never required after one.

### Console shortcuts

`vortex execute CMD...` (or `ex`) sends any console command - the gateway's `POST console`,
which needs `GatewaySystem`, or the SOAP console without the gateway. The words are joined,
so no quoting is needed. These names are shortcuts that send the exact strings the server's
addins match:

| Shortcut | Sends | Effect |
|---|---|---|
| `schedule` | `tell agenda schedule` | lists every scheduled action and its next run (raw text; `vortex agenda` parses it) |
| `refresh-agenda` | `tell agenda refresh` | AGENDA rereads every schedule now |
| `refresh-design APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` | rebuilds the application's design from its template, as `app refresh` does (a clone is then out of date) |
| `flush-cache` | `tell http cache flush` | clears the design cache and all action class loaders |

`tell http flush cache` is **not** a valid command: the HTTP addin only matches
`cache flush`, and anything else does nothing, silently. Use `flush-cache`.

`vortex app refresh APP_ID` also lives with its noun: it sends
`tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N`, which rebuilds the design from its
template (a clone is then out of date).

### Running an action

`vortex object run ID --app-id N` runs an ACTION now, as your identity, and waits for it:
the gateway's `POST run` (`GatewaySystem`) loads the action's class, executes it and replies
with how long it took, its redirect, content type and reply size. `--output` also prints the
action's reply (its buffer, as text). An action that throws exits 1 with `ACTION_FAILED` and
the exception; a path with no compiled ACTION is `NOT_FOUND`.

- **ACTIONs only.** A SCHEDULED_ACTION (or any other type) is `WRONG_TYPE`. Scheduled
  actions are queued with AGENDA: `vortex agenda run ID --app-id N` ([The agenda](#the-agenda)).
- **No client connection.** The action's `getOutputStream()` is null, as under AGENDA, so it
  must buffer its reply (`write`/`setBuffer`); one that streams unconditionally fails with
  the NullPointerException as its result.
- **Synchronous.** The CLI waits up to 300 s. Behind Cloudflare (prod) the request is cut
  at about 100 s; the action still finishes on the server - check `vortex log`.
- **Gateway required.** It needs a gateway with the `run` route; an older gateway refuses with
  `NOT_EXPOSED`. Earlier versions of `object run` sent `tell agenda run`, which only reached
  scheduled actions on AGENDA's list and did nothing, silently, for an ACTION.

The 8.0 flags (`--show-schedule`, `--refresh-agenda`, `--flush-cache`, `--refresh-design`,
`--run`) were removed in 8.1. `app refresh` was `app refresh-design` in 8.1.0; the old
name still works.

### The server log

`vortex log` prints the newest entries in the server log (`PMALOG`) - through the gateway's
`GET logs` (`GatewaySystem`), or with a blank `gateway_path` a query through webdesign's SQL
route.

```
vortex log                                  the last 10 entries
vortex log --since 2h --errors-only         errors in the last two hours
vortex log --from 02:00 --to 03:00 --errors-only -n 500
                                            what errored between 2 and 3 this morning
vortex log --from 2026-10-13 --to 2026-10-13 --search timeout
vortex log --since 1d --source agenda --all-pages -n 500 --json
vortex log -k                               new entries as they arrive (^C stops)
```

- **Times are the server's clock** - the clock `PMALOG` is stamped with, the gateway compares
  against, and every entry is printed in. `--from`/`--to` take `HH:MM[:SS]` (today),
  `YYYY-MM-DD` (`--to` a date alone means the whole day), `YYYY-MM-DD HH:MM[:SS]`, or ISO with
  an offset (`2026-10-14T02:00+10:00`, `...Z`; it needs the date) for an exact instant. Both ends are inclusive.
  `--since 30m|2h|3d|1w` (or `1h30m`) counts back from the server's now.
- **The server's clock decides.** `--since`, a time alone and a time with an offset are sent
  as given and resolved by the gateway on its own clock (`vortex/gateway` 0.2.0+), which
  replies with the window it applied; later pages and `-k` reuse that window. A date with a
  time is sent as written. An older gateway ignores what it cannot read: the CLI then takes
  the server's UTC offset from that reply (every entry carries it; when nothing matched, its
  default window or one `rows=1` read) and reads the page again - one request more until the
  gateway is updated. Without the gateway entries carry no offset, so this machine's is
  assumed.
- **`--search TEXT`** matches the message, user or server name; `-m` the message only,
  `--source` the source. All are case-insensitive "contains".
- **Bounded reads.** With none of `--since`/`--from`/`--to` the gateway reads only the last
  24 hours (`PMALOG` is never pruned, and the gateway counts the matches on every request);
  `--to` alone reads the 24 hours before it. `--since` goes back at most `520w`.
  The summary after the table says which window was read and how many entries matched.
- **Paging** (gateway only). `-n` is the page size (max 500). `--page N` reads page N, newest
  first; `--all-pages` reads every page, one request at a time, and stops after 20 (10,000
  entries at `-n 500`) - narrow the window for the rest. An entry written while paging is
  read twice and kept once. A blank `gateway_path` refuses both with `UNAVAILABLE`.
- **`--json`**: the same entries, oldest first. With any of the flags above the envelope also
  carries `window` (`{from, to, default}` on the server's clock; `default` when the gateway's
  24-hour window applied) and, through the gateway, `pager`
  (`{page, pagesRead, pages, total, rows}`). Plain `vortex log --json` is unchanged.
- **`-k`** follows new entries after the first read (it can start from a window, but not take
  `--to`, `--page` or `--all-pages`).

### The web log

`vortex weblog` prints the server's HTTP access log - the file `HTTPTextLog` names in
`puakma.config` - through the gateway's `GET logs?log=web` (`GatewaySystem`). Lines are printed
exactly as the server wrote them: their layout is the server's `HTTPLogFormat`, so the CLI
never parses them.

```
vortex weblog                       the last 20 lines of today's file
vortex weblog -n 200 -m /api/       the last 200 lines containing /api/
vortex weblog -m ' 50[0-9] ' --regex
vortex weblog --list                the log files, newest first
vortex weblog --file weblog_20261013.log
vortex weblog --file weblog_20261013.log -o oct13.log    the whole file, saved
vortex weblog -k                    new lines as they are written (^C stops)
vortex weblog -k -n 0               only lines written from now on
```

- **Tail, not search.** The gateway returns at most 256 KB per request and has no filter for
  this log, so `-m` filters on this side and `-n` counts matching lines within the last
  256 KB. For older lines or a rare match, `--all` reads the whole file in 256 KB pieces
  (`-o PATH` saves it instead of printing). Without `-m`, `-o` is a byte-for-byte copy: the
  gateway (0.2.0+) sends the bytes; an older one sends UTF-8 text, so a byte that is not
  UTF-8 arrives as U+FFFD.
- **Following.** `-k` polls every `-d` seconds (default 3) for what was written since the last
  request. Without `--file` it follows whichever file is current, so a date-stamped log that
  rolls over at midnight is picked up: the rest of the old file, then the new one (until the
  server writes the new day's first line, the old file is read on). A line still being
  written is held back until it is complete.
- **`--json`**: the tail and `--all` give `{file, size, from, lines}`; `-o` gives
  `{file, size, output, bytes}`; `--list` gives `{current, files: [{name, size, lastModified,
  current}]}`. With `-k`, one envelope per line `{file, line}`, as for `vortex log -k`.
- **Refusals.** `NOT_CONFIGURED`: the server writes no access log (`HTTPTextLog` is unset).
  `NOT_FOUND`: no such file - only names the `HTTPTextLog` pattern can produce are reachable.
  A gateway too old for `log=web` answers with the server log, which the CLI reports as such.
- **Gateway only.** webdesign cannot read files, so a server with a blank `gateway_path` is
  refused before anything is sent.

### Server statistics

`vortex stats` is a health check from the terminal: the time series each loaded addin keeps
in memory (the admin app's performance chart), through the gateway's `GET stats`
(`GatewaySystem`). One request per run, one row per statistic:

```
$ vortex stats -s dev
dev at 2026-10-14 22:36:58 - addins: STATS, HTTP, AGENDA, WIDGIE, MAILER
Statistic                          Now    Min    Avg    Max    Span  Trend
-------------------------------  -----  -----  -----  -----  ------  --------------------
stats.cpuusageperminute           21.5      0   2.08   14.2     61m  ▁▁▁▂▂▂▁▁▁▁▁▁▁▁▁▁▁▃▁█
stats.memoryappmbinuseperminute    455    270    438    623     61m  ▆▅▅█▅█▆▆▇▆▆▇▇▆██▅▅▁▃
stats.threadcountperminute          78     75   80.8    128     61m  ▁▁▁▅▄▅▅▅▅▅▅▅▅▅▄▃▃█▆▃
http.bytesouttotal               83.1M
http.hitsperminute                  27     13   22.2    176     61m  ▁▁▂▁▆▃▁▁▁▁▁▂▁▁█▁▁▅▅▅
...

vortex stats --addin HTTP           one addin (a short name, or its class name)
vortex stats --key stats.cpuusageperminute
vortex stats -k                     poll every 5 seconds and reprint (^C stops)
```

- **Columns.** `Now` is the newest period, still filling (this minute's hits so far) - or,
  for a total like `http.bytesouttotal`, its value. `Min`/`Avg`/`Max` are over the periods
  that have ended; `Span` is how far back the series goes, `Trend` its last 20 points. A `-`
  means the statistic has no entries yet. Units are in the key (`mb`, `bytes`, `perminute`);
  large numbers are shortened (`614K`, `11.4M`).
- **In memory only.** The server keeps a limited window (about the last hour of a
  per-minute statistic) and starts again on a restart, so a short `Span` means a recent
  restart.
- **`-k`** re-polls every `-d` seconds (default 5) and reprints, clearing the screen when the
  output is a terminal.
- **`--json`**: `data` is the addins as the gateway sends them, every key present -
  `[{name, class, statistics: [{key, captureType, created, lastUpdated, entries: [{from, to,
  value}]}]}]` (`captureType`: `once`, `per-second`, `per-minute`, `per-hour`, `per-day`; a
  `once` entry has no `from`/`to`) - and `time` (the server's clock) beside it. With `-k`, one
  envelope per poll, one per line.
- **Refusals.** `NOT_FOUND`: no addin of that name is loaded (`vortex stats` lists them).
  `NOT_EXPOSED`: the gateway predates `GET stats` - update `vortex/gateway`. A blank
  `gateway_path` is refused before anything is sent.

### Server files

`vortex file` works on the server's filesystem below the Puakma root (the parent of
`config/`), through the gateway's `files` route. **Admin only**, and gateway only: webdesign
cannot read or write files, so a server with a blank `gateway_path` is refused before anything
is sent.

```
vortex file list                          the Puakma root: folders first, then files
vortex file list logs
vortex file get addins/readme.txt         its text on stdout, its metadata on stderr
vortex file get lib/x.jar --hash --meta   size, modified time and SHA-256 only
vortex file download logs/big.log -o ~/tmp/
vortex file upload a.jar b.jar --to addins --overwrite
vortex file delete addins/old.jar         shown, then confirmed (--yes without a terminal)
```

- **Paths** are relative to the Puakma root (`/` is the root itself). The gateway resolves
  them canonically - symlinks and `..` followed - and anything that ends up outside the root
  is `NOT_FOUND`, exactly like a path that does not exist.
- **`get`** prints the text only for a file up to 2 MB with no NUL byte in its first 8 KB,
  decoded as UTF-8. Anything else is `TOO_LARGE` or `BINARY` (exit 1, with the metadata):
  use `download`, which streams the exact bytes to disk at any size. `--meta` asks only for
  the metadata; `--hash` adds the SHA-256.
- **`download`** writes beside the destination and moves the file into place once it has all
  arrived, so a failed download leaves nothing behind. An existing local file is kept unless
  `--overwrite`.
- **`upload`** sends every file in one request, into the folder under its own name (missing
  folders are created). The gateway checks them all before writing any: a name already in
  the folder is `EXISTS` and nothing is written, unless `--overwrite` - and then **each old
  file is kept beside the new one as `name.yyyyMMddHHmmss`**, the only undo there is. The
  SHA-256 the server reports is checked against the local file (`CORRUPT` otherwise).
- **`delete`** removes one file, symlink (never what it points to) or empty folder
  (`NOT_EMPTY` otherwise). It is final: **no backup, no undo**. The entry is shown and
  confirmed first; without a terminal `--yes` is required (`CONFIRMATION_REQUIRED`).
- `upload` and `delete` are writes: a protected default server must be named with `-s`.
- **Nothing under the root is hidden**, including `config/puakma.config` (the system database
  login), keystores and `.pmx` backups. `get` prints what you ask for - nothing reads more than
  the one path given.
- **`--json`**: `list` gives `{type: "dir", root, path, entries: [{name, type, size, modified,
  path}], truncated?}` (at most 5000 entries); `get` gives `{type: "file", root, path, name,
  size, modified, sha256?, tooLarge? | binary, text?}`; `download` gives `{path, output, size,
  sha256}`; `upload` gives `{path, files: [{name, path, size, sha256, replaced, backup?}]}`;
  `delete` gives `{path, type, size?}`.

### Local clones stay in sync

Server commands never need a clone. When the application **is** cloned, every write runs in
this order:

1. Take the application's workspace lock. A running `vortex watch` holds it, so the command
   stops here - before any request - with a hint to stop the watch.
2. Make the change on the server.
3. Write the server's reply into the clone.

If step 3 fails after the server change succeeded, the command exits 1, says what differs and
prints the command to re-sync (`vortex clone APP_ID -s SERVER`).

| Command | Local effect |
|---|---|
| `app update` | details and params; a name or group change moves the folder |
| `fetch --pull` | the objects' files (renamed with the server), `zbin/` classes and manifest entries (local edits kept - see [Working alongside other agents](#working-alongside-other-agents)) |
| `object create` / `update` / `delete` | the object's file and manifest entry (a metadata-only change moves the file but never overwrites its content; an uploaded class also lands in `zbin/`) |
| `object copy` | the target application's clone |
| `keyword set` / `delete` | the clone's stored keywords |
| `compile` | writes `zbin/`; `--upload` stores the uploaded blobs |
| `app refresh` / `execute refresh-design` | none - prints that the clone is out of date |
| `app create` / `import` / `export`, all `db` commands | none |

A clone stores the design objects, the application's details (with its description), its
params, its keywords and its DB connections (id, name, database). `keyword list --local` and
`db list --local` read them with no request. Dictionary tables and columns are not stored.

### Working alongside other agents

Several agents (or people) may edit one application from different sessions, workspaces or
machines. The server has no locking, so the CLI protects uploads itself, from the clone's
manifest: it holds the server's copy of every element as the clone last synced it - its
**base** - and every upload compares three things first:

| Server now vs base | Upload |
|---|---|
| the same: nobody else wrote it | sent; the base becomes what was sent |
| already exactly what you are sending | sent (nothing is lost) |
| anything else | **refused** (`CONFLICT`, nothing sent), naming who changed it and when |

Both blobs count: a class uploaded on top of someone else's newer source is refused too.

```
vortex fetch --app-id 9                # before working: anything outdated?
vortex object diff --app-id 9 --theirs # what did the server get?
vortex fetch --app-id 9 --pull [ID...] # bring the clone up to date
...edit...
vortex compile --app-id 9 --object 396 --upload --include-source
```

**`vortex fetch`** lists the elements that are not in sync - `edited` (yours, ok to
upload), `outdated` (the server changed the content), `metadata` (the server renamed or
retyped it, or changed its comment, params, content type or inherit-from - the Server column
says what, e.g. `renamed home -> landing`), `conflict` (both changed), `unresolved` (a pull
conflict not merged yet), `new` / `deleted` on the server - and exits 1 when anything needs
a pull. It writes nothing. Without `--app-id` it checks the cloned application the current
directory is in, else every clone of the server. A whole application is one request as
heavy as a clone; IDs are one small request each.

**`vortex object diff`** shows the difference between two of the three versions: server ->
local by default (what an upload would change), `--mine` base -> local (your edits; no
request) or `--theirs` base -> server (what changed there). Without IDs it shows every
element that differs. With git installed it runs `git diff --no-index` on temporary files -
colour, `--stat`, `--word-diff` - otherwise Python's difflib. Everything after `--` goes to
`git diff` as-is, after vortex's own options so it wins: options only, values attached
(`-- --ignore-all-space --diff-algorithm=histogram -U10`). `--write DIR` runs no diff and
writes the versions as files instead - `DIR/base`, `DIR/local`, `DIR/server`, each
`<TYPE>/<file>` - for any other tool (`git diff --no-index DIR/base DIR/server`,
`git merge-file`, an IDE).

**`vortex fetch --pull`** brings the server's changes in from the same request. Metadata is
always the server's: a rename moves your file - edits and all - to the new name, and the
comment, params and the rest are updated in the clone. Content: it takes the server's copy
wherever that loses nothing (outdated, new, deleted, missing locally) and keeps local edits
when the server is unchanged. When both
changed it runs git's 3-way merge (`git merge-file` on base, local and server - temporary
files, never a repository): edits that don't overlap are **merged** into your file, the
server's copy becomes the base, and the element is an ordinary edit to review and upload.
Overlapping edits - or binary content, no git, or `--no-merge` - keep your file, write the
server's copy to `<app>/.conflicts/<TYPE>/<file>` and make the server's copy the base. While that copy
exists the element is `unresolved` and every upload of it is refused: merge it into your
file, delete it, then upload. `--force` takes the server's copy over local edits; it only
ever changes the clone. A file already where a new element goes is never overwritten - it
becomes the element's local copy, with the server's under `.conflicts/`; a rename onto such
a file leaves the element where it is until the file is moved. `--pull` is
refused for an application while `vortex watch` runs on it.

`compile --upload` without `--object` sends only the classes whose `.java` was edited since
the clone synced it, so it can't re-send an untouched, older class over someone else's.
`--force` overrides a refusal and discards the server's newer copy; it needs a terminal.

The check is a read then a write, not atomic: two uploads of one element within
milliseconds can still race. Keywords, app params and design params are not covered - each
write replaces the whole set.

### Scheduled actions

A scheduled action's schedule lives in its `Options` (a comma-separated `name=value` string
that the AGENDA addin reads - it is not a design param). `object create` and `object update`
set it with flags, for SCHEDULED_ACTION objects only:

```
vortex object create --app-id 62 --type scheduled_action --name Nightly --schedule N
vortex object update 7901 --app-id 62 --schedule D --start-time 02:30 --days MTWHF
vortex object update 7901 --app-id 62 --schedule N     # stop it running
vortex object get 7901 --app-id 62                     # the parsed schedule
```

| Flag | Option | Values |
|---|---|---|
| `--schedule` | `Schedule=` | `N` never, `S` second, `I` minute, `H` hour, `D` day, `W` week, `M` month, `Y` year |
| `--interval` | `Interval=` | units between runs, >= 1 |
| `--days` | `Days=` | letters from `SMTWHFA` (S=Sun M=Mon T=Tue W=Wed H=Thu F=Fri A=Sat) |
| `--start-time` | `StartTime=` | `HH:mm`, 24-hour; minute `*` = a random minute |
| `--finish-time` | `FinishTime=` | `HH:mm` (up to `24:00`); only used by S/I/H schedules |
| `--date` | `Date=` | 1-31 (M and Y) |
| `--month` | `Month=` | 1-12 (Y) |

- **Merged, never replaced.** Only the keys you pass change, in place and in canonical
  casing; `LastRun` (AGENDA's own) and any other key stay, in order. AGENDA reads the first
  case-insensitive `key=` anywhere in the string, so a change an earlier entry would hide
  (`StartDate=` hides `Date=`) is refused.
- **Checked first.** Values are validated before anything is sent (exit 2). A schedule flag
  on any other `--type` is a usage error; on `object update`, every ID is read first and if
  any is not a scheduled action the whole command fails with `WRONG_TYPE` and nothing is
  written.
- **Applied at once.** After a successful schedule change the CLI sends one
  `tell agenda refresh` for the whole command (the console route `vortex execute` uses:
  the gateway's `POST console`, `GatewaySystem`, or the SOAP console without the gateway).
  It has to: when an action starts, AGENDA writes back the `Options` it cached at its last
  refresh (AGENDA `updateDesignBucket` / `AgendaItem.getOptionString`), so without a
  refresh a run within the next 15 minutes would undo the change. Only the moment between
  the write and the refresh remains. If the refresh fails or is refused, the change is
  still written and the command exits 0 with a loud warning to run
  `vortex execute "tell agenda refresh" -s SERVER` (naming the missing role for
  `FORBIDDEN`); `--json` reports `"agendaRefreshed": true|false` (plus
  `agendaRefreshError` when false).
- **LastRun race.** AGENDA also writes `LastRun` into `Options` whenever the action
  starts; if that lands between the command's read and write, the old `LastRun` is written
  back and the action may run one interval early.

### The agenda

AGENDA has no API beyond its console commands, so `vortex agenda` sends them through the same
console route as `vortex execute` (the gateway's `POST console`, `GatewaySystem`, or the SOAP
console without the gateway) and parses the text they answer.

```
vortex agenda -s dev                     # = agenda list
vortex agenda list --app-id 9 -s dev
vortex agenda run 7825 --app-id 60 -s dev [--param AppID=12] [--refresh] [--force]
```

- **`list`** - `tell agenda schedule`: every scheduled action AGENDA has **loaded**, by next
  run (server time), with the waitlist and AGENDA's counts (running, waitlisted, scheduled,
  max at once). An action with empty `Options` is loaded as *on demand only*. Not listed: an
  action with `Schedule=N`, one in a disabled application or an application with scheduled
  actions disabled, and one created or renamed since AGENDA's last refresh (every 15
  minutes). AGENDA says how many actions are running, never which.
- **`run`** - queues a SCHEDULED_ACTION: `tell agenda run /group/app.pma/Name[?&KEY=VALUE...]`.
  It runs as the system session when one of AGENDA's slots is free; its result is only in
  `vortex log`. AGENDA answers that command with nothing, whatever it did, so `run` checks
  first and sends nothing when AGENDA would not run it:
  - `NOT_ON_AGENDA` - the action has `Schedule=N` (give it a schedule first), or it is not on
    the loaded list. `--refresh` sends `tell agenda refresh` and checks again (for an action
    created or renamed in the last 15 minutes).
  - `ALREADY_QUEUED` - it is already waitlisted; AGENDA would skip it.
  - `PREFIX_COLLISION` - AGENDA matches the path by prefix and queues it once per matching
    item, so `.../SyncAll` would also match an item `.../Sync` and run twice. `--force` sends
    it anyway.
  - `WRONG_TYPE` - not a SCHEDULED_ACTION (an ACTION is run with `vortex object run`).
- A queued run moves the action's next scheduled run (AGENDA resets it), and it can't be
  stopped from here (`vortex execute tell agenda stop /group/app.pma/Name` asks the action to
  quit; it has to check for that itself).

### Keyword values and secrets

Keywords are an application's live configuration, and in practice they hold credentials in
cleartext - API keys, passwords, vendor secrets, signing keys. `vortex keyword` therefore
**redacts by default**, in readable and `--json` output alike:

```
vortex keyword list --app-id 9                     # every keyword (secrets redacted)
vortex keyword get AppVersion --app-id 9           # just this one
vortex keyword set AppVersion 3.7.0 --app-id 9     # replace its values
vortex keyword list --app-id 9 --reveal            # print secret values in full
vortex keyword list --app-id 9 --local             # the clone's copy, no request
```

- A keyword is treated as secret when its **name** contains a marker such as `password`,
  `passwd`, `passphrase`, `secret`, `credential`, `signature`, `apikey`, `privatekey`,
  `keystore`, `webhook`, `connectionstring` or `mnemonic`, or when any *word* of the name
  is one of `key`, `keys`, `pwd`, `token`, `salt`, `hash`, `private`, `auth`, `bearer`,
  `cert`, `pem`, `jwt`, `dsn`, `otp`, `pin`, `seed` or `sig`. Names are split on
  camelCase/snake_case/kebab-case, so `AccessKeyId` and `API_KEY` are redacted while
  `Monkey` and `Concert` are not. The match is on the name only, and deliberately biased
  towards over-redaction: a false positive costs one `--reveal`.
- Redacted values print as `<redacted>`, one per value, so the value *count* stays visible.
- `keyword set` prints the prior values beside the new ones (redacted for secret names).
  Negative numbers such as `-1` work as values as they are. A value that looks like an
  option (`-abc`, `--x`) needs the options first and `--` before the name:
  `vortex keyword set --app-id 9 -- Flags -abc --x`.

### Cloning a whole group

`vortex clone` is the one command that takes names as well as IDs:

```
vortex clone 13                 # by ID
vortex clone bettrackr_app      # by TemplateName
vortex clone bettrackr/app      # by group/name
vortex clone BetTrackr          # every non-inherited application in the BetTrackr group
vortex clone -a BetTrackr       # ... including the inherited ones
vortex clone --group BetTrackr  # explicitly a group, never a TemplateName
```

A group clone skips inherited applications unless `--all`/`-a` is given (the inventory has no
`DisableApp` flag, so disabled applications are not skipped). An application you name
outright is always cloned. Groups are matched the way `app list --group` matches them - a
case-insensitive substring - except that an exact group name always wins, so cloning
`BetTrackr` never drags in `BetTrackrLegacy`. If a partial name still spans several groups,
vortex stops and lists them. A bare word that is genuinely both a TemplateName and a group
is refused: use `--group NAME` or the ID. References may be qualified as `SERVER:REF`
(`dev:BetTrackr`).

### Working with Multiple Servers

Each section in `servers.ini` defines a server (hosts must be unique across
sections). Cloned apps remember which server they came from:

- `vortex watch` watches **every** cloned app and uploads each change to the
  server it was cloned from. Use `--server` to watch a single server only.
- `object grep`, `ls` and `object list` without `--app-id` search across all cloned apps
  unless `--server` is given.
- `watch`, `clone` and `clean` take a workspace-wide lock: only one watch at a
  time, and cloning or cleaning is refused while a watch is running. Commands
  that write into one cloned application (every mirrored server write,
  `compile`) take that application's lock, so they are refused for apps a
  watch is watching and run concurrently otherwise.
- In VS Code, app folders are listed in per-server blocks with the server's jars
  on the Java classpath. For a guaranteed-correct classpath, open a single
  server's own workspace with `vortex code -s <server>`.

Every server command targets `-s <server>`, else the `vortex use` default:

```
vortex server list      # what is defined, and which is the default
vortex server get dev   # every option of one server and where it comes from
vortex use dev          # = vortex server use dev
vortex app list
```

#### Credentials

`username`/`password` can be left out of `servers.ini` (when set there, they win).
Each server's credentials otherwise resolve in this order and are only requested
when a command actually connects to that server - `vortex server get NAME` says which
one applies:

1. The system keyring - store with `keyring set vortex-cli:<server> <username>`
   (requires `pip install keyring`)
2. Per-server environment variables `VORTEX_USERNAME_<SERVER>` /
   `VORTEX_PASSWORD_<SERVER>` (e.g. `VORTEX_PASSWORD_DEV`)
3. `VORTEX_USERNAME` / `VORTEX_PASSWORD`
4. An interactive prompt naming the server

#### Protected Servers

`protected = true` means two things:

- `vortex watch` skips the server unless `--include-protected` is given, so saving a file
  can never hot-deploy to it by accident.
- A write or console command never reaches it by default: when a protected server is only
  the `vortex use` default, those commands refuse to run until it is named with `-s`.

Only deletes are confirmed (`--yes` without a terminal); other writes are not - the gateway's
roles are the protection, so run agents against production with an identity whose roles fit
the job.

### Backend: gateway or webdesign

Each server picks where requests go, explicitly - nothing is probed and nothing falls back:

| `gateway_path` | Behaviour |
|---|---|
| set (the default `vortex/gateway.pma`) | through the vortex gateway: required and presumed installed; any failure is an error |
| blank | webdesign's `vortex` action directly (`webdesign_path` + `/vortex`): the same routes and bodies, with **no** role checks |

The gateway (`vortex/gateway`, a Puakma application you deploy) is `system/webdesign`'s
`vortex` JSON API at a different address - `https://<host>/vortex/gateway.pma/api/<path>` is
webdesign's `/system/webdesign.pma/vortex/<path>` - with three things in front of it:

- **Role checks.** Every route needs a role, checked on the server: `GatewayDesignRead`,
  `GatewayDesignWrite`, `GatewayDBRead`, `GatewayDBWrite`, `GatewaySystem` and `Admin`
  (every route). Write implies read. A route the gateway does not map is refused.
  `vortex --show-permissions` shows which commands your identity may run.
- **Guards.** The gateway never addresses the Puakma system database (`SYSTEM_DB`), never
  lets an id from one application be used under another (`NOT_FOUND`), and runs exactly one
  `SELECT`/`INSERT`/`UPDATE`/`DELETE` per SQL request (`SQL_NOT_ALLOWED`: DDL, `WITH`, and
  any `;`).
- **Its own routes:** `whoami` (a bare `vortex`), the server and web logs (`log`, `weblog`), the console (`execute`),
  addin statistics (`stats`), `.pmx` export and import, and the server's files (`file`, `Admin`).

On a server with a blank `gateway_path`:

| Command | Behaviour |
|---|---|
| `app`, `object`, `keyword`, `db`, `clone`, `compile`, `watch`, `server libs` | the same routes, sent to webdesign's `/vortex` |
| `db query` | a CLI-side guard replaces the gateway's: one `SELECT`/`INSERT`/`UPDATE`/`DELETE` statement, refused otherwise with `SQL_NOT_ALLOWED` before anything is sent |
| `app export` | webdesign's `ExportPMX` |
| `vortex` (no command) | the console `status` command over SOAP (`soap_path`); `--show-permissions` says "no gateway: webdesign direct, no role checks" |
| `log` | a `PMALOG` query through webdesign's SQL route, on the system-database connection owned by the ungrouped `puakma` application (found once per run); `--since`/`--from`/`--to`/`--search` go into the query, `--page`/`--all-pages` are refused |
| `execute` | the console command over SOAP |
| `weblog` | unavailable: webdesign cannot read the access log |
| `stats` | unavailable: webdesign has no route to the addin statistics |
| `file` | unavailable: webdesign cannot read or write files |
| `app import` | unavailable (`UNAVAILABLE`) |

Without the gateway there are no role checks, no system-database block and no cross-app
ownership checks: use the gateway on any server an agent works against. If a gateway deploy
ever breaks the gateway, blanking `gateway_path` reaches the server through webdesign to fix
it.

There is **no undo**: nothing keeps a journal of what a write replaced. Deletes and uploads
are final.

> **Note:** the gateway's roles are only a real boundary for an identity whose *sole* route to
> the server is the gateway. Any identity that can reach `system/webdesign` or deploy code can
> grant itself any role.

### Java Design Objects, `zbin/` and `vortex compile`

The server runs compiled classes, not source. There are three ways to get a class there:

- **`vortex compile --app-id ID [--object ID...] --upload`** builds the cloned application's
  Java into `zbin/` with ecj 3.46.0 (the compiler PuakmaVortexVSCode uses), run by the
  server's `java_home` against the server's own jars, for the server's
  `java_environment_name` (`JavaSE-17` -> `--release 17`; unset is an error naming the
  setting). `--upload` sends each compiled class to its existing design element, one at a
  time (`--include-source` adds the `.java`): the `--object` ones, else those whose `.java`
  was edited since the clone synced it (`--all`: every one). It refuses when anything failed
  to compile, refuses each element whose server copy changed since (see
  [Working alongside other agents](#working-alongside-other-agents)), and never creates
  elements (`object create` does). ecj is downloaded once from Maven Central;
  the server's libraries are fetched on first use.
- **`vortex object update ID --app-id APP --data Foo.class [--source Foo.java]`** uploads a
  class you built yourself.
- **`vortex watch`** uploads what the IDE's Java build writes into `zbin/`.

Tornado loads one class per design element, so nested classes never ship. `compile` treats
a source as a **compile error** - exit 1, its classes named, nothing written to `zbin/` for
it, never uploaded - when it produces `Outer$*.class` files (inner, anonymous or local
classes). Use top-level SHARED_CODE classes instead. Lambdas and method references are
fine - they compile into the class itself (`invokedynamic`), no `$` class. A `switch`
over an enum is fine: ecj keeps it inside the class (javac would add a synthetic `Outer$1`).
`watch` likewise refuses to upload a class compiled alongside `$` siblings.

`compile` always writes into the clone, so it takes the application's lock and is refused
while `vortex watch` runs - with or without `--upload`.

### Server-Provided Java Libraries

The IDE's Java build, IntelliSense and `vortex compile` need the Puakma framework jar and the
server's shared libraries on the classpath. vortex downloads them from each server's
webdesign `vortex` API (`systemjar` and `libraries`, through the gateway when
`gateway_path` is set - `GatewayDesignRead`) and caches them per server under
`<workspace>/<host>/.lib/`:

- `clone` does **not** download them; it prints a one-line `vortex server libs SERVER --refresh`
  hint when they are not cached. `compile` and `watch` fetch them on first use.
- `vortex server libs` shows what's cached for each server; `vortex server libs --refresh`
  re-downloads (name a server for just that one), e.g. after a server upgrade.
- Each server's VS Code workspace (`vortex code -s <server>`) uses that server's own cached
  jars, so identical class names on different servers/versions never cross-contaminate.
- `vortex clean` keeps each host's `.lib` cache; pass `--include-libs` to remove it as well.

### Databases and the data dictionary

Every `db` command takes `--app-id` (every database route is app-scoped) and a `DB`: the
connection name, the database name or the connection ID, resolved within that application.
`db list` is the exception: without `--app-id` it lists every application's connections on
the server (one request per application), and every row names its application (id, group
and name). `--local` reads the clones instead - the `--app-id` one, else every clone of the
server.

```
vortex db list                  # every application on the server
vortex db list --app-id 9
vortex db query bettrackr --app-id 9 "SELECT * FROM account" --limit 5
vortex db query 3 --app-id 9 --file report.sql --json
vortex db get-table bettrackr invoice --app-id 9
```

`db query` sends the statement as given, apart from adding `LIMIT n` (default 10) to a
`SELECT` that has none - a full page of rows is flagged as possibly truncated. There is no
`--update` flag: the gateway classifies the statement (`SELECT` needs `GatewayDBRead`,
`INSERT`/`UPDATE`/`DELETE` `GatewayDBWrite`, everything else is refused). A numeric `DB` is one
request; a name is two.

The `create-`/`update-`/`delete-` table and column commands record design-time definitions in
the Puakma data dictionary (the PMATABLE/ATTRIBUTE tables). They **never run DDL** - deletes
only remove dictionary rows, never real tables or columns - so change the real table by hand.

```
vortex db create-table mydb invoice --app-id 9 --description "Customer invoices"
vortex db create-column mydb invoice invoice_id --app-id 9 --type INTEGER --pk --auto-increment
vortex db create-column mydb invoice total --app-id 9 --type NUMERIC --size 10,2 --not-null
vortex db create-column mydb invoice customer_id --app-id 9 --type INTEGER --ref customer
vortex db update-column mydb invoice total --app-id 9 --null
```

- `--type`: `VARCHAR`, `CHAR`, `LONGTEXT`, `INTEGER`, `DATETIME`, `NUMERIC`, `LONGBLOB` or
  `JSON`. `--size` only for `VARCHAR` (default 50), `CHAR` (default 1) and `NUMERIC`
  (default `6,2`); changing the type without `--size` resets the size to the new type's
  default.
- `--ref TABLE` records the referenced table only - the dictionary has no referenced-column
  field. There is no `--default` or `--position` (webdesign ignores both).
