sparkle-design-cli 2.0.7-beta.0 → 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 +52 -7
- package/lib/check.js +93 -11
- package/lib/constants.js +45 -3
- package/lib/font-manager.js +348 -58
- package/lib/generate-css.js +151 -15
- package/lib/setup.js +385 -28
- package/package.json +15 -5
package/lib/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { generateCSS } from './generate-css.js';
|
|
5
|
+
import { GLOBALS_CSS_CANDIDATES } from './constants.js';
|
|
5
6
|
|
|
6
7
|
// デフォルトの sparkle.config.json テンプレート
|
|
7
8
|
// en: Default sparkle.config.json template
|
|
@@ -19,10 +20,23 @@ const DEFAULT_SPARKLE_CONFIG = {
|
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
// 初期 globals.css のテンプレート
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|
|
26
40
|
|
|
27
41
|
// インストール対象パッケージ
|
|
28
42
|
// en: Packages to install
|
|
@@ -41,16 +55,6 @@ const config = {
|
|
|
41
55
|
export default config;
|
|
42
56
|
`;
|
|
43
57
|
|
|
44
|
-
// globals.css 候補パス(優先順)
|
|
45
|
-
// en: globals.css candidate paths in priority order
|
|
46
|
-
const GLOBALS_CSS_CANDIDATES = [
|
|
47
|
-
'src/app/globals.css',
|
|
48
|
-
'app/globals.css',
|
|
49
|
-
'src/globals.css',
|
|
50
|
-
'src/index.css',
|
|
51
|
-
'src/styles/globals.css',
|
|
52
|
-
];
|
|
53
|
-
|
|
54
58
|
const ASSISTANT_CONFIG = {
|
|
55
59
|
claude: {
|
|
56
60
|
path: 'CLAUDE.md',
|
|
@@ -192,12 +196,16 @@ function buildInstructionBlock(target, assistant) {
|
|
|
192
196
|
BLOCK_START,
|
|
193
197
|
heading,
|
|
194
198
|
'',
|
|
195
|
-
'-
|
|
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.',
|
|
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.",
|
|
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).',
|
|
196
206
|
'- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
|
|
197
207
|
`- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
|
|
198
|
-
'-
|
|
199
|
-
'- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag etc.) that the linter cannot detect.',
|
|
200
|
-
'- If semantic guidance is still needed, consult Sparkle Design docs and JSDoc examples.',
|
|
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.',
|
|
201
209
|
BLOCK_END,
|
|
202
210
|
].join('\n');
|
|
203
211
|
}
|
|
@@ -385,7 +393,8 @@ function buildScaffoldTargets(cwd) {
|
|
|
385
393
|
target: defaultGlobalsCssTarget(cwd),
|
|
386
394
|
write: (abs) => {
|
|
387
395
|
ensureDir(abs);
|
|
388
|
-
|
|
396
|
+
const targetRel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
397
|
+
fs.writeFileSync(abs, buildInitialGlobalsCss(targetRel), 'utf8');
|
|
389
398
|
},
|
|
390
399
|
},
|
|
391
400
|
];
|
|
@@ -447,12 +456,37 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
447
456
|
const instructionBlock = buildInstructionBlock(target, options.assistant);
|
|
448
457
|
const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
|
|
449
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
|
+
|
|
450
480
|
if (!options.dryRun) {
|
|
451
481
|
if (packageResult.changed) writeJson(packageJsonPath, packageResult.packageJson);
|
|
452
482
|
if (instructionResult.changed) {
|
|
453
483
|
ensureDir(instructionPath);
|
|
454
484
|
fs.writeFileSync(instructionPath, instructionResult.content, 'utf8');
|
|
455
485
|
}
|
|
486
|
+
if (agentsInstructionResult?.changed && agentsInstructionPath) {
|
|
487
|
+
ensureDir(agentsInstructionPath);
|
|
488
|
+
fs.writeFileSync(agentsInstructionPath, agentsInstructionResult.content, 'utf8');
|
|
489
|
+
}
|
|
456
490
|
}
|
|
457
491
|
|
|
458
492
|
return {
|
|
@@ -461,16 +495,279 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
461
495
|
instructionPath,
|
|
462
496
|
packageResult,
|
|
463
497
|
instructionResult,
|
|
498
|
+
agentsInstructionPath,
|
|
499
|
+
agentsInstructionResult,
|
|
464
500
|
};
|
|
465
501
|
}
|
|
466
502
|
|
|
467
|
-
|
|
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,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
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
|
+
}
|
|
468
757
|
if (skipGenerate || dryRun) return { skipped: true, ran: false };
|
|
469
758
|
try {
|
|
470
759
|
console.log('🎨 sparkle-design.css を生成中...');
|
|
471
|
-
generateCSS();
|
|
472
|
-
return { skipped: false, ran: true };
|
|
760
|
+
const result = generateCSS(null, null, null, { strict: Boolean(strict) });
|
|
761
|
+
return { skipped: false, ran: true, globalsResult: result?.globalsResult };
|
|
473
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
|
+
}
|
|
474
771
|
console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
|
|
475
772
|
return { skipped: false, ran: false, error: error.message };
|
|
476
773
|
}
|
|
@@ -501,7 +798,18 @@ export function setupAssistant(options = {}) {
|
|
|
501
798
|
{ ...options, assistant, dryRun },
|
|
502
799
|
assistantConfig
|
|
503
800
|
);
|
|
504
|
-
|
|
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
|
+
});
|
|
505
813
|
|
|
506
814
|
const summary = {
|
|
507
815
|
assistant,
|
|
@@ -527,6 +835,24 @@ export function setupAssistant(options = {}) {
|
|
|
527
835
|
changed: guard.instructionResult.changed,
|
|
528
836
|
existed: guard.instructionResult.existed,
|
|
529
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,
|
|
530
856
|
};
|
|
531
857
|
|
|
532
858
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -534,7 +860,7 @@ export function setupAssistant(options = {}) {
|
|
|
534
860
|
// セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
|
|
535
861
|
// en: Post-setup reminder. Written to stderr so it stays in the AI transcript
|
|
536
862
|
// without polluting the JSON stdout payload that tooling parses.
|
|
537
|
-
printPostSetupReminder(guard.target, packageManager);
|
|
863
|
+
printPostSetupReminder(guard.target, packageManager, { hook });
|
|
538
864
|
|
|
539
865
|
return summary;
|
|
540
866
|
}
|
|
@@ -556,18 +882,49 @@ function buildLintCommand(packageManager) {
|
|
|
556
882
|
}
|
|
557
883
|
}
|
|
558
884
|
|
|
559
|
-
|
|
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 } = {}) {
|
|
560
892
|
const lintTarget = target || 'src';
|
|
561
893
|
const lintCmd = buildLintCommand(packageManager);
|
|
562
894
|
const lines = [
|
|
563
895
|
'',
|
|
564
896
|
'📝 次のステップ / Next steps:',
|
|
565
|
-
|
|
897
|
+
' 1. Sparkle Design のコンポーネントを使う前に、必ず `node_modules/<パッケージ名>/dist/components/ui/<コンポーネント名>/index.d.ts` の JSDoc を読んでください。Prop 仕様・使用例・アンチパターンは JSDoc が Source of Truth です。',
|
|
898
|
+
' Before using any Sparkle Design component, read `node_modules/<package>/dist/components/ui/<name>/index.d.ts` — the JSDoc includes prop specs, usage, and anti-pattern examples.',
|
|
899
|
+
` 2. UI コンポーネントを作成・変更したら、必ず \`${lintCmd}\` (または \`npx --yes sparkle-design-cli check ${lintTarget} --strict\`) を実行してください。`,
|
|
566
900
|
' After creating or modifying UI components, always run `lint:sparkle` to catch Sparkle Design anti-patterns.',
|
|
567
|
-
'
|
|
901
|
+
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
568
902
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
569
|
-
'',
|
|
570
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('');
|
|
571
928
|
for (const line of lines) {
|
|
572
929
|
console.error(line);
|
|
573
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",
|