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
56 changes: 56 additions & 0 deletions cli/__tests__/lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ await jest.unstable_mockModule('os', () => {
const {
resolveInstanceUrl,
saveInstance,
setActive,
getToken,
listInstances,
DEFAULT_URL,
Expand Down Expand Up @@ -154,6 +155,61 @@ describe('config.js', () => {
expect(getToken('dev')).toBe('cm_dev');
});

test('getToken("https://...") prefers the ACTIVE instance when several keys share the URL', () => {
// Real config shape on 2026-08-29: `dev` (saved in July, stale token) and
// `default` (fresh login) both at api.commonly.me. `.find` returned `dev`
// and `commonly agent detach` / `agent wake` got HTTP 401 from a token
// the user had already replaced.
saveInstance({
key: 'dev', url: 'https://api.commonly.me', token: 'cm_stale',
userId: 'u1', username: 'sam',
});
saveInstance({
key: 'default', url: 'https://api.commonly.me', token: 'cm_fresh',
userId: 'u1', username: 'sam',
});
// saveInstance sets active = last saved key ('default')
expect(getToken('https://api.commonly.me')).toBe('cm_fresh');
});

test('prefers an explicitly-active match over a NEWER one', () => {
// Discriminates the `active` branch from the savedAt sort: the active
// key is the OLDER save, so only the active preference can pick it.
// (The test above is satisfied by savedAt alone — sprint-review mutated
// `active` to null and it stayed green.)
saveInstance({
key: 'old', url: 'https://api.commonly.me', token: 'cm_old',
userId: 'u1', username: 'sam',
});
saveInstance({
key: 'new', url: 'https://api.commonly.me', token: 'cm_new',
userId: 'u1', username: 'sam',
});
setActive('old');
expect(getToken('https://api.commonly.me')).toBe('cm_old');
});

test('getToken("https://...") falls back to the most recently saved match when none is active', () => {
saveInstance({
key: 'old', url: 'https://api.commonly.me', token: 'cm_old',
userId: 'u1', username: 'sam',
});
saveInstance({
key: 'new', url: 'https://api.commonly.me', token: 'cm_new',
userId: 'u1', username: 'sam',
});
// Point `active` somewhere that does NOT share the URL.
saveInstance({
key: 'local', url: 'http://localhost:5000', token: 'cm_local',
userId: 'u1', username: 'sam',
});
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
cfg.instances.old.savedAt = '2026-07-10T05:10:00Z';
cfg.instances.new.savedAt = '2026-08-26T22:40:12Z';
fs.writeFileSync(configFile, JSON.stringify(cfg));
expect(getToken('https://api.commonly.me')).toBe('cm_new');
});

test('resolveInstanceUrl matches saved URL case-insensitively (and preserves unknown URL case)', () => {
saveInstance({
key: 'dev', url: 'https://api-dev.commonly.me', token: 'cm_dev',
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@commonlyai/cli",
"version": "0.1.23",
"version": "0.1.24",
"license": "Apache-2.0",
"description": "The Commonly CLI \u2014 connect agents, manage pods, iterate fast",
"type": "module",
Expand Down
17 changes: 13 additions & 4 deletions cli/src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,19 @@ export const resolveInstance = (identifier = null) => {
const looksLikeUrl = /^https?:\/\//i.test(identifier);
if (looksLikeUrl) {
const normalized = normalizeUrl(identifier);
const urlEntry = Object.entries(config.instances)
.find(([, v]) => normalizeUrl(v.url) === normalized);
if (urlEntry) {
const [key, value] = urlEntry;
const matches = Object.entries(config.instances)
.filter(([, v]) => normalizeUrl(v.url) === normalized);
if (matches.length > 0) {
// Several keys can share one URL (`dev` and `default` both pointing at
// api.commonly.me after a re-login under a new key). The first entry in
// file order is then whichever was saved FIRST — the stale one — and
// every command that resolves by the token file's instanceUrl
// (detach, wake) got a 401 from a token the user had already replaced.
// Prefer the active instance, then the most recently saved.
const active = matches.find(([key]) => key === config.active);
const [key, value] = active || matches
.slice()
.sort(([, a], [, b]) => String(b.savedAt || '').localeCompare(String(a.savedAt || '')))[0];
return { key, ...value };
}
// Unknown URL — still usable for bootstrapping. Preserve the caller's
Expand Down
Loading