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
169 changes: 150 additions & 19 deletions packages/cli/src/__tests__/create-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execSync } from "node:child_process";

Check warning on line 9 in packages/cli/src/__tests__/create-command.test.ts

View workflow job for this annotation

GitHub Actions / test

'execSync' is defined but never used. Allowed unused vars must match /^_/u

describe("create command", () => {
let testDir: string;
Expand Down Expand Up @@ -73,33 +73,164 @@
});

it("creates git-repo docset", async () => {
const cliPath = join(process.cwd(), "dist/index.js");
const cmd = `node ${cliPath} create --preset git-repo --id react-docs --name "React Docs" --url https://github.com/facebook/react.git`;
const { createCommand } = await import("../commands/create.js");

execSync(cmd, { cwd: testDir });
const originalCwd = process.cwd;
const originalLog = console.log;
process.cwd = () => testDir;
console.log = () => {};

const config = await fs.readFile(configPath, "utf-8");
expect(config).toContain("id: react-docs");
expect(config).toContain("name: React Docs");
expect(config).toContain("url: https://github.com/facebook/react.git");
expect(config).toContain("type: git_repo");
try {
await createCommand.parseAsync([
"node",
"create",
"--preset",
"git-repo",
"--id",
"react-docs",
"--name",
"React Docs",
"--url",
"https://github.com/facebook/react.git",
]);

const config = await fs.readFile(configPath, "utf-8");
expect(config).toContain("id: react-docs");
expect(config).toContain("name: React Docs");
expect(config).toContain("url: https://github.com/facebook/react.git");
expect(config).toContain("type: git_repo");
} finally {
process.cwd = originalCwd;
console.log = originalLog;
}
});

it("fails with invalid path", async () => {
const cliPath = join(process.cwd(), "dist/index.js");
const cmd = `node ${cliPath} create --preset local-folder --id test --name "Test" --path ./nonexistent`;
const { createCommand } = await import("../commands/create.js");

const originalCwd = process.cwd;
const originalLog = console.log;
const originalError = console.error;
process.cwd = () => testDir;
console.log = () => {};
console.error = () => {};

try {
await expect(
createCommand.parseAsync([
"node",
"create",
"--preset",
"local-folder",
"--id",
"test",
"--name",
"Test",
"--path",
"./nonexistent",
]),
).rejects.toThrow();
} finally {
process.cwd = originalCwd;
console.log = originalLog;
console.error = originalError;
}
});

it("creates config file when missing and creates symlinks for local folder", async () => {
// Remove the config file to test creation from scratch
await fs.rm(join(testDir, ".knowledge"), { recursive: true, force: true });

// Import and run create command directly
const { createCommand } = await import("../commands/create.js");

// Mock process.cwd and console.log
const originalCwd = process.cwd;
const originalLog = console.log;
process.cwd = () => testDir;
console.log = () => {}; // Suppress output

try {
await createCommand.parseAsync([
"node",
"create",
"--preset",
"local-folder",
"--id",
"test-docs",
"--name",
"Test Docs",
"--path",
"./docs",
]);

// Check config was created
const configExists = await fs
.access(configPath)
.then(() => true)
.catch(() => false);
expect(configExists).toBe(true);

const config = await fs.readFile(configPath, "utf-8");
expect(config).toContain("version: '1.0'");
expect(config).toContain("id: test-docs");
expect(config).toContain("name: Test Docs");
expect(config).toContain("type: local_folder");

expect(() => execSync(cmd, { cwd: testDir, stdio: "pipe" })).toThrow();
// Check symlinks were created
const symlinkDir = join(testDir, ".knowledge", "docsets", "test-docs");
const symlinkExists = await fs
.access(symlinkDir)
.then(() => true)
.catch(() => false);
expect(symlinkExists).toBe(true);
} finally {
process.cwd = originalCwd;
console.log = originalLog;
}
});

it("fails with duplicate ID", async () => {
const cliPath = join(process.cwd(), "dist/index.js");
// Create first docset
const cmd1 = `node ${cliPath} create --preset local-folder --id test-docs --name "Test Docs" --path ./docs`;
execSync(cmd1, { cwd: testDir });

// Try to create duplicate
const cmd2 = `node ${cliPath} create --preset local-folder --id test-docs --name "Test Docs 2" --path ./docs`;
expect(() => execSync(cmd2, { cwd: testDir, stdio: "pipe" })).toThrow();
const { createCommand } = await import("../commands/create.js");

const originalCwd = process.cwd;
const originalLog = console.log;
process.cwd = () => testDir;
console.log = () => {};

try {
// Create first docset
await createCommand.parseAsync([
"node",
"create",
"--preset",
"local-folder",
"--id",
"test-docs",
"--name",
"Test Docs",
"--path",
"./docs",
]);

// Try to create duplicate - should throw
await expect(
createCommand.parseAsync([
"node",
"create",
"--preset",
"local-folder",
"--id",
"test-docs",
"--name",
"Test Docs 2",
"--path",
"./docs",
]),
).rejects.toThrow();
} finally {
process.cwd = originalCwd;
console.log = originalLog;
}
});
});
42 changes: 39 additions & 3 deletions packages/cli/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,27 @@
console.log(chalk.blue("🚀 Creating new docset..."));

const configManager = new ConfigManager();
const { config, configPath } = await configManager.loadConfig(
process.cwd(),
);

// Check if config exists, create if not
let config, configPath;
const configExists = await configManager.configExists(process.cwd());

if (!configExists) {
// Create initial config structure
configPath = path.join(process.cwd(), ".knowledge", "config.yaml");
config = {
version: "1.0",
docsets: [],
};

// Ensure .knowledge directory exists
await fs.mkdir(path.dirname(configPath), { recursive: true });
console.log(chalk.gray("📁 Created .knowledge directory"));
} else {
({ config, configPath } = await configManager.loadConfig(
process.cwd(),
));
}

// Check if docset ID already exists
if (config.docsets.find((d) => d.id === options.id)) {
Expand All @@ -51,6 +69,24 @@
config.docsets.push(newDocset);
await configManager.saveConfig(config, configPath);

// For local folders, create symlinks immediately
if (options.preset === "local-folder") {
console.log(chalk.gray("🔗 Creating symlinks for local folder..."));
const { calculateLocalPathWithSymlinks } = await import(
"@codemcp/knowledge-core"
);
try {
await calculateLocalPathWithSymlinks(newDocset, configPath);
console.log(chalk.gray(" ✅ Symlinks created successfully"));
} catch (error) {
console.log(
chalk.yellow(
` ⚠️ Warning: Could not create symlinks: ${(error as Error).message}`,
),
);
}
}

console.log(
chalk.green(`✅ Created docset '${options.id}' successfully`),
);
Expand Down Expand Up @@ -101,7 +137,7 @@
if (!stat.isDirectory()) {
throw new Error(`Path is not a directory: ${options.path}`);
}
} catch (error) {

Check warning on line 140 in packages/cli/src/commands/create.ts

View workflow job for this annotation

GitHub Actions / test

'error' is defined but never used. Allowed unused caught errors must match /^_/u
throw new Error(`Path does not exist: ${options.path}`);
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { ConfigManager } from "./config/manager.js";
// Export path calculation utilities
export {
calculateLocalPath,
calculateLocalPathWithSymlinks,
formatPath,
validatePath,
validatePathSync,
Expand Down
Loading