@ooneex/cli 1.36.3 → 1.36.5

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
@@ -10362,7 +10362,7 @@ var package_json_default = `{
10362
10362
  `;
10363
10363
 
10364
10364
  // src/templates/app/README.md.txt
10365
- 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\nCopy the environment template and fill in the required values:\n\n```bash\ncp modules/app/.env.example modules/app/.env\n```\n\n| Variable | Description | Required |\n|---|---|---|\n| `APP_ENV` | Application environment (`local`, `development`, `staging`, `testing`, `production`, \u2026) | Yes |\n| `PORT` | Server port number (default: `3000`) | Yes |\n| `HOST_NAME` | Server hostname (default: `0.0.0.0`) | Yes |\n| `LOGS_DATABASE_URL` | Database URL for storing application logs | No |\n| `BETTERSTACK_LOGGER_SOURCE_TOKEN` | Logtail source token for Better Stack logging | No |\n| `BETTERSTACK_EXCEPTION_LOGGER_APPLICATION_TOKEN` | Better Stack token for exception tracking | No |\n| `ANALYTICS_POSTHOG_PROJECT_TOKEN` | PostHog API key for event tracking | No |\n| `CACHE_REDIS_URL` | Redis connection URL for caching | No |\n| `CACHE_UPSTASH_REDIS_REST_URL` | Upstash Redis REST URL 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| `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_CREDENTIALS` | Allow credentials: `true` or `false` | No |\n| `DATABASE_URL` | PostgreSQL connection URL | No |\n| `DATABASE_REDIS_URL` | Redis connection URL for Redis-based database operations | No |\n| `SQLITE_DATABASE_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_BUNNY_ACCESS_KEY` | BunnyCDN access key | No |\n| `STORAGE_BUNNY_STORAGE_ZONE` | BunnyCDN storage zone name | No |\n| `FILESYSTEM_STORAGE_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| `RESEND_API_KEY` | Resend email service API key | No |\n| `JWT_SECRET` | Secret key for HS256 JWT signing and verification | No |\n| `OPENROUTER_API_KEY` | OpenRouter API key for accessing 300+ AI models | No |\n| `OPENAI_API_KEY` | OpenAI API key | No |\n| `ANTHROPIC_API_KEY` | Anthropic API key for Claude models | No |\n| `GEMINI_API_KEY` | Google Gemini API key | No |\n| `GROQ_API_KEY` | Groq API key for LLM inference | No |\n| `POLAR_ACCESS_TOKEN` | Polar payment platform access token | No |\n| `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\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 your local `.env`:\n\n```\nLINEAR_API_KEY=lin_api_xxxxxxxxxx\nLINEAR_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### 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### Spec Plan\n\n`spec:plan` reads an issue YAML file, prompts you to select which resource types to generate (entity, repository, service, controller, and optionals such as cache, mailer, pubsub, etc.), then invokes Claude to produce one `specs/<spec-name>.spec.yml` file per entity/action pair under `modules/<name>/specs/`.\n\n```bash\noo spec:plan # Prompt for issue and resources\noo spec:plan --issue OON-123 # Plan from shared module issue\noo spec:plan --issue OON-123 --module user # Plan from user module issue\n```\n\nThe core resources (entity, repository, service, controller) are pre-selected. Deselect any that are not needed, and add optional resources for more complete scaffolding. The generated spec YAMLs describe the entity shape, required operations, and expected behaviour \u2014 inputs and outputs \u2014 ready to be implemented one at a time.\n\n---\n\n### Spec Implement\n\n`spec:implement` reads a single spec YAML file from `modules/<name>/specs/` and invokes Claude to scaffold the full implementation: entity, repository, service, controller or command, and any optional resources declared in the spec.\n\n```bash\noo spec:implement # Prompt for spec\noo spec:implement --spec create-user # Implement from shared module\noo spec:implement --spec create-user --module user # Implement from user module\n```\n\nRun this once per spec entry. When all entries are implemented, the issue's acceptance criteria should be fully satisfied.\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 spec:plan --issue OON-123 --module user\n \u2192 Prompts for resource types, then generates one spec YAML per entity/action pair\n in modules/user/specs/\n\n3. oo spec:implement --spec <name> --module user (repeat per spec entry)\n \u2192 Invokes Claude to scaffold entity, repository, service, controller, and optional resources\n\n4. 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 spec:plan --issue ABD-042381 --module user\n \u2192 Prompts for resource types, then generates one spec YAML per entity/action pair\n in modules/user/specs/\n\n4. oo spec:implement --spec <name> --module user (repeat per spec entry)\n \u2192 Invokes Claude to scaffold entity, repository, service, controller, and optional resources\n\n5. 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 add it to `.env` and ensure `.env` 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#### Spec\n\n| Skill | Description |\n|---|---|\n| `/spec:plan` | Parse user-provided content and produce one `specs/<spec-name>.spec.yml` file per entity/action pair |\n| `/spec:implement` | Read a single spec YML entry and implement entity, repository, service, controller/command, and optional resources |\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";
10365
+ 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
10366
 
10367
10367
  // src/templates/app/tsconfig.json.txt
10368
10368
  var tsconfig_json_default = `{
@@ -10713,13 +10713,6 @@ This project ships with Claude Code skills that scaffold, implement, and test ar
10713
10713
  | \`/migration:create\` | Generate a migration file with \`up\`/\`down\` methods and structural tests |
10714
10714
  | \`/seed:create\` | Generate a seed class and its YAML data file with idempotent insertion logic |
10715
10715
 
10716
- #### Spec
10717
-
10718
- | Skill | Description |
10719
- |---|---|
10720
- | \`/spec:plan\` | Parse user-provided content and produce one \`specs/<spec-name>.spec.yml\` file per entity/action pair |
10721
- | \`/spec:implement\` | Read a single spec YML entry and implement entity, repository, service, controller/command, and optional resources |
10722
-
10723
10716
  ### Coding Conventions Enforced by Skills
10724
10717
 
10725
10718
  - **Visibility modifiers** \u2014 every class method and property has explicit \`public\`, \`private\`, or \`protected\`
@@ -10883,6 +10876,126 @@ git commit -m "chore(common): Update dependencies and cache package"
10883
10876
  Apply all coding conventions from the \`optimize\` skill.
10884
10877
  `;
10885
10878
 
10879
+ // src/templates/claude/skills/issue.improve.md.txt
10880
+ var issue_improve_md_default = `---
10881
+ name: issue:improve
10882
+ description: Improve a YAML issue file by rewriting the description into a structured format, extracting labels, and optionally splitting into smaller focused issues. Reads from modules/<module>/issues/<ID>.yml.
10883
+ ---
10884
+
10885
+ # Issue Improve
10886
+
10887
+ Improve an existing YAML issue file: rewrite the description, suggest labels, and optionally split into smaller issues.
10888
+
10889
+ ## Workflow
10890
+
10891
+ ### 1. Locate the Issue File
10892
+
10893
+ Ask the user for the issue identifier or file path if not already provided.
10894
+ Look for the file in \`modules/<module>/issues/<ID>.yml\` (default module: \`shared\`).
10895
+ Read the YAML file with the Read tool.
10896
+
10897
+ ### 2. Improve the Description
10898
+
10899
+ Rewrite the \`description\` field into this structured format:
10900
+
10901
+ \`\`\`markdown
10902
+ ## Context
10903
+ <Background information explaining why this issue exists>
10904
+
10905
+ ## Goal
10906
+ <What needs to be achieved>
10907
+
10908
+ ## Acceptance Criteria
10909
+ - [ ] <Condition 1 that must be met>
10910
+ - [ ] <Condition 2 that must be met>
10911
+ - [ ] <\u2026>
10912
+
10913
+ ## Technical Notes
10914
+ <Optional: technical constraints or implementation hints \u2014 omit section if not applicable>
10915
+ \`\`\`
10916
+
10917
+ Rules:
10918
+ - Preserve all factual information from the original description
10919
+ - Keep each section concise and actionable
10920
+ - Acceptance Criteria must be checkboxes (\`- [ ]\`), not prose
10921
+ - Omit \`## Technical Notes\` if there is nothing relevant to add
10922
+
10923
+ ### 3. Extract Labels
10924
+
10925
+ Based on the improved description, suggest relevant labels:
10926
+ - Short (1\u20133 words), lowercase, hyphenated (e.g. \`bug\`, \`enhancement\`, \`performance\`, \`breaking-change\`)
10927
+ - Deduplicate against labels already present in the YAML
10928
+ - Present suggestions to the user and ask which ones to add
10929
+
10930
+ Common label vocabulary:
10931
+ \`bug\`, \`enhancement\`, \`performance\`, \`refactor\`, \`security\`, \`breaking-change\`,
10932
+ \`documentation\`, \`testing\`, \`database\`, \`api\`, \`ui\`, \`infrastructure\`, \`cleanup\`
10933
+
10934
+ ### 4. Check Whether Splitting Is Needed
10935
+
10936
+ Evaluate whether the issue is large or complex enough to split. An issue needs splitting when it:
10937
+ - Spans multiple unrelated concerns or implementation areas
10938
+ - Cannot be completed in one focused developer session
10939
+ - Contains several independent acceptance criteria that could ship separately
10940
+
10941
+ Skip splitting if the issue is already small and focused.
10942
+
10943
+ ### 5. Split Into Sub-Issues (if needed)
10944
+
10945
+ Break the issue into 3\u20137 small, self-contained, independently implementable issues.
10946
+
10947
+ For each sub-issue:
10948
+ - Generate a new identifier using the format \`XXX-000000\` (3 uppercase letters + 6 digits)
10949
+ - Write a new YAML file to the same \`modules/<module>/issues/\` directory
10950
+ - Inherit \`state\`, \`priority\`, and \`labels\` from the parent issue
10951
+
10952
+ Sub-issue YAML structure:
10953
+ \`\`\`yaml
10954
+ id: "<generated-id>"
10955
+ title: "<action-oriented title: verb + noun>"
10956
+ state: "<parent state>"
10957
+ priority: "<parent priority>"
10958
+ description: |
10959
+ <2\u20135 sentences: context, goal, and acceptance criteria>
10960
+ labels:
10961
+ - "<label>"
10962
+ \`\`\`
10963
+
10964
+ ### 6. Save Changes
10965
+
10966
+ Update the original YAML file with the improved description and new labels using the Edit or Write tool.
10967
+ Confirm each file written with a success message showing the relative path.
10968
+
10969
+ ## YAML Structure Reference
10970
+
10971
+ \`\`\`yaml
10972
+ id: "OON-123456"
10973
+ title: "Add user validation"
10974
+ state: "In Progress"
10975
+ priority: "High"
10976
+ description: |
10977
+ ## Context
10978
+ \u2026
10979
+ ## Goal
10980
+ \u2026
10981
+ ## Acceptance Criteria
10982
+ - [ ] \u2026
10983
+ labels:
10984
+ - "enhancement"
10985
+ - "api"
10986
+ comments:
10987
+ - author: "Alice"
10988
+ message: "Some comment"
10989
+ \`\`\`
10990
+
10991
+ ## Notes
10992
+
10993
+ - Never invent facts \u2014 only restructure and clarify what is already in the description
10994
+ - If the description is missing or empty, tell the user and stop
10995
+ - Always show the user the improved description before writing, and ask for confirmation
10996
+ - When splitting, inform the user of each sub-issue file created
10997
+ `;
10998
+
10886
10999
  // src/templates/claude/skills/make.ai.md.txt
10887
11000
  var make_ai_md_default = `---
10888
11001
  name: make:ai
@@ -14013,286 +14126,6 @@ bun run lint
14013
14126
  Apply all coding conventions from the \`optimize\` skill.
14014
14127
  `;
14015
14128
 
14016
- // src/templates/claude/skills/spec.implement.md.txt
14017
- var spec_implement_md_default = `---
14018
- name: spec:implement
14019
- description: Implement a single spec from a YML entry. Use when executing a planned spec, creating or updating an entity, repository, service, controller/command, and optional resources.
14020
- ---
14021
-
14022
- # Spec Implement
14023
-
14024
- Use the single spec YML entry provided by the user and implement it.
14025
-
14026
- ## Important
14027
-
14028
- Always run all commands from the **root of the project** (the monorepo root), not from inside individual packages.
14029
-
14030
- ## Steps
14031
-
14032
- ### 1. Read the spec YML
14033
-
14034
- Use the YML content provided by the user. Do not search for or locate a file on your own.
14035
-
14036
- If no YML content was provided, stop and ask the user to supply the spec YML or run \`/spec:plan\` first.
14037
-
14038
- A spec YML looks like:
14039
-
14040
- \`\`\`yaml
14041
- name: "organization.create"
14042
- entity: "organization"
14043
- description: "Admin creates a new organization"
14044
- roles:
14045
- - super_admin
14046
- - admin
14047
- permissions:
14048
- - name: "organization:create"
14049
- description: "Grants the ability to create a new organization"
14050
- resources:
14051
- entity: OrganizationEntity
14052
- repository: OrganizationRepository
14053
- service: OrganizationCreateService
14054
- controller: OrganizationCreateController
14055
- permission: OrganizationCreatePermission
14056
- \`\`\`
14057
-
14058
- ### 2. Analyse the spec
14059
-
14060
- Extract:
14061
-
14062
- - \`name\` \u2014 dot-notation identifier (e.g. \`"user.create"\`)
14063
- - \`entity\` \u2014 the resource being acted on (e.g. \`"user"\`)
14064
- - \`description\` \u2014 what the spec does
14065
- - \`roles\` \u2014 list of roles with access; check \`modules/shared/src/roles.yml\` to map role slugs to their canonical values
14066
- - \`permissions\` \u2014 list of objects with \`name\` (\`"entity:action"\` format) and optional \`description\`
14067
- - \`resources\` \u2014 map of resource keys to PascalCase class names; this is the authoritative list of what to create
14068
-
14069
- Derive the HTTP method from the action part of \`name\`:
14070
- - \`.create\` \u2192 \`post\`
14071
- - \`.read\` / \`.list\` / \`.search\` \u2192 \`get\`
14072
- - \`.update\` \u2192 \`put\` or \`patch\`
14073
- - \`.delete\` \u2192 \`delete\`
14074
-
14075
- ### 3. Create or update the entity
14076
-
14077
- Only if \`resources.entity\` is present.
14078
-
14079
- \`\`\`
14080
- /make:entity --name=<resources.entity> --module=<module>
14081
- \`\`\`
14082
-
14083
- ### 4. Create or update the repository
14084
-
14085
- Only if \`resources.repository\` is present.
14086
-
14087
- \`\`\`
14088
- /make:repository --name=<resources.repository> --module=<module>
14089
- \`\`\`
14090
-
14091
- Retain only the CRUD methods that are needed by this spec (e.g. \`.create\` spec needs \`save\`; \`.read\` needs \`findById\`; \`.list\` needs \`find\`; \`.delete\` needs \`delete\`).
14092
-
14093
- ### 5. Create or update the service
14094
-
14095
- Only if \`resources.service\` is present.
14096
-
14097
- \`\`\`
14098
- /make:service --name=<resources.service> --module=<module>
14099
- \`\`\`
14100
-
14101
- Inject the repository into the service via the constructor and implement \`execute()\` with the business logic described by the spec.
14102
-
14103
- ### 6. Create or update the controller or command
14104
-
14105
- **Controller** \u2014 only if \`resources.controller\` is present:
14106
-
14107
- \`\`\`
14108
- /make:controller --name=<resources.controller> --module=<module> --route-name=<spec.name> --route-path=<derived-path> --route-method=<derived-method>
14109
- \`\`\`
14110
-
14111
- Derive the route path from the entity and action (e.g. \`"user.create"\` \u2192 \`/users\`, \`"user.read"\` \u2192 \`/users/:id\`).
14112
-
14113
- Inject the service into the controller. Set \`roles\` in the \`@Route\` decorator from \`spec.roles\` as uppercase string literals (e.g., \`"ROLE_USER"\`, \`"ROLE_ADMIN"\`). Apply each permission \`name\` from \`spec.permissions\` to the route decorator when a \`permissions\` field is available on \`@Route\`.
14114
-
14115
- **Command** \u2014 only if \`resources.command\` is present:
14116
-
14117
- \`\`\`
14118
- /make:command --name=<resources.command> --module=<module>
14119
- \`\`\`
14120
-
14121
- Inject the service into the command. Set \`getName()\` to \`"<entity>:<action>"\` format.
14122
-
14123
- ### 7. Create or update optional resources
14124
-
14125
- Create each resource that is present in \`resources\` beyond the core four. Use the class name from the \`resources\` map as the \`--name\` value. Do not create resources that are absent from the map.
14126
-
14127
- | \`resources\` key | Skill |
14128
- |------------------|-----------------------------------------------------------------------------|
14129
- | \`permission\` | \`/make:permission --name=<resources.permission> --module=<module>\` |
14130
- | \`middleware\` | \`/make:middleware --name=<resources.middleware> --module=<module>\` |
14131
- | \`cache\` | \`/make:cache --name=<resources.cache> --module=<module>\` |
14132
- | \`pubsub\` | \`/make:pubsub --name=<resources.pubsub> --module=<module>\` |
14133
- | \`mailer\` | \`/make:mailer --name=<resources.mailer> --module=<module>\` |
14134
- | \`logger\` | \`/make:logger --name=<resources.logger> --module=<module>\` |
14135
- | \`analytics\` | \`/make:analytics --name=<resources.analytics> --module=<module>\` |
14136
- | \`storage\` | \`/make:storage --name=<resources.storage> --module=<module>\` |
14137
- | \`cron\` | \`/make:cron --name=<resources.cron> --module=<module>\` |
14138
- | \`ai\` | \`/make:ai --name=<resources.ai> --module=<module>\` |
14139
- | \`database\` | \`/make:database --name=<resources.database> --module=<module>\` |
14140
- | \`vectorDatabase\` | \`/make:vector-database --name=<resources.vectorDatabase> --module=<module>\` |
14141
-
14142
- ### 8. Lint and format
14143
-
14144
- \`\`\`bash
14145
- bun run fmt
14146
- bun run lint
14147
- \`\`\`
14148
-
14149
- ### 9. Confirm
14150
-
14151
- Report:
14152
-
14153
- - Spec \`name\` implemented
14154
- - Resources created or updated (list each key and class name from \`resources\`)
14155
- - Any step that was skipped and why
14156
-
14157
- ## Notes
14158
-
14159
- - The \`resources\` map is the single source of truth for what to create \u2014 do not infer additional resources beyond what is listed.
14160
- - If a resource already exists, update it rather than overwrite it \u2014 add new methods, columns, or routes without removing existing ones unless they conflict.
14161
- - Derive all names, paths, and methods from the spec; never ask the user for values that can be inferred.
14162
- - Apply all coding conventions from the \`optimize\` skill to every generated file.
14163
- `;
14164
-
14165
- // src/templates/claude/skills/spec.plan.md.txt
14166
- var spec_plan_md_default = `---
14167
- name: spec:plan
14168
- description: Analyse user-provided content and produce one spec YML file per operation under specs/. Use before /spec:implement to plan entities, CRUD actions, roles, and permissions.
14169
- ---
14170
-
14171
- # Spec Plan
14172
-
14173
- Read the user-provided content (feature description, requirements, user stories, or raw notes) and produce one \`specs/<spec-name>.spec.yml\` file per operation.
14174
-
14175
- ## Important
14176
-
14177
- Always run all commands from the **root of the project** (the monorepo root), not from inside individual packages.
14178
-
14179
- ## Steps
14180
-
14181
- ### 1. Read the user content
14182
-
14183
- Use only the content provided by the user in the current message. Do not search files or infer context from the repository.
14184
-
14185
- If no content was provided, stop and ask the user to describe the feature, entity, or workflow they want to plan.
14186
-
14187
- ### 2. Identify entities and operations
14188
-
14189
- Extract every distinct entity mentioned or implied. For each entity determine which CRUD operations are required:
14190
-
14191
- | Action keyword in content | Spec action suffix |
14192
- |---------------------------------------|--------------------|
14193
- | create, add, register, submit, invite | \`.create\` |
14194
- | get, fetch, view, read, show, detail | \`.read\` |
14195
- | list, search, browse, filter, query | \`.list\` |
14196
- | update, edit, modify, change, patch | \`.update\` |
14197
- | delete, remove, archive, deactivate | \`.delete\` |
14198
-
14199
- Produce one spec per \`<entity>.<action>\` pair.
14200
-
14201
- ### 3. Build each spec
14202
-
14203
- For each spec, populate the following fields:
14204
-
14205
- **\`name\`** \u2014 dot-notation identifier: \`"<entity>.<action>"\` (snake_case entity, e.g. \`"blog_post.create"\`).
14206
-
14207
- **\`entity\`** \u2014 the resource name in snake_case (e.g. \`"blog_post"\`).
14208
-
14209
- **\`description\`** \u2014 describes what the operation does, the data it acts on, and any key business rules or constraints an implementer must know. Derived from the user content.
14210
-
14211
- **\`roles\`** \u2014 list of role identifiers that may perform this operation. Derive from the user content; default to \`["user"]\` when not specified. Look inside \`modules/shared/src/roles.yml\` for the list of available roles in the project.
14212
-
14213
- **\`permissions\`** \u2014 list of fine-grained access-control entries that guard this operation. Each object has:
14214
- - \`name\`: \`"<entity>:<action>"\` format (e.g. \`"blog_post:create"\`) \u2014 uniquely identifies the permission across the system
14215
- - \`description\`: one sentence explaining what this permission grants, who it protects, and any conditions or limits (e.g. ownership, tenant scope). Required \u2014 never leave empty.
14216
-
14217
- **\`resources\`** \u2014 map of PascalCase class names to create for this spec. Use only the keys listed under **Resources to generate** in the user message; omit any key not in that list.
14218
-
14219
- **Core (always include):**
14220
- - \`entity\`: the data model class (e.g. \`"BlogPostEntity"\`)
14221
- - \`repository\`: the data-access class (e.g. \`"BlogPostRepository"\`)
14222
- - \`service\`: the business-logic class (e.g. \`"BlogPostService"\`)
14223
- - \`controller\`: the HTTP handler class (e.g. \`"BlogPostCreateController"\`); use \`command\` instead when the spec implies a CLI operation (e.g. \`"BlogPostCreateCommand"\`)
14224
-
14225
- **Optional (include only when the trigger condition applies \u2014 names use \`<PascalSpec>\` derived from \`name\`, e.g. \`"blog_post.create"\` \u2192 \`"BlogPostCreate"\`):**
14226
-
14227
- | Key | Trigger condition | Example value |
14228
- |------------------|-----------------------------------------------------------------|----------------------------------|
14229
- | \`permission\` | Spec has \`roles\` or \`permissions\` defined | \`"BlogPostCreatePermission"\` |
14230
- | \`middleware\` | Spec implies auth, rate-limiting, or request transformation | \`"BlogPostCreateMiddleware"\` |
14231
- | \`cache\` | Spec implies read-heavy or list endpoints | \`"BlogPostCreateCache"\` |
14232
- | \`pubsub\` | Spec mutates state (post, put, patch, delete) | \`"BlogPostCreatePubSub"\` |
14233
- | \`mailer\` | Spec mentions email, notification, or invitation | \`"BlogPostCreateMailer"\` |
14234
- | \`logger\` | Spec mentions audit, log, or traceability | \`"BlogPostCreateLogger"\` |
14235
- | \`analytics\` | Spec mentions tracking, metrics, or reporting | \`"BlogPostCreateAnalytics"\` |
14236
- | \`storage\` | Spec mentions file, upload, or attachment | \`"BlogPostCreateStorage"\` |
14237
- | \`cron\` | Spec mentions scheduled, periodic, or background task | \`"BlogPostCreateCron"\` |
14238
- | \`ai\` | Spec mentions AI, embedding, or generation | \`"BlogPostCreateAi"\` |
14239
- | \`database\` | Spec implies a new database connection or datasource | \`"BlogPostCreateDatabase"\` |
14240
- | \`vectorDatabase\` | Spec mentions vector search or similarity | \`"BlogPostCreateVectorDatabase"\` |
14241
-
14242
- ### 4. Write spec files
14243
-
14244
- For each spec, create the file \`specs/<spec-name>.spec.yml\` where \`<spec-name>\` equals the \`name\` field with dots replaced by hyphens (e.g. \`specs/blog-post-create.spec.yml\`).
14245
-
14246
- Write one YAML block per file using the structure below. Do **not** combine multiple specs into a single file.
14247
-
14248
- \`\`\`yaml
14249
- name: "<entity>.<action>"
14250
- entity: "<entity>"
14251
- description: "<description of what the operation does, the data it acts on, and any key business rules or constraints an implementer must know>"
14252
- roles:
14253
- - <role>
14254
- permissions:
14255
- - name: "<entity>:<action>"
14256
- description: "<what this permission grants, who it protects, and any conditions or limits>"
14257
- resources:
14258
- # Core \u2014 always present
14259
- entity: <EntityName>
14260
- repository: <EntityRepositoryName>
14261
- service: <PascalSpecServiceName>
14262
- controller: <PascalSpecControllerName> # or command: <PascalSpecCommandName> for CLI ops
14263
- # Optional \u2014 include only when triggered
14264
- # permission: <PascalSpecPermissionName>
14265
- # middleware: <PascalSpecMiddlewareName>
14266
- # cache: <PascalSpecCacheName>
14267
- # pubsub: <PascalSpecPubSubName>
14268
- # mailer: <PascalSpecMailerName>
14269
- # logger: <PascalSpecLoggerName>
14270
- # analytics: <PascalSpecAnalyticsName>
14271
- # storage: <PascalSpecStorageName>
14272
- # cron: <PascalSpecCronName>
14273
- # ai: <PascalSpecAiName>
14274
- # database: <PascalSpecDatabaseName>
14275
- # vectorDatabase: <PascalSpecVectorDatabaseName>
14276
- \`\`\`
14277
-
14278
- Create the \`specs/\` directory if it does not exist.
14279
-
14280
- ### 5. Confirm
14281
-
14282
- After all files are written, report:
14283
-
14284
- - Total number of specs created
14285
- - List of file paths written (one per line)
14286
- - Any entity or action that was ambiguous and how it was resolved
14287
-
14288
- ## Notes
14289
-
14290
- - One file per spec \u2014 never merge multiple specs into one file.
14291
- - Derive everything from the user content; never ask for values that can be inferred.
14292
- - If the user content implies a CLI operation rather than an HTTP endpoint, add \`kind: command\` at the top level of the spec.
14293
- - Specs are consumed by \`/spec:implement\` \u2014 keep field names and values precise so that skill can infer types without ambiguity.
14294
- `;
14295
-
14296
14129
  // src/commands/ClaudeSkillCreateCommand.ts
14297
14130
  var skills = {
14298
14131
  "make.ai": make_ai_md_default,
@@ -14314,8 +14147,7 @@ var skills = {
14314
14147
  "make.service": make_service_md_default,
14315
14148
  "make.storage": make_storage_md_default,
14316
14149
  "make.vector-database": make_vector_database_md_default,
14317
- "spec.implement": spec_implement_md_default,
14318
- "spec.plan": spec_plan_md_default,
14150
+ "issue.improve": issue_improve_md_default,
14319
14151
  commit: commit_md_default,
14320
14152
  optimize: optimize_md_default
14321
14153
  };
@@ -14841,46 +14673,6 @@ _oo_modules() {
14841
14673
  fi
14842
14674
  }
14843
14675
 
14844
- _oo_issues() {
14845
- local module="shared"
14846
- local i
14847
- for (( i = 1; i < $#words; i++ )); do
14848
- if [[ "\${words[i]}" == "--module" ]]; then
14849
- module="\${words[i+1]}"
14850
- break
14851
- elif [[ "\${words[i]}" == --module=* ]]; then
14852
- module="\${words[i]#--module=}"
14853
- break
14854
- fi
14855
- done
14856
- local -a issues
14857
- local issues_dir="modules/\${module}/issues"
14858
- if [[ -d "$issues_dir" ]]; then
14859
- issues=(\${(@f)"$(command ls -1 "\${issues_dir}" 2>/dev/null | sed 's/\\.yml$//')"})
14860
- compadd -a issues
14861
- fi
14862
- }
14863
-
14864
- _oo_specs() {
14865
- local module="shared"
14866
- local i
14867
- for (( i = 1; i < $#words; i++ )); do
14868
- if [[ "\${words[i]}" == "--module" ]]; then
14869
- module="\${words[i+1]}"
14870
- break
14871
- elif [[ "\${words[i]}" == --module=* ]]; then
14872
- module="\${words[i]#--module=}"
14873
- break
14874
- fi
14875
- done
14876
- local -a specs
14877
- local specs_dir="modules/\${module}/specs"
14878
- if [[ -d "$specs_dir" ]]; then
14879
- specs=(\${(@f)"$(command ls -1 "\${specs_dir}" 2>/dev/null | sed 's/\\.spec\\.yml$//')"})
14880
- compadd -a specs
14881
- fi
14882
- }
14883
-
14884
14676
  _oo_custom_commands() {
14885
14677
  local -a cmds
14886
14678
  if [[ -d modules ]]; then
@@ -14925,8 +14717,6 @@ _oo() {
14925
14717
  'make\\:resource\\:book:Generate book resource (entity, migration, repository)'
14926
14718
  'issue\\:create:Create a YAML skeleton file for a new issue'
14927
14719
  'issue\\:pull:Pull an issue from Linear and save it as a YAML file'
14928
- 'spec\\:implement:Implement a spec using Claude'
14929
- 'spec\\:plan:Generate a spec plan for an issue using Claude'
14930
14720
  'seed\\:create:Generate a new seed file'
14931
14721
  'seed\\:run:Run seeds for all modules'
14932
14722
  'make\\:service:Generate a new service class'
@@ -15033,16 +14823,6 @@ _oo() {
15033
14823
  '--id=[Linear issue ID or identifier]:id' \\
15034
14824
  '--module=[Module name]:module:_oo_modules'
15035
14825
  ;;
15036
- spec:implement)
15037
- _arguments -s \\
15038
- '--spec=[Spec identifier]:spec:_oo_specs' \\
15039
- '--module=[Module name]:module:_oo_modules'
15040
- ;;
15041
- spec:plan)
15042
- _arguments -s \\
15043
- '--issue=[Issue identifier]:issue:_oo_issues' \\
15044
- '--module=[Module name]:module:_oo_modules'
15045
- ;;
15046
14826
  migration:up|seed:run)
15047
14827
  _arguments -s \\
15048
14828
  '--drop[Drop the database before running]'
@@ -15068,46 +14848,6 @@ _ooneex_modules() {
15068
14848
  fi
15069
14849
  }
15070
14850
 
15071
- _ooneex_issues() {
15072
- local module="shared"
15073
- local i
15074
- for (( i = 1; i < $#words; i++ )); do
15075
- if [[ "\${words[i]}" == "--module" ]]; then
15076
- module="\${words[i+1]}"
15077
- break
15078
- elif [[ "\${words[i]}" == --module=* ]]; then
15079
- module="\${words[i]#--module=}"
15080
- break
15081
- fi
15082
- done
15083
- local -a issues
15084
- local issues_dir="modules/\${module}/issues"
15085
- if [[ -d "$issues_dir" ]]; then
15086
- issues=(\${(@f)"$(command ls -1 "\${issues_dir}" 2>/dev/null | sed 's/\\.yml$//')"})
15087
- compadd -a issues
15088
- fi
15089
- }
15090
-
15091
- _ooneex_specs() {
15092
- local module="shared"
15093
- local i
15094
- for (( i = 1; i < $#words; i++ )); do
15095
- if [[ "\${words[i]}" == "--module" ]]; then
15096
- module="\${words[i+1]}"
15097
- break
15098
- elif [[ "\${words[i]}" == --module=* ]]; then
15099
- module="\${words[i]#--module=}"
15100
- break
15101
- fi
15102
- done
15103
- local -a specs
15104
- local specs_dir="modules/\${module}/specs"
15105
- if [[ -d "$specs_dir" ]]; then
15106
- specs=(\${(@f)"$(command ls -1 "\${specs_dir}" 2>/dev/null | sed 's/\\.spec\\.yml$//')"})
15107
- compadd -a specs
15108
- fi
15109
- }
15110
-
15111
14851
  _ooneex_custom_commands() {
15112
14852
  local -a cmds
15113
14853
  if [[ -d modules ]]; then
@@ -15152,8 +14892,6 @@ _ooneex() {
15152
14892
  'make\\:resource\\:book:Generate book resource (entity, migration, repository)'
15153
14893
  'issue\\:create:Create a YAML skeleton file for a new issue'
15154
14894
  'issue\\:pull:Pull an issue from Linear and save it as a YAML file'
15155
- 'spec\\:implement:Implement a spec using Claude'
15156
- 'spec\\:plan:Generate a spec plan for an issue using Claude'
15157
14895
  'seed\\:create:Generate a new seed file'
15158
14896
  'seed\\:run:Run seeds for all modules'
15159
14897
  'make\\:service:Generate a new service class'
@@ -15260,16 +14998,6 @@ _ooneex() {
15260
14998
  '--id=[Linear issue ID or identifier]:id' \\
15261
14999
  '--module=[Module name]:module:_ooneex_modules'
15262
15000
  ;;
15263
- spec:implement)
15264
- _arguments -s \\
15265
- '--spec=[Spec identifier]:spec:_ooneex_specs' \\
15266
- '--module=[Module name]:module:_ooneex_modules'
15267
- ;;
15268
- spec:plan)
15269
- _arguments -s \\
15270
- '--issue=[Issue identifier]:issue:_ooneex_issues' \\
15271
- '--module=[Module name]:module:_ooneex_modules'
15272
- ;;
15273
15001
  migration:up|seed:run)
15274
15002
  _arguments -s \\
15275
15003
  '--drop[Drop the database before running]'
@@ -15431,168 +15159,6 @@ class IssueCreateCommand {
15431
15159
  showArrow: false,
15432
15160
  useSymbol: true
15433
15161
  });
15434
- const { improveDescription } = description ? await import_enquirer5.prompt({
15435
- type: "confirm",
15436
- name: "improveDescription",
15437
- message: "Improve description with Claude?",
15438
- initial: true
15439
- }) : { improveDescription: false };
15440
- if (improveDescription && description) {
15441
- const improvePrompt = [
15442
- "Rewrite the following issue description into a well-structured format with these sections:",
15443
- "- **Context**: Background information explaining why this issue exists",
15444
- "- **Goal**: What needs to be achieved",
15445
- "- **Acceptance Criteria**: A checklist of conditions that must be met (Definition of Done)",
15446
- "- **Technical Notes**: (optional) Any technical constraints or implementation hints",
15447
- "",
15448
- "Output ONLY the reformatted description. No preamble, no explanation.",
15449
- "",
15450
- "Original description:",
15451
- description
15452
- ].join(`
15453
- `);
15454
- const improveSpinner = createSpinner("Improving description...");
15455
- const proc = Bun.spawn(["claude", "-p", improvePrompt], {
15456
- stdout: "pipe",
15457
- stderr: "pipe"
15458
- });
15459
- await proc.exited;
15460
- improveSpinner.stop();
15461
- description = await new Response(proc.stdout).text();
15462
- const labelPrompt = [
15463
- "Based on the following issue description, suggest relevant labels as a comma-separated list.",
15464
- "Labels should be short (1-3 words), lowercase, hyphenated (e.g. bug, enhancement, performance, breaking-change).",
15465
- "Output ONLY the comma-separated labels. No preamble, no explanation.",
15466
- "",
15467
- "Description:",
15468
- description
15469
- ].join(`
15470
- `);
15471
- const labelSpinner = createSpinner("Extracting labels...");
15472
- const labelProc = Bun.spawn(["claude", "-p", labelPrompt], {
15473
- stdout: "pipe",
15474
- stderr: "pipe"
15475
- });
15476
- await labelProc.exited;
15477
- labelSpinner.stop();
15478
- const suggestedLabels = (await new Response(labelProc.stdout).text()).trim().split(",").map((l2) => l2.trim()).filter((l2) => l2.length > 0 && !labels.includes(l2));
15479
- if (suggestedLabels.length > 0) {
15480
- const { selectedLabels = [] } = await import_enquirer5.prompt({
15481
- type: "multiselect",
15482
- name: "selectedLabels",
15483
- message: "Suggested labels from description (space to toggle)",
15484
- choices: suggestedLabels
15485
- });
15486
- labels = [...new Set([...labels, ...selectedLabels])];
15487
- }
15488
- const updatedYaml = issueToYaml(resolvedId, title?.trim() ?? null, state?.trim() ?? null, priority?.trim() ?? null, description.trim(), labels);
15489
- await Bun.write(filePath, updatedYaml);
15490
- logger.success(`${join11(issuesLocalDir, fileName)} updated with improved description`, undefined, {
15491
- showTimestamp: false,
15492
- showArrow: false,
15493
- useSymbol: true
15494
- });
15495
- }
15496
- const currentDescription = description ?? null;
15497
- const { splitIssue } = currentDescription ? await import_enquirer5.prompt({
15498
- type: "confirm",
15499
- name: "splitIssue",
15500
- message: "Split into small executable issues with Claude?",
15501
- initial: false
15502
- }) : { splitIssue: false };
15503
- if (splitIssue && currentDescription) {
15504
- const checkPrompt = [
15505
- "You are a technical project manager. Evaluate whether the following issue is large or complex enough to benefit from being split into smaller issues.",
15506
- "",
15507
- 'Output ONLY a JSON object: { "needed": boolean, "reason": string }',
15508
- '- "needed": true if the issue spans multiple concerns, steps, or implementation areas; false if it is already small and focused',
15509
- '- "reason": one sentence explaining your decision',
15510
- "No preamble, no explanation, no markdown fences \u2014 raw JSON only",
15511
- "",
15512
- `Issue title: ${title ?? ""}`,
15513
- "",
15514
- "Issue description:",
15515
- currentDescription
15516
- ].join(`
15517
- `);
15518
- const checkSpinner = createSpinner("Checking if split is needed...");
15519
- const checkProc = Bun.spawn(["claude", "-p", checkPrompt], {
15520
- stdout: "pipe",
15521
- stderr: "pipe"
15522
- });
15523
- await checkProc.exited;
15524
- checkSpinner.stop();
15525
- const checkRaw = (await new Response(checkProc.stdout).text()).trim();
15526
- let splitNeeded = true;
15527
- try {
15528
- const checkResult = JSON.parse(checkRaw);
15529
- splitNeeded = checkResult.needed;
15530
- logger.info(`Claude: ${checkResult.reason}`, undefined, {
15531
- showTimestamp: false,
15532
- showArrow: false,
15533
- useSymbol: true
15534
- });
15535
- } catch {}
15536
- if (!splitNeeded) {
15537
- logger.info("Issue does not need splitting", undefined, {
15538
- showTimestamp: false,
15539
- showArrow: false,
15540
- useSymbol: true
15541
- });
15542
- return;
15543
- }
15544
- }
15545
- if (splitIssue && currentDescription) {
15546
- const splitPrompt = [
15547
- "You are a technical project manager. Break the following issue into small, self-contained, executable issues.",
15548
- "Each issue must be independently implementable by a developer in one focused session.",
15549
- "",
15550
- "Output ONLY a JSON array of objects with these fields:",
15551
- ' { "title": string, "description": string }',
15552
- "",
15553
- "Rules:",
15554
- '- title: concise action-oriented title (verb + noun, e.g. "Add user validation")',
15555
- "- description: 2-5 sentences covering context, goal, and acceptance criteria",
15556
- "- Aim for 3-7 issues; never output more than 10",
15557
- "- No preamble, no explanation, no markdown fences \u2014 raw JSON array only",
15558
- "",
15559
- `Issue title: ${title ?? ""}`,
15560
- "",
15561
- "Issue description:",
15562
- currentDescription
15563
- ].join(`
15564
- `);
15565
- const splitSpinner = createSpinner("Splitting issue...");
15566
- const splitProc = Bun.spawn(["claude", "-p", splitPrompt], {
15567
- stdout: "pipe",
15568
- stderr: "pipe"
15569
- });
15570
- await splitProc.exited;
15571
- splitSpinner.stop();
15572
- const rawJson = (await new Response(splitProc.stdout).text()).trim();
15573
- let splitItems = [];
15574
- try {
15575
- splitItems = JSON.parse(rawJson);
15576
- } catch {
15577
- logger.error("Claude returned invalid JSON for split issues", undefined, {
15578
- showTimestamp: false,
15579
- showArrow: false,
15580
- useSymbol: true
15581
- });
15582
- }
15583
- for (const item of splitItems) {
15584
- const splitId = generateId();
15585
- const splitFileName = `${splitId}.yml`;
15586
- const splitFilePath = join11(issuesDir, splitFileName);
15587
- const splitYaml = issueToYaml(splitId, item.title, state?.trim() ?? null, priority?.trim() ?? null, item.description, labels);
15588
- await Bun.write(splitFilePath, splitYaml);
15589
- logger.success(`${join11(issuesLocalDir, splitFileName)} created`, undefined, {
15590
- showTimestamp: false,
15591
- showArrow: false,
15592
- useSymbol: true
15593
- });
15594
- }
15595
- }
15596
15162
  }
15597
15163
  }
15598
15164
  IssueCreateCommand = __legacyDecorateClassTS([
@@ -95552,182 +95118,6 @@ class IssuePullCommand {
95552
95118
  showArrow: false,
95553
95119
  useSymbol: true
95554
95120
  });
95555
- let description = issue.description ?? null;
95556
- const { improveDescription } = description ? await import_enquirer6.prompt({
95557
- type: "confirm",
95558
- name: "improveDescription",
95559
- message: "Improve description with Claude?",
95560
- initial: true
95561
- }) : { improveDescription: false };
95562
- if (improveDescription && description) {
95563
- const improvePrompt = [
95564
- "Rewrite the following issue description into a well-structured format with these sections:",
95565
- "- **Context**: Background information explaining why this issue exists",
95566
- "- **Goal**: What needs to be achieved",
95567
- "- **Acceptance Criteria**: A checklist of conditions that must be met (Definition of Done)",
95568
- "- **Technical Notes**: (optional) Any technical constraints or implementation hints",
95569
- "",
95570
- "Output ONLY the reformatted description. No preamble, no explanation.",
95571
- "",
95572
- "Original description:",
95573
- description
95574
- ].join(`
95575
- `);
95576
- const improveSpinner = createSpinner("Improving description...");
95577
- const proc = Bun.spawn(["claude", "-p", improvePrompt], {
95578
- stdout: "pipe",
95579
- stderr: "pipe"
95580
- });
95581
- await proc.exited;
95582
- improveSpinner.stop();
95583
- description = await new Response(proc.stdout).text();
95584
- const labelPrompt = [
95585
- "Based on the following issue description, suggest relevant labels as a comma-separated list.",
95586
- "Labels should be short (1-3 words), lowercase, hyphenated (e.g. bug, enhancement, performance, breaking-change).",
95587
- "Output ONLY the comma-separated labels. No preamble, no explanation.",
95588
- "",
95589
- "Description:",
95590
- description
95591
- ].join(`
95592
- `);
95593
- const labelSpinner = createSpinner("Extracting labels...");
95594
- const labelProc = Bun.spawn(["claude", "-p", labelPrompt], {
95595
- stdout: "pipe",
95596
- stderr: "pipe"
95597
- });
95598
- await labelProc.exited;
95599
- labelSpinner.stop();
95600
- const existingLabelNames = issue.labels?.map((l2) => l2.name).filter((n2) => n2 != null) ?? [];
95601
- const suggestedLabels = (await new Response(labelProc.stdout).text()).trim().split(",").map((l2) => l2.trim()).filter((l2) => l2.length > 0 && !existingLabelNames.includes(l2));
95602
- let extraLabels = [];
95603
- if (suggestedLabels.length > 0) {
95604
- const { selectedLabels = [] } = await import_enquirer6.prompt({
95605
- type: "multiselect",
95606
- name: "selectedLabels",
95607
- message: "Suggested labels from description (space to toggle)",
95608
- choices: suggestedLabels
95609
- });
95610
- extraLabels = selectedLabels.map((name) => ({ name }));
95611
- }
95612
- const updatedIssue = {
95613
- ...issue,
95614
- description,
95615
- labels: [...issue.labels ?? [], ...extraLabels]
95616
- };
95617
- await Bun.write(filePath, issueToYaml2(updatedIssue));
95618
- logger.success(`${join12(issuesLocalDir, fileName)} updated with improved description`, undefined, {
95619
- showTimestamp: false,
95620
- showArrow: false,
95621
- useSymbol: true
95622
- });
95623
- }
95624
- const currentDescription = description ?? issue.description;
95625
- const { splitIssue } = currentDescription ? await import_enquirer6.prompt({
95626
- type: "confirm",
95627
- name: "splitIssue",
95628
- message: "Split into small executable issues with Claude?",
95629
- initial: false
95630
- }) : { splitIssue: false };
95631
- if (splitIssue && currentDescription) {
95632
- const checkPrompt = [
95633
- "You are a technical project manager. Evaluate whether the following issue is large or complex enough to benefit from being split into smaller issues.",
95634
- "",
95635
- 'Output ONLY a JSON object: { "needed": boolean, "reason": string }',
95636
- '- "needed": true if the issue spans multiple concerns, steps, or implementation areas; false if it is already small and focused',
95637
- '- "reason": one sentence explaining your decision',
95638
- "No preamble, no explanation, no markdown fences \u2014 raw JSON only",
95639
- "",
95640
- `Issue title: ${issue.title ?? ""}`,
95641
- "",
95642
- "Issue description:",
95643
- currentDescription
95644
- ].join(`
95645
- `);
95646
- const checkSpinner = createSpinner("Checking if split is needed...");
95647
- const checkProc = Bun.spawn(["claude", "-p", checkPrompt], {
95648
- stdout: "pipe",
95649
- stderr: "pipe"
95650
- });
95651
- await checkProc.exited;
95652
- checkSpinner.stop();
95653
- const checkRaw = (await new Response(checkProc.stdout).text()).trim();
95654
- let splitNeeded = true;
95655
- try {
95656
- const checkResult = JSON.parse(checkRaw);
95657
- splitNeeded = checkResult.needed;
95658
- logger.info(`Claude: ${checkResult.reason}`, undefined, {
95659
- showTimestamp: false,
95660
- showArrow: false,
95661
- useSymbol: true
95662
- });
95663
- } catch {}
95664
- if (!splitNeeded) {
95665
- logger.info("Issue does not need splitting", undefined, {
95666
- showTimestamp: false,
95667
- showArrow: false,
95668
- useSymbol: true
95669
- });
95670
- return;
95671
- }
95672
- }
95673
- if (splitIssue && currentDescription) {
95674
- const splitPrompt = [
95675
- "You are a technical project manager. Break the following issue into small, self-contained, executable issues.",
95676
- "Each issue must be independently implementable by a developer in one focused session.",
95677
- "",
95678
- "Output ONLY a JSON array of objects with these fields:",
95679
- ' { "title": string, "description": string }',
95680
- "",
95681
- "Rules:",
95682
- '- title: concise action-oriented title (verb + noun, e.g. "Add user validation")',
95683
- "- description: 2-5 sentences covering context, goal, and acceptance criteria",
95684
- "- Aim for 3-7 issues; never output more than 10",
95685
- "- No preamble, no explanation, no markdown fences \u2014 raw JSON array only",
95686
- "",
95687
- `Issue title: ${issue.title ?? ""}`,
95688
- "",
95689
- "Issue description:",
95690
- currentDescription
95691
- ].join(`
95692
- `);
95693
- const splitSpinner = createSpinner("Splitting issue...");
95694
- const splitProc = Bun.spawn(["claude", "-p", splitPrompt], {
95695
- stdout: "pipe",
95696
- stderr: "pipe"
95697
- });
95698
- await splitProc.exited;
95699
- splitSpinner.stop();
95700
- const rawJson = (await new Response(splitProc.stdout).text()).trim();
95701
- let splitItems = [];
95702
- try {
95703
- splitItems = JSON.parse(rawJson);
95704
- } catch {
95705
- logger.error("Claude returned invalid JSON for split issues", undefined, {
95706
- showTimestamp: false,
95707
- showArrow: false,
95708
- useSymbol: true
95709
- });
95710
- }
95711
- for (const item of splitItems) {
95712
- const splitId = generateId2();
95713
- const splitFileName = `${splitId}.yml`;
95714
- const splitFilePath = join12(issuesDir, splitFileName);
95715
- const splitYaml = issueToYaml2({
95716
- identifier: splitId,
95717
- title: item.title,
95718
- description: item.description,
95719
- ...issue.state !== undefined ? { state: issue.state } : {},
95720
- ...issue.priority !== undefined ? { priority: issue.priority } : {},
95721
- ...issue.labels !== undefined ? { labels: issue.labels } : {}
95722
- });
95723
- await Bun.write(splitFilePath, splitYaml);
95724
- logger.success(`${join12(issuesLocalDir, splitFileName)} created`, undefined, {
95725
- showTimestamp: false,
95726
- showArrow: false,
95727
- useSymbol: true
95728
- });
95729
- }
95730
- }
95731
95121
  }
95732
95122
  }
95733
95123
  IssuePullCommand = __legacyDecorateClassTS([
@@ -100855,224 +100245,7 @@ class SeedRunCommand {
100855
100245
  SeedRunCommand = __legacyDecorateClassTS([
100856
100246
  decorator39.command()
100857
100247
  ], SeedRunCommand);
100858
- // src/commands/SpecImplementCommand.ts
100859
- var import_enquirer12 = __toESM(require_enquirer(), 1);
100860
- import { join as join40 } from "path";
100861
- import { decorator as decorator40 } from "@ooneex/command";
100862
- import { TerminalLogger as TerminalLogger38 } from "@ooneex/logger";
100863
- class SpecImplementCommand {
100864
- getName() {
100865
- return "spec:implement";
100866
- }
100867
- getDescription() {
100868
- return "Implement a spec using Claude";
100869
- }
100870
- async run(options) {
100871
- let { spec, module = "shared" } = options;
100872
- await ensureModule(module);
100873
- const specsDir = join40(process.cwd(), "modules", module, "specs");
100874
- if (!spec) {
100875
- const files = [];
100876
- try {
100877
- for await (const file2 of new Bun.Glob("*.spec.yml").scan(specsDir)) {
100878
- files.push(file2.replace(/\.spec\.yml$/, ""));
100879
- }
100880
- } catch {}
100881
- if (files.length === 0) {
100882
- const logger2 = new TerminalLogger38;
100883
- logger2.error(`No specs found in modules/${module}/specs/`, undefined, {
100884
- showTimestamp: false,
100885
- showArrow: false,
100886
- useSymbol: true
100887
- });
100888
- return;
100889
- }
100890
- const response = await import_enquirer12.prompt({
100891
- type: "select",
100892
- name: "spec",
100893
- message: "Select spec",
100894
- choices: files
100895
- });
100896
- spec = response.spec;
100897
- }
100898
- const filePath = join40(specsDir, `${spec}.spec.yml`);
100899
- const file = Bun.file(filePath);
100900
- if (!await file.exists()) {
100901
- const logger2 = new TerminalLogger38;
100902
- logger2.error(`Spec file not found: modules/${module}/specs/${spec}.spec.yml`, undefined, {
100903
- showTimestamp: false,
100904
- showArrow: false,
100905
- useSymbol: true
100906
- });
100907
- return;
100908
- }
100909
- const content = await file.text();
100910
- const implementPrompt = [spec_implement_md_default, "", `**Module:** ${module}`, "", "## Spec", "", content].join(`
100911
- `).trim();
100912
- const logger = new TerminalLogger38;
100913
- logger.info(`Running /spec:implement for ${spec}.spec.yml...`, undefined, {
100914
- showTimestamp: false,
100915
- showArrow: false,
100916
- useSymbol: true
100917
- });
100918
- const implProc = Bun.spawn(["claude", "-p", implementPrompt], {
100919
- stdout: "inherit",
100920
- stderr: "inherit"
100921
- });
100922
- await implProc.exited;
100923
- }
100924
- }
100925
- SpecImplementCommand = __legacyDecorateClassTS([
100926
- decorator40.command()
100927
- ], SpecImplementCommand);
100928
- // src/commands/SpecPlanCommand.ts
100929
- var import_enquirer13 = __toESM(require_enquirer(), 1);
100930
- import { join as join41 } from "path";
100931
- import { decorator as decorator41 } from "@ooneex/command";
100932
- import { TerminalLogger as TerminalLogger39 } from "@ooneex/logger";
100933
- var ALL_RESOURCES = [
100934
- { name: "entity", hint: "Data model class" },
100935
- { name: "repository", hint: "Data-access class" },
100936
- { name: "service", hint: "Business-logic class" },
100937
- { name: "controller", hint: "HTTP handler / CLI command" },
100938
- { name: "permission", hint: "Access-control class" },
100939
- { name: "middleware", hint: "Auth / rate-limit / transform" },
100940
- { name: "cache", hint: "Read-heavy / list endpoints" },
100941
- { name: "pubsub", hint: "State-mutating operations" },
100942
- { name: "mailer", hint: "Email / notification / invitation" },
100943
- { name: "logger", hint: "Audit / log / traceability" },
100944
- { name: "analytics", hint: "Tracking / metrics / reporting" },
100945
- { name: "storage", hint: "File / upload / attachment" },
100946
- { name: "cron", hint: "Scheduled / periodic / background" },
100947
- { name: "ai", hint: "AI / embedding / generation" },
100948
- { name: "database", hint: "New database connection" },
100949
- { name: "vectorDatabase", hint: "Vector search / similarity" }
100950
- ];
100951
- var CORE_RESOURCES = new Set(["entity", "repository", "service", "controller"]);
100952
- var VALID_RESOURCE_NAMES = new Set(ALL_RESOURCES.map((r2) => r2.name));
100953
- async function suggestResources(issueContent) {
100954
- const suggestionPrompt = [
100955
- "You are a backend architecture assistant.",
100956
- "Given the following issue, reply with ONLY a JSON array of resource names that should be generated.",
100957
- `Choose from: ${ALL_RESOURCES.map((r2) => r2.name).join(", ")}.`,
100958
- "No explanation, no markdown, no code block \u2014 just a raw JSON array.",
100959
- "",
100960
- "Issue:",
100961
- issueContent
100962
- ].join(`
100963
- `);
100964
- try {
100965
- const suggestionSpinner = createSpinner("Suggesting resources...");
100966
- const proc = Bun.spawn(["claude", "-p", suggestionPrompt], {
100967
- stdout: "pipe",
100968
- stderr: "pipe"
100969
- });
100970
- await proc.exited;
100971
- suggestionSpinner.stop();
100972
- const raw = await new Response(proc.stdout).text();
100973
- const parsed = JSON.parse(raw.trim());
100974
- if (Array.isArray(parsed)) {
100975
- const valid = parsed.filter((v2) => typeof v2 === "string" && VALID_RESOURCE_NAMES.has(v2));
100976
- if (valid.length > 0)
100977
- return new Set(valid);
100978
- }
100979
- } catch {}
100980
- return new Set(CORE_RESOURCES);
100981
- }
100982
-
100983
- class SpecPlanCommand {
100984
- getName() {
100985
- return "spec:plan";
100986
- }
100987
- getDescription() {
100988
- return "Generate a spec plan for an issue using Claude";
100989
- }
100990
- async run(options) {
100991
- let { issue, module = "shared" } = options;
100992
- await ensureModule(module);
100993
- const issuesDir = join41(process.cwd(), "modules", module, "issues");
100994
- if (!issue) {
100995
- const files = [];
100996
- try {
100997
- for await (const file2 of new Bun.Glob("*.yml").scan(issuesDir)) {
100998
- files.push(file2.replace(/\.yml$/, ""));
100999
- }
101000
- } catch {}
101001
- if (files.length === 0) {
101002
- const logger2 = new TerminalLogger39;
101003
- logger2.error(`No issues found in modules/${module}/issues/`, undefined, {
101004
- showTimestamp: false,
101005
- showArrow: false,
101006
- useSymbol: true
101007
- });
101008
- return;
101009
- }
101010
- const response = await import_enquirer13.prompt({
101011
- type: "select",
101012
- name: "issue",
101013
- message: "Select issue",
101014
- choices: files
101015
- });
101016
- issue = response.issue;
101017
- }
101018
- const filePath = join41(issuesDir, `${issue}.yml`);
101019
- const file = Bun.file(filePath);
101020
- if (!await file.exists()) {
101021
- const logger2 = new TerminalLogger39;
101022
- logger2.error(`Issue file not found: modules/${module}/issues/${issue}.yml`, undefined, {
101023
- showTimestamp: false,
101024
- showArrow: false,
101025
- useSymbol: true
101026
- });
101027
- return;
101028
- }
101029
- const content = await file.text();
101030
- const suggestedResources = await suggestResources(content);
101031
- const { resources } = await import_enquirer13.prompt({
101032
- type: "multiselect",
101033
- name: "resources",
101034
- message: "Select resources to generate",
101035
- choices: ALL_RESOURCES.map((r2) => ({
101036
- name: r2.name,
101037
- hint: r2.hint,
101038
- enabled: suggestedResources.has(r2.name)
101039
- }))
101040
- });
101041
- const specsDir = join41(process.cwd(), "modules", module, "specs");
101042
- const planPrompt = [
101043
- spec_plan_md_default,
101044
- "",
101045
- `**Module:** ${module}`,
101046
- `**Spec output directory:** ${specsDir}`,
101047
- "",
101048
- "## Resources to generate",
101049
- "",
101050
- resources.map((r2) => `- ${r2}`).join(`
101051
- `),
101052
- "",
101053
- "## Issue",
101054
- "",
101055
- content
101056
- ].join(`
101057
- `).trim();
101058
- const logger = new TerminalLogger39;
101059
- logger.info("Running /spec:plan...", undefined, {
101060
- showTimestamp: false,
101061
- showArrow: false,
101062
- useSymbol: true
101063
- });
101064
- const planProc = Bun.spawn(["claude", "-p"], {
101065
- stdin: Buffer.from(planPrompt),
101066
- stdout: "inherit",
101067
- stderr: "inherit"
101068
- });
101069
- await planProc.exited;
101070
- }
101071
- }
101072
- SpecPlanCommand = __legacyDecorateClassTS([
101073
- decorator41.command()
101074
- ], SpecPlanCommand);
101075
100248
  // src/index.ts
101076
100249
  await run();
101077
100250
 
101078
- //# debugId=4D85614A43F301F864756E2164756E21
100251
+ //# debugId=C0A94E3D0B33D65A64756E2164756E21