@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 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 to save your API token.');
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 to save your API token.');
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 to save your API token.');
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 to save your API token.');
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 to save your API token.');
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 to save your API token.');
1814
+ super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
1813
1815
  this.name = "IngestNotLoggedInError";
1814
1816
  }
1815
1817
  };
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "AI-powered assertion testing for LLM apps — write assertions, run them against any provider, get a pass/fail report (local, CI, or diff).",
5
5
  "keywords": [
6
6
  "ai",