@opul/cli 0.1.1 → 0.1.3

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 +84 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -243,6 +243,10 @@ function overlayInitSource() {
243
243
  const POS_KEY = "__opul_cursor_pos";
244
244
  const INK2 = "#0E0E0E";
245
245
  const PAPER = "#F4F4F2";
246
+ const DEFAULT_COLOR = "#C8F24E";
247
+ if (typeof config.color !== "string" || !/^#[0-9a-fA-F]{3,8}$|^(?:rgb|rgba|hsl|hsla)\([0-9.,%\s]+\)$|^[a-zA-Z]{1,20}$/.test(config.color.trim())) {
248
+ config = { color: DEFAULT_COLOR };
249
+ }
246
250
  function ensureFont() {
247
251
  if (document.getElementById("__opul_font"))
248
252
  return;
@@ -280,7 +284,11 @@ function overlayInitSource() {
280
284
  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
285
  root.appendChild(lower);
282
286
  root.appendChild(cursor);
283
- (document.body || document.documentElement).appendChild(root);
287
+ document.documentElement.appendChild(root);
288
+ }
289
+ function ensure() {
290
+ if (!root || !document.documentElement.contains(root))
291
+ build();
284
292
  }
285
293
  function ready() {
286
294
  return new Promise((res) => {
@@ -298,6 +306,7 @@ function overlayInitSource() {
298
306
  const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
299
307
  function move2(x, y, dur) {
300
308
  return new Promise((res) => {
309
+ ensure();
301
310
  const sx = pos.x;
302
311
  const sy = pos.y;
303
312
  const dx = x - sx;
@@ -321,6 +330,7 @@ function overlayInitSource() {
321
330
  }
322
331
  function click() {
323
332
  return new Promise((res) => {
333
+ ensure();
324
334
  cursor.animate([
325
335
  { transform: "translate(" + pos.x + "px," + pos.y + "px) scale(1)" },
326
336
  {
@@ -340,6 +350,7 @@ function overlayInitSource() {
340
350
  }
341
351
  function showLower(text) {
342
352
  return new Promise((res) => {
353
+ ensure();
343
354
  lower.textContent = text;
344
355
  void lower.offsetWidth;
345
356
  lower.style.opacity = "1";
@@ -348,6 +359,7 @@ function overlayInitSource() {
348
359
  }
349
360
  function hideLower2() {
350
361
  return new Promise((res) => {
362
+ ensure();
351
363
  lower.style.opacity = "0";
352
364
  setTimeout(res, 150);
353
365
  });
@@ -487,6 +499,7 @@ async function runStep(page, cast, step) {
487
499
  }
488
500
  case "hover": {
489
501
  const c2 = await centerOf(page, step.selector, timeout);
502
+ await ensureReady(page);
490
503
  if (step.name)
491
504
  await moveOverlay(page, c2.x, c2.y, MOVE_MS, step.name);
492
505
  else
@@ -497,6 +510,7 @@ async function runStep(page, cast, step) {
497
510
  }
498
511
  case "click": {
499
512
  const c2 = await centerOf(page, step.selector, timeout);
513
+ await ensureReady(page);
500
514
  await moveOverlay(page, c2.x, c2.y, MOVE_MS, step.name);
501
515
  await sleep(PRE_CLICK_MS);
502
516
  await overlayClick(page);
@@ -508,6 +522,7 @@ async function runStep(page, cast, step) {
508
522
  }
509
523
  case "type": {
510
524
  const c2 = await centerOf(page, step.selector, timeout);
525
+ await ensureReady(page);
511
526
  await moveOverlay(page, c2.x, c2.y, MOVE_MS, step.name);
512
527
  await sleep(PRE_CLICK_MS);
513
528
  await overlayClick(page);
@@ -889,29 +904,48 @@ function captureSource() {
889
904
  if (w.__opulCap || !w.__opulEmit)
890
905
  return;
891
906
  w.__opulCap = true;
907
+ const uniq = (s) => {
908
+ try {
909
+ return document.querySelectorAll(s).length === 1;
910
+ } catch {
911
+ return false;
912
+ }
913
+ };
914
+ const q = (s) => s.replace(/'/g, "\\'");
892
915
  const sel = (el) => {
893
916
  if (!el || el.nodeType !== 1)
894
917
  return null;
895
918
  const dd = el.closest("[data-demo]");
896
919
  if (dd)
897
920
  return `[data-demo='${dd.getAttribute("data-demo")}']`;
898
- if (el.id && document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1)
921
+ if (el.id && uniq(`#${CSS.escape(el.id)}`))
899
922
  return `#${CSS.escape(el.id)}`;
923
+ const al = el.getAttribute("aria-label");
924
+ if (al && uniq(`[aria-label='${q(al)}']`))
925
+ return `[aria-label='${q(al)}']`;
926
+ if (el.tagName === "A") {
927
+ const href = el.getAttribute("href");
928
+ if (href && href !== "#" && uniq(`a[href='${q(href)}']`))
929
+ return `a[href='${q(href)}']`;
930
+ }
900
931
  const parts = [];
901
932
  let node = el;
902
- while (node && node.nodeType === 1 && parts.length < 4) {
933
+ while (node && node.nodeType === 1 && node.tagName !== "HTML") {
903
934
  if (node.id) {
904
935
  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})`;
936
+ if (uniq(parts.join(" > ")))
937
+ return parts.join(" > ");
938
+ } else {
939
+ const parent = node.parentElement;
940
+ let part = node.tagName.toLowerCase();
941
+ if (parent) {
942
+ const idx = Array.from(parent.children).indexOf(node) + 1;
943
+ part += `:nth-child(${idx})`;
944
+ }
945
+ parts.unshift(part);
946
+ if (uniq(parts.join(" > ")))
947
+ return parts.join(" > ");
913
948
  }
914
- parts.unshift(part);
915
949
  node = node.parentElement;
916
950
  }
917
951
  return parts.join(" > ");
@@ -947,6 +981,18 @@ var ACTIONS = [
947
981
  ];
948
982
  var StepsValidationError = class extends Error {
949
983
  };
984
+ var SAFE_COLOR = /^#[0-9a-fA-F]{3,8}$|^(?:rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s]+\)$|^[a-zA-Z]{1,20}$/;
985
+ function isSafeColor(c2) {
986
+ return SAFE_COLOR.test(c2.trim());
987
+ }
988
+ function isSafeUrl(u) {
989
+ try {
990
+ const p = new URL(u);
991
+ return p.protocol === "http:" || p.protocol === "https:";
992
+ } catch {
993
+ return false;
994
+ }
995
+ }
950
996
  function parseSteps(raw) {
951
997
  if (typeof raw !== "object" || raw === null) {
952
998
  throw new StepsValidationError("steps file must be a JSON object");
@@ -959,6 +1005,16 @@ function parseSteps(raw) {
959
1005
  throw new StepsValidationError('"steps" must be a non-empty array');
960
1006
  }
961
1007
  const steps = obj.steps.map((s, i) => validateStep(s, i));
1008
+ for (const key of ["url", "startUrl"]) {
1009
+ const v = obj[key];
1010
+ if (typeof v === "string" && v !== "" && !isSafeUrl(v)) {
1011
+ throw new StepsValidationError(`"${key}" must be an http(s) URL`);
1012
+ }
1013
+ }
1014
+ const cursorColor = obj.cursor?.color;
1015
+ if (typeof cursorColor === "string" && !isSafeColor(cursorColor)) {
1016
+ throw new StepsValidationError(`cursor.color "${cursorColor}" is not a valid CSS color`);
1017
+ }
962
1018
  const vp = obj.viewport;
963
1019
  const viewport = vp && typeof vp.width === "number" && typeof vp.height === "number" ? { width: vp.width, height: vp.height } : { width: 1440, height: 900 };
964
1020
  return {
@@ -986,8 +1042,12 @@ function validateStep(s, i) {
986
1042
  throw new StepsValidationError(`step ${i} (${a}): "${field}" is required`);
987
1043
  }
988
1044
  };
989
- if (a === "goto")
1045
+ if (a === "goto") {
990
1046
  need("url");
1047
+ if (typeof step.url === "string" && !isSafeUrl(step.url)) {
1048
+ throw new StepsValidationError(`step ${i} (goto): "url" must be http(s)`);
1049
+ }
1050
+ }
991
1051
  if (a === "click" || a === "hover" || a === "wait")
992
1052
  need("selector");
993
1053
  if (a === "type") {
@@ -1212,7 +1272,7 @@ import { createInterface as createInterface2 } from "readline/promises";
1212
1272
  import { stdin, stdout } from "process";
1213
1273
 
1214
1274
  // src/config.ts
1215
- import { readFile as readFile2, writeFile as writeFile7, mkdir as mkdir3 } from "fs/promises";
1275
+ import { readFile as readFile2, writeFile as writeFile7, mkdir as mkdir3, chmod } from "fs/promises";
1216
1276
  import { homedir } from "os";
1217
1277
  import { join as join6 } from "path";
1218
1278
  var dir = join6(homedir(), ".opul");
@@ -1226,8 +1286,12 @@ async function loadConfig() {
1226
1286
  }
1227
1287
  }
1228
1288
  async function saveConfig(cfg) {
1229
- await mkdir3(dir, { recursive: true });
1289
+ await mkdir3(dir, { recursive: true, mode: 448 });
1290
+ await chmod(dir, 448).catch(() => {
1291
+ });
1230
1292
  await writeFile7(file, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
1293
+ await chmod(file, 384).catch(() => {
1294
+ });
1231
1295
  }
1232
1296
  async function requireToken() {
1233
1297
  const { token } = await loadConfig();
@@ -1291,15 +1355,15 @@ async function publishCommand(file2, opts) {
1291
1355
  }
1292
1356
  const videoPath = resolve3(process.cwd(), file2);
1293
1357
  const posterPath = opts.poster ? resolve3(process.cwd(), opts.poster) : join7(dirname(videoPath), "poster.jpg");
1294
- let video;
1358
+ let size;
1295
1359
  try {
1296
- video = await readFile3(videoPath);
1360
+ size = (await stat2(videoPath)).size;
1297
1361
  } catch {
1298
1362
  return fail3(`file not found: ${file2}`);
1299
1363
  }
1300
- const poster = await readFile3(posterPath).catch(() => null);
1301
- const size = (await stat2(videoPath)).size;
1302
1364
  if (size > 200 * 1024 * 1024) return fail3("file exceeds 200 MB upload cap.");
1365
+ const video = await readFile3(videoPath);
1366
+ const poster = await readFile3(posterPath).catch(() => null);
1303
1367
  const meta = await readFile3(join7(dirname(videoPath), "demo.json"), "utf8").then((s) => JSON.parse(s)).catch(() => ({}));
1304
1368
  const title = opts.title ?? meta.title ?? basename(videoPath).replace(/\.[^.]+$/, "");
1305
1369
  const auth = { authorization: `Bearer ${token}` };
@@ -1403,7 +1467,7 @@ function fail4(msg) {
1403
1467
 
1404
1468
  // src/index.ts
1405
1469
  var program = new Command();
1406
- program.name("opul").description("Record a product demo from your running web app.").version("0.1.0");
1470
+ program.name("opul").description("Record a product demo from your running web app.").version("0.1.3");
1407
1471
  program.command("init").description("Create an opul.steps.json stub").option("--out <file>", "output path", "opul.steps.json").action(initCommand);
1408
1472
  program.command("doctor").description("Check that Chrome and ffmpeg are installed").action(doctorCommand);
1409
1473
  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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opul/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Record a polished product-demo video from your running web app — from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {