xtra-cli 0.2.1 → 0.2.3

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.
@@ -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 — Diagnose common xtra-cli setup issues
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
- const PASS = chalk_1.default.green(" ✔");
53
- const FAIL = chalk_1.default.red(" ✗");
54
- const WARN = chalk_1.default.yellow(" âš ");
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
- // ── 1. Node Version ─────────────────────────────────────────────────────────
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
- // ── 2. Auth Token ────────────────────────────────────────────────────────────
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 — run 'xtra login'",
71
+ message: token ? `Set (${token.substring(0, 8)}...)` : "Not set run 'xtra login'",
71
72
  });
72
- // ── 3. API URL Configured ────────────────────────────────────────────────────
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 || "Using default (https://xtra-security.vercel.app/api)",
77
+ status: config.apiUrl || process.env.XTRA_API_URL ? "pass" : "warn",
78
+ message: apiUrl,
78
79
  });
79
- // ── 4. API Reachability ──────────────────────────────────────────────────────
80
+ // 4. API Reachability
80
81
  try {
81
- const res = await axios_1.default.get(`${config.apiUrl}/health`, { timeout: 4000 });
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: `${config.apiUrl} → ${res.status} ${res.statusText}`,
86
+ message: `${res.status} ${res.statusText}`,
86
87
  });
87
88
  }
88
89
  catch (e) {
89
- const msg = e.code === "ECONNREFUSED"
90
- ? "Connection refused — is the server running?"
91
- : e.code === "ETIMEDOUT"
92
- ? "Timed out — server may be slow or unreachable"
93
- : e.message;
94
- checks.push({ name: "API Connectivity", status: "fail", message: msg });
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
- // ── 5. Active Project ────────────────────────────────────────────────────────
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 — run 'xtra project set <id>'",
103
+ message: project ? project : "Not set run 'xtra project set <id>'",
102
104
  });
103
- // ── 6. Active Branch ─────────────────────────────────────────────────────────
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 || "main (default)",
110
+ message: branch,
109
111
  });
110
- // ── 7. .xtrarc / xtra.json ─────────────────────────────────────────────────
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 hasProjectConfig = fs.existsSync(rcPath) || fs.existsSync(jsonPath);
115
+ const hasConfig = fs.existsSync(rcPath) || fs.existsSync(jsonPath);
114
116
  checks.push({
115
- name: "Project Config (.xtrarc)",
116
- status: hasProjectConfig ? "pass" : "warn",
117
- message: hasProjectConfig
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 common setup issues (connectivity, auth, config)")
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(chalk_1.default.bold("\n🩺 xtra doctor — running diagnostics...\n"));
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.bold(" ✅ All checks passed! Your CLI is ready to use."));
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
- console.log(chalk_1.default.red(` ${failures} issue(s) need attention.`));
158
+ parts.push(chalk_1.default.red(`${failures} error(s)`));
172
159
  if (warnings > 0)
173
- console.log(chalk_1.default.yellow(` ${warnings} warning(s) found.`));
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
  });
@@ -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, // false by default; --shell enables for npm scripts / Windows
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);
@@ -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
- // ─── Rendering ────────────────────────────────────────────────────────────────
46
- function clear() { process.stdout.write("\x1Bc"); }
47
- function renderHeader() {
48
- console.log(chalk_1.default.cyan("╔═══════════════════════════════════════════════════════════╗"));
49
- console.log(chalk_1.default.cyan("║") + chalk_1.default.bold.cyan(" 🔒 XtraSecurity — Interactive Shell ") + chalk_1.default.cyan("║"));
50
- console.log(chalk_1.default.cyan("╚═══════════════════════════════════════════════════════════╝"));
51
- console.log(chalk_1.default.gray(" ↑↓ Navigate • Enter Select • Tab Switch Panel • Q Quit\n"));
52
- }
53
- function renderList(title, items, selectedIdx, active) {
54
- const border = active ? chalk_1.default.cyan : chalk_1.default.gray;
55
- console.log(border(`┌─ ${title} ${"─".repeat(Math.max(0, 28 - title.length))}┐`));
56
- items.forEach((item, i) => {
57
- const isSelected = i === selectedIdx;
58
- const icon = isSelected ? (active ? chalk_1.default.cyan("â–¶ ") : chalk_1.default.gray("â–¶ ")) : " ";
59
- const text = isSelected && active ? chalk_1.default.bold.cyan(item) : isSelected ? chalk_1.default.cyan(item) : chalk_1.default.white(item);
60
- console.log(border("│") + ` ${icon}${text}`.padEnd(30) + border("│"));
61
- });
62
- console.log(border(`└${"─".repeat(32)}┘`));
63
- }
64
- function renderSecrets(secrets, selectedIdx, active) {
65
- const border = active ? chalk_1.default.cyan : chalk_1.default.gray;
66
- const COLS = [36, 12, 20];
67
- const sep = "─".repeat(COLS[0]) + "┬" + "─".repeat(COLS[1]) + "┬" + "─".repeat(COLS[2]);
68
- console.log(border(`┌${sep}┐`));
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
- // ─── Main TUI Loop ────────────────────────────────────────────────────────────
71
+ function clear() { process.stdout.write("\x1B[H\x1B[2J\x1B[3J"); }
72
+ // ─── Main TUI ────────────────────────────────────────────────────────────────
100
73
  async function runUI() {
101
- let screen = "project";
74
+ let panel = "projects";
102
75
  let projects = [];
103
- let projectIdx = 0;
76
+ let projIdx = 0;
104
77
  let envIdx = 0;
105
78
  let secrets = [];
106
- let secretIdx = 0;
107
- let status = "Loading projects...";
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
- projects = await api_1.api.getProjects();
88
+ const raw = await api_1.api.getProjects();
89
+ projects = Array.isArray(raw) ? raw : raw.projects ?? [];
112
90
  status = projects.length > 0
113
- ? `Loaded ${projects.length} projects. Use ↑↓ and Enter to navigate.`
114
- : "No projects found. Try running `xtra login`.";
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(`Error: ${e.message}`);
95
+ status = chalk_1.default.red(`✗ ${e.message}`);
118
96
  }
119
97
  draw();
120
98
  }
121
99
  async function loadSecrets() {
122
- if (!projects[projectIdx])
100
+ if (!projects[projIdx])
123
101
  return;
124
102
  loading = true;
125
- status = `Fetching secrets for ${projects[projectIdx].name}/${ENVS[envIdx]}...`;
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
- secrets = await api_1.api.getSecrets(projects[projectIdx].id, ENVS[envIdx]);
129
- secretIdx = 0;
130
- status = `${secrets.length} secret(s) loaded. Tab to switch panel, Q to quit.`;
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(`Error: ${e.message}`);
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
- renderHeader();
142
- const projectNames = projects.map(p => p.name);
143
- renderList("PROJECTS", projectNames.length ? projectNames : ["(loading...)"], projectIdx, screen === "project");
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
- renderList("ENVIRONMENT", ENVS, envIdx, screen === "env");
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
- console.log(chalk_1.default.cyan(" â ‹ Loading secrets..."));
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
- renderSecrets(secrets, secretIdx, screen === "secrets");
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
- renderStatus(status);
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
- // Set up raw mode for keyboard input
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 (str, key) => {
226
+ process.stdin.on("keypress", async (_str, key) => {
160
227
  if (!key)
161
228
  return;
162
229
  // Quit
163
- if (str === "q" || str === "Q" || (key.ctrl && key.name === "c")) {
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
- // Tab to cycle panels
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
- screen = screen === "project" ? "env" : screen === "env" ? "secrets" : "project";
245
+ panel = panel === "projects" ? "env" : panel === "env" ? "secrets" : "projects";
173
246
  draw();
174
247
  return;
175
248
  }
176
- // Navigation based on active panel
177
- if (screen === "project") {
249
+ if (panel === "projects") {
178
250
  if (key.name === "up")
179
- projectIdx = Math.max(0, projectIdx - 1);
251
+ projIdx = Math.max(0, projIdx - 1);
180
252
  if (key.name === "down")
181
- projectIdx = Math.min(projects.length - 1, projectIdx + 1);
253
+ projIdx = Math.min(projects.length - 1, projIdx + 1);
182
254
  if (key.name === "return") {
183
- screen = "env";
184
- await loadSecrets();
255
+ panel = "env";
256
+ draw();
185
257
  return;
186
258
  }
187
259
  }
188
- if (screen === "env") {
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
- screen = "secrets";
266
+ panel = "secrets";
195
267
  await loadSecrets();
196
268
  return;
197
269
  }
198
270
  }
199
- if (screen === "secrets") {
271
+ if (panel === "secrets") {
200
272
  if (key.name === "up")
201
- secretIdx = Math.max(0, secretIdx - 1);
273
+ secIdx = Math.max(0, secIdx - 1);
202
274
  if (key.name === "down")
203
- secretIdx = Math.min(secrets.length - 1, secretIdx + 1);
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
- // ─── Commander Command ────────────────────────────────────────────────────────
286
+ // ─── Commander ───────────────────────────────────────────────────────────────
214
287
  exports.uiCommand = new commander_1.Command("ui")
215
- .description("Launch interactive TUI secrets dashboard (arrow keys to navigate, Q to quit)")
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(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtra-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI for XtraSecurity Platform",
5
5
  "main": "dist/index.js",
6
6
  "bin": {