@vizamodo/viza-cli 1.2.6

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 (36) hide show
  1. package/README.md +9 -0
  2. package/dist/bin/viza.js +35 -0
  3. package/dist/src/commands/admin/index.js +5 -0
  4. package/dist/src/commands/billing/index.js +5 -0
  5. package/dist/src/commands/bootstrap/index.js +5 -0
  6. package/dist/src/commands/design/index.js +5 -0
  7. package/dist/src/commands/dev/index.js +12 -0
  8. package/dist/src/commands/dev/login/aws/index.js +26 -0
  9. package/dist/src/commands/dev/login/index.js +8 -0
  10. package/dist/src/commands/manage/index.js +5 -0
  11. package/dist/src/commands/publish/index.js +5 -0
  12. package/dist/src/commands/super/index.js +5 -0
  13. package/dist/src/core/commandDescriptor.js +1 -0
  14. package/dist/src/core/dispatch.js +65 -0
  15. package/dist/src/core/renderHint.js +9 -0
  16. package/dist/src/core/version.js +53 -0
  17. package/dist/src/errors/handleError.js +6 -0
  18. package/dist/src/index.js +2 -0
  19. package/dist/src/platforms/aws/bootstrap.js +1 -0
  20. package/dist/src/platforms/aws/deploy.js +1 -0
  21. package/dist/src/platforms/aws/login.js +29 -0
  22. package/dist/src/platforms/aws/secret.js +1 -0
  23. package/dist/src/platforms/cloudflare/bootstrap.js +1 -0
  24. package/dist/src/platforms/cloudflare/deploy.js +1 -0
  25. package/dist/src/platforms/cloudflare/login.js +1 -0
  26. package/dist/src/platforms/registry.js +9 -0
  27. package/dist/src/policy/confirmDangerous.js +3 -0
  28. package/dist/src/policy/requireEnv.js +3 -0
  29. package/dist/src/policy/requireRole.js +3 -0
  30. package/dist/src/ui/banner.js +99 -0
  31. package/dist/src/ui/index.js +22 -0
  32. package/dist/src/ui/logRenderer.js +174 -0
  33. package/dist/src/ui/spinner.js +22 -0
  34. package/dist/src/ui/theme.js +1 -0
  35. package/dist/src/utils/fs.js +3 -0
  36. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # viza-cli
2
+
3
+ Unified CLI for Viza system.
4
+
5
+ Examples:
6
+
7
+ viza dev deploy worker
8
+ viza publish worker
9
+ viza billing report
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { handleError } from "../src/errors/handleError.js";
4
+ import { bootstrapCommand } from "../src/commands/bootstrap/index.js";
5
+ import { devCommand } from "../src/commands/dev/index.js";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ function readCliVersion() {
12
+ try {
13
+ const pkgPath = path.resolve(__dirname, "../package.json");
14
+ const raw = fs.readFileSync(pkgPath, "utf8");
15
+ const json = JSON.parse(raw);
16
+ return typeof json.version === "string" && json.version.trim() ? json.version.trim() : "dev";
17
+ }
18
+ catch {
19
+ return "dev";
20
+ }
21
+ }
22
+ const CLI_VERSION = readCliVersion();
23
+ const program = new Command();
24
+ program
25
+ .name("viza")
26
+ .description("Viza CLI")
27
+ .version(CLI_VERSION);
28
+ program.addCommand(devCommand());
29
+ program
30
+ .command("bootstrap")
31
+ .description("Bootstrap Viza configuration")
32
+ .action(async () => {
33
+ await bootstrapCommand();
34
+ });
35
+ program.parseAsync(process.argv).catch(handleError);
@@ -0,0 +1,5 @@
1
+ export const adminCommand = {
2
+ name: "admin",
3
+ description: "admin commands",
4
+ children: []
5
+ };
@@ -0,0 +1,5 @@
1
+ export const billingCommand = {
2
+ name: "billing",
3
+ description: "billing commands",
4
+ children: []
5
+ };
@@ -0,0 +1,5 @@
1
+ import { bootstrapGatewayConfigFromGitHub } from "@vizamodo/viza-dispatcher";
2
+ export async function bootstrapCommand() {
3
+ await bootstrapGatewayConfigFromGitHub();
4
+ console.log("✅ Bootstrap completed.");
5
+ }
@@ -0,0 +1,5 @@
1
+ export const designCommand = {
2
+ name: "design",
3
+ description: "design commands",
4
+ children: []
5
+ };
@@ -0,0 +1,12 @@
1
+ // src/commands/dev/index.ts
2
+ import { Command } from "commander";
3
+ import { devLoginCommand } from "./login/index.js";
4
+ const DEV_CTX = {
5
+ targetTeam: "viza-deployer",
6
+ };
7
+ export function devCommand(ctx = DEV_CTX) {
8
+ const cmd = new Command("dev");
9
+ cmd.description("Development commands");
10
+ cmd.addCommand(devLoginCommand(ctx));
11
+ return cmd;
12
+ }
@@ -0,0 +1,26 @@
1
+ // src/commands/dev/aws/index.ts
2
+ import { Command } from "commander";
3
+ import { awsLogin, awsCreateSsoUrl } from "../../../../platforms/aws/login.js";
4
+ export function devLoginAwsCommand(ctx) {
5
+ const aws = new Command("aws");
6
+ aws
7
+ .description("Login AWS via remote dispatcher (SSO/session bootstrap)")
8
+ .option("--status", "Check status only (do not dispatch new run)")
9
+ .action(async (opts) => {
10
+ await awsLogin({
11
+ targetTeam: ctx.targetTeam,
12
+ mode: opts.status ? "status" : "dispatch",
13
+ });
14
+ });
15
+ aws
16
+ .command("sso-url")
17
+ .description("Create AWS SSO login URL via remote dispatcher")
18
+ .option("--status", "Check status only (do not dispatch new run)")
19
+ .action(async (opts) => {
20
+ await awsCreateSsoUrl({
21
+ targetTeam: ctx.targetTeam,
22
+ mode: opts.status ? "status" : "dispatch",
23
+ });
24
+ });
25
+ return aws;
26
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { devLoginAwsCommand } from "./aws/index.js";
3
+ export function devLoginCommand(ctx) {
4
+ const cmd = new Command("login");
5
+ cmd.description("Login commands");
6
+ cmd.addCommand(devLoginAwsCommand(ctx));
7
+ return cmd;
8
+ }
@@ -0,0 +1,5 @@
1
+ export const manageCommand = {
2
+ name: "manage",
3
+ description: "manage commands",
4
+ children: []
5
+ };
@@ -0,0 +1,5 @@
1
+ export const publishCommand = {
2
+ name: "publish",
3
+ description: "publish commands",
4
+ children: []
5
+ };
@@ -0,0 +1,5 @@
1
+ export const superCommand = {
2
+ name: "super",
3
+ description: "super commands",
4
+ children: []
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { dispatcherDispatch, } from "@vizamodo/viza-dispatcher";
2
+ import { startSpinner, stopSpinner } from "../ui/spinner.js";
3
+ import { renderLog } from "../ui/logRenderer.js";
4
+ import { showDispatchBanner } from "../ui/banner.js";
5
+ import { getCliVersion } from "./version.js";
6
+ /**
7
+ * KISS log rendering.
8
+ * - success + hide => no log
9
+ * - otherwise => render log (if present)
10
+ */
11
+ function maybeRenderLog(result, policy) {
12
+ // KISS: only skip logs when success + hide.
13
+ if (result.status === "success" && policy === "hide") {
14
+ return;
15
+ }
16
+ if (result.logBuffer) {
17
+ renderLog(result.logBuffer, { status: result.status });
18
+ }
19
+ }
20
+ async function dispatchIntent(input, mode = "dispatch") {
21
+ const dispatchInput = {
22
+ intent: input.intent,
23
+ eventType: input.eventType,
24
+ payload: input.payload,
25
+ runnerLabel: input.runnerLabel,
26
+ };
27
+ const handle = await dispatcherDispatch(dispatchInput, {
28
+ auth: {
29
+ targetTeam: input.targetTeam,
30
+ },
31
+ }, mode);
32
+ return handle;
33
+ }
34
+ /**
35
+ * Dispatch and wait for completion.
36
+ *
37
+ * This is the recommended "one door" for most viza-cli commands
38
+ * so that waiting/log/result handling does not spread across the codebase.
39
+ */
40
+ export async function dispatchIntentAndWait(input, opts = {}) {
41
+ const policy = opts.log ?? "hide";
42
+ const mode = opts.mode ?? "dispatch";
43
+ const cliVersion = getCliVersion();
44
+ const meta = {
45
+ cliVersion,
46
+ subtitle: input.eventType,
47
+ clearScreen: true,
48
+ };
49
+ showDispatchBanner(input, meta);
50
+ const handle = await dispatchIntent(input, mode);
51
+ const spinner = startSpinner("Waiting for dispatch session");
52
+ try {
53
+ const result = await handle.wait();
54
+ stopSpinner(spinner, result.status === "success" ? "✅ Done" : "❌ Failed");
55
+ maybeRenderLog(result, policy);
56
+ if (result.status !== "success") {
57
+ throw new Error(`Dispatch failed: ${result.status}`);
58
+ }
59
+ return result;
60
+ }
61
+ catch (err) {
62
+ stopSpinner(spinner, "❌ Failed");
63
+ throw err;
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ export function renderHint(cmd) {
2
+ console.log("\n" + cmd.name + ": " + cmd.description + "\n");
3
+ if (cmd.children?.length) {
4
+ console.log("Available commands:");
5
+ for (const c of cmd.children) {
6
+ console.log(" • " + c.name + " — " + c.description);
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,53 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ let _cached;
5
+ /**
6
+ * Returns the current viza-cli version.
7
+ *
8
+ * - Prefers build-time injected `VIZA_CLI_VERSION`.
9
+ * - Falls back to reading nearest package.json.
10
+ * - Always returns a non-empty string ("dev" as last resort).
11
+ */
12
+ export function getCliVersion() {
13
+ if (_cached)
14
+ return _cached;
15
+ const injected = process.env.VIZA_CLI_VERSION;
16
+ if (injected && injected.trim()) {
17
+ _cached = injected.trim();
18
+ return _cached;
19
+ }
20
+ try {
21
+ const here = fileURLToPath(import.meta.url);
22
+ const dir = resolve(here, "..");
23
+ const candidates = [
24
+ resolve(dir, "../../package.json"),
25
+ resolve(dir, "../../../package.json"),
26
+ resolve(dir, "../../../../package.json"),
27
+ ];
28
+ let raw;
29
+ let usedPath;
30
+ for (const p of candidates) {
31
+ try {
32
+ raw = readFileSync(p, "utf8");
33
+ usedPath = p;
34
+ break;
35
+ }
36
+ catch {
37
+ // try next
38
+ }
39
+ }
40
+ if (!raw || !usedPath) {
41
+ throw new Error(`package.json not found (tried: ${candidates.join(", ")})`);
42
+ }
43
+ const json = JSON.parse(raw);
44
+ if (json.version && String(json.version).trim()) {
45
+ _cached = String(json.version).trim();
46
+ return _cached;
47
+ }
48
+ }
49
+ catch (err) {
50
+ }
51
+ _cached = "dev";
52
+ return _cached;
53
+ }
@@ -0,0 +1,6 @@
1
+ import { normalizeDispatcherError } from "@vizamodo/viza-dispatcher";
2
+ export function handleError(err) {
3
+ const e = normalizeDispatcherError(err);
4
+ console.error(e.message);
5
+ process.exit(e.exitCode);
6
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ // public exports (optional)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ // src/platforms/aws/login.ts
2
+ import { dispatchIntentAndWait } from "../../core/dispatch.js";
3
+ export async function awsLogin(ctx) {
4
+ const plainPayload = {
5
+ action: "login",
6
+ provider: "aws",
7
+ type: "sso",
8
+ };
9
+ await dispatchIntentAndWait({
10
+ intent: "hub",
11
+ eventType: "viza.aws.login",
12
+ targetTeam: ctx.targetTeam,
13
+ runnerLabel: "native",
14
+ payload: plainPayload,
15
+ }, { log: "show", mode: ctx.mode ?? "dispatch" });
16
+ }
17
+ export async function awsCreateSsoUrl(ctx) {
18
+ const plainPayload = {
19
+ action: "sso-url",
20
+ provider: "aws",
21
+ };
22
+ await dispatchIntentAndWait({
23
+ intent: "hub",
24
+ eventType: "viza.aws.sso-url",
25
+ targetTeam: ctx.targetTeam,
26
+ runnerLabel: "native",
27
+ payload: plainPayload,
28
+ }, { log: "show", mode: ctx.mode ?? "dispatch" });
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ // src/platforms/registry.ts
2
+ import { awsLogin } from "./aws/login.js";
3
+ export const registry = {
4
+ aws: {
5
+ login: awsLogin,
6
+ // deploy: awsDeploy,
7
+ },
8
+ // cloudflare: { login: cfLogin }
9
+ };
@@ -0,0 +1,3 @@
1
+ export async function confirmDangerous(message) {
2
+ console.log(message);
3
+ }
@@ -0,0 +1,3 @@
1
+ export function requireEnv(env) {
2
+ // enforced later
3
+ }
@@ -0,0 +1,3 @@
1
+ export function requireRole(role) {
2
+ // enforced later
3
+ }
@@ -0,0 +1,99 @@
1
+ import chalk from "chalk";
2
+ import figlet from "figlet";
3
+ const TEAM_BANNER_CONFIG = {
4
+ "viza-deployer": { title: "Viza Deployer", color: "cyanBright", env: "dev" },
5
+ "viza-designer": { title: "Viza Designer", color: "greenBright", env: "dev" },
6
+ "viza-billing": { title: "Viza Billing", color: "yellowBright", env: "dev" },
7
+ "viza-publisher": { title: "Viza Publisher", color: "redBright", env: "prod" },
8
+ "viza-manager": { title: "Viza Manager", color: "cyanBright", env: "dev" },
9
+ "viza-admin": { title: "Viza Administrator", color: "yellowBright", env: "sys" },
10
+ "viza-super": { title: "Viza Supervisor", color: "redBright", env: "sys" },
11
+ };
12
+ function formatEnvLabelForTeam(team) {
13
+ // Keep env values constrained to BannerOptions, while giving a clear mapping.
14
+ // dev: Development/Designer/Billing/Manager
15
+ // prod: Production
16
+ // sys: System/Root
17
+ if (team === "viza-publisher")
18
+ return "prod";
19
+ if (team === "viza-admin" || team === "viza-super")
20
+ return "sys";
21
+ return "dev";
22
+ }
23
+ function pickBannerConfig(targetTeam) {
24
+ const cfg = TEAM_BANNER_CONFIG[targetTeam];
25
+ if (cfg)
26
+ return cfg;
27
+ // Default: derive something reasonable and safe.
28
+ const prettyTitle = targetTeam
29
+ .split("-")
30
+ .map((x) => (x ? x[0].toUpperCase() + x.slice(1) : x))
31
+ .join(" ");
32
+ return {
33
+ title: prettyTitle || "Viza",
34
+ color: "cyanBright",
35
+ env: formatEnvLabelForTeam(targetTeam) ?? "dev",
36
+ };
37
+ }
38
+ /**
39
+ * Show a banner right before calling dispatchIntent.
40
+ * Maps targetTeam -> title/color/env, and renders meta (github login + viza-cli version).
41
+ */
42
+ export function showDispatchBanner(input, meta) {
43
+ const cfg = pickBannerConfig(input.targetTeam);
44
+ // Default subtitle: event type (or caller-provided subtitle)
45
+ const subtitle = meta?.subtitle ?? `Command: ${input.eventType}`;
46
+ showBanner({
47
+ title: cfg.title,
48
+ subtitle,
49
+ color: cfg.color,
50
+ env: cfg.env,
51
+ runner: input.runnerLabel === "native"
52
+ ? { type: "github" }
53
+ : {
54
+ type: "self-hosted",
55
+ label: input.runnerLabel,
56
+ },
57
+ meta: {
58
+ version: meta?.cliVersion,
59
+ },
60
+ clearScreen: meta?.clearScreen ?? true,
61
+ });
62
+ }
63
+ export function showBanner(opts) {
64
+ const { title, subtitle, color = "cyanBright", env, runner, meta, clearScreen = true, } = opts;
65
+ if (clearScreen) {
66
+ process.stdout.write("\x1Bc");
67
+ }
68
+ const bannerText = figlet.textSync(title, { font: "Standard" });
69
+ console.log(chalk[color](bannerText));
70
+ if (subtitle) {
71
+ console.log(chalk.gray("Command:"), chalk.magenta(subtitle));
72
+ }
73
+ // Environment line removed; replaced by user-friendly info line below
74
+ if (runner) {
75
+ if (runner.type === "github") {
76
+ console.log(chalk.gray("Runner:"), chalk.yellow("GitHub-hosted"));
77
+ }
78
+ else {
79
+ console.log(chalk.gray("Runner:"), chalk.yellow("self-hosted"), runner.label ? chalk.gray("| label:") : "", runner.label ? chalk.cyan(runner.label) : "");
80
+ }
81
+ }
82
+ // User-friendly info line
83
+ {
84
+ const parts = [];
85
+ if (meta?.version) {
86
+ parts.push(`Version: ${chalk.cyan(meta.version)}`);
87
+ }
88
+ if (env) {
89
+ const envColor = env === "prod" ? chalk.redBright :
90
+ env === "dev" ? chalk.greenBright :
91
+ chalk.yellowBright;
92
+ parts.push(`Environment: ${envColor(env.toUpperCase())}`);
93
+ }
94
+ if (parts.length) {
95
+ console.log(chalk.gray(parts.join(chalk.gray(" | "))));
96
+ }
97
+ }
98
+ console.log(chalk.gray("────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"));
99
+ }
@@ -0,0 +1,22 @@
1
+ // public exports (optional)
2
+ import { showBanner } from "./banner.js";
3
+ import { startSpinner, stopSpinner } from "./spinner.js";
4
+ import { renderLog } from "./logRenderer.js";
5
+ export function beginUi(opts) {
6
+ if (opts.banner) {
7
+ showBanner(opts.banner);
8
+ }
9
+ let spinner;
10
+ if (opts.spinnerMessage) {
11
+ spinner = startSpinner(opts.spinnerMessage);
12
+ }
13
+ return { spinner };
14
+ }
15
+ export function endUi(session, opts) {
16
+ if (session.spinner) {
17
+ stopSpinner(session.spinner, opts?.finalMessage);
18
+ }
19
+ }
20
+ export function renderArtifactLog(buffer, status) {
21
+ renderLog(buffer, { status });
22
+ }
@@ -0,0 +1,174 @@
1
+ import chalk from "chalk";
2
+ import AdmZip from "adm-zip";
3
+ /**
4
+ * Render deployment log artifact.
5
+ *
6
+ * This function:
7
+ * - Unzips workflow log artifact
8
+ * - Delegates step-level parsing to parseDeployLog
9
+ * - Prints a final status banner
10
+ *
11
+ * It does NOT:
12
+ * - fetch logs
13
+ * - manage spinner
14
+ * - exit process
15
+ */
16
+ export function renderLog(zipBuffer, options) {
17
+ if (!Buffer.isBuffer(zipBuffer)) {
18
+ throw new Error("Invalid log artifact: expected Buffer");
19
+ }
20
+ // Validate ZIP structure early
21
+ try {
22
+ new AdmZip(zipBuffer);
23
+ }
24
+ catch {
25
+ throw new Error("Invalid log artifact: not a ZIP file");
26
+ }
27
+ // Print detailed workflow log
28
+ parseAndPrintDeployLog(zipBuffer);
29
+ // Print final status banner
30
+ const status = options.status ?? "unknown";
31
+ let color = chalk.gray;
32
+ if (status === "success")
33
+ color = chalk.greenBright;
34
+ else if (status === "failure")
35
+ color = chalk.redBright;
36
+ else if (status === "cancelled")
37
+ color = chalk.yellowBright;
38
+ console.log(color(`\n────── DEPLOY STATUS: ${String(status).toUpperCase()} ─────────────────────────────────────────────────────────────────────────────────────────────\n`));
39
+ }
40
+ const RUNNER_TIMESTAMP_REGEX = /^\uFEFF?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s*/;
41
+ const MARKERS_TO_REMOVE = [
42
+ { type: "startsWith", value: "0_" },
43
+ { type: "endsWith", value: "system.txt" },
44
+ { type: "endsWith", value: "_Complete job.txt" },
45
+ { type: "includes", value: "_📤 Dispatch log collector" },
46
+ { type: "includes", value: "_Post " },
47
+ ];
48
+ export function parseAndPrintDeployLog(zipBuffer) {
49
+ const zip = new AdmZip(zipBuffer);
50
+ const entries = zip.getEntries();
51
+ // Sort entries like GitHub steps
52
+ const sorted = entries
53
+ .filter(e => !e.isDirectory)
54
+ .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true }));
55
+ // Filter unwanted files
56
+ const filtered = sorted.filter(e => {
57
+ const n = e.entryName;
58
+ return !MARKERS_TO_REMOVE.some(rule => {
59
+ if (rule.type === "startsWith")
60
+ return n.startsWith(rule.value);
61
+ if (rule.type === "endsWith")
62
+ return n.endsWith(rule.value);
63
+ if (rule.type === "includes")
64
+ return n.includes(rule.value);
65
+ return false;
66
+ });
67
+ });
68
+ const GROUP_START = /^##\[group\]\s*(.*)/;
69
+ const GROUP_END = /^##\[endgroup\]/;
70
+ // Group entries by job (top-level folder)
71
+ const jobs = new Map();
72
+ for (const e of filtered) {
73
+ const parts = e.entryName.split("/");
74
+ const jobName = parts.length > 1 ? parts[0] : "__root__";
75
+ if (!jobs.has(jobName))
76
+ jobs.set(jobName, []);
77
+ jobs.get(jobName).push(e);
78
+ }
79
+ const sortedJobs = [...jobs.entries()].sort((a, b) => {
80
+ const numA = parseInt(a[0]);
81
+ const numB = parseInt(b[0]);
82
+ return (isNaN(numA) ? 9999 : numA) - (isNaN(numB) ? 9999 : numB);
83
+ });
84
+ for (const [jobName, entriesOfJob] of sortedJobs) {
85
+ let printedJobName;
86
+ if (jobName === "__root__") {
87
+ printedJobName = "Global Steps";
88
+ }
89
+ else {
90
+ // Remove leading numbering like "1. ", "2. "
91
+ const cleaned = jobName.replace(/^[0-9]+\.\s*/, "");
92
+ // Replace dots with arrows for readability
93
+ const beautified = cleaned.replace(/\./g, " → ");
94
+ printedJobName = beautified.trim();
95
+ }
96
+ console.log(chalk.yellow(`\n\n────── ${printedJobName} ───────────────────────────────────────────────────────────────────────────────────────────────────────────\n`));
97
+ for (const entry of entriesOfJob) {
98
+ let stepTitle = entry.entryName.replace(".txt", "");
99
+ // Remove leading directory (job folder)
100
+ if (stepTitle.includes("/")) {
101
+ stepTitle = stepTitle.split("/").pop();
102
+ }
103
+ // Replace first "_" with ". "
104
+ const underscoreIndex = stepTitle.indexOf("_");
105
+ if (underscoreIndex > 0) {
106
+ const num = stepTitle.slice(0, underscoreIndex);
107
+ const title = stepTitle.slice(underscoreIndex + 1);
108
+ stepTitle = `${num}. ${title}`;
109
+ }
110
+ console.log(chalk.cyan(`\n────── ${stepTitle} ───────────────────────────────────────────────────────────────────────────────────────────────────────────`));
111
+ const raw = entry.getData().toString("utf8");
112
+ const lines = raw.split(/\r?\n/);
113
+ let inGroup = false;
114
+ let currentGroupTitle = "";
115
+ let displayIdx = 1;
116
+ for (let line of lines) {
117
+ // Remove timestamps & ANSI codes
118
+ line = line
119
+ .replace(RUNNER_TIMESTAMP_REGEX, "")
120
+ .replace(/\x1b\[[0-9;]*m/g, "");
121
+ if (!line.trim()) {
122
+ const ln = chalk.gray(String(displayIdx++).padStart(3, " "));
123
+ console.log(`${ln} `);
124
+ continue;
125
+ }
126
+ const groupStart = line.match(GROUP_START);
127
+ if (groupStart) {
128
+ inGroup = true;
129
+ currentGroupTitle = groupStart[1] || "";
130
+ console.log(chalk.magenta(`\n▼`), chalk.whiteBright(`${currentGroupTitle}`));
131
+ continue;
132
+ }
133
+ if (GROUP_END.test(line)) {
134
+ inGroup = false;
135
+ continue;
136
+ }
137
+ const ln = chalk.gray(String(displayIdx++).padStart(5, " "));
138
+ if (inGroup) {
139
+ // Inside a group block — indent and highlight commands
140
+ if (/^Run /.test(line)) {
141
+ console.log(`${ln} ${chalk.blue(line)}`);
142
+ }
143
+ else if (/^\[ERROR\]/.test(line)) {
144
+ console.log(`${ln} ${chalk.redBright(line)}`);
145
+ }
146
+ else if (/\[INFO\]/.test(line)) {
147
+ console.log(`${ln} ${chalk.green(line)}`);
148
+ }
149
+ else {
150
+ console.log(`${ln} ${chalk.white(line)}`);
151
+ }
152
+ continue;
153
+ }
154
+ // Normal (non-group) line
155
+ if (/##\[error\]/i.test(line)) {
156
+ console.log(`${ln} ${chalk.redBright(line)}`);
157
+ }
158
+ else if (/^Run /.test(line)) {
159
+ console.log(`${ln} ${chalk.blueBright(line)}`);
160
+ }
161
+ else if (/^\[ERROR\]/.test(line)) {
162
+ console.log(`${ln} ${chalk.redBright(line)}`);
163
+ }
164
+ else if (/\[INFO\]/.test(line)) {
165
+ console.log(`${ln} ${chalk.green(line)}`);
166
+ }
167
+ else {
168
+ console.log(`${ln} ${chalk.white(line)}`);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ return true;
174
+ }
@@ -0,0 +1,22 @@
1
+ import chalk from 'chalk';
2
+ export function startSpinner(message, delayX = 500) {
3
+ const spinner = ['|', '/', '-', '\\'];
4
+ const dotStages = ['. ', '.. ', '...'];
5
+ let i = 0;
6
+ let dotIdx = 0;
7
+ const startTime = Date.now();
8
+ return setInterval(() => {
9
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
10
+ const dots = dotStages[dotIdx % dotStages.length];
11
+ process.stdout.write(`\r${spinner[i % spinner.length]} ${message} [${chalk.magenta(`${elapsed}s`)}] ${dots}`);
12
+ i++;
13
+ dotIdx++;
14
+ }, delayX);
15
+ }
16
+ export function stopSpinner(spinner, finalMessage) {
17
+ clearInterval(spinner);
18
+ process.stdout.write('\r\x1b[2K');
19
+ if (finalMessage) {
20
+ console.log(finalMessage);
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ export const theme = {};
@@ -0,0 +1,3 @@
1
+ export function listDirs() {
2
+ return [];
3
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@vizamodo/viza-cli",
3
+ "version": "1.2.6",
4
+ "type": "module",
5
+ "description": "Viza unified command line interface",
6
+ "bin": {
7
+ "viza": "dist/bin/viza.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build",
15
+ "dev": "ts-node bin/viza.ts",
16
+ "release:prod": "rm -rf dist && npx npm-check-updates -u && npm install && git add package.json package-lock.json && git commit -m 'chore(deps): auto update dependencies before release' || echo 'No changes' && node versioning.js && npm login && npm publish --tag latest --access public && git push"
17
+ },
18
+ "dependencies": {
19
+ "@vizamodo/viza-dispatcher": "^1.4.23",
20
+ "adm-zip": "^0.5.16",
21
+ "chalk": "^5.6.2",
22
+ "commander": "^14.0.2",
23
+ "figlet": "^1.9.4"
24
+ },
25
+ "devDependencies": {
26
+ "@types/adm-zip": "^0.5.7",
27
+ "@types/figlet": "^1.7.0",
28
+ "@types/node": "^25.0.9",
29
+ "ts-node": "^10.9.2",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }