@codexa/cli 9.0.32 → 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.
package/db/connection.ts CHANGED
@@ -4,9 +4,13 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
 
6
6
  let client: Client | null = null;
7
+ let dbProvisioned = false;
8
+
9
+ const TURSO_API = "https://api.turso.tech/v1";
10
+ const CONFIG_PATH = () => join(process.cwd(), ".codexa", "config.json");
7
11
 
8
12
  // ═══════════════════════════════════════════════════════════════
9
- // Resolucao de DB URL: env → config local → derivacao do git
13
+ // Resolucao de DB URL: env → config local → provisionar via API
10
14
  // ═══════════════════════════════════════════════════════════════
11
15
 
12
16
  function resolveDbUrl(): string {
@@ -14,7 +18,7 @@ function resolveDbUrl(): string {
14
18
  if (process.env.CODEXA_DB_URL) return process.env.CODEXA_DB_URL;
15
19
 
16
20
  // 2. Cache local (.codexa/config.json)
17
- const configPath = join(process.cwd(), ".codexa", "config.json");
21
+ const configPath = CONFIG_PATH();
18
22
  if (existsSync(configPath)) {
19
23
  try {
20
24
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -22,48 +26,33 @@ function resolveDbUrl(): string {
22
26
  } catch { /* config invalido, continuar */ }
23
27
  }
24
28
 
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);
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
+ }
45
33
 
46
- return url;
34
+ class NeedsProvisioningError extends Error {
35
+ constructor() {
36
+ super("DB_NEEDS_PROVISIONING");
37
+ }
47
38
  }
48
39
 
49
- function deriveSlugFromGit(): string {
40
+ function deriveDbNameFromGit(): string {
50
41
  try {
51
42
  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
43
  const match = remote.match(/[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
55
44
  if (match) {
56
- return `${match[1]}--${match[2]}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
45
+ return match[2].toLowerCase().replace(/[^a-z0-9-]/g, "-");
57
46
  }
58
47
  } catch { /* git nao disponivel */ }
59
48
 
60
- // Fallback: nome da pasta atual
61
49
  const dirName = process.cwd().split(/[/\\]/).pop() || "unknown";
62
50
  return dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
63
51
  }
64
52
 
65
- function saveConfig(configPath: string, url: string): void {
53
+ function saveConfig(url: string): void {
66
54
  try {
55
+ const configPath = CONFIG_PATH();
67
56
  const dir = dirname(configPath);
68
57
  if (!existsSync(dir)) {
69
58
  mkdirSync(dir, { recursive: true });
@@ -72,58 +61,164 @@ function saveConfig(configPath: string, url: string): void {
72
61
  } catch { /* falha ao salvar cache, nao e critico */ }
73
62
  }
74
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
+
75
79
  // ═══════════════════════════════════════════════════════════════
76
- // Provisioning: cria o DB no Turso se nao existir
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" } }
77
83
  // ═══════════════════════════════════════════════════════════════
78
84
 
79
- export async function ensureDatabase(): Promise<void> {
80
- const url = resolveDbUrl();
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
+ );
81
91
 
82
- // Somente provisionar para URLs Turso (nao para file: ou :memory:)
83
- if (!url.startsWith("libsql://")) return;
92
+ if (response.status === 404) return null;
84
93
 
85
- const authToken = process.env.TURSO_AUTH_TOKEN;
86
- const org = process.env.TURSO_ORG;
87
- if (!authToken || !org) return;
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
+ }
88
105
 
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];
106
+ if (!response.ok) return null;
93
107
 
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 */ }
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
+ // ═══════════════════════════════════════════════════════════════
101
122
 
102
- // Criar via Turso Platform API
123
+ async function createDatabase(org: string, dbName: string): Promise<string> {
103
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
+
173
+ // ═══════════════════════════════════════════════════════════════
174
+ // Provisioning: garante que o DB existe e resolve a URL real
175
+ // ═══════════════════════════════════════════════════════════════
176
+
177
+ export async function ensureDatabase(): Promise<void> {
178
+ if (dbProvisioned) return;
179
+
180
+ // Se ja tem URL resolvida (env ou config), apenas marcar como provisionado
104
181
  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
- }
182
+ resolveDbUrl();
183
+ dbProvisioned = true;
184
+ return;
185
+ } catch (e) {
186
+ if (!(e instanceof NeedsProvisioningError)) throw e;
187
+ }
188
+
189
+ // Precisa provisionar via API
190
+ const org = process.env.TURSO_ORG;
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..."
115
201
  );
202
+ }
116
203
 
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}`);
124
- }
125
- } catch (e: any) {
126
- console.error(`[codexa] Erro ao provisionar banco: ${e.message}`);
204
+ const dbName = deriveDbNameFromGit();
205
+
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);
212
+ }
213
+
214
+ // 3. Salvar no config para proximas execucoes
215
+ saveConfig(url);
216
+ dbProvisioned = true;
217
+
218
+ // 4. Resetar client para usar a nova URL
219
+ if (client) {
220
+ client.close();
221
+ client = null;
127
222
  }
128
223
  }
129
224
 
@@ -153,10 +248,12 @@ export function closeDb(): void {
153
248
 
154
249
  export function resetClient(): void {
155
250
  client = null;
251
+ dbProvisioned = false;
156
252
  }
157
253
 
158
254
  export function setClient(c: Client): void {
159
255
  client = c;
256
+ dbProvisioned = true;
160
257
  }
161
258
 
162
259
  export function getResolvedDbUrl(): string {
package/db/schema.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { Client, Transaction } from "@libsql/client";
2
- import { getDb, setClient, dbGet, dbAll, dbRun, dbExec } from "./connection";
2
+ import { getDb, setClient, dbGet, dbAll, dbRun, dbExec, ensureDatabase } from "./connection";
3
3
 
4
4
  export async function initSchema(): Promise<void> {
5
+ await ensureDatabase();
5
6
  await dbExec(`
6
7
  CREATE TABLE IF NOT EXISTS specs (
7
8
  id TEXT PRIMARY KEY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.32",
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": {