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.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/bin/xtra.js +124 -0
- package/dist/commands/access.js +107 -0
- package/dist/commands/admin.js +118 -0
- package/dist/commands/audit.js +67 -0
- package/dist/commands/branch.js +216 -0
- package/dist/commands/checkout.js +74 -0
- package/dist/commands/ci.js +330 -0
- package/dist/commands/completion.js +227 -0
- package/dist/commands/diff.js +163 -0
- package/dist/commands/doctor.js +176 -0
- package/dist/commands/env.js +70 -0
- package/dist/commands/export.js +84 -0
- package/dist/commands/generate.js +180 -0
- package/dist/commands/history.js +77 -0
- package/dist/commands/import.js +122 -0
- package/dist/commands/init.js +162 -0
- package/dist/commands/integration.js +188 -0
- package/dist/commands/local.js +198 -0
- package/dist/commands/login.js +176 -0
- package/dist/commands/login.test.js +51 -0
- package/dist/commands/logs.js +121 -0
- package/dist/commands/profile.js +184 -0
- package/dist/commands/project.js +98 -0
- package/dist/commands/rollback.js +96 -0
- package/dist/commands/rotate.js +94 -0
- package/dist/commands/run.js +215 -0
- package/dist/commands/scan.js +127 -0
- package/dist/commands/secrets.js +265 -0
- package/dist/commands/simulate.js +92 -0
- package/dist/commands/status.js +94 -0
- package/dist/commands/template.js +276 -0
- package/dist/commands/ui.js +218 -0
- package/dist/commands/watch.js +121 -0
- package/dist/lib/api.js +172 -0
- package/dist/lib/api.test.js +89 -0
- package/dist/lib/audit.js +136 -0
- package/dist/lib/config.js +42 -0
- package/dist/lib/config.test.js +47 -0
- package/dist/lib/crypto.js +50 -0
- package/dist/lib/manifest.js +52 -0
- package/dist/lib/profiles.js +103 -0
- 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
|
+
});
|