@navios/commander 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +242 -0
  3. package/dist/src/commander.application.d.mts +29 -0
  4. package/dist/src/commander.application.d.mts.map +1 -0
  5. package/dist/src/commander.factory.d.mts +8 -0
  6. package/dist/src/commander.factory.d.mts.map +1 -0
  7. package/dist/src/decorators/cli-module.decorator.d.mts +7 -0
  8. package/dist/src/decorators/cli-module.decorator.d.mts.map +1 -0
  9. package/dist/src/decorators/command.decorator.d.mts +8 -0
  10. package/dist/src/decorators/command.decorator.d.mts.map +1 -0
  11. package/dist/src/decorators/index.d.mts +3 -0
  12. package/dist/src/decorators/index.d.mts.map +1 -0
  13. package/dist/src/index.d.mts +8 -0
  14. package/dist/src/index.d.mts.map +1 -0
  15. package/dist/src/interfaces/cli-module.interface.d.mts +5 -0
  16. package/dist/src/interfaces/cli-module.interface.d.mts.map +1 -0
  17. package/dist/src/interfaces/command-handler.interface.d.mts +4 -0
  18. package/dist/src/interfaces/command-handler.interface.d.mts.map +1 -0
  19. package/dist/src/interfaces/index.d.mts +3 -0
  20. package/dist/src/interfaces/index.d.mts.map +1 -0
  21. package/dist/src/interfaces/module.interface.d.mts +5 -0
  22. package/dist/src/interfaces/module.interface.d.mts.map +1 -0
  23. package/dist/src/metadata/cli-module.metadata.d.mts +11 -0
  24. package/dist/src/metadata/cli-module.metadata.d.mts.map +1 -0
  25. package/dist/src/metadata/command.metadata.d.mts +12 -0
  26. package/dist/src/metadata/command.metadata.d.mts.map +1 -0
  27. package/dist/src/metadata/index.d.mts +3 -0
  28. package/dist/src/metadata/index.d.mts.map +1 -0
  29. package/dist/src/services/cli-parser.service.d.mts +44 -0
  30. package/dist/src/services/cli-parser.service.d.mts.map +1 -0
  31. package/dist/src/services/index.d.mts +3 -0
  32. package/dist/src/services/index.d.mts.map +1 -0
  33. package/dist/src/services/module-loader.service.d.mts +33 -0
  34. package/dist/src/services/module-loader.service.d.mts.map +1 -0
  35. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -0
  37. package/dist/tsup.config.d.mts +3 -0
  38. package/dist/tsup.config.d.mts.map +1 -0
  39. package/dist/vitest.config.d.mts +3 -0
  40. package/dist/vitest.config.d.mts.map +1 -0
  41. package/lib/_tsup-dts-rollup.d.mts +456 -0
  42. package/lib/_tsup-dts-rollup.d.ts +456 -0
  43. package/lib/index.d.mts +98 -0
  44. package/lib/index.d.ts +98 -0
  45. package/lib/index.js +541 -0
  46. package/lib/index.js.map +1 -0
  47. package/lib/index.mjs +524 -0
  48. package/lib/index.mjs.map +1 -0
  49. package/package.json +40 -0
  50. package/project.json +66 -0
  51. package/src/__tests__/commander.factory.e2e.spec.mts +965 -0
  52. package/src/commander.application.mts +159 -0
  53. package/src/commander.factory.mts +20 -0
  54. package/src/decorators/cli-module.decorator.mts +39 -0
  55. package/src/decorators/command.decorator.mts +29 -0
  56. package/src/decorators/index.mts +2 -0
  57. package/src/index.mts +7 -0
  58. package/src/interfaces/command-handler.interface.mts +3 -0
  59. package/src/interfaces/index.mts +2 -0
  60. package/src/interfaces/module.interface.mts +4 -0
  61. package/src/metadata/cli-module.metadata.mts +54 -0
  62. package/src/metadata/command.metadata.mts +54 -0
  63. package/src/metadata/index.mts +2 -0
  64. package/src/services/__tests__/cli-parser.service.spec.mts +404 -0
  65. package/src/services/cli-parser.service.mts +231 -0
  66. package/src/services/index.mts +2 -0
  67. package/src/services/module-loader.service.mts +120 -0
  68. package/tsconfig.json +18 -0
  69. package/tsconfig.lib.json +8 -0
  70. package/tsconfig.spec.json +13 -0
  71. package/tsup.config.mts +12 -0
  72. package/vitest.config.mts +9 -0
@@ -0,0 +1,159 @@
1
+ import type { ClassTypeWithInstance, InjectionToken } from '@navios/di'
2
+
3
+ import { Container, inject, Injectable } from '@navios/di'
4
+
5
+ import type { CommandHandler, Module } from './interfaces/index.mjs'
6
+
7
+ import { CliParserService, ModuleLoaderService } from './services/index.mjs'
8
+
9
+ export interface CommanderApplicationOptions {}
10
+
11
+ @Injectable()
12
+ export class CommanderApplication {
13
+ private moduleLoader = inject(ModuleLoaderService)
14
+ private cliParser = inject(CliParserService)
15
+ protected container = inject(Container)
16
+
17
+ private appModule: ClassTypeWithInstance<Module> | null = null
18
+ private options: CommanderApplicationOptions = {}
19
+
20
+ isInitialized = false
21
+
22
+ async setup(
23
+ appModule: ClassTypeWithInstance<Module>,
24
+ options: CommanderApplicationOptions = {},
25
+ ) {
26
+ this.appModule = appModule
27
+ this.options = options
28
+ }
29
+
30
+ getContainer() {
31
+ return this.container
32
+ }
33
+
34
+ async init() {
35
+ if (!this.appModule) {
36
+ throw new Error(
37
+ '[Navios Commander] App module is not set. Call setup() first.',
38
+ )
39
+ }
40
+ await this.moduleLoader.loadModules(this.appModule)
41
+ this.isInitialized = true
42
+ }
43
+
44
+ async executeCommand(commandPath: string, options: any = {}) {
45
+ if (!this.isInitialized) {
46
+ throw new Error(
47
+ '[Navios Commander] Application is not initialized. Call init() first.',
48
+ )
49
+ }
50
+
51
+ // Use pre-collected command metadata from module loading
52
+ const commandWithMetadata = this.moduleLoader.getCommandByPath(commandPath)
53
+
54
+ if (!commandWithMetadata) {
55
+ throw new Error(`[Navios Commander] Command not found: ${commandPath}`)
56
+ }
57
+
58
+ const { class: commandClass, metadata } = commandWithMetadata
59
+
60
+ // Validate options with zod schema if provided
61
+ let validatedOptions = options
62
+ if (metadata.optionsSchema) {
63
+ validatedOptions = metadata.optionsSchema.parse(options)
64
+ }
65
+
66
+ // Get command instance and execute
67
+ const commandInstance = await this.container.get<CommandHandler>(
68
+ commandClass as unknown as InjectionToken<CommandHandler>,
69
+ )
70
+
71
+ if (!commandInstance.execute) {
72
+ throw new Error(
73
+ `[Navios Commander] Command ${commandPath} does not implement execute method`,
74
+ )
75
+ }
76
+
77
+ await commandInstance.execute(validatedOptions)
78
+ }
79
+
80
+ getAllCommands() {
81
+ // Use pre-collected command metadata from module loading
82
+ const commandsMap = this.moduleLoader.getAllCommandsWithMetadata()
83
+ const commandsWithMetadata: Array<{
84
+ path: string
85
+ class: ClassTypeWithInstance<any>
86
+ }> = []
87
+
88
+ for (const [, { class: cmd, metadata }] of commandsMap) {
89
+ commandsWithMetadata.push({
90
+ path: metadata.path,
91
+ class: cmd,
92
+ })
93
+ }
94
+
95
+ return commandsWithMetadata
96
+ }
97
+
98
+ /**
99
+ * Runs the CLI application by parsing process.argv and executing the command
100
+ * @param argv - Command-line arguments (defaults to process.argv)
101
+ */
102
+ async run(argv: string[] = process.argv) {
103
+ if (!this.isInitialized) {
104
+ throw new Error(
105
+ '[Navios Commander] Application is not initialized. Call init() first.',
106
+ )
107
+ }
108
+
109
+ try {
110
+ // First, try to extract the command path to get its schema
111
+ // We need to do a preliminary parse to find the command
112
+ const preliminaryParse = this.cliParser.parse(argv)
113
+ const commandWithMetadata = this.moduleLoader.getCommandByPath(
114
+ preliminaryParse.command,
115
+ )
116
+
117
+ // Re-parse with schema if available
118
+ const parsed = commandWithMetadata?.metadata.optionsSchema
119
+ ? this.cliParser.parse(argv, commandWithMetadata.metadata.optionsSchema)
120
+ : preliminaryParse
121
+
122
+ // Handle special commands
123
+ if (
124
+ parsed.command === 'help' ||
125
+ parsed.options.help ||
126
+ parsed.options.h
127
+ ) {
128
+ const commands = this.getAllCommands()
129
+ console.log(this.cliParser.formatCommandList(commands))
130
+ return
131
+ }
132
+
133
+ // Execute the command
134
+ await this.executeCommand(parsed.command, parsed.options)
135
+ } catch (error) {
136
+ if (error instanceof Error) {
137
+ console.error(`Error: ${error.message}`)
138
+
139
+ // Show available commands on error
140
+ if (error.message.includes('Command not found')) {
141
+ console.log(
142
+ '\n' + this.cliParser.formatCommandList(this.getAllCommands()),
143
+ )
144
+ }
145
+ }
146
+ throw error
147
+ }
148
+ }
149
+
150
+ async dispose() {
151
+ if (this.moduleLoader) {
152
+ this.moduleLoader.dispose()
153
+ }
154
+ }
155
+
156
+ async close() {
157
+ await this.dispose()
158
+ }
159
+ }
@@ -0,0 +1,20 @@
1
+ import type { ClassTypeWithInstance } from '@navios/di'
2
+
3
+ import { Container } from '@navios/di'
4
+
5
+ import type { CommanderApplicationOptions } from './commander.application.mjs'
6
+ import type { Module } from './interfaces/index.mjs'
7
+
8
+ import { CommanderApplication } from './commander.application.mjs'
9
+
10
+ export class CommanderFactory {
11
+ static async create(
12
+ appModule: ClassTypeWithInstance<Module>,
13
+ options: CommanderApplicationOptions = {},
14
+ ) {
15
+ const container = new Container()
16
+ const app = await container.get(CommanderApplication)
17
+ await app.setup(appModule, options)
18
+ return app
19
+ }
20
+ }
@@ -0,0 +1,39 @@
1
+ import type { ClassType } from '@navios/di'
2
+
3
+ import { Injectable, InjectableScope, InjectionToken } from '@navios/di'
4
+
5
+ import { getCliModuleMetadata } from '../metadata/index.mjs'
6
+
7
+ export interface CliModuleOptions {
8
+ commands?: ClassType[] | Set<ClassType>
9
+ imports?: ClassType[] | Set<ClassType>
10
+ }
11
+
12
+ export function CliModule(
13
+ { commands = [], imports = [] }: CliModuleOptions = {
14
+ commands: [],
15
+ imports: [],
16
+ },
17
+ ) {
18
+ return (target: ClassType, context: ClassDecoratorContext) => {
19
+ if (context.kind !== 'class') {
20
+ throw new Error(
21
+ '[Navios Commander] @CliModule decorator can only be used on classes.',
22
+ )
23
+ }
24
+ // Register the module in the service locator
25
+ const token = InjectionToken.create(target)
26
+ const moduleMetadata = getCliModuleMetadata(target, context)
27
+ for (const command of commands) {
28
+ moduleMetadata.commands.add(command)
29
+ }
30
+ for (const importedModule of imports) {
31
+ moduleMetadata.imports.add(importedModule)
32
+ }
33
+
34
+ return Injectable({
35
+ token,
36
+ scope: InjectableScope.Singleton,
37
+ })(target, context)
38
+ }
39
+ }
@@ -0,0 +1,29 @@
1
+ import type { ClassType } from '@navios/di'
2
+ import type { ZodObject } from 'zod'
3
+
4
+ import { Injectable, InjectableScope, InjectionToken } from '@navios/di'
5
+
6
+ import { getCommandMetadata } from '../metadata/index.mjs'
7
+
8
+ export interface CommandOptions {
9
+ path: string
10
+ optionsSchema?: ZodObject
11
+ }
12
+
13
+ export function Command({ path, optionsSchema }: CommandOptions) {
14
+ return function (target: ClassType, context: ClassDecoratorContext) {
15
+ if (context.kind !== 'class') {
16
+ throw new Error(
17
+ '[Navios Commander] @Command decorator can only be used on classes.',
18
+ )
19
+ }
20
+ const token = InjectionToken.create(target)
21
+ if (context.metadata) {
22
+ getCommandMetadata(target, context, path, optionsSchema)
23
+ }
24
+ return Injectable({
25
+ token,
26
+ scope: InjectableScope.Singleton,
27
+ })(target, context)
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ export * from './command.decorator.mjs'
2
+ export * from './cli-module.decorator.mjs'
package/src/index.mts ADDED
@@ -0,0 +1,7 @@
1
+ export * from '@navios/di'
2
+ export * from './commander.application.mjs'
3
+ export * from './commander.factory.mjs'
4
+ export * from './decorators/index.mjs'
5
+ export * from './interfaces/index.mjs'
6
+ export * from './metadata/index.mjs'
7
+ export * from './services/index.mjs'
@@ -0,0 +1,3 @@
1
+ export interface CommandHandler<TOptions = any> {
2
+ execute(options: TOptions): void | Promise<void>
3
+ }
@@ -0,0 +1,2 @@
1
+ export * from './module.interface.mjs'
2
+ export * from './command-handler.interface.mjs'
@@ -0,0 +1,4 @@
1
+ export interface Module {
2
+ onModuleInit?(): void | Promise<void>
3
+ onModuleDestroy?(): void | Promise<void>
4
+ }
@@ -0,0 +1,54 @@
1
+ import type { ClassType } from '@navios/di'
2
+
3
+ export const CliModuleMetadataKey = Symbol('CliModuleMetadataKey')
4
+
5
+ export interface CliModuleMetadata {
6
+ commands: Set<ClassType>
7
+ imports: Set<ClassType>
8
+ customAttributes: Map<string | symbol, any>
9
+ }
10
+
11
+ export function getCliModuleMetadata(
12
+ target: ClassType,
13
+ context: ClassDecoratorContext,
14
+ ): CliModuleMetadata {
15
+ if (context.metadata) {
16
+ const metadata = context.metadata[CliModuleMetadataKey] as
17
+ | CliModuleMetadata
18
+ | undefined
19
+ if (metadata) {
20
+ return metadata
21
+ } else {
22
+ const newMetadata: CliModuleMetadata = {
23
+ commands: new Set<ClassType>(),
24
+ imports: new Set<ClassType>(),
25
+ customAttributes: new Map<string | symbol, any>(),
26
+ }
27
+ context.metadata[CliModuleMetadataKey] = newMetadata
28
+ // @ts-expect-error We add a custom metadata key to the target
29
+ target[CliModuleMetadataKey] = newMetadata
30
+ return newMetadata
31
+ }
32
+ }
33
+ throw new Error('[Navios Commander] Wrong environment.')
34
+ }
35
+
36
+ export function extractCliModuleMetadata(
37
+ target: ClassType,
38
+ ): CliModuleMetadata {
39
+ // @ts-expect-error We add a custom metadata key to the target
40
+ const metadata = target[CliModuleMetadataKey] as
41
+ | CliModuleMetadata
42
+ | undefined
43
+ if (!metadata) {
44
+ throw new Error(
45
+ `[Navios Commander] Module metadata not found for ${target.name}. Make sure to use @CliModule decorator.`,
46
+ )
47
+ }
48
+ return metadata
49
+ }
50
+
51
+ export function hasCliModuleMetadata(target: ClassType): boolean {
52
+ // @ts-expect-error We add a custom metadata key to the target
53
+ return !!target[CliModuleMetadataKey]
54
+ }
@@ -0,0 +1,54 @@
1
+ import type { ClassType } from '@navios/di'
2
+ import type { ZodObject } from 'zod'
3
+
4
+ export const CommandMetadataKey = Symbol('CommandMetadataKey')
5
+
6
+ export interface CommandMetadata {
7
+ path: string
8
+ optionsSchema?: ZodObject
9
+ customAttributes: Map<string | symbol, any>
10
+ }
11
+
12
+ export function getCommandMetadata(
13
+ target: ClassType,
14
+ context: ClassDecoratorContext,
15
+ path: string,
16
+ optionsSchema?: ZodObject,
17
+ ): CommandMetadata {
18
+ if (context.metadata) {
19
+ const metadata = context.metadata[CommandMetadataKey] as
20
+ | CommandMetadata
21
+ | undefined
22
+ if (metadata) {
23
+ return metadata
24
+ } else {
25
+ const newMetadata: CommandMetadata = {
26
+ path,
27
+ optionsSchema,
28
+ customAttributes: new Map<string | symbol, any>(),
29
+ }
30
+ context.metadata[CommandMetadataKey] = newMetadata
31
+ // @ts-expect-error We add a custom metadata key to the target
32
+ target[CommandMetadataKey] = newMetadata
33
+ return newMetadata
34
+ }
35
+ }
36
+ throw new Error('[Navios Commander] Wrong environment.')
37
+ }
38
+
39
+ export function extractCommandMetadata(target: ClassType): CommandMetadata {
40
+ // @ts-expect-error We add a custom metadata key to the target
41
+ const metadata = target[CommandMetadataKey] as CommandMetadata | undefined
42
+ if (!metadata) {
43
+ throw new Error(
44
+ '[Navios Commander] Command metadata not found. Make sure to use @Command decorator.',
45
+ )
46
+ }
47
+ return metadata
48
+ }
49
+
50
+ export function hasCommandMetadata(target: ClassType): boolean {
51
+ // @ts-expect-error We add a custom metadata key to the target
52
+ const metadata = target[CommandMetadataKey] as CommandMetadata | undefined
53
+ return !!metadata
54
+ }
@@ -0,0 +1,2 @@
1
+ export * from './command.metadata.mjs'
2
+ export * from './cli-module.metadata.mjs'