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
20 changes: 20 additions & 0 deletions cli/docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Use an API token to authenticate CLI commands.

```bash
operately auth login --token <your-token>
operately auth profiles
operately auth status
operately auth whoami
```
Expand Down Expand Up @@ -48,6 +49,8 @@ Good pattern: one profile per environment.
- `staging` profile -> staging URL
- `local` profile -> localhost URL

When an interactive auth flow asks for a profile name, pressing Enter uses the current active profile.

Examples:

```bash
Expand Down Expand Up @@ -93,6 +96,23 @@ OPERATELY_BASE_URL=https://preview.operately.com operately get_me --profile stag
operately get_me --profile staging --base-url http://localhost:4000
```

### Profiles

List the profiles saved in local CLI config:

```bash
operately auth profiles
```

This command:

- marks the active profile with `*`
- shows whether each profile is currently logged in
- shows saved user/company metadata when available
- shows the effective saved base URL for each profile

Use `--profile <name>` with any command to select one of the saved profiles explicitly.

### Status

Check current authentication setup:
Expand Down
12 changes: 5 additions & 7 deletions cli/src/auth/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DEFAULT_BASE_URL,
type CliConfig,
} from "./config";
import { printError, printSuccess } from "../core/output";
import { printError, printInfo, printSuccess } from "../core/output";
import { callInternalMutation, callInternalQuery } from "../core/internal-api";
import { askQuestion, askChoice, askPassword } from "../core/prompts";
import { callEndpoint } from "../core/http";
Expand All @@ -17,6 +17,7 @@ import { selectCompany } from "./shared/company-selection";
import { createTokenAndSaveProfile } from "./shared/token-creation";
import { openExternalUrl } from "./shared/api";
import { handleBootstrapError } from "./shared/errors";
import { resolveProfileName } from "./shared/helpers";
import type { EndpointRegistry } from "../commands/registry";
import type { Company, AuthMethod } from "./types";
import type { ChildProcess } from "child_process";
Expand All @@ -30,6 +31,7 @@ export interface BootstrapDeps {
callEndpoint: typeof callEndpoint;
openUrl: (url: string) => Promise<ChildProcess | boolean | undefined>;
printError: typeof printError;
printInfo: typeof printInfo;
printSuccess: typeof printSuccess;
saveProfile: typeof saveProfile;
writeConfig: typeof writeConfig;
Expand All @@ -45,6 +47,7 @@ const defaultDeps: BootstrapDeps = {
callEndpoint,
openUrl: (url: string) => openExternalUrl(url),
printError,
printInfo,
printSuccess,
saveProfile,
writeConfig,
Expand Down Expand Up @@ -78,12 +81,7 @@ export async function executeAuthBootstrap(
baseUrl = answer.trim() || null;
}

if (!profile) {
const answer = await d.askQuestion(
`Profile name (default: default):`,
);
profile = answer.trim() || "default";
}
profile = await resolveProfileName(config, profile, d.askQuestion);

const runtime = d.resolveRuntimeOptions(config, {
token: null,
Expand Down
5 changes: 5 additions & 0 deletions cli/src/auth/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { executeAuthBootstrap } from "./bootstrap";
import { runSignupFlow } from "./flows/signup";
import { runJoinInviteFlow } from "./flows/join-invite";
import { runCreateCompanyFlow } from "./flows/create-company";
import { executeAuthProfiles } from "./profiles";
import { fetchProfileMetadata } from "./shared/profile-metadata";
import type { AuthAction } from "../core/parser-types";
import type { EndpointRegistry } from "../commands/registry";
Expand Down Expand Up @@ -45,6 +46,10 @@ export async function executeAuthCommand(input: AuthExecutionInput): Promise<num
return executeAuthStatus(input.flags, config);
}

if (input.action === "profiles") {
return executeAuthProfiles(config);
}

if (input.action === "logout") {
return executeAuthLogout(input.flags, config);
}
Expand Down
2 changes: 1 addition & 1 deletion cli/src/auth/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import os from "node:os";
import path from "node:path";

export const DEFAULT_BASE_URL = "https://app.operately.com";
export const DEFAULT_PROFILE = "default";

export interface ProfileConfig {
token?: string;
Expand Down Expand Up @@ -31,7 +32,6 @@ export interface RuntimeOverrideOptions {
}

const CONFIG_FILE = "config.json";
const DEFAULT_PROFILE = "default";
const DEFAULT_TIMEOUT_MS = 30_000;

export function configPath(): string {
Expand Down
4 changes: 3 additions & 1 deletion cli/src/auth/flows/create-company.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEFAULT_BASE_URL, resolveRuntimeOptions, type CliConfig } from "../config";
import { printError, printSuccess } from "../../core/output";
import { printError, printInfo, printSuccess } from "../../core/output";
import { callInternalMutation, callInternalQuery } from "../../core/internal-api";
import { askChoice, askPassword, askQuestion, PromptCancelledError } from "../../core/prompts";
import { callEndpoint, ApiError } from "../../core/http";
Expand All @@ -22,6 +22,7 @@ interface CreateCompanyFlowDeps {
callEndpoint: typeof callEndpoint;
openUrl: (url: string) => Promise<ChildProcess | boolean | undefined>;
printError: typeof printError;
printInfo: typeof printInfo;
printSuccess: typeof printSuccess;
resolveRuntimeOptions: typeof resolveRuntimeOptions;
}
Expand All @@ -35,6 +36,7 @@ const defaultDeps: CreateCompanyFlowDeps = {
callEndpoint,
openUrl: (url: string) => openExternalUrl(url),
printError,
printInfo,
printSuccess,
resolveRuntimeOptions,
};
Expand Down
6 changes: 2 additions & 4 deletions cli/src/auth/flows/join-invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { runPasswordFlow } from "./login-password";
import { runGoogleFlow } from "./login-google";
import { openExternalUrl } from "../shared/api";
import { handleBootstrapError } from "../shared/errors";
import { resolveProfileName } from "../shared/helpers";
import type { EndpointRegistry } from "../../commands/registry";
import type { Company } from "../types";
import type { ChildProcess } from "child_process";
Expand Down Expand Up @@ -78,10 +79,7 @@ export async function runJoinInviteFlow(
baseUrl = answer.trim() || null;
}

if (!profile) {
const answer = await d.askQuestion(`Profile name (default: default):`);
profile = answer.trim() || "default";
}
profile = await resolveProfileName(config, profile, d.askQuestion);

const runtime = d.resolveRuntimeOptions(config, {
token: null,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/auth/flows/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ async function joinCompanyAndSaveProfile(
bootstrapToken,
)) as { company?: Company };

const profile = await resolveProfileName(profileFlag, d.askQuestion);
const profile = await resolveProfileName(config, profileFlag, d.askQuestion);

return await createTokenAndSaveProfile({
baseUrl: explicitBaseUrl,
Expand Down
40 changes: 40 additions & 0 deletions cli/src/auth/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DEFAULT_BASE_URL, DEFAULT_PROFILE, type CliConfig } from "./config";
import { getOrderedProfileNames } from "./shared/helpers";

export function executeAuthProfiles(config: CliConfig): number {
const activeProfile = config.activeProfile?.trim() || DEFAULT_PROFILE;
const profileNames = getOrderedProfileNames(config);

if (profileNames.length === 0) {
console.log("No saved CLI profiles.");
console.log("Use `operately auth login`, `operately auth signup`, `operately auth join`, or `operately auth create-company` to create one.");
return 0;
}

console.log("Saved CLI profiles:");

for (const profileName of profileNames) {
const profile = config.profiles[profileName] ?? {};
const isActive = profileName === activeProfile;
const marker = isActive ? "*" : "-";
const label = isActive ? `${profileName} (active)` : profileName;

console.log(`${marker} ${label}`);
console.log(` Status: ${profile.token ? "Logged in" : "Not logged in"}`);

if (profile.name) {
console.log(` Name: ${profile.name}`);
}

if (profile.companyName) {
console.log(` Company: ${profile.companyName}`);
}

console.log(` Base URL: ${profile.baseUrl || DEFAULT_BASE_URL}`);
}

console.log("");
console.log("Use `--profile <name>` with any command to select a saved profile.");

return 0;
}
2 changes: 1 addition & 1 deletion cli/src/auth/shared/company-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function createCompanyAndSaveProfile(input: CreateCompanyAndSavePro
)) as { company?: Company };
const company = requireCompany(result.company, "Company creation failed: no company returned.");

const profile = await resolveProfileName(input.profileFlag, input.deps.askQuestion);
const profile = await resolveProfileName(input.config, input.profileFlag, input.deps.askQuestion);

return createTokenAndSaveProfile({
baseUrl: input.explicitBaseUrl,
Expand Down
17 changes: 15 additions & 2 deletions cli/src/auth/shared/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { askQuestion } from "../../core/prompts";
import { DEFAULT_PROFILE, type CliConfig } from "../config";
import type { Company } from "../types";

export function requireCompany(company: Company | undefined, failureMessage: string): Company {
Expand All @@ -10,6 +11,7 @@ export function requireCompany(company: Company | undefined, failureMessage: str
}

export async function resolveProfileName(
config: CliConfig,
profileFlag: string | null,
askQuestionFn: typeof askQuestion,
): Promise<string> {
Expand All @@ -19,6 +21,17 @@ export async function resolveProfileName(
return profile;
}

const answer = await askQuestionFn("Profile name (default: default):");
return answer.trim() || "default";
const defaultProfile = config.activeProfile?.trim() || DEFAULT_PROFILE;
const answer = await askQuestionFn(`Profile name (default: ${defaultProfile}):`);
return answer.trim() || defaultProfile;
}

export function getOrderedProfileNames(config: CliConfig): string[] {
const activeProfile = config.activeProfile?.trim() || DEFAULT_PROFILE;

return Object.keys(config.profiles).sort((left, right) => {
if (left === activeProfile) return -1;
if (right === activeProfile) return 1;
return left.localeCompare(right);
});
}
15 changes: 15 additions & 0 deletions cli/src/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Authentication & Setup:
auth signup [--base-url <url>] [--profile <name>]
auth join [--invite-token <token>] [--base-url <url>] [--profile <name>]
auth create-company [--base-url <url>] [--profile <name>]
auth profiles
auth status [--profile <name>]
auth whoami [--profile <name>]
auth logout [--profile <name>]
Expand Down Expand Up @@ -171,6 +172,20 @@ const AUTH_COMMAND_HELP: Record<AuthAction, AuthCommandHelp> = {
"operately auth create-company --base-url https://staging.operately.com --profile staging",
],
},
profiles: {
description: [
"List saved CLI profiles",
"",
" Shows the saved profiles stored in local CLI config, marks the",
" active profile, and displays saved login metadata and base URLs.",
"",
" Use `--profile <name>` with any other command to select one of the",
" saved profiles explicitly.",
],
usage: "operately auth profiles",
flags: ["(none)"],
examples: ["operately auth profiles"],
},
status: {
description: "Show authentication status for current profile",
usage: "operately auth status [--profile <name>]",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/core/parser-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CatalogEndpoint } from "../types/catalog";

export class UsageError extends Error {}

export const AUTH_ACTIONS = ["login", "signup", "join", "create-company", "status", "whoami", "logout"] as const;
export const AUTH_ACTIONS = ["login", "signup", "join", "create-company", "profiles", "status", "whoami", "logout"] as const;

export type AuthAction = (typeof AUTH_ACTIONS)[number];

Expand Down
Loading