@plune-ai/cli 0.29.0 → 0.30.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
@@ -121,7 +121,7 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
121
121
  | `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
122
122
  | `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` → Account → 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. |
123
123
  | `plune logout` | Remove the saved token. |
124
- | `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. |
124
+ | `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. Against a platform with `POST /v1/jobs` the upload is also reported as a job (kind `cli-sync`) that ends failed with the error text if the sync fails; an older platform without the route is not told, and a job report never fails the sync. |
125
125
  | `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected from the file when omitted; a folder is Allure), `--name <text>` to call the run something other than the [folder-based default](#what-the-run-is-called), `--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, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way), `--upload-concurrency <n>` for files uploaded at once ([1 to 32, default 16](#many-screenshots-upload-concurrency-and-retries)), `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze when this import finished the run. |
126
126
  | `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--name <text>` to call the run something other than the [default](#what-the-run-is-called), `--json` for one machine-readable line. |
127
127
  | `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze. |
package/dist/cli.cjs CHANGED
@@ -4367,30 +4367,46 @@ async function handleSync(deps = {}) {
4367
4367
  const endpoint = `${apiUrl}/v1/runs`;
4368
4368
  const payload = JSON.stringify(run);
4369
4369
  const idempotencyKey = (0, import_node_crypto4.createHash)("sha256").update(payload).digest("hex");
4370
- let res;
4370
+ const bearer = token;
4371
+ const job = new JobReporter(doFetch, apiUrl, bearer);
4372
+ await job.open(path9.basename(deps.file !== void 0 ? path9.resolve(cwd, deps.file) : cwd));
4371
4373
  try {
4372
- res = await doFetch(endpoint, {
4373
- method: "POST",
4374
- headers: {
4375
- "content-type": "application/json",
4376
- authorization: `Bearer ${token}`,
4377
- "idempotency-key": idempotencyKey
4378
- },
4379
- body: payload
4380
- });
4381
- } catch {
4382
- throw new SyncNetworkError(apiUrl);
4374
+ const result = await upload();
4375
+ await job.finish({ ok: true });
4376
+ return result;
4377
+ } catch (err) {
4378
+ await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
4379
+ throw err;
4383
4380
  }
4384
- if (!res.ok) {
4385
- const detail = await safeErrorDetail(res);
4386
- if (res.status === 401) throw new TokenRejectedError();
4387
- throw new SyncHttpError(res.status, detail);
4381
+ async function upload() {
4382
+ let res;
4383
+ try {
4384
+ res = await doFetch(endpoint, {
4385
+ method: "POST",
4386
+ headers: {
4387
+ "content-type": "application/json",
4388
+ authorization: `Bearer ${bearer}`,
4389
+ "idempotency-key": idempotencyKey
4390
+ },
4391
+ body: payload
4392
+ });
4393
+ } catch {
4394
+ throw new SyncNetworkError(apiUrl);
4395
+ }
4396
+ if (!res.ok) {
4397
+ const detail = await safeErrorDetail(res);
4398
+ if (res.status === 401) throw new TokenRejectedError();
4399
+ throw new SyncHttpError(res.status, detail);
4400
+ }
4401
+ const body = await res.json().catch(() => ({}));
4402
+ const id = typeof body.id === "string" ? body.id : "";
4403
+ const result = { id, url: `${endpoint}/${id}` };
4404
+ await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
4405
+ if (deps.failOnFailures === true) {
4406
+ result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
4407
+ }
4408
+ return result;
4388
4409
  }
4389
- const body = await res.json().catch(() => ({}));
4390
- const id = typeof body.id === "string" ? body.id : "";
4391
- const result = { id, url: `${endpoint}/${id}` };
4392
- if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
4393
- return result;
4394
4410
  }
4395
4411
  async function readVerdict(doFetch, url, token) {
4396
4412
  try {
@@ -4412,7 +4428,7 @@ function reportSyncFailure(err, write) {
4412
4428
  }
4413
4429
  return null;
4414
4430
  }
4415
- var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
4431
+ var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError, JOB_KIND, JOB_TIMEOUT_MS, JobReporter;
4416
4432
  var init_sync = __esm({
4417
4433
  "src/cli/commands/sync.ts"() {
4418
4434
  "use strict";
@@ -4460,6 +4476,58 @@ var init_sync = __esm({
4460
4476
  }
4461
4477
  status;
4462
4478
  };
4479
+ JOB_KIND = "cli-sync";
4480
+ JOB_TIMEOUT_MS = 5e3;
4481
+ JobReporter = class {
4482
+ constructor(doFetch, base, token) {
4483
+ this.doFetch = doFetch;
4484
+ this.base = base;
4485
+ this.token = token;
4486
+ }
4487
+ doFetch;
4488
+ base;
4489
+ token;
4490
+ id;
4491
+ async send(method, url, body) {
4492
+ try {
4493
+ const res = await this.doFetch(url, {
4494
+ method,
4495
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
4496
+ body: JSON.stringify(body),
4497
+ signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
4498
+ });
4499
+ const json = await res.json().catch(() => void 0);
4500
+ return res.ok ? json ?? {} : void 0;
4501
+ } catch {
4502
+ return void 0;
4503
+ }
4504
+ }
4505
+ async open(subjectName) {
4506
+ const body = await this.send("POST", `${this.base}/v1/jobs`, {
4507
+ kind: JOB_KIND,
4508
+ subjectName,
4509
+ progress: 0
4510
+ });
4511
+ if (typeof body?.id === "string") this.id = body.id;
4512
+ }
4513
+ async report(progress, cursor) {
4514
+ if (this.id === void 0) return;
4515
+ await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
4516
+ progress,
4517
+ ...cursor !== void 0 ? { cursor } : {}
4518
+ });
4519
+ }
4520
+ async finish(outcome) {
4521
+ if (this.id === void 0) return;
4522
+ const id = this.id;
4523
+ this.id = void 0;
4524
+ await this.send(
4525
+ "PATCH",
4526
+ `${this.base}/v1/jobs/${encodeURIComponent(id)}`,
4527
+ outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
4528
+ );
4529
+ }
4530
+ };
4463
4531
  }
4464
4532
  });
4465
4533
 
package/dist/index.cjs CHANGED
@@ -1892,6 +1892,58 @@ var SyncHttpError = class extends Error {
1892
1892
  }
1893
1893
  status;
1894
1894
  };
1895
+ var JOB_KIND = "cli-sync";
1896
+ var JOB_TIMEOUT_MS = 5e3;
1897
+ var JobReporter = class {
1898
+ constructor(doFetch, base, token) {
1899
+ this.doFetch = doFetch;
1900
+ this.base = base;
1901
+ this.token = token;
1902
+ }
1903
+ doFetch;
1904
+ base;
1905
+ token;
1906
+ id;
1907
+ async send(method, url, body) {
1908
+ try {
1909
+ const res = await this.doFetch(url, {
1910
+ method,
1911
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
1912
+ body: JSON.stringify(body),
1913
+ signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
1914
+ });
1915
+ const json = await res.json().catch(() => void 0);
1916
+ return res.ok ? json ?? {} : void 0;
1917
+ } catch {
1918
+ return void 0;
1919
+ }
1920
+ }
1921
+ async open(subjectName) {
1922
+ const body = await this.send("POST", `${this.base}/v1/jobs`, {
1923
+ kind: JOB_KIND,
1924
+ subjectName,
1925
+ progress: 0
1926
+ });
1927
+ if (typeof body?.id === "string") this.id = body.id;
1928
+ }
1929
+ async report(progress, cursor) {
1930
+ if (this.id === void 0) return;
1931
+ await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
1932
+ progress,
1933
+ ...cursor !== void 0 ? { cursor } : {}
1934
+ });
1935
+ }
1936
+ async finish(outcome) {
1937
+ if (this.id === void 0) return;
1938
+ const id = this.id;
1939
+ this.id = void 0;
1940
+ await this.send(
1941
+ "PATCH",
1942
+ `${this.base}/v1/jobs/${encodeURIComponent(id)}`,
1943
+ outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
1944
+ );
1945
+ }
1946
+ };
1895
1947
  function readRun(cwd, file) {
1896
1948
  const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
1897
1949
  let raw;
@@ -1932,30 +1984,46 @@ async function handleSync(deps = {}) {
1932
1984
  const endpoint = `${apiUrl}/v1/runs`;
1933
1985
  const payload = JSON.stringify(run);
1934
1986
  const idempotencyKey = (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex");
1935
- let res;
1987
+ const bearer = token;
1988
+ const job = new JobReporter(doFetch, apiUrl, bearer);
1989
+ await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
1936
1990
  try {
1937
- res = await doFetch(endpoint, {
1938
- method: "POST",
1939
- headers: {
1940
- "content-type": "application/json",
1941
- authorization: `Bearer ${token}`,
1942
- "idempotency-key": idempotencyKey
1943
- },
1944
- body: payload
1945
- });
1946
- } catch {
1947
- throw new SyncNetworkError(apiUrl);
1991
+ const result = await upload();
1992
+ await job.finish({ ok: true });
1993
+ return result;
1994
+ } catch (err) {
1995
+ await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
1996
+ throw err;
1948
1997
  }
1949
- if (!res.ok) {
1950
- const detail = await safeErrorDetail(res);
1951
- if (res.status === 401) throw new TokenRejectedError();
1952
- throw new SyncHttpError(res.status, detail);
1998
+ async function upload() {
1999
+ let res;
2000
+ try {
2001
+ res = await doFetch(endpoint, {
2002
+ method: "POST",
2003
+ headers: {
2004
+ "content-type": "application/json",
2005
+ authorization: `Bearer ${bearer}`,
2006
+ "idempotency-key": idempotencyKey
2007
+ },
2008
+ body: payload
2009
+ });
2010
+ } catch {
2011
+ throw new SyncNetworkError(apiUrl);
2012
+ }
2013
+ if (!res.ok) {
2014
+ const detail = await safeErrorDetail(res);
2015
+ if (res.status === 401) throw new TokenRejectedError();
2016
+ throw new SyncHttpError(res.status, detail);
2017
+ }
2018
+ const body = await res.json().catch(() => ({}));
2019
+ const id = typeof body.id === "string" ? body.id : "";
2020
+ const result = { id, url: `${endpoint}/${id}` };
2021
+ await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
2022
+ if (deps.failOnFailures === true) {
2023
+ result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
2024
+ }
2025
+ return result;
1953
2026
  }
1954
- const body = await res.json().catch(() => ({}));
1955
- const id = typeof body.id === "string" ? body.id : "";
1956
- const result = { id, url: `${endpoint}/${id}` };
1957
- if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
1958
- return result;
1959
2027
  }
1960
2028
  async function readVerdict(doFetch, url, token) {
1961
2029
  try {
package/dist/index.js CHANGED
@@ -1832,6 +1832,58 @@ var SyncHttpError = class extends Error {
1832
1832
  }
1833
1833
  status;
1834
1834
  };
1835
+ var JOB_KIND = "cli-sync";
1836
+ var JOB_TIMEOUT_MS = 5e3;
1837
+ var JobReporter = class {
1838
+ constructor(doFetch, base, token) {
1839
+ this.doFetch = doFetch;
1840
+ this.base = base;
1841
+ this.token = token;
1842
+ }
1843
+ doFetch;
1844
+ base;
1845
+ token;
1846
+ id;
1847
+ async send(method, url, body) {
1848
+ try {
1849
+ const res = await this.doFetch(url, {
1850
+ method,
1851
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
1852
+ body: JSON.stringify(body),
1853
+ signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
1854
+ });
1855
+ const json = await res.json().catch(() => void 0);
1856
+ return res.ok ? json ?? {} : void 0;
1857
+ } catch {
1858
+ return void 0;
1859
+ }
1860
+ }
1861
+ async open(subjectName) {
1862
+ const body = await this.send("POST", `${this.base}/v1/jobs`, {
1863
+ kind: JOB_KIND,
1864
+ subjectName,
1865
+ progress: 0
1866
+ });
1867
+ if (typeof body?.id === "string") this.id = body.id;
1868
+ }
1869
+ async report(progress, cursor) {
1870
+ if (this.id === void 0) return;
1871
+ await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
1872
+ progress,
1873
+ ...cursor !== void 0 ? { cursor } : {}
1874
+ });
1875
+ }
1876
+ async finish(outcome) {
1877
+ if (this.id === void 0) return;
1878
+ const id = this.id;
1879
+ this.id = void 0;
1880
+ await this.send(
1881
+ "PATCH",
1882
+ `${this.base}/v1/jobs/${encodeURIComponent(id)}`,
1883
+ outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
1884
+ );
1885
+ }
1886
+ };
1835
1887
  function readRun(cwd, file) {
1836
1888
  const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
1837
1889
  let raw;
@@ -1872,30 +1924,46 @@ async function handleSync(deps = {}) {
1872
1924
  const endpoint = `${apiUrl}/v1/runs`;
1873
1925
  const payload = JSON.stringify(run);
1874
1926
  const idempotencyKey = createHash2("sha256").update(payload).digest("hex");
1875
- let res;
1927
+ const bearer = token;
1928
+ const job = new JobReporter(doFetch, apiUrl, bearer);
1929
+ await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
1876
1930
  try {
1877
- res = await doFetch(endpoint, {
1878
- method: "POST",
1879
- headers: {
1880
- "content-type": "application/json",
1881
- authorization: `Bearer ${token}`,
1882
- "idempotency-key": idempotencyKey
1883
- },
1884
- body: payload
1885
- });
1886
- } catch {
1887
- throw new SyncNetworkError(apiUrl);
1931
+ const result = await upload();
1932
+ await job.finish({ ok: true });
1933
+ return result;
1934
+ } catch (err) {
1935
+ await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
1936
+ throw err;
1888
1937
  }
1889
- if (!res.ok) {
1890
- const detail = await safeErrorDetail(res);
1891
- if (res.status === 401) throw new TokenRejectedError();
1892
- throw new SyncHttpError(res.status, detail);
1938
+ async function upload() {
1939
+ let res;
1940
+ try {
1941
+ res = await doFetch(endpoint, {
1942
+ method: "POST",
1943
+ headers: {
1944
+ "content-type": "application/json",
1945
+ authorization: `Bearer ${bearer}`,
1946
+ "idempotency-key": idempotencyKey
1947
+ },
1948
+ body: payload
1949
+ });
1950
+ } catch {
1951
+ throw new SyncNetworkError(apiUrl);
1952
+ }
1953
+ if (!res.ok) {
1954
+ const detail = await safeErrorDetail(res);
1955
+ if (res.status === 401) throw new TokenRejectedError();
1956
+ throw new SyncHttpError(res.status, detail);
1957
+ }
1958
+ const body = await res.json().catch(() => ({}));
1959
+ const id = typeof body.id === "string" ? body.id : "";
1960
+ const result = { id, url: `${endpoint}/${id}` };
1961
+ await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
1962
+ if (deps.failOnFailures === true) {
1963
+ result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
1964
+ }
1965
+ return result;
1893
1966
  }
1894
- const body = await res.json().catch(() => ({}));
1895
- const id = typeof body.id === "string" ? body.id : "";
1896
- const result = { id, url: `${endpoint}/${id}` };
1897
- if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
1898
- return result;
1899
1967
  }
1900
1968
  async function readVerdict(doFetch, url, token) {
1901
1969
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.29.0",
3
+ "version": "0.30.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",