sparkle-design-cli 2.0.7-beta.1 → 2.0.7-beta.10
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/README.md +135 -24
- package/bin/sparkle-design.js +12 -1
- package/lib/anti-pattern-rules.js +29 -15
- package/lib/check.js +93 -11
- package/lib/constants.js +19 -2
- package/lib/font-manager.js +188 -37
- package/lib/generate-css.js +151 -15
- package/lib/setup.js +379 -16
- package/package.json +15 -5
package/lib/setup.js
CHANGED
|
@@ -20,10 +20,23 @@ const DEFAULT_SPARKLE_CONFIG = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// 初期 globals.css のテンプレート
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// target のディレクトリと sparkle-design.css の想定出力先
|
|
24
|
+
// (`src/app/sparkle-design.css`) との相対関係で import path を切り替える。
|
|
25
|
+
// Next.js App Router (src/app/globals.css) なら同一 dir で `./sparkle-design.css`、
|
|
26
|
+
// Vite (src/index.css) なら 1 階層下なので `./app/sparkle-design.css` になる。
|
|
27
|
+
// 同一 dir 前提のハードコードで生成していた beta.7 以前は Vite レイアウトで
|
|
28
|
+
// path が切れ、後続 generate が相対 path を再計算しても scaffold 時点では
|
|
29
|
+
// 間違った path で書かれていたため、AI が手で直す副作用が出ていた。
|
|
30
|
+
// en: Render initial entry CSS with a path that actually resolves to
|
|
31
|
+
// `src/app/sparkle-design.css` given the target file location.
|
|
32
|
+
function buildInitialGlobalsCss(targetRelPath) {
|
|
33
|
+
const targetDir = path.dirname(targetRelPath);
|
|
34
|
+
const sparkleDesignRel = 'src/app/sparkle-design.css';
|
|
35
|
+
let rel = path.relative(targetDir, sparkleDesignRel).split(path.sep).join('/');
|
|
36
|
+
if (!rel) rel = 'sparkle-design.css';
|
|
37
|
+
if (!rel.startsWith('.')) rel = `./${rel}`;
|
|
38
|
+
return `@import "tailwindcss";\n@import "${rel}";\n`;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
// インストール対象パッケージ
|
|
29
42
|
// en: Packages to install
|
|
@@ -183,13 +196,16 @@ function buildInstructionBlock(target, assistant) {
|
|
|
183
196
|
BLOCK_START,
|
|
184
197
|
heading,
|
|
185
198
|
'',
|
|
186
|
-
'-
|
|
199
|
+
'- **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
200
|
"- **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
|
-
'-
|
|
201
|
+
'- **Structural invariants — verify before hand-editing any Sparkle file**:',
|
|
202
|
+
' - Entry CSS (e.g. `src/index.css`, `src/app/globals.css`) must start with `@import "tailwindcss";` and keep **all `@import` statements before any `@source` directive**. CSS-spec-compliant processors silently drop any `@import` that follows another at-rule, which removes Sparkle tokens from the output.',
|
|
203
|
+
' - Sparkle fonts (Google Fonts preconnect + Material Symbols + the configured pro/mono fonts) must be present in the document `<head>`: React layouts via `<SparkleHead />` placed inside `<head>` in the root layout, Vite projects via the managed `<!-- sparkle-design-cli:fonts:start -->` … `end` block in `index.html`.',
|
|
204
|
+
' - Do **not** hand-define `--color-primary-*` or other Sparkle tokens as fallbacks when they look missing. Those come from `sparkle-design.css`; missing values mean the `@import` path is wrong (e.g. `./sparkle-design.css` used when the file lives under `./app/`). Fix the import path, do not duplicate the tokens.',
|
|
205
|
+
' - If you detect any of the above drifted, re-run `npx --yes sparkle-design-cli generate` before hand-editing. The CLI restores the canonical state (correct relative paths, `@source` placement, `index.html` injection).',
|
|
189
206
|
'- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
|
|
190
207
|
`- 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.',
|
|
208
|
+
'- You must inspect every entry in both `findings` and `manualReviewReminders`. For each entry in `manualReviewReminders`, **echo the reminder ID in your final reply together with a short review note** (e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts, so the usage is fine.`). Silence on a reminder means it was skipped; always state at least a one-line judgment per ID. This is not blocked by the Stop hook (reminders are judgment calls), but reviewers / humans rely on the explicit acknowledgment to trust that each item was actually considered.',
|
|
193
209
|
BLOCK_END,
|
|
194
210
|
].join('\n');
|
|
195
211
|
}
|
|
@@ -377,7 +393,8 @@ function buildScaffoldTargets(cwd) {
|
|
|
377
393
|
target: defaultGlobalsCssTarget(cwd),
|
|
378
394
|
write: (abs) => {
|
|
379
395
|
ensureDir(abs);
|
|
380
|
-
|
|
396
|
+
const targetRel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
397
|
+
fs.writeFileSync(abs, buildInitialGlobalsCss(targetRel), 'utf8');
|
|
381
398
|
},
|
|
382
399
|
},
|
|
383
400
|
];
|
|
@@ -439,12 +456,37 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
439
456
|
const instructionBlock = buildInstructionBlock(target, options.assistant);
|
|
440
457
|
const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
|
|
441
458
|
|
|
459
|
+
// --assistant claude のときに、プロジェクトルートに既存の AGENTS.md が
|
|
460
|
+
// 「すでにある」場合だけ Guard を追記する。create-next-app などが先行生成
|
|
461
|
+
// した AGENTS.md や、ユーザーが Codex/Gemini 併用のために置いているファイル
|
|
462
|
+
// には courtesy として Guard を載せる一方、存在しない AGENTS.md を
|
|
463
|
+
// Sparkle 側が勝手に作ることはしない(「指定していない AI 指示書が
|
|
464
|
+
// あったら書く」という緩い方針)。
|
|
465
|
+
// en: When --assistant=claude, also upsert Guard into AGENTS.md **only if
|
|
466
|
+
// the file already exists**. This covers projects where create-next-app or
|
|
467
|
+
// another tool shipped an AGENTS.md (or where the user keeps one for
|
|
468
|
+
// Codex/Gemini), without the CLI unilaterally creating a file the user
|
|
469
|
+
// didn't opt into.
|
|
470
|
+
let agentsInstructionResult = null;
|
|
471
|
+
let agentsInstructionPath = null;
|
|
472
|
+
if (options.assistant === 'claude' && !options.instructionsPath) {
|
|
473
|
+
const candidate = path.resolve(cwd, 'AGENTS.md');
|
|
474
|
+
if (candidate !== instructionPath && fs.existsSync(candidate)) {
|
|
475
|
+
agentsInstructionPath = candidate;
|
|
476
|
+
agentsInstructionResult = updateInstructionFile(agentsInstructionPath, instructionBlock);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
442
480
|
if (!options.dryRun) {
|
|
443
481
|
if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
|
|
444
482
|
if (instructionResult.changed) {
|
|
445
483
|
ensureDir(instructionPath);
|
|
446
484
|
fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
|
|
447
485
|
}
|
|
486
|
+
if (agentsInstructionResult?.changed && agentsInstructionPath) {
|
|
487
|
+
ensureDir(agentsInstructionPath);
|
|
488
|
+
fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
|
|
489
|
+
}
|
|
448
490
|
}
|
|
449
491
|
|
|
450
492
|
return {
|
|
@@ -453,16 +495,279 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
453
495
|
instructionPath,
|
|
454
496
|
packageResult,
|
|
455
497
|
instructionResult,
|
|
498
|
+
agentsInstructionPath,
|
|
499
|
+
agentsInstructionResult,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Agent 別の hook 設定ファイルに「lint:sparkle を強制実行する」hook を追加する。
|
|
505
|
+
*
|
|
506
|
+
* 各 agent が持つ hook システムの schema は異なるため、agent ごとに以下の
|
|
507
|
+
* ヘルパーを持つ。共通ポイントは:
|
|
508
|
+
* - 実行コマンドは `npx --yes sparkle-design-cli check <target> --strict || exit 2`
|
|
509
|
+
* - exit 2 にエスカレーションすることで、ブロック対応する agent では
|
|
510
|
+
* 応答終了を止めて findings 修正に向かわせる
|
|
511
|
+
* - 既存の hook 設定ファイルは非破壊マージし、同一 command が含まれていれば skip
|
|
512
|
+
* (冪等)
|
|
513
|
+
* - 壊れた JSON は silent 上書きせず、ユーザーに修正を促すエラーを出す
|
|
514
|
+
*
|
|
515
|
+
* en: Install an agent-specific hook that runs `lint:sparkle --strict || exit 2`.
|
|
516
|
+
* Each agent ships its own hook config format; these helpers emit the right
|
|
517
|
+
* shape while preserving existing user content and staying idempotent on rerun.
|
|
518
|
+
*
|
|
519
|
+
* References (2026-04 時点):
|
|
520
|
+
* - Claude Code : https://docs.claude.com/en/docs/claude-code/hooks
|
|
521
|
+
* - Cursor : https://cursor.com/docs/hooks
|
|
522
|
+
* - Codex : https://developers.openai.com/codex/hooks
|
|
523
|
+
*/
|
|
524
|
+
function buildManagedHookCommand(target) {
|
|
525
|
+
return `npx --yes sparkle-design-cli check ${target} --strict || exit 2`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function loadHookJson(filePath, label) {
|
|
529
|
+
if (!fs.existsSync(filePath)) {
|
|
530
|
+
return { existed: false, config: null };
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
return { existed: true, config: readJson(filePath) };
|
|
534
|
+
} catch (error) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`${label} が不正な JSON です (${error.message})。修正してから再実行してください。`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Claude Code の Stop hook を読んでくれるのは「Claude を起動した作業ディレクトリ」
|
|
543
|
+
* 直下の `.claude/settings.json` であって、cwd とは限らない。ユーザーが親フォルダ
|
|
544
|
+
* で Claude を起動してからサブディレクトリに `cd` してこの setup を実行した
|
|
545
|
+
* 場合、`.claude/settings.json` はサブディレクトリに置かれるが Claude は親側を
|
|
546
|
+
* 見ているので hook が効かない、という失敗モードがある。
|
|
547
|
+
*
|
|
548
|
+
* Claude Code が Bash tool 経由で環境変数 `CLAUDE_PROJECT_DIR` を設定するので、
|
|
549
|
+
* それを検出して cwd と違えば「hook が効かない可能性あり」と警告する。環境変数
|
|
550
|
+
* が無い場合(別の agent から呼ばれている / Claude Code 以外)は警告なし。
|
|
551
|
+
*
|
|
552
|
+
* en: Claude Code loads `.claude/settings.json` from the directory where it was
|
|
553
|
+
* launched, not the current working directory. If the user started Claude at a
|
|
554
|
+
* parent folder and ran `sparkle-design-cli setup` inside a subdirectory, the
|
|
555
|
+
* hook lands in a place Claude won't read. Detect this mismatch via the
|
|
556
|
+
* `CLAUDE_PROJECT_DIR` env var that Claude Code exports into shell tools.
|
|
557
|
+
*/
|
|
558
|
+
function detectClaudeSessionRootMismatch(cwd) {
|
|
559
|
+
const sessionDir = process.env.CLAUDE_PROJECT_DIR;
|
|
560
|
+
if (!sessionDir) return null;
|
|
561
|
+
try {
|
|
562
|
+
const resolvedSession = fs.realpathSync(path.resolve(sessionDir));
|
|
563
|
+
const resolvedCwd = fs.realpathSync(path.resolve(cwd));
|
|
564
|
+
if (resolvedSession === resolvedCwd) return null;
|
|
565
|
+
return { sessionDir: resolvedSession, cwd: resolvedCwd };
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function runClaudeHook(cwd, target, dryRun) {
|
|
572
|
+
const settingsPath = path.resolve(cwd, '.claude/settings.json');
|
|
573
|
+
const managedCommand = buildManagedHookCommand(target);
|
|
574
|
+
const { existed, config } = loadHookJson(settingsPath, '.claude/settings.json');
|
|
575
|
+
|
|
576
|
+
const nextSettings =
|
|
577
|
+
typeof config === 'object' && config ? { ...config } : {};
|
|
578
|
+
const hooks =
|
|
579
|
+
typeof nextSettings.hooks === 'object' && nextSettings.hooks
|
|
580
|
+
? { ...nextSettings.hooks }
|
|
581
|
+
: {};
|
|
582
|
+
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
583
|
+
|
|
584
|
+
const alreadyPresent = stopGroups.some(
|
|
585
|
+
(group) =>
|
|
586
|
+
Array.isArray(group?.hooks) &&
|
|
587
|
+
group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const mismatch = detectClaudeSessionRootMismatch(cwd);
|
|
591
|
+
if (alreadyPresent) {
|
|
592
|
+
return {
|
|
593
|
+
assistant: 'claude',
|
|
594
|
+
changed: false,
|
|
595
|
+
existed,
|
|
596
|
+
path: settingsPath,
|
|
597
|
+
reason: 'already-present',
|
|
598
|
+
command: managedCommand,
|
|
599
|
+
sessionRootMismatch: mismatch,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
stopGroups.push({
|
|
604
|
+
hooks: [{ type: 'command', command: managedCommand }],
|
|
605
|
+
});
|
|
606
|
+
hooks.Stop = stopGroups;
|
|
607
|
+
nextSettings.hooks = hooks;
|
|
608
|
+
|
|
609
|
+
if (!dryRun) {
|
|
610
|
+
ensureDir(settingsPath);
|
|
611
|
+
writeJson(settingsPath, nextSettings);
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
assistant: 'claude',
|
|
615
|
+
changed: true,
|
|
616
|
+
existed,
|
|
617
|
+
path: settingsPath,
|
|
618
|
+
reason: existed ? 'appended' : 'created',
|
|
619
|
+
command: managedCommand,
|
|
620
|
+
sessionRootMismatch: mismatch,
|
|
456
621
|
};
|
|
457
622
|
}
|
|
458
623
|
|
|
459
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Cursor 1.7+ の hook 設定(`.cursor/hooks.json`)に stop hook を追加する。
|
|
626
|
+
* schema: `{ version: 1, hooks: { stop: [{ command: "..." }] } }`
|
|
627
|
+
*/
|
|
628
|
+
function runCursorHook(cwd, target, dryRun) {
|
|
629
|
+
const hooksPath = path.resolve(cwd, '.cursor/hooks.json');
|
|
630
|
+
const managedCommand = buildManagedHookCommand(target);
|
|
631
|
+
const { existed, config } = loadHookJson(hooksPath, '.cursor/hooks.json');
|
|
632
|
+
|
|
633
|
+
const nextConfig = typeof config === 'object' && config ? { ...config } : {};
|
|
634
|
+
if (!nextConfig.version) nextConfig.version = 1;
|
|
635
|
+
const hooks =
|
|
636
|
+
typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
|
|
637
|
+
const stopEntries = Array.isArray(hooks.stop) ? [...hooks.stop] : [];
|
|
638
|
+
|
|
639
|
+
const alreadyPresent = stopEntries.some((entry) => entry?.command === managedCommand);
|
|
640
|
+
if (alreadyPresent) {
|
|
641
|
+
return {
|
|
642
|
+
assistant: 'cursor',
|
|
643
|
+
changed: false,
|
|
644
|
+
existed,
|
|
645
|
+
path: hooksPath,
|
|
646
|
+
reason: 'already-present',
|
|
647
|
+
command: managedCommand,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
stopEntries.push({ command: managedCommand });
|
|
652
|
+
hooks.stop = stopEntries;
|
|
653
|
+
nextConfig.hooks = hooks;
|
|
654
|
+
|
|
655
|
+
if (!dryRun) {
|
|
656
|
+
ensureDir(hooksPath);
|
|
657
|
+
writeJson(hooksPath, nextConfig);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
assistant: 'cursor',
|
|
662
|
+
changed: true,
|
|
663
|
+
existed,
|
|
664
|
+
path: hooksPath,
|
|
665
|
+
reason: existed ? 'appended' : 'created',
|
|
666
|
+
command: managedCommand,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Codex の hook 設定(`.codex/hooks.json`)に Stop hook を追加する。
|
|
672
|
+
* schema は Claude とほぼ同じネスト構造。
|
|
673
|
+
* 有効化には `~/.codex/config.toml` に `[features] codex_hooks = true` が必要。
|
|
674
|
+
*/
|
|
675
|
+
function runCodexHook(cwd, target, dryRun) {
|
|
676
|
+
const hooksPath = path.resolve(cwd, '.codex/hooks.json');
|
|
677
|
+
const managedCommand = buildManagedHookCommand(target);
|
|
678
|
+
const { existed, config } = loadHookJson(hooksPath, '.codex/hooks.json');
|
|
679
|
+
|
|
680
|
+
const nextConfig = typeof config === 'object' && config ? { ...config } : {};
|
|
681
|
+
const hooks =
|
|
682
|
+
typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
|
|
683
|
+
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
684
|
+
|
|
685
|
+
const alreadyPresent = stopGroups.some(
|
|
686
|
+
(group) =>
|
|
687
|
+
Array.isArray(group?.hooks) &&
|
|
688
|
+
group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
if (alreadyPresent) {
|
|
692
|
+
return {
|
|
693
|
+
assistant: 'codex',
|
|
694
|
+
changed: false,
|
|
695
|
+
existed,
|
|
696
|
+
path: hooksPath,
|
|
697
|
+
reason: 'already-present',
|
|
698
|
+
command: managedCommand,
|
|
699
|
+
// 有効化にはユーザー側の opt-in が必要な旨を summary に載せる。
|
|
700
|
+
// en: Codex hooks are behind an opt-in feature flag; surface that in summary.
|
|
701
|
+
featureFlagNote:
|
|
702
|
+
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
stopGroups.push({
|
|
707
|
+
hooks: [{ type: 'command', command: managedCommand }],
|
|
708
|
+
});
|
|
709
|
+
hooks.Stop = stopGroups;
|
|
710
|
+
nextConfig.hooks = hooks;
|
|
711
|
+
|
|
712
|
+
if (!dryRun) {
|
|
713
|
+
ensureDir(hooksPath);
|
|
714
|
+
writeJson(hooksPath, nextConfig);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
assistant: 'codex',
|
|
719
|
+
changed: true,
|
|
720
|
+
existed,
|
|
721
|
+
path: hooksPath,
|
|
722
|
+
reason: existed ? 'appended' : 'created',
|
|
723
|
+
command: managedCommand,
|
|
724
|
+
featureFlagNote:
|
|
725
|
+
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* --assistant 値から該当 hook writer に dispatch する。generic は hook なし。
|
|
731
|
+
* en: Dispatch to the appropriate hook writer for the selected assistant.
|
|
732
|
+
*/
|
|
733
|
+
function runAssistantHook(assistant, cwd, target, dryRun) {
|
|
734
|
+
switch (assistant) {
|
|
735
|
+
case 'claude':
|
|
736
|
+
return runClaudeHook(cwd, target, dryRun);
|
|
737
|
+
case 'cursor':
|
|
738
|
+
return runCursorHook(cwd, target, dryRun);
|
|
739
|
+
case 'codex':
|
|
740
|
+
return runCodexHook(cwd, target, dryRun);
|
|
741
|
+
default:
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function runGenerate({ skipGenerate, dryRun, strict }) {
|
|
747
|
+
// --skip-generate / --dry-run と --strict が同時指定された場合、strict チェックを
|
|
748
|
+
// 走らせる対象 (generate) そのものがスキップされるため strict は事実上無意味。
|
|
749
|
+
// サイレントに無効化するとユーザーが CI で気付けないので警告を出す。
|
|
750
|
+
// en: --strict has no effect if generate is skipped — warn the user rather
|
|
751
|
+
// than silently no-op, which would mask CI misconfiguration.
|
|
752
|
+
if (strict && (skipGenerate || dryRun)) {
|
|
753
|
+
console.warn(
|
|
754
|
+
'⚠️ --strict は generate の失敗のみ検出します。--skip-generate / --dry-run と併用した場合は strict チェックは走りません。'
|
|
755
|
+
);
|
|
756
|
+
}
|
|
460
757
|
if (skipGenerate || dryRun) return { skipped: true, ran: false };
|
|
461
758
|
try {
|
|
462
759
|
console.log('🎨 sparkle-design.css を生成中...');
|
|
463
|
-
generateCSS();
|
|
464
|
-
return { skipped: false, ran: true };
|
|
760
|
+
const result = generateCSS(null, null, null, { strict: Boolean(strict) });
|
|
761
|
+
return { skipped: false, ran: true, globalsResult: result?.globalsResult };
|
|
465
762
|
} catch (error) {
|
|
763
|
+
// strict モード、または sparkle.config.json の `globals-path` typo などの
|
|
764
|
+
// 明示指定 not-found は、非 strict でもユーザーが必ず気付くべきなので
|
|
765
|
+
// 再 throw する(#33 の挙動を setup 経由でも保つ)。
|
|
766
|
+
// en: Always re-throw explicit --globals-path typos so `setup` surfaces
|
|
767
|
+
// them via exit code, matching the behaviour of `generate` directly.
|
|
768
|
+
if (strict || error.code === 'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND') {
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
466
771
|
console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
|
|
467
772
|
return { skipped: false, ran: false, error: error.message };
|
|
468
773
|
}
|
|
@@ -493,7 +798,18 @@ export function setupAssistant(options = {}) {
|
|
|
493
798
|
{ ...options, assistant, dryRun },
|
|
494
799
|
assistantConfig
|
|
495
800
|
);
|
|
496
|
-
|
|
801
|
+
// assistant 別に hook 設定ファイル(`.claude/settings.json` /
|
|
802
|
+
// `.cursor/hooks.json` / `.codex/hooks.json`)に `lint:sparkle --strict` を
|
|
803
|
+
// 走らせる stop / Stop hook を自動設定する。instruction 頼みではなく hook
|
|
804
|
+
// で強制することで lint:sparkle の実行漏れを防ぐ。generic は hook なし。
|
|
805
|
+
// en: For supported assistants, install a stop hook so `lint:sparkle --strict`
|
|
806
|
+
// becomes a hard gate. Generic assistant has no hook target.
|
|
807
|
+
const hook = runAssistantHook(assistant, cwd, guard.target, dryRun);
|
|
808
|
+
const generate = runGenerate({
|
|
809
|
+
skipGenerate: Boolean(options.skipGenerate),
|
|
810
|
+
dryRun,
|
|
811
|
+
strict: Boolean(options.strict),
|
|
812
|
+
});
|
|
497
813
|
|
|
498
814
|
const summary = {
|
|
499
815
|
assistant,
|
|
@@ -519,6 +835,24 @@ export function setupAssistant(options = {}) {
|
|
|
519
835
|
changed: guard.instructionResult.changed,
|
|
520
836
|
existed: guard.instructionResult.existed,
|
|
521
837
|
},
|
|
838
|
+
agentsInstructions: guard.agentsInstructionPath
|
|
839
|
+
? {
|
|
840
|
+
path: path.relative(cwd, guard.agentsInstructionPath),
|
|
841
|
+
changed: guard.agentsInstructionResult?.changed ?? false,
|
|
842
|
+
existed: guard.agentsInstructionResult?.existed ?? false,
|
|
843
|
+
}
|
|
844
|
+
: null,
|
|
845
|
+
hook: hook
|
|
846
|
+
? {
|
|
847
|
+
assistant: hook.assistant,
|
|
848
|
+
path: path.relative(cwd, hook.path),
|
|
849
|
+
changed: hook.changed,
|
|
850
|
+
existed: hook.existed,
|
|
851
|
+
reason: hook.reason,
|
|
852
|
+
featureFlagNote: hook.featureFlagNote ?? null,
|
|
853
|
+
sessionRootMismatch: hook.sessionRootMismatch ?? null,
|
|
854
|
+
}
|
|
855
|
+
: null,
|
|
522
856
|
};
|
|
523
857
|
|
|
524
858
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -526,7 +860,7 @@ export function setupAssistant(options = {}) {
|
|
|
526
860
|
// セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
|
|
527
861
|
// en: Post-setup reminder. Written to stderr so it stays in the AI transcript
|
|
528
862
|
// without polluting the JSON stdout payload that tooling parses.
|
|
529
|
-
printPostSetupReminder(guard.target, packageManager);
|
|
863
|
+
printPostSetupReminder(guard.target, packageManager, { hook });
|
|
530
864
|
|
|
531
865
|
return summary;
|
|
532
866
|
}
|
|
@@ -548,7 +882,13 @@ function buildLintCommand(packageManager) {
|
|
|
548
882
|
}
|
|
549
883
|
}
|
|
550
884
|
|
|
551
|
-
|
|
885
|
+
const HOOK_LABELS = {
|
|
886
|
+
claude: { name: 'Claude Code', file: '.claude/settings.json', event: 'Stop' },
|
|
887
|
+
cursor: { name: 'Cursor', file: '.cursor/hooks.json', event: 'stop' },
|
|
888
|
+
codex: { name: 'Codex', file: '.codex/hooks.json', event: 'Stop' },
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
function printPostSetupReminder(target, packageManager, { hook } = {}) {
|
|
552
892
|
const lintTarget = target || 'src';
|
|
553
893
|
const lintCmd = buildLintCommand(packageManager);
|
|
554
894
|
const lines = [
|
|
@@ -560,8 +900,31 @@ function printPostSetupReminder(target, packageManager) {
|
|
|
560
900
|
' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
|
|
561
901
|
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
562
902
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
563
|
-
'',
|
|
564
903
|
];
|
|
904
|
+
if (hook && (hook.changed || hook.reason === 'already-present')) {
|
|
905
|
+
const meta = HOOK_LABELS[hook.assistant] ?? HOOK_LABELS.claude;
|
|
906
|
+
const label =
|
|
907
|
+
hook.reason === 'already-present'
|
|
908
|
+
? '(既存のまま)'
|
|
909
|
+
: hook.reason === 'created'
|
|
910
|
+
? '(新規作成)'
|
|
911
|
+
: '(既存設定に追記)';
|
|
912
|
+
lines.push(
|
|
913
|
+
` 4. ${meta.name} の ${meta.event} hook を ${meta.file} に設定しました ${label}。応答を終える直前に \`lint:sparkle --strict\` が走り、findings があれば exit 2 で停止がブロックされます。`,
|
|
914
|
+
` 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.`
|
|
915
|
+
);
|
|
916
|
+
if (hook.featureFlagNote) {
|
|
917
|
+
lines.push(` ⚠️ ${hook.featureFlagNote}`);
|
|
918
|
+
}
|
|
919
|
+
if (hook.sessionRootMismatch) {
|
|
920
|
+
const { sessionDir, cwd: mismatchCwd } = hook.sessionRootMismatch;
|
|
921
|
+
lines.push(
|
|
922
|
+
` ⚠️ Claude Code は ${sessionDir} を session root として ${path.join(sessionDir, '.claude/settings.json')} を読みます。今回の hook は ${path.join(mismatchCwd, '.claude/settings.json')} に書かれたため、このまま だと hook が発火しません。Claude Code をこのディレクトリから再起動するか、同じ設定を session root の .claude/settings.json にコピーしてください。`,
|
|
923
|
+
` ⚠️ Claude Code reads \`.claude/settings.json\` from its session root, not cwd. The hook written to the current directory won't trigger — relaunch Claude from here, or copy the config to the session root.`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
lines.push('');
|
|
565
928
|
for (const line of lines) {
|
|
566
929
|
console.error(line);
|
|
567
930
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "2.0.7-beta.
|
|
4
|
-
"description": "Sparkle Design CSS
|
|
3
|
+
"version": "2.0.7-beta.10",
|
|
4
|
+
"description": "Sparkle Design CLI — プロジェクトセットアップ、CSS・フォント生成、アンチパターン検査、AI エージェント(Claude Code / Cursor / Codex)向けのガードと hook 設定まで一括で行う sparkle-design 公式 CLI。",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
7
7
|
"access": "public"
|
|
@@ -21,10 +21,20 @@
|
|
|
21
21
|
"sync:anti-pattern-docs": "node scripts/sync-anti-pattern-docs.mjs"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
|
-
"
|
|
24
|
+
"sparkle-design",
|
|
25
25
|
"design-system",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"cli",
|
|
27
|
+
"setup",
|
|
28
|
+
"scaffold",
|
|
29
|
+
"css",
|
|
30
|
+
"tailwindcss",
|
|
31
|
+
"theming",
|
|
32
|
+
"anti-pattern",
|
|
33
|
+
"linter",
|
|
34
|
+
"ai-agent",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"cursor",
|
|
37
|
+
"codex"
|
|
28
38
|
],
|
|
29
39
|
"author": "Goodpatch Inc. <sparkle-design@goodpatch.com>",
|
|
30
40
|
"license": "MIT",
|