@olomonhelpdesk2/helpdesk 0.1.1 → 0.1.2

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.
@@ -65,8 +65,8 @@ function actionFor(outcome) {
65
65
  return `${outcome.route} asked for ${outcome.missingEndpoint ?? "data"}, which is not in the recording. Visit this screen while recording, then run this again.`;
66
66
  }
67
67
  }
68
- function subjectOf(path2) {
69
- const segments = path2.split("/").filter(Boolean).filter((segment) => !/^(api|v\d+)$/.test(segment));
68
+ function subjectOf(path3) {
69
+ const segments = path3.split("/").filter(Boolean).filter((segment) => !/^(api|v\d+)$/.test(segment));
70
70
  while (segments.length > 1 && looksLikeAnId(segments[segments.length - 1])) segments.pop();
71
71
  const last = segments[segments.length - 1] ?? "record";
72
72
  return last.replace(/[-_]/g, " ").replace(/s$/, "");
@@ -172,11 +172,11 @@ function canonicalisation(asked, landed) {
172
172
  function samePlace(asked, landed) {
173
173
  return stripQuery(asked) === stripQuery(landed);
174
174
  }
175
- function stripQuery(path2) {
176
- return path2.split(/[?#]/)[0] ?? path2;
175
+ function stripQuery(path3) {
176
+ return path3.split(/[?#]/)[0] ?? path3;
177
177
  }
178
- function normalise(path2) {
179
- return stripQuery(path2).toLowerCase().replace(/\/index(\.html?)?$/, "/").replace(/\/+$/, "").replace(/^$/, "/");
178
+ function normalise(path3) {
179
+ return stripQuery(path3).toLowerCase().replace(/\/index(\.html?)?$/, "/").replace(/\/+$/, "").replace(/^$/, "/");
180
180
  }
181
181
  function firstSentence(text2) {
182
182
  const sentence = text2.trim().split(/(?<=[.!?])\s|\n/)[0] ?? text2;
@@ -304,6 +304,11 @@ function attestation(run) {
304
304
  ].filter(Boolean).join("\n");
305
305
  }
306
306
 
307
+ // adapters/capture-cli.ts
308
+ import { createRequire } from "node:module";
309
+ import path from "node:path";
310
+ import { pathToFileURL } from "node:url";
311
+
307
312
  // core/capture/scrub.ts
308
313
  var BRANDS = [
309
314
  "Chase",
@@ -1991,28 +1996,49 @@ async function captureSignIn(options) {
1991
1996
  await browser.close();
1992
1997
  }
1993
1998
  }
1999
+ async function resolveFrom(directory, specifier) {
2000
+ const resolve = createRequire(path.join(directory, "package.json"));
2001
+ return import(
2002
+ /* webpackIgnore: true */
2003
+ pathToFileURL(resolve.resolve(specifier)).href
2004
+ );
2005
+ }
1994
2006
  async function loadPlaywright() {
1995
2007
  const specifier = "playwright-core";
1996
- try {
1997
- return await import(
2008
+ const given = process.env["CAPTURE_PLAYWRIGHT"];
2009
+ const sources = [
2010
+ ...given ? [() => resolveFrom(given, specifier)] : [],
2011
+ // Beside this file, which covers a local devDependency and a bundled copy.
2012
+ () => import(
1998
2013
  /* webpackIgnore: true */
1999
2014
  specifier
2000
- );
2001
- } catch {
2002
- throw new Error(
2003
- "Capture needs Playwright. Install it beside this command with `npm i -D playwright-core` and make sure a Chrome is available (set CAPTURE_CHROME to its path if it is not on the default one)."
2004
- );
2015
+ ),
2016
+ // From the repository being photographed, which covers an npx invocation
2017
+ // in a project that already has Playwright. Many do: the capture rig
2018
+ // detector exists because so many repositories already drive a browser.
2019
+ () => resolveFrom(process.cwd(), specifier)
2020
+ ];
2021
+ for (const source of sources) {
2022
+ try {
2023
+ const loaded = await source();
2024
+ const chromium = loaded.chromium ?? loaded.default?.chromium;
2025
+ if (chromium) return { chromium };
2026
+ } catch {
2027
+ }
2005
2028
  }
2029
+ throw new Error(
2030
+ "Capture needs Playwright. Install it in the repository you are photographing with `npm i -D playwright-core`, or point CAPTURE_PLAYWRIGHT at a copy, and make sure a Chrome is available (set CAPTURE_CHROME to its path if it is not on the default one)."
2031
+ );
2006
2032
  }
2007
2033
 
2008
2034
  // adapters/invoked-as-command.ts
2009
2035
  import { realpathSync } from "node:fs";
2010
- import { pathToFileURL } from "node:url";
2036
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
2011
2037
  function runningAsCommand(moduleUrl) {
2012
2038
  const invoked = process.argv[1];
2013
2039
  if (!invoked) return false;
2014
2040
  try {
2015
- return moduleUrl === pathToFileURL(realpathSync(invoked)).href;
2041
+ return moduleUrl === pathToFileURL2(realpathSync(invoked)).href;
2016
2042
  } catch {
2017
2043
  return false;
2018
2044
  }
@@ -2045,7 +2071,7 @@ function looksLikeIdentifier(segment) {
2045
2071
  return /\d/.test(segment) && segment.length >= 3;
2046
2072
  }
2047
2073
  function templatise(paths) {
2048
- const split = paths.map((path2) => path2.split("/").filter(Boolean));
2074
+ const split = paths.map((path3) => path3.split("/").filter(Boolean));
2049
2075
  const depth2 = Math.max(0, ...split.map((segments) => segments.length));
2050
2076
  const parameters = /* @__PURE__ */ new Set();
2051
2077
  const prefixOf = (segments, upTo) => {
@@ -2148,9 +2174,9 @@ var CassetteIndex = class {
2148
2174
  return void 0;
2149
2175
  }
2150
2176
  };
2151
- function matchesTemplate(template, path2) {
2177
+ function matchesTemplate(template, path3) {
2152
2178
  const wanted = template.split("/").filter(Boolean);
2153
- const actual = path2.split("/").filter(Boolean);
2179
+ const actual = path3.split("/").filter(Boolean);
2154
2180
  if (wanted.length !== actual.length) return false;
2155
2181
  return wanted.every((segment, index) => segment.startsWith(":") || segment === actual[index]);
2156
2182
  }
@@ -2181,10 +2207,10 @@ function shut(server) {
2181
2207
  });
2182
2208
  }
2183
2209
  function splitUrl(url) {
2184
- const [path2, search] = url.split("?");
2210
+ const [path3, search] = url.split("?");
2185
2211
  const query = {};
2186
2212
  for (const [name, value] of new URLSearchParams(search ?? "")) query[name] = value;
2187
- return { path: path2 ?? "/", query };
2213
+ return { path: path3 ?? "/", query };
2188
2214
  }
2189
2215
  async function replayFrom(options) {
2190
2216
  const index = new CassetteIndex(options.cassette);
@@ -2192,14 +2218,14 @@ async function replayFrom(options) {
2192
2218
  const served = /* @__PURE__ */ new Set();
2193
2219
  const server = createServer((incoming, outgoing) => {
2194
2220
  void (async () => {
2195
- const { path: path2, query } = splitUrl(incoming.url ?? "/");
2221
+ const { path: path3, query } = splitUrl(incoming.url ?? "/");
2196
2222
  await bodyOf(incoming);
2197
- const found = index.find({ method: incoming.method ?? "GET", path: path2, query });
2223
+ const found = index.find({ method: incoming.method ?? "GET", path: path3, query });
2198
2224
  if (!found) {
2199
2225
  const from = incoming.headers["referer"] ?? incoming.headers["x-seed-page"];
2200
2226
  const miss = {
2201
2227
  method: incoming.method ?? "GET",
2202
- path: path2,
2228
+ path: path3,
2203
2229
  ...Object.keys(query).length ? { query } : {},
2204
2230
  ...typeof from === "string" ? { from: pageOf(from) } : {}
2205
2231
  };
@@ -2435,8 +2461,8 @@ function gapsFrom(misses, cassette) {
2435
2461
  const templates = templatise([...known, ...misses.map((miss) => miss.path)]);
2436
2462
  const grouped = /* @__PURE__ */ new Map();
2437
2463
  for (const miss of misses) {
2438
- const path2 = templates.get(miss.path) ?? miss.path;
2439
- const key = interactionKey({ method: miss.method, path: path2, ...miss.query ? { query: miss.query } : {} });
2464
+ const path3 = templates.get(miss.path) ?? miss.path;
2465
+ const key = interactionKey({ method: miss.method, path: path3, ...miss.query ? { query: miss.query } : {} });
2440
2466
  const existing = grouped.get(key);
2441
2467
  if (existing) {
2442
2468
  existing.asked += 1;
@@ -2445,7 +2471,7 @@ function gapsFrom(misses, cassette) {
2445
2471
  }
2446
2472
  grouped.set(key, {
2447
2473
  method: miss.method.toUpperCase(),
2448
- path: path2,
2474
+ path: path3,
2449
2475
  asked: 1,
2450
2476
  broke: miss.from ? [miss.from] : [],
2451
2477
  example: miss.path
@@ -2515,7 +2541,7 @@ function coverageReport(found, total) {
2515
2541
 
2516
2542
  // adapters/capture-bin.ts
2517
2543
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2518
- import path from "node:path";
2544
+ import path2 from "node:path";
2519
2545
 
2520
2546
  // adapters/map-cli.ts
2521
2547
  import { execFileSync } from "node:child_process";
@@ -2528,14 +2554,14 @@ var ROUTE_GROUP = /^\((.+)\)$/;
2528
2554
  var DYNAMIC = /^\[{1,2}(\.{3})?(.+?)\]{1,2}$/;
2529
2555
  var PARALLEL = /^@/;
2530
2556
  var INTERCEPTING = /^\(\.+\)/;
2531
- function classify(path2) {
2532
- if (path2 === "/api" || path2.startsWith("/api/")) {
2557
+ function classify(path3) {
2558
+ if (path3 === "/api" || path3.startsWith("/api/")) {
2533
2559
  return { skipped: "not-html", note: "an API route, not a screen" };
2534
2560
  }
2535
- if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(path2)) {
2561
+ if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(path3)) {
2536
2562
  return { skipped: "internal-tooling", note: "internal tooling" };
2537
2563
  }
2538
- if (/(^|\/)(login|logout|signin|sign-in|signup|sign-up|auth|verify|reset-password)(\/|$)/.test(path2)) {
2564
+ if (/(^|\/)(login|logout|signin|sign-in|signup|sign-up|auth|verify|reset-password)(\/|$)/.test(path3)) {
2539
2565
  return { skipped: "unauthenticated-flow", note: "an authentication step" };
2540
2566
  }
2541
2567
  return void 0;
@@ -2639,8 +2665,8 @@ function enclosingLayouts(pageFile, layouts) {
2639
2665
  const dir = pageFile.slice(0, pageFile.lastIndexOf("/"));
2640
2666
  return layouts.filter((layout) => dir.startsWith(layout.slice(0, layout.lastIndexOf("/")))).sort((a, b) => a.length - b.length);
2641
2667
  }
2642
- function depth(path2) {
2643
- return path2 === "/" ? 0 : path2.split("/").filter(Boolean).length;
2668
+ function depth(path3) {
2669
+ return path3 === "/" ? 0 : path3.split("/").filter(Boolean).length;
2644
2670
  }
2645
2671
 
2646
2672
  // core/discovery/article-plan.ts
@@ -2677,8 +2703,8 @@ var ENTRY_POINT = /* @__PURE__ */ new Set([
2677
2703
  "integration",
2678
2704
  "integrations"
2679
2705
  ]);
2680
- function hasSegment(path2, vocabulary) {
2681
- return path2.split("/").filter(Boolean).some((segment) => vocabulary.has(segment.toLowerCase()));
2706
+ function hasSegment(path3, vocabulary) {
2707
+ return path3.split("/").filter(Boolean).some((segment) => vocabulary.has(segment.toLowerCase()));
2682
2708
  }
2683
2709
  function score(route, signals) {
2684
2710
  const because = [];
@@ -2717,8 +2743,8 @@ function tierFor(rank, count) {
2717
2743
  if (rank <= Math.max(16, Math.ceil(count * 0.6))) return "supporting";
2718
2744
  return "minor";
2719
2745
  }
2720
- function slugFor(path2) {
2721
- const segments = path2.split("/").filter(Boolean).filter((segment) => !segment.startsWith("["));
2746
+ function slugFor(path3) {
2747
+ const segments = path3.split("/").filter(Boolean).filter((segment) => !segment.startsWith("["));
2722
2748
  return segments.length ? segments.join("/") : "overview";
2723
2749
  }
2724
2750
  function titleFor(route) {
@@ -2777,7 +2803,7 @@ function buildArticlePlan(options) {
2777
2803
  let covered = 0;
2778
2804
  const needs = [...merged.entries()].map(([slug, group], index) => {
2779
2805
  const rank = index + 1;
2780
- const claimed = [group.entry.route.path, ...group.routes.slice(1) ?? []].map((path2) => options.documented?.[path2]).find(Boolean);
2806
+ const claimed = [group.entry.route.path, ...group.routes.slice(1) ?? []].map((path3) => options.documented?.[path3]).find(Boolean);
2781
2807
  const already = existing.has(slug) || Boolean(claimed);
2782
2808
  if (already) covered += 1;
2783
2809
  return {
@@ -2821,7 +2847,7 @@ function buildStoryboard(options) {
2821
2847
  const beneath = options.routes.filter(
2822
2848
  (route) => !covered.has(route.path) && !route.skipped && [...covered].some((base) => base !== "/" && route.path.startsWith(`${base}/`))
2823
2849
  );
2824
- const ordered = [...covered].map((path2) => byPath.get(path2)).filter((route) => Boolean(route) && !route.skipped).sort((a, b) => a.path.length - b.path.length).concat(beneath.sort((a, b) => a.path.length - b.path.length));
2850
+ const ordered = [...covered].map((path3) => byPath.get(path3)).filter((route) => Boolean(route) && !route.skipped).sort((a, b) => a.path.length - b.path.length).concat(beneath.sort((a, b) => a.path.length - b.path.length));
2825
2851
  const limit = options.maxSteps ?? 5;
2826
2852
  const steps = ordered.slice(0, limit).map((route, index) => ({
2827
2853
  route: route.path,
@@ -2896,7 +2922,7 @@ function shotsFromRepo(repo, previous) {
2896
2922
  }
2897
2923
  async function previousFrames(out) {
2898
2924
  try {
2899
- const manifest = JSON.parse(await readFile(path.join(out, "manifest.json"), "utf8"));
2925
+ const manifest = JSON.parse(await readFile(path2.join(out, "manifest.json"), "utf8"));
2900
2926
  return new Map((manifest.frames ?? []).map((frame) => [frame.id, frame.sha256]));
2901
2927
  } catch {
2902
2928
  return /* @__PURE__ */ new Map();
@@ -2961,7 +2987,7 @@ Approve one on the plan page, then run this again.`
2961
2987
  appUrl: baseUrl,
2962
2988
  seedCommand: parsed.seedCommand,
2963
2989
  ...parsed.appCommand ? { appCommand: parsed.appCommand } : {},
2964
- sessionOut: parsed.session ?? path.join(process.cwd(), "helpdesk-demo-session.json"),
2990
+ sessionOut: parsed.session ?? path2.join(process.cwd(), "helpdesk-demo-session.json"),
2965
2991
  onLog: (line) => console.log(line)
2966
2992
  });
2967
2993
  if (seeded.sessionPath) parsed.session = seeded.sessionPath;
@@ -3071,7 +3097,7 @@ Approve one on the plan page, then run this again.`
3071
3097
  async send(frame) {
3072
3098
  if (local) {
3073
3099
  const file = `${frame.shot.id}.png`;
3074
- await writeFile(path.join(local, file), frame.image);
3100
+ await writeFile(path2.join(local, file), frame.image);
3075
3101
  frames.push({
3076
3102
  id: frame.shot.id,
3077
3103
  route: frame.shot.route,
@@ -3134,7 +3160,7 @@ ${report.captured} photographed, ${report.unchanged} unchanged, ${report.skipped
3134
3160
  });
3135
3161
  }
3136
3162
  await writeFile(
3137
- path.join(local, "manifest.json"),
3163
+ path2.join(local, "manifest.json"),
3138
3164
  `${JSON.stringify(
3139
3165
  {
3140
3166
  schema: "olomon.capture-manifest/1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olomonhelpdesk2/helpdesk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Reads a product's screens, plans its help articles, and photographs them.",
5
5
  "type": "module",
6
6
  "engines": {