feat(cli): add arg parsing; chore(config): organize package.json/tsconfig
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
21
package.json
21
package.json
@@ -1,14 +1,23 @@
|
||||
{
|
||||
"name": "issue-scheduler",
|
||||
"description": "Schedule issues in Gitea or Github from issue templates",
|
||||
"version": "0.0.0",
|
||||
"description": "Issue scheduler",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "test"
|
||||
"main": "dist/index.js",
|
||||
|
||||
"author": "Luís Figueiredo (TxFig)",
|
||||
"homepage": "https://gitea.alluna.pt/TxFig/issue-scheduler",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.alluna.pt/TxFig/issue-scheduler.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"keywords": [],
|
||||
|
||||
"scripts": {
|
||||
"test": "vitest --run",
|
||||
"build": "tsc",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
|
||||
41
src/cli.test.ts
Normal file
41
src/cli.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { test, expect, describe } from "vitest";
|
||||
import * as cli from "./cli.js"
|
||||
|
||||
|
||||
|
||||
test("get flag value", () => {
|
||||
|
||||
})
|
||||
|
||||
test("optional string flag", () => {
|
||||
|
||||
})
|
||||
|
||||
test("optional string literal flag", () => {
|
||||
|
||||
})
|
||||
|
||||
test("optional boolean flag", () => {
|
||||
// no flags at all
|
||||
expect(cli.optionalBooleanFlag([], [])).toBe(false)
|
||||
// target flag not present in args
|
||||
expect(cli.optionalBooleanFlag([], ["--verbose"])).toBe(false)
|
||||
// flags list empty (nothing to match)
|
||||
expect(cli.optionalBooleanFlag(["--verbose"], [])).toBe(false)
|
||||
// flag present with no value
|
||||
expect(cli.optionalBooleanFlag(["--verbose"], ["--verbose"])).toBe(true)
|
||||
// flag followed by another flag
|
||||
expect(cli.optionalBooleanFlag(["--verbose", "--other"], ["--verbose"])).toBe(true)
|
||||
// duplicate flag; second ignored even if invalid
|
||||
expect(cli.optionalBooleanFlag(["--verbose", "--verbose", "true"], ["--verbose"])).toBe(true)
|
||||
// inline assignment not recognized as the flag token
|
||||
expect(cli.optionalBooleanFlag(["--verbose=true"], ["--verbose"])).toBe(false)
|
||||
// flag followed by a value
|
||||
expect(() => cli.optionalBooleanFlag(["--verbose", "true"], ["--verbose"])).toThrowError(cli.NoValueExpected)
|
||||
// flag followed by empty string (still a value)
|
||||
expect(() => cli.optionalBooleanFlag(["--verbose", ""], ["--verbose"])).toThrowError(cli.NoValueExpected)
|
||||
})
|
||||
|
||||
test("parse args", () => {
|
||||
|
||||
})
|
||||
121
src/cli.ts
Normal file
121
src/cli.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export interface CLIConfig {
|
||||
// path to config file
|
||||
config_filepath: string,
|
||||
git_client: "gitea" | "github",
|
||||
log_file: string | undefined,
|
||||
// verbose logging
|
||||
verbose: boolean,
|
||||
// doesn't log to stdout/stderr (still logs to file if --log-file)
|
||||
quiet: boolean
|
||||
}
|
||||
|
||||
export const defaults: CLIConfig = {
|
||||
config_filepath: "config.toml",
|
||||
git_client: "gitea",
|
||||
log_file: undefined,
|
||||
verbose: false,
|
||||
quiet: false
|
||||
}
|
||||
|
||||
export class EmptyArgumentError extends Error {}
|
||||
export class NoValueExpected extends Error {}
|
||||
export class InvalidValue extends Error {}
|
||||
|
||||
|
||||
/**
|
||||
* Searches for first argument from flags options
|
||||
* Valid values don't start with dashes
|
||||
* @example Flag + Valid value
|
||||
* getFlagValue(["--config", "config.toml"], ["--config", "-c"])
|
||||
* // result: "config.toml"
|
||||
* @example Flag + No value
|
||||
* getFlagValue(["-c", "--verbose"], ["--config", "-c"])
|
||||
* // result: null
|
||||
* @example No Flag
|
||||
* getFlagValue(["config.toml"], ["--config", "-c"])
|
||||
* // result: undefined
|
||||
*
|
||||
* @param args An array of all command-line arguments
|
||||
* @param flags An array of flag strings to search for
|
||||
* @return {string}: When a flag is found and has a valid value
|
||||
* @return {null}: When a flag is found but has no valid value
|
||||
* @return {undefined}: When no flag is found in the arguments
|
||||
*
|
||||
*/
|
||||
export function getFlagValue(args: string[], flags: string[]): string | null | undefined {
|
||||
const index = args.findIndex(arg => flags.includes(arg))
|
||||
if (index == -1) return undefined
|
||||
if (index == args.length - 1) return null
|
||||
|
||||
const next = args[index + 1]!
|
||||
if (next.startsWith("-")) {
|
||||
return null
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for when the flag is optional, but a value is required if the flag appears.
|
||||
* @throws EmptyArgumentError when the flag is present but has no value.
|
||||
* @returns string | undefined — string when present with value, undefined when flag not present
|
||||
*/
|
||||
export function optionalStringFlag(args: string[], flags: string[]): string | undefined {
|
||||
const value = getFlagValue(args, flags)
|
||||
if (value === null) {
|
||||
const errorMessage = `${flags.join(", ")} flag requires a value`
|
||||
throw new EmptyArgumentError(errorMessage)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function optionalStringLiteralFlag<Option extends string>(
|
||||
args: string[], flags: string[], options: Option[]
|
||||
): Option | undefined {
|
||||
const value = getFlagValue(args, flags)
|
||||
if (value === null) {
|
||||
const errorMessage = `${flags.join(", ")} flag requires a value`
|
||||
throw new EmptyArgumentError(errorMessage)
|
||||
}
|
||||
|
||||
if (value && !(options as string[]).includes(value)) {
|
||||
const errorMessage = `${flags.join(", ")} flag requires one of these options ${options.join(", ")}`
|
||||
throw new InvalidValue(errorMessage)
|
||||
}
|
||||
|
||||
return value as (Option | undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
export function optionalBooleanFlag(args: string[], flags: string[]): boolean {
|
||||
const value = getFlagValue(args, flags)
|
||||
if (typeof value === "string") {
|
||||
throw new NoValueExpected()
|
||||
}
|
||||
|
||||
return value === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments from a string array (process.argv)
|
||||
* @param args Command line arguments
|
||||
* @return {CLIConfig} Parsed flags
|
||||
*/
|
||||
export function parseArgs(args: string[]): CLIConfig {
|
||||
const config = defaults
|
||||
|
||||
const configValue = optionalStringFlag(args, ["--config", "-c"])
|
||||
if (configValue) config.config_filepath = configValue
|
||||
|
||||
const gitClientValue = optionalStringLiteralFlag(args, ["--git-client", "-gc"], ["gitea", "github"])
|
||||
if (gitClientValue) config.git_client = gitClientValue
|
||||
const logFileValue = optionalStringFlag(args, ["--log-file", "-lf"])
|
||||
if (logFileValue) config.log_file = logFileValue
|
||||
const verboseValue = optionalBooleanFlag(args, ["--verbose"])
|
||||
if (verboseValue) config.verbose = verboseValue
|
||||
const quietValue = optionalBooleanFlag(args, ["--quiet"])
|
||||
if (quietValue) config.quiet = quietValue
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
console.log("Hello World")
|
||||
import * as cli from "./cli.js"
|
||||
|
||||
|
||||
const config = cli.parseArgs(process.argv)
|
||||
console.log(config)
|
||||
|
||||
@@ -1,41 +1,28 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": [],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"moduleDetection": "force",
|
||||
"esModuleInterop": true,
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user