@codexa/cli 9.0.33 → 9.0.35
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 +194 -88
- package/package.json +1 -1
package/db/connection.ts
CHANGED
|
@@ -6,137 +6,243 @@ 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
|
-
//
|
|
13
|
+
// Config: database URL + database token (cached locally)
|
|
14
|
+
//
|
|
15
|
+
// Tokens no Turso:
|
|
16
|
+
// TURSO_API_TOKEN → Platform API (criar/gerenciar DBs)
|
|
17
|
+
// TURSO_AUTH_TOKEN → Conexao ao DB (libSQL protocol)
|
|
18
|
+
//
|
|
19
|
+
// O auto-provisioning usa TURSO_API_TOKEN para criar o DB e
|
|
20
|
+
// mintar um database token, que e salvo no config.json junto
|
|
21
|
+
// com a URL. Depois disso, so TURSO_AUTH_TOKEN e necessario
|
|
22
|
+
// (ou o token do config).
|
|
11
23
|
// ═══════════════════════════════════════════════════════════════
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
interface CodexaConfig {
|
|
26
|
+
database?: string;
|
|
27
|
+
token?: string;
|
|
28
|
+
}
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
const configPath =
|
|
30
|
+
function loadConfig(): CodexaConfig {
|
|
31
|
+
const configPath = CONFIG_PATH();
|
|
19
32
|
if (existsSync(configPath)) {
|
|
20
33
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} catch { /* config invalido, continuar */ }
|
|
34
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
35
|
+
} catch { /* config invalido */ }
|
|
24
36
|
}
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
function saveConfig(config: CodexaConfig): void {
|
|
41
|
+
try {
|
|
42
|
+
const configPath = CONFIG_PATH();
|
|
43
|
+
const dir = dirname(configPath);
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
48
|
+
} catch { /* falha ao salvar cache, nao e critico */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════
|
|
52
|
+
// Resolucao: URL e token para conexao
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
function resolveDbUrl(): string {
|
|
56
|
+
if (process.env.CODEXA_DB_URL) return process.env.CODEXA_DB_URL;
|
|
57
|
+
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
if (config.database) return config.database;
|
|
60
|
+
|
|
61
|
+
throw new NeedsProvisioningError();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveDbToken(): string | undefined {
|
|
65
|
+
// 1. Env var explicita (sempre tem prioridade)
|
|
66
|
+
if (process.env.TURSO_AUTH_TOKEN) return process.env.TURSO_AUTH_TOKEN;
|
|
43
67
|
|
|
44
|
-
//
|
|
45
|
-
|
|
68
|
+
// 2. Token do config (mintado durante provisioning)
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
if (config.token) return config.token;
|
|
46
71
|
|
|
47
|
-
return
|
|
72
|
+
return undefined;
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
|
|
75
|
+
class NeedsProvisioningError extends Error {
|
|
76
|
+
constructor() { super("DB_NEEDS_PROVISIONING"); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function deriveDbNameFromGit(): string {
|
|
51
80
|
try {
|
|
52
81
|
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
82
|
const match = remote.match(/[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
56
83
|
if (match) {
|
|
57
|
-
return
|
|
84
|
+
return match[2].toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
58
85
|
}
|
|
59
86
|
} catch { /* git nao disponivel */ }
|
|
60
87
|
|
|
61
|
-
// Fallback: nome da pasta atual
|
|
62
88
|
const dirName = process.cwd().split(/[/\\]/).pop() || "unknown";
|
|
63
89
|
return dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
64
90
|
}
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
// ═══════════════════════════════════════════════════════════════
|
|
93
|
+
// Turso Platform API helpers
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════
|
|
95
|
+
|
|
96
|
+
function apiToken(): string {
|
|
97
|
+
const token = process.env.TURSO_API_TOKEN || process.env.TURSO_AUTH_TOKEN;
|
|
98
|
+
if (!token) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"Token Turso nao configurado.\n\n" +
|
|
101
|
+
"Para auto-provisioning:\n" +
|
|
102
|
+
" export TURSO_API_TOKEN=<platform-api-token>\n" +
|
|
103
|
+
" export TURSO_ORG=sua-org\n\n" +
|
|
104
|
+
"Gerar API token: turso auth api-tokens mint codexa\n" +
|
|
105
|
+
"Ou usar sessao CLI: export TURSO_API_TOKEN=$(turso auth token)"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return token;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function apiHeaders(): Record<string, string> {
|
|
112
|
+
return {
|
|
113
|
+
"Authorization": `Bearer ${apiToken()}`,
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function apiFetchDatabase(org: string, dbName: string): Promise<string | null> {
|
|
119
|
+
const response = await fetch(
|
|
120
|
+
`${TURSO_API}/organizations/${org}/databases/${dbName}`,
|
|
121
|
+
{ headers: apiHeaders() }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (response.status === 404) return null;
|
|
125
|
+
if (response.status === 401 || response.status === 403) {
|
|
126
|
+
throwAuthError();
|
|
127
|
+
}
|
|
128
|
+
if (!response.ok) return null;
|
|
129
|
+
|
|
130
|
+
const data = await response.json() as any;
|
|
131
|
+
const hostname = data.database?.Hostname;
|
|
132
|
+
return hostname ? `libsql://${hostname}` : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function apiCreateDatabase(org: string, dbName: string): Promise<string> {
|
|
136
|
+
const group = process.env.TURSO_GROUP || "default";
|
|
137
|
+
|
|
138
|
+
const response = await fetch(
|
|
139
|
+
`${TURSO_API}/organizations/${org}/databases`,
|
|
140
|
+
{
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: apiHeaders(),
|
|
143
|
+
body: JSON.stringify({ name: dbName, group }),
|
|
71
144
|
}
|
|
72
|
-
|
|
73
|
-
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (response.ok) {
|
|
148
|
+
console.log(`[codexa] Banco criado no Turso: ${dbName}`);
|
|
149
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
150
|
+
return (await apiFetchDatabase(org, dbName)) || `libsql://${dbName}-${org}.turso.io`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (response.status === 409) {
|
|
154
|
+
return (await apiFetchDatabase(org, dbName)) || `libsql://${dbName}-${org}.turso.io`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (response.status === 401 || response.status === 403) {
|
|
158
|
+
throwAuthError();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const body = await response.text();
|
|
162
|
+
throw new Error(`Falha ao criar banco (${response.status}): ${body}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function apiMintDbToken(org: string, dbName: string): Promise<string> {
|
|
166
|
+
const response = await fetch(
|
|
167
|
+
`${TURSO_API}/organizations/${org}/databases/${dbName}/auth/tokens`,
|
|
168
|
+
{ method: "POST", headers: apiHeaders() }
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const body = await response.text();
|
|
173
|
+
throw new Error(`Falha ao criar database token (${response.status}): ${body}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const data = await response.json() as any;
|
|
177
|
+
return data.jwt;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function throwAuthError(): never {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"Turso Platform API rejeitou o token.\n\n" +
|
|
183
|
+
"Para auto-provisioning, use um Platform API token:\n" +
|
|
184
|
+
" turso auth api-tokens mint codexa\n" +
|
|
185
|
+
" export TURSO_API_TOKEN=<token-gerado>\n\n" +
|
|
186
|
+
"Ou se o banco ja existe, aponte direto:\n" +
|
|
187
|
+
" export CODEXA_DB_URL=libsql://seu-db.region.turso.io\n" +
|
|
188
|
+
" export TURSO_AUTH_TOKEN=<database-token>"
|
|
189
|
+
);
|
|
74
190
|
}
|
|
75
191
|
|
|
76
192
|
// ═══════════════════════════════════════════════════════════════
|
|
77
|
-
// Provisioning: cria
|
|
78
|
-
// Usa API-first (POST idempotente) para evitar bug de
|
|
79
|
-
// compatibilidade Bun no @libsql/hrana-client (resp.body.cancel)
|
|
193
|
+
// Provisioning: cria DB + minta token + salva config
|
|
80
194
|
// ═══════════════════════════════════════════════════════════════
|
|
81
195
|
|
|
82
196
|
export async function ensureDatabase(): Promise<void> {
|
|
83
197
|
if (dbProvisioned) return;
|
|
84
198
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!url.startsWith("libsql://")) {
|
|
199
|
+
// Se ja tem URL resolvida (env ou config), nada a fazer
|
|
200
|
+
try {
|
|
201
|
+
resolveDbUrl();
|
|
89
202
|
dbProvisioned = true;
|
|
90
203
|
return;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (!(e instanceof NeedsProvisioningError)) throw e;
|
|
91
206
|
}
|
|
92
207
|
|
|
93
|
-
|
|
208
|
+
// Precisa provisionar via Platform API
|
|
94
209
|
const org = process.env.TURSO_ORG;
|
|
95
|
-
if (!
|
|
96
|
-
|
|
97
|
-
|
|
210
|
+
if (!org) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
"Banco de dados nao configurado.\n\n" +
|
|
213
|
+
"Opcao 1 — Auto-provisioning:\n" +
|
|
214
|
+
" export TURSO_ORG=sua-org\n" +
|
|
215
|
+
" export TURSO_API_TOKEN=<platform-api-token>\n" +
|
|
216
|
+
" codexa init\n\n" +
|
|
217
|
+
"Opcao 2 — Apontar para banco existente:\n" +
|
|
218
|
+
" export CODEXA_DB_URL=libsql://seu-db.region.turso.io\n" +
|
|
219
|
+
" export TURSO_AUTH_TOKEN=<database-token>"
|
|
220
|
+
);
|
|
98
221
|
}
|
|
99
222
|
|
|
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
|
-
);
|
|
223
|
+
const dbName = deriveDbNameFromGit();
|
|
124
224
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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}`);
|
|
225
|
+
// 1. Buscar ou criar o DB
|
|
226
|
+
let url = await apiFetchDatabase(org, dbName);
|
|
227
|
+
if (!url) {
|
|
228
|
+
url = await apiCreateDatabase(org, dbName);
|
|
137
229
|
}
|
|
138
230
|
|
|
231
|
+
// 2. Mintar um database token para conexao
|
|
232
|
+
console.log(`[codexa] Gerando token de acesso...`);
|
|
233
|
+
const dbToken = await apiMintDbToken(org, dbName);
|
|
234
|
+
|
|
235
|
+
// 3. Salvar tudo no config (URL + token)
|
|
236
|
+
saveConfig({ database: url, token: dbToken });
|
|
237
|
+
console.log(`[codexa] Configuracao salva em .codexa/config.json`);
|
|
238
|
+
|
|
139
239
|
dbProvisioned = true;
|
|
240
|
+
|
|
241
|
+
// 4. Resetar client para usar a nova URL/token
|
|
242
|
+
if (client) {
|
|
243
|
+
client.close();
|
|
244
|
+
client = null;
|
|
245
|
+
}
|
|
140
246
|
}
|
|
141
247
|
|
|
142
248
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -146,7 +252,7 @@ export async function ensureDatabase(): Promise<void> {
|
|
|
146
252
|
export function getDb(): Client {
|
|
147
253
|
if (!client) {
|
|
148
254
|
const url = resolveDbUrl();
|
|
149
|
-
const authToken =
|
|
255
|
+
const authToken = resolveDbToken();
|
|
150
256
|
|
|
151
257
|
client = createClient({
|
|
152
258
|
url,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexa/cli",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.35",
|
|
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": {
|