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,83 @@
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.getRcConfig)().project;
26
+ if (!branch) {
27
+ branch = (0, config_1.getRcConfig)().branch || "main";
28
+ }
29
+ // Normalize Env
30
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
31
+ env = envMap[env] || env;
32
+ if (!project) {
33
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
34
+ process.exit(1);
35
+ }
36
+ const spinner = require("ora")(`Fetching secrets for export (${env} @ ${branch})...`).start();
37
+ try {
38
+ const secrets = await api_1.api.getSecrets(project, env, branch);
39
+ spinner.stop();
40
+ if (!secrets || Object.keys(secrets).length === 0) {
41
+ console.warn(chalk_1.default.yellow("No secrets found to export."));
42
+ return;
43
+ }
44
+ let content = "";
45
+ switch (format.toLowerCase()) {
46
+ case "json":
47
+ content = JSON.stringify(secrets, null, 2);
48
+ break;
49
+ case "dotenv":
50
+ content = Object.entries(secrets)
51
+ .map(([key, value]) => `${key}="${String(value).replace(/"/g, '\\"')}"`)
52
+ .join("\n");
53
+ break;
54
+ case "csv":
55
+ const records = Object.entries(secrets).map(([key, value]) => ({ key, value }));
56
+ content = (0, sync_1.stringify)(records, { header: true });
57
+ break;
58
+ default:
59
+ console.error(chalk_1.default.red(`Error: Unsupported format '${format}'. Use json, dotenv, or csv.`));
60
+ process.exit(1);
61
+ }
62
+ if (output) {
63
+ const outputPath = path_1.default.resolve(process.cwd(), output);
64
+ fs_1.default.writeFileSync(outputPath, content, "utf-8");
65
+ console.log(chalk_1.default.green(`รขล“โ€ Secrets exported to ${outputPath}`));
66
+ }
67
+ else {
68
+ console.log(content);
69
+ }
70
+ // Log Audit
71
+ try {
72
+ const { logAudit } = require("../lib/audit");
73
+ logAudit("SECRET_EXPORT", project, env, { format, destination: output || "stdout", branch });
74
+ }
75
+ catch (e) { }
76
+ }
77
+ catch (error) {
78
+ spinner.fail("Export failed.");
79
+ const safeErr = error?.response?.data?.error || error.message || "Unknown error";
80
+ console.error(chalk_1.default.red(safeErr));
81
+ process.exit(1);
82
+ }
83
+ });
@@ -0,0 +1,179 @@
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.getRcConfig)().project;
85
+ if (!branch) {
86
+ branch = (0, config_1.getRcConfig)().branch || "main";
87
+ }
88
+ // Normalize Env
89
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
90
+ env = envMap[env] || env;
91
+ if (!project) {
92
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
93
+ process.exit(1);
94
+ }
95
+ // Determine if we should merge or overwrite
96
+ const shouldMerge = !output && format === "env";
97
+ // Determine output filename
98
+ let outputPath = output;
99
+ if (!outputPath) {
100
+ switch (format) {
101
+ case "json":
102
+ outputPath = "secrets.json";
103
+ break;
104
+ case "yaml":
105
+ outputPath = "secrets.yaml";
106
+ break;
107
+ default:
108
+ outputPath = ".env";
109
+ break;
110
+ }
111
+ }
112
+ const fullPath = path_1.default.resolve(process.cwd(), outputPath);
113
+ const fileExists = fs_1.default.existsSync(fullPath);
114
+ const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
115
+ try {
116
+ const secrets = await api_1.api.getSecrets(project, env, branch);
117
+ if (!secrets || Object.keys(secrets).length === 0) {
118
+ spinner.warn(chalk_1.default.yellow("No secrets found. File not modified."));
119
+ return;
120
+ }
121
+ let content = "";
122
+ let added = [];
123
+ let updated = [];
124
+ // Handle merge for .env format when no -o flag
125
+ if (shouldMerge && fileExists) {
126
+ spinner.text = "Merging with existing .env file...";
127
+ const existingContent = fs_1.default.readFileSync(fullPath, 'utf-8');
128
+ const mergeResult = mergeEnvContent(existingContent, secrets);
129
+ content = mergeResult.content;
130
+ added = mergeResult.added;
131
+ updated = mergeResult.updated;
132
+ if (added.length === 0 && updated.length === 0) {
133
+ spinner.succeed(chalk_1.default.green("No changes needed - .env is already up to date."));
134
+ return;
135
+ }
136
+ }
137
+ else {
138
+ // Full overwrite mode
139
+ switch (format.toLowerCase()) {
140
+ case "json":
141
+ content = JSON.stringify(secrets, null, 2);
142
+ break;
143
+ case "yaml":
144
+ content = yaml_1.default.stringify(secrets);
145
+ break;
146
+ case "env":
147
+ default:
148
+ content = Object.entries(secrets)
149
+ .map(([key, value]) => `${key}="${value}"`)
150
+ .join("\n");
151
+ break;
152
+ }
153
+ }
154
+ // Write file
155
+ fs_1.default.writeFileSync(fullPath, content, "utf-8");
156
+ // Update manifest
157
+ const { hash } = require("../lib/crypto");
158
+ const { updateManifest } = require("../lib/manifest");
159
+ updateManifest(project, env, secrets, hash);
160
+ // Show result
161
+ if (shouldMerge && fileExists) {
162
+ spinner.succeed(chalk_1.default.green(`Updated ${outputPath}`));
163
+ if (added.length > 0) {
164
+ console.log(chalk_1.default.cyan(` + Added: ${added.join(', ')}`));
165
+ }
166
+ if (updated.length > 0) {
167
+ console.log(chalk_1.default.yellow(` ~ Updated: ${updated.join(', ')}`));
168
+ }
169
+ }
170
+ else {
171
+ spinner.succeed(`Generated ${outputPath} (${Object.keys(secrets).length} secrets)`);
172
+ }
173
+ }
174
+ catch (error) {
175
+ spinner.fail("Failed to generate file.");
176
+ console.error(chalk_1.default.red(error.message));
177
+ process.exit(1);
178
+ }
179
+ });
@@ -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.getRcConfig)().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.getRcConfig)().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,121 @@
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.getRcConfig)().project;
29
+ if (!branch) {
30
+ branch = (0, config_1.getRcConfig)().branch || "main";
31
+ }
32
+ // Normalize Env
33
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
34
+ env = envMap[env] || env;
35
+ if (!project) {
36
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
37
+ process.exit(1);
38
+ }
39
+ const filePath = path_1.default.resolve(process.cwd(), file);
40
+ if (!fs_1.default.existsSync(filePath)) {
41
+ console.error(chalk_1.default.red(`Error: File not found: ${filePath}`));
42
+ process.exit(1);
43
+ }
44
+ let detectedFormat = format;
45
+ if (!detectedFormat) {
46
+ const ext = path_1.default.extname(filePath).toLowerCase();
47
+ if (ext === ".json")
48
+ detectedFormat = "json";
49
+ else if (ext === ".env")
50
+ detectedFormat = "dotenv";
51
+ else if (ext === ".csv")
52
+ detectedFormat = "csv";
53
+ else {
54
+ console.error(chalk_1.default.red("Error: Could not detect format. Please specify -f <format>"));
55
+ process.exit(1);
56
+ }
57
+ }
58
+ const spinner = require("ora")(`Reading ${detectedFormat} file...`).start();
59
+ const fileContent = fs_1.default.readFileSync(filePath, "utf-8");
60
+ let secrets = {};
61
+ try {
62
+ switch (detectedFormat.toLowerCase()) {
63
+ case "json":
64
+ secrets = JSON.parse(fileContent);
65
+ break;
66
+ case "dotenv":
67
+ secrets = dotenv_1.default.parse(fileContent);
68
+ break;
69
+ case "csv":
70
+ const records = (0, sync_1.parse)(fileContent, {
71
+ columns: true,
72
+ skip_empty_lines: true
73
+ });
74
+ // Expect header: key, value or Key, Value
75
+ records.forEach((record) => {
76
+ // Try case-insensitive key lookup
77
+ const key = record.key || record.Key || record.KEY;
78
+ const value = record.value || record.Value || record.VALUE;
79
+ if (key) {
80
+ secrets[key] = value || "";
81
+ }
82
+ });
83
+ break;
84
+ default:
85
+ spinner.fail(`Unsupported format: ${detectedFormat}`);
86
+ process.exit(1);
87
+ }
88
+ if (Object.keys(secrets).length === 0) {
89
+ spinner.fail("No valid secrets found in file.");
90
+ return;
91
+ }
92
+ // Apply prefix
93
+ if (prefix) {
94
+ const prefixedSecrets = {};
95
+ Object.entries(secrets).forEach(([key, value]) => {
96
+ prefixedSecrets[`${prefix}${key}`] = value;
97
+ });
98
+ secrets = prefixedSecrets;
99
+ }
100
+ // Upload
101
+ spinner.text = `Importing ${Object.keys(secrets).length} secrets to ${env} (branch: ${branch})...`;
102
+ await api_1.api.setSecrets(project, env, secrets, undefined, branch);
103
+ spinner.succeed(`Successfully imported ${Object.keys(secrets).length} secrets.`);
104
+ // Log Audit
105
+ try {
106
+ const { logAudit } = require("../lib/audit");
107
+ logAudit("SECRET_IMPORT", project, env, {
108
+ file: path_1.default.basename(filePath),
109
+ format: detectedFormat,
110
+ count: Object.keys(secrets).length,
111
+ branch: branch
112
+ });
113
+ }
114
+ catch (e) { }
115
+ }
116
+ catch (error) {
117
+ spinner.fail("Import failed.");
118
+ console.error(chalk_1.default.red(error.message));
119
+ process.exit(1);
120
+ }
121
+ });
@@ -0,0 +1,205 @@
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 { getAuthToken } = await Promise.resolve().then(() => __importStar(require("../lib/config")));
78
+ const token = getAuthToken();
79
+ const ora = (await Promise.resolve().then(() => __importStar(require("ora")))).default;
80
+ let remoteProjects = [];
81
+ if (!token) {
82
+ console.log(chalk_1.default.yellow(" โ„น Not logged in โ€” project list unavailable. Run `xtra login` first."));
83
+ console.log(chalk_1.default.yellow(" You can still enter a Project ID manually below.\n"));
84
+ }
85
+ else {
86
+ const apiSpinner = ora("Fetching your projects...").start();
87
+ try {
88
+ const { api } = await Promise.resolve().then(() => __importStar(require("../lib/api")));
89
+ const result = await api.getProjects();
90
+ // API might return { projects: [...] } or directly an array
91
+ remoteProjects = Array.isArray(result) ? result : result.projects ?? [];
92
+ if (remoteProjects.length === 0) {
93
+ apiSpinner.warn(chalk_1.default.yellow("No projects found in your workspace. Create one at xtra-security.vercel.app first."));
94
+ }
95
+ else {
96
+ apiSpinner.succeed(`Found ${remoteProjects.length} project${remoteProjects.length > 1 ? "s" : ""}`);
97
+ }
98
+ }
99
+ catch (err) {
100
+ const reason = err?.response?.status === 401
101
+ ? "session expired โ€” run `xtra login` again"
102
+ : err?.response?.data?.error || err?.message || "network error";
103
+ apiSpinner.warn(chalk_1.default.yellow(`Could not fetch projects (${reason}). Enter the Project ID manually.`));
104
+ }
105
+ }
106
+ const fallbackProject = project || (0, config_1.getConfigValue)("project") || "";
107
+ const { project: selectedProject } = await inquirer_1.default.prompt([
108
+ {
109
+ type: remoteProjects.length > 0 ? "list" : "input",
110
+ name: "project",
111
+ message: remoteProjects.length > 0 ? "Select a project:" : "Project ID:",
112
+ choices: remoteProjects.length > 0
113
+ ? remoteProjects.map(p => ({ name: `${p.name} (${p.id})`, value: p.id }))
114
+ : undefined,
115
+ default: fallbackProject,
116
+ validate: (v) => v.trim() ? true : "Project ID is required",
117
+ }
118
+ ]);
119
+ project = selectedProject.trim();
120
+ // Fetch branches for selected project
121
+ let remoteBranches = [];
122
+ const branchSpinner = ora("Fetching branches...").start();
123
+ try {
124
+ const { api } = await Promise.resolve().then(() => __importStar(require("../lib/api")));
125
+ remoteBranches = await api.getBranches(project);
126
+ branchSpinner.stop();
127
+ }
128
+ catch (err) {
129
+ branchSpinner.warn(chalk_1.default.yellow("Could not fetch branches."));
130
+ }
131
+ const answers = await inquirer_1.default.prompt([
132
+ {
133
+ type: "list",
134
+ name: "env",
135
+ message: "Default environment:",
136
+ choices: ["development", "staging", "production"],
137
+ default: env || "development",
138
+ },
139
+ {
140
+ type: remoteBranches.length > 0 ? "list" : "input",
141
+ name: "branch",
142
+ message: remoteBranches.length > 0 ? "Default branch:" : "Default branch name:",
143
+ choices: remoteBranches.length > 0
144
+ ? remoteBranches.map(b => ({ name: b.name, value: b.name }))
145
+ : undefined,
146
+ default: branch || (remoteBranches.length > 0 ? remoteBranches[0].name : "main"),
147
+ },
148
+ {
149
+ type: "confirm",
150
+ name: "addGitignore",
151
+ message: "Add .xtrarc to .gitignore? (recommended)",
152
+ default: true,
153
+ },
154
+ ]);
155
+ env = answers.env;
156
+ branch = answers.branch;
157
+ // Add to .gitignore
158
+ if (answers.addGitignore) {
159
+ const gitignorePath = path.join(cwd, ".gitignore");
160
+ let existing = "";
161
+ try {
162
+ existing = fs.readFileSync(gitignorePath, "utf8");
163
+ }
164
+ catch (_) { }
165
+ if (!existing.includes(".xtrarc")) {
166
+ fs.appendFileSync(gitignorePath, "\n# XtraSecurity config\n.xtrarc\n");
167
+ console.log(chalk_1.default.gray(" โœ” Added .xtrarc to .gitignore"));
168
+ }
169
+ }
170
+ }
171
+ if (!project) {
172
+ console.error(chalk_1.default.red("Error: Project ID is required."));
173
+ process.exit(1);
174
+ }
175
+ // Write .xtrarc
176
+ const rc = {
177
+ project,
178
+ env,
179
+ branch,
180
+ apiUrl: process.env.XTRA_API_URL || "https://xtra-security.vercel.app/api",
181
+ createdAt: new Date().toISOString(),
182
+ };
183
+ fs.writeFileSync(rcPath, JSON.stringify(rc, null, 2), "utf8");
184
+ // โ”€โ”€ Sync global conf store so `xtra run` / `xtra secrets` pick up the new values โ”€โ”€
185
+ (0, config_1.setConfig)("project", project);
186
+ (0, config_1.setConfig)("env", env);
187
+ (0, config_1.setConfig)("branch", branch);
188
+ // Summary
189
+ console.log(chalk_1.default.gray(` Branch : ${chalk_1.default.white(branch)}`));
190
+ console.log();
191
+ // โ”€โ”€ Audit Log (Local Chain) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
192
+ try {
193
+ const { logAudit } = await Promise.resolve().then(() => __importStar(require("../lib/audit")));
194
+ logAudit("PROJECT_INIT", project, env, { branch, rcPath });
195
+ }
196
+ catch (e) {
197
+ // Non-fatal if audit fails
198
+ }
199
+ console.log(chalk_1.default.bold(" Next steps:"));
200
+ console.log(chalk_1.default.gray(" xtra login # authenticate"));
201
+ console.log(chalk_1.default.gray(" xtra secrets list # view secrets"));
202
+ console.log(chalk_1.default.gray(" xtra run node app.js # inject and run"));
203
+ console.log(chalk_1.default.gray(" xtra doctor # verify everything works"));
204
+ console.log();
205
+ });