@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.
- package/db/connection.ts +159 -76
- 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 →
|
|
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 =
|
|
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.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
+
class NeedsProvisioningError extends Error {
|
|
35
|
+
constructor() {
|
|
36
|
+
super("DB_NEEDS_PROVISIONING");
|
|
37
|
+
}
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
function
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
189
|
+
// Precisa provisionar via API
|
|
94
190
|
const org = process.env.TURSO_ORG;
|
|
95
|
-
if (!
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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.
|
|
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": {
|