ccdesk 0.0.0-stage → 0.2.0
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/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +131 -2
- package/dist/server/agent-edit.js +77 -0
- package/dist/server/backups.js +303 -0
- package/dist/server/cli.js +105 -0
- package/dist/server/mcp-edit.js +263 -0
- package/dist/server/mutations.js +193 -0
- package/dist/server/open.js +139 -0
- package/dist/server/scan/compare.js +34 -0
- package/dist/server/scan/context.js +140 -0
- package/dist/server/scan/env.js +28 -0
- package/dist/server/scan/files.js +478 -0
- package/dist/server/scan/index.js +280 -0
- package/dist/server/scan/instructions.js +289 -0
- package/dist/server/scan/inventory.js +452 -0
- package/dist/server/scan/mcp.js +313 -0
- package/dist/server/scan/memory.js +90 -0
- package/dist/server/scan/path-test.js +121 -0
- package/dist/server/scan/permissions.js +101 -0
- package/dist/server/scan/plugins.js +124 -0
- package/dist/server/scan/projects.js +101 -0
- package/dist/server/scan/sandbox.js +161 -0
- package/dist/server/scan/settings.js +330 -0
- package/dist/server/scan/tools.js +111 -0
- package/dist/server/scan/usage.js +514 -0
- package/dist/server/schema.js +15 -0
- package/dist/server/secrets.js +63 -0
- package/dist/server/server.js +352 -0
- package/dist/server/settings-edit.js +221 -0
- package/dist/server/watch.js +214 -0
- package/dist/server/write.js +233 -0
- package/dist/shared/builtin-tools.json +235 -0
- package/dist/shared/compare.js +1 -0
- package/dist/shared/hook-events.json +35 -0
- package/dist/shared/mcp-server.schema.json +25 -0
- package/dist/shared/memory.js +4 -0
- package/dist/shared/scopes.js +20 -0
- package/dist/shared/settings-scopes.json +208 -0
- package/dist/shared/settings.schema.json +4623 -0
- package/dist/shared/types.js +2 -0
- package/dist/ui/assets/hanken-grotesk-cyrillic-ext-wght-normal-D_UHSL_T.woff2 +0 -0
- package/dist/ui/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/dist/ui/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/dist/ui/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/dist/ui/assets/index-BQ3Lhx8z.js +60 -0
- package/dist/ui/assets/index-BRgGlpjd.css +1 -0
- package/dist/ui/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/ui/index.html +15 -0
- package/dist/ui/logo.png +0 -0
- package/package.json +86 -4
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { delimiter, join, posix, win32 } from "node:path";
|
|
4
|
+
/** Editors that need a terminal; started from a background server they would have none. */
|
|
5
|
+
const TERMINAL_EDITORS = new Set([
|
|
6
|
+
"vi",
|
|
7
|
+
"vim",
|
|
8
|
+
"nvim",
|
|
9
|
+
"nano",
|
|
10
|
+
"emacs",
|
|
11
|
+
"hx",
|
|
12
|
+
"helix",
|
|
13
|
+
"micro",
|
|
14
|
+
"kak",
|
|
15
|
+
"ed",
|
|
16
|
+
]);
|
|
17
|
+
/** Graphical editors ccdesk looks for when VISUAL and EDITOR name none. */
|
|
18
|
+
const GUI_EDITORS = [
|
|
19
|
+
"code",
|
|
20
|
+
"cursor",
|
|
21
|
+
"zed",
|
|
22
|
+
"codium",
|
|
23
|
+
"subl",
|
|
24
|
+
"kate",
|
|
25
|
+
"gnome-text-editor",
|
|
26
|
+
"gedit",
|
|
27
|
+
];
|
|
28
|
+
/** An editor still running this long after it started is taken to have opened the file. */
|
|
29
|
+
const START_MS = 1500;
|
|
30
|
+
/** Whether `name` is an executable on PATH. */
|
|
31
|
+
function onPath(env, platform, name) {
|
|
32
|
+
const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
|
|
33
|
+
for (const dir of (env.PATH ?? "").split(platform === "win32" ? ";" : delimiter)) {
|
|
34
|
+
for (const ext of extensions) {
|
|
35
|
+
try {
|
|
36
|
+
accessSync(join(dir, name + ext), platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Not in this folder.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The commands to try, in order: `CCDESK_EDITOR` alone when set; else `VISUAL` or `EDITOR` when
|
|
48
|
+
* they name a graphical editor, then graphical editors found on PATH, then the system's opener.
|
|
49
|
+
* The system opener comes last because on Linux it often picks a terminal editor or nothing for
|
|
50
|
+
* JSON and Markdown.
|
|
51
|
+
*/
|
|
52
|
+
export function editorCommands(env, platform, found = (name) => onPath(env, platform, name)) {
|
|
53
|
+
const split = (value) => {
|
|
54
|
+
const [command = "", ...args] = words(value);
|
|
55
|
+
// A wait flag would keep the editor tied to this server's process.
|
|
56
|
+
return [command, args.filter((a) => a !== "-w" && a !== "--wait")];
|
|
57
|
+
};
|
|
58
|
+
const name = (command) => (platform === "win32" ? win32 : posix)
|
|
59
|
+
.basename(command)
|
|
60
|
+
.replace(/\.(exe|cmd|bat)$/i, "")
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
if (env.CCDESK_EDITOR)
|
|
63
|
+
return [split(env.CCDESK_EDITOR)];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const value of [env.VISUAL, env.EDITOR]) {
|
|
66
|
+
if (!value)
|
|
67
|
+
continue;
|
|
68
|
+
const command = split(value);
|
|
69
|
+
if (!TERMINAL_EDITORS.has(name(command[0])))
|
|
70
|
+
out.push(command);
|
|
71
|
+
}
|
|
72
|
+
for (const editor of GUI_EDITORS)
|
|
73
|
+
if (found(editor))
|
|
74
|
+
out.push([editor, []]);
|
|
75
|
+
out.push(platform === "darwin"
|
|
76
|
+
? ["open", []]
|
|
77
|
+
: platform === "win32"
|
|
78
|
+
? ["explorer", []]
|
|
79
|
+
: ["xdg-open", []]);
|
|
80
|
+
return out.filter((c, i) => out.findIndex((o) => o[0] === c[0]) === i);
|
|
81
|
+
}
|
|
82
|
+
/** Splits an editor setting into words, keeping quoted parts such as "C:\Program Files\…" whole. */
|
|
83
|
+
function words(value) {
|
|
84
|
+
return [...value.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* How to start `command` on `path`. On Windows a .cmd or .bat file (VS Code's `code`) or a bare
|
|
88
|
+
* name that may resolve to one cannot be started directly, so it goes through cmd.exe, quoted.
|
|
89
|
+
*/
|
|
90
|
+
export function spawnPlan(command, args, path, platform) {
|
|
91
|
+
const direct = { file: command, args: [...args, path], verbatim: false };
|
|
92
|
+
if (platform !== "win32" || /\.(exe|com)$/i.test(command) || command === "explorer")
|
|
93
|
+
return direct;
|
|
94
|
+
// Inside cmd.exe quotes, " cannot be escaped and %NAME% still expands.
|
|
95
|
+
if ([command, ...args, path].some((part) => /["%]/.test(part)))
|
|
96
|
+
throw new Error("This path cannot be passed safely to a .cmd editor; open it yourself.");
|
|
97
|
+
const line = [command, ...args, path].map((part) => `"${part}"`).join(" ");
|
|
98
|
+
return { file: "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], verbatim: true };
|
|
99
|
+
}
|
|
100
|
+
/** Starts one command; resolves null once it is running or exited cleanly, else why it failed. */
|
|
101
|
+
function launch([command, args], path) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
let plan;
|
|
104
|
+
try {
|
|
105
|
+
plan = spawnPlan(command, args, path, process.platform);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return resolve(error.message);
|
|
109
|
+
}
|
|
110
|
+
const child = spawn(plan.file, plan.args, {
|
|
111
|
+
detached: true,
|
|
112
|
+
stdio: "ignore",
|
|
113
|
+
windowsVerbatimArguments: plan.verbatim,
|
|
114
|
+
});
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
child.unref();
|
|
117
|
+
resolve(null);
|
|
118
|
+
}, START_MS);
|
|
119
|
+
child.once("error", (error) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
resolve(`${command}: ${error.message}`);
|
|
122
|
+
});
|
|
123
|
+
child.once("exit", (code) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
resolve(code === 0 ? null : `${command} exited with code ${code}`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Opens a file or folder in the first editor that starts; resolves with the command used. */
|
|
130
|
+
export async function openInEditor(path) {
|
|
131
|
+
const failures = [];
|
|
132
|
+
for (const command of editorCommands(process.env, process.platform)) {
|
|
133
|
+
const failure = await launch(command, path);
|
|
134
|
+
if (failure === null)
|
|
135
|
+
return command[0];
|
|
136
|
+
failures.push(failure);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Could not open it (${failures.join("; ")}). Set CCDESK_EDITOR to your editor.`);
|
|
139
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import {} from "../../shared/compare.js";
|
|
3
|
+
import {} from "./env.js";
|
|
4
|
+
import { effectiveSettings, readSettingsLayers } from "./settings.js";
|
|
5
|
+
/** Each key Claude Code applies in `dir`, with its value and the files it comes from. */
|
|
6
|
+
async function valuesIn(env, dir) {
|
|
7
|
+
const layers = await readSettingsLayers(env, dir);
|
|
8
|
+
const values = new Map();
|
|
9
|
+
for (const setting of effectiveSettings(layers)) {
|
|
10
|
+
// A key every file sets where Claude Code does not read it applies nowhere.
|
|
11
|
+
if (setting.from.length === 0)
|
|
12
|
+
continue;
|
|
13
|
+
const sources = setting.from.flatMap((scope) => {
|
|
14
|
+
const layer = layers.find((l) => l.scope === scope && l.data?.[setting.key] !== undefined);
|
|
15
|
+
return layer ? [{ scope, path: layer.path }] : [];
|
|
16
|
+
});
|
|
17
|
+
values.set(setting.key, { value: setting.value, sources });
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The effective settings of two directories side by side. Values are compared as ccdesk shows
|
|
23
|
+
* them, so hidden values stay hidden and two of them count as the same.
|
|
24
|
+
*/
|
|
25
|
+
export async function compareSettings(env, a, b) {
|
|
26
|
+
const [left, right] = await Promise.all([valuesIn(env, resolve(a)), valuesIn(env, resolve(b))]);
|
|
27
|
+
const keys = [...new Set([...left.keys(), ...right.keys()])].sort();
|
|
28
|
+
const settings = keys.map((key) => {
|
|
29
|
+
const x = left.get(key) ?? null;
|
|
30
|
+
const y = right.get(key) ?? null;
|
|
31
|
+
return { key, a: x, b: y, same: JSON.stringify(x?.value) === JSON.stringify(y?.value) };
|
|
32
|
+
});
|
|
33
|
+
return { a: resolve(a), b: resolve(b), settings };
|
|
34
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
|
+
import {} from "../../shared/types.js";
|
|
3
|
+
import {} from "./env.js";
|
|
4
|
+
import { isWithin, parseFrontmatter, readTextFile } from "./files.js";
|
|
5
|
+
/**
|
|
6
|
+
* Imports can name any file, so a cloned repo's CLAUDE.md could point at private files.
|
|
7
|
+
* ccdesk shows an import's text only from the project, your Claude folders and ~/.agents.
|
|
8
|
+
*/
|
|
9
|
+
function showable(env, projectRoot, path) {
|
|
10
|
+
if (basename(path) === ".credentials.json")
|
|
11
|
+
return false;
|
|
12
|
+
return [projectRoot, env.claudeDir, join(env.home, ".agents"), env.managedDir].some((root) => isWithin(root, path));
|
|
13
|
+
}
|
|
14
|
+
/** What Claude Code injects from an instruction file: no frontmatter, no block-level HTML comments. */
|
|
15
|
+
function injectedText(text) {
|
|
16
|
+
const body = parseFrontmatter(text).body;
|
|
17
|
+
// Comments inside code fences are kept, so only text outside fences is stripped.
|
|
18
|
+
return body
|
|
19
|
+
.split(/(^(?:```|~~~)[\s\S]*?^(?:```|~~~)[^\n]*$)/m)
|
|
20
|
+
.map((part, i) => (i % 2 === 1 ? part : part.replace(/^[ \t]*<!--[\s\S]*?-->[ \t]*\n?/gm, "")))
|
|
21
|
+
.join("");
|
|
22
|
+
}
|
|
23
|
+
function block(fields, text) {
|
|
24
|
+
return {
|
|
25
|
+
...fields,
|
|
26
|
+
text,
|
|
27
|
+
lines: text === null || text === "" ? 0 : text.split("\n").length,
|
|
28
|
+
bytes: text === null ? 0 : Buffer.byteLength(text, "utf8"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Claude Code caps each skill's description and when_to_use at this many characters in its listing. */
|
|
32
|
+
const SKILL_DESCRIPTION_CAP = 1536;
|
|
33
|
+
const BUILT_IN_STYLES = ["default", "explanatory", "learning"];
|
|
34
|
+
/** Skills and command files Claude can pick on its own; skillOverrides "name-only" drops the description. */
|
|
35
|
+
function skillListing(skills) {
|
|
36
|
+
return skills
|
|
37
|
+
.map((s) => {
|
|
38
|
+
const name = s.id ?? s.name;
|
|
39
|
+
if (s.override === "name-only")
|
|
40
|
+
return `- ${name}`;
|
|
41
|
+
const text = [s.description, s.whenToUse].filter(Boolean).join(" ");
|
|
42
|
+
return `- ${name}: ${text.slice(0, SKILL_DESCRIPTION_CAP)}`;
|
|
43
|
+
})
|
|
44
|
+
.join("\n");
|
|
45
|
+
}
|
|
46
|
+
/** The scope a listing shows: the one its entries share, else user. */
|
|
47
|
+
function commonScope(entries) {
|
|
48
|
+
const first = entries[0]?.scope ?? "user";
|
|
49
|
+
return entries.every((e) => e.scope === first) ? first : "user";
|
|
50
|
+
}
|
|
51
|
+
/** What Claude Code puts in context at session start in this directory, in load order. */
|
|
52
|
+
export async function buildContext(input) {
|
|
53
|
+
const { env, projectRoot, instructions, memory, inventory, effective } = input;
|
|
54
|
+
const blocks = [
|
|
55
|
+
block({
|
|
56
|
+
kind: "not-counted",
|
|
57
|
+
path: "Claude Code's system prompt and built-in tools",
|
|
58
|
+
scope: "managed",
|
|
59
|
+
note: "Part of every session; their size depends on your Claude Code version and model",
|
|
60
|
+
}, null),
|
|
61
|
+
];
|
|
62
|
+
const style = effective.find((s) => s.key === "outputStyle")?.value;
|
|
63
|
+
if (typeof style === "string" && !BUILT_IN_STYLES.includes(style.toLowerCase())) {
|
|
64
|
+
const custom = inventory.outputStyles.find((s) => s.name === style);
|
|
65
|
+
blocks.push(custom
|
|
66
|
+
? block({ kind: "output-style", path: custom.path, scope: custom.scope }, parseFrontmatter(custom.text).body)
|
|
67
|
+
: block({
|
|
68
|
+
kind: "not-counted",
|
|
69
|
+
path: `Output style "${style}"`,
|
|
70
|
+
scope: "user",
|
|
71
|
+
note: "ccdesk could not find this style's file",
|
|
72
|
+
}, null));
|
|
73
|
+
}
|
|
74
|
+
// A file both imported and loaded on its own (a rule, an AGENTS.md) is counted once, as itself.
|
|
75
|
+
const loaded = new Set(instructions.filter((i) => i.status === "loaded").map((i) => i.path));
|
|
76
|
+
const added = new Set();
|
|
77
|
+
// An imported file comes before the text that imports it ("Claude reads the imported file
|
|
78
|
+
// first, then the rest"), and its own imports before it.
|
|
79
|
+
const importsOf = async (file, via) => {
|
|
80
|
+
for (const ref of file.imports.filter((r) => r.via === via)) {
|
|
81
|
+
if (!ref.exists || loaded.has(ref.resolved) || added.has(ref.resolved))
|
|
82
|
+
continue;
|
|
83
|
+
added.add(ref.resolved);
|
|
84
|
+
await importsOf(file, ref.resolved);
|
|
85
|
+
const fields = { kind: "import", path: ref.resolved, scope: file.scope, via };
|
|
86
|
+
if (ref.pending)
|
|
87
|
+
blocks.push(block({ ...fields, note: "Not loaded until you approve imports from outside this project" }, null));
|
|
88
|
+
else if (ref.tooLarge)
|
|
89
|
+
blocks.push(block({ ...fields, note: "Over 4 MiB, so Claude Code does not load it" }, null));
|
|
90
|
+
else if (!showable(env, projectRoot, ref.resolved))
|
|
91
|
+
blocks.push(block({
|
|
92
|
+
...fields,
|
|
93
|
+
note: "Outside this project and your Claude folders, so ccdesk does not show or count it. Claude Code loads it.",
|
|
94
|
+
}, null));
|
|
95
|
+
else {
|
|
96
|
+
const text = await readTextFile(env, ref.resolved);
|
|
97
|
+
blocks.push(block(fields, text ? injectedText(text.text) : null));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
for (const file of instructions) {
|
|
102
|
+
if (file.status !== "loaded")
|
|
103
|
+
continue;
|
|
104
|
+
await importsOf(file, file.path);
|
|
105
|
+
blocks.push(block({ kind: "instruction", path: file.path, scope: file.scope }, injectedText(file.text)));
|
|
106
|
+
}
|
|
107
|
+
const index = memory?.index;
|
|
108
|
+
if (memory?.loaded && index) {
|
|
109
|
+
blocks.push(block({ kind: "memory-index", path: index.path, scope: "memory" }, index.text.split("\n").slice(0, index.loadedLines).join("\n")));
|
|
110
|
+
}
|
|
111
|
+
const listed = [...inventory.skills, ...inventory.commands].filter((s) => s.listed !== false && !s.shadowedBy);
|
|
112
|
+
const listing = skillListing(listed);
|
|
113
|
+
if (listing)
|
|
114
|
+
blocks.push(block({
|
|
115
|
+
kind: "skills",
|
|
116
|
+
path: "Skill listing",
|
|
117
|
+
scope: commonScope(listed),
|
|
118
|
+
note: "Names and descriptions only; a skill's body loads when it is used. Claude Code drops descriptions if the listing passes 1% of the context window.",
|
|
119
|
+
}, listing));
|
|
120
|
+
const agents = inventory.agents
|
|
121
|
+
.filter((a) => !a.shadowedBy && !a.skipped)
|
|
122
|
+
.map((a) => `- ${a.id ?? a.name}: ${a.description}`)
|
|
123
|
+
.join("\n");
|
|
124
|
+
if (agents)
|
|
125
|
+
blocks.push(block({
|
|
126
|
+
kind: "agents",
|
|
127
|
+
path: "Subagent descriptions",
|
|
128
|
+
scope: commonScope(inventory.agents),
|
|
129
|
+
note: "Your subagents' descriptions; built-in subagents are not counted",
|
|
130
|
+
}, agents));
|
|
131
|
+
const servers = inventory.mcpServers.filter((s) => s.status === "active").length;
|
|
132
|
+
if (servers > 0)
|
|
133
|
+
blocks.push(block({
|
|
134
|
+
kind: "not-counted",
|
|
135
|
+
path: `Tool names from ${servers} MCP ${servers === 1 ? "server" : "servers"}`,
|
|
136
|
+
scope: "user",
|
|
137
|
+
note: "ccdesk does not connect to MCP servers, so it cannot count their tools",
|
|
138
|
+
}, null));
|
|
139
|
+
return blocks;
|
|
140
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
/** `configDirFlag`, from `--config-dir`, stands in for CLAUDE_CONFIG_DIR and wins over it. */
|
|
4
|
+
export function scanEnvFromProcess(configDirFlag) {
|
|
5
|
+
const home = homedir();
|
|
6
|
+
const configDir = configDirFlag !== undefined ? resolve(configDirFlag) : process.env.CLAUDE_CONFIG_DIR;
|
|
7
|
+
return {
|
|
8
|
+
home,
|
|
9
|
+
claudeDir: configDir ?? join(home, ".claude"),
|
|
10
|
+
// Claude Code keeps .claude.json beside the config dir it was given.
|
|
11
|
+
claudeJson: configDir ? join(configDir, ".claude.json") : join(home, ".claude.json"),
|
|
12
|
+
managedDir: managedDirFor(process.platform),
|
|
13
|
+
platform: process.platform,
|
|
14
|
+
disableAutoMemory: process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY,
|
|
15
|
+
projectDirName: configDir ? process.env.CLAUDE_CODE_PROJECT_DIR_NAME || undefined : undefined,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Your configuration home: your home directory, or the folder whose .claude is CLAUDE_CONFIG_DIR. */
|
|
19
|
+
export function isConfigHome(env, path) {
|
|
20
|
+
return path === env.home || join(path, ".claude") === env.claudeDir;
|
|
21
|
+
}
|
|
22
|
+
function managedDirFor(platform) {
|
|
23
|
+
if (platform === "darwin")
|
|
24
|
+
return "/Library/Application Support/ClaudeCode";
|
|
25
|
+
if (platform === "win32")
|
|
26
|
+
return "C:\\Program Files\\ClaudeCode";
|
|
27
|
+
return "/etc/claude-code";
|
|
28
|
+
}
|