@opul/cli 0.1.2 → 0.1.4

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 +59 -11
  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;
@@ -977,6 +981,18 @@ var ACTIONS = [
977
981
  ];
978
982
  var StepsValidationError = class extends Error {
979
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
+ }
980
996
  function parseSteps(raw) {
981
997
  if (typeof raw !== "object" || raw === null) {
982
998
  throw new StepsValidationError("steps file must be a JSON object");
@@ -989,6 +1005,16 @@ function parseSteps(raw) {
989
1005
  throw new StepsValidationError('"steps" must be a non-empty array');
990
1006
  }
991
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
+ }
992
1018
  const vp = obj.viewport;
993
1019
  const viewport = vp && typeof vp.width === "number" && typeof vp.height === "number" ? { width: vp.width, height: vp.height } : { width: 1440, height: 900 };
994
1020
  return {
@@ -1016,8 +1042,12 @@ function validateStep(s, i) {
1016
1042
  throw new StepsValidationError(`step ${i} (${a}): "${field}" is required`);
1017
1043
  }
1018
1044
  };
1019
- if (a === "goto")
1045
+ if (a === "goto") {
1020
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
+ }
1021
1051
  if (a === "click" || a === "hover" || a === "wait")
1022
1052
  need("selector");
1023
1053
  if (a === "type") {
@@ -1219,6 +1249,9 @@ async function demoCommand(opts) {
1219
1249
  console.log(` ${rel(result.posterPath)}`);
1220
1250
  console.log(`
1221
1251
  next: ${ink.bold(`opul publish ${rel(result.videoPath)}`)}`);
1252
+ console.log(
1253
+ ` ${ink.dim("\u21B3 turns it into a shareable link. needs a free account \u2014 run")} ${ink.bold("opul login")} ${ink.dim("first.")}`
1254
+ );
1222
1255
  } catch (e) {
1223
1256
  progress.stop();
1224
1257
  if (e instanceof ChromeNotFoundError) fail(e.message);
@@ -1242,7 +1275,7 @@ import { createInterface as createInterface2 } from "readline/promises";
1242
1275
  import { stdin, stdout } from "process";
1243
1276
 
1244
1277
  // src/config.ts
1245
- import { readFile as readFile2, writeFile as writeFile7, mkdir as mkdir3 } from "fs/promises";
1278
+ import { readFile as readFile2, writeFile as writeFile7, mkdir as mkdir3, chmod } from "fs/promises";
1246
1279
  import { homedir } from "os";
1247
1280
  import { join as join6 } from "path";
1248
1281
  var dir = join6(homedir(), ".opul");
@@ -1256,8 +1289,12 @@ async function loadConfig() {
1256
1289
  }
1257
1290
  }
1258
1291
  async function saveConfig(cfg) {
1259
- await mkdir3(dir, { recursive: true });
1292
+ await mkdir3(dir, { recursive: true, mode: 448 });
1293
+ await chmod(dir, 448).catch(() => {
1294
+ });
1260
1295
  await writeFile7(file, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
1296
+ await chmod(file, 384).catch(() => {
1297
+ });
1261
1298
  }
1262
1299
  async function requireToken() {
1263
1300
  const { token } = await loadConfig();
@@ -1321,15 +1358,15 @@ async function publishCommand(file2, opts) {
1321
1358
  }
1322
1359
  const videoPath = resolve3(process.cwd(), file2);
1323
1360
  const posterPath = opts.poster ? resolve3(process.cwd(), opts.poster) : join7(dirname(videoPath), "poster.jpg");
1324
- let video;
1361
+ let size;
1325
1362
  try {
1326
- video = await readFile3(videoPath);
1363
+ size = (await stat2(videoPath)).size;
1327
1364
  } catch {
1328
1365
  return fail3(`file not found: ${file2}`);
1329
1366
  }
1330
- const poster = await readFile3(posterPath).catch(() => null);
1331
- const size = (await stat2(videoPath)).size;
1332
1367
  if (size > 200 * 1024 * 1024) return fail3("file exceeds 200 MB upload cap.");
1368
+ const video = await readFile3(videoPath);
1369
+ const poster = await readFile3(posterPath).catch(() => null);
1333
1370
  const meta = await readFile3(join7(dirname(videoPath), "demo.json"), "utf8").then((s) => JSON.parse(s)).catch(() => ({}));
1334
1371
  const title = opts.title ?? meta.title ?? basename(videoPath).replace(/\.[^.]+$/, "");
1335
1372
  const auth = { authorization: `Bearer ${token}` };
@@ -1340,9 +1377,11 @@ async function publishCommand(file2, opts) {
1340
1377
  body: JSON.stringify({ title, hasPoster: !!poster, byteSize: size })
1341
1378
  }).catch(() => null);
1342
1379
  if (!presign || !presign.ok) {
1343
- return fail3(
1344
- presign ? `publish failed (${presign.status}): ${await presign.text()}` : "could not reach opul.dev \u2014 check your connection."
1345
- );
1380
+ if (!presign) return fail3("could not reach opul.dev \u2014 check your connection.");
1381
+ const body = await parseError(presign);
1382
+ const hint = presign.status === 403 ? `
1383
+ ${ink.dim("\u21B3 upgrade or manage your plan:")} ${ink.bold(`${base}/account/billing`)}` : "";
1384
+ return fail3(`publish failed (${presign.status}): ${body}${hint}`);
1346
1385
  }
1347
1386
  const { id, slug, uploadUrl, posterUploadUrl } = await presign.json();
1348
1387
  process.stderr.write(ink.dim(`uploading ${fmtSize(size)}\u2026`));
@@ -1373,6 +1412,15 @@ async function publishCommand(file2, opts) {
1373
1412
  console.log(`${ink.green("\u2713")} published`);
1374
1413
  console.log(` ${ink.bold(`${base}/d/${slug}`)}`);
1375
1414
  }
1415
+ async function parseError(res) {
1416
+ const text = await res.text().catch(() => "");
1417
+ try {
1418
+ const j = JSON.parse(text);
1419
+ if (j && typeof j.error === "string") return j.error;
1420
+ } catch {
1421
+ }
1422
+ return text || res.statusText;
1423
+ }
1376
1424
  function fail3(msg) {
1377
1425
  console.error(`${ink.red("\u2717")} ${msg}`);
1378
1426
  process.exit(1);
@@ -1433,7 +1481,7 @@ function fail4(msg) {
1433
1481
 
1434
1482
  // src/index.ts
1435
1483
  var program = new Command();
1436
- program.name("opul").description("Record a product demo from your running web app.").version("0.1.2");
1484
+ program.name("opul").description("Record a product demo from your running web app.").version("0.1.4");
1437
1485
  program.command("init").description("Create an opul.steps.json stub").option("--out <file>", "output path", "opul.steps.json").action(initCommand);
1438
1486
  program.command("doctor").description("Check that Chrome and ffmpeg are installed").action(doctorCommand);
1439
1487
  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.2",
3
+ "version": "0.1.4",
4
4
  "description": "Record a polished product-demo video from your running web app — from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {