@codexa/cli 9.0.33 → 9.0.34

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.
Files changed (2) hide show
  1. package/db/connection.ts +159 -76
  2. package/package.json +1 -1
package/db/connection.ts CHANGED
@@ -6,8 +6,11 @@ import { join, dirname } from "path";
6
6
  let client: Client | null = null;
7
7
  let dbProvisioned = false;
8
8
 
9
+ const TURSO_API = "https://api.turso.tech/v1";
10
+ const CONFIG_PATH = () => join(process.cwd(), ".codexa", "config.json");
11
+
9
12
  // ═══════════════════════════════════════════════════════════════
10
- // Resolucao de DB URL: env → config local → derivacao do git
13
+ // Resolucao de DB URL: env → config local → provisionar via API
11
14
  // ═══════════════════════════════════════════════════════════════
12
15
 
13
16
  function resolveDbUrl(): string {
@@ -15,7 +18,7 @@ function resolveDbUrl(): string {
15
18
  if (process.env.CODEXA_DB_URL) return process.env.CODEXA_DB_URL;
16
19
 
17
20
  // 2. Cache local (.codexa/config.json)
18
- const configPath = join(process.cwd(), ".codexa", "config.json");
21
+ const configPath = CONFIG_PATH();
19
22
  if (existsSync(configPath)) {
20
23
  try {
21
24
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -23,48 +26,33 @@ function resolveDbUrl(): string {
23
26
  } catch { /* config invalido, continuar */ }
24
27
  }
25
28
 
26
- // 3. Derivar do git remote
27
- const org = process.env.TURSO_ORG;
28
- if (!org) {
29
- throw new Error(
30
- "Banco de dados nao configurado.\n\n" +
31
- "Opcao 1 — Configurar automaticamente:\n" +
32
- " export TURSO_ORG=sua-org\n" +
33
- " export TURSO_AUTH_TOKEN=eyJ...\n" +
34
- " codexa init\n\n" +
35
- "Opcao 2 — Configurar manualmente:\n" +
36
- " export CODEXA_DB_URL=libsql://seu-db.turso.io\n" +
37
- " export TURSO_AUTH_TOKEN=eyJ..."
38
- );
39
- }
40
-
41
- const slug = deriveSlugFromGit();
42
- const url = `libsql://codexa-${slug}-${org}.turso.io`;
43
-
44
- // Salvar no cache para proximas execucoes
45
- saveConfig(configPath, url);
29
+ // 3. Precisa provisionar retorna placeholder que sera resolvido em ensureDatabase()
30
+ // Nao temos a URL real sem chamar a API (por causa da regiao no hostname)
31
+ throw new NeedsProvisioningError();
32
+ }
46
33
 
47
- return url;
34
+ class NeedsProvisioningError extends Error {
35
+ constructor() {
36
+ super("DB_NEEDS_PROVISIONING");
37
+ }
48
38
  }
49
39
 
50
- function deriveSlugFromGit(): string {
40
+ function deriveDbNameFromGit(): string {
51
41
  try {
52
42
  const remote = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
53
- // github.com/leandro/meu-saas.git → leandro--meu-saas
54
- // git@github.com:leandro/meu-saas.git → leandro--meu-saas
55
43
  const match = remote.match(/[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
56
44
  if (match) {
57
- return `${match[1]}--${match[2]}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
45
+ return match[2].toLowerCase().replace(/[^a-z0-9-]/g, "-");
58
46
  }
59
47
  } catch { /* git nao disponivel */ }
60
48
 
61
- // Fallback: nome da pasta atual
62
49
  const dirName = process.cwd().split(/[/\\]/).pop() || "unknown";
63
50
  return dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
64
51
  }
65
52
 
66
- function saveConfig(configPath: string, url: string): void {
53
+ function saveConfig(url: string): void {
67
54
  try {
55
+ const configPath = CONFIG_PATH();
68
56
  const dir = dirname(configPath);
69
57
  if (!existsSync(dir)) {
70
58
  mkdirSync(dir, { recursive: true });
@@ -73,70 +61,165 @@ function saveConfig(configPath: string, url: string): void {
73
61
  } catch { /* falha ao salvar cache, nao e critico */ }
74
62
  }
75
63
 
64
+ function tursoHeaders(): Record<string, string> {
65
+ const authToken = process.env.TURSO_AUTH_TOKEN;
66
+ if (!authToken) {
67
+ throw new Error(
68
+ "TURSO_AUTH_TOKEN nao configurado.\n\n" +
69
+ " export TURSO_AUTH_TOKEN=$(turso auth token)\n" +
70
+ " export TURSO_ORG=sua-org"
71
+ );
72
+ }
73
+ return {
74
+ "Authorization": `Bearer ${authToken}`,
75
+ "Content-Type": "application/json",
76
+ };
77
+ }
78
+
79
+ // ═══════════════════════════════════════════════════════════════
80
+ // Turso API: buscar hostname real do DB (inclui regiao)
81
+ // GET /v1/organizations/{org}/databases/{name}
82
+ // Retorna { database: { Hostname: "name-org.region.turso.io" } }
83
+ // ═══════════════════════════════════════════════════════════════
84
+
85
+ async function fetchDatabaseUrl(org: string, dbName: string): Promise<string | null> {
86
+ try {
87
+ const response = await fetch(
88
+ `${TURSO_API}/organizations/${org}/databases/${dbName}`,
89
+ { headers: tursoHeaders() }
90
+ );
91
+
92
+ if (response.status === 404) return null;
93
+
94
+ if (response.status === 401 || response.status === 403) {
95
+ const body = await response.text();
96
+ throw new Error(
97
+ `Autenticacao Turso falhou (${response.status}).\n` +
98
+ ` ${body}\n\n` +
99
+ "Verifique:\n" +
100
+ " 1. TURSO_AUTH_TOKEN esta correto e nao expirado\n" +
101
+ " 2. TURSO_ORG corresponde a sua organizacao no Turso\n\n" +
102
+ " Gerar novo token: turso auth token"
103
+ );
104
+ }
105
+
106
+ if (!response.ok) return null;
107
+
108
+ const data = await response.json() as any;
109
+ const hostname = data.database?.Hostname;
110
+ if (hostname) return `libsql://${hostname}`;
111
+ return null;
112
+ } catch (e: any) {
113
+ if (e.message?.includes("Autenticacao Turso")) throw e;
114
+ return null;
115
+ }
116
+ }
117
+
118
+ // ═══════════════════════════════════════════════════════════════
119
+ // Turso API: criar DB e retornar hostname real
120
+ // POST /v1/organizations/{org}/databases
121
+ // ═══════════════════════════════════════════════════════════════
122
+
123
+ async function createDatabase(org: string, dbName: string): Promise<string> {
124
+ const group = process.env.TURSO_GROUP || "default";
125
+
126
+ const response = await fetch(
127
+ `${TURSO_API}/organizations/${org}/databases`,
128
+ {
129
+ method: "POST",
130
+ headers: tursoHeaders(),
131
+ body: JSON.stringify({ name: dbName, group }),
132
+ }
133
+ );
134
+
135
+ if (response.ok) {
136
+ console.log(`[codexa] Banco criado no Turso: ${dbName}`);
137
+ await new Promise((r) => setTimeout(r, 2000));
138
+
139
+ // Buscar hostname real (a resposta do POST pode nao ter o formato completo)
140
+ const url = await fetchDatabaseUrl(org, dbName);
141
+ if (url) return url;
142
+
143
+ // Fallback: construir URL sem regiao (pode funcionar em algumas configs)
144
+ return `libsql://${dbName}-${org}.turso.io`;
145
+ }
146
+
147
+ if (response.status === 409) {
148
+ // Ja existe — buscar hostname real
149
+ const url = await fetchDatabaseUrl(org, dbName);
150
+ if (url) return url;
151
+ return `libsql://${dbName}-${org}.turso.io`;
152
+ }
153
+
154
+ if (response.status === 401 || response.status === 403) {
155
+ const body = await response.text();
156
+ throw new Error(
157
+ `Autenticacao Turso falhou (${response.status}).\n` +
158
+ ` ${body}\n\n` +
159
+ "Verifique:\n" +
160
+ " 1. TURSO_AUTH_TOKEN esta correto e nao expirado\n" +
161
+ " 2. TURSO_ORG corresponde a sua organizacao no Turso\n\n" +
162
+ " Gerar novo token: turso auth token"
163
+ );
164
+ }
165
+
166
+ const body = await response.text();
167
+ throw new Error(
168
+ `Falha ao criar banco no Turso (${response.status}): ${body}\n` +
169
+ ` DB: ${dbName} | Org: ${org} | Group: ${group}`
170
+ );
171
+ }
172
+
76
173
  // ═══════════════════════════════════════════════════════════════
77
- // Provisioning: cria o DB no Turso se nao existir
78
- // Usa API-first (POST idempotente) para evitar bug de
79
- // compatibilidade Bun no @libsql/hrana-client (resp.body.cancel)
174
+ // Provisioning: garante que o DB existe e resolve a URL real
80
175
  // ═══════════════════════════════════════════════════════════════
81
176
 
82
177
  export async function ensureDatabase(): Promise<void> {
83
178
  if (dbProvisioned) return;
84
179
 
85
- const url = resolveDbUrl();
86
-
87
- // Somente provisionar para URLs Turso (nao para file: ou :memory:)
88
- if (!url.startsWith("libsql://")) {
180
+ // Se ja tem URL resolvida (env ou config), apenas marcar como provisionado
181
+ try {
182
+ resolveDbUrl();
89
183
  dbProvisioned = true;
90
184
  return;
185
+ } catch (e) {
186
+ if (!(e instanceof NeedsProvisioningError)) throw e;
91
187
  }
92
188
 
93
- const authToken = process.env.TURSO_AUTH_TOKEN;
189
+ // Precisa provisionar via API
94
190
  const org = process.env.TURSO_ORG;
95
- if (!authToken || !org) {
96
- dbProvisioned = true;
97
- return;
191
+ if (!org) {
192
+ throw new Error(
193
+ "Banco de dados nao configurado.\n\n" +
194
+ "Opcao 1 — Configurar automaticamente:\n" +
195
+ " export TURSO_ORG=sua-org\n" +
196
+ " export TURSO_AUTH_TOKEN=eyJ...\n" +
197
+ " codexa init\n\n" +
198
+ "Opcao 2 — Configurar manualmente:\n" +
199
+ " export CODEXA_DB_URL=libsql://seu-db.turso.io\n" +
200
+ " export TURSO_AUTH_TOKEN=eyJ..."
201
+ );
98
202
  }
99
203
 
100
- // Extrair nome do DB da URL: libsql://codexa-slug-org.turso.io → codexa-slug
101
- const hostname = url.replace("libsql://", "").replace(".turso.io", "");
102
- // O nome do DB no Turso e o hostname completo (sem .turso.io)
103
- // Mas a API espera o nome curto. O hostname = name-org, entao name = hostname sem -org no final
104
- const dbName = hostname.endsWith(`-${org}`)
105
- ? hostname.slice(0, -(org.length + 1))
106
- : hostname;
107
-
108
- // API-first: tenta criar. 409 = ja existe (ok). 200 = criado.
109
- // Isso evita o bug Bun no @libsql/hrana-client que ocorre quando
110
- // tentamos conectar a um DB inexistente (resp.body?.cancel not a function)
111
- const group = process.env.TURSO_GROUP || "default";
112
- try {
113
- const response = await fetch(
114
- `https://api.turso.tech/v1/organizations/${org}/databases`,
115
- {
116
- method: "POST",
117
- headers: {
118
- "Authorization": `Bearer ${authToken}`,
119
- "Content-Type": "application/json",
120
- },
121
- body: JSON.stringify({ name: dbName, group }),
122
- }
123
- );
204
+ const dbName = deriveDbNameFromGit();
124
205
 
125
- if (response.ok) {
126
- console.log(`[codexa] Banco criado no Turso: ${dbName}`);
127
- // Aguardar um momento para o DB ficar disponivel
128
- await new Promise((r) => setTimeout(r, 2000));
129
- } else if (response.status === 409) {
130
- // Ja existe tudo certo
131
- } else {
132
- const body = await response.text();
133
- console.error(`[codexa] Falha ao criar banco (${response.status}): ${body}`);
134
- }
135
- } catch (e: any) {
136
- console.error(`[codexa] Erro ao provisionar banco: ${e.message}`);
206
+ // 1. Verificar se o DB ja existe
207
+ let url = await fetchDatabaseUrl(org, dbName);
208
+
209
+ // 2. Se nao existe, criar
210
+ if (!url) {
211
+ url = await createDatabase(org, dbName);
137
212
  }
138
213
 
214
+ // 3. Salvar no config para proximas execucoes
215
+ saveConfig(url);
139
216
  dbProvisioned = true;
217
+
218
+ // 4. Resetar client para usar a nova URL
219
+ if (client) {
220
+ client.close();
221
+ client = null;
222
+ }
140
223
  }
141
224
 
142
225
  // ═══════════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.33",
3
+ "version": "9.0.34",
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": {