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.
- package/lib/setup.js +148 -7
- 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
|
-
'-
|
|
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
|
-
'-
|
|
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
|
}
|