xtra-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/dist/bin/xtra.js +124 -0
  4. package/dist/commands/access.js +107 -0
  5. package/dist/commands/admin.js +118 -0
  6. package/dist/commands/audit.js +67 -0
  7. package/dist/commands/branch.js +216 -0
  8. package/dist/commands/checkout.js +74 -0
  9. package/dist/commands/ci.js +330 -0
  10. package/dist/commands/completion.js +227 -0
  11. package/dist/commands/diff.js +163 -0
  12. package/dist/commands/doctor.js +176 -0
  13. package/dist/commands/env.js +70 -0
  14. package/dist/commands/export.js +84 -0
  15. package/dist/commands/generate.js +180 -0
  16. package/dist/commands/history.js +77 -0
  17. package/dist/commands/import.js +122 -0
  18. package/dist/commands/init.js +162 -0
  19. package/dist/commands/integration.js +188 -0
  20. package/dist/commands/local.js +198 -0
  21. package/dist/commands/login.js +176 -0
  22. package/dist/commands/login.test.js +51 -0
  23. package/dist/commands/logs.js +121 -0
  24. package/dist/commands/profile.js +184 -0
  25. package/dist/commands/project.js +98 -0
  26. package/dist/commands/rollback.js +96 -0
  27. package/dist/commands/rotate.js +94 -0
  28. package/dist/commands/run.js +215 -0
  29. package/dist/commands/scan.js +127 -0
  30. package/dist/commands/secrets.js +265 -0
  31. package/dist/commands/simulate.js +92 -0
  32. package/dist/commands/status.js +94 -0
  33. package/dist/commands/template.js +276 -0
  34. package/dist/commands/ui.js +218 -0
  35. package/dist/commands/watch.js +121 -0
  36. package/dist/lib/api.js +172 -0
  37. package/dist/lib/api.test.js +89 -0
  38. package/dist/lib/audit.js +136 -0
  39. package/dist/lib/config.js +42 -0
  40. package/dist/lib/config.test.js +47 -0
  41. package/dist/lib/crypto.js +50 -0
  42. package/dist/lib/manifest.js +52 -0
  43. package/dist/lib/profiles.js +103 -0
  44. package/package.json +67 -0
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.simulateCommand = void 0;
7
+ /**
8
+ * simulate.ts β€” Dry-run mode for xtra run
9
+ *
10
+ * Fetches secrets and shows exactly what `xtra run` would inject into
11
+ * the process environment β€” without actually executing the command.
12
+ * Safe to use in any environment.
13
+ */
14
+ const commander_1 = require("commander");
15
+ const api_1 = require("../lib/api");
16
+ const config_1 = require("../lib/config");
17
+ const chalk_1 = __importDefault(require("chalk"));
18
+ const ora_1 = __importDefault(require("ora"));
19
+ const table_1 = require("table");
20
+ exports.simulateCommand = new commander_1.Command("simulate")
21
+ .description("Dry-run: show what 'xtra run' would inject without executing the command")
22
+ .option("-p, --project <id>", "Project ID")
23
+ .option("-e, --env <environment>", "Environment", "development")
24
+ .option("-b, --branch <branch>", "Branch", "main")
25
+ .argument("[command]", "Command that would be executed (display only)")
26
+ .option("--show-values", "Reveal secret values in output (default: masked)", false)
27
+ .option("--diff", "Highlight secrets that differ from local .env / process.env", false)
28
+ .action(async (command, options) => {
29
+ let { project, env, branch, showValues, diff: showDiff } = options;
30
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
31
+ env = envMap[env] || env;
32
+ if (!project)
33
+ project = (0, config_1.getConfigValue)("project");
34
+ if (!project) {
35
+ console.error(chalk_1.default.red("Error: Project ID required. Use -p <id> or run 'xtra project set'."));
36
+ process.exit(1);
37
+ }
38
+ const spinner = (0, ora_1.default)(`Fetching secrets to simulate injection (${env}/${branch})...`).start();
39
+ let secrets;
40
+ try {
41
+ secrets = await api_1.api.getSecrets(project, env, branch);
42
+ spinner.stop();
43
+ }
44
+ catch (e) {
45
+ spinner.fail("Failed to fetch secrets.");
46
+ console.error(chalk_1.default.red(e?.response?.data?.error || e.message));
47
+ process.exit(1);
48
+ }
49
+ const count = Object.keys(secrets).length;
50
+ console.log(chalk_1.default.bold(`\nπŸ”¬ Simulation β€” xtra run ${command ? chalk_1.default.cyan(command) : "(no command specified)"}\n`));
51
+ console.log(chalk_1.default.gray(` Project : ${project}`));
52
+ console.log(chalk_1.default.gray(` Env : ${env}`));
53
+ console.log(chalk_1.default.gray(` Branch : ${branch}`));
54
+ console.log(chalk_1.default.gray(` Secrets : ${count} would be injected\n`));
55
+ if (count === 0) {
56
+ console.log(chalk_1.default.yellow(" No secrets found for this environment/branch."));
57
+ return;
58
+ }
59
+ const rows = [
60
+ [chalk_1.default.bold("Key"), chalk_1.default.bold("Injected Value"), chalk_1.default.bold("Local .env"), chalk_1.default.bold("Status")]
61
+ ];
62
+ for (const [key, value] of Object.entries(secrets)) {
63
+ const localVal = process.env[key];
64
+ const displayVal = showValues ? chalk_1.default.cyan(value) : chalk_1.default.gray("β€’β€’β€’β€’β€’β€’β€’β€’");
65
+ let status = chalk_1.default.green("Injected");
66
+ let localDisplay = "-";
67
+ if (showDiff && localVal !== undefined) {
68
+ localDisplay = showValues ? chalk_1.default.yellow(localVal) : chalk_1.default.gray("β€’β€’β€’β€’β€’β€’β€’β€’");
69
+ if (localVal === value) {
70
+ status = chalk_1.default.gray("Same");
71
+ }
72
+ else {
73
+ status = chalk_1.default.yellow("⚑ Override");
74
+ }
75
+ }
76
+ else if (showDiff) {
77
+ status = chalk_1.default.blue("New");
78
+ localDisplay = chalk_1.default.gray("(not set)");
79
+ }
80
+ rows.push([chalk_1.default.white(key), displayVal, showDiff ? localDisplay : "-", status]);
81
+ }
82
+ console.log((0, table_1.table)(rows));
83
+ // Summary
84
+ if (!showValues) {
85
+ console.log(chalk_1.default.gray(" Tip: use --show-values to reveal actual secret values"));
86
+ }
87
+ if (!showDiff) {
88
+ console.log(chalk_1.default.gray(" Tip: use --diff to compare against your local process.env"));
89
+ }
90
+ console.log(chalk_1.default.bold(`\n βœ… Simulation complete. ${count} secret(s) would be injected.`));
91
+ console.log(chalk_1.default.gray(" Run without 'simulate' to actually execute the command.\n"));
92
+ });
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.statusCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const api_1 = require("../lib/api");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const ora_1 = __importDefault(require("ora"));
11
+ const table_1 = require("table");
12
+ const manifest_1 = require("../lib/manifest");
13
+ const crypto_1 = require("../lib/crypto");
14
+ const config_1 = require("../lib/config");
15
+ exports.statusCommand = new commander_1.Command("status")
16
+ .description("Check synchronization status with XtraSync platform")
17
+ .option("-p, --project <projectId>", "Project ID")
18
+ .option("-e, --env <environment>", "Environment (dev, stg, prod)", "dev")
19
+ .option("-b, --branch <branchName>", "Branch Name")
20
+ .action(async (options) => {
21
+ let { project, env, branch } = options;
22
+ // Use config fallback
23
+ if (!project) {
24
+ project = (0, config_1.getConfigValue)("project");
25
+ }
26
+ if (!branch) {
27
+ branch = (0, config_1.getConfigValue)("branch") || "main";
28
+ }
29
+ if (!project) {
30
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
31
+ process.exit(1);
32
+ }
33
+ // Normalize Env
34
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
35
+ env = envMap[env] || env;
36
+ // Load Manifest
37
+ const manifest = (0, manifest_1.loadManifest)();
38
+ if (!manifest) {
39
+ console.log(chalk_1.default.yellow("No local manifest found. Run 'xtra run' or 'xtra generate' first."));
40
+ return;
41
+ }
42
+ if (manifest.projectId !== project || manifest.environment !== env) {
43
+ console.log(chalk_1.default.yellow(`Manifest found for DIFFERENT project/env (${manifest.projectId}/${manifest.environment}).`));
44
+ console.log("Checking status against requested target anyway...");
45
+ }
46
+ const spinner = (0, ora_1.default)(`Fetching remote state for ${env} (branch: ${branch})...`).start();
47
+ try {
48
+ const secrets = await api_1.api.getSecrets(project, env, branch);
49
+ spinner.stop();
50
+ // Compare
51
+ const rows = [[chalk_1.default.bold("Secret Key"), chalk_1.default.bold("Status"), chalk_1.default.bold("Local Last Updated")]];
52
+ let diffCount = 0;
53
+ // Check Remote vs Local
54
+ Object.entries(secrets).forEach(([key, value]) => {
55
+ const remoteHash = (0, crypto_1.hash)(value);
56
+ const localEntry = manifest.secrets[key];
57
+ if (!localEntry) {
58
+ rows.push([key, chalk_1.default.green("NEW (Remote)"), "-"]);
59
+ diffCount++;
60
+ }
61
+ else if (localEntry.hash !== remoteHash) {
62
+ rows.push([key, chalk_1.default.yellow("MODIFIED"), localEntry.updatedAt]);
63
+ diffCount++;
64
+ }
65
+ else {
66
+ rows.push([key, chalk_1.default.cyan("Synced"), localEntry.updatedAt]);
67
+ }
68
+ });
69
+ // Check Local vs Remote (Deleted keys)
70
+ Object.keys(manifest.secrets).forEach(key => {
71
+ if (!secrets[key]) {
72
+ rows.push([key, chalk_1.default.red("DELETED (Remote)"), manifest.secrets[key].updatedAt]);
73
+ diffCount++;
74
+ }
75
+ });
76
+ console.log("");
77
+ console.log((0, table_1.table)(rows));
78
+ if (diffCount === 0) {
79
+ console.log(chalk_1.default.green("βœ” Everything is in sync."));
80
+ }
81
+ else {
82
+ console.log(chalk_1.default.yellow(`⚠ Found ${diffCount} difference(s).\n`));
83
+ console.log(chalk_1.default.bold(" Next steps:"));
84
+ console.log(chalk_1.default.gray(" xtra generate ") + chalk_1.default.white("# pull cloud secrets β†’ .env (merge)"));
85
+ console.log(chalk_1.default.gray(" xtra generate -f json ") + chalk_1.default.white("# pull cloud secrets β†’ secrets.json"));
86
+ console.log(chalk_1.default.gray(" xtra run node app.js ") + chalk_1.default.white("# inject secrets at runtime (no file)"));
87
+ console.log(chalk_1.default.gray(" xtra local sync ") + chalk_1.default.white("# pull cloud secrets β†’ .env.local (offline mode)"));
88
+ }
89
+ }
90
+ catch (error) {
91
+ spinner.fail("Failed to fetch remote secrets.");
92
+ console.error(chalk_1.default.red(error.message));
93
+ }
94
+ });
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ /**
3
+ * template.ts β€” Secret Templating Engine for xtra-cli
4
+ *
5
+ * Reads a template file containing {{ secrets.KEY }} placeholders,
6
+ * fetches secrets from XtraSecurity Cloud, substitutes all placeholders,
7
+ * and writes the rendered output to the specified file (or stdout).
8
+ *
9
+ * Supported placeholder syntaxes:
10
+ * {{ secrets.DATABASE_URL }} β†’ fetches DATABASE_URL from secrets
11
+ * {{ secrets.PORT | 3000 }} β†’ with fallback default value "3000"
12
+ * {{ env.NODE_ENV }} β†’ reads from local process environment
13
+ *
14
+ * Example:
15
+ * # config.yaml.tpl
16
+ * database:
17
+ * url: {{ secrets.DATABASE_URL }}
18
+ * pool: {{ secrets.DB_POOL_SIZE | 10 }}
19
+ * app:
20
+ * port: {{ env.PORT | 3000 }}
21
+ * env: {{ env.NODE_ENV | development }}
22
+ *
23
+ * Usage:
24
+ * xtra template render config.yaml.tpl -o config.yaml -p proj123 -e production
25
+ * xtra template render nginx.conf.tpl | tee /etc/nginx/nginx.conf
26
+ * xtra template check config.yaml.tpl -p proj123 -e production
27
+ */
28
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ var desc = Object.getOwnPropertyDescriptor(m, k);
31
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
+ desc = { enumerable: true, get: function() { return m[k]; } };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }) : (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ o[k2] = m[k];
38
+ }));
39
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
41
+ }) : function(o, v) {
42
+ o["default"] = v;
43
+ });
44
+ var __importStar = (this && this.__importStar) || (function () {
45
+ var ownKeys = function(o) {
46
+ ownKeys = Object.getOwnPropertyNames || function (o) {
47
+ var ar = [];
48
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
+ return ar;
50
+ };
51
+ return ownKeys(o);
52
+ };
53
+ return function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
60
+ })();
61
+ var __importDefault = (this && this.__importDefault) || function (mod) {
62
+ return (mod && mod.__esModule) ? mod : { "default": mod };
63
+ };
64
+ Object.defineProperty(exports, "__esModule", { value: true });
65
+ exports.templateCommand = void 0;
66
+ const commander_1 = require("commander");
67
+ const api_1 = require("../lib/api");
68
+ const config_1 = require("../lib/config");
69
+ const chalk_1 = __importDefault(require("chalk"));
70
+ const ora_1 = __importDefault(require("ora"));
71
+ const fs = __importStar(require("fs"));
72
+ const path = __importStar(require("path"));
73
+ // ─── Placeholder Regex ────────────────────────────────────────────────────────
74
+ // Matches: {{ secrets.KEY }}, {{ secrets.KEY | default }}, {{ env.KEY | default }}
75
+ const PLACEHOLDER_RE = /\{\{\s*(secrets|env)\.([A-Z0-9_a-z]+)(?:\s*\|\s*([^}]*?))?\s*\}\}/g;
76
+ function renderTemplate(template, secrets, processEnv) {
77
+ const missingKeys = [];
78
+ const usedDefaults = [];
79
+ let replacedCount = 0;
80
+ const output = template.replace(PLACEHOLDER_RE, (_, source, key, defaultVal) => {
81
+ let value;
82
+ if (source === "secrets") {
83
+ value = secrets[key];
84
+ }
85
+ else if (source === "env") {
86
+ value = processEnv[key];
87
+ }
88
+ if (value !== undefined && value !== "") {
89
+ replacedCount++;
90
+ return value;
91
+ }
92
+ if (defaultVal !== undefined) {
93
+ const trimmedDefault = defaultVal.trim();
94
+ usedDefaults.push(`${source}.${key} β†’ "${trimmedDefault}"`);
95
+ replacedCount++;
96
+ return trimmedDefault;
97
+ }
98
+ missingKeys.push(`${source}.${key}`);
99
+ return `{{ ${source}.${key} }}`; // Leave unreplaced β€” user will see it clearly
100
+ });
101
+ return { output, replacedCount, missingKeys, usedDefaults };
102
+ }
103
+ // ─── Command ──────────────────────────────────────────────────────────────────
104
+ exports.templateCommand = new commander_1.Command("template")
105
+ .description("Secret templating engine β€” inject secrets into config file templates")
106
+ .addHelpText("after", `
107
+ Template Syntax:
108
+ {{ secrets.KEY }} Replace with secret value
109
+ {{ secrets.KEY | default }} Replace with secret value, fall back to "default"
110
+ {{ env.KEY }} Replace with local environment variable
111
+ {{ env.KEY | default }} Replace with local env variable, fall back to "default"
112
+
113
+ Examples:
114
+ $ xtra template render config.yaml.tpl -p proj123 -e production -o config.yaml
115
+ $ xtra template render nginx.conf.tpl -p proj123 -e production | sudo tee /etc/nginx/nginx.conf
116
+ $ xtra template check config.yaml.tpl -p proj123 -e production
117
+ $ xtra template list config.yaml.tpl
118
+ `);
119
+ // ── render ────────────────────────────────────────────────────────────────────
120
+ exports.templateCommand
121
+ .command("render <templateFile>")
122
+ .description("Render a template file by injecting secrets and environment variables")
123
+ .option("-p, --project <id>", "Project ID")
124
+ .option("-e, --env <environment>", "Environment (default: development)", "development")
125
+ .option("-b, --branch <branch>", "Branch name", "main")
126
+ .option("-o, --output <file>", "Output file (default: stdout)")
127
+ .option("--strict", "Exit with error if any placeholder is unresolved", false)
128
+ .action(async (templateFile, options) => {
129
+ let { project, env, branch, output, strict } = options;
130
+ // Normalize env shorthand
131
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
132
+ env = envMap[env] || env;
133
+ if (!project)
134
+ project = (0, config_1.getConfigValue)("project");
135
+ if (!project) {
136
+ console.error(chalk_1.default.red("Error: Project ID required. Use -p <id> or run 'xtra project set'."));
137
+ process.exit(1);
138
+ }
139
+ // Read template file
140
+ const templatePath = path.resolve(process.cwd(), templateFile);
141
+ if (!fs.existsSync(templatePath)) {
142
+ console.error(chalk_1.default.red(`Error: Template file not found: ${templatePath}`));
143
+ process.exit(1);
144
+ }
145
+ const template = fs.readFileSync(templatePath, "utf8");
146
+ // Count how many placeholders exist
147
+ const allPlaceholders = [...template.matchAll(PLACEHOLDER_RE)];
148
+ if (allPlaceholders.length === 0) {
149
+ console.warn(chalk_1.default.yellow("Warning: No {{ secrets.* }} or {{ env.* }} placeholders found in template."));
150
+ }
151
+ const spinner = (0, ora_1.default)(`Fetching secrets for ${project}/${env}...`).start();
152
+ let secrets = {};
153
+ try {
154
+ secrets = await api_1.api.getSecrets(project, env, branch);
155
+ spinner.succeed(`Loaded ${Object.keys(secrets).length} secrets.`);
156
+ }
157
+ catch (e) {
158
+ spinner.fail("Failed to fetch secrets.");
159
+ const safeErr = e?.response?.data?.error || e.message;
160
+ console.error(chalk_1.default.red(safeErr));
161
+ process.exit(1);
162
+ }
163
+ // Render
164
+ const { output: rendered, replacedCount, missingKeys, usedDefaults } = renderTemplate(template, secrets, process.env);
165
+ // Report
166
+ console.log(chalk_1.default.green(`βœ… Replaced ${replacedCount} placeholder(s).`));
167
+ if (usedDefaults.length > 0) {
168
+ console.log(chalk_1.default.yellow(`⚠ Used defaults for: ${usedDefaults.join(", ")}`));
169
+ }
170
+ if (missingKeys.length > 0) {
171
+ console.log(chalk_1.default.red(`βœ— Unresolved placeholders: ${missingKeys.join(", ")}`));
172
+ if (strict) {
173
+ console.error(chalk_1.default.red("Aborting due to --strict mode."));
174
+ process.exit(1);
175
+ }
176
+ }
177
+ // Write output
178
+ if (output) {
179
+ const outPath = path.resolve(process.cwd(), output);
180
+ fs.writeFileSync(outPath, rendered, "utf8");
181
+ console.log(chalk_1.default.blue(`β†’ Written to: ${outPath}`));
182
+ }
183
+ else {
184
+ // Print to stdout (so user can pipe it)
185
+ process.stdout.write(rendered);
186
+ }
187
+ // Audit log
188
+ try {
189
+ const { logAudit } = require("../lib/audit");
190
+ logAudit("TEMPLATE_RENDERED", project, env, {
191
+ template: templateFile,
192
+ output: output || "stdout",
193
+ replacedCount,
194
+ missingKeys,
195
+ });
196
+ }
197
+ catch (e) { }
198
+ });
199
+ // ── check ─────────────────────────────────────────────────────────────────────
200
+ exports.templateCommand
201
+ .command("check <templateFile>")
202
+ .description("Validate that all template placeholders have matching secrets (dry-run)")
203
+ .option("-p, --project <id>", "Project ID")
204
+ .option("-e, --env <environment>", "Environment", "development")
205
+ .option("-b, --branch <branch>", "Branch name", "main")
206
+ .action(async (templateFile, options) => {
207
+ let { project, env, branch } = options;
208
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
209
+ env = envMap[env] || env;
210
+ if (!project)
211
+ project = (0, config_1.getConfigValue)("project");
212
+ if (!project) {
213
+ console.error(chalk_1.default.red("Error: Project ID required."));
214
+ process.exit(1);
215
+ }
216
+ const templatePath = path.resolve(process.cwd(), templateFile);
217
+ if (!fs.existsSync(templatePath)) {
218
+ console.error(chalk_1.default.red(`Template file not found: ${templatePath}`));
219
+ process.exit(1);
220
+ }
221
+ const template = fs.readFileSync(templatePath, "utf8");
222
+ const spinner = (0, ora_1.default)(`Fetching secrets for check...`).start();
223
+ let secrets = {};
224
+ try {
225
+ secrets = await api_1.api.getSecrets(project, env, branch);
226
+ spinner.stop();
227
+ }
228
+ catch (e) {
229
+ spinner.fail("Failed to fetch secrets.");
230
+ process.exit(1);
231
+ }
232
+ const { replacedCount, missingKeys, usedDefaults } = renderTemplate(template, secrets, process.env);
233
+ const allCount = [...template.matchAll(PLACEHOLDER_RE)].length;
234
+ console.log(chalk_1.default.bold(`\nTemplate Check: ${templateFile}\n`));
235
+ console.log(` Total placeholders : ${allCount}`);
236
+ console.log(` Resolved : ${chalk_1.default.green(replacedCount)}`);
237
+ console.log(` Using defaults : ${chalk_1.default.yellow(usedDefaults.length)}`);
238
+ console.log(` Unresolved : ${chalk_1.default.red(missingKeys.length)}`);
239
+ if (usedDefaults.length > 0) {
240
+ console.log(chalk_1.default.yellow(`\n Defaults used:`));
241
+ usedDefaults.forEach(d => console.log(` - ${d}`));
242
+ }
243
+ if (missingKeys.length > 0) {
244
+ console.log(chalk_1.default.red(`\n Missing secrets (no default):`));
245
+ missingKeys.forEach(k => console.log(chalk_1.default.red(` βœ— ${k}`)));
246
+ process.exit(1);
247
+ }
248
+ else {
249
+ console.log(chalk_1.default.green("\n βœ… All placeholders are resolvable!"));
250
+ }
251
+ });
252
+ // ── list ──────────────────────────────────────────────────────────────────────
253
+ exports.templateCommand
254
+ .command("list <templateFile>")
255
+ .description("List all placeholders found in a template file (no API call needed)")
256
+ .action((templateFile) => {
257
+ const templatePath = path.resolve(process.cwd(), templateFile);
258
+ if (!fs.existsSync(templatePath)) {
259
+ console.error(chalk_1.default.red(`Template file not found: ${templatePath}`));
260
+ process.exit(1);
261
+ }
262
+ const template = fs.readFileSync(templatePath, "utf8");
263
+ const matches = [...template.matchAll(PLACEHOLDER_RE)];
264
+ if (matches.length === 0) {
265
+ console.log(chalk_1.default.yellow("No placeholders found in this template."));
266
+ return;
267
+ }
268
+ console.log(chalk_1.default.bold(`\nPlaceholders in ${path.basename(templateFile)}:\n`));
269
+ matches.forEach(m => {
270
+ const [, source, key, def] = m;
271
+ const defaultHint = def ? chalk_1.default.gray(` (default: "${def.trim()}")`) : "";
272
+ const icon = source === "secrets" ? "πŸ”’" : "πŸ“¦";
273
+ console.log(` ${icon} ${chalk_1.default.cyan(`{{ ${source}.${key} }}`)}${defaultHint}`);
274
+ });
275
+ console.log(`\n Total: ${chalk_1.default.bold(matches.length)} placeholder(s)\n`);
276
+ });
@@ -0,0 +1,218 @@
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
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.uiCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const readline = __importStar(require("readline"));
43
+ const api_1 = require("../lib/api");
44
+ 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));
98
+ }
99
+ // ─── Main TUI Loop ────────────────────────────────────────────────────────────
100
+ async function runUI() {
101
+ let screen = "project";
102
+ let projects = [];
103
+ let projectIdx = 0;
104
+ let envIdx = 0;
105
+ let secrets = [];
106
+ let secretIdx = 0;
107
+ let status = "Loading projects...";
108
+ let loading = false;
109
+ async function loadProjects() {
110
+ try {
111
+ projects = await api_1.api.getProjects();
112
+ status = projects.length > 0
113
+ ? `Loaded ${projects.length} projects. Use ↑↓ and Enter to navigate.`
114
+ : "No projects found. Try running `xtra login`.";
115
+ }
116
+ catch (e) {
117
+ status = chalk_1.default.red(`Error: ${e.message}`);
118
+ }
119
+ draw();
120
+ }
121
+ async function loadSecrets() {
122
+ if (!projects[projectIdx])
123
+ return;
124
+ loading = true;
125
+ status = `Fetching secrets for ${projects[projectIdx].name}/${ENVS[envIdx]}...`;
126
+ draw();
127
+ 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.`;
131
+ }
132
+ catch (e) {
133
+ secrets = [];
134
+ status = chalk_1.default.red(`Error: ${e.message}`);
135
+ }
136
+ loading = false;
137
+ draw();
138
+ }
139
+ function draw() {
140
+ clear();
141
+ renderHeader();
142
+ const projectNames = projects.map(p => p.name);
143
+ renderList("PROJECTS", projectNames.length ? projectNames : ["(loading...)"], projectIdx, screen === "project");
144
+ console.log();
145
+ renderList("ENVIRONMENT", ENVS, envIdx, screen === "env");
146
+ console.log();
147
+ if (loading) {
148
+ console.log(chalk_1.default.cyan(" β ‹ Loading secrets..."));
149
+ }
150
+ else {
151
+ renderSecrets(secrets, secretIdx, screen === "secrets");
152
+ }
153
+ renderStatus(status);
154
+ }
155
+ // Set up raw mode for keyboard input
156
+ readline.emitKeypressEvents(process.stdin);
157
+ if (process.stdin.isTTY)
158
+ process.stdin.setRawMode(true);
159
+ process.stdin.on("keypress", async (str, key) => {
160
+ if (!key)
161
+ return;
162
+ // Quit
163
+ if (str === "q" || str === "Q" || (key.ctrl && key.name === "c")) {
164
+ if (process.stdin.isTTY)
165
+ process.stdin.setRawMode(false);
166
+ clear();
167
+ console.log(chalk_1.default.cyan("Goodbye! πŸ‘‹"));
168
+ process.exit(0);
169
+ }
170
+ // Tab to cycle panels
171
+ if (key.name === "tab") {
172
+ screen = screen === "project" ? "env" : screen === "env" ? "secrets" : "project";
173
+ draw();
174
+ return;
175
+ }
176
+ // Navigation based on active panel
177
+ if (screen === "project") {
178
+ if (key.name === "up")
179
+ projectIdx = Math.max(0, projectIdx - 1);
180
+ if (key.name === "down")
181
+ projectIdx = Math.min(projects.length - 1, projectIdx + 1);
182
+ if (key.name === "return") {
183
+ screen = "env";
184
+ await loadSecrets();
185
+ return;
186
+ }
187
+ }
188
+ if (screen === "env") {
189
+ if (key.name === "up")
190
+ envIdx = Math.max(0, envIdx - 1);
191
+ if (key.name === "down")
192
+ envIdx = Math.min(ENVS.length - 1, envIdx + 1);
193
+ if (key.name === "return") {
194
+ screen = "secrets";
195
+ await loadSecrets();
196
+ return;
197
+ }
198
+ }
199
+ if (screen === "secrets") {
200
+ if (key.name === "up")
201
+ secretIdx = Math.max(0, secretIdx - 1);
202
+ if (key.name === "down")
203
+ secretIdx = Math.min(secrets.length - 1, secretIdx + 1);
204
+ }
205
+ draw();
206
+ });
207
+ await loadProjects();
208
+ if (projects.length > 0)
209
+ await loadSecrets();
210
+ else
211
+ draw();
212
+ }
213
+ // ─── Commander Command ────────────────────────────────────────────────────────
214
+ 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
+ });