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
1 change: 1 addition & 0 deletions apps/desktop/src/main/lib/plugin-installs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async function runPluginCli(args: string[]): Promise<void> {
try {
await execFileAsync(cli, ["plugins", ...args, "--json"], {
timeout: 60_000,
env: { ...process.env, SUPERSET_CLI_AUDIENCE: "internal" },
});
} catch (error) {
log.warn(
Expand Down
1 change: 1 addition & 0 deletions packages/cli-framework/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ await run({
tree: { commands, groups, middleware },
globals: config.globals,
help: config.help,
audiences: config.audiences?.(),
});
`,
);
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-framework/src/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { GenericBuilderInternals, TypeOf } from "./option";

export type Audience = "internal" | "public";

export type CommandResult =
| { data?: unknown; message?: string }
| unknown[]
Expand All @@ -19,6 +21,7 @@ export type CommandConfig<
> = {
description: string;
aliases?: string[];
audience?: Audience;
skipMiddleware?: TSkip;
options?: TOpts;
args?: TArgs;
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-framework/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dirname, resolve } from "node:path";
import type { BunPlugin } from "bun";
import type { Audience } from "./command";
import type { HelpBranding } from "./help";
import type { GenericBuilderInternals } from "./option";

Expand All @@ -18,6 +19,8 @@ export interface CliConfig {
globals?: Record<string, GenericBuilderInternals>;
/** Branding/curation for the root help screen and interactive browser. */
help?: HelpBranding;
/** Audiences whose commands exist for this run. Defaults to `["public"]`. */
audiences?: () => Audience[];
}

export function defineConfig(config: CliConfig): CliConfig {
Expand Down
1 change: 1 addition & 0 deletions packages/cli-framework/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ export async function runDev(argv: string[]): Promise<void> {
tree: { commands, groups, middleware },
globals: config.globals,
help: config.help,
audiences: config.audiences?.(),
});
}
4 changes: 2 additions & 2 deletions packages/cli-framework/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { CommandConfig, CommandResult } from "./command";
export type { Audience, CommandConfig, CommandResult } from "./command";
export { createCommand } from "./command";
export type { CliConfig } from "./config";
export { defineConfig } from "./config";
Expand All @@ -22,6 +22,6 @@ export { camelToKebab, isAgentMode, parseArgv } from "./parser";
export type { CommandsPluginOptions } from "./plugin";
export { createCommandsPlugin } from "./plugin";
export type { CliCommand, CliGroup } from "./router";
export { buildTree, routeCommand } from "./router";
export { buildTree, filterByAudience, routeCommand } from "./router";
export type { CommandTree, RunOptions } from "./runner";
export { run } from "./runner";
60 changes: 60 additions & 0 deletions packages/cli-framework/src/router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "bun:test";
import type { CommandConfig } from "./command";
import { type CliCommand, type CliGroup, filterByAudience } from "./router";

function cmd(path: string[], audience?: CommandConfig["audience"]): CliCommand {
return {
path,
command: {
description: path.join(" "),
audience,
run: async () => undefined,
},
};
}

const paths = (items: { path: string[] }[]) =>
items.map((item) => item.path.join(" "));

describe("filterByAudience", () => {
const groups: CliGroup[] = [
{ path: ["plugins"], description: "Plugins", audience: "internal" },
{
path: ["plugins", "author"],
description: "Authoring",
audience: "public",
},
{ path: ["tasks"], description: "Tasks" },
{ path: ["lab"], description: "Lab" },
];
const commands = [
cmd(["plugins", "list"]),
cmd(["plugins", "search"], "public"),
cmd(["plugins", "author", "init"]),
cmd(["tasks", "list"]),
cmd(["tasks", "purge"], "internal"),
cmd(["tasks", "purge", "all"]),
cmd(["lab", "try"], "internal"),
];
const publicView = filterByAudience(groups, commands, ["public"]);

it("shows only commands with no internal node in their chain", () => {
expect(paths(publicView.commands)).toEqual(["tasks list"]);
expect(paths(publicView.groups)).toEqual(["tasks"]);
});

it("keeps everything under an internal group internal, even when tagged public", () => {
expect(paths(publicView.commands)).not.toContain("plugins search");
expect(paths(publicView.commands)).not.toContain("plugins author init");
});

it("hides commands nested under an internal command", () => {
expect(paths(publicView.commands)).not.toContain("tasks purge all");
});

it("keeps everything when internal is enabled", () => {
const result = filterByAudience(groups, commands, ["internal", "public"]);
expect(paths(result.commands)).toEqual(paths(commands));
expect(result.groups).toEqual(groups);
});
});
27 changes: 26 additions & 1 deletion packages/cli-framework/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import type { CommandConfig } from "./command";
import type { Audience, CommandConfig } from "./command";
import { CLIError, suggestSimilar } from "./errors";
import type { CommandNode } from "./help";

export type CliGroup = {
path: string[];
description: string;
aliases?: string[];
audience?: Audience;
};

export type CliCommand = {
path: string[];
command: CommandConfig;
};

function isPrefix(prefix: string[], path: string[]): boolean {
return prefix.every((segment, i) => path[i] === segment);
}

export function filterByAudience(
groups: CliGroup[],
commands: CliCommand[],
audiences: Audience[],
): { groups: CliGroup[]; commands: CliCommand[] } {
const hiddenPaths = [
...groups.map((g) => ({ path: g.path, audience: g.audience })),
...commands.map((c) => ({ path: c.path, audience: c.command.audience })),
]
.filter((node) => !audiences.includes(node.audience ?? "public"))
.map((node) => node.path);
const visibleCommands = commands.filter(
(c) => !hiddenPaths.some((hidden) => isPrefix(hidden, c.path)),
);
const visibleGroups = groups.filter((g) =>
visibleCommands.some((c) => isPrefix(g.path, c.path)),
);
return { groups: visibleGroups, commands: visibleCommands };
}

export function buildTree(
groups: CliGroup[],
commands: CliCommand[],
Expand Down
11 changes: 9 additions & 2 deletions packages/cli-framework/src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CommandConfig } from "./command";
import type { Audience, CommandConfig } from "./command";
import { CLIError } from "./errors";
import {
generateCommandHelp,
Expand All @@ -15,6 +15,7 @@ import {
buildTree,
type CliCommand,
type CliGroup,
filterByAudience,
routeCommand,
} from "./router";

Expand All @@ -30,6 +31,7 @@ export interface RunOptions {
tree: CommandTree;
globals?: Record<string, GenericBuilderInternals>;
help?: HelpBranding;
audiences?: Audience[];
}

export async function run(opts: RunOptions): Promise<void> {
Expand Down Expand Up @@ -241,7 +243,12 @@ async function execute(
const { name, version } = opts;
const { middleware } = loaded;
const globalConfigs = processGlobals(opts.globals);
const { root, commandMap } = buildTree(loaded.groups, loaded.commands);
const visible = filterByAudience(
loaded.groups,
loaded.commands,
opts.audiences ?? ["public"],
);
const { root, commandMap } = buildTree(visible.groups, visible.commands);

// EXPERIMENT: bare invocation on a TTY opens the interactive help browser
// instead of dumping static help. Agents/CI keep the static output.
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/cli.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default defineConfig({
.env("SUPERSET_API_KEY")
.desc("Use a Superset API key (sk_live_…) instead of OAuth login"),
},
audiences: () =>
process.env.SUPERSET_CLI_AUDIENCE === "internal"
? ["internal", "public"]
: ["public"],
help: {
tagline: "Command your fleet of coding agents from any shell.",
docsUrl: "https://docs.superset.sh/cli",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/environments/meta.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export default {
audience: "internal",
description: "Inspect the environments cloud workspaces start from",
};
1 change: 1 addition & 0 deletions packages/cli/src/commands/mcp/meta.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export default {
audience: "internal",
description: "Inspect and call the tools your connected plugins expose",
};
1 change: 1 addition & 0 deletions packages/cli/src/commands/plugins/meta.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
audience: "internal",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
description:
"Install and connect plugins, and author them in a marketplace repo",
aliases: ["plugin"],
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/skills/meta.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export default {
audience: "internal",
description: "Inspect the skills installed from plugins",
};
Loading