sparkle-design-cli 2.0.7-beta.7 → 2.0.7-beta.8
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 +14 -1
- package/lib/font-manager.js +56 -5
- package/lib/generate-css.js +107 -0
- package/lib/setup.js +65 -6
- package/package.json +15 -5
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+['"]
|
|
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";',
|
package/lib/font-manager.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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;
|
|
@@ -255,7 +289,12 @@ export function updateGlobalsWithFonts(
|
|
|
255
289
|
}
|
|
256
290
|
|
|
257
291
|
// 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
|
|
258
|
-
const sparkleImportBlock = createSparkleImportBlock(
|
|
292
|
+
const sparkleImportBlock = createSparkleImportBlock(
|
|
293
|
+
sourcePackages,
|
|
294
|
+
globalsPath,
|
|
295
|
+
customCssPath,
|
|
296
|
+
sparkleDesignPath
|
|
297
|
+
);
|
|
259
298
|
|
|
260
299
|
// 5. globals.css を再構築(フォント @import なし)
|
|
261
300
|
const reconstructedContent = reconstructGlobalsCss(
|
|
@@ -518,7 +557,19 @@ export function manageFontImports(
|
|
|
518
557
|
}
|
|
519
558
|
|
|
520
559
|
// 4. globals.css に import / @source を追加
|
|
521
|
-
|
|
560
|
+
// sparkle-design.css の絶対パスを渡すことで、globalsPath と異なる
|
|
561
|
+
// ディレクトリに置かれている場合でも相対 import が正しく計算される
|
|
562
|
+
// (Vite の src/index.css → src/app/sparkle-design.css 等)。
|
|
563
|
+
// en: Pass the absolute sparkle-design.css path so the import can be
|
|
564
|
+
// computed relative to the entry CSS, even in Vite-style layouts where
|
|
565
|
+
// the two files live in different directories.
|
|
566
|
+
const result = updateGlobalsWithFonts(
|
|
567
|
+
fontImports,
|
|
568
|
+
globalsPath,
|
|
569
|
+
sourcePackages,
|
|
570
|
+
customCssPath,
|
|
571
|
+
sparkleDesignPath
|
|
572
|
+
);
|
|
522
573
|
|
|
523
574
|
if (result.status === 'skipped') {
|
|
524
575
|
return result;
|
package/lib/generate-css.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"description": "Sparkle Design CSS
|
|
3
|
+
"version": "2.0.7-beta.8",
|
|
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",
|