From 6b3dc5ba4d33d038d610cc6d8a67c56f7daa8a8b Mon Sep 17 00:00:00 2001 From: jackchuka Date: Tue, 27 Jan 2026 13:32:41 +0900 Subject: [PATCH 1/7] feat(sdk): add workspace commands --- packages/sdk/src/cli/lib.ts | 12 +- packages/sdk/src/cli/utils/format.ts | 18 +++ packages/sdk/src/cli/workspace/app/health.ts | 87 +++++++++++++ packages/sdk/src/cli/workspace/app/index.ts | 17 +++ packages/sdk/src/cli/workspace/app/list.ts | 120 ++++++++++++++++++ .../sdk/src/cli/workspace/app/transform.ts | 73 +++++++++++ packages/sdk/src/cli/workspace/describe.ts | 82 ++++++++++++ packages/sdk/src/cli/workspace/index.ts | 8 ++ packages/sdk/src/cli/workspace/restore.ts | 86 +++++++++++++ packages/sdk/src/cli/workspace/transform.ts | 27 ++-- packages/sdk/src/cli/workspace/user/index.ts | 21 +++ packages/sdk/src/cli/workspace/user/invite.ts | 84 ++++++++++++ packages/sdk/src/cli/workspace/user/list.ts | 112 ++++++++++++++++ packages/sdk/src/cli/workspace/user/remove.ts | 92 ++++++++++++++ .../sdk/src/cli/workspace/user/transform.ts | 44 +++++++ packages/sdk/src/cli/workspace/user/update.ts | 84 ++++++++++++ 16 files changed, 954 insertions(+), 13 deletions(-) create mode 100644 packages/sdk/src/cli/workspace/app/health.ts create mode 100644 packages/sdk/src/cli/workspace/app/index.ts create mode 100644 packages/sdk/src/cli/workspace/app/list.ts create mode 100644 packages/sdk/src/cli/workspace/app/transform.ts create mode 100644 packages/sdk/src/cli/workspace/describe.ts create mode 100644 packages/sdk/src/cli/workspace/restore.ts create mode 100644 packages/sdk/src/cli/workspace/user/index.ts create mode 100644 packages/sdk/src/cli/workspace/user/invite.ts create mode 100644 packages/sdk/src/cli/workspace/user/list.ts create mode 100644 packages/sdk/src/cli/workspace/user/remove.ts create mode 100644 packages/sdk/src/cli/workspace/user/transform.ts create mode 100644 packages/sdk/src/cli/workspace/user/update.ts diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index bcc25b200..dc3aa272a 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -34,7 +34,17 @@ export { remove, type RemoveOptions } from "./remove"; export { createWorkspace, type CreateWorkspaceOptions } from "./workspace/create"; export { listWorkspaces, type ListWorkspacesOptions } from "./workspace/list"; export { deleteWorkspace, type DeleteWorkspaceOptions } from "./workspace/delete"; -export type { WorkspaceInfo } from "./workspace/transform"; +export { describeWorkspace, type DescribeWorkspaceOptions } from "./workspace/describe"; +export { restoreWorkspace, type RestoreWorkspaceOptions } from "./workspace/restore"; +export type { WorkspaceInfo, WorkspaceDetails } from "./workspace/transform"; +export { listUsers, type ListUsersOptions } from "./workspace/user/list"; +export { inviteUser, type InviteUserOptions } from "./workspace/user/invite"; +export { updateUser, type UpdateUserOptions } from "./workspace/user/update"; +export { removeUser, type RemoveUserOptions } from "./workspace/user/remove"; +export type { UserInfo } from "./workspace/user/transform"; +export { listApps, type ListAppsOptions } from "./workspace/app/list"; +export { getAppHealth, type HealthOptions as GetAppHealthOptions } from "./workspace/app/health"; +export type { AppInfo, AppHealthInfo } from "./workspace/app/transform"; export { listMachineUsers, type ListMachineUsersOptions, diff --git a/packages/sdk/src/cli/utils/format.ts b/packages/sdk/src/cli/utils/format.ts index 686a781b9..e2b6397c9 100644 --- a/packages/sdk/src/cli/utils/format.ts +++ b/packages/sdk/src/cli/utils/format.ts @@ -1,8 +1,26 @@ +import { timestampDate } from "@bufbuild/protobuf/wkt"; import { formatDistanceToNowStrict } from "date-fns"; // eslint-disable-next-line no-restricted-imports import { getBorderCharacters, table } from "table"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; import type { TableUserConfig } from "table"; +/** + * Format a protobuf Timestamp to ISO string. + * @param timestamp - Protobuf timestamp + * @returns ISO date string or "N/A" if invalid + */ +export function formatTimestamp(timestamp: Timestamp | undefined): string { + if (!timestamp) { + return "N/A"; + } + const date = timestampDate(timestamp); + if (Number.isNaN(date.getTime())) { + return "N/A"; + } + return date.toISOString(); +} + /** * Formats a table with consistent single-line border style. * Use this instead of importing `table` directly. diff --git a/packages/sdk/src/cli/workspace/app/health.ts b/packages/sdk/src/cli/workspace/app/health.ts new file mode 100644 index 000000000..658d7fe09 --- /dev/null +++ b/packages/sdk/src/cli/workspace/app/health.ts @@ -0,0 +1,87 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { initOperatorClient } from "../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../context"; +import { humanizeRelativeTime } from "../../utils/format"; +import { logger } from "../../utils/logger"; +import { appHealthInfo, type AppHealthInfo } from "./transform"; + +const healthOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + name: z.string().min(1, { message: "name is required" }), +}); + +export type HealthOptions = z.input; + +async function loadOptions(options: HealthOptions) { + const result = healthOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + name: result.data.name, + }; +} + +/** + * Get application schema health status. + * @param options - Health check options + * @returns Application health information + */ +export async function getAppHealth(options: HealthOptions): Promise { + const { client, workspaceId, name } = await loadOptions(options); + + const response = await client.getApplicationSchemaHealth({ + workspaceId, + applicationName: name, + }); + + return appHealthInfo(name, response); +} + +export const healthCommand = defineCommand({ + meta: { + name: "health", + description: "Check application schema health", + }, + args: { + ...commonArgs, + ...jsonArgs, + ...workspaceArgs, + name: { + type: "string", + description: "Application name", + required: true, + alias: "n", + }, + }, + run: withCommonArgs(async (args) => { + const health = await getAppHealth({ + workspaceId: args["workspace-id"], + profile: args.profile, + name: args.name, + }); + + const formattedHealth = args.json + ? health + : { + ...health, + currentServingSchemaUpdatedAt: humanizeRelativeTime(health.currentServingSchemaUpdatedAt), + lastAttemptAt: humanizeRelativeTime(health.lastAttemptAt), + }; + + logger.out(formattedHealth); + }), +}); diff --git a/packages/sdk/src/cli/workspace/app/index.ts b/packages/sdk/src/cli/workspace/app/index.ts new file mode 100644 index 000000000..3417e50bd --- /dev/null +++ b/packages/sdk/src/cli/workspace/app/index.ts @@ -0,0 +1,17 @@ +import { defineCommand, runCommand } from "citty"; +import { healthCommand } from "./health"; +import { listCommand } from "./list"; + +export const appCommand = defineCommand({ + meta: { + name: "app", + description: "Manage workspace applications", + }, + subCommands: { + health: healthCommand, + list: listCommand, + }, + async run() { + await runCommand(listCommand, { rawArgs: [] }); + }, +}); diff --git a/packages/sdk/src/cli/workspace/app/list.ts b/packages/sdk/src/cli/workspace/app/list.ts new file mode 100644 index 000000000..f90060cb4 --- /dev/null +++ b/packages/sdk/src/cli/workspace/app/list.ts @@ -0,0 +1,120 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { initOperatorClient } from "../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../context"; +import { humanizeRelativeTime } from "../../utils/format"; +import { logger } from "../../utils/logger"; +import { appInfo, type AppInfo } from "./transform"; + +const listAppsOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + limit: z.coerce.number().int().positive().optional(), +}); + +export type ListAppsOptions = z.input; + +async function loadOptions(options: ListAppsOptions) { + const result = listAppsOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + limit: result.data.limit, + }; +} + +/** + * List applications in a workspace with an optional limit. + * @param options - Application listing options + * @returns List of applications + */ +export async function listApps(options: ListAppsOptions): Promise { + const { client, workspaceId, limit } = await loadOptions(options); + const hasLimit = limit !== undefined; + + const results: AppInfo[] = []; + let pageToken = ""; + + while (true) { + if (hasLimit && results.length >= limit!) { + break; + } + + const remaining = hasLimit ? limit! - results.length : undefined; + const pageSize = remaining !== undefined && remaining > 0 ? remaining : undefined; + + const { applications, nextPageToken } = await client.listApplications({ + workspaceId, + pageToken, + ...(pageSize !== undefined ? { pageSize } : {}), + }); + + const mapped = applications.map(appInfo); + + if (remaining !== undefined && mapped.length > remaining) { + results.push(...mapped.slice(0, remaining)); + } else { + results.push(...mapped); + } + + if (!nextPageToken) { + break; + } + pageToken = nextPageToken; + } + + return results; +} + +export const listCommand = defineCommand({ + meta: { + name: "list", + description: "List applications in a workspace", + }, + args: { + ...commonArgs, + ...jsonArgs, + ...workspaceArgs, + limit: { + type: "string", + alias: "l", + description: "Maximum number of applications to list", + }, + }, + run: withCommonArgs(async (args) => { + let limit: number | undefined; + if (args.limit) { + limit = parseInt(args.limit, 10); + if (Number.isNaN(limit) || limit <= 0) { + throw new Error(`--limit must be a positive integer, got '${args.limit}'`); + } + } + + const apps = await listApps({ + workspaceId: args["workspace-id"], + profile: args.profile, + limit, + }); + + const formattedApps = args.json + ? apps + : apps.map(({ updatedAt: _, createdAt, ...rest }) => ({ + ...rest, + createdAt: humanizeRelativeTime(createdAt), + })); + + logger.out(formattedApps); + }), +}); diff --git a/packages/sdk/src/cli/workspace/app/transform.ts b/packages/sdk/src/cli/workspace/app/transform.ts new file mode 100644 index 000000000..6ecc88286 --- /dev/null +++ b/packages/sdk/src/cli/workspace/app/transform.ts @@ -0,0 +1,73 @@ +import { + type GetApplicationSchemaHealthResponse, + GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, +} from "@tailor-proto/tailor/v1/application_pb"; +import { ApplicationSchemaUpdateAttemptStatus } from "@tailor-proto/tailor/v1/application_resource_pb"; +import { formatTimestamp } from "../../utils/format"; +import type { Application } from "@tailor-proto/tailor/v1/application_resource_pb"; + +export interface AppInfo { + name: string; + domain: string; + authNamespace: string; + createdAt: string; + updatedAt: string; +} + +export interface AppHealthInfo { + name: string; + status: string; + currentServingSchemaUpdatedAt: string; + lastAttemptStatus: string; + lastAttemptAt: string; + lastAttemptError: string; +} + +const statusToString = ( + status: GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, +): string => { + switch (status) { + case GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus.OK: + return "ok"; + case GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus.COMPOSITION_ERROR: + return "composition_error"; + default: + return "unknown"; + } +}; + +const attemptStatusToString = (status: ApplicationSchemaUpdateAttemptStatus): string => { + switch (status) { + case ApplicationSchemaUpdateAttemptStatus.SUCCEEDED: + return "success"; + case ApplicationSchemaUpdateAttemptStatus.FAILED: + return "failure"; + default: + return "unknown"; + } +}; + +export const appInfo = (app: Application): AppInfo => { + return { + name: app.name, + domain: app.domain, + authNamespace: app.authNamespace, + createdAt: formatTimestamp(app.createTime), + updatedAt: formatTimestamp(app.updateTime), + }; +}; + +export const appHealthInfo = ( + name: string, + health: GetApplicationSchemaHealthResponse, +): AppHealthInfo => { + const attempt = health.lastSchemaUpdateAttempt; + return { + name, + status: statusToString(health.status), + currentServingSchemaUpdatedAt: formatTimestamp(health.currentServingSchemaUpdateTime), + lastAttemptStatus: attempt ? attemptStatusToString(attempt.status) : "N/A", + lastAttemptAt: formatTimestamp(attempt?.attemptTime), + lastAttemptError: attempt?.error ?? "", + }; +}; diff --git a/packages/sdk/src/cli/workspace/describe.ts b/packages/sdk/src/cli/workspace/describe.ts new file mode 100644 index 000000000..0622cacfc --- /dev/null +++ b/packages/sdk/src/cli/workspace/describe.ts @@ -0,0 +1,82 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, jsonArgs, withCommonArgs } from "../args"; +import { initOperatorClient } from "../client"; +import { loadAccessToken } from "../context"; +import { humanizeRelativeTime } from "../utils/format"; +import { logger } from "../utils/logger"; +import { workspaceDetails, type WorkspaceDetails } from "./transform"; + +const describeWorkspaceOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }), +}); + +export type DescribeWorkspaceOptions = z.input; + +async function loadOptions(options: DescribeWorkspaceOptions) { + const result = describeWorkspaceOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + + return { + client, + workspaceId: result.data.workspaceId, + }; +} + +/** + * Get detailed information about a workspace. + * @param options - Workspace describe options + * @returns Workspace details + */ +export async function describeWorkspace( + options: DescribeWorkspaceOptions, +): Promise { + const { client, workspaceId } = await loadOptions(options); + + const response = await client.getWorkspace({ + workspaceId, + }); + + if (!response.workspace) { + throw new Error(`Workspace "${workspaceId}" not found.`); + } + + return workspaceDetails(response.workspace); +} + +export const describeCommand = defineCommand({ + meta: { + name: "describe", + description: "Show detailed information about a workspace", + }, + args: { + ...commonArgs, + ...jsonArgs, + "workspace-id": { + type: "string", + description: "Workspace ID", + required: true, + alias: "w", + }, + }, + run: withCommonArgs(async (args) => { + const workspace = await describeWorkspace({ + workspaceId: args["workspace-id"], + }); + + const formattedWorkspace = args.json + ? workspace + : { + ...workspace, + createdAt: humanizeRelativeTime(workspace.createdAt), + updatedAt: humanizeRelativeTime(workspace.updatedAt), + }; + + logger.out(formattedWorkspace); + }), +}); diff --git a/packages/sdk/src/cli/workspace/index.ts b/packages/sdk/src/cli/workspace/index.ts index 529419489..6a76ba25e 100644 --- a/packages/sdk/src/cli/workspace/index.ts +++ b/packages/sdk/src/cli/workspace/index.ts @@ -1,7 +1,11 @@ import { defineCommand, runCommand } from "citty"; +import { appCommand } from "./app"; import { createCommand } from "./create"; import { deleteCommand } from "./delete"; +import { describeCommand } from "./describe"; import { listCommand } from "./list"; +import { restoreCommand } from "./restore"; +import { userCommand } from "./user"; export const workspaceCommand = defineCommand({ meta: { @@ -9,9 +13,13 @@ export const workspaceCommand = defineCommand({ description: "Manage Tailor Platform workspaces", }, subCommands: { + app: appCommand, create: createCommand, delete: deleteCommand, + describe: describeCommand, list: listCommand, + restore: restoreCommand, + user: userCommand, }, async run() { await runCommand(listCommand, { rawArgs: [] }); diff --git a/packages/sdk/src/cli/workspace/restore.ts b/packages/sdk/src/cli/workspace/restore.ts new file mode 100644 index 000000000..a8637a32e --- /dev/null +++ b/packages/sdk/src/cli/workspace/restore.ts @@ -0,0 +1,86 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, withCommonArgs } from "../args"; +import { initOperatorClient } from "../client"; +import { loadAccessToken } from "../context"; +import { logger } from "../utils/logger"; + +const restoreWorkspaceOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }), +}); + +export type RestoreWorkspaceOptions = z.input; + +async function loadOptions(options: RestoreWorkspaceOptions) { + const result = restoreWorkspaceOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + + return { + client, + workspaceId: result.data.workspaceId, + }; +} + +/** + * Restore a deleted workspace by ID. + * @param options - Workspace restore options + * @returns Promise that resolves when restoration completes + */ +export async function restoreWorkspace(options: RestoreWorkspaceOptions): Promise { + const { client, workspaceId } = await loadOptions(options); + + await client.restoreWorkspace({ + workspaceId, + }); +} + +export const restoreCommand = defineCommand({ + meta: { + name: "restore", + description: "Restore a deleted workspace", + }, + args: { + ...commonArgs, + "workspace-id": { + type: "string", + description: "Workspace ID to restore", + required: true, + alias: "w", + }, + yes: { + type: "boolean", + description: "Skip confirmation prompt", + alias: "y", + default: false, + }, + }, + run: withCommonArgs(async (args) => { + const { client, workspaceId } = await loadOptions({ + workspaceId: args["workspace-id"], + }); + + if (!args.yes) { + const confirmation = await logger.prompt( + `Are you sure you want to restore workspace "${workspaceId}"? (yes/no):`, + { + type: "text", + }, + ); + if (confirmation !== "yes") { + logger.info("Workspace restoration cancelled."); + return; + } + } + + await client.restoreWorkspace({ + workspaceId, + }); + + logger.success(`Workspace "${workspaceId}" restored successfully.`); + }), +}); diff --git a/packages/sdk/src/cli/workspace/transform.ts b/packages/sdk/src/cli/workspace/transform.ts index e63962281..02e582ed9 100644 --- a/packages/sdk/src/cli/workspace/transform.ts +++ b/packages/sdk/src/cli/workspace/transform.ts @@ -1,5 +1,4 @@ -import { timestampDate } from "@bufbuild/protobuf/wkt"; -import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { formatTimestamp } from "../utils/format"; import type { Workspace } from "@tailor-proto/tailor/v1/workspace_resource_pb"; export interface WorkspaceInfo { @@ -10,16 +9,11 @@ export interface WorkspaceInfo { updatedAt: string; } -const formatTimestamp = (timestamp: Timestamp | undefined): string => { - if (!timestamp) { - return "N/A"; - } - const date = timestampDate(timestamp); - if (Number.isNaN(date.getTime())) { - return "N/A"; - } - return date.toISOString(); -}; +export interface WorkspaceDetails extends WorkspaceInfo { + deleteProtection: boolean; + organizationId: string; + folderId: string; +} export const workspaceInfo = (workspace: Workspace): WorkspaceInfo => { return { @@ -30,3 +24,12 @@ export const workspaceInfo = (workspace: Workspace): WorkspaceInfo => { updatedAt: formatTimestamp(workspace.updateTime), }; }; + +export const workspaceDetails = (workspace: Workspace): WorkspaceDetails => { + return { + ...workspaceInfo(workspace), + deleteProtection: workspace.deleteProtection, + organizationId: workspace.organizationId, + folderId: workspace.folderId, + }; +}; diff --git a/packages/sdk/src/cli/workspace/user/index.ts b/packages/sdk/src/cli/workspace/user/index.ts new file mode 100644 index 000000000..65b86ddb9 --- /dev/null +++ b/packages/sdk/src/cli/workspace/user/index.ts @@ -0,0 +1,21 @@ +import { defineCommand, runCommand } from "citty"; +import { inviteCommand } from "./invite"; +import { listCommand } from "./list"; +import { removeCommand } from "./remove"; +import { updateCommand } from "./update"; + +export const userCommand = defineCommand({ + meta: { + name: "user", + description: "Manage workspace users", + }, + subCommands: { + invite: inviteCommand, + list: listCommand, + remove: removeCommand, + update: updateCommand, + }, + async run() { + await runCommand(listCommand, { rawArgs: [] }); + }, +}); diff --git a/packages/sdk/src/cli/workspace/user/invite.ts b/packages/sdk/src/cli/workspace/user/invite.ts new file mode 100644 index 000000000..2cf509b2e --- /dev/null +++ b/packages/sdk/src/cli/workspace/user/invite.ts @@ -0,0 +1,84 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { initOperatorClient } from "../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../context"; +import { logger } from "../../utils/logger"; +import { stringToRole, validRoles } from "./transform"; + +const inviteUserOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + email: z.string().email({ message: "email must be a valid email address" }), + role: z.enum(validRoles, { message: `role must be one of: ${validRoles.join(", ")}` }), +}); + +export type InviteUserOptions = z.input; + +async function loadOptions(options: InviteUserOptions) { + const result = inviteUserOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + email: result.data.email, + role: stringToRole(result.data.role), + }; +} + +/** + * Invite a user to a workspace. + * @param options - User invite options + * @returns Promise that resolves when invitation is sent + */ +export async function inviteUser(options: InviteUserOptions): Promise { + const { client, workspaceId, email, role } = await loadOptions(options); + + await client.inviteWorkspacePlatformUser({ + workspaceId, + email, + role, + }); +} + +export const inviteCommand = defineCommand({ + meta: { + name: "invite", + description: "Invite a user to a workspace", + }, + args: { + ...commonArgs, + ...workspaceArgs, + email: { + type: "string", + description: "Email address of the user to invite", + required: true, + }, + role: { + type: "string", + description: `Role to assign (${validRoles.join(", ")})`, + required: true, + alias: "r", + }, + }, + run: withCommonArgs(async (args) => { + await inviteUser({ + workspaceId: args["workspace-id"], + profile: args.profile, + email: args.email, + role: args.role as (typeof validRoles)[number], + }); + + logger.success(`User "${args.email}" invited successfully with role "${args.role}".`); + }), +}); diff --git a/packages/sdk/src/cli/workspace/user/list.ts b/packages/sdk/src/cli/workspace/user/list.ts new file mode 100644 index 000000000..c6d631bd3 --- /dev/null +++ b/packages/sdk/src/cli/workspace/user/list.ts @@ -0,0 +1,112 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { initOperatorClient } from "../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../context"; +import { logger } from "../../utils/logger"; +import { userInfo, type UserInfo } from "./transform"; + +const listUsersOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + limit: z.coerce.number().int().positive().optional(), +}); + +export type ListUsersOptions = z.input; + +async function loadOptions(options: ListUsersOptions) { + const result = listUsersOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + limit: result.data.limit, + }; +} + +/** + * List users in a workspace with an optional limit. + * @param options - User listing options + * @returns List of workspace users + */ +export async function listUsers(options: ListUsersOptions): Promise { + const { client, workspaceId, limit } = await loadOptions(options); + const hasLimit = limit !== undefined; + + const results: UserInfo[] = []; + let pageToken = ""; + + while (true) { + if (hasLimit && results.length >= limit!) { + break; + } + + const remaining = hasLimit ? limit! - results.length : undefined; + const pageSize = remaining !== undefined && remaining > 0 ? remaining : undefined; + + const { workspacePlatformUsers, nextPageToken } = await client.listWorkspacePlatformUsers({ + workspaceId, + pageToken, + ...(pageSize !== undefined ? { pageSize } : {}), + }); + + const mapped = workspacePlatformUsers.map(userInfo); + + if (remaining !== undefined && mapped.length > remaining) { + results.push(...mapped.slice(0, remaining)); + } else { + results.push(...mapped); + } + + if (!nextPageToken) { + break; + } + pageToken = nextPageToken; + } + + return results; +} + +export const listCommand = defineCommand({ + meta: { + name: "list", + description: "List users in a workspace", + }, + args: { + ...commonArgs, + ...jsonArgs, + ...workspaceArgs, + limit: { + type: "string", + alias: "l", + description: "Maximum number of users to list", + }, + }, + run: withCommonArgs(async (args) => { + let limit: number | undefined; + if (args.limit) { + limit = parseInt(args.limit, 10); + if (Number.isNaN(limit) || limit <= 0) { + throw new Error(`--limit must be a positive integer, got '${args.limit}'`); + } + } + + const users = await listUsers({ + workspaceId: args["workspace-id"], + profile: args.profile, + limit, + }); + + logger.out(users); + }), +}); diff --git a/packages/sdk/src/cli/workspace/user/remove.ts b/packages/sdk/src/cli/workspace/user/remove.ts new file mode 100644 index 000000000..66c653315 --- /dev/null +++ b/packages/sdk/src/cli/workspace/user/remove.ts @@ -0,0 +1,92 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { initOperatorClient } from "../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../context"; +import { logger } from "../../utils/logger"; + +const removeUserOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + email: z.string().email({ message: "email must be a valid email address" }), +}); + +export type RemoveUserOptions = z.input; + +async function loadOptions(options: RemoveUserOptions) { + const result = removeUserOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + email: result.data.email, + }; +} + +/** + * Remove a user from a workspace. + * @param options - User remove options + * @returns Promise that resolves when removal completes + */ +export async function removeUser(options: RemoveUserOptions): Promise { + const { client, workspaceId, email } = await loadOptions(options); + + await client.removeWorkspacePlatformUser({ + workspaceId, + email, + }); +} + +export const removeCommand = defineCommand({ + meta: { + name: "remove", + description: "Remove a user from a workspace", + }, + args: { + ...commonArgs, + ...workspaceArgs, + email: { + type: "string", + description: "Email address of the user to remove", + required: true, + }, + yes: { + type: "boolean", + description: "Skip confirmation prompt", + alias: "y", + default: false, + }, + }, + run: withCommonArgs(async (args) => { + if (!args.yes) { + const confirmation = await logger.prompt( + `Are you sure you want to remove user "${args.email}" from the workspace? (yes/no):`, + { + type: "text", + }, + ); + if (confirmation !== "yes") { + logger.info("User removal cancelled."); + return; + } + } + + await removeUser({ + workspaceId: args["workspace-id"], + profile: args.profile, + email: args.email, + }); + + logger.success(`User "${args.email}" removed from workspace.`); + }), +}); diff --git a/packages/sdk/src/cli/workspace/user/transform.ts b/packages/sdk/src/cli/workspace/user/transform.ts new file mode 100644 index 000000000..563fa5325 --- /dev/null +++ b/packages/sdk/src/cli/workspace/user/transform.ts @@ -0,0 +1,44 @@ +import { WorkspacePlatformUserRole } from "@tailor-proto/tailor/v1/workspace_resource_pb"; +import type { WorkspacePlatformUser } from "@tailor-proto/tailor/v1/workspace_resource_pb"; + +export interface UserInfo { + userId: string; + email: string; + role: string; +} + +const roleToString = (role: WorkspacePlatformUserRole): string => { + switch (role) { + case WorkspacePlatformUserRole.ADMIN: + return "admin"; + case WorkspacePlatformUserRole.EDITOR: + return "editor"; + case WorkspacePlatformUserRole.VIEWER: + return "viewer"; + default: + return "unknown"; + } +}; + +export const stringToRole = (role: string): WorkspacePlatformUserRole => { + switch (role.toLowerCase()) { + case "admin": + return WorkspacePlatformUserRole.ADMIN; + case "editor": + return WorkspacePlatformUserRole.EDITOR; + case "viewer": + return WorkspacePlatformUserRole.VIEWER; + default: + throw new Error(`Invalid role: ${role}. Valid roles: admin, editor, viewer`); + } +}; + +export const userInfo = (user: WorkspacePlatformUser): UserInfo => { + return { + userId: user.platformUser?.userId ?? "", + email: user.platformUser?.email ?? "", + role: roleToString(user.role), + }; +}; + +export const validRoles = ["admin", "editor", "viewer"] as const; diff --git a/packages/sdk/src/cli/workspace/user/update.ts b/packages/sdk/src/cli/workspace/user/update.ts new file mode 100644 index 000000000..58351acb7 --- /dev/null +++ b/packages/sdk/src/cli/workspace/user/update.ts @@ -0,0 +1,84 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { initOperatorClient } from "../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../context"; +import { logger } from "../../utils/logger"; +import { stringToRole, validRoles } from "./transform"; + +const updateUserOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + email: z.string().email({ message: "email must be a valid email address" }), + role: z.enum(validRoles, { message: `role must be one of: ${validRoles.join(", ")}` }), +}); + +export type UpdateUserOptions = z.input; + +async function loadOptions(options: UpdateUserOptions) { + const result = updateUserOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + email: result.data.email, + role: stringToRole(result.data.role), + }; +} + +/** + * Update a user's role in a workspace. + * @param options - User update options + * @returns Promise that resolves when update completes + */ +export async function updateUser(options: UpdateUserOptions): Promise { + const { client, workspaceId, email, role } = await loadOptions(options); + + await client.updateWorkspacePlatformUser({ + workspaceId, + email, + role, + }); +} + +export const updateCommand = defineCommand({ + meta: { + name: "update", + description: "Update a user's role in a workspace", + }, + args: { + ...commonArgs, + ...workspaceArgs, + email: { + type: "string", + description: "Email address of the user to update", + required: true, + }, + role: { + type: "string", + description: `New role to assign (${validRoles.join(", ")})`, + required: true, + alias: "r", + }, + }, + run: withCommonArgs(async (args) => { + await updateUser({ + workspaceId: args["workspace-id"], + profile: args.profile, + email: args.email, + role: args.role as (typeof validRoles)[number], + }); + + logger.success(`User "${args.email}" updated to role "${args.role}".`); + }), +}); From 98fd0bcaaa8426bde4d9410d3736423842d9913f Mon Sep 17 00:00:00 2001 From: r253hmdryou Date: Sun, 1 Feb 2026 23:52:22 +0900 Subject: [PATCH 2/7] add workspace function registry commands --- packages/sdk/src/cli/lib.ts | 2 + .../sdk/src/cli/workspace/function/index.ts | 12 ++ .../cli/workspace/function/registry/get.ts | 102 +++++++++++++++ .../cli/workspace/function/registry/index.ts | 17 +++ .../cli/workspace/function/registry/list.ts | 122 ++++++++++++++++++ .../workspace/function/registry/transform.ts | 22 ++++ packages/sdk/src/cli/workspace/index.ts | 2 + 7 files changed, 279 insertions(+) create mode 100644 packages/sdk/src/cli/workspace/function/index.ts create mode 100644 packages/sdk/src/cli/workspace/function/registry/get.ts create mode 100644 packages/sdk/src/cli/workspace/function/registry/index.ts create mode 100644 packages/sdk/src/cli/workspace/function/registry/list.ts create mode 100644 packages/sdk/src/cli/workspace/function/registry/transform.ts diff --git a/packages/sdk/src/cli/lib.ts b/packages/sdk/src/cli/lib.ts index dc3aa272a..7e5d60149 100644 --- a/packages/sdk/src/cli/lib.ts +++ b/packages/sdk/src/cli/lib.ts @@ -45,6 +45,8 @@ export type { UserInfo } from "./workspace/user/transform"; export { listApps, type ListAppsOptions } from "./workspace/app/list"; export { getAppHealth, type HealthOptions as GetAppHealthOptions } from "./workspace/app/health"; export type { AppInfo, AppHealthInfo } from "./workspace/app/transform"; +export { getFunctionRegistry } from "./workspace/function/registry/get"; +export { listFunctionRegistries } from "./workspace/function/registry/list"; export { listMachineUsers, type ListMachineUsersOptions, diff --git a/packages/sdk/src/cli/workspace/function/index.ts b/packages/sdk/src/cli/workspace/function/index.ts new file mode 100644 index 000000000..b6f1ae5a3 --- /dev/null +++ b/packages/sdk/src/cli/workspace/function/index.ts @@ -0,0 +1,12 @@ +import { defineCommand } from "citty"; +import { registryCommand } from "./registry"; + +export const functionCommand = defineCommand({ + meta: { + name: "function", + description: "Manage workspace functions", + }, + subCommands: { + registry: registryCommand, + }, +}); diff --git a/packages/sdk/src/cli/workspace/function/registry/get.ts b/packages/sdk/src/cli/workspace/function/registry/get.ts new file mode 100644 index 000000000..f28e7aab1 --- /dev/null +++ b/packages/sdk/src/cli/workspace/function/registry/get.ts @@ -0,0 +1,102 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../../args"; +import { initOperatorClient } from "../../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../../context"; +import { humanizeRelativeTime } from "../../../utils/format"; +import { logger } from "../../../utils/logger"; +import { functionRegistryInfo, type FunctionRegistryInfo } from "./transform"; + +const getRegistryOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + name: z.string().min(1, { message: "name is required" }), +}); + +export type GetRegistryOptions = z.input; + +async function loadOptions(options: GetRegistryOptions) { + const result = getRegistryOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + name: result.data.name, + }; +} + +/** + * Get a function registry by name. + * @param options - Function registry get options + * @returns Function registry info + */ +export async function getFunctionRegistry( + options: GetRegistryOptions, +): Promise { + const { client, workspaceId, name } = await loadOptions(options); + + const notFoundErrorMessage = `Function "${name}" not found.`; + try { + const response = await client.getFunctionRegistry({ + workspaceId, + name, + }); + + if (!response.function) { + throw new Error(notFoundErrorMessage); + } + + return functionRegistryInfo(response.function); + } catch (error) { + if (error instanceof ConnectError && error.code === Code.NotFound) { + throw new Error(notFoundErrorMessage); + } + throw error; + } +} + +export const getCommand = defineCommand({ + meta: { + name: "get", + description: "Get a function registry by name", + }, + args: { + ...commonArgs, + ...jsonArgs, + ...workspaceArgs, + name: { + type: "string", + description: "Function name", + required: true, + alias: "n", + }, + }, + run: withCommonArgs(async (args) => { + const fn = await getFunctionRegistry({ + workspaceId: args["workspace-id"], + profile: args.profile, + name: args.name, + }); + + const formatted = args.json + ? fn + : { + ...fn, + createdAt: humanizeRelativeTime(fn.createdAt), + updatedAt: humanizeRelativeTime(fn.updatedAt), + }; + + logger.out(formatted); + }), +}); diff --git a/packages/sdk/src/cli/workspace/function/registry/index.ts b/packages/sdk/src/cli/workspace/function/registry/index.ts new file mode 100644 index 000000000..17043b770 --- /dev/null +++ b/packages/sdk/src/cli/workspace/function/registry/index.ts @@ -0,0 +1,17 @@ +import { defineCommand, runCommand } from "citty"; +import { getCommand } from "./get"; +import { listCommand } from "./list"; + +export const registryCommand = defineCommand({ + meta: { + name: "registry", + description: "Manage function registry entries", + }, + subCommands: { + get: getCommand, + list: listCommand, + }, + async run() { + await runCommand(listCommand, { rawArgs: [] }); + }, +}); diff --git a/packages/sdk/src/cli/workspace/function/registry/list.ts b/packages/sdk/src/cli/workspace/function/registry/list.ts new file mode 100644 index 000000000..cf267e81d --- /dev/null +++ b/packages/sdk/src/cli/workspace/function/registry/list.ts @@ -0,0 +1,122 @@ +import { defineCommand } from "citty"; +import { z } from "zod"; +import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../../args"; +import { initOperatorClient } from "../../../client"; +import { loadAccessToken, loadWorkspaceId } from "../../../context"; +import { humanizeRelativeTime } from "../../../utils/format"; +import { logger } from "../../../utils/logger"; +import { functionRegistryInfo, type FunctionRegistryInfo } from "./transform"; + +const listRegistryOptionsSchema = z.object({ + workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), + profile: z.string().optional(), + limit: z.coerce.number().int().positive().optional(), +}); + +export type ListRegistryOptions = z.input; + +async function loadOptions(options: ListRegistryOptions) { + const result = listRegistryOptionsSchema.safeParse(options); + if (!result.success) { + throw new Error(result.error.issues[0].message); + } + + const accessToken = await loadAccessToken(); + const client = await initOperatorClient(accessToken); + const workspaceId = loadWorkspaceId({ + workspaceId: result.data.workspaceId, + profile: result.data.profile, + }); + + return { + client, + workspaceId, + limit: result.data.limit, + }; +} + +/** + * List function registries in a workspace with an optional limit. + * @param options - Function registry listing options + * @returns List of function registries + */ +export async function listFunctionRegistries( + options: ListRegistryOptions, +): Promise { + const { client, workspaceId, limit } = await loadOptions(options); + const hasLimit = limit !== undefined; + + const results: FunctionRegistryInfo[] = []; + let pageToken = ""; + + while (true) { + if (hasLimit && results.length >= limit!) { + break; + } + + const remaining = hasLimit ? limit! - results.length : undefined; + const pageSize = remaining !== undefined && remaining > 0 ? remaining : undefined; + + const { functions, nextPageToken } = await client.listFunctionRegistries({ + workspaceId, + pageToken, + ...(pageSize !== undefined ? { pageSize } : {}), + }); + + const mapped = functions.map(functionRegistryInfo); + if (remaining !== undefined && mapped.length > remaining) { + results.push(...mapped.slice(0, remaining)); + } else { + results.push(...mapped); + } + + if (!nextPageToken) { + break; + } + pageToken = nextPageToken; + } + + return results; +} + +export const listCommand = defineCommand({ + meta: { + name: "list", + description: "List function registries in a workspace", + }, + args: { + ...commonArgs, + ...jsonArgs, + ...workspaceArgs, + limit: { + type: "string", + alias: "l", + description: "Maximum number of functions to list", + }, + }, + run: withCommonArgs(async (args) => { + let limit: number | undefined; + if (args.limit) { + limit = parseInt(args.limit, 10); + if (Number.isNaN(limit) || limit <= 0) { + throw new Error(`--limit must be a positive integer, got '${args.limit}'`); + } + } + + const functions = await listFunctionRegistries({ + workspaceId: args["workspace-id"], + profile: args.profile, + limit, + }); + + const formatted = args.json + ? functions + : functions.map(({ createdAt, updatedAt, ...rest }) => ({ + ...rest, + createdAt: humanizeRelativeTime(createdAt), + updatedAt: humanizeRelativeTime(updatedAt), + })); + + logger.out(formatted); + }), +}); diff --git a/packages/sdk/src/cli/workspace/function/registry/transform.ts b/packages/sdk/src/cli/workspace/function/registry/transform.ts new file mode 100644 index 000000000..630121727 --- /dev/null +++ b/packages/sdk/src/cli/workspace/function/registry/transform.ts @@ -0,0 +1,22 @@ +import { formatTimestamp } from "../../../utils/format"; +import type { FunctionRegistry } from "@tailor-proto/tailor/v1/function_registry_pb"; + +export interface FunctionRegistryInfo { + name: string; + description: string; + sizeBytes: string; + contentHash: string; + createdAt: string; + updatedAt: string; +} + +export const functionRegistryInfo = (fn: FunctionRegistry): FunctionRegistryInfo => { + return { + name: fn.name, + description: fn.description, + sizeBytes: fn.sizeBytes.toString(), + contentHash: fn.contentHash, + createdAt: formatTimestamp(fn.createdAt), + updatedAt: formatTimestamp(fn.updatedAt), + }; +}; diff --git a/packages/sdk/src/cli/workspace/index.ts b/packages/sdk/src/cli/workspace/index.ts index 6a76ba25e..5c42c1b4c 100644 --- a/packages/sdk/src/cli/workspace/index.ts +++ b/packages/sdk/src/cli/workspace/index.ts @@ -3,6 +3,7 @@ import { appCommand } from "./app"; import { createCommand } from "./create"; import { deleteCommand } from "./delete"; import { describeCommand } from "./describe"; +import { functionCommand } from "./function"; import { listCommand } from "./list"; import { restoreCommand } from "./restore"; import { userCommand } from "./user"; @@ -17,6 +18,7 @@ export const workspaceCommand = defineCommand({ create: createCommand, delete: deleteCommand, describe: describeCommand, + function: functionCommand, list: listCommand, restore: restoreCommand, user: userCommand, From 2bb98e259861d275695c2de53e6e9ff371e1d6eb Mon Sep 17 00:00:00 2001 From: r253hmdryou Date: Wed, 4 Feb 2026 01:20:30 +0900 Subject: [PATCH 3/7] fix(format): update formatTimestamp to return Date object or null instead of ISO string or "N/A" --- packages/sdk/src/cli/utils/format.ts | 10 +++++----- packages/sdk/src/cli/workspace/app/transform.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/cli/utils/format.ts b/packages/sdk/src/cli/utils/format.ts index 349cbf780..576f34979 100644 --- a/packages/sdk/src/cli/utils/format.ts +++ b/packages/sdk/src/cli/utils/format.ts @@ -8,17 +8,17 @@ import type { TableUserConfig } from "table"; /** * Format a protobuf Timestamp to ISO string. * @param timestamp - Protobuf timestamp - * @returns ISO date string or "N/A" if invalid + * @returns Date object or null if invalid */ -export function formatTimestamp(timestamp: Timestamp | undefined): string { +export function formatTimestamp(timestamp: Timestamp | undefined): Date | null { if (!timestamp) { - return "N/A"; + return null; } const date = timestampDate(timestamp); if (Number.isNaN(date.getTime())) { - return "N/A"; + return null; } - return date.toISOString(); + return date; } /** diff --git a/packages/sdk/src/cli/workspace/app/transform.ts b/packages/sdk/src/cli/workspace/app/transform.ts index 6ecc88286..5bf6186c3 100644 --- a/packages/sdk/src/cli/workspace/app/transform.ts +++ b/packages/sdk/src/cli/workspace/app/transform.ts @@ -10,16 +10,16 @@ export interface AppInfo { name: string; domain: string; authNamespace: string; - createdAt: string; - updatedAt: string; + createdAt: Date | null; + updatedAt: Date | null; } export interface AppHealthInfo { name: string; status: string; - currentServingSchemaUpdatedAt: string; + currentServingSchemaUpdatedAt: Date | null; lastAttemptStatus: string; - lastAttemptAt: string; + lastAttemptAt: Date | null; lastAttemptError: string; } From 3127ef5fe8c0ffcb6edce26451b5517144a7081d Mon Sep 17 00:00:00 2001 From: r253hmdryou Date: Wed, 4 Feb 2026 01:21:32 +0900 Subject: [PATCH 4/7] migrate from citty to politty (workspace command) --- packages/sdk/src/cli/workspace/app/health.ts | 18 +++++------- packages/sdk/src/cli/workspace/app/index.ts | 10 +++---- packages/sdk/src/cli/workspace/app/list.ts | 29 ++++++------------- packages/sdk/src/cli/workspace/describe.ts | 20 +++++-------- packages/sdk/src/cli/workspace/restore.ts | 29 +++++++------------ packages/sdk/src/cli/workspace/user/index.ts | 10 +++---- packages/sdk/src/cli/workspace/user/invite.ts | 26 +++++++---------- packages/sdk/src/cli/workspace/user/list.ts | 29 ++++++------------- packages/sdk/src/cli/workspace/user/remove.ts | 27 ++++++----------- packages/sdk/src/cli/workspace/user/update.ts | 24 ++++++--------- 10 files changed, 79 insertions(+), 143 deletions(-) diff --git a/packages/sdk/src/cli/workspace/app/health.ts b/packages/sdk/src/cli/workspace/app/health.ts index 658d7fe09..ffeaeae69 100644 --- a/packages/sdk/src/cli/workspace/app/health.ts +++ b/packages/sdk/src/cli/workspace/app/health.ts @@ -1,4 +1,4 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../args"; import { initOperatorClient } from "../../client"; @@ -52,21 +52,17 @@ export async function getAppHealth(options: HealthOptions): Promise { const health = await getAppHealth({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/workspace/app/index.ts b/packages/sdk/src/cli/workspace/app/index.ts index 3417e50bd..b6a8443fb 100644 --- a/packages/sdk/src/cli/workspace/app/index.ts +++ b/packages/sdk/src/cli/workspace/app/index.ts @@ -1,17 +1,15 @@ -import { defineCommand, runCommand } from "citty"; +import { defineCommand, runCommand } from "politty"; import { healthCommand } from "./health"; import { listCommand } from "./list"; export const appCommand = defineCommand({ - meta: { - name: "app", - description: "Manage workspace applications", - }, + name: "app", + description: "Manage workspace applications", subCommands: { health: healthCommand, list: listCommand, }, async run() { - await runCommand(listCommand, { rawArgs: [] }); + await runCommand(listCommand, []); }, }); diff --git a/packages/sdk/src/cli/workspace/app/list.ts b/packages/sdk/src/cli/workspace/app/list.ts index f90060cb4..347462c0a 100644 --- a/packages/sdk/src/cli/workspace/app/list.ts +++ b/packages/sdk/src/cli/workspace/app/list.ts @@ -1,6 +1,6 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; -import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { commonArgs, jsonArgs, positiveIntArg, withCommonArgs, workspaceArgs } from "../../args"; import { initOperatorClient } from "../../client"; import { loadAccessToken, loadWorkspaceId } from "../../context"; import { humanizeRelativeTime } from "../../utils/format"; @@ -79,33 +79,22 @@ export async function listApps(options: ListAppsOptions): Promise { } export const listCommand = defineCommand({ - meta: { - name: "list", - description: "List applications in a workspace", - }, - args: { + name: "list", + description: "List applications in a workspace", + args: z.object({ ...commonArgs, ...jsonArgs, ...workspaceArgs, - limit: { - type: "string", + limit: arg(positiveIntArg.optional(), { alias: "l", description: "Maximum number of applications to list", - }, - }, + }), + }), run: withCommonArgs(async (args) => { - let limit: number | undefined; - if (args.limit) { - limit = parseInt(args.limit, 10); - if (Number.isNaN(limit) || limit <= 0) { - throw new Error(`--limit must be a positive integer, got '${args.limit}'`); - } - } - const apps = await listApps({ workspaceId: args["workspace-id"], profile: args.profile, - limit, + limit: args.limit, }); const formattedApps = args.json diff --git a/packages/sdk/src/cli/workspace/describe.ts b/packages/sdk/src/cli/workspace/describe.ts index 0622cacfc..6dd82a7fe 100644 --- a/packages/sdk/src/cli/workspace/describe.ts +++ b/packages/sdk/src/cli/workspace/describe.ts @@ -1,4 +1,4 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; import { commonArgs, jsonArgs, withCommonArgs } from "../args"; import { initOperatorClient } from "../client"; @@ -50,20 +50,16 @@ export async function describeWorkspace( } export const describeCommand = defineCommand({ - meta: { - name: "describe", - description: "Show detailed information about a workspace", - }, - args: { + name: "describe", + description: "Show detailed information about a workspace", + args: z.object({ ...commonArgs, ...jsonArgs, - "workspace-id": { - type: "string", - description: "Workspace ID", - required: true, + "workspace-id": arg(z.string(), { alias: "w", - }, - }, + description: "Workspace ID", + }), + }), run: withCommonArgs(async (args) => { const workspace = await describeWorkspace({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/workspace/restore.ts b/packages/sdk/src/cli/workspace/restore.ts index a8637a32e..730fe09e5 100644 --- a/packages/sdk/src/cli/workspace/restore.ts +++ b/packages/sdk/src/cli/workspace/restore.ts @@ -1,6 +1,6 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; -import { commonArgs, withCommonArgs } from "../args"; +import { commonArgs, confirmationArgs, withCommonArgs } from "../args"; import { initOperatorClient } from "../client"; import { loadAccessToken } from "../context"; import { logger } from "../utils/logger"; @@ -40,25 +40,16 @@ export async function restoreWorkspace(options: RestoreWorkspaceOptions): Promis } export const restoreCommand = defineCommand({ - meta: { - name: "restore", - description: "Restore a deleted workspace", - }, - args: { + name: "restore", + description: "Restore a deleted workspace", + args: z.object({ ...commonArgs, - "workspace-id": { - type: "string", - description: "Workspace ID to restore", - required: true, + "workspace-id": arg(z.string(), { alias: "w", - }, - yes: { - type: "boolean", - description: "Skip confirmation prompt", - alias: "y", - default: false, - }, - }, + description: "Workspace ID", + }), + ...confirmationArgs, + }), run: withCommonArgs(async (args) => { const { client, workspaceId } = await loadOptions({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/workspace/user/index.ts b/packages/sdk/src/cli/workspace/user/index.ts index 65b86ddb9..120c4e5ff 100644 --- a/packages/sdk/src/cli/workspace/user/index.ts +++ b/packages/sdk/src/cli/workspace/user/index.ts @@ -1,14 +1,12 @@ -import { defineCommand, runCommand } from "citty"; +import { defineCommand, runCommand } from "politty"; import { inviteCommand } from "./invite"; import { listCommand } from "./list"; import { removeCommand } from "./remove"; import { updateCommand } from "./update"; export const userCommand = defineCommand({ - meta: { - name: "user", - description: "Manage workspace users", - }, + name: "user", + description: "Manage workspace users", subCommands: { invite: inviteCommand, list: listCommand, @@ -16,6 +14,6 @@ export const userCommand = defineCommand({ update: updateCommand, }, async run() { - await runCommand(listCommand, { rawArgs: [] }); + await runCommand(listCommand, []); }, }); diff --git a/packages/sdk/src/cli/workspace/user/invite.ts b/packages/sdk/src/cli/workspace/user/invite.ts index 2cf509b2e..76c9b7649 100644 --- a/packages/sdk/src/cli/workspace/user/invite.ts +++ b/packages/sdk/src/cli/workspace/user/invite.ts @@ -1,4 +1,4 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; import { commonArgs, withCommonArgs, workspaceArgs } from "../../args"; import { initOperatorClient } from "../../client"; @@ -9,7 +9,7 @@ import { stringToRole, validRoles } from "./transform"; const inviteUserOptionsSchema = z.object({ workspaceId: z.uuid({ message: "workspace-id must be a valid UUID" }).optional(), profile: z.string().optional(), - email: z.string().email({ message: "email must be a valid email address" }), + email: z.email({ message: "email must be a valid email address" }), role: z.enum(validRoles, { message: `role must be one of: ${validRoles.join(", ")}` }), }); @@ -52,25 +52,19 @@ export async function inviteUser(options: InviteUserOptions): Promise { } export const inviteCommand = defineCommand({ - meta: { - name: "invite", - description: "Invite a user to a workspace", - }, - args: { + name: "invite", + description: "Invite a user to a workspace", + args: z.object({ ...commonArgs, ...workspaceArgs, - email: { - type: "string", + email: arg(z.email(), { description: "Email address of the user to invite", - required: true, - }, - role: { - type: "string", + }), + role: arg(z.enum(validRoles), { description: `Role to assign (${validRoles.join(", ")})`, - required: true, alias: "r", - }, - }, + }), + }), run: withCommonArgs(async (args) => { await inviteUser({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/workspace/user/list.ts b/packages/sdk/src/cli/workspace/user/list.ts index c6d631bd3..0ea6f4b42 100644 --- a/packages/sdk/src/cli/workspace/user/list.ts +++ b/packages/sdk/src/cli/workspace/user/list.ts @@ -1,6 +1,6 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; -import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { commonArgs, jsonArgs, positiveIntArg, withCommonArgs, workspaceArgs } from "../../args"; import { initOperatorClient } from "../../client"; import { loadAccessToken, loadWorkspaceId } from "../../context"; import { logger } from "../../utils/logger"; @@ -78,33 +78,22 @@ export async function listUsers(options: ListUsersOptions): Promise } export const listCommand = defineCommand({ - meta: { - name: "list", - description: "List users in a workspace", - }, - args: { + name: "list", + description: "List users in a workspace", + args: z.object({ ...commonArgs, ...jsonArgs, ...workspaceArgs, - limit: { - type: "string", + limit: arg(positiveIntArg.optional(), { alias: "l", description: "Maximum number of users to list", - }, - }, + }), + }), run: withCommonArgs(async (args) => { - let limit: number | undefined; - if (args.limit) { - limit = parseInt(args.limit, 10); - if (Number.isNaN(limit) || limit <= 0) { - throw new Error(`--limit must be a positive integer, got '${args.limit}'`); - } - } - const users = await listUsers({ workspaceId: args["workspace-id"], profile: args.profile, - limit, + limit: args.limit, }); logger.out(users); diff --git a/packages/sdk/src/cli/workspace/user/remove.ts b/packages/sdk/src/cli/workspace/user/remove.ts index 66c653315..85ad0a1e6 100644 --- a/packages/sdk/src/cli/workspace/user/remove.ts +++ b/packages/sdk/src/cli/workspace/user/remove.ts @@ -1,6 +1,6 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; -import { commonArgs, withCommonArgs, workspaceArgs } from "../../args"; +import { commonArgs, confirmationArgs, withCommonArgs, workspaceArgs } from "../../args"; import { initOperatorClient } from "../../client"; import { loadAccessToken, loadWorkspaceId } from "../../context"; import { logger } from "../../utils/logger"; @@ -48,25 +48,16 @@ export async function removeUser(options: RemoveUserOptions): Promise { } export const removeCommand = defineCommand({ - meta: { - name: "remove", - description: "Remove a user from a workspace", - }, - args: { + name: "remove", + description: "Remove a user from a workspace", + args: z.object({ ...commonArgs, ...workspaceArgs, - email: { - type: "string", + email: arg(z.email(), { description: "Email address of the user to remove", - required: true, - }, - yes: { - type: "boolean", - description: "Skip confirmation prompt", - alias: "y", - default: false, - }, - }, + }), + ...confirmationArgs, + }), run: withCommonArgs(async (args) => { if (!args.yes) { const confirmation = await logger.prompt( diff --git a/packages/sdk/src/cli/workspace/user/update.ts b/packages/sdk/src/cli/workspace/user/update.ts index 58351acb7..4717bdd2d 100644 --- a/packages/sdk/src/cli/workspace/user/update.ts +++ b/packages/sdk/src/cli/workspace/user/update.ts @@ -1,4 +1,4 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; import { commonArgs, withCommonArgs, workspaceArgs } from "../../args"; import { initOperatorClient } from "../../client"; @@ -52,25 +52,19 @@ export async function updateUser(options: UpdateUserOptions): Promise { } export const updateCommand = defineCommand({ - meta: { - name: "update", - description: "Update a user's role in a workspace", - }, - args: { + name: "update", + description: "Update a user's role in a workspace", + args: z.object({ ...commonArgs, ...workspaceArgs, - email: { - type: "string", + email: arg(z.email(), { description: "Email address of the user to update", - required: true, - }, - role: { - type: "string", + }), + role: arg(z.enum(validRoles), { description: `New role to assign (${validRoles.join(", ")})`, - required: true, alias: "r", - }, - }, + }), + }), run: withCommonArgs(async (args) => { await updateUser({ workspaceId: args["workspace-id"], From 13db9c73d4b4955a669889d024e7c6670b824d00 Mon Sep 17 00:00:00 2001 From: r253hmdryou Date: Wed, 4 Feb 2026 01:23:24 +0900 Subject: [PATCH 5/7] update document (workspace command) --- packages/sdk/docs/cli/workspace.md | 238 ++++++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 5 deletions(-) diff --git a/packages/sdk/docs/cli/workspace.md b/packages/sdk/docs/cli/workspace.md index c5b654766..7e0808814 100644 --- a/packages/sdk/docs/cli/workspace.md +++ b/packages/sdk/docs/cli/workspace.md @@ -16,11 +16,15 @@ tailor-sdk workspace [command] **Commands** -| Command | Description | -| --------------------------------------- | --------------------------------------- | -| [`workspace create`](#workspace-create) | Create a new Tailor Platform workspace. | -| [`workspace delete`](#workspace-delete) | Delete a Tailor Platform workspace. | -| [`workspace list`](#workspace-list) | List all Tailor Platform workspaces. | +| Command | Description | +| ------------------------------------------- | ------------------------------------------- | +| [`workspace app`](#workspace-app) | Manage workspace applications | +| [`workspace create`](#workspace-create) | Create a new Tailor Platform workspace. | +| [`workspace delete`](#workspace-delete) | Delete a Tailor Platform workspace. | +| [`workspace describe`](#workspace-describe) | Show detailed information about a workspace | +| [`workspace list`](#workspace-list) | List all Tailor Platform workspaces. | +| [`workspace restore`](#workspace-restore) | Restore a deleted workspace | +| [`workspace user`](#workspace-user) | Manage workspace users | @@ -203,3 +207,227 @@ tailor-sdk profile delete [options] | `name` | Profile name | Yes | + + + +### workspace app + +Manage workspace applications + +**Usage** + +``` +tailor-sdk workspace app [command] +``` + +**Commands** + +| Command | Description | +| ----------------------------------------------- | -------------------------------- | +| [`workspace app health`](#workspace-app-health) | Check application schema health | +| [`workspace app list`](#workspace-app-list) | List applications in a workspace | + + + + + +#### workspace app health + +Check application schema health + +**Usage** + +``` +tailor-sdk workspace app health [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | ----------------- | ------- | +| `--json` | `-j` | Output as JSON | `false` | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--profile ` | `-p` | Workspace profile | - | +| `--name ` | `-n` | Application name | - | + + + + + +#### workspace app list + +List applications in a workspace + +**Usage** + +``` +tailor-sdk workspace app list [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | -------------------------------------- | ------- | +| `--json` | `-j` | Output as JSON | `false` | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--profile ` | `-p` | Workspace profile | - | +| `--limit ` | `-l` | Maximum number of applications to list | - | + + + + + +### workspace describe + +Show detailed information about a workspace + +**Usage** + +``` +tailor-sdk workspace describe [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | -------------- | ------- | +| `--json` | `-j` | Output as JSON | `false` | +| `--workspace-id ` | `-w` | Workspace ID | - | + + + + + +### workspace restore + +Restore a deleted workspace + +**Usage** + +``` +tailor-sdk workspace restore [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | ------------------------- | ------- | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--yes` | `-y` | Skip confirmation prompts | `false` | + + + + + +### workspace user + +Manage workspace users + +**Usage** + +``` +tailor-sdk workspace user [command] +``` + +**Commands** + +| Command | Description | +| ------------------------------------------------- | ----------------------------------- | +| [`workspace user invite`](#workspace-user-invite) | Invite a user to a workspace | +| [`workspace user list`](#workspace-user-list) | List users in a workspace | +| [`workspace user remove`](#workspace-user-remove) | Remove a user from a workspace | +| [`workspace user update`](#workspace-user-update) | Update a user's role in a workspace | + + + + + +#### workspace user invite + +Invite a user to a workspace + +**Usage** + +``` +tailor-sdk workspace user invite [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | -------------------------------------- | ------- | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--profile ` | `-p` | Workspace profile | - | +| `--email ` | - | Email address of the user to invite | - | +| `--role ` | `-r` | Role to assign (admin, editor, viewer) | - | + + + + + +#### workspace user list + +List users in a workspace + +**Usage** + +``` +tailor-sdk workspace user list [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | ------------------------------- | ------- | +| `--json` | `-j` | Output as JSON | `false` | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--profile ` | `-p` | Workspace profile | - | +| `--limit ` | `-l` | Maximum number of users to list | - | + + + + + +#### workspace user remove + +Remove a user from a workspace + +**Usage** + +``` +tailor-sdk workspace user remove [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | ----------------------------------- | ------- | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--profile ` | `-p` | Workspace profile | - | +| `--email ` | - | Email address of the user to remove | - | +| `--yes` | `-y` | Skip confirmation prompts | `false` | + + + + + +#### workspace user update + +Update a user's role in a workspace + +**Usage** + +``` +tailor-sdk workspace user update [options] +``` + +**Options** + +| Option | Alias | Description | Default | +| ------------------------------- | ----- | ------------------------------------------ | ------- | +| `--workspace-id ` | `-w` | Workspace ID | - | +| `--profile ` | `-p` | Workspace profile | - | +| `--email ` | - | Email address of the user to update | - | +| `--role ` | `-r` | New role to assign (admin, editor, viewer) | - | + + From 0162f17aa476614176b4687ea1491b647f6bbcf4 Mon Sep 17 00:00:00 2001 From: r253hmdryou Date: Wed, 4 Feb 2026 01:46:44 +0900 Subject: [PATCH 6/7] fix: update FunctionRegistryInfo to use Date type for createdAt and updatedAt --- packages/sdk/src/cli/workspace/function/registry/transform.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/workspace/function/registry/transform.ts b/packages/sdk/src/cli/workspace/function/registry/transform.ts index 630121727..d4c74c66d 100644 --- a/packages/sdk/src/cli/workspace/function/registry/transform.ts +++ b/packages/sdk/src/cli/workspace/function/registry/transform.ts @@ -6,8 +6,8 @@ export interface FunctionRegistryInfo { description: string; sizeBytes: string; contentHash: string; - createdAt: string; - updatedAt: string; + createdAt: Date | null; + updatedAt: Date | null; } export const functionRegistryInfo = (fn: FunctionRegistry): FunctionRegistryInfo => { From 49f8fd9418ad768a7c010c3e34fdebd2258bf420 Mon Sep 17 00:00:00 2001 From: r253hmdryou Date: Wed, 4 Feb 2026 01:51:07 +0900 Subject: [PATCH 7/7] refactor: migrate from citty to politty and update command structure in workspace functions --- packages/sdk/knip.json | 4 +++ .../sdk/src/cli/workspace/function/index.ts | 14 ++++----- .../cli/workspace/function/registry/get.ts | 20 ++++++------ .../cli/workspace/function/registry/index.ts | 10 +++--- .../cli/workspace/function/registry/list.ts | 31 +++++++------------ packages/sdk/src/cli/workspace/index.ts | 4 +-- 6 files changed, 37 insertions(+), 46 deletions(-) diff --git a/packages/sdk/knip.json b/packages/sdk/knip.json index f4288b9bf..07d9baaf2 100644 --- a/packages/sdk/knip.json +++ b/packages/sdk/knip.json @@ -6,6 +6,10 @@ }, "tags": ["-lintignore"], "ignore": ["scripts/**", "e2e/fixtures/**"], + "ignoreFiles": [ + "src/cli/workspace/function/index.ts", + "src/cli/workspace/function/registry/index.ts" + ], "ignoreDependencies": [ "@tailor-platform/function-kysely-tailordb", "@typescript/native-preview", diff --git a/packages/sdk/src/cli/workspace/function/index.ts b/packages/sdk/src/cli/workspace/function/index.ts index b6f1ae5a3..88d8c1eac 100644 --- a/packages/sdk/src/cli/workspace/function/index.ts +++ b/packages/sdk/src/cli/workspace/function/index.ts @@ -1,12 +1,12 @@ -import { defineCommand } from "citty"; -import { registryCommand } from "./registry"; +import { defineCommand } from "politty"; +// import { registryCommand } from "./registry"; export const functionCommand = defineCommand({ - meta: { - name: "function", - description: "Manage workspace functions", - }, + name: "function", + description: "Manage workspace functions", subCommands: { - registry: registryCommand, + // The implementation of Registry get-type commands is complete, but currently the registry is not deployed, + // resulting in always returning 0 records. This command will be enabled after the fix. + // registry: registryCommand, }, }); diff --git a/packages/sdk/src/cli/workspace/function/registry/get.ts b/packages/sdk/src/cli/workspace/function/registry/get.ts index f28e7aab1..bcf2ee0e6 100644 --- a/packages/sdk/src/cli/workspace/function/registry/get.ts +++ b/packages/sdk/src/cli/workspace/function/registry/get.ts @@ -1,5 +1,5 @@ import { Code, ConnectError } from "@connectrpc/connect"; -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../../args"; import { initOperatorClient } from "../../../client"; @@ -66,22 +66,20 @@ export async function getFunctionRegistry( } } +// oxlint-disable-next-line jsdoc/check-tag-names +/** @lintignore */ export const getCommand = defineCommand({ - meta: { - name: "get", - description: "Get a function registry by name", - }, - args: { + name: "get", + description: "Get a function registry by name", + args: z.object({ ...commonArgs, ...jsonArgs, ...workspaceArgs, - name: { - type: "string", + name: arg(z.string(), { description: "Function name", - required: true, alias: "n", - }, - }, + }), + }), run: withCommonArgs(async (args) => { const fn = await getFunctionRegistry({ workspaceId: args["workspace-id"], diff --git a/packages/sdk/src/cli/workspace/function/registry/index.ts b/packages/sdk/src/cli/workspace/function/registry/index.ts index 17043b770..d42617caa 100644 --- a/packages/sdk/src/cli/workspace/function/registry/index.ts +++ b/packages/sdk/src/cli/workspace/function/registry/index.ts @@ -1,17 +1,15 @@ -import { defineCommand, runCommand } from "citty"; +import { defineCommand, runCommand } from "politty"; import { getCommand } from "./get"; import { listCommand } from "./list"; export const registryCommand = defineCommand({ - meta: { - name: "registry", - description: "Manage function registry entries", - }, + name: "registry", + description: "Manage function registry entries", subCommands: { get: getCommand, list: listCommand, }, async run() { - await runCommand(listCommand, { rawArgs: [] }); + await runCommand(listCommand, []); }, }); diff --git a/packages/sdk/src/cli/workspace/function/registry/list.ts b/packages/sdk/src/cli/workspace/function/registry/list.ts index cf267e81d..650d244ff 100644 --- a/packages/sdk/src/cli/workspace/function/registry/list.ts +++ b/packages/sdk/src/cli/workspace/function/registry/list.ts @@ -1,6 +1,6 @@ -import { defineCommand } from "citty"; +import { arg, defineCommand } from "politty"; import { z } from "zod"; -import { commonArgs, jsonArgs, withCommonArgs, workspaceArgs } from "../../../args"; +import { commonArgs, jsonArgs, positiveIntArg, withCommonArgs, workspaceArgs } from "../../../args"; import { initOperatorClient } from "../../../client"; import { loadAccessToken, loadWorkspaceId } from "../../../context"; import { humanizeRelativeTime } from "../../../utils/format"; @@ -79,34 +79,25 @@ export async function listFunctionRegistries( return results; } +// oxlint-disable-next-line jsdoc/check-tag-names +/** @lintignore */ export const listCommand = defineCommand({ - meta: { - name: "list", - description: "List function registries in a workspace", - }, - args: { + name: "list", + description: "List function registries in a workspace", + args: z.object({ ...commonArgs, ...jsonArgs, ...workspaceArgs, - limit: { - type: "string", + limit: arg(positiveIntArg.optional(), { alias: "l", description: "Maximum number of functions to list", - }, - }, + }), + }), run: withCommonArgs(async (args) => { - let limit: number | undefined; - if (args.limit) { - limit = parseInt(args.limit, 10); - if (Number.isNaN(limit) || limit <= 0) { - throw new Error(`--limit must be a positive integer, got '${args.limit}'`); - } - } - const functions = await listFunctionRegistries({ workspaceId: args["workspace-id"], profile: args.profile, - limit, + limit: args.limit, }); const formatted = args.json diff --git a/packages/sdk/src/cli/workspace/index.ts b/packages/sdk/src/cli/workspace/index.ts index 3b335b50a..4e140c7e4 100644 --- a/packages/sdk/src/cli/workspace/index.ts +++ b/packages/sdk/src/cli/workspace/index.ts @@ -3,7 +3,7 @@ import { appCommand } from "./app"; import { createCommand } from "./create"; import { deleteCommand } from "./delete"; import { describeCommand } from "./describe"; -import { functionCommand } from "./function"; +// import { functionCommand } from "./function"; import { listCommand } from "./list"; import { restoreCommand } from "./restore"; import { userCommand } from "./user"; @@ -16,7 +16,7 @@ export const workspaceCommand = defineCommand({ create: createCommand, delete: deleteCommand, describe: describeCommand, - function: functionCommand, + // function: functionCommand, list: listCommand, restore: restoreCommand, user: userCommand,