@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.
- package/LICENSE.md +57 -0
- package/README.md +125 -2
- package/dist/LICENSE.md +57 -0
- package/dist/acorn-DAgDT1om.js +5087 -0
- package/dist/angular-l0_4PK8N.js +3598 -0
- package/dist/authShared-Dray38Ls.js +1161 -0
- package/dist/babel-CJeRQd5u.js +10038 -0
- package/dist/buildResults-tiq1nugP.js +21180 -0
- package/dist/chunk-1VJ9lAw7.js +42 -0
- package/dist/cliAuth-38JjD6hw.js +15 -0
- package/dist/env-B0bHiXJJ.js +29 -0
- package/dist/errors-CqKamGCu.js +481 -0
- package/dist/estree-E8WG8UHj.js +6930 -0
- package/dist/flow-6xaRIbPQ.js +52255 -0
- package/dist/glimmer-aJPjsfbT.js +7272 -0
- package/dist/graphql-mcgtPEiZ.js +1901 -0
- package/dist/html--eeea3Kl.js +6553 -0
- package/dist/leed.eleventy.mjs +6525 -0
- package/dist/leed.mjs +88952 -0
- package/dist/leed.sources.json +798 -0
- package/dist/leedLogger-Zkada5aO.js +78 -0
- package/dist/markdown-D73jNKwx.js +6836 -0
- package/dist/meriyah-CMp1nDDW.js +8932 -0
- package/dist/package.json +101 -0
- package/dist/postcss-h4fBr2Fn.js +6760 -0
- package/dist/prettier-CwFSKxfN.js +16821 -0
- package/dist/static/customer-site-worker/package.json +9 -0
- package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
- package/dist/static/customer-site-worker/src/constants.ts +20 -0
- package/dist/static/customer-site-worker/src/cors.ts +120 -0
- package/dist/static/customer-site-worker/src/index.ts +242 -0
- package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
- package/dist/static/customer-site-worker/src/proxy.ts +92 -0
- package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
- package/dist/static/customer-site-worker/src/tier.ts +40 -0
- package/dist/static/customer-site-worker/src/types.ts +82 -0
- package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
- package/dist/static/customer-site-worker/tsconfig.json +19 -0
- package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
- package/dist/static/install/husky/colors.mjs +5 -0
- package/dist/static/install/husky/hooks/commit-msg +1 -0
- package/dist/static/install/husky/hooks/pre-commit +1 -0
- package/dist/static/install/husky/hooks/pre-push +1 -0
- package/dist/static/install/husky/valid-command.mjs +29 -0
- package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
- package/dist/static/install/package.json +10 -0
- package/dist/static/install/packages.txt +4 -0
- package/dist/static/install/tasks.json +111 -0
- package/dist/static/javascript/detectIncognito.js +195 -0
- package/dist/static/javascript/documentation.js +2781 -0
- package/dist/static/javascript/fp/fp.esm.js +2896 -0
- package/dist/static/javascript/fp/fp.js +3067 -0
- package/dist/static/javascript/live-search.js +986 -0
- package/dist/static/javascript/lunr-search.js +843 -0
- package/dist/static/javascript/timeme.js +451 -0
- package/dist/static/javascript/tracker/webmcp.js +67 -0
- package/dist/static/javascript/tracker/whisper.js +1 -0
- package/dist/static/javascript/utilities.js +819 -0
- package/dist/static/javascript/versions.json +74 -0
- package/dist/static/javascript/zoomable.js +488 -0
- package/dist/static/site/_data/eleventyComputed.js +228 -0
- package/dist/static/site/_data/leed-form-data.json +253 -0
- package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
- package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
- package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
- package/dist/static/site/_includes/leed/api/object.hbs +110 -0
- package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
- package/dist/static/site/_includes/leed/api/property.hbs +153 -0
- package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
- package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
- package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
- package/dist/static/site/_includes/leed/api/security.hbs +75 -0
- package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
- package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
- package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
- package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
- package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
- package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
- package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
- package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
- package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
- package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
- package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
- package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
- package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
- package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
- package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
- package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
- package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
- package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
- package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
- package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
- package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
- package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
- package/dist/static/site/_includes/leed/form.hbs +266 -0
- package/dist/static/site/_includes/leed/head.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
- package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
- package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
- package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
- package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
- package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
- package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
- package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
- package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
- package/dist/static/site/leed-feed-atom.hbs +42 -0
- package/dist/static/site/leed-feed-json.hbs +56 -0
- package/dist/static/site/leed-feed-rss.hbs +39 -0
- package/dist/static/site/leed-headers.hbs +38 -0
- package/dist/static/site/leed-redirects.hbs +10 -0
- package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
- package/dist/static/site/leed-sitemap.hbs +19 -0
- package/dist/static/site/leed-unsubscribe.hbs +44 -0
- package/dist/static/site/leed-unsubscribed.hbs +40 -0
- package/dist/static/tailwind/components/alerts.css +132 -0
- package/dist/static/tailwind/components/api-methods.css +39 -0
- package/dist/static/tailwind/components/breadcrumbs.css +31 -0
- package/dist/static/tailwind/components/code.css +88 -0
- package/dist/static/tailwind/components/collapsible.css +38 -0
- package/dist/static/tailwind/components/document-search.css +222 -0
- package/dist/static/tailwind/components/form.css +185 -0
- package/dist/static/tailwind/components/kbd.css +7 -0
- package/dist/static/tailwind/components/pagination.css +81 -0
- package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
- package/dist/static/tailwind/components/tab-groups.css +117 -0
- package/dist/static/tailwind/components/tables.css +0 -0
- package/dist/static/tailwind/components/toc.css +60 -0
- package/dist/static/tailwind/components.css +248 -0
- package/dist/static/tailwind/defaults.css +260 -0
- package/dist/static/tailwind/docs/api.css +701 -0
- package/dist/static/tailwind/docs/components/blockquote.css +5 -0
- package/dist/static/tailwind/docs/components/button.css +11 -0
- package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
- package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
- package/dist/static/tailwind/docs/components/navigation.css +279 -0
- package/dist/static/tailwind/docs/components/table.css +54 -0
- package/dist/static/tailwind/docs/components.css +6 -0
- package/dist/static/tailwind/docs/docs.css +427 -0
- package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
- package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
- package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
- package/dist/static/tailwind/docs/layouts.css +3 -0
- package/dist/static/tailwind/docs/themes/amber.css +113 -0
- package/dist/static/tailwind/docs/themes/blue.css +94 -0
- package/dist/static/tailwind/docs/themes/cyan.css +111 -0
- package/dist/static/tailwind/docs/themes/emerald.css +111 -0
- package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
- package/dist/static/tailwind/docs/themes/green.css +95 -0
- package/dist/static/tailwind/docs/themes/indigo.css +111 -0
- package/dist/static/tailwind/docs/themes/lime.css +111 -0
- package/dist/static/tailwind/docs/themes/orange.css +111 -0
- package/dist/static/tailwind/docs/themes/pink.css +111 -0
- package/dist/static/tailwind/docs/themes/purple.css +111 -0
- package/dist/static/tailwind/docs/themes/red.css +111 -0
- package/dist/static/tailwind/docs/themes/rose.css +111 -0
- package/dist/static/tailwind/docs/themes/sky.css +111 -0
- package/dist/static/tailwind/docs/themes/teal.css +111 -0
- package/dist/static/tailwind/docs/themes/violet.css +111 -0
- package/dist/static/tailwind/docs/themes/yellow.css +111 -0
- package/dist/static/tailwind/docs/themes.css +84 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
- package/dist/static/tailwind/fonts/all.css +6 -0
- package/dist/static/tailwind/fonts/config.jsonc +31 -0
- package/dist/static/tailwind/fonts/geist-mono.css +142 -0
- package/dist/static/tailwind/fonts/geist.css +142 -0
- package/dist/static/tailwind/fonts/inter.css +215 -0
- package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
- package/dist/static/tailwind/fonts/noto-sans.css +134 -0
- package/dist/static/tailwind/fonts/open-sans.css +89 -0
- package/dist/static/tailwind/highlighters.css +14 -0
- package/dist/static/tailwind/hljs/README.md +53 -0
- package/dist/static/tailwind/hljs/a11y.css +96 -0
- package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
- package/dist/static/tailwind/hljs/atom-one.css +164 -0
- package/dist/static/tailwind/hljs/default.css +164 -0
- package/dist/static/tailwind/hljs/github.css +101 -0
- package/dist/static/tailwind/hljs/google-code.css +112 -0
- package/dist/static/tailwind/hljs/grayscale.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
- package/dist/static/tailwind/hljs/papercolor.css +120 -0
- package/dist/static/tailwind/hljs/qt-creator.css +91 -0
- package/dist/static/tailwind/hljs/summerfruit.css +115 -0
- package/dist/static/tailwind/hljs/tomorrow.css +115 -0
- package/dist/static/tailwind/leed.css +170 -0
- package/dist/static/tailwind/site-tokens.reference.css +105 -0
- package/dist/static/tailwind/utils/icons.css +102 -0
- package/dist/static/tailwind/utils/utilities.css +105 -0
- package/dist/static/templates/pages/PAGE.hbs +27 -0
- package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
- package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
- package/dist/static/templates/raw-content/.ci/build.sh +58 -0
- package/dist/static/templates/raw-content/README.md +179 -0
- package/dist/static/templates/raw-content/_gitignore +50 -0
- package/dist/static/templates/raw-content/src/404.hbs +21 -0
- package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
- package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
- package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
- package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
- package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
- package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
- package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/raw-content/src/index.hbs +21 -0
- package/dist/static/templates/raw-content/src/robots.hbs +9 -0
- package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
- package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
- package/dist/static/templates/sandbox/_gitignore +50 -0
- package/dist/static/templates/sandbox/identity.json +1 -0
- package/dist/static/templates/sandbox/src/404.hbs +21 -0
- package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
- package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
- package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
- package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
- package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
- package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
- package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
- package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/sandbox/src/index.hbs +20 -0
- package/dist/static/templates/sandbox/src/robots.hbs +9 -0
- package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
- package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
- package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
- package/dist/thrown-sukVkp_1.js +113 -0
- package/dist/typescript-D4T0V_Hn.js +23004 -0
- package/dist/yaml-9wR8GxF_.js +5236 -0
- 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 };
|