@danhachuel/thunderbolt 0.2.13

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.
@@ -0,0 +1,115 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, copyFileSync, readdirSync } from "node:fs";
3
+ import { homedir, platform } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
8
+ const args = process.argv.slice(2);
9
+
10
+ function getUserHome() {
11
+ if (platform() !== "win32") return homedir();
12
+ const driveHome = process.env.HOMEDRIVE && process.env.HOMEPATH ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` : null;
13
+ return process.env.USERPROFILE || driveHome || homedir();
14
+ }
15
+
16
+ const userHome = getUserHome();
17
+ const defaultThunderboltHome = platform() === "win32"
18
+ ? join(process.env.LOCALAPPDATA || join(userHome, "AppData", "Local"), "THUNDERBOLT")
19
+ : join(userHome, ".thunderbolt");
20
+ const thunderboltHome = resolve(process.env.THUNDERBOLT_HOME || defaultThunderboltHome);
21
+ const venvDir = process.env.THUNDERBOLT_VENV || process.env.HERMES_VENV || join(thunderboltHome, ".venv");
22
+ const venvPython = platform() === "win32" ? join(venvDir, "Scripts", "python.exe") : join(venvDir, "bin", "python");
23
+ const python = process.env.THUNDERBOLT_PYTHON || process.env.HERMES_PYTHON || (existsSync(venvPython) ? venvPython : (platform() === "win32" ? "python" : "python3"));
24
+ const main = resolve(root, "app", "main.py");
25
+ const settingsPath = join(thunderboltHome, "storage", "state", "settings.json");
26
+
27
+ function run(command, commandArgs) {
28
+ const result = spawnSync(command, commandArgs, { stdio: "inherit", env: process.env });
29
+ process.exit(result.status ?? 1);
30
+ }
31
+
32
+ function pythonVersion() {
33
+ return spawnSync(python, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"], { encoding: "utf8" });
34
+ }
35
+
36
+ function moduleAvailable(moduleName) {
37
+ const code = `import importlib.util,sys; sys.exit(0 if importlib.util.find_spec(${JSON.stringify(moduleName)}) else 1)`;
38
+ return spawnSync(python, ["-c", code], { stdio: "ignore" }).status === 0;
39
+ }
40
+
41
+ function configuredMoneyPrinterPath() {
42
+ if (!existsSync(settingsPath)) return join(thunderboltHome, "MoneyPrinterTurbo");
43
+ try {
44
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
45
+ return settings.moneyprinter_path || join(thunderboltHome, "MoneyPrinterTurbo");
46
+ } catch {
47
+ return join(thunderboltHome, "MoneyPrinterTurbo");
48
+ }
49
+ }
50
+
51
+ function ensureRuntimeStorage() {
52
+ const storageRoot = process.env.THUNDERBOLT_STORAGE_DIR || join(thunderboltHome, "storage");
53
+ const directories = [
54
+ storageRoot,
55
+ join(storageRoot, "state"),
56
+ join(storageRoot, "blueprints"),
57
+ join(storageRoot, "blueprints", "canais"),
58
+ join(storageRoot, "blueprints", "nichos"),
59
+ join(storageRoot, "blueprints", "importados"),
60
+ join(storageRoot, "blueprints", "brandings"),
61
+ join(storageRoot, "metadata_cleaner"),
62
+ join(storageRoot, "metadata_cleaner", "originals"),
63
+ join(storageRoot, "metadata_cleaner", "outputs"),
64
+ ];
65
+ for (const directory of directories) mkdirSync(directory, { recursive: true });
66
+ const seedRoot = resolve(root, "seed", "blueprints");
67
+ const destination = join(storageRoot, "blueprints", "importados");
68
+ if (existsSync(seedRoot)) {
69
+ for (const filename of readdirSync(seedRoot)) {
70
+ if (!filename.endsWith(".json")) continue;
71
+ const target = join(destination, filename);
72
+ if (!existsSync(target)) copyFileSync(join(seedRoot, filename), target);
73
+ }
74
+ }
75
+ }
76
+
77
+ function check() {
78
+ const version = pythonVersion();
79
+ if (version.status !== 0) {
80
+ console.error(`Python não encontrado. Execute: npx.cmd --yes @danhachuel/thunderbolt install`);
81
+ process.exit(1);
82
+ }
83
+ const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg"];
84
+ const missing = requiredModules.filter((moduleName) => !moduleAvailable(moduleName));
85
+ const ffmpeg = moduleAvailable("imageio_ffmpeg");
86
+ const mptPath = configuredMoneyPrinterPath();
87
+ const mptReady = existsSync(join(mptPath, "requirements.txt")) || existsSync(join(mptPath, "pyproject.toml"));
88
+ console.log(`Thunderbolt: ${thunderboltHome}`);
89
+ console.log(`Python: ${version.stdout.trim()}`);
90
+ console.log(`Dependências da aplicação: ${missing.length ? `em falta (${missing.join(", ")})` : "OK"}`);
91
+ console.log(`FFmpeg: ${ffmpeg ? "OK via imageio-ffmpeg" : "em falta"}`);
92
+ console.log(`Motor de vídeo: ${mptReady ? `OK (${mptPath})` : `não encontrado (${mptPath})`}`);
93
+ if (missing.length || !ffmpeg) {
94
+ console.error("Instalação incompleta. Execute `npx.cmd --yes @danhachuel/thunderbolt install`; dependências já válidas serão reutilizadas.");
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ if (args[0] === "install") run(process.execPath, [resolve(root, "scripts", "install.mjs"), ...args.slice(1)]);
100
+ if (args[0] === "doctor" || args.includes("--check")) {
101
+ check();
102
+ process.exit(0);
103
+ }
104
+ if (!existsSync(main)) {
105
+ console.error(`Entrada não encontrada: ${main}`);
106
+ process.exit(1);
107
+ }
108
+ ensureRuntimeStorage();
109
+ const port = process.env.THUNDERBOLT_PORT || process.env.HERMES_PORT || "3030";
110
+ const child = spawn(python, ["-m", "streamlit", "run", main, "--server.port", port, "--server.address", "localhost"], {
111
+ cwd: root,
112
+ stdio: "inherit",
113
+ env: { ...process.env, THUNDERBOLT_STORAGE_DIR: process.env.THUNDERBOLT_STORAGE_DIR || join(thunderboltHome, "storage") },
114
+ });
115
+ child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
@@ -0,0 +1,315 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, renameSync, cpSync, copyFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { homedir, platform } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+
8
+ const root = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
9
+ const args = process.argv.slice(2);
10
+
11
+ function getUserHome() {
12
+ if (platform() !== "win32") return homedir();
13
+ const driveHome = process.env.HOMEDRIVE && process.env.HOMEPATH ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` : null;
14
+ const candidates = [process.env.USERPROFILE, driveHome, homedir()].filter(Boolean);
15
+ const slash = String.fromCharCode(92);
16
+ for (const candidate of candidates) {
17
+ const normalized = candidate.replaceAll("/", slash);
18
+ const lower = normalized.toLowerCase();
19
+ const userMarker = `${slash}users${slash}`;
20
+ const suffixes = [
21
+ `${slash}appdata${slash}local${slash}hermes`,
22
+ `${slash}appdata${slash}local${slash}hermes-ui`,
23
+ `${slash}appdata${slash}roaming${slash}mobaxterm${slash}home`,
24
+ ];
25
+ const suffix = suffixes.find((value) => lower.endsWith(value));
26
+ if (suffix && lower.includes(userMarker)) return normalized.slice(0, -suffix.length);
27
+ if (lower.includes(userMarker)) return normalized;
28
+ }
29
+ return candidates[0] || homedir();
30
+ }
31
+
32
+ const home = getUserHome();
33
+ const explicitThunderboltHome = process.env.THUNDERBOLT_HOME || "";
34
+ const defaultThunderboltHome = platform() === "win32"
35
+ ? join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), "THUNDERBOLT")
36
+ : join(home, ".thunderbolt");
37
+ const thunderboltHome = resolve(explicitThunderboltHome || defaultThunderboltHome);
38
+ const venvPath = process.env.THUNDERBOLT_VENV || process.env.HERMES_VENV || join(thunderboltHome, ".venv");
39
+ const pythonBin = platform() === "win32" ? join(venvPath, "Scripts", "python.exe") : join(venvPath, "bin", "python");
40
+ const defaultMpt = process.env.MONEYPRINTER_PATH || join(thunderboltHome, "MoneyPrinterTurbo");
41
+ const dependencyStatePath = join(thunderboltHome, "storage", "state", "install-state.json");
42
+ const forceDeps = args.includes("--force-deps");
43
+ const refreshMoneyPrinter = args.includes("--refresh-moneyprinter");
44
+
45
+ function legacyRoots() {
46
+ const userProfile = process.env.USERPROFILE || home;
47
+ const localAppData = process.env.LOCALAPPDATA || join(userProfile, "AppData", "Local");
48
+ const roots = [
49
+ join(home, "Hermes-UI"),
50
+ join(localAppData, "hermes"),
51
+ join(localAppData, "Hermes-UI"),
52
+ join(userProfile, ".content-hermes"),
53
+ join(userProfile, "hermes"),
54
+ ];
55
+ return [...new Set(roots.map((candidate) => resolve(candidate)))].filter((candidate) => candidate !== thunderboltHome);
56
+ }
57
+
58
+ function commandExists(command) {
59
+ const result = spawnSync(platform() === "win32" ? "where" : "which", [command], { stdio: "ignore" });
60
+ return result.status === 0;
61
+ }
62
+
63
+ function run(command, commandArgs, options = {}) {
64
+ console.log(`\n> ${command} ${commandArgs.join(" ")}`);
65
+ const result = spawnSync(command, commandArgs, { stdio: "inherit", ...options });
66
+ if (result.status !== 0) process.exit(result.status || 1);
67
+ }
68
+
69
+ function probePython(command, commandArgs = []) {
70
+ const result = spawnSync(command, [...commandArgs, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"], { encoding: "utf8" });
71
+ if (result.status !== 0) return null;
72
+ const versionText = result.stdout.trim();
73
+ const version = versionText.split(".").map(Number);
74
+ if (version[0] > 3 || (version[0] === 3 && version[1] >= 11)) return { command, args: commandArgs, version: versionText };
75
+ return null;
76
+ }
77
+
78
+ function findPython() {
79
+ const candidates = [];
80
+ if (existsSync(pythonBin)) candidates.push({ command: pythonBin, args: [] });
81
+ if (process.env.THUNDERBOLT_PYTHON || process.env.HERMES_PYTHON) {
82
+ candidates.push({ command: process.env.THUNDERBOLT_PYTHON || process.env.HERMES_PYTHON, args: [] });
83
+ }
84
+ candidates.push(...(platform() === "win32"
85
+ ? [{ command: "py", args: ["-3.11"] }, { command: "py", args: [] }, { command: "python", args: [] }, { command: "python3", args: [] }]
86
+ : [{ command: "python3.11", args: [] }, { command: "python3", args: [] }, { command: "python", args: [] }]));
87
+ for (const candidate of candidates) {
88
+ const found = probePython(candidate.command, candidate.args);
89
+ if (found) return found;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function ensureDirs() {
95
+ const storageRoot = join(thunderboltHome, "storage");
96
+ const directories = [
97
+ thunderboltHome,
98
+ storageRoot,
99
+ join(storageRoot, "state"),
100
+ join(storageRoot, "blueprints"),
101
+ join(storageRoot, "blueprints", "canais"),
102
+ join(storageRoot, "blueprints", "nichos"),
103
+ join(storageRoot, "blueprints", "importados"),
104
+ join(storageRoot, "blueprints", "brandings"),
105
+ join(storageRoot, "metadata_cleaner"),
106
+ join(storageRoot, "metadata_cleaner", "originals"),
107
+ join(storageRoot, "metadata_cleaner", "outputs"),
108
+ join(storageRoot, "artifacts"),
109
+ ];
110
+ for (const directory of directories) mkdirSync(directory, { recursive: true });
111
+ copySeedBlueprints(storageRoot);
112
+ }
113
+
114
+ function copySeedBlueprints(storageRoot) {
115
+ const seedRoot = join(root, "seed", "blueprints");
116
+ const destination = join(storageRoot, "blueprints", "importados");
117
+ if (!existsSync(seedRoot)) return;
118
+ for (const filename of readdirSync(seedRoot)) {
119
+ if (!filename.endsWith(".json")) continue;
120
+ const source = join(seedRoot, filename);
121
+ const target = join(destination, filename);
122
+ if (!existsSync(target)) copyFileSync(source, target);
123
+ }
124
+ }
125
+
126
+ function copyOrMove(source, target, label) {
127
+ if (!existsSync(source) || existsSync(target)) return false;
128
+ mkdirSync(dirname(target), { recursive: true });
129
+ try {
130
+ renameSync(source, target);
131
+ console.log(`Migração concluída: ${label} -> ${target}`);
132
+ } catch {
133
+ cpSync(source, target, { recursive: true });
134
+ console.log(`Dados copiados para Thunderbolt: ${label} -> ${target}`);
135
+ }
136
+ return true;
137
+ }
138
+
139
+ function migrateLegacyInstallation() {
140
+ if (explicitThunderboltHome) return;
141
+ const candidates = legacyRoots();
142
+ const legacy = candidates.find((candidate) => existsSync(join(candidate, "storage")) || existsSync(join(candidate, ".venv")) || existsSync(join(candidate, "MoneyPrinterTurbo")));
143
+ if (!legacy) return;
144
+ console.warn(`Foi encontrada uma instalação antiga em ${legacy}. O Thunderbolt usará ${thunderboltHome}.`);
145
+ copyOrMove(join(legacy, "storage"), join(thunderboltHome, "storage"), "storage legado");
146
+ copyOrMove(join(legacy, ".venv"), join(thunderboltHome, ".venv"), "ambiente Python legado");
147
+ copyOrMove(join(legacy, "MoneyPrinterTurbo"), join(thunderboltHome, "MoneyPrinterTurbo"), "MoneyPrinterTurbo legado");
148
+ console.log("A pasta legada não será usada pelo Thunderbolt; foi preservada quando a cópia foi necessária.");
149
+ }
150
+
151
+ function removePath(path) {
152
+ if (!existsSync(path)) return;
153
+ console.log(`A remover componente técnico: ${path}`);
154
+ try {
155
+ rmSync(path, { recursive: true, force: true });
156
+ } catch (error) {
157
+ console.error(`Não foi possível remover ${path}: ${error.message}`);
158
+ console.error("Feche processos Thunderbolt/Node/Python que estejam a usar a pasta e execute novamente.");
159
+ process.exit(1);
160
+ }
161
+ }
162
+
163
+ function containsUserData(path) {
164
+ return [
165
+ join(path, "storage", "blueprints"),
166
+ join(path, "storage", "brandings"),
167
+ join(path, "storage", "state"),
168
+ ].some((candidate) => existsSync(candidate));
169
+ }
170
+
171
+ function cleanInstallationRoots(moneyprinterPath) {
172
+ if (args.includes("--purge-data")) {
173
+ console.warn("ATENÇÃO: --purge-data apaga Blueprints, Brandings, configurações, storage e artefactos locais.");
174
+ removePath(thunderboltHome);
175
+ return;
176
+ }
177
+
178
+ if (refreshMoneyPrinter && resolve(moneyprinterPath).startsWith(thunderboltHome)) removePath(moneyprinterPath);
179
+ for (const legacyRoot of legacyRoots()) {
180
+ if (existsSync(legacyRoot) && containsUserData(legacyRoot)) {
181
+ console.warn(`Instalação antiga com dados preservada para revisão manual: ${legacyRoot}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ function installPythonWindows() {
187
+ if (process.env.THUNDERBOLT_SKIP_PYTHON_INSTALL === "1" || process.env.HERMES_SKIP_PYTHON_INSTALL === "1") return null;
188
+ if (!commandExists("winget")) {
189
+ console.error("Python 3.11+ não foi encontrado e o winget também não está disponível.");
190
+ console.error("Instale Python 3.11+ a partir de https://www.python.org/downloads/windows/ ou instale o App Installer da Microsoft para obter o winget.");
191
+ console.error("Depois execute novamente: npx.cmd --yes @danhachuel/thunderbolt install");
192
+ process.exit(1);
193
+ }
194
+ console.log("Python 3.11+ não encontrado. A instalar Python automaticamente através do winget...");
195
+ const result = spawnSync("winget", ["install", "--exact", "--id", "Python.Python.3.11", "--source", "winget", "--scope", "user", "--accept-source-agreements", "--accept-package-agreements", "--silent"], { stdio: "inherit" });
196
+ if (result.status !== 0) process.exit(result.status || 1);
197
+ const found = findPython();
198
+ if (!found) {
199
+ console.error("Python foi instalado, mas o terminal actual ainda não encontrou o comando. Feche e reabra o terminal e repita.");
200
+ process.exit(1);
201
+ }
202
+ return found;
203
+ }
204
+
205
+ function ensurePython() {
206
+ let found = findPython();
207
+ if (found) {
208
+ console.log(`Python compatível encontrado: ${found.command} ${found.args.join(" ")} (${found.version})`);
209
+ return found;
210
+ }
211
+ if (existsSync(venvPath)) removePath(venvPath);
212
+ found = findPython();
213
+ if (!found && platform() === "win32") found = installPythonWindows();
214
+ if (!found) {
215
+ console.error("Python 3.11 ou superior não foi encontrado. Instale Python 3.11+ e execute novamente.");
216
+ process.exit(1);
217
+ }
218
+ return found;
219
+ }
220
+
221
+ function cloneMoneyPrinter(path) {
222
+ if (existsSync(join(path, ".git")) || existsSync(join(path, "pyproject.toml"))) {
223
+ console.log(`MoneyPrinterTurbo já existe em ${path}; será reutilizado.`);
224
+ return;
225
+ }
226
+ if (!commandExists("git")) {
227
+ console.error("Git não encontrado. Instale Git ou defina MONEYPRINTER_PATH para uma cópia local do MoneyPrinterTurbo.");
228
+ process.exit(1);
229
+ }
230
+ const destination = resolve(path);
231
+ const parent = resolve(destination, "..");
232
+ mkdirSync(parent, { recursive: true });
233
+ run("git", ["clone", "--depth", "1", "https://github.com/harry0703/MoneyPrinterTurbo.git", basename(destination)], { cwd: parent });
234
+ }
235
+
236
+ function fileHash(path) {
237
+ if (!existsSync(path)) return "missing";
238
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
239
+ }
240
+
241
+ function readDependencyState() {
242
+ if (!existsSync(dependencyStatePath)) return {};
243
+ try { return JSON.parse(readFileSync(dependencyStatePath, "utf8")); } catch { return {}; }
244
+ }
245
+
246
+ function writeDependencyState(updates) {
247
+ mkdirSync(dirname(dependencyStatePath), { recursive: true });
248
+ const state = { ...readDependencyState(), ...updates, updated_at: new Date().toISOString() };
249
+ writeFileSync(dependencyStatePath, JSON.stringify(state, null, 2) + "\n", "utf8");
250
+ }
251
+
252
+ function importsAvailable(modules) {
253
+ if (!existsSync(pythonBin)) return false;
254
+ const code = `import importlib.util,sys; missing=[m for m in ${JSON.stringify(modules)} if importlib.util.find_spec(m) is None]; sys.exit(1 if missing else 0)`;
255
+ return spawnSync(pythonBin, ["-c", code], { stdio: "ignore" }).status === 0;
256
+ }
257
+
258
+ function installRequirementIfNeeded(requirementsPath, stateKey, modules, label) {
259
+ if (!existsSync(requirementsPath)) return;
260
+ const currentHash = fileHash(requirementsPath);
261
+ const state = readDependencyState();
262
+ const importsOk = importsAvailable(modules);
263
+ const hashMatches = state[stateKey] === currentHash;
264
+ if (!forceDeps && importsOk && (hashMatches || !state[stateKey])) {
265
+ console.log(`${label}: dependências detectadas e reutilizadas; nenhuma reinstalação necessária.`);
266
+ writeDependencyState({ [stateKey]: currentHash });
267
+ return;
268
+ }
269
+ console.log(`${label}: dependências ausentes, incompletas ou alteradas; a instalar agora.`);
270
+ run(pythonBin, ["-m", "pip", "install", "-r", requirementsPath]);
271
+ writeDependencyState({ [stateKey]: currentHash });
272
+ }
273
+
274
+ function writeSettings(moneyprinterPath) {
275
+ const stateDir = join(thunderboltHome, "storage", "state");
276
+ mkdirSync(stateDir, { recursive: true });
277
+ const settingsPath = join(stateDir, "settings.json");
278
+ let settings = {};
279
+ if (existsSync(settingsPath)) {
280
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch { settings = {}; }
281
+ }
282
+ settings.moneyprinter_path = moneyprinterPath;
283
+ settings.port = Number(process.env.THUNDERBOLT_PORT || process.env.HERMES_PORT || settings.port || 3030);
284
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
285
+ }
286
+
287
+ function installThunderboltDependencies(python) {
288
+ if (!existsSync(pythonBin)) run(python.command, [...python.args, "-m", "venv", venvPath]);
289
+ installRequirementIfNeeded(join(root, "requirements.txt"), "thunderbolt_requirements_sha256", ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg"], "Thunderbolt");
290
+ }
291
+
292
+ function installMoneyPrinterDependencies(moneyprinterPath) {
293
+ installRequirementIfNeeded(join(moneyprinterPath, "requirements.txt"), "moneyprinter_requirements_sha256", ["fastapi", "moviepy", "PIL", "numpy", "requests"], "MoneyPrinterTurbo");
294
+ }
295
+
296
+ function main() {
297
+ const skipMpt = args.includes("--skip-moneyprinter");
298
+ const skipDeps = args.includes("--skip-python-deps");
299
+ const moneyprinterPath = process.env.MONEYPRINTER_PATH || defaultMpt;
300
+ migrateLegacyInstallation();
301
+ cleanInstallationRoots(moneyprinterPath);
302
+ ensureDirs();
303
+ const python = skipDeps ? null : ensurePython();
304
+ if (!skipMpt) cloneMoneyPrinter(moneyprinterPath);
305
+ if (!skipDeps) installThunderboltDependencies(python);
306
+ if (!skipDeps && !skipMpt) installMoneyPrinterDependencies(moneyprinterPath);
307
+ writeSettings(moneyprinterPath);
308
+ console.log("\nInstalação do Thunderbolt concluída.");
309
+ console.log(`Pasta Thunderbolt: ${thunderboltHome}`);
310
+ console.log(`Ambiente Python: ${venvPath}`);
311
+ console.log(`Motor de vídeo: ${moneyprinterPath}`);
312
+ console.log("Execute `npx.cmd --yes @danhachuel/thunderbolt` no Windows ou `npx --yes @danhachuel/thunderbolt` noutros sistemas.");
313
+ }
314
+
315
+ main();