@viza-cli/app 1.5.27

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 +25 -0
  2. package/dist/bin/viza.js +5 -0
  3. package/dist/src/cli/options.js +6 -0
  4. package/dist/src/cli/program.js +23 -0
  5. package/dist/src/cli/resolveOptions.js +9 -0
  6. package/dist/src/commands/aws/register.js +4 -0
  7. package/dist/src/commands/aws/rolesanywhere/bootstrap/bootstrap.js +46 -0
  8. package/dist/src/commands/aws/rolesanywhere/bootstrap/register.js +13 -0
  9. package/dist/src/commands/aws/rolesanywhere/enable-profiles/enable-profiles.js +46 -0
  10. package/dist/src/commands/aws/rolesanywhere/enable-profiles/register.js +13 -0
  11. package/dist/src/commands/aws/rolesanywhere/register.js +12 -0
  12. package/dist/src/commands/aws/rolesanywhere/rollback/rollback.js +1 -0
  13. package/dist/src/commands/aws/rolesanywhere/rotate/rotate.js +1 -0
  14. package/dist/src/commands/bootstrap/index.js +5 -0
  15. package/dist/src/commands/bootstrap/register.js +16 -0
  16. package/dist/src/commands/login/aws/aws.js +87 -0
  17. package/dist/src/commands/login/aws/register.js +15 -0
  18. package/dist/src/commands/login/register.js +4 -0
  19. package/dist/src/commands/whoami/index.js +69 -0
  20. package/dist/src/context/env.js +13 -0
  21. package/dist/src/context/hubIntent.js +8 -0
  22. package/dist/src/core/commandDescriptor.js +1 -0
  23. package/dist/src/core/dispatch.js +130 -0
  24. package/dist/src/core/renderHint.js +9 -0
  25. package/dist/src/core/resolveExecutionMode.js +5 -0
  26. package/dist/src/core/version.js +148 -0
  27. package/dist/src/errors/handleError.js +6 -0
  28. package/dist/src/types/cli.js +1 -0
  29. package/dist/src/ui/banner.js +75 -0
  30. package/dist/src/ui/index.js +22 -0
  31. package/dist/src/ui/logRenderer.js +174 -0
  32. package/dist/src/ui/spinner.js +22 -0
  33. package/dist/src/ui/sso/awsLoginMenu.js +38 -0
  34. package/dist/src/ui/theme.js +1 -0
  35. package/dist/src/utils/fs.js +3 -0
  36. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # viza-cli
2
+
3
+ Unified CLI for Viza system.
4
+
5
+ ## Install
6
+
7
+ Recommended (global install):
8
+
9
+ ```bash
10
+ npm i -g @@viza-cli/app
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ viza --help
17
+ ```
18
+
19
+ ## Examples
20
+
21
+ ```bash
22
+ viza dev deploy worker
23
+ viza publish worker
24
+ viza billing report
25
+ ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { createProgram } from "../src/cli/program.js";
3
+ import { handleError } from "../src/errors/handleError.js";
4
+ const program = createProgram();
5
+ program.parseAsync(process.argv).catch(handleError);
@@ -0,0 +1,6 @@
1
+ export function registerGlobalOptions(program) {
2
+ program
3
+ .option("--status", "Show status only (no execution)")
4
+ .option("--remove-log", "Remove execution logs after completion", false)
5
+ .option("--self-hosted", "Use self-hosted runner (viza-builder)", false);
6
+ }
@@ -0,0 +1,23 @@
1
+ import { Command } from "commander";
2
+ import { getCliVersion } from "../core/version.js";
3
+ import { registerGlobalOptions } from "./options.js";
4
+ import { registerLoginCommand } from "../commands/login/register.js";
5
+ import { registerBootstrapCommand } from "../commands/bootstrap/register.js";
6
+ import { whoamiCommand } from "../commands/whoami/index.js";
7
+ import { registerAwsCommand } from "../commands/aws/register.js";
8
+ export function createProgram() {
9
+ const program = new Command();
10
+ program
11
+ .name("viza")
12
+ .description("Viza Command Line Interface")
13
+ .version(getCliVersion());
14
+ registerGlobalOptions(program);
15
+ registerBootstrapCommand(program);
16
+ registerLoginCommand(program);
17
+ registerAwsCommand(program);
18
+ program
19
+ .command("whoami")
20
+ .description("Show current GitHub identity and Viza team memberships (local only)")
21
+ .action(whoamiCommand);
22
+ return program;
23
+ }
@@ -0,0 +1,9 @@
1
+ export function getResolvedOptions(command) {
2
+ let allOptions = {};
3
+ let current = command;
4
+ while (current) {
5
+ allOptions = { ...current.opts(), ...allOptions };
6
+ current = current.parent;
7
+ }
8
+ return allOptions;
9
+ }
@@ -0,0 +1,4 @@
1
+ import { registerAwsRolesAnywhereCommand } from "./rolesanywhere/register.js";
2
+ export function registerAwsCommand(program) {
3
+ registerAwsRolesAnywhereCommand(program);
4
+ }
@@ -0,0 +1,46 @@
1
+ import { resolveEnv } from "../../../../context/env.js";
2
+ import { resolveHubIntent } from "../../../../context/hubIntent.js";
3
+ import { dispatchIntentAndWait } from "../../../../core/dispatch.js";
4
+ /**
5
+ * Target teams for `viza aws rolesanywhere bootstrap`.
6
+ * CLI-only fail-fast UX constraint.
7
+ * NOT a policy and MUST NOT be sent to gateway.
8
+ */
9
+ const TARGET_TEAMS = {
10
+ dev: ["viza-super"],
11
+ prod: ["viza-super"],
12
+ };
13
+ /**
14
+ * viza aws rolesanywhere bootstrap
15
+ *
16
+ * Flow:
17
+ * 1) Resolve env
18
+ * 2) Resolve hub intent
19
+ * 3) Derive allowed teams (CLI UX only)
20
+ * 4) Dispatch frozen intent
21
+ */
22
+ export async function bootstrapAwsRolesAnywhereCommand(options) {
23
+ // 1) Resolve environment
24
+ const env = resolveEnv(options);
25
+ const intent = resolveHubIntent(env);
26
+ // 2) Resolve allowed teams (no status mode for bootstrap)
27
+ const allowedTeams = TARGET_TEAMS[env];
28
+ // 3) Dispatch intent (freeze)
29
+ await dispatchIntentAndWait({
30
+ intent,
31
+ commandType: "aws.rolesanywhere.bootstrap",
32
+ infraKey: "aws",
33
+ targetEnv: env,
34
+ allowedTeams,
35
+ // Canonical CLI contract (explicit, non-magical)
36
+ selfHosted: options.selfHosted === true,
37
+ keepLog: options.removeLog !== true,
38
+ flowGates: {
39
+ secrets: true,
40
+ },
41
+ payload: {}
42
+ }, {
43
+ status: options.status === true,
44
+ log: "show",
45
+ });
46
+ }
@@ -0,0 +1,13 @@
1
+ import { bootstrapAwsRolesAnywhereCommand } from "./bootstrap.js";
2
+ import { getResolvedOptions } from "../../../../cli/resolveOptions.js";
3
+ export function registerAwsRolesAnywhereBootstrap(program) {
4
+ program
5
+ .command("bootstrap")
6
+ .description("Bootstrap AWS RolesAnywhere infrastructure")
7
+ .option("--prod", "Use production environment")
8
+ .option("--dev", "Use development environment")
9
+ .action(async (_opts, command) => {
10
+ const fullOpts = getResolvedOptions(command);
11
+ await bootstrapAwsRolesAnywhereCommand(fullOpts);
12
+ });
13
+ }
@@ -0,0 +1,46 @@
1
+ import { resolveEnv } from "../../../../context/env.js";
2
+ import { resolveHubIntent } from "../../../../context/hubIntent.js";
3
+ import { dispatchIntentAndWait } from "../../../../core/dispatch.js";
4
+ /**
5
+ * Target teams for `viza aws rolesanywhere enable-profiles`.
6
+ * CLI-only fail-fast UX constraint.
7
+ * NOT a policy and MUST NOT be sent to gateway.
8
+ */
9
+ const TARGET_TEAMS = {
10
+ dev: ["viza-super"],
11
+ prod: ["viza-super"],
12
+ };
13
+ /**
14
+ * viza aws rolesanywhere enable-profiles
15
+ *
16
+ * Flow:
17
+ * 1) Resolve env
18
+ * 2) Resolve hub intent
19
+ * 3) Derive allowed teams (CLI UX only)
20
+ * 4) Dispatch frozen intent
21
+ */
22
+ export async function enableProfilesAwsRolesAnywhereCommand(options) {
23
+ // 1) Resolve environment
24
+ const env = resolveEnv(options);
25
+ const intent = resolveHubIntent(env);
26
+ // 2) Resolve allowed teams (no status mode for enable-profiles)
27
+ const allowedTeams = TARGET_TEAMS[env];
28
+ // 3) Dispatch intent (freeze)
29
+ await dispatchIntentAndWait({
30
+ intent,
31
+ commandType: "aws.rolesanywhere.enable-profiles",
32
+ infraKey: "aws",
33
+ targetEnv: env,
34
+ allowedTeams,
35
+ // Canonical CLI contract (explicit, non-magical)
36
+ selfHosted: options.selfHosted === true,
37
+ keepLog: options.removeLog !== true,
38
+ flowGates: {
39
+ secrets: false,
40
+ },
41
+ payload: {}
42
+ }, {
43
+ status: options.status === true,
44
+ log: "show",
45
+ });
46
+ }
@@ -0,0 +1,13 @@
1
+ import { enableProfilesAwsRolesAnywhereCommand } from "./enable-profiles.js";
2
+ import { getResolvedOptions } from "../../../../cli/resolveOptions.js";
3
+ export function registerAwsRolesAnywhereEnableProfiles(program) {
4
+ program
5
+ .command("enable-profiles")
6
+ .description("Enable AWS RolesAnywhere profiles")
7
+ .option("--prod", "Use production environment")
8
+ .option("--dev", "Use development environment")
9
+ .action(async (_opts, command) => {
10
+ const fullOpts = getResolvedOptions(command);
11
+ await enableProfilesAwsRolesAnywhereCommand(fullOpts);
12
+ });
13
+ }
@@ -0,0 +1,12 @@
1
+ import { registerAwsRolesAnywhereBootstrap } from "./bootstrap/register.js";
2
+ import { registerAwsRolesAnywhereEnableProfiles } from "./enable-profiles/register.js";
3
+ export function registerAwsRolesAnywhereCommand(program) {
4
+ const aws = program.command("aws").description("AWS related commands");
5
+ const rolesanywhere = aws
6
+ .command("rolesanywhere")
7
+ .description("AWS RolesAnywhere operations");
8
+ registerAwsRolesAnywhereBootstrap(rolesanywhere);
9
+ registerAwsRolesAnywhereEnableProfiles(rolesanywhere);
10
+ // registerAwsRolesAnywhereRotate(rolesanywhere);
11
+ // registerAwsRolesAnywhereRollback(rolesanywhere);
12
+ }
@@ -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,16 @@
1
+ import { bootstrapCommand } from "./index.js";
2
+ /**
3
+ * Register `viza bootstrap` commands
4
+ */
5
+ export function registerBootstrapCommand(program) {
6
+ const cmd = program
7
+ .command("bootstrap")
8
+ .description("Bootstrap infrastructure or system components");
9
+ // Default bootstrap
10
+ cmd.action(async (_opts, command) => {
11
+ await bootstrapCommand();
12
+ });
13
+ // ⬇️ Sau này mở rộng rất dễ:
14
+ // registerBootstrapAwsCommand(cmd);
15
+ // registerBootstrapCloudflareCommand(cmd);
16
+ }
@@ -0,0 +1,87 @@
1
+ import { resolveEnv } from "../../../context/env.js";
2
+ import { resolveHubIntent } from "../../../context/hubIntent.js";
3
+ import { dispatchIntentAndWait } from "../../../core/dispatch.js";
4
+ import { showSsoLinkMenu } from "../../../ui/sso/awsLoginMenu.js";
5
+ function parseAwsLoginResult(buffer) {
6
+ try {
7
+ const json = JSON.parse(buffer.toString("utf8"));
8
+ if (typeof json?.loginUrl === "string" &&
9
+ typeof json?.shortUrl === "string" &&
10
+ typeof json?.ttlHours === "number") {
11
+ return json;
12
+ }
13
+ throw new Error("invalid_aws_login_result_shape");
14
+ }
15
+ catch (err) {
16
+ throw new Error("failed_to_parse_aws_login_result");
17
+ }
18
+ }
19
+ /**
20
+ * Target teams for `viza login aws`.
21
+ * This is a CLI-only UX constraint for fail-fast validation.
22
+ * NOT a policy and MUST NOT be sent to gateway.
23
+ */
24
+ const TARGET_TEAMS = {
25
+ "dev": [
26
+ "viza-deployer",
27
+ "viza-manager",
28
+ "viza-admin",
29
+ "viza-super"
30
+ ],
31
+ "prod": [
32
+ "viza-publisher",
33
+ "viza-admin",
34
+ "viza-super"
35
+ ]
36
+ };
37
+ /**
38
+ * viza login aws
39
+ *
40
+ * Flow:
41
+ * 1) Resolve env (deterministic)
42
+ * 2) Resolve user identity (trusted via gh auth)
43
+ * 3) CLI pre-check against target teams (fail-fast UX)
44
+ * 4) Derive ONE valid team (deterministic)
45
+ * 5) Dispatch frozen intent to gateway
46
+ */
47
+ export async function loginAwsCommand(options) {
48
+ // 1) Resolve environment
49
+ const env = resolveEnv(options);
50
+ const intent = resolveHubIntent(env);
51
+ // Resolve allowed teams
52
+ // - Dispatch mode: restrict by targetEnv
53
+ // - Status mode: allow union of all env teams (read-only query)
54
+ const allowedTeams = options.status === true && env === "dev"
55
+ ? Array.from(new Set([
56
+ ...TARGET_TEAMS.dev,
57
+ ...TARGET_TEAMS.prod,
58
+ ]))
59
+ : TARGET_TEAMS[env];
60
+ // 5) Dispatch intent (freeze)
61
+ const result = await dispatchIntentAndWait({
62
+ intent,
63
+ commandType: "aws.login",
64
+ infraKey: "aws",
65
+ targetEnv: env,
66
+ allowedTeams,
67
+ selfHosted: options.selfHosted === true,
68
+ keepLog: options.removeLog !== true,
69
+ flowGates: {
70
+ secrets: false,
71
+ },
72
+ payload: {}
73
+ }, {
74
+ status: options.status === true,
75
+ log: "hide",
76
+ });
77
+ if (!result)
78
+ return;
79
+ if (result.status !== "success") {
80
+ return;
81
+ }
82
+ if (!result.resultBuffer) {
83
+ return;
84
+ }
85
+ const awsResult = parseAwsLoginResult(result.resultBuffer);
86
+ await showSsoLinkMenu(awsResult.loginUrl, awsResult.shortUrl, awsResult.ttlHours);
87
+ }
@@ -0,0 +1,15 @@
1
+ import { loginAwsCommand } from "./aws.js";
2
+ import { getResolvedOptions } from "../../../cli/resolveOptions.js";
3
+ export function registerLoginAwsCommand(program) {
4
+ program
5
+ .command("login")
6
+ .description("Login to cloud providers")
7
+ .command("aws")
8
+ .description("Login to AWS")
9
+ .option("--prod", "Use production environment")
10
+ .option("--dev", "Use development environment")
11
+ .action(async (_opts, command) => {
12
+ const fullOpts = getResolvedOptions(command);
13
+ await loginAwsCommand(fullOpts);
14
+ });
15
+ }
@@ -0,0 +1,4 @@
1
+ import { registerLoginAwsCommand } from "./aws/register.js";
2
+ export function registerLoginCommand(program) {
3
+ registerLoginAwsCommand(program);
4
+ }
@@ -0,0 +1,69 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import chalk from "chalk";
4
+ const execFileAsync = promisify(execFile);
5
+ // ⚠️ UX-only role descriptions (NOT security enforcement)
6
+ const TEAM_DESCRIPTIONS = {
7
+ "viza-designer": "Product designer. Full access to sign in, view, edit, and delete designs in Figma.",
8
+ "viza-deployer": "Software developer. Builds mobile / IoT / web applications and deploys resources to the DEV environment on AWS / Cloudflare.",
9
+ "viza-manager": "Engineering manager. Manages GitHub resources and can grant or revoke access for developers and designers.",
10
+ "viza-publisher": "Release manager. Deploys resources to the PRODUCTION environment on AWS / Cloudflare and publishes Android / iOS applications.",
11
+ "viza-billing": "Billing administrator. Views, adds, and manages billing information across Figma, AWS, Cloudflare, and GitHub.",
12
+ "viza-admin": "System administrator. Deploys to both DEV and PROD environments and manages access for publishers, managers, and billing roles.",
13
+ "viza-super": "Super administrator. Highest authority with full deployment and access control privileges, including system administrators.",
14
+ };
15
+ const AUTH_ORG = "Modo-Auth";
16
+ async function getGithubLogin() {
17
+ const { stdout } = await execFileAsync("gh", ["api", "user", "--jq", ".login"]);
18
+ return stdout.trim();
19
+ }
20
+ async function getUserTeams(login) {
21
+ const { stdout } = await execFileAsync("gh", [
22
+ "api",
23
+ "user/teams",
24
+ "--paginate",
25
+ "--jq",
26
+ `.[] | select(.organization.login=="${AUTH_ORG}") | .slug`,
27
+ ]);
28
+ return stdout
29
+ .split("\n")
30
+ .map((s) => s.trim())
31
+ .filter(Boolean);
32
+ }
33
+ export async function whoamiCommand() {
34
+ process.stdout.write("\u001b[2J\u001b[3J\u001b[H");
35
+ console.log(chalk.cyanBright("👤 Viza WhoAmI"));
36
+ console.log(chalk.gray("─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"));
37
+ let login;
38
+ let teams;
39
+ try {
40
+ login = await getGithubLogin();
41
+ }
42
+ catch {
43
+ console.error(chalk.red("❌ Unable to resolve GitHub identity. Are you logged in via `gh auth login`?"));
44
+ process.exit(1);
45
+ }
46
+ try {
47
+ teams = await getUserTeams(login);
48
+ }
49
+ catch {
50
+ console.error(chalk.red("❌ Unable to resolve GitHub team membership."));
51
+ process.exit(1);
52
+ }
53
+ console.log(`${chalk.gray("GitHub User:")} ${chalk.magentaBright.bold(login)}`);
54
+ console.log();
55
+ if (teams.length === 0) {
56
+ console.log(chalk.yellow("⚠️ You are not a member of any Viza teams."));
57
+ return;
58
+ }
59
+ console.log(chalk.gray("Teams & Capabilities:"));
60
+ console.log(chalk.gray("─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"));
61
+ for (const team of teams) {
62
+ console.log(chalk.green(`✓ ${team}`));
63
+ const description = TEAM_DESCRIPTIONS[team] ?? "Chưa có mô tả quyền cho nhóm này.";
64
+ console.log(chalk.gray(` ${description}`));
65
+ console.log();
66
+ }
67
+ console.log(chalk.gray("ℹ️ Capabilities shown above are UX hints only.\n" +
68
+ " Actual permissions are enforced server-side."));
69
+ }
@@ -0,0 +1,13 @@
1
+ export function resolveEnv(flags) {
2
+ // Fail fast on conflicting deterministic flags
3
+ if (flags.prod && flags.dev) {
4
+ throw new Error("Conflicting flags: --prod and --dev cannot be used together");
5
+ }
6
+ // Deterministic environment resolution
7
+ if (flags.prod)
8
+ return "prod";
9
+ if (flags.dev)
10
+ return "dev";
11
+ // Default fallback
12
+ return "dev";
13
+ }
@@ -0,0 +1,8 @@
1
+ // src/commands/_shared/hubIntent.ts
2
+ export const HUB_INTENT_BY_ENV = {
3
+ dev: "hub-dev",
4
+ prod: "hub",
5
+ };
6
+ export function resolveHubIntent(env) {
7
+ return HUB_INTENT_BY_ENV[env];
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,130 @@
1
+ import { dispatcherDispatch, } from "@vizamodo/viza-dispatcher";
2
+ import chalk from "chalk";
3
+ import { startSpinner, stopSpinner } from "../ui/spinner.js";
4
+ import { renderLog } from "../ui/logRenderer.js";
5
+ import { showDispatchBanner } from "../ui/banner.js";
6
+ import { getCliVersion, checkForCliUpdateSoft } from "./version.js";
7
+ import { resolveExecutionMode } from "./resolveExecutionMode.js";
8
+ /**
9
+ * KISS log rendering.
10
+ * - success + hide => no log
11
+ * - otherwise => render log (if present)
12
+ */
13
+ function maybeRenderLog(result, policy) {
14
+ // KISS: only skip logs when success + hide.
15
+ if (result.status === "success" && policy === "hide") {
16
+ return;
17
+ }
18
+ if (result.logBuffer) {
19
+ renderLog(result.logBuffer, { status: result.status });
20
+ }
21
+ }
22
+ function assertDispatchInputStrict(input) {
23
+ // Required top-level fields
24
+ if (!input.intent || typeof input.intent !== "string") {
25
+ throw new Error("dispatch_input_missing_intent");
26
+ }
27
+ if (!input.commandType || typeof input.commandType !== "string") {
28
+ throw new Error("dispatch_input_missing_commandType");
29
+ }
30
+ if (!input.infraKey || typeof input.infraKey !== "string") {
31
+ throw new Error("dispatch_input_missing_infraKey");
32
+ }
33
+ if (!("payload" in input)) {
34
+ throw new Error("dispatch_input_missing_payload");
35
+ }
36
+ if (typeof input.payload !== "object" ||
37
+ input.payload === null ||
38
+ Array.isArray(input.payload)) {
39
+ throw new Error("dispatch_input_invalid_payload");
40
+ }
41
+ // runnerLabel must be explicit
42
+ if (!input.runnerLabel || (input.runnerLabel !== "native" && input.runnerLabel !== "selfhosted")) {
43
+ throw new Error("dispatch_input_invalid_runnerLabel");
44
+ }
45
+ // keepLog must be boolean if present
46
+ if ("keepLog" in input && typeof input.keepLog !== "boolean") {
47
+ throw new Error("dispatch_input_invalid_keepLog");
48
+ }
49
+ // flowGates validation (no undefined allowed)
50
+ if ("flowGates" in input) {
51
+ if (input.flowGates === undefined || input.flowGates === null) {
52
+ throw new Error("dispatch_input_invalid_flowGates");
53
+ }
54
+ if (typeof input.flowGates !== "object") {
55
+ throw new Error("dispatch_input_invalid_flowGates");
56
+ }
57
+ if ("secrets" in input.flowGates && typeof input.flowGates.secrets !== "boolean") {
58
+ throw new Error("dispatch_input_invalid_flowGates_secrets");
59
+ }
60
+ if ("encVars" in input.flowGates && typeof input.flowGates.encVars !== "boolean") {
61
+ throw new Error("dispatch_input_invalid_flowGates_encVars");
62
+ }
63
+ }
64
+ // Final hard check: JSON round-trip must still work
65
+ try {
66
+ JSON.parse(JSON.stringify(input));
67
+ }
68
+ catch {
69
+ throw new Error("dispatch_input_not_json_roundtrip_safe");
70
+ }
71
+ }
72
+ async function dispatchIntent(input, mode = "dispatch") {
73
+ const dispatchInput = {
74
+ intent: input.intent,
75
+ commandType: input.commandType,
76
+ infraKey: input.infraKey,
77
+ payload: input.payload,
78
+ runnerLabel: input.selfHosted ? "selfhosted" : "native",
79
+ keepLog: input.keepLog,
80
+ flowGates: input.flowGates,
81
+ };
82
+ // CLI fail-fast: never dispatch dirty envelope
83
+ assertDispatchInputStrict(dispatchInput);
84
+ const handle = await dispatcherDispatch(dispatchInput, {
85
+ auth: {
86
+ targetEnv: input.targetEnv,
87
+ allowedTeams: input.allowedTeams,
88
+ },
89
+ }, mode);
90
+ return handle;
91
+ }
92
+ /**
93
+ * Dispatch and wait for completion.
94
+ *
95
+ * This is the recommended "one door" for most viza-cli commands
96
+ * so that waiting/log/result handling does not spread across the codebase.
97
+ */
98
+ export async function dispatchIntentAndWait(input, opts = {}) {
99
+ const policy = opts.log ?? "hide";
100
+ const mode = resolveExecutionMode(opts);
101
+ const cliVersion = getCliVersion();
102
+ const meta = {
103
+ cliVersion,
104
+ };
105
+ showDispatchBanner(input, meta, opts.status);
106
+ const handle = await dispatchIntent(input, mode);
107
+ const spinner = startSpinner("Waiting for dispatch session");
108
+ // Fast update check (kept before wait/log so the hint is not lost)
109
+ const updateInfo = await checkForCliUpdateSoft().catch(() => null);
110
+ if (updateInfo?.hasUpdate) {
111
+ const title = chalk.gray.bold("\n⬆️ Update available");
112
+ const ver = chalk.yellow(`${updateInfo.current} → ${updateInfo.latest}`);
113
+ const cmd = chalk.cyan("npm i -g @@viza-cli/app");
114
+ console.log(`\n${title} ${ver}`);
115
+ console.log(chalk.dim(" Run:") + " " + cmd + "\n");
116
+ }
117
+ try {
118
+ const result = await handle.wait();
119
+ stopSpinner(spinner, result.status === "success" ? "✅ Done" : "❌ Failed");
120
+ maybeRenderLog(result, policy);
121
+ if (result.status !== "success") {
122
+ throw new Error(`Dispatch failed: ${result.status}`);
123
+ }
124
+ return result;
125
+ }
126
+ catch (err) {
127
+ stopSpinner(spinner, "❌ Failed");
128
+ throw err;
129
+ }
130
+ }
@@ -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,5 @@
1
+ export function resolveExecutionMode(options) {
2
+ if (options.status)
3
+ return "status";
4
+ return "dispatch";
5
+ }
@@ -0,0 +1,148 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { resolve, join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import os from "node:os";
5
+ let _cached;
6
+ /**
7
+ * Returns the current viza-cli version.
8
+ *
9
+ * - Prefers build-time injected `VIZA_CLI_VERSION`.
10
+ * - Falls back to reading nearest package.json.
11
+ * - Always returns a non-empty string ("dev" as last resort).
12
+ */
13
+ export function getCliVersion() {
14
+ if (_cached)
15
+ return _cached;
16
+ const injected = process.env.VIZA_CLI_VERSION;
17
+ if (injected && injected.trim()) {
18
+ _cached = injected.trim();
19
+ return _cached;
20
+ }
21
+ try {
22
+ const here = fileURLToPath(import.meta.url);
23
+ let dir = dirname(here);
24
+ // Walk up to find the package.json that belongs to this CLI package.
25
+ for (let i = 0; i < 8; i++) {
26
+ const pkgPath = resolve(dir, "package.json");
27
+ try {
28
+ const raw = readFileSync(pkgPath, "utf8");
29
+ const json = JSON.parse(raw);
30
+ if (json?.name === "@@viza-cli/app" && typeof json.version === "string" && json.version.trim()) {
31
+ _cached = json.version.trim();
32
+ return _cached;
33
+ }
34
+ }
35
+ catch {
36
+ // ignore and keep walking up
37
+ }
38
+ const parent = resolve(dir, "..");
39
+ if (parent === dir)
40
+ break;
41
+ dir = parent;
42
+ }
43
+ }
44
+ catch {
45
+ // ignore
46
+ }
47
+ _cached = "dev";
48
+ return _cached;
49
+ }
50
+ function resolveVizaConfigDir() {
51
+ if (process.platform === "win32") {
52
+ const appData = process.env.APPDATA || join(process.env.USERPROFILE || "", "AppData", "Roaming");
53
+ return join(appData, "viza");
54
+ }
55
+ else {
56
+ return join(os.homedir(), ".config", "viza");
57
+ }
58
+ }
59
+ function resolveUpdateCachePath() {
60
+ const configDir = resolveVizaConfigDir();
61
+ return join(configDir, "update.json");
62
+ }
63
+ function semverCompare(a, b) {
64
+ // minimal semver compare for x.y.z numeric only
65
+ const parse = (v) => {
66
+ const parts = v.split(".");
67
+ if (parts.length !== 3)
68
+ return null;
69
+ const nums = parts.map(p => {
70
+ const n = Number(p);
71
+ return Number.isNaN(n) ? null : n;
72
+ });
73
+ if (nums.includes(null))
74
+ return null;
75
+ return nums;
76
+ };
77
+ const pa = parse(a);
78
+ const pb = parse(b);
79
+ if (!pa || !pb) {
80
+ // fallback to string compare
81
+ if (a === b)
82
+ return 0;
83
+ return a > b ? 1 : -1;
84
+ }
85
+ for (let i = 0; i < 3; i++) {
86
+ if (pa[i] > pb[i])
87
+ return 1;
88
+ if (pa[i] < pb[i])
89
+ return -1;
90
+ }
91
+ return 0;
92
+ }
93
+ /**
94
+ * Checks npm for the latest published version of @@viza-cli/app without throwing and without blocking the main flow.
95
+ *
96
+ * Returns null on failure.
97
+ */
98
+ export async function checkForCliUpdateSoft() {
99
+ const cachePath = resolveUpdateCachePath();
100
+ const now = Date.now();
101
+ const ttl = 10 * 60 * 1000; // 10 minutes (Fixed window)
102
+ const current = getCliVersion();
103
+ try {
104
+ if (existsSync(cachePath)) {
105
+ const raw = readFileSync(cachePath, "utf8");
106
+ const cached = JSON.parse(raw);
107
+ // Kiểm tra Version Mismatch HOẶC Cache Expired
108
+ // KHÔNG cập nhật checkedAt ở đây để đảm bảo đúng 10p sẽ check lại 1 lần
109
+ if (cached.current === current && cached.checkedAt && (now - cached.checkedAt) < ttl) {
110
+ return cached;
111
+ }
112
+ }
113
+ }
114
+ catch { /* ignore */ }
115
+ // --- Bắt đầu Fetch mới khi cache hết hạn hoặc sai version ---
116
+ try {
117
+ const registryUrl = `https://registry.npmjs.org/@@viza-cli/app/latest?t=${now}`;
118
+ const res = await fetch(registryUrl, {
119
+ cache: "no-store",
120
+ headers: { 'Cache-Control': 'no-cache' }
121
+ });
122
+ if (!res.ok)
123
+ return null;
124
+ const json = await res.json();
125
+ const latest = json.version;
126
+ if (!latest || typeof latest !== "string")
127
+ return null;
128
+ const cmp = semverCompare(latest, current);
129
+ const hasUpdate = cmp > 0 && current !== "dev";
130
+ const info = {
131
+ current,
132
+ latest,
133
+ hasUpdate,
134
+ checkedAt: now, // Chỉ cập nhật mốc thời gian tại đây
135
+ };
136
+ try {
137
+ const configDir = dirname(cachePath);
138
+ if (!existsSync(configDir))
139
+ mkdirSync(configDir, { recursive: true });
140
+ writeFileSync(cachePath, JSON.stringify(info), "utf8");
141
+ }
142
+ catch { /* ignore */ }
143
+ return info;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
@@ -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 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import chalk from "chalk";
2
+ import figlet from "figlet";
3
+ const ENV_BANNER_CONFIG = {
4
+ "dev": { title: "Viza Development", color: "cyanBright" },
5
+ "prod": { title: "Viza Production", color: "yellowBright" },
6
+ };
7
+ function pickBannerConfig(env) {
8
+ const cfg = ENV_BANNER_CONFIG[env];
9
+ if (cfg)
10
+ return cfg;
11
+ return {
12
+ title: `Viza CLI`,
13
+ color: "cyanBright"
14
+ };
15
+ }
16
+ /**
17
+ * Show a banner right before calling dispatchIntent.
18
+ * Maps targetTeam -> title/color/env, and renders meta (github login + viza-cli version).
19
+ */
20
+ export function showDispatchBanner(input, meta, status) {
21
+ const cfg = pickBannerConfig(input.targetEnv);
22
+ // Default subtitle: commandType (or caller-provided subtitle)
23
+ const subtitle = `${input.commandType}`;
24
+ showBanner({
25
+ title: status ? `Viza Command Line` : cfg.title,
26
+ subtitle,
27
+ color: status ? "magentaBright" : cfg.color,
28
+ env: input.targetEnv,
29
+ runner: input.selfHosted
30
+ ? {
31
+ type: "selfhosted",
32
+ label: "viza-builder",
33
+ } :
34
+ { type: "github" },
35
+ meta: {
36
+ version: meta?.cliVersion,
37
+ }
38
+ });
39
+ }
40
+ export function showBanner(opts) {
41
+ const { title, subtitle, color = "cyanBright", runner, meta } = opts;
42
+ process.stdout.write("\u001b[2J\u001b[3J\u001b[H");
43
+ const font = opts.env === "prod"
44
+ ? "Ogre"
45
+ : opts.env === "dev"
46
+ ? "Ogre"
47
+ : "Small";
48
+ const bannerText = figlet.textSync(title, { font });
49
+ console.log(chalk[color](bannerText));
50
+ if (subtitle) {
51
+ console.log(chalk.gray("Command:"), chalk.magentaBright(subtitle));
52
+ }
53
+ // Environment line removed; replaced by user-friendly info line below
54
+ if (runner) {
55
+ if (runner.type === "github") {
56
+ console.log(chalk.gray("Runner:"), chalk.yellowBright("GitHub-hosted"));
57
+ }
58
+ else {
59
+ console.log(chalk.gray("Runner:"), chalk.yellowBright("Self-hosted"), runner.label ? chalk.gray(" | label:") : "", runner.label ? chalk.cyan(runner.label) : "");
60
+ }
61
+ }
62
+ // User-friendly info line
63
+ {
64
+ const parts = [];
65
+ parts.push(`Viza CLI`);
66
+ if (meta?.version) {
67
+ parts.push(`Version: ${chalk.cyan(meta.version)}`);
68
+ }
69
+ parts.push(`Install: ${chalk.gray(`npm i -g @@viza-cli/app`)}`);
70
+ if (parts.length) {
71
+ console.log(chalk.gray(parts.join(chalk.gray(" | "))));
72
+ }
73
+ }
74
+ console.log(chalk.gray("────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"));
75
+ }
@@ -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]} ${chalk.gray(`${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,38 @@
1
+ import chalk from "chalk";
2
+ import prompts from "prompts";
3
+ import open from "open";
4
+ import clipboardy from "clipboardy";
5
+ /**
6
+ * Hiển thị menu chọn hành động sau khi tạo SSO URL
7
+ * @param ssoUrl - Đường dẫn zero-click AWS login URL
8
+ * @param ttlHours - Thời hạn hiệu lực của link (mặc định 12h)
9
+ */
10
+ export async function showSsoLinkMenu(ssoUrl, shortUrl, ttlHours = 12) {
11
+ console.log(chalk.gray(`\n───────────────────────────────────── ${chalk.magentaBright(`AWS Zero-Click Login`)} ─────────────────────────────────────────────────`));
12
+ console.log(chalk.gray(`✅ Đã tạo liên kết đăng nhập thành công (${chalk.yellow(`hiệu lực:`)} ${chalk.cyanBright(`${ttlHours}`)} giờ)`));
13
+ console.log(chalk.yellow('🔗 Liên kết đăng nhập:'));
14
+ console.log(chalk.whiteBright(`${shortUrl}`));
15
+ console.log(chalk.gray('───────────────────────────────────────────────────────────────────────────────────────────────────────────'));
16
+ // Non-interactive fallback (CI / pipe / limited TTY)
17
+ if (!process.stdin.isTTY) {
18
+ console.log(shortUrl);
19
+ return;
20
+ }
21
+ const { action } = await prompts({
22
+ type: "select",
23
+ name: "action",
24
+ message: "👉 Hãy chọn hành động bạn muốn thực hiện:",
25
+ choices: [
26
+ { title: "🌐 Mở trong trình duyệt", value: "open" },
27
+ { title: "📋 Sao chép liên kết", value: "copy" },
28
+ ],
29
+ });
30
+ if (action === 'open') {
31
+ console.log(chalk.blue('🔍 Đang mở liên kết trong trình duyệt...'));
32
+ await open(ssoUrl);
33
+ }
34
+ else if (action === 'copy') {
35
+ clipboardy.writeSync(ssoUrl);
36
+ console.log(chalk.green('✅ Liên kết đã được sao chép vào clipboard!'));
37
+ }
38
+ }
@@ -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,36 @@
1
+ {
2
+ "name": "@viza-cli/app",
3
+ "version": "1.5.27",
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.85",
20
+ "adm-zip": "^0.5.16",
21
+ "chalk": "^5.6.2",
22
+ "clipboardy": "^5.2.1",
23
+ "commander": "^14.0.3",
24
+ "figlet": "^1.10.0",
25
+ "open": "^11.0.0",
26
+ "prompts": "^2.4.2"
27
+ },
28
+ "devDependencies": {
29
+ "@types/adm-zip": "^0.5.7",
30
+ "@types/figlet": "^1.7.0",
31
+ "@types/node": "^25.2.3",
32
+ "@types/prompts": "^2.4.9",
33
+ "ts-node": "^10.9.2",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }