@lpdjs/firestore-repo-service 2.6.2-beta.1 → 2.6.3

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
- import {existsSync,mkdirSync,writeFileSync,readFileSync,readdirSync,statSync}from'fs';import {resolve,dirname,relative,join,sep}from'path';import {stdin,stdout}from'process';import {createInterface}from'readline/promises';var I={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function L(e,t=I){let s=new Set(t.skipSegments.map(r=>r.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(r=>!s.has(r.toLowerCase())).map(r=>t.casing==="kebab"?X(r):r).join("/")}function X(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function U(e,t,s){let o=q(e),r=q(t),i=0;for(;i<o.length&&i<r.length&&o[i]===r[i];)i++;let c=o.length-i,n=r.slice(i),g=(n[n.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),p=s===""?g:`${g}${s}`;return n[n.length-1]=p,(c===0?"./":"../".repeat(c))+n.join("/")}function q(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,o)=>!(o===0&&s===""))}var re="/**\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 G(e,t){let s=dirname(t.outFile);mkdirSync(s,{recursive:true});let o=t.banner??re,r=(t.now??new Date).toISOString(),i=t.importExtension,c=[],n=[],a=[];e.forEach((p,l)=>{let v=U(s,p.absPath,i),y=L(p.relDir,t.derive);c.push(`import mod${l} from ${JSON.stringify(v)};`),n.push(` { __derivedPath: ${JSON.stringify(y)}, mod: mod${l} },`),a.push({source:p.relPath,url:y});});let g=`${o}// Generated at ${r} \u2014 ${e.length} route file${e.length===1?"":"s"}.
2
+ import {existsSync,mkdirSync,writeFileSync,readFileSync,readdirSync,statSync}from'fs';import {resolve,dirname,relative,join,sep}from'path';import {stdin,stdout}from'process';import {createInterface}from'readline/promises';var I={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function G(e,t=I){let s=new Set(t.skipSegments.map(r=>r.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(r=>!s.has(r.toLowerCase())).map(r=>t.casing==="kebab"?te(r):r).join("/")}function te(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function B(e,t,s){let i=z(e),r=z(t),n=0;for(;n<i.length&&n<r.length&&i[n]===r[n];)n++;let c=i.length-n,o=r.slice(n),m=(o[o.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),u=s===""?m:`${m}${s}`;return o[o.length-1]=u,(c===0?"./":"../".repeat(c))+o.join("/")}function z(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,i)=>!(i===0&&s===""))}var ie="/**\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 M(e,t){let s=dirname(t.outFile);mkdirSync(s,{recursive:true});let i=t.banner??ie,r=(t.now??new Date).toISOString(),n=t.importExtension,c=[],o=[],a=[];e.forEach((u,l)=>{let h=B(s,u.absPath,n),v=G(u.relDir,t.derive);c.push(`import mod${l} from ${JSON.stringify(h)};`),o.push(` { __derivedPath: ${JSON.stringify(v)}, mod: mod${l} },`),a.push({source:u.relPath,url:v});});let m=`${i}// Generated at ${r} \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
- `+n.join(`
12
- `)+(n.length?`
11
+ `+o.join(`
12
+ `)+(o.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 writeFileSync(t.outFile,g,"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 B(e,t=E){let s=[];return z(e,e,t,s),s.sort((o,r)=>o.relPath.localeCompare(r.relPath)),s}function z(e,t,s,o){let r;try{r=readdirSync(t);}catch{return}for(let i of r){if(s.excludeSegments.includes(i))continue;let c=join(t,i),n;try{n=statSync(c);}catch{continue}if(n.isDirectory())z(e,c,s,o);else if(n.isFile()&&i===s.routesFile){let a=relative(e,c).split(sep).join("/"),g=a.replace(/\/?[^/]+$/,"");o.push({absPath:c,relPath:a,relDir:g});}}}var V=".frsrc.json";function Y(e=process.cwd()){let t=resolve(e,V);if(!existsSync(t))return {};try{return JSON.parse(readFileSync(t,"utf8"))}catch{return {}}}function le(e,t=process.cwd()){let s=resolve(t,V),r={...Y(t),...e};return writeFileSync(s,`${JSON.stringify(r,null,2)}
20
- `,"utf8"),s}function de(e){let[t,...s]=e,o={};for(let r=0;r<s.length;r++){let i=s[r];if(!i.startsWith("--"))continue;let c=i.slice(2),n=s[r+1];n&&!n.startsWith("--")?(o[c]=n,r++):o[c]=true;}return {command:t??"help",flags:o}}function H(){console.log(`frs \u2014 Hono file-based codegen
19
+ `;return writeFileSync(t.outFile,m,"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 H(e,t=E){let s=[];return W(e,e,t,s),s.sort((i,r)=>i.relPath.localeCompare(r.relPath)),s}function W(e,t,s,i){let r;try{r=readdirSync(t);}catch{return}for(let n of r){if(s.excludeSegments.includes(n))continue;let c=join(t,n),o;try{o=statSync(c);}catch{continue}if(o.isDirectory())W(e,c,s,i);else if(o.isFile()&&n===s.routesFile){let a=relative(e,c).split(sep).join("/"),m=a.replace(/\/?[^/]+$/,"");i.push({absPath:c,relPath:a,relDir:m});}}}var Z=".frsrc.json";function Q(e=process.cwd()){let t=resolve(e,Z);if(!existsSync(t))return {};try{return JSON.parse(readFileSync(t,"utf8"))}catch{return {}}}function fe(e,t=process.cwd()){let s=resolve(t,Z),r={...Q(t),...e};return writeFileSync(s,`${JSON.stringify(r,null,2)}
20
+ `,"utf8"),s}function me(e){let[t,...s]=e,i={};for(let r=0;r<s.length;r++){let n=s[r];if(!n.startsWith("--"))continue;let c=n.slice(2),o=s[r+1];o&&!o.startsWith("--")?(i[c]=o,r++):i[c]=true;}return {command:t??"help",flags:i}}function K(){console.log(`frs \u2014 Hono file-based codegen
21
21
 
22
22
  Usage:
23
23
  frs init [flags]
@@ -78,82 +78,105 @@ Examples:
78
78
  frs new createPost --domain posts --method post
79
79
  frs new listPosts --domain posts --method get --api v1
80
80
  frs add service postRepo
81
- `);}function W(e){if(typeof e=="string")return e.split(",").map(t=>t.trim()).filter(Boolean)}function f(e){return typeof e=="string"?e:void 0}function Z(e){if(e||!stdin.isTTY)return {ask:async(s,o)=>o??"",askChoice:async(s,o,r)=>r??"",askBool:async(s,o)=>o,close:()=>{}};let t=createInterface({input:stdin,output:stdout});return {async ask(s,o){let r=o?` (${o})`:"";return (await t.question(`? ${s}${r} \u203A `)).trim()||o||""},async askChoice(s,o,r){let i=` [${o.join("/")}${r?`, default: ${r}`:""}]`;for(;;){let c=(await t.question(`? ${s}${i} \u203A `)).trim().toLowerCase();if(!c&&r)return r;if(o.includes(c))return c;console.log(` invalid choice \u2014 pick one of: ${o.join(", ")}`);}},async askBool(s,o){let r=` (${o?"Y/n":"y/N"})`,i=(await t.question(`? ${s}${r} \u203A `)).trim().toLowerCase();return i?i==="y"||i==="yes"||i==="true":o},close:()=>t.close()}}async function fe(e){let t=f(e.root);t||(console.error("[frs] --root is required"),process.exit(2));let s=resolve(process.cwd(),t);existsSync(s)||(console.error(`[frs] root not found: ${s}`),process.exit(2));let o=f(e.out)??"__generated__/routes.ts",r=W(e.skip)??I.skipSegments,i=f(e.casing)==="kebab"?"kebab":I.casing,c={skipSegments:r,casing:i},n=f(e.ext)??".js",a=W(e.exclude)??E.excludeSegments,g=f(e["routes-file"])??E.routesFile,l=B(s,{routesFile:g,excludeSegments:a});l.length===0&&console.warn(`[frs] no "${g}" files found under ${s} \u2014 generated an empty manifest.`);let v=G(l,{outFile:resolve(s,o),derive:c,importExtension:n});if(!e.silent){console.log(`[frs] wrote ${v.outFile} (${v.routeCount} route${v.routeCount===1?"":"s"})`);for(let{source:y,url:$}of v.derivedPaths)console.log(` ${$.padEnd(48)} \u2190 ${y}`);}}async function me(e,t){let s=t.yes===true,o=Z(s);try{let r=e&&!e.startsWith("--")?e:void 0;r||(r=(await o.ask("Route name (e.g. createPost)")).trim(),r||(console.error("[frs] route name is required"),process.exit(2)));let i=f(t.domain);i||(i=(await o.ask("Domain name (e.g. posts)")).trim(),i||(console.error("[frs] --domain is required"),process.exit(2)));let c=f(t.root)??"src/domains",n=f(t.method)?.toLowerCase();n||(n=await o.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(n)||(console.error(`[frs] invalid --method: ${n}`),process.exit(2));let a=f(t.api);a||(a=(await o.ask("API tag","v1")).trim()||"v1");let g=f(t["usecase-folder"])??"useCases",p=t["with-usecase"]===void 0?s?!0:await o.askBool("Scaffold useCase.ts?",!0):t["with-usecase"]!==!1,l=t["with-test"]===void 0?s||!p?p:await o.askBool("Scaffold useCase.test.ts (Vitest)?",!0):t["with-test"]!==!1,v=t.force===!0,y=resolve(process.cwd(),c),$=resolve(y,i,g,r),b=resolve($,"routes.ts"),D=resolve($,"useCase.ts"),x=resolve($,"useCase.test.ts");mkdirSync($,{recursive:!0});let m=`${r.charAt(0).toUpperCase()}${r.slice(1)}UseCase`,C=`/**
82
- * ${m} \u2014 pure business logic, no HTTP awareness.
83
- * Reusable across multiple routes / cron jobs / triggers.
81
+ `);}function V(e){if(typeof e=="string")return e.split(",").map(t=>t.trim()).filter(Boolean)}function f(e){return typeof e=="string"?e:void 0}function X(e){if(e||!stdin.isTTY)return {ask:async(s,i)=>i??"",askChoice:async(s,i,r)=>r??"",askBool:async(s,i)=>i,close:()=>{}};let t=createInterface({input:stdin,output:stdout});return {async ask(s,i){let r=i?` (${i})`:"";return (await t.question(`? ${s}${r} \u203A `)).trim()||i||""},async askChoice(s,i,r){let n=` [${i.join("/")}${r?`, default: ${r}`:""}]`;for(;;){let c=(await t.question(`? ${s}${n} \u203A `)).trim().toLowerCase();if(!c&&r)return r;if(i.includes(c))return c;console.log(` invalid choice \u2014 pick one of: ${i.join(", ")}`);}},async askBool(s,i){let r=` (${i?"Y/n":"y/N"})`,n=(await t.question(`? ${s}${r} \u203A `)).trim().toLowerCase();return n?n==="y"||n==="yes"||n==="true":i},close:()=>t.close()}}async function ge(e){let t=f(e.root);t||(console.error("[frs] --root is required"),process.exit(2));let s=resolve(process.cwd(),t);existsSync(s)||(console.error(`[frs] root not found: ${s}`),process.exit(2));let i=f(e.out)??"__generated__/routes.ts",r=V(e.skip)??I.skipSegments,n=f(e.casing)==="kebab"?"kebab":I.casing,c={skipSegments:r,casing:n},o=f(e.ext)??".js",a=V(e.exclude)??E.excludeSegments,m=f(e["routes-file"])??E.routesFile,l=H(s,{routesFile:m,excludeSegments:a});l.length===0&&console.warn(`[frs] no "${m}" files found under ${s} \u2014 generated an empty manifest.`);let h=M(l,{outFile:resolve(s,i),derive:c,importExtension:o});if(!e.silent){console.log(`[frs] wrote ${h.outFile} (${h.routeCount} route${h.routeCount===1?"":"s"})`);for(let{source:v,url:y}of h.derivedPaths)console.log(` ${y.padEnd(48)} \u2190 ${v}`);}}async function he(e,t){let s=t.yes===true,i=X(s);try{let r=e&&!e.startsWith("--")?e:void 0;r||(r=(await i.ask("Route name (e.g. createPost)")).trim(),r||(console.error("[frs] route name is required"),process.exit(2)));let n=f(t.domain);n||(n=(await i.ask("Domain name (e.g. posts)")).trim(),n||(console.error("[frs] --domain is required"),process.exit(2)));let c=f(t.root)??"src/domains",o=f(t.method)?.toLowerCase();o||(o=await i.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(o)||(console.error(`[frs] invalid --method: ${o}`),process.exit(2));let a=f(t.api);a||(a=(await i.ask("API tag","v1")).trim()||"v1");let m=f(t["usecase-folder"])??"useCases",u=t["with-usecase"]===void 0?s?!0:await i.askBool("Scaffold useCase.ts?",!0):t["with-usecase"]!==!1,l=t["with-test"]===void 0?s||!u?u:await i.askBool("Scaffold useCase.test.ts (Vitest)?",!0):t["with-test"]!==!1,h=t.force===!0,v=resolve(process.cwd(),c),y=resolve(v,n,m,r),b=resolve(y,"routes.ts"),F=resolve(y,"useCase.ts"),x=resolve(y,"useCase.test.ts");mkdirSync(y,{recursive:!0});let $=`${r.charAt(0).toUpperCase()}${r.slice(1)}UseCase`,P="@lpdjs/firestore-repo-service/servers/hono",k=we(v,y),D=o==="get"?"// GET \u2192 lu depuis les query params":`// ${o.toUpperCase()} \u2192 lu depuis le body JSON`,A=`/**
82
+ * ${$} \u2014 pure business logic, no HTTP awareness.
83
+ *
84
+ * Owns its Zod \`input\` / \`output\` schemas (declared as \`static\` members, the
85
+ * single source of truth shared with \`routes.ts\`) and runs the logic in
86
+ * \`execute\`. The shared \`services\` container is injected by the \`UseCase\` base
87
+ * class via the constructor.
84
88
  */
85
89
 
86
- export interface ${m}Input {
87
- // TODO: define the input shape
88
- example: string;
89
- }
90
+ import { z } from "zod";
91
+ import { UseCase } from "${P}";
92
+ import type { Services } from "${k}";
90
93
 
91
- export interface ${m}Output {
92
- // TODO: define the output shape
93
- id: string;
94
- }
94
+ const input = z.object({
95
+ ${D}
96
+ example: z.string(),
97
+ });
98
+
99
+ const output = z.object({
100
+ id: z.string(),
101
+ });
95
102
 
96
- export class ${m} {
97
- // TODO: inject repositories / services via the constructor.
98
- // constructor(private readonly repo: SomeRepository) {}
103
+ export class ${$} extends UseCase<typeof input, typeof output, Services> {
104
+ static readonly input = input;
105
+ static readonly output = output;
99
106
 
100
- async execute(input: ${m}Input): Promise<${m}Output> {
101
- // TODO: implement
102
- return { id: input.example };
107
+ async execute(
108
+ payload: z.infer<typeof input>,
109
+ ): Promise<z.infer<typeof output>> {
110
+ // TODO: implement using \`this.services\`
111
+ return { id: payload.example };
103
112
  }
104
113
  }
105
- `,F=n==="get"?`z.object({
106
- // GET \u2192 lu depuis les query params
107
- example: z.string(),
108
- })`:`z.object({
109
- // ${n.toUpperCase()} \u2192 lu depuis le body JSON
110
- example: z.string(),
111
- })`,_=p?` const useCase = new ${m}();
112
- const data = await useCase.execute(input);
113
- return data;`:` // TODO: business logic
114
- return { id: input.example };`,A=p?`import { ${m} } from "./useCase.js";
115
- `:"",S=`import { z } from "zod";
116
- import { defineRoute } from "${f(t["apis-import"])??ve(y,$)}";
117
- ${A}
118
- export default defineRoute({
119
- api: "${a}",
120
- method: "${n}",
121
-
122
- input: ${F},
123
-
124
- output: z.object({
125
- id: z.string(),
114
+ `,g=o==="get"?`
115
+ source: "query",`:"",C=u?`import { ${$} } from "./useCase.js";
116
+ `:"",j=f(t["apis-import"])??$e(v,y),d=u?`import { defineRoutes } from "${P}";
117
+ import { useCaseRoute } from "${j}";
118
+ ${C}
119
+ export default defineRoutes([
120
+ useCaseRoute(${$}, {
121
+ api: "${a}",
122
+ method: "${o}",${g}
123
+ summary: "TODO: ${r}",
124
+ tags: ["${n}"],
126
125
  }),
126
+ ]);
127
+ `:`import { z } from "zod";
128
+ import { defineRoutes } from "${P}";
129
+ import { defineRoute } from "${j}";
130
+
131
+ export default defineRoutes([
132
+ defineRoute({
133
+ api: "${a}",
134
+ method: "${o}",
135
+
136
+ input: z.object({
137
+ ${D}
138
+ example: z.string(),
139
+ }),
140
+
141
+ output: z.object({
142
+ id: z.string(),
143
+ }),
144
+
145
+ summary: "TODO: ${r}",
146
+ tags: ["${n}"],
147
+
148
+ handler: async ({ input }) => {
149
+ // TODO: business logic
150
+ return { id: input.example };
151
+ },
152
+ }),
153
+ ]);
154
+ `,_=[],U=[],q=(S,ee)=>{if(existsSync(S)&&!h){U.push(S);return}writeFileSync(S,ee,"utf8"),_.push(S);};if(q(b,d),u&&q(F,A),u&&l){let S=`import { describe, it, expect } from "vitest";
155
+ import type { Services } from "${k}";
156
+ import { ${$} } from "./useCase.js";
127
157
 
128
- summary: "TODO: ${r}",
129
- tags: ["${i}"],
130
-
131
- handler: async ({ input }) => {
132
- ${_}
133
- },
134
- });
135
- `,j=[],u=[],R=(w,Q)=>{if(existsSync(w)&&!v){u.push(w);return}writeFileSync(w,Q,"utf8"),j.push(w);};if(R(b,S),p&&R(D,C),p&&l){let w=`import { describe, it, expect } from "vitest";
136
- import { ${m} } from "./useCase.js";
137
-
138
- describe("${m}", () => {
158
+ describe("${$}", () => {
139
159
  it("returns a response shaped like the output schema", async () => {
140
- const useCase = new ${m}();
160
+ // TODO: replace with real mocks for the services the useCase consumes.
161
+ const services = {} as unknown as Services;
162
+
163
+ const useCase = new ${$}(services);
141
164
  const result = await useCase.execute({ example: "hello" });
142
165
  expect(result).toMatchObject({ id: expect.any(String) });
143
166
  });
144
167
 
145
168
  // TODO: add error-path tests, repository mocks, etc.
146
169
  });
147
- `;R(x,w);}for(let w of j)console.log(`[frs] wrote ${w}`);for(let w of u)console.log(`[frs] skipped ${w} (use --force to overwrite)`);console.log(`
148
- [frs] reminder: run "frs gen --root ${c}" to refresh the manifest.`);}finally{o.close();}}async function ge(e){let t=e.yes===true,s=Z(t);try{let o=e.force===!0,r=f(e.root);r||(r=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let i=f(e["apis-file"]);i||(i=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let c=f(e["services-file"]);if(!c){let u=i.replace(/apis\.ts$/,"services.ts")||"src/services.ts";c=(await s.ask("services.ts location",u)).trim()||u;}let n=f(e.apis);n||(n=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let a=n.split(",").map(u=>u.trim()).filter(Boolean);a.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let g=f(e["base-path"]),p=resolve(process.cwd(),r),l=resolve(process.cwd(),i),v=resolve(process.cwd(),c),y=resolve(p,"__generated__"),$=resolve(y,"routes.ts"),b=[],D=[],x=(u,R)=>{if(mkdirSync(dirname(u),{recursive:!0}),existsSync(u)&&!o){D.push(u);return}writeFileSync(u,R,"utf8"),b.push(u);},m=a.map(u=>{let R=g??`/${u}`;return ` ${u}: {
149
- basePath: "${R}",
170
+ `;q(x,S);}for(let S of _)console.log(`[frs] wrote ${S}`);for(let S of U)console.log(`[frs] skipped ${S} (use --force to overwrite)`);console.log(`
171
+ [frs] reminder: run "frs gen --root ${c}" to refresh the manifest.`);}finally{i.close();}}async function ve(e){let t=e.yes===true,s=X(t);try{let i=e.force===!0,r=f(e.root);r||(r=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let n=f(e["apis-file"]);n||(n=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let c=f(e["services-file"]);if(!c){let d=n.replace(/apis\.ts$/,"services.ts")||"src/services.ts";c=(await s.ask("services.ts location",d)).trim()||d;}let o=f(e.apis);o||(o=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let a=o.split(",").map(d=>d.trim()).filter(Boolean);a.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let m=f(e["base-path"]),u=resolve(process.cwd(),r),l=resolve(process.cwd(),n),h=resolve(process.cwd(),c),v=resolve(u,"__generated__"),y=resolve(v,"routes.ts"),b=[],F=[],x=(d,_)=>{if(mkdirSync(dirname(d),{recursive:!0}),existsSync(d)&&!i){F.push(d);return}writeFileSync(d,_,"utf8"),b.push(d);},$=a.map(d=>{let _=m??`/${d}`;return ` ${d}: {
172
+ basePath: "${_}",
150
173
  openapi: {
151
- info: { title: "${u.toUpperCase()} API", version: "1.0.0", description: "" },
174
+ info: { title: "${d.toUpperCase()} API", version: "1.0.0", description: "" },
152
175
  },
153
176
  verbose: process.env["NODE_ENV"] !== "production",
154
177
  },`}).join(`
155
- `),C=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
156
- import { services } from "${T(dirname(l),v)}";
178
+ `),P=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
179
+ import { services } from "${T(dirname(l),h)}";
157
180
 
158
181
  /**
159
182
  * Single source of truth for every API exposed by this project.
@@ -165,14 +188,15 @@ import { services } from "${T(dirname(l),v)}";
165
188
  */
166
189
  export const apis = createApiRegistry(
167
190
  {
168
- ${m}
191
+ ${$}
169
192
  },
170
193
  { services },
171
194
  );
172
195
 
173
- /** Typed helper used inside every route file. */
196
+ /** Typed helpers used inside every route file. */
174
197
  export const defineRoute = apis.defineRoute;
175
- `;x(l,C),x(v,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
198
+ export const useCaseRoute = apis.useCaseRoute;
199
+ `;x(l,P),x(h,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
176
200
 
177
201
  /**
178
202
  * Global DI container \u2014 declare every singleton (repositories, SDK
@@ -205,20 +229,20 @@ export const services = createServices({
205
229
 
206
230
  /** Convenience type \u2014 \`function fn(svc: Services) { ... }\`. */
207
231
  export type Services = typeof services;
208
- `);let _=`// AUTO-GENERATED by frs \u2014 do not edit.
232
+ `);let D=`// AUTO-GENERATED by frs \u2014 do not edit.
209
233
  // Run \`frs gen --root ${r}\` to refresh.
210
234
 
211
235
  import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
212
236
 
213
237
  export const routes: AnyRouteDef[] = [];
214
- `;x($,_);let A=le({root:r,apisFile:i,servicesFile:c});b.push(A);let h=T(dirname(l),l),S=T(dirname(l),$),j=a.length===1?`export const { ${a[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${a.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let u of b)console.log(`[frs] wrote ${u}`);for(let u of D)console.log(`[frs] skipped ${u} (use --force to overwrite)`);console.log(`
238
+ `;x(y,D);let A=fe({root:r,apisFile:n,servicesFile:c});b.push(A);let g=T(dirname(l),l),C=T(dirname(l),y),j=a.length===1?`export const { ${a[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${a.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let d of b)console.log(`[frs] wrote ${d}`);for(let d of F)console.log(`[frs] skipped ${d} (use --force to overwrite)`);console.log(`
215
239
  Next steps:
216
240
 
217
241
  1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
218
242
 
219
243
  import { onRequest } from "firebase-functions/v2/https";
220
- import { apis } from "${h}";
221
- import { routes } from "${S}";
244
+ import { apis } from "${g}";
245
+ import { routes } from "${C}";
222
246
 
223
247
  ${j}
224
248
  defaults: { region: "us-central1", invoker: "public" },
@@ -231,17 +255,17 @@ Next steps:
231
255
  3. Refresh the manifest before each build:
232
256
 
233
257
  frs gen --root ${r}
234
- `);}finally{s.close();}}function T(e,t){let s=relative(e,t).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function he(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 o=s.force===true,r=Y(),c=[f(s["services-file"]),r.servicesFile,"src/services.ts","services.ts"].filter(h=>typeof h=="string"&&h.length>0),n;for(let h of c){let S=resolve(process.cwd(),h);if(existsSync(S)){n=S;break}}if(!n){let h=c.map(S=>resolve(process.cwd(),S)).join(`
258
+ `);}finally{s.close();}}function T(e,t){let s=relative(e,t).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function ye(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 i=s.force===true,r=Q(),c=[f(s["services-file"]),r.servicesFile,"src/services.ts","services.ts"].filter(g=>typeof g=="string"&&g.length>0),o;for(let g of c){let C=resolve(process.cwd(),g);if(existsSync(C)){o=C;break}}if(!o){let g=c.map(C=>resolve(process.cwd(),C)).join(`
235
259
  `);console.error(`[frs] services file not found. Tried:
236
- ${h}
237
- Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=f(s["services-dir"])??r.servicesDir??resolve(dirname(n),"services"),g=resolve(process.cwd(),a);mkdirSync(g,{recursive:true});let p=`${t.charAt(0).toUpperCase()}${t.slice(1)}Service`,l=resolve(g,`${t}.ts`),v=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
260
+ ${g}
261
+ Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=f(s["services-dir"])??r.servicesDir??resolve(dirname(o),"services"),m=resolve(process.cwd(),a);mkdirSync(m,{recursive:true});let u=`${t.charAt(0).toUpperCase()}${t.slice(1)}Service`,l=resolve(m,`${t}.ts`),h=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
238
262
 
239
263
  /**
240
- * ${p} \u2014 generated by \`frs add service ${t}\`.
264
+ * ${u} \u2014 generated by \`frs add service ${t}\`.
241
265
  *
242
266
  * Registered with a **factory** in \`services.ts\` so dependencies are
243
267
  * destructured at registration time. Add new constructor parameters here
244
- * and update the factory line (\`({ ctx, otherSvc }) => new ${p}(ctx, otherSvc)\`)
268
+ * and update the factory line (\`({ ctx, otherSvc }) => new ${u}(ctx, otherSvc)\`)
245
269
  * \u2014 TypeScript will tell you when something is missing.
246
270
  *
247
271
  * Async resources (DB connections, SDK clients) should stay lazy-loaded
@@ -255,7 +279,7 @@ Next steps:
255
279
  * }
256
280
  * \`\`\`
257
281
  */
258
- export class ${p} {
282
+ export class ${u} {
259
283
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
260
284
  constructor(private readonly ctx: RequestContext) {}
261
285
 
@@ -263,9 +287,9 @@ export class ${p} {
263
287
  return \`hello from ${t} \u2014 user=\${this.ctx.maybeC?.get("user")?.id ?? "anonymous"}\`;
264
288
  }
265
289
  }
266
- `;existsSync(l)&&!o?console.log(`[frs] skipped ${l} (use --force to overwrite)`):(writeFileSync(l,v,"utf8"),console.log(`[frs] wrote ${l}`));let y=readFileSync(n,"utf8"),$=T(dirname(n),l),b=`import { ${p} } from "${$}";`,D=` ${t}: ({ ctx }) => new ${p}(ctx),`;if(y.includes(b)){console.log(`[frs] services.ts already registers "${t}" \u2014 skipping.`);return}let x=y.split(`
267
- `),m=-1;for(let h=0;h<x.length;h++)/^import\s/.test(x[h])&&(m=h);m>=0?x.splice(m+1,0,b):x.unshift(b);let C=x.join(`
268
- `),F=C.match(/createServices\s*\(\s*\{/);if(!F){console.error(`[frs] could not find \`createServices({\` in ${n} \u2014 register "${t}" manually.`);return}let _=F.index+F[0].length,A=C.slice(0,_)+`
269
- `+D+C.slice(_);writeFileSync(n,A,"utf8"),console.log(`[frs] updated ${n} (+ ${t})`);}function ve(e,t){let s=["apis.ts","apis.js","api.ts","api.js"],o=[e,dirname(e),dirname(dirname(e))];for(let r of o)for(let i of s){let c=resolve(r,i);if(existsSync(c)){let n=relative(t,c).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js").replace(/\.js$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../apis.js"}async function ye(){let e=process.argv.slice(2),{command:t,flags:s}=de(e);switch(t){case "init":await ge(s);return;case "gen":await fe(s);return;case "new":await me(e[1],s);return;case "add":await he(e[1],e[2],s);return;case "help":case "--help":case "-h":H();return;default:console.error(`[frs] unknown command: ${t}
270
- `),H(),process.exit(2);}}ye().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.js.map
290
+ `;existsSync(l)&&!i?console.log(`[frs] skipped ${l} (use --force to overwrite)`):(writeFileSync(l,h,"utf8"),console.log(`[frs] wrote ${l}`));let v=readFileSync(o,"utf8"),y=T(dirname(o),l),b=`import { ${u} } from "${y}";`,F=` ${t}: ({ ctx }) => new ${u}(ctx),`;if(v.includes(b)){console.log(`[frs] services.ts already registers "${t}" \u2014 skipping.`);return}let x=v.split(`
291
+ `),$=-1;for(let g=0;g<x.length;g++)/^import\s/.test(x[g])&&($=g);$>=0?x.splice($+1,0,b):x.unshift(b);let P=x.join(`
292
+ `),k=P.match(/createServices\s*\(\s*\{/);if(!k){console.error(`[frs] could not find \`createServices({\` in ${o} \u2014 register "${t}" manually.`);return}let D=k.index+k[0].length,A=P.slice(0,D)+`
293
+ `+F+P.slice(D);writeFileSync(o,A,"utf8"),console.log(`[frs] updated ${o} (+ ${t})`);}function $e(e,t){let s=["apis.ts","apis.js","api.ts","api.js"],i=[e,dirname(e),dirname(dirname(e))];for(let r of i)for(let n of s){let c=resolve(r,n);if(existsSync(c)){let o=relative(t,c).replace(/\\/g,"/");return o=o.replace(/\.ts$/,".js").replace(/\.js$/,".js"),o.startsWith(".")||(o=`./${o}`),o}}return "../../../../apis.js"}function we(e,t){let s=["services.ts","services.js"],i=[e,dirname(e),dirname(dirname(e))];for(let r of i)for(let n of s){let c=resolve(r,n);if(existsSync(c)){let o=relative(t,c).replace(/\\/g,"/");return o=o.replace(/\.ts$/,".js"),o.startsWith(".")||(o=`./${o}`),o}}return "../../../../services.js"}async function xe(){let e=process.argv.slice(2),{command:t,flags:s}=me(e);switch(t){case "init":await ve(s);return;case "gen":await ge(s);return;case "new":await he(e[1],s);return;case "add":await ye(e[1],e[2],s);return;case "help":case "--help":case "-h":K();return;default:console.error(`[frs] unknown command: ${t}
294
+ `),K(),process.exit(2);}}xe().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.js.map
271
295
  //# sourceMappingURL=cli.js.map