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
10 changes: 5 additions & 5 deletions src/commands/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type DeviceResponse = {

type TokenResponse = {
access_token: string;
organization_name?: string;
workspace_name?: string;
};

type TokenErrorResponse = {
Expand All @@ -47,7 +47,7 @@ export default function Login({options}: Props) {
const [status, setStatus] = useState<'init' | 'polling' | 'success' | 'error'>('init');
const [errorMsg, setErrorMsg] = useState('');
const [userCode, setUserCode] = useState('');
const [orgName, setOrgName] = useState<string | undefined>();
const [workspaceName, setWorkspaceName] = useState<string | undefined>();

useEffect(() => {
void startDeviceFlow();
Expand Down Expand Up @@ -111,12 +111,12 @@ export default function Login({options}: Props) {
config.sessionToken = token.access_token;
writeConfig(config);

setOrgName(token.organization_name);
setWorkspaceName(token.workspace_name);

if (options.json) {
console.log(JSON.stringify({
authenticated: true,
...(token.organization_name ? {organization: token.organization_name} : {}),
...(token.workspace_name ? {workspace: token.workspace_name} : {}),
}));
process.exit(0);
Comment on lines 116 to 121
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the shared JSON-mode path here instead of options.json + console.log.

This success branch still keys off --json only and bypasses jsonOutput(...), so redirected stdout can miss the machine-readable path and still emit Ink output.

Suggested fix
-				if (options.json) {
-					console.log(JSON.stringify({
-						authenticated: true,
-						...(token.workspace_name ? {workspace: token.workspace_name} : {}),
-					}));
-					process.exit(0);
-				}
+				if (isJsonMode(options)) {
+					jsonOutput({
+						authenticated: true,
+						...(token.workspace_name ? {workspace: token.workspace_name} : {}),
+					});
+					return;
+				}

Add the corresponding helper imports at the top of the file.

As per coding guidelines, "Use isJsonMode(options) to detect JSON output mode (when stdout is not a TTY or --json flag is set)" and "use jsonOutput(data) for success".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/login.tsx` around lines 116 - 121, Replace the direct
options.json check and console.log in the successful auth branch with the shared
JSON path: call isJsonMode(options) to detect JSON mode and use
jsonOutput({...}) to emit the machine-readable object (including
token.workspace_name as workspace when present) instead of console.log; ensure
you import isJsonMode and jsonOutput at the top of the file and retain the
process.exit(0) behavior if needed.

}
Expand Down Expand Up @@ -180,7 +180,7 @@ export default function Login({options}: Props) {
return (
<Box flexDirection="column">
<Text color="green">✓ Authenticated successfully</Text>
{orgName && <Text color="green">✓ Organization: {orgName}</Text>}
{workspaceName && <Text color="green">✓ Workspace: {workspaceName}</Text>}
</Box>
);
}
Expand Down
16 changes: 8 additions & 8 deletions src/commands/whoami.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Props = {

type Result = {
authenticated: boolean;
organization?: string;
workspace?: string;
error?: string;
};

Expand All @@ -39,10 +39,10 @@ export default function WhoAmI({options}: Props) {

const client = createApiClient({token});

let orgName: string | undefined;
let workspaceName: string | undefined;
try {
const whoami = await client.get<{organizationName?: string}>('/v1/whoami');
orgName = whoami.organizationName;
const whoami = await client.get<{workspaceName?: string}>('/v1/whoami');
workspaceName = whoami.workspaceName;
} catch (error) {
if (options.json) {
handleError(error, true);
Expand All @@ -57,7 +57,7 @@ export default function WhoAmI({options}: Props) {

const info: Result = {
authenticated: true,
...(orgName ? {organization: orgName} : {}),
...(workspaceName ? {workspace: workspaceName} : {}),
};

if (options.json) {
Expand Down Expand Up @@ -86,10 +86,10 @@ export default function WhoAmI({options}: Props) {
<Text bold>{'Status: '}</Text>
<Text color="green">Authenticated</Text>
</Text>
{result.organization && (
{result.workspace && (
<Text>
<Text bold>{'Organization: '}</Text>
<Text>{result.organization}</Text>
<Text bold>{'Workspace: '}</Text>
<Text>{result.workspace}</Text>
</Text>
)}
</Box>
Expand Down
18 changes: 9 additions & 9 deletions src/components/ReplView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type {ReplEntry} from './ReplOutput.js';

const COMMANDS_HINT = 'logs stats flows flows show whoami login logout config list help exit';

function ReplHeader({token, orgName}: {token: string | null; orgName: string | null}) {
function ReplHeader({token, workspaceName}: {token: string | null; workspaceName: string | null}) {
const cols = process.stdout.columns || 80;
return (
<Box flexDirection="column">
Expand All @@ -34,7 +34,7 @@ function ReplHeader({token, orgName}: {token: string | null; orgName: string | n
<Text dimColor>v{CLI_VERSION}</Text>
</Box>
{token ? (
<Text><Text color="green">● </Text><Text>{orgName ?? 'Authenticated'}</Text></Text>
<Text><Text color="green">● </Text><Text>{workspaceName ?? 'Authenticated'}</Text></Text>
) : (
<Text><Text color="red">● </Text><Text dimColor>Not logged in</Text></Text>
)}
Expand Down Expand Up @@ -73,13 +73,13 @@ export default function ReplView() {
const [phase, setPhase] = useState<Phase>({tag: 'idle'});
const [history, setHistory] = useState<string[]>([]);
const [token, setToken] = useState<string | null>(null);
const [orgName, setOrgName] = useState<string | null>(null);
const [workspaceName, setWorkspaceName] = useState<string | null>(null);

const fetchOrgName = useCallback(async (t: string) => {
const fetchWorkspaceName = useCallback(async (t: string) => {
try {
const client = createApiClient({token: t});
const whoami = await client.get<{organizationName?: string}>('/v1/whoami');
if (whoami.organizationName) setOrgName(whoami.organizationName);
const whoami = await client.get<{workspaceName?: string}>('/v1/whoami');
if (whoami.workspaceName) setWorkspaceName(whoami.workspaceName);
Comment on lines +78 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear stale workspaceName when the token changes or the fetch fails.

This branch only writes truthy values, so a previous workspace can survive a failed /v1/whoami call or a response without workspaceName. The header can then show the wrong workspace for the current session.

Suggested fix
 const fetchWorkspaceName = useCallback(async (t: string) => {
 	try {
 		const client = createApiClient({token: t});
 		const whoami = await client.get<{workspaceName?: string}>('/v1/whoami');
-		if (whoami.workspaceName) setWorkspaceName(whoami.workspaceName);
+		setWorkspaceName(whoami.workspaceName ?? null);
 	} catch {
+		setWorkspaceName(null);
 		// silently fail — header shows "Authenticated" fallback
 	}
 }, []);
 
 useEffect(() => {
-	if (!token) { setWorkspaceName(null); return; }
+	if (!token) { setWorkspaceName(null); return; }
+	setWorkspaceName(null);
 	void fetchWorkspaceName(token);
-}, [token]);
+}, [token, fetchWorkspaceName]);

Also applies to: 94-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ReplView.tsx` around lines 78 - 82, The fetchWorkspaceName
function only sets workspace name on truthy responses, so when the token changes
or the /v1/whoami call fails you must clear any stale value; update
fetchWorkspaceName (and the other place that sets workspaceName) to
setWorkspaceName(undefined) before/when starting the fetch and also in the
catch/failure branch (or when whoami.workspaceName is absent) so the header
cannot display a previous session's workspace; reference the fetchWorkspaceName
function and setWorkspaceName state updater when making these changes.

} catch {
// silently fail — header shows "Authenticated" fallback
}
Expand All @@ -91,8 +91,8 @@ export default function ReplView() {
}, []);

useEffect(() => {
if (!token) { setOrgName(null); return; }
void fetchOrgName(token);
if (!token) { setWorkspaceName(null); return; }
void fetchWorkspaceName(token);
}, [token]);

function addEntry(entry: ReplEntry) {
Expand Down Expand Up @@ -276,7 +276,7 @@ export default function ReplView() {

return (
<Box flexDirection="column">
<ReplHeader token={token} orgName={orgName} />
<ReplHeader token={token} workspaceName={workspaceName} />
{visibleEntries.map((entry, i) => (
<ReplOutput key={i} entry={entry} />
))}
Expand Down
6 changes: 3 additions & 3 deletions src/components/commandViews/LoginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type DeviceResponse = {

type TokenResponse = {
access_token: string;
organization_name?: string;
workspace_name?: string;
};

type TokenErrorResponse = {
Expand Down Expand Up @@ -99,8 +99,8 @@ export default function LoginView({onDone, onError}: Props) {
onDone(
<Box flexDirection="column">
<Text color="green">✓ Authenticated successfully</Text>
{token.organization_name ? (
<Text color="green">✓ Organization: {token.organization_name}</Text>
{token.workspace_name ? (
<Text color="green">✓ Workspace: {token.workspace_name}</Text>
) : null}
</Box>,
false,
Expand Down
6 changes: 3 additions & 3 deletions src/components/commandViews/WhoamiView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export default function WhoamiView({token, onDone, onError}: Props) {
async function run() {
try {
const client = createApiClient({token});
const whoami = await client.get<{organizationName?: string}>('/v1/whoami');
const whoami = await client.get<{workspaceName?: string}>('/v1/whoami');
onDone(
<Box flexDirection="column">
<Text><Text bold>{'Status: '}</Text><Text color="green">Authenticated</Text></Text>
{whoami.organizationName ? (
<Text><Text bold>{'Organization: '}</Text><Text>{whoami.organizationName}</Text></Text>
{whoami.workspaceName ? (
<Text><Text bold>{'Workspace: '}</Text><Text>{whoami.workspaceName}</Text></Text>
) : null}
</Box>,
false,
Expand Down
Loading