chore(release): claudemesh-cli@1.6.1
Patch release on top of 1.6.0: - Revoke-by-id-prefix bug fix (broker.revokeApiKey now returns structured status; CLI surfaces not_found / not_unique). Pasting the 8-char prefix from `apikey list` output now works as users expect, instead of silently no-op'ing with a misleading "✔ revoked" message. Already deployed to broker. - whoami falls back to local mesh-config view when no web session is signed in. Users who joined via invite (and never ran `claudemesh login`) now see their member ids and pubkey prefixes per mesh, instead of a "Not signed in" dead end. - README updated: REST surface lives at claudemesh.com/api/v1/* (web app), NOT ic.claudemesh.com/api/v1/* (broker). Surfaced during CLI-only smoke test against prod when curl on the broker host returned 404. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { whoAmI } from "~/services/auth/facade.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
import { bold, dim } from "~/ui/styles.js";
|
||||
import { bold, clay, dim } from "~/ui/styles.js";
|
||||
import { EXIT } from "~/constants/exit-codes.js";
|
||||
|
||||
export async function whoami(opts: { json?: boolean }): Promise<number> {
|
||||
@@ -8,22 +8,40 @@ export async function whoami(opts: { json?: boolean }): Promise<number> {
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify({ schema_version: "1.0", ...result }, null, 2));
|
||||
return EXIT.SUCCESS;
|
||||
return result.signed_in || result.local ? EXIT.SUCCESS : EXIT.AUTH_FAILED;
|
||||
}
|
||||
|
||||
if (!result.signed_in) {
|
||||
render.err("Not signed in", "Run `claudemesh login` to sign in.");
|
||||
// Show whatever we have. Both the web session and the local mesh
|
||||
// config are independent surfaces of identity; suppress sections that
|
||||
// are empty.
|
||||
if (!result.signed_in && !result.local) {
|
||||
render.err("Not signed in", "Run `claudemesh login` to sign in or `claudemesh <invite>` to join.");
|
||||
return EXIT.AUTH_FAILED;
|
||||
}
|
||||
|
||||
render.section("whoami");
|
||||
render.kv([
|
||||
["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`],
|
||||
["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`],
|
||||
...(result.meshes
|
||||
? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`] as [string, string]]
|
||||
: []),
|
||||
]);
|
||||
if (result.signed_in) {
|
||||
render.kv([
|
||||
["user", `${bold(result.user!.display_name)} ${dim(`(${result.user!.email})`)}`],
|
||||
["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`],
|
||||
...(result.meshes
|
||||
? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`] as [string, string]]
|
||||
: []),
|
||||
]);
|
||||
} else {
|
||||
render.kv([
|
||||
["web", dim("not signed in · run `claudemesh login` for account features")],
|
||||
]);
|
||||
}
|
||||
if (result.local) {
|
||||
render.blank();
|
||||
render.kv([
|
||||
["local", `${result.local.meshes.length} mesh${result.local.meshes.length === 1 ? "" : "es"} · ${dim(result.local.config_path)}`],
|
||||
]);
|
||||
for (const m of result.local.meshes) {
|
||||
console.log(` ${clay("●")} ${bold(m.slug)} ${dim(`member ${m.member_id.slice(0, 8)}… pk ${m.pubkey_prefix}…`)}`);
|
||||
}
|
||||
}
|
||||
render.blank();
|
||||
|
||||
return EXIT.SUCCESS;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { my } from "~/services/api/facade.js";
|
||||
import { ApiError } from "~/services/api/facade.js";
|
||||
import { readConfig } from "~/services/config/facade.js";
|
||||
import { PATHS } from "~/constants/paths.js";
|
||||
import { getStoredToken, clearToken } from "./token-store.js";
|
||||
import { NotSignedIn } from "./errors.js";
|
||||
import type { WhoAmIResult } from "./schemas.js";
|
||||
@@ -10,9 +12,27 @@ function requireToken(): string {
|
||||
return auth.session_token;
|
||||
}
|
||||
|
||||
/** Snapshot the local mesh-config view for whoami. Always populated when
|
||||
* config.json has any mesh entries — independent of web session state. */
|
||||
function localView(): WhoAmIResult["local"] {
|
||||
const cfg = readConfig();
|
||||
if (cfg.meshes.length === 0) return undefined;
|
||||
return {
|
||||
config_path: PATHS.CONFIG_FILE,
|
||||
meshes: cfg.meshes.map((m) => ({
|
||||
slug: m.slug,
|
||||
mesh_id: m.meshId,
|
||||
member_id: m.memberId,
|
||||
pubkey_prefix: m.pubkey.slice(0, 12),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function whoAmI(): Promise<WhoAmIResult> {
|
||||
const auth = getStoredToken();
|
||||
if (!auth) return { signed_in: false };
|
||||
const local = localView();
|
||||
|
||||
if (!auth) return { signed_in: false, local };
|
||||
|
||||
try {
|
||||
const profile = await my.getProfile(auth.session_token);
|
||||
@@ -23,11 +43,12 @@ export async function whoAmI(): Promise<WhoAmIResult> {
|
||||
user: profile,
|
||||
token_source: auth.token_source,
|
||||
meshes: { owned, guest: meshes.length - owned },
|
||||
local,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.isUnauthorized) {
|
||||
clearToken();
|
||||
return { signed_in: false };
|
||||
return { signed_in: false, local };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -18,4 +18,15 @@ export interface WhoAmIResult {
|
||||
};
|
||||
token_source?: string;
|
||||
meshes?: { owned: number; guest: number };
|
||||
/**
|
||||
* Local mesh memberships from ~/.claudemesh/config.json. Always present
|
||||
* when the config has any mesh entries, regardless of whether a web
|
||||
* session is also signed in. Lets `claudemesh whoami` show useful
|
||||
* identity info for users who joined via invite without ever signing
|
||||
* in to claudemesh.com.
|
||||
*/
|
||||
local?: {
|
||||
config_path: string;
|
||||
meshes: Array<{ slug: string; mesh_id: string; member_id: string; pubkey_prefix: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user