refactor(cli): 重构配置和 .env 文件查找逻辑,支持从 cwd 向上遍历目录树

- 调整 getSpaceflowDir:从 cwd 向上遍历查找已存在的 .spaceflow 目录,未找到时回退到 cwd/.spaceflow
- 重构 getConfigPaths 和 getEnvFilePaths:支持向上遍历目录树收集所有祖先目录的配置文件,越靠近 cwd 优先级越高
- 新增 local 选项:getDependencies 和 readConfigSync 支持 local 模式,仅读取当前目录和全局配置,不向上遍历
This commit is contained in:
Lyda
2026-02-26 18:21:38 +08:00
parent 4c6b825d44
commit 62a381bac3
11 changed files with 116 additions and 75 deletions

View File

@@ -4,7 +4,6 @@
"type": "module",
"dependencies": {
"@spaceflow/core": "workspace:*",
"@spaceflow/shared": "workspace:*",
"@spaceflow/shell": "workspace:*",
"@spaceflow/scripts": "workspace:*",
"@spaceflow/review": "workspace:*",

View File

@@ -1,5 +0,0 @@
# Spaceflow Extension dependencies
node_modules/
pnpm-lock.yaml
config-schema.json
bin/

View File

@@ -1,8 +0,0 @@
{
"name": "spaceflow",
"private": true,
"type": "module",
"dependencies": {
"@spaceflow/core": "latest"
}
}

View File

@@ -1,5 +0,0 @@
# Spaceflow Extension dependencies
node_modules/
pnpm-lock.yaml
config-schema.json
bin/

View File

@@ -1,8 +0,0 @@
{
"name": "spaceflow",
"private": true,
"type": "module",
"dependencies": {
"@spaceflow/core": "latest"
}
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { join, dirname, resolve } from "path";
import { execSync } from "child_process";
import { homedir } from "os";
import {
@@ -22,25 +22,39 @@ import {
*/
/**
* 获取 .spaceflow 目录路径(优先本地,回退全局)
* 获取 .spaceflow 目录路径
* 从 cwd 向上遍历查找已存在的 .spaceflow 目录,
* 如果整个目录树中都没有,则回退到 cwd/.spaceflow
*/
function getSpaceflowDir(): string {
const localDir = join(process.cwd(), SPACEFLOW_DIR);
if (existsSync(localDir)) {
return localDir;
let current = resolve(process.cwd());
const home = homedir();
while (true) {
const candidate = join(current, SPACEFLOW_DIR);
if (existsSync(candidate)) {
return candidate;
}
const parent = dirname(current);
if (parent === current) break; // 文件系统根
current = parent;
}
const globalDir = join(homedir(), SPACEFLOW_DIR);
// 检查全局目录
const globalDir = join(home, SPACEFLOW_DIR);
if (existsSync(globalDir)) {
return globalDir;
}
return localDir;
// 都没有,回退到 cwd后续 ensureSpaceflowPackageJson 会创建)
return join(process.cwd(), SPACEFLOW_DIR);
}
/**
* 从 spaceflow.json / .spaceflowrc 读取外部扩展包名列表
*/
function readExternalExtensions(): string[] {
const deps = getDependencies();
const deps = getDependencies(undefined, { local: true });
return Object.keys(deps);
}
@@ -55,8 +69,7 @@ function generateIndexContent(extensions: string[]): string {
.map((name) => ` import('${name}').then(m => m.default || m.extension || m),`)
.join("\n");
return `import { exec, initCliI18n } from '@spaceflow/core';
import { loadEnvFiles, getEnvFilePaths } from '@spaceflow/shared';
return `import { exec, initCliI18n, loadEnvFiles, getEnvFilePaths } from '@spaceflow/core';
async function bootstrap() {
// 1. 先加载 .env 文件,确保 process.env 在 schema 求值前已就绪

View File

@@ -1,3 +1,3 @@
export * from "./spaceflow.config";
export * from "./schema-generator.service";
export { loadEnvFiles } from "@spaceflow/shared";
export { loadEnvFiles, getEnvFilePaths } from "@spaceflow/shared";

View File

@@ -108,7 +108,9 @@ export class OpenAIAdapter implements LlmAdapter {
const model = options?.model || openaiConf.model;
if (shouldLog(options?.verbose, 1)) {
console.log(`[LLMProxy.OpenAIAdapter.chatStream] 配置: Model=${model}`);
console.log(
`[LLMProxy.OpenAIAdapter.chatStream] 配置: Model=${model}, BaseURL=${openaiConf.baseUrl || "默认"}`,
);
}
try {
const stream = await client.chat.completions.create({

View File

@@ -1,5 +1,5 @@
import { readFileSync, existsSync, writeFileSync } from "fs";
import { join } from "path";
import { join, dirname, resolve } from "path";
import { homedir } from "os";
import stringify from "json-stringify-pretty-compact";
import { config as dotenvConfig } from "dotenv";
@@ -69,39 +69,89 @@ export function getConfigPath(cwd?: string): string {
/**
* 获取所有配置文件路径(按优先级从低到高排列)
* 优先级: ~/.spaceflow/spaceflow.json < ~/.spaceflowrc < ./.spaceflow/spaceflow.json < ./.spaceflowrc
* 从 cwd 逐级向上遍历查找 .spaceflowrc .spaceflow/spaceflow.json
* 越靠近 cwd 的优先级越高。全局配置优先级最低。
*
* 优先级示例(从低到高):
* ~/.spaceflow/spaceflow.json < ~/.spaceflowrc
* < /project/.spaceflow/spaceflow.json < /project/.spaceflowrc
* < /project/extensions/publish/.spaceflow/spaceflow.json < /project/extensions/publish/.spaceflowrc
*
* @param cwd 工作目录,默认为 process.cwd()
*/
export function getConfigPaths(cwd?: string): string[] {
const workDir = cwd || process.cwd();
return [
join(homedir(), ".spaceflow", CONFIG_FILE_NAME),
join(homedir(), RC_FILE_NAME),
join(workDir, ".spaceflow", CONFIG_FILE_NAME),
join(workDir, RC_FILE_NAME),
];
export function getConfigPaths(cwd?: string, options?: { local?: boolean }): string[] {
const workDir = resolve(cwd || process.cwd());
const home = homedir();
// local 模式:只读当前目录和全局目录,不向上遍历
if (options?.local) {
return [
join(home, ".spaceflow", CONFIG_FILE_NAME),
join(home, RC_FILE_NAME),
join(workDir, ".spaceflow", CONFIG_FILE_NAME),
join(workDir, RC_FILE_NAME),
];
}
// 从 cwd 向上收集所有祖先目录(不含 homehome 单独处理)
const ancestors: string[] = [];
let current = workDir;
while (true) {
ancestors.push(current);
const parent = dirname(current);
if (parent === current) break; // 到达文件系统根
current = parent;
}
// 全局配置(最低优先级)
const paths: string[] = [join(home, ".spaceflow", CONFIG_FILE_NAME), join(home, RC_FILE_NAME)];
// 从最远祖先到 cwd优先级递增
for (let i = ancestors.length - 1; i >= 0; i--) {
const dir = ancestors[i];
// 跳过 home 目录(已在全局配置中处理)
if (dir === home) continue;
paths.push(join(dir, ".spaceflow", CONFIG_FILE_NAME));
paths.push(join(dir, RC_FILE_NAME));
}
return paths;
}
/**
* 获取所有 .env 文件路径(按优先级从高到低排列,供 ConfigModule.envFilePath 使用
*
* NestJS ConfigModule 中 envFilePath 数组靠前的优先级高(先读到的变量不会被后覆盖)
* 因此返回顺序为从高到低:
* 1. ./.env (程序启动目录,最高优先级)
* 2. ./.spaceflow/.env (项目配置目录)
* 3. ~/.env (全局 home 目录)
* 4. ~/.spaceflow/.env (全局配置目录,最低优先级)
* 获取所有 .env 文件路径(按优先级从高到低排列)
* 从 cwd 逐级向上遍历查找 .env 和 .spaceflow/.env
* 越靠近 cwd 的优先级高(先加载的变量不会被后加载的覆盖)
*
* @param cwd 工作目录,默认为 process.cwd()
*/
export function getEnvFilePaths(cwd?: string): string[] {
const workDir = cwd || process.cwd();
return [
join(workDir, ENV_FILE_NAME),
join(workDir, ".spaceflow", ENV_FILE_NAME),
join(homedir(), ENV_FILE_NAME),
join(homedir(), ".spaceflow", ENV_FILE_NAME),
];
const workDir = resolve(cwd || process.cwd());
const home = homedir();
// 从 cwd 向上收集所有祖先目录
const ancestors: string[] = [];
let current = workDir;
while (true) {
ancestors.push(current);
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
// 从 cwd 到最远祖先(优先级递减)
const paths: string[] = [];
for (const dir of ancestors) {
if (dir === home) continue;
paths.push(join(dir, ENV_FILE_NAME));
paths.push(join(dir, ".spaceflow", ENV_FILE_NAME));
}
// 全局配置(最低优先级)
paths.push(join(home, ENV_FILE_NAME));
paths.push(join(home, ".spaceflow", ENV_FILE_NAME));
return paths;
}
/**
@@ -144,8 +194,11 @@ function readSingleConfigSync(configPath: string): Record<string, unknown> {
* 4. ./.spaceflowrc (项目根目录 RC 配置,最高优先级)
* @param cwd 工作目录,默认为 process.cwd()
*/
export function readConfigSync(cwd?: string): Record<string, unknown> {
const configPaths = getConfigPaths(cwd);
export function readConfigSync(
cwd?: string,
options?: { local?: boolean },
): Record<string, unknown> {
const configPaths = getConfigPaths(cwd, options);
const configs = configPaths.map((p) => readSingleConfigSync(p));
return deepMerge(...configs);
}
@@ -173,8 +226,11 @@ export function getSupportedEditors(cwd?: string): string[] {
* 获取 dependencies
* @param cwd 工作目录,默认为 process.cwd()
*/
export function getDependencies(cwd?: string): Record<string, string> {
const config = readConfigSync(cwd);
export function getDependencies(
cwd?: string,
options?: { local?: boolean },
): Record<string, string> {
const config = readConfigSync(cwd, options);
return (config.dependencies as Record<string, string>) || {};
}

View File

@@ -102,13 +102,13 @@ export function ensureSpaceflowPackageJson(spaceflowDir: string): void {
const packageJsonPath = join(spaceflowDir, PACKAGE_JSON);
const coreVersion = getSpaceflowCoreVersion();
// 从 spaceflow.json/.spaceflowrc 读取扩展依赖
const extDeps = getDependencies();
// 只从当前目录级别读取扩展依赖(不向上遍历祖先目录)
const projectDir = join(spaceflowDir, "..");
const extDeps = getDependencies(projectDir, { local: true });
// 构建期望的 dependencies@spaceflow/core + @spaceflow/shared + 所有扩展包
// 构建期望的 dependencies@spaceflow/core + 所有扩展包
const expectedDeps: Record<string, string> = {
"@spaceflow/core": coreVersion,
"@spaceflow/shared": coreVersion,
...extDeps,
};

3
pnpm-lock.yaml generated
View File

@@ -95,9 +95,6 @@ importers:
'@spaceflow/scripts':
specifier: workspace:*
version: link:../extensions/scripts
'@spaceflow/shared':
specifier: workspace:*
version: link:../packages/shared
'@spaceflow/shell':
specifier: workspace:*
version: link:../extensions/shell