create-zudo-doc 4.1.0 → 4.2.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/dist/api.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { PresetHeaderRightItem, PresetMetaTagsConfig } from "./preset.js";
1
2
  export type { UserChoices } from "./prompts.js";
2
3
  export interface CreateOptions {
3
4
  projectName: string;
@@ -15,7 +16,17 @@ export interface CreateOptions {
15
16
  /** GitHub repository URL — drives the header GitHub link and body-foot
16
17
  * "View source on GitHub" link. Empty = disabled. */
17
18
  githubUrl?: string;
19
+ /** Enable remark-cjk-friendly plugin (intelligent spacing around CJK text). Default: false. */
20
+ cjkFriendly?: boolean;
21
+ /** Minify production HTML output. Default: true. */
22
+ minifyHtml?: boolean;
18
23
  packageManager: "pnpm" | "npm" | "yarn" | "bun";
24
+ /** Header-right items override, validated against the v1 preset allowlist
25
+ * (see `validateHeaderRightItems`). When omitted, keeps the package default. */
26
+ headerRightItems?: PresetHeaderRightItem[];
27
+ /** Meta tags config, validated against the v1 preset shape
28
+ * (see `validateMetaTags`). When omitted, keeps the package defaults. */
29
+ metaTags?: PresetMetaTagsConfig;
19
30
  /** Install dependencies after scaffolding (default: false) */
20
31
  install?: boolean;
21
32
  /**
package/dist/api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import { SINGLE_SCHEMES, THEME_PACKS } from "./constants.js";
3
+ import { validateHeaderRightItems, validateMetaTags } from "./preset.js";
3
4
  import { scaffold } from "./scaffold.js";
4
5
  import { initGitRepo, installDependencies, validateProjectName } from "./utils.js";
5
6
  export async function createZudoDoc(options) {
@@ -30,6 +31,20 @@ export async function createZudoDoc(options) {
30
31
  const catalog = THEME_PACKS.map((t) => t.slug).join(", ");
31
32
  throw new Error(`Unknown theme pack "${rest.themePack}". Available: ${catalog}`);
32
33
  }
34
+ // Validate headerRightItems/metaTags shape like the preset (preset.ts) path
35
+ // does (#2922) — the two validators are shared via preset.ts so this can
36
+ // never drift from `validatePreset()`'s rules. Booleans (cjkFriendly,
37
+ // minifyHtml) need no validation.
38
+ if (rest.headerRightItems !== undefined) {
39
+ const err = validateHeaderRightItems(rest.headerRightItems);
40
+ if (err)
41
+ throw new Error(err);
42
+ }
43
+ if (rest.metaTags !== undefined) {
44
+ const err = validateMetaTags(rest.metaTags);
45
+ if (err)
46
+ throw new Error(err);
47
+ }
33
48
  const choices = { ...rest, defaultLang: rest.defaultLang ?? "en" };
34
49
  await scaffold(choices);
35
50
  const targetDir = path.resolve(process.cwd(), choices.projectName);
@@ -4,7 +4,7 @@ import type { UserChoices } from "./prompts.js";
4
4
  * zudolab/zudo-doc#2651, Wave 6 #2660). Rewritten from scratch — the old
5
5
  * generator described a 64-file project (`src/components/admonitions/`,
6
6
  * `src/layouts/`, `src/utils/`, per-page `pages/lib/*` wiring) that no
7
- * longer exists. The scaffolded project is now ~12 files; almost
7
+ * longer exists. The scaffolded project is now ~13 files; almost
8
8
  * everything referenced here lives in `node_modules/@takazudo/zudo-doc`.
9
9
  */
10
10
  export declare function generateCLAUDEFile(choices: UserChoices): string;
@@ -4,7 +4,7 @@ import { capitalize, pmRunCommand } from "./utils.js";
4
4
  * zudolab/zudo-doc#2651, Wave 6 #2660). Rewritten from scratch — the old
5
5
  * generator described a 64-file project (`src/components/admonitions/`,
6
6
  * `src/layouts/`, `src/utils/`, per-page `pages/lib/*` wiring) that no
7
- * longer exists. The scaffolded project is now ~12 files; almost
7
+ * longer exists. The scaffolded project is now ~13 files; almost
8
8
  * everything referenced here lives in `node_modules/@takazudo/zudo-doc`.
9
9
  */
10
10
  export function generateCLAUDEFile(choices) {
@@ -28,7 +28,14 @@ export function generateCLAUDEFile(choices) {
28
28
  lines.push(`## Commands`);
29
29
  lines.push(``);
30
30
  const pm = choices.packageManager;
31
- lines.push(`- \`${pmRunCommand(pm, "dev")}\` — zfb dev server (port 4321)`);
31
+ if (choices.features.includes("docHistory")) {
32
+ lines.push(`- \`${pmRunCommand(pm, "dev")}\` — runs the zfb dev server (port 4321) and the doc-history API server (port 4322) concurrently via \`run-p\` (\`${pmRunCommand(pm, "dev:zfb")}\` / \`${pmRunCommand(pm, "dev:history")}\` individually)`);
33
+ lines.push(`- \`${pmRunCommand(pm, "dev:network")}\` — same, but zfb binds \`--host 0.0.0.0\` for LAN access (\`${pmRunCommand(pm, "dev:zfb:network")}\` individually); the doc-history server stays loopback-only and LAN clients reach it through zfb's \`/doc-history/*\` dev proxy`);
34
+ lines.push(`- \`run-p\` swallows trailing args, so other zfb flags don't forward through \`${pmRunCommand(pm, "dev")}\` — pass them directly instead: \`${pm} run dev:zfb -- <flags>\``);
35
+ }
36
+ else {
37
+ lines.push(`- \`${pmRunCommand(pm, "dev")}\` — zfb dev server (port 4321)`);
38
+ }
32
39
  lines.push(`- \`${pmRunCommand(pm, "build")}\` — static HTML export to \`dist/\``);
33
40
  lines.push(`- \`${pmRunCommand(pm, "check")}\` — TypeScript type checking`);
34
41
  lines.push(`- \`${pmRunCommand(pm, "preview")}\` — serve the built \`dist/\``);
package/dist/preset.d.ts CHANGED
@@ -26,6 +26,18 @@ export interface PresetMetaTagsConfig {
26
26
  twitterSite?: string;
27
27
  twitterCreator?: string;
28
28
  }
29
+ /**
30
+ * Validates a `headerRightItems` value against the v1 preset allowlist.
31
+ * Shared by `validatePreset()` (JSON preset path) and `createZudoDoc()`
32
+ * (programmatic API path, #2922) so the two entry points can never drift.
33
+ */
34
+ export declare function validateHeaderRightItems(items: unknown): string | null;
35
+ /**
36
+ * Validates a `metaTags` value's sub-field types. Shared by `validatePreset()`
37
+ * (JSON preset path) and `createZudoDoc()` (programmatic API path, #2922) so
38
+ * the two entry points can never drift.
39
+ */
40
+ export declare function validateMetaTags(metaTags: unknown): string | null;
29
41
  export interface PresetJson {
30
42
  projectName?: string;
31
43
  defaultLang?: string;
package/dist/preset.js CHANGED
@@ -12,6 +12,88 @@ const VALID_HEADER_RIGHT_TRIGGERS = new Set([
12
12
  "design-token-panel",
13
13
  "ai-chat",
14
14
  ]);
15
+ /**
16
+ * Validates a `headerRightItems` value against the v1 preset allowlist.
17
+ * Shared by `validatePreset()` (JSON preset path) and `createZudoDoc()`
18
+ * (programmatic API path, #2922) so the two entry points can never drift.
19
+ */
20
+ export function validateHeaderRightItems(items) {
21
+ if (!Array.isArray(items)) {
22
+ return `"headerRightItems" must be an array`;
23
+ }
24
+ for (let i = 0; i < items.length; i++) {
25
+ const item = items[i];
26
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
27
+ return `headerRightItems[${i}] must be an object`;
28
+ }
29
+ const t = item.type;
30
+ if (t === "link" || t === "html") {
31
+ return `headerRightItems[${i}] type "${t}" is not supported in presets (v1) — edit zfb.config.ts after scaffold`;
32
+ }
33
+ if (t === "component") {
34
+ const component = item.component;
35
+ if (typeof component !== "string") {
36
+ return `headerRightItems[${i}].component must be a string`;
37
+ }
38
+ if (!VALID_HEADER_RIGHT_COMPONENTS.has(component)) {
39
+ return `headerRightItems[${i}] unknown component "${component}". Allowed: ${[
40
+ ...VALID_HEADER_RIGHT_COMPONENTS,
41
+ ].join(", ")}`;
42
+ }
43
+ }
44
+ else if (t === "trigger") {
45
+ const trigger = item.trigger;
46
+ if (typeof trigger !== "string") {
47
+ return `headerRightItems[${i}].trigger must be a string`;
48
+ }
49
+ if (!VALID_HEADER_RIGHT_TRIGGERS.has(trigger)) {
50
+ return `headerRightItems[${i}] unknown trigger "${trigger}". Allowed: ${[
51
+ ...VALID_HEADER_RIGHT_TRIGGERS,
52
+ ].join(", ")}`;
53
+ }
54
+ }
55
+ else {
56
+ return `headerRightItems[${i}] must have type "component" or "trigger" (got ${JSON.stringify(t)})`;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Validates a `metaTags` value's sub-field types. Shared by `validatePreset()`
63
+ * (JSON preset path) and `createZudoDoc()` (programmatic API path, #2922) so
64
+ * the two entry points can never drift.
65
+ */
66
+ export function validateMetaTags(metaTags) {
67
+ if (typeof metaTags !== "object" || metaTags === null || Array.isArray(metaTags)) {
68
+ return `"metaTags" must be an object`;
69
+ }
70
+ const mt = metaTags;
71
+ if (mt.description !== undefined && typeof mt.description !== "boolean") {
72
+ return `"metaTags.description" must be a boolean`;
73
+ }
74
+ if (mt.keywords !== undefined && mt.keywords !== false && typeof mt.keywords !== "string") {
75
+ return `"metaTags.keywords" must be a string or false`;
76
+ }
77
+ if (mt.ogImage !== undefined && mt.ogImage !== false && typeof mt.ogImage !== "string") {
78
+ return `"metaTags.ogImage" must be a string or false`;
79
+ }
80
+ if (mt.ogSiteName !== undefined && typeof mt.ogSiteName !== "boolean") {
81
+ return `"metaTags.ogSiteName" must be a boolean`;
82
+ }
83
+ if (mt.twitterCard !== undefined &&
84
+ mt.twitterCard !== false &&
85
+ mt.twitterCard !== "summary" &&
86
+ mt.twitterCard !== "summary_large_image") {
87
+ return `"metaTags.twitterCard" must be "summary", "summary_large_image", or false`;
88
+ }
89
+ if (mt.twitterSite !== undefined && typeof mt.twitterSite !== "string") {
90
+ return `"metaTags.twitterSite" must be a string`;
91
+ }
92
+ if (mt.twitterCreator !== undefined && typeof mt.twitterCreator !== "string") {
93
+ return `"metaTags.twitterCreator" must be a string`;
94
+ }
95
+ return null;
96
+ }
15
97
  export function loadPreset(pathOrStdin) {
16
98
  let raw;
17
99
  if (pathOrStdin === "-") {
@@ -85,74 +167,14 @@ export function validatePreset(json) {
85
167
  return `"minifyHtml" must be a boolean in preset`;
86
168
  }
87
169
  if (p.headerRightItems !== undefined) {
88
- if (!Array.isArray(p.headerRightItems)) {
89
- return `"headerRightItems" must be an array in preset`;
90
- }
91
- for (let i = 0; i < p.headerRightItems.length; i++) {
92
- const item = p.headerRightItems[i];
93
- if (item === null || typeof item !== "object" || Array.isArray(item)) {
94
- return `headerRightItems[${i}] must be an object`;
95
- }
96
- const t = item.type;
97
- if (t === "link" || t === "html") {
98
- return `headerRightItems[${i}] type "${t}" is not supported in presets (v1) — edit settings.ts after scaffold`;
99
- }
100
- if (t === "component") {
101
- const component = item.component;
102
- if (typeof component !== "string") {
103
- return `headerRightItems[${i}].component must be a string`;
104
- }
105
- if (!VALID_HEADER_RIGHT_COMPONENTS.has(component)) {
106
- return `headerRightItems[${i}] unknown component "${component}". Allowed: ${[
107
- ...VALID_HEADER_RIGHT_COMPONENTS,
108
- ].join(", ")}`;
109
- }
110
- }
111
- else if (t === "trigger") {
112
- const trigger = item.trigger;
113
- if (typeof trigger !== "string") {
114
- return `headerRightItems[${i}].trigger must be a string`;
115
- }
116
- if (!VALID_HEADER_RIGHT_TRIGGERS.has(trigger)) {
117
- return `headerRightItems[${i}] unknown trigger "${trigger}". Allowed: ${[
118
- ...VALID_HEADER_RIGHT_TRIGGERS,
119
- ].join(", ")}`;
120
- }
121
- }
122
- else {
123
- return `headerRightItems[${i}] must have type "component" or "trigger" (got ${JSON.stringify(t)})`;
124
- }
125
- }
170
+ const err = validateHeaderRightItems(p.headerRightItems);
171
+ if (err)
172
+ return err;
126
173
  }
127
174
  if (p.metaTags !== undefined) {
128
- if (typeof p.metaTags !== "object" || p.metaTags === null || Array.isArray(p.metaTags)) {
129
- return `"metaTags" must be an object in preset`;
130
- }
131
- const mt = p.metaTags;
132
- if (mt.description !== undefined && typeof mt.description !== "boolean") {
133
- return `"metaTags.description" must be a boolean`;
134
- }
135
- if (mt.keywords !== undefined && mt.keywords !== false && typeof mt.keywords !== "string") {
136
- return `"metaTags.keywords" must be a string or false`;
137
- }
138
- if (mt.ogImage !== undefined && mt.ogImage !== false && typeof mt.ogImage !== "string") {
139
- return `"metaTags.ogImage" must be a string or false`;
140
- }
141
- if (mt.ogSiteName !== undefined && typeof mt.ogSiteName !== "boolean") {
142
- return `"metaTags.ogSiteName" must be a boolean`;
143
- }
144
- if (mt.twitterCard !== undefined &&
145
- mt.twitterCard !== false &&
146
- mt.twitterCard !== "summary" &&
147
- mt.twitterCard !== "summary_large_image") {
148
- return `"metaTags.twitterCard" must be "summary", "summary_large_image", or false`;
149
- }
150
- if (mt.twitterSite !== undefined && typeof mt.twitterSite !== "string") {
151
- return `"metaTags.twitterSite" must be a string`;
152
- }
153
- if (mt.twitterCreator !== undefined && typeof mt.twitterCreator !== "string") {
154
- return `"metaTags.twitterCreator" must be a string`;
155
- }
175
+ const err = validateMetaTags(p.metaTags);
176
+ if (err)
177
+ return err;
156
178
  }
157
179
  // Cross-field validation
158
180
  if (p.colorSchemeMode === "single" && (p.lightScheme || p.darkScheme)) {
@@ -18,5 +18,5 @@ export { getSecondaryLang };
18
18
  *
19
19
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
20
20
  */
21
- export declare const ZUDO_DOC_PIN = "^4.1.0";
21
+ export declare const ZUDO_DOC_PIN = "^4.2.0";
22
22
  export declare function scaffold(choices: UserChoices): Promise<void>;
package/dist/scaffold.js CHANGED
@@ -5,7 +5,7 @@ import { generateZfbConfig } from "./zfb-config-gen.js";
5
5
  import { generateCLAUDEFile } from "./claude-md-gen.js";
6
6
  import { composeFeatures } from "./compose.js";
7
7
  import { featureModules } from "./features/index.js";
8
- import { capitalize, getSecondaryLang, pmRunCommand } from "./utils.js";
8
+ import { capitalize, getSecondaryLang, hasAncestorPnpmWorkspace, pmRunCommand, } from "./utils.js";
9
9
  export { getSecondaryLang };
10
10
  /**
11
11
  * Pinned `@takazudo/zudo-doc` version used by `generatePackageJson()`.
@@ -24,7 +24,7 @@ export { getSecondaryLang };
24
24
  *
25
25
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
26
26
  */
27
- export const ZUDO_DOC_PIN = "^4.1.0";
27
+ export const ZUDO_DOC_PIN = "^4.2.0";
28
28
  /**
29
29
  * Files in `templates/base/**` that must not be copied by the unconditional
30
30
  * base mirror. Each entry is matched against the path relative to
@@ -208,8 +208,6 @@ export async function scaffold(choices) {
208
208
  const templatesDir = path.join(pkgRoot, "templates");
209
209
  const baseDir = path.join(templatesDir, "base");
210
210
  const featuresDir = path.join(templatesDir, "features");
211
- // Still needed for source-checkout-only assets such as Claude skills.
212
- const monorepoRoot = path.resolve(pkgRoot, "../..");
213
211
  await fs.ensureDir(targetDir);
214
212
  // 1. Copy base template
215
213
  // Honour EXCLUDE_FROM_MIRROR so paths like `pages/api/**` (worker-only SSR
@@ -227,19 +225,31 @@ export async function scaffold(choices) {
227
225
  }
228
226
  // 2b. Copy user-facing Claude Code skills when enabled
229
227
  // Ships the curated zudo-doc-* skills (design-system, translate, version-bump)
230
- // from the monorepo's .claude/skills/ into the user's .claude/skills/.
228
+ // from the package's own templates/ (npm `files` cannot reach outside the
229
+ // package dir, so these are committed, scaffold-authored variants of the
230
+ // monorepo's .claude/skills/, not read from there directly and not
231
+ // byte-identical to it — each was rewritten to describe the scaffold-real
232
+ // flow (no monorepo-only paths/scripts) instead of the monorepo's own
233
+ // flow — see #2921 (original copy step) and epic #2946 (variant
234
+ // conversion, #2947/#2948).
231
235
  if (choices.features.includes("claudeSkills")) {
232
236
  const userFacingSkills = [
233
237
  "zudo-doc-design-system",
234
238
  "zudo-doc-translate",
235
239
  "zudo-doc-version-bump",
236
240
  ];
241
+ const skillsTemplateDir = path.join(featuresDir, "claudeSkills/files/.claude/skills");
237
242
  for (const skill of userFacingSkills) {
238
- const skillSrc = path.join(monorepoRoot, ".claude/skills", skill);
243
+ const skillSrc = path.join(skillsTemplateDir, skill);
239
244
  const skillDest = path.join(targetDir, ".claude/skills", skill);
240
245
  if (await fs.pathExists(skillSrc)) {
241
246
  await fs.copy(skillSrc, skillDest);
242
247
  }
248
+ else {
249
+ // Defensive only — unreachable in a healthy publish, since the
250
+ // template files are committed alongside this source.
251
+ console.warn(`claudeSkills: missing template source for "${skill}", skipping`);
252
+ }
243
253
  }
244
254
  }
245
255
  const defaultLang = choices.defaultLang;
@@ -305,6 +315,7 @@ export async function scaffold(choices) {
305
315
  "node_modules",
306
316
  "dist",
307
317
  ".zfb",
318
+ ".zfb-build/",
308
319
  "",
309
320
  "# macOS",
310
321
  ".DS_Store",
@@ -356,6 +367,33 @@ export async function scaffold(choices) {
356
367
  // under a strict trust policy without disabling the guard wholesale. Pinned to
357
368
  // the exact known-safe version so a future undici-types bump is re-reviewed.
358
369
  await fs.outputFile(path.join(targetDir, ".npmrc"), "trust-policy-exclude[]=undici-types@6.21.0\n");
370
+ // Emit a pnpm-workspace.yaml disabling pnpm 11's minimumReleaseAge gate.
371
+ // pnpm >= 11 defaults minimumReleaseAge to 1440 (1 day), which blocks
372
+ // `pnpm install`/CI from resolving a freshly-published @takazudo bump for
373
+ // a full day; the built-in minimumReleaseAgeExclude matcher can't be
374
+ // pointed at this project's peer-nested lockfile keys (upstream pnpm
375
+ // limitation), so the gate is disabled outright rather than excluded
376
+ // per-package. As of pnpm 11, non-auth/registry settings like this one
377
+ // live in pnpm-workspace.yaml, not .npmrc (.npmrc is auth/registry only).
378
+ // Skipped when an ANCESTOR directory already has a pnpm-workspace.yaml
379
+ // (e.g. scaffolding into `apps/docs/` under an existing pnpm monorepo) —
380
+ // pnpm resolves the nearest pnpm-workspace.yaml upward from cwd as the
381
+ // workspace root, so writing a new one here would carve the generated
382
+ // project out of the parent workspace instead of joining it. Mirrors
383
+ // `initGitRepo`'s "never nest" precedent (see utils.ts).
384
+ if (!hasAncestorPnpmWorkspace(targetDir)) {
385
+ await fs.outputFile(path.join(targetDir, "pnpm-workspace.yaml"), "# pnpm 11 defaults minimumReleaseAge to 1440min; its exclude matcher can't match this project's peer-nested lockfile keys (upstream pnpm bug), so disable the gate outright.\nminimumReleaseAge: 0\n");
386
+ }
387
+ else {
388
+ // Ancestor already has a pnpm-workspace.yaml: we deliberately do NOT write
389
+ // our own (it would carve this project out of the parent workspace — see
390
+ // above). But then pnpm applies the PARENT workspace's minimumReleaseAge
391
+ // (1440min by default in pnpm 11), so freshly-published @takazudo bumps
392
+ // still can't install for a day — the exact failure this file guards
393
+ // against. We can't safely edit the parent's config, so instruct the user
394
+ // to disable the gate there instead of silently leaving them blocked.
395
+ console.warn("pnpm-workspace.yaml exists in an ancestor directory — skipping the generated one to avoid nesting a second workspace. If `pnpm install` blocks freshly-published @takazudo releases, add `minimumReleaseAge: 0` to your parent pnpm-workspace.yaml.");
396
+ }
359
397
  const claudeContent = generateCLAUDEFile(choices);
360
398
  await fs.outputFile(path.join(targetDir, "CLAUDE.md"), claudeContent);
361
399
  // 4. Compose features (copy feature files + inject into shared files)
@@ -622,10 +660,16 @@ function generatePackageJson(choices) {
622
660
  // doc-history helpers, which in turn import
623
661
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
624
662
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
625
- deps["@takazudo/zudo-doc-history-server"] = "^4.1.0";
663
+ deps["@takazudo/zudo-doc-history-server"] = "^4.2.0";
626
664
  // tsx is no longer needed here: the relocated package plugin imports the
627
665
  // runner directly (no `tsx -e` spawn) since the package ships compiled
628
666
  // dist/ — package-first migration #2321 (#2337).
667
+ // npm-run-all2 provides `run-p`, used by the docHistory `dev` script
668
+ // (below) to run the zfb dev server and the doc-history API server
669
+ // concurrently — otherwise the :4322 proxy target never starts and the
670
+ // feature silently looks broken (#2926). Same maintained fork/pin this
671
+ // monorepo's own root package.json uses.
672
+ devDeps["npm-run-all2"] = "^7.0.2";
629
673
  }
630
674
  // claudeResources: tsx is no longer needed. The relocated package plugin
631
675
  // (@takazudo/zudo-doc/plugins/claude-resources) imports the runner directly
@@ -644,6 +688,38 @@ function generatePackageJson(choices) {
644
688
  preview: "zfb preview",
645
689
  check: "zfb check",
646
690
  };
691
+ if (choices.features.includes("docHistory")) {
692
+ // A docHistory-enabled project needs the zfb dev server AND the
693
+ // doc-history API server (:4322, proxied by the zfb doc-history plugin)
694
+ // running concurrently — otherwise the Created/Updated/Author block
695
+ // silently never appears in dev (#2926). `doc-history-server` is the bin
696
+ // shipped by the @takazudo/zudo-doc-history-server dep added above;
697
+ // `run-p` (npm-run-all2, added to devDependencies above) runs both.
698
+ scripts.dev = "run-p dev:zfb dev:history";
699
+ scripts["dev:zfb"] = "zfb dev";
700
+ // run-p swallows trailing args and npm-run-all2 v7's `{@}` placeholder
701
+ // strips flag names, so `pnpm dev -- --host 0.0.0.0` is silently ignored
702
+ // (verified in issue #2940) — dev:network is a dedicated LAN-bound script
703
+ // instead. Only zfb binds 0.0.0.0; the history server stays loopback-only
704
+ // and LAN clients reach it through zfb's `/doc-history/*` dev proxy.
705
+ scripts["dev:zfb:network"] = "zfb dev --host 0.0.0.0";
706
+ scripts["dev:network"] = "run-p dev:zfb:network dev:history";
707
+ // Relative --content-dir/--locale paths are resolved by resolveContentPath
708
+ // (packages/doc-history-server/src/args.ts) against INIT_CWD (falling back
709
+ // to process.cwd()) — correct for the supported invocation (`<pm> dev` /
710
+ // `<pm> run dev` from the project root, which is what run-p's child
711
+ // processes inherit). It resolves against the WRONG directory only if this
712
+ // generated project is itself nested inside a larger pnpm/npm workspace
713
+ // and dev:history is invoked via `<pm> --filter <this-package> ...` from
714
+ // that outer workspace root — an unsupported, non-generator invocation
715
+ // path, not the default `<pm> dev`.
716
+ let devHistoryScript = "doc-history-server --port 4322 --content-dir src/content/docs";
717
+ if (choices.features.includes("i18n")) {
718
+ const secondaryLang = getSecondaryLang(choices.defaultLang);
719
+ devHistoryScript += ` --locale ${secondaryLang}:src/content/docs-${secondaryLang}`;
720
+ }
721
+ scripts["dev:history"] = devHistoryScript;
722
+ }
647
723
  if (choices.features.includes("tagGovernance")) {
648
724
  // Both package-owned bins load the same explicit project config. `--`
649
725
  // supplied by pnpm/npm is preserved by the runners for forwarded options.
package/dist/utils.d.ts CHANGED
@@ -34,6 +34,18 @@ export type GitInitResult = {
34
34
  * none configured, so a normal user's commit keeps their own identity.
35
35
  */
36
36
  export declare function initGitRepo(dir: string): GitInitResult;
37
+ /**
38
+ * Whether an ANCESTOR of `dir` (walking up from `dir`'s parent, not `dir`
39
+ * itself — `dir` is the fresh project root and never has one yet) already
40
+ * has a `pnpm-workspace.yaml`. Mirrors `initGitRepo`'s "never nest"
41
+ * precedent above: pnpm resolves the nearest `pnpm-workspace.yaml` upward
42
+ * from cwd as the workspace root, so writing a new one inside an existing
43
+ * pnpm monorepo (e.g. scaffolding into `apps/docs/` under a parent
44
+ * workspace) would carve the generated project out of that parent
45
+ * workspace's install/lockfile instead of joining it (codex-review finding,
46
+ * #2923).
47
+ */
48
+ export declare function hasAncestorPnpmWorkspace(dir: string): boolean;
37
49
  export declare function capitalize(str: string): string;
38
50
  /** Get a short uppercase label for a language code (e.g. "en" → "EN", "zh-cn" → "ZH-CN"). */
39
51
  export declare function getLangLabel(langCode: string): string;
package/dist/utils.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execSync, execFileSync } from "child_process";
2
2
  import fs from "fs-extra";
3
+ import path from "path";
3
4
  // Project-name grammar (locked by F4 — S4 #2013):
4
5
  // /^[a-z0-9][a-z0-9._-]*$/, max 214 chars, unscoped, used as both directory
5
6
  // name and package name. Mirrors npm's unscoped-name rules + max path safety.
@@ -107,6 +108,28 @@ export function initGitRepo(dir) {
107
108
  };
108
109
  }
109
110
  }
111
+ /**
112
+ * Whether an ANCESTOR of `dir` (walking up from `dir`'s parent, not `dir`
113
+ * itself — `dir` is the fresh project root and never has one yet) already
114
+ * has a `pnpm-workspace.yaml`. Mirrors `initGitRepo`'s "never nest"
115
+ * precedent above: pnpm resolves the nearest `pnpm-workspace.yaml` upward
116
+ * from cwd as the workspace root, so writing a new one inside an existing
117
+ * pnpm monorepo (e.g. scaffolding into `apps/docs/` under a parent
118
+ * workspace) would carve the generated project out of that parent
119
+ * workspace's install/lockfile instead of joining it (codex-review finding,
120
+ * #2923).
121
+ */
122
+ export function hasAncestorPnpmWorkspace(dir) {
123
+ let current = path.dirname(path.resolve(dir));
124
+ while (true) {
125
+ if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")))
126
+ return true;
127
+ const parent = path.dirname(current);
128
+ if (parent === current)
129
+ return false; // reached the filesystem root
130
+ current = parent;
131
+ }
132
+ }
110
133
  export function capitalize(str) {
111
134
  return str.replace(/\b\w/g, (c) => c.toUpperCase());
112
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -74,6 +74,16 @@ fi
74
74
  # Use the main worktree path so symlinks survive worktree removal
75
75
  REPO_ROOT="$(git -C "$ROOT_DIR" worktree list | head -1 | awk '{print $1}')"
76
76
 
77
+ # Path from the repository root to the project directory: "" at the repo
78
+ # root, "doc/" (trailing slash) when the project lives in a subdirectory
79
+ # (nested layout, #2918). `rev-parse --show-prefix` is relative to the
80
+ # CURRENT worktree's top level, so combining it with REPO_ROOT (the MAIN
81
+ # worktree root, above) reconstructs the equivalent path there even when
82
+ # running from inside a different worktree.
83
+ PROJECT_PREFIX="$(git -C "$ROOT_DIR" rev-parse --show-prefix)"
84
+ REPO_DOCS_DIR="$REPO_ROOT/${PROJECT_PREFIX}src/content/docs"
85
+ REPO_DOCS_JA_DIR="$REPO_ROOT/${PROJECT_PREFIX}src/content/docs-ja"
86
+
77
87
  DOCS_DIR="$ROOT_DIR/src/content/docs"
78
88
 
79
89
  # Validate docs directory exists
@@ -107,6 +117,33 @@ for dir in "$DOCS_DIR"/*/; do
107
117
  "
108
118
  done
109
119
 
120
+ # The SKILL.md "Format"/"Verify" steps must only reference commands that
121
+ # actually exist on the running project's package.json — a fresh
122
+ # create-zudo-doc scaffold has no format script at all, while this repo's own
123
+ # showcase exposes `format` (there is no `format:md` script anywhere, #2918).
124
+ # `format:md` is checked too in case a downstream project defines its own
125
+ # script under that literal name.
126
+ FORMAT_SCRIPT="$(node -e "
127
+ const scripts = (require('$ROOT_DIR/package.json').scripts) || {};
128
+ console.log(
129
+ scripts.format ? 'format'
130
+ : scripts['format:mdx'] ? 'format:mdx'
131
+ : scripts['format:md'] ? 'format:md'
132
+ : ''
133
+ );
134
+ ")"
135
+ if [ -n "$FORMAT_SCRIPT" ]; then
136
+ FORMAT_STEP="Run \`pnpm $FORMAT_SCRIPT\` to format the new/changed MDX files."
137
+ else
138
+ FORMAT_STEP="No format script is configured for this project; formatting is optional."
139
+ fi
140
+
141
+ # Name the project directory explicitly so the instruction resolves correctly
142
+ # even when the project is nested inside a larger git repo (running `pnpm
143
+ # build` from the outer repo root would otherwise invoke the wrong
144
+ # package.json, #2918).
145
+ VERIFY_STEP="Run \`pnpm build\` from \`$ROOT_DIR\` to confirm the site builds correctly."
146
+
110
147
  resolve_targets() {
111
148
  case "$TARGET_MODE" in
112
149
  claude) echo "claude" ;;
@@ -145,12 +182,12 @@ generate_skill() {
145
182
 
146
183
  mkdir -p "$skill_dir"
147
184
 
148
- ensure_symlink "$skill_dir/docs" "$REPO_ROOT/src/content/docs"
149
- echo " [$target] Created docs symlink -> $REPO_ROOT/src/content/docs"
185
+ ensure_symlink "$skill_dir/docs" "$REPO_DOCS_DIR"
186
+ echo " [$target] Created docs symlink -> $REPO_DOCS_DIR"
150
187
 
151
188
  if [ "$HAS_JA" = "true" ]; then
152
- ensure_symlink "$skill_dir/docs-ja" "$REPO_ROOT/src/content/docs-ja"
153
- echo " [$target] Created docs-ja symlink -> $REPO_ROOT/src/content/docs-ja"
189
+ ensure_symlink "$skill_dir/docs-ja" "$REPO_DOCS_JA_DIR"
190
+ echo " [$target] Created docs-ja symlink -> $REPO_DOCS_JA_DIR"
154
191
  fi
155
192
 
156
193
  cat > "$skill_dir/SKILL.md" << SKILLEOF
@@ -167,7 +204,7 @@ argument-hint: "[-u|--update] [topic keyword, e.g., 'configuration', 'sidebar',
167
204
  # $PROJECT_NAME Documentation Reference
168
205
 
169
206
  Look up documentation from the $PROJECT_NAME project for $assistant_label.
170
- Documentation base path: \`src/content/docs\` (relative to repo root)
207
+ Documentation base path: \`src/content/docs\` (relative to the project root: \`$ROOT_DIR\`)
171
208
 
172
209
  ## Mode Detection
173
210
 
@@ -197,7 +234,7 @@ The user has new information and wants to add or update documentation in this re
197
234
  the topic. Read them to understand what is already covered.
198
235
  3. **Decide create vs update**: If an existing article covers the topic, update
199
236
  it. Otherwise, create a new \`.mdx\` file in the appropriate subdirectory.
200
- 4. **Write the content**: Follow the doc-authoring rules in the root CLAUDE.md:
237
+ 4. **Write the content**: Follow the doc-authoring rules in this project's CLAUDE.md (\`$ROOT_DIR/CLAUDE.md\`):
201
238
  - Required frontmatter: \`title\` (string). Always set \`sidebar_position\`.
202
239
  Optional: \`description\`, \`sidebar_label\`, \`tags\`, etc.
203
240
  - Do NOT use \`# h1\` in content — the frontmatter \`title\` renders as h1.
@@ -210,8 +247,8 @@ The user has new information and wants to add or update documentation in this re
210
247
  \`docs-ja/\` mirroring the English directory structure. Keep code blocks,
211
248
  Mermaid diagrams, and \`<HtmlPreview>\` blocks identical — only translate
212
249
  surrounding prose. Exception: pages with \`generated: true\` skip translation.
213
- 6. **Format**: Run \`pnpm format:md\` to format the new/changed MDX files.
214
- 7. **Verify**: Run \`pnpm build\` to confirm the site builds correctly.
250
+ 6. **Format**: ${FORMAT_STEP}
251
+ 7. **Verify**: ${VERIFY_STEP}
215
252
 
216
253
  ## Documentation Structure
217
254
 
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: zudo-doc-design-system
3
+ description: "Project-specific CSS and component rules for zudo-doc. Must be consulted before writing or editing CSS, Tailwind classes, color tokens, or component markup in this project. Covers: component-first strategy, design token system, three-tier color architecture, and palette index convention. Triggered by 'design system', 'zudo-doc-design-system', 'zudo-doc-css-wisdom' (old name)."
4
+ user-invocable: true
5
+ argument-hint: "[topic: tokens, colors, component-first, palette]"
6
+ ---
7
+
8
+ # zudo-doc CSS & Component Rules
9
+
10
+ **IMPORTANT**: These rules are mandatory for all code changes in this project that touch CSS, Tailwind classes, color tokens, or component markup. Read the relevant section before making changes.
11
+
12
+ ## How to Use
13
+
14
+ Based on the topic, read the specific reference doc. These pages aren't
15
+ scaffolded into a fresh project — they're the published zudo-doc showcase
16
+ docs, kept up to date at the source:
17
+
18
+ | Topic | Where to look |
19
+ |-------|------|
20
+ | Spacing, typography, layout tokens | https://zudo-doc.takazudomodular.com/docs/reference/design-system |
21
+ | Component-first methodology | https://zudo-doc.takazudomodular.com/docs/reference/component-first |
22
+ | Color tokens, palette, schemes | https://zudo-doc.takazudomodular.com/docs/reference/color |
23
+
24
+ For the actual token values in THIS project, the source of truth is local:
25
+ `src/styles/global.css` (the `@theme` override block) plus the shipped
26
+ `@takazudo/zudo-doc/theme.css` and `@takazudo/zudo-doc/content.css` (imported
27
+ by `global.css` — read them via `node_modules/@takazudo/zudo-doc/dist/` for
28
+ the resolved token names/values).
29
+
30
+ Read ONLY the section relevant to your task. Apply its rules strictly.
31
+
32
+ ## Quick Rules (always apply)
33
+
34
+ ### Component First (no custom CSS classes)
35
+
36
+ - **NEVER** create CSS module files, custom class names, or separate stylesheets
37
+ - **ALWAYS** use Tailwind utility classes directly in component markup
38
+ - The component itself is the abstraction — `.card`, `.btn-primary` are forbidden
39
+ - Use props for variants, not CSS modifiers
40
+
41
+ ### Design Tokens (no arbitrary values)
42
+
43
+ - **NEVER** use Tailwind default colors (`bg-gray-500`, `text-blue-600`) — they are reset to `initial`
44
+ - **NEVER** use arbitrary values (`text-[0.875rem]`, `p-[1.2rem]`) when a token exists
45
+ - **ALWAYS** use project tokens: `text-fg`, `bg-surface`, `border-muted`, `p-hsp-md`, `text-small`
46
+ - Spacing: `hsp-*` (horizontal), `vsp-*` (vertical) — see the design-system reference doc above for the full list
47
+ - Typography: `text-caption`, `text-small`, `text-body`, `text-heading` etc.
48
+
49
+ ### Color Tokens (three-tier system)
50
+
51
+ - **Tier 1** (ramps): shared `base` (5 stops), `accent` (3 stops), and `state` (`danger`/`success`/`warning`/`info`) OKLCH ramps — no Tailwind utility reaches these directly (no `p0`–`p15`-style classes); they only feed Tier 2
52
+ - **Tier 2** (semantic): `text-fg`, `bg-surface`, `border-muted`, `text-accent` — the only Tailwind-facing color tokens; prefer these always
53
+ - **NEVER** use hardcoded hex values in components
54
+ - Both bundled schemes (`Default Light`, `Default Dark`) share the same ramps; only their per-mode wiring (`map`) differs. This project doesn't own a copy of the ramp/map definitions — they're package-owned, shipped compiled under `node_modules/@takazudo/zudo-doc/dist/color-schemes-defaults/`. Only override the `@theme` tokens you actually need to change, in `src/styles/global.css`
55
+
56
+ ### Search & highlight tokens (role-split)
57
+
58
+ Highlight roles are deliberately split across dedicated semantic tokens — do **not** share one token across unrelated highlight UIs.
59
+
60
+ - `matched-keyword-bg` / `matched-keyword-fg` — background and foreground of the search panel `<mark>` element. Driven by `--color-matched-keyword-bg` / `--color-matched-keyword-fg`; live-editable in the Design Token Panel, if that feature is enabled. This is the single source of truth for "why is this color yellow in the search results" — the panel swatch matches the rendered highlight 1:1.
61
+ - `warning` — drives admonitions (`:::warning`), find-in-page (`.find-match`, `.find-match-active`), and any UI that is semantically a warning. Do **not** reuse it for new UI-chrome highlights.
62
+
63
+ **Rule**: when a new highlight role appears (new kind of mark, new pill, new callout), add a dedicated semantic token rather than bolting it onto `--color-warning` or another existing token. Each visible highlight color should map to exactly one panel swatch.
64
+
65
+ ### Hover-state underline for link-like elements
66
+
67
+ Any element that navigates (rendered as `<a href>` or behaves as a link) MUST have `hover:underline focus-visible:underline`. Keyboard users need the same affordance as mouse users — never add `hover:underline` without the `focus-visible:underline` pair.
68
+
69
+ - **Links (do underline)**: doc content links, sidebar items, header main-nav, header overflow menu items, color-tweak panel unselected tabs, search result rows, footer links, doc history entries, breadcrumb trails, mobile TOC entries.
70
+ - **Controls (do NOT underline)**: buttons, toggles, sidebar resizer, palette selectors, color swatches, close icons. These use border/bg hover instead.
71
+
72
+ Precedent to copy the pattern from: every package-owned interactive
73
+ component already follows it (sidebar tree nav, header nav, breadcrumb
74
+ trail, and more, all shipped from `@takazudo/zudo-doc`) — see the live
75
+ showcase at https://zudo-doc.takazudomodular.com for a working reference.
76
+ Match the same hover/focus pairing in any new component you add under
77
+ `src/components/`, or in a package component you swizzle out via
78
+ `zudo-doc eject <component>`.
79
+
80
+ ### Light-mode / dark-mode contrast
81
+
82
+ - Every color token must resolve to a legible foreground/background pairing
83
+ in BOTH bundled schemes (`Default Light`, `Default Dark`) — never
84
+ hardcode a value that only works in one mode.
85
+ - Prefer the Tier 2 semantic tokens (`text-fg`, `bg-surface`, `border-muted`,
86
+ ...) over any raw color — they're the only values guaranteed to be
87
+ re-mapped per scheme.
88
+ - When adding a new token, define it for both scheme `map`s (or as a
89
+ scheme-agnostic ramp value) so light/dark parity is never accidental.
90
+
91
+ ### Server-rendered Preact vs client islands
92
+
93
+ - All components in this project are Preact `.tsx` — there are no `.astro`
94
+ files.
95
+ - Default to **server-rendered Preact `.tsx`** (no `client:*` directive) —
96
+ emits zero JS.
97
+ - Promote to a **client island** only when interactivity is needed
98
+ - Both follow the same utility-class approach
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: zudo-doc-translate
3
+ description: Translate zudo-doc documentation between English and Japanese with project-specific conventions.
4
+ triggers:
5
+ - translate
6
+ - 翻訳
7
+ - i18n
8
+ - en to ja
9
+ - ja to en
10
+ - translate docs
11
+ - ドキュメント翻訳
12
+ ---
13
+
14
+ # zudo-doc Translation Skill
15
+
16
+ Translate documentation between English and Japanese following project-specific conventions.
17
+
18
+ ## i18n Structure
19
+
20
+ This skill only applies when this project's i18n feature is enabled. A
21
+ non-i18n scaffold has no secondary-locale content directory (e.g. no
22
+ `docs-ja/`) and no locale-prefixed routes — there's nothing to translate.
23
+ Check `zfb.config.ts`'s `zudoDoc({...})` call: if it sets a non-empty
24
+ `locales` field, i18n is on.
25
+
26
+ - Default-locale docs: `src/content/docs/` — routes at `/docs/...` (the
27
+ default locale is never prefixed)
28
+ - Secondary-locale docs: `src/content/docs-<locale>/` — routes at
29
+ `/<locale>/docs/...`. For the common preset (English default, Japanese
30
+ secondary) this is `src/content/docs-ja/` at `/ja/docs/...`; the rest of
31
+ this skill assumes that pairing, but confirm the actual locale code and
32
+ directory from `zfb.config.ts` if this project uses a different one
33
+ - Directory structures must mirror each other exactly (same filenames, same folder hierarchy)
34
+ - Locale configuration lives in this project's `zfb.config.ts`, inside the
35
+ `zudoDoc({...})` call: `defaultLocale` (the default locale code — always
36
+ unprefixed, no separate flag needed) and `locales` (a map of additional
37
+ locale code → `{ label, dir }`; each entry becomes a `/<code>/docs/...`
38
+ route tree)
39
+
40
+ ## Translation Rules
41
+
42
+ ### Keep in English (do NOT translate)
43
+
44
+ - Component names: `<Note>`, `<Tip>`, `<Info>`, `<Warning>`, `<Danger>`, `<Tabs>`, `<TabItem>`, `<Details>`
45
+ - Code blocks — code is universal
46
+ - File paths: `src/content/docs/...`, `.claude/skills/...`, etc.
47
+ - CLI commands: `<pm> run dev`, `<pm> run build`, etc. (`<pm>` = this project's package manager)
48
+ - Technical terms that are standard in English (e.g., component, props, frontmatter, slug)
49
+ - Frontmatter field keys (`title`, `description`, `sidebar_position`, `category`)
50
+
51
+ ### Translate
52
+
53
+ - Frontmatter field values (e.g., the `title` value, the `description` value)
54
+ - The `title` prop of admonition components (e.g., `<Note title="注意">`)
55
+ - Prose content, headings, list items, table cells (except as noted below)
56
+
57
+ ### Table conventions
58
+
59
+ - In tables with a "Required" column: use **"Yes"** / **"No"** directly, NOT "はい" / "いいえ" — Japanese conversational yes/no is unnatural in technical documentation
60
+
61
+ ### Internal links
62
+
63
+ - Adjust link paths when translating:
64
+ - En→Ja: `/docs/getting-started` → `/ja/docs/getting-started`
65
+ - Ja→En: `/ja/docs/getting-started` → `/docs/getting-started`
66
+
67
+ ## File Naming
68
+
69
+ - Japanese files use the **same filenames** as English (e.g., `writing-docs.mdx`)
70
+ - Only the parent directory differs: `docs/` vs `docs-ja/`
71
+ - Example: `src/content/docs/guides/writing-docs.mdx` → `src/content/docs-ja/guides/writing-docs.mdx`
72
+
73
+ ## Workflow
74
+
75
+ ### En→Ja Translation
76
+
77
+ 1. Read the English source file from `src/content/docs/`
78
+ 2. Check if the corresponding Japanese file already exists in `src/content/docs-ja/`
79
+ - If it exists, read it first — use it as a base and update from the English source rather than overwriting from scratch
80
+ - If it does not exist, create the file at the equivalent path in `src/content/docs-ja/`
81
+ 3. Translate the content following the rules above
82
+ 4. Verify internal links point to `/ja/docs/...`
83
+
84
+ ### Ja→En Translation
85
+
86
+ 1. Read the Japanese source file from `src/content/docs-ja/`
87
+ 2. Check if the corresponding English file already exists in `src/content/docs/`
88
+ - If it exists, read it first — use it as a base and update from the Japanese source rather than overwriting from scratch
89
+ - If it does not exist, create the file at the equivalent path in `src/content/docs/`
90
+ 3. Translate the content following the rules above
91
+ 4. Verify internal links point to `/docs/...` (no `/ja/` prefix)
92
+
93
+ ### Post-Translation Checks
94
+
95
+ - Frontmatter keys are unchanged (only values translated)
96
+ - All admonition component names remain in English
97
+ - Code blocks are untouched
98
+ - Internal links use the correct locale prefix
99
+ - Directory structure mirrors the source language
@@ -0,0 +1,315 @@
1
+ ---
2
+ name: zudo-doc-version-bump
3
+ description: >-
4
+ Bump package version, update changelog and versioned docs snapshot (if those features are
5
+ enabled), commit, tag, and create a GitHub release. Use when: (1) User says 'version bump',
6
+ 'bump version', 'release', or 'zudo-doc-version-bump', (2) User wants to create a new release
7
+ of this project.
8
+ user-invocable: true
9
+ disable-model-invocation: true
10
+ argument-description: "Optional: major, minor, or patch to skip the proposal step"
11
+ ---
12
+
13
+ # /zudo-doc-version-bump
14
+
15
+ Bump the version, update changelog content (if the changelog feature is enabled), optionally
16
+ archive the current docs as a versioned snapshot (if the versioning feature is enabled), commit,
17
+ tag, and create a GitHub release.
18
+
19
+ ## Preconditions
20
+
21
+ Before doing anything else, verify ALL of the following. If any check fails, stop and tell the
22
+ user.
23
+
24
+ 1. Current branch is `main` (or your project's default branch)
25
+ 2. Working tree is clean (`git status --porcelain` returns empty)
26
+
27
+ Find the latest version tag, if any:
28
+
29
+ ```bash
30
+ git tag -l 'v*' --sort=-v:refname | head -1
31
+ ```
32
+
33
+ A freshly scaffolded project has no tags yet — that's expected on the first release. If no tag
34
+ is found, do NOT ask the user to create one first; proceed straight to analyzing the full commit
35
+ history in the next step instead.
36
+
37
+ ## Analyze changes since last tag
38
+
39
+ If a previous tag was found, diff against it:
40
+
41
+ ```bash
42
+ git log <last-tag>..HEAD --oneline
43
+ git diff <last-tag>..HEAD --stat
44
+ ```
45
+
46
+ If no tag exists yet (first release), analyze the whole history instead:
47
+
48
+ ```bash
49
+ git log --oneline
50
+ ```
51
+
52
+ Categorize each commit by its conventional-commit prefix:
53
+
54
+ - **Breaking Changes**: commits with an exclamation mark suffix (e.g. `feat!:`) or BREAKING CHANGE in body
55
+ - **Features**: `feat:` prefix
56
+ - **Bug Fixes**: `fix:` prefix
57
+ - **Other Changes**: everything else (`docs:`, `chore:`, `refactor:`, `ci:`, `test:`, `style:`, `perf:`, etc.)
58
+
59
+ ## Propose version bump
60
+
61
+ Based on the changes:
62
+
63
+ - If there are breaking changes → propose **major** bump
64
+ - If there are features (no breaking) → propose **minor** bump
65
+ - Otherwise → propose **patch** bump
66
+
67
+ If the user passed an argument (`major`, `minor`, or `patch`), use that directly instead of proposing.
68
+
69
+ Present the proposal to the user:
70
+
71
+ ```
72
+ Proposed bump: {current} → {new} ({type})
73
+
74
+ Breaking Changes:
75
+ - description (hash)
76
+
77
+ Features:
78
+ - description (hash)
79
+
80
+ Bug Fixes:
81
+ - description (hash)
82
+
83
+ Other Changes:
84
+ - description (hash)
85
+ ```
86
+
87
+ Only show sections that have entries. **Wait for user confirmation before proceeding.**
88
+
89
+ If this is a **major** version bump and the project has the versioning feature enabled (see
90
+ "Archive docs as a versioned snapshot" below), ask the user whether they want to archive the
91
+ current docs as a versioned snapshot before continuing.
92
+
93
+ ## Bump the version
94
+
95
+ A `create-zudo-doc` scaffold has no bundled version-bump script — bump `package.json` directly:
96
+
97
+ ```bash
98
+ npm version {NEW_VERSION} --no-git-tag-version
99
+ ```
100
+
101
+ `npm version` is a plain `npm` CLI feature (Node always ships `npm`), so this works no matter
102
+ which package manager (`<pm>`) the project otherwise uses. `--no-git-tag-version` stops it from
103
+ also creating a commit/tag — this skill handles that later, once everything else is ready. A
104
+ direct edit of the `"version"` field in `package.json` works identically if preferred.
105
+
106
+ ## Update the changelog (if enabled)
107
+
108
+ Check whether the project has a changelog page:
109
+
110
+ ```bash
111
+ test -f src/content/docs/changelog/index.mdx && echo "changelog present"
112
+ ```
113
+
114
+ If that file does not exist, the changelog feature is not in use — skip this whole section and
115
+ go straight to "Archive docs as a versioned snapshot".
116
+
117
+ Unlike a per-version file, a scaffolded changelog is a single `index.mdx` page. Add the new
118
+ release as a section **above** the existing ones (newest first), using only the categories that
119
+ have entries.
120
+
121
+ ### English changelog (`src/content/docs/changelog/index.mdx`)
122
+
123
+ ```mdx
124
+ ## {NEW_VERSION}
125
+
126
+ ### Breaking Changes
127
+
128
+ - Description (commit-hash)
129
+
130
+ ### Features
131
+
132
+ - Description (commit-hash)
133
+
134
+ ### Bug Fixes
135
+
136
+ - Description (commit-hash)
137
+
138
+ ### Other Changes
139
+
140
+ - Description (commit-hash)
141
+ ```
142
+
143
+ On the very first bump, the page still has the scaffold's starter `## Unreleased` section —
144
+ replace that heading with `## {NEW_VERSION}` rather than adding a second heading.
145
+
146
+ ### Japanese changelog (`src/content/docs-ja/changelog/index.mdx`)
147
+
148
+ Only applies when i18n is enabled (the `docs-ja` directory exists). If it doesn't, skip this file.
149
+
150
+ ```mdx
151
+ ## {NEW_VERSION}
152
+
153
+ ### 破壊的変更
154
+
155
+ - Description (commit-hash)
156
+
157
+ ### 機能
158
+
159
+ - Description (commit-hash)
160
+
161
+ ### バグ修正
162
+
163
+ - Description (commit-hash)
164
+
165
+ ### その他の変更
166
+
167
+ - Description (commit-hash)
168
+ ```
169
+
170
+ On the first bump, replace the starter `## 未リリース` heading the same way.
171
+
172
+ Rules:
173
+
174
+ - Only include sections that have entries
175
+ - Each entry should be the commit subject with the short hash in parentheses
176
+
177
+ ## Archive docs as a versioned snapshot (if enabled, major bumps only)
178
+
179
+ Only relevant when the project has the versioning feature enabled — check for a `versions`
180
+ array in `zfb.config.ts`'s `zudoDoc({...})` call. If `versions` is absent, `false`, or the user
181
+ declined the snapshot offer above, skip this whole section.
182
+
183
+ 1. Derive the old version's slug by dropping the patch component, e.g. `0.1.0` → `0.1`,
184
+ `1.2.3` → `1.2`.
185
+ 2. Copy the current docs into a versioned directory:
186
+
187
+ ```bash
188
+ cp -r src/content/docs src/content/docs-v{OLD_SLUG}
189
+ ```
190
+
191
+ 3. If i18n is enabled (a `docs-ja` directory exists), also copy the Japanese docs:
192
+
193
+ ```bash
194
+ cp -r src/content/docs-ja src/content/docs-v{OLD_SLUG}-ja
195
+ ```
196
+
197
+ 4. Add an entry to the `versions` array in `zfb.config.ts`:
198
+
199
+ ```ts
200
+ versions: [
201
+ {
202
+ slug: "{OLD_SLUG}",
203
+ label: "{OLD_VERSION}",
204
+ docsDir: "src/content/docs-v{OLD_SLUG}",
205
+ locales: {
206
+ ja: { dir: "src/content/docs-v{OLD_SLUG}-ja" },
207
+ },
208
+ banner: "unmaintained",
209
+ },
210
+ // ...existing versions
211
+ ],
212
+ ```
213
+
214
+ Drop the `locales` block entirely when i18n is not enabled.
215
+
216
+ 5. `src/content/docs/` (and `src/content/docs-ja/`) now represent the new, latest version —
217
+ no further action needed there. The version switcher and versions listing page pick up the
218
+ new entry automatically at build time; nothing else needs wiring.
219
+
220
+ ## Build and test
221
+
222
+ Run the project's pre-push validation script:
223
+
224
+ ```bash
225
+ <pm> run b4push
226
+ ```
227
+
228
+ If anything fails, fix the issue and re-run. Do not proceed with committing until all checks
229
+ pass. (In a freshly scaffolded project this script may just be a `check`-then-`build` stub —
230
+ that's fine; expand it into a richer pipeline as the project's testing needs grow.)
231
+
232
+ ## Commit changes
233
+
234
+ Stage and commit **all** version bump changes:
235
+
236
+ ```bash
237
+ git add package.json
238
+ git add src/content/docs/changelog/index.mdx src/content/docs-ja/changelog/index.mdx 2>/dev/null
239
+ git add src/content/docs-v* 2>/dev/null
240
+ # Also stage any other modified files (e.g. formatting fixes from the build/test step)
241
+ git diff --name-only | xargs -r git add
242
+ git commit -m "chore: Bump version to v{NEW_VERSION}"
243
+ ```
244
+
245
+ ## Push and wait for CI (if configured)
246
+
247
+ Push the commits first (without the tag):
248
+
249
+ ```bash
250
+ git push
251
+ ```
252
+
253
+ If the project has CI configured (e.g. a GitHub Actions workflow), wait for it to pass. Use
254
+ `gh run list --branch main --limit 1 --json status,conclusion,headSha` and verify the `headSha`
255
+ matches the pushed commit. Poll every 30 seconds, with a **maximum of 10 minutes**. If CI is
256
+ still running after 10 minutes, ask the user whether to keep waiting or proceed. If the project
257
+ has no CI workflow, skip straight to tagging.
258
+
259
+ If CI fails, investigate the failure with `gh run view <run-id> --log-failed`, fix the issue,
260
+ commit, and push again.
261
+
262
+ **Do not tag or publish until CI is green (or there is no CI to wait for).**
263
+
264
+ ## Tag, push tag, and create GitHub release
265
+
266
+ **Ask the user for confirmation before tagging.**
267
+
268
+ ```bash
269
+ git tag v{NEW_VERSION}
270
+ git push --tags
271
+ ```
272
+
273
+ After pushing the tag, create a GitHub release. If the changelog feature is enabled, pull the
274
+ section you just wrote out of the single changelog page as the release notes:
275
+
276
+ ```bash
277
+ NOTES=$(awk -v ver="## {NEW_VERSION}" '$0==ver{f=1;next} f&&/^## /{f=0} f' src/content/docs/changelog/index.mdx)
278
+ gh release create v{NEW_VERSION} --title "v{NEW_VERSION}" --notes "$NOTES"
279
+ ```
280
+
281
+ If the changelog feature is off, write the release notes directly from the categorized commit
282
+ analysis instead:
283
+
284
+ ```bash
285
+ gh release create v{NEW_VERSION} --title "v{NEW_VERSION}" --notes "..."
286
+ ```
287
+
288
+ ## Publish to npm (if applicable)
289
+
290
+ If the package is **not** marked as `"private": true` in `package.json`, tell the user to publish:
291
+
292
+ ```
293
+ The package is ready for npm publishing. Run:
294
+
295
+ <pm> publish
296
+
297
+ (This requires browser-based 2FA and must be done manually.)
298
+ ```
299
+
300
+ If the package is `"private": true`, skip this step and inform the user:
301
+
302
+ ```
303
+ Package is marked as private — skipping npm publish.
304
+ ```
305
+
306
+ ## Done
307
+
308
+ Report the summary:
309
+
310
+ - Version bumped: `{OLD_VERSION}` → `{NEW_VERSION}`
311
+ - Changelog updated (EN + JA, if the changelog feature is enabled)
312
+ - Docs snapshot created (if the versioning feature is enabled and a snapshot was taken)
313
+ - Git tag: `v{NEW_VERSION}`
314
+ - GitHub release: link to the release
315
+ - npm publish status (published / skipped for private package)
@@ -10,7 +10,7 @@
10
10
  "app": {
11
11
  "windows": [],
12
12
  "security": {
13
- "csp": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data:; font-src 'self' data: https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://esm.sh; connect-src 'self' https://esm.sh https://cdn.jsdelivr.net"
13
+ "csp": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data:; font-src 'self' data: https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://esm.sh https://cdn.jsdelivr.net; connect-src 'self' https://esm.sh https://cdn.jsdelivr.net"
14
14
  }
15
15
  },
16
16
  "bundle": {