sparkle-design-cli 2.0.7 → 2.0.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/bin/sparkle-design.js +13 -1
- package/lib/setup.js +145 -49
- package/lib/stop-hook.js +60 -0
- package/package.json +1 -1
package/bin/sparkle-design.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { generateCSS } from '../lib/generate-css.js';
|
|
4
4
|
import { checkProject } from '../lib/check.js';
|
|
5
5
|
import { setupAssistant } from '../lib/setup.js';
|
|
6
|
+
import { runStopHook } from '../lib/stop-hook.js';
|
|
6
7
|
|
|
7
|
-
const SUBCOMMANDS = new Set(['generate', 'check', 'setup']);
|
|
8
|
+
const SUBCOMMANDS = new Set(['generate', 'check', 'setup', 'stop-hook']);
|
|
8
9
|
|
|
9
10
|
function requireOptionValue(args, index, flags) {
|
|
10
11
|
const value = args[index + 1];
|
|
@@ -138,6 +139,7 @@ Commands:
|
|
|
138
139
|
generate sparkle.config.json から CSS を生成
|
|
139
140
|
check Sparkle Design のアンチパターンを検査
|
|
140
141
|
setup Sparkle Design プロジェクトをセットアップ(パッケージ導入 + 初期ファイル + AI ガード + generate)
|
|
142
|
+
stop-hook AI assistant の Stop hook 用 internal subcommand(findings 検出時に exit 2 で 1 度だけ停止をブロック / 再発火は自動回避)
|
|
141
143
|
|
|
142
144
|
Generate:
|
|
143
145
|
sparkle-design-cli generate
|
|
@@ -302,6 +304,16 @@ function main() {
|
|
|
302
304
|
return;
|
|
303
305
|
}
|
|
304
306
|
|
|
307
|
+
if (command === 'stop-hook') {
|
|
308
|
+
// setup で各 agent の hook 設定ファイルから呼ばれる internal subcommand。
|
|
309
|
+
// 第 2 引数は lint 対象 path(setup 時に決まったもの)。option flag は未対応。
|
|
310
|
+
// en: Internal subcommand invoked by agent stop hooks. The single positional
|
|
311
|
+
// arg is the lint target path determined at setup time.
|
|
312
|
+
const target = args[1];
|
|
313
|
+
const exitCode = runStopHook(target);
|
|
314
|
+
process.exit(exitCode);
|
|
315
|
+
}
|
|
316
|
+
|
|
305
317
|
// 未知のコマンド: エラー終了ではなく note + help 表示にする(typo 時の迷子を防ぐ)
|
|
306
318
|
// en: Unknown command: show a short note + help instead of throwing
|
|
307
319
|
console.warn(
|
package/lib/setup.js
CHANGED
|
@@ -594,16 +594,20 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
594
594
|
*
|
|
595
595
|
* 各 agent が持つ hook システムの schema は異なるため、agent ごとに以下の
|
|
596
596
|
* ヘルパーを持つ。共通ポイントは:
|
|
597
|
-
* - 実行コマンドは `npx --yes sparkle-design-cli
|
|
598
|
-
*
|
|
599
|
-
*
|
|
597
|
+
* - 実行コマンドは `npx --yes sparkle-design-cli stop-hook <target>`
|
|
598
|
+
* (内部で check --strict を回し、初回 findings 検出時のみ exit 2 で停止を
|
|
599
|
+
* ブロックする。Claude Code の `stop_hook_active` を読んで再発火時は
|
|
600
|
+
* exit 0 で抜けることでセッション内ループを防ぐ)
|
|
600
601
|
* - 既存の hook 設定ファイルは非破壊マージし、同一 command が含まれていれば skip
|
|
601
|
-
*
|
|
602
|
+
* (冪等)。2.0.7 までの旧コマンド `check ... --strict || exit 2` を見つけたら
|
|
603
|
+
* 新 subcommand へ自動移行する
|
|
602
604
|
* - 壊れた JSON は silent 上書きせず、ユーザーに修正を促すエラーを出す
|
|
603
605
|
*
|
|
604
|
-
* en: Install an agent-specific hook that runs `
|
|
605
|
-
*
|
|
606
|
-
*
|
|
606
|
+
* en: Install an agent-specific hook that runs the `stop-hook` subcommand,
|
|
607
|
+
* which surfaces findings once and avoids the infinite re-fire loop the
|
|
608
|
+
* pre-2.0.8 form had. Each agent ships its own hook config format; these
|
|
609
|
+
* helpers emit the right shape, stay idempotent on rerun, and migrate
|
|
610
|
+
* legacy 2.0.7 commands when found.
|
|
607
611
|
*
|
|
608
612
|
* References (2026-04 時点):
|
|
609
613
|
* - Claude Code : https://docs.claude.com/en/docs/claude-code/hooks
|
|
@@ -611,7 +615,26 @@ function runAssistantGuard(cwd, packageJsonPath, options, assistantConfig) {
|
|
|
611
615
|
* - Codex : https://developers.openai.com/codex/hooks
|
|
612
616
|
*/
|
|
613
617
|
function buildManagedHookCommand(target) {
|
|
614
|
-
|
|
618
|
+
// Stop / stop hook は専用 subcommand に集約する。subcommand 側で
|
|
619
|
+
// `stop_hook_active` を見て初回だけ exit 2、再発火時は exit 0 で抜ける
|
|
620
|
+
// ことでセッション内ループを防ぐ。
|
|
621
|
+
// en: Route through the dedicated `stop-hook` subcommand so we can detect
|
|
622
|
+
// re-fires (Claude Code's `stop_hook_active`) and avoid infinite loops.
|
|
623
|
+
return `npx --yes sparkle-design-cli stop-hook ${target}`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* 2.0.7 までは `npx --yes sparkle-design-cli check <target> --strict || exit 2` を
|
|
628
|
+
* 直接 hook に書いていたが、findings が残っていると Stop hook が再発火し続けて
|
|
629
|
+
* セッションが無限ループする問題があった。`stop-hook` subcommand に切り替える
|
|
630
|
+
* ため、既存 hook 設定にこの旧コマンドが残っている場合は新コマンドへ置換する。
|
|
631
|
+
*
|
|
632
|
+
* en: Detect legacy hook commands installed by 2.0.7 and earlier so setup
|
|
633
|
+
* reruns can heal them by swapping in the loop-safe `stop-hook` subcommand.
|
|
634
|
+
*/
|
|
635
|
+
function isLegacyManagedHookCommand(command) {
|
|
636
|
+
if (typeof command !== 'string') return false;
|
|
637
|
+
return /sparkle-design-cli\s+check\s+\S+\s+--strict\s*\|\|\s*exit\s+2/.test(command);
|
|
615
638
|
}
|
|
616
639
|
|
|
617
640
|
function loadHookJson(filePath, label) {
|
|
@@ -685,14 +708,40 @@ function runClaudeHook(cwd, target, dryRun) {
|
|
|
685
708
|
typeof nextSettings.hooks === 'object' && nextSettings.hooks ? { ...nextSettings.hooks } : {};
|
|
686
709
|
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
687
710
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
711
|
+
// 1st pass: legacy エントリを発見。alreadyPresent / migrated を判定する。
|
|
712
|
+
// 2 回目以降の setup や、ユーザーが手動で新コマンドを足した状態でも
|
|
713
|
+
// 重複が出ないよう、変換は 2nd pass で行う。
|
|
714
|
+
// en: First pass detects state; second pass mutates without creating dupes.
|
|
715
|
+
let alreadyPresent = false;
|
|
716
|
+
let hasLegacy = false;
|
|
717
|
+
for (const group of stopGroups) {
|
|
718
|
+
if (!Array.isArray(group?.hooks)) continue;
|
|
719
|
+
for (const entry of group.hooks) {
|
|
720
|
+
if (entry?.type !== 'command') continue;
|
|
721
|
+
if (entry.command === managedCommand) alreadyPresent = true;
|
|
722
|
+
else if (isLegacyManagedHookCommand(entry.command)) hasLegacy = true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const migrated = hasLegacy;
|
|
726
|
+
|
|
727
|
+
const updatedGroups = stopGroups.map((group) => {
|
|
728
|
+
if (!Array.isArray(group?.hooks)) return group;
|
|
729
|
+
const updatedHooks = group.hooks
|
|
730
|
+
.map((entry) => {
|
|
731
|
+
if (entry?.type === 'command' && isLegacyManagedHookCommand(entry.command)) {
|
|
732
|
+
// legacy + 新コマンドが両方あったら legacy を削除(dedupe)。
|
|
733
|
+
// legacy のみなら新コマンドへ in-place 置換。
|
|
734
|
+
// en: Drop legacy when the new command coexists; otherwise rewrite.
|
|
735
|
+
return alreadyPresent ? null : { ...entry, command: managedCommand };
|
|
736
|
+
}
|
|
737
|
+
return entry;
|
|
738
|
+
})
|
|
739
|
+
.filter((entry) => entry !== null);
|
|
740
|
+
return { ...group, hooks: updatedHooks };
|
|
741
|
+
});
|
|
693
742
|
|
|
694
743
|
const mismatch = detectClaudeSessionRootMismatch(cwd);
|
|
695
|
-
if (alreadyPresent) {
|
|
744
|
+
if (alreadyPresent && !migrated) {
|
|
696
745
|
return {
|
|
697
746
|
assistant: 'claude',
|
|
698
747
|
changed: false,
|
|
@@ -704,10 +753,12 @@ function runClaudeHook(cwd, target, dryRun) {
|
|
|
704
753
|
};
|
|
705
754
|
}
|
|
706
755
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
756
|
+
let nextStopGroups = updatedGroups;
|
|
757
|
+
if (!migrated && !alreadyPresent) {
|
|
758
|
+
nextStopGroups = [...updatedGroups, { hooks: [{ type: 'command', command: managedCommand }] }];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
hooks.Stop = nextStopGroups;
|
|
711
762
|
nextSettings.hooks = hooks;
|
|
712
763
|
|
|
713
764
|
if (!dryRun) {
|
|
@@ -719,7 +770,7 @@ function runClaudeHook(cwd, target, dryRun) {
|
|
|
719
770
|
changed: true,
|
|
720
771
|
existed,
|
|
721
772
|
path: settingsPath,
|
|
722
|
-
reason: existed ? 'appended' : 'created',
|
|
773
|
+
reason: migrated ? 'migrated' : existed ? 'appended' : 'created',
|
|
723
774
|
command: managedCommand,
|
|
724
775
|
sessionRootMismatch: mismatch,
|
|
725
776
|
};
|
|
@@ -740,8 +791,24 @@ function runCursorHook(cwd, target, dryRun) {
|
|
|
740
791
|
typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
|
|
741
792
|
const stopEntries = Array.isArray(hooks.stop) ? [...hooks.stop] : [];
|
|
742
793
|
|
|
743
|
-
|
|
744
|
-
|
|
794
|
+
let alreadyPresent = false;
|
|
795
|
+
let hasLegacy = false;
|
|
796
|
+
for (const entry of stopEntries) {
|
|
797
|
+
if (entry?.command === managedCommand) alreadyPresent = true;
|
|
798
|
+
else if (isLegacyManagedHookCommand(entry?.command)) hasLegacy = true;
|
|
799
|
+
}
|
|
800
|
+
const migrated = hasLegacy;
|
|
801
|
+
|
|
802
|
+
const updatedEntries = stopEntries
|
|
803
|
+
.map((entry) => {
|
|
804
|
+
if (isLegacyManagedHookCommand(entry?.command)) {
|
|
805
|
+
return alreadyPresent ? null : { ...entry, command: managedCommand };
|
|
806
|
+
}
|
|
807
|
+
return entry;
|
|
808
|
+
})
|
|
809
|
+
.filter((entry) => entry !== null);
|
|
810
|
+
|
|
811
|
+
if (alreadyPresent && !migrated) {
|
|
745
812
|
return {
|
|
746
813
|
assistant: 'cursor',
|
|
747
814
|
changed: false,
|
|
@@ -752,8 +819,12 @@ function runCursorHook(cwd, target, dryRun) {
|
|
|
752
819
|
};
|
|
753
820
|
}
|
|
754
821
|
|
|
755
|
-
|
|
756
|
-
|
|
822
|
+
let nextStopEntries = updatedEntries;
|
|
823
|
+
if (!migrated && !alreadyPresent) {
|
|
824
|
+
nextStopEntries = [...updatedEntries, { command: managedCommand }];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
hooks.stop = nextStopEntries;
|
|
757
828
|
nextConfig.hooks = hooks;
|
|
758
829
|
|
|
759
830
|
if (!dryRun) {
|
|
@@ -766,7 +837,7 @@ function runCursorHook(cwd, target, dryRun) {
|
|
|
766
837
|
changed: true,
|
|
767
838
|
existed,
|
|
768
839
|
path: hooksPath,
|
|
769
|
-
reason: existed ? 'appended' : 'created',
|
|
840
|
+
reason: migrated ? 'migrated' : existed ? 'appended' : 'created',
|
|
770
841
|
command: managedCommand,
|
|
771
842
|
};
|
|
772
843
|
}
|
|
@@ -786,13 +857,37 @@ function runCodexHook(cwd, target, dryRun) {
|
|
|
786
857
|
typeof nextConfig.hooks === 'object' && nextConfig.hooks ? { ...nextConfig.hooks } : {};
|
|
787
858
|
const stopGroups = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
788
859
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
860
|
+
let alreadyPresent = false;
|
|
861
|
+
let hasLegacy = false;
|
|
862
|
+
for (const group of stopGroups) {
|
|
863
|
+
if (!Array.isArray(group?.hooks)) continue;
|
|
864
|
+
for (const entry of group.hooks) {
|
|
865
|
+
if (entry?.type !== 'command') continue;
|
|
866
|
+
if (entry.command === managedCommand) alreadyPresent = true;
|
|
867
|
+
else if (isLegacyManagedHookCommand(entry.command)) hasLegacy = true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const migrated = hasLegacy;
|
|
871
|
+
|
|
872
|
+
const updatedGroups = stopGroups.map((group) => {
|
|
873
|
+
if (!Array.isArray(group?.hooks)) return group;
|
|
874
|
+
const updatedHooks = group.hooks
|
|
875
|
+
.map((entry) => {
|
|
876
|
+
if (entry?.type === 'command' && isLegacyManagedHookCommand(entry.command)) {
|
|
877
|
+
return alreadyPresent ? null : { ...entry, command: managedCommand };
|
|
878
|
+
}
|
|
879
|
+
return entry;
|
|
880
|
+
})
|
|
881
|
+
.filter((entry) => entry !== null);
|
|
882
|
+
return { ...group, hooks: updatedHooks };
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// 有効化にはユーザー側の opt-in が必要な旨を summary に載せる。
|
|
886
|
+
// en: Codex hooks are behind an opt-in feature flag; surface that in summary.
|
|
887
|
+
const featureFlagNote =
|
|
888
|
+
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。';
|
|
794
889
|
|
|
795
|
-
if (alreadyPresent) {
|
|
890
|
+
if (alreadyPresent && !migrated) {
|
|
796
891
|
return {
|
|
797
892
|
assistant: 'codex',
|
|
798
893
|
changed: false,
|
|
@@ -800,17 +895,16 @@ function runCodexHook(cwd, target, dryRun) {
|
|
|
800
895
|
path: hooksPath,
|
|
801
896
|
reason: 'already-present',
|
|
802
897
|
command: managedCommand,
|
|
803
|
-
|
|
804
|
-
// en: Codex hooks are behind an opt-in feature flag; surface that in summary.
|
|
805
|
-
featureFlagNote:
|
|
806
|
-
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
898
|
+
featureFlagNote,
|
|
807
899
|
};
|
|
808
900
|
}
|
|
809
901
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
902
|
+
let nextStopGroups = updatedGroups;
|
|
903
|
+
if (!migrated && !alreadyPresent) {
|
|
904
|
+
nextStopGroups = [...updatedGroups, { hooks: [{ type: 'command', command: managedCommand }] }];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
hooks.Stop = nextStopGroups;
|
|
814
908
|
nextConfig.hooks = hooks;
|
|
815
909
|
|
|
816
910
|
if (!dryRun) {
|
|
@@ -823,10 +917,9 @@ function runCodexHook(cwd, target, dryRun) {
|
|
|
823
917
|
changed: true,
|
|
824
918
|
existed,
|
|
825
919
|
path: hooksPath,
|
|
826
|
-
reason: existed ? 'appended' : 'created',
|
|
920
|
+
reason: migrated ? 'migrated' : existed ? 'appended' : 'created',
|
|
827
921
|
command: managedCommand,
|
|
828
|
-
featureFlagNote
|
|
829
|
-
'Codex で有効化するには `~/.codex/config.toml` に `[features]` セクションを作り `codex_hooks = true` を設定してください。',
|
|
922
|
+
featureFlagNote,
|
|
830
923
|
};
|
|
831
924
|
}
|
|
832
925
|
|
|
@@ -918,11 +1011,12 @@ export function setupAssistant(options = {}) {
|
|
|
918
1011
|
assistantConfig
|
|
919
1012
|
);
|
|
920
1013
|
// assistant 別に hook 設定ファイル(`.claude/settings.json` /
|
|
921
|
-
// `.cursor/hooks.json` / `.codex/hooks.json`)に `
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
1014
|
+
// `.cursor/hooks.json` / `.codex/hooks.json`)に `stop-hook` subcommand を
|
|
1015
|
+
// 設定する。instruction 頼みではなく hook で強制することで lint:sparkle の
|
|
1016
|
+
// 実行漏れを防ぐ。subcommand 側で `stop_hook_active` を見て再発火を抑止し、
|
|
1017
|
+
// findings があるときの 1 回だけ exit 2 でブロックする。generic は hook なし。
|
|
1018
|
+
// en: Install the loop-safe `stop-hook` subcommand so lint:sparkle becomes a
|
|
1019
|
+
// hard gate. Generic assistant has no hook target.
|
|
926
1020
|
const hook = runAssistantHook(assistant, cwd, guard.target, dryRun);
|
|
927
1021
|
const generate = runGenerate({
|
|
928
1022
|
skipGenerate: Boolean(options.skipGenerate),
|
|
@@ -1045,10 +1139,12 @@ function printPostSetupReminder(target, packageManager, { hook, legacyCursorGuar
|
|
|
1045
1139
|
? '(既存のまま)'
|
|
1046
1140
|
: hook.reason === 'created'
|
|
1047
1141
|
? '(新規作成)'
|
|
1048
|
-
: '
|
|
1142
|
+
: hook.reason === 'migrated'
|
|
1143
|
+
? '(旧 || exit 2 形式から自動移行)'
|
|
1144
|
+
: '(既存設定に追記)';
|
|
1049
1145
|
lines.push(
|
|
1050
|
-
` 4. ${meta.name} の ${meta.event} hook を ${meta.file} に設定しました ${label}。応答を終える直前に \`
|
|
1051
|
-
` Installed a ${meta.name} ${meta.event} hook in ${meta.file}: it runs \`
|
|
1146
|
+
` 4. ${meta.name} の ${meta.event} hook を ${meta.file} に設定しました ${label}。応答を終える直前に \`sparkle-design-cli stop-hook\` が走り、findings があれば 1 度だけ exit 2 で停止をブロックします(再発火は \`stop_hook_active\` を見て自動回避)。`,
|
|
1147
|
+
` Installed a ${meta.name} ${meta.event} hook in ${meta.file}: it runs \`sparkle-design-cli stop-hook\` before the turn ends, blocks once with exit 2 when findings exist, and skips re-blocking on stop_hook_active.`
|
|
1052
1148
|
);
|
|
1053
1149
|
if (hook.featureFlagNote) {
|
|
1054
1150
|
lines.push(` ⚠️ ${hook.featureFlagNote}`);
|
package/lib/stop-hook.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { checkProject } from './check.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AI assistant の Stop / stop hook 用エントリポイント。
|
|
6
|
+
*
|
|
7
|
+
* 単に `lint:sparkle --strict || exit 2` を毎回走らせると、Claude Code
|
|
8
|
+
* のように exit 2 を「もう一度ターンを継続させる」シグナルとして解釈する
|
|
9
|
+
* agent では、findings が残っている限り Stop hook が再発火し続けて
|
|
10
|
+
* セッションが無限ループに陥る。
|
|
11
|
+
*
|
|
12
|
+
* Claude Code は再発火時に stdin JSON へ `stop_hook_active: true` を
|
|
13
|
+
* 渡してくるので、そのフラグが立っていたらここで block せず exit 0 で
|
|
14
|
+
* 抜ける。これで「最初の 1 回だけ exit 2 で停止をブロックして findings
|
|
15
|
+
* を通知し、以降はユーザーの判断に委ねる」フローになる。
|
|
16
|
+
*
|
|
17
|
+
* en: Run `check --strict` once per session to surface findings, but stop
|
|
18
|
+
* blocking on subsequent invocations within the same Stop hook chain.
|
|
19
|
+
* Claude Code sets `stop_hook_active: true` on re-fires so the hook can
|
|
20
|
+
* exit cleanly instead of looping forever (see Claude Code hooks docs).
|
|
21
|
+
*/
|
|
22
|
+
export function runStopHook(target) {
|
|
23
|
+
const payload = readStdinJsonSafely();
|
|
24
|
+
|
|
25
|
+
if (payload && payload.stop_hook_active === true) {
|
|
26
|
+
process.stderr.write(
|
|
27
|
+
'sparkle-design-cli stop-hook: stop_hook_active=true を検知したため再ブロックしません。' +
|
|
28
|
+
'前回の exit 2 で findings は通知済みです。' +
|
|
29
|
+
' / Detected stop_hook_active=true; not re-blocking. Findings were already surfaced on the first call.\n'
|
|
30
|
+
);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const targets = target ? [target] : [];
|
|
35
|
+
const hasFindings = checkProject(targets, { strict: true, format: 'text' });
|
|
36
|
+
|
|
37
|
+
if (hasFindings) {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
'\nsparkle-design-cli stop-hook: findings を検出したため応答終了を 1 度だけブロックしました。' +
|
|
40
|
+
'上記の findings を修正するか、対応しない判断であればユーザーに確認してから再度応答を完了してください。' +
|
|
41
|
+
' / Blocked once because findings were detected. Fix them or confirm with the user before completing the response again.\n'
|
|
42
|
+
);
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readStdinJsonSafely() {
|
|
50
|
+
// stdin が tty / 空 / 非 JSON の場合は「初回呼び出し」として扱う。
|
|
51
|
+
// en: Treat missing or non-JSON stdin as a first invocation.
|
|
52
|
+
try {
|
|
53
|
+
if (process.stdin.isTTY) return null;
|
|
54
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
55
|
+
if (!raw || !raw.trim()) return null;
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
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",
|