cc-plan-viewer 0.2.1 → 0.2.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.
@@ -27755,6 +27755,7 @@ var init_server = __esm({
27755
27755
  import fs6 from "node:fs";
27756
27756
  import path6 from "node:path";
27757
27757
  import os3 from "node:os";
27758
+ import readline from "node:readline";
27758
27759
  import { execSync } from "node:child_process";
27759
27760
  var INSTALL_DIR = path6.join(os3.homedir(), ".cc-plan-viewer");
27760
27761
  var INSTALLED_HOOK = path6.join(INSTALL_DIR, "plan-viewer-hook.cjs");
@@ -27769,18 +27770,6 @@ function getPkgRoot() {
27769
27770
  return PKG_ROOT_DEV;
27770
27771
  }
27771
27772
  var DEFAULT_SETTINGS = path6.join(os3.homedir(), ".claude", "settings.json");
27772
- function resolveSettingsPath() {
27773
- const configIdx = process.argv.indexOf("--config");
27774
- if (configIdx !== -1 && process.argv[configIdx + 1]) {
27775
- const custom = path6.resolve(process.argv[configIdx + 1]);
27776
- if (!fs6.existsSync(path6.dirname(custom))) {
27777
- console.error(`[cc-plan-viewer] Directory does not exist: ${path6.dirname(custom)}`);
27778
- process.exit(1);
27779
- }
27780
- return custom;
27781
- }
27782
- return DEFAULT_SETTINGS;
27783
- }
27784
27773
  function readSettings(settingsPath) {
27785
27774
  try {
27786
27775
  return JSON.parse(fs6.readFileSync(settingsPath, "utf8"));
@@ -27794,6 +27783,131 @@ function writeSettings(settingsPath, settings) {
27794
27783
  fs6.mkdirSync(dir, { recursive: true });
27795
27784
  fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
27796
27785
  }
27786
+ function findAllClaudeSettings() {
27787
+ const home = os3.homedir();
27788
+ const found = /* @__PURE__ */ new Set();
27789
+ try {
27790
+ for (const name of fs6.readdirSync(home)) {
27791
+ if (!name.startsWith(".claude"))
27792
+ continue;
27793
+ const candidate = path6.join(home, name, "settings.json");
27794
+ if (fs6.existsSync(candidate))
27795
+ found.add(candidate);
27796
+ }
27797
+ } catch {
27798
+ }
27799
+ if (process.env.CLAUDE_CONFIG_DIR) {
27800
+ const candidate = path6.join(process.env.CLAUDE_CONFIG_DIR, "settings.json");
27801
+ if (fs6.existsSync(candidate))
27802
+ found.add(candidate);
27803
+ }
27804
+ return [...found].sort();
27805
+ }
27806
+ function tildePath(p) {
27807
+ return p.replace(os3.homedir(), "~");
27808
+ }
27809
+ async function promptMultiSelect(options, question, activeConfigDir) {
27810
+ if (!process.stdin.isTTY) {
27811
+ if (activeConfigDir) {
27812
+ const match = options.findIndex((p) => p.startsWith(activeConfigDir));
27813
+ return match >= 0 ? [match] : [0];
27814
+ }
27815
+ return options.map((_, i) => i);
27816
+ }
27817
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
27818
+ console.log("");
27819
+ console.log(`[cc-plan-viewer] Found ${options.length} Claude config directories:`);
27820
+ options.forEach((opt, i) => {
27821
+ const active = activeConfigDir && opt.startsWith(activeConfigDir) ? " (active)" : "";
27822
+ console.log(` ${i + 1}. ${tildePath(opt)}${active}`);
27823
+ });
27824
+ console.log("");
27825
+ return new Promise((resolve) => {
27826
+ rl.question(`${question} (comma-separated numbers, or "all"): `, (answer) => {
27827
+ rl.close();
27828
+ const trimmed = answer.trim().toLowerCase();
27829
+ if (trimmed === "all" || trimmed === "") {
27830
+ resolve(options.map((_, i) => i));
27831
+ return;
27832
+ }
27833
+ const indices = trimmed.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((i) => i >= 0 && i < options.length);
27834
+ resolve(indices);
27835
+ });
27836
+ });
27837
+ }
27838
+ function addHookToSettings(settingsPath) {
27839
+ const settings = readSettings(settingsPath);
27840
+ if (!settings.hooks || typeof settings.hooks !== "object") {
27841
+ settings.hooks = {};
27842
+ }
27843
+ const hooks = settings.hooks;
27844
+ if (!Array.isArray(hooks.PostToolUse)) {
27845
+ hooks.PostToolUse = [];
27846
+ }
27847
+ const hookCommand = getHookCommand();
27848
+ hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
27849
+ if (typeof entry !== "object" || entry === null)
27850
+ return true;
27851
+ const e = entry;
27852
+ if (!Array.isArray(e.hooks))
27853
+ return true;
27854
+ return !e.hooks.some((h) => {
27855
+ if (typeof h !== "object" || h === null)
27856
+ return false;
27857
+ const cmd = h.command;
27858
+ return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
27859
+ });
27860
+ });
27861
+ hooks.PostToolUse.push({
27862
+ matcher: "Write|Edit",
27863
+ hooks: [{ type: "command", command: hookCommand }]
27864
+ });
27865
+ writeSettings(settingsPath, settings);
27866
+ }
27867
+ function removeHookFromSettings(settingsPath) {
27868
+ const settings = readSettings(settingsPath);
27869
+ const hooks = settings.hooks;
27870
+ if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse))
27871
+ return false;
27872
+ const before = hooks.PostToolUse.length;
27873
+ hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
27874
+ if (typeof entry !== "object" || entry === null)
27875
+ return true;
27876
+ const e = entry;
27877
+ if (!Array.isArray(e.hooks))
27878
+ return true;
27879
+ return !e.hooks.some((h) => {
27880
+ if (typeof h !== "object" || h === null)
27881
+ return false;
27882
+ const cmd = h.command;
27883
+ return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
27884
+ });
27885
+ });
27886
+ if (hooks.PostToolUse.length < before) {
27887
+ writeSettings(settingsPath, settings);
27888
+ return true;
27889
+ }
27890
+ return false;
27891
+ }
27892
+ function settingsHasHook(settingsPath) {
27893
+ const settings = readSettings(settingsPath);
27894
+ const hooks = settings.hooks;
27895
+ if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse))
27896
+ return false;
27897
+ return hooks.PostToolUse.some((entry) => {
27898
+ if (typeof entry !== "object" || entry === null)
27899
+ return false;
27900
+ const e = entry;
27901
+ if (!Array.isArray(e.hooks))
27902
+ return false;
27903
+ return e.hooks.some((h) => {
27904
+ if (typeof h !== "object" || h === null)
27905
+ return false;
27906
+ const cmd = h.command;
27907
+ return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
27908
+ });
27909
+ });
27910
+ }
27797
27911
  function copyDir(src, dest) {
27798
27912
  if (!fs6.existsSync(src))
27799
27913
  return;
@@ -27836,50 +27950,48 @@ function installFiles() {
27836
27950
  function getHookCommand() {
27837
27951
  return `node "${INSTALLED_HOOK}"`;
27838
27952
  }
27839
- function install() {
27840
- const settingsPath = resolveSettingsPath();
27953
+ async function install() {
27841
27954
  console.log("[cc-plan-viewer] Installing...");
27842
27955
  console.log(`[cc-plan-viewer] Install dir: ${INSTALL_DIR}`);
27843
- console.log(`[cc-plan-viewer] Settings: ${settingsPath}`);
27844
27956
  installFiles();
27845
27957
  console.log("[cc-plan-viewer] Files copied to ~/.cc-plan-viewer/");
27846
27958
  patchHookPaths();
27847
- const settings = readSettings(settingsPath);
27848
- if (!settings.hooks || typeof settings.hooks !== "object") {
27849
- settings.hooks = {};
27850
- }
27851
- const hooks = settings.hooks;
27852
- if (!Array.isArray(hooks.PostToolUse)) {
27853
- hooks.PostToolUse = [];
27959
+ const configIdx = process.argv.indexOf("--config");
27960
+ if (configIdx !== -1 && process.argv[configIdx + 1]) {
27961
+ const settingsPath = path6.resolve(process.argv[configIdx + 1]);
27962
+ if (!fs6.existsSync(path6.dirname(settingsPath))) {
27963
+ console.error(`[cc-plan-viewer] Directory does not exist: ${path6.dirname(settingsPath)}`);
27964
+ process.exit(1);
27965
+ }
27966
+ addHookToSettings(settingsPath);
27967
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(settingsPath)}`);
27968
+ } else {
27969
+ const allSettings = findAllClaudeSettings();
27970
+ if (allSettings.length === 0) {
27971
+ addHookToSettings(DEFAULT_SETTINGS);
27972
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(DEFAULT_SETTINGS)}`);
27973
+ } else if (allSettings.length === 1) {
27974
+ addHookToSettings(allSettings[0]);
27975
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(allSettings[0])}`);
27976
+ } else {
27977
+ const selected = await promptMultiSelect(allSettings, "Install hook in which configs?", process.env.CLAUDE_CONFIG_DIR);
27978
+ if (selected.length === 0) {
27979
+ console.log("[cc-plan-viewer] No configs selected. Hook not installed.");
27980
+ return;
27981
+ }
27982
+ for (const idx of selected) {
27983
+ addHookToSettings(allSettings[idx]);
27984
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(allSettings[idx])}`);
27985
+ }
27986
+ }
27854
27987
  }
27855
- const hookCommand = getHookCommand();
27856
- hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
27857
- if (typeof entry !== "object" || entry === null)
27858
- return true;
27859
- const e = entry;
27860
- if (!Array.isArray(e.hooks))
27861
- return true;
27862
- return !e.hooks.some((h) => {
27863
- if (typeof h !== "object" || h === null)
27864
- return false;
27865
- const cmd = h.command;
27866
- return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
27867
- });
27868
- });
27869
- hooks.PostToolUse.push({
27870
- matcher: "Write|Edit",
27871
- hooks: [{ type: "command", command: hookCommand }]
27872
- });
27873
- writeSettings(settingsPath, settings);
27988
+ console.log("");
27874
27989
  console.log("[cc-plan-viewer] Hook installed successfully.");
27875
27990
  console.log("");
27876
27991
  console.log(" Next time Claude Code writes a plan, the viewer will open in your browser.");
27877
27992
  console.log("");
27878
27993
  console.log(" Update anytime: npx cc-plan-viewer@latest update");
27879
27994
  console.log(" Uninstall: npx cc-plan-viewer uninstall");
27880
- if (settingsPath !== DEFAULT_SETTINGS) {
27881
- console.log(` Custom config: ${settingsPath}`);
27882
- }
27883
27995
  }
27884
27996
  function patchHookPaths() {
27885
27997
  if (!fs6.existsSync(INSTALLED_HOOK))
@@ -27915,29 +28027,28 @@ function update() {
27915
28027
  }
27916
28028
  console.log("[cc-plan-viewer] Files updated in ~/.cc-plan-viewer/");
27917
28029
  }
27918
- function uninstall() {
27919
- const settingsPath = resolveSettingsPath();
28030
+ async function uninstall() {
27920
28031
  console.log("[cc-plan-viewer] Uninstalling...");
27921
- const settings = readSettings(settingsPath);
27922
- const hooks = settings.hooks;
27923
- if (hooks?.PostToolUse && Array.isArray(hooks.PostToolUse)) {
27924
- const before = hooks.PostToolUse.length;
27925
- hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
27926
- if (typeof entry !== "object" || entry === null)
27927
- return true;
27928
- const e = entry;
27929
- if (!Array.isArray(e.hooks))
27930
- return true;
27931
- return !e.hooks.some((h) => {
27932
- if (typeof h !== "object" || h === null)
27933
- return false;
27934
- const cmd = h.command;
27935
- return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
27936
- });
27937
- });
27938
- if (hooks.PostToolUse.length < before) {
27939
- writeSettings(settingsPath, settings);
27940
- console.log(`[cc-plan-viewer] Hook removed from ${settingsPath}`);
28032
+ const configIdx = process.argv.indexOf("--config");
28033
+ if (configIdx !== -1 && process.argv[configIdx + 1]) {
28034
+ const settingsPath = path6.resolve(process.argv[configIdx + 1]);
28035
+ if (removeHookFromSettings(settingsPath)) {
28036
+ console.log(`[cc-plan-viewer] Hook removed from ${tildePath(settingsPath)}`);
28037
+ }
28038
+ } else {
28039
+ const allSettings = findAllClaudeSettings();
28040
+ const withHook = allSettings.filter(settingsHasHook);
28041
+ if (withHook.length === 0) {
28042
+ console.log("[cc-plan-viewer] No hooks found in any Claude config.");
28043
+ } else if (withHook.length === 1) {
28044
+ removeHookFromSettings(withHook[0]);
28045
+ console.log(`[cc-plan-viewer] Hook removed from ${tildePath(withHook[0])}`);
28046
+ } else {
28047
+ const selected = await promptMultiSelect(withHook, "Remove hook from which configs?");
28048
+ for (const idx of selected) {
28049
+ removeHookFromSettings(withHook[idx]);
28050
+ console.log(`[cc-plan-viewer] Hook removed from ${tildePath(withHook[idx])}`);
28051
+ }
27941
28052
  }
27942
28053
  }
27943
28054
  if (fs6.existsSync(INSTALL_DIR)) {
@@ -27977,13 +28088,13 @@ function version() {
27977
28088
  var command = process.argv[2];
27978
28089
  switch (command) {
27979
28090
  case "install":
27980
- install();
28091
+ await install();
27981
28092
  break;
27982
28093
  case "update":
27983
28094
  update();
27984
28095
  break;
27985
28096
  case "uninstall":
27986
- uninstall();
28097
+ await uninstall();
27987
28098
  break;
27988
28099
  case "start":
27989
28100
  start();
@@ -27999,11 +28110,14 @@ switch (command) {
27999
28110
  cc-plan-viewer \u2014 Browser-based review UI for Claude Code plans
28000
28111
 
28001
28112
  Usage:
28002
- npx cc-plan-viewer install Install hook + viewer files
28003
- npx cc-plan-viewer install --config <path> Use custom settings.json path
28004
- npx cc-plan-viewer@latest update Update to latest version
28005
- npx cc-plan-viewer uninstall Remove hook + viewer files
28006
- npx cc-plan-viewer version Show installed version
28113
+ npx cc-plan-viewer install Install hook + viewer files
28114
+ npx cc-plan-viewer install --config <path> Use specific settings.json path
28115
+ npx cc-plan-viewer@latest update Update to latest version
28116
+ npx cc-plan-viewer uninstall Remove hook + viewer files
28117
+ npx cc-plan-viewer version Show installed version
28118
+
28119
+ When multiple Claude configs are detected (~/.claude*/settings.json),
28120
+ you'll be prompted to choose which ones to install the hook in.
28007
28121
 
28008
28122
  Files are installed to ~/.cc-plan-viewer/ so they persist across npm cache clears.
28009
28123
  `);
@@ -2,6 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
+ import readline from 'node:readline';
5
6
  import { execSync } from 'node:child_process';
6
7
  // ─── Stable install location ───
7
8
  const INSTALL_DIR = path.join(os.homedir(), '.cc-plan-viewer');
@@ -23,19 +24,6 @@ function getPkgRoot() {
23
24
  }
24
25
  // ─── Settings file resolution ───
25
26
  const DEFAULT_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
26
- function resolveSettingsPath() {
27
- // Check --config flag
28
- const configIdx = process.argv.indexOf('--config');
29
- if (configIdx !== -1 && process.argv[configIdx + 1]) {
30
- const custom = path.resolve(process.argv[configIdx + 1]);
31
- if (!fs.existsSync(path.dirname(custom))) {
32
- console.error(`[cc-plan-viewer] Directory does not exist: ${path.dirname(custom)}`);
33
- process.exit(1);
34
- }
35
- return custom;
36
- }
37
- return DEFAULT_SETTINGS;
38
- }
39
27
  function readSettings(settingsPath) {
40
28
  try {
41
29
  return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -50,6 +38,139 @@ function writeSettings(settingsPath, settings) {
50
38
  fs.mkdirSync(dir, { recursive: true });
51
39
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
52
40
  }
41
+ // ─── Multi-config detection ───
42
+ function findAllClaudeSettings() {
43
+ const home = os.homedir();
44
+ const found = new Set();
45
+ // Scan ~/.claude*/settings.json
46
+ try {
47
+ for (const name of fs.readdirSync(home)) {
48
+ if (!name.startsWith('.claude'))
49
+ continue;
50
+ const candidate = path.join(home, name, 'settings.json');
51
+ if (fs.existsSync(candidate))
52
+ found.add(candidate);
53
+ }
54
+ }
55
+ catch { }
56
+ // Include CLAUDE_CONFIG_DIR if set (may point outside ~/.claude*)
57
+ if (process.env.CLAUDE_CONFIG_DIR) {
58
+ const candidate = path.join(process.env.CLAUDE_CONFIG_DIR, 'settings.json');
59
+ if (fs.existsSync(candidate))
60
+ found.add(candidate);
61
+ }
62
+ return [...found].sort();
63
+ }
64
+ function tildePath(p) {
65
+ return p.replace(os.homedir(), '~');
66
+ }
67
+ async function promptMultiSelect(options, question, activeConfigDir) {
68
+ // Non-interactive: fall back to CLAUDE_CONFIG_DIR or all
69
+ if (!process.stdin.isTTY) {
70
+ if (activeConfigDir) {
71
+ const match = options.findIndex(p => p.startsWith(activeConfigDir));
72
+ return match >= 0 ? [match] : [0];
73
+ }
74
+ return options.map((_, i) => i);
75
+ }
76
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
77
+ console.log('');
78
+ console.log(`[cc-plan-viewer] Found ${options.length} Claude config directories:`);
79
+ options.forEach((opt, i) => {
80
+ const active = activeConfigDir && opt.startsWith(activeConfigDir) ? ' (active)' : '';
81
+ console.log(` ${i + 1}. ${tildePath(opt)}${active}`);
82
+ });
83
+ console.log('');
84
+ return new Promise((resolve) => {
85
+ rl.question(`${question} (comma-separated numbers, or "all"): `, (answer) => {
86
+ rl.close();
87
+ const trimmed = answer.trim().toLowerCase();
88
+ if (trimmed === 'all' || trimmed === '') {
89
+ resolve(options.map((_, i) => i));
90
+ return;
91
+ }
92
+ const indices = trimmed.split(',')
93
+ .map(s => parseInt(s.trim(), 10) - 1)
94
+ .filter(i => i >= 0 && i < options.length);
95
+ resolve(indices);
96
+ });
97
+ });
98
+ }
99
+ function addHookToSettings(settingsPath) {
100
+ const settings = readSettings(settingsPath);
101
+ if (!settings.hooks || typeof settings.hooks !== 'object') {
102
+ settings.hooks = {};
103
+ }
104
+ const hooks = settings.hooks;
105
+ if (!Array.isArray(hooks.PostToolUse)) {
106
+ hooks.PostToolUse = [];
107
+ }
108
+ const hookCommand = getHookCommand();
109
+ // Remove any existing cc-plan-viewer hooks first (handles upgrades)
110
+ hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
111
+ if (typeof entry !== 'object' || entry === null)
112
+ return true;
113
+ const e = entry;
114
+ if (!Array.isArray(e.hooks))
115
+ return true;
116
+ return !e.hooks.some((h) => {
117
+ if (typeof h !== 'object' || h === null)
118
+ return false;
119
+ const cmd = h.command;
120
+ return typeof cmd === 'string' && cmd.includes('plan-viewer-hook');
121
+ });
122
+ });
123
+ // Add fresh hook entry
124
+ hooks.PostToolUse.push({
125
+ matcher: 'Write|Edit',
126
+ hooks: [{ type: 'command', command: hookCommand }],
127
+ });
128
+ writeSettings(settingsPath, settings);
129
+ }
130
+ function removeHookFromSettings(settingsPath) {
131
+ const settings = readSettings(settingsPath);
132
+ const hooks = settings.hooks;
133
+ if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse))
134
+ return false;
135
+ const before = hooks.PostToolUse.length;
136
+ hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
137
+ if (typeof entry !== 'object' || entry === null)
138
+ return true;
139
+ const e = entry;
140
+ if (!Array.isArray(e.hooks))
141
+ return true;
142
+ return !e.hooks.some((h) => {
143
+ if (typeof h !== 'object' || h === null)
144
+ return false;
145
+ const cmd = h.command;
146
+ return typeof cmd === 'string' && cmd.includes('plan-viewer-hook');
147
+ });
148
+ });
149
+ if (hooks.PostToolUse.length < before) {
150
+ writeSettings(settingsPath, settings);
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+ function settingsHasHook(settingsPath) {
156
+ const settings = readSettings(settingsPath);
157
+ const hooks = settings.hooks;
158
+ if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse))
159
+ return false;
160
+ return hooks.PostToolUse.some((entry) => {
161
+ if (typeof entry !== 'object' || entry === null)
162
+ return false;
163
+ const e = entry;
164
+ if (!Array.isArray(e.hooks))
165
+ return false;
166
+ return e.hooks.some((h) => {
167
+ if (typeof h !== 'object' || h === null)
168
+ return false;
169
+ const cmd = h.command;
170
+ return typeof cmd === 'string' && cmd.includes('plan-viewer-hook');
171
+ });
172
+ });
173
+ }
53
174
  // ─── Copy files to stable location ───
54
175
  function copyDir(src, dest) {
55
176
  if (!fs.existsSync(src))
@@ -100,60 +221,57 @@ function installFiles() {
100
221
  function getHookCommand() {
101
222
  return `node "${INSTALLED_HOOK}"`;
102
223
  }
103
- function install() {
104
- const settingsPath = resolveSettingsPath();
224
+ async function install() {
105
225
  console.log('[cc-plan-viewer] Installing...');
106
226
  console.log(`[cc-plan-viewer] Install dir: ${INSTALL_DIR}`);
107
- console.log(`[cc-plan-viewer] Settings: ${settingsPath}`);
108
227
  // Copy files to stable location
109
228
  installFiles();
110
229
  console.log('[cc-plan-viewer] Files copied to ~/.cc-plan-viewer/');
111
- // Patch the installed hook to know where the server is
112
- // The hook uses __dirname to find the server — now it's in ~/.cc-plan-viewer/
113
- // and the server is at ~/.cc-plan-viewer/server/server/index.js
114
- // Hook already resolves path.join(__dirname, '..', 'dist', 'server', 'server', 'index.js')
115
- // but now it should look at path.join(__dirname, '..', 'server', 'server', 'index.js')
116
- // Let's patch the hook to check both locations
117
230
  patchHookPaths();
118
- // Add hook to settings
119
- const settings = readSettings(settingsPath);
120
- if (!settings.hooks || typeof settings.hooks !== 'object') {
121
- settings.hooks = {};
231
+ // Determine which settings files to update
232
+ const configIdx = process.argv.indexOf('--config');
233
+ if (configIdx !== -1 && process.argv[configIdx + 1]) {
234
+ // Explicit --config flag: use exactly that path
235
+ const settingsPath = path.resolve(process.argv[configIdx + 1]);
236
+ if (!fs.existsSync(path.dirname(settingsPath))) {
237
+ console.error(`[cc-plan-viewer] Directory does not exist: ${path.dirname(settingsPath)}`);
238
+ process.exit(1);
239
+ }
240
+ addHookToSettings(settingsPath);
241
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(settingsPath)}`);
122
242
  }
123
- const hooks = settings.hooks;
124
- if (!Array.isArray(hooks.PostToolUse)) {
125
- hooks.PostToolUse = [];
243
+ else {
244
+ const allSettings = findAllClaudeSettings();
245
+ if (allSettings.length === 0) {
246
+ // No existing configs — create default
247
+ addHookToSettings(DEFAULT_SETTINGS);
248
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(DEFAULT_SETTINGS)}`);
249
+ }
250
+ else if (allSettings.length === 1) {
251
+ // Single config — use it directly
252
+ addHookToSettings(allSettings[0]);
253
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(allSettings[0])}`);
254
+ }
255
+ else {
256
+ // Multiple configs — prompt user
257
+ const selected = await promptMultiSelect(allSettings, 'Install hook in which configs?', process.env.CLAUDE_CONFIG_DIR);
258
+ if (selected.length === 0) {
259
+ console.log('[cc-plan-viewer] No configs selected. Hook not installed.');
260
+ return;
261
+ }
262
+ for (const idx of selected) {
263
+ addHookToSettings(allSettings[idx]);
264
+ console.log(`[cc-plan-viewer] Hook added to ${tildePath(allSettings[idx])}`);
265
+ }
266
+ }
126
267
  }
127
- const hookCommand = getHookCommand();
128
- // Remove any existing cc-plan-viewer hooks first (handles upgrades)
129
- hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
130
- if (typeof entry !== 'object' || entry === null)
131
- return true;
132
- const e = entry;
133
- if (!Array.isArray(e.hooks))
134
- return true;
135
- return !e.hooks.some((h) => {
136
- if (typeof h !== 'object' || h === null)
137
- return false;
138
- const cmd = h.command;
139
- return typeof cmd === 'string' && cmd.includes('plan-viewer-hook');
140
- });
141
- });
142
- // Add fresh hook entry
143
- hooks.PostToolUse.push({
144
- matcher: 'Write|Edit',
145
- hooks: [{ type: 'command', command: hookCommand }],
146
- });
147
- writeSettings(settingsPath, settings);
268
+ console.log('');
148
269
  console.log('[cc-plan-viewer] Hook installed successfully.');
149
270
  console.log('');
150
271
  console.log(' Next time Claude Code writes a plan, the viewer will open in your browser.');
151
272
  console.log('');
152
273
  console.log(' Update anytime: npx cc-plan-viewer@latest update');
153
274
  console.log(' Uninstall: npx cc-plan-viewer uninstall');
154
- if (settingsPath !== DEFAULT_SETTINGS) {
155
- console.log(` Custom config: ${settingsPath}`);
156
- }
157
275
  }
158
276
  function patchHookPaths() {
159
277
  // The installed hook is at ~/.cc-plan-viewer/plan-viewer-hook.cjs
@@ -197,30 +315,32 @@ function update() {
197
315
  }
198
316
  console.log('[cc-plan-viewer] Files updated in ~/.cc-plan-viewer/');
199
317
  }
200
- function uninstall() {
201
- const settingsPath = resolveSettingsPath();
318
+ async function uninstall() {
202
319
  console.log('[cc-plan-viewer] Uninstalling...');
203
- // Remove hook from settings
204
- const settings = readSettings(settingsPath);
205
- const hooks = settings.hooks;
206
- if (hooks?.PostToolUse && Array.isArray(hooks.PostToolUse)) {
207
- const before = hooks.PostToolUse.length;
208
- hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
209
- if (typeof entry !== 'object' || entry === null)
210
- return true;
211
- const e = entry;
212
- if (!Array.isArray(e.hooks))
213
- return true;
214
- return !e.hooks.some((h) => {
215
- if (typeof h !== 'object' || h === null)
216
- return false;
217
- const cmd = h.command;
218
- return typeof cmd === 'string' && cmd.includes('plan-viewer-hook');
219
- });
220
- });
221
- if (hooks.PostToolUse.length < before) {
222
- writeSettings(settingsPath, settings);
223
- console.log(`[cc-plan-viewer] Hook removed from ${settingsPath}`);
320
+ // Determine which settings files to clean up
321
+ const configIdx = process.argv.indexOf('--config');
322
+ if (configIdx !== -1 && process.argv[configIdx + 1]) {
323
+ const settingsPath = path.resolve(process.argv[configIdx + 1]);
324
+ if (removeHookFromSettings(settingsPath)) {
325
+ console.log(`[cc-plan-viewer] Hook removed from ${tildePath(settingsPath)}`);
326
+ }
327
+ }
328
+ else {
329
+ const allSettings = findAllClaudeSettings();
330
+ const withHook = allSettings.filter(settingsHasHook);
331
+ if (withHook.length === 0) {
332
+ console.log('[cc-plan-viewer] No hooks found in any Claude config.');
333
+ }
334
+ else if (withHook.length === 1) {
335
+ removeHookFromSettings(withHook[0]);
336
+ console.log(`[cc-plan-viewer] Hook removed from ${tildePath(withHook[0])}`);
337
+ }
338
+ else {
339
+ const selected = await promptMultiSelect(withHook, 'Remove hook from which configs?');
340
+ for (const idx of selected) {
341
+ removeHookFromSettings(withHook[idx]);
342
+ console.log(`[cc-plan-viewer] Hook removed from ${tildePath(withHook[idx])}`);
343
+ }
224
344
  }
225
345
  }
226
346
  // Remove installed files
@@ -266,13 +386,13 @@ function version() {
266
386
  const command = process.argv[2];
267
387
  switch (command) {
268
388
  case 'install':
269
- install();
389
+ await install();
270
390
  break;
271
391
  case 'update':
272
392
  update();
273
393
  break;
274
394
  case 'uninstall':
275
- uninstall();
395
+ await uninstall();
276
396
  break;
277
397
  case 'start':
278
398
  start();
@@ -288,11 +408,14 @@ switch (command) {
288
408
  cc-plan-viewer — Browser-based review UI for Claude Code plans
289
409
 
290
410
  Usage:
291
- npx cc-plan-viewer install Install hook + viewer files
292
- npx cc-plan-viewer install --config <path> Use custom settings.json path
293
- npx cc-plan-viewer@latest update Update to latest version
294
- npx cc-plan-viewer uninstall Remove hook + viewer files
295
- npx cc-plan-viewer version Show installed version
411
+ npx cc-plan-viewer install Install hook + viewer files
412
+ npx cc-plan-viewer install --config <path> Use specific settings.json path
413
+ npx cc-plan-viewer@latest update Update to latest version
414
+ npx cc-plan-viewer uninstall Remove hook + viewer files
415
+ npx cc-plan-viewer version Show installed version
416
+
417
+ When multiple Claude configs are detected (~/.claude*/settings.json),
418
+ you'll be prompted to choose which ones to install the hook in.
296
419
 
297
420
  Files are installed to ~/.cc-plan-viewer/ so they persist across npm cache clears.
298
421
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-plan-viewer",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Browser-based PR-style review UI for Claude Code plans",
5
5
  "type": "module",
6
6
  "bin": {