redeploy-cli 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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # ReDeploy CLI
2
+
3
+ Deploy to the edge from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # npm
9
+ npm install github:revyid/redeploy --save-dev
10
+
11
+ # pnpm
12
+ pnpm add github:revyid/redeploy -D
13
+
14
+ # yarn
15
+ yarn add github:revyid/redeploy --dev
16
+
17
+ # bun
18
+ bun add github:revyid/redeploy --dev
19
+ ```
20
+
21
+ On install, the CLI **auto-detects your package manager** and:
22
+
23
+ 1. Adds scripts to your `package.json`:
24
+ ```json
25
+ {
26
+ "scripts": {
27
+ "deploy": "redeploy deploy",
28
+ "redeploy": "redeploy",
29
+ "redeploy:login": "redeploy login",
30
+ "redeploy:init": "redeploy init",
31
+ "redeploy:whoami": "redeploy whoami",
32
+ "redeploy:logout": "redeploy logout"
33
+ }
34
+ }
35
+ ```
36
+
37
+ 2. Generates `.redeploy.json` config:
38
+ ```json
39
+ {
40
+ "name": "my-project",
41
+ "slug": "my-project",
42
+ "framework": "auto",
43
+ "packageManager": "pnpm",
44
+ "buildCommand": null,
45
+ "env": {}
46
+ }
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```bash
52
+ # npm # pnpm # yarn / bun
53
+ npm run redeploy:login pnpm run redeploy:login yarn redeploy:login
54
+ npm run deploy pnpm run deploy yarn deploy
55
+
56
+ # Or use directly
57
+ npx redeploy deploy pnpm exec redeploy deploy bunx redeploy deploy
58
+ ```
59
+
60
+ ## Commands
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `redeploy login` | Authenticate via browser |
65
+ | `redeploy deploy` | Deploy current directory |
66
+ | `redeploy init` | Create `.redeploy.json` config |
67
+ | `redeploy whoami` | Check auth status |
68
+ | `redeploy logout` | Remove stored credentials |
69
+ | `redeploy config` | Manage CLI configuration |
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ReDeploy CLI
5
+ * Deploy to the edge from your terminal.
6
+ *
7
+ * © ReDeploy — All rights reserved.
8
+ * This software is protected under the MIT License.
9
+ * Unauthorized modification may break CLI functionality.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ require("../src/index.js");
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "redeploy-cli",
3
+ "version": "1.0.0",
4
+ "description": "ReDeploy CLI — Deploy to the edge from your terminal",
5
+ "author": "ReDeploy",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "redeploy": "./bin/redeploy.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/revyid/redeploy-cli.git"
13
+ },
14
+ "main": "./src/index.js",
15
+ "files": [
16
+ "bin/",
17
+ "src/",
18
+ "scripts/",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "scripts": {},
23
+ "dependencies": {
24
+ "chalk": "^5.4.1",
25
+ "commander": "^13.1.0",
26
+ "open": "^10.1.2",
27
+ "ora": "^8.2.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "keywords": [
33
+ "redeploy",
34
+ "deploy",
35
+ "cli",
36
+ "vercel",
37
+ "edge"
38
+ ]
39
+ }
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ReDeploy CLI — Postinstall Script
5
+ * Auto-generates package.json scripts AND .redeploy.json config.
6
+ * Detects package manager (npm, pnpm, yarn, bun) automatically.
7
+ *
8
+ * Install via: npm install github:revyid/redeploy --save-dev
9
+ */
10
+
11
+ "use strict";
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const SCRIPTS_TO_ADD = {
17
+ "deploy": "redeploy deploy",
18
+ "redeploy": "redeploy",
19
+ "redeploy:login": "redeploy login",
20
+ "redeploy:init": "redeploy init",
21
+ "redeploy:whoami": "redeploy whoami",
22
+ "redeploy:logout": "redeploy logout",
23
+ };
24
+
25
+ // ── Package Manager Detection ──────────────────────────
26
+
27
+ function detectPackageManager(projectDir) {
28
+ const checks = [
29
+ { file: "bun.lockb", name: "bun", run: "bun run", exec: "bunx" },
30
+ { file: "bun.lock", name: "bun", run: "bun run", exec: "bunx" },
31
+ { file: "pnpm-lock.yaml", name: "pnpm", run: "pnpm run", exec: "pnpm exec" },
32
+ { file: "yarn.lock", name: "yarn", run: "yarn", exec: "yarn" },
33
+ { file: "package-lock.json", name: "npm", run: "npm run", exec: "npx" },
34
+ ];
35
+
36
+ for (const c of checks) {
37
+ if (fs.existsSync(path.join(projectDir, c.file))) {
38
+ return { name: c.name, run: c.run, exec: c.exec };
39
+ }
40
+ }
41
+
42
+ // Fallback: check npm_config_user_agent env
43
+ const agent = process.env.npm_config_user_agent || "";
44
+ if (agent.includes("pnpm")) return { name: "pnpm", run: "pnpm run", exec: "pnpm exec" };
45
+ if (agent.includes("yarn")) return { name: "yarn", run: "yarn", exec: "yarn" };
46
+ if (agent.includes("bun")) return { name: "bun", run: "bun run", exec: "bunx" };
47
+
48
+ return { name: "npm", run: "npm run", exec: "npx" };
49
+ }
50
+
51
+ // ── Project Root Finder ────────────────────────────────
52
+
53
+ function findProjectRoot() {
54
+ let dir = __dirname;
55
+ for (let i = 0; i < 10; i++) {
56
+ dir = path.dirname(dir);
57
+ const pkgPath = path.join(dir, "package.json");
58
+ if (fs.existsSync(pkgPath)) {
59
+ try {
60
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
61
+ if (pkg.name === "redeploy-cli") continue;
62
+ return { dir, pkgPath, pkg };
63
+ } catch {
64
+ continue;
65
+ }
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // ── Script Injection ───────────────────────────────────
72
+
73
+ function injectScripts(project) {
74
+ const { pkgPath, pkg } = project;
75
+
76
+ if (!pkg.scripts) pkg.scripts = {};
77
+
78
+ let added = 0;
79
+ const skipped = [];
80
+
81
+ for (const [key, value] of Object.entries(SCRIPTS_TO_ADD)) {
82
+ if (pkg.scripts[key] && pkg.scripts[key] !== value) {
83
+ skipped.push(key);
84
+ continue;
85
+ }
86
+ if (pkg.scripts[key] === value) continue;
87
+ pkg.scripts[key] = value;
88
+ added++;
89
+ }
90
+
91
+ if (added > 0) {
92
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
93
+ }
94
+
95
+ return { added, skipped };
96
+ }
97
+
98
+ // ── Config Generation ──────────────────────────────────
99
+
100
+ function generateConfig(project, pm) {
101
+ const configPath = path.join(project.dir, ".redeploy.json");
102
+
103
+ if (fs.existsSync(configPath)) {
104
+ return { created: false, existed: true };
105
+ }
106
+
107
+ const projectName = project.pkg.name || path.basename(project.dir);
108
+ const config = {
109
+ "$schema": "https://deploy.revy.my.id/schema/redeploy.json",
110
+ "name": projectName,
111
+ "slug": projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
112
+ "framework": "auto",
113
+ "packageManager": pm.name,
114
+ "buildCommand": project.pkg.scripts?.build || null,
115
+ "outputDirectory": null,
116
+ "env": {}
117
+ };
118
+
119
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
120
+ return { created: true, existed: false };
121
+ }
122
+
123
+ // ── Main ───────────────────────────────────────────────
124
+
125
+ function main() {
126
+ const project = findProjectRoot();
127
+ if (!project) return;
128
+
129
+ const pm = detectPackageManager(project.dir);
130
+ const scripts = injectScripts(project);
131
+ const config = generateConfig(project, pm);
132
+
133
+ console.log();
134
+ console.log(" \x1b[36m◆\x1b[0m \x1b[1mReDeploy CLI\x1b[0m installed successfully!");
135
+ console.log(" \x1b[2mDetected package manager: \x1b[0m\x1b[1m" + pm.name + "\x1b[0m");
136
+ console.log();
137
+
138
+ if (scripts.added > 0) {
139
+ console.log(" \x1b[32m✓\x1b[0m Added \x1b[1m" + scripts.added + "\x1b[0m script(s) to package.json:");
140
+ for (const [key, value] of Object.entries(SCRIPTS_TO_ADD)) {
141
+ if (!scripts.skipped.includes(key)) {
142
+ console.log(" \x1b[2m" + key + "\x1b[0m → \x1b[36m" + value + "\x1b[0m");
143
+ }
144
+ }
145
+ console.log();
146
+ }
147
+
148
+ if (config.created) {
149
+ console.log(" \x1b[32m✓\x1b[0m Generated \x1b[1m.redeploy.json\x1b[0m config");
150
+ console.log();
151
+ } else if (config.existed) {
152
+ console.log(" \x1b[2m⊘ .redeploy.json already exists — skipped\x1b[0m");
153
+ console.log();
154
+ }
155
+
156
+ console.log(" \x1b[2mQuick start:\x1b[0m");
157
+ console.log(" \x1b[36m" + pm.run + " redeploy:login\x1b[0m — Authenticate via browser");
158
+ console.log(" \x1b[36m" + pm.run + " deploy\x1b[0m — Deploy your project");
159
+ console.log(" \x1b[36m" + pm.exec + " redeploy deploy\x1b[0m — Or use directly");
160
+ console.log();
161
+
162
+ if (scripts.skipped.length > 0) {
163
+ console.log(" \x1b[33m⚠\x1b[0m Skipped scripts (already defined): " + scripts.skipped.join(", "));
164
+ console.log();
165
+ }
166
+ }
167
+
168
+ try {
169
+ main();
170
+ } catch {
171
+ // Fail silently — postinstall should never break install
172
+ }
package/src/auth.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ReDeploy CLI — Browser-based Authentication
3
+ * Opens browser for OAuth, captures token via local HTTP server.
4
+ *
5
+ * @module redeploy/auth
6
+ * @private
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const http = require("http");
12
+ const crypto = require("crypto");
13
+ const config = require("./config");
14
+
15
+ async function login(options = {}) {
16
+ const chalk = (await import("chalk")).default;
17
+ const ora = (await import("ora")).default;
18
+ const open = (await import("open")).default;
19
+
20
+ const existing = config.getToken();
21
+ if (existing && !options.force) {
22
+ const user = config.getUserInfo();
23
+ console.log(chalk.yellow("⚠ Already logged in" + (user ? ` as ${user.email}` : "")));
24
+ console.log(chalk.dim(" Use --force to re-authenticate"));
25
+ return;
26
+ }
27
+
28
+ const baseUrl = options.url || config.getBaseUrl();
29
+ const state = crypto.randomBytes(24).toString("hex");
30
+
31
+ return new Promise((resolve, reject) => {
32
+ const server = http.createServer((req, res) => {
33
+ const url = new URL(req.url, `http://localhost`);
34
+
35
+ if (url.pathname === "/callback") {
36
+ const token = url.searchParams.get("token");
37
+ const returnedState = url.searchParams.get("state");
38
+ const userId = url.searchParams.get("user_id");
39
+ const email = url.searchParams.get("email");
40
+
41
+ // CSRF check
42
+ if (returnedState !== state) {
43
+ res.writeHead(400, { "Content-Type": "text/html" });
44
+ res.end("<html><body style='background:#0a0f15;color:#ff4444;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0'><div><h2>⚠ Security Error</h2><p>State mismatch — please try again.</p></div></body></html>");
45
+ server.close();
46
+ reject(new Error("State mismatch"));
47
+ return;
48
+ }
49
+
50
+ if (token) {
51
+ config.setToken(token);
52
+ if (userId || email) {
53
+ config.setUserInfo({ userId, email });
54
+ }
55
+
56
+ res.writeHead(200, { "Content-Type": "text/html" });
57
+ res.end(`<html><body style='background:#0a0f15;color:#fff;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0'>
58
+ <div style='text-align:center'>
59
+ <div style='width:60px;height:60px;border-radius:50%;background:rgba(19,127,236,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 16px'>
60
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#137fec" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg>
61
+ </div>
62
+ <h2 style='margin:0 0 8px;color:#137fec'>Authenticated!</h2>
63
+ <p style='color:#8b9ab5;margin:0'>You can close this tab and return to your terminal.</p>
64
+ </div>
65
+ </body></html>`);
66
+
67
+ server.close();
68
+ resolve(token);
69
+ } else {
70
+ res.writeHead(400, { "Content-Type": "text/html" });
71
+ res.end("<html><body style='background:#0a0f15;color:#ff4444;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0'><div><h2>⚠ Authentication Failed</h2><p>No token received.</p></div></body></html>");
72
+ server.close();
73
+ reject(new Error("No token received"));
74
+ }
75
+ return;
76
+ }
77
+
78
+ res.writeHead(404);
79
+ res.end();
80
+ });
81
+
82
+ server.listen(0, "127.0.0.1", async () => {
83
+ const port = server.address().port;
84
+ const authUrl = `${baseUrl}/auth/cli?port=${port}&state=${state}`;
85
+
86
+ console.log();
87
+ console.log(chalk.bold(" ReDeploy CLI Authentication"));
88
+ console.log(chalk.dim(" ─────────────────────────────"));
89
+ console.log();
90
+
91
+ const spinner = ora({
92
+ text: "Opening browser for authentication...",
93
+ color: "cyan",
94
+ }).start();
95
+
96
+ try {
97
+ await open(authUrl);
98
+ spinner.text = "Waiting for browser authentication...";
99
+ spinner.color = "yellow";
100
+ } catch {
101
+ spinner.stop();
102
+ console.log(chalk.yellow(" Could not open browser automatically."));
103
+ console.log(chalk.dim(" Open this URL manually:"));
104
+ console.log();
105
+ console.log(chalk.cyan(` ${authUrl}`));
106
+ console.log();
107
+ }
108
+
109
+ // Timeout after 5 minutes
110
+ const timeout = setTimeout(() => {
111
+ server.close();
112
+ spinner?.stop();
113
+ reject(new Error("Authentication timed out (5 minutes)"));
114
+ }, 5 * 60 * 1000);
115
+
116
+ server.on("close", () => {
117
+ clearTimeout(timeout);
118
+ spinner?.stop();
119
+ });
120
+ });
121
+
122
+ server.on("error", (err) => {
123
+ reject(new Error(`Could not start auth server: ${err.message}`));
124
+ });
125
+ });
126
+ }
127
+
128
+ async function logout() {
129
+ const chalk = (await import("chalk")).default;
130
+ config.clearAuth();
131
+ console.log(chalk.green("✓ Logged out successfully."));
132
+ console.log(chalk.dim(` Config cleared: ${config.CONFIG_DIR}`));
133
+ }
134
+
135
+ async function whoami() {
136
+ const chalk = (await import("chalk")).default;
137
+ const ora = (await import("ora")).default;
138
+
139
+ const token = config.getToken();
140
+ if (!token) {
141
+ console.log(chalk.red("✗ Not logged in."));
142
+ console.log(chalk.dim(" Run: redeploy login"));
143
+ return;
144
+ }
145
+
146
+ const spinner = ora("Verifying session...").start();
147
+ const baseUrl = config.getBaseUrl();
148
+
149
+ try {
150
+ const res = await fetch(`${baseUrl}/api/auth/cli/verify`, {
151
+ headers: { Authorization: `Bearer ${token}` },
152
+ });
153
+ const data = await res.json();
154
+
155
+ if (data.valid) {
156
+ spinner.succeed(chalk.green("Authenticated"));
157
+ console.log(chalk.dim(` User ID: ${data.user_id}`));
158
+ if (data.email) console.log(chalk.dim(` Email: ${data.email}`));
159
+ console.log(chalk.dim(` Server: ${baseUrl}`));
160
+ } else {
161
+ spinner.fail(chalk.red("Token expired or invalid"));
162
+ console.log(chalk.dim(" Run: redeploy login"));
163
+ }
164
+ } catch (err) {
165
+ spinner.fail(chalk.red("Could not verify: " + err.message));
166
+ }
167
+ }
168
+
169
+ module.exports = { login, logout, whoami };
package/src/config.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * ReDeploy CLI — Configuration Manager
3
+ * Handles persistent CLI configuration storage.
4
+ *
5
+ * @module redeploy/config
6
+ * @private
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const os = require("os");
14
+ const crypto = require("crypto");
15
+
16
+ const CONFIG_DIR = path.join(os.homedir(), ".redeploy");
17
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
18
+ const SESSION_FILE = path.join(CONFIG_DIR, ".session");
19
+
20
+ // Simple obfuscation for stored tokens (not security, just discouragement)
21
+ const _k = () => crypto.createHash("md5").update(os.hostname() + os.userInfo().username).digest("hex");
22
+
23
+ function _enc(val) {
24
+ const key = _k();
25
+ const iv = crypto.randomBytes(16);
26
+ const cipher = crypto.createCipheriv("aes-256-cbc", crypto.createHash("sha256").update(key).digest(), iv);
27
+ let encrypted = cipher.update(val, "utf8", "hex");
28
+ encrypted += cipher.final("hex");
29
+ return iv.toString("hex") + ":" + encrypted;
30
+ }
31
+
32
+ function _dec(val) {
33
+ try {
34
+ const key = _k();
35
+ const [ivHex, enc] = val.split(":");
36
+ const iv = Buffer.from(ivHex, "hex");
37
+ const decipher = crypto.createDecipheriv("aes-256-cbc", crypto.createHash("sha256").update(key).digest(), iv);
38
+ let decrypted = decipher.update(enc, "hex", "utf8");
39
+ decrypted += decipher.final("utf8");
40
+ return decrypted;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function ensureDir() {
47
+ if (!fs.existsSync(CONFIG_DIR)) {
48
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
49
+ }
50
+ }
51
+
52
+ function readConfig() {
53
+ ensureDir();
54
+ try {
55
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
56
+ } catch {
57
+ return {};
58
+ }
59
+ }
60
+
61
+ function writeConfig(data) {
62
+ ensureDir();
63
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
64
+ }
65
+
66
+ function getToken() {
67
+ const cfg = readConfig();
68
+ if (!cfg._t) return null;
69
+ return _dec(cfg._t);
70
+ }
71
+
72
+ function setToken(token) {
73
+ const cfg = readConfig();
74
+ cfg._t = _enc(token);
75
+ cfg._ts = Date.now();
76
+ writeConfig(cfg);
77
+ }
78
+
79
+ function getBaseUrl() {
80
+ const cfg = readConfig();
81
+ return cfg.baseUrl || "https://deploy.revy.my.id";
82
+ }
83
+
84
+ function setBaseUrl(url) {
85
+ const cfg = readConfig();
86
+ cfg.baseUrl = url;
87
+ writeConfig(cfg);
88
+ }
89
+
90
+ function getProjectConfig(dir) {
91
+ const p = path.join(dir || process.cwd(), ".redeploy.json");
92
+ try {
93
+ return JSON.parse(fs.readFileSync(p, "utf8"));
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ function writeProjectConfig(dir, config) {
100
+ const p = path.join(dir || process.cwd(), ".redeploy.json");
101
+ fs.writeFileSync(p, JSON.stringify(config, null, 2));
102
+ }
103
+
104
+ function clearAuth() {
105
+ const cfg = readConfig();
106
+ delete cfg._t;
107
+ delete cfg._ts;
108
+ delete cfg._u;
109
+ writeConfig(cfg);
110
+ try { fs.unlinkSync(SESSION_FILE); } catch { /* */ }
111
+ }
112
+
113
+ function setUserInfo(info) {
114
+ const cfg = readConfig();
115
+ cfg._u = info;
116
+ writeConfig(cfg);
117
+ }
118
+
119
+ function getUserInfo() {
120
+ const cfg = readConfig();
121
+ return cfg._u || null;
122
+ }
123
+
124
+ module.exports = {
125
+ getToken,
126
+ setToken,
127
+ getBaseUrl,
128
+ setBaseUrl,
129
+ getProjectConfig,
130
+ writeProjectConfig,
131
+ clearAuth,
132
+ setUserInfo,
133
+ getUserInfo,
134
+ readConfig,
135
+ writeConfig,
136
+ CONFIG_DIR,
137
+ };
package/src/deploy.js ADDED
@@ -0,0 +1,502 @@
1
+ /**
2
+ * ReDeploy CLI — Deploy Module
3
+ * Handles project file collection, upload, and live log streaming.
4
+ *
5
+ * @module redeploy/deploy
6
+ * @private
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const config = require("./config");
14
+
15
+ const ALWAYS_IGNORE_DIRS = new Set([
16
+ "node_modules", ".git", ".next", ".vercel", "__pycache__",
17
+ ".DS_Store", ".cache", ".turbo", ".svelte-kit",
18
+ ".nuxt", ".output", "coverage", ".nyc_output", ".redeploy",
19
+ ]);
20
+ const ALWAYS_IGNORE_FILES = new Set([
21
+ ".redeploy.json", ".env.local", ".env.production",
22
+ ]);
23
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB per file
24
+
25
+ // ── .gitignore parser ──────────────────────────────────
26
+
27
+ function parseGitignore(baseDir) {
28
+ const gitignorePath = path.join(baseDir, ".gitignore");
29
+ if (!fs.existsSync(gitignorePath)) return [];
30
+
31
+ const content = fs.readFileSync(gitignorePath, "utf8");
32
+ return content
33
+ .split("\n")
34
+ .map(l => l.trim())
35
+ .filter(l => l && !l.startsWith("#"));
36
+ }
37
+
38
+ function matchesGitignore(relPath, patterns) {
39
+ const normalized = relPath.replace(/\\/g, "/");
40
+ for (const pattern of patterns) {
41
+ const clean = pattern.replace(/^\//, "").replace(/\/$/, "");
42
+
43
+ // Exact match
44
+ if (normalized === clean) return true;
45
+
46
+ // Directory match (pattern ends with /)
47
+ if (pattern.endsWith("/") && normalized.startsWith(clean + "/")) return true;
48
+ if (pattern.endsWith("/") && normalized === clean) return true;
49
+
50
+ // Basename match (no slash in pattern = match anywhere)
51
+ if (!pattern.includes("/")) {
52
+ const basename = path.basename(normalized);
53
+ if (basename === clean) return true;
54
+ // Glob: *.ext
55
+ if (clean.startsWith("*.")) {
56
+ const ext = clean.slice(1);
57
+ if (basename.endsWith(ext)) return true;
58
+ }
59
+ }
60
+
61
+ // Path prefix match
62
+ if (normalized.startsWith(clean + "/")) return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ // ── File collector ─────────────────────────────────────
68
+
69
+ function collectFiles(dir, baseDir, gitignorePatterns, files = []) {
70
+ let entries;
71
+ try {
72
+ entries = fs.readdirSync(dir, { withFileTypes: true });
73
+ } catch {
74
+ return files;
75
+ }
76
+
77
+ for (const entry of entries) {
78
+ const fullPath = path.join(dir, entry.name);
79
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
80
+
81
+ if (entry.isDirectory()) {
82
+ if (ALWAYS_IGNORE_DIRS.has(entry.name)) continue;
83
+ if (entry.name.startsWith(".") && entry.name !== ".env") continue;
84
+ if (matchesGitignore(relPath, gitignorePatterns)) continue;
85
+ collectFiles(fullPath, baseDir, gitignorePatterns, files);
86
+ } else {
87
+ if (ALWAYS_IGNORE_FILES.has(entry.name)) continue;
88
+ if (matchesGitignore(relPath, gitignorePatterns)) continue;
89
+
90
+ try {
91
+ const stat = fs.statSync(fullPath);
92
+ if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
93
+
94
+ files.push({
95
+ file: relPath,
96
+ data: fs.readFileSync(fullPath).toString("base64"),
97
+ size: stat.size,
98
+ });
99
+ } catch {
100
+ continue;
101
+ }
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+
107
+ // ── Framework auto-detection ───────────────────────────
108
+
109
+ function detectFramework(cwd) {
110
+ const pkgPath = path.join(cwd, "package.json");
111
+ if (!fs.existsSync(pkgPath)) {
112
+ if (fs.existsSync(path.join(cwd, "index.html"))) return "static";
113
+ return "other";
114
+ }
115
+
116
+ try {
117
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
118
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
119
+
120
+ if (deps["next"]) return "nextjs";
121
+ if (deps["nuxt"] || deps["nuxt3"]) return "nuxt";
122
+ if (deps["@sveltejs/kit"]) return "sveltekit";
123
+ if (deps["svelte"]) return "svelte";
124
+ if (deps["vue"]) return "vue";
125
+ if (deps["react"]) return "react";
126
+ if (deps["astro"]) return "astro";
127
+ if (deps["vite"]) return "vite";
128
+ return "node";
129
+ } catch {
130
+ return "other";
131
+ }
132
+ }
133
+
134
+ // ── Deploy ─────────────────────────────────────────────
135
+
136
+ async function deploy(options = {}) {
137
+ const chalk = (await import("chalk")).default;
138
+ const ora = (await import("ora")).default;
139
+
140
+ const token = config.getToken();
141
+ if (!token) {
142
+ console.log(chalk.red("\n ✗ Not authenticated. Run: redeploy login\n"));
143
+ process.exit(1);
144
+ }
145
+
146
+ const baseUrl = config.getBaseUrl();
147
+ const cwd = process.cwd();
148
+ const projectConfig = config.getProjectConfig(cwd);
149
+ const projectName = options.name || projectConfig?.name || path.basename(cwd);
150
+ const slug = options.slug || projectConfig?.slug || undefined;
151
+ const framework = detectFramework(cwd);
152
+
153
+ console.log();
154
+ console.log(chalk.bold(" ReDeploy Deploy"));
155
+ console.log(chalk.dim(" ─────────────────────────────"));
156
+ console.log(chalk.dim(` Project: ${chalk.white(projectName)}`));
157
+ console.log(chalk.dim(` Framework: ${chalk.white(framework)}`));
158
+ console.log(chalk.dim(` Dir: ${cwd}`));
159
+ console.log(chalk.dim(` Server: ${baseUrl}`));
160
+ console.log();
161
+
162
+ // Parse .gitignore
163
+ const gitignorePatterns = parseGitignore(cwd);
164
+
165
+ // Collect files
166
+ const spinner = ora("Scanning files...").start();
167
+ const files = collectFiles(cwd, cwd, gitignorePatterns);
168
+
169
+ if (files.length === 0) {
170
+ spinner.fail(chalk.red("No deployable files found"));
171
+ process.exit(1);
172
+ }
173
+
174
+ const totalSize = files.reduce((s, f) => s + f.size, 0);
175
+ spinner.succeed(chalk.green(
176
+ `Found ${files.length} files (${(totalSize / 1024).toFixed(0)} KB)`
177
+ ));
178
+
179
+ if (gitignorePatterns.length > 0) {
180
+ console.log(chalk.dim(` .gitignore: ${gitignorePatterns.length} patterns applied`));
181
+ }
182
+
183
+ // Upload as JSON (no zip — instant)
184
+ const uploadSpinner = ora("Uploading to ReDeploy...").start();
185
+
186
+ try {
187
+ const payload = {
188
+ project_name: projectName,
189
+ framework,
190
+ files: files.map(f => ({ file: f.file, data: f.data })),
191
+ };
192
+ if (slug) payload.slug = slug;
193
+
194
+ // Env vars from .redeploy.json
195
+ if (projectConfig?.env && Object.keys(projectConfig.env).length > 0) {
196
+ payload.env_vars = projectConfig.env;
197
+ }
198
+
199
+ const body = JSON.stringify(payload);
200
+ uploadSpinner.text = `Uploading ${files.length} files (${(Buffer.byteLength(body) / 1024).toFixed(0)} KB)...`;
201
+
202
+ const controller = new AbortController();
203
+ const timeout = setTimeout(() => controller.abort(), 120000); // 120s timeout
204
+
205
+ const res = await fetch(`${baseUrl}/api/deploy/cli`, {
206
+ method: "POST",
207
+ headers: {
208
+ Authorization: `Bearer ${token}`,
209
+ "Content-Type": "application/json",
210
+ },
211
+ body,
212
+ signal: controller.signal,
213
+ });
214
+
215
+ clearTimeout(timeout);
216
+
217
+ if (!res.ok) {
218
+ let errMsg;
219
+ try {
220
+ const errData = await res.json();
221
+ errMsg = errData.error || `HTTP ${res.status}`;
222
+ } catch {
223
+ errMsg = `HTTP ${res.status} ${res.statusText}`;
224
+ }
225
+ uploadSpinner.fail(chalk.red(`Deploy failed: ${errMsg}`));
226
+ process.exit(1);
227
+ }
228
+
229
+ const data = await res.json();
230
+
231
+ if (!data.success) {
232
+ uploadSpinner.fail(chalk.red(`Deploy failed: ${data.error}`));
233
+ process.exit(1);
234
+ }
235
+
236
+ uploadSpinner.succeed(chalk.green("Uploaded successfully"));
237
+
238
+ if (data.framework) {
239
+ console.log(chalk.dim(` Framework: ${data.framework}`));
240
+ }
241
+ console.log(chalk.dim(` Files: ${data.files_count}`));
242
+ if (data.url) {
243
+ console.log(chalk.dim(` URL: ${chalk.cyan(data.url)}`));
244
+ }
245
+ console.log();
246
+
247
+ // Stream build logs
248
+ console.log(chalk.bold.cyan(" ▶ Build Output"));
249
+ console.log(chalk.dim(" ─────────────────────────────"));
250
+
251
+ await streamLogs(baseUrl, token, data.deployment_id, data.project_id, data.vercel_project_id, chalk);
252
+
253
+ } catch (err) {
254
+ if (err.name === "AbortError") {
255
+ uploadSpinner.fail(chalk.red("Upload timed out (120s). Check your network connection."));
256
+ } else {
257
+ uploadSpinner.fail(chalk.red(`Upload failed: ${err.message}`));
258
+ }
259
+ process.exit(1);
260
+ }
261
+ }
262
+
263
+ // ── Stream Logs ────────────────────────────────────────
264
+
265
+ async function streamLogs(baseUrl, token, deploymentId, projectId, vercelProjectId, chalk) {
266
+ let lastLogCount = 0;
267
+ let done = false;
268
+ let retries = 0;
269
+ const maxRetries = 90; // 90 * 2s = 3 minutes max
270
+
271
+ while (!done && retries < maxRetries) {
272
+ await new Promise((r) => setTimeout(r, 2000));
273
+ retries++;
274
+
275
+ // Check status
276
+ try {
277
+ const statusUrl = `${baseUrl}/api/deploy/status?deployment_id=${deploymentId}&project_id=${projectId}&vercel_project_id=${vercelProjectId}`;
278
+ const statusRes = await fetch(statusUrl, {
279
+ headers: { Authorization: `Bearer ${token}` },
280
+ signal: AbortSignal.timeout(10000),
281
+ });
282
+ const status = await statusRes.json();
283
+
284
+ if (status.done) {
285
+ done = true;
286
+
287
+ // Final log fetch
288
+ try {
289
+ const logRes = await fetch(`${baseUrl}/api/deploy/logs?deployment_id=${deploymentId}`, {
290
+ headers: { Authorization: `Bearer ${token}` },
291
+ signal: AbortSignal.timeout(10000),
292
+ });
293
+ const logData = await logRes.json();
294
+ if (logData.logs?.length > lastLogCount) {
295
+ for (let i = lastLogCount; i < logData.logs.length; i++) {
296
+ printLog(logData.logs[i], chalk);
297
+ }
298
+ }
299
+ } catch { /* */ }
300
+
301
+ console.log();
302
+ if (status.status === "ready") {
303
+ console.log(chalk.bold.green(" ✓ Deployment ready!"));
304
+ if (status.url) {
305
+ console.log(chalk.dim(` URL: ${chalk.cyan(status.url)}`));
306
+ }
307
+ } else {
308
+ console.log(chalk.bold.red(` ✗ Build failed: ${status.error_message || "Unknown error"}`));
309
+ process.exit(1);
310
+ }
311
+ console.log();
312
+ return;
313
+ }
314
+ } catch { /* continue polling */ }
315
+
316
+ // Fetch logs
317
+ try {
318
+ const logRes = await fetch(`${baseUrl}/api/deploy/logs?deployment_id=${deploymentId}`, {
319
+ headers: { Authorization: `Bearer ${token}` },
320
+ signal: AbortSignal.timeout(10000),
321
+ });
322
+ const logData = await logRes.json();
323
+ if (logData.logs?.length > lastLogCount) {
324
+ for (let i = lastLogCount; i < logData.logs.length; i++) {
325
+ printLog(logData.logs[i], chalk);
326
+ }
327
+ lastLogCount = logData.logs.length;
328
+ }
329
+ } catch { /* */ }
330
+ }
331
+
332
+ if (!done) {
333
+ console.log();
334
+ console.log(chalk.yellow(" ⚠ Timed out waiting for build. Check the dashboard for status."));
335
+ console.log();
336
+ }
337
+ }
338
+
339
+ function printLog(log, chalk) {
340
+ const msg = log.message || "";
341
+ if (!msg) return;
342
+ const prefix = " ";
343
+ switch (log.level) {
344
+ case "error":
345
+ console.log(prefix + chalk.red(msg));
346
+ break;
347
+ case "warn":
348
+ console.log(prefix + chalk.yellow(msg));
349
+ break;
350
+ case "success":
351
+ console.log(prefix + chalk.green(msg));
352
+ break;
353
+ default:
354
+ console.log(prefix + chalk.dim(msg));
355
+ }
356
+ }
357
+
358
+ // ── Init ───────────────────────────────────────────────
359
+
360
+ async function init(options = {}) {
361
+ const chalk = (await import("chalk")).default;
362
+
363
+ const cwd = process.cwd();
364
+ const pkgPath = path.join(cwd, "package.json");
365
+
366
+ // ── Detect package manager ─────────────────────────
367
+ function detectPM() {
368
+ const checks = [
369
+ { file: "bun.lockb", name: "bun", run: "bun run", exec: "bunx" },
370
+ { file: "bun.lock", name: "bun", run: "bun run", exec: "bunx" },
371
+ { file: "pnpm-lock.yaml", name: "pnpm", run: "pnpm run", exec: "pnpm exec" },
372
+ { file: "yarn.lock", name: "yarn", run: "yarn", exec: "yarn" },
373
+ { file: "package-lock.json", name: "npm", run: "npm run", exec: "npx" },
374
+ ];
375
+ for (const c of checks) {
376
+ if (fs.existsSync(path.join(cwd, c.file))) return c;
377
+ }
378
+ return { name: "npm", run: "npm run", exec: "npx" };
379
+ }
380
+
381
+ const pm = detectPM();
382
+ const framework = detectFramework(cwd);
383
+
384
+ // ── Create .redeploy.json ──────────────────────────
385
+ const existing = config.getProjectConfig(cwd);
386
+ let configCreated = false;
387
+
388
+ if (existing && !options.force) {
389
+ // Config exists, don't overwrite
390
+ } else {
391
+ const projectName = options.name || (fs.existsSync(pkgPath)
392
+ ? JSON.parse(fs.readFileSync(pkgPath, "utf8")).name || path.basename(cwd)
393
+ : path.basename(cwd));
394
+
395
+ const cfg = {
396
+ "$schema": "https://deploy.revy.my.id/schema/redeploy.json",
397
+ name: projectName,
398
+ slug: projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
399
+ framework: framework,
400
+ packageManager: pm.name,
401
+ env: {},
402
+ };
403
+
404
+ config.writeProjectConfig(cwd, cfg);
405
+ configCreated = true;
406
+ }
407
+
408
+ // ── Inject scripts into package.json ───────────────
409
+ const SCRIPTS = {
410
+ "deploy": "redeploy deploy",
411
+ "redeploy": "redeploy",
412
+ "redeploy:login": "redeploy login",
413
+ "redeploy:init": "redeploy init",
414
+ "redeploy:whoami": "redeploy whoami",
415
+ "redeploy:logout": "redeploy logout",
416
+ };
417
+
418
+ let scriptsAdded = 0;
419
+ const skippedScripts = [];
420
+
421
+ if (fs.existsSync(pkgPath)) {
422
+ try {
423
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
424
+ if (!pkg.scripts) pkg.scripts = {};
425
+
426
+ for (const [key, value] of Object.entries(SCRIPTS)) {
427
+ if (pkg.scripts[key] && pkg.scripts[key] !== value) {
428
+ skippedScripts.push(key);
429
+ continue;
430
+ }
431
+ if (pkg.scripts[key] === value) continue;
432
+ pkg.scripts[key] = value;
433
+ scriptsAdded++;
434
+ }
435
+
436
+ if (scriptsAdded > 0) {
437
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
438
+ }
439
+ } catch { /* skip */ }
440
+ }
441
+
442
+ // ── Add .redeploy.json to .gitignore ──────────────
443
+ let gitignoreUpdated = false;
444
+ const gitignorePath = path.join(cwd, ".gitignore");
445
+
446
+ try {
447
+ let content = "";
448
+ if (fs.existsSync(gitignorePath)) {
449
+ content = fs.readFileSync(gitignorePath, "utf8");
450
+ }
451
+
452
+ const entries = [".redeploy.json"];
453
+ const toAdd = entries.filter(e => !content.split("\n").some(line => line.trim() === e));
454
+
455
+ if (toAdd.length > 0) {
456
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
457
+ const header = content.length === 0 ? "" : separator;
458
+ const block = `${header}\n# ReDeploy\n${toAdd.join("\n")}\n`;
459
+ fs.appendFileSync(gitignorePath, block);
460
+ gitignoreUpdated = true;
461
+ }
462
+ } catch { /* skip */ }
463
+
464
+ // ── Print summary ──────────────────────────────────
465
+ console.log();
466
+ console.log(chalk.bold(" ReDeploy Init"));
467
+ console.log(chalk.dim(" ─────────────────────────────"));
468
+ console.log(chalk.dim(` Package manager: ${chalk.white(pm.name)}`));
469
+ console.log(chalk.dim(` Framework: ${chalk.white(framework)}`));
470
+ console.log();
471
+
472
+ if (configCreated) {
473
+ console.log(chalk.green(" ✓ Created .redeploy.json"));
474
+ } else if (existing) {
475
+ console.log(chalk.dim(" ⊘ .redeploy.json already exists (use --force to overwrite)"));
476
+ }
477
+
478
+ if (gitignoreUpdated) {
479
+ console.log(chalk.green(" ✓ Added .redeploy.json to .gitignore"));
480
+ }
481
+
482
+ if (scriptsAdded > 0) {
483
+ console.log(chalk.green(` ✓ Added ${scriptsAdded} script(s) to package.json:`));
484
+ for (const [key, value] of Object.entries(SCRIPTS)) {
485
+ if (!skippedScripts.includes(key)) {
486
+ console.log(chalk.dim(` ${key}`) + " → " + chalk.cyan(value));
487
+ }
488
+ }
489
+ }
490
+
491
+ if (skippedScripts.length > 0) {
492
+ console.log(chalk.yellow(` ⚠ Skipped (already defined): ${skippedScripts.join(", ")}`));
493
+ }
494
+
495
+ console.log();
496
+ console.log(chalk.dim(" Quick start:"));
497
+ console.log(chalk.cyan(` ${pm.run} redeploy:login`) + chalk.dim(" — Authenticate via browser"));
498
+ console.log(chalk.cyan(` ${pm.run} deploy`) + chalk.dim(" — Deploy your project"));
499
+ console.log();
500
+ }
501
+
502
+ module.exports = { deploy, init };
package/src/index.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * ReDeploy CLI
3
+ * Command-line interface for deploying to ReDeploy.
4
+ *
5
+ * @module redeploy
6
+ * @version 1.0.0
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const { Command } = require("commander");
12
+ const { login, logout, whoami } = require("./auth");
13
+ const { deploy, init } = require("./deploy");
14
+ const config = require("./config");
15
+
16
+ const VERSION = process.env.REDEPLOY_CLI_VERSION || "1.0.0";
17
+ const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
18
+
19
+ const program = new Command();
20
+
21
+ program
22
+ .name("redeploy")
23
+ .description("ReDeploy CLI — Deploy to the edge from your terminal")
24
+ .version(VERSION, "-v, --version");
25
+
26
+ // ── Auto-update check ─────────────────────────────────
27
+
28
+ async function checkForUpdate() {
29
+ try {
30
+ const cfg = config.readConfig ? config.readConfig() : {};
31
+ const lastCheck = cfg._lastUpdateCheck || 0;
32
+ if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL) return;
33
+
34
+ const res = await fetch("https://raw.githubusercontent.com/revyid/redeploy-cli/main/package.json", {
35
+ signal: AbortSignal.timeout(3000),
36
+ });
37
+ if (!res.ok) return;
38
+
39
+ const remote = await res.json();
40
+ const remoteVersion = remote.version;
41
+
42
+ // Save last check time
43
+ if (config.writeConfig) {
44
+ const c = config.readConfig ? config.readConfig() : {};
45
+ c._lastUpdateCheck = Date.now();
46
+ config.writeConfig(c);
47
+ }
48
+
49
+ if (remoteVersion && remoteVersion !== VERSION) {
50
+ const chalk = (await import("chalk")).default;
51
+ console.log();
52
+ console.log(chalk.yellow(` ⚠ Update available: ${chalk.dim(VERSION)} → ${chalk.bold(remoteVersion)}`));
53
+ console.log(chalk.dim(" Run: npm install -g github:revyid/redeploy-cli"));
54
+ console.log();
55
+ }
56
+ } catch {
57
+ // Silently ignore — never block CLI usage for update checks
58
+ }
59
+ }
60
+
61
+ // ── login ──────────────────────────────────────────────
62
+ program
63
+ .command("login")
64
+ .description("Authenticate with ReDeploy via browser")
65
+ .option("--force", "Force re-authentication")
66
+ .option("--url <url>", "Custom server URL")
67
+ .action(async (opts) => {
68
+ try {
69
+ const token = await login({ force: opts.force, url: opts.url });
70
+ if (token) {
71
+ const chalk = (await import("chalk")).default;
72
+ console.log(chalk.green("\n ✓ Authentication successful!\n"));
73
+ }
74
+ } catch (err) {
75
+ const chalk = (await import("chalk")).default;
76
+ console.error(chalk.red(`\n ✗ Login failed: ${err.message}\n`));
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ // ── logout ─────────────────────────────────────────────
82
+ program
83
+ .command("logout")
84
+ .description("Remove stored authentication")
85
+ .action(async () => {
86
+ await logout();
87
+ });
88
+
89
+ // ── whoami ─────────────────────────────────────────────
90
+ program
91
+ .command("whoami")
92
+ .description("Check authentication status")
93
+ .action(async () => {
94
+ await whoami();
95
+ });
96
+
97
+ // ── deploy ─────────────────────────────────────────────
98
+ program
99
+ .command("deploy")
100
+ .description("Deploy current directory to ReDeploy")
101
+ .option("-n, --name <name>", "Project name")
102
+ .option("-s, --slug <slug>", "Vercel subdomain slug")
103
+ .action(async (opts) => {
104
+ try {
105
+ await deploy({ name: opts.name, slug: opts.slug });
106
+ } catch (err) {
107
+ const chalk = (await import("chalk")).default;
108
+ console.error(chalk.red(`\n ✗ Deploy failed: ${err.message}\n`));
109
+ process.exit(1);
110
+ }
111
+ });
112
+
113
+ // ── init ───────────────────────────────────────────────
114
+ program
115
+ .command("init")
116
+ .description("Initialize .redeploy.json in current directory")
117
+ .option("-n, --name <name>", "Project name")
118
+ .option("--force", "Overwrite existing config")
119
+ .action(async (opts) => {
120
+ await init({ name: opts.name, force: opts.force });
121
+ });
122
+
123
+ // ── config ─────────────────────────────────────────────
124
+ program
125
+ .command("config")
126
+ .description("Manage CLI configuration")
127
+ .option("--set-url <url>", "Set custom server URL")
128
+ .option("--get-url", "Show current server URL")
129
+ .option("--reset", "Reset all configuration")
130
+ .action(async (opts) => {
131
+ const chalk = (await import("chalk")).default;
132
+
133
+ if (opts.setUrl) {
134
+ config.setBaseUrl(opts.setUrl);
135
+ console.log(chalk.green(`\n ✓ Server URL set to: ${opts.setUrl}\n`));
136
+ } else if (opts.getUrl) {
137
+ console.log(chalk.dim(`\n Server: ${config.getBaseUrl()}\n`));
138
+ } else if (opts.reset) {
139
+ config.clearAuth();
140
+ console.log(chalk.green("\n ✓ Configuration reset.\n"));
141
+ } else {
142
+ console.log(chalk.dim(`\n Server: ${config.getBaseUrl()}`));
143
+ console.log(chalk.dim(` Config: ${config.CONFIG_DIR}\n`));
144
+ }
145
+ });
146
+
147
+ // ── Run ────────────────────────────────────────────────
148
+
149
+ async function main() {
150
+ // Check for updates in background (non-blocking)
151
+ await checkForUpdate();
152
+ program.parse(process.argv);
153
+
154
+ // Show help if no command
155
+ if (!process.argv.slice(2).length) {
156
+ program.outputHelp();
157
+ }
158
+ }
159
+
160
+ main();