Metadata-Version: 2.4
Name: vortex_cli
Version: 8.0.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 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.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` |

- **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 `status` 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

── 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 copy   ID... --app-id SRC --to-app-id TGT [--copy-params]
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]

── Workspace ────────────────────────────────────────────────────────────────
vortex ls        [app list filters]               = app list --local
vortex clone     APP... [--group --reclone --all --get-resources --open-urls --timeout]
vortex compile   --app-id ID [--object ID...] [--upload [--include-source]] [--show-warnings]
vortex watch     [--include-protected]
vortex clean     [--app-id ID] [--all --include-libs]
vortex grep      PATTERN [--app-id ID] [--output-paths|--output-apps]
                 [--include-resources|--type]
vortex find      QUERY [--app-id ID] [--strict --inherits-from|--parent-page --ids-only
                 --show-params --type]
vortex code
vortex libs      [--refresh]

── Server and setup ─────────────────────────────────────────────────────────
vortex status    [--show-permissions] [--json]
vortex log       [-n --source -m --errors-only|--debug-only|--info-only -k -d]
vortex execute   CMD | --refresh-design APP_ID | --run PATH | --show-schedule
                 | --refresh-agenda | --flush-cache
vortex config    --sample | --list-servers | --set S O V | --set-password
                 | --output-config-path | --output-workspace-path | --output-server-config
                 | --update-vscode-settings | --reset-vscode-settings
vortex use       SERVER_NAME
vortex docs      [--serve --port]
```

Every server command takes `-s/--server NAME` (default: the server set with `vortex use`).
`--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, and `status`) prints exactly one JSON envelope
on stdout; logs, progress and prompts always go to stderr:

```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` 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 status --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` sends any console command (the gateway's `POST console`, which needs
`GatewaySystem`, or the SOAP console without the gateway). The shortcuts send the exact
strings the server's addins match:

| Shortcut | Sends | Effect |
|---|---|---|
| `--show-schedule` | `tell agenda schedule` | lists every scheduled action and its next run (was `--schedule`) |
| `--refresh-agenda` | `tell agenda refresh` | AGENDA rereads every schedule now |
| `--flush-cache` | `tell http cache flush` | clears the design cache and all action class loaders |
| `--refresh-design APP_ID` | `tell agenda run .../RefreshDesign?&AppID=N` | rebuilds the application's design from its template |
| `--run PATH` | `tell agenda run /group/app.pma/action` | runs the action at that local path now |

`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`.
`execute --schedule` was renamed to `--show-schedule` (`object update --schedule` now sets
a schedule); the old flag runs nothing and prints the new one.

### 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 |
| `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 |
| `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.

### 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.

### 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.
- `find`, `grep` and `ls` 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 use dev
vortex app list
```

#### Credentials

`username`/`password` can be left out of `servers.ini`. Each server's
credentials resolve in this order and are only requested when a command
actually connects to that server:

1. The system keyring - store with `vortex config --set-password -s <server>`
   (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 one thing: `vortex watch` skips the server unless
`--include-protected` is given, so saving a file can never hot-deploy to it by
accident. There are no write confirmations - 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 status --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` (`status`), the server log (`log`), the console (`execute`),
  `.pmx` export and import.

On a server with a blank `gateway_path`:

| Command | Behaviour |
|---|---|
| `app`, `object`, `keyword`, `db`, `clone`, `compile`, `watch`, `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` |
| `status` | 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) |
| `execute` | the console command over SOAP |
| `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`); it refuses when anything failed to compile 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) or uses lambdas or method references (its bytes reference
`java/lang/invoke/LambdaMetafactory`). Use top-level SHARED_CODE classes instead. 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 libs --refresh -s SERVER`
  hint when they are not cached. `compile` and `watch` fetch them on first use.
- `vortex libs` shows what's cached for each server; `vortex libs --refresh` re-downloads
  (add `-s <server>` for one server), 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.

```
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).
