@sixthsummit/ship-api 0.0.0-stage → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +605 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +58 -2
- package/cli/commands/add.js +537 -0
- package/cli/commands/contract.js +2 -0
- package/cli/commands/create.js +250 -0
- package/cli/commands/dev.js +227 -0
- package/cli/commands/doctor.js +286 -0
- package/cli/commands/generate.js +937 -0
- package/cli/commands/graph.js +2 -0
- package/cli/commands/migrate-orm.js +1171 -0
- package/cli/commands/update.js +37 -0
- package/cli/ship-api.js +285 -0
- package/lib/app/auth/AuthProvider.d.ts +91 -0
- package/lib/app/auth/AuthProvider.d.ts.map +1 -0
- package/lib/app/auth/AuthProvider.js +13 -0
- package/lib/app/auth/AuthProvider.js.map +1 -0
- package/lib/app/auth/adapters/firebase.d.ts +79 -0
- package/lib/app/auth/adapters/firebase.d.ts.map +1 -0
- package/lib/app/auth/adapters/firebase.js +122 -0
- package/lib/app/auth/adapters/firebase.js.map +1 -0
- package/lib/app/auth/adapters/jwtCookie.d.ts +55 -0
- package/lib/app/auth/adapters/jwtCookie.d.ts.map +1 -0
- package/lib/app/auth/adapters/jwtCookie.js +120 -0
- package/lib/app/auth/adapters/jwtCookie.js.map +1 -0
- package/lib/app/auth/index.d.ts +8 -0
- package/lib/app/auth/index.d.ts.map +1 -0
- package/lib/app/auth/index.js +7 -0
- package/lib/app/auth/index.js.map +1 -0
- package/lib/app/auth/jwt/AuthFlow.d.ts +94 -0
- package/lib/app/auth/jwt/AuthFlow.d.ts.map +1 -0
- package/lib/app/auth/jwt/AuthFlow.js +127 -0
- package/lib/app/auth/jwt/AuthFlow.js.map +1 -0
- package/lib/app/auth/jwt/DrizzleRefreshTokenStore.d.ts +80 -0
- package/lib/app/auth/jwt/DrizzleRefreshTokenStore.d.ts.map +1 -0
- package/lib/app/auth/jwt/DrizzleRefreshTokenStore.js +134 -0
- package/lib/app/auth/jwt/DrizzleRefreshTokenStore.js.map +1 -0
- package/lib/app/auth/jwt/JwtSigner.d.ts +123 -0
- package/lib/app/auth/jwt/JwtSigner.d.ts.map +1 -0
- package/lib/app/auth/jwt/JwtSigner.js +118 -0
- package/lib/app/auth/jwt/JwtSigner.js.map +1 -0
- package/lib/app/auth/jwt/RefreshTokenStore.d.ts +97 -0
- package/lib/app/auth/jwt/RefreshTokenStore.d.ts.map +1 -0
- package/lib/app/auth/jwt/RefreshTokenStore.js +122 -0
- package/lib/app/auth/jwt/RefreshTokenStore.js.map +1 -0
- package/lib/app/auth/permissionDefinition.d.ts +20 -0
- package/lib/app/auth/permissionDefinition.d.ts.map +1 -0
- package/lib/app/auth/permissionDefinition.js +2 -0
- package/lib/app/auth/permissionDefinition.js.map +1 -0
- package/lib/app/context/RequestContext.d.ts +59 -0
- package/lib/app/context/RequestContext.d.ts.map +1 -0
- package/lib/app/context/RequestContext.js +25 -0
- package/lib/app/context/RequestContext.js.map +1 -0
- package/lib/app/db/QueryOptions.d.ts +82 -0
- package/lib/app/db/QueryOptions.d.ts.map +1 -0
- package/lib/app/db/QueryOptions.js +37 -0
- package/lib/app/db/QueryOptions.js.map +1 -0
- package/lib/app/db/baseFields.d.ts +60 -0
- package/lib/app/db/baseFields.d.ts.map +1 -0
- package/lib/app/db/baseFields.js +54 -0
- package/lib/app/db/baseFields.js.map +1 -0
- package/lib/app/db/buildWhere.d.ts +22 -0
- package/lib/app/db/buildWhere.d.ts.map +1 -0
- package/lib/app/db/buildWhere.js +75 -0
- package/lib/app/db/buildWhere.js.map +1 -0
- package/lib/app/db/client.d.ts +111 -0
- package/lib/app/db/client.d.ts.map +1 -0
- package/lib/app/db/client.js +99 -0
- package/lib/app/db/client.js.map +1 -0
- package/lib/app/db/tenantScoped.d.ts +59 -0
- package/lib/app/db/tenantScoped.d.ts.map +1 -0
- package/lib/app/db/tenantScoped.js +54 -0
- package/lib/app/db/tenantScoped.js.map +1 -0
- package/lib/app/db/transaction.d.ts +58 -0
- package/lib/app/db/transaction.d.ts.map +1 -0
- package/lib/app/db/transaction.js +106 -0
- package/lib/app/db/transaction.js.map +1 -0
- package/lib/app/health/HealthCheck.d.ts +121 -0
- package/lib/app/health/HealthCheck.d.ts.map +1 -0
- package/lib/app/health/HealthCheck.js +151 -0
- package/lib/app/health/HealthCheck.js.map +1 -0
- package/lib/app/health/redisHealthCheck.d.ts +14 -0
- package/lib/app/health/redisHealthCheck.d.ts.map +1 -0
- package/lib/app/health/redisHealthCheck.js +21 -0
- package/lib/app/health/redisHealthCheck.js.map +1 -0
- package/lib/app/lifecycle/Bootstrapper.d.ts +92 -0
- package/lib/app/lifecycle/Bootstrapper.d.ts.map +1 -0
- package/lib/app/lifecycle/Bootstrapper.js +99 -0
- package/lib/app/lifecycle/Bootstrapper.js.map +1 -0
- package/lib/app/registry/routes.d.ts +89 -0
- package/lib/app/registry/routes.d.ts.map +1 -0
- package/lib/app/registry/routes.js +72 -0
- package/lib/app/registry/routes.js.map +1 -0
- package/lib/app/services/DrizzleEntityService.d.ts +123 -0
- package/lib/app/services/DrizzleEntityService.d.ts.map +1 -0
- package/lib/app/services/DrizzleEntityService.js +342 -0
- package/lib/app/services/DrizzleEntityService.js.map +1 -0
- package/lib/app/services/RedisProvider.d.ts +52 -0
- package/lib/app/services/RedisProvider.d.ts.map +1 -0
- package/lib/app/services/RedisProvider.js +75 -0
- package/lib/app/services/RedisProvider.js.map +1 -0
- package/lib/app/services/cache/Cache.d.ts +70 -0
- package/lib/app/services/cache/Cache.d.ts.map +1 -0
- package/lib/app/services/cache/Cache.js +18 -0
- package/lib/app/services/cache/Cache.js.map +1 -0
- package/lib/app/services/cache/InMemoryCache.d.ts +35 -0
- package/lib/app/services/cache/InMemoryCache.d.ts.map +1 -0
- package/lib/app/services/cache/InMemoryCache.js +76 -0
- package/lib/app/services/cache/InMemoryCache.js.map +1 -0
- package/lib/app/services/cache/RedisCache.d.ts +99 -0
- package/lib/app/services/cache/RedisCache.d.ts.map +1 -0
- package/lib/app/services/cache/RedisCache.js +150 -0
- package/lib/app/services/cache/RedisCache.js.map +1 -0
- package/lib/app/services/cache/cached.d.ts +30 -0
- package/lib/app/services/cache/cached.d.ts.map +1 -0
- package/lib/app/services/cache/cached.js +38 -0
- package/lib/app/services/cache/cached.js.map +1 -0
- package/lib/app/services/cache/index.d.ts +5 -0
- package/lib/app/services/cache/index.d.ts.map +1 -0
- package/lib/app/services/cache/index.js +4 -0
- package/lib/app/services/cache/index.js.map +1 -0
- package/lib/app/services/cron/defineCron.d.ts +57 -0
- package/lib/app/services/cron/defineCron.d.ts.map +1 -0
- package/lib/app/services/cron/defineCron.js +15 -0
- package/lib/app/services/cron/defineCron.js.map +1 -0
- package/lib/app/services/cron/index.d.ts +2 -0
- package/lib/app/services/cron/index.d.ts.map +1 -0
- package/lib/app/services/cron/index.js +2 -0
- package/lib/app/services/cron/index.js.map +1 -0
- package/lib/app/services/hooks/LifecycleHooks.d.ts +61 -0
- package/lib/app/services/hooks/LifecycleHooks.d.ts.map +1 -0
- package/lib/app/services/hooks/LifecycleHooks.js +85 -0
- package/lib/app/services/hooks/LifecycleHooks.js.map +1 -0
- package/lib/app/services/jobs/JobsManager.d.ts +67 -0
- package/lib/app/services/jobs/JobsManager.d.ts.map +1 -0
- package/lib/app/services/jobs/JobsManager.js +121 -0
- package/lib/app/services/jobs/JobsManager.js.map +1 -0
- package/lib/app/services/jobs/bullBoard.d.ts +72 -0
- package/lib/app/services/jobs/bullBoard.d.ts.map +1 -0
- package/lib/app/services/jobs/bullBoard.js +96 -0
- package/lib/app/services/jobs/bullBoard.js.map +1 -0
- package/lib/app/services/jobs/defineJob.d.ts +81 -0
- package/lib/app/services/jobs/defineJob.d.ts.map +1 -0
- package/lib/app/services/jobs/defineJob.js +49 -0
- package/lib/app/services/jobs/defineJob.js.map +1 -0
- package/lib/app/services/jobs/index.d.ts +4 -0
- package/lib/app/services/jobs/index.d.ts.map +1 -0
- package/lib/app/services/jobs/index.js +4 -0
- package/lib/app/services/jobs/index.js.map +1 -0
- package/lib/app/services/rateLimit/InMemorySlidingWindowBackend.d.ts +23 -0
- package/lib/app/services/rateLimit/InMemorySlidingWindowBackend.d.ts.map +1 -0
- package/lib/app/services/rateLimit/InMemorySlidingWindowBackend.js +51 -0
- package/lib/app/services/rateLimit/InMemorySlidingWindowBackend.js.map +1 -0
- package/lib/app/services/rateLimit/RateLimitBackend.d.ts +41 -0
- package/lib/app/services/rateLimit/RateLimitBackend.d.ts.map +1 -0
- package/lib/app/services/rateLimit/RateLimitBackend.js +19 -0
- package/lib/app/services/rateLimit/RateLimitBackend.js.map +1 -0
- package/lib/app/services/rateLimit/RedisSlidingWindowBackend.d.ts +16 -0
- package/lib/app/services/rateLimit/RedisSlidingWindowBackend.d.ts.map +1 -0
- package/lib/app/services/rateLimit/RedisSlidingWindowBackend.js +80 -0
- package/lib/app/services/rateLimit/RedisSlidingWindowBackend.js.map +1 -0
- package/lib/app/services/rateLimit/index.d.ts +4 -0
- package/lib/app/services/rateLimit/index.d.ts.map +1 -0
- package/lib/app/services/rateLimit/index.js +3 -0
- package/lib/app/services/rateLimit/index.js.map +1 -0
- package/lib/app/services/storage/DiskStorageAdapter.d.ts +62 -0
- package/lib/app/services/storage/DiskStorageAdapter.d.ts.map +1 -0
- package/lib/app/services/storage/DiskStorageAdapter.js +295 -0
- package/lib/app/services/storage/DiskStorageAdapter.js.map +1 -0
- package/lib/app/services/storage/GCSStorageAdapter.d.ts +44 -0
- package/lib/app/services/storage/GCSStorageAdapter.d.ts.map +1 -0
- package/lib/app/services/storage/GCSStorageAdapter.js +133 -0
- package/lib/app/services/storage/GCSStorageAdapter.js.map +1 -0
- package/lib/app/services/storage/S3StorageAdapter.d.ts +57 -0
- package/lib/app/services/storage/S3StorageAdapter.d.ts.map +1 -0
- package/lib/app/services/storage/S3StorageAdapter.js +162 -0
- package/lib/app/services/storage/S3StorageAdapter.js.map +1 -0
- package/lib/app/services/storage/StorageService.d.ts +88 -0
- package/lib/app/services/storage/StorageService.d.ts.map +1 -0
- package/lib/app/services/storage/StorageService.js +18 -0
- package/lib/app/services/storage/StorageService.js.map +1 -0
- package/lib/app/services/storage/index.d.ts +5 -0
- package/lib/app/services/storage/index.d.ts.map +1 -0
- package/lib/app/services/storage/index.js +4 -0
- package/lib/app/services/storage/index.js.map +1 -0
- package/lib/app/utils/ContextValidators.d.ts +46 -0
- package/lib/app/utils/ContextValidators.d.ts.map +1 -0
- package/lib/app/utils/ContextValidators.js +81 -0
- package/lib/app/utils/ContextValidators.js.map +1 -0
- package/lib/app/utils/defineEnv.d.ts +80 -0
- package/lib/app/utils/defineEnv.d.ts.map +1 -0
- package/lib/app/utils/defineEnv.js +95 -0
- package/lib/app/utils/defineEnv.js.map +1 -0
- package/lib/client.d.ts +139 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +98 -0
- package/lib/client.js.map +1 -0
- package/lib/contract/application.d.ts +3 -0
- package/lib/contract/application.d.ts.map +1 -0
- package/lib/contract/application.js +2 -0
- package/lib/contract/application.js.map +1 -0
- package/lib/contract/client.d.ts +3 -0
- package/lib/contract/client.d.ts.map +1 -0
- package/lib/contract/client.js +3 -0
- package/lib/contract/client.js.map +1 -0
- package/lib/contract/index.d.ts +3 -0
- package/lib/contract/index.d.ts.map +1 -0
- package/lib/contract/index.js +3 -0
- package/lib/contract/index.js.map +1 -0
- package/lib/contract/manifest.d.ts +3 -0
- package/lib/contract/manifest.d.ts.map +1 -0
- package/lib/contract/manifest.js +2 -0
- package/lib/contract/manifest.js.map +1 -0
- package/lib/contract/openapi.d.ts +3 -0
- package/lib/contract/openapi.d.ts.map +1 -0
- package/lib/contract/openapi.js +3 -0
- package/lib/contract/openapi.js.map +1 -0
- package/lib/contract/project.d.ts +3 -0
- package/lib/contract/project.d.ts.map +1 -0
- package/lib/contract/project.js +3 -0
- package/lib/contract/project.js.map +1 -0
- package/lib/contract/schema.d.ts +3 -0
- package/lib/contract/schema.d.ts.map +1 -0
- package/lib/contract/schema.js +3 -0
- package/lib/contract/schema.js.map +1 -0
- package/lib/drizzle/index.d.ts +3 -0
- package/lib/drizzle/index.d.ts.map +1 -0
- package/lib/drizzle/index.js +3 -0
- package/lib/drizzle/index.js.map +1 -0
- package/lib/hono/auth.d.ts +6 -0
- package/lib/hono/auth.d.ts.map +1 -0
- package/lib/hono/auth.js +54 -0
- package/lib/hono/auth.js.map +1 -0
- package/lib/hono/index.d.ts +4 -0
- package/lib/hono/index.d.ts.map +1 -0
- package/lib/hono/index.js +4 -0
- package/lib/hono/index.js.map +1 -0
- package/lib/http/app/createApp.d.ts +132 -0
- package/lib/http/app/createApp.d.ts.map +1 -0
- package/lib/http/app/createApp.js +215 -0
- package/lib/http/app/createApp.js.map +1 -0
- package/lib/http/app/devRoutes.d.ts +31 -0
- package/lib/http/app/devRoutes.d.ts.map +1 -0
- package/lib/http/app/devRoutes.js +145 -0
- package/lib/http/app/devRoutes.js.map +1 -0
- package/lib/http/app/errors.d.ts +45 -0
- package/lib/http/app/errors.d.ts.map +1 -0
- package/lib/http/app/errors.js +93 -0
- package/lib/http/app/errors.js.map +1 -0
- package/lib/http/app/health.d.ts +66 -0
- package/lib/http/app/health.d.ts.map +1 -0
- package/lib/http/app/health.js +104 -0
- package/lib/http/app/health.js.map +1 -0
- package/lib/http/errors/AppError.d.ts +10 -0
- package/lib/http/errors/AppError.d.ts.map +1 -0
- package/lib/http/errors/AppError.js +19 -0
- package/lib/http/errors/AppError.js.map +1 -0
- package/lib/http/errors/AuthenticationError.d.ts +8 -0
- package/lib/http/errors/AuthenticationError.d.ts.map +1 -0
- package/lib/http/errors/AuthenticationError.js +10 -0
- package/lib/http/errors/AuthenticationError.js.map +1 -0
- package/lib/http/errors/AuthorizationError.d.ts +8 -0
- package/lib/http/errors/AuthorizationError.d.ts.map +1 -0
- package/lib/http/errors/AuthorizationError.js +10 -0
- package/lib/http/errors/AuthorizationError.js.map +1 -0
- package/lib/http/errors/BadRequestError.d.ts +8 -0
- package/lib/http/errors/BadRequestError.d.ts.map +1 -0
- package/lib/http/errors/BadRequestError.js +10 -0
- package/lib/http/errors/BadRequestError.js.map +1 -0
- package/lib/http/errors/ConflictError.d.ts +8 -0
- package/lib/http/errors/ConflictError.d.ts.map +1 -0
- package/lib/http/errors/ConflictError.js +10 -0
- package/lib/http/errors/ConflictError.js.map +1 -0
- package/lib/http/errors/InternalServerError.d.ts +8 -0
- package/lib/http/errors/InternalServerError.d.ts.map +1 -0
- package/lib/http/errors/InternalServerError.js +10 -0
- package/lib/http/errors/InternalServerError.js.map +1 -0
- package/lib/http/errors/NotFoundError.d.ts +8 -0
- package/lib/http/errors/NotFoundError.d.ts.map +1 -0
- package/lib/http/errors/NotFoundError.js +10 -0
- package/lib/http/errors/NotFoundError.js.map +1 -0
- package/lib/http/errors/RateLimitError.d.ts +13 -0
- package/lib/http/errors/RateLimitError.d.ts.map +1 -0
- package/lib/http/errors/RateLimitError.js +16 -0
- package/lib/http/errors/RateLimitError.js.map +1 -0
- package/lib/http/errors/ServiceUnavailableError.d.ts +8 -0
- package/lib/http/errors/ServiceUnavailableError.d.ts.map +1 -0
- package/lib/http/errors/ServiceUnavailableError.js +10 -0
- package/lib/http/errors/ServiceUnavailableError.js.map +1 -0
- package/lib/http/errors/ValidationError.d.ts +9 -0
- package/lib/http/errors/ValidationError.d.ts.map +1 -0
- package/lib/http/errors/ValidationError.js +12 -0
- package/lib/http/errors/ValidationError.js.map +1 -0
- package/lib/http/errors/index.d.ts +11 -0
- package/lib/http/errors/index.d.ts.map +1 -0
- package/lib/http/errors/index.js +12 -0
- package/lib/http/errors/index.js.map +1 -0
- package/lib/http/middleware/apiKey.d.ts +9 -0
- package/lib/http/middleware/apiKey.d.ts.map +1 -0
- package/lib/http/middleware/apiKey.js +71 -0
- package/lib/http/middleware/apiKey.js.map +1 -0
- package/lib/http/middleware/csrf.d.ts +63 -0
- package/lib/http/middleware/csrf.d.ts.map +1 -0
- package/lib/http/middleware/csrf.js +169 -0
- package/lib/http/middleware/csrf.js.map +1 -0
- package/lib/http/middleware/httpLogger.d.ts +39 -0
- package/lib/http/middleware/httpLogger.d.ts.map +1 -0
- package/lib/http/middleware/httpLogger.js +118 -0
- package/lib/http/middleware/httpLogger.js.map +1 -0
- package/lib/http/middleware/idempotency.d.ts +48 -0
- package/lib/http/middleware/idempotency.d.ts.map +1 -0
- package/lib/http/middleware/idempotency.js +180 -0
- package/lib/http/middleware/idempotency.js.map +1 -0
- package/lib/http/middleware/rateLimit.d.ts +105 -0
- package/lib/http/middleware/rateLimit.d.ts.map +1 -0
- package/lib/http/middleware/rateLimit.js +84 -0
- package/lib/http/middleware/rateLimit.js.map +1 -0
- package/lib/http/middleware/requireAuth.d.ts +58 -0
- package/lib/http/middleware/requireAuth.d.ts.map +1 -0
- package/lib/http/middleware/requireAuth.js +63 -0
- package/lib/http/middleware/requireAuth.js.map +1 -0
- package/lib/http/middleware/requirePermission.d.ts +23 -0
- package/lib/http/middleware/requirePermission.d.ts.map +1 -0
- package/lib/http/middleware/requirePermission.js +50 -0
- package/lib/http/middleware/requirePermission.js.map +1 -0
- package/lib/http/middleware/resolveTenant.d.ts +41 -0
- package/lib/http/middleware/resolveTenant.d.ts.map +1 -0
- package/lib/http/middleware/resolveTenant.js +17 -0
- package/lib/http/middleware/resolveTenant.js.map +1 -0
- package/lib/http/streaming/sse.d.ts +31 -0
- package/lib/http/streaming/sse.d.ts.map +1 -0
- package/lib/http/streaming/sse.js +11 -0
- package/lib/http/streaming/sse.js.map +1 -0
- package/lib/http/streaming/streamLLM.d.ts +94 -0
- package/lib/http/streaming/streamLLM.d.ts.map +1 -0
- package/lib/http/streaming/streamLLM.js +94 -0
- package/lib/http/streaming/streamLLM.js.map +1 -0
- package/lib/http/utils/authCookies.d.ts +74 -0
- package/lib/http/utils/authCookies.d.ts.map +1 -0
- package/lib/http/utils/authCookies.js +102 -0
- package/lib/http/utils/authCookies.js.map +1 -0
- package/lib/http/utils/clientIp.d.ts +85 -0
- package/lib/http/utils/clientIp.d.ts.map +1 -0
- package/lib/http/utils/clientIp.js +249 -0
- package/lib/http/utils/clientIp.js.map +1 -0
- package/lib/http/utils/index.d.ts +4 -0
- package/lib/http/utils/index.d.ts.map +1 -0
- package/lib/http/utils/index.js +5 -0
- package/lib/http/utils/index.js.map +1 -0
- package/lib/http/utils/metaTags.d.ts +49 -0
- package/lib/http/utils/metaTags.d.ts.map +1 -0
- package/lib/http/utils/metaTags.js +32 -0
- package/lib/http/utils/metaTags.js.map +1 -0
- package/lib/http/utils/publicSuffix.d.ts +34 -0
- package/lib/http/utils/publicSuffix.d.ts.map +1 -0
- package/lib/http/utils/publicSuffix.js +276 -0
- package/lib/http/utils/publicSuffix.js.map +1 -0
- package/lib/http/utils/uploads.d.ts +92 -0
- package/lib/http/utils/uploads.d.ts.map +1 -0
- package/lib/http/utils/uploads.js +126 -0
- package/lib/http/utils/uploads.js.map +1 -0
- package/lib/http/utils/validate.d.ts +41 -0
- package/lib/http/utils/validate.d.ts.map +1 -0
- package/lib/http/utils/validate.js +55 -0
- package/lib/http/utils/validate.js.map +1 -0
- package/lib/http/utils/webhooks.d.ts +63 -0
- package/lib/http/utils/webhooks.d.ts.map +1 -0
- package/lib/http/utils/webhooks.js +133 -0
- package/lib/http/utils/webhooks.js.map +1 -0
- package/lib/http/ws/auth.d.ts +46 -0
- package/lib/http/ws/auth.d.ts.map +1 -0
- package/lib/http/ws/auth.js +123 -0
- package/lib/http/ws/auth.js.map +1 -0
- package/lib/index.d.ts +80 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +61 -0
- package/lib/index.js.map +1 -0
- package/lib/kernel/index.d.ts +3 -0
- package/lib/kernel/index.d.ts.map +1 -0
- package/lib/kernel/index.js +3 -0
- package/lib/kernel/index.js.map +1 -0
- package/lib/libraries/.gitkeep +0 -0
- package/lib/libraries/ai/CHANGELOG.md +17 -0
- package/lib/libraries/ai/README.md +164 -0
- package/lib/libraries/ai/adapters/AnthropicAdapter.ts +382 -0
- package/lib/libraries/ai/adapters/LLMProvider.ts +76 -0
- package/lib/libraries/ai/adapters/OpenAIAdapter.ts +466 -0
- package/lib/libraries/ai/experimental/index.ts +20 -0
- package/lib/libraries/ai/index.ts +128 -0
- package/lib/libraries/ai/library.json +19 -0
- package/lib/libraries/ai/loop/agentLoop.ts +377 -0
- package/lib/libraries/ai/prompts/definePrompt.ts +0 -0
- package/lib/libraries/ai/rag/retrieveAndAnswer.ts +177 -0
- package/lib/libraries/ai/streaming/streamLLM.ts +94 -0
- package/lib/libraries/ai/structured/completeStructured.ts +143 -0
- package/lib/libraries/ai/tokens/Tokenizer.ts +216 -0
- package/lib/libraries/ai/tools/defineTool.ts +208 -0
- package/lib/libraries/ai/types.ts +260 -0
- package/lib/libraries/chunking/ChunkingService.ts +103 -0
- package/lib/libraries/chunking/README.md +104 -0
- package/lib/libraries/chunking/docs/api-reference.md +549 -0
- package/lib/libraries/chunking/docs/best-practices.md +321 -0
- package/lib/libraries/chunking/docs/code-organization.md +538 -0
- package/lib/libraries/chunking/docs/configuration.md +418 -0
- package/lib/libraries/chunking/docs/index.md +236 -0
- package/lib/libraries/chunking/docs/quick-start.md +298 -0
- package/lib/libraries/chunking/docs/strategies.md +407 -0
- package/lib/libraries/chunking/docs/summary.md +258 -0
- package/lib/libraries/chunking/docs/troubleshooting.md +527 -0
- package/lib/libraries/chunking/index.ts +17 -0
- package/lib/libraries/chunking/library.json +16 -0
- package/lib/libraries/chunking/strategies/ChunkingStrategy.ts +13 -0
- package/lib/libraries/chunking/strategies/FixedLengthChunkingStrategy.ts +88 -0
- package/lib/libraries/chunking/strategies/SemanticChunkingStrategy.ts +389 -0
- package/lib/libraries/chunking/strategies/SlidingWindowChunkingStrategy.ts +258 -0
- package/lib/libraries/chunking/types/index.ts +63 -0
- package/lib/libraries/email/EmailService.ts +591 -0
- package/lib/libraries/email/README.md +777 -0
- package/lib/libraries/email/TemplateRegistry.ts +148 -0
- package/lib/libraries/email/TemplateRenderer.ts +273 -0
- package/lib/libraries/email/index.ts +4 -0
- package/lib/libraries/email/initializeEmailService.ts +60 -0
- package/lib/libraries/email/library.json +12 -0
- package/lib/libraries/email/types/EmailAddress.ts +4 -0
- package/lib/libraries/email/types/EmailAttachment.ts +11 -0
- package/lib/libraries/email/types/EmailTag.ts +9 -0
- package/lib/libraries/email/types/EmailTemplate.ts +124 -0
- package/lib/libraries/email/types/SendBatchEmailsOptions.ts +9 -0
- package/lib/libraries/email/types/SendBatchEmailsResult.ts +9 -0
- package/lib/libraries/email/types/SendEmailOptions.ts +33 -0
- package/lib/libraries/email/types/SendEmailResult.ts +9 -0
- package/lib/libraries/email/types/index.ts +8 -0
- package/lib/libraries/weaviate/QueryBuilder.ts +292 -0
- package/lib/libraries/weaviate/README.md +284 -0
- package/lib/libraries/weaviate/Weaviate.ts +1419 -0
- package/lib/libraries/weaviate/WeaviateConfig.ts +268 -0
- package/lib/libraries/weaviate/docs/.gitkeep +1 -0
- package/lib/libraries/weaviate/docs/API_REFERENCE.md +1067 -0
- package/lib/libraries/weaviate/docs/CONFIGURATION.md +320 -0
- package/lib/libraries/weaviate/docs/ERROR_HANDLING.md +271 -0
- package/lib/libraries/weaviate/docs/MULTI_TENANCY.md +348 -0
- package/lib/libraries/weaviate/docs/PERFORMANCE.md +602 -0
- package/lib/libraries/weaviate/docs/PLUGIN_DEVELOPMENT.md +547 -0
- package/lib/libraries/weaviate/docs/QUICK_START.md +443 -0
- package/lib/libraries/weaviate/docs/README.md +269 -0
- package/lib/libraries/weaviate/docs/SCHEMA_GUIDE.md +630 -0
- package/lib/libraries/weaviate/docs/WEAVIATE_README.md +520 -0
- package/lib/libraries/weaviate/errors/WeaviateError.ts +92 -0
- package/lib/libraries/weaviate/errors/index.ts +1 -0
- package/lib/libraries/weaviate/index.ts +35 -0
- package/lib/libraries/weaviate/library.json +26 -0
- package/lib/libraries/weaviate/plugins/index.ts +2 -0
- package/lib/libraries/weaviate/schemas/SchemaRegistry.ts +284 -0
- package/lib/libraries/weaviate/schemas/index.ts +2 -0
- package/lib/libraries/weaviate/types/BM25SearchOptions.ts +9 -0
- package/lib/libraries/weaviate/types/ExtendedWeaviateClient.ts +7 -0
- package/lib/libraries/weaviate/types/HybridSearchOptions.ts +11 -0
- package/lib/libraries/weaviate/types/MultiTenancy.ts +81 -0
- package/lib/libraries/weaviate/types/Plugin.ts +17 -0
- package/lib/libraries/weaviate/types/SchemaConfig.ts +25 -0
- package/lib/libraries/weaviate/types/SearchMethod.ts +8 -0
- package/lib/libraries/weaviate/types/SearchOptions.ts +16 -0
- package/lib/libraries/weaviate/types/SearchRequest.ts +19 -0
- package/lib/libraries/weaviate/types/SearchResult.ts +16 -0
- package/lib/libraries/weaviate/types/SearchableField.ts +87 -0
- package/lib/libraries/weaviate/types/VectorIndexConfig.ts +19 -0
- package/lib/libraries/weaviate/types/VectorSearchOptions.ts +18 -0
- package/lib/libraries/weaviate/types/VectorizerConfig.ts +8 -0
- package/lib/libraries/weaviate/types/index.ts +29 -0
- package/lib/libraries/weaviate/utils/TenantContextManager.ts +94 -0
- package/lib/modules/accounts/index.ts +47 -0
- package/lib/modules/accounts/module.json +15 -0
- package/lib/modules/accounts/permissions.ts +33 -0
- package/lib/modules/accounts/routes.ts +200 -0
- package/lib/modules/accounts/schema.ts +219 -0
- package/lib/modules/accounts/services/AccountService.ts +44 -0
- package/lib/modules/accounts/validation/schemas.ts +70 -0
- package/lib/modules/oauth/adapters/GmailOAuthAdapter.ts +35 -0
- package/lib/modules/oauth/adapters/GoogleOAuthAdapter.ts +278 -0
- package/lib/modules/oauth/adapters/MicrosoftOAuthAdapter.ts +269 -0
- package/lib/modules/oauth/adapters/MockOAuthAdapter.ts +138 -0
- package/lib/modules/oauth/adapters/OAuthAdapter.ts +211 -0
- package/lib/modules/oauth/adapters/OAuthAdapterRegistry.ts +77 -0
- package/lib/modules/oauth/adapters/OutlookOAuthAdapter.ts +38 -0
- package/lib/modules/oauth/index.ts +79 -0
- package/lib/modules/oauth/module.json +23 -0
- package/lib/modules/oauth/permissions.ts +39 -0
- package/lib/modules/oauth/routes.ts +497 -0
- package/lib/modules/oauth/schema.ts +127 -0
- package/lib/modules/oauth/services/OAuthConnectionService.ts +147 -0
- package/lib/modules/oauth/state.ts +234 -0
- package/lib/modules/oauth/stateJtiCache.ts +85 -0
- package/lib/modules/oauth/verifyIdToken.ts +178 -0
- package/lib/modules/outbox/EventEmitter.ts +106 -0
- package/lib/modules/outbox/OutboxDrainer.ts +163 -0
- package/lib/modules/outbox/handlers/EventHandler.ts +115 -0
- package/lib/modules/outbox/index.ts +43 -0
- package/lib/modules/outbox/module.json +15 -0
- package/lib/modules/outbox/permissions.ts +36 -0
- package/lib/modules/outbox/routes.ts +219 -0
- package/lib/modules/outbox/schema.ts +207 -0
- package/lib/modules/outbox/services/OutboxEventService.ts +223 -0
- package/lib/observability/logger.d.ts +30 -0
- package/lib/observability/logger.d.ts.map +1 -0
- package/lib/observability/logger.js +110 -0
- package/lib/observability/logger.js.map +1 -0
- package/lib/observability/metrics.d.ts +3 -0
- package/lib/observability/metrics.d.ts.map +1 -0
- package/lib/observability/metrics.js +3 -0
- package/lib/observability/metrics.js.map +1 -0
- package/lib/observability/otel.d.ts +3 -0
- package/lib/observability/otel.d.ts.map +1 -0
- package/lib/observability/otel.js +3 -0
- package/lib/observability/otel.js.map +1 -0
- package/lib/observability/safeErrorFields.d.ts +14 -0
- package/lib/observability/safeErrorFields.d.ts.map +1 -0
- package/lib/observability/safeErrorFields.js +14 -0
- package/lib/observability/safeErrorFields.js.map +1 -0
- package/lib/observability/tracing.d.ts +3 -0
- package/lib/observability/tracing.d.ts.map +1 -0
- package/lib/observability/tracing.js +3 -0
- package/lib/observability/tracing.js.map +1 -0
- package/lib/package.json +4 -0
- package/lib/testing.d.ts +3 -0
- package/lib/testing.d.ts.map +1 -0
- package/lib/testing.js +3 -0
- package/lib/testing.js.map +1 -0
- package/package.json +193 -3
- package/templates/default/.claude/agents/qa-brief.md +106 -0
- package/templates/default/.claude/commands/sherpa-brief.md +23 -0
- package/templates/default/.cursor/rules/api.mdc +80 -0
- package/templates/default/.cursor/rules/commits.mdc +88 -0
- package/templates/default/.cursor/rules/security.mdc +92 -0
- package/templates/default/.cursor/rules/testing.mdc +123 -0
- package/templates/default/.cursor/rules/typescript.mdc +86 -0
- package/templates/default/.env.example +79 -0
- package/templates/default/.github/workflows/sherpa-brief.yml +180 -0
- package/templates/default/AGENTS.md +166 -0
- package/templates/default/CLAUDE.md +17 -0
- package/templates/default/README.md +97 -0
- package/templates/default/_gitignore +27 -0
- package/templates/default/docker-compose.yml +83 -0
- package/templates/default/drizzle.config.ts +34 -0
- package/templates/default/eslint.config.mjs +106 -0
- package/templates/default/package.json +57 -0
- package/templates/default/ship-api.config.ts +21 -0
- package/templates/default/src/client.ts +34 -0
- package/templates/default/src/config/modules.ts +23 -0
- package/templates/default/src/db/schema.ts +26 -0
- package/templates/default/src/environment.ts +81 -0
- package/templates/default/src/index.test.ts +53 -0
- package/templates/default/src/index.ts +11 -0
- package/templates/default/src/instrumentation.ts +13 -0
- package/templates/default/src/server.ts +90 -0
- package/templates/default/tsconfig.json +24 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ship-api g <type> <name>` — framework component generators per plan §C.
|
|
8
|
+
*
|
|
9
|
+
* This file ships **`g module <name>`** — the foundational generator that
|
|
10
|
+
* scaffolds a complete v4 module (schema + service + routes + validation
|
|
11
|
+
* + permissions + index barrel) from the canonical template at
|
|
12
|
+
* `packages/core/cli/templates/module/`. The template was extracted from
|
|
13
|
+
* the `modules/accounts/` port (the canonical reference).
|
|
14
|
+
*
|
|
15
|
+
* Future subcommands (`g resource`, `g service`, `g schema`, `g middleware`,
|
|
16
|
+
* `g job`, `g test`) layer on the same template-substitution mechanism.
|
|
17
|
+
*
|
|
18
|
+
* Substitution placeholders:
|
|
19
|
+
*
|
|
20
|
+
* {{namePlural}} kebab-case-plural ("accounts", "user-tokens") — for paths + string literals
|
|
21
|
+
* {{nameSingular}} kebab-case-singular ("account", "user-token")
|
|
22
|
+
* {{nameCamelPlural}} camelCase-plural JS identifier ("accounts", "userTokens")
|
|
23
|
+
* {{nameCamelSingular}} camelCase-singular JS identifier ("account", "userToken")
|
|
24
|
+
* {{NamePlural}} PascalCase-plural ("Accounts", "UserTokens")
|
|
25
|
+
* {{NameSingular}} PascalCase-singular ("Account", "UserToken")
|
|
26
|
+
* {{NAME_PLURAL}} SCREAMING_SNAKE-plural ("ACCOUNTS", "USER_TOKENS")
|
|
27
|
+
* {{NAME_SINGULAR}} SCREAMING_SNAKE-singular ("ACCOUNT", "USER_TOKEN")
|
|
28
|
+
*
|
|
29
|
+
* The substitution applies to file *contents* and to *file names*: a
|
|
30
|
+
* template file `services/Service.ts.tpl` becomes
|
|
31
|
+
* `services/{{NameSingular}}Service.ts.tpl` in the generator's name-rule
|
|
32
|
+
* (e.g., `services/AccountService.ts`). Template files end in `.tpl` so
|
|
33
|
+
* they're not picked up by tsc / vitest scanning the cli/ tree.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = path.dirname(__filename);
|
|
38
|
+
const TEMPLATE_ROOT = path.join(__dirname, "..", "templates", "module");
|
|
39
|
+
|
|
40
|
+
export async function generateCommand(type, name, opts = {}) {
|
|
41
|
+
switch (type) {
|
|
42
|
+
case "module":
|
|
43
|
+
return generateModule(name, opts);
|
|
44
|
+
case "resource":
|
|
45
|
+
return generateResource(name, opts);
|
|
46
|
+
case "middleware":
|
|
47
|
+
return generateMiddleware(name, opts);
|
|
48
|
+
case "service":
|
|
49
|
+
return generateService(name, opts);
|
|
50
|
+
case "schema":
|
|
51
|
+
return generateSchema(name, opts);
|
|
52
|
+
case "job":
|
|
53
|
+
return generateJob(name, opts);
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(
|
|
56
|
+
`ship-api g ${type} is not yet implemented. ` +
|
|
57
|
+
`Currently supported: module, resource, middleware, service, schema, job. ` +
|
|
58
|
+
`(Other generators like \`test\` land in subsequent commits.)`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `g resource <name> --fields "title:string,body:text,published:boolean?"`.
|
|
65
|
+
*
|
|
66
|
+
* Like `g module` but customizes the schema + validation files to the
|
|
67
|
+
* supplied field spec instead of the default `name:varchar(200)`. Field
|
|
68
|
+
* format: `name:type[?]` — the `?` suffix marks the field nullable
|
|
69
|
+
* (Drizzle column lacks `.notNull()`, Zod schema is `.optional()`).
|
|
70
|
+
*
|
|
71
|
+
* Supported types: string (varchar), text, integer, bigint, boolean,
|
|
72
|
+
* timestamp, uuid, json. Any unknown type maps to `text` with a
|
|
73
|
+
* `// TODO[ship-api]` marker on the column.
|
|
74
|
+
*/
|
|
75
|
+
export async function generateResource(rawName, opts = {}) {
|
|
76
|
+
const fields = parseFieldSpec(opts.fields ?? "");
|
|
77
|
+
if (fields.length === 0) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`g resource: --fields is required (e.g. --fields "title:string,body:text,published:boolean?")`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Run the full module scaffold first (skeleton with the default `name`
|
|
84
|
+
// field). Post-process to swap in the field spec.
|
|
85
|
+
const moduleResult = await generateModule(rawName, {
|
|
86
|
+
...opts,
|
|
87
|
+
silent: true,
|
|
88
|
+
generator: "resource",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await rewriteSchemaForFields(path.join(moduleResult.targetRoot, "schema.ts"), fields);
|
|
92
|
+
await rewriteValidationForFields(
|
|
93
|
+
path.join(moduleResult.targetRoot, "validation", "schemas.ts"),
|
|
94
|
+
fields,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!opts.silent) {
|
|
98
|
+
printResourceReport(rawName, moduleResult, fields);
|
|
99
|
+
}
|
|
100
|
+
return { ...moduleResult, fields };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const FIELD_TYPE_MAP = {
|
|
104
|
+
string: { drizzle: "varchar(NAME, { length: 255 })", zod: "z.string()" },
|
|
105
|
+
text: { drizzle: "text(NAME)", zod: "z.string()" },
|
|
106
|
+
integer: { drizzle: "integer(NAME)", zod: "z.number().int()" },
|
|
107
|
+
number: { drizzle: "integer(NAME)", zod: "z.number().int()" },
|
|
108
|
+
bigint: {
|
|
109
|
+
drizzle: 'bigint(NAME, { mode: "number" })',
|
|
110
|
+
zod: "z.number().int()",
|
|
111
|
+
},
|
|
112
|
+
boolean: { drizzle: "boolean(NAME)", zod: "z.boolean()" },
|
|
113
|
+
timestamp: {
|
|
114
|
+
drizzle: "timestamp(NAME, { withTimezone: true })",
|
|
115
|
+
zod: "z.coerce.date()",
|
|
116
|
+
},
|
|
117
|
+
uuid: { drizzle: "uuid(NAME)", zod: "z.string().uuid()" },
|
|
118
|
+
json: {
|
|
119
|
+
drizzle: "jsonb(NAME).$type<Record<string, unknown>>()",
|
|
120
|
+
zod: "z.record(z.string(), z.unknown())",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function parseFieldSpec(spec) {
|
|
125
|
+
if (!spec) return [];
|
|
126
|
+
const fields = [];
|
|
127
|
+
for (const part of spec.split(",")) {
|
|
128
|
+
const trimmed = part.trim();
|
|
129
|
+
if (!trimmed) continue;
|
|
130
|
+
const colon = trimmed.indexOf(":");
|
|
131
|
+
if (colon === -1) {
|
|
132
|
+
throw new Error(`g resource: malformed field "${trimmed}" — expected "name:type[?]"`);
|
|
133
|
+
}
|
|
134
|
+
const name = trimmed.slice(0, colon).trim();
|
|
135
|
+
let typeRaw = trimmed.slice(colon + 1).trim();
|
|
136
|
+
let nullable = false;
|
|
137
|
+
if (typeRaw.endsWith("?")) {
|
|
138
|
+
nullable = true;
|
|
139
|
+
typeRaw = typeRaw.slice(0, -1);
|
|
140
|
+
}
|
|
141
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`g resource: invalid field name "${name}" — must be camelCase starting with a letter`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const known = Object.hasOwn(FIELD_TYPE_MAP, typeRaw);
|
|
147
|
+
fields.push({
|
|
148
|
+
name,
|
|
149
|
+
type: typeRaw,
|
|
150
|
+
nullable,
|
|
151
|
+
known,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return fields;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderSchemaColumns(fields) {
|
|
158
|
+
const lines = [];
|
|
159
|
+
for (const f of fields) {
|
|
160
|
+
const colName = snakeize(f.name);
|
|
161
|
+
const mapping = FIELD_TYPE_MAP[f.type];
|
|
162
|
+
if (!mapping) {
|
|
163
|
+
lines.push(
|
|
164
|
+
` ${f.name}: text("${colName}")${f.nullable ? "" : ".notNull()"}, // TODO[ship-api]: unknown type "${f.type}"`,
|
|
165
|
+
);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const drizzleExpr = mapping.drizzle.replace("NAME", `"${colName}"`);
|
|
169
|
+
const notNull = f.nullable ? "" : ".notNull()";
|
|
170
|
+
lines.push(` ${f.name}: ${drizzleExpr}${notNull},`);
|
|
171
|
+
}
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderImportsForFields(fields) {
|
|
176
|
+
const imports = new Set();
|
|
177
|
+
for (const f of fields) {
|
|
178
|
+
if (!f.known) {
|
|
179
|
+
imports.add("text");
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Extract the function name from the drizzle expression.
|
|
183
|
+
const match = FIELD_TYPE_MAP[f.type].drizzle.match(/^(\w+)\(/);
|
|
184
|
+
if (match) imports.add(match[1]);
|
|
185
|
+
}
|
|
186
|
+
return imports;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderCreateSchemaFields(fields) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
for (const f of fields) {
|
|
192
|
+
const mapping = FIELD_TYPE_MAP[f.type];
|
|
193
|
+
const zod = mapping ? mapping.zod : "z.unknown() /* TODO[ship-api]: unknown type */";
|
|
194
|
+
const optional = f.nullable ? ".optional()" : "";
|
|
195
|
+
lines.push(` ${f.name}: ${zod}${optional},`);
|
|
196
|
+
}
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderUpdateSchemaFields(fields) {
|
|
201
|
+
// Update schemas: every field is optional (PATCH semantics).
|
|
202
|
+
const lines = [];
|
|
203
|
+
for (const f of fields) {
|
|
204
|
+
const mapping = FIELD_TYPE_MAP[f.type];
|
|
205
|
+
const zod = mapping ? mapping.zod : "z.unknown() /* TODO[ship-api]: unknown type */";
|
|
206
|
+
const nullable = f.nullable ? ".nullable()" : "";
|
|
207
|
+
lines.push(` ${f.name}: ${zod}${nullable}.optional(),`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function snakeize(camel) {
|
|
213
|
+
return camel.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function rewriteSchemaForFields(schemaPath, fields) {
|
|
217
|
+
const original = await fs.readFile(schemaPath, "utf8");
|
|
218
|
+
// Replace the default `name: varchar(...)` line with the rendered
|
|
219
|
+
// field columns. The template has exactly one `name:` line between
|
|
220
|
+
// the `id:` line and `...baseFields`.
|
|
221
|
+
const columnsBlock = renderSchemaColumns(fields);
|
|
222
|
+
const replaced = original.replace(
|
|
223
|
+
/ {2}name: varchar\("name", \{ length: 200 \}\)\.notNull\(\),/,
|
|
224
|
+
columnsBlock,
|
|
225
|
+
);
|
|
226
|
+
// Update the imports: the template imports varchar; now we import
|
|
227
|
+
// whatever the field types need.
|
|
228
|
+
const neededImports = renderImportsForFields(fields);
|
|
229
|
+
neededImports.add("pgTable");
|
|
230
|
+
neededImports.add("uuid"); // primary key
|
|
231
|
+
const importList = [...neededImports].sort().join(", ");
|
|
232
|
+
const withImports = replaced.replace(
|
|
233
|
+
/import \{ pgTable, uuid, varchar \} from "drizzle-orm\/pg-core";/,
|
|
234
|
+
`import { ${importList} } from "drizzle-orm/pg-core";`,
|
|
235
|
+
);
|
|
236
|
+
await fs.writeFile(schemaPath, withImports);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function rewriteValidationForFields(validationPath, fields) {
|
|
240
|
+
const original = await fs.readFile(validationPath, "utf8");
|
|
241
|
+
const createBlock = renderCreateSchemaFields(fields);
|
|
242
|
+
const updateBlock = renderUpdateSchemaFields(fields);
|
|
243
|
+
|
|
244
|
+
// The template's create schema has a single `name: z.string()...`
|
|
245
|
+
// line; replace it with the field block. Same for update.
|
|
246
|
+
const createReplaced = original.replace(
|
|
247
|
+
/ {4}name: z\.string\(\)\.min\(1\)\.max\(200\),\n/,
|
|
248
|
+
createBlock + "\n",
|
|
249
|
+
);
|
|
250
|
+
const updateReplaced = createReplaced.replace(
|
|
251
|
+
/ {4}name: z\.string\(\)\.min\(1\)\.max\(200\)\.optional\(\),\n/,
|
|
252
|
+
updateBlock + "\n",
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await fs.writeFile(validationPath, updateReplaced);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function printResourceReport(rawName, moduleResult, fields) {
|
|
259
|
+
console.log("");
|
|
260
|
+
console.log(
|
|
261
|
+
chalk.bold(`ship-api g resource ${rawName}`),
|
|
262
|
+
chalk.dim(`→ ${moduleResult.targetRoot}`),
|
|
263
|
+
);
|
|
264
|
+
console.log("");
|
|
265
|
+
for (const f of moduleResult.files) {
|
|
266
|
+
console.log(" ", chalk.green("✚"), path.relative(moduleResult.targetRoot, f));
|
|
267
|
+
}
|
|
268
|
+
console.log("");
|
|
269
|
+
console.log(chalk.dim("Fields:"));
|
|
270
|
+
for (const f of fields) {
|
|
271
|
+
const marker = f.nullable ? "?" : " ";
|
|
272
|
+
const known = f.known ? "" : chalk.yellow(" [unknown type — review]");
|
|
273
|
+
console.log(` ${marker} ${chalk.cyan(f.name)}: ${f.type}${known}`);
|
|
274
|
+
}
|
|
275
|
+
console.log("");
|
|
276
|
+
console.log(chalk.dim("Next steps:"));
|
|
277
|
+
console.log(
|
|
278
|
+
chalk.dim(" • Add the schema to your composed schema:"),
|
|
279
|
+
chalk.cyan(
|
|
280
|
+
`createApp({ schema: { ${moduleResult.names.namePlural}: ${moduleResult.names.namePlural} } })`,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
console.log(
|
|
284
|
+
chalk.dim(" • Mount the module sub-app on the root:"),
|
|
285
|
+
chalk.cyan(
|
|
286
|
+
`const routedApp = root.route("/", build${moduleResult.names.NamePlural}App({ service, provider }))`,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
console.log(chalk.dim(" • Run:"), chalk.cyan("npm run db:generate"));
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log(
|
|
292
|
+
chalk.dim("Scaffold metadata recorded in module.json (start-point for delivery metrics)."),
|
|
293
|
+
);
|
|
294
|
+
console.log("");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function generateModule(rawName, opts = {}) {
|
|
298
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
299
|
+
const targetRoot =
|
|
300
|
+
opts.targetRoot ?? path.join(cwd, "src", "modules", normalizeNamePlural(rawName));
|
|
301
|
+
|
|
302
|
+
const names = computeNames(rawName);
|
|
303
|
+
await ensureTargetEmpty(targetRoot, opts.force ?? false);
|
|
304
|
+
const written = await writeTemplate(TEMPLATE_ROOT, targetRoot, names);
|
|
305
|
+
await stampScaffoldMetadata(targetRoot, names, opts);
|
|
306
|
+
|
|
307
|
+
if (!opts.silent) {
|
|
308
|
+
printReport(rawName, names, targetRoot, written);
|
|
309
|
+
}
|
|
310
|
+
return { targetRoot, names, files: written };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── In-module file generators ────────────────────────────────────────
|
|
314
|
+
//
|
|
315
|
+
// `g middleware <name>` — adds a Hono middleware skeleton at
|
|
316
|
+
// `src/middleware/<name>.ts` in the consumer's project.
|
|
317
|
+
//
|
|
318
|
+
// `g service <name> -m <module>` — adds a `<NameSingular>Service.ts`
|
|
319
|
+
// to an existing module's `services/` directory.
|
|
320
|
+
//
|
|
321
|
+
// `g schema <name> -m <module>` — adds an extra Drizzle pgTable file
|
|
322
|
+
// to a module's tree at `modules/<module>/schemas/<name>.ts`.
|
|
323
|
+
//
|
|
324
|
+
// `g job <name> -m <module>` — adds a BullMQ job stub at
|
|
325
|
+
// `modules/<module>/jobs/<name>.job.ts`.
|
|
326
|
+
//
|
|
327
|
+
// All four use the same name-substitution machinery as `g module`.
|
|
328
|
+
|
|
329
|
+
export async function generateMiddleware(rawName, opts = {}) {
|
|
330
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
331
|
+
const names = computeMiddlewareNames(rawName);
|
|
332
|
+
const targetPath =
|
|
333
|
+
opts.targetPath ?? path.join(cwd, "src", "middleware", `${names.nameKebab}.ts`);
|
|
334
|
+
await ensureFileVacant(targetPath, opts.force ?? false);
|
|
335
|
+
|
|
336
|
+
const contents = renderMiddleware(names);
|
|
337
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
338
|
+
await fs.writeFile(targetPath, contents);
|
|
339
|
+
|
|
340
|
+
if (!opts.silent) printSingleFileReport("middleware", rawName, targetPath);
|
|
341
|
+
return { targetPath, names };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function generateService(rawName, opts = {}) {
|
|
345
|
+
if (!opts.module) {
|
|
346
|
+
throw new Error("g service: --module is required (which module to add the service to)");
|
|
347
|
+
}
|
|
348
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
349
|
+
const moduleNames = computeNames(opts.module);
|
|
350
|
+
const moduleDir = path.join(cwd, "src", "modules", moduleNames.namePlural);
|
|
351
|
+
await ensureModuleDirExists(moduleDir, opts.module);
|
|
352
|
+
|
|
353
|
+
const names = computeServiceNames(rawName);
|
|
354
|
+
const fileName = `${names.NameSingular}Service.ts`;
|
|
355
|
+
const targetPath = opts.targetPath ?? path.join(moduleDir, "services", fileName);
|
|
356
|
+
await ensureFileVacant(targetPath, opts.force ?? false);
|
|
357
|
+
|
|
358
|
+
const contents = renderService(names, moduleNames);
|
|
359
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
360
|
+
await fs.writeFile(targetPath, contents);
|
|
361
|
+
|
|
362
|
+
if (!opts.silent) printSingleFileReport("service", rawName, targetPath);
|
|
363
|
+
return { targetPath, names, moduleNames };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function generateSchema(rawName, opts = {}) {
|
|
367
|
+
if (!opts.module) {
|
|
368
|
+
throw new Error("g schema: --module is required");
|
|
369
|
+
}
|
|
370
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
371
|
+
const moduleNames = computeNames(opts.module);
|
|
372
|
+
const moduleDir = path.join(cwd, "src", "modules", moduleNames.namePlural);
|
|
373
|
+
await ensureModuleDirExists(moduleDir, opts.module);
|
|
374
|
+
|
|
375
|
+
const names = computeSchemaNames(rawName);
|
|
376
|
+
const targetPath = opts.targetPath ?? path.join(moduleDir, "schemas", `${names.nameKebab}.ts`);
|
|
377
|
+
await ensureFileVacant(targetPath, opts.force ?? false);
|
|
378
|
+
|
|
379
|
+
const contents = renderSchema(names);
|
|
380
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
381
|
+
await fs.writeFile(targetPath, contents);
|
|
382
|
+
|
|
383
|
+
if (!opts.silent) printSingleFileReport("schema", rawName, targetPath);
|
|
384
|
+
return { targetPath, names, moduleNames };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export async function generateJob(rawName, opts = {}) {
|
|
388
|
+
if (!opts.module) {
|
|
389
|
+
throw new Error("g job: --module is required");
|
|
390
|
+
}
|
|
391
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
392
|
+
const moduleNames = computeNames(opts.module);
|
|
393
|
+
const moduleDir = path.join(cwd, "src", "modules", moduleNames.namePlural);
|
|
394
|
+
await ensureModuleDirExists(moduleDir, opts.module);
|
|
395
|
+
|
|
396
|
+
const names = computeJobNames(rawName);
|
|
397
|
+
const targetPath = opts.targetPath ?? path.join(moduleDir, "jobs", `${names.nameKebab}.job.ts`);
|
|
398
|
+
await ensureFileVacant(targetPath, opts.force ?? false);
|
|
399
|
+
|
|
400
|
+
const contents = renderJob(names, moduleNames);
|
|
401
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
402
|
+
await fs.writeFile(targetPath, contents);
|
|
403
|
+
|
|
404
|
+
if (!opts.silent) printSingleFileReport("job", rawName, targetPath);
|
|
405
|
+
return { targetPath, names, moduleNames };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── Name helpers (each subcommand has a slightly different shape) ────
|
|
409
|
+
|
|
410
|
+
function computeMiddlewareNames(raw) {
|
|
411
|
+
const kebab = rawNameToKebab(raw, "g middleware");
|
|
412
|
+
return {
|
|
413
|
+
nameKebab: kebab,
|
|
414
|
+
nameCamel: kebabToCamel(kebab),
|
|
415
|
+
NamePascal: kebabToPascal(kebab),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function computeServiceNames(raw) {
|
|
420
|
+
const kebab = rawNameToKebab(raw, "g service");
|
|
421
|
+
// Service names are typically singular ("Account", not "Accounts").
|
|
422
|
+
const singular = singularizeKebab(kebab);
|
|
423
|
+
return {
|
|
424
|
+
nameKebab: singular,
|
|
425
|
+
NameSingular: kebabToPascal(singular),
|
|
426
|
+
nameCamelSingular: kebabToCamel(singular),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function computeSchemaNames(raw) {
|
|
431
|
+
const kebab = rawNameToKebab(raw, "g schema");
|
|
432
|
+
// Pluralized for table name (matches the v4 convention — tables are plural).
|
|
433
|
+
const plural = pluralizeKebab(singularizeKebab(kebab));
|
|
434
|
+
return {
|
|
435
|
+
nameKebab: kebab,
|
|
436
|
+
namePlural: plural,
|
|
437
|
+
nameSingular: singularizeKebab(plural),
|
|
438
|
+
nameCamelPlural: kebabToCamel(plural),
|
|
439
|
+
NamePascal: kebabToPascal(singularizeKebab(plural)),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function computeJobNames(raw) {
|
|
444
|
+
const kebab = rawNameToKebab(raw, "g job");
|
|
445
|
+
return {
|
|
446
|
+
nameKebab: kebab,
|
|
447
|
+
nameCamel: kebabToCamel(kebab),
|
|
448
|
+
NamePascal: kebabToPascal(kebab),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function rawNameToKebab(raw, contextLabel) {
|
|
453
|
+
const trimmed = raw.trim();
|
|
454
|
+
if (!trimmed) throw new Error(`${contextLabel}: name cannot be empty`);
|
|
455
|
+
const kebab = trimmed
|
|
456
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
457
|
+
.replace(/[_\s]+/g, "-")
|
|
458
|
+
.toLowerCase();
|
|
459
|
+
const cleaned = kebab.replace(/^-+|-+$/g, "");
|
|
460
|
+
if (!/^[a-z][a-z0-9-]*$/.test(cleaned)) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`${contextLabel}: invalid name "${raw}" — must be kebab-case starting with a letter`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return cleaned;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── File renderers ────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
function renderMiddleware(names) {
|
|
471
|
+
return `import type { MiddlewareHandler } from "hono";
|
|
472
|
+
import type { AppEnv } from "@sixthsummit/ship-api/http/app/createApp";
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* \`${names.nameCamel}({ ... })\` — Hono middleware factory.
|
|
476
|
+
*
|
|
477
|
+
* Customize the options + the before/after-handler logic. Wire the
|
|
478
|
+
* middleware into the route chain (or globally via \`app.use("*", ...)\`).
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
export interface ${names.NamePascal}Options {
|
|
482
|
+
// TODO: declare middleware-specific options here.
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function ${names.nameCamel}(
|
|
486
|
+
opts: ${names.NamePascal}Options = {},
|
|
487
|
+
): MiddlewareHandler<AppEnv> {
|
|
488
|
+
void opts;
|
|
489
|
+
return async (c, next) => {
|
|
490
|
+
// Before-handler logic (auth checks, request enrichment, etc.).
|
|
491
|
+
|
|
492
|
+
await next();
|
|
493
|
+
|
|
494
|
+
// After-handler logic (response shaping, headers, observability).
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function renderService(names, moduleNames) {
|
|
501
|
+
return `import { DrizzleEntityService } from "@sixthsummit/ship-api/app/services/DrizzleEntityService";
|
|
502
|
+
import { ${moduleNames.nameCamelPlural} } from "../schema.js";
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* \`${names.NameSingular}Service\` — extends \`DrizzleEntityService\` against
|
|
506
|
+
* the module's \`${moduleNames.nameCamelPlural}\` table. Inherits get / getAll /
|
|
507
|
+
* list / count / create / update / delete with soft-delete + audit-column
|
|
508
|
+
* auto-stamping; add domain-specific methods below.
|
|
509
|
+
*/
|
|
510
|
+
export class ${names.NameSingular}Service extends DrizzleEntityService<
|
|
511
|
+
typeof ${moduleNames.nameCamelPlural},
|
|
512
|
+
{ ${moduleNames.nameCamelPlural}: typeof ${moduleNames.nameCamelPlural} }
|
|
513
|
+
> {
|
|
514
|
+
protected getTable() {
|
|
515
|
+
return ${moduleNames.nameCamelPlural};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
protected override getDefaultSearchFields(): readonly string[] {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Domain-specific methods go here. Example:
|
|
523
|
+
//
|
|
524
|
+
// async findByExternalId(externalId: string) { ... }
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function renderSchema(names) {
|
|
530
|
+
return `import { pgTable, uuid, varchar } from "drizzle-orm/pg-core";
|
|
531
|
+
import { baseFields } from "@sixthsummit/ship-api/app/db/baseFields";
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Replace this skeleton with the real columns + indexes for
|
|
535
|
+
* \`${names.namePlural}\`. Run \`npm run db:generate\` after editing.
|
|
536
|
+
*/
|
|
537
|
+
export const ${names.nameCamelPlural} = pgTable("${names.namePlural}", {
|
|
538
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
539
|
+
name: varchar("name", { length: 200 }).notNull(),
|
|
540
|
+
...baseFields,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
export type ${names.NamePascal} = typeof ${names.nameCamelPlural}.$inferSelect;
|
|
544
|
+
export type New${names.NamePascal} = typeof ${names.nameCamelPlural}.$inferInsert;
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function renderJob(names, moduleNames) {
|
|
549
|
+
return `import { z } from "zod";
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* \`${names.nameKebab}\` — BullMQ job for the ${moduleNames.namePlural} module.
|
|
553
|
+
*
|
|
554
|
+
* Replace the payload schema + processor body with the real
|
|
555
|
+
* implementation. The framework auto-discovers default exports
|
|
556
|
+
* named \`${names.nameKebab}.job.ts\` when \`InitJobs\` runs at boot
|
|
557
|
+
* (with \`WORKER_MODE=worker\` or \`all\`).
|
|
558
|
+
*
|
|
559
|
+
* NOTE: This is a stub that ships ahead of the framework's
|
|
560
|
+
* \`defineJob\` API export. Once \`defineJob\` lands in the public
|
|
561
|
+
* barrel, swap the placeholder export below for:
|
|
562
|
+
*
|
|
563
|
+
* import { defineJob } from "@sixthsummit/ship-api";
|
|
564
|
+
* export default defineJob({ ... });
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
export const ${names.nameCamel}PayloadSchema = z.object({
|
|
568
|
+
// TODO: declare the payload shape.
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
export type ${names.NamePascal}Payload = z.infer<
|
|
572
|
+
typeof ${names.nameCamel}PayloadSchema
|
|
573
|
+
>;
|
|
574
|
+
|
|
575
|
+
export const ${names.nameCamel}Job = {
|
|
576
|
+
queue: "${moduleNames.namePlural}",
|
|
577
|
+
name: "${names.nameKebab}",
|
|
578
|
+
payloadSchema: ${names.nameCamel}PayloadSchema,
|
|
579
|
+
async processor(payload: ${names.NamePascal}Payload): Promise<void> {
|
|
580
|
+
void payload;
|
|
581
|
+
// TODO: implement.
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
export default ${names.nameCamel}Job;
|
|
586
|
+
`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ─── Shared filesystem helpers ─────────────────────────────────────────
|
|
590
|
+
|
|
591
|
+
async function ensureFileVacant(targetPath, force) {
|
|
592
|
+
try {
|
|
593
|
+
await fs.access(targetPath);
|
|
594
|
+
if (!force) {
|
|
595
|
+
throw new Error(`Target ${targetPath} already exists (pass --force to overwrite)`);
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
if (err && err.code === "ENOENT") return;
|
|
599
|
+
if (err.message?.startsWith("Target")) throw err;
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function ensureModuleDirExists(moduleDir, moduleName) {
|
|
605
|
+
try {
|
|
606
|
+
const stat = await fs.stat(moduleDir);
|
|
607
|
+
if (!stat.isDirectory()) {
|
|
608
|
+
throw new Error(`Module path ${moduleDir} is not a directory`);
|
|
609
|
+
}
|
|
610
|
+
} catch (err) {
|
|
611
|
+
if (err && err.code === "ENOENT") {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Module "${moduleName}" not found at ${moduleDir} — run \`ship-api g module ${moduleName}\` first`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function printSingleFileReport(kind, rawName, targetPath) {
|
|
621
|
+
console.log("");
|
|
622
|
+
console.log(chalk.bold(`ship-api g ${kind} ${rawName}`), chalk.dim(`→ ${targetPath}`));
|
|
623
|
+
console.log("");
|
|
624
|
+
console.log(" ", chalk.green("✚"), targetPath);
|
|
625
|
+
console.log("");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function normalizeNamePlural(raw) {
|
|
629
|
+
// Accept "accounts", "Account", "user-tokens", "userToken", etc.; emit
|
|
630
|
+
// canonical kebab-case-plural. Idempotent — feeding "accounts" back in
|
|
631
|
+
// returns "accounts" (not "accountses").
|
|
632
|
+
const trimmed = raw.trim();
|
|
633
|
+
if (!trimmed) throw new Error("g module: name cannot be empty");
|
|
634
|
+
// Convert camelCase / PascalCase boundaries to dashes, lowercase.
|
|
635
|
+
const kebab = trimmed
|
|
636
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
637
|
+
.replace(/[_\s]+/g, "-")
|
|
638
|
+
.toLowerCase();
|
|
639
|
+
const cleaned = kebab.replace(/^-+|-+$/g, "");
|
|
640
|
+
if (!/^[a-z][a-z0-9-]*$/.test(cleaned)) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`g module: invalid name "${raw}" — must be kebab-case starting with a letter (a-z, 0-9, -).`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
// Idempotent pluralization: re-pluralizing the singularized form
|
|
646
|
+
// recovers the same plural regardless of what the user passed.
|
|
647
|
+
return pluralizeKebab(singularizeKebab(cleaned));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function computeNames(raw) {
|
|
651
|
+
const namePlural = normalizeNamePlural(raw);
|
|
652
|
+
const nameSingular = singularizeKebab(namePlural);
|
|
653
|
+
return {
|
|
654
|
+
namePlural,
|
|
655
|
+
nameSingular,
|
|
656
|
+
nameCamelPlural: kebabToCamel(namePlural),
|
|
657
|
+
nameCamelSingular: kebabToCamel(nameSingular),
|
|
658
|
+
NamePlural: kebabToPascal(namePlural),
|
|
659
|
+
NameSingular: kebabToPascal(nameSingular),
|
|
660
|
+
NAME_PLURAL: namePlural.replace(/-/g, "_").toUpperCase(),
|
|
661
|
+
NAME_SINGULAR: nameSingular.replace(/-/g, "_").toUpperCase(),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function kebabToPascal(kebab) {
|
|
666
|
+
return kebab
|
|
667
|
+
.split("-")
|
|
668
|
+
.filter(Boolean)
|
|
669
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
670
|
+
.join("");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function kebabToCamel(kebab) {
|
|
674
|
+
const parts = kebab.split("-").filter(Boolean);
|
|
675
|
+
if (parts.length === 0) return "";
|
|
676
|
+
return (
|
|
677
|
+
parts[0] +
|
|
678
|
+
parts
|
|
679
|
+
.slice(1)
|
|
680
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
681
|
+
.join("")
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Pluralize the LAST segment of a kebab-case identifier — keeps multi-
|
|
687
|
+
* word module names readable (`user-token` → `user-tokens`, not
|
|
688
|
+
* `user-tokens` becoming the unholy `user-tokenses`).
|
|
689
|
+
*/
|
|
690
|
+
function pluralizeKebab(kebab) {
|
|
691
|
+
const parts = kebab.split("-");
|
|
692
|
+
parts[parts.length - 1] = pluralizeWord(parts[parts.length - 1]);
|
|
693
|
+
return parts.join("-");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function singularizeKebab(kebab) {
|
|
697
|
+
const parts = kebab.split("-");
|
|
698
|
+
parts[parts.length - 1] = singularizeWord(parts[parts.length - 1]);
|
|
699
|
+
return parts.join("-");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Minimal English pluralization — handles the common cases that show up
|
|
704
|
+
* in module names. Engineers naming a module `mongoose-octopi` get
|
|
705
|
+
* what they deserve.
|
|
706
|
+
*/
|
|
707
|
+
function pluralizeWord(word) {
|
|
708
|
+
if (word.endsWith("y") && !"aeiou".includes(word[word.length - 2]))
|
|
709
|
+
return word.slice(0, -1) + "ies";
|
|
710
|
+
if (
|
|
711
|
+
word.endsWith("s") ||
|
|
712
|
+
word.endsWith("x") ||
|
|
713
|
+
word.endsWith("z") ||
|
|
714
|
+
word.endsWith("ch") ||
|
|
715
|
+
word.endsWith("sh")
|
|
716
|
+
)
|
|
717
|
+
return word + "es";
|
|
718
|
+
if (word.endsWith("o") && !"aeiou".includes(word[word.length - 2])) return word + "es";
|
|
719
|
+
return word + "s";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function singularizeWord(word) {
|
|
723
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
724
|
+
if (word.endsWith("sses") || word.endsWith("shes") || word.endsWith("ches"))
|
|
725
|
+
return word.slice(0, -2);
|
|
726
|
+
if (word.endsWith("xes") || word.endsWith("zes") || word.endsWith("oes"))
|
|
727
|
+
return word.slice(0, -2);
|
|
728
|
+
if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
|
|
729
|
+
return word;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Backwards-compat exports for tests that already imported the per-segment
|
|
733
|
+
// helpers under the original names.
|
|
734
|
+
const pluralize = pluralizeWord;
|
|
735
|
+
const singularize = singularizeWord;
|
|
736
|
+
|
|
737
|
+
async function ensureTargetEmpty(target, force) {
|
|
738
|
+
try {
|
|
739
|
+
const stat = await fs.stat(target);
|
|
740
|
+
if (stat.isDirectory()) {
|
|
741
|
+
const entries = await fs.readdir(target);
|
|
742
|
+
if (entries.length > 0 && !force) {
|
|
743
|
+
throw new Error(`g module: target ${target} is not empty (pass --force to overwrite)`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
} catch (err) {
|
|
747
|
+
if (err && err.code === "ENOENT") return;
|
|
748
|
+
if (err.message?.startsWith("g module:")) throw err;
|
|
749
|
+
throw err;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function writeTemplate(templateRoot, targetRoot, names) {
|
|
754
|
+
const files = [];
|
|
755
|
+
await walk(templateRoot, templateRoot, async (srcPath, relPath) => {
|
|
756
|
+
if (!srcPath.endsWith(".tpl")) return;
|
|
757
|
+
const targetRel = applyNameSubstitutions(relPath.slice(0, -".tpl".length), names);
|
|
758
|
+
const targetPath = path.join(targetRoot, targetRel);
|
|
759
|
+
const contents = await fs.readFile(srcPath, "utf8");
|
|
760
|
+
const substituted = applyNameSubstitutions(contents, names);
|
|
761
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
762
|
+
await fs.writeFile(targetPath, substituted);
|
|
763
|
+
files.push(targetPath);
|
|
764
|
+
});
|
|
765
|
+
return files;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Recursive directory walker. The `root` argument is the original walk
|
|
770
|
+
* root and stays constant through recursion so `relPath` is always the
|
|
771
|
+
* subpath under the original root, not just the filename.
|
|
772
|
+
*/
|
|
773
|
+
async function walk(root, dir, visit) {
|
|
774
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
775
|
+
for (const entry of entries) {
|
|
776
|
+
const full = path.join(dir, entry.name);
|
|
777
|
+
if (entry.isDirectory()) {
|
|
778
|
+
await walk(root, full, visit);
|
|
779
|
+
} else if (entry.isFile()) {
|
|
780
|
+
const rel = path.relative(root, full);
|
|
781
|
+
await visit(full, rel);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function applyNameSubstitutions(text, names) {
|
|
787
|
+
// Order matters: replace the longer, more-specific tokens first so
|
|
788
|
+
// `{{NamePlural}}` doesn't get half-eaten by `{{Name`-prefix matches.
|
|
789
|
+
return text
|
|
790
|
+
.replaceAll("{{nameCamelPlural}}", names.nameCamelPlural)
|
|
791
|
+
.replaceAll("{{nameCamelSingular}}", names.nameCamelSingular)
|
|
792
|
+
.replaceAll("{{NamePlural}}", names.NamePlural)
|
|
793
|
+
.replaceAll("{{NameSingular}}", names.NameSingular)
|
|
794
|
+
.replaceAll("{{NAME_PLURAL}}", names.NAME_PLURAL)
|
|
795
|
+
.replaceAll("{{NAME_SINGULAR}}", names.NAME_SINGULAR)
|
|
796
|
+
.replaceAll("{{namePlural}}", names.namePlural)
|
|
797
|
+
.replaceAll("{{nameSingular}}", names.nameSingular);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Stamp un-backfillable scaffold provenance into the module's
|
|
802
|
+
* `module.json`. This is the start-point breadcrumb for the
|
|
803
|
+
* "time-to-module" delivery metric (see `docs/DELIVERY_METRICS.md`, M3):
|
|
804
|
+
* it records *when* a module was generated, by *which* generator, and
|
|
805
|
+
* under *which* framework version — independent of commit-message
|
|
806
|
+
* discipline, because the field rides into git as part of the module and
|
|
807
|
+
* cannot be reconstructed after the fact.
|
|
808
|
+
*
|
|
809
|
+
* The timestamp and framework version are injectable via `opts.now` and
|
|
810
|
+
* `opts.frameworkVersion` so the generator stays deterministic under test.
|
|
811
|
+
* If a (future) template ships no `module.json`, stamping is a no-op
|
|
812
|
+
* rather than an error.
|
|
813
|
+
*/
|
|
814
|
+
async function stampScaffoldMetadata(targetRoot, names, opts) {
|
|
815
|
+
const manifestPath = path.join(targetRoot, "module.json");
|
|
816
|
+
let manifest;
|
|
817
|
+
try {
|
|
818
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
819
|
+
} catch {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
manifest.scaffold = {
|
|
823
|
+
generator: opts.generator ?? "module",
|
|
824
|
+
name: names.namePlural,
|
|
825
|
+
at: normalizeTimestamp(opts.now),
|
|
826
|
+
frameworkVersion: opts.frameworkVersion ?? (await resolveFrameworkVersion()),
|
|
827
|
+
tool: detectScaffoldTool(opts),
|
|
828
|
+
};
|
|
829
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Best-effort detection of the agent/editor that drove this scaffold, for the
|
|
834
|
+
* by-tool split of the time-to-module metric (DELIVERY_METRICS M3/M6 — agent
|
|
835
|
+
* output-parity). Precedence: explicit `opts.tool` (test injection) >
|
|
836
|
+
* `SHIP_API_SCAFFOLD_TOOL` (the reliable override) > env markers > "cli".
|
|
837
|
+
*
|
|
838
|
+
* Marker caveats — why the override is the canonical path:
|
|
839
|
+
* - Claude Code exports `CLAUDECODE=1` (its documented presence flag). We key
|
|
840
|
+
* on exactly that, not the broader `CLAUDE_CODE_*` family, because env is
|
|
841
|
+
* inherited by child processes — a Cursor/CLI run launched from a shell that
|
|
842
|
+
* once hosted Claude Code could otherwise be mislabelled. (The single flag
|
|
843
|
+
* narrows, though doesn't eliminate, that inheritance surface.)
|
|
844
|
+
* - Cursor has no canonical equivalent of `CLAUDECODE`; `CURSOR_TRACE_ID` is a
|
|
845
|
+
* best-effort signal and is often absent, so a Cursor run usually falls
|
|
846
|
+
* through to "cli". Cursor users should set `SHIP_API_SCAFFOLD_TOOL=cursor`
|
|
847
|
+
* so the by-tool metric attributes correctly.
|
|
848
|
+
*
|
|
849
|
+
* "cli" is a valid, honest answer, not a failure. `env` is injectable for
|
|
850
|
+
* tests. (Reading process.env here is fine — build tooling, not framework
|
|
851
|
+
* runtime, so it doesn't route through `defineEnv`.)
|
|
852
|
+
*/
|
|
853
|
+
export function detectScaffoldTool(opts = {}, env = process.env) {
|
|
854
|
+
if (opts.tool) return opts.tool;
|
|
855
|
+
if (env.SHIP_API_SCAFFOLD_TOOL) return env.SHIP_API_SCAFFOLD_TOOL;
|
|
856
|
+
if (env.CLAUDECODE) return "claude-code";
|
|
857
|
+
if (env.CURSOR_TRACE_ID) return "cursor";
|
|
858
|
+
return "cli";
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function normalizeTimestamp(now) {
|
|
862
|
+
if (now instanceof Date) return now.toISOString();
|
|
863
|
+
if (typeof now === "string" && now.length > 0) return now;
|
|
864
|
+
return new Date().toISOString();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
let _frameworkVersionCache;
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* The version of `@sixthsummit/ship-api` running this generator, read from
|
|
871
|
+
* the package root (two levels up from `cli/commands/`) — used as the
|
|
872
|
+
* default `frameworkVersion` in scaffold provenance. Falls back to
|
|
873
|
+
* `"unknown"` if the package.json can't be read. Memoized.
|
|
874
|
+
*/
|
|
875
|
+
async function resolveFrameworkVersion() {
|
|
876
|
+
if (_frameworkVersionCache !== undefined) return _frameworkVersionCache;
|
|
877
|
+
try {
|
|
878
|
+
const pkgPath = path.join(__dirname, "..", "..", "package.json");
|
|
879
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
880
|
+
_frameworkVersionCache = typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
881
|
+
} catch {
|
|
882
|
+
_frameworkVersionCache = "unknown";
|
|
883
|
+
}
|
|
884
|
+
return _frameworkVersionCache;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function printReport(rawName, names, targetRoot, written) {
|
|
888
|
+
console.log("");
|
|
889
|
+
console.log(chalk.bold(`ship-api g module ${rawName}`), chalk.dim(`→ ${targetRoot}`));
|
|
890
|
+
console.log("");
|
|
891
|
+
for (const f of written) {
|
|
892
|
+
console.log(" ", chalk.green("✚"), path.relative(targetRoot, f));
|
|
893
|
+
}
|
|
894
|
+
console.log("");
|
|
895
|
+
console.log(chalk.dim("Next steps:"));
|
|
896
|
+
console.log(
|
|
897
|
+
chalk.dim(" • Add the schema to your composed schema:"),
|
|
898
|
+
chalk.cyan(`createApp({ schema: { ${names.namePlural}: ${names.namePlural} } })`),
|
|
899
|
+
);
|
|
900
|
+
console.log(
|
|
901
|
+
chalk.dim(
|
|
902
|
+
" • Mount the module sub-app on the root (capture the return — it carries the RPC types):",
|
|
903
|
+
),
|
|
904
|
+
chalk.cyan(
|
|
905
|
+
`const routedApp = root.route("/", build${names.NamePlural}App({ service, provider }))`,
|
|
906
|
+
),
|
|
907
|
+
);
|
|
908
|
+
console.log(
|
|
909
|
+
chalk.dim(" and point the typed-client export at it:"),
|
|
910
|
+
chalk.cyan(`export type ShipApiClient = typeof routedApp`),
|
|
911
|
+
);
|
|
912
|
+
console.log(chalk.dim(" • Run:"), chalk.cyan("npm run db:generate"));
|
|
913
|
+
console.log("");
|
|
914
|
+
console.log(
|
|
915
|
+
chalk.dim("Scaffold metadata recorded in module.json (start-point for delivery metrics)."),
|
|
916
|
+
);
|
|
917
|
+
console.log("");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Exposed for tests.
|
|
921
|
+
export const __test = {
|
|
922
|
+
computeNames,
|
|
923
|
+
applyNameSubstitutions,
|
|
924
|
+
pluralize,
|
|
925
|
+
singularize,
|
|
926
|
+
kebabToPascal,
|
|
927
|
+
kebabToCamel,
|
|
928
|
+
computeMiddlewareNames,
|
|
929
|
+
computeServiceNames,
|
|
930
|
+
computeSchemaNames,
|
|
931
|
+
computeJobNames,
|
|
932
|
+
parseFieldSpec,
|
|
933
|
+
renderSchemaColumns,
|
|
934
|
+
renderCreateSchemaFields,
|
|
935
|
+
renderUpdateSchemaFields,
|
|
936
|
+
TEMPLATE_ROOT,
|
|
937
|
+
};
|