@codexa/cli 9.0.2 → 9.0.3

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.
@@ -1,17 +1,5 @@
1
1
  import { getDb } from "../db/connection";
2
- import { initSchema } from "../db/schema";
3
-
4
- function getNextDecisionId(specId: string): string {
5
- const db = getDb();
6
- const last = db
7
- .query("SELECT id FROM decisions WHERE spec_id = ? ORDER BY created_at DESC LIMIT 1")
8
- .get(specId) as any;
9
-
10
- if (!last) return "DEC-001";
11
-
12
- const num = parseInt(last.id.replace("DEC-", "")) + 1;
13
- return `DEC-${num.toString().padStart(3, "0")}`;
14
- }
2
+ import { initSchema, getNextDecisionId } from "../db/schema";
15
3
 
16
4
  interface ConflictAnalysis {
17
5
  hasConflict: boolean;
@@ -182,21 +170,33 @@ export function decide(title: string, decision: string, options: { rationale?: s
182
170
  .query("SELECT * FROM tasks WHERE spec_id = ? AND status = 'running' LIMIT 1")
183
171
  .get(spec.id) as any;
184
172
 
185
- const decisionId = getNextDecisionId(spec.id);
186
-
187
- db.run(
188
- `INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
189
- VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
190
- [
191
- decisionId,
192
- spec.id,
193
- currentTask?.number || null,
194
- title,
195
- decision,
196
- options.rationale || null,
197
- now,
198
- ]
199
- );
173
+ let decisionId = "";
174
+ let retries = 3;
175
+ while (retries > 0) {
176
+ decisionId = getNextDecisionId(spec.id);
177
+ try {
178
+ db.run(
179
+ `INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
180
+ VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
181
+ [
182
+ decisionId,
183
+ spec.id,
184
+ currentTask?.number || null,
185
+ title,
186
+ decision,
187
+ options.rationale || null,
188
+ now,
189
+ ]
190
+ );
191
+ break;
192
+ } catch (e: any) {
193
+ if (e.message?.includes("UNIQUE constraint") && retries > 1) {
194
+ retries--;
195
+ continue;
196
+ }
197
+ throw e;
198
+ }
199
+ }
200
200
 
201
201
  console.log(`\nDecisao registrada: ${decisionId}`);
202
202
  console.log(`Titulo: ${title}`);
@@ -183,9 +183,9 @@ export function discoverConfirm(): void {
183
183
  // Mover de pending para default
184
184
  db.run("DELETE FROM project WHERE id = 'pending'");
185
185
  db.run(
186
- `INSERT INTO project (id, name, stack, discovered_at, updated_at, cli_version)
187
- VALUES ('default', ?, ?, ?, ?, ?)`,
188
- ["Projeto", JSON.stringify(data.stack), now, now, pkg.version]
186
+ `INSERT INTO project (id, name, stack, discovered_at, updated_at, cli_version, last_discover_at)
187
+ VALUES ('default', ?, ?, ?, ?, ?, ?)`,
188
+ ["Projeto", JSON.stringify(data.stack), now, now, pkg.version, now]
189
189
  );
190
190
 
191
191
  // Criar standards baseados na estrutura detectada
@@ -460,6 +460,131 @@ export async function discoverRefresh(options: { force?: boolean } = {}): Promis
460
460
  }
461
461
  }
462
462
 
463
+ export async function discoverIncremental(): Promise<void> {
464
+ initSchema();
465
+ const db = getDb();
466
+
467
+ const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
468
+ if (!project) {
469
+ console.error("\nProjeto nao descoberto.");
470
+ console.error("Execute: discover start primeiro\n");
471
+ process.exit(1);
472
+ }
473
+
474
+ // Data de referencia: last_discover_at > discovered_at > fallback 30 dias
475
+ const sinceDate = project.last_discover_at || project.discovered_at ||
476
+ new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
477
+
478
+ console.log(`\nDiscover incremental desde: ${sinceDate}\n`);
479
+
480
+ // Listar arquivos modificados via git
481
+ const gitResult = spawnSync("git", [
482
+ "diff", "--name-only", "--diff-filter=ACMR",
483
+ `--since=${sinceDate}`, "HEAD"
484
+ ], { encoding: "utf-8", timeout: 10000 });
485
+
486
+ // Fallback: se --since nao funciona com diff, usar log
487
+ let modifiedFiles: string[] = [];
488
+ if (gitResult.status !== 0 || !gitResult.stdout.trim()) {
489
+ const logResult = spawnSync("git", [
490
+ "log", "--since", sinceDate, "--name-only", "--pretty=format:", "--diff-filter=ACMR"
491
+ ], { encoding: "utf-8", timeout: 10000 });
492
+
493
+ if (logResult.status === 0 && logResult.stdout.trim()) {
494
+ modifiedFiles = [...new Set(logResult.stdout.trim().split("\n").filter(f => f.trim()))];
495
+ }
496
+ } else {
497
+ modifiedFiles = [...new Set(gitResult.stdout.trim().split("\n").filter(f => f.trim()))];
498
+ }
499
+
500
+ // Filtrar apenas arquivos que ainda existem e sao codigo
501
+ const codeExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".cs", ".dart"];
502
+ const existingFiles = modifiedFiles.filter(f => {
503
+ const fullPath = join(process.cwd(), f);
504
+ return existsSync(fullPath) && codeExtensions.some(ext => f.endsWith(ext));
505
+ });
506
+
507
+ if (existingFiles.length === 0) {
508
+ console.log("Nenhum arquivo de codigo modificado desde ultimo discover.");
509
+ console.log("Stack e utilities estao atualizados.\n");
510
+
511
+ // Atualizar timestamp mesmo sem mudancas
512
+ const now = new Date().toISOString();
513
+ db.run("UPDATE project SET last_discover_at = ? WHERE id = 'default'", [now]);
514
+ return;
515
+ }
516
+
517
+ console.log(`[1/3] Analisando ${existingFiles.length} arquivo(s) modificado(s)...`);
518
+
519
+ // Importar funcoes de patterns
520
+ const { extractUtilitiesFromFile, inferScopeFromPath } = await import("./patterns");
521
+ const { upsertUtility } = await import("../db/schema");
522
+
523
+ // Extrair utilities dos arquivos modificados
524
+ let utilitiesUpdated = 0;
525
+ for (const file of existingFiles) {
526
+ const fullPath = join(process.cwd(), file);
527
+ const utilities = extractUtilitiesFromFile(fullPath);
528
+ const scope = inferScopeFromPath(file);
529
+
530
+ for (const util of utilities) {
531
+ upsertUtility({
532
+ filePath: file,
533
+ utilityName: util.name,
534
+ utilityType: util.type,
535
+ scope,
536
+ signature: util.signature,
537
+ });
538
+ utilitiesUpdated++;
539
+ }
540
+ }
541
+
542
+ console.log(` ✓ ${utilitiesUpdated} utilities registradas/atualizadas`);
543
+
544
+ // Re-detectar stack e comparar
545
+ console.log("\n[2/3] Verificando mudancas no stack...");
546
+ const currentStack = JSON.parse(project.stack);
547
+ const newStack = await detectStack();
548
+
549
+ const stackChanges: { key: string; from: string | undefined; to: string | undefined }[] = [];
550
+ const allKeys = new Set([...Object.keys(currentStack), ...Object.keys(newStack)]);
551
+
552
+ for (const key of allKeys) {
553
+ const current = currentStack[key as keyof StackDetection];
554
+ const detected = newStack[key as keyof StackDetection];
555
+ if (current !== detected) {
556
+ stackChanges.push({ key, from: current, to: detected });
557
+ }
558
+ }
559
+
560
+ if (stackChanges.length > 0) {
561
+ console.log(" ⚠ Mudancas detectadas no stack:");
562
+ for (const change of stackChanges) {
563
+ const from = change.from || "(nao definido)";
564
+ const to = change.to || "(removido)";
565
+ console.log(` ${change.key}: ${from} → ${to}`);
566
+ }
567
+ console.log(" Use: discover refresh --force para aplicar");
568
+ } else {
569
+ console.log(" ✓ Stack inalterado");
570
+ }
571
+
572
+ // Atualizar timestamp
573
+ console.log("\n[3/3] Atualizando timestamp...");
574
+ const now = new Date().toISOString();
575
+ db.run("UPDATE project SET last_discover_at = ?, updated_at = ? WHERE id = 'default'", [now, now]);
576
+
577
+ // Resumo
578
+ console.log("\n" + "═".repeat(50));
579
+ console.log("DISCOVER INCREMENTAL CONCLUIDO");
580
+ console.log("═".repeat(50));
581
+ console.log(` Arquivos analisados: ${existingFiles.length}`);
582
+ console.log(` Utilities atualizadas: ${utilitiesUpdated}`);
583
+ console.log(` Mudancas no stack: ${stackChanges.length}`);
584
+ console.log(` Timestamp atualizado: ${now}`);
585
+ console.log("═".repeat(50) + "\n");
586
+ }
587
+
463
588
  function generateStandardsMarkdown(): void {
464
589
  const db = getDb();
465
590
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
@@ -1,5 +1,5 @@
1
1
  import { getDb } from "../db/connection";
2
- import { initSchema, getRelatedDecisions, getRelatedFiles, findContradictions } from "../db/schema";
2
+ import { initSchema, getRelatedDecisions, getRelatedFiles } from "../db/schema";
3
3
 
4
4
  type KnowledgeCategory = "discovery" | "decision" | "blocker" | "pattern" | "constraint";
5
5
  type KnowledgeSeverity = "info" | "warning" | "critical";
@@ -233,7 +233,6 @@ export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[]
233
233
  export function queryGraph(options: {
234
234
  file?: string;
235
235
  decision?: string;
236
- contradictions?: boolean;
237
236
  json?: boolean;
238
237
  }): void {
239
238
  initSchema();
@@ -245,29 +244,6 @@ export function queryGraph(options: {
245
244
  process.exit(1);
246
245
  }
247
246
 
248
- // Buscar contradicoes
249
- if (options.contradictions) {
250
- const contradictions = findContradictions(spec.id);
251
-
252
- if (options.json) {
253
- console.log(JSON.stringify({ contradictions }));
254
- return;
255
- }
256
-
257
- if (contradictions.length === 0) {
258
- console.log("\nNenhuma contradicao detectada.\n");
259
- return;
260
- }
261
-
262
- console.log(`\nContradicoes detectadas (${contradictions.length}):`);
263
- console.log(`${"─".repeat(50)}`);
264
- for (const c of contradictions) {
265
- console.log(` [!] "${c.decision1}" <-> "${c.decision2}"`);
266
- console.log(` Detectada em: ${c.createdAt}\n`);
267
- }
268
- return;
269
- }
270
-
271
247
  // Buscar relacoes para um arquivo
272
248
  if (options.file) {
273
249
  const decisions = getRelatedDecisions(options.file, "file");
@@ -357,8 +333,7 @@ export function queryGraph(options: {
357
333
  }
358
334
  console.log(`\nComandos:`);
359
335
  console.log(` knowledge graph --file <path> Relacoes de um arquivo`);
360
- console.log(` knowledge graph --decision <id> Arquivos afetados por decisao`);
361
- console.log(` knowledge graph --contradictions Detectar contradicoes\n`);
336
+ console.log(` knowledge graph --decision <id> Arquivos afetados por decisao\n`);
362
337
  }
363
338
 
364
339
  // v9.0: Resolver/reconhecer knowledge critico (para desbloquear task start)
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, afterAll } from "bun:test";
2
+ import { extractUtilitiesFromFile, inferScopeFromPath } from "./patterns";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const TMP_DIR = join(import.meta.dir, "__test_tmp__");
7
+
8
+ function setupTmpDir() {
9
+ mkdirSync(TMP_DIR, { recursive: true });
10
+ }
11
+
12
+ function cleanTmpDir() {
13
+ try { rmSync(TMP_DIR, { recursive: true }); } catch {}
14
+ }
15
+
16
+ function writeTmpFile(name: string, content: string): string {
17
+ setupTmpDir();
18
+ const path = join(TMP_DIR, name);
19
+ writeFileSync(path, content);
20
+ return path;
21
+ }
22
+
23
+ describe("extractUtilitiesFromFile", () => {
24
+ afterAll(() => cleanTmpDir());
25
+
26
+ it("extracts exported function", () => {
27
+ const path = writeTmpFile("func.ts", 'export function createUser(name: string): User {\n return { name };\n}');
28
+ const utils = extractUtilitiesFromFile(path);
29
+ expect(utils).toHaveLength(1);
30
+ expect(utils[0].name).toBe("createUser");
31
+ expect(utils[0].type).toBe("function");
32
+ expect(utils[0].signature).toContain("(name: string)");
33
+ });
34
+
35
+ it("extracts exported async function", () => {
36
+ const path = writeTmpFile("async.ts", 'export async function fetchData(url: string): Promise<Data> {\n return await fetch(url);\n}');
37
+ const utils = extractUtilitiesFromFile(path);
38
+ expect(utils).toHaveLength(1);
39
+ expect(utils[0].name).toBe("fetchData");
40
+ expect(utils[0].type).toBe("function");
41
+ });
42
+
43
+ it("extracts exported const", () => {
44
+ const path = writeTmpFile("const.ts", 'export const MAX_RETRIES: number = 3;');
45
+ const utils = extractUtilitiesFromFile(path);
46
+ expect(utils).toHaveLength(1);
47
+ expect(utils[0].name).toBe("MAX_RETRIES");
48
+ expect(utils[0].type).toBe("const");
49
+ expect(utils[0].signature).toBe("number");
50
+ });
51
+
52
+ it("extracts exported const without type annotation", () => {
53
+ const path = writeTmpFile("const2.ts", 'export const config = { port: 3000 };');
54
+ const utils = extractUtilitiesFromFile(path);
55
+ expect(utils).toHaveLength(1);
56
+ expect(utils[0].name).toBe("config");
57
+ expect(utils[0].type).toBe("const");
58
+ });
59
+
60
+ it("extracts exported class", () => {
61
+ const path = writeTmpFile("class.ts", 'export class UserService {\n constructor() {}\n}');
62
+ const utils = extractUtilitiesFromFile(path);
63
+ expect(utils).toHaveLength(1);
64
+ expect(utils[0].name).toBe("UserService");
65
+ expect(utils[0].type).toBe("class");
66
+ });
67
+
68
+ it("extracts exported interface", () => {
69
+ const path = writeTmpFile("iface.ts", 'export interface UserProps {\n name: string;\n}');
70
+ const utils = extractUtilitiesFromFile(path);
71
+ expect(utils).toHaveLength(1);
72
+ expect(utils[0].name).toBe("UserProps");
73
+ expect(utils[0].type).toBe("interface");
74
+ });
75
+
76
+ it("extracts exported type", () => {
77
+ const path = writeTmpFile("type.ts", 'export type Status = "active" | "inactive";');
78
+ const utils = extractUtilitiesFromFile(path);
79
+ expect(utils).toHaveLength(1);
80
+ expect(utils[0].name).toBe("Status");
81
+ expect(utils[0].type).toBe("type");
82
+ });
83
+
84
+ it("extracts multiple exports from one file", () => {
85
+ const path = writeTmpFile("multi.ts", [
86
+ 'export function foo() {}',
87
+ 'export const BAR = 1;',
88
+ 'export class Baz {}',
89
+ 'export interface Qux {}',
90
+ 'export type Quux = string;',
91
+ ].join("\n"));
92
+ const utils = extractUtilitiesFromFile(path);
93
+ expect(utils).toHaveLength(5);
94
+ expect(utils.map(u => u.name)).toEqual(["foo", "BAR", "Baz", "Qux", "Quux"]);
95
+ });
96
+
97
+ it("returns empty array for non-existent file", () => {
98
+ const utils = extractUtilitiesFromFile("/nonexistent/path.ts");
99
+ expect(utils).toEqual([]);
100
+ });
101
+
102
+ it("returns empty array for file without exports", () => {
103
+ const path = writeTmpFile("noexport.ts", 'const internal = 1;\nfunction helper() {}');
104
+ const utils = extractUtilitiesFromFile(path);
105
+ expect(utils).toEqual([]);
106
+ });
107
+
108
+ it("ignores non-export declarations", () => {
109
+ const path = writeTmpFile("mixed.ts", [
110
+ 'const private1 = 1;',
111
+ 'function private2() {}',
112
+ 'export function public1() {}',
113
+ ].join("\n"));
114
+ const utils = extractUtilitiesFromFile(path);
115
+ expect(utils).toHaveLength(1);
116
+ expect(utils[0].name).toBe("public1");
117
+ });
118
+ });
119
+
120
+ describe("inferScopeFromPath", () => {
121
+ it("identifies backend paths", () => {
122
+ expect(inferScopeFromPath("src/app/api/users/route.ts")).toBe("backend");
123
+ expect(inferScopeFromPath("src/server/index.ts")).toBe("backend");
124
+ expect(inferScopeFromPath("src/backend/auth.ts")).toBe("backend");
125
+ expect(inferScopeFromPath("src/services/user.ts")).toBe("backend");
126
+ expect(inferScopeFromPath("src/actions/submit.ts")).toBe("backend");
127
+ });
128
+
129
+ it("identifies frontend paths", () => {
130
+ expect(inferScopeFromPath("src/components/Button.tsx")).toBe("frontend");
131
+ expect(inferScopeFromPath("src/hooks/useAuth.ts")).toBe("frontend");
132
+ expect(inferScopeFromPath("src/pages/index.tsx")).toBe("frontend");
133
+ expect(inferScopeFromPath("src/frontend/App.tsx")).toBe("frontend");
134
+ });
135
+
136
+ it("identifies frontend for /app/ paths without /api/", () => {
137
+ expect(inferScopeFromPath("src/app/dashboard/page.tsx")).toBe("frontend");
138
+ });
139
+
140
+ it("identifies database paths", () => {
141
+ expect(inferScopeFromPath("src/db/schema.ts")).toBe("database");
142
+ expect(inferScopeFromPath("src/schema/users.ts")).toBe("database");
143
+ expect(inferScopeFromPath("src/migrations/001.sql")).toBe("database");
144
+ expect(inferScopeFromPath("src/database/connection.ts")).toBe("database");
145
+ });
146
+
147
+ it("identifies testing paths", () => {
148
+ expect(inferScopeFromPath("src/tests/user.test.ts")).toBe("testing");
149
+ // Note: /components/ matches frontend before .test. matches testing
150
+ expect(inferScopeFromPath("src/components/Button.test.tsx")).toBe("frontend");
151
+ expect(inferScopeFromPath("src/lib/auth.spec.ts")).toBe("testing");
152
+ });
153
+
154
+ it("returns shared for unrecognized paths", () => {
155
+ expect(inferScopeFromPath("src/lib/utils.ts")).toBe("shared");
156
+ expect(inferScopeFromPath("src/config/env.ts")).toBe("shared");
157
+ expect(inferScopeFromPath("package.json")).toBe("shared");
158
+ });
159
+
160
+ it("handles Windows-style paths (backslashes)", () => {
161
+ expect(inferScopeFromPath("src\\components\\Button.tsx")).toBe("frontend");
162
+ expect(inferScopeFromPath("src\\db\\schema.ts")).toBe("database");
163
+ });
164
+
165
+ it("is case-insensitive", () => {
166
+ expect(inferScopeFromPath("src/Components/Button.tsx")).toBe("frontend");
167
+ expect(inferScopeFromPath("src/DB/Schema.ts")).toBe("database");
168
+ });
169
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { generateSpecId } from "./plan";
3
+
4
+ describe("generateSpecId", () => {
5
+ it("generates ID with date-slug-hash format", () => {
6
+ const id = generateSpecId("Add user authentication");
7
+ // Format: YYYY-MM-DD-slug-hash
8
+ expect(id).toMatch(/^\d{4}-\d{2}-\d{2}-[a-z0-9-]+-[a-z0-9]{4}$/);
9
+ });
10
+
11
+ it("normalizes slug to lowercase", () => {
12
+ const id = generateSpecId("Add User Auth");
13
+ const parts = id.split("-");
14
+ // After date (3 parts), all slug parts should be lowercase
15
+ const slugAndHash = parts.slice(3).join("-");
16
+ expect(slugAndHash).toBe(slugAndHash.toLowerCase());
17
+ });
18
+
19
+ it("removes special characters from slug", () => {
20
+ const id = generateSpecId("Add auth (v2) @special!");
21
+ // Should not contain (, ), @, !
22
+ expect(id).not.toMatch(/[()@!]/);
23
+ });
24
+
25
+ it("truncates slug at 30 characters", () => {
26
+ const longName = "This is a very long feature name that exceeds thirty characters by a lot";
27
+ const id = generateSpecId(longName);
28
+ // Date is YYYY-MM-DD (10 chars) + dash + slug (max 30) + dash + hash (4)
29
+ const parts = id.split("-");
30
+ // date = parts[0]-parts[1]-parts[2], hash = last part
31
+ const datePart = parts.slice(0, 3).join("-"); // YYYY-MM-DD
32
+ const hashPart = parts[parts.length - 1]; // 4 char hash
33
+ const slugPart = parts.slice(3, -1).join("-"); // everything between date and hash
34
+ expect(slugPart.length).toBeLessThanOrEqual(30);
35
+ });
36
+
37
+ it("includes a 4-character hash suffix", () => {
38
+ const id = generateSpecId("Test feature");
39
+ const parts = id.split("-");
40
+ const hash = parts[parts.length - 1];
41
+ expect(hash.length).toBe(4);
42
+ // Hash should be base36
43
+ expect(hash).toMatch(/^[a-z0-9]+$/);
44
+ });
45
+
46
+ it("starts with today's date", () => {
47
+ const id = generateSpecId("Some feature");
48
+ const today = new Date().toISOString().split("T")[0];
49
+ expect(id.startsWith(today)).toBe(true);
50
+ });
51
+
52
+ it("generates different IDs for different names", () => {
53
+ const id1 = generateSpecId("Feature A");
54
+ const id2 = generateSpecId("Feature B");
55
+ expect(id1).not.toBe(id2);
56
+ });
57
+
58
+ it("removes trailing dashes from slug", () => {
59
+ const id = generateSpecId("test-feature-");
60
+ // Should not have double dashes or trailing dash before hash
61
+ expect(id).not.toMatch(/--/);
62
+ });
63
+
64
+ it("handles empty-ish name gracefully", () => {
65
+ const id = generateSpecId("a");
66
+ expect(id).toMatch(/^\d{4}-\d{2}-\d{2}-a-[a-z0-9]{4}$/);
67
+ });
68
+
69
+ it("converts spaces and special chars to dashes", () => {
70
+ const id = generateSpecId("add user auth");
71
+ expect(id).toContain("add-user-auth");
72
+ });
73
+ });
package/commands/plan.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import { getDb } from "../db/connection";
2
2
  import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
3
3
 
4
- function generateSpecId(name: string): string {
4
+ export function generateSpecId(name: string): string {
5
5
  const date = new Date().toISOString().split("T")[0];
6
6
  const slug = name
7
7
  .toLowerCase()
8
8
  .replace(/[^a-z0-9]+/g, "-")
9
+ .replace(/-+$/, "")
9
10
  .substring(0, 30);
10
- return `${date}-${slug}`;
11
+ const hash = Bun.hash(name + Date.now()).toString(36).substring(0, 4);
12
+ return `${date}-${slug}-${hash}`;
11
13
  }
12
14
 
13
15
  // v8.4: Suporte a --from-analysis para import automatico de baby steps
@@ -0,0 +1,90 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ // Agents que sao referencias (nao spawnables como subagent_type)
6
+ const REFERENCE_FILES = new Set([
7
+ "common-directives.md",
8
+ "subagent-return-protocol.md",
9
+ ]);
10
+
11
+ function findPluginAgentsDir(): string | null {
12
+ // 1. Dev repo: git root + plugins/codexa-workflow/agents/
13
+ try {
14
+ const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
15
+ const pluginDir = join(gitRoot, "plugins", "codexa-workflow", "agents");
16
+ if (existsSync(pluginDir)) return pluginDir;
17
+ } catch {
18
+ // Not in git repo
19
+ }
20
+
21
+ // 2. Global install: resolve from @codexa/cli package location
22
+ try {
23
+ const cliPath = execSync("bun pm ls -g 2>/dev/null || npm list -g @codexa/cli --parseable 2>/dev/null", {
24
+ encoding: "utf-8",
25
+ }).trim();
26
+ if (cliPath) {
27
+ // Navigate from cli package to sibling plugin
28
+ const lines = cliPath.split("\n").filter(l => l.includes("codexa"));
29
+ for (const line of lines) {
30
+ const trimmed = line.trim();
31
+ // Try parent paths to find the monorepo root
32
+ const parts = trimmed.split(/[/\\]/);
33
+ for (let i = parts.length - 1; i >= 0; i--) {
34
+ const candidate = join(parts.slice(0, i + 1).join("/"), "plugins", "codexa-workflow", "agents");
35
+ if (existsSync(candidate)) return candidate;
36
+ }
37
+ }
38
+ }
39
+ } catch {
40
+ // Package manager not available
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ export function syncAgents(options: { force?: boolean } = {}): void {
47
+ const pluginDir = findPluginAgentsDir();
48
+
49
+ if (!pluginDir) {
50
+ console.error("\n[ERRO] Diretorio de agents do plugin nao encontrado.");
51
+ console.error(" Verifique se voce esta no repositorio codexa ou se @codexa/cli esta instalado globalmente.\n");
52
+ process.exit(1);
53
+ }
54
+
55
+ const targetDir = join(process.cwd(), ".claude", "agents");
56
+ mkdirSync(targetDir, { recursive: true });
57
+
58
+ const agentFiles = readdirSync(pluginDir).filter(
59
+ (f) => f.endsWith(".md") && !REFERENCE_FILES.has(f)
60
+ );
61
+
62
+ let copied = 0;
63
+ let skipped = 0;
64
+
65
+ for (const file of agentFiles) {
66
+ const src = join(pluginDir, file);
67
+ const dst = join(targetDir, file);
68
+
69
+ if (existsSync(dst) && !options.force) {
70
+ skipped++;
71
+ continue;
72
+ }
73
+
74
+ copyFileSync(src, dst);
75
+ copied++;
76
+ }
77
+
78
+ console.log(`\n[OK] Agents sincronizados para .claude/agents/`);
79
+ console.log(` Copiados: ${copied}`);
80
+ if (skipped > 0) {
81
+ console.log(` Ignorados (ja existem): ${skipped} — use --force para sobrescrever`);
82
+ }
83
+ console.log(` Total disponivel: ${agentFiles.length}`);
84
+ console.log(`\nAgents disponiveis como subagent_type:`);
85
+ for (const file of agentFiles) {
86
+ const name = file.replace(".md", "");
87
+ console.log(` - ${name}`);
88
+ }
89
+ console.log();
90
+ }