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 +89 -0
- package/dist/api/client.js +27 -0
- package/dist/api/service.js +64 -0
- package/dist/commands/config.js +17 -0
- package/dist/commands/configure.js +30 -0
- package/dist/commands/doctor.js +105 -0
- package/dist/commands/help.js +42 -0
- package/dist/commands/import.js +64 -0
- package/dist/commands/login.js +38 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/switch.js +59 -0
- package/dist/commands/sync.js +122 -0
- package/dist/config/config.js +40 -0
- package/dist/config/guard.js +8 -0
- package/dist/constants.js +1 -0
- package/dist/index.js +61 -0
- package/dist/ui/components/AddVarsFlow.js +88 -0
- package/dist/ui/components/App.js +110 -0
- package/dist/ui/components/ConfigureForm.js +80 -0
- package/dist/ui/components/CreateEnvFlow.js +58 -0
- package/dist/ui/components/Dashboard.js +18 -0
- package/dist/ui/components/EnvironmentPicker.js +33 -0
- package/dist/ui/components/Header.js +12 -0
- package/dist/ui/components/LinkFlow.js +160 -0
- package/dist/ui/components/LoginForm.js +48 -0
- package/dist/ui/components/Logout.js +16 -0
- package/dist/ui/components/ProjectPicker.js +44 -0
- package/dist/ui/components/PushFlow.js +83 -0
- package/dist/ui/components/Screen.js +6 -0
- package/dist/ui/components/Status.js +36 -0
- package/dist/ui/components/SyncFlow.js +83 -0
- package/dist/ui/index.js +28 -0
- package/package.json +57 -0
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
|
+
}
|