@gxp-dev/tools 2.0.71 → 2.0.73
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 +108 -81
- package/bin/lib/cli.js +18 -0
- package/bin/lib/commands/index.js +2 -0
- package/bin/lib/commands/init.js +104 -82
- package/bin/lib/commands/lint.js +77 -0
- package/bin/lib/constants.js +12 -0
- package/bin/lib/lint/formatter.js +91 -0
- package/bin/lib/lint/index.js +284 -0
- package/bin/lib/lint/schemas/app-manifest.schema.json +124 -0
- package/bin/lib/lint/schemas/card.schema.json +165 -0
- package/bin/lib/lint/schemas/common.schema.json +62 -0
- package/bin/lib/lint/schemas/configuration.schema.json +19 -0
- package/bin/lib/lint/schemas/field.schema.json +230 -0
- package/bin/lib/utils/ai-scaffold.js +137 -0
- package/mcp/gxp-api-server.js +87 -129
- package/mcp/lib/api-tools.js +543 -0
- package/mcp/lib/config-ops.js +234 -0
- package/mcp/lib/config-tools.js +549 -0
- package/mcp/lib/docs-tools.js +142 -0
- package/mcp/lib/docs.js +263 -0
- package/mcp/lib/specs.js +135 -0
- package/mcp/lib/test-tools.js +358 -0
- package/package.json +3 -1
- package/runtime/stores/gxpPortalConfigStore.js +88 -87
- package/runtime/vite.config.js +5 -3
- package/template/.claude/agents/gxp-developer.md +377 -50
- package/template/.prettierrc +10 -0
- package/template/AGENTS.md +265 -21
- package/template/GEMINI.md +181 -19
- package/template/README.md +205 -240
- package/template/app-instructions.md +91 -0
- package/template/eslint.config.js +32 -0
- package/template/githooks/pre-commit +37 -0
package/template/AGENTS.md
CHANGED
|
@@ -2,6 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
This is a GxP plugin project for the GxP kiosk platform. Follow these guidelines when working with this codebase.
|
|
4
4
|
|
|
5
|
+
## Development Workflow
|
|
6
|
+
|
|
7
|
+
Every task starts with understanding and ends with a validated, linted build. Do not skip steps.
|
|
8
|
+
|
|
9
|
+
### 1. Understand what the client is building
|
|
10
|
+
|
|
11
|
+
Before touching code, get specific about the feature:
|
|
12
|
+
|
|
13
|
+
- What is the user-facing outcome? Who uses it (attendee, staff, admin)?
|
|
14
|
+
- Which real-world data does it read or write?
|
|
15
|
+
- What events does it react to in real time?
|
|
16
|
+
- What needs to be configurable by an admin (text, assets, colors, thresholds, feature flags)?
|
|
17
|
+
|
|
18
|
+
Ask clarifying questions instead of guessing. A 30-second question beats a 30-minute rewrite.
|
|
19
|
+
|
|
20
|
+
### 2. Discover the right data sources via MCP
|
|
21
|
+
|
|
22
|
+
Use the `gxp-api` MCP server to ground the implementation in the real platform — never invent endpoints or event names.
|
|
23
|
+
|
|
24
|
+
- **Find endpoints** — `api_list_tags`, `api_list_operation_ids` (optionally scoped by tag), `search_api_endpoints` (keyword).
|
|
25
|
+
- **Inspect a specific endpoint** — `api_get_operation_parameters`, `get_endpoint_details`.
|
|
26
|
+
- **Find endpoints by payload shape** — `api_find_endpoints_by_schema` (search by request/response field names).
|
|
27
|
+
- **Find WebSocket events** — `api_find_events_for_operation` (given an operationId, returns events via `x-triggered-by`), `api_list_events`, `search_websocket_events`, `get_asyncapi_spec`. Run `api_find_events_for_operation` for every operationId you plan to call so you subscribe instead of polling.
|
|
28
|
+
- **Generate dependency JSON** — `api_generate_dependency` produces the canonical entry for `app-manifest.json` → `dependencies`.
|
|
29
|
+
- **Read the docs** — `docs_list_pages`, `docs_search`, `docs_get_page` (full-text search across docs.gxp.dev).
|
|
30
|
+
|
|
31
|
+
Output of this step: a short list of the operationIds, WebSocket events, and dependencies the plugin will use.
|
|
32
|
+
|
|
33
|
+
### 3. Plan with the client, including the admin config form
|
|
34
|
+
|
|
35
|
+
Before writing code, produce a plan and confirm it with the client. The plan must include:
|
|
36
|
+
|
|
37
|
+
- **Screens/components** — what's rendered and in which layout (Public/Private/System).
|
|
38
|
+
- **Data flow** — which store getters read which sections, which API calls populate them, which socket events mutate state.
|
|
39
|
+
- **Admin configuration form** — every piece of customizable content belongs in `configuration.json` as a card/field so admins can edit it without a code change. Cover text (strings), images (assets), colors/thresholds (settings), and feature toggles.
|
|
40
|
+
- **Strings and assets inventory** — the exact keys that will appear in `app-manifest.json`.
|
|
41
|
+
|
|
42
|
+
Do not proceed to implementation until the client has reviewed the plan.
|
|
43
|
+
|
|
44
|
+
### 4. Implement
|
|
45
|
+
|
|
46
|
+
Build against the plan:
|
|
47
|
+
|
|
48
|
+
- Use the GxP store for **all** data access — `store.callApi(operationId, identifier, data)` for platform API calls, plus sockets, strings, assets, settings, state. Never use `axios` or `fetch` directly.
|
|
49
|
+
- Declare every permission identifier used by `callApi` in `app-manifest.json` → `dependencies` + `permissions`. Use `"project"` for project-wide / top-level operations and pass any required IDs in `data`.
|
|
50
|
+
- Wire every piece of user-facing text with `gxp-string` and every image with `gxp-src` so the admin form controls them.
|
|
51
|
+
- Keep components under `src/`. The runtime container (layouts, routing, dev tools) is not yours to edit.
|
|
52
|
+
|
|
53
|
+
### 5. Sync the manifest and build the admin form
|
|
54
|
+
|
|
55
|
+
As soon as you've added a new `store.callApi`, `store.listen`, `gxp-string`, or `gxp-src` — and before you move on to testing — close the loop on the admin-editable surface:
|
|
56
|
+
|
|
57
|
+
1. **Extract the configurable surface into `app-manifest.json`.** Run the MCP tool `config_extract_strings` with `writeTo: "app-manifest.json"`. It scans `src/` for every `gxp-string`, `gxp-src`, `store.getString/getSetting/getAsset/getState`, and `store.callApi` identifier and merges the new keys into the manifest (linter-guarded; falls back to `gxdev extract-config` on the CLI if you need it). Do this every time you touch the plugin's public surface so the manifest never drifts from the code.
|
|
58
|
+
|
|
59
|
+
2. **Build `configuration.json` from what's in the manifest.** For every entry in the manifest, add a matching field in the admin form using the MCP `config_*` tools — they validate each write against the schema. Default mappings:
|
|
60
|
+
|
|
61
|
+
| Manifest entry | `configuration.json` field type |
|
|
62
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
|
63
|
+
| `strings.default.*` | `text` (or `textarea` for long copy) |
|
|
64
|
+
| `assets.*` (driven by `gxp-src`) | `selectAsset` |
|
|
65
|
+
| `dependencies[]` — each identifier | `asyncSelect` wired to the matching resource list endpoint (so the admin binds to a real record) |
|
|
66
|
+
| `settings.*` that represent colors | `colorPicker` |
|
|
67
|
+
| `settings.*` that represent thresholds / numbers | `number` |
|
|
68
|
+
| `settings.*` that represent feature toggles | `boolean` |
|
|
69
|
+
| Anything else discussed with the client | Pick with `config_list_field_types` / `config_get_field_schema` |
|
|
70
|
+
|
|
71
|
+
Group related fields into `fields_list` cards (`config_add_card` + `config_add_field`). If you're unsure what field types exist, call `config_list_field_types` / `config_list_card_types` first. If a mutation is refused, read the validation error and fix the input — do not set `force: true`.
|
|
72
|
+
|
|
73
|
+
3. **Lint.** Run `gxdev lint --all` and fix every error before moving on.
|
|
74
|
+
|
|
75
|
+
### 6. Test with real broadcasts
|
|
76
|
+
|
|
77
|
+
Before declaring done, exercise the plugin against real event shapes:
|
|
78
|
+
|
|
79
|
+
- List available events — `gxdev socket list`.
|
|
80
|
+
- Send one — `gxdev socket send --event <EventName>` (edit or add payloads under `socket-events/`).
|
|
81
|
+
- Hit endpoints against the local mock API via the `test_api_route` MCP tool.
|
|
82
|
+
- Scaffold unit tests with `test_scaffold_component_test` for any non-trivial component.
|
|
83
|
+
|
|
84
|
+
### 7. Final lint
|
|
85
|
+
|
|
86
|
+
Step 5 already runs `gxdev lint --all`, but run it one more time after Step 6's test pass — test changes (adding mock payloads, tweaking identifiers) can re-introduce schema drift.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
gxdev lint # validate default targets
|
|
90
|
+
gxdev lint --all # validate everything
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Fix every error. Do not mark work complete with a failing lint.
|
|
94
|
+
|
|
5
95
|
## Project Architecture
|
|
6
96
|
|
|
7
97
|
This plugin runs inside a container environment provided by the `gxdev` development server. You should ONLY modify files in the `src/` directory. The runtime container (PortalContainer.vue) handles layouts, dev tools, routing, and store initialization.
|
|
@@ -17,30 +107,102 @@ src/
|
|
|
17
107
|
|
|
18
108
|
## Core Principle: Use the GxP Store for Everything
|
|
19
109
|
|
|
20
|
-
The `gxpPortalConfigStore` is the central hub. Import it in any component:
|
|
110
|
+
The `gxpPortalConfigStore` is the central hub for every piece of data — API, sockets, strings, assets, settings, state. Import it in any component:
|
|
21
111
|
|
|
22
112
|
```javascript
|
|
23
113
|
import { useGxpStore } from "@gx-runtime/stores/gxpPortalConfigStore"
|
|
24
114
|
const store = useGxpStore()
|
|
25
115
|
```
|
|
26
116
|
|
|
27
|
-
## API Calls
|
|
117
|
+
## API Calls — Use `store.callApi` with an Operation ID + Permission Identifier
|
|
118
|
+
|
|
119
|
+
**Every call to the GxP platform goes through `store.callApi`.** It handles authorization, URL resolution, team/project scoping, and path-parameter substitution. You only provide three things:
|
|
28
120
|
|
|
29
|
-
|
|
121
|
+
```javascript
|
|
122
|
+
await store.callApi(operationId, identifier, data)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Argument | Purpose |
|
|
126
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `operationId` | The OpenAPI operationId for the call. Look it up with `api_list_operation_ids` or `search_api_endpoints` (MCP). Never invent one. |
|
|
128
|
+
| `identifier` | A **permission identifier** declared in `app-manifest.json` → `dependencies` / `permissions`. Determines which resource's permissions the call runs under, and supplies the parent object ID that gets substituted into the path. |
|
|
129
|
+
| `data` | Additional params — body fields, query params, or path params not covered by the identifier. Pass `"pluginVars.someKey"` as a value to pull from settings at call time. |
|
|
130
|
+
|
|
131
|
+
NEVER use axios/fetch directly. `apiGet`/`apiPost`/`apiPut`/`apiPatch`/`apiDelete` still exist as low-level escape hatches, but `callApi` is the default — only it participates in the permission model.
|
|
132
|
+
|
|
133
|
+
### Permission identifiers
|
|
134
|
+
|
|
135
|
+
Each identifier is declared in `app-manifest.json` and later bound by an admin to a specific resource + permission scope (read, create, update, delete). Pick identifiers that describe the **role the resource plays in the plugin**, not its name.
|
|
136
|
+
|
|
137
|
+
Example — a plugin that reads posts from one social stream and creates posts on another:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
// app-manifest.json
|
|
141
|
+
{
|
|
142
|
+
"dependencies": [
|
|
143
|
+
{ "identifier": "social_stream_one", "model": "SocialStream" },
|
|
144
|
+
{ "identifier": "social_stream_two", "model": "SocialStream" }
|
|
145
|
+
],
|
|
146
|
+
"permissions": [
|
|
147
|
+
{
|
|
148
|
+
"identifier": "social_stream_one",
|
|
149
|
+
"description": "Source stream — read posts"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"identifier": "social_stream_two",
|
|
153
|
+
"description": "Destination stream — create posts"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
30
158
|
|
|
31
159
|
```javascript
|
|
32
|
-
//
|
|
33
|
-
const
|
|
160
|
+
// Read posts from the source stream
|
|
161
|
+
const posts = await store.callApi("posts.index", "social_stream_one")
|
|
162
|
+
|
|
163
|
+
// Create a post on the destination stream
|
|
164
|
+
await store.callApi("posts.store", "social_stream_two", {
|
|
165
|
+
body: "Hello world",
|
|
166
|
+
image_url: imageUrl,
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The admin assigns `social_stream_one` read-only access to stream A and `social_stream_two` create access to stream B. The plugin code never hard-codes IDs.
|
|
171
|
+
|
|
172
|
+
### The `project` identifier — project-wide, top-level operations
|
|
173
|
+
|
|
174
|
+
When you need to create the parent resource itself (e.g. the social stream), or hit any endpoint scoped at the project level rather than to a specific dependency, use the reserved identifier `"project"`. `callApi` will run with project-wide permissions — but you must pass any path params the endpoint needs in `data`.
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Create a new social stream under the project
|
|
178
|
+
const stream = await store.callApi("social_streams.store", "project", {
|
|
179
|
+
name: "New Stream",
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Create a post directly against a known socialStreamId (no dependency binding)
|
|
183
|
+
await store.callApi("posts.store", "project", {
|
|
184
|
+
socialStreamId: stream.id,
|
|
185
|
+
body: "First post",
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Rule of thumb:
|
|
190
|
+
|
|
191
|
+
- Operating on a resource the admin will bind later → declare a dependency identifier and pass it.
|
|
192
|
+
- Creating the parent resource itself, or acting across the whole project → use `"project"` and pass the relevant IDs in `data`.
|
|
193
|
+
|
|
194
|
+
### Low-level methods (avoid unless necessary)
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
await store.apiGet("/api/v1/endpoint", { param: "value" })
|
|
34
198
|
await store.apiPost("/api/v1/endpoint", { data: "value" })
|
|
35
199
|
await store.apiPut("/api/v1/endpoint/123", { data: "value" })
|
|
36
200
|
await store.apiPatch("/api/v1/endpoint/123", { data: "value" })
|
|
37
201
|
await store.apiDelete("/api/v1/endpoint/123")
|
|
38
|
-
|
|
39
|
-
// WRONG - never do this
|
|
40
|
-
// const response = await axios.get(...); // NO!
|
|
41
|
-
// const response = await fetch(...); // NO!
|
|
42
202
|
```
|
|
43
203
|
|
|
204
|
+
These bypass the permission model. Prefer `callApi`.
|
|
205
|
+
|
|
44
206
|
## Store Data Access
|
|
45
207
|
|
|
46
208
|
```javascript
|
|
@@ -58,26 +220,59 @@ store.updateAsset("key", "url")
|
|
|
58
220
|
store.updateState("key", "value")
|
|
59
221
|
```
|
|
60
222
|
|
|
61
|
-
##
|
|
223
|
+
## Real-Time Events
|
|
224
|
+
|
|
225
|
+
Every plugin has two streams of real-time events, both surfaced through the store:
|
|
226
|
+
|
|
227
|
+
1. **The `primary` channel** — a peer channel scoped to users of this plugin right now. Use it for in-app pub/sub (cursor positions, "someone just did X", remote controls).
|
|
228
|
+
2. **Platform API events** — defined in AsyncAPI at `${apiDocsBaseUrl}/api-specs/asyncapi.json` under `components.messages`. Each message can declare `x-triggered-by` pointing at an OpenAPI operationId. When the server handles that operation, the event fires — you subscribe instead of polling.
|
|
62
229
|
|
|
63
|
-
|
|
230
|
+
### The `primary` channel
|
|
64
231
|
|
|
65
232
|
```javascript
|
|
66
|
-
// Listen
|
|
67
|
-
store.
|
|
68
|
-
console.log("
|
|
233
|
+
// Listen for a custom event from other users of this plugin
|
|
234
|
+
store.listen("primary", "cursor_moved", (data) => {
|
|
235
|
+
console.log("Peer moved:", data)
|
|
69
236
|
})
|
|
70
237
|
|
|
71
|
-
//
|
|
72
|
-
store.
|
|
238
|
+
// Broadcast to everyone else on the primary channel
|
|
239
|
+
store.broadcast("primary", "cursor_moved", { x: 42, y: 100 })
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Platform API events
|
|
243
|
+
|
|
244
|
+
Use the AsyncAPI form of `listen` — event name first, permission identifier second, callback third:
|
|
73
245
|
|
|
74
|
-
|
|
75
|
-
store.
|
|
246
|
+
```javascript
|
|
247
|
+
store.listen("SocialStreamPostCreated", "social_stream_two", (post) => {
|
|
248
|
+
console.log("New post on the destination stream:", post)
|
|
249
|
+
})
|
|
76
250
|
```
|
|
77
251
|
|
|
252
|
+
The permission identifier (`"social_stream_two"` above, or `"project"` for project-wide events) is the same identifier you pass to `callApi`. It scopes the subscription to the resource the admin bound for that identifier.
|
|
253
|
+
|
|
254
|
+
### Rule: whenever you add a `callApi`, check for a matching event
|
|
255
|
+
|
|
256
|
+
Before wiring polling or manual refresh after a mutation, ask: does this operation fire a socket event? Use the MCP:
|
|
257
|
+
|
|
258
|
+
- `api_find_events_for_operation` — given an operationId, returns every AsyncAPI message whose `x-triggered-by` matches. If it returns one, subscribe to it instead of polling.
|
|
259
|
+
- `api_list_events` — list every event, optionally filtered by `triggeredBy`.
|
|
260
|
+
- `search_websocket_events` — keyword search across events + channels.
|
|
261
|
+
|
|
262
|
+
Example: you just added `store.callApi("posts.store", "social_stream_two", {...})`. Run `api_find_events_for_operation({ operationId: "posts.store" })`, see `SocialStreamPostCreated`, subscribe with `store.listen("SocialStreamPostCreated", "social_stream_two", cb)`. No polling.
|
|
263
|
+
|
|
264
|
+
### Testing broadcasts locally
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
gxdev socket list # list available events
|
|
268
|
+
gxdev socket send --event EventName # fire a test broadcast
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Payloads live under `socket-events/` — add or edit a JSON file to define a new one.
|
|
272
|
+
|
|
78
273
|
## Vue Directives for Dynamic Content
|
|
79
274
|
|
|
80
|
-
|
|
275
|
+
Every piece of admin-editable content goes through a directive — that's what makes the configuration form meaningful.
|
|
81
276
|
|
|
82
277
|
```html
|
|
83
278
|
<!-- Text from strings -->
|
|
@@ -90,6 +285,40 @@ Use these directives instead of hardcoding text and images:
|
|
|
90
285
|
<img gxp-src="hero_image" src="/placeholder.jpg" alt="Hero" />
|
|
91
286
|
```
|
|
92
287
|
|
|
288
|
+
## Admin Configuration Form (`configuration.json`)
|
|
289
|
+
|
|
290
|
+
`configuration.json` defines the form admins use to customize the plugin after it ships. Every string, asset, dependency, color, and setting your plugin exposes must have a matching field here.
|
|
291
|
+
|
|
292
|
+
The full workflow — run it whenever you've added/changed a `callApi`, `listen`, `gxp-string`, or `gxp-src`:
|
|
293
|
+
|
|
294
|
+
1. **Extract the configurable surface into `app-manifest.json`** — `config_extract_strings` with `writeTo: "app-manifest.json"` (same logic as `gxdev extract-config` on the CLI). It scans `src/` for directives and store usage and merges the new keys into the manifest, linter-guarded.
|
|
295
|
+
2. **Add a form field in `configuration.json` for every manifest entry**, using the mapping below.
|
|
296
|
+
3. **Validate** — `config_validate` on demand, or `gxdev lint --all` at the end.
|
|
297
|
+
|
|
298
|
+
### Default field mapping
|
|
299
|
+
|
|
300
|
+
| Manifest source | `configuration.json` field |
|
|
301
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
302
|
+
| `strings.default.<key>` | `text` (short) or `textarea` (long) |
|
|
303
|
+
| `assets.<key>` (driven by `gxp-src`) | `selectAsset` |
|
|
304
|
+
| `dependencies[]` — each declared identifier | `asyncSelect` bound to the matching resource list endpoint so the admin picks a real record |
|
|
305
|
+
| `settings.<key>` representing a color | `colorPicker` |
|
|
306
|
+
| `settings.<key>` representing a threshold/number | `number` |
|
|
307
|
+
| `settings.<key>` representing a feature toggle | `boolean` |
|
|
308
|
+
| Any other setting discussed with the client | Pick via `config_list_field_types` / `config_get_field_schema` |
|
|
309
|
+
|
|
310
|
+
Group related fields into `fields_list` cards (`config_add_card` then `config_add_field`). Use the `name` of each field to exactly match the manifest key it controls — that's the contract the directives and store getters rely on.
|
|
311
|
+
|
|
312
|
+
### Relevant tools
|
|
313
|
+
|
|
314
|
+
- `config_list_card_types`, `config_list_field_types` — enumerate what's available.
|
|
315
|
+
- `config_get_field_schema` — get the shape (required props, conditional rules) of a specific field type.
|
|
316
|
+
- `config_add_card`, `config_add_field`, `config_move_field`, `config_remove_field`, `config_move_card`, `config_remove_card` — mutate the form. Linter-guarded; invalid writes are refused.
|
|
317
|
+
- `config_extract_strings` — the manifest-sync entry point described above.
|
|
318
|
+
- `config_validate` — validate a file on demand.
|
|
319
|
+
|
|
320
|
+
If a mutation is refused, read the validation error and fix the input — do not reach for `force: true`.
|
|
321
|
+
|
|
93
322
|
## Component Kit
|
|
94
323
|
|
|
95
324
|
Import UI components from `@gramercytech/gx-componentkit`:
|
|
@@ -106,13 +335,26 @@ import {
|
|
|
106
335
|
|
|
107
336
|
Available: GxButton, GxCard, GxInput, GxModal, GxSpinner, GxAlert, GxBadge, GxAvatar, GxProgress, GxTabs, GxAccordion
|
|
108
337
|
|
|
109
|
-
## Configuration
|
|
338
|
+
## Configuration Files
|
|
110
339
|
|
|
111
|
-
|
|
340
|
+
- `app-manifest.json` — strings, assets, settings, dependencies, permissions. Hot-reloaded in dev.
|
|
341
|
+
- `configuration.json` — admin-facing form definition (cards + fields).
|
|
342
|
+
|
|
343
|
+
## Linting
|
|
344
|
+
|
|
345
|
+
Run before declaring any change complete:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
gxdev lint # default targets
|
|
349
|
+
gxdev lint --all # everything
|
|
350
|
+
gxdev lint --json # machine-readable output
|
|
351
|
+
```
|
|
112
352
|
|
|
113
353
|
## Testing
|
|
114
354
|
|
|
115
355
|
- Socket events: `gxdev socket send --event EventName`
|
|
356
|
+
- API calls against local mock: `test_api_route` MCP tool
|
|
357
|
+
- Component tests: `test_scaffold_component_test` MCP tool
|
|
116
358
|
- Dev Tools: Press Ctrl+Shift+D
|
|
117
359
|
- Console: `window.gxDevTools.store()` to inspect store
|
|
118
360
|
|
|
@@ -127,6 +369,8 @@ Set `VITE_API_ENV` in `.env`:
|
|
|
127
369
|
|
|
128
370
|
## API Documentation
|
|
129
371
|
|
|
372
|
+
Prefer the MCP tools (`get_openapi_spec`, `search_api_endpoints`, `docs_search`, etc.) over fetching these URLs directly:
|
|
373
|
+
|
|
130
374
|
- OpenAPI Spec: https://api.zenith-develop.env.eventfinity.app/api-specs/openapi.json
|
|
131
375
|
- AsyncAPI Spec: https://api.zenith-develop.env.eventfinity.app/api-specs/asyncapi.json
|
|
132
376
|
- Webhooks: https://api.zenith-develop.env.eventfinity.app/api-specs/webhooks.json
|
package/template/GEMINI.md
CHANGED
|
@@ -2,6 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
This is a GxP plugin project for the GxP kiosk platform built with Vue 3.
|
|
4
4
|
|
|
5
|
+
## Development Workflow
|
|
6
|
+
|
|
7
|
+
Work through these steps in order. Do not skip.
|
|
8
|
+
|
|
9
|
+
### 1. Understand the feature
|
|
10
|
+
|
|
11
|
+
Before writing anything, clarify with the client:
|
|
12
|
+
|
|
13
|
+
- Who uses it and what's the user-facing outcome?
|
|
14
|
+
- Which real data is read/written? Which real-time events matter?
|
|
15
|
+
- What must an admin be able to customize after ship (text, images, colors, thresholds)?
|
|
16
|
+
|
|
17
|
+
### 2. Discover data sources via the `gxp-api` MCP server
|
|
18
|
+
|
|
19
|
+
Ground the implementation in real platform endpoints and events. Do not invent API paths or event names.
|
|
20
|
+
|
|
21
|
+
- Endpoints — `api_list_tags`, `api_list_operation_ids`, `search_api_endpoints`, `api_get_operation_parameters`, `get_endpoint_details`.
|
|
22
|
+
- Find by payload shape — `api_find_endpoints_by_schema`.
|
|
23
|
+
- WebSocket events — `api_find_events_for_operation` (maps operationId → events via `x-triggered-by`), `api_list_events`, `search_websocket_events`, `get_asyncapi_spec`. Run for every planned operationId so you subscribe instead of polling.
|
|
24
|
+
- Canonical dependency JSON — `api_generate_dependency`.
|
|
25
|
+
- Documentation — `docs_list_pages`, `docs_search`, `docs_get_page`.
|
|
26
|
+
|
|
27
|
+
### 3. Plan with the client
|
|
28
|
+
|
|
29
|
+
Confirm a plan before implementing. It must include:
|
|
30
|
+
|
|
31
|
+
- Screens/components and which layout (Public/Private/System).
|
|
32
|
+
- Which store getters, API calls, and socket events power each piece of data.
|
|
33
|
+
- The **admin configuration form** (`configuration.json`) — every admin-editable string, asset, color, and setting listed as cards/fields.
|
|
34
|
+
- The exact keys you'll add to `app-manifest.json`.
|
|
35
|
+
|
|
36
|
+
### 4. Implement
|
|
37
|
+
|
|
38
|
+
- All data goes through the GxP store — `store.callApi(operationId, identifier, data)` for platform APIs, plus sockets, strings, assets, settings, state. No direct `axios`/`fetch`.
|
|
39
|
+
- Every permission identifier used by `callApi` must be declared in `app-manifest.json` → `dependencies` + `permissions`. Use `"project"` for project-wide / top-level operations.
|
|
40
|
+
- Wire every user-facing text with `gxp-string` and every image with `gxp-src`.
|
|
41
|
+
- Only edit files under `src/`; the runtime container is off-limits.
|
|
42
|
+
|
|
43
|
+
### 5. Sync the manifest and build the admin form
|
|
44
|
+
|
|
45
|
+
Whenever you add or change a `store.callApi`, `store.listen`, `gxp-string`, or `gxp-src`, close the loop on the admin-editable surface before you move on:
|
|
46
|
+
|
|
47
|
+
1. **Extract into `app-manifest.json`** — call the MCP tool `config_extract_strings` with `writeTo: "app-manifest.json"` (same logic as `gxdev extract-config` on the CLI). It scans `src/` for directives and store usage and merges new keys into the manifest, linter-guarded.
|
|
48
|
+
2. **Add a form field in `configuration.json` for every manifest entry**, using the MCP `config_*` tools. Default mapping:
|
|
49
|
+
- `strings.default.*` → `text` (or `textarea` for long copy)
|
|
50
|
+
- `assets.*` (driven by `gxp-src`) → `selectAsset`
|
|
51
|
+
- Each declared dependency identifier → `asyncSelect` bound to the matching resource list endpoint so the admin picks a real record
|
|
52
|
+
- Color settings → `colorPicker`; numeric thresholds → `number`; feature toggles → `boolean`
|
|
53
|
+
- Anything else → enumerate with `config_list_field_types` / `config_get_field_schema`
|
|
54
|
+
Each field's `name` must exactly match the manifest key it controls.
|
|
55
|
+
3. **Lint** — `gxdev lint --all`. Fix every error before moving on.
|
|
56
|
+
|
|
57
|
+
### 6. Test broadcasts
|
|
58
|
+
|
|
59
|
+
- `gxdev socket list` — see available events.
|
|
60
|
+
- `gxdev socket send --event <EventName>` — fire a test broadcast.
|
|
61
|
+
- `test_api_route` MCP tool — hit endpoints against the local mock.
|
|
62
|
+
- `test_scaffold_component_test` MCP tool — generate Vitest files.
|
|
63
|
+
|
|
64
|
+
### 7. Final lint
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gxdev lint --all
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Run again after testing and fix every error before declaring done.
|
|
71
|
+
|
|
5
72
|
## Architecture
|
|
6
73
|
|
|
7
74
|
The plugin runs inside a container provided by the `gxdev` server. Only edit files in `src/`:
|
|
@@ -9,24 +76,71 @@ The plugin runs inside a container provided by the `gxdev` server. Only edit fil
|
|
|
9
76
|
- `src/Plugin.vue` - Main entry point
|
|
10
77
|
- `src/components/` - Reusable components
|
|
11
78
|
- `src/views/` - Page components
|
|
12
|
-
- `app-manifest.json` -
|
|
79
|
+
- `app-manifest.json` - Strings, assets, settings, dependencies (hot-reloaded)
|
|
80
|
+
- `configuration.json` - Admin-facing configuration form (cards + fields)
|
|
13
81
|
|
|
14
|
-
## Critical Rule: Use
|
|
82
|
+
## Critical Rule: Use `store.callApi` for All Platform Calls
|
|
15
83
|
|
|
16
|
-
|
|
84
|
+
Every call to the GxP platform goes through `store.callApi(operationId, identifier, data)`. It handles auth, URL resolution, team/project scoping, and path-parameter substitution.
|
|
17
85
|
|
|
18
86
|
```javascript
|
|
19
87
|
import { useGxpStore } from "@gx-runtime/stores/gxpPortalConfigStore"
|
|
20
88
|
const store = useGxpStore()
|
|
21
89
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
90
|
+
await store.callApi(operationId, identifier, data)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **`operationId`** — OpenAPI operationId. Look it up with `api_list_operation_ids` or `search_api_endpoints` (MCP). Do not invent one.
|
|
94
|
+
- **`identifier`** — a permission identifier declared in `app-manifest.json` → `dependencies` / `permissions`. It scopes the call to the resource the admin has bound to that identifier, and supplies the parent ID for path substitution.
|
|
95
|
+
- **`data`** — body fields, query params, or path params not supplied by the identifier. `"pluginVars.someKey"` pulls a value from settings at call time.
|
|
96
|
+
|
|
97
|
+
### Permission identifier pattern
|
|
98
|
+
|
|
99
|
+
Declare identifiers by the **role** a resource plays, not its name. Example — a plugin that reads posts from one social stream and creates posts on another:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
// app-manifest.json
|
|
103
|
+
{
|
|
104
|
+
"dependencies": [
|
|
105
|
+
{ "identifier": "social_stream_one", "model": "SocialStream" },
|
|
106
|
+
{ "identifier": "social_stream_two", "model": "SocialStream" }
|
|
107
|
+
],
|
|
108
|
+
"permissions": [
|
|
109
|
+
{ "identifier": "social_stream_one", "description": "Source — read posts" },
|
|
110
|
+
{
|
|
111
|
+
"identifier": "social_stream_two",
|
|
112
|
+
"description": "Destination — create posts"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
28
116
|
```
|
|
29
117
|
|
|
118
|
+
```javascript
|
|
119
|
+
const posts = await store.callApi("posts.index", "social_stream_one")
|
|
120
|
+
await store.callApi("posts.store", "social_stream_two", {
|
|
121
|
+
body: "Hello world",
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### The `"project"` identifier
|
|
126
|
+
|
|
127
|
+
For project-wide operations (creating the parent resource itself, or any top-level call not scoped to a dependency) use the reserved identifier `"project"`. You must pass any required IDs in `data`:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const stream = await store.callApi("social_streams.store", "project", {
|
|
131
|
+
name: "New Stream",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await store.callApi("posts.store", "project", {
|
|
135
|
+
socialStreamId: stream.id,
|
|
136
|
+
body: "First post",
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Low-level methods
|
|
141
|
+
|
|
142
|
+
`store.apiGet` / `apiPost` / `apiPut` / `apiPatch` / `apiDelete` bypass the permission model. Avoid unless you have a specific reason. Never use axios or fetch directly.
|
|
143
|
+
|
|
30
144
|
## Store Data Access
|
|
31
145
|
|
|
32
146
|
```javascript
|
|
@@ -43,20 +157,54 @@ store.updateAsset("key", "url")
|
|
|
43
157
|
store.updateState("key", "value")
|
|
44
158
|
```
|
|
45
159
|
|
|
46
|
-
##
|
|
160
|
+
## Real-Time Events
|
|
161
|
+
|
|
162
|
+
Two streams, both through the store:
|
|
163
|
+
|
|
164
|
+
1. **`primary` channel** — peer pub/sub between users of this plugin.
|
|
165
|
+
2. **Platform API events** — AsyncAPI events from `${apiDocsBaseUrl}/api-specs/asyncapi.json` (`components.messages`). Each message may declare `x-triggered-by` pointing at an OpenAPI operationId.
|
|
166
|
+
|
|
167
|
+
### `primary` channel
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
store.listen("primary", "custom_event", (data) => {
|
|
171
|
+
/* ... */
|
|
172
|
+
})
|
|
173
|
+
store.broadcast("primary", "custom_event", { hello: "world" })
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Platform API events
|
|
47
177
|
|
|
48
178
|
```javascript
|
|
49
|
-
//
|
|
50
|
-
store.
|
|
51
|
-
|
|
179
|
+
// Event name first, permission identifier second, callback third
|
|
180
|
+
store.listen("SocialStreamPostCreated", "social_stream_two", (post) => {
|
|
181
|
+
// fires whenever the admin-bound social_stream_two receives a new post
|
|
52
182
|
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The permission identifier is the same one you use in `callApi` — it scopes the subscription to the admin-bound resource. Use `"project"` for project-wide events.
|
|
186
|
+
|
|
187
|
+
### Replace polling with events
|
|
188
|
+
|
|
189
|
+
Whenever you add a `callApi` call, check whether its operationId triggers a socket event using MCP tools:
|
|
53
190
|
|
|
54
|
-
|
|
55
|
-
|
|
191
|
+
- `api_find_events_for_operation { operationId: "posts.store" }` — returns events whose `x-triggered-by` matches.
|
|
192
|
+
- `api_list_events` — list all events; optional `triggeredBy` filter.
|
|
193
|
+
- `search_websocket_events` — keyword search.
|
|
194
|
+
|
|
195
|
+
If a match exists, subscribe instead of polling.
|
|
196
|
+
|
|
197
|
+
### Testing broadcasts
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
gxdev socket list
|
|
201
|
+
gxdev socket send --event EventName
|
|
56
202
|
```
|
|
57
203
|
|
|
58
204
|
## Vue Directives
|
|
59
205
|
|
|
206
|
+
Every admin-editable piece of content goes through a directive — that's the bridge between the component and the admin form.
|
|
207
|
+
|
|
60
208
|
```html
|
|
61
209
|
<!-- Dynamic text from strings -->
|
|
62
210
|
<h1 gxp-string="welcome_title">Default</h1>
|
|
@@ -65,16 +213,30 @@ store.emitSocket("primary", "event-name", data)
|
|
|
65
213
|
<img gxp-src="hero_image" src="/placeholder.jpg" />
|
|
66
214
|
```
|
|
67
215
|
|
|
216
|
+
## Admin Configuration Form
|
|
217
|
+
|
|
218
|
+
`configuration.json` defines the form admins use to customize the plugin. The workflow — run it whenever you touch a `callApi`, `listen`, `gxp-string`, or `gxp-src`:
|
|
219
|
+
|
|
220
|
+
1. **Sync the manifest** — `config_extract_strings` with `writeTo: "app-manifest.json"` (same as `gxdev extract-config`). Scans `src/` and merges directives, store getters, and dependency identifiers into the manifest, linter-guarded.
|
|
221
|
+
2. **Add a form field for every manifest entry** via the MCP `config_*` tools. Default mapping:
|
|
222
|
+
- `strings.default.*` → `text` / `textarea`
|
|
223
|
+
- `assets.*` → `selectAsset`
|
|
224
|
+
- Each `dependencies[]` identifier → `asyncSelect` bound to the resource's list endpoint
|
|
225
|
+
- Colors → `colorPicker`; numbers/thresholds → `number`; toggles → `boolean`
|
|
226
|
+
- Anything else → `config_list_field_types` / `config_get_field_schema`
|
|
227
|
+
Field `name` must match the manifest key it controls.
|
|
228
|
+
3. **Validate** — `config_validate`, then `gxdev lint --all`.
|
|
229
|
+
|
|
230
|
+
Tools: `config_list_card_types`, `config_list_field_types`, `config_get_field_schema`, `config_add_card`, `config_add_field`, `config_move_field`, `config_remove_field`, `config_validate`, `config_extract_strings`. Every mutation is linter-guarded against schemas in `bin/lib/lint/schemas/`.
|
|
231
|
+
|
|
68
232
|
## Component Kit
|
|
69
233
|
|
|
70
234
|
Use `@gramercytech/gx-componentkit` for UI:
|
|
71
235
|
GxButton, GxCard, GxInput, GxModal, GxSpinner, GxAlert, GxBadge, GxProgress, GxTabs
|
|
72
236
|
|
|
73
|
-
## Configuration
|
|
74
|
-
|
|
75
|
-
Edit `app-manifest.json` for strings, assets, settings. Hot-reloads in dev.
|
|
76
|
-
|
|
77
237
|
## API Specs
|
|
78
238
|
|
|
239
|
+
Prefer the MCP tools over direct fetches:
|
|
240
|
+
|
|
79
241
|
- OpenAPI: https://api.zenith-develop.env.eventfinity.app/api-specs/openapi.json
|
|
80
242
|
- AsyncAPI: https://api.zenith-develop.env.eventfinity.app/api-specs/asyncapi.json
|