@codexa/cli 9.0.8 → 9.0.10
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/commands/discover.ts +38 -11
- package/commands/patterns.ts +29 -2
- package/commands/sync.ts +12 -1
- package/context/domains.test.ts +28 -6
- package/context/domains.ts +9 -8
- package/context/generator.ts +2 -0
- package/context/index.ts +3 -0
- package/context/references.test.ts +159 -0
- package/context/references.ts +159 -0
- package/context/sections.ts +16 -0
- package/db/schema.ts +7 -0
- package/detectors/go.ts +55 -1
- package/detectors/node.ts +39 -1
- package/gates/standards-validator.ts +3 -2
- package/package.json +1 -1
- package/workflow.ts +26 -0
package/commands/discover.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type UnifiedDetectionResult,
|
|
13
13
|
} from "../detectors/loader";
|
|
14
14
|
import { CodexaError } from "../errors";
|
|
15
|
+
import { getGrepaiWorkspace } from "./patterns";
|
|
15
16
|
|
|
16
17
|
interface StackDetection {
|
|
17
18
|
frontend?: string;
|
|
@@ -1123,19 +1124,36 @@ export function ensureDeepExploreAgent(): void {
|
|
|
1123
1124
|
console.warn(" Instale com: go install github.com/your-org/grepai@latest");
|
|
1124
1125
|
console.warn(" Sem grepai, o deep-explore usara apenas Grep (menos eficaz).\n");
|
|
1125
1126
|
} else {
|
|
1126
|
-
// Check if grepai index exists for this project
|
|
1127
|
+
// Check if grepai index exists for this project (local GOB or workspace)
|
|
1127
1128
|
const grepaiDir = join(process.cwd(), ".grepai");
|
|
1128
|
-
|
|
1129
|
+
const workspace = getGrepaiWorkspace();
|
|
1130
|
+
if (!existsSync(grepaiDir) && !workspace) {
|
|
1129
1131
|
console.warn("\n⚠ grepai index nao encontrado neste projeto.");
|
|
1130
|
-
console.warn(" Execute: grepai
|
|
1132
|
+
console.warn(" Execute: grepai init && grepai watch");
|
|
1133
|
+
console.warn(" Ou configure um workspace: codexa discover set-stack --grepai-workspace <nome>");
|
|
1131
1134
|
console.warn(" Sem o index, grepai search nao retornara resultados.\n");
|
|
1135
|
+
} else if (workspace) {
|
|
1136
|
+
console.log(`✓ grepai workspace configurado: ${workspace}`);
|
|
1132
1137
|
}
|
|
1133
1138
|
}
|
|
1134
1139
|
|
|
1135
1140
|
// Ensure grepai permissions in target project settings
|
|
1136
1141
|
ensureGrepaiPermissions();
|
|
1137
1142
|
|
|
1138
|
-
if
|
|
1143
|
+
// Check if workspace config changed — regenerate if needed
|
|
1144
|
+
const currentWorkspace = getGrepaiWorkspace();
|
|
1145
|
+
const wsFlag = currentWorkspace ? ` --workspace ${currentWorkspace}` : "";
|
|
1146
|
+
|
|
1147
|
+
if (existsSync(agentPath)) {
|
|
1148
|
+
const existing = readFileSync(agentPath, "utf-8");
|
|
1149
|
+
const hasWorkspace = existing.includes("--workspace ");
|
|
1150
|
+
if ((currentWorkspace && !hasWorkspace) || (!currentWorkspace && hasWorkspace)) {
|
|
1151
|
+
console.log("⟳ Atualizando deep-explore.md com config de workspace...");
|
|
1152
|
+
// Fall through to regenerate
|
|
1153
|
+
} else {
|
|
1154
|
+
return; // Already up to date
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1139
1157
|
|
|
1140
1158
|
const agentsDir = join(process.cwd(), ".claude", "agents");
|
|
1141
1159
|
if (!existsSync(agentsDir)) {
|
|
@@ -1165,7 +1183,7 @@ You are a semantic code exploration agent. Your primary tool is \\\`grepai\\\`
|
|
|
1165
1183
|
**IMPORTANT: Execute ONE grepai search per Bash call. Do NOT run multiple grepai commands in parallel — this causes "Sibling tool call errored". Run them SEQUENTIALLY, one at a time.**
|
|
1166
1184
|
|
|
1167
1185
|
\\\`\\\`\\\`bash
|
|
1168
|
-
grepai search "your query here" --json --compact
|
|
1186
|
+
grepai search "your query here" --json --compact${wsFlag}
|
|
1169
1187
|
\\\`\\\`\\\`
|
|
1170
1188
|
|
|
1171
1189
|
- Queries MUST be in English
|
|
@@ -1176,28 +1194,28 @@ grepai search "your query here" --json --compact
|
|
|
1176
1194
|
Examples (run each one SEPARATELY):
|
|
1177
1195
|
|
|
1178
1196
|
\\\`\\\`\\\`bash
|
|
1179
|
-
grepai search "authentication flow" --json --compact
|
|
1197
|
+
grepai search "authentication flow" --json --compact${wsFlag}
|
|
1180
1198
|
\\\`\\\`\\\`
|
|
1181
1199
|
|
|
1182
1200
|
\\\`\\\`\\\`bash
|
|
1183
|
-
grepai search "error handling middleware" --json --compact
|
|
1201
|
+
grepai search "error handling middleware" --json --compact${wsFlag}
|
|
1184
1202
|
\\\`\\\`\\\`
|
|
1185
1203
|
|
|
1186
1204
|
\\\`\\\`\\\`bash
|
|
1187
|
-
grepai search "database connection management" --json --compact
|
|
1205
|
+
grepai search "database connection management" --json --compact${wsFlag}
|
|
1188
1206
|
\\\`\\\`\\\`
|
|
1189
1207
|
|
|
1190
1208
|
### Step 2: Use grepai trace for relationships
|
|
1191
1209
|
|
|
1192
1210
|
\\\`\\\`\\\`bash
|
|
1193
1211
|
# Find all functions that call a symbol
|
|
1194
|
-
grepai trace callers "HandleRequest" --json
|
|
1212
|
+
grepai trace callers "HandleRequest" --json${wsFlag}
|
|
1195
1213
|
|
|
1196
1214
|
# Find all functions called by a symbol
|
|
1197
|
-
grepai trace callees "ProcessOrder" --json
|
|
1215
|
+
grepai trace callees "ProcessOrder" --json${wsFlag}
|
|
1198
1216
|
|
|
1199
1217
|
# Build complete call graph
|
|
1200
|
-
grepai trace graph "ValidateToken" --depth 3 --json
|
|
1218
|
+
grepai trace graph "ValidateToken" --depth 3 --json${wsFlag}
|
|
1201
1219
|
\\\`\\\`\\\`
|
|
1202
1220
|
|
|
1203
1221
|
### Step 3: Read files identified by grepai
|
|
@@ -1210,6 +1228,15 @@ Use Grep ONLY if you need exact string match (variable name, import path). Never
|
|
|
1210
1228
|
|
|
1211
1229
|
---
|
|
1212
1230
|
|
|
1231
|
+
### Workspace (cross-project search)
|
|
1232
|
+
|
|
1233
|
+
If \\\`--workspace\\\` flag is included in the commands above, this project uses a grepai workspace
|
|
1234
|
+
for cross-project semantic search. The flag was injected automatically by the Codexa CLI.
|
|
1235
|
+
|
|
1236
|
+
- With workspace: searches across ALL projects in the workspace
|
|
1237
|
+
- Without workspace: searches only the local project index
|
|
1238
|
+
- To filter by project: add \\\`--project <name>\\\`
|
|
1239
|
+
|
|
1213
1240
|
### FORBIDDEN
|
|
1214
1241
|
|
|
1215
1242
|
- **DO NOT** use \\\`find\\\` or \\\`ls\\\` to explore code — use \\\`grepai search\\\`
|
package/commands/patterns.ts
CHANGED
|
@@ -33,15 +33,42 @@ export function isGrepaiAvailable(): boolean {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════
|
|
37
|
+
// WORKSPACE CONFIG
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
const WORKSPACE_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
41
|
+
|
|
42
|
+
export function isValidWorkspaceName(name: string): boolean {
|
|
43
|
+
return WORKSPACE_NAME_REGEX.test(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getGrepaiWorkspace(): string | null {
|
|
47
|
+
try {
|
|
48
|
+
initSchema();
|
|
49
|
+
const db = getDb();
|
|
50
|
+
const project = db.query(
|
|
51
|
+
"SELECT grepai_workspace FROM project WHERE id = 'default'"
|
|
52
|
+
).get() as any;
|
|
53
|
+
return project?.grepai_workspace || null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
// ═══════════════════════════════════════════════════════════════
|
|
37
60
|
// BUSCA SEMÂNTICA
|
|
38
61
|
// ═══════════════════════════════════════════════════════════════
|
|
39
62
|
|
|
40
|
-
export function searchWithGrepai(query: string, topK: number = 10): GrepaiResult[] {
|
|
63
|
+
export function searchWithGrepai(query: string, topK: number = 10, workspace?: string): GrepaiResult[] {
|
|
41
64
|
try {
|
|
65
|
+
const args = ["search", query, "--top", topK.toString(), "--format", "json"];
|
|
66
|
+
if (workspace) {
|
|
67
|
+
args.push("--workspace", workspace);
|
|
68
|
+
}
|
|
42
69
|
const result = spawnSync(
|
|
43
70
|
"grepai",
|
|
44
|
-
|
|
71
|
+
args,
|
|
45
72
|
{ encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
|
|
46
73
|
);
|
|
47
74
|
|
package/commands/sync.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { CodexaError } from "../errors";
|
|
5
|
+
import { getGrepaiWorkspace } from "./patterns";
|
|
5
6
|
|
|
6
7
|
// Agents que sao referencias (nao spawnables como subagent_type)
|
|
7
8
|
const REFERENCE_FILES = new Set([
|
|
@@ -71,6 +72,16 @@ export function syncAgents(options: { force?: boolean } = {}): void {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
copyFileSync(src, dst);
|
|
75
|
+
|
|
76
|
+
// Inject workspace flag in deep-explore if configured
|
|
77
|
+
if (file === "deep-explore.md") {
|
|
78
|
+
const workspace = getGrepaiWorkspace();
|
|
79
|
+
const wsFlag = workspace ? ` --workspace ${workspace}` : "";
|
|
80
|
+
let agentContent = readFileSync(dst, "utf-8");
|
|
81
|
+
agentContent = agentContent.replace(/\{\{GREPAI_WS_FLAG\}\}/g, wsFlag);
|
|
82
|
+
writeFileSync(dst, agentContent);
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
copied++;
|
|
75
86
|
}
|
|
76
87
|
|
package/context/domains.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ContextSection } from "./assembly";
|
|
|
11
11
|
function makeSections(): ContextSection[] {
|
|
12
12
|
return [
|
|
13
13
|
{ name: "STANDARDS", content: "...", priority: 1 },
|
|
14
|
+
{ name: "REFERENCIAS", content: "...", priority: 2 },
|
|
14
15
|
{ name: "ALERTAS", content: "...", priority: 2 },
|
|
15
16
|
{ name: "ARQUITETURA", content: "...", priority: 3 },
|
|
16
17
|
{ name: "DECISOES", content: "...", priority: 4 },
|
|
@@ -119,7 +120,7 @@ describe("adjustSectionPriorities", () => {
|
|
|
119
120
|
it("null domain passes all sections through unchanged", () => {
|
|
120
121
|
const sections = makeSections();
|
|
121
122
|
const result = adjustSectionPriorities(sections, null);
|
|
122
|
-
expect(result).toHaveLength(
|
|
123
|
+
expect(result).toHaveLength(13);
|
|
123
124
|
expect(result).toEqual(sections);
|
|
124
125
|
});
|
|
125
126
|
|
|
@@ -127,8 +128,9 @@ describe("adjustSectionPriorities", () => {
|
|
|
127
128
|
const result = adjustSectionPriorities(makeSections(), "explore");
|
|
128
129
|
const names = result.map(s => s.name);
|
|
129
130
|
|
|
130
|
-
// explore profile:
|
|
131
|
+
// explore profile: 8 sections have relevance 0
|
|
131
132
|
expect(names).not.toContain("STANDARDS");
|
|
133
|
+
expect(names).not.toContain("REFERENCIAS");
|
|
132
134
|
expect(names).not.toContain("CONTEXTO DE TASKS ANTERIORES");
|
|
133
135
|
expect(names).not.toContain("GRAPH");
|
|
134
136
|
expect(names).not.toContain("PRODUTO");
|
|
@@ -189,7 +191,7 @@ describe("adjustSectionPriorities", () => {
|
|
|
189
191
|
const result = adjustSectionPriorities(makeSections(), "testing");
|
|
190
192
|
const names = result.map(s => s.name);
|
|
191
193
|
expect(names).not.toContain("PRODUTO");
|
|
192
|
-
expect(result).toHaveLength(
|
|
194
|
+
expect(result).toHaveLength(12);
|
|
193
195
|
});
|
|
194
196
|
|
|
195
197
|
it("testing domain boosts CONTEXTO DE TASKS ANTERIORES (relevance=3)", () => {
|
|
@@ -212,10 +214,30 @@ describe("adjustSectionPriorities", () => {
|
|
|
212
214
|
expect(produto?.priority).toBe(5);
|
|
213
215
|
});
|
|
214
216
|
|
|
215
|
-
it("
|
|
217
|
+
it("backend domain boosts REFERENCIAS (relevance=3)", () => {
|
|
218
|
+
const result = adjustSectionPriorities(makeSections(), "backend");
|
|
219
|
+
const refs = result.find(s => s.name === "REFERENCIAS");
|
|
220
|
+
// backend: REFERENCIAS=3 (base 2 + offset -2 = 0 → floor 1)
|
|
221
|
+
expect(refs?.priority).toBe(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("explore domain excludes REFERENCIAS (relevance=0)", () => {
|
|
225
|
+
const result = adjustSectionPriorities(makeSections(), "explore");
|
|
226
|
+
const names = result.map(s => s.name);
|
|
227
|
+
expect(names).not.toContain("REFERENCIAS");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("architecture domain excludes REFERENCIAS (relevance=0)", () => {
|
|
231
|
+
const result = adjustSectionPriorities(makeSections(), "architecture");
|
|
232
|
+
const names = result.map(s => s.name);
|
|
233
|
+
expect(names).not.toContain("REFERENCIAS");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("security domain excludes PRODUTO and REFERENCIAS (relevance=0)", () => {
|
|
216
237
|
const result = adjustSectionPriorities(makeSections(), "security");
|
|
217
238
|
const names = result.map(s => s.name);
|
|
218
239
|
expect(names).not.toContain("PRODUTO");
|
|
240
|
+
expect(names).not.toContain("REFERENCIAS");
|
|
219
241
|
expect(result).toHaveLength(11);
|
|
220
242
|
});
|
|
221
243
|
|
|
@@ -235,12 +257,12 @@ describe("adjustSectionPriorities", () => {
|
|
|
235
257
|
|
|
236
258
|
describe("DOMAIN_PROFILES completeness", () => {
|
|
237
259
|
const allSectionNames = [
|
|
238
|
-
"STANDARDS", "ALERTAS", "ARQUITETURA", "DECISOES",
|
|
260
|
+
"STANDARDS", "REFERENCIAS", "ALERTAS", "ARQUITETURA", "DECISOES",
|
|
239
261
|
"CONTEXTO DE TASKS ANTERIORES", "GRAPH", "PRODUTO",
|
|
240
262
|
"DISCOVERIES", "UTILITIES", "PATTERNS", "HINTS", "STACK",
|
|
241
263
|
];
|
|
242
264
|
|
|
243
|
-
it("every domain profile covers all
|
|
265
|
+
it("every domain profile covers all 13 sections", () => {
|
|
244
266
|
for (const [domain, profile] of Object.entries(DOMAIN_PROFILES)) {
|
|
245
267
|
for (const section of allSectionNames) {
|
|
246
268
|
expect(profile[section as keyof typeof profile]).toBeDefined();
|
package/context/domains.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type Relevance = 0 | 1 | 2 | 3;
|
|
|
19
19
|
|
|
20
20
|
export type SectionName =
|
|
21
21
|
| "STANDARDS"
|
|
22
|
+
| "REFERENCIAS"
|
|
22
23
|
| "ALERTAS"
|
|
23
24
|
| "ARQUITETURA"
|
|
24
25
|
| "DECISOES"
|
|
@@ -58,42 +59,42 @@ export const AGENT_DOMAIN: Record<string, AgentDomain> = {
|
|
|
58
59
|
|
|
59
60
|
export const DOMAIN_PROFILES: Record<AgentDomain, DomainProfile> = {
|
|
60
61
|
backend: {
|
|
61
|
-
"STANDARDS": 3, "ALERTAS": 3, "ARQUITETURA": 2, "DECISOES": 3,
|
|
62
|
+
"STANDARDS": 3, "REFERENCIAS": 3, "ALERTAS": 3, "ARQUITETURA": 2, "DECISOES": 3,
|
|
62
63
|
"CONTEXTO DE TASKS ANTERIORES": 2, "GRAPH": 2, "PRODUTO": 1,
|
|
63
64
|
"DISCOVERIES": 2, "UTILITIES": 3, "PATTERNS": 2, "HINTS": 2, "STACK": 2,
|
|
64
65
|
},
|
|
65
66
|
frontend: {
|
|
66
|
-
"STANDARDS": 3, "ALERTAS": 3, "ARQUITETURA": 2, "DECISOES": 3,
|
|
67
|
+
"STANDARDS": 3, "REFERENCIAS": 3, "ALERTAS": 3, "ARQUITETURA": 2, "DECISOES": 3,
|
|
67
68
|
"CONTEXTO DE TASKS ANTERIORES": 2, "GRAPH": 2, "PRODUTO": 3,
|
|
68
69
|
"DISCOVERIES": 2, "UTILITIES": 3, "PATTERNS": 3, "HINTS": 2, "STACK": 2,
|
|
69
70
|
},
|
|
70
71
|
database: {
|
|
71
|
-
"STANDARDS": 3, "ALERTAS": 3, "ARQUITETURA": 3, "DECISOES": 3,
|
|
72
|
+
"STANDARDS": 3, "REFERENCIAS": 3, "ALERTAS": 3, "ARQUITETURA": 3, "DECISOES": 3,
|
|
72
73
|
"CONTEXTO DE TASKS ANTERIORES": 2, "GRAPH": 2, "PRODUTO": 0,
|
|
73
74
|
"DISCOVERIES": 2, "UTILITIES": 1, "PATTERNS": 2, "HINTS": 2, "STACK": 3,
|
|
74
75
|
},
|
|
75
76
|
testing: {
|
|
76
|
-
"STANDARDS": 3, "ALERTAS": 3, "ARQUITETURA": 1, "DECISOES": 2,
|
|
77
|
+
"STANDARDS": 3, "REFERENCIAS": 3, "ALERTAS": 3, "ARQUITETURA": 1, "DECISOES": 2,
|
|
77
78
|
"CONTEXTO DE TASKS ANTERIORES": 3, "GRAPH": 1, "PRODUTO": 0,
|
|
78
79
|
"DISCOVERIES": 2, "UTILITIES": 3, "PATTERNS": 3, "HINTS": 2, "STACK": 1,
|
|
79
80
|
},
|
|
80
81
|
review: {
|
|
81
|
-
"STANDARDS": 3, "ALERTAS": 3, "ARQUITETURA": 3, "DECISOES": 3,
|
|
82
|
+
"STANDARDS": 3, "REFERENCIAS": 1, "ALERTAS": 3, "ARQUITETURA": 3, "DECISOES": 3,
|
|
82
83
|
"CONTEXTO DE TASKS ANTERIORES": 2, "GRAPH": 2, "PRODUTO": 1,
|
|
83
84
|
"DISCOVERIES": 2, "UTILITIES": 3, "PATTERNS": 2, "HINTS": 3, "STACK": 2,
|
|
84
85
|
},
|
|
85
86
|
security: {
|
|
86
|
-
"STANDARDS": 3, "ALERTAS": 3, "ARQUITETURA": 2, "DECISOES": 2,
|
|
87
|
+
"STANDARDS": 3, "REFERENCIAS": 0, "ALERTAS": 3, "ARQUITETURA": 2, "DECISOES": 2,
|
|
87
88
|
"CONTEXTO DE TASKS ANTERIORES": 1, "GRAPH": 1, "PRODUTO": 0,
|
|
88
89
|
"DISCOVERIES": 2, "UTILITIES": 1, "PATTERNS": 1, "HINTS": 2, "STACK": 3,
|
|
89
90
|
},
|
|
90
91
|
explore: {
|
|
91
|
-
"STANDARDS": 0, "ALERTAS": 1, "ARQUITETURA": 3, "DECISOES": 1,
|
|
92
|
+
"STANDARDS": 0, "REFERENCIAS": 0, "ALERTAS": 1, "ARQUITETURA": 3, "DECISOES": 1,
|
|
92
93
|
"CONTEXTO DE TASKS ANTERIORES": 0, "GRAPH": 0, "PRODUTO": 0,
|
|
93
94
|
"DISCOVERIES": 1, "UTILITIES": 0, "PATTERNS": 0, "HINTS": 0, "STACK": 3,
|
|
94
95
|
},
|
|
95
96
|
architecture: {
|
|
96
|
-
"STANDARDS": 2, "ALERTAS": 2, "ARQUITETURA": 3, "DECISOES": 3,
|
|
97
|
+
"STANDARDS": 2, "REFERENCIAS": 0, "ALERTAS": 2, "ARQUITETURA": 3, "DECISOES": 3,
|
|
97
98
|
"CONTEXTO DE TASKS ANTERIORES": 2, "GRAPH": 2, "PRODUTO": 3,
|
|
98
99
|
"DISCOVERIES": 2, "UTILITIES": 1, "PATTERNS": 2, "HINTS": 1, "STACK": 3,
|
|
99
100
|
},
|
package/context/generator.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
buildGraphSection,
|
|
19
19
|
buildStackSection,
|
|
20
20
|
buildHintsSection,
|
|
21
|
+
buildReferencesSection,
|
|
21
22
|
} from "./sections";
|
|
22
23
|
|
|
23
24
|
// v9.0: Contexto minimo para subagent (max ~2KB)
|
|
@@ -253,6 +254,7 @@ export function getContextForSubagent(taskId: number): string {
|
|
|
253
254
|
buildProductSection(data),
|
|
254
255
|
buildArchitectureSection(data),
|
|
255
256
|
buildStandardsSection(data),
|
|
257
|
+
buildReferencesSection(data),
|
|
256
258
|
buildDecisionsSection(data),
|
|
257
259
|
buildReasoningSection(data),
|
|
258
260
|
buildAlertsSection(data),
|
package/context/index.ts
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from "fs";
|
|
4
|
+
import { findReferenceFiles } from "./references";
|
|
5
|
+
|
|
6
|
+
// ── Test fixtures ────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const TMP_DIR = join(import.meta.dir, "__test_refs__");
|
|
9
|
+
|
|
10
|
+
function ensureDir(dir: string) {
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeTmpFile(relativePath: string, content: string): string {
|
|
15
|
+
const full = join(TMP_DIR, relativePath);
|
|
16
|
+
ensureDir(join(full, ".."));
|
|
17
|
+
writeFileSync(full, content);
|
|
18
|
+
return relativePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
ensureDir(TMP_DIR);
|
|
23
|
+
|
|
24
|
+
// src/routes/ — 3 route files of different sizes
|
|
25
|
+
writeTmpFile("src/routes/users.ts", "export function getUsers() { return []; }\nexport function createUser() { return {}; }\n// more code here to make it larger\n".repeat(5));
|
|
26
|
+
writeTmpFile("src/routes/products.ts", "export function getProducts() { return []; }\n");
|
|
27
|
+
writeTmpFile("src/routes/orders.ts", "export function getOrders() { return []; }\nexport function createOrder() { return {}; }\n");
|
|
28
|
+
|
|
29
|
+
// src/services/ — 2 service files
|
|
30
|
+
writeTmpFile("src/services/auth.service.ts", "export class AuthService { login() {} logout() {} verify() {} }\n".repeat(3));
|
|
31
|
+
writeTmpFile("src/services/user.service.ts", "export class UserService { getById() {} }\n");
|
|
32
|
+
|
|
33
|
+
// src/components/ — React components
|
|
34
|
+
writeTmpFile("src/components/Button.tsx", "export function Button() { return <button/>; }\n");
|
|
35
|
+
writeTmpFile("src/components/Card.tsx", "export function Card() { return <div/>; }\nexport function CardHeader() { return <div/>; }\n".repeat(3));
|
|
36
|
+
|
|
37
|
+
// src/modules/users/ and src/modules/payments/ — parallel structure
|
|
38
|
+
writeTmpFile("src/modules/users/route.ts", "export function usersRoute() { return {}; }\n".repeat(4));
|
|
39
|
+
writeTmpFile("src/modules/users/service.ts", "export function usersService() {}\n");
|
|
40
|
+
writeTmpFile("src/modules/payments/service.ts", "export function paymentsService() {}\n");
|
|
41
|
+
|
|
42
|
+
// tests/ — test files
|
|
43
|
+
writeTmpFile("tests/routes/users.test.ts", "describe('users', () => { it('works', () => {}); });\n".repeat(2));
|
|
44
|
+
|
|
45
|
+
// node_modules/ — should be ignored
|
|
46
|
+
writeTmpFile("node_modules/pkg/index.ts", "export const x = 1;\n");
|
|
47
|
+
|
|
48
|
+
// Non-code file
|
|
49
|
+
writeTmpFile("docs/readme.md", "# Readme\n");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
rmSync(TMP_DIR, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── Tests ────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe("findReferenceFiles", () => {
|
|
59
|
+
it("finds siblings in the same directory", () => {
|
|
60
|
+
// Target: new file in src/routes/ — should find existing routes
|
|
61
|
+
const refs = findReferenceFiles(["src/routes/payments.ts"], TMP_DIR);
|
|
62
|
+
|
|
63
|
+
expect(refs.length).toBeGreaterThanOrEqual(1);
|
|
64
|
+
expect(refs[0].reason).toBe("mesmo diretorio");
|
|
65
|
+
// Should pick the largest sibling (users.ts has the most content)
|
|
66
|
+
expect(refs[0].path).toBe("src/routes/users.ts");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("prioritizes largest file as reference", () => {
|
|
70
|
+
const refs = findReferenceFiles(["src/components/Modal.tsx"], TMP_DIR);
|
|
71
|
+
|
|
72
|
+
expect(refs.length).toBe(1);
|
|
73
|
+
// Card.tsx is larger than Button.tsx
|
|
74
|
+
expect(refs[0].path).toBe("src/components/Card.tsx");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("finds namesakes when no siblings exist", () => {
|
|
78
|
+
// Target: src/modules/payments/route.ts — no siblings, but src/modules/users/route.ts exists
|
|
79
|
+
const refs = findReferenceFiles(["src/modules/payments/route.ts"], TMP_DIR);
|
|
80
|
+
|
|
81
|
+
expect(refs.length).toBeGreaterThanOrEqual(1);
|
|
82
|
+
const paths = refs.map(r => r.path);
|
|
83
|
+
// Should find the namesake route.ts or a sibling service.ts
|
|
84
|
+
expect(
|
|
85
|
+
paths.some(p => p.includes("route.ts") || p.includes("service.ts"))
|
|
86
|
+
).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns empty for non-code files", () => {
|
|
90
|
+
const refs = findReferenceFiles(["docs/notes.md"], TMP_DIR);
|
|
91
|
+
expect(refs).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns empty for empty task files", () => {
|
|
95
|
+
expect(findReferenceFiles([], TMP_DIR)).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns empty for null/undefined", () => {
|
|
99
|
+
expect(findReferenceFiles(null as any, TMP_DIR)).toEqual([]);
|
|
100
|
+
expect(findReferenceFiles(undefined as any, TMP_DIR)).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("ignores node_modules", () => {
|
|
104
|
+
const refs = findReferenceFiles(["node_modules/pkg/new.ts"], TMP_DIR);
|
|
105
|
+
// Even if node_modules has .ts files, targets in ignored dirs get no refs
|
|
106
|
+
// (the target itself is filtered by extension check, not by ignored dir)
|
|
107
|
+
// But results should never point INTO node_modules
|
|
108
|
+
for (const ref of refs) {
|
|
109
|
+
expect(ref.path).not.toContain("node_modules");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("limits to 3 references max", () => {
|
|
114
|
+
const refs = findReferenceFiles([
|
|
115
|
+
"src/routes/payments.ts",
|
|
116
|
+
"src/services/payment.service.ts",
|
|
117
|
+
"src/components/Modal.tsx",
|
|
118
|
+
"tests/routes/products.test.ts",
|
|
119
|
+
], TMP_DIR);
|
|
120
|
+
|
|
121
|
+
expect(refs.length).toBeLessThanOrEqual(3);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("deduplicates references", () => {
|
|
125
|
+
const refs = findReferenceFiles([
|
|
126
|
+
"src/routes/payments.ts",
|
|
127
|
+
"src/routes/invoices.ts",
|
|
128
|
+
], TMP_DIR);
|
|
129
|
+
|
|
130
|
+
const paths = refs.map(r => r.path);
|
|
131
|
+
const unique = new Set(paths);
|
|
132
|
+
expect(paths.length).toBe(unique.size);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("handles Windows-style backslash paths", () => {
|
|
136
|
+
const refs = findReferenceFiles(["src\\routes\\payments.ts"], TMP_DIR);
|
|
137
|
+
expect(refs.length).toBeGreaterThanOrEqual(1);
|
|
138
|
+
// All returned paths should use forward slashes
|
|
139
|
+
for (const ref of refs) {
|
|
140
|
+
expect(ref.path).not.toContain("\\");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("finds reference for test files", () => {
|
|
145
|
+
const refs = findReferenceFiles(["tests/routes/products.test.ts"], TMP_DIR);
|
|
146
|
+
expect(refs.length).toBeGreaterThanOrEqual(1);
|
|
147
|
+
expect(refs[0].path).toContain("users.test.ts");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("uses parent directory as fallback", () => {
|
|
151
|
+
// Target in a new subdirectory with no siblings or namesakes
|
|
152
|
+
writeTmpFile("src/features/billing/handler.ts", "export function handle() {}\n");
|
|
153
|
+
const refs = findReferenceFiles(["src/features/billing/processor.ts"], TMP_DIR);
|
|
154
|
+
// Should find handler.ts as sibling
|
|
155
|
+
if (refs.length > 0) {
|
|
156
|
+
expect(refs[0].path).toContain("billing");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { join, dirname, basename, extname, resolve, relative } from "path";
|
|
2
|
+
import { globSync } from "fs";
|
|
3
|
+
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════
|
|
5
|
+
// REFERENCE FILES — Pattern by Example (v9.5)
|
|
6
|
+
//
|
|
7
|
+
// Finds existing files similar to task targets so subagents can
|
|
8
|
+
// read them and replicate the coding style exactly.
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
export interface ReferenceFile {
|
|
12
|
+
path: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const IGNORED_DIRS = new Set([
|
|
17
|
+
"node_modules", ".git", "dist", "build", ".next", ".codexa",
|
|
18
|
+
".nuxt", ".output", "coverage", "__pycache__", ".venv", "vendor",
|
|
19
|
+
"target", "bin", "obj",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const CODE_EXTENSIONS = new Set([
|
|
23
|
+
".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte",
|
|
24
|
+
".go", ".rs", ".py", ".cs", ".kt", ".java",
|
|
25
|
+
".dart", ".php", ".rb", ".swift",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizePath(p: string): string {
|
|
29
|
+
return p.replace(/\\/g, "/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isIgnored(filePath: string): boolean {
|
|
33
|
+
const parts = normalizePath(filePath).split("/");
|
|
34
|
+
return parts.some(part => IGNORED_DIRS.has(part));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getFileSize(filePath: string): number {
|
|
38
|
+
try {
|
|
39
|
+
return Bun.file(filePath).size;
|
|
40
|
+
} catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fileExists(filePath: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
return Bun.file(filePath).size > 0;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function globFiles(pattern: string, cwd: string): string[] {
|
|
54
|
+
try {
|
|
55
|
+
const glob = new Bun.Glob(pattern);
|
|
56
|
+
const results: string[] = [];
|
|
57
|
+
for (const match of glob.scanSync({ cwd, dot: false })) {
|
|
58
|
+
const full = join(cwd, match);
|
|
59
|
+
if (!isIgnored(full)) {
|
|
60
|
+
results.push(normalizePath(full));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function pickLargest(files: string[]): string | null {
|
|
70
|
+
if (files.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
let best = files[0];
|
|
73
|
+
let bestSize = getFileSize(files[0]);
|
|
74
|
+
|
|
75
|
+
for (let i = 1; i < files.length; i++) {
|
|
76
|
+
const size = getFileSize(files[i]);
|
|
77
|
+
if (size > bestSize) {
|
|
78
|
+
best = files[i];
|
|
79
|
+
bestSize = size;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return bestSize > 0 ? best : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find reference files for a set of task target files.
|
|
88
|
+
*
|
|
89
|
+
* Strategy per target file:
|
|
90
|
+
* 1. Siblings — other files in the same directory with same extension
|
|
91
|
+
* 2. Namesakes — files with same basename in other directories
|
|
92
|
+
* 3. Parent — files with same extension in parent directory tree
|
|
93
|
+
*
|
|
94
|
+
* Returns max 3 deduplicated reference file paths.
|
|
95
|
+
*/
|
|
96
|
+
export function findReferenceFiles(
|
|
97
|
+
taskFiles: string[],
|
|
98
|
+
projectRoot: string,
|
|
99
|
+
): ReferenceFile[] {
|
|
100
|
+
if (!taskFiles || taskFiles.length === 0) return [];
|
|
101
|
+
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
const results: ReferenceFile[] = [];
|
|
104
|
+
const normalizedRoot = normalizePath(resolve(projectRoot));
|
|
105
|
+
|
|
106
|
+
for (const rawTarget of taskFiles) {
|
|
107
|
+
if (results.length >= 3) break;
|
|
108
|
+
|
|
109
|
+
const target = normalizePath(resolve(projectRoot, rawTarget));
|
|
110
|
+
const ext = extname(target);
|
|
111
|
+
const base = basename(target);
|
|
112
|
+
const dir = dirname(target);
|
|
113
|
+
|
|
114
|
+
if (!ext || !CODE_EXTENSIONS.has(ext)) continue;
|
|
115
|
+
|
|
116
|
+
// Strategy 1: Siblings (same directory, same extension)
|
|
117
|
+
const siblings = globFiles(`*${ext}`, dir)
|
|
118
|
+
.filter(f => f !== target && !seen.has(f) && fileExists(f));
|
|
119
|
+
|
|
120
|
+
const bestSibling = pickLargest(siblings);
|
|
121
|
+
if (bestSibling) {
|
|
122
|
+
const rel = normalizePath(relative(normalizedRoot, bestSibling));
|
|
123
|
+
seen.add(bestSibling);
|
|
124
|
+
results.push({ path: rel, reason: "mesmo diretorio" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Strategy 2: Namesakes (same filename in other directories)
|
|
129
|
+
const namesakes = globFiles(`**/${base}`, normalizedRoot)
|
|
130
|
+
.filter(f => f !== target && !seen.has(f) && fileExists(f));
|
|
131
|
+
|
|
132
|
+
if (namesakes.length > 0) {
|
|
133
|
+
// Pick the one closest by path depth similarity
|
|
134
|
+
const bestNamesake = pickLargest(namesakes);
|
|
135
|
+
if (bestNamesake) {
|
|
136
|
+
const rel = normalizePath(relative(normalizedRoot, bestNamesake));
|
|
137
|
+
seen.add(bestNamesake);
|
|
138
|
+
results.push({ path: rel, reason: "mesmo tipo de arquivo" });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Strategy 3: Parent directory search (same extension, one level up)
|
|
144
|
+
const parentDir = dirname(dir);
|
|
145
|
+
if (parentDir !== dir) {
|
|
146
|
+
const parentFiles = globFiles(`**/*${ext}`, parentDir)
|
|
147
|
+
.filter(f => f !== target && !seen.has(f) && fileExists(f));
|
|
148
|
+
|
|
149
|
+
const bestParent = pickLargest(parentFiles);
|
|
150
|
+
if (bestParent) {
|
|
151
|
+
const rel = normalizePath(relative(normalizedRoot, bestParent));
|
|
152
|
+
seen.add(bestParent);
|
|
153
|
+
results.push({ path: rel, reason: "arquivo similar proximo" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return results;
|
|
159
|
+
}
|
package/context/sections.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getUtilitiesForContext, getAgentHints } from "../db/schema";
|
|
2
2
|
import type { ContextSection, ContextData } from "./assembly";
|
|
3
3
|
import { getAgentDomain, domainToScope } from "./domains";
|
|
4
|
+
import { findReferenceFiles } from "./references";
|
|
4
5
|
|
|
5
6
|
// ── Section Builders ──────────────────────────────────────────
|
|
6
7
|
|
|
@@ -245,3 +246,18 @@ ${hints.map(h => `- ${h}`).join("\n")}
|
|
|
245
246
|
`;
|
|
246
247
|
return { name: "HINTS", content, priority: 10 };
|
|
247
248
|
}
|
|
249
|
+
|
|
250
|
+
export function buildReferencesSection(data: ContextData): ContextSection | null {
|
|
251
|
+
if (data.taskFiles.length === 0) return null;
|
|
252
|
+
|
|
253
|
+
const projectRoot = process.cwd();
|
|
254
|
+
const refs = findReferenceFiles(data.taskFiles, projectRoot);
|
|
255
|
+
if (refs.length === 0) return null;
|
|
256
|
+
|
|
257
|
+
const content = `
|
|
258
|
+
### REFERENCIA DE ESTILO (${refs.length})
|
|
259
|
+
**ANTES de criar/modificar arquivos, leia estas referencias e replique o mesmo estilo** (nomenclatura, estrutura, imports, error handling):
|
|
260
|
+
${refs.map(r => `- \`${r.path}\` (${r.reason})`).join("\n")}
|
|
261
|
+
`;
|
|
262
|
+
return { name: "REFERENCIAS", content, priority: 2 };
|
|
263
|
+
}
|
package/db/schema.ts
CHANGED
|
@@ -480,6 +480,13 @@ const MIGRATIONS: Migration[] = [
|
|
|
480
480
|
db.exec(`ALTER TABLE project ADD COLUMN typecheck_command TEXT`);
|
|
481
481
|
},
|
|
482
482
|
},
|
|
483
|
+
{
|
|
484
|
+
version: "9.8.0",
|
|
485
|
+
description: "Adicionar grepai_workspace na tabela project para suporte a workspace cross-project",
|
|
486
|
+
up: (db) => {
|
|
487
|
+
db.exec(`ALTER TABLE project ADD COLUMN grepai_workspace TEXT`);
|
|
488
|
+
},
|
|
489
|
+
},
|
|
483
490
|
];
|
|
484
491
|
|
|
485
492
|
export function runMigrations(): void {
|
package/detectors/go.ts
CHANGED
|
@@ -55,6 +55,37 @@ function parseGoMod(content: string): GoMod {
|
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
interface GoWork {
|
|
59
|
+
goVersion?: string;
|
|
60
|
+
uses: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseGoWork(content: string): GoWork {
|
|
64
|
+
const result: GoWork = { uses: [] };
|
|
65
|
+
|
|
66
|
+
const goMatch = content.match(/^go\s+(\d+\.\d+(?:\.\d+)?)$/m);
|
|
67
|
+
if (goMatch) result.goVersion = goMatch[1];
|
|
68
|
+
|
|
69
|
+
// Parse use block: use ( ./path1 \n ./path2 )
|
|
70
|
+
const useBlockMatch = content.match(/use\s*\(([\s\S]*?)\)/);
|
|
71
|
+
if (useBlockMatch) {
|
|
72
|
+
for (const line of useBlockMatch[1].split("\n")) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (trimmed && !trimmed.startsWith("//")) {
|
|
75
|
+
result.uses.push(trimmed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Parse single-line: use ./path
|
|
81
|
+
const singleUses = content.matchAll(/^use\s+(\S+)\s*$/gm);
|
|
82
|
+
for (const match of singleUses) {
|
|
83
|
+
result.uses.push(match[1].trim());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
const goDetector: Detector = {
|
|
59
90
|
name: "go",
|
|
60
91
|
ecosystem: "go",
|
|
@@ -117,12 +148,35 @@ const goDetector: Detector = {
|
|
|
117
148
|
category: "runtime",
|
|
118
149
|
});
|
|
119
150
|
|
|
120
|
-
// Create dependency lookup
|
|
151
|
+
// Create dependency lookup from root go.mod
|
|
121
152
|
const deps = new Map<string, string>();
|
|
122
153
|
for (const req of goMod.require) {
|
|
123
154
|
deps.set(req.path, req.version);
|
|
124
155
|
}
|
|
125
156
|
|
|
157
|
+
// Merge workspace module dependencies from go.work
|
|
158
|
+
const goWorkPath = join(cwd, "go.work");
|
|
159
|
+
if (fileExists(goWorkPath)) {
|
|
160
|
+
const goWorkContent = readText(goWorkPath);
|
|
161
|
+
if (goWorkContent) {
|
|
162
|
+
const goWork = parseGoWork(goWorkContent);
|
|
163
|
+
for (const usePath of goWork.uses) {
|
|
164
|
+
const wsModPath = join(cwd, usePath, "go.mod");
|
|
165
|
+
if (fileExists(wsModPath)) {
|
|
166
|
+
const wsContent = readText(wsModPath);
|
|
167
|
+
if (wsContent) {
|
|
168
|
+
const wsMod = parseGoMod(wsContent);
|
|
169
|
+
for (const req of wsMod.require) {
|
|
170
|
+
if (!deps.has(req.path)) {
|
|
171
|
+
deps.set(req.path, req.version);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
126
180
|
// Web Framework Detection
|
|
127
181
|
const webFrameworks = [
|
|
128
182
|
{ paths: ["github.com/gin-gonic/gin"], name: "Gin", confidence: 1.0 },
|
package/detectors/node.ts
CHANGED
|
@@ -22,6 +22,32 @@ import {
|
|
|
22
22
|
readText,
|
|
23
23
|
} from "./index";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Resolve workspace glob patterns from package.json workspaces or pnpm-workspace.yaml.
|
|
27
|
+
* Returns array of glob patterns (e.g. ["apps/*", "packages/*"]).
|
|
28
|
+
*/
|
|
29
|
+
function resolveWorkspacePatterns(cwd: string, pkg: PackageJson): string[] {
|
|
30
|
+
// npm/yarn: workspaces as array or { packages: [] }
|
|
31
|
+
if (pkg.workspaces) {
|
|
32
|
+
if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
|
|
33
|
+
if (pkg.workspaces.packages) return pkg.workspaces.packages;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// pnpm: pnpm-workspace.yaml
|
|
37
|
+
const pnpmPath = join(cwd, "pnpm-workspace.yaml");
|
|
38
|
+
const content = readText(pnpmPath);
|
|
39
|
+
if (content) {
|
|
40
|
+
const patterns: string[] = [];
|
|
41
|
+
for (const line of content.split("\n")) {
|
|
42
|
+
const match = line.match(/^\s*-\s*['"]?([^'"#]+)['"]?\s*$/);
|
|
43
|
+
if (match) patterns.push(match[1].trim());
|
|
44
|
+
}
|
|
45
|
+
return patterns;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
interface PackageJson {
|
|
26
52
|
name?: string;
|
|
27
53
|
version?: string;
|
|
@@ -93,12 +119,24 @@ const nodeDetector: Detector = {
|
|
|
93
119
|
const pkg: PackageJson | null = readJson(pkgPath);
|
|
94
120
|
if (!pkg) return null;
|
|
95
121
|
|
|
96
|
-
const allDeps = {
|
|
122
|
+
const allDeps: Record<string, string> = {
|
|
97
123
|
...pkg.dependencies,
|
|
98
124
|
...pkg.devDependencies,
|
|
99
125
|
...pkg.peerDependencies,
|
|
100
126
|
};
|
|
101
127
|
|
|
128
|
+
// Merge workspace package dependencies
|
|
129
|
+
const workspacePatterns = resolveWorkspacePatterns(cwd, pkg);
|
|
130
|
+
for (const pattern of workspacePatterns) {
|
|
131
|
+
const pkgFiles = findFiles(cwd, `${pattern}/package.json`);
|
|
132
|
+
for (const relPath of pkgFiles) {
|
|
133
|
+
const wsPkg: PackageJson | null = readJson(join(cwd, relPath));
|
|
134
|
+
if (wsPkg) {
|
|
135
|
+
Object.assign(allDeps, wsPkg.dependencies, wsPkg.devDependencies);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
102
140
|
// Detect package manager from lock files
|
|
103
141
|
if (fileExists(join(cwd, "bun.lockb"))) {
|
|
104
142
|
technologies.push({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { basename, resolve } from "path";
|
|
4
|
-
import { isGrepaiAvailable, searchWithGrepai, type GrepaiResult } from "../commands/patterns";
|
|
4
|
+
import { isGrepaiAvailable, searchWithGrepai, getGrepaiWorkspace, type GrepaiResult } from "../commands/patterns";
|
|
5
5
|
|
|
6
6
|
// ═══════════════════════════════════════════════════════════════
|
|
7
7
|
// INTERFACES
|
|
@@ -119,10 +119,11 @@ function validateSemantic(
|
|
|
119
119
|
violations: Violation[],
|
|
120
120
|
warnings: Violation[]
|
|
121
121
|
): void {
|
|
122
|
+
const workspace = getGrepaiWorkspace() ?? undefined;
|
|
122
123
|
for (const std of standards) {
|
|
123
124
|
if (!std.semantic_query) continue;
|
|
124
125
|
|
|
125
|
-
const results = searchWithGrepai(std.semantic_query, 20);
|
|
126
|
+
const results = searchWithGrepai(std.semantic_query, 20, workspace);
|
|
126
127
|
|
|
127
128
|
// Filtrar: apenas arquivos sendo validados + score acima do threshold
|
|
128
129
|
const matchingFiles = results.filter(r => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexa/cli",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.10",
|
|
4
4
|
"description": "Orchestrated workflow system for Claude Code - manages feature development through parallel subagents with structured phases, gates, and quality enforcement.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/workflow.ts
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
discoverRefreshPatterns,
|
|
24
24
|
discoverExportPatterns,
|
|
25
25
|
discoverIncremental,
|
|
26
|
+
ensureDeepExploreAgent,
|
|
26
27
|
} from "./commands/discover";
|
|
28
|
+
import { isValidWorkspaceName } from "./commands/patterns";
|
|
27
29
|
import { clearTasks, clearShow } from "./commands/clear";
|
|
28
30
|
import { standardsList, standardsAdd, standardsEdit, standardsRemove, standardsExport } from "./commands/standards";
|
|
29
31
|
import { productImport, productSet, productConfirm, productShow, productReset } from "./commands/product";
|
|
@@ -553,6 +555,7 @@ discoverCmd
|
|
|
553
555
|
.option("--auth <value>", "Auth (supabase, next-auth, clerk)")
|
|
554
556
|
.option("--testing <value>", "Testing (vitest, jest, playwright)")
|
|
555
557
|
.option("--typecheck-command <cmd>", "Comando custom de typecheck (ex: 'mypy --strict', 'go vet ./...'). Vazio para usar preset automatico.")
|
|
558
|
+
.option("--grepai-workspace <name>", "Nome do workspace grepai para busca semantica cross-project. Vazio para remover.")
|
|
556
559
|
.action((options) => {
|
|
557
560
|
// Tratar typecheck-command separadamente
|
|
558
561
|
if (options.typecheckCommand !== undefined) {
|
|
@@ -569,9 +572,32 @@ discoverCmd
|
|
|
569
572
|
}
|
|
570
573
|
console.log(" Sera usado no gate 4.5 (typecheck-pass) em cada task done.\n");
|
|
571
574
|
}
|
|
575
|
+
// Tratar grepai-workspace separadamente
|
|
576
|
+
if (options.grepaiWorkspace !== undefined) {
|
|
577
|
+
const { initSchema } = require("./db/schema");
|
|
578
|
+
const { getDb } = require("./db/connection");
|
|
579
|
+
initSchema();
|
|
580
|
+
const db = getDb();
|
|
581
|
+
const value = options.grepaiWorkspace === "" ? null : options.grepaiWorkspace;
|
|
582
|
+
if (value && !isValidWorkspaceName(value)) {
|
|
583
|
+
console.error(`\n[erro] Nome de workspace invalido: "${value}"`);
|
|
584
|
+
console.error(" Use apenas letras, numeros, hifens e underscores (ex: meu-workspace, dotnet_ws)\n");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
db.run("UPDATE project SET grepai_workspace = ? WHERE id = 'default'", [value]);
|
|
588
|
+
if (value) {
|
|
589
|
+
console.log(`\n✓ Grepai workspace configurado: ${value}`);
|
|
590
|
+
console.log(" Sera usado em: semantic search (standards), deep-explore agent, grepai trace");
|
|
591
|
+
} else {
|
|
592
|
+
console.log(`\n✓ Grepai workspace removido (usando index local .grepai/)`);
|
|
593
|
+
}
|
|
594
|
+
// Auto-regenerate deep-explore agent with new workspace config
|
|
595
|
+
ensureDeepExploreAgent();
|
|
596
|
+
}
|
|
572
597
|
// Tratar stack options normalmente
|
|
573
598
|
const stackOptions = { ...options };
|
|
574
599
|
delete stackOptions.typecheckCommand;
|
|
600
|
+
delete stackOptions.grepaiWorkspace;
|
|
575
601
|
if (Object.keys(stackOptions).length > 0) {
|
|
576
602
|
discoverSetStack(stackOptions);
|
|
577
603
|
}
|