@leed-ai/cli 0.0.0-stage → 6.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/LICENSE.md +57 -0
  2. package/README.md +125 -2
  3. package/dist/LICENSE.md +57 -0
  4. package/dist/acorn-DAgDT1om.js +5087 -0
  5. package/dist/angular-l0_4PK8N.js +3598 -0
  6. package/dist/authShared-Dray38Ls.js +1161 -0
  7. package/dist/babel-CJeRQd5u.js +10038 -0
  8. package/dist/buildResults-tiq1nugP.js +21180 -0
  9. package/dist/chunk-1VJ9lAw7.js +42 -0
  10. package/dist/cliAuth-38JjD6hw.js +15 -0
  11. package/dist/env-B0bHiXJJ.js +29 -0
  12. package/dist/errors-CqKamGCu.js +481 -0
  13. package/dist/estree-E8WG8UHj.js +6930 -0
  14. package/dist/flow-6xaRIbPQ.js +52255 -0
  15. package/dist/glimmer-aJPjsfbT.js +7272 -0
  16. package/dist/graphql-mcgtPEiZ.js +1901 -0
  17. package/dist/html--eeea3Kl.js +6553 -0
  18. package/dist/leed.eleventy.mjs +6525 -0
  19. package/dist/leed.mjs +88952 -0
  20. package/dist/leed.sources.json +798 -0
  21. package/dist/leedLogger-Zkada5aO.js +78 -0
  22. package/dist/markdown-D73jNKwx.js +6836 -0
  23. package/dist/meriyah-CMp1nDDW.js +8932 -0
  24. package/dist/package.json +101 -0
  25. package/dist/postcss-h4fBr2Fn.js +6760 -0
  26. package/dist/prettier-CwFSKxfN.js +16821 -0
  27. package/dist/static/customer-site-worker/package.json +9 -0
  28. package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
  29. package/dist/static/customer-site-worker/src/constants.ts +20 -0
  30. package/dist/static/customer-site-worker/src/cors.ts +120 -0
  31. package/dist/static/customer-site-worker/src/index.ts +242 -0
  32. package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
  33. package/dist/static/customer-site-worker/src/proxy.ts +92 -0
  34. package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
  35. package/dist/static/customer-site-worker/src/tier.ts +40 -0
  36. package/dist/static/customer-site-worker/src/types.ts +82 -0
  37. package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
  38. package/dist/static/customer-site-worker/tsconfig.json +19 -0
  39. package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
  40. package/dist/static/install/husky/colors.mjs +5 -0
  41. package/dist/static/install/husky/hooks/commit-msg +1 -0
  42. package/dist/static/install/husky/hooks/pre-commit +1 -0
  43. package/dist/static/install/husky/hooks/pre-push +1 -0
  44. package/dist/static/install/husky/valid-command.mjs +29 -0
  45. package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
  46. package/dist/static/install/package.json +10 -0
  47. package/dist/static/install/packages.txt +4 -0
  48. package/dist/static/install/tasks.json +111 -0
  49. package/dist/static/javascript/detectIncognito.js +195 -0
  50. package/dist/static/javascript/documentation.js +2781 -0
  51. package/dist/static/javascript/fp/fp.esm.js +2896 -0
  52. package/dist/static/javascript/fp/fp.js +3067 -0
  53. package/dist/static/javascript/live-search.js +986 -0
  54. package/dist/static/javascript/lunr-search.js +843 -0
  55. package/dist/static/javascript/timeme.js +451 -0
  56. package/dist/static/javascript/tracker/webmcp.js +67 -0
  57. package/dist/static/javascript/tracker/whisper.js +1 -0
  58. package/dist/static/javascript/utilities.js +819 -0
  59. package/dist/static/javascript/versions.json +74 -0
  60. package/dist/static/javascript/zoomable.js +488 -0
  61. package/dist/static/site/_data/eleventyComputed.js +228 -0
  62. package/dist/static/site/_data/leed-form-data.json +253 -0
  63. package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
  64. package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
  65. package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
  66. package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
  67. package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
  68. package/dist/static/site/_includes/leed/api/object.hbs +110 -0
  69. package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
  70. package/dist/static/site/_includes/leed/api/property.hbs +153 -0
  71. package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
  72. package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
  73. package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
  74. package/dist/static/site/_includes/leed/api/security.hbs +75 -0
  75. package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
  76. package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
  77. package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
  78. package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
  79. package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
  80. package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
  81. package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
  82. package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
  83. package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
  84. package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
  85. package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
  86. package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
  87. package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
  88. package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
  89. package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
  90. package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
  91. package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
  92. package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
  93. package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
  94. package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
  95. package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
  96. package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
  97. package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
  98. package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
  99. package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
  100. package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
  101. package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
  102. package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
  103. package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
  104. package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
  105. package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
  106. package/dist/static/site/_includes/leed/form.hbs +266 -0
  107. package/dist/static/site/_includes/leed/head.hbs +67 -0
  108. package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
  109. package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
  110. package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
  111. package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
  112. package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
  113. package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
  114. package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
  115. package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
  116. package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
  117. package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
  118. package/dist/static/site/leed-feed-atom.hbs +42 -0
  119. package/dist/static/site/leed-feed-json.hbs +56 -0
  120. package/dist/static/site/leed-feed-rss.hbs +39 -0
  121. package/dist/static/site/leed-headers.hbs +38 -0
  122. package/dist/static/site/leed-redirects.hbs +10 -0
  123. package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
  124. package/dist/static/site/leed-sitemap.hbs +19 -0
  125. package/dist/static/site/leed-unsubscribe.hbs +44 -0
  126. package/dist/static/site/leed-unsubscribed.hbs +40 -0
  127. package/dist/static/tailwind/components/alerts.css +132 -0
  128. package/dist/static/tailwind/components/api-methods.css +39 -0
  129. package/dist/static/tailwind/components/breadcrumbs.css +31 -0
  130. package/dist/static/tailwind/components/code.css +88 -0
  131. package/dist/static/tailwind/components/collapsible.css +38 -0
  132. package/dist/static/tailwind/components/document-search.css +222 -0
  133. package/dist/static/tailwind/components/form.css +185 -0
  134. package/dist/static/tailwind/components/kbd.css +7 -0
  135. package/dist/static/tailwind/components/pagination.css +81 -0
  136. package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
  137. package/dist/static/tailwind/components/tab-groups.css +117 -0
  138. package/dist/static/tailwind/components/tables.css +0 -0
  139. package/dist/static/tailwind/components/toc.css +60 -0
  140. package/dist/static/tailwind/components.css +248 -0
  141. package/dist/static/tailwind/defaults.css +260 -0
  142. package/dist/static/tailwind/docs/api.css +701 -0
  143. package/dist/static/tailwind/docs/components/blockquote.css +5 -0
  144. package/dist/static/tailwind/docs/components/button.css +11 -0
  145. package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
  146. package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
  147. package/dist/static/tailwind/docs/components/navigation.css +279 -0
  148. package/dist/static/tailwind/docs/components/table.css +54 -0
  149. package/dist/static/tailwind/docs/components.css +6 -0
  150. package/dist/static/tailwind/docs/docs.css +427 -0
  151. package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
  152. package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
  153. package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
  154. package/dist/static/tailwind/docs/layouts.css +3 -0
  155. package/dist/static/tailwind/docs/themes/amber.css +113 -0
  156. package/dist/static/tailwind/docs/themes/blue.css +94 -0
  157. package/dist/static/tailwind/docs/themes/cyan.css +111 -0
  158. package/dist/static/tailwind/docs/themes/emerald.css +111 -0
  159. package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
  160. package/dist/static/tailwind/docs/themes/green.css +95 -0
  161. package/dist/static/tailwind/docs/themes/indigo.css +111 -0
  162. package/dist/static/tailwind/docs/themes/lime.css +111 -0
  163. package/dist/static/tailwind/docs/themes/orange.css +111 -0
  164. package/dist/static/tailwind/docs/themes/pink.css +111 -0
  165. package/dist/static/tailwind/docs/themes/purple.css +111 -0
  166. package/dist/static/tailwind/docs/themes/red.css +111 -0
  167. package/dist/static/tailwind/docs/themes/rose.css +111 -0
  168. package/dist/static/tailwind/docs/themes/sky.css +111 -0
  169. package/dist/static/tailwind/docs/themes/teal.css +111 -0
  170. package/dist/static/tailwind/docs/themes/violet.css +111 -0
  171. package/dist/static/tailwind/docs/themes/yellow.css +111 -0
  172. package/dist/static/tailwind/docs/themes.css +84 -0
  173. package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
  174. package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
  175. package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
  176. package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
  177. package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
  178. package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
  179. package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
  180. package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
  181. package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
  182. package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
  183. package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
  184. package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
  185. package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
  186. package/dist/static/tailwind/fonts/all.css +6 -0
  187. package/dist/static/tailwind/fonts/config.jsonc +31 -0
  188. package/dist/static/tailwind/fonts/geist-mono.css +142 -0
  189. package/dist/static/tailwind/fonts/geist.css +142 -0
  190. package/dist/static/tailwind/fonts/inter.css +215 -0
  191. package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
  192. package/dist/static/tailwind/fonts/noto-sans.css +134 -0
  193. package/dist/static/tailwind/fonts/open-sans.css +89 -0
  194. package/dist/static/tailwind/highlighters.css +14 -0
  195. package/dist/static/tailwind/hljs/README.md +53 -0
  196. package/dist/static/tailwind/hljs/a11y.css +96 -0
  197. package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
  198. package/dist/static/tailwind/hljs/atom-one.css +164 -0
  199. package/dist/static/tailwind/hljs/default.css +164 -0
  200. package/dist/static/tailwind/hljs/github.css +101 -0
  201. package/dist/static/tailwind/hljs/google-code.css +112 -0
  202. package/dist/static/tailwind/hljs/grayscale.css +115 -0
  203. package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
  204. package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
  205. package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
  206. package/dist/static/tailwind/hljs/papercolor.css +120 -0
  207. package/dist/static/tailwind/hljs/qt-creator.css +91 -0
  208. package/dist/static/tailwind/hljs/summerfruit.css +115 -0
  209. package/dist/static/tailwind/hljs/tomorrow.css +115 -0
  210. package/dist/static/tailwind/leed.css +170 -0
  211. package/dist/static/tailwind/site-tokens.reference.css +105 -0
  212. package/dist/static/tailwind/utils/icons.css +102 -0
  213. package/dist/static/tailwind/utils/utilities.css +105 -0
  214. package/dist/static/templates/pages/PAGE.hbs +27 -0
  215. package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
  216. package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
  217. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
  218. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
  219. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
  220. package/dist/static/templates/raw-content/.ci/build.sh +58 -0
  221. package/dist/static/templates/raw-content/README.md +179 -0
  222. package/dist/static/templates/raw-content/_gitignore +50 -0
  223. package/dist/static/templates/raw-content/src/404.hbs +21 -0
  224. package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
  225. package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
  226. package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
  227. package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
  228. package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
  229. package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
  230. package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
  231. package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
  232. package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
  233. package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
  234. package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
  235. package/dist/static/templates/raw-content/src/index.hbs +21 -0
  236. package/dist/static/templates/raw-content/src/robots.hbs +9 -0
  237. package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
  238. package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
  239. package/dist/static/templates/sandbox/_gitignore +50 -0
  240. package/dist/static/templates/sandbox/identity.json +1 -0
  241. package/dist/static/templates/sandbox/src/404.hbs +21 -0
  242. package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
  243. package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
  244. package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
  245. package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
  246. package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
  247. package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
  248. package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
  249. package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
  250. package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
  251. package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
  252. package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
  253. package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
  254. package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
  255. package/dist/static/templates/sandbox/src/index.hbs +20 -0
  256. package/dist/static/templates/sandbox/src/robots.hbs +9 -0
  257. package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
  258. package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
  259. package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
  260. package/dist/thrown-sukVkp_1.js +113 -0
  261. package/dist/typescript-D4T0V_Hn.js +23004 -0
  262. package/dist/yaml-9wR8GxF_.js +5236 -0
  263. package/package.json +157 -4
@@ -0,0 +1,1161 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2024-2025 Leed, Inc.
3
+ * SPDX-License-Identifier: LicenseRef-Leed-EULA
4
+ */
5
+ import { r as __exportAll } from "./chunk-1VJ9lAw7.js";
6
+ import { n as createLogger } from "./leedLogger-Zkada5aO.js";
7
+ import { t as EnvName } from "./env-B0bHiXJJ.js";
8
+ import { n as leedConfigDir, r as CliUserError, t as withFileLock } from "./leed.mjs";
9
+ import fs, { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { Option } from "commander";
12
+ import { z } from "zod";
13
+ import { homedir } from "node:os";
14
+ import slugify from "@sindresorhus/slugify";
15
+ import ky from "ky";
16
+ import { execFileSync } from "node:child_process";
17
+ //#region src/cli/output/jsonMode.ts
18
+ /**
19
+ * Resolution of the CLI's `--json` mode (#333 Phase 2).
20
+ *
21
+ * JSON mode is decided in exactly one place so every consumer agrees. It is on
22
+ * when either the global `--json` flag is present or `LEED_JSON` is set to a
23
+ * truthy value, and it implies non-interactive operation throughout: no color,
24
+ * no prompts, no self-update check.
25
+ */
26
+ /** The flag as registered on the root program. */
27
+ const JSON_OPTION_FLAGS = "--json";
28
+ /** Help text for the global flag; kept here so the wiring and the docs agree. */
29
+ const JSON_OPTION_DESCRIPTION = "Emit a single machine-readable JSON document on stdout instead of formatted text";
30
+ /** Environment opt-in, equivalent to passing the flag. */
31
+ const JSON_ENV_VAR = "LEED_JSON";
32
+ /** Values of `LEED_JSON` that turn JSON mode on. Anything else leaves it off. */
33
+ const TRUTHY = new Set([
34
+ "1",
35
+ "true",
36
+ "yes",
37
+ "on"
38
+ ]);
39
+ /**
40
+ * Whether the environment opts into JSON mode.
41
+ *
42
+ * The variable is an opt-in only: `LEED_JSON=0` does not force JSON mode OFF
43
+ * for a caller who passed `--json` explicitly, it simply fails to turn it on.
44
+ */
45
+ function jsonModeFromEnv(env = process.env) {
46
+ const raw = env[JSON_ENV_VAR];
47
+ return raw !== void 0 && TRUTHY.has(raw.trim().toLowerCase());
48
+ }
49
+ /**
50
+ * Whether this run is in JSON mode, for a helper that holds the parsed OPTIONS
51
+ * rather than the Commander command.
52
+ *
53
+ * The flag reaches a helper through `optsWithGlobals()` as `options.json`, and
54
+ * reading only that field is a real defect rather than a shortcut: it sees
55
+ * `--json` and is blind to `LEED_JSON`, so the documented environment opt-in —
56
+ * the one a harness uses when it cannot control argv — silently ran the
57
+ * interactive login flow inside the auth guard and skipped the failure envelope
58
+ * inside the build. Every such call site goes through here, so the two ways of
59
+ * asking for JSON mode can never diverge again.
60
+ */
61
+ function jsonModeFromOptions(options) {
62
+ if (options?.json === true) return true;
63
+ return jsonModeFromEnv();
64
+ }
65
+ /**
66
+ * Whether this run is in JSON mode.
67
+ *
68
+ * `command` is any command in the tree — the flag lives on the root program and
69
+ * reaches every descendant through `optsWithGlobals()`.
70
+ *
71
+ * **Defaults to `false` whenever the option is not registered**, and never
72
+ * throws. Test harnesses assemble commands under a throwaway parent that has no
73
+ * global options at all; a resolver that assumed the option existed would break
74
+ * every one of them.
75
+ */
76
+ function resolveJsonMode(command) {
77
+ if (jsonModeFromEnv()) return true;
78
+ if (!command) return false;
79
+ try {
80
+ return command.optsWithGlobals().json === true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ //#endregion
86
+ //#region src/cli/output/progress.ts
87
+ /**
88
+ * The diagnostic / progress door (#333 Phase 3).
89
+ *
90
+ * Command *results* leave through `emit()`. Everything else the CLI says —
91
+ * progress while a long step runs, an interactive prompt's narration, a
92
+ * lifecycle announcement, an update notice — is a diagnostic, and diagnostics
93
+ * go to stderr so that stdout carries the result and nothing else.
94
+ *
95
+ * Two rules make this the only sanctioned way to say something outside a
96
+ * result, and they are why `console.*` is banned across `src/cli/**` (see
97
+ * `noConsole.test.ts`):
98
+ *
99
+ * - **stderr, always.** Under `--json` stdout carries a single document; on a
100
+ * human terminal stderr is where progress has always belonged, and Phase 3
101
+ * normalizes stream assignment anyway. A diagnostic on stdout is a bug in
102
+ * both modes.
103
+ * - **`process.stderr.write`, never `console.*`.** A console method is one of
104
+ * the doors `redirectStdoutToStderr()` has to patch (see `./streams.ts`).
105
+ * Writing straight to stderr sidesteps that machinery entirely: the line
106
+ * cannot be re-routed onto the JSON document, and it cannot be reordered
107
+ * relative to the document by a patch that is installed or torn down
108
+ * mid-run.
109
+ *
110
+ * A trailing newline is added here so callers pass lines, not byte strings, and
111
+ * so nobody has to remember that `process.stderr.write` — unlike `console.log`
112
+ * — appends nothing.
113
+ */
114
+ /** Write one diagnostic or progress line to stderr. */
115
+ function progress(line) {
116
+ process.stderr.write(`${line}\n`);
117
+ }
118
+ /** Write several diagnostic or progress lines to stderr, in order. */
119
+ function progressLines(lines) {
120
+ for (const line of lines) progress(line);
121
+ }
122
+ /**
123
+ * Write a human-mode FAILURE render to stderr.
124
+ *
125
+ * The counterpart of `writeResult` in `./streams.ts`, and the other half of one
126
+ * rule: in human mode an `ok: true` render is the answer to the question the
127
+ * caller asked and goes to stdout; an `ok: false` render is a diagnostic and
128
+ * goes to stderr, exactly as the `console.error` calls it replaced did. That is
129
+ * what keeps `leed site list > types.txt` from capturing an error message as if
130
+ * it were a table, and what lets `2>/dev/null` silence a failure the way every
131
+ * other command-line tool does.
132
+ *
133
+ * Under `--json` this is never used: the single document is the contract, so
134
+ * `emit()` sends it to stdout through `writeResult` whatever `ok` says.
135
+ *
136
+ * It lives here rather than beside `writeResult` because it is a diagnostic
137
+ * door like the two above and shares nothing with the redirection machinery:
138
+ * `redirectStdoutToStderr()` only moves writes ONTO stderr, so there is nothing
139
+ * for this to bypass. Keeping it out of `./streams.ts` also keeps that module
140
+ * inside the size budget its no-relative-imports rule leaves it.
141
+ *
142
+ * Unlike `progress()` it appends no newline: it writes a render verbatim, and
143
+ * the renderer decides where its lines break.
144
+ */
145
+ function writeDiagnostic(text) {
146
+ process.stderr.write(text);
147
+ }
148
+ //#endregion
149
+ //#region src/cli/legacyAiCleanup.ts
150
+ /**
151
+ * One-time removal of what older CLIs wrote into a site for AI agents (#438).
152
+ *
153
+ * Older CLIs copied their Claude skills into `raw-content/.claude/skills`
154
+ * on every version change and recorded the directory names in
155
+ * `leed.config.json` under `aiConfiguration.claude.installedSkills`, alongside a
156
+ * `vscodeVersion` stamp for the VS Code files they also rewrote. The skills now
157
+ * ship in the Leed Claude plugin and the VS Code files are written only by
158
+ * `leed site vscode`, so the copies and both keys are dead weight — and the
159
+ * copies would shadow the plugin's newer skills.
160
+ *
161
+ * The names come from a file the user can edit, so each one is untrusted: only
162
+ * a plain directory name sitting directly under `.claude/skills` is ever
163
+ * removed, a symlink is unlinked rather than followed, and anything else is
164
+ * skipped. The rest of `.claude`, the `.code-workspace` and `.vscode/` are left
165
+ * alone.
166
+ */
167
+ const LEGACY_AI_CLEANUP_NOTICE = "Leed skills now come from the Leed Claude plugin; the CLI no longer installs them into .claude/skills.";
168
+ function recordedSkills(aiConfiguration) {
169
+ if (!aiConfiguration || typeof aiConfiguration !== "object") return [];
170
+ const claude = aiConfiguration.claude;
171
+ if (!claude || typeof claude !== "object") return [];
172
+ const installed = claude.installedSkills;
173
+ if (!Array.isArray(installed)) return [];
174
+ return installed.filter((name) => typeof name === "string");
175
+ }
176
+ function isPlainName(name) {
177
+ return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && !name.includes("\0") && !path.isAbsolute(name) && path.basename(name) === name;
178
+ }
179
+ function cleanupLegacyAiConfig(rawConfig, rawContentRepo) {
180
+ const changed = !!rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) && ("aiConfiguration" in rawConfig || "vscodeVersion" in rawConfig);
181
+ if (!changed) return {
182
+ config: rawConfig,
183
+ changed: false,
184
+ removed: [],
185
+ skipped: []
186
+ };
187
+ const { aiConfiguration, vscodeVersion: _vscodeVersion, ...config } = rawConfig;
188
+ const skillsDir = path.resolve(rawContentRepo, ".claude", "skills");
189
+ const removed = [];
190
+ const skipped = [];
191
+ for (const name of recordedSkills(aiConfiguration)) {
192
+ if (!isPlainName(name)) {
193
+ skipped.push({
194
+ name,
195
+ reason: "not a plain directory name"
196
+ });
197
+ continue;
198
+ }
199
+ const target = path.join(skillsDir, name);
200
+ if (path.dirname(target) !== skillsDir) {
201
+ skipped.push({
202
+ name,
203
+ reason: "outside .claude/skills"
204
+ });
205
+ continue;
206
+ }
207
+ try {
208
+ const stat = fs.lstatSync(target);
209
+ if (stat.isSymbolicLink()) {
210
+ fs.unlinkSync(target);
211
+ removed.push(name);
212
+ } else if (stat.isDirectory()) {
213
+ fs.rmSync(target, {
214
+ recursive: true,
215
+ force: true
216
+ });
217
+ removed.push(name);
218
+ } else skipped.push({
219
+ name,
220
+ reason: "not a directory"
221
+ });
222
+ } catch (error) {
223
+ const code = error.code;
224
+ if (code !== "ENOENT") skipped.push({
225
+ name,
226
+ reason: code ?? String(error)
227
+ });
228
+ }
229
+ }
230
+ return {
231
+ config,
232
+ changed,
233
+ removed,
234
+ skipped,
235
+ notice: LEGACY_AI_CLEANUP_NOTICE
236
+ };
237
+ }
238
+ //#endregion
239
+ //#region src/cli/rawContentSlot.ts
240
+ /**
241
+ * The one resolution of the `(environment, companyId)` slot the raw-content
242
+ * credential is filed under (#413).
243
+ *
244
+ * `./rawContentCredential.ts` owns the store; this owns the KEY. Everything
245
+ * that files the token and everything that reads it back resolves that key
246
+ * here, and nowhere else, because the two halves disagreeing is the only way
247
+ * this feature fails: the writer files under one slot, the reader looks in
248
+ * another, and the single symptom is git reporting that it could not read a
249
+ * username on a checkout that was working a moment earlier.
250
+ *
251
+ * **That is not hypothetical — it is the defect this module exists to end.** It
252
+ * was fixed three times over #413: once in the writer, once in the reader, and
253
+ * once more because those two fixes disagreed with each other. A shared
254
+ * resolver is the only shape in which they cannot.
255
+ *
256
+ * The three sources, in precedence order:
257
+ *
258
+ * 1. **The site's `sites.json` entry** — what the command was configured with. First,
259
+ * because `../cli/params.ts`'s `siteCompanyId()` is config-first for the
260
+ * company half, and a key assembled from two different precedences is a
261
+ * third slot neither half ever writes.
262
+ * 2. **`identity.json`** inside the raw-content checkout — the only one of the
263
+ * three a direct-mode checkout has, and the only one present when git runs
264
+ * `leed __credential` from the user's own shell, where `ENV_NAME` is not
265
+ * exported. `libs/data-types/src/automation/gitlabProvisioner.ts` commits
266
+ * it with `{ companyId, environment }`, so every clone carries both.
267
+ * 3. **`ENV_NAME`**, else **no slot** (#462). There used to be a `production`
268
+ * fallback here; a slot guessed for an environment nothing named is one no
269
+ * reader would look in, so the resolver answers null and each caller says
270
+ * what that means for it.
271
+ */
272
+ /** The file a raw-content checkout identifies itself with. */
273
+ const IDENTITY_FILE_NAME = "identity.json";
274
+ /** `value` when it is a non-empty string. */
275
+ function nonEmpty(value) {
276
+ return typeof value === "string" && value !== "" ? value : void 0;
277
+ }
278
+ /**
279
+ * A raw-content checkout's `identity.json`, or null when it has none.
280
+ *
281
+ * Read with `JSON.parse` and never allowed to throw: `leed __credential` runs
282
+ * inside a git process and may not log, prompt or fail, and a malformed
283
+ * identity file has to read as "this checkout names nothing" rather than as an
284
+ * error. Both halves read it through here so neither can start honoring a
285
+ * field the other ignores.
286
+ */
287
+ function readSiteIdentity(directory) {
288
+ if (!directory) return null;
289
+ try {
290
+ const parsed = JSON.parse(readFileSync(path.join(directory, IDENTITY_FILE_NAME), "utf-8"));
291
+ if (typeof parsed !== "object" || parsed === null) return null;
292
+ const identity = parsed;
293
+ return {
294
+ companyId: nonEmpty(identity.companyId),
295
+ environment: nonEmpty(identity.environment)
296
+ };
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+ /** The absolute path of `directory`'s identity file. */
302
+ function siteIdentityPath(directory) {
303
+ return path.join(directory, IDENTITY_FILE_NAME);
304
+ }
305
+ function rawContentSlot(sources) {
306
+ const named = nonEmpty(sources.configEnvironment) ?? sources.identity?.environment;
307
+ const environment = EnvName.safeParse(named).data ?? EnvName.safeParse(process.env.ENV_NAME).data;
308
+ if (!environment) return null;
309
+ return {
310
+ environment,
311
+ companyId: nonEmpty(sources.configCompanyId) ?? sources.identity?.companyId
312
+ };
313
+ }
314
+ //#endregion
315
+ //#region src/cli/shared/checkoutRoot.ts
316
+ /**
317
+ * The root of the site checkout `start` is in, or null when there is none (#459): `start` itself when it holds
318
+ * `identity.json`; else its `raw-content/` when that does — the company folder a checkout has always been set up in,
319
+ * which commands were run from before the registry; else the NEAREST ancestor holding `identity.json`.
320
+ *
321
+ * Every site repo and every sandbox carries that file at its root, so it is what identifies a checkout wherever the
322
+ * user put the folder. The sandbox guard (`../sandbox/placeholders.ts`) and the site registry (`../sites/`) both find
323
+ * the checkout through here, so a real checkout nested under a sandbox (or the reverse) is judged by the checkout the
324
+ * command is actually in, the same way by both. The root is returned as its physical path (see `canonicalPath`).
325
+ */
326
+ function findCheckoutRoot(start = process.cwd()) {
327
+ const from = path.resolve(start);
328
+ if (existsSync(siteIdentityPath(from))) return canonicalPath(from);
329
+ const below = path.join(from, "raw-content");
330
+ if (existsSync(siteIdentityPath(below))) return canonicalPath(below);
331
+ let current = path.dirname(from);
332
+ for (;;) {
333
+ if (existsSync(siteIdentityPath(current))) return canonicalPath(current);
334
+ const parent = path.dirname(current);
335
+ if (parent === current) return null;
336
+ current = parent;
337
+ }
338
+ }
339
+ /**
340
+ * `p` as one spelling of the folder: absolute, with every symlink resolved, so a path recorded through a symlink (on
341
+ * macOS, `/tmp` is one) and the physical working directory compare equal. For a path that does not exist yet, such
342
+ * as a checkout about to be cloned or one since moved, the nearest existing ancestor is resolved and the rest kept.
343
+ */
344
+ function canonicalPath(p) {
345
+ const absolute = path.resolve(p);
346
+ try {
347
+ return realpathSync(absolute);
348
+ } catch {
349
+ const parent = path.dirname(absolute);
350
+ return parent === absolute ? absolute : path.join(canonicalPath(parent), path.basename(absolute));
351
+ }
352
+ }
353
+ //#endregion
354
+ //#region src/cli/shared/ciMode.ts
355
+ /**
356
+ * Whether the CLI runs as a CI build (#459): the `--ci` flag, or `CI=true` in the environment.
357
+ *
358
+ * The one test for it in site resolution, the site registry and the upload, so they cannot disagree about whether
359
+ * a run is CI — and CI never reads or writes `sites.json`. Pass the parsed `--ci` option where the command has one.
360
+ */
361
+ function isCI(ciFlag) {
362
+ return !!ciFlag || process.env.CI === "true";
363
+ }
364
+ //#endregion
365
+ //#region src/cli/shared/privateJson.ts
366
+ /**
367
+ * Write a per-user JSON store the way `../credentials.ts` writes `auth.json` (#459): a 600 file inside a 700
368
+ * directory, written to a temp file and renamed into place, so a reader never sees half a file. A write that fails
369
+ * takes its temp file with it.
370
+ */
371
+ function writePrivateJson(file, value) {
372
+ const dir = path.dirname(file);
373
+ mkdirSync(dir, {
374
+ recursive: true,
375
+ mode: 448
376
+ });
377
+ chmodSync(dir, 448);
378
+ const tmpPath = `${file}.tmp-${process.pid}`;
379
+ try {
380
+ writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
381
+ renameSync(tmpPath, file);
382
+ } catch (e) {
383
+ rmSync(tmpPath, { force: true });
384
+ throw e;
385
+ }
386
+ chmodSync(file, 384);
387
+ }
388
+ //#endregion
389
+ //#region src/cli/sites/sitesStore.ts
390
+ /**
391
+ * The per-user site registry, `$XDG_CONFIG_HOME/leed/sites.json` (default `~/.config/leed/sites.json`) (#459).
392
+ *
393
+ * One entry per site the user has set up: a real site is keyed by `(companyId, environment)`; a sandbox — every one of
394
+ * which names the same placeholder company — is keyed by its checkout path and named by its folder. Nothing about a
395
+ * site lives outside its repo any more: a checkout is identified by its own `identity.json`, and `paths` is only a
396
+ * cache of where its clones were last seen, refreshed by every command run inside one.
397
+ *
398
+ * Stored the way `../credentials.ts` stores `auth.json`: a 600 file inside a 700 directory, written to a temp file and
399
+ * renamed into place, and changed only under a cross-process lock (`../shared/fileLock.ts`) that re-reads the file
400
+ * after taking it, so two CLI processes writing at once never lose each other's entry or leave a torn file.
401
+ *
402
+ * Readers never write and never throw: a missing file is an empty registry, and an entry this build cannot read is
403
+ * skipped (and kept on disk). Only a file that is not JSON at all reads as empty; the next write sets it aside as
404
+ * `sites.json.corrupt-<ms>` rather than overwriting it.
405
+ */
406
+ const log$2 = createLogger("site:sites");
407
+ const SiteEntry = z.object({
408
+ companyId: z.string().min(1).describe("CMS company identifier (the sandbox placeholder for a sandbox)"),
409
+ environment: EnvName.describe("The CMS environment this site targets"),
410
+ domain: z.string().default("").describe("The company's public site domain; empty for a sandbox"),
411
+ companyName: z.string().optional().describe("The site's title, for naming it in commands and output"),
412
+ paths: z.array(z.string()).default([]).describe("Absolute paths of the site's raw-content checkouts, most recently used first"),
413
+ remote: z.string().optional().describe("The checkout's git origin URL, without credentials"),
414
+ userId: z.string().optional().describe("The Leed user who set the site up"),
415
+ initialized: z.boolean().default(false).describe("Whether `leed site init` has finished for this site"),
416
+ huskyVersion: z.string().default("").describe("CLI version whose git hooks were last installed in the checkout"),
417
+ sandbox: z.literal(true).optional().describe("Set on a sandbox site, which has no CMS company behind it"),
418
+ name: z.string().optional().describe("A sandbox's name: its folder name")
419
+ });
420
+ const SitesFile = z.object({
421
+ version: z.literal(1),
422
+ sites: z.array(SiteEntry).default([]),
423
+ migratedFrom: z.array(z.string()).default([]).describe("leed.config.json files already imported; they are never read again")
424
+ });
425
+ function getSitesPath() {
426
+ return path.join(leedConfigDir(), "sites.json");
427
+ }
428
+ function emptySites() {
429
+ return {
430
+ version: 1,
431
+ sites: [],
432
+ migratedFrom: []
433
+ };
434
+ }
435
+ function readOutcome() {
436
+ const sitesPath = getSitesPath();
437
+ const empty = {
438
+ file: emptySites(),
439
+ unreadable: [],
440
+ corrupt: false,
441
+ readError: null,
442
+ newer: null
443
+ };
444
+ if (!existsSync(sitesPath)) return empty;
445
+ let text;
446
+ try {
447
+ text = readFileSync(sitesPath, "utf-8");
448
+ } catch (e) {
449
+ log$2.debug(`Could not read ${sitesPath}: ${e.message}`);
450
+ return {
451
+ ...empty,
452
+ readError: e.message
453
+ };
454
+ }
455
+ let raw;
456
+ try {
457
+ raw = JSON.parse(text);
458
+ } catch (e) {
459
+ log$2.debug(`Ignoring ${sitesPath}, which is not valid JSON: ${e.message}`);
460
+ return {
461
+ ...empty,
462
+ corrupt: true
463
+ };
464
+ }
465
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
466
+ log$2.debug(`Ignoring ${sitesPath}: it is not a JSON object`);
467
+ return {
468
+ ...empty,
469
+ corrupt: true
470
+ };
471
+ }
472
+ const { version, sites, migratedFrom } = raw;
473
+ const newer = typeof version === "number" && version > 1 ? version : null;
474
+ const outcome = {
475
+ ...empty,
476
+ newer
477
+ };
478
+ for (const candidate of Array.isArray(sites) ? sites : []) {
479
+ const parsed = SiteEntry.safeParse(candidate);
480
+ if (parsed.success) outcome.file.sites.push(parsed.data);
481
+ else {
482
+ log$2.debug(`Skipping an unreadable entry in ${sitesPath}: ${parsed.error.message}`);
483
+ outcome.unreadable.push(candidate);
484
+ }
485
+ }
486
+ if (Array.isArray(migratedFrom)) outcome.file.migratedFrom = migratedFrom.filter((file) => typeof file === "string");
487
+ return outcome;
488
+ }
489
+ /** The registry as it is on disk. Synchronous, read-only, and never throws. */
490
+ function readSites() {
491
+ return readOutcome().file;
492
+ }
493
+ /**
494
+ * Change the registry: take the lock, re-read the file, let `mutator` change it in place, and write it back when it
495
+ * changed. The only writer, so every change is a read-modify-write under the lock. Entries this build cannot read are
496
+ * written back untouched; a file a newer CLI wrote, or one that cannot be read at all, is refused rather than
497
+ * rewritten.
498
+ */
499
+ async function updateSites(mutator) {
500
+ return withFileLock(`${getSitesPath()}.lock`, async () => {
501
+ const { file, unreadable, corrupt, readError, newer } = readOutcome();
502
+ if (readError !== null) throw new CliUserError(`${getSitesPath()} could not be read (${readError}), so it is left unchanged.`, "Check the file's permissions: it should belong to you, with mode 600", "validation_failed");
503
+ if (newer !== null) throw new CliUserError(`${getSitesPath()} was written by a newer Leed CLI (format ${newer}; this one understands 1), so it is left unchanged.`, "Update the CLI with `leed self-update`", "validation_failed");
504
+ const before = JSON.stringify(file);
505
+ const result = mutator(file);
506
+ if (corrupt) {
507
+ const aside = `${getSitesPath()}.corrupt-${Date.now()}`;
508
+ renameSync(getSitesPath(), aside);
509
+ log$2.warn(`${getSitesPath()} was not valid JSON and has been moved to ${aside}; starting a new one.`);
510
+ }
511
+ if (corrupt || JSON.stringify(file) !== before) {
512
+ const parsed = SitesFile.parse(file);
513
+ writePrivateJson(getSitesPath(), {
514
+ ...parsed,
515
+ sites: [...parsed.sites, ...unreadable]
516
+ });
517
+ }
518
+ return result;
519
+ });
520
+ }
521
+ /** Whether two entries are the same site: the same company and environment, or for a sandbox a shared checkout. */
522
+ function sameSite(a, b) {
523
+ if (a.sandbox || b.sandbox) {
524
+ const theirs = new Set(b.paths.map(canonicalPath));
525
+ return !!a.sandbox && !!b.sandbox && a.paths.some((p) => theirs.has(canonicalPath(p)));
526
+ }
527
+ return a.companyId === b.companyId && a.environment === b.environment;
528
+ }
529
+ /**
530
+ * Merge `input` into the entry for the same site, or add it. Fields `input` names replace the entry's; its paths go
531
+ * to the front of the entry's, without duplicates. Paths are recorded as physical paths (see `canonicalPath`), so a
532
+ * checkout reached through a symlink is one checkout, not two.
533
+ */
534
+ function mergeSite(file, input) {
535
+ const parsed = SiteEntry.parse(input);
536
+ const incoming = {
537
+ ...parsed,
538
+ paths: [...new Set(parsed.paths.map(canonicalPath))]
539
+ };
540
+ const existing = file.sites.find((entry) => sameSite(entry, incoming));
541
+ if (!existing) {
542
+ file.sites.push(incoming);
543
+ return incoming;
544
+ }
545
+ const named = Object.keys(input).filter((key) => input[key] !== void 0);
546
+ const defined = Object.fromEntries(named.map((key) => [key, incoming[key]]));
547
+ const paths = [...new Set([...incoming.paths, ...existing.paths.map(canonicalPath)])];
548
+ Object.assign(existing, defined, { paths });
549
+ return existing;
550
+ }
551
+ /** Record `input` in the registry, merging it into the entry for the same site. */
552
+ function upsertSite(input) {
553
+ return updateSites((file) => mergeSite(file, input));
554
+ }
555
+ //#endregion
556
+ //#region src/cli/sites/legacyConfigImport.ts
557
+ /**
558
+ * The one-time import of `leed.config.json` files into `sites.json` (#459).
559
+ *
560
+ * CLIs before the registry kept one `leed.config.json` per site, in the folder above the checkout
561
+ * (`<dir>/<customer>/leed.config.json` beside `<dir>/<customer>/raw-content`), and the installer left an
562
+ * uninitialized one in the project directory it created (`~/leed` by default). Each file the CLI meets — in the
563
+ * working directory, any folder above it, `~/leed` or a folder directly inside `~/leed` — is imported once: its site
564
+ * is merged into the registry and its path is recorded in `migratedFrom`, so it is never read again. The file itself
565
+ * is left in place.
566
+ *
567
+ * The keys older CLIs wrote for their AI files (`aiConfiguration`, `vscodeVersion`) are not carried over; the skills
568
+ * they recorded are removed from the checkout here, once, by `../legacyAiCleanup.ts`.
569
+ *
570
+ * The `~/leed` scan uses the real home directory (the `home` parameter exists for tests). Never run in CI, where the
571
+ * CLI uses `identity.json` and the environment alone.
572
+ */
573
+ const log$1 = createLogger("site:sites");
574
+ const LEGACY_CONFIG_FILE_NAME = "leed.config.json";
575
+ /** Every `leed.config.json` the CLI looks for: the working directory and its ancestors, then `~/leed` and its children. */
576
+ function legacyConfigCandidates(cwd, home = homedir()) {
577
+ const found = [];
578
+ const add = (file) => {
579
+ if (!found.includes(file) && existsSync(file)) found.push(file);
580
+ };
581
+ let current = path.resolve(cwd);
582
+ for (;;) {
583
+ add(path.join(current, LEGACY_CONFIG_FILE_NAME));
584
+ const parent = path.dirname(current);
585
+ if (parent === current) break;
586
+ current = parent;
587
+ }
588
+ const installerDir = path.join(home, "leed");
589
+ add(path.join(installerDir, LEGACY_CONFIG_FILE_NAME));
590
+ try {
591
+ for (const child of readdirSync(installerDir, { withFileTypes: true })) if (child.isDirectory()) add(path.join(installerDir, child.name, LEGACY_CONFIG_FILE_NAME));
592
+ } catch {}
593
+ return found;
594
+ }
595
+ /** The checkout a legacy file describes: the first candidate location holding `identity.json`. */
596
+ function legacyCheckout(file, raw) {
597
+ const dir = path.dirname(file);
598
+ const customer = typeof raw.domain === "string" && raw.domain ? slugify(raw.domain) : null;
599
+ const location = typeof raw.location === "string" && raw.location ? raw.location : null;
600
+ return [
601
+ path.join(dir, "raw-content"),
602
+ ...customer ? [path.join(dir, customer, "raw-content")] : [],
603
+ ...customer && location ? [path.join(location, customer, "raw-content")] : []
604
+ ].find((candidate) => existsSync(siteIdentityPath(candidate))) ?? null;
605
+ }
606
+ const text = (value) => typeof value === "string" && value ? value : void 0;
607
+ /** The registry entry a parsed legacy file maps to, or null when it names no company to key it by. */
608
+ function legacyEntry(raw, checkout) {
609
+ const identity = readSiteIdentity(checkout ?? void 0);
610
+ const companyId = text(raw.companyId) ?? identity?.companyId;
611
+ const environment = EnvName.safeParse(raw.environment ?? identity?.environment ?? "production");
612
+ if (!companyId || !environment.success) return null;
613
+ return {
614
+ companyId,
615
+ environment: environment.data,
616
+ domain: text(raw.domain) ?? "",
617
+ paths: checkout ? [checkout] : [],
618
+ ...text(raw.userId) ? { userId: text(raw.userId) } : {},
619
+ ...typeof raw.initialized === "boolean" ? { initialized: raw.initialized } : {},
620
+ ...text(raw.huskyVersion) ? { huskyVersion: text(raw.huskyVersion) } : {}
621
+ };
622
+ }
623
+ function readRaw(file) {
624
+ try {
625
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
626
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
627
+ } catch {
628
+ return null;
629
+ }
630
+ }
631
+ /**
632
+ * Add an imported entry, or fill in the entry already registered for the same site. What the registry already holds
633
+ * wins — an installer's leftover uninitialized file must not undo a finished `leed site init` — and the file's
634
+ * checkout joins the entry's paths at the end.
635
+ */
636
+ function fileLegacyEntry(sites, input) {
637
+ const incoming = SiteEntry.parse(input);
638
+ const existing = sites.sites.find((entry) => sameSite(entry, incoming));
639
+ if (!existing) {
640
+ mergeSite(sites, input);
641
+ return;
642
+ }
643
+ const known = new Set(existing.paths.map(canonicalPath));
644
+ existing.paths.push(...incoming.paths.map(canonicalPath).filter((p) => !known.has(p)));
645
+ existing.userId ??= incoming.userId;
646
+ existing.domain ||= incoming.domain;
647
+ existing.huskyVersion ||= incoming.huskyVersion;
648
+ existing.initialized ||= incoming.initialized;
649
+ }
650
+ /** The one-time notice for an imported file. */
651
+ function legacyImportNotice(file) {
652
+ return `Imported ${file} into ${getSitesPath()}; ${LEGACY_CONFIG_FILE_NAME} is no longer read and can be deleted.`;
653
+ }
654
+ /**
655
+ * File each of `pending` into `sites`, recording it in `migratedFrom`, and return the files that named a site. With
656
+ * `cleanup`, the legacy skills each file recorded are removed from its checkout as well.
657
+ */
658
+ function fileLegacyConfigs(sites, pending, cleanup) {
659
+ const imported = [];
660
+ for (const file of pending) {
661
+ if (sites.migratedFrom.includes(file)) continue;
662
+ sites.migratedFrom.push(file);
663
+ const raw = readRaw(file);
664
+ if (!raw) {
665
+ log$1.debug(`Skipping unreadable ${file}`);
666
+ continue;
667
+ }
668
+ const checkout = legacyCheckout(file, raw);
669
+ const entry = legacyEntry(raw, checkout);
670
+ if (!entry) {
671
+ log$1.debug(`Skipping ${file}: it names no company`);
672
+ continue;
673
+ }
674
+ if (checkout && cleanup) {
675
+ const legacy = cleanupLegacyAiConfig(raw, checkout);
676
+ for (const { name, reason } of legacy.skipped) log$1.debug(`Left legacy skill entry ${name}: ${reason}`);
677
+ if (legacy.notice) log$1.info(legacy.notice);
678
+ }
679
+ fileLegacyEntry(sites, entry);
680
+ imported.push(file);
681
+ }
682
+ return imported;
683
+ }
684
+ /** The legacy files near `cwd` not yet imported. */
685
+ function pendingLegacyConfigs(known, cwd, home) {
686
+ const migrated = new Set(known.migratedFrom);
687
+ return legacyConfigCandidates(cwd, home).filter((file) => !migrated.has(file));
688
+ }
689
+ /**
690
+ * Import every legacy file not yet imported, and return the files imported. Cheap when there is nothing to do: one
691
+ * read of the registry and a few `existsSync`, with the lock taken only when an unimported file exists.
692
+ */
693
+ async function importLegacyConfigs(cwd = process.cwd(), home = homedir()) {
694
+ const pending = pendingLegacyConfigs(readSites(), cwd, home);
695
+ if (pending.length === 0) return [];
696
+ return updateSites((sites) => fileLegacyConfigs(sites, pending, true));
697
+ }
698
+ /**
699
+ * The registry as the next import would leave it, for the commands that must not write (`leed site list`,
700
+ * `leed auth status`, a `--dry-run`): the legacy files nearby are merged in memory, and nothing is written, imported
701
+ * or cleaned up. Never throws; in CI it is the registry alone.
702
+ */
703
+ function sitesWithLegacy(cwd = process.cwd(), home = homedir()) {
704
+ const sites = readSites();
705
+ if (isCI()) return sites;
706
+ try {
707
+ fileLegacyConfigs(sites, pendingLegacyConfigs(sites, cwd, home), false);
708
+ } catch (e) {
709
+ log$1.debug(`Legacy configs not read: ${e.message}`);
710
+ }
711
+ return sites;
712
+ }
713
+ /**
714
+ * Import what is nearby and print the one-time notice for each file imported, unless `quiet` (`--json`). Never in CI,
715
+ * and never fatal: a registry that cannot be written leaves the files to be imported on a later run.
716
+ */
717
+ async function importAndAnnounce(quiet, cwd = process.cwd()) {
718
+ if (isCI()) return;
719
+ try {
720
+ const imported = await importLegacyConfigs(cwd);
721
+ if (!quiet) for (const file of imported) progress(legacyImportNotice(file));
722
+ } catch (e) {
723
+ log$1.debug(`Legacy config import skipped: ${e.message}`);
724
+ }
725
+ }
726
+ //#endregion
727
+ //#region src/cli/sandbox/placeholders.ts
728
+ /**
729
+ * The sandbox site and its placeholder ids (#457).
730
+ *
731
+ * A sandbox is a site checkout with no CMS behind it: `leed site init --sandbox` writes it from the fixture set in
732
+ * `static/templates/sandbox/`, and it builds and previews offline. Its `identity.json` names a company that cannot
733
+ * exist — real company ids are six lowercase letters or digits, and this one has hyphens — so the file doubles as the
734
+ * marker every command checks before it does any auth.
735
+ *
736
+ * Every record id a sandbox holds is a readable placeholder, `ph-<kind>-<n>`. The sandbox move
737
+ * (`/leed:site-sandbox-move`) creates each record in the CMS and swaps each placeholder for the real id, so the
738
+ * format has to make the ids trivial to find and impossible to confuse with a real one. The plugin's move scripts
739
+ * carry their own copy of the kind list and the token pattern (`skills/site-sandbox-move/scripts/lib`); the plugin's
740
+ * tests read this file and fail when the two disagree.
741
+ */
742
+ /** The `companyId` of every sandbox's `identity.json`. Not a real company id, by construction. */
743
+ const SANDBOX_COMPANY_ID = "non-cms-company";
744
+ const KIND_ALTERNATION = [
745
+ "pt",
746
+ "page",
747
+ "label",
748
+ "author",
749
+ "menu",
750
+ "menuitem",
751
+ "form",
752
+ "asset",
753
+ "cta",
754
+ "index",
755
+ "autolink"
756
+ ].join("|");
757
+ new RegExp(`^ph-(${KIND_ALTERNATION})-[1-9]\\d*$`);
758
+ new RegExp(`(?<![A-Za-z0-9_])ph-(${KIND_ALTERNATION})-([1-9]\\d*)(?![A-Za-z0-9_])`, "g");
759
+ /** Whether an `identity.json` names the sandbox company. */
760
+ function isSandboxIdentity(identity) {
761
+ return identity?.companyId === SANDBOX_COMPANY_ID;
762
+ }
763
+ /**
764
+ * The root of the sandbox `start` is inside, or null.
765
+ *
766
+ * Decides on the NEAREST `identity.json` alone (`findCheckoutRoot`), so a real checkout nested under a sandbox (or the
767
+ * reverse) is judged by the checkout the command is actually in.
768
+ */
769
+ function findSandboxRoot(start = process.cwd()) {
770
+ const root = findCheckoutRoot(start);
771
+ return root && isSandboxIdentity(readSiteIdentity(root)) ? root : null;
772
+ }
773
+ /** Where the shipped sandbox fixture set lives under the CLI's app root (`dist/` once bundled). */
774
+ function sandboxTemplateDir(appRoot) {
775
+ return path.join(appRoot, "static", "templates", "sandbox");
776
+ }
777
+ //#endregion
778
+ //#region src/cli/sites/siteLookup.ts
779
+ /**
780
+ * Finding a site in the registry (#459): the site a checkout belongs to, and the site a `--site` reference names.
781
+ *
782
+ * Pure over a `SitesFile` already read, so the sync callers (option defaults, the git credential helper) and the
783
+ * async ones resolve a site the same way.
784
+ */
785
+ /**
786
+ * The entry for the checkout rooted at `root`, whose `identity.json` says `identity`.
787
+ *
788
+ * An entry that lists `root` among its paths wins, provided it agrees with the checkout's own `identity.json` (see
789
+ * `agreesWithIdentity`) and, when `env` is given, is for that environment: paths are a cache, and a folder reused for
790
+ * another site must not resolve to the old one. Paths compare as physical paths, so one recorded through a symlink
791
+ * still matches. Otherwise, for a real site, the initialized entry for the identity's company in the identity's
792
+ * environment (or `env`, when the identity names none); with no environment to go on, the company's only initialized
793
+ * entry. An entry still waiting for `leed site init` (an installer's leftover, say) claims no checkout it was not
794
+ * recorded at, so a clone of that company made by hand stays in direct mode. A sandbox is matched by path alone, since
795
+ * every sandbox names the same placeholder company.
796
+ */
797
+ function findSiteForCheckout(file, root, identity, env) {
798
+ const resolved = canonicalPath(root);
799
+ const byPath = file.sites.filter((entry) => entry.paths.some((p) => canonicalPath(p) === resolved) && agreesWithIdentity(entry, identity));
800
+ const pathMatch = env ? byPath.find((entry) => entry.environment === env) : byPath[0];
801
+ if (pathMatch) return pathMatch;
802
+ if (!identity?.companyId || isSandboxIdentity(identity)) return null;
803
+ const company = file.sites.filter((entry) => !entry.sandbox && entry.initialized && entry.companyId === identity.companyId);
804
+ const wanted = identity.environment ?? env;
805
+ if (wanted) return company.find((entry) => entry.environment === wanted) ?? null;
806
+ return company.length === 1 ? company[0] : null;
807
+ }
808
+ /**
809
+ * Whether `entry` can be the site of a checkout whose `identity.json` says `identity`: a sandbox entry for a sandbox,
810
+ * and for a real site the same company, in the same environment when the identity names one. A checkout whose
811
+ * identity cannot be read, or names no company, agrees with any entry, as it did before the identity was checked.
812
+ */
813
+ function agreesWithIdentity(entry, identity) {
814
+ if (!identity?.companyId) return true;
815
+ if (isSandboxIdentity(identity)) return !!entry.sandbox;
816
+ if (entry.sandbox || entry.companyId !== identity.companyId) return false;
817
+ return !identity.environment || entry.environment === identity.environment;
818
+ }
819
+ /** Whether `entry` answers to `ref`: its companyId, its domain or its name, compared without case. */
820
+ function answersTo(entry, ref) {
821
+ const wanted = ref.trim().toLowerCase();
822
+ return [
823
+ entry.companyId,
824
+ entry.domain,
825
+ entry.companyName,
826
+ entry.name
827
+ ].some((name) => !!name && name.toLowerCase() === wanted);
828
+ }
829
+ /**
830
+ * The entry `ref` names — a companyId, a domain, a site's name or a sandbox's folder name — narrowed to `env` when
831
+ * one is given. Several matches (one company in two environments, say, with no `env`) are ambiguous, not a guess.
832
+ */
833
+ function resolveSiteRef(file, ref, env) {
834
+ const named = file.sites.filter((entry) => answersTo(entry, ref));
835
+ const matches = env ? named.filter((entry) => entry.environment === env) : named;
836
+ if (matches.length === 1) return {
837
+ ok: true,
838
+ entry: matches[0]
839
+ };
840
+ if (matches.length === 0) return {
841
+ ok: false,
842
+ code: "not_found",
843
+ candidates: file.sites
844
+ };
845
+ return {
846
+ ok: false,
847
+ code: "site_ambiguous",
848
+ candidates: matches
849
+ };
850
+ }
851
+ /**
852
+ * The first of the entry's paths that still holds this site's checkout — an `identity.json` that agrees with the
853
+ * entry (see `agreesWithIdentity`) — or null when none does. A folder since reused for another site is not this
854
+ * site's checkout, however much it still exists.
855
+ */
856
+ function livePath(entry) {
857
+ return entry.paths.find((p) => existsSync(siteIdentityPath(p)) && agreesWithIdentity(entry, readSiteIdentity(p))) ?? null;
858
+ }
859
+ /** How a site is named in output: its name, else its domain, else its companyId. */
860
+ function siteLabel(entry) {
861
+ return entry.companyName || entry.name || entry.domain || entry.companyId;
862
+ }
863
+ /** One line per site, for the messages that list the registered sites. */
864
+ function describeSites(entries) {
865
+ return entries.map((entry) => {
866
+ const where = livePath(entry) ?? entry.paths[0] ?? "no checkout";
867
+ const kind = entry.sandbox ? "sandbox" : entry.environment;
868
+ return ` ${siteLabel(entry)} (${kind}, ${entry.companyId}) ${where}`;
869
+ }).join("\n");
870
+ }
871
+ //#endregion
872
+ //#region src/cli/sites/siteConfig.ts
873
+ /**
874
+ * The site the CLI is working on, as every command sees it (#459).
875
+ *
876
+ * The checkout is the nearest folder holding `identity.json`; the site is that checkout's entry in `sites.json`.
877
+ * `currentSite` is synchronous and read-only, for the callers that resolve a site at module load (option defaults)
878
+ * or may not write at all (the git credential helper). `refreshSite` is the one writer: run by a command inside a
879
+ * checkout, it moves the checkout to the front of the entry's paths and refreshes its remote and name, so a moved
880
+ * or renamed folder fixes itself on the next command.
881
+ */
882
+ const log = createLogger("site:sites");
883
+ /**
884
+ * The checkout the working directory is in, and its entry in `sites` (the registry on disk unless given). Null
885
+ * outside any checkout.
886
+ */
887
+ function currentSite(cwd = process.cwd(), env, sites = readSites()) {
888
+ const root = findCheckoutRoot(cwd);
889
+ if (!root) return null;
890
+ const identity = readSiteIdentity(root);
891
+ const entry = findSiteForCheckout(sites, root, identity, env);
892
+ return {
893
+ root,
894
+ identity,
895
+ entry,
896
+ sandbox: isSandboxIdentity(identity) || !!entry?.sandbox
897
+ };
898
+ }
899
+ /**
900
+ * The company, environment and domain of the checkout the working directory is in: the registry entry's, else the
901
+ * checkout's own `identity.json` (which names no domain). Null outside a checkout and in a sandbox, which has no
902
+ * company to name. Never throws and never writes.
903
+ */
904
+ function siteFacts(cwd = process.cwd()) {
905
+ try {
906
+ const site = currentSite(cwd);
907
+ if (!site || site.sandbox) return null;
908
+ const companyId = site.entry?.companyId ?? site.identity?.companyId;
909
+ const environment = site.entry?.environment ?? EnvName.safeParse(site.identity?.environment).data;
910
+ const domain = site.entry?.domain || void 0;
911
+ return {
912
+ ...companyId ? { companyId } : {},
913
+ ...environment ? { environment } : {},
914
+ ...domain ? { domain } : {}
915
+ };
916
+ } catch (e) {
917
+ log.debug(`No site facts: ${e.message}`);
918
+ return null;
919
+ }
920
+ }
921
+ /** The checkout's origin URL without any credential in it, or undefined when git cannot say. */
922
+ function originUrl(root) {
923
+ try {
924
+ const url = execFileSync("git", [
925
+ "-C",
926
+ root,
927
+ "remote",
928
+ "get-url",
929
+ "origin"
930
+ ], {
931
+ encoding: "utf-8",
932
+ stdio: [
933
+ "ignore",
934
+ "pipe",
935
+ "ignore"
936
+ ],
937
+ timeout: 5e3
938
+ }).trim();
939
+ if (!url) return void 0;
940
+ try {
941
+ const parsed = new URL(url);
942
+ parsed.username = "";
943
+ parsed.password = "";
944
+ return parsed.toString();
945
+ } catch {
946
+ return url;
947
+ }
948
+ } catch {
949
+ return;
950
+ }
951
+ }
952
+ /** The site's title from its own `src/src.11tydata.json`, or undefined. */
953
+ function siteTitle(root) {
954
+ try {
955
+ const data = JSON.parse(readFileSync(path.join(root, "src", "src.11tydata.json"), "utf-8"));
956
+ return typeof data?.siteTitle === "string" && data.siteTitle.trim() ? data.siteTitle.trim() : void 0;
957
+ } catch {
958
+ return;
959
+ }
960
+ }
961
+ /**
962
+ * A sandbox entry that has lost its checkout: the same folder name, and none of its paths exists any more. A moved
963
+ * sandbox is re-found this way, since nothing inside a sandbox tells two of them apart.
964
+ */
965
+ function movedSandbox(sites, root) {
966
+ const name = path.basename(root);
967
+ return sites.find((entry) => entry.sandbox && entry.name === name && !entry.paths.some((p) => existsSync(p)));
968
+ }
969
+ /** Take the checkout's root out of the paths of every entry its `identity.json` says it no longer belongs to. */
970
+ function dropStalePaths(sites, site) {
971
+ for (const entry of sites) if (entry.paths.some((p) => canonicalPath(p) === site.root) && !agreesWithIdentity(entry, site.identity)) entry.paths = entry.paths.filter((p) => canonicalPath(p) !== site.root);
972
+ }
973
+ /**
974
+ * Record where the checkout is now: its root goes to the front of the entry's paths, and its remote and title are
975
+ * refreshed, and any other entry still listing the root for a site its `identity.json` no longer names loses it. An
976
+ * unregistered sandbox is registered (sandboxes made before the registry, or moved). An unregistered
977
+ * real checkout is left alone — it is a direct-mode checkout, and direct mode never touches the registry. Best
978
+ * effort: a registry that cannot be written is logged, never fatal. Returns the entry as it now stands.
979
+ */
980
+ async function refreshSite(site) {
981
+ if (!site.entry && !site.sandbox) return null;
982
+ const remote = originUrl(site.root);
983
+ const companyName = site.sandbox ? void 0 : siteTitle(site.root);
984
+ try {
985
+ return await updateSites((file) => {
986
+ const target = (site.entry ? file.sites.find((candidate) => sameSite(candidate, site.entry)) : null) ?? (site.sandbox ? movedSandbox(file.sites, site.root) : void 0);
987
+ dropStalePaths(file.sites, site);
988
+ if (target) {
989
+ target.paths = [...new Set([site.root, ...target.paths.map(canonicalPath)])];
990
+ if (remote) target.remote = remote;
991
+ if (companyName) target.companyName = companyName;
992
+ return target;
993
+ }
994
+ if (!site.sandbox) return null;
995
+ return mergeSite(file, {
996
+ companyId: site.identity?.companyId ?? "non-cms-company",
997
+ environment: "dev",
998
+ sandbox: true,
999
+ name: path.basename(site.root),
1000
+ paths: [site.root],
1001
+ initialized: true,
1002
+ ...remote ? { remote } : {}
1003
+ });
1004
+ });
1005
+ } catch (e) {
1006
+ log.debug(`Could not record ${site.root} in ${getSitesPath()}: ${e.message}`);
1007
+ return site.entry;
1008
+ }
1009
+ }
1010
+ /**
1011
+ * The `config` object site commands receive, built from a registry entry: the shape `leed.config.json` gave them,
1012
+ * so callers did not change. `location` is the folder two above the checkout, as it always was; callers use the
1013
+ * checkout path itself (`rawContentRepo`), never a path rebuilt from `location`.
1014
+ */
1015
+ function siteConfigFromEntry(entry, root) {
1016
+ return {
1017
+ environment: entry.environment,
1018
+ domain: entry.domain,
1019
+ companyId: entry.companyId,
1020
+ userId: entry.userId ?? "",
1021
+ initialized: entry.initialized,
1022
+ huskyVersion: entry.huskyVersion,
1023
+ location: path.dirname(path.dirname(root)),
1024
+ configPath: getSitesPath()
1025
+ };
1026
+ }
1027
+ /** Change fields of the registered entry for the same site as `entry`. */
1028
+ function saveSiteConfig(entry, patch) {
1029
+ return updateSites((file) => {
1030
+ const target = file.sites.find((candidate) => sameSite(candidate, entry));
1031
+ if (target) Object.assign(target, patch);
1032
+ });
1033
+ }
1034
+ //#endregion
1035
+ //#region src/cli/authShared.ts
1036
+ var authShared_exports = /* @__PURE__ */ __exportAll({
1037
+ cliApi: () => cliApi,
1038
+ companyOption: () => companyOption,
1039
+ detectSiteCompany: () => detectSiteCompany,
1040
+ envOption: () => envOption,
1041
+ loadSiteContext: () => loadSiteContext,
1042
+ notASiteCheckout: () => notASiteCheckout,
1043
+ siteEnvironment: () => siteEnvironment,
1044
+ siteRepoScope: () => siteRepoScope
1045
+ });
1046
+ /**
1047
+ * The pieces every `leed auth` subcommand shares (#333 Phase 3).
1048
+ *
1049
+ * `src/cli/auth.ts` was 596 lines and five commands deep before the refactor;
1050
+ * it is now one file per subcommand, and this is what they have in common: the
1051
+ * HTTP client, the `--env` option, and the site context that `rotate`, `reset`
1052
+ * and `status` all resolve the same way.
1053
+ */
1054
+ /** An HTTP client that identifies the CLI and its platform to the CMS. */
1055
+ function cliApi(version) {
1056
+ return ky.create({ headers: { "User-Agent": `leed-cli/${version} (${process.platform} ${process.arch})` } });
1057
+ }
1058
+ /** The environment the current checkout's site targets, or null outside a checkout (and in a sandbox). */
1059
+ function siteEnvironment() {
1060
+ return siteFacts()?.environment ?? null;
1061
+ }
1062
+ /**
1063
+ * The checkout's company for `env` (#347), or undefined outside a checkout, when the checkout names no company, or
1064
+ * when it targets another environment — companies are per environment, so a production site's id means nothing on
1065
+ * staging. Only the checkout the working directory is in counts (#459: its `sites.json` entry, else its own
1066
+ * `identity.json`), never a site above it.
1067
+ */
1068
+ function detectSiteCompany(env) {
1069
+ const site = siteFacts();
1070
+ if (!site?.companyId) return void 0;
1071
+ if ((site.environment ?? EnvName.safeParse(process.env.ENV_NAME).data) !== env) return void 0;
1072
+ return {
1073
+ companyId: site.companyId,
1074
+ ...site.domain ? { domain: site.domain } : {}
1075
+ };
1076
+ }
1077
+ /**
1078
+ * What the current checkout says about where it lives (#351): null outside a checkout, else the companyId and
1079
+ * environment it names — each absent when it does not name it, never defaulted.
1080
+ */
1081
+ function siteRepoScope() {
1082
+ return siteFacts();
1083
+ }
1084
+ /** The `--company` option shared by the per-company `auth` subcommands. */
1085
+ function companyOption() {
1086
+ return new Option("--company <company>", "Company to act on (companyId or domain)");
1087
+ }
1088
+ /**
1089
+ * The `--env` option shared by `auth login` and `auth logout`.
1090
+ *
1091
+ * A factory rather than a shared instance: the two commands now live in
1092
+ * separate modules, and Commander options carry per-command state. It has no
1093
+ * default (#462): unset, the command reads the checkout's environment at action
1094
+ * time, and outside a checkout it refuses rather than assume one.
1095
+ */
1096
+ function envOption() {
1097
+ return new Option("-e, --env <name>", "Target environment").env("ENV_NAME").choices(EnvName.options).hideHelp();
1098
+ }
1099
+ /**
1100
+ * Load the site context of the checkout the working directory is in (#459): its `sites.json` entry, else — for a
1101
+ * checkout that is not registered — its own `identity.json`, the way direct mode reads it. Any `leed.config.json` an
1102
+ * older CLI left nearby is imported first. With `readOnly` (a command that must not change disk, such as
1103
+ * `leed auth status` or a `--dry-run`) nothing is imported or refreshed: the legacy files are only read.
1104
+ *
1105
+ * Throws a `CliUserError` outside a checkout: `rotate` and `reset` both need a site context to know which company
1106
+ * they are operating against, and under `--json` `runAndEmit` turns that throw into a `not_a_site_repo` envelope
1107
+ * carrying the same sentence.
1108
+ */
1109
+ async function loadSiteContext(options = {}) {
1110
+ let site;
1111
+ if (options.readOnly) site = currentSite(process.cwd(), void 0, sitesWithLegacy());
1112
+ else {
1113
+ await importAndAnnounce(jsonModeFromEnv() || process.argv.includes("--json"));
1114
+ site = currentSite();
1115
+ }
1116
+ if (site?.entry && !site.sandbox) {
1117
+ const entry = (options.readOnly ? null : await refreshSite(site)) ?? site.entry;
1118
+ const config = siteConfigFromEntry(entry, site.root);
1119
+ return {
1120
+ env: entry.environment,
1121
+ config,
1122
+ customer: slugify(entry.domain),
1123
+ rawContentRepo: site.root
1124
+ };
1125
+ }
1126
+ if (site && !site.sandbox && site.identity?.companyId) {
1127
+ const env = identityEnvironment(site.identity.environment);
1128
+ return {
1129
+ env,
1130
+ config: {
1131
+ environment: env,
1132
+ domain: "",
1133
+ companyId: site.identity.companyId,
1134
+ userId: "",
1135
+ initialized: true,
1136
+ huskyVersion: "",
1137
+ location: path.dirname(path.dirname(site.root)),
1138
+ configPath: ""
1139
+ },
1140
+ customer: path.basename(path.dirname(site.root)),
1141
+ rawContentRepo: site.root
1142
+ };
1143
+ }
1144
+ throw notASiteCheckout();
1145
+ }
1146
+ /**
1147
+ * The environment of a checkout known only by its `identity.json`: the one it names, else `ENV_NAME`, else a
1148
+ * refusal — never production (#462). The working directory is a checkout, so the refusal says what it lacks rather
1149
+ * than `not_a_site_repo`.
1150
+ */
1151
+ function identityEnvironment(named) {
1152
+ const env = EnvName.safeParse(named).data ?? EnvName.safeParse(process.env.ENV_NAME).data;
1153
+ if (env) return env;
1154
+ throw new CliUserError("This site checkout's identity.json names no environment, and ENV_NAME is not set, so the environment is unknown.", "Set ENV_NAME=dev|staging|production, or pass -e <env> where the command takes it", "invalid_arguments");
1155
+ }
1156
+ /** What `loadSiteContext` throws outside a site checkout. */
1157
+ function notASiteCheckout() {
1158
+ return new CliUserError("This command must be run from inside a Leed site checkout.", "cd into your site's checkout (the folder holding identity.json; see leed site list) and try again.", "not_a_site_repo");
1159
+ }
1160
+ //#endregion
1161
+ export { isCI as A, JSON_OPTION_FLAGS as B, importAndAnnounce as C, sameSite as D, readSites as E, siteIdentityPath as F, jsonModeFromOptions as H, progress as I, progressLines as L, findCheckoutRoot as M, rawContentSlot as N, upsertSite as O, readSiteIdentity as P, writeDiagnostic as R, sandboxTemplateDir as S, getSitesPath as T, resolveJsonMode as U, jsonModeFromEnv as V, resolveSiteRef as _, envOption as a, findSandboxRoot as b, siteRepoScope as c, saveSiteConfig as d, siteConfigFromEntry as f, livePath as g, findSiteForCheckout as h, detectSiteCompany as i, canonicalPath as j, writePrivateJson as k, currentSite as l, describeSites as m, cliApi as n, loadSiteContext as o, agreesWithIdentity as p, companyOption as r, siteEnvironment as s, authShared_exports as t, refreshSite as u, siteLabel as v, sitesWithLegacy as w, isSandboxIdentity as x, SANDBOX_COMPANY_ID as y, JSON_OPTION_DESCRIPTION as z };