@postman/postman-plugin 0.3.0 → 0.4.1-rc.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 +46 -9
- package/dist/hosts/antigravity.js +47 -0
- package/dist/hosts/codex-hooks.js +68 -0
- package/dist/hosts/codex.js +87 -8
- package/dist/hosts/copilot.js +60 -0
- package/dist/hosts/index.js +3 -1
- package/dist/json-rpc.js +113 -0
- package/dist/system.js +40 -3
- 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 -145
- package/skills/api-discovery/reference/orbit.md +17 -5
- package/skills/api-documentation/SKILL.md +41 -46
- package/skills/api-engineer/SKILL.md +78 -23
- package/skills/api-mocking/SKILL.md +117 -163
- package/skills/api-monitoring/SKILL.md +52 -129
- package/skills/api-simulation/SKILL.md +130 -178
- package/skills/api-simulation/reference/sim_yaml_schema.md +41 -103
- package/skills/api-testing/SKILL.md +48 -95
- package/skills/bootstrap/SKILL.md +123 -224
- package/skills/ci-integration/SKILL.md +53 -113
- 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 -315
- 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 +82 -0
- package/skills/performance-testing/references/jmeter-import/import-report.md +52 -0
- package/skills/performance-testing/references/jmeter-import/mapping-table.md +95 -0
- package/skills/performance-testing/references/jmeter-import/postman-perf-capabilities.md +56 -0
- package/skills/performance-testing/references/jmeter-import/script-translation.md +97 -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 +44 -87
- 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,30 @@ 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
|
+
For Codex, installation also approves Postman's session-start hook, which adds
|
|
32
|
+
instructions for using Postman's API skills. There is no separate hook prompt
|
|
33
|
+
or dump of its instructions. Already trusted hooks are left alone. If Codex's
|
|
34
|
+
hook API is unavailable, installation continues with a note to approve the hook
|
|
35
|
+
inside Codex. Run the installer and Codex with the same `HOME` and `CODEX_HOME`;
|
|
36
|
+
hook approval applies to that profile. If Codex later refreshes the plugin from
|
|
37
|
+
`main` and its contents change, run the installer again to approve the update.
|
|
38
|
+
|
|
31
39
|
You can also use the following commands to install individually:
|
|
32
40
|
|
|
41
|
+
### Antigravity
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
agy plugin install https://github.com/postmanlabs/postman-plugin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run the same command again to update; `agy plugin uninstall postman` removes it.
|
|
48
|
+
In the Antigravity app, **Customizations → Plugins → + Install from URL** takes
|
|
49
|
+
the same URL.
|
|
50
|
+
|
|
33
51
|
### Claude Code
|
|
34
52
|
|
|
35
53
|
[View Postman on Claude Plugins](https://claude.com/plugins/postman)
|
|
@@ -48,10 +66,15 @@ does nothing where the marketplace is already registered.
|
|
|
48
66
|
[View Postman on ChatGPT Plugins](https://chatgpt.com/plugins/postman?open_in_app)
|
|
49
67
|
|
|
50
68
|
```bash
|
|
51
|
-
codex plugin marketplace add postmanlabs/postman-plugin
|
|
69
|
+
codex plugin marketplace add postmanlabs/postman-plugin --ref main
|
|
70
|
+
codex plugin marketplace upgrade postman
|
|
52
71
|
codex plugin add postman@postman
|
|
53
72
|
```
|
|
54
73
|
|
|
74
|
+
These commands track `main`; review the session-start hook in Codex after a
|
|
75
|
+
manual install. The npx installer also approves the hook after each install or
|
|
76
|
+
update, and moves an existing `codex-release` registration to `main`.
|
|
77
|
+
|
|
55
78
|
### Cursor
|
|
56
79
|
|
|
57
80
|
[View Postman on the Cursor Marketplace](https://cursor.com/marketplace/postman)
|
|
@@ -60,6 +83,16 @@ codex plugin add postman@postman
|
|
|
60
83
|
/add-plugin postman
|
|
61
84
|
```
|
|
62
85
|
|
|
86
|
+
### GitHub Copilot CLI
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
copilot plugin marketplace add postmanlabs/postman-plugin
|
|
90
|
+
copilot plugin install postman@postman
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`copilot plugin marketplace update postman`, then `copilot plugin update postman`, updates it. Sign in to Postman's MCP server
|
|
94
|
+
with `/mcp` inside a Copilot CLI session.
|
|
95
|
+
|
|
63
96
|
### Factory Droid
|
|
64
97
|
|
|
65
98
|
```bash
|
|
@@ -172,15 +205,19 @@ test consumers without waiting for the real service to be ready or available.
|
|
|
172
205
|
|
|
173
206
|
The mock stays local until you choose to push and deploy it. When teammates or
|
|
174
207
|
external systems need access, the same mock can become a durable hosted URL
|
|
175
|
-
without rebuilding it in another tool.
|
|
208
|
+
without rebuilding it in another tool. Pushed to the service's own workspace,
|
|
209
|
+
it's the mock other teams' simulations pull first.
|
|
176
210
|
|
|
177
211
|
### Develop against simulated dependencies
|
|
178
212
|
|
|
179
|
-
The [`api-simulation`](https://github.com/postmanlabs/postman-plugin/blob/main/skills/api-simulation/) skill
|
|
180
|
-
|
|
181
|
-
the
|
|
182
|
-
`.sim.yaml
|
|
183
|
-
|
|
213
|
+
The [`api-simulation`](https://github.com/postmanlabs/postman-plugin/blob/main/skills/api-simulation/) skill runs your code (a service
|
|
214
|
+
you're changing, a frontend, a test suite or a demo) for real against stand-ins
|
|
215
|
+
for the HTTP services it calls, served together on one port from one
|
|
216
|
+
`.sim.yaml`. Each stand-in is the owning team's mock when they publish one.
|
|
217
|
+
It reuses an existing `.sim.yaml`, proves a change with a request through the
|
|
218
|
+
changed path, and answers "what happens if X goes down" by running each failure.
|
|
219
|
+
Pushing the simulation is optional and needs your approval. It needs Postman CLI
|
|
220
|
+
1.70.0 or later:
|
|
184
221
|
|
|
185
222
|
```bash
|
|
186
223
|
postman simulation run postman/simulations/orders-dev.sim.yaml --port 4900
|
|
@@ -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,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export const POSTMAN_HOOK_KEY = 'postman@postman:hooks/codex-hooks.json:session_start:0:0';
|
|
3
|
+
async function postmanHook(rpc, cwd) {
|
|
4
|
+
const result = await rpc.request('hooks/list', { cwds: [cwd] });
|
|
5
|
+
if (!Array.isArray(result?.data) || result.data.some((entry) => !Array.isArray(entry.hooks) || entry.errors?.length)) {
|
|
6
|
+
throw new Error('hooks/list returned no usable hook listing');
|
|
7
|
+
}
|
|
8
|
+
const hooks = result.data.flatMap((entry) => entry.hooks).filter((hook) => hook.key === POSTMAN_HOOK_KEY);
|
|
9
|
+
if (hooks.length > 1) {
|
|
10
|
+
throw new Error('hooks/list returned more than one Postman session-start hook');
|
|
11
|
+
}
|
|
12
|
+
return hooks[0];
|
|
13
|
+
}
|
|
14
|
+
const trustEdit = (hash) => ({
|
|
15
|
+
edits: [{ keyPath: 'hooks.state', value: { [POSTMAN_HOOK_KEY]: { trusted_hash: hash } }, mergeStrategy: 'upsert' }],
|
|
16
|
+
reloadUserConfig: true
|
|
17
|
+
});
|
|
18
|
+
/** Approves only Postman's current hook after installation; unsupported APIs never fail the install. */
|
|
19
|
+
export async function approvePostmanHook(system) {
|
|
20
|
+
const note = (message) => system.log(` note: ${message}`);
|
|
21
|
+
let writeAttempted = false;
|
|
22
|
+
if (system.dryRun) {
|
|
23
|
+
note('Would inspect and approve Postman\'s current session-start hook after installation through codex app-server:');
|
|
24
|
+
system.log(` config/batchWrite ${JSON.stringify(trustEdit('<currentHash from hooks/list>'))}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await system.withJsonRpc('codex', ['app-server'], async (rpc) => {
|
|
29
|
+
// Version of this small protocol client, independent of the plugin's release.
|
|
30
|
+
await rpc.request('initialize', { clientInfo: { name: 'postman-plugin-installer', version: '1' } });
|
|
31
|
+
rpc.notify('initialized');
|
|
32
|
+
const hook = await postmanHook(rpc, system.cwd);
|
|
33
|
+
if (!hook) {
|
|
34
|
+
note('Postman\'s session-start hook was not found; hook approval skipped.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (hook.trustStatus === 'trusted') {
|
|
38
|
+
note('Postman\'s Codex session-start hook is already trusted.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!['untrusted', 'modified'].includes(hook.trustStatus) || !hook.currentHash || typeof hook.currentHash !== 'string') {
|
|
42
|
+
throw new Error('Postman\'s hook has an unsupported trust status or hash');
|
|
43
|
+
}
|
|
44
|
+
// Confirm the installed instructions are present before approving their hook.
|
|
45
|
+
const mandateFile = path.resolve(path.dirname(hook.sourcePath), 'session-start-context.md'), mandate = await system.readFile(mandateFile);
|
|
46
|
+
if (!mandate?.trim()) {
|
|
47
|
+
throw new Error('could not read the installed hooks/session-start-context.md');
|
|
48
|
+
}
|
|
49
|
+
// Startup can refresh marketplaces. A changed hash needs another check,
|
|
50
|
+
// rather than approving a different version from the one inspected above.
|
|
51
|
+
const current = await postmanHook(rpc, system.cwd);
|
|
52
|
+
if (!current || current.currentHash !== hook.currentHash || !['untrusted', 'modified'].includes(current.trustStatus)) {
|
|
53
|
+
note('Postman\'s hook changed while checking it; approval skipped. Re-run to approve the current version.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
writeAttempted = true;
|
|
57
|
+
await rpc.request('config/batchWrite', trustEdit(hook.currentHash));
|
|
58
|
+
const after = await postmanHook(rpc, system.cwd);
|
|
59
|
+
note(after?.trustStatus === 'trusted' && after.currentHash === hook.currentHash ?
|
|
60
|
+
'Approved Postman\'s Codex session-start hook.' :
|
|
61
|
+
'Codex accepted the approval write, but the hook is not confirmed trusted. Review it in Codex.');
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Older Codex releases may lack app-server or hooks/list. Installation still succeeds.
|
|
66
|
+
note(`Hook approval ${writeAttempted ? 'could not be confirmed' : 'skipped'}: ${error instanceof Error ? error.message : String(error)}. Review the hook in Codex.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/hosts/codex.js
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import { REPO, isSameRepo, redact } from '../source.js';
|
|
2
|
-
import { blocked, failed, guard, mustProbeJson, mustRun, parseJson } from './shared.js';
|
|
1
|
+
import { BRANCH, GIT_URL, REPO, isSameRepo, redact } from '../source.js';
|
|
2
|
+
import { StepFailed, blocked, failed, guard, mustProbeJson, mustRun, parseJson } from './shared.js';
|
|
3
3
|
import { result } from './types.js';
|
|
4
|
+
import { approvePostmanHook } from './codex-hooks.js';
|
|
4
5
|
// Codex names the marketplace after `.claude-plugin/marketplace.json`'s `name`.
|
|
5
6
|
// `npx plugins add` installs a second copy under its own `plugins-cli` marketplace.
|
|
6
|
-
const MARKETPLACE = 'postman', PLUGIN_ID = `postman@${MARKETPLACE}`, SHADOW_IDS = ['postman@plugins-cli'], NEXT = 'Restart Codex for the change to take effect.';
|
|
7
|
+
const MARKETPLACE = 'postman', PLUGIN_ID = `postman@${MARKETPLACE}`, LEGACY_REF = 'codex-release', SHADOW_IDS = ['postman@plugins-cli'], NEXT = 'Restart Codex for the change to take effect.';
|
|
7
8
|
function isInstalled(plugins, id) {
|
|
8
9
|
return plugins.some((plugin) => plugin.pluginId === id && plugin.installed !== false);
|
|
9
10
|
}
|
|
10
11
|
async function listPlugins(system) {
|
|
11
12
|
return (await mustProbeJson(system, 'codex', ['plugin', 'list', '--json'])).installed ?? [];
|
|
12
13
|
}
|
|
14
|
+
/** The marketplace's ref as Codex's own config reader reports it; `marketplace list --json` doesn't. */
|
|
15
|
+
export async function registeredRef(system) {
|
|
16
|
+
const replies = await system.rpc('codex', ['app-server'], [
|
|
17
|
+
{ method: 'initialize', params: { clientInfo: { name: 'postman-plugin', version: '0' } } },
|
|
18
|
+
{ method: 'initialized', notify: true },
|
|
19
|
+
{ method: 'config/read', params: {} }
|
|
20
|
+
]), read = replies?.[1]?.result, entry = read?.config?.marketplaces?.[MARKETPLACE];
|
|
21
|
+
if (!entry || typeof entry !== 'object') {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return typeof entry.ref === 'string' ? entry.ref : entry.ref === null || entry.ref === undefined ? null : undefined;
|
|
25
|
+
}
|
|
26
|
+
async function addMarketplace(system) {
|
|
27
|
+
await mustRun(system, 'codex', ['plugin', 'marketplace', 'add', REPO, '--ref', BRANCH, '--json']);
|
|
28
|
+
// `add` records no revision, so without this Codex's next startup refetches the plugin and asks for approval again.
|
|
29
|
+
await mustRun(system, 'codex', ['plugin', 'marketplace', 'upgrade', MARKETPLACE, '--json']);
|
|
30
|
+
}
|
|
31
|
+
/** Adds or upgrades the marketplace, leaving the old release branch for `migrate`. */
|
|
13
32
|
async function refreshMarketplace(system) {
|
|
14
33
|
const listing = await mustProbeJson(system, 'codex', ['plugin', 'marketplace', 'list', '--json']), existing = listing.marketplaces?.find((marketplace) => marketplace.name === MARKETPLACE);
|
|
15
34
|
if (!existing) {
|
|
16
|
-
await
|
|
17
|
-
return;
|
|
35
|
+
await addMarketplace(system);
|
|
36
|
+
return 'ready';
|
|
18
37
|
}
|
|
19
38
|
// A local marketplace can carry any path text, so only a git source counts as this repo.
|
|
20
39
|
const { sourceType, source } = existing.marketplaceSource ?? {};
|
|
@@ -22,7 +41,58 @@ async function refreshMarketplace(system) {
|
|
|
22
41
|
const from = source ? `${sourceType ?? 'unknown'} source ${redact(source)}` : 'an unknown source';
|
|
23
42
|
blocked(`marketplace ${MARKETPLACE} is registered from ${from}, not ${REPO}`);
|
|
24
43
|
}
|
|
25
|
-
|
|
44
|
+
if (system.dryRun) {
|
|
45
|
+
system.log(` note: Would ask Codex for marketplace ${MARKETPLACE}'s ref: upgrade it on ${BRANCH}, migrate ${LEGACY_REF} to ${BRANCH}, preserve an unreadable ref, or stop for a custom ref. App-server is deferred because its startup can refresh plugins.`);
|
|
46
|
+
return 'deferred';
|
|
47
|
+
}
|
|
48
|
+
const ref = await registeredRef(system);
|
|
49
|
+
if (ref === undefined) {
|
|
50
|
+
system.log(` note: Could not read marketplace ${MARKETPLACE}'s ref through codex app-server; keeping its registration and skipping ref migration.`);
|
|
51
|
+
}
|
|
52
|
+
if (ref === BRANCH || ref === null || ref === undefined) {
|
|
53
|
+
await mustRun(system, 'codex', ['plugin', 'marketplace', 'upgrade', MARKETPLACE, '--json']);
|
|
54
|
+
return 'ready';
|
|
55
|
+
}
|
|
56
|
+
if (ref !== LEGACY_REF) {
|
|
57
|
+
blocked(`marketplace ${MARKETPLACE} tracks ${ref}, not ${BRANCH}; run \`codex plugin marketplace remove ${MARKETPLACE}\` and re-run`);
|
|
58
|
+
}
|
|
59
|
+
return 'legacy';
|
|
60
|
+
}
|
|
61
|
+
/** Re-registers the previous marketplace and its plugin; true when every step exits 0. */
|
|
62
|
+
async function restoreLegacy(system, wasInstalled) {
|
|
63
|
+
// Exits non-zero when the failed step left nothing registered, which is the state wanted here.
|
|
64
|
+
await system.run('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE, '--json']);
|
|
65
|
+
const steps = [['plugin', 'marketplace', 'add', REPO, '--ref', LEGACY_REF, '--json'], ...(wasInstalled ? [['plugin', 'add', PLUGIN_ID, '--json']] : [])];
|
|
66
|
+
for (const args of steps) {
|
|
67
|
+
if ((await system.run('codex', args)).code !== 0) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Moves a marketplace registered at the old release branch to main, then adds the plugin. `remove`
|
|
75
|
+
* uninstalls Postman too, so a failure after it puts the previous registration back.
|
|
76
|
+
*/
|
|
77
|
+
async function migrate(system, wasInstalled) {
|
|
78
|
+
const reach = await system.probe('git', ['ls-remote', '--exit-code', GIT_URL, `refs/heads/${BRANCH}`]);
|
|
79
|
+
if (reach.code !== 0) {
|
|
80
|
+
failed(`can't reach ${BRANCH} in ${REPO}, so marketplace ${MARKETPLACE} was left tracking ${LEGACY_REF}`);
|
|
81
|
+
}
|
|
82
|
+
await mustRun(system, 'codex', ['plugin', 'marketplace', 'remove', MARKETPLACE, '--json']);
|
|
83
|
+
try {
|
|
84
|
+
await addMarketplace(system);
|
|
85
|
+
await mustRun(system, 'codex', ['plugin', 'add', PLUGIN_ID, '--json']);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!(error instanceof StepFailed)) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const restored = await restoreLegacy(system, wasInstalled);
|
|
92
|
+
failed(`${error.outcome.message}\n${restored ?
|
|
93
|
+
`restored marketplace ${MARKETPLACE} tracking ${LEGACY_REF}${wasInstalled ? ` and ${PLUGIN_ID}` : ''}` :
|
|
94
|
+
`could not restore it either; run \`codex plugin marketplace remove ${MARKETPLACE}\`, \`codex plugin marketplace add ${REPO} --ref ${BRANCH}\`, \`codex plugin marketplace upgrade ${MARKETPLACE}\` and \`codex plugin add ${PLUGIN_ID}\``}`);
|
|
95
|
+
}
|
|
26
96
|
}
|
|
27
97
|
async function removeInstalled(system, plugins, ids) {
|
|
28
98
|
const present = ids.filter((id) => isInstalled(plugins, id));
|
|
@@ -44,18 +114,27 @@ export const codex = {
|
|
|
44
114
|
return { installed: null, detail: 'could not read `codex plugin list --json`', notes: [] };
|
|
45
115
|
}
|
|
46
116
|
const installed = plugins.find((plugin) => plugin.pluginId === PLUGIN_ID && plugin.installed !== false), notes = SHADOW_IDS.filter((id) => isInstalled(plugins, id)).map((id) => `${id} duplicates it and will be removed`);
|
|
117
|
+
if (installed && await registeredRef(system) === LEGACY_REF) {
|
|
118
|
+
notes.push(`marketplace ${MARKETPLACE} tracks ${LEGACY_REF} and will be re-added at ${BRANCH}; the installer then approves its session-start hook`);
|
|
119
|
+
}
|
|
47
120
|
return installed ?
|
|
48
121
|
{ installed: true, detail: `${PLUGIN_ID} ${installed.version ?? ''}`.trim(), notes } :
|
|
49
122
|
{ installed: false, detail: 'not installed', notes };
|
|
50
123
|
},
|
|
51
124
|
install(system) {
|
|
52
125
|
return guard(async () => {
|
|
53
|
-
|
|
126
|
+
// Listed first: re-registering the marketplace uninstalls its plugins.
|
|
54
127
|
const plugins = await listPlugins(system), wasInstalled = isInstalled(plugins, PLUGIN_ID);
|
|
55
128
|
// `add` is idempotent and is also how Codex updates an installed plugin. It runs
|
|
56
129
|
// before the duplicate is removed, so a failed add still leaves a working copy.
|
|
57
|
-
await
|
|
130
|
+
if (await refreshMarketplace(system) === 'legacy') {
|
|
131
|
+
await migrate(system, wasInstalled);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
await mustRun(system, 'codex', ['plugin', 'add', PLUGIN_ID, '--json']);
|
|
135
|
+
}
|
|
58
136
|
await removeInstalled(system, plugins, SHADOW_IDS);
|
|
137
|
+
await approvePostmanHook(system);
|
|
59
138
|
return result('done', `${wasInstalled ? 'updated' : 'installed'} ${PLUGIN_ID}`, NEXT);
|
|
60
139
|
});
|
|
61
140
|
},
|
|
@@ -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];
|
package/dist/json-rpc.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
export class JsonRpcError extends Error {
|
|
4
|
+
reply;
|
|
5
|
+
constructor(reply) {
|
|
6
|
+
super(`app-server RPC error ${reply.code ?? ''}: ${reply.message ?? 'unknown error'}`);
|
|
7
|
+
this.reply = reply;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** One stdio session with a deadline for each request. */
|
|
11
|
+
export async function withJsonRpc(file, args, env, use, timeoutMs) {
|
|
12
|
+
// npm's Windows launchers need cmd.exe, just as System.run's launchers do.
|
|
13
|
+
const quote = (arg) => /^[\w@./:=-]+$/.test(arg) ? arg : `"${arg}"`, shell = /\.(cmd|bat)$/i.test(file), child = shell ?
|
|
14
|
+
spawn(quote(file), args.map(quote), { env, shell: true, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }) :
|
|
15
|
+
spawn(file, args, { env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }), lines = createInterface({ input: child.stdout }), pending = new Map();
|
|
16
|
+
let nextId = 0, failure;
|
|
17
|
+
function fail(error) {
|
|
18
|
+
failure = error;
|
|
19
|
+
for (const entry of pending.values()) {
|
|
20
|
+
clearTimeout(entry.timer);
|
|
21
|
+
entry.reject(error);
|
|
22
|
+
}
|
|
23
|
+
pending.clear();
|
|
24
|
+
}
|
|
25
|
+
child.on('error', fail);
|
|
26
|
+
child.stdin.on('error', fail);
|
|
27
|
+
// Drain diagnostics without putting arbitrary app-server output into installer logs.
|
|
28
|
+
child.stderr.resume();
|
|
29
|
+
const closed = new Promise((resolve) => child.once('close', (code) => {
|
|
30
|
+
fail(new Error(`app-server closed (exit ${code ?? 'signal'})`));
|
|
31
|
+
resolve();
|
|
32
|
+
}));
|
|
33
|
+
lines.on('line', (line) => {
|
|
34
|
+
let message;
|
|
35
|
+
try {
|
|
36
|
+
message = JSON.parse(line);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
fail(new Error('app-server returned invalid JSON'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Notifications are not replies. A server request is unsupported by this client.
|
|
43
|
+
if (message && typeof message.method === 'string') {
|
|
44
|
+
if (message.id !== undefined) {
|
|
45
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: 'Method not found' } }) + '\n');
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const entry = pending.get(message?.id);
|
|
50
|
+
if (!entry) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
clearTimeout(entry.timer);
|
|
54
|
+
pending.delete(message.id);
|
|
55
|
+
if (message.error) {
|
|
56
|
+
entry.reject(new JsonRpcError(message.error));
|
|
57
|
+
}
|
|
58
|
+
else if ('result' in message) {
|
|
59
|
+
entry.resolve(message.result);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
entry.reject(new Error('app-server reply has no result'));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
return await use({
|
|
67
|
+
request(method, params) {
|
|
68
|
+
if (failure) {
|
|
69
|
+
return Promise.reject(failure);
|
|
70
|
+
}
|
|
71
|
+
const id = ++nextId;
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const timer = setTimeout(() => fail(new Error(`app-server timed out waiting for ${method}`)), timeoutMs);
|
|
74
|
+
pending.set(id, { resolve: (value) => resolve(value), reject, timer });
|
|
75
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
notify(method, params) {
|
|
79
|
+
if (failure) {
|
|
80
|
+
throw failure;
|
|
81
|
+
}
|
|
82
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
fail(new Error('app-server session ended'));
|
|
88
|
+
lines.close();
|
|
89
|
+
child.stdin.end();
|
|
90
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
91
|
+
if (shell && process.platform === 'win32' && child.pid) {
|
|
92
|
+
// Killing cmd.exe alone leaves its Codex child holding the pipes open.
|
|
93
|
+
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true }).on('error', () => child.kill());
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
child.kill();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let force;
|
|
100
|
+
const forced = new Promise((resolve) => {
|
|
101
|
+
force = setTimeout(() => {
|
|
102
|
+
child.kill('SIGKILL');
|
|
103
|
+
// A descendant retaining a pipe must not hold the installer open.
|
|
104
|
+
child.stdin.destroy();
|
|
105
|
+
child.stdout.destroy();
|
|
106
|
+
child.stderr.destroy();
|
|
107
|
+
resolve();
|
|
108
|
+
}, 1000);
|
|
109
|
+
});
|
|
110
|
+
await Promise.race([closed, forced]);
|
|
111
|
+
clearTimeout(force);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/system.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { redact } from './source.js';
|
|
6
|
+
import { JsonRpcError, withJsonRpc } from './json-rpc.js';
|
|
6
7
|
// A missing file, or a path through something that isn't a directory. Anything else,
|
|
7
8
|
// such as a file that exists but can't be read, must not pass for "absent".
|
|
8
9
|
const ABSENT = ['ENOENT', 'ENOTDIR'];
|
|
@@ -35,8 +36,8 @@ function execute(file, args, env) {
|
|
|
35
36
|
export function formatCommand(command, args) {
|
|
36
37
|
return [command, ...args].map((part) => redact(part)).map((part) => (/^[\w@./:=~-]+$/.test(part) ? part : JSON.stringify(part))).join(' ');
|
|
37
38
|
}
|
|
38
|
-
export function createSystem({ dryRun = false, log = (line) => console.log(line) } = {}) {
|
|
39
|
-
const
|
|
39
|
+
export function createSystem({ dryRun = false, log = (line) => console.log(line), env = process.env, home = os.homedir(), cwd = process.cwd() } = {}) {
|
|
40
|
+
const platform = process.platform;
|
|
40
41
|
async function which(command) {
|
|
41
42
|
const extensions = platform === 'win32' ? (env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : [''];
|
|
42
43
|
for (const dir of (env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
@@ -59,10 +60,11 @@ export function createSystem({ dryRun = false, log = (line) => console.log(line)
|
|
|
59
60
|
return (await which(command)) || command;
|
|
60
61
|
}
|
|
61
62
|
return {
|
|
62
|
-
home
|
|
63
|
+
home,
|
|
63
64
|
platform,
|
|
64
65
|
env,
|
|
65
66
|
dryRun,
|
|
67
|
+
cwd,
|
|
66
68
|
which,
|
|
67
69
|
async exists(file) {
|
|
68
70
|
try {
|
|
@@ -101,6 +103,35 @@ export function createSystem({ dryRun = false, log = (line) => console.log(line)
|
|
|
101
103
|
async probe(command, args) {
|
|
102
104
|
return execute(await resolve(command), args, env);
|
|
103
105
|
},
|
|
106
|
+
async rpc(command, args, calls) {
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return await withJsonRpc(await resolve(command), args, env, async (rpc) => {
|
|
112
|
+
const replies = [];
|
|
113
|
+
for (const call of calls) {
|
|
114
|
+
if (call.notify) {
|
|
115
|
+
rpc.notify(call.method, call.params);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
replies.push({ result: await rpc.request(call.method, call.params) });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!(error instanceof JsonRpcError)) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
replies.push({ error: error.reply });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return replies;
|
|
129
|
+
}, 30000);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
104
135
|
async run(command, args, options = {}) {
|
|
105
136
|
log(` $ ${formatCommand(command, args)}`);
|
|
106
137
|
if (dryRun) {
|
|
@@ -108,6 +139,12 @@ export function createSystem({ dryRun = false, log = (line) => console.log(line)
|
|
|
108
139
|
}
|
|
109
140
|
return execute(await resolve(command), args, { ...env, ...options.env });
|
|
110
141
|
},
|
|
142
|
+
async withJsonRpc(command, args, use, timeoutMs = 15000) {
|
|
143
|
+
log(` $ ${formatCommand(command, args)} (stdio RPC)`);
|
|
144
|
+
if (!dryRun) {
|
|
145
|
+
return withJsonRpc(await resolve(command), args, env, use, timeoutMs);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
111
148
|
async writeFile(file, content) {
|
|
112
149
|
log(` write ${file}`);
|
|
113
150
|
if (!dryRun) {
|
package/mcp.pi.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"description": "Postman workspaces, collections, specs, environments, mocks and monitors",
|
|
7
7
|
"headers": {
|
|
8
8
|
"X-Source": "postman-pi-plugin",
|
|
9
|
-
"X-Plugin-Version": "0.
|
|
10
|
-
"User-Agent": "postman-pi-plugin/0.
|
|
9
|
+
"X-Plugin-Version": "0.4.1-rc.0",
|
|
10
|
+
"User-Agent": "postman-pi-plugin/0.4.1-rc.0"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postman/postman-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1-rc.0",
|
|
4
4
|
"description": "Postman's API engineering skills for coding agents: a Pi package, and an npx installer that sets up Claude Code, Codex, Cursor, Kimi Code, Factory Droid, OpenCode and Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|