sparkle-design-cli 2.0.7-beta.7 → 2.0.7-beta.9

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/constants.js CHANGED
@@ -44,8 +44,16 @@ export const REGEX = {
44
44
  /\/\*\s*フォントのインポート[^*]*\*\/\s*\n?(?:@import\s+(?:url\([^)]+fonts\.googleapis\.com[^)]+\)|['"][^"']*fonts\.googleapis\.com[^"']*['"]);?\s*\n?)*\n?/g,
45
45
 
46
46
  // Sparkle Design関連
47
+ // `@import "sparkle-design.css"` / `@import "./sparkle-design.css"` だけでなく
48
+ // `@import "./app/sparkle-design.css"` 等、任意の相対パスを含む variant も
49
+ // 除去対象にする。Vite のように entry CSS (src/index.css) と sparkle-design.css
50
+ // (src/app/sparkle-design.css) が別ディレクトリに置かれるレイアウトでは
51
+ // 後者のような path になるので、同一 dir 前提の regex だと AI が手で直したもの
52
+ // を除去できず二重 import が残ってしまう。
53
+ // en: Match any relative path ending in `sparkle-design.css`, including layouts
54
+ // like Vite where the entry CSS and sparkle-design.css live in different dirs.
47
55
  SPARKLE_IMPORT:
48
- /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"](\.\/)?sparkle-design\.css['"];?\s*\n?/g,
56
+ /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"][^"']*sparkle-design\.css['"];?\s*\n?/g,
49
57
  TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
50
58
 
51
59
  // @source ディレクティブ関連
@@ -137,6 +145,11 @@ export const COMMENTS = {
137
145
  };
138
146
 
139
147
  // インポート文のテンプレート
148
+ // SPARKLE_DESIGN は entry CSS と sparkle-design.css が同じディレクトリにある
149
+ // 場合のデフォルト。異なる場合は buildSparkleImportStatement() で相対 path を
150
+ // 計算して差し替える(Vite の src/index.css → src/app/sparkle-design.css 等)。
151
+ // en: Default when entry CSS and sparkle-design.css share a directory. Use
152
+ // buildSparkleImportStatement() for layouts that place them in different dirs.
140
153
  export const IMPORTS = {
141
154
  SPARKLE_DESIGN: '@import "./sparkle-design.css";',
142
155
  TAILWIND: '@import "tailwindcss";',
@@ -88,14 +88,44 @@ function createCustomCssImportBlock(customCssPath, globalsPath) {
88
88
  return [COMMENTS.CUSTOM_CSS, `@import "${relativePath}";`].join('\n');
89
89
  }
90
90
 
91
+ /**
92
+ * entry CSS (globals.css / index.css) から sparkle-design.css への相対 path を
93
+ * 計算し、`@import` 文を組み立てる。同一ディレクトリなら `./sparkle-design.css`、
94
+ * 異なるなら `./app/sparkle-design.css` のような相対 path になる。Vite の
95
+ * `src/index.css` → `src/app/sparkle-design.css` レイアウトで正しい path が
96
+ * 出るようにするために追加された関数(beta.7 まではハードコード)。
97
+ * en: Compute the relative path from the entry CSS to sparkle-design.css.
98
+ * Handles layouts like Vite's `src/index.css` → `src/app/sparkle-design.css`,
99
+ * where the two files live in different directories.
100
+ */
101
+ function buildSparkleImportStatement(globalsPath, sparkleDesignPath) {
102
+ if (!globalsPath || !sparkleDesignPath) {
103
+ return IMPORTS.SPARKLE_DESIGN;
104
+ }
105
+ const globalsDir = path.dirname(globalsPath);
106
+ const rel = path.relative(globalsDir, sparkleDesignPath).split(path.sep).join('/');
107
+ // path.relative が空文字を返すのは globalsPath === sparkleDesignPath のときのみ
108
+ // (想定外)。念のため fallback として同一 dir 前提の default import を返す。
109
+ // en: path.relative returns '' only when paths match; use the same-dir default.
110
+ if (!rel) return IMPORTS.SPARKLE_DESIGN;
111
+ const importPath = rel.startsWith('.') ? rel : `./${rel}`;
112
+ return `@import "${importPath}";`;
113
+ }
114
+
91
115
  /**
92
116
  * sparkle-design.css importブロックを生成する
93
117
  * @param {Array<string>} sourcePackages 追加パッケージ名の配列
94
118
  * @param {string} globalsPath globals.css の絶対パス(@source パス計算用)
95
119
  * @param {string|null} customCssPath custom-css ファイルの相対パス
120
+ * @param {string|null} sparkleDesignPath sparkle-design.css の絶対パス(相対 import 計算用)
96
121
  * @returns {string} sparkle-design.css importブロック
97
122
  */
98
- function createSparkleImportBlock(sourcePackages = [], globalsPath = null, customCssPath = null) {
123
+ function createSparkleImportBlock(
124
+ sourcePackages = [],
125
+ globalsPath = null,
126
+ customCssPath = null,
127
+ sparkleDesignPath = null
128
+ ) {
99
129
  const parts = [''];
100
130
 
101
131
  // CSS 仕様上 `@import` は `@charset` / `@layer` 以外の at-rule より前に
@@ -112,7 +142,10 @@ function createSparkleImportBlock(sourcePackages = [], globalsPath = null, custo
112
142
  // downstream `@import "./sparkle-design.css"` silently drop in some
113
143
  // PostCSS toolchains, which removes Sparkle tokens from the output.
114
144
  // We now emit @import blocks first, then any @source directives.
115
- parts.push(COMMENTS.SPARKLE_IMPORT, IMPORTS.SPARKLE_DESIGN);
145
+ parts.push(
146
+ COMMENTS.SPARKLE_IMPORT,
147
+ buildSparkleImportStatement(globalsPath, sparkleDesignPath)
148
+ );
116
149
 
117
150
  // カスタムCSS import を sparkle-design.css の後に配置
118
151
  const customBlock = createCustomCssImportBlock(customCssPath, globalsPath);
@@ -223,7 +256,8 @@ export function updateGlobalsWithFonts(
223
256
  fontImports,
224
257
  globalsPath,
225
258
  sourcePackages = null,
226
- customCssPath = null
259
+ customCssPath = null,
260
+ sparkleDesignPath = null
227
261
  ) {
228
262
  const hasFonts = fontImports.length > 0;
229
263
  const hasSourcePackages = sourcePackages !== null && sourcePackages !== undefined;
@@ -243,19 +277,42 @@ export function updateGlobalsWithFonts(
243
277
  // 2. 既存のimportを削除
244
278
  globalsContent = removeExistingImports(globalsContent);
245
279
 
246
- // 3. Tailwind import の位置を見つける
247
- const tailwindInfo = findTailwindImport(globalsContent);
280
+ // 3. Tailwind import の位置を見つける。無ければ先頭に自動で prepend する。
281
+ // ここで silent に失敗すると `create-vite` 等が生成した既存 index.css に
282
+ // `@import "tailwindcss"` が無いプロジェクトで setup が no-op に終わり、
283
+ // AI が後から手書きで import を補うが `@source` を知らず落とす、という
284
+ // 連鎖的な崩れが起きていた(beta.8 試用で確認)。canonical な import を
285
+ // 自動投入することでその連鎖を根絶する。
286
+ // en: If `@import "tailwindcss"` is missing from an existing entry CSS
287
+ // (e.g. untouched `create-vite` default), auto-prepend it instead of
288
+ // silently warning. Without this recovery, the whole patch becomes a no-op
289
+ // and AI ends up hand-writing imports without `@source`, producing drifted
290
+ // output (observed in beta.8 user testing).
291
+ let tailwindInfo = findTailwindImport(globalsContent);
248
292
  if (!tailwindInfo) {
249
- // 表示はプロジェクト相対パスにする。絶対パスだと「どのファイルをいじるのか」
250
- // が一目で分かりにくく、リポジトリ間で絶対パスが変わるので diff も読みにくい。
251
- // en: Show the project-relative path so it's obvious which file to edit.
252
293
  const displayPath = path.relative(process.cwd(), globalsPath) || globalsPath;
253
- console.warn(MESSAGES.TAILWIND_NOT_FOUND(displayPath));
254
- return { status: 'failed', reason: 'tailwind-import-missing' };
294
+ console.warn(
295
+ `⚠️ ${displayPath} に \`@import "tailwindcss";\` が見つからなかったため先頭に追記します(canonical な Tailwind v4 entry point にするため)。`
296
+ );
297
+ globalsContent = `${IMPORTS.TAILWIND}\n${globalsContent.startsWith('\n') ? '' : ''}${globalsContent}`;
298
+ tailwindInfo = findTailwindImport(globalsContent);
299
+ if (!tailwindInfo) {
300
+ // prepend したのに正規表現で拾えないのは `REGEX.TAILWIND_IMPORT` が
301
+ // 想定と違う(= コードの不整合)。fail-fast する。
302
+ // en: We just wrote the canonical import but the regex doesn't see it —
303
+ // this is an internal inconsistency, not a user error. Fail loud.
304
+ console.error(MESSAGES.TAILWIND_NOT_FOUND(displayPath));
305
+ return { status: 'failed', reason: 'tailwind-import-missing-after-prepend' };
306
+ }
255
307
  }
256
308
 
257
309
  // 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
258
- const sparkleImportBlock = createSparkleImportBlock(sourcePackages, globalsPath, customCssPath);
310
+ const sparkleImportBlock = createSparkleImportBlock(
311
+ sourcePackages,
312
+ globalsPath,
313
+ customCssPath,
314
+ sparkleDesignPath
315
+ );
259
316
 
260
317
  // 5. globals.css を再構築(フォント @import なし)
261
318
  const reconstructedContent = reconstructGlobalsCss(
@@ -412,11 +469,22 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
412
469
  const candidates = isViteProject(cwd)
413
470
  ? GLOBALS_CSS_CANDIDATES.slice().sort((a, b) => vitePriority(a) - vitePriority(b))
414
471
  : GLOBALS_CSS_CANDIDATES;
472
+ // 2-a) まず既知候補の中で `@import "tailwindcss"` を持つファイルを探す。
473
+ // 持っているファイルが第一候補(canonical Tailwind entry CSS)。
474
+ // 2-b) 2-a で見つからなかった場合、`@import` 無しで既存の候補ファイルが
475
+ // あればそれを拾う(Vite の `create-vite` 直後のように entry CSS は
476
+ // あるが Tailwind 未設定のプロジェクト)。これで上流の
477
+ // `updateGlobalsWithFonts` が auto-prepend で tailwind import を
478
+ // 補えるため、silent に no-op 終了する回帰を避けられる(beta.8 で再現)。
479
+ // en: First pick candidates that already contain `@import "tailwindcss"`.
480
+ // If none match, fall back to the first existing candidate anyway so the
481
+ // upstream patcher can auto-prepend the canonical tailwind import. Without
482
+ // this fallback, a Vite project created by `create-vite` with an untouched
483
+ // `src/index.css` would cause setup to end as a silent no-op.
484
+ let fallbackExistingPath = null;
415
485
  for (const candidate of candidates) {
416
486
  const absolute = path.resolve(cwd, candidate);
417
487
  if (!fs.existsSync(absolute)) continue;
418
- // 自身(sparkle-design.css)は除外
419
- // en: Skip sparkle-design.css itself just in case a candidate points at it.
420
488
  if (path.resolve(absolute) === path.resolve(sparkleDesignPath)) continue;
421
489
  try {
422
490
  const content = fs.readFileSync(absolute, 'utf8');
@@ -424,6 +492,9 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
424
492
  console.log(`📝 Tailwind エントリポイントを検出しました(project root): ${candidate}`);
425
493
  return { path: absolute, source: 'project' };
426
494
  }
495
+ if (!fallbackExistingPath) {
496
+ fallbackExistingPath = { path: absolute, candidate };
497
+ }
427
498
  } catch (err) {
428
499
  // ENOENT 系は existsSync で既に弾いているので、ここに来るのは権限不足や
429
500
  // ディレクトリ衝突などデバッグ価値のあるケース。silent に消さずに理由を出す。
@@ -435,6 +506,13 @@ function resolveGlobalsPath(sparkleDesignPath, explicitGlobalsPath = null) {
435
506
  }
436
507
  }
437
508
 
509
+ if (fallbackExistingPath) {
510
+ console.log(
511
+ `📝 Tailwind import 未設定の既存 entry CSS を検出しました(project root): ${fallbackExistingPath.candidate}。後続処理で \`@import "tailwindcss";\` を自動追記します。`
512
+ );
513
+ return { path: fallbackExistingPath.path, source: 'existing-no-tailwind' };
514
+ }
515
+
438
516
  // 3. デフォルト: sparkle-design.css と同じディレクトリの globals.css
439
517
  // en: Last resort — a globals.css next to sparkle-design.css.
440
518
  const defaultPath = path.join(dir, 'globals.css');
@@ -518,7 +596,19 @@ export function manageFontImports(
518
596
  }
519
597
 
520
598
  // 4. globals.css に import / @source を追加
521
- const result = updateGlobalsWithFonts(fontImports, globalsPath, sourcePackages, customCssPath);
599
+ // sparkle-design.css の絶対パスを渡すことで、globalsPath と異なる
600
+ // ディレクトリに置かれている場合でも相対 import が正しく計算される
601
+ // (Vite の src/index.css → src/app/sparkle-design.css 等)。
602
+ // en: Pass the absolute sparkle-design.css path so the import can be
603
+ // computed relative to the entry CSS, even in Vite-style layouts where
604
+ // the two files live in different directories.
605
+ const result = updateGlobalsWithFonts(
606
+ fontImports,
607
+ globalsPath,
608
+ sourcePackages,
609
+ customCssPath,
610
+ sparkleDesignPath
611
+ );
522
612
 
523
613
  if (result.status === 'skipped') {
524
614
  return result;
@@ -256,6 +256,92 @@ function writeSparkleHead(content, sparkleDesignCssPath) {
256
256
  console.log(' → ルートレイアウトの <head> 内に <SparkleHead /> を追加してください');
257
257
  }
258
258
 
259
+ const VITE_INDEX_HTML_BLOCK_START = '<!-- sparkle-design-cli:fonts:start -->';
260
+ const VITE_INDEX_HTML_BLOCK_END = '<!-- sparkle-design-cli:fonts:end -->';
261
+
262
+ /**
263
+ * SparkleHead.tsx と同じフォント <link> タグ群を HTML 文字列として生成する。
264
+ * Vite のように `<head>` が `index.html` にある環境では、React コンポーネントを
265
+ * 挟めないため HTML に直接注入する必要がある。
266
+ * en: Return the font/link tags as HTML strings so we can inject them into
267
+ * Vite's `index.html`, where a React SparkleHead component can't reach the head.
268
+ */
269
+ function buildSparkleFontLinksHtml(resolvedFonts) {
270
+ const materialSymbolsUrl = extractUrlFromImport(FONT_DEFAULTS.MATERIAL_SYMBOLS_IMPORT);
271
+ const fontImportLines = generateMergedFontImports([...resolvedFonts.pro, ...resolvedFonts.mono]);
272
+ const fontUrls = fontImportLines.map(extractUrlFromImport).filter(Boolean);
273
+ return [
274
+ `<link rel="preconnect" href="${FONT_DOMAINS.GOOGLEAPIS}" />`,
275
+ `<link rel="preconnect" href="${FONT_DOMAINS.GSTATIC}" crossorigin />`,
276
+ `<link rel="stylesheet" href="${materialSymbolsUrl}" />`,
277
+ ...fontUrls.map((url) => `<link rel="stylesheet" href="${url}" />`),
278
+ ];
279
+ }
280
+
281
+ /**
282
+ * Vite プロジェクトの `index.html` に managed block で Sparkle のフォント
283
+ * `<link>` タグを upsert する。存在しない場合は `</head>` 直前に挿入、
284
+ * 既にマーカー付きブロックがあれば内容を置換する。
285
+ *
286
+ * - Vite 以外(Next.js の src/app など)は index.html を持たないので呼ばれない
287
+ * - 既存ユーザーの head 内に書いた自作の link は触らない(managed block の
288
+ * 範囲だけを管理するので non-destructive)
289
+ *
290
+ * en: Upsert a managed block of Sparkle font `<link>` tags into Vite's
291
+ * `index.html`, injecting before `</head>` on first run and replacing the
292
+ * block content on re-runs. Non-destructive against user-authored links
293
+ * outside the markers.
294
+ */
295
+ function upsertViteIndexHtmlFonts(cwd, resolvedFonts) {
296
+ const indexHtmlPath = path.resolve(cwd, 'index.html');
297
+ if (!fs.existsSync(indexHtmlPath)) {
298
+ return { status: 'skipped', reason: 'no-index-html' };
299
+ }
300
+ let html;
301
+ try {
302
+ html = fs.readFileSync(indexHtmlPath, 'utf8');
303
+ } catch (error) {
304
+ return { status: 'failed', reason: `read-error: ${error.code ?? error.message}` };
305
+ }
306
+
307
+ const links = buildSparkleFontLinksHtml(resolvedFonts);
308
+ // ブロック内部の各行は innerIndent で統一する。マーカー行自体も含める。
309
+ // en: Apply innerIndent to every line of the managed block, markers included.
310
+ const innerIndent = ' ';
311
+ const block = [VITE_INDEX_HTML_BLOCK_START, ...links, VITE_INDEX_HTML_BLOCK_END]
312
+ .map((line) => innerIndent + line)
313
+ .join('\n');
314
+
315
+ if (html.includes(VITE_INDEX_HTML_BLOCK_START) && html.includes(VITE_INDEX_HTML_BLOCK_END)) {
316
+ // 既存ブロックの各行インデントを尊重しつつ中身だけ置換すると複雑になるので、
317
+ // BLOCK_START の前にある空白を再利用してブロックごと丸ごと差し替える。
318
+ // en: Replace the entire managed block while preserving the leading
319
+ // whitespace that already sits in front of BLOCK_START.
320
+ const existingBlockRegex = new RegExp(
321
+ `([^\\S\\n]*)${VITE_INDEX_HTML_BLOCK_START}[\\s\\S]*?${VITE_INDEX_HTML_BLOCK_END}`
322
+ );
323
+ const replaced = html.replace(existingBlockRegex, (_match, leadingWs) => {
324
+ // 既存 leading を innerIndent に揃える。マーカー前の空白が行頭にある想定。
325
+ return [VITE_INDEX_HTML_BLOCK_START, ...links, VITE_INDEX_HTML_BLOCK_END]
326
+ .map((line, idx) => (idx === 0 ? leadingWs : innerIndent) + line)
327
+ .join('\n');
328
+ });
329
+ if (replaced === html) return { status: 'unchanged', path: indexHtmlPath };
330
+ fs.writeFileSync(indexHtmlPath, replaced, 'utf8');
331
+ return { status: 'updated', path: indexHtmlPath };
332
+ }
333
+
334
+ const headCloseRegex = /(\s*)<\/head>/i;
335
+ const match = html.match(headCloseRegex);
336
+ if (!match) {
337
+ return { status: 'failed', reason: 'no-head-close-tag' };
338
+ }
339
+ const leading = match[1]; // 通常 `\n ` (`</head>` 直前の改行とインデント)
340
+ const replaced = html.replace(headCloseRegex, `\n${block}${leading}</head>`);
341
+ fs.writeFileSync(indexHtmlPath, replaced, 'utf8');
342
+ return { status: 'created', path: indexHtmlPath };
343
+ }
344
+
259
345
  /**
260
346
  * テンプレート変数を設定値で置換する
261
347
  * @param {string} template CSSテンプレート
@@ -450,6 +536,27 @@ export function generateCSS(
450
536
  const sparkleHeadContent = generateSparkleHeadContent(resolvedFonts);
451
537
  writeSparkleHead(sparkleHeadContent, resolvedOutputPath);
452
538
 
539
+ // 7.5 Vite プロジェクトの index.html に Sparkle のフォント <link> タグを upsert
540
+ // する。Vite は <head> が index.html 側にあるため、React コンポーネントの
541
+ // SparkleHead を挟めない。managed block でマーカー間だけを管理し、既存の
542
+ // head 内容は非破壊に保つ。
543
+ // en: Vite ships its <head> in index.html, outside React's reach, so mirror
544
+ // the SparkleHead link tags into a managed block there.
545
+ const cwdForHtml = process.cwd();
546
+ if (fs.existsSync(path.resolve(cwdForHtml, 'index.html'))) {
547
+ const htmlResult = upsertViteIndexHtmlFonts(cwdForHtml, resolvedFonts);
548
+ if (htmlResult.status === 'created' || htmlResult.status === 'updated') {
549
+ const displayPath = path.relative(cwdForHtml, htmlResult.path) || htmlResult.path;
550
+ console.log(
551
+ `✅ index.html の Sparkle フォント <link> ブロックを${htmlResult.status === 'created' ? '挿入' : '更新'}しました: ${displayPath}`
552
+ );
553
+ } else if (htmlResult.status === 'failed') {
554
+ console.warn(
555
+ `⚠️ index.html への Sparkle フォント <link> 挿入に失敗しました (${htmlResult.reason})。手動で <head> 内に SparkleHead.tsx 相当の <link> タグを追加してください。`
556
+ );
557
+ }
558
+ }
559
+
453
560
  // 8. フォント管理の自動処理を実行(globals.css にはフォント @import を差し込まない)
454
561
  // source-packages は以下の合成で決まる:
455
562
  // 1. package.json から既知のデザインシステムパッケージを自動検出(baseline)
package/lib/setup.js CHANGED
@@ -20,10 +20,23 @@ const DEFAULT_SPARKLE_CONFIG = {
20
20
  };
21
21
 
22
22
  // 初期 globals.css のテンプレート
23
- // en: Initial globals.css template
24
- const INITIAL_GLOBALS_CSS = `@import "tailwindcss";
25
- @import "./sparkle-design.css";
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
@@ -185,6 +198,11 @@ function buildInstructionBlock(target, assistant) {
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.",
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).',
188
206
  '- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
189
207
  `- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
190
208
  '- You must inspect every entry in both `findings` and `manualReviewReminders` — the reminders flag judgment calls (Badge vs Tag, etc.) that the linter cannot detect.',
@@ -375,7 +393,8 @@ function buildScaffoldTargets(cwd) {
375
393
  target: defaultGlobalsCssTarget(cwd),
376
394
  write: (abs) => {
377
395
  ensureDir(abs);
378
- fs.writeFileSync(abs, INITIAL_GLOBALS_CSS, 'utf8');
396
+ const targetRel = path.relative(cwd, abs).split(path.sep).join('/');
397
+ fs.writeFileSync(abs, buildInitialGlobalsCss(targetRel), 'utf8');
379
398
  },
380
399
  },
381
400
  ];
@@ -519,6 +538,36 @@ function loadHookJson(filePath, label) {
519
538
  }
520
539
  }
521
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
+
522
571
  function runClaudeHook(cwd, target, dryRun) {
523
572
  const settingsPath = path.resolve(cwd, '.claude/settings.json');
524
573
  const managedCommand = buildManagedHookCommand(target);
@@ -538,6 +587,7 @@ function runClaudeHook(cwd, target, dryRun) {
538
587
  group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
539
588
  );
540
589
 
590
+ const mismatch = detectClaudeSessionRootMismatch(cwd);
541
591
  if (alreadyPresent) {
542
592
  return {
543
593
  assistant: 'claude',
@@ -546,6 +596,7 @@ function runClaudeHook(cwd, target, dryRun) {
546
596
  path: settingsPath,
547
597
  reason: 'already-present',
548
598
  command: managedCommand,
599
+ sessionRootMismatch: mismatch,
549
600
  };
550
601
  }
551
602
 
@@ -559,7 +610,6 @@ function runClaudeHook(cwd, target, dryRun) {
559
610
  ensureDir(settingsPath);
560
611
  writeJson(settingsPath, nextSettings);
561
612
  }
562
-
563
613
  return {
564
614
  assistant: 'claude',
565
615
  changed: true,
@@ -567,6 +617,7 @@ function runClaudeHook(cwd, target, dryRun) {
567
617
  path: settingsPath,
568
618
  reason: existed ? 'appended' : 'created',
569
619
  command: managedCommand,
620
+ sessionRootMismatch: mismatch,
570
621
  };
571
622
  }
572
623
 
@@ -799,6 +850,7 @@ export function setupAssistant(options = {}) {
799
850
  existed: hook.existed,
800
851
  reason: hook.reason,
801
852
  featureFlagNote: hook.featureFlagNote ?? null,
853
+ sessionRootMismatch: hook.sessionRootMismatch ?? null,
802
854
  }
803
855
  : null,
804
856
  };
@@ -864,6 +916,13 @@ function printPostSetupReminder(target, packageManager, { hook } = {}) {
864
916
  if (hook.featureFlagNote) {
865
917
  lines.push(` ⚠️ ${hook.featureFlagNote}`);
866
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
+ }
867
926
  }
868
927
  lines.push('');
869
928
  for (const line of lines) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "2.0.7-beta.7",
4
- "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
3
+ "version": "2.0.7-beta.9",
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
- "css",
24
+ "sparkle-design",
25
25
  "design-system",
26
- "css-generator",
27
- "theming"
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",