sparkle-design-cli 2.0.7-beta.0 → 2.0.7-beta.10

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/lib/setup.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawnSync } from 'child_process';
4
4
  import { generateCSS } from './generate-css.js';
5
+ import { GLOBALS_CSS_CANDIDATES } from './constants.js';
5
6
 
6
7
  // デフォルトの sparkle.config.json テンプレート
7
8
  // en: Default sparkle.config.json template
@@ -19,10 +20,23 @@ const DEFAULT_SPARKLE_CONFIG = {
19
20
  };
20
21
 
21
22
  // 初期 globals.css のテンプレート
22
- // en: Initial globals.css template
23
- const INITIAL_GLOBALS_CSS = `@import "tailwindcss";
24
- @import "./sparkle-design.css";
25
- `;
23
+ // target のディレクトリと sparkle-design.css の想定出力先
24
+ // (`src/app/sparkle-design.css`) との相対関係で import path を切り替える。
25
+ // Next.js App Router (src/app/globals.css) なら同一 dir で `./sparkle-design.css`、
26
+ // Vite (src/index.css) なら 1 階層下なので `./app/sparkle-design.css` になる。
27
+ // 同一 dir 前提のハードコードで生成していた beta.7 以前は Vite レイアウトで
28
+ // path が切れ、後続 generate が相対 path を再計算しても scaffold 時点では
29
+ // 間違った path で書かれていたため、AI が手で直す副作用が出ていた。
30
+ // en: Render initial entry CSS with a path that actually resolves to
31
+ // `src/app/sparkle-design.css` given the target file location.
32
+ function buildInitialGlobalsCss(targetRelPath) {
33
+ const targetDir = path.dirname(targetRelPath);
34
+ const sparkleDesignRel = 'src/app/sparkle-design.css';
35
+ let rel = path.relative(targetDir, sparkleDesignRel).split(path.sep).join('/');
36
+ if (!rel) rel = 'sparkle-design.css';
37
+ if (!rel.startsWith('.')) rel = `./${rel}`;
38
+ return `@import "tailwindcss";\n@import "${rel}";\n`;
39
+ }
26
40
 
27
41
  // インストール対象パッケージ
28
42
  // en: Packages to install
@@ -41,16 +55,6 @@ const config = {
41
55
  export default config;
42
56
  `;
43
57
 
44
- // globals.css 候補パス(優先順)
45
- // en: globals.css candidate paths in priority order
46
- const GLOBALS_CSS_CANDIDATES = [
47
- 'src/app/globals.css',
48
- 'app/globals.css',
49
- 'src/globals.css',
50
- 'src/index.css',
51
- 'src/styles/globals.css',
52
- ];
53
-
54
58
  const ASSISTANT_CONFIG = {
55
59
  claude: {
56
60
  path: 'CLAUDE.md',
@@ -192,12 +196,16 @@ function buildInstructionBlock(target, assistant) {
192
196
  BLOCK_START,
193
197
  heading,
194
198
  '',
195
- '- **必ず実行**: UI コンポーネントを作成・変更した後は、終了前に `lint:sparkle` を実行してください。Sparkle Design のアンチパターンを検出します。',
199
+ '- **Scope**: Sparkle Design is a UI component library. For capability areas it does not cover (charts, data visualization, maps, rich text editors, animation libraries, etc.), **feel free to adopt other libraries** (e.g. Recharts, D3, Chart.js) — do not try to solve everything inside Sparkle Design. Pass Sparkle Design CSS tokens (`--color-primary-*` etc.) into those libraries to keep visuals consistent.',
200
+ "- **Required reading**: Before using any Sparkle Design component, read the installed package's type definitions at `node_modules/<package>/dist/components/ui/<component>/index.d.ts` — prop specs, usage, and anti-patterns (with ✅ / ❌ examples) live in the JSDoc and are the source of truth. Target the installed package(s), e.g. `sparkle-design` and/or `@goodpatch/sparkle-design-internal`, and read the full JSDoc, not just the type signature.",
201
+ '- **Structural invariants — verify before hand-editing any Sparkle file**:',
202
+ ' - Entry CSS (e.g. `src/index.css`, `src/app/globals.css`) must start with `@import "tailwindcss";` and keep **all `@import` statements before any `@source` directive**. CSS-spec-compliant processors silently drop any `@import` that follows another at-rule, which removes Sparkle tokens from the output.',
203
+ ' - Sparkle fonts (Google Fonts preconnect + Material Symbols + the configured pro/mono fonts) must be present in the document `<head>`: React layouts via `<SparkleHead />` placed inside `<head>` in the root layout, Vite projects via the managed `<!-- sparkle-design-cli:fonts:start -->` … `end` block in `index.html`.',
204
+ ' - Do **not** hand-define `--color-primary-*` or other Sparkle tokens as fallbacks when they look missing. Those come from `sparkle-design.css`; missing values mean the `@import` path is wrong (e.g. `./sparkle-design.css` used when the file lives under `./app/`). Fix the import path, do not duplicate the tokens.',
205
+ ' - If you detect any of the above drifted, re-run `npx --yes sparkle-design-cli generate` before hand-editing. The CLI restores the canonical state (correct relative paths, `@source` placement, `index.html` injection).',
196
206
  '- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
197
207
  `- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
198
- '- JSON 出力の `findings` に加えて、**`manualReviewReminders` の内容も必ず 1 項目ずつ確認**してください。機械検出できない Badge/Tag の使い分けなどが含まれます。',
199
- '- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag etc.) that the linter cannot detect.',
200
- '- If semantic guidance is still needed, consult Sparkle Design docs and JSDoc examples.',
208
+ '- You must inspect every entry in both `findings` and `manualReviewReminders`. For each entry in `manualReviewReminders`, **echo the reminder ID in your final reply together with a short review note** (e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts, so the usage is fine.`). Silence on a reminder means it was skipped; always state at least a one-line judgment per ID. This is not blocked by the Stop hook (reminders are judgment calls), but reviewers / humans rely on the explicit acknowledgment to trust that each item was actually considered.',
201
209
  BLOCK_END,
202
210
  ].join('\n');
203
211
  }
@@ -385,7 +393,8 @@ function buildScaffoldTargets(cwd) {
385
393
  target: defaultGlobalsCssTarget(cwd),
386
394
  write: (abs) => {
387
395
  ensureDir(abs);
388
- fs.writeFileSync(abs, INITIAL_GLOBALS_CSS, 'utf8');
396
+ const targetRel = path.relative(cwd, abs).split(path.sep).join('/');
397
+ fs.writeFileSync(abs, buildInitialGlobalsCss(targetRel), 'utf8');
389
398
  },
390
399
  },
391
400
  ];
@@ -447,12 +456,37 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
447
456
  const instructionBlock = buildInstructionBlock(target, options.assistant);
448
457
  const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
449
458
 
459
+ // --assistant claude のときに、プロジェクトルートに既存の AGENTS.md が
460
+ // 「すでにある」場合だけ Guard を追記する。create-next-app などが先行生成
461
+ // した AGENTS.md や、ユーザーが Codex/Gemini 併用のために置いているファイル
462
+ // には courtesy として Guard を載せる一方、存在しない AGENTS.md を
463
+ // Sparkle 側が勝手に作ることはしない(「指定していない AI 指示書が
464
+ // あったら書く」という緩い方針)。
465
+ // en: When --assistant=claude, also upsert Guard into AGENTS.md **only if
466
+ // the file already exists**. This covers projects where create-next-app or
467
+ // another tool shipped an AGENTS.md (or where the user keeps one for
468
+ // Codex/Gemini), without the CLI unilaterally creating a file the user
469
+ // didn't opt into.
470
+ let agentsInstructionResult = null;
471
+ let agentsInstructionPath = null;
472
+ if (options.assistant === 'claude' && !options.instructionsPath) {
473
+ const candidate = path.resolve(cwd, 'AGENTS.md');
474
+ if (candidate !== instructionPath && fs.existsSync(candidate)) {
475
+ agentsInstructionPath = candidate;
476
+ agentsInstructionResult = updateInstructionFile(agentsInstructionPath, instructionBlock);
477
+ }
478
+ }
479
+
450
480
  if (!options.dryRun) {
451
481
  if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
452
482
  if (instructionResult.changed) {
453
483
  ensureDir(instructionPath);
454
484
  fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
455
485
  }
486
+ if (agentsInstructionResult?.changed && agentsInstructionPath) {
487
+ ensureDir(agentsInstructionPath);
488
+ fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
489
+ }
456
490
  }
457
491
 
458
492
  return {
@@ -461,16 +495,279 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
461
495
  instructionPath,
462
496
  packageResult,
463
497
  instructionResult,
498
+ agentsInstructionPath,
499
+ agentsInstructionResult,
464
500
  };
465
501
  }
466
502
 
467
- function runGenerate({ skipGenerate, dryRun }) {
503
+ /**
504
+ * Agent 別の hook 設定ファイルに「lint:sparkle を強制実行する」hook を追加する。
505
+ *
506
+ * 各 agent が持つ hook システムの schema は異なるため、agent ごとに以下の
507
+ * ヘルパーを持つ。共通ポイントは:
508
+ * - 実行コマンドは `npx --yes sparkle-design-cli check <target> --strict || exit 2`
509
+ * - exit 2 にエスカレーションすることで、ブロック対応する agent では
510
+ * 応答終了を止めて findings 修正に向かわせる
511
+ * - 既存の hook 設定ファイルは非破壊マージし、同一 command が含まれていれば skip
512
+ * (冪等)
513
+ * - 壊れた JSON は silent 上書きせず、ユーザーに修正を促すエラーを出す
514
+ *
515
+ * en: Install an agent-specific hook that runs `lint:sparkle --strict || exit 2`.
516
+ * Each agent ships its own hook config format; these helpers emit the right
517
+ * shape while preserving existing user content and staying idempotent on rerun.
518
+ *
519
+ * References (2026-04 時点):
520
+ * - Claude Code : https://docs.claude.com/en/docs/claude-code/hooks
521
+ * - Cursor : https://cursor.com/docs/hooks
522
+ * - Codex : https://developers.openai.com/codex/hooks
523
+ */
524
+ function buildManagedHookCommand(target) {
525
+ return `npx --yes sparkle-design-cli check ${target} --strict || exit 2`;
526
+ }
527
+
528
+ function loadHookJson(filePath, label) {
529
+ if (!fs.existsSync(filePath)) {
530
+ return { existed: false, config: null };
531
+ }
532
+ try {
533
+ return { existed: true, config: readJson(filePath) };
534
+ } catch (error) {
535
+ throw new Error(
536
+ `${label} が不正な JSON です (${error.message})。修正してから再実行してください。`
537
+ );
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Claude Code の Stop hook を読んでくれるのは「Claude を起動した作業ディレクトリ」
543
+ * 直下の `.claude/settings.json` であって、cwd とは限らない。ユーザーが親フォルダ
544
+ * で Claude を起動してからサブディレクトリに `cd` してこの setup を実行した
545
+ * 場合、`.claude/settings.json` はサブディレクトリに置かれるが Claude は親側を
546
+ * 見ているので hook が効かない、という失敗モードがある。
547
+ *
548
+ * Claude Code が Bash tool 経由で環境変数 `CLAUDE_PROJECT_DIR` を設定するので、
549
+ * それを検出して cwd と違えば「hook が効かない可能性あり」と警告する。環境変数
550
+ * が無い場合(別の agent から呼ばれている / Claude Code 以外)は警告なし。
551
+ *
552
+ * en: Claude Code loads `.claude/settings.json` from the directory where it was
553
+ * launched, not the current working directory. If the user started Claude at a
554
+ * parent folder and ran `sparkle-design-cli setup` inside a subdirectory, the
555
+ * hook lands in a place Claude won't read. Detect this mismatch via the
556
+ * `CLAUDE_PROJECT_DIR` env var that Claude Code exports into shell tools.
557
+ */
558
+ function detectClaudeSessionRootMismatch(cwd) {
559
+ const sessionDir = process.env.CLAUDE_PROJECT_DIR;
560
+ if (!sessionDir) return null;
561
+ try {
562
+ const resolvedSession = fs.realpathSync(path.resolve(sessionDir));
563
+ const resolvedCwd = fs.realpathSync(path.resolve(cwd));
564
+ if (resolvedSession === resolvedCwd) return null;
565
+ return { sessionDir: resolvedSession, cwd: resolvedCwd };
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+
571
+ function runClaudeHook(cwd, target, dryRun) {
572
+ const settingsPath = path.resolve(cwd, '.claude/settings.json');
573
+ const managedCommand = buildManagedHookCommand(target);
574
+ const { existed, config } = loadHookJson(settingsPath, '.claude/settings.json');
575
+
576
+ const nextSettings =
577
+ typeof config === 'object' && config ? { ...config } : {};
578
+ const hooks =
579
+ typeof nextSettings.hooks === 'object' && nextSettings.hooks
580
+ ? { ...nextSettings.hooks }
581
+ : {};
582
+ const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
583
+
584
+ const alreadyPresent = stopGroups.some(
585
+ (group) =>
586
+ Array.isArray(group?.hooks) &&
587
+ group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
588
+ );
589
+
590
+ const mismatch = detectClaudeSessionRootMismatch(cwd);
591
+ if (alreadyPresent) {
592
+ return {
593
+ assistant: 'claude',
594
+ changed: false,
595
+ existed,
596
+ path: settingsPath,
597
+ reason: 'already-present',
598
+ command: managedCommand,
599
+ sessionRootMismatch: mismatch,
600
+ };
601
+ }
602
+
603
+ stopGroups.push({
604
+ hooks: [{ type: 'command', command: managedCommand }],
605
+ });
606
+ hooks.Stop = stopGroups;
607
+ nextSettings.hooks = hooks;
608
+
609
+ if (!dryRun) {
610
+ ensureDir(settingsPath);
611
+ writeJson(settingsPath, nextSettings);
612
+ }
613
+ return {
614
+ assistant: 'claude',
615
+ changed: true,
616
+ existed,
617
+ path: settingsPath,
618
+ reason: existed ? 'appended' : 'created',
619
+ command: managedCommand,
620
+ sessionRootMismatch: mismatch,
621
+ };
622
+ }
623
+
624
+ /**
625
+ * Cursor 1.7+ の hook 設定(`.cursor/hooks.json`)に stop hook を追加する。
626
+ * schema: `{ version: 1, hooks: { stop: [{ command: "..." }] } }`
627
+ */
628
+ function runCursorHook(cwd, target, dryRun) {
629
+ const hooksPath = path.resolve(cwd, '.cursor/hooks.json');
630
+ const managedCommand = buildManagedHookCommand(target);
631
+ const { existed, config } = loadHookJson(hooksPath, '.cursor/hooks.json');
632
+
633
+ const nextConfig = typeof config === 'object' && config ? { ...config } : {};
634
+ if (!nextConfig.version) nextConfig.version = 1;
635
+ const hooks =
636
+ typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
637
+ const stopEntries = Array.isArray(hooks.stop) ? [...hooks.stop] : [];
638
+
639
+ const alreadyPresent = stopEntries.some((entry) => entry?.command === managedCommand);
640
+ if (alreadyPresent) {
641
+ return {
642
+ assistant: 'cursor',
643
+ changed: false,
644
+ existed,
645
+ path: hooksPath,
646
+ reason: 'already-present',
647
+ command: managedCommand,
648
+ };
649
+ }
650
+
651
+ stopEntries.push({ command: managedCommand });
652
+ hooks.stop = stopEntries;
653
+ nextConfig.hooks = hooks;
654
+
655
+ if (!dryRun) {
656
+ ensureDir(hooksPath);
657
+ writeJson(hooksPath, nextConfig);
658
+ }
659
+
660
+ return {
661
+ assistant: 'cursor',
662
+ changed: true,
663
+ existed,
664
+ path: hooksPath,
665
+ reason: existed ? 'appended' : 'created',
666
+ command: managedCommand,
667
+ };
668
+ }
669
+
670
+ /**
671
+ * Codex の hook 設定(`.codex/hooks.json`)に Stop hook を追加する。
672
+ * schema は Claude とほぼ同じネスト構造。
673
+ * 有効化には `~/.codex/config.toml` に `[features] codex_hooks = true` が必要。
674
+ */
675
+ function runCodexHook(cwd, target, dryRun) {
676
+ const hooksPath = path.resolve(cwd, '.codex/hooks.json');
677
+ const managedCommand = buildManagedHookCommand(target);
678
+ const { existed, config } = loadHookJson(hooksPath, '.codex/hooks.json');
679
+
680
+ const nextConfig = typeof config === 'object' && config ? { ...config } : {};
681
+ const hooks =
682
+ typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
683
+ const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
684
+
685
+ const alreadyPresent = stopGroups.some(
686
+ (group) =>
687
+ Array.isArray(group?.hooks) &&
688
+ group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
689
+ );
690
+
691
+ if (alreadyPresent) {
692
+ return {
693
+ assistant: 'codex',
694
+ changed: false,
695
+ existed,
696
+ path: hooksPath,
697
+ reason: 'already-present',
698
+ command: managedCommand,
699
+ // 有効化にはユーザー側の opt-in が必要な旨を summary に載せる。
700
+ // en: Codex hooks are behind an opt-in feature flag; surface that in summary.
701
+ featureFlagNote:
702
+ 'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
703
+ };
704
+ }
705
+
706
+ stopGroups.push({
707
+ hooks: [{ type: 'command', command: managedCommand }],
708
+ });
709
+ hooks.Stop = stopGroups;
710
+ nextConfig.hooks = hooks;
711
+
712
+ if (!dryRun) {
713
+ ensureDir(hooksPath);
714
+ writeJson(hooksPath, nextConfig);
715
+ }
716
+
717
+ return {
718
+ assistant: 'codex',
719
+ changed: true,
720
+ existed,
721
+ path: hooksPath,
722
+ reason: existed ? 'appended' : 'created',
723
+ command: managedCommand,
724
+ featureFlagNote:
725
+ 'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
726
+ };
727
+ }
728
+
729
+ /**
730
+ * --assistant 値から該当 hook writer に dispatch する。generic は hook なし。
731
+ * en: Dispatch to the appropriate hook writer for the selected assistant.
732
+ */
733
+ function runAssistantHook(assistant, cwd, target, dryRun) {
734
+ switch (assistant) {
735
+ case 'claude':
736
+ return runClaudeHook(cwd, target, dryRun);
737
+ case 'cursor':
738
+ return runCursorHook(cwd, target, dryRun);
739
+ case 'codex':
740
+ return runCodexHook(cwd, target, dryRun);
741
+ default:
742
+ return null;
743
+ }
744
+ }
745
+
746
+ function runGenerate({ skipGenerate, dryRun, strict }) {
747
+ // --skip-generate / --dry-run と --strict が同時指定された場合、strict チェックを
748
+ // 走らせる対象 (generate) そのものがスキップされるため strict は事実上無意味。
749
+ // サイレントに無効化するとユーザーが CI で気付けないので警告を出す。
750
+ // en: --strict has no effect if generate is skipped — warn the user rather
751
+ // than silently no-op, which would mask CI misconfiguration.
752
+ if (strict && (skipGenerate || dryRun)) {
753
+ console.warn(
754
+ '⚠️ --strict は generate の失敗のみ検出します。--skip-generate / --dry-run と併用した場合は strict チェックは走りません。'
755
+ );
756
+ }
468
757
  if (skipGenerate || dryRun) return { skipped: true, ran: false };
469
758
  try {
470
759
  console.log('🎨 sparkle-design.css を生成中...');
471
- generateCSS();
472
- return { skipped: false, ran: true };
760
+ const result = generateCSS(null, null, null, { strict: Boolean(strict) });
761
+ return { skipped: false, ran: true, globalsResult: result?.globalsResult };
473
762
  } catch (error) {
763
+ // strict モード、または sparkle.config.json の `globals-path` typo などの
764
+ // 明示指定 not-found は、非 strict でもユーザーが必ず気付くべきなので
765
+ // 再 throw する(#33 の挙動を setup 経由でも保つ)。
766
+ // en: Always re-throw explicit --globals-path typos so `setup` surfaces
767
+ // them via exit code, matching the behaviour of `generate` directly.
768
+ if (strict || error.code === 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND') {
769
+ throw error;
770
+ }
474
771
  console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
475
772
  return { skipped: false, ran: false, error: error.message };
476
773
  }
@@ -501,7 +798,18 @@ export function setupAssistant(options = {}) {
501
798
  { ...options, assistant, dryRun },
502
799
  assistantConfig
503
800
  );
504
- const generate = runGenerate({ skipGenerate: Boolean(options.skipGenerate), dryRun });
801
+ // assistant 別に hook 設定ファイル(`.claude/settings.json` /
802
+ // `.cursor/hooks.json` / `.codex/hooks.json`)に `lint:sparkle --strict` を
803
+ // 走らせる stop / Stop hook を自動設定する。instruction 頼みではなく hook
804
+ // で強制することで lint:sparkle の実行漏れを防ぐ。generic は hook なし。
805
+ // en: For supported assistants, install a stop hook so `lint:sparkle --strict`
806
+ // becomes a hard gate. Generic assistant has no hook target.
807
+ const hook = runAssistantHook(assistant, cwd, guard.target, dryRun);
808
+ const generate = runGenerate({
809
+ skipGenerate: Boolean(options.skipGenerate),
810
+ dryRun,
811
+ strict: Boolean(options.strict),
812
+ });
505
813
 
506
814
  const summary = {
507
815
  assistant,
@@ -527,6 +835,24 @@ export function setupAssistant(options = {}) {
527
835
  changed: guard.instructionResult.changed,
528
836
  existed: guard.instructionResult.existed,
529
837
  },
838
+ agentsInstructions: guard.agentsInstructionPath
839
+ ? {
840
+ path: path.relative(cwd, guard.agentsInstructionPath),
841
+ changed: guard.agentsInstructionResult?.changed ?? false,
842
+ existed: guard.agentsInstructionResult?.existed ?? false,
843
+ }
844
+ : null,
845
+ hook: hook
846
+ ? {
847
+ assistant: hook.assistant,
848
+ path: path.relative(cwd, hook.path),
849
+ changed: hook.changed,
850
+ existed: hook.existed,
851
+ reason: hook.reason,
852
+ featureFlagNote: hook.featureFlagNote ?? null,
853
+ sessionRootMismatch: hook.sessionRootMismatch ?? null,
854
+ }
855
+ : null,
530
856
  };
531
857
 
532
858
  console.log(JSON.stringify(summary, null, 2));
@@ -534,7 +860,7 @@ export function setupAssistant(options = {}) {
534
860
  // セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
535
861
  // en: Post-setup reminder. Written to stderr so it stays in the AI transcript
536
862
  // without polluting the JSON stdout payload that tooling parses.
537
- printPostSetupReminder(guard.target, packageManager);
863
+ printPostSetupReminder(guard.target, packageManager, { hook });
538
864
 
539
865
  return summary;
540
866
  }
@@ -556,18 +882,49 @@ function buildLintCommand(packageManager) {
556
882
  }
557
883
  }
558
884
 
559
- function printPostSetupReminder(target, packageManager) {
885
+ const HOOK_LABELS = {
886
+ claude: { name: 'Claude Code', file: '.claude/settings.json', event: 'Stop' },
887
+ cursor: { name: 'Cursor', file: '.cursor/hooks.json', event: 'stop' },
888
+ codex: { name: 'Codex', file: '.codex/hooks.json', event: 'Stop' },
889
+ };
890
+
891
+ function printPostSetupReminder(target, packageManager, { hook } = {}) {
560
892
  const lintTarget = target || 'src';
561
893
  const lintCmd = buildLintCommand(packageManager);
562
894
  const lines = [
563
895
  '',
564
896
  '📝 次のステップ / Next steps:',
565
- ` 1. UI コンポーネントを作成・変更したら、必ず \`${lintCmd}\` (または \`npx --yes sparkle-design-cli check ${lintTarget} --strict\`) を実行してください。`,
897
+ ' 1. Sparkle Design のコンポーネントを使う前に、必ず `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` の JSDoc を読んでください。Prop 仕様・使用例・アンチパターンは JSDoc が Source of Truth です。',
898
+ ' Before using any Sparkle Design component, read `node_modules/<package>/dist/components/ui/<name>/index.d.ts` — the JSDoc includes prop specs, usage, and anti-pattern examples.',
899
+ ` 2. UI コンポーネントを作成・変更したら、必ず \`${lintCmd}\` (または \`npx --yes sparkle-design-cli check ${lintTarget} --strict\`) を実行してください。`,
566
900
  ' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
567
- ' 2. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
901
+ ' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
568
902
  ' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
569
- '',
570
903
  ];
904
+ if (hook && (hook.changed || hook.reason === 'already-present')) {
905
+ const meta = HOOK_LABELS[hook.assistant] ?? HOOK_LABELS.claude;
906
+ const label =
907
+ hook.reason === 'already-present'
908
+ ? '(既存のまま)'
909
+ : hook.reason === 'created'
910
+ ? '(新規作成)'
911
+ : '(既存設定に追記)';
912
+ lines.push(
913
+ ` 4. ${meta.name} の ${meta.event} hook を ${meta.file} に設定しました ${label}。応答を終える直前に \`lint:sparkle --strict\` が走り、findings があれば exit 2 で停止がブロックされます。`,
914
+ ` Installed a ${meta.name} ${meta.event} hook in ${meta.file}: it runs \`lint:sparkle --strict\` before the turn ends and exits with code 2 when findings exist.`
915
+ );
916
+ if (hook.featureFlagNote) {
917
+ lines.push(` ⚠️ ${hook.featureFlagNote}`);
918
+ }
919
+ if (hook.sessionRootMismatch) {
920
+ const { sessionDir, cwd: mismatchCwd } = hook.sessionRootMismatch;
921
+ lines.push(
922
+ ` ⚠️ Claude Code は ${sessionDir} を session root として ${path.join(sessionDir, '.claude/settings.json')} を読みます。今回の hook は ${path.join(mismatchCwd, '.claude/settings.json')} に書かれたため、このまま だと hook が発火しません。Claude Code をこのディレクトリから再起動するか、同じ設定を session root の .claude/settings.json にコピーしてください。`,
923
+ ` ⚠️ Claude Code reads \`.claude/settings.json\` from its session root, not cwd. The hook written to the current directory won't trigger — relaunch Claude from here, or copy the config to the session root.`
924
+ );
925
+ }
926
+ }
927
+ lines.push('');
571
928
  for (const line of lines) {
572
929
  console.error(line);
573
930
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.0.7-beta.0",
4
- "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
3
+ "version": "2.0.7-beta.10",
4
+ "description": "Sparkle Design CLI — プロジェクトセットアップ、CSS・フォント生成、アンチパターン検査、AI エージェント(Claude Code / Cursor / Codex)向けのガードと hook 設定まで一括で行う sparkle-design 公式 CLI。",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
7
7
  "access": "public"
@@ -21,10 +21,20 @@
21
21
  "sync:anti-pattern-docs": "node scripts/sync-anti-pattern-docs.mjs"
22
22
  },
23
23
  "keywords": [
24
- "css",
24
+ "sparkle-design",
25
25
  "design-system",
26
- "css-generator",
27
- "theming"
26
+ "cli",
27
+ "setup",
28
+ "scaffold",
29
+ "css",
30
+ "tailwindcss",
31
+ "theming",
32
+ "anti-pattern",
33
+ "linter",
34
+ "ai-agent",
35
+ "claude-code",
36
+ "cursor",
37
+ "codex"
28
38
  ],
29
39
  "author": "Goodpatch Inc. <sparkle-design@goodpatch.com>",
30
40
  "license": "MIT",