Skip to content
Draft
238 changes: 233 additions & 5 deletions packages/sdk/docs/cli/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

<!-- politty:command:workspace:end -->
<!-- politty:command:workspace create:start -->
Expand Down Expand Up @@ -203,3 +207,227 @@ tailor-sdk profile delete [options] <name>
| `name` | Profile name | Yes |

<!-- politty:command:profile delete:end -->

<!-- politty:command:workspace app:start -->

### 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 |

<!-- politty:command:workspace app:end -->

<!-- politty:command:workspace app health:start -->

#### 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 <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--profile <PROFILE>` | `-p` | Workspace profile | - |
| `--name <NAME>` | `-n` | Application name | - |

<!-- politty:command:workspace app health:end -->

<!-- politty:command:workspace app list:start -->

#### 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 <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--profile <PROFILE>` | `-p` | Workspace profile | - |
| `--limit <LIMIT>` | `-l` | Maximum number of applications to list | - |

<!-- politty:command:workspace app list:end -->

<!-- politty:command:workspace describe:start -->

### 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 <WORKSPACE_ID>` | `-w` | Workspace ID | - |

<!-- politty:command:workspace describe:end -->

<!-- politty:command:workspace restore:start -->

### workspace restore

Restore a deleted workspace

**Usage**

```
tailor-sdk workspace restore [options]
```

**Options**

| Option | Alias | Description | Default |
| ------------------------------- | ----- | ------------------------- | ------- |
| `--workspace-id <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--yes` | `-y` | Skip confirmation prompts | `false` |

<!-- politty:command:workspace restore:end -->

<!-- politty:command:workspace user:start -->

### 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 |

<!-- politty:command:workspace user:end -->

<!-- politty:command:workspace user invite:start -->

#### workspace user invite

Invite a user to a workspace

**Usage**

```
tailor-sdk workspace user invite [options]
```

**Options**

| Option | Alias | Description | Default |
| ------------------------------- | ----- | -------------------------------------- | ------- |
| `--workspace-id <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--profile <PROFILE>` | `-p` | Workspace profile | - |
| `--email <EMAIL>` | - | Email address of the user to invite | - |
| `--role <ROLE>` | `-r` | Role to assign (admin, editor, viewer) | - |

<!-- politty:command:workspace user invite:end -->

<!-- politty:command:workspace user list:start -->

#### 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 <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--profile <PROFILE>` | `-p` | Workspace profile | - |
| `--limit <LIMIT>` | `-l` | Maximum number of users to list | - |

<!-- politty:command:workspace user list:end -->

<!-- politty:command:workspace user remove:start -->

#### workspace user remove

Remove a user from a workspace

**Usage**

```
tailor-sdk workspace user remove [options]
```

**Options**

| Option | Alias | Description | Default |
| ------------------------------- | ----- | ----------------------------------- | ------- |
| `--workspace-id <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--profile <PROFILE>` | `-p` | Workspace profile | - |
| `--email <EMAIL>` | - | Email address of the user to remove | - |
| `--yes` | `-y` | Skip confirmation prompts | `false` |

<!-- politty:command:workspace user remove:end -->

<!-- politty:command:workspace user update:start -->

#### 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 <WORKSPACE_ID>` | `-w` | Workspace ID | - |
| `--profile <PROFILE>` | `-p` | Workspace profile | - |
| `--email <EMAIL>` | - | Email address of the user to update | - |
| `--role <ROLE>` | `-r` | New role to assign (admin, editor, viewer) | - |

<!-- politty:command:workspace user update:end -->
4 changes: 4 additions & 0 deletions packages/sdk/knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion packages/sdk/src/cli/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,19 @@ 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 { getFunctionRegistry } from "./workspace/function/registry/get";
export { listFunctionRegistries } from "./workspace/function/registry/list";
export {
listMachineUsers,
type ListMachineUsersOptions,
Expand Down
18 changes: 18 additions & 0 deletions packages/sdk/src/cli/utils/format.ts
Original file line number Diff line number Diff line change
@@ -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 Date object or null if invalid
*/
export function formatTimestamp(timestamp: Timestamp | undefined): Date | null {
if (!timestamp) {
return null;
}
const date = timestampDate(timestamp);
if (Number.isNaN(date.getTime())) {
return null;
}
return date;
}

/**
* Formats a table with consistent single-line border style.
* Use this instead of importing `table` directly.
Expand Down
83 changes: 83 additions & 0 deletions packages/sdk/src/cli/workspace/app/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { arg, defineCommand } from "politty";
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<typeof healthOptionsSchema>;

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<AppHealthInfo> {
const { client, workspaceId, name } = await loadOptions(options);

const response = await client.getApplicationSchemaHealth({
workspaceId,
applicationName: name,
});

return appHealthInfo(name, response);
}

export const healthCommand = defineCommand({
name: "health",
description: "Check application schema health",
args: z.object({
...commonArgs,
...jsonArgs,
...workspaceArgs,
name: arg(z.string(), {
description: "Application name",
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);
}),
});
Loading
Loading