@happyvertical/smrt-core 0.44.0 → 0.45.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 (104) hide show
  1. package/AGENTS.md +8 -8
  2. package/agents/change-feed.md +3 -2
  3. package/agents/generators.md +34 -0
  4. package/agents/revision-guard.md +66 -0
  5. package/dist/browser.js +2 -1
  6. package/dist/cascade.d.ts +5 -9
  7. package/dist/cascade.d.ts.map +1 -1
  8. package/dist/cascade.js +65 -30
  9. package/dist/cascade.js.map +1 -1
  10. package/dist/change-feed.d.ts +60 -1
  11. package/dist/change-feed.d.ts.map +1 -1
  12. package/dist/change-feed.js +464 -27
  13. package/dist/change-feed.js.map +1 -1
  14. package/dist/change-signals.d.ts.map +1 -1
  15. package/dist/change-signals.js +13 -11
  16. package/dist/change-signals.js.map +1 -1
  17. package/dist/embedded-write-queue.d.ts +8 -0
  18. package/dist/embedded-write-queue.d.ts.map +1 -1
  19. package/dist/embedded-write-queue.js +12 -2
  20. package/dist/embedded-write-queue.js.map +1 -1
  21. package/dist/generators/cli.d.ts.map +1 -1
  22. package/dist/generators/cli.js +10 -18
  23. package/dist/generators/cli.js.map +1 -1
  24. package/dist/generators/custom-action.d.ts +216 -0
  25. package/dist/generators/custom-action.d.ts.map +1 -1
  26. package/dist/generators/custom-action.js +256 -1
  27. package/dist/generators/custom-action.js.map +1 -1
  28. package/dist/generators/index.d.ts +2 -1
  29. package/dist/generators/index.d.ts.map +1 -1
  30. package/dist/generators/index.js +3 -2
  31. package/dist/generators/mcp.d.ts.map +1 -1
  32. package/dist/generators/mcp.js +14 -39
  33. package/dist/generators/mcp.js.map +1 -1
  34. package/dist/generators/preflight-route.d.ts +151 -0
  35. package/dist/generators/preflight-route.d.ts.map +1 -0
  36. package/dist/generators/preflight-route.js +194 -0
  37. package/dist/generators/preflight-route.js.map +1 -0
  38. package/dist/generators/rest.d.ts +12 -0
  39. package/dist/generators/rest.d.ts.map +1 -1
  40. package/dist/generators/rest.js +14 -15
  41. package/dist/generators/rest.js.map +1 -1
  42. package/dist/generators/tool-schema.d.ts.map +1 -1
  43. package/dist/generators/tool-schema.js +2 -8
  44. package/dist/generators/tool-schema.js.map +1 -1
  45. package/dist/generators.js +3 -2
  46. package/dist/index.d.ts +3 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +7 -4
  49. package/dist/knowledge.d.ts.map +1 -1
  50. package/dist/knowledge.js +283 -35
  51. package/dist/knowledge.js.map +1 -1
  52. package/dist/manifest/static-manifest.d.ts.map +1 -1
  53. package/dist/manifest/static-manifest.js +6 -2
  54. package/dist/manifest/static-manifest.js.map +1 -1
  55. package/dist/manifest/store.js +1 -1
  56. package/dist/manifest/store.js.map +1 -1
  57. package/dist/manifest.json +8 -2
  58. package/dist/object.d.ts +31 -1
  59. package/dist/object.d.ts.map +1 -1
  60. package/dist/object.js +57 -9
  61. package/dist/object.js.map +1 -1
  62. package/dist/registry/framework-base-classes.d.ts +10 -0
  63. package/dist/registry/framework-base-classes.d.ts.map +1 -0
  64. package/dist/registry/framework-base-classes.js +92 -0
  65. package/dist/registry/framework-base-classes.js.map +1 -0
  66. package/dist/registry/schema-builder.d.ts.map +1 -1
  67. package/dist/registry/schema-builder.js +2 -0
  68. package/dist/registry/schema-builder.js.map +1 -1
  69. package/dist/registry/types.d.ts +27 -6
  70. package/dist/registry/types.d.ts.map +1 -1
  71. package/dist/registry.d.ts +1 -0
  72. package/dist/registry.d.ts.map +1 -1
  73. package/dist/registry.js +2 -1
  74. package/dist/registry.js.map +1 -1
  75. package/dist/revision-guard.d.ts +84 -0
  76. package/dist/revision-guard.d.ts.map +1 -0
  77. package/dist/revision-guard.js +120 -0
  78. package/dist/revision-guard.js.map +1 -0
  79. package/dist/scanner/manifest-generator.d.ts +46 -16
  80. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  81. package/dist/scanner/manifest-generator.js +198 -45
  82. package/dist/scanner/manifest-generator.js.map +1 -1
  83. package/dist/smrt-knowledge.json +18 -9
  84. package/dist/system/bootstrap.d.ts.map +1 -1
  85. package/dist/system/bootstrap.js +2 -1
  86. package/dist/system/bootstrap.js.map +1 -1
  87. package/dist/system/schema.d.ts +75 -2
  88. package/dist/system/schema.d.ts.map +1 -1
  89. package/dist/system/schema.js +326 -4
  90. package/dist/system/schema.js.map +1 -1
  91. package/dist/vite-plugin/index.d.ts.map +1 -1
  92. package/dist/vite-plugin/index.js +48 -23
  93. package/dist/vite-plugin/index.js.map +1 -1
  94. package/dist/vite-plugin/sveltekit-generator.d.ts +33 -7
  95. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  96. package/dist/vite-plugin/sveltekit-generator.js +88 -17
  97. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  98. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  99. package/dist/vite-plugin/sync-apply-route.js +2 -0
  100. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  101. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  102. package/dist/vite-plugin/web-collections.js +28 -5
  103. package/dist/vite-plugin/web-collections.js.map +1 -1
  104. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !/^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n return typeof db?.url === 'string' && db.url.length > 0 ? db.url : db;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, () =>\n transaction((bound) => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n }),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;;;;;;;AAY5E,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,sBAAsB,KAAK,IAAI,OAAO,EAAE;AAClD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;AAEA,SAAS,SAAS,IAAwC;CACxD,OAAO,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,SAAS,IAAI,GAAG,MAAM;AACrE;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACY;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,iBAChC,aAAa,UAAU;EACrB,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;EACtC,MAAM,SAAS,gBAAgB,SAAS;EACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;CACF,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n return typeof db?.url === 'string' && db.url.length > 0 ? db.url : db;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, () =>\n transaction((bound) => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n }),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;;;;;;;AAY5E,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;AAEA,SAAS,SAAS,IAAwC;CACxD,OAAO,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,SAAS,IAAI,GAAG,MAAM;AACrE;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACY;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,iBAChC,aAAa,UAAU;EACrB,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;EACtC,MAAM,SAAS,gBAAgB,SAAS;EACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;CACF,CAAC,CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/generators/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AA8BrD;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,yCAAyC;IACzC,EAAE,CAAC,EAAE,eAAe,GAAG,QAAQ,CAAC;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,sDAAsD;IACtD,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAoCD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;gBAEhB,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;IAU5D,wBAAwB;IACxB,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,mBAAmB;IACnB,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED;;;;OAIG;IACH,eAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC;IAMpD;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CxC;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAkDjB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAWpB;;;;OAIG;YACW,oBAAoB;IA+ClC;;;;;;OAMG;YACW,aAAa;IAS3B;;OAEG;YACW,aAAa;IAwF3B;;;;OAIG;YACW,mBAAmB;IAuFjC,OAAO,CAAC,qBAAqB;IAM7B;;;;OAIG;YACW,WAAW;IAsBzB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAuBrB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA+C3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,oBAAoB;IAI5B,qEAAqE;IAC/D,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAmCzB,QAAQ;IAoBtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IAIlB,2EAA2E;IAC3E,OAAO,CAAC,WAAW;CAiBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;gBAGnD,MAAM,EAAE;;EAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,GAAE,SAAc,EACtB,OAAO,GAAE,UAAe,UAroBE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAyoBrD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/generators/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAmCrD;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,yCAAyC;IACzC,EAAE,CAAC,EAAE,eAAe,GAAG,QAAQ,CAAC;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,sDAAsD;IACtD,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAiCD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;gBAEhB,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;IAU5D,wBAAwB;IACxB,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,mBAAmB;IACnB,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED;;;;OAIG;IACH,eAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC;IAMpD;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CxC;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAkDjB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAWpB;;;;OAIG;YACW,oBAAoB;IA2DlC;;;;;;OAMG;YACW,aAAa;IAS3B;;OAEG;YACW,aAAa;IAwF3B;;;;OAIG;YACW,mBAAmB;IAuFjC,OAAO,CAAC,qBAAqB;IAM7B;;;;OAIG;YACW,WAAW;IAsBzB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAuBrB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA+C3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,oBAAoB;IAI5B,qEAAqE;IAC/D,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YA0CzB,QAAQ;IAoBtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IAIlB,2EAA2E;IAC3E,OAAO,CAAC,WAAW;CAiBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;gBAGnD,MAAM,EAAE;;EAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,GAAE,SAAc,EACtB,OAAO,GAAE,UAAe,UAxpBE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CA4pBrD"}
@@ -1,16 +1,9 @@
1
- import { buildCustomActionInvocationArgs, customActionParameterInputName, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
1
+ import { CRUD_OPERATIONS, buildCustomActionInvocationArgs, customActionParameterInputName, isCrudOperation, isFrameworkLifecycleMethod, normalizeCustomActionFailure, resolveCustomActionMetadata, resolveCustomActionNames } from "./custom-action.js";
2
+ import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
2
3
  import { ObjectRegistry } from "../registry.js";
3
4
  import { SmrtCollection } from "../collection.js";
4
5
  import { runWithTenantGate } from "./tenant-gate.js";
5
6
  //#region src/generators/cli.ts
6
- /** Standard CRUD verbs handled directly by the generator. */
7
- var CRUD_OPERATIONS = [
8
- "list",
9
- "get",
10
- "create",
11
- "update",
12
- "delete"
13
- ];
14
7
  /**
15
8
  * Flags consumed by the CLI itself — never treated as create/update field
16
9
  * values or as custom-action options.
@@ -156,10 +149,11 @@ var CLIGenerator = class {
156
149
  if (cliConfig === false) throw new Error(`CLI is disabled for ${objectName}`);
157
150
  const included = typeof cliConfig === "object" ? cliConfig?.include : void 0;
158
151
  if ((typeof cliConfig === "object" && cliConfig?.exclude ? cliConfig.exclude : []).includes(action)) throw new Error(`Command '${action}' is excluded for ${objectName}`);
159
- if (CRUD_OPERATIONS.includes(action)) {
152
+ if (isCrudOperation(action)) {
160
153
  if (included && !included.includes(action)) throw new Error(`Command '${action}' is not enabled for ${objectName}`);
161
154
  return;
162
155
  }
156
+ if (isFrameworkLifecycleMethod(action)) throw new Error(`Command '${action}' is a framework lifecycle method, not a custom action, on ${objectName}`);
163
157
  const methodDef = (await ObjectRegistry.getAllMethods(objectName)).get(action);
164
158
  if (!methodDef) throw new Error(`Unknown command '${action}' for ${objectName}`);
165
159
  if (!methodDef.isPublic) throw new Error(`Command '${action}' is not public on ${objectName}`);
@@ -389,6 +383,7 @@ var CLIGenerator = class {
389
383
  const commands = [];
390
384
  for (const [key, classInfo] of ObjectRegistry.getAllClasses()) {
391
385
  const objectName = classInfo.name || key;
386
+ if (isFrameworkBaseClass(objectName, classInfo.packageName)) continue;
392
387
  const cliConfig = ObjectRegistry.getConfig(objectName)?.cli;
393
388
  if (cliConfig === false) continue;
394
389
  const lower = objectName.toLowerCase();
@@ -396,14 +391,11 @@ var CLIGenerator = class {
396
391
  const excluded = typeof cliConfig === "object" && cliConfig?.exclude ? cliConfig.exclude : [];
397
392
  const expose = (cmd) => !excluded.includes(cmd) && (!included || included.includes(cmd));
398
393
  for (const verb of CRUD_OPERATIONS) if (expose(verb)) commands.push(`${lower}:${verb}`);
399
- const methods = await ObjectRegistry.getAllMethods(objectName);
400
- for (const [methodName, methodDef] of methods) {
401
- if (CRUD_OPERATIONS.includes(methodName)) continue;
402
- if (!methodDef.isPublic) continue;
403
- if (excluded.includes(methodName)) continue;
404
- if (included !== void 0 && !included.includes(methodName)) continue;
405
- commands.push(`${lower}:${methodName}`);
406
- }
394
+ const customActions = resolveCustomActionNames(await ObjectRegistry.getAllMethods(objectName), {
395
+ include: included,
396
+ exclude: excluded
397
+ }, CRUD_OPERATIONS);
398
+ for (const methodName of customActions) commands.push(`${lower}:${methodName}`);
407
399
  }
408
400
  return commands.sort();
409
401
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../../src/generators/cli.ts"],"sourcesContent":["/**\n * CLI command generator for smrt objects\n *\n * Exposes registered `@smrt()` objects as a runnable admin CLI: each object gets\n * `list`/`get`/`create`/`update`/`delete` commands plus its public custom\n * methods, dispatched as `objectname:action`.\n *\n * Security parity with the REST/MCP generators (#1540, #1547, #1554, #1556):\n * - **Mass-assignment guard** — create/update bodies are filtered through the\n * `@smrt({ api: { writable: [...] } })` allowlist, dropping server-managed and\n * `@field({ readonly })` fields ({@link CLIGenerator.applyWritablePolicy}).\n * - **Exhaustive include** — an `include` list is the COMPLETE allowlist for the\n * surface; custom methods are gated on `isPublic`.\n * - **Sensitive redaction** — command *output* is serialized through\n * {@link CLIGenerator.toPublicData} so `@field({ sensitive })` values never\n * print (input bodies are guarded by the writable allowlist above).\n * - **Fail-closed tenant context** — tenant-scoped reads/writes run inside the\n * tenancy gate; without `--tenant <id>` / `--all-tenants` (and with tenancy\n * enabled) the command throws rather than ranging across all tenants.\n */\n\nimport type { AIClient, AIClientOptions } from '@happyvertical/ai';\nimport { SmrtCollection } from '../collection';\nimport type { DatabaseConfig } from '../database.js';\nimport type { PublicJsonOptions, SmrtObject } from '../object';\nimport { ObjectRegistry } from '../registry';\nimport type { RegisteredClass } from '../registry/types.js';\nimport {\n buildCustomActionInvocationArgs,\n customActionParameterInputName,\n normalizeCustomActionFailure,\n resolveCustomActionMetadata,\n} from './custom-action.js';\nimport { runWithTenantGate } from './tenant-gate.js';\n\n/**\n * Public-data view exposed by SMRT objects: `toPublicJSON()` strips\n * `@field({ sensitive })` values before serialization (#1540).\n */\ninterface PublicSerializable {\n toPublicJSON(options?: PublicJsonOptions): unknown;\n}\n\n/**\n * View of an instance/collection for dispatching custom methods by name. Each\n * key is potentially a callable taking the parsed CLI options; callers narrow\n * with `typeof === 'function'` before invoking.\n */\ntype DynamicallyCallable = Record<\n string,\n ((...args: unknown[]) => unknown) | unknown\n>;\n\n/**\n * Configuration for a generated CLI.\n */\nexport interface CLIConfig {\n /** CLI program name (used in help output). */\n name?: string;\n /** CLI version. */\n version?: string;\n /** CLI description. */\n description?: string;\n}\n\n/**\n * Per-invocation context for a generated CLI.\n */\nexport interface CLIContext {\n /** Database handle passed to collections. */\n db?: DatabaseConfig;\n /** AI provider passed to collections. */\n ai?: AIClientOptions | AIClient;\n /** Authenticated operator, when the host establishes one. */\n user?: {\n id: string;\n roles?: string[];\n };\n /** Resolved permission slugs held by the operator. */\n permissions?: Iterable<string>;\n /**\n * Default tenant for tenant-scoped commands when no `--tenant` flag is given.\n * Hosts that authenticate an operator may set this from the principal.\n */\n tenantId?: string;\n /**\n * Default cross-tenant opt-in when no `--all-tenants` flag is given. Hosts\n * may set this for trusted operator/admin shells.\n */\n allowCrossTenant?: boolean;\n}\n\n/** Standard CRUD verbs handled directly by the generator. */\nconst CRUD_OPERATIONS = ['list', 'get', 'create', 'update', 'delete'];\n\n/**\n * Flags consumed by the CLI itself — never treated as create/update field\n * values or as custom-action options.\n */\nconst RESERVED_FLAGS = new Set([\n 'tenant',\n 'all-tenants',\n 'from-file',\n 'limit',\n 'offset',\n 'order-by',\n 'orderBy',\n 'where',\n 'format',\n 'json',\n 'help',\n 'id',\n]);\n\n/** Parsed CLI invocation. */\ninterface ParsedInvocation {\n /** Object/collection segment (lowercased), e.g. `product`. */\n objectName: string;\n /** Action segment, e.g. `list` or a custom method name. */\n action: string;\n /** First positional argument after the command (typically an id). */\n positional?: string;\n /** Parsed flags. String for `--k v`/`--k=v`, `true` for bare `--flag`. */\n flags: Record<string, string | boolean>;\n}\n\n/**\n * Generate and run an admin CLI for the registered `@smrt()` objects.\n */\nexport class CLIGenerator {\n private config: CLIConfig;\n private context: CLIContext;\n\n constructor(config: CLIConfig = {}, context: CLIContext = {}) {\n this.config = {\n name: 'smrt-cli',\n version: '1.0.0',\n description: 'Auto-generated CLI from smrt objects',\n ...config,\n };\n this.context = context;\n }\n\n /** CLI program name. */\n get name(): string | undefined {\n return this.config.name;\n }\n\n /** CLI version. */\n get version(): string | undefined {\n return this.config.version;\n }\n\n /**\n * Return the argv handler invoked by the generated `setupCLI()` wrapper.\n *\n * @returns An async function accepting the post-`node script` argv slice.\n */\n generateHandler(): (args: string[]) => Promise<void> {\n return async (args: string[]) => {\n await this.run(args ?? []);\n };\n }\n\n /**\n * Parse and execute a single CLI invocation.\n *\n * @param args - argv slice (command + flags).\n */\n async run(args: string[]): Promise<void> {\n if (args.length === 0 || args[0] === '--help' || args[0] === 'help') {\n console.log(await this.helpText());\n return;\n }\n\n const parsed = this.parseArgs(args);\n if (!parsed) {\n console.log(await this.helpText());\n return;\n }\n\n const classInfo = this.resolveClass(parsed.objectName);\n if (!classInfo) {\n throw new Error(`Object type '${parsed.objectName}' not found`);\n }\n const objectName: string = classInfo.name || parsed.objectName;\n\n await this.assertCommandExposed(objectName, parsed.action);\n\n const tenantId =\n typeof parsed.flags.tenant === 'string'\n ? parsed.flags.tenant\n : this.context.tenantId;\n const allowCrossTenant =\n parsed.flags['all-tenants'] === true ||\n this.context.allowCrossTenant === true;\n\n // Tenant-scoping is resolved authoritatively inside tenancy (by class name),\n // matching the interceptor and covering both @TenantScoped and\n // @smrt({ tenantScoped }) registrations (#1554).\n const result = await runWithTenantGate(\n { className: objectName, tenantId, allowCrossTenant, surface: 'CLI' },\n async () => {\n const collection = await this.getCollection(objectName);\n return this.executeAction(collection, objectName, parsed);\n },\n );\n\n if (result !== undefined) {\n console.log(JSON.stringify(this.toPublicData(result), null, 2));\n }\n }\n\n /**\n * Parse argv into a command + flags. Supports `name:action` and\n * `name action` command forms, `--key value`, `--key=value`, and bare\n * `--flag` booleans.\n */\n private parseArgs(args: string[]): ParsedInvocation | null {\n const [command, ...rest] = args;\n if (!command || command.startsWith('-')) return null;\n\n let objectName: string;\n let action: string;\n let tail = rest;\n\n if (command.includes(':')) {\n const [obj, act] = command.split(':');\n objectName = obj.toLowerCase();\n action = act;\n } else if (rest.length > 0 && !rest[0].startsWith('-')) {\n objectName = command.toLowerCase();\n action = rest[0];\n tail = rest.slice(1);\n } else {\n // A bare object name with no action is not a runnable command.\n return null;\n }\n\n if (!objectName || !action) return null;\n\n const flags: Record<string, string | boolean> = {};\n let positional: string | undefined;\n\n for (let i = 0; i < tail.length; i++) {\n const token = tail[i];\n if (token.startsWith('--')) {\n const body = token.slice(2);\n const eq = body.indexOf('=');\n if (eq !== -1) {\n flags[body.slice(0, eq)] = body.slice(eq + 1);\n } else {\n const next = tail[i + 1];\n if (next !== undefined && !next.startsWith('--')) {\n flags[body] = next;\n i++;\n } else {\n flags[body] = true;\n }\n }\n } else if (positional === undefined) {\n positional = token;\n }\n }\n\n return { objectName, action, positional, flags };\n }\n\n /**\n * Resolve a registered class by simple name (case-insensitive), mirroring the\n * MCP generator's lookup.\n */\n private resolveClass(objectName: string): RegisteredClass | null {\n const registeredClasses = ObjectRegistry.getAllClasses();\n for (const [key, info] of registeredClasses) {\n const simpleName = info.name || key;\n if (simpleName.toLowerCase() === objectName.toLowerCase()) {\n return info;\n }\n }\n return null;\n }\n\n /**\n * Throw if `action` is not exposed for `objectName` under its `@smrt({ cli })`\n * config. `cli: false` disables the object entirely; an `include` list is the\n * complete allowlist; custom methods must be `isPublic`.\n */\n private async assertCommandExposed(\n objectName: string,\n action: string,\n ): Promise<void> {\n const config = ObjectRegistry.getConfig(objectName);\n const cliConfig = config?.cli;\n\n if (cliConfig === false) {\n throw new Error(`CLI is disabled for ${objectName}`);\n }\n\n const included: string[] | undefined =\n typeof cliConfig === 'object' ? cliConfig?.include : undefined;\n const excluded: string[] =\n typeof cliConfig === 'object' && cliConfig?.exclude\n ? cliConfig.exclude\n : [];\n\n if (excluded.includes(action)) {\n throw new Error(`Command '${action}' is excluded for ${objectName}`);\n }\n\n const isCrud = CRUD_OPERATIONS.includes(action);\n\n if (isCrud) {\n // An include list, when present, is the complete allowlist for CRUD too.\n if (included && !included.includes(action)) {\n throw new Error(`Command '${action}' is not enabled for ${objectName}`);\n }\n return;\n }\n\n // Custom method: must be public, and — when an include list is present — it\n // is the COMPLETE allowlist (parity with MCP exhaustive-include, #1547).\n const methods = await ObjectRegistry.getAllMethods(objectName);\n const methodDef = methods.get(action);\n if (!methodDef) {\n throw new Error(`Unknown command '${action}' for ${objectName}`);\n }\n if (!methodDef.isPublic) {\n throw new Error(`Command '${action}' is not public on ${objectName}`);\n }\n if (included !== undefined && !included.includes(action)) {\n throw new Error(`Command '${action}' is not enabled for ${objectName}`);\n }\n }\n\n /**\n * Get the collection for an object via the registry factory. Uses\n * `ObjectRegistry.getCollection`, which caches, initializes, and — for plain\n * `@smrt()` models without a hand-written collection class — auto-creates the\n * default collection. (A bare `collectionConstructor` check would wrongly\n * throw for those models even though CRUD is advertised.)\n */\n private async getCollection(\n objectName: string,\n ): Promise<SmrtCollection<SmrtObject>> {\n return ObjectRegistry.getCollection(objectName, {\n ai: this.context.ai,\n db: this.context.db,\n });\n }\n\n /**\n * Execute a parsed command against a collection.\n */\n private async executeAction(\n collection: SmrtCollection<SmrtObject>,\n objectName: string,\n parsed: ParsedInvocation,\n ): Promise<unknown> {\n const { action, positional, flags } = parsed;\n\n switch (action) {\n case 'list': {\n const listOptions: {\n limit: number;\n offset: number;\n orderBy?: string;\n where?: Record<string, unknown>;\n } = {\n limit: Math.min(this.toNumber(flags.limit, 50), 1000),\n offset: this.toNumber(flags.offset, 0),\n };\n const orderBy = flags['order-by'] ?? flags.orderBy;\n if (typeof orderBy === 'string') listOptions.orderBy = orderBy;\n if (typeof flags.where === 'string') {\n listOptions.where = JSON.parse(flags.where);\n }\n return collection.list(listOptions);\n }\n\n case 'get': {\n const id = positional ?? this.flagString(flags.id);\n if (!id) throw new Error('An id is required for get');\n const item = await collection.get(id);\n if (!item) throw new Error(`${objectName} not found`);\n return item;\n }\n\n case 'create': {\n const data = this.applyWritablePolicy(\n objectName,\n await this.readPayload(flags),\n );\n if (this.context.user) {\n data.created_by = this.context.user.id;\n data.owner_id = this.context.user.id;\n }\n const created = await collection.create(data);\n await created.save();\n return created;\n }\n\n case 'update': {\n const id = positional ?? this.flagString(flags.id);\n if (!id) throw new Error('An id is required for update');\n const existing = await collection.get(id);\n if (!existing) throw new Error(`${objectName} not found`);\n const data = this.applyWritablePolicy(\n objectName,\n await this.readPayload(flags),\n );\n Object.assign(existing, data);\n if (this.context.user) {\n // Stamp the server-managed audit column on the instance; this field\n // is not part of the public SmrtObject surface.\n (existing as unknown as Record<string, unknown>).updated_by =\n this.context.user.id;\n }\n await existing.save();\n return existing;\n }\n\n case 'delete': {\n const id = positional ?? this.flagString(flags.id);\n if (!id) throw new Error('An id is required for delete');\n const existing = await collection.get(id);\n if (!existing) throw new Error(`${objectName} not found`);\n await existing.delete();\n return { success: true, id, message: `${objectName} deleted` };\n }\n\n default:\n return this.executeCustomAction(\n objectName,\n collection,\n action,\n positional,\n flags,\n );\n }\n }\n\n /**\n * Execute a custom method against its manifest-declared item/collection\n * target. The legacy collection fallback remains only for unavailable\n * metadata, where target scope cannot be established safely.\n */\n private async executeCustomAction(\n objectName: string,\n collection: SmrtCollection<SmrtObject>,\n action: string,\n positional: string | undefined,\n flags: Record<string, string | boolean>,\n ): Promise<unknown> {\n const id = positional ?? this.flagString(flags.id);\n const methodDef = (await ObjectRegistry.getAllMethods(objectName)).get(\n action,\n );\n const metadata = resolveCustomActionMetadata({\n actionName: action,\n method: methodDef,\n apiConfig: ObjectRegistry.getConfig(objectName).api,\n ...(this.hasCollectionReceiver(ObjectRegistry.getClass(objectName))\n ? { defaultScope: 'collection' as const }\n : {}),\n });\n const args = this.customOptions(flags, metadata);\n const invocationArgs =\n metadata.parameters?.length === 1 &&\n metadata.parameters[0]?.name === 'options'\n ? Object.keys(args).length > 0\n ? { options: args }\n : {}\n : args;\n const methodArgs = buildCustomActionInvocationArgs(\n metadata,\n invocationArgs,\n );\n\n if (methodDef && metadata.idRequired && !id) {\n throw new Error(`An id is required for custom action '${action}'`);\n }\n if (methodDef && !metadata.idRequired && id) {\n throw new Error(\n `Custom action '${action}' is collection-scoped and does not accept an id`,\n );\n }\n\n if (id) {\n const object = await collection.get(id);\n if (!object) throw new Error('Object not found');\n // Custom methods are dispatched dynamically by name; index through a\n // callable-keyed view of the instance, narrowing before invoking.\n const candidate = (object as unknown as DynamicallyCallable)[action];\n if (typeof candidate !== 'function') {\n throw new Error(`Method '${action}' not found on object instance`);\n }\n const result = await candidate.call(object, ...methodArgs);\n const failure = normalizeCustomActionFailure(result);\n if (failure) throw new Error(JSON.stringify({ error: failure }));\n return result;\n }\n\n if (methodDef?.isStatic) {\n const classInfo = ObjectRegistry.getClass(objectName);\n const candidate = (\n classInfo?.constructor as unknown as DynamicallyCallable | undefined\n )?.[action];\n if (typeof candidate !== 'function') {\n throw new Error(`Static method '${action}' not found on ${objectName}`);\n }\n const result = await candidate.call(\n classInfo?.constructor,\n ...methodArgs,\n );\n const failure = normalizeCustomActionFailure(result);\n if (failure) throw new Error(JSON.stringify({ error: failure }));\n return result;\n }\n\n const collectionCandidate = (collection as unknown as DynamicallyCallable)[\n action\n ];\n if (typeof collectionCandidate === 'function') {\n const result = await collectionCandidate.call(collection, ...methodArgs);\n const failure = normalizeCustomActionFailure(result);\n if (failure) throw new Error(JSON.stringify({ error: failure }));\n return result;\n }\n throw new Error(\n `Method '${action}' not found on collection. Provide an id for object-specific actions.`,\n );\n }\n\n private hasCollectionReceiver(classInfo?: RegisteredClass): boolean {\n return (\n !!classInfo && classInfo.constructor.prototype instanceof SmrtCollection\n );\n }\n\n /**\n * Resolve the create/update payload: `--from-file <path>` (JSON) takes\n * precedence; otherwise non-reserved `--field value` flags form the body.\n * Reading from a file also keeps secrets off the process argv (#1556).\n */\n private async readPayload(\n flags: Record<string, string | boolean>,\n ): Promise<Record<string, unknown>> {\n const fromFile = flags['from-file'];\n if (typeof fromFile === 'string' && fromFile) {\n const { readFile } = await import('node:fs/promises');\n const content = await readFile(fromFile, 'utf-8');\n const parsed = JSON.parse(content);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('--from-file must contain a JSON object');\n }\n return parsed as Record<string, unknown>;\n }\n\n const data: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(flags)) {\n if (RESERVED_FLAGS.has(key)) continue;\n data[key] = this.coerceValue(value);\n }\n return data;\n }\n\n /**\n * Collect custom-action flags. Typed action parameters may deliberately use\n * standard CLI names such as `limit`, `offset`, `where`, or `format`; retain\n * those rather than silently dropping them as CRUD flags. Item actions keep\n * `--id` for their receiver and accept an action parameter named `id` as\n * `--action-id` to preserve separate namespaces.\n */\n private customOptions(\n flags: Record<string, string | boolean>,\n metadata?: { parameters?: Array<{ name: string }>; idRequired: boolean },\n ): Record<string, unknown> {\n const declaredInputs = new Set(\n (metadata?.parameters ?? []).map((parameter) =>\n customActionParameterInputName(\n metadata ?? { idRequired: false },\n parameter.name,\n ),\n ),\n );\n const options: Record<string, unknown> = {};\n for (const [rawKey, value] of Object.entries(flags)) {\n const key = rawKey === 'action-id' ? 'actionId' : rawKey;\n if (rawKey === 'id') continue;\n if (RESERVED_FLAGS.has(rawKey) && !declaredInputs.has(key)) continue;\n if (rawKey === 'action-id' && !declaredInputs.has(key)) continue;\n options[key] = this.coerceValue(value);\n }\n return options;\n }\n\n /**\n * Mass-assignment guard (#1540/#1556): strip framework/server-managed and\n * `@field({ readonly })` fields, and — when an `@smrt({ api: { writable } })`\n * allowlist is set — intersect with it. Identical policy to the REST/MCP\n * generators so a CLI create/update cannot set fields the API forbids.\n */\n private applyWritablePolicy(\n objectName: string | undefined,\n data: unknown,\n ): Record<string, unknown> {\n if (!data || typeof data !== 'object') return {};\n\n const serverManaged = new Set([\n 'id',\n 'tenantId',\n 'tenant_id',\n 'createdAt',\n 'created_at',\n 'updatedAt',\n 'updated_at',\n ]);\n\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n\n if (objectName) {\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (\n apiConfig &&\n typeof apiConfig === 'object' &&\n Array.isArray((apiConfig as { writable?: unknown }).writable)\n ) {\n writable = (apiConfig as { writable: string[] }).writable;\n }\n\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && (def.readonly === true || def._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n }\n\n /**\n * Serialize command output, excluding `@field({ sensitive })` fields (#1540).\n * Recurses arrays/plain objects so a SmrtObject nested in a custom-action\n * result is stripped too; a cycle guard prevents infinite loops.\n */\n private toPublicData(\n value: unknown,\n seen: WeakSet<object> = new WeakSet(),\n options: PublicJsonOptions = this.getPublicJsonOptions(),\n ): unknown {\n if (value === null || typeof value !== 'object') return value;\n if (\n typeof (value as Partial<PublicSerializable>).toPublicJSON === 'function'\n ) {\n return (value as PublicSerializable).toPublicJSON(options);\n }\n if (Array.isArray(value)) {\n if (seen.has(value)) return value;\n seen.add(value);\n return value.map((entry) => this.toPublicData(entry, seen, options));\n }\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) return value;\n if (seen.has(value)) return value;\n seen.add(value);\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = this.toPublicData(entry, seen, options);\n }\n return out;\n }\n\n private getPublicJsonOptions(): PublicJsonOptions {\n return { permissions: this.context.permissions };\n }\n\n /** List the runnable commands grouped by object, for help output. */\n async listCommands(): Promise<string[]> {\n const commands: string[] = [];\n for (const [key, classInfo] of ObjectRegistry.getAllClasses()) {\n const objectName = classInfo.name || key;\n const config = ObjectRegistry.getConfig(objectName);\n const cliConfig = config?.cli;\n if (cliConfig === false) continue;\n\n const lower = objectName.toLowerCase();\n const included: string[] | undefined =\n typeof cliConfig === 'object' ? cliConfig?.include : undefined;\n const excluded: string[] =\n typeof cliConfig === 'object' && cliConfig?.exclude\n ? cliConfig.exclude\n : [];\n\n const expose = (cmd: string) =>\n !excluded.includes(cmd) && (!included || included.includes(cmd));\n\n for (const verb of CRUD_OPERATIONS) {\n if (expose(verb)) commands.push(`${lower}:${verb}`);\n }\n\n const methods = await ObjectRegistry.getAllMethods(objectName);\n for (const [methodName, methodDef] of methods) {\n if (CRUD_OPERATIONS.includes(methodName)) continue;\n if (!methodDef.isPublic) continue;\n if (excluded.includes(methodName)) continue;\n if (included !== undefined && !included.includes(methodName)) continue;\n commands.push(`${lower}:${methodName}`);\n }\n }\n return commands.sort();\n }\n\n private async helpText(): Promise<string> {\n const commands = await this.listCommands();\n return [\n `${this.config.name} ${this.config.version}`,\n this.config.description ?? '',\n '',\n 'Usage: <object>:<action> [id] [--flags]',\n '',\n 'Global flags:',\n ' --tenant <id> run the command inside a specific tenant',\n ' --all-tenants allow cross-tenant access (operator opt-in)',\n ' --from-file <p> read create/update payload from a JSON file',\n ' --where <json> filter for list',\n ' --limit / --offset / --order-by',\n '',\n 'Commands:',\n ...commands.map((c) => ` ${c}`),\n ].join('\\n');\n }\n\n private toNumber(\n value: string | boolean | undefined,\n fallback: number,\n ): number {\n if (typeof value !== 'string') return fallback;\n const n = Number.parseInt(value, 10);\n return Number.isNaN(n) ? fallback : n;\n }\n\n private flagString(value: string | boolean | undefined): string | undefined {\n return typeof value === 'string' ? value : undefined;\n }\n\n /** Coerce a flag value to a JSON scalar/object where it parses cleanly. */\n private coerceValue(value: string | boolean): unknown {\n if (typeof value !== 'string') return value;\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value !== '' && !Number.isNaN(Number(value))) return Number(value);\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n }\n}\n\n/**\n * Convenience runner used by the generated `setupCLI()` wrapper.\n *\n * @param config - CLI configuration.\n * @param context - Per-invocation context.\n * @returns A `{ run, generator }` pair; `run(argv)` strips the leading\n * `node script` entries before dispatching.\n */\nexport function setupCLI(config: CLIConfig = {}, context: CLIContext = {}) {\n const generator = new CLIGenerator(config, context);\n return {\n run: async (argv: string[]) => {\n const handler = generator.generateHandler();\n await handler(argv.slice(2));\n },\n generator,\n };\n}\n\n/**\n * Get a bare argv handler for a generated CLI.\n *\n * @param config - CLI configuration.\n * @param context - Per-invocation context.\n * @returns An async argv-slice handler.\n */\nexport function getCLIHandler(\n config: CLIConfig = {},\n context: CLIContext = {},\n) {\n const generator = new CLIGenerator(config, context);\n return generator.generateHandler();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../../src/generators/cli.ts"],"sourcesContent":["/**\n * CLI command generator for smrt objects\n *\n * Exposes registered `@smrt()` objects as a runnable admin CLI: each object gets\n * `list`/`get`/`create`/`update`/`delete` commands plus its public custom\n * methods, dispatched as `objectname:action`.\n *\n * Security parity with the REST/MCP generators (#1540, #1547, #1554, #1556):\n * - **Mass-assignment guard** — create/update bodies are filtered through the\n * `@smrt({ api: { writable: [...] } })` allowlist, dropping server-managed and\n * `@field({ readonly })` fields ({@link CLIGenerator.applyWritablePolicy}).\n * - **Exhaustive include** — an `include` list is the COMPLETE allowlist for the\n * surface; custom methods are gated on `isPublic`.\n * - **Sensitive redaction** — command *output* is serialized through\n * {@link CLIGenerator.toPublicData} so `@field({ sensitive })` values never\n * print (input bodies are guarded by the writable allowlist above).\n * - **Fail-closed tenant context** — tenant-scoped reads/writes run inside the\n * tenancy gate; without `--tenant <id>` / `--all-tenants` (and with tenancy\n * enabled) the command throws rather than ranging across all tenants.\n */\n\nimport type { AIClient, AIClientOptions } from '@happyvertical/ai';\nimport { SmrtCollection } from '../collection';\nimport type { DatabaseConfig } from '../database.js';\nimport type { PublicJsonOptions, SmrtObject } from '../object';\nimport { ObjectRegistry } from '../registry';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport type { RegisteredClass } from '../registry/types.js';\nimport {\n buildCustomActionInvocationArgs,\n CRUD_OPERATIONS,\n customActionParameterInputName,\n isCrudOperation,\n isFrameworkLifecycleMethod,\n normalizeCustomActionFailure,\n resolveCustomActionMetadata,\n resolveCustomActionNames,\n} from './custom-action.js';\nimport { runWithTenantGate } from './tenant-gate.js';\n\n/**\n * Public-data view exposed by SMRT objects: `toPublicJSON()` strips\n * `@field({ sensitive })` values before serialization (#1540).\n */\ninterface PublicSerializable {\n toPublicJSON(options?: PublicJsonOptions): unknown;\n}\n\n/**\n * View of an instance/collection for dispatching custom methods by name. Each\n * key is potentially a callable taking the parsed CLI options; callers narrow\n * with `typeof === 'function'` before invoking.\n */\ntype DynamicallyCallable = Record<\n string,\n ((...args: unknown[]) => unknown) | unknown\n>;\n\n/**\n * Configuration for a generated CLI.\n */\nexport interface CLIConfig {\n /** CLI program name (used in help output). */\n name?: string;\n /** CLI version. */\n version?: string;\n /** CLI description. */\n description?: string;\n}\n\n/**\n * Per-invocation context for a generated CLI.\n */\nexport interface CLIContext {\n /** Database handle passed to collections. */\n db?: DatabaseConfig;\n /** AI provider passed to collections. */\n ai?: AIClientOptions | AIClient;\n /** Authenticated operator, when the host establishes one. */\n user?: {\n id: string;\n roles?: string[];\n };\n /** Resolved permission slugs held by the operator. */\n permissions?: Iterable<string>;\n /**\n * Default tenant for tenant-scoped commands when no `--tenant` flag is given.\n * Hosts that authenticate an operator may set this from the principal.\n */\n tenantId?: string;\n /**\n * Default cross-tenant opt-in when no `--all-tenants` flag is given. Hosts\n * may set this for trusted operator/admin shells.\n */\n allowCrossTenant?: boolean;\n}\n\n/**\n * Flags consumed by the CLI itself — never treated as create/update field\n * values or as custom-action options.\n */\nconst RESERVED_FLAGS = new Set([\n 'tenant',\n 'all-tenants',\n 'from-file',\n 'limit',\n 'offset',\n 'order-by',\n 'orderBy',\n 'where',\n 'format',\n 'json',\n 'help',\n 'id',\n]);\n\n/** Parsed CLI invocation. */\ninterface ParsedInvocation {\n /** Object/collection segment (lowercased), e.g. `product`. */\n objectName: string;\n /** Action segment, e.g. `list` or a custom method name. */\n action: string;\n /** First positional argument after the command (typically an id). */\n positional?: string;\n /** Parsed flags. String for `--k v`/`--k=v`, `true` for bare `--flag`. */\n flags: Record<string, string | boolean>;\n}\n\n/**\n * Generate and run an admin CLI for the registered `@smrt()` objects.\n */\nexport class CLIGenerator {\n private config: CLIConfig;\n private context: CLIContext;\n\n constructor(config: CLIConfig = {}, context: CLIContext = {}) {\n this.config = {\n name: 'smrt-cli',\n version: '1.0.0',\n description: 'Auto-generated CLI from smrt objects',\n ...config,\n };\n this.context = context;\n }\n\n /** CLI program name. */\n get name(): string | undefined {\n return this.config.name;\n }\n\n /** CLI version. */\n get version(): string | undefined {\n return this.config.version;\n }\n\n /**\n * Return the argv handler invoked by the generated `setupCLI()` wrapper.\n *\n * @returns An async function accepting the post-`node script` argv slice.\n */\n generateHandler(): (args: string[]) => Promise<void> {\n return async (args: string[]) => {\n await this.run(args ?? []);\n };\n }\n\n /**\n * Parse and execute a single CLI invocation.\n *\n * @param args - argv slice (command + flags).\n */\n async run(args: string[]): Promise<void> {\n if (args.length === 0 || args[0] === '--help' || args[0] === 'help') {\n console.log(await this.helpText());\n return;\n }\n\n const parsed = this.parseArgs(args);\n if (!parsed) {\n console.log(await this.helpText());\n return;\n }\n\n const classInfo = this.resolveClass(parsed.objectName);\n if (!classInfo) {\n throw new Error(`Object type '${parsed.objectName}' not found`);\n }\n const objectName: string = classInfo.name || parsed.objectName;\n\n await this.assertCommandExposed(objectName, parsed.action);\n\n const tenantId =\n typeof parsed.flags.tenant === 'string'\n ? parsed.flags.tenant\n : this.context.tenantId;\n const allowCrossTenant =\n parsed.flags['all-tenants'] === true ||\n this.context.allowCrossTenant === true;\n\n // Tenant-scoping is resolved authoritatively inside tenancy (by class name),\n // matching the interceptor and covering both @TenantScoped and\n // @smrt({ tenantScoped }) registrations (#1554).\n const result = await runWithTenantGate(\n { className: objectName, tenantId, allowCrossTenant, surface: 'CLI' },\n async () => {\n const collection = await this.getCollection(objectName);\n return this.executeAction(collection, objectName, parsed);\n },\n );\n\n if (result !== undefined) {\n console.log(JSON.stringify(this.toPublicData(result), null, 2));\n }\n }\n\n /**\n * Parse argv into a command + flags. Supports `name:action` and\n * `name action` command forms, `--key value`, `--key=value`, and bare\n * `--flag` booleans.\n */\n private parseArgs(args: string[]): ParsedInvocation | null {\n const [command, ...rest] = args;\n if (!command || command.startsWith('-')) return null;\n\n let objectName: string;\n let action: string;\n let tail = rest;\n\n if (command.includes(':')) {\n const [obj, act] = command.split(':');\n objectName = obj.toLowerCase();\n action = act;\n } else if (rest.length > 0 && !rest[0].startsWith('-')) {\n objectName = command.toLowerCase();\n action = rest[0];\n tail = rest.slice(1);\n } else {\n // A bare object name with no action is not a runnable command.\n return null;\n }\n\n if (!objectName || !action) return null;\n\n const flags: Record<string, string | boolean> = {};\n let positional: string | undefined;\n\n for (let i = 0; i < tail.length; i++) {\n const token = tail[i];\n if (token.startsWith('--')) {\n const body = token.slice(2);\n const eq = body.indexOf('=');\n if (eq !== -1) {\n flags[body.slice(0, eq)] = body.slice(eq + 1);\n } else {\n const next = tail[i + 1];\n if (next !== undefined && !next.startsWith('--')) {\n flags[body] = next;\n i++;\n } else {\n flags[body] = true;\n }\n }\n } else if (positional === undefined) {\n positional = token;\n }\n }\n\n return { objectName, action, positional, flags };\n }\n\n /**\n * Resolve a registered class by simple name (case-insensitive), mirroring the\n * MCP generator's lookup.\n */\n private resolveClass(objectName: string): RegisteredClass | null {\n const registeredClasses = ObjectRegistry.getAllClasses();\n for (const [key, info] of registeredClasses) {\n const simpleName = info.name || key;\n if (simpleName.toLowerCase() === objectName.toLowerCase()) {\n return info;\n }\n }\n return null;\n }\n\n /**\n * Throw if `action` is not exposed for `objectName` under its `@smrt({ cli })`\n * config. `cli: false` disables the object entirely; an `include` list is the\n * complete allowlist; custom methods must be `isPublic`.\n */\n private async assertCommandExposed(\n objectName: string,\n action: string,\n ): Promise<void> {\n const config = ObjectRegistry.getConfig(objectName);\n const cliConfig = config?.cli;\n\n if (cliConfig === false) {\n throw new Error(`CLI is disabled for ${objectName}`);\n }\n\n const included: string[] | undefined =\n typeof cliConfig === 'object' ? cliConfig?.include : undefined;\n const excluded: string[] =\n typeof cliConfig === 'object' && cliConfig?.exclude\n ? cliConfig.exclude\n : [];\n\n if (excluded.includes(action)) {\n throw new Error(`Command '${action}' is excluded for ${objectName}`);\n }\n\n const isCrud = isCrudOperation(action);\n\n if (isCrud) {\n // An include list, when present, is the complete allowlist for CRUD too.\n if (included && !included.includes(action)) {\n throw new Error(`Command '${action}' is not enabled for ${objectName}`);\n }\n return;\n }\n\n // A framework lifecycle method (save/initialize/... — see\n // FRAMEWORK_LIFECYCLE_METHOD_NAMES) is never a custom action, even when\n // this class declares its own override: it is the mechanism behind\n // generated CRUD, not a distinct operation, and listCommands() never\n // advertises it. Reject it here too so an advertised command and an\n // invokable one cannot disagree (#2638).\n if (isFrameworkLifecycleMethod(action)) {\n throw new Error(\n `Command '${action}' is a framework lifecycle method, not a custom action, on ${objectName}`,\n );\n }\n\n // Custom method: must be public, and — when an include list is present — it\n // is the COMPLETE allowlist (parity with MCP exhaustive-include, #1547).\n const methods = await ObjectRegistry.getAllMethods(objectName);\n const methodDef = methods.get(action);\n if (!methodDef) {\n throw new Error(`Unknown command '${action}' for ${objectName}`);\n }\n if (!methodDef.isPublic) {\n throw new Error(`Command '${action}' is not public on ${objectName}`);\n }\n if (included !== undefined && !included.includes(action)) {\n throw new Error(`Command '${action}' is not enabled for ${objectName}`);\n }\n }\n\n /**\n * Get the collection for an object via the registry factory. Uses\n * `ObjectRegistry.getCollection`, which caches, initializes, and — for plain\n * `@smrt()` models without a hand-written collection class — auto-creates the\n * default collection. (A bare `collectionConstructor` check would wrongly\n * throw for those models even though CRUD is advertised.)\n */\n private async getCollection(\n objectName: string,\n ): Promise<SmrtCollection<SmrtObject>> {\n return ObjectRegistry.getCollection(objectName, {\n ai: this.context.ai,\n db: this.context.db,\n });\n }\n\n /**\n * Execute a parsed command against a collection.\n */\n private async executeAction(\n collection: SmrtCollection<SmrtObject>,\n objectName: string,\n parsed: ParsedInvocation,\n ): Promise<unknown> {\n const { action, positional, flags } = parsed;\n\n switch (action) {\n case 'list': {\n const listOptions: {\n limit: number;\n offset: number;\n orderBy?: string;\n where?: Record<string, unknown>;\n } = {\n limit: Math.min(this.toNumber(flags.limit, 50), 1000),\n offset: this.toNumber(flags.offset, 0),\n };\n const orderBy = flags['order-by'] ?? flags.orderBy;\n if (typeof orderBy === 'string') listOptions.orderBy = orderBy;\n if (typeof flags.where === 'string') {\n listOptions.where = JSON.parse(flags.where);\n }\n return collection.list(listOptions);\n }\n\n case 'get': {\n const id = positional ?? this.flagString(flags.id);\n if (!id) throw new Error('An id is required for get');\n const item = await collection.get(id);\n if (!item) throw new Error(`${objectName} not found`);\n return item;\n }\n\n case 'create': {\n const data = this.applyWritablePolicy(\n objectName,\n await this.readPayload(flags),\n );\n if (this.context.user) {\n data.created_by = this.context.user.id;\n data.owner_id = this.context.user.id;\n }\n const created = await collection.create(data);\n await created.save();\n return created;\n }\n\n case 'update': {\n const id = positional ?? this.flagString(flags.id);\n if (!id) throw new Error('An id is required for update');\n const existing = await collection.get(id);\n if (!existing) throw new Error(`${objectName} not found`);\n const data = this.applyWritablePolicy(\n objectName,\n await this.readPayload(flags),\n );\n Object.assign(existing, data);\n if (this.context.user) {\n // Stamp the server-managed audit column on the instance; this field\n // is not part of the public SmrtObject surface.\n (existing as unknown as Record<string, unknown>).updated_by =\n this.context.user.id;\n }\n await existing.save();\n return existing;\n }\n\n case 'delete': {\n const id = positional ?? this.flagString(flags.id);\n if (!id) throw new Error('An id is required for delete');\n const existing = await collection.get(id);\n if (!existing) throw new Error(`${objectName} not found`);\n await existing.delete();\n return { success: true, id, message: `${objectName} deleted` };\n }\n\n default:\n return this.executeCustomAction(\n objectName,\n collection,\n action,\n positional,\n flags,\n );\n }\n }\n\n /**\n * Execute a custom method against its manifest-declared item/collection\n * target. The legacy collection fallback remains only for unavailable\n * metadata, where target scope cannot be established safely.\n */\n private async executeCustomAction(\n objectName: string,\n collection: SmrtCollection<SmrtObject>,\n action: string,\n positional: string | undefined,\n flags: Record<string, string | boolean>,\n ): Promise<unknown> {\n const id = positional ?? this.flagString(flags.id);\n const methodDef = (await ObjectRegistry.getAllMethods(objectName)).get(\n action,\n );\n const metadata = resolveCustomActionMetadata({\n actionName: action,\n method: methodDef,\n apiConfig: ObjectRegistry.getConfig(objectName).api,\n ...(this.hasCollectionReceiver(ObjectRegistry.getClass(objectName))\n ? { defaultScope: 'collection' as const }\n : {}),\n });\n const args = this.customOptions(flags, metadata);\n const invocationArgs =\n metadata.parameters?.length === 1 &&\n metadata.parameters[0]?.name === 'options'\n ? Object.keys(args).length > 0\n ? { options: args }\n : {}\n : args;\n const methodArgs = buildCustomActionInvocationArgs(\n metadata,\n invocationArgs,\n );\n\n if (methodDef && metadata.idRequired && !id) {\n throw new Error(`An id is required for custom action '${action}'`);\n }\n if (methodDef && !metadata.idRequired && id) {\n throw new Error(\n `Custom action '${action}' is collection-scoped and does not accept an id`,\n );\n }\n\n if (id) {\n const object = await collection.get(id);\n if (!object) throw new Error('Object not found');\n // Custom methods are dispatched dynamically by name; index through a\n // callable-keyed view of the instance, narrowing before invoking.\n const candidate = (object as unknown as DynamicallyCallable)[action];\n if (typeof candidate !== 'function') {\n throw new Error(`Method '${action}' not found on object instance`);\n }\n const result = await candidate.call(object, ...methodArgs);\n const failure = normalizeCustomActionFailure(result);\n if (failure) throw new Error(JSON.stringify({ error: failure }));\n return result;\n }\n\n if (methodDef?.isStatic) {\n const classInfo = ObjectRegistry.getClass(objectName);\n const candidate = (\n classInfo?.constructor as unknown as DynamicallyCallable | undefined\n )?.[action];\n if (typeof candidate !== 'function') {\n throw new Error(`Static method '${action}' not found on ${objectName}`);\n }\n const result = await candidate.call(\n classInfo?.constructor,\n ...methodArgs,\n );\n const failure = normalizeCustomActionFailure(result);\n if (failure) throw new Error(JSON.stringify({ error: failure }));\n return result;\n }\n\n const collectionCandidate = (collection as unknown as DynamicallyCallable)[\n action\n ];\n if (typeof collectionCandidate === 'function') {\n const result = await collectionCandidate.call(collection, ...methodArgs);\n const failure = normalizeCustomActionFailure(result);\n if (failure) throw new Error(JSON.stringify({ error: failure }));\n return result;\n }\n throw new Error(\n `Method '${action}' not found on collection. Provide an id for object-specific actions.`,\n );\n }\n\n private hasCollectionReceiver(classInfo?: RegisteredClass): boolean {\n return (\n !!classInfo && classInfo.constructor.prototype instanceof SmrtCollection\n );\n }\n\n /**\n * Resolve the create/update payload: `--from-file <path>` (JSON) takes\n * precedence; otherwise non-reserved `--field value` flags form the body.\n * Reading from a file also keeps secrets off the process argv (#1556).\n */\n private async readPayload(\n flags: Record<string, string | boolean>,\n ): Promise<Record<string, unknown>> {\n const fromFile = flags['from-file'];\n if (typeof fromFile === 'string' && fromFile) {\n const { readFile } = await import('node:fs/promises');\n const content = await readFile(fromFile, 'utf-8');\n const parsed = JSON.parse(content);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('--from-file must contain a JSON object');\n }\n return parsed as Record<string, unknown>;\n }\n\n const data: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(flags)) {\n if (RESERVED_FLAGS.has(key)) continue;\n data[key] = this.coerceValue(value);\n }\n return data;\n }\n\n /**\n * Collect custom-action flags. Typed action parameters may deliberately use\n * standard CLI names such as `limit`, `offset`, `where`, or `format`; retain\n * those rather than silently dropping them as CRUD flags. Item actions keep\n * `--id` for their receiver and accept an action parameter named `id` as\n * `--action-id` to preserve separate namespaces.\n */\n private customOptions(\n flags: Record<string, string | boolean>,\n metadata?: { parameters?: Array<{ name: string }>; idRequired: boolean },\n ): Record<string, unknown> {\n const declaredInputs = new Set(\n (metadata?.parameters ?? []).map((parameter) =>\n customActionParameterInputName(\n metadata ?? { idRequired: false },\n parameter.name,\n ),\n ),\n );\n const options: Record<string, unknown> = {};\n for (const [rawKey, value] of Object.entries(flags)) {\n const key = rawKey === 'action-id' ? 'actionId' : rawKey;\n if (rawKey === 'id') continue;\n if (RESERVED_FLAGS.has(rawKey) && !declaredInputs.has(key)) continue;\n if (rawKey === 'action-id' && !declaredInputs.has(key)) continue;\n options[key] = this.coerceValue(value);\n }\n return options;\n }\n\n /**\n * Mass-assignment guard (#1540/#1556): strip framework/server-managed and\n * `@field({ readonly })` fields, and — when an `@smrt({ api: { writable } })`\n * allowlist is set — intersect with it. Identical policy to the REST/MCP\n * generators so a CLI create/update cannot set fields the API forbids.\n */\n private applyWritablePolicy(\n objectName: string | undefined,\n data: unknown,\n ): Record<string, unknown> {\n if (!data || typeof data !== 'object') return {};\n\n const serverManaged = new Set([\n 'id',\n 'tenantId',\n 'tenant_id',\n 'createdAt',\n 'created_at',\n 'updatedAt',\n 'updated_at',\n ]);\n\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n\n if (objectName) {\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (\n apiConfig &&\n typeof apiConfig === 'object' &&\n Array.isArray((apiConfig as { writable?: unknown }).writable)\n ) {\n writable = (apiConfig as { writable: string[] }).writable;\n }\n\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && (def.readonly === true || def._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n }\n\n /**\n * Serialize command output, excluding `@field({ sensitive })` fields (#1540).\n * Recurses arrays/plain objects so a SmrtObject nested in a custom-action\n * result is stripped too; a cycle guard prevents infinite loops.\n */\n private toPublicData(\n value: unknown,\n seen: WeakSet<object> = new WeakSet(),\n options: PublicJsonOptions = this.getPublicJsonOptions(),\n ): unknown {\n if (value === null || typeof value !== 'object') return value;\n if (\n typeof (value as Partial<PublicSerializable>).toPublicJSON === 'function'\n ) {\n return (value as PublicSerializable).toPublicJSON(options);\n }\n if (Array.isArray(value)) {\n if (seen.has(value)) return value;\n seen.add(value);\n return value.map((entry) => this.toPublicData(entry, seen, options));\n }\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) return value;\n if (seen.has(value)) return value;\n seen.add(value);\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = this.toPublicData(entry, seen, options);\n }\n return out;\n }\n\n private getPublicJsonOptions(): PublicJsonOptions {\n return { permissions: this.context.permissions };\n }\n\n /** List the runnable commands grouped by object, for help output. */\n async listCommands(): Promise<string[]> {\n const commands: string[] = [];\n for (const [key, classInfo] of ObjectRegistry.getAllClasses()) {\n const objectName = classInfo.name || key;\n\n // The framework's own abstract base classes are scaffolding, not\n // resources — never expose an `objectname:list`/`:create`/... command\n // for them, regardless of config (#2642).\n if (isFrameworkBaseClass(objectName, classInfo.packageName)) continue;\n\n const config = ObjectRegistry.getConfig(objectName);\n const cliConfig = config?.cli;\n if (cliConfig === false) continue;\n\n const lower = objectName.toLowerCase();\n const included: string[] | undefined =\n typeof cliConfig === 'object' ? cliConfig?.include : undefined;\n const excluded: string[] =\n typeof cliConfig === 'object' && cliConfig?.exclude\n ? cliConfig.exclude\n : [];\n\n const expose = (cmd: string) =>\n !excluded.includes(cmd) && (!included || included.includes(cmd));\n\n for (const verb of CRUD_OPERATIONS) {\n if (expose(verb)) commands.push(`${lower}:${verb}`);\n }\n\n const methods = await ObjectRegistry.getAllMethods(objectName);\n const customActions = resolveCustomActionNames(\n methods,\n { include: included, exclude: excluded },\n CRUD_OPERATIONS,\n );\n for (const methodName of customActions) {\n commands.push(`${lower}:${methodName}`);\n }\n }\n return commands.sort();\n }\n\n private async helpText(): Promise<string> {\n const commands = await this.listCommands();\n return [\n `${this.config.name} ${this.config.version}`,\n this.config.description ?? '',\n '',\n 'Usage: <object>:<action> [id] [--flags]',\n '',\n 'Global flags:',\n ' --tenant <id> run the command inside a specific tenant',\n ' --all-tenants allow cross-tenant access (operator opt-in)',\n ' --from-file <p> read create/update payload from a JSON file',\n ' --where <json> filter for list',\n ' --limit / --offset / --order-by',\n '',\n 'Commands:',\n ...commands.map((c) => ` ${c}`),\n ].join('\\n');\n }\n\n private toNumber(\n value: string | boolean | undefined,\n fallback: number,\n ): number {\n if (typeof value !== 'string') return fallback;\n const n = Number.parseInt(value, 10);\n return Number.isNaN(n) ? fallback : n;\n }\n\n private flagString(value: string | boolean | undefined): string | undefined {\n return typeof value === 'string' ? value : undefined;\n }\n\n /** Coerce a flag value to a JSON scalar/object where it parses cleanly. */\n private coerceValue(value: string | boolean): unknown {\n if (typeof value !== 'string') return value;\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value !== '' && !Number.isNaN(Number(value))) return Number(value);\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n }\n}\n\n/**\n * Convenience runner used by the generated `setupCLI()` wrapper.\n *\n * @param config - CLI configuration.\n * @param context - Per-invocation context.\n * @returns A `{ run, generator }` pair; `run(argv)` strips the leading\n * `node script` entries before dispatching.\n */\nexport function setupCLI(config: CLIConfig = {}, context: CLIContext = {}) {\n const generator = new CLIGenerator(config, context);\n return {\n run: async (argv: string[]) => {\n const handler = generator.generateHandler();\n await handler(argv.slice(2));\n },\n generator,\n };\n}\n\n/**\n * Get a bare argv handler for a generated CLI.\n *\n * @param config - CLI configuration.\n * @param context - Per-invocation context.\n * @returns An async argv-slice handler.\n */\nexport function getCLIHandler(\n config: CLIConfig = {},\n context: CLIContext = {},\n) {\n const generator = new CLIGenerator(config, context);\n return generator.generateHandler();\n}\n"],"mappings":";;;;;;;;;;AAqGA,IAAM,iCAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;AAiBD,IAAa,eAAb,MAA0B;CACxB;CACA;CAEA,YAAY,SAAoB,CAAC,GAAG,UAAsB,CAAC,GAAG;EAC5D,KAAK,SAAS;GACZ,MAAM;GACN,SAAS;GACT,aAAa;GACb,GAAG;EACL;EACA,KAAK,UAAU;CACjB;;CAGA,IAAI,OAA2B;EAC7B,OAAO,KAAK,OAAO;CACrB;;CAGA,IAAI,UAA8B;EAChC,OAAO,KAAK,OAAO;CACrB;;;;;;CAOA,kBAAqD;EACnD,OAAO,OAAO,SAAmB;GAC/B,MAAM,KAAK,IAAI,QAAQ,CAAC,CAAC;EAC3B;CACF;;;;;;CAOA,MAAM,IAAI,MAA+B;EACvC,IAAI,KAAK,WAAW,KAAK,KAAK,OAAO,YAAY,KAAK,OAAO,QAAQ;GACnE,QAAQ,IAAI,MAAM,KAAK,SAAS,CAAC;GACjC;EACF;EAEA,MAAM,SAAS,KAAK,UAAU,IAAI;EAClC,IAAI,CAAC,QAAQ;GACX,QAAQ,IAAI,MAAM,KAAK,SAAS,CAAC;GACjC;EACF;EAEA,MAAM,YAAY,KAAK,aAAa,OAAO,UAAU;EACrD,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,gBAAgB,OAAO,WAAW,YAAY;EAEhE,MAAM,aAAqB,UAAU,QAAQ,OAAO;EAEpD,MAAM,KAAK,qBAAqB,YAAY,OAAO,MAAM;EAazD,MAAM,SAAS,MAAM,kBACnB;GAAE,WAAW;GAAY,UAXzB,OAAO,OAAO,MAAM,WAAW,WAC3B,OAAO,MAAM,SACb,KAAK,QAAQ;GASkB,kBAPnC,OAAO,MAAM,mBAAmB,QAChC,KAAK,QAAQ,qBAAqB;GAMmB,SAAS;EAAM,GACpE,YAAY;GACV,MAAM,aAAa,MAAM,KAAK,cAAc,UAAU;GACtD,OAAO,KAAK,cAAc,YAAY,YAAY,MAAM;EAC1D,CACF;EAEA,IAAI,WAAW,KAAA,GACb,QAAQ,IAAI,KAAK,UAAU,KAAK,aAAa,MAAM,GAAG,MAAM,CAAC,CAAC;CAElE;;;;;;CAOA,UAAkB,MAAyC;EACzD,MAAM,CAAC,SAAS,GAAG,QAAQ;EAC3B,IAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,GAAG,OAAO;EAEhD,IAAI;EACJ,IAAI;EACJ,IAAI,OAAO;EAEX,IAAI,QAAQ,SAAS,GAAG,GAAG;GACzB,MAAM,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;GACpC,aAAa,IAAI,YAAY;GAC7B,SAAS;EACX,OAAO,IAAI,KAAK,SAAS,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,GAAG,GAAG;GACtD,aAAa,QAAQ,YAAY;GACjC,SAAS,KAAK;GACd,OAAO,KAAK,MAAM,CAAC;EACrB,OAEE,OAAO;EAGT,IAAI,CAAC,cAAc,CAAC,QAAQ,OAAO;EAEnC,MAAM,QAA0C,CAAC;EACjD,IAAI;EAEJ,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;GACpC,MAAM,QAAQ,KAAK;GACnB,IAAI,MAAM,WAAW,IAAI,GAAG;IAC1B,MAAM,OAAO,MAAM,MAAM,CAAC;IAC1B,MAAM,KAAK,KAAK,QAAQ,GAAG;IAC3B,IAAI,OAAO,IACT,MAAM,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,MAAM,KAAK,CAAC;SACvC;KACL,MAAM,OAAO,KAAK,IAAI;KACtB,IAAI,SAAS,KAAA,KAAa,CAAC,KAAK,WAAW,IAAI,GAAG;MAChD,MAAM,QAAQ;MACd;KACF,OACE,MAAM,QAAQ;IAElB;GACF,OAAO,IAAI,eAAe,KAAA,GACxB,aAAa;EAEjB;EAEA,OAAO;GAAE;GAAY;GAAQ;GAAY;EAAM;CACjD;;;;;CAMA,aAAqB,YAA4C;EAC/D,MAAM,oBAAoB,eAAe,cAAc;EACvD,KAAK,MAAM,CAAC,KAAK,SAAS,mBAExB,KADmB,KAAK,QAAQ,IAAA,CACjB,YAAY,MAAM,WAAW,YAAY,GACtD,OAAO;EAGX,OAAO;CACT;;;;;;CAOA,MAAc,qBACZ,YACA,QACe;EAEf,MAAM,YADS,eAAe,UAAU,UACtB,CAAA,EAAQ;EAE1B,IAAI,cAAc,OAChB,MAAM,IAAI,MAAM,uBAAuB,YAAY;EAGrD,MAAM,WACJ,OAAO,cAAc,WAAW,WAAW,UAAU,KAAA;EAMvD,KAJE,OAAO,cAAc,YAAY,WAAW,UACxC,UAAU,UACV,CAAC,EAAA,CAEM,SAAS,MAAM,GAC1B,MAAM,IAAI,MAAM,YAAY,OAAO,oBAAoB,YAAY;EAKrE,IAFe,gBAAgB,MAE3B,GAAQ;GAEV,IAAI,YAAY,CAAC,SAAS,SAAS,MAAM,GACvC,MAAM,IAAI,MAAM,YAAY,OAAO,uBAAuB,YAAY;GAExE;EACF;EAQA,IAAI,2BAA2B,MAAM,GACnC,MAAM,IAAI,MACR,YAAY,OAAO,6DAA6D,YAClF;EAMF,MAAM,aAAY,MADI,eAAe,cAAc,UAAU,EAAA,CACnC,IAAI,MAAM;EACpC,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,oBAAoB,OAAO,QAAQ,YAAY;EAEjE,IAAI,CAAC,UAAU,UACb,MAAM,IAAI,MAAM,YAAY,OAAO,qBAAqB,YAAY;EAEtE,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,SAAS,MAAM,GACrD,MAAM,IAAI,MAAM,YAAY,OAAO,uBAAuB,YAAY;CAE1E;;;;;;;;CASA,MAAc,cACZ,YACqC;EACrC,OAAO,eAAe,cAAc,YAAY;GAC9C,IAAI,KAAK,QAAQ;GACjB,IAAI,KAAK,QAAQ;EACnB,CAAC;CACH;;;;CAKA,MAAc,cACZ,YACA,YACA,QACkB;EAClB,MAAM,EAAE,QAAQ,YAAY,UAAU;EAEtC,QAAQ,QAAR;GACE,KAAK,QAAQ;IACX,MAAM,cAKF;KACF,OAAO,KAAK,IAAI,KAAK,SAAS,MAAM,OAAO,EAAE,GAAG,GAAI;KACpD,QAAQ,KAAK,SAAS,MAAM,QAAQ,CAAC;IACvC;IACA,MAAM,UAAU,MAAM,eAAe,MAAM;IAC3C,IAAI,OAAO,YAAY,UAAU,YAAY,UAAU;IACvD,IAAI,OAAO,MAAM,UAAU,UACzB,YAAY,QAAQ,KAAK,MAAM,MAAM,KAAK;IAE5C,OAAO,WAAW,KAAK,WAAW;GACpC;GAEA,KAAK,OAAO;IACV,MAAM,KAAK,cAAc,KAAK,WAAW,MAAM,EAAE;IACjD,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,2BAA2B;IACpD,MAAM,OAAO,MAAM,WAAW,IAAI,EAAE;IACpC,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,GAAG,WAAW,WAAW;IACpD,OAAO;GACT;GAEA,KAAK,UAAU;IACb,MAAM,OAAO,KAAK,oBAChB,YACA,MAAM,KAAK,YAAY,KAAK,CAC9B;IACA,IAAI,KAAK,QAAQ,MAAM;KACrB,KAAK,aAAa,KAAK,QAAQ,KAAK;KACpC,KAAK,WAAW,KAAK,QAAQ,KAAK;IACpC;IACA,MAAM,UAAU,MAAM,WAAW,OAAO,IAAI;IAC5C,MAAM,QAAQ,KAAK;IACnB,OAAO;GACT;GAEA,KAAK,UAAU;IACb,MAAM,KAAK,cAAc,KAAK,WAAW,MAAM,EAAE;IACjD,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,8BAA8B;IACvD,MAAM,WAAW,MAAM,WAAW,IAAI,EAAE;IACxC,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,GAAG,WAAW,WAAW;IACxD,MAAM,OAAO,KAAK,oBAChB,YACA,MAAM,KAAK,YAAY,KAAK,CAC9B;IACA,OAAO,OAAO,UAAU,IAAI;IAC5B,IAAI,KAAK,QAAQ,MAGf,SAAiD,aAC/C,KAAK,QAAQ,KAAK;IAEtB,MAAM,SAAS,KAAK;IACpB,OAAO;GACT;GAEA,KAAK,UAAU;IACb,MAAM,KAAK,cAAc,KAAK,WAAW,MAAM,EAAE;IACjD,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,8BAA8B;IACvD,MAAM,WAAW,MAAM,WAAW,IAAI,EAAE;IACxC,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,GAAG,WAAW,WAAW;IACxD,MAAM,SAAS,OAAO;IACtB,OAAO;KAAE,SAAS;KAAM;KAAI,SAAS,GAAG,WAAW;IAAU;GAC/D;GAEA,SACE,OAAO,KAAK,oBACV,YACA,YACA,QACA,YACA,KACF;EACJ;CACF;;;;;;CAOA,MAAc,oBACZ,YACA,YACA,QACA,YACA,OACkB;EAClB,MAAM,KAAK,cAAc,KAAK,WAAW,MAAM,EAAE;EACjD,MAAM,aAAa,MAAM,eAAe,cAAc,UAAU,EAAA,CAAG,IACjE,MACF;EACA,MAAM,WAAW,4BAA4B;GAC3C,YAAY;GACZ,QAAQ;GACR,WAAW,eAAe,UAAU,UAAU,CAAC,CAAC;GAChD,GAAI,KAAK,sBAAsB,eAAe,SAAS,UAAU,CAAC,IAC9D,EAAE,cAAc,aAAsB,IACtC,CAAC;EACP,CAAC;EACD,MAAM,OAAO,KAAK,cAAc,OAAO,QAAQ;EAQ/C,MAAM,aAAa,gCACjB,UAPA,SAAS,YAAY,WAAW,KAChC,SAAS,WAAW,EAAE,EAAE,SAAS,YAC7B,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,IACzB,EAAE,SAAS,KAAK,IAChB,CAAC,IACH,IAIN;EAEA,IAAI,aAAa,SAAS,cAAc,CAAC,IACvC,MAAM,IAAI,MAAM,wCAAwC,OAAO,EAAE;EAEnE,IAAI,aAAa,CAAC,SAAS,cAAc,IACvC,MAAM,IAAI,MACR,kBAAkB,OAAO,iDAC3B;EAGF,IAAI,IAAI;GACN,MAAM,SAAS,MAAM,WAAW,IAAI,EAAE;GACtC,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,kBAAkB;GAG/C,MAAM,YAAa,OAA0C;GAC7D,IAAI,OAAO,cAAc,YACvB,MAAM,IAAI,MAAM,WAAW,OAAO,+BAA+B;GAEnE,MAAM,SAAS,MAAM,UAAU,KAAK,QAAQ,GAAG,UAAU;GACzD,MAAM,UAAU,6BAA6B,MAAM;GACnD,IAAI,SAAS,MAAM,IAAI,MAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,CAAC,CAAC;GAC/D,OAAO;EACT;EAEA,IAAI,WAAW,UAAU;GACvB,MAAM,YAAY,eAAe,SAAS,UAAU;GACpD,MAAM,aACJ,WAAW,YAAA,GACT;GACJ,IAAI,OAAO,cAAc,YACvB,MAAM,IAAI,MAAM,kBAAkB,OAAO,iBAAiB,YAAY;GAExE,MAAM,SAAS,MAAM,UAAU,KAC7B,WAAW,aACX,GAAG,UACL;GACA,MAAM,UAAU,6BAA6B,MAAM;GACnD,IAAI,SAAS,MAAM,IAAI,MAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,CAAC,CAAC;GAC/D,OAAO;EACT;EAEA,MAAM,sBAAuB,WAC3B;EAEF,IAAI,OAAO,wBAAwB,YAAY;GAC7C,MAAM,SAAS,MAAM,oBAAoB,KAAK,YAAY,GAAG,UAAU;GACvE,MAAM,UAAU,6BAA6B,MAAM;GACnD,IAAI,SAAS,MAAM,IAAI,MAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,CAAC,CAAC;GAC/D,OAAO;EACT;EACA,MAAM,IAAI,MACR,WAAW,OAAO,sEACpB;CACF;CAEA,sBAA8B,WAAsC;EAClE,OACE,CAAC,CAAC,aAAa,UAAU,YAAY,qBAAqB;CAE9D;;;;;;CAOA,MAAc,YACZ,OACkC;EAClC,MAAM,WAAW,MAAM;EACvB,IAAI,OAAO,aAAa,YAAY,UAAU;GAC5C,MAAM,EAAE,aAAa,MAAM,OAAO;GAClC,MAAM,UAAU,MAAM,SAAS,UAAU,OAAO;GAChD,MAAM,SAAS,KAAK,MAAM,OAAO;GACjC,IAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAC/D,MAAM,IAAI,MAAM,wCAAwC;GAE1D,OAAO;EACT;EAEA,MAAM,OAAgC,CAAC;EACvC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAAG;GAChD,IAAI,eAAe,IAAI,GAAG,GAAG;GAC7B,KAAK,OAAO,KAAK,YAAY,KAAK;EACpC;EACA,OAAO;CACT;;;;;;;;CASA,cACE,OACA,UACyB;EACzB,MAAM,iBAAiB,IAAI,KACxB,UAAU,cAAc,CAAC,EAAA,CAAG,KAAK,cAChC,+BACE,YAAY,EAAE,YAAY,MAAM,GAChC,UAAU,IACZ,CACF,CACF;EACA,MAAM,UAAmC,CAAC;EAC1C,KAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,KAAK,GAAG;GACnD,MAAM,MAAM,WAAW,cAAc,aAAa;GAClD,IAAI,WAAW,MAAM;GACrB,IAAI,eAAe,IAAI,MAAM,KAAK,CAAC,eAAe,IAAI,GAAG,GAAG;GAC5D,IAAI,WAAW,eAAe,CAAC,eAAe,IAAI,GAAG,GAAG;GACxD,QAAQ,OAAO,KAAK,YAAY,KAAK;EACvC;EACA,OAAO;CACT;;;;;;;CAQA,oBACE,YACA,MACyB;EACzB,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO,CAAC;EAE/C,MAAM,gCAAgB,IAAI,IAAI;GAC5B;GACA;GACA;GACA;GACA;GACA;GACA;EACF,CAAC;EAED,MAAM,2BAAW,IAAI,IAAY;EACjC,IAAI,WAA4B;EAEhC,IAAI,YAAY;GACd,MAAM,YAAY,eAAe,UAAU,UAAU,CAAC,EAAE;GACxD,IACE,aACA,OAAO,cAAc,YACrB,MAAM,QAAS,UAAqC,QAAQ,GAE5D,WAAY,UAAqC;GAGnD,KAAK,MAAM,CAAC,MAAM,QAAQ,eAAe,UAAU,UAAU,GAC3D,IAAI,QAAQ,IAAI,aAAa,QAAQ,IAAI,OAAO,aAAa,OAC3D,SAAS,IAAI,IAAI;EAGvB;EAEA,MAAM,SAAkC,CAAC;EACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;GAC/C,IAAI,IAAI,WAAW,GAAG,GAAG;GACzB,IAAI,cAAc,IAAI,GAAG,GAAG;GAC5B,IAAI,SAAS,IAAI,GAAG,GAAG;GACvB,IAAI,YAAY,CAAC,SAAS,SAAS,GAAG,GAAG;GACzC,OAAO,OAAO;EAChB;EACA,OAAO;CACT;;;;;;CAOA,aACE,OACA,uBAAwB,IAAI,QAAQ,GACpC,UAA6B,KAAK,qBAAqB,GAC9C;EACT,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;EACxD,IACE,OAAQ,MAAsC,iBAAiB,YAE/D,OAAQ,MAA6B,aAAa,OAAO;EAE3D,IAAI,MAAM,QAAQ,KAAK,GAAG;GACxB,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;GAC5B,KAAK,IAAI,KAAK;GACd,OAAO,MAAM,KAAK,UAAU,KAAK,aAAa,OAAO,MAAM,OAAO,CAAC;EACrE;EACA,MAAM,QAAQ,OAAO,eAAe,KAAK;EACzC,IAAI,UAAU,OAAO,aAAa,UAAU,MAAM,OAAO;EACzD,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;EAC5B,KAAK,IAAI,KAAK;EACd,MAAM,MAA+B,CAAC;EACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,OAAO,KAAK,aAAa,OAAO,MAAM,OAAO;EAEnD,OAAO;CACT;CAEA,uBAAkD;EAChD,OAAO,EAAE,aAAa,KAAK,QAAQ,YAAY;CACjD;;CAGA,MAAM,eAAkC;EACtC,MAAM,WAAqB,CAAC;EAC5B,KAAK,MAAM,CAAC,KAAK,cAAc,eAAe,cAAc,GAAG;GAC7D,MAAM,aAAa,UAAU,QAAQ;GAKrC,IAAI,qBAAqB,YAAY,UAAU,WAAW,GAAG;GAG7D,MAAM,YADS,eAAe,UAAU,UACtB,CAAA,EAAQ;GAC1B,IAAI,cAAc,OAAO;GAEzB,MAAM,QAAQ,WAAW,YAAY;GACrC,MAAM,WACJ,OAAO,cAAc,WAAW,WAAW,UAAU,KAAA;GACvD,MAAM,WACJ,OAAO,cAAc,YAAY,WAAW,UACxC,UAAU,UACV,CAAC;GAEP,MAAM,UAAU,QACd,CAAC,SAAS,SAAS,GAAG,MAAM,CAAC,YAAY,SAAS,SAAS,GAAG;GAEhE,KAAK,MAAM,QAAQ,iBACjB,IAAI,OAAO,IAAI,GAAG,SAAS,KAAK,GAAG,MAAM,GAAG,MAAM;GAIpD,MAAM,gBAAgB,yBACpB,MAFoB,eAAe,cAAc,UAAU,GAG3D;IAAE,SAAS;IAAU,SAAS;GAAS,GACvC,eACF;GACA,KAAK,MAAM,cAAc,eACvB,SAAS,KAAK,GAAG,MAAM,GAAG,YAAY;EAE1C;EACA,OAAO,SAAS,KAAK;CACvB;CAEA,MAAc,WAA4B;EACxC,MAAM,WAAW,MAAM,KAAK,aAAa;EACzC,OAAO;GACL,GAAG,KAAK,OAAO,KAAK,GAAG,KAAK,OAAO;GACnC,KAAK,OAAO,eAAe;GAC3B;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,GAAG,SAAS,KAAK,MAAM,KAAK,GAAG;EACjC,CAAC,CAAC,KAAK,IAAI;CACb;CAEA,SACE,OACA,UACQ;EACR,IAAI,OAAO,UAAU,UAAU,OAAO;EACtC,MAAM,IAAI,OAAO,SAAS,OAAO,EAAE;EACnC,OAAO,OAAO,MAAM,CAAC,IAAI,WAAW;CACtC;CAEA,WAAmB,OAAyD;EAC1E,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;CAC7C;;CAGA,YAAoB,OAAkC;EACpD,IAAI,OAAO,UAAU,UAAU,OAAO;EACtC,IAAI,UAAU,QAAQ,OAAO;EAC7B,IAAI,UAAU,SAAS,OAAO;EAC9B,IAAI,UAAU,MAAM,CAAC,OAAO,MAAM,OAAO,KAAK,CAAC,GAAG,OAAO,OAAO,KAAK;EACrE,IACG,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAC3C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,GAE5C,IAAI;GACF,OAAO,KAAK,MAAM,KAAK;EACzB,QAAQ;GACN,OAAO;EACT;EAEF,OAAO;CACT;AACF;;;;;;;;;AAUA,SAAgB,SAAS,SAAoB,CAAC,GAAG,UAAsB,CAAC,GAAG;CACzE,MAAM,YAAY,IAAI,aAAa,QAAQ,OAAO;CAClD,OAAO;EACL,KAAK,OAAO,SAAmB;GAE7B,MADgB,UAAU,gBACpB,CAAA,CAAQ,KAAK,MAAM,CAAC,CAAC;EAC7B;EACA;CACF;AACF;;;;;;;;AASA,SAAgB,cACd,SAAoB,CAAC,GACrB,UAAsB,CAAC,GACvB;CAEA,OAAO,IADe,aAAa,QAAQ,OACpC,CAAA,CAAU,gBAAgB;AACnC"}
@@ -2,6 +2,222 @@ import { ToolEffect } from '../registry/types.js';
2
2
  import { MethodDefinition } from '../scanner/types.js';
3
3
  export type CustomActionScope = 'item' | 'collection';
4
4
  export type { ToolEffect } from '../registry/types.js';
5
+ /**
6
+ * Public method names declared directly on `SmrtObject`/`SmrtClass`
7
+ * (`src/object.ts`, `src/class.ts`) that form the constructor → `initialize()`
8
+ * → `save()`/`delete()`/`loadFromId()` lifecycle and its immediate supporting
9
+ * mechanism: identity/persistence bookkeeping, transaction binding, and
10
+ * serialization. `save()` is what generated `create`/`update` call;
11
+ * `initialize()` is what `get`/`list` hydration calls; `loadFromId()`/
12
+ * `loadFromSlug()` are what `get()` calls; `toJSON()`/`toPublicJSON()` are
13
+ * what every read serializes through. None of these are a subclass-specific
14
+ * operation, so none is exposed as a generated CLI/MCP custom action --
15
+ * even when a subclass declares its own override (e.g. `User.save()` at
16
+ * `packages/users/src/models/User.ts`). An override is still the same
17
+ * lifecycle operation, not a new one (#2638). `delete` itself is a CRUD verb
18
+ * `CLIGenerator`/`MCPGenerator` already special-case, so it is not repeated
19
+ * here.
20
+ *
21
+ * Scope is deliberately narrower than "every public method on
22
+ * SmrtObject/SmrtClass/SmrtCollection":
23
+ *
24
+ * - AI operations `is()`/`do()`/`describe()` are declared on `SmrtObject`
25
+ * but are explicitly designed to be overridden with domain-specific
26
+ * behavior and exposed as a distinct action -- confirmed by existing,
27
+ * intentional coverage (`generators/cli-commands.spec.ts`'s
28
+ * `describe()` custom-action fixture,
29
+ * `vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.
30
+ * describe(tone)` with its own declared API route). Excluding them here
31
+ * would regress real, working behavior.
32
+ * - Relationship loading (`loadRelated`/`loadRelatedMany`/`getRelated`/
33
+ * `isRelatedLoaded`), memory (`remember`/`recall`/`recallAll`/`forget`/
34
+ * `forgetScope`), embeddings (`generateEmbeddings`/`getEmbedding`/
35
+ * `hasStaleEmbeddings`/`clearEmbeddings`), and AI-usage introspection
36
+ * (`getAiUsageSnapshot`/`resetAiUsage`/`listAiUsage`/`summarizeAiUsage`)
37
+ * are generic capabilities a class may legitimately want to trigger or
38
+ * report on as a distinct action (e.g. "regenerate this record's
39
+ * embeddings"), not "the mechanism behind CRUD" the way `save`/
40
+ * `initialize` are. Nothing in the measured #2638 residual touches them,
41
+ * so blanket-excluding them is a separate, unevidenced call this fix does
42
+ * not make.
43
+ * - `SmrtCollection`'s own surface (`count`, `facets`, `query`, `findOne`,
44
+ * `generateMissingEmbeddings`, ...) is excluded entirely for the same
45
+ * reason: the measured residual is 100% item-class overrides (`User`,
46
+ * `Invoice`, `Payment`, ...), never a collection-class override, and
47
+ * `count`/`facets` read as genuinely distinct query capabilities in
48
+ * existing fixtures elsewhere in the repo (e.g.
49
+ * `packages/content/src/server/content-list-actions.test.ts`). Extending
50
+ * this rule to `SmrtCollection` needs its own review, not a ride-along
51
+ * here.
52
+ *
53
+ * This can only be a METHOD-NAME list, not a reuse of the existing
54
+ * class-name sets (`FRAMEWORK_METHOD_BASE_NAMES` in
55
+ * `scanner/manifest-generator.ts`, the registry's own by-name skip in
56
+ * `registry/inheritance-resolver.ts`, or the broader 8-class
57
+ * `FRAMEWORK_BASE_CLASSES` those two modules also cross-reference for the
58
+ * unrelated schema-field-merge question): those sets answer "is this
59
+ * ancestor one of the excluded classes", which only ever decides whether to
60
+ * MERGE an ancestor's methods onto a subclass that does not declare them.
61
+ * A locally declared override -- the #2638 case -- IS the subclass's own
62
+ * method; there is no ancestor lookup to skip, because the method that
63
+ * needs excluding was never inherited in the first place. The same
64
+ * plumbing-vs-operation judgment #2624 made at the class level has to be
65
+ * re-expressed as a method-name list to reach the override case too, which
66
+ * is why this is a fifth name list rather than a reuse of one of the four.
67
+ *
68
+ * A static, hand-maintained list (not runtime introspection of the actual
69
+ * `SmrtObject`/`SmrtClass` prototypes) matches the existing pattern for all
70
+ * four sibling lists above, and keeps this transport-neutral module usable
71
+ * from a pure manifest/AST build path (`vite-plugin/sveltekit-generator.ts`)
72
+ * that has no live class registry to introspect. Re-derive this list from
73
+ * those two files' own public method signatures if they change.
74
+ */
75
+ export declare const FRAMEWORK_LIFECYCLE_METHOD_NAMES: ReadonlySet<string>;
76
+ /**
77
+ * True when `methodName` is one of the universal `SmrtObject`/`SmrtClass`
78
+ * lifecycle methods above — the mechanism behind generated CRUD, never a
79
+ * subclass-specific custom action, even when the subclass declares its own
80
+ * override.
81
+ */
82
+ export declare function isFrameworkLifecycleMethod(methodName: string): boolean;
83
+ /** Minimal shape `resolveCustomActionNames` needs from a method entry. */
84
+ export interface ResolvableMethod {
85
+ isPublic: boolean;
86
+ }
87
+ /**
88
+ * Resolve the effective set of custom (non-CRUD) command/tool names exposed
89
+ * for an object, given its transport config's `include`/`exclude` and its
90
+ * method map: every public method, minus CRUD verbs, minus framework
91
+ * lifecycle methods, restricted to `include` when present and always minus
92
+ * `exclude`. Reused by three callers, so they agree with each other and
93
+ * with what `listCommands()` advertises:
94
+ *
95
+ * - `CLIGenerator.listCommands()` (over the live `ObjectRegistry`).
96
+ * - `findCliApiCoherenceViolations`'s bare-`cli: true`/`cli: {}` branch
97
+ * (over the static manifest, no explicit `include`) -- see
98
+ * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts`.
99
+ * - `generateCLIModule()` in `vite-plugin/index.ts`, which generates the
100
+ * `smrt:cli` virtual module's static command metadata. It layers one
101
+ * additional filter on top of this function's result: a leading `_` on
102
+ * the method name, because the manifest's `isPublic` is unreliable there
103
+ * (see that call site's own comment) and this function has no other
104
+ * signal to exclude an internal-by-convention method.
105
+ *
106
+ * NOT the one universal resolution, and deliberately not reused by every
107
+ * caller that resolves a CLI command set:
108
+ *
109
+ * - `CLIGenerator.assertCommandExposed()` does not call this for its custom-
110
+ * method branch — it checks `isFrameworkLifecycleMethod()` directly plus
111
+ * its own inline public/include/exclude logic, so it can give a distinct
112
+ * error message per failure reason (unknown vs. not public vs. not
113
+ * enabled vs. lifecycle method) rather than a single boolean membership
114
+ * test.
115
+ * - `findCliApiCoherenceViolations`'s EXPLICIT-`cli.include` branch
116
+ * deliberately bypasses this function too: an `include` entry naming a
117
+ * typo, a getter, or a private/protected method must still surface as
118
+ * "unreachable" at build time (the pre-#2638 behavior), and this function
119
+ * can only ever return names that exist in the manifest's `methods` map —
120
+ * it would silently drop such an entry instead of flagging it. See
121
+ * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts` for the
122
+ * full rationale; do not "simplify" that branch onto this function.
123
+ *
124
+ * `crudActionNames` stays a parameter because the callers do not agree on one
125
+ * list. `CLIGenerator` now passes {@link CRUD_OPERATIONS}, which #2646 moved
126
+ * into this module — so for that caller the argument is a round trip and could
127
+ * be defaulted. The SvelteKit generator still passes its own
128
+ * `STANDARD_API_ACTIONS` and `vite-plugin/index.ts` an inline literal, so the
129
+ * parameter cannot be removed until those copies are retired (#2665).
130
+ */
131
+ export declare function resolveCustomActionNames(methods: Iterable<[string, ResolvableMethod]>, config: {
132
+ include?: string[];
133
+ exclude?: string[];
134
+ } | undefined, crudActionNames: readonly string[]): Set<string>;
135
+ /**
136
+ * The CRUD verbs a generated surface emits directly. A method whose name
137
+ * collides with one of these may not be exposed as a custom action under that
138
+ * name: the generated operation already claims it, so a second command/tool
139
+ * would land on a name that is taken.
140
+ *
141
+ * This is a NAMESPACE rule, independent of where the method came from — a
142
+ * class's own `list()` collides exactly as a merged ancestor's does (#2646).
143
+ *
144
+ * What a collision means differs by EMITTER, so consult the one you are
145
+ * changing rather than assuming a single rule. The reservation lives at each
146
+ * emission site, not here, and several emitters still keep their own inline
147
+ * verb array — find them with:
148
+ *
149
+ * grep -rn "'update', 'delete'" packages --include='*.ts' | grep -v include:
150
+ *
151
+ * (the `include:` filter drops decorator config, which names the same verbs)
152
+ *
153
+ * The sites this rule was audited against (#2646), NOT an exhaustive
154
+ * inventory:
155
+ *
156
+ * - `generators/mcp.ts` — unconditional, case-folded. `executeAction` switches
157
+ * on the verb parsed out of the tool id, so `${object}_list` runs the
158
+ * built-in list whichever branch emitted it: the class's method could never
159
+ * run, and emitting one would hand the caller an operation `include` never
160
+ * named.
161
+ * - `generators/cli.ts` (`CLIGenerator`) — unconditional, exact.
162
+ * `assertCommandExposed` returns inside its `isCrud` branch, so a CRUD-named
163
+ * action never reaches custom-method resolution, and `listCommands` skips
164
+ * those names outright.
165
+ * - `packages/cli/src/cli-generator.ts`, behind the shipped `smrt` object
166
+ * commands — reserves only where the CRUD command is actually emitted, and
167
+ * reserves the command NAMES AND THEIR ALIASES (`ls`, `show`, `new`, `edit`,
168
+ * `rm`), because lookup matches aliases too (#2648). The set is derived from
169
+ * the commands actually pushed, so it cannot drift from those aliases. Each
170
+ * command carries its own handler invoking the class's method, so with
171
+ * `cli: { include: ['list', 'get'] }` neither a public `create()` nor an
172
+ * `edit()` collides with anything and both stay reachable.
173
+ * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, from its own
174
+ * `STANDARD_API_ACTIONS` copy.
175
+ * - `vite-plugin/web-collections.ts` + `tool-schema.ts` — case-folded (ids are
176
+ * lowercased whole like MCP's), CONDITIONAL, and scoped PER COLLECTION.
177
+ * Applied in both of `selectWebMcpToolEntries`' loops and at the shared
178
+ * `buildWebToolDescriptorsForHost` choke point, which the legacy
179
+ * per-collection descriptor export also passes through (#2648).
180
+ *
181
+ * Conditional, unlike `generators/mcp.ts`: MCP dispatch parses the verb out
182
+ * of the tool id, so `${obj}_list` can only ever run the built-in list, but a
183
+ * WebMCP descriptor carries its own `route` from `resolveApiActionRouteConfig`
184
+ * — with `api: { include: ['List'] }`, `product_list` dispatches to
185
+ * `/products/List` and is the only tool for that id, so reserving it would
186
+ * make a custom-action-only model undiscoverable.
187
+ *
188
+ * Per collection, not per host: the id prefix is the OWNER's `className`, so
189
+ * every host mapping to one collection shares the namespace. A host-local
190
+ * check lets a model that excludes `list` but declares `List()` claim
191
+ * `product_list`, after which a sibling's real `list` is dropped by the
192
+ * fold-dedupe. The emitted-verb set is the union over the collection's hosts.
193
+ *
194
+ * `resolveApiActionSet` stays exact-match: REST routes keep declared casing,
195
+ * so `/products/List` is genuinely distinct from `/products`.
196
+ *
197
+ * Still carrying their own verb copies, tracked on #2665:
198
+ * `vite-plugin/api-client-entries.ts`, `vite-plugin/index.ts` and
199
+ * `vite-plugin/templates/default-ui.ts`.
200
+ *
201
+ * Read the list through {@link isCrudOperation} or {@link isCrudToolAction}
202
+ * rather than re-declaring it; the two differ only in case folding, because the
203
+ * transports namespace differently (see below).
204
+ */
205
+ export declare const CRUD_OPERATIONS: readonly ["list", "get", "create", "update", "delete"];
206
+ /**
207
+ * Exact-match test for a case-SENSITIVE surface. The CLI keeps a method's
208
+ * declared casing in its command name (`${object}:${methodName}`) and resolves
209
+ * an action by exact match, so `foo:List` is a distinct, callable custom
210
+ * command that must not be folded into `foo:list`.
211
+ */
212
+ export declare function isCrudOperation(name: string): boolean;
213
+ /**
214
+ * Case-FOLDED test for a lowercase tool namespace. MCP tool identifiers are
215
+ * lowercased whole (`` `${object}_${methodName}`.toLowerCase() ``) for a stable
216
+ * protocol vocabulary, so a method named `List` lands on the identifier
217
+ * `object_list` that the generated CRUD tool already owns. The namespace key is
218
+ * the lowercased name, so the collision test has to be too (#2646).
219
+ */
220
+ export declare function isCrudToolAction(name: string): boolean;
5
221
  export interface CustomActionMetadata {
6
222
  scope: CustomActionScope;
7
223
  /** An item-targeted action requires its target identifier. */
@@ -1 +1 @@
1
- {"version":3,"file":"custom-action.d.ts","sourceRoot":"","sources":["../../src/generators/custom-action.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,YAAY,CAAC;AACtD,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,iBAAiB,CAAC;IACzB,8DAA8D;IAC9D,UAAU,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAC5C,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,+EAA+E;AAC/E,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAC7D,QAAQ,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EACnD,aAAa,EAAE,MAAM,GACpB,MAAM,CAER;AAED,MAAM,WAAW,kCAAkC;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;KAC7C,CAAC;IACF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAClC;AAED,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1C,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,4BAA4B,CAmC9B;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,oBAAoB,GAC7B,UAAU,CAuDZ;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,oBAAoB,EAC9B,IAAI,EAAE,QAAQ,GACb,OAAO,EAAE,CAwBX;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,gFAAgF;AAChF,eAAO,MAAM,qCAAqC,0BAA0B,CAAC;AAE7E;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,OAAO,GACb,mBAAmB,GAAG,SAAS,CA6BjC"}
1
+ {"version":3,"file":"custom-action.d.ts","sourceRoot":"","sources":["../../src/generators/custom-action.ts"],"names":[],"mappings":"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"}