@tatara/cli 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/bin.mjs +590 -14
  2. package/package.json +1 -1
  3. package/templates/features/admin/files/src/apps/admin/server.ts +3 -2
  4. package/templates/features/admin/files/src/modules/example/routes/admin-greeting.routes.ts +1 -1
  5. package/templates/features/auth/files/src/core/auth/middleware.ts +1 -1
  6. package/templates/features/auth/files/src/core/auth/routes/me.routes.ts +1 -1
  7. package/templates/features/auth-keycloak/files/src/core/auth/commands/keycloak-token.ts +1 -1
  8. package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +1 -1
  9. package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +1 -1
  10. package/templates/features/authz/files/src/core/authz/middleware.ts +1 -1
  11. package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +1 -1
  12. package/templates/features/authz/files/src/core/authz/services/grants.ts +1 -1
  13. package/templates/features/authz/files/src/core/authz/services/members.ts +1 -1
  14. package/templates/features/authz/files/src/core/authz/services/roles.ts +1 -1
  15. package/templates/features/base/files/AGENTS.md +1 -0
  16. package/templates/features/base/files/src/apps/api/server.ts +3 -2
  17. package/templates/features/base/files/src/apps/cli/index.ts +4 -4
  18. package/templates/features/base/files/src/core/http/app.ts +28 -41
  19. package/templates/features/base/files/src/core/http/routes.ts +7 -11
  20. package/templates/features/base/files/src/core/logging/logger.ts +4 -12
  21. package/templates/features/base/files/src/modules/example/routes/greeting.routes.ts +1 -1
  22. package/templates/features/db/files/src/core/db/commands/migrate.ts +1 -1
  23. package/templates/features/db/files/src/core/db/commands/reset.ts +1 -1
  24. package/templates/features/db/files/src/core/db/commands/seed.ts +1 -1
  25. package/templates/features/db/files/src/core/db/tests/reset.test.ts +1 -1
  26. package/templates/features/drizzle/files+mysql/src/core/db/client.ts +2 -2
  27. package/templates/features/drizzle/files+postgres/src/core/db/client.ts +2 -2
  28. package/templates/features/email/files/src/core/email/commands/send-test.ts +1 -1
  29. package/templates/features/jobs/files/src/apps/cron/index.ts +2 -2
  30. package/templates/features/jobs/files/src/apps/worker/index.ts +2 -2
  31. package/templates/features/jobs/files/src/core/jobs/commands/enqueue.ts +1 -1
  32. package/templates/features/jobs/files/src/core/jobs/queue.ts +1 -1
  33. package/templates/features/jobs/files/src/core/jobs/redis.ts +1 -1
  34. package/templates/features/prisma/files+mysql/src/core/db/client.ts +2 -2
  35. package/templates/features/prisma/files+postgres/src/core/db/client.ts +2 -2
  36. package/templates/features/storage/files/src/core/storage/commands/check.ts +1 -1
  37. package/templates/generators/command/files/src/modules/__moduleKebab__/commands/__nameKebab__.ts +1 -1
  38. package/templates/generators/module/files/src/modules/__moduleKebab__/routes/__moduleKebab__.routes.ts +1 -1
  39. package/templates/generators/resource/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +1 -1
  40. package/templates/generators/resource/files+drizzle/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +2 -2
  41. package/templates/generators/resource/files+prisma/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +2 -2
  42. package/templates/generators/route/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +1 -1
  43. package/templates/versions.json +3 -1
  44. package/templates/features/base/files/src/core/commands/define.ts +0 -33
  45. package/templates/features/base/files/src/core/commands/runner.ts +0 -54
  46. package/templates/features/base/files/src/core/commands/tests/flags.test.ts +0 -16
  47. package/templates/features/base/files/src/core/http/errors.ts +0 -79
  48. package/templates/features/base/files/src/core/http/health.ts +0 -25
  49. package/templates/features/base/files/src/core/http/middleware/error-handler.ts +0 -36
  50. package/templates/features/base/files/src/core/http/middleware/request-context.ts +0 -24
  51. package/templates/features/base/files/src/core/http/middleware/validation.ts +0 -17
  52. package/templates/features/base/files/src/core/http/response.ts +0 -72
  53. package/templates/features/base/files/src/core/http/server.ts +0 -15
  54. package/templates/features/base/files/src/core/process/lifecycle.ts +0 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tatara/cli",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Create and grow opinionated Hono backends",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
+ import { startServer } from "@tatara/http";
1
2
  import { app } from "#apps/admin/app";
2
3
  import { env } from "#core/config/env";
3
- import { startServer } from "#core/http/server";
4
+ import { logger } from "#core/logging/logger";
4
5
 
5
- startServer(app, env.ADMIN_PORT, "admin");
6
+ startServer(app, { port: env.ADMIN_PORT, name: "admin", logger });
@@ -1,6 +1,6 @@
1
1
  import { createRoute } from "@hono/zod-openapi";
2
2
  import { currentPrincipal } from "#core/auth/middleware";
3
- import { errorResponses, jsonContent, success, successSchema } from "#core/http/response";
3
+ import { errorResponses, jsonContent, success, successSchema } from "@tatara/http";
4
4
  import { createRouter, defineRoutes } from "#core/http/routes";
5
5
  import { greetingSchema } from "#modules/example/validators/greeting.validators";
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { createMiddleware } from "hono/factory";
2
2
  import type { Context } from "hono";
3
3
  import type { AppEnv, Principal } from "#core/http/context";
4
- import { ForbiddenError, UnauthorizedError } from "#core/http/errors";
4
+ import { ForbiddenError, UnauthorizedError } from "@tatara/http";
5
5
 
6
6
  /** 401 unless the request carries a valid session or token. */
7
7
  export function requireAuth() {
@@ -1,6 +1,6 @@
1
1
  import { createRoute, z } from "@hono/zod-openapi";
2
2
  import { currentPrincipal, requireAuth } from "#core/auth/middleware";
3
- import { errorResponses, jsonContent, success, successSchema } from "#core/http/response";
3
+ import { errorResponses, jsonContent, success, successSchema } from "@tatara/http";
4
4
  import { createRouter, defineRoutes } from "#core/http/routes";
5
5
 
6
6
  const principalSchema = z
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { issuer } from "#core/auth/keycloak";
3
- import { defineCommand } from "#core/commands/define";
3
+ import { defineCommand } from "@tatara/core";
4
4
  import { env } from "#core/config/env";
5
5
 
6
6
  export default defineCommand({
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "#core/authz/constants";
3
3
  import { store } from "#core/authz/db/store";
4
4
  import { assignRole } from "#core/authz/services/members";
5
- import { defineCommand } from "#core/commands/define";
5
+ import { defineCommand } from "@tatara/core";
6
6
 
7
7
  export default defineCommand({
8
8
  name: "authz:grant-role",
@@ -1,4 +1,4 @@
1
- import { defineCommand } from "#core/commands/define";
1
+ import { defineCommand } from "@tatara/core";
2
2
  import { syncSystemRoles } from "#core/authz/services/sync";
3
3
 
4
4
  export default defineCommand({
@@ -2,7 +2,7 @@ import { createMiddleware } from "hono/factory";
2
2
  import type { Scope } from "#core/authz/constants";
3
3
  import { checkPermission } from "#core/authz/resolver";
4
4
  import type { AppEnv } from "#core/http/context";
5
- import { ForbiddenError, UnauthorizedError } from "#core/http/errors";
5
+ import { ForbiddenError, UnauthorizedError } from "@tatara/http";
6
6
  import type { PermissionKey } from "#generated/permissions";
7
7
 
8
8
  /**
@@ -19,7 +19,7 @@ import {
19
19
  userParams,
20
20
  userRoleParams,
21
21
  } from "#core/authz/validators";
22
- import { errorResponses, jsonBody, jsonContent, success, successSchema } from "#core/http/response";
22
+ import { errorResponses, jsonBody, jsonContent, success, successSchema } from "@tatara/http";
23
23
  import { createRouter, defineRoutes } from "#core/http/routes";
24
24
  import { catalog } from "#generated/permissions";
25
25
 
@@ -1,7 +1,7 @@
1
1
  import { assertGrantValid, InvalidGrantError, META_RESOURCES } from "#core/authz/catalog";
2
2
  import type { Scope } from "#core/authz/constants";
3
3
  import type { PermissionGrant } from "#core/authz/types";
4
- import { BusinessRuleError } from "#core/http/errors";
4
+ import { BusinessRuleError } from "@tatara/http";
5
5
  import { catalog } from "#generated/permissions";
6
6
 
7
7
  /** Validates grants against the catalog; custom roles and overlays may not grant meta resources. */
@@ -3,7 +3,7 @@ import { type OverlayEffect, type Scope, SYSTEM_ACTOR } from "#core/authz/consta
3
3
  import { store } from "#core/authz/db/store";
4
4
  import { audit } from "#core/authz/services/audit";
5
5
  import { validateGrants } from "#core/authz/services/grants";
6
- import { NotFoundError } from "#core/http/errors";
6
+ import { NotFoundError } from "@tatara/http";
7
7
 
8
8
  export async function getMember(orgId: string, userId: string) {
9
9
  const [roles, overlays] = await Promise.all([
@@ -4,7 +4,7 @@ import { audit } from "#core/authz/services/audit";
4
4
  import { validateGrants } from "#core/authz/services/grants";
5
5
  import { SYSTEM_ROLE_KEYS } from "#core/authz/system-roles";
6
6
  import type { RoleRow } from "#core/authz/types";
7
- import { BusinessRuleError, ConflictError, NotFoundError } from "#core/http/errors";
7
+ import { BusinessRuleError, ConflictError, NotFoundError } from "@tatara/http";
8
8
 
9
9
  export interface RoleInput {
10
10
  key: string;
@@ -40,6 +40,7 @@ __PM_EXEC__ tatara make resource <name> --module <module> # CRUD slice
40
40
  __PM_EXEC__ tatara make route|service|command <name> --module <module>
41
41
  __PM_EXEC__ tatara run --list # app commands
42
42
  __PM_EXEC__ tatara routes # every route and what protects it
43
+ __PM_EXEC__ tatara upgrade # after bumping @tatara/cli: run its codemods, sync, verify
43
44
  ```
44
45
 
45
46
  ## HTTP
@@ -1,5 +1,6 @@
1
+ import { startServer } from "@tatara/http";
1
2
  import { app } from "#apps/api/app";
2
3
  import { env } from "#core/config/env";
3
- import { startServer } from "#core/http/server";
4
+ import { logger } from "#core/logging/logger";
4
5
 
5
- startServer(app, env.PORT, "api");
6
+ startServer(app, { port: env.PORT, name: "api", logger });
@@ -1,6 +1,6 @@
1
- import { runCommand } from "#core/commands/runner";
2
- import { shutdown } from "#core/process/lifecycle";
1
+ import { runCommand, shutdown } from "@tatara/core";
2
+ import { logger } from "#core/logging/logger";
3
3
  import { commands } from "#generated/commands";
4
4
 
5
- const code = await runCommand(commands, process.argv.slice(2));
6
- await shutdown(code);
5
+ const code = await runCommand(commands, process.argv.slice(2), logger);
6
+ await shutdown(code, logger);
@@ -1,57 +1,44 @@
1
- import { OpenAPIHono } from "@hono/zod-openapi";
2
- import { Scalar } from "@scalar/hono-api-reference";
3
- import { bodyLimit } from "hono/body-limit";
4
- import { cors } from "hono/cors";
5
- import { secureHeaders } from "hono/secure-headers";
1
+ import { createApp, handleErrors, mountRoutes, serveOpenApi } from "@tatara/http";
6
2
  import { env } from "#core/config/env";
7
3
  import type { AppEnv } from "#core/http/context";
8
- import { runReadinessChecks } from "#core/http/health";
9
- import { errorHandler, notFoundHandler } from "#core/http/middleware/error-handler";
10
- import { requestContext } from "#core/http/middleware/request-context";
11
- import { validationHook } from "#core/http/middleware/validation";
12
4
  import type { AppName } from "#core/http/routes";
5
+ import { logger } from "#core/logging/logger";
13
6
  import { routes } from "#generated/routes";
14
7
  // tatara:slot imports
15
8
 
16
9
  export const API_PREFIX = "/api";
17
10
 
11
+ /** The app's HTTP wiring: plain Hono, with tatara's defaults from @tatara/http. */
18
12
  export function createHttpApp(name: AppName, options: { title: string }) {
19
- const app = new OpenAPIHono<AppEnv>({ defaultHook: validationHook });
20
-
21
- app.use(requestContext);
22
- app.use(secureHeaders());
23
- app.use(
24
- cors({
25
- origin: env.CORS_ORIGINS,
26
- credentials: true,
27
- allowHeaders: ["Content-Type", "Authorization", "X-Request-Id", "Accept-Language"],
28
- exposeHeaders: ["X-Request-Id"],
29
- }),
30
- );
31
- app.use(bodyLimit({ maxSize: env.BODY_LIMIT_BYTES }));
32
- // tatara:slot middleware
33
-
34
- app.get("/health", (c) => c.json({ status: "ok" }));
35
- app.get("/ready", async (c) => {
36
- const result = await runReadinessChecks();
37
- return c.json(result, result.ready ? 200 : 503);
13
+ const app = createApp<AppEnv>({
14
+ logger,
15
+ cors: {
16
+ origins: env.CORS_ORIGINS,
17
+ exposeHeaders: [
18
+ "X-Request-Id",
19
+ // tatara:slot expose-headers
20
+ ],
21
+ },
22
+ bodyLimitBytes: env.BODY_LIMIT_BYTES,
23
+ defaults: { principal: null, language: "en" },
38
24
  });
25
+ // tatara:slot middleware
39
26
 
40
27
  // tatara:slot routes
41
28
 
42
- for (const definition of routes) {
43
- if (definition.app === name) app.route(`${API_PREFIX}${definition.basePath}`, definition.router);
44
- }
45
-
46
- const openApiConfig = { openapi: "3.1.0", info: { title: options.title, version: "1.0.0" } } as const;
47
- app.get("/openapi.json", async (c) => {
48
- const document = app.getOpenAPI31Document(openApiConfig);
49
- // tatara:slot openapi
50
- return c.json(document);
29
+ mountRoutes(app, routes, { app: name, prefix: API_PREFIX });
30
+ serveOpenApi(app, {
31
+ title: options.title,
32
+ async extend(document) {
33
+ // tatara:slot openapi
34
+ },
35
+ });
36
+ handleErrors(app, {
37
+ message(err, c) {
38
+ let message = err.message;
39
+ // tatara:slot error-message
40
+ return message;
41
+ },
51
42
  });
52
- app.get("/docs", Scalar({ url: "/openapi.json", pageTitle: options.title }));
53
-
54
- app.onError(errorHandler);
55
- app.notFound(notFoundHandler);
56
43
  return app;
57
44
  }
@@ -1,6 +1,8 @@
1
- import { OpenAPIHono } from "@hono/zod-openapi";
1
+ import {
2
+ createRouter as createTataraRouter,
3
+ type RouteDefinition as TataraRouteDefinition,
4
+ } from "@tatara/http";
2
5
  import type { AppEnv } from "#core/http/context";
3
- import { validationHook } from "#core/http/middleware/validation";
4
6
 
5
7
  export const APP_NAMES = [
6
8
  "api",
@@ -9,13 +11,7 @@ export const APP_NAMES = [
9
11
 
10
12
  export type AppName = (typeof APP_NAMES)[number];
11
13
 
12
- export interface RouteDefinition {
13
- /** which HTTP app serves these routes */
14
- app: AppName;
15
- /** mounted under `/api` + basePath, e.g. "/notes" */
16
- basePath: string;
17
- router: OpenAPIHono<AppEnv>;
18
- }
14
+ export type RouteDefinition = TataraRouteDefinition<AppName, AppEnv>;
19
15
 
20
16
  /** Default export of every `routes/*.routes.ts`; discovered by `tatara sync`. */
21
17
  export function defineRoutes(definition: RouteDefinition): RouteDefinition {
@@ -23,6 +19,6 @@ export function defineRoutes(definition: RouteDefinition): RouteDefinition {
23
19
  }
24
20
 
25
21
  /** A router for one routes file. Attach middleware per route or per path, never `use("*")`. */
26
- export function createRouter(): OpenAPIHono<AppEnv> {
27
- return new OpenAPIHono<AppEnv>({ defaultHook: validationHook });
22
+ export function createRouter() {
23
+ return createTataraRouter<AppEnv>();
28
24
  }
@@ -1,17 +1,9 @@
1
+ import { loggerOptions } from "@tatara/core";
1
2
  import { pino } from "pino";
2
3
  import { env } from "#core/config/env";
3
4
 
4
- export const logger = pino({
5
- level: env.LOG_LEVEL,
6
- base: { service: "__APP_NAME__" },
7
- ...(env.NODE_ENV === "development"
8
- ? {
9
- transport: {
10
- target: "pino-pretty",
11
- options: { colorize: true, ignore: "pid,hostname,service" },
12
- },
13
- }
14
- : {}),
15
- });
5
+ export const logger = pino(
6
+ loggerOptions({ env: env.NODE_ENV, level: env.LOG_LEVEL, service: "__APP_NAME__" }),
7
+ );
16
8
 
17
9
  export type Logger = typeof logger;
@@ -1,5 +1,5 @@
1
1
  import { createRoute } from "@hono/zod-openapi";
2
- import { errorResponses, jsonContent, success, successSchema } from "#core/http/response";
2
+ import { errorResponses, jsonContent, success, successSchema } from "@tatara/http";
3
3
  import { createRouter, defineRoutes } from "#core/http/routes";
4
4
  import { greet } from "#modules/example/services/greeting.service";
5
5
  import {
@@ -1,4 +1,4 @@
1
- import { defineCommand } from "#core/commands/define";
1
+ import { defineCommand } from "@tatara/core";
2
2
  import { env } from "#core/config/env";
3
3
  import { migrateDatabase } from "#core/db/migrate";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { defineCommand, flag } from "#core/commands/define";
2
+ import { defineCommand, flag } from "@tatara/core";
3
3
  import { env } from "#core/config/env";
4
4
  import { dropAllTables } from "#core/db/admin";
5
5
  import { migrateDatabase } from "#core/db/migrate";
@@ -1,4 +1,4 @@
1
- import { defineCommand } from "#core/commands/define";
1
+ import { defineCommand } from "@tatara/core";
2
2
  import { seeds } from "#generated/seeds";
3
3
 
4
4
  export default defineCommand({
@@ -1,5 +1,5 @@
1
1
  import { expect, it } from "vitest";
2
- import { parseFlags } from "#core/commands/runner";
2
+ import { parseFlags } from "@tatara/core";
3
3
  import reset from "#core/db/commands/reset";
4
4
 
5
5
  it("does not treat --force false as forcing db:reset", () => {
@@ -1,8 +1,8 @@
1
1
  import { drizzle } from "drizzle-orm/mysql2";
2
2
  import mysql from "mysql2/promise";
3
3
  import { env } from "#core/config/env";
4
- import { registerReadinessCheck } from "#core/http/health";
5
- import { onShutdown } from "#core/process/lifecycle";
4
+ import { registerReadinessCheck } from "@tatara/core";
5
+ import { onShutdown } from "@tatara/core";
6
6
  import * as schema from "#generated/schema";
7
7
 
8
8
  export const pool = mysql.createPool({ uri: env.DATABASE_URL, connectionLimit: env.DATABASE_POOL_MAX });
@@ -1,8 +1,8 @@
1
1
  import { drizzle } from "drizzle-orm/node-postgres";
2
2
  import pg from "pg";
3
3
  import { env } from "#core/config/env";
4
- import { registerReadinessCheck } from "#core/http/health";
5
- import { onShutdown } from "#core/process/lifecycle";
4
+ import { registerReadinessCheck } from "@tatara/core";
5
+ import { onShutdown } from "@tatara/core";
6
6
  import * as schema from "#generated/schema";
7
7
 
8
8
  export const pool = new pg.Pool({ connectionString: env.DATABASE_URL, max: env.DATABASE_POOL_MAX });
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { defineCommand } from "#core/commands/define";
2
+ import { defineCommand } from "@tatara/core";
3
3
  import { sendEmail } from "#core/email/send";
4
4
 
5
5
  export default defineCommand({
@@ -1,7 +1,7 @@
1
1
  import { Cron } from "croner";
2
2
  import { withLock } from "#core/jobs/lock";
3
3
  import { logger } from "#core/logging/logger";
4
- import { handleSignals, onShutdown } from "#core/process/lifecycle";
4
+ import { handleSignals, onShutdown } from "@tatara/core";
5
5
  import { schedules } from "#generated/schedules";
6
6
 
7
7
  const jobs = schedules.map((schedule) => {
@@ -21,5 +21,5 @@ const jobs = schedules.map((schedule) => {
21
21
  onShutdown("cron", () => {
22
22
  for (const job of jobs) job.stop();
23
23
  });
24
- handleSignals();
24
+ handleSignals(logger);
25
25
  logger.info({ schedules: schedules.map((s) => `${s.name} (${s.cron})`) }, "cron started");
@@ -4,7 +4,7 @@ import { processJob } from "#core/jobs/process";
4
4
  import { QUEUE_NAME } from "#core/jobs/queue";
5
5
  import { redis } from "#core/jobs/redis";
6
6
  import { logger } from "#core/logging/logger";
7
- import { handleSignals, onShutdown } from "#core/process/lifecycle";
7
+ import { handleSignals, onShutdown } from "@tatara/core";
8
8
  import { consumers } from "#generated/consumers";
9
9
 
10
10
  const worker = new Worker(
@@ -20,5 +20,5 @@ const worker = new Worker(
20
20
 
21
21
  worker.on("failed", (job, err) => logger.error({ err, jobId: job?.id, topic: job?.name }, "job failed"));
22
22
  onShutdown("worker", () => worker.close());
23
- handleSignals();
23
+ handleSignals(logger);
24
24
  logger.info({ topics: consumers.map((c) => c.topic) }, "worker started");
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { defineCommand } from "#core/commands/define";
2
+ import { defineCommand } from "@tatara/core";
3
3
  import { enqueue } from "#core/jobs/queue";
4
4
 
5
5
  export default defineCommand({
@@ -1,7 +1,7 @@
1
1
  import { type JobsOptions, Queue } from "bullmq";
2
2
  import { env } from "#core/config/env";
3
3
  import { redis } from "#core/jobs/redis";
4
- import { onShutdown } from "#core/process/lifecycle";
4
+ import { onShutdown } from "@tatara/core";
5
5
 
6
6
  export const QUEUE_NAME = "__APP_NAME__-jobs";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { Redis } from "ioredis";
2
2
  import { env } from "#core/config/env";
3
- import { onShutdown } from "#core/process/lifecycle";
3
+ import { onShutdown } from "@tatara/core";
4
4
 
5
5
  let client: Redis | undefined;
6
6
 
@@ -1,8 +1,8 @@
1
1
  import { PrismaMariaDb } from "@prisma/adapter-mariadb";
2
2
  import { env } from "#core/config/env";
3
3
  import { connectionConfig } from "#core/db/admin";
4
- import { registerReadinessCheck } from "#core/http/health";
5
- import { onShutdown } from "#core/process/lifecycle";
4
+ import { registerReadinessCheck } from "@tatara/core";
5
+ import { onShutdown } from "@tatara/core";
6
6
  import { type Prisma, PrismaClient } from "#generated/prisma/client";
7
7
 
8
8
  export const db = new PrismaClient({
@@ -1,7 +1,7 @@
1
1
  import { PrismaPg } from "@prisma/adapter-pg";
2
2
  import { env } from "#core/config/env";
3
- import { registerReadinessCheck } from "#core/http/health";
4
- import { onShutdown } from "#core/process/lifecycle";
3
+ import { registerReadinessCheck } from "@tatara/core";
4
+ import { onShutdown } from "@tatara/core";
5
5
  import { type Prisma, PrismaClient } from "#generated/prisma/client";
6
6
 
7
7
  export const db = new PrismaClient({
@@ -1,4 +1,4 @@
1
- import { defineCommand } from "#core/commands/define";
1
+ import { defineCommand } from "@tatara/core";
2
2
  import { env } from "#core/config/env";
3
3
  import { storage } from "#core/storage/index";
4
4
  import { ensureBucket } from "#core/storage/s3";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { defineCommand, flag } from "#core/commands/define";
2
+ import { defineCommand, flag } from "@tatara/core";
3
3
 
4
4
  export default defineCommand({
5
5
  name: "__moduleKebab__:__nameKebab__",
@@ -1,5 +1,5 @@
1
1
  import { createRoute } from "@hono/zod-openapi";
2
- import { jsonContent, success, successSchema } from "#core/http/response";
2
+ import { jsonContent, success, successSchema } from "@tatara/http";
3
3
  import { createRouter, defineRoutes } from "#core/http/routes";
4
4
  import { __moduleCamel__Status } from "#modules/__moduleKebab__/services/__moduleKebab__.service";
5
5
  import { __moduleCamel__StatusSchema } from "#modules/__moduleKebab__/validators/__moduleKebab__.validators";
@@ -8,7 +8,7 @@ import {
8
8
  paginationQuerySchema,
9
9
  success,
10
10
  successSchema,
11
- } from "#core/http/response";
11
+ } from "@tatara/http";
12
12
  import { createRouter, defineRoutes } from "#core/http/routes";
13
13
  // tatara:slot imports
14
14
  import {
@@ -1,8 +1,8 @@
1
1
  import { count, desc, eq } from "drizzle-orm";
2
2
  import { db, type Executor } from "#core/db/client";
3
3
  import { newId } from "#core/db/id";
4
- import { NotFoundError } from "#core/http/errors";
5
- import type { PaginationQuery } from "#core/http/response";
4
+ import { NotFoundError } from "@tatara/http";
5
+ import type { PaginationQuery } from "@tatara/http";
6
6
  import { __namePluralCamel__ } from "#modules/__moduleKebab__/db/__nameKebab__.schema";
7
7
  import type {
8
8
  Create__namePascal__Input,
@@ -1,7 +1,7 @@
1
1
  import { db, type Executor } from "#core/db/client";
2
2
  import { newId } from "#core/db/id";
3
- import { NotFoundError } from "#core/http/errors";
4
- import type { PaginationQuery } from "#core/http/response";
3
+ import { NotFoundError } from "@tatara/http";
4
+ import type { PaginationQuery } from "@tatara/http";
5
5
  import type {
6
6
  Create__namePascal__Input,
7
7
  __namePascal__,
@@ -1,5 +1,5 @@
1
1
  import { createRoute } from "@hono/zod-openapi";
2
- import { errorResponses, jsonContent, success, successSchema } from "#core/http/response";
2
+ import { errorResponses, jsonContent, success, successSchema } from "@tatara/http";
3
3
  import { createRouter, defineRoutes } from "#core/http/routes";
4
4
  // tatara:slot imports
5
5
  import {
@@ -30,5 +30,7 @@
30
30
  "@aws-sdk/client-s3": "3.1152.0",
31
31
  "@aws-sdk/s3-request-presigner": "3.1152.0",
32
32
  "nodemailer": "10.1.1",
33
- "@types/nodemailer": "8.0.2"
33
+ "@types/nodemailer": "8.0.2",
34
+ "@tatara/core": "0.1.0",
35
+ "@tatara/http": "0.1.0"
34
36
  }
@@ -1,33 +0,0 @@
1
- import { z } from "zod";
2
- import type { Logger } from "#core/logging/logger";
3
-
4
- export interface CommandContext {
5
- logger: Logger;
6
- }
7
-
8
- export interface CommandDefinition<S extends z.ZodType = z.ZodType> {
9
- /** `module:action`, e.g. "billing:recalculate" */
10
- name: string;
11
- description: string;
12
- /** flags parsed from `--key value`; use z.coerce for numbers and `flag()` for booleans */
13
- args?: S;
14
- run(args: z.infer<S>, ctx: CommandContext): Promise<void> | void;
15
- }
16
-
17
-
18
- /** Default export of every `commands/*.ts`; discovered by `tatara sync`. */
19
- export function defineCommand<S extends z.ZodType = typeof noArgs>(
20
- definition: CommandDefinition<S>,
21
- ): CommandDefinition<S> {
22
- return definition;
23
- }
24
-
25
- export const noArgs = z.object({});
26
-
27
- /**
28
- * A boolean flag: bare `--force` is true, `--force false|0|no|off` is false.
29
- * (`z.coerce.boolean()` would read the string "false" as true.)
30
- */
31
- export function flag() {
32
- return z.union([z.boolean(), z.stringbool()]).default(false);
33
- }
@@ -1,54 +0,0 @@
1
- import type { CommandDefinition } from "#core/commands/define";
2
- import { logger } from "#core/logging/logger";
3
-
4
- /** Parses `--key value`, `--key=value` and bare `--flag` (true) into an object. */
5
- export function parseFlags(argv: string[]): Record<string, string | boolean> {
6
- const flags: Record<string, string | boolean> = {};
7
- for (let i = 0; i < argv.length; i++) {
8
- const arg = argv[i] ?? "";
9
- if (!arg.startsWith("--")) continue;
10
- const [rawKey, inline] = arg.slice(2).split("=", 2);
11
- const key = (rawKey ?? "").replace(/-([a-z])/g, (_, ch: string) => ch.toUpperCase());
12
- if (inline !== undefined) {
13
- flags[key] = inline;
14
- } else if (argv[i + 1] !== undefined && !argv[i + 1]?.startsWith("--")) {
15
- flags[key] = argv[i + 1] ?? "";
16
- i++;
17
- } else {
18
- flags[key] = true;
19
- }
20
- }
21
- return flags;
22
- }
23
-
24
- export async function runCommand(commands: CommandDefinition[], argv: string[]): Promise<number> {
25
- const [name, ...rest] = argv;
26
- if (!name || name === "--list" || name === "list" || name === "help") {
27
- const width = Math.max(0, ...commands.map((c) => c.name.length));
28
- console.log("Commands:");
29
- for (const c of [...commands].sort((a, b) => a.name.localeCompare(b.name))) {
30
- console.log(` ${c.name.padEnd(width)} ${c.description}`);
31
- }
32
- return 0;
33
- }
34
- const command = commands.find((c) => c.name === name);
35
- if (!command) {
36
- console.error(`Unknown command "${name}". Run with --list to see available commands.`);
37
- return 1;
38
- }
39
- const parsed = (command.args ?? null)?.safeParse(parseFlags(rest));
40
- if (parsed && !parsed.success) {
41
- console.error(`Invalid arguments for ${name}:`);
42
- for (const issue of parsed.error.issues) {
43
- console.error(` --${issue.path.join(".")}: ${issue.message}`);
44
- }
45
- return 1;
46
- }
47
- try {
48
- await command.run(parsed ? parsed.data : {}, { logger: logger.child({ command: name }) });
49
- return 0;
50
- } catch (error) {
51
- logger.error({ err: error, command: name }, "command failed");
52
- return 1;
53
- }
54
- }