Compare commits

..

No commits in common. "6a7e500b8af063cd2dc8142d1be9853085f2f369" and "d4329ae706f0be7bd061465f478c65f739fcf98a" have entirely different histories.

5 changed files with 100 additions and 474 deletions

4
.gitignore vendored
View File

@ -1,5 +1,3 @@
node_modules/ node_modules/
yarn.lock yarn.lock
package.json.lock package.json.lock
.env
.ngenv

122
README.md
View File

@ -1,121 +1,3 @@
# Ngenv # ngrok-to-dotenv
Ngenv is a Node.js CLI tool that enhances development workflows by seamlessly integrating ngrok tunnels. It simplifies the process of starting an ngrok tunnel and automatically updates your project's `.env` file with the newly generated ngrok URL, ensuring your environment variables are always synchronized with ngrok's dynamic URLs. This tool is invaluable for developers working with webhooks, APIs, and external services that require secure, reliable tunneling to localhost. This is a ngrok start-up wrapper that uses a dotenv file (at '../.env' relative to this project's folder) to get the authtoken and save the https address; it's a node cli tool that you can use with projects that need ngrok. You clone it into your project's folder, and add to the project's package.json a run script to call "cd ngrok-to-dotenv && node ngrok". Be sure to add the folder to your .gitignore and install the one package which is ngrok and command-line-args, using 'npm i' or 'yarn'.
## Features
- **Automation:** Starts an ngrok tunnel and updates the `.env` file without manual intervention.
- **Flexibility:** Supports running ngrok in both foreground for direct interaction and background for uninterrupted terminal use.
- **Customization:** Allows for custom ngrok configurations, including protocol, port, and `.env` file path specifications.
- **Log Management:** Provides utility commands for managing ngrok process logs in background mode.
## Prerequisites
- Node.js (Version 10 or newer)
- An ngrok account and authtoken, obtainable by signing up at [ngrok.com](https://ngrok.com/).
## Installation
1. **Global Installation:**
Install Ngenv globally with npm:
```sh
npm install -g ngenv
```
2. **Configure ngrok:**
Set up your ngrok with your authtoken:
```sh
ngrok authtoken <your-ngrok-authtoken>
```
## Usage
### Running ngrok in the Foreground
The `run` command initiates the ngrok tunnel in the foreground, maintaining it active in the current terminal session for immediate feedback.
```sh
ngenv run
```
### Starting the ngrok Tunnel in the Background
The `start` command launches the ngrok tunnel as a background task, ensuring only one instance runs at a time, freeing up the terminal.
```sh
ngenv start
```
In background mode, ngenv handles log files for output and errors, aiding in debugging and monitoring.
### Stopping the ngrok Tunnel
Terminate a background ngrok tunnel with the `stop` command:
```sh
ngenv stop
```
### Viewing Logs
Inspect output and error logs (only applicable in background mode):
```sh
ngenv logs
```
### Clearing Logs
Remove log files associated with background processes:
```sh
ngenv clear
```
## Command Options (start and run)
- `-P, --proto <protocol>`: Specify the protocol (default: http).
- `-p, --port <port>`: Set the port for ngrok to forward (default: 3000).
- `-e, --env <path>`: Define a custom path to your `.env` file.
## Examples
**Foreground operation on port 8080:**
```sh
ngenv run --port 8080
```
**Background operation with HTTPS protocol:**
```sh
ngenv start --proto https
```
**Custom `.env` file path in background mode:**
```sh
ngenv start --env /path/to/your/.env
```
## Troubleshooting
- Ensure ngrok is properly installed and your authtoken is configured.
- Check permissions for updating `.env` files and managing processes.
- In background mode, consult log files for potential issues.
## Contributing
We welcome contributions! Feel free to open issues or submit pull requests with your ideas, bug fixes, or features.
## License
Ngenv is available under the MIT License. See the LICENSE file for more details.
---
For more information on ngrok, please visit [ngrok's official documentation](https://ngrok.com/docs).

344
ngenv
View File

@ -1,344 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const os = require("os");
const path = require("path");
const { spawn, exec } = require("child_process");
const commandLineArgs = require("command-line-args");
const ngrok = require("ngrok");
const ngrokConfigFile = getNgrokConfig();
if (!ngrokConfigFile) {
console.error(
"No ngrok config file found! Run 'ngrok authtoken <token>' in your terminal."
);
process.exit(1);
}
const projectFolder = findProjectFolder();
if (!projectFolder) {
console.error(
"No project folder found! Make sure there is a .env file in the project folder."
);
process.exit(1);
}
if (!fs.existsSync(`${projectFolder}/.ngenv`)) {
fs.mkdirSync(`${projectFolder}/.ngenv`);
}
const lockFile = `${projectFolder}/.ngenv/running.lock`;
const outLogFile = `${projectFolder}/.ngenv/out.log`;
const errLogFile = `${projectFolder}/.ngenv/err.log`;
let authToken = null; // string || null
function isAlreadyRunning() {
return fs.existsSync(lockFile);
}
function createLockFile(pid) {
fs.writeFileSync(lockFile, pid.toString());
}
function removeLockFile() {
if (isAlreadyRunning()) {
fs.unlinkSync(lockFile);
}
}
function removeLogFiles() {
if (fs.existsSync(outLogFile)) {
fs.unlinkSync(outLogFile);
}
if (fs.existsSync(errLogFile)) {
fs.unlinkSync(errLogFile);
}
}
function startBackgroundProcess() {
if (isAlreadyRunning()) {
console.log("An instance is already running.");
process.exit(1);
}
// Remove old log files
removeLogFiles();
const out = fs.openSync(outLogFile, "a");
const err = fs.openSync(errLogFile, "a");
const subprocess = spawn(
process.argv[0],
[process.argv[1], "background", ...process.argv.slice(3)],
{
detached: true,
stdio: ["ignore", out, err],
}
);
createLockFile(subprocess.pid);
console.log(`Started background process with PID: ${subprocess.pid}`);
subprocess.unref();
}
function stopBackgroundProcess() {
if (!isAlreadyRunning()) {
console.log("No running instance found.");
return;
}
const pid = fs.readFileSync(lockFile, "utf-8");
exec(`ps -p ${pid}`, (error, stdout, stderr) => {
if (error || stderr) {
console.log(`No process with PID ${pid} is running.`);
removeLockFile();
return;
}
// If the process exists, attempt to kill it
try {
process.kill(pid, "SIGTERM");
removeLockFile();
console.log(`Stopped process with PID: ${pid}`);
} catch (err) {
console.error(
`Failed to stop process with PID: ${pid}. Error: ${err.message}`
);
}
});
}
function readLog(path) {
try {
return fs.readFileSync(path, "utf-8");
} catch (err) {
return "";
}
}
function displayLogs() {
const outLog = readLog(outLogFile);
const errLog = readLog(errLogFile);
console.log("STDOUT:\n", outLog.trimStart());
console.log("STDERR:\n", errLog.trimStart());
}
function clearLogs() {
fs.rmSync(outLogFile, { force: true });
fs.rmSync(errLogFile, { force: true });
console.log("Log files cleared.");
}
function getOptions() {
const optionDefinitions = [
{ name: "proto", alias: "P", type: String },
{ name: "port", alias: "p", type: Number },
{ name: "env", alias: "e", type: String },
];
// Parse options, skipping the first three arguments ('node', script name and verb)
const options = commandLineArgs(optionDefinitions, {
argv: process.argv.slice(3),
});
return options;
}
// Process command line arguments
if (process.argv[2] === "start") {
// The result is only used in the background process, but the check is also done here, before the
// background process is started to prevent bad arguments from being passed to the background process.
getOptions();
// Spawn the background process
startBackgroundProcess();
} else if (process.argv[2] === "stop") {
// Stop the background process
stopBackgroundProcess();
} else if (process.argv[2] === "clear") {
// Clear the log files
clearLogs();
} else if (process.argv[2] === "logs") {
// Display the log files
displayLogs();
// tailLogs();
} else if (process.argv[2] === "run" || process.argv[2] === "background") {
if (process.argv[2] === "background") {
// Start-up for the background process
process.on("SIGTERM", () => {
console.log("Stopping background process...");
removeLockFile();
process.exit(0);
});
process.on("exit", (code) => {
console.log(`About to exit with code: ${code}`);
// Perform cleanup or final operations here
removeLockFile();
});
}
const options = getOptions();
backgroundMain(options);
} else {
const scriptName = path.basename(process.argv[1]);
console.log(`Usage: node ${scriptName} [run|start|stop|logs|clear]`);
console.log(`\nCommands:`);
console.log(
` start\t\tStart the background process if it's not already running.`
);
console.log(` stop\t\tStop the background process if it's running.`);
console.log(` logs\t\tDisplay the output and error logs.`);
console.log(` clear\t\tClear the content of the output and error logs.`);
console.log(`\nExample:`);
console.log(` node ${scriptName} start\t\t# Starts the background process`);
}
async function backgroundMain(options) {
// Get the ngrok config file path
let ngrokConfig = getNgrokConfig();
// Check if the ngrok config file exists
if (!fs.existsSync(ngrokConfig)) {
throw new Error("No ngrok config file found");
}
// Read the ngrok config file
try {
const { authToken } = readNgrokConfig(ngrokConfig);
global.authToken = authToken;
} catch (err) {
console.error(err);
process.exit(1);
}
// Start ngrok
const proto = options?.proto ?? "http";
const addr = options?.port ? options.port : 3000;
try {
const url = await ngrok.connect({
authtoken: authToken,
proto,
addr,
onLogEvent: (data) => {
// console.log(data);
},
});
writeDotEnv(url); // Sync the .env file
console.log(`Started ngrok: protocol '${proto}', addr '${addr}'`);
console.log(`Your current ngrok url is ${url}.`);
console.log(`Your .env file has been updated.`);
} catch (err) {
console.error(err.message);
process.exit(1);
}
}
function getNgrokConfig(pathOnly = false) {
const paths = [
os.homedir() + "/.ngrok2", // Newer versions of ngrok
os.homedir() + "/Library/Application Support/ngrok", // MacOS
os.homedir() + "/AppData/Local/ngrok", // Windows
os.homedir() + "/.config/ngrok", // Alternative location for linux
];
for (const p of paths) {
if (fs.existsSync(`${p}/ngrok.yml`)) {
return pathOnly ? p : `${p}/ngrok.yml`;
}
}
if (pathOnly) return null;
throw new Error(
"Ngrok config file not found. Try running 'ngrok authtoken <token>' in your terminal."
);
}
function readNgrokConfig(ngrokConfig) {
const data = fs.readFileSync(ngrokConfig, "utf8");
let authToken;
// Convert string to string array, split at newlines
let lines = data.split("\n"); // string[]
// find the auth token (format: authtoken: token_goes_here)
lines.forEach((element) => {
const [name, value] = element.split(": ");
if (name === "authtoken") authToken = value;
});
// No token found
if (!authToken) {
// https://dashboard.ngrok.com/get-started/your-authtoken
throw new Error("No ngrok authtoken found in the config file");
}
return { authToken };
}
function findProjectFolder(filename = ".env") {
// Walk the path to find .env file... options?.env
let currentDir = __dirname;
while (true) {
const packageJsonPath = path.join(currentDir, "package.json");
if (fs.existsSync(packageJsonPath)) {
break;
}
const envPath = path.join(currentDir, filename);
if (fs.existsSync(envPath)) {
break;
}
const parentDir = path.resolve(currentDir, "..");
if (parentDir === currentDir) {
throw new Error("No project folder found");
}
currentDir = parentDir;
}
return currentDir;
}
// Walk the path to find .env file...
function findDotEnv(filename = ".env") {
const currentDir = findProjectFolder(filename);
return `${currentDir}/${filename}`;
}
function readDotEnv(path) {
const data = fs.readFileSync(path, "utf8");
// Convert string to string array, split at newlines
let lines = data.split("\n"); // string[]
return lines;
}
function writeDotEnv(url) {
// Read the .env file
let lines = readDotEnv(findDotEnv());
// Rebuild lines with the new url
let found = false;
lines = lines.map((element) => {
const [name] = element.split("=");
if (name === "NGROK_SERVERHOST") {
found = true;
return `${name}=${url}`;
}
return element;
});
// Is this variable already in the .env, if not add it.
if (!found) {
lines.unshift(`NGROK_SERVERHOST=${url}`);
}
// convert back to string format
let writeData = "";
lines.forEach((element) => {
writeData += element + "\n";
});
writeData = writeData.slice(0, writeData.length - 1);
// Write the new .env file
fs.writeFileSync(findDotEnv(), writeData);
}

88
ngrok.js Normal file
View File

@ -0,0 +1,88 @@
const ngrok = require("ngrok");
const fs = require("fs");
const { exec } = require("child_process");
const dotenvFile = "../.env";
const optionDefinitions = [
{ name: "proto", alias: 'p', type: String },
{ name: "addr", alias: 'a', type: Number },
];
const commandLineArgs = require("command-line-args");
const options = commandLineArgs(optionDefinitions);
console.log(options);
// Read the .env file
fs.readFile(dotenvFile, "utf8", (err, data) => {
if (err) {
console.error(err);
return;
}
// Convert string to string array, split at newlines
let lines = data.split("\n"); // string[]
let authtoken = null; // string || null
// find the auth token
lines.forEach((element) => {
const [name, value] = element.split("=");
if (name === "NGROK_AUTHTOKEN") authtoken = value;
});
// No token found
if (!authtoken) {
console.error("Setup NGROK_AUTHTOKEN in your .env file");
return;
}
// Start ngrok
(async function () {
const proto = options?.proto ?? "http";
const addr = options?.addr ? options.addr : 3000;
// Start ngrok
try {
const url = await ngrok.connect({ authtoken, proto, addr });
// Rebuild lines with the new url
let found = false;
lines = lines.map((element) => {
const [name] = element.split("=");
if (name === "NGROK_SERVERHOST") {
found = true;
return `${name}=${url}`;
}
return element;
});
// Is this variable already in the .env, if not add it.
if (!found) {
lines.unshift(`NGROK_SERVERHOST=${url}`)
}
// convert back to string format
let writeData = "";
lines.forEach((element) => {
writeData += element + "\n";
});
writeData = writeData.slice(0, writeData.length - 1);
// Write the new .env file
fs.writeFile(dotenvFile, writeData, (err) => {
if (err) {
console.error(err);
return;
}
console.clear();
console.log(`Started ngrok: protocol '${proto}', addr '${addr}'`);
console.log(`Your current ngrok url is ${url}.`);
console.log(`Your .env file has been updated.`);
});
} catch (err) {
console.error(err.message);
process.exit(1);
}
})();
});

View File

@ -1,14 +1,16 @@
{ {
"name": "ngenv", "name": "ngrok-to-dotenv",
"version": "2.0.0", "version": "1.0.0",
"description": "Writes ngrok tunnel url to .env file", "description": "ngrok start-up wrapper that uses a dotenv file to get the authtoken and save the https address",
"main": "ngenv", "main": "ngrok.js",
"license": "MIT", "license": "MIT",
"bin": { "scripts": {
"ngenv": "./ngenv" "test": "echo \"Error: no test specified\" && exit 1",
"debug": "node --inspect ngrok.js",
"start": "node ngrok"
}, },
"dependencies": { "dependencies": {
"command-line-args": "^5.2.0", "command-line-args": "^5.2.0",
"ngrok": "^5.0.0-beta.2" "ngrok": "^4.2.2"
} }
} }