From 868fe8e3491e522ed51ede58d510037fed2bf541 Mon Sep 17 00:00:00 2001 From: "Gregor \"hrax\" Magdolen" Date: Tue, 20 Aug 2024 04:52:55 +0200 Subject: [PATCH] Switch to module 'NodeNext' compilation, fix tests, change in ProfileManager --- eslint.config.mjs => eslint.config.js | 0 jest.config.ts | 1 + package-lock.json | 125 +++++++++++++ package.json | 3 +- spec/core/ProfileManager.spec.ts | 34 ++-- src/cli/helpers.ts | 29 --- src/cli/helpers/CommandLogger.ts | 75 ++++++++ src/cli/now-eslint.ts | 34 ++-- .../subcommands/now-eslint-profile-create.ts | 141 +++++++++++++- .../subcommands/now-eslint-profile-purge.ts | 8 +- .../subcommands/now-eslint-profile-view.ts | 6 +- src/cli/subcommands/now-eslint-profile.ts | 2 +- src/cli/subcommands/now-eslint-report.ts | 40 +++- src/core/OAuthClient.ts | 10 +- src/core/Package.ts | 9 +- src/core/ProfileManager.ts | 172 ++++++++++-------- src/core/sn.ts | 2 +- test.mjs | 3 + tsconfig.json | 4 +- 19 files changed, 530 insertions(+), 168 deletions(-) rename eslint.config.mjs => eslint.config.js (100%) create mode 100644 src/cli/helpers/CommandLogger.ts create mode 100644 test.mjs diff --git a/eslint.config.mjs b/eslint.config.js similarity index 100% rename from eslint.config.mjs rename to eslint.config.js diff --git a/jest.config.ts b/jest.config.ts index d0dcc7a..827eb09 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ /** * For a detailed explanation regarding each configuration property, visit: * https://jestjs.io/docs/configuration diff --git a/package-lock.json b/package-lock.json index fd949cb..1890113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "docx": "^8.5.0", "dotenv": "^16.4.5", "https-proxy-agent": "^7.0.5", + "open": "^10.1.0", "pdfmake": "^0.2.10", "prompts": "^2.4.2", "xpath": "^0.0.34" @@ -2514,6 +2515,20 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2950,6 +2965,32 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2966,6 +3007,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -4096,6 +4148,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4135,6 +4201,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4180,6 +4263,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -6359,6 +6456,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6926,6 +7040,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index b157652..0e2ec8b 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "docx": "^8.5.0", "dotenv": "^16.4.5", "https-proxy-agent": "^7.0.5", + "open": "^10.1.0", "pdfmake": "^0.2.10", "prompts": "^2.4.2", "xpath": "^0.0.34" @@ -84,7 +85,7 @@ "peerDependencies": { "eslint": ">=9.3.0" }, - "type": "commonjs", + "type": "module", "engines": { "node": ">=22" } diff --git a/spec/core/ProfileManager.spec.ts b/spec/core/ProfileManager.spec.ts index a30c1f7..21dc4b2 100644 --- a/spec/core/ProfileManager.spec.ts +++ b/spec/core/ProfileManager.spec.ts @@ -1,6 +1,6 @@ import fs, { Stats } from "fs"; import path from "path"; -import * as ProfileManager from "../../src/core/ProfileManager"; +import profileManager from "../../src/core/ProfileManager"; import { InstanceConfig, Profile, ProfileInfo } from "../../src/core/ProfileManager"; import { RESTClient } from "../../src/core/RESTClient"; import { OAuthClient } from "../../src/core/OAuthClient"; @@ -55,8 +55,8 @@ describe("ProfileManagerSpec", () => { const profilesHomePath = "#profilesHome"; jest.spyOn(path, "normalize").mockImplementation((path) => path); - jest.spyOn(ProfileManager, "homeDirPath").mockReturnValue(profilesHomePath); - jest.spyOn(ProfileManager, "profileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); + jest.spyOn(profileManager, "getHomePath").mockReturnValue(profilesHomePath); + jest.spyOn(profileManager, "getProfileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); when(jest.spyOn(fs, "readdirSync")) .defaultImplementation(jesthelpers.defaultWhenImplementationThrow) @@ -73,17 +73,17 @@ describe("ProfileManagerSpec", () => { .calledWith(`${profilesHomePath}/dev1/profile.json`, "utf8").mockReturnValue(JSON.stringify(expected[0])) .calledWith(`${profilesHomePath}/dev2/profile.json`, "utf8").mockReturnValue(JSON.stringify(expected[1])); - const profiles: ProfileInfo[] = ProfileManager.listProfiles(); + const profiles: ProfileInfo[] = profileManager.listProfiles(); expect(profiles).toStrictEqual(expected); }); it("should purge all existing profiles", () => { const profilesHomePath = "#profilesHome"; - const profilesHomeDirPathSpy = jest.spyOn(ProfileManager, "homeDirPath").mockReturnValue(profilesHomePath); + const profilesHomeDirPathSpy = jest.spyOn(profileManager, "getHomePath").mockReturnValue(profilesHomePath); const rmSpy = jest.spyOn(fs, "rmdirSync").mockReturnValue(undefined); - ProfileManager.purgeProfiles(); + profileManager.purgeProfiles(); expect(profilesHomeDirPathSpy).toHaveBeenCalled(); expect(rmSpy).toHaveBeenCalledWith(profilesHomePath, {recursive: true}); @@ -94,8 +94,8 @@ describe("ProfileManagerSpec", () => { const profileName = "dev1"; jest.spyOn(path, "normalize").mockImplementation((path) => path); - jest.spyOn(ProfileManager, "homeDirPath").mockReturnValue(profilesHomePath); - jest.spyOn(ProfileManager, "profileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); + jest.spyOn(profileManager, "getHomePath").mockReturnValue(profilesHomePath); + jest.spyOn(profileManager, "getProfileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); jest.spyOn(fs, "existsSync").mockReturnValue(true); when(jest.spyOn(fs, "readFileSync")) @@ -110,7 +110,7 @@ describe("ProfileManagerSpec", () => { tables: {} }); - await ProfileManager.loadProfile(profileName, restClient); + await profileManager.loadProfile(profileName, restClient); expect(restClientSpy).toHaveBeenCalledTimes(1); }); @@ -131,13 +131,13 @@ describe("ProfileManagerSpec", () => { const profilesHomePath = "#profilesHome"; jest.spyOn(path, "normalize").mockImplementation((path) => path); - jest.spyOn(ProfileManager, "homeDirPath").mockReturnValue(profilesHomePath); - jest.spyOn(ProfileManager, "profileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); + jest.spyOn(profileManager, "getHomePath").mockReturnValue(profilesHomePath); + jest.spyOn(profileManager, "getProfileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); jest.spyOn(fs, "existsSync").mockReturnValue(true); const writeSpy = jest.spyOn(fs, "writeFileSync").mockReturnValue(undefined); - ProfileManager.saveProfile(profile); + profileManager.saveProfile(profile); expect(writeSpy).toHaveBeenCalledWith(expect.anything(), JSON.stringify(profileData, null, 2), expect.anything()); }); @@ -158,13 +158,13 @@ describe("ProfileManagerSpec", () => { const profilesHomePath = "#profilesHome"; jest.spyOn(path, "normalize").mockImplementation((path) => path); - jest.spyOn(ProfileManager, "homeDirPath").mockReturnValue(profilesHomePath); - jest.spyOn(ProfileManager, "profileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); + jest.spyOn(profileManager, "getHomePath").mockReturnValue(profilesHomePath); + jest.spyOn(profileManager, "getProfileFilePath").mockImplementation((name, file) => `${profilesHomePath}/${name}/${file}`); jest.spyOn(fs, "existsSync").mockReturnValue(true); const writeSpy = jest.spyOn(fs, "writeFileSync").mockImplementation(undefined); - ProfileManager.updateProfileConfig(profile); + profileManager.updateProfileConfig(profile); expect(writeSpy).toHaveBeenCalledWith(expect.anything(), JSON.stringify(profileData, null, 2), expect.anything()); }); @@ -185,11 +185,11 @@ describe("ProfileManagerSpec", () => { const profilesHomePath = "#profilesHome"; jest.spyOn(path, "normalize").mockImplementation((path) => path); - jest.spyOn(ProfileManager, "homeDirPath").mockReturnValue(profilesHomePath); + jest.spyOn(profileManager, "getHomePath").mockReturnValue(profilesHomePath); const rmSpy = jest.spyOn(fs, "rmdirSync").mockReturnValue(undefined); - ProfileManager.purgeProfile(profile); + profileManager.purgeProfile(profile); expect(rmSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts index 4771a39..6df700a 100644 --- a/src/cli/helpers.ts +++ b/src/cli/helpers.ts @@ -1,6 +1,5 @@ /* eslint-disable no-console */ import { InvalidArgumentError, Option } from "commander"; -import { red, green } from "colors/safe"; import * as ProfileManager from "../core/ProfileManager.js"; export const DOMAIN_REGEXP = /^https?:\/\/.*?\/?$/; @@ -28,21 +27,6 @@ export function validateFileName(value: string) { throw new InvalidArgumentError("'file-name' can only contain lowecase/uppercase letters, numbers, underscore and dash."); } -export function debugOption(): Option { - return new Option("--debug") - .default(false); -}; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isDebug(options: any): boolean { - return options.debug === true; -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function debug(options: any): void { - if (isDebug(options)) { - outputKeyValue("Working profiles home directory", `${ProfileManager.homeDirPath()}`, true); - } -}; - export function forceOption(): Option { return new Option("--force"); } @@ -51,19 +35,6 @@ export function isForce(options: any): boolean { return options.force === true; } -export function outputError(str: string, write = console.error) { - write(`${red(str)}`); -} -export function outputInfo(str: string, write = console.info) { - write(`${str}`); -} -export function outputDebug(str: string, write = console.debug) { - write(`${str}`); -} -export function outputKeyValue(key: string, value: string, newLine = false, write = console.debug) { - write(`${key}: ${green(value)}${newLine ? "\n" : ""}`); -} - export function boolYesNo(bool: boolean) { return bool === true ? "Yes" : "No"; } \ No newline at end of file diff --git a/src/cli/helpers/CommandLogger.ts b/src/cli/helpers/CommandLogger.ts new file mode 100644 index 0000000..bc729cf --- /dev/null +++ b/src/cli/helpers/CommandLogger.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-console */ +import { Option } from "commander"; +import colors from "colors/safe.js"; + +export class CommandLogger { + private _debug = false; + private _verbose = false; + private out = console; + + setDebug(debug?: boolean): CommandLogger { + this._debug = debug ?? false; + return this; + } + isDebug(): boolean { + return this._debug; + } + setVerbose(verbose?: boolean): CommandLogger { + if (this.isDebug() && verbose === true) { + this.debug("Debug is on, verbose will be ignored"); + return this; + }; + this._verbose = verbose ?? false; + return this; + } + isVerbose(): boolean { + return this._verbose; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fromOptions(options: any): void { + this.setDebug(options.debug) + .setVerbose(options.verbose); + } + debug(message: string): void { + if (this.isDebug()) { + CommandLogger.outputDebug(message); + } + } + verbose(message: string): void { + if (this.isVerbose() || this.isDebug()) { + CommandLogger.outputInfo(message); + } + } + info(message: string): void { + CommandLogger.outputInfo(message); + } + warning(message: string): void { + CommandLogger.outputWarning(message); + } + error(message: string): void { + CommandLogger.outputError(message); + } + + static debugOption(description?: string): Option { + return new Option("--debug", description ?? "Print debug details of the command execution"); + } + static verboseOption(description?: string): Option { + return new Option("--verbose", description ?? "Print details about command execution"); + } + + static outputWarning(str: string, write = console.error): void { + write(`${colors.yellow(str)}`); + } + static outputError(str: string, write = console.error): void { + write(`${colors.red(str)}`); + } + static outputInfo(str: string, write = console.info): void { + write(`${str}`); + } + static outputDebug(str: string, write = console.debug): void { + write(`[DEBUG] ${str}`); + } + static outputKeyValue(key: string, value: string, newLine = false): void { + CommandLogger.outputInfo(`${key}: ${colors.green(value)}${newLine ? "\n" : ""}`); + } +} \ No newline at end of file diff --git a/src/cli/now-eslint.ts b/src/cli/now-eslint.ts index 5dbd56d..95e1eb3 100644 --- a/src/cli/now-eslint.ts +++ b/src/cli/now-eslint.ts @@ -1,28 +1,28 @@ #!/usr/bin/env node import dotenv from "dotenv"; import { Command } from "commander"; -import { outputError } from "./helpers.js"; import { PACKAGE_VERSION } from "../core/Package.js"; +import { CommandLogger } from "./helpers/CommandLogger.js"; import { profileCommand } from "./subcommands/now-eslint-profile.js"; import { reportCommand } from "./subcommands/now-eslint-report.js"; // Initialize dotenv try { dotenv.config(); -// eslint-disable-next-line no-empty, @typescript-eslint/no-unused-vars -} catch (err) {} - -try { - const program = new Command() - .name("now-eslint") - .description("CLI to ESLint Service Now update sets") - .version(PACKAGE_VERSION, "-v, --version", "current version") - .showHelpAfterError() - .configureOutput({outputError: outputError}) - .addCommand(reportCommand, {isDefault: true}) - .addCommand(profileCommand); - - program.parseAsync(process.argv); } catch (err) { - outputError(err as string); -} \ No newline at end of file + CommandLogger.outputError(`[ERROR] Unknown error occured\n${(err as Error).stack || err}`); +} + +const program = new Command() + .name("now-eslint") + .description("CLI to ESLint Service Now update sets") + .version(PACKAGE_VERSION, "-v, --version", "current version") + .showHelpAfterError() + .configureOutput({ + outputError: CommandLogger.outputWarning + }) + .addCommand(reportCommand, {isDefault: true}) + .addCommand(profileCommand); + +program.parseAsync(process.argv) + .catch((err) => CommandLogger.outputError(`[ERROR] Unknown error occured\n${err.stack || err}`)); \ No newline at end of file diff --git a/src/cli/subcommands/now-eslint-profile-create.ts b/src/cli/subcommands/now-eslint-profile-create.ts index f8449e6..b1a0e94 100644 --- a/src/cli/subcommands/now-eslint-profile-create.ts +++ b/src/cli/subcommands/now-eslint-profile-create.ts @@ -1,15 +1,150 @@ -import commander from "commander"; +import * as commander from "commander"; import * as helpers from "../helpers.js"; +import { CommandLogger } from "../helpers/CommandLogger.js"; +import prompts, { PromptObject, Options } from "prompts"; +import profileManager from "../../core/ProfileManager.js"; +import { RESTClient } from "../../core/RESTClient.js"; +import { OAuthClient } from "../../core/OAuthClient.js"; +import open, { apps } from "open"; + + +const log = new CommandLogger(); const createCommand = new commander.Command("create") + .configureOutput({ + outputError: CommandLogger.outputWarning + }) .description("create new profile for the ServiceNow instance") .argument("", `name of the profile; ${helpers.PROFILE_HELP}`, helpers.validateProfileName) .option("--proxy", "set up proxy connection configuration") .addOption(helpers.forceOption()) - .addOption(helpers.debugOption()); + .addOption(CommandLogger.verboseOption()) + .addOption(CommandLogger.debugOption()); createCommand.action(async function(name, options) { - // TODO:! + log.fromOptions(options); + + log.debug(`Current working directory: ${process.cwd()}`); + log.debug(`Profile home directory: ${profileManager.getHomePath()}`); + log.verbose(`Setting up profile with name '${name}'\n`); + + const oauth = new OAuthClient(); + + const questions: PromptObject[] = [ + { + type: () => options.proxy && "text", + name: "proxy", + message: "Enter proxy url" + }, + { + type: "text", + name: "baseurl", + message: "Enter Service Now instance url", + validate: (value) => { + try { + const url = new URL("/", value); + if (url.protocol !== "http:" && url.protocol !== "https:") { + return "URL can start only with http(s)://"; + } + return true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (err) { + return "URL is not valid."; + } + }, + format: (value) => { + return new URL("/", value).origin; + } + }, + { + type: "select", + name: "oauthtype", + message: "How would you like to authenticate with Service Now instance?", + choices: [ + {title: "OAuth with Code", value: "oauth-token"}, + {title: "OAuth with Username/Password", value: "oauth-password"} + ], + initial: 0 + }, + { + type: "confirm", + name: "createclient", + message: "Would you like to create OAuth Client?" + }, + { + type: "text", + name: "clienid", + message: "Please enter OAuth client ID" + }, + { + type: "text", + name: "clientsecret", + message: "Please enter OAuth client secret" + }, + { + type: (prev, values) => values.oauthtype === "oauth-password" && "text", + name: "username", + message: "Please enter username to authenticate" + }, + { + type: (prev, values) => values.oauthtype === "oauth-password" && "password", + name: "password", + message: "Please enter password to authenticate" + } + ]; + + let cancelled = false; + const response = await prompts(questions, { + onCancel() { + cancelled = true; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async onSubmit(prompt, answer, answers: any) { + if (prompt.name === "createclient" && answer === true) { + const newClient = oauth.getNewClientURL(answers.baseurl); + log.verbose("Trying to open browser to create OAuth client"); + log.verbose(newClient.toString()); + await open(newClient.toString(), {app: {name: apps.browser}}); + } + } + }); + + if (cancelled) { + log.warning("Prompt cancelled"); + return; + } + log.info(JSON.stringify(response, undefined, 2)); + + const profile = profileManager.fromData({ + name: name, + baseUrl: response.baseurl, + auth: { + type: response.oauthtype, + clientID: response.clienid, + clientSecret: response.clientsecret, + lastRetrieved: 0 + } + }); + + if (response.oauthtype === "oauth-password") { + log.warning("Using username/password combination to authenticate with Service Now instance."); + log.warning("Remember that provided user has to be logged on the instance and have interactive session."); + log.warning("!!! Username and password are not saved, to change user or authentication type run `profile reauth` !!!"); + + // TODO: handle errors + // await oauth.requestTokenByUsername(profile, response.username, response.password); + } + + // If oauth-token + // Create http server + // Open code url in browser + // Receive code by server + // Kill server + + // If oauth-password + // Request token with username & password + + // const rest = new RESTClient(oauth); }); export { createCommand }; \ No newline at end of file diff --git a/src/cli/subcommands/now-eslint-profile-purge.ts b/src/cli/subcommands/now-eslint-profile-purge.ts index cfa03f7..d806192 100644 --- a/src/cli/subcommands/now-eslint-profile-purge.ts +++ b/src/cli/subcommands/now-eslint-profile-purge.ts @@ -1,13 +1,17 @@ -import commander from "commander"; +import * as commander from "commander"; import * as helpers from "../helpers.js"; +import { CommandLogger } from "../helpers/CommandLogger.js"; const PURGE_CONFIRM = "PURGE"; const purgeCommand = new commander.Command("purge") + .configureOutput({ + outputError: CommandLogger.outputWarning + }) .description("purge single existing ServiceNow instance profile") .argument("", "name of the profile to set up (lowecase/uppercase letters, numbers, underscore and dash)", helpers.validateProfileName) - .addOption(helpers.debugOption()) + .addOption(CommandLogger.debugOption()) .addOption(helpers.forceOption()) .action(async function(name, options) { // TODO: diff --git a/src/cli/subcommands/now-eslint-profile-view.ts b/src/cli/subcommands/now-eslint-profile-view.ts index a2da804..ed59438 100644 --- a/src/cli/subcommands/now-eslint-profile-view.ts +++ b/src/cli/subcommands/now-eslint-profile-view.ts @@ -1,8 +1,12 @@ -import commander from "commander"; +import * as commander from "commander"; import * as helpers from "../helpers.js"; +import { CommandLogger } from "../helpers/CommandLogger.js"; const viewCommand = new commander.Command("view") + .configureOutput({ + outputError: CommandLogger.outputWarning + }) .argument("", "name of the profile to set up (lowecase/uppercase letters, numbers, underscore and dash)", helpers.validateProfileName) .option("-t, --test-connection", "test connection to the instance") .action(async function(name, options) { diff --git a/src/cli/subcommands/now-eslint-profile.ts b/src/cli/subcommands/now-eslint-profile.ts index d42931e..d632a45 100644 --- a/src/cli/subcommands/now-eslint-profile.ts +++ b/src/cli/subcommands/now-eslint-profile.ts @@ -1,4 +1,4 @@ -import commander from "commander"; +import * as commander from "commander"; import { createCommand } from "./now-eslint-profile-create.js"; import { viewCommand } from "./now-eslint-profile-view.js"; import { purgeCommand } from "./now-eslint-profile-purge.js"; diff --git a/src/cli/subcommands/now-eslint-report.ts b/src/cli/subcommands/now-eslint-report.ts index 7081a9d..466d6bc 100644 --- a/src/cli/subcommands/now-eslint-report.ts +++ b/src/cli/subcommands/now-eslint-report.ts @@ -1,22 +1,48 @@ -import commander from "commander"; +import * as commander from "commander"; import * as helpers from "../helpers.js"; +import prompts, { PromptObject } from "prompts"; +import { CommandLogger } from "../helpers/CommandLogger.js"; + const reportCommand = new commander.Command("report") + .configureOutput({ + outputError: CommandLogger.outputWarning + }) .description("Command to generate update set eslint report from Service Now instance") - .configureOutput({outputError: helpers.outputError}) .argument("", `name of the profile; ${helpers.PROFILE_HELP}`, helpers.validateProfileName) - .option("-t, --title ", "title of the report (in quotes if multiword)") - .option("-f, --file-name ", "file name of the report without an extension", helpers.validateFileName) - .option("-q, --query ", "update set query to perform report on (in quotes if multiword)") + // .option("-t, --title ", "title of the report (in quotes if multiword)") + // .option("-n, --file-name ", "file name of the report without an extension", helpers.validateFileName) + // .option("-q, --query ", "update set query to perform report on (in quotes if multiword)") /* * .option("--json", "generate report as JSON rather than PDF report") * .option("--with-json", "generate JSON for the PDF report; ignored if option --json is used") * .option("--from-json ", "generate PDF report from provided JSON file; ignores all options") */ - .addOption(helpers.debugOption()) + .addOption(CommandLogger.verboseOption()) + .addOption(CommandLogger.debugOption()) .showHelpAfterError() - .action(async(name, options) => { + .action(async(profile, options) => { // TODO: + const questions: PromptObject[] = [ + { + type: "text", + name: "title", + message: "Title of the report" + }, + { + type: "text", + name: "filename", + message: "File name of the report" + }, + { + type: "text", + name: "query", + message: "Update set query" + } + ]; + + const response = await prompts(questions); + CommandLogger.outputInfo(JSON.stringify(response, undefined, 2)); }); export {reportCommand}; \ No newline at end of file diff --git a/src/core/OAuthClient.ts b/src/core/OAuthClient.ts index eb457a5..ebe378e 100644 --- a/src/core/OAuthClient.ts +++ b/src/core/OAuthClient.ts @@ -4,7 +4,7 @@ import { Request, Response } from "./Request.js"; import { RequestOptions } from "https"; import { PACKAGE_NAME, PACKAGE_VERSION } from "./Package.js"; import { SNOAuthTokenData } from "./sn.js"; -import * as ProfileManager from "./ProfileManager.js"; +import profileManager from "./ProfileManager.js"; import { InstanceOAuthTokenData, Profile } from "./ProfileManager.js"; const CLIENT_BASE_URL = "/oauth_entity.do" as const; @@ -42,7 +42,7 @@ export class OAuthClient { return hadTokenFor > expiresIn; } - getNewClientURL(profile: Profile): URL { + getNewClientURL(baseUrl: string): URL { const query = [ "type=client", `name=${PACKAGE_NAME}`, @@ -52,9 +52,9 @@ export class OAuthClient { "logo_url=" ]; - const url = new URL(CLIENT_BASE_URL, profile.getBaseUrl()); + const url = new URL(CLIENT_BASE_URL, baseUrl); url.searchParams.set("sys_id", "-1"); - url.searchParams.set("sysparm_transaction_scope", "global"); + // FIXME: url.searchParams.set("sysparm_transaction_scope", "global"); url.searchParams.set("sysparm_query", query.join("^")); return url; } @@ -190,7 +190,7 @@ export class OAuthClient { if (OAuthClient.isTokenExpired(profile.getInstanceOAuthTokenData())) { await this.refreshToken(profile); // Update profile file with new token - ProfileManager.updateProfileConfig(profile); + profileManager.updateProfileConfig(profile); } if (options.headers == null) { options.headers = {}; diff --git a/src/core/Package.ts b/src/core/Package.ts index 52703e6..955adc5 100644 --- a/src/core/Package.ts +++ b/src/core/Package.ts @@ -1,5 +1,6 @@ -import { name, version, description } from "../../package.json"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import pkg = require("../../package.json"); -export const PACKAGE_NAME = name; -export const PACKAGE_VERSION = version; -export const PACKAGE_DESCRIPTION = description; \ No newline at end of file +export const PACKAGE_NAME = pkg.name; +export const PACKAGE_VERSION = pkg.version; +export const PACKAGE_DESCRIPTION = pkg.description; \ No newline at end of file diff --git a/src/core/ProfileManager.ts b/src/core/ProfileManager.ts index bb58f71..65f5524 100644 --- a/src/core/ProfileManager.ts +++ b/src/core/ProfileManager.ts @@ -20,93 +20,107 @@ const createFolderIfNotExists = function(path: string): void { } }; -/* - * Manages everything around profiles - home folder, profile home and additional files - * list, load, save, purgeAll, purge - */ - -export const profileExists = function(name: string) { - return fs.existsSync(profileDirPath(name)); -}; - -export const isProfileNameValid = function(name: string): boolean { - return PROFILE_NAME_REGEXP.test(name); -}; - -export const homeDirPath = function() { +const homeDirPath = function() { return path.normalize(process.env.NOW_ESLINT_PROFILE_HOME || PROFILES_HOME_DIR_PATH); }; -export const profileDirPath = function(name: string): string { - return path.normalize(`${homeDirPath()}/${name}/`); -}; - -export const profileFilePath = function(name: string, file: ProfileFileName): string { - return path.normalize(`${profileDirPath(name)}/${file}`); -}; - -export const listProfiles = function(): ProfileInfo[] { - const home = homeDirPath(); - return fs.readdirSync(home) - .filter((file) => { - return fs.statSync(path.normalize(`${home}/${file}`)).isDirectory() && fs.existsSync(profileFilePath(file, PROFILE_CONFIG_FILE_NAME)); - }) - .map((profile) => { - const configPath = profileFilePath(profile, PROFILE_CONFIG_FILE_NAME); - const content = fs.readFileSync(configPath, "utf8"); - const config: ProfileInfo = JSON.parse(content); - return { - name: profile, - baseUrl: config.baseUrl - }; - }); -}; - -export const purgeProfiles = function(): void { - const home = homeDirPath(); - fs.rmdirSync(home, {recursive: true}); -}; - -export const fromData = function(data: InstanceConfig): Profile { - return new Profile(data); -}; - -export const loadProfile = async function(name: string, client: RESTClient): Promise { - const home = profileDirPath(name); - if (!fs.existsSync(home)) { - return null; - } - const configFilePath = profileFilePath(name, PROFILE_CONFIG_FILE_NAME); - if (!fs.existsSync(configFilePath)) { - return null; +export class ProfileManager { + private homePath: string; + constructor(homePath: string, force = false) { + this.homePath = path.normalize(homePath); + if (force === true) { + createFolderIfNotExists(homePath); + } } - const configFileData = fs.readFileSync(configFilePath, "utf8"); - const profile = fromData(JSON.parse(configFileData)); - profile.setRESTClient(client); - await profile.fetchTableConfiguration(); - return profile; -}; + getHomePath() { + return this.homePath; + }; + + getProfilePath(name: string) { + return path.normalize(`${this.getHomePath()}/${name}/`); + }; + + getProfileFilePath(name: string, file: ProfileFileName): string { + return path.normalize(`${this.getProfilePath(name)}/${file}`); + }; + + profileExists(name: string) { + return fs.existsSync(this.getProfilePath(name)); + }; + + listProfiles(): ProfileInfo[] { + const home = this.getHomePath(); + return fs.readdirSync(home) + .filter((file) => { + return fs.statSync(path.normalize(`${home}/${file}`)).isDirectory() && fs.existsSync(this.getProfileFilePath(file, PROFILE_CONFIG_FILE_NAME)); + }) + .map((profile) => { + const configPath = this.getProfileFilePath(profile, PROFILE_CONFIG_FILE_NAME); + const content = fs.readFileSync(configPath, "utf8"); + const config: ProfileInfo = JSON.parse(content); + return { + name: profile, + baseUrl: config.baseUrl + }; + }); + }; + + purgeProfiles(): void { + fs.rmdirSync(this.getHomePath(), {recursive: true}); + }; + + fromData(data: InstanceConfig): Profile { + return new Profile(data); + }; + + async loadProfile(name: string, client: RESTClient): Promise { + const home = this.getProfilePath(name); + if (!fs.existsSync(home)) { + return null; + } + const configFilePath = this.getProfileFilePath(name, PROFILE_CONFIG_FILE_NAME); + if (!fs.existsSync(configFilePath)) { + return null; + } + const configFileData = fs.readFileSync(configFilePath, "utf8"); + + const profile = this.fromData(JSON.parse(configFileData)); + profile.setRESTClient(client); + await profile.fetchTableConfiguration(); + return profile; + }; + + saveProfile(profile: Profile): void { + const home = this.getProfilePath(profile.getName()); + createFolderIfNotExists(home); + const config = profile.getConfig(); + const configPath = this.getProfileFilePath(profile.getName(), PROFILE_CONFIG_FILE_NAME); + fs.writeFileSync(configPath, JSON.stringify(config, null, JSON_INDENT), "utf8"); + }; + + updateProfileConfig(profile: Profile): void { + // FIXME: for now 100% same as save, redirect + this.saveProfile(profile); + }; + + purgeProfile(profile: Profile): void { + const home = this.getProfilePath(profile.getName()); + fs.rmdirSync(home, {recursive: true}); + }; +} -export const saveProfile = function(profile: Profile): void { - const home = profileDirPath(profile.getName()); - createFolderIfNotExists(home); - const config = profile.getConfig(); - const configPath = profileFilePath(profile.getName(), PROFILE_CONFIG_FILE_NAME); - fs.writeFileSync(configPath, JSON.stringify(config, null, JSON_INDENT), "utf8"); -}; -export const updateProfileConfig = function(profile: Profile): void { - const home = profileDirPath(profile.getName()); - createFolderIfNotExists(home); - const config = profile.getConfig(); - const configPath = profileFilePath(profile.getName(), PROFILE_CONFIG_FILE_NAME); - fs.writeFileSync(configPath, JSON.stringify(config, null, JSON_INDENT), "utf8"); -}; +const profileManager = new ProfileManager(homeDirPath(), true); +export default profileManager; -export const purgeProfile = function(profile: Profile): void { - const home = profileDirPath(profile.getName()); - fs.rmdirSync(home, {recursive: true}); +/* + * Manages everything around profiles - home folder, profile home and additional files + * list, load, save, purgeAll, purge + */ + +export const isProfileNameValid = function(name: string): boolean { + return PROFILE_NAME_REGEXP.test(name); }; export class Profile { diff --git a/src/core/sn.ts b/src/core/sn.ts index 4cc0ca3..5353e00 100644 --- a/src/core/sn.ts +++ b/src/core/sn.ts @@ -1,5 +1,5 @@ import { DOMParser } from "@xmldom/xmldom"; -import * as xmlhelpers from "../util/xmlhelpers"; +import * as xmlhelpers from "../util/xmlhelpers.js"; export interface SNOAuthTokenData { access_token: string; diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..0d1cf05 --- /dev/null +++ b/test.mjs @@ -0,0 +1,3 @@ +import colors from "colors"; + +console.log(`TEST ${colors.red("test")}`); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9974ce6..35c2620 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "module": "CommonJS", + "module": "NodeNext", "target": "ES2022", + "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "typeRoots": ["./node_modules/@types", "./src/@types"], @@ -12,6 +13,7 @@ "alwaysStrict": true, "noImplicitAny": true, + "skipLibCheck": true, "resolveJsonModule": true, "esModuleInterop": true, "allowJs": true,