@sap/cds-dk 10.0.7 → 10.1.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 (302) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/bin/add.js +7 -23
  3. package/bin/bind.js +10 -0
  4. package/bin/build.js +20 -34
  5. package/bin/cds.js +1 -1
  6. package/bin/compile/index.js +15 -41
  7. package/bin/completion/completionFs.js +11 -8
  8. package/bin/completion/index.js +592 -80
  9. package/bin/completion/scripts/cds.fish +568 -0
  10. package/bin/completion/scripts/cds.ps1 +329 -0
  11. package/bin/completion/scripts/cds.sh +261 -0
  12. package/bin/completion/scripts/cds.zsh +230 -0
  13. package/bin/completion/templates/cds-tmpl.fish +65 -0
  14. package/bin/completion/templates/cds-tmpl.ps1 +208 -0
  15. package/bin/completion/templates/cds-tmpl.sh +93 -0
  16. package/bin/completion/templates/cds-tmpl.zsh +74 -0
  17. package/bin/deploy/index.js +7 -1
  18. package/bin/deploy/options.js +25 -0
  19. package/{lib → bin}/deploy/to-hana/hana.js +8 -8
  20. package/{lib → bin}/deploy/to-hana/hdiDeployUtil.js +1 -1
  21. package/bin/deploy/{to-hana.js → to-hana/index.js} +1 -1
  22. package/{lib → bin}/deploy/to-hana/mtaUtil.js +1 -1
  23. package/bin/deploy/to-sql.js +0 -2
  24. package/bin/export.js +35 -5
  25. package/bin/help/export.md +3 -0
  26. package/bin/help.js +1 -5
  27. package/bin/import.js +6 -0
  28. package/bin/init.js +5 -24
  29. package/bin/inspect.js +6 -0
  30. package/bin/login.js +26 -20
  31. package/bin/pull.js +19 -6
  32. package/bin/push.js +19 -6
  33. package/bin/up.js +14 -11
  34. package/{lib → bin}/upgrade/constants.js +1 -1
  35. package/bin/{upgrade.js → upgrade/index.js} +2 -2
  36. package/{lib → bin}/upgrade/preflight/consistency.js +1 -1
  37. package/{lib → bin}/upgrade/preflight/index.js +1 -1
  38. package/{lib → bin}/upgrade/steps/analyze/index.js +1 -1
  39. package/{lib → bin}/upgrade/steps/install.js +1 -1
  40. package/{lib → bin}/upgrade/steps/openrewrite.js +1 -1
  41. package/{lib → bin}/upgrade/steps/removals.js +1 -1
  42. package/{lib → bin}/upgrade/steps/rewrite.js +1 -1
  43. package/{lib → bin}/upgrade/steps/verify.js +1 -1
  44. package/{lib/upgrade/index.js → bin/upgrade/upgrade.js} +2 -2
  45. package/{lib → bin}/upgrade/util/pom.js +2 -2
  46. package/{lib → bin}/upgrade/util/prompt.js +1 -1
  47. package/bin/util/containerize.js +8 -4
  48. package/bin/version.js +10 -1
  49. package/{lib → bin}/watch/hot-reload.js +1 -1
  50. package/bin/{watch.js → watch/index.js} +17 -9
  51. package/{lib → bin}/watch/livereload-connect.js +1 -1
  52. package/{lib → bin}/watch/vite-watch.js +1 -1
  53. package/{lib → bin}/watch/watched.js +2 -2
  54. package/lib/build/constants.js +1 -1
  55. package/lib/build/plugins/fiori/index.js +31 -2
  56. package/lib/build/plugins/internal.js +4 -2
  57. package/lib/build/plugins/mtx-sidecar/index.js +7 -8
  58. package/lib/build/plugins/nodejs/index.js +64 -7
  59. package/lib/compile/to_asyncapi.js +1 -1
  60. package/lib/env/schema.js +16 -0
  61. package/lib/env/schemas/cds-rc.js +1 -76
  62. package/lib/import/importapi.js +2 -3
  63. package/lib/import/message.js +0 -2
  64. package/lib/init/constants.js +2 -1
  65. package/lib/init/index.js +2 -2
  66. package/lib/init/merge.js +123 -280
  67. package/lib/init/plugin.js +3 -1
  68. package/lib/init/projectReader.js +22 -6
  69. package/lib/init/registries/xs-app.js +3 -5
  70. package/lib/init/template/ai/files/mta.yaml.hbs +1 -1
  71. package/lib/init/template/ai/index.js +14 -2
  72. package/lib/init/template/ams/index.js +2 -2
  73. package/lib/init/template/app-frontend/files/containerize.yaml.hbs +2 -2
  74. package/lib/init/template/app-frontend/index.js +4 -4
  75. package/lib/init/template/application-logging/index.js +2 -2
  76. package/lib/init/template/approuter/files/package.json +1 -1
  77. package/lib/init/template/approuter/index.js +8 -3
  78. package/lib/init/template/attachments/files/package.json.hbs +1 -1
  79. package/lib/init/template/attachments/index.js +2 -2
  80. package/lib/init/template/audit-logging/index.js +2 -2
  81. package/lib/init/template/cloud-logging/files/package.json.hbs +4 -4
  82. package/lib/init/template/cloud-logging/index.js +2 -2
  83. package/lib/init/template/completion/completionSetup.js +36 -39
  84. package/lib/init/template/connectivity/index.js +2 -2
  85. package/lib/init/template/data/as-csv.js +15 -5
  86. package/lib/init/template/data/as-json.js +114 -42
  87. package/lib/init/template/data/index.js +16 -2
  88. package/lib/init/template/destination/index.js +2 -2
  89. package/lib/init/template/dynatrace/files/package.json.hbs +3 -3
  90. package/lib/init/template/dynatrace/index.js +2 -2
  91. package/lib/init/template/enterprise-messaging/index.js +2 -2
  92. package/lib/init/template/enterprise-messaging-shared/index.js +2 -2
  93. package/lib/init/template/extensibility/files/mta.yaml.hbs +11 -0
  94. package/lib/init/template/extensibility/files/xs-security.json.hbs +1 -1
  95. package/lib/init/template/extensibility/index.js +3 -3
  96. package/lib/init/template/github-actions/index.js +2 -2
  97. package/lib/init/template/hana/index.js +3 -2
  98. package/lib/init/template/hana-serverless/files/db/package.json +14 -0
  99. package/lib/init/template/hana-serverless/files/identity.values.yaml.hbs +10 -0
  100. package/lib/init/template/hana-serverless/files/mta.yaml.hbs +64 -0
  101. package/lib/init/template/hana-serverless/files/package.json.hbs +35 -0
  102. package/lib/init/template/hana-serverless/files/src/.hdiconfig +139 -0
  103. package/lib/init/template/hana-serverless/files/undeploy.json +7 -0
  104. package/lib/init/template/hana-serverless/files/values.yaml.hbs +18 -0
  105. package/lib/init/template/hana-serverless/index.js +184 -0
  106. package/lib/init/template/html5-repo/files/containerize.yaml.hbs +2 -2
  107. package/lib/init/template/html5-repo/files/mta.yaml.hbs +5 -0
  108. package/lib/init/template/html5-repo/index.js +18 -14
  109. package/lib/init/template/http/index.js +123 -26
  110. package/lib/init/template/ias/index.js +2 -2
  111. package/lib/init/template/kafka/index.js +2 -2
  112. package/lib/init/template/kyma/index.js +7 -5
  113. package/lib/init/template/malware-scanner/index.js +2 -2
  114. package/lib/init/template/mta/files/mta.yaml.hbs +1 -1
  115. package/lib/init/template/multitenancy/index.js +18 -3
  116. package/lib/init/template/notifications/index.js +2 -2
  117. package/lib/init/template/portal/files/mta.yaml.hbs +4 -0
  118. package/lib/init/template/portal/index.js +13 -8
  119. package/lib/init/template/postgres/index.js +2 -2
  120. package/lib/init/template/redis/index.js +2 -2
  121. package/lib/init/template/sample/files/db/currencies.cds +2 -0
  122. package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Authors.csv +1 -1
  123. package/lib/init/template/tiny-sample/files/nodejs/db/data/CatalogService.Books.csv +2 -2
  124. package/lib/init/template/workzone-standard/index.js +5 -5
  125. package/lib/init/template/xsuaa/index.js +20 -2
  126. package/lib/lint/eslintHelp.json +1 -1
  127. package/lib/mtx/auth_manager.js +103 -37
  128. package/lib/mtx/params.js +3 -2
  129. package/lib/mtx/settings_manager.js +45 -28
  130. package/lib/mtx/tenant_action.js +0 -1
  131. package/lib/mtx/util/credentials.js +62 -0
  132. package/node_modules/@sap/cds/CHANGELOG.md +57 -0
  133. package/node_modules/@sap/cds/app/index.js +5 -4
  134. package/node_modules/@sap/cds/eslint.config.mjs +1 -0
  135. package/node_modules/@sap/cds/lib/compile/minify.js +14 -10
  136. package/node_modules/@sap/cds/lib/env/cds-env.js +1 -1
  137. package/node_modules/@sap/cds/lib/env/cds-requires.js +22 -11
  138. package/node_modules/@sap/cds/lib/env/defaults.js +4 -1
  139. package/node_modules/@sap/cds/lib/env/schemas/cds-rc.js +5 -2
  140. package/node_modules/@sap/cds/lib/plugins.js +9 -5
  141. package/node_modules/@sap/cds/lib/req/spawn.js +1 -1
  142. package/node_modules/@sap/cds/lib/srv/srv-dispatch.js +1 -1
  143. package/node_modules/@sap/cds/lib/srv/srv-models.js +1 -1
  144. package/node_modules/@sap/cds/lib/utils/cds-utils.js +2 -1
  145. package/node_modules/@sap/cds/libx/_runtime/common/generic/assert.js +2 -3
  146. package/node_modules/@sap/cds/libx/_runtime/common/generic/auth/restrict.js +8 -1
  147. package/node_modules/@sap/cds/libx/_runtime/common/generic/crud.js +18 -8
  148. package/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js +25 -17
  149. package/node_modules/@sap/cds/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +9 -3
  150. package/node_modules/@sap/cds/libx/_runtime/remote/Service.js +4 -19
  151. package/node_modules/@sap/cds/libx/_runtime/remote/utils/LruCache.js +42 -0
  152. package/node_modules/@sap/cds/libx/_runtime/remote/utils/client.js +14 -4
  153. package/node_modules/@sap/cds/libx/_runtime/remote/utils/cloudSdkProvider.js +2 -9
  154. package/node_modules/@sap/cds/libx/_runtime/remote/utils/destinationClient.js +150 -0
  155. package/node_modules/@sap/cds/libx/_runtime/remote/utils/error.js +13 -0
  156. package/node_modules/@sap/cds/libx/_runtime/remote/utils/fetchClient.js +26 -18
  157. package/node_modules/@sap/cds/libx/_runtime/remote/utils/query.js +34 -23
  158. package/node_modules/@sap/cds/libx/_runtime/remote/utils/token.js +82 -0
  159. package/node_modules/@sap/cds/libx/_runtime/ucl/Service.js +36 -189
  160. package/node_modules/@sap/cds/libx/common/utils/streaming.js +16 -16
  161. package/node_modules/@sap/cds/libx/http/location.js +3 -3
  162. package/node_modules/@sap/cds/libx/odata/middleware/batch.js +16 -10
  163. package/node_modules/@sap/cds/libx/odata/middleware/metadata.js +9 -6
  164. package/node_modules/@sap/cds/libx/odata/middleware/service-document.js +1 -1
  165. package/node_modules/@sap/cds/libx/odata/parse/afterburner.js +19 -3
  166. package/node_modules/@sap/cds/libx/odata/parse/cqn2odata.js +83 -86
  167. package/node_modules/@sap/cds/libx/queue/SchedulingService.js +17 -2
  168. package/node_modules/@sap/cds/libx/queue/processing.js +1 -3
  169. package/node_modules/@sap/cds/libx/rest/RestAdapter.js +3 -7
  170. package/node_modules/@sap/cds/libx/rest/middleware/delete.js +3 -3
  171. package/node_modules/@sap/cds/libx/rest/middleware/parse.js +13 -8
  172. package/node_modules/@sap/cds/libx/rest/middleware/read.js +3 -3
  173. package/node_modules/@sap/cds/libx/rest/middleware/upsert.js +2 -2
  174. package/node_modules/@sap/cds/package.json +1 -1
  175. package/node_modules/@sap/cds-compiler/CHANGELOG.md +60 -8
  176. package/node_modules/@sap/cds-compiler/bin/cdsc.js +2 -2
  177. package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +46 -19
  178. package/node_modules/@sap/cds-compiler/lib/api/main.js +10 -7
  179. package/node_modules/@sap/cds-compiler/lib/api/options.js +8 -2
  180. package/node_modules/@sap/cds-compiler/lib/base/csnRefs.js +5 -2
  181. package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +4 -0
  182. package/node_modules/@sap/cds-compiler/lib/base/optionProcessor.js +12 -0
  183. package/node_modules/@sap/cds-compiler/lib/checks/enricher.js +1 -0
  184. package/node_modules/@sap/cds-compiler/lib/compiler/assert-consistency.js +8 -7
  185. package/node_modules/@sap/cds-compiler/lib/compiler/checks.js +3 -3
  186. package/node_modules/@sap/cds-compiler/lib/compiler/define.js +301 -75
  187. package/node_modules/@sap/cds-compiler/lib/compiler/dictionaries.js +2 -0
  188. package/node_modules/@sap/cds-compiler/lib/compiler/extend.js +40 -26
  189. package/node_modules/@sap/cds-compiler/lib/compiler/populate.js +45 -60
  190. package/node_modules/@sap/cds-compiler/lib/compiler/propagator.js +16 -7
  191. package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +41 -15
  192. package/node_modules/@sap/cds-compiler/lib/compiler/shared.js +7 -6
  193. package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +62 -50
  194. package/node_modules/@sap/cds-compiler/lib/compiler/utils.js +29 -14
  195. package/node_modules/@sap/cds-compiler/lib/edm/edmAnnoPreprocessor.js +6 -5
  196. package/node_modules/@sap/cds-compiler/lib/gen/CdlGrammar.checksum +1 -1
  197. package/node_modules/@sap/cds-compiler/lib/gen/CdlParser.js +1119 -1109
  198. package/node_modules/@sap/cds-compiler/lib/gen/Dictionary.json +19 -3
  199. package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +10 -16
  200. package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +58 -39
  201. package/node_modules/@sap/cds-compiler/lib/main.d.ts +8 -0
  202. package/node_modules/@sap/cds-compiler/lib/main.js +1 -0
  203. package/node_modules/@sap/cds-compiler/lib/model/csnUtils.js +2 -0
  204. package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +2 -1
  205. package/node_modules/@sap/cds-compiler/lib/parsers/XprTree.js +8 -0
  206. package/node_modules/@sap/cds-compiler/lib/render/toCdl.js +22 -1
  207. package/node_modules/@sap/cds-compiler/lib/render/toSql.js +17 -2
  208. package/node_modules/@sap/cds-compiler/lib/render/utils/common.js +6 -1
  209. package/node_modules/@sap/cds-compiler/lib/render/utils/operators.js +27 -0
  210. package/node_modules/@sap/cds-compiler/lib/render/utils/standardDatabaseFunctions.js +125 -3
  211. package/node_modules/@sap/cds-compiler/lib/tool-lib/revealInternalProperties.js +2 -1
  212. package/node_modules/@sap/cds-compiler/lib/transform/db/applyTransformations.js +3 -0
  213. package/node_modules/@sap/cds-compiler/lib/transform/db/associations.js +8 -2
  214. package/node_modules/@sap/cds-compiler/lib/transform/db/backlinks.js +22 -6
  215. package/node_modules/@sap/cds-compiler/lib/transform/db/constraints.js +62 -1
  216. package/node_modules/@sap/cds-compiler/lib/transform/effective/annotations.js +40 -0
  217. package/node_modules/@sap/cds-compiler/lib/transform/effective/main.js +1 -0
  218. package/node_modules/@sap/cds-compiler/lib/transform/forRelationalDB.js +6 -1
  219. package/node_modules/@sap/cds-compiler/lib/transform/translateAssocsToJoins.js +19 -8
  220. package/node_modules/@sap/cds-compiler/package.json +1 -1
  221. package/node_modules/@sap/cds-mtxs/CHANGELOG.md +15 -0
  222. package/node_modules/@sap/cds-mtxs/bin/cds-mtx.js +34 -16
  223. package/node_modules/@sap/cds-mtxs/lib/utils.js +19 -4
  224. package/node_modules/@sap/cds-mtxs/package.json +1 -1
  225. package/node_modules/@sap/cds-mtxs/srv/cf/abstract-provisioning-service.js +1 -1
  226. package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +22 -13
  227. package/node_modules/@sap/cds-mtxs/srv/model-provider.js +17 -2
  228. package/node_modules/@sap/cds-mtxs/srv/plugins/hana/ctnr-mgr-base.js +1 -1
  229. package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +4 -8
  230. package/node_modules/@sap/xsenv/CHANGELOG.md +7 -0
  231. package/node_modules/@sap/xsenv/package.json +10 -7
  232. package/node_modules/body-parser/node_modules/content-type/README.md +2 -0
  233. package/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +20 -0
  234. package/node_modules/body-parser/node_modules/content-type/dist/index.js +20 -14
  235. package/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -1
  236. package/node_modules/body-parser/node_modules/content-type/package.json +1 -1
  237. package/node_modules/hdb/cfg/VERSION +1 -1
  238. package/node_modules/hdb/lib/protocol/ClientInfo.js +16 -3
  239. package/node_modules/hdb/lib/protocol/Connection.js +1 -0
  240. package/node_modules/hdb/package.json +3 -2
  241. package/node_modules/negotiator/lib/accept.js +54 -0
  242. package/node_modules/negotiator/lib/charset.js +10 -34
  243. package/node_modules/negotiator/lib/encoding.js +9 -30
  244. package/node_modules/negotiator/lib/language.js +15 -36
  245. package/node_modules/negotiator/lib/mediaType.js +16 -134
  246. package/node_modules/negotiator/node_modules/content-type/LICENSE +22 -0
  247. package/node_modules/negotiator/node_modules/content-type/README.md +71 -0
  248. package/node_modules/negotiator/node_modules/content-type/dist/index.d.ts +46 -0
  249. package/node_modules/negotiator/node_modules/content-type/dist/index.js +176 -0
  250. package/node_modules/negotiator/node_modules/content-type/dist/index.js.map +1 -0
  251. package/node_modules/negotiator/node_modules/content-type/package.json +52 -0
  252. package/node_modules/negotiator/package.json +16 -10
  253. package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  254. package/node_modules/qs/CHANGELOG.md +15 -0
  255. package/node_modules/qs/README.md +19 -1
  256. package/node_modules/qs/dist/qs.js +21 -21
  257. package/node_modules/qs/eslint.config.mjs +1 -1
  258. package/node_modules/qs/lib/parse.js +3 -4
  259. package/node_modules/qs/lib/stringify.js +23 -8
  260. package/node_modules/qs/lib/utils.js +10 -3
  261. package/node_modules/qs/package.json +4 -4
  262. package/node_modules/qs/test/parse.js +192 -3
  263. package/node_modules/qs/test/stringify.js +317 -0
  264. package/node_modules/qs/test/utils.js +12 -0
  265. package/node_modules/type-is/node_modules/content-type/README.md +2 -0
  266. package/node_modules/type-is/node_modules/content-type/dist/index.d.ts +20 -0
  267. package/node_modules/type-is/node_modules/content-type/dist/index.js +20 -14
  268. package/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -1
  269. package/node_modules/type-is/node_modules/content-type/package.json +1 -1
  270. package/node_modules/ws/lib/permessage-deflate.js +3 -1
  271. package/node_modules/ws/package.json +1 -1
  272. package/package.json +3 -3
  273. package/bin/completion/scripts/cds-bash.sh +0 -20
  274. package/bin/completion/scripts/cds-fish.fish +0 -13
  275. package/bin/completion/scripts/cds-ps.ps1 +0 -19
  276. package/bin/completion/scripts/cds-zsh.sh +0 -13
  277. package/lib/deploy/index.js +0 -25
  278. package/lib/import/asyncapi/importAsyncAPI.js +0 -33
  279. package/lib/import/asyncapi/index.js +0 -18
  280. package/lib/import/asyncapi/parserContext.js +0 -454
  281. package/lib/import/asyncapi/utils.js +0 -49
  282. package/node_modules/@sap/cds/libx/_runtime/ucl/queries.js +0 -61
  283. package/node_modules/negotiator/HISTORY.md +0 -114
  284. /package/{lib → bin}/upgrade/context.js +0 -0
  285. /package/{lib → bin}/upgrade/migrations/cds10/migrations.json +0 -0
  286. /package/{lib → bin}/upgrade/preflight/cds-version.js +0 -0
  287. /package/{lib → bin}/upgrade/preflight/project-type.js +0 -0
  288. /package/{lib → bin}/upgrade/steps/analyze/classification.js +0 -0
  289. /package/{lib → bin}/upgrade/steps/analyze/detector.js +0 -0
  290. /package/{lib → bin}/upgrade/steps/analyze/evaluator.js +0 -0
  291. /package/{lib → bin}/upgrade/steps/analyze/reporter.js +0 -0
  292. /package/{lib → bin}/upgrade/steps/analyze/setup.js +0 -0
  293. /package/{lib → bin}/upgrade/steps/analyze/utils/detection.js +0 -0
  294. /package/{lib → bin}/upgrade/steps/analyze/utils/files.js +0 -0
  295. /package/{lib → bin}/upgrade/steps/analyze/utils/grep.js +0 -0
  296. /package/{lib → bin}/upgrade/steps/analyze/utils/yaml.js +0 -0
  297. /package/{lib → bin}/upgrade/util/workspaces.js +0 -0
  298. /package/{lib → bin}/watch/esm-resolve-registration.js +0 -0
  299. /package/{lib → bin}/watch/livereload-server.js +0 -0
  300. /package/{lib → bin}/watch/node-watch.js +0 -0
  301. /package/{lib → bin}/watch/open.js +0 -0
  302. /package/{lib → bin}/watch/watcher.js +0 -0
@@ -1,15 +1,31 @@
1
+ const fs = require('node:fs');
1
2
  const path = require('node:path');
2
3
 
3
4
  const cds = require('../../bin/cds');
4
5
  const { bold } = require('../../lib/util/term');
6
+ const completionFs = require('./completionFs');
7
+ const completionSetup = require('../../lib/init/template/completion/completionSetup');
5
8
 
9
+ // Sentinel factories — returned by staticCompletions() to mark special argument types.
10
+ const FILE = (exts) => exts ? { type: ':file', ext: exts.split(',').map(e => e.trim()) } : ':file';
11
+ const DIR = () => ':dir';
12
+ const COMMA = (values) => ({ type: ':comma', values });
6
13
 
14
+ // The '_' key in a staticCompletions map marks the default positional argument.
15
+ const POSITIONAL_KEY = '_';
16
+
17
+ const isFileSentinel = s => s === ':file' || s === ':dir' || (s?.type === ':file' && Array.isArray(s.ext));
18
+ const fileRegex = s => s.ext ? new RegExp(`\\.(${s.ext.join('|')})$`) : null;
19
+ const filePs1 = s => s.ext ? `':file:${s.ext.join(',')}'` : `'${s}'`;
7
20
 
8
21
  module.exports = Object.assign(completion, {
9
- handleCompletion,
22
+ staticCompletions: () => ({ '--shell': completionSetup.supportedShells }),
23
+ FILE,
24
+ DIR,
25
+ COMMA,
10
26
  options: ['--shell', '--curr', '--line', '--prev'],
11
27
  shortcuts: ['-s'],
12
- flags: ['--add', '--profile', '--remove'],
28
+ flags: ['--add', '--profile', '--generate', '--remove'],
13
29
  help: `
14
30
  # SYNOPSIS
15
31
 
@@ -41,6 +57,13 @@ module.exports = Object.assign(completion, {
41
57
  *--remove*
42
58
  Remove shell completion for cds commands (see examples).
43
59
 
60
+ *--profile* _(deprecated)_
61
+ Print the path to the completion script for the given shell.
62
+
63
+ *--generate*
64
+ Generate a static completion script for the given shell and print it to stdout.
65
+ Used by 'npm run build:completion' to pre-build the bundled cache files.
66
+
44
67
  *--shell* <bash | fish | gitbash | ps | zsh>
45
68
  Force the shell completion code to be added to the correct shell profile file.
46
69
  bash: macOS, Linux, WSL
@@ -65,22 +88,13 @@ module.exports = Object.assign(completion, {
65
88
  function getScriptFile(shell) {
66
89
  switch (shell) {
67
90
  case 'bash':
68
- case 'gitbash':
69
- return 'cds-bash.sh';
70
- case 'fish':
71
- return 'cds-fish.fish';
72
- case 'ps':
73
- return 'cds-ps.ps1';
74
- case 'zsh':
75
- return 'cds-zsh.sh';
76
- case '':
77
- case undefined:
78
- case null:
79
- throw 'Internal error: option --shell is missing';
80
- default: {
81
- const completionSetup = require('../../lib/init/template/completion/completionSetup');
91
+ case 'gitbash': return 'cds.sh';
92
+ case 'fish': return 'cds.fish';
93
+ case 'ps': return 'cds.ps1';
94
+ case 'zsh': return 'cds.zsh';
95
+ default:
96
+ if (!shell) throw 'Internal error: option --shell is missing';
82
97
  throw `Internal error: unsupported shell: ${shell}, supported shells are ${completionSetup.supportedShells.join(', ')}`;
83
- }
84
98
  }
85
99
  }
86
100
 
@@ -90,7 +104,24 @@ async function completion(_argv, options) {
90
104
  return console.log(path.join(__dirname, 'scripts', scriptFile));
91
105
  }
92
106
 
93
- const completionSetup = require('../../lib/init/template/completion/completionSetup');
107
+ if (options.generate) {
108
+ if (options.shell) {
109
+ const content = await generateStaticScript(options.shell);
110
+ return content ? process.stdout.write(content) : undefined;
111
+ }
112
+ const shells = { sh: 'bash', zsh: 'zsh', fish: 'fish', ps1: 'ps' };
113
+ const scriptsDir = path.join(__dirname, 'scripts');
114
+ fs.mkdirSync(scriptsDir, { recursive: true });
115
+ for (const [ext, shell] of Object.entries(shells)) {
116
+ const content = await generateStaticScript(shell);
117
+ if (content) {
118
+ fs.writeFileSync(path.join(scriptsDir, `cds.${ext}`), content);
119
+ console.log(`Generated cds.${ext}`);
120
+ }
121
+ }
122
+ return;
123
+ }
124
+
94
125
  if (options.add || options.remove) {
95
126
  await completionSetup.setup(options.add, options);
96
127
  return;
@@ -113,124 +144,605 @@ async function completion(_argv, options) {
113
144
  throw `internal Error: no completion handler found for shell type: ${shell}, supported shells are ${completionSetup.supportedShells.join(', ')}`;
114
145
  }
115
146
 
116
- const currentWord = localOptions.curr?.trim().replace(/^'|'$/g, '');
147
+ const currentWord = localOptions.curr?.trim().replace(/^'|'$/g, '');
117
148
  const previousWord = localOptions.prev?.trim().replace(/^'|'$/g, '');
118
- const line = localOptions.line?.trim().replace(/^'|'$/g, '');
119
- const segments = getSegments(line);
120
- const cdsCmd = segments[1] || '';
149
+ const line = localOptions.line?.trim().replace(/^'|'$/g, '');
150
+ const segments = getSegments(line);
151
+ const cdsCmd = segments[1] || '';
121
152
 
122
153
  const completions = await getCompletions(cdsCmd, previousWord, currentWord, segments);
123
154
  reply(completions);
124
155
  }
125
156
 
126
- async function handleCompletion(currentWord, previousWord, argv) {
127
- const allOptionsFlags = ['--add', '--remove', '--shell']
128
- .filter(e => !argv.includes(e));
157
+ async function buildCompletionMap() {
158
+ const commands = cds.cmds ?? [];
159
+ const commandMap = {};
160
+ for (const cmd of commands) {
161
+ try {
162
+ const obj = cds.load(cmd);
163
+ const raw = await obj.staticCompletions?.() ?? [];
164
+ let positionals = [], optionValues = {}, fileCases = {}, commaCases = {};
165
+ if (Array.isArray(raw)) {
166
+ positionals = raw;
167
+ } else {
168
+ for (const [key, val] of Object.entries(raw)) {
169
+ const resolved = await val;
170
+ if (resolved?.type === ':comma') {
171
+ commaCases[key] = await resolved.values;
172
+ } else if (isFileSentinel(resolved)) {
173
+ fileCases[key] = resolved;
174
+ } else {
175
+ optionValues[key] = resolved;
176
+ }
177
+ }
178
+ }
179
+ commandMap[cmd] = {
180
+ options: (obj.options ?? []).filter(Boolean),
181
+ flags: (obj.flags ?? []).filter(Boolean),
182
+ shortcuts: (obj.shortcuts ?? []).filter(Boolean),
183
+ positionals,
184
+ optionValues,
185
+ fileCases,
186
+ commaCases,
187
+ };
188
+ } catch (e) { cds.debug?.('completion')?.(`skipping command '${cmd}': ${e?.message ?? e}`); }
189
+ }
190
+ return { commands, shortcuts: cds.Shortcuts, commandMap };
191
+ }
129
192
 
130
- if (currentWord?.startsWith('-')) {
131
- return allOptionsFlags;
193
+ async function generateStaticScript(shell) {
194
+ const map = await buildCompletionMap();
195
+ switch (shell) {
196
+ case 'bash':
197
+ case 'gitbash': return generateBash(map);
198
+ case 'zsh': return generateZsh(map);
199
+ case 'fish': return generateFish(map);
200
+ case 'ps': return generatePs(map);
201
+ default:
202
+ throw `Internal error: unsupported shell: ${shell}`;
132
203
  }
204
+ }
205
+
206
+ // --- Code generation helpers ---
207
+
208
+ // Serialises an array of strings for embedding in a shell word-list (e.g. compgen -W "...").
209
+ function shellQuoteList(arr) {
210
+ return arr.map(s => JSON.stringify(s)).join(' ');
211
+ }
212
+
213
+ // Returns the single-char shortcuts from the global shortcuts map.
214
+ function singleCharShortcuts(shortcuts) {
215
+ return Object.entries(shortcuts).filter(([k]) => /^[a-z]$/.test(k));
216
+ }
217
+
218
+ // Returns the merged, deduplicated, sorted list of all flags/options for a command.
219
+ function sortedFlags(e) {
220
+ return [...new Set([...e.options, ...e.flags])].sort();
221
+ }
222
+
223
+ // Generates the bash/zsh case block that resolves single-char shortcuts to canonical names.
224
+ function shortcutCaseBlock(shortcuts, indent = ' ') {
225
+ const entries = singleCharShortcuts(shortcuts)
226
+ .map(([k, v]) => `${indent} ${k}) cmd=${v} ;;`)
227
+ .join('\n');
228
+ if (!entries) return '';
229
+ return `${indent}case "$cmd" in\n${entries}\n${indent}esac\n`;
230
+ }
231
+
232
+ // Bash: expression that completes to files matching a sentinel's extensions.
233
+ function bashFileExpr(sentinel) {
234
+ if (!sentinel.ext) return 'compgen -f -- "$current"';
235
+ return `compgen -f -- "$current" | grep -E '\\.(${sentinel.ext.join('|')})$'`;
236
+ }
237
+
238
+ // Zsh: compadd call that completes to files matching a sentinel's extensions.
239
+ function zshFileComp(sentinel) {
240
+ if (!sentinel.ext) return '_files';
241
+ return `_files -g '(${sentinel.ext.map(e => `*.${e}`).join('|')})'`;
242
+ }
243
+
244
+ // Fish: expression that completes to files matching a sentinel's extensions.
245
+ function fishFileComp(sentinel) {
246
+ if (!sentinel.ext) return '__fish_complete_path';
247
+ return `string match -r '\\.(${sentinel.ext.join('|')})\\$' -- (__fish_complete_path)`;
248
+ }
249
+
250
+ // Reads a shell template and substitutes {{PLACEHOLDER}} tokens.
251
+ // Note: replacement strings go through String.replaceAll, so '$' must be doubled
252
+ // ('$$$$' → '$$' → '$') to prevent JS from interpreting '$1' etc. as back-references.
253
+ async function applyTemplate(templateFile, replacements) {
254
+ const tmpl = await fs.promises.readFile(path.join(__dirname, 'templates', templateFile), 'utf8');
255
+ return Object.entries(replacements).reduce(
256
+ (s, [k, v]) => s.replaceAll(`{{${k}}}`, v.replaceAll('$', '$$$$')),
257
+ tmpl
258
+ );
259
+ }
260
+
261
+ // --- Per-shell case-block generators ---
262
+
263
+ // Bash/zsh: option-value completions (previous word is a known flag → offer its values).
264
+ function optionValueCases(commandMap, emitLine) {
265
+ return Object.entries(commandMap)
266
+ .map(([cmd, e]) => {
267
+ const innerLines = [];
268
+ for (const [flag, val] of Object.entries(e.optionValues ?? {})) {
269
+ innerLines.push(emitLine(flag, val, null));
270
+ }
271
+ for (const [flag, sentinel] of Object.entries(e.fileCases ?? {})) {
272
+ if (flag === POSITIONAL_KEY) continue;
273
+ innerLines.push(emitLine(flag, null, sentinel));
274
+ }
275
+ if (!innerLines.length) return '';
276
+ return ` ${cmd})\n case "$previous" in\n${innerLines.join('\n')}\n esac ;;\n`;
277
+ })
278
+ .filter(Boolean)
279
+ .join('');
280
+ }
281
+
282
+ function bashOptionValueCases(commandMap) {
283
+ return optionValueCases(commandMap, (flag, val, sentinel) => {
284
+ const expr = sentinel
285
+ ? (sentinel === DIR() ? 'compgen -d -- "$current"' : bashFileExpr(sentinel))
286
+ : `compgen -W "${shellQuoteList(val)}" -- "$current"`;
287
+ return ` ${flag}) COMPREPLY=( $(${expr}) ) ; return 0 ;;`;
288
+ });
289
+ }
290
+
291
+ function zshOptionValueCases(commandMap) {
292
+ return optionValueCases(commandMap, (flag, val, sentinel) => {
293
+ const comp = sentinel
294
+ ? (sentinel === DIR() ? '_files -/' : zshFileComp(sentinel))
295
+ : `compadd -S '' -- ${shellQuoteList([...val].sort())}`;
296
+ return ` ${flag}) ${comp} ; return ;;`;
297
+ });
298
+ }
299
+
300
+ // Bash/zsh: comma-separated option completions.
301
+ // Entries keyed by POSITIONAL_KEY match any previous word (when user didn't type a flag);
302
+ // a guard skips them if the previous word is itself a flag (consuming an option value).
303
+ function bashCommaCases(commandMap) {
304
+ const S = '$'; // prevent JS template literal from interpolating shell variables
305
+ return Object.entries(commandMap)
306
+ .map(([cmd, e]) => {
307
+ // Sort so POSITIONAL_KEY ('_') entries come last — they use a catch-all pattern.
308
+ const entries = Object.entries(e.commaCases ?? {}).sort(([a], [b]) =>
309
+ a === POSITIONAL_KEY ? 1 : b === POSITIONAL_KEY ? -1 : 0
310
+ );
311
+ if (!entries.length) return '';
312
+ const innerLines = entries.map(([flag, val]) => {
313
+ const list = shellQuoteList(val);
314
+ const pattern = flag === POSITIONAL_KEY ? '*' : flag;
315
+ // For positional entries: skip if previous word looks like a flag (it's consuming a value).
316
+ const guard = flag === POSITIONAL_KEY
317
+ ? ` [[ "$previous" == -* ]] && return 0\n`
318
+ : '';
319
+ const body = [
320
+ guard + ` local prefix="" partial="" IFS=,`,
321
+ ` if [[ "$current" == *,* ]]; then`,
322
+ ` prefix="` + S + `{current%,*},"`,
323
+ ` partial="` + S + `{current##*,}"`,
324
+ ` else`,
325
+ ` partial="$current"`,
326
+ ` fi`,
327
+ ` local -a _used`,
328
+ ` read -ra _used <<< "` + S + `{prefix%,}"`,
329
+ ` local _all=(${list})`,
330
+ ` for v in "` + S + `{_all[@]}"; do`,
331
+ ` [[ " ` + S + `{_used[*]} " == *" $v "* ]] && continue`,
332
+ ` [[ "$v" == "$partial"* ]] && COMPREPLY+=("$prefix$v")`,
333
+ ` done`,
334
+ ` [[ ` + S + `{#COMPREPLY[@]} -gt 0 ]] && { compopt -o nospace; return 0; } ;;`,
335
+ ].join('\n');
336
+ return ` ${pattern})\n${body}`;
337
+ });
338
+ return ` ${cmd})\n case "$previous" in\n${innerLines.join('\n')}\n esac ;;\n`;
339
+ })
340
+ .filter(Boolean)
341
+ .join('');
342
+ }
343
+
344
+ function zshCommaCases(commandMap) {
345
+ const S = '$'; // prevent JS template literal from interpolating shell variables
346
+ return Object.entries(commandMap)
347
+ .map(([cmd, e]) => {
348
+ const entries = Object.entries(e.commaCases ?? {}).sort(([a], [b]) =>
349
+ a === POSITIONAL_KEY ? 1 : b === POSITIONAL_KEY ? -1 : 0
350
+ );
351
+ if (!entries.length) return '';
352
+ const innerLines = entries.map(([flag, val]) => {
353
+ const list = shellQuoteList(val);
354
+ const pattern = flag === POSITIONAL_KEY ? `*)` : `${flag})`;
355
+ // For positional entries: skip if previous word looks like a flag (it's consuming a value).
356
+ const guard = flag === POSITIONAL_KEY
357
+ ? ` [[ "$previous" == -* ]] && return\n`
358
+ : '';
359
+ const body = [
360
+ guard + ` local -a _used=("` + S + `{(s:,:)current}")`,
361
+ ` local prefix="` + S + `{(j:,:)_used[1,-2]}" partial="` + S + `{_used[-1]}"`,
362
+ ` [[ -n "$prefix" ]] && prefix="$prefix,"`,
363
+ ` local -a _all=(${list}) _result=()`,
364
+ ` for v in "` + S + `{_all[@]}"; do`,
365
+ ` (( ` + S + `{_used[(Ie)$v]} )) && continue`,
366
+ ` [[ "$v" == "$partial"* ]] && _result+=("$v")`,
367
+ ` done`,
368
+ ` compadd -p "$prefix" -S '' -- "` + S + `{_result[@]}" ; return ;;`,
369
+ ].join('\n');
370
+ return ` ${pattern}\n${body}`;
371
+ });
372
+ return ` ${cmd})\n case "$previous" in\n${innerLines.join('\n')}\n esac ;;\n`;
373
+ })
374
+ .filter(Boolean)
375
+ .join('');
376
+ }
133
377
 
134
- if (previousWord === '--shell') {
135
- const { supportedShells } = require('../../lib/init/template/completion/completionSetup');
136
- return supportedShells;
378
+ // --- Static script generators ---
379
+
380
+ async function generateBash(map) {
381
+ const { commands, shortcuts, commandMap } = map;
382
+
383
+ const flagCases = Object.entries(commandMap)
384
+ .map(([cmd, e]) => {
385
+ const flags = sortedFlags(e);
386
+ if (!flags.length) return '';
387
+ return ` ${cmd}) COMPREPLY=( $(compgen -W "${shellQuoteList(flags)}" -- "$current") ) ;;`;
388
+ })
389
+ .filter(Boolean)
390
+ .join('\n');
391
+
392
+ const fileCases = Object.entries(commandMap)
393
+ .map(([cmd, e]) => {
394
+ const sentinel = e.fileCases?.[POSITIONAL_KEY];
395
+ if (!sentinel) return '';
396
+ const expr = sentinel === DIR() ? 'compgen -d -- "$current"' : bashFileExpr(sentinel);
397
+ return ` ${cmd}) COMPREPLY=( $(${expr}) ) ;;`;
398
+ })
399
+ .filter(Boolean)
400
+ .join('\n');
401
+
402
+ const stringPositionalCases = Object.entries(commandMap)
403
+ .map(([cmd, e]) => {
404
+ if (e.fileCases?.[POSITIONAL_KEY] || !e.positionals?.length) return '';
405
+ return ` ${cmd}) COMPREPLY=( $(compgen -W "${shellQuoteList([...e.positionals].sort())}" -- "$current") ) ;;`;
406
+ })
407
+ .filter(Boolean)
408
+ .join('\n');
409
+
410
+ return applyTemplate('cds-tmpl.sh', {
411
+ SHORTCUT_CASE: shortcutCaseBlock(shortcuts),
412
+ COMMANDS: shellQuoteList(commands),
413
+ FLAG_CASES: flagCases,
414
+ OPTION_VALUE_CASES: bashOptionValueCases(commandMap),
415
+ COMMA_CASES: bashCommaCases(commandMap),
416
+ FILE_CASES: fileCases,
417
+ STRING_POSITIONAL_CASES: stringPositionalCases,
418
+ });
419
+ }
420
+
421
+ async function generateZsh(map) {
422
+ const { commands, shortcuts, commandMap } = map;
423
+
424
+ const flagCases = Object.entries(commandMap)
425
+ .map(([cmd, e]) => {
426
+ const flags = sortedFlags(e);
427
+ if (!flags.length) return '';
428
+ return ` ${cmd}) compadd -S '' -- ${shellQuoteList(flags)} ;;`;
429
+ })
430
+ .filter(Boolean)
431
+ .join('\n');
432
+
433
+ const fileCases = Object.entries(commandMap)
434
+ .map(([cmd, e]) => {
435
+ const sentinel = e.fileCases?.[POSITIONAL_KEY];
436
+ if (!sentinel) return '';
437
+ const comp = sentinel === DIR() ? '_files -/' : zshFileComp(sentinel);
438
+ return ` ${cmd}) ${comp} ; return ;;`;
439
+ })
440
+ .filter(Boolean)
441
+ .join('\n');
442
+
443
+ const stringPositionalCases = Object.entries(commandMap)
444
+ .map(([cmd, e]) => {
445
+ if (e.fileCases?.[POSITIONAL_KEY] || !e.positionals?.length) return '';
446
+ return ` ${cmd}) compadd -S '' -- ${shellQuoteList([...e.positionals].sort())} ; return ;;`;
447
+ })
448
+ .filter(Boolean)
449
+ .join('\n');
450
+
451
+ return applyTemplate('cds-tmpl.zsh', {
452
+ SHORTCUT_CASE: shortcutCaseBlock(shortcuts),
453
+ COMMANDS: shellQuoteList(commands),
454
+ FLAG_CASES: flagCases,
455
+ OPTION_VALUE_CASES: zshOptionValueCases(commandMap),
456
+ COMMA_CASES: zshCommaCases(commandMap),
457
+ FILE_CASES: fileCases,
458
+ STRING_POSITIONAL_CASES: stringPositionalCases,
459
+ });
460
+ }
461
+
462
+ async function generateFish(map) {
463
+ const { commands, shortcuts, commandMap } = map;
464
+
465
+ // Reverse map: canonical command name → its single-char shortcut(s).
466
+ const cmdShortcuts = {};
467
+ for (const [shortcut, cmd] of singleCharShortcuts(shortcuts)) {
468
+ (cmdShortcuts[cmd] ??= []).push(shortcut);
469
+ }
470
+
471
+ // Fish condition: fires when the subcommand (or one of its shortcuts) is already on the line.
472
+ const seenCmd = (cmd) =>
473
+ `__fish_seen_subcommand_from ${[cmd, ...(cmdShortcuts[cmd] ?? [])].join(' ')}`;
474
+
475
+ const allCmdsAndShortcuts = [
476
+ ...commands,
477
+ ...singleCharShortcuts(shortcuts).map(([k]) => k),
478
+ ].join(' ');
479
+
480
+ const lines = [];
481
+
482
+ // Top-level commands — only shown before any subcommand is committed.
483
+ for (const cmd of commands) {
484
+ lines.push(`complete -c cds -f -n '__fish_use_subcommand' -a '${cmd}'`);
485
+ }
486
+
487
+ for (const [cmd, e] of Object.entries(commandMap)) {
488
+ const hasFileSentinel = !!e.fileCases?.[POSITIONAL_KEY];
489
+
490
+ // File-sentinel positionals (e.g. *.cds, *.json for compile).
491
+ for (const [flag, sentinel] of Object.entries(e.fileCases ?? {})) {
492
+ const fishComp = sentinel === DIR() ? '__fish_complete_directories' : fishFileComp(sentinel);
493
+ const cond = flag === POSITIONAL_KEY
494
+ ? seenCmd(cmd)
495
+ : `${seenCmd(cmd)} && __fish_prev_arg_in ${flag}`;
496
+ lines.push(`complete -c cds -f -n '${cond}' -a "(${fishComp})"`);
497
+ }
498
+
499
+ // String positionals (e.g. command names for `cds help`).
500
+ for (const pos of [...(e.positionals ?? [])].sort()) {
501
+ lines.push(`complete -c cds -f -n '${seenCmd(cmd)}' -a '${pos}'`);
502
+ }
503
+
504
+ // Flags — gated on current token starting with '-' when a file sentinel is present,
505
+ // so that `cds compile <tab>` shows files only and `cds compile --<tab>` shows flags.
506
+ const flagCond = hasFileSentinel
507
+ ? `${seenCmd(cmd)} && string match -q -- "-*" (commandline -ct)`
508
+ : seenCmd(cmd);
509
+ for (const token of sortedFlags(e)) {
510
+ lines.push(`complete -c cds -f -n '${flagCond}' -a '${token}'`);
511
+ }
512
+
513
+ // Option values (previous word is a known flag → offer its static values).
514
+ for (const [flag, val] of Object.entries(e.optionValues ?? {})) {
515
+ lines.push(`complete -c cds -f -n '${seenCmd(cmd)} && __fish_prev_arg_in ${flag}' -a '${[...val].sort().join(' ')}'`);
516
+ }
517
+
518
+ // Comma-separated values — delegated to the _cdsCommaComplete helper in the template.
519
+ for (const [flag, val] of Object.entries(e.commaCases ?? {})) {
520
+ const values = [...val].sort().join(' ');
521
+ const cond = flag === POSITIONAL_KEY
522
+ ? seenCmd(cmd)
523
+ : `${seenCmd(cmd)} && __fish_prev_arg_in ${flag}`;
524
+ lines.push(`complete -c cds -f -n '${cond}' -a "(_cdsCommaComplete ${values})"`);
525
+ }
137
526
  }
138
527
 
139
- return allOptionsFlags;
528
+ return applyTemplate('cds-tmpl.fish', {
529
+ COMMAND_COMPLETIONS: lines.join('\n'),
530
+ CMD_LIST: commands.join(' '),
531
+ CMD_LIST_WITH_SHORTCUTS: allCmdsAndShortcuts,
532
+ });
140
533
  }
141
534
 
535
+ async function generatePs(map) {
536
+ const { commands, shortcuts, commandMap } = map;
537
+
538
+ const cmdArray = commands.map(c => `'${c}'`).join(', ');
539
+
540
+ const shortcutEntries = singleCharShortcuts(shortcuts)
541
+ .map(([k, v]) => ` '${k}' = '${v}'`)
542
+ .join(";\n");
543
+
544
+ const cmdEntries = Object.entries(commandMap)
545
+ .map(([cmd, e]) => {
546
+ const flags = sortedFlags(e).map(t => `'${t}'`).join(', ');
547
+ return ` '${cmd}' = @(${flags})`;
548
+ })
549
+ .join(";\n");
550
+
551
+ const positionalEntries = Object.entries(commandMap)
552
+ .filter(([, e]) => e.positionals?.length)
553
+ .map(([cmd, e]) => ` '${cmd}' = @(${[...e.positionals].sort().map(p => `'${p}'`).join(', ')})`)
554
+ .join(";\n");
555
+
556
+ const optionValueEntries = Object.entries(commandMap)
557
+ .filter(([, e]) => Object.keys(e.optionValues ?? {}).length || Object.keys(e.fileCases ?? {}).length)
558
+ .map(([cmd, e]) => {
559
+ const inner = [];
560
+ for (const [flag, val] of Object.entries(e.optionValues ?? {})) {
561
+ inner.push(` '${flag}' = @(${[...val].sort().map(v => `'${v}'`).join(', ')})`);
562
+ }
563
+ for (const [flag, sentinel] of Object.entries(e.fileCases ?? {})) {
564
+ inner.push(` '${flag}' = ${filePs1(sentinel)}`);
565
+ }
566
+ return ` '${cmd}' = @{\n${inner.join(";\n")}\n }`;
567
+ })
568
+ .join(";\n");
569
+
570
+ const commaEntries = Object.entries(commandMap)
571
+ .filter(([, e]) => Object.keys(e.commaCases ?? {}).length)
572
+ .map(([cmd, e]) => {
573
+ const inner = Object.entries(e.commaCases).map(([flag, val]) =>
574
+ ` '${flag}' = @(${[...val].sort().map(v => `'${v}'`).join(', ')})`
575
+ );
576
+ return ` '${cmd}' = @{\n${inner.join(";\n")}\n }`;
577
+ })
578
+ .join(";\n");
579
+
580
+ return applyTemplate('cds-tmpl.ps1', {
581
+ CMD_ARRAY: cmdArray,
582
+ SHORTCUT_ENTRIES: shortcutEntries,
583
+ POSITIONAL_ENTRIES: positionalEntries,
584
+ OPTION_VALUE_ENTRIES: optionValueEntries,
585
+ COMMA_ENTRIES: commaEntries,
586
+ CMD_ENTRIES: cmdEntries,
587
+ });
588
+ }
589
+
590
+ // --- Completion engine ---
591
+
142
592
  function reply(values) {
143
- if (!Array.isArray(values) || values.length === 0) {
144
- return;
145
- }
593
+ if (!Array.isArray(values) || values.length === 0) return;
146
594
 
147
- values = values.filter(e => !!(typeof e === 'string' && e.trim()));
148
- values = [...new Set(values)];
595
+ values = [...new Set(values.filter(e => typeof e === 'string' && e.trim()))];
149
596
  values.sort((a, b) => {
150
- if (a.startsWith('-') && !b.startsWith('-')) { return 1; }
151
- if (!a.startsWith('-') && b.startsWith('-')) { return -1; }
597
+ // Files / positionals before flags, then alphabetical within each group.
598
+ const aIsFlag = a.startsWith('-');
599
+ const bIsFlag = b.startsWith('-');
600
+ if (aIsFlag !== bIsFlag) return aIsFlag ? 1 : -1;
152
601
  return a.localeCompare(b);
153
602
  });
154
603
 
155
604
  console.log(values.join('\n'));
156
605
  }
157
606
 
607
+ // Splits a shell command line into tokens, respecting quoted strings and escaped spaces.
158
608
  function getSegments(line) {
159
609
  const cliSplit = /'(.*?)'|"(.*?)"|(\\,|\\ |[^, ])+/gs;
160
- const result = line.match(cliSplit);
161
- return result.map(s =>
610
+ return line.match(cliSplit).map(s =>
162
611
  s.trim()
163
612
  .replace(/(\\ )/g, ' ')
164
613
  .replace(/^["']|["']$/g, '')
165
614
  );
166
615
  }
167
616
 
617
+ // Loads the cds command object for the given name, resolving single-char shortcuts first.
168
618
  function getCdsCmd(cdsCmd) {
169
619
  cdsCmd = cds.Shortcuts?.[cdsCmd] ?? cdsCmd;
170
620
  return cds.load(cdsCmd);
171
621
  }
172
622
 
623
+ // Returns all completable flags/options for a command, excluding flags already used on the line.
173
624
  function getAllOptions(cdsCmdObj, segments) {
174
625
  return [
175
626
  ...cdsCmdObj.options ?? [],
176
- ...cdsCmdObj.flags?.filter(o => !segments.includes(o)) ?? []
627
+ ...cdsCmdObj.flags?.filter(o => !segments.includes(o)) ?? [],
177
628
  ];
178
629
  }
179
630
 
180
- function getUtil() {
181
- const completionFs = require('./completionFs');
182
- return {
183
- cds,
184
- completionFs
185
- }
186
- }
187
-
188
631
  async function getCompletions(cdsCmd, previousWord, currentWord, segments) {
189
- const _filter = (completions, currentWord) => {
190
- return completions.filter(token => token?.startsWith(currentWord));
191
- }
192
-
193
- const _getCompletions = async () => {
632
+ // Returns completions filtered by currentWord prefix, unless commaFiltered is set
633
+ // (comma logic pre-filters its own results to avoid double-filtering).
634
+ let commaFiltered = false;
635
+
636
+ const resolveComma = (values, commaStr, partial) => {
637
+ commaFiltered = true;
638
+ const parts = commaStr.split(',');
639
+ const alreadyUsed = new Set(parts.slice(0, -1).map(s => s.trim()).filter(Boolean));
640
+ const prefix = parts.slice(0, -1).join(',') + (parts.length > 1 ? ',' : '');
641
+ return values.filter(v => !alreadyUsed.has(v) && v.startsWith(partial)).map(v => prefix + v);
642
+ };
643
+
644
+ // Reads the file/dir sentinel for a resolved entry and returns matching paths.
645
+ const resolveSentinel = (sentinel, word) => {
646
+ if (sentinel === DIR()) return completionFs.readdir(word, { files: false });
647
+ if (isFileSentinel(sentinel)) return completionFs.readdir(word, fileRegex(sentinel) ? { fileRegex: fileRegex(sentinel) } : {});
648
+ return null;
649
+ };
650
+
651
+ const computeCompletions = async () => {
652
+ // If the cursor is right after 'cds', offer top-level command names.
194
653
  if (previousWord === 'cds') {
195
- if (!currentWord) {
196
- return cds.cmds;
197
- } else {
198
- const completions = _filter(cds.cmds, currentWord);
199
- if (completions.length > 0) {
200
- return completions;
201
- }
202
- cdsCmd = 'compile';
203
- previousWord = 'compile';
204
- }
654
+ if (!currentWord) return cds.cmds;
655
+ const matching = cds.cmds.filter(c => c.startsWith(currentWord));
656
+ if (matching.length) return matching;
657
+ // No command matched the typed prefix — fall through as if typing a compile file arg.
658
+ cdsCmd = 'compile';
659
+ previousWord = 'compile';
205
660
  }
206
661
 
207
662
  try {
208
663
  const cdsCmdObj = getCdsCmd(cdsCmd);
209
- if (typeof cdsCmdObj.handleCompletion === 'function') {
210
- return await cdsCmdObj.handleCompletion(currentWord, previousWord, segments, getUtil());
211
- }
212
664
 
213
- if (currentWord?.startsWith('-')) {
214
- return getAllOptions(cdsCmdObj, segments);
665
+ // Current token starts with '-' → offer flags/options.
666
+ if (currentWord?.startsWith('-')) return getAllOptions(cdsCmdObj, segments);
667
+
668
+ const raw = await cdsCmdObj.staticCompletions?.();
669
+ if (raw && !Array.isArray(raw)) {
670
+ // Resolve single-char option shortcuts (e.g. -4 → --for) so we can look up
671
+ // the right key in the staticCompletions map.
672
+ const cmdOptions = cdsCmdObj.options ?? [];
673
+ const cmdShortcuts = cdsCmdObj.shortcuts ?? [];
674
+ const shortcutIdx = cmdShortcuts.indexOf(previousWord);
675
+ const lookupKey = shortcutIdx !== -1 ? (cmdOptions[shortcutIdx] ?? previousWord) : previousWord;
676
+
677
+ // Comma-separated values come in two forms:
678
+ // inline: "--add approuter,m" → prev="--add" curr="approuter,m"
679
+ // space-separated: "--add approuter, m" → prev="approuter," curr="m"
680
+ const isSpaceSeparated = lookupKey.endsWith(',');
681
+ const isInlineComma = !isSpaceSeparated && currentWord?.includes(',');
682
+ if (isSpaceSeparated || isInlineComma) {
683
+ // For space-separated form, find the originating option flag in the segments.
684
+ const optKey = isSpaceSeparated
685
+ ? cmdOptions.find(o =>
686
+ segments.includes(o) ||
687
+ cmdShortcuts.some((s, i) => segments.includes(s) && cmdOptions[i] === o)
688
+ )
689
+ : lookupKey;
690
+ const resolved = optKey ? await raw[optKey] : undefined;
691
+ if (resolved?.type === ':comma') {
692
+ const values = await resolved.values;
693
+ const commaStr = isSpaceSeparated ? previousWord : currentWord;
694
+ const partial = isSpaceSeparated ? (currentWord ?? '') : commaStr.split(',').at(-1);
695
+ return resolveComma(values, commaStr, partial);
696
+ }
697
+ }
698
+
699
+ // Previous word is a known option → offer its static values or file completions.
700
+ const entry = raw[lookupKey];
701
+ if (entry !== undefined) {
702
+ const resolved = await entry;
703
+ if (resolved?.type === ':comma') {
704
+ const values = await resolved.values;
705
+ return resolveComma(values, currentWord ?? '', (currentWord ?? '').split(',').at(-1));
706
+ }
707
+ return resolveSentinel(resolved, currentWord) ?? resolved;
708
+ }
709
+
710
+ // No option matched — check the default positional sentinel.
711
+ const defaultPositional = raw[POSITIONAL_KEY] !== undefined ? await raw[POSITIONAL_KEY] : undefined;
712
+ if (defaultPositional !== undefined) {
713
+ if (defaultPositional?.type === ':comma') {
714
+ const values = await defaultPositional.values;
715
+ return resolveComma(values, currentWord ?? '', (currentWord ?? '').split(',').at(-1));
716
+ }
717
+ return resolveSentinel(defaultPositional, currentWord) ?? [];
718
+ }
719
+
720
+ // Previous word is a consumed option value (e.g. "approuter" after "--add").
721
+ // Don't offer more positionals — the user has already provided one.
722
+ const allOpts = [...cmdOptions, ...cmdShortcuts, ...(cdsCmdObj.flags ?? [])];
723
+ if (previousWord !== cdsCmd && !allOpts.includes(previousWord)) return [];
215
724
  }
216
725
 
217
- const completionFs = require('./completionFs');
218
- const all = await completionFs.readdir(currentWord);
219
- if (!(cdsCmdObj.options?.includes(previousWord) || cdsCmdObj.shortcuts?.includes(previousWord))) {
220
- const allOptions = getAllOptions(cdsCmdObj, segments);
221
- all.push(...allOptions);
726
+ // Array form of staticCompletions → fixed positional list (e.g. cds help <tab>).
727
+ if (Array.isArray(raw) && raw.length) {
728
+ const prevIsOption = cdsCmdObj.options?.includes(previousWord) || cdsCmdObj.shortcuts?.includes(previousWord);
729
+ return prevIsOption ? [] : raw;
222
730
  }
223
731
 
224
- return all;
732
+ // No staticCompletions — fall back to files + available flags.
733
+ const files = await completionFs.readdir(currentWord);
734
+ const prevIsOption = cdsCmdObj.options?.includes(previousWord) || cdsCmdObj.shortcuts?.includes(previousWord);
735
+ if (!prevIsOption) files.push(...getAllOptions(cdsCmdObj, segments));
736
+ return files;
737
+
225
738
  } catch {
226
739
  return [];
227
740
  }
228
- }
741
+ };
229
742
 
230
- let completions = await _getCompletions();
231
- if (currentWord) {
232
- completions = _filter(completions, currentWord);
743
+ let completions = await computeCompletions();
744
+ if (currentWord && !commaFiltered) {
745
+ completions = completions.filter(c => c?.startsWith(currentWord));
233
746
  }
234
-
235
747
  return completions;
236
748
  }