cc-plan-viewer 0.2.1 → 0.2.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 +1 -1
- package/dist/cli-bundle.mjs +193 -74
- package/dist/server/bin/cc-plan-viewer.js +206 -83
- package/dist/server/server/app.js +7 -1
- package/dist/server-bundle.mjs +6 -1
- package/hooks/plan-viewer-hook.cjs +14 -8
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli-bundle.mjs
CHANGED
|
@@ -27498,7 +27498,8 @@ var init_lifecycle = __esm({
|
|
|
27498
27498
|
import { createServer } from "node:http";
|
|
27499
27499
|
import fs3 from "node:fs";
|
|
27500
27500
|
import path3 from "node:path";
|
|
27501
|
-
function createApp(
|
|
27501
|
+
function createApp(initialPlansDirs) {
|
|
27502
|
+
const plansDirs2 = [...initialPlansDirs];
|
|
27502
27503
|
const plansDir = plansDirs2[0] || "";
|
|
27503
27504
|
const app = (0, import_express.default)();
|
|
27504
27505
|
const server2 = createServer(app);
|
|
@@ -27569,6 +27570,10 @@ function createApp(plansDirs2) {
|
|
|
27569
27570
|
app.post("/api/plan-updated", (req, res) => {
|
|
27570
27571
|
const { filePath, planOptions } = req.body;
|
|
27571
27572
|
const filename = path3.basename(filePath || "");
|
|
27573
|
+
const dir = path3.dirname(filePath || "");
|
|
27574
|
+
if (dir && !plansDirs2.includes(dir) && fs3.existsSync(dir)) {
|
|
27575
|
+
plansDirs2.push(dir);
|
|
27576
|
+
}
|
|
27572
27577
|
const message = JSON.stringify({
|
|
27573
27578
|
type: "plan-updated",
|
|
27574
27579
|
filename,
|
|
@@ -27755,6 +27760,7 @@ var init_server = __esm({
|
|
|
27755
27760
|
import fs6 from "node:fs";
|
|
27756
27761
|
import path6 from "node:path";
|
|
27757
27762
|
import os3 from "node:os";
|
|
27763
|
+
import readline from "node:readline";
|
|
27758
27764
|
import { execSync } from "node:child_process";
|
|
27759
27765
|
var INSTALL_DIR = path6.join(os3.homedir(), ".cc-plan-viewer");
|
|
27760
27766
|
var INSTALLED_HOOK = path6.join(INSTALL_DIR, "plan-viewer-hook.cjs");
|
|
@@ -27769,18 +27775,6 @@ function getPkgRoot() {
|
|
|
27769
27775
|
return PKG_ROOT_DEV;
|
|
27770
27776
|
}
|
|
27771
27777
|
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
27778
|
function readSettings(settingsPath) {
|
|
27785
27779
|
try {
|
|
27786
27780
|
return JSON.parse(fs6.readFileSync(settingsPath, "utf8"));
|
|
@@ -27794,6 +27788,131 @@ function writeSettings(settingsPath, settings) {
|
|
|
27794
27788
|
fs6.mkdirSync(dir, { recursive: true });
|
|
27795
27789
|
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
27796
27790
|
}
|
|
27791
|
+
function findAllClaudeSettings() {
|
|
27792
|
+
const home = os3.homedir();
|
|
27793
|
+
const found = /* @__PURE__ */ new Set();
|
|
27794
|
+
try {
|
|
27795
|
+
for (const name of fs6.readdirSync(home)) {
|
|
27796
|
+
if (!name.startsWith(".claude"))
|
|
27797
|
+
continue;
|
|
27798
|
+
const candidate = path6.join(home, name, "settings.json");
|
|
27799
|
+
if (fs6.existsSync(candidate))
|
|
27800
|
+
found.add(candidate);
|
|
27801
|
+
}
|
|
27802
|
+
} catch {
|
|
27803
|
+
}
|
|
27804
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
27805
|
+
const candidate = path6.join(process.env.CLAUDE_CONFIG_DIR, "settings.json");
|
|
27806
|
+
if (fs6.existsSync(candidate))
|
|
27807
|
+
found.add(candidate);
|
|
27808
|
+
}
|
|
27809
|
+
return [...found].sort();
|
|
27810
|
+
}
|
|
27811
|
+
function tildePath(p) {
|
|
27812
|
+
return p.replace(os3.homedir(), "~");
|
|
27813
|
+
}
|
|
27814
|
+
async function promptMultiSelect(options, question, activeConfigDir) {
|
|
27815
|
+
if (!process.stdin.isTTY) {
|
|
27816
|
+
if (activeConfigDir) {
|
|
27817
|
+
const match = options.findIndex((p) => p.startsWith(activeConfigDir));
|
|
27818
|
+
return match >= 0 ? [match] : [0];
|
|
27819
|
+
}
|
|
27820
|
+
return options.map((_, i) => i);
|
|
27821
|
+
}
|
|
27822
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
27823
|
+
console.log("");
|
|
27824
|
+
console.log(`[cc-plan-viewer] Found ${options.length} Claude config directories:`);
|
|
27825
|
+
options.forEach((opt, i) => {
|
|
27826
|
+
const active = activeConfigDir && opt.startsWith(activeConfigDir) ? " (active)" : "";
|
|
27827
|
+
console.log(` ${i + 1}. ${tildePath(opt)}${active}`);
|
|
27828
|
+
});
|
|
27829
|
+
console.log("");
|
|
27830
|
+
return new Promise((resolve) => {
|
|
27831
|
+
rl.question(`${question} (comma-separated numbers, or "all"): `, (answer) => {
|
|
27832
|
+
rl.close();
|
|
27833
|
+
const trimmed = answer.trim().toLowerCase();
|
|
27834
|
+
if (trimmed === "all" || trimmed === "") {
|
|
27835
|
+
resolve(options.map((_, i) => i));
|
|
27836
|
+
return;
|
|
27837
|
+
}
|
|
27838
|
+
const indices = trimmed.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((i) => i >= 0 && i < options.length);
|
|
27839
|
+
resolve(indices);
|
|
27840
|
+
});
|
|
27841
|
+
});
|
|
27842
|
+
}
|
|
27843
|
+
function addHookToSettings(settingsPath) {
|
|
27844
|
+
const settings = readSettings(settingsPath);
|
|
27845
|
+
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
27846
|
+
settings.hooks = {};
|
|
27847
|
+
}
|
|
27848
|
+
const hooks = settings.hooks;
|
|
27849
|
+
if (!Array.isArray(hooks.PostToolUse)) {
|
|
27850
|
+
hooks.PostToolUse = [];
|
|
27851
|
+
}
|
|
27852
|
+
const hookCommand = getHookCommand();
|
|
27853
|
+
hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
|
|
27854
|
+
if (typeof entry !== "object" || entry === null)
|
|
27855
|
+
return true;
|
|
27856
|
+
const e = entry;
|
|
27857
|
+
if (!Array.isArray(e.hooks))
|
|
27858
|
+
return true;
|
|
27859
|
+
return !e.hooks.some((h) => {
|
|
27860
|
+
if (typeof h !== "object" || h === null)
|
|
27861
|
+
return false;
|
|
27862
|
+
const cmd = h.command;
|
|
27863
|
+
return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
|
|
27864
|
+
});
|
|
27865
|
+
});
|
|
27866
|
+
hooks.PostToolUse.push({
|
|
27867
|
+
matcher: "Write|Edit",
|
|
27868
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
27869
|
+
});
|
|
27870
|
+
writeSettings(settingsPath, settings);
|
|
27871
|
+
}
|
|
27872
|
+
function removeHookFromSettings(settingsPath) {
|
|
27873
|
+
const settings = readSettings(settingsPath);
|
|
27874
|
+
const hooks = settings.hooks;
|
|
27875
|
+
if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse))
|
|
27876
|
+
return false;
|
|
27877
|
+
const before = hooks.PostToolUse.length;
|
|
27878
|
+
hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
|
|
27879
|
+
if (typeof entry !== "object" || entry === null)
|
|
27880
|
+
return true;
|
|
27881
|
+
const e = entry;
|
|
27882
|
+
if (!Array.isArray(e.hooks))
|
|
27883
|
+
return true;
|
|
27884
|
+
return !e.hooks.some((h) => {
|
|
27885
|
+
if (typeof h !== "object" || h === null)
|
|
27886
|
+
return false;
|
|
27887
|
+
const cmd = h.command;
|
|
27888
|
+
return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
|
|
27889
|
+
});
|
|
27890
|
+
});
|
|
27891
|
+
if (hooks.PostToolUse.length < before) {
|
|
27892
|
+
writeSettings(settingsPath, settings);
|
|
27893
|
+
return true;
|
|
27894
|
+
}
|
|
27895
|
+
return false;
|
|
27896
|
+
}
|
|
27897
|
+
function settingsHasHook(settingsPath) {
|
|
27898
|
+
const settings = readSettings(settingsPath);
|
|
27899
|
+
const hooks = settings.hooks;
|
|
27900
|
+
if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse))
|
|
27901
|
+
return false;
|
|
27902
|
+
return hooks.PostToolUse.some((entry) => {
|
|
27903
|
+
if (typeof entry !== "object" || entry === null)
|
|
27904
|
+
return false;
|
|
27905
|
+
const e = entry;
|
|
27906
|
+
if (!Array.isArray(e.hooks))
|
|
27907
|
+
return false;
|
|
27908
|
+
return e.hooks.some((h) => {
|
|
27909
|
+
if (typeof h !== "object" || h === null)
|
|
27910
|
+
return false;
|
|
27911
|
+
const cmd = h.command;
|
|
27912
|
+
return typeof cmd === "string" && cmd.includes("plan-viewer-hook");
|
|
27913
|
+
});
|
|
27914
|
+
});
|
|
27915
|
+
}
|
|
27797
27916
|
function copyDir(src, dest) {
|
|
27798
27917
|
if (!fs6.existsSync(src))
|
|
27799
27918
|
return;
|
|
@@ -27836,50 +27955,48 @@ function installFiles() {
|
|
|
27836
27955
|
function getHookCommand() {
|
|
27837
27956
|
return `node "${INSTALLED_HOOK}"`;
|
|
27838
27957
|
}
|
|
27839
|
-
function install() {
|
|
27840
|
-
const settingsPath = resolveSettingsPath();
|
|
27958
|
+
async function install() {
|
|
27841
27959
|
console.log("[cc-plan-viewer] Installing...");
|
|
27842
27960
|
console.log(`[cc-plan-viewer] Install dir: ${INSTALL_DIR}`);
|
|
27843
|
-
console.log(`[cc-plan-viewer] Settings: ${settingsPath}`);
|
|
27844
27961
|
installFiles();
|
|
27845
27962
|
console.log("[cc-plan-viewer] Files copied to ~/.cc-plan-viewer/");
|
|
27846
27963
|
patchHookPaths();
|
|
27847
|
-
const
|
|
27848
|
-
if (
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
|
|
27964
|
+
const configIdx = process.argv.indexOf("--config");
|
|
27965
|
+
if (configIdx !== -1 && process.argv[configIdx + 1]) {
|
|
27966
|
+
const settingsPath = path6.resolve(process.argv[configIdx + 1]);
|
|
27967
|
+
if (!fs6.existsSync(path6.dirname(settingsPath))) {
|
|
27968
|
+
console.error(`[cc-plan-viewer] Directory does not exist: ${path6.dirname(settingsPath)}`);
|
|
27969
|
+
process.exit(1);
|
|
27970
|
+
}
|
|
27971
|
+
addHookToSettings(settingsPath);
|
|
27972
|
+
console.log(`[cc-plan-viewer] Hook added to ${tildePath(settingsPath)}`);
|
|
27973
|
+
} else {
|
|
27974
|
+
const allSettings = findAllClaudeSettings();
|
|
27975
|
+
if (allSettings.length === 0) {
|
|
27976
|
+
addHookToSettings(DEFAULT_SETTINGS);
|
|
27977
|
+
console.log(`[cc-plan-viewer] Hook added to ${tildePath(DEFAULT_SETTINGS)}`);
|
|
27978
|
+
} else if (allSettings.length === 1) {
|
|
27979
|
+
addHookToSettings(allSettings[0]);
|
|
27980
|
+
console.log(`[cc-plan-viewer] Hook added to ${tildePath(allSettings[0])}`);
|
|
27981
|
+
} else {
|
|
27982
|
+
const selected = await promptMultiSelect(allSettings, "Install hook in which configs?", process.env.CLAUDE_CONFIG_DIR);
|
|
27983
|
+
if (selected.length === 0) {
|
|
27984
|
+
console.log("[cc-plan-viewer] No configs selected. Hook not installed.");
|
|
27985
|
+
return;
|
|
27986
|
+
}
|
|
27987
|
+
for (const idx of selected) {
|
|
27988
|
+
addHookToSettings(allSettings[idx]);
|
|
27989
|
+
console.log(`[cc-plan-viewer] Hook added to ${tildePath(allSettings[idx])}`);
|
|
27990
|
+
}
|
|
27991
|
+
}
|
|
27854
27992
|
}
|
|
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);
|
|
27993
|
+
console.log("");
|
|
27874
27994
|
console.log("[cc-plan-viewer] Hook installed successfully.");
|
|
27875
27995
|
console.log("");
|
|
27876
27996
|
console.log(" Next time Claude Code writes a plan, the viewer will open in your browser.");
|
|
27877
27997
|
console.log("");
|
|
27878
27998
|
console.log(" Update anytime: npx cc-plan-viewer@latest update");
|
|
27879
27999
|
console.log(" Uninstall: npx cc-plan-viewer uninstall");
|
|
27880
|
-
if (settingsPath !== DEFAULT_SETTINGS) {
|
|
27881
|
-
console.log(` Custom config: ${settingsPath}`);
|
|
27882
|
-
}
|
|
27883
28000
|
}
|
|
27884
28001
|
function patchHookPaths() {
|
|
27885
28002
|
if (!fs6.existsSync(INSTALLED_HOOK))
|
|
@@ -27915,29 +28032,28 @@ function update() {
|
|
|
27915
28032
|
}
|
|
27916
28033
|
console.log("[cc-plan-viewer] Files updated in ~/.cc-plan-viewer/");
|
|
27917
28034
|
}
|
|
27918
|
-
function uninstall() {
|
|
27919
|
-
const settingsPath = resolveSettingsPath();
|
|
28035
|
+
async function uninstall() {
|
|
27920
28036
|
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
|
-
|
|
28037
|
+
const configIdx = process.argv.indexOf("--config");
|
|
28038
|
+
if (configIdx !== -1 && process.argv[configIdx + 1]) {
|
|
28039
|
+
const settingsPath = path6.resolve(process.argv[configIdx + 1]);
|
|
28040
|
+
if (removeHookFromSettings(settingsPath)) {
|
|
28041
|
+
console.log(`[cc-plan-viewer] Hook removed from ${tildePath(settingsPath)}`);
|
|
28042
|
+
}
|
|
28043
|
+
} else {
|
|
28044
|
+
const allSettings = findAllClaudeSettings();
|
|
28045
|
+
const withHook = allSettings.filter(settingsHasHook);
|
|
28046
|
+
if (withHook.length === 0) {
|
|
28047
|
+
console.log("[cc-plan-viewer] No hooks found in any Claude config.");
|
|
28048
|
+
} else if (withHook.length === 1) {
|
|
28049
|
+
removeHookFromSettings(withHook[0]);
|
|
28050
|
+
console.log(`[cc-plan-viewer] Hook removed from ${tildePath(withHook[0])}`);
|
|
28051
|
+
} else {
|
|
28052
|
+
const selected = await promptMultiSelect(withHook, "Remove hook from which configs?");
|
|
28053
|
+
for (const idx of selected) {
|
|
28054
|
+
removeHookFromSettings(withHook[idx]);
|
|
28055
|
+
console.log(`[cc-plan-viewer] Hook removed from ${tildePath(withHook[idx])}`);
|
|
28056
|
+
}
|
|
27941
28057
|
}
|
|
27942
28058
|
}
|
|
27943
28059
|
if (fs6.existsSync(INSTALL_DIR)) {
|
|
@@ -27977,13 +28093,13 @@ function version() {
|
|
|
27977
28093
|
var command = process.argv[2];
|
|
27978
28094
|
switch (command) {
|
|
27979
28095
|
case "install":
|
|
27980
|
-
install();
|
|
28096
|
+
await install();
|
|
27981
28097
|
break;
|
|
27982
28098
|
case "update":
|
|
27983
28099
|
update();
|
|
27984
28100
|
break;
|
|
27985
28101
|
case "uninstall":
|
|
27986
|
-
uninstall();
|
|
28102
|
+
await uninstall();
|
|
27987
28103
|
break;
|
|
27988
28104
|
case "start":
|
|
27989
28105
|
start();
|
|
@@ -27999,11 +28115,14 @@ switch (command) {
|
|
|
27999
28115
|
cc-plan-viewer \u2014 Browser-based review UI for Claude Code plans
|
|
28000
28116
|
|
|
28001
28117
|
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
|
|
28118
|
+
npx cc-plan-viewer install Install hook + viewer files
|
|
28119
|
+
npx cc-plan-viewer install --config <path> Use specific settings.json path
|
|
28120
|
+
npx cc-plan-viewer@latest update Update to latest version
|
|
28121
|
+
npx cc-plan-viewer uninstall Remove hook + viewer files
|
|
28122
|
+
npx cc-plan-viewer version Show installed version
|
|
28123
|
+
|
|
28124
|
+
When multiple Claude configs are detected (~/.claude*/settings.json),
|
|
28125
|
+
you'll be prompted to choose which ones to install the hook in.
|
|
28007
28126
|
|
|
28008
28127
|
Files are installed to ~/.cc-plan-viewer/ so they persist across npm cache clears.
|
|
28009
28128
|
`);
|
|
@@ -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
|
`);
|
|
@@ -6,7 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { parsePlan } from './planParser.js';
|
|
7
7
|
import { saveReview, getReview } from './reviewStore.js';
|
|
8
8
|
import { resetIdleTimer } from './lifecycle.js';
|
|
9
|
-
export function createApp(
|
|
9
|
+
export function createApp(initialPlansDirs) {
|
|
10
|
+
const plansDirs = [...initialPlansDirs];
|
|
10
11
|
const plansDir = plansDirs[0] || '';
|
|
11
12
|
const app = express();
|
|
12
13
|
const server = createServer(app);
|
|
@@ -86,6 +87,11 @@ export function createApp(plansDirs) {
|
|
|
86
87
|
app.post('/api/plan-updated', (req, res) => {
|
|
87
88
|
const { filePath, planOptions } = req.body;
|
|
88
89
|
const filename = path.basename(filePath || '');
|
|
90
|
+
// Dynamically register new plan directories (e.g., project-level .claude/plans/)
|
|
91
|
+
const dir = path.dirname(filePath || '');
|
|
92
|
+
if (dir && !plansDirs.includes(dir) && fs.existsSync(dir)) {
|
|
93
|
+
plansDirs.push(dir);
|
|
94
|
+
}
|
|
89
95
|
// Broadcast to all WebSocket clients
|
|
90
96
|
const message = JSON.stringify({
|
|
91
97
|
type: 'plan-updated',
|
package/dist/server-bundle.mjs
CHANGED
|
@@ -27481,7 +27481,8 @@ function resetIdleTimer() {
|
|
|
27481
27481
|
}
|
|
27482
27482
|
|
|
27483
27483
|
// dist/server/server/app.js
|
|
27484
|
-
function createApp(
|
|
27484
|
+
function createApp(initialPlansDirs) {
|
|
27485
|
+
const plansDirs2 = [...initialPlansDirs];
|
|
27485
27486
|
const plansDir = plansDirs2[0] || "";
|
|
27486
27487
|
const app = (0, import_express.default)();
|
|
27487
27488
|
const server2 = createServer(app);
|
|
@@ -27552,6 +27553,10 @@ function createApp(plansDirs2) {
|
|
|
27552
27553
|
app.post("/api/plan-updated", (req, res) => {
|
|
27553
27554
|
const { filePath, planOptions } = req.body;
|
|
27554
27555
|
const filename = path3.basename(filePath || "");
|
|
27556
|
+
const dir = path3.dirname(filePath || "");
|
|
27557
|
+
if (dir && !plansDirs2.includes(dir) && fs3.existsSync(dir)) {
|
|
27558
|
+
plansDirs2.push(dir);
|
|
27559
|
+
}
|
|
27555
27560
|
const message = JSON.stringify({
|
|
27556
27561
|
type: "plan-updated",
|
|
27557
27562
|
filename,
|
|
@@ -14,17 +14,21 @@ const PORT_FILE = path.join(os.tmpdir(), 'cc-plan-viewer-port');
|
|
|
14
14
|
const DEBOUNCE_FILE = path.join(os.tmpdir(), 'cc-plan-viewer-opened.json');
|
|
15
15
|
const DEBOUNCE_MS = 5000; // 5 seconds (prevents rapid re-opens during multi-chunk writes)
|
|
16
16
|
|
|
17
|
-
// Dynamically find all
|
|
17
|
+
// Dynamically find all plans directories (home-level + project-level)
|
|
18
18
|
function getAllPlansDirs() {
|
|
19
|
+
const dirs = [];
|
|
19
20
|
const home = os.homedir();
|
|
20
21
|
try {
|
|
21
|
-
|
|
22
|
+
fs.readdirSync(home)
|
|
22
23
|
.filter(name => name.startsWith('.claude'))
|
|
23
24
|
.map(name => path.join(home, name, 'plans'))
|
|
24
|
-
.filter(dir => fs.existsSync(dir))
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
25
|
+
.filter(dir => fs.existsSync(dir))
|
|
26
|
+
.forEach(dir => dirs.push(dir));
|
|
27
|
+
} catch {}
|
|
28
|
+
// Project-level: {cwd}/.claude/plans/
|
|
29
|
+
const projectDir = path.join(process.cwd(), '.claude', 'plans');
|
|
30
|
+
if (fs.existsSync(projectDir)) dirs.push(projectDir);
|
|
31
|
+
return dirs;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
function getServerPort() {
|
|
@@ -37,10 +41,12 @@ function getServerPort() {
|
|
|
37
41
|
|
|
38
42
|
function isPlanFile(filePath) {
|
|
39
43
|
if (!filePath || !filePath.endsWith('.md')) return false;
|
|
40
|
-
//
|
|
44
|
+
// Home-level: ~/.claude*/plans/*.md
|
|
41
45
|
const home = os.homedir();
|
|
42
46
|
const rel = path.relative(home, filePath);
|
|
43
|
-
|
|
47
|
+
if (/^\.claude[^/]*\/plans\/[^/]+\.md$/.test(rel)) return true;
|
|
48
|
+
// Project-level: */.claude/plans/*.md
|
|
49
|
+
return /\/\.claude\/plans\/[^/]+\.md$/.test(filePath);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
function shouldOpenBrowser(filename) {
|