mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(npm): add npm package publishing with CLI entry point (#15)
- Add bin/omniroute.mjs CLI with banner, auto-open browser, graceful shutdown - Add scripts/prepublish.mjs to build Next.js standalone into app/ - Add .github/workflows/npm-publish.yml for automated publish on release - Update package.json: name=omniroute, bin, files, engines, keywords, prepublishOnly - Add output: 'standalone' to next.config.mjs - Add MIT LICENSE - Update .npmignore and .gitignore for app/ build artifact Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
4f0155eb4a
commit
92b4c59fec
41
.github/workflows/npm-publish.yml
vendored
Normal file
41
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Publish to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build standalone app
|
||||
run: npm run build:cli
|
||||
|
||||
- name: Sync version from release tag
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
# Remove 'v' prefix if present (v0.1.0 -> 0.1.0)
|
||||
VERSION="${VERSION#v}"
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
echo "Publishing version: $VERSION"
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/app
|
||||
cloud/*
|
||||
|
||||
# misc
|
||||
|
||||
36
.npmignore
36
.npmignore
@@ -3,24 +3,42 @@ data/
|
||||
**/data/
|
||||
**/db.json
|
||||
|
||||
# Development
|
||||
# Source code (pre-built app/ is published instead)
|
||||
src/
|
||||
docs/
|
||||
test/
|
||||
tests/
|
||||
open-sse/
|
||||
docs/
|
||||
tests/
|
||||
cloud/
|
||||
images/
|
||||
logs/
|
||||
scripts/
|
||||
|
||||
# Config files
|
||||
# Config/dev files
|
||||
*.md
|
||||
!README.md
|
||||
.gitignore
|
||||
.git/
|
||||
.github/
|
||||
.husky/
|
||||
.vscode/
|
||||
.env*
|
||||
jsconfig.json
|
||||
eslint.config.mjs
|
||||
prettier.config.mjs
|
||||
postcss.config.mjs
|
||||
next.config.mjs
|
||||
tsconfig.json
|
||||
playwright.config.ts
|
||||
next-env.d.ts
|
||||
|
||||
# Build artifacts that shouldn't be published
|
||||
.next/cache/
|
||||
.next/standalone/data/
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Misc
|
||||
restart.sh
|
||||
AGENTS.md
|
||||
|
||||
# Build artifacts (pre-built goes inside app/)
|
||||
.next/
|
||||
node_modules/
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 diegosouzapw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
189
bin/omniroute.mjs
Executable file
189
bin/omniroute.mjs
Executable file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* OmniRoute CLI — Smart AI Router with Auto Fallback
|
||||
*
|
||||
* Usage:
|
||||
* omniroute Start the server (default port 20128)
|
||||
* omniroute --port 3000 Start on custom port
|
||||
* omniroute --no-open Start without opening browser
|
||||
* omniroute --help Show help
|
||||
* omniroute --version Show version
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const ROOT = join(__dirname, "..");
|
||||
const APP_DIR = join(ROOT, "app");
|
||||
|
||||
// ── Parse args ─────────────────────────────────────────────
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
\x1b[1m\x1b[36m⚡ OmniRoute\x1b[0m — Smart AI Router with Auto Fallback
|
||||
|
||||
\x1b[1mUsage:\x1b[0m
|
||||
omniroute Start the server
|
||||
omniroute --port <port> Use custom port (default: 20128)
|
||||
omniroute --no-open Don't open browser automatically
|
||||
omniroute --help Show this help
|
||||
omniroute --version Show version
|
||||
|
||||
\x1b[1mAfter starting:\x1b[0m
|
||||
Dashboard: http://localhost:<port>
|
||||
API: http://localhost:<port>/v1
|
||||
|
||||
\x1b[1mConnect your tools:\x1b[0m
|
||||
Set your CLI tool (Cursor, Cline, Codex, etc.) to use:
|
||||
\x1b[33mhttp://localhost:20128/v1\x1b[0m
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes("--version") || args.includes("-v")) {
|
||||
try {
|
||||
const pkg = await import(join(ROOT, "package.json"), {
|
||||
with: { type: "json" },
|
||||
});
|
||||
console.log(pkg.default.version);
|
||||
} catch {
|
||||
console.log("unknown");
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Parse --port
|
||||
let port = 20128;
|
||||
const portIdx = args.indexOf("--port");
|
||||
if (portIdx !== -1 && args[portIdx + 1]) {
|
||||
port = parseInt(args[portIdx + 1], 10);
|
||||
if (isNaN(port)) {
|
||||
console.error("\x1b[31m✖ Invalid port number\x1b[0m");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const noOpen = args.includes("--no-open");
|
||||
|
||||
// ── Banner ─────────────────────────────────────────────────
|
||||
console.log(`
|
||||
\x1b[36m ____ _ ____ _
|
||||
/ __ \\ (_) __ \\ | |
|
||||
| | | |_ __ ___ _ __ _| |__) |___ _ _| |_ ___
|
||||
| | | | '_ \` _ \\| '_ \\ | _ // _ \\| | | | __/ _ \\
|
||||
| |__| | | | | | | | | | | | \\ \\ (_) | |_| | || __/
|
||||
\\____/|_| |_| |_|_| |_|_|_| \\_\\___/ \\__,_|\\__\\___|
|
||||
\x1b[0m`);
|
||||
|
||||
// ── Resolve server entry ───────────────────────────────────
|
||||
const serverJs = join(APP_DIR, "server.js");
|
||||
|
||||
if (!existsSync(serverJs)) {
|
||||
console.error(
|
||||
"\x1b[31m✖ Server not found at:\x1b[0m",
|
||||
serverJs,
|
||||
);
|
||||
console.error(
|
||||
" This usually means the package was not built correctly.",
|
||||
);
|
||||
console.error(" Try reinstalling: npm install -g omniroute");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Start server ───────────────────────────────────────────
|
||||
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
HOSTNAME: "0.0.0.0",
|
||||
NODE_ENV: "production",
|
||||
};
|
||||
|
||||
const server = spawn("node", [serverJs], {
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
let started = false;
|
||||
|
||||
server.stdout.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
process.stdout.write(text);
|
||||
|
||||
// Detect server ready
|
||||
if (!started && (text.includes("Ready") || text.includes("started") || text.includes("listening"))) {
|
||||
started = true;
|
||||
onReady();
|
||||
}
|
||||
});
|
||||
|
||||
server.stderr.on("data", (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
console.error("\x1b[31m✖ Failed to start server:\x1b[0m", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`\x1b[31m✖ Server exited with code ${code}\x1b[0m`);
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
|
||||
// ── Graceful shutdown ──────────────────────────────────────
|
||||
function shutdown() {
|
||||
console.log("\n\x1b[33m⏹ Shutting down OmniRoute...\x1b[0m");
|
||||
server.kill("SIGTERM");
|
||||
setTimeout(() => {
|
||||
server.kill("SIGKILL");
|
||||
process.exit(0);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
// ── On ready ───────────────────────────────────────────────
|
||||
async function onReady() {
|
||||
const url = `http://localhost:${port}`;
|
||||
|
||||
console.log(`
|
||||
\x1b[32m✔ OmniRoute is running!\x1b[0m
|
||||
|
||||
\x1b[1m Dashboard:\x1b[0m ${url}
|
||||
\x1b[1m API Base:\x1b[0m ${url}/v1
|
||||
|
||||
\x1b[2m Point your CLI tool (Cursor, Cline, Codex) to:\x1b[0m
|
||||
\x1b[33m ${url}/v1\x1b[0m
|
||||
|
||||
\x1b[2m Press Ctrl+C to stop\x1b[0m
|
||||
`);
|
||||
|
||||
if (!noOpen) {
|
||||
try {
|
||||
const open = await import("open");
|
||||
await open.default(url);
|
||||
} catch {
|
||||
// open is optional — if not available, just skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no "Ready" message detected in 15s, assume server is up
|
||||
setTimeout(() => {
|
||||
if (!started) {
|
||||
started = true;
|
||||
onReady();
|
||||
}
|
||||
}, 15000);
|
||||
@@ -1,13 +1,12 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
transpilePackages: ["@omniroute/open-sse"],
|
||||
allowedDevOrigins: ["192.168.*"],
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_CLOUD_URL: "https://omniroute.dev",
|
||||
},
|
||||
// NEXT_PUBLIC_CLOUD_URL is set in .env — do NOT hardcode here (it overrides .env)
|
||||
webpack: (config, { isServer }) => {
|
||||
// Ignore fs/path modules in browser bundle
|
||||
if (!isServer) {
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "omniroute-app",
|
||||
"version": "0.0.1",
|
||||
"name": "omniroute",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute-app",
|
||||
"version": "0.0.1",
|
||||
"name": "omniroute",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"open-sse"
|
||||
],
|
||||
@@ -38,6 +39,9 @@
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"omniroute": "bin/omniroute.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
@@ -52,6 +56,9 @@
|
||||
"prettier": "^3.8.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -4800,6 +4807,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
||||
44
package.json
44
package.json
@@ -1,14 +1,49 @@
|
||||
{
|
||||
"name": "omniroute-app",
|
||||
"version": "0.0.1",
|
||||
"description": "OmniRoute web dashboard",
|
||||
"private": true,
|
||||
"name": "omniroute",
|
||||
"version": "0.1.0",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"omniroute": "bin/omniroute.mjs"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"app/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"workspaces": [
|
||||
"open-sse"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
"router",
|
||||
"proxy",
|
||||
"openai",
|
||||
"claude",
|
||||
"anthropic",
|
||||
"gemini",
|
||||
"fallback",
|
||||
"cursor",
|
||||
"cline",
|
||||
"codex",
|
||||
"llm",
|
||||
"auto-fallback"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": "diegosouzapw",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diegosouzapw/OmniRoute"
|
||||
},
|
||||
"homepage": "https://github.com/diegosouzapw/OmniRoute",
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 20128",
|
||||
"build": "next build --webpack",
|
||||
"build:cli": "node scripts/prepublish.mjs",
|
||||
"start": "next start --port 20128",
|
||||
"lint": "eslint .",
|
||||
"test:plan3": "node --test tests/unit/plan3-p0.test.mjs",
|
||||
@@ -16,6 +51,7 @@
|
||||
"test": "npm run build",
|
||||
"test:e2e": "npx playwright test",
|
||||
"check": "npm run lint && npm run test",
|
||||
"prepublishOnly": "npm run build:cli",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
104
scripts/prepublish.mjs
Normal file
104
scripts/prepublish.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* OmniRoute — Prepublish Build Script
|
||||
*
|
||||
* Builds the Next.js app in standalone mode and copies output
|
||||
* into the `app/` directory that gets published to npm.
|
||||
*
|
||||
* Run with: node scripts/prepublish.mjs
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
const APP_DIR = join(ROOT, "app");
|
||||
|
||||
console.log("🔨 OmniRoute — Building for npm publish...\n");
|
||||
|
||||
// ── Step 1: Clean previous app/ directory ──────────────────
|
||||
if (existsSync(APP_DIR)) {
|
||||
console.log(" 🧹 Cleaning previous app/ directory...");
|
||||
rmSync(APP_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ── Step 2: Install dependencies ───────────────────────────
|
||||
console.log(" 📦 Installing dependencies...");
|
||||
execSync("npm install", { cwd: ROOT, stdio: "inherit" });
|
||||
|
||||
// ── Step 3: Build Next.js ──────────────────────────────────
|
||||
console.log(" 🏗️ Building Next.js (standalone)...");
|
||||
execSync("npx next build --webpack", { cwd: ROOT, stdio: "inherit" });
|
||||
|
||||
// ── Step 4: Verify standalone output ───────────────────────
|
||||
const standaloneDir = join(ROOT, ".next", "standalone");
|
||||
const serverJs = join(standaloneDir, "server.js");
|
||||
|
||||
if (!existsSync(serverJs)) {
|
||||
console.error("\n ❌ Standalone build not found at:", standaloneDir);
|
||||
console.error(" Make sure next.config.mjs has: output: 'standalone'");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Step 5: Copy standalone output to app/ ─────────────────
|
||||
console.log(" 📋 Copying standalone build to app/...");
|
||||
mkdirSync(APP_DIR, { recursive: true });
|
||||
cpSync(standaloneDir, APP_DIR, { recursive: true });
|
||||
|
||||
// ── Step 6: Copy static assets ─────────────────────────────
|
||||
const staticSrc = join(ROOT, ".next", "static");
|
||||
const staticDest = join(APP_DIR, ".next", "static");
|
||||
if (existsSync(staticSrc)) {
|
||||
console.log(" 📋 Copying static assets...");
|
||||
mkdirSync(staticDest, { recursive: true });
|
||||
cpSync(staticSrc, staticDest, { recursive: true });
|
||||
}
|
||||
|
||||
// ── Step 7: Copy public/ assets ────────────────────────────
|
||||
const publicSrc = join(ROOT, "public");
|
||||
const publicDest = join(APP_DIR, "public");
|
||||
if (existsSync(publicSrc)) {
|
||||
console.log(" 📋 Copying public/ assets...");
|
||||
mkdirSync(publicDest, { recursive: true });
|
||||
cpSync(publicSrc, publicDest, { recursive: true });
|
||||
}
|
||||
|
||||
// ── Step 8: Copy MITM cert utilities (if needed) ───────────
|
||||
const mitmSrc = join(ROOT, "src", "mitm");
|
||||
const mitmDest = join(APP_DIR, "src", "mitm");
|
||||
if (existsSync(mitmSrc)) {
|
||||
console.log(" 📋 Copying MITM utilities...");
|
||||
mkdirSync(mitmDest, { recursive: true });
|
||||
cpSync(mitmSrc, mitmDest, { recursive: true });
|
||||
}
|
||||
|
||||
// ── Step 9: Copy shared utilities needed at runtime ────────
|
||||
const sharedApiKey = join(ROOT, "src", "shared", "utils", "apiKey.js");
|
||||
const sharedApiKeyDest = join(APP_DIR, "src", "shared", "utils");
|
||||
if (existsSync(sharedApiKey)) {
|
||||
console.log(" 📋 Copying shared utilities...");
|
||||
mkdirSync(sharedApiKeyDest, { recursive: true });
|
||||
cpSync(sharedApiKey, join(sharedApiKeyDest, "apiKey.js"));
|
||||
}
|
||||
|
||||
// ── Step 10: Ensure data/ directory exists ──────────────────
|
||||
mkdirSync(join(APP_DIR, "data"), { recursive: true });
|
||||
|
||||
// ── Done ───────────────────────────────────────────────────
|
||||
const appPkg = join(APP_DIR, "package.json");
|
||||
if (existsSync(appPkg)) {
|
||||
const pkg = JSON.parse(readFileSync(appPkg, "utf8"));
|
||||
console.log(`\n ✅ Build complete!`);
|
||||
console.log(` App directory: app/`);
|
||||
console.log(` Server entry: app/server.js`);
|
||||
} else {
|
||||
console.log(`\n ✅ Build complete! (app/ ready for publish)`);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
Reference in New Issue
Block a user