@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.
- package/README.md +25 -0
- package/dist/bin/viza.js +5 -0
- package/dist/src/cli/options.js +6 -0
- package/dist/src/cli/program.js +23 -0
- package/dist/src/cli/resolveOptions.js +9 -0
- package/dist/src/commands/aws/register.js +4 -0
- package/dist/src/commands/aws/rolesanywhere/bootstrap/bootstrap.js +46 -0
- package/dist/src/commands/aws/rolesanywhere/bootstrap/register.js +13 -0
- package/dist/src/commands/aws/rolesanywhere/enable-profiles/enable-profiles.js +46 -0
- package/dist/src/commands/aws/rolesanywhere/enable-profiles/register.js +13 -0
- package/dist/src/commands/aws/rolesanywhere/register.js +12 -0
- package/dist/src/commands/aws/rolesanywhere/rollback/rollback.js +1 -0
- package/dist/src/commands/aws/rolesanywhere/rotate/rotate.js +1 -0
- package/dist/src/commands/bootstrap/index.js +5 -0
- package/dist/src/commands/bootstrap/register.js +16 -0
- package/dist/src/commands/login/aws/aws.js +87 -0
- package/dist/src/commands/login/aws/register.js +15 -0
- package/dist/src/commands/login/register.js +4 -0
- package/dist/src/commands/whoami/index.js +69 -0
- package/dist/src/context/env.js +13 -0
- package/dist/src/context/hubIntent.js +8 -0
- package/dist/src/core/commandDescriptor.js +1 -0
- package/dist/src/core/dispatch.js +130 -0
- package/dist/src/core/renderHint.js +9 -0
- package/dist/src/core/resolveExecutionMode.js +5 -0
- package/dist/src/core/version.js +148 -0
- package/dist/src/errors/handleError.js +6 -0
- package/dist/src/types/cli.js +1 -0
- package/dist/src/ui/banner.js +75 -0
- package/dist/src/ui/index.js +22 -0
- package/dist/src/ui/logRenderer.js +174 -0
- package/dist/src/ui/spinner.js +22 -0
- package/dist/src/ui/sso/awsLoginMenu.js +38 -0
- package/dist/src/ui/theme.js +1 -0
- package/dist/src/utils/fs.js +3 -0
- 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
|
+
```
|
package/dist/bin/viza.js
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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 @@
|
|
|
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,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 @@
|
|
|
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 = {};
|
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
|
+
}
|