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,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
+ });