sparkle-design-cli 2.0.7-beta.9 → 2.0.7-rc.3
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 +134 -113
- package/lib/anti-pattern-rules.js +42 -5
- package/lib/check.js +135 -23
- package/lib/constants.js +39 -10
- package/lib/file-loader.js +13 -1
- package/lib/font-manager.js +61 -25
- package/lib/generate-css.js +205 -37
- package/lib/path-utils.js +66 -0
- package/lib/setup.js +105 -28
- package/package.json +1 -1
package/lib/setup.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { generateCSS } from './generate-css.js';
|
|
5
5
|
import { GLOBALS_CSS_CANDIDATES } from './constants.js';
|
|
6
|
+
import { isViteProject } from './font-manager.js';
|
|
6
7
|
|
|
7
8
|
// デフォルトの sparkle.config.json テンプレート
|
|
8
9
|
// en: Default sparkle.config.json template
|
|
@@ -84,8 +85,19 @@ function ensureDir(filePath) {
|
|
|
84
85
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// `__proto__` / `constructor` / `prototype` を含む JSON を読み込んだ後、
|
|
89
|
+
// 下流の `Object.assign(_.merge(...))` 等でプロトタイプ汚染が発生する可能性
|
|
90
|
+
// がある(本プロセス内の spread は安全だが、書き戻し後に consumer の agent
|
|
91
|
+
// や 3rd-party ツールに値が流れる)。reviser で unsafe key を drop する。
|
|
92
|
+
// en: Strip known prototype-pollution keys when parsing JSON so downstream
|
|
93
|
+
// consumers that use `Object.assign`/merge don't inherit attacker-controlled
|
|
94
|
+
// prototype entries.
|
|
95
|
+
const POLLUTION_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
87
96
|
function readJson(filePath) {
|
|
88
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
97
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'), (key, value) => {
|
|
98
|
+
if (POLLUTION_KEYS.has(key)) return undefined;
|
|
99
|
+
return value;
|
|
100
|
+
});
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
function writeJson(filePath, value) {
|
|
@@ -119,7 +131,12 @@ function normalizePath(cwd, inputPath, label, { checkParentOnly = false } = {})
|
|
|
119
131
|
|
|
120
132
|
const normalized = path.posix.normalize(trimmed.replace(/\\/g, '/'));
|
|
121
133
|
|
|
122
|
-
|
|
134
|
+
// `.` は target にも instructions-path にも適さない。`--instructions-path .`
|
|
135
|
+
// のような誤入力を親 dir チェックが通したあと EISDIR で落ちる、といった分かり
|
|
136
|
+
// にくい失敗を回避するため、ここで一貫して empty 扱いにする。
|
|
137
|
+
// en: Treat `.` as empty regardless of checkParentOnly to avoid confusing
|
|
138
|
+
// downstream EISDIR failures.
|
|
139
|
+
if (normalized === '.' || normalized === '') {
|
|
123
140
|
throw new Error(`${label} is empty`);
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -203,9 +220,9 @@ function buildInstructionBlock(target, assistant) {
|
|
|
203
220
|
' - 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
221
|
' - 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
222
|
' - 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).',
|
|
206
|
-
'- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns.',
|
|
223
|
+
'- **Required**: After creating or modifying any UI component, run `lint:sparkle` before finishing. This catches Sparkle Design anti-patterns. To opt out a specific line from a rule when the Sparkle token does not map cleanly (e.g. sub-12px font sizes for `tailwind-typography`), add `// sparkle-disable-line <rule-id>` on the same line or `// sparkle-disable-next-line <rule-id>` on the line above — but use it sparingly and only when the migration would actually break the design.',
|
|
207
224
|
`- For AI review, prefer \`lint:sparkle:json\` or run \`npx --yes sparkle-design-cli check ${target} --format json\` directly.`,
|
|
208
|
-
'- You must inspect every entry in both `findings` and `manualReviewReminders`
|
|
225
|
+
'- You must inspect every entry in both `findings` and `manualReviewReminders`. For each entry in `manualReviewReminders`, **echo the reminder ID in your final reply together with a short review note** (e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts, so the usage is fine.`). Silence on a reminder means it was skipped; always state at least a one-line judgment per ID. This is not blocked by the Stop hook (reminders are judgment calls), but reviewers / humans rely on the explicit acknowledgment to trust that each item was actually considered.',
|
|
209
226
|
BLOCK_END,
|
|
210
227
|
].join('\n');
|
|
211
228
|
}
|
|
@@ -215,7 +232,21 @@ function updateInstructionFile(filePath, block) {
|
|
|
215
232
|
const current = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
216
233
|
|
|
217
234
|
if (current.includes(BLOCK_START) && current.includes(BLOCK_END)) {
|
|
218
|
-
|
|
235
|
+
// 2 組以上の managed block が残っている場合は `/g` で全部置き換えた上で
|
|
236
|
+
// warn を出す。ユーザーが手動で block をコピー/ペーストしたケースで、
|
|
237
|
+
// 従来の非 /g 置換だと 1 組目しか更新されず古い block が残って AI が
|
|
238
|
+
// 混乱する silent drift の原因になっていた。
|
|
239
|
+
// en: Replace every managed block occurrence (not just the first).
|
|
240
|
+
const markerCount = (current.match(new RegExp(BLOCK_START, 'g')) || []).length;
|
|
241
|
+
if (markerCount > 1) {
|
|
242
|
+
console.warn(
|
|
243
|
+
`⚠️ ${filePath} に Sparkle Design Guard ブロックが ${markerCount} 個見つかりました。すべて最新版で置き換えます(手動でコピーした古い block が残っている可能性があります)。`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const next = current.replace(
|
|
247
|
+
new RegExp(`${BLOCK_START}[\\s\\S]*?${BLOCK_END}`, 'g'),
|
|
248
|
+
block
|
|
249
|
+
);
|
|
219
250
|
return { changed: next !== current, content: next, existed: exists };
|
|
220
251
|
}
|
|
221
252
|
|
|
@@ -329,26 +360,28 @@ function ensurePackagesInstalled(
|
|
|
329
360
|
console.log(`📦 ${packageManager} ${args.join(' ')}`);
|
|
330
361
|
const result = spawnSync(packageManager, args, { cwd, stdio: 'inherit' });
|
|
331
362
|
if (result.status !== 0) {
|
|
332
|
-
|
|
363
|
+
// 失敗の原因を呼び出し側やログに残せるよう exit status / signal / spawn
|
|
364
|
+
// error を具体的に示す。以前は "Failed to install ..." だけで pm のログ
|
|
365
|
+
// 遡及が必要だった。
|
|
366
|
+
// en: Include the exit code / signal / spawn error so users don't need
|
|
367
|
+
// to dig back through the pm log to see what went wrong.
|
|
368
|
+
const details = [];
|
|
369
|
+
if (typeof result.status === 'number') details.push(`exit ${result.status}`);
|
|
370
|
+
if (result.signal) details.push(`signal ${result.signal}`);
|
|
371
|
+
if (result.error) details.push(`spawn error: ${result.error.code ?? result.error.message}`);
|
|
372
|
+
const suffix = details.length > 0 ? ` (${details.join(', ')})` : '';
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Failed to install packages via ${packageManager}${suffix}: ${missing.join(', ')}`
|
|
375
|
+
);
|
|
333
376
|
}
|
|
334
377
|
|
|
335
378
|
return { ran: true, reason: 'installed', packages: missing, packageManager, dev };
|
|
336
379
|
}
|
|
337
380
|
|
|
338
|
-
// Vite
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
'vite.config.js',
|
|
343
|
-
'vite.config.mjs',
|
|
344
|
-
'vite.config.cjs',
|
|
345
|
-
'vite.config.mts',
|
|
346
|
-
'vite.config.cts',
|
|
347
|
-
];
|
|
348
|
-
|
|
349
|
-
function isViteProject(cwd) {
|
|
350
|
-
return VITE_CONFIG_FILES.some((name) => fs.existsSync(path.join(cwd, name)));
|
|
351
|
-
}
|
|
381
|
+
// Vite 判定は font-manager.js の isViteProject を再利用する。以前はここに
|
|
382
|
+
// 同じ配列と関数が定義されていて drift の危険があった。constants.js の
|
|
383
|
+
// VITE_CONFIG_FILES を single source of truth として共通化。
|
|
384
|
+
// en: Reuse isViteProject from font-manager.js (backed by constants).
|
|
352
385
|
|
|
353
386
|
/**
|
|
354
387
|
* エントリポイント CSS の新規作成先を決定する。
|
|
@@ -532,6 +565,16 @@ function loadHookJson(filePath, label) {
|
|
|
532
565
|
try {
|
|
533
566
|
return { existed: true, config: readJson(filePath) };
|
|
534
567
|
} catch (error) {
|
|
568
|
+
// EACCES / EPERM 等の権限系は「不正な JSON」ではないので文言を分ける。
|
|
569
|
+
// 以前は全エラーを「不正な JSON です」として扱い、ユーザーが理由を誤認
|
|
570
|
+
// するケースがあった。
|
|
571
|
+
// en: Differentiate permission errors from actual JSON syntax errors to
|
|
572
|
+
// avoid pointing the user at the wrong fix.
|
|
573
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`${label} の読み込み権限がありません (${error.code})。ファイルの権限を確認してから再実行してください。`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
535
578
|
throw new Error(
|
|
536
579
|
`${label} が不正な JSON です (${error.message})。修正してから再実行してください。`
|
|
537
580
|
);
|
|
@@ -563,7 +606,15 @@ function detectClaudeSessionRootMismatch(cwd) {
|
|
|
563
606
|
const resolvedCwd = fs.realpathSync(path.resolve(cwd));
|
|
564
607
|
if (resolvedSession === resolvedCwd) return null;
|
|
565
608
|
return { sessionDir: resolvedSession, cwd: resolvedCwd };
|
|
566
|
-
} catch {
|
|
609
|
+
} catch (error) {
|
|
610
|
+
// `CLAUDE_PROJECT_DIR` に壊れた path が入っている / realpath 解決に失敗した
|
|
611
|
+
// 等はユーザーの Claude Code 環境側の問題。silent に null を返すと「hook
|
|
612
|
+
// が効かない」状況を見落とすので、debug 価値として warn を出す。
|
|
613
|
+
// en: Surface realpath failures so broken CLAUDE_PROJECT_DIR values don't
|
|
614
|
+
// silently hide a hook-not-firing situation.
|
|
615
|
+
console.warn(
|
|
616
|
+
`⚠️ CLAUDE_PROJECT_DIR (${sessionDir}) の解決に失敗したため Claude session root 判定をスキップします (${error.code ?? error.message})`
|
|
617
|
+
);
|
|
567
618
|
return null;
|
|
568
619
|
}
|
|
569
620
|
}
|
|
@@ -760,12 +811,27 @@ function runGenerate({ skipGenerate, dryRun, strict }) {
|
|
|
760
811
|
const result = generateCSS(null, null, null, { strict: Boolean(strict) });
|
|
761
812
|
return { skipped: false, ran: true, globalsResult: result?.globalsResult };
|
|
762
813
|
} catch (error) {
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
|
|
814
|
+
// 以下の code はユーザーが必ず気付くべき silent drop 系なので、非 strict
|
|
815
|
+
// でも常に再 throw して exit 1 に昇格させる:
|
|
816
|
+
// - E_EXPLICIT_GLOBALS_PATH_NOT_FOUND: sparkle.config.json の
|
|
817
|
+
// `extend.globals-path` typo
|
|
818
|
+
// - E_EXTEND_FILE_READ_FAILED / _PARSE_FAILED / _INVALID_SHAPE: 2.0.7
|
|
819
|
+
// 以降は `extend` ファイルの読み込み / parse 失敗を silent fallback
|
|
820
|
+
// しない
|
|
821
|
+
// en: Always propagate these codes even without --strict so silent
|
|
822
|
+
// misconfigurations become visible.
|
|
823
|
+
const alwaysThrowCodes = new Set([
|
|
824
|
+
'E_EXPLICIT_GLOBALS_PATH_NOT_FOUND',
|
|
825
|
+
'E_EXTEND_FILE_READ_FAILED',
|
|
826
|
+
'E_EXTEND_FILE_PARSE_FAILED',
|
|
827
|
+
'E_EXTEND_FILE_INVALID_SHAPE',
|
|
828
|
+
'E_TAILWIND_IMPORT_PREPEND_FAILED',
|
|
829
|
+
'E_UNSAFE_RELATIVE_PATH',
|
|
830
|
+
'E_UNSAFE_FONT_FAMILY',
|
|
831
|
+
'E_UNSAFE_FONT_WEIGHTS',
|
|
832
|
+
'E_UNSAFE_PACKAGE_NAME',
|
|
833
|
+
]);
|
|
834
|
+
if (strict || alwaysThrowCodes.has(error.code)) {
|
|
769
835
|
throw error;
|
|
770
836
|
}
|
|
771
837
|
console.warn(`⚠️ generate 実行をスキップしました: ${error.message}`);
|
|
@@ -850,7 +916,18 @@ export function setupAssistant(options = {}) {
|
|
|
850
916
|
existed: hook.existed,
|
|
851
917
|
reason: hook.reason,
|
|
852
918
|
featureFlagNote: hook.featureFlagNote ?? null,
|
|
853
|
-
|
|
919
|
+
// summary は stdout に流れるため CI ログ等に残り得る。絶対パスを
|
|
920
|
+
// そのまま残すと内部プロジェクトツリーやホスト名が漏れるので、
|
|
921
|
+
// mismatch の有無(boolean)と cwd 相対 path に絞って伝える。
|
|
922
|
+
// 詳細な絶対パスは stderr の post-setup reminder 側で十分。
|
|
923
|
+
// en: Avoid leaking absolute session / cwd paths via stdout summary.
|
|
924
|
+
sessionRootMismatch: hook.sessionRootMismatch
|
|
925
|
+
? {
|
|
926
|
+
detected: true,
|
|
927
|
+
sessionDir: path.relative(cwd, hook.sessionRootMismatch.sessionDir) ||
|
|
928
|
+
'.',
|
|
929
|
+
}
|
|
930
|
+
: null,
|
|
854
931
|
}
|
|
855
932
|
: null,
|
|
856
933
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "2.0.7-
|
|
3
|
+
"version": "2.0.7-rc.3",
|
|
4
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",
|