commit b96ed99cc497ecd1a87eb7ab835eb874ab7deb0a Author: Feror Date: Wed Mar 12 08:17:34 2025 +0100 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..7026a7e --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# bun-ssh-portfolio + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.5. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2d90eef --- /dev/null +++ b/bun.lock @@ -0,0 +1,62 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun-ssh-portfolio", + "dependencies": { + "@types/ssh2": "^1.15.4", + "ansi-escapes": "^7.0.0", + "ssh2": "^1.16.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], + + "@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], + + "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + + "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], + + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + + "bun-types/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + + "@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + } +} diff --git a/contact/ContactForm.ts b/contact/ContactForm.ts new file mode 100644 index 0000000..0c2a8d2 --- /dev/null +++ b/contact/ContactForm.ts @@ -0,0 +1,33 @@ +import { newLine } from "../front"; + +interface ContactFormProps { + columns: number; + rows: number; + contactFormData: { + name: string; + email: string; + message: string; + }; + contactFormSelectedField: "name" | "email" | "message" | "submit"; +} + +const ContactForm = ({ rows, columns, contactFormData, contactFormSelectedField }: ContactFormProps) => { + return ( + `${newLine}` + + `Send your message to Jokin here.${newLine}` + + `You can switch between fields with the tab key.${newLine}` + + `Then, send the message by selecting the "Submit" button and hitting enter.${newLine}${newLine}` + + ` ${contactFormSelectedField == "name" ? "\x1b[7mYour name\x1b[0m" : "Your name"}: ${contactFormData.name}${ + contactFormSelectedField == "name" ? "|" : "" + }${newLine}` + + ` ${contactFormSelectedField == "email" ? "\x1b[7mYour email\x1b[0m" : "Your email"}: ${ + contactFormData.email + }${contactFormSelectedField == "email" ? "|" : ""}${newLine}` + + ` ${contactFormSelectedField == "message" ? "\x1b[7mYour message\x1b[0m" : "Your message"}:${newLine} ${ + contactFormData.message + }${contactFormSelectedField == "message" ? "|" : ""}${newLine}` + + `${newLine}` + + ` ${contactFormSelectedField == "submit" ? "\x1b[7mSubmit\x1b[0m" : "Submit"}` + ); +}; +export default ContactForm; diff --git a/front/Categories.ts b/front/Categories.ts new file mode 100644 index 0000000..8608d6c --- /dev/null +++ b/front/Categories.ts @@ -0,0 +1,20 @@ +import { newLine } from "."; + +interface CategoriesProps { + width: number; + height: 3; + selectedTab: "About" | "Projects" | "Contact"; +} + +const Categories = ({ width, height, selectedTab }: CategoriesProps) => { + const categories = ["🇦 about", "🇵 projects", "🇨 contact", "🇶 quit"]; + const separatorSpaces = " ".repeat(width / 2 - categories.join(" | ").length / 2 - 4); + const separatorLine = "─".repeat(width - separatorSpaces.length * 2 - 6 - (width % 2 ? 0 : 1)); + return ( + `${separatorSpaces}┃ ${categories + .map((value) => (value.includes(selectedTab.toLowerCase()) ? `\x1b[7m${value}\x1b[0m` : value)) + .join(" | ")} ┃${separatorSpaces}${newLine}` + `${separatorSpaces}┗${separatorLine}┛${separatorSpaces}` + ); +}; + +export default Categories; diff --git a/front/Header.ts b/front/Header.ts new file mode 100644 index 0000000..9d54f89 --- /dev/null +++ b/front/Header.ts @@ -0,0 +1,20 @@ +import { newLine } from "."; + +interface HeaderProps { + width: number; + height: 3; +} + +const Header = ({ width, height }: HeaderProps) => { + const title = "🔥 Welcome to My Portfolio CLI 🔥"; + const separator = "─".repeat(width / 2 - title.length / 2); + const subTitle = "Made by: Jokin Suares"; + const subTitleSeparator = " ".repeat(width / 2 - subTitle.length / 2); + return ( + `${separator}${title}${separator}${newLine}` + + `${subTitleSeparator}${subTitle}${subTitleSeparator}${newLine}` + + `${"─".repeat(width)}` + ); +}; + +export default Header; diff --git a/front/Tabs/About.ts b/front/Tabs/About.ts new file mode 100644 index 0000000..7787a1a --- /dev/null +++ b/front/Tabs/About.ts @@ -0,0 +1,9 @@ +interface AboutProps { + width: number; + height: number; +} + +const About = ({ width, height }: AboutProps) => { + return "About"; +}; +export default About; diff --git a/front/Tabs/Contact.ts b/front/Tabs/Contact.ts new file mode 100644 index 0000000..b5b1b93 --- /dev/null +++ b/front/Tabs/Contact.ts @@ -0,0 +1,20 @@ +import { newLine } from ".."; +import { link } from "ansi-escapes"; + +interface ContactProps { + width: number; + height: number; +} + +const Contact = ({ width, height }: ContactProps) => { + return ( + `${newLine}` + + ` Email: ${link("contact@jokinsuares.fr", "mailto:contact@jokinsuares.fr")}${newLine}` + + ` Phone: ${link("+33 6 02 26 22 00", "tel:+33602262200")}${newLine}` + + ` GitHub: ${link("Feror-BotMaker", "https://github.com/Feror-BotMaker")}${newLine}` + + ` Discord: ${link("Feror", "https://discordapp.com/users/323206956640763916")}${newLine}` + + ` LinkedIn: ${link("Jokin Suares", "https://www.linkedin.com/in/jokin-suares/")}${newLine}` + + ` ssh: $ ssh contact@jokinsuares.fr${newLine}` + ); +}; +export default Contact; diff --git a/front/Tabs/Projects.ts b/front/Tabs/Projects.ts new file mode 100644 index 0000000..33d42e9 --- /dev/null +++ b/front/Tabs/Projects.ts @@ -0,0 +1,9 @@ +interface ProjectsProps { + width: number; + height: number; +} + +const Projects = ({ width, height }: ProjectsProps) => { + return "Projects"; +}; +export default Projects; diff --git a/front/Tabs/TabRouter.ts b/front/Tabs/TabRouter.ts new file mode 100644 index 0000000..dc9fc6f --- /dev/null +++ b/front/Tabs/TabRouter.ts @@ -0,0 +1,21 @@ +import About from "./About"; +import Contact from "./Contact"; +import Projects from "./Projects"; + +interface TabRouterProps { + width: number; + height: number; + selectedTab: "About" | "Projects" | "Contact"; +} + +const TabRouter = ({ width, height, selectedTab }: TabRouterProps) => { + switch (selectedTab) { + case "About": + return About({ width, height }); + case "Projects": + return Projects({ width, height }); + case "Contact": + return Contact({ width, height }); + } +}; +export default TabRouter; diff --git a/front/index.ts b/front/index.ts new file mode 100644 index 0000000..ce1828b --- /dev/null +++ b/front/index.ts @@ -0,0 +1,34 @@ +import Categories from "./Categories"; +import Header from "./Header"; +import TabRouter from "./Tabs/TabRouter"; + +interface FrontProps { + columns: number; + rows: number; + selectedTab: "About" | "Projects" | "Contact"; +} + +const terminalTooSmall = (width: number, height: number) => { + return ( + `Oh no! Your terminal window is too small!${newLine}` + + `Minimum size required: 60x20` + + `Current size: ${width}x${height}` + ); +}; + +const Front = ({ columns, rows, selectedTab }: FrontProps) => { + if (columns < 60 || rows < 20) { + return terminalTooSmall(columns, rows); + } + return ( + `${Header({ width: columns, height: 3 })}${newLine}` + + `${Categories({ width: columns, height: 3, selectedTab })}${newLine}` + + `${TabRouter({ width: columns, height: 3, selectedTab })}` + ); +}; + +const newLine = "\n\r"; + +export default Front; + +export { newLine }; diff --git a/host.key b/host.key new file mode 100644 index 0000000..ca24ddd --- /dev/null +++ b/host.key @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/host.key.pub b/host.key.pub new file mode 100644 index 0000000..9cfc4a2 --- /dev/null +++ b/host.key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDPXG3OLEYlb53XPbL5cqPpGby+2XIgKc1eIbi/o1XOfAKLvxO7Kwl86s/Uqz4bZwXGqK3N3/QytjXtyg8fsYNasBjAZos3BwX0VkUpdmOkh62jeKG8au6ftX0QC/5DHF4jv6/tFaH3tyLdhAVj2J4XnY2O2u6f3MVWJb8lFxDfhqzED9wggAP96RRwXpnbBV82JGcXhs1i8NPlJuTYMrYAEqwlayMZ7L48rTL7JMRHqryGVf5DZft+4B43f5r/9JUei+qSeJzgyCeM1rK9jT9DukDjFaUA/xS3HeRSj1slyd8iMrbYid8Mu4XR6rYWeZTxjz7D/FXtVyO81/EvVLpVoWINbdeelWo9KiyaCjPsO7PR+mOncfkewxOmau7iCXXdR13eQSJIJ+7DAK4ZF6IUltzjotRrRaMNKPNrthgYoEBE7GYrzsjpCaMqxcj4i2e4P1U495a3t9umv4BW7k12Uu2lsneAYP0xyYxYx4/d81/uyu0+f5/iYKdkZ32MX9c= feror@MBP-de-Feror.local diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..698695e --- /dev/null +++ b/index.ts @@ -0,0 +1,259 @@ +import { Server, type ServerChannel } from "ssh2"; +import Front, { newLine } from "./front"; + +import { eraseScreen, cursorTo, cursorHide, cursorShow } from "ansi-escapes"; +import ContactForm from "./contact/ContactForm"; + +const inputCodes = { + int8: { + backspace: 127, + tab: 9, + enter: 13, + space: 32, + escape: 27, + ctrlC: 3, + a: 97, + p: 112, + c: 99, + q: 113, + }, + base64: { + arrows: { + up: "G1tB", + down: "G1tC", + right: "G1tD", + left: "G1tE", + }, + }, +}; + +const server = new Server( + { + hostKeys: [await Bun.file("./host.key").text()], + ident: "portfolio.sh", + }, + (client) => { + console.log("Client connected!"); + + const userSpecs: { + userName?: string; + } = {}; + + client + .on("authentication", (ctx) => { + userSpecs.userName = ctx.username; + ctx.accept(); // On accepte toutes les connexions + }) + .on("ready", () => { + client.on("session", (accept) => { + const session = accept(); + + const userWindow: { + columns: number; + rows: number; + selectedTab: "About" | "Projects" | "Contact"; + stream: ServerChannel | null; + contactFormData: { + name: string; + email: string; + message: string; + }; + contactFormSelectedField: "name" | "email" | "message" | "submit"; + } = { + columns: 80, + rows: 24, + selectedTab: "About", + stream: null, + contactFormData: { + name: "", + email: "", + message: "", + }, + contactFormSelectedField: "name", + }; + + const render = () => { + userWindow.stream?.write(eraseScreen); + userWindow.stream?.write(cursorTo(0, 0)); + userWindow.stream?.write(cursorHide); + if (userSpecs.userName === "contact") { + userWindow.stream?.write(ContactForm(userWindow)); + return; + } + userWindow.stream?.write(Front(userWindow)); + }; + + session.on("pty", (accept, reject, info) => { + console.log(info); + userWindow.columns = info.cols; + userWindow.rows = info.rows; + accept(); + }); + + session.on("window-change", (_accept, _reject, info) => { + console.log(info); + userWindow.columns = info.cols; + userWindow.rows = info.rows; + render(); + }); + + session.on("shell", (accept) => { + const stream = accept(); + + // stream.write(` + // 🔥 Welcome to My Portfolio CLI 🔥 + // ------------------------------ + // Commands: about | projects | contact | exit + + // `); + + userWindow.stream = stream; + + render(); + + stream.on("data", (data: Buffer) => { + const cmd = data.toString(); + const cmdAsInt = data.readInt8(); + + if ( + cmdAsInt == inputCodes.int8.ctrlC || + (cmdAsInt === inputCodes.int8.q && userSpecs.userName !== "contact") + ) { + userWindow.stream?.write(eraseScreen); + userWindow.stream?.write(cursorTo(0, 0)); + stream.write("Goodbye!\n"); + stream.write(cursorShow); + stream.close(); + } + + if (userSpecs.userName === "contact") { + console.log(cmdAsInt); + if (cmdAsInt == inputCodes.int8.tab) { + if (userWindow.contactFormSelectedField === "name") { + userWindow.contactFormSelectedField = "email"; + } else if (userWindow.contactFormSelectedField === "email") { + userWindow.contactFormSelectedField = "message"; + } else if (userWindow.contactFormSelectedField === "message") { + userWindow.contactFormSelectedField = "submit"; + } else if (userWindow.contactFormSelectedField === "submit") { + userWindow.contactFormSelectedField = "name"; + } + render(); + } + + if (cmdAsInt == inputCodes.int8.backspace) { + if (userWindow.contactFormSelectedField === "name") { + userWindow.contactFormData.name = userWindow.contactFormData.name.slice(0, -1); + } else if (userWindow.contactFormSelectedField === "email") { + userWindow.contactFormData.email = userWindow.contactFormData.email.slice( + 0, + -1 + ); + } else if (userWindow.contactFormSelectedField === "message") { + userWindow.contactFormData.message = userWindow.contactFormData.message.slice( + 0, + -1 + ); + } + render(); + } + + if (cmdAsInt == inputCodes.int8.space) { + if (userWindow.contactFormSelectedField === "name") { + userWindow.contactFormData.name += " "; + } else if (userWindow.contactFormSelectedField === "email") { + userWindow.contactFormData.email += " "; + } else if (userWindow.contactFormSelectedField === "message") { + userWindow.contactFormData.message += " "; + } + render(); + } + + if (cmdAsInt == inputCodes.int8.enter) { + if (userWindow.contactFormSelectedField === "message") { + userWindow.contactFormData.message += newLine; + } else if (userWindow.contactFormSelectedField === "submit") { + const webhookUrl = + "https://discord.com/api/webhooks/1349123668914999361/2rvvgNNRyzgGpKFJ0JLnT-PbkbM-phyQZwTxRBkxDWSA_9SybsP2eILbxsegthsWsu8d"; + const messageData = { + content: `<@323206956640763916>\nName: ${ + userWindow.contactFormData.name + }\nEmail: ${ + userWindow.contactFormData.email + }\nMessage: ${userWindow.contactFormData.message.replace(newLine, "\n")}`, + }; + + fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(messageData), + }) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to send message to Discord webhook"); + } + console.log("Message sent to Discord webhook successfully"); + }) + .catch((error) => { + console.error("Error sending message to Discord webhook:", error); + }); + userWindow.stream?.write(eraseScreen); + userWindow.stream?.write(cursorTo(0, 0)); + stream.write(cursorShow); + userWindow.stream?.write("Message sent!\n"); + stream.close(); + } + render(); + } + + if ( + cmdAsInt != inputCodes.int8.backspace && + cmdAsInt != inputCodes.int8.escape && + cmdAsInt != inputCodes.int8.tab && + cmdAsInt != inputCodes.int8.space && + cmdAsInt != inputCodes.int8.enter + ) { + if (userWindow.contactFormSelectedField === "name") { + userWindow.contactFormData.name += cmd.trim(); + } else if (userWindow.contactFormSelectedField === "email") { + userWindow.contactFormData.email += cmd.trim(); + } else if (userWindow.contactFormSelectedField === "message") { + userWindow.contactFormData.message += cmd.trim(); + } + render(); + } + return; + } + + console.log(cmd); + + switch (cmdAsInt) { + case inputCodes.int8.a: + userWindow.selectedTab = "About"; + render(); + break; + case inputCodes.int8.p: + userWindow.selectedTab = "Projects"; + render(); + break; + case inputCodes.int8.c: + userWindow.selectedTab = "Contact"; + render(); + break; + } + }); + }); + }); + }); + + client.on("end", () => { + console.log("Client disconnected"); + }); + } +); + +server.listen(2222, "0.0.0.0", () => { + console.log("🚀 SSH server running on port 2222"); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3fa027b --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "bun-ssh-portfolio", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@types/ssh2": "^1.15.4", + "ansi-escapes": "^7.0.0", + "ssh2": "^1.16.0" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ab0f0b0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["esnext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}