ticketpro-auto-setup 1.0.0
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/dist/cli.d.ts +5 -0
- package/dist/cli.js +119 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +11 -0
- package/dist/installers/claude-code.d.ts +14 -0
- package/dist/installers/claude-code.js +73 -0
- package/dist/installers/code-abyss.d.ts +13 -0
- package/dist/installers/code-abyss.js +79 -0
- package/dist/installers/codex-cli.d.ts +18 -0
- package/dist/installers/codex-cli.js +122 -0
- package/dist/installers/grok-search.d.ts +10 -0
- package/dist/installers/grok-search.js +112 -0
- package/dist/installers/helloagents.d.ts +6 -0
- package/dist/installers/helloagents.js +148 -0
- package/dist/installers/jshook-skill.d.ts +6 -0
- package/dist/installers/jshook-skill.js +68 -0
- package/dist/pages/api-config.d.ts +6 -0
- package/dist/pages/api-config.js +65 -0
- package/dist/pages/claude-setup.d.ts +6 -0
- package/dist/pages/claude-setup.js +75 -0
- package/dist/pages/cleanup.d.ts +5 -0
- package/dist/pages/cleanup.js +74 -0
- package/dist/pages/codex-setup.d.ts +6 -0
- package/dist/pages/codex-setup.js +93 -0
- package/dist/pages/welcome.d.ts +6 -0
- package/dist/pages/welcome.js +35 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/index.js +19 -0
- package/dist/utils/backup.d.ts +9 -0
- package/dist/utils/backup.js +79 -0
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +41 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/platform.d.ts +9 -0
- package/dist/utils/platform.js +55 -0
- package/dist/utils/shell.d.ts +27 -0
- package/dist/utils/shell.js +77 -0
- package/package.json +49 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TicketPro Auto Setup — Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
/** 创建默认状态 */
|
|
5
|
+
export function createDefaultState() {
|
|
6
|
+
return {
|
|
7
|
+
toolChoice: 'both',
|
|
8
|
+
apiKey: '',
|
|
9
|
+
claudeBaseUrl: 'https://api.ticketpro.cc',
|
|
10
|
+
codexBaseUrl: 'https://api.ticketpro.cc/v1',
|
|
11
|
+
installClaude: true,
|
|
12
|
+
installCodex: true,
|
|
13
|
+
claudeExtensions: [],
|
|
14
|
+
codexExtensions: [],
|
|
15
|
+
grokConfig: undefined,
|
|
16
|
+
results: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 备份工具 — 备份/恢复目录
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execCommand } from './shell.js';
|
|
7
|
+
import { getPlatform } from './platform.js';
|
|
8
|
+
/**
|
|
9
|
+
* 备份目录到 .bak(若已存在 .bak 则追加日期)
|
|
10
|
+
*/
|
|
11
|
+
export function backupDirectory(dirPath) {
|
|
12
|
+
if (!fs.existsSync(dirPath)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
let backupPath = dirPath + '.bak';
|
|
16
|
+
// 若 .bak 已存在,追加日期
|
|
17
|
+
if (fs.existsSync(backupPath)) {
|
|
18
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
19
|
+
backupPath = `${dirPath}.bak.${date}`;
|
|
20
|
+
// 若日期备份也存在,追加时间戳
|
|
21
|
+
if (fs.existsSync(backupPath)) {
|
|
22
|
+
backupPath = `${dirPath}.bak.${Date.now()}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const { isWindows } = getPlatform();
|
|
26
|
+
const cmd = isWindows
|
|
27
|
+
? `xcopy "${dirPath}" "${backupPath}" /E /I /H /Y /Q`
|
|
28
|
+
: `cp -r "${dirPath}" "${backupPath}"`;
|
|
29
|
+
const result = execCommand(cmd);
|
|
30
|
+
if (result.success) {
|
|
31
|
+
return backupPath;
|
|
32
|
+
}
|
|
33
|
+
// fallback: 使用 Node.js fs
|
|
34
|
+
try {
|
|
35
|
+
copyDirRecursive(dirPath, backupPath);
|
|
36
|
+
return backupPath;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 删除目录
|
|
44
|
+
*/
|
|
45
|
+
export function removeDirectory(dirPath) {
|
|
46
|
+
if (!fs.existsSync(dirPath)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// fallback: 使用 shell 命令
|
|
55
|
+
const { isWindows } = getPlatform();
|
|
56
|
+
const cmd = isWindows
|
|
57
|
+
? `rmdir /S /Q "${dirPath}"`
|
|
58
|
+
: `rm -rf "${dirPath}"`;
|
|
59
|
+
return execCommand(cmd).success;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 递归复制目录(纯 Node.js)
|
|
64
|
+
*/
|
|
65
|
+
function copyDirRecursive(src, dest) {
|
|
66
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
67
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const srcPath = path.join(src, entry.name);
|
|
70
|
+
const destPath = path.join(dest, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
copyDirRecursive(srcPath, destPath);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
fs.copyFileSync(srcPath, destPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=backup.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Art 生成 — figlet 封装
|
|
3
|
+
*/
|
|
4
|
+
import figlet from 'figlet';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
/**
|
|
7
|
+
* 生成 TicketPro ASCII banner
|
|
8
|
+
*/
|
|
9
|
+
export function generateBanner() {
|
|
10
|
+
try {
|
|
11
|
+
const art = figlet.textSync('TicketPro', {
|
|
12
|
+
font: 'Standard',
|
|
13
|
+
horizontalLayout: 'default',
|
|
14
|
+
verticalLayout: 'default',
|
|
15
|
+
});
|
|
16
|
+
return chalk.cyan(art);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// fallback 如果 figlet 失败
|
|
20
|
+
return chalk.cyan(`
|
|
21
|
+
_____ _ _ _ ____
|
|
22
|
+
|_ _(_) ___| | _____| |_| _ \\ _ __ ___
|
|
23
|
+
| | | |/ __| |/ / _ \\ __| |_) | '__/ _ \\
|
|
24
|
+
| | | | (__| < __/ |_| __/| | | (_) |
|
|
25
|
+
|_| |_|\\___|_|\\_\\___|\\__|_| |_| \\___/
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 生成完成时的 banner
|
|
31
|
+
*/
|
|
32
|
+
export function generateCompleteBanner() {
|
|
33
|
+
return chalk.green(`
|
|
34
|
+
╔═══════════════════════════════════════════╗
|
|
35
|
+
║ ║
|
|
36
|
+
║ ✅ Setup Complete! ║
|
|
37
|
+
║ ║
|
|
38
|
+
╚═══════════════════════════════════════════╝
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const log: {
|
|
2
|
+
info: (msg: string) => void;
|
|
3
|
+
success: (msg: string) => void;
|
|
4
|
+
warn: (msg: string) => void;
|
|
5
|
+
error: (msg: string) => void;
|
|
6
|
+
step: (msg: string) => void;
|
|
7
|
+
dim: (msg: string) => void;
|
|
8
|
+
blank: () => void;
|
|
9
|
+
/** 带标题的分割线 */
|
|
10
|
+
section: (title: string) => void;
|
|
11
|
+
/** 带框的消息 */
|
|
12
|
+
box: (lines: string[]) => void;
|
|
13
|
+
/** 键值对显示 */
|
|
14
|
+
kv: (key: string, value: string) => void;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志 + chalk 封装
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
export const log = {
|
|
6
|
+
info: (msg) => console.log(chalk.cyan(' ℹ ') + msg),
|
|
7
|
+
success: (msg) => console.log(chalk.green(' ✓ ') + msg),
|
|
8
|
+
warn: (msg) => console.log(chalk.yellow(' ⚠ ') + msg),
|
|
9
|
+
error: (msg) => console.log(chalk.red(' ✗ ') + msg),
|
|
10
|
+
step: (msg) => console.log(chalk.blue(' → ') + msg),
|
|
11
|
+
dim: (msg) => console.log(chalk.dim(' ' + msg)),
|
|
12
|
+
blank: () => console.log(),
|
|
13
|
+
/** 带标题的分割线 */
|
|
14
|
+
section: (title) => {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(chalk.bold.white(` ── ${title} ${'─'.repeat(Math.max(0, 50 - title.length))}`));
|
|
17
|
+
console.log();
|
|
18
|
+
},
|
|
19
|
+
/** 带框的消息 */
|
|
20
|
+
box: (lines) => {
|
|
21
|
+
const maxLen = Math.max(...lines.map(l => stripAnsi(l).length));
|
|
22
|
+
const border = '─'.repeat(maxLen + 4);
|
|
23
|
+
console.log(chalk.dim(` ┌${border}┐`));
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const pad = ' '.repeat(Math.max(0, maxLen - stripAnsi(line).length));
|
|
26
|
+
console.log(chalk.dim(' │') + ` ${line}${pad} ` + chalk.dim('│'));
|
|
27
|
+
}
|
|
28
|
+
console.log(chalk.dim(` └${border}┘`));
|
|
29
|
+
},
|
|
30
|
+
/** 键值对显示 */
|
|
31
|
+
kv: (key, value) => {
|
|
32
|
+
console.log(chalk.dim(' ') + chalk.gray(key + ': ') + chalk.white(value));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** 简易去除 ANSI 转义码(用于计算显示宽度) */
|
|
36
|
+
function stripAnsi(str) {
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
return str.replace(/\x1B\[[0-9;]*m/g, '');
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlatformInfo } from '../types/index.js';
|
|
2
|
+
export declare function getPlatform(): PlatformInfo;
|
|
3
|
+
/** 获取 ~/.claude 路径 */
|
|
4
|
+
export declare function getClaudeDir(): string;
|
|
5
|
+
/** 获取 ~/.codex 路径 */
|
|
6
|
+
export declare function getCodexDir(): string;
|
|
7
|
+
/** 获取 shell profile 文件路径 */
|
|
8
|
+
export declare function getShellProfile(): string;
|
|
9
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台工具 — 路径、shell、权限检测
|
|
3
|
+
*/
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
let _platform;
|
|
7
|
+
export function getPlatform() {
|
|
8
|
+
if (_platform)
|
|
9
|
+
return _platform;
|
|
10
|
+
const isWindows = process.platform === 'win32';
|
|
11
|
+
const isMac = process.platform === 'darwin';
|
|
12
|
+
const isLinux = process.platform === 'linux';
|
|
13
|
+
const homeDir = os.homedir();
|
|
14
|
+
// 检测默认 shell
|
|
15
|
+
let shell = 'bash';
|
|
16
|
+
if (isWindows) {
|
|
17
|
+
shell = 'powershell';
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const envShell = process.env.SHELL ?? '';
|
|
21
|
+
if (envShell.includes('zsh'))
|
|
22
|
+
shell = 'zsh';
|
|
23
|
+
else if (envShell.includes('bash'))
|
|
24
|
+
shell = 'bash';
|
|
25
|
+
else
|
|
26
|
+
shell = 'sh';
|
|
27
|
+
}
|
|
28
|
+
// Windows 下 npm/npx 需要 .cmd 后缀
|
|
29
|
+
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
|
|
30
|
+
const npxCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
31
|
+
const gitCmd = isWindows ? 'git.exe' : 'git';
|
|
32
|
+
_platform = { isWindows, isMac, isLinux, homeDir, shell, npmCmd, npxCmd, gitCmd };
|
|
33
|
+
return _platform;
|
|
34
|
+
}
|
|
35
|
+
/** 获取 ~/.claude 路径 */
|
|
36
|
+
export function getClaudeDir() {
|
|
37
|
+
return path.join(getPlatform().homeDir, '.claude');
|
|
38
|
+
}
|
|
39
|
+
/** 获取 ~/.codex 路径 */
|
|
40
|
+
export function getCodexDir() {
|
|
41
|
+
return path.join(getPlatform().homeDir, '.codex');
|
|
42
|
+
}
|
|
43
|
+
/** 获取 shell profile 文件路径 */
|
|
44
|
+
export function getShellProfile() {
|
|
45
|
+
const { homeDir, shell, isWindows } = getPlatform();
|
|
46
|
+
if (isWindows) {
|
|
47
|
+
return path.join(homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
48
|
+
}
|
|
49
|
+
switch (shell) {
|
|
50
|
+
case 'zsh': return path.join(homeDir, '.zshrc');
|
|
51
|
+
case 'bash': return path.join(homeDir, '.bashrc');
|
|
52
|
+
default: return path.join(homeDir, '.profile');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ExecResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
code: number | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 同步执行命令,返回结构化结果
|
|
9
|
+
*/
|
|
10
|
+
export declare function execCommand(command: string, options?: {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
maxBuffer?: number;
|
|
15
|
+
}): ExecResult;
|
|
16
|
+
/**
|
|
17
|
+
* 检查命令是否可用
|
|
18
|
+
*/
|
|
19
|
+
export declare function commandExists(cmd: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 异步执行命令(用于长耗时操作)
|
|
22
|
+
*/
|
|
23
|
+
export declare function execAsync(command: string, options?: {
|
|
24
|
+
cwd?: string;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
}): Promise<ExecResult>;
|
|
27
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令执行封装 — child_process 包装
|
|
3
|
+
*/
|
|
4
|
+
import { execSync, spawn } from 'node:child_process';
|
|
5
|
+
import { getPlatform } from './platform.js';
|
|
6
|
+
/**
|
|
7
|
+
* 同步执行命令,返回结构化结果
|
|
8
|
+
*/
|
|
9
|
+
export function execCommand(command, options) {
|
|
10
|
+
const { isWindows } = getPlatform();
|
|
11
|
+
const shellCmd = isWindows ? 'cmd.exe' : '/bin/bash';
|
|
12
|
+
const shellArg = isWindows ? '/c' : '-c';
|
|
13
|
+
try {
|
|
14
|
+
const stdout = execSync(command, {
|
|
15
|
+
cwd: options?.cwd,
|
|
16
|
+
timeout: options?.timeout ?? 120_000,
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
shell: isWindows ? 'cmd.exe' : '/bin/bash',
|
|
19
|
+
env: { ...process.env, ...options?.env },
|
|
20
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
21
|
+
maxBuffer: options?.maxBuffer ?? 10 * 1024 * 1024, // 10MB default
|
|
22
|
+
});
|
|
23
|
+
return { success: true, stdout: stdout.trim(), stderr: '', code: 0 };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
stdout: (err.stdout ?? '').toString().trim(),
|
|
29
|
+
stderr: (err.stderr ?? '').toString().trim(),
|
|
30
|
+
code: err.status ?? 1,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 检查命令是否可用
|
|
36
|
+
*/
|
|
37
|
+
export function commandExists(cmd) {
|
|
38
|
+
const { isWindows } = getPlatform();
|
|
39
|
+
const checkCmd = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
|
|
40
|
+
return execCommand(checkCmd).success;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 异步执行命令(用于长耗时操作)
|
|
44
|
+
*/
|
|
45
|
+
export function execAsync(command, options) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const { isWindows } = getPlatform();
|
|
48
|
+
const shell = isWindows ? 'cmd.exe' : '/bin/bash';
|
|
49
|
+
const shellArg = isWindows ? '/c' : '-c';
|
|
50
|
+
const child = spawn(shell, [shellArg, command], {
|
|
51
|
+
cwd: options?.cwd,
|
|
52
|
+
env: { ...process.env, ...options?.env },
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
58
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
59
|
+
child.on('close', (code) => {
|
|
60
|
+
resolve({
|
|
61
|
+
success: code === 0,
|
|
62
|
+
stdout: stdout.trim(),
|
|
63
|
+
stderr: stderr.trim(),
|
|
64
|
+
code,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
child.on('error', (err) => {
|
|
68
|
+
resolve({
|
|
69
|
+
success: false,
|
|
70
|
+
stdout: '',
|
|
71
|
+
stderr: err.message,
|
|
72
|
+
code: 1,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=shell.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ticketpro-auto-setup",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TicketPro Auto Setup Wizard — 一键配置 Claude Code CLI / Codex CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ticketpro-auto-setup": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"prepublishOnly": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude-code",
|
|
24
|
+
"codex-cli",
|
|
25
|
+
"auto-setup",
|
|
26
|
+
"ticketpro",
|
|
27
|
+
"anthropic",
|
|
28
|
+
"openai",
|
|
29
|
+
"mcp",
|
|
30
|
+
"code-abyss"
|
|
31
|
+
],
|
|
32
|
+
"author": "TicketPro",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/ticketpro/ticketpro-auto-setup"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@inquirer/prompts": "^7.0.0",
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"figlet": "^1.8.0",
|
|
42
|
+
"ora": "^8.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/figlet": "^1.7.0",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"typescript": "^5.7.0"
|
|
48
|
+
}
|
|
49
|
+
}
|