sparkle-design-cli 2.0.7-beta.6 → 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 +75 -10
- package/lib/generate-css.js +107 -0
- package/lib/setup.js +279 -79
- 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,22 +88,64 @@ 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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
// CSS 仕様上 `@import` は `@charset` / `@layer` 以外の at-rule より前に
|
|
132
|
+
// 書く必要がある。`@source` を `@import` より前に置くと、PostCSS 処理系に
|
|
133
|
+
// よっては後続の `@import "./sparkle-design.css"` が silent に drop され、
|
|
134
|
+
// sparkle tokens が最終 CSS に出ない回帰になる(beta.6 以前で再現)。
|
|
135
|
+
// そのため順序は:
|
|
136
|
+
// 1. @import "./sparkle-design.css" (Sparkle 本体のトークン)
|
|
137
|
+
// 2. @import "./custom-tokens.css" (ユーザーのカスタムトークン、任意)
|
|
138
|
+
// 3. @source "../node_modules/..." (Tailwind スキャン対象、任意)
|
|
139
|
+
// の順で出力する。
|
|
140
|
+
// en: Per CSS spec, all `@import` rules must precede any other at-rule
|
|
141
|
+
// (except `@charset` / `@layer`). Placing `@source` between imports makes
|
|
142
|
+
// downstream `@import "./sparkle-design.css"` silently drop in some
|
|
143
|
+
// PostCSS toolchains, which removes Sparkle tokens from the output.
|
|
144
|
+
// We now emit @import blocks first, then any @source directives.
|
|
145
|
+
parts.push(
|
|
146
|
+
COMMENTS.SPARKLE_IMPORT,
|
|
147
|
+
buildSparkleImportStatement(globalsPath, sparkleDesignPath)
|
|
148
|
+
);
|
|
107
149
|
|
|
108
150
|
// カスタムCSS import を sparkle-design.css の後に配置
|
|
109
151
|
const customBlock = createCustomCssImportBlock(customCssPath, globalsPath);
|
|
@@ -111,6 +153,11 @@ function createSparkleImportBlock(sourcePackages = [], globalsPath = null, custo
|
|
|
111
153
|
parts.push(customBlock);
|
|
112
154
|
}
|
|
113
155
|
|
|
156
|
+
if (sourcePackages !== null && sourcePackages !== undefined) {
|
|
157
|
+
const nodeModulesRel = globalsPath ? resolveNodeModulesRelPath(globalsPath) : '../node_modules';
|
|
158
|
+
parts.push(createSourceBlock(sourcePackages, nodeModulesRel));
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
parts.push('');
|
|
115
162
|
return parts.join('\n');
|
|
116
163
|
}
|
|
@@ -209,7 +256,8 @@ export function updateGlobalsWithFonts(
|
|
|
209
256
|
fontImports,
|
|
210
257
|
globalsPath,
|
|
211
258
|
sourcePackages = null,
|
|
212
|
-
customCssPath = null
|
|
259
|
+
customCssPath = null,
|
|
260
|
+
sparkleDesignPath = null
|
|
213
261
|
) {
|
|
214
262
|
const hasFonts = fontImports.length > 0;
|
|
215
263
|
const hasSourcePackages = sourcePackages !== null && sourcePackages !== undefined;
|
|
@@ -241,7 +289,12 @@ export function updateGlobalsWithFonts(
|
|
|
241
289
|
}
|
|
242
290
|
|
|
243
291
|
// 4. importブロックを生成(フォント @import は SparkleHead に移行したため生成しない)
|
|
244
|
-
const sparkleImportBlock = createSparkleImportBlock(
|
|
292
|
+
const sparkleImportBlock = createSparkleImportBlock(
|
|
293
|
+
sourcePackages,
|
|
294
|
+
globalsPath,
|
|
295
|
+
customCssPath,
|
|
296
|
+
sparkleDesignPath
|
|
297
|
+
);
|
|
245
298
|
|
|
246
299
|
// 5. globals.css を再構築(フォント @import なし)
|
|
247
300
|
const reconstructedContent = reconstructGlobalsCss(
|
|
@@ -504,7 +557,19 @@ export function manageFontImports(
|
|
|
504
557
|
}
|
|
505
558
|
|
|
506
559
|
// 4. globals.css に import / @source を追加
|
|
507
|
-
|
|
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
|
+
);
|
|
508
573
|
|
|
509
574
|
if (result.status === 'skipped') {
|
|
510
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
|
];
|
|
@@ -437,19 +456,23 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
437
456
|
const instructionBlock = buildInstructionBlock(target, options.assistant);
|
|
438
457
|
const instructionResult = updateInstructionFile(instructionPath, instructionBlock);
|
|
439
458
|
|
|
440
|
-
// --assistant claude
|
|
441
|
-
// create-next-app
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
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.
|
|
448
470
|
let agentsInstructionResult = null;
|
|
449
471
|
let agentsInstructionPath = null;
|
|
450
472
|
if (options.assistant === 'claude' && !options.instructionsPath) {
|
|
451
|
-
|
|
452
|
-
if (
|
|
473
|
+
const candidate = path.resolve(cwd, 'AGENTS.md');
|
|
474
|
+
if (candidate !== instructionPath && fs.existsSync(candidate)) {
|
|
475
|
+
agentsInstructionPath = candidate;
|
|
453
476
|
agentsInstructionResult = updateInstructionFile(agentsInstructionPath, instructionBlock);
|
|
454
477
|
}
|
|
455
478
|
}
|
|
@@ -478,74 +501,107 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
478
501
|
}
|
|
479
502
|
|
|
480
503
|
/**
|
|
481
|
-
*
|
|
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 上書きせず、ユーザーに修正を促すエラーを出す
|
|
482
514
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
* `sparkle-design-cli check ... --strict || exit 2` で findings があれば exit 2。
|
|
487
|
-
* - 既存の .claude/settings.json を破壊しないよう、現行の hooks.Stop 配列を残しつつ
|
|
488
|
-
* 同一 command が無いときだけ append する(冪等)。
|
|
489
|
-
* - project-shared な settings.json に書くのでチームメンバーにも共有される。
|
|
490
|
-
* user-local な settings.local.json ではなく settings.json を選ぶのは、これが
|
|
491
|
-
* 全員に共通のガードレールであることを意図しているため。
|
|
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.
|
|
492
518
|
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
* hooks
|
|
497
|
-
* command is not already present.
|
|
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
|
|
498
523
|
*/
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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;
|
|
516
568
|
}
|
|
569
|
+
}
|
|
517
570
|
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
: {};
|
|
522
582
|
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
523
583
|
|
|
524
|
-
// 既存の Stop hook 配列に同一 command が含まれていれば冪等にスキップ。
|
|
525
|
-
// en: Skip if the managed command already appears in any Stop entry.
|
|
526
584
|
const alreadyPresent = stopGroups.some(
|
|
527
585
|
(group) =>
|
|
528
586
|
Array.isArray(group?.hooks) &&
|
|
529
587
|
group.hooks.some((entry) => entry?.type === 'command' && entry?.command === managedCommand)
|
|
530
588
|
);
|
|
531
589
|
|
|
590
|
+
const mismatch = detectClaudeSessionRootMismatch(cwd);
|
|
532
591
|
if (alreadyPresent) {
|
|
533
592
|
return {
|
|
593
|
+
assistant: 'claude',
|
|
534
594
|
changed: false,
|
|
535
595
|
existed,
|
|
536
|
-
settingsPath,
|
|
596
|
+
path: settingsPath,
|
|
537
597
|
reason: 'already-present',
|
|
538
598
|
command: managedCommand,
|
|
599
|
+
sessionRootMismatch: mismatch,
|
|
539
600
|
};
|
|
540
601
|
}
|
|
541
602
|
|
|
542
603
|
stopGroups.push({
|
|
543
|
-
hooks: [
|
|
544
|
-
{
|
|
545
|
-
type: 'command',
|
|
546
|
-
command: managedCommand,
|
|
547
|
-
},
|
|
548
|
-
],
|
|
604
|
+
hooks: [{ type: 'command', command: managedCommand }],
|
|
549
605
|
});
|
|
550
606
|
hooks.Stop = stopGroups;
|
|
551
607
|
nextSettings.hooks = hooks;
|
|
@@ -554,16 +610,139 @@ function runClaudeHook(cwd, target, dryRun) {
|
|
|
554
610
|
ensureDir(settingsPath);
|
|
555
611
|
writeJson(settingsPath, nextSettings);
|
|
556
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
|
+
}
|
|
557
716
|
|
|
558
717
|
return {
|
|
718
|
+
assistant: 'codex',
|
|
559
719
|
changed: true,
|
|
560
720
|
existed,
|
|
561
|
-
|
|
721
|
+
path: hooksPath,
|
|
562
722
|
reason: existed ? 'appended' : 'created',
|
|
563
723
|
command: managedCommand,
|
|
724
|
+
featureFlagNote:
|
|
725
|
+
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
564
726
|
};
|
|
565
727
|
}
|
|
566
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
|
+
|
|
567
746
|
function runGenerate({ skipGenerate, dryRun, strict }) {
|
|
568
747
|
// --skip-generate / --dry-run と --strict が同時指定された場合、strict チェックを
|
|
569
748
|
// 走らせる対象 (generate) そのものがスキップされるため strict は事実上無意味。
|
|
@@ -619,12 +798,13 @@ export function setupAssistant(options = {}) {
|
|
|
619
798
|
{ ...options, assistant, dryRun },
|
|
620
799
|
assistantConfig
|
|
621
800
|
);
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
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);
|
|
628
808
|
const generate = runGenerate({
|
|
629
809
|
skipGenerate: Boolean(options.skipGenerate),
|
|
630
810
|
dryRun,
|
|
@@ -662,12 +842,15 @@ export function setupAssistant(options = {}) {
|
|
|
662
842
|
existed: guard.agentsInstructionResult?.existed ?? false,
|
|
663
843
|
}
|
|
664
844
|
: null,
|
|
665
|
-
|
|
845
|
+
hook: hook
|
|
666
846
|
? {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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,
|
|
671
854
|
}
|
|
672
855
|
: null,
|
|
673
856
|
};
|
|
@@ -677,7 +860,7 @@ export function setupAssistant(options = {}) {
|
|
|
677
860
|
// セットアップ後のリマインダー。AI の会話履歴にも残るよう stderr に出す。
|
|
678
861
|
// en: Post-setup reminder. Written to stderr so it stays in the AI transcript
|
|
679
862
|
// without polluting the JSON stdout payload that tooling parses.
|
|
680
|
-
printPostSetupReminder(guard.target, packageManager, {
|
|
863
|
+
printPostSetupReminder(guard.target, packageManager, { hook });
|
|
681
864
|
|
|
682
865
|
return summary;
|
|
683
866
|
}
|
|
@@ -699,7 +882,13 @@ function buildLintCommand(packageManager) {
|
|
|
699
882
|
}
|
|
700
883
|
}
|
|
701
884
|
|
|
702
|
-
|
|
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 } = {}) {
|
|
703
892
|
const lintTarget = target || 'src';
|
|
704
893
|
const lintCmd = buildLintCommand(packageManager);
|
|
705
894
|
const lines = [
|
|
@@ -712,17 +901,28 @@ function printPostSetupReminder(target, packageManager, { claudeHook } = {}) {
|
|
|
712
901
|
' 3. AI でレビューする場合は `lint:sparkle:json` を使い、`findings` に加えて `manualReviewReminders` の各項目まで必ず確認してください。',
|
|
713
902
|
' For AI review, use `lint:sparkle:json` and inspect every entry of both `findings` and `manualReviewReminders`.',
|
|
714
903
|
];
|
|
715
|
-
if (
|
|
904
|
+
if (hook && (hook.changed || hook.reason === 'already-present')) {
|
|
905
|
+
const meta = HOOK_LABELS[hook.assistant] ?? HOOK_LABELS.claude;
|
|
716
906
|
const label =
|
|
717
|
-
|
|
907
|
+
hook.reason === 'already-present'
|
|
718
908
|
? '(既存のまま)'
|
|
719
|
-
:
|
|
909
|
+
: hook.reason === 'created'
|
|
720
910
|
? '(新規作成)'
|
|
721
911
|
: '(既存設定に追記)';
|
|
722
912
|
lines.push(
|
|
723
|
-
` 4.
|
|
724
|
-
|
|
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.`
|
|
725
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
|
+
}
|
|
726
926
|
}
|
|
727
927
|
lines.push('');
|
|
728
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",
|