@wwkit/shared 1.0.27 → 1.0.29
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.
- package/LICENSE +20 -20
- package/README.md +34 -34
- package/package.json +5 -3
- package/src/args.js +79 -79
- package/src/config-loader.js +250 -250
- package/src/http.js +482 -482
- package/src/index.js +4 -4
- package/src/release.js +282 -138
- package/src/shell.js +569 -336
package/src/config-loader.js
CHANGED
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import process from 'node:process'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import JSON5 from 'json5'
|
|
7
|
-
|
|
8
|
-
const ENV_PATTERN = /\{env:(\w+)\}/g
|
|
9
|
-
const PKG_PATTERN = /\{pkg:([^}]+)\}/g
|
|
10
|
-
|
|
11
|
-
const _selfDir = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 向上查找 pnpm 工作区根目录
|
|
15
|
-
* @param {string} startDir
|
|
16
|
-
* @returns {string|null}
|
|
17
|
-
*/
|
|
18
|
-
function findWorkspaceRoot(startDir) {
|
|
19
|
-
let dir = startDir
|
|
20
|
-
while (dir !== path.dirname(dir)) {
|
|
21
|
-
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir
|
|
22
|
-
const packagesDir = path.join(dir, 'packages')
|
|
23
|
-
if (fs.existsSync(packagesDir) && fs.existsSync(path.join(dir, 'package.json'))) {
|
|
24
|
-
try {
|
|
25
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
26
|
-
if (pkg.private && pkg.workspaces) return dir
|
|
27
|
-
} catch {}
|
|
28
|
-
}
|
|
29
|
-
dir = path.dirname(dir)
|
|
30
|
-
}
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 解析 npm 包的安装目录路径
|
|
36
|
-
*
|
|
37
|
-
* 从当前文件位置向外逐级查找 node_modules 目录,在其中定位目标包。
|
|
38
|
-
* {pkg:XX} 的目标包必然与当前包在同一个 node_modules 空间下,
|
|
39
|
-
* 因此只需向外找到最近的 node_modules 即可命中。
|
|
40
|
-
* 开发环境下回退到工作区 packages/ 目录。
|
|
41
|
-
*
|
|
42
|
-
* @param {string} pkgName - 包名,如 @wwkit/harness
|
|
43
|
-
* @returns {string} 目录路径,找不到时返回空串
|
|
44
|
-
*/
|
|
45
|
-
function resolvePkgPath(pkgName) {
|
|
46
|
-
let dir = _selfDir
|
|
47
|
-
while (dir !== path.dirname(dir)) {
|
|
48
|
-
const pkgJsonPath = path.join(dir, 'node_modules', pkgName, 'package.json')
|
|
49
|
-
if (fs.existsSync(pkgJsonPath)) {
|
|
50
|
-
try {
|
|
51
|
-
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
|
52
|
-
if (pkg.name === pkgName) return path.dirname(pkgJsonPath)
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
dir = path.dirname(dir)
|
|
56
|
-
}
|
|
57
|
-
const wsRoot = findWorkspaceRoot(_selfDir)
|
|
58
|
-
if (wsRoot) {
|
|
59
|
-
const seg = pkgName.includes('/') ? pkgName.split('/').pop() : pkgName
|
|
60
|
-
const candidate = path.join(wsRoot, 'packages', seg)
|
|
61
|
-
const pkgJson = path.join(candidate, 'package.json')
|
|
62
|
-
if (fs.existsSync(pkgJson)) {
|
|
63
|
-
try {
|
|
64
|
-
const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'))
|
|
65
|
-
if (pkg.name === pkgName) return candidate
|
|
66
|
-
} catch {}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return ''
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 递归检查 builtin 是否有 user 缺少的 key(嵌套对象也递归比较)。
|
|
74
|
-
* @param {*} builtin
|
|
75
|
-
* @param {*} user
|
|
76
|
-
* @returns {boolean}
|
|
77
|
-
*/
|
|
78
|
-
function hasMissingKeys(builtin, user) {
|
|
79
|
-
if (!builtin || typeof builtin !== 'object' || Array.isArray(builtin)) return false
|
|
80
|
-
if (!user || typeof user !== 'object' || Array.isArray(user)) return false
|
|
81
|
-
for (const key of Object.keys(builtin)) {
|
|
82
|
-
if (!(key in user)) return true
|
|
83
|
-
const bv = builtin[key]
|
|
84
|
-
const uv = user[key]
|
|
85
|
-
if (bv && typeof bv === 'object' && !Array.isArray(bv)
|
|
86
|
-
&& uv && typeof uv === 'object' && !Array.isArray(uv)) {
|
|
87
|
-
if (hasMissingKeys(bv, uv)) return true
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return false
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 递归合并对象,override 优先,嵌套对象深合并(数组/标量直接覆盖)。
|
|
95
|
-
*/
|
|
96
|
-
export function deepMerge(base, override) {
|
|
97
|
-
const merged = { ...(base ?? {}) }
|
|
98
|
-
for (const [key, value] of Object.entries(override ?? {})) {
|
|
99
|
-
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
100
|
-
&& merged[key] && typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
101
|
-
merged[key] = deepMerge(merged[key], value)
|
|
102
|
-
} else {
|
|
103
|
-
merged[key] = value
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return merged
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 递归解析字符串中的 {env:XXX} 占位符,替换为环境变量值(未设置则为空串)。
|
|
111
|
-
*/
|
|
112
|
-
export function resolveEnvVars(value, env = process.env) {
|
|
113
|
-
if (typeof value === 'string') {
|
|
114
|
-
return value.replace(ENV_PATTERN, (_, name) => env[name] ?? '')
|
|
115
|
-
}
|
|
116
|
-
if (Array.isArray(value)) {
|
|
117
|
-
return value.map((v) => resolveEnvVars(v, env))
|
|
118
|
-
}
|
|
119
|
-
if (value && typeof value === 'object') {
|
|
120
|
-
const out = {}
|
|
121
|
-
for (const [k, v] of Object.entries(value)) out[k] = resolveEnvVars(v, env)
|
|
122
|
-
return out
|
|
123
|
-
}
|
|
124
|
-
return value
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 递归解析字符串中的 {pkg:NAME} 占位符,替换为包安装目录(找不到则为空串)。
|
|
129
|
-
*/
|
|
130
|
-
export function resolvePkgVars(value) {
|
|
131
|
-
if (typeof value === 'string') {
|
|
132
|
-
return value.replace(PKG_PATTERN, (_, name) => resolvePkgPath(name.trim()))
|
|
133
|
-
}
|
|
134
|
-
if (Array.isArray(value)) {
|
|
135
|
-
return value.map((v) => resolvePkgVars(v))
|
|
136
|
-
}
|
|
137
|
-
if (value && typeof value === 'object') {
|
|
138
|
-
const out = {}
|
|
139
|
-
for (const [k, v] of Object.entries(value)) out[k] = resolvePkgVars(v)
|
|
140
|
-
return out
|
|
141
|
-
}
|
|
142
|
-
return value
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* 多层配置加载器:内置 config.json5 + 用户 config.json5 + 环境变量注入内容(优先级从低到高)。
|
|
147
|
-
*
|
|
148
|
-
* 默认行为(无需配置):
|
|
149
|
-
* - 深合并(deepMerge),非浅合并
|
|
150
|
-
* - 结果默认解析 {env:XXX} 占位符
|
|
151
|
-
*
|
|
152
|
-
* 可选层(通过选项开启):
|
|
153
|
-
* - envContentVar: 指定环境变量名,其内容作为"注入配置"这一层参与合并(优先级最高,同样走深合并+占位符解析)
|
|
154
|
-
* - envDirVar: 指定环境变量名,其值覆盖用户配置目录
|
|
155
|
-
*
|
|
156
|
-
* 用法:
|
|
157
|
-
* const loader = createConfigLoader({
|
|
158
|
-
* builtinConfigPath: path.join(__dirname, 'config.json5'),
|
|
159
|
-
* userConfigDir: getXdgConfigDir('cft'),
|
|
160
|
-
* })
|
|
161
|
-
* loader.copyBuiltinConfig() // 复制内置→用户目录(不覆盖已存在)
|
|
162
|
-
* const cfg = loader.getConfig()
|
|
163
|
-
*/
|
|
164
|
-
export function createConfigLoader({ builtinConfigPath, userConfigDir, envContentVar = null, envDirVar = null }) {
|
|
165
|
-
const getUserDir = () => (envDirVar ? (process.env[envDirVar] ?? userConfigDir) : userConfigDir)
|
|
166
|
-
const getUserConfigPath = () => path.join(getUserDir(), 'config.json5')
|
|
167
|
-
|
|
168
|
-
function loadJson5(filePath) {
|
|
169
|
-
if (!filePath || !fs.existsSync(filePath)) return {}
|
|
170
|
-
try {
|
|
171
|
-
return JSON5.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
172
|
-
} catch {
|
|
173
|
-
return {}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function loadContent(content) {
|
|
178
|
-
if (!content) return {}
|
|
179
|
-
try {
|
|
180
|
-
return JSON5.parse(content)
|
|
181
|
-
} catch {
|
|
182
|
-
return {}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getConfig() {
|
|
187
|
-
const builtin = loadJson5(builtinConfigPath)
|
|
188
|
-
const user = loadJson5(getUserConfigPath())
|
|
189
|
-
let merged = deepMerge(builtin, user)
|
|
190
|
-
if (envContentVar) {
|
|
191
|
-
merged = deepMerge(merged, loadContent(process.env[envContentVar]))
|
|
192
|
-
}
|
|
193
|
-
return resolvePkgVars(resolveEnvVars(merged))
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function copyBuiltinConfig() {
|
|
197
|
-
const dir = getUserDir()
|
|
198
|
-
if (!fs.existsSync(dir)) {
|
|
199
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
200
|
-
}
|
|
201
|
-
const userPath = getUserConfigPath()
|
|
202
|
-
if (!fs.existsSync(userPath) && fs.existsSync(builtinConfigPath)) {
|
|
203
|
-
fs.copyFileSync(builtinConfigPath, userPath)
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
return false
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* 合并内置配置到用户配置文件:
|
|
211
|
-
* 用户文件不存在 → 复制 builtin;已存在 → 追加缺失 key(不覆盖已有值)。
|
|
212
|
-
* @returns {boolean} 是否写入了文件
|
|
213
|
-
*/
|
|
214
|
-
function mergeBuiltinConfig() {
|
|
215
|
-
const dir = getUserDir()
|
|
216
|
-
if (!fs.existsSync(dir)) {
|
|
217
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
218
|
-
}
|
|
219
|
-
const userPath = getUserConfigPath()
|
|
220
|
-
if (!fs.existsSync(userPath)) {
|
|
221
|
-
if (fs.existsSync(builtinConfigPath)) {
|
|
222
|
-
fs.copyFileSync(builtinConfigPath, userPath)
|
|
223
|
-
return true
|
|
224
|
-
}
|
|
225
|
-
return false
|
|
226
|
-
}
|
|
227
|
-
const builtin = loadJson5(builtinConfigPath)
|
|
228
|
-
const user = loadJson5(userPath)
|
|
229
|
-
if (!hasMissingKeys(builtin, user)) return false
|
|
230
|
-
const merged = deepMerge(builtin, user)
|
|
231
|
-
fs.writeFileSync(userPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
|
|
232
|
-
return true
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
getConfig,
|
|
237
|
-
copyBuiltinConfig,
|
|
238
|
-
mergeBuiltinConfig,
|
|
239
|
-
userConfigDir: getUserDir(),
|
|
240
|
-
userConfigPath: getUserConfigPath(),
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 获取 XDG 配置目录下的子路径。
|
|
246
|
-
* ~/.config/<subdir> 或 $XDG_CONFIG_HOME/<subdir>
|
|
247
|
-
*/
|
|
248
|
-
export function getXdgConfigDir(subdir) {
|
|
249
|
-
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
250
|
-
return path.join(xdg, subdir)
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import JSON5 from 'json5'
|
|
7
|
+
|
|
8
|
+
const ENV_PATTERN = /\{env:(\w+)\}/g
|
|
9
|
+
const PKG_PATTERN = /\{pkg:([^}]+)\}/g
|
|
10
|
+
|
|
11
|
+
const _selfDir = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 向上查找 pnpm 工作区根目录
|
|
15
|
+
* @param {string} startDir
|
|
16
|
+
* @returns {string|null}
|
|
17
|
+
*/
|
|
18
|
+
function findWorkspaceRoot(startDir) {
|
|
19
|
+
let dir = startDir
|
|
20
|
+
while (dir !== path.dirname(dir)) {
|
|
21
|
+
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir
|
|
22
|
+
const packagesDir = path.join(dir, 'packages')
|
|
23
|
+
if (fs.existsSync(packagesDir) && fs.existsSync(path.join(dir, 'package.json'))) {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
26
|
+
if (pkg.private && pkg.workspaces) return dir
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
dir = path.dirname(dir)
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 解析 npm 包的安装目录路径
|
|
36
|
+
*
|
|
37
|
+
* 从当前文件位置向外逐级查找 node_modules 目录,在其中定位目标包。
|
|
38
|
+
* {pkg:XX} 的目标包必然与当前包在同一个 node_modules 空间下,
|
|
39
|
+
* 因此只需向外找到最近的 node_modules 即可命中。
|
|
40
|
+
* 开发环境下回退到工作区 packages/ 目录。
|
|
41
|
+
*
|
|
42
|
+
* @param {string} pkgName - 包名,如 @wwkit/harness
|
|
43
|
+
* @returns {string} 目录路径,找不到时返回空串
|
|
44
|
+
*/
|
|
45
|
+
function resolvePkgPath(pkgName) {
|
|
46
|
+
let dir = _selfDir
|
|
47
|
+
while (dir !== path.dirname(dir)) {
|
|
48
|
+
const pkgJsonPath = path.join(dir, 'node_modules', pkgName, 'package.json')
|
|
49
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
|
52
|
+
if (pkg.name === pkgName) return path.dirname(pkgJsonPath)
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
dir = path.dirname(dir)
|
|
56
|
+
}
|
|
57
|
+
const wsRoot = findWorkspaceRoot(_selfDir)
|
|
58
|
+
if (wsRoot) {
|
|
59
|
+
const seg = pkgName.includes('/') ? pkgName.split('/').pop() : pkgName
|
|
60
|
+
const candidate = path.join(wsRoot, 'packages', seg)
|
|
61
|
+
const pkgJson = path.join(candidate, 'package.json')
|
|
62
|
+
if (fs.existsSync(pkgJson)) {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'))
|
|
65
|
+
if (pkg.name === pkgName) return candidate
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 递归检查 builtin 是否有 user 缺少的 key(嵌套对象也递归比较)。
|
|
74
|
+
* @param {*} builtin
|
|
75
|
+
* @param {*} user
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function hasMissingKeys(builtin, user) {
|
|
79
|
+
if (!builtin || typeof builtin !== 'object' || Array.isArray(builtin)) return false
|
|
80
|
+
if (!user || typeof user !== 'object' || Array.isArray(user)) return false
|
|
81
|
+
for (const key of Object.keys(builtin)) {
|
|
82
|
+
if (!(key in user)) return true
|
|
83
|
+
const bv = builtin[key]
|
|
84
|
+
const uv = user[key]
|
|
85
|
+
if (bv && typeof bv === 'object' && !Array.isArray(bv)
|
|
86
|
+
&& uv && typeof uv === 'object' && !Array.isArray(uv)) {
|
|
87
|
+
if (hasMissingKeys(bv, uv)) return true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 递归合并对象,override 优先,嵌套对象深合并(数组/标量直接覆盖)。
|
|
95
|
+
*/
|
|
96
|
+
export function deepMerge(base, override) {
|
|
97
|
+
const merged = { ...(base ?? {}) }
|
|
98
|
+
for (const [key, value] of Object.entries(override ?? {})) {
|
|
99
|
+
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
100
|
+
&& merged[key] && typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
101
|
+
merged[key] = deepMerge(merged[key], value)
|
|
102
|
+
} else {
|
|
103
|
+
merged[key] = value
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return merged
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 递归解析字符串中的 {env:XXX} 占位符,替换为环境变量值(未设置则为空串)。
|
|
111
|
+
*/
|
|
112
|
+
export function resolveEnvVars(value, env = process.env) {
|
|
113
|
+
if (typeof value === 'string') {
|
|
114
|
+
return value.replace(ENV_PATTERN, (_, name) => env[name] ?? '')
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map((v) => resolveEnvVars(v, env))
|
|
118
|
+
}
|
|
119
|
+
if (value && typeof value === 'object') {
|
|
120
|
+
const out = {}
|
|
121
|
+
for (const [k, v] of Object.entries(value)) out[k] = resolveEnvVars(v, env)
|
|
122
|
+
return out
|
|
123
|
+
}
|
|
124
|
+
return value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 递归解析字符串中的 {pkg:NAME} 占位符,替换为包安装目录(找不到则为空串)。
|
|
129
|
+
*/
|
|
130
|
+
export function resolvePkgVars(value) {
|
|
131
|
+
if (typeof value === 'string') {
|
|
132
|
+
return value.replace(PKG_PATTERN, (_, name) => resolvePkgPath(name.trim()))
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
return value.map((v) => resolvePkgVars(v))
|
|
136
|
+
}
|
|
137
|
+
if (value && typeof value === 'object') {
|
|
138
|
+
const out = {}
|
|
139
|
+
for (const [k, v] of Object.entries(value)) out[k] = resolvePkgVars(v)
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
return value
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 多层配置加载器:内置 config.json5 + 用户 config.json5 + 环境变量注入内容(优先级从低到高)。
|
|
147
|
+
*
|
|
148
|
+
* 默认行为(无需配置):
|
|
149
|
+
* - 深合并(deepMerge),非浅合并
|
|
150
|
+
* - 结果默认解析 {env:XXX} 占位符
|
|
151
|
+
*
|
|
152
|
+
* 可选层(通过选项开启):
|
|
153
|
+
* - envContentVar: 指定环境变量名,其内容作为"注入配置"这一层参与合并(优先级最高,同样走深合并+占位符解析)
|
|
154
|
+
* - envDirVar: 指定环境变量名,其值覆盖用户配置目录
|
|
155
|
+
*
|
|
156
|
+
* 用法:
|
|
157
|
+
* const loader = createConfigLoader({
|
|
158
|
+
* builtinConfigPath: path.join(__dirname, 'config.json5'),
|
|
159
|
+
* userConfigDir: getXdgConfigDir('cft'),
|
|
160
|
+
* })
|
|
161
|
+
* loader.copyBuiltinConfig() // 复制内置→用户目录(不覆盖已存在)
|
|
162
|
+
* const cfg = loader.getConfig()
|
|
163
|
+
*/
|
|
164
|
+
export function createConfigLoader({ builtinConfigPath, userConfigDir, envContentVar = null, envDirVar = null }) {
|
|
165
|
+
const getUserDir = () => (envDirVar ? (process.env[envDirVar] ?? userConfigDir) : userConfigDir)
|
|
166
|
+
const getUserConfigPath = () => path.join(getUserDir(), 'config.json5')
|
|
167
|
+
|
|
168
|
+
function loadJson5(filePath) {
|
|
169
|
+
if (!filePath || !fs.existsSync(filePath)) return {}
|
|
170
|
+
try {
|
|
171
|
+
return JSON5.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
172
|
+
} catch {
|
|
173
|
+
return {}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function loadContent(content) {
|
|
178
|
+
if (!content) return {}
|
|
179
|
+
try {
|
|
180
|
+
return JSON5.parse(content)
|
|
181
|
+
} catch {
|
|
182
|
+
return {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getConfig() {
|
|
187
|
+
const builtin = loadJson5(builtinConfigPath)
|
|
188
|
+
const user = loadJson5(getUserConfigPath())
|
|
189
|
+
let merged = deepMerge(builtin, user)
|
|
190
|
+
if (envContentVar) {
|
|
191
|
+
merged = deepMerge(merged, loadContent(process.env[envContentVar]))
|
|
192
|
+
}
|
|
193
|
+
return resolvePkgVars(resolveEnvVars(merged))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function copyBuiltinConfig() {
|
|
197
|
+
const dir = getUserDir()
|
|
198
|
+
if (!fs.existsSync(dir)) {
|
|
199
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
200
|
+
}
|
|
201
|
+
const userPath = getUserConfigPath()
|
|
202
|
+
if (!fs.existsSync(userPath) && fs.existsSync(builtinConfigPath)) {
|
|
203
|
+
fs.copyFileSync(builtinConfigPath, userPath)
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 合并内置配置到用户配置文件:
|
|
211
|
+
* 用户文件不存在 → 复制 builtin;已存在 → 追加缺失 key(不覆盖已有值)。
|
|
212
|
+
* @returns {boolean} 是否写入了文件
|
|
213
|
+
*/
|
|
214
|
+
function mergeBuiltinConfig() {
|
|
215
|
+
const dir = getUserDir()
|
|
216
|
+
if (!fs.existsSync(dir)) {
|
|
217
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
218
|
+
}
|
|
219
|
+
const userPath = getUserConfigPath()
|
|
220
|
+
if (!fs.existsSync(userPath)) {
|
|
221
|
+
if (fs.existsSync(builtinConfigPath)) {
|
|
222
|
+
fs.copyFileSync(builtinConfigPath, userPath)
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
const builtin = loadJson5(builtinConfigPath)
|
|
228
|
+
const user = loadJson5(userPath)
|
|
229
|
+
if (!hasMissingKeys(builtin, user)) return false
|
|
230
|
+
const merged = deepMerge(builtin, user)
|
|
231
|
+
fs.writeFileSync(userPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
getConfig,
|
|
237
|
+
copyBuiltinConfig,
|
|
238
|
+
mergeBuiltinConfig,
|
|
239
|
+
userConfigDir: getUserDir(),
|
|
240
|
+
userConfigPath: getUserConfigPath(),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 获取 XDG 配置目录下的子路径。
|
|
246
|
+
* ~/.config/<subdir> 或 $XDG_CONFIG_HOME/<subdir>
|
|
247
|
+
*/
|
|
248
|
+
export function getXdgConfigDir(subdir) {
|
|
249
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
250
|
+
return path.join(xdg, subdir)
|
|
251
251
|
}
|