Skip to content

Commit 2289c44

Browse files
committed
fix: comprehensive bug fixes and improvements
- Fix version: read from package.json instead of hardcoding '1.0.0' - Fix ConfigParser: properly handle string/undefined/null env values - Add auth: push/pull support Bearer token via devConfig.apiKey or ENVX_API_KEY - Add DB error handling: wrap constructor and initDatabase in try-catch - Add DB schema versioning: schema_version table with migration support - Fix env key validation: allow hyphens consistently (align with ConfigValidator) - Improve DevConfig defaults: populate from env vars when config missing - Remove unused Logger class (dead code) - Remove duplicate detectDefaultShell/detectInteractiveShellProgram in export.ts - Add --limit flag to history command
1 parent ad6189c commit 2289c44

11 files changed

Lines changed: 131 additions & 130 deletions

File tree

src/commands/export.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { spawn } from 'child_process';
44
import { join } from 'path';
55
import { existsSync } from 'fs';
66
import { getEnvs } from '@/utils/com';
7+
import { detectDefaultShell, detectInteractiveShellProgram, generateExportCommand } from '@/utils/env';
78

89
type ShellKind = 'sh' | 'cmd' | 'powershell';
910

@@ -16,32 +17,6 @@ interface ExportOptions {
1617
config?: string;
1718
}
1819

19-
function serializeLine(key: string, value: string, shell: ShellKind): string {
20-
const escaped = value.replace(/"/g, '\\"');
21-
if (shell === 'cmd') return `set ${key}="${escaped}"`;
22-
if (shell === 'powershell') return `$Env:${key} = "${escaped}"`;
23-
return `export ${key}="${escaped}"`;
24-
}
25-
26-
function detectDefaultShell(): ShellKind {
27-
if (process.platform === 'win32') {
28-
return 'powershell';
29-
}
30-
return 'sh';
31-
}
32-
33-
function detectInteractiveShellProgram(shell: ShellKind): { program: string; args: string[] } {
34-
if (process.platform === 'win32') {
35-
if (shell === 'powershell') return { program: 'powershell.exe', args: ['-NoExit'] };
36-
if (shell === 'cmd') return { program: 'cmd.exe', args: ['/K'] };
37-
// Fallback to PowerShell
38-
return { program: 'powershell.exe', args: ['-NoExit'] };
39-
}
40-
// POSIX
41-
const userShell = process.env.SHELL || '/bin/sh';
42-
return { program: userShell, args: ['-i'] };
43-
}
44-
4520
export function exportCommand(program: Command): void {
4621
program
4722
.command('export [tag]')
@@ -85,7 +60,7 @@ export function exportCommand(program: Command): void {
8560

8661
// Only print if explicitly requested
8762
if (options.print) {
88-
const lines = Object.entries(envMap).map(([k, v]) => serializeLine(k, v, shell));
63+
const lines = Object.entries(envMap).map(([k, v]) => generateExportCommand(k, v, shell));
8964
const output = lines.join('\n');
9065
if (options.verbose) {
9166
console.log(chalk.gray('\n# Commands to set variables in your current shell'));

src/commands/history.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface HistoryOptions {
1010
format?: 'table' | 'json';
1111
verbose?: boolean;
1212
list?: boolean;
13+
limit?: string;
1314
}
1415

1516
export function historyCommand(program: Command): void {
@@ -25,6 +26,7 @@ export function historyCommand(program: Command): void {
2526
.option('-f, --format <format>', 'Output format: table | json (default: table)', 'table')
2627
.option('-v, --verbose', 'Show detailed information including full values')
2728
.option('-l, --list', 'List all available tags')
29+
.option('-n, --limit <number>', 'Maximum number of records to show')
2830
.action(async (tag: string | undefined, options: HistoryOptions) => {
2931
try {
3032
const configPath = join(process.cwd(), options.config || './envx.config.yaml');
@@ -130,26 +132,21 @@ export function historyCommand(program: Command): void {
130132
}
131133

132134
let records: EnvHistoryRecord[] = [];
135+
const limit = options.limit ? parseInt(options.limit, 10) : undefined;
133136

134-
// 检查是否有任何过滤条件
135-
const hasFilters = options.key;
136-
137-
if (hasFilters) {
138-
// 有过滤条件时,获取过滤后的记录
139-
if (options.key) {
140-
console.log(chalk.gray(`🔍 Filtering by key: ${options.key} in tag: ${tag}`));
141-
const tagRecords = dbManager.getHistoryByTag(tag);
142-
records = tagRecords.filter(record => record.key === options.key);
143-
} else {
144-
console.log(chalk.gray(`🔍 Filtering by tag: ${tag}`));
145-
records = dbManager.getHistoryByTag(tag);
146-
}
137+
if (options.key) {
138+
console.log(chalk.gray(`🔍 Filtering by key: ${options.key} in tag: ${tag}`));
139+
const tagRecords = dbManager.getHistoryByTag(tag);
140+
records = tagRecords.filter(record => record.key === options.key);
147141
} else {
148-
// 没有过滤条件时,获取指定 tag 的所有记录
149142
console.log(chalk.gray(`🔍 Getting all records for tag: ${tag}`));
150143
records = dbManager.getHistoryByTag(tag);
151144
}
152145

146+
if (limit && limit > 0) {
147+
records = records.slice(0, limit);
148+
}
149+
153150
if (records.length === 0) {
154151
console.log(chalk.yellow('📭 No history records found'));
155152
if (options.key) {

src/commands/pull.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,17 @@ export function pullCommand(program: Command): void {
129129
throw new Error('fetch is not available in this Node.js runtime. Please use Node 18+');
130130
}
131131

132+
const headers: Record<string, string> = {
133+
'Content-Type': 'application/json',
134+
};
135+
const apiKey = devConfigResult.config.apiKey || process.env.ENVX_API_KEY;
136+
if (apiKey) {
137+
headers['Authorization'] = `Bearer ${apiKey}`;
138+
}
139+
132140
const response = await fetchFn(fullUrl, {
133141
method: 'GET',
134-
headers: {
135-
'Content-Type': 'application/json',
136-
},
142+
headers,
137143
});
138144

139145
const responseData = (await response.json()) as {

src/commands/push.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,17 @@ export function pushCommand(program: Command): void {
108108
throw new Error('fetch is not available in this Node.js runtime. Please use Node 18+');
109109
}
110110

111+
const headers: Record<string, string> = {
112+
'Content-Type': 'application/json',
113+
};
114+
const apiKey = devConfigResult.config.apiKey || process.env.ENVX_API_KEY;
115+
if (apiKey) {
116+
headers['Authorization'] = `Bearer ${apiKey}`;
117+
}
118+
111119
const response = await fetchFn(remoteUrl, {
112120
method: 'POST',
113-
headers: {
114-
'Content-Type': 'application/json',
115-
},
121+
headers,
116122
body: JSON.stringify(payload)
117123
});
118124

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Command } from 'commander';
44
import chalk from 'chalk';
5+
import { createRequire } from 'module';
56
import { versionCommand } from './commands/version.js';
67
import { initCommand } from './commands/init.js';
78
import { exportCommand } from './commands/export.js';
@@ -14,13 +15,16 @@ import { tagCommand } from './commands/tag.js';
1415
import { pushCommand } from './commands/push.js';
1516
import { pullCommand } from './commands/pull.js';
1617

18+
const require = createRequire(import.meta.url);
19+
const { version } = require('../package.json');
20+
1721
const program = new Command();
1822

1923
// 设置基本信息
2024
program
2125
.name('envx')
2226
.description(chalk.blue('A powerful environment management CLI tool'))
23-
.version('1.0.0');
27+
.version(version);
2428

2529
// 添加命令
2630
versionCommand(program);

src/types/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface DevConfig {
3535
baseUrl?: string;
3636
namespace?: string;
3737
project?: string;
38+
apiKey?: string;
3839
}
3940

4041
export interface DevConfigParseResult {

src/utils/config/config-manager.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -169,26 +169,14 @@ export class ConfigManager {
169169
* 获取环境变量
170170
*/
171171
getEnvVar(key: string): EnvTarget | EnvConfig | string {
172-
const value = ConfigParser.getEnvVar(this.config, key);
172+
const config = ConfigParser.getEnvVar(this.config, key);
173173

174-
if (value === undefined) {
175-
// 如果值为 undefined,返回 key 本身
176-
return key;
177-
} else if (typeof value === 'object' && value !== null) {
178-
const config = value as EnvConfig;
179-
if (config.default !== undefined) {
180-
// 如果有默认值,返回默认值
181-
return config.default;
182-
} else if (config.target) {
183-
// 如果有 target,返回 target
184-
return config.target;
185-
} else {
186-
// 如果没有 target 且没有默认值,返回 key 本身
187-
return key;
188-
}
174+
if (config.default !== undefined) {
175+
return config.default;
176+
} else if (config.target) {
177+
return config.target;
189178
} else {
190-
// 如果是简单值,直接返回
191-
return value;
179+
return key;
192180
}
193181
}
194182

@@ -282,9 +270,15 @@ export class ConfigManager {
282270

283271
/**
284272
* 获取 dev 默认配置
273+
* 从环境变量中尝试填充缺省值
285274
*/
286275
getDefaultDevConfig(): DevConfig {
287-
return {};
276+
return {
277+
baseUrl: process.env.ENVX_BASEURL,
278+
namespace: process.env.ENVX_NAMESPACE,
279+
project: process.env.ENVX_PROJECT,
280+
apiKey: process.env.ENVX_API_KEY,
281+
};
288282
}
289283

290284
/**

src/utils/config/config-parser.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export class ConfigParser {
163163
const result: Array<{ key: string; config: EnvConfig }> = [];
164164

165165
for (const [key, value] of Object.entries(config.env)) {
166-
result.push({ key, config: this.getEnvVar(value as EnvConfig, key) });
166+
result.push({ key, config: this.normalizeEnvValue(value, key) });
167167
}
168168

169169
return result;
@@ -177,32 +177,45 @@ export class ConfigParser {
177177
}
178178

179179
/**
180-
* 获取环境变量配置
180+
* 将 env 配置项的各种格式(string / EnvConfig / undefined / null)
181+
* 统一转换为 EnvConfig 对象
181182
*/
182-
static getEnvVar(config: EnvConfig, key: string): EnvConfig {
183-
if (config === null) {
183+
static normalizeEnvValue(value: EnvTarget | EnvConfig | undefined | null, key: string): EnvConfig {
184+
if (value === null || value === undefined) {
184185
return this.getDefaultEnvConfig(key);
185186
}
187+
if (typeof value === 'string') {
188+
// 简写格式: KEY: ".env" — string 作为 target
189+
return { ...this.getDefaultEnvConfig(key), target: value };
190+
}
191+
// 完整对象格式
186192
return {
187-
target: config.target || key,
188-
files: config.files || undefined,
189-
default: config.default || undefined,
190-
description: config.description || undefined,
191-
required: config.required || false,
193+
target: value.target || key,
194+
files: value.files || undefined,
195+
default: value.default || undefined,
196+
description: value.description || undefined,
197+
required: value.required || false,
192198
};
193199
}
194200

201+
/**
202+
* 获取环境变量配置(兼容旧调用)
203+
*/
204+
static getEnvVar(config: EnvxConfig, key: string): EnvConfig {
205+
return this.normalizeEnvValue(config.env[key], key);
206+
}
207+
195208
/**
196209
* 获取环境变量的目标源
197210
*/
198211
static getEnvVarTarget(config: EnvxConfig, key: string): string | undefined {
199-
return this.getEnvVar(config.env[key] as EnvConfig, key).target;
212+
return this.getEnvVar(config, key).target;
200213
}
201214

202215
/**
203216
* 获取环境变量的默认值
204217
*/
205218
static getEnvVarDefault(config: EnvxConfig, key: string): string | undefined {
206-
return this.getEnvVar(config.env[key] as EnvConfig, key).default;
219+
return this.getEnvVar(config, key).default;
207220
}
208221
}

src/utils/db.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,72 @@ export class DatabaseManager {
1818
constructor(configDir: string) {
1919
// 确保 .envx 目录存在
2020
const envxDir = join(configDir, '.envx');
21-
if (!existsSync(envxDir)) {
22-
mkdirSync(envxDir, { recursive: true });
21+
try {
22+
if (!existsSync(envxDir)) {
23+
mkdirSync(envxDir, { recursive: true });
24+
}
25+
} catch (err) {
26+
throw new Error(`Failed to create .envx directory at ${envxDir}: ${err instanceof Error ? err.message : String(err)}`);
2327
}
2428

2529
this.dbPath = join(envxDir, 'envx.db');
26-
this.db = new Database(this.dbPath);
30+
try {
31+
this.db = new Database(this.dbPath);
32+
} catch (err) {
33+
throw new Error(`Failed to open database at ${this.dbPath}: ${err instanceof Error ? err.message : String(err)}`);
34+
}
35+
2736
this.initDatabase();
2837
}
2938

39+
private static readonly SCHEMA_VERSION = 2;
40+
3041
/**
3142
* 初始化数据库表结构
3243
*/
3344
private initDatabase(): void {
34-
// 创建环境变量历史记录表
35-
this.db.exec(`
36-
CREATE TABLE IF NOT EXISTS env_history (
37-
id INTEGER PRIMARY KEY AUTOINCREMENT,
38-
key TEXT NOT NULL,
39-
value TEXT NOT NULL,
40-
timestamp TEXT NOT NULL,
41-
tag TEXT NOT NULL
42-
)
43-
`);
45+
try {
46+
this.db.exec(`
47+
CREATE TABLE IF NOT EXISTS schema_version (
48+
version INTEGER NOT NULL
49+
)
50+
`);
4451

45-
// 创建索引以提高查询性能
46-
this.db.exec(`
47-
CREATE INDEX IF NOT EXISTS idx_env_history_key ON env_history(key);
48-
CREATE INDEX IF NOT EXISTS idx_env_history_timestamp ON env_history(timestamp);
49-
CREATE INDEX IF NOT EXISTS idx_env_history_tag ON env_history(tag);
50-
-- 为实现基于 (key, tag) 的 UPSERT,增加唯一索引
51-
CREATE UNIQUE INDEX IF NOT EXISTS idx_env_history_key_tag_unique ON env_history(key, tag);
52-
`);
52+
const row = this.db.prepare('SELECT version FROM schema_version LIMIT 1').get() as { version: number } | undefined;
53+
const currentVersion = row?.version ?? 0;
54+
55+
if (currentVersion < 1) {
56+
// Initial schema
57+
this.db.exec(`
58+
CREATE TABLE IF NOT EXISTS env_history (
59+
id INTEGER PRIMARY KEY AUTOINCREMENT,
60+
key TEXT NOT NULL,
61+
value TEXT NOT NULL,
62+
timestamp TEXT NOT NULL,
63+
tag TEXT NOT NULL
64+
)
65+
`);
66+
this.db.exec(`
67+
CREATE INDEX IF NOT EXISTS idx_env_history_key ON env_history(key);
68+
CREATE INDEX IF NOT EXISTS idx_env_history_timestamp ON env_history(timestamp);
69+
CREATE INDEX IF NOT EXISTS idx_env_history_tag ON env_history(tag);
70+
CREATE UNIQUE INDEX IF NOT EXISTS idx_env_history_key_tag_unique ON env_history(key, tag);
71+
`);
72+
}
73+
74+
// Future migrations go here:
75+
// if (currentVersion < 2) { ... }
76+
77+
if (currentVersion < DatabaseManager.SCHEMA_VERSION) {
78+
if (currentVersion === 0) {
79+
this.db.exec(`INSERT INTO schema_version (version) VALUES (${DatabaseManager.SCHEMA_VERSION})`);
80+
} else {
81+
this.db.exec(`UPDATE schema_version SET version = ${DatabaseManager.SCHEMA_VERSION}`);
82+
}
83+
}
84+
} catch (err) {
85+
throw new Error(`Failed to initialize database schema: ${err instanceof Error ? err.message : String(err)}`);
86+
}
5387
}
5488

5589
/**

src/utils/env/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ export async function fetchRemoteEnv(url: string): Promise<EnvMap> {
149149
* 验证环境变量键名是否有效
150150
*/
151151
export function validateEnvKey(key: string): boolean {
152-
// 环境变量键名规则:只能包含字母、数字和下划线,且不能以数字开头
153-
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
152+
// 环境变量键名规则:只能包含字母、数字、下划线和连字符,且不能以数字开头
153+
return /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key);
154154
}
155155

156156
/**

0 commit comments

Comments
 (0)