Skip to content

Commit aa0c0b9

Browse files
feat(config): seed path support, unresolved-var detection, fix password false-positive
- Add optional path to seed schema and resolve it in setup (parity with migrations) - Add findUnresolvedVars helper and optional collector to resolveEnvVars - checkConfigWarnings now only flags literal DB passwords, not keys like password_reset_url
1 parent 71fdb3b commit aa0c0b9

5 files changed

Lines changed: 57 additions & 10 deletions

File tree

src/commands/init.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,13 @@ async function runWizard(projectRoot: string): Promise<DevEnvConfig> {
184184
);
185185
if (addSeed.value) {
186186
const seedCmd = await prompts(
187-
{ type: 'text', name: 'command', message: 'Seed command', initial: 'npm run seed' },
187+
[
188+
{ type: 'text', name: 'command', message: 'Seed command', initial: 'npm run seed' },
189+
{ type: 'text', name: 'path', message: 'Seed path', initial: '.' },
190+
],
188191
{ onCancel: () => process.exit(0) }
189192
);
190-
db.seed = { command: seedCmd.command || 'npm run seed' };
193+
db.seed = { command: seedCmd.command || 'npm run seed', path: seedCmd.path?.trim() || '.' };
191194
}
192195
databases.push(db);
193196
}

src/commands/setup.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,17 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
109109
}
110110
}
111111
if (db.seed && !dryRun) {
112+
const seedPath = db.seed.path ?? '.';
113+
const cwd = path.resolve(projectRoot, seedPath);
112114
const { cmd, args } = parseCommand(db.seed.command);
113-
logger.step(`Running seed (${db.type})...`);
115+
logger.step(`Running seed (${db.type}) in ${seedPath}...`);
114116
await exec(cmd, args, {
115-
cwd: projectRoot,
117+
cwd,
116118
env: { ...process.env, ...env } as Record<string, string>,
117119
silent: false,
118120
});
119121
} else if (db.seed && dryRun) {
120-
logger.info(`[dry-run] Would run seed: ${db.seed.command}`);
122+
logger.info(`[dry-run] Would run seed in ${db.seed.path ?? '.'}: ${db.seed.command}`);
121123
}
122124
}
123125
} else if (skipDb) {

src/core/config/loader.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ export async function loadEnvFile(projectRoot: string): Promise<void> {
1919
}
2020
}
2121

22-
export function resolveEnvVars(value: string, env: Record<string, string> = {}): string {
22+
export function resolveEnvVars(
23+
value: string,
24+
env: Record<string, string> = {},
25+
unresolved?: Set<string>
26+
): string {
2327
const mergedEnv = { ...process.env, ...env };
2428
const MAX_DEPTH = 10;
2529

@@ -29,7 +33,11 @@ export function resolveEnvVars(value: string, env: Record<string, string> = {}):
2933
result = result.replace(/\$\{([^}]+)\}/g, (match, varName) => {
3034
const resolved = mergedEnv[varName];
3135
if (resolved === undefined) {
32-
logger.warn(`Environment variable ${varName} is not set`);
36+
if (unresolved) {
37+
unresolved.add(varName);
38+
} else {
39+
logger.warn(`Environment variable ${varName} is not set`);
40+
}
3341
return match;
3442
}
3543
return resolved;
@@ -39,6 +47,34 @@ export function resolveEnvVars(value: string, env: Record<string, string> = {}):
3947
return result;
4048
}
4149

50+
/**
51+
* Walk a raw (pre-validation) config object and return the names of all
52+
* `${VAR}` references that cannot be resolved from process.env or the config's
53+
* own `env` block. Used by `envkit validate --strict`.
54+
*/
55+
export function findUnresolvedVars(rawConfig: any): string[] {
56+
const unresolved = new Set<string>();
57+
const localEnv: Record<string, string> = {};
58+
if (rawConfig && typeof rawConfig === 'object' && rawConfig.env && typeof rawConfig.env === 'object') {
59+
for (const [k, v] of Object.entries(rawConfig.env)) {
60+
if (typeof v === 'string') localEnv[k] = v;
61+
}
62+
}
63+
64+
const walk = (obj: any): void => {
65+
if (typeof obj === 'string') {
66+
resolveEnvVars(obj, localEnv, unresolved);
67+
} else if (Array.isArray(obj)) {
68+
obj.forEach(walk);
69+
} else if (obj !== null && typeof obj === 'object') {
70+
Object.values(obj).forEach(walk);
71+
}
72+
};
73+
walk(rawConfig);
74+
75+
return [...unresolved];
76+
}
77+
4278
export async function loadConfig(projectRoot: string = process.cwd()): Promise<DevEnvConfig> {
4379
const configPath = path.join(projectRoot, '.dev-env.yml');
4480

src/core/config/validator.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ export function validateConfig(config: any): DevEnvConfig {
2323
/**
2424
* Check for common configuration issues and warn
2525
*/
26+
function isLiteralSecret(value: string | undefined): boolean {
27+
// A real secret is a non-empty string that is not an unresolved ${VAR} placeholder.
28+
return !!value && !value.includes('${');
29+
}
30+
2631
export function checkConfigWarnings(config: DevEnvConfig): void {
27-
// Check for passwords in config (should be in .env)
28-
const configStr = JSON.stringify(config);
29-
if (configStr.includes('password') && !configStr.includes('${')) {
32+
// Check for hardcoded passwords in known secret locations (should be in .env).
33+
// Only inspect real secret fields so keys like `password_reset_url` don't false-positive.
34+
if (config.databases?.some((db) => isLiteralSecret(db.password))) {
3035
logger.warn(
3136
'Warning: Passwords detected in configuration file.\n' +
3237
'Consider using environment variables (${VAR_NAME}) and .env file for secrets.'

src/types/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const DatabaseSchema = z.object({
2424
})).optional(),
2525
seed: z.object({
2626
command: z.string(),
27+
path: z.string().optional().default('.'),
2728
}).optional(),
2829
});
2930

0 commit comments

Comments
 (0)