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,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
+ // Use active branch from config if not specified
69
+ if (!branch) {
70
+ branch = (0, config_1.getConfigValue)("branch") || "main";
71
+ }
72
+ // Normalize Env
73
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
74
+ env = envMap[env] || env;
75
+ // We might want to support default project in config later
76
+ if (!project) {
77
+ project = (0, config_1.getConfigValue)("project");
78
+ }
79
+ if (!project) {
80
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or checkout a branch."));
81
+ process.exit(1);
82
+ }
83
+ const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
84
+ let secrets = null;
85
+ // ── Local mode override ──────────────────────────────────────────────────
86
+ // If XTRA_LOCAL_MODE=true or localMode config flag is set,
87
+ // read from .env.local instead of calling the API.
88
+ const isLocal = (0, config_1.getConfigValue)("localMode") === true
89
+ || process.env.XTRA_LOCAL_MODE === "true";
90
+ if (isLocal) {
91
+ const localFile = path.resolve(process.cwd(), ".env.local");
92
+ if (!fs.existsSync(localFile)) {
93
+ spinner.fail("Local mode is ON but .env.local not found. Run 'xtra local sync' first.");
94
+ process.exit(1);
95
+ }
96
+ const parsed = dotenv_1.default.parse(fs.readFileSync(localFile, "utf8"));
97
+ secrets = parsed;
98
+ spinner.succeed(chalk_1.default.yellow(`🔌 Local mode: loaded ${Object.keys(secrets).length} secrets from .env.local`));
99
+ }
100
+ else {
101
+ try {
102
+ // 1. Fetch Secrets
103
+ secrets = await api_1.api.getSecrets(project, env, branch);
104
+ if (!secrets || Object.keys(secrets).length === 0) {
105
+ spinner.warn(chalk_1.default.yellow("No secrets found for this environment."));
106
+ }
107
+ else {
108
+ spinner.succeed(`Loaded ${Object.keys(secrets).length} secrets (Online).`);
109
+ // Update manifest
110
+ try {
111
+ const { hash } = require("../lib/crypto");
112
+ const { updateManifest } = require("../lib/manifest");
113
+ updateManifest(project, env, secrets, hash);
114
+ }
115
+ catch (mErr) {
116
+ // Manifest update failed
117
+ }
118
+ // Cache secrets locally
119
+ try {
120
+ const cacheKey = `cache.${project}.${env}.${branch}`;
121
+ const encrypted = (0, crypto_1.encrypt)(JSON.stringify(secrets));
122
+ (0, config_1.setConfig)(cacheKey, encrypted);
123
+ // console.log(chalk.gray("Secrets cached successfully."));
124
+ }
125
+ catch (cacheErr) {
126
+ console.error(chalk_1.default.yellow("Warning: Failed to cache secrets."));
127
+ }
128
+ }
129
+ }
130
+ catch (error) {
131
+ // Offline fallback
132
+ const cacheKey = `cache.${project}.${env}.${branch}`;
133
+ const cachedData = (0, config_1.getConfigValue)(cacheKey);
134
+ if (cachedData) {
135
+ try {
136
+ const decrypted = (0, crypto_1.decrypt)(cachedData);
137
+ secrets = JSON.parse(decrypted);
138
+ spinner.warn(chalk_1.default.yellow(`Offline Mode: Loaded ${Object.keys(secrets).length} secrets from cache.`));
139
+ }
140
+ catch (decryptErr) {
141
+ spinner.fail("Failed to fetch secrets and cache is corrupt.");
142
+ process.exit(1);
143
+ }
144
+ }
145
+ else {
146
+ spinner.fail("Failed to fetch secrets and no local cache available.");
147
+ if (error.response) {
148
+ if (error.response.status === 404) {
149
+ console.error(chalk_1.default.red(`\nError: Project '${project}' not found or you don't have access.`));
150
+ }
151
+ else if (error.response.status === 401) {
152
+ console.error(chalk_1.default.red(`\nError: Unauthorized. Please run 'xtra login' again.`));
153
+ }
154
+ else {
155
+ console.error(chalk_1.default.red(`\nError: ${error.response.data.error || error.message}`));
156
+ }
157
+ }
158
+ else {
159
+ console.error(chalk_1.default.red(`\nError: ${error.message}`));
160
+ }
161
+ process.exit(1);
162
+ }
163
+ } // end try/catch (cloud mode)
164
+ } // end else (cloud mode)
165
+ // Log Audit (Always)
166
+ try {
167
+ const { logAudit } = require("../lib/audit");
168
+ // Only log if we have secrets
169
+ if (secrets && Object.keys(secrets).length > 0) {
170
+ logAudit("SECRET_ACCESS", project, env, { method: "run", keys: Object.keys(secrets), branch });
171
+ }
172
+ }
173
+ catch (e) {
174
+ console.error("Audit Error:", e);
175
+ }
176
+ // 2. Prepare Environment
177
+ const envVars = {
178
+ ...process.env,
179
+ ...secrets, // Overwrite local env with injected secrets
180
+ };
181
+ // 3. Spawn Child Process — shell: false by default to prevent command injection
182
+ // When --shell is passed (e.g. for npm run start on Windows), allow shell mode
183
+ const SHELL_UNSAFE = /[;&|`$<>\\\n]/g;
184
+ if (!useShell && SHELL_UNSAFE.test(command)) {
185
+ console.error(chalk_1.default.red("Error: Command contains unsafe characters. Use --shell if intentional."));
186
+ process.exit(1);
187
+ }
188
+ // Production confirmation gate
189
+ if (env === "production") {
190
+ const inquirer = require("inquirer");
191
+ const { confirm } = await inquirer.prompt([{
192
+ type: "confirm",
193
+ name: "confirm",
194
+ message: chalk_1.default.red(`âš  You are about to run a command in PRODUCTION with ${Object.keys(secrets || {}).length} injected secrets. Continue?`),
195
+ default: false,
196
+ }]);
197
+ if (!confirm) {
198
+ console.log(chalk_1.default.yellow("Aborted."));
199
+ return;
200
+ }
201
+ }
202
+ console.log(chalk_1.default.gray(`> ${command} ${args.join(" ")}`));
203
+ const child = (0, child_process_1.spawn)(command, args, {
204
+ env: envVars,
205
+ stdio: "inherit",
206
+ shell: useShell, // false by default; --shell enables for npm scripts / Windows
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,265 @@
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.secretsCommand = 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 config_1 = require("../lib/config");
13
+ exports.secretsCommand = new commander_1.Command("secrets")
14
+ .description("Manage secrets (List, Set, Delete)")
15
+ .option("-p, --project <projectId>", "Project ID")
16
+ .option("-e, --env <environment>", "Environment (development, staging, production)", "development")
17
+ .option("-b, --branch <branchName>", "Branch Name");
18
+ // LIST
19
+ exports.secretsCommand
20
+ .command("list")
21
+ .description("List all secrets for a project/environment")
22
+ .option("--show", "Reveal secret values", false)
23
+ .addHelpText("after", `
24
+ Examples:
25
+ $ xtra secrets list
26
+ $ xtra secrets list -e production --show
27
+ `)
28
+ .action(async (options) => {
29
+ const parentOpts = exports.secretsCommand.opts();
30
+ let { project, env, branch } = parentOpts;
31
+ // Use active branch from config if not specified
32
+ if (!branch) {
33
+ branch = (0, config_1.getConfigValue)("branch") || "main";
34
+ }
35
+ // Normalize Env
36
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
37
+ env = envMap[env] || env;
38
+ const { show } = options;
39
+ if (!project) {
40
+ project = (0, config_1.getConfigValue)("project");
41
+ }
42
+ if (!project) {
43
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or checkout a branch."));
44
+ process.exit(1);
45
+ }
46
+ const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
47
+ try {
48
+ const secrets = await api_1.api.getSecrets(project, env, branch);
49
+ spinner.stop();
50
+ if (!secrets || Object.keys(secrets).length === 0) {
51
+ console.log(chalk_1.default.yellow("No secrets found."));
52
+ return;
53
+ }
54
+ if (show) {
55
+ console.log(chalk_1.default.red("âš  Warning: Secret values will be visible in your terminal and shell history!"));
56
+ }
57
+ const data = [
58
+ [chalk_1.default.bold("Key"), chalk_1.default.bold("Value"), chalk_1.default.bold("Env")]
59
+ ];
60
+ Object.entries(secrets).forEach(([key, value]) => {
61
+ data.push([
62
+ key,
63
+ show ? value : "********",
64
+ env
65
+ ]);
66
+ });
67
+ console.log((0, table_1.table)(data));
68
+ }
69
+ catch (error) {
70
+ spinner.fail("Failed to fetch secrets");
71
+ console.error(chalk_1.default.red(error.message));
72
+ }
73
+ });
74
+ // SET
75
+ exports.secretsCommand
76
+ .command("set")
77
+ .description("Set one or more secrets (KEY=VALUE)")
78
+ .argument("<secrets...>", "Secrets to set (format: KEY=VALUE)")
79
+ .option("-f, --force", "Force update (overwrite remote changes without warning)", false)
80
+ .addHelpText("after", `
81
+ Examples:
82
+ $ xtra secrets set API_KEY=xyz
83
+ $ xtra secrets set DB_USER=admin DB_PASS=secret -e staging
84
+ `)
85
+ .action(async (args, options) => {
86
+ const parentOpts = exports.secretsCommand.opts();
87
+ let { project, env, branch } = parentOpts;
88
+ // Use active branch from config if not specified
89
+ if (!branch) {
90
+ branch = (0, config_1.getConfigValue)("branch") || "main";
91
+ }
92
+ // Normalize Env
93
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
94
+ env = envMap[env] || env;
95
+ if (!project) {
96
+ project = (0, config_1.getConfigValue)("project");
97
+ }
98
+ if (!project) {
99
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
100
+ process.exit(1);
101
+ }
102
+ // Parse key=value pairs
103
+ const payload = {};
104
+ args.forEach((arg) => {
105
+ const idx = arg.indexOf("=");
106
+ if (idx === -1) {
107
+ console.warn(chalk_1.default.yellow(`Skipping invalid format: ${arg} (expected KEY=VALUE)`));
108
+ return;
109
+ }
110
+ const key = arg.substring(0, idx);
111
+ const value = arg.substring(idx + 1);
112
+ payload[key] = value;
113
+ });
114
+ if (Object.keys(payload).length === 0) {
115
+ console.error(chalk_1.default.red("No valid secrets provided."));
116
+ return;
117
+ }
118
+ // 🔒 Production gate: require explicit confirmation before writing to production
119
+ if (env === "production" && !options.force) {
120
+ const inquirer = require("inquirer");
121
+ const { confirm } = await inquirer.prompt([{
122
+ type: "confirm",
123
+ name: "confirm",
124
+ message: chalk_1.default.red(`âš  You are about to SET ${Object.keys(payload).length} secret(s) in PRODUCTION. Are you sure?`),
125
+ default: false,
126
+ }]);
127
+ if (!confirm) {
128
+ console.log(chalk_1.default.yellow("Aborted."));
129
+ return;
130
+ }
131
+ }
132
+ const spinner = (0, ora_1.default)(`Setting ${Object.keys(payload).length} secrets for ${env} (branch: ${branch})...`);
133
+ if (options.force) {
134
+ spinner.start();
135
+ }
136
+ try {
137
+ let expectedVersions = undefined;
138
+ if (!options.force) {
139
+ // Fetch current versions for optimistic locking
140
+ spinner.text = "Fetching current versions...";
141
+ spinner.start();
142
+ try {
143
+ const remoteSecrets = await api_1.api.getSecretVersions(project, env, branch);
144
+ expectedVersions = {};
145
+ spinner.stop();
146
+ // Populate expected versions for keys we are updating
147
+ Object.keys(payload).forEach(key => {
148
+ if (remoteSecrets[key]) {
149
+ expectedVersions[key] = remoteSecrets[key].version;
150
+ }
151
+ });
152
+ }
153
+ catch (verErr) {
154
+ spinner.warn(chalk_1.default.yellow("Could not fetch remote versions. Conflict detection disabled."));
155
+ }
156
+ }
157
+ spinner.start(`Updating secrets...`);
158
+ await api_1.api.setSecrets(project, env, payload, expectedVersions, branch);
159
+ spinner.succeed("Secrets updated successfully.");
160
+ // Log Audit
161
+ try {
162
+ const { logAudit } = require("../lib/audit");
163
+ logAudit("SECRET_UPDATE", project, env, { keys: Object.keys(payload), branch });
164
+ }
165
+ catch (e) { }
166
+ }
167
+ catch (error) {
168
+ spinner.stop();
169
+ if (error.response && error.response.status === 409) {
170
+ // Conflict Detected
171
+ const conflicts = error.response.data.conflicts || [];
172
+ console.log(chalk_1.default.red("\nâš  Conflict Detected! The following secrets have changed remotely:\n"));
173
+ const conflictTable = [[chalk_1.default.bold("Key"), chalk_1.default.bold("Remote Version"), chalk_1.default.bold("Remote Value")]];
174
+ conflicts.forEach((c) => {
175
+ conflictTable.push([c.key, c.actual, c.remoteValue]);
176
+ });
177
+ console.log((0, table_1.table)(conflictTable));
178
+ const inquirer = require("inquirer");
179
+ const { confirm } = await inquirer.prompt([{
180
+ type: "confirm",
181
+ name: "confirm",
182
+ message: "Do you want to overwrite these changes exactly as you specified?",
183
+ default: false
184
+ }]);
185
+ if (confirm) {
186
+ const retrySpinner = (0, ora_1.default)("Overwriting secrets...").start();
187
+ try {
188
+ // Start retry with Force (no expectedVersions)
189
+ await api_1.api.setSecrets(project, env, payload, undefined, branch);
190
+ retrySpinner.succeed("Secrets overwritten successfully.");
191
+ // Log Audit Force
192
+ try {
193
+ const { logAudit } = require("../lib/audit");
194
+ logAudit("SECRET_UPDATE_FORCE", project, env, { keys: Object.keys(payload), branch });
195
+ }
196
+ catch (e) { }
197
+ }
198
+ catch (retryErr) {
199
+ retrySpinner.fail("Failed to overwrite secrets.");
200
+ console.error(chalk_1.default.red(retryErr.message));
201
+ }
202
+ }
203
+ else {
204
+ console.log(chalk_1.default.yellow("Update cancelled."));
205
+ }
206
+ return; // Handled
207
+ }
208
+ if (spinner.isSpinning)
209
+ spinner.fail("Failed to update secrets");
210
+ if (error.response && error.response.data && error.response.data.error) {
211
+ console.error(chalk_1.default.red(`Server Error: ${error.response.data.error}`));
212
+ }
213
+ else {
214
+ console.error(chalk_1.default.red(error.message));
215
+ }
216
+ }
217
+ });
218
+ // LINK
219
+ exports.secretsCommand
220
+ .command("link")
221
+ .description("Link a secret to another secret (Reference)")
222
+ .argument("<key>", "Key of the secret to create (e.g. DB_URL)")
223
+ .requiredOption("--source <source>", "Source path (format: project/env/key)")
224
+ .addHelpText("after", `
225
+ Examples:
226
+ $ xtra secrets link SHARED_DB_URL --source shared-proj/production/DB_URL
227
+ `)
228
+ .action(async (key, options) => {
229
+ const parentOpts = exports.secretsCommand.opts();
230
+ let { project, env } = parentOpts;
231
+ // Normalize Env
232
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
233
+ env = envMap[env] || env;
234
+ const { source } = options;
235
+ if (!project) {
236
+ project = (0, config_1.getConfigValue)("project");
237
+ }
238
+ if (!project) {
239
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
240
+ process.exit(1);
241
+ }
242
+ // Parse Source: "proj-123/prod/DATABASE_URL"
243
+ const parts = source.split("/");
244
+ if (parts.length !== 3) {
245
+ console.error(chalk_1.default.red("Error: Source must be in format 'projectId/env/key'"));
246
+ return;
247
+ }
248
+ const [sourceProjectId, sourceEnv, sourceKey] = parts;
249
+ const spinner = (0, ora_1.default)(`Linking ${key} to ${source}...`).start();
250
+ try {
251
+ await api_1.api.linkSecret(project, env, key, sourceProjectId, sourceEnv, sourceKey);
252
+ spinner.succeed(chalk_1.default.green(`Secret '${key}' successfully linked to '${sourceKey}'`));
253
+ // Audit log
254
+ try {
255
+ const { logAudit } = require("../lib/audit");
256
+ logAudit("SECRET_LINKED", project, env, { key, source });
257
+ }
258
+ catch (e) { }
259
+ }
260
+ catch (error) {
261
+ spinner.fail("Failed to link secret");
262
+ const safeErr = error?.response?.data?.error || error.message || "Unknown error";
263
+ console.error(chalk_1.default.red(`Server Error: ${safeErr}`));
264
+ }
265
+ });