## Purpose Command-line access to a running `app` GUI's live scene graph over the netdebug ZeroMQ backend: navigate and display nodes and properties, set typed values and exprs, create and remove `Layer`/`VectorArt` nodes, and push shape data — all without recompiling the app. ## ADDED Requirements ### Requirement: CLI entrypoint and connection The system SHALL provide a `python -m pydrk` command (runnable from `bin/app`, argparse-based) whose commands talk to a running app's netdebug REQ/REP endpoint. The endpoint SHALL default to `127.0.0.1:9484` and be overridable with `--addr`/`--port` on every subcommand. When no app is listening, commands SHALL print an error naming the endpoint they tried and exit non-zero. #### Scenario: connectivity check - **WHEN** `python -m pydrk ping` runs against a running dev-mode app - **THEN** the command prints `hello` and exits 0 #### Scenario: app not running - **WHEN** a command runs with `--port 9999` and nothing is listening there - **THEN** the command prints an error containing `127.0.0.1:9999` and exits non-zero ### Requirement: Tree navigation and listing The `ls [path]` subcommand SHALL list the contents of a scene node: first its child nodes as one row per child showing the child name, numeric node id, and lowercase type name (e.g. `content 1234567890 layer`), then its properties as one row each showing name, type, and current value summary (exprs as source, shapes as a placeholder). Paths that do not resolve SHALL produce a readable `node_not_found` error and non-zero exit. #### Scenario: list the scene root - **WHEN** `python -m pydrk ls /` - **THEN** the built-in top-level nodes are listed (at least `setting` and `window`) followed by the root's properties #### Scenario: unknown path - **WHEN** `python -m pydrk ls /nope` - **THEN** the command reports `node_not_found` for `/nope` and exits non-zero ### Requirement: Recursive tree display The `tree` subcommand SHALL recursively print a node's descendants with a `--depth N` limit, showing for every node: its name, id, type, properties with current values, signals with registered slots, and methods with full signatures. Property values SHALL be rendered distinctly per status: plain values as literals, exprs as their decompiled source (e.g. `w/2`), null as `null`, unset-with-default as the default, and vector shapes as a placeholder (shapes are write-only over the wire). Methods that declare a result SHALL show the result argument types (not be silently truncated). #### Scenario: shallow dump - **WHEN** `python -m pydrk tree / --depth 2` - **THEN** two levels of the tree print, each property line showing name, type and value, and the command exits 0 #### Scenario: method with result renders its signature - **WHEN** `python -m pydrk tree /plugin/drk` - **THEN** the `get_default_address` method line includes its result signature (a `str` result), demonstrating result decoding ### Requirement: Property metadata and value display The `props [path]` subcommand SHALL list a node's properties with their metadata: name, type, subtype, array length (marking unbounded), null/expr allowance, ranges when bounded, enum items when present, and UI text. The `show [path] PROP` subcommand SHALL print all info for one property: its full metadata (as listed for `props`, plus its depends list) followed by the current per-index values with their statuses. The `get [path] PROP [idx]` subcommand SHALL print the property's per-index values (only index `idx` when given), each on its own line annotated with its status (`value`, `expr`, `null`, or `unset`). For all three, a leading path argument is optional and defaults to the shell cwd (or `/` in one-shot mode); in `get`, when the final argument is an integer it is taken as the index. #### Scenario: metadata shows bounds and enums - **WHEN** `python -m pydrk props /window/content` - **THEN** the `alpha` property (or another bounded one) shows its `[0.0, 1.0]` range #### Scenario: show a single property - **WHEN** `python -m pydrk show /window/content alpha` - **THEN** the output contains the property's metadata (type `float32`, the `[0.0, 1.0]` range, its UI text) followed by `0: value 1.0` #### Scenario: expr value is distinguishable - **WHEN** a property index holds an expr and `python -m pydrk get` is run for it - **THEN** the output line is annotated `expr` and shows the expr source string ### Requirement: Typed property setting The `set` subcommand SHALL set values using the property's server-declared type for encoding (bool, uint32, float32, str, enum, scene_node_id as decimal). Its grammar is `set [path] PROP [idx] VAL`: the last argument is always the value; when the argument before the value is an integer it is taken as the array index (default 0); any remaining leading arguments (joined with `/`) are the node path, optional and defaulting to the shell cwd (or `/` in one-shot mode). The `--expr` flag sends the value as expr source to be compiled server-side. After a successful set the app SHALL redraw. Server rejections (wrong type, out-of-range, invalid enum item, invalid expr syntax, unknown expr global) SHALL be printed readably with the error name and exit non-zero, and a usage error SHALL be reported when the arguments cannot be parsed into the grammar (e.g. a property name is missing). #### Scenario: set a boolean with an explicit path - **WHEN** `python -m pydrk set /window/content/chat is_visible false` - **THEN** the command exits 0, a subsequent `get` shows `false`, and the app window updates #### Scenario: index and path in one command - **WHEN** `python -m pydrk set /window/content rect 2 "w/2" --expr` - **THEN** the command exits 0 and `get /window/content rect 2` shows `expr "w/2"` #### Scenario: out-of-range rejection - **WHEN** setting a bounded float32 property to `5.0` when its range is `[0.0, 1.0]` - **THEN** the command prints `property_out_of_range` and exits non-zero #### Scenario: expr set round-trip - **WHEN** `python -m pydrk set rect 2 "w/2" --expr` - **THEN** the command exits 0 and `get` for that index shows `expr "w/2"` ### Requirement: Node creation The `mknode` subcommand SHALL create and attach a node in one step: `mknode ` where `` is `layer` or `vector_art`. On success it SHALL print the new node's id and full path, and the node SHALL immediately appear in `ls ` with all its factory properties (queryable via `props`). Creating a node whose parent path does not resolve SHALL fail with `node_not_found`; a name colliding with an existing sibling SHALL fail with a name-conflict error; any other type string SHALL fail with a readable `unsupported node type` message without touching the tree. #### Scenario: create a debug layer with vector art - **WHEN** `python -m pydrk mknode /window/content debug_layer layer` then `python -m pydrk mknode /window/content/debug_layer art1 vector_art` - **THEN** both commands print ids and paths, and `props /window/content/debug_layer/art1` lists the factory properties including `shape` #### Scenario: unsupported type - **WHEN** `python -m pydrk mknode /window/content debug_layer chatview` - **THEN** the command reports the type as unsupported and exits non-zero ### Requirement: Node removal The `rmnode ` subcommand SHALL remove any node subtree from its parent (the node and its descendants disappear from listings) and trigger an app redraw; GPU resources owned by removed nodes SHALL be released by the app. This is a debugging tool with full scene-graph access: built-in nodes are removable the same way as wire-created ones. Removing the scene root `/` SHALL fail with a readable error. All removals are runtime-only and undone by restarting the app. #### Scenario: remove a wire-created layer - **WHEN** a layer was created with `mknode` and `python -m pydrk rmnode /window/content/debug_layer` runs - **THEN** `ls /window/content` no longer lists `debug_layer` and the app redraws #### Scenario: remove a built-in node - **WHEN** `python -m pydrk rmnode ` runs against a running dev app - **THEN** the subtree disappears from listings and rendering, and restarting the app restores it #### Scenario: the scene root is not removable - **WHEN** `python -m pydrk rmnode /` - **THEN** the command fails with `node_not_removable` and the tree is unchanged ### Requirement: Shape data creation The `set-shape [--prop NAME] [--index N]` subcommand SHALL build a vector shape from repeatable primitive flags and push it as the property's value: `--box X1 Y1 X2 Y2`, `--gbox X1 Y1 X2 Y2` (top and bottom colors), `--vgradient X1 Y1 X2 Y2 TOPCOLOR BOTCOLOR STRIPS GAMMA`, `--outline X1 Y1 X2 Y2 BORDERPX`, `--line X1 Y1 X2 Y2 THICKNESS`, and `--glow CX CY W H SEGMENTS COLOR`, each taking colors as `R G B A` float groups. Coordinates SHALL accept both plain numbers and expr source strings (e.g. `w/2`), and primitives SHALL join into a single shape in flag order. Shape indices are 16-bit; vertex counts beyond that SHALL be rejected client-side with a readable message. After a successful set the shape SHALL be visible in the app window at the next frame (given a non-empty `rect` and `is_visible`). #### Scenario: draw a red bar over the wire - **WHEN** a `vector_art` node exists with `rect` set, and `python -m pydrk set-shape /window/content/debug_layer/art1 --box 0 0 w 10 --color 1 0 0 1` runs (with `w` passed as an expr coordinate) - **THEN** the command exits 0 and a red bar renders along the top of the node's rect in the app #### Scenario: invalid expr in shape coordinates - **WHEN** a coordinate references an unknown global (e.g. `q/3`) - **THEN** the server rejects the shape and the CLI prints the error name (e.g. `sexpr_global_not_found`) and exits non-zero ### Requirement: Method and signal introspection The `methods ` subcommand SHALL list each method with its argument signatures and, when declared, result signatures; `signals ` SHALL list signal names. The `call [ARGS...]` subcommand SHALL encode positional ARGS according to the method's declared argument types (uint32/uint64/float32/bool/str; hash as 64-char hex), print the decoded result when the method returns one, and `void` when it does not. #### Scenario: call a no-result method - **WHEN** `python -m pydrk call /window/content/chat/view copy_select` - **THEN** the command prints `void` and exits 0 #### Scenario: argument type mismatch - **WHEN** calling a method whose first declared argument is `str` with a non-string token where coercion is impossible, or supplying the wrong number of arguments - **THEN** the CLI rejects it locally with a readable usage error before sending anything ### Requirement: Server error reporting Every subcommand SHALL map netdebug error frames to the human-readable error name from the netdebug error table (e.g. `property_not_found`) together with command context (path, property, method as applicable), and exit non-zero. Unknown error codes SHALL be printed with their numeric value. #### Scenario: unknown property - **WHEN** `python -m pydrk get /window no_such_prop` - **THEN** the output contains `property_not_found` and the exit code is non-zero ### Requirement: Interactive shell mode Running `python -m pydrk` with no subcommand SHALL start an interactive shell connected to the same endpoint, maintaining a current working node path (cwd, initially `/`) shown in the prompt (e.g. `pydrk:/window> `). Shell commands SHALL reuse the one-shot command set with path arguments resolved against cwd; absolute paths starting with `/` SHALL be honored as absolute. `pwd` SHALL print the cwd; `cd ` SHALL change it, `cd` with no argument SHALL go to `/`, `..` SHALL pop one node, and `cd` into a path that has children but is not itself resolvable SHALL fail with `node_not_found` leaving cwd unchanged; `exit` (or EOF) SHALL quit the shell. In the shell, the optional-path grammar of `set`, `get` and `show` SHALL default to the cwd node, so the workflow `cd` into a node, `ls` its contents, `show foo` for full property info, `set foo XXX` (or `set foo 2 XXX` for an array index) works without repeating paths. Server errors in the shell SHALL print the readable error name and return to the prompt (the shell SHALL NOT exit on a failed command). #### Scenario: cd, ls, show, set workflow - **WHEN** in the shell the user runs `cd /window`, then `cd content`, then `ls`, then `show is_visible`, then `set is_visible false` - **THEN** `ls` lists the content node's children and properties, `show` prints the `is_visible` metadata and current value, `set` reports success, the app redraws, and a subsequent `get is_visible` prints `false` #### Scenario: shell survives errors - **WHEN** a shell command fails (e.g. `get no_such_prop`) - **THEN** the error name is printed and the prompt returns; the shell is still usable and cwd is unchanged ### Requirement: Shell tab completion The interactive shell SHALL provide tab completion (stdlib `readline`): completing the first word yields command names; completing a later word that looks like a path yields child node names of the referenced parent, fetched live from the running app (so newly created `mknode` nodes complete after creation); completing the first positional argument of `get`, `set` and `show` yields the cwd node's property names together with its child node paths (both are valid leading arguments under the optional-path grammar). Completion SHALL NOT print duplicates, and completing a partial token SHALL offer all matches when ambiguous. If `readline` is unavailable the shell SHALL still work without completion. #### Scenario: complete a node path - **WHEN** the user types `cd /win` and presses Tab - **THEN** the token completes to `/window/` #### Scenario: complete a property name - **WHEN** the cwd is a node with an `is_visible` property and the user types `set is_v` and presses Tab - **THEN** the token completes to `is_visible ` #### Scenario: complete a freshly created node - **WHEN** `mknode /window/content debug_layer layer` was run and the user types `cd /window/content/debug` and presses Tab - **THEN** the token completes to `/window/content/debug_layer/`