Implement qmd collection rename command
- Added collectionRename() function to rename collections - Updates collection name in database (changes virtual path prefix) - Added CLI handler for "qmd collection rename <old> <new>" - Supports alias "mv" for rename command - Includes comprehensive error handling: * Checks if old collection exists * Prevents renaming to existing collection name * Validates required arguments - Fixed bug in collectionAdd where --name flag was ignored * indexFiles now accepts pwd and name parameters * Collection name is properly passed through to getOrCreateCollection - Updated help text and CLAUDE.md documentation - Added 4 new tests for rename functionality (all passing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d4f971c230
commit
27f9e8b630
@ -1,5 +1,5 @@
|
||||
{"id":"qmd-4ru","title":"Update document retrieval for new schema","description":"Functions like getDocument, findDocument, getMultipleDocuments need to work with new schema (path instead of filepath, content joins, virtual paths).","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-12T15:29:53.911881-05:00","updated_at":"2025-12-12T15:56:11.054888-05:00","closed_at":"2025-12-12T15:56:11.054888-05:00","dependencies":[{"issue_id":"qmd-4ru","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.912607-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-afe","title":"implement qmd collection rename, which changes the global path prefix for the collection","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-12T15:55:54.779325-05:00","updated_at":"2025-12-12T15:55:54.779325-05:00"}
|
||||
{"id":"qmd-afe","title":"implement qmd collection rename, which changes the global path prefix for the collection","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:55:54.779325-05:00","updated_at":"2025-12-12T16:29:24.153196-05:00","closed_at":"2025-12-12T16:29:24.153196-05:00"}
|
||||
{"id":"qmd-ama","title":"Refactor database system","description":"All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection, path, hash,\n┃ created_at, updated_at. (collection,path)\n┃\n┃\n\n┃ All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection_id, path, hash,\n┃ created_at, updated_at. (collection,path) is unique. There is also collection which stores PWD\n┃ + glob pattern, name (\\w+). Every document is treated as path qmd://collection.name/","notes":"## Completed\n- ✅ Implemented content-addressable storage (content table with hash→doc mapping)\n- ✅ Refactored documents table as file system layer (collection_id, path, hash)\n- ✅ Added collection names (e.g., \"pages\", \"journals\", \"archive\")\n- ✅ Implemented virtual paths (qmd://collection-name/path/to/file.md)\n- ✅ Added hierarchical context support (collection-scoped)\n- ✅ Successfully migrated existing database\n- ✅ Updated search functions to work with new schema\n- ✅ Updated indexing logic to use content-addressable storage\n- ✅ Orphaned content hash cleanup\n\n## Still TODO\n- Fix migration SQL to properly extract basename (currently needs manual fix)\n- Implement `qmd collection add . --name \u003cname\u003e --mask '**/*.md'`\n- Implement `qmd ls [path]` for exploring virtual file tree","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T10:57:35.497489-05:00","updated_at":"2025-12-12T15:39:48.879143-05:00","closed_at":"2025-12-12T15:39:48.879143-05:00"}
|
||||
{"id":"qmd-bx1","title":"Fix migration SQL for proper basename extraction","description":"The migration currently generates collection names incorrectly (uses full path instead of basename). Need to fix the SQL in migrateToContentAddressable to properly extract the directory basename.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-12T15:29:53.757723-05:00","updated_at":"2025-12-12T15:50:29.349134-05:00","closed_at":"2025-12-12T15:50:29.349134-05:00","dependencies":[{"issue_id":"qmd-bx1","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.758524-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-c0m","title":"Comprehensive CLI review and consistency pass","description":"Review entire CLI command structure:\n- Consistent naming (add vs create, remove vs delete)\n- Consistent flag usage (--name, --mask, etc)\n- Update help text for all commands\n- Ensure virtual paths work everywhere\n- Test all commands end-to-end","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-12T15:29:38.083564-05:00","updated_at":"2025-12-12T16:06:51.544695-05:00","closed_at":"2025-12-12T16:06:51.544695-05:00"}
|
||||
|
||||
@ -10,6 +10,7 @@ Use Bun instead of Node.js (`bun` not `node`, `bun install` not `npm install`).
|
||||
qmd collection add . --name <n> # Create/index collection
|
||||
qmd collection list # List all collections with details
|
||||
qmd collection remove <name> # Remove a collection by name
|
||||
qmd collection rename <old> <new> # Rename a collection
|
||||
qmd ls [collection[/path]] # List collections or files in a collection
|
||||
qmd context add [path] "text" # Add context for path (defaults to current dir)
|
||||
qmd context list # List all contexts
|
||||
@ -36,6 +37,9 @@ qmd collection add ~/Documents/notes --name mynotes --mask '**/*.md'
|
||||
# Remove a collection
|
||||
qmd collection remove mynotes
|
||||
|
||||
# Rename a collection
|
||||
qmd collection rename mynotes my-notes
|
||||
|
||||
# List all files in a collection
|
||||
qmd ls mynotes
|
||||
|
||||
|
||||
56
cli.test.ts
56
cli.test.ts
@ -616,4 +616,60 @@ describe("CLI Collection Commands", () => {
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("Unknown subcommand");
|
||||
});
|
||||
|
||||
test("renames a collection", async () => {
|
||||
// First verify the collection exists
|
||||
const { stdout: listBefore } = await runQmd(["collection", "list"], { dbPath: localDbPath });
|
||||
expect(listBefore).toMatch(/^fixtures$/m); // Collection name on its own line
|
||||
|
||||
// Rename it
|
||||
const { stdout, exitCode } = await runQmd(["collection", "rename", "fixtures", "my-fixtures"], { dbPath: localDbPath });
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("✓ Renamed collection 'fixtures' to 'my-fixtures'");
|
||||
expect(stdout).toContain("qmd://fixtures/");
|
||||
expect(stdout).toContain("qmd://my-fixtures/");
|
||||
|
||||
// Verify the new name exists and old name is gone
|
||||
const { stdout: listAfter } = await runQmd(["collection", "list"], { dbPath: localDbPath });
|
||||
expect(listAfter).toMatch(/^my-fixtures$/m); // Collection name on its own line
|
||||
expect(listAfter).not.toMatch(/^fixtures$/m); // Old name should not appear as collection name
|
||||
});
|
||||
|
||||
test("handles renaming non-existent collection", async () => {
|
||||
const { stderr, exitCode } = await runQmd(["collection", "rename", "nonexistent", "newname"], { dbPath: localDbPath });
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("Collection not found");
|
||||
});
|
||||
|
||||
test("handles renaming to existing collection name", async () => {
|
||||
// Create a second collection in a temp directory
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "qmd-second-"));
|
||||
await writeFile(join(tempDir, "test.md"), "# Test");
|
||||
const addResult = await runQmd(["collection", "add", tempDir, "--name", "second"], { dbPath: localDbPath });
|
||||
|
||||
if (addResult.exitCode !== 0) {
|
||||
console.error("Failed to add second collection:", addResult.stderr);
|
||||
}
|
||||
expect(addResult.exitCode).toBe(0);
|
||||
|
||||
// Verify both collections exist
|
||||
const { stdout: listBoth } = await runQmd(["collection", "list"], { dbPath: localDbPath });
|
||||
expect(listBoth).toMatch(/^fixtures$/m);
|
||||
expect(listBoth).toMatch(/^second$/m);
|
||||
|
||||
// Try to rename fixtures to second (which already exists)
|
||||
const { stderr, exitCode } = await runQmd(["collection", "rename", "fixtures", "second"], { dbPath: localDbPath });
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("Collection name already exists");
|
||||
});
|
||||
|
||||
test("handles missing rename arguments", async () => {
|
||||
const { stderr: stderr1, exitCode: exitCode1 } = await runQmd(["collection", "rename"], { dbPath: localDbPath });
|
||||
expect(exitCode1).toBe(1);
|
||||
expect(stderr1).toContain("Usage:");
|
||||
|
||||
const { stderr: stderr2, exitCode: exitCode2 } = await runQmd(["collection", "rename", "fixtures"], { dbPath: localDbPath });
|
||||
expect(exitCode2).toBe(1);
|
||||
expect(stderr2).toContain("Usage:");
|
||||
});
|
||||
});
|
||||
|
||||
59
qmd.ts
59
qmd.ts
@ -1334,7 +1334,7 @@ async function collectionAdd(pwd: string, globPattern: string, name?: string): P
|
||||
|
||||
// Create the collection and index files
|
||||
console.log(`Creating collection '${name}'...`);
|
||||
await indexFiles(globPattern);
|
||||
await indexFiles(pwd, globPattern, name);
|
||||
console.log(`${c.green}✓${c.reset} Collection '${name}' created successfully`);
|
||||
}
|
||||
|
||||
@ -1378,6 +1378,37 @@ function collectionRemove(name: string): void {
|
||||
closeDb();
|
||||
}
|
||||
|
||||
function collectionRename(oldName: string, newName: string): void {
|
||||
const db = getDb();
|
||||
|
||||
// Check if old collection exists
|
||||
const coll = getCollectionByName(db, oldName);
|
||||
if (!coll) {
|
||||
console.error(`${c.yellow}Collection not found: ${oldName}${c.reset}`);
|
||||
console.error(`Run 'qmd collection list' to see available collections.`);
|
||||
closeDb();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if new name already exists
|
||||
const existing = getCollectionByName(db, newName);
|
||||
if (existing) {
|
||||
console.error(`${c.yellow}Collection name already exists: ${newName}${c.reset}`);
|
||||
console.error(`Choose a different name or remove the existing collection first.`);
|
||||
closeDb();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Update the collection name
|
||||
db.prepare(`UPDATE collections SET name = ?, updated_at = ? WHERE id = ?`)
|
||||
.run(newName, new Date().toISOString(), coll.id);
|
||||
|
||||
console.log(`${c.green}✓${c.reset} Renamed collection '${oldName}' to '${newName}'`);
|
||||
console.log(` Virtual paths updated: ${c.cyan}qmd://${oldName}/${c.reset} → ${c.cyan}qmd://${newName}/${c.reset}`);
|
||||
|
||||
closeDb();
|
||||
}
|
||||
|
||||
async function dropCollection(globPattern: string): Promise<void> {
|
||||
const db = getDb();
|
||||
const pwd = getPwd();
|
||||
@ -1401,9 +1432,9 @@ async function dropCollection(globPattern: string): Promise<void> {
|
||||
// Don't close db - indexFiles will use it and close at the end
|
||||
}
|
||||
|
||||
async function indexFiles(globPattern: string = DEFAULT_GLOB): Promise<void> {
|
||||
async function indexFiles(pwd?: string, globPattern: string = DEFAULT_GLOB, name?: string): Promise<void> {
|
||||
const db = getDb();
|
||||
const pwd = getPwd();
|
||||
const resolvedPwd = pwd || getPwd();
|
||||
const now = new Date().toISOString();
|
||||
const excludeDirs = ["node_modules", ".git", ".cache", "vendor", "dist", "build"];
|
||||
|
||||
@ -1411,13 +1442,13 @@ async function indexFiles(globPattern: string = DEFAULT_GLOB): Promise<void> {
|
||||
clearCache(db);
|
||||
|
||||
// Get or create collection for this (pwd, glob)
|
||||
const collectionId = getOrCreateCollection(db, pwd, globPattern);
|
||||
console.log(`Collection: ${pwd} (${globPattern})`);
|
||||
const collectionId = getOrCreateCollection(db, resolvedPwd, globPattern, name);
|
||||
console.log(`Collection: ${resolvedPwd} (${globPattern})`);
|
||||
|
||||
progress.indeterminate();
|
||||
const glob = new Glob(globPattern);
|
||||
const files: string[] = [];
|
||||
for await (const file of glob.scan({ cwd: pwd, onlyFiles: true, followSymlinks: true })) {
|
||||
for await (const file of glob.scan({ cwd: resolvedPwd, onlyFiles: true, followSymlinks: true })) {
|
||||
// Skip node_modules, hidden folders (.*), and other common excludes
|
||||
const parts = file.split("/");
|
||||
const shouldSkip = parts.some(part =>
|
||||
@ -1450,7 +1481,7 @@ async function indexFiles(globPattern: string = DEFAULT_GLOB): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
for (const relativeFile of files) {
|
||||
const filepath = getRealPath(resolve(pwd, relativeFile));
|
||||
const filepath = getRealPath(resolve(resolvedPwd, relativeFile));
|
||||
const path = relativeFile; // Use relative path as-is
|
||||
seenPaths.add(path);
|
||||
|
||||
@ -2339,6 +2370,7 @@ function showHelp(): void {
|
||||
console.log(" qmd collection add [path] --name <name> --mask <pattern> - Create/index collection");
|
||||
console.log(" qmd collection list - List all collections with details");
|
||||
console.log(" qmd collection remove <name> - Remove a collection by name");
|
||||
console.log(" qmd collection rename <old> <new> - Rename a collection");
|
||||
console.log(" qmd ls [collection[/path]] - List collections or files in a collection");
|
||||
console.log(" qmd context add [path] \"text\" - Add context for path (defaults to current dir)");
|
||||
console.log(" qmd context list - List all contexts");
|
||||
@ -2544,9 +2576,20 @@ switch (cli.command) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "rename":
|
||||
case "mv": {
|
||||
if (!cli.args[1] || !cli.args[2]) {
|
||||
console.error("Usage: qmd collection rename <old-name> <new-name>");
|
||||
console.error(" Use 'qmd collection list' to see available collections");
|
||||
process.exit(1);
|
||||
}
|
||||
collectionRename(cli.args[1], cli.args[2]);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown subcommand: ${subcommand}`);
|
||||
console.error("Available: list, add, remove");
|
||||
console.error("Available: list, add, remove, rename");
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user