@lpdjs/firestore-repo-service 2.6.7 → 2.6.9

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';var fs=require('fs'),path=require('path'),process$1=require('process'),promises=require('readline/promises');var O={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function M(e,t=O){let s=new Set(t.skipSegments.map(o=>o.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(o=>!s.has(o.toLowerCase())).map(o=>t.casing==="kebab"?se(o):o).join("/")}function se(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function H(e,t,s){let r=B(e),o=B(t),n=0;for(;n<r.length&&n<o.length&&r[n]===o[n];)n++;let c=r.length-n,i=o.slice(n),d=(i[i.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),f=s===""?d:`${d}${s}`;return i[i.length-1]=f,(c===0?"./":"../".repeat(c))+i.join("/")}function B(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,r)=>!(r===0&&s===""))}var ne="/**\n * AUTO-GENERATED by `@lpdjs/firestore-repo-service` Hono codegen.\n * Do not edit by hand \u2014 re-run `hono:gen` after adding / removing route files.\n */\n";function W(e,t){let s=path.dirname(t.outFile);fs.mkdirSync(s,{recursive:true});let r=t.banner??ne,o=(t.now??new Date).toISOString(),n=t.importExtension,c=[],i=[],a=[];e.forEach((f,u)=>{let x=H(s,f.absPath,n),v=M(f.relDir,t.derive);c.push(`import mod${u} from ${JSON.stringify(x)};`),i.push(` { __derivedPath: ${JSON.stringify(v)}, mod: mod${u} },`),a.push({source:f.relPath,url:v});});let d=`${r}// Generated at ${o} \u2014 ${e.length} route file${e.length===1?"":"s"}.
2
+ 'use strict';var fs=require('fs'),path=require('path'),process$1=require('process'),promises=require('readline/promises');var O={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function M(e,t=O){let s=new Set(t.skipSegments.map(o=>o.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(o=>!s.has(o.toLowerCase())).map(o=>t.casing==="kebab"?oe(o):o).join("/")}function oe(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function W(e,t,s){let r=H(e),o=H(t),i=0;for(;i<r.length&&i<o.length&&r[i]===o[i];)i++;let c=r.length-i,n=o.slice(i),d=(n[n.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),f=s===""?d:`${d}${s}`;return n[n.length-1]=f,(c===0?"./":"../".repeat(c))+n.join("/")}function H(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,r)=>!(r===0&&s===""))}var ae="/**\n * AUTO-GENERATED by `@lpdjs/firestore-repo-service` Hono codegen.\n * Do not edit by hand \u2014 re-run `hono:gen` after adding / removing route files.\n */\n";function J(e,t){let s=path.dirname(t.outFile);fs.mkdirSync(s,{recursive:true});let r=t.banner??ae,o=(t.now??new Date).toISOString(),i=t.importExtension,c=[],n=[],a=[];e.forEach((f,p)=>{let P=W(s,f.absPath,i),y=M(f.relDir,t.derive);c.push(`import mod${p} from ${JSON.stringify(P)};`),n.push(` { __derivedPath: ${JSON.stringify(y)}, mod: mod${p} },`),a.push({source:f.relPath,url:y});});let d=`${r}// Generated at ${o} \u2014 ${e.length} route file${e.length===1?"":"s"}.
3
3
 
4
4
  import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-service/servers/hono";
5
5
 
@@ -8,16 +8,16 @@ import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-serv
8
8
 
9
9
  `:`
10
10
  `)+`const __defs: { __derivedPath: string; mod: RouteModuleDefault }[] = [
11
- `+i.join(`
12
- `)+(i.length?`
11
+ `+n.join(`
12
+ `)+(n.length?`
13
13
  `:"")+`];
14
14
 
15
15
  export const routes: AnyRouteDef[] = __defs.flatMap(({ __derivedPath, mod }) => {
16
16
  const list = Array.isArray(mod) ? mod : [mod];
17
17
  return list.map((route) => ({ ...route, path: route.path ?? __derivedPath }));
18
18
  });
19
- `;return fs.writeFileSync(t.outFile,d,"utf8"),{outFile:t.outFile,routeCount:e.length,derivedPaths:a}}var I={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function J(e,t=I){let s=[];return K(e,e,t,s),s.sort((r,o)=>r.relPath.localeCompare(o.relPath)),s}function K(e,t,s,r){let o;try{o=fs.readdirSync(t);}catch{return}for(let n of o){if(s.excludeSegments.includes(n))continue;let c=path.join(t,n),i;try{i=fs.statSync(c);}catch{continue}if(i.isDirectory())K(e,c,s,r);else if(i.isFile()&&n===s.routesFile){let a=path.relative(e,c).split(path.sep).join("/"),d=a.replace(/\/?[^/]+$/,"");r.push({absPath:c,relPath:a,relDir:d});}}}var X=".frsrc.json";function T(e=process.cwd()){let t=path.resolve(e,X);if(!fs.existsSync(t))return {};try{return JSON.parse(fs.readFileSync(t,"utf8"))}catch{return {}}}function me(e,t=process.cwd()){let s=path.resolve(t,X),o={...T(t),...e};return fs.writeFileSync(s,`${JSON.stringify(o,null,2)}
20
- `,"utf8"),s}function ge(e){let[t,...s]=e,r={};for(let o=0;o<s.length;o++){let n=s[o];if(!n.startsWith("--"))continue;let c=n.slice(2),i=s[o+1];i&&!i.startsWith("--")?(r[c]=i,o++):r[c]=true;}return {command:t??"help",flags:r}}function Y(){console.log(`frs \u2014 Hono file-based codegen
19
+ `;return fs.writeFileSync(t.outFile,d,"utf8"),{outFile:t.outFile,routeCount:e.length,derivedPaths:a}}var E={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function K(e,t=E){let s=[];return V(e,e,t,s),s.sort((r,o)=>r.relPath.localeCompare(o.relPath)),s}function V(e,t,s,r){let o;try{o=fs.readdirSync(t);}catch{return}for(let i of o){if(s.excludeSegments.includes(i))continue;let c=path.join(t,i),n;try{n=fs.statSync(c);}catch{continue}if(n.isDirectory())V(e,c,s,r);else if(n.isFile()&&i===s.routesFile){let a=path.relative(e,c).split(path.sep).join("/"),d=a.replace(/\/?[^/]+$/,"");r.push({absPath:c,relPath:a,relDir:d});}}}var ee=".frsrc.json";function T(e=process.cwd()){let t=path.resolve(e,ee);if(!fs.existsSync(t))return {};try{return JSON.parse(fs.readFileSync(t,"utf8"))}catch{return {}}}function he(e,t=process.cwd()){let s=path.resolve(t,ee),o={...T(t),...e};return fs.writeFileSync(s,`${JSON.stringify(o,null,2)}
20
+ `,"utf8"),s}function ve(e){let[t,...s]=e,r={};for(let o=0;o<s.length;o++){let i=s[o];if(!i.startsWith("--"))continue;let c=i.slice(2),n=s[o+1];n&&!n.startsWith("--")?(r[c]=n,o++):r[c]=true;}return {command:t??"help",flags:r}}function Z(){console.log(`frs \u2014 Hono file-based codegen
21
21
 
22
22
  Usage:
23
23
  frs init [flags]
@@ -62,8 +62,10 @@ Flags (new <name>):
62
62
  --usecase-folder <name>
63
63
  Parent folder under <domain>.
64
64
  Default: .frsrc.json "useCaseFolder" or useCases
65
- --with-usecase Also scaffold a sibling useCase.ts file (default: true)
66
- --with-test Also scaffold a sibling useCase.test.ts (Vitest, default: true)
65
+ --with-usecase Also scaffold a sibling <domain>.<name>.useCase.ts file
66
+ (default: true)
67
+ --with-test Also scaffold a sibling <domain>.<name>.useCase.test.ts
68
+ (Vitest, default: true)
67
69
  --apis-import <path> Import path for the registry (default: auto-detect
68
70
  ../../../../apis.js \u2014 adjust if your layout differs)
69
71
  --force Overwrite if files already exist
@@ -81,8 +83,8 @@ Examples:
81
83
  frs new createPost --domain posts --method post
82
84
  frs new listPosts --domain posts --method get --api v1
83
85
  frs add service postRepo
84
- `);}function Z(e){if(typeof e=="string")return e.split(",").map(t=>t.trim()).filter(Boolean)}function m(e){return typeof e=="string"?e:void 0}function ee(e){if(e||!process$1.stdin.isTTY)return {ask:async(s,r)=>r??"",askChoice:async(s,r,o)=>o??"",askBool:async(s,r)=>r,close:()=>{}};let t=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(s,r){let o=r?` (${r})`:"";return (await t.question(`? ${s}${o} \u203A `)).trim()||r||""},async askChoice(s,r,o){let n=` [${r.join("/")}${o?`, default: ${o}`:""}]`;for(;;){let c=(await t.question(`? ${s}${n} \u203A `)).trim().toLowerCase();if(!c&&o)return o;if(r.includes(c))return c;console.log(` invalid choice \u2014 pick one of: ${r.join(", ")}`);}},async askBool(s,r){let o=` (${r?"Y/n":"y/N"})`,n=(await t.question(`? ${s}${o} \u203A `)).trim().toLowerCase();return n?n==="y"||n==="yes"||n==="true":r},close:()=>t.close()}}async function he(e){let t=T(),s=m(e.root)??t.root;s||(console.error("[frs] --root is required (or run `frs init` to write it to .frsrc.json)"),process.exit(2));let r=path.resolve(process.cwd(),s);fs.existsSync(r)||(console.error(`[frs] root not found: ${r}`),process.exit(2));let o=m(e.out)??t.out??"__generated__/routes.ts",n=Z(e.skip)??O.skipSegments,c=m(e.casing)==="kebab"?"kebab":O.casing,i={skipSegments:n,casing:c},a=m(e.ext)??".js",d=Z(e.exclude)??I.excludeSegments,f=m(e["routes-file"])??I.routesFile,x=J(r,{routesFile:f,excludeSegments:d});x.length===0&&console.warn(`[frs] no "${f}" files found under ${r} \u2014 generated an empty manifest.`);let v=W(x,{outFile:path.resolve(r,o),derive:i,importExtension:a});if(!e.silent){console.log(`[frs] wrote ${v.outFile} (${v.routeCount} route${v.routeCount===1?"":"s"})`);for(let{source:S,url:g}of v.derivedPaths)console.log(` ${g.padEnd(48)} \u2190 ${S}`);}}async function ve(e,t){let s=t.yes===true,r=ee(s),o=T();try{let n=e&&!e.startsWith("--")?e:void 0;n||(n=(await r.ask("Route name (e.g. createPost)")).trim(),n||(console.error("[frs] route name is required"),process.exit(2)));let c=m(t.domain);c||(c=(await r.ask("Domain name (e.g. posts)")).trim(),c||(console.error("[frs] --domain is required"),process.exit(2)));let i=m(t.root)??o.root??"src/domains",a=m(t.method)?.toLowerCase();a||(a=await r.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(a)||(console.error(`[frs] invalid --method: ${a}`),process.exit(2));let d=m(t.api);if(!d){let $=o.apis?.[0]??"v1";d=(await r.ask("API tag",$)).trim()||$;}let f=m(t["usecase-folder"])??o.useCaseFolder??"useCases",u=t["with-usecase"]===void 0?s?!0:await r.askBool("Scaffold useCase.ts?",!0):t["with-usecase"]!==!1,x=t["with-test"]===void 0?s||!u?u:await r.askBool("Scaffold useCase.test.ts (Vitest)?",!0):t["with-test"]!==!1,v=t.force===!0,S=path.resolve(process.cwd(),i),g=path.resolve(S,c,f,n),D=path.resolve(g,"routes.ts"),b=path.resolve(g,"useCase.ts"),k=path.resolve(g,"useCase.test.ts");fs.mkdirSync(g,{recursive:!0});let y=`${n.charAt(0).toUpperCase()}${n.slice(1)}UseCase`,P="@lpdjs/firestore-repo-service/servers/hono",F=xe(S,g),_=a==="get"?"// GET \u2192 lu depuis les query params":`// ${a.toUpperCase()} \u2192 lu depuis le body JSON`,h=`/**
85
- * ${y} \u2014 pure business logic, no HTTP awareness.
86
+ `);}function Q(e){if(typeof e=="string")return e.split(",").map(t=>t.trim()).filter(Boolean)}function m(e){return typeof e=="string"?e:void 0}function te(e){if(e||!process$1.stdin.isTTY)return {ask:async(s,r)=>r??"",askChoice:async(s,r,o)=>o??"",askBool:async(s,r)=>r,close:()=>{}};let t=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(s,r){let o=r?` (${r})`:"";return (await t.question(`? ${s}${o} \u203A `)).trim()||r||""},async askChoice(s,r,o){let i=` [${r.join("/")}${o?`, default: ${o}`:""}]`;for(;;){let c=(await t.question(`? ${s}${i} \u203A `)).trim().toLowerCase();if(!c&&o)return o;if(r.includes(c))return c;console.log(` invalid choice \u2014 pick one of: ${r.join(", ")}`);}},async askBool(s,r){let o=` (${r?"Y/n":"y/N"})`,i=(await t.question(`? ${s}${o} \u203A `)).trim().toLowerCase();return i?i==="y"||i==="yes"||i==="true":r},close:()=>t.close()}}async function ye(e){let t=T(),s=m(e.root)??t.root;s||(console.error("[frs] --root is required (or run `frs init` to write it to .frsrc.json)"),process.exit(2));let r=path.resolve(process.cwd(),s);fs.existsSync(r)||(console.error(`[frs] root not found: ${r}`),process.exit(2));let o=m(e.out)??t.out??"__generated__/routes.ts",i=Q(e.skip)??O.skipSegments,c=m(e.casing)==="kebab"?"kebab":O.casing,n={skipSegments:i,casing:c},a=m(e.ext)??".js",d=Q(e.exclude)??E.excludeSegments,f=m(e["routes-file"])??E.routesFile,P=K(r,{routesFile:f,excludeSegments:d});P.length===0&&console.warn(`[frs] no "${f}" files found under ${r} \u2014 generated an empty manifest.`);let y=J(P,{outFile:path.resolve(r,o),derive:n,importExtension:a});if(!e.silent){console.log(`[frs] wrote ${y.outFile} (${y.routeCount} route${y.routeCount===1?"":"s"})`);for(let{source:S,url:g}of y.derivedPaths)console.log(` ${g.padEnd(48)} \u2190 ${S}`);}}async function $e(e,t){let s=t.yes===true,r=te(s),o=T();try{let i=e&&!e.startsWith("--")?e:void 0;i||(i=(await r.ask("Route name (e.g. createPost)")).trim(),i||(console.error("[frs] route name is required"),process.exit(2)));let c=m(t.domain);c||(c=(await r.ask("Domain name (e.g. posts)")).trim(),c||(console.error("[frs] --domain is required"),process.exit(2)));let n=m(t.root)??o.root??"src/domains",a=m(t.method)?.toLowerCase();a||(a=await r.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(a)||(console.error(`[frs] invalid --method: ${a}`),process.exit(2));let d=m(t.api);if(!d){let v=o.apis?.[0]??"v1";d=(await r.ask("API tag",v)).trim()||v;}let f=m(t["usecase-folder"])??o.useCaseFolder??"useCases",p=t["with-usecase"]===void 0?s?!0:await r.askBool("Scaffold useCase.ts?",!0):t["with-usecase"]!==!1,P=t["with-test"]===void 0?s||!p?p:await r.askBool("Scaffold useCase.test.ts (Vitest)?",!0):t["with-test"]!==!1,y=t.force===!0,S=path.resolve(process.cwd(),n),g=path.resolve(S,c,f,i),A=path.resolve(g,"routes.ts"),$=`${c}.${i}.useCase`,R=path.resolve(g,`${$}.ts`),k=path.resolve(g,`${$}.test.ts`);fs.mkdirSync(g,{recursive:!0});let F=v=>v.charAt(0).toUpperCase()+v.slice(1),w=`${F(c)}${F(i)}UseCase`,D="@lpdjs/firestore-repo-service/servers/hono",h=Se(S,g),b=a==="get"?"// GET \u2192 lu depuis les query params":`// ${a.toUpperCase()} \u2192 lu depuis le body JSON`,L=`/**
87
+ * ${w} \u2014 pure business logic, no HTTP awareness.
86
88
  *
87
89
  * Owns its Zod \`input\` / \`output\` schemas (declared as \`static\` members, the
88
90
  * single source of truth shared with \`routes.ts\`) and runs the logic in
@@ -91,11 +93,11 @@ Examples:
91
93
  */
92
94
 
93
95
  import { z } from "zod";
94
- import { UseCase } from "${P}";
95
- import type { Services } from "${F}";
96
+ import { UseCase } from "${D}";
97
+ import type { Services } from "${h}";
96
98
 
97
99
  const input = z.object({
98
- ${_}
100
+ ${b}
99
101
  example: z.string(),
100
102
  });
101
103
 
@@ -103,7 +105,7 @@ const output = z.object({
103
105
  id: z.string(),
104
106
  });
105
107
 
106
- export class ${y} extends UseCase<typeof input, typeof output, Services> {
108
+ export class ${w} extends UseCase<typeof input, typeof output, Services> {
107
109
  static readonly input = input;
108
110
  static readonly output = output;
109
111
 
@@ -114,22 +116,22 @@ export class ${y} extends UseCase<typeof input, typeof output, Services> {
114
116
  return { id: payload.example };
115
117
  }
116
118
  }
117
- `,C=a==="get"?`
118
- source: "query",`:"",N=u?`import { ${y} } from "./useCase.js";
119
- `:"",p=m(t["apis-import"])??we(S,g),j=u?`import { defineRoutes } from "${P}";
120
- import { useCaseRoute } from "${p}";
121
- ${N}
119
+ `,l=a==="get"?`
120
+ source: "query",`:"",_=p?`import { ${w} } from "./${$}.js";
121
+ `:"",U=m(t["apis-import"])??Pe(S,g),se=p?`import { defineRoutes } from "${D}";
122
+ import { useCaseRoute } from "${U}";
123
+ ${_}
122
124
  export default defineRoutes([
123
- useCaseRoute(${y}, {
125
+ useCaseRoute(${w}, {
124
126
  api: "${d}",
125
- method: "${a}",${C}
126
- summary: "TODO: ${n}",
127
+ method: "${a}",${l}
128
+ summary: "TODO: ${i}",
127
129
  tags: ["${c}"],
128
130
  }),
129
131
  ]);
130
132
  `:`import { z } from "zod";
131
- import { defineRoutes } from "${P}";
132
- import { defineRoute } from "${p}";
133
+ import { defineRoutes } from "${D}";
134
+ import { defineRoute } from "${U}";
133
135
 
134
136
  export default defineRoutes([
135
137
  defineRoute({
@@ -137,7 +139,7 @@ export default defineRoutes([
137
139
  method: "${a}",
138
140
 
139
141
  input: z.object({
140
- ${_}
142
+ ${b}
141
143
  example: z.string(),
142
144
  }),
143
145
 
@@ -145,7 +147,7 @@ export default defineRoutes([
145
147
  id: z.string(),
146
148
  }),
147
149
 
148
- summary: "TODO: ${n}",
150
+ summary: "TODO: ${i}",
149
151
  tags: ["${c}"],
150
152
 
151
153
  handler: async ({ input }) => {
@@ -154,44 +156,52 @@ export default defineRoutes([
154
156
  },
155
157
  }),
156
158
  ]);
157
- `,z=[],G=[],q=($,te)=>{if(fs.existsSync($)&&!v){G.push($);return}fs.writeFileSync($,te,"utf8"),z.push($);};if(q(D,j),u&&q(b,h),u&&x){let $=`import { describe, it, expect } from "vitest";
158
- import type { Services } from "${F}";
159
- import { ${y} } from "./useCase.js";
159
+ `,z=[],G=[],N=(v,re)=>{if(fs.existsSync(v)&&!y){G.push(v);return}fs.writeFileSync(v,re,"utf8"),z.push(v);};if(N(A,se),p&&N(R,L),p&&P){let v=`import { describe, it, expect } from "vitest";
160
+ import type { Services } from "${h}";
161
+ import { ${w} } from "./${$}.js";
160
162
 
161
- describe("${y}", () => {
163
+ describe("${w}", () => {
162
164
  it("returns a response shaped like the output schema", async () => {
163
165
  // TODO: replace with real mocks for the services the useCase consumes.
164
166
  const services = {} as unknown as Services;
165
167
 
166
- const useCase = new ${y}(services);
168
+ const useCase = new ${w}(services);
167
169
  const result = await useCase.execute({ example: "hello" });
168
170
  expect(result).toMatchObject({ id: expect.any(String) });
169
171
  });
170
172
 
171
173
  // TODO: add error-path tests, repository mocks, etc.
172
174
  });
173
- `;q(k,$);}for(let $ of z)console.log(`[frs] wrote ${$}`);for(let $ of G)console.log(`[frs] skipped ${$} (use --force to overwrite)`);console.log(`
174
- [frs] reminder: run "frs gen --root ${i}" to refresh the manifest.`);}finally{r.close();}}async function ye(e){let t=e.yes===true,s=ee(t);try{let r=e.force===!0,o=m(e.root);o||(o=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let n=m(e["apis-file"]);n||(n=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let c=m(e["services-file"]);if(!c){let p=n.replace(/apis\.ts$/,"services.ts")||"src/services.ts";c=(await s.ask("services.ts location",p)).trim()||p;}let i=m(e.apis);i||(i=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let a=i.split(",").map(p=>p.trim()).filter(Boolean);a.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let d=m(e["base-path"]),f=path.resolve(process.cwd(),o),u=path.resolve(process.cwd(),n),x=path.resolve(process.cwd(),c),v=path.resolve(f,"__generated__"),S=path.resolve(v,"routes.ts"),g=[],D=[],b=(p,j)=>{if(fs.mkdirSync(path.dirname(p),{recursive:!0}),fs.existsSync(p)&&!r){D.push(p);return}fs.writeFileSync(p,j,"utf8"),g.push(p);},k=a.map(p=>{let j=d??`/${p}`;return ` ${p}: {
175
- basePath: "${j}",
175
+ `;N(k,v);}for(let v of z)console.log(`[frs] wrote ${v}`);for(let v of G)console.log(`[frs] skipped ${v} (use --force to overwrite)`);console.log(`
176
+ [frs] reminder: run "frs gen --root ${n}" to refresh the manifest.`);}finally{r.close();}}async function we(e){let t=e.yes===true,s=te(t);try{let r=e.force===!0,o=m(e.root);o||(o=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let i=m(e["apis-file"]);i||(i=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let c=m(e["services-file"]);if(!c){let l=i.replace(/apis\.ts$/,"services.ts")||"src/services.ts";c=(await s.ask("services.ts location",l)).trim()||l;}let n=m(e.apis);n||(n=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let a=n.split(",").map(l=>l.trim()).filter(Boolean);a.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let d=m(e["base-path"]),f=path.resolve(process.cwd(),o),p=path.resolve(process.cwd(),i),P=path.resolve(process.cwd(),c),y=path.resolve(f,"__generated__"),S=path.resolve(y,"routes.ts"),g=[],A=[],$=(l,_)=>{if(fs.mkdirSync(path.dirname(l),{recursive:!0}),fs.existsSync(l)&&!r){A.push(l);return}fs.writeFileSync(l,_,"utf8"),g.push(l);},R=a.map(l=>{let _=d??`/${l}`;return ` ${l}: {
177
+ basePath: "${_}",
176
178
  openapi: {
177
- info: { title: "${p.toUpperCase()} API", version: "1.0.0", description: "" },
179
+ info: { title: "${l.toUpperCase()} API", version: "1.0.0", description: "" },
178
180
  },
181
+ // Built-in error mapping. Extend BaseErrorHandler to map your own
182
+ // domain errors, then swap it in here (per API).
183
+ errorHandler: new BaseErrorHandler(),
184
+ // Structured console logger. Extend BaseLogger (override \`write\`) to
185
+ // route to your sink, then swap it in here (per API).
186
+ logger: new BaseLogger(),
179
187
  verbose: process.env["NODE_ENV"] !== "production",
180
188
  },`}).join(`
181
- `),y=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
182
- import { services } from "${E(path.dirname(u),x)}";
189
+ `),k=`import { BaseErrorHandler, BaseLogger, createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
190
+ import { services } from "${I(path.dirname(p),P)}";
183
191
 
184
192
  /**
185
193
  * Single source of truth for every API exposed by this project.
186
194
  * Add per-API middlewares, interceptors, OpenAPI metadata here.
187
195
  *
188
- * The shared \`services\` container is injected into every HonoServer the
189
- * registry builds \u2014 handlers / interceptors receive it via \`{ services }\`
190
- * and the built-in \`services.ctx.c\` resolves to the current request.
196
+ * Per-API resources injected into every handler / interceptor / error-handler
197
+ * context (override them per API above):
198
+ * - \`services\` \u2014 shared DI container (\`services.ctx.c\` = current request);
199
+ * - \`errorHandler\` \u2014 maps thrown errors \u2192 HTTP (extend \`BaseErrorHandler\`);
200
+ * - \`logger\` \u2014 structured logging (extend \`BaseLogger\`).
191
201
  */
192
202
  export const apis = createApiRegistry(
193
203
  {
194
- ${k}
204
+ ${R}
195
205
  },
196
206
  { services },
197
207
  );
@@ -199,7 +209,7 @@ ${k}
199
209
  /** Typed helpers used inside every route file. */
200
210
  export const defineRoute = apis.defineRoute;
201
211
  export const useCaseRoute = apis.useCaseRoute;
202
- `;b(u,y),b(x,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
212
+ `;$(p,k),$(P,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
203
213
 
204
214
  /**
205
215
  * Global DI container \u2014 declare every singleton (repositories, SDK
@@ -232,22 +242,22 @@ export const services = createServices({
232
242
 
233
243
  /** Convenience type \u2014 \`function fn(svc: Services) { ... }\`. */
234
244
  export type Services = typeof services;
235
- `);let F=`// AUTO-GENERATED by frs \u2014 do not edit.
245
+ `);let w=`// AUTO-GENERATED by frs \u2014 do not edit.
236
246
  // Run \`frs gen --root ${o}\` to refresh.
237
247
 
238
248
  import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
239
249
 
240
250
  export const routes: AnyRouteDef[] = [];
241
- `;b(S,F);let _=me({root:o,apisFile:n,servicesFile:c,apis:a});g.push(_);let h=E(path.dirname(u),u),C=E(path.dirname(u),S),N=a.length===1?`export const { ${a[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${a.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let p of g)console.log(`[frs] wrote ${p}`);for(let p of D)console.log(`[frs] skipped ${p} (use --force to overwrite)`);console.log(`
251
+ `;$(S,w);let D=he({root:o,apisFile:i,servicesFile:c,apis:a});g.push(D);let h=I(path.dirname(p),p),b=I(path.dirname(p),S),L=a.length===1?`export const { ${a[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${a.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let l of g)console.log(`[frs] wrote ${l}`);for(let l of A)console.log(`[frs] skipped ${l} (use --force to overwrite)`);console.log(`
242
252
  Next steps:
243
253
 
244
254
  1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
245
255
 
246
256
  import { onRequest } from "firebase-functions/v2/https";
247
257
  import { apis } from "${h}";
248
- import { routes } from "${C}";
258
+ import { routes } from "${b}";
249
259
 
250
- ${N}
260
+ ${L}
251
261
  defaults: { region: "us-central1", invoker: "public" },
252
262
  });
253
263
 
@@ -258,10 +268,10 @@ Next steps:
258
268
  3. Refresh the manifest before each build:
259
269
 
260
270
  frs gen --root ${o}
261
- `);}finally{s.close();}}function E(e,t){let s=path.relative(e,t).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function $e(e,t,s){e!=="service"&&(console.error(`[frs] unknown "add" target: ${e??"(missing)"} \u2014 supported: service`),process.exit(2)),t||(console.error("[frs] service name is required: frs add service <name>"),process.exit(2));let r=s.force===true,o=T(),c=[m(s["services-file"]),o.servicesFile,"src/services.ts","services.ts"].filter(h=>typeof h=="string"&&h.length>0),i;for(let h of c){let C=path.resolve(process.cwd(),h);if(fs.existsSync(C)){i=C;break}}if(!i){let h=c.map(C=>path.resolve(process.cwd(),C)).join(`
271
+ `);}finally{s.close();}}function I(e,t){let s=path.relative(e,t).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function xe(e,t,s){e!=="service"&&(console.error(`[frs] unknown "add" target: ${e??"(missing)"} \u2014 supported: service`),process.exit(2)),t||(console.error("[frs] service name is required: frs add service <name>"),process.exit(2));let r=s.force===true,o=T(),c=[m(s["services-file"]),o.servicesFile,"src/services.ts","services.ts"].filter(h=>typeof h=="string"&&h.length>0),n;for(let h of c){let b=path.resolve(process.cwd(),h);if(fs.existsSync(b)){n=b;break}}if(!n){let h=c.map(b=>path.resolve(process.cwd(),b)).join(`
262
272
  `);console.error(`[frs] services file not found. Tried:
263
273
  ${h}
264
- Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=m(s["services-dir"])??o.servicesDir??path.resolve(path.dirname(i),"services"),d=path.resolve(process.cwd(),a);fs.mkdirSync(d,{recursive:true});let f=`${t.charAt(0).toUpperCase()}${t.slice(1)}Service`,u=path.resolve(d,`${t}.ts`),x=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
274
+ Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=m(s["services-dir"])??o.servicesDir??path.resolve(path.dirname(n),"services"),d=path.resolve(process.cwd(),a);fs.mkdirSync(d,{recursive:true});let f=`${t.charAt(0).toUpperCase()}${t.slice(1)}Service`,p=path.resolve(d,`${t}.ts`),P=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
265
275
 
266
276
  /**
267
277
  * ${f} \u2014 generated by \`frs add service ${t}\`.
@@ -290,9 +300,9 @@ export class ${f} {
290
300
  return \`hello from ${t} \u2014 user=\${this.ctx.maybeC?.get("user")?.id ?? "anonymous"}\`;
291
301
  }
292
302
  }
293
- `;fs.existsSync(u)&&!r?console.log(`[frs] skipped ${u} (use --force to overwrite)`):(fs.writeFileSync(u,x,"utf8"),console.log(`[frs] wrote ${u}`));let v=fs.readFileSync(i,"utf8"),S=E(path.dirname(i),u),g=`import { ${f} } from "${S}";`,D=` ${t}: ({ ctx }) => new ${f}(ctx),`;if(v.includes(g)){console.log(`[frs] services.ts already registers "${t}" \u2014 skipping.`);return}let b=v.split(`
294
- `),k=-1;for(let h=0;h<b.length;h++)/^import\s/.test(b[h])&&(k=h);k>=0?b.splice(k+1,0,g):b.unshift(g);let y=b.join(`
295
- `),P=y.match(/createServices\s*\(\s*\{/);if(!P){console.error(`[frs] could not find \`createServices({\` in ${i} \u2014 register "${t}" manually.`);return}let F=P.index+P[0].length,_=y.slice(0,F)+`
296
- `+D+y.slice(F);fs.writeFileSync(i,_,"utf8"),console.log(`[frs] updated ${i} (+ ${t})`);}function we(e,t){let s=["apis.ts","apis.js","api.ts","api.js"],r=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of r)for(let n of s){let c=path.resolve(o,n);if(fs.existsSync(c)){let i=path.relative(t,c).replace(/\\/g,"/");return i=i.replace(/\.ts$/,".js").replace(/\.js$/,".js"),i.startsWith(".")||(i=`./${i}`),i}}return "../../../../apis.js"}function xe(e,t){let s=["services.ts","services.js"],r=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of r)for(let n of s){let c=path.resolve(o,n);if(fs.existsSync(c)){let i=path.relative(t,c).replace(/\\/g,"/");return i=i.replace(/\.ts$/,".js"),i.startsWith(".")||(i=`./${i}`),i}}return "../../../../services.js"}async function be(){let e=process.argv.slice(2),{command:t,flags:s}=ge(e);switch(t){case "init":await ye(s);return;case "gen":await he(s);return;case "new":await ve(e[1],s);return;case "add":await $e(e[1],e[2],s);return;case "help":case "--help":case "-h":Y();return;default:console.error(`[frs] unknown command: ${t}
297
- `),Y(),process.exit(2);}}be().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
303
+ `;fs.existsSync(p)&&!r?console.log(`[frs] skipped ${p} (use --force to overwrite)`):(fs.writeFileSync(p,P,"utf8"),console.log(`[frs] wrote ${p}`));let y=fs.readFileSync(n,"utf8"),S=I(path.dirname(n),p),g=`import { ${f} } from "${S}";`,A=` ${t}: ({ ctx }) => new ${f}(ctx),`;if(y.includes(g)){console.log(`[frs] services.ts already registers "${t}" \u2014 skipping.`);return}let $=y.split(`
304
+ `),R=-1;for(let h=0;h<$.length;h++)/^import\s/.test($[h])&&(R=h);R>=0?$.splice(R+1,0,g):$.unshift(g);let k=$.join(`
305
+ `),F=k.match(/createServices\s*\(\s*\{/);if(!F){console.error(`[frs] could not find \`createServices({\` in ${n} \u2014 register "${t}" manually.`);return}let w=F.index+F[0].length,D=k.slice(0,w)+`
306
+ `+A+k.slice(w);fs.writeFileSync(n,D,"utf8"),console.log(`[frs] updated ${n} (+ ${t})`);}function Pe(e,t){let s=["apis.ts","apis.js","api.ts","api.js"],r=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of r)for(let i of s){let c=path.resolve(o,i);if(fs.existsSync(c)){let n=path.relative(t,c).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js").replace(/\.js$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../apis.js"}function Se(e,t){let s=["services.ts","services.js"],r=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of r)for(let i of s){let c=path.resolve(o,i);if(fs.existsSync(c)){let n=path.relative(t,c).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../services.js"}async function be(){let e=process.argv.slice(2),{command:t,flags:s}=ve(e);switch(t){case "init":await we(s);return;case "gen":await ye(s);return;case "new":await $e(e[1],s);return;case "add":await xe(e[1],e[2],s);return;case "help":case "--help":case "-h":Z();return;default:console.error(`[frs] unknown command: ${t}
307
+ `),Z(),process.exit(2);}}be().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
298
308
  //# sourceMappingURL=cli.cjs.map