envmgr-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.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # πŸš€ EnvMgr CLI Documentation
2
+
3
+ EnvMgr is a CLI tool designed to manage your environment variables across different projects and environments with ease. It provides both a beautiful interactive dashboard and powerful standalone commands.
4
+
5
+ ---
6
+
7
+ ## πŸ—οΈ Getting Started
8
+
9
+ ### 1. Configuration
10
+ Set your API server URL (default: `https://envmgr.vercel.app`).
11
+ ```bash
12
+ envmgr configure
13
+ ```
14
+
15
+ ### 2. Login
16
+ Authenticate your CLI session.
17
+ ```bash
18
+ envmgr login
19
+ ```
20
+
21
+ ### 3. Link Project
22
+ Connect your current directory to a remote EnvMgr project. This creates a `.envmgr/config.json` file to track your linkage.
23
+ ```bash
24
+ envmgr link
25
+ ```
26
+
27
+ ---
28
+
29
+ ## ⚑ Core Workflow Commands
30
+
31
+ ### πŸ”„ Syncing Variables (Remote β†’ Local)
32
+ Pull the latest variables from the remote server into your local environment file (e.g., `.env.local`).
33
+ ```bash
34
+ envmgr sync
35
+ ```
36
+ *Use `--dry-run` to see what would change without modifying files:* `envmgr sync --dry-run`
37
+
38
+ ### πŸ“€ Pushing Variables (Local β†’ Remote)
39
+ Push your local variables back to the remote server. The CLI will ask for confirmation before updating.
40
+ ```bash
41
+ envmgr push
42
+ ```
43
+
44
+ ### πŸ”€ Switch Environment
45
+ Quickly switch between different environments (e.g., staging, production) within your linked project.
46
+ ```bash
47
+ envmgr switch [alias|name]
48
+ ```
49
+
50
+ ---
51
+
52
+ ## πŸ› οΈ Management Commands
53
+
54
+ ### πŸ“ Add Variables (External Editor)
55
+ Bulk add variables by opening your default system editor (Vim, Notepad, etc.).
56
+ ```bash
57
+ envmgr var add
58
+ ```
59
+
60
+ ### βž• Create Environment
61
+ Interactively create a new environment for a project.
62
+ ```bash
63
+ envmgr env create
64
+ ```
65
+
66
+ ### πŸ“‹ System Status
67
+ View your current login status, API URL, and project linkage details.
68
+ ```bash
69
+ envmgr status
70
+ ```
71
+
72
+ ### 🩺 Doctor
73
+ Run a self-diagnostic check to troubleshoot connectivity or configuration issues.
74
+ ```bash
75
+ envmgr doctor
76
+ ```
77
+
78
+ ---
79
+
80
+ ## πŸ’‘ Pro Tips
81
+
82
+ - **Interactive Mode**: Simply run `envmgr` without any arguments to open the management dashboard.
83
+ - **JSON Output**: Use `--json` with the `status` command for machine-readable output.
84
+ - **Escape Key**: Use the `Esc` key on any interactive screen to cancel and go back.
85
+ - **Security**: The `.envmgr/` folder and `.env*` files are automatically ignored in your provided `.gitignore` to keep secrets safe.
86
+
87
+ ---
88
+
89
+ *Need more help? Run `envmgr --help` any time.*
@@ -0,0 +1,27 @@
1
+ import { getApiUrl, getToken } from "../config/config.js";
2
+ const DEFAULT_BASE_URL = "http://localhost:3000";
3
+ function resolveBaseUrl() {
4
+ return (getApiUrl() ||
5
+ process.env.ENVMGR_API_URL ||
6
+ DEFAULT_BASE_URL);
7
+ }
8
+ export async function apiFetch(path, options = {}) {
9
+ const token = getToken();
10
+ const baseUrl = resolveBaseUrl();
11
+ const headers = {
12
+ "Content-Type": "application/json",
13
+ ...options.headers,
14
+ };
15
+ if (token) {
16
+ headers.Authorization = `Bearer ${token}`;
17
+ }
18
+ const res = await fetch(`${baseUrl}${path}`, {
19
+ ...options,
20
+ headers,
21
+ });
22
+ const data = await res.json();
23
+ if (!res.ok) {
24
+ throw new Error(data.message || "Request failed");
25
+ }
26
+ return data;
27
+ }
@@ -0,0 +1,64 @@
1
+ import { getToken, getApiUrl } from '../config/config.js';
2
+ import chalk from 'chalk';
3
+ import { performance } from 'perf_hooks';
4
+ export async function fetchWithAuth(url, options = {}) {
5
+ const token = getToken();
6
+ const isDebug = process.env.ENVMGR_DEBUG === '1';
7
+ const method = options.method || 'GET';
8
+ const headers = {
9
+ 'Content-Type': 'application/json',
10
+ ...options.headers,
11
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
12
+ };
13
+ if (isDebug) {
14
+ console.log(`${chalk.blue('β†’')} ${chalk.bold(method)} ${chalk.dim(url)}`);
15
+ }
16
+ const start = performance.now();
17
+ const response = await fetch(url, { ...options, headers });
18
+ const duration = Math.round(performance.now() - start);
19
+ if (isDebug) {
20
+ const statusColor = response.ok ? chalk.green : chalk.red;
21
+ console.log(`${chalk.blue('←')} ${statusColor(response.status)} ${response.statusText} ${chalk.dim(`(${duration}ms)`)}`);
22
+ }
23
+ if (!response.ok) {
24
+ const errorData = await response.json().catch(() => ({}));
25
+ throw new Error(errorData.message || `API error: ${response.status}`);
26
+ }
27
+ return response.json();
28
+ }
29
+ export async function fetchProjects(search = '', page = 1, limit = 10) {
30
+ const apiUrl = getApiUrl();
31
+ const query = new URLSearchParams({ search, page: page.toString(), limit: limit.toString() });
32
+ return fetchWithAuth(`${apiUrl}/api/projects?${query.toString()}`);
33
+ }
34
+ export async function fetchEnvironments(projectId, search = '') {
35
+ const apiUrl = getApiUrl();
36
+ const query = new URLSearchParams({ projectId, search });
37
+ return fetchWithAuth(`${apiUrl}/api/environments?${query.toString()}`);
38
+ }
39
+ export async function fetchVariables(environmentId) {
40
+ const apiUrl = getApiUrl();
41
+ const query = new URLSearchParams({ environmentId });
42
+ return fetchWithAuth(`${apiUrl}/api/variables?${query.toString()}`);
43
+ }
44
+ export async function createEnvironment(name, projectId) {
45
+ const apiUrl = getApiUrl();
46
+ return fetchWithAuth(`${apiUrl}/api/environments`, {
47
+ method: 'POST',
48
+ body: JSON.stringify({ name, projectId })
49
+ });
50
+ }
51
+ export async function createVariable(key, value, isSecret, environmentId) {
52
+ const apiUrl = getApiUrl();
53
+ return fetchWithAuth(`${apiUrl}/api/variables`, {
54
+ method: 'POST',
55
+ body: JSON.stringify({ key, value, isSecret, environmentId })
56
+ });
57
+ }
58
+ export async function bulkCreateVariables(environmentId, variables) {
59
+ const apiUrl = getApiUrl();
60
+ return fetchWithAuth(`${apiUrl}/api/variables/bulk`, {
61
+ method: 'POST',
62
+ body: JSON.stringify({ environmentId, variables })
63
+ });
64
+ }
@@ -0,0 +1,17 @@
1
+ import chalk from "chalk";
2
+ import { setApiUrl } from "../config/config.js";
3
+ export function configSet(key, value) {
4
+ if (key !== "api-url") {
5
+ console.log(chalk.red("❌ Unknown config key"));
6
+ process.exit(1);
7
+ }
8
+ try {
9
+ new URL(value);
10
+ }
11
+ catch {
12
+ console.log(chalk.red("❌ Invalid URL"));
13
+ process.exit(1);
14
+ }
15
+ setApiUrl(value);
16
+ console.log(chalk.green(`βœ… API URL set to ${value}`));
17
+ }
@@ -0,0 +1,30 @@
1
+ import { confirm, input } from "@inquirer/prompts";
2
+ import { ui } from "../ui/index.js";
3
+ import { setApiUrl } from "../config/config.js";
4
+ import { DEFAULT_API_URL } from "../constants.js";
5
+ export async function configure() {
6
+ ui.box("EnvMgr CLI Setup", "Let’s get you connected πŸš€");
7
+ const isSelfHosted = await confirm({
8
+ message: "Is this a self-hosted EnvMgr instance?",
9
+ default: false,
10
+ });
11
+ if (!isSelfHosted) {
12
+ setApiUrl(DEFAULT_API_URL);
13
+ ui.success("Using default EnvMgr API");
14
+ return;
15
+ }
16
+ const apiUrl = await input({
17
+ message: "Enter API URL:",
18
+ validate(value) {
19
+ try {
20
+ new URL(value);
21
+ return true;
22
+ }
23
+ catch {
24
+ return "Please enter a valid URL";
25
+ }
26
+ },
27
+ });
28
+ setApiUrl(apiUrl);
29
+ ui.success("Configuration saved");
30
+ }
@@ -0,0 +1,105 @@
1
+ import chalk from 'chalk';
2
+ import figures from 'figures';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { getApiUrl, getToken } from '../config/config.js';
6
+ export async function doctor() {
7
+ console.log(chalk.bold('\nEnvMgr Diagnostics\n'));
8
+ // 1. Check API URL
9
+ const apiUrl = getApiUrl();
10
+ if (!apiUrl) {
11
+ console.log(`${chalk.red(figures.cross)} API URL not configured. Use ${chalk.cyan('envmgr configure')} to set it.`);
12
+ }
13
+ else {
14
+ try {
15
+ const res = await fetch(`${apiUrl}/api/health`, { method: 'HEAD' }).catch(() => null);
16
+ if (res && res.ok) {
17
+ console.log(`${chalk.green(figures.tick)} API reachable: ${chalk.dim(apiUrl)}`);
18
+ }
19
+ else {
20
+ console.log(`${chalk.red(figures.cross)} API not reachable at ${chalk.dim(apiUrl)}`);
21
+ }
22
+ }
23
+ catch (err) {
24
+ console.log(`${chalk.red(figures.cross)} API not reachable at ${chalk.dim(apiUrl)}`);
25
+ }
26
+ }
27
+ // 2. Check Token / Auth
28
+ const token = getToken();
29
+ if (!token) {
30
+ console.log(`${chalk.red(figures.cross)} Not authenticated. Use ${chalk.cyan('envmgr login')} to sign in.`);
31
+ }
32
+ else {
33
+ try {
34
+ const res = await fetch(`${apiUrl}/api/auth/me`, {
35
+ headers: { 'Authorization': `Bearer ${token}` }
36
+ });
37
+ if (res.ok) {
38
+ const data = await res.json();
39
+ console.log(`${chalk.green(figures.tick)} Authenticated as ${chalk.cyan(data.data?.email || 'unknown user')}`);
40
+ }
41
+ else {
42
+ console.log(`${chalk.red(figures.cross)} Token invalid or expired. Please login again.`);
43
+ }
44
+ }
45
+ catch (err) {
46
+ console.log(`${chalk.red(figures.cross)} Failed to verify authentication.`);
47
+ }
48
+ }
49
+ // 3. Check Project Link
50
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
51
+ let localConfig = null;
52
+ if (!fs.existsSync(configPath)) {
53
+ console.log(`${chalk.yellow(figures.warning)} No project linked in this directory.`);
54
+ }
55
+ else {
56
+ try {
57
+ localConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
58
+ console.log(`${chalk.green(figures.tick)} Project linked: ${chalk.cyan(localConfig.projectName)}`);
59
+ // 4. Verify Environment
60
+ if (apiUrl && token) {
61
+ try {
62
+ const res = await fetch(`${apiUrl}/api/environments?projectId=${localConfig.projectId}`, {
63
+ headers: { 'Authorization': `Bearer ${token}` }
64
+ });
65
+ if (res.ok) {
66
+ const envs = await res.json();
67
+ const exists = envs.data.some((e) => e.id === localConfig.environmentId);
68
+ if (exists) {
69
+ console.log(`${chalk.green(figures.tick)} Environment exists: ${chalk.cyan(localConfig.environmentName)}`);
70
+ }
71
+ else {
72
+ console.log(`${chalk.red(figures.cross)} Environment ${chalk.dim(localConfig.environmentName)} no longer exists or you lack access.`);
73
+ }
74
+ }
75
+ else {
76
+ console.log(`${chalk.red(figures.cross)} Failed to verify environment.`);
77
+ }
78
+ }
79
+ catch (err) {
80
+ console.log(`${chalk.red(figures.cross)} Failed to connect to API for environment verification.`);
81
+ }
82
+ }
83
+ // 5. Check Env File Writable
84
+ const envFilePath = path.join(process.cwd(), localConfig.envFilePath || '.env.local');
85
+ try {
86
+ if (fs.existsSync(envFilePath)) {
87
+ fs.accessSync(envFilePath, fs.constants.W_OK);
88
+ console.log(`${chalk.green(figures.tick)} Target file writable: ${chalk.dim(localConfig.envFilePath || '.env.local')}`);
89
+ }
90
+ else {
91
+ // Check if directory is writable to create it
92
+ fs.accessSync(path.dirname(envFilePath), fs.constants.W_OK);
93
+ console.log(`${chalk.green(figures.tick)} Target location writable (file will be created): ${chalk.dim(localConfig.envFilePath || '.env.local')}`);
94
+ }
95
+ }
96
+ catch (err) {
97
+ console.log(`${chalk.red(figures.cross)} Target file not writable: ${chalk.dim(localConfig.envFilePath || '.env.local')}`);
98
+ }
99
+ }
100
+ catch (err) {
101
+ console.log(`${chalk.red(figures.cross)} Failed to read local config at .envmgr/config.json`);
102
+ }
103
+ }
104
+ console.log('');
105
+ }
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ export function help() {
4
+ const usage = `
5
+ ${chalk.bold('Usage:')}
6
+ ${chalk.cyan('envmgr')} [command] [options]
7
+
8
+ ${chalk.bold('Commands:')}
9
+ ${chalk.green('link')} Link current directory to an EnvMgr project (Interactive)
10
+ ${chalk.green('switch')} Switch environment using name or alias (e.g., envmgr switch prod)
11
+ ${chalk.green('sync')} Pull latest variables from remote to local file
12
+ ${chalk.green('push')} Push local variables from .env file to remote environment
13
+ ${chalk.green('env create')} Add a new environment to a project
14
+ ${chalk.green('var add')} Add variables (single or bulk paste) to an environment
15
+ ${chalk.green('status')} Show current configuration and link status
16
+ ${chalk.green('doctor')} Run self-diagnostics to troubleshoot issues
17
+ ${chalk.green('configure')} Set API URL and global settings
18
+ ${chalk.green('login')} Authenticate with your EnvMgr account
19
+ ${chalk.green('logout')} Sign out and clear local credentials
20
+
21
+ ${chalk.bold('Options:')}
22
+ ${chalk.yellow('--json')} Output status in machine-readable JSON format
23
+ ${chalk.yellow('--dry-run')} Preview sync changes without modifying files
24
+ ${chalk.yellow('--debug')} Show verbose API request/response logs
25
+ ${chalk.yellow('--help')} Show this help message
26
+
27
+ ${chalk.bold('Examples:')}
28
+ ${chalk.dim('$')} envmgr link
29
+ ${chalk.dim('$')} envmgr switch prod
30
+ ${chalk.dim('$')} envmgr sync --dry-run
31
+ ${chalk.dim('$')} envmgr push
32
+ ${chalk.dim('$')} envmgr status --json
33
+ `;
34
+ console.log(boxen(usage, {
35
+ padding: 1,
36
+ margin: 1,
37
+ borderStyle: 'round',
38
+ borderColor: 'cyan',
39
+ title: 'EnvMgr CLI Help',
40
+ titleAlignment: 'center'
41
+ }));
42
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { spawnSync } from 'child_process';
5
+ import chalk from 'chalk';
6
+ import figures from 'figures';
7
+ import { bulkCreateVariables } from '../api/service.js';
8
+ export async function importVariables() {
9
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
10
+ if (!fs.existsSync(configPath)) {
11
+ console.log(`${chalk.red(figures.cross)} No project linked. Run ${chalk.cyan('envmgr link')} first.`);
12
+ process.exit(1);
13
+ }
14
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
15
+ console.log(chalk.bold(`\nImport Variables to ${chalk.cyan(config.environmentName)}`));
16
+ console.log(chalk.dim('Opening editor... Paste your variables in KEY=VALUE format, save and close.\n'));
17
+ const tmpFile = path.join(os.tmpdir(), `.envmgr-import-${Date.now()}.env`);
18
+ const initialContent = `# Paste your variables below (KEY=VALUE format)
19
+ # Lines starting with # are ignored
20
+ # Example:
21
+ # API_URL=https://api.example.com
22
+ # SECRET_KEY=12345
23
+ `;
24
+ fs.writeFileSync(tmpFile, initialContent);
25
+ const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'vi');
26
+ spawnSync(editor, [tmpFile], { stdio: 'inherit' });
27
+ const content = fs.readFileSync(tmpFile, 'utf-8');
28
+ fs.unlinkSync(tmpFile);
29
+ const lines = content.split('\n');
30
+ const variables = [];
31
+ lines.forEach(line => {
32
+ const trimmed = line.trim();
33
+ if (!trimmed || trimmed.startsWith('#'))
34
+ return;
35
+ let key = '', value = '';
36
+ if (trimmed.includes('=')) {
37
+ const [k, ...v] = trimmed.split('=');
38
+ key = k.trim();
39
+ value = v.join('=').trim();
40
+ }
41
+ else if (trimmed.includes(':')) {
42
+ const [k, ...v] = trimmed.split(':');
43
+ key = k.trim();
44
+ value = v.join(':').trim();
45
+ }
46
+ if (key) {
47
+ const isSecret = /SECRET|PASSWORD|TOKEN|KEY|AUTH|CREDENTIAL|PRIVATE/i.test(key);
48
+ variables.push({ key, value, isSecret });
49
+ }
50
+ });
51
+ if (variables.length === 0) {
52
+ console.log(`${chalk.yellow(figures.warning)} No variables found. Canceled.`);
53
+ return;
54
+ }
55
+ console.log(`${chalk.blue(figures.play)} Adding ${variables.length} variables...`);
56
+ try {
57
+ await bulkCreateVariables(config.environmentId, variables);
58
+ console.log(`${chalk.green(figures.tick)} Successfully added ${chalk.bold(variables.length)} variables!`);
59
+ console.log(`Run ${chalk.cyan('envmgr sync')} to update your local file.`);
60
+ }
61
+ catch (err) {
62
+ console.log(`${chalk.red(figures.cross)} Failed to add variables: ${err.message}`);
63
+ }
64
+ }
@@ -0,0 +1,38 @@
1
+ import { input, password } from "@inquirer/prompts";
2
+ import { ui } from "../ui/index.js";
3
+ import { saveToken } from "../config/config.js";
4
+ import { requireApiConfig } from "../config/guard.js";
5
+ export async function login() {
6
+ const apiUrl = requireApiConfig();
7
+ const email = await input({
8
+ message: "Email:",
9
+ });
10
+ const pass = await password({
11
+ message: "Password:",
12
+ mask: "β€’",
13
+ });
14
+ const spinner = ui.spinner("Logging in…").start();
15
+ try {
16
+ const res = await fetch(`${apiUrl}/api/auth/login`, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({
20
+ email,
21
+ password: pass,
22
+ client: "cli",
23
+ }),
24
+ });
25
+ const data = await res.json();
26
+ if (!res.ok) {
27
+ spinner.fail("Login failed");
28
+ ui.error(data.message);
29
+ process.exit(1);
30
+ }
31
+ saveToken(data.data.token);
32
+ spinner.succeed("Logged in successfully");
33
+ }
34
+ catch {
35
+ spinner.fail("Unable to reach server");
36
+ process.exit(1);
37
+ }
38
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { getApiUrl, getToken } from '../config/config.js';
5
+ export async function status(options = {}) {
6
+ const apiUrl = getApiUrl();
7
+ const token = getToken();
8
+ let localConfig = null;
9
+ try {
10
+ const localConfigPath = path.join(process.cwd(), '.envmgr', 'config.json');
11
+ if (fs.existsSync(localConfigPath)) {
12
+ localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf-8'));
13
+ }
14
+ }
15
+ catch (err) {
16
+ // Ignore
17
+ }
18
+ if (options.json) {
19
+ const output = {
20
+ authenticated: !!token,
21
+ apiUrl: apiUrl || null,
22
+ project: localConfig?.projectName || null,
23
+ environment: localConfig?.environmentName || null,
24
+ envFile: localConfig?.envFilePath || null
25
+ };
26
+ console.log(JSON.stringify(output, null, 2));
27
+ return;
28
+ }
29
+ // Text-based status (for non-interactive shell usage)
30
+ console.log(chalk.bold('\nEnvMgr Status\n'));
31
+ console.log(`${chalk.bold('API URL:')} ${apiUrl ? chalk.green(apiUrl) : chalk.red('Not configured')}`);
32
+ console.log(`${chalk.bold('Auth Status:')} ${token ? chalk.green('Authenticated') : chalk.red('Not logged in')}`);
33
+ if (localConfig) {
34
+ console.log(`\n${chalk.bold('Local Project Linkage')}`);
35
+ console.log(`${chalk.bold(' Project:')} ${chalk.green(localConfig.projectName)}`);
36
+ console.log(`${chalk.bold(' Environment:')} ${chalk.yellow(localConfig.environmentName)}`);
37
+ console.log(`${chalk.bold(' Linked File:')} ${chalk.dim(localConfig.envFilePath)}`);
38
+ }
39
+ else {
40
+ console.log(`\n${chalk.dim('No project linked in this directory')}`);
41
+ }
42
+ console.log('');
43
+ }
@@ -0,0 +1,59 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import figures from 'figures';
5
+ import { getApiUrl, getToken } from '../config/config.js';
6
+ import { fetchEnvironments } from '../api/service.js';
7
+ export async function handleSwitch(args) {
8
+ const target = args[0];
9
+ if (!target) {
10
+ console.log(`${chalk.red(figures.cross)} Please specify an environment name or alias.`);
11
+ console.log(`Usage: ${chalk.cyan('envmgr switch <environment|alias>')}`);
12
+ process.exit(1);
13
+ }
14
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
15
+ if (!fs.existsSync(configPath)) {
16
+ console.log(`${chalk.red(figures.cross)} No project linked in this directory. Use ${chalk.cyan('envmgr link')} first.`);
17
+ process.exit(1);
18
+ }
19
+ let config;
20
+ try {
21
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
22
+ }
23
+ catch (err) {
24
+ console.log(`${chalk.red(figures.cross)} Failed to read .envmgr/config.json`);
25
+ process.exit(1);
26
+ }
27
+ // Resolve alias
28
+ let envName = target;
29
+ if (config.envAliases && config.envAliases[target]) {
30
+ envName = config.envAliases[target];
31
+ console.log(`${chalk.dim(`Resolved alias ${chalk.cyan(target)} to ${chalk.cyan(envName)}`)}`);
32
+ }
33
+ const apiUrl = getApiUrl();
34
+ const token = getToken();
35
+ if (!apiUrl || !token) {
36
+ console.log(`${chalk.red(figures.cross)} You must be logged in and configured to switch environments.`);
37
+ process.exit(1);
38
+ }
39
+ try {
40
+ console.log(`${chalk.blue(figures.play)} Searching for environment "${envName}"...`);
41
+ const { data: envs } = await fetchEnvironments(config.projectId, envName);
42
+ const matchedEnv = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
43
+ if (!matchedEnv) {
44
+ console.log(`${chalk.red(figures.cross)} Environment "${envName}" not found in project "${config.projectName}".`);
45
+ process.exit(1);
46
+ }
47
+ // Update config
48
+ config.environmentId = matchedEnv.id;
49
+ config.environmentName = matchedEnv.name;
50
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
51
+ console.log(`${chalk.green(figures.tick)} Switched to environment: ${chalk.bold(matchedEnv.name)}`);
52
+ console.log(`${chalk.dim(`Project: ${config.projectName}`)}`);
53
+ console.log(`\nRun ${chalk.cyan('envmgr sync')} to update your local file.`);
54
+ }
55
+ catch (err) {
56
+ console.log(`${chalk.red(figures.cross)} Failed to switch environment: ${err.message}`);
57
+ process.exit(1);
58
+ }
59
+ }