@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 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
  )
@@ -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
- console.log("📋 Loading environment variables from .env file")
227
+ logger.info("📋 Loading environment variables from .env file")
109
228
  dotenv.config({ path: envPath })
110
229
  } else if (fs.existsSync(envExamplePath)) {
111
- console.log(
230
+ logger.info(
112
231
  "💡 Tip: Create .env file from .env.example to customize your environment settings",
113
232
  )
114
- console.log(" cp .env.example .env")
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
- console.log(
246
+ logger.warn(
128
247
  "⚠ SSL certificates not found. Run 'npm run setup-ssl' to enable HTTPS",
129
248
  )
130
- console.log("🌐 Starting HTTP development server...")
249
+ logger.info("🌐 Starting HTTP development server...")
131
250
  useHttps = false
132
251
  } else {
133
- console.log("🔒 Starting HTTPS development server...")
134
- console.log(
252
+ logger.info("🔒 Starting HTTPS development server...")
253
+ logger.info(
135
254
  `📁 Using certificate: ${path.basename(existingCerts.certPath)}`,
136
255
  )
137
- console.log(`🔑 Using key: ${path.basename(existingCerts.keyPath)}`)
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
- console.log("🌐 Starting HTTP development server...")
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
- console.log(`🌐 Development server will start on port: ${finalPort}`)
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
- console.log("🎭 Mock API will be enabled")
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
- console.warn("⚠ server.js not found. Skipping Socket.IO server.")
281
+ logger.warn("⚠ server.js not found. Skipping Socket.IO server.")
163
282
  } else {
164
283
  serverJsPath = serverJs.path
165
- console.log(
284
+ logger.info(
166
285
  `📡 Starting Socket.IO server with nodemon... (${
167
286
  serverJs.isLocal ? "local" : "package"
168
287
  } version)`,
169
288
  )
170
- console.log(`📁 Using: ${serverJsPath}`)
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
- console.log("📁 Using local index.html")
306
+ logger.info("📁 Using local index.html")
188
307
  }
189
308
  if (hasLocalMainJs) {
190
- console.log("📁 Using local main.js")
309
+ logger.info("📁 Using local main.js")
191
310
  }
192
311
  if (hasLocalExtend) {
193
- console.log("🧩 Extending vite config from vite.extend.js")
312
+ logger.info("🧩 Extending vite config from vite.extend.js")
194
313
  }
195
314
  if (!hasLocalIndexHtml && !hasLocalMainJs && !hasLocalExtend) {
196
- console.log(
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
- console.log("🦊 Firefox extension will launch with dev server")
238
- console.log(`📁 Extension path: ${firefoxConfig.extensionPath}`)
239
- console.log(`🌐 Start URL: ${firefoxConfig.startUrl}`)
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
- console.warn("⚠️ Firefox extension not found, skipping")
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
- console.log("🚀 Chrome extension will launch with dev server")
252
- console.log(`📁 Extension path: ${chromeConfig.extensionPath}`)
253
- console.log(`🌐 Start URL: ${chromeConfig.startUrl}`)
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
- console.warn("⚠️ Chrome extension not found, skipping")
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
- // Vite is always included
276
- const viteCommand = `npx vite dev --config "${normalizedViteConfigPath}"`
277
- processes.push(`"${viteCommand}"`)
278
- names.push("VITE")
279
- colors.push("cyan")
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
- processes.push(`"npx nodemon \\"${normalizedServerPath}\\""`)
285
- names.push("SOCKET")
286
- colors.push("green")
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
- processes.push(`"${firefoxConfig.command}"`)
292
- names.push(firefoxConfig.name)
293
- colors.push(firefoxConfig.color)
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
- processes.push(`"${chromeConfig.command}"`)
299
- names.push(chromeConfig.name)
300
- colors.push(chromeConfig.color)
412
+ services.push({
413
+ name: chromeConfig.name,
414
+ color: chromeConfig.color,
415
+ command: chromeConfig.command,
416
+ })
301
417
  }
302
418
 
303
- // Build the final command
304
- if (processes.length > 1) {
305
- // Use concurrently to run multiple processes
306
- command = `npx concurrently --names "${names.join(
307
- ",",
308
- )}" --prefix-colors "${colors.join(",")}" ${processes.join(" ")}`
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
- // Just run Vite dev server alone
311
- command = `npx vite dev --config "${normalizedViteConfigPath}"`
434
+ command = services[0].command
312
435
  }
313
436
 
314
437
  shell.exec(command)
@@ -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",
@@ -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
- { capabilities: { tools: {} } },
352
+ {
353
+ capabilities: { tools: {} },
354
+ instructions: SERVER_DESCRIPTION,
355
+ },
352
356
  )
353
357
 
354
358
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.86",
3
+ "version": "2.0.88",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -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.
@@ -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.
@@ -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