Comparing version 0.0.4 to 0.1.0
108
dist/cli.js
// src/cli.ts | ||
import * as fs from "node:fs"; | ||
import * as path from "node:path"; | ||
import {z} from "zod"; | ||
import {zodToJsonSchema} from "zod-to-json-schema"; | ||
// src/option-parsers.ts | ||
function parseBooleanOption(arg) { | ||
if (arg === `false`) | ||
return false; | ||
if (arg === `0`) | ||
return false; | ||
return true; | ||
} | ||
function parseNumberOption(arg) { | ||
if (arg === ``) | ||
return 1; | ||
if (/^,+$/.test(arg)) | ||
return arg.length + 1; | ||
return Number.parseFloat(arg); | ||
} | ||
function parseStringOption(arg) { | ||
return arg; | ||
} | ||
function parseArrayOption(arg) { | ||
return arg.split(` `); | ||
} | ||
// src/retrieve-positional-args.ts | ||
@@ -10,2 +33,3 @@ function retrievePositionalArgs(cliName, positionalArgTree, passed) { | ||
const positionalArgs = endOfOptionsDelimiterIndex === -1 ? undefined : passed.slice(endOfOptionsDelimiterIndex + 1); | ||
const namedPositionalArgs = []; | ||
const validPositionalArgs = []; | ||
@@ -26,3 +50,6 @@ let treePointer = positionalArgTree; | ||
} | ||
return []; | ||
return { | ||
path: [], | ||
route: `` | ||
}; | ||
} | ||
@@ -46,2 +73,3 @@ for (const positionalArg of positionalArgs) { | ||
treePointer = treePointer[1][positionalArg]; | ||
namedPositionalArgs.push(positionalArg); | ||
validPositionalArgs.push(positionalArg); | ||
@@ -52,2 +80,3 @@ } else if (Object.keys(treePointer[1]).length > 0) { | ||
treePointer = treePointer[1][variablePath]; | ||
namedPositionalArgs.push(variablePath); | ||
validPositionalArgs.push(positionalArg); | ||
@@ -83,25 +112,10 @@ continue; | ||
} | ||
return validPositionalArgs; | ||
return { | ||
path: validPositionalArgs, | ||
route: namedPositionalArgs.join(`/`) | ||
}; | ||
} | ||
// src/option-parsers.ts | ||
function parseBooleanOption(arg) { | ||
if (arg === `false`) | ||
return false; | ||
if (arg === `0`) | ||
return false; | ||
return true; | ||
} | ||
function parseNumberOption(arg) { | ||
if (arg === ``) | ||
return 1; | ||
if (/^,+$/.test(arg)) | ||
return arg.length + 1; | ||
return Number.parseFloat(arg); | ||
} | ||
function parseStringOption(arg) { | ||
return arg; | ||
} | ||
// src/encapsulate.ts | ||
var encapsulateConsole = function() { | ||
function encapsulateConsole() { | ||
const createMockFn = () => { | ||
@@ -128,4 +142,4 @@ const calls = []; | ||
return { mockConsoleCalls, restoreConsole }; | ||
}; | ||
var withCapturedOutput = function(fn, options = { | ||
} | ||
function withCapturedOutput(fn, options = { | ||
console: true, | ||
@@ -171,3 +185,3 @@ stdout: true, | ||
}; | ||
}; | ||
} | ||
function encapsulate(fn, options) { | ||
@@ -231,5 +245,11 @@ const { | ||
} | ||
var myTree = required({ | ||
hello: optional({ | ||
world: null, | ||
$name: optional({ good: required({ morning: null }) }) | ||
}) | ||
}); | ||
// src/cli.ts | ||
var retrieveArgValue = function(argument, flag2) { | ||
function retrieveArgValue(argument, flag2) { | ||
const isSwitch = argument.startsWith(`--`); | ||
@@ -246,8 +266,7 @@ const [key, value] = argument.split(`=`); | ||
return retrievedValue; | ||
}; | ||
} | ||
function cli({ | ||
cliName, | ||
positionalArgTree, | ||
options, | ||
optionsSchema, | ||
routes, | ||
routeOptions, | ||
discoverConfigPath = () => path.join(process.cwd(), `${cliName}.config.json`) | ||
@@ -262,5 +281,11 @@ }, logger = { | ||
let optionsFromConfig; | ||
const positionalArgs = positionalArgTree ? retrievePositionalArgs(cliName, positionalArgTree, passed) : []; | ||
const positionalArgs = routes ? retrievePositionalArgs(cliName, routes, passed) : { path: [], route: `` }; | ||
const route = routeOptions[positionalArgs.route]; | ||
const options = route?.options ?? {}; | ||
const optionsSchema = route?.optionsSchema ?? z.object({}); | ||
if (route === undefined) { | ||
throw new Error(`Could not find options for route "${positionalArgs.route}". Valid routes are: \n\t- ${Object.keys(routeOptions).join(`\n\t- `)}`); | ||
} | ||
if (discoverConfigPath) { | ||
const configFilePath = discoverConfigPath(positionalArgs); | ||
const configFilePath = discoverConfigPath(positionalArgs.path); | ||
if (configFilePath) { | ||
@@ -304,7 +329,17 @@ if (fs.existsSync(configFilePath)) { | ||
return { | ||
positionalArgs, | ||
suppliedOptions, | ||
writeJsonSchema: (filepath) => { | ||
const jsonSchema = zodToJsonSchema(optionsSchema); | ||
fs.writeFileSync(filepath, JSON.stringify(jsonSchema, null, `\t`)); | ||
inputs: { | ||
case: positionalArgs.route, | ||
path: positionalArgs.path, | ||
opts: suppliedOptions | ||
}, | ||
writeJsonSchema: (outdir) => { | ||
for (const [unsafeRoute, optionsGroup] of Object.entries(routeOptions)) { | ||
if (optionsGroup === null) { | ||
continue; | ||
} | ||
const safeRoute = unsafeRoute.replaceAll(`/`, `.`); | ||
const jsonSchema = zodToJsonSchema(optionsGroup.optionsSchema); | ||
const filepath = path.resolve(outdir, `${cliName}.${safeRoute || `main`}.schema.json`); | ||
fs.writeFileSync(filepath, JSON.stringify(jsonSchema, null, `\t`)); | ||
} | ||
} | ||
@@ -319,2 +354,3 @@ }; | ||
parseBooleanOption, | ||
parseArrayOption, | ||
optional, | ||
@@ -321,0 +357,0 @@ encapsulate, |
{ | ||
"name": "comline", | ||
"version": "0.0.4", | ||
"version": "0.1.0", | ||
"license": "MIT", | ||
@@ -20,18 +20,22 @@ "author": { | ||
"dependencies": { | ||
"zod": "3.22.4", | ||
"zod-to-json-schema": "3.22.5" | ||
"zod": "3.23.8", | ||
"zod-to-json-schema": "3.23.2" | ||
}, | ||
"devDependencies": { | ||
"@types/bun": "1.0.12", | ||
"@types/bun": "1.1.8", | ||
"@types/node": "22.5.4", | ||
"@types/tmp": "0.2.6", | ||
"concurrently": "8.2.2", | ||
"tmp": "0.2.3", | ||
"tsup": "8.0.2", | ||
"vitest": "1.4.0" | ||
"tsup": "8.2.4", | ||
"vitest": "2.0.5" | ||
}, | ||
"scripts": { | ||
"build": "bun build --outdir dist --target node --external zod --external zod-to-json-schema -- src/cli.ts", | ||
"build:js": "bun build --outdir dist --target node --external zod --external zod-to-json-schema -- src/cli.ts", | ||
"build:dts": "tsup", | ||
"build": "concurrently \"bun:build:*\"", | ||
"lint:biome": "biome check -- .", | ||
"lint:eslint": "eslint .", | ||
"lint:eslint": "eslint --flag unstable_ts_config -- .", | ||
"lint:types": "tsc --noEmit", | ||
"lint:types:watch": "tsc --watch --noEmit", | ||
"lint": "bun run lint:biome && bun run lint:eslint && bun run lint:types", | ||
@@ -38,0 +42,0 @@ "test": "vitest", |
113
src/cli.ts
import * as fs from "node:fs" | ||
import * as path from "node:path" | ||
import type { ZodSchema } from "zod" | ||
import { z } from "zod" | ||
import { zodToJsonSchema } from "zod-to-json-schema" | ||
import type { Flag } from "./flag" | ||
import type { Tree, TreePath } from "./tree" | ||
import { parseStringOption } from "./option-parsers" | ||
import { retrievePositionalArgs } from "./retrieve-positional-args" | ||
import { parseStringOption } from "./option-parsers" | ||
import type { Flat, ToPath, Tree, TreeMap, TreePath } from "./tree" | ||
export * from "./encapsulate" | ||
export * from "./flag" | ||
export * from "./option-parsers" | ||
export * from "./encapsulate" | ||
export * from "./tree" | ||
export * from "./flag" | ||
@@ -24,2 +26,4 @@ export type CliOptionValue = | ||
const FILENAME_CHAR_ALLOWLIST = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-` | ||
export type CliOption<T extends CliOptionValue> = (T extends string | ||
@@ -42,13 +46,29 @@ ? { | ||
export type CommandLineInterface< | ||
PositionalArgTree extends Tree, | ||
Options extends Record<string, CliOptionValue>, | ||
> = { | ||
export type CliParseOutput<CLI extends CommandLineInterface<any>> = Flat< | ||
Readonly<{ | ||
[K in keyof CLI[`routeOptions`]]: K extends string | ||
? Readonly<{ | ||
case: K | ||
path: ToPath<K, `/`> | ||
opts: CLI[`routeOptions`][K] extends { optionsSchema: any } | ||
? z.infer<CLI[`routeOptions`][K][`optionsSchema`]> | ||
: null | ||
}> | ||
: never | ||
}>[keyof CLI[`routeOptions`]] | ||
> | ||
export type OptionsGroup<Options extends Record<string, CliOptionValue> | null> = | ||
Options extends Record<string, CliOptionValue> | ||
? { | ||
options: { [K in keyof Options]: CliOption<Options[K]> } | ||
optionsSchema: ZodSchema<Options> | ||
} | ||
: null | ||
export type CommandLineInterface<Routes extends Tree> = { | ||
cliName: string | ||
discoverConfigPath?: ( | ||
positionalArgs: TreePath<PositionalArgTree>, | ||
) => string | undefined | ||
positionalArgTree?: PositionalArgTree | ||
options: { [K in keyof Options]: CliOption<Options[K]> } | ||
optionsSchema: ZodSchema<Options> | ||
routeOptions: TreeMap<Routes, OptionsGroup<any>> | ||
routes?: Routes | ||
discoverConfigPath?: (positionalArgs: TreePath<Routes>) => string | undefined | ||
} | ||
@@ -75,14 +95,15 @@ | ||
export type CliRoutes<CLI extends CommandLineInterface<any>> = CLI[`routes`] | ||
export function cli< | ||
PositionalArgs extends Tree, | ||
Options extends Record<string, CliOptionValue>, | ||
CLI extends CommandLineInterface<Routes>, | ||
Routes extends Tree = Exclude<CLI[`routes`], undefined>, | ||
>( | ||
{ | ||
cliName, | ||
positionalArgTree, | ||
options, | ||
optionsSchema, | ||
routes, | ||
routeOptions, | ||
discoverConfigPath = () => | ||
path.join(process.cwd(), `${cliName}.config.json`), | ||
}: CommandLineInterface<PositionalArgs, Options>, | ||
}: CLI, | ||
logger = { | ||
@@ -94,14 +115,27 @@ error: (...args: any[]) => { | ||
): (args: string[]) => { | ||
positionalArgs: TreePath<PositionalArgs> | ||
suppliedOptions: Options | ||
writeJsonSchema: (path: string) => void | ||
inputs: CliParseOutput<CLI> | ||
writeJsonSchema: (outdir: string) => void | ||
} { | ||
return (passed = process.argv) => { | ||
type Options = CLI[`routeOptions`][keyof CLI[`routeOptions`]] | ||
let failedValidation = false | ||
let optionsFromConfig: Options | undefined | ||
const positionalArgs = positionalArgTree | ||
? retrievePositionalArgs(cliName, positionalArgTree, passed) | ||
: ([] as any) | ||
const positionalArgs = routes | ||
? retrievePositionalArgs(cliName, routes, passed) | ||
: { path: [] as TreePath<Routes>, route: `` } | ||
const route: OptionsGroup<any> = routeOptions[positionalArgs.route] | ||
const options = route?.options ?? {} | ||
const optionsSchema = route?.optionsSchema ?? z.object({}) | ||
if (route === undefined) { | ||
throw new Error( | ||
`Could not find options for route "${positionalArgs.route}". Valid routes are: \n\t- ${Object.keys(routeOptions).join(`\n\t- `)}`, | ||
) | ||
} | ||
if (discoverConfigPath) { | ||
const configFilePath = discoverConfigPath(positionalArgs) | ||
const configFilePath = discoverConfigPath(positionalArgs.path) | ||
if (configFilePath) { | ||
@@ -167,7 +201,22 @@ if (fs.existsSync(configFilePath)) { | ||
return { | ||
positionalArgs, | ||
suppliedOptions, | ||
writeJsonSchema: (filepath) => { | ||
const jsonSchema = zodToJsonSchema(optionsSchema) | ||
fs.writeFileSync(filepath, JSON.stringify(jsonSchema, null, `\t`)) | ||
inputs: { | ||
case: positionalArgs.route, | ||
path: positionalArgs.path, | ||
opts: suppliedOptions, | ||
} as unknown as CliParseOutput<CLI>, | ||
writeJsonSchema: (outdir) => { | ||
for (const [unsafeRoute, optionsGroup] of Object.entries( | ||
routeOptions as Record<string, OptionsGroup<any> | null>, | ||
)) { | ||
if (optionsGroup === null) { | ||
continue | ||
} | ||
const safeRoute = unsafeRoute.replaceAll(`/`, `.`) | ||
const jsonSchema = zodToJsonSchema(optionsGroup.optionsSchema) | ||
const filepath = path.resolve( | ||
outdir, | ||
`${cliName}.${safeRoute || `main`}.schema.json`, | ||
) | ||
fs.writeFileSync(filepath, JSON.stringify(jsonSchema, null, `\t`)) | ||
} | ||
}, | ||
@@ -174,0 +223,0 @@ } |
@@ -16,1 +16,5 @@ export function parseBooleanOption(arg: string): boolean { | ||
} | ||
export function parseArrayOption(arg: string): string[] { | ||
return arg.split(` `) | ||
} |
@@ -1,2 +0,2 @@ | ||
import type { Tree, TreePath } from "./tree" | ||
import type { Join, Tree, TreePath, TreePathName } from "./tree" | ||
@@ -7,3 +7,6 @@ export function retrievePositionalArgs<PositionalArgTree extends Tree>( | ||
passed: string[], | ||
): TreePath<PositionalArgTree> { | ||
): { | ||
path: TreePath<PositionalArgTree> | ||
route: Join<TreePathName<PositionalArgTree>> | ||
} { | ||
const endOfOptionsDelimiterIndex = passed.indexOf(`--`) | ||
@@ -15,2 +18,3 @@ const positionalArgs = | ||
const namedPositionalArgs: string[] = [] | ||
const validPositionalArgs: string[] = [] | ||
@@ -32,3 +36,6 @@ let treePointer: object = positionalArgTree | ||
} | ||
return [] as TreePath<PositionalArgTree> | ||
return { | ||
path: [] as TreePath<PositionalArgTree>, | ||
route: `` as Join<TreePathName<PositionalArgTree>>, | ||
} | ||
} | ||
@@ -53,2 +60,3 @@ for (const positionalArg of positionalArgs) { | ||
treePointer = treePointer[1][positionalArg] | ||
namedPositionalArgs.push(positionalArg) | ||
validPositionalArgs.push(positionalArg) | ||
@@ -61,2 +69,3 @@ } else if (Object.keys(treePointer[1]).length > 0) { | ||
treePointer = treePointer[1][variablePath] | ||
namedPositionalArgs.push(variablePath) | ||
validPositionalArgs.push(positionalArg) | ||
@@ -94,3 +103,8 @@ continue | ||
} | ||
return validPositionalArgs as TreePath<PositionalArgTree> | ||
return { | ||
path: validPositionalArgs as TreePath<PositionalArgTree>, | ||
route: namedPositionalArgs.join(`/`) as Join< | ||
TreePathName<PositionalArgTree> | ||
>, | ||
} | ||
} |
@@ -16,9 +16,62 @@ export function required<T>(arg: T): [`required`, T] { | ||
? T[1][K] extends Tree | ||
? [K extends `$${string}` ? string : K, ...TreePath<T[1][K]>] | ||
: [K extends `$${string}` ? string : K] | ||
? [K extends `$${string}` ? string & {} : K, ...TreePath<T[1][K]>] | ||
: [K extends `$${string}` ? string & {} : K] | ||
: | ||
| (T[1][K] extends Tree | ||
? [K extends `$${string}` ? string : K, ...TreePath<T[1][K]>] | ||
: [K extends `$${string}` ? string : K]) | ||
? [K extends `$${string}` ? string & {} : K, ...TreePath<T[1][K]>] | ||
: [K extends `$${string}` ? string & {} : K]) | ||
| [] | ||
}[keyof T[1]] | ||
export type TreePathName<T extends Tree> = { | ||
[K in keyof T[1]]: T[0] extends `required` | ||
? T[1][K] extends Tree | ||
? [K, ...TreePathName<T[1][K]>] | ||
: [K] | ||
: (T[1][K] extends Tree ? [K, ...TreePathName<T[1][K]>] : [K]) | [] | ||
}[keyof T[1]] | ||
export type Flat<R extends { [K in PropertyKey]: any }> = { | ||
[K in keyof R]: R[K] | ||
} | ||
export type TreeMap<T extends Tree, P> = { | ||
[K in Join<TreePathName<T>, `/`>]: P | ||
} | ||
export type Join< | ||
Arr extends any[], | ||
Separator extends string = ``, | ||
> = Arr extends [] | ||
? `` | ||
: Arr extends [infer First extends string] | ||
? First | ||
: Arr extends [infer First extends string, ...infer Rest extends string[]] | ||
? `${First}${Separator}${Join<Rest, Separator>}` | ||
: string | ||
export type ToPath< | ||
S extends string, | ||
D extends string, | ||
> = S extends `${infer T extends string}${D}${infer U extends string}` | ||
? T extends `$${string}` | ||
? [string & {}, ...ToPath<U, D>] | ||
: [T, ...ToPath<U, D>] | ||
: S extends `$${string}` | ||
? [string & {}] | ||
: [S] | ||
export type MySplit = ToPath<`hello/$world/good/morning`, `/`> | ||
const myTree = required({ | ||
hello: optional({ | ||
world: null, | ||
$name: optional({ good: required({ morning: null }) }), | ||
}), | ||
}) | ||
type MyTreePath = TreePath<typeof myTree> | ||
type MyTreeMap = TreeMap<typeof myTree, null> | ||
type MyTreePathsJoined = Join<MyTreePath, `/`> | ||
// type MyTreePathsJoined$ = Join<MyTreePath$, `/`> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
37525
10
1042
7
+ Addedzod@3.23.8(transitive)
+ Addedzod-to-json-schema@3.23.2(transitive)
- Removedzod@3.22.4(transitive)
- Removedzod-to-json-schema@3.22.5(transitive)
Updatedzod@3.23.8
Updatedzod-to-json-schema@3.23.2