@theholocron/holocron-plugin-postman 2.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Newton Koumantzelis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # `@theholocron/holocron-plugin-postman`
2
+
3
+ Postman plugin for [Holocron](../cli). Implements the **multi-cardinality**
4
+ `tooling` capability against [Postman's REST API](https://learning.postman.com/docs/developer/postman-api/).
5
+
6
+ ## Why REST, not the CLI
7
+
8
+ Postman ships a CLI (`postman`, the newer + more capable successor to
9
+ `newman`), but for our `tooling` surface — collection / spec / environment
10
+ **management** — REST is the canonical interface and avoids a system-binary
11
+ dependency. The CLI is best suited for running collections (the newman use
12
+ case); we sync, we don't run.
13
+
14
+ ## Auth
15
+
16
+ Token resolution order:
17
+
18
+ 1. `--token <KEY>` flag on the holocron invocation
19
+ 2. `HOLOCRON_POSTMAN_API_KEY` env var
20
+ 3. `POSTMAN_API_KEY` env var (Postman's own standard)
21
+
22
+ Generate the key at https://web.postman.co/settings/me/api-keys.
23
+
24
+ ## Config
25
+
26
+ ```jsonc
27
+ {
28
+ "providers": {
29
+ "tooling": [
30
+ ["postman", {
31
+ "workspaceId": "00000000-0000-0000-0000-000000000000",
32
+ "specFile": "apps/api/openapi.json",
33
+ "specName": "Rando API",
34
+ "collectionName": "Rando API",
35
+ "envFiles": ["apps/api/postman-env-staging.json"]
36
+ }],
37
+ "storybook"
38
+ ]
39
+ }
40
+ }
41
+ ```
42
+
43
+ - `workspaceId` (required) — Postman workspace id. Find via `holocron tooling postman workspaces`.
44
+ - `specFile` (optional) — relative path to the local OpenAPI JSON. `sync` reads + uploads this.
45
+ - `specName` (optional) — display name in Postman's Spec Hub. Defaults to repo name.
46
+ - `collectionName` (optional) — name for the imported collection. Defaults to `specName`.
47
+ - `envFiles` (optional) — local Postman environment JSON files to push.
48
+
49
+ ## What's implemented
50
+
51
+ | Method | What it does |
52
+ | ---------------------------- | ------------------------------------------------------------- |
53
+ | **Tooling interface** | |
54
+ | `sync()` | Reads `specFile`, upserts the Spec Hub spec, delete-then-imports the collection, find-or-creates each env in `envFiles`. |
55
+ | `doctor()` | Probes `/me` + `/workspaces`; returns `{ ok, message }`. |
56
+ | **Postman-specific methods** (on `PostmanTooling`, not on the `Tooling` interface) | |
57
+ | `getMyself` | `GET /me` — authed user identity. |
58
+ | `listWorkspaces` | `GET /workspaces`. |
59
+ | `findCollectionByName` | `GET /collections?workspace=…` + name filter. |
60
+ | `deleteCollection` | `DELETE /collections/{uid}`. |
61
+ | `importOpenApi` | `POST /import/openapi?workspace=…` with the spec stringified into `{ type: "string", input }`. |
62
+ | `findEnvironmentByName` | `GET /environments?workspace=…` + name filter. |
63
+ | `createEnvironment` | `POST /environments?workspace=…`. |
64
+ | `updateEnvironment` | `PUT /environments/{uid}`. |
65
+ | `findSpecByName` | `GET /specs?workspaceId=…` + name filter. |
66
+ | `createSpec` | `POST /specs?workspaceId=…` (flat body — name/type are NOT wrapped under `spec`). |
67
+ | `upsertSpecFile` | `PATCH /specs/{id}/files/{path}` (PUT returns 404 here). |
68
+
69
+ ## Status
70
+
71
+ **v0.0.0 — first port.** Surface matches Rando's `adapters/postman.ts`.
72
+ `PostmanPlanLimitError` is thrown when Postman responds with
73
+ `limitReachedError` (e.g., Free-tier "0 APIs" cap) — callers can
74
+ discriminate to render "upgrade required" instead of a raw API dump.
@@ -0,0 +1,192 @@
1
+ import { Tooling, ToolingDoctorReport } from "@theholocron/cli";
2
+
3
+ //#region src/auth.d.ts
4
+ /**
5
+ * Token resolution for the Postman plugin.
6
+ *
7
+ * Resolution order:
8
+ * 1. explicit `cliToken` argument (from `--token` flag)
9
+ * 2. HOLOCRON_POSTMAN_API_KEY env var (preferred — explicit intent)
10
+ * 3. POSTMAN_API_KEY env var (Postman's own default)
11
+ */
12
+ declare class AuthError extends Error {
13
+ name: string;
14
+ }
15
+ interface ResolveTokenInput {
16
+ /** From `--token` CLI flag. */
17
+ cliToken?: string;
18
+ /** Env vars; passed in for testability. Defaults to `process.env`. */
19
+ env?: NodeJS.ProcessEnv;
20
+ }
21
+ declare function resolveToken(input?: ResolveTokenInput): string;
22
+ //#endregion
23
+ //#region src/rest.d.ts
24
+ /**
25
+ * Thin REST wrapper around api.getpostman.com.
26
+ *
27
+ * Same pattern as the github / vercel / neon / clerk REST clients —
28
+ * the Postman API uses `x-api-key` (not bearer) for auth, but
29
+ * otherwise the shape's identical: JSON only, transport-failure
30
+ * wrapping with `status: 0`.
31
+ */
32
+ interface RestClientOptions {
33
+ token: string;
34
+ fetch?: typeof fetch;
35
+ baseUrl?: string;
36
+ }
37
+ interface RequestOptions {
38
+ method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
39
+ body?: unknown;
40
+ query?: Record<string, string>;
41
+ }
42
+ declare class PostmanRestClient {
43
+ private readonly token;
44
+ private readonly fetchImpl;
45
+ readonly baseUrl: string;
46
+ constructor(opts: RestClientOptions);
47
+ request<T>(path: string, opts?: RequestOptions): Promise<T>;
48
+ }
49
+ //#endregion
50
+ //#region src/capabilities/tooling.d.ts
51
+ interface PostmanToolingOptions {
52
+ workspaceId: string;
53
+ /** Local OpenAPI JSON path (relative to repoRoot). */
54
+ specFile?: string;
55
+ /** Display name in Postman. Defaults to the OpenAPI spec's `info.title`. */
56
+ specName?: string;
57
+ /** Collection name to import the spec as. Defaults to specName. */
58
+ collectionName?: string;
59
+ /** Local Postman environment JSON files to push (relative to repoRoot). */
60
+ envFiles?: string[];
61
+ /** Working repo root. Defaults to process.cwd(). */
62
+ repoRoot?: string;
63
+ }
64
+ interface PostmanUser {
65
+ id: number;
66
+ username: string;
67
+ fullName: string;
68
+ }
69
+ interface PostmanWorkspace {
70
+ id: string;
71
+ name: string;
72
+ type: string;
73
+ }
74
+ interface PostmanCollection {
75
+ id: string;
76
+ uid: string;
77
+ name: string;
78
+ }
79
+ interface PostmanEnvironment {
80
+ id: string;
81
+ uid: string;
82
+ name: string;
83
+ }
84
+ interface PostmanSpec {
85
+ id: string;
86
+ name: string;
87
+ type: string;
88
+ }
89
+ declare class PostmanTooling implements Tooling {
90
+ private readonly rest;
91
+ readonly key: "tooling";
92
+ readonly providerName = "postman";
93
+ private readonly opts;
94
+ private readonly repoRoot;
95
+ constructor(rest: PostmanRestClient, opts: PostmanToolingOptions);
96
+ sync(): Promise<void>;
97
+ doctor(): Promise<ToolingDoctorReport>;
98
+ getMyself(): Promise<PostmanUser>;
99
+ listWorkspaces(): Promise<PostmanWorkspace[]>;
100
+ findCollectionByName(input: {
101
+ workspaceId: string;
102
+ name: string;
103
+ }): Promise<PostmanCollection | null>;
104
+ deleteCollection(uid: string): Promise<void>;
105
+ importOpenApi(input: {
106
+ workspaceId: string;
107
+ spec: unknown;
108
+ }): Promise<PostmanCollection>;
109
+ listEnvironments(input: {
110
+ workspaceId: string;
111
+ }): Promise<PostmanEnvironment[]>;
112
+ findEnvironmentByName(input: {
113
+ workspaceId: string;
114
+ name: string;
115
+ }): Promise<PostmanEnvironment | null>;
116
+ createEnvironment(input: {
117
+ workspaceId: string;
118
+ environment: unknown;
119
+ }): Promise<PostmanEnvironment>;
120
+ updateEnvironment(input: {
121
+ uid: string;
122
+ environment: unknown;
123
+ }): Promise<PostmanEnvironment>;
124
+ findSpecByName(input: {
125
+ workspaceId: string;
126
+ name: string;
127
+ }): Promise<PostmanSpec | null>;
128
+ createSpec(input: {
129
+ workspaceId: string;
130
+ name: string;
131
+ type?: string;
132
+ filePath?: string;
133
+ fileContent: string;
134
+ }): Promise<PostmanSpec>;
135
+ upsertSpecFile(input: {
136
+ specId: string;
137
+ filePath: string;
138
+ content: string;
139
+ }): Promise<void>;
140
+ }
141
+ //#endregion
142
+ //#region src/errors.d.ts
143
+ /**
144
+ * Postman-specific errors that callers might want to discriminate.
145
+ */
146
+ /**
147
+ * Thrown when Postman returns a `limitReachedError` (e.g., a Free-tier
148
+ * account hitting the "0 APIs" cap). Callers can render "upgrade
149
+ * required" instead of dumping the raw API body.
150
+ */
151
+ declare class PostmanPlanLimitError extends Error {
152
+ /** Human-readable plan-limit message from Postman. */
153
+ readonly limitMessage: string;
154
+ /** Original response body (JSON or text). */
155
+ readonly body: string;
156
+ name: string;
157
+ constructor(/** Human-readable plan-limit message from Postman. */
158
+
159
+ limitMessage: string, /** Original response body (JSON or text). */
160
+
161
+ body: string);
162
+ }
163
+ /**
164
+ * Inspect a Postman error body for the plan-limit shape. Returns the
165
+ * limit message when matched; null otherwise (caller throws a generic
166
+ * `ProviderApiError`).
167
+ */
168
+ declare function detectPlanLimit(body: string): string | null;
169
+ //#endregion
170
+ //#region src/index.d.ts
171
+ interface PostmanPluginOptions extends ResolveTokenInput, PostmanToolingOptions {
172
+ /** Working repo root. Used to resolve relative paths in specFile/envFiles. Defaults to process.cwd(). */
173
+ repoRoot?: string;
174
+ /** Override base URL for tests. */
175
+ baseUrl?: string;
176
+ /** Override `fetch` for tests. */
177
+ fetch?: typeof fetch;
178
+ }
179
+ interface PluginContext {
180
+ options: PostmanPluginOptions;
181
+ rest: PostmanRestClient;
182
+ }
183
+ declare function createContext(options: PostmanPluginOptions): PluginContext;
184
+ declare function tooling(ctx: PluginContext): Tooling;
185
+ declare function createPlugin(options: PostmanPluginOptions): {
186
+ name: string;
187
+ capabilities: {
188
+ tooling: () => Tooling;
189
+ };
190
+ };
191
+ //#endregion
192
+ export { AuthError, PluginContext, PostmanPlanLimitError, PostmanPluginOptions, PostmanRestClient, PostmanTooling, ResolveTokenInput, createContext, createPlugin, detectPlanLimit, resolveToken, tooling };
package/dist/index.mjs ADDED
@@ -0,0 +1,350 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
+ import { ProviderApiError } from "@theholocron/cli";
4
+ //#region src/auth.ts
5
+ /**
6
+ * Token resolution for the Postman plugin.
7
+ *
8
+ * Resolution order:
9
+ * 1. explicit `cliToken` argument (from `--token` flag)
10
+ * 2. HOLOCRON_POSTMAN_API_KEY env var (preferred — explicit intent)
11
+ * 3. POSTMAN_API_KEY env var (Postman's own default)
12
+ */
13
+ var AuthError = class extends Error {
14
+ name = "AuthError";
15
+ };
16
+ function resolveToken(input = {}) {
17
+ const env = input.env ?? process.env;
18
+ const token = input.cliToken || env.HOLOCRON_POSTMAN_API_KEY || env.POSTMAN_API_KEY;
19
+ if (!token) throw new AuthError("no Postman API key found. Pass --token <KEY>, or set HOLOCRON_POSTMAN_API_KEY / POSTMAN_API_KEY.");
20
+ return token;
21
+ }
22
+ //#endregion
23
+ //#region src/capabilities/tooling.ts
24
+ /**
25
+ * `tooling` capability for Postman.
26
+ *
27
+ * Ported from rando-id/rando.id `adapters/postman.ts`. The holocron
28
+ * `Tooling` interface stays narrow (`sync` + `doctor`); the
29
+ * Postman-specific surface (workspaces, collections, environments,
30
+ * Spec Hub) lives as additional methods on this class for callers
31
+ * that need direct access.
32
+ *
33
+ * `sync()` flow:
34
+ * 1. Read the local OpenAPI spec from `options.specFile`
35
+ * 2. Find or create the Spec Hub spec by name in the workspace;
36
+ * upsert the spec file content
37
+ * 3. Find any existing collection with the same name and delete it
38
+ * (Postman's import-from-OpenAPI path is create-only — no stable
39
+ * update path for collections produced from a spec)
40
+ * 4. Import the spec as a fresh collection
41
+ * 5. For each `envFiles[i]`, find-or-create the environment
42
+ *
43
+ * `doctor()` probes `/me` for auth + `/workspaces` for workspace
44
+ * reachability and returns a `ToolingDoctorReport`.
45
+ */
46
+ var PostmanTooling = class {
47
+ rest;
48
+ key = "tooling";
49
+ providerName = "postman";
50
+ opts;
51
+ repoRoot;
52
+ constructor(rest, opts) {
53
+ this.rest = rest;
54
+ if (!opts.workspaceId) throw new Error("PostmanTooling requires `workspaceId` in options");
55
+ this.opts = opts;
56
+ this.repoRoot = opts.repoRoot ?? process.cwd();
57
+ }
58
+ async sync() {
59
+ if (!this.opts.specFile) throw new Error("PostmanTooling.sync() requires `specFile` in options (local OpenAPI JSON path)");
60
+ const specPath = resolve(this.repoRoot, this.opts.specFile);
61
+ const specText = await readFile(specPath, "utf8");
62
+ const specObj = JSON.parse(specText);
63
+ const inferredName = specObj.info?.title ?? basename(specPath);
64
+ const specName = this.opts.specName ?? inferredName;
65
+ const collectionName = this.opts.collectionName ?? specName;
66
+ const existingSpec = await this.findSpecByName({
67
+ workspaceId: this.opts.workspaceId,
68
+ name: specName
69
+ });
70
+ if (existingSpec) await this.upsertSpecFile({
71
+ specId: existingSpec.id,
72
+ filePath: "index.json",
73
+ content: specText
74
+ });
75
+ else await this.createSpec({
76
+ workspaceId: this.opts.workspaceId,
77
+ name: specName,
78
+ fileContent: specText
79
+ });
80
+ const existingCollection = await this.findCollectionByName({
81
+ workspaceId: this.opts.workspaceId,
82
+ name: collectionName
83
+ });
84
+ if (existingCollection) await this.deleteCollection(existingCollection.uid);
85
+ await this.importOpenApi({
86
+ workspaceId: this.opts.workspaceId,
87
+ spec: specObj
88
+ });
89
+ for (const file of this.opts.envFiles ?? []) {
90
+ const envText = await readFile(resolve(this.repoRoot, file), "utf8");
91
+ const envObj = JSON.parse(envText);
92
+ if (!envObj.name) throw new Error(`Postman environment file ${file} is missing a "name" field`);
93
+ const existing = await this.findEnvironmentByName({
94
+ workspaceId: this.opts.workspaceId,
95
+ name: envObj.name
96
+ });
97
+ if (existing) await this.updateEnvironment({
98
+ uid: existing.uid,
99
+ environment: envObj
100
+ });
101
+ else await this.createEnvironment({
102
+ workspaceId: this.opts.workspaceId,
103
+ environment: envObj
104
+ });
105
+ }
106
+ }
107
+ async doctor() {
108
+ try {
109
+ const me = await this.getMyself();
110
+ const found = (await this.listWorkspaces()).find((w) => w.id === this.opts.workspaceId);
111
+ if (!found) return {
112
+ ok: false,
113
+ message: `authed as ${me.username}, but workspace ${this.opts.workspaceId} not visible`
114
+ };
115
+ return {
116
+ ok: true,
117
+ message: `authed as ${me.username}; workspace ${found.name} (${found.type}) accessible`
118
+ };
119
+ } catch (err) {
120
+ return {
121
+ ok: false,
122
+ message: err instanceof Error ? err.message : String(err)
123
+ };
124
+ }
125
+ }
126
+ async getMyself() {
127
+ const raw = await this.rest.request("/me");
128
+ return {
129
+ id: raw.user.id,
130
+ username: raw.user.username,
131
+ fullName: raw.user.fullName
132
+ };
133
+ }
134
+ async listWorkspaces() {
135
+ return (await this.rest.request("/workspaces")).workspaces.map((w) => ({
136
+ id: w.id,
137
+ name: w.name,
138
+ type: w.type
139
+ }));
140
+ }
141
+ async findCollectionByName(input) {
142
+ const match = (await this.rest.request("/collections", { query: { workspace: input.workspaceId } })).collections.find((c) => c.name === input.name);
143
+ return match ? {
144
+ id: match.id,
145
+ uid: match.uid,
146
+ name: match.name
147
+ } : null;
148
+ }
149
+ async deleteCollection(uid) {
150
+ await this.rest.request(`/collections/${encodeURIComponent(uid)}`, { method: "DELETE" });
151
+ }
152
+ async importOpenApi(input) {
153
+ const created = (await this.rest.request("/import/openapi", {
154
+ method: "POST",
155
+ query: { workspace: input.workspaceId },
156
+ body: {
157
+ type: "string",
158
+ input: typeof input.spec === "string" ? input.spec : JSON.stringify(input.spec)
159
+ }
160
+ })).collections[0];
161
+ if (!created) throw new ProviderApiError("Postman returned no collection on import — check the OpenAPI spec is well-formed", 500, void 0);
162
+ return {
163
+ id: created.id,
164
+ uid: created.uid,
165
+ name: created.name
166
+ };
167
+ }
168
+ async listEnvironments(input) {
169
+ return (await this.rest.request("/environments", { query: { workspace: input.workspaceId } })).environments.map((e) => ({
170
+ id: e.id,
171
+ uid: e.uid,
172
+ name: e.name
173
+ }));
174
+ }
175
+ async findEnvironmentByName(input) {
176
+ return (await this.listEnvironments(input)).find((e) => e.name === input.name) ?? null;
177
+ }
178
+ async createEnvironment(input) {
179
+ const raw = await this.rest.request("/environments", {
180
+ method: "POST",
181
+ query: { workspace: input.workspaceId },
182
+ body: { environment: input.environment }
183
+ });
184
+ return {
185
+ id: raw.environment.id,
186
+ uid: raw.environment.uid,
187
+ name: raw.environment.name
188
+ };
189
+ }
190
+ async updateEnvironment(input) {
191
+ const raw = await this.rest.request(`/environments/${encodeURIComponent(input.uid)}`, {
192
+ method: "PUT",
193
+ body: { environment: input.environment }
194
+ });
195
+ return {
196
+ id: raw.environment.id,
197
+ uid: raw.environment.uid,
198
+ name: raw.environment.name
199
+ };
200
+ }
201
+ async findSpecByName(input) {
202
+ const match = (await this.rest.request("/specs", { query: { workspaceId: input.workspaceId } })).specs.find((s) => s.name === input.name);
203
+ return match ? {
204
+ id: match.id,
205
+ name: match.name,
206
+ type: match.type
207
+ } : null;
208
+ }
209
+ async createSpec(input) {
210
+ const raw = await this.rest.request("/specs", {
211
+ method: "POST",
212
+ query: { workspaceId: input.workspaceId },
213
+ body: {
214
+ name: input.name,
215
+ type: input.type ?? "OPENAPI:3.0",
216
+ files: [{
217
+ path: input.filePath ?? "index.json",
218
+ content: input.fileContent
219
+ }]
220
+ }
221
+ });
222
+ return {
223
+ id: raw.id,
224
+ name: raw.name,
225
+ type: raw.type
226
+ };
227
+ }
228
+ async upsertSpecFile(input) {
229
+ await this.rest.request(`/specs/${encodeURIComponent(input.specId)}/files/${encodeURIComponent(input.filePath)}`, {
230
+ method: "PATCH",
231
+ body: { content: input.content }
232
+ });
233
+ }
234
+ };
235
+ //#endregion
236
+ //#region src/errors.ts
237
+ /**
238
+ * Postman-specific errors that callers might want to discriminate.
239
+ */
240
+ /**
241
+ * Thrown when Postman returns a `limitReachedError` (e.g., a Free-tier
242
+ * account hitting the "0 APIs" cap). Callers can render "upgrade
243
+ * required" instead of dumping the raw API body.
244
+ */
245
+ var PostmanPlanLimitError = class extends Error {
246
+ limitMessage;
247
+ body;
248
+ name = "PostmanPlanLimitError";
249
+ constructor(limitMessage, body) {
250
+ super(limitMessage);
251
+ this.limitMessage = limitMessage;
252
+ this.body = body;
253
+ }
254
+ };
255
+ /**
256
+ * Inspect a Postman error body for the plan-limit shape. Returns the
257
+ * limit message when matched; null otherwise (caller throws a generic
258
+ * `ProviderApiError`).
259
+ */
260
+ function detectPlanLimit(body) {
261
+ try {
262
+ const parsed = JSON.parse(body);
263
+ if (parsed.error?.name === "limitReachedError" && parsed.error.message) return parsed.error.message;
264
+ } catch {}
265
+ return null;
266
+ }
267
+ //#endregion
268
+ //#region src/rest.ts
269
+ /**
270
+ * Thin REST wrapper around api.getpostman.com.
271
+ *
272
+ * Same pattern as the github / vercel / neon / clerk REST clients —
273
+ * the Postman API uses `x-api-key` (not bearer) for auth, but
274
+ * otherwise the shape's identical: JSON only, transport-failure
275
+ * wrapping with `status: 0`.
276
+ */
277
+ var PostmanRestClient = class {
278
+ token;
279
+ fetchImpl;
280
+ baseUrl;
281
+ constructor(opts) {
282
+ this.token = opts.token;
283
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
284
+ this.baseUrl = (opts.baseUrl ?? "https://api.getpostman.com").replace(/\/+$/, "");
285
+ }
286
+ async request(path, opts = {}) {
287
+ const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
288
+ for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
289
+ const fullUrl = url.toString();
290
+ const headers = {
291
+ "x-api-key": this.token,
292
+ accept: "application/json"
293
+ };
294
+ const init = {
295
+ method: opts.method ?? "GET",
296
+ headers
297
+ };
298
+ if (opts.body !== void 0) {
299
+ headers["content-type"] = "application/json";
300
+ init.body = JSON.stringify(opts.body);
301
+ }
302
+ let res;
303
+ try {
304
+ res = await this.fetchImpl(fullUrl, init);
305
+ } catch (err) {
306
+ const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
307
+ throw new ProviderApiError(`Postman ${init.method} ${path} failed: ${detail}`, 0, void 0);
308
+ }
309
+ if (!res.ok) {
310
+ const body = await res.text().catch(() => "");
311
+ const limit = detectPlanLimit(body);
312
+ if (limit) throw new PostmanPlanLimitError(limit, body);
313
+ throw new ProviderApiError(`Postman ${init.method} ${path} → ${res.status}`, res.status, body);
314
+ }
315
+ if (res.status === 204) return void 0;
316
+ const text = await res.text();
317
+ if (!text) return void 0;
318
+ return JSON.parse(text);
319
+ }
320
+ };
321
+ //#endregion
322
+ //#region src/index.ts
323
+ function createContext(options) {
324
+ if (!options.workspaceId) throw new Error("@theholocron/holocron-plugin-postman requires `workspaceId` in options");
325
+ const restOpts = { token: resolveToken(options) };
326
+ if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
327
+ if (options.fetch !== void 0) restOpts.fetch = options.fetch;
328
+ return {
329
+ options,
330
+ rest: new PostmanRestClient(restOpts)
331
+ };
332
+ }
333
+ function tooling(ctx) {
334
+ const opts = { workspaceId: ctx.options.workspaceId };
335
+ if (ctx.options.specFile !== void 0) opts.specFile = ctx.options.specFile;
336
+ if (ctx.options.specName !== void 0) opts.specName = ctx.options.specName;
337
+ if (ctx.options.collectionName !== void 0) opts.collectionName = ctx.options.collectionName;
338
+ if (ctx.options.envFiles !== void 0) opts.envFiles = ctx.options.envFiles;
339
+ if (ctx.options.repoRoot !== void 0) opts.repoRoot = ctx.options.repoRoot;
340
+ return new PostmanTooling(ctx.rest, opts);
341
+ }
342
+ function createPlugin(options) {
343
+ const ctx = createContext(options);
344
+ return {
345
+ name: "@theholocron/holocron-plugin-postman",
346
+ capabilities: { tooling: () => tooling(ctx) }
347
+ };
348
+ }
349
+ //#endregion
350
+ export { AuthError, PostmanPlanLimitError, PostmanRestClient, PostmanTooling, createContext, createPlugin, detectPlanLimit, resolveToken, tooling };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@theholocron/holocron-plugin-postman",
3
+ "version": "2.0.0-alpha.0",
4
+ "description": "Holocron plugin for Postman. Implements the tooling capability against Postman's REST API — workspace + collection + spec + environment sync.",
5
+ "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-postman#readme",
6
+ "bugs": "https://github.com/theholocron/holocron/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/theholocron/holocron.git",
10
+ "directory": "packages/holocron-plugin-postman"
11
+ },
12
+ "license": "MIT",
13
+ "author": "Newton Koumantzelis",
14
+ "type": "module",
15
+ "main": "./dist/index.mjs",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.mts",
19
+ "import": "./dist/index.mjs",
20
+ "default": "./dist/index.mjs"
21
+ }
22
+ },
23
+ "peerDependencies": {
24
+ "@theholocron/cli": "2.0.0-alpha.0"
25
+ },
26
+ "devDependencies": {
27
+ "@theholocron/tsconfig": "^4.1.0",
28
+ "@tsconfig/node-lts": "^24.0.0",
29
+ "@vitest/coverage-v8": "^3.2.6",
30
+ "eslint": "^9.36.0",
31
+ "globals": "^16.5.0",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^3.2.6",
34
+ "tsdown": "^0.22.3",
35
+ "@theholocron/cli": "2.0.0-alpha.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsdown",
46
+ "lint": "eslint .",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "test:coverage": "vitest run --coverage"
51
+ },
52
+ "types": "./dist/index.d.mts"
53
+ }