@postman/postman-plugin 0.2.2 → 0.4.0
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 +58 -10
- package/dist/hosts/antigravity.js +47 -0
- package/dist/hosts/copilot.js +60 -0
- package/dist/hosts/index.js +3 -1
- package/dist/hosts/opencode-config.js +147 -0
- package/dist/hosts/opencode.js +315 -34
- package/dist/hosts/shared.js +10 -3
- package/dist/source.js +19 -5
- package/dist/system.js +20 -1
- package/hooks/session-start-context.md +1 -1
- package/mcp.pi.json +2 -2
- package/package.json +1 -1
- package/skills/ai-readiness/SKILL.md +40 -44
- package/skills/api-discovery/SKILL.md +64 -129
- package/skills/api-discovery/reference/orbit.md +17 -5
- package/skills/api-documentation/SKILL.md +38 -25
- package/skills/api-engineer/SKILL.md +78 -23
- package/skills/api-mocking/SKILL.md +117 -135
- package/skills/api-monitoring/SKILL.md +52 -129
- package/skills/api-simulation/SKILL.md +138 -0
- package/skills/api-simulation/reference/sim_yaml_schema.md +50 -0
- package/skills/api-testing/SKILL.md +48 -95
- package/skills/bootstrap/SKILL.md +123 -210
- package/skills/bootstrap/reference/cli_installation.md +4 -3
- package/skills/ci-integration/SKILL.md +53 -112
- package/skills/collection-schema-v3/SKILL.md +43 -204
- package/skills/collection-schema-v3/reference/http.md +150 -0
- package/skills/datasets/SKILL.md +89 -314
- package/skills/datasets/reference/spreadsheets.md +25 -0
- package/skills/flows/SKILL.md +57 -206
- package/skills/flows/reference/flow_cli_flags.md +3 -2
- package/skills/performance-testing/SKILL.md +46 -62
- package/skills/performance-testing/references/jmeter-import/README.md +113 -0
- package/skills/performance-testing/references/jmeter-import/import-report.md +52 -0
- package/skills/performance-testing/references/jmeter-import/mapping-table.md +97 -0
- package/skills/performance-testing/references/jmeter-import/postman-perf-capabilities.md +56 -0
- package/skills/performance-testing/references/jmeter-import/script-translation.md +98 -0
- package/skills/performance-testing/references/jmeter-import/scripts/groovy_to_js.py +83 -0
- package/skills/performance-testing/references/jmeter-import/scripts/jmx_to_postman.py +1433 -0
- package/skills/postman-mcp-server/SKILL.md +53 -59
- package/skills/postman-mcp-server/references/docs.md +0 -1
- package/skills/postman-mcp-server/references/learn.md +10 -9
- package/skills/postman-mcp-server/references/mock.md +0 -1
- package/skills/postman-mcp-server/references/search.md +0 -1
- package/skills/postman-mcp-server/references/security.md +0 -1
- package/skills/postman-mcp-server/references/setup.md +0 -1
- package/skills/postman-mcp-server/references/sync.md +0 -1
- package/skills/postman-mcp-server/references/test.md +0 -1
- package/skills/variables/SKILL.md +63 -0
- package/skills/webhook-testing/SKILL.md +103 -0
- package/skills/webhook-testing/reference/forwarding.md +10 -0
- package/skills/webhook-testing/reference/outbound_webhooks.md +25 -0
- package/skills/webhook-testing/reference/webhook_scripts.md +163 -0
package/README.md
CHANGED
|
@@ -24,12 +24,22 @@ Install Postman in every compatible coding agent detected on your machine:
|
|
|
24
24
|
npx @postman/postman-plugin
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
One command configures **Claude Code, Codex,
|
|
28
|
-
OpenCode and Pi**. Run it again to update, `status` to see what's installed,
|
|
27
|
+
One command configures **Antigravity, Claude Code, Codex, GitHub Copilot CLI, Cursor,
|
|
28
|
+
Factory Droid, Kimi Code, OpenCode and Pi**. Run it again to update, `status` to see what's installed,
|
|
29
29
|
and `remove` to uninstall; `--agent <id>` limits any of them to one agent.
|
|
30
30
|
|
|
31
31
|
You can also use the following commands to install individually:
|
|
32
32
|
|
|
33
|
+
### Antigravity
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
agy plugin install https://github.com/postmanlabs/postman-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run the same command again to update; `agy plugin uninstall postman` removes it.
|
|
40
|
+
In the Antigravity app, **Customizations → Plugins → + Install from URL** takes
|
|
41
|
+
the same URL.
|
|
42
|
+
|
|
33
43
|
### Claude Code
|
|
34
44
|
|
|
35
45
|
[View Postman on Claude Plugins](https://claude.com/plugins/postman)
|
|
@@ -60,6 +70,16 @@ codex plugin add postman@postman
|
|
|
60
70
|
/add-plugin postman
|
|
61
71
|
```
|
|
62
72
|
|
|
73
|
+
### GitHub Copilot CLI
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
copilot plugin marketplace add postmanlabs/postman-plugin
|
|
77
|
+
copilot plugin install postman@postman
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`copilot plugin marketplace update postman`, then `copilot plugin update postman`, updates it. Sign in to Postman's MCP server
|
|
81
|
+
with `/mcp` inside a Copilot CLI session.
|
|
82
|
+
|
|
63
83
|
### Factory Droid
|
|
64
84
|
|
|
65
85
|
```bash
|
|
@@ -85,8 +105,14 @@ Postman's MCP server with `/mcp-config login plugin-postman:postman`.
|
|
|
85
105
|
|
|
86
106
|
### OpenCode
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
```bash
|
|
109
|
+
opencode plugin add github:postmanlabs/postman-plugin
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
That is OpenCode 2.0.4 or later. On OpenCode 1.14.33 or later the command is
|
|
113
|
+
`opencode plugin --global github:postmanlabs/postman-plugin`.
|
|
114
|
+
[opencode/README.md](https://github.com/postmanlabs/postman-plugin/blob/main/opencode/README.md#install) has how to update and remove
|
|
115
|
+
it, and the older clone-and-loader install.
|
|
90
116
|
|
|
91
117
|
### Pi
|
|
92
118
|
|
|
@@ -166,17 +192,39 @@ test consumers without waiting for the real service to be ready or available.
|
|
|
166
192
|
|
|
167
193
|
The mock stays local until you choose to push and deploy it. When teammates or
|
|
168
194
|
external systems need access, the same mock can become a durable hosted URL
|
|
169
|
-
without rebuilding it in another tool.
|
|
195
|
+
without rebuilding it in another tool. Pushed to the service's own workspace,
|
|
196
|
+
it's the mock other teams' simulations pull first.
|
|
197
|
+
|
|
198
|
+
### Develop against simulated dependencies
|
|
199
|
+
|
|
200
|
+
The [`api-simulation`](https://github.com/postmanlabs/postman-plugin/blob/main/skills/api-simulation/) skill runs your code (a service
|
|
201
|
+
you're changing, a frontend, a test suite or a demo) for real against stand-ins
|
|
202
|
+
for the HTTP services it calls, served together on one port from one
|
|
203
|
+
`.sim.yaml`. Each stand-in is the owning team's mock when they publish one.
|
|
204
|
+
It reuses an existing `.sim.yaml`, proves a change with a request through the
|
|
205
|
+
changed path, and answers "what happens if X goes down" by running each failure.
|
|
206
|
+
Pushing the simulation is optional and needs your approval. It needs Postman CLI
|
|
207
|
+
1.70.0 or later:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
postman simulation run postman/simulations/orders-dev.sim.yaml --port 4900
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The service under test is never mocked. It runs for real, so a passing run
|
|
214
|
+
shows the change works against what the mocks model; it doesn't replace a run
|
|
215
|
+
against the real upstreams. The simulation can also run in CI, and any
|
|
216
|
+
dependency can take injected latency, error, rate-limit, or chaos conditions to
|
|
217
|
+
exercise failure paths.
|
|
170
218
|
|
|
171
219
|
|
|
172
220
|
## How Postman builds context
|
|
173
221
|
|
|
174
222
|
Work done in a Postman folder produces artifacts: collection runs, lint
|
|
175
223
|
results, request history, mock deployments. When that folder is connected to a
|
|
176
|
-
Postman workspace
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
did.
|
|
224
|
+
Postman workspace after `postman init`, those artifacts are published to it as
|
|
225
|
+
they are produced. The workspace persists that context for future work and
|
|
226
|
+
enables teammates, reviewers, and other agents to share and build on what the
|
|
227
|
+
agent did.
|
|
180
228
|
|
|
181
229
|
What each command contributes to the connected workspace:
|
|
182
230
|
|
|
@@ -185,7 +233,7 @@ What each command contributes to the connected workspace:
|
|
|
185
233
|
| `postman collection run` | Run results and run history |
|
|
186
234
|
| `postman application test` | Test run results |
|
|
187
235
|
| `postman spec lint` | Lint results, including violation counts and pass/fail |
|
|
188
|
-
| `postman workspace push` |
|
|
236
|
+
| `postman workspace push` | Local workspace contents, persisted as context for future work and sharing with others |
|
|
189
237
|
| `postman runner start` | Runner processing and health activity |
|
|
190
238
|
| `postman flows run` | Flow run results |
|
|
191
239
|
| `postman request` | Agent's API interaction activity |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { REPO } from '../source.js';
|
|
2
|
+
import { guard, mustProbeJson, mustRun, parseJson } from './shared.js';
|
|
3
|
+
import { result } from './types.js';
|
|
4
|
+
// `agy plugin install` clones a git URL into ~/.gemini/config/plugins/<name>. It has no update
|
|
5
|
+
// command; installing again replaces the copy in place, so a re-run is the update.
|
|
6
|
+
const PLUGIN_NAME = 'postman', SOURCE = `https://github.com/${REPO}`, NEXT = 'Restart Antigravity for the change to take effect.';
|
|
7
|
+
const isOurs = (plugin) => plugin.name === PLUGIN_NAME;
|
|
8
|
+
export const antigravity = {
|
|
9
|
+
id: 'antigravity',
|
|
10
|
+
name: 'Antigravity',
|
|
11
|
+
route: '.antigravity-plugin',
|
|
12
|
+
async detect(system) {
|
|
13
|
+
return (await system.which('agy')) !== null;
|
|
14
|
+
},
|
|
15
|
+
async status(system) {
|
|
16
|
+
const exec = await system.probe('agy', ['plugin', 'list', '--json']), plugins = exec.code === 0 ? parseJson(exec.stdout) : null;
|
|
17
|
+
if (!Array.isArray(plugins)) {
|
|
18
|
+
return { installed: null, detail: 'could not read `agy plugin list --json`', notes: [] };
|
|
19
|
+
}
|
|
20
|
+
const installed = plugins.find(isOurs);
|
|
21
|
+
if (!installed) {
|
|
22
|
+
return { installed: false, detail: 'not installed', notes: [] };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
installed: true,
|
|
26
|
+
detail: `${PLUGIN_NAME} ${installed.version ?? ''}`.trim(),
|
|
27
|
+
notes: installed.enabled === false ? ['installed but disabled; run `agy plugin enable postman`'] : []
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
install(system) {
|
|
31
|
+
return guard(async () => {
|
|
32
|
+
const plugins = await mustProbeJson(system, 'agy', ['plugin', 'list', '--json']), current = plugins.some(isOurs);
|
|
33
|
+
await mustRun(system, 'agy', ['plugin', 'install', SOURCE]);
|
|
34
|
+
return result('done', `${current ? 'updated' : 'installed'} ${PLUGIN_NAME}`, NEXT);
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
remove(system) {
|
|
38
|
+
return guard(async () => {
|
|
39
|
+
const plugins = await mustProbeJson(system, 'agy', ['plugin', 'list', '--json']);
|
|
40
|
+
if (!plugins.some(isOurs)) {
|
|
41
|
+
return result('skipped', 'not installed');
|
|
42
|
+
}
|
|
43
|
+
await mustRun(system, 'agy', ['plugin', 'uninstall', PLUGIN_NAME]);
|
|
44
|
+
return result('done', `uninstalled ${PLUGIN_NAME}`, NEXT);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { REPO } from '../source.js';
|
|
2
|
+
import { guard, mustProbeJson, mustRun, parseJson } from './shared.js';
|
|
3
|
+
import { result } from './types.js';
|
|
4
|
+
// Copilot CLI is deprecating direct installs (repos, URLs, local paths), so this installs from this
|
|
5
|
+
// repo as a marketplace, which Copilot reads from `.claude-plugin/marketplace.json`. Once Postman is
|
|
6
|
+
// listed in awesome-copilot, which Copilot ships registered, `postman@awesome-copilot` replaces it.
|
|
7
|
+
const PLUGIN_NAME = 'postman', MARKETPLACE = 'postman', PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE}`, NEXT = 'Restart Copilot CLI (or run /restart) for the change to take effect.';
|
|
8
|
+
const isOurs = (plugin) => plugin.name === PLUGIN_NAME,
|
|
9
|
+
// A direct install has no marketplace; it would load the same skills a second time.
|
|
10
|
+
isDirect = (plugin) => isOurs(plugin) && !plugin.marketplace;
|
|
11
|
+
export const copilot = {
|
|
12
|
+
id: 'copilot',
|
|
13
|
+
name: 'GitHub Copilot CLI',
|
|
14
|
+
route: '.github/plugin',
|
|
15
|
+
async detect(system) {
|
|
16
|
+
return (await system.which('copilot')) !== null;
|
|
17
|
+
},
|
|
18
|
+
async status(system) {
|
|
19
|
+
const exec = await system.probe('copilot', ['plugin', 'list', '--json']), plugins = exec.code === 0 ? parseJson(exec.stdout) : null;
|
|
20
|
+
if (!Array.isArray(plugins)) {
|
|
21
|
+
return { installed: null, detail: 'could not read `copilot plugin list --json`', notes: [] };
|
|
22
|
+
}
|
|
23
|
+
const installed = plugins.find(isOurs);
|
|
24
|
+
if (!installed) {
|
|
25
|
+
return { installed: false, detail: 'not installed', notes: [] };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
installed: true,
|
|
29
|
+
detail: `${PLUGIN_NAME} ${installed.version ?? ''}`.trim(),
|
|
30
|
+
notes: installed.enabled === false ? ['installed but disabled; run `copilot plugin enable postman`'] : []
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
install(system) {
|
|
34
|
+
return guard(async () => {
|
|
35
|
+
const plugins = await mustProbeJson(system, 'copilot', ['plugin', 'list', '--json']), marketplaces = await mustProbeJson(system, 'copilot', ['plugin', 'marketplace', 'list', '--json']), current = plugins.some((plugin) => isOurs(plugin) && plugin.marketplace === MARKETPLACE);
|
|
36
|
+
if (marketplaces.some((marketplace) => marketplace.name === MARKETPLACE)) {
|
|
37
|
+
await mustRun(system, 'copilot', ['plugin', 'marketplace', 'update', MARKETPLACE]);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
await mustRun(system, 'copilot', ['plugin', 'marketplace', 'add', REPO]);
|
|
41
|
+
}
|
|
42
|
+
// The same name can't be installed twice, so a direct install goes first.
|
|
43
|
+
if (plugins.some(isDirect)) {
|
|
44
|
+
await mustRun(system, 'copilot', ['plugin', 'uninstall', PLUGIN_NAME]);
|
|
45
|
+
}
|
|
46
|
+
await mustRun(system, 'copilot', current ? ['plugin', 'update', PLUGIN_NAME] : ['plugin', 'install', PLUGIN_ID]);
|
|
47
|
+
return result('done', `${current ? 'updated' : 'installed'} ${PLUGIN_ID}`, NEXT);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
remove(system) {
|
|
51
|
+
return guard(async () => {
|
|
52
|
+
const plugins = await mustProbeJson(system, 'copilot', ['plugin', 'list', '--json']);
|
|
53
|
+
if (!plugins.some(isOurs)) {
|
|
54
|
+
return result('skipped', 'not installed');
|
|
55
|
+
}
|
|
56
|
+
await mustRun(system, 'copilot', ['plugin', 'uninstall', PLUGIN_NAME]);
|
|
57
|
+
return result('done', `uninstalled ${PLUGIN_NAME}`, NEXT);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
package/dist/hosts/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { antigravity } from './antigravity.js';
|
|
1
2
|
import { claudeCode } from './claude-code.js';
|
|
2
3
|
import { codex } from './codex.js';
|
|
4
|
+
import { copilot } from './copilot.js';
|
|
3
5
|
import { cursor } from './cursor.js';
|
|
4
6
|
import { factory } from './factory.js';
|
|
5
7
|
import { kimi } from './kimi.js';
|
|
6
8
|
import { opencode } from './opencode.js';
|
|
7
9
|
import { pi } from './pi.js';
|
|
8
|
-
export const HOSTS = [claudeCode, codex, cursor, factory, kimi, opencode, pi];
|
|
10
|
+
export const HOSTS = [antigravity, claudeCode, codex, copilot, cursor, factory, kimi, opencode, pi];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// OpenCode's config is JSONC: comments and trailing commas are allowed, and users edit it by hand.
|
|
2
|
+
// Reading it and removing one array entry from it must therefore leave everything else as written.
|
|
3
|
+
const PUNCTUATION = '{}[]:,', WHITESPACE = /\s/, WHITESPACE_ONLY = /^\s*$/;
|
|
4
|
+
function tokenize(text) {
|
|
5
|
+
const tokens = [];
|
|
6
|
+
let index = text.charCodeAt(0) === 0xFEFF ? 1 : 0;
|
|
7
|
+
while (index < text.length) {
|
|
8
|
+
const char = text[index];
|
|
9
|
+
if (WHITESPACE.test(char)) {
|
|
10
|
+
index += 1;
|
|
11
|
+
}
|
|
12
|
+
else if (text.startsWith('//', index)) {
|
|
13
|
+
// A line comment ends at either line terminator; a CR-only file has no \n at all.
|
|
14
|
+
const lineEnd = text.slice(index).search(/[\r\n]/);
|
|
15
|
+
index = lineEnd === -1 ? text.length : index + lineEnd + 1;
|
|
16
|
+
}
|
|
17
|
+
else if (text.startsWith('/*', index)) {
|
|
18
|
+
const close = text.indexOf('*/', index + 2);
|
|
19
|
+
if (close === -1) {
|
|
20
|
+
// An unterminated comment is not valid JSONC; keep it as a token JSON.parse will reject.
|
|
21
|
+
tokens.push({ kind: 'other', start: index, end: text.length, text: text.slice(index) });
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
index = close + 2;
|
|
25
|
+
}
|
|
26
|
+
else if (char === '"') {
|
|
27
|
+
let end = index + 1;
|
|
28
|
+
while (end < text.length && text[end] !== '"') {
|
|
29
|
+
end += text[end] === '\\' ? 2 : 1;
|
|
30
|
+
}
|
|
31
|
+
tokens.push({ kind: 'string', start: index, end: end + 1, text: text.slice(index, end + 1) });
|
|
32
|
+
index = end + 1;
|
|
33
|
+
}
|
|
34
|
+
else if (PUNCTUATION.includes(char)) {
|
|
35
|
+
tokens.push({ kind: 'punct', start: index, end: index + 1, text: char });
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
let end = index;
|
|
40
|
+
while (end < text.length && !WHITESPACE.test(text[end]) && !PUNCTUATION.includes(text[end]) && text[end] !== '"' && !text.startsWith('//', end) && !text.startsWith('/*', end)) {
|
|
41
|
+
end += 1;
|
|
42
|
+
}
|
|
43
|
+
tokens.push({ kind: 'other', start: index, end, text: text.slice(index, end) });
|
|
44
|
+
index = end;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return tokens;
|
|
48
|
+
}
|
|
49
|
+
/** Parses JSONC; `null` when it is not valid even after comments and trailing commas are dropped. */
|
|
50
|
+
export function parseJsonc(text) {
|
|
51
|
+
const tokens = tokenize(text);
|
|
52
|
+
let json = '';
|
|
53
|
+
tokens.forEach((token, at) => {
|
|
54
|
+
const next = tokens[at + 1];
|
|
55
|
+
if (token.text === ',' && next && (next.text === ']' || next.text === '}')) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
json += `${token.text} `;
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(json || 'null');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The direct string elements of the root object's array under `key`, or `null` when there is no such array or the root
|
|
69
|
+
* object has `key` more than once: JSON.parse keeps the last one, so editing any of them could leave the one that counts.
|
|
70
|
+
*/
|
|
71
|
+
function stringElements(tokens, key) {
|
|
72
|
+
let depth = 0, found = null, occurrences = 0;
|
|
73
|
+
for (let at = 0; at < tokens.length; at += 1) {
|
|
74
|
+
const token = tokens[at];
|
|
75
|
+
if (token.kind === 'punct' && '{['.includes(token.text)) {
|
|
76
|
+
depth += 1;
|
|
77
|
+
}
|
|
78
|
+
else if (token.kind === 'punct' && '}]'.includes(token.text)) {
|
|
79
|
+
depth -= 1;
|
|
80
|
+
}
|
|
81
|
+
else if (depth === 1 && token.kind === 'string' && tokens[at + 1]?.text === ':') {
|
|
82
|
+
let value;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(token.text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (value !== key) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
occurrences += 1;
|
|
93
|
+
if (tokens[at + 2]?.text !== '[') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const elements = [];
|
|
97
|
+
let nested = 1;
|
|
98
|
+
for (let inner = at + 3; inner < tokens.length && nested > 0; inner += 1) {
|
|
99
|
+
const item = tokens[inner];
|
|
100
|
+
if (item.kind === 'punct' && '{['.includes(item.text)) {
|
|
101
|
+
nested += 1;
|
|
102
|
+
}
|
|
103
|
+
else if (item.kind === 'punct' && '}]'.includes(item.text)) {
|
|
104
|
+
nested -= 1;
|
|
105
|
+
}
|
|
106
|
+
else if (nested === 1 && item.kind === 'string') {
|
|
107
|
+
elements.push({
|
|
108
|
+
start: item.start,
|
|
109
|
+
end: item.end,
|
|
110
|
+
value: JSON.parse(item.text),
|
|
111
|
+
before: tokens[inner - 1].text === ',' ? tokens[inner - 1] : null,
|
|
112
|
+
after: tokens[inner + 1]?.text === ',' ? tokens[inner + 1] : null
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
found ??= elements;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return occurrences === 1 ? found : null;
|
|
120
|
+
}
|
|
121
|
+
/** `text` without the first `value` in the array under `key`, comments and layout intact; `null` if it is not there or the result would not parse. */
|
|
122
|
+
export function withoutArrayString(text, key, value) {
|
|
123
|
+
const element = stringElements(tokenize(text), key)?.find((entry) => entry.value === value);
|
|
124
|
+
if (!element) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
let start = element.start, end = element.end;
|
|
128
|
+
if (element.after) {
|
|
129
|
+
end = element.after.end;
|
|
130
|
+
while (WHITESPACE.test(text[end] ?? '')) {
|
|
131
|
+
end += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (element.before && WHITESPACE_ONLY.test(text.slice(element.before.end, element.start))) {
|
|
135
|
+
start = element.before.start;
|
|
136
|
+
}
|
|
137
|
+
// A comment between the comma and the last element belongs to the element before it, so only the comma goes.
|
|
138
|
+
else if (element.before) {
|
|
139
|
+
while (/[ \t]/.test(text[start - 1] ?? '')) {
|
|
140
|
+
start -= 1;
|
|
141
|
+
}
|
|
142
|
+
const edited = text.slice(0, element.before.start) + text.slice(element.before.end, start) + text.slice(end);
|
|
143
|
+
return parseJsonc(edited) === null ? null : edited;
|
|
144
|
+
}
|
|
145
|
+
const edited = text.slice(0, start) + text.slice(end);
|
|
146
|
+
return parseJsonc(edited) === null ? null : edited;
|
|
147
|
+
}
|