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,89 @@
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
+ const api_1 = require("./api");
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const config_1 = require("./config");
9
+ // Mock axios and config
10
+ jest.mock('axios');
11
+ jest.mock('./config');
12
+ const mockAxios = axios_1.default;
13
+ describe('API Client', () => {
14
+ const mockCreate = jest.fn();
15
+ const mockGet = jest.fn();
16
+ const mockPost = jest.fn();
17
+ beforeAll(() => {
18
+ mockAxios.create = mockCreate;
19
+ config_1.getConfig.mockReturnValue({ apiUrl: 'http://localhost:3000/api' });
20
+ config_1.getAuthToken.mockReturnValue('test-token-123');
21
+ mockCreate.mockReturnValue({
22
+ get: mockGet,
23
+ post: mockPost,
24
+ });
25
+ });
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+ describe('login', () => {
30
+ it('should send login request with email and password', async () => {
31
+ mockPost.mockResolvedValue({ data: { token: 'new-token' } });
32
+ const result = await api_1.api.login('user@example.com', 'password123');
33
+ expect(mockPost).toHaveBeenCalledWith('/auth/cli-login', {
34
+ email: 'user@example.com',
35
+ password: 'password123',
36
+ apiKey: undefined
37
+ });
38
+ expect(result).toEqual({ token: 'new-token' });
39
+ });
40
+ it('should send login request with API key', async () => {
41
+ mockPost.mockResolvedValue({ data: { token: 'api-key-token' } });
42
+ const result = await api_1.api.login(undefined, undefined, 'xtra_key_abc123');
43
+ expect(mockPost).toHaveBeenCalledWith('/auth/cli-login', {
44
+ email: undefined,
45
+ password: undefined,
46
+ apiKey: 'xtra_key_abc123'
47
+ });
48
+ expect(result).toEqual({ token: 'api-key-token' });
49
+ });
50
+ });
51
+ describe('getSecrets', () => {
52
+ it('should fetch secrets for project and environment', async () => {
53
+ const mockSecrets = { secrets: [{ key: 'DB_URL', value: 'postgres://...' }] };
54
+ mockGet.mockResolvedValue({ data: mockSecrets });
55
+ const result = await api_1.api.getSecrets('proj-123', 'production', 'main');
56
+ expect(mockGet).toHaveBeenCalledWith('/projects/proj-123/envs/production/secrets?branch=main');
57
+ expect(result).toEqual(mockSecrets);
58
+ });
59
+ it('should use default branch if not specified', async () => {
60
+ mockGet.mockResolvedValue({ data: { secrets: [] } });
61
+ await api_1.api.getSecrets('proj-123', 'dev');
62
+ expect(mockGet).toHaveBeenCalledWith('/projects/proj-123/envs/dev/secrets?branch=main');
63
+ });
64
+ });
65
+ describe('setSecrets', () => {
66
+ it('should post secrets to API', async () => {
67
+ const secrets = { DB_URL: 'postgres://new', API_KEY: 'key123' };
68
+ mockPost.mockResolvedValue({ data: { success: true } });
69
+ const result = await api_1.api.setSecrets('proj-123', 'staging', secrets);
70
+ expect(mockPost).toHaveBeenCalledWith('/projects/proj-123/envs/staging/secrets', {
71
+ secrets,
72
+ expectedVersions: undefined,
73
+ branch: 'main'
74
+ });
75
+ expect(result).toEqual({ success: true });
76
+ });
77
+ });
78
+ describe('rotateSecret', () => {
79
+ it('should rotate secret with strategy', async () => {
80
+ mockPost.mockResolvedValue({ data: { newValue: 'rotated-value' } });
81
+ const result = await api_1.api.rotateSecret('proj-123', 'prod', 'API_KEY', 'regenerate');
82
+ expect(mockPost).toHaveBeenCalledWith('/projects/proj-123/envs/prod/secrets/API_KEY/rotate', {
83
+ strategy: 'regenerate',
84
+ parsedNewValue: undefined
85
+ });
86
+ expect(result).toEqual({ newValue: 'rotated-value' });
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,136 @@
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.logAudit = logAudit;
7
+ exports.loadAuditLogs = loadAuditLogs;
8
+ exports.saveAuditLogs = saveAuditLogs;
9
+ exports.syncAuditLogs = syncAuditLogs;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const uuid_1 = require("uuid");
13
+ const crypto_1 = require("./crypto");
14
+ const config_1 = require("./config");
15
+ const api_1 = require("./api");
16
+ const MANIFEST_DIR = path_1.default.join(process.cwd(), ".xtra");
17
+ const AUDIT_FILE = path_1.default.join(MANIFEST_DIR, "audit.enc");
18
+ /**
19
+ * Sanitize sensitive data from details
20
+ */
21
+ function sanitizeDetails(details) {
22
+ if (!details || typeof details !== 'object')
23
+ return details;
24
+ const sanitized = { ...details };
25
+ const sensitiveKeys = ['password', 'token', 'secret', 'value', 'apiKey', 'secretValue'];
26
+ for (const key of Object.keys(sanitized)) {
27
+ if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
28
+ sanitized[key] = '[REDACTED]';
29
+ }
30
+ }
31
+ return sanitized;
32
+ }
33
+ /**
34
+ * Send audit log to backend API
35
+ */
36
+ async function sendToBackend(entry) {
37
+ try {
38
+ const config = (0, config_1.getConfig)();
39
+ // Skip if not authenticated
40
+ if (!config.token) {
41
+ return false;
42
+ }
43
+ const payload = {
44
+ action: entry.action,
45
+ entity: 'cli-project',
46
+ entityId: entry.projectId || 'global',
47
+ details: sanitizeDetails(entry.details),
48
+ timestamp: entry.timestamp,
49
+ projectId: entry.projectId,
50
+ };
51
+ await api_1.api.post('/audit/cli-logs', { logs: [payload] });
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ // Silently fail - don't disrupt CLI operations
56
+ if (process.env.DEBUG) {
57
+ console.error('Failed to send audit log to backend:', error);
58
+ }
59
+ return false;
60
+ }
61
+ }
62
+ function logAudit(action, projectId, environment, details) {
63
+ const entry = {
64
+ id: (0, uuid_1.v4)(),
65
+ timestamp: new Date().toISOString(),
66
+ action,
67
+ projectId,
68
+ environment,
69
+ details,
70
+ synced: false
71
+ };
72
+ // Save locally
73
+ const logs = loadAuditLogs();
74
+ logs.push(entry);
75
+ saveAuditLogs(logs);
76
+ // Send to backend (non-blocking)
77
+ sendToBackend(entry).then((synced) => {
78
+ if (synced) {
79
+ // Mark as synced in local logs
80
+ entry.synced = true;
81
+ const updatedLogs = loadAuditLogs();
82
+ const index = updatedLogs.findIndex(l => l.id === entry.id);
83
+ if (index !== -1) {
84
+ updatedLogs[index].synced = true;
85
+ saveAuditLogs(updatedLogs);
86
+ }
87
+ }
88
+ }).catch(() => {
89
+ // Ignore errors
90
+ });
91
+ }
92
+ function loadAuditLogs() {
93
+ if (!fs_1.default.existsSync(AUDIT_FILE)) {
94
+ return [];
95
+ }
96
+ try {
97
+ const encryptedStr = fs_1.default.readFileSync(AUDIT_FILE, "utf-8");
98
+ const encryptedObj = JSON.parse(encryptedStr);
99
+ const decrypted = (0, crypto_1.decrypt)(encryptedObj);
100
+ return JSON.parse(decrypted);
101
+ }
102
+ catch (error) {
103
+ // console.error("Failed to load audit logs", error);
104
+ return [];
105
+ }
106
+ }
107
+ function saveAuditLogs(logs) {
108
+ if (!fs_1.default.existsSync(MANIFEST_DIR)) {
109
+ fs_1.default.mkdirSync(MANIFEST_DIR, { recursive: true });
110
+ }
111
+ const serialized = JSON.stringify(logs);
112
+ const encryptedObj = (0, crypto_1.encrypt)(serialized);
113
+ fs_1.default.writeFileSync(AUDIT_FILE, JSON.stringify(encryptedObj), "utf-8");
114
+ }
115
+ /**
116
+ * Sync unsynced audit logs to backend
117
+ */
118
+ async function syncAuditLogs() {
119
+ const logs = loadAuditLogs();
120
+ const unsynced = logs.filter(l => !l.synced);
121
+ if (unsynced.length === 0) {
122
+ return 0;
123
+ }
124
+ let syncedCount = 0;
125
+ for (const entry of unsynced) {
126
+ const success = await sendToBackend(entry);
127
+ if (success) {
128
+ entry.synced = true;
129
+ syncedCount++;
130
+ }
131
+ }
132
+ if (syncedCount > 0) {
133
+ saveAuditLogs(logs);
134
+ }
135
+ return syncedCount;
136
+ }
@@ -0,0 +1,70 @@
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.getRcConfig = exports.getProjectConfig = exports.getAuthToken = exports.clearConfig = exports.setConfig = exports.getConfigValue = exports.getConfig = void 0;
7
+ const conf_1 = __importDefault(require("conf"));
8
+ const PRODUCTION_API_URL = "https://xtra-security.vercel.app/api";
9
+ const config = new conf_1.default({
10
+ projectName: "xtra-cli",
11
+ defaults: {
12
+ apiUrl: PRODUCTION_API_URL,
13
+ },
14
+ });
15
+ // Auto-heal: if an old localhost value was persisted, clear it so we use production.
16
+ // Users who intentionally want localhost should set XTRA_API_URL env var instead.
17
+ const _storedApiUrl = config.get("apiUrl");
18
+ if (_storedApiUrl && _storedApiUrl.includes("localhost") && !process.env.XTRA_API_URL) {
19
+ config.set("apiUrl", PRODUCTION_API_URL);
20
+ }
21
+ const getConfig = () => ({
22
+ ...config.store,
23
+ apiUrl: process.env.XTRA_API_URL || config.get("apiUrl") || PRODUCTION_API_URL
24
+ });
25
+ exports.getConfig = getConfig;
26
+ const getConfigValue = (key) => config.get(key);
27
+ exports.getConfigValue = getConfigValue;
28
+ const setConfig = (key, value) => config.set(key, value);
29
+ exports.setConfig = setConfig;
30
+ const clearConfig = () => config.clear();
31
+ exports.clearConfig = clearConfig;
32
+ const getAuthToken = () => config.get("token");
33
+ exports.getAuthToken = getAuthToken;
34
+ const fs_1 = __importDefault(require("fs"));
35
+ const path_1 = __importDefault(require("path"));
36
+ const getProjectConfig = async () => {
37
+ try {
38
+ const configPath = path_1.default.join(process.cwd(), "xtra.json");
39
+ if (fs_1.default.existsSync(configPath)) {
40
+ const content = fs_1.default.readFileSync(configPath, "utf-8");
41
+ return JSON.parse(content);
42
+ }
43
+ return null;
44
+ }
45
+ catch (error) {
46
+ return null;
47
+ }
48
+ };
49
+ exports.getProjectConfig = getProjectConfig;
50
+ /**
51
+ * Reads .xtrarc from the current working directory.
52
+ * Returns project/env/branch with fallback to the global conf store.
53
+ * All commands should use this instead of calling getConfigValue() directly.
54
+ */
55
+ const getRcConfig = () => {
56
+ let rc = {};
57
+ try {
58
+ const rcPath = path_1.default.join(process.cwd(), ".xtrarc");
59
+ if (fs_1.default.existsSync(rcPath)) {
60
+ rc = JSON.parse(fs_1.default.readFileSync(rcPath, "utf-8"));
61
+ }
62
+ }
63
+ catch (_) { }
64
+ return {
65
+ project: rc.project || config.get("project") || "",
66
+ env: rc.env || config.get("env") || "development",
67
+ branch: rc.branch || config.get("branch") || "main",
68
+ };
69
+ };
70
+ exports.getRcConfig = getRcConfig;
@@ -0,0 +1,47 @@
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
+ const config_1 = require("./config");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ // Mock Conf module
9
+ jest.mock('conf');
10
+ describe('Config Management', () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+ describe('getConfig', () => {
15
+ it('should return config with API URL from env if set', () => {
16
+ process.env.XTRA_API_URL = 'https://api.production.com';
17
+ const config = (0, config_1.getConfig)();
18
+ expect(config.apiUrl).toBe('https://api.production.com');
19
+ delete process.env.XTRA_API_URL;
20
+ });
21
+ it('should fallback to default API URL', () => {
22
+ delete process.env.XTRA_API_URL;
23
+ const config = (0, config_1.getConfig)();
24
+ expect(config.apiUrl).toBeDefined();
25
+ });
26
+ });
27
+ describe('getProjectConfig', () => {
28
+ it('should return null if xtra.json does not exist', async () => {
29
+ jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(false);
30
+ const projectConfig = await (0, config_1.getProjectConfig)();
31
+ expect(projectConfig).toBeNull();
32
+ });
33
+ it('should parse and return xtra.json if it exists', async () => {
34
+ const mockConfig = { projectId: 'test-project', env: 'production' };
35
+ jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(true);
36
+ jest.spyOn(fs_1.default, 'readFileSync').mockReturnValue(JSON.stringify(mockConfig));
37
+ const projectConfig = await (0, config_1.getProjectConfig)();
38
+ expect(projectConfig).toEqual(mockConfig);
39
+ });
40
+ it('should return null if JSON parsing fails', async () => {
41
+ jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(true);
42
+ jest.spyOn(fs_1.default, 'readFileSync').mockReturnValue('invalid json{');
43
+ const projectConfig = await (0, config_1.getProjectConfig)();
44
+ expect(projectConfig).toBeNull();
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,50 @@
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.hash = exports.decrypt = exports.encrypt = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const conf_1 = __importDefault(require("conf"));
9
+ // We'll use the machine-id or a generated master key stored in the system keychain
10
+ // (or simple config for MVP) to encrypt the local cache.
11
+ // For now, we will generate a random key and store it in the config (simulate keychain).
12
+ const algorithm = "aes-256-gcm";
13
+ const config = new conf_1.default({ projectName: "xtra-cli-crypto" });
14
+ function getMasterKey() {
15
+ let keyHex = config.get("masterKey");
16
+ if (!keyHex) {
17
+ keyHex = crypto_1.default.randomBytes(32).toString("hex");
18
+ config.set("masterKey", keyHex);
19
+ }
20
+ return Buffer.from(keyHex, "hex");
21
+ }
22
+ const encrypt = (text) => {
23
+ const iv = crypto_1.default.randomBytes(16);
24
+ const key = getMasterKey();
25
+ const cipher = crypto_1.default.createCipheriv(algorithm, key, iv);
26
+ let encrypted = cipher.update(text, "utf8", "hex");
27
+ encrypted += cipher.final("hex");
28
+ const tag = cipher.getAuthTag();
29
+ return {
30
+ iv: iv.toString("hex"),
31
+ content: encrypted,
32
+ tag: tag.toString("hex"),
33
+ };
34
+ };
35
+ exports.encrypt = encrypt;
36
+ const decrypt = (encrypted) => {
37
+ const key = getMasterKey();
38
+ const iv = Buffer.from(encrypted.iv, "hex");
39
+ const tag = Buffer.from(encrypted.tag, "hex");
40
+ const decipher = crypto_1.default.createDecipheriv(algorithm, key, iv);
41
+ decipher.setAuthTag(tag);
42
+ let decrypted = decipher.update(encrypted.content, "hex", "utf8");
43
+ decrypted += decipher.final("utf8");
44
+ return decrypted;
45
+ };
46
+ exports.decrypt = decrypt;
47
+ const hash = (text) => {
48
+ return crypto_1.default.createHash("sha256").update(text).digest("hex");
49
+ };
50
+ exports.hash = hash;
@@ -0,0 +1,52 @@
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.MANIFEST_FILE = exports.MANIFEST_DIR = void 0;
7
+ exports.loadManifest = loadManifest;
8
+ exports.saveManifest = saveManifest;
9
+ exports.updateManifest = updateManifest;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ exports.MANIFEST_DIR = path_1.default.join(process.cwd(), ".xtra");
13
+ exports.MANIFEST_FILE = path_1.default.join(exports.MANIFEST_DIR, "manifest.json");
14
+ function loadManifest() {
15
+ if (!fs_1.default.existsSync(exports.MANIFEST_FILE)) {
16
+ return null;
17
+ }
18
+ try {
19
+ const data = fs_1.default.readFileSync(exports.MANIFEST_FILE, "utf-8");
20
+ return JSON.parse(data);
21
+ }
22
+ catch (error) {
23
+ return null;
24
+ }
25
+ }
26
+ function saveManifest(manifest) {
27
+ if (!fs_1.default.existsSync(exports.MANIFEST_DIR)) {
28
+ fs_1.default.mkdirSync(exports.MANIFEST_DIR, { recursive: true });
29
+ }
30
+ fs_1.default.writeFileSync(exports.MANIFEST_FILE, JSON.stringify(manifest, null, 2), "utf-8");
31
+ }
32
+ function updateManifest(projectId, env, secrets, hasher) {
33
+ const manifest = loadManifest() || {
34
+ projectId,
35
+ environment: env,
36
+ lastSyncedAt: new Date().toISOString(),
37
+ secrets: {}
38
+ };
39
+ // Update sync metadata
40
+ manifest.projectId = projectId;
41
+ manifest.environment = env;
42
+ manifest.lastSyncedAt = new Date().toISOString();
43
+ // Update secrets
44
+ Object.entries(secrets).forEach(([key, value]) => {
45
+ const hash = hasher(value);
46
+ manifest.secrets[key] = {
47
+ hash,
48
+ updatedAt: new Date().toISOString()
49
+ };
50
+ });
51
+ saveManifest(manifest);
52
+ }
@@ -0,0 +1,103 @@
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.listProfiles = listProfiles;
7
+ exports.getActiveProfileName = getActiveProfileName;
8
+ exports.getActiveProfile = getActiveProfile;
9
+ exports.setActiveProfile = setActiveProfile;
10
+ exports.createProfile = createProfile;
11
+ exports.deleteProfile = deleteProfile;
12
+ exports.setProfileValue = setProfileValue;
13
+ exports.getProfileValue = getProfileValue;
14
+ exports.useProfile = useProfile;
15
+ /**
16
+ * profiles.ts - Multi-Profile config management for xtra-cli
17
+ *
18
+ * Profiles are stored in the existing Conf store under the key "profiles"
19
+ * as a nested object: { profileName: { token, apiUrl, project, ... } }
20
+ *
21
+ * The active profile is tracked under "activeProfile".
22
+ */
23
+ const conf_1 = __importDefault(require("conf"));
24
+ const store = new conf_1.default({
25
+ projectName: "xtra-cli",
26
+ defaults: {
27
+ activeProfile: "default",
28
+ profiles: {
29
+ default: {
30
+ apiUrl: process.env.XTRA_API_URL || "https://xtra-security.vercel.app/api",
31
+ },
32
+ },
33
+ },
34
+ });
35
+ // ── Profile CRUD ───────────────────────────────────────────────────────────────
36
+ function listProfiles() {
37
+ return store.get("profiles") || {};
38
+ }
39
+ function getActiveProfileName() {
40
+ // Environment variable always wins
41
+ return process.env.XTRA_PROFILE || store.get("activeProfile") || "default";
42
+ }
43
+ function getActiveProfile() {
44
+ const name = getActiveProfileName();
45
+ const profiles = listProfiles();
46
+ return profiles[name] || {};
47
+ }
48
+ function setActiveProfile(name) {
49
+ const profiles = listProfiles();
50
+ if (!profiles[name]) {
51
+ throw new Error(`Profile '${name}' does not exist. Create it first with 'xtra profile create ${name}'.`);
52
+ }
53
+ store.set("activeProfile", name);
54
+ }
55
+ function createProfile(name, data = {}) {
56
+ const profiles = listProfiles();
57
+ if (profiles[name]) {
58
+ throw new Error(`Profile '${name}' already exists. Use 'xtra profile set' to edit it.`);
59
+ }
60
+ profiles[name] = {
61
+ apiUrl: process.env.XTRA_API_URL || "https://xtra-security.vercel.app/api",
62
+ ...data,
63
+ };
64
+ store.set("profiles", profiles);
65
+ }
66
+ function deleteProfile(name) {
67
+ if (name === "default") {
68
+ throw new Error("Cannot delete the 'default' profile.");
69
+ }
70
+ const profiles = listProfiles();
71
+ if (!profiles[name]) {
72
+ throw new Error(`Profile '${name}' not found.`);
73
+ }
74
+ delete profiles[name];
75
+ store.set("profiles", profiles);
76
+ // If the deleted profile was active, fall back to default
77
+ if (getActiveProfileName() === name) {
78
+ store.set("activeProfile", "default");
79
+ }
80
+ }
81
+ function setProfileValue(name, key, value) {
82
+ const profiles = listProfiles();
83
+ if (!profiles[name]) {
84
+ throw new Error(`Profile '${name}' not found.`);
85
+ }
86
+ profiles[name][key] = value;
87
+ store.set("profiles", profiles);
88
+ }
89
+ function getProfileValue(key) {
90
+ return getActiveProfile()[key];
91
+ }
92
+ /**
93
+ * Called at startup when --profile flag is passed.
94
+ * Temporarily overrides the active profile for this CLI run.
95
+ */
96
+ function useProfile(name) {
97
+ const profiles = listProfiles();
98
+ if (!profiles[name]) {
99
+ throw new Error(`Profile '${name}' does not exist. Run 'xtra profile create ${name}' first.`);
100
+ }
101
+ // Override in environment so all subsequent getActiveProfileName() calls pick it up
102
+ process.env.XTRA_PROFILE = name;
103
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "xtra-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for XtraSecurity Platform",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "xtra": "dist/bin/xtra.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/bin/xtra.js",
12
+ "dev": "ts-node src/bin/xtra.ts",
13
+ "test": "jest",
14
+ "test:watch": "jest --watch",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "engines": {
18
+ "node": ">=16.0.0"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "dependencies": {
25
+ "@types/diff": "^7.0.2",
26
+ "@types/dotenv": "^6.1.1",
27
+ "@types/table": "^6.0.0",
28
+ "@types/uuid": "^10.0.0",
29
+ "axios": "^1.6.0",
30
+ "chalk": "^4.1.2",
31
+ "commander": "^11.0.0",
32
+ "conf": "^10.2.0",
33
+ "csv-parse": "^6.1.0",
34
+ "csv-stringify": "^6.6.0",
35
+ "diff": "^8.0.3",
36
+ "dotenv": "^17.2.3",
37
+ "ink": "^6.8.0",
38
+ "ink-select-input": "^6.2.0",
39
+ "ink-text-input": "^6.0.0",
40
+ "inquirer": "^8.2.5",
41
+ "open": "^11.0.0",
42
+ "ora": "^5.4.1",
43
+ "react": "^19.2.4",
44
+ "table": "^6.9.0",
45
+ "uuid": "^13.0.0",
46
+ "yaml": "^2.8.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/babel__core": "^7.20.5",
50
+ "@types/inquirer": "^9.0.7",
51
+ "@types/jest": "^29.5.14",
52
+ "@types/node": "^20.0.0",
53
+ "@types/prettier": "^2.7.3",
54
+ "@types/react": "^19.2.14",
55
+ "jest": "^29.7.0",
56
+ "ts-jest": "^29.4.6",
57
+ "ts-node": "^10.9.1",
58
+ "typescript": "^5.0.0"
59
+ },
60
+ "keywords": [
61
+ "secrets",
62
+ "cli",
63
+ "security"
64
+ ],
65
+ "author": "XtraSecurity",
66
+ "license": "ISC"
67
+ }