@codexa/cli 9.0.34 → 9.0.36

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.
@@ -0,0 +1,137 @@
1
+ import { createClient, type Client } from "@libsql/client";
2
+ import { initSchema } from "../db/schema";
3
+ import { getDb } from "../db/connection";
4
+ import { existsSync } from "fs";
5
+ import { join } from "path";
6
+
7
+ const TABLES_IN_ORDER = [
8
+ "specs",
9
+ "project",
10
+ "standards",
11
+ "product_context",
12
+ "lib_contexts",
13
+ "agent_lib_mappings",
14
+ "implementation_patterns",
15
+ "architectural_analyses",
16
+ "project_utilities",
17
+ "schema_migrations",
18
+ "context",
19
+ "tasks",
20
+ "decisions",
21
+ "artifacts",
22
+ "review",
23
+ "snapshots",
24
+ "knowledge",
25
+ "gate_bypasses",
26
+ "product_goals",
27
+ "product_features",
28
+ "knowledge_graph",
29
+ "reasoning_log",
30
+ "agent_performance",
31
+ "knowledge_acknowledgments",
32
+ ];
33
+
34
+ interface MigrateOptions {
35
+ dbPath?: string;
36
+ dryRun?: boolean;
37
+ }
38
+
39
+ export async function migrateToTurso(options: MigrateOptions = {}): Promise<void> {
40
+ const localPath = options.dbPath || join(process.cwd(), ".codexa", "db", "workflow.db");
41
+
42
+ if (!existsSync(localPath)) {
43
+ console.error(`\nArquivo nao encontrado: ${localPath}`);
44
+ console.error("Use --db-path para especificar o caminho do workflow.db\n");
45
+ return;
46
+ }
47
+
48
+ console.log(`\n${"═".repeat(50)}`);
49
+ console.log("MIGRACAO: SQLite local → Turso");
50
+ console.log(`${"═".repeat(50)}\n`);
51
+ console.log(`Origem: ${localPath}`);
52
+
53
+ // 1. Abrir DB local via @libsql/client file: mode
54
+ const localClient = createClient({ url: `file:${localPath}` });
55
+
56
+ // 2. Garantir que o schema remoto esta atualizado
57
+ console.log("Destino: inicializando schema no Turso...");
58
+ await initSchema();
59
+ const remoteClient = getDb();
60
+
61
+ const remoteUrl = process.env.CODEXA_DB_URL || "(config.json)";
62
+ console.log(`Destino: ${remoteUrl}\n`);
63
+
64
+ if (options.dryRun) {
65
+ console.log("[DRY RUN] Nenhum dado sera escrito.\n");
66
+ }
67
+
68
+ // 3. Para cada tabela, ler colunas do schema remoto
69
+ let totalRows = 0;
70
+ let totalTables = 0;
71
+
72
+ for (const table of TABLES_IN_ORDER) {
73
+ // Verificar se a tabela existe no local
74
+ const localTableCheck = await localClient.execute({
75
+ sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
76
+ args: [table],
77
+ });
78
+ if (localTableCheck.rows.length === 0) continue;
79
+
80
+ // Pegar colunas do schema remoto (as que importam)
81
+ const remoteColumns = await remoteClient.execute(`PRAGMA table_info(${table})`);
82
+ const columnNames = remoteColumns.rows.map((r: any) => r.name as string);
83
+
84
+ if (columnNames.length === 0) continue;
85
+
86
+ // Pegar colunas do local para saber quais existem nos dois
87
+ const localColumns = await localClient.execute(`PRAGMA table_info(${table})`);
88
+ const localColumnNames = new Set(localColumns.rows.map((r: any) => r.name as string));
89
+
90
+ // Usar apenas colunas que existem em ambos
91
+ const commonColumns = columnNames.filter((c) => localColumnNames.has(c));
92
+ if (commonColumns.length === 0) continue;
93
+
94
+ // Ler dados do local (apenas colunas comuns)
95
+ const selectSql = `SELECT ${commonColumns.map((c) => `"${c}"`).join(", ")} FROM "${table}"`;
96
+ const localData = await localClient.execute(selectSql);
97
+
98
+ if (localData.rows.length === 0) continue;
99
+
100
+ const count = localData.rows.length;
101
+ totalRows += count;
102
+ totalTables++;
103
+
104
+ if (options.dryRun) {
105
+ console.log(` ${table}: ${count} registros`);
106
+ continue;
107
+ }
108
+
109
+ // Inserir no remoto em batches (max 20 statements por batch)
110
+ const placeholders = commonColumns.map(() => "?").join(", ");
111
+ const insertSql = `INSERT OR IGNORE INTO "${table}" (${commonColumns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
112
+
113
+ const BATCH_SIZE = 20;
114
+ for (let i = 0; i < localData.rows.length; i += BATCH_SIZE) {
115
+ const batch = localData.rows.slice(i, i + BATCH_SIZE);
116
+ await remoteClient.batch(
117
+ batch.map((row: any) => ({
118
+ sql: insertSql,
119
+ args: commonColumns.map((col) => row[col] ?? null),
120
+ })),
121
+ "write"
122
+ );
123
+ }
124
+
125
+ console.log(` ${table}: ${count} registros migrados`);
126
+ }
127
+
128
+ localClient.close();
129
+
130
+ console.log(`\n${"─".repeat(50)}`);
131
+ if (options.dryRun) {
132
+ console.log(`[DRY RUN] ${totalRows} registros em ${totalTables} tabelas seriam migrados.`);
133
+ } else {
134
+ console.log(`Migracao concluida: ${totalRows} registros em ${totalTables} tabelas.`);
135
+ }
136
+ console.log();
137
+ }
package/db/connection.ts CHANGED
@@ -10,31 +10,70 @@ const TURSO_API = "https://api.turso.tech/v1";
10
10
  const CONFIG_PATH = () => join(process.cwd(), ".codexa", "config.json");
11
11
 
12
12
  // ═══════════════════════════════════════════════════════════════
13
- // Resolucao de DB URL: env config local → provisionar via API
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).
14
23
  // ═══════════════════════════════════════════════════════════════
15
24
 
16
- function resolveDbUrl(): string {
17
- // 1. Override explicito via env var
18
- if (process.env.CODEXA_DB_URL) return process.env.CODEXA_DB_URL;
25
+ interface CodexaConfig {
26
+ database?: string;
27
+ token?: string;
28
+ }
19
29
 
20
- // 2. Cache local (.codexa/config.json)
30
+ function loadConfig(): CodexaConfig {
21
31
  const configPath = CONFIG_PATH();
22
32
  if (existsSync(configPath)) {
23
33
  try {
24
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
25
- if (config.database) return config.database;
26
- } catch { /* config invalido, continuar */ }
34
+ return JSON.parse(readFileSync(configPath, "utf-8"));
35
+ } catch { /* config invalido */ }
27
36
  }
37
+ return {};
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
+ // ═══════════════════════════════════════════════════════════════
54
+
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;
28
60
 
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
61
  throw new NeedsProvisioningError();
32
62
  }
33
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;
67
+
68
+ // 2. Token do config (mintado durante provisioning)
69
+ const config = loadConfig();
70
+ if (config.token) return config.token;
71
+
72
+ return undefined;
73
+ }
74
+
34
75
  class NeedsProvisioningError extends Error {
35
- constructor() {
36
- super("DB_NEEDS_PROVISIONING");
37
- }
76
+ constructor() { super("DB_NEEDS_PROVISIONING"); }
38
77
  }
39
78
 
40
79
  function deriveDbNameFromGit(): string {
@@ -50,84 +89,57 @@ function deriveDbNameFromGit(): string {
50
89
  return dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
51
90
  }
52
91
 
53
- function saveConfig(url: string): void {
54
- try {
55
- const configPath = CONFIG_PATH();
56
- const dir = dirname(configPath);
57
- if (!existsSync(dir)) {
58
- mkdirSync(dir, { recursive: true });
59
- }
60
- writeFileSync(configPath, JSON.stringify({ database: url }, null, 2) + "\n");
61
- } catch { /* falha ao salvar cache, nao e critico */ }
62
- }
92
+ // ═══════════════════════════════════════════════════════════════
93
+ // Turso Platform API helpers
94
+ // ═══════════════════════════════════════════════════════════════
63
95
 
64
- function tursoHeaders(): Record<string, string> {
65
- const authToken = process.env.TURSO_AUTH_TOKEN;
66
- if (!authToken) {
96
+ function apiToken(): string {
97
+ const token = process.env.TURSO_API_TOKEN || process.env.TURSO_AUTH_TOKEN;
98
+ if (!token) {
67
99
  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"
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)"
71
106
  );
72
107
  }
108
+ return token;
109
+ }
110
+
111
+ function apiHeaders(): Record<string, string> {
73
112
  return {
74
- "Authorization": `Bearer ${authToken}`,
113
+ "Authorization": `Bearer ${apiToken()}`,
75
114
  "Content-Type": "application/json",
76
115
  };
77
116
  }
78
117
 
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;
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
+ );
107
123
 
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;
124
+ if (response.status === 404) return null;
125
+ if (response.status === 401 || response.status === 403) {
126
+ throwAuthError();
115
127
  }
116
- }
128
+ if (!response.ok) return null;
117
129
 
118
- // ═══════════════════════════════════════════════════════════════
119
- // Turso API: criar DB e retornar hostname real
120
- // POST /v1/organizations/{org}/databases
121
- // ═══════════════════════════════════════════════════════════════
130
+ const data = await response.json() as any;
131
+ const hostname = data.database?.Hostname;
132
+ return hostname ? `libsql://${hostname}` : null;
133
+ }
122
134
 
123
- async function createDatabase(org: string, dbName: string): Promise<string> {
135
+ async function apiCreateDatabase(org: string, dbName: string): Promise<string> {
124
136
  const group = process.env.TURSO_GROUP || "default";
125
137
 
126
138
  const response = await fetch(
127
139
  `${TURSO_API}/organizations/${org}/databases`,
128
140
  {
129
141
  method: "POST",
130
- headers: tursoHeaders(),
142
+ headers: apiHeaders(),
131
143
  body: JSON.stringify({ name: dbName, group }),
132
144
  }
133
145
  );
@@ -135,49 +147,56 @@ async function createDatabase(org: string, dbName: string): Promise<string> {
135
147
  if (response.ok) {
136
148
  console.log(`[codexa] Banco criado no Turso: ${dbName}`);
137
149
  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`;
150
+ return (await apiFetchDatabase(org, dbName)) || `libsql://${dbName}-${org}.turso.io`;
145
151
  }
146
152
 
147
153
  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`;
154
+ return (await apiFetchDatabase(org, dbName)) || `libsql://${dbName}-${org}.turso.io`;
152
155
  }
153
156
 
154
157
  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
- );
158
+ throwAuthError();
164
159
  }
165
160
 
166
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 {
167
181
  throw new Error(
168
- `Falha ao criar banco no Turso (${response.status}): ${body}\n` +
169
- ` DB: ${dbName} | Org: ${org} | Group: ${group}`
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>"
170
189
  );
171
190
  }
172
191
 
173
192
  // ═══════════════════════════════════════════════════════════════
174
- // Provisioning: garante que o DB existe e resolve a URL real
193
+ // Provisioning: cria DB + minta token + salva config
175
194
  // ═══════════════════════════════════════════════════════════════
176
195
 
177
196
  export async function ensureDatabase(): Promise<void> {
178
197
  if (dbProvisioned) return;
179
198
 
180
- // Se ja tem URL resolvida (env ou config), apenas marcar como provisionado
199
+ // Se ja tem URL resolvida (env ou config), nada a fazer
181
200
  try {
182
201
  resolveDbUrl();
183
202
  dbProvisioned = true;
@@ -186,36 +205,40 @@ export async function ensureDatabase(): Promise<void> {
186
205
  if (!(e instanceof NeedsProvisioningError)) throw e;
187
206
  }
188
207
 
189
- // Precisa provisionar via API
208
+ // Precisa provisionar via Platform API
190
209
  const org = process.env.TURSO_ORG;
191
210
  if (!org) {
192
211
  throw new Error(
193
212
  "Banco de dados nao configurado.\n\n" +
194
- "Opcao 1 — Configurar automaticamente:\n" +
213
+ "Opcao 1 — Auto-provisioning:\n" +
195
214
  " export TURSO_ORG=sua-org\n" +
196
- " export TURSO_AUTH_TOKEN=eyJ...\n" +
215
+ " export TURSO_API_TOKEN=<platform-api-token>\n" +
197
216
  " 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..."
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>"
201
220
  );
202
221
  }
203
222
 
204
223
  const dbName = deriveDbNameFromGit();
205
224
 
206
- // 1. Verificar se o DB ja existe
207
- let url = await fetchDatabaseUrl(org, dbName);
208
-
209
- // 2. Se nao existe, criar
225
+ // 1. Buscar ou criar o DB
226
+ let url = await apiFetchDatabase(org, dbName);
210
227
  if (!url) {
211
- url = await createDatabase(org, dbName);
228
+ url = await apiCreateDatabase(org, dbName);
212
229
  }
213
230
 
214
- // 3. Salvar no config para proximas execucoes
215
- saveConfig(url);
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
+
216
239
  dbProvisioned = true;
217
240
 
218
- // 4. Resetar client para usar a nova URL
241
+ // 4. Resetar client para usar a nova URL/token
219
242
  if (client) {
220
243
  client.close();
221
244
  client = null;
@@ -229,7 +252,7 @@ export async function ensureDatabase(): Promise<void> {
229
252
  export function getDb(): Client {
230
253
  if (!client) {
231
254
  const url = resolveDbUrl();
232
- const authToken = process.env.TURSO_AUTH_TOKEN;
255
+ const authToken = resolveDbToken();
233
256
 
234
257
  client = createClient({
235
258
  url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.34",
3
+ "version": "9.0.36",
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": {
package/workflow.ts CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  architectCancel,
42
42
  } from "./commands/architect";
43
43
  import { teamSuggest } from "./commands/team";
44
+ import { migrateToTurso } from "./commands/migrate";
44
45
  import { initSchema } from "./db/schema";
45
46
  import { closeDb, dbGet, dbRun, ensureDatabase, getResolvedDbUrl } from "./db/connection";
46
47
  import { execSync } from "child_process";
@@ -574,6 +575,15 @@ program
574
575
  console.log(`\nBanco de dados inicializado: ${url}\n`);
575
576
  });
576
577
 
578
+ program
579
+ .command("migrate")
580
+ .description("Migra dados do SQLite local (.codexa/db/workflow.db) para o Turso")
581
+ .option("--db-path <path>", "Caminho do workflow.db local (padrao: .codexa/db/workflow.db)")
582
+ .option("--dry-run", "Simula a migracao sem escrever dados")
583
+ .action(async (options) => {
584
+ await migrateToTurso({ dbPath: options.dbPath, dryRun: options.dryRun });
585
+ });
586
+
577
587
  // ═══════════════════════════════════════════════════════════════
578
588
  // DISCOVER
579
589
  // ═══════════════════════════════════════════════════════════════