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.
@@ -0,0 +1,160 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import SelectInput from 'ink-select-input';
6
+ import Spinner from 'ink-spinner';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { Screen } from './Screen.js';
10
+ import { ProjectPicker } from './ProjectPicker.js';
11
+ import { EnvironmentPicker } from './EnvironmentPicker.js';
12
+ import { fetchVariables } from '../../api/service.js';
13
+ export const LinkFlow = ({ onCancel, isSwitching, onSyncRequest }) => {
14
+ const [step, setStep] = useState(isSwitching ? 'environment' : 'project');
15
+ const [selectedProject, setSelectedProject] = useState(null);
16
+ const [selectedEnv, setSelectedEnv] = useState(null);
17
+ const [alias, setAlias] = useState('');
18
+ const [targetFile, setTargetFile] = useState('.env.local');
19
+ const [customFileName, setCustomFileName] = useState('');
20
+ const [error, setError] = useState(null);
21
+ const [activeConfig, setActiveConfig] = useState(null);
22
+ React.useEffect(() => {
23
+ try {
24
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
25
+ if (fs.existsSync(configPath)) {
26
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
27
+ setActiveConfig(config);
28
+ if (isSwitching) {
29
+ setSelectedProject({ id: config.projectId, name: config.projectName });
30
+ setTargetFile(config.envFilePath || '.env.local');
31
+ }
32
+ }
33
+ }
34
+ catch (err) {
35
+ console.error('Failed to load project from config', err);
36
+ }
37
+ }, [isSwitching]);
38
+ useInput((input, key) => {
39
+ if (step === 'success') {
40
+ onCancel();
41
+ return;
42
+ }
43
+ if ((key.escape || input === '\u001b') && step !== 'cloning') {
44
+ onCancel();
45
+ }
46
+ });
47
+ const handleProjectSelect = (project) => {
48
+ setSelectedProject(project);
49
+ setStep('environment');
50
+ };
51
+ const handleEnvSelect = (env) => {
52
+ setSelectedEnv(env);
53
+ if (isSwitching) {
54
+ startCloning(targetFile, true, env, selectedProject || undefined);
55
+ }
56
+ else {
57
+ setStep('ask-alias');
58
+ }
59
+ };
60
+ const handleAliasSubmit = () => {
61
+ if (fs.existsSync(path.join(process.cwd(), '.env.local'))) {
62
+ setStep('confirm-file');
63
+ }
64
+ else {
65
+ startCloning('.env.local');
66
+ }
67
+ };
68
+ const startCloning = async (fileName, silent = false, envParam, projectParam) => {
69
+ const env = envParam || selectedEnv;
70
+ const project = projectParam || selectedProject;
71
+ if (!env || !project) {
72
+ setError('Project or Environment not selected');
73
+ setStep('error');
74
+ return;
75
+ }
76
+ setTargetFile(fileName);
77
+ setStep('cloning');
78
+ try {
79
+ const configDir = path.join(process.cwd(), '.envmgr');
80
+ if (!fs.existsSync(configDir)) {
81
+ fs.mkdirSync(configDir, { recursive: true });
82
+ }
83
+ const configPath = path.join(configDir, 'config.json');
84
+ const localConfig = {
85
+ ...(activeConfig || {}),
86
+ projectId: project.id,
87
+ projectName: project.name,
88
+ environmentId: env.id,
89
+ environmentName: env.name,
90
+ envFilePath: fileName,
91
+ envAliases: {
92
+ ...(activeConfig?.envAliases || {}),
93
+ ...(alias ? { [alias]: env.name } : {})
94
+ }
95
+ };
96
+ fs.writeFileSync(configPath, JSON.stringify(localConfig, null, 2));
97
+ if (isSwitching) {
98
+ setStep('ask-sync');
99
+ }
100
+ else {
101
+ // Original clone flow
102
+ const { data: variables } = await fetchVariables(env.id);
103
+ const content = variables.map((v) => `${v.key}=${v.value}`).join('\n');
104
+ const filePath = path.join(process.cwd(), fileName);
105
+ fs.writeFileSync(filePath, content);
106
+ setStep('success');
107
+ }
108
+ }
109
+ catch (err) {
110
+ setError(err.message || 'Action failed');
111
+ setStep('error');
112
+ }
113
+ };
114
+ const renderContent = () => {
115
+ switch (step) {
116
+ case 'project':
117
+ return _jsx(ProjectPicker, { onSelect: handleProjectSelect, onCancel: onCancel, activeId: activeConfig?.projectId });
118
+ case 'environment':
119
+ if (!selectedProject)
120
+ return _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Loading project context..."] });
121
+ return _jsx(EnvironmentPicker, { projectId: selectedProject.id, projectName: selectedProject.name, onSelect: handleEnvSelect, onCancel: () => isSwitching ? onCancel() : setStep('project'), activeId: selectedProject.id === activeConfig?.projectId ? activeConfig?.environmentId : undefined });
122
+ case 'ask-alias':
123
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Environment Alias (Optional)" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Create a shortcut for ", selectedEnv?.name, "? (e.g. prod, stg)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: alias, onChange: setAlias, onSubmit: handleAliasSubmit, placeholder: "prod" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to confirm or skip \u2022 Esc to cancel" }) })] }));
124
+ case 'confirm-file':
125
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "\u26A0\uFE0F .env.local already exists." }), _jsx(Text, { children: "Overwrite it?" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
126
+ { label: 'Yes, overwrite', value: 'yes' },
127
+ { label: 'No, use a different file', value: 'no' },
128
+ { label: 'Cancel', value: 'cancel' }
129
+ ], onSelect: (item) => {
130
+ if (item.value === 'yes')
131
+ startCloning('.env.local');
132
+ else if (item.value === 'no')
133
+ setStep('custom-file');
134
+ else
135
+ onCancel();
136
+ } }) })] }));
137
+ case 'ask-sync':
138
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Environment switched to ", selectedEnv?.name, "!"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to sync variables now?" }) }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
139
+ { label: 'Yes, sync now', value: 'sync' },
140
+ { label: 'No, thanks', value: 'no' }
141
+ ], onSelect: (item) => {
142
+ if (item.value === 'sync')
143
+ onSyncRequest?.();
144
+ else
145
+ onCancel();
146
+ } }) })] }));
147
+ case 'custom-file':
148
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter filename for environment variables:" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: customFileName, onChange: setCustomFileName, onSubmit: () => startCloning(customFileName || '.env'), placeholder: ".env.staging" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to confirm \u2022 Esc to cancel" }) })] }));
149
+ case 'cloning':
150
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " ", isSwitching ? 'Updating configuration...' : `Cloning variables to ${targetFile}...`] }) }));
151
+ case 'success':
152
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Successfully linked project!" }), alias && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Alias ", _jsx(Text, { color: "cyan", bold: true, children: alias }), " created for ", _jsx(Text, { color: "yellow", children: selectedEnv?.name })] }) })), _jsxs(Box, { marginTop: alias ? 0 : 1, flexDirection: "column", alignItems: "center", children: [_jsxs(Text, { dimColor: true, children: ["Variables cloned to ", targetFile] }), _jsx(Text, { dimColor: true, children: "Config saved to .envmgr/config.json" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press any key to return to menu" }) })] }));
153
+ case 'error':
154
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [{ label: 'Retry', value: 'retry' }, { label: 'Go Back', value: 'back' }], onSelect: (item) => item.value === 'retry' ? setStep(isSwitching ? 'environment' : 'project') : onCancel() }) })] }));
155
+ default:
156
+ return null;
157
+ }
158
+ };
159
+ return (_jsx(Screen, { children: _jsx(Box, { marginTop: 1, children: renderContent() }) }));
160
+ };
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import Spinner from 'ink-spinner';
6
+ import { Screen } from './Screen.js';
7
+ export const LoginForm = ({ onSubmit, onCancel }) => {
8
+ const [step, setStep] = useState('email');
9
+ const [email, setEmail] = useState('');
10
+ const [password, setPassword] = useState('');
11
+ const [error, setError] = useState(null);
12
+ useInput((input, key) => {
13
+ if (step === 'success') {
14
+ onCancel();
15
+ return;
16
+ }
17
+ if (step === 'error' && key.return) {
18
+ setStep('password');
19
+ setPassword('');
20
+ setError(null);
21
+ return;
22
+ }
23
+ if ((key.escape || input === '\u001b') && step !== 'submitting') {
24
+ onCancel();
25
+ }
26
+ });
27
+ const handleEmailSubmit = () => {
28
+ if (email)
29
+ setStep('password');
30
+ };
31
+ const handlePasswordSubmit = async () => {
32
+ if (password) {
33
+ setStep('submitting');
34
+ try {
35
+ await onSubmit(email, password);
36
+ setStep('success');
37
+ }
38
+ catch (err) {
39
+ setError(err.message || 'Login failed');
40
+ setStep('error');
41
+ }
42
+ }
43
+ };
44
+ if (step === 'success') {
45
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Logged in successfully!" }), _jsxs(Text, { dimColor: true, children: ["Welcome back, ", email] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Returning to menu..." }) })] }) }));
46
+ }
47
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Login to your account" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { marginRight: 1, children: _jsx(Text, { bold: true, children: "Email:" }) }), step === 'email' ? (_jsx(TextInput, { value: email, onChange: setEmail, onSubmit: handleEmailSubmit })) : (_jsx(Text, { color: "gray", children: email }))] }), step !== 'email' && (_jsxs(Box, { children: [_jsx(Box, { marginRight: 1, children: _jsx(Text, { bold: true, children: "Password:" }) }), step === 'password' ? (_jsx(TextInput, { value: password, onChange: setPassword, onSubmit: handlePasswordSubmit, mask: "*" })) : (_jsx(Text, { color: "gray", children: "********" }))] })), _jsxs(Box, { marginTop: 1, children: [step === 'submitting' && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Logging in..."] }) })), step === 'error' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Press Enter to try again or Escape to cancel" })] }))] })] }) }));
48
+ };
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { Screen } from './Screen.js';
5
+ export const Logout = ({ onComplete }) => {
6
+ useEffect(() => {
7
+ async function performLogout() {
8
+ const { clearAll } = await import('../../config/config.js');
9
+ clearAll();
10
+ // Delay slightly to show the message
11
+ setTimeout(onComplete, 1500);
12
+ }
13
+ performLogout();
14
+ }, [onComplete]);
15
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Logging out..." }), _jsx(Text, { dimColor: true, children: "All configurations and tokens are being cleared." })] }) }));
16
+ };
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import SelectInput from 'ink-select-input';
6
+ import Spinner from 'ink-spinner';
7
+ import { fetchProjects } from '../../api/service.js';
8
+ export const ProjectPicker = ({ onSelect, onCancel, activeId }) => {
9
+ const [search, setSearch] = useState('');
10
+ const [projects, setProjects] = useState([]);
11
+ const [loading, setLoading] = useState(true);
12
+ const [page, setPage] = useState(1);
13
+ const [totalPages, setTotalPages] = useState(1);
14
+ const [error, setError] = useState(null);
15
+ const loadProjects = async () => {
16
+ setLoading(true);
17
+ try {
18
+ const res = await fetchProjects(search, page, 5);
19
+ setProjects(res.data.map((p) => ({
20
+ label: p.id === activeId ? `${p.name} (active)` : p.name,
21
+ value: p.id
22
+ })));
23
+ setTotalPages(res.pagination.totalPages);
24
+ setLoading(false);
25
+ }
26
+ catch (err) {
27
+ setError(err.message || 'Failed to fetch projects');
28
+ setLoading(false);
29
+ }
30
+ };
31
+ useEffect(() => {
32
+ const timer = setTimeout(loadProjects, 300);
33
+ return () => clearTimeout(timer);
34
+ }, [search, page]);
35
+ useInput((input, key) => {
36
+ if (key.escape)
37
+ onCancel();
38
+ if (key.leftArrow && page > 1)
39
+ setPage(p => p - 1);
40
+ if (key.rightArrow && page < totalPages)
41
+ setPage(p => p + 1);
42
+ });
43
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Select Project" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Search: " }), _jsx(TextInput, { value: search, onChange: (val) => { setSearch(val); setPage(1); } })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", minHeight: 8, children: loading ? (_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Loading projects..."] })) : error ? (_jsxs(Text, { color: "red", children: ["Error: ", error] })) : projects.length === 0 ? (_jsx(Text, { dimColor: true, children: "No projects found" })) : (_jsx(SelectInput, { items: projects, onSelect: (item) => onSelect({ id: item.value, name: item.label }) })) }), _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: ["Page ", page, " of ", totalPages] }), _jsx(Text, { dimColor: true, children: "\u2190 Prev \u2022 Next \u2192 \u2022 Esc to cancel" })] })] }));
44
+ };
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import SelectInput from 'ink-select-input';
6
+ import { Screen } from './Screen.js';
7
+ import { bulkCreateVariables } from '../../api/service.js';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ export const PushFlow = ({ onCancel }) => {
11
+ const [status, setStatus] = useState('reading');
12
+ const [error, setError] = useState(null);
13
+ const [config, setConfig] = useState(null);
14
+ const [variables, setVariables] = useState([]);
15
+ useEffect(() => {
16
+ async function loadAndParse() {
17
+ try {
18
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
19
+ if (!fs.existsSync(configPath)) {
20
+ throw new Error('No local configuration found. Please link a project first.');
21
+ }
22
+ const localConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
23
+ setConfig(localConfig);
24
+ const filePath = path.join(process.cwd(), localConfig.envFilePath || '.env.local');
25
+ if (!fs.existsSync(filePath)) {
26
+ throw new Error(`Local file ${localConfig.envFilePath} not found.`);
27
+ }
28
+ const content = fs.readFileSync(filePath, 'utf-8');
29
+ const lines = content.split('\n');
30
+ const parsed = [];
31
+ lines.forEach(line => {
32
+ const trimmed = line.trim();
33
+ if (!trimmed || trimmed.startsWith('#'))
34
+ return;
35
+ if (trimmed.includes('=')) {
36
+ const [k, ...v] = trimmed.split('=');
37
+ const key = k.trim();
38
+ const value = v.join('=').trim();
39
+ if (key) {
40
+ const isSecret = /SECRET|PASSWORD|TOKEN|KEY|AUTH|CREDENTIAL|PRIVATE/i.test(key);
41
+ parsed.push({ key, value, isSecret });
42
+ }
43
+ }
44
+ });
45
+ if (parsed.length === 0) {
46
+ throw new Error('No variables found in local file.');
47
+ }
48
+ setVariables(parsed);
49
+ setStatus('confirm');
50
+ }
51
+ catch (err) {
52
+ setError(err.message || 'Failed to read local variables');
53
+ setStatus('error');
54
+ }
55
+ }
56
+ loadAndParse();
57
+ }, []);
58
+ useInput((input, key) => {
59
+ if (status === 'success' || (status === 'error' && key.escape)) {
60
+ onCancel();
61
+ }
62
+ });
63
+ const handleConfirm = async () => {
64
+ setStatus('pushing');
65
+ try {
66
+ await bulkCreateVariables(config.environmentId, variables);
67
+ setStatus('success');
68
+ }
69
+ catch (err) {
70
+ setError(err.message || 'Push failed');
71
+ setStatus('error');
72
+ }
73
+ };
74
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Push Local Variables" }), config && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Project: ", _jsx(Text, { color: "green", children: config.projectName })] }), _jsxs(Text, { dimColor: true, children: ["Environment: ", _jsx(Text, { color: "yellow", children: config.environmentName })] }), _jsxs(Text, { dimColor: true, children: ["Source File: ", _jsx(Text, { italic: true, children: config.envFilePath })] })] })), _jsxs(Box, { marginTop: 1, children: [status === 'reading' && _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Reading local file..."] }), status === 'confirm' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Found ", _jsx(Text, { bold: true, color: "white", children: variables.length }), " variables in your local file."] }), _jsx(Text, { color: "yellow", children: "\u26A0\uFE0F This will update/create these variables on the remote server." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "Are you sure you want to push?" }) }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
75
+ { label: 'Yes, push to remote', value: 'yes' },
76
+ { label: 'No, cancel', value: 'no' }
77
+ ], onSelect: (item) => {
78
+ if (item.value === 'yes')
79
+ handleConfirm();
80
+ else
81
+ onCancel();
82
+ } }) })] })), status === 'pushing' && (_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Uploading ", variables.length, " variables to remote..."] })), status === 'success' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Successfully pushed ", variables.length, " variables!"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press any key to return to menu" }) })] })), status === 'error' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press Escape to return to menu" }) })] }))] })] }) }));
83
+ };
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Header } from './Header.js';
4
+ export const Screen = ({ children }) => {
5
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: 15, children: [_jsx(Header, {}), _jsx(Box, { flexGrow: 1, flexDirection: "row", borderStyle: "bold", borderLeft: true, borderRight: false, borderTop: false, borderBottom: false, borderColor: "cyan", paddingLeft: 1, marginLeft: 1, children: _jsx(Box, { flexGrow: 1, flexDirection: "column", children: children }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray" }), _jsxs(Box, { paddingX: 1, width: "100%", children: [_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: "gray", children: "Selection: Arrow Keys \u2022 Confirm: Enter \u2022 Quit: Ctrl+C" }) }), _jsx(Box, { children: _jsx(Text, { color: "cyan", bold: true, children: "EnvMgr v1.0.0" }) })] })] })] }));
6
+ };
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { Screen } from './Screen.js';
7
+ export const Status = ({ onBack }) => {
8
+ const [config, setConfig] = useState({ apiUrl: null, token: null, local: null });
9
+ useInput((input, key) => {
10
+ if (key.return || key.escape) {
11
+ onBack();
12
+ }
13
+ });
14
+ useEffect(() => {
15
+ async function loadConfig() {
16
+ const { getApiUrl, getToken } = await import('../../config/config.js');
17
+ let local = null;
18
+ try {
19
+ const localConfigPath = path.join(process.cwd(), '.envmgr', 'config.json');
20
+ if (fs.existsSync(localConfigPath)) {
21
+ local = JSON.parse(fs.readFileSync(localConfigPath, 'utf-8'));
22
+ }
23
+ }
24
+ catch (err) {
25
+ // Ignore errors reading local config
26
+ }
27
+ setConfig({
28
+ apiUrl: getApiUrl(),
29
+ token: getToken(),
30
+ local
31
+ });
32
+ }
33
+ loadConfig();
34
+ }, []);
35
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "System Status" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: 15, children: _jsx(Text, { bold: true, children: "API URL:" }) }), _jsx(Text, { color: config.apiUrl ? "green" : "red", children: config.apiUrl || "Not configured" })] }), _jsxs(Box, { children: [_jsx(Box, { width: 15, children: _jsx(Text, { bold: true, children: "Auth Status:" }) }), _jsx(Text, { color: config.token ? "green" : "red", children: config.token ? "Authenticated" : "Not logged in" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Local Project Linkage" }), config.local && config.local.projectName ? (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Box, { width: 13, children: _jsx(Text, { bold: true, children: "Project:" }) }), _jsx(Text, { color: "green", children: config.local.projectName })] }), _jsxs(Box, { children: [_jsx(Box, { width: 13, children: _jsx(Text, { bold: true, children: "Environment:" }) }), _jsx(Text, { color: "yellow", children: config.local.environmentName })] }), _jsxs(Box, { children: [_jsx(Box, { width: 13, children: _jsx(Text, { bold: true, children: "Linked File:" }) }), _jsx(Text, { dimColor: true, children: config.local.envFilePath })] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: "No project linked in this directory" }) }))] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: "Press Enter to return to menu" }) })] }) }));
36
+ };
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { Screen } from './Screen.js';
8
+ import { fetchVariables } from '../../api/service.js';
9
+ export const SyncFlow = ({ onCancel, isDryRun = false }) => {
10
+ const [status, setStatus] = useState('reading');
11
+ const [error, setError] = useState(null);
12
+ const [details, setDetails] = useState(null);
13
+ const [diffs, setDiffs] = useState([]);
14
+ useInput((input, key) => {
15
+ if (status === 'success') {
16
+ onCancel();
17
+ return;
18
+ }
19
+ if ((key.escape || input === '\u001b') && status !== 'syncing') {
20
+ onCancel();
21
+ }
22
+ });
23
+ useEffect(() => {
24
+ async function performSync() {
25
+ try {
26
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
27
+ if (!fs.existsSync(configPath)) {
28
+ throw new Error('No local configuration found. Please link a project first.');
29
+ }
30
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
31
+ setDetails({
32
+ project: config.projectName,
33
+ env: config.environmentName,
34
+ file: config.envFilePath
35
+ });
36
+ setStatus('syncing');
37
+ const { data: variables } = await fetchVariables(config.environmentId);
38
+ const filePath = path.join(process.cwd(), config.envFilePath);
39
+ if (isDryRun) {
40
+ const localVars = {};
41
+ if (fs.existsSync(filePath)) {
42
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
43
+ fileContent.split('\n').forEach(line => {
44
+ const [key, ...val] = line.split('=');
45
+ if (key && key.trim()) {
46
+ localVars[key.trim()] = val.join('=').trim();
47
+ }
48
+ });
49
+ }
50
+ const calculatedDiffs = [];
51
+ const remoteKeys = new Set(variables.map((v) => v.key));
52
+ // Additions and Updates
53
+ variables.forEach((v) => {
54
+ if (!(v.key in localVars)) {
55
+ calculatedDiffs.push({ key: v.key, action: 'add', newValue: v.value });
56
+ }
57
+ else if (localVars[v.key] !== v.value) {
58
+ calculatedDiffs.push({ key: v.key, action: 'update', oldValue: localVars[v.key], newValue: v.value });
59
+ }
60
+ });
61
+ // Removals
62
+ Object.keys(localVars).forEach(key => {
63
+ if (!remoteKeys.has(key)) {
64
+ calculatedDiffs.push({ key, action: 'remove', oldValue: localVars[key] });
65
+ }
66
+ });
67
+ setDiffs(calculatedDiffs);
68
+ }
69
+ else {
70
+ const content = variables.map((v) => `${v.key}=${v.value}`).join('\n');
71
+ fs.writeFileSync(filePath, content);
72
+ }
73
+ setStatus('success');
74
+ }
75
+ catch (err) {
76
+ setError(err.message || 'Sync failed');
77
+ setStatus('error');
78
+ }
79
+ }
80
+ performSync();
81
+ }, []);
82
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Synchronizing Variables" }), details && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Project: ", _jsx(Text, { color: "green", children: details.project })] }), _jsxs(Text, { dimColor: true, children: ["Environment: ", _jsx(Text, { color: "yellow", children: details.env })] }), _jsxs(Text, { dimColor: true, children: ["Target: ", _jsx(Text, { italic: true, children: details.file })] })] })), _jsxs(Box, { marginTop: 1, children: [status === 'reading' && _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Reading local configuration..."] }), status === 'syncing' && _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Fetching remote variables and updating file..."] }), status === 'success' && (_jsxs(Box, { flexDirection: "column", children: [isDryRun ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Dry Run Results (No files were changed):" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: diffs.length === 0 ? (_jsx(Text, { dimColor: true, italic: true, children: "No changes detected. Local file is in sync." })) : (diffs.map((d, i) => (_jsxs(Box, { children: [_jsxs(Box, { width: 2, children: [d.action === 'add' && _jsx(Text, { color: "green", children: "+" }), d.action === 'update' && _jsx(Text, { color: "yellow", children: "~" }), d.action === 'remove' && _jsx(Text, { color: "red", children: "-" })] }), _jsx(Text, { bold: true, children: d.key }), d.action === 'update' && (_jsx(Text, { dimColor: true, children: " (changed)" }))] }, i)))) })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Sync complete!" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Your local file has been updated with the latest remote values." }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press any key to return to menu" }) })] })), status === 'error' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Escape to return to menu" }) })] }))] })] }) }));
83
+ };
@@ -0,0 +1,28 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import figures from "figures";
4
+ import ora from "ora";
5
+ export const ui = {
6
+ spinner(text) {
7
+ return ora({
8
+ text,
9
+ spinner: "dots",
10
+ });
11
+ },
12
+ success(text) {
13
+ console.log(chalk.green(`${figures.tick} ${text}`));
14
+ },
15
+ error(text) {
16
+ console.log(chalk.red(`${figures.cross} ${text}`));
17
+ },
18
+ info(text) {
19
+ console.log(chalk.cyan(`${figures.info} ${text}`));
20
+ },
21
+ box(title, body) {
22
+ console.log(boxen(`${chalk.bold(title)}\n\n${body}`, {
23
+ padding: 1,
24
+ borderStyle: "round",
25
+ borderColor: "cyan",
26
+ }));
27
+ },
28
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "envmgr-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for managing environment variables across projects and environments",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "envmgr": "./dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "dev": "tsx src/index.ts",
16
+ "interactive": "tsx src/index.ts",
17
+ "build": "tsc",
18
+ "prepare": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "env",
22
+ "environment",
23
+ "cli",
24
+ "variables",
25
+ "envmgr",
26
+ "secrets"
27
+ ],
28
+ "author": "Mrudul Kolambe",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@inquirer/prompts": "^8.2.1",
32
+ "@types/react": "^19.2.14",
33
+ "@types/react-dom": "^19.2.3",
34
+ "axios": "^1.13.5",
35
+ "boxen": "^8.0.1",
36
+ "chalk": "^5.6.2",
37
+ "cli-table3": "^0.6.5",
38
+ "commander": "^14.0.3",
39
+ "dotenv": "^17.3.1",
40
+ "figures": "^6.1.0",
41
+ "ink": "^6.8.0",
42
+ "ink-box": "^1.0.0",
43
+ "ink-gradient": "^4.0.0",
44
+ "ink-select-input": "^6.2.0",
45
+ "ink-spinner": "^5.0.0",
46
+ "ink-text-input": "^6.0.0",
47
+ "inquirer": "^13.2.5",
48
+ "ora": "^9.3.0",
49
+ "react": "^19.2.4"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^25.3.0",
53
+ "ts-node": "^10.9.2",
54
+ "tsx": "^4.21.0",
55
+ "typescript": "^5.9.3"
56
+ }
57
+ }