@visualautomate/cli 0.5.1 → 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.
- package/README.md +4 -0
- package/dist/index.js +81 -37
- 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:
|
package/dist/index.js
CHANGED
|
@@ -1823,12 +1823,12 @@ function entryIn(dir) {
|
|
|
1823
1823
|
return ENTRIES.map((file2) => join(dir, file2)).find((file2) => existsSync(file2)) ?? null;
|
|
1824
1824
|
}
|
|
1825
1825
|
function matchFolders(folders, name) {
|
|
1826
|
-
const
|
|
1827
|
-
const exact = folders.filter((folder) => folder.toLowerCase() ===
|
|
1826
|
+
const wanted2 = name.trim().toLowerCase();
|
|
1827
|
+
const exact = folders.filter((folder) => folder.toLowerCase() === wanted2);
|
|
1828
1828
|
if (exact.length > 0) return exact;
|
|
1829
|
-
const prefix = folders.filter((folder) => folder.toLowerCase().startsWith(
|
|
1829
|
+
const prefix = folders.filter((folder) => folder.toLowerCase().startsWith(wanted2));
|
|
1830
1830
|
if (prefix.length > 0) return prefix;
|
|
1831
|
-
return folders.filter((folder) => folder.toLowerCase().includes(
|
|
1831
|
+
return folders.filter((folder) => folder.toLowerCase().includes(wanted2));
|
|
1832
1832
|
}
|
|
1833
1833
|
function findTargets(cwd, only) {
|
|
1834
1834
|
const dir = resolve(cwd);
|
|
@@ -2087,6 +2087,38 @@ async function savePersisted(dir, files, state) {
|
|
|
2087
2087
|
|
|
2088
2088
|
// src/docker.ts
|
|
2089
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
|
|
2090
2122
|
var IMAGE = "ghcr.io/visualautomate/plugin-sandbox";
|
|
2091
2123
|
var TIERS = {
|
|
2092
2124
|
standard: { cpus: "0.25", memory: "1g" },
|
|
@@ -2119,6 +2151,9 @@ function dockerArgs(run) {
|
|
|
2119
2151
|
"ALL",
|
|
2120
2152
|
"--security-opt",
|
|
2121
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(),
|
|
2122
2157
|
...run.user ? ["--user", `${run.user.uid}:${run.user.gid}`] : [],
|
|
2123
2158
|
run.image,
|
|
2124
2159
|
...run.args
|
|
@@ -2142,16 +2177,18 @@ function dockerProblem() {
|
|
|
2142
2177
|
function explainExit(code, tier) {
|
|
2143
2178
|
if (code === 137) {
|
|
2144
2179
|
const next = Object.keys(TIERS)[Object.keys(TIERS).indexOf(tier) + 1];
|
|
2145
|
-
return
|
|
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
|
+
);
|
|
2146
2183
|
}
|
|
2147
|
-
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);
|
|
2148
2185
|
return null;
|
|
2149
2186
|
}
|
|
2150
2187
|
function ensureDocker(say, waitMs = 12e4) {
|
|
2151
2188
|
const problem = dockerProblem();
|
|
2152
2189
|
if (!problem) return null;
|
|
2153
2190
|
if (problem.includes("not installed")) return problem;
|
|
2154
|
-
say("Docker is not running. Starting Docker Desktop\u2026");
|
|
2191
|
+
say(yellow("Docker is not running. Starting Docker Desktop\u2026"));
|
|
2155
2192
|
const started = spawnSync("docker", ["desktop", "start"], { encoding: "utf8" });
|
|
2156
2193
|
if (started.status !== 0) {
|
|
2157
2194
|
say("Could not start it from here \u2014 start Docker Desktop yourself; this will carry on when it is up.");
|
|
@@ -2185,7 +2222,7 @@ function runInDocker(run) {
|
|
|
2185
2222
|
}
|
|
2186
2223
|
|
|
2187
2224
|
// src/index.ts
|
|
2188
|
-
var VERSION = true ? "0.5.
|
|
2225
|
+
var VERSION = true ? "0.5.2" : "0.0.0-dev";
|
|
2189
2226
|
var DEFAULT_API_URL = "https://visualautomate.com";
|
|
2190
2227
|
function configPath() {
|
|
2191
2228
|
return join3(homedir(), ".visualautomate", "config.json");
|
|
@@ -2212,7 +2249,7 @@ async function tokenFor(flags) {
|
|
|
2212
2249
|
return { token, apiUrl };
|
|
2213
2250
|
}
|
|
2214
2251
|
function fail(message) {
|
|
2215
|
-
console.error(message);
|
|
2252
|
+
console.error(red(message));
|
|
2216
2253
|
throw new SilentExit();
|
|
2217
2254
|
}
|
|
2218
2255
|
var SilentExit = class extends Error {
|
|
@@ -2289,8 +2326,8 @@ async function cmdLogin(flags) {
|
|
|
2289
2326
|
});
|
|
2290
2327
|
const who = await whoami(apiUrl, token);
|
|
2291
2328
|
await writeConfig({ token, apiUrl, email: who.email });
|
|
2292
|
-
ok(`
|
|
2293
|
-
\u2713 Signed in as ${who.email}`);
|
|
2329
|
+
ok(green(`
|
|
2330
|
+
\u2713 Signed in as ${who.email}`));
|
|
2294
2331
|
ok(` Token stored in ${configPath()}`);
|
|
2295
2332
|
}
|
|
2296
2333
|
async function whoami(apiUrl, token) {
|
|
@@ -2376,9 +2413,9 @@ visualautomate push # publish it to your account
|
|
|
2376
2413
|
\`test.json\` is the input, config and state \`test\` runs with.
|
|
2377
2414
|
`
|
|
2378
2415
|
);
|
|
2379
|
-
ok(`
|
|
2416
|
+
ok(green(`
|
|
2380
2417
|
\u2713 Created ${name} from the "${templateName}" template
|
|
2381
|
-
`);
|
|
2418
|
+
`));
|
|
2382
2419
|
ok(" cd " + name);
|
|
2383
2420
|
ok(" visualautomate test");
|
|
2384
2421
|
ok(" visualautomate push\n");
|
|
@@ -2412,12 +2449,12 @@ function line(indent, text) {
|
|
|
2412
2449
|
async function runTarget(target, flags, several) {
|
|
2413
2450
|
const say = (text) => line(several, text);
|
|
2414
2451
|
if (several) ok(`
|
|
2415
|
-
|
|
2452
|
+
${bold(`\u25B8 ${target.name}`)}`);
|
|
2416
2453
|
try {
|
|
2417
2454
|
const code = await codeFor(target);
|
|
2418
2455
|
const check = validatePluginCode(code);
|
|
2419
2456
|
if (!check.valid) {
|
|
2420
|
-
say(
|
|
2457
|
+
say(`${red("\u2717 refused")} ${check.error}`);
|
|
2421
2458
|
return false;
|
|
2422
2459
|
}
|
|
2423
2460
|
const settings = await readTestFile(target.dir, flags.input);
|
|
@@ -2431,7 +2468,7 @@ async function runTarget(target, flags, several) {
|
|
|
2431
2468
|
const manifest = parseJsonc(await readFile3(manifestPath, "utf8")).value;
|
|
2432
2469
|
outputs = manifest.outputs ?? [];
|
|
2433
2470
|
} catch (error) {
|
|
2434
|
-
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)}`));
|
|
2435
2472
|
return false;
|
|
2436
2473
|
}
|
|
2437
2474
|
}
|
|
@@ -2457,33 +2494,37 @@ async function runTarget(target, flags, several) {
|
|
|
2457
2494
|
const port = result.output?.output ?? null;
|
|
2458
2495
|
const expected = settings.expectOutput;
|
|
2459
2496
|
const passed = result.status === "success" && (!expected || port === expected);
|
|
2460
|
-
|
|
2461
|
-
|
|
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)}`);
|
|
2462
2500
|
if (result.output) {
|
|
2463
|
-
|
|
2464
|
-
say(
|
|
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 ")}`);
|
|
2465
2504
|
}
|
|
2466
|
-
if (result.files.size > 0) say(
|
|
2467
|
-
if (result.state.size > 0) say(
|
|
2468
|
-
for (const entry of result.logs ?? []) say(
|
|
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}`);
|
|
2469
2508
|
for (const [host, enforced] of denied) {
|
|
2470
2509
|
say(
|
|
2471
|
-
enforced ?
|
|
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.`
|
|
2472
2511
|
);
|
|
2473
2512
|
}
|
|
2474
2513
|
if (persist) {
|
|
2475
2514
|
const refused = await savePersisted(target.dir, result.files, result.state);
|
|
2476
|
-
say(
|
|
2477
|
-
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`));
|
|
2478
2517
|
}
|
|
2479
2518
|
if (port && outputs.length > 0 && !outputs.includes(port)) {
|
|
2480
2519
|
say(
|
|
2481
|
-
|
|
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
|
+
)
|
|
2482
2523
|
);
|
|
2483
2524
|
}
|
|
2484
2525
|
return passed;
|
|
2485
2526
|
} catch (error) {
|
|
2486
|
-
say(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
|
|
2527
|
+
say(red(`\u2717 ${error instanceof Error ? error.message : String(error)}`));
|
|
2487
2528
|
return false;
|
|
2488
2529
|
}
|
|
2489
2530
|
}
|
|
@@ -2492,8 +2533,11 @@ async function runTargets(targets, flags) {
|
|
|
2492
2533
|
for (const target of targets) {
|
|
2493
2534
|
if (await runTarget(target, flags, targets.length > 1)) passed++;
|
|
2494
2535
|
}
|
|
2495
|
-
if (targets.length > 1)
|
|
2496
|
-
|
|
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
|
+
}
|
|
2497
2541
|
return passed === targets.length;
|
|
2498
2542
|
}
|
|
2499
2543
|
function packageLoader(dir) {
|
|
@@ -2559,7 +2603,7 @@ function sayWhatRuns(targets, picked) {
|
|
|
2559
2603
|
return;
|
|
2560
2604
|
}
|
|
2561
2605
|
ok(`Running ${targets.length} modules: ${targets.map((t) => t.name.replace(/^modules\//, "")).join(", ")}`);
|
|
2562
|
-
if (!picked) ok(`One of them: vsa dev ${targets[0].name.replace(/^modules\//, "")}`);
|
|
2606
|
+
if (!picked) ok(dim(`One of them: vsa dev ${targets[0].name.replace(/^modules\//, "")}`));
|
|
2563
2607
|
}
|
|
2564
2608
|
async function cmdTest(args, flags) {
|
|
2565
2609
|
const targets = targetsOrFail(args, flags);
|
|
@@ -2575,11 +2619,11 @@ async function cmdDev(args, flags) {
|
|
|
2575
2619
|
if (inSandbox) prepareSandbox(flags);
|
|
2576
2620
|
const header = () => {
|
|
2577
2621
|
process.stdout.write("\x1Bc");
|
|
2578
|
-
ok(
|
|
2622
|
+
ok(`${bold("vsa dev")} ${dim(`\u2014 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \u2014 Ctrl+C to stop`)}`);
|
|
2579
2623
|
};
|
|
2580
2624
|
const run = async (chosen) => inSandbox ? runTargetsInDocker(chosen, targets, flags) : runTargets(chosen, flags);
|
|
2581
2625
|
header();
|
|
2582
|
-
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"));
|
|
2583
2627
|
sayWhatRuns(targets, Boolean(picked));
|
|
2584
2628
|
await run(targets);
|
|
2585
2629
|
const ignored = [`${sep2}node_modules${sep2}`, `${sep2}.git${sep2}`, `${sep2}dist${sep2}`, `${sep2}.visualautomate${sep2}`];
|
|
@@ -2598,14 +2642,14 @@ async function cmdDev(args, flags) {
|
|
|
2598
2642
|
targets = findTargets(root, picked);
|
|
2599
2643
|
} catch (error) {
|
|
2600
2644
|
header();
|
|
2601
|
-
ok(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
|
|
2645
|
+
ok(red(`\u2717 ${error instanceof Error ? error.message : String(error)}`));
|
|
2602
2646
|
running = false;
|
|
2603
2647
|
return;
|
|
2604
2648
|
}
|
|
2605
2649
|
const hit = changed.map((path) => targetForPath(targets, path));
|
|
2606
2650
|
const chosen = hit.some((t) => t === null) ? targets : [...new Set(hit)];
|
|
2607
2651
|
header();
|
|
2608
|
-
ok(`changed: ${changed.map((p) => relative3(root, p)).join(", ")}`);
|
|
2652
|
+
ok(dim(`changed: ${changed.map((p) => relative3(root, p)).join(", ")}`));
|
|
2609
2653
|
await run(chosen);
|
|
2610
2654
|
running = false;
|
|
2611
2655
|
};
|
|
@@ -2707,8 +2751,8 @@ ${text.slice(0, 500)}`);
|
|
|
2707
2751
|
if (!res.ok || !data.success) {
|
|
2708
2752
|
fail(`Push refused: ${data.error ?? `status ${res.status}`}`);
|
|
2709
2753
|
}
|
|
2710
|
-
ok(`
|
|
2711
|
-
\u2713 Published (id: ${data.pluginId})`);
|
|
2754
|
+
ok(green(`
|
|
2755
|
+
\u2713 Published (id: ${data.pluginId})`));
|
|
2712
2756
|
ok(` A version snapshot was saved, so you can roll back from the dashboard.`);
|
|
2713
2757
|
}
|
|
2714
2758
|
function printHelp() {
|