@duckcodeailabs/dql-cli 0.10.2 → 1.0.1
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/args.d.ts +1 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +4 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{index-BXbAhaFG.js → index-BI2YwGNM.js} +72 -71
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +30 -9
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +23 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fmt.js +2 -2
- package/dist/commands/fmt.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +50 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +28 -9
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.d.ts +17 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +187 -6
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +7 -7
- package/package.json +8 -8
package/dist/local-runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
4
4
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
5
|
-
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, } from '@duckcodeailabs/dql-core';
|
|
5
|
+
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, } from '@duckcodeailabs/dql-core';
|
|
6
6
|
import { listBlockTemplates } from './block-templates.js';
|
|
7
7
|
import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
|
|
8
8
|
export async function startLocalServer(opts) {
|
|
@@ -219,7 +219,8 @@ export async function startLocalServer(opts) {
|
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
222
|
-
|
|
222
|
+
const toWrite = absPath.endsWith('.dql') ? canonicalizeSafe(content) : content;
|
|
223
|
+
writeFileSync(absPath, toWrite, 'utf-8');
|
|
223
224
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
224
225
|
res.end(serializeJSON({ ok: true }));
|
|
225
226
|
}
|
|
@@ -229,6 +230,55 @@ export async function startLocalServer(opts) {
|
|
|
229
230
|
}
|
|
230
231
|
return;
|
|
231
232
|
}
|
|
233
|
+
// ── run snapshots (v0.11) ───────────────────────────────────────────────
|
|
234
|
+
// Captures executed notebook state (query results + timings) in a
|
|
235
|
+
// sibling `.run.json` so notebooks can show last-run output without
|
|
236
|
+
// re-executing after a reload. Snapshots are git-ignored by default.
|
|
237
|
+
if (req.method === 'GET' && path === '/api/run-snapshot') {
|
|
238
|
+
const notebookPath = url.searchParams.get('path') ?? '';
|
|
239
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
240
|
+
res.end(serializeJSON(readRunSnapshot(projectRoot, notebookPath)));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (req.method === 'PUT' && path === '/api/run-snapshot') {
|
|
244
|
+
try {
|
|
245
|
+
const body = await readJSON(req);
|
|
246
|
+
if (!body.path || typeof body.path !== 'string' || !body.snapshot) {
|
|
247
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
248
|
+
res.end(serializeJSON({ error: 'Missing path or snapshot' }));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
writeRunSnapshot(projectRoot, body.path, body.snapshot);
|
|
252
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
253
|
+
res.end(serializeJSON({ ok: true }));
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
257
|
+
res.end(serializeJSON({ error: err instanceof Error ? err.message : String(err) }));
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// ── git read-only API (v0.11) ───────────────────────────────────────────
|
|
262
|
+
// GET /api/git/status — branch, clean, changed files
|
|
263
|
+
// GET /api/git/log — last N commits (?limit=20)
|
|
264
|
+
// GET /api/git/diff — unified diff for a single file (?path=relative/path)
|
|
265
|
+
if (req.method === 'GET' && path === '/api/git/status') {
|
|
266
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
267
|
+
res.end(serializeJSON(await readGitStatus(projectRoot)));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (req.method === 'GET' && path === '/api/git/log') {
|
|
271
|
+
const limit = Math.min(Number(url.searchParams.get('limit') ?? 20), 200);
|
|
272
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
273
|
+
res.end(serializeJSON(await readGitLog(projectRoot, limit)));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (req.method === 'GET' && path === '/api/git/diff') {
|
|
277
|
+
const filePath = url.searchParams.get('path') ?? '';
|
|
278
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
279
|
+
res.end(serializeJSON(await readGitDiff(projectRoot, filePath)));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
232
282
|
if (req.method === 'GET' && path === '/api/schema') {
|
|
233
283
|
try {
|
|
234
284
|
const dataFiles = scanDataFiles(projectRoot);
|
|
@@ -2659,6 +2709,17 @@ function extractBlockStudioSemanticReferences(source) {
|
|
|
2659
2709
|
segments: Array.from(segments),
|
|
2660
2710
|
};
|
|
2661
2711
|
}
|
|
2712
|
+
function canonicalizeSafe(source) {
|
|
2713
|
+
try {
|
|
2714
|
+
return canonicalize(source);
|
|
2715
|
+
}
|
|
2716
|
+
catch {
|
|
2717
|
+
// If the block body has content the parser rejects (e.g. unsupported
|
|
2718
|
+
// syntax in a user-provided template), keep the original bytes rather
|
|
2719
|
+
// than fail the write — format header gets added next time it passes fmt.
|
|
2720
|
+
return source;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2662
2723
|
export function createBlockArtifacts(projectRoot, options) {
|
|
2663
2724
|
const slug = options.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'block';
|
|
2664
2725
|
const safeDomain = (options.domain ?? '')
|
|
@@ -2675,13 +2736,13 @@ export function createBlockArtifacts(projectRoot, options) {
|
|
|
2675
2736
|
const templateContent = options.template
|
|
2676
2737
|
? listBlockTemplates().find((template) => template.id === options.template)?.content
|
|
2677
2738
|
: undefined;
|
|
2678
|
-
const fileContent = normalizeBlockStudioContent({
|
|
2739
|
+
const fileContent = canonicalizeSafe(normalizeBlockStudioContent({
|
|
2679
2740
|
name: options.name,
|
|
2680
2741
|
domain: safeDomain || 'uncategorized',
|
|
2681
2742
|
description: options.description,
|
|
2682
2743
|
tags: options.tags,
|
|
2683
2744
|
content: options.content?.trim() || templateContent,
|
|
2684
|
-
});
|
|
2745
|
+
}));
|
|
2685
2746
|
writeFileSync(blockPath, fileContent, 'utf-8');
|
|
2686
2747
|
const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
|
|
2687
2748
|
const companionPath = writeBlockCompanionFile(projectRoot, {
|
|
@@ -2712,9 +2773,9 @@ export function createSemanticBuilderBlock(projectRoot, options) {
|
|
|
2712
2773
|
if (existsSync(blockPath)) {
|
|
2713
2774
|
throw new Error('BLOCK_EXISTS');
|
|
2714
2775
|
}
|
|
2715
|
-
const content = options.blockType === 'custom'
|
|
2776
|
+
const content = canonicalizeSafe(options.blockType === 'custom'
|
|
2716
2777
|
? buildCustomSemanticBlockContent(options)
|
|
2717
|
-
: buildSemanticBlockContent(options);
|
|
2778
|
+
: buildSemanticBlockContent(options));
|
|
2718
2779
|
writeFileSync(blockPath, content, 'utf-8');
|
|
2719
2780
|
const companionPath = writeBlockCompanionFile(projectRoot, {
|
|
2720
2781
|
slug,
|
|
@@ -3083,4 +3144,124 @@ function extractVizChart(block) {
|
|
|
3083
3144
|
}
|
|
3084
3145
|
return undefined;
|
|
3085
3146
|
}
|
|
3147
|
+
async function execGit(cwd, args) {
|
|
3148
|
+
const { execFile } = await import('node:child_process');
|
|
3149
|
+
return new Promise((resolve) => {
|
|
3150
|
+
execFile('git', args, { cwd, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3151
|
+
resolve({
|
|
3152
|
+
stdout: String(stdout ?? ''),
|
|
3153
|
+
stderr: String(stderr ?? ''),
|
|
3154
|
+
code: err ? (err.code ? 1 : err.code ?? 1) : 0,
|
|
3155
|
+
});
|
|
3156
|
+
});
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
async function readGitStatus(cwd) {
|
|
3160
|
+
const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
3161
|
+
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true') {
|
|
3162
|
+
return { inRepo: false, branch: null, ahead: 0, behind: 0, changes: [] };
|
|
3163
|
+
}
|
|
3164
|
+
const branchRes = await execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3165
|
+
const branch = branchRes.code === 0 ? branchRes.stdout.trim() : null;
|
|
3166
|
+
const trackRes = await execGit(cwd, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
|
|
3167
|
+
let ahead = 0;
|
|
3168
|
+
let behind = 0;
|
|
3169
|
+
if (trackRes.code === 0) {
|
|
3170
|
+
const match = trackRes.stdout.trim().split(/\s+/);
|
|
3171
|
+
behind = Number(match[0] ?? 0);
|
|
3172
|
+
ahead = Number(match[1] ?? 0);
|
|
3173
|
+
}
|
|
3174
|
+
const statusRes = await execGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal']);
|
|
3175
|
+
const changes = [];
|
|
3176
|
+
if (statusRes.code === 0) {
|
|
3177
|
+
for (const line of statusRes.stdout.split('\n')) {
|
|
3178
|
+
if (!line)
|
|
3179
|
+
continue;
|
|
3180
|
+
const code = line.slice(0, 2);
|
|
3181
|
+
const p = line.slice(3);
|
|
3182
|
+
changes.push({ path: p, status: code });
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
return { inRepo: true, branch, ahead, behind, changes };
|
|
3186
|
+
}
|
|
3187
|
+
async function readGitLog(cwd, limit) {
|
|
3188
|
+
const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
3189
|
+
if (isRepo.code !== 0)
|
|
3190
|
+
return { inRepo: false, commits: [] };
|
|
3191
|
+
const sep = '\x1f';
|
|
3192
|
+
const end = '\x1e';
|
|
3193
|
+
const fmt = ['%H', '%an', '%ad', '%s'].join(sep) + end;
|
|
3194
|
+
const res = await execGit(cwd, ['log', `-${limit}`, `--pretty=format:${fmt}`, '--date=short']);
|
|
3195
|
+
if (res.code !== 0)
|
|
3196
|
+
return { inRepo: true, commits: [] };
|
|
3197
|
+
const commits = [];
|
|
3198
|
+
for (const entry of res.stdout.split(end)) {
|
|
3199
|
+
const trimmed = entry.replace(/^\n/, '');
|
|
3200
|
+
if (!trimmed)
|
|
3201
|
+
continue;
|
|
3202
|
+
const [hash, author, date, subject] = trimmed.split(sep);
|
|
3203
|
+
if (hash)
|
|
3204
|
+
commits.push({ hash, author, date, subject });
|
|
3205
|
+
}
|
|
3206
|
+
return { inRepo: true, commits };
|
|
3207
|
+
}
|
|
3208
|
+
function snapshotPathFor(projectRoot, notebookPath) {
|
|
3209
|
+
const abs = safeJoin(projectRoot, notebookPath);
|
|
3210
|
+
if (!abs)
|
|
3211
|
+
return null;
|
|
3212
|
+
// Strip extension and append `.run.json` so `foo.dqlnb` → `foo.run.json`
|
|
3213
|
+
// and `bar.dql` → `bar.run.json`. Keeps the sibling file next to source.
|
|
3214
|
+
const dot = abs.lastIndexOf('.');
|
|
3215
|
+
const base = dot > abs.lastIndexOf('/') ? abs.slice(0, dot) : abs;
|
|
3216
|
+
return `${base}.run.json`;
|
|
3217
|
+
}
|
|
3218
|
+
function readRunSnapshot(projectRoot, notebookPath) {
|
|
3219
|
+
const p = snapshotPathFor(projectRoot, notebookPath);
|
|
3220
|
+
if (!p || !existsSync(p))
|
|
3221
|
+
return { found: false, snapshot: null };
|
|
3222
|
+
try {
|
|
3223
|
+
const raw = readFileSync(p, 'utf-8');
|
|
3224
|
+
return { found: true, snapshot: JSON.parse(raw) };
|
|
3225
|
+
}
|
|
3226
|
+
catch {
|
|
3227
|
+
return { found: false, snapshot: null };
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
function writeRunSnapshot(projectRoot, notebookPath, snapshot) {
|
|
3231
|
+
const p = snapshotPathFor(projectRoot, notebookPath);
|
|
3232
|
+
if (!p)
|
|
3233
|
+
throw new Error('Invalid path');
|
|
3234
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
3235
|
+
writeFileSync(p, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
3236
|
+
// Append `*.run.json` to .gitignore once, so snapshots don't pollute git
|
|
3237
|
+
// history unless the user deliberately un-ignores them.
|
|
3238
|
+
ensureGitignoreEntry(projectRoot, '*.run.json');
|
|
3239
|
+
}
|
|
3240
|
+
function ensureGitignoreEntry(projectRoot, pattern) {
|
|
3241
|
+
try {
|
|
3242
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
3243
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
|
|
3244
|
+
const lines = existing.split('\n').map((l) => l.trim());
|
|
3245
|
+
if (lines.includes(pattern))
|
|
3246
|
+
return;
|
|
3247
|
+
const next = existing.endsWith('\n') || existing === ''
|
|
3248
|
+
? `${existing}${pattern}\n`
|
|
3249
|
+
: `${existing}\n${pattern}\n`;
|
|
3250
|
+
writeFileSync(gitignorePath, next, 'utf-8');
|
|
3251
|
+
}
|
|
3252
|
+
catch {
|
|
3253
|
+
// Best-effort; failure to write .gitignore shouldn't fail the snapshot.
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
async function readGitDiff(cwd, filePath) {
|
|
3257
|
+
const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
3258
|
+
if (isRepo.code !== 0)
|
|
3259
|
+
return { inRepo: false, diff: '' };
|
|
3260
|
+
if (!filePath) {
|
|
3261
|
+
const res = await execGit(cwd, ['diff', '--no-color']);
|
|
3262
|
+
return { inRepo: true, diff: res.stdout };
|
|
3263
|
+
}
|
|
3264
|
+
const res = await execGit(cwd, ['diff', '--no-color', '--', filePath]);
|
|
3265
|
+
return { inRepo: true, diff: res.stdout };
|
|
3266
|
+
}
|
|
3086
3267
|
//# sourceMappingURL=local-runtime.js.map
|