@plune-ai/cli 0.4.0 → 0.6.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/dist/cli.cjs +266 -7
- package/dist/index.cjs +377 -6
- package/dist/index.d.cts +118 -1
- package/dist/index.d.ts +118 -1
- package/dist/index.js +365 -5
- package/package.json +11 -3
package/dist/index.cjs
CHANGED
|
@@ -30,13 +30,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
NotLoggedInError: () => NotLoggedInError,
|
|
33
34
|
RUN_RESULT_SCHEMA_VERSION: () => RUN_RESULT_SCHEMA_VERSION,
|
|
35
|
+
SyncFileError: () => SyncFileError,
|
|
36
|
+
SyncHttpError: () => SyncHttpError,
|
|
37
|
+
SyncNetworkError: () => SyncNetworkError,
|
|
38
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
34
39
|
assertionConfigSchema: () => assertionConfigSchema,
|
|
35
40
|
assertionResultRecordSchema: () => assertionResultRecordSchema,
|
|
36
41
|
binaryVerdictSchema: () => binaryVerdictSchema,
|
|
42
|
+
formatIngestResult: () => formatIngestResult,
|
|
43
|
+
ingest: () => handleIngest,
|
|
37
44
|
parseRunResult: () => parseRunResult,
|
|
45
|
+
readCairnRun: () => readCairnRun,
|
|
46
|
+
reportIngestFailure: () => reportIngestFailure,
|
|
47
|
+
reportSyncFailure: () => reportSyncFailure,
|
|
38
48
|
run: () => handleRun,
|
|
39
|
-
runResultSchema: () => runResultSchema
|
|
49
|
+
runResultSchema: () => runResultSchema,
|
|
50
|
+
sync: () => handleSync
|
|
40
51
|
});
|
|
41
52
|
module.exports = __toCommonJS(src_exports);
|
|
42
53
|
|
|
@@ -357,7 +368,7 @@ function normalizeProviderError(err, secret, envVar) {
|
|
|
357
368
|
}
|
|
358
369
|
|
|
359
370
|
// src/providers/retry.ts
|
|
360
|
-
var defaultSleep = (ms) => new Promise((
|
|
371
|
+
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
361
372
|
function retryAfterMs(err) {
|
|
362
373
|
if (typeof err !== "object" || err === null || !("headers" in err)) return void 0;
|
|
363
374
|
const raw = readHeader(err.headers, "retry-after");
|
|
@@ -637,9 +648,11 @@ var XenovaEmbedder = class {
|
|
|
637
648
|
return rows.map((row) => Float32Array.from(row));
|
|
638
649
|
}
|
|
639
650
|
load() {
|
|
640
|
-
this.extractor ??= import("@huggingface/transformers").
|
|
641
|
-
|
|
642
|
-
|
|
651
|
+
this.extractor ??= import("@huggingface/transformers").catch(() => {
|
|
652
|
+
throw new Error(
|
|
653
|
+
"The `semantic-similarity` and RAG assertions need local embeddings, which ship separately: install @huggingface/transformers to enable them. Every other assertion works without it."
|
|
654
|
+
);
|
|
655
|
+
}).then(({ pipeline }) => pipeline("feature-extraction", MODEL_ID));
|
|
643
656
|
return this.extractor;
|
|
644
657
|
}
|
|
645
658
|
};
|
|
@@ -1503,6 +1516,10 @@ async function handleRun(options, depsFactory) {
|
|
|
1503
1516
|
}
|
|
1504
1517
|
}
|
|
1505
1518
|
|
|
1519
|
+
// src/cli/commands/sync.ts
|
|
1520
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
1521
|
+
var path5 = __toESM(require("path"), 1);
|
|
1522
|
+
|
|
1506
1523
|
// src/types/run-result-schema.ts
|
|
1507
1524
|
var import_zod2 = require("zod");
|
|
1508
1525
|
var RUN_RESULT_SCHEMA_VERSION = 1;
|
|
@@ -1561,13 +1578,367 @@ var runResultSchema = import_zod2.z.object({
|
|
|
1561
1578
|
function parseRunResult(value) {
|
|
1562
1579
|
return runResultSchema.parse(value);
|
|
1563
1580
|
}
|
|
1581
|
+
|
|
1582
|
+
// src/cli/credentials.ts
|
|
1583
|
+
var import_node_fs = require("fs");
|
|
1584
|
+
var import_node_os = require("os");
|
|
1585
|
+
var import_node_path = require("path");
|
|
1586
|
+
function credentialsFile() {
|
|
1587
|
+
const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
|
|
1588
|
+
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
1589
|
+
}
|
|
1590
|
+
function loadToken() {
|
|
1591
|
+
try {
|
|
1592
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
1593
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/cli/commands/sync.ts
|
|
1600
|
+
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
1601
|
+
var NotLoggedInError = class extends Error {
|
|
1602
|
+
constructor() {
|
|
1603
|
+
super('Not logged in. Run "plune login" first to save your API token.');
|
|
1604
|
+
this.name = "NotLoggedInError";
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
var TokenRejectedError = class extends Error {
|
|
1608
|
+
constructor() {
|
|
1609
|
+
super(
|
|
1610
|
+
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
1611
|
+
);
|
|
1612
|
+
this.name = "TokenRejectedError";
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
var SyncFileError = class extends Error {
|
|
1616
|
+
constructor(message) {
|
|
1617
|
+
super(message);
|
|
1618
|
+
this.name = "SyncFileError";
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
var SyncNetworkError = class extends Error {
|
|
1622
|
+
constructor(apiUrl) {
|
|
1623
|
+
super(
|
|
1624
|
+
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
1625
|
+
);
|
|
1626
|
+
this.name = "SyncNetworkError";
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
var SyncHttpError = class extends Error {
|
|
1630
|
+
constructor(status, detail) {
|
|
1631
|
+
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
1632
|
+
this.status = status;
|
|
1633
|
+
this.name = "SyncHttpError";
|
|
1634
|
+
}
|
|
1635
|
+
status;
|
|
1636
|
+
};
|
|
1637
|
+
function resolveApiUrl(explicit) {
|
|
1638
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
1639
|
+
return raw.replace(/\/+$/, "");
|
|
1640
|
+
}
|
|
1641
|
+
function readRun(cwd, file) {
|
|
1642
|
+
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1643
|
+
let raw;
|
|
1644
|
+
try {
|
|
1645
|
+
raw = fs6.readFileSync(target, "utf8");
|
|
1646
|
+
} catch {
|
|
1647
|
+
throw new SyncFileError(
|
|
1648
|
+
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
let json;
|
|
1652
|
+
try {
|
|
1653
|
+
json = JSON.parse(raw);
|
|
1654
|
+
} catch {
|
|
1655
|
+
throw new SyncFileError(`Run file is not valid JSON: ${target}`);
|
|
1656
|
+
}
|
|
1657
|
+
try {
|
|
1658
|
+
return parseRunResult(json);
|
|
1659
|
+
} catch {
|
|
1660
|
+
throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async function safeErrorDetail(res) {
|
|
1664
|
+
try {
|
|
1665
|
+
const body = await res.json();
|
|
1666
|
+
return typeof body.error === "string" ? body.error : "";
|
|
1667
|
+
} catch {
|
|
1668
|
+
return "";
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function handleSync(deps = {}) {
|
|
1672
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1673
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
1674
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
1675
|
+
if (token === null) throw new NotLoggedInError();
|
|
1676
|
+
const run = readRun(cwd, deps.file);
|
|
1677
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
1678
|
+
const endpoint = `${apiUrl}/v1/runs`;
|
|
1679
|
+
let res;
|
|
1680
|
+
try {
|
|
1681
|
+
res = await doFetch(endpoint, {
|
|
1682
|
+
method: "POST",
|
|
1683
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
1684
|
+
body: JSON.stringify(run)
|
|
1685
|
+
});
|
|
1686
|
+
} catch {
|
|
1687
|
+
throw new SyncNetworkError(apiUrl);
|
|
1688
|
+
}
|
|
1689
|
+
if (!res.ok) {
|
|
1690
|
+
const detail = await safeErrorDetail(res);
|
|
1691
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
1692
|
+
throw new SyncHttpError(res.status, detail);
|
|
1693
|
+
}
|
|
1694
|
+
const body = await res.json().catch(() => ({}));
|
|
1695
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
1696
|
+
return { id, url: `${endpoint}/${id}` };
|
|
1697
|
+
}
|
|
1698
|
+
function reportSyncFailure(err, write) {
|
|
1699
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
1700
|
+
write(err.message + "\n");
|
|
1701
|
+
return 2;
|
|
1702
|
+
}
|
|
1703
|
+
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
1704
|
+
write(err.message + "\n");
|
|
1705
|
+
return 1;
|
|
1706
|
+
}
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/cli/commands/cairn-artifact.ts
|
|
1711
|
+
var fs7 = __toESM(require("fs/promises"), 1);
|
|
1712
|
+
var path6 = __toESM(require("path"), 1);
|
|
1713
|
+
var SUPPORTED_ARTIFACT_VERSION = 1;
|
|
1714
|
+
var INGEST_SCHEMA_VERSION = 1;
|
|
1715
|
+
var CairnRunNotFoundError = class extends Error {
|
|
1716
|
+
constructor(dir) {
|
|
1717
|
+
super(`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`);
|
|
1718
|
+
this.name = "CairnRunNotFoundError";
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
var CairnRunVersionError = class extends Error {
|
|
1722
|
+
constructor(found) {
|
|
1723
|
+
super(
|
|
1724
|
+
found === void 0 ? "This run predates versioned Cairn artifacts. Re-run it with Cairn 0.7.0 or newer, which stamps the format so it can be read safely." : `Unsupported Cairn artifact version ${String(found)} (this CLI reads version ${SUPPORTED_ARTIFACT_VERSION}). Upgrade @plune-ai/cli.`
|
|
1725
|
+
);
|
|
1726
|
+
this.name = "CairnRunVersionError";
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
var CairnRunPartialError = class extends Error {
|
|
1730
|
+
constructor(reason) {
|
|
1731
|
+
super(`That Cairn run did not finish, so its cases are incomplete: ${reason}`);
|
|
1732
|
+
this.name = "CairnRunPartialError";
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
var CairnEvidenceAmbiguousError = class extends Error {
|
|
1736
|
+
constructor(name) {
|
|
1737
|
+
super(
|
|
1738
|
+
`Cannot attach results safely: the operation "${name}" appears more than once in this run's evidence, and the evidence file carries no case identity to disambiguate it. Nothing was uploaded.`
|
|
1739
|
+
);
|
|
1740
|
+
this.name = "CairnEvidenceAmbiguousError";
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
var asRecord = (v) => typeof v === "object" && v !== null ? v : {};
|
|
1744
|
+
var str = (v, fallback = "") => typeof v === "string" && v !== "" ? v : fallback;
|
|
1745
|
+
async function readJson(file) {
|
|
1746
|
+
try {
|
|
1747
|
+
return JSON.parse(await fs7.readFile(file, "utf8"));
|
|
1748
|
+
} catch {
|
|
1749
|
+
return void 0;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async function defaultReadEvidence(dir) {
|
|
1753
|
+
const raw = await readJson(path6.join(dir, "api-evidence.json"));
|
|
1754
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1755
|
+
return raw.map((r) => {
|
|
1756
|
+
const rec = asRecord(r);
|
|
1757
|
+
return { name: str(rec["name"]), passed: rec["passed"] === true };
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
function casesFromDesign(report) {
|
|
1761
|
+
const list = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
1762
|
+
return list.map((raw) => {
|
|
1763
|
+
const c = asRecord(raw);
|
|
1764
|
+
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
1765
|
+
return {
|
|
1766
|
+
stableId: str(c["stableId"]),
|
|
1767
|
+
title: str(c["title"], "untitled case"),
|
|
1768
|
+
execution: c["execution"] === "manual" ? "manual" : "auto",
|
|
1769
|
+
steps: steps.length > 0 ? steps : ["(no steps recorded)"],
|
|
1770
|
+
expected: str(c["expected"], "(no expected result recorded)"),
|
|
1771
|
+
...typeof c["technique"] === "string" ? { technique: c["technique"] } : {},
|
|
1772
|
+
...typeof c["priority"] === "string" ? { priority: c["priority"] } : {}
|
|
1773
|
+
};
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function casesFromApi(report, verdicts) {
|
|
1777
|
+
const list = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
1778
|
+
return list.map((raw) => {
|
|
1779
|
+
const c = asRecord(raw);
|
|
1780
|
+
const name = str(c["name"], "unnamed operation");
|
|
1781
|
+
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
|
1782
|
+
const verdict = verdicts.get(name);
|
|
1783
|
+
return {
|
|
1784
|
+
stableId: str(c["stableId"]),
|
|
1785
|
+
title: name,
|
|
1786
|
+
execution: "auto",
|
|
1787
|
+
steps: [request],
|
|
1788
|
+
expected: `HTTP ${str(c["expectedStatus"], "default")}`,
|
|
1789
|
+
...verdict !== void 0 ? { verdict } : {},
|
|
1790
|
+
...typeof c["technique"] === "string" ? { technique: c["technique"] } : {}
|
|
1791
|
+
};
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
async function readCairnRun(dir, deps = {}) {
|
|
1795
|
+
const raw = await readJson(path6.join(dir, "report.json"));
|
|
1796
|
+
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
1797
|
+
const report = asRecord(raw);
|
|
1798
|
+
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
1799
|
+
throw new CairnRunVersionError(report["schemaVersion"]);
|
|
1800
|
+
}
|
|
1801
|
+
if (report["partial"] === true) {
|
|
1802
|
+
throw new CairnRunPartialError(str(report["error"], "no reason recorded"));
|
|
1803
|
+
}
|
|
1804
|
+
const mode = report["mode"];
|
|
1805
|
+
if (mode !== "design" && mode !== "explore" && mode !== "api") {
|
|
1806
|
+
throw new CairnRunVersionError(`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`);
|
|
1807
|
+
}
|
|
1808
|
+
let cases;
|
|
1809
|
+
if (mode === "api") {
|
|
1810
|
+
const evidence = await (deps.readEvidence ?? defaultReadEvidence)(dir);
|
|
1811
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
1812
|
+
for (const e of evidence ?? []) {
|
|
1813
|
+
if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
|
|
1814
|
+
verdicts.set(e.name, e.passed ? "passed" : "failed");
|
|
1815
|
+
}
|
|
1816
|
+
cases = casesFromApi(report, verdicts);
|
|
1817
|
+
} else {
|
|
1818
|
+
cases = casesFromDesign(report);
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
1822
|
+
run: {
|
|
1823
|
+
runId: str(report["runId"], path6.basename(dir)),
|
|
1824
|
+
mode,
|
|
1825
|
+
target: str(report["url"], "unknown")
|
|
1826
|
+
},
|
|
1827
|
+
cases
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/cli/commands/ingest.ts
|
|
1832
|
+
var DEFAULT_API_URL2 = "https://beta-api.plune.ai";
|
|
1833
|
+
var IngestNotLoggedInError = class extends Error {
|
|
1834
|
+
constructor() {
|
|
1835
|
+
super('Not logged in. Run "plune login" first to save your API token.');
|
|
1836
|
+
this.name = "IngestNotLoggedInError";
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
var IngestTokenRejectedError = class extends Error {
|
|
1840
|
+
constructor() {
|
|
1841
|
+
super('Your API token was rejected (401). Run "plune login" again with a fresh token.');
|
|
1842
|
+
this.name = "IngestTokenRejectedError";
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
var IngestNetworkError = class extends Error {
|
|
1846
|
+
constructor(apiUrl) {
|
|
1847
|
+
super(`Could not reach the Plune API at ${apiUrl}. Check your connection, then try again.`);
|
|
1848
|
+
this.name = "IngestNetworkError";
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
var IngestHttpError = class extends Error {
|
|
1852
|
+
constructor(status, detail) {
|
|
1853
|
+
super(`Plune API returned ${status}${detail !== "" ? ` \u2014 ${detail}` : ""}.`);
|
|
1854
|
+
this.status = status;
|
|
1855
|
+
this.name = "IngestHttpError";
|
|
1856
|
+
}
|
|
1857
|
+
status;
|
|
1858
|
+
};
|
|
1859
|
+
function resolveApiUrl2(explicit) {
|
|
1860
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL2;
|
|
1861
|
+
return raw.replace(/\/+$/, "");
|
|
1862
|
+
}
|
|
1863
|
+
async function safeErrorDetail2(res) {
|
|
1864
|
+
try {
|
|
1865
|
+
const body = await res.json();
|
|
1866
|
+
return typeof body.error === "string" ? body.error : "";
|
|
1867
|
+
} catch {
|
|
1868
|
+
return "";
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
async function handleIngest(deps) {
|
|
1872
|
+
const apiUrl = resolveApiUrl2(deps.apiUrl);
|
|
1873
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
1874
|
+
if (token === null) throw new IngestNotLoggedInError();
|
|
1875
|
+
const payload = await readCairnRun(deps.dir);
|
|
1876
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
1877
|
+
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
1878
|
+
let res;
|
|
1879
|
+
try {
|
|
1880
|
+
res = await doFetch(endpoint, {
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
1883
|
+
body: JSON.stringify(payload)
|
|
1884
|
+
});
|
|
1885
|
+
} catch {
|
|
1886
|
+
throw new IngestNetworkError(apiUrl);
|
|
1887
|
+
}
|
|
1888
|
+
if (!res.ok) {
|
|
1889
|
+
const detail = await safeErrorDetail2(res);
|
|
1890
|
+
if (res.status === 401) throw new IngestTokenRejectedError();
|
|
1891
|
+
throw new IngestHttpError(res.status, detail);
|
|
1892
|
+
}
|
|
1893
|
+
const body = await res.json().catch(() => ({}));
|
|
1894
|
+
return {
|
|
1895
|
+
runId: typeof body.runId === "string" ? body.runId : "",
|
|
1896
|
+
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
1897
|
+
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
1898
|
+
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function reportIngestFailure(err, write) {
|
|
1902
|
+
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
1903
|
+
write(`${err.message}
|
|
1904
|
+
`);
|
|
1905
|
+
return 2;
|
|
1906
|
+
}
|
|
1907
|
+
if (err instanceof IngestNetworkError || err instanceof IngestHttpError) {
|
|
1908
|
+
write(`${err.message}
|
|
1909
|
+
`);
|
|
1910
|
+
return 1;
|
|
1911
|
+
}
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
function formatIngestResult(r) {
|
|
1915
|
+
const lines = [
|
|
1916
|
+
`Ingested Cairn run \u2192 ${r.runId}`,
|
|
1917
|
+
` ${r.linked} result(s) attached to existing cases`,
|
|
1918
|
+
` ${r.proposed} case(s) proposed \u2014 review them in Plune to accept`,
|
|
1919
|
+
` ${r.skipped} skipped (already queued, already rejected, repeated, or never executed)`
|
|
1920
|
+
];
|
|
1921
|
+
return `${lines.join("\n")}
|
|
1922
|
+
`;
|
|
1923
|
+
}
|
|
1564
1924
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1565
1925
|
0 && (module.exports = {
|
|
1926
|
+
NotLoggedInError,
|
|
1566
1927
|
RUN_RESULT_SCHEMA_VERSION,
|
|
1928
|
+
SyncFileError,
|
|
1929
|
+
SyncHttpError,
|
|
1930
|
+
SyncNetworkError,
|
|
1931
|
+
TokenRejectedError,
|
|
1567
1932
|
assertionConfigSchema,
|
|
1568
1933
|
assertionResultRecordSchema,
|
|
1569
1934
|
binaryVerdictSchema,
|
|
1935
|
+
formatIngestResult,
|
|
1936
|
+
ingest,
|
|
1570
1937
|
parseRunResult,
|
|
1938
|
+
readCairnRun,
|
|
1939
|
+
reportIngestFailure,
|
|
1940
|
+
reportSyncFailure,
|
|
1571
1941
|
run,
|
|
1572
|
-
runResultSchema
|
|
1942
|
+
runResultSchema,
|
|
1943
|
+
sync
|
|
1573
1944
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -214,6 +214,57 @@ interface RunOptions {
|
|
|
214
214
|
}
|
|
215
215
|
declare function handleRun(options: RunOptions, depsFactory?: (config: Config, baseDir: string) => RunDeps): Promise<RunResult>;
|
|
216
216
|
|
|
217
|
+
/** No saved token — the user must `plune login` before syncing. (exit 2) */
|
|
218
|
+
declare class NotLoggedInError extends Error {
|
|
219
|
+
constructor();
|
|
220
|
+
}
|
|
221
|
+
/** The token was present but the server rejected it (401 — revoked or invalid). (exit 2) */
|
|
222
|
+
declare class TokenRejectedError extends Error {
|
|
223
|
+
constructor();
|
|
224
|
+
}
|
|
225
|
+
/** No run to sync, or the run file is unreadable / not a valid RunResult. (exit 2) */
|
|
226
|
+
declare class SyncFileError extends Error {
|
|
227
|
+
constructor(message: string);
|
|
228
|
+
}
|
|
229
|
+
/** The API was unreachable (DNS / TLS / offline). Message names the host, never the token. (exit 1) */
|
|
230
|
+
declare class SyncNetworkError extends Error {
|
|
231
|
+
constructor(apiUrl: string);
|
|
232
|
+
}
|
|
233
|
+
/** The server answered with a non-2xx that is not a 401 (e.g. 400 invalid body, 5xx). (exit 1) */
|
|
234
|
+
declare class SyncHttpError extends Error {
|
|
235
|
+
readonly status: number;
|
|
236
|
+
constructor(status: number, detail: string);
|
|
237
|
+
}
|
|
238
|
+
interface SyncDeps {
|
|
239
|
+
/** Base dir the default run file is read from (config dir or cwd). Defaults to `process.cwd()`. */
|
|
240
|
+
cwd?: string;
|
|
241
|
+
/** Explicit run JSON to sync instead of `<cwd>/.plune/last-run.json`. */
|
|
242
|
+
file?: string;
|
|
243
|
+
/** Override the API base URL. Falls back to `PLUNE_API_URL`, then the beta default. */
|
|
244
|
+
apiUrl?: string;
|
|
245
|
+
/** Token source (injected in tests). Defaults to the real credentials store. */
|
|
246
|
+
loadToken?: () => string | null;
|
|
247
|
+
/** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
|
|
248
|
+
fetchImpl?: typeof fetch;
|
|
249
|
+
}
|
|
250
|
+
interface SyncResult {
|
|
251
|
+
/** Server-assigned run id. */
|
|
252
|
+
id: string;
|
|
253
|
+
/** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
|
|
254
|
+
url: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
|
258
|
+
* Returns the server-assigned id + the read-back URL. Throws a typed error (see the classes above) on
|
|
259
|
+
* every failure path so the CLI can exit cleanly without ever leaking the token or a stack trace.
|
|
260
|
+
*/
|
|
261
|
+
declare function handleSync(deps?: SyncDeps): Promise<SyncResult>;
|
|
262
|
+
/**
|
|
263
|
+
* Map a `handleSync` rejection to a friendly stderr line + the exit code to use, or `null` if the error
|
|
264
|
+
* is not a known sync failure (caller then treats it as unexpected).
|
|
265
|
+
*/
|
|
266
|
+
declare function reportSyncFailure(err: unknown, write: (s: string) => void): number | null;
|
|
267
|
+
|
|
217
268
|
declare const assertionConfigSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
218
269
|
type: z.ZodLiteral<"exact-match">;
|
|
219
270
|
value: z.ZodString;
|
|
@@ -748,4 +799,70 @@ type BinaryVerdict = z.infer<typeof binaryVerdictSchema>;
|
|
|
748
799
|
/** Parse + validate an unknown value as a RunResult. Throws ZodError on failure. */
|
|
749
800
|
declare function parseRunResult(value: unknown): ParsedRunResult;
|
|
750
801
|
|
|
751
|
-
|
|
802
|
+
interface IngestDeps {
|
|
803
|
+
/** The Cairn run directory to read. */
|
|
804
|
+
dir: string;
|
|
805
|
+
apiUrl?: string;
|
|
806
|
+
loadToken?: () => string | null;
|
|
807
|
+
fetchImpl?: typeof fetch;
|
|
808
|
+
}
|
|
809
|
+
/** What the server recorded — the three counters that account for every case sent. */
|
|
810
|
+
interface IngestResult {
|
|
811
|
+
runId: string;
|
|
812
|
+
linked: number;
|
|
813
|
+
proposed: number;
|
|
814
|
+
skipped: number;
|
|
815
|
+
}
|
|
816
|
+
declare function handleIngest(deps: IngestDeps): Promise<IngestResult>;
|
|
817
|
+
/**
|
|
818
|
+
* Map an ingest failure to a stderr line and an exit code, or `null` when it is not ours to explain.
|
|
819
|
+
*
|
|
820
|
+
* The split follows `sync`: something the user can fix here and now exits 2, something that may work
|
|
821
|
+
* on the next attempt exits 1. A malformed or unfinished run is the user's to fix, so it is 2.
|
|
822
|
+
*/
|
|
823
|
+
declare function reportIngestFailure(err: unknown, write: (s: string) => void): number | null;
|
|
824
|
+
/** One line per counter, in the order a reader cares about: what landed, what needs them, what did not. */
|
|
825
|
+
declare function formatIngestResult(r: IngestResult): string;
|
|
826
|
+
|
|
827
|
+
/** The payload version the Plune ingest endpoint validates — ours, not Cairn's. */
|
|
828
|
+
declare const INGEST_SCHEMA_VERSION = 1;
|
|
829
|
+
type CairnRunMode = 'explore' | 'design' | 'api';
|
|
830
|
+
type CairnExecution = 'auto' | 'manual';
|
|
831
|
+
type CairnVerdict = 'passed' | 'failed';
|
|
832
|
+
interface CairnIngestCase {
|
|
833
|
+
stableId: string;
|
|
834
|
+
title: string;
|
|
835
|
+
execution: CairnExecution;
|
|
836
|
+
steps: string[];
|
|
837
|
+
expected: string;
|
|
838
|
+
/** Absent means the run never executed this case — NOT "no result yet". */
|
|
839
|
+
verdict?: CairnVerdict;
|
|
840
|
+
technique?: string;
|
|
841
|
+
priority?: string;
|
|
842
|
+
}
|
|
843
|
+
interface CairnIngestPayload {
|
|
844
|
+
schemaVersion: typeof INGEST_SCHEMA_VERSION;
|
|
845
|
+
run: {
|
|
846
|
+
runId: string;
|
|
847
|
+
mode: CairnRunMode;
|
|
848
|
+
target: string;
|
|
849
|
+
};
|
|
850
|
+
cases: CairnIngestCase[];
|
|
851
|
+
}
|
|
852
|
+
interface EvidenceRecord {
|
|
853
|
+
name: string;
|
|
854
|
+
passed: boolean;
|
|
855
|
+
}
|
|
856
|
+
interface ReadCairnRunDeps {
|
|
857
|
+
/** Injected for tests; returns `undefined` when the run executed nothing and wrote no evidence. */
|
|
858
|
+
readEvidence?: (dir: string) => Promise<EvidenceRecord[] | undefined>;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Read a Cairn run directory into an upload payload, or throw a typed error explaining why not.
|
|
862
|
+
*
|
|
863
|
+
* Pure apart from the two file reads, which are injectable, so the mapping rules can be exercised
|
|
864
|
+
* against real artifacts without a filesystem dance.
|
|
865
|
+
*/
|
|
866
|
+
declare function readCairnRun(dir: string, deps?: ReadCairnRunDeps): Promise<CairnIngestPayload>;
|
|
867
|
+
|
|
868
|
+
export { type AssertionResultRecord, type BinaryVerdict, type CairnIngestCase, type CairnIngestPayload, type Config, type EvalResult, type IngestDeps, type IngestResult, NotLoggedInError, type ParsedRunResult, RUN_RESULT_SCHEMA_VERSION, type RowResult, type RunError, type RunOptions, type RunResult, type Summary, type SyncDeps, SyncFileError, SyncHttpError, SyncNetworkError, type SyncResult, TokenRejectedError, type Usage, assertionConfigSchema, assertionResultRecordSchema, binaryVerdictSchema, formatIngestResult, handleIngest as ingest, parseRunResult, readCairnRun, reportIngestFailure, reportSyncFailure, handleRun as run, runResultSchema, handleSync as sync };
|
package/dist/index.d.ts
CHANGED
|
@@ -214,6 +214,57 @@ interface RunOptions {
|
|
|
214
214
|
}
|
|
215
215
|
declare function handleRun(options: RunOptions, depsFactory?: (config: Config, baseDir: string) => RunDeps): Promise<RunResult>;
|
|
216
216
|
|
|
217
|
+
/** No saved token — the user must `plune login` before syncing. (exit 2) */
|
|
218
|
+
declare class NotLoggedInError extends Error {
|
|
219
|
+
constructor();
|
|
220
|
+
}
|
|
221
|
+
/** The token was present but the server rejected it (401 — revoked or invalid). (exit 2) */
|
|
222
|
+
declare class TokenRejectedError extends Error {
|
|
223
|
+
constructor();
|
|
224
|
+
}
|
|
225
|
+
/** No run to sync, or the run file is unreadable / not a valid RunResult. (exit 2) */
|
|
226
|
+
declare class SyncFileError extends Error {
|
|
227
|
+
constructor(message: string);
|
|
228
|
+
}
|
|
229
|
+
/** The API was unreachable (DNS / TLS / offline). Message names the host, never the token. (exit 1) */
|
|
230
|
+
declare class SyncNetworkError extends Error {
|
|
231
|
+
constructor(apiUrl: string);
|
|
232
|
+
}
|
|
233
|
+
/** The server answered with a non-2xx that is not a 401 (e.g. 400 invalid body, 5xx). (exit 1) */
|
|
234
|
+
declare class SyncHttpError extends Error {
|
|
235
|
+
readonly status: number;
|
|
236
|
+
constructor(status: number, detail: string);
|
|
237
|
+
}
|
|
238
|
+
interface SyncDeps {
|
|
239
|
+
/** Base dir the default run file is read from (config dir or cwd). Defaults to `process.cwd()`. */
|
|
240
|
+
cwd?: string;
|
|
241
|
+
/** Explicit run JSON to sync instead of `<cwd>/.plune/last-run.json`. */
|
|
242
|
+
file?: string;
|
|
243
|
+
/** Override the API base URL. Falls back to `PLUNE_API_URL`, then the beta default. */
|
|
244
|
+
apiUrl?: string;
|
|
245
|
+
/** Token source (injected in tests). Defaults to the real credentials store. */
|
|
246
|
+
loadToken?: () => string | null;
|
|
247
|
+
/** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
|
|
248
|
+
fetchImpl?: typeof fetch;
|
|
249
|
+
}
|
|
250
|
+
interface SyncResult {
|
|
251
|
+
/** Server-assigned run id. */
|
|
252
|
+
id: string;
|
|
253
|
+
/** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
|
|
254
|
+
url: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
|
258
|
+
* Returns the server-assigned id + the read-back URL. Throws a typed error (see the classes above) on
|
|
259
|
+
* every failure path so the CLI can exit cleanly without ever leaking the token or a stack trace.
|
|
260
|
+
*/
|
|
261
|
+
declare function handleSync(deps?: SyncDeps): Promise<SyncResult>;
|
|
262
|
+
/**
|
|
263
|
+
* Map a `handleSync` rejection to a friendly stderr line + the exit code to use, or `null` if the error
|
|
264
|
+
* is not a known sync failure (caller then treats it as unexpected).
|
|
265
|
+
*/
|
|
266
|
+
declare function reportSyncFailure(err: unknown, write: (s: string) => void): number | null;
|
|
267
|
+
|
|
217
268
|
declare const assertionConfigSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
218
269
|
type: z.ZodLiteral<"exact-match">;
|
|
219
270
|
value: z.ZodString;
|
|
@@ -748,4 +799,70 @@ type BinaryVerdict = z.infer<typeof binaryVerdictSchema>;
|
|
|
748
799
|
/** Parse + validate an unknown value as a RunResult. Throws ZodError on failure. */
|
|
749
800
|
declare function parseRunResult(value: unknown): ParsedRunResult;
|
|
750
801
|
|
|
751
|
-
|
|
802
|
+
interface IngestDeps {
|
|
803
|
+
/** The Cairn run directory to read. */
|
|
804
|
+
dir: string;
|
|
805
|
+
apiUrl?: string;
|
|
806
|
+
loadToken?: () => string | null;
|
|
807
|
+
fetchImpl?: typeof fetch;
|
|
808
|
+
}
|
|
809
|
+
/** What the server recorded — the three counters that account for every case sent. */
|
|
810
|
+
interface IngestResult {
|
|
811
|
+
runId: string;
|
|
812
|
+
linked: number;
|
|
813
|
+
proposed: number;
|
|
814
|
+
skipped: number;
|
|
815
|
+
}
|
|
816
|
+
declare function handleIngest(deps: IngestDeps): Promise<IngestResult>;
|
|
817
|
+
/**
|
|
818
|
+
* Map an ingest failure to a stderr line and an exit code, or `null` when it is not ours to explain.
|
|
819
|
+
*
|
|
820
|
+
* The split follows `sync`: something the user can fix here and now exits 2, something that may work
|
|
821
|
+
* on the next attempt exits 1. A malformed or unfinished run is the user's to fix, so it is 2.
|
|
822
|
+
*/
|
|
823
|
+
declare function reportIngestFailure(err: unknown, write: (s: string) => void): number | null;
|
|
824
|
+
/** One line per counter, in the order a reader cares about: what landed, what needs them, what did not. */
|
|
825
|
+
declare function formatIngestResult(r: IngestResult): string;
|
|
826
|
+
|
|
827
|
+
/** The payload version the Plune ingest endpoint validates — ours, not Cairn's. */
|
|
828
|
+
declare const INGEST_SCHEMA_VERSION = 1;
|
|
829
|
+
type CairnRunMode = 'explore' | 'design' | 'api';
|
|
830
|
+
type CairnExecution = 'auto' | 'manual';
|
|
831
|
+
type CairnVerdict = 'passed' | 'failed';
|
|
832
|
+
interface CairnIngestCase {
|
|
833
|
+
stableId: string;
|
|
834
|
+
title: string;
|
|
835
|
+
execution: CairnExecution;
|
|
836
|
+
steps: string[];
|
|
837
|
+
expected: string;
|
|
838
|
+
/** Absent means the run never executed this case — NOT "no result yet". */
|
|
839
|
+
verdict?: CairnVerdict;
|
|
840
|
+
technique?: string;
|
|
841
|
+
priority?: string;
|
|
842
|
+
}
|
|
843
|
+
interface CairnIngestPayload {
|
|
844
|
+
schemaVersion: typeof INGEST_SCHEMA_VERSION;
|
|
845
|
+
run: {
|
|
846
|
+
runId: string;
|
|
847
|
+
mode: CairnRunMode;
|
|
848
|
+
target: string;
|
|
849
|
+
};
|
|
850
|
+
cases: CairnIngestCase[];
|
|
851
|
+
}
|
|
852
|
+
interface EvidenceRecord {
|
|
853
|
+
name: string;
|
|
854
|
+
passed: boolean;
|
|
855
|
+
}
|
|
856
|
+
interface ReadCairnRunDeps {
|
|
857
|
+
/** Injected for tests; returns `undefined` when the run executed nothing and wrote no evidence. */
|
|
858
|
+
readEvidence?: (dir: string) => Promise<EvidenceRecord[] | undefined>;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Read a Cairn run directory into an upload payload, or throw a typed error explaining why not.
|
|
862
|
+
*
|
|
863
|
+
* Pure apart from the two file reads, which are injectable, so the mapping rules can be exercised
|
|
864
|
+
* against real artifacts without a filesystem dance.
|
|
865
|
+
*/
|
|
866
|
+
declare function readCairnRun(dir: string, deps?: ReadCairnRunDeps): Promise<CairnIngestPayload>;
|
|
867
|
+
|
|
868
|
+
export { type AssertionResultRecord, type BinaryVerdict, type CairnIngestCase, type CairnIngestPayload, type Config, type EvalResult, type IngestDeps, type IngestResult, NotLoggedInError, type ParsedRunResult, RUN_RESULT_SCHEMA_VERSION, type RowResult, type RunError, type RunOptions, type RunResult, type Summary, type SyncDeps, SyncFileError, SyncHttpError, SyncNetworkError, type SyncResult, TokenRejectedError, type Usage, assertionConfigSchema, assertionResultRecordSchema, binaryVerdictSchema, formatIngestResult, handleIngest as ingest, parseRunResult, readCairnRun, reportIngestFailure, reportSyncFailure, handleRun as run, runResultSchema, handleSync as sync };
|