@@ -1,2 +1,3 @@ | |||||
node_modules | node_modules | ||||
.DS_Store | .DS_Store | ||||
.idea/ |
@@ -1,8 +0,0 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml |
@@ -1,13 +0,0 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="WEB_MODULE" version="4"> | |||||
<component name="NewModuleRootManager"> | |||||
<content url="file://$MODULE_DIR$"> | |||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" /> | |||||
<excludeFolder url="file://$MODULE_DIR$/packages/core/dist" /> | |||||
<excludeFolder url="file://$MODULE_DIR$/temp" /> | |||||
<excludeFolder url="file://$MODULE_DIR$/tmp" /> | |||||
</content> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@@ -1,8 +0,0 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/backend-template.iml" filepath="$PROJECT_DIR$/.idea/backend-template.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@@ -1,6 +0,0 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="VcsDirectoryMappings"> | |||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||||
</component> | |||||
</project> |
@@ -1,297 +1,9 @@ | |||||
import yargs, {Argv} from 'yargs'; | |||||
import * as clack from '@clack/prompts'; | |||||
import * as tty from 'tty'; | |||||
import { CommandHandler } from './common'; | |||||
import { Cli, CliOptions } from './packages/cli-wrapper'; | |||||
export interface CommandArgs { | |||||
aliases?: string[]; | |||||
command: string; | |||||
parameters: string[]; | |||||
describe: string; | |||||
handler: CommandHandler; | |||||
interactiveMode?: boolean; | |||||
} | |||||
type PromptValueArgs = [Record<string, string>, ...never[]] | |||||
| [string, string] | |||||
| [[string, string], ...never[]] | |||||
| [[string, string][], ...never[]]; | |||||
class DummyWriteStream extends tty.WriteStream { | |||||
private bufferInternal = Buffer.from(''); | |||||
constructor() { | |||||
super(0); | |||||
} | |||||
write(input: Uint8Array | string, _encoding?: BufferEncoding | ((err?: Error) => void), _cb?: (err?: Error) => void): boolean { | |||||
this.bufferInternal = Buffer.concat([this.bufferInternal, Buffer.from(input)]); | |||||
// noop | |||||
return true; | |||||
} | |||||
get buffer(): Buffer { | |||||
return this.bufferInternal; | |||||
} | |||||
} | |||||
export interface TestModeResult { | |||||
exitCode?: number; | |||||
stdout: DummyWriteStream; | |||||
stderr: DummyWriteStream; | |||||
} | |||||
interface InteractiveModeOptions { | |||||
option: string; | |||||
alias: string; | |||||
intro: string; | |||||
cancelled: string; | |||||
outro: string; | |||||
describe: string; | |||||
} | |||||
export interface CliOptions { | |||||
name: string; | |||||
interactiveMode?: Partial<InteractiveModeOptions>; | |||||
} | |||||
export class Cli { | |||||
private testMode = false; | |||||
private promptValues = {} as Record<string, unknown>; | |||||
private testModeResult: TestModeResult = { | |||||
exitCode: undefined as number | undefined, | |||||
stdout: new DummyWriteStream(), | |||||
stderr: new DummyWriteStream(), | |||||
}; | |||||
private readonly cli: Argv; | |||||
constructor(private readonly options: CliOptions) { | |||||
this.cli = yargs() | |||||
.scriptName(options.name) | |||||
.help() | |||||
.fail(false) | |||||
.strict(false); | |||||
} | |||||
private exit(code: number) { | |||||
if (this.testMode) { | |||||
this.testModeResult.exitCode = code; | |||||
return; | |||||
} | |||||
process.exit(code); | |||||
} | |||||
private async prompt(messages: Record<string, string>, interactiveModeOptions: InteractiveModeOptions) { | |||||
const { | |||||
intro, | |||||
cancelled, | |||||
outro, | |||||
} = interactiveModeOptions; | |||||
const messagesEntries = Object.entries(messages); | |||||
if (messagesEntries.length > 0) { | |||||
clack.intro(intro); | |||||
const values = await clack.group( | |||||
Object.fromEntries( | |||||
messagesEntries.map(([key, value]) => [key, () => clack.text({ message: value })]) | |||||
), | |||||
{ | |||||
onCancel: () => { | |||||
clack.cancel(cancelled); | |||||
this.exit(1); | |||||
} | |||||
} | |||||
); | |||||
clack.outro(outro); | |||||
return values; | |||||
} | |||||
return {}; | |||||
} | |||||
private async generateCommandArgs( | |||||
context: any, | |||||
originalCommandArgs: Record<string, unknown>, | |||||
interactive: boolean, | |||||
interactiveModeOptions: InteractiveModeOptions, | |||||
) { | |||||
if (!interactive) { | |||||
return originalCommandArgs; | |||||
} | |||||
// TODO properly filter attributes | |||||
const clackGroup = context.optional | |||||
.filter((optional: { cmd: string[] }) => { | |||||
const { | |||||
'$0': _$0, | |||||
i: _i, | |||||
interactive: _interactive, | |||||
test: _test, | |||||
_: __, | |||||
...rest | |||||
} = originalCommandArgs; | |||||
return !Object.keys(rest).includes(optional.cmd[0]); | |||||
}) | |||||
.reduce( | |||||
(group: Record<string, string>, optional: { cmd: string[], describe: string }) => ({ | |||||
...group, | |||||
[optional.cmd[0]]: optional.describe ?? optional.cmd[0], | |||||
}), | |||||
{} as Record<string, string>, | |||||
); | |||||
const promptedArgs = this.testMode | |||||
? this.promptValues | |||||
: await this.prompt(clackGroup, interactiveModeOptions); | |||||
return { | |||||
...originalCommandArgs, | |||||
...promptedArgs, | |||||
}; | |||||
} | |||||
private generateLogger() { | |||||
return { | |||||
log: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`${message?.toString()}\n`); | |||||
}, | |||||
warn: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`WARN: ${message?.toString()}\n`); | |||||
}, | |||||
debug: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`DEBUG: ${message?.toString()}\n`); | |||||
}, | |||||
error: (message: unknown) => { | |||||
const stderr = this.testMode ? this.testModeResult.stderr : process.stderr; | |||||
stderr.write(`${message?.toString()}\n`); | |||||
}, | |||||
}; | |||||
} | |||||
private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) { | |||||
const thisCli = this; | |||||
return async function handler(this: any, commandArgs: Record<string, unknown>) { | |||||
const self = this; | |||||
const { option, alias } = interactiveModeOptions; | |||||
const interactiveLong = option in commandArgs ? Boolean(commandArgs.interactive) : false; | |||||
const interactiveShort = alias in commandArgs ? Boolean(commandArgs.i) : false; | |||||
const interactive = interactiveLong || interactiveShort; | |||||
const buildArgs = await thisCli.generateCommandArgs( | |||||
self, | |||||
commandArgs, | |||||
interactive, | |||||
interactiveModeOptions, | |||||
); | |||||
let exited = false; | |||||
const returnCode = await handlerFn({ | |||||
self, | |||||
interactive, | |||||
logger: thisCli.generateLogger(), | |||||
send: (code: number) => { | |||||
exited = true; | |||||
thisCli.exit(code); | |||||
}, | |||||
args: buildArgs, | |||||
}); | |||||
if (!exited) { | |||||
thisCli.exit(returnCode ? returnCode : 0); | |||||
} | |||||
}; | |||||
} | |||||
command({ parameters, command, interactiveMode = true, handler, ...args }: CommandArgs): Cli { | |||||
const { | |||||
option = 'interactive', | |||||
alias = 'i', | |||||
describe = 'Interactive mode', | |||||
intro = 'Please provide the following values:', | |||||
cancelled = 'Operation cancelled.', | |||||
outro = 'Thank you!', | |||||
} = this.options?.interactiveMode ?? {}; | |||||
const commandArgs = { | |||||
...args, | |||||
command: `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}`, | |||||
usage: parameters.map((p) => `${command} ${p}`).join('\n'), | |||||
handler: this.buildHandler(handler, { | |||||
option, | |||||
alias, | |||||
intro, | |||||
cancelled, | |||||
outro, | |||||
describe, | |||||
}), | |||||
} as Record<string, unknown> | |||||
if (interactiveMode) { | |||||
commandArgs.options = { | |||||
...(commandArgs.options ?? {}), | |||||
[option]: { | |||||
alias, | |||||
describe, | |||||
type: 'boolean', | |||||
default: false, | |||||
hidden: true, | |||||
}, | |||||
}; | |||||
} | |||||
this.cli.command(commandArgs as unknown as Parameters<typeof this.cli.command>[0]); | |||||
return this; | |||||
} | |||||
async run(args: string[]): Promise<void> { | |||||
if (args.length > 0) { | |||||
await this.cli.parse(args); | |||||
return; | |||||
} | |||||
this.cli.showHelp(); | |||||
} | |||||
test() { | |||||
this.testMode = true; | |||||
this.promptValues = {}; | |||||
const thisCli = this; | |||||
return { | |||||
promptValue(...args: PromptValueArgs) { | |||||
const [testArg] = args; | |||||
if (typeof testArg === 'string') { | |||||
const [key, value] = args as [string, unknown]; | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
} | |||||
if (Array.isArray(testArg)) { | |||||
const [key, value] = testArg as [string, unknown]; | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
} | |||||
if (typeof testArg === 'object' && testArg !== null) { | |||||
Object.entries(testArg).forEach(([key, value]) => { | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
}); | |||||
} | |||||
export const createCli = (options: CliOptions) => { | |||||
const cli = new Cli(options); | |||||
return this; | |||||
}, | |||||
async run(args: string[]): Promise<TestModeResult> { | |||||
await thisCli.cli.parse(args); | |||||
return thisCli.testModeResult; | |||||
}, | |||||
}; | |||||
} | |||||
} | |||||
// TODO setup CLI here | |||||
export const createCli = (options: CliOptions) => { | |||||
return new Cli(options); | |||||
return cli; | |||||
}; | }; |
@@ -1,10 +1,10 @@ | |||||
import { Cli } from './cli'; | |||||
import { Cli } from './packages/cli-wrapper'; | |||||
import { AdderController, AdderControllerImpl } from './modules/adder'; | import { AdderController, AdderControllerImpl } from './modules/adder'; | ||||
export const addCommands = (cli: Cli) => { | export const addCommands = (cli: Cli) => { | ||||
const adderController: AdderController = new AdderControllerImpl(); | const adderController: AdderController = new AdderControllerImpl(); | ||||
cli | |||||
return cli | |||||
.command({ | .command({ | ||||
aliases: ['a'], | aliases: ['a'], | ||||
command: 'add', | command: 'add', | ||||
@@ -19,6 +19,4 @@ export const addCommands = (cli: Cli) => { | |||||
describe: 'Subtract two numbers', | describe: 'Subtract two numbers', | ||||
handler: adderController.subtractNumbers, | handler: adderController.subtractNumbers, | ||||
}); | }); | ||||
return cli; | |||||
}; | }; |
@@ -1,16 +0,0 @@ | |||||
export interface Logger { | |||||
log: (message: unknown) => void; | |||||
error: (message: unknown) => void; | |||||
warn: (message: unknown) => void; | |||||
debug: (message: unknown) => void; | |||||
} | |||||
export interface CommandHandlerArgs { | |||||
self: any; | |||||
interactive: boolean; | |||||
send: (message: number) => void; | |||||
logger: Logger; | |||||
args: any; | |||||
} | |||||
export type CommandHandler = (args: CommandHandlerArgs) => void | number | Promise<number> | Promise<void>; |
@@ -4,7 +4,7 @@ import { | |||||
ArgumentOutOfRangeError, | ArgumentOutOfRangeError, | ||||
InvalidArgumentTypeError, | InvalidArgumentTypeError, | ||||
} from './adder.service'; | } from './adder.service'; | ||||
import {CommandHandler} from '@/common'; | |||||
import { CommandHandler } from '../../packages/cli-wrapper'; | |||||
export interface AdderController { | export interface AdderController { | ||||
addNumbers: CommandHandler; | addNumbers: CommandHandler; | ||||
@@ -0,0 +1,309 @@ | |||||
import tty from 'tty'; | |||||
import yargs, {Argv} from 'yargs'; | |||||
import * as clack from '@clack/prompts'; | |||||
export interface Logger { | |||||
log: (message: unknown) => void; | |||||
error: (message: unknown) => void; | |||||
warn: (message: unknown) => void; | |||||
debug: (message: unknown) => void; | |||||
} | |||||
export interface CommandHandlerArgs { | |||||
self: any; | |||||
interactive: boolean; | |||||
send: (message: number) => void; | |||||
logger: Logger; | |||||
args: any; | |||||
} | |||||
export type CommandHandler = (args: CommandHandlerArgs) => void | number | Promise<number> | Promise<void>; | |||||
export interface CommandArgs { | |||||
aliases?: string[]; | |||||
command: string; | |||||
parameters: string[]; | |||||
describe: string; | |||||
handler: CommandHandler; | |||||
interactiveMode?: boolean; | |||||
} | |||||
type PromptValueArgs = [Record<string, string>, ...never[]] | |||||
| [string, string] | |||||
| [[string, string], ...never[]] | |||||
| [[string, string][], ...never[]]; | |||||
class DummyWriteStream extends tty.WriteStream { | |||||
private bufferInternal = Buffer.from(''); | |||||
constructor() { | |||||
super(0); | |||||
} | |||||
write(input: Uint8Array | string, _encoding?: BufferEncoding | ((err?: Error) => void), _cb?: (err?: Error) => void): boolean { | |||||
this.bufferInternal = Buffer.concat([this.bufferInternal, Buffer.from(input)]); | |||||
// noop | |||||
return true; | |||||
} | |||||
get buffer(): Buffer { | |||||
return this.bufferInternal; | |||||
} | |||||
} | |||||
export interface TestModeResult { | |||||
exitCode?: number; | |||||
stdout: DummyWriteStream; | |||||
stderr: DummyWriteStream; | |||||
} | |||||
interface InteractiveModeOptions { | |||||
option: string; | |||||
alias: string; | |||||
intro: string; | |||||
cancelled: string; | |||||
outro: string; | |||||
describe: string; | |||||
} | |||||
export interface CliOptions { | |||||
name: string; | |||||
interactiveMode?: Partial<InteractiveModeOptions>; | |||||
} | |||||
export class Cli { | |||||
private testMode = false; | |||||
private promptValues = {} as Record<string, unknown>; | |||||
private testModeResult: TestModeResult = { | |||||
exitCode: undefined as number | undefined, | |||||
stdout: new DummyWriteStream(), | |||||
stderr: new DummyWriteStream(), | |||||
}; | |||||
private readonly cli: Argv; | |||||
constructor(private readonly options: CliOptions) { | |||||
this.cli = yargs() | |||||
.scriptName(options.name) | |||||
.help() | |||||
.fail(false) | |||||
.strict(false); | |||||
} | |||||
private exit(code: number) { | |||||
if (this.testMode) { | |||||
this.testModeResult.exitCode = code; | |||||
return; | |||||
} | |||||
process.exit(code); | |||||
} | |||||
private async prompt(messages: Record<string, string>, interactiveModeOptions: InteractiveModeOptions) { | |||||
const { | |||||
intro, | |||||
cancelled, | |||||
outro, | |||||
} = interactiveModeOptions; | |||||
const messagesEntries = Object.entries(messages); | |||||
if (messagesEntries.length > 0) { | |||||
clack.intro(intro); | |||||
const values = await clack.group( | |||||
Object.fromEntries( | |||||
messagesEntries.map(([key, value]) => [key, () => clack.text({ message: value })]) | |||||
), | |||||
{ | |||||
onCancel: () => { | |||||
clack.cancel(cancelled); | |||||
this.exit(1); | |||||
} | |||||
} | |||||
); | |||||
clack.outro(outro); | |||||
return values; | |||||
} | |||||
return {}; | |||||
} | |||||
private async generateCommandArgs( | |||||
context: any, | |||||
originalCommandArgs: Record<string, unknown>, | |||||
interactive: boolean, | |||||
interactiveModeOptions: InteractiveModeOptions, | |||||
) { | |||||
if (!interactive) { | |||||
return originalCommandArgs; | |||||
} | |||||
// TODO properly filter attributes | |||||
const clackGroup = context.optional | |||||
.filter((optional: { cmd: string[] }) => { | |||||
const { | |||||
'$0': _$0, | |||||
i: _i, | |||||
interactive: _interactive, | |||||
test: _test, | |||||
_: __, | |||||
...rest | |||||
} = originalCommandArgs; | |||||
return !Object.keys(rest).includes(optional.cmd[0]); | |||||
}) | |||||
.reduce( | |||||
(group: Record<string, string>, optional: { cmd: string[], describe: string }) => ({ | |||||
...group, | |||||
[optional.cmd[0]]: optional.describe ?? optional.cmd[0], | |||||
}), | |||||
{} as Record<string, string>, | |||||
); | |||||
const promptedArgs = this.testMode | |||||
? this.promptValues | |||||
: await this.prompt(clackGroup, interactiveModeOptions); | |||||
return { | |||||
...originalCommandArgs, | |||||
...promptedArgs, | |||||
}; | |||||
} | |||||
private generateLogger() { | |||||
return { | |||||
log: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`${message?.toString()}\n`); | |||||
}, | |||||
warn: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`WARN: ${message?.toString()}\n`); | |||||
}, | |||||
debug: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`DEBUG: ${message?.toString()}\n`); | |||||
}, | |||||
error: (message: unknown) => { | |||||
const stderr = this.testMode ? this.testModeResult.stderr : process.stderr; | |||||
stderr.write(`${message?.toString()}\n`); | |||||
}, | |||||
}; | |||||
} | |||||
private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) { | |||||
const thisCli = this; | |||||
return async function handler(this: any, commandArgs: Record<string, unknown>) { | |||||
const self = this; | |||||
const { option, alias } = interactiveModeOptions; | |||||
const interactiveLong = option in commandArgs ? Boolean(commandArgs.interactive) : false; | |||||
const interactiveShort = alias in commandArgs ? Boolean(commandArgs.i) : false; | |||||
const interactive = interactiveLong || interactiveShort; | |||||
const buildArgs = await thisCli.generateCommandArgs( | |||||
self, | |||||
commandArgs, | |||||
interactive, | |||||
interactiveModeOptions, | |||||
); | |||||
let exited = false; | |||||
const returnCode = await handlerFn({ | |||||
self, | |||||
interactive, | |||||
logger: thisCli.generateLogger(), | |||||
send: (code: number) => { | |||||
exited = true; | |||||
thisCli.exit(code); | |||||
}, | |||||
args: buildArgs, | |||||
}); | |||||
if (!exited) { | |||||
thisCli.exit(returnCode ? returnCode : 0); | |||||
} | |||||
}; | |||||
} | |||||
command({ parameters, command, interactiveMode = true, handler, ...args }: CommandArgs): Cli { | |||||
const { | |||||
option = 'interactive', | |||||
alias = 'i', | |||||
describe = 'Interactive mode', | |||||
intro = 'Please provide the following values:', | |||||
cancelled = 'Operation cancelled.', | |||||
outro = 'Thank you!', | |||||
} = this.options?.interactiveMode ?? {}; | |||||
const commandArgs = { | |||||
...args, | |||||
command: `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}`, | |||||
usage: parameters.map((p) => `${command} ${p}`).join('\n'), | |||||
handler: this.buildHandler(handler, { | |||||
option, | |||||
alias, | |||||
intro, | |||||
cancelled, | |||||
outro, | |||||
describe, | |||||
}), | |||||
} as Record<string, unknown> | |||||
if (interactiveMode) { | |||||
commandArgs.options = { | |||||
...(commandArgs.options ?? {}), | |||||
[option]: { | |||||
alias, | |||||
describe, | |||||
type: 'boolean', | |||||
default: false, | |||||
hidden: true, | |||||
}, | |||||
}; | |||||
} | |||||
this.cli.command(commandArgs as unknown as Parameters<typeof this.cli.command>[0]); | |||||
return this; | |||||
} | |||||
async run(args: string[]): Promise<void> { | |||||
if (args.length > 0) { | |||||
await this.cli.parse(args); | |||||
return; | |||||
} | |||||
this.cli.showHelp(); | |||||
} | |||||
test() { | |||||
this.testMode = true; | |||||
this.promptValues = {}; | |||||
const thisCli = this; | |||||
return { | |||||
promptValue(...args: PromptValueArgs) { | |||||
const [testArg] = args; | |||||
if (typeof testArg === 'string') { | |||||
const [key, value] = args as [string, unknown]; | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
} | |||||
if (Array.isArray(testArg)) { | |||||
const [key, value] = testArg as [string, unknown]; | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
} | |||||
if (typeof testArg === 'object' && testArg !== null) { | |||||
Object.entries(testArg).forEach(([key, value]) => { | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
}); | |||||
} | |||||
return this; | |||||
}, | |||||
async run(args: string[]): Promise<TestModeResult> { | |||||
await thisCli.cli.parse(args); | |||||
return thisCli.testModeResult; | |||||
}, | |||||
}; | |||||
} | |||||
} |
@@ -4,11 +4,10 @@ import { AdderController, AdderControllerImpl } from '@/modules/adder'; | |||||
export const addRoutes = (server: FastifyInstance) => { | export const addRoutes = (server: FastifyInstance) => { | ||||
const adderController: AdderController = new AdderControllerImpl(); | const adderController: AdderController = new AdderControllerImpl(); | ||||
server.route({ | |||||
method: 'POST', | |||||
url: '/', | |||||
handler: adderController.addNumbers, | |||||
}); | |||||
return server; | |||||
return server | |||||
.route({ | |||||
method: 'POST', | |||||
url: '/', | |||||
handler: adderController.addNumbers, | |||||
}); | |||||
}; | }; |