Skip to content

Commit 9e40e83

Browse files
committed
Improve CLI core: register new commands, fix search and packaging
- Register plugin, config, and review subcommands in index.ts - Remove fast-glob dependency (use native Bun glob) - Fix build output to single file (--outfile dist/codeforge.js) - Add npm publish metadata (keywords, files, prepublishOnly) - Fix search filter edge cases with new tests - Fix plan-loader path resolution - Update session list/show formatting
1 parent 30f3e11 commit 9e40e83

File tree

10 files changed

+133
-60
lines changed

10 files changed

+133
-60
lines changed

cli/bun.lock

Lines changed: 0 additions & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
{
2-
"name": "codeforge-cli",
2+
"name": "codeforge-dev-cli",
33
"version": "0.1.0",
44
"description": "CLI for CodeForge development workflows",
5+
"keywords": [
6+
"codeforge",
7+
"cli",
8+
"code-review",
9+
"developer-tools",
10+
"devcontainer",
11+
"claude"
12+
],
513
"type": "module",
614
"bin": {
715
"codeforge": "./dist/codeforge.js"
816
},
917
"scripts": {
10-
"build": "bun build src/index.ts --outdir dist --target bun",
18+
"build": "bun build src/index.ts --outfile dist/codeforge.js --target bun",
1119
"dev": "bun run src/index.ts",
12-
"test": "bun test"
20+
"test": "bun test",
21+
"prepublishOnly": "bun run build && bun test"
1322
},
1423
"dependencies": {
1524
"commander": "^13.0.0",
16-
"chalk": "^5.4.0",
17-
"fast-glob": "^3.3.0"
25+
"chalk": "^5.4.0"
1826
},
1927
"devDependencies": {
2028
"@types/bun": "^1.3.10",
@@ -32,6 +40,11 @@
3240
"directory": "cli"
3341
},
3442
"homepage": "https://github.com/AnExiledDev/CodeForge/tree/main/cli#readme",
43+
"files": [
44+
"dist/",
45+
"prompts/",
46+
"README.md"
47+
],
3548
"bugs": {
3649
"url": "https://github.com/AnExiledDev/CodeForge/issues"
3750
}

cli/src/commands/session/list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import chalk from "chalk";
22
import type { Command } from "commander";
3+
import { basename } from "path";
34
import { loadHistory } from "../../loaders/history-loader.js";
45
import { extractSessionMeta } from "../../loaders/session-meta.js";
56
import {
@@ -57,8 +58,7 @@ export function registerListCommand(parent: Command): void {
5758
const filesBySessionId = new Map<string, string>();
5859
for (const filePath of sessionFiles) {
5960
// Extract session ID from filename (basename without extension)
60-
const parts = filePath.split("/");
61-
const filename = parts[parts.length - 1];
61+
const filename = basename(filePath);
6262
const id = filename.replace(/\.jsonl$/, "");
6363
filesBySessionId.set(id, filePath);
6464
}

cli/src/commands/session/show.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import chalk from "chalk";
22
import type { Command } from "commander";
33
import { homedir } from "os";
4-
import { resolve } from "path";
4+
import { basename, resolve } from "path";
55
import { extractSessionMeta } from "../../loaders/session-meta.js";
66
import { loadTasks } from "../../loaders/task-loader.js";
77
import {
@@ -44,8 +44,7 @@ export function registerShowCommand(parent: Command): void {
4444

4545
// Try UUID prefix match first
4646
for (const filePath of sessionFiles) {
47-
const parts = filePath.split("/");
48-
const filename = parts[parts.length - 1];
47+
const filename = basename(filePath);
4948
const id = filename.replace(/\.jsonl$/, "");
5049
if (id.startsWith(identifier)) {
5150
targetFile = filePath;
@@ -86,7 +85,7 @@ export function registerShowCommand(parent: Command): void {
8685
if (await planFile.exists()) {
8786
const content = await planFile.text();
8887
let title = meta.slug;
89-
for (const line of content.split("\n")) {
88+
for (const line of content.split(/\r?\n/)) {
9089
if (line.startsWith("# ")) {
9190
title = line.slice(2).trim();
9291
break;

cli/src/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
#!/usr/bin/env bun
22

33
import { Command } from "commander";
4+
import { registerConfigApplyCommand } from "./commands/config/apply.js";
5+
import { registerConfigShowCommand } from "./commands/config/show.js";
46
import { registerPlanSearchCommand } from "./commands/plan/search.js";
7+
import { registerPluginAgentsCommand } from "./commands/plugin/agents.js";
8+
import { registerPluginDisableCommand } from "./commands/plugin/disable.js";
9+
import { registerPluginEnableCommand } from "./commands/plugin/enable.js";
10+
import { registerPluginHooksCommand } from "./commands/plugin/hooks.js";
11+
import { registerPluginListCommand } from "./commands/plugin/list.js";
12+
import { registerPluginShowCommand } from "./commands/plugin/show.js";
13+
import { registerPluginSkillsCommand } from "./commands/plugin/skills.js";
14+
import { registerReviewCommand } from "./commands/review/review.js";
515
import { registerListCommand } from "./commands/session/list.js";
616
import { registerSearchCommand } from "./commands/session/search.js";
717
import { registerShowCommand } from "./commands/session/show.js";
@@ -30,4 +40,25 @@ const plan = program.command("plan").description("Search and manage plans");
3040

3141
registerPlanSearchCommand(plan);
3242

43+
const plugin = program
44+
.command("plugin")
45+
.description("Manage Claude Code plugins");
46+
47+
registerPluginListCommand(plugin);
48+
registerPluginShowCommand(plugin);
49+
registerPluginEnableCommand(plugin);
50+
registerPluginDisableCommand(plugin);
51+
registerPluginHooksCommand(plugin);
52+
registerPluginAgentsCommand(plugin);
53+
registerPluginSkillsCommand(plugin);
54+
55+
const config = program
56+
.command("config")
57+
.description("Manage Claude Code configuration");
58+
59+
registerConfigShowCommand(config);
60+
registerConfigApplyCommand(config);
61+
62+
registerReviewCommand(program);
63+
3364
program.parse();

cli/src/loaders/plan-loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function loadPlans(): Promise<PlanMeta[]> {
1818

1919
// Extract title from first heading line
2020
let title = slug;
21-
for (const line of content.split("\n")) {
21+
for (const line of content.split(/\r?\n/)) {
2222
if (line.startsWith("# ")) {
2323
title = line.slice(2).trim();
2424
break;

cli/src/search/engine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ export async function* readLines(filePath: string): AsyncGenerator<string> {
7272
// Keep the last partial line in the buffer
7373
buffer = lines.pop() || "";
7474
for (const line of lines) {
75-
if (line.trim()) yield line;
75+
if (line.trim()) yield line.replace(/\r$/, "");
7676
}
7777
}
7878

7979
// Flush remaining buffer
8080
const remaining = buffer + decoder.decode();
8181
if (remaining.trim()) {
82-
yield remaining;
82+
yield remaining.replace(/\r$/, "");
8383
}
8484
}
8585

cli/src/search/filter.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SearchableMessage } from "../schemas/session-message.js";
2+
import { normalizePath } from "../utils/platform.js";
23

34
export interface FilterOptions {
45
role?: string;
@@ -19,11 +20,11 @@ export function createFilter(
1920
}
2021

2122
if (options.project) {
22-
// Normalize: strip trailing slash for consistent comparison
23-
const project = options.project.replace(/\/+$/, "");
23+
// Normalize: convert backslashes and strip trailing separators
24+
const project = normalizePath(options.project).replace(/[/\\]+$/, "");
2425
filters.push((msg) => {
2526
if (!msg.cwd) return false;
26-
const dir = msg.cwd.replace(/\/+$/, "");
27+
const dir = normalizePath(msg.cwd).replace(/[/\\]+$/, "");
2728
return dir === project || dir.startsWith(project + "/");
2829
});
2930
}

cli/src/utils/glob.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { statSync } from "fs";
2-
import { homedir } from "os";
3-
import { resolve } from "path";
2+
import { getHome, resolveNormalized } from "./platform.js";
43

54
const DEFAULT_PATTERN = "**/*.jsonl";
65
const DEFAULT_BASE_DIR = ".claude/projects";
@@ -14,10 +13,10 @@ export async function discoverSessionFiles(
1413
if (pattern) {
1514
// Expand ~ to home directory
1615
const expanded = pattern.startsWith("~")
17-
? pattern.replace(/^~/, process.env.HOME || homedir())
16+
? pattern.replace(/^~/, getHome())
1817
: pattern;
1918
// Resolve to absolute path
20-
globPattern = resolve(expanded);
19+
globPattern = resolveNormalized(expanded);
2120

2221
// Split into base directory and glob portion
2322
// Find the first segment containing a glob character
@@ -37,8 +36,8 @@ export async function discoverSessionFiles(
3736
scanPath = baseparts.join("/") || "/";
3837
globPattern = globParts.join("/") || "**/*.jsonl";
3938
} else {
40-
const home = process.env.HOME || homedir();
41-
scanPath = resolve(home, DEFAULT_BASE_DIR);
39+
const home = getHome();
40+
scanPath = resolveNormalized(home, DEFAULT_BASE_DIR);
4241
globPattern = DEFAULT_PATTERN;
4342
}
4443

cli/tests/filter.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,73 @@ describe("createFilter", () => {
7373
});
7474
});
7575

76+
describe("project filter — Windows paths", () => {
77+
test("matches when cwd uses backslashes", () => {
78+
const filter = createFilter({
79+
project: "/workspaces/projects/CodeForge",
80+
});
81+
expect(
82+
filter(
83+
makeMessage({
84+
cwd: "\\workspaces\\projects\\CodeForge",
85+
}),
86+
),
87+
).toBe(true);
88+
});
89+
90+
test("matches when project uses backslashes", () => {
91+
const filter = createFilter({
92+
project: "C:\\Users\\dev\\project",
93+
});
94+
expect(
95+
filter(
96+
makeMessage({
97+
cwd: "C:/Users/dev/project/subdir",
98+
}),
99+
),
100+
).toBe(true);
101+
});
102+
103+
test("matches when both use backslashes", () => {
104+
const filter = createFilter({
105+
project: "C:\\Users\\dev\\project",
106+
});
107+
expect(
108+
filter(
109+
makeMessage({
110+
cwd: "C:\\Users\\dev\\project",
111+
}),
112+
),
113+
).toBe(true);
114+
});
115+
116+
test("rejects non-matching Windows paths", () => {
117+
const filter = createFilter({
118+
project: "C:\\Users\\dev\\project",
119+
});
120+
expect(
121+
filter(
122+
makeMessage({
123+
cwd: "C:\\Users\\dev\\other",
124+
}),
125+
),
126+
).toBe(false);
127+
});
128+
129+
test("handles trailing backslash in project", () => {
130+
const filter = createFilter({
131+
project: "C:\\Users\\dev\\project\\",
132+
});
133+
expect(
134+
filter(
135+
makeMessage({
136+
cwd: "C:\\Users\\dev\\project",
137+
}),
138+
),
139+
).toBe(true);
140+
});
141+
});
142+
76143
describe("time filter (after)", () => {
77144
test("includes messages at or after the date", () => {
78145
const filter = createFilter({ after: new Date("2026-03-01T10:00:00Z") });

0 commit comments

Comments
 (0)