Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui),
HTTPS API origin (or `http://localhost` for a local one) and run the helper
without the two `npx` commands. It opens an isolated Chrome or Edge window for
your usual sign-in and project choice; install either browser first. For CLI-managed
local evaluation installs, it discovers the matching dashboard with the installed or
local evaluation installs, it discovers the matching dashboard with the newest installed or
cached CopilotKit CLI and uses that dashboard for login and session requests. Discovery
runs offline with Bun, without downloading the CLI or requiring `npx` on your PATH.
If no CLI is available, manually hosted deployments use their API origin for sign-in. Complete the local auth
Expand Down
125 changes: 119 additions & 6 deletions scripts/local-intelligence-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ test("minimal-PATH child uses an existing npm cache offline without exposing cre
});
});

test("installed CLI wins over cache; otherwise newest cached version wins deterministically", async () => {
test("newest available CLI wins even when an older version is installed", async () => {
await fixture(async (root, install) => {
await install(join(root, ".npm/_npx/z/node_modules/copilotkit"), "4.9.0");
await install(
Expand All @@ -84,13 +84,10 @@ test("installed CLI wins over cache; otherwise newest cached version wins determ
expect(
await localCliEntrypoint(root, { PATH: minimalPath }, root, []),
).toBe(latest);
const installed = await install(
join(root, "node_modules/copilotkit"),
"4.8.0",
);
await install(join(root, "node_modules/copilotkit"), "4.8.0");
expect(
await localCliEntrypoint(root, { PATH: minimalPath }, root, []),
).toBe(installed);
).toBe(latest);
});
});

Expand Down Expand Up @@ -154,3 +151,119 @@ test("cancellation terminates discovery and timeout is a runtime failure", async
).toBe(1);
});
});

test.each(["4.11.0", "4.10.0"])(
"installed CLI wins against cached 4.10.0 when its version is %s",
async (version) => {
await fixture(async (root, install) => {
await install(
join(root, ".npm/_npx/cache/node_modules/copilotkit"),
"4.10.0",
);
const installed = await install(
join(root, "node_modules/copilotkit"),
version,
);
expect(
await localCliEntrypoint(root, { PATH: minimalPath }, root, []),
).toBe(installed);
});
},
);

test("equal cached versions use a deterministic path tie-breaker", async () => {
await fixture(async (root, install) => {
await install(join(root, ".npm/_npx/z/node_modules/copilotkit"), "4.10.0");
const first = await install(
join(root, ".npm/_npx/a/node_modules/copilotkit"),
"4.10.0",
);
expect(
await localCliEntrypoint(root, { PATH: minimalPath }, root, []),
).toBe(first);
});
});

test.each([true, false])(
"Windows default npm cache uses local app data (configured: %s)",
async (configured) => {
await fixture(async (root, install) => {
const local = configured
? join(root, "custom-local")
: join(root, "AppData/Local");
const expected = await install(
join(local, "npm-cache/_npx/cache/node_modules/copilotkit"),
);
await install(
join(root, ".npm/_npx/wrong/node_modules/copilotkit"),
"99.0.0",
);
expect(
await localCliEntrypoint(
root,
{ PATH: minimalPath, ...(configured ? { LOCALAPPDATA: local } : {}) },
root,
[],
"win32",
),
).toBe(expected);
});
},
);

test.each(["npm_config_cache", "NPM_CONFIG_CACHE"])(
"Windows respects explicit %s cache override",
async (key) => {
await fixture(async (root, install) => {
const explicit = join(root, "override");
const expected = await install(
join(explicit, "_npx/cache/node_modules/copilotkit"),
);
await install(
join(
root,
"AppData/Local/npm-cache/_npx/default/node_modules/copilotkit",
),
"99.0.0",
);
expect(
await localCliEntrypoint(
root,
{
PATH: minimalPath,
NPM_CONFIG_CACHE: join(root, "unused"),
[key]: explicit,
},
root,
[],
"win32",
),
).toBe(expected);
});
},
);

test("a stale global installation cannot shadow a newer cached CLI", async () => {
await fixture(async (root, install) => {
const globalRoot = join(root, "global/copilotkit");
await install(globalRoot, "4.8.0");
const cached = await install(
join(root, ".npm/_npx/cache/node_modules/copilotkit"),
"4.10.0",
);
expect(
await localCliEntrypoint(root, { PATH: minimalPath }, root, [globalRoot]),
).toBe(cached);
});
});

test("newest installed CLI wins across local and global installations", async () => {
await fixture(async (root, install) => {
await install(join(root, "node_modules/copilotkit"), "4.8.0");
const globalRoot = join(root, "global/copilotkit");
const global = await install(globalRoot, "4.11.0");
expect(
await localCliEntrypoint(root, { PATH: minimalPath }, root, [globalRoot]),
).toBe(global);
});
});
23 changes: 16 additions & 7 deletions scripts/local-intelligence-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function localCliEntrypoint(
"/opt/homebrew/lib/node_modules/copilotkit",
"/usr/local/lib/node_modules/copilotkit",
],
platform: NodeJS.Platform = process.platform,
): Promise<string | undefined> {
const entry = async (root: string) => {
try {
Expand Down Expand Up @@ -72,29 +73,37 @@ export async function localCliEntrypoint(
.filter(Boolean)
.map((bin) => join(dirname(bin), "lib/node_modules/copilotkit")),
];
const candidates: { path: string; version: string; installed: boolean }[] =
[];
for (const root of installed) {
const found = await entry(root);
if (found) return found.path;
if (found) candidates.push({ ...found, installed: true });
}
// npm's npx cache is a complete installation with its dependency graph. Bun's raw
// package cache is not, so only Bun's installed global package is considered above.
const cacheRoot = join(
environment.npm_config_cache ??
environment.NPM_CONFIG_CACHE ??
join(home, ".npm"),
(platform === "win32"
? join(
environment.LOCALAPPDATA ?? join(home, "AppData/Local"),
"npm-cache",
)
: join(home, ".npm")),
"_npx",
);
const cached: { path: string; version: string }[] = [];
const names = await readdir(cacheRoot).catch(() => [] as string[]);
for (const name of names.sort()) {
const found = await entry(join(cacheRoot, name, "node_modules/copilotkit"));
if (found) cached.push(found);
if (found) candidates.push({ ...found, installed: false });
}
cached.sort(
candidates.sort(
(a, b) =>
Bun.semver.order(b.version, a.version) || a.path.localeCompare(b.path),
Bun.semver.order(b.version, a.version) ||
Number(b.installed) - Number(a.installed) ||
a.path.localeCompare(b.path),
);
return cached[0]?.path;
return candidates[0]?.path;
}

/** Execute the installed CLI with Desktop's already-resolved Bun, even with Finder's PATH. */
Expand Down
Loading