@duckcodeailabs/dql-cli 1.5.2 → 1.6.0

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.
@@ -1043,7 +1043,7 @@ export async function startLocalServer(opts) {
1043
1043
  if (req.method === 'POST' && path === '/api/blocks') {
1044
1044
  try {
1045
1045
  const body = await readJSON(req);
1046
- const { name, domain, content, description, tags, metricRefs, template, } = body;
1046
+ const { name, domain, content, description, tags, metricRefs, template, blockType, } = body;
1047
1047
  if (!name || typeof name !== 'string') {
1048
1048
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1049
1049
  res.end(serializeJSON({ error: 'Missing block name' }));
@@ -1057,6 +1057,7 @@ export async function startLocalServer(opts) {
1057
1057
  tags,
1058
1058
  metricRefs,
1059
1059
  template,
1060
+ blockType,
1060
1061
  });
1061
1062
  res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
1062
1063
  res.end(serializeJSON(created));
@@ -1637,6 +1638,18 @@ export async function startLocalServer(opts) {
1637
1638
  }
1638
1639
  return;
1639
1640
  }
1641
+ if (req.method === 'GET' && path === '/api/block-studio/dbt-status') {
1642
+ try {
1643
+ const status = buildDbtStatus(projectRoot, projectConfig, semanticLastSyncTime);
1644
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1645
+ res.end(serializeJSON(status));
1646
+ }
1647
+ catch (error) {
1648
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
1649
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
1650
+ }
1651
+ return;
1652
+ }
1640
1653
  if (req.method === 'GET' && path === '/api/block-studio/open') {
1641
1654
  try {
1642
1655
  const relativePath = url.searchParams.get('path');
@@ -4466,17 +4479,25 @@ export function createBlockArtifacts(projectRoot, options) {
4466
4479
  ? listBlockTemplates().find((template) => template.id === options.template)?.content
4467
4480
  : undefined;
4468
4481
  const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
4469
- const fileContent = canonicalizeSafe(normalizeBlockStudioContent({
4470
- name: options.name,
4471
- domain: safeDomain || 'uncategorized',
4472
- owner: options.owner,
4473
- description: options.description,
4474
- tags: options.tags,
4475
- llmContext: options.llmContext,
4476
- examples: options.examples,
4477
- invariants: options.invariants,
4478
- content: options.content?.trim() || templateContent,
4479
- }));
4482
+ const fileContent = canonicalizeSafe(options.blockType === 'semantic' && !options.content?.trim() && !templateContent
4483
+ ? buildBlankSemanticBlockContent({
4484
+ name: options.name,
4485
+ domain: safeDomain || 'uncategorized',
4486
+ owner: options.owner,
4487
+ description: options.description,
4488
+ tags: options.tags,
4489
+ })
4490
+ : normalizeBlockStudioContent({
4491
+ name: options.name,
4492
+ domain: safeDomain || 'uncategorized',
4493
+ owner: options.owner,
4494
+ description: options.description,
4495
+ tags: options.tags,
4496
+ llmContext: options.llmContext,
4497
+ examples: options.examples,
4498
+ invariants: options.invariants,
4499
+ content: options.content?.trim() || templateContent,
4500
+ }));
4480
4501
  writeFileSync(blockPath, fileContent, 'utf-8');
4481
4502
  const companionPath = writeBlockCompanionFile(projectRoot, {
4482
4503
  slug,
@@ -4777,6 +4798,25 @@ function buildBlankBlockContent(options) {
4777
4798
  lines.push('}');
4778
4799
  return lines.join('\n') + '\n';
4779
4800
  }
4801
+ function buildBlankSemanticBlockContent(options) {
4802
+ const lines = [
4803
+ `block "${escapeDqlString(options.name)}" {`,
4804
+ ` domain = "${escapeDqlString(options.domain)}"`,
4805
+ ' type = "semantic"',
4806
+ ' status = "draft"',
4807
+ ` description = "${escapeDqlString(options.description?.trim() || options.name)}"`,
4808
+ ` owner = "${escapeDqlString(options.owner?.trim() ?? '')}"`,
4809
+ ` tags = [${(options.tags ?? []).map((tag) => `"${escapeDqlString(tag)}"`).join(', ')}]`,
4810
+ ' metric = ""',
4811
+ ' dimensions = []',
4812
+ '',
4813
+ ' visualization {',
4814
+ ' chart = "table"',
4815
+ ' }',
4816
+ '}',
4817
+ ];
4818
+ return lines.join('\n') + '\n';
4819
+ }
4780
4820
  function parseYamlScalar(value) {
4781
4821
  const trimmed = value.trim();
4782
4822
  if (!trimmed)
@@ -4905,6 +4945,101 @@ function resolveDbtManifestPath(projectRoot) {
4905
4945
  const candidate = join(projectRoot, 'target', 'manifest.json');
4906
4946
  return existsSync(candidate) ? candidate : undefined;
4907
4947
  }
4948
+ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
4949
+ const configuredDbtDir = projectConfig.dbt?.projectDir
4950
+ ? resolve(projectRoot, projectConfig.dbt.projectDir)
4951
+ : undefined;
4952
+ const semanticDbtDir = projectConfig.semanticLayer?.provider === 'dbt' && projectConfig.semanticLayer.projectPath
4953
+ ? resolve(projectRoot, projectConfig.semanticLayer.projectPath)
4954
+ : undefined;
4955
+ const candidateDirs = [
4956
+ configuredDbtDir,
4957
+ semanticDbtDir,
4958
+ projectRoot,
4959
+ resolve(projectRoot, '..'),
4960
+ resolve(projectRoot, '../dbt'),
4961
+ resolve(projectRoot, '../../dbt'),
4962
+ ].filter((value) => Boolean(value));
4963
+ const dbtProjectPath = candidateDirs.find((dir, index, list) => list.indexOf(dir) === index && existsSync(join(dir, 'dbt_project.yml'))) ?? configuredDbtDir ?? semanticDbtDir ?? projectRoot;
4964
+ const configuredManifest = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
4965
+ const manifestPath = resolve(dbtProjectPath, configuredManifest);
4966
+ const catalogPath = resolve(dbtProjectPath, 'target/catalog.json');
4967
+ const semanticManifestPath = resolve(dbtProjectPath, 'target/semantic_manifest.json');
4968
+ const runResultsPath = resolve(dbtProjectPath, 'target/run_results.json');
4969
+ const manifest = readJsonFile(manifestPath);
4970
+ const semanticManifest = readJsonFile(semanticManifestPath);
4971
+ const projectName = typeof manifest?.metadata?.project_name === 'string'
4972
+ ? manifest.metadata.project_name
4973
+ : null;
4974
+ const nodes = manifest && typeof manifest === 'object' && manifest.nodes && typeof manifest.nodes === 'object'
4975
+ ? Object.values(manifest.nodes)
4976
+ : [];
4977
+ const modelCount = nodes.filter((node) => node?.resource_type === 'model').length;
4978
+ const sourceCount = manifest?.sources && typeof manifest.sources === 'object'
4979
+ ? Object.keys(manifest.sources).length
4980
+ : 0;
4981
+ const manifestMetricCount = manifest?.metrics && typeof manifest.metrics === 'object'
4982
+ ? Object.keys(manifest.metrics).length
4983
+ : 0;
4984
+ const semanticMetricCount = Array.isArray(semanticManifest?.metrics)
4985
+ ? semanticManifest.metrics.length
4986
+ : manifestMetricCount;
4987
+ const semanticModelCount = Array.isArray(semanticManifest?.semantic_models)
4988
+ ? semanticManifest.semantic_models.length
4989
+ : 0;
4990
+ const savedQueryCount = Array.isArray(semanticManifest?.saved_queries)
4991
+ ? semanticManifest.saved_queries.length
4992
+ : 0;
4993
+ const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml')) || Boolean(configuredDbtDir || semanticDbtDir);
4994
+ const manifestExists = existsSync(manifestPath);
4995
+ const semanticExists = existsSync(semanticManifestPath);
4996
+ const setupHint = !configured
4997
+ ? 'No dbt project detected. Start without dbt or run DQL from a repo with dbt_project.yml.'
4998
+ : !manifestExists
4999
+ ? 'Run `dbt parse`, `dbt compile`, or `dbt build`, then run `dql sync dbt`.'
5000
+ : !semanticExists
5001
+ ? 'dbt manifest is ready. Run `dbt parse` or `dbt build` if you use dbt Semantic Layer metrics.'
5002
+ : 'dbt artifacts are ready. Build SQL blocks from models or semantic blocks from metrics.';
5003
+ return {
5004
+ configured,
5005
+ provider: projectConfig.semanticLayer?.provider ?? null,
5006
+ projectPath: dbtProjectPath,
5007
+ projectName,
5008
+ artifacts: {
5009
+ manifest: describeArtifact(manifestPath, modelCount + sourceCount, manifest?.metadata?.generated_at),
5010
+ catalog: describeArtifact(catalogPath),
5011
+ semanticManifest: describeArtifact(semanticManifestPath, semanticMetricCount + semanticModelCount + savedQueryCount, semanticManifest?.metadata?.generated_at),
5012
+ runResults: describeArtifact(runResultsPath),
5013
+ },
5014
+ counts: {
5015
+ models: modelCount,
5016
+ sources: sourceCount,
5017
+ metrics: semanticMetricCount,
5018
+ semanticModels: semanticModelCount,
5019
+ savedQueries: savedQueryCount,
5020
+ },
5021
+ lastSyncTime,
5022
+ setupHint,
5023
+ };
5024
+ }
5025
+ function describeArtifact(path, count, generatedAt) {
5026
+ return {
5027
+ path,
5028
+ exists: existsSync(path),
5029
+ count,
5030
+ generatedAt: generatedAt ?? null,
5031
+ };
5032
+ }
5033
+ function readJsonFile(path) {
5034
+ if (!existsSync(path))
5035
+ return null;
5036
+ try {
5037
+ return JSON.parse(readFileSync(path, 'utf-8'));
5038
+ }
5039
+ catch {
5040
+ return null;
5041
+ }
5042
+ }
4908
5043
  function resolveLineageNode(graph, rawNodeId) {
4909
5044
  if (graph.getNode(rawNodeId))
4910
5045
  return graph.getNode(rawNodeId);