@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.
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CLI_PACKAGE_VERSION
4
+ } from "./chunk-B3FBQLFQ.js";
2
5
 
3
6
  // src/index.ts
4
7
  import { Command } from "commander";
@@ -114,7 +117,7 @@ async function ensureWorkspace(dir) {
114
117
  linkedWorkspaces: {},
115
118
  linkedOverrides: { requests: {}, environmentVars: {} },
116
119
  releases: { self: null, perLink: {} },
117
- globalAssets: { schemas: {}, graphql: {} },
120
+ globalAssets: { schemas: {}, graphql: {}, files: {} },
118
121
  mockServers: {},
119
122
  meta: { createdAt: now, updatedAt: now, appVersion: "1.0.0" }
120
123
  },
@@ -131,13 +134,14 @@ async function ensureWorkspace(dir) {
131
134
  retiredBranch: null,
132
135
  sync: { lastPulledSnapshot: null, lastPulledSha: null, lastPulledAt: null, dirtyKeys: [] },
133
136
  linkedCollections: {},
137
+ attachmentCache: {},
134
138
  globalContext: {},
135
139
  mockRuntime: { active: {} },
136
140
  ui: {
137
141
  activeRequestId: null,
138
142
  sidebarExpandedSections: [],
139
- themeId: "studio-dark",
140
- fontId: "system-mono",
143
+ themeId: "command-center",
144
+ fontId: "cascadia-code",
141
145
  fontSizePercent: FONT_SIZE_PERCENT_DEFAULT
142
146
  },
143
147
  settings: { validateOnSend: true, monacoConsumesWheel: false },
@@ -341,7 +345,7 @@ function buildEmptyState(workspaceId, now, withSample) {
341
345
  linkedWorkspaces: {},
342
346
  linkedOverrides: { requests: {}, environmentVars: {} },
343
347
  releases: { self: null, perLink: {} },
344
- globalAssets: { schemas: {}, graphql: {} },
348
+ globalAssets: { schemas: {}, graphql: {}, files: {} },
345
349
  mockServers: {},
346
350
  meta: { createdAt: now, updatedAt: now, appVersion: "1.0.0" }
347
351
  },
@@ -358,13 +362,14 @@ function buildEmptyState(workspaceId, now, withSample) {
358
362
  retiredBranch: null,
359
363
  sync: { lastPulledSnapshot: null, lastPulledSha: null, lastPulledAt: null, dirtyKeys: [] },
360
364
  linkedCollections: {},
365
+ attachmentCache: {},
361
366
  globalContext: {},
362
367
  mockRuntime: { active: {} },
363
368
  ui: {
364
369
  activeRequestId: sample?.id ?? null,
365
370
  sidebarExpandedSections: [],
366
- themeId: "studio-dark",
367
- fontId: "system-mono",
371
+ themeId: "command-center",
372
+ fontId: "cascadia-code",
368
373
  fontSizePercent: 100
369
374
  },
370
375
  settings: { validateOnSend: true, monacoConsumesWheel: false },
@@ -535,13 +540,13 @@ function registerImportCommand(program) {
535
540
  }
536
541
  async function readInput(p) {
537
542
  if (p === "-") {
538
- return new Promise((resolve6, reject) => {
543
+ return new Promise((resolve7, reject) => {
539
544
  let data = "";
540
545
  process.stdin.setEncoding("utf-8");
541
546
  process.stdin.on("data", (chunk) => {
542
547
  data += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
543
548
  });
544
- process.stdin.on("end", () => resolve6(data));
549
+ process.stdin.on("end", () => resolve7(data));
545
550
  process.stdin.on("error", reject);
546
551
  });
547
552
  }
@@ -608,6 +613,274 @@ async function buildSecretsFromCli(options = {}) {
608
613
  return { byId };
609
614
  }
610
615
 
616
+ // src/util/executionAttachments.ts
617
+ import { createHash } from "crypto";
618
+ import { promises as fs6 } from "fs";
619
+ import * as path6 from "path";
620
+ import {
621
+ collectAttachmentSlots
622
+ } from "@apicircle/core";
623
+ var ATTACHMENTS_DIR = path6.join(".apicircle", "attachments");
624
+ async function prepareExecutionAttachments(workspaceDir, state, plan) {
625
+ const cacheDir = path6.resolve(workspaceDir, ATTACHMENTS_DIR);
626
+ const requirements = collectExecutionAttachmentRequirements(state, plan);
627
+ await fs6.mkdir(cacheDir, { recursive: true });
628
+ let downloaded = 0;
629
+ let alreadyPresent = 0;
630
+ let failed = 0;
631
+ const cache = {
632
+ ...state.local.attachmentCache ?? {}
633
+ };
634
+ const entries = [];
635
+ for (const requirement of requirements) {
636
+ const localPath = path6.join(cacheDir, encodeURIComponent(requirement.slotId));
637
+ const present = await hasExpectedFile(localPath, requirement.sha256);
638
+ if (present) {
639
+ alreadyPresent++;
640
+ } else {
641
+ try {
642
+ const bytes = await downloadAttachment(requirement);
643
+ if (!bytes) {
644
+ throw new Error(
645
+ `Attachment ${attachmentLabel(requirement)} was not found in ${sourceLabel(requirement)}.`
646
+ );
647
+ }
648
+ if (requirement.sha256 && sha256Hex(bytes) !== requirement.sha256) {
649
+ throw new Error(
650
+ `Attachment ${attachmentLabel(requirement)} failed checksum verification.`
651
+ );
652
+ }
653
+ await fs6.writeFile(localPath, bytes, { mode: 384 });
654
+ downloaded++;
655
+ } catch (err) {
656
+ failed++;
657
+ throw new Error(
658
+ `Attachment ${attachmentLabel(requirement)} is required by ${requiredByLabel(
659
+ requirement
660
+ )} but could not be downloaded from ${sourceLabel(requirement)}: ${err instanceof Error ? err.message : String(err)}`
661
+ );
662
+ }
663
+ }
664
+ cache[requirement.slotId] = {
665
+ slotId: requirement.slotId,
666
+ filename: requirement.filename ?? requirement.slotId,
667
+ mimeType: requirement.mimeType ?? "application/octet-stream",
668
+ size: requirement.size ?? await fileSize(localPath),
669
+ sha256: requirement.sha256,
670
+ localPath,
671
+ storage: "filesystem",
672
+ source: requirement.source,
673
+ ...requirement.linkedWorkspaceId ? { linkedWorkspaceId: requirement.linkedWorkspaceId } : {},
674
+ requiredBy: requirement.requiredBy,
675
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString()
676
+ };
677
+ entries.push({
678
+ slotId: requirement.slotId,
679
+ filename: requirement.filename ?? requirement.slotId,
680
+ localPath,
681
+ source: requirement.source,
682
+ ...requirement.linkedWorkspaceId ? { linkedWorkspaceId: requirement.linkedWorkspaceId } : {},
683
+ requiredBy: requirement.requiredBy
684
+ });
685
+ }
686
+ const nextState = {
687
+ ...state,
688
+ local: {
689
+ ...state.local,
690
+ attachmentCache: cache
691
+ }
692
+ };
693
+ return {
694
+ state: nextState,
695
+ resolveAttachment: createFileAttachmentResolver(nextState),
696
+ summary: {
697
+ total: requirements.length,
698
+ downloaded,
699
+ alreadyPresent,
700
+ failed,
701
+ cacheDir,
702
+ entries
703
+ }
704
+ };
705
+ }
706
+ function createFileAttachmentResolver(state) {
707
+ return async (slotId) => {
708
+ const meta = state.local.attachmentCache?.[slotId];
709
+ if (!meta) return null;
710
+ const bytes = await fs6.readFile(meta.localPath);
711
+ const view = new Uint8Array(bytes);
712
+ const body = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
713
+ return {
714
+ blob: new Blob([body], { type: meta.mimeType }),
715
+ filename: meta.filename
716
+ };
717
+ };
718
+ }
719
+ function collectExecutionAttachmentRequirements(state, plan) {
720
+ const seen = /* @__PURE__ */ new Map();
721
+ const localRequestFilter = requestFilterForPlan(plan, null);
722
+ const localCollections = localRequestFilter ? {
723
+ ...state.synced.collections,
724
+ requests: Object.fromEntries(
725
+ Object.entries(state.synced.collections.requests).filter(
726
+ ([id]) => localRequestFilter.has(id)
727
+ )
728
+ )
729
+ } : state.synced.collections;
730
+ const workspaceSlots = collectAttachmentSlots({ ...state.synced, collections: localCollections });
731
+ for (const slot of workspaceSlots) {
732
+ const requiredBy = collectRequiredBy(localCollections.requests, slot.slotId);
733
+ if (requiredBy.length === 0) continue;
734
+ addRequirement(seen, {
735
+ ...slot,
736
+ source: "workspace",
737
+ repoFullName: state.local.connectedRepo?.fullName ?? void 0,
738
+ branch: state.local.workingBranch?.name ?? void 0,
739
+ publicRepo: state.local.connectedRepo ? !state.local.connectedRepo.isPrivate : false,
740
+ requiredBy
741
+ });
742
+ }
743
+ for (const [linkedWorkspaceId, snapshot] of Object.entries(state.local.linkedCollections)) {
744
+ const link = state.synced.linkedWorkspaces[linkedWorkspaceId];
745
+ if (!link) continue;
746
+ const linkedRequestFilter = requestFilterForPlan(plan, linkedWorkspaceId);
747
+ if (plan && linkedRequestFilter && linkedRequestFilter.size === 0) continue;
748
+ const linkedCollections = linkedRequestFilter ? {
749
+ ...snapshot.collections,
750
+ requests: Object.fromEntries(
751
+ Object.entries(snapshot.collections.requests).filter(
752
+ ([id]) => linkedRequestFilter.has(id)
753
+ )
754
+ )
755
+ } : snapshot.collections;
756
+ const linkedSynced = {
757
+ ...state.synced,
758
+ collections: linkedCollections,
759
+ environments: snapshot.environments,
760
+ globalAssets: snapshot.globalAssets ?? state.synced.globalAssets
761
+ };
762
+ for (const slot of collectAttachmentSlots(linkedSynced)) {
763
+ const requiredBy = collectRequiredBy(linkedCollections.requests, slot.slotId);
764
+ if (requiredBy.length === 0) continue;
765
+ addRequirement(seen, {
766
+ ...slot,
767
+ source: "linked-workspace",
768
+ linkedWorkspaceId,
769
+ repoFullName: link.source.repoFullName,
770
+ branch: link.source.branch,
771
+ publicRepo: link.kind === "public",
772
+ requiredBy
773
+ });
774
+ }
775
+ }
776
+ return [...seen.values()];
777
+ }
778
+ function requestFilterForPlan(plan, linkedWorkspaceId) {
779
+ if (!plan) return null;
780
+ const ids = /* @__PURE__ */ new Set();
781
+ for (const step of plan.steps) {
782
+ if (step.enabled === false) continue;
783
+ if ((step.linkedWorkspaceId ?? null) === linkedWorkspaceId) ids.add(step.requestId);
784
+ }
785
+ return ids;
786
+ }
787
+ function addRequirement(seen, requirement) {
788
+ const existing = seen.get(requirement.slotId);
789
+ if (!existing) {
790
+ seen.set(requirement.slotId, requirement);
791
+ return;
792
+ }
793
+ for (const usage of requirement.requiredBy) {
794
+ if (!existing.requiredBy.some((item) => item.requestId === usage.requestId)) {
795
+ existing.requiredBy.push(usage);
796
+ }
797
+ }
798
+ }
799
+ function collectRequiredBy(requests, slotId) {
800
+ const requiredBy = [];
801
+ for (const request of Object.values(requests)) {
802
+ if (bodyReferencesSlot(request.body, slotId)) {
803
+ requiredBy.push({ requestId: request.id, requestName: request.name });
804
+ }
805
+ }
806
+ return requiredBy;
807
+ }
808
+ function bodyReferencesSlot(body, slotId) {
809
+ if (body.type === "binary") return body.attachment?.slotId === slotId;
810
+ if (body.type !== "form-data") return false;
811
+ return (body.formRows ?? []).some((row) => row.kind === "file" && row.slotId === slotId);
812
+ }
813
+ async function hasExpectedFile(localPath, sha256) {
814
+ try {
815
+ const bytes = await fs6.readFile(localPath);
816
+ if (!sha256) return true;
817
+ return sha256Hex(bytes) === sha256;
818
+ } catch {
819
+ return false;
820
+ }
821
+ }
822
+ async function fileSize(localPath) {
823
+ try {
824
+ return (await fs6.stat(localPath)).size;
825
+ } catch {
826
+ return 0;
827
+ }
828
+ }
829
+ async function downloadAttachment(requirement) {
830
+ if (!requirement.repoFullName || !requirement.branch) return null;
831
+ const [owner, repo] = requirement.repoFullName.split("/", 2);
832
+ if (!owner || !repo) return null;
833
+ const token = resolveGitHubToken(requirement);
834
+ if (!token && !requirement.publicRepo) {
835
+ throw new Error(
836
+ "private linked attachments need a GitHub token (set APICIRCLE_GITHUB_TOKEN or GITHUB_TOKEN)"
837
+ );
838
+ }
839
+ const apiPath = [".apicircle", "attachments", requirement.slotId].map(encodeURIComponent).join("/");
840
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
841
+ repo
842
+ )}/contents/${apiPath}?ref=${encodeURIComponent(requirement.branch)}`;
843
+ const headers = {
844
+ Accept: "application/vnd.github+json",
845
+ "User-Agent": "apicircle-cli",
846
+ "X-GitHub-Api-Version": "2022-11-28"
847
+ };
848
+ if (token) headers.Authorization = `Bearer ${token}`;
849
+ const res = await fetch(url, { headers, cache: "no-store" });
850
+ if (res.status === 404) return null;
851
+ if (!res.ok) {
852
+ throw new Error(`GitHub returned ${res.status}: ${await res.text()}`);
853
+ }
854
+ const json = await res.json();
855
+ if (json.type !== "file" || typeof json.content !== "string") {
856
+ throw new Error("GitHub response was not a file");
857
+ }
858
+ if (json.encoding !== "base64") {
859
+ throw new Error(`GitHub response used unsupported encoding ${json.encoding ?? "(missing)"}`);
860
+ }
861
+ return new Uint8Array(Buffer.from(json.content.replace(/\n/g, ""), "base64"));
862
+ }
863
+ function resolveGitHubToken(requirement) {
864
+ if (requirement.source === "linked-workspace") {
865
+ 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 ?? "";
866
+ }
867
+ return process.env.APICIRCLE_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.APICIRCLE_E2E_GITHUB_PAT ?? process.env.APICIRCLE_E2E_BOT_PAT ?? "";
868
+ }
869
+ function sha256Hex(bytes) {
870
+ return createHash("sha256").update(bytes).digest("hex");
871
+ }
872
+ function attachmentLabel(requirement) {
873
+ return `${requirement.filename ?? requirement.slotId} (${requirement.slotId})`;
874
+ }
875
+ function sourceLabel(requirement) {
876
+ const repo = requirement.repoFullName ?? "local workspace";
877
+ const branch = requirement.branch ? `@${requirement.branch}` : "";
878
+ return `${repo}${branch}`;
879
+ }
880
+ function requiredByLabel(requirement) {
881
+ return requirement.requiredBy.map((item) => item.requestName).join(", ") || "a request";
882
+ }
883
+
611
884
  // src/commands/run.ts
612
885
  var REPORTERS = ["text", "json", "junit"];
613
886
  function registerRunCommand(program) {
@@ -681,15 +954,27 @@ function registerRunCommand(program) {
681
954
  const onSigint = () => controller.abort(new Error("aborted by SIGINT"));
682
955
  process.on("SIGINT", onSigint);
683
956
  if (text) process.stdout.write(formatHeader(ref.plan, actor, withAssertions, opts));
957
+ let prepared;
958
+ try {
959
+ prepared = await prepareExecutionAttachments(dir, state, ref.plan);
960
+ } catch (err) {
961
+ process.off("SIGINT", onSigint);
962
+ fail(err instanceof Error ? err.message : String(err), 1, "attachment");
963
+ return;
964
+ }
965
+ if (text && prepared.summary.total > 0) {
966
+ process.stdout.write(formatAttachmentPreparation(prepared.summary));
967
+ }
684
968
  let result;
685
969
  try {
686
- result = await runPlan(state, ref.id, {
970
+ result = await runPlan(prepared.state, ref.id, {
687
971
  withAssertions,
688
972
  bail: opts.bail === true,
689
973
  env: opts.env,
690
974
  secretsById,
691
975
  actor,
692
976
  signal: controller.signal,
977
+ resolveAttachment: prepared.resolveAttachment,
693
978
  authorize: checkRunPermission,
694
979
  onStep: text ? (step) => process.stdout.write(formatStepLine(step)) : void 0
695
980
  });
@@ -708,7 +993,7 @@ function registerRunCommand(program) {
708
993
  if (reporter === "json") {
709
994
  process.stdout.write(
710
995
  JSON.stringify(
711
- buildJsonReport(dir, ref.id, ref.plan, actor, result, saved, aborted),
996
+ buildJsonReport(dir, ref.id, ref.plan, actor, result, saved, aborted, prepared.summary),
712
997
  null,
713
998
  2
714
999
  ) + "\n"
@@ -750,6 +1035,26 @@ function formatHeader(plan, actor, withAssertions, opts) {
750
1035
  )}
751
1036
  ${kleur4.dim("Run by")} ${actor.name} ${kleur4.dim(`(${actor.kind})`)}
752
1037
 
1038
+ `;
1039
+ }
1040
+ function formatAttachmentPreparation(summary) {
1041
+ const status = `${summary.downloaded} downloaded, ${summary.alreadyPresent} already local`;
1042
+ const lines = [
1043
+ `${kleur4.bold("Attachments")} ${summary.total} required ${kleur4.dim(
1044
+ `(${status} - ${summary.cacheDir})`
1045
+ )}`
1046
+ ];
1047
+ for (const entry2 of summary.entries) {
1048
+ const source = entry2.source === "linked-workspace" ? `linked:${entry2.linkedWorkspaceId ?? "unknown"}` : "workspace";
1049
+ const requiredBy = entry2.requiredBy.map((item) => item.requestName).join(", ");
1050
+ lines.push(
1051
+ ` ${kleur4.dim("file")} ${entry2.filename} ${kleur4.dim(
1052
+ `${source} - ${requiredBy} - ${entry2.localPath}`
1053
+ )}`
1054
+ );
1055
+ }
1056
+ return `${lines.join("\n")}
1057
+
753
1058
  `;
754
1059
  }
755
1060
  function formatStepLine(step) {
@@ -819,7 +1124,7 @@ ${verdict} ${parts.join(kleur4.dim(" \xB7 "))} ${kleur4.dim(
819
1124
  out += saved ? kleur4.dim("Plan run saved to workspace history.\n") : kleur4.dim("Plan run not saved (--no-save).\n");
820
1125
  return out;
821
1126
  }
822
- function buildJsonReport(workspace, planId, plan, actor, result, saved, aborted) {
1127
+ function buildJsonReport(workspace, planId, plan, actor, result, saved, aborted, attachments) {
823
1128
  return {
824
1129
  workspace,
825
1130
  plan: { id: planId, name: plan.name },
@@ -829,6 +1134,7 @@ function buildJsonReport(workspace, planId, plan, actor, result, saved, aborted)
829
1134
  aborted,
830
1135
  durationMs: result.planRun.durationMs,
831
1136
  saved,
1137
+ attachments,
832
1138
  counts: tally(result),
833
1139
  steps: result.steps.map((s) => ({
834
1140
  step: s.stepIndex + 1,
@@ -1010,7 +1316,7 @@ ${kleur5.dim("Run")} ${kleur5.cyan("apicircle workspaces list")} ${kleur5.dim("t
1010
1316
  // src/index.ts
1011
1317
  function buildProgram() {
1012
1318
  const program = new Command();
1013
- program.name("apicircle").description("Command-line companion to API Circle Studio.").version("1.0.0");
1319
+ program.name("apicircle").description("Command-line companion to API Circle Studio.").version(CLI_PACKAGE_VERSION);
1014
1320
  registerMockCommand(program);
1015
1321
  registerMcpCommand(program);
1016
1322
  registerImportCommand(program);