Skip to content
Open
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
40 changes: 39 additions & 1 deletion packages/cli/__tests__/commands/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,43 @@ describe("start command — autoCreateConfig", () => {
expect(config.projects[projectIds[0]!]!.path).toBe(realpathSync(tmpDir));
});

it("sanitizes auto-created project IDs from directory names", async () => {
const { detectEnvironment } = await import("../../src/lib/detect-env.js");
vi.mocked(detectEnvironment).mockResolvedValue({
isGitRepo: true,
gitRemote: "https://github.com/ggerganov/llama.cpp.git",
ownerRepo: "ggerganov/llama.cpp",
currentBranch: "master",
defaultBranch: "master",
hasTmux: true,
hasGh: false,
ghAuthed: false,
hasLinearKey: false,
hasSlackWebhook: false,
});

const { detectAvailableAgents, detectAgentRuntime } =
await import("../../src/lib/detect-agent.js");
vi.mocked(detectAvailableAgents).mockResolvedValue([]);
vi.mocked(detectAgentRuntime).mockResolvedValue("claude-code");

const repoDir = join(tmpDir, "llama.cpp");
mkdirSync(repoDir);
mockProcessCwd.mockReturnValue(repoDir);

const callerContext = await import("../../src/lib/caller-context.js");
vi.spyOn(callerContext, "isHumanCaller").mockReturnValue(false);

const config = await autoCreateConfig(repoDir);

const projectIds = Object.keys(config.projects);
expect(projectIds).toHaveLength(1);
expect(projectIds[0]).toMatch(/^llama-cpp_[0-9a-f]{10}$/);
const projectId = projectIds[0]!;
expect(config.projects[projectId]?.name).toBe("llama.cpp");
expect(config.projects[projectId]?.path).toBe(realpathSync(repoDir));
});

it("removes the flat local config when global registration fails", async () => {
const { detectEnvironment } = await import("../../src/lib/detect-env.js");
vi.mocked(detectEnvironment).mockResolvedValue({
Expand Down Expand Up @@ -2458,11 +2495,12 @@ describe("start command — autoCreateConfig", () => {
const callerContext = await import("../../src/lib/caller-context.js");
vi.spyOn(callerContext, "isHumanCaller").mockReturnValue(false);

const registeredProjectId = generateExternalId(realpathSync(tmpDir), null);
writeFileSync(
process.env["AO_GLOBAL_CONFIG"]!,
[
"projects:",
` ${basename(tmpDir)}:`,
` ${registeredProjectId}:`,
` path: ${join(tmpDir, "other-repo")}`,
"",
].join("\n"),
Expand Down
18 changes: 11 additions & 7 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
recordActivityEvent,
registerProjectInGlobalConfig,
getGlobalConfigPath,
sanitizeProjectId,
type OrchestratorConfig,
type LocalProjectConfig,
type ProjectConfig,
Expand Down Expand Up @@ -148,7 +149,8 @@ function writeProjectBehaviorConfig(projectPath: string, config: LocalProjectCon
*/
async function registerFlatConfig(configPath: string): Promise<string | null> {
const projectPath = resolve(dirname(configPath));
const projectId = basename(projectPath);
const rawProjectId = basename(projectPath);
const projectId = sanitizeProjectId(rawProjectId) || "project";

// Read flat config fields
const raw = readFileSync(configPath, "utf-8");
Expand All @@ -169,7 +171,7 @@ async function registerFlatConfig(configPath: string): Promise<string | null> {

console.log(chalk.dim(`\n Registering project "${projectId}" in global config...\n`));

const registeredProjectId = registerProjectInGlobalConfig(projectId, projectId, projectPath, {
const registeredProjectId = registerProjectInGlobalConfig(projectId, rawProjectId, projectPath, {
defaultBranch,
sessionPrefix: prefix,
...(repo ? { repo } : {}),
Expand Down Expand Up @@ -537,7 +539,8 @@ export async function autoCreateConfig(workingDir: string): Promise<Orchestrator
const agentRules = generateRulesFromTemplates(projectType);

// Build config with smart defaults
const projectId = basename(workingDir);
const rawProjectId = basename(workingDir);
const projectId = sanitizeProjectId(rawProjectId) || "project";
let repo: string | undefined = env.ownerRepo ?? undefined;
const path = workingDir;
const defaultBranch = env.defaultBranch || "main";
Expand Down Expand Up @@ -582,7 +585,7 @@ export async function autoCreateConfig(workingDir: string): Promise<Orchestrator
writeLocalProjectConfig(workingDir, localConfig, outputPath);

try {
const registeredProjectId = registerProjectInGlobalConfig(projectId, projectId, path, {
const registeredProjectId = registerProjectInGlobalConfig(projectId, rawProjectId, path, {
...(repo ? { repo } : {}),
defaultBranch,
sessionPrefix: generateSessionPrefix(projectId),
Expand Down Expand Up @@ -662,7 +665,8 @@ async function addProjectToConfig(

await ensureGit("adding projects");

let projectId = basename(resolvedPath);
const rawProjectId = basename(resolvedPath);
let projectId = sanitizeProjectId(rawProjectId) || "project";

// Avoid overwriting an existing project with the same directory name
if (config.projects[projectId]) {
Expand Down Expand Up @@ -746,7 +750,7 @@ async function addProjectToConfig(
if (isCanonicalGlobalConfigPath(config.configPath)) {
const registeredProjectId = registerProjectInGlobalConfig(
projectId,
projectId,
rawProjectId,
resolvedPath,
{ defaultBranch, sessionPrefix: prefix },
config.configPath,
Expand All @@ -763,7 +767,7 @@ async function addProjectToConfig(
if (!rawConfig.projects) rawConfig.projects = {};

rawConfig.projects[projectId] = {
name: projectId,
name: rawProjectId,
...(ownerRepo ? { repo: ownerRepo } : {}),
path: resolvedPath,
defaultBranch,
Expand Down