@apicircle/cli 1.0.6 → 1.0.8
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/bin/cli.cjs +237 -72
- package/dist/bin/cli.cjs.map +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/{chunk-HNK3FW57.js → chunk-M5PWIZZF.js} +22 -2
- package/dist/chunk-M5PWIZZF.js.map +1 -0
- package/dist/index.cjs +224 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +202 -61
- package/dist/index.js.map +1 -1
- package/package.json +25 -5
- package/dist/chunk-HNK3FW57.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CLI_PACKAGE_VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-M5PWIZZF.js";
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -140,8 +140,8 @@ async function ensureWorkspace(dir) {
|
|
|
140
140
|
ui: {
|
|
141
141
|
activeRequestId: null,
|
|
142
142
|
sidebarExpandedSections: [],
|
|
143
|
-
themeId: "
|
|
144
|
-
fontId: "
|
|
143
|
+
themeId: "one-dark-pro",
|
|
144
|
+
fontId: "system-sans",
|
|
145
145
|
fontSizePercent: FONT_SIZE_PERCENT_DEFAULT
|
|
146
146
|
},
|
|
147
147
|
settings: { validateOnSend: true, monacoConsumesWheel: false },
|
|
@@ -368,8 +368,8 @@ function buildEmptyState(workspaceId, now, withSample) {
|
|
|
368
368
|
ui: {
|
|
369
369
|
activeRequestId: sample?.id ?? null,
|
|
370
370
|
sidebarExpandedSections: [],
|
|
371
|
-
themeId: "
|
|
372
|
-
fontId: "
|
|
371
|
+
themeId: "one-dark-pro",
|
|
372
|
+
fontId: "system-sans",
|
|
373
373
|
fontSizePercent: 100
|
|
374
374
|
},
|
|
375
375
|
settings: { validateOnSend: true, monacoConsumesWheel: false },
|
|
@@ -427,7 +427,7 @@ function registerMcpCommand(program) {
|
|
|
427
427
|
import { promises as fs4 } from "fs";
|
|
428
428
|
import * as path4 from "path";
|
|
429
429
|
import kleur3 from "kleur";
|
|
430
|
-
import { applyMutation } from "@apicircle/core";
|
|
430
|
+
import { applyMutation, parseApicircleFolderExport } from "@apicircle/core";
|
|
431
431
|
import { saveToFile as saveToFile3 } from "@apicircle/core/workspace/file-backed";
|
|
432
432
|
import {
|
|
433
433
|
parseInsomniaToEndpoints,
|
|
@@ -436,7 +436,10 @@ import {
|
|
|
436
436
|
} from "@apicircle/mock-server-core";
|
|
437
437
|
import { generateId as generateId4 } from "@apicircle/shared";
|
|
438
438
|
function registerImportCommand(program) {
|
|
439
|
-
program.command("import").description("Import a spec into a workspace folder").argument(
|
|
439
|
+
program.command("import").description("Import a spec into a workspace folder").argument(
|
|
440
|
+
"<type>",
|
|
441
|
+
"Source type: openapi | postman | insomnia | curl | apicircle (the apicircle.folder/v1 envelope produced by `apicircle export folder`)"
|
|
442
|
+
).argument("<input>", "Path to a spec file, or `-` to read from stdin").option(
|
|
440
443
|
"--workspace-name <name-or-id>",
|
|
441
444
|
"Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
|
|
442
445
|
).option(
|
|
@@ -526,6 +529,40 @@ function registerImportCommand(program) {
|
|
|
526
529
|
})
|
|
527
530
|
);
|
|
528
531
|
}
|
|
532
|
+
} else if (type === "apicircle") {
|
|
533
|
+
let parsedEnvelope;
|
|
534
|
+
try {
|
|
535
|
+
parsedEnvelope = parseApicircleFolderExport(raw);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
process.stderr.write(
|
|
538
|
+
`${kleur3.red("error")}: ${err instanceof Error ? err.message : String(err)}
|
|
539
|
+
`
|
|
540
|
+
);
|
|
541
|
+
process.exit(2);
|
|
542
|
+
}
|
|
543
|
+
const out = applyMutation(
|
|
544
|
+
{ synced: nextSynced, local: nextLocal },
|
|
545
|
+
{ kind: "folder.import_apicircle", parsed: parsedEnvelope, parentFolderId: null }
|
|
546
|
+
);
|
|
547
|
+
nextSynced = out.next.synced;
|
|
548
|
+
nextLocal = out.next.local;
|
|
549
|
+
for (const r of parsedEnvelope.requests) created.push(r.id);
|
|
550
|
+
for (const w of parsedEnvelope.warnings) {
|
|
551
|
+
process.stderr.write(`${kleur3.yellow("warning")}: ${w}
|
|
552
|
+
`);
|
|
553
|
+
}
|
|
554
|
+
await saveToFile3(dir, { synced: nextSynced, local: nextLocal });
|
|
555
|
+
process.stdout.write(
|
|
556
|
+
`${kleur3.green("imported")} folder "${parsedEnvelope.rootFolder.name}" (${parsedEnvelope.subfolders.length + 1} folders, ${parsedEnvelope.requests.length} requests) into ${dir}
|
|
557
|
+
`
|
|
558
|
+
);
|
|
559
|
+
if (parsedEnvelope.dependencies.files.length > 0) {
|
|
560
|
+
process.stderr.write(
|
|
561
|
+
`${kleur3.yellow("note")}: ${parsedEnvelope.dependencies.files.length} file asset${parsedEnvelope.dependencies.files.length === 1 ? "" : "s"} landed without bytes \u2014 re-attach them inside Global Assets \u2192 Global Files.
|
|
562
|
+
`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
529
566
|
} else {
|
|
530
567
|
process.stderr.write(`${kleur3.red("error")}: unknown type '${String(type)}'
|
|
531
568
|
`);
|
|
@@ -540,13 +577,13 @@ function registerImportCommand(program) {
|
|
|
540
577
|
}
|
|
541
578
|
async function readInput(p) {
|
|
542
579
|
if (p === "-") {
|
|
543
|
-
return new Promise((
|
|
580
|
+
return new Promise((resolve8, reject) => {
|
|
544
581
|
let data = "";
|
|
545
582
|
process.stdin.setEncoding("utf-8");
|
|
546
583
|
process.stdin.on("data", (chunk) => {
|
|
547
584
|
data += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
548
585
|
});
|
|
549
|
-
process.stdin.on("end", () =>
|
|
586
|
+
process.stdin.on("end", () => resolve8(data));
|
|
550
587
|
process.stdin.on("error", reject);
|
|
551
588
|
});
|
|
552
589
|
}
|
|
@@ -570,9 +607,112 @@ function blankRequest(partial) {
|
|
|
570
607
|
};
|
|
571
608
|
}
|
|
572
609
|
|
|
610
|
+
// src/commands/export.ts
|
|
611
|
+
import { promises as fs5 } from "fs";
|
|
612
|
+
import * as path5 from "path";
|
|
613
|
+
import kleur4 from "kleur";
|
|
614
|
+
import {
|
|
615
|
+
collectFolderExport,
|
|
616
|
+
redactFolderExportCredentials,
|
|
617
|
+
serializeFolderExport,
|
|
618
|
+
suggestFolderExportFilename
|
|
619
|
+
} from "@apicircle/core";
|
|
620
|
+
function registerExportCommand(program) {
|
|
621
|
+
const exportCmd = program.command("export").description("Export workspace entities to portable JSON.");
|
|
622
|
+
exportCmd.command("folder").description("Export a folder (and its subtree) as an apicircle.folder/v1 JSON envelope.").argument(
|
|
623
|
+
"<folder>",
|
|
624
|
+
"Folder id, or display name (case-insensitive). Unique within the workspace."
|
|
625
|
+
).option("-o, --out <path>", "Write the JSON to this file. Defaults to stdout.").option(
|
|
626
|
+
"--include-credential <id>",
|
|
627
|
+
"Keep the credential field with this id (repeatable). Use --list-credentials to see ids.",
|
|
628
|
+
(value, prev = []) => [...prev, value],
|
|
629
|
+
[]
|
|
630
|
+
).option(
|
|
631
|
+
"--list-credentials",
|
|
632
|
+
"Print the detected credentials + their ids and exit without writing anything."
|
|
633
|
+
).option(
|
|
634
|
+
"--workspace-name <name-or-id>",
|
|
635
|
+
"Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
|
|
636
|
+
).option(
|
|
637
|
+
"-w, --workspace-path <dir>",
|
|
638
|
+
"Filesystem directory containing workspace.synced.json (skips the registry)."
|
|
639
|
+
).action(async (folder, opts) => {
|
|
640
|
+
let dir;
|
|
641
|
+
try {
|
|
642
|
+
const resolved = await resolveWorkspace({
|
|
643
|
+
name: opts.workspaceName,
|
|
644
|
+
path: opts.workspacePath
|
|
645
|
+
});
|
|
646
|
+
dir = resolved.dir;
|
|
647
|
+
} catch (err) {
|
|
648
|
+
if (err instanceof WorkspaceResolutionError) {
|
|
649
|
+
process.stderr.write(`${kleur4.red("error")}: ${err.message}
|
|
650
|
+
`);
|
|
651
|
+
process.exit(2);
|
|
652
|
+
}
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
const state = await ensureWorkspace(dir);
|
|
656
|
+
const folderId = resolveFolderId(state.synced.collections.folders, folder);
|
|
657
|
+
if (!folderId) {
|
|
658
|
+
process.stderr.write(`${kleur4.red("error")}: no folder matches "${folder}" in ${dir}
|
|
659
|
+
`);
|
|
660
|
+
process.exit(2);
|
|
661
|
+
}
|
|
662
|
+
const collected = collectFolderExport({ synced: state.synced, folderId });
|
|
663
|
+
if (!collected) {
|
|
664
|
+
process.stderr.write(`${kleur4.red("error")}: folder "${folder}" no longer exists
|
|
665
|
+
`);
|
|
666
|
+
process.exit(2);
|
|
667
|
+
}
|
|
668
|
+
if (opts.listCredentials) {
|
|
669
|
+
if (collected.report.credentials.length === 0) {
|
|
670
|
+
process.stdout.write("No credential-bearing auth fields detected.\n");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
for (const cred of collected.report.credentials) {
|
|
674
|
+
process.stdout.write(`${cred.id} ${cred.label} ${cred.ownerName}
|
|
675
|
+
`);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const includeIds = new Set(opts.includeCredential ?? []);
|
|
680
|
+
const envelope = redactFolderExportCredentials(collected.envelope, includeIds);
|
|
681
|
+
const json = serializeFolderExport(envelope);
|
|
682
|
+
if (opts.out) {
|
|
683
|
+
const outPath = path5.resolve(opts.out);
|
|
684
|
+
await fs5.writeFile(outPath, json, "utf-8");
|
|
685
|
+
process.stderr.write(
|
|
686
|
+
`${kleur4.green("exported")} folder "${collected.report.folderName}" \u2192 ${outPath}
|
|
687
|
+
`
|
|
688
|
+
);
|
|
689
|
+
} else {
|
|
690
|
+
process.stdout.write(json);
|
|
691
|
+
process.stdout.write("\n");
|
|
692
|
+
process.stderr.write(
|
|
693
|
+
`${kleur4.green("exported")} folder "${collected.report.folderName}" (${collected.report.totalFolderCount} folders, ${collected.report.requestCount} requests, ${collected.report.credentials.length - includeIds.size} credentials redacted)
|
|
694
|
+
`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (!opts.out) {
|
|
698
|
+
process.stderr.write(
|
|
699
|
+
`${kleur4.dim("hint")}: save with .apicircle.json, e.g. ${suggestFolderExportFilename(envelope)}
|
|
700
|
+
`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function resolveFolderId(folders, query) {
|
|
706
|
+
if (folders[query]) return query;
|
|
707
|
+
const norm = query.trim().toLowerCase();
|
|
708
|
+
const matches = Object.values(folders).filter((f) => f.name.trim().toLowerCase() === norm);
|
|
709
|
+
if (matches.length === 1) return matches[0].id;
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
573
713
|
// src/commands/run.ts
|
|
574
714
|
import * as os2 from "os";
|
|
575
|
-
import
|
|
715
|
+
import kleur5 from "kleur";
|
|
576
716
|
import {
|
|
577
717
|
ANONYMOUS_ACTOR,
|
|
578
718
|
PlanRunDeniedError,
|
|
@@ -582,16 +722,16 @@ import {
|
|
|
582
722
|
import { loadFromFile as loadFromFile2, saveToFile as saveToFile4 } from "@apicircle/core/workspace/file-backed";
|
|
583
723
|
|
|
584
724
|
// src/util/secrets.ts
|
|
585
|
-
import * as
|
|
586
|
-
import { promises as
|
|
725
|
+
import * as path6 from "path";
|
|
726
|
+
import { promises as fs6 } from "fs";
|
|
587
727
|
var DEFAULT_PREFIX = "APICIRCLE_SECRET_";
|
|
588
728
|
async function buildSecretsFromCli(options = {}) {
|
|
589
729
|
const env = options.env ?? process.env;
|
|
590
730
|
const prefix = options.envPrefix ?? DEFAULT_PREFIX;
|
|
591
731
|
const byId = {};
|
|
592
732
|
if (options.secretsFile) {
|
|
593
|
-
const resolved =
|
|
594
|
-
const raw = await
|
|
733
|
+
const resolved = path6.resolve(options.secretsFile);
|
|
734
|
+
const raw = await fs6.readFile(resolved, "utf8");
|
|
595
735
|
const parsed = JSON.parse(raw);
|
|
596
736
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
597
737
|
throw new Error(
|
|
@@ -615,16 +755,16 @@ async function buildSecretsFromCli(options = {}) {
|
|
|
615
755
|
|
|
616
756
|
// src/util/executionAttachments.ts
|
|
617
757
|
import { createHash } from "crypto";
|
|
618
|
-
import { promises as
|
|
619
|
-
import * as
|
|
758
|
+
import { promises as fs7 } from "fs";
|
|
759
|
+
import * as path7 from "path";
|
|
620
760
|
import {
|
|
621
761
|
collectAttachmentSlots
|
|
622
762
|
} from "@apicircle/core";
|
|
623
|
-
var ATTACHMENTS_DIR =
|
|
763
|
+
var ATTACHMENTS_DIR = path7.join(".apicircle", "attachments");
|
|
624
764
|
async function prepareExecutionAttachments(workspaceDir, state, plan) {
|
|
625
|
-
const cacheDir =
|
|
765
|
+
const cacheDir = path7.resolve(workspaceDir, ATTACHMENTS_DIR);
|
|
626
766
|
const requirements = collectExecutionAttachmentRequirements(state, plan);
|
|
627
|
-
await
|
|
767
|
+
await fs7.mkdir(cacheDir, { recursive: true });
|
|
628
768
|
let downloaded = 0;
|
|
629
769
|
let alreadyPresent = 0;
|
|
630
770
|
let failed = 0;
|
|
@@ -633,7 +773,7 @@ async function prepareExecutionAttachments(workspaceDir, state, plan) {
|
|
|
633
773
|
};
|
|
634
774
|
const entries = [];
|
|
635
775
|
for (const requirement of requirements) {
|
|
636
|
-
const localPath =
|
|
776
|
+
const localPath = path7.join(cacheDir, encodeURIComponent(requirement.slotId));
|
|
637
777
|
const present = await hasExpectedFile(localPath, requirement.sha256);
|
|
638
778
|
if (present) {
|
|
639
779
|
alreadyPresent++;
|
|
@@ -650,7 +790,7 @@ async function prepareExecutionAttachments(workspaceDir, state, plan) {
|
|
|
650
790
|
`Attachment ${attachmentLabel(requirement)} failed checksum verification.`
|
|
651
791
|
);
|
|
652
792
|
}
|
|
653
|
-
await
|
|
793
|
+
await fs7.writeFile(localPath, bytes, { mode: 384 });
|
|
654
794
|
downloaded++;
|
|
655
795
|
} catch (err) {
|
|
656
796
|
failed++;
|
|
@@ -707,7 +847,7 @@ function createFileAttachmentResolver(state) {
|
|
|
707
847
|
return async (slotId) => {
|
|
708
848
|
const meta = state.local.attachmentCache?.[slotId];
|
|
709
849
|
if (!meta) return null;
|
|
710
|
-
const bytes = await
|
|
850
|
+
const bytes = await fs7.readFile(meta.localPath);
|
|
711
851
|
const view = new Uint8Array(bytes);
|
|
712
852
|
const body = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
713
853
|
return {
|
|
@@ -812,7 +952,7 @@ function bodyReferencesSlot(body, slotId) {
|
|
|
812
952
|
}
|
|
813
953
|
async function hasExpectedFile(localPath, sha256) {
|
|
814
954
|
try {
|
|
815
|
-
const bytes = await
|
|
955
|
+
const bytes = await fs7.readFile(localPath);
|
|
816
956
|
if (!sha256) return true;
|
|
817
957
|
return sha256Hex(bytes) === sha256;
|
|
818
958
|
} catch {
|
|
@@ -821,7 +961,7 @@ async function hasExpectedFile(localPath, sha256) {
|
|
|
821
961
|
}
|
|
822
962
|
async function fileSize(localPath) {
|
|
823
963
|
try {
|
|
824
|
-
return (await
|
|
964
|
+
return (await fs7.stat(localPath)).size;
|
|
825
965
|
} catch {
|
|
826
966
|
return 0;
|
|
827
967
|
}
|
|
@@ -901,7 +1041,7 @@ function registerRunCommand(program) {
|
|
|
901
1041
|
dir = resolved.dir;
|
|
902
1042
|
if (resolved.fromRegistry) {
|
|
903
1043
|
process.stderr.write(
|
|
904
|
-
`${
|
|
1044
|
+
`${kleur5.dim("workspace")}: ${kleur5.cyan(resolved.name ?? resolved.id ?? "")} ${kleur5.dim(`(${dir})`)}
|
|
905
1045
|
`
|
|
906
1046
|
);
|
|
907
1047
|
}
|
|
@@ -1030,17 +1170,17 @@ function formatHeader(plan, actor, withAssertions, opts) {
|
|
|
1030
1170
|
opts.bail ? "bail" : null,
|
|
1031
1171
|
opts.env ? `env=${opts.env}` : null
|
|
1032
1172
|
].filter((f) => f !== null);
|
|
1033
|
-
return `${
|
|
1173
|
+
return `${kleur5.bold("Plan")} ${plan.name} ${kleur5.dim(
|
|
1034
1174
|
`(${enabled}/${plan.steps.length} steps \xB7 ${flags.join(" \xB7 ")})`
|
|
1035
1175
|
)}
|
|
1036
|
-
${
|
|
1176
|
+
${kleur5.dim("Run by")} ${actor.name} ${kleur5.dim(`(${actor.kind})`)}
|
|
1037
1177
|
|
|
1038
1178
|
`;
|
|
1039
1179
|
}
|
|
1040
1180
|
function formatAttachmentPreparation(summary) {
|
|
1041
1181
|
const status = `${summary.downloaded} downloaded, ${summary.alreadyPresent} already local`;
|
|
1042
1182
|
const lines = [
|
|
1043
|
-
`${
|
|
1183
|
+
`${kleur5.bold("Attachments")} ${summary.total} required ${kleur5.dim(
|
|
1044
1184
|
`(${status} - ${summary.cacheDir})`
|
|
1045
1185
|
)}`
|
|
1046
1186
|
];
|
|
@@ -1048,7 +1188,7 @@ function formatAttachmentPreparation(summary) {
|
|
|
1048
1188
|
const source = entry2.source === "linked-workspace" ? `linked:${entry2.linkedWorkspaceId ?? "unknown"}` : "workspace";
|
|
1049
1189
|
const requiredBy = entry2.requiredBy.map((item) => item.requestName).join(", ");
|
|
1050
1190
|
lines.push(
|
|
1051
|
-
` ${
|
|
1191
|
+
` ${kleur5.dim("file")} ${entry2.filename} ${kleur5.dim(
|
|
1052
1192
|
`${source} - ${requiredBy} - ${entry2.localPath}`
|
|
1053
1193
|
)}`
|
|
1054
1194
|
);
|
|
@@ -1061,31 +1201,31 @@ function formatStepLine(step) {
|
|
|
1061
1201
|
const n = `${step.stepIndex + 1}.`.padEnd(3);
|
|
1062
1202
|
const method = (step.requestMethod || "\u2014").padEnd(7);
|
|
1063
1203
|
if (step.skipped) {
|
|
1064
|
-
return ` ${
|
|
1204
|
+
return ` ${kleur5.dim("\u2013")} ${kleur5.dim(n)} ${kleur5.dim(method)} ${kleur5.dim(
|
|
1065
1205
|
`${step.requestName} skipped`
|
|
1066
1206
|
)}
|
|
1067
1207
|
`;
|
|
1068
1208
|
}
|
|
1069
|
-
const mark = step.passed ?
|
|
1209
|
+
const mark = step.passed ? kleur5.green("\u2713") : kleur5.red("\u2717");
|
|
1070
1210
|
const status = step.result?.status != null ? String(step.result.status) : "\u2014";
|
|
1071
1211
|
const duration = step.result ? `${step.result.durationMs}ms` : "";
|
|
1072
1212
|
const name = step.requestName.padEnd(28);
|
|
1073
|
-
let line = ` ${mark} ${n} ${method} ${name} ${status.padEnd(4)} ${
|
|
1213
|
+
let line = ` ${mark} ${n} ${method} ${name} ${status.padEnd(4)} ${kleur5.dim(duration)}`;
|
|
1074
1214
|
if (step.assertionResults.length > 0) {
|
|
1075
1215
|
const passed = step.assertionResults.filter((a) => a.passed).length;
|
|
1076
|
-
line += ` ${
|
|
1216
|
+
line += ` ${kleur5.dim(`${passed}/${step.assertionResults.length} assertions`)}`;
|
|
1077
1217
|
}
|
|
1078
1218
|
line += "\n";
|
|
1079
1219
|
if (step.error) {
|
|
1080
|
-
line += ` ${
|
|
1220
|
+
line += ` ${kleur5.red(step.error)}
|
|
1081
1221
|
`;
|
|
1082
1222
|
}
|
|
1083
1223
|
for (const a of step.assertionResults) {
|
|
1084
|
-
if (!a.passed) line += ` ${
|
|
1224
|
+
if (!a.passed) line += ` ${kleur5.red("\u2717")} ${a.detail ?? `${a.kind} ${a.op}`}
|
|
1085
1225
|
`;
|
|
1086
1226
|
}
|
|
1087
1227
|
if (step.missingVariables.length > 0) {
|
|
1088
|
-
line += ` ${
|
|
1228
|
+
line += ` ${kleur5.yellow("\u26A0")} unresolved: ${step.missingVariables.map((v) => `{{${v}}}`).join(", ")}
|
|
1089
1229
|
`;
|
|
1090
1230
|
}
|
|
1091
1231
|
return line;
|
|
@@ -1104,24 +1244,24 @@ function tally(result) {
|
|
|
1104
1244
|
function formatSummary(result, saved, aborted) {
|
|
1105
1245
|
if (result.steps.length === 0) {
|
|
1106
1246
|
return `
|
|
1107
|
-
${
|
|
1247
|
+
${kleur5.yellow("Plan has no steps.")}
|
|
1108
1248
|
`;
|
|
1109
1249
|
}
|
|
1110
1250
|
const { passed, failed, skipped } = tally(result);
|
|
1111
1251
|
const parts = [
|
|
1112
|
-
|
|
1113
|
-
failed > 0 ?
|
|
1114
|
-
|
|
1252
|
+
kleur5.green(`${passed} passed`),
|
|
1253
|
+
failed > 0 ? kleur5.red(`${failed} failed`) : kleur5.dim(`${failed} failed`),
|
|
1254
|
+
kleur5.dim(`${skipped} skipped`)
|
|
1115
1255
|
];
|
|
1116
|
-
const verdict = result.passed && !aborted ?
|
|
1256
|
+
const verdict = result.passed && !aborted ? kleur5.green("PASS") : kleur5.red("FAIL");
|
|
1117
1257
|
let out = `
|
|
1118
|
-
${verdict} ${parts.join(
|
|
1258
|
+
${verdict} ${parts.join(kleur5.dim(" \xB7 "))} ${kleur5.dim(
|
|
1119
1259
|
`\xB7 ${result.planRun.durationMs}ms`
|
|
1120
1260
|
)}
|
|
1121
1261
|
`;
|
|
1122
|
-
if (aborted) out += `${
|
|
1262
|
+
if (aborted) out += `${kleur5.yellow("Run aborted before every step finished.")}
|
|
1123
1263
|
`;
|
|
1124
|
-
out += saved ?
|
|
1264
|
+
out += saved ? kleur5.dim("Plan run saved to workspace history.\n") : kleur5.dim("Plan run not saved (--no-save).\n");
|
|
1125
1265
|
return out;
|
|
1126
1266
|
}
|
|
1127
1267
|
function buildJsonReport(workspace, planId, plan, actor, result, saved, aborted, attachments) {
|
|
@@ -1197,13 +1337,13 @@ ${cases.join("\n")}
|
|
|
1197
1337
|
`;
|
|
1198
1338
|
}
|
|
1199
1339
|
function fail(message, code = 2, kind = "error") {
|
|
1200
|
-
process.stderr.write(`${
|
|
1340
|
+
process.stderr.write(`${kleur5.red(kind)}: ${message}
|
|
1201
1341
|
`);
|
|
1202
1342
|
process.exitCode = code;
|
|
1203
1343
|
}
|
|
1204
1344
|
|
|
1205
1345
|
// src/commands/workspaces.ts
|
|
1206
|
-
import
|
|
1346
|
+
import kleur6 from "kleur";
|
|
1207
1347
|
import { findWorkspaceEntry as findWorkspaceEntry2, setActiveWorkspace } from "@apicircle/core/workspace/registry";
|
|
1208
1348
|
function registerWorkspacesCommand(program) {
|
|
1209
1349
|
const ws = program.command("workspaces").description("List, create, or switch the active workspace");
|
|
@@ -1215,15 +1355,15 @@ function registerWorkspacesCommand(program) {
|
|
|
1215
1355
|
}
|
|
1216
1356
|
if (registry.workspaces.length === 0) {
|
|
1217
1357
|
process.stdout.write(
|
|
1218
|
-
`${
|
|
1219
|
-
${
|
|
1358
|
+
`${kleur6.dim("No workspaces registered yet at")} ${root}
|
|
1359
|
+
${kleur6.dim("Run")} ${kleur6.cyan("apicircle workspaces create <name>")} ${kleur6.dim(
|
|
1220
1360
|
"or open the desktop app to seed one."
|
|
1221
1361
|
)}
|
|
1222
1362
|
`
|
|
1223
1363
|
);
|
|
1224
1364
|
return;
|
|
1225
1365
|
}
|
|
1226
|
-
process.stdout.write(`${
|
|
1366
|
+
process.stdout.write(`${kleur6.dim("registry")}: ${root}
|
|
1227
1367
|
|
|
1228
1368
|
`);
|
|
1229
1369
|
const rows = [...registry.workspaces].sort(
|
|
@@ -1232,22 +1372,22 @@ ${kleur5.dim("Run")} ${kleur5.cyan("apicircle workspaces create <name>")} ${kleu
|
|
|
1232
1372
|
const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
|
|
1233
1373
|
const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
|
|
1234
1374
|
process.stdout.write(
|
|
1235
|
-
|
|
1375
|
+
kleur6.bold(
|
|
1236
1376
|
` ${"".padEnd(1)} ${"NAME".padEnd(nameWidth)} ${"ID".padEnd(idWidth)} LAST OPENED
|
|
1237
1377
|
`
|
|
1238
1378
|
)
|
|
1239
1379
|
);
|
|
1240
1380
|
for (const w of rows) {
|
|
1241
|
-
const mark = w.id === registry.activeWorkspaceId ?
|
|
1381
|
+
const mark = w.id === registry.activeWorkspaceId ? kleur6.green("\u25CF") : " ";
|
|
1242
1382
|
process.stdout.write(
|
|
1243
|
-
` ${mark} ${w.name.padEnd(nameWidth)} ${
|
|
1383
|
+
` ${mark} ${w.name.padEnd(nameWidth)} ${kleur6.dim(
|
|
1244
1384
|
w.id.padEnd(idWidth)
|
|
1245
|
-
)} ${
|
|
1385
|
+
)} ${kleur6.dim(w.lastOpenedAt)}
|
|
1246
1386
|
`
|
|
1247
1387
|
);
|
|
1248
1388
|
}
|
|
1249
1389
|
process.stdout.write(`
|
|
1250
|
-
${
|
|
1390
|
+
${kleur6.dim("\u25CF = active")}
|
|
1251
1391
|
`);
|
|
1252
1392
|
});
|
|
1253
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) => {
|
|
@@ -1257,17 +1397,17 @@ ${kleur5.dim("\u25CF = active")}
|
|
|
1257
1397
|
sampleRequest: opts.sample ?? false
|
|
1258
1398
|
});
|
|
1259
1399
|
process.stdout.write(
|
|
1260
|
-
`${
|
|
1400
|
+
`${kleur6.green("created")} workspace ${kleur6.cyan(entry2.name)} ${kleur6.dim(`(${entry2.id})`)}
|
|
1261
1401
|
at ${dir}
|
|
1262
1402
|
`
|
|
1263
1403
|
);
|
|
1264
1404
|
if (registry.activeWorkspaceId === entry2.id) {
|
|
1265
|
-
process.stdout.write(`${
|
|
1405
|
+
process.stdout.write(`${kleur6.dim("marked as active")}
|
|
1266
1406
|
`);
|
|
1267
1407
|
}
|
|
1268
1408
|
} catch (err) {
|
|
1269
1409
|
process.stderr.write(
|
|
1270
|
-
`${
|
|
1410
|
+
`${kleur6.red("error")}: ${err instanceof Error ? err.message : String(err)}
|
|
1271
1411
|
`
|
|
1272
1412
|
);
|
|
1273
1413
|
process.exit(2);
|
|
@@ -1278,8 +1418,8 @@ ${kleur5.dim("\u25CF = active")}
|
|
|
1278
1418
|
const entry2 = findWorkspaceEntry2(registry, selector);
|
|
1279
1419
|
if (!entry2) {
|
|
1280
1420
|
process.stderr.write(
|
|
1281
|
-
`${
|
|
1282
|
-
${
|
|
1421
|
+
`${kleur6.red("error")}: no workspace named "${selector}" in the registry at ${root}.
|
|
1422
|
+
${kleur6.dim("Run")} ${kleur6.cyan("apicircle workspaces list")} ${kleur6.dim("to see what is available.")}
|
|
1283
1423
|
`
|
|
1284
1424
|
);
|
|
1285
1425
|
process.exit(2);
|
|
@@ -1288,7 +1428,7 @@ ${kleur5.dim("Run")} ${kleur5.cyan("apicircle workspaces list")} ${kleur5.dim("t
|
|
|
1288
1428
|
const next = await setActiveWorkspace(root, entry2.id);
|
|
1289
1429
|
void next;
|
|
1290
1430
|
process.stdout.write(
|
|
1291
|
-
`${
|
|
1431
|
+
`${kleur6.green("active")} workspace is now ${kleur6.cyan(entry2.name)} ${kleur6.dim(`(${entry2.id})`)}
|
|
1292
1432
|
`
|
|
1293
1433
|
);
|
|
1294
1434
|
});
|
|
@@ -1301,7 +1441,7 @@ ${kleur5.dim("Run")} ${kleur5.cyan("apicircle workspaces list")} ${kleur5.dim("t
|
|
|
1301
1441
|
const entry2 = findWorkspaceEntry2(registry, selector);
|
|
1302
1442
|
if (!entry2) {
|
|
1303
1443
|
process.stderr.write(
|
|
1304
|
-
`${
|
|
1444
|
+
`${kleur6.red("error")}: no workspace named "${selector}" in the registry at ${root}.
|
|
1305
1445
|
`
|
|
1306
1446
|
);
|
|
1307
1447
|
process.exit(2);
|
|
@@ -1320,6 +1460,7 @@ function buildProgram() {
|
|
|
1320
1460
|
registerMockCommand(program);
|
|
1321
1461
|
registerMcpCommand(program);
|
|
1322
1462
|
registerImportCommand(program);
|
|
1463
|
+
registerExportCommand(program);
|
|
1323
1464
|
registerRunCommand(program);
|
|
1324
1465
|
registerWorkspacesCommand(program);
|
|
1325
1466
|
return program;
|