@gxp-dev/tools 2.0.72 → 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.
@@ -9,8 +9,9 @@
9
9
  * 3. Run interactive configuration:
10
10
  * - App name (prepopulated from package.json)
11
11
  * - Description (prepopulated from package.json)
12
- * - AI scaffolding (optional)
13
- * 4. Prompt to start the app
12
+ * - SSL setup
13
+ * - Launch AI agent (2nd-to-last replaces the start-server step when chosen)
14
+ * 4. Start Development (skipped if an AI agent was launched)
14
15
  * 5. Prompt to launch browser with extension
15
16
  */
16
17
 
@@ -34,6 +35,7 @@ const {
34
35
  generateSSLCertificates,
35
36
  updateEnvWithCertPaths,
36
37
  runAIScaffolding,
38
+ launchInteractiveAISession,
37
39
  getAvailableProviders,
38
40
  } = require("../utils")
39
41
 
@@ -401,65 +403,100 @@ async function runInteractiveConfig(projectPath, initialName, isLocal = false) {
401
403
  updatePackageJson(projectPath, appName, description)
402
404
  updateAppManifest(projectPath, appName, description)
403
405
 
404
- // 3. AI Scaffolding
406
+ // 3. SSL Setup
405
407
  console.log("")
406
408
  console.log("─".repeat(50))
407
- console.log("🤖 AI-Powered Scaffolding")
409
+ console.log("🔒 SSL Configuration")
408
410
  console.log("─".repeat(50))
409
- console.log(" Describe what you want to build and AI will generate")
410
- console.log(" starter components, views, and manifest configuration.")
411
+
412
+ const sslChoice = await arrowSelectPrompt(
413
+ "Set up SSL certificates for HTTPS development?",
414
+ [
415
+ {
416
+ label: "Yes, set up SSL",
417
+ value: "yes",
418
+ description: "Recommended for full feature access",
419
+ },
420
+ {
421
+ label: "Skip SSL setup",
422
+ value: "no",
423
+ description: "Can be set up later with npm run setup-ssl",
424
+ },
425
+ ],
426
+ )
427
+
428
+ let sslSetup = false
429
+ if (sslChoice === "yes") {
430
+ console.log("\n🔒 Setting up HTTPS development environment...")
431
+ ensureMkcertInstalled()
432
+ const certs = generateSSLCertificates(projectPath)
433
+ if (certs) {
434
+ updateEnvWithCertPaths(projectPath, certs)
435
+ sslSetup = true
436
+ }
437
+ }
438
+
439
+ // 4. Build with an AI agent (2nd-to-last step)
440
+ //
441
+ // Launches the selected CLI in interactive mode with an initial prompt
442
+ // that points the agent at AGENTS.md / GEMINI.md in the scaffolded project
443
+ // and instructs it to greet the user, ask clarifying questions until it
444
+ // has enough detail, then plan and implement. When an agent is launched
445
+ // we skip the "Start Development" step — the agent session replaces it.
446
+ console.log("")
447
+ console.log("─".repeat(50))
448
+ console.log("🤖 Build with an AI Agent")
449
+ console.log("─".repeat(50))
450
+ console.log(
451
+ " Launch an AI coding agent that already knows the GxP toolkit,",
452
+ )
453
+ console.log(
454
+ " MCP tools, and workflow. The agent will ask you what you want",
455
+ )
456
+ console.log(" to build and then plan and implement it with you.")
411
457
  console.log("")
412
458
 
413
459
  // Check available AI providers
414
460
  const providers = await getAvailableProviders()
415
- const availableProviders = providers.filter((p) => p.available)
461
+ const interactiveProviders = providers.filter(
462
+ (p) => p.available && (p.id !== "gemini" || p.method === "cli"),
463
+ )
416
464
 
417
465
  let aiChoice = "skip"
418
466
  let selectedProvider = null
419
467
 
420
- if (availableProviders.length === 0) {
421
- console.log(" ⚠️ No AI providers available.")
422
- console.log(" To enable AI scaffolding, set up one of:")
468
+ if (interactiveProviders.length === 0) {
469
+ console.log(" ⚠️ No AI CLIs detected.")
470
+ console.log(
471
+ " To use an AI agent here, install one of the following and retry:",
472
+ )
423
473
  console.log(
424
474
  " • Claude CLI: npm install -g @anthropic-ai/claude-code && claude login",
425
475
  )
426
476
  console.log(" • Codex CLI: npm install -g @openai/codex && codex auth")
427
477
  console.log(" • Gemini CLI: npm install -g @google/gemini-cli && gemini")
428
- console.log(" • Gemini API: export GEMINI_API_KEY=your_key")
429
478
  console.log("")
430
479
  aiChoice = "skip"
431
480
  } else {
432
481
  // Build provider options
433
- const providerOptions = [{ label: "Skip AI scaffolding", value: "skip" }]
434
-
435
- for (const provider of availableProviders) {
436
- let authInfo = ""
437
- if (provider.id === "gemini") {
438
- switch (provider.method) {
439
- case "cli":
440
- authInfo = "logged in"
441
- break
442
- case "api_key":
443
- authInfo = "via API key"
444
- break
445
- case "gcloud":
446
- authInfo = "via gcloud"
447
- break
448
- default:
449
- authInfo = ""
450
- }
451
- } else {
452
- authInfo = "logged in"
453
- }
482
+ const providerOptions = [
483
+ {
484
+ label: "Skip I'll build it myself",
485
+ value: "skip",
486
+ description: "You can launch an AI agent later from the project root",
487
+ },
488
+ ]
489
+
490
+ for (const provider of interactiveProviders) {
454
491
  providerOptions.push({
455
- label: `${provider.name}`,
492
+ label: provider.name,
456
493
  value: provider.id,
457
- description: `${authInfo}`,
494
+ description: "logged in",
458
495
  })
459
496
  }
460
497
 
461
498
  aiChoice = await arrowSelectPrompt(
462
- "Choose AI provider for scaffolding",
499
+ "Launch an AI agent to build your plugin?",
463
500
  providerOptions,
464
501
  )
465
502
  if (aiChoice !== "skip") {
@@ -467,55 +504,17 @@ async function runInteractiveConfig(projectPath, initialName, isLocal = false) {
467
504
  }
468
505
  }
469
506
 
470
- let buildPrompt = ""
507
+ // If an AI agent was chosen, it becomes the last step — skip starting the
508
+ // dev server. The agent is responsible for driving the rest of the session.
471
509
  if (selectedProvider) {
472
- buildPrompt = await multiLinePrompt(
473
- "📝 Describe your plugin (what it does, key features, UI elements):",
474
- "Press Enter twice when done",
510
+ await launchInteractiveAISession(
511
+ projectPath,
512
+ appName,
513
+ description,
514
+ selectedProvider,
475
515
  )
476
-
477
- if (buildPrompt) {
478
- await runAIScaffolding(
479
- projectPath,
480
- appName,
481
- description,
482
- buildPrompt,
483
- selectedProvider,
484
- )
485
- }
486
- }
487
-
488
- // 4. SSL Setup
489
- console.log("")
490
- console.log("─".repeat(50))
491
- console.log("🔒 SSL Configuration")
492
- console.log("─".repeat(50))
493
-
494
- const sslChoice = await arrowSelectPrompt(
495
- "Set up SSL certificates for HTTPS development?",
496
- [
497
- {
498
- label: "Yes, set up SSL",
499
- value: "yes",
500
- description: "Recommended for full feature access",
501
- },
502
- {
503
- label: "Skip SSL setup",
504
- value: "no",
505
- description: "Can be set up later with npm run setup-ssl",
506
- },
507
- ],
508
- )
509
-
510
- let sslSetup = false
511
- if (sslChoice === "yes") {
512
- console.log("\n🔒 Setting up HTTPS development environment...")
513
- ensureMkcertInstalled()
514
- const certs = generateSSLCertificates(projectPath)
515
- if (certs) {
516
- updateEnvWithCertPaths(projectPath, certs)
517
- sslSetup = true
518
- }
516
+ printFinalInstructions(projectPath, appName, sslSetup, isLocal)
517
+ return null
519
518
  }
520
519
 
521
520
  // 5. Start App
@@ -953,6 +953,141 @@ async function runAIScaffolding(
953
953
  return result.errors.length === 0
954
954
  }
955
955
 
956
+ /**
957
+ * Build the initial prompt sent to an interactive AI CLI session.
958
+ * The prompt anchors the agent in the scaffolded project, points it at
959
+ * the project's instruction files (which describe the full tool set),
960
+ * and tells it to keep asking questions until it has enough detail.
961
+ *
962
+ * @param {string} projectName - Plugin name
963
+ * @param {string} description - Plugin description
964
+ * @param {string} provider - Selected AI provider (claude, codex, gemini)
965
+ * @returns {string} Prompt text
966
+ */
967
+ function buildInteractiveInitialPrompt(projectName, description, provider) {
968
+ const instructionFile = provider === "gemini" ? "GEMINI.md" : "AGENTS.md"
969
+ const claudeAgentHint =
970
+ provider === "claude"
971
+ ? " You can also delegate to the `gxp-developer` subagent defined in `.claude/agents/gxp-developer.md`."
972
+ : ""
973
+
974
+ return [
975
+ `I just ran \`gxdev init\` to scaffold a new GxP plugin called "${projectName}"${
976
+ description ? ` (${description})` : ""
977
+ } in this directory.`,
978
+ "",
979
+ `Start a new GxP plugin development session. First read \`${instructionFile}\` and \`app-instructions.md\` in this project — they describe the workflow, conventions, and the full set of tools available to you.${claudeAgentHint}`,
980
+ "",
981
+ "You have the `gxp-api` MCP server available with 29 tools across five families:",
982
+ "- **API spec discovery** — `search_api_endpoints`, `api_list_operation_ids`, `api_get_operation_parameters`, `api_find_endpoints_by_schema`, `api_generate_dependency`, `get_endpoint_details`.",
983
+ "- **WebSocket events** — `api_find_events_for_operation` (maps an operationId to the AsyncAPI events it triggers), `api_list_events`, `search_websocket_events`.",
984
+ "- **Config editing** — `config_add_card`, `config_add_field`, `config_list_field_types`, `config_get_field_schema`, `config_extract_strings`, `config_validate`, etc. Every mutation is linter-guarded against the schemas in `bin/lib/lint/schemas/`.",
985
+ "- **Docs search** — `docs_search`, `docs_get_page`, `docs_list_pages` (full-text search across docs.gxp.dev).",
986
+ "- **Test helpers** — `test_scaffold_component_test`, `test_api_route`.",
987
+ "",
988
+ "Follow the full workflow from the instructions: (1) understand the feature, (2) discover data sources via MCP, (3) plan including the admin configuration form, (4) implement, (5) **sync the manifest and build the admin form**, (6) test with real broadcasts, (7) final `gxdev lint --all`.",
989
+ "",
990
+ "Step 5 is not optional. Every time you add or change a `store.callApi`, `store.listen`, `gxp-string`, or `gxp-src`, close the loop:",
991
+ '- Call `config_extract_strings` with `writeTo: "app-manifest.json"` — it scans `src/` and merges every directive, store getter, and dependency identifier into the manifest (same logic as `gxdev extract-config`, linter-guarded).',
992
+ "- For every entry now in the manifest, add a matching field in `configuration.json` using the MCP `config_*` tools. Default mapping: `strings.default.*` → `text`/`textarea`, `assets.*` → `selectAsset`, each declared `dependencies[]` identifier → `asyncSelect` bound to the resource's list endpoint, colors → `colorPicker`, numbers → `number`, toggles → `boolean`. Field `name` must match the manifest key exactly.",
993
+ "- Run `gxdev lint --all` before moving on.",
994
+ "",
995
+ "Do NOT start implementing until you have enough detail. Keep asking me clarifying questions until you know:",
996
+ "- The user-facing outcome and who uses it (attendee, staff, admin)",
997
+ "- What real-world data it reads/writes — identify the concrete platform operationIds via the MCP, never invent them",
998
+ "- Which real-time events matter (use `api_find_events_for_operation` for each planned operationId)",
999
+ "- Every piece of admin-editable content: strings, assets, colors/thresholds/settings, feature toggles",
1000
+ "",
1001
+ "Then propose a plan — screens/components, data flow, admin configuration form, and the exact keys you'll add to `app-manifest.json` — and get my confirmation before implementing.",
1002
+ "",
1003
+ "Begin now by greeting me briefly and asking what I want to build. Ask one focused question at a time rather than dumping a full questionnaire.",
1004
+ ].join("\n")
1005
+ }
1006
+
1007
+ /**
1008
+ * Launch the selected AI CLI in interactive mode with an initial prompt.
1009
+ * The user talks to the agent directly in the terminal; the agent is
1010
+ * responsible for eliciting the feature spec and building it.
1011
+ *
1012
+ * @param {string} projectPath - Project directory (cwd for the spawned CLI)
1013
+ * @param {string} projectName - Plugin name
1014
+ * @param {string} description - Plugin description
1015
+ * @param {string} provider - claude | codex | gemini
1016
+ * @returns {Promise<boolean>} True if the CLI exited successfully
1017
+ */
1018
+ function launchInteractiveAISession(
1019
+ projectPath,
1020
+ projectName,
1021
+ description,
1022
+ provider,
1023
+ ) {
1024
+ return new Promise((resolve) => {
1025
+ const initialPrompt = buildInteractiveInitialPrompt(
1026
+ projectName,
1027
+ description,
1028
+ provider,
1029
+ )
1030
+
1031
+ let command
1032
+ let args
1033
+
1034
+ switch (provider) {
1035
+ case "claude":
1036
+ command = "claude"
1037
+ args = [initialPrompt]
1038
+ break
1039
+ case "codex":
1040
+ command = "codex"
1041
+ args = [initialPrompt]
1042
+ break
1043
+ case "gemini":
1044
+ command = "gemini"
1045
+ args = ["-i", initialPrompt]
1046
+ break
1047
+ default:
1048
+ console.error(
1049
+ `❌ Interactive AI sessions are not supported for provider: ${provider}`,
1050
+ )
1051
+ resolve(false)
1052
+ return
1053
+ }
1054
+
1055
+ console.log("")
1056
+ console.log("─".repeat(50))
1057
+ console.log(`🚀 Launching ${provider} in interactive mode...`)
1058
+ console.log("─".repeat(50))
1059
+ console.log("")
1060
+ console.log(
1061
+ " The agent will read this project's instruction files, greet you,",
1062
+ )
1063
+ console.log(
1064
+ " and ask what you want to build. Answer its questions — it will",
1065
+ )
1066
+ console.log(
1067
+ " keep asking until it has enough detail, then plan, confirm, and",
1068
+ )
1069
+ console.log(" implement. Exit the agent when you're done.")
1070
+ console.log("")
1071
+
1072
+ const child = spawn(command, args, {
1073
+ cwd: projectPath,
1074
+ stdio: "inherit",
1075
+ shell: true,
1076
+ })
1077
+
1078
+ child.on("close", (code) => {
1079
+ console.log("")
1080
+ console.log(`✅ ${provider} session ended.`)
1081
+ resolve(code === 0)
1082
+ })
1083
+
1084
+ child.on("error", (err) => {
1085
+ console.error(`❌ Failed to launch ${provider}: ${err.message}`)
1086
+ resolve(false)
1087
+ })
1088
+ })
1089
+ }
1090
+
956
1091
  module.exports = {
957
1092
  SCAFFOLD_SYSTEM_PROMPT,
958
1093
  AI_PROVIDERS,
@@ -961,4 +1096,6 @@ module.exports = {
961
1096
  applyScaffold,
962
1097
  generateScaffold,
963
1098
  runAIScaffolding,
1099
+ buildInteractiveInitialPrompt,
1100
+ launchInteractiveAISession,
964
1101
  }
@@ -63,12 +63,40 @@ function searchEndpoints(spec, query) {
63
63
  }
64
64
 
65
65
  /**
66
- * Search AsyncAPI spec for channels/events matching a query
66
+ * Search AsyncAPI spec for channels/events matching a query.
67
+ *
68
+ * Matches across:
69
+ * - components.messages (event name, summary, description, x-triggered-by)
70
+ * - channels (channel name, description)
71
+ *
72
+ * For messages, the returned `eventName` is what you pass to
73
+ * store.listen(eventName, permissionIdentifier, callback) on the client.
67
74
  */
68
75
  function searchEvents(spec, query) {
69
76
  const results = []
70
77
  const queryLower = query.toLowerCase()
71
78
 
79
+ const messages = spec?.components?.messages || {}
80
+ for (const [eventName, message] of Object.entries(messages)) {
81
+ if (typeof message !== "object" || message === null) continue
82
+ const trigger = message["x-triggered-by"] || ""
83
+ if (
84
+ eventName.toLowerCase().includes(queryLower) ||
85
+ message.summary?.toLowerCase().includes(queryLower) ||
86
+ message.description?.toLowerCase().includes(queryLower) ||
87
+ trigger.toLowerCase().includes(queryLower)
88
+ ) {
89
+ results.push({
90
+ kind: "event",
91
+ eventName,
92
+ summary: message.summary || "",
93
+ description: message.description || "",
94
+ triggeredBy: trigger || null,
95
+ payloadRef: message.payload?.$ref || null,
96
+ })
97
+ }
98
+ }
99
+
72
100
  if (spec.channels) {
73
101
  for (const [channel, details] of Object.entries(spec.channels)) {
74
102
  if (
@@ -92,6 +120,7 @@ function searchEvents(spec, query) {
92
120
  }
93
121
 
94
122
  results.push({
123
+ kind: "channel",
95
124
  channel,
96
125
  description: details.description || "",
97
126
  operations,
@@ -200,7 +229,7 @@ const API_TOOLS = [
200
229
  {
201
230
  name: "search_websocket_events",
202
231
  description:
203
- "Search for WebSocket channels/events matching a query. Searches channel names and descriptions.",
232
+ "Search AsyncAPI events matching a query. Searches components.messages (event name, summary, description, x-triggered-by) and channel definitions. The returned eventName is what you pass to store.listen(eventName, permissionIdentifier, callback).",
204
233
  inputSchema: {
205
234
  type: "object",
206
235
  properties: {
@@ -158,6 +158,38 @@ const EXT_API_TOOLS = [
158
158
  required: [],
159
159
  },
160
160
  },
161
+ {
162
+ name: "api_find_events_for_operation",
163
+ description:
164
+ "Given an OpenAPI operationId, return every AsyncAPI message whose x-triggered-by matches. Use this after adding a callApi(operationId, ...) call to discover whether a socket event is fired server-side — subscribe to it with store.listen(eventName, permissionIdentifier, cb) instead of polling.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ operationId: {
169
+ type: "string",
170
+ description:
171
+ "OpenAPI operationId (e.g. 'posts.store'). Bare ids and 'portal.v1.project.<id>' are both accepted.",
172
+ },
173
+ },
174
+ required: ["operationId"],
175
+ },
176
+ },
177
+ {
178
+ name: "api_list_events",
179
+ description:
180
+ "List all AsyncAPI events from components.messages. Each entry includes the event name, summary/description, and x-triggered-by (if declared). Optionally filter by triggeredBy operationId.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ triggeredBy: {
185
+ type: "string",
186
+ description:
187
+ "Only return events whose x-triggered-by equals this operationId. Omit for all events.",
188
+ },
189
+ },
190
+ required: [],
191
+ },
192
+ },
161
193
  {
162
194
  name: "api_generate_dependency",
163
195
  description:
@@ -312,6 +344,50 @@ async function findEndpointsBySchema(filters) {
312
344
  return out
313
345
  }
314
346
 
347
+ function normalizeOperationId(id) {
348
+ if (!id) return id
349
+ return id.replace(/^portal\.v1\.project\./, "")
350
+ }
351
+
352
+ async function listAsyncApiEvents(triggeredBy) {
353
+ const spec = await fetchSpec("asyncapi")
354
+ const messages = spec?.components?.messages || {}
355
+ const targetTrigger = triggeredBy ? normalizeOperationId(triggeredBy) : null
356
+
357
+ const out = []
358
+ for (const [eventName, message] of Object.entries(messages)) {
359
+ if (typeof message !== "object" || message === null) continue
360
+ const trigger = message["x-triggered-by"] || null
361
+ if (targetTrigger && normalizeOperationId(trigger) !== targetTrigger) {
362
+ continue
363
+ }
364
+ out.push({
365
+ eventName,
366
+ summary: message.summary || "",
367
+ description: message.description || "",
368
+ triggeredBy: trigger,
369
+ payloadRef: message.payload?.$ref || null,
370
+ })
371
+ }
372
+ return out
373
+ }
374
+
375
+ async function findEventsForOperation(operationId) {
376
+ if (!operationId) {
377
+ return { ok: false, error: "operationId is required" }
378
+ }
379
+ const events = await listAsyncApiEvents(operationId)
380
+ return {
381
+ ok: true,
382
+ operationId: normalizeOperationId(operationId),
383
+ events,
384
+ note:
385
+ events.length === 0
386
+ ? "No AsyncAPI messages declare x-triggered-by for this operationId. Either the operation does not fire a platform event, or the spec has not declared the trigger yet."
387
+ : "Subscribe with store.listen(eventName, permissionIdentifier, callback) to receive these events live.",
388
+ }
389
+ }
390
+
315
391
  async function generateDependency({
316
392
  identifier,
317
393
  tag,
@@ -429,6 +505,14 @@ async function handleExtApiToolCall(name, args = {}) {
429
505
  results: await findEndpointsBySchema(args || {}),
430
506
  })
431
507
 
508
+ case "api_find_events_for_operation":
509
+ return contentResult(await findEventsForOperation(args.operationId))
510
+
511
+ case "api_list_events":
512
+ return contentResult({
513
+ events: await listAsyncApiEvents(args.triggeredBy),
514
+ })
515
+
432
516
  case "api_generate_dependency":
433
517
  return contentResult(await generateDependency(args))
434
518
 
@@ -453,4 +537,7 @@ module.exports = {
453
537
  getOperationParameters,
454
538
  findEndpointsBySchema,
455
539
  generateDependency,
540
+ listAsyncApiEvents,
541
+ findEventsForOperation,
542
+ normalizeOperationId,
456
543
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.72",
3
+ "version": "2.0.73",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {