hd-garden 0.10.3 → 0.11.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 (85) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +11 -10
  2. package/README.en.md +44 -6
  3. package/README.md +45 -7
  4. package/app/components/garden/GardenBlock.tsx +20 -4
  5. package/app/components/garden/blocks.tsx +6 -118
  6. package/app/globals.css +1 -0
  7. package/app/lib/garden-docs-meta.ts +3 -3
  8. package/app/lib/generated-content.ts +1 -1
  9. package/app/lib/i18n.ts +0 -6
  10. package/app/lib/remark-garden.mjs +61 -5
  11. package/app/styles/a2.css +25 -112
  12. package/app/styles/tokens.css +5 -0
  13. package/bin/garden.mjs +150 -112
  14. package/docs/handbook/ai-writing.md +33 -7
  15. package/docs/handbook/components/compose.md +354 -0
  16. package/docs/handbook/components/html.md +1 -1
  17. package/docs/handbook/components/image.md +1 -1
  18. package/docs/handbook/components/interactive.md +4 -1
  19. package/docs/handbook/components/link-card.md +1 -1
  20. package/docs/handbook/components.md +23 -35
  21. package/docs/handbook/design.md +25 -34
  22. package/docs/handbook/en/ai-writing.md +27 -7
  23. package/docs/handbook/en/components/compose.md +352 -0
  24. package/docs/handbook/en/components/html.md +1 -1
  25. package/docs/handbook/en/components/image.md +1 -1
  26. package/docs/handbook/en/components/interactive.md +4 -1
  27. package/docs/handbook/en/components.md +23 -36
  28. package/docs/handbook/en/design.md +25 -33
  29. package/docs/handbook/en/index.md +6 -5
  30. package/docs/handbook/en/reference/compose.md +99 -0
  31. package/docs/handbook/en/reference.md +2 -2
  32. package/docs/handbook/en/start.md +13 -0
  33. package/docs/handbook/en/workflow.md +47 -7
  34. package/docs/handbook/index.md +6 -5
  35. package/docs/handbook/reference/compose.md +99 -0
  36. package/docs/handbook/reference.md +2 -2
  37. package/docs/handbook/start.md +13 -0
  38. package/docs/handbook/workflow.md +47 -7
  39. package/editors/dist/typora/main.js +412 -0
  40. package/editors/dist/typora/manifest.json +12 -0
  41. package/editors/dist/typora/style.css +1215 -0
  42. package/editors/dist/vscode/garden-ui-0.1.0.vsix +0 -0
  43. package/eslint.config.mjs +1 -0
  44. package/package.json +17 -3
  45. package/scripts/editors-kit.mjs +352 -0
  46. package/scripts/handbook-allowlist.mjs +0 -8
  47. package/scripts/handbook-facts.mjs +6 -36
  48. package/scripts/pack-editors.mjs +241 -0
  49. package/scripts/ui-cli.mjs +5 -1
  50. package/templates/garden-blocks.md +34 -31
  51. package/templates/garden-snippets.plist +3 -8
  52. package/templates/site/README.en.md +2 -0
  53. package/templates/site/README.md +2 -0
  54. package/templates/skills-en/garden-ui/SKILL.md +11 -10
  55. package/ui/README.md +1 -0
  56. package/ui/compose/compose.css +806 -0
  57. package/ui/compose/dil.mjs +443 -0
  58. package/ui/compose/examples.mjs +28 -0
  59. package/ui/compose/index.mjs +7 -0
  60. package/ui/compose/markup.mjs +364 -0
  61. package/ui/compose/parse.mjs +21 -0
  62. package/ui/compose/render.mjs +247 -0
  63. package/ui/core/cli-locale.mjs +4 -0
  64. package/ui/core/fence.mjs +13 -1
  65. package/ui/core/fix-prompt.mjs +1 -0
  66. package/ui/core/garden-fence.mjs +4 -1
  67. package/ui/core/scan.mjs +28 -2
  68. package/ui/core/transform.mjs +12 -0
  69. package/ui/core/validate.mjs +6 -4
  70. package/ui/garden/catalog.mjs +58 -35
  71. package/ui/garden/check.mjs +22 -6
  72. package/ui/garden/parse.mjs +26 -0
  73. package/ui/garden/snippets.mjs +101 -12
  74. package/ui/garden/word-count.mjs +10 -18
  75. package/ui/preview/dist/garden-ui.esm.js +158 -132
  76. package/ui/preview/dist/garden-ui.iife.js +157 -131
  77. package/ui/prompt/build.mjs +1 -0
  78. package/ui/prompt/recipes/compose.md +40 -0
  79. package/ui/prompt/rules.md +3 -3
  80. package/ui/react/CatalogPage.tsx +2 -2
  81. package/ui/react/runtime.tsx +5 -1
  82. package/docs/handbook/components/figure.md +0 -59
  83. package/docs/handbook/en/components/figure.md +0 -59
  84. package/docs/handbook/en/reference/figure.md +0 -84
  85. package/docs/handbook/reference/figure.md +0 -84
@@ -1,3 +1,4 @@
1
+ import { renderCompose } from "../../ui/compose/render.mjs";
1
2
  import { diffToNewCode, protectMathIdentifiers } from "../../ui/garden/enhance.mjs";
2
3
  import { classifyFence } from "../../ui/garden/parse.mjs";
3
4
 
@@ -29,12 +30,58 @@ function fenceNodeToRecord(node) {
29
30
  return { lang: String(node.lang || "").toLowerCase(), meta: String(node.meta || ""), info: `${node.lang || ""} ${node.meta || ""}`.trim() };
30
31
  }
31
32
 
32
- function rewriteChildren(node) {
33
+ function htmlElement(value) {
34
+ return { type: "html", value };
35
+ }
36
+
37
+ const PAIR_ALT = new Set(["ChatGPT 原图", "ChatGPT original"]);
38
+
39
+ function pairImage(node) {
40
+ if (node?.type !== "paragraph" || !Array.isArray(node.children)) return null;
41
+ const images = node.children.filter((child) => child?.type === "image");
42
+ if (images.length !== 1 || node.children.some((child) => child?.type !== "image" && String(child.value ?? "").trim())) return null;
43
+ const image = images[0];
44
+ const alt = String(image.alt ?? "").trim();
45
+ return PAIR_ALT.has(alt) ? { image, alt } : null;
46
+ }
47
+
48
+ function element(tag, className, children) {
49
+ return {
50
+ type: "paragraph",
51
+ data: { hName: tag, hProperties: { className: [className] } },
52
+ children,
53
+ };
54
+ }
55
+
56
+ function composePair(image, alt, composeHtml) {
57
+ return element("div", "g-compose-pair", [
58
+ element("figure", "g-compose-pair-ref", [
59
+ element("figcaption", "g-compose-pair-k", [{ type: "text", value: alt }]),
60
+ image,
61
+ ]),
62
+ element("div", "g-compose-pair-live", [
63
+ element("p", "g-compose-pair-k", [{ type: "text", value: "Garden Compose" }]),
64
+ htmlElement(composeHtml),
65
+ ]),
66
+ ]);
67
+ }
68
+
69
+ function rewriteChildren(node, nextScope) {
33
70
  if (!Array.isArray(node.children)) return;
34
71
  const next = [];
35
72
  for (let index = 0; index < node.children.length; index += 1) {
36
73
  const child = node.children[index];
37
74
  const following = node.children[index + 1];
75
+ const pair = pairImage(child);
76
+ if (pair && following?.type === "code") {
77
+ const record = fenceNodeToRecord(following);
78
+ const kind = classifyFence({ ...record, body: following.value, line: 1, endLine: 1 });
79
+ if (kind.kind === "garden" && kind.block?.name === "compose" && !kind.block.error && kind.block.mode !== "play") {
80
+ next.push(composePair(pair.image, pair.alt, renderCompose(kind.block.props, { scope: nextScope() })));
81
+ index += 1;
82
+ continue;
83
+ }
84
+ }
38
85
  if (child?.type === "code" && following?.type === "code" && String(following.lang || "").toLowerCase() === "diff") {
39
86
  const current = fenceNodeToRecord(child);
40
87
  const kind = classifyFence({ lang: current.lang, meta: current.meta, info: current.info });
@@ -51,6 +98,10 @@ function rewriteChildren(node) {
51
98
  next.push(gardenElement({ legacy: true, from: kind.lang || record.lang, suggestion: kind.suggestion }));
52
99
  continue;
53
100
  }
101
+ if (kind.kind === "garden" && kind.block?.name === "compose" && !kind.block.error && kind.block.mode !== "play") {
102
+ next.push(htmlElement(renderCompose(kind.block.props, { scope: nextScope() })));
103
+ continue;
104
+ }
54
105
  if (kind.kind === "garden" && kind.block) {
55
106
  next.push(gardenElement(kind.block));
56
107
  continue;
@@ -61,20 +112,25 @@ function rewriteChildren(node) {
61
112
  node.children = next;
62
113
  }
63
114
 
64
- function walk(node) {
115
+ function walk(node, nextScope) {
65
116
  if (!node || typeof node !== "object") return;
66
117
  if (node.type === "math" || node.type === "inlineMath") {
67
118
  node.value = protectMathIdentifiers(node.value);
68
119
  }
69
120
  if (Array.isArray(node.children)) {
70
- for (const child of node.children) walk(child);
71
- rewriteChildren(node);
121
+ for (const child of node.children) walk(child, nextScope);
122
+ rewriteChildren(node, nextScope);
72
123
  }
73
124
  }
74
125
 
75
126
  export function remarkGarden() {
127
+ let composeCount = 0;
128
+ const nextScope = () => {
129
+ composeCount += 1;
130
+ return `gc${composeCount}`;
131
+ };
76
132
  return (tree) => {
77
- walk(tree);
133
+ walk(tree, nextScope);
78
134
  };
79
135
  }
80
136
 
package/app/styles/a2.css CHANGED
@@ -73,8 +73,7 @@ body {
73
73
  .g-card,
74
74
  .g-timeline,
75
75
  .g-sandbox,
76
- .g-html,
77
- .g-figure {
76
+ .g-html {
78
77
  box-shadow: none;
79
78
  }
80
79
 
@@ -289,6 +288,17 @@ body {
289
288
  max-width: var(--col);
290
289
  }
291
290
 
291
+ /* The compare post puts two wide diagrams side by side. */
292
+ @media (min-width: 1260px) {
293
+ .post-reading-page:has(.g-compose-pair) .article-reading-layout {
294
+ grid-template-columns: 24px minmax(0, 1fr) var(--rail);
295
+ }
296
+
297
+ .post-reading-page:has(.g-compose-pair) .article-reading-column {
298
+ max-width: none;
299
+ }
300
+ }
301
+
292
302
  .post-reading-page .article-reading-layout > .toc {
293
303
  grid-column: 3;
294
304
  grid-row: 1;
@@ -785,6 +795,17 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
785
795
  background: transparent;
786
796
  }
787
797
 
798
+ .table-wrap td .markdown-image-trigger {
799
+ margin: 0;
800
+ }
801
+
802
+ .table-wrap td .markdown-image-trigger img {
803
+ width: 280px;
804
+ max-width: 100%;
805
+ height: auto;
806
+ border-radius: 6px;
807
+ }
808
+
788
809
  @media (min-width: 900px) {
789
810
  .ux-wide {
790
811
  margin-left: calc(-1 * var(--wl, 0px)) !important;
@@ -885,8 +906,6 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
885
906
  .g-cmp,
886
907
  .g-timeline,
887
908
  .g-chart,
888
- .g-figure,
889
- .g-explore,
890
909
  .g-html,
891
910
  .g-idea,
892
911
  .g-legacy,
@@ -895,77 +914,9 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
895
914
  box-shadow: none;
896
915
  }
897
916
 
898
- .g-explore {
899
- border: 1px solid var(--rule);
900
- border-radius: var(--r);
901
- padding: 12px 14px 14px;
902
- background: transparent;
903
- max-width: 100%;
904
- }
905
-
906
- .g-explore [role="tablist"] {
907
- display: flex;
908
- flex-wrap: wrap;
909
- gap: 6px;
910
- margin: 0;
911
- padding: 0;
912
- }
913
-
914
- .g-explore [role="tab"] {
915
- font: 500 14px / 1 var(--sans);
916
- padding: 9px 12px;
917
- min-height: 44px;
918
- border-radius: 999px;
919
- border: 1px solid var(--rule);
920
- background: transparent;
921
- color: var(--soft);
922
- cursor: pointer;
923
- }
924
-
925
- .g-explore [role="tab"][aria-selected="true"] {
926
- border-color: var(--blue);
927
- color: var(--blue);
928
- background: var(--blue-tint);
929
- }
930
-
931
- .dx-wrap {
932
- overflow: visible;
933
- margin-top: 10px;
934
- max-width: 100%;
935
- }
936
-
937
- .dx-wrap svg {
938
- display: block;
939
- width: 100%;
940
- max-width: 100%;
941
- height: auto;
942
- }
943
-
944
- .g-explore:not([data-sel="all"]) .dx-wrap [data-p]:not(.on),
945
- .g-explore:not([data-sel="all"]) .dx-wrap text:not(.on) {
946
- opacity: 0.22;
947
- }
948
-
949
- .g-explore .on {
950
- opacity: 1;
951
- }
952
-
953
- .dx-exp p {
954
- margin: 10px 0 0;
955
- }
956
-
957
- .g-explore figcaption {
958
- font-size: 13px;
959
- color: var(--muted);
960
- margin-top: 8px;
961
- text-align: left;
962
- }
963
-
964
917
  .g-sandbox,
965
918
  .g-play,
966
- .g-html,
967
- .g-explore,
968
- .dx-wrap {
919
+ .g-html {
969
920
  max-width: 100%;
970
921
  }
971
922
 
@@ -973,7 +924,6 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
973
924
  .g-stat,
974
925
  .g-cmp,
975
926
  .g-timeline,
976
- .g-figure,
977
927
  .g-html {
978
928
  border: 1px solid var(--rule);
979
929
  border-radius: var(--r);
@@ -982,8 +932,7 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
982
932
 
983
933
  .g-card,
984
934
  .g-cmp,
985
- .g-html,
986
- .g-figure {
935
+ .g-html {
987
936
  padding: 14px 16px;
988
937
  }
989
938
 
@@ -1163,7 +1112,6 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
1163
1112
  min-width: 3px;
1164
1113
  }
1165
1114
 
1166
- .g-figure svg,
1167
1115
  .g-mermaid svg,
1168
1116
  .dx-svg {
1169
1117
  display: block;
@@ -1172,16 +1120,6 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
1172
1120
  height: auto;
1173
1121
  }
1174
1122
 
1175
- .g-figure:not([data-sel="all"]) [data-p]:not(.on),
1176
- .g-figure:not([data-sel="all"]) text:not(.on) {
1177
- opacity: 0.22;
1178
- }
1179
-
1180
- .g-figure [data-p].on,
1181
- .g-figure text.on {
1182
- opacity: 1;
1183
- }
1184
-
1185
1123
  .g-mermaid {
1186
1124
  margin: 28px 0;
1187
1125
  padding: 16px;
@@ -1399,19 +1337,10 @@ html[data-theme="dark"] .markdown-body > p > img:not([class]) {
1399
1337
  padding-right: 44px;
1400
1338
  }
1401
1339
 
1402
- .ux-fs:not(.g-sandbox) > .ux-fsbar + .g-explore {
1403
- padding-right: 14px;
1404
- }
1405
-
1406
1340
  .ux-fs:not(.g-sandbox) > .ux-fsbar + .g-sandbox {
1407
1341
  padding-right: 0;
1408
1342
  }
1409
1343
 
1410
- .ux-fs:not(.g-sandbox) > .ux-fsbar + .g-explore > [role="tablist"] {
1411
- padding-right: 44px;
1412
- box-sizing: border-box;
1413
- }
1414
-
1415
1344
  .ux-fs:fullscreen,
1416
1345
  .ux-fs.ux-fs-on {
1417
1346
  background: var(--paper);
@@ -1793,13 +1722,6 @@ html[data-theme="dark"] .gf-scrim { background: rgb(0 0 0 / 45%); }
1793
1722
  font-family: var(--sans);
1794
1723
  }
1795
1724
 
1796
- .garden-doc-page .g-explore:not([data-sel="all"]) .dx-wrap [data-p]:not(.on),
1797
- .garden-doc-page .g-explore:not([data-sel="all"]) .dx-wrap text:not(.on),
1798
- .garden-doc-page .g-figure:not([data-sel="all"]) [data-p]:not(.on),
1799
- .garden-doc-page .g-figure:not([data-sel="all"]) text:not(.on) {
1800
- opacity: 0.56;
1801
- }
1802
-
1803
1725
  .garden-doc-page > .toc {
1804
1726
  margin-top: 8px;
1805
1727
  position: sticky;
@@ -2641,15 +2563,6 @@ html[data-theme="dark"] .ref-demo { --rd-s: #262624; --rd-p: #1f1e1d; --rd-i: #e
2641
2563
  .ref-demo-in .toc-mobile-trigger { opacity: 0; visibility: hidden; pointer-events: none; }
2642
2564
  }
2643
2565
 
2644
- /* figure reference page: the drawing needs room, so the example stacks
2645
- (preview on top, full width; code below). Scoped to the reference demo;
2646
- figures in articles are untouched. */
2647
- @media (min-width: 1024px) {
2648
- .ref-demo[data-name="figure"] { grid-template-columns: minmax(0, 1fr); background: var(--rd-p); }
2649
- .ref-demo[data-name="figure"] .ref-pane[data-p="v"] { grid-column: 1; grid-row: 1; }
2650
- .ref-demo[data-name="figure"] .ref-pane[data-p="c"] { grid-column: 1; grid-row: 2; background: var(--rd-s); border-top: 1px solid var(--rd-r); }
2651
- }
2652
-
2653
2566
  /* ===== v0.10.1 tip keep (after git checkout of a2.css from 9a80f7e5) ===== */
2654
2567
 
2655
2568
  /* List pages: PageIntro rides on .sh-head; keep eyebrow in the same vertical rhythm. */
@@ -33,6 +33,9 @@
33
33
  --accent-soft: var(--blue-tint);
34
34
  --selection: #e8e5db;
35
35
  --hl: #ffe58f;
36
+ /* Two lane-arrow emphases. Same hues as the note and tip callouts, not a second palette. */
37
+ --flow: #2c55b4;
38
+ --flow-back: #2d6a3e;
36
39
  --red: #d82d48;
37
40
  --red-text: #c2263f;
38
41
  --red-tint: #fbeef0;
@@ -94,6 +97,8 @@ html[data-theme="dark"] {
94
97
  --accent-soft: var(--blue-tint);
95
98
  --selection: rgb(238 236 231 / 18%);
96
99
  --hl: #5e4e17;
100
+ --flow: #93b2f4;
101
+ --flow-back: #8fcb9f;
97
102
  --red: #f29aab;
98
103
  --red-text: #f29aab;
99
104
  --red-tint: rgb(242 154 171 / 12%);
package/bin/garden.mjs CHANGED
@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { createRequire } from "node:module";
7
+ import { editorsInstall, offerEditorsOnce } from "../scripts/editors-kit.mjs";
7
8
  import { gardenPackageRoot, gardenSiteRoot } from "../scripts/garden-paths.mjs";
8
9
  import { langOf, siteCliLang } from "../ui/core/cli-locale.mjs";
9
10
 
@@ -196,29 +197,30 @@ function copyIfMissing(from, to) {
196
197
  return true;
197
198
  }
198
199
 
199
- function ensureSiteKit(destinationRoot = siteRoot) {
200
- if (samePath(destinationRoot, gardenPackageRoot)) return;
200
+ async function ensureSiteKit(destinationRoot = siteRoot) {
201
201
  const lang = siteCliLang(destinationRoot);
202
+ if (!samePath(destinationRoot, gardenPackageRoot)) {
203
+ const added = [];
204
+ if (
205
+ copyIfMissing(
206
+ path.join(gardenPackageRoot, "templates", "site", lang === "en" ? "Makefile.en" : "Makefile"),
207
+ path.join(destinationRoot, "Makefile"),
208
+ )
209
+ ) {
210
+ added.push("Makefile");
211
+ }
212
+ if (added.length) {
213
+ process.stdout.write(
214
+ lang === "en"
215
+ ? `Added missing site files: ${added.join(", ")}\n`
216
+ : `已补齐站点文件:${added.join("、")}\n`,
217
+ );
218
+ }
202
219
 
203
- const added = [];
204
- if (
205
- copyIfMissing(
206
- path.join(gardenPackageRoot, "templates", "site", lang === "en" ? "Makefile.en" : "Makefile"),
207
- path.join(destinationRoot, "Makefile"),
208
- )
209
- ) {
210
- added.push("Makefile");
220
+ installSkill(destinationRoot, { onlyMissing: true, lang });
221
+ if (process.env.GARDEN_SKIP_HOOKS !== "1") installHook(destinationRoot, { quiet: true, lang });
211
222
  }
212
- if (added.length) {
213
- process.stdout.write(
214
- lang === "en"
215
- ? `Added missing site files: ${added.join(", ")}\n`
216
- : `已补齐站点文件:${added.join("、")}\n`,
217
- );
218
- }
219
-
220
- installSkill(destinationRoot, { onlyMissing: true, lang });
221
- if (process.env.GARDEN_SKIP_HOOKS !== "1") installHook(destinationRoot, { quiet: true, lang });
223
+ await offerEditorsOnce(destinationRoot, lang);
222
224
  }
223
225
 
224
226
  const HOOK_MARKER = "# garden: ui check";
@@ -272,19 +274,19 @@ function installHook(destinationRoot = siteRoot, { quiet = false, force = false,
272
274
  );
273
275
  }
274
276
 
275
- function prepare() {
277
+ async function prepare() {
276
278
  ensureEnv();
277
279
  ensurePublic();
278
280
  fs.mkdirSync(path.join(siteRoot, "content", "posts"), { recursive: true });
279
281
  fs.mkdirSync(path.join(siteRoot, "content", "thoughts"), { recursive: true });
280
- ensureSiteKit();
282
+ await ensureSiteKit();
281
283
  }
282
284
 
283
- function sync() {
285
+ async function sync() {
284
286
  if (!fs.existsSync(path.join(siteRoot, "site.config.yaml"))) {
285
287
  fail(`当前目录不是 Garden 站点(缺少 site.config.yaml):${siteRoot}`);
286
288
  }
287
- prepare();
289
+ await prepare();
288
290
  runNode("scripts/build-content.mjs");
289
291
  }
290
292
 
@@ -313,8 +315,8 @@ function ensureDevGitHubLogin() {
313
315
  fs.writeFileSync(local, text.endsWith("\n") ? text : `${text}\n`);
314
316
  }
315
317
 
316
- function runVinext(vinextArgs, { copyDist = false } = {}) {
317
- sync();
318
+ async function runVinext(vinextArgs, { copyDist = false } = {}) {
319
+ await sync();
318
320
  const cwd = vinextRoot();
319
321
  const result = spawnSync(process.execPath, [resolveVinext(), ...vinextArgs], {
320
322
  cwd,
@@ -378,7 +380,7 @@ function writeEngineDependency(destination) {
378
380
  fs.writeFileSync(sitePkgPath, `${JSON.stringify(sitePkg, null, 2)}\n`);
379
381
  }
380
382
 
381
- function initSite() {
383
+ async function initSite() {
382
384
  const { destination, lang } = parseInitArgs(rest);
383
385
  const template = path.join(gardenPackageRoot, "templates", "site");
384
386
  if (!fs.existsSync(template)) fail("Garden 包里缺少 templates/site。");
@@ -405,9 +407,10 @@ function initSite() {
405
407
  }
406
408
  }
407
409
  writeEngineDependency(destination);
410
+ process.stdout.write(lang === "en" ? `Created site: ${destination}\n` : `已创建站点:${destination}\n`);
411
+ installSkill(destination, { lang });
412
+ await offerEditorsOnce(destination, lang);
408
413
  if (lang === "en") {
409
- process.stdout.write(`Created site: ${destination}\n`);
410
- installSkill(destination, { lang });
411
414
  process.stdout.write("Next steps:\n");
412
415
  process.stdout.write(` cd ${destination}\n`);
413
416
  process.stdout.write(" npm install\n");
@@ -417,8 +420,6 @@ function initSite() {
417
420
  printGiscusGuide("en");
418
421
  return;
419
422
  }
420
- process.stdout.write(`已创建站点:${destination}\n`);
421
- installSkill(destination);
422
423
  process.stdout.write("下一步:\n");
423
424
  process.stdout.write(` cd ${destination}\n`);
424
425
  process.stdout.write(" npm install\n");
@@ -428,6 +429,35 @@ function initSite() {
428
429
  printGiscusGuide("zh");
429
430
  }
430
431
 
432
+ function editorInstallFlags(argv) {
433
+ const known = new Set(["--vscode", "--typora", "--yes"]);
434
+ for (const arg of argv) {
435
+ if (!known.has(arg)) {
436
+ const lang = siteCliLang(siteRoot);
437
+ fail(lang === "en" ? `Unknown option: ${arg}` : `未知参数:${arg}`);
438
+ }
439
+ }
440
+ return {
441
+ vscode: argv.includes("--vscode"),
442
+ typora: argv.includes("--typora"),
443
+ yes: argv.includes("--yes"),
444
+ };
445
+ }
446
+
447
+ async function editorsCommand(argv) {
448
+ const lang = siteCliLang(siteRoot);
449
+ const sub = argv[0];
450
+ if (sub !== "install") {
451
+ fail(
452
+ lang === "en"
453
+ ? "Usage: garden editors install [--vscode] [--typora] [--yes]"
454
+ : "用法:garden editors install [--vscode] [--typora] [--yes]",
455
+ );
456
+ }
457
+ const status = await editorsInstall(siteRoot, lang, editorInstallFlags(argv.slice(1)));
458
+ if (status) process.exit(status);
459
+ }
460
+
431
461
  function packagedSkills() {
432
462
  const root = path.join(gardenPackageRoot, ".agents", "skills");
433
463
  if (!fs.existsSync(root)) return [];
@@ -473,6 +503,7 @@ function help() {
473
503
  garden check <文章.md> 检查一篇博文的硬性结构
474
504
  garden ui <子命令> Intelligent UI:scan、check、apply、prompt、schema、catalog
475
505
  garden hooks 安装提交前检查(检查暂存稿件里的 ui 块)
506
+ garden editors install 安装 Intelligent UI 插件到 VS Code / Cursor 与 Typora(--vscode、--typora、--yes)
476
507
  garden skill 把引擎自带的 skill 装进当前站点
477
508
  garden pages:validate 检查 dist/client
478
509
  garden pages:prepare 写入公开产物仓库
@@ -480,85 +511,92 @@ function help() {
480
511
  `);
481
512
  }
482
513
 
483
- switch (command) {
484
- case "help":
485
- case "--help":
486
- case "-h":
487
- help();
488
- break;
489
- case "sync":
490
- sync();
491
- break;
492
- case "dev":
493
- ensureDevGitHubLogin();
494
- runVinext(["dev", ...rest]);
495
- break;
496
- case "build":
497
- // Favicons are refetched on every build; failures fall back to link-assets.json.
498
- if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/link-assets.mjs");
499
- // Remote image sizes: only new images are probed; the rest come from image-sizes.json.
500
- if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/image-sizes.mjs");
501
- // Sizes of remote files in garden file blocks; failures fall back to file-sizes.json.
502
- if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/file-sizes.mjs");
503
- runVinext(["build", ...rest], { copyDist: true });
504
- // /favicon.ico from the site's own icon (brand.favicon).
505
- runNode("scripts/favicon-ico.mjs");
506
- // Pages for old addresses listed under redirects in site.config.yaml (static export only).
507
- if (process.env.STATIC_EXPORT === "1") {
508
- // What the export wrote for empty dynamic routes (ui/garden/static-params.mjs).
509
- runNode("scripts/static-placeholders.mjs");
510
- runNode("scripts/redirects.mjs");
511
- // <html lang="en"> on the English handbook pages.
512
- runNode("scripts/html-lang.mjs");
513
- // 404.html also under basePath when the host serves that folder alone.
514
- runNode("scripts/static-404.mjs");
515
- }
516
- break;
517
- case "links":
518
- runNode("scripts/link-assets.mjs");
519
- break;
520
- case "images":
521
- runNode("scripts/image-sizes.mjs");
522
- break;
523
- case "files":
524
- runNode("scripts/file-sizes.mjs");
525
- break;
526
- case "start":
527
- runVinext(["start", ...rest]);
528
- break;
529
- case "new":
530
- runNode("scripts/new-post.mjs", rest);
531
- break;
532
- case "embed":
533
- runNode("scripts/fetch-embed.mjs", rest);
534
- break;
535
- case "check":
536
- runNode("scripts/check-post.mjs", rest);
537
- break;
538
- case "ui":
539
- runNode("scripts/ui-cli.mjs", rest, { cwd: process.cwd() });
540
- break;
541
- case "hooks":
542
- installHook(siteRoot, { force: rest.includes("--force") });
543
- break;
544
- case "init":
545
- initSite();
546
- break;
547
- case "skill":
548
- case "skills":
549
- installSkill();
550
- break;
551
- case "pages:validate":
552
- runNode("scripts/prepare-pages-output.mjs", [
553
- rest[0] ?? path.join(siteRoot, "dist", "client"),
554
- ]);
555
- break;
556
- case "pages:prepare":
557
- runNode("scripts/prepare-pages-output.mjs", rest);
558
- break;
559
- case "webmentions":
560
- runNode("scripts/send-webmentions.mjs", rest);
561
- break;
562
- default:
563
- fail(`未知命令:${command}`);
514
+ async function main() {
515
+ switch (command) {
516
+ case "help":
517
+ case "--help":
518
+ case "-h":
519
+ help();
520
+ break;
521
+ case "sync":
522
+ await sync();
523
+ break;
524
+ case "dev":
525
+ ensureDevGitHubLogin();
526
+ await runVinext(["dev", ...rest]);
527
+ break;
528
+ case "build":
529
+ // Favicons are refetched on every build; failures fall back to link-assets.json.
530
+ if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/link-assets.mjs");
531
+ // Remote image sizes: only new images are probed; the rest come from image-sizes.json.
532
+ if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/image-sizes.mjs");
533
+ // Sizes of remote files in garden file blocks; failures fall back to file-sizes.json.
534
+ if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/file-sizes.mjs");
535
+ await runVinext(["build", ...rest], { copyDist: true });
536
+ // /favicon.ico from the site's own icon (brand.favicon).
537
+ runNode("scripts/favicon-ico.mjs");
538
+ // Pages for old addresses listed under redirects in site.config.yaml (static export only).
539
+ if (process.env.STATIC_EXPORT === "1") {
540
+ // What the export wrote for empty dynamic routes (ui/garden/static-params.mjs).
541
+ runNode("scripts/static-placeholders.mjs");
542
+ runNode("scripts/redirects.mjs");
543
+ // <html lang="en"> on the English handbook pages.
544
+ runNode("scripts/html-lang.mjs");
545
+ // 404.html also under basePath when the host serves that folder alone.
546
+ runNode("scripts/static-404.mjs");
547
+ }
548
+ break;
549
+ case "links":
550
+ runNode("scripts/link-assets.mjs");
551
+ break;
552
+ case "images":
553
+ runNode("scripts/image-sizes.mjs");
554
+ break;
555
+ case "files":
556
+ runNode("scripts/file-sizes.mjs");
557
+ break;
558
+ case "start":
559
+ await runVinext(["start", ...rest]);
560
+ break;
561
+ case "new":
562
+ runNode("scripts/new-post.mjs", rest);
563
+ break;
564
+ case "embed":
565
+ runNode("scripts/fetch-embed.mjs", rest);
566
+ break;
567
+ case "check":
568
+ runNode("scripts/check-post.mjs", rest);
569
+ break;
570
+ case "ui":
571
+ runNode("scripts/ui-cli.mjs", rest, { cwd: process.cwd() });
572
+ break;
573
+ case "hooks":
574
+ installHook(siteRoot, { force: rest.includes("--force") });
575
+ break;
576
+ case "editors":
577
+ await editorsCommand(rest);
578
+ break;
579
+ case "init":
580
+ await initSite();
581
+ break;
582
+ case "skill":
583
+ case "skills":
584
+ installSkill();
585
+ break;
586
+ case "pages:validate":
587
+ runNode("scripts/prepare-pages-output.mjs", [
588
+ rest[0] ?? path.join(siteRoot, "dist", "client"),
589
+ ]);
590
+ break;
591
+ case "pages:prepare":
592
+ runNode("scripts/prepare-pages-output.mjs", rest);
593
+ break;
594
+ case "webmentions":
595
+ runNode("scripts/send-webmentions.mjs", rest);
596
+ break;
597
+ default:
598
+ fail(`未知命令:${command}`);
599
+ }
564
600
  }
601
+
602
+ await main();