Primeiro Commit v0.beta1
This commit is contained in:
6917
scripts/fetch-sales-intelligence.mjs
Normal file
6917
scripts/fetch-sales-intelligence.mjs
Normal file
File diff suppressed because it is too large
Load Diff
256
scripts/lib/auth-shared.mjs
Normal file
256
scripts/lib/auth-shared.mjs
Normal file
@@ -0,0 +1,256 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
function domainMatches(hostname, cookieDomain) {
|
||||
const normalized = String(cookieDomain || "").replace(/^\./, "").toLowerCase();
|
||||
const host = String(hostname || "").toLowerCase();
|
||||
if (!normalized || !host) return false;
|
||||
if (normalized === host) return true;
|
||||
if (normalized === "localhost") return host === "localhost";
|
||||
return host.endsWith(`.${normalized}`);
|
||||
}
|
||||
|
||||
export function normalizeSameSite(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
if (normalized === "strict") return "Strict";
|
||||
if (normalized === "none") return "None";
|
||||
return "Lax";
|
||||
}
|
||||
|
||||
export function normalizeCookie(cookie) {
|
||||
if (!cookie?.name || !cookie?.domain) return null;
|
||||
return {
|
||||
path: "/",
|
||||
expires: -1,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: "Lax",
|
||||
...cookie,
|
||||
domain: String(cookie.domain).replace(/^\./, "").toLowerCase(),
|
||||
path: cookie.path || "/",
|
||||
expires:
|
||||
Number.isFinite(cookie.expires) || cookie.expires === -1
|
||||
? cookie.expires
|
||||
: Number(cookie.expires) || -1,
|
||||
httpOnly: Boolean(cookie.httpOnly),
|
||||
secure: Boolean(cookie.secure),
|
||||
sameSite: normalizeSameSite(cookie.sameSite),
|
||||
};
|
||||
}
|
||||
|
||||
function findCookieIndex(cookies, candidate) {
|
||||
return cookies.findIndex(
|
||||
(cookie) =>
|
||||
cookie.name === candidate.name &&
|
||||
String(cookie.domain || "").replace(/^\./, "").toLowerCase() === candidate.domain &&
|
||||
(cookie.path || "/") === candidate.path
|
||||
);
|
||||
}
|
||||
|
||||
export function cookieHeaderFor(urlString, cookies) {
|
||||
const url = new URL(urlString);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return cookies
|
||||
.map(normalizeCookie)
|
||||
.filter((cookie) => {
|
||||
if (!cookie) return false;
|
||||
if (!domainMatches(url.hostname, cookie.domain)) return false;
|
||||
if (!url.pathname.startsWith(cookie.path || "/")) return false;
|
||||
if (cookie.secure && url.protocol !== "https:") return false;
|
||||
if (cookie.expires && cookie.expires !== -1 && cookie.expires < now) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((left, right) => (right.path?.length || 0) - (left.path?.length || 0))
|
||||
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
export function splitSetCookieHeader(headerValue) {
|
||||
if (!headerValue) return [];
|
||||
const parts = [];
|
||||
let current = "";
|
||||
let inExpires = false;
|
||||
for (let index = 0; index < headerValue.length; index += 1) {
|
||||
const char = headerValue[index];
|
||||
const next = headerValue.slice(index);
|
||||
if (!inExpires && next.toLowerCase().startsWith("expires=")) {
|
||||
inExpires = true;
|
||||
}
|
||||
if (char === "," && !inExpires) {
|
||||
const trimmed = current.trim();
|
||||
if (trimmed) parts.push(trimmed);
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
current += char;
|
||||
if (inExpires && char === ";") {
|
||||
inExpires = false;
|
||||
}
|
||||
}
|
||||
const trimmed = current.trim();
|
||||
if (trimmed) parts.push(trimmed);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function defaultCookiePath(url) {
|
||||
const pathname = url.pathname || "/";
|
||||
if (!pathname.startsWith("/") || pathname === "/") return "/";
|
||||
const lastSlash = pathname.lastIndexOf("/");
|
||||
return lastSlash <= 0 ? "/" : pathname.slice(0, lastSlash);
|
||||
}
|
||||
|
||||
export function mergeCookiesFromHeader(state, urlString, headerValue) {
|
||||
const url = new URL(urlString);
|
||||
const parts = splitSetCookieHeader(headerValue);
|
||||
let changed = false;
|
||||
for (const part of parts) {
|
||||
const segments = part.split(";").map((segment) => segment.trim()).filter(Boolean);
|
||||
const [nameValue, ...attributes] = segments;
|
||||
const eqIndex = nameValue?.indexOf("=") ?? -1;
|
||||
if (eqIndex <= 0) continue;
|
||||
const name = nameValue.slice(0, eqIndex);
|
||||
const value = nameValue.slice(eqIndex + 1);
|
||||
const nextCookie = {
|
||||
name,
|
||||
value,
|
||||
domain: url.hostname.toLowerCase(),
|
||||
path: defaultCookiePath(url),
|
||||
expires: -1,
|
||||
httpOnly: false,
|
||||
secure: url.protocol === "https:",
|
||||
sameSite: "Lax",
|
||||
};
|
||||
for (const attribute of attributes) {
|
||||
const separator = attribute.indexOf("=");
|
||||
const rawKey = (separator >= 0 ? attribute.slice(0, separator) : attribute).trim();
|
||||
const rawValue = separator >= 0 ? attribute.slice(separator + 1).trim() : "";
|
||||
const key = rawKey.toLowerCase();
|
||||
if (key === "domain") nextCookie.domain = rawValue.replace(/^\./, "").toLowerCase() || url.hostname.toLowerCase();
|
||||
if (key === "path") nextCookie.path = rawValue || "/";
|
||||
if (key === "secure") nextCookie.secure = true;
|
||||
if (key === "httponly") nextCookie.httpOnly = true;
|
||||
if (key === "samesite") nextCookie.sameSite = normalizeSameSite(rawValue);
|
||||
if (key === "expires") {
|
||||
const seconds = Math.floor(new Date(rawValue).getTime() / 1000);
|
||||
nextCookie.expires = Number.isFinite(seconds) ? seconds : -1;
|
||||
}
|
||||
if (key === "max-age") {
|
||||
const maxAge = Number(rawValue);
|
||||
nextCookie.maxAge = maxAge;
|
||||
nextCookie.expires = Number.isFinite(maxAge)
|
||||
? Math.floor(Date.now() / 1000) + maxAge
|
||||
: nextCookie.expires;
|
||||
}
|
||||
}
|
||||
const normalized = normalizeCookie(nextCookie);
|
||||
if (!normalized) continue;
|
||||
const existingIndex = findCookieIndex(state.cookies, normalized);
|
||||
if (normalized.expires === 0 || normalized.maxAge === 0) {
|
||||
if (existingIndex >= 0) {
|
||||
state.cookies.splice(existingIndex, 1);
|
||||
changed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (existingIndex >= 0) {
|
||||
const previous = JSON.stringify(normalizeCookie(state.cookies[existingIndex]));
|
||||
state.cookies[existingIndex] = { ...state.cookies[existingIndex], ...normalized };
|
||||
if (previous !== JSON.stringify(normalizeCookie(state.cookies[existingIndex]))) {
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
state.cookies.push(normalized);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function getHeaderValue(headers, name) {
|
||||
if (!headers) return null;
|
||||
if (typeof headers.get === "function") {
|
||||
return headers.get(name) || headers.get(name.toLowerCase()) || headers.get(name.toUpperCase()) || null;
|
||||
}
|
||||
return headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()] || null;
|
||||
}
|
||||
|
||||
export function extractClientBin(html, headers = null) {
|
||||
const fromHeader = getHeaderValue(headers, "x-bitech-svrbin");
|
||||
if (fromHeader) return fromHeader;
|
||||
return String(html || "").match(/oracle-jet-bundles\/[^.]+\.([a-z0-9]+)\//i)?.[1] || null;
|
||||
}
|
||||
|
||||
export function extractCsrfToken(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
for (const [key, candidate] of Object.entries(value)) {
|
||||
if (typeof candidate === "string" && key.toLowerCase() === "csrftoken") {
|
||||
return candidate;
|
||||
}
|
||||
const nested = extractCsrfToken(candidate);
|
||||
if (nested) return nested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function readJson(filePath, label) {
|
||||
let raw;
|
||||
try {
|
||||
raw = await fs.readFile(filePath, "utf8");
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") {
|
||||
throw new Error(
|
||||
`Could not find ${label} at ${filePath}. Run scripts/refresh-auth-direct.mjs first to create the session files.`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new Error(`Could not parse ${label} at ${filePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function persistSessionArtifacts(statePath, metaPath, state, meta, { ensureParentDir } = {}) {
|
||||
if (typeof ensureParentDir === "function") {
|
||||
await ensureParentDir(statePath);
|
||||
await ensureParentDir(metaPath);
|
||||
}
|
||||
await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||
await fs.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
export function replaceObjectContents(target, nextValue) {
|
||||
for (const key of Object.keys(target)) {
|
||||
delete target[key];
|
||||
}
|
||||
Object.assign(target, nextValue);
|
||||
return target;
|
||||
}
|
||||
|
||||
export function inferRuntimeHome(statePath, metaPath) {
|
||||
const stateDir = statePath ? path.dirname(statePath) : null;
|
||||
const metaDir = metaPath ? path.dirname(metaPath) : null;
|
||||
if (stateDir && metaDir && stateDir === metaDir) {
|
||||
return stateDir;
|
||||
}
|
||||
return stateDir || metaDir || null;
|
||||
}
|
||||
|
||||
export function isAuthRefreshRequiredError(error) {
|
||||
const message = String(error?.message || "");
|
||||
return [
|
||||
"Refresh auth state first",
|
||||
"Request redirected",
|
||||
"Session redirected to Oracle sign-in",
|
||||
"Oracle DV session restore redirected to Oracle sign-in",
|
||||
"Request redirected while restoring Oracle DV session",
|
||||
"Oracle DV session is not valid anymore",
|
||||
"Could not find auth state",
|
||||
"Could not find session metadata",
|
||||
"Session metadata at",
|
||||
"Expected JSON",
|
||||
].some((fragment) => message.includes(fragment));
|
||||
}
|
||||
30
scripts/lib/cli-shared.mjs
Normal file
30
scripts/lib/cli-shared.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
export function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const token = argv[index];
|
||||
if (!token.startsWith("--")) continue;
|
||||
const key = token.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
args[key] = "true";
|
||||
continue;
|
||||
}
|
||||
const valueParts = [next];
|
||||
let valueIndex = index + 2;
|
||||
while (valueIndex < argv.length && !argv[valueIndex].startsWith("--")) {
|
||||
valueParts.push(argv[valueIndex]);
|
||||
valueIndex += 1;
|
||||
}
|
||||
args[key] = valueParts.join(" ");
|
||||
index = valueIndex - 1;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
export function splitCsv(value, fallback = []) {
|
||||
if (!value) return fallback;
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
340
scripts/lib/date-windows.mjs
Normal file
340
scripts/lib/date-windows.mjs
Normal file
@@ -0,0 +1,340 @@
|
||||
const ANNUAL_WEEK_START_MONTH_INDEX = 5;
|
||||
const ANNUAL_WEEK_START_DAY = 1;
|
||||
const CURRENT_PERIOD_TOKENS = new Set([
|
||||
"current",
|
||||
"current-week",
|
||||
"this-week",
|
||||
"week-current",
|
||||
"semana-atual",
|
||||
"week atual",
|
||||
]);
|
||||
const CURRENT_MONTH_TOKENS = new Set([
|
||||
"current",
|
||||
"current-month",
|
||||
"this-month",
|
||||
"month-current",
|
||||
"mes-atual",
|
||||
"mês-atual",
|
||||
"mes atual",
|
||||
"mês atual",
|
||||
]);
|
||||
const CURRENT_QUARTER_TOKENS = new Set([
|
||||
"current",
|
||||
"current-quarter",
|
||||
"this-quarter",
|
||||
"quarter-current",
|
||||
"quarter atual",
|
||||
"trimestre-atual",
|
||||
"trimestre atual",
|
||||
]);
|
||||
const CURRENT_FISCAL_YEAR_TOKENS = new Set([
|
||||
"current",
|
||||
"current-fy",
|
||||
"this-fy",
|
||||
"fy-current",
|
||||
"fy atual",
|
||||
"ano-fiscal-atual",
|
||||
"ano fiscal atual",
|
||||
]);
|
||||
|
||||
function quoteLiteral(value) {
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function lastDayOfMonth(year, monthIndex) {
|
||||
return new Date(year, monthIndex + 1, 0);
|
||||
}
|
||||
|
||||
function formatIsoDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function addDays(date, days) {
|
||||
const next = new Date(date.getTime());
|
||||
next.setDate(next.getDate() + days);
|
||||
return next;
|
||||
}
|
||||
|
||||
function isLeapYear(year) {
|
||||
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||||
}
|
||||
|
||||
function isValidIsoCalendarDate(raw) {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
||||
if (!match) return false;
|
||||
const [, yearText, monthText, dayText] = match;
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
const day = Number(dayText);
|
||||
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
||||
return false;
|
||||
}
|
||||
if (month < 1 || month > 12) return false;
|
||||
const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
return day >= 1 && day <= daysInMonth[month - 1];
|
||||
}
|
||||
|
||||
function resolveDateInput(value, fallback) {
|
||||
const raw = String(value || fallback || "").trim();
|
||||
if (!raw) {
|
||||
throw new Error("Date input is required.");
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
||||
if (!isValidIsoCalendarDate(raw)) {
|
||||
throw new Error(`Invalid calendar date "${raw}". Use a real date in YYYY-MM-DD format.`);
|
||||
}
|
||||
return { input: raw, isoDate: raw };
|
||||
}
|
||||
if (raw.toLowerCase() === "today") {
|
||||
return { input: raw, isoDate: formatIsoDate(new Date()) };
|
||||
}
|
||||
const relativeMatch = /^(?:today)?([+-]\d+)d$/i.exec(raw);
|
||||
if (relativeMatch) {
|
||||
const days = Number(relativeMatch[1]);
|
||||
return { input: raw, isoDate: formatIsoDate(addDays(new Date(), days)) };
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported date token "${raw}". Use YYYY-MM-DD, today, +7d, -3d, or today+7d.`
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMonthInput(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (CURRENT_MONTH_TOKENS.has(raw.toLowerCase())) {
|
||||
const today = new Date();
|
||||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const last = lastDayOfMonth(today.getFullYear(), today.getMonth());
|
||||
return {
|
||||
input: raw,
|
||||
fromDate: formatIsoDate(first),
|
||||
toDate: formatIsoDate(last),
|
||||
};
|
||||
}
|
||||
if (!/^\d{4}-\d{2}$/.test(raw)) {
|
||||
throw new Error(`Unsupported month token "${raw}". Use YYYY-MM, for example 2026-04.`);
|
||||
}
|
||||
const [yearText, monthText] = raw.split("-");
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
|
||||
throw new Error(`Unsupported month token "${raw}". Use YYYY-MM, for example 2026-04.`);
|
||||
}
|
||||
const first = new Date(year, month - 1, 1);
|
||||
const last = lastDayOfMonth(year, month - 1);
|
||||
return {
|
||||
input: raw,
|
||||
fromDate: formatIsoDate(first),
|
||||
toDate: formatIsoDate(last),
|
||||
};
|
||||
}
|
||||
|
||||
function fiscalQuarterToDateWindow(fiscalYear, quarter) {
|
||||
const normalizedFiscalYear = String(fiscalYear || "").trim().toUpperCase();
|
||||
const normalizedQuarter = String(quarter || "").trim().toUpperCase();
|
||||
const fiscalMatch = /^FY(\d{2,4})$/.exec(normalizedFiscalYear);
|
||||
const quarterMatch = /^Q([1-4])$/.exec(normalizedQuarter);
|
||||
if (!fiscalMatch || !quarterMatch) {
|
||||
throw new Error(`Unsupported fiscal quarter token "${fiscalYear} ${quarter}". Use FY26-Q4 or Q4 FY26.`);
|
||||
}
|
||||
const endYear = fiscalMatch[1].length === 2 ? 2000 + Number(fiscalMatch[1]) : Number(fiscalMatch[1]);
|
||||
const quarterNumber = Number(quarterMatch[1]);
|
||||
const startYear = endYear - 1;
|
||||
const quarterWindows = {
|
||||
1: { from: new Date(startYear, 5, 1), to: lastDayOfMonth(startYear, 7) },
|
||||
2: { from: new Date(startYear, 8, 1), to: lastDayOfMonth(startYear, 10) },
|
||||
3: { from: new Date(startYear, 11, 1), to: lastDayOfMonth(endYear, 1) },
|
||||
4: { from: new Date(endYear, 2, 1), to: lastDayOfMonth(endYear, 4) },
|
||||
};
|
||||
return {
|
||||
fiscalYear: normalizedFiscalYear,
|
||||
quarter: `Q${quarterNumber}`,
|
||||
fromDate: formatIsoDate(quarterWindows[quarterNumber].from),
|
||||
toDate: formatIsoDate(quarterWindows[quarterNumber].to),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveQuarterInput(value) {
|
||||
const raw = String(value || "").trim();
|
||||
const normalized = raw.toLowerCase();
|
||||
if (CURRENT_QUARTER_TOKENS.has(normalized)) {
|
||||
const today = new Date();
|
||||
const cycleYear = resolveWeekYearInput("");
|
||||
const monthIndex = today.getMonth();
|
||||
let quarter = "Q1";
|
||||
if (monthIndex >= 5 && monthIndex <= 7) {
|
||||
quarter = "Q1";
|
||||
} else if (monthIndex >= 8 && monthIndex <= 10) {
|
||||
quarter = "Q2";
|
||||
} else if (monthIndex === 11 || monthIndex <= 1) {
|
||||
quarter = "Q3";
|
||||
} else {
|
||||
quarter = "Q4";
|
||||
}
|
||||
const fiscalYear = `FY${String(cycleYear + 1).padStart(2, "0").slice(-2)}`;
|
||||
return {
|
||||
input: raw,
|
||||
...fiscalQuarterToDateWindow(fiscalYear, quarter),
|
||||
};
|
||||
}
|
||||
const compactMatch = /^(FY\d{2,4})[-\s]Q([1-4])$/i.exec(raw);
|
||||
if (compactMatch) {
|
||||
return {
|
||||
input: raw,
|
||||
...fiscalQuarterToDateWindow(compactMatch[1], `Q${compactMatch[2]}`),
|
||||
};
|
||||
}
|
||||
const reversedMatch = /^Q([1-4])[-\s]?(FY\d{2,4})$/i.exec(raw);
|
||||
if (reversedMatch) {
|
||||
return {
|
||||
input: raw,
|
||||
...fiscalQuarterToDateWindow(reversedMatch[2], `Q${reversedMatch[1]}`),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unsupported quarter token "${raw}". Use FY26-Q4, Q4 FY26, or current-quarter.`);
|
||||
}
|
||||
|
||||
export function resolveWeekYearInput(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw || CURRENT_FISCAL_YEAR_TOKENS.has(raw.toLowerCase())) {
|
||||
const today = new Date();
|
||||
const cycleStart = new Date(today.getFullYear(), ANNUAL_WEEK_START_MONTH_INDEX, ANNUAL_WEEK_START_DAY);
|
||||
return today >= cycleStart ? today.getFullYear() : today.getFullYear() - 1;
|
||||
}
|
||||
if (!/^\d{4}$/.test(raw)) {
|
||||
throw new Error(`Unsupported week year token "${raw}". Use the cycle start year YYYY, for example 2025.`);
|
||||
}
|
||||
return Number(raw);
|
||||
}
|
||||
|
||||
export function resolveAnnualWeekInput(value, weekYear) {
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
if (CURRENT_PERIOD_TOKENS.has(raw)) {
|
||||
const year = resolveWeekYearInput(weekYear);
|
||||
const cycleStart = new Date(year, ANNUAL_WEEK_START_MONTH_INDEX, ANNUAL_WEEK_START_DAY);
|
||||
const cycleEnd = new Date(year + 1, ANNUAL_WEEK_START_MONTH_INDEX, 0);
|
||||
const today = new Date();
|
||||
const boundedToday =
|
||||
today < cycleStart ? cycleStart : today > cycleEnd ? cycleEnd : today;
|
||||
const week = Math.floor((boundedToday - cycleStart) / (7 * 24 * 60 * 60 * 1000)) + 1;
|
||||
return resolveAnnualWeekInput(`w${week}`, year);
|
||||
}
|
||||
const match = raw.match(/^(?:w|week\s*)?([1-9]|[1-4]\d|5[0-3])$/);
|
||||
if (!match) {
|
||||
throw new Error(`Unsupported week token "${value}". Use 1..53, w1..w53, or week 1..week 53.`);
|
||||
}
|
||||
const week = Number(match[1]);
|
||||
const year = resolveWeekYearInput(weekYear);
|
||||
const cycleStart = new Date(year, ANNUAL_WEEK_START_MONTH_INDEX, ANNUAL_WEEK_START_DAY);
|
||||
const cycleEnd = new Date(year + 1, ANNUAL_WEEK_START_MONTH_INDEX, 0);
|
||||
const weekStart = addDays(cycleStart, (week - 1) * 7);
|
||||
if (weekStart > cycleEnd) {
|
||||
throw new Error(`Week ${week} is outside the ${year}-${year + 1} cycle. Use 1..53.`);
|
||||
}
|
||||
const provisionalWeekEnd = addDays(weekStart, 6);
|
||||
const weekEnd = provisionalWeekEnd > cycleEnd ? cycleEnd : provisionalWeekEnd;
|
||||
return {
|
||||
input: String(value || "").trim(),
|
||||
week,
|
||||
weekLabel: `W${week}`,
|
||||
year,
|
||||
fromDate: formatIsoDate(weekStart),
|
||||
toDate: formatIsoDate(weekEnd),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDateWindow(args, defaults = { from: "today", to: "+7d" }) {
|
||||
const hasSingleDate = Boolean(args.date);
|
||||
const hasMonth = Boolean(args.month);
|
||||
const hasWeek = Boolean(args.week);
|
||||
const hasQuarter = Boolean(args.quarter);
|
||||
const hasExplicitWindow = Boolean(args["from-date"] || args["to-date"]);
|
||||
if (
|
||||
[hasSingleDate, hasMonth, hasWeek, hasQuarter].filter(Boolean).length > 1 ||
|
||||
((hasSingleDate || hasMonth || hasWeek || hasQuarter) && hasExplicitWindow)
|
||||
) {
|
||||
throw new Error(
|
||||
"Use either --date, --month, --quarter, --week, or the pair --from-date/--to-date for date-window queries."
|
||||
);
|
||||
}
|
||||
if (hasSingleDate) {
|
||||
const exact = resolveDateInput(args.date);
|
||||
return {
|
||||
fromDateInput: exact.input,
|
||||
toDateInput: exact.input,
|
||||
fromDate: exact.isoDate,
|
||||
toDate: exact.isoDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(exact.isoDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(exact.isoDate)}`,
|
||||
preset: "single-date",
|
||||
};
|
||||
}
|
||||
if (hasMonth) {
|
||||
const month = resolveMonthInput(args.month);
|
||||
return {
|
||||
fromDateInput: month.input,
|
||||
toDateInput: month.input,
|
||||
fromDate: month.fromDate,
|
||||
toDate: month.toDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(month.fromDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(month.toDate)}`,
|
||||
preset: "calendar-month",
|
||||
};
|
||||
}
|
||||
if (hasQuarter) {
|
||||
const quarter = resolveQuarterInput(args.quarter);
|
||||
return {
|
||||
fromDateInput: quarter.input,
|
||||
toDateInput: quarter.input,
|
||||
fromDate: quarter.fromDate,
|
||||
toDate: quarter.toDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(quarter.fromDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(quarter.toDate)}`,
|
||||
preset: "fiscal-quarter",
|
||||
fiscalYear: quarter.fiscalYear,
|
||||
fiscalQuarter: quarter.quarter,
|
||||
};
|
||||
}
|
||||
if (hasWeek) {
|
||||
const week = resolveAnnualWeekInput(args.week, args["week-year"]);
|
||||
return {
|
||||
fromDateInput: week.input,
|
||||
toDateInput: week.input,
|
||||
fromDate: week.fromDate,
|
||||
toDate: week.toDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(week.fromDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(week.toDate)}`,
|
||||
preset: "annual-week",
|
||||
week: week.week,
|
||||
weekLabel: week.weekLabel,
|
||||
weekYear: week.year,
|
||||
};
|
||||
}
|
||||
const from = resolveDateInput(args["from-date"], defaults.from);
|
||||
const to = resolveDateInput(args["to-date"], defaults.to);
|
||||
if (from.isoDate > to.isoDate) {
|
||||
throw new Error(`Invalid date window: from-date ${from.isoDate} is after to-date ${to.isoDate}.`);
|
||||
}
|
||||
return {
|
||||
fromDateInput: from.input,
|
||||
toDateInput: to.input,
|
||||
fromDate: from.isoDate,
|
||||
toDate: to.isoDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(from.isoDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(to.isoDate)}`,
|
||||
preset: "date-window",
|
||||
};
|
||||
}
|
||||
|
||||
export function hasAnyDateWindowArgs(args) {
|
||||
return Boolean(
|
||||
args.date ||
|
||||
args.month ||
|
||||
args.quarter ||
|
||||
args.week ||
|
||||
args["from-date"] ||
|
||||
args["to-date"]
|
||||
);
|
||||
}
|
||||
200
scripts/lib/output-contract.mjs
Normal file
200
scripts/lib/output-contract.mjs
Normal file
@@ -0,0 +1,200 @@
|
||||
import fs from "node:fs/promises";
|
||||
import {
|
||||
ensureParentDir,
|
||||
resolveOutputFilePath,
|
||||
} from "./runtime-paths.mjs";
|
||||
|
||||
const NULL_SENTINEL = "__CODEX_NULL__";
|
||||
const MONETARY_FIELD_KEYS = new Set(["bookingValue", "opportunityValue", "workloadAmount", "pipelineAmount"]);
|
||||
const THOUSANDS_DISPLAY_KEYS = new Set([
|
||||
...MONETARY_FIELD_KEYS,
|
||||
"totalBookingValue",
|
||||
"totalOpportunityValue",
|
||||
"totalWorkloadAmount",
|
||||
"totalPipelineAmount",
|
||||
]);
|
||||
const THOUSANDS_DISPLAY_SOURCE_FIELDS = new Set(MONETARY_FIELD_KEYS);
|
||||
const ORACLE_MONTHS = {
|
||||
JAN: "01",
|
||||
FEB: "02",
|
||||
MAR: "03",
|
||||
APR: "04",
|
||||
MAY: "05",
|
||||
JUN: "06",
|
||||
JUL: "07",
|
||||
AUG: "08",
|
||||
SEP: "09",
|
||||
OCT: "10",
|
||||
NOV: "11",
|
||||
DEC: "12",
|
||||
};
|
||||
|
||||
export function buildWarnings({ requestedLimit, recordsCount, customSql, customSqlColumnsResolved = false }) {
|
||||
const warnings = [];
|
||||
if (requestedLimit > 0 && recordsCount >= requestedLimit) {
|
||||
warnings.push(
|
||||
`The query returned ${recordsCount} rows, which matches the requested limit (${requestedLimit}). Results may be truncated; rerun with a higher --limit if you need a complete extract.`
|
||||
);
|
||||
}
|
||||
if (customSql && !customSqlColumnsResolved) {
|
||||
warnings.push("Custom SQL records are returned as raw row arrays because the Oracle DV response does not guarantee stable field aliases.");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
export function parseNumericValue(value) {
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
||||
const normalized = String(value ?? "")
|
||||
.replace(/,/g, "")
|
||||
.trim();
|
||||
if (!normalized) return 0;
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function isLikelyMonetaryFieldName(fieldName) {
|
||||
return /booking|workload|pipeline|opportunityvalue/i.test(String(fieldName || ""));
|
||||
}
|
||||
|
||||
function formatThousandsDisplayValue(value) {
|
||||
const thousands = Math.trunc(parseNumericValue(value) / 1000);
|
||||
const grouped = String(thousands).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
return `${grouped}k`;
|
||||
}
|
||||
|
||||
function formatDisplaySection(node, displayKeys = THOUSANDS_DISPLAY_KEYS) {
|
||||
if (Array.isArray(node)) {
|
||||
return node.map((item) => formatDisplaySection(item, displayKeys));
|
||||
}
|
||||
if (!node || typeof node !== "object") {
|
||||
return node;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(node).map(([key, value]) => {
|
||||
if ((displayKeys.has(key) || isLikelyMonetaryFieldName(key)) && value !== null && value !== undefined && value !== "") {
|
||||
return [key, formatThousandsDisplayValue(value)];
|
||||
}
|
||||
return [key, formatDisplaySection(value, displayKeys)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function collectThousandsDisplayKeys(payload) {
|
||||
const displayKeys = new Set(THOUSANDS_DISPLAY_KEYS);
|
||||
for (const columnName of payload?.columns || []) {
|
||||
if (MONETARY_FIELD_KEYS.has(columnName) || isLikelyMonetaryFieldName(columnName)) {
|
||||
displayKeys.add(columnName);
|
||||
}
|
||||
}
|
||||
const querySpec = payload?.request?.query;
|
||||
if (querySpec && typeof querySpec === "object") {
|
||||
for (const item of [...(querySpec.select || []), ...(querySpec.measure || [])]) {
|
||||
if (!item || typeof item !== "object") continue;
|
||||
if (!THOUSANDS_DISPLAY_SOURCE_FIELDS.has(item.fieldName)) continue;
|
||||
if (item.outputName) displayKeys.add(item.outputName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const inferredColumn of payload?.request?.inferredColumns || []) {
|
||||
if (MONETARY_FIELD_KEYS.has(inferredColumn) || isLikelyMonetaryFieldName(inferredColumn)) {
|
||||
displayKeys.add(inferredColumn);
|
||||
}
|
||||
}
|
||||
|
||||
return displayKeys;
|
||||
}
|
||||
|
||||
function rowsPulledFromApi(result) {
|
||||
return Array.isArray(result?.aResults) ? result.aResults.length : 0;
|
||||
}
|
||||
|
||||
function buildApiQueryMetric({
|
||||
label,
|
||||
result = null,
|
||||
rowsReturned = null,
|
||||
source = null,
|
||||
countedInTotal = true,
|
||||
} = {}) {
|
||||
const resolvedRowsReturned = Number.isFinite(rowsReturned) ? rowsReturned : rowsPulledFromApi(result);
|
||||
return {
|
||||
label: label || "query",
|
||||
rowsPulledFromApi: resolvedRowsReturned,
|
||||
source: source || null,
|
||||
countedInTotal: countedInTotal !== false,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiMetrics(entries = []) {
|
||||
const queries = entries
|
||||
.filter(Boolean)
|
||||
.map((entry) =>
|
||||
buildApiQueryMetric({
|
||||
label: entry.label,
|
||||
result: entry.result,
|
||||
rowsReturned: entry.rowsReturned,
|
||||
source: entry.source,
|
||||
countedInTotal: entry.countedInTotal,
|
||||
})
|
||||
);
|
||||
const totalRowsPulledFromApi = queries.reduce(
|
||||
(sum, query) => sum + (query.countedInTotal ? query.rowsPulledFromApi : 0),
|
||||
0
|
||||
);
|
||||
return {
|
||||
totalRowsPulledFromApi,
|
||||
queries,
|
||||
explanation: `Raw rows pulled from API: ${totalRowsPulledFromApi}.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function preparePayloadForOutput(payload) {
|
||||
const displayKeys = collectThousandsDisplayKeys(payload);
|
||||
return {
|
||||
...payload,
|
||||
apiMetrics: payload?.apiMetrics || buildApiMetrics(),
|
||||
...(Object.prototype.hasOwnProperty.call(payload, "records")
|
||||
? { records: formatDisplaySection(payload.records, displayKeys) }
|
||||
: {}),
|
||||
...(payload.summary && typeof payload.summary === "object"
|
||||
? { summary: formatDisplaySection(payload.summary, displayKeys) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOracleDateString(value) {
|
||||
if (typeof value !== "string") return value;
|
||||
const match = /^(\d{2})-([A-Z]{3})-(\d{2}|\d{4})$/.exec(value.trim().toUpperCase());
|
||||
if (!match) return value;
|
||||
const [, day, monthToken, yearToken] = match;
|
||||
const month = ORACLE_MONTHS[monthToken];
|
||||
if (!month) return value;
|
||||
const year =
|
||||
yearToken.length === 4
|
||||
? yearToken
|
||||
: Number(yearToken) >= 70
|
||||
? `19${yearToken}`
|
||||
: `20${yearToken}`;
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function normalizeCell(value) {
|
||||
if (value === NULL_SENTINEL) return null;
|
||||
return normalizeOracleDateString(value ?? null);
|
||||
}
|
||||
|
||||
export function mapRows(fields, rows) {
|
||||
return rows.map((row) =>
|
||||
Object.fromEntries(fields.map((field, index) => [field, normalizeCell(row[index])]))
|
||||
);
|
||||
}
|
||||
|
||||
export function emitJson(args, payload, runtimeHome) {
|
||||
const json = JSON.stringify(preparePayloadForOutput(payload), null, 2);
|
||||
const outputFilePath = resolveOutputFilePath(args["output-file"], runtimeHome);
|
||||
if (outputFilePath) {
|
||||
return ensureParentDir(outputFilePath)
|
||||
.then(() => fs.writeFile(outputFilePath, `${json}\n`, "utf8"))
|
||||
.then(() => json);
|
||||
}
|
||||
return Promise.resolve(json);
|
||||
}
|
||||
351
scripts/lib/runtime-paths.mjs
Normal file
351
scripts/lib/runtime-paths.mjs
Normal file
@@ -0,0 +1,351 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const SKILL_SLUG = "knowledge-one-view-cli";
|
||||
export const LEGACY_SKILL_SLUG = "oracle-sales-intelligence-direct";
|
||||
export const DISPLAY_RUNTIME_HOME = "<workspace>/knowledge-one-view-cli/runtime";
|
||||
export const DISPLAY_STATE_PATH = "<workspace>/knowledge-one-view-cli/runtime/auth-state.json";
|
||||
export const DISPLAY_META_PATH = "<workspace>/knowledge-one-view-cli/runtime/session-meta.json";
|
||||
export const DISPLAY_PROFILE_DIR = "<workspace>/knowledge-one-view-cli/runtime/chrome-profile";
|
||||
export const DISPLAY_CHROME_PATH = "auto-detected by platform";
|
||||
export const LEGACY_DISPLAY_HOME = `~/.codex/${LEGACY_SKILL_SLUG}`;
|
||||
|
||||
const helperDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const SKILL_HOME = path.resolve(helperDir, "..", "..");
|
||||
export const CODEX_HOME = path.join(os.homedir(), ".codex");
|
||||
export const LEGACY_HOME = path.join(os.homedir(), ".codex", LEGACY_SKILL_SLUG);
|
||||
export const LEGACY_STATE_PATH = path.join(LEGACY_HOME, "auth-state.json");
|
||||
export const LEGACY_META_PATH = path.join(LEGACY_HOME, "session-meta.json");
|
||||
export const LEGACY_PROFILE_DIR = path.join(LEGACY_HOME, "chrome-profile");
|
||||
|
||||
function pathExists(targetPath) {
|
||||
return Boolean(targetPath) && fs.existsSync(targetPath);
|
||||
}
|
||||
|
||||
function normalizeDir(targetPath) {
|
||||
return path.resolve(targetPath);
|
||||
}
|
||||
|
||||
function normalizeWorkspaceRuntimeHomeCandidate(targetPath) {
|
||||
const resolved = normalizeDir(targetPath);
|
||||
const runtimeName = path.basename(resolved).toLowerCase();
|
||||
if (runtimeName === "runtime") {
|
||||
return resolved;
|
||||
}
|
||||
if (!runtimeName.startsWith("runtime-")) {
|
||||
return resolved;
|
||||
}
|
||||
const parentDir = path.dirname(resolved);
|
||||
if (path.basename(parentDir) !== SKILL_SLUG) {
|
||||
return resolved;
|
||||
}
|
||||
// Keep one canonical runtime path in the workspace skill folder.
|
||||
return path.join(parentDir, "runtime");
|
||||
}
|
||||
|
||||
function looksLikeTemporaryFileName(fileName) {
|
||||
const lower = String(fileName || "").toLowerCase();
|
||||
return (
|
||||
lower.startsWith("tmp-") ||
|
||||
lower.startsWith("temp-") ||
|
||||
lower.startsWith("scratch-") ||
|
||||
lower.startsWith("intermediate-") ||
|
||||
lower.startsWith("render-output-") ||
|
||||
lower.includes(".tmp.") ||
|
||||
lower.endsWith(".tmp") ||
|
||||
lower.endsWith(".temp")
|
||||
);
|
||||
}
|
||||
|
||||
function isSameOrInside(targetPath, parentPath) {
|
||||
const relative = path.relative(parentPath, targetPath);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function looksLikeInstalledSkillHome(targetPath) {
|
||||
return ["SKILL.md", "scripts", "docs", "references", "tests"].some((name) =>
|
||||
pathExists(path.join(targetPath, name))
|
||||
);
|
||||
}
|
||||
|
||||
function looksLikeWorkspaceSkillHome(targetPath) {
|
||||
if (path.basename(targetPath) !== SKILL_SLUG) return false;
|
||||
if (looksLikeInstalledSkillHome(targetPath)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function candidateChromePaths() {
|
||||
if (process.platform === "win32") {
|
||||
return [
|
||||
process.env.ORACLE_SI_CHROME_PATH,
|
||||
process.env.LOCALAPPDATA
|
||||
? path.join(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")
|
||||
: null,
|
||||
process.env["PROGRAMFILES"]
|
||||
? path.join(process.env["PROGRAMFILES"], "Google", "Chrome", "Application", "chrome.exe")
|
||||
: null,
|
||||
process.env["PROGRAMFILES(X86)"]
|
||||
? path.join(process.env["PROGRAMFILES(X86)"], "Google", "Chrome", "Application", "chrome.exe")
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return [
|
||||
process.env.ORACLE_SI_CHROME_PATH,
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
return [
|
||||
process.env.ORACLE_SI_CHROME_PATH,
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
export function resolveWorkspaceRoot(candidatePath = process.cwd()) {
|
||||
const cwd = normalizeDir(candidatePath);
|
||||
const codexHome = normalizeDir(CODEX_HOME);
|
||||
if (isSameOrInside(cwd, codexHome)) {
|
||||
throw new Error(
|
||||
`Cannot infer a workspace root from ${cwd}. Run this command from the target workspace or pass --runtime-home (or ORACLE_SI_RUNTIME_HOME) explicitly. Refusing to create runtime files inside ${codexHome}.`
|
||||
);
|
||||
}
|
||||
if (looksLikeWorkspaceSkillHome(cwd)) {
|
||||
return path.dirname(cwd);
|
||||
}
|
||||
return cwd;
|
||||
}
|
||||
|
||||
export function buildRuntimeHomeFromWorkspace(workspaceRoot) {
|
||||
return path.join(normalizeDir(workspaceRoot), SKILL_SLUG, "runtime");
|
||||
}
|
||||
|
||||
export function buildLegacyRuntimeHomeFromWorkspace(workspaceRoot) {
|
||||
return path.join(normalizeDir(workspaceRoot), LEGACY_SKILL_SLUG, "runtime");
|
||||
}
|
||||
|
||||
export function resolveWorkspaceSkillHome(runtimeHome) {
|
||||
return path.dirname(resolveRuntimeHome(runtimeHome));
|
||||
}
|
||||
|
||||
export function resolveLegacyWorkspaceSkillHome(runtimeHome) {
|
||||
return path.dirname(resolveLegacyRuntimeHomeCandidate(runtimeHome));
|
||||
}
|
||||
|
||||
export function resolveRuntimeHome(explicitPath) {
|
||||
if (explicitPath) return normalizeWorkspaceRuntimeHomeCandidate(explicitPath);
|
||||
if (process.env.ORACLE_SI_RUNTIME_HOME) {
|
||||
return normalizeWorkspaceRuntimeHomeCandidate(process.env.ORACLE_SI_RUNTIME_HOME);
|
||||
}
|
||||
return buildRuntimeHomeFromWorkspace(resolveWorkspaceRoot());
|
||||
}
|
||||
|
||||
export function resolveLegacyRuntimeHomeCandidate(runtimeHome) {
|
||||
const resolvedRuntimeHome = resolveRuntimeHome(runtimeHome);
|
||||
const workspaceRoot = path.dirname(path.dirname(resolvedRuntimeHome));
|
||||
return buildLegacyRuntimeHomeFromWorkspace(workspaceRoot);
|
||||
}
|
||||
|
||||
export function resolveOutputDir(runtimeHome) {
|
||||
return path.join(resolveWorkspaceSkillHome(runtimeHome), "output");
|
||||
}
|
||||
|
||||
export function resolveOutputFilePath(explicitPath, runtimeHome) {
|
||||
if (!explicitPath) return null;
|
||||
if (path.isAbsolute(explicitPath)) return path.resolve(explicitPath);
|
||||
|
||||
const workspaceSkillHome = resolveWorkspaceSkillHome(runtimeHome);
|
||||
if (path.dirname(explicitPath) === ".") {
|
||||
// Keep user-facing/final outputs in output/, but route temporary/intermediate
|
||||
// file names directly into tmp/ to avoid polluting output/.
|
||||
const fileName = path.basename(explicitPath);
|
||||
if (looksLikeTemporaryFileName(fileName)) {
|
||||
return path.join(resolveTmpDir(runtimeHome), fileName);
|
||||
}
|
||||
return path.join(resolveOutputDir(runtimeHome), explicitPath);
|
||||
}
|
||||
|
||||
return path.join(workspaceSkillHome, explicitPath);
|
||||
}
|
||||
|
||||
export function resolveTmpDir(runtimeHome) {
|
||||
return path.join(resolveWorkspaceSkillHome(runtimeHome), "tmp");
|
||||
}
|
||||
|
||||
export function resolveTestRunsDir(runtimeHome) {
|
||||
return path.join(resolveOutputDir(runtimeHome), "test-runs");
|
||||
}
|
||||
|
||||
export function resolveTmpTestInputsDir(runtimeHome) {
|
||||
return resolveTmpDir(runtimeHome);
|
||||
}
|
||||
|
||||
export function resolveTmpRenderedSqlDir(runtimeHome) {
|
||||
return resolveTmpDir(runtimeHome);
|
||||
}
|
||||
|
||||
export function resolveTmpScratchDir(runtimeHome) {
|
||||
return resolveTmpDir(runtimeHome);
|
||||
}
|
||||
|
||||
export function resolveStateWritePath(options = {}) {
|
||||
if (options.explicitPath) return path.resolve(options.explicitPath);
|
||||
if (process.env.ORACLE_SI_STATE_PATH) return path.resolve(process.env.ORACLE_SI_STATE_PATH);
|
||||
return path.join(resolveRuntimeHome(options.runtimeHome), "auth-state.json");
|
||||
}
|
||||
|
||||
export function resolveMetaWritePath(options = {}) {
|
||||
if (options.explicitPath) return path.resolve(options.explicitPath);
|
||||
if (process.env.ORACLE_SI_META_PATH) return path.resolve(process.env.ORACLE_SI_META_PATH);
|
||||
return path.join(resolveRuntimeHome(options.runtimeHome), "session-meta.json");
|
||||
}
|
||||
|
||||
export function resolveProfileDir(options = {}) {
|
||||
if (options.explicitPath) return path.resolve(options.explicitPath);
|
||||
if (process.env.ORACLE_SI_PROFILE_DIR) return path.resolve(process.env.ORACLE_SI_PROFILE_DIR);
|
||||
return path.join(resolveRuntimeHome(options.runtimeHome), "chrome-profile");
|
||||
}
|
||||
|
||||
export function resolveStateReadPath(options = {}) {
|
||||
const preferred = resolveStateWritePath(options);
|
||||
if (pathExists(preferred)) return preferred;
|
||||
const legacyWorkspacePath = path.join(
|
||||
resolveLegacyRuntimeHomeCandidate(options.runtimeHome),
|
||||
"auth-state.json"
|
||||
);
|
||||
if (pathExists(legacyWorkspacePath)) return legacyWorkspacePath;
|
||||
return pathExists(LEGACY_STATE_PATH) ? LEGACY_STATE_PATH : preferred;
|
||||
}
|
||||
|
||||
export function resolveMetaReadPath(options = {}) {
|
||||
const preferred = resolveMetaWritePath(options);
|
||||
if (pathExists(preferred)) return preferred;
|
||||
const legacyWorkspacePath = path.join(
|
||||
resolveLegacyRuntimeHomeCandidate(options.runtimeHome),
|
||||
"session-meta.json"
|
||||
);
|
||||
if (pathExists(legacyWorkspacePath)) return legacyWorkspacePath;
|
||||
return pathExists(LEGACY_META_PATH) ? LEGACY_META_PATH : preferred;
|
||||
}
|
||||
|
||||
export function resolveChromePath(explicitPath) {
|
||||
if (explicitPath) return path.resolve(explicitPath);
|
||||
if (process.env.ORACLE_SI_CHROME_PATH) return path.resolve(process.env.ORACLE_SI_CHROME_PATH);
|
||||
|
||||
for (const candidate of candidateChromePaths()) {
|
||||
if (pathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return candidateChromePaths()[0] || "google-chrome";
|
||||
}
|
||||
|
||||
export async function ensureDir(dirPath) {
|
||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
export async function ensureParentDir(filePath) {
|
||||
await ensureDir(path.dirname(filePath));
|
||||
}
|
||||
|
||||
async function migrateLegacyTmpSubdirs(tmpDir) {
|
||||
const legacySubdirs = ["test-inputs", "rendered-sql", "scratch"];
|
||||
const moveFileToTmpRoot = async (sourcePath) => {
|
||||
const ext = path.extname(sourcePath);
|
||||
const base = path.basename(sourcePath, ext);
|
||||
let candidateName = path.basename(sourcePath);
|
||||
let targetPath = path.join(tmpDir, candidateName);
|
||||
let suffix = 1;
|
||||
|
||||
while (pathExists(targetPath)) {
|
||||
candidateName = `${base}-migrated-${suffix}${ext}`;
|
||||
targetPath = path.join(tmpDir, candidateName);
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
await fs.promises.rename(sourcePath, targetPath);
|
||||
};
|
||||
|
||||
const walkAndMoveFiles = async (currentDir) => {
|
||||
const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(currentDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walkAndMoveFiles(absolutePath);
|
||||
continue;
|
||||
}
|
||||
await moveFileToTmpRoot(absolutePath);
|
||||
}
|
||||
};
|
||||
|
||||
for (const subdirName of legacySubdirs) {
|
||||
const legacyDir = path.join(tmpDir, subdirName);
|
||||
if (!pathExists(legacyDir)) continue;
|
||||
await walkAndMoveFiles(legacyDir);
|
||||
await fs.promises.rm(legacyDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateTemporaryOutputFiles(outputDir, tmpDir) {
|
||||
if (!pathExists(outputDir)) return;
|
||||
const entries = await fs.promises.readdir(outputDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!looksLikeTemporaryFileName(entry.name)) continue;
|
||||
const sourcePath = path.join(outputDir, entry.name);
|
||||
let candidateName = entry.name;
|
||||
let targetPath = path.join(tmpDir, candidateName);
|
||||
let suffix = 1;
|
||||
const ext = path.extname(entry.name);
|
||||
const base = path.basename(entry.name, ext);
|
||||
while (pathExists(targetPath)) {
|
||||
candidateName = `${base}-migrated-${suffix}${ext}`;
|
||||
targetPath = path.join(tmpDir, candidateName);
|
||||
suffix += 1;
|
||||
}
|
||||
await fs.promises.rename(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureRuntimeTree(runtimeHome) {
|
||||
const resolvedRuntimeHome = resolveRuntimeHome(runtimeHome);
|
||||
const workspaceSkillHome = resolveWorkspaceSkillHome(resolvedRuntimeHome);
|
||||
const outputDir = resolveOutputDir(resolvedRuntimeHome);
|
||||
const testRunsDir = resolveTestRunsDir(resolvedRuntimeHome);
|
||||
const tmpDir = resolveTmpDir(resolvedRuntimeHome);
|
||||
const tmpTestInputsDir = resolveTmpTestInputsDir(resolvedRuntimeHome);
|
||||
const tmpRenderedSqlDir = resolveTmpRenderedSqlDir(resolvedRuntimeHome);
|
||||
const tmpScratchDir = resolveTmpScratchDir(resolvedRuntimeHome);
|
||||
const profileDir = resolveProfileDir({ runtimeHome: resolvedRuntimeHome });
|
||||
|
||||
await ensureDir(workspaceSkillHome);
|
||||
await ensureDir(resolvedRuntimeHome);
|
||||
await ensureDir(outputDir);
|
||||
await ensureDir(testRunsDir);
|
||||
await ensureDir(tmpDir);
|
||||
await migrateLegacyTmpSubdirs(tmpDir);
|
||||
await migrateTemporaryOutputFiles(outputDir, tmpDir);
|
||||
await ensureDir(tmpTestInputsDir);
|
||||
await ensureDir(tmpRenderedSqlDir);
|
||||
await ensureDir(tmpScratchDir);
|
||||
await ensureDir(profileDir);
|
||||
|
||||
return {
|
||||
workspaceSkillHome,
|
||||
runtimeHome: resolvedRuntimeHome,
|
||||
outputDir,
|
||||
testRunsDir,
|
||||
tmpDir,
|
||||
tmpTestInputsDir,
|
||||
tmpRenderedSqlDir,
|
||||
tmpScratchDir,
|
||||
profileDir,
|
||||
};
|
||||
}
|
||||
1201
scripts/refresh-auth-direct.mjs
Normal file
1201
scripts/refresh-auth-direct.mjs
Normal file
File diff suppressed because it is too large
Load Diff
295
scripts/refresh-auth-state.mjs
Normal file
295
scripts/refresh-auth-state.mjs
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import {
|
||||
DISPLAY_CHROME_PATH,
|
||||
DISPLAY_META_PATH,
|
||||
DISPLAY_RUNTIME_HOME,
|
||||
DISPLAY_PROFILE_DIR,
|
||||
DISPLAY_STATE_PATH,
|
||||
ensureDir,
|
||||
ensureParentDir,
|
||||
ensureRuntimeTree,
|
||||
resolveChromePath,
|
||||
resolveMetaWritePath,
|
||||
resolveProfileDir,
|
||||
resolveRuntimeHome,
|
||||
resolveStateWritePath,
|
||||
} from "./lib/runtime-paths.mjs";
|
||||
import { parseArgs } from "./lib/cli-shared.mjs";
|
||||
import { extractClientBin } from "./lib/auth-shared.mjs";
|
||||
|
||||
const DEFAULT_URL =
|
||||
"https://salesintelligence-dv.oracle.com/ui/dv/ui/project.jsp?pageid=visualAnalyzer&reportmode=presentation&reportpath=%2F%40Catalog%2Fshared%2FTeam%20Sharing%20Area%2FLAD%2FLAD%20Sales%20Operations%2FLAD%20Production%2FLAD%20Knowledge%20One%20View";
|
||||
const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const skillHome = path.resolve(scriptsDir, "..");
|
||||
const skillsHome = path.resolve(skillHome, "..");
|
||||
|
||||
function usage() {
|
||||
return `Refresh Oracle Sales Intelligence auth by opening Chrome.
|
||||
|
||||
This is an optional development and troubleshooting helper. Daily use should prefer scripts/refresh-auth-direct.mjs.
|
||||
|
||||
Usage:
|
||||
node scripts/refresh-auth-state.mjs [options]
|
||||
|
||||
Options:
|
||||
--runtime-home <dir> Override workspace runtime root (default: ${DISPLAY_RUNTIME_HOME})
|
||||
--state-path <file> Override auth-state.json output path (default: ${DISPLAY_STATE_PATH})
|
||||
--meta-path <file> Override session-meta.json output path (default: ${DISPLAY_META_PATH})
|
||||
--profile-dir <dir> Override Chrome profile directory (default: ${DISPLAY_PROFILE_DIR})
|
||||
--chrome-path <file> Override Chrome executable path (default: ${DISPLAY_CHROME_PATH})
|
||||
--url <url> Target Oracle DV workbook URL
|
||||
--timeout-ms <ms> Wait time for authenticated Oracle DV page (default: 600000)
|
||||
--help Show this help
|
||||
`;
|
||||
}
|
||||
|
||||
async function pathExists(targetPath) {
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function uniquePaths(values) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const value of values.filter(Boolean)) {
|
||||
const resolved = path.resolve(value);
|
||||
if (seen.has(resolved)) continue;
|
||||
seen.add(resolved);
|
||||
result.push(resolved);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function bundledRuntimePackageCandidates(packageName) {
|
||||
const execDir = path.dirname(process.execPath || "");
|
||||
const envNodeModules = [
|
||||
process.env.CODEX_NODE_MODULES,
|
||||
process.env.CODEX_BUNDLED_NODE_MODULES,
|
||||
process.env.NODE_PATH,
|
||||
].filter(Boolean);
|
||||
|
||||
const packageJsonCandidates = [];
|
||||
for (const nodeModulesPath of envNodeModules) {
|
||||
for (const basePath of String(nodeModulesPath).split(path.delimiter)) {
|
||||
if (!basePath) continue;
|
||||
packageJsonCandidates.push(path.join(basePath, packageName, "package.json"));
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.CODEX_PRIMARY_RUNTIME_HOME) {
|
||||
packageJsonCandidates.push(
|
||||
path.join(
|
||||
process.env.CODEX_PRIMARY_RUNTIME_HOME,
|
||||
"dependencies",
|
||||
"node",
|
||||
"node_modules",
|
||||
packageName,
|
||||
"package.json",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
packageJsonCandidates.push(
|
||||
path.join(execDir, "node_modules", packageName, "package.json"),
|
||||
path.join(execDir, "..", "node_modules", packageName, "package.json"),
|
||||
path.join(execDir, "..", "..", "node_modules", packageName, "package.json"),
|
||||
path.join(
|
||||
os.homedir(),
|
||||
".cache",
|
||||
"codex-runtimes",
|
||||
"codex-primary-runtime",
|
||||
"dependencies",
|
||||
"node",
|
||||
"node_modules",
|
||||
packageName,
|
||||
"package.json",
|
||||
),
|
||||
);
|
||||
|
||||
return uniquePaths(packageJsonCandidates);
|
||||
}
|
||||
|
||||
async function loadPlaywright() {
|
||||
const packageNames = ["playwright", "playwright-core"];
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
try {
|
||||
return {
|
||||
runtime: await import(packageName),
|
||||
sourceLabel: `local-module:${packageName}`,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const sharedSkillPackageJson = path.join(skillsHome, "playwright", "package.json");
|
||||
if (await pathExists(sharedSkillPackageJson)) {
|
||||
const requireFromSharedSkill = createRequire(sharedSkillPackageJson);
|
||||
for (const packageName of packageNames) {
|
||||
try {
|
||||
return {
|
||||
runtime: requireFromSharedSkill(packageName),
|
||||
sourceLabel: `shared-skill:${packageName}`,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
for (const packageJsonPath of bundledRuntimePackageCandidates(packageName)) {
|
||||
if (!(await pathExists(packageJsonPath))) continue;
|
||||
try {
|
||||
const requireFromBundledRuntime = createRequire(packageJsonPath);
|
||||
return {
|
||||
runtime: requireFromBundledRuntime(packageName),
|
||||
sourceLabel: `bundled-runtime:${packageJsonPath}`,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
"Could not load Playwright for the browser auth helper.",
|
||||
"Tried local module resolution, ~/.codex/skills/playwright, and bundled Codex runtime locations.",
|
||||
"Install a shared Playwright runtime under ~/.codex/skills/playwright or run this helper from a Codex environment with bundled node_modules.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureChromeExecutable(chromePath) {
|
||||
try {
|
||||
await fs.access(chromePath);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Could not find a Chrome executable at ${chromePath}. Pass --chrome-path or set ORACLE_SI_CHROME_PATH to a valid Chrome installation.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
const args = parseArgs(argv);
|
||||
if (args.help === "true") {
|
||||
console.log(usage());
|
||||
return;
|
||||
}
|
||||
const runtimeHome = resolveRuntimeHome(args["runtime-home"]);
|
||||
await ensureRuntimeTree(runtimeHome);
|
||||
const statePath = resolveStateWritePath({ explicitPath: args["state-path"], runtimeHome });
|
||||
const metaPath = resolveMetaWritePath({ explicitPath: args["meta-path"], runtimeHome });
|
||||
const profileDir = resolveProfileDir({ explicitPath: args["profile-dir"], runtimeHome });
|
||||
const chromePath = resolveChromePath(args["chrome-path"]);
|
||||
const targetUrl = args.url || process.env.ORACLE_SI_TARGET_URL || DEFAULT_URL;
|
||||
const timeoutMs = Number(args["timeout-ms"] || 10 * 60 * 1000);
|
||||
|
||||
await ensureChromeExecutable(chromePath);
|
||||
const { runtime: playwrightRuntime, sourceLabel } = await loadPlaywright();
|
||||
const { chromium } = playwrightRuntime;
|
||||
|
||||
await ensureParentDir(statePath);
|
||||
await ensureParentDir(metaPath);
|
||||
await ensureDir(profileDir);
|
||||
|
||||
console.log(`Opening Chrome profile at ${profileDir}`);
|
||||
console.log(`Using browser automation runtime: ${sourceLabel}`);
|
||||
console.log("Complete Oracle login in the opened window if prompted.");
|
||||
|
||||
const context = await chromium.launchPersistentContext(profileDir, {
|
||||
executablePath: chromePath,
|
||||
headless: false,
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1600, height: 900 },
|
||||
});
|
||||
|
||||
let headerMeta = null;
|
||||
const requestListener = (request) => {
|
||||
if (headerMeta) return;
|
||||
if (!request.url().includes("/ui/dv/ui/api/")) return;
|
||||
const headers = request.headers();
|
||||
const csrfToken = headers["x-csrf-token"] || null;
|
||||
const clientBin = headers["x-bitech-clientbin"] || null;
|
||||
if (!csrfToken || !clientBin) return;
|
||||
headerMeta = {
|
||||
xCsrfToken: csrfToken,
|
||||
xBitechClientbin: clientBin,
|
||||
referer: headers.referer || targetUrl,
|
||||
targetUrl,
|
||||
capturedAt: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
context.on("request", requestListener);
|
||||
|
||||
try {
|
||||
const page = context.pages()[0] ?? (await context.newPage());
|
||||
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 120000 });
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
await page.waitForTimeout(2000);
|
||||
const url = page.url();
|
||||
const title = await page.title().catch(() => "");
|
||||
console.log(`Waiting for Oracle Analytics session: ${url}`);
|
||||
|
||||
const authenticated =
|
||||
url.startsWith("https://salesintelligence-dv.oracle.com/") &&
|
||||
!url.includes("signon.oracle.com") &&
|
||||
!url.includes("signon-int.oracle.com") &&
|
||||
/Oracle Analytics|LAD Knowledge One View/i.test(title || url);
|
||||
|
||||
if (!authenticated) continue;
|
||||
|
||||
const html = await page.content();
|
||||
const fallbackClientBin = extractClientBin(html);
|
||||
|
||||
if (!headerMeta) {
|
||||
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
|
||||
await page.waitForTimeout(4000);
|
||||
}
|
||||
|
||||
if (!headerMeta?.xBitechClientbin && fallbackClientBin) {
|
||||
headerMeta = {
|
||||
...(headerMeta || {}),
|
||||
xCsrfToken: headerMeta?.xCsrfToken || null,
|
||||
xBitechClientbin: fallbackClientBin,
|
||||
referer: headerMeta?.referer || targetUrl,
|
||||
targetUrl,
|
||||
capturedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!headerMeta?.xCsrfToken || !headerMeta?.xBitechClientbin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const state = await context.storageState();
|
||||
await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||
await fs.writeFile(metaPath, `${JSON.stringify(headerMeta, null, 2)}\n`, "utf8");
|
||||
|
||||
console.log(`Saved auth state to ${statePath}`);
|
||||
console.log(`Saved session metadata to ${metaPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for an authenticated Oracle DV page after ${timeoutMs}ms.`);
|
||||
} finally {
|
||||
context.off("request", requestListener);
|
||||
await context.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
const entryUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
|
||||
if (import.meta.url === entryUrl) {
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
92
scripts/run-sales-intelligence-sql.mjs
Normal file
92
scripts/run-sales-intelligence-sql.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const fetchScriptPath = path.join(scriptDir, "fetch-sales-intelligence.mjs");
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage:
|
||||
node "${path.resolve(process.argv[1] || "run-sales-intelligence-sql.mjs")}" --sql-file <file> [fetch args...]
|
||||
node "${path.resolve(process.argv[1] || "run-sales-intelligence-sql.mjs")}" --stdin [fetch args...]
|
||||
|
||||
Examples:
|
||||
node "${path.resolve(process.argv[1] || "run-sales-intelligence-sql.mjs")}" --sql-file query.sql --limit 200
|
||||
Get-Content query.sql | node "${path.resolve(process.argv[1] || "run-sales-intelligence-sql.mjs")}" --stdin --render-sql
|
||||
|
||||
Notes:
|
||||
- This helper preserves complex logical SQL that is awkward to pass through PowerShell quoting.
|
||||
- All flags other than --sql-file / --stdin / --help are passed through to fetch-sales-intelligence.mjs.
|
||||
`);
|
||||
}
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let sqlFilePath = null;
|
||||
let useStdin = false;
|
||||
const passthroughArgs = [];
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--help") {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
if (arg === "--sql-file") {
|
||||
sqlFilePath = args[index + 1];
|
||||
index += 1;
|
||||
if (!sqlFilePath) {
|
||||
console.error("Missing value for --sql-file");
|
||||
process.exit(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--stdin") {
|
||||
useStdin = true;
|
||||
continue;
|
||||
}
|
||||
passthroughArgs.push(arg);
|
||||
}
|
||||
|
||||
if (sqlFilePath && useStdin) {
|
||||
console.error("Use either --sql-file or --stdin, not both.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!sqlFilePath && !useStdin) {
|
||||
console.error("One of --sql-file or --stdin is required.");
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sql = useStdin ? await readStdin() : await fs.readFile(path.resolve(sqlFilePath), "utf8");
|
||||
if (!sql.trim()) {
|
||||
console.error("SQL input is empty.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const stdout = execFileSync(process.execPath, [fetchScriptPath, "--sql", sql, ...passthroughArgs], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
process.stdout.write(stdout);
|
||||
} catch (error) {
|
||||
if (typeof error.stdout === "string" && error.stdout.length > 0) {
|
||||
process.stdout.write(error.stdout);
|
||||
}
|
||||
if (typeof error.stderr === "string" && error.stderr.length > 0) {
|
||||
process.stderr.write(error.stderr);
|
||||
}
|
||||
process.exit(typeof error.status === "number" ? error.status : 1);
|
||||
}
|
||||
Reference in New Issue
Block a user