@myagentroam/marmgr 0.9.112
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 +202 -0
- package/NOTICE +4 -0
- package/bin/marmgr.mjs +6 -0
- package/package.json +29 -0
- package/src/cli.mjs +137 -0
- package/src/client.mjs +212 -0
- package/src/config.mjs +150 -0
- package/src/direct.mjs +769 -0
- package/src/i18n/en-US.mjs +72 -0
- package/src/i18n/index.mjs +20 -0
- package/src/i18n/zh-CN.mjs +70 -0
- package/src/terminal.mjs +479 -0
package/src/config.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createTranslator } from './i18n/index.mjs';
|
|
5
|
+
|
|
6
|
+
export const CONFIG_KEYS = Object.freeze(['MAR_MGR_SERVER_URL', 'MAR_MGR_KEY']);
|
|
7
|
+
|
|
8
|
+
export class MarmgrError extends Error {
|
|
9
|
+
constructor(message, options = {}) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = 'MarmgrError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function defaultConfigPath({
|
|
16
|
+
env = process.env,
|
|
17
|
+
platform = process.platform,
|
|
18
|
+
homeDirectory = os.homedir()
|
|
19
|
+
} = {}) {
|
|
20
|
+
const configuredHome =
|
|
21
|
+
nonEmptyString(env.HOME) ??
|
|
22
|
+
(platform === 'win32' ? nonEmptyString(env.USERPROFILE) : undefined) ??
|
|
23
|
+
homeDirectory;
|
|
24
|
+
return path.join(configuredHome, '.marmgr', 'default.env');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadConfig({
|
|
28
|
+
env = process.env,
|
|
29
|
+
envFile,
|
|
30
|
+
platform = process.platform,
|
|
31
|
+
homeDirectory = os.homedir(),
|
|
32
|
+
translate = createTranslator(env)
|
|
33
|
+
} = {}) {
|
|
34
|
+
const configPath = envFile ?? defaultConfigPath({ env, platform, homeDirectory });
|
|
35
|
+
const values = await readConfigFile(configPath, { platform, translate });
|
|
36
|
+
|
|
37
|
+
const serverUrlValue = nonEmptyString(values.MAR_MGR_SERVER_URL);
|
|
38
|
+
if (serverUrlValue === undefined)
|
|
39
|
+
throw new MarmgrError(translate('errors.configServerUrlMissing'));
|
|
40
|
+
|
|
41
|
+
const keyValue = nonEmptyString(values.MAR_MGR_KEY);
|
|
42
|
+
if (keyValue === undefined) throw new MarmgrError(translate('errors.configKeyMissing'));
|
|
43
|
+
if (/[\u0000-\u001f\u007f]/u.test(keyValue))
|
|
44
|
+
throw new MarmgrError(translate('errors.configKeyInvalid'));
|
|
45
|
+
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
serverUrl: validateServerUrl(serverUrlValue, translate),
|
|
48
|
+
key: keyValue
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateServerUrl(value, translate = createTranslator()) {
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(value);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new MarmgrError(translate('errors.serverUrlInvalid'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isLoopbackHttp =
|
|
61
|
+
url.protocol === 'http:' &&
|
|
62
|
+
(url.hostname === 'localhost' ||
|
|
63
|
+
url.hostname === '127.0.0.1' ||
|
|
64
|
+
url.hostname === '::1' ||
|
|
65
|
+
url.hostname === '[::1]');
|
|
66
|
+
if (url.protocol !== 'https:' && !isLoopbackHttp)
|
|
67
|
+
throw new MarmgrError(translate('errors.serverUrlTransport'));
|
|
68
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/')
|
|
69
|
+
throw new MarmgrError(translate('errors.serverUrlOrigin'));
|
|
70
|
+
|
|
71
|
+
return url.origin;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readConfigFile(filePath, { platform, translate }) {
|
|
75
|
+
let before;
|
|
76
|
+
try {
|
|
77
|
+
before = await lstat(filePath);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error?.code === 'ENOENT') throw new MarmgrError(translate('errors.envFileMissing'));
|
|
80
|
+
throw new MarmgrError(translate('errors.envFileRead'));
|
|
81
|
+
}
|
|
82
|
+
assertSecureConfigFile(before, platform, translate);
|
|
83
|
+
|
|
84
|
+
let content;
|
|
85
|
+
try {
|
|
86
|
+
content = await readFile(filePath, 'utf8');
|
|
87
|
+
} catch {
|
|
88
|
+
throw new MarmgrError(translate('errors.envFileRead'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let after;
|
|
92
|
+
try {
|
|
93
|
+
after = await lstat(filePath);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new MarmgrError(translate('errors.envFileRead'));
|
|
96
|
+
}
|
|
97
|
+
assertSecureConfigFile(after, platform, translate);
|
|
98
|
+
if (before.dev !== after.dev || before.ino !== after.ino)
|
|
99
|
+
throw new MarmgrError(translate('errors.envFileRead'));
|
|
100
|
+
|
|
101
|
+
const values = {};
|
|
102
|
+
for (const rawLine of content.replace(/^\uFEFF/u, '').split(/\r?\n/u)) {
|
|
103
|
+
const line = rawLine.trim();
|
|
104
|
+
if (line === '' || line.startsWith('#')) continue;
|
|
105
|
+
|
|
106
|
+
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u.exec(line);
|
|
107
|
+
if (match === null) throw new MarmgrError(translate('errors.envFileEntry'));
|
|
108
|
+
|
|
109
|
+
const [, name, rawValue] = match;
|
|
110
|
+
if (!CONFIG_KEYS.includes(name)) throw new MarmgrError(translate('errors.envFileUnsupported'));
|
|
111
|
+
values[name] = parseEnvValue(rawValue, translate);
|
|
112
|
+
}
|
|
113
|
+
return values;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseEnvValue(rawValue, translate) {
|
|
117
|
+
const value = rawValue.trim();
|
|
118
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
119
|
+
const quote = value[0];
|
|
120
|
+
if (!value.endsWith(quote) || value.length < 2)
|
|
121
|
+
throw new MarmgrError(translate('errors.envFileQuoted'));
|
|
122
|
+
const body = value.slice(1, -1);
|
|
123
|
+
if (quote === "'") return body;
|
|
124
|
+
return body.replace(/\\(["\\nrt])/gu, (_match, escaped) => {
|
|
125
|
+
if (escaped === 'n') return '\n';
|
|
126
|
+
if (escaped === 'r') return '\r';
|
|
127
|
+
if (escaped === 't') return '\t';
|
|
128
|
+
return escaped;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertSecureConfigFile(stats, platform, translate) {
|
|
135
|
+
if (stats.isSymbolicLink()) throw new MarmgrError(translate('errors.envFileSymlink'));
|
|
136
|
+
if (!stats.isFile()) throw new MarmgrError(translate('errors.envFileType'));
|
|
137
|
+
if (platform === 'win32') return;
|
|
138
|
+
|
|
139
|
+
const permissions = stats.mode & 0o7777;
|
|
140
|
+
const ownerUid = typeof process.getuid === 'function' ? process.getuid() : undefined;
|
|
141
|
+
if (
|
|
142
|
+
(ownerUid !== undefined && stats.uid !== ownerUid) ||
|
|
143
|
+
(permissions !== 0o600 && permissions !== 0o400)
|
|
144
|
+
)
|
|
145
|
+
throw new MarmgrError(translate('errors.envFilePermissions'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function nonEmptyString(value) {
|
|
149
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
150
|
+
}
|