@duckcodeailabs/dql-cli 1.6.1 → 1.6.2
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/apps-api.test.js +43 -1
- package/dist/apps-api.test.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-60sOoPrg.js +3599 -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/init.test.js +4 -0
- package/dist/commands/init.test.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/new.test.js +106 -0
- package/dist/commands/new.test.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/validate.test.js +85 -0
- package/dist/commands/validate.test.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 +712 -46
- package/dist/local-runtime.js.map +1 -1
- package/dist/local-runtime.test.js +64 -1
- package/dist/local-runtime.test.js.map +1 -1
- package/dist/template-adoption.test.js +3 -0
- package/dist/template-adoption.test.js.map +1 -1
- package/package.json +10 -10
- 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/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,259 @@ 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 authenticator = read('authenticator');
|
|
5307
|
+
const authMethod = privateKeyPath
|
|
5308
|
+
? 'key_pair'
|
|
5309
|
+
: authenticator?.toLowerCase() === 'externalbrowser'
|
|
5310
|
+
? 'external_browser'
|
|
5311
|
+
: 'password';
|
|
5312
|
+
return {
|
|
5313
|
+
adapter,
|
|
5314
|
+
connection: compactConnection({
|
|
5315
|
+
driver: 'snowflake',
|
|
5316
|
+
account: read('account'),
|
|
5317
|
+
warehouse: read('warehouse'),
|
|
5318
|
+
database: read('database'),
|
|
5319
|
+
schema: read('schema'),
|
|
5320
|
+
username: read('user', 'username'),
|
|
5321
|
+
password: read('password'),
|
|
5322
|
+
role: read('role'),
|
|
5323
|
+
privateKeyPath,
|
|
5324
|
+
privateKeyPassphrase: read('private_key_passphrase', 'privateKeyPassphrase'),
|
|
5325
|
+
authenticator,
|
|
5326
|
+
authMethod,
|
|
5327
|
+
}),
|
|
5328
|
+
envRefs: [...envRefs],
|
|
5329
|
+
warnings,
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
case 'bigquery': {
|
|
5333
|
+
const keyFilename = read('keyfile', 'keyFilename');
|
|
5334
|
+
return {
|
|
5335
|
+
adapter,
|
|
5336
|
+
connection: compactConnection({
|
|
5337
|
+
driver: 'bigquery',
|
|
5338
|
+
projectId: read('project', 'projectId'),
|
|
5339
|
+
schema: read('dataset', 'schema'),
|
|
5340
|
+
location: read('location'),
|
|
5341
|
+
keyFilename,
|
|
5342
|
+
authMethod: keyFilename ? 'service_account_key_file' : 'application_default',
|
|
5343
|
+
}),
|
|
5344
|
+
envRefs: [...envRefs],
|
|
5345
|
+
warnings,
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
case 'duckdb':
|
|
5349
|
+
return {
|
|
5350
|
+
adapter,
|
|
5351
|
+
connection: compactConnection({
|
|
5352
|
+
driver: 'duckdb',
|
|
5353
|
+
filepath: read('path', 'database') ?? ':memory:',
|
|
5354
|
+
}),
|
|
5355
|
+
envRefs: [...envRefs],
|
|
5356
|
+
warnings,
|
|
5357
|
+
};
|
|
5358
|
+
case 'databricks':
|
|
5359
|
+
return {
|
|
5360
|
+
adapter,
|
|
5361
|
+
connection: compactConnection({
|
|
5362
|
+
driver: 'databricks',
|
|
5363
|
+
host: read('host', 'server_hostname'),
|
|
5364
|
+
httpPath: read('http_path', 'httpPath'),
|
|
5365
|
+
warehouse: read('warehouse', 'warehouse_id'),
|
|
5366
|
+
catalog: read('catalog'),
|
|
5367
|
+
database: read('catalog', 'database'),
|
|
5368
|
+
schema: read('schema'),
|
|
5369
|
+
token: read('token'),
|
|
5370
|
+
authMethod: 'token',
|
|
5371
|
+
}),
|
|
5372
|
+
envRefs: [...envRefs],
|
|
5373
|
+
warnings,
|
|
5374
|
+
};
|
|
5375
|
+
default:
|
|
5376
|
+
return null;
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
function text(source, ...keys) {
|
|
5380
|
+
for (const key of keys) {
|
|
5381
|
+
const raw = source[key];
|
|
5382
|
+
if (raw === undefined || raw === null)
|
|
5383
|
+
continue;
|
|
5384
|
+
const value = String(raw).trim();
|
|
5385
|
+
if (!value)
|
|
5386
|
+
continue;
|
|
5387
|
+
return resolveDbtEnvVars(value);
|
|
5388
|
+
}
|
|
5389
|
+
return { envRefs: [] };
|
|
5390
|
+
}
|
|
5391
|
+
function resolveDbtEnvVars(value) {
|
|
5392
|
+
const envRefs = [];
|
|
5393
|
+
const replaced = value.replace(/\{\{\s*env_var\(\s*['"]([^'"]+)['"]\s*(?:,\s*(['"])(.*?)\2)?\s*\)\s*\}\}/g, (_match, envKey, _quote, fallback) => {
|
|
5394
|
+
const envValue = process.env[envKey];
|
|
5395
|
+
if (envValue !== undefined)
|
|
5396
|
+
return envValue;
|
|
5397
|
+
if (fallback !== undefined)
|
|
5398
|
+
return fallback;
|
|
5399
|
+
envRefs.push(envKey);
|
|
5400
|
+
return `\${${envKey}}`;
|
|
5401
|
+
});
|
|
5402
|
+
return { value: replaced, envRefs };
|
|
5403
|
+
}
|
|
5404
|
+
function numberValue(source, key) {
|
|
5405
|
+
const raw = source[key];
|
|
5406
|
+
if (raw === undefined || raw === null || raw === '')
|
|
5407
|
+
return undefined;
|
|
5408
|
+
const value = Number(raw);
|
|
5409
|
+
return Number.isFinite(value) ? value : undefined;
|
|
5410
|
+
}
|
|
5411
|
+
function compactConnection(connection) {
|
|
5412
|
+
const compact = {};
|
|
5413
|
+
for (const [key, value] of Object.entries(connection)) {
|
|
5414
|
+
if (value === undefined || value === null || value === '')
|
|
5415
|
+
continue;
|
|
5416
|
+
compact[key] = value;
|
|
5417
|
+
}
|
|
5418
|
+
return compact;
|
|
5419
|
+
}
|
|
5420
|
+
function requiredConnectionFields(connection, envRefs) {
|
|
5421
|
+
const missing = new Set();
|
|
5422
|
+
const needs = (field) => {
|
|
5423
|
+
const value = connection[field];
|
|
5424
|
+
if (value === undefined || value === null || value === '')
|
|
5425
|
+
missing.add(String(field));
|
|
5426
|
+
};
|
|
5427
|
+
switch (connection.driver) {
|
|
5428
|
+
case 'postgresql':
|
|
5429
|
+
case 'redshift':
|
|
5430
|
+
needs('host');
|
|
5431
|
+
needs('database');
|
|
5432
|
+
needs('username');
|
|
5433
|
+
break;
|
|
5434
|
+
case 'snowflake':
|
|
5435
|
+
needs('account');
|
|
5436
|
+
needs('warehouse');
|
|
5437
|
+
needs('database');
|
|
5438
|
+
needs('schema');
|
|
5439
|
+
needs('username');
|
|
5440
|
+
if (connection.authMethod === 'key_pair') {
|
|
5441
|
+
needs('privateKeyPath');
|
|
5442
|
+
}
|
|
5443
|
+
else if (connection.authMethod !== 'external_browser') {
|
|
5444
|
+
needs('password');
|
|
5445
|
+
}
|
|
5446
|
+
break;
|
|
5447
|
+
case 'bigquery':
|
|
5448
|
+
needs('projectId');
|
|
5449
|
+
break;
|
|
5450
|
+
case 'duckdb':
|
|
5451
|
+
needs('filepath');
|
|
5452
|
+
break;
|
|
5453
|
+
case 'databricks':
|
|
5454
|
+
needs('host');
|
|
5455
|
+
if (!connection.httpPath && !connection.warehouse)
|
|
5456
|
+
missing.add('httpPath');
|
|
5457
|
+
needs('token');
|
|
5458
|
+
break;
|
|
5459
|
+
}
|
|
5460
|
+
for (const envKey of envRefs) {
|
|
5461
|
+
if (!process.env[envKey])
|
|
5462
|
+
missing.add(`env:${envKey}`);
|
|
5463
|
+
}
|
|
5464
|
+
return [...missing];
|
|
5465
|
+
}
|
|
5466
|
+
export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
5467
|
+
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
5003
5468
|
const configuredManifest = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
|
|
5004
5469
|
const manifestPath = resolve(dbtProjectPath, configuredManifest);
|
|
5005
5470
|
const catalogPath = resolve(dbtProjectPath, 'target/catalog.json');
|
|
@@ -5029,7 +5494,9 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
|
5029
5494
|
const savedQueryCount = Array.isArray(semanticManifest?.saved_queries)
|
|
5030
5495
|
? semanticManifest.saved_queries.length
|
|
5031
5496
|
: 0;
|
|
5032
|
-
const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
|
|
5497
|
+
const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
|
|
5498
|
+
|| Boolean(projectConfig.dbt?.projectDir)
|
|
5499
|
+
|| Boolean(projectConfig.semanticLayer?.provider === 'dbt' && projectConfig.semanticLayer.projectPath);
|
|
5033
5500
|
const manifestExists = existsSync(manifestPath);
|
|
5034
5501
|
const semanticExists = existsSync(semanticManifestPath);
|
|
5035
5502
|
const setupHint = !configured
|
|
@@ -5146,14 +5613,21 @@ async function execGit(cwd, args) {
|
|
|
5146
5613
|
});
|
|
5147
5614
|
});
|
|
5148
5615
|
}
|
|
5149
|
-
async function
|
|
5616
|
+
async function resolveGitRoot(cwd) {
|
|
5150
5617
|
const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
5151
|
-
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
|
|
5618
|
+
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
|
|
5619
|
+
return null;
|
|
5620
|
+
const root = await execGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
5621
|
+
return root.code === 0 && root.stdout.trim() ? root.stdout.trim() : cwd;
|
|
5622
|
+
}
|
|
5623
|
+
async function readGitStatus(cwd) {
|
|
5624
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5625
|
+
if (!gitRoot) {
|
|
5152
5626
|
return { inRepo: false, branch: null, ahead: 0, behind: 0, changes: [] };
|
|
5153
5627
|
}
|
|
5154
|
-
const branchRes = await execGit(
|
|
5628
|
+
const branchRes = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5155
5629
|
const branch = branchRes.code === 0 ? branchRes.stdout.trim() : null;
|
|
5156
|
-
const trackRes = await execGit(
|
|
5630
|
+
const trackRes = await execGit(gitRoot, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
|
|
5157
5631
|
let ahead = 0;
|
|
5158
5632
|
let behind = 0;
|
|
5159
5633
|
if (trackRes.code === 0) {
|
|
@@ -5161,7 +5635,7 @@ async function readGitStatus(cwd) {
|
|
|
5161
5635
|
behind = Number(match[0] ?? 0);
|
|
5162
5636
|
ahead = Number(match[1] ?? 0);
|
|
5163
5637
|
}
|
|
5164
|
-
const statusRes = await execGit(
|
|
5638
|
+
const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=normal']);
|
|
5165
5639
|
const changes = [];
|
|
5166
5640
|
if (statusRes.code === 0) {
|
|
5167
5641
|
for (const line of statusRes.stdout.split('\n')) {
|
|
@@ -5175,13 +5649,13 @@ async function readGitStatus(cwd) {
|
|
|
5175
5649
|
return { inRepo: true, branch, ahead, behind, changes };
|
|
5176
5650
|
}
|
|
5177
5651
|
async function readGitLog(cwd, limit) {
|
|
5178
|
-
const
|
|
5179
|
-
if (
|
|
5652
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5653
|
+
if (!gitRoot)
|
|
5180
5654
|
return { inRepo: false, commits: [] };
|
|
5181
5655
|
const sep = '\x1f';
|
|
5182
5656
|
const end = '\x1e';
|
|
5183
5657
|
const fmt = ['%H', '%an', '%ad', '%s'].join(sep) + end;
|
|
5184
|
-
const res = await execGit(
|
|
5658
|
+
const res = await execGit(gitRoot, ['log', `-${limit}`, `--pretty=format:${fmt}`, '--date=short']);
|
|
5185
5659
|
if (res.code !== 0)
|
|
5186
5660
|
return { inRepo: true, commits: [] };
|
|
5187
5661
|
const commits = [];
|
|
@@ -5244,23 +5718,97 @@ function ensureGitignoreEntry(projectRoot, pattern) {
|
|
|
5244
5718
|
}
|
|
5245
5719
|
}
|
|
5246
5720
|
async function readGitDiff(cwd, filePath, staged = false) {
|
|
5247
|
-
const
|
|
5248
|
-
if (
|
|
5721
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5722
|
+
if (!gitRoot) {
|
|
5249
5723
|
return { inRepo: false, diff: '', before: null, after: null, diffReport: null };
|
|
5250
5724
|
}
|
|
5251
5725
|
const baseArgs = staged ? ['diff', '--cached', '--no-color'] : ['diff', '--no-color'];
|
|
5252
5726
|
if (!filePath) {
|
|
5253
|
-
const res = await execGit(
|
|
5727
|
+
const res = await execGit(gitRoot, baseArgs);
|
|
5254
5728
|
return { inRepo: true, diff: res.stdout, before: null, after: null, diffReport: null };
|
|
5255
5729
|
}
|
|
5256
5730
|
const isSemantic = filePath.endsWith('.dql') || filePath.endsWith('.dqlnb');
|
|
5257
5731
|
const [diffRes, before, after] = await Promise.all([
|
|
5258
|
-
execGit(
|
|
5259
|
-
isSemantic ? readHeadBlob(
|
|
5260
|
-
isSemantic ? readWorkingCopy(join(
|
|
5732
|
+
execGit(gitRoot, [...baseArgs, '--', filePath]),
|
|
5733
|
+
isSemantic ? readHeadBlob(gitRoot, filePath) : Promise.resolve(null),
|
|
5734
|
+
isSemantic ? readWorkingCopy(join(gitRoot, filePath)) : Promise.resolve(null),
|
|
5261
5735
|
]);
|
|
5736
|
+
const diffText = !staged && !diffRes.stdout.trim()
|
|
5737
|
+
? (await readUntrackedTextDiff(gitRoot, filePath)) || diffRes.stdout
|
|
5738
|
+
: diffRes.stdout;
|
|
5262
5739
|
const diffReport = isSemantic ? computeSemanticDiff(filePath, before, after) : null;
|
|
5263
|
-
return { inRepo: true, diff:
|
|
5740
|
+
return { inRepo: true, diff: diffText, before, after, diffReport };
|
|
5741
|
+
}
|
|
5742
|
+
const MAX_UNTRACKED_DIFF_FILES = 20;
|
|
5743
|
+
const MAX_UNTRACKED_DIFF_BYTES = 512 * 1024;
|
|
5744
|
+
async function readUntrackedTextDiff(cwd, filePath) {
|
|
5745
|
+
const status = await execGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '--', filePath]);
|
|
5746
|
+
if (status.code !== 0 || !status.stdout.split('\n').some((line) => line.startsWith('?? '))) {
|
|
5747
|
+
return '';
|
|
5748
|
+
}
|
|
5749
|
+
const listed = await execGit(cwd, ['ls-files', '--others', '--exclude-standard', '--', filePath]);
|
|
5750
|
+
if (listed.code !== 0)
|
|
5751
|
+
return '';
|
|
5752
|
+
const chunks = [];
|
|
5753
|
+
let totalBytes = 0;
|
|
5754
|
+
for (const rawPath of listed.stdout.split('\n').map((p) => p.trim()).filter(Boolean)) {
|
|
5755
|
+
if (chunks.length >= MAX_UNTRACKED_DIFF_FILES || totalBytes >= MAX_UNTRACKED_DIFF_BYTES)
|
|
5756
|
+
break;
|
|
5757
|
+
const absPath = safeJoin(cwd, rawPath);
|
|
5758
|
+
if (!absPath || !existsSync(absPath))
|
|
5759
|
+
continue;
|
|
5760
|
+
const st = statSync(absPath);
|
|
5761
|
+
if (!st.isFile())
|
|
5762
|
+
continue;
|
|
5763
|
+
if (st.size > MAX_UNTRACKED_DIFF_BYTES) {
|
|
5764
|
+
chunks.push(formatBinaryAddedDiff(rawPath));
|
|
5765
|
+
continue;
|
|
5766
|
+
}
|
|
5767
|
+
const buf = readFileSync(absPath);
|
|
5768
|
+
if (buf.includes(0)) {
|
|
5769
|
+
chunks.push(formatBinaryAddedDiff(rawPath));
|
|
5770
|
+
continue;
|
|
5771
|
+
}
|
|
5772
|
+
totalBytes += buf.length;
|
|
5773
|
+
chunks.push(formatAddedFileDiff(rawPath, buf.toString('utf-8')));
|
|
5774
|
+
}
|
|
5775
|
+
if (chunks.length === 0)
|
|
5776
|
+
return '';
|
|
5777
|
+
const omitted = listed.stdout.split('\n').filter(Boolean).length - chunks.length;
|
|
5778
|
+
if (omitted > 0) {
|
|
5779
|
+
chunks.push(`diff --git a/${filePath} b/${filePath}\n# ${omitted} additional untracked file${omitted === 1 ? '' : 's'} omitted from preview`);
|
|
5780
|
+
}
|
|
5781
|
+
return chunks.join('\n');
|
|
5782
|
+
}
|
|
5783
|
+
function formatAddedFileDiff(filePath, content) {
|
|
5784
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
5785
|
+
const hasFinalNewline = normalized.endsWith('\n');
|
|
5786
|
+
const lines = normalized.length === 0
|
|
5787
|
+
? []
|
|
5788
|
+
: normalized.split('\n').slice(0, hasFinalNewline ? -1 : undefined);
|
|
5789
|
+
const hunk = lines.length > 0
|
|
5790
|
+
? [`@@ -0,0 +1,${lines.length} @@`, ...lines.map((line) => `+${line}`)]
|
|
5791
|
+
: [];
|
|
5792
|
+
if (!hasFinalNewline && normalized.length > 0)
|
|
5793
|
+
hunk.push('\');
|
|
5794
|
+
return [
|
|
5795
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
5796
|
+
'new file mode 100644',
|
|
5797
|
+
'index 0000000..0000000',
|
|
5798
|
+
'--- /dev/null',
|
|
5799
|
+
`+++ b/${filePath}`,
|
|
5800
|
+
...hunk,
|
|
5801
|
+
].join('\n');
|
|
5802
|
+
}
|
|
5803
|
+
function formatBinaryAddedDiff(filePath) {
|
|
5804
|
+
return [
|
|
5805
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
5806
|
+
'new file mode 100644',
|
|
5807
|
+
'index 0000000..0000000',
|
|
5808
|
+
'--- /dev/null',
|
|
5809
|
+
`+++ b/${filePath}`,
|
|
5810
|
+
`Binary file ${filePath} added`,
|
|
5811
|
+
].join('\n');
|
|
5264
5812
|
}
|
|
5265
5813
|
// ── git write operations ──────────────────────────────────────────────────
|
|
5266
5814
|
// Each helper validates inputs, shells out via execFile (no shell expansion),
|
|
@@ -5290,89 +5838,118 @@ function gitErrorOutput(res) {
|
|
|
5290
5838
|
return (res.stderr || res.stdout || '').trim();
|
|
5291
5839
|
}
|
|
5292
5840
|
async function gitStage(cwd, paths) {
|
|
5293
|
-
const
|
|
5841
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5842
|
+
if (!gitRoot)
|
|
5843
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5844
|
+
const v = validatePaths(gitRoot, paths);
|
|
5294
5845
|
if (!v.ok)
|
|
5295
5846
|
return { ok: false, error: v.error };
|
|
5296
|
-
const res = await execGit(
|
|
5847
|
+
const res = await execGit(gitRoot, ['add', '--', ...v.paths]);
|
|
5297
5848
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5298
5849
|
}
|
|
5299
5850
|
async function gitUnstage(cwd, paths) {
|
|
5300
|
-
const
|
|
5851
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5852
|
+
if (!gitRoot)
|
|
5853
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5854
|
+
const v = validatePaths(gitRoot, paths);
|
|
5301
5855
|
if (!v.ok)
|
|
5302
5856
|
return { ok: false, error: v.error };
|
|
5303
5857
|
// `restore --staged` works with or without HEAD; for an initial commit (no
|
|
5304
5858
|
// HEAD yet) git's `rm --cached` is the fallback. Try restore first.
|
|
5305
|
-
const res = await execGit(
|
|
5859
|
+
const res = await execGit(gitRoot, ['restore', '--staged', '--', ...v.paths]);
|
|
5306
5860
|
if (res.code === 0)
|
|
5307
5861
|
return { ok: true };
|
|
5308
|
-
const fallback = await execGit(
|
|
5862
|
+
const fallback = await execGit(gitRoot, ['rm', '--cached', '-r', '--', ...v.paths]);
|
|
5309
5863
|
return fallback.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(fallback) };
|
|
5310
5864
|
}
|
|
5311
5865
|
async function gitDiscard(cwd, paths) {
|
|
5312
|
-
const
|
|
5866
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5867
|
+
if (!gitRoot)
|
|
5868
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5869
|
+
const v = validatePaths(gitRoot, paths);
|
|
5313
5870
|
if (!v.ok)
|
|
5314
5871
|
return { ok: false, error: v.error };
|
|
5315
5872
|
// For tracked files: `restore --worktree` reverts to HEAD. For untracked
|
|
5316
5873
|
// files: that's a no-op and we delete them via `clean -f`. Run both so
|
|
5317
5874
|
// the caller doesn't have to know which list each path is in.
|
|
5318
|
-
const restore = await execGit(
|
|
5319
|
-
const clean = await execGit(
|
|
5875
|
+
const restore = await execGit(gitRoot, ['restore', '--worktree', '--', ...v.paths]);
|
|
5876
|
+
const clean = await execGit(gitRoot, ['clean', '-f', '--', ...v.paths]);
|
|
5320
5877
|
if (restore.code !== 0 && clean.code !== 0) {
|
|
5321
5878
|
return { ok: false, error: gitErrorOutput(restore) || gitErrorOutput(clean) };
|
|
5322
5879
|
}
|
|
5323
5880
|
return { ok: true };
|
|
5324
5881
|
}
|
|
5325
5882
|
async function gitCommit(cwd, message, stageAll) {
|
|
5883
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5884
|
+
if (!gitRoot)
|
|
5885
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5326
5886
|
const trimmed = message.trim();
|
|
5327
5887
|
if (!trimmed)
|
|
5328
5888
|
return { ok: false, error: 'Commit message required' };
|
|
5329
5889
|
if (stageAll) {
|
|
5330
|
-
const add = await execGit(
|
|
5890
|
+
const add = await execGit(gitRoot, ['add', '-A']);
|
|
5331
5891
|
if (add.code !== 0)
|
|
5332
5892
|
return { ok: false, error: gitErrorOutput(add) };
|
|
5333
5893
|
}
|
|
5334
|
-
const res = await execGit(
|
|
5894
|
+
const res = await execGit(gitRoot, ['commit', '-m', trimmed]);
|
|
5335
5895
|
if (res.code !== 0)
|
|
5336
5896
|
return { ok: false, error: gitErrorOutput(res) };
|
|
5337
|
-
const hashRes = await execGit(
|
|
5897
|
+
const hashRes = await execGit(gitRoot, ['rev-parse', 'HEAD']);
|
|
5338
5898
|
return { ok: true, hash: hashRes.code === 0 ? hashRes.stdout.trim() : undefined };
|
|
5339
5899
|
}
|
|
5340
5900
|
async function gitPush(cwd) {
|
|
5341
|
-
const
|
|
5901
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5902
|
+
if (!gitRoot)
|
|
5903
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5904
|
+
const branch = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5905
|
+
const upstream = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
5906
|
+
const remotes = await execGit(gitRoot, ['remote']);
|
|
5907
|
+
const remote = remotes.stdout.split('\n').map((s) => s.trim()).find(Boolean) ?? 'origin';
|
|
5908
|
+
const branchName = branch.code === 0 ? branch.stdout.trim() : '';
|
|
5909
|
+
const args = upstream.code === 0 || !branchName || branchName === 'HEAD'
|
|
5910
|
+
? ['push']
|
|
5911
|
+
: ['push', '-u', remote, branchName];
|
|
5912
|
+
const res = await execGit(gitRoot, args);
|
|
5342
5913
|
return res.code === 0
|
|
5343
5914
|
? { ok: true, output: gitErrorOutput(res) }
|
|
5344
5915
|
: { ok: false, error: gitErrorOutput(res) };
|
|
5345
5916
|
}
|
|
5346
5917
|
async function gitPull(cwd) {
|
|
5918
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5919
|
+
if (!gitRoot)
|
|
5920
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5347
5921
|
// `--ff-only` keeps the operation non-destructive: if the local branch has
|
|
5348
5922
|
// diverged from upstream, we surface the error rather than auto-merging.
|
|
5349
5923
|
// The user can resolve via the terminal or a future merge UI.
|
|
5350
|
-
const res = await execGit(
|
|
5924
|
+
const res = await execGit(gitRoot, ['pull', '--ff-only']);
|
|
5351
5925
|
return res.code === 0
|
|
5352
5926
|
? { ok: true, output: gitErrorOutput(res) }
|
|
5353
5927
|
: { ok: false, error: gitErrorOutput(res) };
|
|
5354
5928
|
}
|
|
5355
5929
|
async function readGitBranches(cwd) {
|
|
5356
|
-
const
|
|
5357
|
-
if (
|
|
5930
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5931
|
+
if (!gitRoot)
|
|
5358
5932
|
return { inRepo: false, current: null, branches: [] };
|
|
5359
|
-
const cur = await execGit(
|
|
5360
|
-
const list = await execGit(
|
|
5933
|
+
const cur = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5934
|
+
const list = await execGit(gitRoot, ['branch', '--list', '--format=%(refname:short)']);
|
|
5361
5935
|
const branches = list.code === 0
|
|
5362
5936
|
? list.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
5363
5937
|
: [];
|
|
5364
5938
|
return { inRepo: true, current: cur.code === 0 ? cur.stdout.trim() : null, branches };
|
|
5365
5939
|
}
|
|
5366
5940
|
async function readGitRemote(cwd) {
|
|
5367
|
-
const
|
|
5368
|
-
if (
|
|
5941
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5942
|
+
if (!gitRoot)
|
|
5369
5943
|
return { inRepo: false, url: null, name: null };
|
|
5370
|
-
const remoteName = await execGit(
|
|
5944
|
+
const remoteName = await execGit(gitRoot, ['config', '--get', 'remote.pushDefault']);
|
|
5371
5945
|
const name = remoteName.code === 0 && remoteName.stdout.trim() ? remoteName.stdout.trim() : 'origin';
|
|
5372
|
-
const url = await execGit(
|
|
5946
|
+
const url = await execGit(gitRoot, ['remote', 'get-url', name]);
|
|
5373
5947
|
return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
|
|
5374
5948
|
}
|
|
5375
5949
|
async function gitCreateBranch(cwd, name, checkout) {
|
|
5950
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5951
|
+
if (!gitRoot)
|
|
5952
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5376
5953
|
const trimmed = name.trim();
|
|
5377
5954
|
// Branch names can't start with `-` (would be parsed as a flag) and must be
|
|
5378
5955
|
// non-empty. git itself enforces the rest of the ref-name rules.
|
|
@@ -5381,17 +5958,20 @@ async function gitCreateBranch(cwd, name, checkout) {
|
|
|
5381
5958
|
if (trimmed.startsWith('-'))
|
|
5382
5959
|
return { ok: false, error: 'Invalid branch name' };
|
|
5383
5960
|
const res = checkout
|
|
5384
|
-
? await execGit(
|
|
5385
|
-
: await execGit(
|
|
5961
|
+
? await execGit(gitRoot, ['checkout', '-b', trimmed])
|
|
5962
|
+
: await execGit(gitRoot, ['branch', trimmed]);
|
|
5386
5963
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5387
5964
|
}
|
|
5388
5965
|
async function gitCheckout(cwd, name) {
|
|
5966
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5967
|
+
if (!gitRoot)
|
|
5968
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5389
5969
|
const trimmed = name.trim();
|
|
5390
5970
|
if (!trimmed)
|
|
5391
5971
|
return { ok: false, error: 'Branch name required' };
|
|
5392
5972
|
if (trimmed.startsWith('-'))
|
|
5393
5973
|
return { ok: false, error: 'Invalid branch name' };
|
|
5394
|
-
const res = await execGit(
|
|
5974
|
+
const res = await execGit(gitRoot, ['checkout', trimmed]);
|
|
5395
5975
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5396
5976
|
}
|
|
5397
5977
|
async function readHeadBlob(cwd, filePath) {
|
|
@@ -5521,6 +6101,92 @@ function isAiPinRefreshDue(lastRefreshedAt) {
|
|
|
5521
6101
|
return true;
|
|
5522
6102
|
return Date.now() - last >= 24 * 60 * 60 * 1000;
|
|
5523
6103
|
}
|
|
6104
|
+
function buildAgentSchemaContext(question, rows) {
|
|
6105
|
+
const byRelation = new Map();
|
|
6106
|
+
for (const row of rows) {
|
|
6107
|
+
if (!row || typeof row !== 'object')
|
|
6108
|
+
continue;
|
|
6109
|
+
const record = row;
|
|
6110
|
+
const schema = stringFromRecord(record, 'table_schema');
|
|
6111
|
+
const table = stringFromRecord(record, 'table_name');
|
|
6112
|
+
const column = stringFromRecord(record, 'column_name');
|
|
6113
|
+
if (!schema || !table || !column)
|
|
6114
|
+
continue;
|
|
6115
|
+
const relation = `${schema}.${table}`;
|
|
6116
|
+
const current = byRelation.get(relation) ?? {
|
|
6117
|
+
relation,
|
|
6118
|
+
schema,
|
|
6119
|
+
name: table,
|
|
6120
|
+
source: 'runtime information_schema',
|
|
6121
|
+
columns: [],
|
|
6122
|
+
};
|
|
6123
|
+
if (current.columns.length < 80) {
|
|
6124
|
+
current.columns.push({
|
|
6125
|
+
name: column,
|
|
6126
|
+
type: stringFromRecord(record, 'data_type'),
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
6129
|
+
byRelation.set(relation, current);
|
|
6130
|
+
}
|
|
6131
|
+
const tokens = agentSchemaTokens(question);
|
|
6132
|
+
return Array.from(byRelation.values())
|
|
6133
|
+
.map((table) => ({ table, score: scoreAgentSchemaTable(table, tokens) }))
|
|
6134
|
+
.filter((entry) => entry.score > 0)
|
|
6135
|
+
.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
|
|
6136
|
+
.slice(0, 12)
|
|
6137
|
+
.map((entry) => entry.table);
|
|
6138
|
+
}
|
|
6139
|
+
function scoreAgentSchemaTable(table, tokens) {
|
|
6140
|
+
let score = 0;
|
|
6141
|
+
const relationTokens = agentSchemaTokens(`${table.schema ?? ''} ${table.name} ${table.relation}`);
|
|
6142
|
+
for (const token of tokens) {
|
|
6143
|
+
if (relationTokens.has(token))
|
|
6144
|
+
score += 8;
|
|
6145
|
+
}
|
|
6146
|
+
for (const column of table.columns) {
|
|
6147
|
+
const columnTokens = agentSchemaTokens(column.name);
|
|
6148
|
+
for (const token of tokens) {
|
|
6149
|
+
if (columnTokens.has(token))
|
|
6150
|
+
score += 3;
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
if (/(customer|order|revenue|product|location|date|month)/i.test(table.name))
|
|
6154
|
+
score += 1;
|
|
6155
|
+
return score;
|
|
6156
|
+
}
|
|
6157
|
+
function agentSchemaTokens(value) {
|
|
6158
|
+
const tokens = new Set();
|
|
6159
|
+
for (const raw of value.toLowerCase().match(/[a-z0-9_]+/g) ?? []) {
|
|
6160
|
+
for (const part of raw.split('_')) {
|
|
6161
|
+
const normalized = normalizeAgentSchemaToken(part);
|
|
6162
|
+
if (!normalized || normalized.length < 3 || AGENT_SCHEMA_STOPWORDS.has(normalized))
|
|
6163
|
+
continue;
|
|
6164
|
+
tokens.add(normalized);
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
return tokens;
|
|
6168
|
+
}
|
|
6169
|
+
const AGENT_SCHEMA_STOPWORDS = new Set([
|
|
6170
|
+
'all', 'and', 'are', 'can', 'data', 'for', 'from', 'have', 'how', 'many', 'me',
|
|
6171
|
+
'show', 'the', 'this', 'who', 'with', 'value',
|
|
6172
|
+
]);
|
|
6173
|
+
function normalizeAgentSchemaToken(token) {
|
|
6174
|
+
if (token === 'orders')
|
|
6175
|
+
return 'order';
|
|
6176
|
+
if (token === 'customers')
|
|
6177
|
+
return 'customer';
|
|
6178
|
+
if (token === 'products')
|
|
6179
|
+
return 'product';
|
|
6180
|
+
if (token.endsWith('ies') && token.length > 4)
|
|
6181
|
+
return `${token.slice(0, -3)}y`;
|
|
6182
|
+
if (token.endsWith('s') && token.length > 4)
|
|
6183
|
+
return token.slice(0, -1);
|
|
6184
|
+
return token;
|
|
6185
|
+
}
|
|
6186
|
+
function stringFromRecord(record, key) {
|
|
6187
|
+
const value = record[key];
|
|
6188
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
6189
|
+
}
|
|
5524
6190
|
function isMemoryScope(value) {
|
|
5525
6191
|
return value === 'thread'
|
|
5526
6192
|
|| value === 'notebook'
|