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,188 @@
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.kubernetesCommand = exports.integrationCommand = 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
+ const inquirer_1 = __importDefault(require("inquirer"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ exports.integrationCommand = new commander_1.Command("integration");
14
+ exports.integrationCommand
15
+ .command("sync")
16
+ .description("Sync secrets to external integrations (GitHub)")
17
+ .option("-p, --project <id>", "Project ID (defaults to current directory config)")
18
+ .option("-e, --env <environment>", "Environment (development, staging, production)", "development")
19
+ .option("--github", "Sync to GitHub")
20
+ .option("--repo <owner/repo>", "GitHub repository (e.g., owner/repo)")
21
+ .option("--prefix <prefix>", "Prefix for secret keys")
22
+ .action(async (options) => {
23
+ try {
24
+ let projectId = options.project;
25
+ if (!projectId) {
26
+ projectId = (0, config_1.getConfigValue)("project");
27
+ if (!projectId) {
28
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
29
+ process.exit(1);
30
+ }
31
+ }
32
+ // Check if GitHub connected
33
+ const status = await api_1.api.getIntegrationStatus("github");
34
+ if (!status.connected) {
35
+ console.log(chalk_1.default.yellow("\nGitHub not connected. Please connect via the web dashboard first."));
36
+ console.log(chalk_1.default.blue("Visit: http://localhost:3000/integrations\n"));
37
+ process.exit(1);
38
+ }
39
+ console.log(chalk_1.default.green(`\nConnected as: ${status.username}`));
40
+ let repo = options.repo;
41
+ // If repo not provided, fetch list and ask user
42
+ if (!repo) {
43
+ console.log("Fetching repositories...");
44
+ const repos = await api_1.api.getIntegrationRepos("github");
45
+ const answer = await inquirer_1.default.prompt([
46
+ {
47
+ type: "list",
48
+ name: "repo",
49
+ message: "Select a repository to sync to:",
50
+ choices: repos.map((r) => ({
51
+ name: `${r.fullName} ${r.private ? "(šŸ”’)" : ""}`,
52
+ value: r.id.toString(),
53
+ short: r.fullName
54
+ }))
55
+ }
56
+ ]);
57
+ // Find selected repo details
58
+ const selectedRepo = repos.find((r) => r.id.toString() === answer.repo);
59
+ if (selectedRepo) {
60
+ repo = { owner: selectedRepo.owner, name: selectedRepo.name };
61
+ }
62
+ }
63
+ else {
64
+ // Parse repo string
65
+ const [owner, name] = repo.split("/");
66
+ if (!owner || !name) {
67
+ console.error(chalk_1.default.red("Error: Invalid repository format. Use owner/repo"));
68
+ process.exit(1);
69
+ }
70
+ repo = { owner, name };
71
+ }
72
+ console.log(chalk_1.default.blue(`\nSyncing secrets from ${options.env} to GitHub repo: ${repo.owner}/${repo.name}...`));
73
+ const result = await api_1.api.syncSecretsToGithub({
74
+ projectId,
75
+ environment: options.env,
76
+ repoOwner: repo.owner,
77
+ repoName: repo.name,
78
+ secretPrefix: options.prefix
79
+ });
80
+ console.log(chalk_1.default.green(`\nSuccessfully synced ${result.summary.synced} secrets!`));
81
+ if (result.summary.failed > 0) {
82
+ console.log(chalk_1.default.red(`${result.summary.failed} secrets failed to sync.`));
83
+ }
84
+ // Show details
85
+ result.results.forEach((r) => {
86
+ if (r.success) {
87
+ console.log(chalk_1.default.green(`āœ“ ${r.key}`));
88
+ }
89
+ else {
90
+ console.log(chalk_1.default.red(`āœ— ${r.key}: ${r.error}`));
91
+ }
92
+ });
93
+ }
94
+ catch (error) {
95
+ console.error(chalk_1.default.red("\nError syncing secrets:"), error.message || error);
96
+ process.exit(1);
97
+ }
98
+ });
99
+ exports.kubernetesCommand = new commander_1.Command("kubernetes");
100
+ exports.kubernetesCommand
101
+ .command("export")
102
+ .description("Export secrets as Kubernetes manifest")
103
+ .option("-p, --project <id>", "Project ID")
104
+ .option("-e, --env <environment>", "Environment", "development")
105
+ .option("-n, --namespace <namespace>", "Kubernetes namespace", "default")
106
+ .option("-o, --output <file>", "Output file path")
107
+ .option("--name <name>", "Secret resource name")
108
+ .action(async (options) => {
109
+ try {
110
+ let projectId = options.project;
111
+ if (!projectId) {
112
+ projectId = (0, config_1.getConfigValue)("project");
113
+ if (!projectId) {
114
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
115
+ process.exit(1);
116
+ }
117
+ }
118
+ console.log(chalk_1.default.blue(`Generating Kubernetes manifest for ${options.env}...`));
119
+ const manifest = await api_1.api.exportKubernetesSecret(projectId, {
120
+ environment: options.env,
121
+ namespace: options.namespace,
122
+ name: options.name
123
+ });
124
+ if (options.output) {
125
+ fs_1.default.writeFileSync(options.output, manifest);
126
+ console.log(chalk_1.default.green(`\nManifest saved to ${options.output}`));
127
+ }
128
+ else {
129
+ console.log("\n" + manifest);
130
+ }
131
+ }
132
+ catch (error) {
133
+ console.error(chalk_1.default.red("\nError generating manifest:"), error.message || error);
134
+ process.exit(1);
135
+ }
136
+ });
137
+ exports.kubernetesCommand
138
+ .command("apply")
139
+ .description("Apply secrets directly to Kubernetes cluster (requires kubectl)")
140
+ .option("-p, --project <id>", "Project ID")
141
+ .option("-e, --env <environment>", "Environment", "development")
142
+ .option("-n, --namespace <namespace>", "Kubernetes namespace", "default")
143
+ .action(async (options) => {
144
+ try {
145
+ // Check for kubectl
146
+ const { execSync } = require("child_process");
147
+ try {
148
+ execSync("kubectl version --client", { stdio: "ignore" });
149
+ }
150
+ catch (e) {
151
+ console.error(chalk_1.default.red("Error: kubectl not found or not configured."));
152
+ process.exit(1);
153
+ }
154
+ let projectId = options.project;
155
+ if (!projectId) {
156
+ projectId = (0, config_1.getConfigValue)("project");
157
+ if (!projectId) {
158
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
159
+ process.exit(1);
160
+ }
161
+ }
162
+ console.log(chalk_1.default.blue(`Fetching secrets for ${options.env}...`));
163
+ const manifest = await api_1.api.exportKubernetesSecret(projectId, {
164
+ environment: options.env,
165
+ namespace: options.namespace,
166
+ // No custom name, let backend generate it
167
+ });
168
+ console.log(chalk_1.default.blue("Applying to Kubernetes..."));
169
+ try {
170
+ const child = require("child_process").spawn("kubectl", ["apply", "-f", "-"]);
171
+ child.stdin.write(manifest);
172
+ child.stdin.end();
173
+ child.stdout.on("data", (data) => {
174
+ console.log(data.toString());
175
+ });
176
+ child.stderr.on("data", (data) => {
177
+ console.error(chalk_1.default.yellow(data.toString()));
178
+ });
179
+ }
180
+ catch (e) {
181
+ console.error(chalk_1.default.red("Failed to apply manifest:"), e.message);
182
+ }
183
+ }
184
+ catch (error) {
185
+ console.error(chalk_1.default.red("\nError:"), error.message || error);
186
+ process.exit(1);
187
+ }
188
+ });
@@ -0,0 +1,198 @@
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.localCommand = void 0;
40
+ /**
41
+ * local.ts — Toggle between cloud/local-only mode for offline development
42
+ *
43
+ * In "local" mode:
44
+ * - xtra run reads from .env.local instead of calling the API
45
+ * - A flag XTRA_LOCAL_MODE=true is written to the CLI config
46
+ * - All API calls are bypassed — completely offline capable
47
+ *
48
+ * Usage:
49
+ * xtra local on # Enable local mode (reads from .env.local)
50
+ * xtra local off # Disable local mode (back to cloud)
51
+ * xtra local status # Show current mode
52
+ * xtra local sync # Pull cloud secrets and write to .env.local
53
+ */
54
+ const commander_1 = require("commander");
55
+ const api_1 = require("../lib/api");
56
+ const config_1 = require("../lib/config");
57
+ const chalk_1 = __importDefault(require("chalk"));
58
+ const ora_1 = __importDefault(require("ora"));
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
61
+ const readline = __importStar(require("readline"));
62
+ const LOCAL_ENV_FILE = ".env.local";
63
+ function isLocalMode() {
64
+ return (0, config_1.getConfigValue)("localMode") === true
65
+ || process.env.XTRA_LOCAL_MODE === "true";
66
+ }
67
+ exports.localCommand = new commander_1.Command("local")
68
+ .description("Toggle cloud/local mode for offline development")
69
+ .addHelpText("after", `
70
+ In local mode, 'xtra run' reads secrets from .env.local instead of the cloud API.
71
+ This allows fully offline development without any API calls.
72
+
73
+ Examples:
74
+ $ xtra local status # Check current mode
75
+ $ xtra local on # Enable offline (local) mode
76
+ $ xtra local off # Disable — back to cloud mode
77
+ $ xtra local sync # Pull cloud secrets → .env.local
78
+ $ xtra local sync -p proj -e production # Pull production to .env.local
79
+ `);
80
+ // ── status ────────────────────────────────────────────────────────────────────
81
+ exports.localCommand
82
+ .command("status")
83
+ .description("Show current cloud/local mode")
84
+ .action(() => {
85
+ const mode = isLocalMode();
86
+ const envFilePath = path.join(process.cwd(), LOCAL_ENV_FILE);
87
+ const hasLocalFile = fs.existsSync(envFilePath);
88
+ console.log(chalk_1.default.bold("\nMode Status:\n"));
89
+ console.log(` Mode : ${mode ? chalk_1.default.yellow("šŸ”Œ LOCAL (offline)") : chalk_1.default.green("☁ CLOUD")}`);
90
+ console.log(` .env.local : ${hasLocalFile ? chalk_1.default.green("Found") : chalk_1.default.gray("Not found")}`);
91
+ console.log(` Config flag : ${chalk_1.default.gray((0, config_1.getConfigValue)("localMode") ? "true" : "false")}`);
92
+ console.log(` Env var : ${chalk_1.default.gray(process.env.XTRA_LOCAL_MODE || "(not set)")}`);
93
+ console.log();
94
+ if (!mode) {
95
+ console.log(chalk_1.default.gray(" Run 'xtra local on' to switch to offline mode."));
96
+ }
97
+ else {
98
+ console.log(chalk_1.default.gray(" Run 'xtra local off' to switch back to cloud mode."));
99
+ if (!hasLocalFile) {
100
+ console.log(chalk_1.default.yellow(" ⚠ No .env.local file found. Run 'xtra local sync' to pull secrets."));
101
+ }
102
+ }
103
+ console.log();
104
+ });
105
+ // ── on ────────────────────────────────────────────────────────────────────────
106
+ exports.localCommand
107
+ .command("on")
108
+ .description("Enable local mode — secrets read from .env.local")
109
+ .action(() => {
110
+ (0, config_1.setConfig)("localMode", true);
111
+ console.log(chalk_1.default.yellow("šŸ”Œ Local mode ENABLED."));
112
+ console.log(chalk_1.default.gray(" 'xtra run' will now read from .env.local instead of the cloud."));
113
+ const localFilePath = path.join(process.cwd(), LOCAL_ENV_FILE);
114
+ if (!fs.existsSync(localFilePath)) {
115
+ console.log(chalk_1.default.yellow(` ⚠ No .env.local file found. Run 'xtra local sync' to populate it.`));
116
+ }
117
+ });
118
+ // ── off ───────────────────────────────────────────────────────────────────────
119
+ exports.localCommand
120
+ .command("off")
121
+ .description("Disable local mode — secrets fetched from cloud again")
122
+ .action(() => {
123
+ (0, config_1.setConfig)("localMode", false);
124
+ console.log(chalk_1.default.green("☁ Cloud mode ENABLED."));
125
+ console.log(chalk_1.default.gray(" 'xtra run' will now fetch secrets from XtraSecurity Cloud."));
126
+ });
127
+ // ── sync ─────────────────────────────────────────────────────────────────────
128
+ exports.localCommand
129
+ .command("sync")
130
+ .description("Pull cloud secrets to .env.local for offline use")
131
+ .option("-p, --project <id>", "Project ID")
132
+ .option("-e, --env <environment>", "Environment", "development")
133
+ .option("-b, --branch <branch>", "Branch", "main")
134
+ .option("-o, --output <file>", "Output file", LOCAL_ENV_FILE)
135
+ .option("--overwrite", "Overwrite existing file without prompt", false)
136
+ .action(async (options) => {
137
+ let { project, env, branch, output, overwrite } = options;
138
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
139
+ env = envMap[env] || env;
140
+ if (!project)
141
+ project = (0, config_1.getConfigValue)("project");
142
+ if (!project) {
143
+ console.error(chalk_1.default.red("Error: Project ID required. Use -p <id>."));
144
+ process.exit(1);
145
+ }
146
+ const outputPath = path.resolve(process.cwd(), output);
147
+ // Warn if production
148
+ if (env === "production" && !overwrite) {
149
+ console.log(chalk_1.default.red(`⚠ You are syncing PRODUCTION secrets to ${output}.`));
150
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
151
+ const confirmed = await new Promise(resolve => {
152
+ rl.question(chalk_1.default.yellow("Type 'yes' to confirm: "), (ans) => {
153
+ rl.close();
154
+ resolve(ans.trim().toLowerCase() === "yes");
155
+ });
156
+ });
157
+ if (!confirmed) {
158
+ console.log(chalk_1.default.gray("Sync cancelled."));
159
+ return;
160
+ }
161
+ }
162
+ const spinner = (0, ora_1.default)(`Fetching ${env} secrets from cloud...`).start();
163
+ try {
164
+ const secrets = await api_1.api.getSecrets(project, env, branch);
165
+ spinner.stop();
166
+ const count = Object.keys(secrets).length;
167
+ if (count === 0) {
168
+ console.log(chalk_1.default.yellow("No secrets found."));
169
+ return;
170
+ }
171
+ // Write dotenv format
172
+ const lines = [
173
+ `# xtra local sync — ${env}/${branch}`,
174
+ `# Generated: ${new Date().toISOString()}`,
175
+ `# DO NOT COMMIT THIS FILE`,
176
+ "",
177
+ ...Object.entries(secrets).map(([k, v]) => `${k}=${v}`)
178
+ ];
179
+ fs.writeFileSync(outputPath, lines.join("\n") + "\n", "utf8");
180
+ console.log(chalk_1.default.green(`āœ… Synced ${count} secrets to ${output}`));
181
+ console.log(chalk_1.default.gray(" Run 'xtra local on' to switch to local mode."));
182
+ // Remind about .gitignore
183
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
184
+ let gitignore = "";
185
+ try {
186
+ gitignore = fs.readFileSync(gitignorePath, "utf8");
187
+ }
188
+ catch (_) { }
189
+ if (!gitignore.includes(output)) {
190
+ console.log(chalk_1.default.yellow(`\n ⚠ Remember to add '${output}' to your .gitignore!`));
191
+ }
192
+ }
193
+ catch (e) {
194
+ spinner.fail("Sync failed.");
195
+ console.error(chalk_1.default.red(e?.response?.data?.error || e.message));
196
+ process.exit(1);
197
+ }
198
+ });
@@ -0,0 +1,176 @@
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.loginCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const inquirer_1 = __importDefault(require("inquirer"));
42
+ const api_1 = require("../lib/api");
43
+ const config_1 = require("../lib/config");
44
+ const chalk_1 = __importDefault(require("chalk"));
45
+ const ora_1 = __importDefault(require("ora"));
46
+ exports.loginCommand = new commander_1.Command("login")
47
+ .description("Authenticate with XtraSecurity")
48
+ .option("-k, --key <key>", "Login using an Access Key")
49
+ .option("-e, --email <email>", "Login using Email")
50
+ .option("--sso", "Login via Web (SSO)")
51
+ .action(async (options) => {
52
+ try {
53
+ // Interactive Login Choice
54
+ if (!options.key && !options.email && !options.sso) {
55
+ const { method } = await inquirer_1.default.prompt([
56
+ {
57
+ type: "list",
58
+ name: "method",
59
+ message: "How would you like to login?",
60
+ choices: [
61
+ { name: "Browser Login (SSO)", value: "sso" },
62
+ { name: "Access Key", value: "key" },
63
+ { name: "Email & Password", value: "email" }
64
+ ],
65
+ },
66
+ ]);
67
+ if (method === "sso") {
68
+ await handleSSOLogin();
69
+ return;
70
+ }
71
+ else if (method === "key") {
72
+ const { key } = await inquirer_1.default.prompt([{ type: "password", name: "key", message: "Enter Access Key:" }]);
73
+ await handleKeyLogin(key);
74
+ return;
75
+ }
76
+ // Fall through to Email
77
+ }
78
+ if (options.sso) {
79
+ await handleSSOLogin();
80
+ }
81
+ else if (options.key) {
82
+ await handleKeyLogin(options.key);
83
+ }
84
+ else {
85
+ // Abstract Email Login
86
+ let { email } = options;
87
+ if (!email) {
88
+ const answers = await inquirer_1.default.prompt([
89
+ { type: "input", name: "email", message: "Email:" },
90
+ ]);
91
+ email = answers.email;
92
+ }
93
+ const answers = await inquirer_1.default.prompt([
94
+ { type: "password", name: "password", message: "Password:" },
95
+ ]);
96
+ const password = answers.password;
97
+ const spinner = (0, ora_1.default)("Authenticating...").start();
98
+ const data = await api_1.api.login(email, password);
99
+ (0, config_1.setConfig)("token", data.token);
100
+ spinner.succeed("Logged in successfully!");
101
+ }
102
+ }
103
+ catch (error) {
104
+ if (error.response) {
105
+ console.error(chalk_1.default.red(`\nError: ${error.response.data.error || 'Login failed'}`));
106
+ }
107
+ else {
108
+ console.error(chalk_1.default.red(`\nError: ${error.message}`));
109
+ }
110
+ process.exit(1);
111
+ }
112
+ });
113
+ async function handleKeyLogin(key) {
114
+ const spinner = (0, ora_1.default)("Authenticating with Access Key...").start();
115
+ const data = await api_1.api.login(undefined, undefined, key);
116
+ (0, config_1.setConfig)("token", data.token);
117
+ if (data.user) {
118
+ console.log(chalk_1.default.green(`\nLogged in as ${data.user.email}`));
119
+ }
120
+ spinner.succeed("Logged in successfully!");
121
+ }
122
+ async function handleSSOLogin() {
123
+ const http = require("http");
124
+ // 'open' is strictly ESM, so we must use dynamic import
125
+ const { default: open } = await Promise.resolve().then(() => __importStar(require("open")));
126
+ return new Promise((resolve) => {
127
+ const server = http.createServer(async (req, res) => {
128
+ const url = new URL(req.url, `http://${req.headers.host}`);
129
+ const token = url.searchParams.get("token");
130
+ const email = url.searchParams.get("email");
131
+ const workspaceId = url.searchParams.get("workspaceId");
132
+ const workspaceName = url.searchParams.get("workspaceName");
133
+ if (token) {
134
+ (0, config_1.setConfig)("token", token);
135
+ if (workspaceId)
136
+ (0, config_1.setConfig)("workspace", workspaceId);
137
+ res.writeHead(200, { "Content-Type": "text/html" });
138
+ res.end(`
139
+ <html>
140
+ <body style="background:#111;color:#eee;font-family:sans-serif;text-align:center;padding:50px;">
141
+ <h1>Login Successful</h1>
142
+ <p>You have successfully logged in to XtraSync.</p>
143
+ <p>You can now close this tab and return to your terminal.</p>
144
+ <script>
145
+ setTimeout(function() { window.close(); }, 1000);
146
+ </script>
147
+ </body>
148
+ </html>
149
+ `);
150
+ console.log(chalk_1.default.green(`\nāœ” SS0 Login Successful! Logged in as ${email}`));
151
+ if (workspaceId) {
152
+ console.log(chalk_1.default.dim(`Selected Workspace: ${workspaceName} (${workspaceId})`));
153
+ }
154
+ res.end(() => {
155
+ server.close();
156
+ setTimeout(() => {
157
+ process.exit(0);
158
+ }, 1000);
159
+ });
160
+ }
161
+ else {
162
+ res.writeHead(400);
163
+ res.end("Missing token");
164
+ }
165
+ });
166
+ server.listen(0, async () => {
167
+ const address = server.address();
168
+ const port = typeof address === 'string' ? 0 : address?.port;
169
+ const callbackUrl = `http://localhost:${port}`;
170
+ const { apiUrl } = require("../lib/config").getConfig();
171
+ const ssoUrl = `${apiUrl}/auth/cli/sso?callbackUrl=${encodeURIComponent(callbackUrl)}`;
172
+ console.log(chalk_1.default.blue(`Waiting for browser login... (Opening ${ssoUrl})`));
173
+ await open(ssoUrl);
174
+ });
175
+ });
176
+ }
@@ -0,0 +1,51 @@
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 login_1 = require("./login");
7
+ const api_1 = require("../lib/api");
8
+ const config_1 = require("../lib/config");
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ // Mock dependencies
11
+ jest.mock('../lib/api');
12
+ jest.mock('../lib/config');
13
+ jest.mock('inquirer');
14
+ jest.mock('ora', () => {
15
+ return jest.fn(() => ({
16
+ start: jest.fn().mockReturnThis(),
17
+ succeed: jest.fn().mockReturnThis(),
18
+ fail: jest.fn().mockReturnThis(),
19
+ }));
20
+ });
21
+ describe('Login Command', () => {
22
+ const mockApi = api_1.api;
23
+ const mockInquirer = inquirer_1.default;
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+ it('should login with API key option', async () => {
28
+ mockApi.login.mockResolvedValue({ token: 'test-token-from-key' });
29
+ // Simulate command execution with --key option
30
+ const mockOptions = { key: 'xtra_key_abc123' };
31
+ // Execute the action handler directly
32
+ await login_1.loginCommand._actionHandler(mockOptions);
33
+ expect(mockApi.login).toHaveBeenCalledWith(undefined, undefined, 'xtra_key_abc123');
34
+ expect(config_1.setConfig).toHaveBeenCalledWith('token', 'test-token-from-key');
35
+ });
36
+ it('should prompt for email and password if no options provided', async () => {
37
+ mockInquirer.prompt
38
+ .mockResolvedValueOnce({ method: 'email' })
39
+ .mockResolvedValueOnce({ email: 'test@example.com' })
40
+ .mockResolvedValueOnce({ password: 'password123' });
41
+ mockApi.login.mockResolvedValue({ token: 'email-token' });
42
+ await login_1.loginCommand._actionHandler({});
43
+ expect(mockInquirer.prompt).toHaveBeenCalledTimes(3);
44
+ expect(mockApi.login).toHaveBeenCalledWith('test@example.com', 'password123', undefined);
45
+ });
46
+ it('should handle login failure gracefully', async () => {
47
+ mockApi.login.mockRejectedValue(new Error('Invalid credentials'));
48
+ const mockOptions = { key: 'bad-key' };
49
+ await expect(login_1.loginCommand._actionHandler(mockOptions)).rejects.toThrow('Invalid credentials');
50
+ });
51
+ });