@duckcodeailabs/dql-cli 1.6.12 → 1.6.13

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.
@@ -1,6 +1,7 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execFileSync, 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 { createRequire } from 'node:module';
4
5
  import { homedir } from 'node:os';
5
6
  import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
6
7
  import Anthropic from '@anthropic-ai/sdk';
@@ -1928,8 +1929,32 @@ export async function startLocalServer(opts) {
1928
1929
  ?? Object.keys(connections)[0]
1929
1930
  ?? 'default';
1930
1931
  const dbtProfiles = discoverDbtProfileConnections(projectRoot, cfg);
1932
+ const connectorStatus = getConnectorInstallStatuses(projectRoot);
1931
1933
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1932
- res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles }));
1934
+ res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles, connectorStatus }));
1935
+ return;
1936
+ }
1937
+ if (req.method === 'POST' && path === '/api/connectors/install') {
1938
+ try {
1939
+ const body = await readJSON(req);
1940
+ const driver = typeof body.driver === 'string' ? body.driver : '';
1941
+ const status = installConnectorPackage(projectRoot, driver);
1942
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1943
+ res.end(serializeJSON({
1944
+ ok: true,
1945
+ status,
1946
+ connectorStatus: getConnectorInstallStatuses(projectRoot),
1947
+ }));
1948
+ }
1949
+ catch (error) {
1950
+ const message = error instanceof Error ? error.message : String(error);
1951
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1952
+ res.end(serializeJSON({
1953
+ ok: false,
1954
+ error: message,
1955
+ connectorStatus: getConnectorInstallStatuses(projectRoot),
1956
+ }));
1957
+ }
1933
1958
  return;
1934
1959
  }
1935
1960
  // Save/update connections
@@ -3636,6 +3661,90 @@ function getProjectConnectionsForApi(config) {
3636
3661
  }
3637
3662
  return connections;
3638
3663
  }
3664
+ const CONNECTOR_INSTALLS = {
3665
+ duckdb: {
3666
+ driver: 'duckdb',
3667
+ label: 'DuckDB',
3668
+ packageName: 'duckdb',
3669
+ packageSpec: 'duckdb@^1.1.0',
3670
+ builtIn: false,
3671
+ },
3672
+ snowflake: {
3673
+ driver: 'snowflake',
3674
+ label: 'Snowflake',
3675
+ packageName: 'snowflake-sdk',
3676
+ packageSpec: 'snowflake-sdk@^1.12.0',
3677
+ builtIn: false,
3678
+ },
3679
+ databricks: {
3680
+ driver: 'databricks',
3681
+ label: 'Databricks',
3682
+ builtIn: true,
3683
+ },
3684
+ };
3685
+ function connectorInstallRoot(projectRoot) {
3686
+ return join(projectRoot, '.dql', 'connectors');
3687
+ }
3688
+ function connectorModuleSearchPaths(projectRoot) {
3689
+ return [connectorInstallRoot(projectRoot), projectRoot];
3690
+ }
3691
+ function connectorInstallCommand(projectRoot, packageSpec) {
3692
+ return `npm install --prefix ${connectorInstallRoot(projectRoot)} ${packageSpec}`;
3693
+ }
3694
+ function isConnectorPackageInstalled(projectRoot, packageName) {
3695
+ for (const basePath of connectorModuleSearchPaths(projectRoot)) {
3696
+ try {
3697
+ const req = createRequire(join(basePath, 'package.json'));
3698
+ req.resolve(packageName);
3699
+ return true;
3700
+ }
3701
+ catch {
3702
+ // Try the next supported location.
3703
+ }
3704
+ }
3705
+ return false;
3706
+ }
3707
+ export function getConnectorInstallStatuses(projectRoot) {
3708
+ return Object.values(CONNECTOR_INSTALLS).map((definition) => {
3709
+ const installPath = connectorInstallRoot(projectRoot);
3710
+ const installed = definition.builtIn || (definition.packageName
3711
+ ? isConnectorPackageInstalled(projectRoot, definition.packageName)
3712
+ : true);
3713
+ return {
3714
+ ...definition,
3715
+ installed,
3716
+ installPath,
3717
+ installCommand: definition.packageSpec
3718
+ ? connectorInstallCommand(projectRoot, definition.packageSpec)
3719
+ : undefined,
3720
+ };
3721
+ });
3722
+ }
3723
+ function installConnectorPackage(projectRoot, driver) {
3724
+ const definition = CONNECTOR_INSTALLS[driver];
3725
+ if (!definition) {
3726
+ throw new Error(`Unknown connector "${driver}".`);
3727
+ }
3728
+ if (definition.builtIn || !definition.packageSpec) {
3729
+ return getConnectorInstallStatuses(projectRoot).find((status) => status.driver === definition.driver);
3730
+ }
3731
+ const installRoot = connectorInstallRoot(projectRoot);
3732
+ mkdirSync(installRoot, { recursive: true });
3733
+ const packageJsonPath = join(installRoot, 'package.json');
3734
+ if (!existsSync(packageJsonPath)) {
3735
+ writeFileSync(packageJsonPath, JSON.stringify({
3736
+ private: true,
3737
+ description: 'Project-local DQL connector packages',
3738
+ }, null, 2) + '\n', 'utf-8');
3739
+ }
3740
+ execFileSync('npm', ['install', '--prefix', installRoot, '--no-audit', '--no-fund', definition.packageSpec], {
3741
+ cwd: projectRoot,
3742
+ encoding: 'utf-8',
3743
+ stdio: 'pipe',
3744
+ timeout: 10 * 60 * 1000,
3745
+ });
3746
+ return getConnectorInstallStatuses(projectRoot).find((status) => status.driver === definition.driver);
3747
+ }
3639
3748
  function getStoredConnections(raw) {
3640
3749
  const value = raw.connections;
3641
3750
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -3940,6 +4049,9 @@ export function normalizeProjectConnection(connection, projectRoot) {
3940
4049
  if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
3941
4050
  normalized.filepath = resolve(projectRoot, normalized.filepath);
3942
4051
  }
4052
+ if (normalized.driver === 'file' || normalized.driver === 'duckdb' || normalized.driver === 'snowflake') {
4053
+ normalized.moduleSearchPaths = connectorModuleSearchPaths(projectRoot);
4054
+ }
3943
4055
  if (normalized.driver === 'sqlite' && normalized.database && normalized.database !== ':memory:' && !isAbsoluteLikePath(normalized.database)) {
3944
4056
  normalized.database = resolve(projectRoot, normalized.database);
3945
4057
  }
@@ -5711,45 +5823,7 @@ function mapDbtProfileOutput(output) {
5711
5823
  result.envRefs.forEach((ref) => envRefs.add(ref));
5712
5824
  return result.value;
5713
5825
  };
5714
- const port = numberValue(output, 'port');
5715
- const sslRaw = read('ssl', 'sslmode');
5716
- const ssl = sslRaw === undefined
5717
- ? undefined
5718
- : !['false', '0', 'disable', 'disabled', 'off'].includes(sslRaw.toLowerCase());
5719
5826
  switch (adapter) {
5720
- case 'postgres':
5721
- case 'postgresql':
5722
- return {
5723
- adapter,
5724
- connection: compactConnection({
5725
- driver: 'postgresql',
5726
- host: read('host'),
5727
- port,
5728
- database: read('dbname', 'database'),
5729
- schema: read('schema'),
5730
- username: read('user', 'username'),
5731
- password: read('password', 'pass'),
5732
- ssl,
5733
- }),
5734
- envRefs: [...envRefs],
5735
- warnings,
5736
- };
5737
- case 'redshift':
5738
- return {
5739
- adapter,
5740
- connection: compactConnection({
5741
- driver: 'redshift',
5742
- host: read('host'),
5743
- port: port ?? 5439,
5744
- database: read('dbname', 'database'),
5745
- schema: read('schema'),
5746
- username: read('user', 'username'),
5747
- password: read('password', 'pass'),
5748
- ssl,
5749
- }),
5750
- envRefs: [...envRefs],
5751
- warnings,
5752
- };
5753
5827
  case 'snowflake': {
5754
5828
  const privateKeyPath = read('private_key_path', 'privateKeyPath');
5755
5829
  const privateKey = read('private_key', 'privateKey');
@@ -5759,9 +5833,19 @@ function mapDbtProfileOutput(output) {
5759
5833
  ? 'key_pair'
5760
5834
  : normalizedAuthenticator === 'externalbrowser'
5761
5835
  ? 'external_browser'
5762
- : normalizedAuthenticator === 'oauth' || normalizedAuthenticator === 'programmaticaccesstoken'
5763
- ? 'oauth'
5764
- : 'password';
5836
+ : normalizedAuthenticator === 'usernamepasswordmfa'
5837
+ ? 'mfa'
5838
+ : normalizedAuthenticator === 'oauthauthorizationcode'
5839
+ ? 'oauth_authorization_code'
5840
+ : normalizedAuthenticator === 'oauthclientcredentials'
5841
+ ? 'oauth_client_credentials'
5842
+ : normalizedAuthenticator === 'programmaticaccesstoken'
5843
+ ? 'programmatic_access_token'
5844
+ : normalizedAuthenticator === 'workloadidentity'
5845
+ ? 'workload_identity'
5846
+ : normalizedAuthenticator === 'oauth'
5847
+ ? 'oauth'
5848
+ : 'password';
5765
5849
  return {
5766
5850
  adapter,
5767
5851
  connection: compactConnection({
@@ -5778,22 +5862,34 @@ function mapDbtProfileOutput(output) {
5778
5862
  privateKeyPassphrase: read('private_key_passphrase', 'privateKeyPassphrase'),
5779
5863
  authenticator,
5780
5864
  authMethod,
5781
- }),
5782
- envRefs: [...envRefs],
5783
- warnings,
5784
- };
5785
- }
5786
- case 'bigquery': {
5787
- const keyFilename = read('keyfile', 'keyFilename');
5788
- return {
5789
- adapter,
5790
- connection: compactConnection({
5791
- driver: 'bigquery',
5792
- projectId: read('project', 'projectId'),
5793
- schema: read('dataset', 'schema'),
5794
- location: read('location'),
5795
- keyFilename,
5796
- authMethod: keyFilename ? 'service_account_key_file' : 'application_default',
5865
+ token: read('token'),
5866
+ accessUrl: read('access_url', 'accessUrl'),
5867
+ application: read('application'),
5868
+ browserActionTimeout: readNumber(output, 'browser_action_timeout', 'browserActionTimeout'),
5869
+ clientRequestMFAToken: readBoolean(output, 'client_request_mfa_token', 'clientRequestMFAToken'),
5870
+ clientStoreTemporaryCredential: readBoolean(output, 'client_store_temporary_credential', 'clientStoreTemporaryCredential'),
5871
+ clientSessionKeepAlive: readBoolean(output, 'client_session_keep_alive', 'clientSessionKeepAlive'),
5872
+ clientSessionKeepAliveHeartbeatFrequency: readNumber(output, 'client_session_keep_alive_heartbeat_frequency', 'clientSessionKeepAliveHeartbeatFrequency'),
5873
+ credentialCacheDir: read('credential_cache_dir', 'credentialCacheDir'),
5874
+ keepAlive: readBoolean(output, 'keep_alive', 'keepAlive'),
5875
+ noProxy: read('no_proxy', 'noProxy'),
5876
+ oauthAuthorizationUrl: read('oauth_authorization_url', 'oauthAuthorizationUrl'),
5877
+ oauthClientId: read('oauth_client_id', 'oauthClientId'),
5878
+ oauthClientSecret: read('oauth_client_secret', 'oauthClientSecret'),
5879
+ oauthRedirectUri: read('oauth_redirect_uri', 'oauthRedirectUri'),
5880
+ oauthScope: read('oauth_scope', 'oauthScope'),
5881
+ oauthTokenRequestUrl: read('oauth_token_request_url', 'oauthTokenRequestUrl'),
5882
+ passcode: read('passcode'),
5883
+ passcodeInPassword: readBoolean(output, 'passcode_in_password', 'passcodeInPassword'),
5884
+ proxyHost: read('proxy_host', 'proxyHost'),
5885
+ proxyPassword: read('proxy_password', 'proxyPassword'),
5886
+ proxyPort: readNumber(output, 'proxy_port', 'proxyPort'),
5887
+ proxyProtocol: read('proxy_protocol', 'proxyProtocol'),
5888
+ proxyUser: read('proxy_user', 'proxyUser'),
5889
+ queryTag: read('query_tag', 'queryTag'),
5890
+ timeout: readNumber(output, 'timeout'),
5891
+ workloadIdentityProvider: read('workload_identity_provider', 'workloadIdentityProvider'),
5892
+ workloadIdentityAzureClientId: read('workload_identity_azure_client_id', 'workloadIdentityAzureClientId'),
5797
5893
  }),
5798
5894
  envRefs: [...envRefs],
5799
5895
  warnings,
@@ -5809,7 +5905,9 @@ function mapDbtProfileOutput(output) {
5809
5905
  envRefs: [...envRefs],
5810
5906
  warnings,
5811
5907
  };
5812
- case 'databricks':
5908
+ case 'databricks': {
5909
+ const databricksAuth = read('auth_type', 'auth_method', 'authMethod');
5910
+ const authMethod = databricksAuth?.toLowerCase().includes('oauth') ? 'oauth' : 'token';
5813
5911
  return {
5814
5912
  adapter,
5815
5913
  connection: compactConnection({
@@ -5821,15 +5919,44 @@ function mapDbtProfileOutput(output) {
5821
5919
  database: read('catalog', 'database'),
5822
5920
  schema: read('schema'),
5823
5921
  token: read('token'),
5824
- authMethod: 'token',
5922
+ authMethod,
5923
+ waitTimeout: read('wait_timeout', 'waitTimeout'),
5924
+ byteLimit: readNumber(output, 'byte_limit', 'byteLimit'),
5825
5925
  }),
5826
5926
  envRefs: [...envRefs],
5827
5927
  warnings,
5828
5928
  };
5929
+ }
5829
5930
  default:
5830
5931
  return null;
5831
5932
  }
5832
5933
  }
5934
+ function readBoolean(source, ...keys) {
5935
+ for (const key of keys) {
5936
+ const raw = source[key];
5937
+ if (typeof raw === 'boolean')
5938
+ return raw;
5939
+ if (raw === undefined || raw === null)
5940
+ continue;
5941
+ const value = String(raw).trim().toLowerCase();
5942
+ if (['true', '1', 'yes', 'y'].includes(value))
5943
+ return true;
5944
+ if (['false', '0', 'no', 'n'].includes(value))
5945
+ return false;
5946
+ }
5947
+ return undefined;
5948
+ }
5949
+ function readNumber(source, ...keys) {
5950
+ for (const key of keys) {
5951
+ const raw = source[key];
5952
+ if (raw === undefined || raw === null || raw === '')
5953
+ continue;
5954
+ const value = Number(raw);
5955
+ if (Number.isFinite(value))
5956
+ return value;
5957
+ }
5958
+ return undefined;
5959
+ }
5833
5960
  function text(source, ...keys) {
5834
5961
  for (const key of keys) {
5835
5962
  const raw = source[key];
@@ -5855,13 +5982,6 @@ function resolveDbtEnvVars(value) {
5855
5982
  });
5856
5983
  return { value: replaced, envRefs };
5857
5984
  }
5858
- function numberValue(source, key) {
5859
- const raw = source[key];
5860
- if (raw === undefined || raw === null || raw === '')
5861
- return undefined;
5862
- const value = Number(raw);
5863
- return Number.isFinite(value) ? value : undefined;
5864
- }
5865
5985
  function compactConnection(connection) {
5866
5986
  const compact = {};
5867
5987
  for (const [key, value] of Object.entries(connection)) {
@@ -5879,12 +5999,6 @@ function requiredConnectionFields(connection, envRefs) {
5879
5999
  missing.add(String(field));
5880
6000
  };
5881
6001
  switch (connection.driver) {
5882
- case 'postgresql':
5883
- case 'redshift':
5884
- needs('host');
5885
- needs('database');
5886
- needs('username');
5887
- break;
5888
6002
  case 'snowflake':
5889
6003
  needs('account');
5890
6004
  needs('warehouse');
@@ -5901,13 +6015,27 @@ function requiredConnectionFields(connection, envRefs) {
5901
6015
  missing.add('token');
5902
6016
  }
5903
6017
  }
6018
+ else if (connection.authMethod === 'programmatic_access_token') {
6019
+ if (!connection.token && !connection.password) {
6020
+ missing.add('token');
6021
+ }
6022
+ }
6023
+ else if (connection.authMethod === 'oauth_authorization_code') {
6024
+ needs('oauthClientId');
6025
+ needs('oauthClientSecret');
6026
+ }
6027
+ else if (connection.authMethod === 'oauth_client_credentials') {
6028
+ needs('oauthClientId');
6029
+ needs('oauthClientSecret');
6030
+ needs('oauthTokenRequestUrl');
6031
+ }
6032
+ else if (connection.authMethod === 'workload_identity') {
6033
+ needs('workloadIdentityProvider');
6034
+ }
5904
6035
  else if (connection.authMethod !== 'external_browser') {
5905
6036
  needs('password');
5906
6037
  }
5907
6038
  break;
5908
- case 'bigquery':
5909
- needs('projectId');
5910
- break;
5911
6039
  case 'duckdb':
5912
6040
  needs('filepath');
5913
6041
  break;