pagetrace 0.8.0 → 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/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  While the version is below 1.0.0, breaking changes ship in a minor release.
8
8
 
9
+ ## [0.9.0] - 2026-09-07
10
+
11
+ ### Added
12
+
13
+ - `pagetrace update` asks the npm registry whether a newer version is published
14
+ and installs it. `--check` reports without installing. A project-local install
15
+ is not overwritten with a global one; the command prints the package-manager
16
+ command instead, because installing globally would leave the version the
17
+ project actually runs untouched.
18
+
19
+ ### Fixed
20
+
21
+ - A bare `pagetrace`, or a misspelled command, printed nothing and exited 0,
22
+ which reads as a silent success. Both now print the command list, and an
23
+ unknown command exits 2.
24
+
25
+ ## [0.8.1] - 2026-09-06
26
+
27
+ ### Fixed
28
+
29
+ - The report summary said "1 findings across 13 pages". Both counts are now
30
+ pluralised properly.
31
+
9
32
  ## [0.8.0] - 2026-09-06
10
33
 
11
34
  ### Fixed
@@ -249,6 +272,7 @@ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
249
272
  crawling; diff classified by transition; absolute, cross-page and hreflang audit
250
273
  rules; pretty, JSON, markdown, GitHub and HTML reporters.
251
274
 
275
+ [0.8.1]: https://github.com/shyamexe/pagetrace/compare/v0.8.0...v0.8.1
252
276
  [0.8.0]: https://github.com/shyamexe/pagetrace/compare/v0.7.0...v0.8.0
253
277
  [0.7.0]: https://github.com/shyamexe/pagetrace/compare/v0.6.0...v0.7.0
254
278
  [0.6.0]: https://github.com/shyamexe/pagetrace/compare/v0.5.0...v0.6.0
package/README.md CHANGED
@@ -115,6 +115,18 @@ Routes are discovered from `robots.txt` sitemap declarations, falling back to `/
115
115
 
116
116
  URLs pointing at another host are skipped. A page that cannot be fetched stops the run with an error rather than being dropped from the snapshot — a page silently missing from a crawl is indistinguishable from a page you deleted, and reporting a transient outage as a site-wide deletion is worse than failing.
117
117
 
118
+ ### Version and updates
119
+
120
+ ```bash
121
+ pagetrace # command list (same as --help)
122
+ pagetrace <command> --help # flags for one command
123
+ pagetrace --version # installed version
124
+ pagetrace update --check # ask npm whether a newer one exists
125
+ pagetrace update # install it globally
126
+ ```
127
+
128
+ `update` refuses to install globally over a project-local copy and prints the package-manager command instead. Unrelated to `check --update`, which rewrites the lockfile.
129
+
118
130
  ## What it records
119
131
 
120
132
  **Per page** — title, meta description, canonical, robots directives, Open Graph and Twitter Card tags, hreflang alternates, `h1` text, heading outline, every JSON-LD entity with its property list, word count, images missing `alt`, and the length of the first quotable paragraph.
package/dist/cli.cjs CHANGED
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var import_node_child_process2 = require("child_process");
27
28
  var import_promises2 = require("fs/promises");
28
29
  var import_cac = require("cac");
29
30
  var import_picocolors2 = __toESM(require("picocolors"), 1);
@@ -1030,9 +1031,10 @@ function formatAuditPretty(groups, meta, columns = 80) {
1030
1031
  const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(import_picocolors.default.dim(" \xB7 "));
1031
1032
  lines.push(import_picocolors.default.dim("\u2500".repeat(width)));
1032
1033
  lines.push(`${import_picocolors.default.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
1034
+ const found = instances.error + instances.warn + instances.info;
1033
1035
  lines.push(
1034
1036
  import_picocolors.default.dim(
1035
- `${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
1037
+ `${found} finding${found === 1 ? "" : "s"} across ${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`
1036
1038
  )
1037
1039
  );
1038
1040
  lines.push("");
@@ -1515,6 +1517,27 @@ async function snapshotFromOrigin(origin, options = {}) {
1515
1517
  return { schemaVersion: 1, createdAt: (/* @__PURE__ */ new Date()).toISOString(), site, pages };
1516
1518
  }
1517
1519
 
1520
+ // src/update.ts
1521
+ var REGISTRY = "https://registry.npmjs.org";
1522
+ function isNewer(latest, current) {
1523
+ const parts = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
1524
+ const [a, b] = [parts(latest), parts(current)];
1525
+ for (let i = 0; i < 3; i += 1) {
1526
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
1527
+ }
1528
+ return false;
1529
+ }
1530
+ async function latestVersion(name, timeoutMs = 5e3) {
1531
+ const response = await fetch(`${REGISTRY}/${name}/latest`, {
1532
+ headers: { accept: "application/json" },
1533
+ signal: AbortSignal.timeout(timeoutMs)
1534
+ });
1535
+ if (!response.ok) throw new Error(`npm registry answered ${response.status} for ${name}.`);
1536
+ const body = await response.json();
1537
+ if (!body.version) throw new Error(`npm registry returned no version for ${name}.`);
1538
+ return body.version;
1539
+ }
1540
+
1518
1541
  // src/cli.ts
1519
1542
  var DEFAULT_LOCKFILE = "pagetrace.lock.json";
1520
1543
  var DEFAULT_CONFIG = "pagetrace.config.json";
@@ -1644,11 +1667,44 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
1644
1667
  process.exitCode = EXIT_FINDINGS;
1645
1668
  }
1646
1669
  });
1670
+ cli.command("update", "Check npm for a newer pagetrace and install it").option("--check", "Only report whether an update exists").action(async (flags) => {
1671
+ const latest = await latestVersion("pagetrace");
1672
+ if (!isNewer(latest, "0.9.0")) {
1673
+ console.log(import_picocolors2.default.green(`pagetrace ${"0.9.0"} is the latest version.`));
1674
+ return;
1675
+ }
1676
+ console.log(import_picocolors2.default.yellow(`Update available: ${"0.9.0"} \u2192 ${latest}`));
1677
+ if (flags.check) return;
1678
+ if (process.argv[1]?.startsWith(process.cwd())) {
1679
+ console.log(
1680
+ import_picocolors2.default.dim("This is a project-local install. Update it with your package manager, e.g.")
1681
+ );
1682
+ console.log(` npm install -D pagetrace@${latest}`);
1683
+ return;
1684
+ }
1685
+ const { status, error } = (0, import_node_child_process2.spawnSync)("npm", ["install", "-g", `pagetrace@${latest}`], {
1686
+ stdio: "inherit"
1687
+ });
1688
+ if (error || status !== 0) {
1689
+ throw new Error(`npm install failed. Run \`npm install -g pagetrace@${latest}\` yourself.`);
1690
+ }
1691
+ console.log(import_picocolors2.default.green(`Updated to pagetrace ${latest}.`));
1692
+ });
1647
1693
  cli.help();
1648
- cli.version("0.8.0");
1694
+ cli.version("0.9.0");
1649
1695
  async function main() {
1650
1696
  try {
1651
1697
  cli.parse(process.argv, { run: false });
1698
+ if (!cli.matchedCommand) {
1699
+ if (cli.options.help || cli.options.version) return;
1700
+ cli.outputHelp();
1701
+ if (cli.args[0]) {
1702
+ console.error(import_picocolors2.default.red(`
1703
+ Unknown command "${cli.args[0]}".`));
1704
+ process.exitCode = EXIT_FAILURE;
1705
+ }
1706
+ return;
1707
+ }
1652
1708
  await cli.runMatchedCommand();
1653
1709
  } catch (error) {
1654
1710
  console.error(import_picocolors2.default.red(error.message));
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { spawnSync } from "child_process";
4
5
  import { readFile as readFile2, writeFile } from "fs/promises";
5
6
  import { cac } from "cac";
6
7
  import pc2 from "picocolors";
@@ -1007,9 +1008,10 @@ function formatAuditPretty(groups, meta, columns = 80) {
1007
1008
  const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(pc.dim(" \xB7 "));
1008
1009
  lines.push(pc.dim("\u2500".repeat(width)));
1009
1010
  lines.push(`${pc.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
1011
+ const found = instances.error + instances.warn + instances.info;
1010
1012
  lines.push(
1011
1013
  pc.dim(
1012
- `${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
1014
+ `${found} finding${found === 1 ? "" : "s"} across ${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`
1013
1015
  )
1014
1016
  );
1015
1017
  lines.push("");
@@ -1492,6 +1494,27 @@ async function snapshotFromOrigin(origin, options = {}) {
1492
1494
  return { schemaVersion: 1, createdAt: (/* @__PURE__ */ new Date()).toISOString(), site, pages };
1493
1495
  }
1494
1496
 
1497
+ // src/update.ts
1498
+ var REGISTRY = "https://registry.npmjs.org";
1499
+ function isNewer(latest, current) {
1500
+ const parts = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
1501
+ const [a, b] = [parts(latest), parts(current)];
1502
+ for (let i = 0; i < 3; i += 1) {
1503
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
1504
+ }
1505
+ return false;
1506
+ }
1507
+ async function latestVersion(name, timeoutMs = 5e3) {
1508
+ const response = await fetch(`${REGISTRY}/${name}/latest`, {
1509
+ headers: { accept: "application/json" },
1510
+ signal: AbortSignal.timeout(timeoutMs)
1511
+ });
1512
+ if (!response.ok) throw new Error(`npm registry answered ${response.status} for ${name}.`);
1513
+ const body = await response.json();
1514
+ if (!body.version) throw new Error(`npm registry returned no version for ${name}.`);
1515
+ return body.version;
1516
+ }
1517
+
1495
1518
  // src/cli.ts
1496
1519
  var DEFAULT_LOCKFILE = "pagetrace.lock.json";
1497
1520
  var DEFAULT_CONFIG = "pagetrace.config.json";
@@ -1621,11 +1644,44 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
1621
1644
  process.exitCode = EXIT_FINDINGS;
1622
1645
  }
1623
1646
  });
1647
+ cli.command("update", "Check npm for a newer pagetrace and install it").option("--check", "Only report whether an update exists").action(async (flags) => {
1648
+ const latest = await latestVersion("pagetrace");
1649
+ if (!isNewer(latest, "0.9.0")) {
1650
+ console.log(pc2.green(`pagetrace ${"0.9.0"} is the latest version.`));
1651
+ return;
1652
+ }
1653
+ console.log(pc2.yellow(`Update available: ${"0.9.0"} \u2192 ${latest}`));
1654
+ if (flags.check) return;
1655
+ if (process.argv[1]?.startsWith(process.cwd())) {
1656
+ console.log(
1657
+ pc2.dim("This is a project-local install. Update it with your package manager, e.g.")
1658
+ );
1659
+ console.log(` npm install -D pagetrace@${latest}`);
1660
+ return;
1661
+ }
1662
+ const { status, error } = spawnSync("npm", ["install", "-g", `pagetrace@${latest}`], {
1663
+ stdio: "inherit"
1664
+ });
1665
+ if (error || status !== 0) {
1666
+ throw new Error(`npm install failed. Run \`npm install -g pagetrace@${latest}\` yourself.`);
1667
+ }
1668
+ console.log(pc2.green(`Updated to pagetrace ${latest}.`));
1669
+ });
1624
1670
  cli.help();
1625
- cli.version("0.8.0");
1671
+ cli.version("0.9.0");
1626
1672
  async function main() {
1627
1673
  try {
1628
1674
  cli.parse(process.argv, { run: false });
1675
+ if (!cli.matchedCommand) {
1676
+ if (cli.options.help || cli.options.version) return;
1677
+ cli.outputHelp();
1678
+ if (cli.args[0]) {
1679
+ console.error(pc2.red(`
1680
+ Unknown command "${cli.args[0]}".`));
1681
+ process.exitCode = EXIT_FAILURE;
1682
+ }
1683
+ return;
1684
+ }
1629
1685
  await cli.runMatchedCommand();
1630
1686
  } catch (error) {
1631
1687
  console.error(pc2.red(error.message));
package/dist/index.cjs CHANGED
@@ -1275,9 +1275,10 @@ function formatAuditPretty(groups, meta, columns = 80) {
1275
1275
  const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(import_picocolors.default.dim(" \xB7 "));
1276
1276
  lines.push(import_picocolors.default.dim("\u2500".repeat(width)));
1277
1277
  lines.push(`${import_picocolors.default.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
1278
+ const found = instances.error + instances.warn + instances.info;
1278
1279
  lines.push(
1279
1280
  import_picocolors.default.dim(
1280
- `${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
1281
+ `${found} finding${found === 1 ? "" : "s"} across ${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`
1281
1282
  )
1282
1283
  );
1283
1284
  lines.push("");
package/dist/index.js CHANGED
@@ -1204,9 +1204,10 @@ function formatAuditPretty(groups, meta, columns = 80) {
1204
1204
  const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(pc.dim(" \xB7 "));
1205
1205
  lines.push(pc.dim("\u2500".repeat(width)));
1206
1206
  lines.push(`${pc.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
1207
+ const found = instances.error + instances.warn + instances.info;
1207
1208
  lines.push(
1208
1209
  pc.dim(
1209
- `${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
1210
+ `${found} finding${found === 1 ? "" : "s"} across ${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`
1210
1211
  )
1211
1212
  );
1212
1213
  lines.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagetrace",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Baseline your site's SEO and AEO surface, diff every build against it, and fail CI on regressions.",
5
5
  "main": "./dist/index.cjs",
6
6
  "scripts": {