@lpdjs/firestore-repo-service 2.6.3 → 2.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -317,11 +317,15 @@ npm i -D @asteasolutions/zod-to-openapi
317
317
  ### Bootstrap
318
318
 
319
319
  ```bash
320
- npx frs init # interactive — creates apis.ts + manifest stub
320
+ npx frs init # interactive — creates apis.ts + manifest stub + .frsrc.json
321
321
  npx frs new createPost --domain posts --method post --api v1
322
322
  npx frs gen --root src/domains # refresh manifest (run before each build)
323
323
  ```
324
324
 
325
+ `frs init` also writes a `.frsrc.json` (project root, e.g. `root`, `apisFile`,
326
+ `apis`). Later commands read it so flags become optional — precedence is
327
+ **flag → `.frsrc.json` → default**. See the Hono guide for the full key list.
328
+
325
329
  ### Configure your APIs (`apis.ts`)
326
330
 
327
331
  ```typescript
@@ -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 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=path.dirname(t.outFile);fs.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"}.
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"}.
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
- `+o.join(`
12
- `)+(o.length?`
11
+ `+i.join(`
12
+ `)+(i.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,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=fs.readdirSync(t);}catch{return}for(let n of r){if(s.excludeSegments.includes(n))continue;let c=path.join(t,n),o;try{o=fs.statSync(c);}catch{continue}if(o.isDirectory())W(e,c,s,i);else if(o.isFile()&&n===s.routesFile){let a=path.relative(e,c).split(path.sep).join("/"),m=a.replace(/\/?[^/]+$/,"");i.push({absPath:c,relPath:a,relDir:m});}}}var Z=".frsrc.json";function Q(e=process.cwd()){let t=path.resolve(e,Z);if(!fs.existsSync(t))return {};try{return JSON.parse(fs.readFileSync(t,"utf8"))}catch{return {}}}function fe(e,t=process.cwd()){let s=path.resolve(t,Z),r={...Q(t),...e};return fs.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
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
21
21
 
22
22
  Usage:
23
23
  frs init [flags]
@@ -38,9 +38,10 @@ Flags (init):
38
38
  --yes Skip prompts, use defaults / flag values
39
39
 
40
40
  Flags (gen):
41
- --root <dir> Domain root to scan (required, e.g. src/domains)
41
+ --root <dir> Domain root to scan (e.g. src/domains)
42
+ \u2014 falls back to "root" in .frsrc.json
42
43
  --out <file> Output file relative to --root
43
- (default: __generated__/routes.ts)
44
+ (default: .frsrc.json "out" or __generated__/routes.ts)
44
45
  --routes-file <name> Filename to look for (default: routes.ts)
45
46
  --skip <list> Comma-separated path segments to drop from URLs
46
47
  (default: useCases,useCase,use-cases,use-case)
@@ -53,12 +54,14 @@ Flags (gen):
53
54
  --silent Do not print the generated route table
54
55
 
55
56
  Flags (new <name>):
56
- --root <dir> Domain root (default: src/domains)
57
+ --root <dir> Domain root (default: .frsrc.json "root" or src/domains)
57
58
  --domain <name> Domain name (e.g. posts) \u2014 prompted if missing
58
59
  --method <verb> HTTP method (default: post) \u2014 prompted if missing
59
- --api <tag> API tag (default: v1) \u2014 prompted if missing
60
+ --api <tag> API tag (default: .frsrc.json first "apis" or v1)
61
+ \u2014 prompted if missing
60
62
  --usecase-folder <name>
61
- Parent folder under <domain>. Default: useCases
63
+ Parent folder under <domain>.
64
+ Default: .frsrc.json "useCaseFolder" or useCases
62
65
  --with-usecase Also scaffold a sibling useCase.ts file (default: true)
63
66
  --with-test Also scaffold a sibling useCase.test.ts (Vitest, default: true)
64
67
  --apis-import <path> Import path for the registry (default: auto-detect
@@ -78,8 +81,8 @@ Examples:
78
81
  frs new createPost --domain posts --method post
79
82
  frs new listPosts --domain posts --method get --api v1
80
83
  frs add service postRepo
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||!process$1.stdin.isTTY)return {ask:async(s,i)=>i??"",askChoice:async(s,i,r)=>r??"",askBool:async(s,i)=>i,close:()=>{}};let t=promises.createInterface({input:process$1.stdin,output:process$1.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=path.resolve(process.cwd(),t);fs.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:path.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=path.resolve(process.cwd(),c),y=path.resolve(v,n,m,r),b=path.resolve(y,"routes.ts"),F=path.resolve(y,"useCase.ts"),x=path.resolve(y,"useCase.test.ts");fs.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.
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.
83
86
  *
84
87
  * Owns its Zod \`input\` / \`output\` schemas (declared as \`static\` members, the
85
88
  * single source of truth shared with \`routes.ts\`) and runs the logic in
@@ -89,10 +92,10 @@ Examples:
89
92
 
90
93
  import { z } from "zod";
91
94
  import { UseCase } from "${P}";
92
- import type { Services } from "${k}";
95
+ import type { Services } from "${F}";
93
96
 
94
97
  const input = z.object({
95
- ${D}
98
+ ${_}
96
99
  example: z.string(),
97
100
  });
98
101
 
@@ -100,7 +103,7 @@ const output = z.object({
100
103
  id: z.string(),
101
104
  });
102
105
 
103
- export class ${$} extends UseCase<typeof input, typeof output, Services> {
106
+ export class ${y} extends UseCase<typeof input, typeof output, Services> {
104
107
  static readonly input = input;
105
108
  static readonly output = output;
106
109
 
@@ -111,30 +114,30 @@ export class ${$} extends UseCase<typeof input, typeof output, Services> {
111
114
  return { id: payload.example };
112
115
  }
113
116
  }
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}
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
122
  export default defineRoutes([
120
- useCaseRoute(${$}, {
121
- api: "${a}",
122
- method: "${o}",${g}
123
- summary: "TODO: ${r}",
124
- tags: ["${n}"],
123
+ useCaseRoute(${y}, {
124
+ api: "${d}",
125
+ method: "${a}",${C}
126
+ summary: "TODO: ${n}",
127
+ tags: ["${c}"],
125
128
  }),
126
129
  ]);
127
130
  `:`import { z } from "zod";
128
131
  import { defineRoutes } from "${P}";
129
- import { defineRoute } from "${j}";
132
+ import { defineRoute } from "${p}";
130
133
 
131
134
  export default defineRoutes([
132
135
  defineRoute({
133
- api: "${a}",
134
- method: "${o}",
136
+ api: "${d}",
137
+ method: "${a}",
135
138
 
136
139
  input: z.object({
137
- ${D}
140
+ ${_}
138
141
  example: z.string(),
139
142
  }),
140
143
 
@@ -142,8 +145,8 @@ export default defineRoutes([
142
145
  id: z.string(),
143
146
  }),
144
147
 
145
- summary: "TODO: ${r}",
146
- tags: ["${n}"],
148
+ summary: "TODO: ${n}",
149
+ tags: ["${c}"],
147
150
 
148
151
  handler: async ({ input }) => {
149
152
  // TODO: business logic
@@ -151,32 +154,32 @@ export default defineRoutes([
151
154
  },
152
155
  }),
153
156
  ]);
154
- `,_=[],U=[],q=(S,ee)=>{if(fs.existsSync(S)&&!h){U.push(S);return}fs.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";
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";
157
160
 
158
- describe("${$}", () => {
161
+ describe("${y}", () => {
159
162
  it("returns a response shaped like the output schema", async () => {
160
163
  // TODO: replace with real mocks for the services the useCase consumes.
161
164
  const services = {} as unknown as Services;
162
165
 
163
- const useCase = new ${$}(services);
166
+ const useCase = new ${y}(services);
164
167
  const result = await useCase.execute({ example: "hello" });
165
168
  expect(result).toMatchObject({ id: expect.any(String) });
166
169
  });
167
170
 
168
171
  // TODO: add error-path tests, repository mocks, etc.
169
172
  });
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=path.resolve(process.cwd(),r),l=path.resolve(process.cwd(),n),h=path.resolve(process.cwd(),c),v=path.resolve(u,"__generated__"),y=path.resolve(v,"routes.ts"),b=[],F=[],x=(d,_)=>{if(fs.mkdirSync(path.dirname(d),{recursive:!0}),fs.existsSync(d)&&!i){F.push(d);return}fs.writeFileSync(d,_,"utf8"),b.push(d);},$=a.map(d=>{let _=m??`/${d}`;return ` ${d}: {
172
- basePath: "${_}",
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}",
173
176
  openapi: {
174
- info: { title: "${d.toUpperCase()} API", version: "1.0.0", description: "" },
177
+ info: { title: "${p.toUpperCase()} API", version: "1.0.0", description: "" },
175
178
  },
176
179
  verbose: process.env["NODE_ENV"] !== "production",
177
180
  },`}).join(`
178
- `),P=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
179
- import { services } from "${T(path.dirname(l),h)}";
181
+ `),y=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
182
+ import { services } from "${E(path.dirname(u),x)}";
180
183
 
181
184
  /**
182
185
  * Single source of truth for every API exposed by this project.
@@ -188,7 +191,7 @@ import { services } from "${T(path.dirname(l),h)}";
188
191
  */
189
192
  export const apis = createApiRegistry(
190
193
  {
191
- ${$}
194
+ ${k}
192
195
  },
193
196
  { services },
194
197
  );
@@ -196,7 +199,7 @@ ${$}
196
199
  /** Typed helpers used inside every route file. */
197
200
  export const defineRoute = apis.defineRoute;
198
201
  export const useCaseRoute = apis.useCaseRoute;
199
- `;x(l,P),x(h,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
202
+ `;b(u,y),b(x,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
200
203
 
201
204
  /**
202
205
  * Global DI container \u2014 declare every singleton (repositories, SDK
@@ -229,22 +232,22 @@ export const services = createServices({
229
232
 
230
233
  /** Convenience type \u2014 \`function fn(svc: Services) { ... }\`. */
231
234
  export type Services = typeof services;
232
- `);let D=`// AUTO-GENERATED by frs \u2014 do not edit.
233
- // Run \`frs gen --root ${r}\` to refresh.
235
+ `);let F=`// AUTO-GENERATED by frs \u2014 do not edit.
236
+ // Run \`frs gen --root ${o}\` to refresh.
234
237
 
235
238
  import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
236
239
 
237
240
  export const routes: AnyRouteDef[] = [];
238
- `;x(y,D);let A=fe({root:r,apisFile:n,servicesFile:c});b.push(A);let g=T(path.dirname(l),l),C=T(path.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(`
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(`
239
242
  Next steps:
240
243
 
241
244
  1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
242
245
 
243
246
  import { onRequest } from "firebase-functions/v2/https";
244
- import { apis } from "${g}";
247
+ import { apis } from "${h}";
245
248
  import { routes } from "${C}";
246
249
 
247
- ${j}
250
+ ${N}
248
251
  defaults: { region: "us-central1", invoker: "public" },
249
252
  });
250
253
 
@@ -254,18 +257,18 @@ Next steps:
254
257
 
255
258
  3. Refresh the manifest before each build:
256
259
 
257
- frs gen --root ${r}
258
- `);}finally{s.close();}}function T(e,t){let s=path.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=path.resolve(process.cwd(),g);if(fs.existsSync(C)){o=C;break}}if(!o){let g=c.map(C=>path.resolve(process.cwd(),C)).join(`
260
+ 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(`
259
262
  `);console.error(`[frs] services file not found. Tried:
260
- ${g}
261
- Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=f(s["services-dir"])??r.servicesDir??path.resolve(path.dirname(o),"services"),m=path.resolve(process.cwd(),a);fs.mkdirSync(m,{recursive:true});let u=`${t.charAt(0).toUpperCase()}${t.slice(1)}Service`,l=path.resolve(m,`${t}.ts`),h=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
263
+ ${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";
262
265
 
263
266
  /**
264
- * ${u} \u2014 generated by \`frs add service ${t}\`.
267
+ * ${f} \u2014 generated by \`frs add service ${t}\`.
265
268
  *
266
269
  * Registered with a **factory** in \`services.ts\` so dependencies are
267
270
  * destructured at registration time. Add new constructor parameters here
268
- * and update the factory line (\`({ ctx, otherSvc }) => new ${u}(ctx, otherSvc)\`)
271
+ * and update the factory line (\`({ ctx, otherSvc }) => new ${f}(ctx, otherSvc)\`)
269
272
  * \u2014 TypeScript will tell you when something is missing.
270
273
  *
271
274
  * Async resources (DB connections, SDK clients) should stay lazy-loaded
@@ -279,7 +282,7 @@ Next steps:
279
282
  * }
280
283
  * \`\`\`
281
284
  */
282
- export class ${u} {
285
+ export class ${f} {
283
286
  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
284
287
  constructor(private readonly ctx: RequestContext) {}
285
288
 
@@ -287,9 +290,9 @@ export class ${u} {
287
290
  return \`hello from ${t} \u2014 user=\${this.ctx.maybeC?.get("user")?.id ?? "anonymous"}\`;
288
291
  }
289
292
  }
290
- `;fs.existsSync(l)&&!i?console.log(`[frs] skipped ${l} (use --force to overwrite)`):(fs.writeFileSync(l,h,"utf8"),console.log(`[frs] wrote ${l}`));let v=fs.readFileSync(o,"utf8"),y=T(path.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);fs.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,path.dirname(e),path.dirname(path.dirname(e))];for(let r of i)for(let n of s){let c=path.resolve(r,n);if(fs.existsSync(c)){let o=path.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,path.dirname(e),path.dirname(path.dirname(e))];for(let r of i)for(let n of s){let c=path.resolve(r,n);if(fs.existsSync(c)){let o=path.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.cjs.map
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
295
298
  //# sourceMappingURL=cli.cjs.map