Skip to content

Commit 45d8227

Browse files
authored
Merge pull request #157 from Phala-Network/chore/credentials-profiles
feat(cli): multi-profile credentials & switch command
2 parents 4570771 + 21a545b commit 45d8227

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2389
-558
lines changed

cli/src/commands/api/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
44
import { createClient } from "@phala/cloud";
55
import { defineCommand } from "@/src/core/define-command";
66
import type { CommandContext } from "@/src/core/types";
7-
import { getApiKey } from "@/src/utils/credentials";
7+
import { resolveAuthForContext } from "@/src/lib/client";
88
import { applyJqFilter, formatJqOutput } from "./jq-filter";
99
import { apiCommandMeta, apiCommandSchema } from "./command";
1010
import type { ApiCommandInput } from "./command";
@@ -234,9 +234,9 @@ export async function runApiCommand(
234234
input: ApiCommandInput,
235235
context: CommandContext,
236236
): Promise<number | undefined> {
237-
const apiKey = input.apiToken || getApiKey();
237+
const auth = resolveAuthForContext(context, { apiToken: input.apiToken });
238238

239-
if (!apiKey) {
239+
if (!auth.apiKey) {
240240
context.stderr.write(
241241
'Error: Not authenticated. Run "phala login" first.\n',
242242
);
@@ -245,7 +245,8 @@ export async function runApiCommand(
245245

246246
// Create client with User-Agent header
247247
const client = createClient({
248-
apiKey,
248+
apiKey: auth.apiKey,
249+
baseURL: auth.baseURL,
249250
headers: {
250251
"User-Agent": `phala-cli/${CLI_VERSION}`,
251252
},

cli/src/commands/auth/login/index.ts

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,40 @@ import { safeGetCurrentUser } from "@phala/cloud";
33
import { defineCommand } from "@/src/core/define-command";
44
import type { CommandContext } from "@/src/core/types";
55
import { getClientWithKey } from "@/src/lib/client";
6-
import { removeApiKey, saveApiKey } from "@/src/utils/credentials";
6+
import { DEFAULT_API_PREFIX, upsertProfile } from "@/src/utils/credentials";
77
import { CLOUD_URL } from "@/src/utils/constants";
88

99
import { logger } from "@/src/utils/logger";
10-
import type { UserInfoResponse } from "@/src/api/types";
1110
import { loginCommandMeta, loginCommandSchema } from "./command";
1211
import type { LoginCommandInput } from "./command";
1312

14-
async function validateAndPersistApiKey(
15-
apiKey: string,
16-
): Promise<UserInfoResponse> {
17-
try {
18-
await saveApiKey(apiKey);
19-
const client = await getClientWithKey(apiKey);
20-
const result = await safeGetCurrentUser(client);
21-
const userData = result.data as UserInfoResponse;
22-
23-
if (!result.success || !userData?.username) {
24-
await removeApiKey();
25-
throw new Error("Invalid API key");
26-
}
13+
type CurrentUserInfo = {
14+
username: string;
15+
email?: string;
16+
team_name?: string;
17+
};
2718

28-
return userData;
29-
} catch (error) {
30-
await removeApiKey();
31-
throw error instanceof Error ? error : new Error(String(error));
19+
async function validateApiKey(options: {
20+
apiKey: string;
21+
baseURL: string;
22+
}): Promise<CurrentUserInfo> {
23+
const client = await getClientWithKey(options.apiKey, {
24+
baseURL: options.baseURL,
25+
});
26+
const result = await safeGetCurrentUser(client);
27+
const userData = result.data as CurrentUserInfo;
28+
29+
if (!result.success || !userData?.username) {
30+
throw new Error("Invalid API key");
3231
}
32+
33+
return userData;
3334
}
3435

35-
async function promptForApiKey(): Promise<{
36-
apiKey: string;
37-
user: UserInfoResponse;
38-
}> {
39-
let cachedUser: UserInfoResponse | undefined;
36+
async function promptForApiKey(options: {
37+
baseURL: string;
38+
}): Promise<{ apiKey: string; user: CurrentUserInfo }> {
39+
let cachedUser: CurrentUserInfo | undefined;
4040
const response = await prompts({
4141
type: "password",
4242
name: "apiKey",
@@ -46,7 +46,10 @@ async function promptForApiKey(): Promise<{
4646
return "API key cannot be empty";
4747
}
4848
try {
49-
cachedUser = await validateAndPersistApiKey(value);
49+
cachedUser = await validateApiKey({
50+
apiKey: value,
51+
baseURL: options.baseURL,
52+
});
5053
return true;
5154
} catch (error) {
5255
return error instanceof Error ? error.message : "Invalid API key";
@@ -59,15 +62,18 @@ async function promptForApiKey(): Promise<{
5962
}
6063

6164
if (!cachedUser) {
62-
cachedUser = await validateAndPersistApiKey(response.apiKey);
65+
cachedUser = await validateApiKey({
66+
apiKey: response.apiKey,
67+
baseURL: options.baseURL,
68+
});
6369
}
6470

6571
return { apiKey: response.apiKey, user: cachedUser };
6672
}
6773

6874
async function runLoginCommand(
6975
input: LoginCommandInput,
70-
_context: CommandContext,
76+
context: CommandContext,
7177
): Promise<number> {
7278
// Show deprecation warning
7379
logger.warn(
@@ -77,23 +83,40 @@ async function runLoginCommand(
7783
logger.break();
7884

7985
try {
86+
const baseURL = context.env.PHALA_CLOUD_API_PREFIX || DEFAULT_API_PREFIX;
87+
8088
let apiKey = input.apiKey;
81-
let user: UserInfoResponse | undefined;
89+
let user: CurrentUserInfo | undefined;
8290

8391
if (!apiKey) {
84-
const result = await promptForApiKey();
92+
const result = await promptForApiKey({ baseURL });
8593
apiKey = result.apiKey;
8694
user = result.user;
8795
} else {
88-
user = await validateAndPersistApiKey(apiKey);
96+
user = await validateApiKey({ apiKey, baseURL });
8997
}
9098

9199
if (!user) {
92100
throw new Error("Failed to validate API key");
93101
}
94102

103+
const workspaceName = user.team_name || "default";
104+
const profileName = workspaceName;
105+
106+
upsertProfile({
107+
profileName,
108+
token: apiKey,
109+
apiPrefix: baseURL,
110+
workspaceName,
111+
user: {
112+
username: user.username,
113+
email: user.email,
114+
},
115+
setCurrent: true,
116+
});
117+
95118
logger.success(
96-
`Welcome ${user.username}! API key validated and saved successfully`,
119+
`Welcome ${user.username}! Credentials saved successfully (profile: ${profileName})`,
97120
);
98121
logger.break();
99122
logger.info(`Open in Web UI at ${CLOUD_URL}/dashboard/`);

cli/src/commands/auth/logout/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineCommand } from "@/src/core/define-command";
22
import type { CommandContext } from "@/src/core/types";
3-
import { removeApiKey } from "@/src/utils/credentials";
3+
import { loadCredentialsFile, removeProfile } from "@/src/utils/credentials";
44

55
import { logger } from "@/src/utils/logger";
66
import {
@@ -21,11 +21,17 @@ async function runLogoutCommand(
2121
logger.break();
2222

2323
try {
24-
await removeApiKey();
25-
logger.success("API key removed successfully");
24+
const current = loadCredentialsFile();
25+
const profile = current?.current_profile;
26+
removeProfile();
27+
logger.success(
28+
profile
29+
? `Credentials removed successfully (profile: ${profile})`
30+
: "Credentials removed successfully",
31+
);
2632
return 0;
2733
} catch (error) {
28-
logger.error("Failed to remove API key");
34+
logger.error("Failed to remove credentials");
2935
logger.logDetailedError(error);
3036
return 1;
3137
}

cli/src/commands/config/command.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export const configGroup: CommandGroup = {
44
path: ["config"],
55
meta: {
66
name: "config",
7-
description: "Manage your local configuration",
8-
stability: "stable",
7+
description:
8+
"[DEPRECATED] Manage local CLI state (will be removed in a future version)",
9+
stability: "deprecated",
910
},
1011
};

cli/src/commands/config/get/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand } from "@/src/core/define-command";
2-
import { getConfigValue } from "@/src/utils/config";
2+
import { getStateValue } from "@/src/utils/state";
33
import { logger } from "@/src/utils/logger";
44

55
import type { CommandContext } from "@/src/core/types";
@@ -13,8 +13,11 @@ async function runConfigGet(
1313
input: ConfigGetCommandInput,
1414
context: CommandContext,
1515
): Promise<number> {
16+
logger.warn(
17+
'The "phala config" commands are deprecated and will be removed in a future version.',
18+
);
1619
try {
17-
const value = getConfigValue(input.key);
20+
const value = getStateValue(input.key);
1821

1922
if (value === undefined) {
2023
context.stderr.write(`Configuration key '${input.key}' not found\n`);

cli/src/commands/config/list/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand } from "@/src/core/define-command";
2-
import { listConfigValues } from "@/src/utils/config";
2+
import { listStateValues } from "@/src/utils/state";
33

44
import { logger, setJsonMode } from "@/src/utils/logger";
55
import type { CommandContext } from "@/src/core/types";
@@ -16,8 +16,11 @@ async function runConfigList(
1616
// Enable JSON mode if --json flag is set
1717
setJsonMode(input.json);
1818

19+
logger.warn(
20+
'The "phala config" commands are deprecated and will be removed in a future version.',
21+
);
1922
try {
20-
const config = listConfigValues();
23+
const config = listStateValues();
2124

2225
if (input.json) {
2326
context.success(config);

cli/src/commands/config/set/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand } from "@/src/core/define-command";
2-
import { setConfigValue } from "@/src/utils/config";
2+
import { setStateValue } from "@/src/utils/state";
33
import { logger } from "@/src/utils/logger";
44

55
import type { CommandContext } from "@/src/core/types";
@@ -30,7 +30,10 @@ async function runConfigSet(
3030
}
3131
}
3232

33-
setConfigValue(input.key, parsedValue as string | number | boolean);
33+
logger.warn(
34+
'The "phala config" commands are deprecated and will be removed in a future version.',
35+
);
36+
setStateValue(input.key, parsedValue as string | number | boolean);
3437
context.stdout.write(
3538
`Configuration value for '${input.key}' set successfully\n`,
3639
);

cli/src/commands/deploy/handler.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from "node:path";
22
import os from "node:os";
33
import type { CommandContext } from "@/src/core/types";
4-
import { getApiKey } from "@/src/utils/credentials";
4+
import { resolveAuthForContext } from "@/src/lib/client";
55
import { logger, setJsonMode } from "@/src/utils/logger";
66
import {
77
CLOUD_URL,
@@ -194,26 +194,15 @@ async function getApiClient({
194194
}: Readonly<Pick<Options, "apiToken" | "interactive">>): Promise<
195195
Client<typeof API_VERSION>
196196
> {
197-
// Priority 1: Command-line provided API key
198-
if (apiToken) {
199-
return createClient({ apiKey: apiToken, version: API_VERSION });
200-
}
201-
202-
// Priority 2: Environment variable
203-
if (process.env.PHALA_CLOUD_API_KEY) {
197+
const resolved = resolveAuthForContext(undefined, { apiToken });
198+
if (resolved.apiKey) {
204199
return createClient({
205-
apiKey: process.env.PHALA_CLOUD_API_KEY,
200+
apiKey: resolved.apiKey,
201+
baseURL: resolved.baseURL,
206202
version: API_VERSION,
207203
});
208204
}
209205

210-
// Priority 3: Saved API key from config file
211-
const savedApiKey = getApiKey();
212-
if (savedApiKey) {
213-
return createClient({ apiKey: savedApiKey, version: API_VERSION });
214-
}
215-
216-
// Priority 4: Interactive prompt (only if no API key found)
217206
if (interactive) {
218207
const { apiToken: promptedToken } = await inquirer.prompt([
219208
{
@@ -224,12 +213,15 @@ async function getApiClient({
224213
input.trim() ? true : "API token is required",
225214
},
226215
]);
227-
return createClient({ apiKey: promptedToken, version: API_VERSION });
216+
return createClient({
217+
apiKey: promptedToken,
218+
baseURL: resolved.baseURL,
219+
version: API_VERSION,
220+
});
228221
}
229222

230-
// No API key available
231223
throw new Error(
232-
"API token is required. Please run 'phala auth login' or set PHALA_CLOUD_API_KEY environment variable",
224+
"API token is required. Please run 'phala login' or set PHALA_CLOUD_API_KEY environment variable",
233225
);
234226
}
235227

0 commit comments

Comments
 (0)