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