sparkle-design-cli 2.0.7-beta.4 → 2.0.7-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/setup.js +148 -7
  2. package/package.json +1 -1
package/lib/setup.js CHANGED
@@ -183,13 +183,11 @@ function buildInstructionBlock(target, assistant) {
183
183
  BLOCK_START,
184
184
  heading,
185
185
  '',
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 まで読み切ること。',
186
+ '- **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
187
  "- **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 のアンチパターンを検出します。',
189
188
  '- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
190
189
  `- 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.',
190
+ '- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag, etc.) that the linter cannot detect.',
193
191
  BLOCK_END,
194
192
  ].join('\n');
195
193
  }
@@ -439,12 +437,33 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
439
437
  const instructionBlock = buildInstructionBlock(target, options.assistant);
440
438
  const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
441
439
 
440
+ // --assistant claude の場合は CLAUDE.md に加えて AGENTS.md にも Guard を書く。
441
+ // create-next-app 等が生成する AGENTS.md に Next.js 固有指示が残っていて、
442
+ // CLAUDE.md から `@AGENTS.md` で import しているプロジェクトでは、Sparkle
443
+ // Design Guard が AGENTS.md 側に無いと import 経由で読ませる動線が切れる。
444
+ // 他の assistant は本来の AGENTS.md / .cursor/rules だけに書く。
445
+ // en: When --assistant=claude, also upsert Guard into AGENTS.md so projects
446
+ // that reference `@AGENTS.md` from CLAUDE.md (common Next.js setup) still
447
+ // propagate the Guard via that import path.
448
+ let agentsInstructionResult = null;
449
+ let agentsInstructionPath = null;
450
+ if (options.assistant === 'claude' && !options.instructionsPath) {
451
+ agentsInstructionPath = path.resolve(cwd, 'AGENTS.md');
452
+ if (agentsInstructionPath !== instructionPath) {
453
+ agentsInstructionResult = updateInstructionFile(agentsInstructionPath, instructionBlock);
454
+ }
455
+ }
456
+
442
457
  if (!options.dryRun) {
443
458
  if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
444
459
  if (instructionResult.changed) {
445
460
  ensureDir(instructionPath);
446
461
  fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
447
462
  }
463
+ if (agentsInstructionResult?.changed && agentsInstructionPath) {
464
+ ensureDir(agentsInstructionPath);
465
+ fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
466
+ }
448
467
  }
449
468
 
450
469
  return {
@@ -453,6 +472,95 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
453
472
  instructionPath,
454
473
  packageResult,
455
474
  instructionResult,
475
+ agentsInstructionPath,
476
+ agentsInstructionResult,
477
+ };
478
+ }
479
+
480
+ /**
481
+ * .claude/settings.json に lint:sparkle を強制実行する Stop hook を追加する。
482
+ *
483
+ * 設計:
484
+ * - Stop hook: Claude が応答を終えようとする直前に実行される hook。exit 2 を返すと
485
+ * 停止がブロックされ、Claude が lint findings を修正してから停止する動線になる。
486
+ * `sparkle-design-cli check ... --strict || exit 2` で findings があれば exit 2。
487
+ * - 既存の .claude/settings.json を破壊しないよう、現行の hooks.Stop 配列を残しつつ
488
+ * 同一 command が無いときだけ append する(冪等)。
489
+ * - project-shared な settings.json に書くのでチームメンバーにも共有される。
490
+ * user-local な settings.local.json ではなく settings.json を選ぶのは、これが
491
+ * 全員に共通のガードレールであることを意図しているため。
492
+ *
493
+ * en: Write a Stop hook to `.claude/settings.json` that runs `lint:sparkle` and
494
+ * exits with code 2 on findings, which blocks Claude Code from finishing the
495
+ * turn until the linter passes. The existing file (including user-authored
496
+ * hooks) is preserved by merging; we only append our entry if an identical
497
+ * command is not already present.
498
+ */
499
+ function runClaudeHook(cwd, target, dryRun) {
500
+ const settingsPath = path.resolve(cwd, '.claude/settings.json');
501
+ const managedCommand = `npx --yes sparkle-design-cli check ${target} --strict || exit 2`;
502
+
503
+ let settings = {};
504
+ let existed = false;
505
+ if (fs.existsSync(settingsPath)) {
506
+ existed = true;
507
+ try {
508
+ settings = readJson(settingsPath);
509
+ } catch (error) {
510
+ // 壊れた JSON を黙って上書きしないようユーザーに明示的に判断を求める。
511
+ // en: Refuse to overwrite a broken settings.json silently.
512
+ throw new Error(
513
+ `.claude/settings.json が不正な JSON です (${error.message})。修正してから再実行してください。`
514
+ );
515
+ }
516
+ }
517
+
518
+ const nextSettings = typeof settings === 'object' && settings ? { ...settings } : {};
519
+ const hooks = typeof nextSettings.hooks === 'object' && nextSettings.hooks
520
+ ? { ...nextSettings.hooks }
521
+ : {};
522
+ const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
523
+
524
+ // 既存の Stop hook 配列に同一 command が含まれていれば冪等にスキップ。
525
+ // en: Skip if the managed command already appears in any Stop entry.
526
+ const alreadyPresent = stopGroups.some(
527
+ (group) =>
528
+ Array.isArray(group?.hooks) &&
529
+ group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
530
+ );
531
+
532
+ if (alreadyPresent) {
533
+ return {
534
+ changed: false,
535
+ existed,
536
+ settingsPath,
537
+ reason: 'already-present',
538
+ command: managedCommand,
539
+ };
540
+ }
541
+
542
+ stopGroups.push({
543
+ hooks: [
544
+ {
545
+ type: 'command',
546
+ command: managedCommand,
547
+ },
548
+ ],
549
+ });
550
+ hooks.Stop = stopGroups;
551
+ nextSettings.hooks = hooks;
552
+
553
+ if (!dryRun) {
554
+ ensureDir(settingsPath);
555
+ writeJson(settingsPath, nextSettings);
556
+ }
557
+
558
+ return {
559
+ changed: true,
560
+ existed,
561
+ settingsPath,
562
+ reason: existed ? 'appended' : 'created',
563
+ command: managedCommand,
456
564
  };
457
565
  }
458
566
 
@@ -511,6 +619,12 @@ export function setupAssistant(options = {}) {
511
619
  { ...options, assistant, dryRun },
512
620
  assistantConfig
513
621
  );
622
+ // --assistant claude のみ .claude/settings.json に Stop hook を自動設定する。
623
+ // これで lint:sparkle の実行が instruction 頼りではなく hook で強制される。
624
+ // en: For --assistant=claude, install a Stop hook in .claude/settings.json
625
+ // so lint:sparkle becomes a hard gate instead of a soft instruction.
626
+ const claudeHook =
627
+ assistant === 'claude' ? runClaudeHook(cwd, guard.target, dryRun) : null;
514
628
  const generate = runGenerate({
515
629
  skipGenerate: Boolean(options.skipGenerate),
516
630
  dryRun,
@@ -541,6 +655,21 @@ export function setupAssistant(options = {}) {
541
655
  changed: guard.instructionResult.changed,
542
656
  existed: guard.instructionResult.existed,
543
657
  },
658
+ agentsInstructions: guard.agentsInstructionPath
659
+ ? {
660
+ path: path.relative(cwd, guard.agentsInstructionPath),
661
+ changed: guard.agentsInstructionResult?.changed ?? false,
662
+ existed: guard.agentsInstructionResult?.existed ?? false,
663
+ }
664
+ : null,
665
+ claudeHook: claudeHook
666
+ ? {
667
+ path: path.relative(cwd, claudeHook.settingsPath),
668
+ changed: claudeHook.changed,
669
+ existed: claudeHook.existed,
670
+ reason: claudeHook.reason,
671
+ }
672
+ : null,
544
673
  };
545
674
 
546
675
  console.log(JSON.stringify(summary, null, 2));
@@ -548,7 +677,7 @@ export function setupAssistant(options = {}) {
548
677
  // セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
549
678
  // en: Post-setup reminder. Written to stderr so it stays in the AI transcript
550
679
  // without polluting the JSON stdout payload that tooling parses.
551
- printPostSetupReminder(guard.target, packageManager);
680
+ printPostSetupReminder(guard.target, packageManager, { claudeHook });
552
681
 
553
682
  return summary;
554
683
  }
@@ -570,7 +699,7 @@ function buildLintCommand(packageManager) {
570
699
  }
571
700
  }
572
701
 
573
- function printPostSetupReminder(target, packageManager) {
702
+ function printPostSetupReminder(target, packageManager, { claudeHook } = {}) {
574
703
  const lintTarget = target || 'src';
575
704
  const lintCmd = buildLintCommand(packageManager);
576
705
  const lines = [
@@ -582,8 +711,20 @@ function printPostSetupReminder(target, packageManager) {
582
711
  ' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
583
712
  ' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
584
713
  ' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
585
- '',
586
714
  ];
715
+ if (claudeHook?.changed || claudeHook?.reason === 'already-present') {
716
+ const label =
717
+ claudeHook.reason === 'already-present'
718
+ ? '(既存のまま)'
719
+ : claudeHook.reason === 'created'
720
+ ? '(新規作成)'
721
+ : '(既存設定に追記)';
722
+ lines.push(
723
+ ` 4. Claude Code の Stop hook を .claude/settings.json に設定しました ${label}。Claude が応答を終える直前に \`lint:sparkle --strict\` が走り、findings があれば exit 2 で停止がブロックされます。`,
724
+ ' Installed a Claude Code Stop hook in .claude/settings.json: `lint:sparkle --strict` runs before each turn ends and exit 2 blocks the stop until findings are resolved.'
725
+ );
726
+ }
727
+ lines.push('');
587
728
  for (const line of lines) {
588
729
  console.error(line);
589
730
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.0.7-beta.4",
3
+ "version": "2.0.7-beta.6",
4
4
  "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",