@codexa/cli 9.0.31 → 9.0.32

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,4 +1,4 @@
1
- import { getDb } from "../db/connection";
1
+ import { dbGet, dbAll } from "../db/connection";
2
2
  import { initSchema, getPatternsForFiles, getRelatedDecisions, getArchitecturalAnalysisForSpec } from "../db/schema";
3
3
  import { getKnowledgeForTask } from "../commands/knowledge";
4
4
  import type { ContextSection, ContextData } from "./assembly";
@@ -21,57 +21,55 @@ import {
21
21
  buildReferencesSection,
22
22
  } from "./sections";
23
23
 
24
- // v9.0: Contexto minimo para subagent (max ~2KB)
25
24
  const MAX_MINIMAL_CONTEXT = 2048;
26
25
 
27
- export function getMinimalContextForSubagent(taskId: number): string {
28
- initSchema();
29
- const db = getDb();
26
+ export async function getMinimalContextForSubagent(taskId: number): Promise<string> {
27
+ await initSchema();
30
28
 
31
- const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
29
+ const task = await dbGet<any>("SELECT * FROM tasks WHERE id = ?", [taskId]);
32
30
  if (!task) return "ERRO: Task nao encontrada";
33
31
 
34
- const spec = db.query("SELECT * FROM specs WHERE id = ?").get(task.spec_id) as any;
35
- const context = db.query("SELECT * FROM context WHERE spec_id = ?").get(task.spec_id) as any;
32
+ const spec = await dbGet<any>("SELECT * FROM specs WHERE id = ?", [task.spec_id]);
33
+ const context = await dbGet<any>("SELECT * FROM context WHERE spec_id = ?", [task.spec_id]);
36
34
 
37
35
  const taskFiles = task.files ? JSON.parse(task.files) : [];
38
36
  const domain = domainToScope(getAgentDomain(task.agent));
39
37
 
40
- // 1. Standards REQUIRED apenas (nao recommended)
41
- const requiredStandards = db.query(
38
+ const requiredStandards = await dbAll<any>(
42
39
  `SELECT rule FROM standards
43
40
  WHERE enforcement = 'required' AND (scope = 'all' OR scope = ?)
44
- LIMIT 10`
45
- ).all(domain) as any[];
41
+ LIMIT 10`,
42
+ [domain]
43
+ );
46
44
 
47
- // 2. Blockers CRITICAL (spec atual + cross-feature)
48
- const criticalBlockers = db.query(
45
+ const criticalBlockers = await dbAll<any>(
49
46
  `SELECT DISTINCT content FROM knowledge
50
47
  WHERE severity = 'critical'
51
48
  ORDER BY CASE WHEN spec_id = ? THEN 0 ELSE 1 END, created_at DESC
52
- LIMIT 5`
53
- ).all(task.spec_id) as any[];
49
+ LIMIT 5`,
50
+ [task.spec_id]
51
+ );
54
52
 
55
- // 3. Decisoes da task anterior (dependency direta)
56
53
  const dependsOn = task.depends_on ? JSON.parse(task.depends_on) : [];
57
54
  let depDecisions: any[] = [];
58
55
  if (dependsOn.length > 0) {
59
56
  const placeholders = dependsOn.map(() => '?').join(',');
60
- const depTasks = db.query(
61
- `SELECT id FROM tasks WHERE spec_id = ? AND number IN (${placeholders})`
62
- ).all(task.spec_id, ...dependsOn) as any[];
57
+ const depTasks = await dbAll<any>(
58
+ `SELECT id FROM tasks WHERE spec_id = ? AND number IN (${placeholders})`,
59
+ [task.spec_id, ...dependsOn]
60
+ );
63
61
 
64
62
  for (const dt of depTasks) {
65
- const reasoning = db.query(
63
+ const reasoning = await dbAll<any>(
66
64
  `SELECT thought FROM reasoning_log
67
65
  WHERE spec_id = ? AND task_id = ? AND category = 'recommendation'
68
- ORDER BY created_at DESC LIMIT 2`
69
- ).all(task.spec_id, dt.id) as any[];
66
+ ORDER BY created_at DESC LIMIT 2`,
67
+ [task.spec_id, dt.id]
68
+ );
70
69
  depDecisions.push(...reasoning);
71
70
  }
72
71
  }
73
72
 
74
- // Montar output minimo
75
73
  let output = `## CONTEXTO MINIMO (Task #${task.number})
76
74
 
77
75
  **Feature:** ${spec.name}
@@ -100,7 +98,6 @@ export function getMinimalContextForSubagent(taskId: number): string {
100
98
  codexa context detail architecture # Analise arquitetural
101
99
  `;
102
100
 
103
- // Truncar se exceder limite
104
101
  if (output.length > MAX_MINIMAL_CONTEXT) {
105
102
  output = output.substring(0, MAX_MINIMAL_CONTEXT - 100) + "\n\n[CONTEXTO TRUNCADO - use: codexa context detail <secao>]\n";
106
103
  }
@@ -108,44 +105,33 @@ export function getMinimalContextForSubagent(taskId: number): string {
108
105
  return output;
109
106
  }
110
107
 
111
- // ═══════════════════════════════════════════════════════════════
112
- // CONTEXT BUILDER (v11.0 inline delivery, no file system)
113
- // ═══════════════════════════════════════════════════════════════
114
-
115
- function fetchContextData(taskId: number): ContextData | null {
116
- const db = getDb();
117
-
118
- const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
108
+ async function fetchContextData(taskId: number): Promise<ContextData | null> {
109
+ const task = await dbGet<any>("SELECT * FROM tasks WHERE id = ?", [taskId]);
119
110
  if (!task) return null;
120
111
 
121
- const spec = db.query("SELECT * FROM specs WHERE id = ?").get(task.spec_id) as any;
122
- const context = db.query("SELECT * FROM context WHERE spec_id = ?").get(task.spec_id) as any;
123
- const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
112
+ const spec = await dbGet<any>("SELECT * FROM specs WHERE id = ?", [task.spec_id]);
113
+ const context = await dbGet<any>("SELECT * FROM context WHERE spec_id = ?", [task.spec_id]);
114
+ const project = await dbGet<any>("SELECT * FROM project WHERE id = 'default'");
124
115
 
125
- // v8.4: Analise arquitetural (link explicito via analysis_id ou nome)
126
- const archAnalysis = getArchitecturalAnalysisForSpec(spec.name, task.spec_id);
116
+ const archAnalysis = await getArchitecturalAnalysisForSpec(spec.name, task.spec_id);
127
117
 
128
- // Arquivos da task para filtrar contexto relevante
129
118
  const taskFiles = task.files ? JSON.parse(task.files) : [];
130
119
  const domain = domainToScope(getAgentDomain(task.agent));
131
120
 
132
- // v11.0: Raised caps — 50 decisions (was 8), filtered by relevance
133
- const allDecisions = db
134
- .query(`SELECT * FROM decisions WHERE spec_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 100`)
135
- .all(task.spec_id) as any[];
121
+ const allDecisions = await dbAll<any>(
122
+ `SELECT * FROM decisions WHERE spec_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 100`,
123
+ [task.spec_id]
124
+ );
136
125
  const decisions = filterRelevantDecisions(allDecisions, taskFiles, 50);
137
126
 
138
- // Standards required + recommended que se aplicam aos arquivos
139
- const standards = db
140
- .query(
141
- `SELECT * FROM standards
127
+ const standards = await dbAll<any>(
128
+ `SELECT * FROM standards
142
129
  WHERE (scope = 'all' OR scope = ?)
143
- ORDER BY enforcement DESC, category`
144
- )
145
- .all(domain) as any[];
130
+ ORDER BY enforcement DESC, category`,
131
+ [domain]
132
+ );
146
133
  const relevantStandards = filterRelevantStandards(standards, taskFiles);
147
134
 
148
- // v11.0: Raised caps — 50 critical (was 20), 30 info (was 10)
149
135
  const allKnowledge = getKnowledgeForTask(task.spec_id, taskId);
150
136
  const allCriticalKnowledge = allKnowledge.filter((k: any) => k.severity === 'critical' || k.severity === 'warning');
151
137
  const criticalKnowledge = allCriticalKnowledge.slice(0, 50);
@@ -154,46 +140,48 @@ function fetchContextData(taskId: number): ContextData | null {
154
140
  const infoKnowledge = allInfoKnowledge.slice(0, 30);
155
141
  const truncatedInfo = allInfoKnowledge.length - infoKnowledge.length;
156
142
 
157
- const productContext = db.query("SELECT * FROM product_context WHERE id = 'default'").get() as any;
158
- const patterns = getPatternsForFiles(taskFiles);
143
+ const productContext = await dbGet<any>("SELECT * FROM product_context WHERE id = 'default'");
144
+ const patterns = await getPatternsForFiles(taskFiles);
159
145
 
160
- // v8.2: Reasoning de tasks dependentes + todas tasks completas recentes
161
146
  const dependsOn = task.depends_on ? JSON.parse(task.depends_on) : [];
162
147
  const depReasoning: any[] = [];
163
148
  const seenTaskIds = new Set<number>();
164
149
 
165
150
  if (dependsOn.length > 0) {
166
151
  const placeholders = dependsOn.map(() => '?').join(',');
167
- const depTasks = db.query(
168
- `SELECT id, number FROM tasks WHERE spec_id = ? AND number IN (${placeholders})`
169
- ).all(task.spec_id, ...dependsOn) as any[];
152
+ const depTasks = await dbAll<any>(
153
+ `SELECT id, number FROM tasks WHERE spec_id = ? AND number IN (${placeholders})`,
154
+ [task.spec_id, ...dependsOn]
155
+ );
170
156
 
171
157
  for (const depTask of depTasks) {
172
158
  seenTaskIds.add(depTask.id);
173
- const reasoning = db.query(
159
+ const reasoning = await dbAll<any>(
174
160
  `SELECT category, thought FROM reasoning_log
175
161
  WHERE spec_id = ? AND task_id = ?
176
162
  AND category IN ('recommendation', 'decision', 'challenge')
177
163
  ORDER BY
178
164
  CASE importance WHEN 'critical' THEN 1 WHEN 'high' THEN 2 ELSE 3 END,
179
165
  created_at DESC
180
- LIMIT 3`
181
- ).all(task.spec_id, depTask.id) as any[];
166
+ LIMIT 3`,
167
+ [task.spec_id, depTask.id]
168
+ );
182
169
  for (const r of reasoning) {
183
170
  depReasoning.push({ ...r, fromTask: depTask.number });
184
171
  }
185
172
  }
186
173
  }
187
174
 
188
- const completedTasks = db.query(
175
+ const completedTasks = await dbAll<any>(
189
176
  `SELECT t.id, t.number FROM tasks t
190
177
  WHERE t.spec_id = ? AND t.status = 'done' AND t.id != ?
191
- ORDER BY t.completed_at DESC LIMIT 10`
192
- ).all(task.spec_id, taskId) as any[];
178
+ ORDER BY t.completed_at DESC LIMIT 10`,
179
+ [task.spec_id, taskId]
180
+ );
193
181
 
194
182
  for (const ct of completedTasks) {
195
183
  if (seenTaskIds.has(ct.id)) continue;
196
- const reasoning = db.query(
184
+ const reasoning = await dbAll<any>(
197
185
  `SELECT category, thought FROM reasoning_log
198
186
  WHERE spec_id = ? AND task_id = ?
199
187
  AND category IN ('recommendation', 'challenge')
@@ -201,21 +189,22 @@ function fetchContextData(taskId: number): ContextData | null {
201
189
  ORDER BY
202
190
  CASE importance WHEN 'critical' THEN 1 ELSE 2 END,
203
191
  created_at DESC
204
- LIMIT 2`
205
- ).all(task.spec_id, ct.id) as any[];
192
+ LIMIT 2`,
193
+ [task.spec_id, ct.id]
194
+ );
206
195
  for (const r of reasoning) {
207
196
  depReasoning.push({ ...r, fromTask: ct.number });
208
197
  }
209
198
  }
210
199
 
211
- const libContexts = db.query(
200
+ const libContexts = await dbAll<any>(
212
201
  "SELECT lib_name, version FROM lib_contexts ORDER BY lib_name LIMIT 10"
213
- ).all() as any[];
202
+ );
214
203
 
215
204
  const graphDecisions: any[] = [];
216
205
  for (const file of taskFiles) {
217
206
  try {
218
- const related = getRelatedDecisions(file, "file");
207
+ const related = await getRelatedDecisions(file, "file");
219
208
  for (const d of related) {
220
209
  if (!graphDecisions.find((gd: any) => gd.id === d.id)) {
221
210
  graphDecisions.push(d);
@@ -227,7 +216,7 @@ function fetchContextData(taskId: number): ContextData | null {
227
216
  const discoveredPatterns = context?.patterns ? JSON.parse(context.patterns) : [];
228
217
 
229
218
  return {
230
- db, task, spec, context, project, taskFiles, domain, archAnalysis,
219
+ task, spec, context, project, taskFiles, domain, archAnalysis,
231
220
  decisions, allDecisions, relevantStandards,
232
221
  criticalKnowledge, truncatedCritical, infoKnowledge, truncatedInfo,
233
222
  productContext, patterns, depReasoning, libContexts,
@@ -235,10 +224,8 @@ function fetchContextData(taskId: number): ContextData | null {
235
224
  };
236
225
  }
237
226
 
238
- // ── Main Entry Point ──────────────────────────────────────────
239
-
240
- function buildContext(taskId: number): { content: string; data: ContextData } | null {
241
- const data = fetchContextData(taskId);
227
+ async function buildContext(taskId: number): Promise<{ content: string; data: ContextData } | null> {
228
+ const data = await fetchContextData(taskId);
242
229
  if (!data) return null;
243
230
 
244
231
  const header = `## CONTEXTO (Task #${data.task.number})
@@ -258,10 +245,10 @@ function buildContext(taskId: number): { content: string; data: ContextData } |
258
245
  buildAlertsSection(data),
259
246
  buildDiscoveriesSection(data),
260
247
  buildPatternsSection(data),
261
- buildUtilitiesSection(data),
248
+ await buildUtilitiesSection(data),
262
249
  buildGraphSection(data),
263
250
  buildStackSection(data),
264
- buildHintsSection(data),
251
+ await buildHintsSection(data),
265
252
  ].filter((s): s is ContextSection => s !== null);
266
253
 
267
254
  const agentDomain = getAgentDomain(data.task.agent);
@@ -270,8 +257,8 @@ function buildContext(taskId: number): { content: string; data: ContextData } |
270
257
  return { content: assembleSections(header, sections), data };
271
258
  }
272
259
 
273
- export function getContextForSubagent(taskId: number): string {
274
- initSchema();
275
- const result = buildContext(taskId);
260
+ export async function getContextForSubagent(taskId: number): Promise<string> {
261
+ await initSchema();
262
+ const result = await buildContext(taskId);
276
263
  return result?.content || "ERRO: Task nao encontrada";
277
264
  }
@@ -1,10 +1,9 @@
1
+ import { dbGet } from "../db/connection";
1
2
  import { getUtilitiesForContext, getAgentHints } from "../db/schema";
2
3
  import type { ContextSection, ContextData } from "./assembly";
3
4
  import { getAgentDomain, domainToScope } from "./domains";
4
5
  import { findReferenceFiles } from "./references";
5
6
 
6
- // ── Section Builders ──────────────────────────────────────────
7
-
8
7
  export function buildProductSection(data: ContextData): ContextSection | null {
9
8
  if (!data.productContext) return null;
10
9
 
@@ -42,8 +41,6 @@ export function buildProductSection(data: ContextData): ContextSection | null {
42
41
 
43
42
  content += "\n";
44
43
 
45
- // v10.2: Product context is small (~500-2000 bytes) but high-impact.
46
- // Priority 1 ensures it is NEVER truncated.
47
44
  return { name: "PRODUTO", content, priority: 1 };
48
45
  }
49
46
 
@@ -114,7 +111,6 @@ export function buildStandardsSection(data: ContextData): ContextSection {
114
111
  }
115
112
 
116
113
  export function buildDecisionsSection(data: ContextData): ContextSection {
117
- // v11.0: Show all decisions (raised cap to 50 in generator)
118
114
  const decisionsToShow = data.allDecisions;
119
115
  const truncatedDecisions = data.allDecisions.length - decisionsToShow.length;
120
116
  const content = `
@@ -170,7 +166,6 @@ ${data.patterns.map((p: any) => {
170
166
  }
171
167
 
172
168
  if (data.discoveredPatterns.length > 0) {
173
- // v11.0: Show all discovered patterns (no cap)
174
169
  content += `
175
170
  ### PATTERNS DESCOBERTOS (${data.discoveredPatterns.length})
176
171
  ${data.discoveredPatterns.map((p: any) => `- ${p.pattern}${p.source_task ? ` (Task #${p.source_task})` : ""}`).join("\n")}
@@ -181,7 +176,7 @@ ${data.discoveredPatterns.map((p: any) => `- ${p.pattern}${p.source_task ? ` (Ta
181
176
  return { name: "PATTERNS", content, priority: 9 };
182
177
  }
183
178
 
184
- export function buildUtilitiesSection(data: ContextData): ContextSection | null {
179
+ export async function buildUtilitiesSection(data: ContextData): Promise<ContextSection | null> {
185
180
  try {
186
181
  const taskDirs = [...new Set(data.taskFiles.map((f: string) => {
187
182
  const parts = f.replace(/\\/g, "/").split("/");
@@ -189,12 +184,11 @@ export function buildUtilitiesSection(data: ContextData): ContextSection | null
189
184
  }).filter(Boolean))];
190
185
 
191
186
  const agentScope = domainToScope(getAgentDomain(data.task.agent)) || undefined;
192
- // v11.0: Raised cap to 50 (was 15)
193
187
  const utilLimit = 50;
194
- let relevantUtilities = getUtilitiesForContext(taskDirs, undefined, utilLimit);
188
+ let relevantUtilities = await getUtilitiesForContext(taskDirs, undefined, utilLimit);
195
189
 
196
190
  if (relevantUtilities.length < 5 && agentScope) {
197
- const scopeUtils = getUtilitiesForContext([], agentScope, 15);
191
+ const scopeUtils = await getUtilitiesForContext([], agentScope, 15);
198
192
  const existingKeys = new Set(relevantUtilities.map((u: any) => `${u.file_path}:${u.utility_name}`));
199
193
  for (const u of scopeUtils) {
200
194
  if (!existingKeys.has(`${u.file_path}:${u.utility_name}`)) {
@@ -206,7 +200,8 @@ export function buildUtilitiesSection(data: ContextData): ContextSection | null
206
200
 
207
201
  if (relevantUtilities.length === 0) return null;
208
202
 
209
- const totalCount = (data.db.query("SELECT COUNT(*) as c FROM project_utilities").get() as any)?.c || 0;
203
+ const totalCountRow = await dbGet<any>("SELECT COUNT(*) as c FROM project_utilities");
204
+ const totalCount = totalCountRow?.c || 0;
210
205
  const truncated = totalCount - relevantUtilities.length;
211
206
  const content = `
212
207
  ### UTILITIES EXISTENTES (${relevantUtilities.length}${truncated > 0 ? ` [+${truncated} mais]` : ''})
@@ -241,7 +236,6 @@ export function buildStackSection(data: ContextData): ContextSection | null {
241
236
  if (data.project) {
242
237
  const stack = JSON.parse(data.project.stack);
243
238
  const allStackEntries = Object.entries(stack);
244
- // v11.0: Show all stack entries (no cap)
245
239
  const mainStack = allStackEntries;
246
240
  const truncatedStack = allStackEntries.length - mainStack.length;
247
241
  content += `
@@ -261,11 +255,11 @@ ${data.libContexts.map((l: any) => `- ${l.lib_name}${l.version ? ` v${l.version}
261
255
  return { name: "STACK", content, priority: 11 };
262
256
  }
263
257
 
264
- export function buildHintsSection(data: ContextData): ContextSection | null {
258
+ export async function buildHintsSection(data: ContextData): Promise<ContextSection | null> {
265
259
  const agentType = data.task.agent;
266
260
  if (!agentType) return null;
267
261
 
268
- const hints = getAgentHints(agentType);
262
+ const hints = await getAgentHints(agentType);
269
263
  if (hints.length === 0) return null;
270
264
 
271
265
  const content = `
package/db/connection.ts CHANGED
@@ -1,32 +1,208 @@
1
- import { Database } from "bun:sqlite";
1
+ import { createClient, type Client, type InValue } from "@libsql/client";
2
+ import { execSync } from "child_process";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
4
  import { join, dirname } from "path";
3
- import { mkdirSync, existsSync } from "fs";
4
5
 
5
- const DB_PATH = join(process.cwd(), ".codexa", "db", "workflow.db");
6
+ let client: Client | null = null;
6
7
 
7
- let db: Database | null = null;
8
+ // ═══════════════════════════════════════════════════════════════
9
+ // Resolucao de DB URL: env → config local → derivacao do git
10
+ // ═══════════════════════════════════════════════════════════════
8
11
 
9
- export function getDb(): Database {
10
- if (!db) {
11
- // Garantir que o diretorio existe
12
- const dbDir = dirname(DB_PATH);
13
- if (!existsSync(dbDir)) {
14
- mkdirSync(dbDir, { recursive: true });
12
+ function resolveDbUrl(): string {
13
+ // 1. Override explicito via env var
14
+ if (process.env.CODEXA_DB_URL) return process.env.CODEXA_DB_URL;
15
+
16
+ // 2. Cache local (.codexa/config.json)
17
+ const configPath = join(process.cwd(), ".codexa", "config.json");
18
+ if (existsSync(configPath)) {
19
+ try {
20
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
21
+ if (config.database) return config.database;
22
+ } catch { /* config invalido, continuar */ }
23
+ }
24
+
25
+ // 3. Derivar do git remote
26
+ const org = process.env.TURSO_ORG;
27
+ if (!org) {
28
+ throw new Error(
29
+ "Banco de dados nao configurado.\n\n" +
30
+ "Opcao 1 — Configurar automaticamente:\n" +
31
+ " export TURSO_ORG=sua-org\n" +
32
+ " export TURSO_AUTH_TOKEN=eyJ...\n" +
33
+ " codexa init\n\n" +
34
+ "Opcao 2 — Configurar manualmente:\n" +
35
+ " export CODEXA_DB_URL=libsql://seu-db.turso.io\n" +
36
+ " export TURSO_AUTH_TOKEN=eyJ..."
37
+ );
38
+ }
39
+
40
+ const slug = deriveSlugFromGit();
41
+ const url = `libsql://codexa-${slug}-${org}.turso.io`;
42
+
43
+ // Salvar no cache para proximas execucoes
44
+ saveConfig(configPath, url);
45
+
46
+ return url;
47
+ }
48
+
49
+ function deriveSlugFromGit(): string {
50
+ try {
51
+ const remote = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
52
+ // github.com/leandro/meu-saas.git → leandro--meu-saas
53
+ // git@github.com:leandro/meu-saas.git → leandro--meu-saas
54
+ const match = remote.match(/[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
55
+ if (match) {
56
+ return `${match[1]}--${match[2]}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
57
+ }
58
+ } catch { /* git nao disponivel */ }
59
+
60
+ // Fallback: nome da pasta atual
61
+ const dirName = process.cwd().split(/[/\\]/).pop() || "unknown";
62
+ return dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
63
+ }
64
+
65
+ function saveConfig(configPath: string, url: string): void {
66
+ try {
67
+ const dir = dirname(configPath);
68
+ if (!existsSync(dir)) {
69
+ mkdirSync(dir, { recursive: true });
70
+ }
71
+ writeFileSync(configPath, JSON.stringify({ database: url }, null, 2) + "\n");
72
+ } catch { /* falha ao salvar cache, nao e critico */ }
73
+ }
74
+
75
+ // ═══════════════════════════════════════════════════════════════
76
+ // Provisioning: cria o DB no Turso se nao existir
77
+ // ═══════════════════════════════════════════════════════════════
78
+
79
+ export async function ensureDatabase(): Promise<void> {
80
+ const url = resolveDbUrl();
81
+
82
+ // Somente provisionar para URLs Turso (nao para file: ou :memory:)
83
+ if (!url.startsWith("libsql://")) return;
84
+
85
+ const authToken = process.env.TURSO_AUTH_TOKEN;
86
+ const org = process.env.TURSO_ORG;
87
+ if (!authToken || !org) return;
88
+
89
+ // Extrair nome do DB da URL: libsql://codexa-slug-org.turso.io → codexa-slug
90
+ const hostMatch = url.match(/^libsql:\/\/(.+)-[^-]+\.turso\.io$/);
91
+ if (!hostMatch) return;
92
+ const dbName = hostMatch[1];
93
+
94
+ // Tentar conectar primeiro — se o DB ja existe, nao precisa criar
95
+ try {
96
+ const testClient = createClient({ url, authToken });
97
+ await testClient.execute("SELECT 1");
98
+ testClient.close();
99
+ return;
100
+ } catch { /* DB nao existe, criar */ }
101
+
102
+ // Criar via Turso Platform API
103
+ const group = process.env.TURSO_GROUP || "default";
104
+ try {
105
+ const response = await fetch(
106
+ `https://api.turso.tech/v1/organizations/${org}/databases`,
107
+ {
108
+ method: "POST",
109
+ headers: {
110
+ "Authorization": `Bearer ${authToken}`,
111
+ "Content-Type": "application/json",
112
+ },
113
+ body: JSON.stringify({ name: dbName, group }),
114
+ }
115
+ );
116
+
117
+ if (response.ok) {
118
+ console.log(`[codexa] Banco criado no Turso: ${dbName}`);
119
+ } else if (response.status === 409) {
120
+ // Ja existe — tudo certo
121
+ } else {
122
+ const body = await response.text();
123
+ console.error(`[codexa] Falha ao criar banco: ${response.status} ${body}`);
15
124
  }
16
- db = new Database(DB_PATH, { create: true });
17
- db.exec("PRAGMA journal_mode = WAL");
18
- db.exec("PRAGMA foreign_keys = ON");
125
+ } catch (e: any) {
126
+ console.error(`[codexa] Erro ao provisionar banco: ${e.message}`);
19
127
  }
20
- return db;
128
+ }
129
+
130
+ // ═══════════════════════════════════════════════════════════════
131
+ // Client management
132
+ // ═══════════════════════════════════════════════════════════════
133
+
134
+ export function getDb(): Client {
135
+ if (!client) {
136
+ const url = resolveDbUrl();
137
+ const authToken = process.env.TURSO_AUTH_TOKEN;
138
+
139
+ client = createClient({
140
+ url,
141
+ authToken: authToken || undefined,
142
+ });
143
+ }
144
+ return client;
21
145
  }
22
146
 
23
147
  export function closeDb(): void {
24
- if (db) {
25
- db.close();
26
- db = null;
148
+ if (client) {
149
+ client.close();
150
+ client = null;
27
151
  }
28
152
  }
29
153
 
30
- export function getDbPath(): string {
31
- return DB_PATH;
154
+ export function resetClient(): void {
155
+ client = null;
156
+ }
157
+
158
+ export function setClient(c: Client): void {
159
+ client = c;
160
+ }
161
+
162
+ export function getResolvedDbUrl(): string {
163
+ return resolveDbUrl();
164
+ }
165
+
166
+ // ═══════════════════════════════════════════════════════════════
167
+ // Helpers: drop-in async replacements for bun:sqlite patterns
168
+ // ═══════════════════════════════════════════════════════════════
169
+
170
+ type Args = InValue[];
171
+
172
+ export async function dbGet<T = any>(sql: string, args: Args = []): Promise<T | null> {
173
+ const db = getDb();
174
+ const result = await db.execute({ sql, args });
175
+ return (result.rows[0] as T) ?? null;
176
+ }
177
+
178
+ export async function dbAll<T = any>(sql: string, args: Args = []): Promise<T[]> {
179
+ const db = getDb();
180
+ const result = await db.execute({ sql, args });
181
+ return result.rows as T[];
182
+ }
183
+
184
+ export async function dbRun(sql: string, args: Args = []): Promise<{ changes: number; lastInsertRowid: bigint | undefined }> {
185
+ const db = getDb();
186
+ const result = await db.execute({ sql, args });
187
+ return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid };
188
+ }
189
+
190
+ export async function dbExec(sql: string): Promise<void> {
191
+ const db = getDb();
192
+ const statements = sql
193
+ .split(";")
194
+ .map((s) => s.trim())
195
+ .filter((s) => s.length > 0 && !s.startsWith("--"));
196
+
197
+ if (statements.length === 0) return;
198
+
199
+ if (statements.length === 1) {
200
+ await db.execute(statements[0]);
201
+ return;
202
+ }
203
+
204
+ await db.batch(
205
+ statements.map((s) => ({ sql: s, args: [] })),
206
+ "write"
207
+ );
32
208
  }