sparkle-design-cli 2.0.7-beta.1 → 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
@@ -20,10 +20,23 @@ const DEFAULT_SPARKLE_CONFIG = {
20
20
  };
21
21
 
22
22
  // 初期 globals.css のテンプレート
23
- // en: Initial globals.css template
24
- const INITIAL_GLOBALS_CSS = `@import "tailwindcss";
25
- @import "./sparkle-design.css";
26
- `;
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
+ }
27
40
 
28
41
  // インストール対象パッケージ
29
42
  // en: Packages to install
@@ -183,13 +196,16 @@ function buildInstructionBlock(target, assistant) {
183
196
  BLOCK_START,
184
197
  heading,
185
198
  '',
186
- '- **必ず読む**: Sparkle Design のコンポーネントを使う前に、インストール済みパッケージの型定義 `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` を必ず読んでください。Prop 仕様・使用例・アンチパターンは JSDoc に書かれている(✅ / ❌ 例込み)ので、これが Source of Truth です。`node_modules/sparkle-design/dist/` や `node_modules/@goodpatch/sparkle-design-internal/dist/` を対象に、`index.d.ts` の JSDoc まで読み切ること。',
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.',
187
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.",
188
- '- **必ず実行**: UI コンポーネントを作成・変更した後は、終了前に `lint:sparkle` を実行してください。Sparkle Design のアンチパターンを検出します。',
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).',
189
206
  '- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
190
207
  `- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
191
- '- JSON 出力の `findings` に加えて、**`manualReviewReminders` の内容も必ず 1 項目ずつ確認**してください。機械検出できない Badge/Tag の使い分けなどが含まれます。',
192
- '- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag etc.) that the linter cannot detect.',
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.',
193
209
  BLOCK_END,
194
210
  ].join('\n');
195
211
  }
@@ -377,7 +393,8 @@ function buildScaffoldTargets(cwd) {
377
393
  target: defaultGlobalsCssTarget(cwd),
378
394
  write: (abs) => {
379
395
  ensureDir(abs);
380
- 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');
381
398
  },
382
399
  },
383
400
  ];
@@ -439,12 +456,37 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
439
456
  const instructionBlock = buildInstructionBlock(target, options.assistant);
440
457
  const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
441
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
+
442
480
  if (!options.dryRun) {
443
481
  if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
444
482
  if (instructionResult.changed) {
445
483
  ensureDir(instructionPath);
446
484
  fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
447
485
  }
486
+ if (agentsInstructionResult?.changed && agentsInstructionPath) {
487
+ ensureDir(agentsInstructionPath);
488
+ fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
489
+ }
448
490
  }
449
491
 
450
492
  return {
@@ -453,16 +495,279 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
453
495
  instructionPath,
454
496
  packageResult,
455
497
  instructionResult,
498
+ agentsInstructionPath,
499
+ agentsInstructionResult,
500
+ };
501
+ }
502
+
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,
456
621
  };
457
622
  }
458
623
 
459
- function runGenerate({ skipGenerate, dryRun }) {
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
+ }
460
757
  if (skipGenerate || dryRun) return { skipped: true, ran: false };
461
758
  try {
462
759
  console.log('🎨 sparkle-design.css を生成中...');
463
- generateCSS();
464
- return { skipped: false, ran: true };
760
+ const result = generateCSS(null, null, null, { strict: Boolean(strict) });
761
+ return { skipped: false, ran: true, globalsResult: result?.globalsResult };
465
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
+ }
466
771
  console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
467
772
  return { skipped: false, ran: false, error: error.message };
468
773
  }
@@ -493,7 +798,18 @@ export function setupAssistant(options = {}) {
493
798
  { ...options, assistant, dryRun },
494
799
  assistantConfig
495
800
  );
496
- 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
+ });
497
813
 
498
814
  const summary = {
499
815
  assistant,
@@ -519,6 +835,24 @@ export function setupAssistant(options = {}) {
519
835
  changed: guard.instructionResult.changed,
520
836
  existed: guard.instructionResult.existed,
521
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,
522
856
  };
523
857
 
524
858
  console.log(JSON.stringify(summary, null, 2));
@@ -526,7 +860,7 @@ export function setupAssistant(options = {}) {
526
860
  // セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
527
861
  // en: Post-setup reminder. Written to stderr so it stays in the AI transcript
528
862
  // without polluting the JSON stdout payload that tooling parses.
529
- printPostSetupReminder(guard.target, packageManager);
863
+ printPostSetupReminder(guard.target, packageManager, { hook });
530
864
 
531
865
  return summary;
532
866
  }
@@ -548,7 +882,13 @@ function buildLintCommand(packageManager) {
548
882
  }
549
883
  }
550
884
 
551
- 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 } = {}) {
552
892
  const lintTarget = target || 'src';
553
893
  const lintCmd = buildLintCommand(packageManager);
554
894
  const lines = [
@@ -560,8 +900,31 @@ function printPostSetupReminder(target, packageManager) {
560
900
  ' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
561
901
  ' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
562
902
  ' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
563
- '',
564
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('');
565
928
  for (const line of lines) {
566
929
  console.error(line);
567
930
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.0.7-beta.1",
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",