Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,21 +691,38 @@ Validate input data and run one connector action.
existing unsupported errors because the self-hosted runtime does not expose
the async lifecycle contract.

### `oo connector apps <serviceName>`
### `oo connector apps [serviceName]`

List connected connector apps for one service. This command is read-only.
List connected connector apps under the effective identity. This command is
read-only.

- Arguments: `<serviceName>` is the service name.
- Arguments: `[serviceName]` is optional. When omitted, the command lists every
connected app across all providers. When provided, the listing is scoped to
that one service.
- Options: `--organization <name>` lists connected apps under the given
organization identity instead of your personal identity. `--org <name>` is an
alias for `--organization <name>`. When omitted, the listing uses the
`identity.organization` config default if set, otherwise your personal
identity.
- Options: `--personal` lists connected apps under your personal identity and
ignores any configured default organization. It cannot be combined with
`--organization`.
- Options: `--format=json` and `--json` print a JSON array.
- Output: JSON entries include the stable CLI fields `service`,
`connectionName`, `displayName`, `accountLabel`, `status`, `authType`,
`isDefault`, and `scopes`. App id fields are not included.
- Output: when an app has no connection name, JSON output uses `null` and text
output prints `-`.
- Output: text output prints one tab-separated row per app with connection
name, name, status, auth type, and default marker.
- Output: text output prints one column-aligned row per app. The listing across
all providers leads with a `Service` column; the single-service listing omits
it because the service is fixed by the argument. On a color-capable terminal
the status and default columns are color-coded; piped or `NO_COLOR` output is
plain aligned text.
- Notes: use the listed `connectionName` value with
`oo connector run <serviceName> --connection-name <connection-name>`.
- Notes: against a self-hosted connector, `--organization` is rejected with exit
`2`, a configured `identity.organization` default is ignored, and `--personal`
is accepted.

### `oo connector proxy <serviceName>`

Expand Down
18 changes: 14 additions & 4 deletions docs/commands.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,19 +591,29 @@ CLI 默认记录受隐私约束的命令使用 telemetry。事件不包含 free-
由于自部署 runtime 不提供异步 lifecycle contract,`--wait` 和
`--wait-result` 会以现有的“不支持”错误失败。

### `oo connector apps <serviceName>`
### `oo connector apps [serviceName]`

列出一个服务下已连接的 connector app。该命令只读。
按当前生效身份列出已连接的 connector app。该命令只读。

- 参数:`<serviceName>` 为服务名。
- 参数:`[serviceName]` 可选。省略时列出所有 provider 下已连接的 app;提供时仅列出
该服务的 app。
- 选项:`--organization <name>` 以指定组织身份列出已连接的 app,而非个人身份。
`--org <name>` 是 `--organization <name>` 的别名。省略时,若配置了
`identity.organization` 默认值则按该组织列出,否则按个人身份列出。
- 选项:`--personal` 以个人身份列出已连接的 app,并忽略已配置的默认组织。该选项不能与
`--organization` 同时使用。
- 选项:`--format=json` 和 `--json` 会输出 JSON 数组。
- 输出:JSON 条目包含稳定 CLI 字段 `service`、`connectionName`、`displayName`、
`accountLabel`、`status`、`authType`、`isDefault` 和 `scopes`。不会包含
app id 字段。
- 输出:当 app 没有连接名称时,JSON 输出使用 `null`,文本输出显示 `-`。
- 输出:文本输出每个 app 一行,以 tab 分隔连接名称、名称、状态、认证类型和默认标记。
- 输出:文本输出每个 app 一行,列对齐。跨全部 provider 的列表以 `Service` 列开头;
单服务列表则省略该列,因为服务已由参数固定。在支持颜色的终端上,状态列与默认列会
着色;管道输出或 `NO_COLOR` 下为纯对齐文本。
- 说明:可将列出的 `connectionName` 值传给
`oo connector run <serviceName> --connection-name <connection-name>`。
- 说明:对自部署 Connector,`--organization` 会以退出码 `2` 拒绝,已配置的
`identity.organization` 默认值会被忽略,`--personal` 可正常使用。

### `oo connector proxy <serviceName>`

Expand Down
131 changes: 131 additions & 0 deletions src/application/commands/connector/apps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, expect, test } from "bun:test";

import { createTerminalColors } from "../../terminal-colors.ts";
import { formatConnectorAppsAsText } from "./apps.ts";

type ConnectorAppRow = Parameters<typeof formatConnectorAppsAsText>[0][number];

const greenOpenCode = "";
const yellowOpenCode = "";
const redOpenCode = "";

describe("formatConnectorAppsAsText", () => {
test("color-codes an active status, the service, and the default marker", () => {
const output = formatConnectorAppsAsText(
[sampleApp({ isDefault: true, service: "gmail", status: "active" })],
"all",
createTranslatorStub(),
createTerminalColors(true),
);

expect(output).toContain("[");
expect(output).toContain(greenOpenCode);
expect(output).toContain("✓");
expect(output).toContain("gmail");
});

test("colors a reauth-required status yellow and an error status red", () => {
const colors = createTerminalColors(true);
const reauth = formatConnectorAppsAsText(
[sampleApp({ status: "reauth_required" })],
"service",
createTranslatorStub(),
colors,
);
const errored = formatConnectorAppsAsText(
[sampleApp({ status: "error" })],
"service",
createTranslatorStub(),
colors,
);

expect(reauth).toContain(yellowOpenCode);
expect(errored).toContain(redOpenCode);
});

test("aligns columns as plain text without escape sequences when colors are disabled", () => {
const output = formatConnectorAppsAsText(
[
sampleApp({ displayName: "Work Gmail", service: "gmail" }),
sampleApp({
connectionName: null,
displayName: "Linear",
isDefault: false,
service: "x",
}),
],
"all",
createTranslatorStub(),
createTerminalColors(false),
);
const lines = output.split("\n");

expect(output).not.toContain("[");
// Different service-name widths are padded to a shared column width, so
// the following column lines up across rows.
expect(lines[1]!.indexOf("Work Gmail")).toBe(lines[2]!.indexOf("Linear"));
// A missing connection name and a non-default app both render a dash.
expect(output).toContain("-");
});

test("pads wide CJK names by display width so later columns stay aligned", () => {
const output = formatConnectorAppsAsText(
[
// "钉钉" is 2 UTF-16 units but 4 display columns; "AB" is 2 of
// each. Padding by display width keeps the status column aligned.
sampleApp({ displayName: "钉钉", service: "dingtalk", status: "active" }),
sampleApp({ displayName: "AB", service: "notion", status: "active" }),
],
"all",
createTranslatorStub(),
createTerminalColors(false),
);
const lines = output.split("\n");
const displayWidthBeforeStatus = (line: string): number =>
Bun.stringWidth(line.slice(0, line.indexOf("active")));

expect(displayWidthBeforeStatus(lines[1]!)).toBe(
displayWidthBeforeStatus(lines[2]!),
);
});

test("returns the no-connections message for an empty all-scope listing", () => {
const output = formatConnectorAppsAsText(
[],
"all",
createTranslatorStub(),
createTerminalColors(false),
);

expect(output).toBe("connector.apps.text.noConnections");
});

test("returns the per-service no-results message for an empty service-scope listing", () => {
const output = formatConnectorAppsAsText(
[],
"service",
createTranslatorStub(),
createTerminalColors(false),
);

expect(output).toBe("connector.apps.text.noResults");
});
});

function sampleApp(overrides: Partial<ConnectorAppRow> = {}): ConnectorAppRow {
return {
accountLabel: "user@example.com",
authType: "oauth2",
connectionName: "work",
displayName: "Work Gmail",
isDefault: true,
scopes: [],
service: "gmail",
status: "active",
...overrides,
};
}

function createTranslatorStub(): { t: (key: string) => string } {
return { t: key => key };
}
Loading