Compare commits
10 Commits
d4329ae706
...
6a7e500b8a
Author | SHA1 | Date |
---|---|---|
Josh Guyette | 6a7e500b8a | |
Josh Guyette | 1281de7795 | |
Josh Guyette | 22029d8a8e | |
Josh Guyette | 22a007080c | |
Josh Guyette | a1df8ebcb0 | |
Josh Guyette | 544b19ec1d | |
Josh Guyette | 80da640084 | |
nightness | 3e769771f9 | |
Josh Guyette | 17f69f80e8 | |
Josh Guyette | 0c762168af |
|
@ -1,3 +1,5 @@
|
|||
node_modules/
|
||||
yarn.lock
|
||||
package.json.lock
|
||||
.env
|
||||
.ngenv
|
||||
|
|
122
README.md
122
README.md
|
@ -1,3 +1,121 @@
|
|||
# ngrok-to-dotenv
|
||||
# Ngenv
|
||||
|
||||
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'.
|
||||
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.
|
||||
|
||||
## 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).
|
||||
|
|
|
@ -0,0 +1,344 @@
|
|||
#!/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
88
ngrok.js
|
@ -1,88 +0,0 @@
|
|||
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);
|
||||
}
|
||||
})();
|
||||
});
|
16
package.json
16
package.json
|
@ -1,16 +1,14 @@
|
|||
{
|
||||
"name": "ngrok-to-dotenv",
|
||||
"version": "1.0.0",
|
||||
"description": "ngrok start-up wrapper that uses a dotenv file to get the authtoken and save the https address",
|
||||
"main": "ngrok.js",
|
||||
"name": "ngenv",
|
||||
"version": "2.0.0",
|
||||
"description": "Writes ngrok tunnel url to .env file",
|
||||
"main": "ngenv",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"debug": "node --inspect ngrok.js",
|
||||
"start": "node ngrok"
|
||||
"bin": {
|
||||
"ngenv": "./ngenv"
|
||||
},
|
||||
"dependencies": {
|
||||
"command-line-args": "^5.2.0",
|
||||
"ngrok": "^4.2.2"
|
||||
"ngrok": "^5.0.0-beta.2"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue