@glubean/cli 0.8.3 โ 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/commands/dry-run.d.ts +105 -0
- package/dist/commands/dry-run.d.ts.map +1 -0
- package/dist/commands/dry-run.js +238 -0
- package/dist/commands/dry-run.js.map +1 -0
- package/dist/commands/load.d.ts.map +1 -1
- package/dist/commands/load.js +20 -2
- package/dist/commands/load.js.map +1 -1
- package/dist/commands/run.d.ts +18 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +188 -24
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/sync.d.ts +21 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +322 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/lib/active_env.d.ts +16 -1
- package/dist/lib/active_env.d.ts.map +1 -1
- package/dist/lib/active_env.js +46 -1
- package/dist/lib/active_env.js.map +1 -1
- package/dist/lib/only-selectors.d.ts +61 -0
- package/dist/lib/only-selectors.d.ts.map +1 -0
- package/dist/lib/only-selectors.js +79 -0
- package/dist/lib/only-selectors.js.map +1 -0
- package/dist/lib/upload.d.ts +53 -0
- package/dist/lib/upload.d.ts.map +1 -1
- package/dist/lib/upload.js +196 -9
- package/dist/lib/upload.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +72 -3
- package/dist/main.js.map +1 -1
- package/package.json +7 -7
- package/dist/lib/env.d.ts +0 -29
- package/dist/lib/env.d.ts.map +0 -1
- package/dist/lib/env.js +0 -59
- package/dist/lib/env.js.map +0 -1
package/dist/commands/run.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { bootstrap, evaluateThresholds, MetricCollector, ProjectRunner, buildRunContext, } from "@glubean/runner";
|
|
2
|
+
import { buildOnlySelectorsFromFlags, deriveRerunSelectors } from "../lib/only-selectors.js";
|
|
2
3
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { stat, readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
5
6
|
import { glob } from "node:fs/promises";
|
|
6
7
|
import { CONFIG_DEFAULTS, mergeRunOptions, toSharedRunConfig } from "../lib/config.js";
|
|
7
8
|
import { loadProjectEnv } from "@glubean/runner";
|
|
8
|
-
import { resolveEnvFileName } from "../lib/active_env.js";
|
|
9
|
+
import { resolveEnvFileName, SensitiveActiveEnvError } from "../lib/active_env.js";
|
|
9
10
|
import { shouldSkipTest } from "../lib/skip.js";
|
|
10
11
|
import { extractContractCases, extractFromSource } from "@glubean/scanner/static";
|
|
11
|
-
import { buildSuffixes, classifyByStem, extractContractFromFile, findTemplateMatch, GLUBEAN_KINDS, loadProjectOverlays, matchesTemplateFilter, } from "@glubean/scanner";
|
|
12
|
+
import { buildSuffixes, classifyByStem, extractContractFromFile, findTemplateMatch, findTemplateMatches, GLUBEAN_KINDS, loadProjectOverlays, matchesTemplateFilter, } from "@glubean/scanner";
|
|
12
13
|
import { applyEnvTemplating } from "@glubean/runner";
|
|
13
14
|
// ANSI color codes for pretty output
|
|
14
15
|
const colors = {
|
|
@@ -519,8 +520,8 @@ export async function runCommand(target, options = {}) {
|
|
|
519
520
|
const interactive = capabilityProfile.browser;
|
|
520
521
|
const traceCollector = [];
|
|
521
522
|
console.log(`\n${colors.bold}${colors.blue}๐งช Glubean Test Runner${colors.reset}\n`);
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
let testFiles = await resolveTestFiles(target);
|
|
524
|
+
let isMultiFile = testFiles.length > 1;
|
|
524
525
|
// Single string view of target for serialization / display paths
|
|
525
526
|
// (result.json, junit, traces). Multi-suite passes an array; join with
|
|
526
527
|
// ", " so downstream consumers still see a printable target field.
|
|
@@ -544,6 +545,59 @@ export async function runCommand(target, options = {}) {
|
|
|
544
545
|
}
|
|
545
546
|
const startDir = testFiles[0].substring(0, testFiles[0].lastIndexOf("/"));
|
|
546
547
|
const { rootDir } = await findProjectConfig(startDir);
|
|
548
|
+
// โโ B2 M3 โ `{id, rowIndex}` "only" selectors โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
549
|
+
// Validate the --only-id / --row / --rerun-failed combo up front (single
|
|
550
|
+
// gate), then resolve the active selector set. `--rerun-failed` reads the
|
|
551
|
+
// previous run and narrows discovery to the files that failed; `--only-id` /
|
|
552
|
+
// `--row` narrow `testsToRun` below by template-matching concrete ids against
|
|
553
|
+
// static `.each` template ids. Either way the precise per-row filter runs in
|
|
554
|
+
// the harness subprocess via the GLUBEAN_RUNNER_ONLY_SELECTORS env channel.
|
|
555
|
+
let onlySelectors = [];
|
|
556
|
+
const selectorFlags = buildOnlySelectorsFromFlags({
|
|
557
|
+
onlyId: options.onlyId,
|
|
558
|
+
row: options.row,
|
|
559
|
+
rerunFailed: options.rerunFailed,
|
|
560
|
+
});
|
|
561
|
+
if (!selectorFlags.ok) {
|
|
562
|
+
console.error(`\n${colors.red}โ ${selectorFlags.error}${colors.reset}\n`);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
if (options.rerunFailed) {
|
|
566
|
+
const lastRunPath = resolve(rootDir, ".glubean", "last-run.result.json");
|
|
567
|
+
let lastRun;
|
|
568
|
+
try {
|
|
569
|
+
lastRun = JSON.parse(await readFile(lastRunPath, "utf-8"));
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
console.error(`\n${colors.red}โ No previous run found. Run \`glubean run\` first.${colors.reset}\n` +
|
|
573
|
+
`${colors.dim}--rerun-failed reads ${lastRunPath}.${colors.reset}\n`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
const { selectors, files } = deriveRerunSelectors({ tests: lastRun.tests ?? [] });
|
|
577
|
+
if (selectors.length === 0) {
|
|
578
|
+
console.log(`\n${colors.green}โ Last run had no failures โ nothing to rerun.${colors.reset}\n`);
|
|
579
|
+
process.exit(0);
|
|
580
|
+
}
|
|
581
|
+
// Narrow discovery to the files that contained a failure. `files` were
|
|
582
|
+
// written relative to rootDir (resultPayload.tests.filePath) โ rootDir is the
|
|
583
|
+
// stable basis (the dir holding .glubean/), so rerun resolves correctly even
|
|
584
|
+
// when invoked from a different cwd than the original run. Resolve both sides
|
|
585
|
+
// to absolute for an exact match against the discovered testFiles.
|
|
586
|
+
const failedFilesAbs = new Set(files.map((f) => resolve(rootDir, f)));
|
|
587
|
+
testFiles = testFiles.filter((f) => failedFilesAbs.has(resolve(f)));
|
|
588
|
+
isMultiFile = testFiles.length > 1;
|
|
589
|
+
if (testFiles.length === 0) {
|
|
590
|
+
console.error(`\n${colors.red}โ --rerun-failed: none of the ${failedFilesAbs.size} failed file(s) ` +
|
|
591
|
+
`from the last run are in the current target.${colors.reset}\n`);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
onlySelectors = selectors;
|
|
595
|
+
console.log(`${colors.dim}--rerun-failed: ${selectors.length} failed test(s) across ` +
|
|
596
|
+
`${testFiles.length} file(s)${colors.reset}\n`);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
onlySelectors = selectorFlags.selectors;
|
|
600
|
+
}
|
|
547
601
|
// Config consolidation (docs/06 P2): the legacy package.json `glubean`
|
|
548
602
|
// flat-shape is no longer read. Profile runs get run/redaction/thresholds
|
|
549
603
|
// from the resolved plan (threaded via `options`); non-profile target runs
|
|
@@ -573,10 +627,27 @@ export async function runCommand(target, options = {}) {
|
|
|
573
627
|
console.log(`${colors.dim}Log file: ${logPath}${colors.reset}`);
|
|
574
628
|
}
|
|
575
629
|
// Resolve env file: --env-file flag > .glubean/active-env > config default > .env
|
|
630
|
+
// GLU-88: resolveEnvFileName throws SensitiveActiveEnvError instead of
|
|
631
|
+
// silently returning a prod-like active-env file โ surface it as a clear,
|
|
632
|
+
// actionable CLI error rather than let it propagate as an unhandled
|
|
633
|
+
// rejection.
|
|
576
634
|
const userSpecifiedEnvFile = !!options.envFile;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
635
|
+
let envFileName;
|
|
636
|
+
if (userSpecifiedEnvFile) {
|
|
637
|
+
envFileName = effectiveRun.envFile;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
try {
|
|
641
|
+
envFileName = await resolveEnvFileName(rootDir);
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
if (err instanceof SensitiveActiveEnvError) {
|
|
645
|
+
console.error(`\n${colors.red}Error: ${err.message}${colors.reset}\n`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
580
651
|
const envPath = resolve(rootDir, envFileName);
|
|
581
652
|
if (userSpecifiedEnvFile) {
|
|
582
653
|
try {
|
|
@@ -797,7 +868,7 @@ export async function runCommand(target, options = {}) {
|
|
|
797
868
|
}
|
|
798
869
|
const hasTags = options.tags && options.tags.length > 0;
|
|
799
870
|
const hasExcludeTags = options.excludeTags && options.excludeTags.length > 0;
|
|
800
|
-
|
|
871
|
+
let testsToRun = allFileTests.filter((ft) => {
|
|
801
872
|
const tc = ft.test;
|
|
802
873
|
if (tc.meta.skip)
|
|
803
874
|
return false;
|
|
@@ -811,6 +882,45 @@ export async function runCommand(target, options = {}) {
|
|
|
811
882
|
return false;
|
|
812
883
|
return true;
|
|
813
884
|
});
|
|
885
|
+
// B2 M3 โ narrow testsToRun to the selected ids. Selector ids are CONCRETE
|
|
886
|
+
// (e.g. `user-0`); a `.each` export appears in discovery under its TEMPLATE id
|
|
887
|
+
// (e.g. `user-$index`). Template-match each selector id against the discovered
|
|
888
|
+
// template ids so concrete selectors reach the right export โ the harness then
|
|
889
|
+
// applies the precise per-row filter at runtime.
|
|
890
|
+
//
|
|
891
|
+
// This runs for `--rerun-failed` too (GLU-67 follow-up): `--rerun-failed`
|
|
892
|
+
// narrows DISCOVERY to the failed FILES above, but a failed file can also hold
|
|
893
|
+
// PASSED tests. Without narrowing testsToRun here as well, those passed tests
|
|
894
|
+
// reach the capability-skip pass below and emit spurious `โ skipped` rows that
|
|
895
|
+
// aren't part of the rerun set (the harness `matchOnly` still filters them at
|
|
896
|
+
// runtime, but only after the CLI has already printed the noise). Narrowing here
|
|
897
|
+
// makes the rerun path's ordering match the `--only-id` path: filter to the
|
|
898
|
+
// selected ids FIRST, then capability-skip only what remains.
|
|
899
|
+
//
|
|
900
|
+
// keep-all-matches (GLU-67 follow-up): `findTemplateMatches` (plural) keeps
|
|
901
|
+
// EVERY export a selector id matches โ a bare id shared by two files, or two
|
|
902
|
+
// overlapping `.each` templates, all run โ instead of `findTemplateMatch`'s
|
|
903
|
+
// silent first-match-wins drop.
|
|
904
|
+
if (onlySelectors.length > 0) {
|
|
905
|
+
const selectorIds = Array.from(new Set(onlySelectors.map((s) => (typeof s === "string" ? s : s.id))));
|
|
906
|
+
const indexed = testsToRun.map((ft) => ({ id: ft.test.meta.id, ft }));
|
|
907
|
+
const kept = new Set();
|
|
908
|
+
for (const selId of selectorIds) {
|
|
909
|
+
for (const match of findTemplateMatches(indexed, selId))
|
|
910
|
+
kept.add(match.ft);
|
|
911
|
+
}
|
|
912
|
+
testsToRun = testsToRun.filter((ft) => kept.has(ft));
|
|
913
|
+
if (testsToRun.length === 0) {
|
|
914
|
+
// In rerun mode the ids come from the last run's failed set, not a
|
|
915
|
+
// `--only-id` flag โ report accordingly (e.g. a failed test was renamed
|
|
916
|
+
// or removed since the recorded run).
|
|
917
|
+
console.error(options.rerunFailed
|
|
918
|
+
? `\n${colors.red}โ --rerun-failed: none of the last run's failed test ids ` +
|
|
919
|
+
`(${selectorIds.join(", ")}) still exist in the target files.${colors.reset}\n`
|
|
920
|
+
: `\n${colors.red}โ No tests match --only-id ${selectorIds.join(", ")}${colors.reset}\n`);
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
814
924
|
if (testsToRun.length === 0) {
|
|
815
925
|
if (options.filter || hasTags) {
|
|
816
926
|
const parts = [];
|
|
@@ -952,6 +1062,15 @@ export async function runCommand(target, options = {}) {
|
|
|
952
1062
|
delete process.env["GLUBEAN_RUNNER_BOOTSTRAP_INPUT_MAP"];
|
|
953
1063
|
delete process.env["GLUBEAN_RUNNER_FORCE_STANDALONE_IDS"];
|
|
954
1064
|
}
|
|
1065
|
+
// B2 M3 โ hand the resolved selector set to the harness subprocess (it applies
|
|
1066
|
+
// the precise per-row filter at runtime). Clear it otherwise so a stale value
|
|
1067
|
+
// never leaks across in-process invocations (parity with the input maps above).
|
|
1068
|
+
if (onlySelectors.length > 0) {
|
|
1069
|
+
process.env["GLUBEAN_RUNNER_ONLY_SELECTORS"] = JSON.stringify(onlySelectors);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
delete process.env["GLUBEAN_RUNNER_ONLY_SELECTORS"];
|
|
1073
|
+
}
|
|
955
1074
|
if (options.pick) {
|
|
956
1075
|
process.env.GLUBEAN_PICK = options.pick;
|
|
957
1076
|
console.log(`${colors.dim} pick: ${options.pick}${colors.reset}`);
|
|
@@ -1024,6 +1143,8 @@ export async function runCommand(target, options = {}) {
|
|
|
1024
1143
|
let currentTestItems;
|
|
1025
1144
|
let testId = "";
|
|
1026
1145
|
let testName = "";
|
|
1146
|
+
let testRowIndex = undefined;
|
|
1147
|
+
let testEach = undefined;
|
|
1027
1148
|
let testItem = null;
|
|
1028
1149
|
let startTime = Date.now();
|
|
1029
1150
|
let testEvents = [];
|
|
@@ -1089,6 +1210,8 @@ export async function runCommand(target, options = {}) {
|
|
|
1089
1210
|
success: skippedClean ? true : finalSuccess,
|
|
1090
1211
|
durationMs: duration,
|
|
1091
1212
|
groupId: testItem?.meta.groupId,
|
|
1213
|
+
rowIndex: testRowIndex,
|
|
1214
|
+
each: testEach,
|
|
1092
1215
|
});
|
|
1093
1216
|
addLogEntry("result", skippedClean ? "SKIPPED" : finalSuccess ? "PASSED" : "FAILED", {
|
|
1094
1217
|
duration,
|
|
@@ -1406,6 +1529,14 @@ export async function runCommand(target, options = {}) {
|
|
|
1406
1529
|
(currentTestItems ? findFileTestByRuntimeId(currentTestItems, event.id) : undefined);
|
|
1407
1530
|
testId = event.id;
|
|
1408
1531
|
testName = entry?.test.meta.name || event.name || event.id;
|
|
1532
|
+
// rowIndex (B2 M3): the runtime start event is authoritative for the
|
|
1533
|
+
// per-row `.each` index (static discovery only sees the template id,
|
|
1534
|
+
// so it carries no rowIndex). undefined for non-each tests.
|
|
1535
|
+
testRowIndex = event.rowIndex;
|
|
1536
|
+
// each (B3 T3): same reasoning โ the runtime start event is
|
|
1537
|
+
// authoritative for row-identity provenance (idTemplate/rowKey/
|
|
1538
|
+
// stable). undefined for non-each tests.
|
|
1539
|
+
testEach = event.each;
|
|
1409
1540
|
testItem = entry?.test || null;
|
|
1410
1541
|
startTime = Date.now();
|
|
1411
1542
|
testEvents = [];
|
|
@@ -1871,6 +2002,18 @@ export async function runCommand(target, options = {}) {
|
|
|
1871
2002
|
success: r.success,
|
|
1872
2003
|
durationMs: r.durationMs,
|
|
1873
2004
|
events: r.events,
|
|
2005
|
+
// B2 M3 โ persist rowIndex + filePath so `--rerun-failed` can reconstruct
|
|
2006
|
+
// the failed `{id, rowIndex}` selector set and narrow to the failed files.
|
|
2007
|
+
// filePath is relative to rootDir (the stable .glubean/ basis), NOT cwd, so
|
|
2008
|
+
// rerun resolves correctly when run from a different working directory.
|
|
2009
|
+
...(r.rowIndex !== undefined && { rowIndex: r.rowIndex }),
|
|
2010
|
+
// B3 T3 (`run-evidence-identity-model.md` ยง7/ยง14) โ persist row-identity
|
|
2011
|
+
// provenance (idTemplate/rowKey/stable) on each test entry, so the
|
|
2012
|
+
// uploaded run blob (`result.tests[].each`) self-describes row identity
|
|
2013
|
+
// without depending on a projection join. Undefined for non-each tests
|
|
2014
|
+
// (backward compatible: old runs / old CLI builds simply omit the field).
|
|
2015
|
+
...(r.each !== undefined && { each: r.each }),
|
|
2016
|
+
filePath: relative(rootDir, r.filePath),
|
|
1874
2017
|
})),
|
|
1875
2018
|
...(thresholdSummary && { thresholds: thresholdSummary }),
|
|
1876
2019
|
...(options.meta && Object.keys(options.meta).length > 0 && { customMetadata: options.meta }),
|
|
@@ -1924,29 +2067,30 @@ export async function runCommand(target, options = {}) {
|
|
|
1924
2067
|
}
|
|
1925
2068
|
}
|
|
1926
2069
|
// โโ Screenshot paths โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2070
|
+
// This run's exact screenshot files, pulled from the `browser:screenshot`
|
|
2071
|
+
// event stream. Doubles as the upload whitelist below so `--upload` attaches
|
|
2072
|
+
// only THIS run's screenshots, not every file in the shared dir (ART1).
|
|
2073
|
+
const screenshotPaths = [];
|
|
2074
|
+
for (const run of collectedRuns) {
|
|
2075
|
+
for (const event of run.events) {
|
|
2076
|
+
if (event.type !== "event")
|
|
2077
|
+
continue;
|
|
2078
|
+
const ev = event.data;
|
|
2079
|
+
if (ev.type === "browser:screenshot" && typeof ev.data?.path === "string") {
|
|
2080
|
+
screenshotPaths.push(resolve(rootDir, ev.data.path));
|
|
1937
2081
|
}
|
|
1938
2082
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}
|
|
1943
|
-
console.log();
|
|
2083
|
+
}
|
|
2084
|
+
if (screenshotPaths.length > 0) {
|
|
2085
|
+
for (const p of screenshotPaths) {
|
|
2086
|
+
console.log(`${colors.dim}Screenshot: ${colors.reset}${p}`);
|
|
1944
2087
|
}
|
|
2088
|
+
console.log();
|
|
1945
2089
|
}
|
|
1946
2090
|
// โโ Cloud upload โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1947
2091
|
if (options.upload) {
|
|
1948
2092
|
const { resolveToken, resolveProjectId, resolveApiUrl } = await import("../lib/auth.js");
|
|
1949
|
-
const { uploadToCloud } = await import("../lib/upload.js");
|
|
2093
|
+
const { uploadToCloud, removeUploadedScreenshots } = await import("../lib/upload.js");
|
|
1950
2094
|
const authOpts = {
|
|
1951
2095
|
token: options.token,
|
|
1952
2096
|
project: options.project,
|
|
@@ -2096,7 +2240,27 @@ export async function runCommand(target, options = {}) {
|
|
|
2096
2240
|
targetId,
|
|
2097
2241
|
envFile: effectiveRun.envFile,
|
|
2098
2242
|
rootDir,
|
|
2243
|
+
// Upload only THIS run's screenshots (whitelist), not the whole
|
|
2244
|
+
// shared `.glubean/screenshots` dir which accumulates prior runs.
|
|
2245
|
+
screenshotPaths,
|
|
2099
2246
|
});
|
|
2247
|
+
// ART1-B โ the shared screenshots dir only ever grows, so once the
|
|
2248
|
+
// Cloud confirmed it received this run's screenshots, delete the local
|
|
2249
|
+
// copies. Deletes ONLY uploadedFiles โฉ screenshotPaths (both server-
|
|
2250
|
+
// confirmed and provably this run's), realpath-contained to
|
|
2251
|
+
// `.glubean/screenshots`, and only while the on-disk file still has
|
|
2252
|
+
// its upload-time stat identity (re-checked just before each unlink)
|
|
2253
|
+
// โ a failed/partial upload keeps its files, and a concurrently
|
|
2254
|
+
// recreated path is kept.
|
|
2255
|
+
if (!options.keepLocal &&
|
|
2256
|
+
uploadReceipt.artifactUpload.status === "uploaded" &&
|
|
2257
|
+
uploadReceipt.artifactUpload.uploadedFiles?.length &&
|
|
2258
|
+
screenshotPaths.length > 0) {
|
|
2259
|
+
const { removed } = await removeUploadedScreenshots(rootDir, screenshotPaths, uploadReceipt.artifactUpload.uploadedFiles);
|
|
2260
|
+
if (removed > 0) {
|
|
2261
|
+
console.log(`${colors.dim}Cleaned up ${removed} uploaded screenshot(s) from .glubean/screenshots (use --keep-local to keep them)${colors.reset}`);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2100
2264
|
if (options.uploadReceiptJson) {
|
|
2101
2265
|
const receiptPath = resolveOutputPath(options.uploadReceiptJson, process.cwd());
|
|
2102
2266
|
await mkdir(dirname(receiptPath), { recursive: true });
|