@opul/cli 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.
Files changed (2) hide show
  1. package/dist/index.js +85 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -280,7 +280,11 @@ function overlayInitSource() {
280
280
  lower.style.cssText = "position:absolute;left:24px;bottom:24px;max-width:60%;background:" + INK2 + ";color:" + PAPER + ";font-size:16px;font-weight:600;line-height:1.2;letter-spacing:-0.025em;padding:8px 12px;border-radius:6px;opacity:0;transition:opacity 150ms ease;white-space:nowrap;";
281
281
  root.appendChild(lower);
282
282
  root.appendChild(cursor);
283
- (document.body || document.documentElement).appendChild(root);
283
+ document.documentElement.appendChild(root);
284
+ }
285
+ function ensure() {
286
+ if (!root || !document.documentElement.contains(root))
287
+ build();
284
288
  }
285
289
  function ready() {
286
290
  return new Promise((res) => {
@@ -298,6 +302,7 @@ function overlayInitSource() {
298
302
  const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
299
303
  function move2(x, y, dur) {
300
304
  return new Promise((res) => {
305
+ ensure();
301
306
  const sx = pos.x;
302
307
  const sy = pos.y;
303
308
  const dx = x - sx;
@@ -321,6 +326,7 @@ function overlayInitSource() {
321
326
  }
322
327
  function click() {
323
328
  return new Promise((res) => {
329
+ ensure();
324
330
  cursor.animate([
325
331
  { transform: "translate(" + pos.x + "px," + pos.y + "px) scale(1)" },
326
332
  {
@@ -340,6 +346,7 @@ function overlayInitSource() {
340
346
  }
341
347
  function showLower(text) {
342
348
  return new Promise((res) => {
349
+ ensure();
343
350
  lower.textContent = text;
344
351
  void lower.offsetWidth;
345
352
  lower.style.opacity = "1";
@@ -348,6 +355,7 @@ function overlayInitSource() {
348
355
  }
349
356
  function hideLower2() {
350
357
  return new Promise((res) => {
358
+ ensure();
351
359
  lower.style.opacity = "0";
352
360
  setTimeout(res, 150);
353
361
  });
@@ -487,6 +495,7 @@ async function runStep(page, cast, step) {
487
495
  }
488
496
  case "hover": {
489
497
  const c2 = await centerOf(page, step.selector, timeout);
498
+ await ensureReady(page);
490
499
  if (step.name)
491
500
  await moveOverlay(page, c2.x, c2.y, MOVE_MS, step.name);
492
501
  else
@@ -497,6 +506,7 @@ async function runStep(page, cast, step) {
497
506
  }
498
507
  case "click": {
499
508
  const c2 = await centerOf(page, step.selector, timeout);
509
+ await ensureReady(page);
500
510
  await moveOverlay(page, c2.x, c2.y, MOVE_MS, step.name);
501
511
  await sleep(PRE_CLICK_MS);
502
512
  await overlayClick(page);
@@ -508,6 +518,7 @@ async function runStep(page, cast, step) {
508
518
  }
509
519
  case "type": {
510
520
  const c2 = await centerOf(page, step.selector, timeout);
521
+ await ensureReady(page);
511
522
  await moveOverlay(page, c2.x, c2.y, MOVE_MS, step.name);
512
523
  await sleep(PRE_CLICK_MS);
513
524
  await overlayClick(page);
@@ -889,29 +900,48 @@ function captureSource() {
889
900
  if (w.__opulCap || !w.__opulEmit)
890
901
  return;
891
902
  w.__opulCap = true;
903
+ const uniq = (s) => {
904
+ try {
905
+ return document.querySelectorAll(s).length === 1;
906
+ } catch {
907
+ return false;
908
+ }
909
+ };
910
+ const q = (s) => s.replace(/'/g, "\\'");
892
911
  const sel = (el) => {
893
912
  if (!el || el.nodeType !== 1)
894
913
  return null;
895
914
  const dd = el.closest("[data-demo]");
896
915
  if (dd)
897
916
  return `[data-demo='${dd.getAttribute("data-demo")}']`;
898
- if (el.id && document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1)
917
+ if (el.id && uniq(`#${CSS.escape(el.id)}`))
899
918
  return `#${CSS.escape(el.id)}`;
919
+ const al = el.getAttribute("aria-label");
920
+ if (al && uniq(`[aria-label='${q(al)}']`))
921
+ return `[aria-label='${q(al)}']`;
922
+ if (el.tagName === "A") {
923
+ const href = el.getAttribute("href");
924
+ if (href && href !== "#" && uniq(`a[href='${q(href)}']`))
925
+ return `a[href='${q(href)}']`;
926
+ }
900
927
  const parts = [];
901
928
  let node = el;
902
- while (node && node.nodeType === 1 && parts.length < 4) {
929
+ while (node && node.nodeType === 1 && node.tagName !== "HTML") {
903
930
  if (node.id) {
904
931
  parts.unshift(`#${CSS.escape(node.id)}`);
905
- break;
906
- }
907
- let part = node.tagName.toLowerCase();
908
- const parent = node.parentElement;
909
- if (parent) {
910
- const same = Array.from(parent.children).filter((c2) => c2.tagName === node.tagName);
911
- if (same.length > 1)
912
- part += `:nth-of-type(${same.indexOf(node) + 1})`;
932
+ if (uniq(parts.join(" > ")))
933
+ return parts.join(" > ");
934
+ } else {
935
+ const parent = node.parentElement;
936
+ let part = node.tagName.toLowerCase();
937
+ if (parent) {
938
+ const idx = Array.from(parent.children).indexOf(node) + 1;
939
+ part += `:nth-child(${idx})`;
940
+ }
941
+ parts.unshift(part);
942
+ if (uniq(parts.join(" > ")))
943
+ return parts.join(" > ");
913
944
  }
914
- parts.unshift(part);
915
945
  node = node.parentElement;
916
946
  }
917
947
  return parts.join(" > ");
@@ -1091,23 +1121,52 @@ import { join as join5, resolve as resolve2, relative as relative2 } from "path"
1091
1121
  async function demoCommand(opts) {
1092
1122
  const stepsPath = resolve2(process.cwd(), opts.steps);
1093
1123
  const outDir = resolve2(process.cwd(), opts.out);
1094
- let raw;
1124
+ let raw = null;
1095
1125
  try {
1096
1126
  raw = await readFile(stepsPath, "utf8");
1097
1127
  } catch {
1098
- fail(
1099
- `no steps file at ${ink.bold(opts.steps)}
1100
- run ${ink.bold("opul init")} to create one.`
1101
- );
1102
- return;
1128
+ raw = null;
1103
1129
  }
1104
1130
  let steps;
1105
- try {
1106
- steps = parseSteps(JSON.parse(raw));
1107
- } catch (e) {
1108
- if (e instanceof StepsValidationError) fail(`invalid steps: ${e.message}`);
1109
- else fail(`steps file is not valid JSON: ${e.message}`);
1110
- return;
1131
+ if (raw === null) {
1132
+ if (!opts.url) {
1133
+ fail(
1134
+ `no steps file at ${ink.bold(opts.steps)} and no ${ink.bold("--url")}.
1135
+ run ${ink.bold("opul record --url <url>")} first, or ${ink.bold("opul init")}.`
1136
+ );
1137
+ return;
1138
+ }
1139
+ console.error(
1140
+ `${ink.dim("no steps file \u2014 let's capture your click-through first.")}
1141
+ `
1142
+ );
1143
+ try {
1144
+ steps = await recordSteps({
1145
+ url: opts.url,
1146
+ outPath: stepsPath,
1147
+ chromePath: opts.chrome
1148
+ });
1149
+ } catch (e) {
1150
+ if (e instanceof ChromeNotFoundError) fail(e.message);
1151
+ else fail(e.message);
1152
+ return;
1153
+ }
1154
+ if (steps.steps.length === 0) {
1155
+ fail("no steps captured \u2014 click or type in the window before saving, then re-run.");
1156
+ return;
1157
+ }
1158
+ console.error(
1159
+ `${ink.green("\u2713")} captured ${steps.steps.length} step${steps.steps.length === 1 ? "" : "s"} \u2192 ${ink.bold(opts.steps)}. rendering the video\u2026
1160
+ `
1161
+ );
1162
+ } else {
1163
+ try {
1164
+ steps = parseSteps(JSON.parse(raw));
1165
+ } catch (e) {
1166
+ if (e instanceof StepsValidationError) fail(`invalid steps: ${e.message}`);
1167
+ else fail(`steps file is not valid JSON: ${e.message}`);
1168
+ return;
1169
+ }
1111
1170
  }
1112
1171
  if (opts.url) steps.startUrl = opts.url;
1113
1172
  if (opts.profile) {
@@ -1374,11 +1433,11 @@ function fail4(msg) {
1374
1433
 
1375
1434
  // src/index.ts
1376
1435
  var program = new Command();
1377
- program.name("opul").description("Record a product demo from your running web app.").version("0.1.0");
1436
+ program.name("opul").description("Record a product demo from your running web app.").version("0.1.2");
1378
1437
  program.command("init").description("Create an opul.steps.json stub").option("--out <file>", "output path", "opul.steps.json").action(initCommand);
1379
1438
  program.command("doctor").description("Check that Chrome and ffmpeg are installed").action(doctorCommand);
1380
1439
  program.command("record").description("Click through your app to generate a steps file").option("--url <url>", "app URL to open").option("--out <file>", "steps file to write", "opul.steps.json").option("--name <name>", "product/demo name").option("--chrome <path>", "path to Chrome executable").action(recordCommand);
1381
- program.command("demo").description("Record a demo MP4 from your app").option("--url <url>", "app URL to open first (overrides startUrl)").option("--steps <file>", "steps file", "opul.steps.json").option("--out <dir>", "output directory", "opul-out").option("--headed", "run Chrome headed with a 3s pre-roll").option("--height <px>", "output height (width derived to 16:9)").option("--profile <dir>", "use an existing Chrome profile (prints a warning)").option("--chrome <path>", "path to Chrome executable").action(demoCommand);
1440
+ program.command("demo").description("Record a demo MP4 (captures a click-through first if no steps file)").option("--url <url>", "app URL to open first (overrides startUrl)").option("--steps <file>", "steps file", "opul.steps.json").option("--out <dir>", "output directory", "opul-out").option("--headed", "run Chrome headed with a 3s pre-roll").option("--height <px>", "output height (width derived to 16:9)").option("--profile <dir>", "use an existing Chrome profile (prints a warning)").option("--chrome <path>", "path to Chrome executable").action(demoCommand);
1382
1441
  program.command("publish <file>").description("Publish an MP4 and get a shareable link").option("--title <title>", "demo title").option("--replace <slug>", "replace an existing demo, keeping its URL").option("--poster <file>", "poster image (defaults to ./poster.jpg)").action(publishCommand);
1383
1442
  program.command("ls").description("List your published demos").action(lsCommand);
1384
1443
  program.command("rm <slug>").description("Delete a published demo").action(rmCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opul/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Record a polished product-demo video from your running web app — from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {