Merge branch 'db-coverage'

This commit is contained in:
igm
2026-01-12 15:51:48 +08:00
10 changed files with 930 additions and 46 deletions

View File

@@ -1,5 +1,4 @@
import type { LocalContext } from "../../context.js";
import { ORPCError } from "@orpc/client";
import { buildCommand } from "@stricli/core";
import { createApiClient } from "../../utils/api-client.js";
import { formatError } from "../../utils/format-error.js";
@@ -21,12 +20,7 @@ async function completeLogin(
console.log(`Completed login request for: ${flags.email}`);
} catch (error) {
if (error instanceof ORPCError) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- ORPCError.code is typed as any
console.error(`Error [${error.code}]:`, error.message);
} else {
console.error("Error:", formatError(error));
}
console.error("Error:", formatError(error));
this.process.exit(1);
}
}

View File

@@ -17,16 +17,16 @@ function formatRelativeTime(date: Date): string {
if (diffDays < 0) {
return `${Math.abs(diffDays).toLocaleString()} days ago`;
}
if (diffDays === 0) {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours <= 0) {
return "expired";
}
return `in ${diffHours.toLocaleString()} hours`;
return diffHours <= 0 ? "expired" : `in ${diffHours.toLocaleString()} hours`;
}
if (diffDays === 1) {
return "tomorrow";
}
return `in ${diffDays.toLocaleString()} days`;
}

View File

@@ -1,9 +1,8 @@
import type { LocalContext } from "../context.js";
import { buildCommand } from "@stricli/core";
type Shell = "bash" | "zsh" | "fish";
const SUPPORTED_SHELLS: readonly Shell[] = ["bash", "zsh", "fish"] as const;
const SUPPORTED_SHELLS = ["bash", "zsh", "fish"] as const;
type Shell = (typeof SUPPORTED_SHELLS)[number];
function parseShell(value: string): Shell {
const shell = value.toLowerCase();

View File

@@ -5,18 +5,20 @@ import { formatError } from "../../utils/format-error.js";
type OrgRole = "owner" | "admin" | "member";
const validRoles: OrgRole[] = ["owner", "admin", "member"];
const VALID_ROLES: readonly OrgRole[] = ["owner", "admin", "member"] as const;
function parseRole(role: string | undefined): OrgRole | undefined {
if (!role) {
return undefined;
}
if (validRoles.includes(role as OrgRole)) {
return role as OrgRole;
if (!VALID_ROLES.includes(role as OrgRole)) {
throw new Error(
`Invalid role: ${role}. Must be one of: ${VALID_ROLES.join(", ")}`,
);
}
throw new Error(
`Invalid role: ${role}. Must be one of: ${validRoles.join(", ")}`,
);
return role as OrgRole;
}
interface CreateUserFlags {

View File

@@ -10,6 +10,14 @@ import { readConfig } from "./config.js";
export type ApiClient = ContractRouterClient<typeof contract>;
function buildClient(apiUrl: string, token: string): ApiClient {
const link = new RPCLink({
url: `${apiUrl}/api/v1/rpc`,
headers: { "X-API-Key": token },
});
return createORPCClient(link) as unknown as ApiClient;
}
/**
* Create an oRPC API client with provided credentials
*/
@@ -25,18 +33,10 @@ export function createApiClient(
apiUrl?: string,
token?: string,
): ApiClient | Promise<ApiClient> {
// If both arguments are provided, create client directly
if (apiUrl !== undefined && token !== undefined) {
const link = new RPCLink({
url: `${apiUrl}/api/v1/rpc`,
headers: {
"X-API-Key": token,
},
});
return createORPCClient(link) as unknown as ApiClient;
return buildClient(apiUrl, token);
}
// Otherwise, read from config asynchronously
return (async (): Promise<ApiClient> => {
const config = await readConfig();
if (!config) {
@@ -44,14 +44,6 @@ export function createApiClient(
"Not logged in. Run 'reviq bootstrap' or 'reviq auth login' first.",
);
}
const link = new RPCLink({
url: `${config.apiUrl}/api/v1/rpc`,
headers: {
"X-API-Key": config.token,
},
});
return createORPCClient(link) as unknown as ApiClient;
return buildClient(config.apiUrl, config.token);
})();
}

View File

@@ -19,40 +19,42 @@ const CONFIG_FILE = join(CONFIG_DIR, "credentials.json");
/**
* Get the path to the config file
*/
export const getConfigPath = (): string => CONFIG_FILE;
export function getConfigPath(): string {
return CONFIG_FILE;
}
/**
* Read the config file
* Returns null if the file doesn't exist or is invalid
*/
export const readConfig = async (): Promise<Config | null> => {
export async function readConfig(): Promise<Config | null> {
try {
const data = await readFile(CONFIG_FILE, "utf-8");
return JSON.parse(data) as Config;
} catch {
return null;
}
};
}
/**
* Write the config file
* Creates the config directory if it doesn't exist
*/
export const writeConfig = async (config: Config): Promise<void> => {
export async function writeConfig(config: Config): Promise<void> {
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), {
mode: 0o600,
});
};
}
/**
* Delete the config file
* Ignores errors if the file doesn't exist
*/
export const deleteConfig = async (): Promise<void> => {
export async function deleteConfig(): Promise<void> {
try {
await unlink(CONFIG_FILE);
} catch {
// Ignore if doesn't exist
}
};
}

View File

@@ -1,8 +1,14 @@
import { ORPCError } from "@orpc/client";
/**
* Format an unknown error value into a string message.
* Handles Error instances, strings, and other types safely.
* Handles ORPCError, Error instances, strings, and other types safely.
*/
export function formatError(error: unknown): string {
if (error instanceof ORPCError) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- ORPCError.code is typed as any
return `[${error.code}] ${error.message}`;
}
if (error instanceof Error) {
return error.message;
}