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
9 changes: 6 additions & 3 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"node": ">=16"
},
"scripts": {
"build": "tsc",
"build": "bun build src/cli.tsx --outfile=dist/cli.js --target=node --packages=bundle",
"dev": "tsc --watch",
"start": "bun server.ts",
"start": "bun src/server.ts",
"typecheck": "tsc --noEmit",
"test": "prettier --check . && xo && ava",
"lint": "biome check --write .",
Expand All @@ -22,7 +22,10 @@
"dist"
],
"dependencies": {
"commander": "^14.0.1",
"ink": "^6.5.0",
"ink-select-input": "^6.2.0",
"ink-table": "^3.1.0",
"lowdb": "^7.0.1",
"meow": "^11.0.0",
"react": "^19.1.1"
Expand All @@ -32,7 +35,7 @@
"@types/react": "^19.1.11",
"@vdemedes/prettier-config": "^2.0.1",
"ava": "^5.2.0",
"bun-types": "^1.3.2",
"bun-types": "^1.3.1",
"chalk": "^5.6.2",
"chokidar": "^3.5.3",
"eslint-config-xo-react": "^0.27.0",
Expand Down
282 changes: 258 additions & 24 deletions apps/cli/src/cli.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,263 @@
#!/usr/bin/env node
import { Command } from "commander";
import { render } from "ink";
import meow from "meow";
import React from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused React import.

The React import is not needed as the JSX transform handles it automatically.

Apply this diff:

-import React from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React from "react";
🧰 Tools
🪛 GitHub Actions: CI

[warning] 4-4: lint/correctness/noUnusedImports: Unused import 'React'.

🤖 Prompt for AI Agents
In apps/cli/src/cli.tsx at line 4, remove the unused import "import React from
\"react\";" since the project uses the automatic JSX transform; delete that
import line and ensure no other references to React remain in the file.

import App from "./app.js";

const cli = meow(
`
Usage
$ cli

Options
--name Your name

Examples
$ cli --name=Jane
Hello, Jane
`,
{
importMeta: import.meta,
flags: {
name: {
type: "string",
},
import {
AgentCreate,
AgentDelete,
AgentGet,
AgentList,
AgentStop,
AgentStopAll,
ChangeCreate,
ChangeDelete,
ChangeList,
EnvCreate,
EnvDelete,
EnvGet,
EnvList,
WorkspaceCreate,
WorkspaceDelete,
WorkspaceGet,
WorkspaceList,
} from "./commands/index";
import { AgentType, ProcessType } from "./types/process";
import { WorkspaceType } from "./types/workspace";

const program = new Command();

program
.name("superset")
.description(
"Superset CLI - Manage environments, workspaces, agents, and changes",
)
.version("0.1.0");

// Environment commands
const env = program.command("env").description("Manage environments");

env
.command("list")
.description("List all environments")
.action(() => {
render(<EnvList onComplete={() => process.exit(0)} />);
});

env
.command("get")
.description("Get environment by ID")
.argument("<id>", "Environment ID")
.action((id: string) => {
render(<EnvGet id={id} onComplete={() => process.exit(0)} />);
});

env
.command("create")
.description("Create a new environment")
.action(() => {
render(<EnvCreate onComplete={() => process.exit(0)} />);
});

env
.command("delete")
.description(
"Delete an environment (cascades to workspaces, processes, changes)",
)
.argument("<id>", "Environment ID")
.action((id: string) => {
render(<EnvDelete id={id} onComplete={() => process.exit(0)} />);
});
Comment on lines +36 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Exit code is always 0 even when commands encounter runtime errors.

All env commands render components with onComplete={() => process.exit(0)}, and the corresponding components (EnvList, EnvGet, etc. in apps/cli/src/commands/env.tsx) always call onComplete in a finally block, regardless of whether an error occurred. That means the CLI will typically exit with code 0 even if the orchestrator throws and the user sees an Error: message.

For automation/scripts, it’s usually preferable to return a non‑zero exit code when the command fails. Consider threading a success/error flag through onComplete (or similar) so the CLI can process.exit(1) on failures.

🤖 Prompt for AI Agents
In apps/cli/src/cli.tsx around lines 36 to 69, the env subcommands always call
onComplete with process.exit(0) which forces a zero exit code even when the
underlying component encountered an error; change the pattern so onComplete
accepts a boolean or numeric status (e.g., onComplete(success: boolean)) and
propagate success/failure from the Env* components (they should call
onComplete(false) on error and onComplete(true) on success), then update each
CLI command to call process.exit(0) on success and process.exit(1) on failure
based on that status.


// Workspace commands
const workspace = program.command("workspace").description("Manage workspaces");

workspace
.command("list")
.description("List all workspaces")
.option("--env <environmentId>", "Filter by environment ID")
.action((options: { env?: string }) => {
render(
<WorkspaceList
environmentId={options.env}
onComplete={() => process.exit(0)}
/>,
);
});

workspace
.command("get")
.description("Get workspace by ID")
.argument("<id>", "Workspace ID")
.action((id: string) => {
render(<WorkspaceGet id={id} onComplete={() => process.exit(0)} />);
});

workspace
.command("create")
.description("Create a new workspace")
.argument("<environmentId>", "Environment ID")
.argument(
"<type>",
`Workspace type (${Object.values(WorkspaceType).join(", ")})`,
)
.option("--path <path>", "Path for local workspace")
.action((environmentId: string, type: string, options: { path?: string }) => {
// Validate workspace type
if (!Object.values(WorkspaceType).includes(type as WorkspaceType)) {
console.error(
`Invalid workspace type: ${type}. Must be one of: ${Object.values(WorkspaceType).join(", ")}`,
);
process.exit(1);
}

render(
<WorkspaceCreate
environmentId={environmentId}
type={type as WorkspaceType}
path={options.path}
onComplete={() => process.exit(0)}
/>,
);
});

workspace
.command("delete")
.description("Delete a workspace (cascades to processes and changes)")
.argument("<id>", "Workspace ID")
.action((id: string) => {
render(<WorkspaceDelete id={id} onComplete={() => process.exit(0)} />);
});

// Agent/Process commands
const agent = program
.command("agent")
.description("Manage agents and processes");

agent
.command("list")
.description("List all agents/processes")
.option("--workspace <workspaceId>", "Filter by workspace ID")
.action((options: { workspace?: string }) => {
render(
<AgentList
workspaceId={options.workspace}
onComplete={() => process.exit(0)}
/>,
);
});

agent
.command("get")
.description("Get agent/process by ID")
.argument("<id>", "Agent/Process ID")
.action((id: string) => {
render(<AgentGet id={id} onComplete={() => process.exit(0)} />);
});

agent
.command("create")
.description("Create a new agent/process")
.argument("<workspaceId>", "Workspace ID")
.argument("<type>", `Process type (${Object.values(ProcessType).join(", ")})`)
.option(
"--agent-type <agentType>",
`Agent type (${Object.values(AgentType).join(", ")}) - required if type is 'agent'`,
)
.action(
(workspaceId: string, type: string, options: { agentType?: string }) => {
// Validate process type
if (!Object.values(ProcessType).includes(type as ProcessType)) {
console.error(
`Invalid process type: ${type}. Must be one of: ${Object.values(ProcessType).join(", ")}`,
);
process.exit(1);
}

// Validate agent type if provided
let agentType: AgentType | undefined;
if (options.agentType) {
if (
!Object.values(AgentType).includes(options.agentType as AgentType)
) {
console.error(
`Invalid agent type: ${options.agentType}. Must be one of: ${Object.values(AgentType).join(", ")}`,
);
process.exit(1);
}
agentType = options.agentType as AgentType;
}

render(
<AgentCreate
workspaceId={workspaceId}
type={type as ProcessType}
agentType={agentType}
onComplete={() => process.exit(0)}
/>,
);
},
},
);
);
Comment on lines +157 to +199
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce --agent-type when creating an agent process.

The help text says --agent-type is “required if type is 'agent', but the implementation only validates agentTypewhen it is provided and otherwise proceeds withagentTypeasundefined. This can lead to confusing behavior or downstream errors in ProcessOrchestrator.create`.

You can enforce the requirement directly in this action handler, e.g.:

   // Validate process type
   if (!Object.values(ProcessType).includes(type as ProcessType)) {
     console.error(
       `Invalid process type: ${type}. Must be one of: ${Object.values(ProcessType).join(", ")}`,
     );
     process.exit(1);
   }

+  // If creating an agent process, require an explicit agent type
+  if (type === ProcessType.AGENT && !options.agentType) {
+    console.error(
+      "Missing required option: --agent-type <agentType> when type is 'agent'.",
+    );
+    process.exit(1);
+  }
+
   // Validate agent type if provided
   let agentType: AgentType | undefined;

(Adjust the ProcessType.AGENT member name to match your actual enum.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
agent
.command("create")
.description("Create a new agent/process")
.argument("<workspaceId>", "Workspace ID")
.argument("<type>", `Process type (${Object.values(ProcessType).join(", ")})`)
.option(
"--agent-type <agentType>",
`Agent type (${Object.values(AgentType).join(", ")}) - required if type is 'agent'`,
)
.action(
(workspaceId: string, type: string, options: { agentType?: string }) => {
// Validate process type
if (!Object.values(ProcessType).includes(type as ProcessType)) {
console.error(
`Invalid process type: ${type}. Must be one of: ${Object.values(ProcessType).join(", ")}`,
);
process.exit(1);
}
// Validate agent type if provided
let agentType: AgentType | undefined;
if (options.agentType) {
if (
!Object.values(AgentType).includes(options.agentType as AgentType)
) {
console.error(
`Invalid agent type: ${options.agentType}. Must be one of: ${Object.values(AgentType).join(", ")}`,
);
process.exit(1);
}
agentType = options.agentType as AgentType;
}
render(
<AgentCreate
workspaceId={workspaceId}
type={type as ProcessType}
agentType={agentType}
onComplete={() => process.exit(0)}
/>,
);
},
},
);
);
agent
.command("create")
.description("Create a new agent/process")
.argument("<workspaceId>", "Workspace ID")
.argument("<type>", `Process type (${Object.values(ProcessType).join(", ")})`)
.option(
"--agent-type <agentType>",
`Agent type (${Object.values(AgentType).join(", ")}) - required if type is 'agent'`,
)
.action(
(workspaceId: string, type: string, options: { agentType?: string }) => {
// Validate process type
if (!Object.values(ProcessType).includes(type as ProcessType)) {
console.error(
`Invalid process type: ${type}. Must be one of: ${Object.values(ProcessType).join(", ")}`,
);
process.exit(1);
}
// If creating an agent process, require an explicit agent type
if (type === ProcessType.AGENT && !options.agentType) {
console.error(
"Missing required option: --agent-type <agentType> when type is 'agent'.",
);
process.exit(1);
}
// Validate agent type if provided
let agentType: AgentType | undefined;
if (options.agentType) {
if (
!Object.values(AgentType).includes(options.agentType as AgentType)
) {
console.error(
`Invalid agent type: ${options.agentType}. Must be one of: ${Object.values(AgentType).join(", ")}`,
);
process.exit(1);
}
agentType = options.agentType as AgentType;
}
render(
<AgentCreate
workspaceId={workspaceId}
type={type as ProcessType}
agentType={agentType}
onComplete={() => process.exit(0)}
/>,
);
},
);
🤖 Prompt for AI Agents
In apps/cli/src/cli.tsx around lines 157 to 199, the action handler does not
enforce that --agent-type is provided when the selected process type is the
agent variant; update the handler to check if type === ProcessType.AGENT (or the
correct enum member) and if so, ensure options.agentType is present — if
missing, print a clear error listing valid AgentType values and exit(1); keep
the existing validation for provided agentType and then set agentType
accordingly before calling render.


agent
.command("stop")
.description("Stop an agent/process")
.argument("<id>", "Agent/Process ID")
.action((id: string) => {
render(<AgentStop id={id} onComplete={() => process.exit(0)} />);
});

agent
.command("stop-all")
.description("Stop all agents/processes")
.action(() => {
render(<AgentStopAll onComplete={() => process.exit(0)} />);
});

agent
.command("delete")
.description("Delete an agent/process (cascades to agent summaries)")
.argument("<id>", "Agent/Process ID")
.action((id: string) => {
render(<AgentDelete id={id} onComplete={() => process.exit(0)} />);
});

// Change commands
const change = program.command("change").description("Manage changes");

change
.command("list")
.description("List changes for a workspace")
.argument("<workspaceId>", "Workspace ID")
.action((workspaceId: string) => {
render(
<ChangeList
workspaceId={workspaceId}
onComplete={() => process.exit(0)}
/>,
);
});

change
.command("create")
.description("Create a new change")
.argument("<workspaceId>", "Workspace ID")
.argument("<summary>", "Change summary")
.action((workspaceId: string, summary: string) => {
render(
<ChangeCreate
workspaceId={workspaceId}
summary={summary}
onComplete={() => process.exit(0)}
/>,
);
});

change
.command("delete")
.description("Delete a change (cascades to file diffs)")
.argument("<id>", "Change ID")
.action((id: string) => {
render(<ChangeDelete id={id} onComplete={() => process.exit(0)} />);
});

render(<App name={cli.flags.name} />);
program.parse();
Loading
Loading