@mcowger/plexus-cli 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/package.json +14 -2
- package/skills/plexus-cli/SKILL.md +192 -0
- package/src/cli.ts +28 -3
- package/src/skill.ts +1 -0
- package/src/__tests__/cli.test.ts +0 -177
- package/test/integration/cli.e2e.test.ts +0 -112
- package/tsconfig.json +0 -7
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Plexus CLI
|
|
2
|
+
|
|
3
|
+
`@mcowger/plexus-cli` is a Bun-powered command-line client for the Plexus
|
|
4
|
+
management API. It discovers the target server's OpenAPI document at runtime,
|
|
5
|
+
so documented management operations are available without a CLI update.
|
|
6
|
+
|
|
7
|
+
## Install or run
|
|
8
|
+
|
|
9
|
+
Run once with Bun:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bunx @mcowger/plexus-cli api list
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install the `plexuscli` command globally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun install -g @mcowger/plexus-cli
|
|
19
|
+
plexuscli --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Connect to Plexus
|
|
23
|
+
|
|
24
|
+
Set the target URL and management key. The local Plexus dev stack defaults to
|
|
25
|
+
`password`; use a real admin key for non-local instances.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export PLEXUS_URL=http://localhost:4000
|
|
29
|
+
export PLEXUS_ADMIN_KEY=password
|
|
30
|
+
|
|
31
|
+
plexuscli api call getV0ManagementAuthVerify
|
|
32
|
+
plexuscli api list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `--url` and `--admin-key` to override the environment for one invocation.
|
|
36
|
+
|
|
37
|
+
## Agent guidance
|
|
38
|
+
|
|
39
|
+
Print the packaged skill for complete operational and safety guidance:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
plexuscli skill > SKILL.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The skill covers discovery, JSON request bodies, paginated reads, debug traces,
|
|
46
|
+
errors, system logs, and runtime logging/debug configuration.
|
package/package.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcowger/plexus-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Dynamic command line client for Plexus management APIs",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/mcowger/plexus"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"bin": {
|
|
7
|
-
"plexuscli": "
|
|
11
|
+
"plexuscli": "src/index.ts"
|
|
8
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"README.md",
|
|
15
|
+
"skills",
|
|
16
|
+
"src/cli.ts",
|
|
17
|
+
"src/index.ts",
|
|
18
|
+
"src/skill.ts"
|
|
19
|
+
],
|
|
9
20
|
"scripts": {
|
|
10
21
|
"start": "bun run src/index.ts",
|
|
22
|
+
"prepack": "bun run --cwd ../.. sync:cli-skill",
|
|
11
23
|
"typecheck": "bun x tsc --noEmit",
|
|
12
24
|
"test": "bunx --bun vitest run"
|
|
13
25
|
},
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plexus-cli
|
|
3
|
+
description: >-
|
|
4
|
+
Use this skill to inspect or administer a running Plexus instance through
|
|
5
|
+
plexuscli. Use it before plexus-rest-api whenever `bun run plexuscli` or
|
|
6
|
+
`plexuscli` is available; it is the preferred interface for live
|
|
7
|
+
request/debug-trace/error/system-log review and runtime configuration.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Plexus Management CLI
|
|
11
|
+
|
|
12
|
+
Use `plexuscli` to call the Management API. The CLI discovers the running
|
|
13
|
+
server's public OpenAPI document on each invocation, so use `api list` and
|
|
14
|
+
`api describe` when an operation ID is uncertain.
|
|
15
|
+
|
|
16
|
+
## Preference
|
|
17
|
+
|
|
18
|
+
Use this skill whenever either `bun run plexuscli --help` or
|
|
19
|
+
`plexuscli --help` succeeds. Use `plexus-rest-api` only when neither command
|
|
20
|
+
is available or when this CLI intentionally does not support the required API
|
|
21
|
+
operation, such as SSE streaming.
|
|
22
|
+
|
|
23
|
+
## Local Dev Setup
|
|
24
|
+
|
|
25
|
+
Get the active worktree server port; do not derive it manually:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
PORT=$(bun run dev:get:port)
|
|
29
|
+
export PLEXUS_URL="http://localhost:$PORT"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The standard local dev stack uses `password` as its admin key. This is for
|
|
33
|
+
local development only; never assume it for staging or production.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export PLEXUS_ADMIN_KEY=password
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run the checkout-local CLI:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
PLEXUSCLI='bun run plexuscli'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
After installation, use `PLEXUSCLI=plexuscli` instead. Verify access before
|
|
46
|
+
making a change:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
$PLEXUSCLI api call getV0ManagementAuthVerify
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For a remote instance, require `PLEXUS_URL` and `PLEXUS_ADMIN_KEY` from the
|
|
53
|
+
user. Never print a supplied key. The CLI does not redact API responses, so
|
|
54
|
+
avoid broad key/config reads unless the user requested them.
|
|
55
|
+
|
|
56
|
+
## CLI Conventions
|
|
57
|
+
|
|
58
|
+
- `api list` prints the available supported management operations.
|
|
59
|
+
- `api describe <operationId>` shows parameters and body requirements.
|
|
60
|
+
- `api call <operationId>` invokes an operation. An operation ID alone is a
|
|
61
|
+
shorthand for `api call`.
|
|
62
|
+
- Use `--param name=value` for path/query parameters. JSON literals are
|
|
63
|
+
coerced, so `--param limit=50` is numeric and
|
|
64
|
+
`--param fields='["requestId"]'` is an array.
|
|
65
|
+
- Use `--body '{...}'` or `--body-file payload.json` for writes.
|
|
66
|
+
- Use `--output json|yaml|table`. Non-interactive output defaults to JSON;
|
|
67
|
+
interactive output defaults to a deterministic table.
|
|
68
|
+
- Use `--all` only for standardized paginated list operations.
|
|
69
|
+
- `DELETE` and operations named delete, restore, restart, reset, clear,
|
|
70
|
+
rotate, or disable prompt for confirmation. Use `--yes` only when the user
|
|
71
|
+
explicitly requested that action.
|
|
72
|
+
- The CLI deliberately hides SSE operations, including live system-log
|
|
73
|
+
streaming. Use the recent-log endpoint instead.
|
|
74
|
+
|
|
75
|
+
## Common Read Workflows
|
|
76
|
+
|
|
77
|
+
### Inspect Available Operations
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
$PLEXUSCLI api list --output table
|
|
81
|
+
$PLEXUSCLI api describe getV0ManagementDebugLogs --output yaml
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Review Debug Trace Configuration
|
|
85
|
+
|
|
86
|
+
Debug capture configuration is in memory and resets when Plexus restarts.
|
|
87
|
+
Capture is inclusive: global capture, key targets, alias targets, and provider
|
|
88
|
+
targets each independently cause a trace to be captured.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
$PLEXUSCLI api call getV0ManagementDebug
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Review Recent Debug Traces
|
|
95
|
+
|
|
96
|
+
List newest traces, then request the full trace by ID:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
$PLEXUSCLI api call getV0ManagementDebugLogs --param limit=50
|
|
100
|
+
|
|
101
|
+
$PLEXUSCLI api call getV0ManagementDebugLogsByrequestId \
|
|
102
|
+
--param requestId='<request-id>' \
|
|
103
|
+
--output json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Trace payloads and headers can contain customer data or secrets. Report only
|
|
107
|
+
the specific redacted fields needed to diagnose the issue.
|
|
108
|
+
|
|
109
|
+
### Review Inference Errors
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
$PLEXUSCLI api call getV0ManagementErrors --param limit=50
|
|
113
|
+
|
|
114
|
+
# Retrieve every documented error page only when the user needs the full set.
|
|
115
|
+
$PLEXUSCLI api call getV0ManagementErrors --all --param limit=100
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Review Recent System Logs
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
$PLEXUSCLI api call getV0SystemLogsRecent --param limit=100
|
|
122
|
+
|
|
123
|
+
$PLEXUSCLI api call getV0SystemLogsRecent --all --param limit=100
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The recent-log endpoint is bounded in memory. It is not a persistent audit
|
|
127
|
+
log, and `--all` can only retrieve entries the server still retains.
|
|
128
|
+
|
|
129
|
+
## Runtime Configuration Workflows
|
|
130
|
+
|
|
131
|
+
Inspect the current state before changing it. State the intended change to the
|
|
132
|
+
user, then send the smallest PATCH/PUT body that satisfies the API.
|
|
133
|
+
|
|
134
|
+
### Adjust Debug Trace Capture
|
|
135
|
+
|
|
136
|
+
Enable temporary global capture:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
$PLEXUSCLI api call patchV0ManagementDebug --body '{"enabled":true}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Capture only selected request dimensions while global capture stays off:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
$PLEXUSCLI api call patchV0ManagementDebug \
|
|
146
|
+
--body '{"enabled":false,"keys":["mobile-app"],"aliases":["support"],"providers":["openai"]}'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Clear provider targeting and disable global capture after the investigation:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
$PLEXUSCLI api call patchV0ManagementDebug \
|
|
153
|
+
--body '{"enabled":false,"providers":null}'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`keys`, `aliases`, and `providers` replace their corresponding target lists;
|
|
157
|
+
send `null` or `[]` to clear one. Do not leave broad debug capture enabled
|
|
158
|
+
longer than required.
|
|
159
|
+
|
|
160
|
+
### Adjust Runtime Log Verbosity
|
|
161
|
+
|
|
162
|
+
Read the current and startup log levels:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
$PLEXUSCLI api call getV0ManagementLoggingLevel
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Increase temporary detail:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
$PLEXUSCLI api call putV0ManagementLoggingLevel --body '{"level":"debug"}'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Valid levels, least to most verbose: `error`, `warn`, `info`, `debug`,
|
|
175
|
+
`verbose`, `silly`. The setting is runtime-only and reverts on restart.
|
|
176
|
+
|
|
177
|
+
Reset it to the startup default only when explicitly requested:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
$PLEXUSCLI api call deleteV0ManagementLoggingLevel --yes
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Failure Handling
|
|
184
|
+
|
|
185
|
+
- If spec discovery fails, confirm the URL with `bun run dev:get:port` locally
|
|
186
|
+
or ask for the remote base URL.
|
|
187
|
+
- If an operation ID is unavailable, run `api list`; do not guess a path or
|
|
188
|
+
substitute raw HTTP requests.
|
|
189
|
+
- On HTTP validation errors, report the exact response and stop rather than
|
|
190
|
+
guessing a new payload.
|
|
191
|
+
- For a 401/403, verify the correct `PLEXUS_ADMIN_KEY` was supplied and do not
|
|
192
|
+
retry with unrelated credentials.
|
package/src/cli.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { plexusCliSkill } from './skill';
|
|
2
|
+
|
|
1
3
|
export const DEFAULT_URL = 'http://localhost:4000';
|
|
2
4
|
const OPENAPI_PATH = '/.well-known/plexus/openapi.json';
|
|
3
5
|
const ALLOWED_PATH = /^\/v0\/(management\/|system\/logs\/)/;
|
|
@@ -115,15 +117,24 @@ export function discoverOperations(document: {
|
|
|
115
117
|
const candidates: Operation[] = [];
|
|
116
118
|
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
|
117
119
|
if (!ALLOWED_PATH.test(path)) continue;
|
|
120
|
+
const pathParameters = Array.isArray(pathItem.parameters)
|
|
121
|
+
? (pathItem.parameters as OpenApiParameter[])
|
|
122
|
+
: [];
|
|
118
123
|
for (const [method, value] of Object.entries(pathItem)) {
|
|
119
124
|
if (!HTTP_METHODS.has(method) || !value || typeof value !== 'object') continue;
|
|
120
125
|
const operation = value as OpenApiOperation;
|
|
121
126
|
if (isStreamOperation(operation)) continue;
|
|
127
|
+
const parameters = new Map(
|
|
128
|
+
pathParameters.map((parameter) => [`${parameter.in}:${parameter.name}`, parameter])
|
|
129
|
+
);
|
|
130
|
+
for (const parameter of operation.parameters ?? []) {
|
|
131
|
+
parameters.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
132
|
+
}
|
|
122
133
|
candidates.push({
|
|
123
134
|
id: operation.operationId ?? fallbackId(method, path, operation.tags),
|
|
124
135
|
method,
|
|
125
136
|
path,
|
|
126
|
-
operation,
|
|
137
|
+
operation: { ...operation, parameters: [...parameters.values()] },
|
|
127
138
|
});
|
|
128
139
|
}
|
|
129
140
|
}
|
|
@@ -278,7 +289,7 @@ async function fetchAllPages(
|
|
|
278
289
|
limit ??= page.data.length;
|
|
279
290
|
if (!limit || page.data.length === 0) break;
|
|
280
291
|
allData.push(...page.data);
|
|
281
|
-
offset +=
|
|
292
|
+
offset += page.data.length;
|
|
282
293
|
}
|
|
283
294
|
|
|
284
295
|
return { data: allData, total: total ?? allData.length, limit: limit ?? 0, offset: 0 };
|
|
@@ -290,7 +301,15 @@ export function formatOutput(value: unknown, output: 'json' | 'yaml' | 'table'):
|
|
|
290
301
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
291
302
|
}
|
|
292
303
|
|
|
293
|
-
const HELP = `Usage: plexuscli [options]
|
|
304
|
+
const HELP = `Usage: plexuscli [options] <skill|api> [operation]
|
|
305
|
+
|
|
306
|
+
AGENTS: Run \`plexuscli skill\` to print the \`plexus-cli\` skill and understand how to use this tool.
|
|
307
|
+
|
|
308
|
+
Commands:
|
|
309
|
+
skill Print the bundled plexus-cli SKILL.md
|
|
310
|
+
api list List discovered management operations
|
|
311
|
+
api describe OPERATION Describe a discovered operation
|
|
312
|
+
api call OPERATION Call a discovered operation
|
|
294
313
|
|
|
295
314
|
Options:
|
|
296
315
|
--url URL Plexus URL (default: PLEXUS_URL or ${DEFAULT_URL})
|
|
@@ -325,6 +344,10 @@ export async function run(
|
|
|
325
344
|
runtime.stdout(HELP);
|
|
326
345
|
return 0;
|
|
327
346
|
}
|
|
347
|
+
if (args.positionals.length === 1 && args.positionals[0] === 'skill') {
|
|
348
|
+
runtime.stdout(plexusCliSkill);
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
328
351
|
const baseUrl = args.url.replace(/\/$/, '');
|
|
329
352
|
const specResponse = await runtime.fetch(`${baseUrl}${OPENAPI_PATH}`, { cache: 'no-store' });
|
|
330
353
|
if (!specResponse.ok)
|
|
@@ -391,6 +414,8 @@ export async function run(
|
|
|
391
414
|
if (args.adminKey) request.headers.set('x-admin-key', args.adminKey);
|
|
392
415
|
if (body) request.headers.set('content-type', 'application/json');
|
|
393
416
|
if (args.all) {
|
|
417
|
+
if (operation.method !== 'get')
|
|
418
|
+
throw new CliError('--all can only be used with GET operations');
|
|
394
419
|
if (body) throw new CliError('--all cannot be used with a request body');
|
|
395
420
|
runtime.stdout(
|
|
396
421
|
formatOutput(
|
package/src/skill.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const plexusCliSkill = "---\nname: plexus-cli\ndescription: >-\n Use this skill to inspect or administer a running Plexus instance through\n plexuscli. Use it before plexus-rest-api whenever `bun run plexuscli` or\n `plexuscli` is available; it is the preferred interface for live\n request/debug-trace/error/system-log review and runtime configuration.\n---\n\n# Plexus Management CLI\n\nUse `plexuscli` to call the Management API. The CLI discovers the running\nserver's public OpenAPI document on each invocation, so use `api list` and\n`api describe` when an operation ID is uncertain.\n\n## Preference\n\nUse this skill whenever either `bun run plexuscli --help` or\n`plexuscli --help` succeeds. Use `plexus-rest-api` only when neither command\nis available or when this CLI intentionally does not support the required API\noperation, such as SSE streaming.\n\n## Local Dev Setup\n\nGet the active worktree server port; do not derive it manually:\n\n```bash\nPORT=$(bun run dev:get:port)\nexport PLEXUS_URL=\"http://localhost:$PORT\"\n```\n\nThe standard local dev stack uses `password` as its admin key. This is for\nlocal development only; never assume it for staging or production.\n\n```bash\nexport PLEXUS_ADMIN_KEY=password\n```\n\nRun the checkout-local CLI:\n\n```bash\nPLEXUSCLI='bun run plexuscli'\n```\n\nAfter installation, use `PLEXUSCLI=plexuscli` instead. Verify access before\nmaking a change:\n\n```bash\n$PLEXUSCLI api call getV0ManagementAuthVerify\n```\n\nFor a remote instance, require `PLEXUS_URL` and `PLEXUS_ADMIN_KEY` from the\nuser. Never print a supplied key. The CLI does not redact API responses, so\navoid broad key/config reads unless the user requested them.\n\n## CLI Conventions\n\n- `api list` prints the available supported management operations.\n- `api describe <operationId>` shows parameters and body requirements.\n- `api call <operationId>` invokes an operation. An operation ID alone is a\n shorthand for `api call`.\n- Use `--param name=value` for path/query parameters. JSON literals are\n coerced, so `--param limit=50` is numeric and\n `--param fields='[\"requestId\"]'` is an array.\n- Use `--body '{...}'` or `--body-file payload.json` for writes.\n- Use `--output json|yaml|table`. Non-interactive output defaults to JSON;\n interactive output defaults to a deterministic table.\n- Use `--all` only for standardized paginated list operations.\n- `DELETE` and operations named delete, restore, restart, reset, clear,\n rotate, or disable prompt for confirmation. Use `--yes` only when the user\n explicitly requested that action.\n- The CLI deliberately hides SSE operations, including live system-log\n streaming. Use the recent-log endpoint instead.\n\n## Common Read Workflows\n\n### Inspect Available Operations\n\n```bash\n$PLEXUSCLI api list --output table\n$PLEXUSCLI api describe getV0ManagementDebugLogs --output yaml\n```\n\n### Review Debug Trace Configuration\n\nDebug capture configuration is in memory and resets when Plexus restarts.\nCapture is inclusive: global capture, key targets, alias targets, and provider\ntargets each independently cause a trace to be captured.\n\n```bash\n$PLEXUSCLI api call getV0ManagementDebug\n```\n\n### Review Recent Debug Traces\n\nList newest traces, then request the full trace by ID:\n\n```bash\n$PLEXUSCLI api call getV0ManagementDebugLogs --param limit=50\n\n$PLEXUSCLI api call getV0ManagementDebugLogsByrequestId \\\n --param requestId='<request-id>' \\\n --output json\n```\n\nTrace payloads and headers can contain customer data or secrets. Report only\nthe specific redacted fields needed to diagnose the issue.\n\n### Review Inference Errors\n\n```bash\n$PLEXUSCLI api call getV0ManagementErrors --param limit=50\n\n# Retrieve every documented error page only when the user needs the full set.\n$PLEXUSCLI api call getV0ManagementErrors --all --param limit=100\n```\n\n### Review Recent System Logs\n\n```bash\n$PLEXUSCLI api call getV0SystemLogsRecent --param limit=100\n\n$PLEXUSCLI api call getV0SystemLogsRecent --all --param limit=100\n```\n\nThe recent-log endpoint is bounded in memory. It is not a persistent audit\nlog, and `--all` can only retrieve entries the server still retains.\n\n## Runtime Configuration Workflows\n\nInspect the current state before changing it. State the intended change to the\nuser, then send the smallest PATCH/PUT body that satisfies the API.\n\n### Adjust Debug Trace Capture\n\nEnable temporary global capture:\n\n```bash\n$PLEXUSCLI api call patchV0ManagementDebug --body '{\"enabled\":true}'\n```\n\nCapture only selected request dimensions while global capture stays off:\n\n```bash\n$PLEXUSCLI api call patchV0ManagementDebug \\\n --body '{\"enabled\":false,\"keys\":[\"mobile-app\"],\"aliases\":[\"support\"],\"providers\":[\"openai\"]}'\n```\n\nClear provider targeting and disable global capture after the investigation:\n\n```bash\n$PLEXUSCLI api call patchV0ManagementDebug \\\n --body '{\"enabled\":false,\"providers\":null}'\n```\n\n`keys`, `aliases`, and `providers` replace their corresponding target lists;\nsend `null` or `[]` to clear one. Do not leave broad debug capture enabled\nlonger than required.\n\n### Adjust Runtime Log Verbosity\n\nRead the current and startup log levels:\n\n```bash\n$PLEXUSCLI api call getV0ManagementLoggingLevel\n```\n\nIncrease temporary detail:\n\n```bash\n$PLEXUSCLI api call putV0ManagementLoggingLevel --body '{\"level\":\"debug\"}'\n```\n\nValid levels, least to most verbose: `error`, `warn`, `info`, `debug`,\n`verbose`, `silly`. The setting is runtime-only and reverts on restart.\n\nReset it to the startup default only when explicitly requested:\n\n```bash\n$PLEXUSCLI api call deleteV0ManagementLoggingLevel --yes\n```\n\n## Failure Handling\n\n- If spec discovery fails, confirm the URL with `bun run dev:get:port` locally\n or ask for the remote base URL.\n- If an operation ID is unavailable, run `api list`; do not guess a path or\n substitute raw HTTP requests.\n- On HTTP validation errors, report the exact response and stop rather than\n guessing a new payload.\n- For a 401/403, verify the correct `PLEXUS_ADMIN_KEY` was supplied and do not\n retry with unrelated credentials.\n";
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
buildRequest,
|
|
4
|
-
discoverOperations,
|
|
5
|
-
formatOutput,
|
|
6
|
-
isRisky,
|
|
7
|
-
parseArgs,
|
|
8
|
-
run,
|
|
9
|
-
type Json,
|
|
10
|
-
type Operation,
|
|
11
|
-
} from '../cli';
|
|
12
|
-
|
|
13
|
-
const document = {
|
|
14
|
-
paths: {
|
|
15
|
-
'/v0/management/keys/{id}': {
|
|
16
|
-
delete: {
|
|
17
|
-
operationId: 'deleteKey',
|
|
18
|
-
parameters: [{ name: 'id', in: 'path', required: true }],
|
|
19
|
-
},
|
|
20
|
-
get: { operationId: 'getKey', tags: ['Keys'] },
|
|
21
|
-
},
|
|
22
|
-
'/v0/system/logs/stream': {
|
|
23
|
-
get: {
|
|
24
|
-
operationId: 'streamLogs',
|
|
25
|
-
responses: { 200: { content: { 'text/event-stream': {} } } },
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
'/v1/models': { get: { operationId: 'listModels' } },
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
describe('dynamic OpenAPI routing', () => {
|
|
33
|
-
it('limits discovery to supported non-stream operations', () => {
|
|
34
|
-
expect(discoverOperations(document).map((operation) => operation.id)).toEqual([
|
|
35
|
-
'deleteKey',
|
|
36
|
-
'getKey',
|
|
37
|
-
]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('builds encoded path and query parameters', () => {
|
|
41
|
-
const operation: Operation = {
|
|
42
|
-
id: 'get',
|
|
43
|
-
method: 'get',
|
|
44
|
-
path: '/v0/management/items/{id}',
|
|
45
|
-
operation: {
|
|
46
|
-
parameters: [
|
|
47
|
-
{ name: 'id', in: 'path', required: true },
|
|
48
|
-
{ name: 'limit', in: 'query' },
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
const request = buildRequest(
|
|
53
|
-
operation,
|
|
54
|
-
new Map([
|
|
55
|
-
['id', 'a/b' as Json],
|
|
56
|
-
['limit', 10 as Json],
|
|
57
|
-
])
|
|
58
|
-
);
|
|
59
|
-
expect(request.url).toBe('/v0/management/items/a%2Fb?limit=10');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('recognizes destructive operations', () => {
|
|
63
|
-
expect(isRisky(discoverOperations(document)[0]!)).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('arguments and output', () => {
|
|
68
|
-
it('uses environment defaults and coerces JSON literal parameters', () => {
|
|
69
|
-
const args = parseArgs(['api', 'call', 'getKey', '--param', 'limit=10'], {
|
|
70
|
-
PLEXUS_URL: 'http://plexus',
|
|
71
|
-
});
|
|
72
|
-
expect(args.url).toBe('http://plexus');
|
|
73
|
-
expect(args.params.get('limit')).toBe(10);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('parses the all-pages flag', () => {
|
|
77
|
-
expect(parseArgs(['api', 'call', 'getKey', '--all'], {}).all).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('formats tables with deterministic columns', () => {
|
|
81
|
-
expect(formatOutput([{ b: 2, a: 'one' }], 'table')).toBe('a b\n--- -\none 2\n');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('execution', () => {
|
|
86
|
-
it('fetches the schema without caching and uses JSON for non-TTY output', async () => {
|
|
87
|
-
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
88
|
-
let stdout = '';
|
|
89
|
-
const exitCode = await run(
|
|
90
|
-
['api', 'call', 'getKey'],
|
|
91
|
-
{},
|
|
92
|
-
{
|
|
93
|
-
fetch: async (url, init) => {
|
|
94
|
-
requests.push({ url: String(url), init });
|
|
95
|
-
return requests.length === 1
|
|
96
|
-
? new Response(JSON.stringify(document))
|
|
97
|
-
: new Response(JSON.stringify({ value: true }));
|
|
98
|
-
},
|
|
99
|
-
stdin: async () => '',
|
|
100
|
-
stdout: (text) => {
|
|
101
|
-
stdout += text;
|
|
102
|
-
},
|
|
103
|
-
stderr: () => {},
|
|
104
|
-
isTTY: false,
|
|
105
|
-
confirm: async () => false,
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
expect(exitCode).toBe(0);
|
|
109
|
-
expect(requests[0]).toMatchObject({
|
|
110
|
-
url: 'http://localhost:4000/.well-known/plexus/openapi.json',
|
|
111
|
-
init: { cache: 'no-store' },
|
|
112
|
-
});
|
|
113
|
-
expect(stdout).toBe('{\n "value": true\n}\n');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('sends the management credential with x-admin-key', async () => {
|
|
117
|
-
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
118
|
-
const exitCode = await run(
|
|
119
|
-
['api', 'call', 'getKey', '--admin-key', 'secret'],
|
|
120
|
-
{},
|
|
121
|
-
{
|
|
122
|
-
fetch: async (url, init) => {
|
|
123
|
-
requests.push({ url: String(url), init });
|
|
124
|
-
return requests.length === 1
|
|
125
|
-
? new Response(JSON.stringify(document))
|
|
126
|
-
: new Response(JSON.stringify({ value: true }));
|
|
127
|
-
},
|
|
128
|
-
stdin: async () => '',
|
|
129
|
-
stdout: () => {},
|
|
130
|
-
stderr: () => {},
|
|
131
|
-
isTTY: false,
|
|
132
|
-
confirm: async () => false,
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
expect(exitCode).toBe(0);
|
|
136
|
-
expect(new Headers(requests[1]?.init?.headers).get('x-admin-key')).toBe('secret');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('retrieves all standardized pages', async () => {
|
|
140
|
-
const paginatedDocument = {
|
|
141
|
-
paths: {
|
|
142
|
-
'/v0/management/items': {
|
|
143
|
-
get: {
|
|
144
|
-
operationId: 'listItems',
|
|
145
|
-
parameters: [
|
|
146
|
-
{ name: 'limit', in: 'query' },
|
|
147
|
-
{ name: 'offset', in: 'query' },
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
let stdout = '';
|
|
154
|
-
const exitCode = await run(
|
|
155
|
-
['api', 'call', 'listItems', '--all', '--param', 'limit=2'],
|
|
156
|
-
{},
|
|
157
|
-
{
|
|
158
|
-
fetch: async (url) => {
|
|
159
|
-
if (String(url).includes('openapi.json'))
|
|
160
|
-
return new Response(JSON.stringify(paginatedDocument));
|
|
161
|
-
return String(url).includes('offset=2')
|
|
162
|
-
? new Response(JSON.stringify({ data: [3], total: 3, limit: 2, offset: 2 }))
|
|
163
|
-
: new Response(JSON.stringify({ data: [1, 2], total: 3, limit: 2, offset: 0 }));
|
|
164
|
-
},
|
|
165
|
-
stdin: async () => '',
|
|
166
|
-
stdout: (text) => {
|
|
167
|
-
stdout += text;
|
|
168
|
-
},
|
|
169
|
-
stderr: () => {},
|
|
170
|
-
isTTY: false,
|
|
171
|
-
confirm: async () => true,
|
|
172
|
-
}
|
|
173
|
-
);
|
|
174
|
-
expect(exitCode).toBe(0);
|
|
175
|
-
expect(JSON.parse(stdout)).toMatchObject({ data: [1, 2, 3], total: 3 });
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { rm } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
|
|
5
|
-
|
|
6
|
-
const openApiDocument = {
|
|
7
|
-
openapi: '3.1.0',
|
|
8
|
-
paths: {
|
|
9
|
-
'/v0/management/usage': {
|
|
10
|
-
get: {
|
|
11
|
-
operationId: 'getV0ManagementUsage',
|
|
12
|
-
parameters: [
|
|
13
|
-
{ name: 'limit', in: 'query' },
|
|
14
|
-
{ name: 'offset', in: 'query' },
|
|
15
|
-
],
|
|
16
|
-
responses: {
|
|
17
|
-
200: {
|
|
18
|
-
content: {
|
|
19
|
-
'application/json': {},
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
describe('plexuscli end-to-end', () => {
|
|
29
|
-
let server: ReturnType<typeof Bun.serve> | undefined;
|
|
30
|
-
const buildDir = join(tmpdir(), `plexuscli-e2e-${crypto.randomUUID()}`);
|
|
31
|
-
let cliPath: string;
|
|
32
|
-
|
|
33
|
-
beforeAll(async () => {
|
|
34
|
-
const result = await Bun.build({
|
|
35
|
-
entrypoints: [join(process.cwd(), 'src', 'index.ts')],
|
|
36
|
-
outdir: buildDir,
|
|
37
|
-
compile: true,
|
|
38
|
-
target: 'bun',
|
|
39
|
-
});
|
|
40
|
-
if (!result.success) throw new Error(result.logs.map((log) => log.message).join('\n'));
|
|
41
|
-
cliPath = result.outputs[0]!.path;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
server?.stop(true);
|
|
46
|
-
server = undefined;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
afterAll(async () => {
|
|
50
|
-
await rm(buildDir, { force: true, recursive: true });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('discovers and calls a paginated management operation', async () => {
|
|
54
|
-
const adminKeys: string[] = [];
|
|
55
|
-
server = Bun.serve({
|
|
56
|
-
port: 0,
|
|
57
|
-
fetch(request) {
|
|
58
|
-
const url = new URL(request.url);
|
|
59
|
-
if (url.pathname === '/.well-known/plexus/openapi.json') {
|
|
60
|
-
return Response.json(openApiDocument);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (url.pathname === '/v0/management/usage') {
|
|
64
|
-
adminKeys.push(request.headers.get('x-admin-key') ?? '');
|
|
65
|
-
const offset = Number(url.searchParams.get('offset') ?? '0');
|
|
66
|
-
return Response.json(
|
|
67
|
-
offset === 0
|
|
68
|
-
? { data: [{ requestId: 'one' }], total: 2, limit: 1, offset }
|
|
69
|
-
: { data: [{ requestId: 'two' }], total: 2, limit: 1, offset }
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return new Response('Not Found', { status: 404 });
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const child = Bun.spawn(
|
|
78
|
-
[
|
|
79
|
-
cliPath,
|
|
80
|
-
'--url',
|
|
81
|
-
`http://localhost:${server.port}`,
|
|
82
|
-
'--admin-key',
|
|
83
|
-
'e2e-admin-key',
|
|
84
|
-
'api',
|
|
85
|
-
'call',
|
|
86
|
-
'getV0ManagementUsage',
|
|
87
|
-
'--all',
|
|
88
|
-
'--param',
|
|
89
|
-
'limit=1',
|
|
90
|
-
'--output',
|
|
91
|
-
'json',
|
|
92
|
-
],
|
|
93
|
-
{
|
|
94
|
-
stdout: 'pipe',
|
|
95
|
-
stderr: 'pipe',
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
99
|
-
new Response(child.stdout).text(),
|
|
100
|
-
new Response(child.stderr).text(),
|
|
101
|
-
child.exited,
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
expect(exitCode).toBe(0);
|
|
105
|
-
expect(stderr).toBe('');
|
|
106
|
-
expect(JSON.parse(stdout)).toMatchObject({
|
|
107
|
-
data: [{ requestId: 'one' }, { requestId: 'two' }],
|
|
108
|
-
total: 2,
|
|
109
|
-
});
|
|
110
|
-
expect(adminKeys).toEqual(['e2e-admin-key', 'e2e-admin-key']);
|
|
111
|
-
});
|
|
112
|
-
});
|