sparkle-design-cli 2.0.7-beta.5 → 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 +146 -3
- package/package.json +1 -1
package/lib/setup.js
CHANGED
|
@@ -437,12 +437,33 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
437
437
|
const instructionBlock = buildInstructionBlock(target, options.assistant);
|
|
438
438
|
const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
|
|
439
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
|
+
|
|
440
457
|
if (!options.dryRun) {
|
|
441
458
|
if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
|
|
442
459
|
if (instructionResult.changed) {
|
|
443
460
|
ensureDir(instructionPath);
|
|
444
461
|
fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
|
|
445
462
|
}
|
|
463
|
+
if (agentsInstructionResult?.changed && agentsInstructionPath) {
|
|
464
|
+
ensureDir(agentsInstructionPath);
|
|
465
|
+
fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
|
|
466
|
+
}
|
|
446
467
|
}
|
|
447
468
|
|
|
448
469
|
return {
|
|
@@ -451,6 +472,95 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
451
472
|
instructionPath,
|
|
452
473
|
packageResult,
|
|
453
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,
|
|
454
564
|
};
|
|
455
565
|
}
|
|
456
566
|
|
|
@@ -509,6 +619,12 @@ export function setupAssistant(options = {}) {
|
|
|
509
619
|
{ ...options, assistant, dryRun },
|
|
510
620
|
assistantConfig
|
|
511
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;
|
|
512
628
|
const generate = runGenerate({
|
|
513
629
|
skipGenerate: Boolean(options.skipGenerate),
|
|
514
630
|
dryRun,
|
|
@@ -539,6 +655,21 @@ export function setupAssistant(options = {}) {
|
|
|
539
655
|
changed: guard.instructionResult.changed,
|
|
540
656
|
existed: guard.instructionResult.existed,
|
|
541
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,
|
|
542
673
|
};
|
|
543
674
|
|
|
544
675
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -546,7 +677,7 @@ export function setupAssistant(options = {}) {
|
|
|
546
677
|
// セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
|
|
547
678
|
// en: Post-setup reminder. Written to stderr so it stays in the AI transcript
|
|
548
679
|
// without polluting the JSON stdout payload that tooling parses.
|
|
549
|
-
printPostSetupReminder(guard.target, packageManager);
|
|
680
|
+
printPostSetupReminder(guard.target, packageManager, { claudeHook });
|
|
550
681
|
|
|
551
682
|
return summary;
|
|
552
683
|
}
|
|
@@ -568,7 +699,7 @@ function buildLintCommand(packageManager) {
|
|
|
568
699
|
}
|
|
569
700
|
}
|
|
570
701
|
|
|
571
|
-
function printPostSetupReminder(target, packageManager) {
|
|
702
|
+
function printPostSetupReminder(target, packageManager, { claudeHook } = {}) {
|
|
572
703
|
const lintTarget = target || 'src';
|
|
573
704
|
const lintCmd = buildLintCommand(packageManager);
|
|
574
705
|
const lines = [
|
|
@@ -580,8 +711,20 @@ function printPostSetupReminder(target, packageManager) {
|
|
|
580
711
|
' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
|
|
581
712
|
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
582
713
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
583
|
-
'',
|
|
584
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('');
|
|
585
728
|
for (const line of lines) {
|
|
586
729
|
console.error(line);
|
|
587
730
|
}
|