@ooneex/cli 1.38.2 → 1.40.1

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.
package/dist/index.js CHANGED
@@ -6042,18 +6042,18 @@ var askName = async (config) => {
6042
6042
 
6043
6043
  // src/templates/app/app-database.txt
6044
6044
  var app_database_default = `import { SharedModule } from "@module/shared/SharedModule";
6045
- import { AppEnv } from "@ooneex/app-env";
6046
- import { inject } from "@ooneex/container";
6047
6045
  import { DatabaseException, decorator, TypeormDatabase } from "@ooneex/database";
6046
+ import { inject } from "@ooneex/container";
6047
+ import { AppEnv } from "@ooneex/app-env";
6048
6048
  import { DataSource } from "typeorm";
6049
6049
 
6050
6050
  @decorator.database()
6051
6051
  export class SharedDatabase extends TypeormDatabase {
6052
- constructor(@inject(AppEnv) private readonly env: AppEnv) {
6052
+ public constructor(@inject(AppEnv) private readonly env: AppEnv) {
6053
6053
  super();
6054
6054
  }
6055
6055
 
6056
- public getSource(): DataSource {
6056
+ public getSource(_database?: string): DataSource {
6057
6057
  if (this.source) {
6058
6058
  return this.source;
6059
6059
  }
@@ -6210,6 +6210,27 @@ const app = new App({
6210
6210
  await app.run();
6211
6211
  `;
6212
6212
 
6213
+ // src/templates/app/package.json.txt
6214
+ var package_json_default = `{
6215
+ "name": "{{NAME}}",
6216
+ "scripts": {
6217
+ "fmt": "bunx biome check --write",
6218
+ "lint": "bunx nx run-many -t lint --output-style=stream --verbose",
6219
+ "test": "bunx nx run-many -t test --output-style=stream --verbose",
6220
+ "check": "bun install && bun run build && bun run lint && bun run test",
6221
+ "commit": "bunx commit"
6222
+ },
6223
+ "workspaces": [
6224
+ "modules/*"
6225
+ ],
6226
+ "lint-staged": {
6227
+ "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
6228
+ "biome check --files-ignore-unknown=true"
6229
+ ]
6230
+ }
6231
+ }
6232
+ `;
6233
+
6213
6234
  // src/templates/bitbucket/pipelines.yml.txt
6214
6235
  var pipelines_yml_default = `# Bitbucket Pipelines
6215
6236
  # Place at bitbucket-pipelines.yml in the repository root.
@@ -10341,26 +10362,6 @@ var nx_json_default = `{
10341
10362
  }
10342
10363
  `;
10343
10364
 
10344
- // src/templates/app/package.json.txt
10345
- var package_json_default = `{
10346
- "name": "{{NAME}}",
10347
- "scripts": {
10348
- "fmt": "bunx biome check --write",
10349
- "lint": "bunx nx run-many -t lint --output-style=stream --verbose",
10350
- "test": "bunx nx run-many -t test --output-style=stream --verbose",
10351
- "commit": "bunx commit"
10352
- },
10353
- "workspaces": [
10354
- "modules/*"
10355
- ],
10356
- "lint-staged": {
10357
- "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
10358
- "biome check --files-ignore-unknown=true"
10359
- ]
10360
- }
10361
- }
10362
- `;
10363
-
10364
10365
  // src/templates/app/README.md.txt
10365
10366
  var README_md_default = "# {{NAME}}\n\nA modular, enterprise-grade backend framework built with TypeScript and Bun, powered by the **@ooneex** ecosystem.\n\n## Prerequisites\n\n- [Bun](https://bun.sh) (latest)\n- [Docker](https://www.docker.com/) & Docker Compose\n\n## Getting Started\n\n### Install Dependencies\n\n```bash\nbun install\n```\n\n### Environment Configuration\n\nThe environment file is created automatically at `modules/app/.env.yml`. Edit it to fill in the required values:\n\n| Variable | Description | Required |\n|---|---|---|\n| `app.env` | Application environment (`local`, `development`, `staging`, `testing`, `production`, \u2026) | Yes |\n| `app.port` | Server port number (default: `3000`) | Yes |\n| `app.host` | Server hostname (default: `0.0.0.0`) | Yes |\n| `logs.database_url` | Database URL for storing application logs | No |\n| `logs.betterstack.source_token` | Logtail source token for Better Stack logging | No |\n| `logs.betterstack.ingesting_host` | Better Stack log ingestion host URL | No |\n| `exception.betterstack.application_token` | Better Stack token for exception tracking | No |\n| `exception.betterstack.ingesting_host` | Better Stack exception ingestion host URL | No |\n| `analytics.posthog.project_token` | PostHog project token for event tracking | No |\n| `analytics.posthog.host` | PostHog ingest host (default: `https://eu.i.posthog.com`) | No |\n| `cache.redis.url` | Redis connection URL for caching | No |\n| `cache.upstash.rest_url` | Upstash Redis REST URL for serverless caching | No |\n| `cache.upstash.rest_token` | Upstash Redis REST token for serverless caching | No |\n| `pubsub.redis.url` | Redis connection URL for pub/sub messaging | No |\n| `rate_limit.redis.url` | Redis connection URL for rate limiting | No |\n| `rate_limit.upstash.url` | Upstash Redis URL for serverless rate limiting | No |\n| `rate_limit.upstash.token` | Upstash Redis token for serverless rate limiting | No |\n| `cors.origins` | Comma-separated allowed origins or `*` (default: `*`) | No |\n| `cors.methods` | Comma-separated HTTP methods | No |\n| `cors.headers` | Comma-separated allowed request headers | No |\n| `cors.exposed_headers` | Comma-separated headers exposed to the browser | No |\n| `cors.credentials` | Allow credentials: `true` or `false` | No |\n| `cors.max_age` | Preflight response cache duration in seconds | No |\n| `database.url` | PostgreSQL connection URL | No |\n| `database.redis.url` | Redis connection URL for Redis-based database operations | No |\n| `database.sqlite.path` | SQLite database file path | No |\n| `storage.cloudflare.access_key` | Cloudflare R2 access key ID | No |\n| `storage.cloudflare.secret_key` | Cloudflare R2 secret access key | No |\n| `storage.cloudflare.endpoint` | Cloudflare R2 endpoint URL | No |\n| `storage.cloudflare.region` | Cloudflare R2 region (default: `EEUR`) | No |\n| `storage.bunny.access_key` | BunnyCDN access key | No |\n| `storage.bunny.storage_zone` | BunnyCDN storage zone name | No |\n| `storage.bunny.region` | BunnyCDN region (default: `de`) | No |\n| `storage.filesystem.path` | Base directory path for local file storage | No |\n| `mailer.sender.name` | Display name for email sender | No |\n| `mailer.sender.address` | Email address for sender | No |\n| `mailer.resend.api_key` | Resend email service API key | No |\n| `jwt.secret` | Secret key for HS256 JWT signing and verification | No |\n| `ai.openrouter.api_key` | OpenRouter API key for accessing 300+ AI models | No |\n| `ai.openai.api_key` | OpenAI API key | No |\n| `ai.anthropic.api_key` | Anthropic API key for Claude models | No |\n| `ai.gemini.api_key` | Google Gemini API key | No |\n| `ai.groq.api_key` | Groq API key for LLM inference | No |\n| `ai.ollama.host` | Ollama host URL for local LLM inference | No |\n| `payment.polar.access_token` | Polar payment platform access token | No |\n| `payment.polar.environment` | Polar environment (`sandbox` or `production`) | No |\n| `authentication.auth_token` | Shared secret token for service-to-service authentication | No |\n| `authentication.clerk.secret_key` | Clerk authentication secret key | No |\n| `linear.api_key` | Linear API key for issue sync (`issue:pull`, `issue:push`) | No |\n| `linear.team_id` | Linear team ID \u2014 skips team selection prompt in `issue:push` | No |\n| `allowed_users.development` | Comma-separated emails allowed in the `development` environment | No |\n| `allowed_users.staging` | Comma-separated emails allowed in the `staging` environment | No |\n| `allowed_users.testing` | Comma-separated emails allowed in the `testing` environment | No |\n| `allowed_users.test` | Comma-separated emails allowed in the `test` environment | No |\n| `allowed_users.qa` | Comma-separated emails allowed in the `qa` environment | No |\n| `allowed_users.uat` | Comma-separated emails allowed in the `uat` environment | No |\n| `allowed_users.integration` | Comma-separated emails allowed in the `integration` environment | No |\n| `allowed_users.preview` | Comma-separated emails allowed in the `preview` environment | No |\n| `allowed_users.demo` | Comma-separated emails allowed in the `demo` environment | No |\n| `allowed_users.sandbox` | Comma-separated emails allowed in the `sandbox` environment | No |\n| `allowed_users.beta` | Comma-separated emails allowed in the `beta` environment | No |\n| `allowed_users.canary` | Comma-separated emails allowed in the `canary` environment | No |\n| `allowed_users.hotfix` | Comma-separated emails allowed in the `hotfix` environment | No |\n| `allowed_users.system` | Comma-separated system user emails | No |\n| `allowed_users.super_admin` | Comma-separated super-admin user emails | No |\n| `allowed_users.admin` | Comma-separated admin user emails | No |\n\n### Start the App\n\nStarts Docker services (if a `docker-compose.yml` is present) and launches the application with hot reload:\n\n```bash\noo app:start\n```\n\n### Stop the App\n\nStops all Docker services:\n\n```bash\noo app:stop\n```\n\n### Build the App\n\nCompiles the application for production. Output is written to the `dist` directory:\n\n```bash\noo app:build\n```\n\n---\n\n## Modules\n\nModules are the core organizational unit. Each module lives under `modules/<name>/` and contains its own controllers, services, repositories, entities, migrations, and seeds.\n\n### Why Modules?\n\nA flat codebase quickly becomes hard to navigate and reason about as an application grows. Modules address this by enforcing **vertical slicing**: all code related to a single business domain (e.g. `user`, `order`, `billing`) lives together rather than being scattered across generic `controllers/`, `services/`, and `repositories/` folders.\n\n**Benefits:**\n\n- **Domain isolation** \u2014 changes to one feature cannot accidentally break another; each module is a self-contained unit with explicit boundaries.\n- **Parallel development** \u2014 teams can work on separate modules without stepping on each other's code or creating merge conflicts in shared directories.\n- **Independent deployment** \u2014 a module can be extracted into a standalone service later with minimal refactoring because its code is already co-located.\n- **Simpler onboarding** \u2014 a new developer understands the scope of a feature by exploring a single directory instead of tracing imports across the whole project.\n- **Scoped migrations & seeds** \u2014 database changes stay close to the entities they affect, making rollbacks and reviews straightforward.\n\n**Good practices:**\n\n- Name modules after business domains (`user`, `product`, `payment`), not technical layers (`controllers`, `helpers`).\n- Keep modules small and focused. If a module grows beyond ~10 controllers or services, consider splitting it by sub-domain.\n- Never import from another module's internal files directly \u2014 expose only what is needed through the module's public index.\n- Register all entities in `SharedModule` (done automatically by `oo make:entity`) so the ORM has a single source of truth.\n- Run `oo module:lock` after every migration or seed change to protect file integrity in CI.\n- Prefix route names with the module name (e.g. `api.user.create`) to avoid collisions across modules.\n\n### Create a Module\n\nScaffolds a new module with all required structure, registers it in `AppModule`, adds its entities to `SharedModule`, and updates `tsconfig.json` path aliases and commitlint config:\n\n```bash\noo module:create\noo module:create --name user\n```\n\n### Lock a Module\n\nHashes migration and seed files into a module manifest (`.yml`) for integrity verification. Run this after adding or modifying migrations/seeds:\n\n```bash\noo module:lock --module user\noo module:lock --module user --override # force update existing hashes\n```\n\n### Delete a Module\n\nRemoves a module and cleans up all references from `AppModule`, `SharedModule`, `tsconfig.json`, and commitlint config. The `app` and `shared` core modules cannot be removed:\n\n```bash\noo module:remove\noo module:remove --name user\noo module:remove --name user --silent # skip confirmation prompt\n```\n\n---\n\n## Migrations\n\nMigrations are versioned, incremental SQL scripts that evolve your database schema over time. Each migration lives in `modules/<name>/src/migrations/` and is executed in order, making every schema change reproducible, reviewable, and reversible.\n\n### Why Migrations?\n\nManually altering a database in production is error-prone and leaves no audit trail. Migrations solve this by treating schema changes as code: they are committed to git, reviewed in pull requests, and applied the same way in every environment \u2014 local, staging, and production.\n\n**Benefits:**\n\n- **Reproducibility** \u2014 any developer can recreate the exact database state by running `oo migration:up` from scratch.\n- **History & auditability** \u2014 every schema change is tied to a commit, a date, and an author, making it easy to understand when and why a column or table was added.\n- **Safe deployments** \u2014 CI runs migrations before the application boots, catching schema errors before they reach production.\n- **Rollback capability** \u2014 because changes are incremental and versioned, you can reason about what to undo if a deployment goes wrong.\n- **Team coordination** \u2014 concurrent feature branches each generate their own migration file; the timestamp ordering ensures they are applied in the correct sequence when merged.\n\n**Good practices:**\n\n- One migration per logical change (e.g. add a table, add a column, add an index). Avoid bundling unrelated schema changes in a single file.\n- Never edit a migration that has already been applied in any shared environment \u2014 create a new migration to amend it instead.\n- Always write both `up` and `down` logic so migrations can be rolled back cleanly.\n- Keep migrations idempotent where possible (e.g. `CREATE TABLE IF NOT EXISTS`) to prevent failures on re-runs.\n- Run `oo module:lock` after every new migration to hash the file into the module manifest and enable integrity checks in CI.\n- Test migrations against a real database in CI \u2014 schema errors are invisible to TypeScript's type checker.\n- Avoid putting business logic or data transformations in migrations; keep them strictly structural (DDL) or use a dedicated seed for data.\n\n### Create a Migration\n\nGenerates a timestamped migration file in `modules/<name>/src/migrations/` along with a test file and a `bin/migration/up.ts` runner (if not already present):\n\n```bash\noo migration:create --module user\n```\n\n### Run Migrations\n\nExecutes migrations across all modules sequentially:\n\n```bash\noo migration:up\noo migration:up --drop # drop database before migrating\n```\n\n---\n\n## Seeds\n\nSeeds populate your database with initial or reference data \u2014 default roles, configuration values, demo content, or test fixtures. Each seed lives in `modules/<name>/src/seeds/` as a YAML file and is executed in order via the seed runner.\n\n### Why Seeds?\n\nHardcoding initial data in migration files mixes structural changes with data concerns, making both harder to maintain. Seeds keep data separate from schema, so they can be re-run independently, updated without touching migrations, and disabled in environments where they are not needed.\n\n**Benefits:**\n\n- **Consistent starting state** \u2014 every developer and every environment boots with the same baseline data, eliminating \"works on my machine\" data discrepancies.\n- **Readable data definitions** \u2014 YAML files are easy to read, diff, and review in pull requests without understanding SQL or TypeScript.\n- **Repeatable setup** \u2014 onboarding a new developer or spinning up a fresh staging environment requires only `oo seed:run` instead of manual database operations.\n- **Separation of concerns** \u2014 schema structure (migrations) and data content (seeds) evolve independently, reducing noise in both histories.\n- **Safe resets** \u2014 `oo seed:run --drop` wipes and repopulates data without touching the schema, useful for resetting demo or test environments.\n\n**Good practices:**\n\n- Use seeds for **reference data** (roles, permissions, countries, currencies) and **development fixtures** (demo users, sample products). Avoid using them for production user data.\n- Keep each seed file focused on a single entity or dataset \u2014 one file per table is a reasonable default.\n- Write seeds so they are idempotent: inserting with `ON CONFLICT DO NOTHING` or checking for existence before inserting prevents errors on re-runs.\n- Never depend on data inserted by another module's seed unless you control the execution order explicitly.\n- Run `oo module:lock` after adding or modifying a seed to update the module manifest and protect file integrity in CI.\n- Mirror your migration order: seed data for a table only after the migration that creates it has been applied.\n- Avoid generating seeds with random or time-dependent values \u2014 deterministic data makes debugging and comparison across environments reliable.\n\n### Create a Seed\n\nGenerates a YAML seed file in `modules/<name>/src/seeds/` along with a test file and a `bin/seed/run.ts` runner (if not already present):\n\n```bash\noo seed:create --module user\noo seed:create --name initial-roles --module user\n```\n\n### Run Seeds\n\nExecutes seeds across all modules sequentially:\n\n```bash\noo seed:run\noo seed:run --drop # drop data before seeding\n```\n\n---\n\n## Issues\n\nIssues bridge your Linear project management board and your local codebase. Each issue is represented as a YAML file under `modules/<name>/issues/` and serves as the single source of truth for what is being built, why, and what \"done\" looks like.\n\n**Requirements:** `linear.api_key` must be set in the environment for commands that talk to Linear (`issue:pull`, `issue:push`). Add it to `modules/app/.env.yml`:\n\n```yaml\nlinear:\n api_key: lin_api_xxxxxxxxxx\n team_id: your_team_id # optional \u2014 skips the team selection prompt in issue:push\n```\n\n### Issue YAML format\n\nEvery issue file follows a consistent schema:\n\n```yaml\nid: \"OON-123\"\ntitle: \"Add password reset flow\"\nstate: \"In Progress\"\npriority: \"High\"\ndescription: |\n **Context**: Users currently have no way to recover a forgotten password.\n\n **Goal**: Implement a secure email-based password reset flow.\n\n **Acceptance Criteria**:\n - [ ] User can request a reset link via their email address\n - [ ] Link expires after 1 hour\n - [ ] Used links are invalidated immediately\n\n **Technical Notes**: Use JWT with short TTL; store a one-time token hash in the database.\nlabels:\n - \"authentication\"\n - \"security\"\ncomments:\n - author: \"Alice\"\n message: \"Make sure the token is hashed before storage, not stored in plain text.\"\n```\n\n---\n\n### Create an Issue\n\n`issue:create` scaffolds a new issue YAML locally with a generated `[A-F]{3}-[0-9]{6}` placeholder identifier. No Linear connection is required \u2014 use this when you want to define and refine an issue offline before pushing it to Linear.\n\nBy default all fields are empty; pass `--interactive` to be prompted for each one:\n\n```bash\noo issue:create # Empty skeleton in shared module\noo issue:create --title \"Add password reset\" --module user # Pre-fill title, save to user module\noo issue:create --interactive # Prompt for title, state, priority, labels, description\noo issue:create --interactive --module user # Interactive + save into modules/user/issues/\n```\n\nAfter writing the file the command optionally **improves the description with Claude** \u2014 rewrites a raw description into the structured *Context / Goal / Acceptance Criteria / Technical Notes* format and suggests labels derived from the description.\n\n---\n\n### Pull an Issue\n\n`issue:pull` fetches an existing Linear issue by its identifier and saves it as `modules/<name>/issues/<identifier>.yml`. Use this to sync a ticket into your local workspace before starting implementation.\n\n```bash\noo issue:pull # Prompt for issue ID\noo issue:pull --id OON-123 # Pull a specific issue\noo issue:pull --id OON-123 --module user # Save into modules/user/issues/\n```\n\nAfter saving the file the command optionally improves the description with Claude, the same as `issue:create`.\n\n---\n\n### Improve an Issue\n\n`/issue:improve` rewrites the description of an existing issue YAML into the structured *Context / Goal / Acceptance Criteria / Technical Notes* format, suggests labels, and optionally splits the issue into smaller focused sub-issues.\n\n```\n/issue:improve --id OON-123 --module user\n```\n\n---\n\n### Push an Issue\n\n`issue:push` reads a local YAML file and syncs it back to Linear. It detects whether the issue already exists and either **updates** it (title, description, state, priority, labels, new comments) or **creates** it from scratch in a team you select interactively.\n\n```bash\noo issue:push # Prompt for issue ID\noo issue:push --id OON-123 # Push modules/shared/issues/OON-123.yml\noo issue:push --id OON-123 --module user # Push modules/user/issues/OON-123.yml\n```\n\nWhen creating a new issue (no matching Linear issue found), the command:\n- Prompts you to select a target team\n- Resolves or creates states and labels as needed\n- Renames the local file to match the real Linear identifier once the issue is created\n\n---\n\n### Recommended Workflow\n\nThere are two natural entry points depending on whether the ticket originates in Linear or locally.\n\n#### Linear-first (existing ticket)\n\nUse this when a ticket is already created in Linear (e.g. by a product manager) and you are picking it up for implementation:\n\n```\n1. oo issue:pull --id OON-123 --module user\n \u2192 Downloads the issue YAML into modules/user/issues/OON-123.yml\n \u2192 Optionally improves the description with Claude\n\n2. oo issue:push --id OON-123 --module user\n \u2192 Pushes any local edits (description improvements, label changes, comments) back to Linear\n \u2192 Updates the state to reflect current progress (e.g. \"In Progress\" \u2192 \"In Review\")\n```\n\n#### Offline-first (new idea or local spike)\n\nUse this when you are defining a new feature locally before it exists in Linear:\n\n```\n1. oo issue:create --interactive --module user\n \u2192 Creates a YAML skeleton with a placeholder ID (e.g. ABD-042381)\n \u2192 Optionally improves the description with Claude and suggests labels\n\n2. Refine the YAML: adjust title, state, priority, description, labels.\n\n3. oo issue:push --id ABD-042381 --module user\n \u2192 Creates the issue in Linear under the team you select\n \u2192 Renames the local file to the real Linear identifier (e.g. OON-124.yml)\n```\n\n#### Day-to-day tips\n\n- **Commit issue YAMLs alongside implementation commits** \u2014 the file is the authoritative record of what was planned and why. Future reviewers and `git blame` will thank you.\n- **Use the description improvement step** \u2014 a well-structured description (Context / Goal / Acceptance Criteria) makes the issue easier to implement and review.\n- **Keep state in sync** \u2014 before marking a PR ready for review, run `issue:push` to move the Linear ticket to \"In Review\" so the board reflects reality.\n- **Never commit `linear.api_key`** \u2014 keep it in `modules/app/.env.yml` and ensure `.env.yml` is in `.gitignore`.\n\n---\n\n## Generators\n\n### AI\n\nGenerates an AI integration class in `modules/<name>/src/ai/<Name>Ai.ts`. Automatically installs `@ooneex/ai`:\n\n```bash\noo make:ai --name Chat --module user\n```\n\n**Why:** Calling LLM APIs directly from controllers or services mixes infrastructure concerns with business logic and makes it hard to swap providers. A dedicated AI class encapsulates the model, prompt strategy, and configuration in one place.\n\n**Benefits:** Provider-agnostic abstraction, centralised prompt management, easy to mock in tests, swap or upgrade models without touching business logic.\n\n**Good practices:**\n- One class per distinct AI capability (e.g. `ChatAi`, `SummarizeAi`, `ModerationAi`) \u2014 avoid a single god class.\n- Keep system prompts and model configuration inside the class, not scattered across callers.\n- Always handle rate-limit and timeout errors gracefully; never let an AI failure crash the request.\n- Log both the prompt and the response at `debug` level to simplify troubleshooting.\n\n---\n\n### Analytics\n\nGenerates an analytics handler class in `modules/<name>/src/analytics/<Name>Analytics.ts`. Automatically installs `@ooneex/analytics`:\n\n```bash\noo make:analytics --name User --module user\n```\n\n**Why:** Sprinkling raw analytics calls (PostHog, Segment, etc.) throughout controllers couples business logic to a third-party SDK. A dedicated analytics class centralises event definitions and makes it trivial to change or disable the provider.\n\n**Benefits:** Single source of truth for event names and properties, easy to stub in tests, provider can be swapped without touching controllers, events are discoverable by name.\n\n**Good practices:**\n- Name events after business actions, not UI interactions (e.g. `user.subscribed`, not `button.clicked`).\n- Define event property shapes as TypeScript types inside the class.\n- Always fire analytics asynchronously \u2014 never `await` a tracking call on the critical path.\n- Keep one analytics class per domain concept so event ownership is clear.\n\n---\n\n### Cache\n\nGenerates a cache handler class in `modules/<name>/src/cache/<Name>Cache.ts`. Automatically installs `@ooneex/cache`:\n\n```bash\noo make:cache --name User --module user\n```\n\n**Why:** Inline cache calls scattered through services create duplicated TTL values, inconsistent key formats, and make it hard to invalidate related entries. A dedicated cache class owns the key scheme and TTL policy for a given domain object.\n\n**Benefits:** Consistent key naming, single place to update TTLs, easy to add cache warming or invalidation logic, simple to disable caching during tests.\n\n**Good practices:**\n- Prefix keys with the module and entity name to avoid collisions (e.g. `user:profile:{id}`).\n- Define TTLs as named constants, not magic numbers.\n- Always invalidate or update the cache when the underlying data changes \u2014 stale reads are harder to debug than cache misses.\n- Use the cache for reads only; never store the primary copy of data in a cache.\n\n---\n\n### Controller\n\nGenerates an HTTP or WebSocket controller registered in the target module. Prompts for route name, path, and HTTP method if not provided. Automatically installs `@ooneex/controller`:\n\n```bash\noo make:controller --name UserCreate --module user\noo make:controller --name UserCreate --module user --route.name api.user.create --route.path /api/users --route.method POST\noo make:controller --name UserSocket --module user --isSocket\n```\n\n**Why:** Controllers are the entry point for all HTTP and WebSocket interactions. Generating them with a consistent structure ensures route registration, validation, and error handling are set up correctly from the start.\n\n**Benefits:** Automatic route registration in the module, consistent request/response patterns, clear separation between routing and business logic, WebSocket support out of the box.\n\n**Good practices:**\n- One controller per HTTP action (e.g. `UserCreateController`, `UserListController`) \u2014 avoid multi-action controllers.\n- Controllers should be thin: validate input, call a service, return a response. No business logic inside.\n- Prefix route names with the module (e.g. `api.user.create`) to avoid cross-module collisions.\n- Use the generated route name constant everywhere instead of hardcoding URL strings.\n\n---\n\n### Cron\n\nGenerates a cron job class in `modules/<name>/src/crons/<Name>Cron.ts` and registers it in the module. Automatically installs `@ooneex/cron`:\n\n```bash\noo make:cron --name Cleanup --module user\n```\n\n**Why:** Ad-hoc scheduled scripts outside the application are fragile, hard to monitor, and disconnected from the codebase. Cron classes live inside the module that owns the task, are registered with the DI container, and benefit from the same logging and error handling as the rest of the application.\n\n**Benefits:** Centralised schedule management, full access to injected services and repositories, observable via the application logger, easy to disable per environment.\n\n**Good practices:**\n- Name cron classes after the task, not the schedule (e.g. `ExpiredTokenCleanupCron`, not `DailyCron`).\n- Keep the cron class small \u2014 delegate actual work to a service method so the logic is testable independently.\n- Always log the start, end, and outcome of each run.\n- Make cron jobs idempotent: running the same job twice should not corrupt data.\n- Disable background jobs in the `testing` environment to keep tests fast and deterministic.\n\n---\n\n### Database\n\nGenerates a database adapter class in `modules/<name>/src/databases/<Name>Database.ts`. Automatically installs `@ooneex/database`:\n\n```bash\noo make:database --name User --module user\n```\n\n**Why:** Direct use of the ORM or raw query builder in services tightly couples business logic to the database layer. A database adapter wraps the connection and query interface, making it straightforward to configure per-module connections, switch drivers, or mock the database in tests.\n\n**Benefits:** Consistent connection configuration per module, single place to add query logging or instrumentation, easier to test services in isolation by injecting a mock adapter.\n\n**Good practices:**\n- Use the database adapter for raw queries or bulk operations that fall outside the repository pattern.\n- Never expose the raw ORM connection object outside the adapter class.\n- Configure connection pool sizes and timeouts in one place inside the adapter.\n- Prefer the repository pattern for standard CRUD; reach for the database adapter only when you need full query control.\n\n---\n\n### Docker\n\nAdds a Docker service to `modules/app/docker-compose.yml`. Creates the file if it does not exist. Available services: `postgres`, `mysql`, `mongodb`, `redis`, `rabbitmq`, `nats`, `elasticsearch`, `clickhouse`, `minio`, `memcached`, `prometheus`, `grafana`, `jaeger`, `keycloak`, `vault`, `temporal`, `libretranslate`, `maildev`:\n\n```bash\noo make:docker\noo make:docker --name postgres\noo make:docker --name redis\n```\n\n**Why:** Manually writing Docker Compose service definitions is repetitive, error-prone, and inconsistent across projects. The generator adds a pre-configured, production-ready service block so local infrastructure matches what the application expects.\n\n**Benefits:** Consistent port mappings and environment variables across the team, generated health-check and volume definitions, no need to memorise image names or versions.\n\n**Good practices:**\n- Commit `docker-compose.yml` to source control so every developer uses identical service versions.\n- Use named volumes for persistent data (databases) and anonymous volumes for ephemeral services (caches).\n- Never use Docker Compose in production \u2014 it is a local development tool only.\n- Pin image versions (e.g. `postgres:16`) rather than using `latest` to keep environments stable.\n- Add service dependencies (`depends_on`) so the application container only starts after its databases are healthy.\n\n---\n\n### Entity\n\nGenerates a TypeORM entity class in `modules/<name>/src/entities/<Name>Entity.ts` and registers it in `SharedModule`. Automatically installs `@ooneex/entity`:\n\n```bash\noo make:entity --name User --module user\noo make:entity --name User --module user --tableName users\n```\n\n**Why:** Entities are the single source of truth for your database schema. Generating them with consistent decorators and base class inheritance ensures that timestamps, soft deletes, and other cross-cutting concerns are applied uniformly across every table.\n\n**Benefits:** Automatic registration in `SharedModule`, consistent column conventions (snake_case table names, `created_at`/`updated_at` timestamps), TypeScript types aligned with the database schema.\n\n**Good practices:**\n- Always specify the table name explicitly to avoid surprises from TypeORM's pluralisation logic.\n- Keep entities as pure data structures \u2014 no business logic, no service calls inside entity methods.\n- Add database indexes to columns used in frequent `WHERE` or `ORDER BY` clauses.\n- Use enums for columns with a fixed set of values to enforce data integrity at the database level.\n- Create a migration immediately after adding or modifying an entity so schema and code stay in sync.\n\n---\n\n### Logger\n\nGenerates a logger class in `modules/<name>/src/loggers/<Name>Logger.ts`. Automatically installs `@ooneex/logger`:\n\n```bash\noo make:logger --name Audit --module user\n```\n\n**Why:** Using a global logger everywhere produces undifferentiated log output that is hard to filter by feature or severity. A dedicated logger class per domain adds a consistent context (module name, logger name) to every log entry, making logs actionable in production.\n\n**Benefits:** Structured log entries with automatic context tags, easy to route specific loggers to different backends (file, Better Stack, etc.), no need to repeat context strings across every log call.\n\n**Good practices:**\n- Use the `Audit` logger for security-sensitive actions (login, permission changes) and the default app logger for operational events.\n- Always log at the correct level: `debug` for internal state, `info` for significant business events, `warn` for recoverable problems, `error` for failures that need attention.\n- Include relevant IDs (user ID, request ID) in every log entry to enable correlation across services.\n- Never log sensitive data such as passwords, tokens, or payment details.\n\n---\n\n### Mailer\n\nGenerates a mailer class (`<Name>Mailer.ts`) and its JSX template (`<Name>MailerTemplate.tsx`) in `modules/<name>/src/mailers/`. Automatically installs `@ooneex/mailer`:\n\n```bash\noo make:mailer --name Welcome --module user\n```\n\n**Why:** Composing email HTML inline or with raw string templates is brittle and hard to preview. The generator creates a typed mailer class paired with a JSX template, keeping email logic and presentation cleanly separated and type-safe.\n\n**Benefits:** JSX template enables component reuse and design-system consistency across emails, typed mailer class prevents missing required variables, easy to test rendering without sending real emails.\n\n**Good practices:**\n- One mailer per transactional email type (e.g. `WelcomeMailer`, `PasswordResetMailer`).\n- Never send emails synchronously on the request path \u2014 enqueue them via a job or pub/sub event.\n- Always provide a plain-text fallback alongside the HTML template for clients that block images.\n- Test email rendering in multiple clients (Gmail, Outlook, Apple Mail) before deploying \u2014 CSS support varies widely.\n- Use the `testing` environment mailer backend to capture sent emails without delivering them.\n\n---\n\n### Middleware\n\nGenerates an HTTP or WebSocket middleware class registered in the target module. Automatically installs `@ooneex/middleware`:\n\n```bash\noo make:middleware --name Auth --module user\noo make:middleware --name Auth --module user --isSocket\n```\n\n**Why:** Cross-cutting concerns like authentication, CORS, request logging, and input sanitisation should not live inside controllers. Middleware classes intercept requests before they reach a controller, keeping that logic reusable and independently testable.\n\n**Benefits:** Reusable across multiple controllers within the module, auto-registered in the middleware pipeline, full access to injected services via DI, works identically for HTTP and WebSocket routes.\n\n**Good practices:**\n- Keep middleware single-purpose (e.g. `AuthMiddleware` only handles authentication, not rate limiting).\n- Always call `next()` explicitly or return a response \u2014 never leave the pipeline hanging.\n- Order matters: authentication middleware must run before authorisation middleware.\n- Avoid heavy computation in middleware; defer expensive work to the service layer.\n- Write unit tests that verify the middleware passes, blocks, and modifies requests as expected.\n\n---\n\n### Permission\n\nGenerates a permission class in `modules/<name>/src/permissions/<Name>Permission.ts`. Automatically installs `@ooneex/permission`:\n\n```bash\noo make:permission --name User --module user\n```\n\n**Why:** Scattering permission checks across controllers and services leads to inconsistency and makes auditing access control difficult. A dedicated permission class per domain centralises all access rules for that domain in one reviewable place.\n\n**Benefits:** All permissions for a domain are discoverable in a single file, easy to audit who can do what, permissions can be reused across multiple controllers without duplication, changes to access rules require touching only one class.\n\n**Good practices:**\n- Name permissions after the action and resource (e.g. `UserPermission` checks `canCreate`, `canUpdate`, `canDelete`).\n- Always check permissions in middleware or the service layer \u2014 never in the entity or repository.\n- Combine with roles (RBAC) for coarse-grained access and permissions for fine-grained control.\n- Write tests that assert both allowed and denied cases for every permission rule.\n- Never hardcode user IDs or emails inside permission logic; use roles and attributes instead.\n\n---\n\n### PubSub\n\nGenerates a pub/sub event class in `modules/<name>/src/events/<Name>Event.ts` and registers it in the module. Automatically installs `@ooneex/pub-sub`:\n\n```bash\noo make:pubsub --name UserCreated --module user\noo make:pubsub --name UserCreated --module user --channel user-created\n```\n\n**Why:** Direct calls between modules create tight coupling that makes the codebase fragile and hard to extend. Pub/sub events decouple the publisher from the subscriber: the `user` module emits `UserCreated` without knowing or caring which other modules react to it.\n\n**Benefits:** Loose coupling between modules, multiple subscribers can react to the same event independently, easy to add new reactions without modifying the publisher, events are named and typed so their contracts are explicit.\n\n**Good practices:**\n- Name events in the past tense (e.g. `UserCreated`, `OrderShipped`) to make clear they describe something that already happened.\n- Keep event payloads small and serialisable \u2014 include IDs rather than full entity objects where possible.\n- Subscribe in the module that owns the reaction, not in the module that owns the event.\n- Make event handlers idempotent: the same event may be delivered more than once in failure scenarios.\n- Log every event published and consumed to simplify debugging distributed flows.\n\n---\n\n### Repository\n\nGenerates a repository class for data access in `modules/<name>/src/repositories/<Name>Repository.ts`. Automatically installs `@ooneex/repository`:\n\n```bash\noo make:repository --name User --module user\n```\n\n**Why:** Putting database queries directly in services mixes business logic with persistence concerns and makes services impossible to test without a database. The repository pattern isolates all data-access code behind a typed interface that can be mocked in tests.\n\n**Benefits:** Services remain database-agnostic, all queries for an entity are discoverable in one class, easy to swap the underlying ORM or database without touching business logic, clean boundary for unit testing.\n\n**Good practices:**\n- One repository per entity (e.g. `UserRepository` owns all queries against the `users` table).\n- Repositories should only contain query logic \u2014 no business rules, no validation, no event publishing.\n- Use descriptive method names that reflect intent (e.g. `findActiveByEmail`, `findExpiredTokens`) rather than generic `find` with complex filter objects.\n- Return domain objects or plain data \u2014 never expose raw ORM query builders to callers.\n- Add pagination to any method that can return an unbounded number of rows.\n\n---\n\n### Service\n\nGenerates a service class in `modules/<name>/src/services/<Name>Service.ts`. Automatically installs `@ooneex/service`:\n\n```bash\noo make:service --name User --module user\n```\n\n**Why:** Business logic scattered across controllers is impossible to test, reuse, or reason about. Services are the home of all business rules, orchestrating repositories, events, and other services in a single, testable unit.\n\n**Benefits:** Business logic is testable without HTTP, reusable across multiple controllers or cron jobs, dependencies are explicit via constructor injection, easy to mock at the service boundary in integration tests.\n\n**Good practices:**\n- One service per aggregate or major business capability (e.g. `UserService` for user lifecycle, `BillingService` for payment flows).\n- Services should call repositories for data, never query the database directly.\n- Keep services stateless \u2014 all required data should come from method arguments or injected dependencies.\n- Raise typed exceptions (e.g. `UserNotFoundException`) rather than returning `null` or error codes.\n- A service method should do one thing; if a method grows beyond ~20 lines, consider extracting a helper or sub-service.\n\n---\n\n### Storage\n\nGenerates a file storage class in `modules/<name>/src/storage/<Name>Storage.ts`. Automatically installs `@ooneex/storage`:\n\n```bash\noo make:storage --name Avatar --module user\n```\n\n**Why:** Coupling file upload logic directly to a specific provider (S3, Cloudflare R2, local filesystem) makes it painful to change backends and duplicates configuration across the codebase. A storage class abstracts the provider behind a consistent interface scoped to a specific asset type.\n\n**Benefits:** Provider-agnostic uploads and reads, all storage configuration for an asset type in one class, easy to switch between local filesystem (development) and cloud storage (production) via environment variables, consistent file-naming and path conventions.\n\n**Good practices:**\n- Name storage classes after the asset they manage (e.g. `AvatarStorage`, `InvoiceStorage`).\n- Always validate file type and size before passing to the storage class \u2014 never trust client-supplied content types.\n- Generate deterministic, collision-resistant file names (e.g. `{userId}/{uuid}.{ext}`) rather than using the original filename.\n- Never store the full URL in the database \u2014 store only the path or key and derive the URL at read time so it works across environments.\n- Delete orphaned files when the associated entity is deleted to avoid unbounded storage growth.\n\n---\n\n### Vector Database\n\nGenerates a vector database class in `modules/<name>/src/databases/<Name>VectorDatabase.ts`. Automatically installs `@ooneex/rag`:\n\n```bash\noo make:vector-database --name Knowledge --module user\n```\n\n**Why:** Semantic search and Retrieval-Augmented Generation (RAG) require storing and querying high-dimensional embeddings, which relational databases are not designed for. A dedicated vector database class encapsulates the embedding model, index configuration, and similarity-search logic in one place.\n\n**Benefits:** Centralised embedding and retrieval strategy per knowledge domain, easy to swap vector backends (pgvector, Qdrant, etc.), consistent chunking and metadata conventions, decoupled from the LLM layer so both can evolve independently.\n\n**Good practices:**\n- One vector database class per knowledge domain (e.g. `KnowledgeVectorDatabase`, `ProductCatalogVectorDatabase`).\n- Always store the source document ID alongside each embedding so retrieved chunks can be traced back to their origin.\n- Use a consistent chunking strategy (fixed-size or sentence-boundary) within a class \u2014 mixing strategies pollutes the index.\n- Re-index when the embedding model changes; embeddings from different models are not comparable.\n- Cache frequently retrieved embeddings to avoid redundant model calls on hot queries.\n\n---\n\n## Claude\n\nThis project ships with a set of **Claude Code skills** \u2014 pre-built instruction sets that tell Claude exactly how to scaffold, implement, and test each type of artefact in this codebase. Skills eliminate the gap between \"generate the files\" and \"ship working code\": each skill runs the CLI generator, reads the output, completes the implementation, writes tests, and lints \u2014 all in one `/command`.\n\n### Why Skills?\n\nRunning `oo make:service` creates a skeleton. A Claude skill does that *and* completes the business logic, writes a meaningful test suite, applies coding conventions, and formats the result \u2014 turning a 10-step workflow into a single prompt.\n\n**Benefits:**\n- Consistent code quality across every artefact, regardless of who wrote it\n- Conventions (visibility modifiers, naming suffixes, DI patterns) are enforced automatically\n- Tests are generated alongside implementation, not as an afterthought\n- Skills chain together \u2014 `make:controller` automatically triggers `make:service` and `make:pubsub`\n\n### Setup\n\nBefore using skills, generate the `.claude/` directory with all skill files and the project `CLAUDE.md`:\n\n```bash\noo claude:skill:create\n```\n\nThis writes `.claude/CLAUDE.md` and one `SKILL.md` per skill under `.claude/skills/<skill-name>/`. Re-run any time you upgrade the CLI to pick up new or updated skills. Commit the generated files so every team member gets the same skill definitions without running the command themselves.\n\n### How to Use\n\nOpen Claude Code in the project root and type `/skill-name`. Claude will execute the full workflow described in the skill. You can pass arguments inline:\n\n```\n/make:service --name=UserCreate --module=user\n/make:controller --name=UserCreate --module=user --route.path=/users --route.method=POST\n/commit\n```\n\nIf you omit arguments, Claude will prompt you for the required values.\n\n> **Important:** Always run skills from the project root, not from inside a module directory.\n\n### Available Skills\n\n#### Code Quality\n\n| Skill | Description |\n|---|---|\n| `/optimize` | Enforce naming conventions, remove duplication, improve performance, and restructure tests across a module |\n| `/commit` | Analyze staged changes, group them by module, and create properly scoped conventional commits |\n\n#### Generators\n\n| Skill | Description |\n|---|---|\n| `/make:ai` | Generate an AI integration class with `run` and `runStream` methods, wired to `@ooneex/ai` |\n| `/make:analytics` | Generate an analytics handler class for tracking domain events via `@ooneex/analytics` |\n| `/make:cache` | Generate a cache handler class with key and TTL management via `@ooneex/cache` |\n| `/make:command` | Generate a CLI command class implementing `ICommand` via `@ooneex/cli` |\n| `/make:controller` | Generate an HTTP or WebSocket controller with route type, validation, roles, service, and pub/sub event |\n| `/make:cron` | Generate a cron job class registered in its module via `@ooneex/cron` |\n| `/make:database` | Generate a database adapter class for raw queries via `@ooneex/database` |\n| `/make:entity` | Generate a TypeORM entity class registered in `SharedModule` via `@ooneex/entity` |\n| `/make:logger` | Generate a structured logger class with domain context via `@ooneex/logger` |\n| `/make:mailer` | Generate a mailer class and its JSX email template via `@ooneex/mailer` |\n| `/make:middleware` | Generate an HTTP or WebSocket middleware class registered in the module via `@ooneex/middleware` |\n| `/make:permission` | Generate a permission class centralising access rules for a domain via `@ooneex/permission` |\n| `/make:pubsub` | Generate a pub/sub event class registered in the module via `@ooneex/pub-sub` |\n| `/make:repository` | Generate a repository class for typed data access via `@ooneex/repository` |\n| `/make:service` | Generate a service class implementing `IService` with business logic and tests |\n| `/make:storage` | Generate a file storage class for asset management via `@ooneex/storage` |\n| `/make:vector-database` | Generate a vector database class for semantic search and RAG via `@ooneex/rag` |\n\n#### Database\n\n| Skill | Description |\n|---|---|\n| `/migration:create` | Generate a migration file with `up`/`down` methods, index guidance, and structural tests |\n| `/seed:create` | Generate a seed class and its YAML data file with idempotent insertion logic |\n\n### Coding Conventions Enforced by Skills\n\nAll generator skills automatically apply the conventions defined in `/optimize`:\n\n- **Visibility modifiers** \u2014 every class method and property has explicit `public`, `private`, or `protected`\n- **Naming suffixes** \u2014 types end with `Type`, interfaces start with `I`\n- **Arrow functions** \u2014 used everywhere except class methods\n- **No non-null assertions** \u2014 use default values or optional types instead\n- **Dependency injection** \u2014 constructor injection via `@inject()` from `@ooneex/container`\n- **Code hygiene** \u2014 no unused imports, no dead code, no bare `TODO` comments\n\n---\n\n## Release\n\n```bash\noo make:release\n```\n\nScans every `packages/` and `modules/` directory for unreleased conventional commits and, for each one that has them, performs a full release cycle automatically:\n\n1. **Detects unreleased work** \u2014 finds the last git tag matching `<package-name>@*` and lists all conventional commits that have landed since then in that directory. Packages with no new commits are skipped.\n2. **Bumps the version** in `package.json`:\n - `minor` (e.g. `1.2.0 \u2192 1.3.0`) when any commit is of type `feat`\n - `patch` (e.g. `1.2.0 \u2192 1.2.1`) for all other types (`fix`, `refactor`, `perf`, `docs`, `chore`, \u2026)\n3. **Updates `CHANGELOG.md`** with a dated version section. Commits are grouped into standard Keep-a-Changelog categories and linked to their SHA on the remote:\n\n | Commit type | Changelog category |\n |---|---|\n | `feat` | Added |\n | `fix` | Fixed |\n | `refactor`, `perf`, `style`, `docs`, `build`, `ci`, `chore` | Changed |\n | `revert` | Removed |\n\n4. **Creates a release commit** \u2014 stages `package.json` and `CHANGELOG.md` then commits with the message `chore(release): <name>@<version>`.\n5. **Creates an annotated git tag** \u2014 `<name>@<version>` (e.g. `@acme/user@1.3.0`).\n6. **Prompts to push** \u2014 after all packages are processed, asks whether to push commits and tags to the remote. If confirmed, it also runs `bun install`, commits the updated `bun.lock`, and pushes everything.\n\n> **Note:** This command requires conventional commits (`type(scope): Subject`). Non-conventional commits are silently ignored when building the changelog.\n\n---\n\n## Scripts\n\n| Command | Description |\n|---|---|\n| `bun run fmt` | Format all source files with Biome |\n| `bun run lint` | Lint all modules with Biome and TypeScript |\n| `bun run test` | Run tests across all modules |\n| `bun run commit` | Interactive conventional commit prompt |\n";
10366
10367
 
@@ -10761,6 +10762,10 @@ chore(common): Update bun.lock dependencies
10761
10762
 
10762
10763
  Use the \`/commit\` skill to automate this workflow.
10763
10764
 
10765
+ ### Commit Trailers
10766
+
10767
+ Do not add any \`Co-Authored-By\` trailer to commits.
10768
+
10764
10769
  ## Naming Conventions
10765
10770
 
10766
10771
  **Strictly enforced** by DI decorators \u2014 violations throw at startup:
@@ -10871,6 +10876,10 @@ git commit -m "chore(common): Update dependencies and cache package"
10871
10876
  - **Deleted files only**: use \`refactor\` (e.g., \`refactor(user): Remove deprecated UserAdapter\`)
10872
10877
  - **Renamed/moved files**: use \`refactor\` (e.g., \`refactor(product): Reorganize service file structure\`)
10873
10878
 
10879
+ ## Commit Trailers
10880
+
10881
+ Do not add any \`Co-Authored-By\` trailer to commits.
10882
+
10874
10883
  ## Coding Conventions
10875
10884
 
10876
10885
  Apply all coding conventions from the \`optimize\` skill.
@@ -10988,7 +10997,7 @@ Only if \`resources.repository\` is present.
10988
10997
  /make:repository --name=<resources.repository> --module=<module>
10989
10998
  \`\`\`
10990
10999
 
10991
- Retain only the CRUD methods that are needed by this issue (e.g. \`.create\` needs \`save\`; \`.read\` needs \`findById\`; \`.list\` needs \`find\`; \`.delete\` needs \`delete\`).
11000
+ Retain only the CRUD methods that are needed by this issue (e.g. \`.create\` needs \`save\`; \`.read\` needs \`findById\`; \`.list\` needs \`find\`; \`.delete\` needs \`delete\`). For each repository, remove any methods that are not called by a service or other consumer \u2014 do not keep unused methods.
10992
11001
 
10993
11002
  ### 6. Create or update the service
10994
11003
 
@@ -13620,6 +13629,8 @@ Read \`modules/<module>/src/storage/<Name>Storage.ts\`, then implement:
13620
13629
 
13621
13630
  \`\`\`typescript
13622
13631
  import { Storage, decorator, StorageException } from "@ooneex/storage";
13632
+ import { inject } from "@ooneex/container";
13633
+ import { AppEnv } from "@ooneex/app-env";
13623
13634
  import type { S3Options } from "bun";
13624
13635
 
13625
13636
  @decorator.storage()
@@ -13630,24 +13641,27 @@ export class <Name>Storage extends Storage {
13630
13641
  private readonly endpoint: string;
13631
13642
  private readonly region: string;
13632
13643
 
13633
- constructor(options?: {
13634
- accessKey?: string;
13635
- secretKey?: string;
13636
- endpoint?: string;
13637
- region?: string;
13638
- }) {
13644
+ public constructor(
13645
+ @inject(AppEnv) private readonly env: AppEnv,
13646
+ options?: {
13647
+ accessKey?: string;
13648
+ secretKey?: string;
13649
+ endpoint?: string;
13650
+ region?: string;
13651
+ },
13652
+ ) {
13639
13653
  super();
13640
13654
 
13641
- const accessKey = options?.accessKey || Bun.env.STORAGE_<NAME_UPPER>_ACCESS_KEY;
13642
- const secretKey = options?.secretKey || Bun.env.STORAGE_<NAME_UPPER>_SECRET_KEY;
13643
- const endpoint = options?.endpoint || Bun.env.STORAGE_<NAME_UPPER>_ENDPOINT;
13655
+ const accessKey = options?.accessKey || this.env.STORAGE_<NAME_UPPER>_ACCESS_KEY;
13656
+ const secretKey = options?.secretKey || this.env.STORAGE_<NAME_UPPER>_SECRET_KEY;
13657
+ const endpoint = options?.endpoint || this.env.STORAGE_<NAME_UPPER>_ENDPOINT;
13644
13658
 
13645
13659
  // ... validation throws StorageException if missing ...
13646
13660
 
13647
13661
  this.accessKey = accessKey;
13648
13662
  this.secretKey = secretKey;
13649
13663
  this.endpoint = endpoint;
13650
- this.region = options?.region || Bun.env.STORAGE_<NAME_UPPER>_REGION || "auto";
13664
+ this.region = options?.region || this.env.STORAGE_<NAME_UPPER>_REGION || "auto";
13651
13665
  }
13652
13666
 
13653
13667
  public getOptions(): S3Options {
@@ -14401,13 +14415,12 @@ class ClaudeSkillCreateCommand {
14401
14415
  }
14402
14416
  async run() {
14403
14417
  const claudeLocalDir = ".claude";
14404
- const claudeDir = join4(process.cwd(), claudeLocalDir);
14405
14418
  const skillsLocalDir = join4(claudeLocalDir, "skills");
14406
14419
  const skillsDir = join4(process.cwd(), skillsLocalDir);
14407
14420
  const logger = new TerminalLogger3;
14408
- const claudeMdPath = join4(claudeDir, "CLAUDE.md");
14421
+ const claudeMdPath = join4(process.cwd(), "CLAUDE.md");
14409
14422
  await Bun.write(claudeMdPath, CLAUDE_md_default);
14410
- logger.success(`${join4(claudeLocalDir, "CLAUDE.md")} created successfully`, undefined, {
14423
+ logger.success("CLAUDE.md created successfully", undefined, {
14411
14424
  showTimestamp: false,
14412
14425
  showArrow: false,
14413
14426
  useSymbol: true
@@ -14560,6 +14573,8 @@ class AppCreateCommand {
14560
14573
  const dockerfileContent = Dockerfile_default.replace(/{{NAME}}/g, snakeName);
14561
14574
  await Bun.write(join6(destination, "modules", "app", "Dockerfile"), dockerfileContent);
14562
14575
  await Bun.write(join6(destination, "modules", "app", "var", ".gitkeep"), "");
14576
+ const appInitCommand = new AppInitCommand;
14577
+ await appInitCommand.run({ name, destination, silent: true, appType: "api" });
14563
14578
  const addDeps = Bun.spawn([
14564
14579
  "bun",
14565
14580
  "add",
@@ -14590,7 +14605,7 @@ class AppCreateCommand {
14590
14605
  "react-dom",
14591
14606
  "apache-arrow",
14592
14607
  "reflect-metadata",
14593
- "typeorm"
14608
+ "typeorm@^1.0.0"
14594
14609
  ], { cwd: destination, stdout: "ignore", stderr: "inherit" });
14595
14610
  await addDeps.exited;
14596
14611
  const addDevDeps = Bun.spawn(["bun", "add", "-D", "@ooneex/command", "@ooneex/migrations", "@ooneex/seeds"], {
@@ -14599,8 +14614,14 @@ class AppCreateCommand {
14599
14614
  stderr: "inherit"
14600
14615
  });
14601
14616
  await addDevDeps.exited;
14602
- const appInitCommand = new AppInitCommand;
14603
- await appInitCommand.run({ name, destination, silent: true, appType: "api" });
14617
+ const rootPackagePath = join6(destination, "package.json");
14618
+ const rootPackage = await Bun.file(rootPackagePath).json();
14619
+ const templatePackage = JSON.parse(package_json_default.replace(/{{NAME}}/g, kebabName));
14620
+ rootPackage.scripts ??= templatePackage.scripts;
14621
+ rootPackage.workspaces ??= templatePackage.workspaces;
14622
+ rootPackage["lint-staged"] ??= templatePackage["lint-staged"];
14623
+ await Bun.write(rootPackagePath, `${JSON.stringify(rootPackage, null, 2)}
14624
+ `);
14604
14625
  const logger = new TerminalLogger5;
14605
14626
  logger.success(`${kebabName} created successfully at ${destination}`, undefined, {
14606
14627
  showTimestamp: false,
@@ -14872,7 +14893,7 @@ class CommandRunCommand {
14872
14893
  const proc = Bun.spawn(["bun", "run", commandRunPath, commandName, ...extraArgs], {
14873
14894
  cwd: process.cwd(),
14874
14895
  stdout: "inherit",
14875
- stderr: "inherit"
14896
+ stderr: "pipe"
14876
14897
  });
14877
14898
  const exitCode = await proc.exited;
14878
14899
  if (exitCode === 0) {
@@ -14883,7 +14904,9 @@ class CommandRunCommand {
14883
14904
  });
14884
14905
  return;
14885
14906
  }
14886
- logger.warn(`Command "${commandName}" not found in ${name}`, undefined, {
14907
+ const errorOutput = await new Response(proc.stderr).text();
14908
+ const trimmed = errorOutput.trim();
14909
+ logger.warn(`Command "${commandName}" not found in ${name}`, trimmed ? { message: trimmed } : undefined, {
14887
14910
  showTimestamp: false,
14888
14911
  showArrow: false,
14889
14912
  useSymbol: false
@@ -96567,41 +96590,51 @@ import { decorator as decorator20 } from "@ooneex/command";
96567
96590
  import { TerminalLogger as TerminalLogger19 } from "@ooneex/logger";
96568
96591
  import { toPascalCase as toPascalCase7 } from "@ooneex/utils";
96569
96592
 
96570
- // src/templates/database.test.txt
96571
- var database_test_default = `import { describe, expect, test } from "bun:test";
96572
- import { {{NAME}}Database } from "@module/{{MODULE}}/databases/{{NAME}}Database";
96573
-
96574
- describe("{{NAME}}Database", () => {
96575
- test("should have class name ending with 'Database'", () => {
96576
- expect({{NAME}}Database.name.endsWith("Database")).toBe(true);
96577
- });
96578
-
96579
- test("should have 'getSource' method", () => {
96580
- expect({{NAME}}Database.prototype.getSource).toBeDefined();
96581
- expect(typeof {{NAME}}Database.prototype.getSource).toBe("function");
96593
+ // src/prompts/askDatabaseType.ts
96594
+ var import_enquirer11 = __toESM(require_enquirer(), 1);
96595
+ var askDatabaseType = async (config) => {
96596
+ const response = await import_enquirer11.prompt({
96597
+ type: "select",
96598
+ name: "type",
96599
+ message: config.message,
96600
+ choices: ["postgres", "sqlite"]
96582
96601
  });
96583
- });
96584
- `;
96602
+ return response.type;
96603
+ };
96585
96604
 
96586
- // src/templates/database.txt
96587
- var database_default = `import { DataSource } from "typeorm";
96605
+ // src/templates/database.pg.txt
96606
+ var database_pg_default = `import { DataSource } from "typeorm";
96588
96607
  import { TypeormDatabase, DatabaseException, decorator } from "@ooneex/database";
96608
+ import { inject } from "@ooneex/container";
96609
+ import { AppEnv } from "@ooneex/app-env";
96589
96610
 
96590
96611
  @decorator.database()
96591
96612
  export class {{NAME}}Database extends TypeormDatabase {
96592
- public getSource(database?: string): DataSource {
96593
- database = database || "var/db";
96613
+ public constructor(@inject(AppEnv) private readonly env: AppEnv) {
96614
+ super();
96615
+ }
96616
+
96617
+ public getSource(_database?: string): DataSource {
96618
+ const url = this.env.DATABASE_URL ?? "";
96619
+
96620
+ if (!url) {
96621
+ throw new DatabaseException(
96622
+ "Database URL is required. Please provide a URL either through the constructor options or set the DATABASE_URL environment variable.",
96623
+ "CONNECTION_FAILED",
96624
+ );
96625
+ }
96594
96626
 
96595
96627
  this.source = new DataSource({
96596
96628
  synchronize: false,
96597
96629
  entities: [
96598
96630
  // TODO: Load your entities here
96599
96631
  ],
96600
- enableWAL: true,
96601
- busyErrorRetry: 2000,
96602
- busyTimeout: 30_000,
96603
- database,
96604
- type: "sqlite",
96632
+ extra: {
96633
+ max: 10,
96634
+ // idleTimeoutMillis: 30000,
96635
+ },
96636
+ url,
96637
+ type: "postgres",
96605
96638
  });
96606
96639
 
96607
96640
  return this.source;
@@ -96609,6 +96642,62 @@ export class {{NAME}}Database extends TypeormDatabase {
96609
96642
  }
96610
96643
  `;
96611
96644
 
96645
+ // src/templates/database.sqlite.txt
96646
+ var database_sqlite_default = `import { DataSource } from "typeorm";
96647
+ import { TypeormDatabase, DatabaseException, decorator } from "@ooneex/database";
96648
+ import { inject } from "@ooneex/container";
96649
+ import { AppEnv } from "@ooneex/app-env";
96650
+
96651
+ @decorator.database()
96652
+ export class {{NAME}}Database extends TypeormDatabase {
96653
+ public constructor(@inject(AppEnv) private readonly env: AppEnv) {
96654
+ super();
96655
+ }
96656
+
96657
+ public getSource(database?: string): DataSource {
96658
+ database = database || this.env.SQLITE_DATABASE_PATH || "var/db";
96659
+
96660
+ if (!database) {
96661
+ throw new DatabaseException(
96662
+ "SQLite database path is required. Please provide a database path either through the constructor options or set the SQLITE_DATABASE_PATH environment variable.",
96663
+ "CONNECTION_FAILED",
96664
+ );
96665
+ }
96666
+
96667
+ if (!this.source) {
96668
+ this.source = new DataSource({
96669
+ synchronize: false,
96670
+ entities: [
96671
+ // TODO: Load your entities here
96672
+ ],
96673
+ enableWAL: true,
96674
+ timeout: 30_000,
96675
+ database,
96676
+ type: "better-sqlite3",
96677
+ });
96678
+ }
96679
+
96680
+ return this.source;
96681
+ }
96682
+ }
96683
+ `;
96684
+
96685
+ // src/templates/database.test.txt
96686
+ var database_test_default = `import { describe, expect, test } from "bun:test";
96687
+ import { {{NAME}}Database } from "@module/{{MODULE}}/databases/{{NAME}}Database";
96688
+
96689
+ describe("{{NAME}}Database", () => {
96690
+ test("should have class name ending with 'Database'", () => {
96691
+ expect({{NAME}}Database.name.endsWith("Database")).toBe(true);
96692
+ });
96693
+
96694
+ test("should have 'getSource' method", () => {
96695
+ expect({{NAME}}Database.prototype.getSource).toBeDefined();
96696
+ expect(typeof {{NAME}}Database.prototype.getSource).toBe("function");
96697
+ });
96698
+ });
96699
+ `;
96700
+
96612
96701
  // src/commands/MakeDatabaseCommand.ts
96613
96702
  class MakeDatabaseCommand {
96614
96703
  getName() {
@@ -96623,7 +96712,9 @@ class MakeDatabaseCommand {
96623
96712
  name = await askName({ message: "Enter database name" });
96624
96713
  }
96625
96714
  name = toPascalCase7(name).replace(/DatabaseAdapter$/, "").replace(/Database$/, "");
96626
- const content = database_default.replace(/{{NAME}}/g, name);
96715
+ const dbType = await askDatabaseType({ message: "Select database type" });
96716
+ const template = dbType === "postgres" ? database_pg_default : database_sqlite_default;
96717
+ const content = template.replace(/{{NAME}}/g, name);
96627
96718
  await ensureModule(module);
96628
96719
  const base = join20("modules", module);
96629
96720
  const databaseLocalDir = join20(base, "src", "databases");
@@ -96670,7 +96761,7 @@ import { TerminalLogger as TerminalLogger20 } from "@ooneex/logger";
96670
96761
  var {YAML } = globalThis.Bun;
96671
96762
 
96672
96763
  // src/prompts/askDockerService.ts
96673
- var import_enquirer11 = __toESM(require_enquirer(), 1);
96764
+ var import_enquirer12 = __toESM(require_enquirer(), 1);
96674
96765
  var DOCKER_SERVICES = [
96675
96766
  "clickhouse",
96676
96767
  "elasticsearch",
@@ -96692,7 +96783,7 @@ var DOCKER_SERVICES = [
96692
96783
  "vault"
96693
96784
  ];
96694
96785
  var askDockerService = async (config) => {
96695
- const response = await import_enquirer11.prompt({
96786
+ const response = await import_enquirer12.prompt({
96696
96787
  type: "autocomplete",
96697
96788
  name: "service",
96698
96789
  message: config.message,
@@ -98676,759 +98767,10 @@ class MakeRepositoryCommand {
98676
98767
  MakeRepositoryCommand = __legacyDecorateClassTS([
98677
98768
  decorator29.command()
98678
98769
  ], MakeRepositoryCommand);
98679
- // src/commands/MakeResourceBookCommand.ts
98680
- import { join as join32 } from "path";
98681
- import { decorator as decorator32 } from "@ooneex/command";
98682
- var {Glob } = globalThis.Bun;
98683
-
98684
- // src/templates/resources/book/BookEntity.txt
98685
- var BookEntity_default = `import type { LocaleType } from "@ooneex/translation";
98686
- import { random } from "@ooneex/utils";
98687
- import {
98688
- Column,
98689
- CreateDateColumn,
98690
- Entity,
98691
- PrimaryColumn,
98692
- UpdateDateColumn,
98693
- } from "typeorm";
98694
-
98695
- @Entity({
98696
- name: "books",
98697
- })
98698
- export class BookEntity {
98699
- @PrimaryColumn({ name: "id", type: "varchar", length: 25, nullable: false })
98700
- id: string = random.id();
98701
-
98702
- @Column({ name: "title", type: "varchar", length: 255, nullable: false })
98703
- title: string;
98704
-
98705
- @Column({ name: "color", type: "varchar", length: 50, nullable: true })
98706
- color?: string | null;
98707
-
98708
- @Column({ name: "description", type: "text", nullable: true })
98709
- description?: string | null;
98710
-
98711
- @Column({ name: "summarize", type: "text", nullable: true })
98712
- summarize?: string | null;
98713
-
98714
- @Column({ name: "page_count", type: "int", nullable: true })
98715
- pageCount?: number | null;
98716
-
98717
- @Column({ name: "src", type: "varchar", length: 255, nullable: false })
98718
- src: string;
98719
-
98720
- @Column({ name: "cover_image", type: "varchar", length: 255, nullable: true })
98721
- coverImage?: string | null;
98722
-
98723
- @Column({ name: "tags", type: "text", array: true, nullable: true })
98724
- tags?: string[] | null;
98725
-
98726
- @Column({ name: "topics", type: "text", array: true, nullable: true })
98727
- topics?: string[] | null;
98728
-
98729
- @Column({ name: "categories", type: "text", array: true, nullable: true })
98730
- categories?: string[] | null;
98731
-
98732
- @Column({ name: "lang", type: "varchar", length: 10, nullable: true })
98733
- lang?: LocaleType | null;
98734
-
98735
- @Column({ name: "status", type: "varchar", length: 50, nullable: true })
98736
- status?: string | null;
98737
-
98738
- @CreateDateColumn({ name: "created_at", nullable: true })
98739
- createdAt?: Date | null;
98740
-
98741
- @UpdateDateColumn({ name: "updated_at", nullable: true })
98742
- updatedAt?: Date | null;
98743
- }
98744
- `;
98745
-
98746
- // src/templates/resources/book/BookMigration.txt
98747
- var BookMigration_default = `import { decorator, type IMigration, type MigrationClassType } from '@ooneex/migrations';
98748
- import type { TransactionSQL } from 'bun';
98749
-
98750
- @decorator.migration()
98751
- export class {{ name }} implements IMigration {
98752
- public async up(tx: TransactionSQL): Promise<void> {
98753
- await tx\`
98754
- CREATE TABLE IF NOT EXISTS books (
98755
- id VARCHAR(25) PRIMARY KEY,
98756
- title VARCHAR(255) NOT NULL,
98757
- color VARCHAR(50),
98758
- description TEXT,
98759
- summarize TEXT,
98760
- page_count INT,
98761
- src VARCHAR(255) NOT NULL,
98762
- cover_image VARCHAR(255),
98763
- tags TEXT[],
98764
- topics TEXT[],
98765
- categories TEXT[],
98766
- lang VARCHAR(10),
98767
- status VARCHAR(50),
98768
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
98769
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
98770
- )
98771
- \`;
98772
-
98773
- await tx\`
98774
- CREATE INDEX IF NOT EXISTS idx_books_title ON books (title)
98775
- \`;
98776
- }
98777
-
98778
- public async down(tx: TransactionSQL): Promise<void> {
98779
- await tx\`DROP TABLE IF EXISTS books\`;
98780
- }
98781
-
98782
- public getVersion(): string {
98783
- return '{{ version }}';
98784
- }
98785
-
98786
- public getDependencies(): MigrationClassType[] {
98787
- return [];
98788
- }
98789
- }
98790
- `;
98791
-
98792
- // src/templates/resources/book/BookRepository.txt
98793
- var BookRepository_default = `import { inject } from "@ooneex/container";
98794
- import type { ITypeormDatabase } from "@ooneex/database";
98795
- import { decorator } from "@ooneex/repository";
98796
- import type { FilterResultType } from "@ooneex/types";
98797
- import type { FindManyOptions, FindOptionsWhere, Repository, SaveOptions, UpdateResult } from "typeorm";
98798
- import { ILike } from "typeorm";
98799
- import { BookEntity } from "../entities/BookEntity";
98800
-
98801
- @decorator.repository()
98802
- export class BookRepository {
98803
- constructor(
98804
- @inject("database")
98805
- private readonly database: ITypeormDatabase,
98806
- ) {}
98807
-
98808
- public async open(): Promise<Repository<BookEntity>> {
98809
- return await this.database.open(BookEntity);
98810
- }
98811
-
98812
- public async close(): Promise<void> {
98813
- await this.database.close();
98814
- }
98815
-
98816
- public async find(
98817
- criteria: FindManyOptions<BookEntity> & { page?: number; limit?: number; order?: "ASC" | "DESC"; orderBy?: string; q?: string },
98818
- ): Promise<FilterResultType<BookEntity>> {
98819
- const repository = await this.open();
98820
-
98821
- const { page = 1, limit = 100, order, orderBy, q, ...rest } = criteria;
98822
-
98823
- let skip: number | undefined;
98824
- const take = limit === 0 ? 100 : limit;
98825
-
98826
- if (page && page > 0 && limit && limit > 0) {
98827
- skip = (page - 1) * take;
98828
- }
98829
-
98830
- let findOptions = {
98831
- ...rest,
98832
- take,
98833
- ...(skip !== undefined && { skip }),
98834
- ...(orderBy && { order: { [orderBy]: order ?? "ASC" } }),
98835
- };
98836
- if (q) {
98837
- findOptions = {
98838
- ...findOptions,
98839
- where: {
98840
- ...rest.where,
98841
- title: ILike(\`%\${q}%\`),
98842
- },
98843
- };
98844
- }
98845
-
98846
- const result = await repository.find(findOptions);
98847
-
98848
- let countWhere = rest.where;
98849
- if (q) {
98850
- countWhere = {
98851
- ...rest.where,
98852
- title: ILike(\`%\${q}%\`),
98853
- };
98854
- }
98855
-
98856
- const total = await this.count(countWhere);
98857
- const totalPages = Math.ceil(total / limit);
98858
-
98859
- return {
98860
- resources: result,
98861
- total,
98862
- totalPages,
98863
- page,
98864
- limit,
98865
- };
98866
- }
98867
-
98868
- public async findOne(id: string): Promise<BookEntity | null> {
98869
- const repository = await this.open();
98870
-
98871
- return await repository.findOne({
98872
- where: { id },
98873
- });
98874
- }
98875
-
98876
- public async findOneBy(criteria: FindOptionsWhere<BookEntity>): Promise<BookEntity | null> {
98877
- const repository = await this.open();
98878
-
98879
- return await repository.findOne({
98880
- where: criteria,
98881
- });
98882
- }
98883
-
98884
- public async create(entity: BookEntity, options?: SaveOptions): Promise<BookEntity> {
98885
- const repository = await this.open();
98886
-
98887
- return await repository.save(entity, options);
98888
- }
98889
-
98890
- public async createMany(entities: BookEntity[], options?: SaveOptions): Promise<BookEntity[]> {
98891
- const repository = await this.open();
98892
-
98893
- return await repository.save(entities, options);
98894
- }
98895
-
98896
- public async update(entity: Partial<BookEntity> & { id: string }): Promise<UpdateResult> {
98897
- const repository = await this.open();
98898
-
98899
- return await repository.update(entity.id, entity);
98900
- }
98901
-
98902
- public async updateMany(entities: (Partial<BookEntity> & { id: string })[]): Promise<UpdateResult[]> {
98903
- const repository = await this.open();
98904
-
98905
- return await Promise.all(entities.map((entity) => repository.update(entity.id, entity)));
98906
- }
98907
-
98908
- public async delete(
98909
- criteria: FindOptionsWhere<BookEntity> | FindOptionsWhere<BookEntity>[],
98910
- ): Promise<UpdateResult> {
98911
- const repository = await this.open();
98912
-
98913
- return await repository.softDelete(criteria);
98914
- }
98915
-
98916
- public async count(criteria?: FindOptionsWhere<BookEntity> | FindOptionsWhere<BookEntity>[]): Promise<number> {
98917
- const repository = await this.open();
98918
-
98919
- return await repository.count(criteria ? { where: criteria } : {});
98920
- }
98921
- }
98922
- `;
98923
-
98924
- // src/templates/resources/book/controllers/CreateBookController.txt
98925
- var CreateBookController_default = `import type { ContextType } from "@ooneex/controller";
98926
- import { inject } from "@ooneex/container";
98927
- import { ERole } from "@ooneex/role";
98928
- import { Route } from "@ooneex/routing";
98929
- import type { LocaleType } from "@ooneex/translation";
98930
- import { Assert } from "@ooneex/validation";
98931
- import type { BookEntity } from "../entities/BookEntity";
98932
- import { CreateBookService } from "../services/CreateBookService";
98933
-
98934
- type CreateBookRouteType = {
98935
- payload: {
98936
- title: string;
98937
- color?: string;
98938
- description?: string;
98939
- summarize?: string;
98940
- pageCount?: number;
98941
- src: string;
98942
- coverImage?: string;
98943
- tags?: string[];
98944
- topics?: string[];
98945
- categories?: string[];
98946
- lang?: LocaleType;
98947
- status?: string;
98948
- };
98949
- response: BookEntity | Record<string, never>;
98950
- };
98951
-
98952
- @Route.post("/books", {
98953
- name: "book.create",
98954
- version: 1,
98955
- description: "Create a new book",
98956
- payload: Assert({
98957
- title: "string",
98958
- color: "string?",
98959
- description: "string?",
98960
- summarize: "string?",
98961
- pageCount: "number?",
98962
- src: "string",
98963
- coverImage: "string?",
98964
- tags: "string[]?",
98965
- topics: "string[]?",
98966
- categories: "string[]?",
98967
- lang: "string?",
98968
- status: "string?",
98969
- }),
98970
- response: Assert({
98971
- id: "string",
98972
- title: "string",
98973
- color: "string?",
98974
- description: "string?",
98975
- summarize: "string?",
98976
- pageCount: "number?",
98977
- src: "string",
98978
- coverImage: "string?",
98979
- tags: "string[]?",
98980
- topics: "string[]?",
98981
- categories: "string[]?",
98982
- lang: "string?",
98983
- status: "string?",
98984
- }),
98985
- roles: ["ROLE_USER"],
98986
- })
98987
- export class CreateBookController {
98988
- constructor(
98989
- @inject(CreateBookService) private readonly service: CreateBookService,
98990
- ) {}
98991
-
98992
- public async index(context: ContextType<CreateBookRouteType>) {
98993
- const { title, color, description, summarize, pageCount, src, coverImage, tags, topics, categories, lang, status } = context.payload;
98994
-
98995
- const book = await this.service.execute({ title, color, description, summarize, pageCount, src, coverImage, tags, topics, categories, lang, status });
98996
-
98997
- return context.response.json(book || {});
98998
- }
98999
- }
99000
- `;
99001
-
99002
- // src/templates/resources/book/controllers/DeleteBookController.txt
99003
- var DeleteBookController_default = `import type { ContextType } from "@ooneex/controller";
99004
- import { inject } from "@ooneex/container";
99005
- import { ERole } from "@ooneex/role";
99006
- import { Route } from "@ooneex/routing";
99007
- import { Assert } from "@ooneex/validation";
99008
- import { DeleteBookService } from "../services/DeleteBookService";
99009
-
99010
- type DeleteBookRouteType = {
99011
- params: { id: string };
99012
- response: { id: string };
99013
- };
99014
-
99015
- @Route.delete("/books/:id", {
99016
- name: "book.delete",
99017
- version: 1,
99018
- description: "Delete a book",
99019
- params: {
99020
- id: Assert("string"),
99021
- },
99022
- response: Assert({
99023
- id: "string",
99024
- }),
99025
- roles: ["ROLE_USER"],
99026
- })
99027
- export class DeleteBookController {
99028
- constructor(
99029
- @inject(DeleteBookService) private readonly service: DeleteBookService,
99030
- ) {}
99031
-
99032
- public async index(context: ContextType<DeleteBookRouteType>) {
99033
- const { id } = context.params;
99034
-
99035
- await this.service.execute({ id });
99036
-
99037
- return context.response.json({ id });
99038
- }
99039
- }
99040
- `;
99041
-
99042
- // src/templates/resources/book/controllers/GetBookController.txt
99043
- var GetBookController_default = `import type { ContextType } from "@ooneex/controller";
99044
- import { inject } from "@ooneex/container";
99045
- import { ERole } from "@ooneex/role";
99046
- import { Route } from "@ooneex/routing";
99047
- import { Assert } from "@ooneex/validation";
99048
- import type { BookEntity } from "../entities/BookEntity";
99049
- import { GetBookService } from "../services/GetBookService";
99050
-
99051
- type GetBookRouteType = {
99052
- params: { id: string };
99053
- response: BookEntity | Record<string, never>;
99054
- };
99055
-
99056
- @Route.get("/books/:id", {
99057
- name: "book.get",
99058
- version: 1,
99059
- description: "Get a book by id",
99060
- params: {
99061
- id: Assert("string"),
99062
- },
99063
- roles: ["ROLE_USER"],
99064
- })
99065
- export class GetBookController {
99066
- constructor(
99067
- @inject(GetBookService) private readonly service: GetBookService,
99068
- ) {}
99069
-
99070
- public async index(context: ContextType<GetBookRouteType>) {
99071
- const { id } = context.params;
99072
-
99073
- const book = await this.service.execute({ id });
99074
-
99075
- return context.response.json(book || {});
99076
- }
99077
- }
99078
- `;
99079
-
99080
- // src/templates/resources/book/controllers/ListBooksController.txt
99081
- var ListBooksController_default = `import type { ContextType } from "@ooneex/controller";
99082
- import { inject } from "@ooneex/container";
99083
- import { ERole } from "@ooneex/role";
99084
- import { Route } from "@ooneex/routing";
99085
- import type { FilterResultType } from "@ooneex/types";
99086
- import { Assert } from "@ooneex/validation";
99087
- import type { BookEntity } from "../entities/BookEntity";
99088
- import { ListBooksService } from "../services/ListBooksService";
99089
-
99090
- type ListBooksRouteType = {
99091
- queries: { page?: number; limit?: number; q?: string };
99092
- response: FilterResultType<BookEntity>;
99093
- };
99094
-
99095
- @Route.get("/books", {
99096
- name: "book.list",
99097
- version: 1,
99098
- description: "List all books",
99099
- queries: Assert({
99100
- page: "number?",
99101
- limit: "number?",
99102
- q: "string?",
99103
- }),
99104
- roles: ["ROLE_USER"],
99105
- })
99106
- export class ListBooksController {
99107
- constructor(
99108
- @inject(ListBooksService) private readonly service: ListBooksService,
99109
- ) {}
99110
-
99111
- public async index(context: ContextType<ListBooksRouteType>) {
99112
- const { page, limit, q } = context.queries;
99113
-
99114
- const result = await this.service.execute({ page, limit, q });
99115
-
99116
- return context.response.json(result);
99117
- }
99118
- }
99119
- `;
99120
-
99121
- // src/templates/resources/book/controllers/UpdateBookController.txt
99122
- var UpdateBookController_default = `import type { ContextType } from "@ooneex/controller";
99123
- import { inject } from "@ooneex/container";
99124
- import { ERole } from "@ooneex/role";
99125
- import { Route } from "@ooneex/routing";
99126
- import type { LocaleType } from "@ooneex/translation";
99127
- import { Assert } from "@ooneex/validation";
99128
- import type { BookEntity } from "../entities/BookEntity";
99129
- import { UpdateBookService } from "../services/UpdateBookService";
99130
-
99131
- type UpdateBookRouteType = {
99132
- params: { id: string };
99133
- payload: {
99134
- title?: string;
99135
- color?: string;
99136
- description?: string;
99137
- summarize?: string;
99138
- pageCount?: number;
99139
- src?: string;
99140
- coverImage?: string;
99141
- tags?: string[];
99142
- topics?: string[];
99143
- categories?: string[];
99144
- lang?: LocaleType;
99145
- status?: string;
99146
- };
99147
- response: BookEntity | Record<string, never>;
99148
- };
99149
-
99150
- @Route.patch("/books/:id", {
99151
- name: "book.update",
99152
- version: 1,
99153
- description: "Update a book",
99154
- params: {
99155
- id: Assert("string"),
99156
- },
99157
- payload: Assert({
99158
- title: "string?",
99159
- color: "string?",
99160
- description: "string?",
99161
- summarize: "string?",
99162
- pageCount: "number?",
99163
- src: "string?",
99164
- coverImage: "string?",
99165
- tags: "string[]?",
99166
- topics: "string[]?",
99167
- categories: "string[]?",
99168
- lang: "string?",
99169
- status: "string?",
99170
- }),
99171
- response: Assert({
99172
- id: "string",
99173
- title: "string",
99174
- color: "string?",
99175
- description: "string?",
99176
- summarize: "string?",
99177
- pageCount: "number?",
99178
- src: "string",
99179
- coverImage: "string?",
99180
- tags: "string[]?",
99181
- topics: "string[]?",
99182
- categories: "string[]?",
99183
- lang: "string?",
99184
- status: "string?",
99185
- }),
99186
- roles: ["ROLE_USER"],
99187
- })
99188
- export class UpdateBookController {
99189
- constructor(
99190
- @inject(UpdateBookService) private readonly service: UpdateBookService,
99191
- ) {}
99192
-
99193
- public async index(context: ContextType<UpdateBookRouteType>) {
99194
- const { id } = context.params;
99195
- const { title, color, description, summarize, pageCount, src, coverImage, tags, topics, categories, lang, status } = context.payload;
99196
-
99197
- const book = await this.service.execute({ id, title, color, description, summarize, pageCount, src, coverImage, tags, topics, categories, lang, status });
99198
-
99199
- return context.response.json(book || {});
99200
- }
99201
- }
99202
- `;
99203
-
99204
- // src/templates/resources/book/services/CreateBookService.txt
99205
- var CreateBookService_default = `import { inject } from "@ooneex/container";
99206
- import { decorator } from "@ooneex/service";
99207
- import type { IService } from "@ooneex/service";
99208
- import type { LocaleType } from "@ooneex/translation";
99209
- import { BookEntity } from "../entities/BookEntity";
99210
- import { BookRepository } from "../repositories/BookRepository";
99211
-
99212
- type ServiceDataType = {
99213
- title: string;
99214
- color?: string;
99215
- description?: string;
99216
- summarize?: string;
99217
- pageCount?: number;
99218
- src: string;
99219
- coverImage?: string;
99220
- tags?: string[];
99221
- topics?: string[];
99222
- categories?: string[];
99223
- lang?: LocaleType;
99224
- status?: string;
99225
- };
99226
-
99227
- @decorator.service()
99228
- export class CreateBookService implements IService {
99229
- constructor(
99230
- @inject(BookRepository) private readonly repository: BookRepository,
99231
- ) {}
99232
-
99233
- public async execute(data?: ServiceDataType): Promise<BookEntity | null> {
99234
- if (!data) return null;
99235
-
99236
- const book = new BookEntity();
99237
- book.title = data.title;
99238
- book.color = data.color;
99239
- book.description = data.description;
99240
- book.summarize = data.summarize;
99241
- book.pageCount = data.pageCount;
99242
- book.src = data.src;
99243
- book.coverImage = data.coverImage;
99244
- book.tags = data.tags;
99245
- book.topics = data.topics;
99246
- book.categories = data.categories;
99247
- book.lang = data.lang;
99248
- book.status = data.status;
99249
-
99250
- return await this.repository.create(book);
99251
- }
99252
- }
99253
- `;
99254
-
99255
- // src/templates/resources/book/services/DeleteBookService.txt
99256
- var DeleteBookService_default = `import { inject } from "@ooneex/container";
99257
- import { decorator } from "@ooneex/service";
99258
- import type { IService } from "@ooneex/service";
99259
- import { BookRepository } from "../repositories/BookRepository";
99260
-
99261
- type ServiceDataType = {
99262
- id: string;
99263
- };
99264
-
99265
- @decorator.service()
99266
- export class DeleteBookService implements IService {
99267
- constructor(
99268
- @inject(BookRepository) private readonly repository: BookRepository,
99269
- ) {}
99270
-
99271
- public async execute(data?: ServiceDataType): Promise<void> {
99272
- if (!data) return;
99273
-
99274
- await this.repository.delete({ id: data.id });
99275
- }
99276
- }
99277
- `;
99278
-
99279
- // src/templates/resources/book/services/GetBookService.txt
99280
- var GetBookService_default = `import { inject } from "@ooneex/container";
99281
- import { decorator } from "@ooneex/service";
99282
- import type { IService } from "@ooneex/service";
99283
- import { BookEntity } from "../entities/BookEntity";
99284
- import { BookRepository } from "../repositories/BookRepository";
99285
-
99286
- type ServiceDataType = {
99287
- id: string;
99288
- };
99289
-
99290
- @decorator.service()
99291
- export class GetBookService implements IService {
99292
- constructor(
99293
- @inject(BookRepository) private readonly repository: BookRepository,
99294
- ) {}
99295
-
99296
- public async execute(data?: ServiceDataType): Promise<BookEntity | null> {
99297
- if (!data) return null;
99298
-
99299
- return await this.repository.findOne(data.id);
99300
- }
99301
- }
99302
- `;
99303
-
99304
- // src/templates/resources/book/services/ListBooksService.txt
99305
- var ListBooksService_default = `import { inject } from "@ooneex/container";
99306
- import { decorator } from "@ooneex/service";
99307
- import type { IService } from "@ooneex/service";
99308
- import type { FilterResultType } from "@ooneex/types";
99309
- import { BookEntity } from "../entities/BookEntity";
99310
- import { BookRepository } from "../repositories/BookRepository";
99311
-
99312
- type ServiceDataType = {
99313
- page?: number;
99314
- limit?: number;
99315
- q?: string;
99316
- };
99317
-
99318
- @decorator.service()
99319
- export class ListBooksService implements IService {
99320
- constructor(
99321
- @inject(BookRepository) private readonly repository: BookRepository,
99322
- ) {}
99323
-
99324
- public async execute(data?: ServiceDataType): Promise<FilterResultType<BookEntity>> {
99325
- return await this.repository.find({
99326
- page: data?.page,
99327
- limit: data?.limit,
99328
- q: data?.q,
99329
- });
99330
- }
99331
- }
99332
- `;
99333
-
99334
- // src/templates/resources/book/services/UpdateBookService.txt
99335
- var UpdateBookService_default = `import { inject } from "@ooneex/container";
99336
- import { decorator } from "@ooneex/service";
99337
- import type { IService } from "@ooneex/service";
99338
- import type { LocaleType } from "@ooneex/translation";
99339
- import { BookEntity } from "../entities/BookEntity";
99340
- import { BookRepository } from "../repositories/BookRepository";
99341
-
99342
- type ServiceDataType = {
99343
- id: string;
99344
- title?: string;
99345
- color?: string;
99346
- description?: string;
99347
- summarize?: string;
99348
- pageCount?: number;
99349
- src?: string;
99350
- coverImage?: string;
99351
- tags?: string[];
99352
- topics?: string[];
99353
- categories?: string[];
99354
- lang?: LocaleType;
99355
- status?: string;
99356
- };
99357
-
99358
- @decorator.service()
99359
- export class UpdateBookService implements IService {
99360
- constructor(
99361
- @inject(BookRepository) private readonly repository: BookRepository,
99362
- ) {}
99363
-
99364
- public async execute(data?: ServiceDataType): Promise<BookEntity | null> {
99365
- if (!data) return null;
99366
-
99367
- const { id, ...fields } = data;
99368
-
99369
- await this.repository.update({ id, ...fields });
99370
-
99371
- return await this.repository.findOne(id);
99372
- }
99373
- }
99374
- `;
99375
-
99376
- // src/commands/MigrationCreateCommand.ts
98770
+ // src/commands/MakeServiceCommand.ts
99377
98771
  import { join as join30 } from "path";
99378
98772
  import { decorator as decorator30 } from "@ooneex/command";
99379
98773
  import { TerminalLogger as TerminalLogger29 } from "@ooneex/logger";
99380
- import { migrationCreate } from "@ooneex/migrations";
99381
-
99382
- // src/templates/module/migration.up.txt
99383
- var migration_up_default = `#!/usr/bin/env bun
99384
-
99385
- import { up } from "@ooneex/migrations";
99386
- import "@module/{{name}}/migrations/migrations";
99387
-
99388
- await up({
99389
- databaseUrl: Bun.env.DATABASE_URL,
99390
- tableName: "migrations",
99391
- });
99392
- `;
99393
-
99394
- // src/commands/MigrationCreateCommand.ts
99395
- class MigrationCreateCommand {
99396
- getName() {
99397
- return "migration:create";
99398
- }
99399
- getDescription() {
99400
- return "Generate a new migration file";
99401
- }
99402
- async run(options) {
99403
- const { module = "shared" } = options;
99404
- await ensureModule(module);
99405
- const base = join30("modules", module);
99406
- const { migrationPath: filePath } = await migrationCreate({
99407
- migrationsDir: join30(base, "src", "migrations"),
99408
- testsDir: join30(base, "tests", "migrations"),
99409
- module
99410
- });
99411
- const binMigrationUpPath = join30(process.cwd(), base, "bin", "migration", "up.ts");
99412
- const binMigrationUpFile = Bun.file(binMigrationUpPath);
99413
- if (!await binMigrationUpFile.exists()) {
99414
- await Bun.write(binMigrationUpPath, migration_up_default.replace(/{{name}}/g, module));
99415
- }
99416
- const logger = new TerminalLogger29;
99417
- logger.success(`${filePath} created successfully`, undefined, {
99418
- showTimestamp: false,
99419
- showArrow: false,
99420
- useSymbol: true
99421
- });
99422
- }
99423
- }
99424
- MigrationCreateCommand = __legacyDecorateClassTS([
99425
- decorator30.command()
99426
- ], MigrationCreateCommand);
99427
-
99428
- // src/commands/MakeServiceCommand.ts
99429
- import { join as join31 } from "path";
99430
- import { decorator as decorator31 } from "@ooneex/command";
99431
- import { TerminalLogger as TerminalLogger30 } from "@ooneex/logger";
99432
98774
  import { toPascalCase as toPascalCase15 } from "@ooneex/utils";
99433
98775
 
99434
98776
  // src/templates/service.test.txt
@@ -99478,28 +98820,28 @@ class MakeServiceCommand {
99478
98820
  name = toPascalCase15(name).replace(/Service$/, "");
99479
98821
  const content = service_default.replace(/{{NAME}}/g, name);
99480
98822
  await ensureModule(module);
99481
- const base = join31("modules", module);
99482
- const serviceLocalDir = join31(base, "src", "services");
99483
- const serviceDir = join31(process.cwd(), serviceLocalDir);
99484
- const filePath = join31(serviceDir, `${name}Service.ts`);
98823
+ const base = join30("modules", module);
98824
+ const serviceLocalDir = join30(base, "src", "services");
98825
+ const serviceDir = join30(process.cwd(), serviceLocalDir);
98826
+ const filePath = join30(serviceDir, `${name}Service.ts`);
99485
98827
  await Bun.write(filePath, content);
99486
98828
  const testContent = service_test_default.replace(/{{NAME}}/g, name).replace(/{{MODULE}}/g, module);
99487
- const testsLocalDir = join31(base, "tests", "services");
99488
- const testsDir = join31(process.cwd(), testsLocalDir);
99489
- const testFilePath = join31(testsDir, `${name}Service.spec.ts`);
98829
+ const testsLocalDir = join30(base, "tests", "services");
98830
+ const testsDir = join30(process.cwd(), testsLocalDir);
98831
+ const testFilePath = join30(testsDir, `${name}Service.spec.ts`);
99490
98832
  await Bun.write(testFilePath, testContent);
99491
- const logger = new TerminalLogger30;
99492
- logger.success(`${join31(serviceLocalDir, name)}Service.ts created successfully`, undefined, {
98833
+ const logger = new TerminalLogger29;
98834
+ logger.success(`${join30(serviceLocalDir, name)}Service.ts created successfully`, undefined, {
99493
98835
  showTimestamp: false,
99494
98836
  showArrow: false,
99495
98837
  useSymbol: true
99496
98838
  });
99497
- logger.success(`${join31(testsLocalDir, name)}Service.spec.ts created successfully`, undefined, {
98839
+ logger.success(`${join30(testsLocalDir, name)}Service.spec.ts created successfully`, undefined, {
99498
98840
  showTimestamp: false,
99499
98841
  showArrow: false,
99500
98842
  useSymbol: true
99501
98843
  });
99502
- const packageJsonPath = join31(process.cwd(), "package.json");
98844
+ const packageJsonPath = join30(process.cwd(), "package.json");
99503
98845
  const packageJson = await Bun.file(packageJsonPath).json();
99504
98846
  const deps = packageJson.dependencies ?? {};
99505
98847
  const devDeps = packageJson.devDependencies ?? {};
@@ -99514,79 +98856,12 @@ class MakeServiceCommand {
99514
98856
  }
99515
98857
  }
99516
98858
  MakeServiceCommand = __legacyDecorateClassTS([
99517
- decorator31.command()
98859
+ decorator30.command()
99518
98860
  ], MakeServiceCommand);
99519
-
99520
- // src/commands/MakeResourceBookCommand.ts
99521
- class MakeResourceBookCommand {
99522
- getName() {
99523
- return "make:resource:book";
99524
- }
99525
- getDescription() {
99526
- return "Generate book resource (entity, migration, repository)";
99527
- }
99528
- async run() {
99529
- const module = "book";
99530
- const base = join32("modules", module);
99531
- const makeModuleCommand = new ModuleCreateCommand;
99532
- await makeModuleCommand.run({ name: module, silent: true });
99533
- const makeEntityCommand = new MakeEntityCommand;
99534
- await makeEntityCommand.run({ name: "Book", module, tableName: "books" });
99535
- const makeMigrationCommand = new MigrationCreateCommand;
99536
- await makeMigrationCommand.run({ module });
99537
- const makeRepositoryCommand = new MakeRepositoryCommand;
99538
- await makeRepositoryCommand.run({ name: "Book", module });
99539
- const makeControllerCommand = new MakeControllerCommand;
99540
- const controllers = [
99541
- { name: "CreateBook", route: { name: "book.create", path: "/books", method: "POST" } },
99542
- { name: "GetBook", route: { name: "book.get", path: "/books/:id", method: "GET" } },
99543
- { name: "ListBooks", route: { name: "book.list", path: "/books", method: "GET" } },
99544
- { name: "UpdateBook", route: { name: "book.update", path: "/books/:id", method: "PATCH" } },
99545
- { name: "DeleteBook", route: { name: "book.delete", path: "/books/:id", method: "DELETE" } }
99546
- ];
99547
- for (const controller of controllers) {
99548
- await makeControllerCommand.run({ ...controller, module, isSocket: false });
99549
- }
99550
- const controllersDir = join32(process.cwd(), base, "src", "controllers");
99551
- await Bun.write(join32(controllersDir, "CreateBookController.ts"), CreateBookController_default);
99552
- await Bun.write(join32(controllersDir, "GetBookController.ts"), GetBookController_default);
99553
- await Bun.write(join32(controllersDir, "ListBooksController.ts"), ListBooksController_default);
99554
- await Bun.write(join32(controllersDir, "UpdateBookController.ts"), UpdateBookController_default);
99555
- await Bun.write(join32(controllersDir, "DeleteBookController.ts"), DeleteBookController_default);
99556
- const makeServiceCommand = new MakeServiceCommand;
99557
- const services = ["CreateBook", "GetBook", "ListBooks", "UpdateBook", "DeleteBook"];
99558
- for (const name of services) {
99559
- await makeServiceCommand.run({ name, module });
99560
- }
99561
- const servicesDir = join32(process.cwd(), base, "src", "services");
99562
- await Bun.write(join32(servicesDir, "CreateBookService.ts"), CreateBookService_default);
99563
- await Bun.write(join32(servicesDir, "GetBookService.ts"), GetBookService_default);
99564
- await Bun.write(join32(servicesDir, "ListBooksService.ts"), ListBooksService_default);
99565
- await Bun.write(join32(servicesDir, "UpdateBookService.ts"), UpdateBookService_default);
99566
- await Bun.write(join32(servicesDir, "DeleteBookService.ts"), DeleteBookService_default);
99567
- const entityPath = join32(process.cwd(), base, "src", "entities", "BookEntity.ts");
99568
- await Bun.write(entityPath, BookEntity_default);
99569
- const migrationsDir = join32(process.cwd(), base, "src", "migrations");
99570
- const glob = new Glob("Migration*.ts");
99571
- for await (const file of glob.scan(migrationsDir)) {
99572
- if (file === "migrations.ts")
99573
- continue;
99574
- const name = file.replace(/\.ts$/, "");
99575
- const version = name.replace("Migration", "");
99576
- const content = BookMigration_default.replaceAll("{{ name }}", name).replaceAll("{{ version }}", version);
99577
- await Bun.write(join32(migrationsDir, file), content);
99578
- }
99579
- const repositoryPath = join32(process.cwd(), base, "src", "repositories", "BookRepository.ts");
99580
- await Bun.write(repositoryPath, BookRepository_default);
99581
- }
99582
- }
99583
- MakeResourceBookCommand = __legacyDecorateClassTS([
99584
- decorator32.command()
99585
- ], MakeResourceBookCommand);
99586
98861
  // src/commands/MakeStorageCommand.ts
99587
- import { join as join33 } from "path";
99588
- import { decorator as decorator33 } from "@ooneex/command";
99589
- import { TerminalLogger as TerminalLogger31 } from "@ooneex/logger";
98862
+ import { join as join31 } from "path";
98863
+ import { decorator as decorator31 } from "@ooneex/command";
98864
+ import { TerminalLogger as TerminalLogger30 } from "@ooneex/logger";
99590
98865
  import { toPascalCase as toPascalCase16, toSnakeCase as toSnakeCase3 } from "@ooneex/utils";
99591
98866
 
99592
98867
  // src/templates/storage.test.txt
@@ -99611,6 +98886,8 @@ describe("{{NAME}}StorageAdapter", () => {
99611
98886
 
99612
98887
  // src/templates/storage.txt
99613
98888
  var storage_default = `import { Storage, decorator, StorageException } from "@ooneex/storage";
98889
+ import { inject } from "@ooneex/container";
98890
+ import { AppEnv } from "@ooneex/app-env";
99614
98891
  import type { S3Options } from "bun";
99615
98892
 
99616
98893
  @decorator.storage()
@@ -99621,17 +98898,20 @@ export class {{NAME}}Storage extends Storage {
99621
98898
  private readonly endpoint: string;
99622
98899
  private readonly region: string;
99623
98900
 
99624
- constructor(options?: {
99625
- accessKey?: string;
99626
- secretKey?: string;
99627
- endpoint?: string;
99628
- region?: string;
99629
- }) {
98901
+ public constructor(
98902
+ @inject(AppEnv) private readonly env: AppEnv,
98903
+ options?: {
98904
+ accessKey?: string;
98905
+ secretKey?: string;
98906
+ endpoint?: string;
98907
+ region?: string;
98908
+ },
98909
+ ) {
99630
98910
  super();
99631
98911
 
99632
- const accessKey = options?.accessKey || Bun.env.STORAGE_{{NAME_UPPER}}_ACCESS_KEY;
99633
- const secretKey = options?.secretKey || Bun.env.STORAGE_{{NAME_UPPER}}_SECRET_KEY;
99634
- const endpoint = options?.endpoint || Bun.env.STORAGE_{{NAME_UPPER}}_ENDPOINT;
98912
+ const accessKey = options?.accessKey || this.env.STORAGE_{{NAME_UPPER}}_ACCESS_KEY;
98913
+ const secretKey = options?.secretKey || this.env.STORAGE_{{NAME_UPPER}}_SECRET_KEY;
98914
+ const endpoint = options?.endpoint || this.env.STORAGE_{{NAME_UPPER}}_ENDPOINT;
99635
98915
 
99636
98916
  if (!accessKey) {
99637
98917
  throw new StorageException(
@@ -99652,7 +98932,7 @@ export class {{NAME}}Storage extends Storage {
99652
98932
  this.accessKey = accessKey;
99653
98933
  this.secretKey = secretKey;
99654
98934
  this.endpoint = endpoint;
99655
- this.region = options?.region || Bun.env.STORAGE_{{NAME_UPPER}}_REGION || "auto";
98935
+ this.region = options?.region || this.env.STORAGE_{{NAME_UPPER}}_REGION || "auto";
99656
98936
  }
99657
98937
 
99658
98938
  public getOptions(): S3Options {
@@ -99684,28 +98964,28 @@ class MakeStorageCommand {
99684
98964
  const nameUpper = toSnakeCase3(name).toUpperCase();
99685
98965
  const content = storage_default.replace(/{{NAME}}/g, name).replace(/{{NAME_UPPER}}/g, nameUpper);
99686
98966
  await ensureModule(module);
99687
- const base = join33("modules", module);
99688
- const storageLocalDir = join33(base, "src", "storage");
99689
- const storageDir = join33(process.cwd(), storageLocalDir);
99690
- const filePath = join33(storageDir, `${name}Storage.ts`);
98967
+ const base = join31("modules", module);
98968
+ const storageLocalDir = join31(base, "src", "storage");
98969
+ const storageDir = join31(process.cwd(), storageLocalDir);
98970
+ const filePath = join31(storageDir, `${name}Storage.ts`);
99691
98971
  await Bun.write(filePath, content);
99692
98972
  const testContent = storage_test_default.replace(/{{NAME}}/g, name).replace(/{{MODULE}}/g, module);
99693
- const testsLocalDir = join33(base, "tests", "storage");
99694
- const testsDir = join33(process.cwd(), testsLocalDir);
99695
- const testFilePath = join33(testsDir, `${name}Storage.spec.ts`);
98973
+ const testsLocalDir = join31(base, "tests", "storage");
98974
+ const testsDir = join31(process.cwd(), testsLocalDir);
98975
+ const testFilePath = join31(testsDir, `${name}Storage.spec.ts`);
99696
98976
  await Bun.write(testFilePath, testContent);
99697
- const logger = new TerminalLogger31;
99698
- logger.success(`${join33(storageLocalDir, name)}Storage.ts created successfully`, undefined, {
98977
+ const logger = new TerminalLogger30;
98978
+ logger.success(`${join31(storageLocalDir, name)}Storage.ts created successfully`, undefined, {
99699
98979
  showTimestamp: false,
99700
98980
  showArrow: false,
99701
98981
  useSymbol: true
99702
98982
  });
99703
- logger.success(`${join33(testsLocalDir, name)}Storage.spec.ts created successfully`, undefined, {
98983
+ logger.success(`${join31(testsLocalDir, name)}Storage.spec.ts created successfully`, undefined, {
99704
98984
  showTimestamp: false,
99705
98985
  showArrow: false,
99706
98986
  useSymbol: true
99707
98987
  });
99708
- const packageJsonPath = join33(process.cwd(), "package.json");
98988
+ const packageJsonPath = join31(process.cwd(), "package.json");
99709
98989
  const packageJson = await Bun.file(packageJsonPath).json();
99710
98990
  const deps = packageJson.dependencies ?? {};
99711
98991
  const devDeps = packageJson.devDependencies ?? {};
@@ -99720,12 +99000,12 @@ class MakeStorageCommand {
99720
99000
  }
99721
99001
  }
99722
99002
  MakeStorageCommand = __legacyDecorateClassTS([
99723
- decorator33.command()
99003
+ decorator31.command()
99724
99004
  ], MakeStorageCommand);
99725
99005
  // src/commands/MakeVectorDatabaseCommand.ts
99726
- import { join as join34 } from "path";
99727
- import { decorator as decorator34 } from "@ooneex/command";
99728
- import { TerminalLogger as TerminalLogger32 } from "@ooneex/logger";
99006
+ import { join as join32 } from "path";
99007
+ import { decorator as decorator32 } from "@ooneex/command";
99008
+ import { TerminalLogger as TerminalLogger31 } from "@ooneex/logger";
99729
99009
  import { toPascalCase as toPascalCase17 } from "@ooneex/utils";
99730
99010
 
99731
99011
  // src/templates/vector-database.test.txt
@@ -99797,28 +99077,28 @@ class MakeVectorDatabaseCommand {
99797
99077
  name = toPascalCase17(name).replace(/VectorDatabase$/, "").replace(/Database$/, "");
99798
99078
  const content = vector_database_default.replace(/{{NAME}}/g, name);
99799
99079
  await ensureModule(module);
99800
- const base = join34("modules", module);
99801
- const vectorDatabaseLocalDir = join34(base, "src", "databases");
99802
- const vectorDatabaseDir = join34(process.cwd(), vectorDatabaseLocalDir);
99803
- const filePath = join34(vectorDatabaseDir, `${name}VectorDatabase.ts`);
99080
+ const base = join32("modules", module);
99081
+ const vectorDatabaseLocalDir = join32(base, "src", "databases");
99082
+ const vectorDatabaseDir = join32(process.cwd(), vectorDatabaseLocalDir);
99083
+ const filePath = join32(vectorDatabaseDir, `${name}VectorDatabase.ts`);
99804
99084
  await Bun.write(filePath, content);
99805
99085
  const testContent = vector_database_test_default.replace(/{{NAME}}/g, name).replace(/{{MODULE}}/g, module);
99806
- const testsLocalDir = join34(base, "tests", "databases");
99807
- const testsDir = join34(process.cwd(), testsLocalDir);
99808
- const testFilePath = join34(testsDir, `${name}VectorDatabase.spec.ts`);
99086
+ const testsLocalDir = join32(base, "tests", "databases");
99087
+ const testsDir = join32(process.cwd(), testsLocalDir);
99088
+ const testFilePath = join32(testsDir, `${name}VectorDatabase.spec.ts`);
99809
99089
  await Bun.write(testFilePath, testContent);
99810
- const logger = new TerminalLogger32;
99811
- logger.success(`${join34(vectorDatabaseLocalDir, name)}VectorDatabase.ts created successfully`, undefined, {
99090
+ const logger = new TerminalLogger31;
99091
+ logger.success(`${join32(vectorDatabaseLocalDir, name)}VectorDatabase.ts created successfully`, undefined, {
99812
99092
  showTimestamp: false,
99813
99093
  showArrow: false,
99814
99094
  useSymbol: true
99815
99095
  });
99816
- logger.success(`${join34(testsLocalDir, name)}VectorDatabase.spec.ts created successfully`, undefined, {
99096
+ logger.success(`${join32(testsLocalDir, name)}VectorDatabase.spec.ts created successfully`, undefined, {
99817
99097
  showTimestamp: false,
99818
99098
  showArrow: false,
99819
99099
  useSymbol: true
99820
99100
  });
99821
- const packageJsonPath = join34(process.cwd(), "package.json");
99101
+ const packageJsonPath = join32(process.cwd(), "package.json");
99822
99102
  const packageJson = await Bun.file(packageJsonPath).json();
99823
99103
  const deps = packageJson.dependencies ?? {};
99824
99104
  const devDeps = packageJson.devDependencies ?? {};
@@ -99833,12 +99113,66 @@ class MakeVectorDatabaseCommand {
99833
99113
  }
99834
99114
  }
99835
99115
  MakeVectorDatabaseCommand = __legacyDecorateClassTS([
99836
- decorator34.command()
99116
+ decorator32.command()
99837
99117
  ], MakeVectorDatabaseCommand);
99118
+ // src/commands/MigrationCreateCommand.ts
99119
+ import { join as join33 } from "path";
99120
+ import { decorator as decorator33 } from "@ooneex/command";
99121
+ import { TerminalLogger as TerminalLogger32 } from "@ooneex/logger";
99122
+ import { migrationCreate } from "@ooneex/migrations";
99123
+
99124
+ // src/templates/module/migration.up.txt
99125
+ var migration_up_default = `#!/usr/bin/env bun
99126
+
99127
+ import { up } from "@ooneex/migrations";
99128
+ import { AppEnv } from "@ooneex/app-env";
99129
+ import "@module/{{name}}/migrations/migrations";
99130
+
99131
+ const env = new AppEnv();
99132
+
99133
+ await up({
99134
+ databaseUrl: env.DATABASE_URL,
99135
+ tableName: "migrations",
99136
+ });
99137
+ `;
99138
+
99139
+ // src/commands/MigrationCreateCommand.ts
99140
+ class MigrationCreateCommand {
99141
+ getName() {
99142
+ return "migration:create";
99143
+ }
99144
+ getDescription() {
99145
+ return "Generate a new migration file";
99146
+ }
99147
+ async run(options) {
99148
+ const { module = "shared" } = options;
99149
+ await ensureModule(module);
99150
+ const base = join33("modules", module);
99151
+ const { migrationPath: filePath } = await migrationCreate({
99152
+ migrationsDir: join33(base, "src", "migrations"),
99153
+ testsDir: join33(base, "tests", "migrations"),
99154
+ module
99155
+ });
99156
+ const binMigrationUpPath = join33(process.cwd(), base, "bin", "migration", "up.ts");
99157
+ const binMigrationUpFile = Bun.file(binMigrationUpPath);
99158
+ if (!await binMigrationUpFile.exists()) {
99159
+ await Bun.write(binMigrationUpPath, migration_up_default.replace(/{{name}}/g, module));
99160
+ }
99161
+ const logger = new TerminalLogger32;
99162
+ logger.success(`${filePath} created successfully`, undefined, {
99163
+ showTimestamp: false,
99164
+ showArrow: false,
99165
+ useSymbol: true
99166
+ });
99167
+ }
99168
+ }
99169
+ MigrationCreateCommand = __legacyDecorateClassTS([
99170
+ decorator33.command()
99171
+ ], MigrationCreateCommand);
99838
99172
  // src/commands/MigrationUpCommand.ts
99839
99173
  import { existsSync as existsSync2 } from "fs";
99840
- import { join as join35 } from "path";
99841
- import { decorator as decorator35 } from "@ooneex/command";
99174
+ import { join as join34 } from "path";
99175
+ import { decorator as decorator34 } from "@ooneex/command";
99842
99176
  import { TerminalLogger as TerminalLogger33 } from "@ooneex/logger";
99843
99177
  class MigrationUpCommand {
99844
99178
  getName() {
@@ -99849,7 +99183,7 @@ class MigrationUpCommand {
99849
99183
  }
99850
99184
  async run(options) {
99851
99185
  const logger = new TerminalLogger33;
99852
- const modulesDir = join35(process.cwd(), "modules");
99186
+ const modulesDir = join34(process.cwd(), "modules");
99853
99187
  if (!existsSync2(modulesDir)) {
99854
99188
  logger.warn("No modules with migrations found", undefined, {
99855
99189
  showTimestamp: false,
@@ -99862,10 +99196,10 @@ class MigrationUpCommand {
99862
99196
  const modules = [];
99863
99197
  for await (const match of glob.scan({ cwd: modulesDir, onlyFiles: true })) {
99864
99198
  const entry = match.replace("/package.json", "");
99865
- const moduleDir = join35(modulesDir, entry);
99866
- const migrationUpFile = Bun.file(join35(moduleDir, "bin", "migration", "up.ts"));
99199
+ const moduleDir = join34(modulesDir, entry);
99200
+ const migrationUpFile = Bun.file(join34(moduleDir, "bin", "migration", "up.ts"));
99867
99201
  if (await migrationUpFile.exists()) {
99868
- const packageJson = await Bun.file(join35(modulesDir, match)).json();
99202
+ const packageJson = await Bun.file(join34(modulesDir, match)).json();
99869
99203
  modules.push({ name: packageJson.name ?? entry, dir: moduleDir });
99870
99204
  }
99871
99205
  }
@@ -99878,7 +99212,7 @@ class MigrationUpCommand {
99878
99212
  return;
99879
99213
  }
99880
99214
  for (const { name, dir } of modules) {
99881
- const migrationUpPath = join35(dir, "bin", "migration", "up.ts");
99215
+ const migrationUpPath = join34(dir, "bin", "migration", "up.ts");
99882
99216
  logger.info(`Running migrations for ${name}...`, undefined, {
99883
99217
  showTimestamp: false,
99884
99218
  showArrow: false,
@@ -99891,7 +99225,7 @@ class MigrationUpCommand {
99891
99225
  const proc = Bun.spawn(args, {
99892
99226
  cwd: dir,
99893
99227
  stdout: "inherit",
99894
- stderr: "inherit"
99228
+ stderr: "pipe"
99895
99229
  });
99896
99230
  const exitCode = await proc.exited;
99897
99231
  if (exitCode === 0) {
@@ -99901,7 +99235,9 @@ class MigrationUpCommand {
99901
99235
  useSymbol: true
99902
99236
  });
99903
99237
  } else {
99904
- logger.error(`Migrations failed for ${name} (exit code: ${exitCode})`, undefined, {
99238
+ const errorOutput = await new Response(proc.stderr).text();
99239
+ const trimmed = errorOutput.trim();
99240
+ logger.error(`Migrations failed for ${name} (exit code: ${exitCode})`, trimmed ? { message: trimmed } : undefined, {
99905
99241
  showTimestamp: false,
99906
99242
  showArrow: false,
99907
99243
  useSymbol: true
@@ -99911,11 +99247,11 @@ class MigrationUpCommand {
99911
99247
  }
99912
99248
  }
99913
99249
  MigrationUpCommand = __legacyDecorateClassTS([
99914
- decorator35.command()
99250
+ decorator34.command()
99915
99251
  ], MigrationUpCommand);
99916
99252
  // src/commands/ModuleLockCommand.ts
99917
- import { join as join36, relative } from "path";
99918
- import { decorator as decorator36 } from "@ooneex/command";
99253
+ import { join as join35, relative } from "path";
99254
+ import { decorator as decorator35 } from "@ooneex/command";
99919
99255
  import { TerminalLogger as TerminalLogger34 } from "@ooneex/logger";
99920
99256
  import { migrationTestCreate } from "@ooneex/migrations";
99921
99257
  import { seedTestCreate } from "@ooneex/seeds";
@@ -99931,16 +99267,16 @@ class ModuleLockCommand {
99931
99267
  const { module = "shared", override } = options;
99932
99268
  const logger = new TerminalLogger34;
99933
99269
  await ensureModule(module);
99934
- const base = join36("modules", module);
99270
+ const base = join35("modules", module);
99935
99271
  const moduleName = module;
99936
- const ymlPath = join36(process.cwd(), base, `${moduleName}.yml`);
99272
+ const ymlPath = join35(process.cwd(), base, `${moduleName}.yml`);
99937
99273
  if (!await Bun.file(ymlPath).exists()) {
99938
99274
  await Bun.write(ymlPath, yml_default.replace(/{{name}}/g, moduleName));
99939
99275
  }
99940
99276
  const { migrations: registeredMigrations, seeds: registeredSeeds } = await this.parseYml(ymlPath);
99941
99277
  let added = 0;
99942
99278
  let skipped = 0;
99943
- const migrationsDir = join36(process.cwd(), base, "src", "migrations");
99279
+ const migrationsDir = join35(process.cwd(), base, "src", "migrations");
99944
99280
  const migrationFiles = [];
99945
99281
  try {
99946
99282
  const glob = new Bun.Glob("Migration*.ts");
@@ -99950,17 +99286,17 @@ class ModuleLockCommand {
99950
99286
  } catch {}
99951
99287
  for (const file of migrationFiles.sort()) {
99952
99288
  const name = file.replace(/\.ts$/, "");
99953
- const testsDir = join36(base, "tests", "migrations");
99289
+ const testsDir = join35(base, "tests", "migrations");
99954
99290
  await migrationTestCreate({ name, testsDir, ...module && { module } });
99955
99291
  if (registeredMigrations[name] && !override) {
99956
99292
  skipped++;
99957
99293
  continue;
99958
99294
  }
99959
- const content = await Bun.file(join36(migrationsDir, file)).text();
99295
+ const content = await Bun.file(join35(migrationsDir, file)).text();
99960
99296
  registeredMigrations[name] = new Bun.CryptoHasher("sha256").update(content).digest("hex");
99961
99297
  added++;
99962
99298
  }
99963
- const seedsDir = join36(process.cwd(), base, "src", "seeds");
99299
+ const seedsDir = join35(process.cwd(), base, "src", "seeds");
99964
99300
  const seedFiles = [];
99965
99301
  try {
99966
99302
  const glob = new Bun.Glob("*-seed.yml");
@@ -99970,13 +99306,13 @@ class ModuleLockCommand {
99970
99306
  } catch {}
99971
99307
  for (const file of seedFiles.sort()) {
99972
99308
  const name = file.replace(/\.yml$/, "");
99973
- const testsDir = join36(base, "tests", "seeds");
99309
+ const testsDir = join35(base, "tests", "seeds");
99974
99310
  await seedTestCreate({ name: toPascalCase18(name), testsDir, ...module && { module } });
99975
99311
  if (registeredSeeds[name] && !override) {
99976
99312
  skipped++;
99977
99313
  continue;
99978
99314
  }
99979
- const content = await Bun.file(join36(seedsDir, file)).text();
99315
+ const content = await Bun.file(join35(seedsDir, file)).text();
99980
99316
  registeredSeeds[name] = new Bun.CryptoHasher("sha256").update(content).digest("hex");
99981
99317
  added++;
99982
99318
  }
@@ -100043,12 +99379,12 @@ class ModuleLockCommand {
100043
99379
  }
100044
99380
  }
100045
99381
  ModuleLockCommand = __legacyDecorateClassTS([
100046
- decorator36.command()
99382
+ decorator35.command()
100047
99383
  ], ModuleLockCommand);
100048
99384
  // src/commands/ModuleRemoveCommand.ts
100049
99385
  import { rmdir } from "fs/promises";
100050
- import { join as join37 } from "path";
100051
- import { decorator as decorator37 } from "@ooneex/command";
99386
+ import { join as join36 } from "path";
99387
+ import { decorator as decorator36 } from "@ooneex/command";
100052
99388
  import { TerminalLogger as TerminalLogger35 } from "@ooneex/logger";
100053
99389
  import { toKebabCase as toKebabCase6, toPascalCase as toPascalCase19 } from "@ooneex/utils";
100054
99390
  class ModuleRemoveCommand {
@@ -100129,8 +99465,8 @@ class ModuleRemoveCommand {
100129
99465
  }
100130
99466
  return;
100131
99467
  }
100132
- const moduleDir = join37(cwd, "modules", kebabName);
100133
- const moduleDirExists = await Bun.file(join37(moduleDir, "package.json")).exists();
99468
+ const moduleDir = join36(cwd, "modules", kebabName);
99469
+ const moduleDirExists = await Bun.file(join36(moduleDir, "package.json")).exists();
100134
99470
  if (!moduleDirExists) {
100135
99471
  if (!silent) {
100136
99472
  const logger = new TerminalLogger35;
@@ -100150,13 +99486,13 @@ class ModuleRemoveCommand {
100150
99486
  if (!confirmed)
100151
99487
  return;
100152
99488
  }
100153
- const appModulePath = join37(cwd, "modules", "app", "src", "AppModule.ts");
99489
+ const appModulePath = join36(cwd, "modules", "app", "src", "AppModule.ts");
100154
99490
  await this.removeFromAppModule(appModulePath, pascalName, kebabName);
100155
- const sharedModulePath = join37(cwd, "modules", "shared", "src", "SharedModule.ts");
99491
+ const sharedModulePath = join36(cwd, "modules", "shared", "src", "SharedModule.ts");
100156
99492
  await this.removeFromSharedModule(sharedModulePath, pascalName, kebabName);
100157
- const appTsconfigPath = join37(cwd, "tsconfig.json");
99493
+ const appTsconfigPath = join36(cwd, "tsconfig.json");
100158
99494
  await this.removePathAlias(appTsconfigPath, kebabName);
100159
- const commitlintPath = join37(cwd, ".commitlintrc.ts");
99495
+ const commitlintPath = join36(cwd, ".commitlintrc.ts");
100160
99496
  await this.removeModuleScope(commitlintPath, kebabName);
100161
99497
  await rmdir(moduleDir, { recursive: true });
100162
99498
  if (!silent) {
@@ -100170,11 +99506,11 @@ class ModuleRemoveCommand {
100170
99506
  }
100171
99507
  }
100172
99508
  ModuleRemoveCommand = __legacyDecorateClassTS([
100173
- decorator37.command()
99509
+ decorator36.command()
100174
99510
  ], ModuleRemoveCommand);
100175
99511
  // src/commands/SeedCreateCommand.ts
100176
- import { join as join38 } from "path";
100177
- import { decorator as decorator38 } from "@ooneex/command";
99512
+ import { join as join37 } from "path";
99513
+ import { decorator as decorator37 } from "@ooneex/command";
100178
99514
  import { TerminalLogger as TerminalLogger36 } from "@ooneex/logger";
100179
99515
  import { seedCreate } from "@ooneex/seeds";
100180
99516
 
@@ -100212,18 +99548,18 @@ class SeedCreateCommand {
100212
99548
  name = await askName({ message: "Enter seed name" });
100213
99549
  }
100214
99550
  await ensureModule(module);
100215
- const base = join38("modules", module);
99551
+ const base = join37("modules", module);
100216
99552
  const {
100217
99553
  seedPath: filePath,
100218
99554
  dataPath,
100219
99555
  testPath
100220
99556
  } = await seedCreate({
100221
99557
  name,
100222
- seedsDir: join38(base, "src", "seeds"),
100223
- testsDir: join38(base, "tests", "seeds"),
99558
+ seedsDir: join37(base, "src", "seeds"),
99559
+ testsDir: join37(base, "tests", "seeds"),
100224
99560
  module
100225
99561
  });
100226
- const binSeedRunPath = join38(process.cwd(), base, "bin", "seed", "run.ts");
99562
+ const binSeedRunPath = join37(process.cwd(), base, "bin", "seed", "run.ts");
100227
99563
  const binSeedRunFile = Bun.file(binSeedRunPath);
100228
99564
  if (!await binSeedRunFile.exists()) {
100229
99565
  await Bun.write(binSeedRunPath, seed_run_default.replace(/{{name}}/g, module));
@@ -100247,12 +99583,12 @@ class SeedCreateCommand {
100247
99583
  }
100248
99584
  }
100249
99585
  SeedCreateCommand = __legacyDecorateClassTS([
100250
- decorator38.command()
99586
+ decorator37.command()
100251
99587
  ], SeedCreateCommand);
100252
99588
  // src/commands/SeedRunCommand.ts
100253
99589
  import { existsSync as existsSync3 } from "fs";
100254
- import { join as join39 } from "path";
100255
- import { decorator as decorator39 } from "@ooneex/command";
99590
+ import { join as join38 } from "path";
99591
+ import { decorator as decorator38 } from "@ooneex/command";
100256
99592
  import { TerminalLogger as TerminalLogger37 } from "@ooneex/logger";
100257
99593
  class SeedRunCommand {
100258
99594
  getName() {
@@ -100263,7 +99599,7 @@ class SeedRunCommand {
100263
99599
  }
100264
99600
  async run(options) {
100265
99601
  const logger = new TerminalLogger37;
100266
- const modulesDir = join39(process.cwd(), "modules");
99602
+ const modulesDir = join38(process.cwd(), "modules");
100267
99603
  if (!existsSync3(modulesDir)) {
100268
99604
  logger.warn("No modules with seeds found", undefined, {
100269
99605
  showTimestamp: false,
@@ -100276,10 +99612,10 @@ class SeedRunCommand {
100276
99612
  const modules = [];
100277
99613
  for await (const match of glob.scan({ cwd: modulesDir, onlyFiles: true })) {
100278
99614
  const entry = match.replace("/package.json", "");
100279
- const moduleDir = join39(modulesDir, entry);
100280
- const seedRunFile = Bun.file(join39(moduleDir, "bin", "seed", "run.ts"));
99615
+ const moduleDir = join38(modulesDir, entry);
99616
+ const seedRunFile = Bun.file(join38(moduleDir, "bin", "seed", "run.ts"));
100281
99617
  if (await seedRunFile.exists()) {
100282
- const packageJson = await Bun.file(join39(modulesDir, match)).json();
99618
+ const packageJson = await Bun.file(join38(modulesDir, match)).json();
100283
99619
  modules.push({ name: packageJson.name ?? entry, dir: moduleDir });
100284
99620
  }
100285
99621
  }
@@ -100292,7 +99628,7 @@ class SeedRunCommand {
100292
99628
  return;
100293
99629
  }
100294
99630
  for (const { name, dir } of modules) {
100295
- const seedRunPath = join39(dir, "bin", "seed", "run.ts");
99631
+ const seedRunPath = join38(dir, "bin", "seed", "run.ts");
100296
99632
  logger.info(`Running seeds for ${name}...`, undefined, {
100297
99633
  showTimestamp: false,
100298
99634
  showArrow: false,
@@ -100306,7 +99642,7 @@ class SeedRunCommand {
100306
99642
  const proc = Bun.spawn(args, {
100307
99643
  cwd: dir,
100308
99644
  stdout: "inherit",
100309
- stderr: "inherit",
99645
+ stderr: "pipe",
100310
99646
  env
100311
99647
  });
100312
99648
  const exitCode = await proc.exited;
@@ -100317,7 +99653,9 @@ class SeedRunCommand {
100317
99653
  useSymbol: true
100318
99654
  });
100319
99655
  } else {
100320
- logger.error(`Seeds failed for ${name} (exit code: ${exitCode})`, undefined, {
99656
+ const errorOutput = await new Response(proc.stderr).text();
99657
+ const trimmed = errorOutput.trim();
99658
+ logger.error(`Seeds failed for ${name} (exit code: ${exitCode})`, trimmed ? { message: trimmed } : undefined, {
100321
99659
  showTimestamp: false,
100322
99660
  showArrow: false,
100323
99661
  useSymbol: true
@@ -100327,9 +99665,9 @@ class SeedRunCommand {
100327
99665
  }
100328
99666
  }
100329
99667
  SeedRunCommand = __legacyDecorateClassTS([
100330
- decorator39.command()
99668
+ decorator38.command()
100331
99669
  ], SeedRunCommand);
100332
99670
  // src/index.ts
100333
99671
  await run();
100334
99672
 
100335
- //# debugId=62106AD907A8879564756E2164756E21
99673
+ //# debugId=61AFC67C24C1FDDE64756E2164756E21