@plune-ai/cli 0.12.0 → 0.13.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 +3 -2
- package/dist/cli.cjs +90 -2
- package/dist/index.cjs +4 -2
- package/dist/index.js +4 -2
- package/dist/reporter-core.cjs +2 -0
- package/dist/reporter-core.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ provider API key is read from the environment based on `provider.type`:
|
|
|
100
100
|
| `plune report` | Re-render the most recent run. Flags: `--format`, `-o`. |
|
|
101
101
|
| `plune diff <baseline> <current>` | Compare two `plune run --format json` outputs and report pass→fail regressions. Flags: `--fail-on-regression`, `--format`, `-o`. |
|
|
102
102
|
| `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
|
|
103
|
-
| `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` and `ingest` can reach it. The token is **checked against the API before it is saved**, so a wrong one fails here rather than two commands later. Get one at `https://beta.plune.ai` → Settings → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). |
|
|
103
|
+
| `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` and `ingest` can reach it. The token is **checked against the API before it is saved**, so a wrong one fails here rather than two commands later. Get one at `https://beta.plune.ai` → Settings → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). In CI, skip this step: every platform command reads `PLUNE_TOKEN` from the environment first, and the saved login only when it is not set. |
|
|
104
104
|
| `plune logout` | Remove the saved token. |
|
|
105
105
|
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON. |
|
|
106
106
|
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json` (detected from the file when omitted), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue. |
|
|
@@ -112,6 +112,7 @@ provider API key is read from the environment based on `provider.type`:
|
|
|
112
112
|
| `plune ingest [dir]` | Record a [Cairn](https://github.com/plune-ai/cairn) run in Plune. Omit `[dir]` for the newest run under `./runs`, or name the directory holding `report.json`. Generated cases arrive as **review proposals** — nothing is created until a person approves it. |
|
|
113
113
|
| `plune pull [file]` | Write the project's test cases as **one Markdown document** — Testomat's classical format, plus Plune's own columns — to `plune/cases.md` (or `[file]`). Refuses to overwrite a file git sees as modified; `--force` overrides, `--suite <id>` takes one suite or folder and what is under it. |
|
|
114
114
|
| `plune push [file] [--dry-run]` | Send the document back. Cases are matched by `id`; a block without one becomes a draft; an unknown id is refused by line; **nothing is deleted**. Prints the report — created, updated, unchanged, refused, warnings — and with `--dry-run` writes nothing. Exit `4` when anything was refused. |
|
|
115
|
+
| `plune plan grep <id>` | Print a [test plan](https://docs.plune.ai/platform/plans/) as **one `--grep` pattern** — `npx playwright test --grep "$(plune plan grep <id>)"` or `npx vitest run -t "$(plune plan grep <id>)"` runs exactly what the plan collects. Each case contributes its title path from the `path-title` key the reporter wrote (the file left out, `[ >#]+` between the segments — a space for Playwright, jest, mocha and vitest ≤ 4, ` > ` for vitest 5), escaped, joined with `\|`; a case without one goes in by title. The pattern is all that reaches stdout; the summary — how many cases, how many keyed — goes to stderr. An empty plan prints `(?!)`, which matches nothing, because an empty `--grep` would run everything. |
|
|
115
116
|
|
|
116
117
|
Global flags: `-c, --config <path>` · `-v, --verbose` · `--no-color`.
|
|
117
118
|
|
|
@@ -124,7 +125,7 @@ suite, there is nothing to generate — the results exist and only have to arriv
|
|
|
124
125
|
costs nothing beyond a Plune token.
|
|
125
126
|
|
|
126
127
|
```bash
|
|
127
|
-
plune login
|
|
128
|
+
plune login # once, on your machine; in CI export PLUNE_TOKEN instead
|
|
128
129
|
plune run import ./junit.xml # Jest, Vitest, pytest, PHPUnit, Surefire, Cypress, …
|
|
129
130
|
plune run import ./playwright.json # or Playwright's own JSON report
|
|
130
131
|
```
|
package/dist/cli.cjs
CHANGED
|
@@ -2159,6 +2159,8 @@ function saveToken(token) {
|
|
|
2159
2159
|
return file;
|
|
2160
2160
|
}
|
|
2161
2161
|
function loadToken() {
|
|
2162
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
2163
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
2162
2164
|
try {
|
|
2163
2165
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
2164
2166
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -4029,7 +4031,7 @@ var init_sync = __esm({
|
|
|
4029
4031
|
init_credentials();
|
|
4030
4032
|
NotLoggedInError = class extends Error {
|
|
4031
4033
|
constructor() {
|
|
4032
|
-
super('Not logged in. Run "plune login" first
|
|
4034
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
4033
4035
|
this.name = "NotLoggedInError";
|
|
4034
4036
|
}
|
|
4035
4037
|
};
|
|
@@ -4306,7 +4308,7 @@ var init_ingest = __esm({
|
|
|
4306
4308
|
init_cairn_artifact();
|
|
4307
4309
|
IngestNotLoggedInError = class extends Error {
|
|
4308
4310
|
constructor() {
|
|
4309
|
-
super('Not logged in. Run "plune login" first
|
|
4311
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
4310
4312
|
this.name = "IngestNotLoggedInError";
|
|
4311
4313
|
}
|
|
4312
4314
|
};
|
|
@@ -4472,6 +4474,68 @@ var init_cases = __esm({
|
|
|
4472
4474
|
}
|
|
4473
4475
|
});
|
|
4474
4476
|
|
|
4477
|
+
// src/cli/commands/plan.ts
|
|
4478
|
+
var plan_exports = {};
|
|
4479
|
+
__export(plan_exports, {
|
|
4480
|
+
formatGrep: () => formatGrep,
|
|
4481
|
+
grepFragment: () => grepFragment,
|
|
4482
|
+
handlePlanGrep: () => handlePlanGrep
|
|
4483
|
+
});
|
|
4484
|
+
function grepFragment(c) {
|
|
4485
|
+
const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
|
|
4486
|
+
const segments = pathTitle?.split("#") ?? [c.title];
|
|
4487
|
+
return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
|
|
4488
|
+
}
|
|
4489
|
+
function formatGrep(cases) {
|
|
4490
|
+
if (cases.length === 0) return "(?!)";
|
|
4491
|
+
return cases.map(grepFragment).join("|");
|
|
4492
|
+
}
|
|
4493
|
+
async function safeErrorDetail4(res) {
|
|
4494
|
+
try {
|
|
4495
|
+
const body = await res.json();
|
|
4496
|
+
return typeof body.error === "string" ? body.error : "";
|
|
4497
|
+
} catch {
|
|
4498
|
+
return "";
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
async function handlePlanGrep(deps) {
|
|
4502
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4503
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
4504
|
+
if (token === null) throw new NotLoggedInError();
|
|
4505
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4506
|
+
let res;
|
|
4507
|
+
try {
|
|
4508
|
+
res = await doFetch(`${apiUrl}/v1/plans/${encodeURIComponent(deps.planId)}/cases`, {
|
|
4509
|
+
headers: { authorization: `Bearer ${token}` }
|
|
4510
|
+
});
|
|
4511
|
+
} catch {
|
|
4512
|
+
throw new SyncNetworkError(apiUrl);
|
|
4513
|
+
}
|
|
4514
|
+
if (!res.ok) {
|
|
4515
|
+
const detail = await safeErrorDetail4(res);
|
|
4516
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
4517
|
+
throw new SyncHttpError(res.status, detail);
|
|
4518
|
+
}
|
|
4519
|
+
const { cases } = await res.json();
|
|
4520
|
+
return {
|
|
4521
|
+
pattern: formatGrep(cases),
|
|
4522
|
+
cases: cases.length,
|
|
4523
|
+
keyed: cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length
|
|
4524
|
+
};
|
|
4525
|
+
}
|
|
4526
|
+
var escape, BOUNDARY;
|
|
4527
|
+
var init_plan = __esm({
|
|
4528
|
+
"src/cli/commands/plan.ts"() {
|
|
4529
|
+
"use strict";
|
|
4530
|
+
init_cjs_shims();
|
|
4531
|
+
init_api_url();
|
|
4532
|
+
init_credentials();
|
|
4533
|
+
init_sync();
|
|
4534
|
+
escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4535
|
+
BOUNDARY = "[ >#]+";
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
|
|
4475
4539
|
// src/cli.ts
|
|
4476
4540
|
var cli_exports = {};
|
|
4477
4541
|
__export(cli_exports, {
|
|
@@ -4905,6 +4969,30 @@ function createProgram() {
|
|
|
4905
4969
|
failUnexpected(err, verbose);
|
|
4906
4970
|
}
|
|
4907
4971
|
});
|
|
4972
|
+
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
4973
|
+
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
4974
|
+
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
4975
|
+
).action(async (id, _options, command) => {
|
|
4976
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
4977
|
+
const { handlePlanGrep: handlePlanGrep2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
4978
|
+
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
4979
|
+
try {
|
|
4980
|
+
const out = await handlePlanGrep2({ planId: id });
|
|
4981
|
+
process.stdout.write(out.pattern + "\n");
|
|
4982
|
+
process.stderr.write(
|
|
4983
|
+
out.cases === 0 ? "plune: the plan matches no case \u2014 the pattern matches nothing.\n" : `plune: ${out.cases} case(s) in the plan, ${out.keyed} with a test key; the rest by title.
|
|
4984
|
+
`
|
|
4985
|
+
);
|
|
4986
|
+
} catch (err) {
|
|
4987
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
4988
|
+
if (code !== null) {
|
|
4989
|
+
maybeStack(err, verbose);
|
|
4990
|
+
process.exitCode = code;
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
failUnexpected(err, verbose);
|
|
4994
|
+
}
|
|
4995
|
+
});
|
|
4908
4996
|
program.on("command:*", (operands) => {
|
|
4909
4997
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
4910
4998
|
`);
|
package/dist/index.cjs
CHANGED
|
@@ -1595,6 +1595,8 @@ function credentialsFile() {
|
|
|
1595
1595
|
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
1596
1596
|
}
|
|
1597
1597
|
function loadToken() {
|
|
1598
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
1599
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
1598
1600
|
try {
|
|
1599
1601
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
1600
1602
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -1606,7 +1608,7 @@ function loadToken() {
|
|
|
1606
1608
|
// src/cli/commands/sync.ts
|
|
1607
1609
|
var NotLoggedInError = class extends Error {
|
|
1608
1610
|
constructor() {
|
|
1609
|
-
super('Not logged in. Run "plune login" first
|
|
1611
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1610
1612
|
this.name = "NotLoggedInError";
|
|
1611
1613
|
}
|
|
1612
1614
|
};
|
|
@@ -1862,7 +1864,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1862
1864
|
// src/cli/commands/ingest.ts
|
|
1863
1865
|
var IngestNotLoggedInError = class extends Error {
|
|
1864
1866
|
constructor() {
|
|
1865
|
-
super('Not logged in. Run "plune login" first
|
|
1867
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1866
1868
|
this.name = "IngestNotLoggedInError";
|
|
1867
1869
|
}
|
|
1868
1870
|
};
|
package/dist/index.js
CHANGED
|
@@ -1542,6 +1542,8 @@ function credentialsFile() {
|
|
|
1542
1542
|
return join3(base, "plune", "credentials.json");
|
|
1543
1543
|
}
|
|
1544
1544
|
function loadToken() {
|
|
1545
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
1546
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
1545
1547
|
try {
|
|
1546
1548
|
const parsed = JSON.parse(readFileSync3(credentialsFile(), "utf-8"));
|
|
1547
1549
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -1553,7 +1555,7 @@ function loadToken() {
|
|
|
1553
1555
|
// src/cli/commands/sync.ts
|
|
1554
1556
|
var NotLoggedInError = class extends Error {
|
|
1555
1557
|
constructor() {
|
|
1556
|
-
super('Not logged in. Run "plune login" first
|
|
1558
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1557
1559
|
this.name = "NotLoggedInError";
|
|
1558
1560
|
}
|
|
1559
1561
|
};
|
|
@@ -1809,7 +1811,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1809
1811
|
// src/cli/commands/ingest.ts
|
|
1810
1812
|
var IngestNotLoggedInError = class extends Error {
|
|
1811
1813
|
constructor() {
|
|
1812
|
-
super('Not logged in. Run "plune login" first
|
|
1814
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1813
1815
|
this.name = "IngestNotLoggedInError";
|
|
1814
1816
|
}
|
|
1815
1817
|
};
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -60,6 +60,8 @@ function credentialsFile() {
|
|
|
60
60
|
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
61
61
|
}
|
|
62
62
|
function loadToken() {
|
|
63
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
64
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
63
65
|
try {
|
|
64
66
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
65
67
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
package/dist/reporter-core.js
CHANGED
|
@@ -17,6 +17,8 @@ function credentialsFile() {
|
|
|
17
17
|
return join(base, "plune", "credentials.json");
|
|
18
18
|
}
|
|
19
19
|
function loadToken() {
|
|
20
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
21
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
20
22
|
try {
|
|
21
23
|
const parsed = JSON.parse(readFileSync(credentialsFile(), "utf-8"));
|
|
22
24
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
package/package.json
CHANGED