@visualautomate/cli 0.5.0 → 0.5.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 (3) hide show
  1. package/README.md +10 -5
  2. package/dist/index.js +111 -49
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -59,6 +59,10 @@ state {"runs":1}
59
59
  A run passes when it succeeds and, if the test file names an `expectOutput`,
60
60
  leaves by that port — the same verdict as the GitHub check.
61
61
 
62
+ The verdict is green when it passed and red when it did not, in the sandbox as
63
+ well as out here. `NO_COLOR` turns that off, and output that is not a terminal —
64
+ a pipe, a file, a CI log — is plain anyway; `FORCE_COLOR` turns it back on.
65
+
62
66
  ### In a GitHub repository
63
67
 
64
68
  `test` works wherever the platform tests your code on a push:
@@ -79,16 +83,17 @@ my-app/ an app's repository
79
83
  …
80
84
  ```
81
85
 
82
- At the root of an app's repository every module runs, one after another;
83
- `--module send-message` runs one. `index.js` in a repository is run as written,
86
+ At the root of an app's repository every module runs, one after another. Name one
87
+ to run only that: `vsa test send-message`, or any short name that picks it out —
88
+ `vsa test send`. `--module <name>` is the same thing. `index.js` in a repository is run as written,
84
89
  unbundled, because that is the file the sandbox loads.
85
90
 
86
91
  ## `dev`
87
92
 
88
93
  ```bash
89
- vsa dev
90
- vsa dev --module send-message
91
- vsa dev --tier boosted
94
+ vsa dev # every module in modules/
95
+ vsa dev send-message # one of them; the shortest name that picks it will do
96
+ vsa dev --tier boosted # a bigger machine
92
97
  vsa dev --local # on this machine's Node, without the sandbox
93
98
  ```
94
99
 
package/dist/index.js CHANGED
@@ -1822,6 +1822,14 @@ var ENTRIES = ["plugin.ts", "plugin.js", "index.ts", "index.js"];
1822
1822
  function entryIn(dir) {
1823
1823
  return ENTRIES.map((file2) => join(dir, file2)).find((file2) => existsSync(file2)) ?? null;
1824
1824
  }
1825
+ function matchFolders(folders, name) {
1826
+ const wanted2 = name.trim().toLowerCase();
1827
+ const exact = folders.filter((folder) => folder.toLowerCase() === wanted2);
1828
+ if (exact.length > 0) return exact;
1829
+ const prefix = folders.filter((folder) => folder.toLowerCase().startsWith(wanted2));
1830
+ if (prefix.length > 0) return prefix;
1831
+ return folders.filter((folder) => folder.toLowerCase().includes(wanted2));
1832
+ }
1825
1833
  function findTargets(cwd, only) {
1826
1834
  const dir = resolve(cwd);
1827
1835
  const modulesDir = join(dir, "modules");
@@ -1830,16 +1838,16 @@ function findTargets(cwd, only) {
1830
1838
  if (folders.length === 0) {
1831
1839
  throw new Error("There is a modules/ folder here, but none of its folders has a manifest.json.");
1832
1840
  }
1833
- const chosen = only ? folders.filter((f) => f === only) : folders;
1841
+ const chosen = only ? matchFolders(folders, only) : folders;
1834
1842
  if (only && chosen.length === 0) {
1835
- throw new Error(`No module "${only}" in modules/. There is: ${folders.join(", ")}.`);
1843
+ throw new Error(`No module matches "${only}". This repository has: ${folders.join(", ")}.`);
1836
1844
  }
1837
1845
  return chosen.map((folder) => targetIn(join(modulesDir, folder), `modules/${folder}`)).filter(Boolean);
1838
1846
  }
1839
1847
  const target = targetIn(dir, basename(dir));
1840
1848
  if (!target) {
1841
1849
  throw new Error(
1842
- "Nothing to test here: no plugin.ts, plugin.js or index.js, and no modules/ folder.\nRun this in a plugin's folder, at the root of its repository, or run `visualautomate init <name>`."
1850
+ "Nothing to test here: no plugin.ts, plugin.js or index.js, and no modules/ folder.\nRun this in a module's folder, at the root of its repository, or run `vsa init <name>`."
1843
1851
  );
1844
1852
  }
1845
1853
  return [target];
@@ -2079,6 +2087,38 @@ async function savePersisted(dir, files, state) {
2079
2087
 
2080
2088
  // src/docker.ts
2081
2089
  import { spawnSync } from "node:child_process";
2090
+
2091
+ // src/colour.ts
2092
+ var CODES = {
2093
+ reset: "\x1B[0m",
2094
+ bold: "\x1B[1m",
2095
+ dim: "\x1B[2m",
2096
+ red: "\x1B[31m",
2097
+ green: "\x1B[32m",
2098
+ yellow: "\x1B[33m",
2099
+ blue: "\x1B[34m"
2100
+ };
2101
+ function wanted() {
2102
+ const env = process.env;
2103
+ if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
2104
+ if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") return true;
2105
+ return Boolean(process.stdout.isTTY);
2106
+ }
2107
+ var ON = wanted();
2108
+ function paint(code) {
2109
+ return (text) => ON ? `${code}${text}${CODES.reset}` : text;
2110
+ }
2111
+ var red = paint(CODES.red);
2112
+ var green = paint(CODES.green);
2113
+ var yellow = paint(CODES.yellow);
2114
+ var blue = paint(CODES.blue);
2115
+ var dim = paint(CODES.dim);
2116
+ var bold = paint(CODES.bold);
2117
+ function colourEnv() {
2118
+ return ON ? ["--env", "FORCE_COLOR=1"] : ["--env", "NO_COLOR=1"];
2119
+ }
2120
+
2121
+ // src/docker.ts
2082
2122
  var IMAGE = "ghcr.io/visualautomate/plugin-sandbox";
2083
2123
  var TIERS = {
2084
2124
  standard: { cpus: "0.25", memory: "1g" },
@@ -2111,6 +2151,9 @@ function dockerArgs(run) {
2111
2151
  "ALL",
2112
2152
  "--security-opt",
2113
2153
  "no-new-privileges",
2154
+ // So a run inside the container is as readable as one out here: its
2155
+ // stdout is a pipe to Docker, which would otherwise turn colour off.
2156
+ ...colourEnv(),
2114
2157
  ...run.user ? ["--user", `${run.user.uid}:${run.user.gid}`] : [],
2115
2158
  run.image,
2116
2159
  ...run.args
@@ -2134,16 +2177,18 @@ function dockerProblem() {
2134
2177
  function explainExit(code, tier) {
2135
2178
  if (code === 137) {
2136
2179
  const next = Object.keys(TIERS)[Object.keys(TIERS).indexOf(tier) + 1];
2137
- return `\u2717 stopped the step used more than the ${TIERS[tier].memory.replace("g", " GB")} of memory the ${tier} tier has, and the container was killed. On the platform it would stop the same way.` + (next ? ` To check it fits a bigger machine: --tier ${next}` : "");
2180
+ return red(
2181
+ `\u2717 stopped the step used more than the ${TIERS[tier].memory.replace("g", " GB")} of memory the ${tier} tier has, and the container was killed. On the platform it would stop the same way.` + (next ? ` To check it fits a bigger machine: --tier ${next}` : "")
2182
+ );
2138
2183
  }
2139
- if (code === 125) return "\u2717 Docker could not start the container. Is the image available? Try: docker pull " + IMAGE;
2184
+ if (code === 125) return red("\u2717 Docker could not start the container. Is the image available? Try: docker pull " + IMAGE);
2140
2185
  return null;
2141
2186
  }
2142
2187
  function ensureDocker(say, waitMs = 12e4) {
2143
2188
  const problem = dockerProblem();
2144
2189
  if (!problem) return null;
2145
2190
  if (problem.includes("not installed")) return problem;
2146
- say("Docker is not running. Starting Docker Desktop\u2026");
2191
+ say(yellow("Docker is not running. Starting Docker Desktop\u2026"));
2147
2192
  const started = spawnSync("docker", ["desktop", "start"], { encoding: "utf8" });
2148
2193
  if (started.status !== 0) {
2149
2194
  say("Could not start it from here \u2014 start Docker Desktop yourself; this will carry on when it is up.");
@@ -2177,7 +2222,7 @@ function runInDocker(run) {
2177
2222
  }
2178
2223
 
2179
2224
  // src/index.ts
2180
- var VERSION = true ? "0.5.0" : "0.0.0-dev";
2225
+ var VERSION = true ? "0.5.2" : "0.0.0-dev";
2181
2226
  var DEFAULT_API_URL = "https://visualautomate.com";
2182
2227
  function configPath() {
2183
2228
  return join3(homedir(), ".visualautomate", "config.json");
@@ -2204,7 +2249,7 @@ async function tokenFor(flags) {
2204
2249
  return { token, apiUrl };
2205
2250
  }
2206
2251
  function fail(message) {
2207
- console.error(message);
2252
+ console.error(red(message));
2208
2253
  throw new SilentExit();
2209
2254
  }
2210
2255
  var SilentExit = class extends Error {
@@ -2281,8 +2326,8 @@ async function cmdLogin(flags) {
2281
2326
  });
2282
2327
  const who = await whoami(apiUrl, token);
2283
2328
  await writeConfig({ token, apiUrl, email: who.email });
2284
- ok(`
2285
- \u2713 Signed in as ${who.email}`);
2329
+ ok(green(`
2330
+ \u2713 Signed in as ${who.email}`));
2286
2331
  ok(` Token stored in ${configPath()}`);
2287
2332
  }
2288
2333
  async function whoami(apiUrl, token) {
@@ -2368,9 +2413,9 @@ visualautomate push # publish it to your account
2368
2413
  \`test.json\` is the input, config and state \`test\` runs with.
2369
2414
  `
2370
2415
  );
2371
- ok(`
2416
+ ok(green(`
2372
2417
  \u2713 Created ${name} from the "${templateName}" template
2373
- `);
2418
+ `));
2374
2419
  ok(" cd " + name);
2375
2420
  ok(" visualautomate test");
2376
2421
  ok(" visualautomate push\n");
@@ -2404,12 +2449,12 @@ function line(indent, text) {
2404
2449
  async function runTarget(target, flags, several) {
2405
2450
  const say = (text) => line(several, text);
2406
2451
  if (several) ok(`
2407
- \u25B8 ${target.name}`);
2452
+ ${bold(`\u25B8 ${target.name}`)}`);
2408
2453
  try {
2409
2454
  const code = await codeFor(target);
2410
2455
  const check = validatePluginCode(code);
2411
2456
  if (!check.valid) {
2412
- say(`\u2717 refused ${check.error}`);
2457
+ say(`${red("\u2717 refused")} ${check.error}`);
2413
2458
  return false;
2414
2459
  }
2415
2460
  const settings = await readTestFile(target.dir, flags.input);
@@ -2423,7 +2468,7 @@ async function runTarget(target, flags, several) {
2423
2468
  const manifest = parseJsonc(await readFile3(manifestPath, "utf8")).value;
2424
2469
  outputs = manifest.outputs ?? [];
2425
2470
  } catch (error) {
2426
- say(`\u2717 manifest.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
2471
+ say(red(`\u2717 manifest.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`));
2427
2472
  return false;
2428
2473
  }
2429
2474
  }
@@ -2449,33 +2494,37 @@ async function runTarget(target, flags, several) {
2449
2494
  const port = result.output?.output ?? null;
2450
2495
  const expected = settings.expectOutput;
2451
2496
  const passed = result.status === "success" && (!expected || port === expected);
2452
- say(`${passed ? "\u2713 passed" : "\u2717 failed"} ${result.status}, ${result.durationMs}ms`);
2453
- if (result.error) say(`error ${result.error}`);
2497
+ const verdict = passed ? green(bold("\u2713 passed")) : red(bold("\u2717 failed"));
2498
+ say(`${verdict} ${dim(`${result.status}, ${result.durationMs}ms`)}`);
2499
+ if (result.error) say(`${dim("error")} ${red(result.error)}`);
2454
2500
  if (result.output) {
2455
- say(`output ${port}${expected ? ` (expected ${expected})` : ""}`);
2456
- say(`data ${JSON.stringify(result.output.data, null, 2).replace(/\n/g, "\n ")}`);
2501
+ const port_ = passed || !expected ? port : red(String(port));
2502
+ say(`${dim("output")} ${port_}${expected ? dim(` (expected ${expected})`) : ""}`);
2503
+ say(`${dim("data")} ${JSON.stringify(result.output.data, null, 2).replace(/\n/g, "\n ")}`);
2457
2504
  }
2458
- if (result.files.size > 0) say(`files ${[...result.files.keys()].join(", ")}`);
2459
- if (result.state.size > 0) say(`state ${JSON.stringify(Object.fromEntries(result.state))}`);
2460
- for (const entry of result.logs ?? []) say(`log ${entry}`);
2505
+ if (result.files.size > 0) say(`${dim("files")} ${[...result.files.keys()].join(", ")}`);
2506
+ if (result.state.size > 0) say(`${dim("state")} ${JSON.stringify(Object.fromEntries(result.state))}`);
2507
+ for (const entry of result.logs ?? []) say(`${dim("log")} ${entry}`);
2461
2508
  for (const [host, enforced] of denied) {
2462
2509
  say(
2463
- enforced ? `egress blocked ${host} \u2014 not in allowedDomains` : `! egress ${host} is not allowed on the platform unless the plugin declares it. Add "allowedDomains": ["${host}"] to the test file to check with the same rule.`
2510
+ enforced ? `${dim("egress")} ${red(`blocked ${host}`)} \u2014 not in allowedDomains` : yellow(`! egress ${host} is not allowed on the platform unless the plugin declares it. `) + `Add "allowedDomains": ["${host}"] to the test file to check with the same rule.`
2464
2511
  );
2465
2512
  }
2466
2513
  if (persist) {
2467
2514
  const refused = await savePersisted(target.dir, result.files, result.state);
2468
- say(`kept storage and state in ${relative3(process.cwd(), join3(target.dir, ".visualautomate")) || ".visualautomate"}`);
2469
- for (const path of refused) say(`! storage path "${path}" points outside the storage folder and was not written`);
2515
+ say(`${dim("kept")} ${dim(`storage and state in ${relative3(process.cwd(), join3(target.dir, ".visualautomate")) || ".visualautomate"}`)}`);
2516
+ for (const path of refused) say(yellow(`! storage path "${path}" points outside the storage folder and was not written`));
2470
2517
  }
2471
2518
  if (port && outputs.length > 0 && !outputs.includes(port)) {
2472
2519
  say(
2473
- `! This returned output "${port}", which manifest.json does not list (it has ${outputs.map((o) => `"${o}"`).join(", ")}). On a canvas nothing would follow it.`
2520
+ yellow(
2521
+ `! This returned output "${port}", which manifest.json does not list (it has ${outputs.map((o) => `"${o}"`).join(", ")}). On a canvas nothing would follow it.`
2522
+ )
2474
2523
  );
2475
2524
  }
2476
2525
  return passed;
2477
2526
  } catch (error) {
2478
- say(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
2527
+ say(red(`\u2717 ${error instanceof Error ? error.message : String(error)}`));
2479
2528
  return false;
2480
2529
  }
2481
2530
  }
@@ -2484,8 +2533,11 @@ async function runTargets(targets, flags) {
2484
2533
  for (const target of targets) {
2485
2534
  if (await runTarget(target, flags, targets.length > 1)) passed++;
2486
2535
  }
2487
- if (targets.length > 1) ok(`
2488
- ${passed} of ${targets.length} passed`);
2536
+ if (targets.length > 1) {
2537
+ const tally = `${passed} of ${targets.length} passed`;
2538
+ ok(`
2539
+ ${passed === targets.length ? green(bold(tally)) : red(bold(tally))}`);
2540
+ }
2489
2541
  return passed === targets.length;
2490
2542
  }
2491
2543
  function packageLoader(dir) {
@@ -2538,31 +2590,41 @@ function runTargetsInDocker(targets, all, flags) {
2538
2590
  }
2539
2591
  return passed;
2540
2592
  }
2541
- function targetsOrFail(flags) {
2593
+ function targetsOrFail(args, flags) {
2542
2594
  try {
2543
- return findTargets(process.cwd(), flags.module);
2595
+ return findTargets(process.cwd(), args[0] ?? flags.module);
2544
2596
  } catch (error) {
2545
2597
  fail(error instanceof Error ? error.message : String(error));
2546
2598
  }
2547
2599
  }
2548
- async function cmdTest(flags) {
2549
- const targets = targetsOrFail(flags);
2600
+ function sayWhatRuns(targets, picked) {
2601
+ if (targets.length === 1) {
2602
+ ok(`Running ${targets[0].name}`);
2603
+ return;
2604
+ }
2605
+ ok(`Running ${targets.length} modules: ${targets.map((t) => t.name.replace(/^modules\//, "")).join(", ")}`);
2606
+ if (!picked) ok(dim(`One of them: vsa dev ${targets[0].name.replace(/^modules\//, "")}`));
2607
+ }
2608
+ async function cmdTest(args, flags) {
2609
+ const targets = targetsOrFail(args, flags);
2550
2610
  if (flags.docker) prepareSandbox(flags);
2551
2611
  const passed = flags.docker ? runTargetsInDocker(targets, targets, flags) : await runTargets(targets, flags);
2552
2612
  if (!passed) process.exitCode = 1;
2553
2613
  }
2554
- async function cmdDev(flags) {
2614
+ async function cmdDev(args, flags) {
2555
2615
  const root = process.cwd();
2556
- let targets = targetsOrFail(flags);
2616
+ const picked = args[0] ?? flags.module;
2617
+ let targets = targetsOrFail(args, flags);
2557
2618
  const inSandbox = flags.local !== "true";
2558
2619
  if (inSandbox) prepareSandbox(flags);
2559
2620
  const header = () => {
2560
2621
  process.stdout.write("\x1Bc");
2561
- ok(`vsa dev \u2014 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \u2014 Ctrl+C to stop`);
2622
+ ok(`${bold("vsa dev")} ${dim(`\u2014 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \u2014 Ctrl+C to stop`)}`);
2562
2623
  };
2563
2624
  const run = async (chosen) => inSandbox ? runTargetsInDocker(chosen, targets, flags) : runTargets(chosen, flags);
2564
2625
  header();
2565
- ok(inSandbox ? `in ${imageFor(flags)}, tier ${flags.tier || "standard"}` : "on this machine, not in the sandbox");
2626
+ ok(dim(inSandbox ? `in ${imageFor(flags)}, tier ${flags.tier || "standard"}` : "on this machine, not in the sandbox"));
2627
+ sayWhatRuns(targets, Boolean(picked));
2566
2628
  await run(targets);
2567
2629
  const ignored = [`${sep2}node_modules${sep2}`, `${sep2}.git${sep2}`, `${sep2}dist${sep2}`, `${sep2}.visualautomate${sep2}`];
2568
2630
  const pending = /* @__PURE__ */ new Set();
@@ -2577,17 +2639,17 @@ async function cmdDev(flags) {
2577
2639
  const changed = [...pending];
2578
2640
  pending.clear();
2579
2641
  try {
2580
- targets = findTargets(root, flags.module);
2642
+ targets = findTargets(root, picked);
2581
2643
  } catch (error) {
2582
2644
  header();
2583
- ok(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
2645
+ ok(red(`\u2717 ${error instanceof Error ? error.message : String(error)}`));
2584
2646
  running = false;
2585
2647
  return;
2586
2648
  }
2587
2649
  const hit = changed.map((path) => targetForPath(targets, path));
2588
2650
  const chosen = hit.some((t) => t === null) ? targets : [...new Set(hit)];
2589
2651
  header();
2590
- ok(`changed: ${changed.map((p) => relative3(root, p)).join(", ")}`);
2652
+ ok(dim(`changed: ${changed.map((p) => relative3(root, p)).join(", ")}`));
2591
2653
  await run(chosen);
2592
2654
  running = false;
2593
2655
  };
@@ -2689,8 +2751,8 @@ ${text.slice(0, 500)}`);
2689
2751
  if (!res.ok || !data.success) {
2690
2752
  fail(`Push refused: ${data.error ?? `status ${res.status}`}`);
2691
2753
  }
2692
- ok(`
2693
- \u2713 Published (id: ${data.pluginId})`);
2754
+ ok(green(`
2755
+ \u2713 Published (id: ${data.pluginId})`));
2694
2756
  ok(` A version snapshot was saved, so you can roll back from the dashboard.`);
2695
2757
  }
2696
2758
  function printHelp() {
@@ -2699,8 +2761,8 @@ vsa \u2014 build and publish VisualAutomate plugins
2699
2761
 
2700
2762
  vsa login sign this machine in
2701
2763
  vsa init <name> [--template] start a plugin
2702
- vsa dev [--module <name>] the sandbox, and a run again on every save
2703
- vsa test [--module <name>] run it once, as the platform's check does
2764
+ vsa dev [<module>] the sandbox, and a run again on every save
2765
+ vsa test [<module>] run it once, as the platform's check does
2704
2766
  vsa push publish it
2705
2767
  vsa whoami which account this is
2706
2768
  vsa logout forget the token
@@ -2712,11 +2774,11 @@ Templates: ${Object.keys(TEMPLATES).join(", ")}
2712
2774
 
2713
2775
  \`test\` and \`dev\` work in a plugin made with \`init\`, at the root of a plugin's
2714
2776
  GitHub repository, and at the root of an app's repository, where every folder
2715
- in modules/ is run (or the one named with --module). The run's settings come
2777
+ in modules/ is run (or the one you name: vsa dev send-message). The run's settings come
2716
2778
  from visualautomate.test.json, test.json, or --input <file>.
2717
2779
 
2718
2780
  Options for \`dev\` and \`test\`:
2719
- --module <name> one folder of modules/ instead of all of them
2781
+ --module <name> the same as naming it: vsa dev send-message
2720
2782
  --tier <name> the sandbox's size: standard, boosted, high or max
2721
2783
  --image <ref> a different sandbox image
2722
2784
  --persist keep storage and state in .visualautomate/ between runs
@@ -2761,17 +2823,17 @@ async function main() {
2761
2823
  case "new":
2762
2824
  return cmdInit(args, flags);
2763
2825
  case "test":
2764
- return cmdTest(flags);
2826
+ return cmdTest(args, flags);
2765
2827
  case "dev":
2766
2828
  case "watch":
2767
- return cmdDev(flags);
2829
+ return cmdDev(args, flags);
2768
2830
  case "push":
2769
2831
  case "publish":
2770
2832
  return cmdPush(flags);
2771
2833
  // `plugin test` and `plugin push` are accepted as aliases.
2772
2834
  case "plugin": {
2773
2835
  const sub = args.shift();
2774
- if (sub === "test") return cmdTest(flags);
2836
+ if (sub === "test") return cmdTest(args, flags);
2775
2837
  if (sub === "push") return cmdPush(flags);
2776
2838
  printHelp();
2777
2839
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visualautomate/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Build and publish VisualAutomate plugins.",
5
5
  "license": "MIT",
6
6
  "type": "module",