{"id":48441897,"url":"https://github.com/tada5hi/orkos","last_synced_at":"2026-04-06T16:01:08.140Z","repository":{"id":347598660,"uuid":"1192945028","full_name":"tada5hi/orkos","owner":"tada5hi","description":"A lightweight modular application orchestrator for TypeScript with dependency-ordered startup, shutdown, and topological module resolution.","archived":false,"fork":false,"pushed_at":"2026-03-28T15:33:27.000Z","size":263,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-28T17:48:52.390Z","etag":null,"topics":["dependency-resolution","lifecycle-management","modular-architecture","module-loader","module-orchestrator","plugin-system","startup-shutdown"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tada5hi.png","metadata":{"files":{"readme":"README.MD","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["tada5hi"]}},"created_at":"2026-03-26T18:04:50.000Z","updated_at":"2026-03-28T15:32:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tada5hi/orkos","commit_stats":null,"previous_names":["tada5hi/orkos"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/tada5hi/orkos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tada5hi%2Forkos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tada5hi%2Forkos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tada5hi%2Forkos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tada5hi%2Forkos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tada5hi","download_url":"https://codeload.github.com/tada5hi/orkos/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tada5hi%2Forkos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31479006,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T14:34:32.243Z","status":"ssl_error","status_checked_at":"2026-04-06T14:34:31.723Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dependency-resolution","lifecycle-management","modular-architecture","module-loader","module-orchestrator","plugin-system","startup-shutdown"],"created_at":"2026-04-06T16:01:07.185Z","updated_at":"2026-04-06T16:01:08.124Z","avatar_url":"https://github.com/tada5hi.png","language":"TypeScript","funding_links":["https://github.com/sponsors/tada5hi"],"categories":[],"sub_categories":[],"readme":"# orkos 🎻\n\n[![npm version][npm-version-src]][npm-version-href]\n[![codecov][codecov-src]][codecov-href]\n[![Master Workflow][workflow-src]][workflow-href]\n[![Known Vulnerabilities][snyk-src]][snyk-href]\n[![Conventional Commits][conventional-src]][conventional-href]\n\nA lightweight modular application orchestrator for TypeScript.\n\nDefine modules with dependencies, and orkos sets them up in the right order and tears them down in reverse. Built on top of [eldin](https://github.com/tada5hi/eldin) for dependency injection.\n\n## Core Philosophy\n\nApplication startup is deceptively complex: services depend on each other, initialization order matters, and teardown must reverse that order reliably. orkos handles this with a single, explicit pattern — modules declare their dependencies, and a topological sort determines the rest. No implicit wiring, no decorator magic, no runtime surprises. The shared [eldin](https://github.com/tada5hi/eldin) container gives modules a clean way to exchange services without tight coupling.\n\n**Table of Contents**\n\n- [Core Philosophy](#core-philosophy)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Modules](#modules)\n- [defineModule](#definemodule)\n- [External Modules](#external-modules)\n- [Dependency Ordering](#dependency-ordering)\n- [Module Lifecycle](#module-lifecycle)\n- [Application API](#application-api)\n- [Error Handling](#error-handling)\n- [Usage with eldin](#usage-with-eldin)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Installation\n\n```bash\nnpm install orkos eldin --save\n```\n\n## Quick Start\n\n```typescript\nimport { Application } from 'orkos';\nimport type { IModule } from 'orkos';\nimport type { IContainer } from 'eldin';\n\nclass ConfigModule implements IModule {\n    readonly name = 'config';\n\n    async setup(container: IContainer): Promise\u003cvoid\u003e {\n        container.register(ConfigToken, { useValue: { port: 3000 } });\n    }\n}\n\nclass DatabaseModule implements IModule {\n    readonly name = 'database';\n    readonly dependencies = ['config'];\n\n    async setup(container: IContainer): Promise\u003cvoid\u003e {\n        const config = container.resolve(ConfigToken);\n        const db = new Database(config);\n        await db.connect();\n        container.register(DatabaseToken, { useValue: db });\n    }\n\n    async teardown(container: IContainer): Promise\u003cvoid\u003e {\n        const db = container.resolve(DatabaseToken);\n        await db.disconnect();\n    }\n}\n\nclass HttpModule implements IModule {\n    readonly name = 'http';\n    readonly dependencies = ['config', 'database'];\n\n    async setup(container: IContainer): Promise\u003cvoid\u003e {\n        const config = container.resolve(ConfigToken);\n        const server = createServer(config.port);\n        await server.listen();\n    }\n}\n\nconst app = new Application({\n    modules: [\n        new ConfigModule(),\n        new DatabaseModule(),\n        new HttpModule(),\n    ],\n});\n\n// Sets up in dependency order: config -\u003e database -\u003e http\nawait app.setup();\n\n// Tears down in reverse: http -\u003e database -\u003e config\nawait app.teardown();\n```\n\n## Modules\n\nA module is any object implementing the `IModule` interface:\n\n```typescript\ninterface IModule {\n    readonly name: string;\n    readonly version?: string;\n    readonly dependencies?: (string | ModuleDependency)[];\n\n    setup(container: IContainer): Promise\u003cvoid\u003e;\n    teardown?(container: IContainer): Promise\u003cvoid\u003e;\n\n    onReady?(container: IContainer): Promise\u003cvoid\u003e;\n    onError?(error: Error, container: IContainer): Promise\u003cvoid\u003e;\n}\n```\n\n| Property | Description |\n|----------|-------------|\n| `name` | Unique identifier for the module |\n| `version` | Optional semver version string |\n| `dependencies` | Array of module names or `ModuleDependency` objects |\n| `setup()` | Called during startup with the shared DI container |\n| `teardown()` | Optional cleanup, called during shutdown |\n| `onReady()` | Called after all modules have been set up successfully |\n| `onError()` | Called when this module's `setup()` throws an error |\n\nModules receive the application's shared [eldin](https://github.com/tada5hi/eldin) container. Use it to register and resolve dependencies across modules.\n\n### ModuleDependency\n\nDependencies can be plain strings (module names) or objects with additional constraints:\n\n```typescript\ninterface ModuleDependency {\n    name: string;         // module name to depend on\n    version?: string;     // semver range (e.g. '\u003e=2.0.0', '^1.3.0')\n    optional?: boolean;   // skip silently if not registered or resolvable\n    package?: string;     // npm package name, if different from the module name\n}\n```\n\nExamples:\n\n```typescript\nconst dependencies = [\n    'config',                                          // simple: depend on module named 'config'\n    { name: 'database', version: '\u003e=2.0.0' },         // require database v2+\n    { name: 'cache', optional: true },                 // skip if cache isn't available\n    { name: 'redis', package: '@myorg/orkos-redis' },  // resolve from a different package name\n];\n```\n\nThe `package` field is used during [external module resolution](#external-modules) — it tells orkos which npm package to `import()` when the module isn't already registered. Without it, orkos uses the `name` as both the module name and the package name.\n\n## defineModule\n\nThe `defineModule` helper provides a convenient way to create configurable modules with typed options and defaults.\n\n### Inline Definition\n\n```typescript\nimport { defineModule } from 'orkos';\n\nconst CacheModule = defineModule\u003c{ driver: 'memory' | 'redis'; ttl: number }\u003e({\n    name: 'cache',\n    dependencies: ['config'],\n    defaults: { driver: 'memory', ttl: 3600 },\n\n    async setup(options, container) {\n        // options is { driver, ttl } with defaults merged\n        container.register(CacheToken, {\n            useFactory: () =\u003e createCache(options),\n        });\n    },\n\n    async teardown(options, container) {\n        const cache = container.resolve(CacheToken);\n        await cache.close();\n    },\n});\n\nconst app = new Application({\n    modules: [\n        CacheModule(),                           // use defaults\n        CacheModule({ driver: 'redis' }),        // override driver, keep ttl default\n        CacheModule(false),                      // disable (no-op)\n    ],\n});\n```\n\n### Factory Definition\n\nWrap a class-based `IModule` implementation with options:\n\n```typescript\nimport { defineModule } from 'orkos';\n\nclass CacheModule implements IModule {\n    readonly name = 'cache';\n\n    constructor(private options: { driver: string; ttl: number }) {}\n\n    async setup(container: IContainer) {\n        // use this.options\n    }\n}\n\nconst createCacheModule = defineModule\u003c{ driver: string; ttl: number }\u003e({\n    defaults: { driver: 'memory', ttl: 3600 },\n    factory: (options) =\u003e new CacheModule(options),\n});\n\nconst app = new Application({\n    modules: [\n        createCacheModule(),\n        createCacheModule({ driver: 'redis' }),\n    ],\n});\n```\n\n## External Modules\n\nModules can be referenced by npm package name and resolved automatically via dynamic `import()`. This is useful for sharing modules across projects as npm packages.\n\n### Referencing External Modules\n\nPass a string (package name) or a `[string, options]` tuple anywhere you'd pass an `IModule`:\n\n```typescript\nconst app = new Application({\n    modules: [\n        new ConfigModule(),                          // internal module (IModule)\n        'orkos-redis',                               // external: resolve from node_modules\n        ['orkos-redis', { host: '10.0.0.1' }],      // external with options\n    ],\n});\n\n// Or via addModule:\napp.addModule('orkos-redis');\napp.addModule(['orkos-redis', { host: '10.0.0.1' }]);\n```\n\nExternal modules are resolved lazily — the `import()` call happens during `setup()`, not at registration time. This keeps `addModule()` synchronous.\n\n### Package Convention\n\nAn orkos-compatible npm package must export a **default export** that is either a `ModuleFactory` (function) or an `IModule` (object).\n\n**Factory export** (recommended — supports options):\n\n```typescript\n// orkos-redis/src/index.ts\nimport { defineModule } from 'orkos';\n\nexport default defineModule\u003c{ host: string; port: number }\u003e({\n    name: 'orkos-redis',\n    defaults: { host: 'localhost', port: 6379 },\n    async setup(options, container) {\n        const client = createRedisClient(options);\n        container.register(RedisToken, { useValue: client });\n    },\n    async teardown(options, container) {\n        const client = container.resolve(RedisToken);\n        await client.disconnect();\n    },\n});\n```\n\n**Plain IModule export** (no options support):\n\n```typescript\n// orkos-metrics/src/index.ts\nimport type { IModule } from 'orkos';\n\nconst metricsModule: IModule = {\n    name: 'orkos-metrics',\n    async setup(container) {\n        // initialize metrics collection\n    },\n};\n\nexport default metricsModule;\n```\n\nWhen orkos imports a package:\n\n- **Function** → calls it as a `ModuleFactory` with provided options (or no args)\n- **Object with `name` and `setup`** → uses it directly as an `IModule`\n- **Anything else** → throws `INVALID_MODULE_EXPORT`\n\n\u003e **Name matching:** The module's `name` must match the package name used to reference it. If you add `'orkos-redis'`, the exported module must have `name: 'orkos-redis'`. A mismatch throws `INVALID_MODULE_EXPORT`. To use a different module name, declare the mapping via the [`package` field](#moduledependency) in a `ModuleDependency`.\n\n### End-to-End Example\n\n**Package author** publishes `orkos-redis`:\n\n```typescript\n// orkos-redis/src/index.ts\nimport { defineModule } from 'orkos';\n\nexport default defineModule\u003c{ url: string }\u003e({\n    name: 'orkos-redis',\n    defaults: { url: 'redis://localhost:6379' },\n    async setup(options, container) {\n        const client = new RedisClient(options.url);\n        await client.connect();\n        container.register(RedisToken, { useValue: client });\n    },\n    async teardown(options, container) {\n        const client = container.resolve(RedisToken);\n        await client.disconnect();\n    },\n});\n```\n\n**Consumer** uses it in their application:\n\n```typescript\nimport { Application } from 'orkos';\n\nconst app = new Application({\n    modules: [\n        new ConfigModule(),\n        ['orkos-redis', { url: 'redis://prod:6379' }],  // resolved via import('orkos-redis')\n        new AuthModule(),                                 // can depend on 'orkos-redis'\n    ],\n});\n\nawait app.setup();\n```\n\n### Dependency Resolution Flow\n\nWhen `setup()` is called, orkos resolves modules in this order:\n\n1. **Resolve explicit externals** — strings and tuples passed to `addModule` or the constructor\n2. **Scan dependencies** — check each resolved module's `dependencies` for unregistered names\n3. **Auto-resolve missing deps** — attempt `import(name)` (or `import(package)` if the `package` field is set)\n4. **Repeat recursively** — newly resolved modules may have their own unresolved dependencies\n5. **Topological sort** — once all modules are resolved, determine setup order\n6. **Throw on failure** — if a non-optional dependency can't be found after auto-resolution, throw `MODULE_NOT_FOUND`\n\nOptional dependencies (`optional: true`) are silently skipped if they can't be resolved.\n\nThe recursive resolution has a configurable depth limit (`maxResolveDepth`, default 10) to prevent runaway chains.\n\n### Auto-Install\n\nBy default, missing packages throw an error with a helpful install command:\n\n```\nApplicationError: Module \"orkos-redis\" could not be resolved. Run: npm install orkos-redis\n```\n\nEnable `autoInstall` to install them automatically via [@antfu/install-pkg](https://github.com/antfu-collective/install-pkg):\n\n```typescript\nconst app = new Application({\n    autoInstall: true,   // attempts npm install before throwing\n    modules: ['orkos-redis'],\n});\n```\n\n### Re-Resolution\n\nExternal modules are cached after first resolution. Subsequent `setup()` calls reuse the cached modules. To force a fresh `import()` of all previously resolved externals (e.g. during development):\n\n```typescript\nawait app.setup({ resolveCache: false });\n```\n\nThis removes all previously resolved external modules and re-imports them.\n\n## Dependency Ordering\n\norkos resolves module setup order using topological sort (Kahn's algorithm):\n\n```typescript\nconst app = new Application({\n    modules: [\n        new HttpModule(),      // dependencies: ['config', 'database']\n        new ConfigModule(),    // no dependencies\n        new DatabaseModule(),  // dependencies: ['config']\n    ],\n});\n\n// Registration order doesn't matter — orkos resolves:\n// 1. config (no deps)\n// 2. database (depends on config)\n// 3. http (depends on config + database)\nawait app.setup();\n```\n\n### Circular Dependencies\n\nCircular dependencies are detected and throw an `ApplicationError` with code `CIRCULAR_DEPENDENCY`:\n\n```typescript\n// Module A depends on B, B depends on A\n// → ApplicationError: Circular module dependency detected involving: A, B\n```\n\n### Missing Dependencies\n\nWhen a module declares a dependency that isn't registered, orkos first attempts [auto-resolution](#dependency-resolution-flow) from `node_modules`. If the dependency still can't be found:\n\n- **Non-optional** — `setup()` throws an `ApplicationError` with code `MODULE_NOT_FOUND`\n- **Optional** (`optional: true`) — the dependency is silently skipped\n\n## Module Lifecycle\n\n### Versioning\n\nModules can declare a `version` and dependents can enforce semver constraints via [`ModuleDependency`](#moduledependency):\n\n```typescript\nclass DatabaseModule implements IModule {\n    readonly name = 'database';\n    readonly version = '2.3.0';\n    // ...\n}\n\nclass AuthModule implements IModule {\n    readonly name = 'auth';\n    readonly dependencies = [\n        { name: 'database', version: '\u003e=2.0.0' },  // requires database v2+\n    ];\n    // ...\n}\n```\n\nSupported ranges: `\u003e=`, `\u003e`, `\u003c=`, `\u003c`, `~`, `^`, and exact match. If a constraint is not satisfied, `setup()` throws an `ApplicationError` with code `VERSION_MISMATCH`.\n\n### Hooks\n\n**`onReady(container)`** — Called after **all** modules have been set up successfully, in dependency order. Use it for tasks that require the full application to be initialized (e.g. starting background jobs).\n\n**`onError(error, container)`** — Called when this module's `setup()` throws. The original error is re-thrown after the hook runs. On failure, orkos automatically tears down any modules that were already set up successfully, in reverse order.\n\n### Status Tracking\n\nEach module moves through a lifecycle: `Pending → SettingUp → Ready` (or `Failed`), and `TearingDown → TornDown`. Query state via `getModuleStatus(name)` or `getStatus()` for the full map.\n\n## Application API\n\n```typescript\nimport { Application } from 'orkos';\nimport type { IApplication, ApplicationContext, SetupOptions } from 'orkos';\n\nconst app: IApplication = new Application({\n    modules: [...],\n    container: myContainer,    // optional: pre-configured eldin container\n    autoInstall: false,        // optional: auto-install missing packages\n    maxResolveDepth: 10,       // optional: recursive resolution depth limit\n});\n```\n\n| Method | Description |\n|--------|-------------|\n| `addModule(module)` | Register a module (`IModule`, string, or `[string, options]` tuple) |\n| `addModules(modules)` | Register multiple modules at once |\n| `setup(options?)` | Resolve externals, sort dependencies, and set up all modules |\n| `teardown()` | Tear down all modules in reverse setup order |\n| `getModuleStatus(name)` | Get the current status of a module |\n| `getStatus()` | Get a map of all module statuses |\n| `container` | The shared [eldin](https://github.com/tada5hi/eldin) `IContainer` instance |\n\n`setup()` accepts an optional `SetupOptions` object:\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `resolveCache` | `boolean` | `true` | When `false`, re-imports all previously resolved external modules |\n\n### Dynamic Module Registration\n\n```typescript\nconst app = new Application();\n\napp.addModule(new ConfigModule());\napp.addModule('orkos-redis');\napp.addModule(['orkos-cache', { ttl: 60 }]);\napp.addModules([new HttpModule(), new CacheModule()]);\n\nawait app.setup();\n```\n\n## Error Handling\n\norkos uses structured error codes via [ebec](https://github.com/tada5hi/ebec). All errors are instances of `ApplicationError` (extends `BaseError`) with a `code` property for programmatic handling:\n\n```typescript\nimport { ApplicationError, ApplicationErrorCode } from 'orkos';\nimport { isBaseError } from 'ebec';\n\ntry {\n    await app.setup();\n} catch (error) {\n    if (isBaseError(error) \u0026\u0026 error.code === ApplicationErrorCode.VERSION_MISMATCH) {\n        // handle version mismatch\n    }\n}\n```\n\n| Code | When |\n|------|------|\n| `CIRCULAR_DEPENDENCY` | Circular module dependency detected |\n| `MODULE_NOT_FOUND` | Required module/package not registered or resolvable |\n| `INVALID_MODULE_EXPORT` | Package default export is not a valid `ModuleFactory` or `IModule` |\n| `MODULE_INSTALL_FAILED` | Auto-install of a package failed |\n| `OPTIONS_NOT_SUPPORTED` | Options passed to a package that exports `IModule` (not a factory) |\n| `RESOLUTION_DEPTH_EXCEEDED` | Recursive resolution exceeded `maxResolveDepth` |\n| `VERSION_MISMATCH` | Dependency version constraint not satisfied |\n| `MODULE_NOT_REGISTERED` | `getModuleStatus()` called with unknown module name |\n\n## Usage with eldin\n\norkos uses [eldin](https://github.com/tada5hi/eldin) for dependency injection. Define typed tokens with eldin and use them across modules:\n\n```typescript\nimport { TypedToken } from 'eldin';\nimport type { IContainer } from 'eldin';\nimport { Application } from 'orkos';\nimport type { IModule } from 'orkos';\n\nconst ConfigToken = new TypedToken\u003cConfig\u003e('Config');\nconst DatabaseToken = new TypedToken\u003cDatabase\u003e('Database');\n\nclass ConfigModule implements IModule {\n    readonly name = 'config';\n\n    async setup(container: IContainer) {\n        container.register(ConfigToken, {\n            useValue: { host: 'localhost', port: 5432 },\n        });\n    }\n}\n\nclass DatabaseModule implements IModule {\n    readonly name = 'database';\n    readonly dependencies = ['config'];\n\n    async setup(container: IContainer) {\n        // Type-safe resolution — no manual generics needed\n        const config = container.resolve(ConfigToken); // Config\n        container.register(DatabaseToken, {\n            useFactory: () =\u003e new Database(config),\n        });\n    }\n}\n\nconst app = new Application({\n    modules: [\n        new ConfigModule(),\n        new DatabaseModule(),\n    ],\n});\n\nawait app.setup();\n\n// Access the container directly\nconst db = app.container.resolve(DatabaseToken); // Database\n```\n\n## Contributing\n\nBefore starting to work on a pull request, it is important to review the guidelines for\n[contributing](./CONTRIBUTING.md) and the [code of conduct](./CODE_OF_CONDUCT.md).\nThese guidelines will help to ensure that contributions are made effectively and are accepted.\n\n## License\n\nMade with 💚\n\nPublished under [MIT License](./LICENSE).\n\n[npm-version-src]: https://badge.fury.io/js/orkos.svg\n[npm-version-href]: https://npmjs.com/package/orkos\n[codecov-src]: https://codecov.io/gh/tada5hi/orkos/branch/master/graph/badge.svg?token=4KNSG8L13V\n[codecov-href]: https://codecov.io/gh/tada5hi/orkos\n[workflow-src]: https://github.com/tada5hi/orkos/workflows/CI/badge.svg\n[workflow-href]: https://github.com/tada5hi/orkos\n[snyk-src]: https://snyk.io/test/github/tada5hi/orkos/badge.svg?targetFile=package.json\n[snyk-href]: https://snyk.io/test/github/tada5hi/orkos?targetFile=package.json\n[conventional-src]: https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits\u0026logoColor=white\n[conventional-href]: https://conventionalcommits.org\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftada5hi%2Forkos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftada5hi%2Forkos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftada5hi%2Forkos/lists"}