@youzan-cloud/zan-saas-cli 0.0.0-stage → 0.1.0-beta.4

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 (329) hide show
  1. package/DEPENDENCIES.json +50 -0
  2. package/INSTALL.md +79 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE +1 -0
  5. package/README.md +151 -2
  6. package/THIRD_PARTY_NOTICES.txt +162 -0
  7. package/bin/zan-saas-cli.js +28 -0
  8. package/internal/registry/index.js +123 -0
  9. package/internal/registry/manifest.json +424 -0
  10. package/npm-shrinkwrap.json +86 -0
  11. package/package.json +33 -4
  12. package/skills/README.md +30 -0
  13. package/skills/zan-saas/SKILL.md +54 -0
  14. package/skills/zan-saas/references/host-entry.md +25 -0
  15. package/skills/zan-saas-access/SKILL.md +37 -0
  16. package/skills/zan-saas-access/references/role-list.md +11 -0
  17. package/skills/zan-saas-access/references/staff-perms-get.md +11 -0
  18. package/skills/zan-saas-aftersale/SKILL.md +31 -0
  19. package/skills/zan-saas-aftersale/references/case-list.md +13 -0
  20. package/skills/zan-saas-aftersale/references/case-scan.md +31 -0
  21. package/skills/zan-saas-benefit/SKILL.md +46 -0
  22. package/skills/zan-saas-benefit/references/card-get.md +7 -0
  23. package/skills/zan-saas-benefit/references/card-grant-list.md +11 -0
  24. package/skills/zan-saas-benefit/references/card-list.md +7 -0
  25. package/skills/zan-saas-benefit/references/customer-card-get.md +7 -0
  26. package/skills/zan-saas-benefit/references/customer-card-list.md +7 -0
  27. package/skills/zan-saas-coupon/SKILL.md +41 -0
  28. package/skills/zan-saas-coupon/references/activity-get.md +11 -0
  29. package/skills/zan-saas-coupon/references/activity-list.md +15 -0
  30. package/skills/zan-saas-coupon/references/voucher-get.md +13 -0
  31. package/skills/zan-saas-coupon/references/voucher-list.md +25 -0
  32. package/skills/zan-saas-customer/SKILL.md +73 -0
  33. package/skills/zan-saas-customer/references/customer-scan.md +11 -0
  34. package/skills/zan-saas-customer/references/segment-list.md +22 -0
  35. package/skills/zan-saas-fulfillment/SKILL.md +36 -0
  36. package/skills/zan-saas-fulfillment/references/delivery-get.md +11 -0
  37. package/skills/zan-saas-fulfillment/references/shipment-list.md +11 -0
  38. package/skills/zan-saas-fulfillment/references/waybill-get.md +9 -0
  39. package/skills/zan-saas-interaction/SKILL.md +37 -0
  40. package/skills/zan-saas-interaction/references/lottery-get.md +11 -0
  41. package/skills/zan-saas-interaction/references/lottery-list.md +19 -0
  42. package/skills/zan-saas-item/SKILL.md +61 -0
  43. package/skills/zan-saas-item/references/attribute-list.md +11 -0
  44. package/skills/zan-saas-item/references/brand-list.md +7 -0
  45. package/skills/zan-saas-item/references/classification-get.md +7 -0
  46. package/skills/zan-saas-item/references/classification-list.md +7 -0
  47. package/skills/zan-saas-item/references/group-list.md +9 -0
  48. package/skills/zan-saas-item/references/item-content.md +9 -0
  49. package/skills/zan-saas-item/references/item-get.md +9 -0
  50. package/skills/zan-saas-item/references/item-search.md +23 -0
  51. package/skills/zan-saas-item/references/product-identity.md +11 -0
  52. package/skills/zan-saas-member/SKILL.md +41 -0
  53. package/skills/zan-saas-member/references/customer-level-get.md +7 -0
  54. package/skills/zan-saas-member/references/growth-log-list.md +17 -0
  55. package/skills/zan-saas-member/references/membership-get.md +7 -0
  56. package/skills/zan-saas-order/SKILL.md +56 -0
  57. package/skills/zan-saas-order/references/order-get.md +26 -0
  58. package/skills/zan-saas-order/references/order-list.md +25 -0
  59. package/skills/zan-saas-order/references/order-scan.md +35 -0
  60. package/skills/zan-saas-org/SKILL.md +37 -0
  61. package/skills/zan-saas-org/references/staff-get.md +15 -0
  62. package/skills/zan-saas-org/references/staff-role-get.md +11 -0
  63. package/skills/zan-saas-org/references/unit-get.md +11 -0
  64. package/skills/zan-saas-org/references/unit-list.md +15 -0
  65. package/skills/zan-saas-points/SKILL.md +42 -0
  66. package/skills/zan-saas-points/references/account-get.md +7 -0
  67. package/skills/zan-saas-points/references/log-list.md +17 -0
  68. package/skills/zan-saas-points/references/rule-list.md +2 -0
  69. package/skills/zan-saas-privacy/SKILL.md +30 -0
  70. package/skills/zan-saas-privacy/references/field-decrypt.md +19 -0
  71. package/skills/zan-saas-shared/SKILL.md +94 -0
  72. package/skills/zan-saas-shared/references/authentication.md +52 -0
  73. package/skills/zan-saas-shared/references/value-processing.md +27 -0
  74. package/skills/zan-saas-stock/SKILL.md +28 -0
  75. package/skills/zan-saas-stock/references/log-list.md +11 -0
  76. package/skills/zan-saas-valuecard/SKILL.md +37 -0
  77. package/skills/zan-saas-valuecard/references/card-get.md +13 -0
  78. package/skills/zan-saas-valuecard/references/card-list.md +11 -0
  79. package/src/auth/browser.js +28 -0
  80. package/src/auth/callback-server.js +99 -0
  81. package/src/auth/config.js +53 -0
  82. package/src/auth/credential-store.js +109 -0
  83. package/src/auth/oauth-client.js +105 -0
  84. package/src/auth/pkce.js +28 -0
  85. package/src/auth/session.js +178 -0
  86. package/src/auth/tool.js +25 -0
  87. package/src/cli/command-paths.js +126 -0
  88. package/src/cli/context-help.js +102 -0
  89. package/src/cli/diagnostic-command.js +57 -0
  90. package/src/cli/domain-catalog.js +42 -0
  91. package/src/cli/help-text.js +32 -0
  92. package/src/cli/native-loader.js +120 -0
  93. package/src/cli/native-path.js +22 -0
  94. package/src/cli/parser-policy.js +141 -0
  95. package/src/cli/program.js +134 -0
  96. package/src/cli/skill-catalog.js +241 -0
  97. package/src/commands/native/access/commands.index.json +24 -0
  98. package/src/commands/native/access/domain.json +3 -0
  99. package/src/commands/native/access/role/list/command.js +141 -0
  100. package/src/commands/native/access/role/list/schema.json +393 -0
  101. package/src/commands/native/access/staff-perms/get/command.js +119 -0
  102. package/src/commands/native/access/staff-perms/get/schema.json +703 -0
  103. package/src/commands/native/aftersale/case/get/command.js +104 -0
  104. package/src/commands/native/aftersale/case/get/exact-json.js +52 -0
  105. package/src/commands/native/aftersale/case/get/money.js +28 -0
  106. package/src/commands/native/aftersale/case/get/projection.js +120 -0
  107. package/src/commands/native/aftersale/case/get/query.js +72 -0
  108. package/src/commands/native/aftersale/case/get/schema.json +134 -0
  109. package/src/commands/native/aftersale/case/list/command.js +135 -0
  110. package/src/commands/native/aftersale/case/list/schema.json +1095 -0
  111. package/src/commands/native/aftersale/case/scan/command.js +110 -0
  112. package/src/commands/native/aftersale/case/scan/query.js +62 -0
  113. package/src/commands/native/aftersale/case/scan/scan.js +78 -0
  114. package/src/commands/native/aftersale/case/scan/schema.json +964 -0
  115. package/src/commands/native/aftersale/case/scan/search.js +108 -0
  116. package/src/commands/native/aftersale/commands.index.json +42 -0
  117. package/src/commands/native/aftersale/domain.json +3 -0
  118. package/src/commands/native/aftersale/refund/list/command.js +97 -0
  119. package/src/commands/native/aftersale/refund/list/money.js +25 -0
  120. package/src/commands/native/aftersale/refund/list/projection.js +50 -0
  121. package/src/commands/native/aftersale/refund/list/query.js +73 -0
  122. package/src/commands/native/aftersale/refund/list/schema.json +130 -0
  123. package/src/commands/native/aftersale/shared/query.js +58 -0
  124. package/src/commands/native/aftersale/shared/search.js +128 -0
  125. package/src/commands/native/benefit/card-definition/get/command.js +110 -0
  126. package/src/commands/native/benefit/card-definition/get/schema.json +545 -0
  127. package/src/commands/native/benefit/card-definition/list/command.js +106 -0
  128. package/src/commands/native/benefit/card-definition/list/schema.json +690 -0
  129. package/src/commands/native/benefit/card-grant/list/command.js +135 -0
  130. package/src/commands/native/benefit/card-grant/list/schema.json +758 -0
  131. package/src/commands/native/benefit/commands.index.json +51 -0
  132. package/src/commands/native/benefit/customer-card/get/command.js +110 -0
  133. package/src/commands/native/benefit/customer-card/get/schema.json +340 -0
  134. package/src/commands/native/benefit/customer-card/list/command.js +103 -0
  135. package/src/commands/native/benefit/customer-card/list/schema.json +731 -0
  136. package/src/commands/native/benefit/domain.json +3 -0
  137. package/src/commands/native/coupon/activity/get/command.js +183 -0
  138. package/src/commands/native/coupon/activity/get/schema.json +624 -0
  139. package/src/commands/native/coupon/activity/list/command.js +169 -0
  140. package/src/commands/native/coupon/activity/list/schema.json +845 -0
  141. package/src/commands/native/coupon/commands.index.json +42 -0
  142. package/src/commands/native/coupon/domain.json +3 -0
  143. package/src/commands/native/coupon/shared/query.js +15 -0
  144. package/src/commands/native/coupon/voucher/get/command.js +113 -0
  145. package/src/commands/native/coupon/voucher/get/schema.json +147 -0
  146. package/src/commands/native/coupon/voucher/list/command.js +155 -0
  147. package/src/commands/native/coupon/voucher/list/schema.json +772 -0
  148. package/src/commands/native/customer/commands.index.json +96 -0
  149. package/src/commands/native/customer/customer/get/adapter.js +82 -0
  150. package/src/commands/native/customer/customer/get/command.js +106 -0
  151. package/src/commands/native/customer/customer/get/schema.json +958 -0
  152. package/src/commands/native/customer/customer/list/command.js +78 -0
  153. package/src/commands/native/customer/customer/list/input.js +145 -0
  154. package/src/commands/native/customer/customer/list/query.js +103 -0
  155. package/src/commands/native/customer/customer/list/schema.json +969 -0
  156. package/src/commands/native/customer/customer/scan/command.js +80 -0
  157. package/src/commands/native/customer/customer/scan/scan.js +92 -0
  158. package/src/commands/native/customer/customer/scan/schema.json +211 -0
  159. package/src/commands/native/customer/customer-tag/get/command.js +166 -0
  160. package/src/commands/native/customer/customer-tag/get/schema.json +317 -0
  161. package/src/commands/native/customer/customer-tag/list/command.js +143 -0
  162. package/src/commands/native/customer/customer-tag/list/schema.json +259 -0
  163. package/src/commands/native/customer/domain.json +3 -0
  164. package/src/commands/native/customer/segment/list/command.js +96 -0
  165. package/src/commands/native/customer/segment/list/query.js +98 -0
  166. package/src/commands/native/customer/segment/list/schema.json +135 -0
  167. package/src/commands/native/customer/segment-customer/list/command.js +171 -0
  168. package/src/commands/native/customer/segment-customer/list/schema.json +123 -0
  169. package/src/commands/native/customer/shared/search-input.js +179 -0
  170. package/src/commands/native/customer/shared/search-page.js +54 -0
  171. package/src/commands/native/customer/tag/get/command.js +73 -0
  172. package/src/commands/native/customer/tag/get/schema.json +339 -0
  173. package/src/commands/native/customer/tagcategory/get/command.js +80 -0
  174. package/src/commands/native/customer/tagcategory/get/query.js +98 -0
  175. package/src/commands/native/customer/tagcategory/get/schema.json +339 -0
  176. package/src/commands/native/customer/tagcategory/list/command.js +127 -0
  177. package/src/commands/native/customer/tagcategory/list/schema.json +394 -0
  178. package/src/commands/native/fulfillment/commands.index.json +33 -0
  179. package/src/commands/native/fulfillment/delivery/get/command.js +107 -0
  180. package/src/commands/native/fulfillment/delivery/get/projection.js +142 -0
  181. package/src/commands/native/fulfillment/delivery/get/schema.json +173 -0
  182. package/src/commands/native/fulfillment/domain.json +3 -0
  183. package/src/commands/native/fulfillment/shipment/list/command.js +83 -0
  184. package/src/commands/native/fulfillment/shipment/list/projection.js +130 -0
  185. package/src/commands/native/fulfillment/shipment/list/schema.json +752 -0
  186. package/src/commands/native/fulfillment/waybill/get/command.js +193 -0
  187. package/src/commands/native/fulfillment/waybill/get/response.js +68 -0
  188. package/src/commands/native/fulfillment/waybill/get/schema.json +190 -0
  189. package/src/commands/native/interaction/commands.index.json +24 -0
  190. package/src/commands/native/interaction/domain.json +3 -0
  191. package/src/commands/native/interaction/lottery/get/command.js +61 -0
  192. package/src/commands/native/interaction/lottery/get/query.js +152 -0
  193. package/src/commands/native/interaction/lottery/get/schema.json +1004 -0
  194. package/src/commands/native/interaction/lottery/list/command.js +176 -0
  195. package/src/commands/native/interaction/lottery/list/schema.json +898 -0
  196. package/src/commands/native/item/attribute/list/command.js +122 -0
  197. package/src/commands/native/item/attribute/list/schema.json +727 -0
  198. package/src/commands/native/item/brand/list/command.js +114 -0
  199. package/src/commands/native/item/brand/list/schema.json +725 -0
  200. package/src/commands/native/item/classification/get/command.js +87 -0
  201. package/src/commands/native/item/classification/get/schema.json +596 -0
  202. package/src/commands/native/item/classification/list/command.js +105 -0
  203. package/src/commands/native/item/classification/list/schema.json +676 -0
  204. package/src/commands/native/item/commands.index.json +87 -0
  205. package/src/commands/native/item/domain.json +1 -0
  206. package/src/commands/native/item/group/list/command.js +122 -0
  207. package/src/commands/native/item/group/list/schema.json +809 -0
  208. package/src/commands/native/item/item/get/command.js +122 -0
  209. package/src/commands/native/item/item/get/schema.json +3799 -0
  210. package/src/commands/native/item/item/list/command.js +7 -0
  211. package/src/commands/native/item/item/list/schema.json +1118 -0
  212. package/src/commands/native/item/item/scan/command.js +7 -0
  213. package/src/commands/native/item/item/scan/schema.json +1046 -0
  214. package/src/commands/native/item/item-content/get/command.js +54 -0
  215. package/src/commands/native/item/item-content/get/schema.json +677 -0
  216. package/src/commands/native/item/shared/api.js +31 -0
  217. package/src/commands/native/item/shared/content.js +20 -0
  218. package/src/commands/native/item/shared/diagnostics.js +61 -0
  219. package/src/commands/native/item/shared/h5.js +63 -0
  220. package/src/commands/native/item/shared/identity.js +62 -0
  221. package/src/commands/native/item/shared/normalize.js +49 -0
  222. package/src/commands/native/item/shared/scope.js +22 -0
  223. package/src/commands/native/item/shared/search-command.js +61 -0
  224. package/src/commands/native/item/shared/search-options.js +150 -0
  225. package/src/commands/native/item/shared/search.js +158 -0
  226. package/src/commands/native/member/commands.index.json +60 -0
  227. package/src/commands/native/member/customer-level/get/command.js +122 -0
  228. package/src/commands/native/member/customer-level/get/schema.json +300 -0
  229. package/src/commands/native/member/domain.json +3 -0
  230. package/src/commands/native/member/growth/get/command.js +64 -0
  231. package/src/commands/native/member/growth/get/schema.json +283 -0
  232. package/src/commands/native/member/growth-log/list/command.js +203 -0
  233. package/src/commands/native/member/growth-log/list/schema.json +695 -0
  234. package/src/commands/native/member/level/get/command.js +70 -0
  235. package/src/commands/native/member/level/get/query.js +125 -0
  236. package/src/commands/native/member/level/get/schema.json +339 -0
  237. package/src/commands/native/member/level/list/command.js +50 -0
  238. package/src/commands/native/member/level/list/query.js +109 -0
  239. package/src/commands/native/member/level/list/schema.json +104 -0
  240. package/src/commands/native/member/membership/get/command.js +78 -0
  241. package/src/commands/native/member/membership/get/schema.json +283 -0
  242. package/src/commands/native/order/commands.index.json +33 -0
  243. package/src/commands/native/order/domain.json +3 -0
  244. package/src/commands/native/order/order/get/command.js +155 -0
  245. package/src/commands/native/order/order/get/schema.json +1167 -0
  246. package/src/commands/native/order/order/list/command.js +230 -0
  247. package/src/commands/native/order/order/list/schema.json +1363 -0
  248. package/src/commands/native/order/order/scan/command.js +148 -0
  249. package/src/commands/native/order/order/scan/scan.js +89 -0
  250. package/src/commands/native/order/order/scan/schema.json +1445 -0
  251. package/src/commands/native/order/shared/projection.js +183 -0
  252. package/src/commands/native/order/shared/query.js +84 -0
  253. package/src/commands/native/order/shared/search.js +112 -0
  254. package/src/commands/native/org/commands.index.json +42 -0
  255. package/src/commands/native/org/domain.json +3 -0
  256. package/src/commands/native/org/shared/single-staff.js +115 -0
  257. package/src/commands/native/org/staff/get/command.js +148 -0
  258. package/src/commands/native/org/staff/get/schema.json +114 -0
  259. package/src/commands/native/org/staff-role/get/command.js +45 -0
  260. package/src/commands/native/org/staff-role/get/schema.json +93 -0
  261. package/src/commands/native/org/unit/get/command.js +131 -0
  262. package/src/commands/native/org/unit/get/schema.json +478 -0
  263. package/src/commands/native/org/unit/list/command.js +187 -0
  264. package/src/commands/native/org/unit/list/schema.json +828 -0
  265. package/src/commands/native/points/account/get/command.js +65 -0
  266. package/src/commands/native/points/account/get/schema.json +303 -0
  267. package/src/commands/native/points/commands.index.json +33 -0
  268. package/src/commands/native/points/domain.json +3 -0
  269. package/src/commands/native/points/log/list/command.js +141 -0
  270. package/src/commands/native/points/log/list/schema.json +761 -0
  271. package/src/commands/native/points/rule/list/command.js +82 -0
  272. package/src/commands/native/points/rule/list/schema.json +294 -0
  273. package/src/commands/native/privacy/commands.index.json +15 -0
  274. package/src/commands/native/privacy/domain.json +3 -0
  275. package/src/commands/native/privacy/field/decrypt/command.js +185 -0
  276. package/src/commands/native/privacy/field/decrypt/schema.json +656 -0
  277. package/src/commands/native/privacy/shared/order-source.js +33 -0
  278. package/src/commands/native/stock/commands.index.json +15 -0
  279. package/src/commands/native/stock/domain.json +3 -0
  280. package/src/commands/native/stock/log/list/command.js +207 -0
  281. package/src/commands/native/stock/log/list/schema.json +922 -0
  282. package/src/commands/native/valuecard/card/get/command.js +107 -0
  283. package/src/commands/native/valuecard/card/get/schema.json +683 -0
  284. package/src/commands/native/valuecard/card/list/command.js +136 -0
  285. package/src/commands/native/valuecard/card/list/pagination.js +23 -0
  286. package/src/commands/native/valuecard/card/list/schema.json +780 -0
  287. package/src/commands/native/valuecard/commands.index.json +24 -0
  288. package/src/commands/native/valuecard/domain.json +3 -0
  289. package/src/commands/native/valuecard/shared/projection.js +57 -0
  290. package/src/commands/system/api.js +47 -0
  291. package/src/commands/system/auth.js +118 -0
  292. package/src/commands/system/contracts.js +373 -0
  293. package/src/commands/system/doctor.js +24 -0
  294. package/src/commands/system/install.js +41 -0
  295. package/src/commands/system/printer.js +16 -0
  296. package/src/commands/system/schema.js +162 -0
  297. package/src/commands/system/shop-guidance.js +9 -0
  298. package/src/commands/system/shop.js +237 -0
  299. package/src/commands/system/skills.js +49 -0
  300. package/src/execution/beijing-date-time.js +29 -0
  301. package/src/execution/beijing-time.js +21 -0
  302. package/src/execution/bounded-input.js +42 -0
  303. package/src/execution/command-io.js +137 -0
  304. package/src/execution/command-policy.js +41 -0
  305. package/src/execution/command-schema.js +131 -0
  306. package/src/execution/datetime.js +83 -0
  307. package/src/execution/error-details.js +399 -0
  308. package/src/execution/error-model.js +82 -0
  309. package/src/execution/exact-json.js +80 -0
  310. package/src/execution/list-total.js +10 -0
  311. package/src/execution/money.js +59 -0
  312. package/src/execution/parser-error.js +66 -0
  313. package/src/execution/privacy.js +197 -0
  314. package/src/execution/public-schema.js +52 -0
  315. package/src/execution/query-api.js +101 -0
  316. package/src/execution/range.js +84 -0
  317. package/src/execution/request-state.js +55 -0
  318. package/src/execution/schema-completeness.js +116 -0
  319. package/src/execution/shop-query.js +54 -0
  320. package/src/execution/target-shop-default.js +23 -0
  321. package/src/execution/target-shop.js +49 -0
  322. package/src/identity/context-store.js +54 -0
  323. package/src/identity/context.js +38 -0
  324. package/src/installation/install.js +158 -0
  325. package/src/installation/npm-error.js +93 -0
  326. package/src/installation/skill.js +113 -0
  327. package/src/transport/account-api.js +152 -0
  328. package/src/transport/api.js +199 -0
  329. package/src/transport/shop-app.js +63 -0
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const { configDir } = require('./config');
7
+
8
+ const fileName = 'credentials.json';
9
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
10
+
11
+ function storePath(name, env) {
12
+ return path.join(configDir(env), name);
13
+ }
14
+
15
+ function credentialPath(env = process.env) {
16
+ return storePath(fileName, env);
17
+ }
18
+
19
+ function ensureDir(env) {
20
+ const dir = configDir(env);
21
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
22
+ return dir;
23
+ }
24
+
25
+ // Every config file is one JSON object keyed by oauth origin.
26
+ function readStore(name, env = process.env) {
27
+ let text;
28
+ try {
29
+ text = fs.readFileSync(storePath(name, env), 'utf8');
30
+ } catch (error) {
31
+ if (error.code === 'ENOENT') return {};
32
+ throw error;
33
+ }
34
+ try {
35
+ const value = JSON.parse(text);
36
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+
42
+ // Writes through a temporary file and rename so a crash never leaves half a file.
43
+ function writeStore(name, data, env = process.env) {
44
+ const dir = ensureDir(env);
45
+ const target = storePath(name, env);
46
+ const temporary = path.join(dir, `.${name}.${process.pid}.${crypto.randomBytes(4).toString('hex')}`);
47
+ fs.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
48
+ fs.renameSync(temporary, target);
49
+ fs.chmodSync(target, 0o600);
50
+ }
51
+
52
+ const readAll = env => readStore(fileName, env);
53
+ const writeAll = (data, env) => writeStore(fileName, data, env);
54
+
55
+ // Credentials are keyed by oauth origin so logins to different environments do not overwrite each other.
56
+ function readCredential(server, env = process.env) {
57
+ const entry = readAll(env)[server];
58
+ return entry && typeof entry.accessToken === 'string' && typeof entry.refreshToken === 'string'
59
+ ? entry : null;
60
+ }
61
+
62
+ function saveCredential(server, credential, env = process.env) {
63
+ const data = readAll(env);
64
+ data[server] = credential;
65
+ writeAll(data, env);
66
+ }
67
+
68
+ function deleteCredential(server, env = process.env) {
69
+ const data = readAll(env);
70
+ if (!Object.hasOwn(data, server)) return false;
71
+ delete data[server];
72
+ writeAll(data, env);
73
+ return true;
74
+ }
75
+
76
+ // Serializes refresh across CLI processes. refresh_token rotates on every use, so two concurrent
77
+ // refreshes would invalidate each other.
78
+ async function withFileLock(env, task, { staleMs = 30000, waitMs = 15000 } = {}) {
79
+ const lock = path.join(ensureDir(env), `${fileName}.lock`);
80
+ const started = Date.now();
81
+ let handle;
82
+ for (;;) {
83
+ try {
84
+ handle = fs.openSync(lock, 'wx', 0o600);
85
+ break;
86
+ } catch (error) {
87
+ if (error.code !== 'EEXIST') throw error;
88
+ try {
89
+ if (Date.now() - fs.statSync(lock).mtimeMs > staleMs) {
90
+ fs.rmSync(lock, { force: true });
91
+ continue;
92
+ }
93
+ } catch { continue; }
94
+ if (Date.now() - started > waitMs) throw new Error('Timed out waiting for the credential lock');
95
+ await sleep(100);
96
+ }
97
+ }
98
+ try {
99
+ return await task();
100
+ } finally {
101
+ fs.closeSync(handle);
102
+ fs.rmSync(lock, { force: true });
103
+ }
104
+ }
105
+
106
+ module.exports = {
107
+ credentialPath, deleteCredential, readCredential, readStore, saveCredential, storePath, withFileLock,
108
+ writeStore
109
+ };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const { beginRequest } = require('../execution/request-state');
4
+
5
+ class OAuthError extends Error {
6
+ constructor(message, { httpStatus, error } = {}) {
7
+ super(message);
8
+ this.httpStatus = httpStatus;
9
+ this.oauthError = error;
10
+ }
11
+ }
12
+
13
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
14
+ const errorCode = /^[a-z_]{1,64}$/;
15
+
16
+ async function postForm(config, path, params) {
17
+ const body = new URLSearchParams({ client_id: config.clientId, ...params });
18
+ const encodedBody = body.toString();
19
+ const signal = AbortSignal.timeout(10000);
20
+ let response;
21
+ const finish = beginRequest();
22
+ try {
23
+ response = await fetch(`${config.oauthBaseUrl}${path}`, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'content-type': 'application/x-www-form-urlencoded',
27
+ accept: 'application/json'
28
+ },
29
+ body: encodedBody,
30
+ redirect: 'manual',
31
+ signal
32
+ });
33
+ } catch {
34
+ finish?.('unknown');
35
+ throw new OAuthError(`OAuth request to ${path} failed or timed out`);
36
+ }
37
+ let text;
38
+ try {
39
+ text = await response.text();
40
+ finish?.('sent');
41
+ } catch {
42
+ finish?.('unknown');
43
+ text = '';
44
+ }
45
+ let json = null;
46
+ try { json = text ? JSON.parse(text) : null; } catch { json = null; }
47
+ if (!response.ok) {
48
+ // Only the RFC 6749 error code is surfaced; error_description may echo request values.
49
+ const error = typeof json?.error === 'string' && errorCode.test(json.error) ? json.error : undefined;
50
+ throw new OAuthError(
51
+ `OAuth ${path} returned HTTP ${response.status}${error ? ` ${error}` : ''}`,
52
+ { httpStatus: response.status, error });
53
+ }
54
+ return { httpStatus: response.status, json };
55
+ }
56
+
57
+ function tokenSet(json, now = Date.now()) {
58
+ if (!json || typeof json.access_token !== 'string' || !json.access_token ||
59
+ typeof json.refresh_token !== 'string' || !json.refresh_token) {
60
+ throw new OAuthError('OAuth token response is missing access_token or refresh_token');
61
+ }
62
+ const seconds = value => (Number.isSafeInteger(value) && value > 0 ? value : undefined);
63
+ const accessSeconds = seconds(json.expires_in);
64
+ const refreshSeconds = seconds(json.refresh_expires_in);
65
+ return {
66
+ accessToken: json.access_token,
67
+ refreshToken: json.refresh_token,
68
+ tokenType: typeof json.token_type === 'string' ? json.token_type : 'Bearer',
69
+ accessExpiresAt: accessSeconds ? now + accessSeconds * 1000 : undefined,
70
+ refreshExpiresAt: refreshSeconds ? now + refreshSeconds * 1000 : undefined
71
+ };
72
+ }
73
+
74
+ // The code stays valid for 60 seconds. oauth answers 503 temporarily_unavailable when another
75
+ // login of the same user holds the lock, so retry within that window.
76
+ async function exchangeCode(config, { code, verifier, redirectUri }, {
77
+ retryDelayMs = 2000, deadlineMs = 50000
78
+ } = {}) {
79
+ const started = Date.now();
80
+ for (;;) {
81
+ try {
82
+ const { json } = await postForm(config, '/oauth/token', {
83
+ grant_type: 'authorization_code', code, code_verifier: verifier, redirect_uri: redirectUri
84
+ });
85
+ return tokenSet(json);
86
+ } catch (error) {
87
+ const retryable = error instanceof OAuthError && error.httpStatus === 503;
88
+ if (!retryable || Date.now() - started + retryDelayMs > deadlineMs) throw error;
89
+ await sleep(retryDelayMs);
90
+ }
91
+ }
92
+ }
93
+
94
+ async function refreshTokens(config, refreshToken) {
95
+ const { json } = await postForm(config, '/oauth/token', {
96
+ grant_type: 'refresh_token', refresh_token: refreshToken
97
+ });
98
+ return tokenSet(json);
99
+ }
100
+
101
+ async function revokeToken(config, token) {
102
+ await postForm(config, '/oauth/revoke', { token });
103
+ }
104
+
105
+ module.exports = { OAuthError, exchangeCode, refreshTokens, revokeToken, tokenSet };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+
5
+ const base64url = buffer => buffer.toString('base64url');
6
+
7
+ // 32 random bytes encode to a 43-character verifier, the RFC 7636 minimum length.
8
+ function createVerifier() {
9
+ return base64url(crypto.randomBytes(32));
10
+ }
11
+
12
+ function challengeFor(verifier) {
13
+ return base64url(crypto.createHash('sha256').update(verifier, 'ascii').digest());
14
+ }
15
+
16
+ // 16 random bytes encode to a 22-character state.
17
+ function createState() {
18
+ return base64url(crypto.randomBytes(16));
19
+ }
20
+
21
+ function createPkce() {
22
+ const verifier = createVerifier();
23
+ return Object.freeze({
24
+ verifier, challenge: challengeFor(verifier), method: 'S256', state: createState()
25
+ });
26
+ }
27
+
28
+ module.exports = { challengeFor, createPkce, createState, createVerifier };
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ const { oauthConfig } = require('./config');
4
+ const { createPkce } = require('./pkce');
5
+ const { detectTool } = require('./tool');
6
+ const { startCallbackServer, defaultPort } = require('./callback-server');
7
+ const { openBrowser } = require('./browser');
8
+ const { OAuthError, exchangeCode, refreshTokens, revokeToken } = require('./oauth-client');
9
+ const store = require('./credential-store');
10
+ const { deleteContext } = require('../identity/context-store');
11
+
12
+ const dayMs = 24 * 60 * 60 * 1000;
13
+
14
+ class AuthRequiredError extends Error {}
15
+
16
+ function loginUrl(config, { tool, redirectUri, pkce }) {
17
+ const url = new URL(config.loginPageUrl);
18
+ url.search = new URLSearchParams({
19
+ tool,
20
+ redirectUri,
21
+ codeChallenge: pkce.challenge,
22
+ codeChallengeMethod: pkce.method,
23
+ state: pkce.state
24
+ }).toString();
25
+ return url.toString();
26
+ }
27
+
28
+ function summary(server, credential, now = Date.now()) {
29
+ if (!credential) return { loggedIn: false, server };
30
+ const iso = value => (Number.isFinite(value) ? new Date(value).toISOString() : null);
31
+ return {
32
+ loggedIn: Number.isFinite(credential.refreshExpiresAt) ? credential.refreshExpiresAt > now : true,
33
+ server,
34
+ clientId: credential.clientId,
35
+ tool: credential.tool,
36
+ loginAt: iso(credential.loginAt),
37
+ accessExpiresAt: iso(credential.accessExpiresAt),
38
+ refreshExpiresAt: iso(credential.refreshExpiresAt)
39
+ };
40
+ }
41
+
42
+ async function login({
43
+ env = process.env, browser = true, port = defaultPort, log = message => process.stderr.write(`${message}\n`),
44
+ open = openBrowser, timeoutMs
45
+ } = {}) {
46
+ const config = oauthConfig(env);
47
+ const pkce = createPkce();
48
+ const tool = detectTool(env);
49
+ const server = await startCallbackServer({
50
+ state: pkce.state, port, timeoutMs, allowedOrigin: new URL(config.loginPageUrl).origin
51
+ });
52
+ const url = loginUrl(config, { tool, redirectUri: server.redirectUri, pkce });
53
+ let code;
54
+ try {
55
+ const opened = browser ? await open(url) : false;
56
+ log(opened ? '已在浏览器打开登录页。如未自动打开,请手动访问:' : '请在浏览器打开以下地址完成授权:');
57
+ log(url);
58
+ code = await server.waitForCode();
59
+ } finally {
60
+ await server.close();
61
+ }
62
+ let tokens;
63
+ try {
64
+ tokens = await exchangeCode(config, {
65
+ code, verifier: pkce.verifier, redirectUri: server.redirectUri
66
+ });
67
+ } catch (error) {
68
+ if (error instanceof OAuthError && error.httpStatus === 404) {
69
+ error.message += '。本次登录未完成,未保存新凭证。请先核对 CLI 版本、' +
70
+ 'ZAN_SAAS_OAUTH_BASE_URL 和授权服务路由;浏览器授权完成不代表 CLI 已登录。';
71
+ }
72
+ throw error;
73
+ }
74
+ const credential = {
75
+ ...tokens, clientId: config.clientId, oauthBaseUrl: config.oauthBaseUrl, tool, loginAt: Date.now()
76
+ };
77
+ store.saveCredential(config.server, credential, env);
78
+ return summary(config.server, credential);
79
+ }
80
+
81
+ async function logout({ env = process.env } = {}) {
82
+ const config = oauthConfig(env);
83
+ const credential = store.readCredential(config.server, env);
84
+ // The selected shop belongs to this login, so it goes with it.
85
+ deleteContext(env);
86
+ if (!credential) return { loggedOut: true, revoked: false, server: config.server };
87
+ let remoteRevocation = 'revoked';
88
+ let httpStatus;
89
+ try {
90
+ await revokeToken(config, credential.refreshToken);
91
+ } catch (error) {
92
+ httpStatus = error instanceof OAuthError ? error.httpStatus : undefined;
93
+ remoteRevocation = httpStatus === undefined ? 'unknown' : 'failed';
94
+ }
95
+ let localCredential = 'deleted';
96
+ try {
97
+ store.deleteCredential(config.server, env);
98
+ } catch {
99
+ localCredential = 'delete_failed';
100
+ }
101
+ if (remoteRevocation !== 'revoked' || localCredential !== 'deleted') {
102
+ const error = new Error(localCredential === 'deleted'
103
+ ? '本机个人凭证已清除,服务端会话撤销未确认;请核对授权服务状态。'
104
+ : '本机个人凭证清理失败;请按退出结果核对本地凭证与服务端会话。');
105
+ error.cliDetails = {
106
+ command: 'auth.logout', server: config.server, localCredential, remoteRevocation,
107
+ ...(httpStatus === undefined ? {} : { httpStatus })
108
+ };
109
+ throw error;
110
+ }
111
+ return { loggedOut: true, revoked: true, server: config.server };
112
+ }
113
+
114
+ function status({ env = process.env } = {}) {
115
+ const { server } = oauthConfig(env);
116
+ return summary(server, store.readCredential(server, env));
117
+ }
118
+
119
+ function interactive(env) {
120
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY) && !env.CI &&
121
+ env.ZAN_SAAS_NO_BROWSER !== '1';
122
+ }
123
+
124
+ async function refreshIfNeeded(config, env, now) {
125
+ const key = config.server;
126
+ return store.withFileLock(env, async () => {
127
+ // Another process may have refreshed while this one waited for the lock.
128
+ const credential = store.readCredential(key, env);
129
+ if (!credential) return null;
130
+ if (!(credential.accessExpiresAt - now < dayMs)) return credential;
131
+ if (credential.refreshExpiresAt <= now) {
132
+ store.deleteCredential(key, env);
133
+ return null;
134
+ }
135
+ let tokens;
136
+ try {
137
+ tokens = await refreshTokens(config, credential.refreshToken);
138
+ } catch (error) {
139
+ if (error instanceof OAuthError && error.oauthError === 'invalid_grant') {
140
+ store.deleteCredential(key, env);
141
+ return null;
142
+ }
143
+ // Keep using an access token that has not expired yet when refresh is unreachable.
144
+ if (credential.accessExpiresAt > now) return credential;
145
+ throw error;
146
+ }
147
+ const next = { ...credential, ...tokens };
148
+ store.saveCredential(key, next, env);
149
+ return next;
150
+ });
151
+ }
152
+
153
+ // Resolves the user token for one request. An explicit ZAN_SAAS_USER_ACCESS_TOKEN keeps priority so
154
+ // existing controlled environments behave as before.
155
+ async function resolveAccessToken(env = process.env, { now = Date.now(), loginImpl = login } = {}) {
156
+ if (env.ZAN_SAAS_USER_ACCESS_TOKEN) {
157
+ return { token: env.ZAN_SAAS_USER_ACCESS_TOKEN, source: 'env' };
158
+ }
159
+ const config = oauthConfig(env);
160
+ let credential = store.readCredential(config.server, env);
161
+ if (credential && Number.isFinite(credential.accessExpiresAt) &&
162
+ credential.accessExpiresAt - now < dayMs) {
163
+ credential = await refreshIfNeeded(config, env, now);
164
+ }
165
+ if (!credential) {
166
+ if (!interactive(env)) {
167
+ throw new AuthRequiredError('未找到可用登录凭证,请先执行 `zan-saas auth login`;受控环境也可以配置 ZAN_SAAS_USER_ACCESS_TOKEN');
168
+ }
169
+ await loginImpl({ env });
170
+ credential = store.readCredential(config.server, env);
171
+ if (!credential) throw new AuthRequiredError('Login did not produce a credential');
172
+ }
173
+ return { token: credential.accessToken, source: 'login' };
174
+ }
175
+
176
+ module.exports = {
177
+ AuthRequiredError, login, loginUrl, logout, resolveAccessToken, status
178
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const maxLength = 32;
4
+
5
+ // Ordered by specificity; the first matching marker names the calling Agent tool.
6
+ const markers = [
7
+ ['claude-code', env => env.CLAUDECODE === '1' || Boolean(env.CLAUDE_CODE_ENTRYPOINT)],
8
+ ['codex', env => Boolean(env.CODEX_SANDBOX || env.CODEX_MANAGED_BY_NPM || env.CODEX_HOME)],
9
+ ['gemini-cli', env => env.GEMINI_CLI === '1'],
10
+ ['cursor', env => Boolean(env.CURSOR_TRACE_ID || env.CURSOR_AGENT)],
11
+ ['opencode', env => Boolean(env.OPENCODE)]
12
+ ];
13
+
14
+ function normalize(value) {
15
+ const text = String(value).trim().replace(/[^\w.-]+/g, '-').slice(0, maxLength);
16
+ return text || 'unknown';
17
+ }
18
+
19
+ function detectTool(env = process.env) {
20
+ if (env.ZAN_SAAS_TOOL) return normalize(env.ZAN_SAAS_TOOL);
21
+ const match = markers.find(([, test]) => test(env));
22
+ return match ? match[0] : 'unknown';
23
+ }
24
+
25
+ module.exports = { detectTool, maxLength };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const { validDomainName } = require('./domain-catalog');
5
+ const { validateAt } = require('../execution/command-policy');
6
+ const { riskLabels } = require('./help-text');
7
+
8
+ const namePattern = /^[a-z][a-z0-9-]*$/;
9
+ const taskPattern = /^\+[a-z][a-z0-9-]*$/;
10
+ const risks = new Set(['read', 'low', 'medium', 'high']);
11
+ const contracts = new WeakMap();
12
+ const domains = new WeakMap();
13
+
14
+ function group(parent, name) {
15
+ let command = parent.commands.find(entry => entry.name() === name);
16
+ if (!command) {
17
+ command = new Command(name).copyInheritedSettings(parent);
18
+ parent.addCommand(command);
19
+ }
20
+ if (command._actionHandler) throw new Error(`Command is already executable: ${name}`);
21
+ return command;
22
+ }
23
+
24
+ function registerDomain(program, definition) {
25
+ const { name, description, skill } = definition;
26
+ if (!validDomainName(name) || !description || skill !== `zan-saas-${name}`) {
27
+ throw new Error(`Invalid native domain: ${name}`);
28
+ }
29
+ const command = group(program, name);
30
+ if (domains.has(command) || command.commands.length) {
31
+ throw new Error(`Domain is already registered: ${name}`);
32
+ }
33
+ command.description(description);
34
+ command.allowExcessArguments(false)
35
+ .exitOverride()
36
+ .configureOutput({ writeErr: () => {} });
37
+ command.addHelpText('after', () =>
38
+ `${command.commands.some(child => child.name() !== 'help') ? '' :
39
+ '\n本领域尚未发布业务命令。\n'}` +
40
+ `\n领域指引:zan-saas skills read ${skill}`);
41
+ domains.set(command, { name, description, skill });
42
+ }
43
+
44
+ function publishedDomains(program) {
45
+ return program.commands.map(command => domains.get(command)).filter(Boolean);
46
+ }
47
+
48
+ function validateAvailability(availability = 'available') {
49
+ if (!['available', 'preview_only'].includes(availability)) {
50
+ throw new Error('Command availability must be available or preview_only');
51
+ }
52
+ return availability;
53
+ }
54
+
55
+ function addBusinessCommand(program, kind, domain, resource, command, risk, at, availability) {
56
+ const action = command.name();
57
+ const resources = Array.isArray(resource) ? resource : [resource];
58
+ const valid = namePattern.test(domain) &&
59
+ (kind === 'atomic'
60
+ ? resources.length > 0 && resources.every(value => typeof value === 'string' && namePattern.test(value)) &&
61
+ namePattern.test(action)
62
+ : kind === 'task' && resource === null && taskPattern.test(action));
63
+ if (!valid || !risks.has(risk) || !command._actionHandler) {
64
+ throw new Error(`Invalid ${kind} command path or risk: ${domain} ${resource || ''} ${action}`);
65
+ }
66
+ validateAt(at);
67
+ availability = validateAvailability(availability);
68
+ const domainGroup = group(program, domain);
69
+ const parent = kind === 'atomic' ? resources.reduce(group, domainGroup) : domainGroup;
70
+ if (parent.commands.some(entry => entry.name() === action)) {
71
+ throw new Error(`Command is already registered: ${domain} ${resource || ''} ${action}`);
72
+ }
73
+ const explicitExitCallback = command._exitCallback;
74
+ command.copyInheritedSettings(parent);
75
+ // Commander also copies the parent's exit callback; preserve a leaf's explicit policy.
76
+ if (explicitExitCallback) command.exitOverride(explicitExitCallback);
77
+ parent.addCommand(command);
78
+ const skill = domains.get(domainGroup)?.skill;
79
+ contracts.set(command, { kind, risk, skill, at, availability });
80
+ command.addHelpText('after', `\n风险:${riskLabels[risk]}${skill ? `\n领域指引:zan-saas skills read ${skill}` : ''}`);
81
+ return command;
82
+ }
83
+
84
+ function addAtomicCommand(program, domain, resource, command, risk, at, availability) {
85
+ return addBusinessCommand(program, 'atomic', domain, resource, command, risk, at, availability);
86
+ }
87
+
88
+ function addTaskCommand(program, domain, command, risk, at, availability) {
89
+ return addBusinessCommand(program, 'task', domain, null, command, risk, at, availability);
90
+ }
91
+
92
+ function commandPath(command) {
93
+ const parts = [];
94
+ for (let current = command; current.parent; current = current.parent) {
95
+ parts.unshift(current.name());
96
+ }
97
+ return parts.join('.');
98
+ }
99
+
100
+ function commandContract(path, command) {
101
+ if (['schema', 'doctor', 'skills.list', 'skills.read'].includes(path)) {
102
+ return { kind: 'system', risk: 'read', availability: 'available' };
103
+ }
104
+ if (['auth.status', 'auth.whoami', 'shop.list', 'shop.current'].includes(path)) {
105
+ return { kind: 'system', risk: 'read', availability: 'available' };
106
+ }
107
+ // Login and logout change the local credential and the server-side session; shop use and clear
108
+ // change the saved context.
109
+ if (['install', 'auth.login', 'auth.logout', 'shop.use', 'shop.clear'].includes(path)) {
110
+ return { kind: 'system', risk: 'low', availability: 'available' };
111
+ }
112
+ if (path === 'api') return { kind: 'raw-api', risk: 'unknown', availability: 'available' };
113
+ const parts = path.split('.');
114
+ const contract = contracts.get(command);
115
+ if (contract?.kind === 'atomic' &&
116
+ parts.length >= 3 && parts.every(part => namePattern.test(part))) return contract;
117
+ if (contract?.kind === 'task' &&
118
+ parts.length === 2 && namePattern.test(parts[0]) &&
119
+ taskPattern.test(parts[1])) return contract;
120
+ throw new Error(`Command has no valid published contract: ${path}`);
121
+ }
122
+
123
+ module.exports = {
124
+ registerDomain, publishedDomains, addAtomicCommand, addTaskCommand, commandContract, commandPath,
125
+ validateAvailability
126
+ };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const { Command, Help } = require('commander');
4
+ const { commandContract, commandPath } = require('./command-paths');
5
+ const { isAvailable } = require('../execution/command-policy');
6
+ const { chineseHelp, positionLabels } = require('./help-text');
7
+ const { parserFailure } = require('./parser-policy');
8
+
9
+ const helpCommands = new WeakSet();
10
+ const isContextHelp = command => helpCommands.has(command);
11
+
12
+ function businessContract(command) {
13
+ if (!command._actionHandler || isContextHelp(command)) return null;
14
+ const contract = commandContract(commandPath(command), command);
15
+ return ['atomic', 'task'].includes(contract.kind) ? contract : null;
16
+ }
17
+
18
+ function configureContextHelp(program, state, loadDomain) {
19
+ function applicable(command) {
20
+ if (isContextHelp(command)) return true;
21
+ const contract = businessContract(command);
22
+ if (contract) return isAvailable(contract.at, state.context);
23
+ const children = command.commands.filter(child => !isContextHelp(child));
24
+ return !children.length || children.some(applicable);
25
+ }
26
+
27
+ function positions(command, result = new Set()) {
28
+ const contract = businessContract(command);
29
+ if (contract) result.add(contract.at);
30
+ for (const child of command.commands) positions(child, result);
31
+ return [...result].sort();
32
+ }
33
+
34
+ const helpConfiguration = {
35
+ ...chineseHelp,
36
+ visibleCommands(command) {
37
+ const all = Help.prototype.visibleCommands.call(this, command);
38
+ return state.status === 'ready' ? all.filter(applicable) : all;
39
+ },
40
+ subcommandDescription(command) {
41
+ const description = Help.prototype.subcommandDescription.call(this, command);
42
+ if (command.parent === program) return description;
43
+ const at = positions(command);
44
+ const labels = at.includes('all') ? positionLabels.all :
45
+ at.map(position => positionLabels[position]).join('、');
46
+ return labels ? `${description} [适用位置:${labels}]` : description;
47
+ }
48
+ };
49
+ program.configureHelp(helpConfiguration);
50
+ program.addHelpText('afterAll', ({ command }) => {
51
+ const contract = businessContract(command);
52
+ const declared = contract ? `适用位置:${positionLabels[contract.at]}\n` : '';
53
+ if (state.status !== 'ready') {
54
+ const status = state.status === 'missing'
55
+ ? '当前店铺:未设置,已列出全部已发布命令。\n' +
56
+ '设置店铺:zan-saas shop list,然后执行 zan-saas shop use <kdtId>。'
57
+ : '店铺上下文无效,已列出全部已发布命令。\n' +
58
+ '检查 ZAN_SAAS_CONTEXT 中的 userId、hq、shopId 和 orgType,或清除无效配置。';
59
+ return `\n${declared}${status}`;
60
+ }
61
+ const status = `当前位置:${positionLabels[state.context.orgType]};实际权限由服务端校验。`;
62
+ if (contract) {
63
+ return `\n${declared}${status}\n${isAvailable(contract.at, state.context)
64
+ ? '此命令适用于当前位置。'
65
+ : `当前组织不可使用此命令,需要${positionLabels[contract.at]}上下文。`}`;
66
+ }
67
+ const children = command.commands.filter(child => !isContextHelp(child));
68
+ return `\n${status}${children.length && !children.some(applicable)
69
+ ? '\n当前位置没有适用的业务命令。' : ''}`;
70
+ });
71
+
72
+ function install(command = program) {
73
+ command.configureHelp(helpConfiguration).helpOption('-h, --help', '显示帮助');
74
+ if (command._actionHandler || isContextHelp(command)) return;
75
+ command.addHelpCommand(false);
76
+ if (!command.commands.some(child => isContextHelp(child))) {
77
+ const help = new Command('help').copyInheritedSettings(command)
78
+ .description('查看指定命令的帮助')
79
+ .argument('[path...]', '用空格或点分隔的子命令路径')
80
+ .allowExcessArguments(false)
81
+ .action(parts => {
82
+ let target = command;
83
+ for (const part of parts.flatMap(value => value.split('.'))) {
84
+ if (target === program) loadDomain(part);
85
+ const child = target.commands.find(child =>
86
+ child.name() === part || child.aliases().includes(part));
87
+ if (!child || isContextHelp(child)) {
88
+ throw parserFailure(target, 'unknown_command', { token: part });
89
+ }
90
+ target = child;
91
+ }
92
+ target.outputHelp();
93
+ });
94
+ helpCommands.add(help);
95
+ command.addCommand(help);
96
+ }
97
+ for (const child of command.commands) install(child);
98
+ }
99
+ return install;
100
+ }
101
+
102
+ module.exports = { configureContextHelp, isContextHelp };