@apicircle/cli 1.0.3 → 1.0.5

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.
@@ -0,0 +1,1560 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // package.json
35
+ var package_default;
36
+ var init_package = __esm({
37
+ "package.json"() {
38
+ package_default = {
39
+ name: "@apicircle/cli",
40
+ version: "1.0.5",
41
+ private: false,
42
+ type: "module",
43
+ description: "Command-line interface for API Circle Studio. Run mock servers, drive the MCP server, and import OpenAPI / Postman / Insomnia collections from any terminal.",
44
+ license: "SEE LICENSE IN LICENSE",
45
+ repository: {
46
+ type: "git",
47
+ url: "git+https://github.com/apicircle/studio.git",
48
+ directory: "packages/cli"
49
+ },
50
+ homepage: "https://github.com/apicircle/studio#readme",
51
+ bugs: "https://github.com/apicircle/studio/issues",
52
+ engines: {
53
+ node: ">=20"
54
+ },
55
+ main: "./src/index.ts",
56
+ types: "./src/index.ts",
57
+ bin: {
58
+ apicircle: "./dist/bin/cli.cjs"
59
+ },
60
+ exports: {
61
+ ".": "./src/index.ts"
62
+ },
63
+ files: [
64
+ "dist"
65
+ ],
66
+ publishConfig: {
67
+ main: "./dist/index.cjs",
68
+ module: "./dist/index.js",
69
+ types: "./dist/index.d.cts",
70
+ exports: {
71
+ ".": {
72
+ import: {
73
+ types: "./dist/index.d.ts",
74
+ default: "./dist/index.js"
75
+ },
76
+ require: {
77
+ types: "./dist/index.d.cts",
78
+ default: "./dist/index.cjs"
79
+ }
80
+ }
81
+ }
82
+ },
83
+ scripts: {
84
+ build: "tsup",
85
+ check: "tsc --noEmit",
86
+ test: "vitest run",
87
+ clean: "rm -rf dist node_modules"
88
+ },
89
+ dependencies: {
90
+ "@apicircle/core": "workspace:*",
91
+ "@apicircle/mcp-server": "workspace:*",
92
+ "@apicircle/mock-server-core": "workspace:*",
93
+ "@apicircle/shared": "workspace:*",
94
+ commander: "^12.0.0",
95
+ kleur: "^4.1.5"
96
+ },
97
+ devDependencies: {
98
+ "@types/node": "^20.0.0",
99
+ tsup: "^8.3.0",
100
+ typescript: "^5.4.0",
101
+ vitest: "^2.0.0"
102
+ }
103
+ };
104
+ }
105
+ });
106
+
107
+ // src/packageVersion.ts
108
+ function readPackageVersion() {
109
+ const version = package_default.version;
110
+ if (typeof version !== "string" || version.length === 0) {
111
+ throw new Error("Unable to read @apicircle/cli package version");
112
+ }
113
+ return version;
114
+ }
115
+ var CLI_PACKAGE_VERSION;
116
+ var init_packageVersion = __esm({
117
+ "src/packageVersion.ts"() {
118
+ "use strict";
119
+ init_package();
120
+ CLI_PACKAGE_VERSION = readPackageVersion();
121
+ }
122
+ });
123
+
124
+ // src/commands/mock.ts
125
+ function registerMockCommand(program) {
126
+ program.command("mock").description("Run a mock server from an OpenAPI / Postman / Insomnia file").argument("<spec>", "Path to the spec file").option("-p, --port <number>", "TCP port to bind (defaults to a free port)").option("-h, --host <host>", "Hostname to bind", "127.0.0.1").option("-t, --type <type>", "Source type: openapi | postman | insomnia | auto", "auto").option("-f, --format <format>", "OpenAPI format: json | yaml | auto", "auto").option("--cors", "Enable permissive CORS", true).action(async (spec, opts) => {
127
+ const absolute = path.resolve(spec);
128
+ const raw = await import_node_fs.promises.readFile(absolute, "utf-8");
129
+ const type = inferType(absolute, opts.type ?? "auto");
130
+ const format = type === "openapi" ? inferFormat(absolute, opts.format ?? "auto") : "json";
131
+ const source = makeSource(type, format, raw);
132
+ const parsed = await (0, import_mock_server_core.parseSourceToEndpoints)(source);
133
+ const mock = {
134
+ id: (0, import_shared.generateId)(),
135
+ name: path.basename(absolute),
136
+ source,
137
+ endpoints: parsed.endpoints,
138
+ defaultPort: opts.port ? Number(opts.port) : null,
139
+ cors: { enabled: opts.cors !== false, origins: ["*"] },
140
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
141
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
142
+ };
143
+ const handle = await (0, import_mock_server_core.startMockServer)(mock, {
144
+ port: opts.port ? Number(opts.port) : void 0,
145
+ host: opts.host
146
+ });
147
+ process.stdout.write(
148
+ `${import_kleur.default.green("Mock server")} listening on ${import_kleur.default.cyan(`http://${opts.host}:${handle.port}`)} with ${parsed.endpoints.length} endpoints (type=${type}). Press Ctrl-C to stop.
149
+ `
150
+ );
151
+ if (parsed.warnings.length) {
152
+ for (const w of parsed.warnings) {
153
+ process.stderr.write(`${import_kleur.default.yellow("warn")}: ${w}
154
+ `);
155
+ }
156
+ }
157
+ installShutdown(handle);
158
+ });
159
+ }
160
+ function inferType(filePath, hint) {
161
+ if (hint && hint !== "auto") return hint;
162
+ const lower = filePath.toLowerCase();
163
+ if (lower.includes("postman")) return "postman";
164
+ if (lower.includes("insomnia")) return "insomnia";
165
+ return "openapi";
166
+ }
167
+ function inferFormat(filePath, hint) {
168
+ if (hint && hint !== "auto") return hint;
169
+ const lower = filePath.toLowerCase();
170
+ return lower.endsWith(".yaml") || lower.endsWith(".yml") ? "yaml" : "json";
171
+ }
172
+ function makeSource(type, format, raw) {
173
+ switch (type) {
174
+ case "openapi":
175
+ return { kind: "openapi", spec: raw, format };
176
+ case "postman":
177
+ return { kind: "postman", collection: raw };
178
+ case "insomnia":
179
+ return { kind: "insomnia", export: raw };
180
+ }
181
+ }
182
+ function installShutdown(handle) {
183
+ let closing = false;
184
+ const shutdown = async () => {
185
+ if (closing) return;
186
+ closing = true;
187
+ await handle.close();
188
+ process.exit(0);
189
+ };
190
+ process.on("SIGINT", () => void shutdown());
191
+ process.on("SIGTERM", () => void shutdown());
192
+ }
193
+ var import_node_fs, path, import_kleur, import_mock_server_core, import_shared;
194
+ var init_mock = __esm({
195
+ "src/commands/mock.ts"() {
196
+ "use strict";
197
+ import_node_fs = require("fs");
198
+ path = __toESM(require("path"), 1);
199
+ import_kleur = __toESM(require("kleur"), 1);
200
+ import_mock_server_core = require("@apicircle/mock-server-core");
201
+ import_shared = require("@apicircle/shared");
202
+ }
203
+ });
204
+
205
+ // src/util/loadWorkspace.ts
206
+ async function ensureWorkspace(dir) {
207
+ const resolved = path2.resolve(dir);
208
+ await import_node_fs2.promises.mkdir(resolved, { recursive: true });
209
+ const existing = await (0, import_file_backed.loadFromFile)(resolved, { allowMissing: true });
210
+ if (existing) return existing;
211
+ const now = (/* @__PURE__ */ new Date()).toISOString();
212
+ const workspaceId = (0, import_shared2.generateId)();
213
+ const fresh = {
214
+ synced: {
215
+ schemaVersion: 1,
216
+ workspaceId,
217
+ collections: {
218
+ tree: { id: (0, import_shared2.generateId)(), type: "root", children: [] },
219
+ requests: {},
220
+ folders: {}
221
+ },
222
+ environments: { items: {}, activeName: null, priorityOrder: [] },
223
+ linkedWorkspaces: {},
224
+ linkedOverrides: { requests: {}, environmentVars: {} },
225
+ releases: { self: null, perLink: {} },
226
+ globalAssets: { schemas: {}, graphql: {}, files: {} },
227
+ mockServers: {},
228
+ meta: { createdAt: now, updatedAt: now, appVersion: "1.0.0" }
229
+ },
230
+ local: {
231
+ schemaVersion: 1,
232
+ workspaceId,
233
+ executionPlans: {},
234
+ history: { requestRuns: [], planRuns: [] },
235
+ secretIndex: { entries: {} },
236
+ sessions: { github: { workspace: null, links: {} } },
237
+ connectedRepo: null,
238
+ workingBranch: null,
239
+ seededWorkspaceSha: null,
240
+ retiredBranch: null,
241
+ sync: { lastPulledSnapshot: null, lastPulledSha: null, lastPulledAt: null, dirtyKeys: [] },
242
+ linkedCollections: {},
243
+ attachmentCache: {},
244
+ globalContext: {},
245
+ mockRuntime: { active: {} },
246
+ ui: {
247
+ activeRequestId: null,
248
+ sidebarExpandedSections: [],
249
+ themeId: "command-center",
250
+ fontId: "cascadia-code",
251
+ fontSizePercent: import_shared2.FONT_SIZE_PERCENT_DEFAULT
252
+ },
253
+ settings: { validateOnSend: true, monacoConsumesWheel: false },
254
+ snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 }
255
+ }
256
+ };
257
+ await (0, import_file_backed.saveToFile)(resolved, fresh);
258
+ return fresh;
259
+ }
260
+ var path2, import_node_fs2, import_file_backed, import_shared2;
261
+ var init_loadWorkspace = __esm({
262
+ "src/util/loadWorkspace.ts"() {
263
+ "use strict";
264
+ path2 = __toESM(require("path"), 1);
265
+ import_node_fs2 = require("fs");
266
+ import_file_backed = require("@apicircle/core/workspace/file-backed");
267
+ import_shared2 = require("@apicircle/shared");
268
+ }
269
+ });
270
+
271
+ // src/util/resolveWorkspace.ts
272
+ function defaultWorkspacesRoot() {
273
+ const override = process.env.APICIRCLE_WORKSPACES_ROOT;
274
+ if (override && override.length > 0) return path3.resolve(override);
275
+ return path3.join(electronUserDataDir(), WORKSPACES_DIRNAME);
276
+ }
277
+ function electronUserDataDir() {
278
+ const home = os.homedir();
279
+ switch (process.platform) {
280
+ case "win32": {
281
+ const appdata = process.env.APPDATA ?? path3.join(home, "AppData", "Roaming");
282
+ return path3.join(appdata, APP_NAME, APP_SUBDIR);
283
+ }
284
+ case "darwin":
285
+ return path3.join(home, "Library", "Application Support", APP_NAME, APP_SUBDIR);
286
+ default:
287
+ return path3.join(
288
+ process.env.XDG_CONFIG_HOME ?? path3.join(home, ".config"),
289
+ APP_NAME,
290
+ APP_SUBDIR
291
+ );
292
+ }
293
+ }
294
+ async function resolveWorkspace(opts = {}) {
295
+ const root = opts.workspacesRoot ?? defaultWorkspacesRoot();
296
+ const nameSelector = opts.name?.trim();
297
+ const pathSelector = opts.path?.trim();
298
+ const expectExists = opts.expectExists ?? true;
299
+ if (nameSelector && pathSelector) {
300
+ throw new WorkspaceResolutionError(
301
+ "--workspace-name and --workspace-path are mutually exclusive. Pass one or neither.",
302
+ "both-flags"
303
+ );
304
+ }
305
+ if (pathSelector) {
306
+ const expanded = expandTilde(pathSelector);
307
+ const dir = path3.resolve(expanded);
308
+ if (expectExists && !await dirExists(dir)) {
309
+ throw new WorkspaceResolutionError(`Workspace directory not found: ${dir}`, "path-missing");
310
+ }
311
+ return { dir, id: null, name: null, fromRegistry: false, registryRoot: null };
312
+ }
313
+ const registry = await (0, import_registry.loadRegistry)(root);
314
+ if (nameSelector) {
315
+ if (!registry) {
316
+ throw new WorkspaceResolutionError(
317
+ `No workspaces are registered at ${root}. Open the desktop app once to seed the registry, or pass --workspace-path <dir> to point at a workspace directory directly.`,
318
+ "no-registry"
319
+ );
320
+ }
321
+ const entry3 = (0, import_registry.findWorkspaceEntry)(registry, nameSelector);
322
+ if (!entry3) {
323
+ throw new WorkspaceResolutionError(
324
+ `No workspace named "${nameSelector}" in the registry at ${root}. Run \`apicircle workspaces list\` to see what's available.`,
325
+ "not-found"
326
+ );
327
+ }
328
+ return {
329
+ dir: (0, import_registry.workspaceDirFor)(root, entry3.id),
330
+ id: entry3.id,
331
+ name: entry3.name,
332
+ fromRegistry: true,
333
+ registryRoot: root
334
+ };
335
+ }
336
+ if (registry && registry.activeWorkspaceId) {
337
+ const active = registry.workspaces.find((w) => w.id === registry.activeWorkspaceId);
338
+ if (active) {
339
+ return {
340
+ dir: (0, import_registry.workspaceDirFor)(root, active.id),
341
+ id: active.id,
342
+ name: active.name,
343
+ fromRegistry: true,
344
+ registryRoot: root
345
+ };
346
+ }
347
+ }
348
+ return {
349
+ dir: path3.resolve(process.cwd()),
350
+ id: null,
351
+ name: null,
352
+ fromRegistry: false,
353
+ registryRoot: null
354
+ };
355
+ }
356
+ async function createWorkspaceOnDisk(args) {
357
+ const root = args.workspacesRoot ?? defaultWorkspacesRoot();
358
+ const trimmed = args.name.trim();
359
+ if (!trimmed) throw new Error("Workspace name is required");
360
+ const existing = await (0, import_registry.loadRegistry)(root);
361
+ if (existing && existing.workspaces.some((w) => w.name.toLowerCase() === trimmed.toLowerCase())) {
362
+ throw new Error(`A workspace named "${trimmed}" already exists`);
363
+ }
364
+ const workspaceId = (0, import_shared3.generateId)();
365
+ const now = (/* @__PURE__ */ new Date()).toISOString();
366
+ const state = buildEmptyState(workspaceId, now, args.sampleRequest ?? false);
367
+ const dir = (0, import_registry.workspaceDirFor)(root, workspaceId);
368
+ await (0, import_file_backed2.saveToFile)(dir, state);
369
+ const entry3 = {
370
+ id: workspaceId,
371
+ name: trimmed,
372
+ createdAt: now,
373
+ lastOpenedAt: now
374
+ };
375
+ const registry = await (0, import_registry.registerWorkspace)(root, entry3);
376
+ return { registry, entry: entry3, state, dir };
377
+ }
378
+ async function listWorkspacesOnDisk(args = {}) {
379
+ const root = args.workspacesRoot ?? defaultWorkspacesRoot();
380
+ const registry = await (0, import_registry.loadRegistry)(root) ?? {
381
+ schemaVersion: 1,
382
+ activeWorkspaceId: null,
383
+ workspaces: []
384
+ };
385
+ return { registry, root };
386
+ }
387
+ async function saveRegistryToDisk(registry, workspacesRoot) {
388
+ await (0, import_registry.saveRegistry)(workspacesRoot ?? defaultWorkspacesRoot(), registry);
389
+ }
390
+ function expandTilde(value) {
391
+ if (value === "~") return os.homedir();
392
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
393
+ return path3.join(os.homedir(), value.slice(2));
394
+ }
395
+ return value;
396
+ }
397
+ async function dirExists(p) {
398
+ try {
399
+ const st = await import_node_fs3.promises.stat(p);
400
+ return st.isDirectory();
401
+ } catch {
402
+ return false;
403
+ }
404
+ }
405
+ function buildEmptyState(workspaceId, now, withSample) {
406
+ const sample = withSample ? {
407
+ id: (0, import_shared3.generateId)(),
408
+ name: "Sample: GET /anything",
409
+ folderId: null,
410
+ method: "GET",
411
+ url: "https://httpbin.org/anything",
412
+ headers: [{ key: "Accept", value: "application/json", enabled: true }],
413
+ query: [],
414
+ body: { type: "none", content: "" },
415
+ auth: { type: "inherit" },
416
+ contextVars: [],
417
+ extractions: [],
418
+ assertions: [],
419
+ createdAt: now,
420
+ updatedAt: now
421
+ } : null;
422
+ const folders = {};
423
+ const requests = sample ? { [sample.id]: sample } : {};
424
+ return {
425
+ synced: {
426
+ schemaVersion: 1,
427
+ workspaceId,
428
+ collections: {
429
+ tree: {
430
+ id: (0, import_shared3.generateId)(),
431
+ type: "root",
432
+ children: sample ? [{ kind: "request", id: sample.id }] : []
433
+ },
434
+ requests,
435
+ folders
436
+ },
437
+ environments: { items: {}, activeName: null, priorityOrder: [] },
438
+ linkedWorkspaces: {},
439
+ linkedOverrides: { requests: {}, environmentVars: {} },
440
+ releases: { self: null, perLink: {} },
441
+ globalAssets: { schemas: {}, graphql: {}, files: {} },
442
+ mockServers: {},
443
+ meta: { createdAt: now, updatedAt: now, appVersion: "1.0.0" }
444
+ },
445
+ local: {
446
+ schemaVersion: 1,
447
+ workspaceId,
448
+ executionPlans: {},
449
+ history: { requestRuns: [], planRuns: [] },
450
+ secretIndex: { entries: {} },
451
+ sessions: { github: { workspace: null, links: {} } },
452
+ connectedRepo: null,
453
+ workingBranch: null,
454
+ seededWorkspaceSha: null,
455
+ retiredBranch: null,
456
+ sync: { lastPulledSnapshot: null, lastPulledSha: null, lastPulledAt: null, dirtyKeys: [] },
457
+ linkedCollections: {},
458
+ attachmentCache: {},
459
+ globalContext: {},
460
+ mockRuntime: { active: {} },
461
+ ui: {
462
+ activeRequestId: sample?.id ?? null,
463
+ sidebarExpandedSections: [],
464
+ themeId: "command-center",
465
+ fontId: "cascadia-code",
466
+ fontSizePercent: 100
467
+ },
468
+ settings: { validateOnSend: true, monacoConsumesWheel: false },
469
+ snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 }
470
+ }
471
+ };
472
+ }
473
+ var os, path3, import_node_fs3, import_registry, import_file_backed2, import_shared3, APP_NAME, APP_SUBDIR, WORKSPACES_DIRNAME, WorkspaceResolutionError;
474
+ var init_resolveWorkspace = __esm({
475
+ "src/util/resolveWorkspace.ts"() {
476
+ "use strict";
477
+ os = __toESM(require("os"), 1);
478
+ path3 = __toESM(require("path"), 1);
479
+ import_node_fs3 = require("fs");
480
+ import_registry = require("@apicircle/core/workspace/registry");
481
+ import_file_backed2 = require("@apicircle/core/workspace/file-backed");
482
+ import_shared3 = require("@apicircle/shared");
483
+ APP_NAME = "@apicircle";
484
+ APP_SUBDIR = "desktop";
485
+ WORKSPACES_DIRNAME = "workspaces";
486
+ WorkspaceResolutionError = class extends Error {
487
+ code;
488
+ constructor(message, code) {
489
+ super(message);
490
+ this.name = "WorkspaceResolutionError";
491
+ this.code = code;
492
+ }
493
+ };
494
+ }
495
+ });
496
+
497
+ // src/commands/mcp.ts
498
+ function registerMcpCommand(program) {
499
+ program.command("mcp").description("Run the API Circle MCP server (stdio transport)").option(
500
+ "--workspace-name <name-or-id>",
501
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
502
+ ).option(
503
+ "--workspace-path <dir>",
504
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
505
+ ).action(async (opts) => {
506
+ let dir;
507
+ let label;
508
+ try {
509
+ const resolved = await resolveWorkspace({
510
+ name: opts.workspaceName,
511
+ path: opts.workspacePath,
512
+ expectExists: false
513
+ });
514
+ dir = resolved.dir;
515
+ label = resolved.fromRegistry ? `${resolved.name ?? resolved.id} (${dir})` : dir;
516
+ } catch (err) {
517
+ if (err instanceof WorkspaceResolutionError) {
518
+ process.stderr.write(`${import_kleur2.default.red("error")}: ${err.message}
519
+ `);
520
+ process.exit(2);
521
+ }
522
+ throw err;
523
+ }
524
+ try {
525
+ await ensureWorkspace(dir);
526
+ } catch (err) {
527
+ process.stderr.write(
528
+ `${import_kleur2.default.red("failed to initialise workspace")} at ${dir}: ${err instanceof Error ? err.message : String(err)}
529
+ `
530
+ );
531
+ process.exit(1);
532
+ }
533
+ const workspace = new import_mcp_server.FileBackedWorkspaceProvider(dir);
534
+ const mock = new import_mcp_server.InProcessMockController();
535
+ const host = (0, import_mcp_server.createMcpServer)({ workspace, mock });
536
+ process.stderr.write(`${import_kleur2.default.green("apicircle-mcp")} ready \xB7 workspace=${label}
537
+ `);
538
+ await host.connect();
539
+ });
540
+ }
541
+ var import_kleur2, import_mcp_server;
542
+ var init_mcp = __esm({
543
+ "src/commands/mcp.ts"() {
544
+ "use strict";
545
+ import_kleur2 = __toESM(require("kleur"), 1);
546
+ import_mcp_server = require("@apicircle/mcp-server");
547
+ init_loadWorkspace();
548
+ init_resolveWorkspace();
549
+ }
550
+ });
551
+
552
+ // src/commands/import.ts
553
+ function registerImportCommand(program) {
554
+ program.command("import").description("Import a spec into a workspace folder").argument("<type>", "Source type: openapi | postman | insomnia | curl").argument("<input>", "Path to a spec file, or `-` to read from stdin").option(
555
+ "--workspace-name <name-or-id>",
556
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
557
+ ).option(
558
+ "--workspace-path <dir>",
559
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
560
+ ).option("-f, --format <format>", "OpenAPI format: json | yaml", "json").action(async (type, input, opts) => {
561
+ let dir;
562
+ try {
563
+ const resolved = await resolveWorkspace({
564
+ name: opts.workspaceName,
565
+ path: opts.workspacePath,
566
+ expectExists: false
567
+ });
568
+ dir = resolved.dir;
569
+ if (resolved.fromRegistry) {
570
+ process.stderr.write(
571
+ `${import_kleur3.default.dim("workspace")}: ${import_kleur3.default.cyan(resolved.name ?? resolved.id ?? "")} ${import_kleur3.default.dim(`(${dir})`)}
572
+ `
573
+ );
574
+ }
575
+ } catch (err) {
576
+ if (err instanceof WorkspaceResolutionError) {
577
+ process.stderr.write(`${import_kleur3.default.red("error")}: ${err.message}
578
+ `);
579
+ process.exit(2);
580
+ }
581
+ throw err;
582
+ }
583
+ const raw = await readInput(input);
584
+ const state = await ensureWorkspace(dir);
585
+ let nextSynced = state.synced;
586
+ let nextLocal = state.local;
587
+ const created = [];
588
+ const append = (req) => {
589
+ const out = (0, import_core.applyMutation)(
590
+ { synced: nextSynced, local: nextLocal },
591
+ { kind: "request.create", request: req }
592
+ );
593
+ nextSynced = out.next.synced;
594
+ nextLocal = out.next.local;
595
+ created.push(req.id);
596
+ };
597
+ if (type === "curl") {
598
+ const { parseCurl } = await import("@apicircle/core");
599
+ const parsed = parseCurl(raw);
600
+ append(
601
+ blankRequest({
602
+ name: `cURL ${parsed.method} ${parsed.url}`.slice(0, 80),
603
+ method: parsed.method,
604
+ url: parsed.url,
605
+ headers: parsed.headers,
606
+ query: parsed.query,
607
+ body: parsed.body,
608
+ auth: parsed.auth
609
+ })
610
+ );
611
+ } else if (type === "openapi") {
612
+ const parsed = await (0, import_mock_server_core2.parseOpenApiToEndpoints)(raw, opts.format ?? "json");
613
+ for (const ep of parsed.endpoints) {
614
+ append(
615
+ blankRequest({
616
+ name: ep.example ?? `${ep.method} ${ep.pathPattern}`,
617
+ method: ep.method,
618
+ url: ep.pathPattern
619
+ })
620
+ );
621
+ }
622
+ } else if (type === "postman") {
623
+ const parsed = (0, import_mock_server_core2.parsePostmanToEndpoints)(raw);
624
+ for (const ep of parsed.endpoints) {
625
+ append(
626
+ blankRequest({
627
+ name: ep.example ?? `${ep.method} ${ep.pathPattern}`,
628
+ method: ep.method,
629
+ url: ep.pathPattern
630
+ })
631
+ );
632
+ }
633
+ } else if (type === "insomnia") {
634
+ const parsed = (0, import_mock_server_core2.parseInsomniaToEndpoints)(raw);
635
+ for (const ep of parsed.endpoints) {
636
+ append(
637
+ blankRequest({
638
+ name: ep.example ?? `${ep.method} ${ep.pathPattern}`,
639
+ method: ep.method,
640
+ url: ep.pathPattern
641
+ })
642
+ );
643
+ }
644
+ } else {
645
+ process.stderr.write(`${import_kleur3.default.red("error")}: unknown type '${String(type)}'
646
+ `);
647
+ process.exit(2);
648
+ }
649
+ await (0, import_file_backed3.saveToFile)(dir, { synced: nextSynced, local: nextLocal });
650
+ process.stdout.write(
651
+ `${import_kleur3.default.green("imported")} ${created.length} request${created.length === 1 ? "" : "s"} into ${dir}
652
+ `
653
+ );
654
+ });
655
+ }
656
+ async function readInput(p) {
657
+ if (p === "-") {
658
+ return new Promise((resolve7, reject) => {
659
+ let data = "";
660
+ process.stdin.setEncoding("utf-8");
661
+ process.stdin.on("data", (chunk) => {
662
+ data += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
663
+ });
664
+ process.stdin.on("end", () => resolve7(data));
665
+ process.stdin.on("error", reject);
666
+ });
667
+ }
668
+ return import_node_fs4.promises.readFile(path4.resolve(p), "utf-8");
669
+ }
670
+ function blankRequest(partial) {
671
+ const now = (/* @__PURE__ */ new Date()).toISOString();
672
+ return {
673
+ id: (0, import_shared4.generateId)(),
674
+ folderId: null,
675
+ headers: [],
676
+ query: [],
677
+ body: { type: "none", content: "" },
678
+ auth: { type: "none" },
679
+ contextVars: [],
680
+ extractions: [],
681
+ assertions: [],
682
+ createdAt: now,
683
+ updatedAt: now,
684
+ ...partial
685
+ };
686
+ }
687
+ var import_node_fs4, path4, import_kleur3, import_core, import_file_backed3, import_mock_server_core2, import_shared4;
688
+ var init_import = __esm({
689
+ "src/commands/import.ts"() {
690
+ "use strict";
691
+ import_node_fs4 = require("fs");
692
+ path4 = __toESM(require("path"), 1);
693
+ import_kleur3 = __toESM(require("kleur"), 1);
694
+ import_core = require("@apicircle/core");
695
+ import_file_backed3 = require("@apicircle/core/workspace/file-backed");
696
+ import_mock_server_core2 = require("@apicircle/mock-server-core");
697
+ import_shared4 = require("@apicircle/shared");
698
+ init_loadWorkspace();
699
+ init_resolveWorkspace();
700
+ }
701
+ });
702
+
703
+ // src/util/secrets.ts
704
+ async function buildSecretsFromCli(options = {}) {
705
+ const env = options.env ?? process.env;
706
+ const prefix = options.envPrefix ?? DEFAULT_PREFIX;
707
+ const byId = {};
708
+ if (options.secretsFile) {
709
+ const resolved = path5.resolve(options.secretsFile);
710
+ const raw = await import_node_fs5.promises.readFile(resolved, "utf8");
711
+ const parsed = JSON.parse(raw);
712
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
713
+ throw new Error(
714
+ `--secrets ${options.secretsFile}: expected an object mapping secretKeyId \u2192 value`
715
+ );
716
+ }
717
+ for (const [id, value] of Object.entries(parsed)) {
718
+ if (typeof value !== "string") {
719
+ throw new Error(`--secrets ${options.secretsFile}: value for "${id}" must be a string`);
720
+ }
721
+ byId[id] = value;
722
+ }
723
+ }
724
+ for (const [name, value] of Object.entries(env)) {
725
+ if (!name.startsWith(prefix) || typeof value !== "string") continue;
726
+ const id = name.slice(prefix.length);
727
+ if (id) byId[id] = value;
728
+ }
729
+ return { byId };
730
+ }
731
+ var path5, import_node_fs5, DEFAULT_PREFIX;
732
+ var init_secrets = __esm({
733
+ "src/util/secrets.ts"() {
734
+ "use strict";
735
+ path5 = __toESM(require("path"), 1);
736
+ import_node_fs5 = require("fs");
737
+ DEFAULT_PREFIX = "APICIRCLE_SECRET_";
738
+ }
739
+ });
740
+
741
+ // src/util/executionAttachments.ts
742
+ async function prepareExecutionAttachments(workspaceDir, state, plan) {
743
+ const cacheDir = path6.resolve(workspaceDir, ATTACHMENTS_DIR);
744
+ const requirements = collectExecutionAttachmentRequirements(state, plan);
745
+ await import_node_fs6.promises.mkdir(cacheDir, { recursive: true });
746
+ let downloaded = 0;
747
+ let alreadyPresent = 0;
748
+ let failed = 0;
749
+ const cache = {
750
+ ...state.local.attachmentCache ?? {}
751
+ };
752
+ const entries = [];
753
+ for (const requirement of requirements) {
754
+ const localPath = path6.join(cacheDir, encodeURIComponent(requirement.slotId));
755
+ const present = await hasExpectedFile(localPath, requirement.sha256);
756
+ if (present) {
757
+ alreadyPresent++;
758
+ } else {
759
+ try {
760
+ const bytes = await downloadAttachment(requirement);
761
+ if (!bytes) {
762
+ throw new Error(
763
+ `Attachment ${attachmentLabel(requirement)} was not found in ${sourceLabel(requirement)}.`
764
+ );
765
+ }
766
+ if (requirement.sha256 && sha256Hex(bytes) !== requirement.sha256) {
767
+ throw new Error(
768
+ `Attachment ${attachmentLabel(requirement)} failed checksum verification.`
769
+ );
770
+ }
771
+ await import_node_fs6.promises.writeFile(localPath, bytes, { mode: 384 });
772
+ downloaded++;
773
+ } catch (err) {
774
+ failed++;
775
+ throw new Error(
776
+ `Attachment ${attachmentLabel(requirement)} is required by ${requiredByLabel(
777
+ requirement
778
+ )} but could not be downloaded from ${sourceLabel(requirement)}: ${err instanceof Error ? err.message : String(err)}`
779
+ );
780
+ }
781
+ }
782
+ cache[requirement.slotId] = {
783
+ slotId: requirement.slotId,
784
+ filename: requirement.filename ?? requirement.slotId,
785
+ mimeType: requirement.mimeType ?? "application/octet-stream",
786
+ size: requirement.size ?? await fileSize(localPath),
787
+ sha256: requirement.sha256,
788
+ localPath,
789
+ storage: "filesystem",
790
+ source: requirement.source,
791
+ ...requirement.linkedWorkspaceId ? { linkedWorkspaceId: requirement.linkedWorkspaceId } : {},
792
+ requiredBy: requirement.requiredBy,
793
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString()
794
+ };
795
+ entries.push({
796
+ slotId: requirement.slotId,
797
+ filename: requirement.filename ?? requirement.slotId,
798
+ localPath,
799
+ source: requirement.source,
800
+ ...requirement.linkedWorkspaceId ? { linkedWorkspaceId: requirement.linkedWorkspaceId } : {},
801
+ requiredBy: requirement.requiredBy
802
+ });
803
+ }
804
+ const nextState = {
805
+ ...state,
806
+ local: {
807
+ ...state.local,
808
+ attachmentCache: cache
809
+ }
810
+ };
811
+ return {
812
+ state: nextState,
813
+ resolveAttachment: createFileAttachmentResolver(nextState),
814
+ summary: {
815
+ total: requirements.length,
816
+ downloaded,
817
+ alreadyPresent,
818
+ failed,
819
+ cacheDir,
820
+ entries
821
+ }
822
+ };
823
+ }
824
+ function createFileAttachmentResolver(state) {
825
+ return async (slotId) => {
826
+ const meta = state.local.attachmentCache?.[slotId];
827
+ if (!meta) return null;
828
+ const bytes = await import_node_fs6.promises.readFile(meta.localPath);
829
+ const view = new Uint8Array(bytes);
830
+ const body = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
831
+ return {
832
+ blob: new Blob([body], { type: meta.mimeType }),
833
+ filename: meta.filename
834
+ };
835
+ };
836
+ }
837
+ function collectExecutionAttachmentRequirements(state, plan) {
838
+ const seen = /* @__PURE__ */ new Map();
839
+ const localRequestFilter = requestFilterForPlan(plan, null);
840
+ const localCollections = localRequestFilter ? {
841
+ ...state.synced.collections,
842
+ requests: Object.fromEntries(
843
+ Object.entries(state.synced.collections.requests).filter(
844
+ ([id]) => localRequestFilter.has(id)
845
+ )
846
+ )
847
+ } : state.synced.collections;
848
+ const workspaceSlots = (0, import_core2.collectAttachmentSlots)({ ...state.synced, collections: localCollections });
849
+ for (const slot of workspaceSlots) {
850
+ const requiredBy = collectRequiredBy(localCollections.requests, slot.slotId);
851
+ if (requiredBy.length === 0) continue;
852
+ addRequirement(seen, {
853
+ ...slot,
854
+ source: "workspace",
855
+ repoFullName: state.local.connectedRepo?.fullName ?? void 0,
856
+ branch: state.local.workingBranch?.name ?? void 0,
857
+ publicRepo: state.local.connectedRepo ? !state.local.connectedRepo.isPrivate : false,
858
+ requiredBy
859
+ });
860
+ }
861
+ for (const [linkedWorkspaceId, snapshot] of Object.entries(state.local.linkedCollections)) {
862
+ const link = state.synced.linkedWorkspaces[linkedWorkspaceId];
863
+ if (!link) continue;
864
+ const linkedRequestFilter = requestFilterForPlan(plan, linkedWorkspaceId);
865
+ if (plan && linkedRequestFilter && linkedRequestFilter.size === 0) continue;
866
+ const linkedCollections = linkedRequestFilter ? {
867
+ ...snapshot.collections,
868
+ requests: Object.fromEntries(
869
+ Object.entries(snapshot.collections.requests).filter(
870
+ ([id]) => linkedRequestFilter.has(id)
871
+ )
872
+ )
873
+ } : snapshot.collections;
874
+ const linkedSynced = {
875
+ ...state.synced,
876
+ collections: linkedCollections,
877
+ environments: snapshot.environments,
878
+ globalAssets: snapshot.globalAssets ?? state.synced.globalAssets
879
+ };
880
+ for (const slot of (0, import_core2.collectAttachmentSlots)(linkedSynced)) {
881
+ const requiredBy = collectRequiredBy(linkedCollections.requests, slot.slotId);
882
+ if (requiredBy.length === 0) continue;
883
+ addRequirement(seen, {
884
+ ...slot,
885
+ source: "linked-workspace",
886
+ linkedWorkspaceId,
887
+ repoFullName: link.source.repoFullName,
888
+ branch: link.source.branch,
889
+ publicRepo: link.kind === "public",
890
+ requiredBy
891
+ });
892
+ }
893
+ }
894
+ return [...seen.values()];
895
+ }
896
+ function requestFilterForPlan(plan, linkedWorkspaceId) {
897
+ if (!plan) return null;
898
+ const ids = /* @__PURE__ */ new Set();
899
+ for (const step of plan.steps) {
900
+ if (step.enabled === false) continue;
901
+ if ((step.linkedWorkspaceId ?? null) === linkedWorkspaceId) ids.add(step.requestId);
902
+ }
903
+ return ids;
904
+ }
905
+ function addRequirement(seen, requirement) {
906
+ const existing = seen.get(requirement.slotId);
907
+ if (!existing) {
908
+ seen.set(requirement.slotId, requirement);
909
+ return;
910
+ }
911
+ for (const usage of requirement.requiredBy) {
912
+ if (!existing.requiredBy.some((item) => item.requestId === usage.requestId)) {
913
+ existing.requiredBy.push(usage);
914
+ }
915
+ }
916
+ }
917
+ function collectRequiredBy(requests, slotId) {
918
+ const requiredBy = [];
919
+ for (const request of Object.values(requests)) {
920
+ if (bodyReferencesSlot(request.body, slotId)) {
921
+ requiredBy.push({ requestId: request.id, requestName: request.name });
922
+ }
923
+ }
924
+ return requiredBy;
925
+ }
926
+ function bodyReferencesSlot(body, slotId) {
927
+ if (body.type === "binary") return body.attachment?.slotId === slotId;
928
+ if (body.type !== "form-data") return false;
929
+ return (body.formRows ?? []).some((row) => row.kind === "file" && row.slotId === slotId);
930
+ }
931
+ async function hasExpectedFile(localPath, sha256) {
932
+ try {
933
+ const bytes = await import_node_fs6.promises.readFile(localPath);
934
+ if (!sha256) return true;
935
+ return sha256Hex(bytes) === sha256;
936
+ } catch {
937
+ return false;
938
+ }
939
+ }
940
+ async function fileSize(localPath) {
941
+ try {
942
+ return (await import_node_fs6.promises.stat(localPath)).size;
943
+ } catch {
944
+ return 0;
945
+ }
946
+ }
947
+ async function downloadAttachment(requirement) {
948
+ if (!requirement.repoFullName || !requirement.branch) return null;
949
+ const [owner, repo] = requirement.repoFullName.split("/", 2);
950
+ if (!owner || !repo) return null;
951
+ const token = resolveGitHubToken(requirement);
952
+ if (!token && !requirement.publicRepo) {
953
+ throw new Error(
954
+ "private linked attachments need a GitHub token (set APICIRCLE_GITHUB_TOKEN or GITHUB_TOKEN)"
955
+ );
956
+ }
957
+ const apiPath = [".apicircle", "attachments", requirement.slotId].map(encodeURIComponent).join("/");
958
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
959
+ repo
960
+ )}/contents/${apiPath}?ref=${encodeURIComponent(requirement.branch)}`;
961
+ const headers = {
962
+ Accept: "application/vnd.github+json",
963
+ "User-Agent": "apicircle-cli",
964
+ "X-GitHub-Api-Version": "2022-11-28"
965
+ };
966
+ if (token) headers.Authorization = `Bearer ${token}`;
967
+ const res = await fetch(url, { headers, cache: "no-store" });
968
+ if (res.status === 404) return null;
969
+ if (!res.ok) {
970
+ throw new Error(`GitHub returned ${res.status}: ${await res.text()}`);
971
+ }
972
+ const json = await res.json();
973
+ if (json.type !== "file" || typeof json.content !== "string") {
974
+ throw new Error("GitHub response was not a file");
975
+ }
976
+ if (json.encoding !== "base64") {
977
+ throw new Error(`GitHub response used unsupported encoding ${json.encoding ?? "(missing)"}`);
978
+ }
979
+ return new Uint8Array(Buffer.from(json.content.replace(/\n/g, ""), "base64"));
980
+ }
981
+ function resolveGitHubToken(requirement) {
982
+ if (requirement.source === "linked-workspace") {
983
+ return process.env.APICIRCLE_E2E_BOT_PAT_LINK_DEDICATED ?? process.env.APICIRCLE_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.APICIRCLE_E2E_GITHUB_PAT ?? process.env.APICIRCLE_E2E_BOT_PAT ?? "";
984
+ }
985
+ return process.env.APICIRCLE_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.APICIRCLE_E2E_GITHUB_PAT ?? process.env.APICIRCLE_E2E_BOT_PAT ?? "";
986
+ }
987
+ function sha256Hex(bytes) {
988
+ return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
989
+ }
990
+ function attachmentLabel(requirement) {
991
+ return `${requirement.filename ?? requirement.slotId} (${requirement.slotId})`;
992
+ }
993
+ function sourceLabel(requirement) {
994
+ const repo = requirement.repoFullName ?? "local workspace";
995
+ const branch = requirement.branch ? `@${requirement.branch}` : "";
996
+ return `${repo}${branch}`;
997
+ }
998
+ function requiredByLabel(requirement) {
999
+ return requirement.requiredBy.map((item) => item.requestName).join(", ") || "a request";
1000
+ }
1001
+ var import_node_crypto, import_node_fs6, path6, import_core2, ATTACHMENTS_DIR;
1002
+ var init_executionAttachments = __esm({
1003
+ "src/util/executionAttachments.ts"() {
1004
+ "use strict";
1005
+ import_node_crypto = require("crypto");
1006
+ import_node_fs6 = require("fs");
1007
+ path6 = __toESM(require("path"), 1);
1008
+ import_core2 = require("@apicircle/core");
1009
+ ATTACHMENTS_DIR = path6.join(".apicircle", "attachments");
1010
+ }
1011
+ });
1012
+
1013
+ // src/commands/run.ts
1014
+ function registerRunCommand(program) {
1015
+ program.command("run").description("Run a saved execution plan from a workspace and report the result").argument("<plan>", "Plan name or id to run").option(
1016
+ "--workspace-name <name-or-id>",
1017
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
1018
+ ).option(
1019
+ "--workspace-path <dir>",
1020
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
1021
+ ).option("--no-assertions", "Run requests without evaluating their assertions").option("-s, --secrets <file>", "JSON file mapping secretKeyId \u2192 plaintext value").option("--no-save", "Do not write the plan run to workspace history").option("--reporter <format>", "Report format: text | json | junit", "text").option("--bail", "Stop the run at the first failed step").option("-e, --env <name>", "Layer a local environment on top of the run").option("--as <actor>", "Override the recorded runner identity").action(async (planRef, opts) => {
1022
+ let dir;
1023
+ try {
1024
+ const resolved = await resolveWorkspace({
1025
+ name: opts.workspaceName,
1026
+ path: opts.workspacePath,
1027
+ expectExists: false
1028
+ });
1029
+ dir = resolved.dir;
1030
+ if (resolved.fromRegistry) {
1031
+ process.stderr.write(
1032
+ `${import_kleur4.default.dim("workspace")}: ${import_kleur4.default.cyan(resolved.name ?? resolved.id ?? "")} ${import_kleur4.default.dim(`(${dir})`)}
1033
+ `
1034
+ );
1035
+ }
1036
+ } catch (err) {
1037
+ if (err instanceof WorkspaceResolutionError) {
1038
+ fail(err.message);
1039
+ return;
1040
+ }
1041
+ throw err;
1042
+ }
1043
+ const reporter = opts.reporter ?? "text";
1044
+ if (!isReporter(reporter)) {
1045
+ fail(`unknown --reporter "${reporter}" (expected: ${REPORTERS.join(", ")})`);
1046
+ return;
1047
+ }
1048
+ const state = await (0, import_file_backed4.loadFromFile)(dir, { allowMissing: true });
1049
+ if (!state) {
1050
+ fail(`no workspace found at ${dir} (expected workspace.synced.json)`);
1051
+ return;
1052
+ }
1053
+ const ref = (0, import_core3.resolvePlanRef)(state.synced, planRef);
1054
+ if (!ref.ok) {
1055
+ fail(ref.error);
1056
+ if (ref.available.length > 0) {
1057
+ process.stderr.write(`Available plans: ${ref.available.join(", ")}
1058
+ `);
1059
+ }
1060
+ return;
1061
+ }
1062
+ if (opts.env && !state.synced.environments.items[opts.env]) {
1063
+ const names = Object.keys(state.synced.environments.items);
1064
+ fail(`no environment named "${opts.env}" in this workspace`);
1065
+ if (names.length > 0) {
1066
+ process.stderr.write(`Available environments: ${names.join(", ")}
1067
+ `);
1068
+ }
1069
+ return;
1070
+ }
1071
+ let secretsById;
1072
+ try {
1073
+ secretsById = (await buildSecretsFromCli({ secretsFile: opts.secrets })).byId;
1074
+ } catch (err) {
1075
+ fail(err instanceof Error ? err.message : String(err));
1076
+ return;
1077
+ }
1078
+ const actor = resolveActor(state.local, opts.as);
1079
+ const withAssertions = opts.assertions !== false;
1080
+ const text = reporter === "text";
1081
+ const controller = new AbortController();
1082
+ const onSigint = () => controller.abort(new Error("aborted by SIGINT"));
1083
+ process.on("SIGINT", onSigint);
1084
+ if (text) process.stdout.write(formatHeader(ref.plan, actor, withAssertions, opts));
1085
+ let prepared;
1086
+ try {
1087
+ prepared = await prepareExecutionAttachments(dir, state, ref.plan);
1088
+ } catch (err) {
1089
+ process.off("SIGINT", onSigint);
1090
+ fail(err instanceof Error ? err.message : String(err), 1, "attachment");
1091
+ return;
1092
+ }
1093
+ if (text && prepared.summary.total > 0) {
1094
+ process.stdout.write(formatAttachmentPreparation(prepared.summary));
1095
+ }
1096
+ let result;
1097
+ try {
1098
+ result = await (0, import_core3.runPlan)(prepared.state, ref.id, {
1099
+ withAssertions,
1100
+ bail: opts.bail === true,
1101
+ env: opts.env,
1102
+ secretsById,
1103
+ actor,
1104
+ signal: controller.signal,
1105
+ resolveAttachment: prepared.resolveAttachment,
1106
+ authorize: checkRunPermission,
1107
+ onStep: text ? (step) => process.stdout.write(formatStepLine(step)) : void 0
1108
+ });
1109
+ } catch (err) {
1110
+ process.off("SIGINT", onSigint);
1111
+ if (err instanceof import_core3.PlanRunDeniedError) {
1112
+ fail(err.message, 3, "denied");
1113
+ return;
1114
+ }
1115
+ throw err;
1116
+ }
1117
+ process.off("SIGINT", onSigint);
1118
+ const aborted = controller.signal.aborted;
1119
+ const saved = opts.save !== false;
1120
+ if (saved) await (0, import_file_backed4.saveToFile)(dir, result.nextState);
1121
+ if (reporter === "json") {
1122
+ process.stdout.write(
1123
+ JSON.stringify(
1124
+ buildJsonReport(dir, ref.id, ref.plan, actor, result, saved, aborted, prepared.summary),
1125
+ null,
1126
+ 2
1127
+ ) + "\n"
1128
+ );
1129
+ } else if (reporter === "junit") {
1130
+ process.stdout.write(buildJunitReport(ref.plan, result));
1131
+ } else {
1132
+ process.stdout.write(formatSummary(result, saved, aborted));
1133
+ }
1134
+ process.exitCode = result.passed && !aborted ? 0 : 1;
1135
+ });
1136
+ }
1137
+ function isReporter(value) {
1138
+ return REPORTERS.includes(value);
1139
+ }
1140
+ function resolveActor(local, override) {
1141
+ const explicit = override?.trim();
1142
+ if (explicit) return { kind: "unknown", name: explicit };
1143
+ const login = local.sessions.github.workspace?.accountLogin;
1144
+ if (login) return { kind: "github", name: login };
1145
+ try {
1146
+ const username = os2.userInfo().username;
1147
+ if (username) return { kind: "os", name: username };
1148
+ } catch {
1149
+ }
1150
+ return import_core3.ANONYMOUS_ACTOR;
1151
+ }
1152
+ function checkRunPermission(_ctx) {
1153
+ }
1154
+ function formatHeader(plan, actor, withAssertions, opts) {
1155
+ const enabled = plan.steps.filter((s) => s.enabled !== false).length;
1156
+ const flags = [
1157
+ withAssertions ? "assertions on" : "assertions off",
1158
+ opts.bail ? "bail" : null,
1159
+ opts.env ? `env=${opts.env}` : null
1160
+ ].filter((f) => f !== null);
1161
+ return `${import_kleur4.default.bold("Plan")} ${plan.name} ${import_kleur4.default.dim(
1162
+ `(${enabled}/${plan.steps.length} steps \xB7 ${flags.join(" \xB7 ")})`
1163
+ )}
1164
+ ${import_kleur4.default.dim("Run by")} ${actor.name} ${import_kleur4.default.dim(`(${actor.kind})`)}
1165
+
1166
+ `;
1167
+ }
1168
+ function formatAttachmentPreparation(summary) {
1169
+ const status = `${summary.downloaded} downloaded, ${summary.alreadyPresent} already local`;
1170
+ const lines = [
1171
+ `${import_kleur4.default.bold("Attachments")} ${summary.total} required ${import_kleur4.default.dim(
1172
+ `(${status} - ${summary.cacheDir})`
1173
+ )}`
1174
+ ];
1175
+ for (const entry3 of summary.entries) {
1176
+ const source = entry3.source === "linked-workspace" ? `linked:${entry3.linkedWorkspaceId ?? "unknown"}` : "workspace";
1177
+ const requiredBy = entry3.requiredBy.map((item) => item.requestName).join(", ");
1178
+ lines.push(
1179
+ ` ${import_kleur4.default.dim("file")} ${entry3.filename} ${import_kleur4.default.dim(
1180
+ `${source} - ${requiredBy} - ${entry3.localPath}`
1181
+ )}`
1182
+ );
1183
+ }
1184
+ return `${lines.join("\n")}
1185
+
1186
+ `;
1187
+ }
1188
+ function formatStepLine(step) {
1189
+ const n = `${step.stepIndex + 1}.`.padEnd(3);
1190
+ const method = (step.requestMethod || "\u2014").padEnd(7);
1191
+ if (step.skipped) {
1192
+ return ` ${import_kleur4.default.dim("\u2013")} ${import_kleur4.default.dim(n)} ${import_kleur4.default.dim(method)} ${import_kleur4.default.dim(
1193
+ `${step.requestName} skipped`
1194
+ )}
1195
+ `;
1196
+ }
1197
+ const mark = step.passed ? import_kleur4.default.green("\u2713") : import_kleur4.default.red("\u2717");
1198
+ const status = step.result?.status != null ? String(step.result.status) : "\u2014";
1199
+ const duration = step.result ? `${step.result.durationMs}ms` : "";
1200
+ const name = step.requestName.padEnd(28);
1201
+ let line = ` ${mark} ${n} ${method} ${name} ${status.padEnd(4)} ${import_kleur4.default.dim(duration)}`;
1202
+ if (step.assertionResults.length > 0) {
1203
+ const passed = step.assertionResults.filter((a) => a.passed).length;
1204
+ line += ` ${import_kleur4.default.dim(`${passed}/${step.assertionResults.length} assertions`)}`;
1205
+ }
1206
+ line += "\n";
1207
+ if (step.error) {
1208
+ line += ` ${import_kleur4.default.red(step.error)}
1209
+ `;
1210
+ }
1211
+ for (const a of step.assertionResults) {
1212
+ if (!a.passed) line += ` ${import_kleur4.default.red("\u2717")} ${a.detail ?? `${a.kind} ${a.op}`}
1213
+ `;
1214
+ }
1215
+ if (step.missingVariables.length > 0) {
1216
+ line += ` ${import_kleur4.default.yellow("\u26A0")} unresolved: ${step.missingVariables.map((v) => `{{${v}}}`).join(", ")}
1217
+ `;
1218
+ }
1219
+ return line;
1220
+ }
1221
+ function tally(result) {
1222
+ let passed = 0;
1223
+ let failed = 0;
1224
+ let skipped = 0;
1225
+ for (const s of result.steps) {
1226
+ if (s.skipped) skipped++;
1227
+ else if (s.passed) passed++;
1228
+ else failed++;
1229
+ }
1230
+ return { passed, failed, skipped };
1231
+ }
1232
+ function formatSummary(result, saved, aborted) {
1233
+ if (result.steps.length === 0) {
1234
+ return `
1235
+ ${import_kleur4.default.yellow("Plan has no steps.")}
1236
+ `;
1237
+ }
1238
+ const { passed, failed, skipped } = tally(result);
1239
+ const parts = [
1240
+ import_kleur4.default.green(`${passed} passed`),
1241
+ failed > 0 ? import_kleur4.default.red(`${failed} failed`) : import_kleur4.default.dim(`${failed} failed`),
1242
+ import_kleur4.default.dim(`${skipped} skipped`)
1243
+ ];
1244
+ const verdict = result.passed && !aborted ? import_kleur4.default.green("PASS") : import_kleur4.default.red("FAIL");
1245
+ let out = `
1246
+ ${verdict} ${parts.join(import_kleur4.default.dim(" \xB7 "))} ${import_kleur4.default.dim(
1247
+ `\xB7 ${result.planRun.durationMs}ms`
1248
+ )}
1249
+ `;
1250
+ if (aborted) out += `${import_kleur4.default.yellow("Run aborted before every step finished.")}
1251
+ `;
1252
+ out += saved ? import_kleur4.default.dim("Plan run saved to workspace history.\n") : import_kleur4.default.dim("Plan run not saved (--no-save).\n");
1253
+ return out;
1254
+ }
1255
+ function buildJsonReport(workspace, planId, plan, actor, result, saved, aborted, attachments) {
1256
+ return {
1257
+ workspace,
1258
+ plan: { id: planId, name: plan.name },
1259
+ actor,
1260
+ withAssertions: result.planRun.withAssertions,
1261
+ passed: result.passed && !aborted,
1262
+ aborted,
1263
+ durationMs: result.planRun.durationMs,
1264
+ saved,
1265
+ attachments,
1266
+ counts: tally(result),
1267
+ steps: result.steps.map((s) => ({
1268
+ step: s.stepIndex + 1,
1269
+ request: s.requestName,
1270
+ method: s.requestMethod,
1271
+ skipped: s.skipped,
1272
+ status: s.result?.status ?? null,
1273
+ ok: s.result?.ok ?? false,
1274
+ durationMs: s.result?.durationMs ?? 0,
1275
+ passed: s.passed,
1276
+ error: s.error ?? null,
1277
+ missingVariables: s.missingVariables,
1278
+ assertions: s.assertionResults.map((a) => ({
1279
+ kind: a.kind,
1280
+ op: a.op,
1281
+ target: a.target,
1282
+ expected: a.expected,
1283
+ passed: a.passed,
1284
+ detail: a.detail
1285
+ }))
1286
+ }))
1287
+ };
1288
+ }
1289
+ function xmlEscape(value) {
1290
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1291
+ }
1292
+ function buildJunitReport(plan, result) {
1293
+ const { failed, skipped } = tally(result);
1294
+ const total = result.steps.length;
1295
+ const suite = xmlEscape(plan.name);
1296
+ const suiteTime = (result.planRun.durationMs / 1e3).toFixed(3);
1297
+ const cases = result.steps.map((s) => {
1298
+ const name = xmlEscape(`${s.stepIndex + 1}. ${s.requestName}`);
1299
+ const time = ((s.result?.durationMs ?? 0) / 1e3).toFixed(3);
1300
+ const open = ` <testcase name="${name}" classname="${suite}" time="${time}"`;
1301
+ if (s.skipped) return `${open}>
1302
+ <skipped/>
1303
+ </testcase>`;
1304
+ if (s.passed) return `${open}/>`;
1305
+ const reasons = [];
1306
+ if (s.error) reasons.push(s.error);
1307
+ for (const a of s.assertionResults) {
1308
+ if (!a.passed) reasons.push(a.detail ?? `assertion ${a.kind} ${a.op} failed`);
1309
+ }
1310
+ if (s.result && !s.result.ok && s.result.status != null) {
1311
+ reasons.push(`HTTP ${s.result.status}`);
1312
+ }
1313
+ const detail = xmlEscape(reasons.join("\n") || "step failed");
1314
+ const summary = detail.split("\n")[0];
1315
+ return `${open}>
1316
+ <failure message="${summary}">${detail}</failure>
1317
+ </testcase>`;
1318
+ });
1319
+ return `<?xml version="1.0" encoding="UTF-8"?>
1320
+ <testsuites name="${suite}" tests="${total}" failures="${failed}" skipped="${skipped}" time="${suiteTime}">
1321
+ <testsuite name="${suite}" tests="${total}" failures="${failed}" skipped="${skipped}" time="${suiteTime}">
1322
+ ${cases.join("\n")}
1323
+ </testsuite>
1324
+ </testsuites>
1325
+ `;
1326
+ }
1327
+ function fail(message, code = 2, kind = "error") {
1328
+ process.stderr.write(`${import_kleur4.default.red(kind)}: ${message}
1329
+ `);
1330
+ process.exitCode = code;
1331
+ }
1332
+ var os2, import_kleur4, import_core3, import_file_backed4, REPORTERS;
1333
+ var init_run = __esm({
1334
+ "src/commands/run.ts"() {
1335
+ "use strict";
1336
+ os2 = __toESM(require("os"), 1);
1337
+ import_kleur4 = __toESM(require("kleur"), 1);
1338
+ import_core3 = require("@apicircle/core");
1339
+ import_file_backed4 = require("@apicircle/core/workspace/file-backed");
1340
+ init_secrets();
1341
+ init_resolveWorkspace();
1342
+ init_executionAttachments();
1343
+ REPORTERS = ["text", "json", "junit"];
1344
+ }
1345
+ });
1346
+
1347
+ // src/commands/workspaces.ts
1348
+ function registerWorkspacesCommand(program) {
1349
+ const ws = program.command("workspaces").description("List, create, or switch the active workspace");
1350
+ ws.command("list").description("List every workspace registered on this machine").option("--json", "Emit JSON instead of a formatted table").action(async (opts) => {
1351
+ const { registry, root } = await listWorkspacesOnDisk();
1352
+ if (opts.json) {
1353
+ process.stdout.write(JSON.stringify({ root, registry }, null, 2) + "\n");
1354
+ return;
1355
+ }
1356
+ if (registry.workspaces.length === 0) {
1357
+ process.stdout.write(
1358
+ `${import_kleur5.default.dim("No workspaces registered yet at")} ${root}
1359
+ ${import_kleur5.default.dim("Run")} ${import_kleur5.default.cyan("apicircle workspaces create <name>")} ${import_kleur5.default.dim(
1360
+ "or open the desktop app to seed one."
1361
+ )}
1362
+ `
1363
+ );
1364
+ return;
1365
+ }
1366
+ process.stdout.write(`${import_kleur5.default.dim("registry")}: ${root}
1367
+
1368
+ `);
1369
+ const rows = [...registry.workspaces].sort(
1370
+ (a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt)
1371
+ );
1372
+ const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
1373
+ const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
1374
+ process.stdout.write(
1375
+ import_kleur5.default.bold(
1376
+ ` ${"".padEnd(1)} ${"NAME".padEnd(nameWidth)} ${"ID".padEnd(idWidth)} LAST OPENED
1377
+ `
1378
+ )
1379
+ );
1380
+ for (const w of rows) {
1381
+ const mark = w.id === registry.activeWorkspaceId ? import_kleur5.default.green("\u25CF") : " ";
1382
+ process.stdout.write(
1383
+ ` ${mark} ${w.name.padEnd(nameWidth)} ${import_kleur5.default.dim(
1384
+ w.id.padEnd(idWidth)
1385
+ )} ${import_kleur5.default.dim(w.lastOpenedAt)}
1386
+ `
1387
+ );
1388
+ }
1389
+ process.stdout.write(`
1390
+ ${import_kleur5.default.dim("\u25CF = active")}
1391
+ `);
1392
+ });
1393
+ ws.command("create").description("Create a new workspace and add it to the registry").argument("<name>", "Human-readable label for the workspace").option("--sample", "Seed the workspace with one sample request", false).action(async (name, opts) => {
1394
+ try {
1395
+ const { entry: entry3, dir, registry } = await createWorkspaceOnDisk({
1396
+ name,
1397
+ sampleRequest: opts.sample ?? false
1398
+ });
1399
+ process.stdout.write(
1400
+ `${import_kleur5.default.green("created")} workspace ${import_kleur5.default.cyan(entry3.name)} ${import_kleur5.default.dim(`(${entry3.id})`)}
1401
+ at ${dir}
1402
+ `
1403
+ );
1404
+ if (registry.activeWorkspaceId === entry3.id) {
1405
+ process.stdout.write(`${import_kleur5.default.dim("marked as active")}
1406
+ `);
1407
+ }
1408
+ } catch (err) {
1409
+ process.stderr.write(
1410
+ `${import_kleur5.default.red("error")}: ${err instanceof Error ? err.message : String(err)}
1411
+ `
1412
+ );
1413
+ process.exit(2);
1414
+ }
1415
+ });
1416
+ ws.command("use").description("Set the active workspace by id or name").argument("<selector>", "Workspace id or name").action(async (selector) => {
1417
+ const { registry, root } = await listWorkspacesOnDisk();
1418
+ const entry3 = (0, import_registry2.findWorkspaceEntry)(registry, selector);
1419
+ if (!entry3) {
1420
+ process.stderr.write(
1421
+ `${import_kleur5.default.red("error")}: no workspace named "${selector}" in the registry at ${root}.
1422
+ ${import_kleur5.default.dim("Run")} ${import_kleur5.default.cyan("apicircle workspaces list")} ${import_kleur5.default.dim("to see what is available.")}
1423
+ `
1424
+ );
1425
+ process.exit(2);
1426
+ return;
1427
+ }
1428
+ const next = await (0, import_registry2.setActiveWorkspace)(root, entry3.id);
1429
+ void next;
1430
+ process.stdout.write(
1431
+ `${import_kleur5.default.green("active")} workspace is now ${import_kleur5.default.cyan(entry3.name)} ${import_kleur5.default.dim(`(${entry3.id})`)}
1432
+ `
1433
+ );
1434
+ });
1435
+ ws.command("path").description("Print the on-disk path for a workspace (or the workspaces root)").argument("[selector]", "Optional workspace id or name; prints the root when omitted").action(async (selector) => {
1436
+ if (!selector) {
1437
+ process.stdout.write(defaultWorkspacesRoot() + "\n");
1438
+ return;
1439
+ }
1440
+ const { registry, root } = await listWorkspacesOnDisk();
1441
+ const entry3 = (0, import_registry2.findWorkspaceEntry)(registry, selector);
1442
+ if (!entry3) {
1443
+ process.stderr.write(
1444
+ `${import_kleur5.default.red("error")}: no workspace named "${selector}" in the registry at ${root}.
1445
+ `
1446
+ );
1447
+ process.exit(2);
1448
+ return;
1449
+ }
1450
+ void saveRegistryToDisk;
1451
+ const { workspaceDirFor: workspaceDirFor2 } = await import("@apicircle/core/workspace/registry");
1452
+ process.stdout.write(workspaceDirFor2(root, entry3.id) + "\n");
1453
+ });
1454
+ }
1455
+ var import_kleur5, import_registry2;
1456
+ var init_workspaces = __esm({
1457
+ "src/commands/workspaces.ts"() {
1458
+ "use strict";
1459
+ import_kleur5 = __toESM(require("kleur"), 1);
1460
+ init_resolveWorkspace();
1461
+ import_registry2 = require("@apicircle/core/workspace/registry");
1462
+ }
1463
+ });
1464
+
1465
+ // src/index.ts
1466
+ var src_exports = {};
1467
+ __export(src_exports, {
1468
+ buildProgram: () => buildProgram,
1469
+ runCli: () => runCli
1470
+ });
1471
+ function buildProgram() {
1472
+ const program = new import_commander.Command();
1473
+ program.name("apicircle").description("Command-line companion to API Circle Studio.").version(CLI_PACKAGE_VERSION);
1474
+ registerMockCommand(program);
1475
+ registerMcpCommand(program);
1476
+ registerImportCommand(program);
1477
+ registerRunCommand(program);
1478
+ registerWorkspacesCommand(program);
1479
+ return program;
1480
+ }
1481
+ async function runCli(argv = process.argv) {
1482
+ await buildProgram().parseAsync(argv);
1483
+ }
1484
+ var import_commander, entry;
1485
+ var init_src = __esm({
1486
+ "src/index.ts"() {
1487
+ "use strict";
1488
+ import_commander = require("commander");
1489
+ init_mock();
1490
+ init_mcp();
1491
+ init_import();
1492
+ init_run();
1493
+ init_workspaces();
1494
+ init_packageVersion();
1495
+ entry = process.argv[1] ?? "";
1496
+ if (entry.endsWith("apicircle") || entry.endsWith("index.cjs") || entry.endsWith("index.ts")) {
1497
+ void runCli();
1498
+ }
1499
+ }
1500
+ });
1501
+
1502
+ // src/bin/cli.ts
1503
+ var cli_exports = {};
1504
+ __export(cli_exports, {
1505
+ runBin: () => runBin
1506
+ });
1507
+ module.exports = __toCommonJS(cli_exports);
1508
+ init_packageVersion();
1509
+
1510
+ // src/bin/args.ts
1511
+ function hasRootVersionFlag(args) {
1512
+ return args.length === 1 && ["--version", "-v", "-V"].includes(args[0] ?? "");
1513
+ }
1514
+ function hasRootHelpFlag(args) {
1515
+ return args.length === 1 && ["--help", "-h", "help"].includes(args[0] ?? "");
1516
+ }
1517
+ function formatRootHelp() {
1518
+ return `Usage: apicircle [options] [command]
1519
+
1520
+ Command-line companion to API Circle Studio.
1521
+
1522
+ Options:
1523
+ -v, -V, --version Print the version number.
1524
+ -h, --help Show help.
1525
+
1526
+ Commands:
1527
+ mock [options] <workspace> Start a local mock server.
1528
+ mcp [options] [workspace] Start the MCP stdio server.
1529
+ import <source> <workspace> Import OpenAPI, Postman, Insomnia, or curl.
1530
+ run [options] <plan-id> Run an execution plan.
1531
+ workspaces Manage local workspace registries.
1532
+
1533
+ Run "apicircle <command> --help" for command-specific help.
1534
+ `;
1535
+ }
1536
+
1537
+ // src/bin/cli.ts
1538
+ async function runBin(argv = process.argv) {
1539
+ const args = argv.slice(2);
1540
+ if (hasRootVersionFlag(args)) {
1541
+ process.stdout.write(`${CLI_PACKAGE_VERSION}
1542
+ `);
1543
+ return;
1544
+ }
1545
+ if (hasRootHelpFlag(args)) {
1546
+ process.stdout.write(formatRootHelp());
1547
+ return;
1548
+ }
1549
+ const { runCli: runCli2 } = await Promise.resolve().then(() => (init_src(), src_exports));
1550
+ await runCli2(argv);
1551
+ }
1552
+ var entry2 = process.argv[1] ?? "";
1553
+ if (entry2.endsWith("apicircle") || entry2.endsWith("cli.cjs") || entry2.endsWith("cli.ts")) {
1554
+ void runBin();
1555
+ }
1556
+ // Annotate the CommonJS export names for ESM import in node:
1557
+ 0 && (module.exports = {
1558
+ runBin
1559
+ });
1560
+ //# sourceMappingURL=cli.cjs.map