sparkle-design-cli 2.0.7-beta.6 → 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 +215 -74
- 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,19 +437,23 @@ 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
|
|
441
|
-
// create-next-app
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
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.
|
|
448
451
|
let agentsInstructionResult = null;
|
|
449
452
|
let agentsInstructionPath = null;
|
|
450
453
|
if (options.assistant === 'claude' && !options.instructionsPath) {
|
|
451
|
-
|
|
452
|
-
if (
|
|
454
|
+
const candidate = path.resolve(cwd, 'AGENTS.md');
|
|
455
|
+
if (candidate !== instructionPath && fs.existsSync(candidate)) {
|
|
456
|
+
agentsInstructionPath = candidate;
|
|
453
457
|
agentsInstructionResult = updateInstructionFile(agentsInstructionPath, instructionBlock);
|
|
454
458
|
}
|
|
455
459
|
}
|
|
@@ -478,51 +482,56 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
478
482
|
}
|
|
479
483
|
|
|
480
484
|
/**
|
|
481
|
-
*
|
|
485
|
+
* Agent 別の hook 設定ファイルに「lint:sparkle を強制実行する」hook を追加する。
|
|
482
486
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* 全員に共通のガードレールであることを意図しているため。
|
|
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 上書きせず、ユーザーに修正を促すエラーを出す
|
|
492
495
|
*
|
|
493
|
-
* en:
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
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
|
|
498
504
|
*/
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
`.claude/settings.json が不正な JSON です (${error.message})。修正してから再実行してください。`
|
|
514
|
-
);
|
|
515
|
-
}
|
|
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
|
+
);
|
|
516
519
|
}
|
|
520
|
+
}
|
|
517
521
|
|
|
518
|
-
|
|
519
|
-
const
|
|
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
|
+
: {};
|
|
522
533
|
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
523
534
|
|
|
524
|
-
// 既存の Stop hook 配列に同一 command が含まれていれば冪等にスキップ。
|
|
525
|
-
// en: Skip if the managed command already appears in any Stop entry.
|
|
526
535
|
const alreadyPresent = stopGroups.some(
|
|
527
536
|
(group) =>
|
|
528
537
|
Array.isArray(group?.hooks) &&
|
|
@@ -531,21 +540,17 @@ function runClaudeHook(cwd, target, dryRun) {
|
|
|
531
540
|
|
|
532
541
|
if (alreadyPresent) {
|
|
533
542
|
return {
|
|
543
|
+
assistant: 'claude',
|
|
534
544
|
changed: false,
|
|
535
545
|
existed,
|
|
536
|
-
settingsPath,
|
|
546
|
+
path: settingsPath,
|
|
537
547
|
reason: 'already-present',
|
|
538
548
|
command: managedCommand,
|
|
539
549
|
};
|
|
540
550
|
}
|
|
541
551
|
|
|
542
552
|
stopGroups.push({
|
|
543
|
-
hooks: [
|
|
544
|
-
{
|
|
545
|
-
type: 'command',
|
|
546
|
-
command: managedCommand,
|
|
547
|
-
},
|
|
548
|
-
],
|
|
553
|
+
hooks: [{ type: 'command', command: managedCommand }],
|
|
549
554
|
});
|
|
550
555
|
hooks.Stop = stopGroups;
|
|
551
556
|
nextSettings.hooks = hooks;
|
|
@@ -556,14 +561,137 @@ function runClaudeHook(cwd, target, dryRun) {
|
|
|
556
561
|
}
|
|
557
562
|
|
|
558
563
|
return {
|
|
564
|
+
assistant: 'claude',
|
|
559
565
|
changed: true,
|
|
560
566
|
existed,
|
|
561
|
-
settingsPath,
|
|
567
|
+
path: settingsPath,
|
|
562
568
|
reason: existed ? 'appended' : 'created',
|
|
563
569
|
command: managedCommand,
|
|
564
570
|
};
|
|
565
571
|
}
|
|
566
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,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
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
|
+
|
|
567
695
|
function runGenerate({ skipGenerate, dryRun, strict }) {
|
|
568
696
|
// --skip-generate / --dry-run と --strict が同時指定された場合、strict チェックを
|
|
569
697
|
// 走らせる対象 (generate) そのものがスキップされるため strict は事実上無意味。
|
|
@@ -619,12 +747,13 @@ export function setupAssistant(options = {}) {
|
|
|
619
747
|
{ ...options, assistant, dryRun },
|
|
620
748
|
assistantConfig
|
|
621
749
|
);
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
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);
|
|
628
757
|
const generate = runGenerate({
|
|
629
758
|
skipGenerate: Boolean(options.skipGenerate),
|
|
630
759
|
dryRun,
|
|
@@ -662,12 +791,14 @@ export function setupAssistant(options = {}) {
|
|
|
662
791
|
existed: guard.agentsInstructionResult?.existed ?? false,
|
|
663
792
|
}
|
|
664
793
|
: null,
|
|
665
|
-
|
|
794
|
+
hook: hook
|
|
666
795
|
? {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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,
|
|
671
802
|
}
|
|
672
803
|
: null,
|
|
673
804
|
};
|
|
@@ -677,7 +808,7 @@ export function setupAssistant(options = {}) {
|
|
|
677
808
|
// セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
|
|
678
809
|
// en: Post-setup reminder. Written to stderr so it stays in the AI transcript
|
|
679
810
|
// without polluting the JSON stdout payload that tooling parses.
|
|
680
|
-
printPostSetupReminder(guard.target, packageManager, {
|
|
811
|
+
printPostSetupReminder(guard.target, packageManager, { hook });
|
|
681
812
|
|
|
682
813
|
return summary;
|
|
683
814
|
}
|
|
@@ -699,7 +830,13 @@ function buildLintCommand(packageManager) {
|
|
|
699
830
|
}
|
|
700
831
|
}
|
|
701
832
|
|
|
702
|
-
|
|
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 } = {}) {
|
|
703
840
|
const lintTarget = target || 'src';
|
|
704
841
|
const lintCmd = buildLintCommand(packageManager);
|
|
705
842
|
const lines = [
|
|
@@ -712,17 +849,21 @@ function printPostSetupReminder(target, packageManager, { claudeHook } = {}) {
|
|
|
712
849
|
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
713
850
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
714
851
|
];
|
|
715
|
-
if (
|
|
852
|
+
if (hook && (hook.changed || hook.reason === 'already-present')) {
|
|
853
|
+
const meta = HOOK_LABELS[hook.assistant] ?? HOOK_LABELS.claude;
|
|
716
854
|
const label =
|
|
717
|
-
|
|
855
|
+
hook.reason === 'already-present'
|
|
718
856
|
? '(既存のまま)'
|
|
719
|
-
:
|
|
857
|
+
: hook.reason === 'created'
|
|
720
858
|
? '(新規作成)'
|
|
721
859
|
: '(既存設定に追記)';
|
|
722
860
|
lines.push(
|
|
723
|
-
` 4.
|
|
724
|
-
|
|
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.`
|
|
725
863
|
);
|
|
864
|
+
if (hook.featureFlagNote) {
|
|
865
|
+
lines.push(` ⚠️ ${hook.featureFlagNote}`);
|
|
866
|
+
}
|
|
726
867
|
}
|
|
727
868
|
lines.push('');
|
|
728
869
|
for (const line of lines) {
|