@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.
Files changed (2) hide show
  1. package/db/connection.ts +194 -88
  2. 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
- // Resolucao de DB URL: env config local → derivacao do git
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
- function resolveDbUrl(): string {
14
- // 1. Override explicito via env var
15
- if (process.env.CODEXA_DB_URL) return process.env.CODEXA_DB_URL;
25
+ interface CodexaConfig {
26
+ database?: string;
27
+ token?: string;
28
+ }
16
29
 
17
- // 2. Cache local (.codexa/config.json)
18
- const configPath = join(process.cwd(), ".codexa", "config.json");
30
+ function loadConfig(): CodexaConfig {
31
+ const configPath = CONFIG_PATH();
19
32
  if (existsSync(configPath)) {
20
33
  try {
21
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
22
- if (config.database) return config.database;
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
- // 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
+ 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
- const slug = deriveSlugFromGit();
42
- const url = `libsql://codexa-${slug}-${org}.turso.io`;
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
- // Salvar no cache para proximas execucoes
45
- saveConfig(configPath, url);
68
+ // 2. Token do config (mintado durante provisioning)
69
+ const config = loadConfig();
70
+ if (config.token) return config.token;
46
71
 
47
- return url;
72
+ return undefined;
48
73
  }
49
74
 
50
- function deriveSlugFromGit(): string {
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 `${match[1]}--${match[2]}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
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
- function saveConfig(configPath: string, url: string): void {
67
- try {
68
- const dir = dirname(configPath);
69
- if (!existsSync(dir)) {
70
- mkdirSync(dir, { recursive: true });
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
- writeFileSync(configPath, JSON.stringify({ database: url }, null, 2) + "\n");
73
- } catch { /* falha ao salvar cache, nao e critico */ }
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 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)
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
- const url = resolveDbUrl();
86
-
87
- // Somente provisionar para URLs Turso (nao para file: ou :memory:)
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
- const authToken = process.env.TURSO_AUTH_TOKEN;
208
+ // Precisa provisionar via Platform API
94
209
  const org = process.env.TURSO_ORG;
95
- if (!authToken || !org) {
96
- dbProvisioned = true;
97
- return;
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
- // 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
- );
223
+ const dbName = deriveDbNameFromGit();
124
224
 
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}`);
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 = process.env.TURSO_AUTH_TOKEN;
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.33",
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": {