@duckcodeailabs/dql-cli 1.6.1 → 1.6.3
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/dist/apps-api.d.ts +20 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +71 -0
- package/dist/apps-api.js.map +1 -1
- package/dist/args.d.ts +2 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +3 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/index-CIMLd3Cb.js +3289 -0
- package/dist/assets/dql-notebook/assets/index-RaDW1A5g.css +1 -0
- package/dist/assets/dql-notebook/index.html +3 -3
- package/dist/commands/app.d.ts +4 -3
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +161 -75
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +7 -1
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/compile.d.ts +1 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +40 -4
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/lineage.d.ts +3 -0
- package/dist/commands/lineage.d.ts.map +1 -1
- package/dist/commands/lineage.js +230 -2
- package/dist/commands/lineage.js.map +1 -1
- package/dist/commands/new.js +61 -2
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/parse.d.ts.map +1 -1
- package/dist/commands/parse.js +13 -3
- package/dist/commands/parse.js.map +1 -1
- package/dist/commands/preview.d.ts.map +1 -1
- package/dist/commands/preview.js +7 -1
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +49 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +6 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +79 -68
- package/dist/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +95 -19
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/tools.d.ts.map +1 -1
- package/dist/llm/tools.js +29 -1
- package/dist/llm/tools.js.map +1 -1
- package/dist/llm/types.d.ts +3 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +12 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +724 -46
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +44 -0
- package/package.json +12 -12
- package/dist/apps-api.test.d.ts +0 -2
- package/dist/apps-api.test.d.ts.map +0 -1
- package/dist/apps-api.test.js +0 -154
- package/dist/apps-api.test.js.map +0 -1
- package/dist/args.test.d.ts +0 -2
- package/dist/args.test.d.ts.map +0 -1
- package/dist/args.test.js +0 -41
- package/dist/args.test.js.map +0 -1
- package/dist/assets/dql-notebook/assets/index-C-s-OCLW.css +0 -1
- package/dist/assets/dql-notebook/assets/index-DZ_5zsCw.js +0 -869
- package/dist/block-studio-import.test.d.ts +0 -2
- package/dist/block-studio-import.test.d.ts.map +0 -1
- package/dist/block-studio-import.test.js +0 -168
- package/dist/block-studio-import.test.js.map +0 -1
- package/dist/commands/build.test.d.ts +0 -2
- package/dist/commands/build.test.d.ts.map +0 -1
- package/dist/commands/build.test.js +0 -44
- package/dist/commands/build.test.js.map +0 -1
- package/dist/commands/compile.test.d.ts +0 -2
- package/dist/commands/compile.test.d.ts.map +0 -1
- package/dist/commands/compile.test.js +0 -115
- package/dist/commands/compile.test.js.map +0 -1
- package/dist/commands/doctor.test.d.ts +0 -2
- package/dist/commands/doctor.test.d.ts.map +0 -1
- package/dist/commands/doctor.test.js +0 -44
- package/dist/commands/doctor.test.js.map +0 -1
- package/dist/commands/init.test.d.ts +0 -2
- package/dist/commands/init.test.d.ts.map +0 -1
- package/dist/commands/init.test.js +0 -178
- package/dist/commands/init.test.js.map +0 -1
- package/dist/commands/new.test.d.ts +0 -2
- package/dist/commands/new.test.d.ts.map +0 -1
- package/dist/commands/new.test.js +0 -191
- package/dist/commands/new.test.js.map +0 -1
- package/dist/commands/sync.test.d.ts +0 -2
- package/dist/commands/sync.test.d.ts.map +0 -1
- package/dist/commands/sync.test.js +0 -147
- package/dist/commands/sync.test.js.map +0 -1
- package/dist/commands/validate.test.d.ts +0 -2
- package/dist/commands/validate.test.d.ts.map +0 -1
- package/dist/commands/validate.test.js +0 -55
- package/dist/commands/validate.test.js.map +0 -1
- package/dist/local-runtime.test.d.ts +0 -2
- package/dist/local-runtime.test.d.ts.map +0 -1
- package/dist/local-runtime.test.js +0 -300
- package/dist/local-runtime.test.js.map +0 -1
- package/dist/metricflow.test.d.ts +0 -2
- package/dist/metricflow.test.d.ts.map +0 -1
- package/dist/metricflow.test.js +0 -54
- package/dist/metricflow.test.js.map +0 -1
- package/dist/promote-from-draft.test.d.ts +0 -2
- package/dist/promote-from-draft.test.d.ts.map +0 -1
- package/dist/promote-from-draft.test.js +0 -149
- package/dist/promote-from-draft.test.js.map +0 -1
- package/dist/semantic-import.test.d.ts +0 -2
- package/dist/semantic-import.test.d.ts.map +0 -1
- package/dist/semantic-import.test.js +0 -95
- package/dist/semantic-import.test.js.map +0 -1
- package/dist/template-adoption.test.d.ts +0 -2
- package/dist/template-adoption.test.d.ts.map +0 -1
- package/dist/template-adoption.test.js +0 -102
- package/dist/template-adoption.test.js.map +0 -1
package/dist/local-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
5
6
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
6
7
|
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, } from '@duckcodeailabs/dql-core';
|
|
@@ -213,6 +214,37 @@ export async function startLocalServer(opts) {
|
|
|
213
214
|
blockPath: block.filePath,
|
|
214
215
|
};
|
|
215
216
|
};
|
|
217
|
+
const executeGeneratedSqlForAgent = async (sql) => {
|
|
218
|
+
const boundedSql = buildAgentPreviewSql(sql);
|
|
219
|
+
const semantic = prepareSemanticSql(boundedSql, semanticLayer);
|
|
220
|
+
if (semantic.unresolvedRefs.length > 0) {
|
|
221
|
+
throw new Error(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
const prepared = prepareLocalExecution(semantic.sql, connection, projectRoot, projectConfig);
|
|
224
|
+
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
225
|
+
assertAppAccess({ app, domain: app?.domain, level: 'execute' });
|
|
226
|
+
const rawResult = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
|
|
227
|
+
const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
|
|
228
|
+
return {
|
|
229
|
+
columns: normalized.columns,
|
|
230
|
+
rows: normalized.rows,
|
|
231
|
+
rowCount: normalized.rowCount,
|
|
232
|
+
executionTime: normalized.executionTime,
|
|
233
|
+
sql: prepared.sql,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const getSchemaContextForAgent = async (question) => {
|
|
237
|
+
try {
|
|
238
|
+
const result = await executor.executeQuery(`SELECT table_schema, table_name, column_name, data_type
|
|
239
|
+
FROM information_schema.columns
|
|
240
|
+
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
|
241
|
+
ORDER BY table_schema, table_name, ordinal_position`, [], runtimeVariables({}), connection);
|
|
242
|
+
return buildAgentSchemaContext(question, result.rows);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
};
|
|
216
248
|
// SSE clients for /api/watch hot-reload
|
|
217
249
|
const sseClients = new Set();
|
|
218
250
|
// Watch notebooks/, workbooks/, semantic-layer/, and data/ dirs for changes
|
|
@@ -1777,8 +1809,9 @@ export async function startLocalServer(opts) {
|
|
|
1777
1809
|
const defaultKey = raw.defaultConnection
|
|
1778
1810
|
? 'default'
|
|
1779
1811
|
: Object.keys(connections)[0] ?? 'default';
|
|
1812
|
+
const dbtProfiles = discoverDbtProfileConnections(projectRoot, cfg);
|
|
1780
1813
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1781
|
-
res.end(serializeJSON({ default: defaultKey, connections }));
|
|
1814
|
+
res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles }));
|
|
1782
1815
|
return;
|
|
1783
1816
|
}
|
|
1784
1817
|
// Save/update connections
|
|
@@ -2495,7 +2528,15 @@ export async function startLocalServer(opts) {
|
|
|
2495
2528
|
req.on('close', () => controller.abort());
|
|
2496
2529
|
const emit = (turn) => { res.write(`data: ${JSON.stringify(turn)}\n\n`); };
|
|
2497
2530
|
try {
|
|
2498
|
-
await runner.run({
|
|
2531
|
+
await runner.run({
|
|
2532
|
+
provider: resolvedProvider,
|
|
2533
|
+
messages,
|
|
2534
|
+
upstream,
|
|
2535
|
+
projectRoot,
|
|
2536
|
+
executeCertifiedBlock: executeCertifiedBlockForAgent,
|
|
2537
|
+
executeGeneratedSql: executeGeneratedSqlForAgent,
|
|
2538
|
+
getSchemaContext: getSchemaContextForAgent,
|
|
2539
|
+
}, emit, controller.signal);
|
|
2499
2540
|
}
|
|
2500
2541
|
catch (err) {
|
|
2501
2542
|
emit({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
@@ -3328,6 +3369,7 @@ export function loadProjectConfig(projectRoot) {
|
|
|
3328
3369
|
// Support both `filepath` (correct) and `path` (legacy/init compat)
|
|
3329
3370
|
const filepath = (defaultConn.filepath ?? defaultConn.path);
|
|
3330
3371
|
config.defaultConnection = {
|
|
3372
|
+
...defaultConn,
|
|
3331
3373
|
driver: defaultConn.driver,
|
|
3332
3374
|
...(filepath ? { filepath } : {}),
|
|
3333
3375
|
};
|
|
@@ -3344,6 +3386,100 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
|
|
|
3344
3386
|
connection: normalizedConnection,
|
|
3345
3387
|
};
|
|
3346
3388
|
}
|
|
3389
|
+
const AGENT_PREVIEW_FORBIDDEN_SQL = [
|
|
3390
|
+
'alter',
|
|
3391
|
+
'analyze',
|
|
3392
|
+
'attach',
|
|
3393
|
+
'call',
|
|
3394
|
+
'copy',
|
|
3395
|
+
'create',
|
|
3396
|
+
'delete',
|
|
3397
|
+
'detach',
|
|
3398
|
+
'drop',
|
|
3399
|
+
'export',
|
|
3400
|
+
'grant',
|
|
3401
|
+
'import',
|
|
3402
|
+
'insert',
|
|
3403
|
+
'install',
|
|
3404
|
+
'load',
|
|
3405
|
+
'merge',
|
|
3406
|
+
'pragma',
|
|
3407
|
+
'reset',
|
|
3408
|
+
'revoke',
|
|
3409
|
+
'set',
|
|
3410
|
+
'truncate',
|
|
3411
|
+
'update',
|
|
3412
|
+
'vacuum',
|
|
3413
|
+
];
|
|
3414
|
+
export function buildAgentPreviewSql(sql) {
|
|
3415
|
+
const trimmed = sql.trim();
|
|
3416
|
+
if (!trimmed)
|
|
3417
|
+
throw new Error('Generated SQL preview is empty.');
|
|
3418
|
+
const withoutTrailingSemicolon = trimmed.replace(/;\s*$/, '').trim();
|
|
3419
|
+
const scanSql = stripSqlStringsAndComments(withoutTrailingSemicolon).trim();
|
|
3420
|
+
if (!/^(select|with)\b/i.test(scanSql)) {
|
|
3421
|
+
throw new Error('Generated SQL preview only supports read-only SELECT or WITH queries.');
|
|
3422
|
+
}
|
|
3423
|
+
if (scanSql.includes(';')) {
|
|
3424
|
+
throw new Error('Generated SQL preview only supports one statement.');
|
|
3425
|
+
}
|
|
3426
|
+
const forbiddenPattern = new RegExp(`\\b(${AGENT_PREVIEW_FORBIDDEN_SQL.join('|')})\\b`, 'i');
|
|
3427
|
+
const forbidden = scanSql.match(forbiddenPattern)?.[1];
|
|
3428
|
+
if (forbidden) {
|
|
3429
|
+
throw new Error(`Generated SQL preview rejected unsupported statement keyword: ${forbidden.toUpperCase()}.`);
|
|
3430
|
+
}
|
|
3431
|
+
return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT 200`;
|
|
3432
|
+
}
|
|
3433
|
+
function stripSqlStringsAndComments(sql) {
|
|
3434
|
+
let output = '';
|
|
3435
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
3436
|
+
const current = sql[index];
|
|
3437
|
+
const next = sql[index + 1];
|
|
3438
|
+
if (current === '-' && next === '-') {
|
|
3439
|
+
output += ' ';
|
|
3440
|
+
index += 2;
|
|
3441
|
+
while (index < sql.length && sql[index] !== '\n') {
|
|
3442
|
+
output += ' ';
|
|
3443
|
+
index += 1;
|
|
3444
|
+
}
|
|
3445
|
+
if (index < sql.length)
|
|
3446
|
+
output += '\n';
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
if (current === '/' && next === '*') {
|
|
3450
|
+
output += ' ';
|
|
3451
|
+
index += 2;
|
|
3452
|
+
while (index < sql.length && !(sql[index] === '*' && sql[index + 1] === '/')) {
|
|
3453
|
+
output += sql[index] === '\n' ? '\n' : ' ';
|
|
3454
|
+
index += 1;
|
|
3455
|
+
}
|
|
3456
|
+
if (index < sql.length) {
|
|
3457
|
+
output += ' ';
|
|
3458
|
+
index += 1;
|
|
3459
|
+
}
|
|
3460
|
+
continue;
|
|
3461
|
+
}
|
|
3462
|
+
if (current === "'" || current === '"') {
|
|
3463
|
+
const quote = current;
|
|
3464
|
+
output += ' ';
|
|
3465
|
+
while (index + 1 < sql.length) {
|
|
3466
|
+
index += 1;
|
|
3467
|
+
output += sql[index] === '\n' ? '\n' : ' ';
|
|
3468
|
+
if (sql[index] === quote) {
|
|
3469
|
+
if (sql[index + 1] === quote) {
|
|
3470
|
+
index += 1;
|
|
3471
|
+
output += ' ';
|
|
3472
|
+
continue;
|
|
3473
|
+
}
|
|
3474
|
+
break;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
continue;
|
|
3478
|
+
}
|
|
3479
|
+
output += current;
|
|
3480
|
+
}
|
|
3481
|
+
return output;
|
|
3482
|
+
}
|
|
3347
3483
|
/**
|
|
3348
3484
|
* Shared resolver for `@metric(name)` / `@dim(name)` refs in raw SQL.
|
|
3349
3485
|
* Used by notebook SQL execution and Block Studio validation so both paths
|
|
@@ -3364,7 +3500,7 @@ export function prepareSemanticSql(sql, semanticLayer) {
|
|
|
3364
3500
|
};
|
|
3365
3501
|
}
|
|
3366
3502
|
export function normalizeProjectConnection(connection, projectRoot) {
|
|
3367
|
-
const normalized = { ...connection };
|
|
3503
|
+
const normalized = expandConnectionEnvPlaceholders({ ...connection });
|
|
3368
3504
|
if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
|
|
3369
3505
|
normalized.filepath = resolve(projectRoot, normalized.filepath);
|
|
3370
3506
|
}
|
|
@@ -3373,6 +3509,15 @@ export function normalizeProjectConnection(connection, projectRoot) {
|
|
|
3373
3509
|
}
|
|
3374
3510
|
return normalized;
|
|
3375
3511
|
}
|
|
3512
|
+
function expandConnectionEnvPlaceholders(connection) {
|
|
3513
|
+
const expanded = {};
|
|
3514
|
+
for (const [key, value] of Object.entries(connection)) {
|
|
3515
|
+
expanded[key] = typeof value === 'string'
|
|
3516
|
+
? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, envKey) => process.env[envKey] ?? match)
|
|
3517
|
+
: value;
|
|
3518
|
+
}
|
|
3519
|
+
return expanded;
|
|
3520
|
+
}
|
|
3376
3521
|
export function resolveProjectRelativeSqlPaths(sql, projectRoot, dataDir) {
|
|
3377
3522
|
const resolvedRoot = resolve(projectRoot);
|
|
3378
3523
|
const normalizedDataDir = typeof dataDir === 'string' && dataDir.trim().length > 0
|
|
@@ -3515,6 +3660,8 @@ function scanNotebookFiles(projectRoot) {
|
|
|
3515
3660
|
workbooks: 'workbook',
|
|
3516
3661
|
blocks: 'block',
|
|
3517
3662
|
dashboards: 'dashboard',
|
|
3663
|
+
terms: 'term',
|
|
3664
|
+
'business-views': 'business_view',
|
|
3518
3665
|
};
|
|
3519
3666
|
for (const [folder, type] of Object.entries(folderMap)) {
|
|
3520
3667
|
const dir = join(projectRoot, folder);
|
|
@@ -3536,8 +3683,9 @@ function scanNotebookFiles(projectRoot) {
|
|
|
3536
3683
|
continue;
|
|
3537
3684
|
if (!entry.name.endsWith('.dql') && !entry.name.endsWith('.dqlnb'))
|
|
3538
3685
|
continue;
|
|
3686
|
+
const fallbackName = entry.name.replace(/\.(dql|dqlnb)$/, '');
|
|
3539
3687
|
result.push({
|
|
3540
|
-
name:
|
|
3688
|
+
name: inferDqlArtifactName(fullPath, type, fallbackName),
|
|
3541
3689
|
path: relativePath,
|
|
3542
3690
|
type,
|
|
3543
3691
|
folder: relativeDir.split('/')[0] ?? relativeDir,
|
|
@@ -3547,6 +3695,27 @@ function scanNotebookFiles(projectRoot) {
|
|
|
3547
3695
|
catch { /* skip unreadable dirs */ }
|
|
3548
3696
|
}
|
|
3549
3697
|
}
|
|
3698
|
+
function inferDqlArtifactName(fullPath, type, fallbackName) {
|
|
3699
|
+
if (!fullPath.endsWith('.dql'))
|
|
3700
|
+
return fallbackName;
|
|
3701
|
+
const expectedKind = {
|
|
3702
|
+
block: 'BlockDecl',
|
|
3703
|
+
dashboard: 'Dashboard',
|
|
3704
|
+
term: 'TermDecl',
|
|
3705
|
+
business_view: 'BusinessViewDecl',
|
|
3706
|
+
};
|
|
3707
|
+
const kind = expectedKind[type];
|
|
3708
|
+
if (!kind)
|
|
3709
|
+
return fallbackName;
|
|
3710
|
+
try {
|
|
3711
|
+
const ast = new Parser(readFileSync(fullPath, 'utf-8')).parse();
|
|
3712
|
+
const statement = ast.statements.find((item) => item.kind === kind && typeof item.name === 'string');
|
|
3713
|
+
return statement?.name ?? fallbackName;
|
|
3714
|
+
}
|
|
3715
|
+
catch {
|
|
3716
|
+
return fallbackName;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3550
3719
|
function scanDataFiles(projectRoot) {
|
|
3551
3720
|
const dataDir = join(projectRoot, 'data');
|
|
3552
3721
|
if (!existsSync(dataDir))
|
|
@@ -4984,7 +5153,51 @@ function resolveDbtManifestPath(projectRoot) {
|
|
|
4984
5153
|
const candidate = join(projectRoot, 'target', 'manifest.json');
|
|
4985
5154
|
return existsSync(candidate) ? candidate : undefined;
|
|
4986
5155
|
}
|
|
4987
|
-
export function
|
|
5156
|
+
export function discoverDbtProfileConnections(projectRoot, projectConfig) {
|
|
5157
|
+
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
5158
|
+
const projectProfileName = readDbtProjectProfileName(dbtProjectPath);
|
|
5159
|
+
const profilePaths = findDbtProfilePaths(projectRoot, dbtProjectPath);
|
|
5160
|
+
const candidates = [];
|
|
5161
|
+
for (const profilePath of profilePaths) {
|
|
5162
|
+
const profiles = readYamlFile(profilePath);
|
|
5163
|
+
if (!profiles)
|
|
5164
|
+
continue;
|
|
5165
|
+
for (const [profileName, rawProfile] of Object.entries(profiles)) {
|
|
5166
|
+
if (!rawProfile || typeof rawProfile !== 'object')
|
|
5167
|
+
continue;
|
|
5168
|
+
if (projectProfileName && profileName !== projectProfileName)
|
|
5169
|
+
continue;
|
|
5170
|
+
const profile = rawProfile;
|
|
5171
|
+
const outputs = profile.outputs && typeof profile.outputs === 'object'
|
|
5172
|
+
? profile.outputs
|
|
5173
|
+
: {};
|
|
5174
|
+
const defaultTarget = typeof profile.target === 'string' ? profile.target : 'default';
|
|
5175
|
+
for (const [targetName, output] of Object.entries(outputs)) {
|
|
5176
|
+
if (!output || typeof output !== 'object')
|
|
5177
|
+
continue;
|
|
5178
|
+
const mapped = mapDbtProfileOutput(output);
|
|
5179
|
+
if (!mapped)
|
|
5180
|
+
continue;
|
|
5181
|
+
const warnings = [...mapped.warnings];
|
|
5182
|
+
if (targetName !== defaultTarget) {
|
|
5183
|
+
warnings.push(`Not the default dbt target "${defaultTarget}".`);
|
|
5184
|
+
}
|
|
5185
|
+
candidates.push({
|
|
5186
|
+
id: `${profilePath}:${profileName}:${targetName}`,
|
|
5187
|
+
profileName,
|
|
5188
|
+
targetName,
|
|
5189
|
+
adapter: mapped.adapter,
|
|
5190
|
+
path: profilePath,
|
|
5191
|
+
connection: mapped.connection,
|
|
5192
|
+
missingFields: requiredConnectionFields(mapped.connection, mapped.envRefs),
|
|
5193
|
+
warnings,
|
|
5194
|
+
});
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
return candidates.slice(0, 20);
|
|
5199
|
+
}
|
|
5200
|
+
function findDbtProjectPath(projectRoot, projectConfig) {
|
|
4988
5201
|
const configuredDbtDir = projectConfig.dbt?.projectDir
|
|
4989
5202
|
? resolve(projectRoot, projectConfig.dbt.projectDir)
|
|
4990
5203
|
: undefined;
|
|
@@ -4999,7 +5212,271 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
|
4999
5212
|
resolve(projectRoot, '../dbt'),
|
|
5000
5213
|
resolve(projectRoot, '../../dbt'),
|
|
5001
5214
|
].filter((value) => Boolean(value));
|
|
5002
|
-
|
|
5215
|
+
return candidateDirs.find((dir, index, list) => list.indexOf(dir) === index && existsSync(join(dir, 'dbt_project.yml')))
|
|
5216
|
+
?? configuredDbtDir
|
|
5217
|
+
?? semanticDbtDir
|
|
5218
|
+
?? projectRoot;
|
|
5219
|
+
}
|
|
5220
|
+
function findDbtProfilePaths(projectRoot, dbtProjectPath) {
|
|
5221
|
+
const dirs = [
|
|
5222
|
+
process.env.DBT_PROFILES_DIR,
|
|
5223
|
+
dbtProjectPath,
|
|
5224
|
+
projectRoot,
|
|
5225
|
+
join(homedir(), '.dbt'),
|
|
5226
|
+
].filter((value) => Boolean(value));
|
|
5227
|
+
const paths = [];
|
|
5228
|
+
for (const dir of dirs) {
|
|
5229
|
+
for (const filename of ['profiles.yml', 'profiles.yaml']) {
|
|
5230
|
+
const profilePath = resolve(dir, filename);
|
|
5231
|
+
if (existsSync(profilePath) && !paths.includes(profilePath)) {
|
|
5232
|
+
paths.push(profilePath);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
return paths;
|
|
5237
|
+
}
|
|
5238
|
+
function readDbtProjectProfileName(dbtProjectPath) {
|
|
5239
|
+
const projectFile = join(dbtProjectPath, 'dbt_project.yml');
|
|
5240
|
+
const projectYaml = readYamlFile(projectFile);
|
|
5241
|
+
return typeof projectYaml?.profile === 'string' ? projectYaml.profile : null;
|
|
5242
|
+
}
|
|
5243
|
+
function readYamlFile(path) {
|
|
5244
|
+
if (!existsSync(path))
|
|
5245
|
+
return null;
|
|
5246
|
+
try {
|
|
5247
|
+
const parsed = loadYaml(readFileSync(path, 'utf-8'));
|
|
5248
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
5249
|
+
? parsed
|
|
5250
|
+
: null;
|
|
5251
|
+
}
|
|
5252
|
+
catch {
|
|
5253
|
+
return null;
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
function mapDbtProfileOutput(output) {
|
|
5257
|
+
const adapter = text(output, 'type').value?.toLowerCase();
|
|
5258
|
+
const envRefs = new Set();
|
|
5259
|
+
const warnings = [];
|
|
5260
|
+
const read = (...keys) => {
|
|
5261
|
+
const result = text(output, ...keys);
|
|
5262
|
+
result.envRefs.forEach((ref) => envRefs.add(ref));
|
|
5263
|
+
return result.value;
|
|
5264
|
+
};
|
|
5265
|
+
const port = numberValue(output, 'port');
|
|
5266
|
+
const sslRaw = read('ssl', 'sslmode');
|
|
5267
|
+
const ssl = sslRaw === undefined
|
|
5268
|
+
? undefined
|
|
5269
|
+
: !['false', '0', 'disable', 'disabled', 'off'].includes(sslRaw.toLowerCase());
|
|
5270
|
+
switch (adapter) {
|
|
5271
|
+
case 'postgres':
|
|
5272
|
+
case 'postgresql':
|
|
5273
|
+
return {
|
|
5274
|
+
adapter,
|
|
5275
|
+
connection: compactConnection({
|
|
5276
|
+
driver: 'postgresql',
|
|
5277
|
+
host: read('host'),
|
|
5278
|
+
port,
|
|
5279
|
+
database: read('dbname', 'database'),
|
|
5280
|
+
schema: read('schema'),
|
|
5281
|
+
username: read('user', 'username'),
|
|
5282
|
+
password: read('password', 'pass'),
|
|
5283
|
+
ssl,
|
|
5284
|
+
}),
|
|
5285
|
+
envRefs: [...envRefs],
|
|
5286
|
+
warnings,
|
|
5287
|
+
};
|
|
5288
|
+
case 'redshift':
|
|
5289
|
+
return {
|
|
5290
|
+
adapter,
|
|
5291
|
+
connection: compactConnection({
|
|
5292
|
+
driver: 'redshift',
|
|
5293
|
+
host: read('host'),
|
|
5294
|
+
port: port ?? 5439,
|
|
5295
|
+
database: read('dbname', 'database'),
|
|
5296
|
+
schema: read('schema'),
|
|
5297
|
+
username: read('user', 'username'),
|
|
5298
|
+
password: read('password', 'pass'),
|
|
5299
|
+
ssl,
|
|
5300
|
+
}),
|
|
5301
|
+
envRefs: [...envRefs],
|
|
5302
|
+
warnings,
|
|
5303
|
+
};
|
|
5304
|
+
case 'snowflake': {
|
|
5305
|
+
const privateKeyPath = read('private_key_path', 'privateKeyPath');
|
|
5306
|
+
const privateKey = read('private_key', 'privateKey');
|
|
5307
|
+
const authenticator = read('authenticator');
|
|
5308
|
+
const normalizedAuthenticator = authenticator?.toLowerCase().replace(/[\s_-]/g, '');
|
|
5309
|
+
const authMethod = privateKeyPath || privateKey || normalizedAuthenticator === 'snowflakejwt'
|
|
5310
|
+
? 'key_pair'
|
|
5311
|
+
: normalizedAuthenticator === 'externalbrowser'
|
|
5312
|
+
? 'external_browser'
|
|
5313
|
+
: normalizedAuthenticator === 'oauth' || normalizedAuthenticator === 'programmaticaccesstoken'
|
|
5314
|
+
? 'oauth'
|
|
5315
|
+
: 'password';
|
|
5316
|
+
return {
|
|
5317
|
+
adapter,
|
|
5318
|
+
connection: compactConnection({
|
|
5319
|
+
driver: 'snowflake',
|
|
5320
|
+
account: read('account'),
|
|
5321
|
+
warehouse: read('warehouse'),
|
|
5322
|
+
database: read('database'),
|
|
5323
|
+
schema: read('schema'),
|
|
5324
|
+
username: read('user', 'username'),
|
|
5325
|
+
password: read('password'),
|
|
5326
|
+
role: read('role'),
|
|
5327
|
+
privateKeyPath,
|
|
5328
|
+
privateKey,
|
|
5329
|
+
privateKeyPassphrase: read('private_key_passphrase', 'privateKeyPassphrase'),
|
|
5330
|
+
authenticator,
|
|
5331
|
+
authMethod,
|
|
5332
|
+
}),
|
|
5333
|
+
envRefs: [...envRefs],
|
|
5334
|
+
warnings,
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
case 'bigquery': {
|
|
5338
|
+
const keyFilename = read('keyfile', 'keyFilename');
|
|
5339
|
+
return {
|
|
5340
|
+
adapter,
|
|
5341
|
+
connection: compactConnection({
|
|
5342
|
+
driver: 'bigquery',
|
|
5343
|
+
projectId: read('project', 'projectId'),
|
|
5344
|
+
schema: read('dataset', 'schema'),
|
|
5345
|
+
location: read('location'),
|
|
5346
|
+
keyFilename,
|
|
5347
|
+
authMethod: keyFilename ? 'service_account_key_file' : 'application_default',
|
|
5348
|
+
}),
|
|
5349
|
+
envRefs: [...envRefs],
|
|
5350
|
+
warnings,
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
5353
|
+
case 'duckdb':
|
|
5354
|
+
return {
|
|
5355
|
+
adapter,
|
|
5356
|
+
connection: compactConnection({
|
|
5357
|
+
driver: 'duckdb',
|
|
5358
|
+
filepath: read('path', 'database') ?? ':memory:',
|
|
5359
|
+
}),
|
|
5360
|
+
envRefs: [...envRefs],
|
|
5361
|
+
warnings,
|
|
5362
|
+
};
|
|
5363
|
+
case 'databricks':
|
|
5364
|
+
return {
|
|
5365
|
+
adapter,
|
|
5366
|
+
connection: compactConnection({
|
|
5367
|
+
driver: 'databricks',
|
|
5368
|
+
host: read('host', 'server_hostname'),
|
|
5369
|
+
httpPath: read('http_path', 'httpPath'),
|
|
5370
|
+
warehouse: read('warehouse', 'warehouse_id'),
|
|
5371
|
+
catalog: read('catalog'),
|
|
5372
|
+
database: read('catalog', 'database'),
|
|
5373
|
+
schema: read('schema'),
|
|
5374
|
+
token: read('token'),
|
|
5375
|
+
authMethod: 'token',
|
|
5376
|
+
}),
|
|
5377
|
+
envRefs: [...envRefs],
|
|
5378
|
+
warnings,
|
|
5379
|
+
};
|
|
5380
|
+
default:
|
|
5381
|
+
return null;
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
function text(source, ...keys) {
|
|
5385
|
+
for (const key of keys) {
|
|
5386
|
+
const raw = source[key];
|
|
5387
|
+
if (raw === undefined || raw === null)
|
|
5388
|
+
continue;
|
|
5389
|
+
const value = String(raw).trim();
|
|
5390
|
+
if (!value)
|
|
5391
|
+
continue;
|
|
5392
|
+
return resolveDbtEnvVars(value);
|
|
5393
|
+
}
|
|
5394
|
+
return { envRefs: [] };
|
|
5395
|
+
}
|
|
5396
|
+
function resolveDbtEnvVars(value) {
|
|
5397
|
+
const envRefs = [];
|
|
5398
|
+
const replaced = value.replace(/\{\{\s*env_var\(\s*['"]([^'"]+)['"]\s*(?:,\s*(['"])(.*?)\2)?\s*\)\s*\}\}/g, (_match, envKey, _quote, fallback) => {
|
|
5399
|
+
const envValue = process.env[envKey];
|
|
5400
|
+
if (envValue !== undefined)
|
|
5401
|
+
return envValue;
|
|
5402
|
+
if (fallback !== undefined)
|
|
5403
|
+
return fallback;
|
|
5404
|
+
envRefs.push(envKey);
|
|
5405
|
+
return `\${${envKey}}`;
|
|
5406
|
+
});
|
|
5407
|
+
return { value: replaced, envRefs };
|
|
5408
|
+
}
|
|
5409
|
+
function numberValue(source, key) {
|
|
5410
|
+
const raw = source[key];
|
|
5411
|
+
if (raw === undefined || raw === null || raw === '')
|
|
5412
|
+
return undefined;
|
|
5413
|
+
const value = Number(raw);
|
|
5414
|
+
return Number.isFinite(value) ? value : undefined;
|
|
5415
|
+
}
|
|
5416
|
+
function compactConnection(connection) {
|
|
5417
|
+
const compact = {};
|
|
5418
|
+
for (const [key, value] of Object.entries(connection)) {
|
|
5419
|
+
if (value === undefined || value === null || value === '')
|
|
5420
|
+
continue;
|
|
5421
|
+
compact[key] = value;
|
|
5422
|
+
}
|
|
5423
|
+
return compact;
|
|
5424
|
+
}
|
|
5425
|
+
function requiredConnectionFields(connection, envRefs) {
|
|
5426
|
+
const missing = new Set();
|
|
5427
|
+
const needs = (field) => {
|
|
5428
|
+
const value = connection[field];
|
|
5429
|
+
if (value === undefined || value === null || value === '')
|
|
5430
|
+
missing.add(String(field));
|
|
5431
|
+
};
|
|
5432
|
+
switch (connection.driver) {
|
|
5433
|
+
case 'postgresql':
|
|
5434
|
+
case 'redshift':
|
|
5435
|
+
needs('host');
|
|
5436
|
+
needs('database');
|
|
5437
|
+
needs('username');
|
|
5438
|
+
break;
|
|
5439
|
+
case 'snowflake':
|
|
5440
|
+
needs('account');
|
|
5441
|
+
needs('warehouse');
|
|
5442
|
+
needs('database');
|
|
5443
|
+
needs('schema');
|
|
5444
|
+
needs('username');
|
|
5445
|
+
if (connection.authMethod === 'key_pair') {
|
|
5446
|
+
if (!connection.privateKeyPath && !connection.privateKey) {
|
|
5447
|
+
missing.add('privateKeyPath');
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
else if (connection.authMethod === 'oauth') {
|
|
5451
|
+
if (!connection.token && !connection.password) {
|
|
5452
|
+
missing.add('token');
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
else if (connection.authMethod !== 'external_browser') {
|
|
5456
|
+
needs('password');
|
|
5457
|
+
}
|
|
5458
|
+
break;
|
|
5459
|
+
case 'bigquery':
|
|
5460
|
+
needs('projectId');
|
|
5461
|
+
break;
|
|
5462
|
+
case 'duckdb':
|
|
5463
|
+
needs('filepath');
|
|
5464
|
+
break;
|
|
5465
|
+
case 'databricks':
|
|
5466
|
+
needs('host');
|
|
5467
|
+
if (!connection.httpPath && !connection.warehouse)
|
|
5468
|
+
missing.add('httpPath');
|
|
5469
|
+
needs('token');
|
|
5470
|
+
break;
|
|
5471
|
+
}
|
|
5472
|
+
for (const envKey of envRefs) {
|
|
5473
|
+
if (!process.env[envKey])
|
|
5474
|
+
missing.add(`env:${envKey}`);
|
|
5475
|
+
}
|
|
5476
|
+
return [...missing];
|
|
5477
|
+
}
|
|
5478
|
+
export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
5479
|
+
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
5003
5480
|
const configuredManifest = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
|
|
5004
5481
|
const manifestPath = resolve(dbtProjectPath, configuredManifest);
|
|
5005
5482
|
const catalogPath = resolve(dbtProjectPath, 'target/catalog.json');
|
|
@@ -5029,7 +5506,9 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
|
5029
5506
|
const savedQueryCount = Array.isArray(semanticManifest?.saved_queries)
|
|
5030
5507
|
? semanticManifest.saved_queries.length
|
|
5031
5508
|
: 0;
|
|
5032
|
-
const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
|
|
5509
|
+
const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
|
|
5510
|
+
|| Boolean(projectConfig.dbt?.projectDir)
|
|
5511
|
+
|| Boolean(projectConfig.semanticLayer?.provider === 'dbt' && projectConfig.semanticLayer.projectPath);
|
|
5033
5512
|
const manifestExists = existsSync(manifestPath);
|
|
5034
5513
|
const semanticExists = existsSync(semanticManifestPath);
|
|
5035
5514
|
const setupHint = !configured
|
|
@@ -5146,14 +5625,21 @@ async function execGit(cwd, args) {
|
|
|
5146
5625
|
});
|
|
5147
5626
|
});
|
|
5148
5627
|
}
|
|
5149
|
-
async function
|
|
5628
|
+
async function resolveGitRoot(cwd) {
|
|
5150
5629
|
const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
5151
|
-
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
|
|
5630
|
+
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
|
|
5631
|
+
return null;
|
|
5632
|
+
const root = await execGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
5633
|
+
return root.code === 0 && root.stdout.trim() ? root.stdout.trim() : cwd;
|
|
5634
|
+
}
|
|
5635
|
+
async function readGitStatus(cwd) {
|
|
5636
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5637
|
+
if (!gitRoot) {
|
|
5152
5638
|
return { inRepo: false, branch: null, ahead: 0, behind: 0, changes: [] };
|
|
5153
5639
|
}
|
|
5154
|
-
const branchRes = await execGit(
|
|
5640
|
+
const branchRes = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5155
5641
|
const branch = branchRes.code === 0 ? branchRes.stdout.trim() : null;
|
|
5156
|
-
const trackRes = await execGit(
|
|
5642
|
+
const trackRes = await execGit(gitRoot, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
|
|
5157
5643
|
let ahead = 0;
|
|
5158
5644
|
let behind = 0;
|
|
5159
5645
|
if (trackRes.code === 0) {
|
|
@@ -5161,7 +5647,7 @@ async function readGitStatus(cwd) {
|
|
|
5161
5647
|
behind = Number(match[0] ?? 0);
|
|
5162
5648
|
ahead = Number(match[1] ?? 0);
|
|
5163
5649
|
}
|
|
5164
|
-
const statusRes = await execGit(
|
|
5650
|
+
const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=normal']);
|
|
5165
5651
|
const changes = [];
|
|
5166
5652
|
if (statusRes.code === 0) {
|
|
5167
5653
|
for (const line of statusRes.stdout.split('\n')) {
|
|
@@ -5175,13 +5661,13 @@ async function readGitStatus(cwd) {
|
|
|
5175
5661
|
return { inRepo: true, branch, ahead, behind, changes };
|
|
5176
5662
|
}
|
|
5177
5663
|
async function readGitLog(cwd, limit) {
|
|
5178
|
-
const
|
|
5179
|
-
if (
|
|
5664
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5665
|
+
if (!gitRoot)
|
|
5180
5666
|
return { inRepo: false, commits: [] };
|
|
5181
5667
|
const sep = '\x1f';
|
|
5182
5668
|
const end = '\x1e';
|
|
5183
5669
|
const fmt = ['%H', '%an', '%ad', '%s'].join(sep) + end;
|
|
5184
|
-
const res = await execGit(
|
|
5670
|
+
const res = await execGit(gitRoot, ['log', `-${limit}`, `--pretty=format:${fmt}`, '--date=short']);
|
|
5185
5671
|
if (res.code !== 0)
|
|
5186
5672
|
return { inRepo: true, commits: [] };
|
|
5187
5673
|
const commits = [];
|
|
@@ -5244,23 +5730,97 @@ function ensureGitignoreEntry(projectRoot, pattern) {
|
|
|
5244
5730
|
}
|
|
5245
5731
|
}
|
|
5246
5732
|
async function readGitDiff(cwd, filePath, staged = false) {
|
|
5247
|
-
const
|
|
5248
|
-
if (
|
|
5733
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5734
|
+
if (!gitRoot) {
|
|
5249
5735
|
return { inRepo: false, diff: '', before: null, after: null, diffReport: null };
|
|
5250
5736
|
}
|
|
5251
5737
|
const baseArgs = staged ? ['diff', '--cached', '--no-color'] : ['diff', '--no-color'];
|
|
5252
5738
|
if (!filePath) {
|
|
5253
|
-
const res = await execGit(
|
|
5739
|
+
const res = await execGit(gitRoot, baseArgs);
|
|
5254
5740
|
return { inRepo: true, diff: res.stdout, before: null, after: null, diffReport: null };
|
|
5255
5741
|
}
|
|
5256
5742
|
const isSemantic = filePath.endsWith('.dql') || filePath.endsWith('.dqlnb');
|
|
5257
5743
|
const [diffRes, before, after] = await Promise.all([
|
|
5258
|
-
execGit(
|
|
5259
|
-
isSemantic ? readHeadBlob(
|
|
5260
|
-
isSemantic ? readWorkingCopy(join(
|
|
5744
|
+
execGit(gitRoot, [...baseArgs, '--', filePath]),
|
|
5745
|
+
isSemantic ? readHeadBlob(gitRoot, filePath) : Promise.resolve(null),
|
|
5746
|
+
isSemantic ? readWorkingCopy(join(gitRoot, filePath)) : Promise.resolve(null),
|
|
5261
5747
|
]);
|
|
5748
|
+
const diffText = !staged && !diffRes.stdout.trim()
|
|
5749
|
+
? (await readUntrackedTextDiff(gitRoot, filePath)) || diffRes.stdout
|
|
5750
|
+
: diffRes.stdout;
|
|
5262
5751
|
const diffReport = isSemantic ? computeSemanticDiff(filePath, before, after) : null;
|
|
5263
|
-
return { inRepo: true, diff:
|
|
5752
|
+
return { inRepo: true, diff: diffText, before, after, diffReport };
|
|
5753
|
+
}
|
|
5754
|
+
const MAX_UNTRACKED_DIFF_FILES = 20;
|
|
5755
|
+
const MAX_UNTRACKED_DIFF_BYTES = 512 * 1024;
|
|
5756
|
+
async function readUntrackedTextDiff(cwd, filePath) {
|
|
5757
|
+
const status = await execGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '--', filePath]);
|
|
5758
|
+
if (status.code !== 0 || !status.stdout.split('\n').some((line) => line.startsWith('?? '))) {
|
|
5759
|
+
return '';
|
|
5760
|
+
}
|
|
5761
|
+
const listed = await execGit(cwd, ['ls-files', '--others', '--exclude-standard', '--', filePath]);
|
|
5762
|
+
if (listed.code !== 0)
|
|
5763
|
+
return '';
|
|
5764
|
+
const chunks = [];
|
|
5765
|
+
let totalBytes = 0;
|
|
5766
|
+
for (const rawPath of listed.stdout.split('\n').map((p) => p.trim()).filter(Boolean)) {
|
|
5767
|
+
if (chunks.length >= MAX_UNTRACKED_DIFF_FILES || totalBytes >= MAX_UNTRACKED_DIFF_BYTES)
|
|
5768
|
+
break;
|
|
5769
|
+
const absPath = safeJoin(cwd, rawPath);
|
|
5770
|
+
if (!absPath || !existsSync(absPath))
|
|
5771
|
+
continue;
|
|
5772
|
+
const st = statSync(absPath);
|
|
5773
|
+
if (!st.isFile())
|
|
5774
|
+
continue;
|
|
5775
|
+
if (st.size > MAX_UNTRACKED_DIFF_BYTES) {
|
|
5776
|
+
chunks.push(formatBinaryAddedDiff(rawPath));
|
|
5777
|
+
continue;
|
|
5778
|
+
}
|
|
5779
|
+
const buf = readFileSync(absPath);
|
|
5780
|
+
if (buf.includes(0)) {
|
|
5781
|
+
chunks.push(formatBinaryAddedDiff(rawPath));
|
|
5782
|
+
continue;
|
|
5783
|
+
}
|
|
5784
|
+
totalBytes += buf.length;
|
|
5785
|
+
chunks.push(formatAddedFileDiff(rawPath, buf.toString('utf-8')));
|
|
5786
|
+
}
|
|
5787
|
+
if (chunks.length === 0)
|
|
5788
|
+
return '';
|
|
5789
|
+
const omitted = listed.stdout.split('\n').filter(Boolean).length - chunks.length;
|
|
5790
|
+
if (omitted > 0) {
|
|
5791
|
+
chunks.push(`diff --git a/${filePath} b/${filePath}\n# ${omitted} additional untracked file${omitted === 1 ? '' : 's'} omitted from preview`);
|
|
5792
|
+
}
|
|
5793
|
+
return chunks.join('\n');
|
|
5794
|
+
}
|
|
5795
|
+
function formatAddedFileDiff(filePath, content) {
|
|
5796
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
5797
|
+
const hasFinalNewline = normalized.endsWith('\n');
|
|
5798
|
+
const lines = normalized.length === 0
|
|
5799
|
+
? []
|
|
5800
|
+
: normalized.split('\n').slice(0, hasFinalNewline ? -1 : undefined);
|
|
5801
|
+
const hunk = lines.length > 0
|
|
5802
|
+
? [`@@ -0,0 +1,${lines.length} @@`, ...lines.map((line) => `+${line}`)]
|
|
5803
|
+
: [];
|
|
5804
|
+
if (!hasFinalNewline && normalized.length > 0)
|
|
5805
|
+
hunk.push('\');
|
|
5806
|
+
return [
|
|
5807
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
5808
|
+
'new file mode 100644',
|
|
5809
|
+
'index 0000000..0000000',
|
|
5810
|
+
'--- /dev/null',
|
|
5811
|
+
`+++ b/${filePath}`,
|
|
5812
|
+
...hunk,
|
|
5813
|
+
].join('\n');
|
|
5814
|
+
}
|
|
5815
|
+
function formatBinaryAddedDiff(filePath) {
|
|
5816
|
+
return [
|
|
5817
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
5818
|
+
'new file mode 100644',
|
|
5819
|
+
'index 0000000..0000000',
|
|
5820
|
+
'--- /dev/null',
|
|
5821
|
+
`+++ b/${filePath}`,
|
|
5822
|
+
`Binary file ${filePath} added`,
|
|
5823
|
+
].join('\n');
|
|
5264
5824
|
}
|
|
5265
5825
|
// ── git write operations ──────────────────────────────────────────────────
|
|
5266
5826
|
// Each helper validates inputs, shells out via execFile (no shell expansion),
|
|
@@ -5290,89 +5850,118 @@ function gitErrorOutput(res) {
|
|
|
5290
5850
|
return (res.stderr || res.stdout || '').trim();
|
|
5291
5851
|
}
|
|
5292
5852
|
async function gitStage(cwd, paths) {
|
|
5293
|
-
const
|
|
5853
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5854
|
+
if (!gitRoot)
|
|
5855
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5856
|
+
const v = validatePaths(gitRoot, paths);
|
|
5294
5857
|
if (!v.ok)
|
|
5295
5858
|
return { ok: false, error: v.error };
|
|
5296
|
-
const res = await execGit(
|
|
5859
|
+
const res = await execGit(gitRoot, ['add', '--', ...v.paths]);
|
|
5297
5860
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5298
5861
|
}
|
|
5299
5862
|
async function gitUnstage(cwd, paths) {
|
|
5300
|
-
const
|
|
5863
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5864
|
+
if (!gitRoot)
|
|
5865
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5866
|
+
const v = validatePaths(gitRoot, paths);
|
|
5301
5867
|
if (!v.ok)
|
|
5302
5868
|
return { ok: false, error: v.error };
|
|
5303
5869
|
// `restore --staged` works with or without HEAD; for an initial commit (no
|
|
5304
5870
|
// HEAD yet) git's `rm --cached` is the fallback. Try restore first.
|
|
5305
|
-
const res = await execGit(
|
|
5871
|
+
const res = await execGit(gitRoot, ['restore', '--staged', '--', ...v.paths]);
|
|
5306
5872
|
if (res.code === 0)
|
|
5307
5873
|
return { ok: true };
|
|
5308
|
-
const fallback = await execGit(
|
|
5874
|
+
const fallback = await execGit(gitRoot, ['rm', '--cached', '-r', '--', ...v.paths]);
|
|
5309
5875
|
return fallback.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(fallback) };
|
|
5310
5876
|
}
|
|
5311
5877
|
async function gitDiscard(cwd, paths) {
|
|
5312
|
-
const
|
|
5878
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5879
|
+
if (!gitRoot)
|
|
5880
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5881
|
+
const v = validatePaths(gitRoot, paths);
|
|
5313
5882
|
if (!v.ok)
|
|
5314
5883
|
return { ok: false, error: v.error };
|
|
5315
5884
|
// For tracked files: `restore --worktree` reverts to HEAD. For untracked
|
|
5316
5885
|
// files: that's a no-op and we delete them via `clean -f`. Run both so
|
|
5317
5886
|
// the caller doesn't have to know which list each path is in.
|
|
5318
|
-
const restore = await execGit(
|
|
5319
|
-
const clean = await execGit(
|
|
5887
|
+
const restore = await execGit(gitRoot, ['restore', '--worktree', '--', ...v.paths]);
|
|
5888
|
+
const clean = await execGit(gitRoot, ['clean', '-f', '--', ...v.paths]);
|
|
5320
5889
|
if (restore.code !== 0 && clean.code !== 0) {
|
|
5321
5890
|
return { ok: false, error: gitErrorOutput(restore) || gitErrorOutput(clean) };
|
|
5322
5891
|
}
|
|
5323
5892
|
return { ok: true };
|
|
5324
5893
|
}
|
|
5325
5894
|
async function gitCommit(cwd, message, stageAll) {
|
|
5895
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5896
|
+
if (!gitRoot)
|
|
5897
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5326
5898
|
const trimmed = message.trim();
|
|
5327
5899
|
if (!trimmed)
|
|
5328
5900
|
return { ok: false, error: 'Commit message required' };
|
|
5329
5901
|
if (stageAll) {
|
|
5330
|
-
const add = await execGit(
|
|
5902
|
+
const add = await execGit(gitRoot, ['add', '-A']);
|
|
5331
5903
|
if (add.code !== 0)
|
|
5332
5904
|
return { ok: false, error: gitErrorOutput(add) };
|
|
5333
5905
|
}
|
|
5334
|
-
const res = await execGit(
|
|
5906
|
+
const res = await execGit(gitRoot, ['commit', '-m', trimmed]);
|
|
5335
5907
|
if (res.code !== 0)
|
|
5336
5908
|
return { ok: false, error: gitErrorOutput(res) };
|
|
5337
|
-
const hashRes = await execGit(
|
|
5909
|
+
const hashRes = await execGit(gitRoot, ['rev-parse', 'HEAD']);
|
|
5338
5910
|
return { ok: true, hash: hashRes.code === 0 ? hashRes.stdout.trim() : undefined };
|
|
5339
5911
|
}
|
|
5340
5912
|
async function gitPush(cwd) {
|
|
5341
|
-
const
|
|
5913
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5914
|
+
if (!gitRoot)
|
|
5915
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5916
|
+
const branch = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5917
|
+
const upstream = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
5918
|
+
const remotes = await execGit(gitRoot, ['remote']);
|
|
5919
|
+
const remote = remotes.stdout.split('\n').map((s) => s.trim()).find(Boolean) ?? 'origin';
|
|
5920
|
+
const branchName = branch.code === 0 ? branch.stdout.trim() : '';
|
|
5921
|
+
const args = upstream.code === 0 || !branchName || branchName === 'HEAD'
|
|
5922
|
+
? ['push']
|
|
5923
|
+
: ['push', '-u', remote, branchName];
|
|
5924
|
+
const res = await execGit(gitRoot, args);
|
|
5342
5925
|
return res.code === 0
|
|
5343
5926
|
? { ok: true, output: gitErrorOutput(res) }
|
|
5344
5927
|
: { ok: false, error: gitErrorOutput(res) };
|
|
5345
5928
|
}
|
|
5346
5929
|
async function gitPull(cwd) {
|
|
5930
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5931
|
+
if (!gitRoot)
|
|
5932
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5347
5933
|
// `--ff-only` keeps the operation non-destructive: if the local branch has
|
|
5348
5934
|
// diverged from upstream, we surface the error rather than auto-merging.
|
|
5349
5935
|
// The user can resolve via the terminal or a future merge UI.
|
|
5350
|
-
const res = await execGit(
|
|
5936
|
+
const res = await execGit(gitRoot, ['pull', '--ff-only']);
|
|
5351
5937
|
return res.code === 0
|
|
5352
5938
|
? { ok: true, output: gitErrorOutput(res) }
|
|
5353
5939
|
: { ok: false, error: gitErrorOutput(res) };
|
|
5354
5940
|
}
|
|
5355
5941
|
async function readGitBranches(cwd) {
|
|
5356
|
-
const
|
|
5357
|
-
if (
|
|
5942
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5943
|
+
if (!gitRoot)
|
|
5358
5944
|
return { inRepo: false, current: null, branches: [] };
|
|
5359
|
-
const cur = await execGit(
|
|
5360
|
-
const list = await execGit(
|
|
5945
|
+
const cur = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5946
|
+
const list = await execGit(gitRoot, ['branch', '--list', '--format=%(refname:short)']);
|
|
5361
5947
|
const branches = list.code === 0
|
|
5362
5948
|
? list.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
5363
5949
|
: [];
|
|
5364
5950
|
return { inRepo: true, current: cur.code === 0 ? cur.stdout.trim() : null, branches };
|
|
5365
5951
|
}
|
|
5366
5952
|
async function readGitRemote(cwd) {
|
|
5367
|
-
const
|
|
5368
|
-
if (
|
|
5953
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5954
|
+
if (!gitRoot)
|
|
5369
5955
|
return { inRepo: false, url: null, name: null };
|
|
5370
|
-
const remoteName = await execGit(
|
|
5956
|
+
const remoteName = await execGit(gitRoot, ['config', '--get', 'remote.pushDefault']);
|
|
5371
5957
|
const name = remoteName.code === 0 && remoteName.stdout.trim() ? remoteName.stdout.trim() : 'origin';
|
|
5372
|
-
const url = await execGit(
|
|
5958
|
+
const url = await execGit(gitRoot, ['remote', 'get-url', name]);
|
|
5373
5959
|
return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
|
|
5374
5960
|
}
|
|
5375
5961
|
async function gitCreateBranch(cwd, name, checkout) {
|
|
5962
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5963
|
+
if (!gitRoot)
|
|
5964
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5376
5965
|
const trimmed = name.trim();
|
|
5377
5966
|
// Branch names can't start with `-` (would be parsed as a flag) and must be
|
|
5378
5967
|
// non-empty. git itself enforces the rest of the ref-name rules.
|
|
@@ -5381,17 +5970,20 @@ async function gitCreateBranch(cwd, name, checkout) {
|
|
|
5381
5970
|
if (trimmed.startsWith('-'))
|
|
5382
5971
|
return { ok: false, error: 'Invalid branch name' };
|
|
5383
5972
|
const res = checkout
|
|
5384
|
-
? await execGit(
|
|
5385
|
-
: await execGit(
|
|
5973
|
+
? await execGit(gitRoot, ['checkout', '-b', trimmed])
|
|
5974
|
+
: await execGit(gitRoot, ['branch', trimmed]);
|
|
5386
5975
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5387
5976
|
}
|
|
5388
5977
|
async function gitCheckout(cwd, name) {
|
|
5978
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5979
|
+
if (!gitRoot)
|
|
5980
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5389
5981
|
const trimmed = name.trim();
|
|
5390
5982
|
if (!trimmed)
|
|
5391
5983
|
return { ok: false, error: 'Branch name required' };
|
|
5392
5984
|
if (trimmed.startsWith('-'))
|
|
5393
5985
|
return { ok: false, error: 'Invalid branch name' };
|
|
5394
|
-
const res = await execGit(
|
|
5986
|
+
const res = await execGit(gitRoot, ['checkout', trimmed]);
|
|
5395
5987
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5396
5988
|
}
|
|
5397
5989
|
async function readHeadBlob(cwd, filePath) {
|
|
@@ -5521,6 +6113,92 @@ function isAiPinRefreshDue(lastRefreshedAt) {
|
|
|
5521
6113
|
return true;
|
|
5522
6114
|
return Date.now() - last >= 24 * 60 * 60 * 1000;
|
|
5523
6115
|
}
|
|
6116
|
+
function buildAgentSchemaContext(question, rows) {
|
|
6117
|
+
const byRelation = new Map();
|
|
6118
|
+
for (const row of rows) {
|
|
6119
|
+
if (!row || typeof row !== 'object')
|
|
6120
|
+
continue;
|
|
6121
|
+
const record = row;
|
|
6122
|
+
const schema = stringFromRecord(record, 'table_schema');
|
|
6123
|
+
const table = stringFromRecord(record, 'table_name');
|
|
6124
|
+
const column = stringFromRecord(record, 'column_name');
|
|
6125
|
+
if (!schema || !table || !column)
|
|
6126
|
+
continue;
|
|
6127
|
+
const relation = `${schema}.${table}`;
|
|
6128
|
+
const current = byRelation.get(relation) ?? {
|
|
6129
|
+
relation,
|
|
6130
|
+
schema,
|
|
6131
|
+
name: table,
|
|
6132
|
+
source: 'runtime information_schema',
|
|
6133
|
+
columns: [],
|
|
6134
|
+
};
|
|
6135
|
+
if (current.columns.length < 80) {
|
|
6136
|
+
current.columns.push({
|
|
6137
|
+
name: column,
|
|
6138
|
+
type: stringFromRecord(record, 'data_type'),
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
byRelation.set(relation, current);
|
|
6142
|
+
}
|
|
6143
|
+
const tokens = agentSchemaTokens(question);
|
|
6144
|
+
return Array.from(byRelation.values())
|
|
6145
|
+
.map((table) => ({ table, score: scoreAgentSchemaTable(table, tokens) }))
|
|
6146
|
+
.filter((entry) => entry.score > 0)
|
|
6147
|
+
.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
|
|
6148
|
+
.slice(0, 12)
|
|
6149
|
+
.map((entry) => entry.table);
|
|
6150
|
+
}
|
|
6151
|
+
function scoreAgentSchemaTable(table, tokens) {
|
|
6152
|
+
let score = 0;
|
|
6153
|
+
const relationTokens = agentSchemaTokens(`${table.schema ?? ''} ${table.name} ${table.relation}`);
|
|
6154
|
+
for (const token of tokens) {
|
|
6155
|
+
if (relationTokens.has(token))
|
|
6156
|
+
score += 8;
|
|
6157
|
+
}
|
|
6158
|
+
for (const column of table.columns) {
|
|
6159
|
+
const columnTokens = agentSchemaTokens(column.name);
|
|
6160
|
+
for (const token of tokens) {
|
|
6161
|
+
if (columnTokens.has(token))
|
|
6162
|
+
score += 3;
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
if (/(customer|order|revenue|product|location|date|month)/i.test(table.name))
|
|
6166
|
+
score += 1;
|
|
6167
|
+
return score;
|
|
6168
|
+
}
|
|
6169
|
+
function agentSchemaTokens(value) {
|
|
6170
|
+
const tokens = new Set();
|
|
6171
|
+
for (const raw of value.toLowerCase().match(/[a-z0-9_]+/g) ?? []) {
|
|
6172
|
+
for (const part of raw.split('_')) {
|
|
6173
|
+
const normalized = normalizeAgentSchemaToken(part);
|
|
6174
|
+
if (!normalized || normalized.length < 3 || AGENT_SCHEMA_STOPWORDS.has(normalized))
|
|
6175
|
+
continue;
|
|
6176
|
+
tokens.add(normalized);
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
return tokens;
|
|
6180
|
+
}
|
|
6181
|
+
const AGENT_SCHEMA_STOPWORDS = new Set([
|
|
6182
|
+
'all', 'and', 'are', 'can', 'data', 'for', 'from', 'have', 'how', 'many', 'me',
|
|
6183
|
+
'show', 'the', 'this', 'who', 'with', 'value',
|
|
6184
|
+
]);
|
|
6185
|
+
function normalizeAgentSchemaToken(token) {
|
|
6186
|
+
if (token === 'orders')
|
|
6187
|
+
return 'order';
|
|
6188
|
+
if (token === 'customers')
|
|
6189
|
+
return 'customer';
|
|
6190
|
+
if (token === 'products')
|
|
6191
|
+
return 'product';
|
|
6192
|
+
if (token.endsWith('ies') && token.length > 4)
|
|
6193
|
+
return `${token.slice(0, -3)}y`;
|
|
6194
|
+
if (token.endsWith('s') && token.length > 4)
|
|
6195
|
+
return token.slice(0, -1);
|
|
6196
|
+
return token;
|
|
6197
|
+
}
|
|
6198
|
+
function stringFromRecord(record, key) {
|
|
6199
|
+
const value = record[key];
|
|
6200
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
6201
|
+
}
|
|
5524
6202
|
function isMemoryScope(value) {
|
|
5525
6203
|
return value === 'thread'
|
|
5526
6204
|
|| value === 'notebook'
|