@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.
- package/dist/index.js +59 -11
- 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
|
|
1361
|
+
let size;
|
|
1325
1362
|
try {
|
|
1326
|
-
|
|
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
|
-
|
|
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.
|
|
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);
|