@classytic/arc 2.15.3 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-BZX6zzDM.mjs +300 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3036
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
  32. package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
  39. package/dist/errors-C1lX_jlm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +1 -1
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
  49. package/dist/generate-BWFwgcCM.d.mts +38 -0
  50. package/dist/generate-CYac-OLv.mjs +654 -0
  51. package/dist/hooks/index.d.mts +1 -1
  52. package/dist/hooks/index.mjs +1 -1
  53. package/dist/idempotency/index.d.mts +2 -2
  54. package/dist/idempotency/index.mjs +1 -1
  55. package/dist/idempotency/redis.d.mts +1 -1
  56. package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
  57. package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
  58. package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +7 -8
  61. package/dist/init-Dv71MsJr.d.mts +71 -0
  62. package/dist/init-HDvoO9L5.mjs +3098 -0
  63. package/dist/integrations/event-gateway.d.mts +2 -2
  64. package/dist/integrations/event-gateway.mjs +1 -1
  65. package/dist/integrations/index.d.mts +2 -2
  66. package/dist/integrations/jobs.mjs +3 -3
  67. package/dist/integrations/mcp/index.d.mts +239 -7
  68. package/dist/integrations/mcp/index.mjs +2 -528
  69. package/dist/integrations/mcp/testing.d.mts +2 -2
  70. package/dist/integrations/mcp/testing.mjs +6 -10
  71. package/dist/integrations/streamline.d.mts +71 -2
  72. package/dist/integrations/streamline.mjs +81 -8
  73. package/dist/integrations/websocket-redis.d.mts +1 -1
  74. package/dist/integrations/websocket.d.mts +1 -1
  75. package/dist/integrations/websocket.mjs +1 -0
  76. package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
  77. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
  78. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  79. package/dist/middleware/index.d.mts +1 -1
  80. package/dist/middleware/index.mjs +1 -1
  81. package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
  82. package/dist/permissions/index.d.mts +2 -2
  83. package/dist/permissions/index.mjs +1 -1
  84. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  85. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  86. package/dist/pipeline/index.d.mts +1 -1
  87. package/dist/pipeline/index.mjs +1 -1
  88. package/dist/plugins/index.d.mts +5 -5
  89. package/dist/plugins/index.mjs +10 -10
  90. package/dist/plugins/response-cache.mjs +5 -5
  91. package/dist/plugins/tracing-entry.d.mts +1 -1
  92. package/dist/plugins/tracing-entry.mjs +1 -1
  93. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  94. package/dist/presets/filesUpload.d.mts +4 -4
  95. package/dist/presets/filesUpload.mjs +2 -2
  96. package/dist/presets/index.d.mts +1 -1
  97. package/dist/presets/index.mjs +1 -1
  98. package/dist/presets/multiTenant.d.mts +1 -1
  99. package/dist/presets/multiTenant.mjs +4 -3
  100. package/dist/presets/search.d.mts +2 -2
  101. package/dist/presets/search.mjs +1 -1
  102. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  103. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  104. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
  105. package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
  106. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
  107. package/dist/registry/index.d.mts +319 -2
  108. package/dist/registry/index.mjs +3 -3
  109. package/dist/registry-BBE23CDj.mjs +576 -0
  110. package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
  111. package/dist/scope/index.d.mts +3 -3
  112. package/dist/scope/index.mjs +3 -3
  113. package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
  114. package/dist/testing/index.d.mts +2 -2
  115. package/dist/testing/index.mjs +16 -7
  116. package/dist/testing/storageContract.d.mts +1 -1
  117. package/dist/types/index.d.mts +5 -5
  118. package/dist/types/storage.d.mts +1 -1
  119. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  120. package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
  121. package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
  122. package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
  123. package/dist/utils/index.d.mts +1286 -2
  124. package/dist/utils/index.mjs +1 -1
  125. package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
  126. package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
  127. package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
  128. package/package.json +22 -29
  129. package/skills/arc/SKILL.md +299 -689
  130. package/skills/arc/references/auth.md +19 -7
  131. package/skills/arc-code-review/SKILL.md +1 -1
  132. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  133. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  134. package/dist/index-bRjYu21O.d.mts +0 -1320
  135. package/dist/org/index.d.mts +0 -66
  136. package/dist/org/index.mjs +0 -486
  137. package/dist/org/types.d.mts +0 -82
  138. package/dist/org/types.mjs +0 -1
  139. package/dist/registry-I-ogLgL9.mjs +0 -46
  140. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
  141. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
  142. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  143. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
  144. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  145. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
  146. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
  147. /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
  148. /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
  149. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  150. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
  151. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
  152. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
  153. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
  154. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
  155. /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
  156. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
  157. /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
  158. /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
  159. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
@@ -0,0 +1,654 @@
1
+ import { t as pluralize } from "./pluralize-B9M8xvy-.mjs";
2
+ import { join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ //#region src/cli/commands/generate/name-helpers.ts
5
+ /**
6
+ * Case conversion for resource-name normalisation.
7
+ *
8
+ * `arc generate r org-profile` accepts kebab-case names from the CLI;
9
+ * templates need PascalCase for class identifiers (`OrgProfileRepository`)
10
+ * and camelCase for variable / instance names (`orgProfileResource`).
11
+ * Centralising the converters here keeps every template emitting the
12
+ * same shape — no one-off `.charAt(0).toUpperCase()` chains scattered
13
+ * through template code.
14
+ */
15
+ /** Convert kebab-case to PascalCase: `org-profile` → `OrgProfile`. */
16
+ function toPascalCase(name) {
17
+ return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
18
+ }
19
+ /** Convert PascalCase to camelCase: `OrgProfile` → `orgProfile`. */
20
+ function toCamelCase(pascalName) {
21
+ return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
22
+ }
23
+ //#endregion
24
+ //#region src/cli/commands/generate/project-config.ts
25
+ /**
26
+ * Read the host project's `.arcrc` (written at scaffold time by `arc init`)
27
+ * and detect whether it's a TypeScript project via the presence of
28
+ * `tsconfig.json`. Both checks degrade gracefully — a missing `.arcrc`
29
+ * just means defaults, and the TS check uses the file marker rather than
30
+ * inspecting `package.json` because the marker is what `tsx` / `tsc` also
31
+ * key off.
32
+ */
33
+ function readProjectConfig() {
34
+ try {
35
+ const rcPath = join(process.cwd(), ".arcrc");
36
+ return JSON.parse(readFileSync(rcPath, "utf-8"));
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+ function isTypeScriptProject() {
42
+ return existsSync(join(process.cwd(), "tsconfig.json"));
43
+ }
44
+ //#endregion
45
+ //#region src/cli/commands/generate/file-writer.ts
46
+ /**
47
+ * Scaffolding I/O for `arc generate`.
48
+ *
49
+ * Templates are pure (see `./templates.ts`); this module owns every
50
+ * disk-touching step:
51
+ *
52
+ * - `generateResource` writes the full resource folder
53
+ * (`<name>.model.ts` + `.repository.ts` + `.resource.ts` + optional
54
+ * `.mcp.ts`) plus the companion test file.
55
+ * - `generateFile` writes a single file (`controller` / `model` /
56
+ * `repository` / `schemas` / `mcp`).
57
+ *
58
+ * Both functions skip existing files with a clear console warning
59
+ * (idempotent re-runs are safe) and use `node:fs` sync APIs — the CLI
60
+ * is already synchronous in spirit and `Promise.all` parallelism on
61
+ * file writes is not worth the readability tax.
62
+ */
63
+ async function generateResource(name, lowerName, resourcePath, templates, ext, includeMcp = false) {
64
+ console.log(`\nGenerating resource: ${name}...\n`);
65
+ if (!existsSync(resourcePath)) {
66
+ mkdirSync(resourcePath, { recursive: true });
67
+ console.log(` + Created: src/resources/${lowerName}/`);
68
+ }
69
+ const files = {
70
+ [`${lowerName}.model.${ext}`]: templates.model(name, lowerName),
71
+ [`${lowerName}.repository.${ext}`]: templates.repository(name, lowerName),
72
+ [`${lowerName}.resource.${ext}`]: templates.resource(name, lowerName)
73
+ };
74
+ if (includeMcp) files[`${lowerName}.mcp.${ext}`] = templates.mcp(name, lowerName);
75
+ for (const [filename, content] of Object.entries(files)) {
76
+ const filepath = join(resourcePath, filename);
77
+ if (existsSync(filepath)) console.warn(` ! Skipped: ${filename} (already exists)`);
78
+ else {
79
+ writeFileSync(filepath, content);
80
+ console.log(` + Created: ${filename}`);
81
+ }
82
+ }
83
+ const testsDir = join(process.cwd(), "tests");
84
+ if (!existsSync(testsDir)) mkdirSync(testsDir, { recursive: true });
85
+ const testPath = join(testsDir, `${lowerName}.test.${ext}`);
86
+ if (!existsSync(testPath)) {
87
+ writeFileSync(testPath, templates.test(name, lowerName));
88
+ console.log(` + Created: tests/${lowerName}.test.${ext}`);
89
+ }
90
+ const camel = toCamelCase(name);
91
+ const isMultiTenant = readProjectConfig().tenant === "multi";
92
+ console.log(`
93
+ ╔═══════════════════════════════════════════════════════════════╗
94
+ ║ Resource Generated ║
95
+ ╚═══════════════════════════════════════════════════════════════╝
96
+
97
+ Next steps:
98
+
99
+ 1. Register in src/resources/index.${ext}:
100
+ import ${camel}Resource from './${lowerName}/${lowerName}.resource.js';
101
+
102
+ export const resources = [
103
+ // ... existing resources
104
+ ${camel}Resource,
105
+ ];
106
+
107
+ 2. Customize the model schema in:
108
+ src/resources/${lowerName}/${lowerName}.model.${ext}
109
+
110
+ 3. Adjust permissions in ${lowerName}.resource.${ext}:
111
+ ${isMultiTenant ? ` - requireOrgMembership() → member of the current organization
112
+ - multiTenantPreset() → injects and filters organizationId
113
+ - requireRoles(['admin']) → admin writes inside the org scope` : ` - requireAuth() → any authenticated user
114
+ - requireRoles(['admin']) → specific platform roles`}
115
+
116
+ 4. Run tests:
117
+ npm test
118
+ ${includeMcp ? `
119
+ 5. MCP tools file created: ${lowerName}.mcp.${ext}
120
+ Uncomment and customize the example tools.
121
+ Import and add to extraTools in your mcpPlugin config.
122
+ ` : ""}`);
123
+ }
124
+ async function generateFile(name, lowerName, resourcePath, fileType, template, ext) {
125
+ console.log(`\nGenerating ${fileType}: ${name}...\n`);
126
+ if (!existsSync(resourcePath)) {
127
+ mkdirSync(resourcePath, { recursive: true });
128
+ console.log(` + Created: src/resources/${lowerName}/`);
129
+ }
130
+ const filename = `${lowerName}.${fileType}.${ext}`;
131
+ const filepath = join(resourcePath, filename);
132
+ if (existsSync(filepath)) throw new Error(`${filename} already exists. Remove it first or use a different name.`);
133
+ writeFileSync(filepath, template(name, lowerName));
134
+ console.log(` + Created: ${filename}`);
135
+ }
136
+ //#endregion
137
+ //#region src/cli/commands/generate/templates.ts
138
+ /**
139
+ * Template factory for `arc generate`.
140
+ *
141
+ * `getTemplates(ts, config)` returns a record of per-file template
142
+ * functions. Each closure captures the language flag (`ts`) and project
143
+ * config (multi-tenant, adapter, etc.) so call sites only need to pass
144
+ * the resource-specific `name` / `fileName`. Templates remain pure —
145
+ * no I/O, so the scaffolder's file-writer is the only spot that touches
146
+ * disk.
147
+ *
148
+ * Templates included:
149
+ * - `model` Mongoose schema with optional tenant field + index
150
+ * - `repository` MongoKit-backed repo (or custom-adapter stub)
151
+ * - `controller` BaseController extension scaffold
152
+ * - `schemas` `buildCrudSchemasFromModel` with the field-rule menu
153
+ * - `resource` `defineResource` wiring — branches on multi-tenant
154
+ * - `mcp` Custom MCP tool examples (defineTool + zod schemas)
155
+ * - `test` Vitest spec using `createTestApp` + `expectArc`
156
+ */
157
+ /**
158
+ * Template functions accept:
159
+ * - `name`: PascalCase class name (e.g. `OrgProfile`)
160
+ * - `fileName`: kebab-case for file paths (e.g. `org-profile`)
161
+ */
162
+ function getTemplates(ts, config = {}) {
163
+ const isMultiTenant = config.tenant === "multi";
164
+ return {
165
+ model: (name, _fileName) => {
166
+ const camel = toCamelCase(name);
167
+ return `/**
168
+ * ${name} Model
169
+ * Generated by Arc CLI
170
+ */
171
+
172
+ ${ts ? "import mongoose, { type HydratedDocument, type Model, type Types } from 'mongoose';" : "import mongoose from 'mongoose';"}
173
+
174
+ const { Schema } = mongoose;
175
+ ${ts ? [
176
+ "",
177
+ "/**",
178
+ " * Persisted shape — what `.lean()` and `.toObject()` return. Carrying this",
179
+ ` * through \`Model<I${name}>\` lets \`.select(...)\` / \`.find(...)\` / \`.lean()\``,
180
+ " * infer correctly so domain methods don't need `as` casts.",
181
+ " *",
182
+ " * Replace the placeholder fields with your real domain shape.",
183
+ " */",
184
+ `export interface I${name} {`,
185
+ " _id: Types.ObjectId;",
186
+ ...isMultiTenant ? [" organizationId: Types.ObjectId;"] : [],
187
+ " // TODO: define your fields here",
188
+ " createdAt: Date;",
189
+ " updatedAt: Date;",
190
+ "}",
191
+ "",
192
+ `export type ${name}Document = HydratedDocument<I${name}>;`,
193
+ ""
194
+ ].join("\n") : ""}
195
+ const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
196
+ {
197
+ ${isMultiTenant ? " organizationId: { type: Schema.Types.ObjectId, required: true, index: true },\n" : ""} // TODO: declare your fields here, e.g. name: { type: String, required: true, trim: true },
198
+ },
199
+ { timestamps: true }
200
+ );
201
+
202
+ ${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, createdAt: -1 });\n` : ""}${ts ? [
203
+ `const ${name}: Model<I${name}> =`,
204
+ ` (mongoose.models.${name} as Model<I${name}> | undefined) ??`,
205
+ ` mongoose.model<I${name}>('${name}', ${camel}Schema);`
206
+ ].join("\n") : `const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);`}
207
+
208
+ export default ${name};
209
+ `;
210
+ },
211
+ repository: (name, fileName) => {
212
+ const camel = toCamelCase(name);
213
+ if (!(config.adapter === "mongokit" || !config.adapter)) {
214
+ const generic = ts ? `<I${name}>` : "";
215
+ return `/**
216
+ * ${name} Repository
217
+ * Generated by Arc CLI
218
+ *
219
+ * This project uses a custom adapter — wire your repository to whichever
220
+ * kit you're using (sqlitekit, prismakit, custom). Replace the body below
221
+ * with your kit's Repository constructor. Arc only requires the
222
+ * \`MinimalRepo\` floor (getAll/getById/create/update/delete) declared in
223
+ * \`@classytic/repo-core/repository\`.
224
+ */
225
+ ${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
226
+
227
+ // Replace with your kit's repository instance:
228
+ // import { Repository } from '@classytic/sqlitekit';
229
+ // const ${camel}Repository = new Repository${generic}(${name}Table);
230
+ // export default ${camel}Repository;
231
+
232
+ const ${camel}Repository = {
233
+ // TODO: implement MinimalRepo<${ts ? `I${name}` : "any"}>
234
+ } as never;
235
+
236
+ export default ${camel}Repository;
237
+ `;
238
+ }
239
+ const generic = ts ? `<I${name}>` : "";
240
+ return `/**
241
+ * ${name} Repository
242
+ * Generated by Arc CLI
243
+ */
244
+
245
+ import {
246
+ Repository,
247
+ methodRegistryPlugin,
248
+ softDeletePlugin,
249
+ } from '@classytic/mongokit';
250
+ import ${name} from './${fileName}.model.js';${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
251
+
252
+ class ${name}Repository extends Repository${generic} {
253
+ constructor() {
254
+ super(${name}, [methodRegistryPlugin(), softDeletePlugin()]);
255
+ }
256
+
257
+ // Add domain methods here. arc + mongokit ship the standard CRUD
258
+ // (getAll/getById/create/update/delete) on the base class, so only
259
+ // write a method when there's real domain logic to capture.
260
+ }
261
+
262
+ const ${camel}Repository = new ${name}Repository();
263
+ export default ${camel}Repository;
264
+ export { ${name}Repository };
265
+ `;
266
+ },
267
+ controller: (name, fileName) => {
268
+ const camel = toCamelCase(name);
269
+ return `/**
270
+ * ${name} Controller
271
+ * Generated by Arc CLI
272
+ *
273
+ * Note: defineResource() auto-creates a controller from the adapter.
274
+ * Only create a custom controller when you need custom methods.
275
+ */
276
+
277
+ import { BaseController } from '@classytic/arc';
278
+ import ${camel}Repository from './${fileName}.repository.js';
279
+
280
+ class ${name}Controller extends BaseController {
281
+ constructor() {
282
+ super(${camel}Repository, {
283
+ resourceName: '${fileName}',
284
+ });
285
+ }
286
+
287
+ // Add custom controller methods here
288
+ }
289
+
290
+ const ${camel}Controller = new ${name}Controller();
291
+ export default ${camel}Controller;
292
+ `;
293
+ },
294
+ schemas: (name, fileName) => `/**
295
+ * ${name} Schemas
296
+ * Generated by Arc CLI
297
+ */
298
+
299
+ import ${name} from './${fileName}.model.js';
300
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit';
301
+
302
+ /**
303
+ * CRUD Schemas with Field Rules
304
+ */
305
+ const crudSchemas = buildCrudSchemasFromModel(${name}, {
306
+ strictAdditionalProperties: true,
307
+ fieldRules: {
308
+ // systemManaged: framework stamps it — strip from body + required[]
309
+ // deletedAt: { systemManaged: true },
310
+ // immutable: cannot be updated after creation
311
+ // slug: { immutable: true },
312
+ // immutableAfterCreate: alias for immutable
313
+ // organizationId: { immutableAfterCreate: true },
314
+ // optional: removed from required[] (properties kept)
315
+ // description: { optional: true },
316
+ // nullable: widen JSON-Schema type to accept null (Zod .nullable() rescue)
317
+ // priceMode: { nullable: true },
318
+ // preserveForElevated: elevated admins keep the field on ingest (cross-tenant writes)
319
+ // organizationId: { systemManaged: true, preserveForElevated: true },
320
+ },
321
+ query: {
322
+ // Add your filterable fields here. createdAt is the default so the
323
+ // generated routes accept ?createdAt[gte]=2026-01-01 with no extra
324
+ // wiring. Add domain fields below as your model grows.
325
+ filterableFields: {
326
+ createdAt: 'date',
327
+ },
328
+ },
329
+ });
330
+
331
+ export default crudSchemas;
332
+ `,
333
+ resource: (name, fileName) => {
334
+ const camel = toCamelCase(name);
335
+ const useMongoKit = config.adapter === "mongokit" || !config.adapter;
336
+ const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
337
+ const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
338
+ const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
339
+ const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
340
+ return isMultiTenant ? `/**
341
+ * ${name} Resource
342
+ * Generated by Arc CLI
343
+ */
344
+
345
+ import { defineResource } from '@classytic/arc';
346
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter';
347
+ import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
348
+ import { multiTenantPreset } from '@classytic/arc/presets';
349
+ ${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
350
+ import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
351
+
352
+ const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
353
+ name: '${fileName}',
354
+ adapter: ${adapterCall},${queryParserConfig}
355
+
356
+ // Multi-tenant default: scope reads/writes by \`organizationId\`. For
357
+ // company-wide tables (lookup data, platform settings) set
358
+ // \`tenantField: false\` instead — otherwise queries silently return
359
+ // nothing because the column doesn't exist.
360
+ //
361
+ // Multi-level tenancy (org + branch + project, etc.) — replace with:
362
+ // multiTenantPreset({ tenantFields: [
363
+ // { field: 'organizationId', type: 'org' },
364
+ // { field: 'branchId', contextKey: 'branchId' },
365
+ // ] })
366
+ presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
367
+
368
+ // Tenant-cleanup strategy. Runs when an organization is deleted —
369
+ // wire \`cascadeDeleteForOrganization(arc.registry, { organizationId })\`
370
+ // into your auth lifecycle (Better Auth's afterDeleteOrganization,
371
+ // billing webhook, etc.) and every flagged resource is cleaned in one
372
+ // pass. Strategies: 'hard' (delete), 'soft' (deletedAt), 'anonymize'
373
+ // (clear PII, keep row — for legal-retained ledgers), 'skip' (no-op
374
+ // with mandatory reason). See arc/docs/compliance/tenant-cleanup.md.
375
+ onTenantDelete: { strategy: { type: 'hard' } },
376
+
377
+ // 2.16 — declarative CRUD allow-list (positive form). Uncomment to
378
+ // restrict the surface; a future arc CRUD op won't silently leak in
379
+ // because every op is opt-in. Mutually exclusive with \`disabledRoutes\`.
380
+ // crud: { list: true, get: true },
381
+
382
+ permissions: {
383
+ list: requireOrgMembership(),
384
+ get: requireOrgMembership(),
385
+ create: allOf(requireOrgMembership(), requireRoles(['admin'])),
386
+ update: allOf(requireOrgMembership(), requireRoles(['admin'])),
387
+ delete: allOf(requireOrgMembership(), requireRoles(['admin'])),
388
+ },
389
+
390
+ // 2.16 — typed state-transition actions. \`defineAction()\` captures the
391
+ // schema's literal type so \`data\` is inferred — no \`as MyShape\` casts.
392
+ // Set \`id: false\` for resource-root actions (propose / search / bulk).
393
+ //
394
+ // import { defineAction } from '@classytic/arc';
395
+ // import { z } from 'zod';
396
+ //
397
+ // actions: {
398
+ // approve: defineAction({
399
+ // schema: z.object({ reviewedBy: z.string() }),
400
+ // permissions: requireRoles(['admin']),
401
+ // handler: async (id, data) => { /* data.reviewedBy is typed */ },
402
+ // }),
403
+ // },
404
+ });
405
+
406
+ export default ${camel}Resource;
407
+ ` : `/**
408
+ * ${name} Resource
409
+ * Generated by Arc CLI
410
+ */
411
+
412
+ import { defineResource } from '@classytic/arc';
413
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter';
414
+ import { requireAuth, requireRoles } from '@classytic/arc/permissions';
415
+ ${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
416
+ import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
417
+
418
+ const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
419
+ name: '${fileName}',
420
+ adapter: ${adapterCall},${queryParserConfig}
421
+
422
+ // Single-tenant default: arc auto-infers \`tenantField: false\` when
423
+ // the model has no \`organizationId\` path (silent-zero-results
424
+ // footgun closed in 2.12). Set \`tenantField: '<field>'\` to opt INTO
425
+ // tenant scoping, or \`tenantField: false\` to make it explicit.
426
+ presets: ['softDelete'],
427
+
428
+ // 2.16 — declarative CRUD allow-list (positive form). Uncomment to
429
+ // restrict the surface; a future arc CRUD op won't silently leak in
430
+ // because every op is opt-in. Mutually exclusive with \`disabledRoutes\`.
431
+ // crud: { list: true, get: true },
432
+
433
+ permissions: {
434
+ list: requireAuth(),
435
+ get: requireAuth(),
436
+ create: requireRoles(['admin']),
437
+ update: requireRoles(['admin']),
438
+ delete: requireRoles(['admin']),
439
+ },
440
+
441
+ // 2.16 — typed state-transition actions. \`defineAction()\` captures the
442
+ // schema's literal type so \`data\` is inferred. Set \`id: false\` for
443
+ // resource-root actions (propose / search / bulk).
444
+ //
445
+ // import { defineAction } from '@classytic/arc';
446
+ // import { z } from 'zod';
447
+ //
448
+ // actions: {
449
+ // publish: defineAction({
450
+ // schema: z.object({ scheduledFor: z.string().datetime().optional() }),
451
+ // permissions: requireRoles(['admin']),
452
+ // handler: async (id, data) => { /* data.scheduledFor is typed */ },
453
+ // }),
454
+ // },
455
+ });
456
+
457
+ export default ${camel}Resource;
458
+ `;
459
+ },
460
+ mcp: (name, fileName) => {
461
+ const camel = toCamelCase(name);
462
+ return `/**
463
+ * ${name} MCP Tools
464
+ * Generated by Arc CLI
465
+ *
466
+ * Custom MCP tools for the ${name} domain.
467
+ * CRUD tools (list/get/create/update/delete) are auto-generated by mcpPlugin.
468
+ * Add domain-specific tools here (actions, analytics, workflows).
469
+ */
470
+
471
+ import { defineTool } from '@classytic/arc/mcp';
472
+ ${ts ? "import { z } from 'zod';\n" : "const { z } = require('zod');\n"}
473
+ // Example: domain-specific action tool
474
+ // export const activate${name}Tool = defineTool('activate_${fileName}', {
475
+ // description: 'Activate a ${name.toLowerCase()} by ID',
476
+ // input: { id: z.string().describe('${name} ID') },
477
+ // annotations: { destructiveHint: true, idempotentHint: true },
478
+ // handler: async ({ id }, ctx) => {
479
+ // // Your logic here
480
+ // return { content: [{ type: 'text', text: \`Activated ${name.toLowerCase()} \${id}\` }] };
481
+ // },
482
+ // });
483
+
484
+ // Example: read-only analytics tool
485
+ // export const ${camel}StatsTool = defineTool('${fileName}_stats', {
486
+ // description: 'Get ${name.toLowerCase()} statistics',
487
+ // input: { period: z.enum(['7d', '30d', '90d']).optional() },
488
+ // annotations: { readOnlyHint: true },
489
+ // handler: async ({ period }) => {
490
+ // // Your logic here
491
+ // return { content: [{ type: 'text', text: JSON.stringify({ total: 0, period }) }] };
492
+ // },
493
+ // });
494
+ `;
495
+ },
496
+ test: (name, fileName) => {
497
+ const camel = toCamelCase(name);
498
+ return `/**
499
+ * ${name} Tests
500
+ * Generated by Arc CLI
501
+ *
502
+ * Testing surface (arc 2.12+):
503
+ * - createTestApp turnkey Fastify + in-memory Mongo + auth + fixtures
504
+ * - expectArc fluent matchers — .ok / .failed / .unauthorized /
505
+ * .forbidden / .notFound / .conflict / .validationError /
506
+ * .paginated / .hidesField / .hasData / .hasStatus
507
+ * - ctx.auth unified TestAuthProvider — register a role, reuse .headers
508
+ *
509
+ * Wire shape (post-2.12): single-doc responses are flat (\`{_id, name, ...}\`),
510
+ * paginated responses are \`{ method: 'offset', data: [...], page, ... }\`.
511
+ * No \`success\` envelope — HTTP status discriminates success vs error;
512
+ * errors carry the canonical ErrorContract \`{ code, message, status }\`.
513
+ */
514
+
515
+ import { describe, it, beforeAll, afterAll, expect } from 'vitest';
516
+ import { createTestApp, expectArc } from '@classytic/arc/testing';
517
+ import type { TestAppContext } from '@classytic/arc/testing';
518
+ import ${camel}Resource from '../src/resources/${fileName}/${fileName}.resource.js';
519
+
520
+ describe('${name} Resource', () => {
521
+ let ctx${ts ? ": TestAppContext" : ""};
522
+
523
+ beforeAll(async () => {
524
+ ctx = await createTestApp({
525
+ resources: [${camel}Resource],
526
+ authMode: 'jwt',
527
+ connectMongoose: true,
528
+ });
529
+
530
+ ctx.auth${ts ? "!" : ""}.register('admin', {
531
+ user: { id: '1', roles: ['admin'] },
532
+ orgId: 'org-1',
533
+ });
534
+ });
535
+
536
+ afterAll(() => ctx.close());
537
+
538
+ describe('GET /${pluralize(fileName)}', () => {
539
+ it('returns a paginated list', async () => {
540
+ const res = await ctx.app.inject({
541
+ method: 'GET',
542
+ url: '/${pluralize(fileName)}',
543
+ headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
544
+ });
545
+ expectArc(res).ok().paginated();
546
+ });
547
+
548
+ it('rejects unauthenticated requests', async () => {
549
+ const res = await ctx.app.inject({ method: 'GET', url: '/${pluralize(fileName)}' });
550
+ expectArc(res).unauthorized();
551
+ });
552
+ });
553
+
554
+ describe('POST /${pluralize(fileName)}', () => {
555
+ it('creates a record (flat wire shape — no envelope)', async () => {
556
+ const res = await ctx.app.inject({
557
+ method: 'POST',
558
+ url: '/${pluralize(fileName)}',
559
+ headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
560
+ payload: { name: 'Example' },
561
+ });
562
+ expectArc(res).ok(201);
563
+ // Single-doc response is flat: \`body._id\` (not \`body.data._id\`).
564
+ const body = res.json()${ts ? " as { _id: string; name: string }" : ""};
565
+ expect(body._id).toBeDefined();
566
+ expect(body.name).toBe('Example');
567
+ });
568
+ });
569
+
570
+ // More patterns to extend:
571
+ // - expectArc(res).forbidden() / .notFound() / .validationError()
572
+ // - expectArc(res).hidesField('password') for field-level perms
573
+ // - ctx.fixtures.create('${fileName}', {...}) for seeded data
574
+ // - error wire shape: body.code === 'arc.not_found' (ErrorContract)
575
+ });
576
+ `;
577
+ }
578
+ };
579
+ }
580
+ //#endregion
581
+ //#region src/cli/commands/generate/index.ts
582
+ /**
583
+ * `arc generate` orchestrator.
584
+ *
585
+ * Routes the user-supplied type (`resource` / `controller` / `model` /
586
+ * `repository` / `schemas` / `mcp`, plus their short aliases) into the
587
+ * matching file-writer call. The orchestrator owns:
588
+ *
589
+ * - Name normalisation (kebab-case → PascalCase, kebab stays as the
590
+ * filename root).
591
+ * - The Better Auth-owned collection guard — refusing to generate a
592
+ * model named `user` / `organization` / etc. since BA writes those
593
+ * itself and a duplicate Mongoose registration silently collides.
594
+ * - Project-config detection (`.arcrc` + `tsconfig.json`) to pick the
595
+ * right template set + extension.
596
+ *
597
+ * Every step downstream is pure (templates) or local I/O (file-writer);
598
+ * no network, no DB, no global state.
599
+ */
600
+ async function generate(type, args) {
601
+ if (!type) throw new Error("Missing type argument\nUsage: arc generate <resource|controller|model|repository|schemas> <name>");
602
+ const [name, ...restArgs] = args;
603
+ if (!name) throw new Error("Missing name argument\nUsage: arc generate <type> <name>\nExample: arc generate resource product");
604
+ const capitalizedName = toPascalCase(name);
605
+ const lowerName = name.toLowerCase();
606
+ if ((type === "resource" || type === "r" || type === "model" || type === "m") && new Set([
607
+ "user",
608
+ "session",
609
+ "account",
610
+ "verification",
611
+ "organization",
612
+ "member",
613
+ "invitation",
614
+ "team",
615
+ "team-member",
616
+ "apikey"
617
+ ]).has(lowerName)) {
618
+ console.warn(`\n[arc generate] "${lowerName}" is a Better Auth-owned collection.\nBetter Auth's organization/admin/bearer plugins write this collection directly,\nso generating a parallel arc model would create a duplicate registration.\n\nRecommended pattern:\n import { createBetterAuthOverlay } from '@classytic/mongokit/better-auth';\n const adapter = await createBetterAuthOverlay({\n auth, mongoose, collection: '${lowerName}',\n });\n // ...then \`defineResource({ name: '${lowerName}', adapter, ... })\` reads\n // BA's collection through arc with full pagination/filters/permissions.\n\nAborting. Re-run with a different name if you need a separate domain model.\n`);
619
+ return;
620
+ }
621
+ const projectConfig = readProjectConfig();
622
+ const ts = projectConfig.typescript ?? isTypeScriptProject();
623
+ const ext = ts ? "ts" : "js";
624
+ const templates = getTemplates(ts, projectConfig);
625
+ const resourcePath = join(process.cwd(), "src", "resources", lowerName);
626
+ switch (type) {
627
+ case "resource":
628
+ case "r":
629
+ await generateResource(capitalizedName, lowerName, resourcePath, templates, ext, projectConfig.mcp === true || restArgs.includes("--mcp"));
630
+ break;
631
+ case "controller":
632
+ case "c":
633
+ await generateFile(capitalizedName, lowerName, resourcePath, "controller", templates.controller, ext);
634
+ break;
635
+ case "model":
636
+ case "m":
637
+ await generateFile(capitalizedName, lowerName, resourcePath, "model", templates.model, ext);
638
+ break;
639
+ case "repository":
640
+ case "repo":
641
+ await generateFile(capitalizedName, lowerName, resourcePath, "repository", templates.repository, ext);
642
+ break;
643
+ case "schemas":
644
+ case "s":
645
+ await generateFile(capitalizedName, lowerName, resourcePath, "schemas", templates.schemas, ext);
646
+ break;
647
+ case "mcp":
648
+ await generateFile(capitalizedName, lowerName, resourcePath, "mcp", templates.mcp, ext);
649
+ break;
650
+ default: throw new Error(`Unknown type: ${type}\nAvailable types: resource, controller, model, repository, schemas, mcp`);
651
+ }
652
+ }
653
+ //#endregion
654
+ export { generate as t };
@@ -1,2 +1,2 @@
1
- import { An as afterCreate, Cn as HookContext, Dn as HookRegistration, En as HookPhase, Fn as beforeUpdate, In as createHookSystem, Ln as defineHook, Mn as afterUpdate, Nn as beforeCreate, On as HookSystem, Pn as beforeDelete, Sn as DefineHookOptions, Tn as HookOperation, jn as afterDelete, kn as HookSystemOptions, wn as HookHandler } from "../index-BswOSJCE.mjs";
1
+ import { An as HookSystem, Dn as HookOperation, En as HookHandler, Fn as beforeCreate, In as beforeDelete, Ln as beforeUpdate, Mn as afterCreate, Nn as afterDelete, On as HookPhase, Pn as afterUpdate, Rn as createHookSystem, Tn as HookContext, jn as HookSystemOptions, kn as HookRegistration, wn as DefineHookOptions, zn as defineHook } from "../index-CkW0flkU.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,2 +1,2 @@
1
- import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-Iiebom92.mjs";
1
+ import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-Cmf7-Etp.mjs";
2
2
  export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,5 +1,5 @@
1
- import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-DfLGcus7.mjs";
2
- import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-DiMkdHEl.mjs";
1
+ import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-NwJ_qPlY.mjs";
2
+ import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-Cyzrz6SX.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { RepositoryLike } from "@classytic/repo-core/adapter";
5
5
 
@@ -1,4 +1,4 @@
1
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-BkIN9-vu.mjs";
1
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-B0sunfZZ.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
4
  import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";