@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
120 lines (101 loc) • 4.73 kB
text/typescript
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { canonicalHostPath, sameHostPath } from "./bindings.ts";
export interface InstallProbe {
moduleDir: string;
repoRoot: string;
homeDir: string;
platform: NodeJS.Platform;
env: NodeJS.ProcessEnv;
}
export type InstallKind = "npm-global" | "npm-local" | "pi-git" | "claude-marketplace" | "source" | "unknown";
export type ClassifiedInstallKind = InstallKind | "npm-package";
export interface InstallKindReport {
kind: ClassifiedInstallKind;
root: string;
instruction: string;
}
export function kxmSourceCheckoutRoot(dir: string): string | undefined {
if (!existsSync(join(dir, ".git"))) return undefined;
if (!packageIsKxm(dir)) return undefined;
return dir;
}
function packageIsKxm(root: string): boolean {
try {
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { name?: unknown };
return pkg.name === "@kontextmind/kxm";
} catch {
return false;
}
}
function pathSegments(path: string): string[] {
return canonicalHostPath(path).split(/[/\\]+/).filter((part) => part.length > 0);
}
function isUnder(child: string, parent: string, platform: NodeJS.Platform): boolean {
if (sameHostPath(child, parent, platform)) return true;
const childParts = pathSegments(child);
const parentParts = pathSegments(parent);
if (parentParts.length === 0 || childParts.length < parentParts.length) return false;
return parentParts.every((part, i) => segmentEq(part, childParts[i]!, platform));
}
function lastSegments(path: string): string[] {
return pathSegments(path).slice(-3);
}
function segmentEq(left: string, right: string, platform: NodeJS.Platform): boolean {
return platform === "win32"
? left.toLocaleLowerCase("en-US") === right.toLocaleLowerCase("en-US")
: left === right;
}
function isNpmPackageLayout(repoRoot: string, platform: NodeJS.Platform): boolean {
const last = lastSegments(repoRoot);
const expected = ["node_modules", "@kontextmind", "kxm"];
return last.length === 3 && last.every((part, i) => segmentEq(part, expected[i]!, platform));
}
export function installKindInstruction(kind: ClassifiedInstallKind, root: string): string {
switch (kind) {
case "pi-git":
return `kxm is installed by Pi at ${root}; update it with: pi update`;
case "claude-marketplace":
return `kxm is a Claude Code marketplace plugin at ${root}; update it with: claude plugin update kxm@kxm`;
case "source":
return `kxm is running from source at ${root}; update it with git pull there, not kxm update --kxm`;
case "npm-local":
return `kxm is installed as a project dependency at ${root}; update it with: npm install @kontextmind/kxm@latest in that project`;
case "npm-global":
return `kxm is an npm global install at ${root}`;
case "npm-package":
return `kxm is an npm package at ${root}`;
default:
return `kxm cannot tell how it was installed (${root}); update it the way it was installed, or reinstall with: npm install --global @kontextmind/kxm`;
}
}
function report(kind: ClassifiedInstallKind, root: string): InstallKindReport {
return { kind, root, instruction: installKindInstruction(kind, root) };
}
export function classifyInstallRoot(probe: InstallProbe): InstallKindReport {
const piGitRoot = join(probe.homeDir, ".pi", "agent", "git");
if (isUnder(probe.repoRoot, piGitRoot, probe.platform)) return report("pi-git", probe.repoRoot);
const claudeHome = probe.env.CLAUDE_CONFIG_DIR?.trim() || join(probe.homeDir, ".claude");
const pluginsRoot = join(claudeHome, "plugins");
if (isUnder(probe.moduleDir, pluginsRoot, probe.platform)) return report("claude-marketplace", probe.moduleDir);
const hasGit = existsSync(join(probe.repoRoot, ".git"));
if (hasGit && packageIsKxm(probe.repoRoot)) return report("source", probe.repoRoot);
if (isNpmPackageLayout(probe.repoRoot, probe.platform) && packageIsKxm(probe.repoRoot) && !hasGit) {
return report("npm-package", probe.repoRoot);
}
return report("unknown", probe.repoRoot);
}
export function resolveInstallKind(
probe: InstallProbe,
npmGlobalRoot: () => string | undefined,
): InstallKindReport {
const classified = classifyInstallRoot(probe);
if (classified.kind !== "npm-package") return classified;
const globalRoot = npmGlobalRoot();
if (typeof globalRoot !== "string" || globalRoot.trim() === "") {
return report("unknown", classified.root);
}
const expected = dirname(dirname(classified.root));
if (sameHostPath(globalRoot.trim(), expected, probe.platform)) return report("npm-global", classified.root);
return report("npm-local", classified.root);
}