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

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 +105 -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 +102 -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,54 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ QueryError, readApiEnvelope, assertApiSuccess, object
5
+ } = require('./query-api');
6
+ const { snapshotApiEnvironment } = require('../transport/api');
7
+
8
+ const bindingPath = 'youzan.shop.basic.get/3.0.0';
9
+ const nextStep = '核对原店铺、对象标识和服务端授权;店铺核验失败时业务请求未发送,不自动切换店铺、身份或重试。';
10
+
11
+ function failure(command, category, stage, scope = {}, requestState = 'not_sent', facts = {}) {
12
+ return new QueryError(category, stage, scope, facts, requestState, command, nextStep);
13
+ }
14
+
15
+ async function createBoundShopQuery(target, command, bindingStage = 'shop-binding', { onResponse, transportOptions } = {}) {
16
+ const scope = Object.freeze({ kdtId: target.kdtId });
17
+ if (!Number.isSafeInteger(scope.kdtId) || scope.kdtId < 1) {
18
+ throw failure(command, 'validation', 'scope');
19
+ }
20
+ let env;
21
+ try {
22
+ env = await snapshotApiEnvironment(undefined, scope.kdtId);
23
+ } catch (error) {
24
+ const rejected = failure(command, 'configuration', 'configuration', scope);
25
+ rejected.diagnosticReason = error.diagnosticReason;
26
+ throw rejected;
27
+ }
28
+ async function read(path, body, stage, successCodes = [200]) {
29
+ const { result, facts, rawBody, responseBytes } = await readApiEnvelope(path, body,
30
+ (category, details, state, at) => failure(command, category,
31
+ at === 'configuration' || category === transportOptions?.limitCategory ? at : stage,
32
+ scope, state, details), env, onResponse, transportOptions);
33
+ assertApiSuccess(result, category => {
34
+ throw failure(command, category, stage, scope, 'sent', facts);
35
+ }, successCodes);
36
+ return { data: result.data, facts, envelope: result, rawBody, responseBytes };
37
+ }
38
+ const binding = await read(bindingPath, {}, bindingStage);
39
+ const { data, facts } = binding;
40
+ if (!object(data) || !Number.isSafeInteger(data.sid) || data.sid < 1) {
41
+ throw failure(command, 'protocol', bindingStage, scope, 'sent', facts);
42
+ }
43
+ if (data.sid !== scope.kdtId) {
44
+ throw failure(command, 'scope_unresolved', bindingStage, scope, 'sent', facts);
45
+ }
46
+ return { read, binding };
47
+ }
48
+
49
+ async function createShopQuery(target, command, options = {}) {
50
+ const { read } = await createBoundShopQuery(target, command, 'shop-binding', options);
51
+ return read;
52
+ }
53
+
54
+ module.exports = { createShopQuery, createBoundShopQuery, failure, bindingPath };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { readContext } = require('../identity/context');
4
+
5
+ // Complete only the CLI option, before the leaf validates its normalized business input.
6
+ // The invocation context is captured when its command is built; authorization stays in the leaf.
7
+ function withTargetShopDefault(createCommand) {
8
+ return function createCommandWithTargetShopDefault(options = {}) {
9
+ const state = readContext(options.context);
10
+ const command = createCommand(options);
11
+ const target = command.options.find(option => option.long === '--kdt-id');
12
+ if (!target) return command;
13
+ target.makeOptionMandatory(false);
14
+ command.hook('preAction', () => {
15
+ if (!Object.hasOwn(command.opts(), 'kdtId') && state.status === 'ready') {
16
+ command.setOptionValueWithSource('kdtId', state.context.shopId, 'context');
17
+ }
18
+ });
19
+ return command;
20
+ };
21
+ }
22
+
23
+ module.exports = { withTargetShopDefault };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { readContext } = require('../identity/context');
4
+
5
+ function resolveTargetShop(options, context, command) {
6
+ const state = readContext(context);
7
+ const explicit = Object.hasOwn(options, 'kdtId');
8
+ function reject(message, category, stage, targetScope = {}, reason) {
9
+ const error = new Error(message);
10
+ error.diagnosticReason = reason;
11
+ error.cliDetails = {
12
+ category, stage, command, targetScope, requestState: 'not_sent',
13
+ nextStep: '执行前需要登录并选择店铺:未登录先执行 `zan-saas auth login`;登录后执行 `zan-saas shop list` 和 `zan-saas shop use <kdtId>`。目标解析不证明凭证授权,不自动切换店铺或身份。'
14
+ };
15
+ error.issues = [{
16
+ source: stage === 'validation' ? 'argv' : 'context',
17
+ reason: stage === 'validation' ? 'invalid_format' : 'constraint',
18
+ ...(stage === 'validation' ? { option: '--kdt-id' } : {})
19
+ }];
20
+ throw error;
21
+ }
22
+
23
+ if (state.status === 'invalid') {
24
+ reject('当前用户上下文无效,请检查 userId、hq、shopId 和 orgType',
25
+ 'configuration', 'context', {}, 'context_invalid');
26
+ }
27
+ if (!explicit && state.status === 'missing') {
28
+ reject('缺少目标店铺;请先登录选店,或在 --dry-run 中显式提供 --kdt-id',
29
+ 'configuration', 'context', {}, 'target_missing');
30
+ }
31
+
32
+ const raw = explicit ? options.kdtId : state.context.shopId;
33
+ if (typeof raw !== 'string' || !/^[1-9]\d*$/.test(raw) ||
34
+ !Number.isSafeInteger(Number(raw))) {
35
+ reject(`${explicit ? '--kdt-id' : 'context.shopId'} must be a positive safe integer`,
36
+ explicit ? 'validation' : 'configuration', explicit ? 'validation' : 'context');
37
+ }
38
+ const scope = Object.freeze({ kdtId: Number(raw) });
39
+ if (state.status === 'missing' && options.dryRun !== true) {
40
+ reject('真实执行前需要登录并选择店铺', 'configuration', 'context', scope, 'context_missing');
41
+ }
42
+ if (state.status === 'ready' && state.context.orgType === 'shop' &&
43
+ String(scope.kdtId) !== state.context.shopId) {
44
+ reject('--kdt-id must match context.shopId in shop context', 'permission', 'scope', scope);
45
+ }
46
+ return scope;
47
+ }
48
+
49
+ module.exports = { resolveTargetShop };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const { oauthConfig } = require('../auth/config');
4
+ const { readStore, storePath, writeStore } = require('../auth/credential-store');
5
+ const { createContext, readContext } = require('./context');
6
+
7
+ const fileName = 'context.json';
8
+
9
+ function contextPath(env = process.env) {
10
+ return storePath(fileName, env);
11
+ }
12
+
13
+ // The selected shop is kept per oauth origin, like the credential it was chosen with.
14
+ function readSavedContext(env = process.env) {
15
+ const entry = readStore(fileName, env)[oauthConfig(env).server];
16
+ const state = readContext(entry?.context);
17
+ if (state.status !== 'ready') return null;
18
+ return Object.freeze({
19
+ context: state.context,
20
+ shopName: typeof entry.shopName === 'string' ? entry.shopName : null,
21
+ savedAt: typeof entry.savedAt === 'string' ? entry.savedAt : null
22
+ });
23
+ }
24
+
25
+ function saveContext(input, { shopName } = {}, env = process.env) {
26
+ const context = createContext(input);
27
+ const { server } = oauthConfig(env);
28
+ const data = readStore(fileName, env);
29
+ data[server] = { context, ...(shopName ? { shopName } : {}), savedAt: new Date().toISOString() };
30
+ writeStore(fileName, data, env);
31
+ return context;
32
+ }
33
+
34
+ function deleteContext(env = process.env) {
35
+ const { server } = oauthConfig(env);
36
+ const data = readStore(fileName, env);
37
+ if (!Object.hasOwn(data, server)) return false;
38
+ delete data[server];
39
+ writeStore(fileName, data, env);
40
+ return true;
41
+ }
42
+
43
+ // ZAN_SAAS_CONTEXT wins, even when invalid; otherwise the shop chosen with `shop use`.
44
+ function contextInput(env = process.env) {
45
+ if (env.ZAN_SAAS_CONTEXT) return env.ZAN_SAAS_CONTEXT;
46
+ try {
47
+ return readSavedContext(env)?.context;
48
+ } catch {
49
+ // Unreadable files or bad oauth configuration behave like no saved shop.
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ module.exports = { contextInput, contextPath, deleteContext, readSavedContext, saveContext };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const fields = ['userId', 'hq', 'shopId', 'orgType'];
4
+ const shopId = /^[1-9]\d*$/;
5
+
6
+ function createContext(input) {
7
+ if (!input || typeof input !== 'object' || Array.isArray(input) ||
8
+ Object.keys(input).some(key => !fields.includes(key)) ||
9
+ fields.some(key => !Object.hasOwn(input, key) ||
10
+ typeof input[key] !== 'string' || !input[key].trim())) {
11
+ throw new Error('User context requires only userId, hq, shopId and orgType strings');
12
+ }
13
+ const { userId, hq, shopId: selectedShop, orgType } = input;
14
+ if (!shopId.test(hq) || !shopId.test(selectedShop)) {
15
+ throw new Error('Context hq and shopId must be positive integer ID strings');
16
+ }
17
+ if (!['hq', 'shop'].includes(orgType)) {
18
+ throw new Error('Context orgType must be hq or shop');
19
+ }
20
+ if (orgType === 'hq' && hq !== selectedShop) {
21
+ throw new Error('Context orgType conflicts with hq and shopId');
22
+ }
23
+ return Object.freeze({ userId, hq, shopId: selectedShop, orgType });
24
+ }
25
+
26
+ function readContext(input) {
27
+ if (input === undefined || input === null || input === '') {
28
+ return Object.freeze({ status: 'missing' });
29
+ }
30
+ try {
31
+ const value = typeof input === 'string' ? JSON.parse(input) : input;
32
+ return Object.freeze({ status: 'ready', context: createContext(value) });
33
+ } catch {
34
+ return Object.freeze({ status: 'invalid' });
35
+ }
36
+ }
37
+
38
+ module.exports = { createContext, readContext };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const { createInterface } = require('node:readline/promises');
8
+ const { hosts, stat, selectedHosts, detectedHosts, readTemplate, prepareSkill, writeSkill } = require('./skill');
9
+ const { npmFailure } = require('./npm-error');
10
+
11
+ const packageName = '@youzan-cloud/zan-saas-cli';
12
+ const root = path.resolve(__dirname, '../..');
13
+ const skillNextStep = '请在 Agent 技能列表中确认 zan-saas 已启用,再重新加载技能或打开新会话。未发现时,核对宿主的 Skill 根目录,再用 --directory <目录> 安装入口。';
14
+
15
+ function npmRunner(env) {
16
+ let npmCli = env.npm_execpath;
17
+ if (!npmCli || !stat(npmCli)?.isFile() || !/npm-cli\.js$/i.test(npmCli)) {
18
+ npmCli = process.platform === 'win32'
19
+ ? path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js') : undefined;
20
+ }
21
+ return args => {
22
+ const result = spawnSync(npmCli ? process.execPath : 'npm', npmCli ? [npmCli, ...args] : args,
23
+ { encoding: 'utf8', env, timeout: 180000, maxBuffer: 2 * 1024 * 1024, shell: false });
24
+ if (result.error || result.status !== 0) {
25
+ throw npmFailure(result, args, env);
26
+ }
27
+ return result.stdout.trim();
28
+ };
29
+ }
30
+
31
+ function readPackage(directory) {
32
+ try { return JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8')); }
33
+ catch (error) {
34
+ if (error.code === 'ENOENT') return undefined;
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ async function chooseHosts(options, { home, cwd, input, output }) {
40
+ if (options.directory !== undefined) {
41
+ if (!options.directory.trim() || options.host !== undefined) {
42
+ throw new Error('--directory 与 --host 二选一。');
43
+ }
44
+ return [];
45
+ }
46
+ if (options.host !== undefined) return selectedHosts(options.host);
47
+ const detected = detectedHosts(home, cwd);
48
+ if (options.yes) {
49
+ if (detected.length === 0) throw new Error('未检测到 Agent;请用 --host 指定,例如 --host workbuddy。');
50
+ return detected;
51
+ }
52
+ if (!input.isTTY || !output.isTTY) {
53
+ throw new Error('非交互安装请指定宿主,例如 zan-saas install --yes --host workbuddy。');
54
+ }
55
+ output.write('安装有赞 SaaS CLI 与 Agent Skill,默认使用个人目录。\n');
56
+ hosts.forEach((host, index) => output.write(` ${index + 1}. ${host.label}${detected.includes(host) ? '(已检测到)' : ''}\n`));
57
+ const defaults = detected.map(host => hosts.indexOf(host) + 1).join(',');
58
+ const readline = createInterface({ input, output });
59
+ try {
60
+ const answer = (await readline.question(`选择 Agent,多个用逗号分隔${defaults ? `,回车使用 ${defaults}` : ''}:`)).trim() || defaults;
61
+ const choices = answer.split(',').map(value => value.trim());
62
+ if (choices.some(value => !/^[1-9]\d*$/.test(value) || Number(value) > hosts.length)) {
63
+ throw new Error('未选择有效宿主,安装已停止。');
64
+ }
65
+ return [...new Set(choices.map(value => hosts[Number(value) - 1]))];
66
+ } finally { readline.close(); }
67
+ }
68
+
69
+ function globalState(npm, source) {
70
+ const prefix = npm(['prefix', '-g']);
71
+ const modules = npm(['root', '-g']);
72
+ if (!path.isAbsolute(prefix) || !path.isAbsolute(modules)) throw new Error('npm 未返回有效全局目录。');
73
+ const directory = path.join(modules, '@youzan-cloud/zan-saas-cli');
74
+ const launcher = path.join(directory, 'bin/zan-saas-cli.js');
75
+ const executable = path.join(prefix, process.platform === 'win32' ? 'zan-saas.cmd' : 'bin/zan-saas');
76
+ const installed = readPackage(directory);
77
+ let legacyPackage;
78
+ if (stat(executable)) {
79
+ let owns;
80
+ if (process.platform === 'win32') {
81
+ const shim = fs.readFileSync(executable, 'utf8').replaceAll('\\', '/');
82
+ owns = name => readPackage(path.join(modules, name))?.name === name &&
83
+ shim.includes(`node_modules/${name}/bin/zan-saas-cli.js`);
84
+ } else {
85
+ const actual = fs.realpathSync(executable);
86
+ owns = name => {
87
+ const directory = path.join(modules, name);
88
+ const entry = path.join(directory, 'bin/zan-saas-cli.js');
89
+ return readPackage(directory)?.name === name && stat(entry) && actual === fs.realpathSync(entry);
90
+ };
91
+ }
92
+ if (!owns(packageName)) {
93
+ legacyPackage = ['zan-saas-cli', '@youzan-cloud/zan-saas'].find(owns);
94
+ if (!legacyPackage) throw new Error(`现有 zan-saas 命令不属于本项目,已保留:${executable}`);
95
+ }
96
+ }
97
+ const reuse = installed?.name === packageName && installed.version === source.version &&
98
+ source.name === packageName && stat(launcher)?.isFile() && Boolean(stat(executable)) && !legacyPackage;
99
+ return { directory, launcher, legacyPackage, reuse };
100
+ }
101
+
102
+ async function install(options, { env = process.env, home = os.homedir(), cwd = process.cwd(),
103
+ input = process.stdin, output = process.stderr, packageRoot = root, npm = npmRunner(env) } = {}) {
104
+ const scope = options.scope || 'user';
105
+ if (!['user', 'project'].includes(scope)) throw new Error('--scope 仅支持 user 或 project。');
106
+ const selected = await chooseHosts(options, { home, cwd, input, output });
107
+ const source = readPackage(packageRoot);
108
+ if (!source) throw new Error('当前 npm 包缺少版本信息。');
109
+ let packageSource = `${packageName}@${source.name === packageName ? source.version : 'latest'}`;
110
+ if (options.package !== undefined) {
111
+ packageSource = path.resolve(cwd, options.package);
112
+ if (!packageSource.endsWith('.tgz') || !stat(packageSource)?.isFile()) {
113
+ throw new Error('--package 须指定本地 npm .tgz 文件。');
114
+ }
115
+ }
116
+ const state = globalState(npm, source);
117
+ const template = readTemplate(packageRoot);
118
+ const targets = options.directory !== undefined
119
+ ? [{ host: 'custom', filename: path.join(path.resolve(cwd, options.directory), 'zan-saas/SKILL.md') }]
120
+ : selected.map(host => ({ host: host.name,
121
+ filename: path.join(scope === 'user' ? home : cwd,
122
+ scope === 'project' ? host.projectDirectory || host.directory : host.directory,
123
+ 'skills/zan-saas/SKILL.md') }));
124
+ const context = { node: process.execPath, launcher: state.launcher };
125
+ let plans = targets.map(target => ({ ...target, ...prepareSkill(target.filename, template, context) }));
126
+ const preview = { cli: { action: state.reuse ? 'reuse' : 'install', package: packageSource },
127
+ scope, skills: plans.map(plan => ({ host: plan.host, path: plan.filename, action: plan.action })) };
128
+ if (options.dryRun) return { success: true, ...preview, dryRun: true };
129
+ if (!state.reuse) {
130
+ if (state.legacyPackage) {
131
+ npm(['uninstall', '-g', state.legacyPackage, '--ignore-scripts', '--no-audit', '--no-fund']);
132
+ }
133
+ npm(['install', '-g', packageSource, '--ignore-scripts', '--engine-strict', '--no-audit', '--no-fund',
134
+ '--registry=https://registry.npmjs.org/']);
135
+ }
136
+ const installed = readPackage(state.directory);
137
+ if (installed?.name !== packageName ||
138
+ (source.name === packageName && installed.version !== source.version)) {
139
+ throw new Error('全局安装结果与本次 npm 包不一致,Skill 尚未写入。');
140
+ }
141
+ const verified = spawnSync(process.execPath, [state.launcher, '--version'],
142
+ { env, encoding: 'utf8', timeout: 30000 });
143
+ if (verified.status !== 0 || verified.stdout.trim() !== installed.version || verified.stderr) {
144
+ throw new Error('全局 CLI 版本验证失败,Skill 尚未写入。');
145
+ }
146
+ const installedTemplate = readTemplate(state.directory);
147
+ plans = targets.map(target => ({ ...target, ...prepareSkill(target.filename, installedTemplate, context) }));
148
+ try {
149
+ for (const plan of new Map(plans.map(plan => [plan.filename, plan])).values()) writeSkill(plan);
150
+ } catch (error) {
151
+ throw new Error(`入口安装未完成:${error.message}。已完成的安装会保留,修复后可重复执行。`);
152
+ }
153
+ return { success: true, version: installed.version, launcher: state.launcher, scope,
154
+ skills: plans.map(plan => ({ host: plan.host, path: plan.filename, action: plan.action })),
155
+ cliAction: state.reuse ? 'reuse' : 'install', skillLoading: 'not_verified', nextStep: skillNextStep };
156
+ }
157
+
158
+ module.exports = { install, chooseHosts, globalState, npmRunner };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ // Only structured npm codes and rename paths are retained. Free-form npm output can contain credentials.
6
+ const codes = new Set([
7
+ 'ENOTEMPTY', 'EEXIST', 'EACCES', 'EPERM', 'ENOENT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT',
8
+ 'ECONNRESET', 'ECONNREFUSED', 'EAI_AGAIN', 'ENOTFOUND', 'ENETUNREACH', 'EHOSTUNREACH',
9
+ 'CERT_HAS_EXPIRED', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'SELF_SIGNED_CERT_IN_CHAIN',
10
+ 'DEPTH_ZERO_SELF_SIGNED_CERT', 'ERR_TLS_CERT_ALTNAME_INVALID', 'EBADENGINE',
11
+ 'E404', 'E401', 'E403', 'EUSAGE', 'ENOSPC', 'EROFS', 'EINTEGRITY', 'ERR_INVALID_URL',
12
+ 'ENOBUFS', 'EPIPE', 'EMFILE', 'ENFILE', 'EBUSY'
13
+ ]);
14
+ const operations = {
15
+ prefix: '读取 npm 全局前缀失败', root: '读取 npm 全局模块目录失败',
16
+ uninstall: '卸载旧版本失败', install: '安装 CLI 失败'
17
+ };
18
+ const networkCodes = new Set(['ECONNRESET', 'ECONNREFUSED', 'EAI_AGAIN', 'ENOTFOUND', 'ENETUNREACH', 'EHOSTUNREACH']);
19
+ const certificateCodes = new Set(['CERT_HAS_EXPIRED', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
20
+ 'SELF_SIGNED_CERT_IN_CHAIN', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'ERR_TLS_CERT_ALTNAME_INVALID']);
21
+
22
+ function npmField(stderr, name) {
23
+ const match = new RegExp(`^npm (?:error|ERR!) ${name} (.+)$`, 'm').exec(stderr);
24
+ return match?.[1].trim();
25
+ }
26
+
27
+ function diagnosticPath(value, env) {
28
+ if (!value || value.length > 1024 || /[\x00-\x1f\x7f?&#]/.test(value) ||
29
+ /(?:https?|file):|(?:token|secret|password|authorization|_auth)\s*[:=]/i.test(value) ||
30
+ (!path.isAbsolute(value) && !path.win32.isAbsolute(value))) return undefined;
31
+ for (const [name, secret] of Object.entries(env)) {
32
+ if (/(?:token|secret|password|authorization|credential|(?:^|_)key$)/i.test(name) &&
33
+ typeof secret === 'string' && secret.length >= 4) value = value.replaceAll(secret, '[REDACTED]');
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function advice(code, failedToStart) {
39
+ if (code === 'ENOTEMPTY') return [
40
+ '目标目录非空,npm 无法完成操作。',
41
+ '核对 npm 的冲突路径;确认目标是旧安装残留后,先备份并移走,再重新运行安装器。安装器不会自动删除目录。'
42
+ ];
43
+ if (code === 'EEXIST') return ['目标文件或目录已存在。',
44
+ '核对 npm 日志中的冲突路径及归属,保留其他程序的文件;处理冲突后再重新运行安装器。'];
45
+ if (code === 'EACCES' || code === 'EPERM') return ['npm 访问文件或目录时权限不足。',
46
+ '核对 npm 日志中的路径及写入权限;全局安装应使用当前用户可写的 npm prefix,不要强制覆盖。'];
47
+ if (code === 'ENOENT') return failedToStart
48
+ ? ['无法启动 npm。', '确认 npm 已安装且当前进程的 PATH 可以找到它,再重新运行安装器。']
49
+ : ['npm 找不到所需文件或目录。', '查看 npm 日志中的缺失路径,检查本地安装包或安装目录是否存在,再重新运行安装器。'];
50
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return ['npm 操作超时。',
51
+ '检查网络和代理,并核对当前安装状态;确认后再重新运行安装器。'];
52
+ if (networkCodes.has(code)) return ['npm 网络连接失败。',
53
+ '检查网络、代理和 registry 的连接;恢复后再重新运行安装器。'];
54
+ if (certificateCodes.has(code)) return ['npm 无法验证连接证书。',
55
+ '检查系统时间、代理证书和 npm 证书配置;不要关闭证书校验。'];
56
+ if (code === 'ENOBUFS') return ['npm 输出超过安装器的读取上限。',
57
+ '查看 npm 日志并核对当前安装状态,确认失败步骤后再重新运行安装器。'];
58
+ if (code === 'ENOSPC') return ['安装所需的磁盘空间不足。', '检查磁盘空间,释放足够空间后再重新运行安装器。'];
59
+ if (code === 'EROFS') return ['安装目录所在文件系统只读。', '改用可写的 npm prefix,再重新运行安装器。'];
60
+ if (code === 'EBADENGINE') return ['当前 Node.js 或 npm 版本不符合安装要求。',
61
+ '按安装指南使用支持的 Node.js 和 npm 版本,再重新运行安装器。'];
62
+ if (code === 'E401' || code === 'E403') return ['npm registry 拒绝访问。',
63
+ '检查 npm registry 的登录状态及包的访问权限;这不是 CLI 的业务登录错误。'];
64
+ if (code === 'E404') return ['npm registry 中找不到指定包或版本。',
65
+ '核对包名、版本和 registry;尚未发布的候选包请使用 --package 指定本地 tgz。'];
66
+ if (code === 'EINTEGRITY') return ['npm 包完整性校验失败。',
67
+ '检查安装包、缓存和 registry 返回内容;不要跳过完整性校验。'];
68
+ return ['npm 未能完成操作。', '查看 npm 日志,核对失败步骤和当前安装状态后再处理;不要直接强制覆盖或删除全局目录。'];
69
+ }
70
+
71
+ function npmFailure(result, args, env = {}) {
72
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
73
+ const reportedCode = result.error?.code || npmField(stderr, 'code');
74
+ const npmCode = codes.has(reportedCode) ? reportedCode : null;
75
+ const operation = Object.hasOwn(operations, args[0]) ? args[0] : 'other';
76
+ const [reason, nextStep] = advice(npmCode, Boolean(result.error));
77
+ const error = new Error(`${operations[operation] || '执行 npm 失败'}${npmCode ? `(${npmCode})` : ''}:${reason}`);
78
+ error.cliDetails = {
79
+ category: 'installation', stage: 'npm', operation, npmCode,
80
+ exitCode: Number.isInteger(result.status) && result.status >= 0 ? result.status : null,
81
+ signal: typeof result.signal === 'string' && /^SIG[A-Z0-9]{1,12}$/.test(result.signal) ? result.signal : null,
82
+ nextStep
83
+ };
84
+ if (npmCode === 'ENOTEMPTY' && npmField(stderr, 'syscall') === 'rename') {
85
+ for (const [name, field] of [['conflictPath', 'path'], ['destinationPath', 'dest']]) {
86
+ const value = diagnosticPath(npmField(stderr, field), env);
87
+ if (value !== undefined) error.cliDetails[name] = value;
88
+ }
89
+ }
90
+ return error;
91
+ }
92
+
93
+ module.exports = { npmFailure };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { createHash, randomUUID } = require('node:crypto');
6
+
7
+ const hosts = [
8
+ { name: 'workbuddy', label: 'WorkBuddy', directory: '.workbuddy', projectDirectory: '.codebuddy',
9
+ detect: ['.workbuddy'], projectDetect: [] },
10
+ { name: 'codebuddy', label: 'CodeBuddy', directory: '.codebuddy', detect: ['.codebuddy'] },
11
+ { name: 'codex', label: 'Codex', directory: '.agents', detect: ['.codex'] },
12
+ { name: 'claude-code', label: 'Claude Code', directory: '.claude', detect: ['.claude'] },
13
+ { name: 'cursor', label: 'Cursor', directory: '.cursor', detect: ['.cursor'] },
14
+ { name: 'trae', label: 'Trae(中国版)', directory: '.trae-cn',
15
+ projectDirectory: '.trae', detect: ['.trae-cn'] }
16
+ ];
17
+
18
+ function stat(filename) {
19
+ try { return fs.lstatSync(filename); }
20
+ catch (error) {
21
+ if (error.code === 'ENOENT') return undefined;
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ function sha256(text) {
27
+ return createHash('sha256').update(text).digest('hex');
28
+ }
29
+
30
+ function selectedHosts(value) {
31
+ const names = value.split(',').map(name => name.trim());
32
+ if (names.some(name => !name)) throw new Error('--host 使用逗号分隔宿主名称。');
33
+ return [...new Set(names.map(name => name === 'claude' ? 'claude-code' : name))].map(name => {
34
+ const host = hosts.find(host => host.name === name);
35
+ if (!host) throw new Error(`不支持宿主 ${name};请查看 zan-saas install --help。`);
36
+ return host;
37
+ });
38
+ }
39
+
40
+ function detectedHosts(home, cwd) {
41
+ return hosts.filter(host => host.detect.some(name => stat(path.join(home, name))?.isDirectory()) ||
42
+ (host.projectDetect || [host.projectDirectory || host.directory])
43
+ .some(name => stat(path.join(cwd, name))?.isDirectory()));
44
+ }
45
+
46
+ function readTemplate(root) {
47
+ const document = fs.readFileSync(path.join(root, 'skills/zan-saas/references/host-entry.md'), 'utf8')
48
+ .replaceAll('\r\n', '\n');
49
+ const blocks = [...document.matchAll(/```markdown\n([\s\S]*?)\n```/g)];
50
+ const text = blocks.length === 1 ? `${blocks[0][1].trim()}\n` : '';
51
+ if (!/^---\nname: zan-saas\ndescription: [^\n]+\n---\n/.test(text) ||
52
+ !text.includes('zan-saas --version') || !text.includes('zan-saas skills read zan-saas')) {
53
+ throw new Error('当前包未提供有效的宿主入口模板。');
54
+ }
55
+ return text;
56
+ }
57
+
58
+ function shellArgument(value, platform) {
59
+ if (/[\r\n`]/.test(value)) throw new Error('CLI 安装路径包含不支持的字符。');
60
+ if (platform === 'win32') {
61
+ return `'${value.replaceAll("'", "''")}'`;
62
+ }
63
+ return `'${value.replaceAll("'", "'\\''")}'`;
64
+ }
65
+
66
+ function prepareSkill(filename, template, { node, launcher, platform = process.platform }) {
67
+ const invocation = `${platform === 'win32' ? '& ' : ''}${shellArgument(node, platform)} ${shellArgument(launcher, platform)}`;
68
+ const text = template.replaceAll('`zan-saas --version`', `\`${invocation} --version\``)
69
+ .replaceAll('`zan-saas skills read zan-saas`', `\`${invocation} skills read zan-saas\``) +
70
+ '\n后续指引中的 `zan-saas` 均使用上述 Node.js 与 CLI 入口调用。\n' +
71
+ (platform === 'win32' ? 'Windows 使用 PowerShell 执行上述命令。\n' : '');
72
+ const content = `${text}\n<!-- zan-saas-cli host-entry sha256:${sha256(text)} -->\n`;
73
+ const directory = path.dirname(filename);
74
+ const folder = stat(directory);
75
+ if (folder && (!folder.isDirectory() || folder.isSymbolicLink())) {
76
+ throw new Error(`目标不是普通目录,已保留:${directory}`);
77
+ }
78
+ const existing = stat(filename);
79
+ if (existing && !existing.isFile()) throw new Error(`目标不是普通文件,已保留:${filename}`);
80
+ const previous = existing ? fs.readFileSync(filename, 'utf8') : undefined;
81
+ if (previous !== undefined && previous !== content && previous !== template) {
82
+ const owned = /^([\s\S]*)\n<!-- zan-saas-cli host-entry sha256:([a-f0-9]{64}) -->\n$/.exec(previous);
83
+ if (!owned || sha256(owned[1]) !== owned[2]) {
84
+ throw new Error(`同名 Skill 不属于本安装器或已被修改,已保留:${filename}`);
85
+ }
86
+ }
87
+ if (!existing && folder && fs.readdirSync(directory).length > 0) {
88
+ throw new Error(`同名目录包含其他文件,已保留:${directory}`);
89
+ }
90
+ return { filename, content, previous, mode: existing ? existing.mode & 0o777 : 0o644,
91
+ action: previous === content ? 'unchanged' : existing ? 'update' : 'create' };
92
+ }
93
+
94
+ function writeSkill(plan) {
95
+ if (plan.action === 'unchanged') return;
96
+ const { filename, content, previous, mode } = plan;
97
+ const directory = path.dirname(filename);
98
+ fs.mkdirSync(directory, { recursive: true });
99
+ if (previous === undefined) {
100
+ fs.writeFileSync(filename, content, { flag: 'wx', mode });
101
+ return;
102
+ }
103
+ const temporary = path.join(directory, `.SKILL-${randomUUID()}.tmp`);
104
+ try {
105
+ fs.writeFileSync(temporary, content, { flag: 'wx', mode });
106
+ if (!stat(filename)?.isFile() || fs.readFileSync(filename, 'utf8') !== previous) {
107
+ throw new Error(`安装期间目标发生变化,已保留:${filename}`);
108
+ }
109
+ fs.renameSync(temporary, filename);
110
+ } finally { fs.rmSync(temporary, { force: true }); }
111
+ }
112
+
113
+ module.exports = { hosts, stat, selectedHosts, detectedHosts, readTemplate, prepareSkill, writeSkill };