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,122 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { fetchVariables } from '../api/service.js';
5
+ export async function sync(options = {}) {
6
+ try {
7
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
8
+ if (!fs.existsSync(configPath)) {
9
+ console.error(chalk.red('Error: No local configuration found. Please link a project first.'));
10
+ process.exit(1);
11
+ }
12
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
13
+ const filePath = path.join(process.cwd(), config.envFilePath);
14
+ if (options.dryRun) {
15
+ console.log(chalk.bold(`\nDry Run: Syncing ${chalk.cyan(config.projectName)} (${chalk.yellow(config.environmentName)}) to ${chalk.dim(config.envFilePath)}\n`));
16
+ }
17
+ else {
18
+ console.log(chalk.bold(`\nSyncing ${chalk.cyan(config.projectName)} (${chalk.yellow(config.environmentName)}) to ${chalk.dim(config.envFilePath)}...`));
19
+ }
20
+ const { data: remoteVars } = await fetchVariables(config.environmentId);
21
+ const remoteMap = new Map();
22
+ remoteVars.forEach((v) => remoteMap.set(v.key, v.value));
23
+ const localMap = new Map();
24
+ if (fs.existsSync(filePath)) {
25
+ const localContent = fs.readFileSync(filePath, 'utf-8');
26
+ localContent.split('\n').forEach(line => {
27
+ const [key, ...rest] = line.split('=');
28
+ if (key && key.trim()) {
29
+ localMap.set(key.trim(), rest.join('=').trim());
30
+ }
31
+ });
32
+ }
33
+ const added = [];
34
+ const modified = [];
35
+ const removed = [];
36
+ const unchanged = [];
37
+ remoteMap.forEach((value, key) => {
38
+ if (!localMap.has(key)) {
39
+ added.push(key);
40
+ }
41
+ else if (localMap.get(key) !== value) {
42
+ modified.push(key);
43
+ }
44
+ else {
45
+ unchanged.push(key);
46
+ }
47
+ });
48
+ localMap.forEach((_, key) => {
49
+ if (!remoteMap.has(key)) {
50
+ removed.push(key);
51
+ }
52
+ });
53
+ if (options.dryRun) {
54
+ if (added.length === 0 && modified.length === 0 && removed.length === 0) {
55
+ console.log(chalk.green('No changes detected. Local file is up to date.'));
56
+ }
57
+ else {
58
+ added.forEach(key => console.log(`${chalk.green('+')} ${key}`));
59
+ modified.forEach(key => console.log(`${chalk.yellow('~')} ${key}`));
60
+ removed.forEach(key => console.log(`${chalk.red('-')} ${key}`));
61
+ console.log(`\nSummary: ${chalk.green(added.length + ' added')}, ${chalk.yellow(modified.length + ' modified')}, ${chalk.red(removed.length + ' removed')}`);
62
+ console.log(chalk.dim('\nThis was a dry run. No files were modified.'));
63
+ }
64
+ }
65
+ else {
66
+ const resolvedMap = new Map();
67
+ const { select } = await import('@inquirer/prompts');
68
+ // Process remote variables
69
+ for (const [key, remoteValue] of remoteMap) {
70
+ const localValue = localMap.get(key);
71
+ if (localValue !== undefined && localValue !== remoteValue && process.stdout.isTTY) {
72
+ console.log(chalk.yellow(`\nConflict detected for ${chalk.bold(key)}`));
73
+ console.log(`${chalk.dim(' Local: ')} ${localValue}`);
74
+ console.log(`${chalk.dim(' Remote:')} ${remoteValue}`);
75
+ const choice = await select({
76
+ message: 'Action:',
77
+ choices: [
78
+ { name: 'Use remote', value: 'remote' },
79
+ { name: 'Keep local', value: 'local' },
80
+ { name: 'Skip', value: 'skip' }
81
+ ]
82
+ });
83
+ if (choice === 'remote')
84
+ resolvedMap.set(key, remoteValue);
85
+ else if (choice === 'local')
86
+ resolvedMap.set(key, localValue);
87
+ // skip = don't include in final map
88
+ }
89
+ else {
90
+ // No conflict or non-interactive: use remote value
91
+ resolvedMap.set(key, remoteValue);
92
+ }
93
+ }
94
+ // Handle local-only variables (optional: user might want to keep them)
95
+ const localOnly = Array.from(localMap.keys()).filter(key => !remoteMap.has(key));
96
+ for (const key of localOnly) {
97
+ if (process.stdout.isTTY) {
98
+ console.log(chalk.blue(`\nLocal-only variable found: ${chalk.bold(key)}`));
99
+ const choice = await select({
100
+ message: 'Action:',
101
+ choices: [
102
+ { name: 'Remove (Sync with remote)', value: 'remove' },
103
+ { name: 'Keep local-only', value: 'keep' }
104
+ ]
105
+ });
106
+ if (choice === 'keep')
107
+ resolvedMap.set(key, localMap.get(key));
108
+ }
109
+ }
110
+ const content = Array.from(resolvedMap.entries())
111
+ .map(([key, value]) => `${key}=${value}`)
112
+ .join('\n');
113
+ fs.writeFileSync(filePath, content);
114
+ console.log(chalk.green('\n✓ Sync complete!'));
115
+ }
116
+ console.log('');
117
+ }
118
+ catch (err) {
119
+ console.error(chalk.red(`\nError: ${err.message}`));
120
+ process.exit(1);
121
+ }
122
+ }
@@ -0,0 +1,40 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ const CONFIG_DIR = path.join(os.homedir(), ".envmgr");
5
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
6
+ function readConfig() {
7
+ if (!fs.existsSync(CONFIG_FILE))
8
+ return {};
9
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
10
+ }
11
+ function writeConfig(config) {
12
+ if (!fs.existsSync(CONFIG_DIR)) {
13
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
14
+ }
15
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
16
+ }
17
+ export function saveToken(token) {
18
+ const config = readConfig();
19
+ writeConfig({ ...config, token });
20
+ }
21
+ export function getToken() {
22
+ return readConfig().token ?? null;
23
+ }
24
+ export function clearToken() {
25
+ const config = readConfig();
26
+ delete config.token;
27
+ writeConfig(config);
28
+ }
29
+ export function setApiUrl(apiUrl) {
30
+ const config = readConfig();
31
+ writeConfig({ ...config, apiUrl });
32
+ }
33
+ export function getApiUrl() {
34
+ return readConfig().apiUrl ?? null;
35
+ }
36
+ export function clearAll() {
37
+ if (fs.existsSync(CONFIG_FILE)) {
38
+ fs.unlinkSync(CONFIG_FILE);
39
+ }
40
+ }
@@ -0,0 +1,8 @@
1
+ import { getApiUrl } from "./config.js";
2
+ export function requireApiConfig() {
3
+ const apiUrl = getApiUrl() || process.env.ENVMGR_API_URL;
4
+ if (!apiUrl) {
5
+ throw new Error("API URL not configured. Please run 'configure' first.");
6
+ }
7
+ return apiUrl;
8
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_API_URL = "https://envmgr.vercel.app";
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import React from 'react';
4
+ import { render } from 'ink';
5
+ import { App } from './ui/components/App.js';
6
+ async function main() {
7
+ const [command] = process.argv.slice(2);
8
+ const args = process.argv.slice(2);
9
+ const isJson = args.includes('--json');
10
+ const isDebug = args.includes('--debug') || process.env.ENVMGR_DEBUG === '1';
11
+ const isDryRun = args.includes('--dry-run');
12
+ if (isDebug) {
13
+ process.env.ENVMGR_DEBUG = '1';
14
+ }
15
+ const uiCommands = ['login', 'configure', 'link', 'sync', 'status', 'logout', 'create-env', 'push'];
16
+ // Command mapping for convenience
17
+ let activeCommand = command;
18
+ if (command === 'env' && args[1] === 'create')
19
+ activeCommand = 'create-env';
20
+ if (command === 'var' && args[1] === 'add') {
21
+ const { importVariables } = await import('./commands/import.js');
22
+ await importVariables();
23
+ process.exit(0);
24
+ }
25
+ const isHelp = args.includes('--help') || command === 'help';
26
+ if (isHelp) {
27
+ const { help } = await import('./commands/help.js');
28
+ help();
29
+ process.exit(0);
30
+ }
31
+ if (!activeCommand || uiCommands.includes(activeCommand)) {
32
+ if (activeCommand === 'status' && (isJson || !process.stdout.isTTY)) {
33
+ const { status } = await import('./commands/status.js');
34
+ await status({ json: isJson });
35
+ }
36
+ else {
37
+ const shouldExit = !!activeCommand;
38
+ render(React.createElement(App, {
39
+ initialView: activeCommand || 'dashboard',
40
+ isDryRun,
41
+ shouldExit
42
+ }));
43
+ }
44
+ }
45
+ else if (activeCommand === 'switch') {
46
+ const { handleSwitch } = await import('./commands/switch.js');
47
+ await handleSwitch(process.argv.slice(3));
48
+ }
49
+ else if (command === 'doctor') {
50
+ const { doctor } = await import('./commands/doctor.js');
51
+ await doctor();
52
+ }
53
+ else {
54
+ console.log(`Unknown command: ${command}`);
55
+ process.exit(1);
56
+ }
57
+ }
58
+ main().catch((err) => {
59
+ console.error("Unexpected error", err);
60
+ process.exit(1);
61
+ });
@@ -0,0 +1,88 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } 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 Spinner from 'ink-spinner';
6
+ import SelectInput from 'ink-select-input';
7
+ import { Screen } from './Screen.js';
8
+ import { bulkCreateVariables } from '../../api/service.js';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ export const AddVarsFlow = ({ onCancel }) => {
12
+ const [step, setStep] = useState('mode');
13
+ const [inputContent, setInputContent] = useState('');
14
+ const [error, setError] = useState(null);
15
+ const [config, setConfig] = useState(null);
16
+ const [parsedVars, setParsedVars] = useState([]);
17
+ useEffect(() => {
18
+ try {
19
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
20
+ if (fs.existsSync(configPath)) {
21
+ setConfig(JSON.parse(fs.readFileSync(configPath, 'utf-8')));
22
+ }
23
+ }
24
+ catch (err) {
25
+ setError('Failed to load project configuration');
26
+ setStep('error');
27
+ }
28
+ }, []);
29
+ useInput((input, key) => {
30
+ if (step === 'success' || (step === 'error' && key.escape)) {
31
+ onCancel();
32
+ }
33
+ });
34
+ const parseVariables = (text) => {
35
+ const lines = text.split('\n');
36
+ const vars = [];
37
+ lines.forEach(line => {
38
+ const trimmed = line.trim();
39
+ if (!trimmed || trimmed.startsWith('#'))
40
+ return;
41
+ // Handle KEY=VALUE or KEY: VALUE
42
+ let key = '', value = '';
43
+ if (trimmed.includes('=')) {
44
+ const [k, ...v] = trimmed.split('=');
45
+ key = k.trim();
46
+ value = v.join('=').trim();
47
+ }
48
+ else if (trimmed.includes(':')) {
49
+ const [k, ...v] = trimmed.split(':');
50
+ key = k.trim();
51
+ value = v.join(':').trim();
52
+ }
53
+ if (key) {
54
+ // Basic heuristic for secrets: if key contains SECRET, KEY, PASS, TOKEN, etc.
55
+ const isSecret = /SECRET|PASSWORD|TOKEN|KEY|AUTH|CREDENTIAL|PRIVATE/i.test(key);
56
+ vars.push({ key, value, isSecret });
57
+ }
58
+ });
59
+ return vars;
60
+ };
61
+ const handleSubmit = async () => {
62
+ const vars = parseVariables(inputContent);
63
+ if (vars.length === 0) {
64
+ setError('No variables found in input. Use KEY=VALUE format.');
65
+ setStep('error');
66
+ return;
67
+ }
68
+ setParsedVars(vars);
69
+ setStep('submitting');
70
+ try {
71
+ await bulkCreateVariables(config.environmentId, vars);
72
+ setStep('success');
73
+ }
74
+ catch (err) {
75
+ setError(err.message || 'Failed to add variables');
76
+ setStep('error');
77
+ }
78
+ };
79
+ return (_jsx(Screen, { children: _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Add Variables to ", config?.environmentName] }), _jsxs(Text, { dimColor: true, children: ["Project: ", config?.projectName] }), _jsxs(Box, { marginTop: 1, children: [step === 'mode' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select input mode:" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
80
+ { label: 'Paste multiple variables (KEY=VALUE)', value: 'paste' },
81
+ { label: 'Cancel', value: 'cancel' }
82
+ ], onSelect: (item) => {
83
+ if (item.value === 'paste')
84
+ setStep('paste');
85
+ else
86
+ onCancel();
87
+ } }) })] })), step === 'paste' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Paste your variables below and press Enter twice to confirm:" }), _jsx(Box, { borderStyle: "round", borderColor: "dim", paddingX: 1, marginTop: 1, children: _jsx(TextInput, { value: inputContent, onChange: setInputContent, onSubmit: handleSubmit, placeholder: "DB_URL=postgres://...\nAPI_KEY=sk_test_..." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Note: Secret status is automatically detected for keys like API_KEY, PASSWORD, etc." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Escape to cancel" }) })] })), step === 'submitting' && (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Adding ", parsedVars.length, " variables..."] }) })), step === 'success' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", parsedVars.length, " variables added successfully!"] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [parsedVars.slice(0, 5).map((v, i) => (_jsxs(Text, { dimColor: true, children: [" + ", v.key] }, i))), parsedVars.length > 5 && _jsxs(Text, { dimColor: true, children: [" ... and ", parsedVars.length - 5, " more"] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press any key to return to menu" }) })] })), step === '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 go back" }) })] }))] })] }) }));
88
+ };
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useState } from 'react';
3
+ import { useApp } from 'ink';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { DEFAULT_API_URL } from '../../constants.js';
7
+ import { Dashboard } from './Dashboard.js';
8
+ import { LoginForm } from './LoginForm.js';
9
+ import { ConfigureForm } from './ConfigureForm.js';
10
+ import { Status } from './Status.js';
11
+ import { Logout } from './Logout.js';
12
+ import { LinkFlow } from './LinkFlow.js';
13
+ import { SyncFlow } from './SyncFlow.js';
14
+ import { CreateEnvFlow } from './CreateEnvFlow.js';
15
+ import { PushFlow } from './PushFlow.js';
16
+ export const App = ({ initialView = 'dashboard', isDryRun = false, shouldExit = false }) => {
17
+ const [view, setView] = useState(initialView);
18
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
19
+ const [isConfigured, setIsConfigured] = useState(false);
20
+ const [isLinked, setIsLinked] = useState(false);
21
+ const [localConfig, setLocalConfig] = useState(null);
22
+ const { exit } = useApp();
23
+ const handleCancel = () => {
24
+ if (shouldExit) {
25
+ exit();
26
+ }
27
+ else {
28
+ setView('dashboard');
29
+ }
30
+ };
31
+ React.useEffect(() => {
32
+ async function checkStatus() {
33
+ const { getToken, getApiUrl } = await import('../../config/config.js');
34
+ setIsLoggedIn(!!getToken());
35
+ setIsConfigured(!!getApiUrl());
36
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
37
+ let linked = false;
38
+ if (fs.existsSync(configPath)) {
39
+ try {
40
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
41
+ setLocalConfig(config);
42
+ linked = !!config.projectId;
43
+ }
44
+ catch (e) {
45
+ console.error('Failed to parse config');
46
+ }
47
+ }
48
+ setIsLinked(linked);
49
+ }
50
+ checkStatus();
51
+ }, [view]);
52
+ const handleAction = (item) => {
53
+ if (item.value === 'exit') {
54
+ exit();
55
+ return;
56
+ }
57
+ setView(item.value);
58
+ };
59
+ const handleLogin = async (email, pass) => {
60
+ const { requireApiConfig } = await import('../../config/guard.js');
61
+ const { saveToken } = await import('../../config/config.js');
62
+ const apiUrl = requireApiConfig();
63
+ const res = await fetch(`${apiUrl}/api/auth/login`, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({
67
+ email,
68
+ password: pass,
69
+ client: "cli",
70
+ }),
71
+ });
72
+ const data = await res.json();
73
+ if (!res.ok) {
74
+ if (res.status === 401) {
75
+ throw new Error('Incorrect email or password. Please try again.');
76
+ }
77
+ throw new Error(data.message || 'Login failed');
78
+ }
79
+ saveToken(data.data.token);
80
+ // Wait a bit before returning to dashboard or exiting
81
+ setTimeout(() => handleCancel(), 2000);
82
+ };
83
+ const handleConfigure = async (apiUrl) => {
84
+ const { setApiUrl } = await import('../../config/config.js');
85
+ setApiUrl(apiUrl);
86
+ // Wait a bit before returning to dashboard or exiting
87
+ setTimeout(() => handleCancel(), 2000);
88
+ };
89
+ switch (view) {
90
+ case 'login':
91
+ return _jsx(LoginForm, { onSubmit: handleLogin, onCancel: handleCancel });
92
+ case 'configure':
93
+ return (_jsx(ConfigureForm, { onSubmit: handleConfigure, onCancel: handleCancel, defaultUrl: DEFAULT_API_URL, config: localConfig, isLoggedIn: isLoggedIn, onEditProject: () => setView('link') }));
94
+ case 'status':
95
+ return _jsx(Status, { onBack: handleCancel });
96
+ case 'logout':
97
+ return _jsx(Logout, { onComplete: handleCancel });
98
+ case 'link':
99
+ return _jsx(LinkFlow, { onCancel: handleCancel, isSwitching: isLinked, onSyncRequest: () => setView('sync') });
100
+ case 'sync':
101
+ return _jsx(SyncFlow, { onCancel: handleCancel, isDryRun: isDryRun });
102
+ case 'create-env':
103
+ return _jsx(CreateEnvFlow, { onCancel: handleCancel });
104
+ case 'push':
105
+ return _jsx(PushFlow, { onCancel: handleCancel });
106
+ case 'dashboard':
107
+ default:
108
+ return _jsx(Dashboard, { onSelect: handleAction, isLoggedIn: isLoggedIn, isConfigured: isConfigured, isLinked: isLinked });
109
+ }
110
+ };
@@ -0,0 +1,80 @@
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 SelectInput from 'ink-select-input';
6
+ import { Screen } from './Screen.js';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ export const ConfigureForm = ({ onSubmit, onCancel, defaultUrl, config, isLoggedIn, onEditProject }) => {
10
+ const [step, setStep] = useState((isLoggedIn && config) ? 'choice' : 'hosted-choice');
11
+ const [apiUrl, setApiUrl] = useState('');
12
+ const [targetFile, setTargetFile] = useState(config?.envFilePath || '.env.local');
13
+ const [error, setError] = useState(null);
14
+ useInput((input, key) => {
15
+ if (step === 'success') {
16
+ onCancel();
17
+ return;
18
+ }
19
+ if (key.escape || input === '\u001b') {
20
+ onCancel();
21
+ }
22
+ });
23
+ const handleChoiceSelect = (item) => {
24
+ if (item.value === 'api')
25
+ setStep('hosted-choice');
26
+ else if (item.value === 'file')
27
+ setStep('edit-file');
28
+ else if (item.value === 'project')
29
+ onEditProject?.();
30
+ else
31
+ onCancel();
32
+ };
33
+ const handleHostedSelect = (item) => {
34
+ if (item.value === 'default') {
35
+ onSubmit(defaultUrl);
36
+ setStep('success');
37
+ }
38
+ else {
39
+ setStep('custom-url');
40
+ }
41
+ };
42
+ const handleUrlSubmit = () => {
43
+ try {
44
+ new URL(apiUrl);
45
+ onSubmit(apiUrl);
46
+ setStep('success');
47
+ }
48
+ catch {
49
+ setError('Please enter a valid URL');
50
+ }
51
+ };
52
+ const handleFileSubmit = () => {
53
+ if (!targetFile) {
54
+ setError('Please enter a filename');
55
+ return;
56
+ }
57
+ try {
58
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
59
+ const currentConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
60
+ currentConfig.envFilePath = targetFile;
61
+ fs.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
62
+ setStep('success');
63
+ }
64
+ catch (err) {
65
+ setError(err.message || 'Failed to update config');
66
+ }
67
+ };
68
+ if (step === 'success') {
69
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Configuration updated!" }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }) }));
70
+ }
71
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: "Configure EnvMgr" }), step === 'choice' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { children: "What would you like to configure?" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
72
+ { label: 'API Server URL', value: 'api' },
73
+ { label: 'Target File (.env)', value: 'file' },
74
+ { label: 'Project & Environment', value: 'project' },
75
+ { label: 'Back', value: 'back' }
76
+ ], onSelect: handleChoiceSelect }) })] })), step === 'hosted-choice' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { children: "Which EnvMgr instance are you using?" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
77
+ { label: 'Cloud (envmgr.vercel.app)', value: 'default' },
78
+ { label: 'Self-hosted Instance', value: 'custom' },
79
+ ], onSelect: handleHostedSelect }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Escape to go back" }) })] })), step === 'custom-url' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "API URL:" }), _jsx(TextInput, { value: apiUrl, onChange: setApiUrl, onSubmit: handleUrlSubmit, placeholder: "https://api.your-instance.com" })] }), error && _jsx(Text, { color: "red", children: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Escape to go back" }) })] })), step === 'edit-file' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Target File Path:" }), _jsx(TextInput, { value: targetFile, onChange: setTargetFile, onSubmit: handleFileSubmit, placeholder: ".env.local" })] }), error && _jsx(Text, { color: "red", children: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save \u2022 Escape to go back" }) })] }))] }) }));
80
+ };
@@ -0,0 +1,58 @@
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 Spinner from 'ink-spinner';
6
+ import { Screen } from './Screen.js';
7
+ import { ProjectPicker } from './ProjectPicker.js';
8
+ import { createEnvironment } from '../../api/service.js';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ export const CreateEnvFlow = ({ onCancel }) => {
12
+ const [step, setStep] = useState('project');
13
+ const [selectedProject, setSelectedProject] = useState(null);
14
+ const [envName, setEnvName] = useState('');
15
+ const [error, setError] = useState(null);
16
+ useEffect(() => {
17
+ // Check if we already have a linked project to pre-select it
18
+ try {
19
+ const configPath = path.join(process.cwd(), '.envmgr', 'config.json');
20
+ if (fs.existsSync(configPath)) {
21
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
22
+ setSelectedProject({ id: config.projectId, name: config.projectName });
23
+ setStep('name');
24
+ }
25
+ }
26
+ catch (err) {
27
+ // Ignore
28
+ }
29
+ }, []);
30
+ useInput((input, key) => {
31
+ if (step === 'success') {
32
+ onCancel();
33
+ return;
34
+ }
35
+ if (key.escape && step !== 'submitting') {
36
+ onCancel();
37
+ return;
38
+ }
39
+ // Fallback for some terminals where key.escape might not be detected
40
+ if (input === '\u001b' && step !== 'submitting') {
41
+ onCancel();
42
+ }
43
+ });
44
+ const handleCreate = async () => {
45
+ if (!selectedProject || !envName.trim())
46
+ return;
47
+ setStep('submitting');
48
+ try {
49
+ await createEnvironment(envName.trim(), selectedProject.id);
50
+ setStep('success');
51
+ }
52
+ catch (err) {
53
+ setError(err.message || 'Failed to create environment');
54
+ setStep('error');
55
+ }
56
+ };
57
+ return (_jsx(Screen, { children: _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Add New Environment" }), _jsxs(Box, { marginTop: 1, children: [step === 'project' && (_jsx(ProjectPicker, { onSelect: (p) => { setSelectedProject(p); setStep('name'); }, onCancel: onCancel })), step === 'name' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Project: ", _jsx(Text, { color: "green", children: selectedProject?.name })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Environment Name: " }), _jsx(TextInput, { value: envName, onChange: setEnvName, onSubmit: handleCreate, placeholder: "e.g. staging, testing" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press Enter to create \u2022 Esc to cancel" }) })] })), step === 'submitting' && (_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Creating environment..."] })), step === 'success' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Environment \"", envName, "\" created successfully!"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "Press any key to return to menu" }) })] })), step === '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 go back" }) })] }))] })] }) }));
58
+ };
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import { Screen } from './Screen.js';
5
+ export const Dashboard = ({ onSelect, isLoggedIn, isConfigured, isLinked }) => {
6
+ const items = [
7
+ isLoggedIn && isLinked && { label: 'Sync Variables', value: 'sync' },
8
+ isLoggedIn && isLinked && { label: 'Push Variables', value: 'push' },
9
+ isLoggedIn && isLinked && { label: 'Add Environment', value: 'create-env' },
10
+ isLoggedIn && { label: isLinked ? 'Switch Environment' : 'Link Project', value: 'link' },
11
+ isLoggedIn && { label: 'Status', value: 'status' },
12
+ !isLoggedIn && isConfigured && { label: 'Login', value: 'login' },
13
+ { label: 'Configure', value: 'configure' },
14
+ isLoggedIn && { label: 'Logout', value: 'logout' },
15
+ { label: 'Exit', value: 'exit' },
16
+ ].filter(Boolean);
17
+ return (_jsx(Screen, { children: _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Main Menu" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: items, onSelect: onSelect }) })] }) }));
18
+ };
@@ -0,0 +1,33 @@
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 SelectInput from 'ink-select-input';
5
+ import Spinner from 'ink-spinner';
6
+ import { fetchEnvironments } from '../../api/service.js';
7
+ export const EnvironmentPicker = ({ projectId, projectName, onSelect, onCancel, activeId }) => {
8
+ const [envs, setEnvs] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [error, setError] = useState(null);
11
+ useEffect(() => {
12
+ async function loadEnvs() {
13
+ try {
14
+ const res = await fetchEnvironments(projectId);
15
+ setEnvs(res.data.map((e) => ({
16
+ label: e.id === activeId ? `${e.name} (active)` : e.name,
17
+ value: e.id
18
+ })));
19
+ setLoading(false);
20
+ }
21
+ catch (err) {
22
+ setError(err.message || 'Failed to fetch environments');
23
+ setLoading(false);
24
+ }
25
+ }
26
+ loadEnvs();
27
+ }, [projectId, activeId]);
28
+ useInput((input, key) => {
29
+ if (key.escape)
30
+ onCancel();
31
+ });
32
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Select Environment for " }), _jsx(Text, { bold: true, color: "yellow", children: projectName })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", minHeight: 8, children: loading ? (_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Loading environments..."] })) : error ? (_jsxs(Text, { color: "red", children: ["Error: ", error] })) : envs.length === 0 ? (_jsx(Text, { dimColor: true, children: "No environments found" })) : (_jsx(SelectInput, { items: envs, onSelect: (item) => onSelect({ id: item.value, name: item.label }) })) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc to go back" }) })] }));
33
+ };
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const LOGO = `
4
+ ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ ██████╗ ██████╗
5
+ ██╔════╝████╗ ██║██║ ██║████╗ ████║██╔════╝ ██╔══██╗
6
+ █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║██║ ███╗██████╔╝
7
+ ██╔══╝ ██║╚██╗██║╚██╗ ██╔╝██║╚██╔╝██║██║ ██║██╔══██╗
8
+ ███████╗██║ ╚████║ ╚████╔╝ ██║ ╚═╝ ██║╚██████╔╝██║ ██║
9
+ ╚══════╝╚═╝ ╚═══╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝`;
10
+ export const Header = () => {
11
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { color: "#34B27B", bold: true, children: LOGO }) }), _jsxs(Box, { marginTop: 1, paddingX: 2, flexDirection: "row", justifyContent: "space-between", width: 65, children: [_jsx(Text, { dimColor: true, bold: true, children: "WORKSPACE CONSOLE" }), _jsx(Text, { dimColor: true, italic: true, children: "Unified Management \u2022 v1.0.0" })] }), _jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray", borderDimColor: true, marginTop: 1, width: 65 })] }));
12
+ };