@zerozhang-giza/kdl-agent 0.1.0-beta.1

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/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@zerozhang-giza/kdl-agent",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "快代理 CLI:账户与订单查询、授权操作及代理便利命令",
5
+ "license": "MIT",
6
+ "engines": { "node": ">=22.14.0" },
7
+ "bin": { "kdl-agent": "scripts/run.cjs" },
8
+ "repository": { "type": "git", "url": "git+https://github.com/gizaZerozhang/kdl-agent-cli.git" },
9
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
10
+ "files": ["scripts/runtime", "scripts/run.cjs", "scripts/install.cjs", "scripts/wizard.cjs", "scripts/verify-package.cjs", "SHA256SUMS", "release.json", "CHANGELOG.md", "THIRD_PARTY_NOTICES.txt"],
11
+ "scripts": {
12
+ "postinstall": "node scripts/install.cjs",
13
+ "prepack": "node scripts/verify-package.cjs",
14
+ "test": "node --test tests/*.test.cjs",
15
+ "release:prepare": "node scripts/prepare-package.cjs"
16
+ },
17
+ "dependencies": { "https-proxy-agent": "7.0.6", "tar": "7.5.22", "yauzl": "3.4.0" },
18
+ "devDependencies": { "yazl": "3.3.1" },
19
+ "kdl": { "command": "kdl-agent", "skill": "kdl-agent", "skillsPackage": "skills@1.5.25" }
20
+ }
package/release.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": "0.1.0-beta.1",
3
+ "repository": "gizaZerozhang/kdl-agent-cli",
4
+ "commit": "a50bc08164a9ab11a8f9d8b5435eb3553fcdeed8",
5
+ "dirty": false,
6
+ "macosSigned": false,
7
+ "sha256": "856588de2e6e97c166f8c0a1573a1ef97c747db9df132e3f9ee5c3a95e1061e9"
8
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // npx 安装向导先执行预检;真正调用业务命令时仍会校验并补装原生程序。
4
+ if (process.env.npm_command !== 'exec') {
5
+ require('./runtime/install.cjs').ensureInstalled().catch(error => {
6
+ console.error(`安装失败:${error.message}`);
7
+ process.exitCode = 1;
8
+ });
9
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { spawn } = require('node:child_process');
4
+ async function main() {
5
+ const args = process.argv.slice(2);
6
+ if (args[0] === 'install') return require('./wizard.cjs').main(args.slice(1));
7
+ const binary = await require('./runtime/install.cjs').ensureInstalled();
8
+ const child = spawn(binary, args, { stdio: 'inherit' });
9
+ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal));
10
+ child.on('error', error => { console.error(`运行失败:${error.message}`); process.exitCode = 1; });
11
+ child.on('exit', (code, signal) => { process.exitCode = code ?? (signal === 'SIGINT' ? 130 : 1); });
12
+ }
13
+ main().catch(error => { console.error(`运行失败:${error.message}`); process.exitCode = 1; });
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const https = require('node:https');
5
+ const crypto = require('node:crypto');
6
+ const { pipeline } = require('node:stream/promises');
7
+ const { Transform } = require('node:stream');
8
+ const { spawnSync } = require('node:child_process');
9
+ const { HttpsProxyAgent } = require('https-proxy-agent');
10
+ const tar = require('tar');
11
+ const yauzl = require('yauzl');
12
+
13
+ const ROOT = path.resolve(__dirname, '../..');
14
+ const MAX_BYTES = 256 * 1024 * 1024;
15
+ const HOSTS = new Set(['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com']);
16
+ const TARGETS = new Set(['darwin-arm64', 'darwin-amd64', 'linux-arm64', 'linux-amd64', 'windows-amd64']);
17
+
18
+ function config(root = ROOT) {
19
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
20
+ if (!/^\d+\.\d+\.\d+(?:-beta\.\d+)?$/.test(pkg.version)) throw new Error('不支持的发行版本');
21
+ const url = new URL(pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, ''));
22
+ if (url.origin !== 'https://github.com' || !/^\/[\w-]+\/[\w.-]+$/.test(url.pathname)) throw new Error('无效源码仓库地址');
23
+ return { pkg, repository: url.pathname.slice(1), root };
24
+ }
25
+
26
+ function target(version, platform = process.platform, arch = process.arch) {
27
+ const system = platform === 'win32' ? 'windows' : platform;
28
+ const cpu = arch === 'x64' ? 'amd64' : arch;
29
+ if (!TARGETS.has(`${system}-${cpu}`)) throw new Error(`不支持的平台:${platform}/${arch}`);
30
+ const ext = system === 'windows' ? 'zip' : 'tar.gz';
31
+ return { name: `kdl-agent_${version}_${system}_${cpu}.${ext}`, binary: system === 'windows' ? 'kdl-agent.exe' : 'kdl-agent' };
32
+ }
33
+
34
+ function checksums(text) {
35
+ const values = new Map();
36
+ for (const line of text.trim().split(/\r?\n/)) {
37
+ const match = /^([a-f0-9]{64}) ([A-Za-z0-9_.-]+)$/.exec(line);
38
+ if (!match || values.has(match[2])) throw new Error('校验清单格式错误或文件重复');
39
+ values.set(match[2], match[1]);
40
+ }
41
+ return values;
42
+ }
43
+
44
+ function allowedURL(raw) {
45
+ const url = new URL(raw);
46
+ if (url.protocol !== 'https:' || url.username || url.password || (url.port && url.port !== '443') || !HOSTS.has(url.hostname)) {
47
+ throw new Error('下载地址不属于允许的 GitHub HTTPS 资源');
48
+ }
49
+ return url;
50
+ }
51
+
52
+ async function download(raw, destination, redirects = 0) {
53
+ const url = allowedURL(raw);
54
+ if (redirects > 5) throw new Error('下载重定向次数过多');
55
+ const proxy = process.env.HTTPS_PROXY || process.env.https_proxy;
56
+ const response = await new Promise((resolve, reject) => {
57
+ const request = https.get(url, { agent: proxy ? new HttpsProxyAgent(proxy) : undefined, headers: { 'User-Agent': 'kdl-agent-installer' } }, resolve);
58
+ request.setTimeout(60000, () => request.destroy(new Error('下载超时,请检查网络或 HTTPS_PROXY')));
59
+ request.on('error', reject);
60
+ });
61
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
62
+ response.resume();
63
+ if (!response.headers.location) throw new Error('下载重定向缺少地址');
64
+ return download(new URL(response.headers.location, url).href, destination, redirects + 1);
65
+ }
66
+ if (response.statusCode !== 200) {
67
+ response.resume();
68
+ throw new Error(`下载失败 HTTP ${response.statusCode};请核对版本及 Release 是否公开`);
69
+ }
70
+ let size = 0;
71
+ const limit = new Transform({ transform(chunk, encoding, callback) {
72
+ size += chunk.length;
73
+ callback(size > MAX_BYTES ? new Error('发行包超过大小限制') : null, chunk);
74
+ } });
75
+ await pipeline(response, limit, fs.createWriteStream(destination, { flags: 'wx', mode: 0o600 }));
76
+ }
77
+
78
+ async function hashFile(file) {
79
+ const hash = crypto.createHash('sha256');
80
+ for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
81
+ return hash.digest('hex');
82
+ }
83
+
84
+ function safeEntry(name) {
85
+ if (!name || name.includes('\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name) || name.split('/').includes('..')) throw new Error('归档包含非法路径');
86
+ }
87
+
88
+ async function extractBinary(archive, binary, destination) {
89
+ let found = false;
90
+ async function save(stream, name, size) {
91
+ if (name !== binary) { stream.resume(); return; }
92
+ if (found || size > MAX_BYTES) throw new Error('归档可执行文件重复或过大');
93
+ found = true;
94
+ await pipeline(stream, fs.createWriteStream(destination, { flags: 'wx', mode: 0o755 }));
95
+ }
96
+ if (archive.endsWith('.zip')) {
97
+ await new Promise((resolve, reject) => yauzl.open(archive, { lazyEntries: true }, (error, zip) => {
98
+ if (error) return reject(error);
99
+ const fail = err => { zip.close(); reject(err); };
100
+ zip.on('error', fail);
101
+ zip.on('end', resolve);
102
+ zip.on('entry', entry => {
103
+ try {
104
+ safeEntry(entry.fileName);
105
+ const mode = entry.externalFileAttributes >>> 16;
106
+ if ((mode & 0o170000) === 0o120000) throw new Error('归档不允许符号链接');
107
+ if (entry.fileName !== binary) return zip.readEntry();
108
+ zip.openReadStream(entry, (err, stream) => {
109
+ if (err) return fail(err);
110
+ save(stream, entry.fileName, entry.uncompressedSize).then(() => zip.readEntry(), fail);
111
+ });
112
+ } catch (err) { fail(err); }
113
+ });
114
+ zip.readEntry();
115
+ }));
116
+ } else {
117
+ const pending = [];
118
+ let invalid;
119
+ await tar.t({ file: archive, strict: true, onReadEntry(entry) {
120
+ try {
121
+ safeEntry(entry.path);
122
+ if (!['File', 'Directory', 'ExtendedHeader', 'GlobalExtendedHeader'].includes(entry.type)) throw new Error('归档不允许链接或特殊文件');
123
+ const task = save(entry, entry.path, entry.size);
124
+ task.catch(() => {});
125
+ pending.push(task);
126
+ } catch (err) { invalid = err; entry.resume(); }
127
+ } });
128
+ await Promise.all(pending);
129
+ if (invalid) throw invalid;
130
+ }
131
+ if (!found) throw new Error('归档缺少可执行文件');
132
+ fs.chmodSync(destination, 0o755);
133
+ }
134
+
135
+ function regular(file) {
136
+ const stat = fs.lstatSync(file);
137
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('程序路径不是普通文件');
138
+ }
139
+
140
+ function verifyBinary(file, version, commit) {
141
+ regular(file);
142
+ const result = spawnSync(file, ['--version'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
143
+ const output = result.stdout?.trim();
144
+ if (result.status !== 0 || !output?.startsWith(`kdl-agent ${version} (commit `) || (commit && output !== `kdl-agent ${version} (commit ${commit})`)) throw new Error('下载程序无法运行或版本、来源不匹配');
145
+ }
146
+
147
+ async function ensureInstalled({ root = ROOT, fetchFile = download, platform = process.platform, arch = process.arch, verify = verifyBinary } = {}) {
148
+ const { pkg, repository } = config(root);
149
+ const release = JSON.parse(fs.readFileSync(path.join(root, 'release.json'), 'utf8'));
150
+ if (release.version !== pkg.version || release.repository !== repository || !/^[a-f0-9]{40}$/.test(release.commit) || release.dirty !== false) throw new Error('发行元数据与 npm 包不一致');
151
+ const artifact = target(pkg.version, platform, arch);
152
+ const expected = checksums(fs.readFileSync(path.join(root, 'SHA256SUMS'), 'utf8')).get(artifact.name);
153
+ if (!expected) throw new Error('npm 包缺少对应平台的校验值,请重新安装完整发行包');
154
+ const directory = path.join(root, '.native');
155
+ fs.mkdirSync(directory, { recursive: true });
156
+ if (fs.lstatSync(directory).isSymbolicLink()) throw new Error('安装目录不能是符号链接');
157
+ const lock = path.join(directory, '.lock');
158
+ try { fs.mkdirSync(lock); } catch (error) {
159
+ if (error.code === 'EEXIST') throw new Error('另一个安装正在进行;确认没有安装进程后可删除 .native/.lock 重试');
160
+ throw error;
161
+ }
162
+ const destination = path.join(directory, artifact.binary);
163
+ const old = destination + '.old';
164
+ let temporary;
165
+ try {
166
+ if (fs.existsSync(old) && !fs.existsSync(destination)) { regular(old); fs.renameSync(old, destination); }
167
+ if (fs.existsSync(destination)) {
168
+ try { verify(destination, pkg.version, release.commit); return destination; } catch { regular(destination); }
169
+ }
170
+ temporary = fs.mkdtempSync(path.join(directory, '.download-'));
171
+ const archive = path.join(temporary, artifact.name);
172
+ const extracted = path.join(temporary, artifact.binary);
173
+ await fetchFile(`https://github.com/${repository}/releases/download/v${pkg.version}/${artifact.name}`, archive);
174
+ if (await hashFile(archive) !== expected) throw new Error('SHA256 校验失败,未替换现有程序');
175
+ await extractBinary(archive, artifact.binary, extracted);
176
+ verify(extracted, pkg.version, release.commit);
177
+ if (fs.existsSync(old)) { regular(old); fs.unlinkSync(old); }
178
+ const hadOld = fs.existsSync(destination);
179
+ if (hadOld) fs.renameSync(destination, old);
180
+ try { fs.renameSync(extracted, destination); } catch (error) {
181
+ if (hadOld) fs.renameSync(old, destination);
182
+ throw error;
183
+ }
184
+ if (hadOld) fs.unlinkSync(old);
185
+ return destination;
186
+ } finally {
187
+ if (temporary) fs.rmSync(temporary, { recursive: true, force: true });
188
+ fs.rmdirSync(lock);
189
+ }
190
+ }
191
+
192
+ module.exports = { ROOT, TARGETS, config, target, checksums, allowedURL, download, hashFile, extractBinary, ensureInstalled, verifyBinary };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { config, checksums, TARGETS, target } = require('./runtime/install.cjs');
5
+ function verify(root = path.resolve(__dirname, '..')) {
6
+ const { pkg, repository } = config(root);
7
+ if (pkg.name.includes('pending') || !/^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(pkg.name)) throw new Error('请先绑定实际个人 npm 包名');
8
+ const release = JSON.parse(fs.readFileSync(path.join(root, 'release.json'), 'utf8'));
9
+ if (release.version !== pkg.version || release.repository !== repository || !/^[a-f0-9]{40}$/.test(release.commit) || release.dirty !== false) throw new Error('发行来源、版本或仓库不一致');
10
+ if (!pkg.version.includes('-beta.') && !release.macosSigned) throw new Error('稳定发行缺少 macOS 签名公证记录');
11
+ const sums = checksums(fs.readFileSync(path.join(root, 'SHA256SUMS'), 'utf8'));
12
+ for (const item of TARGETS) {
13
+ const [platform, arch] = item.split('-');
14
+ if (!sums.has(target(pkg.version, platform, arch).name)) throw new Error(`缺少平台校验:${item}`);
15
+ }
16
+ for (const file of ['kdl-agent-skill.zip', 'openapi.yaml', 'BUILD.json', 'install.md', 'cli-guide.md', 'api-guide.md', 'release-notes.md']) if (!sums.has(file)) throw new Error(`缺少配套资料校验:${file}`);
17
+ if (release.sha256 !== require('node:crypto').createHash('sha256').update(fs.readFileSync(path.join(root, 'SHA256SUMS'))).digest('hex')) throw new Error('校验清单与发行记录不一致');
18
+ return release;
19
+ }
20
+ if (require.main === module) {
21
+ try { const release = verify(); console.log(`发行检查通过:${release.version} (${release.commit})`); }
22
+ catch (error) { console.error(`发行检查失败:${error.message}`); process.exitCode = 1; }
23
+ }
24
+ module.exports = { verify };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const readline = require('node:readline/promises');
6
+ const { config, ensureInstalled, verifyBinary } = require('./runtime/install.cjs');
7
+
8
+ function command(name, args, options = {}) {
9
+ // npm/npx 在 Windows 是 .cmd;只传入配置中校验过的标识和固定参数。
10
+ if (process.platform === 'win32' && ['npm', 'npx'].includes(name)) {
11
+ if (args.some(arg => /[&|<>^%!"\r\n]/.test(arg))) throw new Error('命令参数包含 Windows shell 特殊字符');
12
+ return spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', `${name}.cmd ${args.map(arg => `"${arg}"`).join(' ')}`], { encoding: 'utf8', ...options });
13
+ }
14
+ return spawnSync(name, args, { encoding: 'utf8', ...options });
15
+ }
16
+
17
+ function parse(args) {
18
+ const options = { yes: false, noLogin: false, noSkills: false, agents: [] };
19
+ for (let i = 0; i < args.length; i++) {
20
+ const arg = args[i];
21
+ if (arg === '--yes' || arg === '-y') options.yes = true;
22
+ else if (arg === '--no-login') options.noLogin = true;
23
+ else if (arg === '--no-skills') options.noSkills = true;
24
+ else if (arg === '--agent') {
25
+ const name = args[++i];
26
+ if (!name || !/^[a-z0-9-]+$/.test(name)) throw new Error('--agent 需要目标工具标识');
27
+ options.agents.push(name);
28
+ } else if (arg === '--help' || arg === '-h') options.help = true;
29
+ else throw new Error(`未知安装参数:${arg}`);
30
+ }
31
+ return options;
32
+ }
33
+
34
+ async function main(args, { run = command, install = ensureInstalled, readConfig = config, checkBinary = verifyBinary, tty = process.stdin.isTTY } = {}) {
35
+ const options = parse(args);
36
+ if (options.help) {
37
+ console.log('安装快代理 CLI 与同版本 Skill\n\n用法:npx <包名>@<版本> install [--yes] [--agent codex] [--no-login] [--no-skills]\n\n--yes 非交互安装;安装 Skill 时须指定 --agent\n--no-login 仅安装,稍后在本地终端执行 kdl-agent auth login\n--no-skills 跳过 Skill,适用于直接使用 CLI\n升级或回退:使用目标版本再次运行;卸载 npm 包保留 .kdl 与 Skill。');
38
+ return;
39
+ }
40
+ if (!tty && !options.yes) throw new Error('非交互环境需 --yes;Agent 请同时使用 --no-login,敏感凭证由用户在本地终端输入');
41
+ if (options.yes && !options.noSkills && !options.agents.length) throw new Error('非交互 Skill 安装须通过 --agent 指定目标工具,或显式 --no-skills');
42
+ const { pkg, repository } = readConfig();
43
+ if (!/^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(pkg.name)) throw new Error('npm 包名尚未绑定');
44
+ console.log(`目标版本:${pkg.name}@${pkg.version}`);
45
+ console.log('正在下载并校验原生程序...');
46
+ await install();
47
+ const prefixResult = run('npm', ['prefix', '-g'], { timeout: 15000 });
48
+ if (prefixResult.status !== 0) throw new Error('无法读取 npm 全局目录,请检查 Node.js/npm');
49
+ const prefix = prefixResult.stdout.trim();
50
+ const modules = process.platform === 'win32' ? path.join(prefix, 'node_modules') : path.join(prefix, 'lib/node_modules');
51
+ const installedRoot = path.join(modules, ...pkg.name.split('/'));
52
+ let previous;
53
+ try { previous = JSON.parse(fs.readFileSync(path.join(installedRoot, 'package.json'), 'utf8')).version; } catch {}
54
+ const binary = path.join(installedRoot, '.native', process.platform === 'win32' ? 'kdl-agent.exe' : 'kdl-agent');
55
+ let current = false;
56
+ if (previous === pkg.version) { try { checkBinary(binary, pkg.version); current = true; } catch {} }
57
+ if (!current) {
58
+ const result = run('npm', ['install', '-g', `${pkg.name}@${pkg.version}`, '--registry=https://registry.npmjs.org/'], { stdio: 'inherit', timeout: 180000 });
59
+ if (result.status !== 0) {
60
+ if (previous && /^\d+\.\d+\.\d+(?:-beta\.\d+)?$/.test(previous)) {
61
+ console.error(`正在尝试恢复原版本 ${previous}...`);
62
+ const restore = run('npm', ['install', '-g', `${pkg.name}@${previous}`, '--registry=https://registry.npmjs.org/'], { stdio: 'inherit', timeout: 180000 });
63
+ console.error(restore.status === 0 ? '已恢复原 npm 版本' : `恢复未完成,请执行 npm install -g ${pkg.name}@${previous}`);
64
+ }
65
+ throw new Error('全局安装失败;请检查网络和 npm 全局目录权限,不要使用 sudo 自动提权');
66
+ }
67
+ }
68
+ await install({ root: installedRoot });
69
+ console.log(`CLI:已安装 ${pkg.version}\n程序:${binary}\n命令目录:${process.platform === 'win32' ? prefix : path.join(prefix, 'bin')}(须位于 PATH)`);
70
+ if (!options.noSkills) {
71
+ const source = `https://github.com/${repository}/tree/v${pkg.version}/skills/${pkg.kdl.skill}`;
72
+ const skillArgs = ['--yes', pkg.kdl.skillsPackage, 'add', source, '--global', '--skill', pkg.kdl.skill];
73
+ if (options.yes) skillArgs.push('--yes');
74
+ for (const agent of options.agents) skillArgs.push('--agent', agent);
75
+ const result = run('npx', skillArgs, { stdio: 'inherit', timeout: 180000 });
76
+ if (result.status !== 0) throw new Error('CLI 已安装,Skill 安装未完成;重新运行此向导重试,已安装 CLI 将跳过');
77
+ console.log('Skill:已安装;目标 Agent 可能需要刷新会话');
78
+ } else console.log('Skill:按要求跳过');
79
+ if (options.noLogin) { console.log('登录:按要求跳过;用户稍后在本地终端执行 kdl-agent auth login 和 kdl-agent auth status'); return; }
80
+ let status = run(binary, ['auth', 'status', '--format', 'json'], { timeout: 25000 });
81
+ if (status.status !== 0) {
82
+ if (!tty) throw new Error('CLI/Skill 已安装,登录待用户在本地终端完成:kdl-agent auth login');
83
+ console.log('凭证管理:https://www.kuaidaili.com/uc/agent/settings/\n请仅在本地终端输入凭证,不发送给 Agent。');
84
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
85
+ let answer;
86
+ try { answer = await prompt.question('现在隐藏输入并验证登录?[y/N] '); } finally { prompt.close(); }
87
+ if (!/^y(es)?$/i.test(answer.trim())) throw new Error('安装已完成,登录待完成;稍后执行 kdl-agent auth login');
88
+ const login = run(binary, ['auth', 'login'], { stdio: 'inherit' });
89
+ if (login.status !== 0) throw new Error('登录未完成;安装结果保留,请检查凭证或 Gateway 后重试');
90
+ status = run(binary, ['auth', 'status', '--format', 'json'], { timeout: 25000 });
91
+ }
92
+ if (status.status !== 0) throw new Error('远端状态未通过验证;请执行 kdl-agent auth status 排查');
93
+ const query = run(binary, ['account', 'summary', '--format', 'json'], { timeout: 25000 });
94
+ if (query.status !== 0) throw new Error('状态有效,首次查询失败;请执行 kdl-agent account summary 排查');
95
+ console.log('登录:远端验证有效\n首次只读查询:通过(账户数据未输出到安装日志)');
96
+ }
97
+ module.exports = { main, parse, command };