xtra-cli 0.2.1 → 0.2.4
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/dist/commands/doctor.js +52 -64
- package/dist/commands/project.js +67 -12
- package/dist/commands/run.js +2 -1
- package/dist/commands/ui.js +168 -97
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -38,10 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.doctorCommand = void 0;
|
|
40
40
|
/**
|
|
41
|
-
* doctor.ts
|
|
42
|
-
*
|
|
43
|
-
* Runs a series of self-diagnostic checks and reports
|
|
44
|
-
* pass / warn / fail for each one.
|
|
41
|
+
* doctor.ts — Diagnose common xtra-cli setup issues
|
|
45
42
|
*/
|
|
46
43
|
const commander_1 = require("commander");
|
|
47
44
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -49,128 +46,119 @@ const fs = __importStar(require("fs"));
|
|
|
49
46
|
const path = __importStar(require("path"));
|
|
50
47
|
const axios_1 = __importDefault(require("axios"));
|
|
51
48
|
const config_1 = require("../lib/config");
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
49
|
+
// ─── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
50
|
+
const W = 65;
|
|
51
|
+
const B = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
|
|
52
|
+
function hline(w) { return B.h.repeat(w); }
|
|
53
|
+
const PASS = chalk_1.default.green("✓ ");
|
|
54
|
+
const FAIL = chalk_1.default.red("✗ ");
|
|
55
|
+
const WARN = chalk_1.default.yellow("⚠ ");
|
|
55
56
|
async function runChecks() {
|
|
56
57
|
const checks = [];
|
|
57
58
|
const config = (0, config_1.getConfig)();
|
|
58
|
-
//
|
|
59
|
+
// 1. Node Version
|
|
59
60
|
const nodeMajor = parseInt(process.versions.node.split(".")[0]);
|
|
60
61
|
checks.push({
|
|
61
62
|
name: "Node.js Version",
|
|
62
63
|
status: nodeMajor >= 18 ? "pass" : "fail",
|
|
63
64
|
message: `v${process.versions.node} ${nodeMajor >= 18 ? "(OK)" : "(requires >=18)"}`,
|
|
64
65
|
});
|
|
65
|
-
//
|
|
66
|
+
// 2. Auth Token
|
|
66
67
|
const token = (0, config_1.getAuthToken)();
|
|
67
68
|
checks.push({
|
|
68
69
|
name: "Auth Token",
|
|
69
70
|
status: token ? "pass" : "fail",
|
|
70
|
-
message: token ? `Set (${token.substring(0, 8)}...)` : "Not set
|
|
71
|
+
message: token ? `Set (${token.substring(0, 8)}...)` : "Not set — run 'xtra login'",
|
|
71
72
|
});
|
|
72
|
-
//
|
|
73
|
-
const apiUrl = config.apiUrl || process.env.XTRA_API_URL;
|
|
73
|
+
// 3. API URL Configured
|
|
74
|
+
const apiUrl = config.apiUrl || process.env.XTRA_API_URL || "https://xtra-security.vercel.app/api";
|
|
74
75
|
checks.push({
|
|
75
76
|
name: "API URL",
|
|
76
|
-
status: apiUrl ? "pass" : "warn",
|
|
77
|
-
message: apiUrl
|
|
77
|
+
status: config.apiUrl || process.env.XTRA_API_URL ? "pass" : "warn",
|
|
78
|
+
message: apiUrl,
|
|
78
79
|
});
|
|
79
|
-
//
|
|
80
|
+
// 4. API Reachability
|
|
80
81
|
try {
|
|
81
|
-
const res = await axios_1.default.get(`${
|
|
82
|
+
const res = await axios_1.default.get(`${apiUrl}/health`, { timeout: 4000 });
|
|
82
83
|
checks.push({
|
|
83
84
|
name: "API Connectivity",
|
|
84
85
|
status: res.status === 200 ? "pass" : "warn",
|
|
85
|
-
message: `${
|
|
86
|
+
message: `${res.status} ${res.statusText}`,
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
catch (e) {
|
|
89
|
-
const msg = e.code === "ECONNREFUSED"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const msg = e.code === "ECONNREFUSED" ? "Connection refused (offline?)"
|
|
91
|
+
: e.code === "ETIMEDOUT" ? "Timed out (unreachable)"
|
|
92
|
+
: e.response?.status === 404 ? "API reachable (health endpoint missing)"
|
|
93
|
+
: e.message;
|
|
94
|
+
// Special case: if it hits a 404 it means DNS and port work, but route is missing
|
|
95
|
+
const status = e.response?.status === 404 ? "pass" : "fail";
|
|
96
|
+
checks.push({ name: "API Connectivity", status, message: msg });
|
|
95
97
|
}
|
|
96
|
-
//
|
|
98
|
+
// 5. Active Project
|
|
97
99
|
const project = (0, config_1.getRcConfig)().project;
|
|
98
100
|
checks.push({
|
|
99
101
|
name: "Active Project",
|
|
100
102
|
status: project ? "pass" : "warn",
|
|
101
|
-
message: project ? project : "Not set
|
|
103
|
+
message: project ? project : "Not set — run 'xtra project set <id>'",
|
|
102
104
|
});
|
|
103
|
-
//
|
|
104
|
-
const branch = (0, config_1.getRcConfig)().branch;
|
|
105
|
+
// 6. Active Branch
|
|
106
|
+
const branch = (0, config_1.getRcConfig)().branch || "main";
|
|
105
107
|
checks.push({
|
|
106
108
|
name: "Active Branch",
|
|
107
109
|
status: "pass",
|
|
108
|
-
message: branch
|
|
110
|
+
message: branch,
|
|
109
111
|
});
|
|
110
|
-
//
|
|
112
|
+
// 7. Config files
|
|
111
113
|
const rcPath = path.join(process.cwd(), ".xtrarc");
|
|
112
114
|
const jsonPath = path.join(process.cwd(), "xtra.json");
|
|
113
|
-
const
|
|
115
|
+
const hasConfig = fs.existsSync(rcPath) || fs.existsSync(jsonPath);
|
|
114
116
|
checks.push({
|
|
115
|
-
name: "Project Config
|
|
116
|
-
status:
|
|
117
|
-
message:
|
|
118
|
-
? fs.existsSync(rcPath) ? ".xtrarc found" : "xtra.json found"
|
|
119
|
-
: "Not found — run 'xtra init' to create one",
|
|
120
|
-
});
|
|
121
|
-
// ── 8. .env file present ────────────────────────────────────────────────────
|
|
122
|
-
const envPath = path.join(process.cwd(), ".env");
|
|
123
|
-
checks.push({
|
|
124
|
-
name: ".env File",
|
|
125
|
-
status: "pass",
|
|
126
|
-
message: fs.existsSync(envPath) ? ".env found" : "No .env (not required if using xtra run)",
|
|
127
|
-
});
|
|
128
|
-
// ── 9. XTRA_MACHINE_TOKEN (for CI) ─────────────────────────────────────────
|
|
129
|
-
const machineToken = process.env.XTRA_MACHINE_TOKEN;
|
|
130
|
-
checks.push({
|
|
131
|
-
name: "CI Machine Token",
|
|
132
|
-
status: "pass", // Always pass — optional
|
|
133
|
-
message: machineToken ? "XTRA_MACHINE_TOKEN is set ✔" : "Not set (only needed for CI/CD pipelines)",
|
|
117
|
+
name: "Project Config",
|
|
118
|
+
status: hasConfig ? "pass" : "warn",
|
|
119
|
+
message: hasConfig ? (fs.existsSync(rcPath) ? ".xtrarc found" : "xtra.json found") : "Not found (run 'xtra init')",
|
|
134
120
|
});
|
|
135
121
|
return checks;
|
|
136
122
|
}
|
|
137
123
|
exports.doctorCommand = new commander_1.Command("doctor")
|
|
138
|
-
.description("Diagnose
|
|
124
|
+
.description("Diagnose CLI configuration and API connectivity")
|
|
139
125
|
.option("--json", "Output results as JSON", false)
|
|
140
126
|
.action(async (options) => {
|
|
141
127
|
if (!options.json) {
|
|
142
|
-
console.log(
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(chalk_1.default.bold.cyan(" ⚕ XtraSecurity Diagnostics"));
|
|
130
|
+
console.log(chalk_1.default.hex("#4a5568")(" " + B.tl + hline(W) + B.tr));
|
|
143
131
|
}
|
|
144
132
|
const checks = await runChecks();
|
|
145
133
|
if (options.json) {
|
|
146
134
|
process.stdout.write(JSON.stringify(checks, null, 2) + "\n");
|
|
147
135
|
return;
|
|
148
136
|
}
|
|
149
|
-
let failures = 0;
|
|
150
|
-
let warnings = 0;
|
|
137
|
+
let failures = 0, warnings = 0;
|
|
151
138
|
for (const c of checks) {
|
|
152
|
-
const icon = c.status === "pass" ? PASS : c.status === "warn" ? WARN : FAIL;
|
|
153
|
-
const nameStr = chalk_1.default.bold(c.name.padEnd(22));
|
|
154
|
-
const msgStr = c.status === "fail"
|
|
155
|
-
? chalk_1.default.red(c.message)
|
|
156
|
-
: c.status === "warn"
|
|
157
|
-
? chalk_1.default.yellow(c.message)
|
|
158
|
-
: chalk_1.default.gray(c.message);
|
|
159
|
-
console.log(`${icon} ${nameStr}${msgStr}`);
|
|
160
139
|
if (c.status === "fail")
|
|
161
140
|
failures++;
|
|
162
141
|
if (c.status === "warn")
|
|
163
142
|
warnings++;
|
|
143
|
+
const icon = c.status === "pass" ? PASS : c.status === "warn" ? WARN : FAIL;
|
|
144
|
+
const name = chalk_1.default.bold(chalk_1.default.white(c.name.padEnd(20)));
|
|
145
|
+
const msg = c.status === "pass" ? chalk_1.default.hex("#94a3b8")(c.message)
|
|
146
|
+
: c.status === "warn" ? chalk_1.default.yellow(c.message)
|
|
147
|
+
: chalk_1.default.red(c.message);
|
|
148
|
+
console.log(chalk_1.default.hex("#4a5568")(" " + B.v) + ` ${icon}${name} ${msg}`.padEnd(W) + chalk_1.default.hex("#4a5568")(B.v));
|
|
164
149
|
}
|
|
150
|
+
console.log(chalk_1.default.hex("#4a5568")(" " + B.bl + hline(W) + B.br));
|
|
165
151
|
console.log();
|
|
166
152
|
if (failures === 0 && warnings === 0) {
|
|
167
|
-
console.log(chalk_1.default.green
|
|
153
|
+
console.log(chalk_1.default.green(" ✓ All systems operational. The CLI is ready to use."));
|
|
168
154
|
}
|
|
169
155
|
else {
|
|
156
|
+
const parts = [];
|
|
170
157
|
if (failures > 0)
|
|
171
|
-
|
|
158
|
+
parts.push(chalk_1.default.red(`${failures} error(s)`));
|
|
172
159
|
if (warnings > 0)
|
|
173
|
-
|
|
160
|
+
parts.push(chalk_1.default.yellow(`${warnings} warning(s)`));
|
|
161
|
+
console.log(` Found ${parts.join(" and ")}.`);
|
|
174
162
|
}
|
|
175
163
|
console.log();
|
|
176
164
|
});
|
package/dist/commands/project.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -10,9 +43,41 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
10
43
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
44
|
const config_1 = require("../lib/config");
|
|
12
45
|
const api_1 = require("../lib/api");
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
13
48
|
exports.projectCommand = new commander_1.Command("project")
|
|
14
49
|
.alias("projects")
|
|
15
50
|
.description("Manage project context");
|
|
51
|
+
const updateConfigs = (projectId, projectName) => {
|
|
52
|
+
const oldProject = (0, config_1.getConfigValue)("project");
|
|
53
|
+
// Always update global
|
|
54
|
+
(0, config_1.setConfig)("project", projectId);
|
|
55
|
+
// Clear branch if project changed
|
|
56
|
+
if (oldProject !== projectId) {
|
|
57
|
+
(0, config_1.setConfig)("branch", "main"); // Reset to main or clear? Projects have different branches.
|
|
58
|
+
}
|
|
59
|
+
// Update local .xtrarc if exists
|
|
60
|
+
const rcPath = path.join(process.cwd(), ".xtrarc");
|
|
61
|
+
if (fs.existsSync(rcPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const rc = JSON.parse(fs.readFileSync(rcPath, "utf-8"));
|
|
64
|
+
rc.project = projectId;
|
|
65
|
+
if (oldProject !== projectId) {
|
|
66
|
+
rc.branch = "main"; // Sync local branch too
|
|
67
|
+
}
|
|
68
|
+
fs.writeFileSync(rcPath, JSON.stringify(rc, null, 2), "utf8");
|
|
69
|
+
console.log(chalk_1.default.gray(` ✔ Updated local .xtrarc`));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.error(chalk_1.default.yellow(` ⚠ Could not update local .xtrarc: ${e instanceof Error ? e.message : 'Unknown error'}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk_1.default.green(`✔ Default project set to '${projectName}'`));
|
|
76
|
+
const currentBranch = (0, config_1.getConfigValue)("branch");
|
|
77
|
+
if (currentBranch) {
|
|
78
|
+
console.log(chalk_1.default.gray(` Active branch: ${currentBranch}`));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
16
81
|
// SET
|
|
17
82
|
exports.projectCommand
|
|
18
83
|
.command("set")
|
|
@@ -21,12 +86,7 @@ exports.projectCommand
|
|
|
21
86
|
.action(async (projectId) => {
|
|
22
87
|
// If projectId is provided, set it directly
|
|
23
88
|
if (projectId) {
|
|
24
|
-
(
|
|
25
|
-
console.log(chalk_1.default.green(`✔ Default project set to '${projectId}'`));
|
|
26
|
-
const currentBranch = (0, config_1.getConfigValue)("branch");
|
|
27
|
-
if (currentBranch) {
|
|
28
|
-
console.log(chalk_1.default.gray(` Active branch: ${currentBranch}`));
|
|
29
|
-
}
|
|
89
|
+
updateConfigs(projectId, projectId);
|
|
30
90
|
return;
|
|
31
91
|
}
|
|
32
92
|
// Otherwise, fetch projects and show interactive selector
|
|
@@ -49,13 +109,8 @@ exports.projectCommand
|
|
|
49
109
|
message: "Select a project:",
|
|
50
110
|
choices
|
|
51
111
|
}]);
|
|
52
|
-
(0, config_1.setConfig)("project", selectedProject);
|
|
53
112
|
const selectedName = projects.find((p) => p.id === selectedProject)?.name || selectedProject;
|
|
54
|
-
|
|
55
|
-
const currentBranch = (0, config_1.getConfigValue)("branch");
|
|
56
|
-
if (currentBranch) {
|
|
57
|
-
console.log(chalk_1.default.gray(` Active branch: ${currentBranch}`));
|
|
58
|
-
}
|
|
113
|
+
updateConfigs(selectedProject, selectedName);
|
|
59
114
|
}
|
|
60
115
|
catch (error) {
|
|
61
116
|
spinner.fail("Failed to fetch projects");
|
package/dist/commands/run.js
CHANGED
|
@@ -199,10 +199,11 @@ Examples:
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
console.log(chalk_1.default.gray(`> ${command} ${args.join(" ")}`));
|
|
202
|
+
const isWindows = process.platform === "win32";
|
|
202
203
|
const child = (0, child_process_1.spawn)(command, args, {
|
|
203
204
|
env: envVars,
|
|
204
205
|
stdio: "inherit",
|
|
205
|
-
shell: useShell, //
|
|
206
|
+
shell: useShell || isWindows, // Auto-enable shell on Windows for better compatibility (e.g. npm, etc)
|
|
206
207
|
});
|
|
207
208
|
child.on("exit", (code) => {
|
|
208
209
|
process.exit(code ?? 0);
|
package/dist/commands/ui.js
CHANGED
|
@@ -41,178 +41,249 @@ const commander_1 = require("commander");
|
|
|
41
41
|
const chalk_1 = __importDefault(require("chalk"));
|
|
42
42
|
const readline = __importStar(require("readline"));
|
|
43
43
|
const api_1 = require("../lib/api");
|
|
44
|
+
const config_1 = require("../lib/config");
|
|
44
45
|
const ENVS = ["development", "staging", "production"];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const header = [
|
|
70
|
-
chalk_1.default.bold.yellow("KEY".padEnd(COLS[0])),
|
|
71
|
-
chalk_1.default.bold.yellow("VALUE".padEnd(COLS[1])),
|
|
72
|
-
chalk_1.default.bold.yellow("UPDATED".padEnd(COLS[2]))
|
|
73
|
-
].join(border("│"));
|
|
74
|
-
console.log(border("│") + header + border("│"));
|
|
75
|
-
console.log(border(`├${sep}┤`));
|
|
76
|
-
if (secrets.length === 0) {
|
|
77
|
-
console.log(border("│") + chalk_1.default.gray(" No secrets found.".padEnd(COLS[0] + COLS[1] + COLS[2] + 2)) + border("│"));
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
secrets.forEach((s, i) => {
|
|
81
|
-
const isSelected = i === selectedIdx;
|
|
82
|
-
const fmt = (t, w) => {
|
|
83
|
-
const str = t.length > w - 2 ? t.slice(0, w - 5) + "..." : t;
|
|
84
|
-
return isSelected && active ? chalk_1.default.bold.bgCyan.black(str.padEnd(w)) : isSelected ? chalk_1.default.cyan(str.padEnd(w)) : str.padEnd(w);
|
|
85
|
-
};
|
|
86
|
-
const row = [
|
|
87
|
-
fmt(s.key, COLS[0]),
|
|
88
|
-
fmt("*".repeat(8), COLS[1]),
|
|
89
|
-
fmt(new Date(s.updatedAt).toLocaleDateString(), COLS[2]),
|
|
90
|
-
].join(border("│"));
|
|
91
|
-
console.log(border("│") + row + border("│"));
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
console.log(border(`└${"─".repeat(COLS[0])}┴${"─".repeat(COLS[1])}┴${"─".repeat(COLS[2])}┘`));
|
|
95
|
-
}
|
|
96
|
-
function renderStatus(msg) {
|
|
97
|
-
console.log("\n" + chalk_1.default.gray(" â— ") + chalk_1.default.white(msg));
|
|
46
|
+
const ENV_COLORS = {
|
|
47
|
+
development: chalk_1.default.green,
|
|
48
|
+
staging: chalk_1.default.yellow,
|
|
49
|
+
production: chalk_1.default.red,
|
|
50
|
+
};
|
|
51
|
+
const W = process.stdout.columns || 120;
|
|
52
|
+
// ─── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
53
|
+
const B = {
|
|
54
|
+
tl: "╭", tr: "╮", bl: "╰", br: "╯",
|
|
55
|
+
h: "─", v: "│", x: "┼",
|
|
56
|
+
lt: "├", rt: "┤", tt: "┬", bt: "┴",
|
|
57
|
+
};
|
|
58
|
+
function hline(width, c = B.h) { return c.repeat(width); }
|
|
59
|
+
function box(title, lines, width, active) {
|
|
60
|
+
const border = active ? chalk_1.default.cyan : chalk_1.default.hex("#2d3a4d");
|
|
61
|
+
const inner = width - 2;
|
|
62
|
+
const pad = (s) => s.padEnd(inner).slice(0, inner);
|
|
63
|
+
const label = active
|
|
64
|
+
? chalk_1.default.bold.cyan(` ${title} `)
|
|
65
|
+
: chalk_1.default.hex("#4a5568")(` ${title} `);
|
|
66
|
+
const titleLine = border(B.tl) + label + border(hline(inner - title.length - 2)) + border(B.tr);
|
|
67
|
+
const body = lines.map(l => border(B.v) + l.slice(0, inner).padEnd(inner) + border(B.v));
|
|
68
|
+
const foot = border(B.bl) + border(hline(inner)) + border(B.br);
|
|
69
|
+
return [titleLine, ...body, foot];
|
|
98
70
|
}
|
|
99
|
-
|
|
71
|
+
function clear() { process.stdout.write("\x1B[H\x1B[2J\x1B[3J"); }
|
|
72
|
+
// ─── Main TUI ────────────────────────────────────────────────────────────────
|
|
100
73
|
async function runUI() {
|
|
101
|
-
let
|
|
74
|
+
let panel = "projects";
|
|
102
75
|
let projects = [];
|
|
103
|
-
let
|
|
76
|
+
let projIdx = 0;
|
|
104
77
|
let envIdx = 0;
|
|
105
78
|
let secrets = [];
|
|
106
|
-
let
|
|
107
|
-
let status = "Loading
|
|
79
|
+
let secIdx = 0;
|
|
80
|
+
let status = "Loading…";
|
|
108
81
|
let loading = false;
|
|
82
|
+
let showValues = false;
|
|
83
|
+
// ── Loaders ────────────────────────────────────────────────────────────────
|
|
109
84
|
async function loadProjects() {
|
|
85
|
+
status = "Loading projects…";
|
|
86
|
+
draw();
|
|
110
87
|
try {
|
|
111
|
-
|
|
88
|
+
const raw = await api_1.api.getProjects();
|
|
89
|
+
projects = Array.isArray(raw) ? raw : raw.projects ?? [];
|
|
112
90
|
status = projects.length > 0
|
|
113
|
-
?
|
|
114
|
-
: "No projects found.
|
|
91
|
+
? `${projects.length} project(s) found · ↑↓ navigate · Enter to load secrets`
|
|
92
|
+
: "No projects found. Run `xtra login` first.";
|
|
115
93
|
}
|
|
116
94
|
catch (e) {
|
|
117
|
-
status = chalk_1.default.red(
|
|
95
|
+
status = chalk_1.default.red(`✗ ${e.message}`);
|
|
118
96
|
}
|
|
119
97
|
draw();
|
|
120
98
|
}
|
|
121
99
|
async function loadSecrets() {
|
|
122
|
-
if (!projects[
|
|
100
|
+
if (!projects[projIdx])
|
|
123
101
|
return;
|
|
124
102
|
loading = true;
|
|
125
|
-
status = `Fetching secrets for ${projects[
|
|
103
|
+
status = `Fetching secrets for ${chalk_1.default.cyan(projects[projIdx].name)} / ${ENV_COLORS[ENVS[envIdx]](ENVS[envIdx])}…`;
|
|
126
104
|
draw();
|
|
127
105
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
106
|
+
const rc = (0, config_1.getRcConfig)();
|
|
107
|
+
const branch = rc.branch || "main";
|
|
108
|
+
const raw = await api_1.api.getSecrets(projects[projIdx].id, ENVS[envIdx], branch);
|
|
109
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
110
|
+
secrets = Object.entries(raw).map(([key, value]) => ({
|
|
111
|
+
id: key, key, value: String(value), updatedAt: new Date().toISOString(),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
else if (Array.isArray(raw)) {
|
|
115
|
+
secrets = raw;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
secrets = [];
|
|
119
|
+
}
|
|
120
|
+
secIdx = 0;
|
|
121
|
+
status = secrets.length > 0
|
|
122
|
+
? `${secrets.length} secret(s) · V toggle values · Tab switch panel · Q quit`
|
|
123
|
+
: "No secrets in this environment.";
|
|
131
124
|
}
|
|
132
125
|
catch (e) {
|
|
133
126
|
secrets = [];
|
|
134
|
-
status = chalk_1.default.red(
|
|
127
|
+
status = chalk_1.default.red(`✗ ${e.message}`);
|
|
135
128
|
}
|
|
136
129
|
loading = false;
|
|
137
130
|
draw();
|
|
138
131
|
}
|
|
132
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
139
133
|
function draw() {
|
|
140
134
|
clear();
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
135
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
136
|
+
const appTitle = chalk_1.default.bold.cyan(" 🔒 XtraSecurity") + chalk_1.default.hex("#4a5a6b")(" — Interactive Secrets Shell");
|
|
137
|
+
const env = ENV_COLORS[ENVS[envIdx]] ? ENV_COLORS[ENVS[envIdx]](ENVS[envIdx]) : chalk_1.default.white(ENVS[envIdx]);
|
|
138
|
+
const crumb = projects[projIdx]
|
|
139
|
+
? chalk_1.default.hex("#4a5568")(` ${projects[projIdx].name} `) + chalk_1.default.hex("#4a5568")("/") + chalk_1.default.hex("#4a5568")(` ${ENVS[envIdx]}`)
|
|
140
|
+
: chalk_1.default.hex("#4a5568")(" No project selected");
|
|
144
141
|
console.log();
|
|
145
|
-
|
|
142
|
+
console.log(appTitle + chalk_1.default.hex("#2d3a4d")(" │") + crumb);
|
|
143
|
+
console.log(chalk_1.default.hex("#1e293b")(hline(W)));
|
|
146
144
|
console.log();
|
|
145
|
+
// ── Left column: Projects (22 chr) + Env (22 chr) ─────────────────────
|
|
146
|
+
const LEFT_W = 26;
|
|
147
|
+
const RIGHT_W = W - LEFT_W - 3;
|
|
148
|
+
// Projects panel
|
|
149
|
+
const projLines = (projects.length > 0 ? projects : [{ id: "", name: "(loading…)" }]).map((p, i) => {
|
|
150
|
+
const active = i === projIdx;
|
|
151
|
+
const icon = active ? chalk_1.default.cyan("▶ ") : " ";
|
|
152
|
+
const name = active && panel === "projects"
|
|
153
|
+
? chalk_1.default.bold.bgCyan.black(` ${p.name} `.padEnd(LEFT_W - 4))
|
|
154
|
+
: active
|
|
155
|
+
? chalk_1.default.bold.cyan(p.name)
|
|
156
|
+
: chalk_1.default.hex("#6b7c93")(p.name);
|
|
157
|
+
return ` ${icon}${name}`;
|
|
158
|
+
});
|
|
159
|
+
const projBox = box("PROJECTS", projLines, LEFT_W, panel === "projects");
|
|
160
|
+
// Env panel
|
|
161
|
+
const envLines = ENVS.map((e, i) => {
|
|
162
|
+
const active = i === envIdx;
|
|
163
|
+
const icon = active ? chalk_1.default.cyan("▶ ") : " ";
|
|
164
|
+
const col = ENV_COLORS[e] ?? chalk_1.default.white;
|
|
165
|
+
const name = active && panel === "env"
|
|
166
|
+
? chalk_1.default.bold.bgCyan.black(` ${e} `.padEnd(LEFT_W - 4))
|
|
167
|
+
: active
|
|
168
|
+
? col.bold(e)
|
|
169
|
+
: chalk_1.default.hex("#6b7c93")(e);
|
|
170
|
+
return ` ${icon}${name}`;
|
|
171
|
+
});
|
|
172
|
+
const envBox = box("ENVIRONMENT", envLines, LEFT_W, panel === "env");
|
|
173
|
+
// Secrets panel
|
|
174
|
+
const KW = Math.floor((RIGHT_W - 3) * 0.45);
|
|
175
|
+
const VW = Math.floor((RIGHT_W - 3) * 0.35);
|
|
176
|
+
const DW = RIGHT_W - KW - VW - 4;
|
|
177
|
+
const secHeader = chalk_1.default.bold.hex("#64748b")("KEY".padEnd(KW)) + " " + chalk_1.default.bold.hex("#64748b")("VALUE".padEnd(VW)) + " " + chalk_1.default.bold.hex("#64748b")("UPDATED".padEnd(DW));
|
|
178
|
+
const divider = chalk_1.default.hex("#1e293b")(hline(KW) + " " + hline(VW) + " " + hline(DW));
|
|
179
|
+
let secLines;
|
|
147
180
|
if (loading) {
|
|
148
|
-
|
|
181
|
+
secLines = [` ${chalk_1.default.cyan("⠋")} Loading…`];
|
|
182
|
+
}
|
|
183
|
+
else if (secrets.length === 0) {
|
|
184
|
+
secLines = [` ${chalk_1.default.hex("#4a5568")("No secrets in this environment.")}`];
|
|
149
185
|
}
|
|
150
186
|
else {
|
|
151
|
-
|
|
187
|
+
secLines = [" " + secHeader, " " + divider, ...secrets.map((s, i) => {
|
|
188
|
+
const sel = i === secIdx && panel === "secrets";
|
|
189
|
+
const key = s.key.padEnd(KW).slice(0, KW);
|
|
190
|
+
const val = showValues ? s.value.padEnd(VW).slice(0, VW) : "●".repeat(Math.min(8, VW)).padEnd(VW).slice(0, VW);
|
|
191
|
+
const dt = new Date(s.updatedAt).toLocaleDateString("en-GB", { day: "2-digit", month: "short" }).padEnd(DW);
|
|
192
|
+
if (sel) {
|
|
193
|
+
return chalk_1.default.bold.bgHex("#0e243e")(" ▶ " + chalk_1.default.cyan(key) + " " + chalk_1.default.greenBright(val) + " " + chalk_1.default.hex("#4a9eff")(dt) + " ");
|
|
194
|
+
}
|
|
195
|
+
return " " + chalk_1.default.white(key) + " " + chalk_1.default.hex("#4a5568")(val) + " " + chalk_1.default.hex("#374151")(dt);
|
|
196
|
+
})];
|
|
197
|
+
}
|
|
198
|
+
const secBox = box(`SECRETS (${secrets.length})`, secLines, RIGHT_W, panel === "secrets");
|
|
199
|
+
// ── Print side-by-side ────────────────────────────────────────────────
|
|
200
|
+
const leftPanel = [...projBox, ...["", ""], ...envBox];
|
|
201
|
+
const maxRows = Math.max(leftPanel.length, secBox.length);
|
|
202
|
+
const gap = chalk_1.default.hex("#1e293b")(" │ ");
|
|
203
|
+
for (let r = 0; r < maxRows; r++) {
|
|
204
|
+
const l = leftPanel[r] ?? " ".repeat(LEFT_W);
|
|
205
|
+
const rr = secBox[r] ?? " ".repeat(RIGHT_W);
|
|
206
|
+
console.log(l + gap + rr);
|
|
152
207
|
}
|
|
153
|
-
|
|
208
|
+
// ── Footer ───────────────────────────────────────────────────────────
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(chalk_1.default.hex("#1e293b")(hline(W)));
|
|
211
|
+
const keys = [
|
|
212
|
+
[chalk_1.default.hex("#64748b")("↑↓"), "Navigate"],
|
|
213
|
+
[chalk_1.default.hex("#64748b")("Enter"), "Select"],
|
|
214
|
+
[chalk_1.default.hex("#64748b")("Tab"), "Switch Panel"],
|
|
215
|
+
[chalk_1.default.hex("#64748b")("V"), showValues ? chalk_1.default.green("Hide values") : chalk_1.default.yellow("Show values")],
|
|
216
|
+
[chalk_1.default.hex("#64748b")("Q"), "Quit"],
|
|
217
|
+
].map(([k, a]) => `${k} ${chalk_1.default.hex("#374151")(a)}`).join(chalk_1.default.hex("#1e293b")(" · "));
|
|
218
|
+
console.log(" " + keys);
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(" " + (status || ""));
|
|
154
221
|
}
|
|
155
|
-
//
|
|
222
|
+
// ── Keyboard ──────────────────────────────────────────────────────────────
|
|
156
223
|
readline.emitKeypressEvents(process.stdin);
|
|
157
224
|
if (process.stdin.isTTY)
|
|
158
225
|
process.stdin.setRawMode(true);
|
|
159
|
-
process.stdin.on("keypress", async (
|
|
226
|
+
process.stdin.on("keypress", async (_str, key) => {
|
|
160
227
|
if (!key)
|
|
161
228
|
return;
|
|
162
229
|
// Quit
|
|
163
|
-
if (
|
|
230
|
+
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
164
231
|
if (process.stdin.isTTY)
|
|
165
232
|
process.stdin.setRawMode(false);
|
|
166
233
|
clear();
|
|
167
|
-
console.log(chalk_1.default.cyan("Goodbye!
|
|
234
|
+
console.log(chalk_1.default.cyan(" Goodbye! 👋\n"));
|
|
168
235
|
process.exit(0);
|
|
169
236
|
}
|
|
170
|
-
//
|
|
237
|
+
// Toggle secret values with V
|
|
238
|
+
if (key.name === "v") {
|
|
239
|
+
showValues = !showValues;
|
|
240
|
+
draw();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Tab cycle panels
|
|
171
244
|
if (key.name === "tab") {
|
|
172
|
-
|
|
245
|
+
panel = panel === "projects" ? "env" : panel === "env" ? "secrets" : "projects";
|
|
173
246
|
draw();
|
|
174
247
|
return;
|
|
175
248
|
}
|
|
176
|
-
|
|
177
|
-
if (screen === "project") {
|
|
249
|
+
if (panel === "projects") {
|
|
178
250
|
if (key.name === "up")
|
|
179
|
-
|
|
251
|
+
projIdx = Math.max(0, projIdx - 1);
|
|
180
252
|
if (key.name === "down")
|
|
181
|
-
|
|
253
|
+
projIdx = Math.min(projects.length - 1, projIdx + 1);
|
|
182
254
|
if (key.name === "return") {
|
|
183
|
-
|
|
184
|
-
|
|
255
|
+
panel = "env";
|
|
256
|
+
draw();
|
|
185
257
|
return;
|
|
186
258
|
}
|
|
187
259
|
}
|
|
188
|
-
if (
|
|
260
|
+
if (panel === "env") {
|
|
189
261
|
if (key.name === "up")
|
|
190
262
|
envIdx = Math.max(0, envIdx - 1);
|
|
191
263
|
if (key.name === "down")
|
|
192
264
|
envIdx = Math.min(ENVS.length - 1, envIdx + 1);
|
|
193
265
|
if (key.name === "return") {
|
|
194
|
-
|
|
266
|
+
panel = "secrets";
|
|
195
267
|
await loadSecrets();
|
|
196
268
|
return;
|
|
197
269
|
}
|
|
198
270
|
}
|
|
199
|
-
if (
|
|
271
|
+
if (panel === "secrets") {
|
|
200
272
|
if (key.name === "up")
|
|
201
|
-
|
|
273
|
+
secIdx = Math.max(0, secIdx - 1);
|
|
202
274
|
if (key.name === "down")
|
|
203
|
-
|
|
275
|
+
secIdx = Math.min(secrets.length - 1, secIdx + 1);
|
|
204
276
|
}
|
|
205
277
|
draw();
|
|
206
278
|
});
|
|
279
|
+
// ── Boot ──────────────────────────────────────────────────────────────────
|
|
207
280
|
await loadProjects();
|
|
208
281
|
if (projects.length > 0)
|
|
209
282
|
await loadSecrets();
|
|
210
283
|
else
|
|
211
284
|
draw();
|
|
212
285
|
}
|
|
213
|
-
//
|
|
286
|
+
// ─── Commander ───────────────────────────────────────────────────────────────
|
|
214
287
|
exports.uiCommand = new commander_1.Command("ui")
|
|
215
|
-
.description("Launch interactive TUI secrets dashboard (arrow keys
|
|
216
|
-
.action(async () => {
|
|
217
|
-
await runUI();
|
|
218
|
-
});
|
|
288
|
+
.description("Launch interactive TUI secrets dashboard (arrow keys, Tab, Q to quit)")
|
|
289
|
+
.action(async () => { await runUI(); });
|