Adding profile support to the v3 CLI (#953)

This commit is contained in:
Eric Allam
2024-03-18 17:35:29 +00:00
committed by GitHub
parent 9304c447be
commit e55ca0279b
10 changed files with 178 additions and 159 deletions
+5 -3
View File
@@ -10,12 +10,14 @@ export const CommonCommandOptions = z.object({
apiUrl: z.string().optional(),
logLevel: z.enum(["debug", "info", "log", "warn", "error", "none"]).default("log"),
skipTelemetry: z.boolean().default(false),
profile: z.string().default("default"),
});
export type CommonCommandOptions = z.infer<typeof CommonCommandOptions>;
export function commonOptions(command: Command) {
return command
.option("--profile <profile>", "The login profile to use", "default")
.option("-a, --api-url <value>", "Override the API URL", "https://api.trigger.dev")
.option(
"-l, --log-level <level>",
@@ -25,9 +27,9 @@ export function commonOptions(command: Command) {
.option("--skip-telemetry", "Opt-out of sending telemetry");
}
export class SkipLoggingError extends Error {}
export class SkipCommandError extends Error {}
export class OutroCommandError extends SkipCommandError {}
export class SkipLoggingError extends Error { }
export class SkipCommandError extends Error { }
export class OutroCommandError extends SkipCommandError { }
export async function handleTelemetry(action: () => Promise<void>) {
try {
+2 -32
View File
@@ -3,12 +3,10 @@ import { configureDeployCommand } from "../commands/deploy.js";
import { configureDevCommand } from "../commands/dev.js";
import { configureInitCommand } from "../commands/init.js";
import { configureLoginCommand } from "../commands/login.js";
import { logoutCommand } from "../commands/logout.js";
import { updateCommand } from "../commands/update.js";
import { configureLogoutCommand } from "../commands/logout.js";
import { configureWhoamiCommand } from "../commands/whoami.js";
import { COMMAND_NAME } from "../consts.js";
import { getVersion } from "../utilities/getVersion.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
export const program = new Command();
@@ -19,35 +17,7 @@ program
configureLoginCommand(program);
configureInitCommand(program);
program
.command("logout")
.description("Logout of Trigger.dev")
.version(getVersion(), "-v, --version", "Display the version number")
.action(async (options) => {
try {
await printInitialBanner(false);
await logoutCommand(options);
//todo login command
} catch (e) {
//todo error reporting
throw e;
}
});
configureDevCommand(program);
configureDeployCommand(program);
program
.command("update")
.description(
"Updates all @trigger.dev/* packages to their latest compatible versions or the specified version"
)
.argument("[path]", "The path to the directory that contains the package.json file", ".")
.option("-t, --to <version tag>", "The version to update to (ex: 2.1.4)", "latest")
.action(async (path, options) => {
await printInitialBanner(false);
await updateCommand(path, options);
});
configureWhoamiCommand(program);
configureLogoutCommand(program);
+1 -1
View File
@@ -134,7 +134,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
intro("Deploying project");
const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl });
const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl, profile: options.profile });
if (!authorization.ok) {
if (authorization.error === "fetch failed") {
+24 -39
View File
@@ -24,7 +24,7 @@ import { ClientOptions, WebSocket as wsWebSocket } from "ws";
import { z } from "zod";
import * as packageJson from "../../package.json";
import { CliApiClient } from "../apiClient";
import { CommonCommandOptions } from "../cli/common.js";
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
import { readConfig } from "../utilities/configFiles";
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
import { detectPackageNameFromImportPath } from "../utilities/installPackages";
@@ -47,46 +47,31 @@ const DevCommandOptions = CommonCommandOptions.extend({
type DevCommandOptions = z.infer<typeof DevCommandOptions>;
export function configureDevCommand(program: Command) {
program
.command("dev")
.description("Run your Trigger.dev tasks locally")
.argument("[path]", "The path to the project", ".")
.option(
"-l, --log-level <level>",
"The log level to use (debug, info, log, warn, error, none)",
"log"
)
.option(
"-c, --config <config file>",
"The name of the config file, found at [path]",
"trigger.config.mjs"
)
.option(
"-p, --project-ref <project ref>",
"The project ref. Required if there is no config file."
)
.option("--debugger", "Enable the debugger")
.option("--debug-otel", "Enable OpenTelemetry debugging")
.action(async (path, options) => {
try {
await devCommand(path, options);
} catch (e) {
//todo error reporting
throw e;
}
return commonOptions(
program
.command("dev")
.description("Run your Trigger.dev tasks locally")
.argument("[path]", "The path to the project", ".")
.option(
"-c, --config <config file>",
"The name of the config file, found at [path]",
"trigger.config.mjs"
)
.option(
"-p, --project-ref <project ref>",
"The project ref. Required if there is no config file."
)
.option("--debugger", "Enable the debugger")
.option("--debug-otel", "Enable OpenTelemetry debugging")
).action(async (path, options) => {
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
await devCommand(path, opts);
});
});
}
export async function devCommand(dir: string, anyOptions: unknown) {
const options = DevCommandOptions.safeParse(anyOptions);
if (!options.success) {
console.log(fromZodError(options.error).toString());
process.exit(1);
}
const authorization = await isLoggedIn();
export async function devCommand(dir: string, options: DevCommandOptions) {
const authorization = await isLoggedIn(options.profile);
if (!authorization.ok) {
if (authorization.error === "fetch failed") {
@@ -101,7 +86,7 @@ export async function devCommand(dir: string, anyOptions: unknown) {
let watcher;
try {
const devInstance = await startDev(dir, options.data, authorization.auth);
const devInstance = await startDev(dir, options, authorization.auth);
watcher = devInstance.watcher;
const { waitUntilExit } = devInstance.devReactElement;
await waitUntilExit();
+2 -1
View File
@@ -80,7 +80,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
intro("Initializing project");
const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl });
const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl, profile: options.profile });
if (!authorization.ok) {
if (authorization.error === "fetch failed") {
@@ -96,6 +96,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
"cli.userId": authorization.userId,
"cli.email": authorization.email,
"cli.config.apiUrl": authorization.auth.apiUrl,
"cli.config.profile": authorization.profile,
});
if (!options.overrideConfig) {
+12 -6
View File
@@ -14,11 +14,12 @@ import {
wrapCommandAction,
} from "../cli/common.js";
import { chalkLink } from "../utilities/colors.js";
import { readAuthConfigFile, writeAuthConfigFile } from "../utilities/configFiles.js";
import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js";
import { getVersion } from "../utilities/getVersion.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { LoginResult } from "../utilities/session.js";
import { whoAmI } from "./whoami.js";
import { logger } from "../utilities/logger.js";
export const LoginCommandOptions = CommonCommandOptions.extend({
apiUrl: z.string(),
@@ -48,12 +49,13 @@ export async function loginCommand(options: unknown) {
}
async function _loginCommand(options: LoginCommandOptions) {
return login({ defaultApiUrl: options.apiUrl, embedded: false });
return login({ defaultApiUrl: options.apiUrl, embedded: false, profile: options.profile });
}
export type LoginOptions = {
defaultApiUrl?: string;
embedded?: boolean;
profile?: string;
};
export async function login(options?: LoginOptions): Promise<LoginResult> {
@@ -63,16 +65,17 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
span.setAttributes({
"cli.config.apiUrl": opts.defaultApiUrl,
"cli.options.profile": opts.profile,
});
if (!opts.embedded) {
intro("Logging in to Trigger.dev");
}
const authConfig = readAuthConfigFile();
const authConfig = readAuthConfigProfile(options?.profile);
if (authConfig && authConfig.accessToken) {
const whoAmIResult = await whoAmI(undefined, opts.embedded);
const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel }, opts.embedded);
if (!whoAmIResult.success) {
throw new Error(whoAmIResult.error);
@@ -106,6 +109,7 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
return {
ok: true as const,
profile: options?.profile ?? "default",
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
@@ -126,6 +130,7 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
return {
ok: true as const,
profile: options?.profile ?? "default",
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
@@ -170,9 +175,9 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`);
writeAuthConfigFile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl });
writeAuthConfigProfile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl }, options?.profile);
const whoAmIResult = await whoAmI(undefined, opts.embedded);
const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel }, opts.embedded);
if (!whoAmIResult.success) {
throw new Error(whoAmIResult.error);
@@ -188,6 +193,7 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
return {
ok: true as const,
profile: options?.profile ?? "default",
userId: whoAmIResult.data.userId,
email: whoAmIResult.data.email,
dashboardUrl: whoAmIResult.data.dashboardUrl,
+32 -6
View File
@@ -1,15 +1,41 @@
import { readAuthConfigFile, writeAuthConfigFile } from "../utilities/configFiles.js";
import { Command } from "commander";
import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js";
import { logger } from "../utilities/logger.js";
import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { z } from "zod";
export async function logoutCommand(options: any) {
const config = readAuthConfigFile();
const LogoutCommandOptions = CommonCommandOptions;
type LogoutCommandOptions = z.infer<typeof LogoutCommandOptions>;
export function configureLogoutCommand(program: Command) {
return commonOptions(program
.command("logout")
.description("Logout of Trigger.dev"))
.action(async (options) => {
await handleTelemetry(async () => {
await printInitialBanner(false);
await logoutCommand(options);
});
});
}
export async function logoutCommand(options: unknown) {
return await wrapCommandAction("logoutCommand", LogoutCommandOptions, options, async (opts) => {
return await logout(opts);
});
}
export async function logout(options: LogoutCommandOptions) {
const config = readAuthConfigProfile(options.profile);
if (!config?.accessToken) {
logger.info("You are already logged out");
logger.info(`You are already logged out [${options.profile ?? "default"}]`);
return;
}
writeAuthConfigFile({ ...config, accessToken: undefined, apiUrl: undefined });
writeAuthConfigProfile({ ...config, accessToken: undefined, apiUrl: undefined }, options.profile);
logger.info("Logged out");
logger.info(`Logged out of Trigger.dev [${options.profile ?? "default"}]`);
}
+28 -31
View File
@@ -1,68 +1,65 @@
import { note, spinner } from "@clack/prompts";
import { intro, note, spinner } from "@clack/prompts";
import { chalkLink } from "../utilities/colors.js";
import { logger } from "../utilities/logger.js";
import { isLoggedIn } from "../utilities/session.js";
import { Command } from "commander";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { CommonCommandOptions } from "../cli/common.js";
import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js";
import { z } from "zod";
import { CliApiClient } from "../apiClient.js";
type WhoAmIResult =
| {
success: true;
data: {
userId: string;
email: string;
dashboardUrl: string;
};
}
| {
success: false;
error: string;
success: true;
data: {
userId: string;
email: string;
dashboardUrl: string;
};
}
| {
success: false;
error: string;
};
const WhoamiCommandOptions = CommonCommandOptions;
type WhoamiCommandOptions = z.infer<typeof WhoamiCommandOptions>;
export function configureWhoamiCommand(program: Command) {
program
return commonOptions(program
.command("whoami")
.description("display the current logged in user and project details")
.option(
"-l, --log-level <level>",
"The log level to use (debug, info, log, warn, error, none)",
"log"
)
.description("display the current logged in user and project details"))
.action(async (options) => {
try {
await printInitialBanner();
await whoAmI(WhoamiCommandOptions.parse(options));
} catch (e) {
throw e;
}
await handleTelemetry(async () => {
await printInitialBanner(false);
await whoAmICommand(options);
});
});
}
export async function whoAmICommand(options: unknown) {
return await wrapCommandAction("whoamiCommand", WhoamiCommandOptions, options, async (opts) => {
return await whoAmI(opts);
});
}
export async function whoAmI(
options?: WhoamiCommandOptions,
embedded: boolean = false
): Promise<WhoAmIResult> {
if (options?.logLevel) {
logger.loggerLevel = options?.logLevel;
}
intro(`Displaying your account details [${options?.profile ?? "default"}]`);
const loadingSpinner = spinner();
loadingSpinner.start("Checking your account details");
const authentication = await isLoggedIn();
const authentication = await isLoggedIn(options?.profile);
if (!authentication.ok) {
if (authentication.error === "fetch failed") {
loadingSpinner.stop("Fetch failed. Platform down?");
} else {
loadingSpinner.stop("You must login first. Use `trigger.dev login` to login.");
loadingSpinner.stop(`You must login first. Use \`trigger.dev login --profile ${options?.profile ?? "default"}\` to login.`);
}
return {
@@ -90,7 +87,7 @@ export async function whoAmI(
Email: ${userData.data.email}
URL: ${chalkLink(authentication.auth.apiUrl)}
`,
"Account details"
`Account details [${authentication.profile}]`
);
} else {
loadingSpinner.stop(`Retrieved your account details for ${userData.data.email}`);
+50 -21
View File
@@ -24,11 +24,53 @@ export const UserAuthConfigSchema = z.object({
export type UserAuthConfig = z.infer<typeof UserAuthConfigSchema>;
const UserAuthConfigFileSchema = z.record(UserAuthConfigSchema);
type UserAuthConfigFile = z.infer<typeof UserAuthConfigFileSchema>;
function getAuthConfigFilePath() {
return path.join(getGlobalConfigFolderPath(), "default.json");
}
export function writeAuthConfigFile(config: UserAuthConfig) {
export function writeAuthConfigProfile(config: UserAuthConfig, profile: string = "default") {
const existingConfig = readAuthConfigFile() || {};
existingConfig[profile] = config;
writeAuthConfigFile(existingConfig);
}
export function readAuthConfigProfile(profile: string = "default"): UserAuthConfig | undefined {
try {
const authConfigFilePath = getAuthConfigFilePath();
logger.debug(`Reading auth config file`, { authConfigFilePath });
const json = readJSONFileSync(authConfigFilePath);
const parsed = UserAuthConfigFileSchema.parse(json);
return parsed[profile];
} catch (error) {
logger.debug(`Error reading auth config file: ${error}`);
return undefined;
}
}
function readAuthConfigFile(): UserAuthConfigFile | undefined {
try {
const authConfigFilePath = getAuthConfigFilePath();
logger.debug(`Reading auth config file`, { authConfigFilePath });
const json = readJSONFileSync(authConfigFilePath);
const parsed = UserAuthConfigFileSchema.parse(json);
return parsed;
} catch (error) {
logger.debug(`Error reading auth config file: ${error}`);
return undefined;
}
}
function writeAuthConfigFile(config: UserAuthConfigFile) {
const authConfigFilePath = getAuthConfigFilePath();
mkdirSync(path.dirname(authConfigFilePath), {
recursive: true,
@@ -38,19 +80,6 @@ export function writeAuthConfigFile(config: UserAuthConfig) {
});
}
export function readAuthConfigFile(): UserAuthConfig | undefined {
try {
const authConfigFilePath = getAuthConfigFilePath();
const json = readJSONFileSync(authConfigFilePath);
const parsed = UserAuthConfigSchema.parse(json);
return parsed;
} catch (error) {
logger.debug(`Error reading auth config file: ${error}`);
return undefined;
}
}
async function getConfigPath(dir: string, fileName?: string): Promise<string | undefined> {
return await findUp(fileName ? [fileName] : CONFIG_FILES, { cwd: dir });
}
@@ -62,14 +91,14 @@ export type ReadConfigOptions = {
export type ReadConfigResult =
| {
status: "file";
config: ResolvedConfig;
path: string;
}
status: "file";
config: ResolvedConfig;
path: string;
}
| {
status: "in-memory";
config: ResolvedConfig;
};
status: "in-memory";
config: ResolvedConfig;
};
export async function readConfig(
dir: string,
+22 -19
View File
@@ -1,34 +1,35 @@
import { recordSpanException } from "@trigger.dev/core/v3";
import { CliApiClient } from "../apiClient.js";
import { readAuthConfigFile } from "./configFiles.js";
import { readAuthConfigProfile } from "./configFiles.js";
import { getTracer } from "../telemetry/tracing.js";
const tracer = getTracer();
export type LoginResult =
| {
ok: true;
userId: string;
email: string;
dashboardUrl: string;
auth: {
apiUrl: string;
accessToken: string;
};
}
| {
ok: false;
error: string;
auth?: {
apiUrl: string;
accessToken: string;
};
ok: true;
profile: string,
userId: string;
email: string;
dashboardUrl: string;
auth: {
apiUrl: string;
accessToken: string;
};
}
| {
ok: false;
error: string;
auth?: {
apiUrl: string;
accessToken: string;
};
};
export async function isLoggedIn(): Promise<LoginResult> {
export async function isLoggedIn(profile: string = "default"): Promise<LoginResult> {
return await tracer.startActiveSpan("isLoggedIn", async (span) => {
try {
const config = readAuthConfigFile();
const config = readAuthConfigProfile(profile);
if (!config?.accessToken || !config?.apiUrl) {
span.recordException(new Error("You must login first"));
@@ -57,12 +58,14 @@ export async function isLoggedIn(): Promise<LoginResult> {
"login.userId": userData.data.userId,
"login.email": userData.data.email,
"login.dashboardUrl": userData.data.dashboardUrl,
"login.profile": profile,
});
span.end();
return {
ok: true as const,
profile,
userId: userData.data.userId,
email: userData.data.email,
dashboardUrl: userData.data.dashboardUrl,