mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.8.46
Release v3.8.46. Full changelog: CHANGELOG.md → [3.8.46]. Contributor attribution in the CHANGELOG entries.
This commit is contained in:
committed by
GitHub
parent
3ddcee6369
commit
92715c8f2c
@@ -19,6 +19,16 @@ const STRIPPED_CODEX_ENV_KEYS = [
|
||||
/** Placeholder so codex's `env_key` is always satisfied when the backend is open. */
|
||||
const NO_AUTH_SENTINEL = "omniroute-no-auth";
|
||||
|
||||
// On Windows the `codex` binary is an npm `.cmd` shim that `spawn` cannot resolve
|
||||
// without a shell (bare "codex" → ENOENT). Mirror the qodercli Windows fix (#6263):
|
||||
// spawn `codex.cmd` through a shell on win32, and the bare binary elsewhere.
|
||||
export function resolveCodexSpawn(platform) {
|
||||
if (platform === "win32") {
|
||||
return { command: "codex.cmd", shell: true };
|
||||
}
|
||||
return { command: "codex", shell: undefined };
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value) {
|
||||
let s = String(value);
|
||||
let end = s.length;
|
||||
@@ -126,10 +136,10 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
|
||||
|
||||
if (!(await healthCheck(baseUrl))) {
|
||||
console.error(
|
||||
(t("launch.notRunning") || "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.").replace(
|
||||
"{port}",
|
||||
baseUrl
|
||||
)
|
||||
(
|
||||
t("launch.notRunning") ||
|
||||
"OmniRoute is not reachable at {port}. Start it with 'omniroute serve'."
|
||||
).replace("{port}", baseUrl)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
@@ -142,7 +152,12 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
|
||||
const env = buildCodexEnv(process.env, authToken);
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const child = spawn("codex", extraArgs, { env, stdio: "inherit" });
|
||||
const { command: codexLaunch, shell: shellValue } = resolveCodexSpawn(process.platform);
|
||||
const child = spawn(codexLaunch, extraArgs, {
|
||||
env,
|
||||
stdio: "inherit",
|
||||
shell: shellValue,
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
if (err?.code === "ENOENT") {
|
||||
console.error(
|
||||
@@ -165,10 +180,16 @@ export function registerLaunchCodex(program) {
|
||||
t("launchCodex.description") || "Launch Codex CLI pointed at OmniRoute (local or remote VPS)"
|
||||
)
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute base URL, e.g. http://192.168.0.15:20128 (overrides --port + context)")
|
||||
.option(
|
||||
"--remote <url>",
|
||||
"Remote OmniRoute base URL, e.g. http://192.168.0.15:20128 (overrides --port + context)"
|
||||
)
|
||||
.option("--profile <name>", "Codex profile to activate (passed as --profile <name>)")
|
||||
.option("-p, --p <name>", "Alias for --profile")
|
||||
.option("--api-key <key>", "OmniRoute API key (overrides OMNIROUTE_API_KEY env var for this invocation)")
|
||||
.option(
|
||||
"--api-key <key>",
|
||||
"OmniRoute API key (overrides OMNIROUTE_API_KEY env var for this invocation)"
|
||||
)
|
||||
.allowUnknownOption(true)
|
||||
.allowExcessArguments(true)
|
||||
.argument("[codexArgs...]", "arguments passed through to the codex binary")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* Special bypasses (handled before Commander):
|
||||
* --mcp Start MCP server over stdio
|
||||
* reset-encrypted-columns Recovery tool for broken encrypted credentials
|
||||
* reset-password Reset the admin/management password
|
||||
*
|
||||
* All other commands are routed through Commander (bin/cli/program.mjs).
|
||||
*/
|
||||
@@ -210,6 +211,15 @@ if (process.argv.includes("reset-encrypted-columns")) {
|
||||
process.exit(exitCode ?? 0);
|
||||
}
|
||||
|
||||
if (process.argv.includes("reset-password")) {
|
||||
// bin/reset-password.mjs self-executes its `main()` on import and calls
|
||||
// process.exit() on completion/error. Await a never-resolving promise so
|
||||
// control never falls through to Commander (which would then reject
|
||||
// `reset-password` as an unknown command). See #6261.
|
||||
await import(pathToFileURL(join(ROOT, "bin", "reset-password.mjs")).href);
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
try {
|
||||
const { createProgram } = await import(
|
||||
pathToFileURL(join(ROOT, "bin", "cli", "program.mjs")).href
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
*
|
||||
* Usage:
|
||||
* node bin/reset-password.mjs
|
||||
* npx omniroute reset-password
|
||||
* omniroute reset-password
|
||||
*
|
||||
* Non-interactive / scripted usage (piped stdin, e.g. CI or Docker):
|
||||
* printf 'NewPass123\nNewPass123\n' | omniroute reset-password
|
||||
* printf 'NewPass123' | omniroute reset-password --password-stdin
|
||||
*
|
||||
* Resets the admin password for OmniRoute.
|
||||
* Prompts for a new password and updates the database directly.
|
||||
* Prompts for a new password (interactive TTY) or reads it from stdin
|
||||
* (non-TTY) and updates the database directly.
|
||||
*
|
||||
* @module bin/reset-password
|
||||
*/
|
||||
@@ -21,19 +26,61 @@ import { readManagementPasswordState, resetManagementPassword } from "./cli/sqli
|
||||
const DATA_DIR = resolveDataDir();
|
||||
const DB_PATH = resolveStoragePath(DATA_DIR);
|
||||
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
function ask(question) {
|
||||
return new Promise((resolve) => rl.question(question, resolve));
|
||||
/** Read the entire stdin stream as a UTF-8 string (used for non-TTY input). */
|
||||
function readAllStdin() {
|
||||
return new Promise((resolve) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => resolve(data));
|
||||
process.stdin.on("error", () => resolve(data));
|
||||
// Resuming is implied by attaching a 'data' listener, but be explicit so a
|
||||
// paused stream (some spawn setups) still flows to EOF.
|
||||
process.stdin.resume();
|
||||
});
|
||||
}
|
||||
|
||||
function exitWithError(message) {
|
||||
console.error(message);
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
/**
|
||||
* Obtain the new password (and its confirmation).
|
||||
*
|
||||
* - `--password-stdin`: the ENTIRE stdin is the password, no confirmation.
|
||||
* - non-TTY stdin (piped): read all of stdin once; first line is the password,
|
||||
* second line — when present — is the confirmation, else the first line is
|
||||
* reused (a single-line pipe means "no separate confirmation").
|
||||
* - interactive TTY: two sequential prompts (unchanged behavior).
|
||||
*
|
||||
* The non-TTY path exists because two sequential `rl.question` promises never
|
||||
* settle under a piped EOF — the second read blocks forever, so the reset was
|
||||
* silently never applied (#6258).
|
||||
*/
|
||||
async function collectPassword() {
|
||||
if (process.argv.includes("--password-stdin")) {
|
||||
const raw = await readAllStdin();
|
||||
const password = raw.replace(/[\r\n]+$/, "");
|
||||
return { password, confirm: password };
|
||||
}
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
const raw = await readAllStdin();
|
||||
const lines = raw.split(/\r?\n/);
|
||||
const password = lines[0] ?? "";
|
||||
const confirm = lines[1] ? lines[1] : password;
|
||||
return { password, confirm };
|
||||
}
|
||||
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
try {
|
||||
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
|
||||
const password = await ask("Enter new password (min 8 chars): ");
|
||||
const confirm = await ask("Confirm new password: ");
|
||||
return { password, confirm };
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n🔑 OmniRoute — Password Reset\n");
|
||||
@@ -54,27 +101,34 @@ async function main() {
|
||||
console.log("ℹ️ No password is currently set.");
|
||||
}
|
||||
|
||||
const password = await ask("Enter new password (min 8 chars): ");
|
||||
const { password, confirm } = await collectPassword();
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
exitWithError("\n❌ Password must be at least 8 characters.\n");
|
||||
if (!password || password.length < MIN_PASSWORD_LENGTH) {
|
||||
console.error(`\n❌ Password must be at least ${MIN_PASSWORD_LENGTH} characters.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const confirm = await ask("Confirm new password: ");
|
||||
|
||||
if (password !== confirm) {
|
||||
exitWithError("\n❌ Passwords do not match.\n");
|
||||
console.error("\n❌ Passwords do not match.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await resetManagementPassword(password, DB_PATH);
|
||||
rl.close();
|
||||
|
||||
console.log("\n✅ Password reset successfully!");
|
||||
console.log(" Restart OmniRoute for changes to take effect.\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`\n❌ Error: ${err.message}\n`);
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
});
|
||||
main()
|
||||
.then(() => {
|
||||
// Explicit exit(0) so a caller that imports this module (bin/omniroute.mjs
|
||||
// routes `omniroute reset-password` here) terminates cleanly instead of
|
||||
// hanging / exiting with code 13 on an unsettled wrapper await. On POSIX,
|
||||
// console.log to a pipe is synchronous, so the success line is already
|
||||
// flushed by the time we exit.
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`\n❌ Error: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user