@starpak/config 0.0.0-stage → 1.0.6
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 +22 -0
- package/lib/errors.d.ts +13 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +27 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +114 -0
- package/lib/index.js.map +1 -0
- package/package.json +23 -3
- package/src/errors.ts +30 -0
- package/src/index.ts +116 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Spak Team
|
|
4
|
+
Copyright (c) 2019-present Shigma
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SpakError, type FormatKind } from '@starpak/error';
|
|
2
|
+
export interface ConfigErrorOptions {
|
|
3
|
+
cause?: unknown;
|
|
4
|
+
where?: string;
|
|
5
|
+
context?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** 构造一个带错误码的配置错误,message 保留本次发生的具体细节。 */
|
|
8
|
+
export declare function spakError(code: string, message: string, options?: ConfigErrorOptions): SpakError;
|
|
9
|
+
/** 任意异常归一成 `SpakError`,保留原始 cause。 */
|
|
10
|
+
export declare function normalizeError(error: unknown, code?: string, message?: string): SpakError;
|
|
11
|
+
/** 渲染成文本 / JSON / HTML / Markdown;调用方决定往哪儿写。 */
|
|
12
|
+
export declare function renderError(error: unknown, kind?: FormatKind): string;
|
|
13
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAMA,OAAO,EAAS,SAAS,EAAuB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEvF,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED,yCAAyC;AACzC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,SAAS,CAEpG;AAED,sCAAsC;AACtC,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,MAA6B,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAI/G;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,UAAmB,GAAG,MAAM,CAE7E"}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ===== config — @starpak/error bridge =====
|
|
3
|
+
//
|
|
4
|
+
// 配置文件读写失败一律带稳定错误码(SPAK-xxxx)。抛出用 `spakError()`,
|
|
5
|
+
// 输出用 `renderError()`(内部只走 `format()`),这样 CLI / 日志 / SpakOS
|
|
6
|
+
// 面板拿到的是同一份 `ErrorInfo`。
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.spakError = spakError;
|
|
9
|
+
exports.normalizeError = normalizeError;
|
|
10
|
+
exports.renderError = renderError;
|
|
11
|
+
const error_1 = require("@starpak/error");
|
|
12
|
+
/** 构造一个带错误码的配置错误,message 保留本次发生的具体细节。 */
|
|
13
|
+
function spakError(code, message, options = {}) {
|
|
14
|
+
return new error_1.SpakError(code, { message, ...options });
|
|
15
|
+
}
|
|
16
|
+
/** 任意异常归一成 `SpakError`,保留原始 cause。 */
|
|
17
|
+
function normalizeError(error, code = error_1.CODES.CONFIG_INVALID, message) {
|
|
18
|
+
if ((0, error_1.isSpakError)(error))
|
|
19
|
+
return error;
|
|
20
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
21
|
+
return new error_1.SpakError(code, { message: message ?? cause.message, cause });
|
|
22
|
+
}
|
|
23
|
+
/** 渲染成文本 / JSON / HTML / Markdown;调用方决定往哪儿写。 */
|
|
24
|
+
function renderError(error, kind = 'text') {
|
|
25
|
+
return (0, error_1.format)(normalizeError(error).toInfo(), kind);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";AAAA,6CAA6C;AAC7C,EAAE;AACF,iDAAiD;AACjD,4DAA4D;AAC5D,yBAAyB;;AAWzB,8BAEC;AAGD,wCAIC;AAGD,kCAEC;AAvBD,0CAAuF;AAQvF,yCAAyC;AACzC,SAAgB,SAAS,CAAC,IAAY,EAAE,OAAe,EAAE,UAA8B,EAAE;IACvF,OAAO,IAAI,iBAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;AACrD,CAAC;AAED,sCAAsC;AACtC,SAAgB,cAAc,CAAC,KAAc,EAAE,OAAe,aAAK,CAAC,cAAc,EAAE,OAAgB;IAClG,IAAI,IAAA,mBAAW,EAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACpC,MAAM,KAAK,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IACvE,OAAO,IAAI,iBAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;AAC1E,CAAC;AAED,gDAAgD;AAChD,SAAgB,WAAW,CAAC,KAAc,EAAE,OAAmB,MAAM;IACnE,OAAO,IAAA,cAAM,EAAC,cAAc,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,CAAA;AACrD,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SpakConfig {
|
|
2
|
+
plugins: Record<string, any>;
|
|
3
|
+
cpc?: {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
sandbox?: {
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
};
|
|
8
|
+
processIsolation?: {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
ssetps?: {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Memory limit in MB for SSetPS RSS monitoring; 80% of this trips the warning + circuit breaker. */
|
|
14
|
+
memoryLimitMB?: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function loadConfig(): SpakConfig;
|
|
19
|
+
export declare function saveConfig(config: SpakConfig): void;
|
|
20
|
+
export declare function getConfig(key: string): any;
|
|
21
|
+
export declare function setConfig(key: string, value: any): SpakConfig;
|
|
22
|
+
export { renderError, spakError } from './errors';
|
|
23
|
+
declare const _default: {
|
|
24
|
+
loadConfig: typeof loadConfig;
|
|
25
|
+
saveConfig: typeof saveConfig;
|
|
26
|
+
getConfig: typeof getConfig;
|
|
27
|
+
setConfig: typeof setConfig;
|
|
28
|
+
};
|
|
29
|
+
export default _default;
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,GAAG,CAAC,EAAE;QACJ,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,OAAO,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAA;QAC/B,gBAAgB,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAA;QACxC,MAAM,CAAC,EAAE;YACP,OAAO,CAAC,EAAE,OAAO,CAAA;YACjB,qGAAqG;YACrG,aAAa,CAAC,EAAE,MAAM,CAAA;SACvB,CAAA;KACF,CAAA;CACF;AAwBD,wBAAgB,UAAU,IAAI,UAAU,CAgBvC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAwBnD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAS1C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,UAAU,CAW7D;AAED,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;;;;;;;AACjD,wBAA+D"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spakError = exports.renderError = void 0;
|
|
4
|
+
exports.loadConfig = loadConfig;
|
|
5
|
+
exports.saveConfig = saveConfig;
|
|
6
|
+
exports.getConfig = getConfig;
|
|
7
|
+
exports.setConfig = setConfig;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const util_1 = require("@starpak/util");
|
|
11
|
+
const error_1 = require("@starpak/error");
|
|
12
|
+
const errors_1 = require("./errors");
|
|
13
|
+
// 数据目录 = 项目源码根下的 data/(尊重 SPAK_DATA_DIR 覆盖,不写 ~/.spak)
|
|
14
|
+
const CONFIG_DIR = (0, util_1.projectDataDir)();
|
|
15
|
+
const CONFIG_FILE = (0, path_1.resolve)(CONFIG_DIR, 'config.json');
|
|
16
|
+
const defaultConfig = {
|
|
17
|
+
plugins: {},
|
|
18
|
+
};
|
|
19
|
+
/** 调试时把配置错误按统一格式打到 stderr(只走 format,不裸写 message)。 */
|
|
20
|
+
function diagnose(error) {
|
|
21
|
+
if (!process.env.SPAK_DEBUG)
|
|
22
|
+
return;
|
|
23
|
+
process.stderr.write((0, errors_1.renderError)(error) + '\n');
|
|
24
|
+
}
|
|
25
|
+
function ensureConfigDir() {
|
|
26
|
+
if ((0, fs_1.existsSync)(CONFIG_DIR))
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw (0, errors_1.spakError)(error_1.CODES.OUTPUT_NOT_WRITABLE, `无法创建配置目录:${CONFIG_DIR}`, {
|
|
33
|
+
cause: error,
|
|
34
|
+
where: CONFIG_DIR,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function loadConfig() {
|
|
39
|
+
ensureConfigDir();
|
|
40
|
+
if (!(0, fs_1.existsSync)(CONFIG_FILE)) {
|
|
41
|
+
saveConfig(defaultConfig);
|
|
42
|
+
return { ...defaultConfig };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const data = (0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8');
|
|
46
|
+
return { ...defaultConfig, ...JSON.parse(data) };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
diagnose((0, errors_1.spakError)(error_1.CODES.CONFIG_INVALID, `配置读取失败,已回退默认值:${CONFIG_FILE}`, {
|
|
50
|
+
cause: error,
|
|
51
|
+
where: CONFIG_FILE,
|
|
52
|
+
}));
|
|
53
|
+
return { ...defaultConfig };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function saveConfig(config) {
|
|
57
|
+
ensureConfigDir();
|
|
58
|
+
// Atomic write: write to a temp file first, then rename into place.
|
|
59
|
+
// This prevents half-written JSON when the process crashes mid-write, and
|
|
60
|
+
// reduces the race window between two concurrent setConfig() calls.
|
|
61
|
+
const tmpFile = CONFIG_FILE + '.tmp.' + process.pid.toString(36);
|
|
62
|
+
try {
|
|
63
|
+
(0, fs_1.writeFileSync)(tmpFile, JSON.stringify(config, null, 2), 'utf-8');
|
|
64
|
+
// rename() is atomic on POSIX filesystems; on Windows we fallback to
|
|
65
|
+
// replace via copy+unlink if rename fails.
|
|
66
|
+
try {
|
|
67
|
+
(0, fs_1.renameSync)(tmpFile, CONFIG_FILE);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
diagnose((0, errors_1.spakError)(error_1.CODES.FS_RENAME_UNSUPPORTED, `rename 失败,改用 copy+unlink:${tmpFile}`, { cause: error }));
|
|
71
|
+
(0, fs_1.copyFileSync)(tmpFile, CONFIG_FILE);
|
|
72
|
+
(0, fs_1.unlinkSync)(tmpFile);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
try {
|
|
77
|
+
(0, fs_1.unlinkSync)(tmpFile);
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
throw (0, errors_1.spakError)(error_1.CODES.OUTPUT_NOT_WRITABLE, `配置写入失败:${CONFIG_FILE}`, {
|
|
81
|
+
cause: error,
|
|
82
|
+
where: CONFIG_FILE,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function getConfig(key) {
|
|
87
|
+
const config = loadConfig();
|
|
88
|
+
const parts = key.split('.');
|
|
89
|
+
let value = config;
|
|
90
|
+
for (const part of parts) {
|
|
91
|
+
if (value === undefined || value === null)
|
|
92
|
+
return undefined;
|
|
93
|
+
value = value[part];
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function setConfig(key, value) {
|
|
98
|
+
const config = loadConfig();
|
|
99
|
+
const parts = key.split('.');
|
|
100
|
+
let obj = config;
|
|
101
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
102
|
+
if (!obj[parts[i]])
|
|
103
|
+
obj[parts[i]] = {};
|
|
104
|
+
obj = obj[parts[i]];
|
|
105
|
+
}
|
|
106
|
+
obj[parts[parts.length - 1]] = value;
|
|
107
|
+
saveConfig(config);
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
110
|
+
var errors_2 = require("./errors");
|
|
111
|
+
Object.defineProperty(exports, "renderError", { enumerable: true, get: function () { return errors_2.renderError; } });
|
|
112
|
+
Object.defineProperty(exports, "spakError", { enumerable: true, get: function () { return errors_2.spakError; } });
|
|
113
|
+
exports.default = { loadConfig, saveConfig, getConfig, setConfig };
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AA8CA,gCAgBC;AAED,gCAwBC;AAED,8BASC;AAED,8BAWC;AAhHD,2BAA6G;AAC7G,+BAA8B;AAC9B,wCAA8C;AAC9C,0CAAsC;AACtC,qCAAiD;AAEjD,uDAAuD;AACvD,MAAM,UAAU,GAAG,IAAA,qBAAc,GAAE,CAAA;AACnC,MAAM,WAAW,GAAG,IAAA,cAAO,EAAC,UAAU,EAAE,aAAa,CAAC,CAAA;AAgBtD,MAAM,aAAa,GAAe;IAChC,OAAO,EAAE,EAAE;CACZ,CAAA;AAED,qDAAqD;AACrD,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU;QAAE,OAAM;IACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAA,oBAAW,EAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAA;AACjD,CAAC;AAED,SAAS,eAAe;IACtB,IAAI,IAAA,eAAU,EAAC,UAAU,CAAC;QAAE,OAAM;IAClC,IAAI,CAAC;QACH,IAAA,cAAS,EAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAA,kBAAS,EAAC,aAAK,CAAC,mBAAmB,EAAE,YAAY,UAAU,EAAE,EAAE;YACnE,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,UAAU;SAClB,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,SAAgB,UAAU;IACxB,eAAe,EAAE,CAAA;IACjB,IAAI,CAAC,IAAA,eAAU,EAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,UAAU,CAAC,aAAa,CAAC,CAAA;QACzB,OAAO,EAAE,GAAG,aAAa,EAAE,CAAA;IAC7B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAA,iBAAY,EAAC,WAAW,EAAE,OAAO,CAAC,CAAA;QAC/C,OAAO,EAAE,GAAG,aAAa,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAA;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,QAAQ,CAAC,IAAA,kBAAS,EAAC,aAAK,CAAC,cAAc,EAAE,iBAAiB,WAAW,EAAE,EAAE;YACvE,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,WAAW;SACnB,CAAC,CAAC,CAAA;QACH,OAAO,EAAE,GAAG,aAAa,EAAE,CAAA;IAC7B,CAAC;AACH,CAAC;AAED,SAAgB,UAAU,CAAC,MAAkB;IAC3C,eAAe,EAAE,CAAA;IACjB,oEAAoE;IACpE,0EAA0E;IAC1E,oEAAoE;IACpE,MAAM,OAAO,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IAChE,IAAI,CAAC;QACH,IAAA,kBAAa,EAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;QAChE,qEAAqE;QACrE,2CAA2C;QAC3C,IAAI,CAAC;YACH,IAAA,eAAU,EAAC,OAAO,EAAE,WAAW,CAAC,CAAA;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,IAAA,kBAAS,EAAC,aAAK,CAAC,qBAAqB,EAAE,4BAA4B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;YACzG,IAAA,iBAAY,EAAC,OAAO,EAAE,WAAW,CAAC,CAAA;YAClC,IAAA,eAAU,EAAC,OAAO,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YAAC,IAAA,eAAU,EAAC,OAAO,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAClD,MAAM,IAAA,kBAAS,EAAC,aAAK,CAAC,mBAAmB,EAAE,UAAU,WAAW,EAAE,EAAE;YAClE,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,WAAW;SACnB,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,SAAgB,SAAS,CAAC,GAAW;IACnC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,KAAK,GAAQ,MAAM,CAAA;IACvB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QAC3D,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAgB,SAAS,CAAC,GAAW,EAAE,KAAU;IAC/C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,GAAG,GAAQ,MAAM,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QACtC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IACrB,CAAC;IACD,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;IACpC,UAAU,CAAC,MAAM,CAAC,CAAA;IAClB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,mCAAiD;AAAxC,qGAAA,WAAW,OAAA;AAAE,mGAAA,SAAS,OAAA;AAC/B,kBAAe,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starpak/config",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Spak Config - Central configuration manager",
|
|
4
|
+
"version": "1.0.6",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"import": "./lib/index.js",
|
|
11
|
+
"require": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./src/*": "./src/*",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"author": "Spak Team",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@starpak/util": "^1.0.5",
|
|
24
|
+
"@starpak/error": "^0.1.4"
|
|
25
|
+
}
|
|
6
26
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ===== config — @starpak/error bridge =====
|
|
2
|
+
//
|
|
3
|
+
// 配置文件读写失败一律带稳定错误码(SPAK-xxxx)。抛出用 `spakError()`,
|
|
4
|
+
// 输出用 `renderError()`(内部只走 `format()`),这样 CLI / 日志 / SpakOS
|
|
5
|
+
// 面板拿到的是同一份 `ErrorInfo`。
|
|
6
|
+
|
|
7
|
+
import { CODES, SpakError, format, isSpakError, type FormatKind } from '@starpak/error'
|
|
8
|
+
|
|
9
|
+
export interface ConfigErrorOptions {
|
|
10
|
+
cause?: unknown
|
|
11
|
+
where?: string
|
|
12
|
+
context?: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 构造一个带错误码的配置错误,message 保留本次发生的具体细节。 */
|
|
16
|
+
export function spakError(code: string, message: string, options: ConfigErrorOptions = {}): SpakError {
|
|
17
|
+
return new SpakError(code, { message, ...options })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 任意异常归一成 `SpakError`,保留原始 cause。 */
|
|
21
|
+
export function normalizeError(error: unknown, code: string = CODES.CONFIG_INVALID, message?: string): SpakError {
|
|
22
|
+
if (isSpakError(error)) return error
|
|
23
|
+
const cause = error instanceof Error ? error : new Error(String(error))
|
|
24
|
+
return new SpakError(code, { message: message ?? cause.message, cause })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 渲染成文本 / JSON / HTML / Markdown;调用方决定往哪儿写。 */
|
|
28
|
+
export function renderError(error: unknown, kind: FormatKind = 'text'): string {
|
|
29
|
+
return format(normalizeError(error).toInfo(), kind)
|
|
30
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, copyFileSync, unlinkSync } from 'fs'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
import { projectDataDir } from '@starpak/util'
|
|
4
|
+
import { CODES } from '@starpak/error'
|
|
5
|
+
import { renderError, spakError } from './errors'
|
|
6
|
+
|
|
7
|
+
// 数据目录 = 项目源码根下的 data/(尊重 SPAK_DATA_DIR 覆盖,不写 ~/.spak)
|
|
8
|
+
const CONFIG_DIR = projectDataDir()
|
|
9
|
+
const CONFIG_FILE = resolve(CONFIG_DIR, 'config.json')
|
|
10
|
+
|
|
11
|
+
export interface SpakConfig {
|
|
12
|
+
plugins: Record<string, any>
|
|
13
|
+
cpc?: {
|
|
14
|
+
enabled?: boolean
|
|
15
|
+
sandbox?: { enabled?: boolean }
|
|
16
|
+
processIsolation?: { enabled?: boolean }
|
|
17
|
+
ssetps?: {
|
|
18
|
+
enabled?: boolean
|
|
19
|
+
/** Memory limit in MB for SSetPS RSS monitoring; 80% of this trips the warning + circuit breaker. */
|
|
20
|
+
memoryLimitMB?: number
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultConfig: SpakConfig = {
|
|
26
|
+
plugins: {},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 调试时把配置错误按统一格式打到 stderr(只走 format,不裸写 message)。 */
|
|
30
|
+
function diagnose(error: unknown) {
|
|
31
|
+
if (!process.env.SPAK_DEBUG) return
|
|
32
|
+
process.stderr.write(renderError(error) + '\n')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureConfigDir() {
|
|
36
|
+
if (existsSync(CONFIG_DIR)) return
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw spakError(CODES.OUTPUT_NOT_WRITABLE, `无法创建配置目录:${CONFIG_DIR}`, {
|
|
41
|
+
cause: error,
|
|
42
|
+
where: CONFIG_DIR,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function loadConfig(): SpakConfig {
|
|
48
|
+
ensureConfigDir()
|
|
49
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
50
|
+
saveConfig(defaultConfig)
|
|
51
|
+
return { ...defaultConfig }
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const data = readFileSync(CONFIG_FILE, 'utf-8')
|
|
55
|
+
return { ...defaultConfig, ...JSON.parse(data) }
|
|
56
|
+
} catch (error) {
|
|
57
|
+
diagnose(spakError(CODES.CONFIG_INVALID, `配置读取失败,已回退默认值:${CONFIG_FILE}`, {
|
|
58
|
+
cause: error,
|
|
59
|
+
where: CONFIG_FILE,
|
|
60
|
+
}))
|
|
61
|
+
return { ...defaultConfig }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveConfig(config: SpakConfig): void {
|
|
66
|
+
ensureConfigDir()
|
|
67
|
+
// Atomic write: write to a temp file first, then rename into place.
|
|
68
|
+
// This prevents half-written JSON when the process crashes mid-write, and
|
|
69
|
+
// reduces the race window between two concurrent setConfig() calls.
|
|
70
|
+
const tmpFile = CONFIG_FILE + '.tmp.' + process.pid.toString(36)
|
|
71
|
+
try {
|
|
72
|
+
writeFileSync(tmpFile, JSON.stringify(config, null, 2), 'utf-8')
|
|
73
|
+
// rename() is atomic on POSIX filesystems; on Windows we fallback to
|
|
74
|
+
// replace via copy+unlink if rename fails.
|
|
75
|
+
try {
|
|
76
|
+
renameSync(tmpFile, CONFIG_FILE)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
diagnose(spakError(CODES.FS_RENAME_UNSUPPORTED, `rename 失败,改用 copy+unlink:${tmpFile}`, { cause: error }))
|
|
79
|
+
copyFileSync(tmpFile, CONFIG_FILE)
|
|
80
|
+
unlinkSync(tmpFile)
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
try { unlinkSync(tmpFile) } catch { /* ignore */ }
|
|
84
|
+
throw spakError(CODES.OUTPUT_NOT_WRITABLE, `配置写入失败:${CONFIG_FILE}`, {
|
|
85
|
+
cause: error,
|
|
86
|
+
where: CONFIG_FILE,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getConfig(key: string): any {
|
|
92
|
+
const config = loadConfig()
|
|
93
|
+
const parts = key.split('.')
|
|
94
|
+
let value: any = config
|
|
95
|
+
for (const part of parts) {
|
|
96
|
+
if (value === undefined || value === null) return undefined
|
|
97
|
+
value = value[part]
|
|
98
|
+
}
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function setConfig(key: string, value: any): SpakConfig {
|
|
103
|
+
const config = loadConfig()
|
|
104
|
+
const parts = key.split('.')
|
|
105
|
+
let obj: any = config
|
|
106
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
107
|
+
if (!obj[parts[i]]) obj[parts[i]] = {}
|
|
108
|
+
obj = obj[parts[i]]
|
|
109
|
+
}
|
|
110
|
+
obj[parts[parts.length - 1]] = value
|
|
111
|
+
saveConfig(config)
|
|
112
|
+
return config
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { renderError, spakError } from './errors'
|
|
116
|
+
export default { loadConfig, saveConfig, getConfig, setConfig }
|
package/README.md
DELETED