xtra-cli 1.0.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 +138 -0
  4. package/dist/commands/access.js +184 -0
  5. package/dist/commands/admin.js +118 -0
  6. package/dist/commands/audit.js +67 -0
  7. package/dist/commands/branch.js +212 -0
  8. package/dist/commands/checkout.js +73 -0
  9. package/dist/commands/ci.js +341 -0
  10. package/dist/commands/completion.js +227 -0
  11. package/dist/commands/diff.js +162 -0
  12. package/dist/commands/doctor.js +164 -0
  13. package/dist/commands/env.js +70 -0
  14. package/dist/commands/export.js +83 -0
  15. package/dist/commands/generate.js +179 -0
  16. package/dist/commands/history.js +77 -0
  17. package/dist/commands/import.js +121 -0
  18. package/dist/commands/init.js +205 -0
  19. package/dist/commands/integration.js +188 -0
  20. package/dist/commands/local.js +198 -0
  21. package/dist/commands/login.js +198 -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 +165 -0
  26. package/dist/commands/rollback.js +95 -0
  27. package/dist/commands/rotate.js +93 -0
  28. package/dist/commands/run.js +215 -0
  29. package/dist/commands/scan.js +127 -0
  30. package/dist/commands/secrets.js +305 -0
  31. package/dist/commands/simulate.js +109 -0
  32. package/dist/commands/status.js +93 -0
  33. package/dist/commands/template.js +276 -0
  34. package/dist/commands/ui.js +289 -0
  35. package/dist/commands/watch.js +123 -0
  36. package/dist/lib/api.js +187 -0
  37. package/dist/lib/api.test.js +89 -0
  38. package/dist/lib/audit.js +136 -0
  39. package/dist/lib/config.js +70 -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,215 @@
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.runCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const api_1 = require("../lib/api");
42
+ const child_process_1 = require("child_process");
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const ora_1 = __importDefault(require("ora"));
45
+ const config_1 = require("../lib/config");
46
+ const crypto_1 = require("../lib/crypto");
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const dotenv_1 = __importDefault(require("dotenv"));
50
+ exports.runCommand = new commander_1.Command("run")
51
+ .description("Run a command with injected secrets")
52
+ .option("-p, --project <projectId>", "Project ID")
53
+ .option("-e, --env <environment>", "Environment (development, staging, production)", "development")
54
+ .option("-b, --branch <branchName>", "Branch Name")
55
+ .option("--shell", "Enable shell mode (needed for npm run, shell built-ins on Windows)", false)
56
+ .argument("<command>", "Command to run")
57
+ .argument("[args...]", "Command arguments")
58
+ .addHelpText("after", `
59
+ Examples:
60
+ $ xtra run npm start
61
+ $ xtra run -e production -- npm run build
62
+ $ xtra run -p proj_123 -b feature-branch -- python script.py
63
+ $ xtra run --shell "echo $SECRET_KEY"
64
+ `)
65
+ .action(async (command, args, options) => {
66
+ // console.log("Run Options:", options);
67
+ let { project, env, branch, shell: useShell } = options;
68
+ // Load .xtrarc from CWD first (project-local config), then fall back to global conf store
69
+ const rc = (0, config_1.getRcConfig)();
70
+ // Use active branch from config if not specified
71
+ if (!branch)
72
+ branch = rc.branch;
73
+ // Normalize Env
74
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
75
+ env = envMap[env] || env || rc.env;
76
+ if (!project)
77
+ project = rc.project;
78
+ if (!project) {
79
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or checkout a branch."));
80
+ process.exit(1);
81
+ }
82
+ const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
83
+ let secrets = null;
84
+ // ── Local mode override ──────────────────────────────────────────────────
85
+ // If XTRA_LOCAL_MODE=true or localMode config flag is set,
86
+ // read from .env.local instead of calling the API.
87
+ const isLocal = (0, config_1.getConfigValue)("localMode") === true
88
+ || process.env.XTRA_LOCAL_MODE === "true";
89
+ if (isLocal) {
90
+ const localFile = path.resolve(process.cwd(), ".env.local");
91
+ if (!fs.existsSync(localFile)) {
92
+ spinner.fail("Local mode is ON but .env.local not found. Run 'xtra local sync' first.");
93
+ process.exit(1);
94
+ }
95
+ const parsed = dotenv_1.default.parse(fs.readFileSync(localFile, "utf8"));
96
+ secrets = parsed;
97
+ spinner.succeed(chalk_1.default.yellow(`🔌 Local mode: loaded ${Object.keys(secrets).length} secrets from .env.local`));
98
+ }
99
+ else {
100
+ try {
101
+ // 1. Fetch Secrets
102
+ secrets = await api_1.api.getSecrets(project, env, branch);
103
+ if (!secrets || Object.keys(secrets).length === 0) {
104
+ spinner.warn(chalk_1.default.yellow("No secrets found for this environment."));
105
+ }
106
+ else {
107
+ spinner.succeed(`Loaded ${Object.keys(secrets).length} secrets (Online).`);
108
+ // Update manifest
109
+ try {
110
+ const { hash } = require("../lib/crypto");
111
+ const { updateManifest } = require("../lib/manifest");
112
+ updateManifest(project, env, secrets, hash);
113
+ }
114
+ catch (mErr) {
115
+ // Manifest update failed
116
+ }
117
+ // Cache secrets locally
118
+ try {
119
+ const cacheKey = `cache.${project}.${env}.${branch}`;
120
+ const encrypted = (0, crypto_1.encrypt)(JSON.stringify(secrets));
121
+ (0, config_1.setConfig)(cacheKey, encrypted);
122
+ // console.log(chalk.gray("Secrets cached successfully."));
123
+ }
124
+ catch (cacheErr) {
125
+ console.error(chalk_1.default.yellow("Warning: Failed to cache secrets."));
126
+ }
127
+ }
128
+ }
129
+ catch (error) {
130
+ // Offline fallback
131
+ const cacheKey = `cache.${project}.${env}.${branch}`;
132
+ const cachedData = (0, config_1.getConfigValue)(cacheKey);
133
+ if (cachedData) {
134
+ try {
135
+ const decrypted = (0, crypto_1.decrypt)(cachedData);
136
+ secrets = JSON.parse(decrypted);
137
+ spinner.warn(chalk_1.default.yellow(`Offline Mode: Loaded ${Object.keys(secrets).length} secrets from cache.`));
138
+ }
139
+ catch (decryptErr) {
140
+ spinner.fail("Failed to fetch secrets and cache is corrupt.");
141
+ process.exit(1);
142
+ }
143
+ }
144
+ else {
145
+ spinner.fail("Failed to fetch secrets and no local cache available.");
146
+ if (error.response) {
147
+ if (error.response.status === 404) {
148
+ console.error(chalk_1.default.red(`\nError: Project '${project}' not found or you don't have access.`));
149
+ }
150
+ else if (error.response.status === 401) {
151
+ console.error(chalk_1.default.red(`\nError: Unauthorized. Please run 'xtra login' again.`));
152
+ }
153
+ else {
154
+ console.error(chalk_1.default.red(`\nError: ${error.response.data.error || error.message}`));
155
+ }
156
+ }
157
+ else {
158
+ console.error(chalk_1.default.red(`\nError: ${error.message}`));
159
+ }
160
+ process.exit(1);
161
+ }
162
+ } // end try/catch (cloud mode)
163
+ } // end else (cloud mode)
164
+ // Log Audit (Always)
165
+ try {
166
+ const { logAudit } = require("../lib/audit");
167
+ // Only log if we have secrets
168
+ if (secrets && Object.keys(secrets).length > 0) {
169
+ logAudit("SECRET_ACCESS", project, env, { method: "run", keys: Object.keys(secrets), branch });
170
+ }
171
+ }
172
+ catch (e) {
173
+ console.error("Audit Error:", e);
174
+ }
175
+ // 2. Prepare Environment
176
+ const envVars = {
177
+ ...process.env,
178
+ ...secrets, // Overwrite local env with injected secrets
179
+ };
180
+ // 3. Spawn Child Process — shell: false by default to prevent command injection
181
+ // When --shell is passed (e.g. for npm run start on Windows), allow shell mode
182
+ const SHELL_UNSAFE = /[;&|`$<>\\\n]/g;
183
+ if (!useShell && SHELL_UNSAFE.test(command)) {
184
+ console.error(chalk_1.default.red("Error: Command contains unsafe characters. Use --shell if intentional."));
185
+ process.exit(1);
186
+ }
187
+ // Production confirmation gate
188
+ if (env === "production") {
189
+ const inquirer = require("inquirer");
190
+ const { confirm } = await inquirer.prompt([{
191
+ type: "confirm",
192
+ name: "confirm",
193
+ message: chalk_1.default.red(`âš  You are about to run a command in PRODUCTION with ${Object.keys(secrets || {}).length} injected secrets. Continue?`),
194
+ default: false,
195
+ }]);
196
+ if (!confirm) {
197
+ console.log(chalk_1.default.yellow("Aborted."));
198
+ return;
199
+ }
200
+ }
201
+ console.log(chalk_1.default.gray(`> ${command} ${args.join(" ")}`));
202
+ const isWindows = process.platform === "win32";
203
+ const child = (0, child_process_1.spawn)(command, args, {
204
+ env: envVars,
205
+ stdio: "inherit",
206
+ shell: useShell || isWindows, // Auto-enable shell on Windows for better compatibility (e.g. npm, etc)
207
+ });
208
+ child.on("exit", (code) => {
209
+ process.exit(code ?? 0);
210
+ });
211
+ child.on("error", (err) => {
212
+ console.error(chalk_1.default.red(`Failed to start command: ${err.message}`));
213
+ process.exit(1);
214
+ });
215
+ });
@@ -0,0 +1,127 @@
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.scanCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
12
+ exports.scanCommand = new commander_1.Command("scan")
13
+ .description("Scan project for secrets and configuration leaks")
14
+ .option("--staged", "Scan only staged files (for pre-commit hooks)", false)
15
+ .option("--install-hook", "Install the git pre-commit hook")
16
+ .action(async (options) => {
17
+ const { staged, installHook } = options;
18
+ // Install Hook
19
+ if (installHook) {
20
+ installGitHook();
21
+ return;
22
+ }
23
+ // Scanning Logic
24
+ console.log(chalk_1.default.bold("XtraSync Secret Scan"));
25
+ let issues = 0;
26
+ // 1. Check .env tracking
27
+ if (isEnvFileTracked()) {
28
+ console.error(chalk_1.default.red("CRITICAL: .env file is tracked by git!"));
29
+ issues++;
30
+ }
31
+ else {
32
+ // console.log(chalk.green("✔ .env file is safe (not tracked)."));
33
+ }
34
+ // 2. Scan Files
35
+ const filesToScan = staged ? getStagedFiles() : getAllFiles();
36
+ // Pattern: xs_ (Looking for API keys or similar tokens if we had a specific format)
37
+ // Also naive check for "password", "secret", "key" = "..."
38
+ const patterns = [
39
+ { name: "Xtra API Key", regex: /xs_[a-zA-Z0-9]{10,}/ },
40
+ // Add more patterns here
41
+ ];
42
+ filesToScan.forEach(file => {
43
+ try {
44
+ // Skip binary or large files?
45
+ if (!fs_1.default.existsSync(file))
46
+ return;
47
+ const content = fs_1.default.readFileSync(file, "utf-8");
48
+ patterns.forEach(p => {
49
+ if (p.regex.test(content)) {
50
+ console.error(chalk_1.default.red(`[${file}] Potential Secret Detected: ${p.name}`));
51
+ issues++;
52
+ }
53
+ });
54
+ }
55
+ catch (e) {
56
+ // Ignore (e.g. dir or binary)
57
+ }
58
+ });
59
+ if (issues > 0) {
60
+ console.error(chalk_1.default.red(`\n✖ Scan failed. found ${issues} issues.`));
61
+ process.exit(1);
62
+ }
63
+ else {
64
+ console.log(chalk_1.default.green("\n✔ Scan passed. No secrets found."));
65
+ }
66
+ });
67
+ function installGitHook() {
68
+ const hookDir = path_1.default.join(process.cwd(), ".git", "hooks");
69
+ const hookPath = path_1.default.join(hookDir, "pre-commit");
70
+ if (!fs_1.default.existsSync(hookDir)) {
71
+ console.error(chalk_1.default.red("Error: .git directory not found. Is this a git repository?"));
72
+ return;
73
+ }
74
+ const hookContent = `#!/bin/sh
75
+ # XtraSync Secret Scan
76
+ echo "Running XtraSync Secret Scan..."
77
+
78
+ # Check if 'xtra' is globally installed or use local script
79
+ if command -v xtra >/dev/null 2>&1; then
80
+ xtra scan --staged
81
+ elif [ -f "./node_modules/.bin/xtra" ]; then
82
+ ./node_modules/.bin/xtra scan --staged
83
+ else
84
+ # Using the binary directly if running in dev environment (unlikely in prod use case)
85
+ # For dev testing:
86
+ if [ -f "./dist/bin/xtra.js" ]; then
87
+ node ./dist/bin/xtra.js scan --staged
88
+ else
89
+ echo "Warning: XtraSync CLI not found. Skipping scan."
90
+ fi
91
+ fi
92
+
93
+ # Exit code of scan is passed through
94
+ `;
95
+ fs_1.default.writeFileSync(hookPath, hookContent, { mode: 0o755 });
96
+ console.log(chalk_1.default.green("✔ Pre-commit hook installed successfully."));
97
+ }
98
+ function isEnvFileTracked() {
99
+ try {
100
+ // git ls-files --error-unmatch .env
101
+ (0, child_process_1.execSync)("git ls-files --error-unmatch .env", { stdio: "ignore" });
102
+ return true; // Command succeeded, file is tracked
103
+ }
104
+ catch (e) {
105
+ return false; // Command failed, file not tracked
106
+ }
107
+ }
108
+ function getStagedFiles() {
109
+ try {
110
+ const output = (0, child_process_1.execSync)("git diff --cached --name-only", { encoding: "utf-8" });
111
+ return output.split("\n").filter(f => f.trim().length > 0);
112
+ }
113
+ catch (e) {
114
+ return [];
115
+ }
116
+ }
117
+ function getAllFiles() {
118
+ // Naive implementation for now, or recurse.
119
+ // Usually 'git ls-files' is better if generic scan.
120
+ try {
121
+ const output = (0, child_process_1.execSync)("git ls-files", { encoding: "utf-8" });
122
+ return output.split("\n").filter(f => f.trim().length > 0);
123
+ }
124
+ catch (e) {
125
+ return [];
126
+ }
127
+ }
@@ -0,0 +1,305 @@
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.secretsCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const api_1 = require("../lib/api");
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const ora_1 = __importDefault(require("ora"));
44
+ const table_1 = require("table");
45
+ const config_1 = require("../lib/config");
46
+ exports.secretsCommand = new commander_1.Command("secrets")
47
+ .description("Manage secrets (List, Set, Delete)")
48
+ .option("-p, --project <projectId>", "Project ID")
49
+ .option("-e, --env <environment>", "Environment (development, staging, production)", "development")
50
+ .option("-b, --branch <branchName>", "Branch Name");
51
+ // LIST
52
+ exports.secretsCommand
53
+ .command("list")
54
+ .description("List all secrets for a project/environment")
55
+ .option("--show", "Reveal secret values", false)
56
+ .addHelpText("after", `
57
+ Examples:
58
+ $ xtra secrets list
59
+ $ xtra secrets list -e production --show
60
+ `)
61
+ .action(async (options) => {
62
+ const parentOpts = exports.secretsCommand.opts();
63
+ let { project, env, branch } = parentOpts;
64
+ // Load .xtrarc from CWD (local project config has priority over global conf store)
65
+ const rc = (0, config_1.getRcConfig)();
66
+ if (!project)
67
+ project = rc.project;
68
+ if (!branch)
69
+ branch = rc.branch;
70
+ if (!env || env === "development")
71
+ env = rc.env; // only override if still at default
72
+ // Normalize Env
73
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
74
+ env = envMap[env] || env;
75
+ const { show } = options;
76
+ if (!project) {
77
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run `xtra init` first."));
78
+ process.exit(1);
79
+ }
80
+ const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
81
+ try {
82
+ const secrets = await api_1.api.getSecrets(project, env, branch);
83
+ spinner.stop();
84
+ if (!secrets || Object.keys(secrets).length === 0) {
85
+ console.log(chalk_1.default.yellow("No secrets found."));
86
+ return;
87
+ }
88
+ if (show) {
89
+ console.log(chalk_1.default.red("âš  Warning: Secret values will be visible in your terminal and shell history!"));
90
+ }
91
+ const data = [
92
+ [chalk_1.default.bold("Key"), chalk_1.default.bold("Value"), chalk_1.default.bold("Env")]
93
+ ];
94
+ Object.entries(secrets).forEach(([key, value]) => {
95
+ data.push([
96
+ key,
97
+ show ? value : "********",
98
+ env
99
+ ]);
100
+ });
101
+ console.log((0, table_1.table)(data));
102
+ // Audit Log
103
+ try {
104
+ const { logAudit } = await Promise.resolve().then(() => __importStar(require("../lib/audit")));
105
+ logAudit("SECRET_LIST", project, env, { branch, count: Object.keys(secrets).length });
106
+ }
107
+ catch (e) { }
108
+ }
109
+ catch (error) {
110
+ spinner.fail("Failed to fetch secrets");
111
+ console.error(chalk_1.default.red(error.message));
112
+ }
113
+ });
114
+ // SET
115
+ exports.secretsCommand
116
+ .command("set")
117
+ .description("Set one or more secrets (KEY=VALUE)")
118
+ .argument("<secrets...>", "Secrets to set (format: KEY=VALUE)")
119
+ .option("-f, --force", "Force update (overwrite remote changes without warning)", false)
120
+ .addHelpText("after", `
121
+ Examples:
122
+ $ xtra secrets set API_KEY=xyz
123
+ $ xtra secrets set DB_USER=admin DB_PASS=secret -e staging
124
+ `)
125
+ .action(async (args, options) => {
126
+ const parentOpts = exports.secretsCommand.opts();
127
+ let { project, env, branch } = parentOpts;
128
+ // Use active branch from config if not specified
129
+ if (!branch) {
130
+ branch = (0, config_1.getConfigValue)("branch") || "main";
131
+ }
132
+ // Normalize Env
133
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
134
+ env = envMap[env] || env;
135
+ if (!project) {
136
+ project = (0, config_1.getConfigValue)("project");
137
+ }
138
+ if (!project) {
139
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
140
+ process.exit(1);
141
+ }
142
+ // Parse key=value pairs
143
+ const payload = {};
144
+ args.forEach((arg) => {
145
+ const idx = arg.indexOf("=");
146
+ if (idx === -1) {
147
+ console.warn(chalk_1.default.yellow(`Skipping invalid format: ${arg} (expected KEY=VALUE)`));
148
+ return;
149
+ }
150
+ const key = arg.substring(0, idx);
151
+ const value = arg.substring(idx + 1);
152
+ payload[key] = value;
153
+ });
154
+ if (Object.keys(payload).length === 0) {
155
+ console.error(chalk_1.default.red("No valid secrets provided."));
156
+ return;
157
+ }
158
+ // 🔒 Production gate: require explicit confirmation before writing to production
159
+ if (env === "production" && !options.force) {
160
+ const inquirer = require("inquirer");
161
+ const { confirm } = await inquirer.prompt([{
162
+ type: "confirm",
163
+ name: "confirm",
164
+ message: chalk_1.default.red(`âš  You are about to SET ${Object.keys(payload).length} secret(s) in PRODUCTION. Are you sure?`),
165
+ default: false,
166
+ }]);
167
+ if (!confirm) {
168
+ console.log(chalk_1.default.yellow("Aborted."));
169
+ return;
170
+ }
171
+ }
172
+ const spinner = (0, ora_1.default)(`Setting ${Object.keys(payload).length} secrets for ${env} (branch: ${branch})...`);
173
+ if (options.force) {
174
+ spinner.start();
175
+ }
176
+ try {
177
+ let expectedVersions = undefined;
178
+ if (!options.force) {
179
+ // Fetch current versions for optimistic locking
180
+ spinner.text = "Fetching current versions...";
181
+ spinner.start();
182
+ try {
183
+ const remoteSecrets = await api_1.api.getSecretVersions(project, env, branch);
184
+ expectedVersions = {};
185
+ spinner.stop();
186
+ // Populate expected versions for keys we are updating
187
+ Object.keys(payload).forEach(key => {
188
+ if (remoteSecrets[key]) {
189
+ expectedVersions[key] = remoteSecrets[key].version;
190
+ }
191
+ });
192
+ }
193
+ catch (verErr) {
194
+ spinner.warn(chalk_1.default.yellow("Could not fetch remote versions. Conflict detection disabled."));
195
+ }
196
+ }
197
+ spinner.start(`Updating secrets...`);
198
+ await api_1.api.setSecrets(project, env, payload, expectedVersions, branch);
199
+ spinner.succeed("Secrets updated successfully.");
200
+ // Log Audit
201
+ try {
202
+ const { logAudit } = require("../lib/audit");
203
+ logAudit("SECRET_UPDATE", project, env, { keys: Object.keys(payload), branch });
204
+ }
205
+ catch (e) { }
206
+ }
207
+ catch (error) {
208
+ spinner.stop();
209
+ if (error.response && error.response.status === 409) {
210
+ // Conflict Detected
211
+ const conflicts = error.response.data.conflicts || [];
212
+ console.log(chalk_1.default.red("\nâš  Conflict Detected! The following secrets have changed remotely:\n"));
213
+ const conflictTable = [[chalk_1.default.bold("Key"), chalk_1.default.bold("Remote Version"), chalk_1.default.bold("Remote Value")]];
214
+ conflicts.forEach((c) => {
215
+ conflictTable.push([c.key, c.actual, c.remoteValue]);
216
+ });
217
+ console.log((0, table_1.table)(conflictTable));
218
+ const inquirer = require("inquirer");
219
+ const { confirm } = await inquirer.prompt([{
220
+ type: "confirm",
221
+ name: "confirm",
222
+ message: "Do you want to overwrite these changes exactly as you specified?",
223
+ default: false
224
+ }]);
225
+ if (confirm) {
226
+ const retrySpinner = (0, ora_1.default)("Overwriting secrets...").start();
227
+ try {
228
+ // Start retry with Force (no expectedVersions)
229
+ await api_1.api.setSecrets(project, env, payload, undefined, branch);
230
+ retrySpinner.succeed("Secrets overwritten successfully.");
231
+ // Log Audit Force
232
+ try {
233
+ const { logAudit } = require("../lib/audit");
234
+ logAudit("SECRET_UPDATE_FORCE", project, env, { keys: Object.keys(payload), branch });
235
+ }
236
+ catch (e) { }
237
+ }
238
+ catch (retryErr) {
239
+ retrySpinner.fail("Failed to overwrite secrets.");
240
+ console.error(chalk_1.default.red(retryErr.message));
241
+ }
242
+ }
243
+ else {
244
+ console.log(chalk_1.default.yellow("Update cancelled."));
245
+ }
246
+ return; // Handled
247
+ }
248
+ if (spinner.isSpinning)
249
+ spinner.fail("Failed to update secrets");
250
+ if (error.response && error.response.data && error.response.data.error) {
251
+ console.error(chalk_1.default.red(`Server Error: ${error.response.data.error}`));
252
+ }
253
+ else {
254
+ console.error(chalk_1.default.red(error.message));
255
+ }
256
+ }
257
+ });
258
+ // LINK
259
+ exports.secretsCommand
260
+ .command("link")
261
+ .description("Link a secret to another secret (Reference)")
262
+ .argument("<key>", "Key of the secret to create (e.g. DB_URL)")
263
+ .requiredOption("--source <source>", "Source path (format: project/env/key)")
264
+ .addHelpText("after", `
265
+ Examples:
266
+ $ xtra secrets link SHARED_DB_URL --source shared-proj/production/DB_URL
267
+ `)
268
+ .action(async (key, options) => {
269
+ const parentOpts = exports.secretsCommand.opts();
270
+ let { project, env } = parentOpts;
271
+ // Normalize Env
272
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
273
+ env = envMap[env] || env;
274
+ const { source } = options;
275
+ if (!project) {
276
+ project = (0, config_1.getConfigValue)("project");
277
+ }
278
+ if (!project) {
279
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
280
+ process.exit(1);
281
+ }
282
+ // Parse Source: "proj-123/prod/DATABASE_URL"
283
+ const parts = source.split("/");
284
+ if (parts.length !== 3) {
285
+ console.error(chalk_1.default.red("Error: Source must be in format 'projectId/env/key'"));
286
+ return;
287
+ }
288
+ const [sourceProjectId, sourceEnv, sourceKey] = parts;
289
+ const spinner = (0, ora_1.default)(`Linking ${key} to ${source}...`).start();
290
+ try {
291
+ await api_1.api.linkSecret(project, env, key, sourceProjectId, sourceEnv, sourceKey);
292
+ spinner.succeed(chalk_1.default.green(`Secret '${key}' successfully linked to '${sourceKey}'`));
293
+ // Audit log
294
+ try {
295
+ const { logAudit } = require("../lib/audit");
296
+ logAudit("SECRET_LINKED", project, env, { key, source });
297
+ }
298
+ catch (e) { }
299
+ }
300
+ catch (error) {
301
+ spinner.fail("Failed to link secret");
302
+ const safeErr = error?.response?.data?.error || error.message || "Unknown error";
303
+ console.error(chalk_1.default.red(`Server Error: ${safeErr}`));
304
+ }
305
+ });