Add browser profile option and TSV output mode

- add --browser-profile for login and validate browser/profile combinations\n- validate browser options eagerly and keep default-browser behavior when omitted\n- add TSV output format (no header)\n- change header default to auto; add --header original/-H o\n- remove explicit json/j output mode usage and keep JSON as implicit default\n- add tini to Dockerfile entrypoint path to improve signal handling
This commit is contained in:
2026-02-08 21:35:40 +01:00
parent 2180d5aa4c
commit 064ee1db21
5 changed files with 145 additions and 26 deletions

View File

@@ -17,6 +17,7 @@ const RESOURCE_SCOPE_BY_NAME = {
const DEFAULT_RESOURCES = ["graph", "devops", "arm"];
const LOGIN_REQUIRED_MESSAGE = "Login required. Run: sk-az-tools login";
const BROWSER_KEYWORDS = Object.keys(apps).sort();
const CHROMIUM_BROWSERS = new Set(["edge", "chrome", "brave"]);
function getCacheRoot() {
const isWindows = process.platform === "win32";
@@ -80,21 +81,70 @@ function getBrowserAppName(browser) {
return null;
}
const requested = browser.trim().toLowerCase();
if (requested === "default") {
return null;
}
const keyword = BROWSER_KEYWORDS.find((name) => name.toLowerCase() === requested);
const keyword = BROWSER_KEYWORDS.find(
(name) => name.toLowerCase() === browser.trim().toLowerCase(),
);
if (!keyword) {
throw new Error(
`Invalid browser '${browser}'. Allowed: default, ${BROWSER_KEYWORDS.join(", ")}`,
`Invalid browser '${browser}'. Allowed: ${BROWSER_KEYWORDS.join(", ")}`,
);
}
return apps[keyword];
}
function getBrowserKeyword(browser) {
if (!browser || browser.trim() === "") {
return "";
}
const requested = browser.trim().toLowerCase();
const keyword = BROWSER_KEYWORDS.find((name) => name.toLowerCase() === requested);
if (!keyword) {
throw new Error(
`Invalid browser '${browser}'. Allowed: ${BROWSER_KEYWORDS.join(", ")}`,
);
}
return keyword.toLowerCase();
}
function getBrowserOpenOptions({ browser, browserProfile }) {
const browserName = getBrowserAppName(browser);
const options = browserName
? { wait: false, app: { name: browserName } }
: { wait: false };
if (!browserProfile || browserProfile.trim() === "") {
return options;
}
const browserKeyword = getBrowserKeyword(browser);
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
throw new Error(
"--browser-profile is supported only with --browser edge|chrome|brave",
);
}
options.app.arguments = [`--profile-directory=${browserProfile.trim()}`];
return options;
}
function validateBrowserOptions({ browser, browserProfile }) {
if (browser && browser.trim() !== "") {
getBrowserAppName(browser);
}
if (browserProfile && browserProfile.trim() !== "") {
const browserKeyword = getBrowserKeyword(browser);
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
throw new Error(
"--browser-profile is supported only with --browser edge|chrome|brave",
);
}
}
}
export function parseResources(resourcesCsv) {
if (!resourcesCsv || resourcesCsv.trim() === "") {
return [...DEFAULT_RESOURCES];
@@ -217,11 +267,13 @@ export async function loginInteractive({
scopes,
showAuthUrlOnly = false,
browser,
browserProfile,
}) {
if (!tenantId) throw new Error("tenantId is required");
if (!clientId) throw new Error("clientId is required");
if (!Array.isArray(scopes) || scopes.length === 0)
throw new Error("scopes[] is required");
validateBrowserOptions({ browser, browserProfile });
const pca = await createPca({ tenantId, clientId });
@@ -235,10 +287,7 @@ export async function loginInteractive({
writeStderr(`Visit:\n${url}`);
return;
}
const browserName = getBrowserAppName(browser);
const options = browserName
? { wait: false, app: { name: browserName } }
: { wait: false };
const options = getBrowserOpenOptions({ browser, browserProfile });
return open(url, options).catch(() => {
writeStderr(`Visit:\n${url}`);
});
@@ -272,9 +321,11 @@ export async function login({
useDeviceCode = false,
noBrowser = false,
browser,
browserProfile,
}) {
if (!tenantId) throw new Error("tenantId is required");
if (!clientId) throw new Error("clientId is required");
validateBrowserOptions({ browser, browserProfile });
const resources = parseResources(resourcesCsv);
const scopes = getScopesForResources(resources);
@@ -312,10 +363,7 @@ export async function login({
writeStderr(`Visit:\n${url}`);
return;
}
const browserName = getBrowserAppName(browser);
const options = browserName
? { wait: false, app: { name: browserName } }
: { wait: false };
const options = getBrowserOpenOptions({ browser, browserProfile });
return open(url, options).catch(() => {
writeStderr(`Visit:\n${url}`);
});