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,84 @@
|
|
|
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.exportCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const api_1 = require("../lib/api");
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const sync_1 = require("csv-stringify/sync");
|
|
13
|
+
const config_1 = require("../lib/config");
|
|
14
|
+
exports.exportCommand = new commander_1.Command("export")
|
|
15
|
+
.description("Export secrets to a file (JSON, Dotenv, CSV)")
|
|
16
|
+
.option("-p, --project <projectId>", "Project ID")
|
|
17
|
+
.option("-e, --env <environment>", "Environment (development, staging, production)", "development")
|
|
18
|
+
.option("-b, --branch <branchName>", "Branch Name")
|
|
19
|
+
.option("-f, --format <format>", "Output format (json, dotenv, csv)", "json")
|
|
20
|
+
.option("-o, --output <file>", "Output file path (default: stdout)")
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
let { project, env, branch, format, output } = options;
|
|
23
|
+
// Use config fallback
|
|
24
|
+
if (!project) {
|
|
25
|
+
project = (0, config_1.getConfigValue)("project");
|
|
26
|
+
}
|
|
27
|
+
if (!branch) {
|
|
28
|
+
branch = (0, config_1.getConfigValue)("branch") || "main";
|
|
29
|
+
}
|
|
30
|
+
// Normalize Env
|
|
31
|
+
const envMap = { dev: "development", stg: "staging", prod: "production" };
|
|
32
|
+
env = envMap[env] || env;
|
|
33
|
+
if (!project) {
|
|
34
|
+
console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const spinner = require("ora")(`Fetching secrets for export (${env} @ ${branch})...`).start();
|
|
38
|
+
try {
|
|
39
|
+
const secrets = await api_1.api.getSecrets(project, env, branch);
|
|
40
|
+
spinner.stop();
|
|
41
|
+
if (!secrets || Object.keys(secrets).length === 0) {
|
|
42
|
+
console.warn(chalk_1.default.yellow("No secrets found to export."));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let content = "";
|
|
46
|
+
switch (format.toLowerCase()) {
|
|
47
|
+
case "json":
|
|
48
|
+
content = JSON.stringify(secrets, null, 2);
|
|
49
|
+
break;
|
|
50
|
+
case "dotenv":
|
|
51
|
+
content = Object.entries(secrets)
|
|
52
|
+
.map(([key, value]) => `${key}="${String(value).replace(/"/g, '\\"')}"`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
break;
|
|
55
|
+
case "csv":
|
|
56
|
+
const records = Object.entries(secrets).map(([key, value]) => ({ key, value }));
|
|
57
|
+
content = (0, sync_1.stringify)(records, { header: true });
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
console.error(chalk_1.default.red(`Error: Unsupported format '${format}'. Use json, dotenv, or csv.`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (output) {
|
|
64
|
+
const outputPath = path_1.default.resolve(process.cwd(), output);
|
|
65
|
+
fs_1.default.writeFileSync(outputPath, content, "utf-8");
|
|
66
|
+
console.log(chalk_1.default.green(`ā Secrets exported to ${outputPath}`));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(content);
|
|
70
|
+
}
|
|
71
|
+
// Log Audit
|
|
72
|
+
try {
|
|
73
|
+
const { logAudit } = require("../lib/audit");
|
|
74
|
+
logAudit("SECRET_EXPORT", project, env, { format, destination: output || "stdout", branch });
|
|
75
|
+
}
|
|
76
|
+
catch (e) { }
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
spinner.fail("Export failed.");
|
|
80
|
+
const safeErr = error?.response?.data?.error || error.message || "Unknown error";
|
|
81
|
+
console.error(chalk_1.default.red(safeErr));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
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.generateCommand = 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 fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
14
|
+
const config_1 = require("../lib/config");
|
|
15
|
+
// Helper: Parse .env file into key-value pairs
|
|
16
|
+
function parseEnvFile(content) {
|
|
17
|
+
const result = {};
|
|
18
|
+
const lines = content.split(/\r?\n/);
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
// Skip empty lines and comments
|
|
22
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
23
|
+
continue;
|
|
24
|
+
const idx = trimmed.indexOf('=');
|
|
25
|
+
if (idx === -1)
|
|
26
|
+
continue;
|
|
27
|
+
const key = trimmed.substring(0, idx).trim();
|
|
28
|
+
let value = trimmed.substring(idx + 1).trim();
|
|
29
|
+
// Remove surrounding quotes if present
|
|
30
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
31
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
32
|
+
value = value.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
result[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
// Helper: Merge secrets into existing file content, preserving comments and order
|
|
39
|
+
function mergeEnvContent(existingContent, newSecrets) {
|
|
40
|
+
const lines = existingContent.split(/\r?\n/);
|
|
41
|
+
const existingKeys = new Set();
|
|
42
|
+
const added = [];
|
|
43
|
+
const updated = [];
|
|
44
|
+
// First pass: update existing keys
|
|
45
|
+
const updatedLines = lines.map(line => {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
48
|
+
return line;
|
|
49
|
+
const idx = trimmed.indexOf('=');
|
|
50
|
+
if (idx === -1)
|
|
51
|
+
return line;
|
|
52
|
+
const key = trimmed.substring(0, idx).trim();
|
|
53
|
+
existingKeys.add(key);
|
|
54
|
+
if (key in newSecrets) {
|
|
55
|
+
const oldValue = parseEnvFile(line)[key];
|
|
56
|
+
if (oldValue !== newSecrets[key]) {
|
|
57
|
+
updated.push(key);
|
|
58
|
+
}
|
|
59
|
+
return `${key}="${newSecrets[key]}"`;
|
|
60
|
+
}
|
|
61
|
+
return line;
|
|
62
|
+
});
|
|
63
|
+
// Second pass: add new keys
|
|
64
|
+
for (const [key, value] of Object.entries(newSecrets)) {
|
|
65
|
+
if (!existingKeys.has(key)) {
|
|
66
|
+
updatedLines.push(`${key}="${value}"`);
|
|
67
|
+
added.push(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { content: updatedLines.join('\n'), added, updated };
|
|
71
|
+
}
|
|
72
|
+
exports.generateCommand = new commander_1.Command("generate")
|
|
73
|
+
.description("Generate local configuration files from secrets")
|
|
74
|
+
.option("-p, --project <projectId>", "Project ID")
|
|
75
|
+
.option("-e, --env <environment>", "Environment (dev, stg, prod)", "dev")
|
|
76
|
+
.option("-b, --branch <branchName>", "Branch Name")
|
|
77
|
+
.option("-o, --output <path>", "Output file path (forces complete overwrite)")
|
|
78
|
+
.option("-f, --format <format>", "Output format (env, json, yaml)", "env")
|
|
79
|
+
.option("--force", "Skip confirmation prompts", false)
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
let { project, env, branch, output, format, force } = options;
|
|
82
|
+
// Use config fallback
|
|
83
|
+
if (!project) {
|
|
84
|
+
project = (0, config_1.getConfigValue)("project");
|
|
85
|
+
}
|
|
86
|
+
if (!branch) {
|
|
87
|
+
branch = (0, config_1.getConfigValue)("branch") || "main";
|
|
88
|
+
}
|
|
89
|
+
// Normalize Env
|
|
90
|
+
const envMap = { dev: "development", stg: "staging", prod: "production" };
|
|
91
|
+
env = envMap[env] || env;
|
|
92
|
+
if (!project) {
|
|
93
|
+
console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// Determine if we should merge or overwrite
|
|
97
|
+
const shouldMerge = !output && format === "env";
|
|
98
|
+
// Determine output filename
|
|
99
|
+
let outputPath = output;
|
|
100
|
+
if (!outputPath) {
|
|
101
|
+
switch (format) {
|
|
102
|
+
case "json":
|
|
103
|
+
outputPath = "secrets.json";
|
|
104
|
+
break;
|
|
105
|
+
case "yaml":
|
|
106
|
+
outputPath = "secrets.yaml";
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
outputPath = ".env";
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const fullPath = path_1.default.resolve(process.cwd(), outputPath);
|
|
114
|
+
const fileExists = fs_1.default.existsSync(fullPath);
|
|
115
|
+
const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
|
|
116
|
+
try {
|
|
117
|
+
const secrets = await api_1.api.getSecrets(project, env, branch);
|
|
118
|
+
if (!secrets || Object.keys(secrets).length === 0) {
|
|
119
|
+
spinner.warn(chalk_1.default.yellow("No secrets found. File not modified."));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let content = "";
|
|
123
|
+
let added = [];
|
|
124
|
+
let updated = [];
|
|
125
|
+
// Handle merge for .env format when no -o flag
|
|
126
|
+
if (shouldMerge && fileExists) {
|
|
127
|
+
spinner.text = "Merging with existing .env file...";
|
|
128
|
+
const existingContent = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
129
|
+
const mergeResult = mergeEnvContent(existingContent, secrets);
|
|
130
|
+
content = mergeResult.content;
|
|
131
|
+
added = mergeResult.added;
|
|
132
|
+
updated = mergeResult.updated;
|
|
133
|
+
if (added.length === 0 && updated.length === 0) {
|
|
134
|
+
spinner.succeed(chalk_1.default.green("No changes needed - .env is already up to date."));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Full overwrite mode
|
|
140
|
+
switch (format.toLowerCase()) {
|
|
141
|
+
case "json":
|
|
142
|
+
content = JSON.stringify(secrets, null, 2);
|
|
143
|
+
break;
|
|
144
|
+
case "yaml":
|
|
145
|
+
content = yaml_1.default.stringify(secrets);
|
|
146
|
+
break;
|
|
147
|
+
case "env":
|
|
148
|
+
default:
|
|
149
|
+
content = Object.entries(secrets)
|
|
150
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
151
|
+
.join("\n");
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Write file
|
|
156
|
+
fs_1.default.writeFileSync(fullPath, content, "utf-8");
|
|
157
|
+
// Update manifest
|
|
158
|
+
const { hash } = require("../lib/crypto");
|
|
159
|
+
const { updateManifest } = require("../lib/manifest");
|
|
160
|
+
updateManifest(project, env, secrets, hash);
|
|
161
|
+
// Show result
|
|
162
|
+
if (shouldMerge && fileExists) {
|
|
163
|
+
spinner.succeed(chalk_1.default.green(`Updated ${outputPath}`));
|
|
164
|
+
if (added.length > 0) {
|
|
165
|
+
console.log(chalk_1.default.cyan(` + Added: ${added.join(', ')}`));
|
|
166
|
+
}
|
|
167
|
+
if (updated.length > 0) {
|
|
168
|
+
console.log(chalk_1.default.yellow(` ~ Updated: ${updated.join(', ')}`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
spinner.succeed(`Generated ${outputPath} (${Object.keys(secrets).length} secrets)`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
spinner.fail("Failed to generate file.");
|
|
177
|
+
console.error(chalk_1.default.red(error.message));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
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.rollbackCommand = exports.historyCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const api_1 = require("../lib/api");
|
|
10
|
+
const config_1 = require("../lib/config");
|
|
11
|
+
exports.historyCommand = new commander_1.Command("history");
|
|
12
|
+
exports.historyCommand
|
|
13
|
+
.argument("<key>", "Secret key")
|
|
14
|
+
.option("-p, --project <id>", "Project ID")
|
|
15
|
+
.option("-e, --env <environment>", "Environment", "development")
|
|
16
|
+
.description("View version history of a secret")
|
|
17
|
+
.action(async (key, options) => {
|
|
18
|
+
try {
|
|
19
|
+
let projectId = options.project;
|
|
20
|
+
if (!projectId) {
|
|
21
|
+
projectId = (0, config_1.getConfigValue)("project");
|
|
22
|
+
if (!projectId) {
|
|
23
|
+
console.error(chalk_1.default.red("Error: Project ID not found. Use -p <id> or run 'xtra project set' first."));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const data = await api_1.api.getSecretHistory(projectId, options.env, key);
|
|
28
|
+
console.log(chalk_1.default.bold(`\nHistory for ${key} (${options.env})`));
|
|
29
|
+
console.log(chalk_1.default.gray(`Current Version: ${data.currentVersion}\n`));
|
|
30
|
+
const history = data.history || [];
|
|
31
|
+
if (history.length === 0) {
|
|
32
|
+
console.log("No history found.");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
history.forEach((h) => {
|
|
36
|
+
const dateStr = new Date(h.updatedAt).toLocaleString();
|
|
37
|
+
console.log(chalk_1.default.blue(`v${h.version}`) + ` - ${dateStr} by ${h.updatedBy}`);
|
|
38
|
+
if (h.description)
|
|
39
|
+
console.log(chalk_1.default.gray(` ${h.description}`));
|
|
40
|
+
console.log("");
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(chalk_1.default.red("Error fetching history:"), error.message || error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
exports.rollbackCommand = new commander_1.Command("rollback");
|
|
49
|
+
exports.rollbackCommand
|
|
50
|
+
.argument("<key>", "Secret key")
|
|
51
|
+
.argument("<version>", "Version to rollback to")
|
|
52
|
+
.option("-p, --project <id>", "Project ID")
|
|
53
|
+
.option("-e, --env <environment>", "Environment", "development")
|
|
54
|
+
.description("Rollback a secret to a previous version")
|
|
55
|
+
.action(async (key, version, options) => {
|
|
56
|
+
try {
|
|
57
|
+
let projectId = options.project;
|
|
58
|
+
if (!projectId) {
|
|
59
|
+
projectId = (0, config_1.getConfigValue)("project");
|
|
60
|
+
if (!projectId) {
|
|
61
|
+
console.error(chalk_1.default.red("Error: Project ID not found. Use -p <id> or run 'xtra project set' first."));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk_1.default.yellow(`Rolling back ${key} to v${version}...`));
|
|
66
|
+
const result = await api_1.api.rollbackSecret(projectId, options.env, key, version);
|
|
67
|
+
if (result.success) {
|
|
68
|
+
console.log(chalk_1.default.green(`\nSuccessfully rolled back to v${version} (New current version: v${result.version})`));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(chalk_1.default.red("Rollback failed."));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error(chalk_1.default.red("Error rolling back:"), error.message || error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
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.importCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const api_1 = require("../lib/api");
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
13
|
+
const sync_1 = require("csv-parse/sync");
|
|
14
|
+
const config_1 = require("../lib/config");
|
|
15
|
+
exports.importCommand = new commander_1.Command("import")
|
|
16
|
+
.description("Import secrets from a file (JSON, Dotenv, CSV)")
|
|
17
|
+
.argument("<file>", "Path to the file to import")
|
|
18
|
+
.option("-p, --project <projectId>", "Project ID")
|
|
19
|
+
.option("-e, --env <environment>", "Environment (development, staging, production)", "development")
|
|
20
|
+
.option("-b, --branch <branchName>", "Branch Name")
|
|
21
|
+
.option("-f, --format <format>", "Input format (json, dotenv, csv). Auto-detected if omitted.")
|
|
22
|
+
.option("--prefix <prefix>", "Add prefix to all imported keys")
|
|
23
|
+
.option("--overwrite", "Overwrite existing secrets (default: merged, but API usually upserts)", true)
|
|
24
|
+
.action(async (file, options) => {
|
|
25
|
+
let { project, env, branch, format, prefix } = options;
|
|
26
|
+
// Use config fallback
|
|
27
|
+
if (!project) {
|
|
28
|
+
project = (0, config_1.getConfigValue)("project");
|
|
29
|
+
}
|
|
30
|
+
if (!branch) {
|
|
31
|
+
branch = (0, config_1.getConfigValue)("branch") || "main";
|
|
32
|
+
}
|
|
33
|
+
// Normalize Env
|
|
34
|
+
const envMap = { dev: "development", stg: "staging", prod: "production" };
|
|
35
|
+
env = envMap[env] || env;
|
|
36
|
+
if (!project) {
|
|
37
|
+
console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const filePath = path_1.default.resolve(process.cwd(), file);
|
|
41
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
42
|
+
console.error(chalk_1.default.red(`Error: File not found: ${filePath}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
let detectedFormat = format;
|
|
46
|
+
if (!detectedFormat) {
|
|
47
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
48
|
+
if (ext === ".json")
|
|
49
|
+
detectedFormat = "json";
|
|
50
|
+
else if (ext === ".env")
|
|
51
|
+
detectedFormat = "dotenv";
|
|
52
|
+
else if (ext === ".csv")
|
|
53
|
+
detectedFormat = "csv";
|
|
54
|
+
else {
|
|
55
|
+
console.error(chalk_1.default.red("Error: Could not detect format. Please specify -f <format>"));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const spinner = require("ora")(`Reading ${detectedFormat} file...`).start();
|
|
60
|
+
const fileContent = fs_1.default.readFileSync(filePath, "utf-8");
|
|
61
|
+
let secrets = {};
|
|
62
|
+
try {
|
|
63
|
+
switch (detectedFormat.toLowerCase()) {
|
|
64
|
+
case "json":
|
|
65
|
+
secrets = JSON.parse(fileContent);
|
|
66
|
+
break;
|
|
67
|
+
case "dotenv":
|
|
68
|
+
secrets = dotenv_1.default.parse(fileContent);
|
|
69
|
+
break;
|
|
70
|
+
case "csv":
|
|
71
|
+
const records = (0, sync_1.parse)(fileContent, {
|
|
72
|
+
columns: true,
|
|
73
|
+
skip_empty_lines: true
|
|
74
|
+
});
|
|
75
|
+
// Expect header: key, value or Key, Value
|
|
76
|
+
records.forEach((record) => {
|
|
77
|
+
// Try case-insensitive key lookup
|
|
78
|
+
const key = record.key || record.Key || record.KEY;
|
|
79
|
+
const value = record.value || record.Value || record.VALUE;
|
|
80
|
+
if (key) {
|
|
81
|
+
secrets[key] = value || "";
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
spinner.fail(`Unsupported format: ${detectedFormat}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(secrets).length === 0) {
|
|
90
|
+
spinner.fail("No valid secrets found in file.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Apply prefix
|
|
94
|
+
if (prefix) {
|
|
95
|
+
const prefixedSecrets = {};
|
|
96
|
+
Object.entries(secrets).forEach(([key, value]) => {
|
|
97
|
+
prefixedSecrets[`${prefix}${key}`] = value;
|
|
98
|
+
});
|
|
99
|
+
secrets = prefixedSecrets;
|
|
100
|
+
}
|
|
101
|
+
// Upload
|
|
102
|
+
spinner.text = `Importing ${Object.keys(secrets).length} secrets to ${env} (branch: ${branch})...`;
|
|
103
|
+
await api_1.api.setSecrets(project, env, secrets, undefined, branch);
|
|
104
|
+
spinner.succeed(`Successfully imported ${Object.keys(secrets).length} secrets.`);
|
|
105
|
+
// Log Audit
|
|
106
|
+
try {
|
|
107
|
+
const { logAudit } = require("../lib/audit");
|
|
108
|
+
logAudit("SECRET_IMPORT", project, env, {
|
|
109
|
+
file: path_1.default.basename(filePath),
|
|
110
|
+
format: detectedFormat,
|
|
111
|
+
count: Object.keys(secrets).length,
|
|
112
|
+
branch: branch
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (e) { }
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
spinner.fail("Import failed.");
|
|
119
|
+
console.error(chalk_1.default.red(error.message));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
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.initCommand = void 0;
|
|
40
|
+
/**
|
|
41
|
+
* init.ts ā Bootstrap a new project with .xtrarc and recommended structure
|
|
42
|
+
*/
|
|
43
|
+
const commander_1 = require("commander");
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const config_1 = require("../lib/config");
|
|
49
|
+
const RC_FILE = ".xtrarc";
|
|
50
|
+
exports.initCommand = new commander_1.Command("init")
|
|
51
|
+
.description("Bootstrap a new project ā creates .xtrarc and recommended structure")
|
|
52
|
+
.option("--project <id>", "Project ID (skip prompt)")
|
|
53
|
+
.option("--env <env>", "Default environment", "development")
|
|
54
|
+
.option("--branch <branch>", "Default branch", "main")
|
|
55
|
+
.option("-y, --yes", "Accept all defaults without prompting", false)
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const rcPath = path.join(cwd, RC_FILE);
|
|
59
|
+
console.log(chalk_1.default.bold("\nš xtra init ā setting up your project\n"));
|
|
60
|
+
// Warn if already initialized
|
|
61
|
+
if (fs.existsSync(rcPath)) {
|
|
62
|
+
const { overwrite } = await inquirer_1.default.prompt([{
|
|
63
|
+
type: "confirm",
|
|
64
|
+
name: "overwrite",
|
|
65
|
+
message: chalk_1.default.yellow(`.xtrarc already exists. Overwrite it?`),
|
|
66
|
+
default: false,
|
|
67
|
+
}]);
|
|
68
|
+
if (!overwrite) {
|
|
69
|
+
console.log(chalk_1.default.gray("Aborted."));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let { project, env, branch } = options;
|
|
74
|
+
// Interactive prompts if not --yes
|
|
75
|
+
if (!options.yes) {
|
|
76
|
+
// āā API Projects Fetch āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
77
|
+
const ora = (await Promise.resolve().then(() => __importStar(require("ora")))).default;
|
|
78
|
+
const apiSpinner = ora("Fetching your projects...").start();
|
|
79
|
+
let remoteProjects = [];
|
|
80
|
+
try {
|
|
81
|
+
const { api } = await Promise.resolve().then(() => __importStar(require("../lib/api")));
|
|
82
|
+
remoteProjects = await api.getProjects();
|
|
83
|
+
apiSpinner.succeed(`Found ${remoteProjects.length} projects`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
apiSpinner.warn(chalk_1.default.yellow("Could not fetch projects. You will need to enter the ID manually."));
|
|
87
|
+
}
|
|
88
|
+
const fallbackProject = project || (0, config_1.getConfigValue)("project") || "";
|
|
89
|
+
const answers = await inquirer_1.default.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: remoteProjects.length > 0 ? "list" : "input",
|
|
92
|
+
name: "project",
|
|
93
|
+
message: remoteProjects.length > 0 ? "Select a project:" : "Project ID:",
|
|
94
|
+
choices: remoteProjects.length > 0
|
|
95
|
+
? remoteProjects.map(p => ({ name: `${p.name} (${p.id})`, value: p.id }))
|
|
96
|
+
: undefined,
|
|
97
|
+
default: fallbackProject,
|
|
98
|
+
validate: (v) => v.trim() ? true : "Project ID is required",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "list",
|
|
102
|
+
name: "env",
|
|
103
|
+
message: "Default environment:",
|
|
104
|
+
choices: ["development", "staging", "production"],
|
|
105
|
+
default: env || "development",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "input",
|
|
109
|
+
name: "branch",
|
|
110
|
+
message: "Default branch:",
|
|
111
|
+
default: branch || "main",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: "confirm",
|
|
115
|
+
name: "addGitignore",
|
|
116
|
+
message: "Add .xtrarc to .gitignore? (recommended)",
|
|
117
|
+
default: true,
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
project = answers.project.trim();
|
|
121
|
+
env = answers.env;
|
|
122
|
+
branch = answers.branch;
|
|
123
|
+
// Add to .gitignore
|
|
124
|
+
if (answers.addGitignore) {
|
|
125
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
126
|
+
let existing = "";
|
|
127
|
+
try {
|
|
128
|
+
existing = fs.readFileSync(gitignorePath, "utf8");
|
|
129
|
+
}
|
|
130
|
+
catch (_) { }
|
|
131
|
+
if (!existing.includes(".xtrarc")) {
|
|
132
|
+
fs.appendFileSync(gitignorePath, "\n# XtraSecurity config\n.xtrarc\n");
|
|
133
|
+
console.log(chalk_1.default.gray(" ā Added .xtrarc to .gitignore"));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!project) {
|
|
138
|
+
console.error(chalk_1.default.red("Error: Project ID is required."));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
// Write .xtrarc
|
|
142
|
+
const rc = {
|
|
143
|
+
project,
|
|
144
|
+
env,
|
|
145
|
+
branch,
|
|
146
|
+
apiUrl: process.env.XTRA_API_URL || "http://localhost:3000/api",
|
|
147
|
+
createdAt: new Date().toISOString(),
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(rcPath, JSON.stringify(rc, null, 2), "utf8");
|
|
150
|
+
// Summary
|
|
151
|
+
console.log(chalk_1.default.green(`\n ā
Initialized! Created ${RC_FILE}:\n`));
|
|
152
|
+
console.log(chalk_1.default.gray(` Project : ${chalk_1.default.white(project)}`));
|
|
153
|
+
console.log(chalk_1.default.gray(` Env : ${chalk_1.default.white(env)}`));
|
|
154
|
+
console.log(chalk_1.default.gray(` Branch : ${chalk_1.default.white(branch)}`));
|
|
155
|
+
console.log();
|
|
156
|
+
console.log(chalk_1.default.bold(" Next steps:"));
|
|
157
|
+
console.log(chalk_1.default.gray(" xtra login # authenticate"));
|
|
158
|
+
console.log(chalk_1.default.gray(" xtra secrets list # view secrets"));
|
|
159
|
+
console.log(chalk_1.default.gray(" xtra run node app.js # inject and run"));
|
|
160
|
+
console.log(chalk_1.default.gray(" xtra doctor # verify everything works"));
|
|
161
|
+
console.log();
|
|
162
|
+
});
|