@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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-pointer operations + GxP-aware walkers used by the MCP config tools.
|
|
3
|
+
*
|
|
4
|
+
* Paths are RFC-6901 JSON pointers:
|
|
5
|
+
* "/additionalTabs/0/cards/1/fieldsList/2"
|
|
6
|
+
* Empty or "/" means "the whole document".
|
|
7
|
+
*
|
|
8
|
+
* All functions are pure: they operate on a parsed doc and return either a
|
|
9
|
+
* value (readers) or a new doc (writers). No file IO here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function parsePointer(pointer) {
|
|
13
|
+
if (pointer === "" || pointer === "/") {
|
|
14
|
+
return []
|
|
15
|
+
}
|
|
16
|
+
if (!pointer.startsWith("/")) {
|
|
17
|
+
throw new Error(`Invalid JSON pointer (must start with "/"): ${pointer}`)
|
|
18
|
+
}
|
|
19
|
+
return pointer
|
|
20
|
+
.slice(1)
|
|
21
|
+
.split("/")
|
|
22
|
+
.map((segment) =>
|
|
23
|
+
decodeURIComponent(segment.replace(/~1/g, "/").replace(/~0/g, "~")),
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function encodeSegment(segment) {
|
|
28
|
+
return String(segment).replace(/~/g, "~0").replace(/\//g, "~1")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildPointer(segments) {
|
|
32
|
+
if (!segments.length) return ""
|
|
33
|
+
return "/" + segments.map(encodeSegment).join("/")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isIndex(segment) {
|
|
37
|
+
return /^\d+$/.test(segment)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getByPointer(doc, pointer) {
|
|
41
|
+
const segments = parsePointer(pointer)
|
|
42
|
+
let cur = doc
|
|
43
|
+
for (const seg of segments) {
|
|
44
|
+
if (cur === null || cur === undefined) return undefined
|
|
45
|
+
if (Array.isArray(cur)) {
|
|
46
|
+
if (!isIndex(seg)) return undefined
|
|
47
|
+
cur = cur[Number(seg)]
|
|
48
|
+
} else if (typeof cur === "object") {
|
|
49
|
+
cur = cur[seg]
|
|
50
|
+
} else {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return cur
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getParent(doc, pointer) {
|
|
58
|
+
const segments = parsePointer(pointer)
|
|
59
|
+
if (!segments.length) {
|
|
60
|
+
throw new Error("Cannot get parent of root")
|
|
61
|
+
}
|
|
62
|
+
const last = segments[segments.length - 1]
|
|
63
|
+
const parent = getByPointer(doc, buildPointer(segments.slice(0, -1)))
|
|
64
|
+
return { parent, key: last, isIndex: isIndex(last) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Produce a deep-cloned document with the value at `pointer` set to `value`.
|
|
69
|
+
* Throws if the parent doesn't exist.
|
|
70
|
+
*/
|
|
71
|
+
function setByPointer(doc, pointer, value) {
|
|
72
|
+
const clone = structuredClone(doc)
|
|
73
|
+
const segments = parsePointer(pointer)
|
|
74
|
+
if (!segments.length) {
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
let cur = clone
|
|
78
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
79
|
+
const seg = segments[i]
|
|
80
|
+
if (cur[seg] === undefined || cur[seg] === null) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Path does not exist at "${buildPointer(segments.slice(0, i + 1))}"`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
cur = cur[seg]
|
|
86
|
+
}
|
|
87
|
+
const last = segments[segments.length - 1]
|
|
88
|
+
if (Array.isArray(cur) && isIndex(last)) {
|
|
89
|
+
cur[Number(last)] = value
|
|
90
|
+
} else {
|
|
91
|
+
cur[last] = value
|
|
92
|
+
}
|
|
93
|
+
return clone
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function deleteByPointer(doc, pointer) {
|
|
97
|
+
const clone = structuredClone(doc)
|
|
98
|
+
const segments = parsePointer(pointer)
|
|
99
|
+
if (!segments.length) {
|
|
100
|
+
throw new Error("Cannot delete root")
|
|
101
|
+
}
|
|
102
|
+
let cur = clone
|
|
103
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
104
|
+
cur = cur[segments[i]]
|
|
105
|
+
if (cur === undefined) {
|
|
106
|
+
throw new Error(`Path does not exist: ${pointer}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const last = segments[segments.length - 1]
|
|
110
|
+
if (Array.isArray(cur) && isIndex(last)) {
|
|
111
|
+
cur.splice(Number(last), 1)
|
|
112
|
+
} else {
|
|
113
|
+
delete cur[last]
|
|
114
|
+
}
|
|
115
|
+
return clone
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Insert a value into an array at `pointer` (must point to an array) at the
|
|
120
|
+
* given position. `position` may be an integer or "end".
|
|
121
|
+
*/
|
|
122
|
+
function insertAt(doc, arrayPointer, item, position = "end") {
|
|
123
|
+
const clone = structuredClone(doc)
|
|
124
|
+
const arr = getByPointer(clone, arrayPointer)
|
|
125
|
+
if (!Array.isArray(arr)) {
|
|
126
|
+
throw new Error(`Pointer must reference an array: ${arrayPointer}`)
|
|
127
|
+
}
|
|
128
|
+
if (position === "end" || position === undefined) {
|
|
129
|
+
arr.push(item)
|
|
130
|
+
return { doc: clone, index: arr.length - 1 }
|
|
131
|
+
}
|
|
132
|
+
const idx = Number(position)
|
|
133
|
+
if (!Number.isInteger(idx) || idx < 0 || idx > arr.length) {
|
|
134
|
+
throw new Error(`Invalid position: ${position}`)
|
|
135
|
+
}
|
|
136
|
+
arr.splice(idx, 0, item)
|
|
137
|
+
return { doc: clone, index: idx }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Move an item from one pointer to another. Both must point to items inside
|
|
142
|
+
* arrays; targetArray is the pointer to the destination array.
|
|
143
|
+
*/
|
|
144
|
+
function moveItem(doc, fromPointer, targetArrayPointer, position = "end") {
|
|
145
|
+
const item = getByPointer(doc, fromPointer)
|
|
146
|
+
if (item === undefined) {
|
|
147
|
+
throw new Error(`Source does not exist: ${fromPointer}`)
|
|
148
|
+
}
|
|
149
|
+
const withoutItem = deleteByPointer(doc, fromPointer)
|
|
150
|
+
|
|
151
|
+
// If the move is within the same array and we're shifting forward, the
|
|
152
|
+
// source-removal may have reindexed the target. We recompute here: parse
|
|
153
|
+
// the target array pointer and verify it still exists after removal.
|
|
154
|
+
const targetArr = getByPointer(withoutItem, targetArrayPointer)
|
|
155
|
+
if (!Array.isArray(targetArr)) {
|
|
156
|
+
throw new Error(`Target must be an array: ${targetArrayPointer}`)
|
|
157
|
+
}
|
|
158
|
+
return insertAt(withoutItem, targetArrayPointer, item, position)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Recursively walk a configuration document and return a flat list of all
|
|
163
|
+
* cards with their JSON pointer, type, title, and a summary of children.
|
|
164
|
+
*
|
|
165
|
+
* Cards are discovered under:
|
|
166
|
+
* additionalTabs[] (root array of cards)
|
|
167
|
+
* <card>.cards[] (card_list)
|
|
168
|
+
* <card>.tabsList[].cards[] (tabs_list)
|
|
169
|
+
*/
|
|
170
|
+
function listCards(doc) {
|
|
171
|
+
const out = []
|
|
172
|
+
|
|
173
|
+
function walk(node, pointer) {
|
|
174
|
+
if (!node || typeof node !== "object") return
|
|
175
|
+
|
|
176
|
+
// If node looks like a card (has `type`), record it.
|
|
177
|
+
if (typeof node.type === "string") {
|
|
178
|
+
out.push({
|
|
179
|
+
path: pointer,
|
|
180
|
+
type: node.type,
|
|
181
|
+
title: node.title ?? null,
|
|
182
|
+
tabId: node.tabId ?? null,
|
|
183
|
+
fieldCount: Array.isArray(node.fieldsList) ? node.fieldsList.length : 0,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (Array.isArray(node.cards)) {
|
|
188
|
+
node.cards.forEach((c, i) => walk(c, `${pointer}/cards/${i}`))
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(node.tabsList)) {
|
|
191
|
+
node.tabsList.forEach((tab, i) => {
|
|
192
|
+
if (Array.isArray(tab.cards)) {
|
|
193
|
+
tab.cards.forEach((c, j) =>
|
|
194
|
+
walk(c, `${pointer}/tabsList/${i}/cards/${j}`),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(doc?.additionalTabs)) {
|
|
202
|
+
doc.additionalTabs.forEach((c, i) => walk(c, `/additionalTabs/${i}`))
|
|
203
|
+
}
|
|
204
|
+
return out
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* List all fields under a fields_list card, with their JSON pointer.
|
|
209
|
+
*/
|
|
210
|
+
function listFields(doc, cardPointer) {
|
|
211
|
+
const card = getByPointer(doc, cardPointer)
|
|
212
|
+
if (!card || !Array.isArray(card.fieldsList)) {
|
|
213
|
+
return []
|
|
214
|
+
}
|
|
215
|
+
return card.fieldsList.map((f, i) => ({
|
|
216
|
+
path: `${cardPointer}/fieldsList/${i}`,
|
|
217
|
+
type: f.type,
|
|
218
|
+
name: f.name ?? null,
|
|
219
|
+
label: f.label ?? null,
|
|
220
|
+
}))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
parsePointer,
|
|
225
|
+
buildPointer,
|
|
226
|
+
getByPointer,
|
|
227
|
+
getParent,
|
|
228
|
+
setByPointer,
|
|
229
|
+
deleteByPointer,
|
|
230
|
+
insertAt,
|
|
231
|
+
moveItem,
|
|
232
|
+
listCards,
|
|
233
|
+
listFields,
|
|
234
|
+
}
|