@olomonhelpdesk2/helpdesk 0.1.0 → 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,17 +1996,51 @@ 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
- );
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
+ }
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
+ );
2032
+ }
2033
+
2034
+ // adapters/invoked-as-command.ts
2035
+ import { realpathSync } from "node:fs";
2036
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
2037
+ function runningAsCommand(moduleUrl) {
2038
+ const invoked = process.argv[1];
2039
+ if (!invoked) return false;
2040
+ try {
2041
+ return moduleUrl === pathToFileURL2(realpathSync(invoked)).href;
2001
2042
  } 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
- );
2043
+ return false;
2005
2044
  }
2006
2045
  }
2007
2046
 
@@ -2032,7 +2071,7 @@ function looksLikeIdentifier(segment) {
2032
2071
  return /\d/.test(segment) && segment.length >= 3;
2033
2072
  }
2034
2073
  function templatise(paths) {
2035
- const split = paths.map((path2) => path2.split("/").filter(Boolean));
2074
+ const split = paths.map((path3) => path3.split("/").filter(Boolean));
2036
2075
  const depth2 = Math.max(0, ...split.map((segments) => segments.length));
2037
2076
  const parameters = /* @__PURE__ */ new Set();
2038
2077
  const prefixOf = (segments, upTo) => {
@@ -2135,9 +2174,9 @@ var CassetteIndex = class {
2135
2174
  return void 0;
2136
2175
  }
2137
2176
  };
2138
- function matchesTemplate(template, path2) {
2177
+ function matchesTemplate(template, path3) {
2139
2178
  const wanted = template.split("/").filter(Boolean);
2140
- const actual = path2.split("/").filter(Boolean);
2179
+ const actual = path3.split("/").filter(Boolean);
2141
2180
  if (wanted.length !== actual.length) return false;
2142
2181
  return wanted.every((segment, index) => segment.startsWith(":") || segment === actual[index]);
2143
2182
  }
@@ -2168,10 +2207,10 @@ function shut(server) {
2168
2207
  });
2169
2208
  }
2170
2209
  function splitUrl(url) {
2171
- const [path2, search] = url.split("?");
2210
+ const [path3, search] = url.split("?");
2172
2211
  const query = {};
2173
2212
  for (const [name, value] of new URLSearchParams(search ?? "")) query[name] = value;
2174
- return { path: path2 ?? "/", query };
2213
+ return { path: path3 ?? "/", query };
2175
2214
  }
2176
2215
  async function replayFrom(options) {
2177
2216
  const index = new CassetteIndex(options.cassette);
@@ -2179,14 +2218,14 @@ async function replayFrom(options) {
2179
2218
  const served = /* @__PURE__ */ new Set();
2180
2219
  const server = createServer((incoming, outgoing) => {
2181
2220
  void (async () => {
2182
- const { path: path2, query } = splitUrl(incoming.url ?? "/");
2221
+ const { path: path3, query } = splitUrl(incoming.url ?? "/");
2183
2222
  await bodyOf(incoming);
2184
- const found = index.find({ method: incoming.method ?? "GET", path: path2, query });
2223
+ const found = index.find({ method: incoming.method ?? "GET", path: path3, query });
2185
2224
  if (!found) {
2186
2225
  const from = incoming.headers["referer"] ?? incoming.headers["x-seed-page"];
2187
2226
  const miss = {
2188
2227
  method: incoming.method ?? "GET",
2189
- path: path2,
2228
+ path: path3,
2190
2229
  ...Object.keys(query).length ? { query } : {},
2191
2230
  ...typeof from === "string" ? { from: pageOf(from) } : {}
2192
2231
  };
@@ -2422,8 +2461,8 @@ function gapsFrom(misses, cassette) {
2422
2461
  const templates = templatise([...known, ...misses.map((miss) => miss.path)]);
2423
2462
  const grouped = /* @__PURE__ */ new Map();
2424
2463
  for (const miss of misses) {
2425
- const path2 = templates.get(miss.path) ?? miss.path;
2426
- 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 } : {} });
2427
2466
  const existing = grouped.get(key);
2428
2467
  if (existing) {
2429
2468
  existing.asked += 1;
@@ -2432,7 +2471,7 @@ function gapsFrom(misses, cassette) {
2432
2471
  }
2433
2472
  grouped.set(key, {
2434
2473
  method: miss.method.toUpperCase(),
2435
- path: path2,
2474
+ path: path3,
2436
2475
  asked: 1,
2437
2476
  broke: miss.from ? [miss.from] : [],
2438
2477
  example: miss.path
@@ -2502,7 +2541,7 @@ function coverageReport(found, total) {
2502
2541
 
2503
2542
  // adapters/capture-bin.ts
2504
2543
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2505
- import path from "node:path";
2544
+ import path2 from "node:path";
2506
2545
 
2507
2546
  // adapters/map-cli.ts
2508
2547
  import { execFileSync } from "node:child_process";
@@ -2515,14 +2554,14 @@ var ROUTE_GROUP = /^\((.+)\)$/;
2515
2554
  var DYNAMIC = /^\[{1,2}(\.{3})?(.+?)\]{1,2}$/;
2516
2555
  var PARALLEL = /^@/;
2517
2556
  var INTERCEPTING = /^\(\.+\)/;
2518
- function classify(path2) {
2519
- if (path2 === "/api" || path2.startsWith("/api/")) {
2557
+ function classify(path3) {
2558
+ if (path3 === "/api" || path3.startsWith("/api/")) {
2520
2559
  return { skipped: "not-html", note: "an API route, not a screen" };
2521
2560
  }
2522
- if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(path2)) {
2561
+ if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(path3)) {
2523
2562
  return { skipped: "internal-tooling", note: "internal tooling" };
2524
2563
  }
2525
- 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)) {
2526
2565
  return { skipped: "unauthenticated-flow", note: "an authentication step" };
2527
2566
  }
2528
2567
  return void 0;
@@ -2626,8 +2665,8 @@ function enclosingLayouts(pageFile, layouts) {
2626
2665
  const dir = pageFile.slice(0, pageFile.lastIndexOf("/"));
2627
2666
  return layouts.filter((layout) => dir.startsWith(layout.slice(0, layout.lastIndexOf("/")))).sort((a, b) => a.length - b.length);
2628
2667
  }
2629
- function depth(path2) {
2630
- return path2 === "/" ? 0 : path2.split("/").filter(Boolean).length;
2668
+ function depth(path3) {
2669
+ return path3 === "/" ? 0 : path3.split("/").filter(Boolean).length;
2631
2670
  }
2632
2671
 
2633
2672
  // core/discovery/article-plan.ts
@@ -2664,8 +2703,8 @@ var ENTRY_POINT = /* @__PURE__ */ new Set([
2664
2703
  "integration",
2665
2704
  "integrations"
2666
2705
  ]);
2667
- function hasSegment(path2, vocabulary) {
2668
- 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()));
2669
2708
  }
2670
2709
  function score(route, signals) {
2671
2710
  const because = [];
@@ -2704,8 +2743,8 @@ function tierFor(rank, count) {
2704
2743
  if (rank <= Math.max(16, Math.ceil(count * 0.6))) return "supporting";
2705
2744
  return "minor";
2706
2745
  }
2707
- function slugFor(path2) {
2708
- 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("["));
2709
2748
  return segments.length ? segments.join("/") : "overview";
2710
2749
  }
2711
2750
  function titleFor(route) {
@@ -2764,7 +2803,7 @@ function buildArticlePlan(options) {
2764
2803
  let covered = 0;
2765
2804
  const needs = [...merged.entries()].map(([slug, group], index) => {
2766
2805
  const rank = index + 1;
2767
- 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);
2768
2807
  const already = existing.has(slug) || Boolean(claimed);
2769
2808
  if (already) covered += 1;
2770
2809
  return {
@@ -2808,7 +2847,7 @@ function buildStoryboard(options) {
2808
2847
  const beneath = options.routes.filter(
2809
2848
  (route) => !covered.has(route.path) && !route.skipped && [...covered].some((base) => base !== "/" && route.path.startsWith(`${base}/`))
2810
2849
  );
2811
- 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));
2812
2851
  const limit = options.maxSteps ?? 5;
2813
2852
  const steps = ordered.slice(0, limit).map((route, index) => ({
2814
2853
  route: route.path,
@@ -2883,7 +2922,7 @@ function shotsFromRepo(repo, previous) {
2883
2922
  }
2884
2923
  async function previousFrames(out) {
2885
2924
  try {
2886
- 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"));
2887
2926
  return new Map((manifest.frames ?? []).map((frame) => [frame.id, frame.sha256]));
2888
2927
  } catch {
2889
2928
  return /* @__PURE__ */ new Map();
@@ -2948,7 +2987,7 @@ Approve one on the plan page, then run this again.`
2948
2987
  appUrl: baseUrl,
2949
2988
  seedCommand: parsed.seedCommand,
2950
2989
  ...parsed.appCommand ? { appCommand: parsed.appCommand } : {},
2951
- sessionOut: parsed.session ?? path.join(process.cwd(), "helpdesk-demo-session.json"),
2990
+ sessionOut: parsed.session ?? path2.join(process.cwd(), "helpdesk-demo-session.json"),
2952
2991
  onLog: (line) => console.log(line)
2953
2992
  });
2954
2993
  if (seeded.sessionPath) parsed.session = seeded.sessionPath;
@@ -3058,7 +3097,7 @@ Approve one on the plan page, then run this again.`
3058
3097
  async send(frame) {
3059
3098
  if (local) {
3060
3099
  const file = `${frame.shot.id}.png`;
3061
- await writeFile(path.join(local, file), frame.image);
3100
+ await writeFile(path2.join(local, file), frame.image);
3062
3101
  frames.push({
3063
3102
  id: frame.shot.id,
3064
3103
  route: frame.shot.route,
@@ -3121,7 +3160,7 @@ ${report.captured} photographed, ${report.unchanged} unchanged, ${report.skipped
3121
3160
  });
3122
3161
  }
3123
3162
  await writeFile(
3124
- path.join(local, "manifest.json"),
3163
+ path2.join(local, "manifest.json"),
3125
3164
  `${JSON.stringify(
3126
3165
  {
3127
3166
  schema: "olomon.capture-manifest/1",
@@ -3261,7 +3300,7 @@ The session will be saved to ${sessionPath} and sent nowhere.
3261
3300
  Saved ${sessionPath}. Pass it to a capture run with --session ${sessionPath}.`);
3262
3301
  return 0;
3263
3302
  }
3264
- if (import.meta.url === `file://${process.argv[1]}`) {
3303
+ if (runningAsCommand(import.meta.url)) {
3265
3304
  main(process.argv.slice(2)).then(
3266
3305
  (code) => process.exit(code),
3267
3306
  (error) => {
package/helpdesk-seed.mjs CHANGED
@@ -10,6 +10,19 @@ function helpdeskCommand(command, args = []) {
10
10
  return parts.join(" ");
11
11
  }
12
12
 
13
+ // adapters/invoked-as-command.ts
14
+ import { realpathSync } from "node:fs";
15
+ import { pathToFileURL } from "node:url";
16
+ function runningAsCommand(moduleUrl) {
17
+ const invoked = process.argv[1];
18
+ if (!invoked) return false;
19
+ try {
20
+ return moduleUrl === pathToFileURL(realpathSync(invoked)).href;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
13
26
  // core/seed/cassette.ts
14
27
  var CASSETTE_SCHEMA = "olomon.seed-cassette/1";
15
28
  var DROP_HEADERS = /* @__PURE__ */ new Set([
@@ -1367,6 +1380,15 @@ async function routes(args) {
1367
1380
  function missFile(cassette) {
1368
1381
  return cassette.replace(/\.json$/, "") + ".misses.json";
1369
1382
  }
1383
+ if (runningAsCommand(import.meta.url)) {
1384
+ main(process.argv.slice(2)).then(
1385
+ (code) => process.exit(code),
1386
+ (error) => {
1387
+ console.error(error instanceof Error ? error.message : String(error));
1388
+ process.exit(1);
1389
+ }
1390
+ );
1391
+ }
1370
1392
  export {
1371
1393
  carriedReport,
1372
1394
  main
package/helpdesk.mjs CHANGED
@@ -1067,6 +1067,19 @@ function originOf(cwd) {
1067
1067
  }
1068
1068
  }
1069
1069
 
1070
+ // adapters/invoked-as-command.ts
1071
+ import { realpathSync } from "node:fs";
1072
+ import { pathToFileURL } from "node:url";
1073
+ function runningAsCommand(moduleUrl) {
1074
+ const invoked = process.argv[1];
1075
+ if (!invoked) return false;
1076
+ try {
1077
+ return moduleUrl === pathToFileURL(realpathSync(invoked)).href;
1078
+ } catch {
1079
+ return false;
1080
+ }
1081
+ }
1082
+
1070
1083
  // adapters/map-bin.ts
1071
1084
  function arg(name) {
1072
1085
  const index = process.argv.indexOf(`--${name}`);
@@ -1091,7 +1104,9 @@ async function main() {
1091
1104
  });
1092
1105
  console.log(result.report);
1093
1106
  }
1094
- main().catch((error) => {
1095
- console.error(error instanceof Error ? error.message : String(error));
1096
- process.exit(1);
1097
- });
1107
+ if (runningAsCommand(import.meta.url)) {
1108
+ main().catch((error) => {
1109
+ console.error(error instanceof Error ? error.message : String(error));
1110
+ process.exit(1);
1111
+ });
1112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olomonhelpdesk2/helpdesk",
3
- "version": "0.1.0",
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": {