sparkle-design-cli 2.0.7-beta.5 → 2.0.7-beta.7
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/font-manager.js +19 -5
- package/lib/setup.js +287 -3
- package/package.json +1 -1
package/lib/font-manager.js
CHANGED
|
@@ -98,11 +98,20 @@ function createCustomCssImportBlock(customCssPath, globalsPath) {
|
|
|
98
98
|
function createSparkleImportBlock(sourcePackages = [], globalsPath = null, customCssPath = null) {
|
|
99
99
|
const parts = [''];
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
// CSS 仕様上 `@import` は `@charset` / `@layer` 以外の at-rule より前に
|
|
102
|
+
// 書く必要がある。`@source` を `@import` より前に置くと、PostCSS 処理系に
|
|
103
|
+
// よっては後続の `@import "./sparkle-design.css"` が silent に drop され、
|
|
104
|
+
// sparkle tokens が最終 CSS に出ない回帰になる(beta.6 以前で再現)。
|
|
105
|
+
// そのため順序は:
|
|
106
|
+
// 1. @import "./sparkle-design.css" (Sparkle 本体のトークン)
|
|
107
|
+
// 2. @import "./custom-tokens.css" (ユーザーのカスタムトークン、任意)
|
|
108
|
+
// 3. @source "../node_modules/..." (Tailwind スキャン対象、任意)
|
|
109
|
+
// の順で出力する。
|
|
110
|
+
// en: Per CSS spec, all `@import` rules must precede any other at-rule
|
|
111
|
+
// (except `@charset` / `@layer`). Placing `@source` between imports makes
|
|
112
|
+
// downstream `@import "./sparkle-design.css"` silently drop in some
|
|
113
|
+
// PostCSS toolchains, which removes Sparkle tokens from the output.
|
|
114
|
+
// We now emit @import blocks first, then any @source directives.
|
|
106
115
|
parts.push(COMMENTS.SPARKLE_IMPORT, IMPORTS.SPARKLE_DESIGN);
|
|
107
116
|
|
|
108
117
|
// カスタムCSS import を sparkle-design.css の後に配置
|
|
@@ -111,6 +120,11 @@ function createSparkleImportBlock(sourcePackages = [], globalsPath = null, custo
|
|
|
111
120
|
parts.push(customBlock);
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
if (sourcePackages !== null && sourcePackages !== undefined) {
|
|
124
|
+
const nodeModulesRel = globalsPath ? resolveNodeModulesRelPath(globalsPath) : '../node_modules';
|
|
125
|
+
parts.push(createSourceBlock(sourcePackages, nodeModulesRel));
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
parts.push('');
|
|
115
129
|
return parts.join('\n');
|
|
116
130
|
}
|
package/lib/setup.js
CHANGED
|
@@ -437,12 +437,37 @@ 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 のときに、プロジェクトルートに既存の AGENTS.md が
|
|
441
|
+
// 「すでにある」場合だけ Guard を追記する。create-next-app などが先行生成
|
|
442
|
+
// した AGENTS.md や、ユーザーが Codex/Gemini 併用のために置いているファイル
|
|
443
|
+
// には courtesy として Guard を載せる一方、存在しない AGENTS.md を
|
|
444
|
+
// Sparkle 側が勝手に作ることはしない(「指定していない AI 指示書が
|
|
445
|
+
// あったら書く」という緩い方針)。
|
|
446
|
+
// en: When --assistant=claude, also upsert Guard into AGENTS.md **only if
|
|
447
|
+
// the file already exists**. This covers projects where create-next-app or
|
|
448
|
+
// another tool shipped an AGENTS.md (or where the user keeps one for
|
|
449
|
+
// Codex/Gemini), without the CLI unilaterally creating a file the user
|
|
450
|
+
// didn't opt into.
|
|
451
|
+
let agentsInstructionResult = null;
|
|
452
|
+
let agentsInstructionPath = null;
|
|
453
|
+
if (options.assistant === 'claude' && !options.instructionsPath) {
|
|
454
|
+
const candidate = path.resolve(cwd, 'AGENTS.md');
|
|
455
|
+
if (candidate !== instructionPath && fs.existsSync(candidate)) {
|
|
456
|
+
agentsInstructionPath = candidate;
|
|
457
|
+
agentsInstructionResult = updateInstructionFile(agentsInstructionPath, instructionBlock);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
440
461
|
if (!options.dryRun) {
|
|
441
462
|
if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
|
|
442
463
|
if (instructionResult.changed) {
|
|
443
464
|
ensureDir(instructionPath);
|
|
444
465
|
fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
|
|
445
466
|
}
|
|
467
|
+
if (agentsInstructionResult?.changed && agentsInstructionPath) {
|
|
468
|
+
ensureDir(agentsInstructionPath);
|
|
469
|
+
fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
|
|
470
|
+
}
|
|
446
471
|
}
|
|
447
472
|
|
|
448
473
|
return {
|
|
@@ -451,9 +476,222 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
451
476
|
instructionPath,
|
|
452
477
|
packageResult,
|
|
453
478
|
instructionResult,
|
|
479
|
+
agentsInstructionPath,
|
|
480
|
+
agentsInstructionResult,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Agent 別の hook 設定ファイルに「lint:sparkle を強制実行する」hook を追加する。
|
|
486
|
+
*
|
|
487
|
+
* 各 agent が持つ hook システムの schema は異なるため、agent ごとに以下の
|
|
488
|
+
* ヘルパーを持つ。共通ポイントは:
|
|
489
|
+
* - 実行コマンドは `npx --yes sparkle-design-cli check <target> --strict || exit 2`
|
|
490
|
+
* - exit 2 にエスカレーションすることで、ブロック対応する agent では
|
|
491
|
+
* 応答終了を止めて findings 修正に向かわせる
|
|
492
|
+
* - 既存の hook 設定ファイルは非破壊マージし、同一 command が含まれていれば skip
|
|
493
|
+
* (冪等)
|
|
494
|
+
* - 壊れた JSON は silent 上書きせず、ユーザーに修正を促すエラーを出す
|
|
495
|
+
*
|
|
496
|
+
* en: Install an agent-specific hook that runs `lint:sparkle --strict || exit 2`.
|
|
497
|
+
* Each agent ships its own hook config format; these helpers emit the right
|
|
498
|
+
* shape while preserving existing user content and staying idempotent on rerun.
|
|
499
|
+
*
|
|
500
|
+
* References (2026-04 時点):
|
|
501
|
+
* - Claude Code : https://docs.claude.com/en/docs/claude-code/hooks
|
|
502
|
+
* - Cursor : https://cursor.com/docs/hooks
|
|
503
|
+
* - Codex : https://developers.openai.com/codex/hooks
|
|
504
|
+
*/
|
|
505
|
+
function buildManagedHookCommand(target) {
|
|
506
|
+
return `npx --yes sparkle-design-cli check ${target} --strict || exit 2`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function loadHookJson(filePath, label) {
|
|
510
|
+
if (!fs.existsSync(filePath)) {
|
|
511
|
+
return { existed: false, config: null };
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
return { existed: true, config: readJson(filePath) };
|
|
515
|
+
} catch (error) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`${label} が不正な JSON です (${error.message})。修正してから再実行してください。`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function runClaudeHook(cwd, target, dryRun) {
|
|
523
|
+
const settingsPath = path.resolve(cwd, '.claude/settings.json');
|
|
524
|
+
const managedCommand = buildManagedHookCommand(target);
|
|
525
|
+
const { existed, config } = loadHookJson(settingsPath, '.claude/settings.json');
|
|
526
|
+
|
|
527
|
+
const nextSettings =
|
|
528
|
+
typeof config === 'object' && config ? { ...config } : {};
|
|
529
|
+
const hooks =
|
|
530
|
+
typeof nextSettings.hooks === 'object' && nextSettings.hooks
|
|
531
|
+
? { ...nextSettings.hooks }
|
|
532
|
+
: {};
|
|
533
|
+
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
534
|
+
|
|
535
|
+
const alreadyPresent = stopGroups.some(
|
|
536
|
+
(group) =>
|
|
537
|
+
Array.isArray(group?.hooks) &&
|
|
538
|
+
group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
if (alreadyPresent) {
|
|
542
|
+
return {
|
|
543
|
+
assistant: 'claude',
|
|
544
|
+
changed: false,
|
|
545
|
+
existed,
|
|
546
|
+
path: settingsPath,
|
|
547
|
+
reason: 'already-present',
|
|
548
|
+
command: managedCommand,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
stopGroups.push({
|
|
553
|
+
hooks: [{ type: 'command', command: managedCommand }],
|
|
554
|
+
});
|
|
555
|
+
hooks.Stop = stopGroups;
|
|
556
|
+
nextSettings.hooks = hooks;
|
|
557
|
+
|
|
558
|
+
if (!dryRun) {
|
|
559
|
+
ensureDir(settingsPath);
|
|
560
|
+
writeJson(settingsPath, nextSettings);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
assistant: 'claude',
|
|
565
|
+
changed: true,
|
|
566
|
+
existed,
|
|
567
|
+
path: settingsPath,
|
|
568
|
+
reason: existed ? 'appended' : 'created',
|
|
569
|
+
command: managedCommand,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Cursor 1.7+ の hook 設定(`.cursor/hooks.json`)に stop hook を追加する。
|
|
575
|
+
* schema: `{ version: 1, hooks: { stop: [{ command: "..." }] } }`
|
|
576
|
+
*/
|
|
577
|
+
function runCursorHook(cwd, target, dryRun) {
|
|
578
|
+
const hooksPath = path.resolve(cwd, '.cursor/hooks.json');
|
|
579
|
+
const managedCommand = buildManagedHookCommand(target);
|
|
580
|
+
const { existed, config } = loadHookJson(hooksPath, '.cursor/hooks.json');
|
|
581
|
+
|
|
582
|
+
const nextConfig = typeof config === 'object' && config ? { ...config } : {};
|
|
583
|
+
if (!nextConfig.version) nextConfig.version = 1;
|
|
584
|
+
const hooks =
|
|
585
|
+
typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
|
|
586
|
+
const stopEntries = Array.isArray(hooks.stop) ? [...hooks.stop] : [];
|
|
587
|
+
|
|
588
|
+
const alreadyPresent = stopEntries.some((entry) => entry?.command === managedCommand);
|
|
589
|
+
if (alreadyPresent) {
|
|
590
|
+
return {
|
|
591
|
+
assistant: 'cursor',
|
|
592
|
+
changed: false,
|
|
593
|
+
existed,
|
|
594
|
+
path: hooksPath,
|
|
595
|
+
reason: 'already-present',
|
|
596
|
+
command: managedCommand,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
stopEntries.push({ command: managedCommand });
|
|
601
|
+
hooks.stop = stopEntries;
|
|
602
|
+
nextConfig.hooks = hooks;
|
|
603
|
+
|
|
604
|
+
if (!dryRun) {
|
|
605
|
+
ensureDir(hooksPath);
|
|
606
|
+
writeJson(hooksPath, nextConfig);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
assistant: 'cursor',
|
|
611
|
+
changed: true,
|
|
612
|
+
existed,
|
|
613
|
+
path: hooksPath,
|
|
614
|
+
reason: existed ? 'appended' : 'created',
|
|
615
|
+
command: managedCommand,
|
|
454
616
|
};
|
|
455
617
|
}
|
|
456
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Codex の hook 設定(`.codex/hooks.json`)に Stop hook を追加する。
|
|
621
|
+
* schema は Claude とほぼ同じネスト構造。
|
|
622
|
+
* 有効化には `~/.codex/config.toml` に `[features] codex_hooks = true` が必要。
|
|
623
|
+
*/
|
|
624
|
+
function runCodexHook(cwd, target, dryRun) {
|
|
625
|
+
const hooksPath = path.resolve(cwd, '.codex/hooks.json');
|
|
626
|
+
const managedCommand = buildManagedHookCommand(target);
|
|
627
|
+
const { existed, config } = loadHookJson(hooksPath, '.codex/hooks.json');
|
|
628
|
+
|
|
629
|
+
const nextConfig = typeof config === 'object' && config ? { ...config } : {};
|
|
630
|
+
const hooks =
|
|
631
|
+
typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
|
|
632
|
+
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
633
|
+
|
|
634
|
+
const alreadyPresent = stopGroups.some(
|
|
635
|
+
(group) =>
|
|
636
|
+
Array.isArray(group?.hooks) &&
|
|
637
|
+
group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
if (alreadyPresent) {
|
|
641
|
+
return {
|
|
642
|
+
assistant: 'codex',
|
|
643
|
+
changed: false,
|
|
644
|
+
existed,
|
|
645
|
+
path: hooksPath,
|
|
646
|
+
reason: 'already-present',
|
|
647
|
+
command: managedCommand,
|
|
648
|
+
// 有効化にはユーザー側の opt-in が必要な旨を summary に載せる。
|
|
649
|
+
// en: Codex hooks are behind an opt-in feature flag; surface that in summary.
|
|
650
|
+
featureFlagNote:
|
|
651
|
+
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
stopGroups.push({
|
|
656
|
+
hooks: [{ type: 'command', command: managedCommand }],
|
|
657
|
+
});
|
|
658
|
+
hooks.Stop = stopGroups;
|
|
659
|
+
nextConfig.hooks = hooks;
|
|
660
|
+
|
|
661
|
+
if (!dryRun) {
|
|
662
|
+
ensureDir(hooksPath);
|
|
663
|
+
writeJson(hooksPath, nextConfig);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
assistant: 'codex',
|
|
668
|
+
changed: true,
|
|
669
|
+
existed,
|
|
670
|
+
path: hooksPath,
|
|
671
|
+
reason: existed ? 'appended' : 'created',
|
|
672
|
+
command: managedCommand,
|
|
673
|
+
featureFlagNote:
|
|
674
|
+
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* --assistant 値から該当 hook writer に dispatch する。generic は hook なし。
|
|
680
|
+
* en: Dispatch to the appropriate hook writer for the selected assistant.
|
|
681
|
+
*/
|
|
682
|
+
function runAssistantHook(assistant, cwd, target, dryRun) {
|
|
683
|
+
switch (assistant) {
|
|
684
|
+
case 'claude':
|
|
685
|
+
return runClaudeHook(cwd, target, dryRun);
|
|
686
|
+
case 'cursor':
|
|
687
|
+
return runCursorHook(cwd, target, dryRun);
|
|
688
|
+
case 'codex':
|
|
689
|
+
return runCodexHook(cwd, target, dryRun);
|
|
690
|
+
default:
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
457
695
|
function runGenerate({ skipGenerate, dryRun, strict }) {
|
|
458
696
|
// --skip-generate / --dry-run と --strict が同時指定された場合、strict チェックを
|
|
459
697
|
// 走らせる対象 (generate) そのものがスキップされるため strict は事実上無意味。
|
|
@@ -509,6 +747,13 @@ export function setupAssistant(options = {}) {
|
|
|
509
747
|
{ ...options, assistant, dryRun },
|
|
510
748
|
assistantConfig
|
|
511
749
|
);
|
|
750
|
+
// assistant 別に hook 設定ファイル(`.claude/settings.json` /
|
|
751
|
+
// `.cursor/hooks.json` / `.codex/hooks.json`)に `lint:sparkle --strict` を
|
|
752
|
+
// 走らせる stop / Stop hook を自動設定する。instruction 頼みではなく hook
|
|
753
|
+
// で強制することで lint:sparkle の実行漏れを防ぐ。generic は hook なし。
|
|
754
|
+
// en: For supported assistants, install a stop hook so `lint:sparkle --strict`
|
|
755
|
+
// becomes a hard gate. Generic assistant has no hook target.
|
|
756
|
+
const hook = runAssistantHook(assistant, cwd, guard.target, dryRun);
|
|
512
757
|
const generate = runGenerate({
|
|
513
758
|
skipGenerate: Boolean(options.skipGenerate),
|
|
514
759
|
dryRun,
|
|
@@ -539,6 +784,23 @@ export function setupAssistant(options = {}) {
|
|
|
539
784
|
changed: guard.instructionResult.changed,
|
|
540
785
|
existed: guard.instructionResult.existed,
|
|
541
786
|
},
|
|
787
|
+
agentsInstructions: guard.agentsInstructionPath
|
|
788
|
+
? {
|
|
789
|
+
path: path.relative(cwd, guard.agentsInstructionPath),
|
|
790
|
+
changed: guard.agentsInstructionResult?.changed ?? false,
|
|
791
|
+
existed: guard.agentsInstructionResult?.existed ?? false,
|
|
792
|
+
}
|
|
793
|
+
: null,
|
|
794
|
+
hook: hook
|
|
795
|
+
? {
|
|
796
|
+
assistant: hook.assistant,
|
|
797
|
+
path: path.relative(cwd, hook.path),
|
|
798
|
+
changed: hook.changed,
|
|
799
|
+
existed: hook.existed,
|
|
800
|
+
reason: hook.reason,
|
|
801
|
+
featureFlagNote: hook.featureFlagNote ?? null,
|
|
802
|
+
}
|
|
803
|
+
: null,
|
|
542
804
|
};
|
|
543
805
|
|
|
544
806
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -546,7 +808,7 @@ export function setupAssistant(options = {}) {
|
|
|
546
808
|
// セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
|
|
547
809
|
// en: Post-setup reminder. Written to stderr so it stays in the AI transcript
|
|
548
810
|
// without polluting the JSON stdout payload that tooling parses.
|
|
549
|
-
printPostSetupReminder(guard.target, packageManager);
|
|
811
|
+
printPostSetupReminder(guard.target, packageManager, { hook });
|
|
550
812
|
|
|
551
813
|
return summary;
|
|
552
814
|
}
|
|
@@ -568,7 +830,13 @@ function buildLintCommand(packageManager) {
|
|
|
568
830
|
}
|
|
569
831
|
}
|
|
570
832
|
|
|
571
|
-
|
|
833
|
+
const HOOK_LABELS = {
|
|
834
|
+
claude: { name: 'Claude Code', file: '.claude/settings.json', event: 'Stop' },
|
|
835
|
+
cursor: { name: 'Cursor', file: '.cursor/hooks.json', event: 'stop' },
|
|
836
|
+
codex: { name: 'Codex', file: '.codex/hooks.json', event: 'Stop' },
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
function printPostSetupReminder(target, packageManager, { hook } = {}) {
|
|
572
840
|
const lintTarget = target || 'src';
|
|
573
841
|
const lintCmd = buildLintCommand(packageManager);
|
|
574
842
|
const lines = [
|
|
@@ -580,8 +848,24 @@ function printPostSetupReminder(target, packageManager) {
|
|
|
580
848
|
' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
|
|
581
849
|
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
582
850
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
583
|
-
'',
|
|
584
851
|
];
|
|
852
|
+
if (hook && (hook.changed || hook.reason === 'already-present')) {
|
|
853
|
+
const meta = HOOK_LABELS[hook.assistant] ?? HOOK_LABELS.claude;
|
|
854
|
+
const label =
|
|
855
|
+
hook.reason === 'already-present'
|
|
856
|
+
? '(既存のまま)'
|
|
857
|
+
: hook.reason === 'created'
|
|
858
|
+
? '(新規作成)'
|
|
859
|
+
: '(既存設定に追記)';
|
|
860
|
+
lines.push(
|
|
861
|
+
` 4. ${meta.name} の ${meta.event} hook を ${meta.file} に設定しました ${label}。応答を終える直前に \`lint:sparkle --strict\` が走り、findings があれば exit 2 で停止がブロックされます。`,
|
|
862
|
+
` 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.`
|
|
863
|
+
);
|
|
864
|
+
if (hook.featureFlagNote) {
|
|
865
|
+
lines.push(` ⚠️ ${hook.featureFlagNote}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
lines.push('');
|
|
585
869
|
for (const line of lines) {
|
|
586
870
|
console.error(line);
|
|
587
871
|
}
|