sparkle-design-cli 2.0.7-beta.8 → 2.0.7-rc.2

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 CHANGED
@@ -4,24 +4,13 @@ Sparkle Design のプロジェクト初期セットアップ、CSS 生成、導
4
4
 
5
5
  ## クイックスタート
6
6
 
7
- 既存の Next.js / Vite プロジェクトに Sparkle Design を導入する場合、以下 1 コマンドで完了します:
7
+ 既存の Next.js / Vite プロジェクトで以下を実行するだけで導入完了します。詳細は [`setup` セクション](#setup-プロジェクトのフルセットアップ) を参照してください。
8
8
 
9
9
  ```bash
10
10
  npx --yes sparkle-design-cli setup --assistant claude
11
11
  ```
12
12
 
13
- setup は次を自動で行います:
14
-
15
- 1. パッケージマネージャー(pnpm / npm / yarn / bun)を自動検出
16
- 2. `sparkle-design` を dependencies、`tailwindcss` + `@tailwindcss/postcss` を devDependencies に追加
17
- 3. 未作成の場合のみ初期ファイルを生成:
18
- - `sparkle.config.json`(デフォルトは blue / BIZ UDPGothic / BIZ UDGothic / md — sparkle-design 本体のデフォルトに合わせて統一)
19
- - `postcss.config.mjs`
20
- - Tailwind エントリ CSS — Next.js App Router なら `src/app/globals.css`、Vite なら `src/index.css`、それ以外は `src/globals.css`(プロジェクト構成から自動判定)
21
- 4. AI 指示ファイル(`CLAUDE.md` / `AGENTS.md` / Cursor rules)に Sparkle Design Guard ブロックを追加
22
- 5. `sparkle-design-cli generate` を実行し、`sparkle-design.css` と `SparkleHead.tsx` を生成
23
-
24
- 既存ファイルは上書きされません。
13
+ `--assistant` は `claude` / `cursor` / `codex` / `generic` から選べます。既存ファイルは上書きされません。
25
14
 
26
15
  ## インストール
27
16
 
@@ -33,22 +22,24 @@ npm install -g sparkle-design-cli
33
22
 
34
23
  ### リリースチャネル
35
24
 
36
- npm の dist-tag でチャネルを分離しています。通常は latest を使い、品質保証中の変更を試したい場合のみ beta を指定してください。
25
+ npm の dist-tag でチャネルを分けています。通常は `latest` を使い、RC や beta は先行試用時のみ指定してください。
37
26
 
38
- | チャネル | dist-tag | 用途 | 指定方法 |
39
- | -------- | -------- | ------------------------------------------------------------- | ----------------------------------- |
40
- | 安定版 | `latest` | 本番運用向け。`npx --yes sparkle-design-cli` は常にこれを取得 | (デフォルト) |
41
- | Beta | `beta` | 品質保証中の検証用 | `npx --yes sparkle-design-cli@beta` |
27
+ | チャネル | dist-tag | バージョン形式 | 用途 |
28
+ | --- | --- | --- | --- |
29
+ | 安定版 | `latest` | `X.Y.Z` | 本番運用向け(デフォルト) |
30
+ | Release Candidate | `next` | `X.Y.Z-rc.N` | GA 直前の検証、チーム展開前 |
31
+ | Beta | `beta` | `X.Y.Z-beta.N` | 品質保証中の検証用 |
42
32
 
43
33
  ```bash
44
- # beta で setup を試す
45
- npx --yes sparkle-design-cli@beta setup --assistant claude
34
+ # latest を使う(デフォルト)
35
+ npx --yes sparkle-design-cli setup --assistant claude
46
36
 
47
- # beta で check を試す
48
- npx --yes sparkle-design-cli@beta check src --strict
49
- ```
37
+ # RC を使う
38
+ npx --yes sparkle-design-cli@next setup --assistant claude
50
39
 
51
- Beta バージョンは `X.Y.Z-beta.N` 形式で publish されます。GA(正式版)へ昇格するときは、beta の -beta.N を外した `X.Y.Z` を改めて publish します。
40
+ # beta を使う
41
+ npx --yes sparkle-design-cli@beta setup --assistant claude
42
+ ```
52
43
 
53
44
  ## 使用方法
54
45
 
@@ -231,6 +222,111 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
231
222
 
232
223
  `setup` は通常実行時も JSON サマリーを stdout に表示します。`--dry-run` を付けると、その JSON を表示したままファイル変更だけを抑止します。
233
224
 
225
+ ### 手動セットアップ(Next.js / Vite 以外のプロジェクト向け)
226
+
227
+ `setup` コマンドは **Next.js App Router** と **Vite** を前提に scaffold / entry CSS の配置を自動判定します。Remix / Astro / TanStack Start など他のフレームワークや、独自ディレクトリ構成のプロジェクトでは自動判定が外れるため、以下の手順で手動セットアップすることを推奨します。
228
+
229
+ #### 手順
230
+
231
+ **1. パッケージをインストール**
232
+
233
+ ```bash
234
+ # 本体
235
+ pnpm add sparkle-design # or npm install / yarn add / bun add
236
+
237
+ # Tailwind v4
238
+ pnpm add -D tailwindcss @tailwindcss/postcss
239
+ ```
240
+
241
+ **2. `sparkle.config.json` をプロジェクトルートに作成**
242
+
243
+ ```json
244
+ {
245
+ "primary": "blue",
246
+ "font-pro": "Inter",
247
+ "font-mono": "JetBrains Mono",
248
+ "radius": "md"
249
+ }
250
+ ```
251
+
252
+ 選択肢の詳細は本 README の「[設定オプション](#設定オプション)」を参照してください。
253
+
254
+ **3. `postcss.config.mjs` をプロジェクトルートに作成**
255
+
256
+ ```js
257
+ export default {
258
+ plugins: {
259
+ '@tailwindcss/postcss': {},
260
+ },
261
+ };
262
+ ```
263
+
264
+ **4. Tailwind エントリ CSS を自前で用意**
265
+
266
+ 既存プロジェクトに組み込む場合、`@import "tailwindcss";` を含む CSS ファイル(例: `src/styles/app.css`)があればそれを使います。無ければ作成してアプリケーションのエントリから import します。
267
+
268
+ **5. `generate` を実行**
269
+
270
+ ```bash
271
+ npx --yes sparkle-design-cli generate
272
+ ```
273
+
274
+ これで `sparkle-design.css` が `src/app/sparkle-design.css` に生成され、`SparkleHead.tsx` も同じ場所に出ます。エントリ CSS の検出に失敗する場合は `sparkle.config.json` の `extend.globals-path` に明示指定してください。
275
+
276
+ ```json
277
+ {
278
+ "primary": "blue",
279
+ "extend": {
280
+ "globals-path": "src/styles/app.css"
281
+ }
282
+ }
283
+ ```
284
+
285
+ **6. フォントの `<link>` タグを手動で配置**
286
+
287
+ Next.js / Vite 以外ではアプリケーションフレームワークごとに「`<head>` 相当」の配置方法が異なります。自動生成される `SparkleHead.tsx` はそのまま使えるはずですが、使えない場合は中身(`preconnect` + Google Fonts の `<link>` タグ群)を自前のレイアウトにコピーしてください。
288
+
289
+ たとえば Astro なら `src/layouts/BaseLayout.astro` の `<head>` に直接書きます:
290
+
291
+ ```html
292
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
293
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
294
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:FILL,wght@0..1,500&display=block" />
295
+ <!-- sparkle.config.json の font-pro / font-mono に合わせた Google Fonts URL -->
296
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" />
297
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" />
298
+ ```
299
+
300
+ **7. アンチパターン検査を package.json に追加(任意)**
301
+
302
+ ```json
303
+ {
304
+ "scripts": {
305
+ "lint:sparkle": "npx --yes sparkle-design-cli check src --strict",
306
+ "lint:sparkle:json": "npx --yes sparkle-design-cli check src --format json"
307
+ }
308
+ }
309
+ ```
310
+
311
+ **8. AI エージェントを使うなら Guard ブロックと hook を手動で配置**
312
+
313
+ AI ガード(`CLAUDE.md` / `AGENTS.md` / `.cursor/rules/*.mdc`)と hook 設定(`.claude/settings.json` / `.cursor/hooks.json` / `.codex/hooks.json`)は `setup` に任せるのが最も楽です。
314
+
315
+ ```bash
316
+ # ガード追加・hook 設定のみ(パッケージインストールや scaffold はスキップ)
317
+ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaffold --skip-generate
318
+ ```
319
+
320
+ この `--skip-*` 組み合わせは手動セットアップ済みのプロジェクトに対して Guard + hook だけ差し込むのに使えます。
321
+
322
+ #### 既知の未対応ケース
323
+
324
+ - **モノレポ(workspaces)**: `generate` は実行した cwd からパッケージマネージャーを遡及検出しないため、サブパッケージで直接実行するのが確実です。
325
+ - **CSS 以外のエントリ(CSS-in-JS / vanilla-extract 等)**: Sparkle Design は Tailwind v4 の `@source` + CSS variable 前提で作られているため、ビルドパイプラインの外で CSS を扱うソリューションは対象外です。
326
+ - **Tailwind v3**: `@source` ディレクティブ依存のため v4 必須です。
327
+
328
+ Next.js / Vite 以外で導入したい方で困ったときは Issue で教えていただけると助かります。
329
+
234
330
  ## 設定ファイル (sparkle.config.json)
235
331
 
236
332
  ### 設定ファイルの作成
@@ -305,39 +401,15 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
305
401
  - カスタム出力先: `-o` オプションで指定可能
306
402
  - 実行場所を基準として相対パスで処理されます
307
403
 
308
- ## 自動フォント管理
309
-
310
- **v1.2.0 以降の新機能**
311
-
312
- CLI は自動的にフォント管理を行います:
313
-
314
- 1. **フォント検出**: `sparkle-design.css` から Google Fonts の `@import` 文を検出
315
- 2. **エントリ CSS への移動**: フォントの `@import` を Tailwind エントリ CSS(`globals.css` / `index.css` 等、自動検出)の先頭に移動
316
- 3. **sparkle-design.css から削除**: 元のファイルからフォント `@import` を削除
317
-
318
- ### なぜこの処理が必要なのか?
319
-
320
- CSS の仕様では、`@import` 文は `@charset` と `@layer` 以外のすべてのルールより前に記述する必要があります。Tailwind CSS v4 と組み合わせた場合、この順序が正しくないとビルド時に警告が発生します。
321
-
322
- この機能により、以下の警告が自動的に解決されます:
323
-
324
- ```text
325
- ⚠️ @import rules must precede all rules aside from @charset and @layer statements
326
- ```
327
-
328
- ### 動作条件
404
+ ## フォントと entry CSS の自動管理
329
405
 
330
- - Tailwind エントリ CSS(`@import "tailwindcss"` を含む CSS ファイル)が `sparkle-design.css` と同じディレクトリに存在する場合のみ実行されます(ファイル名に依存しません)
331
- - エントリ CSS が存在しない場合は、フォント管理処理はスキップされます
406
+ CLI は **Tailwind エントリ CSS(`@import "tailwindcss"` を含む CSS ファイル)** を自動検出し、以下を 1 回で揃えます:
332
407
 
333
- ### 出力例
408
+ - `sparkle-design.css` の `@import`
409
+ - `@source "../node_modules/sparkle-design/dist"`(v4 が node_modules のクラスを拾うのに必要)
410
+ - フォント `<link>` タグ(React 向けは `SparkleHead.tsx`、Vite 向けは `index.html` の managed block に自動注入)
334
411
 
335
- ```text
336
- 📦 フォント管理処理を開始します...
337
- 📝 3個のフォントimportを検出しました
338
- ✅ エントリ CSS にフォントimportを追加しました
339
- ✅ sparkle-design.css からフォントimportを削除しました
340
- ```
412
+ CSS 仕様上 `@import` は他の at-rule より前に書く必要があるため、順序も適切に整えます。`@import "tailwindcss"` が欠けている場合は先頭に自動追記されます。
341
413
 
342
414
  ## 開発
343
415
 
@@ -354,71 +426,20 @@ npm link
354
426
  ### 開発用コマンド
355
427
 
356
428
  ```bash
357
- # テスト実行
358
- npm test
359
-
360
- # テスト監視モード
361
- npm run test:watch
362
-
363
- # Lint実行
364
- npm run lint
365
-
366
- # Lintエラーを自動修正
367
- npm run lint:fix
368
-
369
- # コードフォーマット
370
- npm run format
371
-
372
- # フォーマットチェック
373
- npm run format:check
374
- ```
375
-
376
- ### CLI実行テスト
377
-
378
- ```bash
379
- # CSS生成
380
- sparkle-design-cli generate
381
-
382
- # ヘルプ表示
383
- sparkle-design-cli --help
384
- sparkle-design-cli generate --help
385
-
386
- # 検査
387
- sparkle-design-cli check src --strict
429
+ npm test # Node.js test runner
430
+ npm run lint # ESLint
431
+ npm run format # Prettier
388
432
  ```
389
433
 
390
434
  ### リリース手順(メンテナ向け)
391
435
 
392
- npm publish は GitHub Actions (`Publish to npm`) 経由で行います。ローカルから `npm publish` しないでください。
393
-
394
- #### 安定版(latest)
395
-
396
- 1. `main` ブランチに変更をマージ
397
- 2. `package.json` の `version` を SemVer で更新(例: `2.0.6` → `2.1.0`)し、`CHANGELOG.md` に該当セクションを追加
398
- 3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `latest`)で手動実行
399
- 4. workflow は lint / test を流したうえで `npm publish`(dist-tag = latest)を実行
400
-
401
- #### Beta(beta)
436
+ publish は GitHub Actions の **Publish to npm** workflow 経由。ローカル `npm publish` は禁止。
402
437
 
403
- 品質保証中の変更を先行公開したいときに使います。latest には影響しません。
438
+ 1. `package.json` の `version` を更新(安定版: `X.Y.Z` / RC: `X.Y.Z-rc.N` / Beta: `X.Y.Z-beta.N`)
439
+ 2. `CHANGELOG.md` に該当セクションを追加
440
+ 3. PR をマージ後、**Publish to npm** workflow を `channel: auto` で実行
404
441
 
405
- 1. `package.json` の `version` を `X.Y.Z-beta.N` 形式に更新(例: `2.1.0-beta.0`、次の beta は `2.1.0-beta.1`)
406
- 2. `CHANGELOG.md` に `[X.Y.Z-beta.N]` セクションを追加
407
- 3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `beta`)で手動実行
408
- 4. workflow が version を判定して `npm publish --tag beta` を実行
409
- 5. 利用者側は `npx --yes sparkle-design-cli@beta ...` で検証
410
-
411
- #### Beta から GA(latest)へ昇格
412
-
413
- 同じ成果物を latest にする場合は、新しい安定版 `X.Y.Z` を publish するか、既存 beta バージョンに latest タグを付け替えます。
414
-
415
- ```bash
416
- # 選択肢 A: 新たに X.Y.Z を publish する(推奨)
417
- # version を X.Y.Z に更新 → workflow を latest で実行
418
-
419
- # 選択肢 B: 既存の X.Y.Z-beta.N に latest タグを付け替える
420
- npm dist-tag add sparkle-design-cli@X.Y.Z-beta.N latest
421
- ```
442
+ `channel: auto` は `package.json` の version 形式から dist-tag を自動判定します(`-beta.N` → `beta` / `-rc.N` → `next` / それ以外 → `latest`)。既存 RC / Beta を latest に昇格させる場合は、新しい `X.Y.Z` として改めて publish します(`npm dist-tag add` での手動付け替えも可能ですが、version 管理が明確になる前者を推奨)。
422
443
 
423
444
  ## ライセンス
424
445
 
@@ -1049,8 +1049,28 @@ const ANTI_PATTERN_GROUPS = [
1049
1049
  description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
1050
1050
  recommendation:
1051
1051
  'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
1052
- pattern:
1053
- /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*(?:"[^"]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^"]*"|'[^']*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^']*'|\{[^}]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^}]*\})[^>]*>/g,
1052
+ // 旧 pattern は `[^"]*\b...[^"]*` の 2 つの `[^"]*` が同じ文字列を食い合い
1053
+ // quadratic backtracking になっていた(閉じ quote 無しの壊れた className
1054
+ // で n=80k / 12s の regression)。2-pass 化して linear time を保証する:
1055
+ // 1. `<CardHeader className="..." ...>` / `{...}` 形式の className 値
1056
+ // を確定マッチで抽出
1057
+ // 2. 抽出した className 値に padding utility が含まれているか
1058
+ // `\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)` で test する
1059
+ // en: Switch to a 2-pass matcher. The old regex had quadratic
1060
+ // backtracking on malformed input; now we first capture the whole
1061
+ // className value, then test for padding utilities in isolation.
1062
+ match: (content) => {
1063
+ const OPEN_TAG =
1064
+ /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*("[^"]*"|'[^']*'|\{[^}]*\})[^>]*>/g;
1065
+ const PADDING_UTIL = /\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)/;
1066
+ const results = [];
1067
+ for (const m of content.matchAll(OPEN_TAG)) {
1068
+ if (PADDING_UTIL.test(m[1])) {
1069
+ results.push({ index: m.index, text: m[0] });
1070
+ }
1071
+ }
1072
+ return results;
1073
+ },
1054
1074
  },
1055
1075
  featureSection: lines([
1056
1076
  '### Card 系コンポーネントの padding を上書きしない',
@@ -1146,7 +1166,18 @@ const ANTI_PATTERN_GROUPS = [
1146
1166
  description: 'Icon の children にテキストを渡さない',
1147
1167
  recommendation:
1148
1168
  'Icon には icon prop でアイコン名を渡してください。children にテキストを渡すのは旧 Material Icons の書き方です。',
1149
- pattern: /<Icon\b(?:[^>]|\/(?!>))*>[^<]+<\/Icon>/g,
1169
+ // 旧 pattern は `(?:[^>]|\/(?!>))*` が `/ ` を両分岐にマッチさせるため、
1170
+ // 閉じ `>` が無い入力で exponential backtracking する ReDoS の種だった
1171
+ // (n=200 で 48s 消費)。属性部を `[^>]*` の単一分岐に戻して linear に
1172
+ // する。代償として自己閉じ `<Icon ... />` にも `<Icon ... >...</Icon>`
1173
+ // として表面的にはマッチし得るが、自己閉じは `/` が末尾に来るので
1174
+ // `>[^<]+` の後続(children text)に空白以外が必要になり、現実の JSX
1175
+ // では自己閉じと open+close タグが入り乱れる頻度は低い。1-pass で拾い
1176
+ // きれないケースは 2-pass に引き上げることで将来対応する。
1177
+ // en: Old pattern suffered exponential ReDoS because `/ ` fit both
1178
+ // branches of the alternation. Use a linear `[^>]*` and accept a
1179
+ // small risk of self-closing false positives (rare in practice).
1180
+ pattern: /<Icon\b[^>]*>[^<]+<\/Icon>/g,
1150
1181
  },
1151
1182
  featureSection: lines([
1152
1183
  '### Icon の children にテキストを渡さない',
package/lib/check.js CHANGED
@@ -11,7 +11,18 @@ const RULES = getCheckRules();
11
11
  const BASE_MANUAL_REVIEW_REMINDERS = getManualReviewReminders();
12
12
 
13
13
  const SPARKLE_HEAD_JSX_PATTERN = /<SparkleHead\s*\/?\s*>/;
14
- const CSP_PATTERN = /content-security-policy|contentSecurityPolicy|Content-Security-Policy/i;
14
+ // CSP key が「実際の設定 key として記述されている」コンテキストに絞る。
15
+ // 以前は単純に `/content-security-policy|.../i` だったため、コメント行
16
+ // (`// TODO: Add Content-Security-Policy later` 等)にだけマッチして
17
+ // false-positive の finding を emit していた。
18
+ // - `'Content-Security-Policy': ...` (object property with quoted string key)
19
+ // - `contentSecurityPolicy: ...` (JS camelCase property)
20
+ // - `"Content-Security-Policy"` を value として持つ header エントリ(`key: 'Content-Security-Policy'`)
21
+ // を許容し、単体コメント文字列には反応しないようにする。
22
+ // en: Restrict CSP detection to contexts that look like actual config entries,
23
+ // avoiding false positives from mere comment mentions.
24
+ const CSP_PATTERN =
25
+ /(?:['"]Content-Security-Policy['"]\s*[:,]|contentSecurityPolicy\s*[:=]|key\s*:\s*['"]Content-Security-Policy['"])/;
15
26
  const NEXT_CONFIG_CANDIDATES = [
16
27
  'next.config.js',
17
28
  'next.config.ts',
@@ -160,22 +171,38 @@ function collectNextjsCspFindings(cwd) {
160
171
  let content;
161
172
  try {
162
173
  content = fs.readFileSync(configPath, 'utf8');
163
- } catch {
174
+ } catch (error) {
175
+ // ENOENT は候補が存在しないだけなので静かに次へ。EACCES などは
176
+ // デバッグ価値があるので warn する(silent skip で「check したが
177
+ // CSP 設定が無かった」と誤解されないように)。
178
+ // en: ENOENT = config file absent, move on quietly. Other codes
179
+ // are worth surfacing so the user knows detection was skipped.
180
+ if (error.code !== 'ENOENT') {
181
+ console.warn(
182
+ `⚠️ ${candidate} の読み込みに失敗したため CSP 検査をスキップします (${error.code ?? error.message})`
183
+ );
184
+ }
164
185
  continue;
165
186
  }
166
187
 
167
188
  if (!CSP_PATTERN.test(content)) continue;
168
189
 
190
+ // host の `.` を regex escape しないと `fontsXgoogleapisXcom` などにも
191
+ // マッチし、本来足りない allowlist を「足りている」と誤判定する false
192
+ // negative が起きる。literal `.` として検査する。
193
+ // en: Escape `.` so we match the literal hostname and don't accept
194
+ // arbitrary strings as an allowlist match.
195
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
169
196
  const missingDomains = [
170
197
  {
171
198
  domain: 'fonts.googleapis.com',
172
199
  directive: 'style-src',
173
- pattern: new RegExp(FONT_DOMAINS.GOOGLEAPIS.replace('https://', '')),
200
+ pattern: new RegExp(escapeRegex(FONT_DOMAINS.GOOGLEAPIS.replace('https://', ''))),
174
201
  },
175
202
  {
176
203
  domain: 'fonts.gstatic.com',
177
204
  directive: 'font-src',
178
- pattern: new RegExp(FONT_DOMAINS.GSTATIC.replace('https://', '')),
205
+ pattern: new RegExp(escapeRegex(FONT_DOMAINS.GSTATIC.replace('https://', ''))),
179
206
  },
180
207
  ].filter(({ pattern }) => !pattern.test(content));
181
208
 
@@ -268,9 +295,37 @@ function printTextReport(report, options = {}) {
268
295
  }
269
296
  }
270
297
 
271
- console.log('Manual review reminders:');
272
- for (const reminder of report.manualReviewReminders) {
273
- console.log(`- [${reminder.id}] ${reminder.message}`);
298
+ const reminders = report.manualReviewReminders ?? [];
299
+ if (reminders.length === 0) {
300
+ // 0 件のときは従来通り静かに済ます。
301
+ // en: Nothing to review — keep quiet.
302
+ } else {
303
+ // Guard block から参照される acknowledgment フォーマットと合わせるため、
304
+ // AI 向けに「各 ID を response に echo して確認したことを示してほしい」
305
+ // と明示的に要求する。hook による exit blocking はしない(判断事項のため)。
306
+ // en: Ask the AI to echo each reminder ID in its final response, so humans
307
+ // can see which reminders were considered. This is not enforced by hook
308
+ // exit code — reminders are judgment calls, not hard failures.
309
+ console.log('');
310
+ console.log('=== Manual review reminders (must acknowledge each ID in your response) ===');
311
+ console.log(
312
+ 'These are judgment calls the linter cannot detect. For every item below,'
313
+ );
314
+ console.log(
315
+ 'explicitly state the reminder ID in your reply together with whether the'
316
+ );
317
+ console.log(
318
+ 'current code already satisfies it, or what change is needed. Silence = skipped.'
319
+ );
320
+ console.log('');
321
+ for (const reminder of reminders) {
322
+ console.log(`- [${reminder.id}] ${reminder.message}`);
323
+ }
324
+ console.log('');
325
+ console.log(
326
+ 'Example acknowledgment: "[badge-tag-semantics] reviewed — Badge used only for counts (OK). [dialog-modal-ux] reviewed — changed Dialog to Modal for form case."'
327
+ );
328
+ console.log('=========================================================================');
274
329
  }
275
330
 
276
331
  if (options.strict && report.findings.length > 0) {
@@ -281,6 +336,7 @@ function printTextReport(report, options = {}) {
281
336
  function printJsonReport(report, options = {}) {
282
337
  const strictMode = Boolean(options.strict);
283
338
  const passed = !(strictMode && report.findings.length > 0);
339
+ const reminders = report.manualReviewReminders ?? [];
284
340
 
285
341
  console.log(
286
342
  JSON.stringify(
@@ -289,6 +345,18 @@ function printJsonReport(report, options = {}) {
289
345
  targetCount: report.targets.length,
290
346
  checkedFileCount: report.checkedFiles.length,
291
347
  findingCount: report.findings.length,
348
+ reminderCount: reminders.length,
349
+ // AI 向けの attention フラグ。reminder が 1 件でもあれば true になり、
350
+ // AI は最終 response で各 reminder ID を echo する必要がある。
351
+ // exit code には寄与しない(判断事項を hook で block するのは過剰)。
352
+ // en: AI attention flag. When reminders exist, the AI MUST echo each
353
+ // reminder ID in its final reply. Not tied to exit code — reminders
354
+ // are judgment calls, not hard errors.
355
+ manualReviewRequired: reminders.length > 0,
356
+ reminderAcknowledgmentFormat:
357
+ reminders.length > 0
358
+ ? 'Respond with each reminder ID followed by your review, e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts (OK)`.'
359
+ : null,
292
360
  strictMode,
293
361
  passed,
294
362
  exitCode: passed ? 0 : 1,
package/lib/constants.js CHANGED
@@ -33,6 +33,21 @@ export const GLOBALS_CSS_CANDIDATES = [
33
33
  'src/styles/globals.css',
34
34
  ];
35
35
 
36
+ // Vite プロジェクト判定に使う config ファイル候補。setup.js(scaffold
37
+ // 判定)と font-manager.js(resolveGlobalsPath / index.html 注入判定)
38
+ // で同じリストを参照する。片方だけ変えると scaffold と generate が別
39
+ // 判定をしてしまうので、唯一の source of truth としてここに置く。
40
+ // en: Single source of truth for Vite project detection. Reused by
41
+ // setup.js's scaffold logic and font-manager.js's resolver.
42
+ export const VITE_CONFIG_FILES = [
43
+ 'vite.config.ts',
44
+ 'vite.config.js',
45
+ 'vite.config.mjs',
46
+ 'vite.config.cjs',
47
+ 'vite.config.mts',
48
+ 'vite.config.cts',
49
+ ];
50
+
36
51
  // 正規表現パターン
37
52
  export const REGEX = {
38
53
  // フォント関連
@@ -44,16 +59,20 @@ export const REGEX = {
44
59
  /\/\*\s*フォントのインポート[^*]*\*\/\s*\n?(?:@import\s+(?:url\([^)]+fonts\.googleapis\.com[^)]+\)|['"][^"']*fonts\.googleapis\.com[^"']*['"]);?\s*\n?)*\n?/g,
45
60
 
46
61
  // 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.
62
+ // `@import "sparkle-design.css"` / `@import "./sparkle-design.css"` だけでなく、
63
+ // `@import "./app/sparkle-design.css"` のように entry CSS と sparkle-design.css
64
+ // が別ディレクトリにある Vite 系レイアウトの variant も除去対象にする。
65
+ // ただし **path prefix を `./` または `../` に限定**し、末尾の `sparkle-design.css`
66
+ // は path セパレータ直後であることを要求することで、ユーザーが独自命名した
67
+ // `custom-sparkle-design.css` / `my-sparkle-design.css` などを誤削除しないように
68
+ // している(beta.10 まではここが緩く、suffix match で user-authored ファイルを
69
+ // 巻き込む silent data loss リスクがあった)。
70
+ // en: Match paths ending in `sparkle-design.css` but require a `./` or `../`
71
+ // prefix and a path separator just before the filename. This preserves user-
72
+ // authored files like `custom-sparkle-design.css` that would otherwise be
73
+ // deleted by a loose suffix match.
55
74
  SPARKLE_IMPORT:
56
- /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"][^"']*sparkle-design\.css['"];?\s*\n?/g,
75
+ /\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"](?:(?:\.{1,2}\/)[^"']*\/)?sparkle-design\.css['"];?\s*\n?/g,
57
76
  TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
58
77
 
59
78
  // @source ディレクティブ関連
@@ -70,8 +89,18 @@ export const REGEX = {
70
89
  /\/\*\s*npm パッケージのコンテンツスキャン[^*]*\*\/\s*\n?(?:@source\s+["'][^"']*["'];?\s*\n?)*/g,
71
90
 
72
91
  // カスタムCSS関連
92
+ // 除去対象は「CLI が書き込んだ custom CSS の managed block」のみ。以前は
93
+ // 文字列 "custom" を含む任意の import をすべて削っていたため、
94
+ // `customer-module.css` / `my-customer-ui.css` / `customizer.css` など
95
+ // ユーザー命名の import まで巻き込んで silent data loss していた。
96
+ // CLI が書くのは `/* プロジェクト固有のカスタムトークン */` コメント直後の
97
+ // 1 行 `@import` だけなので、そのペアだけをマッチする。
98
+ // en: Match only the managed block the CLI writes (comment + one `@import`
99
+ // line). Previously any `@import` containing the substring "custom" was
100
+ // deleted, which would silently remove user-authored files like
101
+ // `customer-module.css`.
73
102
  CUSTOM_CSS_IMPORT:
74
- /\/\*\s*プロジェクト固有のカスタムトークン[^*]*\*\/\s*\n?|@import\s+['"][^"']*custom[^"']*['"];?\s*\n?/g,
103
+ /\/\*\s*プロジェクト固有のカスタムトークン[^*]*\*\/\s*\n?(?:@import\s+['"][^"']+['"];?\s*\n?)?/g,
75
104
 
76
105
  // テンプレート関連
77
106
  COLOR_TOKENS_PLACEHOLDER: /[ \t]*\/\* \{\{COLOR_TOKENS\}\} \*\/[ \t]*\n?/,
@@ -46,11 +46,23 @@ function readFile(filePath, successMessage, errorMessage, additionalErrorHandler
46
46
  * @param {Function} additionalErrorHandler 追加のエラーハンドラ(オプション)
47
47
  * @returns {Object} パース済みのJSONオブジェクト
48
48
  */
49
+ // Prototype pollution 対策: `__proto__` / `constructor` / `prototype` を
50
+ // JSON から drop する reviver。本プロセス内の spread は安全だが、書き戻し
51
+ // 先(hook JSON / package.json 等)経由で下流 consumer に流れたときに
52
+ // プロトタイプ汚染が起きる古典パターンを断つ。
53
+ // en: Strip prototype-pollution keys at parse time so they cannot travel
54
+ // through serialized files to downstream consumers.
55
+ const POLLUTION_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
56
+ function jsonPollutionReviver(key, value) {
57
+ if (POLLUTION_KEYS.has(key)) return undefined;
58
+ return value;
59
+ }
60
+
49
61
  function readJsonFile(filePath, successMessage, errorMessage, additionalErrorHandler = null) {
50
62
  const content = readFile(filePath, successMessage, errorMessage, additionalErrorHandler);
51
63
 
52
64
  try {
53
- return JSON.parse(content);
65
+ return JSON.parse(content, jsonPollutionReviver);
54
66
  } catch (error) {
55
67
  const message =
56
68
  typeof errorMessage === 'function'