@gxp-dev/tools 2.0.86 → 2.0.88
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/bin/lib/cli.js +6 -0
- package/bin/lib/commands/dev.js +182 -59
- package/bin/lib/commands/init.js +5 -0
- package/mcp/lib/config-tools.js +1 -1
- package/mcp/lib/server.js +6 -2
- package/package.json +1 -1
- package/runtime/dev-tools/StoreInspector.vue +26 -0
- package/runtime/stores/gxpPortalConfigStore.js +61 -3
- package/template/.claude/agents/gxp-developer.md +24 -0
- package/template/AGENTS.md +26 -0
- package/template/CLAUDE.md +25 -0
- package/template/GEMINI.md +14 -0
package/bin/lib/cli.js
CHANGED
|
@@ -157,6 +157,12 @@ const cli = yargs
|
|
|
157
157
|
default: false,
|
|
158
158
|
alias: "m",
|
|
159
159
|
},
|
|
160
|
+
json: {
|
|
161
|
+
describe:
|
|
162
|
+
"Emit all dev-server logs as newline-delimited JSON (one record per line) for cloud log collectors",
|
|
163
|
+
type: "boolean",
|
|
164
|
+
default: false,
|
|
165
|
+
},
|
|
160
166
|
},
|
|
161
167
|
devCommand,
|
|
162
168
|
)
|
package/bin/lib/commands/dev.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const path = require("path")
|
|
9
9
|
const fs = require("fs")
|
|
10
|
+
const { spawn } = require("child_process")
|
|
10
11
|
const shell = require("shelljs")
|
|
11
12
|
const dotenv = require("dotenv")
|
|
12
13
|
const {
|
|
@@ -16,6 +17,123 @@ const {
|
|
|
16
17
|
findExistingCertificates,
|
|
17
18
|
} = require("../utils")
|
|
18
19
|
|
|
20
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]/g
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a logger that either emits NDJSON lines or plain text.
|
|
24
|
+
* In JSON mode, every record is a single line: {timestamp, service, level, message}
|
|
25
|
+
*/
|
|
26
|
+
function createLogger(jsonMode) {
|
|
27
|
+
function emit(level, service, message) {
|
|
28
|
+
if (jsonMode) {
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
service,
|
|
33
|
+
level,
|
|
34
|
+
message,
|
|
35
|
+
}) + "\n",
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const stream =
|
|
40
|
+
level === "error" || level === "warn" ? process.stderr : process.stdout
|
|
41
|
+
stream.write(message + "\n")
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
jsonMode,
|
|
45
|
+
info: (message, service = "GXDEV") => emit("info", service, message),
|
|
46
|
+
warn: (message, service = "GXDEV") => emit("warn", service, message),
|
|
47
|
+
error: (message, service = "GXDEV") => emit("error", service, message),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Spawn a service and pipe each line of its stdout/stderr through the logger.
|
|
53
|
+
* Returns the child process.
|
|
54
|
+
*/
|
|
55
|
+
function spawnService(name, command, logger) {
|
|
56
|
+
const child = spawn(command, {
|
|
57
|
+
shell: true,
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
env: process.env,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
function pipe(stream, level) {
|
|
63
|
+
let buffer = ""
|
|
64
|
+
stream.setEncoding("utf8")
|
|
65
|
+
stream.on("data", (chunk) => {
|
|
66
|
+
buffer += chunk
|
|
67
|
+
let idx
|
|
68
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
69
|
+
const line = buffer.slice(0, idx).replace(/\r$/, "")
|
|
70
|
+
buffer = buffer.slice(idx + 1)
|
|
71
|
+
const clean = line.replace(ANSI_REGEX, "")
|
|
72
|
+
if (clean.length > 0) {
|
|
73
|
+
logger[level](clean, name)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
stream.on("end", () => {
|
|
78
|
+
if (buffer.length > 0) {
|
|
79
|
+
const clean = buffer.replace(ANSI_REGEX, "").trim()
|
|
80
|
+
if (clean) {
|
|
81
|
+
logger[level](clean, name)
|
|
82
|
+
}
|
|
83
|
+
buffer = ""
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pipe(child.stdout, "info")
|
|
89
|
+
pipe(child.stderr, "error")
|
|
90
|
+
return child
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run a list of services concurrently, wiring up line-by-line JSON logging
|
|
95
|
+
* and best-effort shutdown when any one exits or the user hits Ctrl+C.
|
|
96
|
+
*/
|
|
97
|
+
function runServicesJson(services, logger) {
|
|
98
|
+
const children = services.map((svc) => {
|
|
99
|
+
logger.info(`starting ${svc.name}: ${svc.command}`, svc.name)
|
|
100
|
+
return { svc, child: spawnService(svc.name, svc.command, logger) }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
let shuttingDown = false
|
|
104
|
+
function shutdown(code) {
|
|
105
|
+
if (shuttingDown) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
shuttingDown = true
|
|
109
|
+
for (const { child } of children) {
|
|
110
|
+
if (!child.killed && child.exitCode === null) {
|
|
111
|
+
child.kill("SIGTERM")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
process.exit(code ?? 0)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const { svc, child } of children) {
|
|
118
|
+
child.on("exit", (code, signal) => {
|
|
119
|
+
logger.info(
|
|
120
|
+
`${svc.name} exited (code=${code ?? "null"}${
|
|
121
|
+
signal ? `, signal=${signal}` : ""
|
|
122
|
+
})`,
|
|
123
|
+
svc.name,
|
|
124
|
+
)
|
|
125
|
+
shutdown(code ?? 0)
|
|
126
|
+
})
|
|
127
|
+
child.on("error", (err) => {
|
|
128
|
+
logger.error(`${svc.name} failed to spawn: ${err.message}`, svc.name)
|
|
129
|
+
shutdown(1)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.on("SIGINT", () => shutdown(130))
|
|
134
|
+
process.on("SIGTERM", () => shutdown(143))
|
|
135
|
+
}
|
|
136
|
+
|
|
19
137
|
/**
|
|
20
138
|
* Get browser extension paths and commands
|
|
21
139
|
* @param {string} browser - "firefox" or "chrome"
|
|
@@ -96,6 +214,7 @@ function getBrowserExtensionConfig(browser, projectPath, paths, options = {}) {
|
|
|
96
214
|
* Development command - starts the dev server
|
|
97
215
|
*/
|
|
98
216
|
function devCommand(argv) {
|
|
217
|
+
const logger = createLogger(!!argv.json)
|
|
99
218
|
const paths = resolveGxPaths()
|
|
100
219
|
const projectPath = findProjectRoot()
|
|
101
220
|
|
|
@@ -105,13 +224,13 @@ function devCommand(argv) {
|
|
|
105
224
|
|
|
106
225
|
// Load .env file into process.env
|
|
107
226
|
if (fs.existsSync(envPath)) {
|
|
108
|
-
|
|
227
|
+
logger.info("📋 Loading environment variables from .env file")
|
|
109
228
|
dotenv.config({ path: envPath })
|
|
110
229
|
} else if (fs.existsSync(envExamplePath)) {
|
|
111
|
-
|
|
230
|
+
logger.info(
|
|
112
231
|
"💡 Tip: Create .env file from .env.example to customize your environment settings",
|
|
113
232
|
)
|
|
114
|
-
|
|
233
|
+
logger.info(" cp .env.example .env")
|
|
115
234
|
}
|
|
116
235
|
|
|
117
236
|
// Check for SSL certificates unless explicitly disabled
|
|
@@ -124,32 +243,32 @@ function devCommand(argv) {
|
|
|
124
243
|
const existingCerts = findExistingCertificates(certsDir)
|
|
125
244
|
|
|
126
245
|
if (!existingCerts) {
|
|
127
|
-
|
|
246
|
+
logger.warn(
|
|
128
247
|
"⚠ SSL certificates not found. Run 'npm run setup-ssl' to enable HTTPS",
|
|
129
248
|
)
|
|
130
|
-
|
|
249
|
+
logger.info("🌐 Starting HTTP development server...")
|
|
131
250
|
useHttps = false
|
|
132
251
|
} else {
|
|
133
|
-
|
|
134
|
-
|
|
252
|
+
logger.info("🔒 Starting HTTPS development server...")
|
|
253
|
+
logger.info(
|
|
135
254
|
`📁 Using certificate: ${path.basename(existingCerts.certPath)}`,
|
|
136
255
|
)
|
|
137
|
-
|
|
256
|
+
logger.info(`🔑 Using key: ${path.basename(existingCerts.keyPath)}`)
|
|
138
257
|
certPath = existingCerts.certPath
|
|
139
258
|
keyPath = existingCerts.keyPath
|
|
140
259
|
}
|
|
141
260
|
} else {
|
|
142
|
-
|
|
261
|
+
logger.info("🌐 Starting HTTP development server...")
|
|
143
262
|
}
|
|
144
263
|
|
|
145
264
|
// Determine final port value (priority: CLI arg > .env > default)
|
|
146
265
|
const finalPort = argv.port || process.env.NODE_PORT || 3000
|
|
147
|
-
|
|
266
|
+
logger.info(`🌐 Development server will start on port: ${finalPort}`)
|
|
148
267
|
|
|
149
268
|
// Check if mock API should be enabled
|
|
150
269
|
const withMock = argv["with-mock"]
|
|
151
270
|
if (withMock) {
|
|
152
|
-
|
|
271
|
+
logger.info("🎭 Mock API will be enabled")
|
|
153
272
|
}
|
|
154
273
|
|
|
155
274
|
// Socket server starts by default unless --no-socket is passed
|
|
@@ -159,15 +278,15 @@ function devCommand(argv) {
|
|
|
159
278
|
// Check for local server.js first, then runtime directory
|
|
160
279
|
const serverJs = resolveFilePath("server.cjs", "", "runtime")
|
|
161
280
|
if (!fs.existsSync(serverJs.path)) {
|
|
162
|
-
|
|
281
|
+
logger.warn("⚠ server.js not found. Skipping Socket.IO server.")
|
|
163
282
|
} else {
|
|
164
283
|
serverJsPath = serverJs.path
|
|
165
|
-
|
|
284
|
+
logger.info(
|
|
166
285
|
`📡 Starting Socket.IO server with nodemon... (${
|
|
167
286
|
serverJs.isLocal ? "local" : "package"
|
|
168
287
|
} version)`,
|
|
169
288
|
)
|
|
170
|
-
|
|
289
|
+
logger.info(`📁 Using: ${serverJsPath}`)
|
|
171
290
|
}
|
|
172
291
|
}
|
|
173
292
|
|
|
@@ -184,16 +303,16 @@ function devCommand(argv) {
|
|
|
184
303
|
fs.existsSync(path.join(projectPath, "vite.extend.mjs"))
|
|
185
304
|
|
|
186
305
|
if (hasLocalIndexHtml) {
|
|
187
|
-
|
|
306
|
+
logger.info("📁 Using local index.html")
|
|
188
307
|
}
|
|
189
308
|
if (hasLocalMainJs) {
|
|
190
|
-
|
|
309
|
+
logger.info("📁 Using local main.js")
|
|
191
310
|
}
|
|
192
311
|
if (hasLocalExtend) {
|
|
193
|
-
|
|
312
|
+
logger.info("🧩 Extending vite config from vite.extend.js")
|
|
194
313
|
}
|
|
195
314
|
if (!hasLocalIndexHtml && !hasLocalMainJs && !hasLocalExtend) {
|
|
196
|
-
|
|
315
|
+
logger.info(
|
|
197
316
|
"📦 Using runtime dev files (create vite.extend.js to customize)",
|
|
198
317
|
)
|
|
199
318
|
}
|
|
@@ -234,11 +353,11 @@ function devCommand(argv) {
|
|
|
234
353
|
port: finalPort,
|
|
235
354
|
})
|
|
236
355
|
if (firefoxConfig) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
356
|
+
logger.info("🦊 Firefox extension will launch with dev server")
|
|
357
|
+
logger.info(`📁 Extension path: ${firefoxConfig.extensionPath}`)
|
|
358
|
+
logger.info(`🌐 Start URL: ${firefoxConfig.startUrl}`)
|
|
240
359
|
} else {
|
|
241
|
-
|
|
360
|
+
logger.warn("⚠️ Firefox extension not found, skipping")
|
|
242
361
|
}
|
|
243
362
|
}
|
|
244
363
|
|
|
@@ -248,11 +367,11 @@ function devCommand(argv) {
|
|
|
248
367
|
port: finalPort,
|
|
249
368
|
})
|
|
250
369
|
if (chromeConfig) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
370
|
+
logger.info("🚀 Chrome extension will launch with dev server")
|
|
371
|
+
logger.info(`📁 Extension path: ${chromeConfig.extensionPath}`)
|
|
372
|
+
logger.info(`🌐 Start URL: ${chromeConfig.startUrl}`)
|
|
254
373
|
} else {
|
|
255
|
-
|
|
374
|
+
logger.warn("⚠️ Chrome extension not found, skipping")
|
|
256
375
|
}
|
|
257
376
|
}
|
|
258
377
|
|
|
@@ -261,54 +380,58 @@ function devCommand(argv) {
|
|
|
261
380
|
process.env.CHROME_EXTENSION_PATH = chromeConfig.extensionPath
|
|
262
381
|
}
|
|
263
382
|
|
|
264
|
-
// Build the command based on what's requested
|
|
265
|
-
let command
|
|
266
|
-
|
|
267
|
-
// Collect all processes to run
|
|
268
|
-
const processes = []
|
|
269
|
-
const names = []
|
|
270
|
-
const colors = []
|
|
271
|
-
|
|
272
383
|
// Normalize path separators to forward slashes for cross-platform shell compatibility
|
|
273
384
|
const normalizedViteConfigPath = viteConfigPath.replace(/\\/g, "/")
|
|
274
385
|
|
|
275
|
-
//
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
386
|
+
// Build the canonical service list (raw commands, no concurrently wrapping)
|
|
387
|
+
const services = []
|
|
388
|
+
services.push({
|
|
389
|
+
name: "VITE",
|
|
390
|
+
color: "cyan",
|
|
391
|
+
command: `npx vite dev --config "${normalizedViteConfigPath}"`,
|
|
392
|
+
})
|
|
280
393
|
|
|
281
|
-
// Socket server (on by default, skip if --no-socket or server.js not found)
|
|
282
394
|
if (serverJsPath) {
|
|
283
395
|
const normalizedServerPath = serverJsPath.replace(/\\/g, "/")
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
396
|
+
services.push({
|
|
397
|
+
name: "SOCKET",
|
|
398
|
+
color: "green",
|
|
399
|
+
command: `npx nodemon "${normalizedServerPath}"`,
|
|
400
|
+
})
|
|
287
401
|
}
|
|
288
402
|
|
|
289
|
-
// Firefox extension (optional)
|
|
290
403
|
if (firefoxConfig) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
404
|
+
services.push({
|
|
405
|
+
name: firefoxConfig.name,
|
|
406
|
+
color: firefoxConfig.color,
|
|
407
|
+
command: firefoxConfig.command,
|
|
408
|
+
})
|
|
294
409
|
}
|
|
295
410
|
|
|
296
|
-
// Chrome extension (optional)
|
|
297
411
|
if (chromeConfig) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
412
|
+
services.push({
|
|
413
|
+
name: chromeConfig.name,
|
|
414
|
+
color: chromeConfig.color,
|
|
415
|
+
command: chromeConfig.command,
|
|
416
|
+
})
|
|
301
417
|
}
|
|
302
418
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
419
|
+
// In JSON mode we orchestrate the children ourselves so we can wrap every
|
|
420
|
+
// stdout/stderr line as NDJSON. Concurrently's prefixed output would defeat
|
|
421
|
+
// that. Outside JSON mode, keep the legacy concurrently-based behavior.
|
|
422
|
+
if (logger.jsonMode) {
|
|
423
|
+
runServicesJson(services, logger)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let command
|
|
428
|
+
if (services.length > 1) {
|
|
429
|
+
const quoted = services.map((s) => `"${s.command}"`).join(" ")
|
|
430
|
+
const names = services.map((s) => s.name).join(",")
|
|
431
|
+
const colors = services.map((s) => s.color).join(",")
|
|
432
|
+
command = `npx concurrently --names "${names}" --prefix-colors "${colors}" ${quoted}`
|
|
309
433
|
} else {
|
|
310
|
-
|
|
311
|
-
command = `npx vite dev --config "${normalizedViteConfigPath}"`
|
|
434
|
+
command = services[0].command
|
|
312
435
|
}
|
|
313
436
|
|
|
314
437
|
shell.exec(command)
|
package/bin/lib/commands/init.js
CHANGED
|
@@ -173,6 +173,11 @@ function copyBundleFiles(projectPath, paths, overwrite = false) {
|
|
|
173
173
|
dest: "GEMINI.md",
|
|
174
174
|
desc: "GEMINI.md (Gemini Code Assist instructions)",
|
|
175
175
|
},
|
|
176
|
+
{
|
|
177
|
+
src: "CLAUDE.md",
|
|
178
|
+
dest: "CLAUDE.md",
|
|
179
|
+
desc: "CLAUDE.md (Claude Code project instructions)",
|
|
180
|
+
},
|
|
176
181
|
{
|
|
177
182
|
src: ".claude/agents/gxp-developer.md",
|
|
178
183
|
dest: ".claude/agents/gxp-developer.md",
|
package/mcp/lib/config-tools.js
CHANGED
|
@@ -285,7 +285,7 @@ const CONFIG_TOOLS = [
|
|
|
285
285
|
{
|
|
286
286
|
name: "config_extract_strings",
|
|
287
287
|
description:
|
|
288
|
-
"Scan a plugin's src/ directory for GxP datastore usage and directives (gxp-string, gxp-src, store.getString/getSetting/getAsset/getState calls) and return the extracted keys. Optionally merge them into app-manifest.json (linter-guarded — invalid writes are refused unless force=true).",
|
|
288
|
+
"Scan a plugin's src/ directory for GxP datastore usage and directives (gxp-string, gxp-src, store.getString/getSetting/getAsset/getState calls) and return the extracted keys. Optionally merge them into app-manifest.json (linter-guarded — invalid writes are refused unless force=true). Note: store.getUser/getUserName/getUserEmail/isAuthenticated and store.user are also valid accessors but read the platform-injected logged-in user (null when logged out) and have no manifest entry.",
|
|
289
289
|
inputSchema: {
|
|
290
290
|
type: "object",
|
|
291
291
|
properties: {
|
package/mcp/lib/server.js
CHANGED
|
@@ -46,7 +46,8 @@ const SERVER_INFO = {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const SERVER_DESCRIPTION =
|
|
49
|
-
"GxP toolkit MCP server: API specs, data models, config/manifest editing, documentation search, and plugin test helpers for AI coding assistants. UIKit component/story tools are served by the uikit's own Storybook MCP (gxdev storybook)
|
|
49
|
+
"GxP toolkit MCP server: API specs, data models, config/manifest editing, documentation search, and plugin test helpers for AI coding assistants. UIKit component/story tools are served by the uikit's own Storybook MCP (gxdev storybook).\n\n" +
|
|
50
|
+
"IMPORTANT context for plugin authors: GxP is a multi-tenant platform. The platform admin UI (not the plugin) owns all configuration of forms, quizzes, surveys, quiz builders, leaderboards, settings, strings, assets, and project metadata. Plugins do NOT define these — they consume them. At runtime the platform injects manifest data (settings, strings, assets, dependencies, permissions) and the logged-in user into the GxP store, and exposes platform-managed resources via the REST API documented in the OpenAPI spec. Plugins should access forms/quizzes/surveys and their admin-built questions exclusively through `store.callApi(operationId, identifier, data)` — for example `forms.show`, `forms.fields.index`, `forms.responses.store`, `quiz.state`, `quiz.questions`, `quiz.answer`, `quiz.leaderboard`, `survey.metrics`. Use the API spec tools (`search_api_endpoints`, `api_list_tags`, `get_endpoint_details`, `describe_data_models`) to discover the exact operationIds, parameters, and response schemas. The logged-in user is read via `store.user` / `store.getUser()` / `store.getUserName()` / `store.getUserEmail()` (null when logged out)."
|
|
50
51
|
|
|
51
52
|
/* -------------------- API spec search helpers (in-file) ------------------- */
|
|
52
53
|
|
|
@@ -348,7 +349,10 @@ async function startServer() {
|
|
|
348
349
|
|
|
349
350
|
const server = new Server(
|
|
350
351
|
{ name: SERVER_INFO.name, version: SERVER_INFO.version },
|
|
351
|
-
{
|
|
352
|
+
{
|
|
353
|
+
capabilities: { tools: {} },
|
|
354
|
+
instructions: SERVER_DESCRIPTION,
|
|
355
|
+
},
|
|
352
356
|
)
|
|
353
357
|
|
|
354
358
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
package/package.json
CHANGED
|
@@ -212,6 +212,31 @@
|
|
|
212
212
|
</div>
|
|
213
213
|
</div>
|
|
214
214
|
|
|
215
|
+
<div class="inspector-section">
|
|
216
|
+
<h3 class="section-title" @click="toggleSection('user')">
|
|
217
|
+
<span class="toggle-icon">{{ expandedSections.user ? "▼" : "▶" }}</span>
|
|
218
|
+
Logged-in User
|
|
219
|
+
<span class="item-count">{{ store.user ? "auth" : "null" }}</span>
|
|
220
|
+
</h3>
|
|
221
|
+
<div v-if="expandedSections.user" class="section-content">
|
|
222
|
+
<div v-if="!store.user" class="empty-state">
|
|
223
|
+
No user logged in (store.user === null)
|
|
224
|
+
</div>
|
|
225
|
+
<div v-else class="property-list">
|
|
226
|
+
<div
|
|
227
|
+
v-for="(value, key) in store.user"
|
|
228
|
+
:key="key"
|
|
229
|
+
class="property-item"
|
|
230
|
+
>
|
|
231
|
+
<span class="property-key">{{ key }}</span>
|
|
232
|
+
<span class="property-value" :class="getValueType(value)">
|
|
233
|
+
{{ formatValue(value) }}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
215
240
|
<div class="inspector-actions">
|
|
216
241
|
<button
|
|
217
242
|
class="action-btn"
|
|
@@ -251,6 +276,7 @@ const expandedSections = reactive({
|
|
|
251
276
|
assetList: false,
|
|
252
277
|
triggerState: false,
|
|
253
278
|
dependencyList: false,
|
|
279
|
+
user: false,
|
|
254
280
|
})
|
|
255
281
|
|
|
256
282
|
const editingKey = ref(null)
|
|
@@ -94,6 +94,19 @@ function getApiConfig() {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Dev-only fallback user. In production the platform injects the real
|
|
98
|
+
// authenticated user (or null) — this dummy only ships when running under
|
|
99
|
+
// the Vite dev server so plugins can develop against the happy-path shape.
|
|
100
|
+
const DEV_DUMMY_USER = {
|
|
101
|
+
id: "dev-user-001",
|
|
102
|
+
first_name: "Jane",
|
|
103
|
+
last_name: "Developer",
|
|
104
|
+
name: "Jane Developer",
|
|
105
|
+
email: "jane.developer@example.com",
|
|
106
|
+
avatar: null,
|
|
107
|
+
roles: ["attendee"],
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
// Default values used when app-manifest.json doesn't exist or is missing keys
|
|
98
111
|
const defaultData = {
|
|
99
112
|
pluginVars: {
|
|
@@ -108,6 +121,7 @@ const defaultData = {
|
|
|
108
121
|
triggerState: {},
|
|
109
122
|
auth: null,
|
|
110
123
|
userSession: null,
|
|
124
|
+
user: import.meta.env.DEV ? { ...DEV_DUMMY_USER } : null,
|
|
111
125
|
pluginData: {},
|
|
112
126
|
portalAssets: {},
|
|
113
127
|
portal: null,
|
|
@@ -126,6 +140,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
126
140
|
// User session data (injected by platform in production)
|
|
127
141
|
const auth = ref(defaultData.auth)
|
|
128
142
|
const userSession = ref(defaultData.userSession)
|
|
143
|
+
const user = ref(defaultData.user ? { ...defaultData.user } : null)
|
|
129
144
|
const pluginData = ref({ ...defaultData.pluginData })
|
|
130
145
|
const portalAssets = ref({ ...defaultData.portalAssets })
|
|
131
146
|
const portal = ref(defaultData.portal)
|
|
@@ -599,6 +614,46 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
599
614
|
return permissionFlags.value.includes(flag)
|
|
600
615
|
}
|
|
601
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Return the logged-in user object, or null when no user is authenticated.
|
|
619
|
+
*
|
|
620
|
+
* In production the platform injects the real user. In dev (Vite dev
|
|
621
|
+
* server) a dummy user is provided so plugins can develop against the
|
|
622
|
+
* happy path without a backend — clear it from the Dev Tools store
|
|
623
|
+
* inspector to simulate the logged-out state.
|
|
624
|
+
*/
|
|
625
|
+
function getUser() {
|
|
626
|
+
return user.value ?? null
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function isAuthenticated() {
|
|
630
|
+
return user.value !== null && user.value !== undefined
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Convenience helper — returns the user's display name (or `name`,
|
|
635
|
+
* or `first_name + last_name` if `name` is absent). Returns `fallback`
|
|
636
|
+
* when no user is logged in.
|
|
637
|
+
*/
|
|
638
|
+
function getUserName(fallback = null) {
|
|
639
|
+
const u = user.value
|
|
640
|
+
if (!u) {
|
|
641
|
+
return fallback
|
|
642
|
+
}
|
|
643
|
+
if (u.name) {
|
|
644
|
+
return u.name
|
|
645
|
+
}
|
|
646
|
+
const parts = [u.first_name, u.last_name].filter(Boolean)
|
|
647
|
+
return parts.length > 0 ? parts.join(" ") : fallback
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Returns the user's email, or `fallback` when no user is logged in.
|
|
652
|
+
*/
|
|
653
|
+
function getUserEmail(fallback = null) {
|
|
654
|
+
return user.value?.email ?? fallback
|
|
655
|
+
}
|
|
656
|
+
|
|
602
657
|
// Convenience method to add dev assets with proper URL
|
|
603
658
|
function addDevAsset(key, filename) {
|
|
604
659
|
const appPort =
|
|
@@ -640,9 +695,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
640
695
|
const callback = arg3
|
|
641
696
|
const primary = socketConnections.primary
|
|
642
697
|
if (!primary) {
|
|
643
|
-
console.warn(
|
|
644
|
-
"[GxP Store] listen(): primary socket not initialized",
|
|
645
|
-
)
|
|
698
|
+
console.warn("[GxP Store] listen(): primary socket not initialized")
|
|
646
699
|
return () => {}
|
|
647
700
|
}
|
|
648
701
|
if (
|
|
@@ -753,6 +806,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
753
806
|
permissionFlags,
|
|
754
807
|
auth,
|
|
755
808
|
userSession,
|
|
809
|
+
user,
|
|
756
810
|
pluginData,
|
|
757
811
|
portalAssets,
|
|
758
812
|
portal,
|
|
@@ -775,6 +829,10 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
|
|
|
775
829
|
getSetting,
|
|
776
830
|
getAsset,
|
|
777
831
|
getState,
|
|
832
|
+
getUser,
|
|
833
|
+
getUserName,
|
|
834
|
+
getUserEmail,
|
|
835
|
+
isAuthenticated,
|
|
778
836
|
hasPermission,
|
|
779
837
|
findDependency,
|
|
780
838
|
|
|
@@ -9,6 +9,19 @@ model: sonnet
|
|
|
9
9
|
|
|
10
10
|
You are an expert GxP plugin developer. You help build Vue 3 components for the GxP kiosk platform. You have access to the `gxp-api` MCP server — use it; do not guess at API shapes.
|
|
11
11
|
|
|
12
|
+
## Platform vs. plugin — read first
|
|
13
|
+
|
|
14
|
+
The **GxP platform itself** (not the plugin) manages all admin configuration. Plugins consume what the platform provides:
|
|
15
|
+
|
|
16
|
+
- **Forms, quizzes, surveys, and the quiz builder** are admin-built in the platform UI. The plugin reads admin-built form fields, quiz questions, scoring rules, leaderboards, and response data through `store.callApi` — never define them locally. Key operation families (discover the full set with `api_list_operation_ids` / `search_api_endpoints`):
|
|
17
|
+
- **Forms** — `forms.index`, `forms.show`, `forms.fields.index`, `forms.fields.show`, `forms.responses.index/store/show/update/destroy`, `my-responses.index/show`.
|
|
18
|
+
- **Quizzes** — `quiz.state`, `quiz.start`, `quiz.restart`, `quiz.questions`, `quiz.answer`, `quiz.answer.status`, `quiz.complete`, `quiz.timeout`, `quiz.leaderboard`.
|
|
19
|
+
- **Surveys** — `survey.metrics`, `survey.metrics.question`, `survey.metrics.questions`, `survey.metrics.timeseries`, `survey.live-results`.
|
|
20
|
+
- **Project settings, assets, strings, dependencies, permissions, and the logged-in user** are injected into the GxP store at boot. The plugin reads them via `store.getSetting/getString/getAsset/getState/hasPermission/getUser`.
|
|
21
|
+
- The plugin's `app-manifest.json` + `configuration.json` describe **only what the admin needs to configure for this specific plugin instance** — text, images, colors, which form/quiz the plugin should bind to (via `asyncSelect` against the platform list endpoints). Anything that already exists in the platform belongs upstream; don't recreate it.
|
|
22
|
+
|
|
23
|
+
If you're unsure whether the platform already provides something, search before building.
|
|
24
|
+
|
|
12
25
|
## Workflow — Follow This Every Time
|
|
13
26
|
|
|
14
27
|
Every plugin feature goes through seven phases. Do not skip phases. Do not implement before the plan is confirmed.
|
|
@@ -189,8 +202,19 @@ store.getSetting("primary_color", "#FFD600")
|
|
|
189
202
|
store.getAsset("hero_image", "/fallback.jpg")
|
|
190
203
|
store.getState("current_step", 0)
|
|
191
204
|
store.hasPermission("admin")
|
|
205
|
+
|
|
206
|
+
// Logged-in user (returns null when no user is authenticated)
|
|
207
|
+
store.getUser() // Full user object or null
|
|
208
|
+
store.getUserName("Guest") // Display name with fallback
|
|
209
|
+
store.getUserEmail() // Email or null
|
|
210
|
+
store.isAuthenticated() // boolean
|
|
192
211
|
```
|
|
193
212
|
|
|
213
|
+
`store.user` is **null when no user is logged in** — always guard. The
|
|
214
|
+
shape is `{ id, first_name, last_name, name, email, avatar, roles[] }`.
|
|
215
|
+
The platform injects this in production; `gxdev dev` ships a dummy
|
|
216
|
+
authenticated user so happy-path UI renders without a backend.
|
|
217
|
+
|
|
194
218
|
## API Calls — `store.callApi(operationId, identifier, data)`
|
|
195
219
|
|
|
196
220
|
**Every call to the GxP platform goes through `store.callApi`.** It is the primary, permission-aware API method. The low-level verb methods (`apiGet`/`apiPost`/...) still exist as escape hatches but they bypass the permission model — prefer `callApi` for all real work. Never use axios or fetch directly.
|
package/template/AGENTS.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
## Platform vs. plugin — who owns what
|
|
6
|
+
|
|
7
|
+
**The GxP platform itself owns all admin configuration.** Plugins are consumers, not configurators:
|
|
8
|
+
|
|
9
|
+
- **Forms, quizzes, and surveys (including the quiz builder)** are built by admins in the platform UI. The plugin never defines fields, questions, scoring rules, leaderboards, or response schemas. At runtime the plugin reads the admin-built form/quiz/survey — and its questions — through `store.callApi`. Relevant operations: `forms.show`, `forms.fields.index`, `forms.responses.store/show/update`, `quiz.state`, `quiz.start`, `quiz.questions`, `quiz.answer`, `quiz.complete`, `quiz.leaderboard`, `survey.metrics`, `survey.live-results`. Discover the full set with `api_list_operation_ids` filtered by the `forms`/`quiz`/`survey` tags.
|
|
10
|
+
- **Project settings, assets, strings, dependencies, permissions, and the logged-in user** are injected into the GxP store at boot — plugins read them, they don't create them.
|
|
11
|
+
- The plugin's own `app-manifest.json` + `configuration.json` describe **what the admin must configure for THIS plugin instance** (text, images, colors, which form/quiz to bind to, etc.). Everything else lives upstream.
|
|
12
|
+
|
|
13
|
+
When in doubt, search for the operation via `search_api_endpoints` / `api_list_tags` before inventing local state.
|
|
14
|
+
|
|
5
15
|
## Development Workflow
|
|
6
16
|
|
|
7
17
|
Every task starts with understanding and ends with a validated, linted build. Do not skip steps.
|
|
@@ -222,6 +232,22 @@ store.updateAsset("key", "url")
|
|
|
222
232
|
store.updateState("key", "value")
|
|
223
233
|
```
|
|
224
234
|
|
|
235
|
+
### Logged-in user
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
const user = store.getUser() // Full user object, or `null` if logged out
|
|
239
|
+
store.getUserName("Guest") // Display name with fallback
|
|
240
|
+
store.getUserEmail() // Email or null
|
|
241
|
+
store.isAuthenticated() // boolean
|
|
242
|
+
// Or read the ref directly: store.user
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`store.user` is **`null` when no user is authenticated** — always guard
|
|
246
|
+
before dereferencing. Shape: `{ id, first_name, last_name, name, email,
|
|
247
|
+
avatar, roles[] }`. In `gxdev dev` a dummy authenticated user is injected
|
|
248
|
+
so the happy path renders without a backend; in production the platform
|
|
249
|
+
injects the real user.
|
|
250
|
+
|
|
225
251
|
## Real-Time Events
|
|
226
252
|
|
|
227
253
|
Every plugin has two streams of real-time events, both surfaced through the store:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# GxP Plugin — Claude Code Instructions
|
|
2
|
+
|
|
3
|
+
This is a GxP plugin project for the GxP kiosk platform. Use the shared
|
|
4
|
+
project guidelines below for everything you do here.
|
|
5
|
+
|
|
6
|
+
## Project guidelines (shared with Codex/Cursor)
|
|
7
|
+
|
|
8
|
+
@AGENTS.md
|
|
9
|
+
|
|
10
|
+
## Claude Code specifics
|
|
11
|
+
|
|
12
|
+
- **Subagent:** `.claude/agents/gxp-developer.md` — auto-invoked for GxP
|
|
13
|
+
plugin tasks. Use it for any non-trivial Vue/store/`callApi` work.
|
|
14
|
+
- **MCP servers:** wired in `.mcp.json` at the project root.
|
|
15
|
+
- `gxp-api` (via `mcp-serve` on PATH) — API specs, data models,
|
|
16
|
+
config/manifest editing, docs search, test helpers.
|
|
17
|
+
- `gxp-uikit-storybook` — UIKit component/story tools (only available
|
|
18
|
+
when `gxdev storybook` is running, served at
|
|
19
|
+
`http://localhost:6006/mcp`).
|
|
20
|
+
- **Settings:** `.claude/settings.json` — pre-allows the `gxp-api` MCP
|
|
21
|
+
tools so you don't get prompted on every call.
|
|
22
|
+
|
|
23
|
+
If the `api_*` / `config_*` / `docs_*` MCP tools aren't available, run
|
|
24
|
+
`claude mcp add gxp-api mcp-serve` and restart the session before
|
|
25
|
+
proceeding. Do not invent endpoints — discover them through the MCP.
|
package/template/GEMINI.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This is a GxP plugin project for the GxP kiosk platform built with Vue 3.
|
|
4
4
|
|
|
5
|
+
## Platform vs. plugin
|
|
6
|
+
|
|
7
|
+
The GxP platform owns admin configuration. Plugins consume it, they don't define it.
|
|
8
|
+
|
|
9
|
+
- **Forms, quizzes, surveys (including the quiz builder) are built in the platform admin UI.** The plugin reads admin-built fields/questions/scoring/leaderboards at runtime via `store.callApi` — operations like `forms.show`, `forms.fields.index`, `forms.responses.store`, `quiz.state`, `quiz.questions`, `quiz.answer`, `quiz.leaderboard`, `survey.metrics`. Use `api_list_operation_ids` / `search_api_endpoints` to discover the full set.
|
|
10
|
+
- **Project settings, assets, strings, dependencies, permissions, and the logged-in user** are injected into the GxP store at boot.
|
|
11
|
+
- The plugin's `app-manifest.json` + `configuration.json` describe only what the admin needs to configure for this plugin instance (text, images, colors, which form/quiz to bind to). Don't reinvent platform features locally.
|
|
12
|
+
|
|
5
13
|
## Development Workflow
|
|
6
14
|
|
|
7
15
|
Work through these steps in order. Do not skip.
|
|
@@ -157,6 +165,12 @@ store.updateString("key", "value")
|
|
|
157
165
|
store.updateSetting("key", "value")
|
|
158
166
|
store.updateAsset("key", "url")
|
|
159
167
|
store.updateState("key", "value")
|
|
168
|
+
|
|
169
|
+
// Logged-in user — returns `null` when no user is authenticated
|
|
170
|
+
store.getUser() // Full user object or null
|
|
171
|
+
store.getUserName("Guest") // Display name with fallback
|
|
172
|
+
store.getUserEmail() // Email or null
|
|
173
|
+
store.isAuthenticated() // boolean
|
|
160
174
|
```
|
|
161
175
|
|
|
162
176
|
## Real-Time Events
|