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.
- package/dist/cli-bundle.mjs +187 -73
- package/dist/server/bin/cc-plan-viewer.js +206 -83
- package/package.json +1 -1
package/dist/cli-bundle.mjs
CHANGED
|
@@ -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
|
|
27848
|
-
if (
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
|
|
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
|
-
|
|
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
|
|
27922
|
-
|
|
27923
|
-
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
|
|
27930
|
-
|
|
27931
|
-
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
|
|
27935
|
-
|
|
27936
|
-
|
|
27937
|
-
|
|
27938
|
-
|
|
27939
|
-
|
|
27940
|
-
|
|
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
|
|
28003
|
-
npx cc-plan-viewer install --config <path> Use
|
|
28004
|
-
npx cc-plan-viewer@latest update
|
|
28005
|
-
npx cc-plan-viewer uninstall
|
|
28006
|
-
npx cc-plan-viewer 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
|
-
//
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
292
|
-
npx cc-plan-viewer install --config <path> Use
|
|
293
|
-
npx cc-plan-viewer@latest update
|
|
294
|
-
npx cc-plan-viewer uninstall
|
|
295
|
-
npx cc-plan-viewer 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
|
`);
|