opencode-conductor-cdd-plugin 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +163 -0
- package/README.test.md +51 -0
- package/dist/commands/implement.d.ts +1 -0
- package/dist/commands/implement.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +122 -0
- package/dist/prompts/agent/cdd.md +41 -0
- package/dist/prompts/agent/implementer.md +22 -0
- package/dist/prompts/agent.md +23 -0
- package/dist/prompts/cdd/implement.json +4 -0
- package/dist/prompts/cdd/newTrack.json +4 -0
- package/dist/prompts/cdd/revert.json +4 -0
- package/dist/prompts/cdd/setup.json +4 -0
- package/dist/prompts/cdd/setup.test.d.ts +1 -0
- package/dist/prompts/cdd/setup.test.js +132 -0
- package/dist/prompts/cdd/setup.test.ts +168 -0
- package/dist/prompts/cdd/status.json +4 -0
- package/dist/prompts/strategies/delegate.md +11 -0
- package/dist/prompts/strategies/manual.md +9 -0
- package/dist/templates/code_styleguides/c.md +28 -0
- package/dist/templates/code_styleguides/cpp.md +46 -0
- package/dist/templates/code_styleguides/csharp.md +115 -0
- package/dist/templates/code_styleguides/dart.md +238 -0
- package/dist/templates/code_styleguides/general.md +23 -0
- package/dist/templates/code_styleguides/go.md +48 -0
- package/dist/templates/code_styleguides/html-css.md +49 -0
- package/dist/templates/code_styleguides/java.md +39 -0
- package/dist/templates/code_styleguides/javascript.md +51 -0
- package/dist/templates/code_styleguides/julia.md +27 -0
- package/dist/templates/code_styleguides/kotlin.md +41 -0
- package/dist/templates/code_styleguides/php.md +37 -0
- package/dist/templates/code_styleguides/python.md +37 -0
- package/dist/templates/code_styleguides/react.md +37 -0
- package/dist/templates/code_styleguides/ruby.md +39 -0
- package/dist/templates/code_styleguides/rust.md +44 -0
- package/dist/templates/code_styleguides/shell.md +35 -0
- package/dist/templates/code_styleguides/solidity.md +60 -0
- package/dist/templates/code_styleguides/sql.md +39 -0
- package/dist/templates/code_styleguides/swift.md +36 -0
- package/dist/templates/code_styleguides/typescript.md +43 -0
- package/dist/templates/code_styleguides/vue.md +38 -0
- package/dist/templates/code_styleguides/zig.md +27 -0
- package/dist/templates/workflow.md +336 -0
- package/dist/tools/background.d.ts +54 -0
- package/dist/tools/background.js +198 -0
- package/dist/tools/commands.d.ts +11 -0
- package/dist/tools/commands.js +80 -0
- package/dist/tools/commands.test.d.ts +1 -0
- package/dist/tools/commands.test.js +142 -0
- package/dist/tools/delegate.d.ts +3 -0
- package/dist/tools/delegate.js +45 -0
- package/dist/utils/autogenerateFlow.d.ts +65 -0
- package/dist/utils/autogenerateFlow.js +391 -0
- package/dist/utils/autogenerateFlow.test.d.ts +1 -0
- package/dist/utils/autogenerateFlow.test.js +610 -0
- package/dist/utils/bootstrap.d.ts +1 -0
- package/dist/utils/bootstrap.js +46 -0
- package/dist/utils/commandFactory.d.ts +11 -0
- package/dist/utils/commandFactory.js +69 -0
- package/dist/utils/commitMessages.d.ts +35 -0
- package/dist/utils/commitMessages.js +33 -0
- package/dist/utils/commitMessages.test.d.ts +1 -0
- package/dist/utils/commitMessages.test.js +79 -0
- package/dist/utils/configDetection.d.ts +7 -0
- package/dist/utils/configDetection.js +49 -0
- package/dist/utils/configDetection.test.d.ts +1 -0
- package/dist/utils/configDetection.test.js +119 -0
- package/dist/utils/contentGeneration.d.ts +10 -0
- package/dist/utils/contentGeneration.js +141 -0
- package/dist/utils/contentGeneration.test.d.ts +1 -0
- package/dist/utils/contentGeneration.test.js +147 -0
- package/dist/utils/contextAnalysis.d.ts +100 -0
- package/dist/utils/contextAnalysis.js +308 -0
- package/dist/utils/contextAnalysis.test.d.ts +1 -0
- package/dist/utils/contextAnalysis.test.js +307 -0
- package/dist/utils/gitNotes.d.ts +23 -0
- package/dist/utils/gitNotes.js +53 -0
- package/dist/utils/gitNotes.test.d.ts +1 -0
- package/dist/utils/gitNotes.test.js +105 -0
- package/dist/utils/ignoreMatcher.d.ts +9 -0
- package/dist/utils/ignoreMatcher.js +77 -0
- package/dist/utils/ignoreMatcher.test.d.ts +1 -0
- package/dist/utils/ignoreMatcher.test.js +126 -0
- package/dist/utils/stateManager.d.ts +10 -0
- package/dist/utils/stateManager.js +30 -0
- package/package.json +90 -0
- package/scripts/convert-legacy.cjs +17 -0
- package/scripts/postinstall.cjs +38 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
async function loadPrompt(filename, replacements = {}) {
|
|
8
|
+
const pathsToTry = [
|
|
9
|
+
join(__dirname, "..", "prompts", filename),
|
|
10
|
+
join(__dirname, "..", "..", filename),
|
|
11
|
+
];
|
|
12
|
+
let content = "";
|
|
13
|
+
for (const p of pathsToTry) {
|
|
14
|
+
try {
|
|
15
|
+
content = await readFile(p, "utf-8");
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!content) {
|
|
23
|
+
console.error(`[CDD] Error loading prompt ${filename}: Not found in any tried paths`);
|
|
24
|
+
return {
|
|
25
|
+
prompt: `SYSTEM ERROR: Failed to load prompt ${filename}`,
|
|
26
|
+
description: "Error loading command",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(content);
|
|
31
|
+
const description = parsed.description || "CDD Command";
|
|
32
|
+
let promptText = parsed.prompt || "";
|
|
33
|
+
if (!promptText)
|
|
34
|
+
throw new Error(`Could not parse prompt text from ${filename}`);
|
|
35
|
+
const defaults = {
|
|
36
|
+
templatesDir: join(dirname(dirname(__dirname)), "templates"),
|
|
37
|
+
};
|
|
38
|
+
const finalReplacements = { ...defaults, ...replacements };
|
|
39
|
+
for (const [key, value] of Object.entries(finalReplacements)) {
|
|
40
|
+
promptText = promptText.replaceAll(`{{${key}}}`, value || "");
|
|
41
|
+
}
|
|
42
|
+
return { prompt: promptText, description };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`[CDD] Error parsing prompt ${filename}:`, error);
|
|
46
|
+
return {
|
|
47
|
+
prompt: `SYSTEM ERROR: Failed to parse prompt ${filename}`,
|
|
48
|
+
description: "Error parsing command",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function createCDDCommand(config) {
|
|
53
|
+
return (ctx) => {
|
|
54
|
+
return tool({
|
|
55
|
+
description: config.description,
|
|
56
|
+
args: config.args,
|
|
57
|
+
async execute(args) {
|
|
58
|
+
const additionalContext = config.additionalContext
|
|
59
|
+
? await config.additionalContext(ctx, args)
|
|
60
|
+
: {};
|
|
61
|
+
const replacements = { ...additionalContext };
|
|
62
|
+
const { prompt } = await loadPrompt(config.name, replacements);
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
directives: prompt
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conventional commit types
|
|
3
|
+
*/
|
|
4
|
+
export type CommitType = "feat" | "fix" | "docs" | "style" | "refactor" | "test" | "chore";
|
|
5
|
+
export interface TaskCommitMessageOptions {
|
|
6
|
+
type: CommitType;
|
|
7
|
+
scope?: string;
|
|
8
|
+
description: string;
|
|
9
|
+
trackId: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PhaseCheckpointMessageOptions {
|
|
13
|
+
phaseName: string;
|
|
14
|
+
trackId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PlanUpdateMessageOptions {
|
|
17
|
+
updateType: "task" | "phase";
|
|
18
|
+
description: string;
|
|
19
|
+
trackId: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates a conventional commit message for a task
|
|
23
|
+
* Format: <type>(<scope>): <description>\n\n[body]\n\nTrack: <trackId>
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateTaskCommitMessage(options: TaskCommitMessageOptions): string;
|
|
26
|
+
/**
|
|
27
|
+
* Generates a commit message for a phase checkpoint
|
|
28
|
+
* Format: cdd(checkpoint): Checkpoint end of <phaseName>\n\nTrack: <trackId>
|
|
29
|
+
*/
|
|
30
|
+
export declare function generatePhaseCheckpointMessage(options: PhaseCheckpointMessageOptions): string;
|
|
31
|
+
/**
|
|
32
|
+
* Generates a commit message for plan.md updates
|
|
33
|
+
* Format: cdd(plan): Mark <updateType> '<description>' as complete\n\nTrack: <trackId>
|
|
34
|
+
*/
|
|
35
|
+
export declare function generatePlanUpdateMessage(options: PlanUpdateMessageOptions): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a conventional commit message for a task
|
|
3
|
+
* Format: <type>(<scope>): <description>\n\n[body]\n\nTrack: <trackId>
|
|
4
|
+
*/
|
|
5
|
+
export function generateTaskCommitMessage(options) {
|
|
6
|
+
const { type, scope, description, trackId, body } = options;
|
|
7
|
+
// Build the subject line
|
|
8
|
+
const scopePart = scope ? `(${scope})` : "";
|
|
9
|
+
const subject = `${type}${scopePart}: ${description}`;
|
|
10
|
+
// Build the full message
|
|
11
|
+
const parts = [subject];
|
|
12
|
+
if (body) {
|
|
13
|
+
parts.push("", body); // Empty line before body
|
|
14
|
+
}
|
|
15
|
+
parts.push("", `Track: ${trackId}`);
|
|
16
|
+
return parts.join("\n");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generates a commit message for a phase checkpoint
|
|
20
|
+
* Format: cdd(checkpoint): Checkpoint end of <phaseName>\n\nTrack: <trackId>
|
|
21
|
+
*/
|
|
22
|
+
export function generatePhaseCheckpointMessage(options) {
|
|
23
|
+
const { phaseName, trackId } = options;
|
|
24
|
+
return `cdd(checkpoint): Checkpoint end of ${phaseName}\n\nTrack: ${trackId}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generates a commit message for plan.md updates
|
|
28
|
+
* Format: cdd(plan): Mark <updateType> '<description>' as complete\n\nTrack: <trackId>
|
|
29
|
+
*/
|
|
30
|
+
export function generatePlanUpdateMessage(options) {
|
|
31
|
+
const { updateType, description, trackId } = options;
|
|
32
|
+
return `cdd(plan): Mark ${updateType} '${description}' as complete\n\nTrack: ${trackId}`;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { generateTaskCommitMessage, generatePhaseCheckpointMessage, generatePlanUpdateMessage } from "./commitMessages.js";
|
|
3
|
+
describe("commitMessages", () => {
|
|
4
|
+
describe("generateTaskCommitMessage", () => {
|
|
5
|
+
it("should generate a commit message for a feature task", () => {
|
|
6
|
+
const message = generateTaskCommitMessage({
|
|
7
|
+
type: "feat",
|
|
8
|
+
scope: "auth",
|
|
9
|
+
description: "Add JWT authentication",
|
|
10
|
+
trackId: "track_123"
|
|
11
|
+
});
|
|
12
|
+
expect(message).toBe("feat(auth): Add JWT authentication\n\nTrack: track_123");
|
|
13
|
+
});
|
|
14
|
+
it("should generate a commit message for a fix task", () => {
|
|
15
|
+
const message = generateTaskCommitMessage({
|
|
16
|
+
type: "fix",
|
|
17
|
+
scope: "api",
|
|
18
|
+
description: "Correct endpoint validation",
|
|
19
|
+
trackId: "bug_fix_456"
|
|
20
|
+
});
|
|
21
|
+
expect(message).toBe("fix(api): Correct endpoint validation\n\nTrack: bug_fix_456");
|
|
22
|
+
});
|
|
23
|
+
it("should handle commit message without scope", () => {
|
|
24
|
+
const message = generateTaskCommitMessage({
|
|
25
|
+
type: "test",
|
|
26
|
+
description: "Add unit tests",
|
|
27
|
+
trackId: "track_789"
|
|
28
|
+
});
|
|
29
|
+
expect(message).toBe("test: Add unit tests\n\nTrack: track_789");
|
|
30
|
+
});
|
|
31
|
+
it("should include body when provided", () => {
|
|
32
|
+
const message = generateTaskCommitMessage({
|
|
33
|
+
type: "feat",
|
|
34
|
+
scope: "ui",
|
|
35
|
+
description: "Create user dashboard",
|
|
36
|
+
trackId: "track_abc",
|
|
37
|
+
body: "Implemented responsive layout with mobile support"
|
|
38
|
+
});
|
|
39
|
+
expect(message).toContain("feat(ui): Create user dashboard");
|
|
40
|
+
expect(message).toContain("Implemented responsive layout with mobile support");
|
|
41
|
+
expect(message).toContain("Track: track_abc");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("generatePhaseCheckpointMessage", () => {
|
|
45
|
+
it("should generate a checkpoint message for a phase", () => {
|
|
46
|
+
const message = generatePhaseCheckpointMessage({
|
|
47
|
+
phaseName: "Phase 1: Research & Analysis",
|
|
48
|
+
trackId: "track_123"
|
|
49
|
+
});
|
|
50
|
+
expect(message).toBe("cdd(checkpoint): Checkpoint end of Phase 1: Research & Analysis\n\nTrack: track_123");
|
|
51
|
+
});
|
|
52
|
+
it("should handle phase names with special characters", () => {
|
|
53
|
+
const message = generatePhaseCheckpointMessage({
|
|
54
|
+
phaseName: "Phase 2: Implementation & Testing",
|
|
55
|
+
trackId: "track_xyz"
|
|
56
|
+
});
|
|
57
|
+
expect(message).toContain("Phase 2: Implementation & Testing");
|
|
58
|
+
expect(message).toContain("Track: track_xyz");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe("generatePlanUpdateMessage", () => {
|
|
62
|
+
it("should generate a message for task completion", () => {
|
|
63
|
+
const message = generatePlanUpdateMessage({
|
|
64
|
+
updateType: "task",
|
|
65
|
+
description: "Create user model",
|
|
66
|
+
trackId: "track_123"
|
|
67
|
+
});
|
|
68
|
+
expect(message).toBe("cdd(plan): Mark task 'Create user model' as complete\n\nTrack: track_123");
|
|
69
|
+
});
|
|
70
|
+
it("should generate a message for phase completion", () => {
|
|
71
|
+
const message = generatePlanUpdateMessage({
|
|
72
|
+
updateType: "phase",
|
|
73
|
+
description: "Phase 1: Setup",
|
|
74
|
+
trackId: "track_456"
|
|
75
|
+
});
|
|
76
|
+
expect(message).toBe("cdd(plan): Mark phase 'Phase 1: Setup' as complete\n\nTrack: track_456");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
export function detectCDDConfig() {
|
|
5
|
+
const opencodeConfigDir = join(homedir(), ".config", "opencode");
|
|
6
|
+
const opencodeJsonPath = join(opencodeConfigDir, "opencode.json");
|
|
7
|
+
const omoJsonPath = join(opencodeConfigDir, "oh-my-opencode.json");
|
|
8
|
+
let hasCDDInOpenCode = false;
|
|
9
|
+
let hasCDDInOMO = false;
|
|
10
|
+
let cddModel;
|
|
11
|
+
// Check oh-my-opencode.json first (higher priority)
|
|
12
|
+
if (existsSync(omoJsonPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const config = JSON.parse(readFileSync(omoJsonPath, "utf-8"));
|
|
15
|
+
if (config.agents && config.agents.cdd) {
|
|
16
|
+
hasCDDInOMO = true;
|
|
17
|
+
// Extract model from oh-my-opencode.json
|
|
18
|
+
if (config.agents.cdd.model) {
|
|
19
|
+
cddModel = config.agents.cdd.model;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
// Silently fail on parse errors
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Check opencode.json (fallback if model not found in OMO)
|
|
28
|
+
if (existsSync(opencodeJsonPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
|
|
31
|
+
if (config.agent && config.agent.cdd) {
|
|
32
|
+
hasCDDInOpenCode = true;
|
|
33
|
+
// Only use this model if we didn't find one in oh-my-opencode.json
|
|
34
|
+
if (!cddModel && config.agent.cdd.model) {
|
|
35
|
+
cddModel = config.agent.cdd.model;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
// Silently fail on parse errors
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
hasCDDInOpenCode,
|
|
45
|
+
hasCDDInOMO,
|
|
46
|
+
synergyActive: hasCDDInOMO,
|
|
47
|
+
cddModel,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { detectCDDConfig } from "./configDetection.js";
|
|
4
|
+
vi.mock("fs", () => ({
|
|
5
|
+
existsSync: vi.fn(),
|
|
6
|
+
readFileSync: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("os", () => ({
|
|
9
|
+
homedir: vi.fn(() => "/home/user"),
|
|
10
|
+
}));
|
|
11
|
+
describe("configDetection", () => {
|
|
12
|
+
const opencodeJsonPath = "/home/user/.config/opencode/opencode.json";
|
|
13
|
+
const omoJsonPath = "/home/user/.config/opencode/oh-my-opencode.json";
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
it("should detect cdd in opencode.json", () => {
|
|
18
|
+
vi.mocked(existsSync).mockImplementation((path) => path === opencodeJsonPath);
|
|
19
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
20
|
+
if (path === opencodeJsonPath) {
|
|
21
|
+
return JSON.stringify({ agent: { cdd: { model: "anthropic/claude-3-5-sonnet" } } });
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
});
|
|
25
|
+
const result = detectCDDConfig();
|
|
26
|
+
expect(result.hasCDDInOpenCode).toBe(true);
|
|
27
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
28
|
+
expect(result.synergyActive).toBe(false);
|
|
29
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-sonnet");
|
|
30
|
+
});
|
|
31
|
+
it("should detect cdd in oh-my-opencode.json and activate synergy", () => {
|
|
32
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
33
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
34
|
+
if (path === omoJsonPath) {
|
|
35
|
+
return JSON.stringify({ agents: { cdd: { model: "anthropic/claude-3-5-haiku" } } });
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
});
|
|
39
|
+
const result = detectCDDConfig();
|
|
40
|
+
expect(result.hasCDDInOpenCode).toBe(false);
|
|
41
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
42
|
+
expect(result.synergyActive).toBe(true);
|
|
43
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-haiku");
|
|
44
|
+
});
|
|
45
|
+
it("should handle both configs present", () => {
|
|
46
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
47
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
48
|
+
if (path === opencodeJsonPath) {
|
|
49
|
+
return JSON.stringify({ agent: { cdd: { model: "anthropic/claude-3-5-sonnet" } } });
|
|
50
|
+
}
|
|
51
|
+
if (path === omoJsonPath) {
|
|
52
|
+
return JSON.stringify({ agents: { cdd: { model: "anthropic/claude-3-5-haiku" } } });
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
});
|
|
56
|
+
const result = detectCDDConfig();
|
|
57
|
+
expect(result.hasCDDInOpenCode).toBe(true);
|
|
58
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
59
|
+
expect(result.synergyActive).toBe(true);
|
|
60
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-haiku");
|
|
61
|
+
});
|
|
62
|
+
it("should handle missing configs", () => {
|
|
63
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
64
|
+
const result = detectCDDConfig();
|
|
65
|
+
expect(result.hasCDDInOpenCode).toBe(false);
|
|
66
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
67
|
+
expect(result.synergyActive).toBe(false);
|
|
68
|
+
expect(result.cddModel).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it("should handle malformed JSON", () => {
|
|
71
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
72
|
+
vi.mocked(readFileSync).mockReturnValue("invalid json");
|
|
73
|
+
const result = detectCDDConfig();
|
|
74
|
+
expect(result.hasCDDInOpenCode).toBe(false);
|
|
75
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
76
|
+
expect(result.synergyActive).toBe(false);
|
|
77
|
+
expect(result.cddModel).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
it("should prioritize oh-my-opencode.json model over opencode.json", () => {
|
|
80
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
81
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
82
|
+
if (path === opencodeJsonPath) {
|
|
83
|
+
return JSON.stringify({ agent: { cdd: { model: "model-from-opencode" } } });
|
|
84
|
+
}
|
|
85
|
+
if (path === omoJsonPath) {
|
|
86
|
+
return JSON.stringify({ agents: { cdd: { model: "model-from-omo" } } });
|
|
87
|
+
}
|
|
88
|
+
return "";
|
|
89
|
+
});
|
|
90
|
+
const result = detectCDDConfig();
|
|
91
|
+
expect(result.cddModel).toBe("model-from-omo");
|
|
92
|
+
});
|
|
93
|
+
it("should fallback to opencode.json model when oh-my-opencode.json has no model", () => {
|
|
94
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
95
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
96
|
+
if (path === opencodeJsonPath) {
|
|
97
|
+
return JSON.stringify({ agent: { cdd: { model: "model-from-opencode" } } });
|
|
98
|
+
}
|
|
99
|
+
if (path === omoJsonPath) {
|
|
100
|
+
return JSON.stringify({ agents: { cdd: {} } });
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
103
|
+
});
|
|
104
|
+
const result = detectCDDConfig();
|
|
105
|
+
expect(result.cddModel).toBe("model-from-opencode");
|
|
106
|
+
});
|
|
107
|
+
it("should handle cdd config without model field", () => {
|
|
108
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
109
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
110
|
+
if (path === omoJsonPath) {
|
|
111
|
+
return JSON.stringify({ agents: { cdd: {} } });
|
|
112
|
+
}
|
|
113
|
+
return "";
|
|
114
|
+
});
|
|
115
|
+
const result = detectCDDConfig();
|
|
116
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
117
|
+
expect(result.cddModel).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProjectContext } from "./contextAnalysis.js";
|
|
2
|
+
export type { ProjectContext };
|
|
3
|
+
export interface GenerationResult {
|
|
4
|
+
content: string;
|
|
5
|
+
confidence: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateProductGuide(context: ProjectContext): GenerationResult;
|
|
8
|
+
export declare function generateProductGuidelines(context: ProjectContext): GenerationResult;
|
|
9
|
+
export declare function generateTechStack(context: ProjectContext): GenerationResult;
|
|
10
|
+
export declare function generateWorkflow(context: ProjectContext): GenerationResult;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export function generateProductGuide(context) {
|
|
2
|
+
const parts = [];
|
|
3
|
+
let confidence = 0.0;
|
|
4
|
+
if (context.raw.docs.length > 0) {
|
|
5
|
+
const readme = context.raw.docs[0].content;
|
|
6
|
+
const lines = readme.split("\n").slice(0, 10);
|
|
7
|
+
const description = lines.find((line) => line.length > 20 && !line.startsWith("#") && !line.startsWith("-"));
|
|
8
|
+
if (description) {
|
|
9
|
+
parts.push(`Project Description: ${description.trim()}`);
|
|
10
|
+
confidence += 0.4;
|
|
11
|
+
}
|
|
12
|
+
if (readme.toLowerCase().includes("feature")) {
|
|
13
|
+
const featureSection = readme.split("\n").find((line) => line.toLowerCase().includes("feature") && line.startsWith("#"));
|
|
14
|
+
if (featureSection) {
|
|
15
|
+
parts.push("\nKey Features: Based on documentation");
|
|
16
|
+
confidence += 0.2;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (context.insights.projectType) {
|
|
21
|
+
parts.push(`\nProject Type: ${context.insights.projectType}`);
|
|
22
|
+
confidence += 0.2;
|
|
23
|
+
}
|
|
24
|
+
if (context.insights.techStack.frameworks.length > 0) {
|
|
25
|
+
const mainFramework = context.insights.techStack.frameworks[0].name;
|
|
26
|
+
parts.push(`\nBuilt with: ${mainFramework}`);
|
|
27
|
+
confidence += 0.2;
|
|
28
|
+
}
|
|
29
|
+
if (parts.length === 0) {
|
|
30
|
+
parts.push("Unable to automatically determine product details from project context.");
|
|
31
|
+
confidence = 0.3;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
content: parts.join("\n"),
|
|
35
|
+
confidence: Math.min(confidence, 1.0),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function generateProductGuidelines(context) {
|
|
39
|
+
const guidelines = [];
|
|
40
|
+
let confidence = 0.0;
|
|
41
|
+
if (context.insights.techStack.languages.length > 0) {
|
|
42
|
+
const lang = context.insights.techStack.languages[0].name;
|
|
43
|
+
guidelines.push(`- Use ${lang} best practices and conventions`);
|
|
44
|
+
confidence += 0.3;
|
|
45
|
+
}
|
|
46
|
+
if (context.insights.techStack.frameworks.length > 0) {
|
|
47
|
+
const framework = context.insights.techStack.frameworks[0].name;
|
|
48
|
+
guidelines.push(`- Follow ${framework} component patterns`);
|
|
49
|
+
confidence += 0.2;
|
|
50
|
+
}
|
|
51
|
+
if (context.insights.techStack.testing.length > 0) {
|
|
52
|
+
guidelines.push("- Write tests for all new features");
|
|
53
|
+
confidence += 0.2;
|
|
54
|
+
}
|
|
55
|
+
if (context.insights.workflow.commitConvention === "conventional") {
|
|
56
|
+
guidelines.push("- Use conventional commit messages");
|
|
57
|
+
confidence += 0.3;
|
|
58
|
+
}
|
|
59
|
+
if (guidelines.length === 0) {
|
|
60
|
+
guidelines.push("- Follow project coding standards");
|
|
61
|
+
guidelines.push("- Ensure code quality and maintainability");
|
|
62
|
+
confidence = 0.4;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
content: guidelines.join("\n"),
|
|
66
|
+
confidence: Math.min(confidence, 1.0),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function generateTechStack(context) {
|
|
70
|
+
const sections = [];
|
|
71
|
+
let confidence = 0.0;
|
|
72
|
+
if (context.insights.techStack.languages.length > 0) {
|
|
73
|
+
const langs = context.insights.techStack.languages
|
|
74
|
+
.map((l) => `${l.name} (${l.percentage}%)`)
|
|
75
|
+
.join(", ");
|
|
76
|
+
sections.push(`Languages: ${langs}`);
|
|
77
|
+
confidence += 0.3;
|
|
78
|
+
}
|
|
79
|
+
if (context.insights.techStack.frameworks.length > 0) {
|
|
80
|
+
const frameworks = context.insights.techStack.frameworks
|
|
81
|
+
.map((f) => f.name)
|
|
82
|
+
.join(", ");
|
|
83
|
+
sections.push(`Frameworks: ${frameworks}`);
|
|
84
|
+
confidence += 0.3;
|
|
85
|
+
}
|
|
86
|
+
if (context.insights.techStack.testing.length > 0) {
|
|
87
|
+
const testing = context.insights.techStack.testing
|
|
88
|
+
.map((t) => t.framework)
|
|
89
|
+
.join(", ");
|
|
90
|
+
sections.push(`Testing: ${testing}`);
|
|
91
|
+
confidence += 0.2;
|
|
92
|
+
}
|
|
93
|
+
if (context.insights.techStack.databases.length > 0) {
|
|
94
|
+
const dbs = context.insights.techStack.databases
|
|
95
|
+
.map((d) => d.type)
|
|
96
|
+
.join(", ");
|
|
97
|
+
sections.push(`Databases: ${dbs}`);
|
|
98
|
+
confidence += 0.2;
|
|
99
|
+
}
|
|
100
|
+
if (sections.length === 0) {
|
|
101
|
+
sections.push("Unable to automatically detect tech stack from project files.");
|
|
102
|
+
confidence = 0.3;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
content: sections.join("\n"),
|
|
106
|
+
confidence: Math.min(confidence, 1.0),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function generateWorkflow(context) {
|
|
110
|
+
const workflow = [];
|
|
111
|
+
let confidence = 0.0;
|
|
112
|
+
if (context.insights.workflow.commitConvention === "conventional") {
|
|
113
|
+
workflow.push("Commit Convention: Conventional Commits (feat:, fix:, etc.)");
|
|
114
|
+
confidence += 0.3;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
workflow.push("Commit Convention: Standard git commits");
|
|
118
|
+
confidence += 0.1;
|
|
119
|
+
}
|
|
120
|
+
if (context.insights.techStack.testing.length > 0) {
|
|
121
|
+
const testFramework = context.insights.techStack.testing[0].framework;
|
|
122
|
+
workflow.push(`Testing: ${testFramework} for unit and integration tests`);
|
|
123
|
+
confidence += 0.2;
|
|
124
|
+
}
|
|
125
|
+
if (context.raw.cicd.length > 0) {
|
|
126
|
+
workflow.push("CI/CD: Automated testing and deployment via GitHub Actions");
|
|
127
|
+
confidence += 0.2;
|
|
128
|
+
}
|
|
129
|
+
if (context.insights.workflow.testingStrategy && context.insights.workflow.testingStrategy !== "unknown") {
|
|
130
|
+
workflow.push(`Testing Strategy: ${context.insights.workflow.testingStrategy}`);
|
|
131
|
+
confidence += 0.3;
|
|
132
|
+
}
|
|
133
|
+
if (workflow.length === 0) {
|
|
134
|
+
workflow.push("Standard git workflow with feature branches");
|
|
135
|
+
confidence = 0.4;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
content: workflow.join("\n"),
|
|
139
|
+
confidence: Math.min(confidence, 1.0),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|