mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
This commit is contained in:
committed by
GitHub
parent
009ad13a91
commit
fd26e601a2
@@ -224,6 +224,26 @@ export async function syncStandaloneExtraModules(
|
||||
sourcePath: path.join(rootDir, "scripts", "dev", "run-standalone.mjs"),
|
||||
destRelative: path.join("dev", "run-standalone.mjs"),
|
||||
},
|
||||
{
|
||||
// WS-aware wrapper that run-standalone.mjs prefers over bare server.js.
|
||||
// It installs the trusted peer-IP stamp the authz middleware needs to
|
||||
// allow loopback/LAN access to LOCAL_ONLY routes; without it the Docker
|
||||
// container fails closed (every LOCAL_ONLY request 403s). Imports
|
||||
// peer-stamp.mjs + responses-ws-proxy.mjs, so all three are co-located.
|
||||
label: "WS/peer-stamp standalone server wrapper",
|
||||
sourcePath: path.join(rootDir, "scripts", "dev", "standalone-server-ws.mjs"),
|
||||
destRelative: "server-ws.mjs",
|
||||
},
|
||||
{
|
||||
label: "peer-stamp helper (server-ws.mjs dependency)",
|
||||
sourcePath: path.join(rootDir, "scripts", "dev", "peer-stamp.mjs"),
|
||||
destRelative: "peer-stamp.mjs",
|
||||
},
|
||||
{
|
||||
label: "responses-ws-proxy (server-ws.mjs dependency)",
|
||||
sourcePath: path.join(rootDir, "scripts", "dev", "responses-ws-proxy.mjs"),
|
||||
destRelative: "responses-ws-proxy.mjs",
|
||||
},
|
||||
{
|
||||
label: "runtime-env script",
|
||||
sourcePath: path.join(rootDir, "scripts", "build", "runtime-env.mjs"),
|
||||
|
||||
@@ -32,6 +32,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
|
||||
"docs/reference/openapi.yaml",
|
||||
"open-sse/mcp-server/server.js",
|
||||
"package.json",
|
||||
"peer-stamp.mjs",
|
||||
"responses-ws-proxy.mjs",
|
||||
"scripts/dev/sync-env.mjs",
|
||||
"server.js",
|
||||
@@ -104,6 +105,7 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
"app/server.js",
|
||||
"app/server-ws.mjs",
|
||||
"app/responses-ws-proxy.mjs",
|
||||
"app/peer-stamp.mjs",
|
||||
"bin/cli/program.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
|
||||
@@ -202,13 +202,19 @@ cpSync(standaloneDir, APP_DIR, { recursive: true });
|
||||
|
||||
const standaloneWsSrc = join(ROOT, "scripts", "dev", "standalone-server-ws.mjs");
|
||||
const responsesWsProxySrc = join(ROOT, "scripts", "dev", "responses-ws-proxy.mjs");
|
||||
if (existsSync(standaloneWsSrc) && existsSync(responsesWsProxySrc)) {
|
||||
const peerStampSrc = join(ROOT, "scripts", "dev", "peer-stamp.mjs");
|
||||
if (existsSync(standaloneWsSrc) && existsSync(responsesWsProxySrc) && existsSync(peerStampSrc)) {
|
||||
console.log(" 📋 Adding Responses WebSocket standalone wrapper...");
|
||||
cpSync(standaloneWsSrc, join(APP_DIR, "server-ws.mjs"));
|
||||
writeFileSync(
|
||||
join(APP_DIR, "responses-ws-proxy.mjs"),
|
||||
'export * from "../scripts/dev/responses-ws-proxy.mjs";\n'
|
||||
);
|
||||
// server-ws.mjs imports ./peer-stamp.mjs (the trusted peer-IP stamp helper
|
||||
// the authz middleware relies on). It is self-contained (node builtins only),
|
||||
// so copy it directly alongside server-ws.mjs. Without this the wrapper throws
|
||||
// ERR_MODULE_NOT_FOUND on boot and the server falls back to no peer stamp.
|
||||
cpSync(peerStampSrc, join(APP_DIR, "peer-stamp.mjs"));
|
||||
}
|
||||
|
||||
// ── Next.js Turbopack Standalone Tracer Fix ───────────────
|
||||
|
||||
@@ -5,6 +5,20 @@ export function parsePort(value, fallback) {
|
||||
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the V8 heap ceiling (MB) for the server process from
|
||||
* `OMNIROUTE_MEMORY_MB`, mirroring `omniroute serve`. Clamped to [64, 16384];
|
||||
* invalid/unset → fallback (512). The standalone launcher uses this so
|
||||
* OMNIROUTE_MEMORY_MB can override the Docker image's NODE_OPTIONS fallback
|
||||
* without clobbering any other runtime flags (#2939).
|
||||
* @param {string | number | undefined | null} value
|
||||
* @param {number} [fallback]
|
||||
*/
|
||||
export function resolveMaxOldSpaceMb(value, fallback = 512) {
|
||||
const parsed = Number.parseInt(String(value), 10);
|
||||
return Number.isFinite(parsed) && parsed >= 64 && parsed <= 16384 ? parsed : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [fromEnv]
|
||||
* Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn.
|
||||
|
||||
@@ -11,9 +11,8 @@ fi
|
||||
if [ -d "/app/data" ] && [ ! -w "/app/data" ]; then
|
||||
echo "WARNING: /app/data is not writable by the current user (UID $(id -u))."
|
||||
echo "Run this on the Docker host to fix:"
|
||||
echo " sudo chown -R 1000:1000 ./data"
|
||||
echo " sudo chown -R $(id -u):$(id -g) /app/data"
|
||||
echo " chmod -R u+rwX ./data"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -71,6 +71,8 @@ const IGNORE_FROM_CODE = new Set([
|
||||
"NEXT_RUNTIME",
|
||||
"NODE_TEST_CONTEXT",
|
||||
"VITEST",
|
||||
// Instruction snippet shown to users (Traffic Inspector HttpProxySnippetCard) — not OmniRoute config.
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
// CI providers (set by the runner).
|
||||
"GITHUB_BASE_REF",
|
||||
"GITHUB_BASE_SHA",
|
||||
|
||||
53
scripts/dev/peer-stamp.mjs
Normal file
53
scripts/dev/peer-stamp.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Trusted peer-IP stamping for the custom Node HTTP servers.
|
||||
*
|
||||
* The Next.js middleware runtime (proxy.ts → runAuthzPipeline) exposes NO socket
|
||||
* or peer IP — only request headers, ALL of which are client-controlled. The
|
||||
* LOCAL_ONLY route guard (spawn-capable routes) must decide locality from the
|
||||
* real TCP peer, never from the spoofable Host header.
|
||||
*
|
||||
* Our custom servers DO have the real `req.socket.remoteAddress`. They stamp it
|
||||
* into PEER_IP_HEADER as `<token>|<ip>`, where <token> is a per-process secret
|
||||
* (OMNIROUTE_PEER_STAMP_TOKEN). Any client-supplied value of PEER_IP_HEADER is
|
||||
* deleted first, so a remote caller cannot pre-populate it. The middleware
|
||||
* (src/server/authz/policies/management.ts → resolveStampedPeer) trusts the IP
|
||||
* ONLY when the token matches this process's secret; otherwise it fails closed.
|
||||
*
|
||||
* Keep PEER_IP_HEADER in sync with PEER_IP_HEADER in
|
||||
* src/server/authz/headers.ts (the TS side cannot import this .mjs).
|
||||
*/
|
||||
export const PEER_IP_HEADER = "x-omniroute-peer-ip";
|
||||
|
||||
/** Generate (once) and return the per-process stamp token, persisting it in env
|
||||
* so the middleware running in the same process reads the identical value. */
|
||||
export function ensurePeerStampToken() {
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN ||= randomUUID();
|
||||
return process.env.OMNIROUTE_PEER_STAMP_TOKEN;
|
||||
}
|
||||
|
||||
/** Strip any client-supplied PEER_IP_HEADER and stamp the real TCP peer IP,
|
||||
* token-prefixed. Never throws — a stamping failure must not block a request
|
||||
* (it degrades to "locality unknown" → fail closed in the middleware). */
|
||||
export function stampPeerIp(req) {
|
||||
try {
|
||||
if (!req || !req.headers) return;
|
||||
// Node lowercases incoming header names; delete kills any client value.
|
||||
delete req.headers[PEER_IP_HEADER];
|
||||
const ip = req.socket && req.socket.remoteAddress;
|
||||
if (ip) {
|
||||
req.headers[PEER_IP_HEADER] = `${ensurePeerStampToken()}|${ip}`;
|
||||
}
|
||||
} catch {
|
||||
/* never block a request on peer stamping */
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap a Node request listener so every request is peer-stamped first. */
|
||||
export function wrapRequestListenerWithPeerStamp(listener) {
|
||||
return function peerStampingRequestHandler(req, res) {
|
||||
stampPeerIp(req);
|
||||
return listener.call(this, req, res);
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { bootstrapEnv } from "../build/bootstrap-env.mjs";
|
||||
import { resolveRuntimePorts, withRuntimePortEnv } from "../build/runtime-env.mjs";
|
||||
import { createOmnirouteWsBridge } from "./v1-ws-bridge.mjs";
|
||||
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
// Pre-read DATA_DIR from local .env before bootstrap resolves paths
|
||||
@@ -49,6 +50,9 @@ const { dashboardPort } = runtimePorts;
|
||||
const hostname = process.env.HOST || "0.0.0.0";
|
||||
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK === "1";
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret used to prove the trusted peer-IP stamp came from this
|
||||
// server (read by the authz middleware in the same process). See peer-stamp.mjs.
|
||||
ensurePeerStampToken();
|
||||
|
||||
const nextApp = next({
|
||||
dev,
|
||||
@@ -71,7 +75,12 @@ async function start() {
|
||||
baseUrl: `http://127.0.0.1:${dashboardPort}`,
|
||||
});
|
||||
|
||||
const server = http.createServer((req, res) => requestHandler(req, res));
|
||||
const server = http.createServer((req, res) => {
|
||||
// Stamp the real TCP peer IP before Next sees the request, so the authz
|
||||
// middleware can decide LOCAL_ONLY locality without trusting the Host header.
|
||||
stampPeerIp(req);
|
||||
return requestHandler(req, res);
|
||||
});
|
||||
server.on("upgrade", async (req, socket, head) => {
|
||||
try {
|
||||
const responsesWsHandled = await responsesWsProxy.handleUpgrade(req, socket, head);
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import {
|
||||
resolveRuntimePorts,
|
||||
withRuntimePortEnv,
|
||||
resolveMaxOldSpaceMb,
|
||||
spawnWithForwardedSignals,
|
||||
} from "../build/runtime-env.mjs";
|
||||
import { bootstrapEnv } from "../build/bootstrap-env.mjs";
|
||||
|
||||
const env = bootstrapEnv();
|
||||
const runtimePorts = resolveRuntimePorts(env);
|
||||
const childEnv = withRuntimePortEnv(env, runtimePorts);
|
||||
|
||||
spawnWithForwardedSignals("node", ["server.js"], {
|
||||
// #2939: honor OMNIROUTE_MEMORY_MB (default 512), the same knob
|
||||
// `omniroute serve` uses, so Docker users can control the server heap under
|
||||
// load / large SQLite DBs. A trailing --max-old-space-size wins, so this
|
||||
// overrides the image fallback without clobbering any other NODE_OPTIONS flags.
|
||||
const maxOldSpaceMb = resolveMaxOldSpaceMb(childEnv.OMNIROUTE_MEMORY_MB);
|
||||
childEnv.NODE_OPTIONS =
|
||||
`${childEnv.NODE_OPTIONS || ""} --max-old-space-size=${maxOldSpaceMb}`.trim();
|
||||
|
||||
// Prefer the WS-aware wrapper (server-ws.mjs) over the bare Next standalone
|
||||
// server.js: it installs the trusted peer-IP stamp (scripts/dev/peer-stamp.mjs)
|
||||
// that the authz middleware needs to allow loopback/LAN access to LOCAL_ONLY
|
||||
// routes. Falling back to server.js fails CLOSED (every LOCAL_ONLY request 403s)
|
||||
// rather than trusting the spoofable Host header.
|
||||
const entry = existsSync("server-ws.mjs") ? "server-ws.mjs" : "server.js";
|
||||
|
||||
spawnWithForwardedSignals("node", [entry], {
|
||||
stdio: "inherit",
|
||||
env: withRuntimePortEnv(env, runtimePorts),
|
||||
env: childEnv,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import http from "node:http";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs";
|
||||
|
||||
const originalCreateServer = http.createServer.bind(http);
|
||||
const proxiesByPort = new Map();
|
||||
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret proving the trusted peer-IP stamp came from this server.
|
||||
ensurePeerStampToken();
|
||||
|
||||
function getPort(server) {
|
||||
const address = server.address?.();
|
||||
@@ -46,6 +49,13 @@ function wrapUpgradeListener(server, listener) {
|
||||
}
|
||||
|
||||
http.createServer = function createServerWithResponsesWs(...args) {
|
||||
// Next's standalone server.js may pass its request listener directly to
|
||||
// createServer; wrap it so the real TCP peer IP is stamped before Next runs.
|
||||
const lastFnIdx = args.map((a) => typeof a === "function").lastIndexOf(true);
|
||||
if (lastFnIdx >= 0) {
|
||||
args[lastFnIdx] = wrapRequestListenerWithPeerStamp(args[lastFnIdx]);
|
||||
}
|
||||
|
||||
const server = originalCreateServer(...args);
|
||||
const originalOn = server.on.bind(server);
|
||||
const originalAddListener = server.addListener.bind(server);
|
||||
@@ -54,6 +64,10 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
if (eventName === "upgrade" && typeof listener === "function") {
|
||||
return originalOn(eventName, wrapUpgradeListener(server, listener));
|
||||
}
|
||||
// …or it may attach the handler via server.on("request"): wrap that too.
|
||||
if (eventName === "request" && typeof listener === "function") {
|
||||
return originalOn(eventName, wrapRequestListenerWithPeerStamp(listener));
|
||||
}
|
||||
return originalOn(eventName, listener);
|
||||
};
|
||||
|
||||
@@ -61,6 +75,9 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
if (eventName === "upgrade" && typeof listener === "function") {
|
||||
return originalAddListener(eventName, wrapUpgradeListener(server, listener));
|
||||
}
|
||||
if (eventName === "request" && typeof listener === "function") {
|
||||
return originalAddListener(eventName, wrapRequestListenerWithPeerStamp(listener));
|
||||
}
|
||||
return originalAddListener(eventName, listener);
|
||||
};
|
||||
|
||||
|
||||
44
scripts/i18n/fill-missing-from-en.mjs
Normal file
44
scripts/i18n/fill-missing-from-en.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* fill-missing-from-en.mjs — fills missing keys in all non-EN locale JSON files
|
||||
* with the EN fallback value. Does NOT add translation markers (__MISSING__).
|
||||
* Only fills keys that are absent — never overwrites existing translated values.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/i18n/fill-missing-from-en.mjs
|
||||
*
|
||||
* Idempotent. Safe to run repeatedly.
|
||||
*/
|
||||
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = new URL("../../src/i18n/messages/", import.meta.url).pathname;
|
||||
const EN = JSON.parse(readFileSync(join(ROOT, "en.json"), "utf-8"));
|
||||
|
||||
function fillMissing(target, source) {
|
||||
for (const k of Object.keys(source)) {
|
||||
if (typeof source[k] === "object" && source[k] !== null && !Array.isArray(source[k])) {
|
||||
target[k] = target[k] && typeof target[k] === "object" ? target[k] : {};
|
||||
fillMissing(target[k], source[k]);
|
||||
} else if (!(k in target)) {
|
||||
target[k] = source[k]; // fallback EN value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let touched = 0;
|
||||
for (const file of readdirSync(ROOT)) {
|
||||
if (!file.endsWith(".json") || file === "en.json") continue;
|
||||
const path = join(ROOT, file);
|
||||
const data = JSON.parse(readFileSync(path, "utf-8"));
|
||||
const before = JSON.stringify(data);
|
||||
fillMissing(data, EN);
|
||||
const after = JSON.stringify(data);
|
||||
if (before !== after) {
|
||||
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
||||
touched++;
|
||||
console.log(`[i18n] filled missing in ${file}`);
|
||||
}
|
||||
}
|
||||
console.log(`[i18n] done — touched ${touched} locale files`);
|
||||
183
scripts/skills/generate-agent-skills.mjs
Normal file
183
scripts/skills/generate-agent-skills.mjs
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-agent-skills.mjs — CLI wrapper for src/lib/agentSkills/generator.ts
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/skills/generate-agent-skills.mjs # dry-run (default)
|
||||
* node scripts/skills/generate-agent-skills.mjs --apply # write SKILL.md files
|
||||
* node scripts/skills/generate-agent-skills.mjs --prune # detect orphans (dry-run)
|
||||
* node scripts/skills/generate-agent-skills.mjs --apply --prune # write + delete orphans
|
||||
* node scripts/skills/generate-agent-skills.mjs --only=omni-providers,cli-serve
|
||||
* node scripts/skills/generate-agent-skills.mjs --json # JSON output to stdout
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — success (dry-run with no changes, or apply completed)
|
||||
* 1 — error (import or generator threw)
|
||||
* 2 — dry-run detected changes (use for CI fail-on-stale check)
|
||||
*
|
||||
* Security: no shell interpolation of user input (Hard Rule #13).
|
||||
* All runtime values are passed as JS variables, not shell strings.
|
||||
*/
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
// ── Resolve project root from script location ─────────────────────────────────
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// scripts/skills/generate-agent-skills.mjs → up 2 levels → project root
|
||||
const projectRoot = path.resolve(__dirname, "..", "..");
|
||||
|
||||
// ── Argument parsing ──────────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function hasFlag(flag) {
|
||||
return args.some((a) => a === flag || a.startsWith(flag + "="));
|
||||
}
|
||||
|
||||
function getFlagValue(flag) {
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith(flag + "=")) return arg.slice(flag.length + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const applyMode = hasFlag("--apply");
|
||||
const pruneMode = hasFlag("--prune");
|
||||
const jsonOutput = hasFlag("--json");
|
||||
const onlyRaw = getFlagValue("--only");
|
||||
const onlyIds = onlyRaw ? onlyRaw.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
||||
|
||||
// ── Utility: print table ──────────────────────────────────────────────────────
|
||||
|
||||
function printTable(report) {
|
||||
const { generated, unchanged, pruned, orphansDetected, errors } = report;
|
||||
|
||||
console.log(`\nGenerated: ${generated.length} · Unchanged: ${unchanged.length} · Pruned: ${pruned.length} · Orphans: ${orphansDetected.length} · Errors: ${errors.length}\n`);
|
||||
|
||||
if (generated.length > 0) {
|
||||
console.log(" GENERATED:");
|
||||
for (const id of generated) {
|
||||
console.log(` + ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (unchanged.length > 0 && unchanged.length <= 10) {
|
||||
console.log(" UNCHANGED:");
|
||||
for (const id of unchanged) {
|
||||
console.log(` = ${id}`);
|
||||
}
|
||||
} else if (unchanged.length > 10) {
|
||||
console.log(` UNCHANGED: ${unchanged.length} skills (all up-to-date)`);
|
||||
}
|
||||
|
||||
if (orphansDetected.length > 0) {
|
||||
console.log(" ORPHANS DETECTED:");
|
||||
for (const id of orphansDetected) {
|
||||
console.log(` ? ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (pruned.length > 0) {
|
||||
console.log(" PRUNED:");
|
||||
for (const id of pruned) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(" ERRORS:");
|
||||
for (const e of errors) {
|
||||
console.log(` ! ${e.id}: ${e.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
// Change CWD to project root so generator resolves files correctly
|
||||
process.chdir(projectRoot);
|
||||
|
||||
if (!jsonOutput) {
|
||||
const mode = applyMode ? "apply" : "dry-run";
|
||||
const prune = pruneMode ? " + prune" : "";
|
||||
const filter = onlyIds ? ` (only: ${onlyIds.join(", ")})` : "";
|
||||
console.log(`\nAgent Skills Generator [${mode}${prune}]${filter}`);
|
||||
console.log("─".repeat(60));
|
||||
}
|
||||
|
||||
// Dynamic import via tsx runtime — works because package.json has tsx devDep
|
||||
// and node is invoked with --import tsx/esm by the caller (or we use tsx directly).
|
||||
// To support plain `node` invocation, we use a dynamic import with tsx register.
|
||||
let generateAgentSkills;
|
||||
try {
|
||||
// Try direct import first (when running under tsx or compiled)
|
||||
const mod = await import("../../src/lib/agentSkills/generator.ts");
|
||||
generateAgentSkills = mod.generateAgentSkills;
|
||||
} catch (importErr) {
|
||||
// Fallback: try tsx register approach
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
// Register tsx for TypeScript support
|
||||
const tsxPath = require.resolve("tsx/esm");
|
||||
const { register } = await import("node:module");
|
||||
register(tsxPath, import.meta.url);
|
||||
const mod = await import("../../src/lib/agentSkills/generator.ts");
|
||||
generateAgentSkills = mod.generateAgentSkills;
|
||||
} catch (fallbackErr) {
|
||||
console.error(
|
||||
"Error: Could not import generator. Run with tsx:\n" +
|
||||
" node --import tsx/esm scripts/skills/generate-agent-skills.mjs\n" +
|
||||
`Import error: ${importErr instanceof Error ? importErr.message : String(importErr)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let report;
|
||||
try {
|
||||
report = await generateAgentSkills({
|
||||
dryRun: !applyMode,
|
||||
prune: pruneMode,
|
||||
outputDir: "skills",
|
||||
onlyIds,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (jsonOutput) {
|
||||
console.log(JSON.stringify({ error: msg }, null, 2));
|
||||
} else {
|
||||
console.error(`\nGenerator error: ${msg}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (jsonOutput) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
printTable(report);
|
||||
if (!applyMode) {
|
||||
console.log(
|
||||
"\n(dry-run) No files written. Use --apply to write SKILL.md files.\n",
|
||||
);
|
||||
} else {
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
// Exit code 2 if dry-run detected pending changes (useful for CI)
|
||||
if (!applyMode && report.generated.length > 0) {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Unexpected error:", err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user